python-opencv学习笔记(九):图像的仿射变换与应用实例

介绍

这篇文章是在实验楼做的一个实验,因为感觉整个过程很顺利,脉络也比较清晰。我添加了一些自己的理解,整理成学习笔记。

图像平移

图像翻译的数学推导

简单来说,图像的本质可以看做一个三维矩阵,第一维为长度,第二维是宽度,第三维是通道数(RGB),如果一张图在 python 中是一个变量 image,那么其长宽即 width, height = image.shape[:2]。

图像的平移就是在 xy 平面内对图像进行移动,所以该操作有两个自由度。其表达式为:

python-opencv学习笔记(九):图像的仿射变换与应用实例

其中,python-opencv学习笔记(九):图像的仿射变换与应用实例为偏​​移量,python-opencv学习笔记(九):图像的仿射变换与应用实例python-opencv学习笔记(九):图像的仿射变换与应用实例的偏移量,python-opencv学习笔记(九):图像的仿射变换与应用实例python-opencv学习笔记(九):图像的仿射变换与应用实例的偏移量,单位为像素。

例如,如果要将图像向右平移 10 个像素,向下平移 30 个像素,那么变换矩阵 M 表示为如下:
python-opencv学习笔记(九):图像的仿射变换与应用实例

代码

在opencv中,在平移之前,我们需要先构造一个移动矩阵,所谓移动矩阵,就是说明在 x 轴方向上移动多少距离,在 y 轴上移动多少距离。

可以通过 numpy 来构造这个矩阵,并将其传给仿射函数cv2.warpAffine()

仿射函数cv2.warpAffine()传入三个参数:

  • 用于图像变换的原始图像矩阵
  • 移动矩阵
  • 图像变换的大小

据此可以写一个demo,实现将图像向右平移 10 个像素,向下平移 30 个像素:

import cv2
import numpy as np

img = cv2.imread('lena.jpg')
height,width,channel = img.shape

# 声明变换矩阵 向右平移10个像素, 向下平移30个像素
M = np.float32([[1, 0, 10], [0, 1, 30]])
# 仿射变换
shifted = cv2.warpAffine(img, M, (width, height))

cv2.imwrite('shift_right_10_down_30.png', shifted)

python-opencv学习笔记(九):图像的仿射变换与应用实例

如果要将图像向左平移 10 个像素, 向上平移 30 个像素,那么只需要将以上代码中声明变换矩阵的代码换为 M = np.float32([[1, 0, -10], [0, 1, -30]]),其它不变:python-opencv学习笔记(九):图像的仿射变换与应用实例

图像旋转

首先要明确一点,旋转是在二维中绕某点旋转,在三维中绕某轴旋转。

图像旋转的数学推导

绕坐标原点旋转

2D旋转中最简单的场景就是绕坐标原点旋转,如下图所示:

python-opencv学习笔记(九):图像的仿射变换与应用实例
如图所示,将python-opencv学习笔记(九):图像的仿射变换与应用实例点绕原点旋转python-opencv学习笔记(九):图像的仿射变换与应用实例角,得到python-opencv学习笔记(九):图像的仿射变换与应用实例点。假设点python-opencv学习笔记(九):图像的仿射变换与应用实例的坐标为python-opencv学习笔记(九):图像的仿射变换与应用实例,则可以推导出点python-opencv学习笔记(九):图像的仿射变换与应用实例的坐标(假设原点到python-opencv学习笔记(九):图像的仿射变换与应用实例的距离为python-opencv学习笔记(九):图像的仿射变换与应用实例,原点到python-opencv学习笔记(九):图像的仿射变换与应用实例的向量为python-opencv学习笔记(九):图像的仿射变换与应用实例轴的夹角为python-opencv学习笔记(九):图像的仿射变换与应用实例
python-opencv学习笔记(九):图像的仿射变换与应用实例

通过展开三角函数,我们得到:
python-opencv学习笔记(九):图像的仿射变换与应用实例

带入 x 和 y 表达式得到:
python-opencv学习笔记(九):图像的仿射变换与应用实例

写成矩阵的形式是:
python-opencv学习笔记(九):图像的仿射变换与应用实例

尽管图示中仅仅表示的是旋转一个锐角 θ 的情形,但是我们推导中使用的是三角函数的基本定义来计算坐标的,因此当旋转的角度是任意角度(例如大于 180 度,导致python-opencv学习笔记(九):图像的仿射变换与应用实例点进入到第四象限)结论仍然是成立的。

围绕任意点旋转

您可以先将当前旋转中心点平移到原点,在原点处旋转,然后再平移回来。

假设旋转中心为python-opencv学习笔记(九):图像的仿射变换与应用实例为平移矩阵,python-opencv学习笔记(九):图像的仿射变换与应用实例为平移矩阵的逆矩阵,python-opencv学习笔记(九):图像的仿射变换与应用实例为原点旋转矩阵。

python-opencv学习笔记(九):图像的仿射变换与应用实例

在:

python-opencv学习笔记(九):图像的仿射变换与应用实例。平移矩阵的逆矩阵为python-opencv学习笔记(九):图像的仿射变换与应用实例,原点处的旋转矩阵为python-opencv学习笔记(九):图像的仿射变换与应用实例

所以启动:

python-opencv学习笔记(九):图像的仿射变换与应用实例

至此,就是旋转的描述过程。

代码

绕着坐标原点进行旋转的方式在 OpenCV 中提供了cv2.getRotationMatrix2D函数获得变换矩阵。

  • 第一个参数指定旋转点;
  • 第二个参数指定旋转角度;
  • 第二个参数指定比例因子。

这里实现的效果为围绕原点将 lena 图像逆时针旋转 15/45/60 度:

import numpy as np
import cv2
import math
from matplotlib import pyplot as plt

img = cv2.imread('sub_picture.jpg')

height, width, channel = img.shape

def getRotationMatrix2D(theta):
    # 角度值转换为弧度值
    # 因为图像的左上角是原点 需要×-1
    theta = math.radians(-theta)

    M = np.float32([
        [math.cos(theta), -math.sin(theta), 0],
        [math.sin(theta), math.cos(theta), 0]])
    return M

# 进行仿射变换
# 围绕原点 逆时针旋转15度
M = getRotationMatrix2D(15)
rotated_15 = cv2.warpAffine(img, M, (width, height))

# 围绕原点 逆时针旋转45度
M = getRotationMatrix2D(45)
rotated_45 = cv2.warpAffine(img, M, (width, height))

# 围绕原点 逆时针旋转60度
M = getRotationMatrix2D(60)
rotated_60 = cv2.warpAffine(img, M, (width, height))

plt.subplot(221)
plt.title("Src Image")
plt.imshow(img[:,:,::-1])

plt.subplot(222)
plt.title("Rotated 15 Degree")
plt.imshow(rotated_15[:,:,::-1])

plt.subplot(223)
plt.title("Rotated 45 Degree")
plt.imshow(rotated_45[:,:,::-1])

plt.subplot(224)
plt.title("Rotated 60 Degree")
plt.imshow(rotated_60[:,:,::-1])

plt.show()

python-opencv学习笔记(九):图像的仿射变换与应用实例

绕着坐标原点进行旋转的方式在 OpenCV 中将sub_picture 图像旋转 30/45/60 度代码为:

import numpy as np
import cv2
from math import cos,sin,radians
from matplotlib import pyplot as plt

img = cv2.imread('sub_picture.jpg')
height, width, channel = img.shape
theta = 45

def getRotationMatrix2D(theta, cx=0, cy=0):
    # 角度值转换为弧度值
    # 因为图像的左上角是原点 需要×-1
    theta = radians(-1 * theta)

    M = np.float32([
        [cos(theta), -sin(theta), (1-cos(theta))*cx + sin(theta)*cy],
        [sin(theta), cos(theta), -sin(theta)*cx + (1-cos(theta))*cy]])
    return M

# 求得图片中心点, 作为旋转的轴心
cx = int(width / 2)
cy = int(height / 2)

# 进行仿射变换
# 围绕原点 逆时针旋转30度
M = getRotationMatrix2D(30, cx=cx, cy=cy)
rotated_30 = cv2.warpAffine(img, M, (width, height))

# 围绕原点 逆时针旋转45度
M = getRotationMatrix2D(45, cx=cx, cy=cy)
rotated_45 = cv2.warpAffine(img, M, (width, height))

# 围绕原点 逆时针旋转60度
M = getRotationMatrix2D(60, cx=cx, cy=cy)
rotated_60 = cv2.warpAffine(img, M, (width, height))

plt.subplot(221)
plt.title("Src Image")
plt.imshow(img[:,:,::-1])

plt.subplot(222)
plt.title("Rotated 30 Degree")
plt.imshow(rotated_30[:,:,::-1])

plt.subplot(223)
plt.title("Rotated 45 Degree")
plt.imshow(rotated_45[:,:,::-1])

plt.subplot(224)
plt.title("Rotated 60 Degree")
plt.imshow(rotated_60[:,:,::-1])

plt.show()

python-opencv学习笔记(九):图像的仿射变换与应用实例

图像缩放

图像缩放的数学推导

对图像的伸缩变换的变换矩阵 M 为:

python-opencv学习笔记(九):图像的仿射变换与应用实例

其中,python-opencv学习笔记(九):图像的仿射变换与应用实例代表python-opencv学习笔记(九):图像的仿射变换与应用实例轴的焦距(比例因子),python-opencv学习笔记(九):图像的仿射变换与应用实例代表python-opencv学习笔记(九):图像的仿射变换与应用实例轴的焦距(比例因子)。

化简分别给出python-opencv学习笔记(九):图像的仿射变换与应用实例python-opencv学习笔记(九):图像的仿射变换与应用实例
python-opencv学习笔记(九):图像的仿射变换与应用实例

代码

图像的放大和缩小有一个专门的函数:cv2.resize(),这其中就需要设置缩放的比例,一种办法是设置缩放因子,另一种办法是直接设置图像的大小。resize 函数可表示为:

resize(src, dsize[, dst[, fx[, fy[, interpolation]]]]) -> dst

函数对应的参数分析:

  • src输入图片
  • dsize输出图片的尺寸
  • dst输出图片
  • fxx 轴的缩放因子
  • fyy 轴的缩放因子
  • interpolation插值方式
  • INTER_NEAREST- 最近邻插值
  • INTER_LINEAR- 线性插值(默认)
  • INTER_AREA- 区域插值
  • INTER_CUBIC- 三次样条插值
  • INTER_LANCZOS4- Lanczos 插值

在缩放以后,图像必然会发生变化,这就涉及到图像的插值问题。缩放有几种不同的插值(interpolation)方法,在缩小时推荐使用cv2.INTER_AREA,扩大时推荐使用cv2.INTER_CUBICcv2.INTER_LINEAR

如下例子通过两种方式实现对 sub_picture 图片的缩放:
python-opencv学习笔记(九):图像的仿射变换与应用实例

图像翻转

图像翻转可以分为水平翻转、垂直翻转以及同时水平翻转和垂直翻转,具体公式遇到可分为如下:(注: width 代表图像的宽度 height 代表图像的高度)

图像翻转推导

水平翻转变换矩阵:

python-opencv学习笔记(九):图像的仿射变换与应用实例

垂直翻转的变换矩阵:

python-opencv学习笔记(九):图像的仿射变换与应用实例

同时水平和垂直翻转:

python-opencv学习笔记(九):图像的仿射变换与应用实例

代码

import cv2
import numpy as np
from matplotlib import pyplot as plt

img = cv2.imread('sub_picture.jpg')
height,width,channel = img.shape

# 水平翻转
M1 = np.float32([[-1, 0, width], [0, 1, 0]])
flip_h =  cv2.warpAffine(img, M1, (width, height))

# 垂直翻转
M2 = np.float32([[1, 0, 0], [0, -1, height]])
flip_v =  cv2.warpAffine(img, M2, (width, height))

# 水平垂直同时翻转
M3 = np.float32([[-1, 0, width], [0, -1, height]])
flip_hv =  cv2.warpAffine(img, M3, (width, height))

# 将颜色空间从BGR转换为RGB
def bgr2rgb(img):
    return img[:,:,::-1]

plt.subplot(221)
plt.title('SRC')
plt.imshow(bgr2rgb(img))

plt.subplot(222)
plt.title('Horizontally')
plt.imshow(bgr2rgb(flip_h))

plt.subplot(223)
plt.title('Vertically')
plt.imshow(bgr2rgb(flip_v))

plt.subplot(224)
plt.title('Horizontally & Vertically')
plt.imshow(bgr2rgb(flip_hv))

plt.show()

python-opencv学习笔记(九):图像的仿射变换与应用实例

示例:提取手写数字样本

我们需要先制作一张写有 1-9 之间数字的图片:
python-opencv学习笔记(九):图像的仿射变换与应用实例
这个我本来想用代码来做,但这种必须有模板,特别是这种在空间中数字旋转角度与字体都有所改变的,网上的方法大多基于 MNIST 数据集,我试了脚本之家下Python生成数字图片代码分享的其中旋转生成字体的demo,其中有两个bug顺带修改了:

# -*- coding:utf-8 -*-
from PIL import Image,ImageFont,ImageDraw,ImageFilter
import random
import os
import time
class Captcha(object):
  def __init__(self,size=(20,24),fontSize=20):
    self.font = ImageFont.truetype('Arial.ttf',fontSize)
    self.size = size
    self.image = Image.new('RGBA',self.size,(255,)*4)
    self.text = ''
  def rotate(self, angle):
    rot = self.image.rotate(angle,expand=0)
    fff = Image.new('RGBA',rot.size,(255,)*4)
    self.image = Image.composite(rot,fff,rot)
  def randColor(self):
    self.fontColor = (random.randint(0,250),random.randint(0,250),random.randint(0,250))
 
  def setNum(self, num):
    return num;
  def write(self,text,x,y):
    draw = ImageDraw.Draw(self.image)
    draw.text((x,y),text,fill=self.fontColor,font=self.font)
  def writeNum(self, num, angle):
    x = 2
    y = -2
    self.text = num
    self.fontColor = (0, 0, 0)
    self.write(num, x, y)
    self.rotate(angle)
    return self.text
  def save(self, save_path):
    # self.image = self.image.filter(ImageFilter.EDGE_ENHANCE_MORE) #滤镜,边界加强
    self.image.save(save_path)
pic_root_path = './pic'
if not os.path.exists(pic_root_path):
  os.mkdir(pic_root_path)
angles = [(45,90),(-45,45),(-90,-45)]
for i in range(10):
  pic_num_path = os.path.join(pic_root_path, 'x'+str(i))
  if not os.path.exists(pic_num_path):
    os.mkdir(pic_num_path)
  for angle_i in angles:
    angle_name = str(angle_i[0])+'_'+str(angle_i[1])
    pic_angle_path = os.path.join(pic_num_path, angle_name)
    if not os.path.exists(pic_angle_path):
      os.mkdir(pic_angle_path)
    for fontsize in range(25,29):
      for j in range(2500):
        # Keep 5 decimal places
        angle = round(random.uniform(angle_i[0], angle_i[1]),5)
        img = Captcha(size=(20, 24), fontSize=fontsize)
        num = img.writeNum(str(i), angle)
        img_name = 'x'+str(j)+'_'+str(fontsize)+'_'+str(angle)+'_'+str(num)+'.png'
        print(img_name,".....img_name....")
        save_path = os.path.join(pic_angle_path, img_name)
        print(save_path,"........save_path......")
        # print(dir(Captcha))
        # img=img.convert('RGB')
        img.save(save_path)

python-opencv学习笔记(九):图像的仿射变换与应用实例

生成的图像是:
python-opencv学习笔记(九):图像的仿射变换与应用实例

另外就是记起来之前有写过一篇Django的web验证码登录注册功能,有做的验证码图片,但那是一个维度,而且有模板模块,如果要将其扩展,好像也很难,翻了下之前做的图为:
在这里插入图片描述
那这里就用上面那张图做如下实验,我们可以根据 minAreaRect 函数返回的数据结构,用来提取最小外接矩形区域, 以矩形中心 (cx, cy) 作为对原来图像旋转的中心点,旋转角度设定为 theta:

# 声明旋转矩阵
rotateMatrix = cv2.getRotationMatrix2D((cx, cy), theta, 1.0)
# 获取旋转后的图像
rotatedImg = cv2.warpAffine(img, rotateMatrix, (img.shape[1], img.shape[0]))

运行代码是:

'''
    利用minAreaRect绘制最小面积矩形并绘制
'''
import numpy as np
import cv2

# 读入黑背景下的彩色手写数字
img = cv2.imread("random_color_number.jpg")
# 转换为gray灰度图
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# 寻找轮廓
contours, hier = cv2.findContours(gray, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)

for cidx,cnt in enumerate(contours):
    minAreaRect = cv2.minAreaRect(cnt)
    # 转换为整数点集坐标
    # rectCnt = np.int64(cv2.boxPoints(minAreaRect))
    ((cx, cy), (w, h), theta) = minAreaRect

    cx = int(cx)
    cy = int(cy)
    w = int(w)
    h = int(h)
    # 获取旋转矩阵
    rotateMatrix = cv2.getRotationMatrix2D((cx, cy), theta, 1.0)
    rotatedImg = cv2.warpAffine(img, rotateMatrix, (img.shape[1], img.shape[0]))
    pt1 = (int(cx - w/2), int(cy - h/2))
    pt2 = (int(cx + w/2), int(cy + h/2))
    # 原图绘制矩形区域
    cv2.rectangle(rotatedImg, pt1=pt1, pt2=pt2,color=(255, 255, 255), thickness=3)
    # 绘制中心点
    cv2.circle(rotatedImg, (cx, cy), 5, color=(255, 0, 0), thickness=-1)
    cv2.imwrite("minarearect_cidx_{}.png".format(cidx), rotatedImg)

python-opencv学习笔记(九):图像的仿射变换与应用实例
在对手写数字图片样本进行提取之前需要先将图片转为灰度图,再进行轮廓提取。需要用到的函数为 boudningRect(),用来获取正外接矩形,传入的参数为轮廓点集(单个) Points。具体用法如下:

rect = cv2.boundingRect(cnt)
(x, y, w, h) = rect

完整的代码是:

import numpy as np
import cv2

# 读入黑背景下的彩色手写数字
img = cv2.imread("random_color_number.jpg")
# 转换为gray灰度图
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# 寻找轮廓
contours, hier = cv2.findContours(gray, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)

# 声明画布 拷贝自img
canvas = np.copy(img)

for cidx,cnt in enumerate(contours):
    (x, y, w, h) = cv2.boundingRect(cnt)
    print('RECT: x={}, y={}, w={}, h={}'.format(x, y, w, h))
    # 原图绘制圆形
    cv2.rectangle(canvas, pt1=(x, y), pt2=(x+w, y+h),color=(255, 255, 255), thickness=3)
    # 截取ROI图像
    cv2.imwrite("number_boudingrect_cidx_{}.png".format(cidx), img[y:y+h, x:x+w])

cv2.imwrite("number_boundingrect_canvas.png", canvas)
"""
RECT: x=92, y=378, w=94, h=64
RECT: x=381, y=328, w=69, h=102
RECT: x=234, y=265, w=86, h=70
RECT: x=53, y=260, w=61, h=95
RECT: x=420, y=184, w=49, h=66
RECT: x=65, y=124, w=48, h=83
RECT: x=281, y=71, w=70, h=108
True
"""

python-opencv学习笔记(九):图像的仿射变换与应用实例

文章出处登录后可见!

已经登录?立即刷新

共计人评分,平均

到目前为止还没有投票!成为第一位评论此文章。

(0)
青葱年少的头像青葱年少普通用户
上一篇 2022年4月6日 下午3:26
下一篇 2022年4月6日 下午3:43

相关推荐