OpenCV(Python 版)

计算机中的图像本质上是由无数像素组成的网格,彩色图像通常通过红(R)、绿(G)、蓝(B)三个独立的颜色通道来记录信息,每个通道都是一个二维数字矩阵,矩阵中的数值代表对应位置像素该颜色的亮度(通常为 0-255),三个通道的数值组合决定了像素最终呈现的颜色,计算机存储的就是这些通道矩阵数据,以及图像的宽、高、通道数等元信息。

图像的分类

二值图像

一幅二值图像的二维矩阵仅由0、1两个值构成,“0”代表黑色,“1”代白色。由于每一像素(矩阵中每一元素)取值仅有0、1两种可能,所以计算机中二值图像的数据类型通常为1个二进制位。二值图像通常用于文字、线条图的扫描识别(OCR)和掩膜图像的存储。

灰度图

每个像素只有一个采样颜色的图像,这类图像通常显示为从最暗黑色到最亮的白色的灰度,尽管理论上这个采样可以任何颜色的不同深浅,甚至可以是不同亮度上的不同颜色。灰度图像与黑白图像不同,在计算机图像领域中黑白图像只有黑色与白色两种颜色;但是,灰度图像在黑色与白色之间还有许多级的颜色深度。灰度图像经常是在单个电磁波频谱如可见光内测量每个像素的亮度得到的,用于显示的灰度图像通常用每个采样像素8位的非线性尺度来保存,这样可以有256级灰度(如果用16位,则有65536级)。

彩色图

每个像素通常是由红(R)、绿(G)、蓝(B)三个分量来表示的,分量介于(0,255)。RGB图像与索引图像一样都可以用来表示彩色图像。与索引图像一样,它分别用红(R)、绿(G)、蓝(B)三原色的组合来表示每个像素的颜色。但与索引图像不同的是,RGB图像每一个像素的颜色值(由RGB三原色表示)直接存放在图像矩阵中,由于每一像素的颜色需由R、G、B三个分量来表示,M、N分别表示图像的行列数,三个M x N的二维矩阵分别表示各个像素的R、G、B三个颜色分量。RGB图像的数据类型一般为8位无符号整形,通常用于表示和存放真彩色图像。

1.图片视频基础读取

读取与展示图片

安装 OpenCV  使用 imread 读取图片。

pip install opencv-python

读取图片(核心)grayscale /ˈɡreɪskeɪl/  灰度;unchanged /ʌnˈtʃeɪndʒd/ 未改变的;shape /ʃeɪp/ 形状

import cv2

# 1. 彩色图(默认 BGR 顺序)
img = cv2.imread("cat.jpg")  

# 2. 灰度图
img_gray = cv2.imread("cat.jpg", cv2.IMREAD_GRAYSCALE)  

# 3. 含透明通道(PNG)
img_alpha = cv2.imread("cat.png", cv2.IMREAD_UNCHANGED)

参数:

  • 要读取的图像路径

  • 读取方式的标志

    • cv.IMREAD_COLOR:以彩色模式加载图像,任何图像的透明度都将被忽略。这是默认参数。

    • cv.IMREAD_GRAYSCALE:以灰度模式加载图像

    • cv.IMREAD_UNCHANGED:包括 alpha 通道的加载图像模式(png/gif图片)。

      可以使用1、0或者-1来替代上面三个标志

检查是否读取成功 

if img is None:
    print("读取失败,检查路径或文件是否存在")
else:
    print("正常形状:", img.shape)  
    print("灰度形状:", img_gray.shape) 

注意:如果加载的路径有错误,不会报错,会返回一个None值

正常形状: (180, 300, 3)

灰度形状: (180, 300)

含透明通道:(180, 300, 4)

显示与保存

cv2.imshow("image", img)
cv2.waitKey(0)
cv2.destroyAllWindows()

cv2.imwrite("save.jpg", img)

参数:

  • 显示图像的窗口名称,以字符串类型表示
  • 要加载的图像

注意:在调用显示图像的API后,要调用cv.waitKey()给图像绘制留下时间,否则窗口会出现无响应情况,并且图像无法显示出来

cv2.destroyAllWindows():关掉所有打开的图片窗口,清理内存。

另外我们也可使用matplotlib对图像进行展示。OpenCV 默认通道顺序是 BGR,不是 RGB。

# opencv中显示
cv.imshow('image',img)
cv.waitKey(0)
# matplotlib中展示
plt.imshow(img[:,:,::-1])

img 打印出来是一个3维数组

array([[[  6,  43, 125],
        [  6,  43, 125],
        [  5,  42, 122],
        ...,
        [174, 207, 222],
        [174, 207, 222],
        [173, 206, 221]]], shape=(180, 300, 3), dtype=uint8)

img_gray 打印出来是一个2维数组

array([[ 63,  63,  62, ...,  36,  36,  36],
       [ 62,  62,  61, ...,  36,  36,  36],
       [ 59,  59,  58, ...,  36,  36,  36],
       ...,
       [202, 205, 207, ..., 206, 205, 205],
       [201, 199, 197, ..., 207, 206, 206],
       [198, 194, 193, ..., 208, 208, 207]], shape=(180, 300), dtype=uint8)

获取类型

type(img)

numpy.ndarray

img.size

162000

img.dtype

dtype('uint8') 

dtype('uint8')  unsigned 8-bit integer(无符号 8 位整数):每个像素值用 1 字节(8 位)无符号整数存储,范围 0–255,只能存非负数,没有负数。

视频基础读取

OpenCV 读取视频和读图片逻辑几乎一样,只是把 cv2.imread 换成视频捕获对象,一帧一帧读取画面。capture /ˈkæptʃər/  捕获;拍摄  frame /freɪm/ 帧; release /rɪˈliːs/ 释放

import cv2

# 1. 打开视频(可以是本地视频 或 摄像头 0)
cap = cv2.VideoCapture("movie.mp4")  # 本地视频
# cap = cv2.VideoCapture(0)         # 电脑摄像头(直接用0)

# 检查是否打开成功
if not cap.isOpened():
    print("视频打开失败!")
    exit()

# 2. 循环读取每一帧
while True:
    # 读一帧画面ret: 获取成功返回True,获取失败,返回False
    ret, frame = cap.read()
    
    # 如果读不到帧(视频结束),退出循环
    if not ret:
        break
    #img = cv2.cvtColor(frame, cv2.IMREAD_COLOR)
    img = cv2.cvtColor(frame, cv2.COLOR_RGB2GRAY)
    cv2.imshow('result', img)

    # 按 q 键退出,等待25毫秒刷新
    if cv2.waitKey(25) & 0xFF == ord('q'):
        break

# 3. 释放资源 + 关闭窗口
cap.release()
cv2.destroyAllWindows()

调用cv.imshow()显示图像,在显示图像时使用cv.waitkey()设置适当的持续时间,如果太低视频会播放的非常快,如果太高就会播放的非常慢,通常情况下我们设置25ms就可以了。

视频的属性信息

2.1. 获取视频的某些属性,

retval = cap.get(propId)

参数:

  • propId: 从0到18的数字,每个数字表示视频的属性

    常用属性有:

2. 绘制几何图形

1 绘制直线

cv.line(img,start,end,color,thickness)

参数:

  • img:要绘制直线的图像
  • Start,end: 直线的起点和终点
  • color: 线条的颜色
  • Thickness: 线条宽度

2 绘制圆形

cv.circle(img,centerpoint, r, color, thickness)

参数:

  • img:要绘制圆形的图像
  • Centerpoint, r: 圆心和半径
  • color: 线条的颜色
  • Thickness: 线条宽度,为-1时生成闭合图案并填充颜色

3 绘制矩形

cv.rectangle(img,leftupper,rightdown,color,thickness)

参数:

  • img:要绘制矩形的图像
  • Leftupper, rightdown: 矩形的左上角和右下角坐标
  • color: 线条的颜色
  • Thickness: 线条宽度

2.4 向图像中添加文字

cv.putText(img,text,station, font, fontsize,color,thickness,cv.LINE_AA)

参数:

  • img: 图像
  • text:要写入的文本数据
  • station:文本的放置位置
  • font:字体
  • Fontsize :字体大小

2.5 效果展示

我们生成一个全黑的图像,然后在里面绘制图像并添加文字

import numpy as np
import cv2 as cv
import matplotlib.pyplot as plt
# 1 创建一个空白的图像
img = np.zeros((512,512,3), np.uint8)
# 2 绘制图形
cv.line(img,(0,0),(511,511),(255,0,0),5)
cv.rectangle(img,(384,0),(510,128),(0,255,0),3)
cv.circle(img,(447,63), 63, (0,0,255), -1)
font = cv.FONT_HERSHEY_SIMPLEX
cv.putText(img,'OpenCV',(10,500), font, 4,(255,255,255),2,cv.LINE_AA)
# 3 图像展示
plt.imshow(img[:,:,::-1])
plt.title('匹配结果'), plt.xticks([]), plt.yticks([])
plt.show()

2.图像基本操作

1.截取部分图像数据

OpenCV 里截取图片区域(ROI) 的操作,非常常用!

# 定义显示函数(你代码里用的就是这个)
def cv_show(name, img):
    cv2.imshow(name, img)
    cv2.waitKey(0)
    cv2.destroyAllWindows()

# 读取 + 截取 + 显示
img = cv2.imread('cat.jpg')
cat = img[0:50, 0:200]   # 截左上角:高50,宽200
cv_show('cat', cat)

关键:图像切片规则

  • img[ 行范围, 列范围 ]
  • img[0:50, 0:200] = 截取
    • 高度:第 0 行~第 50 行
    • 宽度:第 0 列~第 200 列
  • 也就是左上角一小块

2.颜色通道提取

把一张彩色图片,拆分成 蓝、绿、红 三个独立的颜色通道。

b,g,r=cv2.split(img)
  • img:你读取的彩色图(BGR 格式)
  • cv2.split()通道拆分
  • 得到 3 张灰度图:
    • b:蓝色通道
    • g:绿色通道
    • r:红色通道

r、g、b 三个数据格式如下:

array([[125, 125, 122, ...,  39,  39,  39],
       [121, 121, 120, ...,  39,  39,  39],
       [114, 112, 111, ...,  39,  39,  39],
       ...,
       [213, 216, 218, ..., 220, 219, 219],
       [212, 210, 208, ..., 221, 220, 220],
       [209, 205, 204, ..., 222, 222, 221]], shape=(180, 300), dtype=uint8)

它们的 shape 都为 (180, 300)

只保留R通道

cur_img = img.copy()
cur_img[:,:,0] = 0
cur_img[:,:,1] = 0
cv_show('R',cur_img)

只保留G通道

cur_img = img.copy()
cur_img[:,:,0] = 0
cur_img[:,:,2] = 0
cv_show('G',cur_img)

只保留B通道

cur_img = img.copy()
cur_img[:,:,1] = 0
cur_img[:,:,2] = 0
cv_show('B',cur_img)

有时需要在B,G,R通道图像上单独工作。在这种情况下,需要将BGR图像分割为单个通道。或者在其他情况下,可能需要将这些单独的通道合并到BGR图像。你可以通过以下方式完成。

# 通道拆分
b,g,r = cv.split(img)
# 通道合并
img = cv.merge((b,g,r))

我们可以通过行和列的坐标值获取该像素点的像素值。对于BGR图像,它返回一个蓝,绿,红值的数组。对于灰度图像,仅返回相应的强度值。使用相同的方法对像素值进行修改。

import numpy as np
import cv2 as cv
img = cv.imread('messi5.jpg')
# 获取某个像素点的值
px = img[100,100]
# 仅获取蓝色通道的强度值
blue = img[100,100,0]
# 修改某个位置的像素值
img[100,100] = [255,255,255]

3.边界填充

OpenCV 边界填充就是给图片四周加上一圈边框,常用于深度学习预处理、图像扩展、卷积操作等,用函数 cv2.copyMakeBorder() 实现。

dst = cv2.copyMakeBorder(
    img,          # 原图
    top,          # 上边框宽度
    bottom,       # 下边框宽度
    left,         # 左边框宽度
    right,        # 右边框宽度
    borderType,   # 填充类型
    value         # 纯色填充时的颜色(BGR)
)

5 种填充类型(最常用)

cv2.BORDER_CONSTANT:纯色填充 、cv2.BORDER_REPLICATE:复制边缘像素填充(最常用)、cv2.BORDER_REFLECT:镜像反射填充、cv2.BORDER_REFLECT_101:更自然的镜像填充、cv2.BORDER_WRAP:平铺重复填充。constant /ˈkɑːnstənt/ 常量;replicate /ˈreplɪkeɪt/ 复制;reflect /rɪˈflekt/  反射,反光;wrap /ræp/ 包裹

import cv2

# 1. 读取图片
img = cv2.imread('cat.jpg')

# 2. 设置填充大小(上下左右各填充20像素)
top = bottom = left = right = 20

# 3. 5种填充效果
# ① 纯色填充(蓝色:BGR(255,0,0))
constant = cv2.copyMakeBorder(img, top, bottom, left, right, cv2.BORDER_CONSTANT, value=(255,0,0))

# ② 复制边缘填充
replicate = cv2.copyMakeBorder(img, top, bottom, left, right, cv2.BORDER_REPLICATE)

# ③ 镜像反射
reflect = cv2.copyMakeBorder(img, top, bottom, left, right, cv2.BORDER_REFLECT)

# ④ 更自然的镜像
reflect101 = cv2.copyMakeBorder(img, top, bottom, left, right, cv2.BORDER_REFLECT_101)

# ⑤ 平铺重复
wrap = cv2.copyMakeBorder(img, top, bottom, left, right, cv2.BORDER_WRAP)

显示:

import matplotlib.pyplot as plt
plt.subplot(231), plt.imshow(img, 'gray'), plt.title('ORIGINAL')
plt.subplot(232), plt.imshow(replicate, 'gray'), plt.title('REPLICATE')
plt.subplot(233), plt.imshow(reflect, 'gray'), plt.title('REFLECT')
plt.subplot(234), plt.imshow(reflect101, 'gray'), plt.title('REFLECT_101')
plt.subplot(235), plt.imshow(wrap, 'gray'), plt.title('WRAP')
plt.subplot(236), plt.imshow(constant, 'gray'), plt.title('CONSTANT')

plt.show()

OpenCV 读取是 BGR 顺序,matplotlib 显示是 RGB 顺序,所以直接用 plt.imshow() 颜色会失真、变蓝 / 变绿!只是缺少 BGR 转 RGB,所以颜色不对,加一行 cv2.cvtColor(res, cv2.COLOR_BGR2RGB) 就完美了

BORDER_REPLICATE:复制法,也就是复制最边缘像素。
BORDER_REFLECT:反射法,对感兴趣的图像中的像素在两边进行复制例如:fedcba|abcdefgh|hgfedcb
BORDER_REFLECT_101:反射法,也就是以最边缘像素为轴,对称,gfedcb|abcdefgh|gfedcba
BORDER_WRAP:外包装法cdefgh|abcdefgh|abcdefg
BORDER_CONSTANT:常量法,常数值填充。

最常用:BORDER_REPLICATE(复制边缘)、BORDER_CONSTANT(纯色)

4.数值计算

OpenCV 图片是 uint8 类型,数值范围 0 ~ 255。如果做 +10 运算:
原本 ≤245 的值 → 正常加 10
原本 >245 的值 → 会溢出从头算(255+1=0)
比如:
255 + 10 = 9 (不是 265!)
250 + 10 = 260 → 4
这叫 uint8 溢出(wrap around)。

import cv2
img_cat = cv2.imread('cat.jpg')  # 读取图片,dtype=uint8 (0-255)
img_cat2 = img_cat + 10          # 每个像素值 +10
img_cat[:5, :, 0]                # 查看:前5行、所有列、第0通道(B) 的像素值

输出:

array([[6, 6, 5, ..., 2, 2, 2],
       [5, 5, 4, ..., 2, 2, 2],
       [4, 4, 3, ..., 2, 2, 2],
       [3, 3, 1, ..., 3, 3, 3],
       [4, 3, 2, ..., 3, 3, 3]], shape=(5, 300), dtype=uint8)

img_cat2[:5,:,0] 输出

array([[16, 16, 15, ..., 12, 12, 12],
       [15, 15, 14, ..., 12, 12, 12],
       [14, 14, 13, ..., 12, 12, 12],
       [13, 13, 11, ..., 13, 13, 13],
       [14, 13, 12, ..., 13, 13, 13]], shape=(5, 300), dtype=uint8)

相当于% 256
(img_cat + img_cat2)[:5,:,0]  输出

array([[22, 22, 20, ..., 14, 14, 14],
       [20, 20, 18, ..., 14, 14, 14],
       [18, 18, 16, ..., 14, 14, 14],
       [16, 16, 12, ..., 16, 16, 16],
       [18, 16, 14, ..., 16, 16, 16]], shape=(5, 300), dtype=uint8)

cv2.add(img_cat,img_cat2)[:5,:,0] 称为 OpenCV 安全加法。
规则:相加超过 255 就截断成 255
公式:min(a + b, 255)
例子:255 + 10 = 265 → 255

5.图像缩放与融合

两张照片可以直接进行相加融合,但是前提条件是两张照片 shape 形状相同。

import cv2
img_cat=cv2.imread('cat.jpg')
img_dog=cv2.imread('dog.jpg')
img_cat + img_dog

运行报错

---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
Cell In[24], line 4
      1 import cv2
      2 img_cat=cv2.imread('cat.jpg')
      3 img_dog=cv2.imread('dog.jpg')
----> 4 img_cat + img_dog

ValueError: operands could not be broadcast together with shapes (180,300,3) (193,300,3) 

使用

import cv2
import matplotlib.pyplot as plt

# 读取图片
img_cat = cv2.imread('cat.jpg')
img_dog = cv2.imread('dog.jpg')

# 把狗的尺寸改成 和猫一样大(必须同尺寸才能融合)
img_dog = cv2.resize(img_dog, (300, 180))

# 图像融合:权重相加
res = cv2.addWeighted(img_cat, 0.4, img_dog, 0.6, 0)

# 显示
plt.imshow(res)

cv2.addWeighted 图像融合公式

dst = cv2.addWeighted(src1, alpha, src2, beta, gamma)

参数说明

  • src1:第一张输入图像
  • alpha:第一张图权重(0~1 常用)
  • src2:第二张输入图像
  • beta:第二张图权重(0~1 常用)
  • gamma:亮度偏移量(标量,整体加值)
  • dst:输出融合图像

计算公式

dst = src1 × α + src2 × β + γ

关键前提:两张图像尺寸、通道数必须完全一致,否则报错。

OpenCV 读取是 BGR 顺序,matplotlib 显示是 RGB 顺序,所以直接用 plt.imshow() 颜色会失真、变蓝 / 变绿!只是缺少 BGR 转 RGB,所以颜色不对,加一行 cv2.cvtColor(res, cv2.COLOR_BGR2RGB) 就完美了

res_rgb = cv2.cvtColor(res, cv2.COLOR_BGR2RGB)
plt.imshow(res_rgb)

cv2.resize 有两种用法:

1.指定目标尺寸

cv2.resize(img, (宽, 高))

2.指定缩放比例

cv2.resize(img, (0,0), fx=倍数, fy=倍数)

2.平移与旋转

图像平移将图像按照指定方向和距离,移动到相应的位置

cv.warpAffine(img,M,dsize)

参数:

  • img: 输入图像

  • M: 2∗3移动矩阵

    对于(x,y)处的像素点,要把它移动到处时,M矩阵应如下设置:

    注意:将M设置为np.float32类型的Numpy数组。

  • dsize: 输出图像的大小

    注意:输出图像的大小,它应该是(宽度,高度)的形式。请记住,width=列数,height=行数。

  • 示例

需求是将图像的像素点移动(50,100)的距离:

import numpy as np
import cv2 as cv
import matplotlib.pyplot as plt
# 1. 读取图像
img = cv.imread("cat.jpg")

# 2. 图像平移
rows,cols = img.shape[:2]
M = M = np.float32([[1,0,100],[0,1,50]])# 平移矩阵
dst = cv.warpAffine(img,M,(cols,rows))

# 3. 图像显示
fig,axes=plt.subplots(nrows=1,ncols=2,figsize=(10,8),dpi=100)
axes[0].imshow(img[:,:,::-1])
axes[0].set_title("Original Image")
axes[1].imshow(dst[:,:,::-1])
axes[1].set_title("Move")
plt.show()

3 图像旋转

图像旋转是指图像按照某个位置转动一定角度的过程,旋转中图像仍保持这原始尺寸。图像旋转后图像的水平对称轴、垂直对称轴及中心坐标原点都可能会发生变换,因此需要对图像旋转中的坐标进行相应转换。

那图像是怎么进行旋转的呢?如下图所示:

假设图像逆时针旋转θ,则根据坐标转换可得旋转转换为:

其中:

带入上面的公式中,有:

也可以写成:

同时我们要修正原点的位置,因为原图像中的坐标原点在图像的左上角,经过旋转后图像的大小会有所变化,原点也需要修正。

假设在旋转的时候是以旋转中心为坐标原点的,旋转结束后还需要将坐标原点移到图像左上角,也就是还要进行一次变换。

在OpenCV中图像旋转首先根据旋转角度和旋转中心获取旋转矩阵,然后根据旋转矩阵进行变换,即可实现任意角度和任意中心的旋转效果

cv2.getRotationMatrix2D(center, angle, scale)

参数:

返回:

  • center:旋转中心
  • angle:旋转角度
  • scale:缩放比例
  • M:旋转矩阵

    调用cv.warpAffine完成图像的旋转

示例

import numpy as np
import cv2 as cv
import matplotlib.pyplot as plt

img = cv.imread("cat.jpg")

rows,cols = img.shape[:2]
# 1 生成旋转矩阵
M = cv.getRotationMatrix2D((cols/2,rows/2),90,1)
# 2 进行旋转变换
dst = cv.warpAffine(img,M,(cols,rows))

# 3 图像展示
fig,axes=plt.subplots(nrows=1,ncols=2,figsize=(10,8),dpi=100)
axes[0].imshow(img1[:,:,::-1])
axes[0].set_title("Original Image")
axes[1].imshow(dst[:,:,::-1])
axes[1].set_title("Rotation")
plt.show()

4 仿射变换与透射变换

图像的仿射变换涉及到图像的形状位置角度的变化,是深度学习预处理中常到的功能,仿射变换主要是对图像的缩放,旋转,翻转和平移等操作的组合。

那什么是图像的仿射变换,如下图所示,图1中的点1, 2 和 3 与图二中三个点一一映射, 仍然形成三角形, 但形状已经大大改变,通过这样两组三点(感兴趣点)求出仿射变换, 接下来我们就能把仿射变换应用到图像中所有的点中,就完成了图像的仿射变换。

在OpenCV中,仿射变换的矩阵是一个2×3的矩阵

其中左边的2×2子矩阵$A$是线性变换矩阵,右边的2×1子矩阵$B$是平移项:

对于图像上的任一位置(x,y),仿射变换执行的是如下的操作:

需要注意的是,对于图像而言,宽度方向是x,高度方向是y,坐标的顺序和图像像素对应下标一致。所以原点的位置不是左下角而是右上角,y的方向也不是向上,而是向下。

在仿射变换中,原图中所有的平行线在结果图像中同样平行。为了创建这个矩阵我们需要从原图像中找到三个点以及他们在输出图像中的位置。然后cv2.getAffineTransform 会创建一个 2x3 的矩阵,最后这个矩阵会被传给函数 cv2.warpAffine。

示例

import numpy as np
import cv2 as cv
import matplotlib.pyplot as plt
# 1 图像读取
img = cv.imread("cat.jpg")

# 2 仿射变换
rows,cols = img.shape[:2]
# 2.1 创建变换矩阵
pts1 = np.float32([[50,50],[200,50],[50,200]])
pts2 = np.float32([[100,100],[200,50],[100,250]])
M = cv.getAffineTransform(pts1,pts2)
# 2.2 完成仿射变换
dst = cv.warpAffine(img,M,(cols,rows))

# 3 图像显示
fig,axes=plt.subplots(nrows=1,ncols=2,figsize=(10,8),dpi=100)
axes[0].imshow(img[:,:,::-1])
axes[0].set_title("Original Image")
axes[1].imshow(dst[:,:,::-1])
axes[1].set_title("Affine")
plt.show()

5 透射变换

透射变换是视角变化的结果,是指利用透视中心、像点、目标点三点共线的条件,按透视旋转定律使承影面(透视面)绕迹线(透视轴)旋转某一角度,破坏原有的投影光线束,仍能保持承影面上投影几何图形不变的变换。

它的本质将图像投影到一个新的视平面,其通用变换公式为:

其中,(u,v)是原始的图像像素坐标,w取值为1,(x=x'/z',y=y'/z')是透射变换后的结果。后面的矩阵称为透视变换矩阵,一般情况下,我们将其分为三部分:

其中:T1表示对图像进行线性变换,T2对图像进行平移,T3表示对图像进行投射变换,a22一般设为1.

在opencv中,我们要找到四个点,其中任意三个不共线,然后获取变换矩阵T,再进行透射变换。通过函数cv.getPerspectiveTransform找到变换矩阵,将cv.warpPerspective应用于此3x3变换矩阵。

import numpy as np
import cv2 as cv
import matplotlib.pyplot as plt
# 1 读取图像
img = cv.imread("cat.jpg")
# 2 透射变换
rows,cols = img.shape[:2]
# 2.1 创建变换矩阵
pts1 = np.float32([[56,65],[368,52],[28,387],[389,390]])
pts2 = np.float32([[10,15],[200,100],[80,290],[310,300]])

T = cv.getPerspectiveTransform(pts1,pts2)
# 2.2 进行变换
dst = cv.warpPerspective(img,T,(cols,rows))

# 3 图像显示
fig,axes=plt.subplots(nrows=1,ncols=2,figsize=(10,8),dpi=100)
axes[0].imshow(img[:,:,::-1])
axes[0].set_title("Original Image")
axes[1].imshow(dst[:,:,::-1])
axes[1].set_title("Perspective")
plt.show()

3.图像阈值(二值化)

cv2.threshold 是 OpenCV 里最简单的图像分割方法,把图像变成只有黑和白两种颜色(二值图)。threshold [ˈθreʃhəʊld] 阈值。

ret, dst = cv2.threshold(src, thresh, maxval, type)

src原图(最好是灰度图

thresh阈值(你自己设定的分割线,比如 127)

maxval最大值(一般填 255

type阈值类型(决定怎么分割)

thresh /θreʃ/ 脱粒;binary /ˈbaɪnəri/ 二进制的;trunc 截断;tozero 归零

ret 返回你设定的阈值dst处理后的二值图像

import cv2 
import matplotlib.pyplot as plt


img = cv2.imread('cat.jpg')
# 2. 将图片转换为灰度图(阈值处理必须使用灰度图)
img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# 3. 将BGR格式转换为RGB格式,方便matplotlib正常显示原图颜色
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)

# 4. 5种不同的阈值处理
# 二进制阈值:大于127(白),小于等于127(黑)
ret, thresh1 = cv2.threshold(img_gray, 127, 255, cv2.THRESH_BINARY)
# 反二进制阈值:大于127(黑),小于等于127(白)
ret, thresh2 = cv2.threshold(img_gray, 127, 255, cv2.THRESH_BINARY_INV)
# 截断阈值:大于127的部分变为127,小于127不变
ret, thresh3 = cv2.threshold(img_gray, 127, 255, cv2.THRESH_TRUNC)
# 阈值化为0:大于127不变,小于127变为0
ret, thresh4 = cv2.threshold(img_gray, 127, 255, cv2.THRESH_TOZERO)
# 反阈值化为0:大于127变为0,小于127不变
ret, thresh5 = cv2.threshold(img_gray, 127, 255, cv2.THRESH_TOZERO_INV)

# 5. 定义每个子图的标题
titles = ['Original Image', 'BINARY', 'BINARY_INV', 'TRUNC', 'TOZERO', 'TOZERO_INV']
# 把所有图片放进列表中,方便循环显示
images = [img, thresh1, thresh2, thresh3, thresh4, thresh5]

# 6. 循环绘制6张子图(2行3列)
for i in range(6):
    # 绘制子图:2行3列,第i+1个位置
    plt.subplot(2, 3, i + 1)
    # 显示图片,cmap='gray'表示以灰度图显示
    plt.imshow(images[i], 'gray')
    # 设置子图标题
    plt.title(titles[i])
    # 隐藏x轴和y轴刻度
    plt.xticks([])
    plt.yticks([])

# 7. 显示所有图像
plt.show()

效果图:

4.HSV 

HSV 是另一种色彩表达模型,和 RGB/BGR 不同,它更贴合人眼对颜色的感知,由三个分量组成:

  1. H (Hue) 色相 / 色调
    • 代表是什么颜色,取值范围 0~179(OpenCV 中)。
    • 对应色环:红→黄→绿→青→蓝→紫,循环变化。
  2. S (Saturation) 饱和度
    • 代表颜色鲜艳程度,取值 0~255
    • 数值越高颜色越浓郁;越低越偏向灰白,0 就是灰度。
  3. V (Value) 明度 / 亮度
    • 代表明暗程度,取值 0~255
    • 0 为纯黑,255 为最亮。
hsv=cv2.cvtColor(img,cv2.COLOR_BGR2HSV)

cv2.imshow("hsv", hsv)
cv2.waitKey(0)    
cv2.destroyAllWindows()


和 BGR/RGB 的区别

  • BGR/RGB:靠三原色混合描述颜色,适合屏幕显示;不适合颜色筛选
  • HSV:把「颜色、鲜艳度、亮度」拆分开,OpenCV 颜色追踪、目标提取首选

补充小知识点

  1. cv2.imshow 直接显示 HSV 图,画面会看起来怪异,因为窗口仍按 BGR 规则解析数据,仅用于查看数组,不代表真实色彩
  2. 常用场景:按指定色相范围抠取特定颜色物体(比如提取画面中的红色、蓝色物体)。

最简记忆

  • H = 什么颜色
  • S = 艳不艳
  • V = 亮不亮

5.图像平滑(滤波)

图像平滑 / 滤波主要用来降噪、模糊图像,常用四类:均值滤波、高斯滤波、中值滤波、方框滤波

先统一前置代码:

import cv2
import matplotlib.pyplot as plt

# 读取图片
img = cv2.imread("cat.jpg")
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)  # 转RGB,方便matplotlib显示

一、均值滤波 cv2.blur()

原理:邻域像素求平均值,简单模糊,降噪同时会丢失细节。 语法:

dst = cv2.blur(src, ksize)
  • ksize:卷积核大小 (w, h),必须为正奇数(如 (3,3)、(5,5))

示例:

# 3×3 均值滤波
blur = cv2.blur(img, (3, 3))

二、高斯滤波 cv2.GaussianBlur()

按高斯分布加权平均,中心像素权重更高,模糊更自然


像素原理图

40+107*2+5+198*2+226*8+223*2+37+68*2+193=3275

3275/20=163.75  
像素原理图
原理:和均值滤波类似,但每个像素的权重由高斯核决定,离中心越近权重越大
特点:比均值滤波更平滑,是最常用的降噪方法,同样会模糊边缘

最常用,模拟人眼模糊,平滑效果更自然,优先用于普通降噪。 语法:

dst = cv2.GaussianBlur(src, ksize, sigmaX)
  • ksize:核大小,宽高必须是奇数
  • sigmaX:X 方向高斯标准差,控制模糊程度;设为 0 会自动计算

示例:

# 5×5 高斯核,sigmaX=0
gauss = cv2.GaussianBlur(img, (5, 5), 0)

三、中值滤波 cv2.medianBlur()

专门处理椒盐噪声(黑白噪点),效果极强,同时保留边缘。 原理:取邻域像素中间值替换当前像素。 语法:

dst = cv2.medianBlur(src, ksize)
  • ksize:单个数字,奇数(3 / 5 / 7)
# 3×3 中值滤波
median = cv2.medianBlur(img, 3)

四、方框滤波(Box Filter)

基本和均值滤波一样,可以选择是否归一化

box = cv2.boxFilter(img, -1, (3,3), normalize=True)  

cv2.imshow('box', box)
cv2.waitKey(0)
cv2.destroyAllWindows()

参数说明

cv2.boxFilter(src, ddepth, ksize, normalize=True)

  • img:输入图像
  • -1:输出图像的深度(-1 表示和原图一致)
  • (3,3):卷积核大小,这里是 3×3
  • normalize=True:是否做归一化

二、和均值滤波的关系

方框滤波其实是均值滤波的 “通用版”:

  • normalize=True 时,和 cv2.blur() 完全等价
    • 公式:输出 = (邻域像素和) / (核大小)
  • normalize=False 时,只是做邻域像素和,不除以核大小
    • 结果容易超出 0-255 范围,会出现 uint8 溢出(255+1=0)

三、两种模式对比

import cv2

img = cv2.imread('lenaNoise.png')

# 1. 归一化(等价于均值滤波)
box_norm = cv2.boxFilter(img, -1, (3,3), normalize=True)
# 2. 不归一化(直接求和,容易溢出)
box_no_norm = cv2.boxFilter(img, -1, (3,3), normalize=False)
# 3. 对比均值滤波
blur = cv2.blur(img, (3,3))

cv2.imshow('original', img)
cv2.imshow('box normalized', box_norm)
cv2.imshow('box no normalize', box_no_norm)
cv2.imshow('blur', blur)
cv2.waitKey(0)
cv2.destroyAllWindows()

四、关键结论

  • cv2.boxFilter + normalize=True = cv2.blur
  • normalize=False 只在特殊场景用,普通降噪推荐用归一化模式
  • 不归一化的结果很容易因为像素值过大溢出,导致图像变白或出现异常

完整对比代码(一键运行 + 可视化)

import cv2
import matplotlib.pyplot as plt

# 读图并转RGB
img = cv2.imread("lenaNoise.png")
img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)

# 四种滤波
blur = cv2.blur(img_rgb, (3, 3))                # 均值
gauss = cv2.GaussianBlur(img_rgb, (5, 5), 0)   # 高斯
median = cv2.medianBlur(img_rgb, 3)            # 中值
box = cv2.boxFilter(img, -1, (3,3), normalize=False) # 方框

# 绘图展示
titles = ["Original", "Blur", "Gaussian", "Median", "Box"]
imgs = [img_rgb, blur, gauss, median, box]

plt.figure(figsize=(12,6))
for i in range(5):
    plt.subplot(2,3,i+1)
    plt.imshow(imgs[i])
    plt.title(titles[i])
    plt.xticks([]), plt.yticks([])
plt.show()

补充要点

  • 核尺寸越大,模糊程度越强
  • 均值、高斯会把边缘一起模糊掉;中值能较好保留边缘。

6.腐蚀操作(Erosion)

腐蚀是形态学处理的基础操作,核心作用是收缩图像中亮区域、消除细小噪声、断开粘连物体边缘,常用来处理二值图像。erosion [ɪˈroʊʒn] 腐蚀

如下是一个十字核(结构B)在原图上腐蚀的过程,结构B的中心点会在结构A上每个位置扫一遍,只要出现有一个绿色和红色点位对不上就会导致该点位被腐蚀。

一、腐蚀操作原理

和你之前看的卷积类似,腐蚀也是用一个 ** 结构元素(核)** 在图像上滑动:

  1. 核的中心对准图像的每个像素;
  2. 取核覆盖区域内的最小值,作为中心像素的新值;
  3. 对二值图来说,只要核内有一个像素是 0(黑色),中心像素就会被 “腐蚀” 成 0,相当于把亮区域的边缘 “啃掉” 一圈。

二、OpenCV 核心函数

cv2.erode(src, kernel, iterations=1)
  • src:输入图像(一般是二值图,也支持灰度 / 彩色图)
  • kernel:腐蚀用的结构元素(核),决定腐蚀的形状和大小
  • iterations:腐蚀次数,次数越多,图像收缩越明显

常用核的创建方式

# 1. 3×3 全1正方形核(最常用)
kernel = np.ones((3,3), np.uint8)

# 2. 自定义形状核(如十字形、圆形,用 cv2.getStructuringElement)
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (3,3))  # 矩形核
kernel = cv2.getStructuringElement(cv2.MORPH_CROSS, (3,3)) # 十字核
kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (3,3)) # 圆形核

三、完整可运行示例

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

# 1. 读取图像(推荐用带噪声/边缘粘连的二值图)
img = cv2.imread('noise.jpg', cv2.IMREAD_GRAYSCALE)

# 2. 创建腐蚀核
kernel = np.ones((3,3), np.uint8)

# 3. 执行腐蚀操作(iterations=1,腐蚀1次)
erosion1 = cv2.erode(img, kernel, iterations=1)
# 腐蚀2次
erosion3 = cv2.erode(img, kernel, iterations=2)

# 4. 对比显示
titles = ['Original Binary', 'Erosion 1x', 'Erosion 3x']
images = [img, erosion1, erosion3]

for i in range(3):
    plt.subplot(1,3,i+1)
    plt.imshow(images[i], cmap='gray')
    plt.title(titles[i])
    plt.xticks([]), plt.yticks([])
plt.show()

四、关键效果与用途

场景效果说明
去除椒盐噪声细小的白色噪点会被腐蚀消失
断开粘连物体两个靠得近的物体,边缘被腐蚀后会分开
细化轮廓文字、线条会变细,适合骨架提取前处理

注意:腐蚀次数不是越多越好,过度腐蚀会让目标物体被过度收缩,甚至消失。

五、和膨胀操作的关系(延伸)

腐蚀的 “反向操作” 是膨胀(cv2.dilate),它会让亮区域变大。dilate [daɪˈleɪt ] 扩张
 

  • 腐蚀 + 膨胀组合:开运算(先腐蚀后膨胀,去噪同时保留物体大小)
  • 膨胀 + 腐蚀组合:闭运算(先膨胀后腐蚀,填充孔洞、连接断裂)
import cv2
import numpy as np
import matplotlib.pyplot as plt

# 1. 读取图像(推荐用带噪声/边缘粘连的二值图)
img = cv2.imread('noise.jpg', cv2.IMREAD_GRAYSCALE)

# 2. 创建腐蚀核
kernel = np.ones((3,3), np.uint8)

# 3. 执行腐蚀操作(iterations=1,腐蚀1次)
erosion1 = cv2.erode(img, kernel, iterations=1)
# 膨胀1次
dilate = cv2.dilate(erosion1,kernel,iterations = 1)

# 4. 对比显示
titles = ['Original Binary', 'Erosion 1x', 'Dilate']
images = [img, erosion1, dilate]

for i in range(3):
    plt.subplot(1,3,i+1)
    plt.imshow(images[i], cmap='gray')
    plt.title(titles[i])
    plt.xticks([]), plt.yticks([])
plt.show()

开运算(Open Operation)
定义:先腐蚀,再膨胀
公式:开运算 = 膨胀(腐蚀(原图))
作用:去除小的白色噪声点,同时保留物体整体大小
场景:去噪、断开物体间的细小粘连

morphology /mɔːrˈfɑːlədʒi/ 形态学

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

# 1. 读取图像(推荐用带噪声/边缘粘连的二值图)
img = cv2.imread('noise.jpg', cv2.IMREAD_GRAYSCALE)

# 2. 创建腐蚀核
kernel = np.ones((3,3), np.uint8)

opening = cv2.morphologyEx(img, cv2.MORPH_OPEN, kernel)

# 4. 对比显示
titles = ['Original Binary', 'Opening']
images = [img, opening]

for i in range(2):
    plt.subplot(1,2,i+1)
    plt.imshow(images[i], cmap='gray')
    plt.title(titles[i])
    plt.xticks([]), plt.yticks([])
plt.show()

闭运算(Close Operation)
定义:先膨胀,再腐蚀
公式:闭运算 = 腐蚀(膨胀(原图))
作用:填充物体内部的小黑洞,连接断裂的线条
场景:修复孔洞、连接断开的物体

# 闭运算
closing = cv2.morphologyEx(img, cv2.MORPH_CLOSE, kernel)

1. 形态学梯度(Gradient)

公式:膨胀 - 腐蚀

  • 效果:提取物体的轮廓边缘
  • 场景:边缘检测、轮廓提取
gradient = cv2.morphologyEx(img, cv2.MORPH_GRADIENT, kernel)

2. 顶帽运算(Top Hat)

公式:原图 - 开运算

  • 效果:提取原图中比背景亮的细小部分(比如白色噪声、纹理)
  • 场景:提取亮细节、校正光照不均
tophat = cv2.morphologyEx(img, cv2.MORPH_TOPHAT, kernel)

3. 黑帽运算(Black Hat)

公式:闭运算 - 原图

  • 效果:提取原图中比背景暗的细小部分(比如物体内部的小黑洞)
  • 场景:提取暗细节、孔洞检测
blackhat = cv2.morphologyEx(img, cv2.MORPH_BLACKHAT, kernel)

完整对比代码(一键运行)

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

# 1. 生成带噪声、粘连和孔洞的演示二值图
h, w = 200, 300
img = np.zeros((h, w), dtype=np.uint8)
# 两个粘连方块
cv2.rectangle(img, (50, 50), (120, 120), 255, -1)
cv2.rectangle(img, (130, 50), (200, 120), 255, -1)
# 方块内部加一个小黑洞
cv2.rectangle(img, (80, 80), (90, 90), 0, -1)
# 加白色噪声点
np.random.seed(0)
noise = (np.random.rand(h, w) > 0.98) * 255
img = cv2.bitwise_or(img, noise.astype(np.uint8))

# 2. 定义核
kernel = np.ones((3,3), np.uint8)

# 3. 各种形态学操作
opening = cv2.morphologyEx(img, cv2.MORPH_OPEN, kernel)    # 开运算
closing = cv2.morphologyEx(img, cv2.MORPH_CLOSE, kernel)  # 闭运算
gradient = cv2.morphologyEx(img, cv2.MORPH_GRADIENT, kernel) # 梯度
tophat = cv2.morphologyEx(img, cv2.MORPH_TOPHAT, kernel)   # 顶帽
blackhat = cv2.morphologyEx(img, cv2.MORPH_BLACKHAT, kernel) # 黑帽

# 4. 显示对比
titles = ['Original', 'Open', 'Close', 'Gradient', 'TopHat', 'BlackHat']
images = [img, opening, closing, gradient, tophat, blackhat]

plt.figure(figsize=(12, 6))
for i in range(6):
    plt.subplot(2, 3, i+1)
    plt.imshow(images[i], cmap='gray')
    plt.title(titles[i])
    plt.xticks([]), plt.yticks([])
plt.tight_layout()
plt.show()

7.图像梯度-Sobel算子

Sobel 算子用于检测图像边缘,原理是计算像素灰度的变化率(梯度),分为 水平梯度(X 方向)垂直梯度(Y 方向)

# Sobel 算子函数
dst = cv2.Sobel(src, ddepth, dx, dy, ksize)

参数说明:

  1. src:输入图像(建议先转灰度图)
  2. ddepth:输出图像深度,推荐填 cv2.CV_64F。原因:边缘存在正负梯度,uint8 会截断负数,丢失反向边缘
  3. dxX 方向梯度(水平边缘),1开启 / 0关闭
  4. dyY 方向梯度(垂直边缘),1开启 / 0关闭
  5. ksize:卷积核大小,必须是奇数,常用 3

img = cv2.imread('pie.png', cv2.IMREAD_GRAYSCALE)
sobelx = cv2.Sobel(img, cv2.CV_64F, 1, 0, ksize=3)
cv_show(sobelx, 'sobelx')

二、关键知识点

  1. X 方向梯度 dx=1, dy=0 检测垂直边缘(左右灰度变化大的位置)
  2. Y 方向梯度 dx=0, dy=1 检测水平边缘(上下灰度变化大的位置)
  3. 取绝对值 Sobel 计算会出现负值,用 cv2.convertScaleAbs() 取绝对值,还原完整边缘。
img = cv2.imread('pie.png',cv2.IMREAD_GRAYSCALE)
sobelx = cv2.Sobel(img,cv2.CV_64F,1,0,ksize=3)
sobelx = cv2.convertScaleAbs(sobelx)
cv_show(sobelx,'sobelx')

三、完整可运行代码

单方向 + 合并梯度演示

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

# 读取图像并转灰度图
img = cv2.imread("pie.png")
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

# 1. X方向 Sobel(检测垂直边缘)
sobel_x = cv2.Sobel(gray, cv2.CV_64F, dx=1, dy=0, ksize=3)
sobel_x = cv2.convertScaleAbs(sobel_x)  # 取绝对值

# 2. Y方向 Sobel(检测水平边缘)
sobel_y = cv2.Sobel(gray, cv2.CV_64F, dx=0, dy=1, ksize=3)
sobel_y = cv2.convertScaleAbs(sobel_y)

# 3. 合并 X+Y 梯度(完整边缘)
sobel_xy = cv2.addWeighted(sobel_x, 0.5, sobel_y, 0.5, 0)

# 可视化对比
titles = ["Original Gray", "Sobel X", "Sobel Y", "Sobel X+Y"]
images = [gray, sobel_x, sobel_y, sobel_xy]

plt.figure(figsize=(12, 6))
for i in range(4):
    plt.subplot(1, 4, i+1)
    plt.imshow(images[i], cmap="gray")
    plt.title(titles[i])
    plt.xticks([]), plt.yticks([])
plt.show()

四、易错点强调

  1. 不要直接用 uint8 灰度由亮→暗时,梯度为负数,uint8 会直接置 0,边缘缺失。 正确流程:CV_64F 计算 → 取绝对值 → 转回正常图像。

  2. 不建议直接 dx=1, dy=1 cv2.Sobel(gray, cv2.CV_64F, 1, 1, 3) 效果差、边缘模糊, 标准做法:分别计算 X、Y 再加权融合

五、简化写法(一行合并)

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

# 读取图像并转灰度图
img = cv2.imread("cat.jpg")
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

# 分别求梯度再融合(推荐)
gx = cv2.convertScaleAbs(cv2.Sobel(gray, cv2.CV_64F, 1, 0, 3))
gy = cv2.convertScaleAbs(cv2.Sobel(gray, cv2.CV_64F, 0, 1, 3))
combined = cv2.add(gx, gy)


# 可视化对比
titles = ["Original Gray", "combined"]
images = [gray, combined]

plt.figure(figsize=(12, 6))
for i in range(2):
    plt.subplot(1,2, i+1)
    plt.imshow(images[i], cmap="gray")
    plt.title(titles[i])
    plt.xticks([]), plt.yticks([])
plt.show()

六、总结

  • dx=1,dy=0垂直边缘
  • dx=0,dy=1水平边缘
  • 固定搭配:CV_64F + convertScaleAbs()
  • 完整边缘:分方向计算后再相加 / 加权融合

图像梯度-Scharr算子

图像梯度-laplacian算子

img = cv2.imread('cat.jpg',cv2.IMREAD_GRAYSCALE)
sobelx = cv2.Sobel(img,cv2.CV_64F,1,0,ksize=3)
sobely = cv2.Sobel(img,cv2.CV_64F,0,1,ksize=3)
sobelx = cv2.convertScaleAbs(sobelx)   
sobely = cv2.convertScaleAbs(sobely)  
sobelxy =  cv2.addWeighted(sobelx,0.5,sobely,0.5,0)  

scharrx = cv2.Scharr(img,cv2.CV_64F,1,0)
scharry = cv2.Scharr(img,cv2.CV_64F,0,1)
scharrx = cv2.convertScaleAbs(scharrx)   
scharry = cv2.convertScaleAbs(scharry)  
scharrxy =  cv2.addWeighted(scharrx,0.5,scharry,0.5,0) 

laplacian = cv2.Laplacian(img,cv2.CV_64F)
laplacian = cv2.convertScaleAbs(laplacian)   

res = np.hstack((sobelxy,scharrxy,laplacian))
cv_show(res,'res')

8.Canny 边缘检测

Canny 是最优边缘检测算法,流程严谨、抗噪性强,是工程中最常用的边缘提取方法。

一、函数语法

edges = cv2.Canny(image, threshold1, threshold2)

参数说明:

  • image:输入图像,建议先转灰度图
  • threshold1:低阈值
  • threshold2:高阈值

阈值规则(核心)

  1. 像素梯度 > 高阈值:判定为强边缘,保留
  2. 像素梯度 < 低阈值:判定为非边缘,直接剔除
  3. 介于两阈值之间:仅当和强边缘相连时,才保留为边缘

经验:高低阈值比例常取 1:2 ~ 1:3

二、Canny 完整四大步骤

  1. 高斯滤波:先模糊图像,抑制噪声(算法内部自动执行)
  2. 计算梯度:用类似 Sobel 算子,求 X/Y 方向梯度与方向
  3. 非极大值抑制:只保留局部梯度最大值,细化边缘(单像素边缘)
  4. 双阈值检测 + 边缘连接:按高低阈值筛选、补充边缘
1:高斯滤波器

2:梯度和方向

三、基础使用代码

import cv2
import matplotlib.pyplot as plt

# 读取图片并转灰度
img = cv2.imread("cat.jpg")
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

# Canny 边缘检测
canny = cv2.Canny(gray, 50, 150)

# 显示对比
plt.subplot(121), plt.imshow(gray, cmap="gray"), plt.title("Gray")
plt.subplot(122), plt.imshow(canny, cmap="gray"), plt.title("Canny Edge")
plt.xticks([]), plt.yticks([])
plt.show()

四、调节阈值对比(直观演示)

阈值大小直接控制边缘多少

  • 阈值偏小:检出大量细节、噪声也会被当成边缘
  • 阈值偏大:只保留明显主边缘,细小边缘丢失
import cv2
import matplotlib.pyplot as plt

img = cv2.imread("cat.jpg")
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

# 不同阈值组合
edge1 = cv2.Canny(gray, 20, 60)
edge2 = cv2.Canny(gray, 50, 150)
edge3 = cv2.Canny(gray, 100, 200)

titles = ["20/60", "50/150", "100/200"]
imgs = [edge1, edge2, edge3]

plt.figure(figsize=(12, 4))
for i in range(3):
    plt.subplot(1, 3, i + 1)
    plt.imshow(imgs[i], cmap="gray")
    plt.title(titles[i])
    plt.xticks([]), plt.yticks([])
plt.show()

9.图像金字塔

图像金字塔是图像多尺度表达的一种,最主要用于图像的分割,是一种以多分辨率来解释图像的有效但概念简单的结构。

图像金字塔用于机器视觉和图像压缩,一幅图像的金字塔是一系列以金字塔形状排列的分辨率逐步降低,且来源于同一张原始图的图像集合。其通过梯次向下采样获得,直到达到某个终止条件才停止采样。

金字塔的底部是待处理图像的高分辨率表示,而顶部是低分辨率的近似,层级越高,图像越小,分辨率越低。

一、高斯金字塔

高斯金字塔实现图像缩小放大,核心两步:高斯模糊 + 尺寸变换。

1. 向下采样(缩小图像)cv2.pyrDown()

作用:图像尺寸变为原来 1/2,宽高都减半,画面缩小。 原理:

  1. 先做高斯模糊降噪
  2. 隔行、隔列删除像素,尺寸减半

dst = cv2.pyrDown(src)

2. 向上采样(放大图像)cv2.pyrUp()

作用:图像尺寸变为原来 2 倍,宽高都翻倍,画面放大。 原理:

  1. 原像素之间插入空白像素(补 0)
  2. 再做高斯模糊填充像素

dst = cv2.pyrUp(src)

重点:先缩小再放大,图像会丢失细节,无法复原

高斯金字塔完整演示代码

import cv2
import matplotlib.pyplot as plt

img = cv2.imread("cat.jpg")
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)

# 向下采样(缩小)
down = cv2.pyrDown(img)

# 向上采样(放大)
up = cv2.pyrUp(img)

up_down = cv2.pyrUp(down)

# 展示
titles = ["Original", "Down", "Up", "Up Down"]
imgs = [img, down, up, up_down]

plt.figure(figsize=(10,6))
for i in range(4):
    plt.subplot(2,2,i+1)
    plt.imshow(imgs[i])
    plt.title(titles[i])   
plt.show()

二、拉普拉斯金字塔

拉普拉斯金字塔不做缩放,用来保存缩放丢失的细节,常配合高斯金字塔做图像重建、图像融合

公式

Li​=Gi​−pyrUp(pyrDown(Gi​))

  • Gi:高斯金字塔第 i 层图像
  • Li:拉普拉斯金字塔第 i 层(差值 = 原图 - 先缩小再放大的图)
  • 结果类似边缘 / 残差图

结合高斯金字塔 + 拉普拉斯金字塔,可以由低层图像 + 残差 还原高层原图

拉普拉斯金字塔代码

import cv2
import matplotlib.pyplot as plt

img = cv2.imread("AM.png")
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)

# 高斯下采样
g1 = cv2.pyrDown(img)
# 高斯上采样
g1_up = cv2.pyrUp(g1)

# 计算拉普拉斯层(残差)
laplacian = img - g1_up

# 显示
plt.subplot(121)
plt.imshow(img)
plt.title("Original")
plt.xticks([]), plt.yticks([])

plt.subplot(122)
plt.imshow(laplacian)
plt.title("Laplacian")
plt.xticks([]), plt.yticks([])
plt.show()

10.图像轮廓

轮廓(Contour[ˈkɑːntʊr])是二值图像中连通的边缘像素集合,常用于物体检测、形状识别、目标裁剪。核心流程:灰度化 → 二值化 → 查找轮廓 → 绘制 / 计算轮廓

一、核心函数说明

1. 基础流程函数

  1. cv2.cvtColor(img, cv2.COLOR_BGR2GRAY):彩色图转灰度图

  2. cv2.threshold():图像二值化(区分前景 / 背景,找轮廓前提)

  3. cv2.findContours()查找轮廓(核心)

    contours, hierarchy = cv2.findContours(binary, mode, method)
    
    • binary:二值图像(黑白图)
    • mode:轮廓检索模式
      • cv2.RETR_EXTERNAL:只检测最外层轮廓(最常用)
      • cv2.RETR_TREE:检测所有轮廓,建立层级关系(内外轮廓)
    • method:轮廓逼近方法
      • cv2.CHAIN_APPROX_SIMPLE:压缩轮廓点,只保留拐点(省内存,推荐)
      • cv2.CHAIN_APPROX_NONE:保留全部轮廓点
    • 返回值:
      • contours:列表,每个元素是一个轮廓 (点坐标数组)
      • hierarchy:轮廓层级(内外轮廓关系,简单场景可忽略)
  4. cv2.drawContours()绘制轮廓

    cv2.drawContours(dst, contours, idx, color, thickness)
    
    • dst:绘制目标图像
    • idx:轮廓索引,-1 表示绘制所有轮廓

2. 轮廓常用计算(形状、面积、周长)

  • 轮廓面积:cv2.contourArea(contour)
  • 轮廓周长:cv2.arcLength(contour, True)True= 闭合轮廓)
  • 外接矩形:cv2.boundingRect(contour)(x, y, w, h)
  • 最小外接矩形 / 圆、多边形拟合:用于形状识别

二、完整入门代码(基础轮廓检测 + 绘制)

示例 1:基础轮廓查找 & 绘制

import cv2
import numpy as np

# 1. 读取图像
img = cv2.imread("pie.png")
img_copy = img.copy()  # 备份原图,用于绘制
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

# 2. 二值化(阈值分割,黑白分离)
ret, binary = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY)

# 3. 查找轮廓
contours, hierarchy = cv2.findContours(
    binary,
    cv2.RETR_TREE,    # 取所有轮廓
    cv2.CHAIN_APPROX_SIMPLE
)

# 4. 绘制所有轮廓:颜色(蓝,绿,红),线宽2
cv2.drawContours(img_copy, contours, -1, (0, 0, 255), 2)

# 5. 显示结果
cv2.imshow("Original", img)
cv2.imshow("Binary", binary)
cv2.imshow("Contours", img_copy)
cv2.waitKey(0)
cv2.destroyAllWindows()

三、实用进阶案例

案例 1:筛选轮廓(按面积过滤噪声)

图片常有小白点噪声,通过轮廓面积过滤小轮廓:

import cv2
import numpy as np

img = cv2.imread("AM.png")
img_copy = img.copy()
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
ret, binary = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY)

contours, hierarchy = cv2.findContours(binary, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)

# 遍历所有轮廓,过滤面积过小的噪声
for cnt in contours:
    area = cv2.contourArea(cnt)
    print(area)
    if area > 200:  # 面积阈值,根据图片调整
        cv2.drawContours(img_copy, [cnt], -1, (0, 255, 0), 2)

cv2.imshow("Filter Contours", img_copy)
cv2.waitKey(0)
cv2.destroyAllWindows()

案例 2:绘制轮廓外接矩形

框选出每个物体的包围盒:

import cv2
import numpy as np

img = cv2.imread("test.jpg")
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
ret, binary = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY)
contours, _ = cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)

for cnt in contours:
    area = cv2.contourArea(cnt)
    if area > 200:
        # 外接矩形:x,y 左上角坐标;w,h 宽高
        x, y, w, h = cv2.boundingRect(cnt)
        # 画矩形
        cv2.rectangle(img, (x, y), (x+w, y+h), (255, 0, 0), 2)

cv2.imshow("Bounding Rect", img)
cv2.waitKey(0)
cv2.destroyAllWindows()

案例 3:Canny 边缘 + 轮廓(复杂图像推荐)

单纯阈值效果差时,先用边缘检测再找轮廓:

import cv2

img = cv2.imread("test.jpg")
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# 高斯模糊去噪
blur = cv2.GaussianBlur(gray, (3, 3), 0)
# Canny边缘检测
edges = cv2.Canny(blur, 50, 150)

# 找轮廓并绘制
contours, _ = cv2.findContours(edges, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
cv2.drawContours(img, contours, -1, (0, 255, 255), 2)

cv2.imshow("Canny Edge", edges)
cv2.imshow("Contour from Edge", img)
cv2.waitKey(0)
cv2.destroyAllWindows()

四、常见问题 & 注意事项

  1. 轮廓找不到 / 全是噪声

    • 先做高斯模糊去噪;
    • 调整二值化阈值 127,或改用自适应二值化 cv2.adaptiveThreshold
  2. 轮廓内外重叠(孔洞也被检测)

    • 检索模式改用 cv2.RETR_EXTERNAL,只保留最外层轮廓。
  3. drawContours 报错

    • 传入单个轮廓时,必须写成 [cnt](列表格式)。
  4. 黑白颠倒(背景白、物体黑)

    • 二值化加反转:cv2.threshold(gray,127,255,cv2.THRESH_BINARY_INV)

五、常用拓展接口

# 1. 轮廓周长
perimeter = cv2.arcLength(cnt, True)

# 2. 多边形拟合(轮廓近似)
approx = cv2.approxPolyDP(cnt, 0.02 * perimeter, True)

# 3. 最小外接圆
(x, y), r = cv2.minEnclosingCircle(cnt)

11.OpenCV 图像直方图(histogram)

1、概念

直方图:统计图像每个像素灰度值 (0~255) 出现的像素数量

  • x 轴:灰度值 0~255
  • y 轴:该灰度的像素个数

用途:对比度调整、亮度分析、直方图均衡化、阈值选取

2、核心函数

① cv2.calcHist () 计算直方图

hist = cv2.calcHist([img], channels, mask, histSize, ranges)
  • [img]:图像放列表里
  • channels=[0]:灰度图;彩色[0]B [1]G [2]R
  • mask:掩码,全图统计填None
  • histSize=[256]:分 256 个 bin
  • ranges=[0,256]:像素范围

② cv2.equalizeHist () 全局直方图均衡(灰度增强)

③ cv2.createCLAHE () 自适应均衡 CLAHE(防局部过曝)

3、代码 1:灰度图直方图绘制

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

img = cv2.imread("cat.jpg",0)

# 计算直方图
hist = cv2.calcHist([img],[0],None,[256],[0,256])

plt.plot(hist)
plt.xlim([0,256])
plt.title("Gray Hist")
plt.show()

4、代码 2:彩色三通道直方图

img = cv2.imread("cat.jpg")
color = ("b","g","r")
for i,c in enumerate(color):
    hist = cv2.calcHist([img],[i],None,[256],[0,256])
    plt.plot(hist,color=c)
plt.xlim([0,256])
plt.show()

5、直方图均衡化(提亮偏暗图片)

全局均衡 equalizeHist

gray = cv2.imread("cat.jpg",0)
equ = cv2.equalizeHist(gray)

cv2.imshow("src",gray)
cv2.imshow("equal",equ)
cv2.waitKey(0)

CLAHE 自适应均衡(推荐,细节保留更好)

clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8,8))
cla_img = clahe.apply(gray)
cv2.imshow("clahe",cla_img)
cv2.waitKey(0)

  • clipLimit:对比度限制,越大增强越强
  • tileGridSize:分块大小

6、掩码局部直方图(只统计图片某一块)

gray = cv2.imread("test.jpg",0)
mask = np.zeros(gray.shape[:2],np.uint8)
mask[100:300,100:300]=255 #感兴趣区域

hist_mask = cv2.calcHist([gray],[0],mask,[256],[0,256])
plt.plot(hist_mask)
plt.show()

7、要点总结

  1. 图像偏暗:直方图扎堆在左侧;偏亮扎堆右侧。
  2. 均衡化:把像素灰度拉开分布,提升对比度。
  3. 全局均衡容易大面积发白,工程优先用 CLAHE

12.模板匹配 templateMatch

一、原理

模板匹配和卷积原理很像,模板在原图像上从原点开始滑动,计算模板与(图像被模板覆盖的地方)的差别程度,这个差别程度的计算方法在opencv里有6种,然后将每次计算的结果放入一个矩阵里,作为结果输出。假如原图形是AxB大小,而模板是axb大小,则输出结果的矩阵是(A-a+1)x(B-b+1)

res = cv2.matchTemplate(img, template, method)
  • img:原图(待搜索大图)
  • template:小模板
  • method:匹配算法(6 种)

二、6 种匹配方法

1. cv2.TM_SQDIFF        平方差 → 值越小越匹配
2. cv2.TM_SQDIFF_NORMED 归一平方差 → 0最佳
3. cv2.TM_CCORR         相关 → 值越大越匹配
4. cv2.TM_CCORR_NORMED  归一相关 → 1最佳
5. cv2.TM_COEFF         相关系数 → 越大越好
6. cv2.TM_COEFF_NORMED  归一相关系数 → 1最佳【最常用】

日常首选:TM_CCOEFF_NORMED

三、完整基础代码

import cv2
import numpy as np

# 大图、小模板
img = cv2.imread("lena.jpg")
tpl = cv2.imread("face.jpg")
h, w = tpl.shape[:2]

# 模板匹配
res = cv2.matchTemplate(img, tpl, cv2.TM_CCOEFF_NORMED)

# 找最大最小值位置
min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(res)

# TM_CCOEFF_NORMED取最大值位置
top_left = max_loc
bottom_right = (top_left[0]+w, top_left[1]+h)

# 画框
cv2.rectangle(img, top_left, bottom_right, (0,0,255), 2)

cv2.imshow("res",res)
cv2.imshow("result", img)
cv2.waitKey(0)
cv2.destroyAllWindows()

四、多目标匹配(阈值筛选多个物体)

一张图多个相同目标,用阈值筛选:

import cv2
import numpy as np

img = cv2.imread("big.jpg")
tpl = cv2.imread("small.jpg")
h,w = tpl.shape[:2]

res = cv2.matchTemplate(img, tpl, cv2.TM_CCOEFF_NORMED)

# 阈值,大于0.7认为匹配成功
threshold = 0.7
loc = np.where(res >= threshold)

# 遍历所有匹配点
for pt in zip(*loc[::-1]):
    cv2.rectangle(img, pt, (pt[0]+w, pt[1]+h), (0,255,0), 2)

cv2.imshow("multi", img)
cv2.waitKey(0)

五、关键点

  1. 模板不能大于原图,否则报错
  2. 归一化系列 (NORMED) 数值范围[0~1],非归一数值浮动大
  3. 缩放、旋转、大小不一样 → 模板匹配失效(模板匹配只支持同尺寸目标)
    • 解决:多尺度遍历缩放模板 / 改用特征匹配 (SIFT)

六、优缺点

✅优点:简单、速度快 ❌缺点:不抗缩放、旋转、形变

13.图像傅里叶变换

傅里叶变换:把空间域图像转为频率域低频对应图像整体灰度、大轮廓;高频对应边缘、噪声、细节。 常用场景:低通滤波 (模糊)、高通滤波 (锐化 / 提边缘)、去噪

一、核心原理

  1. 图像是二维信号,使用 二维离散傅里叶变换 (DFT)
  2. 频域图规则:
    • 图像中心:低频分量
    • 图像四周:高频分量
  3. 流程: 原图 → 灰度图 → 转浮点 → DFT 傅里叶变换 → 频域中心化 → 频域掩码 (滤波) → 逆傅里叶变换 (IDFT) → 转回正常图像

二、关键函数

# 1. 傅里叶正变换
np.fft.fft2(img)       # 二维傅里叶
np.fft.fftshift()      # 低频移到图像中心(中心化,必用)

# 2. 傅里叶逆变换
np.fft.ifftshift()     # 先还原频域位置
np.fft.ifft2()         # 逆傅里叶
np.abs()               # 取幅值(去除复数虚部)

# 3. 可视化频域:对数缩放(原始频域太暗看不清)
np.log(1 + np.abs(频谱))

三、完整代码 1:基础傅里叶变换 + 频域可视化

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

# 1. 读取图像,转灰度
img = cv2.imread("test.jpg", 0)  # 0=直接灰度图
h, w = img.shape

# 2. 转浮点型(傅里叶要求)
img_float = np.float32(img)

# 3. 傅里叶变换
dft = np.fft.fft2(img_float)
dft_shift = np.fft.fftshift(dft)  # 低频移中心

# 4. 频域可视化(对数缩放)
magnitude = 20 * np.log(np.abs(dft_shift))

# 5. 显示原图 & 频谱
plt.figure(figsize=(10,5))
plt.subplot(121),plt.imshow(img, cmap="gray"),plt.title("Original")
plt.subplot(122),plt.imshow(magnitude, cmap="gray"),plt.title("Frequency Spectrum")
plt.show()

四、完整代码 2:低通滤波(模糊、去高频噪声)

低通:保留中心低频,遮挡四周高频 → 图像变模糊、抑制噪声

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

img = cv2.imread("test.jpg", 0)
h, w = img.shape
img_float = np.float32(img)

# 傅里叶变换
dft = np.fft.fft2(img_float)
dft_shift = np.fft.fftshift(dft)

# ========== 构建低通掩码 ==========
# 半径:控制保留多少低频,值越大图像越清晰
radius = 30
mask = np.zeros((h, w), np.uint8)
# 中心坐标
cx, cy = w // 2, h // 2
# 画圆形掩码:只保留中心低频
cv2.circle(mask, (cx, cy), radius, 255, -1)

# 频域相乘滤波
dft_shift = dft_shift * mask

# ========== 逆傅里叶变换 ==========
idft_shift = np.fft.ifftshift(dft_shift)
img_idft = np.fft.ifft2(idft_shift)
img_result = np.abs(img_idft)
# 归一化到 0~255
img_result = cv2.normalize(img_result, None, 0, 255, cv2.NORM_MINMAX, dtype=cv2.CV_8U)

# 显示
plt.figure(figsize=(12, 4))
plt.subplot(131), plt.imshow(img, cmap="gray"), plt.title("Original")
plt.subplot(132), plt.imshow(mask, cmap="gray"), plt.title("Low-Pass Mask")
plt.subplot(133), plt.imshow(img_result, cmap="gray"), plt.title("Low-Pass Result")
plt.show()

五、完整代码 3:高通滤波(提取边缘、锐化)

高通:遮挡中心低频,保留四周高频 → 只显示边缘、细节

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

img = cv2.imread("lena.jpg", 0)
h, w = img.shape
img_float = np.float32(img)

dft = np.fft.fft2(img_float)
dft_shift = np.fft.fftshift(dft)

# ========== 高通掩码(和低通相反) ==========
radius = 30
mask = np.ones((h, w), np.uint8) * 255
cx, cy = w // 2, h // 2
# 涂黑中心低频区域
cv2.circle(mask, (cx, cy), radius, 0, -1)
dft_shift = dft_shift * mask

# 逆变换
idft_shift = np.fft.ifftshift(dft_shift)
img_idft = np.fft.ifft2(idft_shift)
img_result = np.abs(img_idft)
img_result = cv2.normalize(img_result, None, 0, 255, cv2.NORM_MINMAX, dtype=cv2.CV_8U)

# 显示
plt.figure(figsize=(12, 4))
plt.subplot(131), plt.imshow(img, cmap="gray"), plt.title("Original")
plt.subplot(132), plt.imshow(mask, cmap="gray"), plt.title("Low-Pass Mask")
plt.subplot(133), plt.imshow(img_result, cmap="gray"), plt.title("High-Pass (Edge)")
plt.show()

六、核心知识点总结

  1. 必须步骤

    • 图像转 float32
    • fft2fftshift(低频居中)
    • 滤波后:ifftshiftifft2 → 取绝对值
  2. 频域对应关系

    • 低频:整体亮度、大块区域 → 低通滤波 = 模糊、去噪
    • 高频:边缘、纹理、椒盐噪声 → 高通滤波 = 提边缘、锐化
  3. 调参技巧

    • radius 越小:低通越模糊 / 高通边缘越强
    • 频域图发黑:一定要用 20*np.log(1+np.abs(频谱)) 对数缩放可视化
  4. 常见报错

    • 未转 float:傅里叶计算异常
    • 忘记 fftshift:低频不在中心,滤波完全失效
    • 逆变换后不取 abs:出现复数,图像乱色

傅里叶变换在图像处理中的基本原理是将图像从空间域转换到频率域,通过分解图像为不同频率的正弦和余弦波的叠加来分析图像的频率成分。通过傅里叶变换实现图像的频域滤波,首先对图像进行傅里叶变换得到频域表示,然后在频域中应用滤波器去除或增强特定频率成分,最后对滤波后的频域图像进行傅里叶逆变换转换回空间域,得到滤波后的图像。

14.透视变换

透视变换 warpPerspective(四点矫正,证件 / 倾斜文档拉直)

公式原理

dst=M⋅src

  • M:3×3 透视变换矩阵getPerspectiveTransform由 4 组对应点算出
  • 4 个源点(原图倾斜四边形)→4 个目标点(规整矩形)

两个核心 API

#1 根据四点求变换矩阵
M = cv2.getPerspectiveTransform(src_points, dst_points)
#2 执行透视变换
out = cv2.warpPerspective(img, M, (out_w, out_h))

坐标必须 np.float32,四点顺序统一:左上→右上→右下→左下

完整示例:倾斜图转正,输出 400×500

import cv2
import numpy as np

img = cv2.imread("doc.jpg")

# 【原图4个倾斜角点:左上、右上、右下、左下】
src = np.array([[196, 196], [713, 18], [774, 532], [64, 525]], dtype=np.float32)

# 【目标矩形宽500 高300】
W, H = 500, 300
dst = np.array([[0, 0], [W, 0], [W, H], [0, H]], dtype=np.float32)

# 求透视矩阵+变换
M = cv2.getPerspectiveTransform(src, dst)
res = cv2.warpPerspective(img, M, (W, H))

cv2.imshow("src", img)
cv2.imshow("wrap", res)
cv2.waitKey(0)
cv2.destroyAllWindows()

从轮廓自动提取四点 + 自动排序(常用 OCR 文档矫正)

你前面获取的上下左右极值点是乱序,用坐标求和 / 差值自动排序四点

def order_points(pts):
    # pts:(4,2) 无序四点,返回规范[左上,右上,右下,左下]
    rect = np.zeros((4,2), dtype=np.float32)
    s = pts.sum(axis=1)
    rect[0] = pts[np.argmin(s)]   #左上:x+y最小
    rect[2] = pts[np.argmax(s)]   #右下:x+y最大

    diff = np.diff(pts,axis=1)
    rect[1] = pts[np.argmin(diff)]#右上:y-x最小
    rect[3] = pts[np.argmax(diff)]#左下:y-x最大
    return rect

#====使用====
# cnt是目标物体轮廓
pts = cnt.reshape(-1,2)
# 取轮廓四个极值点(也可用minAreaRect四点)
left=pts[np.argmin(pts[:,0])]
right=pts[np.argmax(pts[:,0])]
top=pts[np.argmin(pts[:,1])]
bot=pts[np.argmax(pts[:,1])]
four = np.array([left,right,top,bot])
src = order_points(four) #自动排好顺序

#目标尺寸
W,H=400,500
dst = np.array([[0,0],[W,0],[W,H],[0,H]],np.float32)
M = cv2.getPerspectiveTransform(src,dst)
res = cv2.warpPerspective(img,M,(W,H))

仿射 vs 透视区别(容易混淆)

  1. 仿射变换 warpAffine:3 对点,平行关系不变,只能平移 / 旋转 / 缩放 / 错切
  2. 透视变换 warpPerspective:4 对点,任意四边形→矩形,倾斜矫正必备

易错点

  1. 四点顺序错乱 → 图片扭曲撕裂
  2. warpPerspective(..., (宽度,高度)),先宽后高
  3. 坐标必须float32,int 报错

15.霍夫变换

霍夫变换常用来提取图像中的直线和圆等几何形状,如下图所示:

1 原理

  1. 原理

在笛卡尔坐标系中,一条直线由两个点A=(x1,y1)A=(x​1​​,y​1​​)和B=(x2,y2)B=(x​2​​,y​2​​)确定,如下图所示:

将直线y=kx+q可写成关于(k,q)(k,q)的函数表达式:

对应的变换通过图形直观的表示下:

变换后的空间我们叫做霍夫空间。即:笛卡尔坐标系中的一条直线,对应于霍夫空间中的一个点。反过来,同样成立,霍夫空间中的一条线,对应于笛卡尔坐标系中一个点,如下所示: 

我们再来看下A、B两个点,对应于霍夫空间的情形:

在看下三点共线的情况:

可以看出如果在笛卡尔坐标系的点共线,那么这些点在霍夫空间中对应的直线交于一点

如果不止存在一条直线时,如下所示:

我们选择尽可能多的直线汇成的点,上图中三条直线汇成的A、B两点,将其对应回笛卡尔坐标系中的直线:

到这里我们似乎已经完成了霍夫变换的求解。但如果像下图这种情况时:

上图中的直线是x=2x=2,那(k,q)(k,q)怎么确定呢?

为了解决这个问题,我们考虑将笛卡尔坐标系转换为极坐标。

在极坐标下是一样的,极坐标中的点对应于霍夫空间的线,这时的霍夫空间是不在是参数(k,q)(k,q)的空间,而是(ρ,θ)的空间,ρ是原点到直线的垂直距离,θθ表示直线的垂线与横轴顺时针方向的夹角,垂直线的角度为0度,水平线的角度是180度。

我们只要求得霍夫空间中的交点的位置,即可得到原坐标系下的直线。

实现流程

假设有一个大小为100∗100的图片,使用霍夫变换检测图片中的直线,则步骤如下所示:

  • 直线都可以使用(ρ,θ)表示,首先创建一个2D数组,我们叫做累加器,初始化所有值为0,行表示ρρ ,列表示θ 。

  • 该数组的大小决定了结果的准确性,若希望角度的精度为1度,那就需要180列。对于ρρ,最大值为图片对角线的距离,如果希望精度达到像素级别,行数应该与图像的对角线的距离相等。

  • 取直线上的第一个点(x,y),将其带入直线在极坐标中的公式中,然后遍历θ的取值:0,1,2,...,180,分别求出对应的ρρ值,如果这个数值在上述累加器中存在相应的位置,则在该位置上加1.

  • 取直线上的第二个点,重复上述步骤,更新累加器中的值。对图像中的直线上的每个点都直线以上步骤,每次更新累加器中的值。

  • 搜索累加器中的最大值,并找到其对应的(ρ,θ),就可将图像中的直线表示出来。

2 霍夫线检测

在OpenCV中做霍夫线检测是使用的API是:

cv.HoughLines(img, rho, theta, threshold)

参数:

  • img: 检测的图像,要求是二值化的图像,所以在调用霍夫变换之前首先要进行二值化,或者进行Canny边缘检测

  • rho、theta: ρρ 和θθ的精确度

threshold: 阈值,只有累加器中的值高于该阈值时才被认为是直线。

霍夫线检测的整个流程如下图所示,这是在stackflow上一个关于霍夫线变换的解释:

示例:

检测下述图像中的直线:

import numpy as np
import random
import cv2 as cv
import matplotlib.pyplot as plt
# 1.加载图片,转为二值图
img = cv.imread('rili.jpg')

gray = cv.cvtColor(img, cv.COLOR_BGR2GRAY)
edges = cv.Canny(gray, 20, 60)


# 2.霍夫直线变换
lines = cv.HoughLines(edges, 0.8, np.pi / 180, 250)
# 3.将检测的线绘制在图像上(注意是极坐标噢)
for line in lines:
    rho, theta = line[0]
    a = np.cos(theta)
    b = np.sin(theta)
    x0 = a * rho
    y0 = b * rho
    x1 = int(x0 + 1000 * (-b))
    y1 = int(y0 + 1000 * (a))
    x2 = int(x0 - 1000 * (-b))
    y2 = int(y0 - 1000 * (a))
    cv.line(img, (x1, y1), (x2, y2), (0, 255, 0))
# 4. 图像显示
plt.figure(figsize=(10,8),dpi=100)
plt.imshow(img[:,:,::-1]),plt.title('HoughLines')
plt.xticks([]), plt.yticks([])
plt.show()

3 霍夫圆检测[了解]

圆的表示式是:

其中a和b表示圆心坐标,r表示圆半径,因此标准的霍夫圆检测就是在这三个参数组成的三维空间累加器上进行圆形检测,此时效率就会很低,所以OpenCV中使用霍夫梯度法进行圆形的检测。

霍夫梯度法将霍夫圆检测范围两个阶段,第一阶段检测圆心,第二阶段利用圆心推导出圆半径。

原则上霍夫变换可以检测任何形状,但复杂的形状需要的参数就多,霍夫空间的维数就多,因此在程序实现上所需的内存空间以及运行效率上都不利于把标准霍夫变换应用于实际复杂图形的检测中。霍夫梯度法是霍夫变换的改进,它的目的是减小霍夫空间的维度,提高效率。

  • 圆心检测的原理:圆心是圆周法线的交汇处,设置一个阈值,在某点的相交的直线的条数大于这个阈值就认为该交汇点为圆心。

  • 圆半径确定原理:圆心到圆周上的距离(半径)是相同的,确定一个阈值,只要相同距离的数量大于该阈值,就认为该距离是该圆心的半径。

在OpenCV中检测图像中的圆环使用的是API是:

circles = cv.HoughCircles(image, method, dp, minDist, param1=100, param2=100, minRadius=0,maxRadius=0 )

参数:

返回:

  • image:输入图像,应输入灰度图像

  • method:使用霍夫变换圆检测的算法,它的参数是CV_HOUGH_GRADIENT

  • dp:霍夫空间的分辨率,dp=1时表示霍夫空间与输入图像空间的大小一致,dp=2时霍夫空间是输入图像空间的一半,以此类推

  • minDist为圆心之间的最小距离,如果检测到的两个圆心之间距离小于该值,则认为它们是同一个圆心

  • param1:边缘检测时使用Canny算子的高阈值,低阈值是高阈值的一半。

  • param2:检测圆心和确定半径时所共有的阈值

  • minRadius和maxRadius为所检测到的圆半径的最小值和最大值

  • circles:输出圆向量,包括三个浮点型的元素——圆心横坐标,圆心纵坐标和圆半径

实现

由于霍夫圆检测对噪声比较敏感,所以首先对图像进行中值滤波。

import cv2 as cv
import numpy as np
import matplotlib.pyplot as plt
# 1 读取图像,并转换为灰度图
planets = cv.imread("star.png")
gay_img = cv.cvtColor(planets, cv.COLOR_BGRA2GRAY)
# 2 进行中值模糊,去噪点
img = cv.medianBlur(gay_img, 7)  
# 3 霍夫圆检测
circles = cv.HoughCircles(img, cv.HOUGH_GRADIENT, 1, 140, param1=100, param2=30, minRadius=0, maxRadius=500)
print(circles)
# 4 将检测结果绘制在图像上
for i in circles[0, :]:  # 遍历矩阵每一行的数据
    # 绘制圆形
    cv.circle(planets, (int(i[0]), int(i[1])), int(i[2]), (0, 255, 0), 2)
    # 绘制圆心
    cv.circle(planets, (int(i[0]), int(i[1])), 2, (0, 0, 255), 3)
# 5 图像显示
plt.figure(figsize=(10,8),dpi=100)
plt.imshow(planets[:,:,::-1]),plt.title('HoughCircles')
plt.xticks([]), plt.yticks([])
plt.show()

16.图像的特征

大多数人都玩过拼图游戏。首先拿到完整图像的碎片,然后把这些碎片以正确的方式排列起来从而重建这幅图像。如果把拼图游戏的原理写成计算机程序,那计算机就也会玩拼图游戏了。

在拼图时,我们要寻找一些唯一的特征,这些特征要适于被跟踪,容易被比较。我们在一副图像中搜索这样的特征,找到它们,而且也能在其他图像中找到这些特征,然后再把它们拼接到一起。我们的这些能力都是天生的。

那这些特征是什么呢?我们希望这些特征也能被计算机理解。

如果我们深入的观察一些图像并搜索不同的区域,以下图为例:

在图像的上方给出了六个小图。找到这些小图在原始图像中的位置。你能找到多少正确结果呢?

A 和 B 是平面,而且它们的图像中很多地方都存在。很难找到这些小图的准确位置。

C 和 D 也很简单。它们是建筑的边缘。可以找到它们的近似位置,但是准确位置还是很难找到。这是因为:沿着边缘,所有的地方都一样。所以边缘是比平面更好的特征,但是还不够好。

最后 E 和 F 是建筑的一些角点。它们能很容易的被找到。因为在角点的地方,无论你向哪个方向移动小图,结果都会有很大的不同。所以可以把它们当 成一个好的特征。为了更好的理解这个概念我们再举个更简单的例子。

如上图所示,蓝色框中的区域是一个平面很难被找到和跟踪。无论向哪个方向移动蓝色框,都是一样的。对于黑色框中的区域,它是一个边缘。如果沿垂直方向移动,它会改变。但是如果沿水平方向移动就不会改变。而红色框中的角点,无论你向那个方向移动,得到的结果都不同,这说明它是唯一的。 所以,我们说角点是一个好的图像特征,也就回答了前面的问题。

角点是图像很重要的特征,对图像图形的理解和分析有很重要的作用。角点在三维场景重建运动估计,目标跟踪、目标识别、图像配准与匹配等计算机视觉领域起着非常重要的作用。在现实世界中,角点对应于物体的拐角,道路的十字路口、丁字路口等

那我们怎样找到这些角点呢?接下来我们使用 OpenCV 中的各种算法来查找图像的特征,并对它们进行描述。

1 Harris角点检测

1.1 原理

Harris角点检测的思想是通过图像的局部的小窗口观察图像,角点的特征是窗口沿任意方向移动都会导致图像灰度的明显变化,如下图所示:

将上述思想转换为数学形式,即将局部窗口向各个方向移动(u,v)并计算所有灰度差异的总和,表达式如下:

​​其中I(x,y)是局部窗口的图像灰度,I(x+u,y+v)是平移后的图像灰度,w(x,y)是窗口函数,该可以是矩形窗口,也可以是对每一个像素赋予不同权重的高斯窗口,如下所示:

角点检测中使E(u,v)的值最大。利用一阶泰勒展开有:

其中Ix​​和 Iy 是沿x和y方向的导数,可用sobel算子计算。

推导如下:

M矩阵决定了E(u,v)的取值,下面我们利用M来求角点,M是Ix和II​y​​的二次项函数,可以表示成椭圆的形状,椭圆的长短半轴由M的特征值λ1​​和λ2​​决定,方向由特征矢量决定,如下图所示:

椭圆函数特征值与图像中的角点、直线(边缘)和平面之间的关系如下图所示。

共可分为三种情况:

  • 图像中的直线。一个特征值大,另一个特征值小,λ1>>λ2或 λ2>>λ1。椭圆函数值在某一方向上大,在其他方向上小。
  • 图像中的平面。两个特征值都小,且近似相等;椭圆函数数值在各个方向上都小。
  • 图像中的角点。两个特征值都大,且近似相等,椭圆函数在所有方向都增大

Harris给出的角点计算方法并不需要计算具体的特征值,而是计算一个角点响应值R来判断角点。R的计算公式为:

​​式中,detM为矩阵M的行列式;traceM为矩阵M的迹;α为常数,取值范围为0.04~0.06。事实上,特征是隐含在detM和traceM中,因为:

那我们怎么判断角点呢?如下图所示:

  • 当R为大数值的正数时是角点
  • 当R为大数值的负数时是边界
  • 当R为小数是认为是平坦区域

1.2 实现

在OpenCV中实现Hariis检测使用的API是:

dst=cv.cornerHarris(src, blockSize, ksize, k)

参数:

  • img:数据类型为 float32 的输入图像。

  • blockSize:角点检测中要考虑的邻域大小。

  • ksize:sobel求导使用的核大小

  • k :角点检测方程中的自由参数,取值参数为 [0.04,0.06].

示例:

import cv2 as cv
import numpy as np 
import matplotlib.pyplot as plt
# 1 读取图像,并转换成灰度图像
img = cv.imread('qt.jpg')
gray = cv.cvtColor(img, cv.COLOR_BGR2GRAY)
# 2 角点检测
# 2.1 输入图像必须是 float32
gray = np.float32(gray)

# 2.2 最后一个参数在 0.04 到 0.05 之间
dst = cv.cornerHarris(gray,2,3,0.04)
# 3 设置阈值,将角点绘制出来,阈值根据图像进行选择
img[dst>0.001*dst.max()] = [0,0,255]
# 4 图像显示
plt.figure(figsize=(10,8),dpi=100)
plt.imshow(img[:,:,::-1]),plt.title('Harris')
plt.xticks([]), plt.yticks([])
plt.show()

结果如下:

Harris角点检测的优缺点:

优点:

  • 旋转不变性,椭圆转过一定角度但是其形状保持不变(特征值保持不变)
  • 对于图像灰度的仿射变化具有部分的不变性,由于仅仅使用了图像的一介导数,对于图像灰度平移变化不变;对于图像灰度尺度变化不变

缺点:

  • 对尺度很敏感,不具备几何尺度不变性。
  • 提取的角点是像素级的

2 Shi-Tomasi角点检测

1 原理

Shi-Tomasi算法是对Harris角点检测算法的改进,一般会比Harris算法得到更好的角点。Harris 算法的角点响应函数是将矩阵 M 的行列式值与 M 的迹相减,利用差值判断是否为角点。后来Shi 和Tomasi 提出改进的方法是,若矩阵M的两个特征值中较小的一个大于阈值,则认为他是角点,即:

如下图所示:

从这幅图中,可以看出来只有当 λ1 和 λ 2 都大于最小值时,才被认为是角点。

2.2 实现

在OpenCV中实现Shi-Tomasi角点检测使用API:

corners = cv2.goodFeaturesToTrack ( image, maxcorners, qualityLevel, minDistance )

参数:

  • Image: 输入灰度图像
  • maxCorners : 获取角点数的数目。
  • qualityLevel:该参数指出最低可接受的角点质量水平,在0-1之间。
  • minDistance:角点之间最小的欧式距离,避免得到相邻特征点。

返回:

  • Corners: 搜索到的角点,在这里所有低于质量水平的角点被排除掉,然后把合格的角点按质量排序,然后将质量较好的角点附近(小于最小欧式距离)的角点删掉,最后找到maxCorners个角点返回。

示例:

import numpy as np 
import cv2 as cv
import matplotlib.pyplot as plt
# 1 读取图像
img = cv.imread('tv.jpg') 
gray = cv.cvtColor(img,cv.COLOR_BGR2GRAY)
# 2 角点检测
corners = cv.goodFeaturesToTrack(gray,1000,0.01,10)  
# 3 绘制角点
for i in corners:
    x,y = i.ravel()
    cv.circle(img,(int(x),int(y)),2,(0,0,255),-1)
# 4 图像展示
plt.figure(figsize=(10,8),dpi=100)
plt.imshow(img[:,:,::-1]),plt.title('shi-tomasi')
plt.xticks([]), plt.yticks([])
plt.show()

结果如下:

总结

  1. Harris算法

    思想:通过图像的局部的小窗口观察图像,角点的特征是窗口沿任意方向移动都会导致图像灰度的明显变化。

    API: cv.cornerHarris()

  2. Shi-Tomasi算法

    对Harris算法的改进,能够更好地检测角点

    API: cv2.goodFeatureToTrack()

20.SIFT/SURF算法

1 SIFT原理

前面两节我们介绍了Harris和Shi-Tomasi角点检测算法,这两种算法具有旋转不变性,但不具有尺度不变性,以下图为例,在左侧小图中可以检测到角点,但是图像被放大后,在使用同样的窗口,就检测不到角点了。

所以,下面我们来介绍一种计算机视觉的算法,尺度不变特征转换即SIFT (Scale-invariant feature transform)。它用来侦测与描述影像中的局部性特征,它在空间尺度中寻找极值点,并提取出其位置、尺度、旋转不变量,此算法由 David Lowe在1999年所发表,2004年完善总结。应用范围包含物体辨识、机器人地图感知与导航、影像缝合、3D模型建立、手势辨识、影像追踪和动作比对等领域。

SIFT算法的实质是在不同的尺度空间上查找关键点(特征点),并计算出关键点的方向。SIFT所查找到的关键点是一些十分突出,不会因光照,仿射变换和噪音等因素而变化的点,如角点、边缘点、暗区的亮点及亮区的暗点等。

1.1 基本流程

Lowe将SIFT算法分解为如下四步

  1. 尺度空间极值检测:搜索所有尺度上的图像位置。通过高斯差分函数来识别潜在的对于尺度和旋转不变的关键点。
  2. 关键点定位:在每个候选的位置上,通过一个拟合精细的模型来确定位置和尺度。关键点的选择依据于它们的稳定程度。
  3. 关键点方向确定:基于图像局部的梯度方向,分配给每个关键点位置一个或多个方向。所有后面的对图像数据的操作都相对于关键点的方向、尺度和位置进行变换,从而保证了对于这些变换的不变性。
  4. 关键点描述:在每个关键点周围的邻域内,在选定的尺度上测量图像局部的梯度。这些梯度作为关键点的描述符,它允许比较大的局部形状的变形或光照变化。

我们就沿着Lowe的步骤,对SIFT算法的实现过程进行介绍:

1.2 尺度空间极值检测

在不同的尺度空间是不能使用相同的窗口检测极值点,对小的关键点使用小的窗口,对大的关键点使用大的窗口,为了达到上述目的,我们使用尺度空间滤波器。

高斯核是唯一可以产生多尺度空间的核函数。-《Scale-space theory: A basic tool for analysing structures at different scales》。

一个图像的尺度空间L(x,y,σ),定义为原始图像I(x,y)与一个可变尺度的2维高斯函数G(x,y,σ)卷积运算 ,即:

其中:

σ是尺度空间因子,它决定了图像的模糊的程度。在大尺度下(σ值大)表现的是图像的概貌信息,在小尺度下(σ值小)表现的是图像的细节信息。

在计算高斯函数的离散近似时,在大概3σ距离之外的像素都可以看作不起作用,这些像素的计算也就可以忽略。所以,在实际应用中,只计算(6σ+1)*(6σ+1)的高斯卷积核就可以保证相关像素影响。

下面我们构建图像的高斯金字塔,它采用高斯函数对图像进行模糊以及降采样处理得到的,高斯金字塔构建过程中,首先将图像扩大一倍,在扩大的图像的基础之上构建高斯金字塔,然后对该尺寸下图像进行高斯模糊,几幅模糊之后的图像集合构成了一个Octave,然后对该Octave下选择一幅图像进行下采样,长和宽分别缩短一倍,图像面积变为原来四分之一。这幅图像就是下一个Octave的初始图像,在初始图像的基础上完成属于这个Octave的高斯模糊处理,以此类推完成整个算法所需要的所有八度构建,这样这个高斯金字塔就构建出来了,整个流程如下图所示:

利用LoG(高斯拉普拉斯方法),即图像的二阶导数,可以在不同的尺度下检测图像的关键点信息,从而确定图像的特征点。但LoG的计算量大,效率低。所以我们通过两个相邻高斯尺度空间的图像的相减,得到DoG(高斯差分)来近似LoG。

为了计算DoG我们构建高斯差分金字塔,该金字塔是在上述的高斯金字塔的基础上构建而成的,建立过程是:在高斯金字塔中每个Octave中相邻两层相减就构成了高斯差分金字塔。如下图所示:

高斯差分金字塔的第1组第1层是由高斯金字塔的第1组第2层减第1组第1层得到的。以此类推,逐组逐层生成每一个差分图像,所有差分图像构成差分金字塔。概括为DOG金字塔的第o组第l层图像是有高斯金字塔的第o组第l+1层减第o组第l层得到的。后续Sift特征点的提取都是在DOG金字塔上进行的

在 DoG 搞定之后,就可以在不同的尺度空间中搜索局部最大值了。对于图像中的一个像素点而言,它需要与自己周围的 8 邻域,以及尺度空间中上下两层中的相邻的 18(2x9)个点相比。如果是局部最大值,它就可能是一个关键点。基本上来说关键点是图像在相应尺度空间中的最好代表。如下图所示:

搜索过程从每组的第二层开始,以第二层为当前层,对第二层的DoG图像中的每个点取一个3×3的立方体,立方体上下层为第一层与第三层。这样,搜索得到的极值点既有位置坐标(DoG的图像坐标),又有空间尺度坐标(层坐标)。当第二层搜索完成后,再以第三层作为当前层,其过程与第二层的搜索类似。当S=3时,每组里面要搜索3层,所以在DOG中就有S+2层,在初使构建的金字塔中每组有S+3层。

1.1.3 关键点定位

由于DoG对噪声和边缘比较敏感,因此在上面高斯差分金字塔中检测到的局部极值点需经过进一步的检验才能精确定位为特征点。

使用尺度空间的泰勒级数展开来获得极值的准确位置, 如果极值点的 灰度值小于阈值(一般为0.03或0.04)就会被忽略掉。 在 OpenCV 中这种阈值被称为 contrastThreshold。

DoG 算法对边界非常敏感, 所以我们必须要把边界去除。 Harris 算法除了可以用于角点检测之外还可以用于检测边界。从 Harris 角点检测的算法中,当一个特征值远远大于另外一个特征值时检测到的是边界。那在DoG算法中欠佳的关键点在平行边缘的方向有较大的主曲率,而在垂直于边缘的方向有较小的曲率,两者的比值如果高于某个阈值(在OpenCV中叫做边界阈值),就认为该关键点为边界,将被忽略,一般将该阈值设置为10。

将低对比度和边界的关键点去除,得到的就是我们感兴趣的关键点。

1.1.4 关键点方向确定

经过上述两个步骤,图像的关键点就完全找到了,这些关键点具有尺度不变性。为了实现旋转不变性,还需要为每个关键点分配一个方向角度,也就是根据检测到的关键点所在高斯尺度图像的邻域结构中求得一个方向基准。

对于任一关键点,我们采集其所在高斯金字塔图像以r为半径的区域内所有像素的梯度特征(幅值和幅角),半径r为:

其中σ是关键点所在octave的图像的尺度,可以得到对应的尺度图像。

梯度的幅值和方向的计算公式为:

邻域像素梯度的计算结果如下图所示:

完成关键点梯度计算后,使用直方图统计关键点邻域内像素的梯度幅值和方向。具体做法是,将360°分为36柱,每10°为一柱,然后在以r为半径的区域内,将梯度方向在某一个柱内的像素找出来,然后将他们的幅值相加在一起作为柱的高度。因为在r为半径的区域内像素的梯度幅值对中心像素的贡献是不同的,因此还需要对幅值进行加权处理,采用高斯加权,方差为1.5σ。如下图所示,为简化图中只画了8个方向的直方图。

每个特征点必须分配一个主方向,还需要一个或多个辅方向,增加辅方向的目的是为了增强图像匹配的鲁棒性。辅方向的定义是,当一个柱体的高度大于主方向柱体高度的80%时,则该柱体所代表的的方向就是给特征点的辅方向。

直方图的峰值,即最高的柱代表的方向是特征点邻域范围内图像梯度的主方向,但该柱体代表的角度是一个范围,所以我们还要对离散的直方图进行插值拟合,以得到更精确的方向角度值。利用抛物线对离散的直方图进行拟合,如下图所示:

获得图像关键点主方向后,每个关键点有三个信息(x,y,σ,θ):位置、尺度、方向。由此我们可以确定一个SIFT特征区域。通常使用一个带箭头的圆或直接使用箭头表示SIFT区域的三个值:中心表示特征点位置,半径表示关键点尺度,箭头表示方向。如下图所示:

1.1.5 关键点描述

通过以上步骤,每个关键点就被分配了位置,尺度和方向信息。接下来我们为每个关键点建立一个描述符,该描述符既具有可区分性,又具有对某些变量的不变性,如光照,视角等。而且描述符不仅仅包含关键点,也包括关键点周围对其有贡献的的像素点。主要思路就是通过将关键点周围图像区域分块,计算块内的梯度直方图,生成具有特征向量,对图像信息进行抽象。

描述符与特征点所在的尺度有关,所以我们在关键点所在的高斯尺度图像上生成对应的描述符。以特征点为中心,将其附近邻域划分为d∗dd∗d个子区域(一般取d=4),每个子区域都是一个正方形,边长为3σ,考虑到实际计算时,需进行三次线性插值,所以特征点邻域的为

的范围,如下图所示:

为了保证特征点的旋转不变性,以特征点为中心,将坐标轴旋转为关键点的主方向,如下图所示:

计算子区域内的像素的梯度,并按照σ=0.5d进行高斯加权,然后插值计算得到每个种子点的八个方向的梯度,插值方法如下图所示:

每个种子点的梯度都是由覆盖其的4个子区域插值而得的。如图中的红色点,落在第0行和第1行之间,对这两行都有贡献。对第0行第3列种子点的贡献因子为dr,对第1行第3列的贡献因子为1-dr,同理,对邻近两列的贡献因子为dc和1-dc,对邻近两个方向的贡献因子为do和1-do。则最终累加在每个方向上的梯度大小为:

其中k,m,n为0或为1。 如上统计4∗4∗8=128个梯度信息即为该关键点的特征向量,按照特征点的对每个关键点的特征向量进行排序,就得到了SIFT特征描述向量。

1.1.6 总结

SIFT在图像的不变特征提取方面拥有无与伦比的优势,但并不完美,仍然存在实时性不高,有时特征点较少,对边缘光滑的目标无法准确提取特征点等缺陷,自SIFT算法问世以来,人们就一直对其进行优化和改进,其中最著名的就是SURF算法。

1.2 SURF原理

使用 SIFT 算法进行关键点检测和描述的执行速度比较慢, 需要速度更快的算法。 2006 年 Bay提出了 SURF 算法,是SIFT算法的增强版,它的计算量小,运算速度快,提取的特征与SIFT几乎相同,将其与SIFT算法对比如下:

1.3 实现

在OpenCV中利用SIFT检测关键点的流程如下所示:

1.实例化sift

sift = cv.SIFT.create()

2.利用sift.detectAndCompute()检测关键点并计算

kp,des = sift.detectAndCompute(gray,None)

参数:

  • gray: 进行关键点检测的图像,注意是灰度图像

返回:

  • kp: 关键点信息,包括位置,尺度,方向信息
  • des: 关键点描述符,每个关键点对应128个梯度信息的特征向量

3.将关键点检测结果绘制在图像上

cv.drawKeypoints(image, keypoints, outputimage, color, flags)

参数:

  • image: 原始图像
  • keypoints:关键点信息,将其绘制在图像上
  • outputimage:输出图片,可以是原始图像
  • color:颜色设置,通过修改(b,g,r)的值,更改画笔的颜色,b=蓝色,g=绿色,r=红色。
  • flags:绘图功能的标识设置
    1. cv2.DRAW_MATCHES_FLAGS_DEFAULT:创建输出图像矩阵,使用现存的输出图像绘制匹配对和特征点,对每一个关键点只绘制中间点
    2. cv2.DRAW_MATCHES_FLAGS_DRAW_OVER_OUTIMG:不创建输出图像矩阵,而是在输出图像上绘制匹配对
    3. cv2.DRAW_MATCHES_FLAGS_DRAW_RICH_KEYPOINTS:对每一个特征点绘制带大小和方向的关键点图形
    4. cv2.DRAW_MATCHES_FLAGS_NOT_DRAW_SINGLE_POINTS:单点的特征点不被绘制

SURF算法的应用与上述流程是一致,这里就不在赘述。

示例:

利用SIFT算法在中央电视台的图片上检测关键点,并将其绘制出来:

import cv2 as cv 
import numpy as np
import matplotlib.pyplot as plt
# 1 读取图像
img = cv.imread('tv.jpg')
gray= cv.cvtColor(img,cv.COLOR_BGR2GRAY)
# 2 sift关键点检测
# 2.1 实例化sift对象
sift = cv.SIFT.create()

# 2.2 关键点检测:kp关键点信息包括方向,尺度,位置信息,des是关键点的描述符
kp,des=sift.detectAndCompute(gray,None)
# 2.3 在图像上绘制关键点的检测结果
cv.drawKeypoints(img,kp,img,flags=cv.DRAW_MATCHES_FLAGS_DRAW_RICH_KEYPOINTS)
# 3 图像显示
plt.figure(figsize=(8,6),dpi=100)
plt.imshow(img[:,:,::-1]),plt.title('sift')
plt.xticks([]), plt.yticks([])
plt.show()

返回值是 cv2.KeyPoint(关键点) 对象组成的列表,每个 KeyPoint 对象包含以下核心属性:

                pt:关键点的像素坐标 (x, y)

                size:关键点的尺度(代表该点在哪个尺度下被检测到,值越大对应越大的特征)

                angle:关键点的主方向。

                response:关键点的响应值(值越大,该关键点的显著性越高)

总结

SIFT原理:

  • 尺度空间极值检测:构建高斯金字塔,高斯差分金字塔,检测极值点。

  • 关键点定位:去除对比度较小和边缘对极值点的影响。

  • 关键点方向确定:利用梯度直方图确定关键点的方向。

  • 关键点描述:对关键点周围图像区域分块,计算块内的梯度直方图,生成具有特征向量,对关键点信息进行描述。

SURF算法:

对SIFT算法的改进,在尺度空间极值检测,关键点方向确定,关键点描述方面都有改进,提高效率

特征匹配 Brute-Force蛮力匹配

import cv2 
import numpy as np

def cv_show(name,img):
    cv2.imshow(name, img)
    cv2.waitKey(0)
    cv2.destroyAllWindows()
img1 = cv2.imread('box.png', 0)
img2 = cv2.imread('box_in_scene.png', 0)
sift = cv2.SIFT.create()
kp1, des1 = sift.detectAndCompute(img1, None)
kp2, des2 = sift.detectAndCompute(img2, None)
# crossCheck表示两个特征点要互相匹,例如A中的第i个特征点与B中的第j个特征点最近的,并且B中的第j个特征点到A中的第i个特征点也是 
#NORM_L2: 归一化数组的(欧几里德距离),如果其他特征计算方法需要考虑不同的匹配计算方式
bf = cv2.BFMatcher(crossCheck=True)
matches = bf.match(des1, des2)
matches = sorted(matches, key=lambda x: x.distance)

img3 = cv2.drawMatches(img1, kp1, img2, kp2, matches[:18], None,flags=2)
cv_show('img3',img3)

import cv2
import numpy as np


img1 = cv2.imread('box.png', 0)       # 模板图
img2 = cv2.imread('box_in_scene.png', 0)  # 场景图

# 2. 初始化SIFT,检测特征点+描述子
sift = cv2.SIFT.create()
kp1, des1 = sift.detectAndCompute(img1, None)
kp2, des2 = sift.detectAndCompute(img2, None)

# 3. 暴力匹配 + 交叉校验
bf = cv2.BFMatcher(cv2.NORM_L2, crossCheck=True)
matches = bf.match(des1, des2)
matches = sorted(matches, key=lambda x: x.distance)

# 4. 取优质匹配点(前18个)
good_matches = matches[:18]

# -------------------------- 核心:计算单应性矩阵,定位目标区域 --------------------------
# 提取匹配点的坐标
src_pts = np.float32([kp1[m.queryIdx].pt for m in good_matches]).reshape(-1, 1, 2)
dst_pts = np.float32([kp2[m.trainIdx].pt for m in good_matches]).reshape(-1, 1, 2)

# 计算单应性矩阵 H(RANSAC去除误匹配)
H, mask = cv2.findHomography(src_pts, dst_pts, cv2.RANSAC, 5.0)

# 获取模板图 img1 的四个角点
h, w = img1.shape
pts = np.float32([[0, 0], [w, 0], [w, h], [0, h]]).reshape(-1, 1, 2)

# 角点通过单应性矩阵映射到场景图 img2 中
dst_pts_box = cv2.perspectiveTransform(pts, H)

# 转为彩色图,方便绘制彩色框线
img2_color = cv2.cvtColor(img2, cv2.COLOR_GRAY2BGR)
# 在场景图上绘制匹配目标的轮廓(红色框)
img2_color = cv2.polylines(img2_color, [np.int32(dst_pts_box)], True, (0, 0, 255), 2)

# 5. 绘制匹配连线图(原图功能保留)
img3 = cv2.drawMatches(img1, kp1, img2_color, kp2, good_matches, None, flags=2)

# 显示结果
cv_show('Match & Draw Box', img3)

# 单独查看场景图上的红色框
cv_show('Scene with Box', img2_color)

  • 特征点坐标提取 queryIdx 是模板图img1的特征点索引,trainIdx 是场景图img2的特征点索引,取出两组匹配点坐标。

  • 单应性矩阵 findHomography 通过匹配点计算两张图的透视变换关系RANSAC 算法自动剔除错误匹配点,保证定位精度。

  • 角点透视映射 取出模板图四个角落坐标,通过矩阵H映射到场景图,得到目标在场景中的四个顶点。

  • 绘制外接框 polylines 根据映射后的顶点绘制红色矩形框,精准圈出img1img2中匹配到的区域。

Fast和ORB算法

1 Fast算法

1.1 原理

我们前面已经介绍过几个特征检测器,它们的效果都很好,特别是SIFT和SURF算法,但是从实时处理的角度来看,效率还是太低了。为了解决这个问题,Edward Rosten和Tom Drummond在2006年提出了FAST算法,并在2010年对其进行了修正。

FAST (全称Features from accelerated segment test)是一种用于角点检测的算法,该算法的原理是取图像中检测点,以该点为圆心的周围邻域内像素点判断检测点是否为角点,通俗的讲就是若一个像素周围有一定数量的像素与该点像素值不同,则认为其为角点

1.1.1 FAST算法的基本流程

  1. 在图像中选取一个像素点 p,来判断它是不是关键点。等于像素点 p的灰度值。

  2. 以r为半径画圆,覆盖p点周围的M个像素,通常情狂下,设置 r=3,则 M=16,如下图所示:

  1. 设置一个阈值t,如果在这 16 个像素点中存在 n 个连续像素点的灰度值都高于,或者低于,那么像素点 p 就被认为是一个角点。如上图中的虚线所示,n 一般取值为 12。

  2. 由于在检测特征点时是需要对图像中所有的像素点进行检测,然而图像中的绝大多数点都不是特征点,如果对每个像素点都进行上述的检测过程,那显然会浪费许多时间,因此采用一种进行非特征点判别的方法:首先对候选点的周围每个 90 度的点:1,9,5,13 进行测试(先测试 1 和 19, 如果它们符合阈值要求再测试 5 和 13)。如果 p 是角点,那么这四个点中至少有 3 个要符合阈值要求,否则直接剔除。对保留下来的点再继续进行测试(是否有 12 的点符合阈值要求)。

虽然这个检测器的效率很高,但它有以下几条缺点:

  • 获得的候选点比较多

  • 特征点的选取不是最优的,因为它的效果取决与要解决的问题和角点的分布情况。

  • 进行非特征点判别时大量的点被丢弃

  • 检测到的很多特征点都是相邻的

前 3 个问题可以通过机器学习的方法解决,最后一个问题可以使用非最大值抑制的方法解决。

1.1.2机器学习的角点检测器

  1. 选择一组训练图片(最好是跟最后应用相关的图片)

  2. 使用 FAST 算法找出每幅图像的特征点,对图像中的每一个特征点,将其周围的 16 个像素存储构成一个向量P。

  1. 每一个特征点的 16 像素点都属于下列三类中的一种

  1. 根据这些像素点的分类,特征向量 P 也被分为 3 个子集:Pd ,Ps ,Pb,

  2. 定义一个新的布尔变量 ,如果 p 是角点就设置为 Ture,如果不是就设置为 False。

  3. 利用特征值向量p,目标值是$K_p$,训练ID3 树(决策树分类器)。

  4. 将构建好的决策树运用于其他图像的快速的检测。

1.1.3 非极大值抑制

在筛选出来的候选角点中有很多是紧挨在一起的,需要通过非极大值抑制来消除这种影响。为所有的候选角点都确定一个打分函数的值可这样计算:先分别计算与圆上16个点的像素值差值,取绝对值,再将这16个绝对值相加,就得到了的值  最后比较毗邻候选角点的 V 值,把V值较小的候选角点pass掉。

FAST算法的思想与我们对角点的直观认识非常接近,化繁为简。FAST算法比其它角点的检测算法快,但是在噪声较高时不够稳定,这需要设置合适的阈值。

1.2 实现

OpenCV中的FAST检测算法是用传统方法实现的,

1.实例化fast

fast = =cv.FastFeatureDetector_create( threshold, nonmaxSuppression)

参数:

  • threshold:阈值t,有默认值10

  • nonmaxSuppression:是否进行非极大值抑制,默认值True

返回:

  • Fast:创建的FastFeatureDetector对象

2.利用fast.detect检测关键点,没有对应的关键点描述

kp = fast.detect(grayImg, None)

参数:

  • gray: 进行关键点检测的图像,注意是灰度图像

返回:

  • kp: 关键点信息,包括位置,尺度,方向信息

3.将关键点检测结果绘制在图像上,与在sift中是一样的

cv.drawKeypoints(image, keypoints, outputimage, color, flags)

示例:

import numpy as np
import cv2 as cv
from matplotlib import pyplot as plt
# 1 读取图像
img = cv.imread('tv.jpg')
# 2 Fast角点检测
# 2.1 创建一个Fast对象,传入阈值,注意:可以处理彩色空间图像
fast = cv.FastFeatureDetector_create(threshold=60)

# 2.2 检测图像上的关键点
kp = fast.detect(img,None)
# 2.3 在图像上绘制关键点
img2 = cv.drawKeypoints(img, kp, None, color=(0,0,255))

# 2.4 输出默认参数
print( "Threshold: {}".format(fast.getThreshold()) )
print( "nonmaxSuppression:{}".format(fast.getNonmaxSuppression()) )
print( "neighborhood: {}".format(fast.getType()) )
print( "Total Keypoints with nonmaxSuppression: {}".format(len(kp)) )


# 2.5 关闭非极大值抑制
fast.setNonmaxSuppression(0)
kp = fast.detect(img,None)

print( "Total Keypoints without nonmaxSuppression: {}".format(len(kp)) )
# 2.6 绘制为进行非极大值抑制的结果
img3 = cv.drawKeypoints(img, kp, None, color=(0,0,255))

# 3 绘制图像
fig,axes=plt.subplots(nrows=1,ncols=2,figsize=(10,8),dpi=100)
axes[0].imshow(img2[:,:,::-1])
axes[0].set_title("with nonmaxSuppression")
axes[1].imshow(img3[:,:,::-1])
axes[1].set_title("without nonmaxSuppression")
plt.show()

结果:

2 ORB 算法

2.1 原理

SIFT和SURF算法是受专利保护的,在使用他们时我们是要付费的,但是ORB(Oriented Fast and Rotated Brief)不需要,它可以用来对图像中的关键点快速创建特征向量,并用这些特征向量来识别图像中的对象。

2.1.1 ORB算法流程

ORB算法结合了Fast和Brief算法,提出了构造金字塔,为Fast特征点添加了方向,从而使得关键点具有了尺度不变性和旋转不变性。具体流程描述如下:

  • 构造尺度金字塔,金字塔共有n层,与SIFT不同的是,每一层仅有一幅图像。第s层的尺度为:是初始尺度,默认为1.2,原图在第0层。

第s层图像的大小:

  • 在不同的尺度上利用Fast算法检测特征点,采用Harris角点响应函数,根据角点的响应值排序,选取前N个特征点,作为本尺度的特征点。

  • 计算特征点的主方向,计算以特征点为圆心半径为r的圆形邻域内的灰度质心位置,将从特征点位置到质心位置的方向做特征点的主方向。

计算方法如下:

质心位置:

主方向:

  • 为了解决旋转不变性,将特征点的邻域旋转到主方向上利用Brief算法构建特征描述符,至此就得到了ORB的特征描述向量。

2.1.2 BRIEF算法

BRIEF是一种特征描述子提取算法,并非特征点的提取算法,一种生成二值化描述子的算法,不提取代价低,匹配只需要使用简单的汉明距离(Hamming Distance)利用比特之间的异或操作就可以完成。因此,时间代价低,空间代价低,效果还挺好是最大的优点。

算法的步骤介绍如下

  1. 图像滤波:原始图像中存在噪声时,会对结果产生影响,所以需要对图像进行滤波,去除部分噪声。

  2. 选取点对:以特征点为中心,取S*S的邻域窗口,在窗口内随机选取N组点对,一般N=128,256,512,默认是256,关于如何选取随机点对,提供了五种形式,结果如下图所示:

    图中一条线段的两个端点就是一组点对,其中第二种方法的结果比较好。

    • x,y方向平均分布采样

    • x,y均服从Gauss(0,S^2/25)各向同性采样

    • x服从Gauss(0,S^2/25),y服从Gauss(0,S^2/100)采样

    • x,y从网格中随机获取

    • x一直在(0,0),y从网格中随机选取

  3. 构建描述符:假设x,y是某个点对的两个端点,p(x),p(y)是两点对应的像素值,则有:

  1. 对每一个点对都进行上述的二进制赋值,形成BRIEF的关键点的描述特征向量,该向量一般为 128-512 位的字符串,其中仅包含 1 和 0,如下图所示:

2.2 实现

在OpenCV中实现ORB算法,使用的是:

1.实例化ORB

orb = cv.xfeatures2d.orb_create(nfeatures)

参数:

  • nfeatures: 特征点的最大数量

2.利用orb.detectAndCompute()检测关键点并计算

kp,des = orb.detectAndCompute(gray,None)

参数:

  • gray: 进行关键点检测的图像,注意是灰度图像

返回:

  • kp: 关键点信息,包括位置,尺度,方向信息

  • des: 关键点描述符,每个关键点BRIEF特征向量,二进制字符串,

3.将关键点检测结果绘制在图像上

cv.drawKeypoints(image, keypoints, outputimage, color, flags)

示例:

import numpy as np
import cv2 as cv
from matplotlib import pyplot as plt
# 1 图像读取
img = cv.imread('tv.jpg')

# 2 ORB角点检测
# 2.1 实例化ORB对象
orb = cv.ORB_create(nfeatures=500)
# 2.2 检测关键点,并计算特征描述符
kp,des = orb.detectAndCompute(img,None)

print(des.shape)

# 3 将关键点绘制在图像上
img2 = cv.drawKeypoints(img, kp, None, color=(0,0,255), flags=0)

# 4. 绘制图像
plt.figure(figsize=(10,8),dpi=100)
plt.imshow(img2[:,:,::-1])
plt.xticks([]), plt.yticks([])
plt.show()

总结

  1. Fast算法

    原理:若一个像素周围有一定数量的像素与该点像素值不同,则认为其为角点

    API: cv.FastFeatureDetector_create()

  2. ORB算法

    原理:是FAST算法和BRIEF算法的结合

    API:cv.ORB_create()

视频追踪

1.meanshift

1.1原理

meanshift算法的原理很简单。假设你有一堆点集,还有一个小的窗口,这个窗口可能是圆形的,现在你可能要移动这个窗口到点集密度最大的区域当中。

如下图:

最开始的窗口是蓝色圆环的区域,命名为C1。蓝色圆环的圆心用一个蓝色的矩形标注,命名为C1_o。

而窗口中所有点的点集构成的质心在蓝色圆形点C1_r处,显然圆环的形心和质心并不重合。所以,移动蓝色的窗口,使得形心与之前得到的质心重合。在新移动后的圆环的区域当中再次寻找圆环当中所包围点集的质心,然后再次移动,通常情况下,形心和质心是不重合的。不断执行上面的移动过程,直到形心和质心大致重合结束。 这样,最后圆形的窗口会落到像素分布最大的地方,也就是图中的绿色圈,命名为C2。

meanshift算法除了应用在视频追踪当中,在聚类,平滑等等各种涉及到数据以及非监督学习的场合当中均有重要应用,是一个应用广泛的算法。

图像是一个矩阵信息,如何在一个视频当中使用meanshift算法来追踪一个运动的物体呢? 大致流程如下:

  1. 首先在图像上选定一个目标区域

  2. 计算选定区域的直方图分布,一般是HSV色彩空间的直方图。

  3. 对下一帧图像b同样计算直方图分布。

  4. 计算图像b当中与选定区域直方图分布最为相似的区域,使用meanshift算法将选定区域沿着最为相似的部分进行移动,直到找到最相似的区域,便完成了在图像b中的目标追踪。

  5. 重复3到4的过程,就完成整个视频目标追踪。

    通常情况下我们使用直方图反向投影得到的图像和第一帧目标对象的起始位置,当目标对象的移动会反映到直方图反向投影图中,meanshift 算法就把我们的窗口移动到反向投影图像中灰度密度最大的区域了。如下图所示:

直方图反向投影的流程是:

假设我们有一张100x100的输入图像,有一张10x10的模板图像,查找的过程是这样的:

  1. 从输入图像的左上角(0,0)开始,切割一块(0,0)至(10,10)的临时图像;
  2. 生成临时图像的直方图;
  3. 用临时图像的直方图和模板图像的直方图对比,对比结果记为c;
  4. 直方图对比结果c,就是结果图像(0,0)处的像素值;
  5. 切割输入图像从(0,1)至(10,11)的临时图像,对比直方图,并记录到结果图像;
  6. 重复1~5步直到输入图像的右下角,就形成了直方图的反向投影。

1.2 实现

在OpenCV中实现Meanshift的API是:

cv.meanShift(probImage, window, criteria)

参数:

  • probImage: ROI区域,即目标的直方图的反向投影

  • window: 初始搜索窗口,就是定义ROI的rect

  • criteria: 确定窗口搜索停止的准则,主要有迭代次数达到设置的最大值,窗口中心的漂移值大于某个设定的限值等。

实现Meanshift的主要流程是:

  1. 读取视频文件:cv.videoCapture()
  2. 感兴趣区域设置:获取第一帧图像,并设置目标区域,即感兴趣区域
  3. 计算直方图:计算感兴趣区域的HSV直方图,并进行归一化
  4. 目标追踪:设置窗口搜索停止条件,直方图反向投影,进行目标追踪,并在目标位置绘制矩形框。

示例:

import numpy as np
import cv2 as cv
# 1.获取图像
cap = cv.VideoCapture('DOG.wmv')

# 2.获取第一帧图像,并指定目标位置
ret,frame = cap.read()
# 2.1 目标位置(行,高,列,宽)
r,h,c,w = 197,141,0,208  
track_window = (c,r,w,h)
# 2.2 指定目标的感兴趣区域
roi = frame[r:r+h, c:c+w]

# 3. 计算直方图
# 3.1 转换色彩空间(HSV)
hsv_roi =  cv.cvtColor(roi, cv.COLOR_BGR2HSV)
# 3.2 去除低亮度的值
# mask = cv.inRange(hsv_roi, np.array((0., 60.,32.)), np.array((180.,255.,255.)))
# 3.3 计算直方图
roi_hist = cv.calcHist([hsv_roi],[0],None,[180],[0,180])
# 3.4 归一化
cv.normalize(roi_hist,roi_hist,0,255,cv.NORM_MINMAX)

# 4. 目标追踪
# 4.1 设置窗口搜索终止条件:最大迭代次数,窗口中心漂移最小值
term_crit = ( cv.TERM_CRITERIA_EPS | cv.TERM_CRITERIA_COUNT, 10, 1 )

while(True):
    # 4.2 获取每一帧图像
    ret ,frame = cap.read()
    if ret == True:
        # 4.3 计算直方图的反向投影
        hsv = cv.cvtColor(frame, cv.COLOR_BGR2HSV)
        dst = cv.calcBackProject([hsv],[0],roi_hist,[0,180],1)

        # 4.4 进行meanshift追踪
        ret, track_window = cv.meanShift(dst, track_window, term_crit)

        # 4.5 将追踪的位置绘制在视频上,并进行显示
        x,y,w,h = track_window
        img2 = cv.rectangle(frame, (x,y), (x+w,y+h), 255,2)
        cv.imshow('frame',img2)

        if cv.waitKey(60) & 0xFF == ord('q'):
            break        
    else:
        break
# 5. 资源释放        
cap.release()
cv.destroyAllWindows()

下面是三帧图像的跟踪结果:

2 Camshift

大家认真看下上面的结果,有一个问题,就是检测的窗口的大小是固定的,而狗狗由近及远是一个逐渐变小的过程,固定的窗口是不合适的。所以我们需要根据目标的大小和角度来对窗口的大小和角度进行修正。CamShift可以帮我们解决这个问题。

CamShift算法全称是“Continuously Adaptive Mean-Shift”(连续自适应MeanShift算法),是对MeanShift算法的改进算法,可随着跟踪目标的大小变化实时调整搜索窗口的大小,具有较好的跟踪效果。

Camshift算法首先应用meanshift,一旦meanshift收敛,它就会更新窗口的大小,还计算最佳拟合椭圆的方向,从而根据目标的位置和大小更新搜索窗口。如下图所示:

Camshift在OpenCV中实现时,只需将上述的meanshift函数改为Camshift函数即可:

将Camshift中的:

 # 4.4 进行meanshift追踪
        ret, track_window = cv.meanShift(dst, track_window, term_crit)

        # 4.5 将追踪的位置绘制在视频上,并进行显示
        x,y,w,h = track_window
        img2 = cv.rectangle(frame, (x,y), (x+w,y+h), 255,2)

改为:

  #进行camshift追踪
    ret, track_window = cv.CamShift(dst, track_window, term_crit)

        # 绘制追踪结果
        pts = cv.boxPoints(ret)
        pts = np.int0(pts)
        img2 = cv.polylines(frame,[pts],True, 255,2)

3 算法总结

Meanshift和camshift算法都各有优势,自然也有劣势:

  • Meanshift算法:简单,迭代次数少,但无法解决目标的遮挡问题并且不能适应运动目标的的形状和大小变化。

  • camshift算法:可适应运动目标的大小形状的改变,具有较好的跟踪效果,但当背景色和目标颜色接近时,容易使目标的区域变大,最终有可能导致目标跟踪丢失。

总结

  1. meanshift

    原理:一个迭代的步骤,即先算出当前点的偏移均值,移动该点到其偏移均值,然后以此为新的起始点,继续移动,直到满足一定的条件结束。

    API:cv.meanshift()

    优缺点:简单,迭代次数少,但无法解决目标的遮挡问题并且不能适应运动目标的的形状和大小变化

  2. camshift

    原理:对meanshift算法的改进,首先应用meanshift,一旦meanshift收敛,它就会更新窗口的大小,还计算最佳拟合椭圆的方向,从而根据目标的位置和大小更新搜索窗口。

    API:cv.camshift()

    优缺点:可适应运动目标的大小形状的改变,具有较好的跟踪效果,但当背景色和目标颜色接近时,容易使目标的区域变大,最终有可能导致目标跟踪丢失

案例:人脸案例

1 基础

我们使用机器学习的方法完成人脸检测,首先需要大量的正样本图像(面部图像)和负样本图像(不含面部的图像)来训练分类器。我们需要从其中提取特征。下图中的 Haar 特征会被使用,就像我们的卷积核,每一个特征是一 个值,这个值等于黑色矩形中的像素值之后减去白色矩形中的像素值之和。

Haar特征值反映了图像的灰度变化情况。例如:脸部的一些特征能由矩形特征简单的描述,眼睛要比脸颊颜色要深,鼻梁两侧比鼻梁颜色要深,嘴巴比周围颜色要深等。

Haar特征可用于于图像任意位置,大小也可以任意改变,所以矩形特征值是矩形模版类别、矩形位置和矩形大小这三个因素的函数。故类别、大小和位置的变化,使得很小的检测窗口含有非常多的矩形特征。

得到图像的特征后,训练一个决策树构建的adaboost级联决策器来识别是否为人脸。

2.实现

OpenCV中自带已训练好的检测器,包括面部,眼睛,猫脸等,都保存在XML文件中,我们可以通过以下程序找到他们:

import cv2 as cv
print(cv.__file__)

找到的文件如下所示:

那我们就利用这些文件来识别人脸,眼睛等。检测流程如下:

  1. 读取图片,并转换成灰度图

  2. 实例化人脸和眼睛检测的分类器对象

    # 实例化级联分类器
    classifier =cv.CascadeClassifier( "haarcascade_frontalface_default.xml" ) 
    # 加载分类器
    classifier.load('haarcascade_frontalface_default.xml')
  3. 进行人脸和眼睛的检测

    rect = classifier.detectMultiScale(gray, scaleFactor, minNeighbors, minSize,maxsize)
    

    参数:

    • Gray: 要进行检测的人脸图像
    • scaleFactor: 前后两次扫描中,搜索窗口的比例系数
    • minneighbors:目标至少被检测到minNeighbors次才会被认为是目标
    • minsize和maxsize: 目标的最小尺寸和最大尺寸
  4. 将检测结果绘制出来就可以了。

主程序如下所示:

import cv2 as cv
import matplotlib.pyplot as plt
# 1.以灰度图的形式读取图片
img = cv.imread("16.jpg")
gray = cv.cvtColor(img,cv.COLOR_BGR2GRAY)

# 2.实例化OpenCV人脸和眼睛识别的分类器 
face_cas = cv.CascadeClassifier( "haarcascade_frontalface_default.xml" ) 
face_cas.load('haarcascade_frontalface_default.xml')

eyes_cas = cv.CascadeClassifier("haarcascade_eye.xml")
eyes_cas.load("haarcascade_eye.xml")

# 3.调用识别人脸 
faceRects = face_cas.detectMultiScale( gray, scaleFactor=1.2, minNeighbors=3, minSize=(32, 32)) 
for faceRect in faceRects: 
    x, y, w, h = faceRect 
    # 框出人脸 
    cv.rectangle(img, (x, y), (x + h, y + w),(0,255,0), 3) 
    # 4.在识别出的人脸中进行眼睛的检测
    roi_color = img[y:y+h, x:x+w]
    roi_gray = gray[y:y+h, x:x+w]
    eyes = eyes_cas.detectMultiScale(roi_gray) 
    for (ex,ey,ew,eh) in eyes:
        cv.rectangle(roi_color,(ex,ey),(ex+ew,ey+eh),(0,255,0),2)
# 5. 检测结果的绘制
plt.figure(figsize=(8,6),dpi=100)
plt.imshow(img[:,:,::-1]),plt.title('检测结果')
plt.xticks([]), plt.yticks([])
plt.show()

我们也可在视频或摄像头中对人脸进行检测:

import cv2 as cv
import matplotlib.pyplot as plt
# 1.读取视频
cap = cv.VideoCapture(0)
# 2.在每一帧数据中进行人脸识别
while(cap.isOpened()):
    ret, frame = cap.read()
    if ret==True:
        gray = cv.cvtColor(frame, cv.COLOR_BGR2GRAY)
        # 3.实例化OpenCV人脸识别的分类器 
        face_cas = cv.CascadeClassifier( "haarcascade_frontalface_default.xml" ) 
        face_cas.load('haarcascade_frontalface_default.xml')
        # 4.调用识别人脸 
        faceRects = face_cas.detectMultiScale(gray, scaleFactor=1.2, minNeighbors=3, minSize=(32, 32)) 
        for faceRect in faceRects: 
            x, y, w, h = faceRect 
            # 框出人脸 
            cv.rectangle(frame, (x, y), (x + h, y + w),(0,255,0), 3) 
        cv.imshow("frame",frame)
        if cv.waitKey(1) & 0xFF == ord('q'):
            break
# 5. 释放资源
cap.release()  
cv.destroyAllWindows()

总结

opencv中人脸识别的流程是:

  1. 读取图片,并转换成灰度图

  2. 实例化人脸和眼睛检测的分类器对象

# 实例化级联分类器
classifier =cv.CascadeClassifier( "haarcascade_frontalface_default.xml" ) 
# 加载分类器
classifier.load('haarcascade_frontalface_default.xml')
  1. 进行人脸和眼睛的检测
rect = classifier.detectMultiScale(gray, scaleFactor, minNeighbors, minSize,maxsize)
  1. 将检测结果绘制出来就可以了。

我们也可以在视频中进行人脸识别

项目:银行卡号识别

素材:

工具类 myutils.py

import cv2


def sort_contours(contours, method="left-to-right"):
    reverse = False
    i = 0
    if method == "right-to-left" or method == "bottom-to-top":
        reverse = True

    if method == "top-to-bottom" or method == "bottom-to-top":
        i = 1
    bounding_boxes = [cv2.boundingRect(c) for c in contours]  # 用一个最小的矩形,把找到的形状包起来x,y,h,w
    (contours, bounding_boxes) = zip(*sorted(zip(contours, bounding_boxes),
                                             key=lambda b: b[1][i], reverse=reverse))

    return contours, bounding_boxes


def resize(image, width=None, height=None, inter=cv2.INTER_AREA):
    (h, w) = image.shape[:2]
    if width is None and height is None:
        return image
    if width is None:
        r = height / float(h)
        dim = (int(w * r), height)
    else:
        r = width / float(w)
        dim = (width, int(h * r))
    resized = cv2.resize(image, dim, interpolation=inter)
    return resized

主程序代码:

# 导入工具包
import cv2
import numpy as np

import myutils

template_path = "tep.png"
image_path = "card1.png"


# 绘图展示
def cv_show(name, img):
    cv2.imshow(name, img)
    cv2.waitKey(0)
    cv2.destroyAllWindows()


# 读取一个模板图像
img = cv2.imread(template_path)
cv_show('img', img)
# 灰度图
ref = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
cv_show('ref', ref)
# 二值图像
ref = cv2.threshold(ref, 10, 255, cv2.THRESH_BINARY_INV)[1]
cv_show('ref', ref)

# 计算轮廓
# cv2.findContours()函数接受的参数为二值图,即黑白的(不是灰度图),cv2.RETR_EXTERNAL只检测外轮廓,cv2.CHAIN_APPROX_SIMPLE只保留终点坐标
# 返回的list中每个元素都是图像中的一个轮廓
refCnts, hierarchy = cv2.findContours(ref.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)

cv2.drawContours(img, refCnts, -1, (0, 0, 255), 3)
cv_show('img', img)
print("轮廓总数量:", len(refCnts))
refCnts = myutils.sort_contours(refCnts, method="left-to-right")[0]  # 排序,从左到右,从上到下
digits = {}

# 遍历每一个轮廓
for (i, c) in enumerate(refCnts):
    # 计算外接矩形并且resize成合适大小
    (x, y, w, h) = cv2.boundingRect(c)
    roi = ref[y:y + h, x:x + w]
    roi = cv2.resize(roi, (57, 88))
    cv_show('img', roi)
    # 每一个数字对应每一个模板
    digits[i] = roi

# 初始化卷积核
rectKernel = cv2.getStructuringElement(cv2.MORPH_RECT, (9, 3))
sqKernel = cv2.getStructuringElement(cv2.MORPH_RECT, (5, 5))

# 读取输入图像,预处理
image = cv2.imread(image_path)
cv_show('image', image)
image = myutils.resize(image, width=300)
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
cv_show('gray', gray)

# 礼帽操作,突出更明亮的区域
tophat = cv2.morphologyEx(gray, cv2.MORPH_TOPHAT, rectKernel)
cv_show('tophat', tophat)
#  # ksize=-1相当于用3*3的
gradX = cv2.Sobel(tophat, ddepth=cv2.CV_32F, dx=1, dy=0, ksize=-1)

gradX = np.absolute(gradX)

(minVal, maxVal) = (np.min(gradX), np.max(gradX))
gradX = (255 * ((gradX - minVal) / (maxVal - minVal)))
gradX = gradX.astype("uint8")

print(np.array(gradX).shape)
cv_show('gradX', gradX)

# 通过闭操作(先膨胀,再腐蚀)将数字连在一起
gradX = cv2.morphologyEx(gradX, cv2.MORPH_CLOSE, rectKernel)
cv_show('gradX', gradX)
# THRESH_OTSU会自动寻找合适的阈值,适合双峰,需把阈值参数设置为0
thresh = cv2.threshold(gradX, 0, 255,
                       cv2.THRESH_BINARY | cv2.THRESH_OTSU)[1]
cv_show('thresh', thresh)

# 再来一个闭操作

thresh = cv2.morphologyEx(thresh, cv2.MORPH_CLOSE, sqKernel)  # 再来一个闭操作
cv_show('thresh', thresh)

# 计算轮廓
threshCnts, hierarchy = cv2.findContours(thresh.copy(), cv2.RETR_EXTERNAL,
                                         cv2.CHAIN_APPROX_SIMPLE)

cur_img = image.copy()
cv2.drawContours(cur_img, threshCnts, -1, (0, 0, 255), 3)
cv_show('img', cur_img)
locs = []

# 遍历轮廓
for (i, c) in enumerate(threshCnts):
    # 计算矩形
    (x, y, w, h) = cv2.boundingRect(c)
    ar = w / float(h)
    # 选择合适的区域,根据实际任务来,这里的基本都是四个数字一组
    if ar > 2.5 and ar < 4.0:
        if (w > 40 and w < 55) and (h > 10 and h < 20):
            # 符合的留下来
            locs.append((x, y, w, h))

# 将符合的轮廓从左到右排序
locs = sorted(locs, key=lambda x: x[0])
output = []

# 遍历每一个轮廓中的数字
for (i, (gX, gY, gW, gH)) in enumerate(locs):
    # initialize the list of group digits
    groupOutput = []

    # 根据坐标提取每一个组
    group = gray[gY - 5:gY + gH + 5, gX - 5:gX + gW + 5]
    cv_show('group', group)
    # 自动全局阈值分割,不用手动填阈值,适合明暗区分明显的字符、二维码
    group = cv2.threshold(group, 0, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU)[1]
    cv_show('group', group)
    # 计算每一组的轮廓
    digitCnts = cv2.findContours(group.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)[0]
    digitCnts = myutils.sort_contours(digitCnts, method="left-to-right")[0]

    # 计算每一组中的每一个数值
    for c in digitCnts:
        # 找到当前数值的轮廓,resize成合适的的大小
        (x, y, w, h) = cv2.boundingRect(c)
        roi = group[y:y + h, x:x + w]
        roi = cv2.resize(roi, (57, 88))
        cv_show('roi', roi)

        # 计算匹配得分
        scores = []

        # 在模板中计算每一个得分
        for (digit, digitROI) in digits.items():
            # 模板匹配
            result = cv2.matchTemplate(roi, digitROI,
                                       cv2.TM_CCOEFF)
            (_, score, _, _) = cv2.minMaxLoc(result)
            scores.append(score)

        # 得到最合适的数字
        groupOutput.append(str(np.argmax(scores)))

    # 画出来
    cv2.rectangle(image, (gX - 5, gY - 5),
                  (gX + gW + 5, gY + gH + 5), (0, 0, 255), 1)
    cv2.putText(image, "".join(groupOutput), (gX, gY - 15),
                cv2.FONT_HERSHEY_SIMPLEX, 0.65, (0, 0, 255), 2)

    # 得到结果
    output.extend(groupOutput)

# 打印结果
print("Credit Card #: {}".format("".join(output)))
cv2.imshow("Image", image)
cv2.waitKey(0)

项目:答题卡识别

import numpy as np
import cv2

# 正确答案
ANSWER_KEY = {0: 1, 1: 4, 2: 0, 3: 3, 4: 1}
# 答题卡图片
img_path = "images/test_01.png"


def four_point_transform(image, pts):
    rect = np.zeros((4, 2), dtype="float32")
    # 按顺序找到对应坐标0123分别是 左上,右上,右下,左下
    # 计算左上,右下
    s = pts.sum(axis=1)
    rect[0] = pts[np.argmin(s)]
    rect[2] = pts[np.argmax(s)]
    # 计算右上和左下
    diff = np.diff(pts, axis=1)
    rect[1] = pts[np.argmin(diff)]
    rect[3] = pts[np.argmax(diff)]
    (tl, tr, br, bl) = rect
    # 计算输入的w和h值
    widthA = np.sqrt(((br[0] - bl[0]) ** 2) + ((br[1] - bl[1]) ** 2))
    widthB = np.sqrt(((tr[0] - tl[0]) ** 2) + ((tr[1] - tl[1]) ** 2))
    maxWidth = max(int(widthA), int(widthB))
    heightA = np.sqrt(((tr[0] - br[0]) ** 2) + ((tr[1] - br[1]) ** 2))
    heightB = np.sqrt(((tl[0] - bl[0]) ** 2) + ((tl[1] - bl[1]) ** 2))
    maxHeight = max(int(heightA), int(heightB))
    # 变换后对应坐标位置
    dst = np.array([
        [0, 0],
        [maxWidth - 1, 0],
        [maxWidth - 1, maxHeight - 1],
        [0, maxHeight - 1]], dtype="float32")
    # 计算变换矩阵
    M = cv2.getPerspectiveTransform(rect, dst)
    warped = cv2.warpPerspective(image, M, (maxWidth, maxHeight))
    # 返回变换后结果
    return warped


def sort_contours(cnts, method="left-to-right"):
    reverse = False
    i = 0
    if method == "right-to-left" or method == "bottom-to-top":
        reverse = True
    if method == "top-to-bottom" or method == "bottom-to-top":
        i = 1
    boundingBoxes = [cv2.boundingRect(c) for c in cnts]
    (cnts, boundingBoxes) = zip(*sorted(zip(cnts, boundingBoxes), key=lambda b: b[1][i], reverse=reverse))
    return cnts, boundingBoxes


def cv_show(name, img):
    cv2.imshow(name, img)
    cv2.waitKey(0)
    cv2.destroyAllWindows()


# 预处理
image = cv2.imread(img_path)
contours_img = image.copy()
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
blurred = cv2.GaussianBlur(gray, (5, 5), 0)
cv_show('blurred', blurred)
edged = cv2.Canny(blurred, 75, 200)
cv_show('edged', edged)

# 轮廓检测
cnts = cv2.findContours(edged.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)[0]
cv2.drawContours(contours_img, cnts, -1, (0, 0, 255), 3)
cv_show('contours_img', contours_img)
docCnt = None

# 确保检测到了
if len(cnts) > 0:
    # 根据轮廓大小进行排序
    cnts = sorted(cnts, key=cv2.contourArea, reverse=True)

    # 遍历每一个轮廓
    for c in cnts:
        # 近似
        peri = cv2.arcLength(c, True)
        approx = cv2.approxPolyDP(c, 0.02 * peri, True)
        # 准备做透视变换
        if len(approx) == 4:
            docCnt = approx
            break

# 执行透视变换

warped = four_point_transform(gray, docCnt.reshape(4, 2))
cv_show('warped', warped)
# Otsu's 阈值处理
thresh = cv2.threshold(warped, 0, 255,
                       cv2.THRESH_BINARY_INV | cv2.THRESH_OTSU)[1]
cv_show('thresh', thresh)
thresh_Contours = thresh.copy()
# 找到每一个圆圈轮廓
cnts = cv2.findContours(thresh.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)[0]
cv2.drawContours(thresh_Contours, cnts, -1, (0, 0, 255), 3)
cv_show('thresh_Contours', thresh_Contours)
questionCnts = []

# 遍历
for c in cnts:
    # 计算比例和大小
    (x, y, w, h) = cv2.boundingRect(c)
    ar = w / float(h)
    # 根据实际情况指定标准
    if w >= 20 and h >= 20 and ar >= 0.9 and ar <= 1.1:
        questionCnts.append(c)

# 按照从上到下进行排序
questionCnts = sort_contours(questionCnts,
                             method="top-to-bottom")[0]
correct = 0

# 每排有5个选项
for (q, i) in enumerate(np.arange(0, len(questionCnts), 5)):
    # 排序
    cnts = sort_contours(questionCnts[i:i + 5])[0]
    bubbled = None
    # 遍历每一个结果
    for (j, c) in enumerate(cnts):
        # 使用mask来判断结果
        mask = np.zeros(thresh.shape, dtype="uint8")
        cv2.drawContours(mask, [c], -1, 255, -1)  # -1表示填充
        # 通过计算非零点数量来算是否选择这个答案
        mask = cv2.bitwise_and(thresh, thresh, mask=mask)
        total = cv2.countNonZero(mask)
        # 通过阈值判断
        if bubbled is None or total > bubbled[0]:
            bubbled = (total, j)
    # 对比正确答案
    color = (0, 0, 255)
    k = ANSWER_KEY[q]
    # 判断正确
    if k == bubbled[1]:
        color = (0, 255, 0)
        correct += 1
    # 绘图
    cv2.drawContours(warped, [cnts[k]], -1, color, 5)
score = (correct / 5.0) * 100
print("[INFO] score: {:.2f}%".format(score))
cv2.putText(warped, "{:.2f}%".format(score), (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.9, (0, 0, 255), 2)
cv_show("Exam", warped)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值