Epipolar a personal journal

写出了一个有趣的 bug ……

背景建筑图片来自 Wikimedia

微软从来不会数数

如题,亮点自寻……

旋转变量求导

在涉及空间三维运动的问题中经常会遇到含有旋转变量的能量,最优化这样的能量时,便需要对旋转变量进行求导。 在当前旋转的切空间上,运用李代数的方法进行局部求导已经是一种标准的做法,但其中推导比较复杂,不熟悉的话容易出错。 为此,这篇文章整理了一些常见的包含旋转变量的能量形式以及它们的导数。

基本方法

我们回避过于形式化的完整推导,采用一种仿照对偶数1的简易推导方法。如果对于一个函数 $f$ 在点 $x$ 处有 其中,$\delta x$ 和 $\delta f$ 看作是微元或者对偶量,那么我们就可以从中提取 $\delta f/\delta x$ 作为 $f(x)$ 的导数。 从具体步骤上来说,首先在需要求导的点 $x$ 上添加微元 $\delta x$ 后应用函数 $f$ ,然后将其展开为 $f(x)$ 加上微元余项的形式,最后就可以提取导数了。

旋转小量

三维旋转只有三个自由度,然而它高度非线性,因而我们采用李代数的方法,在切空间中进行求导。这样一来,旋转小量便对应了三维的向量。旋转切空间的李代数到旋转的李群之间由指数映射对数映射联系起来,对一个旋转 $R$ 添加小量微扰 $\delta R$ 可以表示为 $R\oplus \delta R = R\exp \delta R$ 。相对的,两个相近的旋转 $R_1$ 和 $R_2$ 之间的差可以表示为 $R_2 \ominus R_1 = \log(R_1^\top R_2)$ 。

此外,我们有一些有用的关系:

  1. $\exp(\delta R) = I+[\delta R]_\times$
  2. $\exp(\delta R)U = U\exp(U^\top\delta R)$
  3. $\log(U\exp(\delta R)) = \log U + J_r^{-1}(\log U)\delta R$

注意由于 $\delta R$ 是微元,上面三个关系的等号可以认为成立,否则第一和第三个关系只在 $\delta R$ 比较小时近似成立。

常见基本形式的导数

1. 三维点的旋转

空间三维点的旋转对应于 $f_1(R) = Rx$ 或 $f_2(R) = R^\top x$ ,其中 $x$ 是三维常向量。以 $f_1$ 为例:

因此,对应的导数为 $-R[x]_\times$ 。

下表为上述两个形式对应的导数:

$f_i(R)$ $[f_i(R\oplus\delta R)-f_i(R)]/\delta R$
$Rx$ $-R[x]_\times$
$R^\top x$ $[R^\top x]_\times$

2. 旋转的复合

旋转的复合对应于 $f_3(R) = UR$ 、$f_4(R) = UR^\top$ 、$f_5(R) = RU$ 和 $f_6(R) = R^\top U$ 四种形式中的一种,其中 $U$ 为一个常量旋转。由于这四个形式最终结果是旋转,根据参数化方式的不同,结果的维度也不一样,我们转而在结果的切空间内求导,此时导数对应了 $3\times3$ 矩阵。结果的导数形式中,所有的旋转也相应采用其 $3\times3$ 矩阵形式表达,以 $f_5$ 为例:

因此,对应的切空间内的导数为 $U^\top$ 。 下表列出了上述四个形式对应的导数:

$f_i(R)$ $[f_i(R\oplus \delta R) \ominus f_i(R)]/\delta R$
$UR$ $I_{3\times 3}$
$UR^\top$ $-R$
$RU$ $U^\top$
$R^\top U$ $-U^\top R$

3. 旋转的差值

旋转的差值对应于 $f_7(R) = \log(UR)$ 、 $f_8(R) = \log(UR^\top)$ 、$f_9(R) = \log(RU)$ 和 $f_{10}=\log(R^\top U)$ 四种形式中的一种。以 $f_9$ 为例:

因此,对应的导数为 $J_r^{-1}(f_9(R))U^\top$ 。

下表列出了这四个形式对应的导数:

$f_i(R)$ $[f_i(R\oplus \delta R)-f_i(R)]/\delta R$
$\log(UR)$ $J_r^{-1}(f_7(R))$
$\log(UR^\top)$ $-J_r^{-1}(f_8(R))R$
$\log(RU)$ $J_r^{-1}(f_9(R))U^\top$
$\log(R^\top U)$ $-J_r^{-1}(f_{10}(R))U^\top R$

在面对复合函数时,只要正确使用链式法则结合上面的导数,便可以得到想要的结果了。

OpenCV 中进行 YUV420 到 RGB 的转换

最近的一个项目中,需要从远程接收 H.264 编码的视频流,然后在本地进行解码后使用图像。

本地从 H.264 码流解码出的图像采用了 YCbCr(I420) 格式,对于一帧 $W\times H$ 的图像,它将像素色彩的三个分量分别储存在三个平面上,也就是三个缓冲区,首尾相连。第一个平面对应 Y 色度平面,储存了亮度信息,它的大小也是 $W\times H$。第二个和第三个平面对应 Cb 和 Cr 色度平面,储存了蓝色和红色的色差信息,由于利用了人眼视觉上对色彩的空间分布相对不敏感的特点,这两个色度平面对原始图像的两个方向上各进行了$\frac12$的降采样,也就是说每$2\times 2$的像素共用一组 $(C_b,C_r)$ 色差信息。由于这个原因,这两个平面的大小都是 $\frac W2 \times \frac H2$。

理论虽然是这样,实际要用 OpenCV 来做就比较麻烦了(并不)。

先说说麻烦在那里:由于三个色度平面大小不一,在如何使用 cv::Mat 表示上就产生了许多的猜测。网上一番搜索,基本都是介绍原理,实现需要一个像素一个像素手工转换。对于我这种懒人,我是非常希望可以用一句 cv::cvtColor 搞定的。这样做有很多的好处,比如代码更精简,比如工作量小,比如可以更快,比如有问题了可以甩锅给 OpenCV…… 可稍微看了一下文档就会意识到, OpenCV 的文档根本就是垃圾,从文档是不可能知道该怎么用的!

那么真的要自己去实现一个么?不需要! cv::cvtColor 究竟支不支持这样的转换呢?支持!

所以,怎么做呢?

有的时候,代码就是最好的文档(不过 OpenCV 又一次证明我错了)。经过浏览 OpenCV 中 cv::cvtColor 的代码实现,我注意到了这样的段落(项目中使用的是 OpenCV 2.4.13):

// :::
case CV_YUV2BGR_YV12: case CV_YUV2RGB_YV12: case CV_YUV2BGRA_YV12: case CV_YUV2RGBA_YV12:
case CV_YUV2BGR_IYUV: case CV_YUV2RGB_IYUV: case CV_YUV2BGRA_IYUV: case CV_YUV2RGBA_IYUV:
    {
        //http://www.fourcc.org/yuv.php#YV12 == yuv420p -> It comprises an NxM Y plane followed by (N/2)x(M/2) V and U planes.
        //http://www.fourcc.org/yuv.php#IYUV == I420 -> It comprises an NxN Y plane followed by (N/2)x(N/2) U and V planes
// :::

从这里可以看到 CV_YUV2{BGR|RGB}[A]_{YV12|IYUV} 系列枚举值对应了我们需要的色度转换。通过进一步阅读后面的处理(朋友,我替你读过了,所以不要读了,太浪费生命),可以知道正确的方法是建立一个 cv::Mat ,直接按照标准 YCbCr(I420) 的平面大小分配空间并填充数据,然后调用 cv::cvtColor 。整个过程的代码如下:

cv::Mat YUV420_to_BGR888(
	int width, int height,
	const uchar * Y, const uchar * Cb, const uchar * Cr,
    int strideY, int strideCbCr
) {
	int uvwidth = width / 2;
	int uvheight = height / 2;
	int size = width * height;
	int uvsize = uvwidth * uvheight;
	
	// whole data size = Y + U + V = W*H + (W/2)*(H/2)*2 = W*(H+H/2)
	cv::Mat YCbCrData(cv::Size(width, height + uvheight), CV_8UC1);
	
	// get the pointer to the beginning of each plane.
	uchar* pY = YCbCrData.data;
	uchar* pCb = pY + size;
	uchar* pCr = pCb + uvsize;
	
	// copy Y channel for each line
	for (int i = 0; i < height; ++i) {
	    memcpy(pY + i*width, Y + i*strideY, width);
	}
	
	// copy Cb and Cr channel for each line
	for (int i = 0; i < uvheight; ++i) {
	    memcpy(pCb + i*uvwidth, Cb + i*strideCbCr, uvwidth);
	    memcpy(pCr + i*uvwidth, Cr + i*strideCbCr, uvwidth);
	}
	
	cv::Mat BGRData(cv::Size(width, height), CV_8UC3);
	
	cv::cvtColor(YCbCrData, BGRData, CV_YUV2BGR_IYUV);
	
	return BGRData;
}

在上面的代码里,我假定了 widthheight 都是偶数,奇数的情形还请自行处理。此外,可以看到我这里对 YCbCr 和 YUV 不加区分的使用了,这两者根据场合还是存在不同的。fourcc 的网站上提供了比较详细的信息,可以参考 YUV pixel formatsYUV to RGB Conversion

在 vector 里使用 bool

Q: 我想在 std::vector 里使用 bool 可以么?
A: 当然是不可以!

Q: 为什么不可以?
A: 其实也可以……

Q: 一会儿可以一会儿不可以,别逗我行不行?
A: 你试试 &v[0] ……

Q: 为啥别的类型都行,但 std::vector<bool> 就不让 &v[0]
A: 因为 std::vectorbool 进行了特化, 把若干 bool 压缩存储在了整数类型的每个二进制位上,因此无法取地址。

Q: 好吧那怎么办……
A: 下面这段代码送给你,good luck and have fun!

struct boolean {
    boolean() = default;
    boolean(bool value) : value(value) {}
    operator bool&() { return value; }
    operator const bool&() const { return value; }
    bool* operator&() { return &value; }
    const bool* operator&() const { return &value; }
private:
    bool value;
};