旋转傻乌龟——几何变换实践

这两天新型肺炎病例是指数上升啊!呆在家里没事干,正好想起之前FPGA大赛上有个老哥做了一个图像旋转作品,还在群里发了技术报告。无聊之下就打算学习一下,然后就顺便把平移、旋转、缩放这些几何变换都看了,最后决定把这三个综合起来写个“旋转傻乌龟”的动画。先是用OpenCV内置函数实现了下,感觉不过瘾,又自己写了一遍。老规矩,还是把学过的、做过的东西记录下来!

旋转傻乌龟,效果就是将一只乌龟在窗口中同时进行平移、缩放和旋转,由于最后看起来样子比较傻,因此得名“旋转傻乌龟”。

效果视频:

                

一、几何变换的矩阵表示


1.1 平移的表示

   

上图中的三种表示方法第二种是OpenCV要求的方式,但第一种形式表示起来更具统一性,因此我更倾向于第一种。但无论哪一种,都能展开成第三种的形式。第三种非常直观的反映了平移,只是需要注意正负号的选取——在编程中,图像一般以左上角为(0,0)点。这也就是说,建立坐标系的时候,X轴以右正方向,Y轴以下为正方向。以上矩阵表示将图像向右平移x0,向下平移y0,也可以认为是将坐标系向左平移x0,向上平移y0。平移可以形象地表示如下:

    

1.2 以左上角为定点缩放的表示

    

缩放最容易理解,就是将横纵坐标乘以缩放比例。由于我们以左上角为坐标系原点,所以左上角点的位置并不会变化。

1.3 以左上角点为中心旋转的表示

 

在本文中,规定顺时针方向旋转,θ为正;逆时针旋转,θ为负。旋转前后的坐标关系推导也不难,如下图所示,旋转前先求出旋转半径L,旋转后根据L求出坐标。

为了之后表述的简洁,我们将这三节中的矩阵分别用特定符号简记:

    

1.4 以任一点为中心旋转的表示

有了以上的基础,我们就可以研究更加复杂的变换。例如我们想以任一点(x0,y0)为中心旋转,而我们推导的R(θ)只适用于以坐标系原点为中心旋转。因此,我们可以将图像向上平移x0,向左平移y0,使(x0,y0)点平移到坐标系原点;然后再旋转,旋转完后再向下平移x0,向右平移y0回到原来位置,这一过程可用三个基础基础矩阵表示成如下形式,注意三个矩阵顺序不能调换。

1.5 以任一点为定点缩放的表示

方法同1.4节的旋转,可以表示为下面形式。除此之外,还可以在此基础上进行旋转平移,只要在左边依次乘上相应矩阵即可。

二、旋转傻乌龟OpenCV函数实现



OpenCV提供了仿射变换函数warpAffine。在输入参数中,M表示变换矩阵,可以是平移、旋转和缩放矩阵等;dsize是输入图像的大小;flags是插值方式,一般采用默认的双线性插值。

至于M的获取,平移矩阵只能自己构造;二旋转矩阵可以由函数getRotationMatrix2D得到。输入参数中,center表示旋转中心的坐标;angle为旋转角度,逆时针为正;scale是缩放比例。可见这个函数同时包揽了旋转和缩放的功能。

我的思路是,用正弦函数生成一系列轨迹点,乌龟每到达一个轨迹点,就旋转一定角度,缩放一定比例,而轨迹点的跟踪就是乌龟中心的平移。根据之前的说的原理,我们先让整个图像绕自身中心旋转和缩放,缩放后的乌龟应该是在整个图像的中间,为了让它中心和轨迹重合,就使用平移变换,此时平移的距离应该是path-center。整个过程的代码如下:

 1 import cv2
 2 import numpy as np
 3 import time
 4
 5 img = cv2.imread(‘image/turtle.jpg‘)
 6 size = img.shape[:-1]
 7 cv2.namedWindow(‘img‘)
 8
 9 #平移矩阵
10 def GetMoveMatrix(x,y):
11     M = np.zeros((2, 3), dtype=np.float32)
12
13     M.itemset((0, 0), 1)
14     M.itemset((1, 1), 1)
15     M.itemset((0, 2), x)
16     M.itemset((1, 2), y)
17
18     return M
19
20 if __name__ == ‘__main__‘:
21
22     # shape和坐标是颠倒的
23     center_x = size[1]/2
24     center_y = size[0]/2
25     #计时
26     start_time = time.time()
27
28     for x in np.linspace(0,2*np.pi,100):
29         #角度、缩放
30         angle = -360*x/2/np.pi
31         scale = 0.2+0.2*np.sin(x)
32         #轨迹
33         path_x = x*50+100
34         path_y = (np.sin(x)+1)*100+100
35         #旋转、平移矩阵
36         M1 = cv2.getRotationMatrix2D((center_x, center_y), angle, scale)
37         M2 = GetMoveMatrix(path_x-center_x,path_y-center_y)
38         #仿射变换
39         rotate = cv2.warpAffine(img,M1,size)
40         dst = cv2.warpAffine(rotate,M2,size)
41
42         # cv2.imshow(‘img‘,dst)
43         # cv2.waitKey(1)
44     #花费125ms
45     print(time.time()-start_time)

三、旋转傻乌龟自实现



这个自己用Python实现的话,性能就相当重要了,尤其是双线性插值,如果不优化的话,慢得简直可以让你怀疑人生。比如,一般的是用两个for循环迭代,代码如下。在这个项目里,这个函数执行一次需要花费1.4s的时间。所以不优化的话,这只乌龟真的是名副其实了!

 1 def InterLinearMap(img,size,mapx,mapy):
 2
 3     dst = np.zeros(img.shape,dtype=np.uint8)
 4
 5     for row in range(size[0]):
 6         for col in range(size[1]):
 7
 8             intx = np.int32(mapx.item(row,col))
 9             inty = np.int32(mapy.item(row,col))
10             partx = mapx.item(row,col)-intx
11             party = mapy.item(row,col)-inty
12             resx = 1-partx
13             resy = 1-party
14
15             if party==0 and partx==0:
16                 result=img[inty,intx]
17             else:
18                 result = ((img[inty,intx]*resx+img[inty,intx+1]*partx)*resy
19                           +(img[inty+1,intx]*resx+img[inty+1,intx+1]*partx)*party)
20
21             dst[row,col]=np.uint8(result+0.5)
22
23     return dst

那怎么办?网上有一些优化的方法,主要是将浮点运算转成整数运算,这个方法对于FPGA这样的逻辑器件最适合不过了——但别忘了,我现在用的是Python,整数运算实际上也会被转成浮点运算,所以这个方法显然不适用。我采用的优化是进行矩阵化,据我所知,很多编程语言只要是支持矩阵运算的,其运算都是优化过的。对于双线性插值和仿射变换,运用矩阵也是很合适,只是写起来会有点抽象。。。

首先,先把生成变换矩阵的函数写出来,代码如下。要注意numpy的三角函数接受的参数是弧度制。

 1 #缩放矩阵
 2 def GetResizeMatrix(scalex,scaley):
 3     M = np.zeros((3,3),dtype=np.float32)
 4
 5     M.itemset((0,0),scalex)
 6     M.itemset((1,1),scaley)
 7     M.itemset((2,2),1)
 8
 9     return M
10 #平移矩阵
11 def GetMoveMatrix(x,y):
12     M = np.zeros((3, 3), dtype=np.float32)
13
14     M.itemset((0, 0), 1)
15     M.itemset((1, 1), 1)
16     M.itemset((2, 2), 1)
17     M.itemset((0, 2), x)
18     M.itemset((1, 2), y)
19
20     return M
21 #旋转矩阵
22 def GetRotationMatrix(angle):
23     M = np.zeros((3, 3), dtype=np.float32)
24
25     M.itemset((0, 0), np.cos(angle))
26     M.itemset((0, 1), -np.sin(angle))
27     M.itemset((1, 0), np.sin(angle))
28     M.itemset((1, 1), np.cos(angle))
29     M.itemset((2, 2), 1)
30
31     return M

接下来写仿射变换函数,输入参数为图片数据、变换矩阵和输入图片的大小。这里应该要有逆向思维——现在我要得到变换后的图片,就是要求各坐标位置上的色彩,而色彩取样自变换前图像上的一点(这点的坐标可能不是整数),也就是说我们要将变换后的坐标映射到变换前的坐标。再来看之前的公式(下图左,为了方便,将变换矩阵合成为一个矩阵A),现在我们已知的是左边部分,而要求的映射是等式右边的XY,因此我们将A拿到左边,得到另一个公式(下图右),并依据这个公式,写出仿射变换函数。

       

 1 def WarpAffine(img,Mat,size):
 2
 3     rows = size[0]
 4     cols = size[1]
 5     #生成矩阵[X Y 1]
 6     ones = np.ones((rows, cols), dtype=np.float32)
 7     #gridx/gridy -> shape(rows,cols)
 8     gridx,gridy= np.meshgrid(np.arange(0, cols),np.arange(0, rows))
 9     #dst -> shape(3,rows,cols)
10     dst = np.stack((gridx, gridy, ones))
11
12     #求逆矩阵 M -> shape(3,3)
13     Mat = np.linalg.inv(Mat)
14     #获得矩阵[x,y,1] -> shape(3,rows,cols)
15     src = np.tensordot(Mat,dst,axes=[[-1],[0]])
16
17     #mapx/mapy -> shape(rows,cols)
18     mapx = src[0]#坐标非整数
19     mapy = src[1]#坐标非整数
20     #仿射出界的设为原点
21     flags = (mapy > rows - 2) + (mapy < 0) + (mapx > cols - 2) + (mapx < 0)
22     mapy[flags] = 0
23     mapx[flags] = 0
24     #双线性插值
25
26     result = InterLinearMap(img, mapx, mapy)
27
28     return result

再解决双线性插值,关于该算法的原理挺简单的,读者可以网上查找(提一点,理解双线性插值时可以想象3D模型,Z轴为灰度值)。对于该函数,借鉴一下remap函数,输入参数设两个map,分别表示x,y的映射。map的大小跟图片大小相同,也就是说,一共有rows*cols点需要插值,除了用两个for迭代,我们也可以将rows和cols作为矩阵的两个额外维度,表示样本数。计算的话,利用矩阵的点乘代替凌乱的长算式,显得很简洁,公式如下:

代码如下,经测试,执行一次该函数,花费时间为45ms,这要比原来的1.4s快多了(实在不知道该怎么进一步优化了,mxy、img下表索引、求和各花了15ms)

def InterLinearMap(img,mapx,mapy):

    #(rows,cols)
    inty = np.int32(mapy)
    intx = np.int32(mapx)
    nxty = 1+inty
    nxtx = 1+intx
    #(rows,cols)
    party = mapy - inty
    partx = mapx - intx
    resy = 1-party
    resx = 1-partx

    #(4,rows,cols)
    mxy = np.stack((resy*partx,resy*resx,partx*party, resx*party))
    mxy = np.expand_dims(mxy,axis=-1)

    #(4,rows,cols,3)
    mf = np.stack((img[inty,nxtx],img[inty,intx],img[nxty,nxtx],img[nxty,intx]))

    #res -> shape(rows,cols,3)
    res = np.sum(mxy*mf,axis=0)
    res = np.uint8(res+0.5)

    return res

综上,给出完整代码:

import cv2
import numpy as np

img = cv2.imread(‘image/turtle.jpg‘)
size = img.shape[:-1]
cv2.namedWindow(‘img‘)

#缩放矩阵
def GetResizeMatrix(scalex,scaley):
    M = np.zeros((3,3),dtype=np.float32)

    M.itemset((0,0),scalex)
    M.itemset((1,1),scaley)
    M.itemset((2,2),1)

    return M
#平移矩阵
def GetMoveMatrix(x,y):
    M = np.zeros((3, 3), dtype=np.float32)

    M.itemset((0, 0), 1)
    M.itemset((1, 1), 1)
    M.itemset((2, 2), 1)
    M.itemset((0, 2), x)
    M.itemset((1, 2), y)

    return M
#旋转矩阵
def GetRotationMatrix(angle):
    M = np.zeros((3, 3), dtype=np.float32)

    M.itemset((0, 0), np.cos(angle))
    M.itemset((0, 1), -np.sin(angle))
    M.itemset((1, 0), np.sin(angle))
    M.itemset((1, 1), np.cos(angle))
    M.itemset((2, 2), 1)

    return M

def InterLinearMap(img,mapx,mapy):

    #(rows,cols)
    inty = np.int32(mapy)
    intx = np.int32(mapx)
    nxty = 1+inty
    nxtx = 1+intx
    #(rows,cols)
    party = mapy - inty
    partx = mapx - intx
    resy = 1-party
    resx = 1-partx

    #(4,rows,cols)
    mxy = np.stack((resy*partx,resy*resx,partx*party, resx*party))
    mxy = np.expand_dims(mxy,axis=-1)

    #(4,rows,cols,3)
    mf = np.stack((img[inty,nxtx],img[inty,intx],img[nxty,nxtx],img[nxty,intx]))

    #res -> shape(rows,cols,3)
    res = np.sum(mxy*mf,axis=0)
    res = np.uint8(res+0.5)

    return res

def WarpAffine(img,Mat,size):

    rows = size[0]
    cols = size[1]
    #生成矩阵[X Y 1]
    ones = np.ones((rows, cols), dtype=np.float32)
    #gridx/gridy -> shape(rows,cols)
    gridx,gridy= np.meshgrid(np.arange(0, cols),np.arange(0, rows))
    #dst -> shape(3,rows,cols)
    dst = np.stack((gridx, gridy, ones))

    #求逆矩阵 M -> shape(3,3)
    Mat = np.linalg.inv(Mat)
    #获得矩阵[x,y,1] -> shape(3,rows,cols)
    src = np.tensordot(Mat,dst,axes=[[-1],[0]])

    #mapx/mapy -> shape(rows,cols)
    mapx = src[0]#坐标非整数
    mapy = src[1]#坐标非整数
    #仿射出界的设为原点
    flags = (mapy > rows - 2) + (mapy < 0) + (mapx > cols - 2) + (mapx < 0)
    mapy[flags] = 0
    mapx[flags] = 0
    #双线性插值

    result = InterLinearMap(img, mapx, mapy)

    return result

if __name__ == ‘__main__‘:

    center_x = size[1]/2
    center_y = size[0]/2

    for x in np.linspace(0,2*np.pi,100):

        angle = 360*x/2/np.pi
        scale = 0.2+0.2*np.sin(x)

        path_x = x*50+100
        path_y = (np.sin(x)+1)*100+100

        M = GetMoveMatrix(path_x,path_y)@GetRotationMatrix(x)            @GetResizeMatrix(scale,scale)@GetMoveMatrix(-center_x,-center_y)

        dst = WarpAffine(img,M,size)
        cv2.imshow(‘img‘,dst)
        cv2.waitKey(1)

原文地址:https://www.cnblogs.com/kensporger/p/12236869.html

时间: 2024-10-30 08:48:12

旋转傻乌龟——几何变换实践的相关文章

Android 屏幕旋转 处理 AsyncTask 和 ProgressDialog 的最佳方案

出处:http://blog.csdn.net/lmj623565791/article/details/37936275 1.概述 众所周知,Activity在不明确指定屏幕方向和configChanges时,当用户旋转屏幕会重新启动.当然了,应对这种情况,Android给出了几种方案: a.如果是少量数据,可以通过onSaveInstanceState()和onRestoreInstanceState()进行保存与恢复. Android会在销毁你的Activity之前调用onSaveInst

opencv人脸检测,旋转处理

年会签到,拍自己的大头照,有的人可能会拍成横向的,需要旋转,用人脸检测并修正它(图片). 1. 无脑检测步骤为: 1. opencv 读取图片,灰度转换 2. 使用CascadeClassifier()通过训练数据训练分类器 3. detectMultiScale()检测人脸 训练数据集下最基本的人脸haarcascade_frontalface_default.xml 2. 开始检测 1) 斜脸检测失败 用了一张逃避可耻但有用剧照,不知是gakki脸斜还是不清晰的缘故,face_cascade

2011年08月23日

在 Python 中,比较常用的图像处理库是 PIL(Python Image Library),当前版本是 1.1.6 ,用起来非常方便.大家可以在 http://www.pythonware.com/products/pil/index.htm 下载和学习. 基本图像处理 使用 PIL 之前需要 import Image 模块: import Image 然后你就可以使用Image.open('xx.bmp') 来打开一个位图文件进行处理了.打开文件你不用担心格式,也不用了解格式,无论什么格

【论文笔记】Spatial Transformer Networks

参考文献:**Jaderberg M, Simonyan K, Zisserman A. Spatial transformer networks[C]//Advances in Neural Information Processing Systems. 2015: 2017-2025. 摘要 卷积神经网络(CNN)已经被证明能够训练一个能力强大的分类模型,但与传统的模式识别方法类似,它也会受到数据在空间上多样性的影响.这篇Paper提出了一种叫做空间变换网络(Spatial Transfor

键盘、游戏、ASCII码引出的一系列问题

初学者就爱开发点小游戏,比如贪吃蛇.俄罗斯方块等,开发这种小游戏就必须有按键控制,一般情况下我们是直接用getch()函数接收按键,然后转换成对应的ASCII码,再通过与ASCII码比较判断用户到底是按了什么键,然后根据按键去执行相应的操作,比如移动.旋转. 但是最近实践我发现这其实没那么简单,举个例子:比如贪吃蛇,当用户按下“向上的方向键”时,要控制蛇往上走,为了说明问题,简化一点,如果按了向上的方向键则输出“up”,其他方向键类似.一般我们会去查ASCII码表(下面有), 如图,向上的方向键

OpenCV+TensorFlow 入门人工智能图像处理

第1章 课程导学包括课程概述.课程安排.学习前提等方面的介绍,让同学们对计算机视觉有所理解1-1 计算机视觉导学 第2章 计算机视觉入门通过OpenCV以及TensorFlow两个方面介绍计算机入门的相关知识.OpenCV侧重点在于为大家补充图像处理的相关基础,如像素.文件封装格式.灰度等级.颜色通道等的概念.TensorFlow重点在于通过对常量.变量.矩阵等的介绍,学习并掌握TensorFlow的基本使用....2-1 本章介绍2-2 Mac下一站式开发环境anaconda搭建2-3 Win

图像几何变换:旋转,缩放,斜切

几何变换 几何变换可以看成图像中物体(或像素)空间位置改变,或者说是像素的移动. 几何运算需要空间变换和灰度级差值两个步骤的算法,像素通过变换映射到新的坐标位置,新的位置可能是在几个像素之间,即不一定为整数坐标.这时就需要灰度级差值将映射的新坐标匹配到输出像素之间.最简单的插值方法是最近邻插值,就是令输出像素的灰度值等于映射最近的位置像素,该方法可能会产生锯齿.这种方法也叫零阶插值,相应比较复杂的还有一阶和高阶插值. 插值算法感觉只要了解就可以了,图像处理中比较需要理解的还是空间变换. 空间变换

[opencv] 图像几何变换:旋转,缩放,斜切

几何变换 几何变换可以看成图像中物体(或像素)空间位置改变,或者说是像素的移动. 几何运算需要空间变换和灰度级差值两个步骤的算法,像素通过变换映射到新的坐标位置,新的位置可能是在几个像素之间,即不一定为整数坐标.这时就需要灰度级差值将映射的新坐标匹配到输出像素之间.最简单的插值方法是最近邻插值,就是令输出像素的灰度值等于映射最近的位置像素,该方法可能会产生锯齿.这种方法也叫零阶插值,相应比较复杂的还有一阶和高阶插值. 插值算法感觉只要了解就可以了,图像处理中比较需要理解的还是空间变换. 空间变换

OpenCV2:图像的几何变换,平移、镜像、缩放、旋转(1)

图像的几何变换是在不改变图像内容的前提下对图像像素的进行空间几何变换,主要包括了图像的平移变换.镜像变换.缩放和旋转等.本文首先介绍了图像几何变换的一些基本概念,然后再OpenCV2下实现了图像的平移变换.镜像变换.缩放以及旋转,最后介绍几何的组合变换(平移+缩放+旋转). 1.几何变换的基本概念 1.1 坐标映射关系 图像的几何变换改变了像素的空间位置,建立一种原图像像素与变换后图像像素之间的映射关系,通过这种映射关系能够实现下面两种计算: 原图像任意像素计算该像素在变换后图像的坐标位置 变换