阅读视图

发现新文章,点击刷新页面。

组装一台家用服务器机架

最近因为新冠病毒长期宅在家里,决定开始一个很早就想做的项目:搭一个服务器机架。第一次搭经验不足,买错过几次零件,只能重新下单,前前后后花了不少时间。于是写了这篇博客介绍下用到的设备和配件,给有兴趣自己搭一个的朋友做参考。

机架

我选择的机架是 Raising 的 15U 机架。这个机架优点是够结实,深度可变,价格也比 StarTech 的便宜一点。如果预算足够可以考虑买 StarTech 的机架(12U, 25U)。这里 U 是用来衡量机架中组件高度的单位,1U 约等于 43.66 毫米。机架上每个 U 的高度都会对应的三个孔,如下图所示。

因为我有不少没法直接固定在机架上的设备,得把它们放在隔板上。我一共买了四块隔板:

此外出于美观考虑还可以买挡板,StarTech 的 1U 挡板就挺好。

网线

配线架(Patch Panel)可以让前面板的网线看起来干净清爽。它的背面连接各种设备的背部网线口,正面用短网线连接路由器和其他网线口在正面的设备。网上大多数的配线架都是一面连 CAT 5/6 网线,另一面是打线柱,很少有两面都是网线口的配线架。于是我买了两条 TRENDnet 1U 24 口空白网络配线架,加上 48 个两端都是网线口的 Keystone。这个 Keystone 从用户评论来看,以太网供电(PoE)和网速都不会受到影响。

过长的网线用在前面板也不好看,我先试了 1ft (30cm) 的 Monoprice 网线,装上以后还是觉得网线太长。后来换成了 0.5ft (15cm) 的 网线,看起来干净好多。

设备

设备选择方面,我只是把原有的监控设备和服务器搬了过来,放在了层板上。犹豫过要不要买一个机架式服务器,但是考虑到机架式服务器的耗电量,加上已经有了视频监控机和独立 NAS,最后还是买了个翻新的 HP EliteDesk i7-4785T 用来跑智能家居服务和 Unifi Controller。除了 HP 以外,Dell 和 Lenovo 都有类似的微主机,买一个二手的很适合当智能家居服务器。另外推荐使用 Intel T 系列的 CPU,专门为了省电设计。

机架上的一些其他设备:

  • StarTech 1U 条形插座:带过载保护,单一开关控制所有插口。
  • Netgear 16 口交换机 (JGS516PE):其中 8 个口支持 PoE,配合 Unifi 的无线路由。用了三年了,非常稳定。
  • Unifi 安全网关:配合服务器上跑的 Unifi Controller,可以远程配置 / 监控家庭网络。
  • Samsung SmartThings 中控:我买的第一个智能家居中控,现在主要用 Home Assistant,但是部分智能家居设备还是通过 SmartThings 中控 + MQTT 的方式连接刀 Home Assistant 上。
  • Mac mini (2010 年中):曾经的智能家居中控,买了 EliteDesk 后 Mac mini 处于闲置状态。
  • LaView 监控录像机:买了一整套监控录像机 + 摄像头。它的风扇噪音有点大,是目前机架的主要噪音来源。
  • Qnap TS-853 NAS:最多可以放八个硬盘。数据备份中心,同时跑了 Owncloud 同步服务和 Plex 视频播放。如果没买这个 NAS 的话我可能会直接上机架式服务器。
  • Arlo 视频监控机:另外一套视频监控设备。Arlo 的好处是可以选择用电池不连电源。

具体的智能家居和监控的使用这篇文章就不细谈了,对它们感兴趣的朋友,可以参考我的另外两篇博文:

DIY 洗衣机完成通知

家里用的是三星的洗衣机和烘干机,买的时候为了完成时有提醒特地挑了带有「智能监控」功能的机型。然而对应的三星洗衣机的 app 几乎没法用,推送和状态更新都有问题。于是我打算自己实现一个类似的功能。

因为洗衣机和烘干机用电量比较大,通过监控用电量很容易判断出设备当前的运行状态,于是我决定用智能插座来实现这个功能。现在很多智能插座都有电量检测的功能,选择的时候要注意插座支持的最大电量,我用了 TP-Link 的 HS110,支持最多 1500 瓦的设备,对于一般的洗衣机和烘干机来说绰绰有余。和智能插座对接的系统依然是 Home Assistant,它对 HS110 的支持很好,可以方便地读出设备当前用电量。

接下来是 Home Assistant 的配置部分,主要分状态定义和自动化脚本。

为了简化我只用两种状态(空闲和运转)描述洗衣机和烘干机的状态(代码只给出了洗衣机部分,烘干机部分几乎一样):

input_select:
  washer_status:
    name: Washer Status
    options:
      - Idle
      - Running
    initial: Idle

为什么要定义状态而不是直接通过用电量判断呢?因为这些机器运转过程里会有几次几乎不用电的阶段,如果只通过用电量很容易产生错误信号,用电量配合状态持续时间才能做出更精准的判断。

接下来定义虚拟的洗衣机传感器,我们会通过自动化脚本更新这个传感器的值:

sensor:
  - platform: template
    sensors:
      washer_status:
        value_template: '{{ states.input_select.washer_status.state}}'
        friendly_name: 'Washer Status'

自动化脚本分两块,一块是检测到电量后的更新洗衣机状态为运转。这里我用了 10 瓦作为运转开始的阈值。

automation:
  - alias: Set washer active when power detected
    trigger:
    - platform: numeric_state
      entity_id: switch.hs110_washer
      value_template: '{{ state.attributes.current_power_w }}'
      above: 10
    condition:
      condition: or
      conditions:
        - condition: state
          entity_id: sensor.washer_status
          state: Idle
    action:
    - service: input_select.select_option
      data:
        entity_id: input_select.washer_status
        option: Running

另一块是设备停止运转的检测,根据不同的设备可能要进行微调。我这里设置了洗衣机用电低于 3 瓦且超过 1 分钟以上后,把状态切换成空置并通过 Twilio 发送短信通知。

automation:
  - alias: Set washer inactive
    trigger:
    - platform: numeric_state
      entity_id: switch.hs110_washer
      value_template: '{{ state.attributes.current_power_w }}'
      below: 3
      for:
        minutes: 1
    condition:
      condition: or
      conditions:
        - condition: state
          entity_id: sensor.washer_status
          state: Running
    action:
    - service: input_select.select_option
      data:
        entity_id: input_select.washer_status
        option: Idle
    - service: notify.twilio_sms
      data:
        message: Washer finished at {{ now() }}
        target:
          - 15555555555

这个脚本用了一个多月没出现误报,虽然一开始折腾一点,最后还是省了我们不少时间。

关于智能家居和 Home Assistant 的更多信息,可以参考我写的其他文章

3D 目标检测之鸟瞰图检测 (Pixor / HDNet)

随着无人驾驶的兴起,激光雷达数据的目标检测成为了这几年的研究热点之一。常见的 3D 检测模型可以分成两类,一类是用 3D 小方格代表所有的激光点数据,每个小方格包含了这个方格内的特征,例如 VoxelNetVote3deep 。另一类则是先把三维信息投射到一个二维平面,通常是鸟瞰图 (BEV, Bird Eye View) 或是正视图 (Range View),生成二维特征图后再用传统的二维目标模型检测图中的物体,例如 MV3DFaF

无人驾驶场景中的大多数的目标物体都处在同一地面上,非常适合 BEV。相比正视图,BEV 中的目标在不同位置大小固定,我们可以用已知的常见物体大小优化检测效果。对于 BEV 模型来说,主要的问题在于如何选择最终图像的特征,如何生成的图像中做出精确的检测。此文旨在介绍 BEV 检测相关的模型 Pixor 和 HDNet。

Pixor

Pixor 是 Uber ATG 的 Bin Yang 等人在 CVPR 18 上提出的 BEV 检测模型。在特征生成方面,Pixor 把整个点云切成了 L x W x H 个小方格,每个小方格用 0 或 1 表示这个方格内是否有激光点存在,接着沿高度方向把三维特征压缩到一个二维平面,每个压缩后的二维方格就有了 H 个特征表示这个方格上的不同高度是否有点存在。此外 Pixor 还计算了落在每个方格中激光点的平均强度作为额外的特征,最终拿到了一个分辨率为 L x W、包含 H + 1 个特征通道的图像。

Pixor 结构
Pixor 结构

检测方面,Pixor 采用了基于 RetinaNet 的 one-stage 的结构,如上图所示(这里用了海报上的截图,和论文中的有一些细小的差别)。网络里面用了 ResNet 和类似 FPN 的结构。FPN 结构输出的特征图经过一个头部网络后,直接生成对每个像素点的分类和 bounding box 的回归结果。

优化目标方面,分类目标用了 RetinaNet 中的 focal loss,回归目标用了 smoooth L1。论文中回归目标有笔误,按照海报和 FAQ 的说法应该是 \(\{ \cos 2\theta, \sin 2\theta, dx, dy, \log W, \log L \}\)。这里巧妙的用了 \(2 \theta\) 的三角函数作为车头朝向的回归目标,因为车辆是往前还是往后开这个问题会在 tracker 中处理,在检测的时候只要知道车身线的偏离角度就行了,至于这个角度是 5° 还是 185° 影响不大。

整个网络的运行速度非常快,在 Titan XP 上可以在 35 毫秒内完成。除此之外论文还测试了不同 backbone 网络和头部网络特征层共享的不同选择,做了模型简化测试 (ablation study),感兴趣的朋友可以细读。

HDNet

Pixor 沿高度方向把整个点云切成不同的高度区间,例如 Kitti 数据集上 -2.5 到 1 米之间的点会被分成 35 片,这个区间之外的点会被忽略。当地面坡度较大时,远处的点很容易被排除在外。为了解决这个问题,Bin Yang 等人提出了把高精地图作为额外特征的方案,发表在 CoRL 18 上。

HDNet 中的地图信息
HDNet 中的地图信息

如图所示,HDNet 的核心思路在于引入了高精地图信息,从而可以把地面「摊平」(图 b),并提供道路语义信息帮助检测(图 d)。网络结构和 Pixor 非常相似,采用了 FPN,并把 Pixor 中的反卷积层换成了更高效的双线性插值层。

HDNet 结构
HDNet 结构

为了提高模型的稳定性,确保在缺失地图信息或者地图不准确的情况下仍然有较好的检测结果,作者会在训练模型时随机去除地图信息,并提出了通过 Lidar 扫描结果在线生成地图的方案。

小结

本文介绍了两篇近期的基于纯激光雷达数据的 BEV 检测论文。基于纯激光雷达的模型非常适合车辆的检测,但对于行人检测的精度并不理想,往往需要借助图像提供额外的信息。在接下来的博客中我会和大家一起分享更多的相关研究,同时也欢迎各位提出不同的意见。

最后做一个小广告,我目前在 Ike 做无人卡车视觉的相关工作。我们公司在旧金山,最近拿到了 5200 万 A 轮融资,正在招人中。如果你对无人卡车感兴趣,欢迎联系我。

绿卡终于批了

来美国五年后绿卡终于批了,没有太大曲折,不过也干等了好久,中间还找了议员催绿。总结下流程,希望给后来的朋友有帮助。

时间线

  • 2013 年 10 月入职,2014 年初提交 I-140 (EB-2),排期 (PD) 在 2014 年 3 月。
  • 2015 年中换了工作,重新提交 PERM,排期不变。
  • 2017 年 5 月 EB-3 排期赶超了 EB-2,我的 PD 在 EB-3 下已经 current。于是提交了 EB-3 I-140 的申请,同时提交了 I-485 / I-765 / I-131。I-485 回执日期 (RD) 在 2017 年 5 月。
  • 2017 年 7 月中收到 EAD / AP 卡。
  • 2018 年 3 月中 EB-3 I-140 审核通过。
  • 2018 年 4 月收到 case 转移的通知。
  • 2018 年 6 月初面试,面试后被通知体检过期,重新办了以后月底寄出。
  • 2018 年 7 月 EB-3 的排期倒退,我的 PD 不再 current,由于当时 EB-2 还是 current 的,就让律师 interfile EB-2。
  • 2018 年 8 月初联系议员催绿,月中得到消息说我的 case 还在审核中,最起码要等 45 天。
  • 2018 年 9 月底再次联系议员,三天后得到消息说 USCIS 还在解决我 case 中的一些问题。
  • 2018 年 10 月底查询网站更新状态为「New Card Is Being Produced」,两天后变成「Case Was Approved」,又过了两天变成了「Card Was Mailed To Me」。

整个过程中 EB-3 的 I-140 拖了好久,因为律师说加急需要 9089 原件,而我的原件已经在第一份 I-140 申请中提交了,没法再次加急。另外比较后悔的一件事就是绿卡面试前没有再去准备一份体检卡。面试通知上写着体检要求「valid within last year」,我以为只要是前一年办过体检就行,律师也建议面试完了再看要不要补办。等重新做体检提交,第二个月正好排期倒退了。

另外一个经验就是即使绿卡面试完了,也得催律师重新申请 EAD / AP。我的律师觉得绿卡马上拿到了就没急着帮我重新申 AP,导致我在绿卡迟迟没下来加上 Visa 过期的情况下,年底出游计划受限不少,如果有一张 AP 就可以放心安排出国计划了。

议员催绿

催绿方面,我们研究过参议员和众议员两个方案。两者效果应该差不多,都是通过官方问一下进度,以免你的 case 堆积在某个角落没人管。我们只联系了众议员,具体的流程如下:

  1. https://www.house.gov/htbin/findrep 上输入你的区号找到对应区的议员,可能会要求输入具体地址进一步筛选。
  2. 在议员的主页上找到电话联系方式,负责我的区域的是议员 Jackie,她的联系方式在这个页面
  3. 接下来的步骤不同议员可能会有所区别,具体可以看主页上的说明。对于我所在区的议员,打电话后选移民相关问题,会转接给负责的工作人员。
  4. 工作人员要了我的邮箱地址后给我寄了一份隐私授权书,因为他们向 USCIS 查询我的情况时可能会涉及我的隐私,填完表格后扫描回复即可。一般等几天才会收到 USCIS 的答复。

整个过程没等多久,而且工作人员态度很好,之后发邮件问都是一刻钟内会有回复。

后记

拿到绿卡后似乎还要去更新下 SSN,去掉上面的工作限制。另外就是可以办 Global Entry 啦。

祝愿等绿卡的各位早日顺利拿到绿卡。

SLAM 公开课笔记 4:定位

最后一周讲定位 (localization),也就是 SLAM 里面的 L。主要包括粒子滤波和迭代最近点。

里程计定位 (Odometry)

里程计定位法直接从设备里读取信息,更新当前的位置状态。例如要跟踪车子的位置状态,可以在车轮上安装计数器记录车轮转动的次数,从而了解车子前进了多少。对于转弯的情况,可以从内外轮子的计数差得到转弯的角度。假设内轮转了 \(e_i\) 圈,外轮转了 \(e_o\) 圈,内轮半径 \(r_i\),外轮半径 \(r_o\),则有 \[ e_i = \theta r_i \\ e_o = \theta r_o \] 这里 \(\theta\) 就是车子转动的角度,上面的方程可以解得 \[ \theta = \frac{e_o - e_i}{r_o - r_i} \] 然后就能根据转动角度更新车辆当前的位置了。

这种方法用车辆本身的坐标系统记录位置,虽然实现起来简单但是测量精度非常局限于测量误差。例如上例中轮子打滑或事漂移都会导致结果不准确。更成熟的方案需要结合地图信息。

地图定位 (Map Registration)

可以用下面三张图来来介绍地图定位问题,左图是当前的区域地图,中间是激光测距传感器得到的结果,地图定位的目的就是根据传感器返回的结果判断机器人当前坐标和朝向,右图就是一个理想的结果。

配合第三周讲的占据栅格地图障碍物的概率 \(m(x, y)\),我们可以定义最佳定位结果应该满足 \[ \max_p \sum_r \delta(p_x + r\cos(p_{\theta} + r_{\theta}), p_y + r\sin(p_{\theta} + r_{\theta})) \cdot{m(x,y)} \] 接下来介绍两种解决地图定位的方法。

粒子滤波 (Particle Filters)

粒子滤波(有没有觉得这个术语的中文翻译很科幻)又称为序列蒙特卡洛 (Sequential Monte Carlo),是一种非参数 (non-parametric) 模型。它用一系列样本(粒子)表示可能性分布。每一个粒子都是方差接近于 0 的高斯分布,这种分布又被称为狄拉克δ函数 (Dirac Delta),采用这个分布好处之一是可以把连续数学中的工具应用到离散的结果中。

在地图定位问题中,粒子的状态代表位置和朝向,同时每个粒子分别有自己的权重。初始状态的粒子滤波分布由一开始的假设决定,可能是均匀分布,也可能是以某个点为中心的高斯分布。初始化完成后,重复以下步骤更新粒子的分布:

  1. 根据里程计的信息更新粒子的状态。
  2. 由于里程计本事有不确定,更新每个粒子状态的时候可以加入里程计带入的噪音,通常也是一个高斯分布。
  3. 从每个粒子更新后带噪音的分布中采样。
  4. 根据距离传感器得到的信息,更新每个粒子的权重(例如由传感器得知前方近距离有障碍物,而某个粒子并没有,那么这个粒子的权重都会大幅降低)。
  5. 这样得到的结果可能会有有效粒子过少的问题,需要重新采样。有效粒子的数量可以用 \(n_{effective} = \frac{(\sum_i w_i)^2}{\sum_i w_i^2}\) 表示。
  6. 重新采样的方法就是根据权重重新从粒子集中独立抽取同样数量的粒子,注意这里高权重的粒子可能会被多次抽取。重复抽取的粒子会在下一轮更新时因为里程计噪音再次分散。
  7. 重复步骤,直到粒子集中在某一状态。

迭代最近点 (Iterative Closest Point)

粒子滤波的缺点之一在于高维度中,需要大量的粒子才能保证理想的分布。ICP 算法可以更好的适应高维度的场景。

如图所示,ICP 的目的在于已知测量结果和地图,要求出左侧测量结果如何旋转移动后,可以和右侧的地图局部吻合。这里主要有两个问题,一是旋转和移动矩阵,二是测量点和地图点的对应关系。具体的 ICP 算法可以参考这篇论文,它的大致思路和第一周提到的解高斯混合模型的 EM 算法很接近:

  1. 初始化旋转 (R) 和移动 (t) 矩阵。
  2. 固定 R 和 t,优化点的对应关系:\(y_i = \argmin_{y_j \in Y} \Vert x_i - y_j \Vert\)
  3. 固定点的对应关系,优化 R 和 t:\(R, t = \argmin \sum_{x_i, y_i \in C} \Vert d(x_i, y_i) \Vert ^2\)
  4. 重复 2 和 3,直到稳定。

作业

这次的作业是实现一个粒子滤波,应该是整个课程最麻烦的一次了。问题本身不算难,尤其简化了动力学模型之后。但是调参比较麻烦,尤其是最后提交的那个地图,容易卡在某个转角跑不出来。

动力学模型方面,我一开始假设机器人位置和朝向的变化量和前一次接近,移动一次粒子之后再添加噪音。这个方法在机器人变向的时候容易位置偏离过多导致粒子失效,后来注意到作业的 Tips 里面建议假设机器人不动,利用高斯噪音移动粒子,这样的话相当于就不需要针对动力学模型做任何操作了。

计算每个例子的得分时,一种做法是用上一周的作业中用到的 bresenham 函数计算发出点到障碍物中间所经历的空白格,然后对每个空白格计算加权,但是这个做法实在太慢了,我最后计算得分时只用了障碍物所在格的状态。

关于粒子的数量,我看到网上有一种做法是设置一个得分的阈值(例如 70% 的障碍物要正好打到墙上),如果所有的粒子都没法超过这个阈值,就重新再跑一次。这么做容易在测量不准时卡死,而且和书中的实现有出入,在现实中会导致粒子滤波每次估计位置时使用的时间不稳定。比较好的做法还是把粒子的数量和高斯分布的参数放在一起调,跑偏了可以看看是因为粒子不够还是分布参数有问题。

作业里其实有两个地图,样例地图比较容易过(可能是参数变化不大)。提交用的地图调参要不少时间,我看了自己的输出以后,发现有几个点的运动变化很大,有可能是这几个点的测量本身也不精确。另外两个地图用来标记墙和空白方格的数值也是不一样的。

小结

至此 Robotics Estimation and Learning 这门课就上完了。课程设置不错,但是讲解不够详细,尤其是第二周和第四周,这小哥讲得太简单了,slides 上还有不少错误。推荐《Probabilistic Robotics》这本书,在上课的时候参考了里面一部分章节,写得很详细,作者 Thrun 也是机器人领域的大神。

全课程的笔记链接

SLAM 公开课笔记 3:地图

这一周的内容和地图有关,最后的作业就是通过传感器的数据创建一个地图。

地图类型

常见的题目类型有三种:

  1. 度量地图 (Metric Map):地点用坐标表示,例如用经纬度表示地点的世界地图。
  2. 拓扑地图 (Topological Map):表示地点之间的逻辑关系,例如图论中图的概念,以及地铁图。
  3. 语义地图 (Semantic Map):用标签描述的地图,通过位置关系描述被标记的地点,例如景点游览图。

在现实中绘制地图有几个难点,一是测量误差会导致坐标不精确,二是设备本身需要不断地移动才能绘制,三是地图本身是对现实世界的反应,会不断的变化。

占据栅格地图 (Occupancy Grid Map)

栅格地图用二维栅格表示整个环境,每个栅格都有一个概率值表示这个栅格是否有物体存在。绘制栅格地图常用的设备之一是测距传感器,通过发出激光并测量接受反射所用的时间,传感器可以了解前方障碍物的大致距离。

测距传感器
测距传感器

如图所示,传感器本身测量存在误差,我们只能认为在传感器正前方给定距离处有一定几率存在障碍物。对于这种测量有误差的环境,我们再次引入贝叶斯模型描述。记 \(p(m_{x, y})\) 为 (x, y) 格中存在障碍物的概率的先验知识,根据测量的设备模型我们可以得到条件概率 \(p(z | m_{x,y})\)\(p(z=1|m_{x,y}=1)\) 就代表栅格中有障碍物,且检测成功的概率,而 \(p(z=1|m_{x,y}=0)\) 则代表栅格中不存在障碍物,但是却检测到障碍的概率(假阳性)。

根据贝叶斯公式,我们可以得到测量之后的后验概率 \[ p(m_{x,y}|z) = \frac{p(z | m_{x,y}) p(m_{x,y})}{p(z)} \] 为了简化计算,引入 \(Odd(X) = \frac{p(X)}{p(\neg{X})}\) ,则有 \[ Odd(m_{x,y} = 1 | z) = \frac{p(m_{x,y} = 1 | z)}{p(m_{x,y} = 0 | z)} = \frac{p(z | m_{x,y} = 1) p(m_{x,y} = 1)}{p(z | m_{x,y} = 0) p(m_{x,y} = 0)} \] 其中最后一步可以代入贝叶斯公式得到。对 \(Odd\) 求对数,则有 \[ \begin{align} \log \frac{p(m_{x,y} = 1 | z)}{p(m_{x,y} = 0 | z)} &= \log \frac{p(z | m_{x,y} = 1) p(m_{x,y} = 1)}{p(z | m_{x,y} = 0) p(m_{x,y} = 0)} \\ &= \log \frac{p(z | m_{x,y} = 1)}{p(z | m_{x,y} = 0)} + \log \frac{p(m_{x,y} = 1)}{p(m_{x,y} = 0)} \end{align} \]\(\log{odd\ meas}\) 表示 \(\log \frac{p(z | m_{x,y} = 1)}{p(z | m_{x,y} = 0)}\) ,上式可以简化为 \[ \log odd(m_{x,y} = 1 | z) = \log{odd\ meas} + \log odd(m_{x,y} = 1) \] 如果用 \(\log{odd}\) 表示栅格的状态,这一状态可以简单的通过加减来维护。当检测到障碍物时,该栅格的 \(\log{odd}\) 增加 \(\frac{p(z=1 | m_{x,y} = 1)}{p(z=1 | m_{x,y} = 0)}\),没有检测到障碍物时,该栅格的 \(\log{odd}\) 减少 \(\frac{p(z=0 | m_{x,y} = 0)}{p(z=0 | m_{x,y} = 1)}\)。初始状态为 0,即 \(odd = 1\)

下图为一个具体的例子,第一个栅格地图为 t1 时刻的状态,做了一次探测后,经过的空闲栅格的 \(\log{odd}\) 减少 0.7(这个值由传感器决定),而最后的障碍物所在格的 \(\log{odd}\) 增加了 0.9。第三个栅格地图为 t2 时刻更新后的状态。

log odd 更新例子
log odd 更新例子

三维地图

常见的三维传感器:

  • 三维测距传感器 (如 Lidar)
  • 双目相机 (Stereo Camera)
  • 深度相机 (Depth Camera)

地图的数据格式也有多个方案:

  • 栅格表示:查询单个栅格的状态非常快,但是占用大量内存,而且因为离散化一部分信息会丢失。
  • 列表表示:节省了内存,也不会因为离散化丢失信息,但是查询时需要线性扫描。
  • 树状表示:查询比列表快 (O(logN)),同时内存占用不大。
  • k-d tree:每次选择一条坐标轴,把空间对半分。查询和维护都能在 O(log n) 内完成。
  • octree:把空间均等分成八块,在存在点的分块中继续递归细分。

作业

这次的作业是根据一组 Lidar 测量结果绘制一个二维地图。输入包含四个参数(这里 K 为扫描总次数,N 为 Lidar 光线数):

  1. t 数组记录每组数据的时间点,大小为 \(1 \times K\)
  2. ranges 数组记录每次测试各条激光线测得的距离,大小为 \(N \times K\)
  3. scanAngels 数组记录每条激光线相对机身的转角,大小为 \(N \times 1\),每次测试时这个转角不会变化。
  4. pose 数组记录每次测试时机器人在地图中的位置,大小为 \(3 \times K\),三行数据分别是坐标 x y 和相对地图的转角。

我实现的方法没有用向量并行,用了两层循环依次处理每次测试的各条光线。在 i7 6700K 上大概 25 秒可以跑完最终测试,测试程序里会提到整个过程可能要五分钟,性能差一点的机器这个时间里面应该也能跑完了。

最后跑地图的时候,注意 example_test.m 里传给 occGridMapping 的参数只取了前 1000 次测量结果,所以要制作完整地图要把这个限制去掉。另外文档里面坐标转换时用了 ceil,我在本地测试时只能拿到 27/30 分,换成 round 就能到满分了。

最终生成的地图
最终生成的地图

全课程的笔记链接

SLAM 公开课笔记 2:卡尔曼滤波

这一周主要讲卡尔曼滤波 (Kalman Filter),视频讲得比较简略,slides 做得里也有不少错误。最后看了一些其他网站的文章和视频才有了比较深刻的理解。参考资料推荐在本文结尾。

卡尔曼滤波 KF

卡尔曼滤波可以从一系列包含噪音的观测数据中,估计出每个时间点系统的状态。KF 有几个基本假设:

  1. 当前状态只和前一状态有关,且和前一状态线性相关,即 \(x_{t+1}=A_t x_{t} + B_t u_t + \epsilon_t\),这里 \(x_t\) 是状态向量,\(u_t\) 为控制向量,\(\epsilon_t\) 是均值为 0 的高斯分布噪音。
  2. 测量结果和状态线性相关:\(z_t = C_t x_t + \delta_t\)。这里 \(z_t\) 是测量结果的向量,\(\delta_t\) 表示测量噪音。
  3. 最初状态也呈正态分布。

基于高斯分布的假设,我们可以用贝叶斯模型描述状态 \[ p(x_{t+1}|x_t) = Ap(x_t) \\ p(z_t|x_t) = Cp(x_t) \]

加入运动和观测误差导致的不确定性 \(v_m\)\(v_0\) \[ p(x_{t+1}|x_t) = Ap(x_t)+v_m \\ p(z_t|x_t) = Cp(x_t)+v_0 \]

假设误差基于高斯分布 \[ p(x_{t+1}|x_t) = A\mathcal{N}(x_t, P_t) + \mathcal{N}(0, \Sigma_m) \\ p(z_t|x_t) = C\mathcal{N}(x_t, P_t) + \mathcal{N}(0, \Sigma_0) \]

把线性变换 A 和 C 代入正态分布 \[ p(x_{t+1}|x_t) = \mathcal{N}(Ax_t, A P_t A^T) + \mathcal{N}(0, \Sigma_m) \\ p(z_t|x_t) = \mathcal{N}(Cx_t, C P_t C^T) + \mathcal{N}(0, \Sigma_0) \]

线性加和 \[ p(x_{t+1}|x_t) = \mathcal{N}(Ax_t, A P_t A^T+ \Sigma_m) \\ p(z_t|x_t) = \mathcal{N}(Cx_t, C P_t C^T + \Sigma_0) \]

接下来讲如何估计这两个高斯分布的参数。

最大后验概率估计卡尔曼滤波

最大后验概率估计的目的在于最大化后验概率 \(p(x_t | z_t)\),即在对 \(x_t\) 有一个预测(先验),同时获得了观测结果 \(z_t\) 之后修正 \(x_t\) 的值,用符号表示有 \(\hat{x_t} = \argmax_{x_t} p(x_t | z_t)\)

由贝叶斯公式 \[ p(x_t | z_t) = \frac{p(z_t | x_t) p(x_t)}{P (z_t)} \] 代入正态分布得 \[ \hat{x_t} = \argmax_{x_t} \mathcal{N}(Cx_t, \Sigma_0) \mathcal{N}(Ax_t, A P_t A^T+ \Sigma_m) \]\[ \begin{align} P &= A P_{t-1} A^T + \Sigma_m \\ R &= \Sigma_0 \end{align} \] 代入多元高斯分布(参考第一周笔记)把两个分布合并后求导,或者利用边界条件概率公式,可解得 \[ \begin{align} \hat{x_t} &= (P^{-1} + C^T R^{-1}C)^{-1} (C^T R^{-1} Z_t + P^{-1} A x_{t-1}) \\ \hat{P_t} &= (P^{-1} + C^T R^{-1}C)^{-1} \end{align} \]

接下来根据 Woodbury 矩阵求逆式 (Woodbury Matrix Identity),可以得到 \[ \hat{P_t} = P - P C^T (R^{-1} + C P C^T)^{-1} C P \]\(K = P C^T (R + C P C^T)^{-1}\),有 \(\hat{P_t} = P - KCP\)\(\hat{x_t}\) 简化过程如下 \[ \begin{align} \hat{x_t} &= (P^{-1} + C^T R^{-1}C)^{-1} (C^T R^{-1} z_t + P^{-1} A x_{t-1}) \\ &= (P - KCP) (C^T R^{-1} z_t + P^{-1} A x_{t-1}) \\ &= A x_{t-1} + P C^T R^{-1} z_t - K C A x_{t-1} - KCP C^T R^{-1} z_t \\ &= A x_{t-1} - K C A x_{t-1} + (P C^T R^{-1} - KCP C^T R^{-1}) z_t \\ &= A x_{t-1} - K C A x_{t-1} + K z_t \end{align} \]

这里 K 就是卡尔曼增益 (Kalman gain)。

扩展卡尔曼滤波 (Extended Kalman Filter)

KF 的局限之一在于假设了线性模型,EKF 去掉了线性模型的限制,可以处理更一般的状态变化函数 \[ \begin{align} x_{t+1} = A x_t + B u_t &=> x_{t+1} = f(x_t, u_k) \\ z_t = C x_t &=> z_t = h(x_t) \end{align} \]

于是协方差的预测变成了 \[ p(x_{t+1}|x_t) = \mathcal{N}(f(x_t), \frac{\partial{f}}{\partial{x}} P_t \frac{\partial{f^T}}{\partial{x}} + \Sigma_m) \] 卡尔曼增益为 \[ K = P \frac{\partial{h^T}}{\partial{x}} (\frac{\partial{h}}{\partial{x}} P_t \frac{\partial{h^T}}{\partial{x}})^{-1} \] 更新方程为 \[ \begin{align} \hat{x_t} &= f(x_{t-1}) + K(z_t - h(f(x_t))) \\ \hat{P_t} &= P - K \frac{\partial{h}}{\partial{x}} P \end{align} \]

无迹卡尔曼滤波 (Unscented Kalman Filter)

EKF 的局限之一在于只是对非线性的变换做了近似,用泰勒级数展开后取一阶项,容易产生导致较大的误差。UKF 采用了确定性的取样方法来近似高斯分布,这个取样方法又被称为无迹变换 (Unscented Transformation)。

UKF 取样
UKF 取样

Unscented Transformation

UT 可以用来计算非线形变换后随机变量的分布情况。考虑 L 维随机变量 x 和非线性的函数 \(\pmb{y} = f(\pmb{x})​\),假设 x 的均值为 \(\bar{\pmb{x}}​\),协方差矩阵 \(P_x​\)。为了计算 \(\pmb{y}​\) 的分布情况,我们根据下面三个式子创造一个维数为 2L + 1 的 sigma 矩阵 \(\pmb{\mathcal{X}}​\)\[ \begin{align} \mathcal{X}_0 &= \bar{\pmb{x}} \\ \mathcal{X}_i &= \bar{\pmb{x}} + (\sqrt{(L + \lambda)\pmb{P}_x})_i && i = 1, \dots, L \\ \mathcal{X}_i &= \bar{\pmb{x}} + (\sqrt{(L + \lambda)\pmb{P}_x})_{i - L} && i = L + 1, \dots, 2L \end{align} \] 这里 \(\lambda = \alpha^2(L + \kappa) - L\) 为调节参数,\(\alpha\) 决定采样点围绕均值的扩散程度等参数。下标 i 表示矩阵的第 i 列。这里的 \(\mathcal{X}_i\) 也被称为 sigma 向量,通过非线性函数后转变到同一个矩阵中: \[ \mathcal{Y}_i = f(\mathcal{X}_i) \]

\(\pmb{y}\) 的均值和协方差就可以通过对 \(\mathcal{Y}\) 矩阵列的加权求和获得了 \[ \begin{align} \pmb{y} &\approx \sum_{i=0}^{2L} W_i^{(m)} \mathcal{Y}_i \\ \pmb{P}_y &\approx \sum_{i=0}^{2L} W_i^{(c)} \{\mathcal{Y}_i - \bar{\pmb{y}}\}\{\mathcal{Y}_i - \bar{\pmb{y}}\}^T \end{align} \] 其中权值 \(W_i\) 的定义为 \[ \begin{align} W_0^{(m)} &= \lambda / (L + \lambda) \\ W_0^{(c)} &= \lambda / (L + \lambda) + (1 - \alpha^2 + \beta) \\ W_0^{(c)} &= W_0^{(m)} = 1 / \{ 2(L + \lambda) \} & i = 1, \dots, 2L. \end{align} \] 下图很好地解释了上述几个式子的变换过程

UT 的图形解释
UT 的图形解释

Unscented Kalman Filter

接下来回到 UKF。介绍了 UT 之后 UKF 就容易多了。首先构造一个包含初始状态和噪音的矩阵 \(\pmb{x}_k^{\alpha} = [\pmb{x}_k^T \pmb{v}_k^T \pmb{n}_k^T ]^T\),这里三个向量分别为状态向量、控制向量和噪音向量。接下来对这个矩阵应用无迹变换获得 sigma 矩阵 \(\mathcal{X}_k^{\alpha}\),之后就回到了普通 KF 过程。具体的状态转移公式可以参考本文结尾参考资料《Kalman Filtering and Neural Networks》一书中的章节。

作业

这周的作业是用卡尔曼滤波预测一个小球运动 10 帧之后的位置。理解了卡尔曼滤波后做起来不难,定义好动力学矩阵 A 和测量矩阵 C,接着根据上面的公式计算卡尔曼增益 K、新状态的均值和协方差矩阵,再通过新状态的位置和速度计算 10 帧之后的位置就好。

调参方面有一些小技巧,一开始测试的时候可以先给 \(\Sigma_m\) 设一个很大的值,同时给 \(\Sigma_0\) 设一个很小的值,这样每次算出来的位置都应该是测量出来的位置,否则代码里可能有 bug。接下来就可以根据生成的预测图来调整 \(\Sigma_m\),肉眼估一下坐标的误差范围(注意到 y 的变化速度比 x 的慢很多),然后根据坐标误差算一下速度误差。测量误差根据文档可以给一个 0.01 - 0.1 的参数。

参考资料

全课程的笔记链接

SLAM 公开课笔记 1:高斯分布

最近宾大在 Coursera 上开了一个机器人系列课程,包含了视觉、运动规划、机械设计等课题。我对 SLAM 很感兴趣,于是就选了 Robotics Estimation and Learning 这门课,课程主页是https://www.coursera.org/learn/robotics-learning/。第一周的内容是高斯分布。

一元高斯分布

给定数据集 \(\{x_i\}\),可以通过最大似然 (Maximum Likelihood Estimate) 来估计均值 \(\mu\) 和标准差 \(\sigma\) \[ \hat{\mu}, \hat{\sigma}=\argmax_{\mu, \sigma}{p(\{x_i\}|\mu, \sigma)} \]

假设所有观测数据独立分布,则有

\[ p(\{x_i\}|\mu, \sigma) = \prod_{i=1}^N p(x_i|\mu, \sigma) \]

解这个优化函数:

\[ \begin{align} \hat{\mu}, \hat{\sigma} &= \argmax_{\mu, \sigma} \prod_{i=1}^N p(x_i|\mu, \sigma) \\ &= \argmax_{\mu, \sigma} \prod_{i=1}^N \ln p(x_i|\mu, \sigma) \\ &= \argmax_{\mu, \sigma} \prod_{i=1}^N \ln (\frac{1}{\sqrt{2\pi}\sigma} \exp(-\frac{(x_i-\mu)^2}{2\sigma^2})) \end{align} \]

设损失函数 \(J(\mu, \sigma) =\sum_{i=1}^N (\frac{(x_i-\mu)^2}{2\sigma^2} + \ln{\sigma})\),则有 \(\hat{\mu}, \hat{\sigma} = \argmin_{\mu, \sigma} J(\mu, \sigma)\)

\[ \begin{align} \frac{\partial{J}}{\partial{\mu}} &=\frac{\partial{}}{\partial{\mu}}\sum_{i=1}^N (\frac{(x_i-\mu)^2}{2\sigma^2} + \ln{\sigma}) =\sum_{i=1}^N (\frac{\partial{}}{\partial{\mu}}\frac{(x_i-\mu)^2}{2\sigma^2}) \\ \frac{\partial{J}}{\partial{\sigma}} &=\frac{\partial{}}{\partial{\sigma}}\sum_{i=1}^N (\frac{(x_i-\mu)^2}{2\sigma^2} + \ln{\sigma}) =(\frac{\partial}{\partial{\sigma}} \frac{1}{2\sigma^2}) (\sum_{i=1}^N (x_i-\mu)^2) + \frac{N}{\sigma}) \end{align} \]

求极值令两式都为 0,可以解得 \[ \begin{align} \hat{\mu} &= \frac{1}{N} \sum_{i=1}^{N} x_i \\ \hat{\sigma}^2 &= \frac{1}{N} \sum_{i=1}^N (x_i - \hat{\mu})^2 \end{align} \]

多元高斯分布

\[ p(x) = \frac{1}{(2\pi)^{\frac{D}{2}} |\Sigma|^{\frac{1}{2}}} \exp(-\frac{1}{2}(\pmb{x} - \pmb{\mu})^T \Sigma^{-1} (\pmb{x} - \pmb{\mu})) \]

  • \(D\): 维数
  • \(\pmb{X}\): 数据集
  • \(\pmb{\mu}\): 均值向量
  • \(\Sigma\): 协方差矩阵 (covariance matrix),对角线元素表示方差,非对角线元素表示变量相关性

用最大似然估计多元高斯分布:

\[ \hat{\pmb{\mu}}, \hat{\Sigma}=\argmax_{\pmb{\mu}, \Sigma}{p(\{\pmb{x}_i\}|\pmb{\mu}, \Sigma)} \] 类似于一元高斯分布,假设所有观测独立,则有 \[ \begin{align} \hat{\pmb{\mu}}, \hat{\Sigma} &= \argmax_{\pmb{\mu}, \Sigma} \prod{p(\pmb{x}_i|\pmb{\mu}, \Sigma)} \\ &= \argmax_{\pmb{\mu}, \sigma} \prod_{i=1}^N \ln p(x_i|\pmb{\mu}, \sigma) \\ &= \argmax_{\pmb{\mu}, \sigma} \sum_{i=1}^N (-\frac{1}{2}(\pmb{x}_i - \pmb{\mu})^T \Sigma^{-1} (\pmb{x}_i - \pmb{\mu}) - \frac{1}{2}\ln |\Sigma| + C) \\ &= \argmin_{\pmb{\mu}, \sigma} \sum_{i=1}^N (\frac{1}{2}(\pmb{x}_i - \pmb{\mu})^T \Sigma^{-1} (\pmb{x}_i + \pmb{\mu}) + \frac{1}{2}\ln |\Sigma|) \end{align} \]

设损失函数 \(J(\pmb{\mu, \Sigma}) = \sum_{i=1}^N (\frac{1}{2}(\pmb{x}_i - \pmb{\mu})^T \Sigma^{-1} (\pmb{x}_i + \pmb{\mu}) + \frac{1}{2}\ln |\Sigma|))\),用类似估计一元高斯分布的方法,令 \(\frac{\partial{J}}{\partial{\pmb{\mu}}}\)\(\frac{\partial{J}}{\partial{\Sigma}}\) 为 0,可以解得 \[ \begin{align} \hat{\pmb{\mu}} &= \frac{1}{N} \sum_{i=1}^{N} \pmb{x}_i \\ \hat{\Sigma} &= \frac{1}{N} \sum_{i=1}^N (\pmb{x}_i - \hat{\pmb{\mu}})(\pmb{x}_i - \hat{\pmb{\mu}})^T \end{align} \]

高斯混合模型 (Gaussian Mixture Model)

GMM 就是多个高斯模型的加权和:

\[ p(x) = \sum_{k=1}^K w_k g_k (\pmb{x}|\pmb{u}_k, \Sigma_k) \]

  • \(g_k\): 单个高斯分布函数
  • \(w_k\): 权值函数,总和为 1

解 GMM 的方法之一就是 EM (Expectation-Maximization)。

EM 法解 GMM

引入隐含变量 \[ z_k^i = \frac{g_k(\pmb{x}_i) | \pmb{u}_k, \Sigma_k}{\sum_{k=1}^K g_k(\pmb{x}_i) | \pmb{u}_k, \Sigma_k} \]

\(z_k^i\) 的表示第 i 个观测数据中,第 k 个高斯函数占全体的比重,直观表示如下图

z_k^i 的直观表示
\(z_k^i\) 的直观表示

均值向量和协方差矩阵可以通过 \(z_k\) 估计 \[ \begin{align} \hat{\pmb{\mu}}_k &= \frac{1}{z_k} \sum_{i=1}{N} z_k^i \pmb{x}_i \\ \hat{\Sigma}_k &= \frac{1}{z_k} \sum_{i=1}{N} z_k^i (\pmb{x}_i - \hat{\pmb{\mu_k}})(\pmb{x}_i - \hat{\pmb{\mu_k}})^T \\ z_k &= \sum_{i=1}^N z_k^i \end{align} \]

EM 法具体过程

  1. 初始化 \(\pmb{\mu}\) and \(\Sigma\)
  2. 固定 \(\pmb{\mu}\)\(\Sigma\),并更新 \(z_k^i\) 的值 (E-step)
  3. 固定 \(z_k^i\),并更新 \(\pmb{\mu}\)\(\Sigma\) 的值 (M-step)
  4. 重复第 2、3 步,直到稳定

全课程的笔记链接

Sous Vide 低温慢煮龙虾

Prime Day 的时候入了一套 Sous Vide (真空低温烹饪,读作 soo veed)装备,周末在家尝试了龙虾,味道很不错。

先上最后的效果图:

烹饪过程并不麻烦,只是中间低温煮的时间比较长,具体步骤如下:

  1. 剪开龙虾壳
  2. 把龙虾肉从壳里推出,在肉中加入盐和胡椒
  3. 把龙虾肉推回壳内
  4. 放入真空袋中,加上黄油和柠檬,把袋抽成真空
  5. 用 Sous Vide 控制水温在 140°F (60 °C),放入龙虾煮 1 小时
  6. 1 小时后取出龙虾,把袋口剪开,龙虾放在烤架上
  7. 再次加上黄油后用火焰喷枪烧烤龙虾外层,完成美拉德反应
  8. 加胡椒和葱,摆盘

装备 / 工具篇

这里用到的工具有

其他可以考虑入手的工具

  • Bernzomatic TS8000 火焰喷枪:前面 EurKitchen 的喷枪火力一般,尤其在处理牛排的时候不是很给力。这个喷枪配合下面的喷枪头特别适合处理牛排。
  • Searzall 喷枪头:架在火焰喷枪上,煎烤效果更均匀。

线性代数笔记

最近抽空把线性代数重新过了一遍,整理了一份概念笔记,希望对别人也有用。主要参考了同济大学的《线性代数》《Deep Learning》 的第二章。

行列式

  • 行列式 (determinant) 与它的转置行列式相等。\(D^T = D\)
  • 余子式 (minor):在 n 阶行列式中,把 \((i, j)\)\(a_{ij}\) 所在的第 \(i\) 行和第 \(j\) 列划去后留下的 \(n - 1\) 阶行列式叫做 \((i, j)\)\(a_{ij}\) 的余子式,记作 \(M_{ij}\)
  • 代数余子式 (cofactor) \(A_{ij} = (-1)^{i+j}M_{ij}\)
  • 行列式按行展开:\(D = a_{i1}A_{i1} + a_{i2}A_{i2} + \cdots + a_{in}A_{in}\)
  • 行列式按列展开:\(D = a_{1j}A_{1j} + a_{2j}A_{2j} + \cdots + a_{nj}A_{nj}\)
  • 克拉默法则 (Cramer’s rule):如果线性方程组的系数行列式不等于零,那么方程组有唯一解 \(x_1 = \frac{D_1}{D}, x_2 = \frac{D_2}{D}, \cdots, x_n = \frac{D_n}{D}\), 其中 \(D_j\) 是把系数行列式 D 中第 j 列的元素用方程组右端的常数项代替后得到的 n 阶行列式。

矩阵及其运算

  • 伴随矩阵 (adjugate matrix) \[ adj(A) = \begin{bmatrix} A_{11} & A_{21} & \cdots & A_{n1} \\ A_{12} & A_{22} & \cdots & A_{n2} \\ \vdots & \vdots & \ddots & \vdots \\ A_{1n} & A_{2n} & \cdots & A_{nn} \\ \end{bmatrix} \]

其中 \(A_{ij}\) 为代数余子式

  • \(A \text{adj}(A) = \text{adj}(A) A = |A|I\)
  • 奇异矩阵 (singular matrix):\(|A| = 0\)
  • 非奇异矩阵 (non-singular matrix):\(|A| \neq 0\)

线性相关和生成子空间

  • 线性组合 (linear combination):对于一个向量集 \({v^{(1)}, v^{(2)}, \cdots, v^{(n)}}\)\(\sum_i{c_iv^{(i)}}\) 为它的一个线性组合。一组向量的生成子空间 (span) 是指原是向量线性组合后所能抵达的点的集合。
  • 判断 \(Ax=b\) 是否有解相当于确定向量 b 是否在 A 列向量的生成子空间中。这个特殊的生成子空间被称为 A 的列空间 (column space) 或者 A 的值域 (range)。
  • 线性无关 (linear independene):如果一组向量中的任意一个向量都不能表示称其他向量的线性组合,那么这组向量被称为线性无关。
  • 奇异矩阵 (singular matrix):列向量线性相关的方阵。

范数

  • 范数 (norm) 用来衡量向量大小,\(L^p\) 范数定义为 \(\Vert{x}\Vert_p=(\sum_{i}^{} {\vert{x_i}\vert^p})^{\frac{1}{p}}\)
  • 欧几里得范数 (Euclidean norm):\(L^2\) 范数
  • 最大范数 (max norm):\(L^{\infty}\) 范数
  • Frobenius 范数可以用来衡量矩阵大小:\(\Vert{A}\Vert_F = \sqrt{\sum_{i,j} A_{i,j}^2}\)
  • 向量点积也可以用范数表示,即 \(x^Ty = \Vert{X}\Vert_2 \Vert{Y}\Vert_2 \cos{\theta}\),其中 \(\theta\) 为 x 和 y 的夹角。

特殊矩阵和向量

  • 对焦矩阵 (diagonal matrix):\(\forall {i \neq j}, D_{i,j}=0\),可以用 diag(v) 表示。
  • 对称矩阵 (symmetric matrix):\(A=A^T\)
  • 单位向量 (unit vector):\(\Vert{x}\Vert_2=1\)
  • 正交 (orthogonal):如果 \(x^Ty = 0\),那么向量 x 和向量 y 互相正交。
  • 标准正交 (orthonormal):在 \(ℝ^n\) 中,至多有 n 个范数非零向量互相正交。如果他们互相正交且范数都为 1,则称它们为标准正交。
  • 正交矩阵 (orthogonal matrix):行向量和列向量分别标准正交,即 \(A^TA = AA^T = I\)

特征分解

  • 推荐阅读 Eigenvectors and Eigenvalues
  • 特征向量 \(v\) (eigenvector) 满足 \(Av = \lambda{}v\)。其中 标量 \(\lambda\) 为这个特征向量对应的特征值 (eigenvalue)。
  • 如果矩阵 A 有 n 个线性无关的特征向量 \(\{v^{(1)}, \cdots, v^{(n)}\}\),对应特征值 \(\{\lambda_1, \cdots, \lambda_n\}\)。A 的特征分解为 \(A = V\text{diag}{(\mathbf{\lambda})}V^{-1}\)
  • 每个实对称矩阵可以分解成实特征向量和实特征值:\(A=Q\Lambda{}Q^T\)。其中 Q 是 A 的特征向量组成的正交矩阵。
  • 正定矩阵 (positive definite):所有特征值都是正数的矩阵。
  • 半正定矩阵 (positive semidefinite):所有特征值都是非负数。半正定矩阵 A 满足 \(\forall x, x^TAx \geq 0\)
  • 类似的还有负定 (negative definite) 和半负定 (negative semidefinite)。
  • 奇异值分解 (singular value decomposition) 把矩阵 A 分解成三个矩阵的乘积 \(A=UDV^T\)。推荐阅读 Andrew Gibiansky 的博客
  • Moore-Penrose 逆伪 (psedoinverse):\(A^{+}=VD^{+}U^T\)。其中 U, D, V 是矩阵 A 在 SVD 后的结果。\(x=A^{+}y\) 是所有可行解中 \(\Vert{x}\Vert_2\) 最小的一个。当没有解存在时,伪逆可以使 \(\Vert Ax-y \Vert_2\) 最小。

  • 迹 (trace) 定义为矩阵对焦元素之和:\(\text{Tr}(A) = \sum_i A_{i,i}\)
  • 迹的一些特性:
    • \(\text{Tr}(A) = \text{Tr}(A^T)\)
    • \(\text{Tr}(ABC) = \text{Tr}(CAB) = \text{Tr}(BCA)\)
    • \(\text{Tr}(AB) = \text{Tr}(BA)\) (如果乘法可行)

智能家居之实践篇

装修了半年多,两个月前正式入住,可以开始好好折腾智能家居了。现在用的一些方案和之前写的智能家居之计划篇差了不少,于是有了这篇博客聊聊现在的设计。这里直入主题,之前的计划篇里有更多的背景介绍。

服务器

我用了一台几年前的联想笔记本做服务器,装了个 Debian。这篇文章提到的大多数应用其实在树莓派上都能跑,Home Assistant 还专门给树莓派优化做了一个集成包叫 Hass.io 。下面主要讲软件部分。

中控系统

一开始用的是 SmartThings,尝试了 Home Assistant 之后就决定改用它了。HA 相对于 ST 有不少优势,首先 ST 的大部分需要联网才能工作,增加了额外的不稳定因素和延迟;同时 HA 是开源的 Python 项目,可定制性比 ST 高很多,例如可以把所有状态变化记录到第三方数据库,支持 FloorPlan 等强大的插件。

HA 本身只是个软件,并不直接支持 Z-Wave 和 Zigbee 等协议。我选了 Aeotec Z-Stick Gen5 用来接收 Z-Wave 的信号,家里 Zigbee 的设备不多,需要的时候也可以用 ST 通过 MQTT 传给 HA。

下图就是 Home Assistant 的面板截图,可以设置多个场景方便控制。比如我在睡觉前会看一眼 Security 确保门都锁好,以及其他监控正常。

数据库和监控

Home Assistant 默认会把所有的事件信息保存在 SQLite 数据库里,并不适合长时间保存,而且没法简单的导出给其他应用。我把所有的事件信息都保存到了 InfluxDB 里,在前端搭了一个 Grafana 做监控面板。

HA 对 InfluxDB 的支持很好,参考官方文档就能搞定,设置好以后所有的传感器更新、开关变化等信号都会保存到 InfluxDB 里。下图就是温度、湿度和占空传感器的一个 Grafana 页面。

以及 Unifi AP 的信号监控页面,借用了网上的一个 Grafana 模版

传感器

传感器可以用来监控房间的温度、湿度,是否有人,以及门窗是否关好等。接下来介绍一下我研究过的几款传感器。

Ecobee Room Sensor

因为家里是用 Ecobee 控制暖气的,所以多买几个 Room Sensor 可以很方便的集成到网络里。Ecobee 会根据有人的房间的温度控制暖气,同时 Ecobee API 也会输出这些 Sensor 的数据(温度、是否有人)。购买链接

Monoprice Door Sensor

性价比挺高的门窗传感器,外观也比较低调。基于 Z-Wave Plus 协议,会报告剩余电量。购买链接

Monoprice Z-Wave Plus Multi Sensor

可以报告温度、湿度、是否有人和自身电量。默认的报告频率有点低(差 2 度才会发送更新),需要发个指令调节。购买链接

Wireless Sensor Tags

需要先买一个 Tag Manager,可以接入多达 40 个传感器,而且有效范围在 400ft (120m)。这个方案看起来很不错,不过我用 Ecobee sensor 再加几个 Monoprice 的 multi sensor 已经够用了。

小米的智能家庭套装

小米的温湿度传感器和门窗传感器都只要 ¥49,性价比非常高,而且外观也不错。不过最后我还是没买小米的设备,主要原因是小米用的是私有的 Zigbee 协议,不支持 Smart Things,得买小米自己的中控。然而小米中控的有效范围在 10m 左右,用电池的传感器也不支持信号中继,得在楼上楼下放好几个小米中控才能保证足够的覆盖范围。

Monoprice Z-Wave Plus Door and Window Sensor

Monoprice 的门窗感应器,我在两扇院子门上各装了一个,方便查看院子门有没有关上。购买链接

监控摄像头

一开始我用的是 Arlo Pro,然而用了一阵子后觉得 Arlo 还是有不少问题,比如有录像延迟,检测到物体时经常会错过一开始的几秒,而且不付月租费话不支持 24 小时录像,即使插电源也不可以。

最后决定还是用传统 IP 摄像头 + NVR。视频录制在 NVR 的本地硬盘,出于安全考虑 NVR 不直接暴露给外网,而是通过中控服务器上的 ZoneMinder 间接访问。ZoneMinder 是一个开源的录像监控方案,其实它的功能已经相当强大了,但是同时监控几个摄像头会长时间占用中控的 CPU,所以我还是用了 NVR 专门负责监控录像。

照明

智能开关

研究了几个带亮度控制的开关,主要推荐两款,都是 Z-Wave Plus 协议的:

  • HomeSeer HS-WD100+ Dimmer,有对应的双联开关。HomeSeer 的开关可以实时更新状态,不过得保证开关盒里有零线和地线。有些老房子的开关盒里不一定有零线。
  • GE Z-Wave Plus Dimmer,这款是 GE 的,价格比 HomeSeer 的便宜一点,也有对应的双联开关。GE 的这款开关不支持实时更新状态,但是优点在于不需要零线,老房子也能用。

另外我还试过 Leviton DZMX1-1LZ,不推荐这款,要求有零线,价格不便宜而且还不支持 Z-Wave Plus。Leviton 应该有新款的开关,不过我没研究过。

智能灯泡

这一块没怎么研究,Hue 用过一段时间,还算方便,但就像之前那篇文章里提到的,智能灯泡的问题在于很难和普通开关一起用,得用配套的遥控开关才行,会导致墙上多不少开关。

另外 IKEA 今年出了不少智能灯泡,用了 Zigbee 协议,看评测感觉很有前途。

网络

一开始我在 EeroOrbi 之间纠结,结果有位研究无线网络 4 年的同事给我推荐 Unifi 的无限路由,试了下的确好用。

UniFi Pro AP (UAP‑PRO) 可以通过 PoE 供电。不过 Unifi 设备的 PoE 比较特殊,这款 UAC-PRO 是同时支持 802.3af 和 802.3at 协议的,然而 UAP-AC-LITE 只支持 802.3at。如果你打算用 Unifi 官方的 PoE 网关,不需要担心这个问题。但如果你像我一样用的是其他的(我用了 NETGEAR JGS516PE),买之前得研究下这个供电问题。

Unifi Pro AP 的信号覆盖很好,我家楼上楼下各有 1400 sqft(130 平方米),院子不大。我在楼下入口和楼上靠近院子的房间各放了一个 AP,基本上就做到整个房子包括院子无死角覆盖了。这样算下来成本其实和用 Eero / Orbi 也差不多,但是性能会好很多,因此推荐给房间里布置了网线口的朋友。

Unifi 也出了类似 Eero 的 mesh network 的解决方案,没有研究过所以不做评价。

影音

客厅用了原来的 Harmony 遥控,配合 Amazon Echo 开关电视很方便。

装修的时候在其他房间布置了天花板音响,但是没有现成的价格又不是太贵的多个房间的音响解决方案。研究了一通之后采用了 Echo Dot + T-Amp 的方案,每个房间配一个 Echo Dot 和一个小型功放,用手机控制各个房间的音乐。功放我用的是 Topping TP30,不算音响成本大概在 $130 左右,比起其他动辄两千的解决方案划算多了。

天花板音箱用的是 Polk MC60,天花板音响效果的期望本来就不高,这个 6” 的音箱的音质已经够好了。另外它还防潮,所以厕所也能用。

其他

Homebridge

网上有一个开源的 Homebridge 插件,可以让 Homebridge 支持 Home Assistant,这样在 HomeKit 里面控制 HA 上的设备了。不过我很少用 HomeKit,没有花时间把一个个设备整理好。

门锁

门锁用的是 Schlage Camelot Touchscreen Deadbolt,Z-Wave 协议,很稳定,用到现在没出什么问题。用电量很小,三个月下来我的几个门锁还有 99% 的电(当然也有可能是 Z-Wave 电量报告不准确)。

Automatic

Automatic 是一个车载装置,它可以记录你的车辆行驶状态、当前位置等信息,HA 官方支持 Automatic,可以把车辆信息作为条件放到 HA 的自动化脚本里,比如车在车库里熄火以后关闭内部摄像头。

用电量

试了下 Aeotec 的电量检测工具,需要安装在电箱附近。这套工具价格不贵($15 左右),但是很不好用,有实时更新的问题。Home Assistant 的论坛上有个帖子讨论怎么搞定它的自动更新。

车库门控制

家里车库门的动力引擎用了 LiftMaster,所以我就买了他家的 Chamberlain MYQ-G0201 MyQ-Garage。这个设备不支持 Z-Wave 协议,但是 Home Assistant 有个插件可以以用户名密码的方式登陆后台控制。

如果想要支持 Z-Wave 协议的车库门开关,可以考虑 GoControl GD00Z-4

电动窗帘

看下来 Bali 的方案还不错,Home Depot 可以试,不过最后因为各种原因还是没装。

Twilio

发短信的平台,配合 HA 的自动脚本很好用。比如我的设置里有一条规则是外门超过 5 分钟以上没锁就发短信提醒自己。正常的推送量用 Twilio 很便宜,价格大概是 $0.0075 一条,所以我都没有设置 HA 的推送平台。

One More Thing: Floorplan

最近在折腾的一个叫 Floorplan 的 HA 插件,顾名思义就是让所有的智能设备显示在一个平面图上方便控制。

这是我目前的效果图,现在只加入了灯光、占空和温度信息,点击对应的房间可以控制这个房间的灯光。接下来打算在左侧放一排全局控制的按键,把弄一个平板挂到墙上,就可以在进家和出门的时候方便的控制全屋设备了。类似下图的效果(图片来源)。

智能家居之计划篇

二月中旬开始装修房子,做了不少智能家居的研究,分享一下。坐标湾区,项目目前还在计划和购买阶段,欢迎拍砖,欢迎种草。

家居中控

中控协议是要先决定的一项,因为选定以后就优先考虑支持这个协议的设备了。 现在比较热门的有 Z-Wave、Zigbee 和 Insteon。Zigbee 和 Z-Wave 走的是网状 (mesh) 网络,每一个设备都可以作为中继节点传播信号,所以家里支持这一协议的设备买得越多,信号覆盖就越好。Insteon 除了和 Z-Wave 类似的无线信号外,还可以从电力线直接发送信号,理论上会更稳定些。Insteon 的灯光控制开关做得比较好,不少人因为这个在灯光开关上选择了 Insteon。Zigbee 相比 Z-Wave 是一个更开放的协议,用的频段和 Z-Wave 互不冲突。Z-Wave 的最大优点在于支持的设备类型非常多,窗帘、门锁、灯光都有不少支持的设备。所以最后我还是决定用 Z-Wave 为主的无线控制。

选好协议后就是决定控制中心了,没有花太多时间研究,选了支持 Z-Wave 的 Smart Things,主要是因为它的用户社区很活跃,产品迭代也比较快。

语音控制我选了 Alexa (Echo),出来早,支持的产品多,和 Smart Things 的对接做得也很好。Echo 目前有三个产品,语音功能上都一样,主要是携带性以及音响效果的差别。买几个 Dot 在常去的房间里保证语音控制覆盖就好。

另外可以考虑买一块平板挂在墙上当全屋的控制器,有个叫 SmartTiles 的应用,本质上是一个网页,可以用来控制 Smart Things 的各种功能。

照明

照明灯的远程控制主要有两种方案,一种基于智能开关,一种基于智能灯泡。

基于智能灯泡的方案的最大问题在于设备事实上一直都处于带电状态,对于多路开关控制电灯的场景,需要在每个开关位置边上放一个不走电的无线开关,同时还要避免和物理开关混用,导致设备电源被切断的情况。除非从一开始决定就只用假开关,不在墙上布置多路电开关,否则墙上开关的数量就会多不少,在使用时也容易按错。

而基于智能开关的方案则解决了这个问题,因为把无线开关做进了电开关,所以使用的时候就可以像用普通多路开关一样。智能开关的另一个好处在于成本相对便宜,一个无线调光开关的价格大概在 $50 左右,而一个智能灯泡价格在 $15 左右,对于装 6 个顶灯的大厅来说无线开关的方案会便宜点。

基于智能灯泡的方案的好处在于一些灯泡可以调色温,甚至不同颜色,此外每个灯泡还可以单独控制。对于卧室这种不需要多路开关,而且关灯可以只用手机或者语音的房间,我还是选择了黄白双色的 Hue,这样可以白天用白灯,晚上切换成昏黄的灯光。

具体选购方面,智能开关以前一般推荐 GE 的无线调光开关,不过最近 Homeseer 出了一款新的无线调光开关,相比 GE 的优势在于支持实时状态更新。多路无线开关要买配套的开关。此外不同的灯泡对无线调灯开关的支持不一样,Homeseer 官网上有一份兼容性列表可以参考。

Hue 还有个灯带产品,也挺好玩,可以考虑放在厨房橱柜的顶端做照明。

音响

我在大厅、饭厅、厨房和厕所的天花板都装了无线音响,厨房和厕所的音响需要注意买防潮的。

走线方面,楼上的音响线都绕到一个储物柜里,楼下的音响线都绕到储物间(有网关)。音响线记得要买 CL2 或者 CL3(出于防火考虑),同时如果音响线超过 50 尺,得换用 14 AWG 的厚度。

音源方面,我的计划是买几个 Sonos CONNECT:AMP 分别控制这几个房间的音乐。由于 Sonos 的功放太贵了,打算先把音响和走线搞定了,到时候再一个个买。另外可以考虑把相邻两个房间(比如饭厅和客厅)的音响连到一个 Sonos 功放,不过得确保音响的阻抗是 8 欧姆的。

另外 Sonos 现在有个问题是还不支持 Alexa,不过公司已经明确表明要好好做这个功能了

Crutchfield 上有几片文章讲得很详细,分别关于音响排线,可以参考。

电动窗帘

每天醒来能吼一声让 Alexa 拉开窗帘的感觉应该不错,所以我们很想装个可以远程控制的电动窗帘。做电动窗帘的小公司不少,但是大多数都是用传统的红外控制,这就意味着如果要把它接入到家里的控制网络,还得单独买一个 Z-Wave 到红外的发射器。

最后看下来 Lutron Serena 和 Somfy 两套系统比较靠谱。不过 Serena 不支持 Z-Wave,只好放弃。Somfy 似乎不直接卖窗帘杆,而是和一个专门卖窗帘的品牌 Bali 合作,在卖窗帘的同时提供电动控制的选项。

Bali 电动窗帘可以在两种供电模式里面选择,一是用 8 节 AA 电池,可以用12 到 18 个月;或者插座供电。虽然可以在窗帘杆边上加个新的插口,不过还是觉得接电线的方案太丑了,也没有理性的隐藏方案,所以决定还是用更折腾的电池方案。

无线覆盖

研究了三种解决方案:传统的无线扩展器,EeroOrbi。传统的扩展器方案似乎稳定性不咋的,而且还要花时间设置。Eero 和 Orbi 的主要区别在于 Orbi 的主从设备之间用了独有的信道传输,所以理论上不会影响 WiFi 信道的性能;Eero 支持所有设备连接有限网络从而增强信号。因为是重新装修,每个房间本来就会提供网线接口,Eero 的这个特性就很有用。

家庭影音

没研究别的方案,家里本来就有个 Logitech Harmony,而且对 Smart Things 的支持也很好,所以就继续用了。Harmony 现在最大的好处就是看完电视不用再找遥控器了,用语音就能关掉。

另外推荐一个联想的迷你键鼠 N5902,很适合用来当 HTPC 的遥控,不过好像已经停产了。

安全与其他

视频监控方面选了 Arlo Pro,优点在于可以户外使用,有 7 天免费云存储,也可以备份视频到本地。Arlo Pro 有个延迟的问题,为了省电它会在红外感应到有人进入检测区域后才开始录像,所以可能会有几秒的延迟。网上也有很多人因为这个原因最后放弃了 Arlo。不过我在目前的租房里面试了下,放好相机的位置可以把延迟缩短到 1 秒左右。

门锁还没仔细研究,不过应该会用 Schlage 的电子锁。话说这套 193x 年的老房子,外面铁门的锁从来没换过,居然也是 Schlage 的。

车库门应该也有无线的选项,不过也还没仔细研究。

暖气控制应该会买 Ecobee3,因为家里的供暖系统是统一的,没法按房间分别控制。Ecobee3 可以在不同房间安装感应器保证有人在的房间的温度稳定。

本地 Markdown 预览工具

最近一直用 iA Writer 做笔记,用不同的文件保存不同的主题,由于 iA Writer 并没有很好的管理和浏览功能,于是就想做个 Web 工具方便浏览和管理。

markdown-wiki 是我用 Sinatra 做的一个简单的预览工具,它可以把某个目录下的 Markdown 文件以 Wiki 的形式呈现出来。界面上借用了 Ghost 的 CSS,可以在 http://markdown-wiki-demo.herokuapp.com/ 预览(因为是非本地的内容,上方的 Edit 按钮没有作用)。

Markdown 语法方面,由于用了 redcarpet 所以有不少语法扩展,包括代码块、删除线、下划线、上标等,另外包含了 Wiki 内部链接支持。

安装说明

  1. 下载源代码
  2. 安装 Ruby 和 bundle
  3. 在项目目录下运行 bundle install
  4. 将 app.rb 中的 WIKI_ROOT 改成 Markdown 文档所在的目录
  5. 运行 mac/webeditor opener.app

安装后在项目目录下运行 rackup(如果要限制只能本地访问可以运行 rackup -o localhost),并访问 http://localhost:9292 即可。

本地编辑器(目前只支持 Mac 系统)

在网页上打开本地编辑器的功能是通过 URL Scheme 实现的,mac/webeditor opener.app 会将 wikieditor:// 注册给一个 AppleScript,后者负责运行 iA Writer 并打开相应的文件。mac/ 下已经包含了预先打包好的 app 文件,如果你需要修改 AppleScript,可以打开 mac/webeditor opener.scpt,编辑完成后点击 File -> Export,在 File Format 中选择 Application,并将 mac/Info.plist 放到新生成的应用包中。

如果在打开 iA Writer 时提示权限问题,请先在 iA Writer 中手动打开一次需要编辑的文件,之后就能顺利编辑这个目录下的所有文件了。

其他功能

目前功能比较简单,有几个接下来考虑加入的功能:

  • 文本搜索
  • 目录支持
  • 其他编辑器的支持

源代码在 GitHub 上,欢迎提供建议和 Pull Request。

用外接 PC 键盘控制 Mac 音量

写了个 Alfred 插件,用于外接 PC 键盘控制 Mac 的系统音量。

快捷键为 Alt + F10/F11/F12,分别和 MacBook Pro 键盘的 F10/F11/F12 功能对应:

  • Alt + F10 切换静音模式
  • Alt + F11 减少 10% 音量
  • Alt + F12 增加 10% 音量

音量调整后会播放 /System/Library/Sounds/Frog.aiff,这个声音和默认的调整提示音比较像。(有谁知道默认的音量调整提示音用的是哪个文件吗?)

另外如果在 Alfred 里把中间的 Run NSAppleScript 和右边的 Push Notification 一一连起来,就能在修改音量后看到当前音量的提示,不过我觉得没啥用所以默认取消了。

插件下载

马尔代夫 Dusit 岛

选岛和费用

我们选岛时参考的分类主要是一家淘宝店马尔代夫DIY的选岛页面,最后也是找了后者帮忙订机票和酒店。

因为老婆想要住好点的酒店,我又想浮潜,综合考虑这两个因素后我们选择了 Dusit Thani(都喜天阙岛),四晚豪水 + 水飞 + 早晚餐一共每人 23000 元。

关于住宿,我看到不少代理都给出了两沙两水的配置,这样其实很折腾,中间还要搬一次家。另外 Dusit 岛上的水屋也分普水(Water Villa)和豪水(Ocean Villa),前者在退潮的时候就会变成沙屋,建议大家还是选择四晚豪水,毕竟相比总价两者差不了多少钱。

另外四晚住下来我们都觉得时间有点短,应该选择住五晚或者六晚。

美佳航空

出发前几天在网上搜美佳航空的评价,发现这个航空很不靠谱,在微博上搜索「美佳航空」的结果除了广告几乎都是抱怨。北京机场出发的美佳航班在7月1号7月8号都有过七八个小时的延误。所以建议大家如果去马代的话尽量不要选择这个航空公司。

不过我们在美佳航空的体验还不错,往返航班都是准点的。通过旅行社顺利申请到了蜜月蛋糕,去程航班上空姐还会给每人发一个可爱多。

浮潜

岛上潜水中心可以免费租浮潜装备,但是得先上它们的浮潜课,浮潜课每人 55 美元。另外潜水中心还可以租借水下相机(Sony TX10),一小时 25 美元。浮潜的时候还可以请教练帮忙照相,他会潜入水中近距离拍摄。

岛上的浮潜面罩都是平光镜,高度近视的朋友还是去淘宝上买一副近视面罩吧。另外如果觉得租借的呼吸管不太卫生,也可以自带。脚蹼就没必要自带了。

其他

  • 时差:马累在 +5 时区,而 Dusit 岛在 +6 时区,和马累差了一个小时。
  • WiFi:Dusit 岛上免费 WiFi 覆盖很广,我们去过的地方除了浮潜中心和 SPA 屋,都是可以连接上 WiFi 的。
  • 出行:每位游客都可以免费租用一辆自行车,如果不方便骑车,还可以打电话给总台叫车,也是免费的。
  • 小费:机场里有兑美元的地方,可以把大额美元拆成小额纸币用于小费,但是我们到马代的那天兑换处的零钱都已经用完了,好在岛上总台那里还可以兑换。另外饭店吃完后可以在签单时直接写小费数额,所以不用特地准备零钱给小费。
  • 游泳池:Dusit 岛有目前马尔代夫最大的无边游泳池,一定要试一下。这个游泳池里用的是淡水,不像私人泳池用的是海水。
  • 餐厅:The Market 的晚餐有很不错的甜品,我们去的周日还有烧烤。不过 The Market 的蚊子有点多,记得带防蚊用品。泰国餐厅 Benjarong 里的菜吃大不惯。
  • 蜜月晚餐:提供六个月内的结婚证原件就可以享受一顿免费的蜜月晚餐了。酒店还有送一瓶白葡萄酒,不过味道实在不怎么样。
  • SPA:岛上的 Devarana Spa 是在树上,来了 Dusit 就一定要试一试。
  • 结账:岛上消费都只需要留个房间号,最后一晚管家会把账单送到房间。记得好好过一遍账单,我们的账单有两笔有问题的支出,第二天结账的时候和管家说一下就好。

Kindle 推送知乎日报

知乎日报每天都会更新有意思的问答。我比较习惯用 Kindle 看这样的文章,就写了一个 calibre 的插件抓取每天的内容。

插件使用说明

  1. 下载 calibre
  2. 在 calibre 中点 Fetch news 右侧的小三角,选择 Add a custom news source,在弹出的对话框中选择 Switch to Basic mode,把插件的源码粘贴到文本框中,点击右侧的 Add/Update recipe 就添加成功了。
  3. 点 Fetch news 按钮,在左侧的 Custom 分类中选择刚才添加的「知乎日报」,点 Download Now 即可抓取。

另外 calibre 还有定时抓取和推送的功能。前者可以在抓取的对话框中设置,后者在 Preference -> Change calibre behavior -> Sharing books by email 中设置。结合这两个功能就可以在抓取完成后自动把最新的内容推送到 Kindle 了。

Ruby 命令行中快速查看函数源码

如果要查看 ActiveRecord 的 update_attribute 函数的源代码,一个比较常见的方法是直接在 Rails 源码中搜索 def update_attribute。博客 The Pragmatic Studio 介绍了一个更方便的技巧,在 Ruby 命令行中就能启动编辑器直接访问。

通过 Object#method 方法可以获得 update_attribute 方法的对象,而 Method#source_location 则返回这个方法定义的文件和位置。有了这个信息后,就能启动编辑器查看源代码了:

> method = User.first.method(:update_attribute)
  User Load (0.5ms)  SELECT `users`.* FROM `users` LIMIT 1
=> #<Method: User(ActiveRecord::Persistence)#update_attribute>

> location = method.source_location
=> ["/Users/wyx/.rvm/gems/ruby-1.9.2-p180/gems/activerecord-3.2.11/lib/active_record/persistence.rb",
 177]

> `subl #{location[0]}:#{location[1]}`
=> ""

把这段代码封装成函数,加到 .pryrc 或者 .irbrc 中:

def source_for(object, method)
  location = object.method(method).source_location
  `subl #{location[0]}:#{location[1]}` if location && location[0] != '(eval)'
  location
end

如果要查看 User 的实例方法 update_attribute,可以直接在 pry / irb 中调用

source_for(User.first, :update_attribute)

如果要使用其他编辑器,得把 subl #{location[0]}:#{location[1]} 换成这个编辑器对应的命令行:

# TextMate
mate #{location[0]} -l #{location[1]}

# MacVim
mvim #{location[0]} +#{location[1]}

# Emacs
emacs {location[0]} +#{location[1]}

原文链接:http://pragmaticstudio.com/blog/2013/2/13/view-source-ruby-methods

清除 zsh steeef 主题的未追踪标记

我用的 zsh 提示符是 oh-my-zsh 自带的 steeef。最近发现用这个主题时,有些 Rails 项目即使把所有改动都提交后,还是会有红色标记表示存在未追踪文件:

使用 git statusgit diff,都看不到任何未提交的改动。一开始我以为是 zsh 或者 git 的 bug,把它们的版本都更新到最新版后还是有这个问题。于是看 steeef 主题的源码,发现了红色标记的判断依据:

# check for untracked files or updated submodules, since vcs_info doesn't
if git ls-files --other --exclude-standard --directory 2> /dev/null | grep -q "."; then
    PR_GIT_UPDATE=1
    FMT_BRANCH="(%{$turquoise%}%b%u%c%{$hotpink%}●${PR_RST})"
else
    FMT_BRANCH="(%{$turquoise%}%b%u%c${PR_RST})"
fi

因为 vcs_info 没有提供未追踪文件或模块的方法,作者在这里用了 git ls-files --other --exclude-standard --directory 检测当前项目是否包含未追踪的文件,而在我的项目根目录下运行这个命令后,可以看到有三个目录未被追踪:

$ git ls-files --others --exclude-standard --directory
log/
public/system/
tmp/

这几个目录在 .gitignore 中都有声明,当时项目刚创建时借用了 gitignore 中的模版,相关的声明是:

/log/*
/tmp/*
/public/system/*

看来问题就出在这里用了通配符 *,把目录下的所有文件而不是目录本身忽略了。因为 git 不允许把空目录加到项目中,git statusgit ignore 都不会显示这些目录,而作者检测时用的 git ls-files --directory 又会包含未追踪的空目录,就出现了这个提示有改动却找不到的情况。知道问题后解决方案很简单,把 .gitignore 文件中的 xxx/* 都改成 xxx/,或者把 --directory 参数去掉就好了。

为 Emacs cscope 加入 Java 支持

Emacs 的 xcscope 插件默认不会扫描 Java 文件,另外 Android 源码里有不少 .aidl 的文件,默认也不包含在 xcscope 的扫描范围里。解决这个问题的一个方法是在项目根目录下手动创建 cscope 索引:

$ find . -name "*.java" -or -name "*.aidl" -or -name "*.cpp" > cscope.files

$ cscope -b

这样做的缺点很明显,索引功能没有做到 Emacs 里,需要单独起一个 shell,比较麻烦。我发现这个问题的本质在于 xcscope 创建索引用的是 cscope-indexer 这个脚本,而 cscope-indexer 默认只会扫描 C/C++ 的源码文件。所以其实只要修改 cscope-indexer,把第 140 行从原来的

    egrep -i '\.([chly](xx|pp)*|cc|hh)$' | \

改成

    egrep -i '\.([chly](xx|pp)*|cc|hh|java|aidl)$' | \

之后就能用 C-c s I 在 Emacs 中创建 Android 项目的索引了。

❌