OpenCV笔记

OpenCV笔记

基础

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

图像

  • 读取图像

    img = cv.imread(filename [,flags])
    """
    filename: 工作路径
    flags: 标志,读取图像的方式
    	cv.IMREAD_COLOR:彩色图像(BGR),忽略透明度,默认标志,1
    	cv.IMREAD_GRAYSCALE: 灰度模式,0
    	cv.IMREAD_UNCHANGED: 包括alpha通道,-1
    """
    
  • 显示图像

    cv.imshow(winname, mat)
    """
    winname: 窗口名称
    mat: 图像
    """
    常与cv.waitKey()和cv.destroyAllWindows()搭配使用
    
  • 保存图像

    cv.imwrite(filename,img [,params])
    """
    filename:文件名
    img: 要保存的图像
    """
    
  • 可结合Matplotlib

视频

  • 捕获视频

    cap = cv.VideoCapture(filename [,apiPreference])
    cap = cv.VideoCapture(index [,apiPreference])
    """
    filename: 视频文件名称,图像序列,视频流URL
    index: 视频捕获设备的ID,0为默认打开默认摄像头
    """
    
    cap.isOpened()  # 判断是否初始化,是则为真;否则用cap.open()打开
    
    ret, frame = cap.read()  # ret为布尔值,如果读取帧,则为真
    
  • 保存视频

    out = cv.VideoWriter(filename,fourcc,fps,frameSize [,isColor])
    """
    filename: 文件名
    fourcc:用于压缩帧的4字符编解码器代码,fourcc = cv.VideoWriter_fourcc(*'XVID')
    fps:视频流的帧率
    frameSize:视频帧大小
    isColor:为0则灰度编码,否则彩色
    """
    out.write(frame)
    

绘制函数

  • 线

    img = cv.line(img, pt1, pt2, color[, thickness[, lineType[, shift]]])
    """
    img: 图片
    pt1:第一个点
    pt2:第二个点
    color:线条颜色
    thickness:线条粗细
    lineType:线条类型
    shift:点坐标中的小数位数
    """
    img = np.zeros((512, 512, 3), np.uint8)
    img = cv.line(img, (0, 0), (511, 511), [255, 0, 0], 5)
    
  • 矩形

    img = cv.rectangle(	img,pt1,pt2,color [,thickness [,lineType [,shift]]])
    """
    img: 图片
    pt1:第一个点
    pt2:对角顶点
    color:矩形颜色
    thickness:矩形线条粗细,负则填充
    lineType:线条类型
    shift:点坐标中的小数位数
    """
    img = cv.rectangle(img, (384, 0), (510, 128), [0, 255, 0], 3)
    
  • img = cv.circle(img,center,radius,color [,thickness [,lineType [,shift]]])
    """
    img: 图片
    center: 圆心
    radius:半径
    color:圆颜色
    thickness:圆线条粗细,负则填充
    lineType:线条类型
    shift:中心坐标和半径中的小数位数
    """
    img = cv.circle(img, (447, 63), 63, (0, 0, 255), -1)
    
  • 椭圆

    img = cv.ellipse(img,center,axes,angle,startAngle,endAngle,color [,thickness [,lineType [,shift]]])
    """
    img: 图片
    center: 椭圆中心
    axes:长短半轴,(长半轴,短半轴)
    angle:旋转角,负角度(顺时针)
    startAngle:椭圆弧起始角度(以焦点)
    endAngle:椭圆弧终止角度(以焦点)
    color:椭圆颜色
    thickness:椭圆线条粗细,负则填充
    lineType:线条类型
    shift:中心坐标和轴值的小数位数
    """
    img = cv.ellipse(img, (256, 256), (100, 50), 0, 45, 225, 255, -1)
    
  • 多边形

    img = cv.polylines(img, pts, isClosed, color[, thickness[, lineType[, shift]]])
    """
    img: 图片
    pts: 多边形的数组
    isClosed:折线是否闭合,假则最后一点不与源点相连
    color:折线颜色
    thickness:折线线条粗细
    lineType:线条类型
    shift:顶点坐标中的小数位数
    """
    pts = np.array([[10, 5], [20, 30], [70, 20], [50,10]], np.int32)
    pts = pts.reshape((-1, 1, 2))
    img = cv.polylines(img, [pts], True, (0, 255, 255))
    

鼠标绘画

  • 鼠标事件

    events = [i for i in dir(cv) if 'EVENT' in i]
    print(events)
    # ['EVENT_FLAG_ALTKEY', 'EVENT_FLAG_CTRLKEY', 'EVENT_FLAG_LBUTTON', 'EVENT_FLAG_MBUTTON', 'EVENT_FLAG_RBUTTON', 'EVENT_FLAG_SHIFTKEY', 'EVENT_LBUTTONDBLCLK', 'EVENT_LBUTTONDOWN', 'EVENT_LBUTTONUP', 'EVENT_MBUTTONDBLCLK', 'EVENT_MBUTTONDOWN', 'EVENT_MBUTTONUP', 'EVENT_MOUSEHWHEEL', 'EVENT_MOUSEMOVE', 'EVENT_MOUSEWHEEL', 'EVENT_RBUTTONDBLCLK', 'EVENT_RBUTTONDOWN', 'EVENT_RBUTTONUP']
    
  • 鼠标回调函数

    cv.setMouseCallback(winname, onMouse, userdata=0)
    """
    winname:窗口名
    onMouse:鼠标事件的回调函数
    userdata:传递给回调函数的可选参数
    """
    def draw_circle(event, x, y, flags, param):
         """鼠标回调函数"""
         if event == cv.EVENT_LBUTTONDBLCLK:
             cv.circle(img, (x, y), 100, (255, 0, 0), -1)
    
    
    img = np.zeros((512, 512, 3), np.uint8)
    cv.namedWindow('image')
    cv.setMouseCallback('image', draw_circle)
    while(1):
        cv.imshow('image', img)
        if cv.waitKey(20) == 27:
            break
    cv.destroyAllWindows()
    

轨迹栏作调色板

  • 创建轨迹栏

    cv.createTrackbar(trackbarname, winname, value, count, onChange=0, userdata=0)
    """
    trackbarname:轨迹栏名称
    winname:用于创建轨迹栏的父级窗口名称
    value:初始值
    count:滑块最大位置,最小始终为0
    onChange:每次滑块更改位置时要调用的函数名
    userdata:传递给回调的用户数据。它可用于处理轨迹栏事件而无需使用全局变量
    """
    
  • 获取轨迹栏位置

    retval = cv.getTrackbarPos(trackbarname, winname)
    """
    trackbarname:轨迹栏名称
    winname:用于创建轨迹栏的父级窗口名称
    """
    
    def nothing(x):
        pass
    
    # 创建黑画窗口
    img = np.zeros((300, 512, 3), np.uint8)
    cv.namedWindow('image')
    # 创建改变色彩的轨迹栏
    cv.createTrackbar('R', 'image', 50, 255, nothing)
    cv.createTrackbar('G', 'image', 0, 255, nothing)
    cv.createTrackbar('B', 'image', 0, 255, nothing)
    # 创建开关
    switch = '0: OFF\n1: ON'
    cv.createTrackbar(switch, 'image', 0, 1, nothing)
    
    while(1):
        cv.imshow('image', img)
        k = cv.waitKey(1)
        if k == 27:
            break
        # 获得各轨迹栏的位置
        r = cv.getTrackbarPos('R', 'image')
        g = cv.getTrackbarPos('G', 'image')
        b = cv.getTrackbarPos('B', 'image')
        s = cv.getTrackbarPos(switch, 'image')
        if s == 0:
            img[:] = 0
        else:
            img[:] = [b, g, r]
    cv.destroyAllWindows()
    

核心操作

图像的基本操作

  • 读取和修改像素值

    px = img[100, 100]  # [157, 166, 200]
    # 获得蓝色像素
    blue = img[100, 100, 0]  # 157
    # 修改像素值
    img[100, 100] = [255, 255, 255]
    # 更好的获取和修改方法
    img.item(10, 10, 2)  # 59 获取红色像素
    img.itemset((10, 10, 2), 100)  # 修改红色像素值
    
  • 读取图像属性

    img.shape  # (342, 548, 3) 行、列、通道
    img.size  # 562248 所有像素值
    img.dtype  # uint8 数据类型
    
  • 图像感兴趣区域(ROI)

    roi = img[280: 340, 330: 390]
    
  • 分离合并图像通道

    # 分离
    b, g, r = cv.split(img)  # 很费时的操作
    b = img[:,:,0]
    # 合并
    img = cv.merge((b, g, r))
    
  • 生成图像边框(填充 Padding)

    dst = cv.copyMakeBorder(src, top, bottom, left, right, borderType[, dst[, value]])
    """
    src: 源图像
    top,bottom,left,right:上下左右宽度像素值
    borderType:
    	cv.BORDER_CONSTANT:恒定值
    	cv.BORDER_REFLECT: fedcba|abcdefgh|hgfedcb
    	cv.BORDER_REFLECT_101 or cv.BORDER_DEFAULT: gfedcb|abcdefgh|gfedcba
    	cv.BORDER_REPLICATE:aaaaaa|abcdefgh|hhhhhhh
    	cv.BORDER_WRAP:cdefgh|abcdefgh|abcdefg
    value:颜色值,如果类型为cv.BORDER_CONSTANT
    """
    

图像的算术运算

  • 图像相加

    • cv.add为饱和运算(建议使用),numpy为取模运算
    x = np.uint8([250])
    y = np.uint8([100])
    print(cv.add(x, y))  # 255 建议使用
    print(x + y)  # 94
    
  • 图像融合(Image Blending)

    # 加权相加以达到图片融合
    dst = cv.addWeighted(src1, alpha, src2, beta, gamma[, dst[, dtype]])
    """
    src1、src2:相加数组
    alpha:1的权重
    beta:2的权重
    gamma:偏置
    """
    dst = cv.addWeighted(img1, 0.7, img2, 0.3, 0)
    
  • 逐位运算(Bitwise Operations)

    • 通常在提取部分图片时使用
    • 包括
      • cv.bitwise_and:
        • dst(I)=src1(I)∧src2(I)if mask(I)≠0
        • dst(I)=src1(I)∧src2if mask(I)≠0,通道数与src1相同
        • dst(I)=src1∧src2(I)if mask(I)≠0,通道数与src2相同
      • cv.bitwise_or:
        • dst(I)=src1(I)∨src2(I)if mask(I)≠0
        • dst(I)=src1(I)∨src2if mask(I)≠0,通道数与src1相同
        • dst(I)=src1∨src2(I)if mask(I)≠0,通道数与src2相同
      • cv.bitwise_not:dst(I)=¬src(I)
      • cv.bitwise_xor:
        • dst(I)=src1(I)⊕src2(I)if mask(I)≠0
        • dst(I)=src1(I)⊕src2if mask(I)≠0,通道数与src1相同
        • dst(I)=src1⊕src2(I)if mask(I)≠0,通道数与src2相同
    dst	= cv.bitwise_and(src1, src2[, dst[, mask]])
    """
    src1,src2:数组或标量
    mask:掩膜
    """
    img1 = cv.imread('messi5.jpg')
    img2 = cv.imread('opencv-logo-white.png')
    # 创建ROI
    rows, cols, channels = img2.shape
    roi = img1[0: rows, 0: cols]
    # 创建掩膜和反向掩膜
    img2gray = cv.cvtColor(img2, cv.COLOR_BGR2GRAY)
    ret, mask = cv.threshold(img2gray, 10, 255, cv.THRESH_BINARY)
    mask_inv = cv.bitwise_not(mask)
    # 在ROI过滤出标志位置(黑色)
    img1_bg = cv.bitwise_and(roi,roi,mask = mask_inv)
    # 取出标志
    img2_fg = cv.bitwise_and(img2,img2,mask = mask)
    # 合并
    dst = cv.add(img1_bg,img2_fg)
    img1[0:rows, 0:cols ] = dst
    
    cv.imshow('res',img1)
    cv.waitKey(0)
    cv.destroyAllWindows()
    

性能测试和提升技术

  • 性能测试

    e1 = cv.getTickCount()
    # 需要执行的代码
    e2 = cv.getTickCount()
    time = (e2 - e1) / cv.getTickFrequency()
    
  • 优化默认

    cv.useOptimized()  # True
    cv.setUseOptimized(False)  # 关闭优化
    
  • 性能优化技术

    • 尽量避免使用循环
    • 尽量矢量化,NumpyOpenCV都对其进行了优化
    • 利用缓存一致性
    • 尽量少复制数组

图像处理

改变色彩空间

  • 改变色彩空间

    • 有150多种色彩空间变换方法,比较常用的是BGR<->GRAYBGR<->HSV
    • HSV,色相Hue∈[0, 179],饱和度Saturation∈[0, 255],明度Value∈[0, 255]
    dst	= cv.cvtColor(src, code[, dst[, dstCn]])
    """
    src:8位或16位无符号或单精度浮点图像
    code:色彩空间变换码
    dstCn:最终图片通道数,默认自动
    """
    code = [i for i in dir(cv) if i.startswith('COLOR_')]
    
  • 物体追踪

    • 获取视频的每一帧图像
    • 转化成HSV色彩空间
    • 阈值过滤HSV图像
    • 可以追踪物品,进行其他操作
    cap = cv.VideoCapture(0)
    while True:
        # 获取一帧图片
        _, frame = cap.read()
        # 将BGR-->HSV(色相,饱和度,明度)
        hsv = cv.cvtColor(frame, cv.COLOR_BGR2HSV)
        # 在HSV中,定义蓝色的范围
        lower_blue = np.array([110, 50, 50])
        upper_blue = np.array([130, 255, 255])
        # 过滤HSV图像获得蓝色掩膜
        mask = cv.inRange(hsv, lower_blue, upper_blue)
        # bitwise_and 掩膜和原始图像
        res = cv.bitwise_and(frame, frame, mask=mask)
        cv.imshow('frame', frame)
        cv.imshow('mask', mask)
        cv.imshow('res', res)
        if cv.waitKey(5) == 27:
            break
    cap.release()
    cv.destroyAllWindows()
    
    • 如何找到HSV的值用于追踪
    green = np.uint8([[[0, 255, 0]]])
    hsv_green = cv.cvtColor(green, cv.COLOR_BGR2HSV)
    print(hsv_green)  # [[[ 60 255 255]]]
    

图像的几何变换(Geometric Transformations)

  • 缩放(Scaling)

    • cv.INTER_AREA:用于缩小的插值法
    • cv.INTER_CUBIC:用于放大的插值法,慢
    • cv.INTER_LINEAR:用于放大的插值法,默认
    dst	= cv.resize(src, dsize[, dst[, fx[, fy[, interpolation]]]])
    """
    src:输入图片
    dsize:输出图片尺寸,如果为0,则dsize = Size(round(fx*src.cols), round(fy*src.rows))
    fx:水平缩放系数,如果为0,(double)dsize.width/src.cols
    fy:垂直缩放系数,如果为0,(double)dsize.height/src.rows
    interpolation:插值方法
    """
    img = cv.imread('messi5.jpg')
    res = cv.resize(img, None, fx=2, fy=2, interpolation = cv.INTER_CUBIC)
    # 或
    height, width = img.shape[: 2]
    res = cv.resize(img, (2*width, 2*height), interpolation = cv.INTER_CUBIC)
    
  • 平移(Translation)

    • 平移(tx,ty),则平移矩阵M=[[1, 0, tx], [0, 1, ty]]
    • dst(x,y) = src(M11x + M12y + M13, M21x + M22y + M23)
    dst	= cv.warpAffine(src, M, dsize[, dst[, flags[, borderMode[, borderValue]]]])
    """
    src:输入图片
    M:2 x 3变换矩阵
    dsize:输出尺寸(宽,高)
    flags:结合了插值方法和逆变换,默认线性插值
    """
    img = cv.imread('messi5.jpg', 0)
    rows,cols = img.shape
    M = np.float32([[1, 0, 100], [0, 1, 50]])
    dst = cv.warpAffine(img, M, (cols, rows))
    cv.imshow('img', dst)
    cv.waitKey(0)
    cv.destroyAllWindows()
    
  • 旋转(Rotation)

    • 旋转中心center和旋转角度θ(正为逆时针),则旋转矩阵为M=[[α, β, (1−α)⋅center.x−β⋅center.y], [−β, α, β⋅center.x+(1−α)⋅center.y]],且α=scale⋅cosθ,β=scale⋅sinθ
    retval = cv.getRotationMatrix2D(center, angle, scale)
    """
    得到仿射矩阵
    center:旋转中心
    angle:旋转角度,正值为逆时针
    scale:缩放系数
    """
    img = cv.imread('messi5.jpg', 0)
    rows,cols = img.shape
    M = cv.getRotationMatrix2D(((cols-1)/2.0, (rows-1)/2.0), 90, 1)
    dst = cv.warpAffine(img, M, (cols, rows))
    
  • 仿射变换(Affine Transformation)

    • 找到原图中的三点位置和对应仿射图中的三点位置,形成仿射矩阵
    • [[x′i], [y′i]] = map_matrix · [[xi], [yi], 1], dst(i)=(x′i,y′i), src(i)=(xi, yi),i=0, 1, 2
    retval = cv.getAffineTransform(src, dst)
    """
    得到仿射矩阵
    src:原图中三角形顶点坐标
    dst:目标图中三角形顶点坐标
    """
    img = cv.imread('drawing.png')
    rows,cols,ch = img.shape
    pts1 = np.float32([[50, 50],[200, 50],[50, 200]])
    pts2 = np.float32([[10, 100],[200, 50],[100, 250]])
    M = cv.getAffineTransform(pts1, pts2)
    dst = cv.warpAffine(img,M,(cols, rows))
    plt.subplot(121), plt.imshow(img), plt.title('Input')
    plt.subplot(122), plt.imshow(dst), plt.title('Output')
    plt.show()
    
  • 透视变换(Perspective Transformation)

    • 需要原图4个点(3点不共线)与对应目标图4点位置得到3 x 3的透视矩阵
    • [[ti · x′i], [ti · y′i], [ti]] = map_matrix · [[xi], [yi], 1], dst(i)=(x′i,y′i), src(i)=(xi, yi),i=0, 1, 2, 3
    retval = cv.getPerspectiveTransform(src, dst[, solveMethod])
    """
    得到透视矩阵
    src:原图四边形顶点坐标
    dst:目标图四边形顶点坐标
    solveMethod:传给cv.solve的矩阵分解方法
    """
    img = cv.imread('sudoku.png')
    rows,cols,ch = img.shape
    pts1 = np.float32([[56, 65],[368, 52],[28, 387],[389, 390]])
    pts2 = np.float32([[0, 0],[300, 0],[0, 300],[300, 300]])
    M = cv.getPerspectiveTransform(pts1, pts2)
    dst = cv.warpPerspective(img, M, (300, 300))
    plt.subplot(121), plt.imshow(img), plt.title('Input')
    plt.subplot(122), plt.imshow(dst), plt.title('Output')
    plt.show()
    

图像阈值

  • 简单阈值(Simple Thresholding)

    • 必须为灰度图
    • 大于阈值设置为最大值,否则为0
    • 获得二值图或去除噪声
    • 阈值类型
      • cv.THRESH_BINARYdst(x,y)=maxval, if src(x,y)>thresh, 否则为0
      • cv.THRESH_BINARY_INVdst(x,y)=0, if src(x,y)>thresh,否则为maxval
      • cv.THRESH_TRUNCdst(x,y)=threshold, if src(x,y)>thresh,否则为src(x,y)
      • cv.THRESH_TOZEROdst(x,y)=src(x,y), if src(x,y)>thresh,否则为0
      • cv.THRESH_TOZERO_INVdst(x,y)=0, if src(x,y)>thresh,否则为src(x,y)
      • cv.THRESH_OTSU:用Otsu算法选择最优阈值
      • cv.THRESH_TRIANGLE:用三角算法选择最优阈值
    retval, dst	= cv.threshold(src, thresh, maxval, type[, dst])
    """
    src:多通道或8位、32位浮点数组
    thresh:阈值
    maxval:最大值
    type:阈值类型
    """
    img = cv.imread('gradient.png', 0)
    ret, thresh1 = cv.threshold(img, 127, 255, cv.THRESH_BINARY)
    ret, thresh2 = cv.threshold(img, 127, 255, cv.THRESH_BINARY_INV)
    ret, thresh3 = cv.threshold(img, 127, 255, cv.THRESH_TRUNC)
    ret, thresh4 = cv.threshold(img, 127, 255, cv.THRESH_TOZERO)
    ret, thresh5 = cv.threshold(img, 127, 255, cv.THRESH_TOZERO_INV)
    titles = ['Original Image', 'BINARY', 'BINARY_INV', 'TRUNC', 'TOZERO', 'TOZERO_INV']
    images = [img, thresh1, thresh2, thresh3, thresh4, thresh5]
    for i in range(6):
        plt.subplot(2, 3, i+1), plt.imshow(images[i], 'gray')
        plt.title(titles[i])
        plt.xticks([]), plt.yticks([])
    plt.show()
    
  • 自适应阈值(Adaptive Thresholding)

    • 基于一个像素周围的区域计算阈值,可以获得不同区域的不同阈值,对不同光照强度的图有更好的处理结果
    • 自适应方法
      • cv.ADAPTIVE_THRESH_MEAN_C:阈值=周围平均值-常数
      • cv.ADAPTIVE_THRESH_GAUSSIAN_C:阈值=周围高斯加权和-常数
    dst	= cv.adaptiveThreshold(src, maxValue, adaptiveMethod, thresholdType, blockSize, C[, dst])
    """
    src:8位单通道图
    maxValue:最大值
    adaptiveMethod:自适应方法
    thresholdType:THRESH_BINARY或THRESH_BINARY_INV
    blockSize:像素周围尺寸,3、5、7...
    C:常数
    """
    img = cv.imread('sudoku.png', 0)
    img = cv.medianBlur(img, 5)
    ret, th1 = cv.threshold(img, 127, 255, cv.THRESH_BINARY)
    th2 = cv.adaptiveThreshold(img, 255, cv.ADAPTIVE_THRESH_MEAN_C, cv.THRESH_BINARY, 11, 2)
    th3 = cv.adaptiveThreshold(img, 255, cv.ADAPTIVE_THRESH_GAUSSIAN_C, cv.THRESH_BINARY, 11, 2)
    titles = ['Original Image', 'Global Thresholding (v = 127)', 'Adaptive Mean Thresholding', 'Adaptive Gaussian Thresholding']
    images = [img, th1, th2, th3]
    for i in range(4):
        plt.subplot(2, 2, i+1), plt.imshow(images[i], 'gray')
        plt.title(titles[i])
        plt.xticks([]), plt.yticks([])
    plt.show()
    
  • Otsu二值化

    • Otsu方法可以自动选取阈值
    • 对于双峰图(bimodel image),Otsu方法可以确定一个全局最优的阈值
    img = cv.imread('noisy2.png',0)
    # 全局阈值
    ret1, th1 = cv.threshold(img, 127, 255, cv.THRESH_BINARY)
    # Otsu阈值,第二个参数可以随便填,最优值会自动返回
    ret2, th2 = cv.threshold(img, 0, 255, cv.THRESH_BINARY+cv.THRESH_OTSU)
    # 高斯滤波后Otsu阈值
    blur = cv.GaussianBlur(img, (5,5), 0)
    ret3, th3 = cv.threshold(blur, 0, 255, cv.THRESH_BINARY+cv.THRESH_OTSU)
    # 画出图像及其直方图
    images = [img, 0, th1,
              img, 0, th2,
              blur, 0, th3]
    titles = ['Original Noisy Image', 'Histogram', 'Global Thresholding (v=127)',
              'Original Noisy Image', 'Histogram', "Otsu's Thresholding",
              'Gaussian filtered Image', 'Histogram', "Otsu's Thresholding"]
    for i in range(3):
        plt.subplot(3, 3, i*3+1), plt.imshow(images[i*3], 'gray')
        plt.title(titles[i*3]), plt.xticks([]), plt.yticks([])
        plt.subplot(3, 3, i*3+2), plt.hist(images[i*3].ravel(), 256)
        plt.title(titles[i*3+1]), plt.xticks([]), plt.yticks([])
        plt.subplot(3, 3, i*3+3), plt.imshow(images[i*3+2], 'gray')
        plt.title(titles[i*3+2]), plt.xticks([]), plt.yticks([])
    plt.show()
    
    • Otsu二值化的方法
      • 阈值为加权类方差的最小值
    img = cv.imread('noisy2.png', 0)
    blur = cv.GaussianBlur(img, (5, 5), 0)
    # 找到标准直方图和它的累积分布函数
    hist = cv.calcHist([blur], [0], None, [256], [0, 256])
    hist_norm = hist.ravel() / hist.max()
    Q = hist_norm.cumsum()
    bins = np.arange(256)
    fn_min = np.inf
    thresh = -1
    for i in range(256):
        p1, p2 = np.hsplit(hist_norm,[i]) # 概率
        q1, q2 = Q[i],Q[255]-Q[i] # 累积
        b1, b2 = np.hsplit(bins, [i]) # weights权重
        # 期望和方差
        m1, m2 = np.sum(p1*b1)/q1, np.sum(p2*b2)/q2
        v1, v2 = np.sum(((b1-m1)**2)*p1)/q1, np.sum(((b2-m2)**2)*p2)/q2
        # 计算最小值
        fn = v1*q1 + v2*q2
        if fn < fn_min:
            fn_min = fn
            thresh = i
    # find otsu's threshold value with OpenCV function
    ret, otsu = cv.threshold(blur,0,255,cv.THRESH_BINARY+cv.THRESH_OTSU)
    print( "{} {}".format(thresh, ret) )
    

平滑图像

  • 2D卷积(图像滤波)

    • 低通滤波器(low-pass filter)可以帮助消除噪声,模糊图像
    • 高通滤波器(high-pass filter)可以找到图像边缘
    dst	= cv.filter2D(src, ddepth, kernel[, dst[, anchor[, delta[, borderType]]]])
    """
    src:输入图片
    ddepth:目标图片的深度,-1为保持原样
    kernel:卷积核,单通道浮点矩阵
    anchor:内核基准点,默认为中心
    delta:在储存目标图像前可选的添加到像素的值,默认为0
    borderType:像素外插方法,默认BORDER_DEFAULT。当其中心移动到图像外,函数可以根据指定的边界模式进行插值运算。函数实质上是计算内核与图像的相关性而不是卷积
    """
    img = cv.imread('opencv_logo.png')
    kernel = np.ones((5, 5), np.float32) / 25
    dst = cv.filter2D(img, -1, kernel)
    plt.subplot(121), plt.imshow(img), plt.title('Original')
    plt.xticks([]), plt.yticks([])
    plt.subplot(122), plt.imshow(dst), plt.title('Averaging')
    plt.xticks([]), plt.yticks([])
    plt.show()
    
  • 图像模糊(Image Blurring)

    • 用低通滤波器内核卷积图片,消除高频内容(噪声,边缘)
    • 平均模糊(Averaging)
      • 使用标准方形滤波器,在内核区域取像素平均值再替换中心元素
    dst	= cv.blur(src, ksize[, dst[, anchor[, borderType]]])
    """
    src:任意通道图片,深度可以是CV_8U, CV_16U, CV_16S, CV_32F或CV_64F
    ksize:模糊内核尺寸
    anchor:基准点,默认中心
    borderType:像素外插方法
    """
    img = cv.imread('opencv-logo-white.png')
    blur = cv.blur(img, (5, 5))
    plt.subplot(121), plt.imshow(img), plt.title('Original')
    plt.xticks([]), plt.yticks([])
    plt.subplot(122), plt.imshow(blur), plt.title('Blurred')
    plt.xticks([]), plt.yticks([])
    plt.show()
    
    dst	= cv.boxFilter(src, ddepth, ksize[, dst[, anchor[, normalize[, borderType]]]])
    """
    normalize:True表示标准化,与blur相同
    """
    
  • 高斯模糊(Gaussian Blurring)

    • 运用高斯内核滤波,能高效过滤高斯噪声
    • 核尺寸的宽高必须为正奇数,需要指定X、Y的标准差,如果只定义X,则YX相同,若两者都为0,则根据内核尺寸计算
    retval = cv.getGaussianKernel(ksize, sigma[, ktype])
    """
    创建高斯内核
    ksize:正奇数
    sigma:高斯标准差
    ktype:过滤系数类型,默认CV_64F
    """
    
    dst	= cv.GaussianBlur(src, ksize, sigmaX[, dst[, sigmaY[, borderType]]])
    """
    src:任意通道图片,深度可以是CV_8U, CV_16U, CV_16S, CV_32F或CV_64F
    ksize:宽高正奇数,可为0,由sigma计算出
    sigmaX:X方向的高斯标准差
    sigmaY:Y方向的高斯标准差
    """
    blur = cv.GaussianBlur(img, (5, 5), 0)
    
  • 中值模糊(Median Blurring)

    • 获取像素内核区域的中位数,然后替换中心元素
    • 能高效消除椒盐噪声
    dst	= cv.medianBlur(src, ksize[, dst])
    """
    src:单通道、三通道或四通道图片,当ksize为3或5时,深度为CV_8U, CV_16U或 CV_32F,当ksize更大时,深度只能为CV_8U
    ksize:光圈的线性尺寸,大于1且为奇数,3、5、7...
    """
    median = cv.medianBlur(img,5)
    
  • 双边滤波(Bilateral Filtering)

    • 高效消除噪声且保持边缘完好
    • 运用两个高斯滤波器,一个用来模糊,另一个确保像素强度同中心像素相似的被模糊,因为边缘像素有很大的强度变化
    dst	= cv.bilateralFilter(src, d, sigmaColor, sigmaSpace[, dst[, borderType]])
    """
    src:8位浮点单通道或三通道图片
    d:滤波时临近像素的直径,非正时,可由sigmaSpace计算
    sigmaColor:色彩空间中滤波器的sigma值,值越大,临近像素有更宽广的颜色会混合在一起,产生更大的半相等颜色区域
    sigmaSpace:坐标空间中滤波器的sigma值,值越大,更大区域的像素会受到影响,就像他们的颜色变得很接近一样。d>0,临近尺寸与sigmaSpace无关,否则成正比
    """
    blur = cv.bilateralFilter(img, 9, 75, 75)
    

形态变换

  • 腐蚀(Erosion)

    • 腐蚀前景边缘(一般前景是白色的),所有临近边缘的像素会依据内核的尺寸相应丢弃,前景物体就好像变瘦了或白色区域减小了
    • 能有效消除小白点噪声
    dst	= cv.erode(src, kernel[, dst[, anchor[, iterations[, borderType[, borderValue]]]]])
    """
    src:任意通道图片,深度为CV_8U, CV_16U, CV_16S, CV_32F或 CV_64F
    kernel:腐蚀结构元件
    anchor:基准点,默认中心
    iterations:腐蚀次数
    """
    img = cv.imread('j.png', 0)
    kernel = np.ones((5, 5), np.uint8)
    erosion = cv.erode(img, kernel, iterations = 1)
    
  • 膨胀(Dilation)

    • 与腐蚀相反,膨胀会增加前景尺寸或白色区域
    • 一般,先腐蚀消除白色噪声,在膨胀还原
    • 膨胀对连接目标破断部分很有用
    dst	= cv.dilate(src, kernel[, dst[, anchor[, iterations[, borderType[, borderValue]]]]])
    """
    src:任意通道图片,深度为CV_8U, CV_16U, CV_16S, CV_32F或 CV_64F
    kernel:膨胀结构元件
    anchor:基准点,默认中心
    iterations:膨胀次数
    """
    dilation = cv.dilate(img, kernel, iterations = 1)
    
  • 开运算(Opening)

    • 先腐蚀再膨胀,消除白点噪声
    • dst=open(src,element)=dilate(erode(src,element))
    • 形态操作类型
      • cv.MORPH_OPEN
    dst	= cv.morphologyEx(src, op, kernel[, dst[, anchor[, iterations[, borderType[, borderValue]]]]])
    """
    src:任意通道图片,深度为CV_8U, CV_16U, CV_16S, CV_32F或 CV_64F
    op:形态操作类型
    kernel:结构元件
    anchor:基准点,默认中心
    iterations:膨胀或腐蚀次数,默认为1
    """
    opening = cv.morphologyEx(img, cv.MORPH_OPEN, kernel)
    
  • 闭运算(Closing)

    • 先膨胀再腐蚀,消除前景内部小孔或小黑点
    • dst=close(src,element)=erode(dilate(src,element))
    • 形态操作类型
      • cv.MORPH_CLOSE
    closing = cv.morphologyEx(img, cv.MORPH_CLOSE, kernel)
    
  • 形态梯度(Morphological Gradient)

    • 膨胀 - 腐蚀,突出了团块的边缘
    • dst=morph_grad(src,element)=dilate(src,element)−erode(src,element)
    • 形态操作类型
      • cv.MORPH_GRADIENT
    gradient = cv.morphologyEx(img, cv.MORPH_GRADIENT, kernel)
    
  • 顶帽(Top Hat)

    • 原图 - 开运算,突出了比原图轮廓周围的区域更明亮的区域,且这一操作和选择的核的大小相关
    • dst=tophat(src,element)=src−open(src,element)
    • 形态操作类型
      • cv.MORPH_TOPHAT
    tophat = cv.morphologyEx(img, cv.MORPH_TOPHAT, kernel)
    
  • 黑帽(Black Hat)

    • 闭运算 - 原图,突出了比原图轮廓周围的区域更暗的区域,且这一操作和选择的核的大小相关
    • dst=blackhat(src,element)=close(src,element)−src
    • 形态操作类型
      • cv.MORPH_BLACKHAT
    blackhat = cv.morphologyEx(img, cv.MORPH_BLACKHAT, kernel)
    
  • 结构元件(Structuring Element)

    • 形态形状类型
      • cv.MORPH_RECT:矩形
      • cv.MORPH_CROSS:十字形
      • cv.MORPH_ELLIPSE:椭圆形
    retval	= cv.getStructuringElement(shape, ksize[, anchor])
    """
    shape:形态形状类型
    ksize:结构元件尺寸
    anchor:基准点,默认为中心,只有十字形元素的形状依赖基准点位置,其他基准点只调整形态操作的结果移位了多少
    """
    # 矩形核
    cv.getStructuringElement(cv.MORPH_RECT, (5, 5))
    array([[1, 1, 1, 1, 1],
           [1, 1, 1, 1, 1],
           [1, 1, 1, 1, 1],
           [1, 1, 1, 1, 1],
           [1, 1, 1, 1, 1]], dtype=uint8)
    # 椭圆核
    cv.getStructuringElement(cv.MORPH_ELLIPSE, (5, 5))
    array([[0, 0, 1, 0, 0],
           [1, 1, 1, 1, 1],
           [1, 1, 1, 1, 1],
           [1, 1, 1, 1, 1],
           [0, 0, 1, 0, 0]], dtype=uint8)
    # 十字核
    cv.getStructuringElement(cv.MORPH_CROSS, (5, 5))
    array([[0, 0, 1, 0, 0],
           [0, 0, 1, 0, 0],
           [1, 1, 1, 1, 1],
           [0, 0, 1, 0, 0],
           [0, 0, 1, 0, 0]], dtype=uint8)
    

图像梯度

  • Sobel和Scharr导数

    • Sobel算子结合了高斯平滑和微分运算,有利抵抗噪声
    • 内核用来计算其导数(x或y方向)
      • ksize=1,会使用3 x 11 x 3的内核(没有高斯平滑),且只能用于一阶或二阶xy的偏导
      • ksize=cv.FILTER_SCHARR-1时,会使用3 x 3Scharr滤波器,其精度大于3 x 3Sobel滤波器
      • Scharr滤波器:[[-3, 0, 3], [-10, 0, 10], [-3, 0, 3]]x方向;y方向为x方向的转置
      • 默认ksize=3,Sobel滤波器:[[-1, 0, 1], [-2, 0, 2], [-1, 0, 1]]x方向;y方向为x方向的转置
    dst	= cv.Sobel(src, ddepth, dx, dy[, dst[, ksize[, scale[, delta[, borderType]]]]])
    """
    src:输入图片
    ddepth:输出图片深度
    dx:x偏导阶数
    dy:y偏导阶数
    ksize:Sobel内核,默认为3,可为1、3、5、7
    scale:用于计算导数的缩放系数,默认为1
    delta:结果存入目标矩阵前需要增加的值,默认为0
    """
    # ddepth
    # input         |output
    # CV_8U	        |-1/CV_16S/CV_32F/CV_64F
    # CV_16U/CV_16S	|-1/CV_32F/CV_64F
    # CV_32F	    |-1/CV_32F/CV_64F
    # CV_64F	    |-1/CV_64F
    
  • Laplacian导数

    • ksize>1,利用Sobel算子计算xy的二阶偏导数和
    • ksize=1,利用内核[[0, 1, 0], [1, -4, 1], [0, 1, 0]]计算
    dst	= cv.Laplacian(src, ddepth[, dst[, ksize[, scale[, delta[, borderType]]]]])
    """
    src:输入图片
    ddepth:输出图片深度
    ksize:内核大小,正奇数,默认为1
    scale:用于计算导数的缩放系数,默认为1
    delta:结果存入目标矩阵前需要增加的值,默认为0
    """
    img = cv.imread('dave.jpg', 0)
    laplacian = cv.Laplacian(img, cv.CV_64F)
    sobelx = cv.Sobel(img, cv.CV_64F, 1, 0, ksize=5)
    sobely = cv.Sobel(img, cv.CV_64F, 0, 1, ksize=5)
    plt.subplot(2, 2, 1), plt.imshow(img, cmap = 'gray')
    plt.title('Original'), plt.xticks([]), plt.yticks([])
    plt.subplot(2, 2, 2), plt.imshow(laplacian, cmap = 'gray')
    plt.title('Laplacian'), plt.xticks([]), plt.yticks([])
    plt.subplot(2, 2, 3), plt.imshow(sobelx, cmap = 'gray')
    plt.title('Sobel X'), plt.xticks([]), plt.yticks([])
    plt.subplot(2, 2, 4), plt.imshow(sobely, cmap = 'gray')
    plt.title('Sobel Y'), plt.xticks([]), plt.yticks([])
    plt.show()
    
  • 边缘丢失问题

    • 黑到白变换时是正斜率,而白到黑则是负斜率,所以当转换数据为np.uint8时,所有的负斜率就变成0了,可能丢失边缘
    • 如果想检测所有边缘,最好是选择更高深度类型,如cv.CV_16S, cv.CV_64F等,获得其绝对值然后再转换回cv.CV_8U
    img = cv.imread('box.png',0)
    # 输出类型cv.CV_8U
    sobelx8u = cv.Sobel(img, cv.CV_8U, 1, 0, ksize=5)
    # 输出类型cv.CV_64F.然后获取绝对值后转换回cv.CV_8U
    sobelx64f = cv.Sobel(img, cv.CV_64F, 1, 0, ksize=5)
    abs_sobel64f = np.absolute(sobelx64f)
    sobel_8u = np.uint8(abs_sobel64f)
    plt.subplot(1, 3, 1), plt.imshow(img, cmap = 'gray')
    plt.title('Original'), plt.xticks([]), plt.yticks([])
    plt.subplot(1, 3, 2), plt.imshow(sobelx8u, cmap = 'gray')
    plt.title('Sobel CV_8U'), plt.xticks([]), plt.yticks([])
    plt.subplot(1, 3, 3), plt.imshow(sobel_8u, cmap = 'gray')
    plt.title('Sobel abs(CV_64F)'), plt.xticks([]), plt.yticks([])
    plt.show()
    

Canny边缘检测

  • 检测流程

    • 减少噪声
      • 5 x 5高斯滤波器过滤
    • 找到图片的强度梯度
      • 用Sobel内核找到水平和垂直方向的一阶偏导,找到边缘梯度和方向
      • 梯度方向总是垂直于边缘的,方向四舍五入至水平,垂直或两对角线方向
    • 非极大抑制(Non-maximum Suppression)
      • 梯度方向上保留局部极大值,其他抑制(设为0
    • 滞后阈值(Hysteresis Thresholding)
      • 设定2个阈值,minValmaxVal,强度梯度高于maxVal被确定为边界,低于minVal的丢弃
      • 介于两者之间的,若与确定为边界的连接,则认为是边界,否则丢弃
      • 会去除一些小像素噪声
    edges = cv.Canny(image, threshold1, threshold2[, edges[, apertureSize[, L2gradient]]])
    """
    image:8位图片
    threshold1:低阈值
    threshold2:高阈值
    apertureSize:Sobel滤波器大小,默认为3
    L2gradient:默认为False,为L1  norm =|dI/dx|+|dI/dy|
    """
    img = cv.imread('messi5.jpg', 0)
    edges = cv.Canny(img, 100, 200)
    plt.subplot(121), plt.imshow(img, cmap = 'gray')
    plt.title('Original Image'), plt.xticks([]), plt.yticks([])
    plt.subplot(122), plt.imshow(edges, cmap = 'gray')
    plt.title('Edge Image'), plt.xticks([]), plt.yticks([])
    plt.show()
    

图像金字塔

  • 将图像创建成一系列不同分辨率的图像,如同金字塔型一样

  • 高斯金字塔(Gaussian Pyramid)

    • 通过高斯滤波器卷积,删除偶数的行和列,使得面积为原来的四分之一(倍频程Octave),这一过程为下采样
    • 如此重复下去,获得一系列的图
  • 拉普拉斯金字塔(Laplacian Pyramid)

    • 常用作图片压缩
    • 在高斯金字塔运算的过程中,图像经过向下采样会丢失部分高频细节信息,为保留这些信息,引入拉普拉斯金字塔
    • 将图像扩大为原来的4倍,新增的偶数行列以0填充,再用先前同样的高斯内核卷积,即可得到新图,此过程为上采样
    • 每一层高斯金字塔的图像减去其上一层的上采样图所得残差图,即是该层的拉普拉斯金字塔
  • 图像融合(金字塔方法)

    • 载入苹果和橘子的图片
    • 得到其各自的高斯金字塔(6层)
    • 得到其各自的拉普拉斯金字塔
    • 将每层苹果和橘子的拉普拉斯金字塔左右拼接
    • 拼接图像金字塔,重构原图
    A = cv.imread('apple.jpg')
    B = cv.imread('orange.jpg')
    # 生成A的高斯金字塔
    G = A.copy()
    gpA = [G]
    for i in range(5):
        G = cv.pyrDown(G)
        gpA.append(G)
        print(G.shape)
    print(len(gpA))
    # 生成B的高斯金字塔
    G = B.copy()
    gpB = [G]
    for i in range(5):
        G = cv.pyrDown(G)
        gpB.append(G)
        print(G.shape)
    # 生成A的拉普拉斯金字塔
    lpA = [gpA[5]]
    for i in range(5, 0, -1):
        GE = cv.pyrUp(gpA[i])
        print(gpA[i].shape, GE.shape, gpA[i-1].shape)
        L = cv.subtract(gpA[i-1], GE)
        lpA.append(L)
    # 生成B的拉普拉斯金字塔
    lpB = [gpB[5]]
    for i in range(5,0,-1):
        GE = cv.pyrUp(gpB[i])
        L = cv.subtract(gpB[i-1],GE)
        lpB.append(L)
    # 每一层的左边和右边相加
    LS = []
    for la,lb in zip(lpA,lpB):
        rows,cols,dpt = la.shape
        ls = np.hstack((la[:,0:cols/2], lb[:,cols/2:]))
        LS.append(ls)
    # 重构图片
    ls_ = LS[0]
    for i in range(1,6):
        ls_ = cv.pyrUp(ls_)
        ls_ = cv.add(ls_, LS[i])
    # 图片直接拼接
    real = np.hstack((A[:,:cols/2],B[:,cols/2:]))
    cv.imshow('pyramid_blending', ls_)
    cv.imshow('direct', real)
    

轮廓

轮廓基础
  • 获得轮廓

    • 轮廓就是将连续的点(沿着边缘)连接成线,它们具有相同颜色或强度
    • 返回轮廓和轮廓层次
    • 利于形状分析和目标识别及检测
    • 二值图像,目标为白,背景为黑
    • 轮廓检索模式
      • cv.RETR_EXTERNAL:只检索最外层轮廓,0
      • cv.RETR_LIST:检索所有轮廓,不建立任何层次关系,1
      • cv.RETR_CCOMP:检索所有轮廓,并分为两层,上层是外边界部分,下层是孔的边界(孔内的连接部分属于上层),2
      • cv.RETR_TREE:检索所有轮廓,建立完整的网状层次,3
    • 轮廓近似方法
      • cv.CHAIN_APPROX_NONE:存储所有轮廓点,像素点位置差不超过1,1
      • cv.CHAIN_APPROX_SIMPLE:压缩水平、垂直和对角段数,保留终止点,如矩形轮廓保留4个点,2
      • cv.CHAIN_APPROX_TC89_L1cv.CHAIN_APPROX_TC89_KCOS:Teh-Chin链近似算法的一种,34
    contours, hierarchy	= cv.findContours(image, mode, method[, contours[, hierarchy[, offset]]])
    """
    image:8位单通道图
    mode:轮廓检索模式
    method:轮廓近似方法
    offset:轮廓偏移量,可选
    """
    im = cv.imread('test.jpg')
    imgray = cv.cvtColor(im, cv.COLOR_BGR2GRAY)
    ret, thresh = cv.threshold(imgray, 127, 255, 0)
    contours, hierarchy = cv.findContours(thresh, cv.RETR_TREE, cv.CHAIN_APPROX_SIMPLE)
    
  • 绘制轮廓

    • 轮廓存放在列表,且用Numpy数组坐标表示
    • 可绘制外轮廓或填充的轮廓
    image =	cv.drawContours(image, contours, contourIdx, color[, thickness[, lineType[, hierarchy[, maxLevel[, offset]]]]])
    """
    image:目标图像
    contours:输入轮廓,每个轮廓由点向量表示
    contourIdx:指示所画轮廓参数,负值则绘制所有
    color:轮廓颜色
    thickness:轮廓粗细,默认1
    lineType:线条类型,默认LINE_8
    hierarchy:可选层次信息,若只想绘制一些轮廓,默认noArray()
    maxLevel:绘制轮廓的最大层次,若0,只绘制指定轮廓,若1,绘制轮廓及所有网状轮廓,若2,轮廓、所有网状轮廓及网对网轮廓等,hierarchy选用时可用
    offset:轮廓偏移量
    """
    # 绘制所有轮廓
    cv.drawContours(img, contours, -1, (0, 255, 0), 3)
    # 绘制第4层轮廓
    cv.drawContours(img, contours, 3, (0, 255, 0), 3)
    # 或
    cnt = contours[4]
    cv.drawContours(img, [cnt], 0, (0, 255, 0), 3)
    
轮廓特征
  • 矩(Moments)

    • 图像矩是图像像素强度的加权平均,可以帮助求解质心和面积等
    • 计算向量、多边形或格栅型所有三阶以下的矩值,返回所有矩值存于字典中
    retval = cv.moments(array[, binaryImage])
    """
    array:单通道8位浮点2D数组格栅图或1 x N或N x 1的2D点数组
    binaryimage:适用于图片,若为真,非0像素点当作1
    """
    img = cv.imread('star.jpg', 0)
    ret, thresh = cv.threshold(img, 127, 255, 0)
    contours, hierarchy = cv.findContours(thresh, 1, 2)
    cnt = contours[0]
    M = cv.moments(cnt)
    print(M)
    
  • 质心坐标(Centoid)

    cx = int(M['m10']/M['m00'])
    cy = int(M['m01']/M['m00'])
    
  • 轮廓面积

    retval = cv.contourArea(contour[, oriented])
    """
    contour:2D点向量
    oriented:定向面积标志,若为True,会根据轮廓方向返回正负面积值,默认为False
    """
    area = cv.contourArea(cnt)  # M['m00']也表示面积
    
  • 轮廓周长(Perimeter)

    retval = cv.arcLength(curve, closed)
    """
    curve:2D点向量
    closed:曲线是否闭合
    """
    perimeter = cv.arcLength(cnt, True)
    
  • 轮廓近似(Approximation)

    approxCurve	= cv.approxPolyDP(curve, epsilon, closed[, approxCurve])
    """
    curve:2D点向量
    epsilon:指定近似精度参数,原线与近似线的最大距离
    closed:是否闭合
    """
    epsilon = 0.1 * cv.arcLength(cnt, True)
    approx = cv.approxPolyDP(cnt, epsilon, True)
    
  • 凸包(Convex Hull)

    • 类似于轮廓近似,其主要检查线的凸缺陷,采用Sklansky算法
    hull = cv.convexHull(points[, hull[, clockwise[, returnPoints]]])
    """
    points:2D点集
    clockwise:方向标志,真为顺时针,假为逆时针,默认为假
    returnPoints:运行标志,真则返回包角坐标,假则返回对应角坐标的轮廓点索引,默认为真
    """
    hull = cv.convexHull(cnt)
    
    • 如果要找到凸缺陷,returnPoints=False
  • 检查凸性

    retval = cv.isContourConvex(contour)
    
  • 外接矩形(Bounding Rectangle)

    • 直外接矩形(Straight)
      • 返回左上坐标及宽高(x, y, w, h)
    retval = cv.boundingRect(array)
    """
    array:2D点集或灰度图
    """
    x, y, w, h = cv.boundingRect(cnt)
    cv.rectangle(img, (x, y), (x+w, y+h), (0, 255, 0), 2)
    
    • 旋转外接矩形(Rotated)
      • 最小外接矩形
      • 返回中心坐标、宽高和旋转角((x, y), (w, h), θ)
    retval = cv.minAreaRect(points)
    """
    points:2D点向量
    """
    
    points = cv.boxPoints(box[, points])
    """
    box:输入旋转矩形
    points:输出矩形四个顶点
    """
    rect = cv.minAreaRect(cnt)
    box = cv.boxPoints(rect)
    box = np.int0(box)
    cv.drawContours(img, [box], 0, (0,0,255), 2)
    
  • 最小外接圆(Minimum Enclosing Circle)

    • 返回中心和半径((x, y), r)
    center, radius = cv.minEnclosingCircle(points)
    
    (x, y), radius = cv.minEnclosingCircle(cnt)
    center = (int(x), int(y))
    radius = int(radius)
    cv.circle(img, center, radius, (0, 255, 0), 2)
    
  • 拟合椭圆(Fitting an Ellipse)

    • 返回中心,主轴次轴,旋转角((x, y), (a, b), θ)
    retval = cv.fitEllipse(points)
    
    ellipse = cv.fitEllipse(cnt)
    cv.ellipse(img, ellipse, (0, 255, 0), 2)  # 椭圆可以直接带入
    
  • 拟合线(Fitting a Line)

    • 返回共线向量及线上点坐标(vx, vy, x0, y0)
    • 距离类型
      • cv.DIST_USER:自定义距离
      • cv.DIST_L1distance = |x1-x2| + |y1-y2|
      • cv.DIST_L2:欧式距离
      • cv.DIST_Cdistance = max(|x1-x2|,|y1-y2|)
      • cv.DIST_L12L1-L2 metric: distance = 2(sqrt(1+x*x/2) - 1))
      • cv.DIST_FAIRdistance = c^2(|x|/c-log(1+|x|/c)), c = 1.3998
      • cv.DIST_WELSCHdistance = c^2/2(1-exp(-(x/c)^2)), c = 2.9846
      • cv.DIST_HUBERdistance = |x|<c ? x^2/2 : c(|x|-c/2), c=1.345
    line = cv.fitLine(points, distType, param, reps, aeps[, line])
    """
    points:2D或3D点向量
    distType:距离类型
    param:距离参数C
    reps:半径的足够精度
    aeps:角度的足够精度
    """
    rows, cols = img.shape[: 2]
    [vx, vy, x, y] = cv.fitLine(cnt, cv.DIST_L2, 0, 0.01, 0.01)
    lefty = int((-x*vy/vx) + y)
    righty = int(((cols-x)*vy/vx)+y)
    cv.line(img, (cols-1, righty), (0, lefty), (0, 255, 0), 2)
    
轮廓属性
  • 宽高比(Aspect Ratio)

    x, y, w, h = cv.boundingRect(cnt)
    aspect_ratio = float(w) / h
    
  • 延伸比(Extent)

    • 轮廓面积与外接矩形面积的比值
    area = cv.contourArea(cnt)
    x, y, w, h = cv.boundingRect(cnt)
    rect_area = w * h
    extent = float(area) / rect_area
    
  • 固性比(Solidity)

    • 轮廓面积与凸包面积的比值
    area = cv.contourArea(cnt)
    hull = cv.convexHull(cnt)
    hull_area = cv.contourArea(hull)
    solidity = float(area) / hull_area
    
  • 等量直径(Equivalent Diameter)

    • 与轮廓面积相等圆的直径
    area = cv.contourArea(cnt)
    equi_diameter = np.sqrt(4*area / np.pi)
    
  • 方向(Orientation)

    • 目标指向角度angle
    (x, y), (MA, ma), angle = cv.fitEllipse(cnt)
    
  • 掩膜和像素点

    mask = np.zeros(imgray.shape, np.uint8)
    cv.drawContours(mask, [cnt], 0, 255, -1)
    # 得到的结果是(row,column)
    pixelpoints = np.transpose(np.nonzero(mask))
    # 得到的结果是(x,y)
    pixelpoints = cv.findNonZero(mask)
    
  • 最大值,最小值及其位置

    min_val, max_val, min_loc, max_loc = cv.minMaxLoc(imgray, mask = mask)
    
  • 平均颜色或平均强度

    mean_val = cv.mean(im, mask = mask)
    
  • 极值点(Extreme Points)

    leftmost = tuple(cnt[cnt[:,:,0].argmin()][0])
    rightmost = tuple(cnt[cnt[:,:,0].argmax()][0])
    topmost = tuple(cnt[cnt[:,:,1].argmin()][0])
    bottommost = tuple(cnt[cnt[:,:,1].argmax()][0])
    
更多函数
  • 凸缺陷(Convexity Defects)

    • 返回凸缺陷数组的每一行包含[起点,终点,最远点,到最远点大约的距离],前三个是轮廓索引
    convexityDefects = cv.convexityDefects(contour, convexhull[, convexityDefects])
    """
    contour:输入轮廓
    convexhull:凸包且包含轮廓的索引
    """
    hull = cv.convexHull(cnt, returnPoints = False)
    defects = cv.convexityDefects(cnt, hull)
    for i in range(defects.shape[0]):
        s, e, f, d = defects[i, 0]
        start = tuple(cnt[s][0])
        end = tuple(cnt[e][0])
        far = tuple(cnt[f][0])
        cv.line(img, start, end, [0, 255, 0], 2)
        cv.circle(img, far, 5, [0, 0, 255], -1)
    
  • 点多边形测试(Point Polygon Test)

    • 找到图中点与轮廓的最短距离
    • 返回负数则在轮廓外,正数在内,0在轮廓上
    retval = cv.pointPolygonTest(contour, pt, measureDist)
    """
    contour:输入轮廓
    pt:测试点
    measureDist:若为真,则返回值,若为假,则用-1,0,1表示是否在多边形内
    """
    dist = cv.pointPolygonTest(cnt, (50, 50), True)
    
  • 匹配形状(Match Shapes)

    • 基于胡矩值(hu-moment)比较两个形状,返回的比较结果为浮点数,值越低,说明越匹配
    • 比较方法
      • cv.CONTOURS_MATCH_I1
      • cv.CONTOURS_MATCH_I2
      • cv.CONTOURS_MATCH_I3
    retval = cv.matchShapes(contour1, contour2, method, parameter)
    """
    contour1:轮廓或灰度图1
    contour2:轮廓或灰度图2
    method:比较方法
    parameter:指定方法参数(现在不支持了)
    """
    img1 = cv.imread('star.jpg', 0)
    img2 = cv.imread('star2.jpg', 0)
    ret, thresh = cv.threshold(img1, 127, 255, 0)
    ret, thresh2 = cv.threshold(img2, 127, 255, 0)
    contours, hierarchy = cv.findContours(thresh, 2, 1)
    cnt1 = contours[0]
    contours, hierarchy = cv.findContours(thresh2, 2, 1)
    cnt2 = contours[0]
    ret = cv.matchShapes(cnt1, cnt2, 1, 0.0)
    print(ret)
    
轮廓层次(Contours Hierarchy)
  • 概念

    • 当出现轮廓内含轮廓,通常定义外层为父轮廓,内层位子轮廓,这样将所有轮廓就联系起来,呈现了层次结构
    hierarchy = [next, previous, first_child, parent]
    
    • 没有childparent,值设为-1
  • 轮廓检索模式

    • cv.RETR_LIST
      • 检索所有轮廓,不建立任何层次关系
      • 父子平等,都在同一层
    hierarchy
    array([[[ 1, -1, -1, -1],
            [ 2,  0, -1, -1],
            [ 3,  1, -1, -1],
            [ 4,  2, -1, -1],
            [ 5,  3, -1, -1],
            [ 6,  4, -1, -1],
            [ 7,  5, -1, -1],
            [-1,  6, -1, -1]]])
    
    • cv.RETR_EXTERNAL
      • 只检索最外层轮廓,所有子轮廓丢弃
      • 只留下最老的
    hierarchy
    array([[[ 1, -1, -1, -1],
            [ 2,  0, -1, -1],
            [-1,  1, -1, -1]]])
    
    • cv.RETR_CCOMP
      • 检索所有轮廓,并分为两层,上层是外边界部分,下层是孔的边界内
      • 如果孔内还有轮廓,则其轮廓又为上层,轮廓的孔又为下层
      • 只有父子
    hierarchy
    array([[[ 3, -1,  1, -1],
            [ 2, -1, -1,  0],
            [-1,  1, -1,  0],
            [ 5,  0,  4, -1],
            [-1, -1, -1,  3],
            [ 7,  3,  6, -1],
            [-1, -1, -1,  5],
            [ 8,  5, -1, -1],
            [-1,  7, -1, -1]]])
    
    • cv.RETR_TREE
      • 检索所有轮廓,建立完整的网状层次
      • 全家老小
    hierarchy
    array([[[ 7, -1,  1, -1],
            [-1, -1,  2,  0],
            [-1, -1,  3,  1],
            [-1, -1,  4,  2],
            [-1, -1,  5,  3],
            [ 6, -1, -1,  4],
            [-1,  5, -1,  4],
            [ 8,  0, -1, -1],
            [-1,  7, -1, -1]]])
    

直方图

绘制直方图
  • 概念

    • 图像的强度分布,X轴为0~255像素值,Y轴为相应的像素数量
    • BINS:小段的像素值(如0~7)直方图称为BINBINS数即为32
    • DIMS:收集数据参数的个数,只收集强度值,故为1
    • RANGE:强度值范围,通常为[0, 256]
  • 计算直方图

    • OpenCV
      • 得到256 x 1的数组
    hist = cv.calcHist(images, channels, mask, histSize, ranges[, hist[, accumulate]])
    """
    images:8位或32位浮点图像,放列表中
    channels:指定计算的通道,放列表中,灰度图为[0],彩图分别指定
    mask:掩膜,通常为None
    histSize:BINS数,全范围为[256]
    ranges:强度值范围,通常[0, 256]
    """
    img = cv.imread('home.jpg', 0)
    hist = cv.calcHist([img], [0], None, [256], [0,256])
    
    • Numpy
      • 返回得到hist值是与上面一样的,但是1维数组
      • BINS值个数为257,因为Numpy计算0~0.99,..., 255~255.99, 256,有个单独的256
    hist, bin_edges = np.histogram(a, bins=10, range=None, normed=None, weights=None, density=None)
    """
    a:输入数据
    bins:BINS数,默认为10
    range:BINS的范围
    normed:丢弃不用
    weights:权重,和a的形状一样
    density:假则结果为样本数,真则是密度概率
    """
    hist, bins = np.histogram(img.ravel(), 256, [0, 256])
    
    hist = np.bincount(x, weights=None, minlength=0)
    """
    x:1维数组
    weights:权重,和a的形状一样
    minlength:BINS最小值
    """
    hist = np.bincount(img.ravel(), minlength=256)
    
    • OpenCV的cv.calcHist()速度最快,其次为np.bincount()
  • 绘制直方图

    # 灰度
    img = cv.imread('home.jpg', 0)
    plt.hist(img.ravel(), 256, [0, 256]); plt.show()
    
    # 彩色
    img = cv.imread('home.jpg')
    color = ('b', 'g', 'r')
    for i, col in enumerate(color):
        histr = cv.calcHist([img], [i], None, [256], [0, 256])
        plt.plot(histr, color = col)
        plt.xlim([0, 256])
    plt.show()
    
  • 使用掩膜

    img = cv.imread('home.jpg',0)
    # 创建掩膜
    mask = np.zeros(img.shape[:2], np.uint8)
    mask[100: 300, 100: 400] = 255
    masked_img = cv.bitwise_and(img, img, mask = mask)
    # 计算直方图
    hist_full = cv.calcHist([img], [0], None, [256], [0, 256])
    hist_mask = cv.calcHist([img], [0], mask, [256], [0, 256])
    plt.subplot(221), plt.imshow(img, 'gray')
    plt.subplot(222), plt.imshow(mask, 'gray')
    plt.subplot(223), plt.imshow(masked_img, 'gray')
    plt.subplot(224), plt.plot(hist_full), plt.plot(hist_mask)
    plt.xlim([0,256])
    plt.show()
    
直方图均衡化
  • 概念

    img = cv.imread('wiki.jpg', 0)
    hist,bins = np.histogram(img.flatten(), 256, [0, 256])
    cdf = hist.cumsum()
    cdf_normalized = cdf * float(hist.max()) / cdf.max()
    plt.plot(cdf_normalized, color = 'b')
    plt.hist(img.flatten(), 256, [0, 256], color = 'r')
    plt.xlim([0, 256])
    plt.legend(('cdf', 'histogram'), loc = 'upper left')
    plt.show()
    
    # 掩膜数组,所有的操作只包含非掩膜的元素
    cdf_m = np.ma.masked_equal(cdf, 0)
    cdf_m = (cdf_m - cdf_m.min())*255/(cdf_m.max()-cdf_m.min())
    cdf = np.ma.filled(cdf_m, 0).astype('uint8')
    # 每一个像素值通过cdf映射
    img2 = cdf[img]
    
    • OpenCV实现
    dst	= cv.equalizeHist(src[, dst])
    """
    src:8位单通道图
    """
    img = cv.imread('wiki.jpg', 0)
    equ = cv.equalizeHist(img)
    res = np.hstack((img, equ))  # 横向放一起
    cv.imwrite('res.png', res)
    
  • 对比度限制自适应直方图均衡化(Contrast Limited Adaptive Histogram Equalization)

    • 全局直方图均衡化可能因为过亮的缘故,导致某些信息丢失
    • 采用自适应直方图均衡化,将图片分成小块(称为瓷片tiles),默认8 x 8,分别进行直方图均衡化,这样直方图被限制在一个小范围(如果有噪声,噪声将被放大)
    • 为了避免噪声放大,对比限制起了作用,如果任一直方图箱(bin)超过了指定对比限制(默认为40),在直方图均衡化之前便将这些像素剪切并均匀分布到其他箱体上
    • 均衡化后,使用双线性插值拼接瓷片边界
    img = cv.imread('tsukuba_l.png', 0)
    # 创建CLAHE对象(参数可选)
    clahe = cv.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8))
    cl1 = clahe.apply(img)
    cv.imwrite('clahe_2.jpg',cl1)
    
2D直方图
  • 简介

    • 灰度图只需要考虑一个特征,用1维的直方图,若要考虑2个特征,则需要使用彩色直方图,通常考虑色相(Hue)和饱和度(Saturation)
  • 在OpenCV中的2D直方图

    • channels = [0, 1]:色相和饱和度
    • bins = [180, 256]:色相划分180,饱和度256
    • range = [0, 180, 0, 256]:色相范围0~180,饱和度0~256
    img = cv.imread('home.jpg')
    hsv = cv.cvtColor(img,cv.COLOR_BGR2HSV)
    hist = cv.calcHist([hsv], [0, 1], None, [180, 256], [0, 180, 0, 256])
    
  • 在Numpy中的2D直方图

    • 与1维相似,xedgesyedges会多一个
    h, xedges, yedges = np.histogram2d(x, y, bins=10, range=None, normed=None, weights=None, density=None)
    """
    x, y:数组形状相同
    bins:相同的数或用列表分开表示,[int, int]
    range:范围,形状(2,2),如[[xmin, xmax], [ymin, ymax]]
    """
    hsv = cv.cvtColor(img, cv.COLOR_BGR2HSV)
    h, s, v = cv.split(hsv)
    hist, xbins, ybins = np.histogram2d(h.ravel(), s.ravel(), [180, 256], [[0, 180], [0, 256]])
    
  • 绘制2D直方图

    • cv.imshow()

      • 得到一个灰度图
    • 用Matplotlib

      • X轴是饱和度,Y轴是色相
      img = cv.imread('home.jpg')
      hsv = cv.cvtColor(img, cv.COLOR_BGR2HSV)
      hist = cv.calcHist( [hsv], [0, 1], None, [180, 256], [0, 180, 0, 256] )
      plt.imshow(hist,interpolation = 'nearest')
      plt.show()
      
直方图反向投影(Histogram Backprojection)
  • 理论

    • 用于图片分割或ROI内找目标
    • 创建一个与原图同样大小,且每个像素对应目标像素的概率的图,即输出图的目标处相较于原图部分更白
    • 用连续自适应均值漂移算法(camshift)
  • 实现

    • 创建彩色直方图,包含目标,且目标尽可能的大
    • 计算每个像素属于目标的概率
    • 选择合适阈值展示目标
  • Numpy算法

    • 计算目标和需要搜索图的彩色直方图,MI
    # roi目标或目标区域
    roi = cv.imread('rose_red.png')
    hsv = cv.cvtColor(roi, cv.COLOR_BGR2HSV)
    # 需要搜索目标所在的图
    target = cv.imread('rose.png')
    hsvt = cv.cvtColor(target,cv.COLOR_BGR2HSV)
    # 计算直方图
    M = cv.calcHist([hsv],[0, 1], None, [180, 256], [0, 180, 0, 256] )
    I = cv.calcHist([hsvt],[0, 1], None, [180, 256], [0, 180, 0, 256] )
    
    • 计算比值R = M / I,然后反向投影R,把R当作调色板,创建每个像素对应成为目标的概率的图,B(x, y) = R[h(x, y), s(x, y)],接着B(x, y) = min[B(x, y), 1]
    h, s, v = cv.split(hsvt)
    R = M / I
    B = R[h.ravel(), s.ravel()]
    B = np.minimum(B, 1)
    B = B.reshape(hsvt.shape[: 2])
    
    • 用圆盘D卷积,B = D * B
    disc = cv.getStructuringElement(cv.MORPH_ELLIPSE, (5, 5))
    cv.filter2D(B, -1, disc, B)
    B = np.uint8(B)
    cv.normalize(B, B, 0, 255, cv.NORM_MINMAX)
    
    • 合适阈值处理找到最佳结果
    ret, thresh = cv.threshold(B, 50, 255, 0)
    
  • OpenCV实现反向投影

    • 目标直方图需要标准化后传给反向投射函数,函数返回概率图像,需要圆盘卷积和阈值处理
    roi = cv.imread('rose_red.png')
    hsv = cv.cvtColor(roi, cv.COLOR_BGR2HSV)
    target = cv.imread('rose.png')
    hsvt = cv.cvtColor(target,cv.COLOR_BGR2HSV)
    # 计算目标直方图
    roihist = cv.calcHist([hsv], [0, 1], None, [180, 256], [0, 180, 0, 256] )
    # 标准化直方图并反向投影
    cv.normalize(roihist, roihist, 0, 255, cv.NORM_MINMAX)
    dst = cv.calcBackProject([hsvt], [0, 1], roihist,[0, 180, 0, 256], 1)
    # 卷积
    disc = cv.getStructuringElement(cv.MORPH_ELLIPSE, (5, 5))
    cv.filter2D(dst, -1, disc, dst)
    # 阈值处理
    ret, thresh = cv.threshold(dst, 50, 255, 0)
    thresh = cv.merge((thresh, thresh, thresh))
    res = cv.bitwise_and(target, thresh)
    res = np.vstack((target, thresh, res))
    cv.imwrite('res.jpg',res)
    

图像变换

  • 傅里叶变换(Fourier Transform)

    • 理论
      • 傅里叶变换用于不同滤波器的频率特征分析
      • 图像中,2D离散傅里叶变换(Discrete Fourier Transform,DFT)用来寻找频域,快速傅里叶变换用来计算离散傅里叶变换
      • 把图像当作信号沿两个方向采样,对X和Y方向进行傅里叶变换,得到频谱图
      • 对于正弦信号,如果振幅短时间内变化过快,可以称其为高频信号,如果很慢,则是低频信号,引申到图像上,边缘点或噪声则为高频分量,如果振幅没什么变化,则为低频分量
    • Numpy中的傅里叶变换
      • 傅里叶变换,然后0频分量移动至中心,得到幅度图
      • 用掩膜移除低频,反向移动原0频分量的位置,然后逆傅里叶变换,得到变换图像
      • 结果显示了高通滤波是一个边缘检测的过程,这也表示大多数图像数据是在频谱的低频部分
      • 振铃效应是一种出现在信号快速转换时,附加在转换边缘上导致失真的信号。而在图像或影像上,振铃效应会导致出现在边缘附近的环带或像是"鬼影"的环状伪影
    • 矩形掩膜导致振铃效,出现结构状波纹,所以矩形窗口不用作滤波,最好用高斯窗口
    out = np.fft.fft2(a, s=None, axes=(-2, -1), norm=None)
    """
    a:输入数组
    s:输出形状(变换轴的长度),若小于输入,则输入数组会被剪裁,若大于,则零填充输入,不填则一样
    axes:用于计算FFT的轴,默认使用最后两个轴,轴上的重复索引表示多次执行该轴上的变换,单元素序列意味着执行一维FFT
    norm:标准化模式,默认None
    """
    
    y = np.fft.fftshift(x, axes=None)
    """
    将0频(DC)分量移动到谱中心
    x:输入数组
    axes:移动轴,默认移动所有
    """
    
    img = cv.imread('messi5.jpg', 0)
    f = np.fft.fft2(img)
    fshift = np.fft.fftshift(f)
    magnitude_spectrum = 20*np.log(np.abs(fshift))
    plt.subplot(121), plt.imshow(img, cmap = 'gray')
    plt.title('Input Image'), plt.xticks([]), plt.yticks([])
    plt.subplot(122), plt.imshow(magnitude_spectrum, cmap = 'gray')
    plt.title('Magnitude Spectrum'), plt.xticks([]), plt.yticks([])
    plt.show()
    
    rows, cols = img.shape
    crow, ccol = rows//2 , cols//2
    fshift[crow-30:crow+31, ccol-30:ccol+31] = 0
    # 0频分量移动的逆运算
    f_ishift = np.fft.ifftshift(fshift)
    # 傅里叶变换的逆运算
    img_back = np.fft.ifft2(f_ishift)
    # 返回实数部分
    img_back = np.real(img_back)
    plt.subplot(131), plt.imshow(img, cmap = 'gray')
    plt.title('Input Image'), plt.xticks([]), plt.yticks([])
    plt.subplot(132), plt.imshow(img_back, cmap = 'gray')
    plt.title('Image after HPF'), plt.xticks([]), plt.yticks([])
    plt.subplot(133), plt.imshow(img_back)
    plt.title('Result in JET'), plt.xticks([]), plt.yticks([])
    plt.show()
    
  • Opencv中的傅里叶变换

    • 变换结果与上述相似,返回结果有2个通道,一个是实部,一个是虚部
    • 输入图像应转换为np.float32
    • 变换标志
      • cv.DFT_INVERSE:执行逆1D或2D变换而不是默认的正向变换
      • cv.DFT_SCALE:缩放结果:除以数组元素的数量。通常,它与DFT_INVERSE结合使用
      • cv.DFT_ROWS:执行输入矩阵的每一行的正向或反向变换; 此标志使您可以同时转换多个向量,并可用于减少开销(有时是处理本身的几倍)来执行3D和更高维度的转换等
      • cv.DFT_COMPLEX_OUTPUT:执行1D或2D实数组的正向变换; 结果虽然是一个复数组,但具有复共轭对称性(CCS),这样的数组可以打包成与输入大小相同的实数组,这是最快的选择,这是默认情况下的功能; 若希望获得完整的复数组(用于更简单的频谱分析等),传递标志以使该函数能够生成全尺寸复数输出数组
      • cv.DFT_REAL_OUTPUT:执行1D或2D复数阵列的逆变换; 结果通常是相同大小的复数组,但是,如果输入数组具有共轭 - 复数对称性(例如,它是使用DFT_COMPLEX_OUTPUT标志进行正向变换的结果),则输出是实数数组; 虽然函数本身不检查输入是否对称,但是你可以传递标志,然后函数将采用对称并产生实际输出数组(注意当输入被打包成实数组并且逆变换是执行后,该函数将输入视为打包的复共轭对称数组,输出也将是一个实数组
    • cv.DFT_COMPLEX_INPUT:指定输入是复数输入。如果设置了此标志,则输入必须具有2个通道。另一方面,出于向后兼容性的原因,如果输入有2个通道,则输入已被认为是复数
      • cv.DCT_INVERSE:执行逆1D或2D变换而不是默认的正向变换
      • cv.DCT_ROWS:执行输入矩阵的每一行的正向或反向变换。此标志使您可以同时转换多个向量,并可用于减少开销(有时比处理本身大几倍)以执行3D和更高维度的转换等
    • 反向变换前,创建一个低通滤波器过滤高频,如用掩膜模糊图像
    dst	= cv.dft(src[, dst[, flags[, nonzeroRows]]])
    """
    src:输入数组,实数虚数都行
    flags:变换标志
    nonzeroRows:当此参数不为0,函数假设输入数组的前参数行(DFT_INVERSE未设置)或输出数组(DFT_INVERSE设置)的前参数行包含非0数,因此,函数能高效处理剩余行,节省时间;此技术对计算数组关联性或用DFT卷积很有用
    """
    img = cv.imread('messi5.jpg', 0)
    dft = cv.dft(np.float32(img), flags = cv.DFT_COMPLEX_OUTPUT)
    dft_shift = np.fft.fftshift(dft)
    # magnitude, angle = cv.cartToPolar(x, y)也可以用于计算
    magnitude_spectrum = 20*np.log(cv.magnitude(dft_shift[:,:, 0], dft_shift[:, :, 1]))
    plt.subplot(121), plt.imshow(img, cmap = 'gray')
    plt.title('Input Image'), plt.xticks([]), plt.yticks([])
    plt.subplot(122), plt.imshow(magnitude_spectrum, cmap = 'gray')
    plt.title('Magnitude Spectrum'), plt.xticks([]), plt.yticks([])
    plt.show()
    
    rows, cols = img.shape
    crow, ccol = rows//2, cols//2
    # 创建掩膜,中心为1
    mask = np.zeros((rows, cols, 2), np.uint8)
    mask[crow-30:crow+30, ccol-30:ccol+30] = 1
    # 应用掩膜,逆傅里叶变换
    fshift = dft_shift*mask
    f_ishift = np.fft.ifftshift(fshift)
    img_back = cv.idft(f_ishift)
    img_back = cv.magnitude(img_back[:, :, 0], img_back[:, :, 1])
    plt.subplot(121), plt.imshow(img, cmap = 'gray')
    plt.title('Input Image'), plt.xticks([]), plt.yticks([])
    plt.subplot(122), plt.imshow(img_back, cmap = 'gray')
    plt.title('Magnitude Spectrum'), plt.xticks([]), plt.yticks([])
    plt.show()
    
    
  • DFT的优化操作

    • OpenCV的cv.dft()cv.idft()要比Numpy函数快
    • 数组的尺寸最好是二次方或能被235整除的数,若担心计算效率,可以零填充数组
      • OpenCV需要手动添加,Numpy指定s参数会自动填充
    img = cv.imread('messi5.jpg', 0)
    rows,cols = img.shape
    print("{} {}".format(rows, cols))  # 342 548
    nrows = cv.getOptimalDFTSize(rows)
    ncols = cv.getOptimalDFTSize(cols)
    print("{} {}".format(nrows, ncols))  # 360 576
    # 方法1
    nimg = np.zeros((nrows, ncols))
    nimg[: rows, : cols] = img
    # 方法2
    right = ncols - cols
    bottom = nrows - rows
    bordertype = cv.BORDER_CONSTANT
    nimg = cv.copyMakeBorder(img, 0, bottom, 0, right, bordertype, value = 0)
    
  • 拉普拉斯是高通滤波器的原因

    # 没有缩放系数的简单平均滤波器
    mean_filter = np.ones((3,3))
    # 高斯滤波器
    x = cv.getGaussianKernel(5, 10)
    gaussian = x*x.T
    # 不同的边缘检测滤波器
    # X方向的scharr
    scharr = np.array([[-3, 0, 3],
                       [-10,0,10],
                       [-3, 0, 3]])
    # X方向的sobel 
    sobel_x= np.array([[-1, 0, 1],
                       [-2, 0, 2],
                       [-1, 0, 1]])
    # Y方向的sobel
    sobel_y= np.array([[-1,-2,-1],
                       [0, 0, 0],
                       [1, 2, 1]])
    # 拉普拉斯
    laplacian=np.array([[0, 1, 0],
                        [1,-4, 1],
                        [0, 1, 0]])
    filters = [mean_filter, gaussian, laplacian, sobel_x, sobel_y, scharr]
    filter_name = ['mean_filter', 'gaussian', 'laplacian', 'sobel_x', 'sobel_y', 'scharr_x']
    fft_filters = [np.fft.fft2(x) for x in filters]
    fft_shift = [np.fft.fftshift(y) for y in fft_filters]
    mag_spectrum = [np.log(np.abs(z)+1) for z in fft_shift]
    for i in range(6):
        plt.subplot(2, 3, i+1), plt.imshow(mag_spectrum[i], cmap = 'gray')
        plt.title(filter_name[i]), plt.xticks([]), plt.yticks([])
    plt.show()
    

模板匹配(Template Matching)

  • 理论

    • 模板匹配是一种在大图中搜索和定位模板的方法
    • 将模板在输入图上滑动(像2D卷积一样),在模板图上比较模板和部分输入图
    • 返回一个灰度图,每个像素表示周围像素与模板的匹配程度
    • 输入图尺寸(W, H),模板尺寸(w, h),输出图尺寸(W-w+1, H-h+1)
    • 将得到的结果通过cv.minMaxLoc()找到最大/最小值,然后就可以矩形标注出模板位置
  • OpenCV中的模板匹配

    • 模板匹配方法
      • cv.TM_CCOEFF:取最大值
      • cv.TM_CCOEFF_NORMED:取最大值
      • cv.TM_CCORR:取最大值
      • cv.TM_CCORR_NORMED:取最大值
      • cv.TM_SQDIFF:取最小值
      • cv.TM_SQDIFF_NORMED:取最小值
    result = cv.matchTemplate(image, templ, method[, result[, mask]])
    """
    image:大图,必须为8位或32位浮点
    templ:模板图,不能大于大图且数据类型一致
    method:模板匹配方法
    mask:模板掩膜,和模板尺寸一致,数据类型一致,默认不设置
    """
    img = cv.imread('meissi5.jpg', 0)
    img2 = img.copy()
    template = cv.imread('template.jpg', 0)
    w, h = template.shape[::-1]
    # 6种模板匹配方法
    methods = ['cv.TM_CCOEFF', 'cv.TM_CCOEFF_NORMED', 'cv.TM_CCORR', 'cv.TM_CCORR_NORMED', 'cv.TM_SQDIFF', 'cv.TM_SQDIFF_NORMED']
    for meth in methods:
        img = img2.copy()
        method = eval(meth)
        # 应用模板匹配
        res = cv.matchTemplate(img, template, method)
        min_val, max_val, min_loc, max_loc = cv.minMaxLoc(res)
        # 如果方法为TM_SQDIFF或TM_SQDIFF_NORMED,找最小值
        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()
    
  • 多目标模板匹配

    • 使用阈值
    img_rgb = cv.imread('mario.png')
    img_gray = cv.cvtColor(img_rgb, cv.COLOR_BGR2GRAY)
    template = cv.imread('mario_coin.png', 0)
    w, h = template.shape[::-1]
    res = cv.matchTemplate(img_gray, template, cv.TM_CCOEFF_NORMED)
    threshold = 0.8
    loc = np.where(res >= threshold)
    for pt in zip(*loc[::-1]):
        cv.rectangle(img_rgb, pt, (pt[0] + w, pt[1] + h), (0, 0, 255), 2)
    cv.imwrite('res.png',img_rgb)
    

霍夫线变换(Hough Line Transform)

  • 理论

    • 如果某一形状可以用数学表达,则霍夫变换对检测这一形状很有用,即使这个形状有点破损或失真
    • ρ=xcosθ+ysinθ表示一条直线,ρ表示直接到原点的垂直距离,θ为垂线和水平轴所形成的夹角,夹角不超过180度,超过90度时,ρ为负值
    • 任意线可以用(ρ, θ)表示,用它创建一个初始化为0的2D数组或累加器(可包括2个参数值),让行表示ρ,列表示θ,数组的尺寸根据自己需求精度而定,角度精度为1度,则有180列,取一个像素精度,则图像对角线长度为行数
    • 比如,图像有一条线,则取线上点(x, y),对于点可得180组(ρ, θ),在对应累加器位置加值(投票),再取线上第二点,对应累加,依次取点,对号累加,最后累加器上(ρ1, θ1)的值最大,则图像存在一条距离原点距离为ρ1角度为θ1的线
  • OpenCV中的霍夫变换

    • 返回(ρ, θ)值或(ρ, θ, vote)
    lines =	cv.HoughLines(image, rho, theta, threshold[, lines[, srn[, stn[, min_theta[, max_theta]]]]])
    """
    image:8位单通道二值图,函数可能改变图片
    rho:累加器的距离分辨率,单位像素
    theta:累加器的角度分辨率,单位弧度
    threshold:累加器阈值参数,得到足够投票的线会返回
    srn:对于多形状霍夫变换,它是距离分辨率的除数因子,基础的累加器,距离分辨率是rho,精确累加器的分辨率是rho/srn,如果srn和stn都为0,就是经典霍夫变换,否则他们的值都为正数
    stn:对于多形状霍夫变换,它是角度分辨率的除数因子
    min_theta:最小角度,必须0~max_theta之间
    max_theta:最大角度,必须min_theta~CV_PI之间
    """
    img = cv.imread(cv.samples.findFile('sudoku.png'))
    gray = cv.cvtColor(img, cv.COLOR_BGR2GRAY)
    edges = cv.Canny(gray, 50, 150, apertureSize = 3)
    lines = cv.HoughLines(edges, 1, np.pi/180, 200)
    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, 0, 255), 2)
    cv.imwrite('houghlines3.jpg',img)
    
  • 概率霍夫变换(Probabilistic Hough Transform)

    • 霍夫变换的优化,不考虑所有点,只考虑能充分检测线的随机点子集
    • 基于线的鲁棒检测,参数与经典霍夫变换差不多,返回线的端点
    lines =	cv.HoughLinesP(image, rho, theta, threshold[, lines[, minLineLength[, maxLineGap]]])
    """
    minLineLength:最小线长,线短于其值被舍弃
    maxLineGap:同一线上点的最大允许间隙
    """
    img = cv.imread(cv.samples.findFile('sudoku.png'))
    gray = cv.cvtColor(img, cv.COLOR_BGR2GRAY)
    edges = cv.Canny(gray, 50, 150, apertureSize = 3)
    lines = cv.HoughLinesP(edges, 1, np.pi/180, 100, minLineLength=100, maxLineGap=10)
    for line in lines:
        x1, y1, x2, y2 = line[0]
        cv.line(img, (x1, y1), (x2, y2), (0, 255, 0), 2)
    cv.imwrite('houghlines5.jpg', img)
    

霍夫圆变换

  • 理论

    • (x−xcenter)^2+(y−ycenter)^2=r^2,圆心(xcenter, ycenter)和半径r3个参数,需要用3D累加器来进行霍夫变换,这很低效,所以采取霍夫梯度方法来提高效率
    • 返回圆向量,(x, y, radius)(x, y, radius, votes)
    circles	= cv.HoughCircles(image, method, dp, minDist[, circles[, param1[, param2[, minRadius[, maxRadius]]]]])
    """
    image:8位单通道灰度图
    method:检测方法,目前只有cv.HOUGH_GRADIENT应用
    dp:图像分辨率与累加器分辨率的比值,如果dp=2,累加器高宽是图像的一半
    minDist:检测圆中心的最短距离,太小容易错误检测,太大容易遗漏
    param1:指定方法参数1,对于HOUGH_GRADIENT,是Canny边缘检测器的较大阈值
    param2:指定方法参数2,对于HOUGH_GRADIENT,是圆心检测累加器阈值
    minRadius:最小圆半径,默认0
    maxRadius:最大圆半径,若小于等于0,用最大图片尺寸,若小于0,返回的圆心不带半径
    """
    img = cv.imread('opencv-logo-white.png', 0)
    img = cv.medianBlur(img, 5)
    cimg = cv.cvtColor(img, cv.COLOR_GRAY2BGR)
    circles = cv.HoughCircles(img, cv.HOUGH_GRADIENT, 1, 20, param1=50, param2=30, minRadius=0, maxRadius=0)
    circles = np.uint16(np.around(circles))
    for i in circles[0,:]:
        # 画外接圆
        cv.circle(cimg,(i[0],i[1]),i[2],(0,255,0),2)
        # 画圆心
        cv.circle(cimg,(i[0],i[1]),2,(0,0,255),3)
    cv.imshow('detected circles', cimg)
    cv.waitKey(0)
    cv.destroyAllWindows()
    

分水岭算法分割图像

  • 理论

    • 任意的灰度图可以看作是高强度的山峰和低强度的低谷组成的地形国
    • 用不同颜色的水(标签)填充低谷(局部极小值)
    • 在不同的峰顶(梯度)周围的水涨时,不同山谷的水开始融合
    • 为了避免这种融合,在水融合处创建一些屏障,这些屏障就保证了分割的结果
    • 更多请见,分水岭介绍-CMM
    • OpenCV提供基于标记的分水岭算法,可自定义需要融合的低谷点,将前景或目标区域用一种颜色打上标签,将背景打上另一种颜色的标签,不确定的区域打上0标签,最后应用分水岭算法,目标边界将打上-1标签
  • 实现

    • 距离变换掩膜尺寸
      • cv.DIST_MASK_3mask=3
      • cv.DIST_MASK_5mask=5
      • cv.DIST_MASK_PRECISE:此处不可用
    dst	= cv.distanceTransform(src, distanceType, maskSize[, dst[, dstType]])
    """
    计算每点到最近0像素点的距离
    src:8位单通道图
    distanceType:距离类型
    maskSize:距离变换掩膜尺寸
    """
    
    retval, labels = cv.connectedComponents(image[, labels[, connectivity[, ltype]]])
    """
    计算布尔图中标签过连接部分的图
    image:8位单通道图
    connectivity:8或4连通域,默认8
    ltype:输出图标签类型,目前支持CV_32S和CV_16U,默认为CV_32S
    """
    
    markers	= cv.watershed(image, markers)
    """
    image:8位三通道
    markers:输入/输出32位单通道图(映射),与image尺寸一致
    """
    img = cv.imread('coins.png')
    gray = cv.cvtColor(img, cv.COLOR_BGR2GRAY)
    ret, thresh = cv.threshold(gray, 0, 255, cv.THRESH_BINARY_INV+cv.THRESH_OTSU)
    # 去除噪声
    kernel = np.ones((3, 3), np.uint8)
    opening = cv.morphologyEx(thresh, cv.MORPH_OPEN, kernel, iterations = 2)
    # 确定背景区域
    sure_bg = cv.dilate(opening, kernel, iterations=3)
    # 找到前景区域
    dist_transform = cv.distanceTransform(opening, cv.DIST_L2, 5)
    ret, sure_fg = cv.threshold(dist_transform, 0.7*dist_transform.max(), 255, 0)
    # 找到未知区域
    sure_fg = np.uint8(sure_fg)
    unknown = cv.subtract(sure_bg, sure_fg)
    # 标记标签
    ret, markers = cv.connectedComponents(sure_fg)
    # 标签加1,背景是1
    markers = markers + 1
    # 未知区域标签为0
    markers[unknown==255] = 0
    # 分水岭算法
    markers = cv.watershed(img, markers)
    img[markers == -1] = [255, 0, 0]
    

GrabCut算法提取交互式前景

  • 理论

    • "GrabCut"算法解析参考
    • 用矩形框住前景,并画线标注一些分割错误的区域,白线表示前景,黑线表示背景
    • 计算机会提供的数据初始化一些标签
    • 对前景和背景使用高斯混合模型(GMM)
    • GMM学习并创建新的像素分布,即不确定像素会标签为可能前景或可能背景,这根据其颜色统计与其他难标签像素的相关性进行标签(和聚类相似)
    • 根据像素分布构建图,图中的节点就是像素,增加源节点(Source node)和汇聚节点(Sink node),每个前景像素连接源节点,背景像素连接汇聚节点
    • 通过像素成为前景或背景的概率定义连接像素和源节点/汇聚节点的边的权重,像素间的权重通过边的信息或像素相似性定义,如果像素颜色差异大,则像素间的权重小
    • 然后用最小切割算法(mincut)分割图,用最小损失函数分成源节点和汇聚节点两部分,分割完后,与源节点相连的像素成为前景,与汇聚节点相连的成为背景
    • 直到分类收敛,过程终止
  • 应用

    • GrabCut级别
      • cv.GC_BGD:一个明显背景像素
      • cv.GC_FGD:一个明显前景(目标)像素
      • cv.GC_PR_BGD:一个可能背景像素
      • cv.GC_PR_FGD:一个可能前景像素
    • GrabCut模式
      • cv.GC_INIT_WITH_RECT:利用提供的矩形初始化状态和掩膜
      • cv.GC_INIT_WITH_MASK:利用提供的掩膜初始化状态,可与GC_INIT_WITH_RECT联用,所有外部区域为明显背景
      • cv.GC_EVAL:算法需要重新开始
      • cv.GC_EVAL_FREEZE_MODEL:算法用固定模式运行一次
    mask, bgdModel, fgdModel = cv.grabCut(img, mask, rect, bgdModel, fgdModel, iterCount[, mode])
    """
    img:8位3通道图
    mask:输入/输出8位单通道掩膜,当模式为GC_INIT_WITH_RECT,函数初始化掩膜,它的元素可能为GrabCut级别中的一种
    rect:感兴趣区域包含分割目标,感兴趣区域外像素标记为明显背景,当模式为GC_INIT_WITH_RECT时可用
    bgdModel:背景模式的临时数组,处理同一幅图时别修改
    fgdModel:前景模式的临时数组,处理同一幅图时别修改
    iterCount:返回结果前算法的迭代次数
    mode:运行模式,可为GrabCut模式中的一种
    """
    img = cv.imread('messi5.jpg')
    mask = np.zeros(img.shape[:2], np.uint8)
    bgdModel = np.zeros((1, 65), np.float64)
    fgdModel = np.zeros((1, 65), np.float64)
    rect = (50, 50, 450, 290)
    cv.grabCut(img, mask, rect, bgdModel, fgdModel, 5, cv.GC_INIT_WITH_RECT)
    mask2 = np.where((mask==2)|(mask==0), 0, 1).astype('uint8')
    # newaxis相当于增加一个维度
    img = img*mask2[:, :, np.newaxis]
    plt.imshow(img), plt.colorbar(), plt.show()
    

未完待续

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值