【译】Learn OpenCV之WarpTriangle

这篇文章将讲述的是如何将一个图片内的三角形内容映射到另一个图片内的不同形状的三角形内。

在图形学的研究中,研究者常常进行三角形之间的变换操作,因为任意的3D表面都可以用多个三角形去近似表示。同样的,图片也可以分解成多个三角形来表示。但是在OpenCV中并没有一个直接可以将三角形转换为另一个三角形的函数。

下面将详细的讲述,如果将下图中的左图转化成右图。


warp-triangle-opencv-1024x510.jpg

在此之前,先介绍一个什么是仿射变换。

什么是仿射变换

仿射变换是一种将一个三个点的点集(例如三角形)变换到另一个三点点集的最简单的变换方法。它包括平移、缩放、旋转和剪切操作。下图展示了对一个正方形做仿射变换的效果。但是仿射变换还是有一定局限性的,它不能将一个正方形形变为任意的四边形。一对平行的两条边经过放射变换后仍是平行的。


motion-models.jpg

在OpenCV中,仿射变换可以用一个2 \times 3的矩阵表示,这个矩阵的前两列表示旋转、缩放、裁剪操作,后两列表示平移操作。如下公式所示
S = \left[ \begin{matrix} a & b & t_{x} \\ c & d & t_{y} \end{matrix} \right ]
假设现在给定一点其坐标为(x, y),通过上述仿射变换得到点坐标(x_{t}, y_{t})如下式所示
\left[\begin{matrix}x_{t} \\ y_{t}\end{matrix}\right] =\left[\begin{matrix}a & b \\ c & d\end{matrix}\right] \left[\begin{matrix}x \\ y\end{matrix}\right]+ \left[\begin{matrix}t_{x} \\ t_{y}\end{matrix}\right]

使用OpenCV完成三角形的仿射变换

在OpenCV中,函数warpAffine可以对一张图片进行仿射变换,但是不能直接对图片中的三角形做仿射变换。为了完成三角形的仿射变换,可以在图片中先找到三角形的外界矩形并将它从图片中截取出来,然后将截取的矩形进行仿射变换从而获得输出图像。(其实可以不用截取三角形的外接矩形也能完成,但是使用全图会增加计算量。)最后我们制作一个蒙版(mask),蒙版大小和仿射变换输出图像大小一样,蒙版里需要进行输出的三角形区域值为1其他为0,这样将蒙版与输出图片相乘就可以得到最终的三角形输出了。

下面进行代码的详细讲解,在这里输出图片是一个全白的图片,你也可以将仿射变换后的三角形放置在其他图片中。

C++

// Read input image and convert to float
Mat img1 = imread("robot.jpg");
img1.convertTo(img1, CV_32FC3, 1/255.0);

// Output image is set to white
Mat img2 = Mat::ones(imgIn.size(), imgIn.type());
imgOut = Scalar(1.0, 1.0, 1.0);

// Input triangle
vector <Point2f> tri1;
tri1.push_back(Point2f(360, 200));
tri1.push_back(Point2f(60, 250));
tri1.push_back(Point2f(450, 400));

// Output triangle
vector <Point2f> tri2;
tri2.push_back(Point2f(400, 200));
tri2.push_back(Point2f(160, 270));
tri2.push_back(Point2f(400, 400));

Python

# Read input image
img1 = cv2.imread("robot.jpg")

# Output image is set to white
img2 = 255 * np.ones(img1.shape, dtype = img1.type)

# Define input and output triangles 
tri1 = np.float32([[[360,200], [60,250], [450,400]]])
tri2 = np.float32([[[400,200], [160,270], [400,400]]])

现在输入输出图像,还有要进行仿射变换的三角形已经完成定义了,下面开始进行三角形仿射变换的操作:

1.求三角形的外界矩形

这步可做可不做,主要是为了提高仿射变换的效率,减少计算量。

C++

// Find bounding rectangle for each triangle
Rect r1 = boundingRect(tri1);
Rect r2 = boundingRect(tri2);

Python

# Find bounding box. 
r1 = cv2.boundingRect(tri1)
r2 = cv2.boundingRect(tri2)
2.截取外界矩形并修改三角形的坐标值

因为要在上面获得的外接矩形中进行仿射变换,所以原点从图片的左上角点变成了外接矩形的左上角点,从而要修改三角形的三点坐标。

C++

// Offset points by left top corner of the respective rectangles
vector<Point2f> tri1Cropped, tri2Cropped;
vector<Point2f> tri2CroppedInt;

for(int i = 0; i < 3; i++)
{
    tri1Cropped.push_back(Point2f(tri1[i].x - r1.x, tri1[i].y - r1.y));
    tri2Cropped.push_back(Point2f(tri2[i].x - r2.x, tri2[i].y - r2.y));

    // fillConvexPoly needs a vector of Point and not Point2f
    tri2CroppedInt.push_back(Point((int)(tri2[i].x - r2.x),(int)(tri2[i].y - r2.y)));
}

// Apply warpImage to small rectangular patches
Mat img1Cropped;
img1(r1).copyTo(img1Cropped);

Python

# Offset points by left top corner of the 
# respective rectangles

tri1Cropped = []
tri2Cropped = []

for i in xrange(0, 3):
    tri1Cropped.append(((tri1[0][i][0] - r1[0]),(tri1[0][i][1] - r1[1])))
    tri2Cropped.append(((tri2[0][i][0] - r2[0]),(tri2[0][i][1] - r2[1])))

# Apply warpImage to small rectangular patches
img1Cropped = img1[r1[1]:r1[1] + r1[3], r1[0]:r1[0] + r1[2]]
3.获取仿射变换矩阵

通过上述得到的输入三角形的三个坐标和输出三角形的三个坐标,可以计算出仿射变换要使用的仿射变换矩阵。

C++

// Given a pair of triangles, find the affine transform.
Mat warpMat = getAffineTransform(tri1Cropped, tri2Cropped);

Python

# Given a pair of triangles, find the affine transform.
warpMat = cv2.getAffineTransform(np.float32(tri1Cropped), np.float32(tri2Cropped))
4.对外接矩形做仿射变换

通过OpenCV中的warpAffine函数完成。

C++

// Apply the Affine Transform just found to the src image
Mat img2Cropped = Mat::zeros(r2.height, r2.width, img1Cropped.type());
warpAffine(img1Cropped, img2Cropped, warpMat, img2Cropped.size(), INTER_LINEAR, BORDER_REFLECT_101);

Python

# Apply the Affine Transform just found to the src image
img2Cropped = cv2.warpAffine(img1Cropped, warpMat, (r2[2], r2[3]), None, flags=cv2.INTER_LINEAR, borderMode=cv2.BORDER_REFLECT_101 ))
5.利用蒙版获取最终的三角形仿射变换结果

蒙版可以先建立一个值全为0的图片,然后利用fillConvexPoly函数将要获取的三角形区域填充为1,最后将蒙版与上个步骤输出的图片结果相乘就可以得到对应区域的值了。

C++

// Get mask by filling triangle
Mat mask = Mat::zeros(r2.height, r2.width, CV_32FC3);
fillConvexPoly(mask, tri2CroppedInt, Scalar(1.0, 1.0, 1.0), 16, 0);

// Copy triangular region of the rectangular patch to the output image
multiply(img2Cropped, mask, img2Cropped);
multiply(img2(r2), Scalar(1.0, 1.0, 1.0) - mask, img2(r2));
img2(r2) = img2(r2) + img2Cropped;

Python

# Get mask by filling triangle
mask = np.zeros((r2[3], r2[3], 3), dtype = np.float32)
cv2.fillConvexPoly(mask, np.int32(tri2Cropped), (1.0, 1.0, 1.0), 16, 0)

# Apply mask to cropped region
img2Cropped = img2Cropped * mask

# Copy triangular region of the rectangular patch to the output image
img2[r2[1]:r2[1]+r2[3], r2[0]:r2[0]+r2[2]] = img2[r2[1]:r2[1]+r2[3], r2[0]:r2[0]+r2[2]] * ( (1.0, 1.0, 1.0) - mask )
     
img2[r2[1]:r2[1]+r2[3], r2[0]:r2[0]+r2[2]] = img2[r2[1]:r2[1]+r2[3], r2[0]:r2[0]+r2[2]] + img2Cropped

到这里三角形的仿射变换就完成了。

代码下载
原博客-Warp one triangle to another using OpenCV ( C++ / Python )

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,240评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,328评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,182评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,121评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,135评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,093评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,013评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,854评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,295评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,513评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,678评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,398评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,989评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,636评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,801评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,657评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,558评论 2 352

推荐阅读更多精彩内容