SynthText是OCR领域生成数据集非常经典,且至今看来无人超越的方法。整体可以分为三个大的步骤,分别是生成文字的mask,这里用到了图像的分割信息来选取区域,图像的深度信息来拟合该区域的3D平面,单应性变换来旋转平面;文字上色,用于选取适合背景色的文字颜色;图像融合,用来了泊松图像融合。
整体思路是非常棒的,挑不出毛病。目前有些文本贴图不真实,主要原因是分割的区域不是同一个平面内的区域,加之图像的深度信息也不准,导致了3D平面拟合不准,文字贴上去也就不准了。
这部分需要用到图像的分割信息和深度信息,示意图如图2-1所示。
在当前的场景当中,最好的分割结果是可以把同一个物体的不同平面分割出来;次好的结果是把同一个平面分割出来,可以是不同物体。比如一座房子,不同的墙最好可以单独分开,两座平行的房子上平行的墙,分割在一起也是可以的。对于曲面,整个分割成一个instance就可以了,这个可以通过无法拟合成一个平面来删除。
最好的深度信息,就是准确的深度信息。
xyz = su.DepthCamera.depth2xyz(depth)
这步就是将深度信息转化为空间3D坐标。
regions = TextRegions.get_regions(xyz,seg,area,label)
会对分割区域进行筛选,得到可用于分割的区域。
筛选分为两步:
(1)regions = TextRegions.filter(seg,area,label)
筛除面积过小,或者长宽比极端的区域
(2)regions = TextRegions.filter_depth(xyz,seg,regions)
筛除区域内的点不能构成一个平面的区域,也就是曲面或者多个面,这里会得到平面的拟合结果
regions = self.filter_for_placement(xyz,seg,regions)
对图2-2中的区域进行warp的变换,并获取warp变换所使用的单应性变换矩阵。
这部分有些难理解,我这里举个例子。比如我们有原图2-3。
该区域拟合的平面为图2-6。
将图2-6中的平面绕中点转正,也就是转到平面的法向量对准我们,即法向量与Y轴平行。记录下这个变换过程所用到的单应性变换矩阵及其逆变换矩阵。转正后的图片如下图2-7所示。
我们这里得到的单应性变换矩阵就是图2-4变为图2-8的矩阵。接下来只要往图2-8上贴上文字,然后整体变回图2-4就可以了。
这部分直接关系到了文字贴上图片能否有背景在同一个空间的效果。有许多的深度信息和分割信息是不准的,如果可以优化的话,可以有更好的效果。拿自动驾驶的数据集去当背景图应该是个不错的选择。
在将预估出来的平面转正的时候,发现转出来的z的值总是不同的,这和我们的理解是由偏差了。查了半天问题,发现是place2xyz代码中的两个系数写反了。修正后的代码为
@staticmethod
def plane2xyz(center, ij, plane):
"""
converts image pixel indices to xyz on the PLANE.
center : 2-tuple
ij : nx2 int array
plane : 4-tuple
return nx3 array.
"""
ij = np.atleast_2d(ij)
n = ij.shape[0]
ij = ij.astype('float')
xy_ray = (ij-center[None,:]) / DepthCamera.f
z = -plane[3]/(xy_ray.dot(plane[:2])+plane[2])
# z = -plane[2]/(xy_ray.dot(plane[:2])+plane[3])
xyz = np.c_[xy_ray, np.ones(n)] * z[:,None]
return xyz
接下来解释一下为何如此。如下图2-9,我们的目的是将左边图像平面上的点,转换到右侧的实际平面上,中间的原点是小孔,原点到左侧图像的距离为焦距。实际平面就是代码中的plane
,它有四个值,分别是 a , b , c , d a, b, c, d a,b,c,d,表示平面 a x + b y + c z + d = 0 ax+by+cz+d=0 ax+by+cz+d=0。
图像上点 ( x i , y i ) (x_i, y_i) (xi,yi)经过原点的直线可以表示为
x − 0 x i − 0 = y − 0 y i − 0 = z − 0 z i − 0 (2-1) \frac{x-0}{x_i - 0} = \frac{y-0}{y_i - 0} = \frac{z-0}{z_i - 0} \tag{2-1} xi−0x−0=yi−0y−0=zi−0z−0(2-1)
于是可以有
x = x i z z i , y = y i z z i , z i = f (2-2) x = \frac{x_iz}{z_i}, y = \frac{y_iz}{z_i}, z_i=f \tag{2-2} x=zixiz,y=ziyiz,zi=f(2-2)
将式 ( 2 − 2 ) (2-2) (2−2)代入 a x + b y + c z + d = 0 ax+by+cz+d=0 ax+by+cz+d=0,计算直线与planar scene的交点可以得到
z = − d a x i f + b y i f + c (2-3) z = -\frac{d}{a\frac{x_i}{f} + b\frac{y_i}{f} + c} \tag{2-3} z=−afxi+bfyi+cd(2-3)
代码中plane[2]
就是 c c c,plane[3]
就是 d d d,所有原来是反的。
代码中的xy_ray
就是 x i f \frac{x_i}{f} fxi和 y i f \frac{y_i}{f} fyi。要求 x x x和 y y y,再利用式 ( 2 − 2 ) (2-2) (2−2)就可以了,这也就有了
xyz = np.c_[xy_ray, np.ones(n)] * z[:,None]
把这行代码改了,有奇效!
我们往图2-8上贴文字的时候,需要确定文字的字体和颜色,字体随机即可,文字的颜色是需要根据背景色来确定的。
render_res = self.text_renderer.render_sample(font,collision_mask)
就是贴文字的过程。这部分的位置选取,字体选取,文字尺寸什么的就不说了,说一下这个文字颜色的选取。
选取颜色的过程在 def color_text(self, text_arr, h, bg_arr)
函数当中,这里依赖于一个文件 colors_new.cp
,这个文件中存储的是shape为 (4941, 12)
的array,为表示方便称为colors
。colors[:, :6]
表示颜色A的RGB值和各通道方差,colors[:, 6:12]
表示颜色B的RGB值和各通道方差。表示背景色为A时,文字颜色可以用B;背景色为B时,文字颜色可以用A。
在寻找最匹配的背景色的时候,使用了LAB颜色空间,也就是将所有的RGB值转为到了LAB空间,然后取候选区域的所有LAB值的平均作为背景色LAB色,然后根据LAB色的距离来选取最合适的RGB值。方差就是取值的时候,加上方差,来得到多样性的作用。
这部分是可以优化的,背景色直接取LAB平均也太暴力了,万一颜色比较花,或者有极端值,就会影响很大。可以改成聚类或者mmcq的方式来获取主体色作为背景色。
直到上述部分,文字仍旧还是和背景分离的,得到带有文本的图像和贴图的区域之后,使用了泊松融合的方式来自然融合两图,这里不推荐使用cv2.seamlessClone
,因为cv2.seamlessClone
暴露的参数太少了,作者自己写的blit_images
就非常好,scale_grad
这个参数可以用来调整前景的梯度,也就是前景贴到背景上的明显程度。
使用不同的 scale_grad
参数和 cv2.seamlessClone
的效果如下图2-11所示,cv2.seamlessClone
甚至丢失掉了边界。
将文本贴到warp过的区域之后,就可以使用单应性变换逆矩阵,把warp的区域还原回去,得到最终的效果图。
这么好的效果,其实是挑选过结果的,实际情况下,还是有很多都是效果没那么出色的。怪就怪分割和深度信息不准。
[1] Synthetic Data for Text Localisation in Natural Images
[2] https://github.com/ankush-me/SynthText