python-opencv--从基础到进阶(3.3万字)
在 OpenCV 中,所有算法均用 C++ 实现。但这些算法也可以用到不同语言,比如Python、Java等。这得益于绑定生成器。这些生成器在 C++ 和 Python 之间建立了桥梁,使用户能够从 Python 调用 C++ 函数。使用python版本的优势是:开发速度快(语法简洁、无需编译); 生态丰富(可直接结合 NumPy/Pandas/Matplotlib 做数据处理 / 可视化,缺点是速度不如C++
所有代码已经上传,请在个人ZEEKLOG主页查看
基础篇
01图片/视频的加载保存
图片加载
导入cv2模块,并给它起别名为cv,方便后续调用 import cv2 as cv 导入sys模块,用于系统级操作 import sys 读取图片 img = cv.imread("starry_night.jpg") 检查图片是否成功读取(imread失败会返回None) if img is None: sys.exit("Could not read the image.") 创建一个名为"Display window"的窗口,显示读取的图片 cv.imshow("Display window", img) 等待用户按键输入,参数0表示无限期等待 k = cv.waitKey(0) 如果用户按下了's'键(保存键) if k == ord("s"): 将图片保存为PNG格式到当前目录 cv.imwrite("starry_night.png", img)cv.imread() --- 读取图片---基本语法 img = cv.imread(filename, flags)
其中filename为图片路径,flags为读取方式, 默认是cv.IMREAD_COLOR
还有灰度图cv.IMREAD_GRAYSCALE # 或 0
假设图片包含透明通道cv.IMREAD_UNCHANGED # 或 -1
返回值:成功:返回 numpy.ndarray,形状为 (高度, 宽度, 通道数) 失败:返回 None
注: OpenCV: BGR 顺序
cv.imshow(winname, mat)---显示图片---winname窗口名称,mat图片数组ndarray
视频加载与显示
cap = cv.VideoCapture(0) if not cap.isOpened(): print("Cannot open camera") exit() while True: ret, frame = cap.read() if not ret: print("Can't receive frame (stream end?). Exiting ...") break gray = cv.cvtColor(frame, cv.COLOR_BGR2GRAY) cv.imshow('frame', gray) if cv.waitKey(1) == ord('q'): break When everything done, release the capture cap.release() cv.destroyAllWindows()cv.VideoCapture()---打开视频文件或摄像头 # 参数0: 打开默认摄像头(笔记本内置摄像头) # 参数可以是文件路径: cv.VideoCapture("video.mp4") 播放视频文件 # 参数可以是摄像头索引: 0=第一个摄像头, 1=第二个摄像头...
cap.read()---从摄像头读取一帧画面
# 返回值1 (ret): 布尔值,True=读取成功,False=读取失败
# 返回值2 (frame): 读取到的帧图像(numpy.ndarray 数组)
cv.cvtColor: 颜色空间转换
# 参数1: 源图像 # 参数2: 转换类型
视频保存
视频保存示例:从摄像头读取画面并保存为视频文件 import cv2 as cv cap = cv.VideoCapture(0) fourcc = cv.VideoWriter_fourcc(*'XVID') out = cv.VideoWriter('output.avi', fourcc, 20.0, (640, 480)) while cap.isOpened(): ret, frame = cap.read() if not ret: print("Can't receive frame. Exiting ...") break frame = cv.flip(frame, 1) out.write(frame) cv.imshow('frame', frame) if cv.waitKey(1) == ord('q'): break cap.release() 释放摄像头 out.release() 释放视频写入对象 cv.destroyAllWindows() 关闭所有窗口cv.VideoWriter_fourcc(*'XVID') # VideoWriter_fourcc: 指定视频编码格式 # fourcc 是四字符代码(Four Character Code),用于标识视频编解码器 # 'XVID' 是一种常见的 MPEG-4 编码格式 # *'XVID' 等价于 'X', 'V', 'I', 'D' 四个字符
cv.VideoWriter('output.avi', fourcc, 20.0, (640, 480))
# VideoWriter: 创建视频写入对象 # 参数1: 输出文件名 # 参数2: 编码格式(fourcc) # 参数3: 帧率(fps,每秒帧数) # 参数4: 分辨率(宽度, 高度) cv.flip(frame, 1)# flip: 翻转图像 # 参数1: 要翻转的图像 # 参数2: 翻转方式 # 0 = 沿 X 轴翻转(上下翻转) # 1 = 沿 Y 轴翻转(左右镜像) # -1 = 同时沿 X 和 Y 轴翻转
02基本绘制
import numpy as np import cv2 as cv zeros: 创建全0数组(黑色背景) 参数1: (高度, 宽度, 通道数) → 512x512 的3通道图像(BGR) 参数2: 数据类型,uint8 表示每个像素值范围 0-255 img = np.zeros((512, 512, 3), np.uint8) # ============================================================================= 1. 直线 line(img, 起点, 终点, 颜色, 粗细) # ============================================================================= 参数: 图像, 起点(x,y), 终点(x,y), BGR颜色, 线宽(像素) cv.line(img, (0, 0), (511, 511), (255, 0, 0), 5) # ============================================================================= 2. 矩形 rectangle(img, 左上角, 右下角, 颜色, 粗细) # ============================================================================= cv.rectangle(img, (384, 0), (510, 128), (0, 255, 0), 3) # ============================================================================= 3. 圆形 circle(img, 圆心, 半径, 颜色, 粗细) # ============================================================================= # (447,63) = 圆心坐标, 63 = 半径 # (0,0,255) = 红色, -1 = 填充(正数=线宽) cv.circle(img, (447, 63), 63, (0, 0, 255), -1) # ============================================================================= 4. 椭圆 ellipse(img, 中心, (长轴,短轴), 旋转角, 起始角, 结束角, 颜色, 粗细) # ============================================================================= # (256,256) = 椭圆中心 # (100,50) = 长轴100, 短轴50 # 0, 0, 180 = 旋转0°, 从0°画到180°(半椭圆) # 255 = 纯蓝色(B=255,G=0,R=0), -1 = 填充 cv.ellipse(img, (256, 256), (100, 50), 0, 0, 180, 255, -1) # ============================================================================= 5. 多边形 polylines(img, 点集, 是否闭合, 颜色) # ============================================================================= # 定义多边形的顶点坐标 pts = np.array([[10, 5], [20, 30], [70, 20], [50, 10]], np.int32) # reshape: 转换形状为 (顶点数, 1, 2),这是 polylines 要求的格式 # -1 表示自动计算顶点数量 pts = pts.reshape((-1, 1, 2)) # True = 闭合多边形(首尾相连),(0,255,255) = 青色 cv.polylines(img, [pts], True, (0, 255, 255)) # ============================================================================= 6. 文字 putText(img, 文字, 位置, 字体, 大小, 颜色, 粗细, 线型) # ============================================================================= font = cv.FONT_HERSHEY_SIMPLEX # 设置字体类型 # (10,500) = 文字左下角位置 # 4 = 字体大小, (255,255,255) = 白色 # 2 = 线宽, cv.LINE_AA = 抗锯齿(边缘更平滑) cv.putText(img, 'OpenCV', (10, 500), font, 4, (255, 255, 255), 2, cv.LINE_AA) cv.imshow("window", img) #waitKey让窗口保持显示,等待用户按键 cv.waitKey(0) # 关闭所有窗口 cv.destroyAllWindows()03鼠标事件
import numpy as np import cv2 as cv # ============================================================================= 鼠标回调函数:处理鼠标事件 m切换,ESC键退出 # ============================================================================= def draw_circle(event, x, y, flags): # global: 声明使用全局变量(在函数内修改全局变量必须先声明) global ix, iy, drawing, mode # EVENT_LBUTTONDOWN: 鼠标左键按下事件 if event == cv.EVENT_LBUTTONDOWN: drawing = True # 开始绘制状态 ix, iy = x, y # 记录起始坐标(按下时的位置) # EVENT_MOUSEMOVE: 鼠标移动事件 elif event == cv.EVENT_MOUSEMOVE: # 只有在绘制状态下才进行绘制 if drawing == True: if mode == True: # 矩形模式:从起点(ix,iy)到当前位置(x,y)绘制绿色矩形 # -1 表示填充矩形 cv.rectangle(img, (ix, iy), (x, y), (0, 255, 0), -1) else: # 圆形模式:在当前位置绘制半径为5的红色圆形 cv.circle(img, (x, y), 5, (0, 0, 255), -1) # EVENT_LBUTTONUP: 鼠标左键释放事件 elif event == cv.EVENT_LBUTTONUP: drawing = False # 结束绘制状态 # 最后再绘制一次,确保形状完整 if mode == True: cv.rectangle(img, (ix, iy), (x, y), (0, 255, 0), -1) else: cv.circle(img, (x, y), 5, (0, 0, 255), -1) drawing = False # 绘制状态标志(False=未绘制,True=正在绘制) mode = True # 绘图模式(True=矩形模式,False=圆形模式) ix, iy = -1, -1 # 记录鼠标按下时的起始坐标 img = np.zeros((512, 512, 3), np.uint8) # 创建512x512的黑色画布 cv.namedWindow('image') # 创建名为 'image' 的窗口 setMouseCallback: 为指定窗口设置鼠标回调函数 # 参数1: 窗口名称 # 参数2: 回调函数名(鼠标事件发生时自动调用) cv.setMouseCallback('image', draw_circle) # ============================================================================= 打印所有可用的鼠标事件类型 # ============================================================================= # dir(cv): 获取 cv 模块中所有属性和方法 # 列表推导式:筛选出包含 'EVENT' 的名称 events = [i for i in dir(cv) if 'EVENT' in i] print(events) # 输出所有鼠标事件类型供参考 # ============================================================================= # 主循环:处理键盘事件 # ============================================================================= while True: # 无限循环,直到用户按下ESC键退出 cv.imshow('image', img) # 显示当前画布 # waitKey(1): 等待1毫秒键盘输入 # & 0xFF: 位运算,确保结果为0-255之间 k = cv.waitKey(1) & 0xFF # ord('m'): 获取字符 'm' 的ASCII码 # 按 'm' 键切换绘图模式(矩形 <-> 圆形) if k == ord('m'): mode = not mode # 取反,True变False,False变True # 27 是 ESC 键的 ASCII 码 elif k == 27: break # 关闭所有OpenCV窗口 cv.destroyAllWindows() 04图像基本操作
import numpy as np import cv2 as cv # ============================================================================= # 启用优化 # ============================================================================= # setUseOptimized: 启用或禁用 OpenCV 的优化代码(如 SSE2、AVX 等) # 参数 True: 启用优化(默认已启用) # 返回值: 之前的状态(True=之前已启用,False=之前未启用) # 优化代码可以显著提高某些操作的性能(如像素处理、矩阵运算) cv.setUseOptimized(True) img = cv.imread('02.png') # assert: 断言语句,如果条件为 False 则抛出 AssertionError 并终止程序 # 适用于开发调试阶段,用于捕获"不应该发生"的错误 # 运行时添加 -O 参数可禁用断言(python -O script.py) assert img is not None, "图片加载失败" # 1. 获取图像属性 print(f"图像尺寸: {img.shape}") print(f"高度: {img.shape[0]}") # 行数 print(f"宽度: {img.shape[1]}") # 列数 print(f"通道数: {img.shape[2]}") # size: 像素总数(高度 × 宽度 × 通道数) print(f"像素总数: {img.size}") # dtype: 图像数据类型 print(f"数据类型: {img.dtype}") # 2. 访问像素 pixel = img[100, 100] print(f"坐标(100,100)的像素值(BGR): {pixel}") # 只获取某个通道的值 blue = img[100, 100, 0] # B 通道 green = img[100, 100, 1] # G 通道 red = img[100, 100, 2] # R 通道 # ============================================================================= # 3. ROI(感兴趣区域) # ============================================================================= # 选取图像的一部分(语法: img[行范围, 列范围]) # 注意: 图像坐标是 [y, x] 即 [行, 列] face = img[100:300, 150:350] # 选取 200x200 的区域 # 将选取的区域复制到图像的其他位置 img[0:200, 0:200] = face # 将 face 粘贴到左上角 # ============================================================================= # 4. 通道拆分与合并 # ============================================================================= # split: 将 BGR 图像拆分为三个单通道图像 b, g, r = cv.split(img) # 合并: 将三个单通道图像合并为 BGR 彩色图像 img_merged = cv.merge((b, g, r)) b = img[:, :, 0] img[:, :, 0] = 0 # ============================================================================= # 5. 边界填充(为图像添加边框) # ============================================================================= small_img = np.ones((100, 100, 3), np.uint8) * 100 # copyMakeBorder: 添加边框 # 参数1: 源图像 # 参数2-5: 上、下、左、右 的边框宽度(像素) # 参数6: 边框类型 # 参数7: 边框颜色(当 borderType=cv.BORDER_CONSTANT 时使用) BLUE = [255, 0, 0] # 常见边框类型: # cv.BORDER_CONSTANT - 添加恒定颜色边框 # cv.BORDER_REFLECT - 镜像反射 (fedcba|abcdefgh|hgfedcb) # cv.BORDER_REFLECT_101 - 反射,不含边界 (gfedcb|abcdefgh|gfedcba) # cv.BORDER_REPLICATE - 复制边界像素 (aaaaaa|abcdefgh|hhhhhhh) # cv.BORDER_WRAP - 另一端复制过来 (cdefgh|abcdefgh|abcdefg) replicate = cv.copyMakeBorder(small_img, 10, 10, 10, 10, cv.BORDER_REPLICATE) reflect = cv.copyMakeBorder(small_img, 10, 10, 10, 10, cv.BORDER_REFLECT) reflect101 = cv.copyMakeBorder(small_img, 10, 10, 10, 10, cv.BORDER_REFLECT_101) wrap = cv.copyMakeBorder(small_img, 10, 10, 10, 10, cv.BORDER_WRAP) constant = cv.copyMakeBorder(small_img, 10, 10, 10, 10, cv.BORDER_CONSTANT, value=BLUE)
05图像运算--加法、减法、按位运算
import numpy as np import cv2 as cv # 注意: 两张图片尺寸必须相同才能进行算术运算 img1 = cv.imread('02.png') img2 = cv.imread('02.png') # 实际使用时请替换为不同的图片 assert img1 is not None and img2 is not None, "图片加载失败" # 如果图片尺寸不同,调整到相同尺寸 if img1.shape != img2.shape: img2 = cv.resize(img2, (img1.shape[1], img1.shape[0])) # ============================================================================= # 1. 图像加法 # ============================================================================= # 方法1: cv.add() - 饱和运算(超过255则截断为255) # 150 + 100 = 250 # 200 + 100 = 255 (不是300!) result_add = cv.add(img1, img2) # 方法2: numpy 加法 - 取模运算(超过255则循环) # 150 + 100 = 250 # 200 + 100 = 300 % 256 = 44 result_numpy = img1 + img2 # ============================================================================= # 2. 图像减法 # ============================================================================= # cv.subtract: 饱和减法(小于0则截断为0) result_sub = cv.subtract(img1, img2) # numpy 减法: 会产生负值循环 # 100 - 150 = -50 % 256 = 206 result_numpy_sub = img1 - img2 # ============================================================================= # 3. 图像混合(加权加法) # ============================================================================= # 公式: g(x) = (1-α)f₀(x) + αf₁(x) # α = 0.5 表示两张图片各占 50% # β = 1-α,γ = 0(亮度调整,可选) alpha = 0.5 result_blend = cv.addWeighted(img1, alpha, img2, 1-alpha, 0) # 创建滑块来动态调整混合比例 def nothing(x): pass cv.namedWindow('Blend') cv.createTrackbar('Alpha', 'Blend', 0, 100, nothing) while True: alpha = cv.getTrackbarPos('Alpha', 'Blend') / 100 beta = 1 - alpha dst = cv.addWeighted(img1, alpha, img2, beta, 0) cv.imshow('Blend', dst) k = cv.waitKey(1) & 0xFF if k == 27: # ESC 键退出 break cv.destroyAllWindows() # ============================================================================= # 4. 按位运算(Bitwise Operations) # ============================================================================= # 创建两个简单的图像用于演示 # 圆形图像 img_circle = np.zeros((300, 300, 3), np.uint8) cv.circle(img_circle, (150, 150), 100, (255, 255, 255), -1) # 矩形图像 img_rect = np.zeros((300, 300, 3), np.uint8) cv.rectangle(img_rect, (50, 50), (250, 250), (255, 255, 255), -1) # cv.bitwise_and: 与运算(两个都是白色才为白色) # 只保留两个图形重叠的部分 bitwise_and = cv.bitwise_and(img_circle, img_rect) # cv.bitwise_or: 或运算(有一个是白色就为白色) # 保留两个图形的所有部分 bitwise_or = cv.bitwise_or(img_circle, img_rect) # cv.bitwise_xor: 异或运算(只有一个为白色时才为白色) # 只保留两个图形不重叠的部分 bitwise_xor = cv.bitwise_xor(img_circle, img_rect) # cv.bitwise_not: 非运算(颜色反转) # 白色变黑色,黑色变白色 bitwise_not = cv.bitwise_not(img_circle) # ============================================================================= # 5. 图像叠加(使用掩码) # ============================================================================= # 场景: 把一个小图标/logo 叠加到另一张图片上 # 背景图 background = np.zeros((300, 300, 3), np.uint8) background[:] = [100, 100, 100] # 灰色背景 # 前景图(要叠加的内容) foreground = np.zeros((100, 100, 3), np.uint8) cv.circle(foreground, (50, 50), 40, (0, 255, 0), -1) # 绿色圆形 # 创建掩码(白色=保留,黑色=忽略) mask = np.zeros((100, 100), np.uint8) cv.circle(mask, (50, 50), 40, 255, -1) # 白色圆形 # 指定要叠加到的位置(左上角坐标) x, y = 100, 100 # 放在背景的 (100, 100) 位置 # 提取 ROI(感兴趣区域) roi = background[y:y+foreground.shape[0], x:x+foreground.shape[1]] # 使用掩码叠加 # roi 背景: mask=0 的区域保持原样 # roi 前景: mask=255 的区域使用 foreground 的内容 masked_foreground = cv.bitwise_and(foreground, foreground, mask=mask) masked_roi = cv.bitwise_and(roi, roi, mask=cv.bitwise_not(mask)) combined = cv.add(masked_roi, masked_foreground) # 把结果放回背景图 background[y:y+foreground.shape[0], x:x+foreground.shape[1]] = combined
06图形运算--形态学运算
# ============================================================================= 形态学运算(Morphological Operations) # ============================================================================= 形态学运算需要二值图像(黑白图像)作为输入 主要用于: 去噪、边缘检测、图像分割等 读取图片并转为灰度图 img = cv.imread('02.png', cv.IMREAD_GRAYSCALE) assert img is not None, "图片加载失败" # ============================================================================= 二值化(Binary Thresholding) # ============================================================================= # 将灰度图转为黑白二值图像 像素值 > 127 设为 255(白色),否则为 0(黑色) _, binary = cv.threshold(img, 127, 255, cv.THRESH_BINARY) # ============================================================================= 创建结构元素(Kernel / 卷积核) # ============================================================================= # 形态学运算需要定义一个"结构元素",通常是一个矩形/圆形/十字形 # getStructuringElement: 创建结构元素 参数1: 形状 (MORPH_RECT=矩形, MORPH_ELLIPSE=椭圆, MORPH_CROSS=十字) 参数2: 核大小 (必须是正奇数,如 3x3, 5x5, 7x7) kernel = cv.getStructuringElement(cv.MORPH_RECT, (5, 5)) print("核:\n", kernel) # 不同形状的核 kernel_rect = cv.getStructuringElement(cv.MORPH_RECT, (5, 5)) # 矩形 kernel_ellipse = cv.getStructuringElement(cv.MORPH_ELLIPSE, (5, 5)) # 椭圆 kernel_cross = cv.getStructuringElement(cv.MORPH_CROSS, (5, 5)) # 十字 # ============================================================================= 1. 腐蚀(Erosion) # ============================================================================= 原理: 用核覆盖区域的最小值替换中心像素 效果: 白色区域变小,黑色区域变大(前景物体被"腐蚀") 应用: 去除小白点噪声、断开连接的物体 erosion = cv.erode(binary, kernel, iterations=1) # ============================================================================= 2. 膨胀(Dilation) # ============================================================================= 原理: 用核覆盖区域的最大值替换中心像素 效果: 白色区域变大,黑色区域变小(前景物体"膨胀") 应用: 填充小黑洞、连接断裂的物体 dilation = cv.dilate(binary, kernel, iterations=1) # ============================================================================= 3. 开运算(Opening) # ============================================================================= 定义: 先腐蚀,后膨胀 # 公式: opening = dilate(erode(img)) # 效果: 去除背景中的小白点噪声,同时保持物体大小基本不变 应用: 去噪(尤其是白色噪声) opening = cv.morphologyEx(binary, cv.MORPH_OPEN, kernel) # ============================================================================= 4. 闭运算(Closing) # ============================================================================= # 定义: 先膨胀,后腐蚀 # 公式: closing = erode(dilate(img)) # 效果: 填充物体内部的小黑洞,同时保持物体大小基本不变 应用: 填充前景物体内部的黑孔 closing = cv.morphologyEx(binary, cv.MORPH_CLOSE, kernel) # ============================================================================= 5. 形态学梯度(Morphological Gradient) # ============================================================================= # 定义: 膨胀图 - 腐蚀图 # 公式: gradient = dilate(img) - erode(img) 效果: 提取物体边缘(轮廓) gradient = cv.morphologyEx(binary, cv.MORPH_GRADIENT, kernel) # ============================================================================= 6. 顶帽(Top Hat) # ============================================================================= # 定义: 原图 - 开运算 # 公式: tophat = img - opening(img) # 效果: 提取比核小的亮区域(比周围亮的斑点) 应用: 提取亮噪声、增强亮细节 tophat = cv.morphologyEx(binary, cv.MORPH_TOPHAT, kernel) # ============================================================================= 7. 黑帽(Black Hat) # ============================================================================= # 定义: 闭运算 - 原图 # 公式: blackhat = closing(img) - img # 效果: 提取比核小的暗区域(比周围暗的孔洞) 应用: 提取暗细节、检测暗斑点 blackhat = cv.morphologyEx(binary, cv.MORPH_BLACKHAT, kernel)07图像变换--旋转、仿射、透视
平移(Translation)
import numpy as np import cv2 as cv img = cv.imread('02.png') assert img is not None, "图片加载失败" 获取图像尺寸 rows, cols = img.shape[:2] 创建平移矩阵 M # | 1 0 tx | # | 0 1 ty | # tx: x 方向平移像素(正数向右,负数向左) # ty: y 方向平移像素(正数向下,负数向上) tx, ty = 100, 50 # 向右平移100像素,向下平移50像素 M_translate = np.float32([[1, 0, tx], [0, 1, ty]]) warpAffine: 应用仿射变换 # 参数1: 源图像 # 参数2: 2x3 变换矩阵 # 参数3: 输出图像大小 (宽度, 高度) translated = cv.warpAffine(img, M_translate, (cols, rows))旋转(Rotation)
# getRotationMatrix2D: 获取旋转矩阵 # 参数1: 旋转中心坐标 (x, y) # 参数2: 旋转角度(正数=逆时针,负数=顺时针) # 参数3: 缩放比例(1=不缩放,0.5=缩小一半,2=放大两倍) center = (cols // 2, rows // 2) # 以图像中心为旋转中心 angle = 45 # 逆时针旋转45度 scale = 1.0 # 不缩放 M_rotate = cv.getRotationMatrix2D(center, angle, scale) # 应用旋转 rotated = cv.warpAffine(img, M_rotate, (cols, rows))缩放(Scaling / Resizing)
resize: 调整图像大小 # 参数1: 源图像 # 参数2: 目标尺寸 (宽度, 高度) 或 None # 参数3: fx 水平缩放比例(当 dsize=None 时使用) # 参数4: fy 垂直缩放比例 # 参数5: 插值方法 resized_half = cv.resize(img, (cols // 2, rows // 2)) # 缩小到一半 resized_double = cv.resize(img, (cols * 2, rows * 2)) # 放大到两倍 resized_fx = cv.resize(img, None, fx=0.5, fy=0.5) # 使用比例缩放 插值方法对比(缩小时推荐用 INTER_AREA,放大时推荐用 INTER_CUBIC 或 INTER_LINEAR) resized_nearest = cv.resize(img, (cols // 4, rows // 4), interpolation=cv.INTER_NEAREST) # 最近邻(快但质量差) resized_linear = cv.resize(img, (cols // 4, rows // 4), interpolation=cv.INTER_LINEAR) # 双线性(平衡) resized_cubic = cv.resize(img, (cols // 4, rows // 4), interpolation=cv.INTER_CUBIC) # 双三次(慢但质量好) resized_area = cv.resize(img, (cols // 4, rows // 4), interpolation=cv.INTER_AREA) # 像素区域(缩放推荐)仿射变换(Affine Transformation)
仿射变换需要三个点(原图3个点 → 变换后3个点) 变换后保持直线平行性(平行线仍然平行) # 原图中的三个点 pts1 = np.float32([[50, 50], [200, 50], [50, 200]]) # 变换后的三个点 pts2 = np.float32([[10, 100], [200, 50], [100, 250]]) # getAffineTransform: 计算仿射变换矩阵(2x3) M_affine = cv.getAffineTransform(pts1, pts2) # 应用仿射变换 affine_transformed = cv.warpAffine(img, M_affine, (cols, rows))透视变换(Perspective Transformation)
透视变换需要四个点(原图4个点 → 变换后4个点) 变换后不保持平行性(可产生3D效果) 常用于: 纠正扫描文档倾斜、鸟瞰图转换等 # 原图中的四个点(文档的四个角) pts1_perspective = np.float32([[56, 65], [368, 52], [28, 387], [389, 390]]) # 变换后的四个点(纠正为矩形) pts2_perspective = np.float32([[0, 0], [300, 0], [0, 300], [300, 300]]) # getPerspectiveTransform: 计算透视变换矩阵(3x3) M_perspective = cv.getPerspectiveTransform(pts1_perspective, pts2_perspective)
08图像阈值
阈值处理是将灰度图像转换为二值图像(黑白图像)的方法简单阈值- cv.threshold # 语法: cv.threshold(源图, 阈值, 最大值, 阈值类型) # 最常用THRESH_BINARY - 二值化阈值 # 像素 > thresh → 设为 maxval # 像素 ≤ thresh → 设为 0 # 返回: (阈值 used, 结果图像)大津算法(Otsu's Binarization) # 大津算法自动计算最佳阈值,适用于双峰直方图(明暗分明的图像) # 它实际上找到了一个位于两个峰之间的t值,使得两类的方差都最小。 # 使用方法: 在阈值类型中添加 cv.THRESH_OTSU 标志 # 注意: 使用 Otsu 时,阈值参数设为 0(算法会自动计算)自适应阈值(Adaptive Thresholding)- cv.adaptiveThreshold
# 问题: 固定阈值对光照不均匀的图像效果不好 # 解决: 自适应阈值根据像素邻域计算不同的阈值 # 语法: cv.adaptiveThreshold(源图, 最大值, 自适应方法, 阈值类型, 邻域大小, 常数C) # 参数说明: # - maxValue: 阈化后的最大值(通常是255) # - adaptiveMethod: 自适应方法 # * cv.ADAPTIVE_THRESH_MEAN_C - 邻域均值 # * cv.ADAPTIVE_THRESH_GAUSSIAN_C - 邻域加权和(高斯权重) # - thresholdType: 阈值类型(只能是 BINARY 或 BINARY_INV) # - blockSize: 邻域大小(必须是奇数,如3, 5, 7...) # - C: 从计算出的均值/加权均值中减去的常数
import cv2 as cv import numpy as np from matplotlib import pyplot as plt img = cv.imread('noisy2.png', cv.IMREAD_GRAYSCALE) assert img is not None, "file could not be read, check with os.path.exists()" # global thresholding ret1,th1 = cv.threshold(img,127,255,cv.THRESH_BINARY) # Otsu's thresholding ret2,th2 = cv.threshold(img,0,255,cv.THRESH_BINARY+cv.THRESH_OTSU) # Otsu's thresholding after Gaussian filtering blur = cv.GaussianBlur(img,(5,5),0) ret3,th3 = cv.threshold(blur,0,255,cv.THRESH_BINARY+cv.THRESH_OTSU) # 自适应阈值 - 均值法 thresh_mean = cv.adaptiveThreshold(img, 255, cv.ADAPTIVE_THRESH_MEAN_C, cv.THRESH_BINARY, 11, 2) # 自适应阈值 - 高斯法 thresh_gaussian = cv.adaptiveThreshold(img, 255, cv.ADAPTIVE_THRESH_GAUSSIAN_C, cv.THRESH_BINARY, 11, 2)进阶篇
09图像梯度
# 图像梯度描述图像像素值的变化率,用于边缘检测 # 梯度越大 → 像素变化越剧烈 → 可能是边缘 - Sobel: 通用边缘检测,噪声较多时 - Scharr: 需要更精确边缘时 - Laplacian: 检测各个方向边缘,但噪声敏感 - Canny: 最优边缘检测,推荐用于实际应用
Sobel 算子
# Sobel 算子结合了高斯平滑和微分,对噪声有一定抑制作用 # 语法: cv.Sobel( src, ddepth, dx, dy, ksize ) # 参数说明: # src - 输入图像 # ddepth - 输出图像深度(-1 表示与源图相同,cv.CV_64F 可保存负值) # dx - x 方向上的导数阶数(1, 2) # dy - y 方向上的导数阶数(0, 1, 2) # ksize - Sobel 核大小(1, 3, 5, 7),必须是奇数
sobel_xy_direct = cv.Sobel(img, cv.CV_64F, 1, 1, ksize=3) sobel_xy_direct_abs = cv.convertScaleAbs(sobel_xy_direct) - Sobel/Scharr 计算导数时可能产生负值 - uint8 只能表示 0-255,负值会溢出 - CV_64F 是 64 位浮点数,可以保存负值 - 最后用 convertScaleAbs 转为 uint8 显示Scharr 算子
# Scharr 算子是 Sobel 的改进版本,核更精确,对边缘更敏感 # 语法: cv.Scharr( src, ddepth, dx, dy ) # 注意: Scharr 的核大小固定为 3x3,不需要 ksize 参数 # Scharr 核 (3x3): # x方向: y方向: # [ -3 0 3] [ -3 -10 -3] # [-10 0 10] [ 0 0 0] # [ -3 0 3] [ 3 10 3]
scharr_x = cv.Scharr(img, cv.CV_64F, 1, 0) scharr_x_abs = cv.convertScaleAbs(scharr_x) scharr_y = cv.Scharr(img, cv.CV_64F, 0, 1) scharr_y_abs = cv.convertScaleAbs(scharr_y) scharr_xy = cv.addWeighted(scharr_x_abs, 0.5, scharr_y_abs, 0.5, 0)Laplacian 算子
# 拉普拉斯算子计算二阶导数,对噪声敏感但能检测各个方向的边缘 # 语法: cv.Laplacian( src, ddepth, ksize ) # Laplacian 核 (3x3): # [ 0 1 0] # [ 1 -4 1] # [ 0 1 0]
laplacian = cv.Laplacian(img, cv.CV_64F, ksize=3) laplacian_abs = cv.convertScaleAbs(laplacian)Canny 边缘检测
# 语法: cv.Canny( image, threshold1, threshold2, apertureSize )
# 参数说明: # threshold1 - 低阈值(弱边缘) # threshold2 - 高阈值(强边缘) # apertureSize - Sobel 核大小(默认3)
Canny 提出时就定义了「最优边缘检测器」的三大核心准则,这也是它区别于其他算法的根本:
- 高检测率:尽可能多地检测出图像中的真实边缘,不遗漏(检测率接近 100%);
- 高定位精度:检测到的边缘位置与真实边缘位置偏差最小(定位误差趋近于 0);
- 单边缘响应:同一真实边缘只对应一个检测结果(避免重复检测,即「单像素宽度边缘」)。
1.由于边缘检测容易受图像噪声影响,第一步是用5x5高斯滤波器去除图像中的噪声。
2.用 Sobel 算子计算水平(Gx)和垂直(Gy)梯度;并求梯度幅值和方向
它被四角化,分别代表垂直、水平和两个对角方向。
3.非极大值抑制
对每个像素,沿其梯度方向(垂直于边缘方向)检查相邻像素:如果当前像素的梯度幅值是该方向上的最大值 → 保留(是边缘);如果不是 → 抑制(设为 0,非边缘);
4.双阈值滞后阈值强边缘:幅值 > 高阈值 → 直接保留(确定是真实边缘);弱边缘:低阈值 < 幅值 ≤ 高阈值 → 仅当该像素与强边缘连通时保留(否则抑制);非边缘:幅值 ≤ 低阈值 → 直接抑制;
img = cv.imread('messi5.jpg', cv.IMREAD_GRAYSCALE)
assert img is not None, "file could not be read, check with os.path.exists()"
edges = cv.Canny(img,100,200)
10图像金字塔
在搜索图像中的某物时,比如面部,我们不确定该物体在图像中会出现的大小。在这种情况下,我们需要创建一组不同分辨率的同一图像,并在所有图像中搜索物体。这些分辨率不同的图像集合被称为图像金字塔(底部是分辨率最高的图像,顶部是最低分辨率的图像时,看起来像金字塔)
图像金字塔分为:高斯金字塔和拉普拉斯金字塔
高斯金字塔中的高层(低分辨率)是通过在低层(高分辨率)图像中移除连续的行和列形成的。然后,高层的每个像素由底层层中5个像素的高斯权重贡献而成。(用5*5的高斯卷积核)
# ----------------------------------------------------------------------------- (1) pyrDown - 下采样(降金字塔) # ----------------------------------------------------------------------------- # 操作: 先高斯模糊,再删除偶数行和列 # 效果: 图像尺寸变为原来的 1/2 layer1 = cv.pyrDown(img) # 尺寸: (w/2, h/2) layer2 = cv.pyrDown(layer1) # 尺寸: (w/4, h/4) layer3 = cv.pyrDown(layer2) # 尺寸: (w/8, h/8) layer4 = cv.pyrDown(layer3) # 尺寸: (w/16, h/16) # ----------------------------------------------------------------------------- (2) pyrUp - 上采样(升金字塔) # ----------------------------------------------------------------------------- # 操作: 先在偶数位置插入0,再高斯模糊 # 效果: 图像尺寸变为原来的 2 倍 # 注意: 上采样后的图像比下采样前模糊(丢失了高频信息) layer3_up = cv.pyrUp(layer3) # 尺寸: (w/8, h/8) → (w/4, h/4) layer2_up = cv.pyrUp(layer2) # 尺寸: (w/4, h/4) → (w/2, h/2) layer1_up = cv.pyrUp(layer1) # 尺寸: (w/2, h/2) → (w, h) 拉普拉斯金字塔由高斯金字塔构成。拉普拉斯金字塔图像就像边缘图像。它的大部分元素都是零。它们用于图像压缩。拉普拉斯金字塔中的一个层级是由高斯金字塔中该层级与其上层扩展版本之间的差异所形成。L_i = G_i - pyrUp(G_{i+1})
# 构建拉普拉斯金字塔 # 第0层: L0 = G0 - Up(G1) lp_layer0 = cv.subtract(layer1, cv.pyrUp(layer2)) # 第1层: L1 = G1 - Up(G2) lp_layer1 = cv.subtract(layer2, cv.pyrUp(layer3)) # 第2层: L2 = G2 - Up(G3) lp_layer2 = cv.subtract(layer3, cv.pyrUp(layer4)) # 第3层: L3 = G3 (最顶层,直接复制) lp_layer3 = layer4.copy()11图像轮廓
Contours 可以简单地解释为一条连接所有连续点(沿边界)的曲线,颜色或强度相同。轮廓是形状分析以及物体检测和识别的有用工具。
在OpenCV中,寻找Contours 就像从黑色背景中寻找白色物体。所以记住,要找到的物体应该是白色,背景应该是黑色。
查找轮廓 - cv.findContours()
语法: contours, hierarchy = cv.findContours(图像, 模式, 方法) # 参数说明: # - image: 输入图像(必须是二值图像) # - mode: 轮廓检索模式 # * cv.RETR_EXTERNAL - 只检测最外层轮廓 # * cv.RETR_LIST - 检测所有轮廓,不建立层次关系 # * cv.RETR_CCOMP - 检测所有轮廓,建立两级层次关系 # * cv.RETR_TREE - 检测所有轮廓,建立完整的层次树结构 # - method: 轮廓近似方法 # * cv.CHAIN_APPROX_NONE - 保存所有轮廓点 # * cv.CHAIN_APPROX_SIMPLE - 压缩水平、垂直、对角线方向,只保留端点 # * cv.CHAIN_APPROX_TC89_L1 - Teh-Chin 链近似算法 # * cv.CHAIN_APPROX_TC89_KCOS - Teh-Chin 链近似算法 # 返回: contours(轮廓列表), hierarchy(层次结构)
# 先对图像进行阈值处理(获取二值图像) _, thresh = cv.threshold(demo_gray, 127, 255, cv.THRESH_BINARY_INV) # 查找轮廓 contours, hierarchy = cv.findContours(thresh, cv.RETR_EXTERNAL, cv.CHAIN_APPROX_SIMPLE) print(f"检测到 {len(contours)} 个轮廓")绘制轮廓 - cv.drawContours()
# 语法: cv.drawContours(图像, 轮廓列表, 轮廓索引, 颜色, 厚度) # 参数说明: # - image: 目标图像(直接在原图上绘制) # - contours: 轮廓列表(来自 findContours) # - contourIdx: 轮廓索引 # * -1 - 绘制所有轮廓 # * 0,1,2... - 绘制指定轮廓 # - color: 颜色 (B, G, R) # - thickness: 线条厚度 # * -1 - 填充轮廓内部 # * 1,2,3... - 线条厚度
# 绘制所有轮廓(绿色,厚度2) cv.drawContours(demo_all, contours, -1, [0, 255, 0], 2)
轮廓特征 # 计算质心(Centroid) # 公式: cx = M10/M00, cy = M01/M00 for i, cnt in enumerate(contours): M = cv.moments(cnt) if M['m00'] != 0: # 避免除以零 cx = int(M['m10'] / M['m00']) cy = int(M['m01'] / M['m00']) area = cv.contourArea(cnt)#返回: 轮廓面积(像素数量) perimeter = cv.arcLength(cnt, True)#轮廓周长 # 语法: perimeter = cv.arcLength(contour, closed) # 参数说明: # - contour: 轮廓 # - closed: 是否闭合(True=闭合,False=不闭合)
12直方图
它是一个像素值(范围为0到255,不总是如此)在X轴上,图像中对应的像素数在Y轴的图。
通过观察图像的直方图,你可以直观地了解图像的对比度、亮度、强度分布等。
BINS:将整个直方图拆分为16个子部分,每个子部分的值就是所有像素数的总和。每个子部分称为“BIN”。BINS 在 OpenCV 文档中由 histSize 一词表示。
RANGE :它是你想测量的强度范围。通常,它是 [0,256],即所有强度值
计算直方图 - cv.calcHist()
# 语法: hist = cv.calcHist(图像, 通道列表, 掩码, 直方图大小, 像素值范围) # 参数说明: # - images: 输入图像(源图像,需用[]括起来) # - channels: 通道索引 # * 灰度图: [0] # * BGR图: [0]表示B通道, [1]表示G通道, [2]表示R通道 # - mask: 掩码图像 # * None - 计算整张图像的直方图 # * mask图像 - 只计算mask非零区域的直方图 # - histSize: 直方图的bin数量(每个维度) # * [256] - 表示0-255分成256个bin # - ranges: 像素值范围 # * [0, 256] - 表示0到255(注意上限不包含,所以写256)
# 创建掩码(只计算图像中心区域) mask = np.zeros(img_gray.shape, np.uint8) h, w = img_gray.shape mask[h//4 : 3*h//4, w//4 : 3*w//4] = 255 # 计算直方图 hist_mask = cv.calcHist([img_gray], [0], mask, [256], [0, 256]) hist_full = cv.calcHist([img_gray], [0], None, [256], [0, 256]) plt.hist(img_gray.ravel(), 256, [0, 256], color='gray') plt.title('Grayscale Histogram (plt.hist)') plt.xlabel('Pixel Value') plt.ylabel('Pixel Count')直方图均衡化:较亮的图像会让所有像素都被限制在高值范围内。但一张好的图像会包含图像各个区域的像素。所以你需要把直方图拉伸到两端,这就是直方图均衡的原理。这通常能提升图像的对比度。
img_gray_eq = cv.equalizeHist(img_gray)
adaptive histogram equalization(CLAHE):图像被划分为称为“tiles”的小块(在OpenCV中,tileSize默认为8x8)。然后像往常一样对这些区块进行直方图均衡处理。所以在小区域内,直方图会局限在一个小区域(除非有噪声)。如果有噪声,它会被放大。为避免这种情况,采用了对比度限制。如果任何直方图bin高于指定的对比度限制(OpenCV默认为40),这些像素会被裁剪并均匀分配到其他bin,然后再应用直方图均衡。
# 问题: 普通均衡化可能过度增强噪声 # 解决: CLAHE 在小区域内进行均衡化,并限制对比度增强幅度 # 创建CLAHE对象 # clipLimit: 对比度限制阈值(默认40) # tileGridSize: 网格大小(默认8x8) clahe = cv.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8)) # 应用CLAHE img_gray_clahe = clahe.apply(img_gray) # 计算CLAHE后的直方图 hist_gray_clahe = cv.calcHist([img_gray_clahe], [0], None, [256], [0, 256])
13模版匹配
模板匹配是一种用于搜索和查找模板图像在更大图像中位置的方法。
如果模板尺寸大于搜索图像,OpenCV 会直接报错,因为无法在更小的图像里搜索更大的模板。
模板尺寸越小,匹配速度越快,但可能匹配精度降低;模板尺寸越接近目标区域,匹配精度越高。
下面代码会返回一个全局最值
import cv2 as cv
import numpy as np
from matplotlib import pyplot as plt
img = cv.imread('messi5.jpg', cv.IMREAD_GRAYSCALE)
assert img is not None, "file could not be read, check with os.path.exists()"
img2 = img.copy()
template = cv.imread('template.jpg', cv.IMREAD_GRAYSCALE)
assert template is not None, "file could not be read, check with os.path.exists()"
w, h = template.shape[::-1]
# All the 6 methods for comparison in a list
methods = ['TM_CCOEFF', 'TM_CCOEFF_NORMED', 'TM_CCORR',
'TM_CCORR_NORMED', 'TM_SQDIFF', 'TM_SQDIFF_NORMED']
for meth in methods:
img = img2.copy()
method = getattr(cv, meth)
# Apply template Matching
res = cv.matchTemplate(img,template,method)
min_val, max_val, min_loc, max_loc = cv.minMaxLoc(res)
# If the method is TM_SQDIFF or TM_SQDIFF_NORMED, take minimum
if method in [cv.TM_SQDIFF, cv.TM_SQDIFF_NORMED]:
top_left = min_loc
else:
top_left = max_loc
bottom_right = (top_left[0] + w, top_left[1] + h)
cv.rectangle(img,top_left, bottom_right, 255, 2)
plt.subplot(121),plt.imshow(res,cmap = 'gray')
plt.title('Matching Result'), plt.xticks([]), plt.yticks([])
plt.subplot(122),plt.imshow(img,cmap = 'gray')
plt.title('Detected Point'), plt.xticks([]), plt.yticks([])
plt.suptitle(meth)
plt.show()
14分水岭算法
cv.watershed()是 OpenCV 中经典的图像分割算法,核心作用是将图像根据像素的 “连通性” 和 “标记信息” 分割成不同的区域(类似地理上的 “分水岭” 划分流域),特别适合处理重叠 / 粘连目标的分割(比如一堆粘连的细胞、硬币、米粒)。
# 分水岭算法用于图像分割,特别适合分离接触的物体 # 原理: 将图像看作地形图,从标记点开始"注水",不同水域相遇处即为分水岭 # 应用场景: 分离接触的物体、细胞计数、物体分割
15 角点检测
寻找图像特征被称为特征检测。角点是图像中强度在各个方向上变化较大的区域。
步骤1:定义窗口位移
E(u,v) = Σ [I(x+u, y+v) - I(x,y)]²
- E:位移后的灰度变化量
- (u,v):窗口位移量
步骤2:泰勒展开简化
E(u,v) ≈ [u v] · M · [u v]ᵀ
其中结构张量 M:
M = | Ix² Ix·Iy |
| Ix·Iy Iy² |
- Ix、Iy:x、y 方向的梯度
步骤3:计算响应函数
R = det(M) - k·trace(M)²
= λ1·λ2 - k·(λ1+λ2)²
- λ1、λ2:M 的特征值
- k:经验常数,通常 0.04~0.06
步骤4:判断角点
R 大 → 角点
R 小且正 → 平坦区域
R 负 → 边缘
Harris角点检测器计算R = λ1·λ2 - k(λ1+λ2)² ,但k值是经验值,不够鲁棒。
OpenCV 有一个函数,cv.goodFeaturesToTrack()它通过 Shi-Tomasi 方法,它在大多数场景下精度都优于 Harris。
【Shi-Tomasi 原理】 1. 是 Harris 角点检测的改进版 2. 使用结构张量 M 的特征值 λ1, λ2 来判断 3. Harris: R = λ1·λ2 - k·(λ1+λ2)² 4. Shi-Tomasi: 如果 min(λ1, λ2) > 阈值,则为角点 def shi_tomasi_detection(image_path, max_corners=100, quality=0.01, min_dist=10): """ Shi-Tomasi 角点检测 步骤: 1. 读取图像并转为灰度图 2. 使用 goodFeaturesToTrack 检测角点 3. 在原图上标记角点 参数: max_corners: 最大角点数量 quality: 角点质量阈值(0-1),越小检测越多 min_dist: 角点间最小欧氏距离 """ # 步骤1: 读取图像 img = cv2.imread(image_path) gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 步骤2: Shi-Tomasi 角点检测 corners = cv2.goodFeaturesToTrack( gray, maxCorners=max_corners, qualityLevel=quality, minDistance=min_dist, blockSize=3 # 计算导数的窗口大小 ) # 步骤3: 标记角点 result = img.copy() if corners is not None: for corner in corners: x, y = corner.ravel().astype(int) # 画红色圆圈标记角点 cv2.circle(result, (x, y), 5, (0, 0, 255), -1) # 画绿色十字增强显示 cv2.drawMarker(result, (x, y), (0, 255, 0), cv2.MARKER_CROSS, 15, 2) print(f"[Shi-Tomasi] 检测到 {len(corners) if corners is not None else 0} 个角点") return img, result, cornersFAST 角点检测:FAST 是一种快速角点检测算法,核心是 “判断像素周围 16 个像素是否有连续 N 个像素灰度差超过阈值”,计算速度极快
【FAST 原理】 1. Features from Accelerated Segment Test 2. 取像素点周围 16 个像素(Bresenham 圆) 3. 若连续 N 个像素都亮于或暗于中心点 ± 阈值,则为角点 4. 速度极快,适合实时应用 def fast_detection(image_path, threshold=25, nms=True): """ FAST 角点检测 步骤: 1. 读取图像并转为灰度图 2. 创建 FAST 检测器 3. 检测关键点并绘制 参数: threshold: 亮度差异阈值(越小越敏感) nms: 是否启用非极大值抑制(去除重复角点) """ # 步骤1: 读取图像 img = cv2.imread(image_path) gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 步骤2: 创建 FAST 检测器 # type=9: FAST_9_16(16个点中至少9个连续满足条件) fast = cv2.FastFeatureDetector_create( threshold=threshold, nonmaxSuppression=nms, type=cv2.FAST_FEATURE_DETECTOR_TYPE_9_16 ) # 步骤3: 检测关键点 keypoints = fast.detect(gray, None) # 步骤4: 绘制结果 result = cv2.drawKeypoints( img, keypoints, None, (0, 0, 255), flags=cv2.DRAW_MATCHES_FLAGS_DRAW_RICH_KEYPOINTS ) print(f"[FAST] 检测到 {len(keypoints)} 个角点 (阈值={threshold})") return img, result, keypoints16 特征匹配-SIFT算法(推荐)
角点检测器是旋转不变的,这意味着即使图像被旋转,我们也能找到相同的角。这很明显,因为角在旋转后的图像中仍然是角点。如果图像被缩放,角点可能就不是角点。
LoG 是一种边缘 / 关键点检测算子,本质是先对图像做高斯模糊(去除噪声),再对模糊后的图像做拉普拉斯变换(检测像素值的突变),能稳定检测出图像中 “尺度无关” 的关键点。
LoG 本质:LoG = ∇²G = ∂²G/∂x² + ∂²G/∂y²,是高斯函数的二阶导数,用于检测图像斑点(Blob),是尺度空间检测的核心算子,但无法直接用于特征匹配。
尺度不变特征变换(SIFT),是一套完整的特征提取算法,能从图像中提取 “尺度不变、旋转不变” 的特征点,并为每个特征点生成唯一的描述子(特征向量),用于不同图像间的匹配。
步骤如下:
1.尺度空间极值检测:SIFT 会把图片模糊、缩小好几层,然后在每一层里找特别突出的点
即用DoG替代LoG提升效率
2.关键点定位:把点挪到真正最突出的地方,留下稳定、清晰的关键点
3.定向任务:给这个点定一个主方向,实现旋转不变
4.关键点描述符:围绕关键点取局部区域,计算梯度方向和幅值,生成 128 维的特征向量(描述子),保证光照、视角轻微变化时描述子仍稳定。
5.关键点匹配:两两比对描述符(算距离),距离最近 → 认为是同一个点
import numpy as np import cv2 as cv img = cv.imread('home.jpg') gray= cv.cvtColor(img,cv.COLOR_BGR2GRAY) sift = cv.SIFT_create() sift.detect() 方法会执行 SIFT 算法的前3步 kp = sift.detect(gray,None) img=cv.drawKeypoints(gray,kp,img) cv.imwrite('sift_keypoints.jpg',img) # -------------------------- 准备两张测试图片 -------------------------- # img1:待匹配的目标图像(比如小图),img2:包含目标的大图 img1 = cv.imread('target.jpg', 0) # 灰度模式读取 img2 = cv.imread('scene.jpg', 0) # 灰度模式读取 # -------------------------- 步骤1-4:提取SIFT特征(关键点+描述子) -------------------------- # 初始化SIFT检测器 sift = cv.SIFT_create() # detectAndCompute 一次性完成: # 步骤1(尺度空间极值检测)+ 步骤2(关键点定位)+ 步骤3(定向)+ 步骤4(生成描述子) kp1, des1 = sift.detectAndCompute(img1, None) # 目标图的关键点+描述子 kp2, des2 = sift.detectAndCompute(img2, None) # 场景图的关键点+描述子 # -------------------------- 步骤5:关键点匹配 -------------------------- # 1. 初始化暴力匹配器(SIFT描述子是浮点型,用NORM_L2距离) bf = cv.BFMatcher(cv.NORM_L2, crossCheck=True) # 2. 匹配描述子(核心:计算描述子间的距离,距离越小匹配度越高) matches = bf.match(des1, des2) # 3. 按匹配距离排序(筛选优质匹配对) matches = sorted(matches, key=lambda x: x.distance) # -------------------------- 可视化匹配结果 -------------------------- # 绘制前50个最优匹配对 img_matches = cv.drawMatches(img1, kp1, img2, kp2, matches[:50], None, flags=cv.DrawMatchesFlags_NOT_DRAW_SINGLE_POINTS) # 显示并保存结果 cv.imshow('SIFT Feature Matching', img_matches) cv.imwrite('sift_matches.jpg', img_matches) cv.waitKey(0) cv.destroyAllWindows()17 特征匹配-SURF算法
在SIFT中,Lowe近似高斯拉普拉斯并加高斯差来求尺度空间。SURF更进一步,用Box Filter(Hessian 矩阵)近似LoG。用Box Filter直接近似高斯二阶导数,再用积分图像让任意大小 Box 的卷积都在常数时间完成,速度远超 SIFT。
import cv2 import numpy as np def surf_feature_detection(image_path): """ 使用SURF检测图像的特征点并绘制 image_path: 图像路径 return: 绘制了特征点的图像 """ # 1. 读取图像(灰度模式,SURF处理灰度图) img = cv2.imread(image_path) if img is None: raise ValueError("无法读取图像,请检查路径是否正确") gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 2. 初始化SURF检测器 # hessianThreshold:Hessian行列式阈值(值越大,检测到的特征点越少,越稳定) # nOctaves:八度数量(默认4),nOctaveLayers:每个八度的层数(默认3) # extended:是否生成128维扩展描述子(False为64维,默认) surf = cv2.xfeatures2d.SURF_create(hessianThreshold=400, extended=False) # 3. 检测特征点并计算描述子 # kp:特征点列表(包含位置、尺度、方向等信息) # des:描述子矩阵(形状:(特征点数量, 64/128)) kp, des = surf.detectAndCompute(gray, None) img_with_kp = cv2.drawKeypoints( img, kp, None, color=(0, 255, 0), # 绿色绘制特征点 flags=cv2.DRAW_MATCHES_FLAGS_DRAW_RICH_KEYPOINTS # 绘制特征点的尺度和方向 ) return img_with_kp # 主函数 if __name__ == "__main__": # 替换为你的图像路径 image_path = "test.jpg" try: result_img = surf_feature_detection(image_path) # 显示结果 cv2.imshow("SURF Feature Detection", result_img) cv2.waitKey(0) cv2.destroyAllWindows() # 保存结果 cv2.imwrite("surf_result.jpg", result_img)18 特征匹配-ORB算法
ORB(Oriented FAST and Rotated BRIEF)是 2011 年提出的开源无专利算法,核心是融合 FAST 角点检测(提速)和 BRIEF 描述子(轻量化),并解决了两者 “无尺度 / 旋转不变性” 的问题,是 SIFT/SURF 的最佳开源替代。
步骤 1:构建尺度金字塔(解决尺度不变性)ORB 先对图像构建8 层尺度金字塔(底层为原图,每层缩放因子为 1.2),模拟不同尺度的图像。在每一层金字塔上检测特征点,确保特征点具有尺度不变性(类似 SIFT/SURF 的尺度空间)。
步骤 2:FAST 角点检测 + 方向分配(解决旋转不变性)方向分配(Oriented FAST):原始 FAST 无方向,ORB 通过计算特征点周围的灰度质心确定主方向:以特征点为中心,取一个邻域窗口;计算窗口内的灰度质心(质心坐标 = 加权平均像素坐标,权重为灰度值);特征点到质心的向量方向即为该特征点的主方向。这样就解决了 FAST 的旋转不变性问题。
步骤 3:BRIEF 描述子改进(Rotated BRIEF)原始 BRIEF:BRIEF 是一种二进制描述子,核心是 “在特征点邻域随机选 N 对像素,比较每对像素的灰度值,生成 0/1 二进制串”,维度低(如 32/64/128 位),匹配速度极快,但无旋转不变性。Rotated BRIEF:ORB 根据特征点的主方向,将随机像素对旋转到主方向上再比较,解决了 BRIEF 的旋转不变性问题,得到 “rBRIEF” 描述子。额外优化:ORB 预定义了 256 对 “最优像素对”(避免随机选点的不稳定性),最终生成 256 位二进制描述子。
import numpy as np
import cv2 as cv
from matplotlib import pyplot as plt
img = cv.imread('simple.jpg', cv.IMREAD_GRAYSCALE)
# Initiate ORB detector
orb = cv.ORB_create()
# find the keypoints with ORB
kp = orb.detect(img,None)
# compute the descriptors with ORB
kp, des = orb.compute(img, kp)
# draw only keypoints location,not size and orientation
img2 = cv.drawKeypoints(img, kp, None, color=(0,255,0), flags=0)
plt.imshow(img2), plt.show()
19 特征匹配
import numpy as np
import cv2 as cv
from matplotlib import pyplot as plt
MIN_MATCH_COUNT = 10
img1 = cv.imread('box.png', cv.IMREAD_GRAYSCALE) # queryImage
img2 = cv.imread('box_in_scene.png', cv.IMREAD_GRAYSCALE) # trainImage
# Initiate SIFT detector
sift = cv.SIFT_create()
# find the keypoints and descriptors with SIFT
kp1, des1 = sift.detectAndCompute(img1,None)
kp2, des2 = sift.detectAndCompute(img2,None)
FLANN_INDEX_KDTREE = 1
index_params = dict(algorithm = FLANN_INDEX_KDTREE, trees = 5)
search_params = dict(checks = 50)
flann = cv.FlannBasedMatcher(index_params, search_params)
matches = flann.knnMatch(des1,des2,k=2)
# store all the good matches as per Lowe's ratio test.
good = []
for m,n in matches:
if m.distance < 0.7*n.distance:
good.append(m)
if len(good)>MIN_MATCH_COUNT:
src_pts = np.float32([ kp1[m.queryIdx].pt for m in good ]).reshape(-1,1,2)
dst_pts = np.float32([ kp2[m.trainIdx].pt for m in good ]).reshape(-1,1,2)
M, mask = cv.findHomography(src_pts, dst_pts, cv.RANSAC,5.0)
matchesMask = mask.ravel().tolist()
h,w = img1.shape
pts = np.float32([ [0,0],[0,h-1],[w-1,h-1],[w-1,0] ]).reshape(-1,1,2)
dst = cv.perspectiveTransform(pts,M)
img2 = cv.polylines(img2,[np.int32(dst)],True,255,3, cv.LINE_AA)
else:
print( "Not enough matches are found - {}/{}".format(len(good), MIN_MATCH_COUNT) )
matchesMask = None
draw_params = dict(matchColor = (0,255,0), # draw matches in green color
singlePointColor = None,
matchesMask = matchesMask, # draw only inliers
flags = 2)
img3 = cv.drawMatches(img1,kp1,img2,kp2,good,None,**draw_params)
plt.imshow(img3, 'gray'),plt.show()
番外篇
20相机校准
一些针孔相机会对图像产生显著的畸变。两种主要的畸变类型是径向畸变和切向畸变。
径向畸变使直线看起来弯曲。径向畸变越远,越远离图像中心。