第五章-几何变换
- one. 缩放:
- two. 翻转:
- three. 仿射:
- 1. 平移:
- 2. 旋转:
- 3. 更多复杂的仿射变换:
- four. 透视:
- five. 重映射:
- 1. 映射参数的理解:
- 2. 复制:
- 3. 绕x轴旋转:
- 4. 绕y轴旋转:
- 5. 绕x、y轴旋转:
- 6. x、y轴进行互换:
- 7. 图像缩放:
几何变换是指将一幅图像映射到另 一幅图像内的操作。OpenCV提供了多个与映射有关的函数。
根据OpenCV函数的不同,映射关系可以划分为缩放、翻转、仿射变换、透视、重映射等。
one. 缩放:
在OpenCV中,使用cv2.resize()实现对图像缩放:
-
dst = cv2.resize(src, dsize, [, fx [, fy [, interpolation]]])
- dst:代表输出的目标图像,该图像的类型与src相同,其大小为dsize(当该值非零时),或者可以通过src.size()、fx、fy计算得到。
- src:代表需要缩放的原始图像。
- dsize:代表输出图像大小。
- fx:代表水平方向的缩放比例。
- fy:代表垂直方向的缩放比例。
- interpolation:代表插值方式,具体如下表所示。
在cv2.resize()函数中,目标图像的大小可以通过"参数dsize"或者"参数fx和fy"二者之一来确定,具体情况如下:
-
通过参数dsize指定:
如果指定参数dsize的值,则无论是否指定了参数fx和fy的值,都由参数dsize来决定目标图像的大小。
此时需要注意的是,dsize内第1个参数对应缩放后图像的宽度(width,即列数cols,与参数fx相关),第2个参数对应缩放后图像的高度(height,即行数rows,与参数fy相关)。指定参数dsize的值时,x方向的缩放大小(参数fx)为:
- (double)dsize.width/src.cols
同时,y方向的缩放大小(参数fy)为:
- (double)dsize.height/src.rows
-
通过参数fx和fy指定:
如果参数dsize的值是None,那么目标图像的大小通过参数fx和fy来决定。此时,目标图像的大小为:dsize=Size(round(fx * src.cols),round(fy * src.rows))
插值 :
插值是指在对图像进行几何处理时,给无法直接通过映射得到值的像素点赋值。例如,将图像放大为原来的2倍,必然会多出一些无法被直接映射值的像素点,对于这些像素点,插值方式决定了如何确定它们的值。除此以外,还会存在一些非整数的映射值,例如,反向映射可能会把目标图像中的像素点值映射到原始图像中的非整数值对应的位置上,当然原始图像内是不可能存在这样的非整数位置的,即目标图像上的该像素点不能对应到原始图像的某个具体位置上,此时也要对这些像素点进行插值处理,以完成映射。
注意:
函数cv2.resize()能实现对原始图像的缩放功能,需要注意的是,开始运算前,操作前的目标图像dst自身的大小、类型与最终得到的目标图像dst是没有任何关系的。目标图像dst的最终大小和类型是通过src、dsize、fx、fy指定的。如果想让原始图像调整为和目标图像一样大,则必须通过上述属性指定。
当缩小图像时,使用区域插值方式(INTER_AREA)能够得到最好的效果;当放大图像时,使用三次样条插值(INTER_CUBIC)方式和双线性插值(INTER_LINEAR)方式都能够取得较好的效果。三次样条插值方式速度较慢,双线性插值方式速度相对较快且效果并不逊色。
例:
import cv2img = cv2.imread('../lena.bmp')
rows, cols = img.shape[:2]
size = (int(cols * 0.9), int(rows * 0.5))
rst = cv2.resize(img, size)
print('img.shape=', img.shape)
print('rst.shape=', rst.shape)
cv2.imshow('img', img)
cv2.imshow('rst', rst)
cv2.waitKey()
cv2.destroyAllWindows()# 输出结果
img.shape= (512, 512, 3)
rst.shape= (256, 460, 3)
two. 翻转:
在OpenCV中,图像的翻转采用函数cv2.flip()实现,该函数可以实现图像在水平方向翻转,垂直方向翻转,两个方向同时翻转。
-
dst = cv2.flip( src, filpCode )
- dst:代表和原始图像具有同样大小、类型的目标图像。
- src:代表要处理的原始图像。
- flipCode:代表旋转类型。该参数的意义如表所示。
该函数中,目标像素点与原始像素点的关系可表述为:
dstij={srcsrc.rows−i−1,j,filcCode=0srci,src.cols−j−1,filcCode>0srcsrc.rows−i−1,src.cols−j−1,filcCode<0dst_{ij} = \begin{cases} src_{src.rows-i-1, \quad j}, \quad filcCode = 0 \\ src_{i, \quad src.cols-j-1}, \quad filcCode > 0 \\ src_{src.rows-i-1, \quad src.cols-j-1}, \quad filcCode < 0 \end{cases}dstij=⎩⎪⎨⎪⎧srcsrc.rows−i−1,j,filcCode=0srci,src.cols−j−1,filcCode>0srcsrc.rows−i−1,src.cols−j−1,filcCode<0
其中,dst是目标像素点,src是原始像素点。
例:
import cv2img = cv2.imread('../lena.bmp')
x = cv2.flip(img, 0)
y = cv2.flip(img, 1)
xy = cv2.flip(img, -1)
cv2.imshow('img', img)
cv2.imshow('x', x)
cv2.imshow('y', y)
cv2.imshow('xy', xy)
cv2.waitKey()
cv2.destroyAllWindows()
three. 仿射:
仿射变换是指图像可以通过一系列的几何变换来实现平移、旋转等多种操作。该变换能够保持图像的平直性和平行性。平直性是指图像经过仿射变换后,直线仍然是直线;平行线是指图像在完成仿射变换后,平行线仍然是平行线。
在OpenCV中的实现仿射函数为cv2.warpAffine(),其通过一个变换矩阵(映射矩阵)M实现变换,具体为:dst(x,y)=src(M11x+M12y+M13,M13x+M22y+M23)dst(x, y) = src(M_{11}x + M_{12}y + M_{13}, M_{13}x + M_{22}y + M_{23} )dst(x,y)=src(M11x+M12y+M13,M13x+M22y+M23)
如图所示,可以通过一个变换矩阵M,将原始图像O变换为仿射图像R
因此,可以采用仿射函数cv2.warpAffine()实现对图像的平移、旋转等操作:
- dst = cv2.warpAffine( src, M, dsize [, flags [, borderMode [, borderValue ]]])
- dst:代表仿射后的输出图像,该图像的类型和原始图像的类型相同。dsize决定输出图像的实际大小。
- src:代表要仿射的原始图像。
- M:代表一个2×3的变换矩阵。使用不同的变换矩阵,就可以实现不同的仿射变换。
- dsize:代表输出图像的尺寸大小。
- flags:代表插值方法,默认为INTER_LINEAR。当该值为WARP_INVERSE_MAP时,意味着M是逆变换类型,实现从目标图像dst到原始图像src的逆变换。
- borderMode:代表边类型,默认为 BORDER_CONSTANT。当该值为 BORDER_TRANSPARENT 时,意味着目标图像内的值不做改变,这些值对应原始图像内的异常值。
- borderValue:代表边界值,默认是0。
通过以上分析可知,在OpenCV中使用函数cv2.warpAffine()实现仿射变换,忽略其可选参数后的语法格式为:
- dst=cv2.warpAffine(src ,M ,dsize)
其通过转换矩阵M将原始图像src转换为目标图像dst:
- dst(x,y)=src(M11x+M12y+M13,M13x+M22y+M23)dst(x, y) = src(M_{11}x + M_{12}y + M_{13}, M_{13}x + M_{22}y + M_{23} )dst(x,y)=src(M11x+M12y+M13,M13x+M22y+M23)
因此,进行何种形式的仿射变换完全取决于转换矩阵M。下面分别介绍通过不同的转换矩阵M实现的不同的仿射变换。
1. 平移:
通过转换矩阵M实现将原始图像src转换为目标矩阵dst:
- dst(x,y)=src(M11x+M12y+M13,M13x+M22y+M23)dst(x, y) = src(M_{11}x + M_{12}y + M_{13}, M_{13}x + M_{22}y + M_{23} )dst(x,y)=src(M11x+M12y+M13,M13x+M22y+M23)
将原始图像src向右侧移动100个像素,向下方移动200个像素其对应的关系为:
- dst(x, y) = src( x + 100, y + 200)
将上述表达式不成完整,即:
- dst(x, y) = src( 1 * x + 0 * y + 100, 0 * x + 1 * y + 200)
因此可以初定对饮的转换矩阵M中的各个元素值为:
- M11=1,M12=0,M13=100,M21=0,M22=1,M23=200M_{11} = 1, \quad M_{12} = 0, \quad M_{13} = 100, M_{21} = 0, \quad M_{22} = 1, \quad M_{23} = 200M11=1,M12=0,M13=100,M21=0,M22=1,M23=200
转换矩阵M:
- M=[1010001200]M = \begin{bmatrix} 1 & 0 & 100 \\ 0 & 1 & 200 \end{bmatrix}M=[1001100200]
import cv2
import numpy as npimg = cv2.imread('../lena.bmp')
h, w = img.shape[:2]
x = 100
y = 200
M = np.float32([[1, 0, x], [0, 1, y]])
move = cv2.warpAffine(img, M, (w, h))
cv2.imshow('img', img)
cv2.imshow('move', move)
cv2.waitKey()
cv2.destroyAllWindows()
2. 旋转:
在OpenCV中使用函数cv2.warpAffine()对图像进行旋转时,可以通过函数cv2.getRotationMatrix2D()获取转换矩阵。该函数的语法是:
- retval = cv2.getRotationMatrix2D( center, angle, scale )
- center:旋转的中心点
- angle:旋转的角度,正数表示逆时针旋转,负数表示顺时针旋转。
- scale:变换尺度(缩放大小)
利用函数cv2.getRotationMatrix2D()可以直接生成要使用的转换矩阵M。例如,想要以图像中心为原点,逆时针旋转45度,并将目标图像缩小为原始图像的0.6倍,则在调用函数cv2.getRotationMatrix2D()生成转换矩阵M时的语句为:
- M = cv2.getRotationMatrix2D( ( height/2, width/2 ), 45, 0.6 )
例如:根据上述要求进行图像旋转
import cv2lena = cv2.imread('../lena.bmp')
h, w = lena.shape[:2]
M = cv2.getRotationMatrix2D((h / 2, w / 2), 45, 0.6)
rotate = cv2.warpAffine(lena, M, (w, h))
cv2.imshow('lena', lena)
cv2.imshow('rotate', rotate)
cv2.waitKey()
cv2.destroyAllWindows()
3. 更多复杂的仿射变换:
前面讲了平移和旋转两种仿射变换都比较简单,对于更多复杂的仿射变换,OpenCV提供了函数cv2.getAffineTransFORM()来生成仿射函数cv2.warpAffine()所需要的转换矩阵M。该函数的语法格式是:
- retval = cv2.getAffineTransform(src, dst)
式中:
- src:代表输入图像的三个点坐标
- dst:代表输出图像的三个点坐标
在该函数中,其参数值src和dst是包含三个二维数组(x, y)点的数组。上述参数通过函数cv2.getAffineTransform()定义了两个平行四边形。src和dst中的三个点分别对应平行四边形左上角、右上角、左下角三个点。函数cv2.getAffineTransform()对所指定的点完成映射后,将所有其他点的映射关系按照指定点的关系计算确定。函数cv2.warpAffine()以函数cv2.getAffineTransform()获取的转换矩阵M为参数,将src中的点仿射到dst中。
例:
import cv2
import numpy as npimg = cv2.imread('../lena.bmp')
rows, cols, ch = img.shape
p1 = np.float32([[0, 0], [cols - 1, 0], [0, rows - 1]])
p2 = np.float32([[0, rows * 0.33], [cols * 0.85, rows * 0.25], [cols * 0.15, rows * 0.7]])
M = cv2.getAffineTransform(p1, p2)
dst = cv2.warpAffine(img, M, (cols, rows))
cv2.imshow('img', img)
cv2.imshow('dst', dst)
cv2.waitKey()
cv2.destroyAllWindows()
- 首先构造了两个三分量的点集合p1和p2,分别用来指代原始图像和目标图像内平行四边形的三个顶点(左上角、右上角、左下角)。
- 然后使用M=cv2.getAffineTransform(p1,p2)获取转换矩阵M。
- 接下来,dst=cv2.warpAffine(img,M,(cols,rows))完成了从原始图像到目标图像的仿射。
four. 透视:
仿射可以将矩形映射为任意的平行四边形,透视变换则可以将矩形映射为任意的四边形。
透视变换的函数是cv2.warpPerspective(),具体语法是:
- dst = cv2.warpPerspective( src, M, dsize [, flags [, borderMode [, borderValue]]])
- dst:代表透视处理后的输出图像,该图像和原始图像具有相同的类型。dsize决定输出图像的实际大小。
- src: 代表要透视的图像。
- M:代表一个3×3的变换矩阵。
- dsize:代表输出图像的尺寸大小。
- flags:代表插值方法,默认为INTER_LINEAR。当该值为WARP_INVERSE_MAP时,意味着M是逆变换类型,能实现从目标图像dst到原始图像src的逆变换。
- borderMode:代表边类型,默认为 BORDER_CONSTANT。当该值为 BORDER_TRANSPARENT时,意味着目标图像内的值不做改变,这些值对应原始图像内的异常值。
- borderValue:代表边界值,默认是0。
与仿射变换一样,同样使用一个函数来生成函数cv2.warpPerspective()所使用的转换矩阵。该函数是cv2.getPerspectiveTransform(),语法格式为:
- restval = cv2.getPerspectiveTransform( src, dst )
- src:代表输入图像的四个顶点的坐标
- dst:代表输出图像的四个顶点的坐标
需要注意的是,src 参数和 dst 参数是包含四个点的数组。实际使用中,我们可以根据需要控制src中的四个点映射到dst中的四个点。
例:
import cv2
import numpy as npimg = cv2.imread('../demo.bmp')
rows, cols = img.shape[:2]
pts1 = np.float32([[150, 50], [400, 50], [60, 450], [310, 450]])
pts2 = np.float32([[50, 50], [rows-50, 50], [50, cols-50], [rows-50, cols-50]])
M = cv2.getPerspectiveTransform(pts1, pts2)
dst = cv2.warpPerspective(img, M, (cols, rows))
cv2.imshow('img', img)
cv2.imshow('dst', dst)
cv2.waitKey()
cv2.destroyAllWindows()
- 指定原始图像中平行四边形的四个顶点pts1,指定目标图像中矩形的四个顶点pts2.
- 使用 M=cv2.getPerspectiveTransform(pts1,pts2)生成转换矩阵 M。
- 接下来,使用语句dst=cv2.warpPerspective(img,M,(cols,rows))完成从平行四边形到矩形的转换。
five. 重映射:
把一幅图像内的像素点映射到另一幅图像的指定位置,这个过程称为重映射。OpenCV中有多种重映射的的方式,但是我们有时希望使用自定义的方式来完成重映射。
OpenCV内重映射函数cv2.remap()提供了更方便、更自由的映射方式,其语法格式如下:
-
dst = cv2.remap( src, map1, map2, interpolation [, borderMode [, borderValue]])
- dst:代表目标图像,它和src具有相同的大小和类型。
- src:代表原始图像。
- map1:参数有两种可能的值:
- 表示(x,y)点的一个映射。
- 表示CV_16SC2 ,CV_32FC1,CV_32FC2类型(x,y)点的x值。
- map2:参数同样有两种可能的值:
- 当map1表示(x,y)时,该值为空。
- 当map1表示(x,y)点的x值时,该值是CV_16UC1,CV_32FC1类型(x,y)点的y值。
- Interpolation:代表插值方式,这里不支持INTER_AREA方法。
- borderMode :代表边界模式。当该值为BORDER_TRANSPARENT时,表示目标图像内的对应源图像内奇异点(outliers)的像素不会被修改。
- borderValue :代表边界值,该值默认为0。
1. 映射参数的理解:
**重映射通过修改像素点的位置得到一幅新图像。在构建新图像时,需要确定新图像中每个像素点在原始图像中的位置。因此,映射函数的作用是查找新图像像素在原始图像内的位置。该过程是将新图像像素映射到原始图像的过程,因此被称为反向映射。**在函数 cv2.remap()中,参数map1和参数map2用来说明反向映射,map1针对的是坐标x,map2针对的是坐标y。
需要说明的是,map1和map2的值都是浮点数。因此,目标图像可以映射回一个非整数的值,这意味着目标图像可以“反向映射”到原始图像中两个像素点之间的位置(当然,该位置是不存在像素值的)。这时,可以采用不同的方法实现插值,函数中的interpolation参数可以控制插值方式。正是由于参数map1和参数map2的值是浮点数,所以通过函数cv2.remamp()所能实现的映射关系变得更加随意,可以通过自定义映射参数实现不同形式的映射。
需要注意的是,函数cv2.remap()中参数map1指代的是像素点所在位置的列号,参数map2指代的是像素点所在位置的行号。**例如,我们想将目标图像(映射结果图像)中某个点A映射为原始图像内处于第0行第3列上的像素点B,那么需要将A点所对应的参数map1对应位置上的值设为3,参数map2对应位置上的值设为0。**所以,通常情况下,我们将map1写为mapx,并且将map2写成mapy,以方便理解。
同样,如果想将目标图像(映射结果图像)中所有像素点都映射为原始图像内处于第0行第3列上的像素点B,那么需要将参数map1内的值均设为3,将参数map2内的值均设为0。
例:
- 用来指定列的参数map1(mapx)内的值均为3。
- 用来指定行的参数map2(mapy)内的值均为0。
import cv2
import numpy as npimg = np.random.randint(0, 256, size=[4, 5], dtype=np.uint8)
# rows, cols = img.shape
mapx = np.ones(img.shape, np.float32) * 3
mapy = np.ones(img.shape, np.float32) * 0
rst = cv2.remap(img, mapx, mapy, cv2.INTER_LINEAR)
print('img=\n', img)
print('mapx=\n', mapx)
print('mapy=\n', mapy)
print('rst=\n', rst)# 输出结果
img=[[196 22 15 82 189][228 208 215 88 113][159 6 157 101 174][181 95 194 206 111]]
mapx=[[3. 3. 3. 3. 3.][3. 3. 3. 3. 3.][3. 3. 3. 3. 3.][3. 3. 3. 3. 3.]]
mapy=[[0. 0. 0. 0. 0.][0. 0. 0. 0. 0.][0. 0. 0. 0. 0.][0. 0. 0. 0. 0.]]
rst=[[82 82 82 82 82][82 82 82 82 82][82 82 82 82 82][82 82 82 82 82]]
2. 复制:
为了更好地了解重映射函数cv2.remap()的使用方法,下面介绍如何通过该函数实现图像的复制。在映射时,将参数做如下处理:
- 将map1的值设定为对应位置上的x轴的坐标轴
- 将map2的值设定为对应位置上的y轴的坐标轴
将map1的值设定为对饮位置上的x轴的坐标轴。
例:
import cv2
import numpy as npimg = cv2.imread('../lena.bmp')
rows, cols = img.shape[:2]
mapx = np.zeros(img.shape[:2], np.float32)
mapy = np.zeros(img.shape[:2], np.float32)
for i in range(rows):for j in range(cols):mapx.itemset((i, j), j)mapy.itemset((i, j), i)
rst = cv2.remap(img, mapx, mapy, cv2.INTER_LINEAR)
cv2.imshow('img', img)
cv2.imshow('rst', rst)
cv2.waitKey()
cv2.destroyAllWindows()
3. 绕x轴旋转:
映射过程中如果要绕x轴旋转:
- x坐标轴保持不变
- y坐标轴的值以x轴为对称轴进行交换。
反映在map1, map2上:
- map1值保持不变
- map2的值调整为“总行数-1-当前行号”。
需要注意,OpenCV 中行号的下标是从0开始的,所以在对称关系中存在“当前行号+对称行号=总行数-1”的关系。据此,在绕着x轴翻转时,map2中当前行的行号调整为“总行数-1-当前行号”。
import cv2
import numpy as npimg = cv2.imread('../lena.bmp')
rows, cols = img.shape[:2]
mapx = np.zeros(img.shape[:2], np.float32)
mapy = np.zeros(img.shape[:2], np.float32)
for i in range(rows):for j in range(cols):mapx.itemset((i, j), j)mapy.itemset((i, j), rows-1-i)
rst = cv2.remap(img, mapx, mapy, cv2.INTER_LINEAR)
cv2.imshow('img', img)
cv2.imshow('rst', rst)
cv2.waitKey()
cv2.destroyAllWindows()
4. 绕y轴旋转:
映射过程中如果要绕y轴旋转:
- y坐标轴保持不变
- x坐标轴的值以y轴为对称轴进行交换。
反映在map1, map2上:
- map2值保持不变
- map1的值调整为“总列数-1-当前列号”。
需要注意,OpenCV 中行号的下标是从0开始的,所以在对称关系中存在“当前列号+对称列号=总列数-1”的关系。据此,在绕着y轴翻转时,map1中当前列的列号调整为“总列数-1-当前列号”。
import cv2
import numpy as npimg = cv2.imread('../lena.bmp')
rows, cols = img.shape[:2]
mapx = np.zeros(img.shape[:2], np.float32)
mapy = np.zeros(img.shape[:2], np.float32)
for i in range(rows):for j in range(cols):mapx.itemset((i, j), cols - j -1)mapy.itemset((i, j), i)
rst = cv2.remap(img, mapx, mapy, cv2.INTER_LINEAR)
cv2.imshow('img', img)
cv2.imshow('rst', rst)
cv2.waitKey()
cv2.destroyAllWindows()
5. 绕x、y轴旋转:
映射过程中如果要绕x、y轴旋转:
- x坐标轴的值以y轴为对称轴进行交换。
- y坐标轴的值以x轴为对称轴进行交换。
反映在map1, map2上:
- map1的值调整为“总列数-1-当前列号”
- map2的值调整为“总行数-1-当前行号”。
import cv2
import numpy as npimg = cv2.imread('../lena.bmp')
rows, cols = img.shape[:2]
mapx = np.zeros(img.shape[:2], np.float32)
mapy = np.zeros(img.shape[:2], np.float32)
for i in range(rows):for j in range(cols):mapx.itemset((i, j), cols - j - 1)mapy.itemset((i, j), rows - i - 1)
rst = cv2.remap(img, mapx, mapy, cv2.INTER_LINEAR)
cv2.imshow('img', img)
cv2.imshow('rst', rst)
cv2.waitKey()
cv2.destroyAllWindows()
6. x、y轴进行互换:
如果想让图像的x轴、y轴互换,意味着在映射过程中,对于任意一点,都需要将其x轴、y轴坐标互换。反映在mapx和mapy上:
- mapx的值调整为所在行的行号。
- mapy的值调整为所在列的列号。
需要注意的是,如果行数和列数不一致,上述运算可能存在值无法映射的情况。默认情况下,无法完成映射的值会被处理为0。
import cv2
import numpy as npimg = cv2.imread('../lena.bmp')
rows, cols = img.shape[:2]
mapx = np.zeros(img.shape[:2], np.float32)
mapy = np.zeros(img.shape[:2], np.float32)
for i in range(rows):for j in range(cols):mapx.itemset((i, j), i)mapy.itemset((i, j), j)
rst = cv2.remap(img, mapx, mapy, cv2.INTER_LINEAR)
cv2.imshow('img', img)
cv2.imshow('rst', rst)
cv2.waitKey()
cv2.destroyAllWindows()
7. 图像缩放:
前面的映射都是直接完成整数映射,处理起来比较方便。在处理更复杂的问题时,就需要对行、列值进行比较复杂的运算来实现。
例如:使用cv2.remap()缩小图像。
缩小图像后,可以将图像固定在围绕其中心的某个区域。例如,将其x轴、y轴设置为:
- 在目标图像的x轴(0.25·x轴长度,0.75·x轴长度)区间内生成缩小图像;x轴其余区域的点取样自x轴上任意一点的值。
- 在目标图像的y轴(0.25·y轴长度,0.75·y轴长度)区间内生成缩小图像;y轴其余区域的点取样自y轴上任意一点的值。
为了处理方便,我们让不在上述区域的点都取(0,0)坐标点的值。
import cv2
import numpy as npimg = cv2.imread('../11111.png')
print(img.shape)
rows, cols = img.shape[:2]
mapx = np.zeros(img.shape[:2], np.float32)
mapy = np.zeros(img.shape[:2], np.float32)
for i in range(rows):for j in range(cols):if 0.25 * cols < j < 0.75 * cols and 0.25 * rows < i < 0.75 * rows:mapx.itemset((i, j), 2 * (j - cols * 0.25) + 0.5)mapy.itemset((i, j), 2 * (i - rows * 0.25) + 0.5)else:mapx.itemset((i, j), 0)mapy.itemset((i, j), 0)
rst = cv2.remap(img, mapx, mapy, cv2.INTER_LINEAR)
cv2.imshow('img', img)
cv2.imshow('rst', rst)
cv2.waitKey()
cv2.destroyAllWindows()