当前位置:   article > 正文

OpenCV简单入门学习_简单opencv大妈

简单opencv大妈

OpenCV入门学习

文章部分摘自 OpenCV入门学习笔记之常用的图像处理操作
文章部分来源 OpenCV从入门到实战

引入opencv

import cv2 as cv
  • 1

数据读取-图像

  • cv2.IMREAD_COLOR:彩色图像
  • cv2.IMREAD_GRAYSCALE:灰度图像
src = cv.imread("1.jpg",cv2.IMREAD_COLOR) #括号里是照片地址
print(src)
# 返回np.array的结构,这些矩阵标示着BGR的值
'''
[[[198 195 187]
  [198 195 187]
  [198 195 187]
  ...
  [177 168 159]
  [177 168 159]
  [177 168 159]]
 [[198 195 187]
  [198 195 187]
  [198 195 187]
'''
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

opencv读取的格式是BGR,而不是RGB

# 图像的显示,也可以创建多个窗口
cv.imshow("input image", src)
# 等待时间,毫秒级,0表示任意键终止,如果设置未其他数,等待对应时间后图片自动关闭
cv.waitKey(0)
cv.destroyAllWindows()
  • 1
  • 2
  • 3
  • 4
  • 5

我们可以将展示图片的方法写成一个函数使用

def cv_show(name,img):
    cv.imshow(name, img)
    cv.waitKey(0)
    cv.destroyAllWindows()
  • 1
  • 2
  • 3
  • 4
  • 可以使用.shape方法,获取打开后的图片的形状

得到的是三个数,分别代表着 h w c , 其中c指的是RGB,但顺序为BGR
当我们在imread中使用了读取类型,那灰度图就没有c了

  • 保存图片cv.imwrite('name',src)
  • 获取src类型type(src)
  • 获取像素点个数.size
  • 获取数据类型.dtype

数据读取-视频

  • cv2.videoCapture可以捕获摄像头,用数字来控制不同的设备,例如0.1
  • 如果是视频文件,直接指定好路径即可。

对视频的操作实际上就是将视频拆分成每一帧

vc = cv.VideoCapture('test.mp4')

# 检查是否打开正确
if vc.isOpened():
    open, frame = vc.read() # 布尔值,当前帧的图像
else:
    open = False
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
while open:
    ret, frame = vc.read()
    # 判断当前帧是否存在
    if frame is None:
        break
    if ret == True
        gray = cv.cvtColor(frame,cv2.COLOR_BGR2GRAY)  # 转化成灰度图
        cv2.imshow('result',gray)
        if cv.waitKey(10) & 0xFF == 27: # 这里相当于按了esc
            break
vc.release()
cv.destroyAllWindows()
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

截取部分图像数据

img = cv.imread('xx')
cat = img[0:200,0:200]
cv_show('cat',cat)
  • 1
  • 2
  • 3

颜色通道提取

# 提取各颜色值
b,g,r = cv.split(img)
# 获取形状
b.shape
# 各颜色组合
img = cv.merge((b,g,r))
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

只保留R(B,G)

cur_img = img.copy() # 克隆图片
# 根据颜色顺序BGR屏蔽通道
cur_img[:,:,0] = 0
cur_img[:,:,1] = 0
cv.show('R',cur_img)
  • 1
  • 2
  • 3
  • 4
  • 5

边界填充

  • BORDFR_RFPLICATE:复制法,也就是复制最边缘像素。
  • BORDER_REFLECT:反射法,对感兴趣的图像中的像素在两边进行复制例阳:fedcbalabcdefahlhcfadcb。
  • BORDER_REFLECT_101:反射法,也就是以最边缘像索为轴,对称,giodcblabodofghigfodsba
  • BORDFR_WRAP:外包装法cclefghjabcdeighjabrdafg
  • BORDER_CCNSTANT;常量法。常数值填充。
top_size. bottom_size,left_size. right_size = (50,50,50,50)
# 复制法
replicate = cv2.copyMakeBorder(img,top_size,bottom_size,left_size,right_size, borderType=cv2.BORDER_REPLICATE)
# 反射法1
reflect=cv2.copyMakeBorder(img,top_size,botton_size,left_size,right_size,cv2.BORDER_REFLECT)
# 反射法2
reflect101=cw2.copyMakeBorder(img,top_size,bottom_size, letf_size,right_size,cv2.BOFDER REFLECT_101)
# 外包装法
wrap = cv2.copyMakeBorder(img,top_size,bottom_size,left_size,right_size,cv2.BORDER_WRAP)
# 常量法
constant = ev2.copyMakeBorder(img, top_size,botton_size,left_size,right_size, cv2.BORDEE_CONSTANT, value=0)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

数值计算

img_cat = cv.imread('cat.jpg')
img_cat2 = img.cat+10
  • 1
  • 2

当两个图片的像素相加时,结果超过了255,则超出了范围,那么结果则是 值 = 相加值%256

cv2.add(img_cat,img_cat2)
  • 1

当两个图片的像素相加时,结果超过了255,则超出了范围,那么结果则是 值 = 255

图像融合

当两个尺寸不一致的图像融合,需要转化成相同的像素格式

img_dog = cv2.resize(img_dog,(500,414))
# 还可以更改比例
img_dog = cv2.resize(img_dog,(0,0),fx=1,fy=3)

# 根据权重融合 R = ax1 + ax2 + ... + b
res = cv2.addWeighted(img_cat,0.4,img_dog,0.6,0)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

图像阈值

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

  • src:输入图,只能输入单通道图像,通常来说为灰度图
  • dst:输出图
  • thresh:阈值
  • maxval:当像素值超过了阈值(或者小于阈值,根据typs来决定),所赋予的值
  • type:二值化操作的类型,包含以下5种类型: cv2.THRESH_BINARY;cv2.THRESH_BINARY_INV;cv2.THRESH_TRUNC;cv2.THRESH.TOZERO;cv2.THRESH_TOZERO_INV
    • cv2.THRESHBINARY:超过阈值部分取maxval(最大值),否则取0
    • cv2.THRESH_BINARY_INV , THRESH_BINARY的反转
    • cv2.THRESH_TRUNC大干阈值部分设为阈值,否不变
    • cv2.THRESH_TOZERO大于阈值部分不改变,否则设为0
    • cv2.THRESH_TOZERO_INV THRESH_IOZERO的反转

图像平滑

  • 均值滤波
# 简单的平均卷积操作
blur = cv2.blur(img,(3,3))

cv2.imshow('blur',blur)
cv2.waitKey(0)
cv2.destroyAllWindows()
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 方框滤波
# 基本和均值一样,可以选择归一化
box = cv2.boxFilter(img,-1,(3,3),normalize=True) # 为true时与均值滤波一样

cv2.imshow('box',box)
cv2.waitKey(0)
cv2.destroyAllWindows()
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 高斯滤波
# 高斯模糊的卷积核里的数值是满足高斯分布,相当于更重视中间的
aussian = cv2.GaussianBlur(img,(5,5,1))

cv2.imshow('aussian',aussian)
cv2.waitkey(0)
cv2.destroyAllWindows()
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 中值滤波
#相当于用中值代替
median = cv2.medianBlur(img,5)

cv2.imshow('median', median)
cv2.waitKey(0)
cv2.destroyAllWindows()
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 展示所有的滤波
res = np.hstack ((blur, aussian, median))

cv2.imshow(' median vs average' , res)
cv2.waitKey(0)
cv2.destroyAllwindows()
  • 1
  • 2
  • 3
  • 4
  • 5
  • 傅里叶滤波&&低通滤波

    傅里叶变换我们知道是将图像从时域转换到频域的一种非常强大的武器, 时域中的图像数据就是我们看到的一个个像素点组成的图片,而频域中,我们是能够得到灰度分量的频率大小的。可能图像看起来不是很好理解,拿一段语言来描述最贴切:假设我们录了一段语言,里面各种声音混杂,从时域的角度,这就是按照时间序组成的一段音频,那么我们有办法把这段音频的噪声去掉,只保留重要的音频信息吗? 其实这个在时域中非常难做到,而转到频域里面,就会发现这些声音都是一条条频率不同的声音线组成,通过频率就能非常轻松的过滤出某些我们想要的声音。所以通过傅里叶变换操作,我们能非常容易的拿出图像或者声音中我们需要的某些灰度分量了。

那么回到图像里面, 同样会存在高频或者低频的灰度分量:

  • 高频: 变化剧烈的灰度分量, 例如图片里面的边界信息, 相当于从一个对象到了另一个对象,像素值变换幅度非常大
  • 低频:变换缓慢的灰度分量,比如一片大海,像素值变换幅度非常小

根据频率进行滤波,主要分为两种:

  • 低通滤波器: 只保留低频, 会使得图像模糊, 高频没了, 使得边界没了
  • 高通滤波器: 只保留高频, 会使得图像细节增强, 即边界锐化的感觉

opencv中主要是cv2.dft()cv2.idft(), 输入图像需要先转成np.float32格式

cv2.dft就是把时域转成了频域,但是为了显示,还需要进行逆变换,即cv2.idft()

得到的结果中频率为0的部分会在左上角, 通常要转换到中心位置, 可以通过shift变换来实现。
cv2.dft()返回的结果是双通道的(实部,虚部), 通常还需要转换成图像格式才能展示(0,255)

总结下低通滤波和高通滤波的过程

  • 原始图像 -> cv2.ift -> 得到频域图像dft -> 低频信息移到中间(np.fft.fftshift) -> 这个结果是双通道(实部+虚部) -> 转成图像格式cv2.magnitude
  • 找中心点 -> 制作mask掩码矩阵 -> 频域图像dft* mask -> 得到过滤之后的频域图像fshift
  • 过滤掉高频信息的频域图像fshift -> 低频信息移动到原来位置(np.fft.ifftshift) -> 傅里叶逆变换cv2.idft -> 转成图像格式cv2.magnitude
def dft_idft(img, threshold=30, mode='low'):
    img_float = np.float32(img)  # 转成float
    # 时域 -> 频域
    dft = cv2.dft(img_float, flags=cv2.DFT_COMPLEX_OUTPUT)
    # 把低频值转到中间位置
    dft_shift = np.fft.fftshift(dft)  # 低频值转到中心位置
    
    # 掩码矩阵
    row, col = img.shape
    c_row, c_col = int(row/2), int(col/2)
    if mode == 'low':
        mask = np.zeros((row, col, 2), np.uint8)
        mask[c_row-threshold:c_row+threshold, c_col-threshold:c_row+threshold] = 1
    elif mode == 'high':
        mask = np.ones((row, col, 2), np.uint8)
        mask[c_row-threshold:c_row+threshold, c_col-threshold:c_row+threshold] = 0
    else:
        print("参数错误")
        return
        
    # 下面就是用这个滤波器对频域的那个图像做操作
    fshift = dft_shift * mask
    f_ishift = np.fft.ifftshift(fshift)   # 之前是低频信息shift到了中间位置,而这个操作是从中间位置变到原来位置
    img_back = cv2.idft(f_ishift)  # 逆变换
    img_back = cv2.magnitude(img_back[:, :, 0], img_back[:, :, 1])
    
    return img_back

# 低通滤波和高通滤波
img_low = dft_idft(img, mode='low')
img_high = dft_idft(img, mode='high')



  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34

形态学操作

所谓形态学操作,我理解,对图像本身进行的一些预处理,比如让图像里面的线条变粗或者变细,重点突出图像的某些部分, 去掉图像中的毛刺,处理一些缺陷等。

图像形态学中常用的两个操作是腐蚀和膨胀,腐蚀一般用于处理毛刺问题,能够让线条或者图形变细。 而膨胀一般是填补一些缺陷, 能够让线条变粗

腐蚀操作

img = cv2.imread('img/dige.png')
cv2.imshow('img', img)

kernel = np.ones((3, 3), np.uint8)# 卷积核
erosion_img = cv2.erode(img, kernel, iterations = 1)
# 这里卷积核越大,或者迭代次数越多, 都会使得右边的线条变细。

cv2.imshow('erosion_img', erosion_img)

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

腐蚀算法: 用n*n的卷积核扫描每个元素,用卷积核与其覆盖的二值图像做"与"操作,如果都是1,那么结果中心元素像素值是1,否则是0

膨胀操作

迭代次数和卷积核越大,越有利于膨胀操作, 越来越胖

# 下面尝试把上面腐蚀的图片弄成胖一点的
kernel = np.ones((3, 3), np.uint8)
dige_dilate = cv2.dilate(erosion_img, kernel, iterations=1)
cv2.imshow('dilate_img', dige_dilate)

# 对线条起了加粗的效果
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

膨胀算法: 猜一下,n*n的卷积核扫描每个元素, 用卷积核与其覆盖的二值图像做或操作, 如果有1, 那么中心元素变成1,如果都是0, 那么才是0

开闭运算

类似于一个管道把腐蚀和膨胀两个操作连接到了一起:

  • 开运算cv2.MORPH_OPEN: 先腐蚀,后膨胀
  • 闭运算cv2.MORPH_CLOSE: 先膨胀,后腐蚀
img = cv2.imread('img/dige.png')
cv2.imshow('img', img)

# 开运算: 先腐蚀,再膨胀
kernel = np.ones((5, 5), np.uint8)
opening = cv2.morphologyEx(img, cv2.MORPH_OPEN, kernel)
cv2.imshow('opening', opening)

# 闭运算: 先膨胀后腐蚀
closing = cv2.morphologyEx(img, cv2.MORPH_CLOSE, kernel)
cv2.imshow('closing', closing)

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

梯度运算

cv2.MORPH_GRADIENT这个就是膨胀-腐蚀,容易提取到边缘信息。

# 梯度 = 膨胀 - 腐蚀
pie = cv2.imread('img/pie.png')
kernel = np.ones((5, 5), np.uint8)
dilate = cv2.dilate(pie, kernel, iterations=5)
erosion = cv2.erode(pie, kernel, iterations=5)

res = np.hstack((dilate, erosion))
cv2.imshow('res', res)

cv2.imshow('subtract', dilate-erosion)

gradient = cv2.morphologyEx(pie, cv2.MORPH_GRADIENT, kernel, iterations=5)
cv2.imshow('gradient', gradient)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

这个梯度运算和直接膨胀-腐蚀效果是一样的结果, 背后实现,我猜其实就是膨胀先执行iterations次,然后腐蚀执行iterations次,然后前面结果减去后面的结果。

礼帽和黑帽

  • 礼帽操作cv2.MORPH_TOPHAT: 原始输入 - 开运算结果,即原始输入-(先腐蚀,后膨胀)
  • 黑帽操作cv2.MORPH_BLACKHAT: 闭运算结果-原始输入,即(先膨胀,后腐蚀)-原始输入
# 礼帽
img = cv2.imread('img/dige.png')
tophat = cv2.morphologyEx(img, cv2.MORPH_TOPHAT, kernel)
cv2.imshow('tophat', tophat)

# 黑帽
img = cv2.imread('img/dige.png')
blackhat = cv2.morphologyEx(img, cv2.MORPH_BLACKHAT, kernel)
cv2.imshow('blackhat', blackhat)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

礼帽运算: 取出亮度高的地方

  • 开运算可以消除暗背景下的高亮区域
  • 如果用原图减去开运算结果,就可以得到灰度图中的区域

黑帽运算: 取出亮度低的地方

  • 闭运算可以删除亮区域背景下的暗区域,闭运算减去原图可以得到原图像中灰度较暗的区域

一般用的时候,原始图像转灰度图像,然后进行二值化,然后, 再通过开闭运算, 礼帽和黑帽等操作,就能拿到图像中的想要的区域来了。所以后面这些组合操作很重要,也很实用。比如车牌号识别,信用卡数字识别等,都会用到这些技术

图像的算子操作

图像的算子操作其实可以帮助我们去找图像的轮廓信息,主要有Sobel算子,Scharr算子以及Laplacian算子,区别在于卷积核参数不一样。

Sobel算子

Sobel算子, 这感觉依然是两个卷积核进行操作, 原理如下:

这个东西其实找的是图像的轮廓信息,或者边缘信息,依赖于上面的两个卷积核,一个是水平方向的,一个是垂直方向的,实际计算的时候是这样, 3∗3的卷积核覆盖到一个图像区域,中间点的取值,就是水平的这个卷积核与当前图像卷积结果或者垂直方向的卷积核与当前图像卷积结果。这个就看是从水平上算还是垂直方向上算了。当然, 这个算子也是保证最终结果是0-255,如果超了,就会进行截断操作。

靠中心越近权重越大,所以这里用了2或者-2表示

  • 只要中心点两边的颜色不一样,这个点这里就有梯度, 就不为0, 中心点两边的亮度相差越大, 使得这个边界就会越明显。
  • 如果中心点两边的颜色一样, 这个点就没有梯度,上面运行就是0, 变成黑色
  • 找边缘的时候, 尽量的先找水平,然后找垂直或者先找垂直然后找水平,别一块找,因为一块找有可能会出现抵消, 导致效果不清晰
dst = cv2.Sobel(src, ddepth, dx, dy, ksize)

'''
ddepth: 图像的深度
dx和dx分别表示水平和垂直方向, 是算x方向还是y方向
ksize是Sobel算子的大小, 一般是3*3
'''
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

这里直接说常规使用方法, 对于一张图片, 先通过imread的灰度方式读入进来,然后先求水平上的梯度,再求垂直方向上的梯度, 最后加权融合就能找出图片的梯度来。

lena = cv2.imread('img/lena.jpg', cv2.IMREAD_GRAYSCALE)

# 水平做sobel算子
lena_sobelx = cv2.Sobel(lena, cv2.CV_64F, 1, 0, ksize=3)
lena_sobelx = cv2.convertScaleAbs(lena_sobelx)   # 这里是为了如果出现负数不让他截断成0,而是变成它的绝对值
# cv2.imshow('lena_x', lena_sobelx)

# 垂直做sobel计算
lena_sobely = cv2.Sobel(lena, cv2.CV_64F, 0, 1, ksize=3)
lena_sobely = cv2.convertScaleAbs(lena_sobely)
# cv2.imshow('lena_y', lena_sobely)

# 把这俩合并
lena_margin = cv2.addWeighted(lena_sobelx, 1, lena_sobely, 1, 0)
cv2.imshow('lena_margin', lena_margin)

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

这里一个小经验就是两个方向分开算,要比同时算,最终效果要好, 原因是因为,如果是同时计算的时候, 亮度上会有一些抵消。

Scharr算子

Scharr算子能够找出更加细致的边界,使得图像的纹理信息更加丰富。使用的方法和上面Sobel是一样的,无非就是函数换了下:

# 这里用scharr算子试一下
img = cv2.imread('img/lena.jpg', cv2.IMREAD_GRAYSCALE)
lena_scharr_x = cv2.Scharr(img, cv2.CV_64F, 1, 0)
lena_scharr_x = cv2.convertScaleAbs(lena_scharr_x)
lena_scharr_y = cv2.Scharr(img, cv2.CV_64F, 0, 1)
lena_scharr_y = cv2.convertScaleAbs(lena_scharr_y)
lena_scharr_xy = cv2.addWeighted(lena_scharr_x, 0.5, lena_scharr_y, 0.5, 0)
cv2.imshow('lena_scharr_xy', lena_scharr_xy)

# 可以找出更细致的边界, 我猜的没错 对, 纹理细节这个词用的好
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

Laplacian算子

一般不会单独使用这个, 常和其他算子组合使用, 对噪声会更加敏感, 但噪音点可能不是边界,所以这个效果单独用不好

laplacian = cv2.Laplacian(img, cv2.CV_64F)
laplacian = cv2.convertScaleAbs(laplacian)
cv2.imshow('laplacian', laplacian)
  • 1
  • 2
  • 3

最后对比下各种算子找边缘的效果:

Canny边缘检测算法

Canny边缘检测算法,用于检测图像的边缘信息。主要包括下面的流程:

  • 使用高斯滤波器,以平滑图像,滤除噪声 ---- 滤波器去噪
  • 计算图像中每个像素点的梯度强度和方向 ---- 梯度的强度和方向
  • 应用非极大值抑制(Non-Maximum Suppression),以消除边缘检测带来的杂散响应 ---- 梯度小的像素点抑制掉
  • 应用双阈值(Double-Threshold)检测来确定真实和潜在的边缘 ---- 双阈再检测
  • 通过抑制孤立的弱边缘最终完成边缘检测

高斯滤波

这一步是对原始图像处理,去掉一些噪声,让其本身更加平滑,在Canny算法中用的是高
斯滤波器

其他滤波器,像均值滤波器,中值滤波器等都比较常用,这是滤波器那里的相关知识。

梯度和方向

这里就是求各个中心点的梯度, Canny算法中用的是sobel算子

非极大值抑制

这里是为了去除一些梯度值很小的边缘信息, 以消除边缘信息带来的杂散效应。

方法一——线性插值法

线性插值法:设g1的梯度幅值Mg1),g2的梯度幅值M(g2),则dtmp1可以很得到:
M(dtmp1)=w*M(g2)+(1-w)*M(g1)
其中w=distance(dtmp1,g2)/distance(g1,g2)
distance(g1,g2)表示两点之间的距离。

这里描述下这种方法是怎么做的,这个属实有些复杂,这里是判断像素C这个点要不要被抑制点,即判断C这个点是不是极大值,可以这么做:

  • 首先,根据上面的求梯度的方法,计算C点的梯度,有了梯度,就有了方向(和边界垂直),这样就能画出C点处像素的梯度来
  • C点的梯度就那条蓝色的线,延伸一下,会和上下的边界有交点,上面交到了dtmp1, 下面交到了dtmp2
  • 下面算出dtmp1和dtmp2点的梯度来,用M表示梯度的话,然后比较M(dtmp1), M(dtmp2), M©的大小,如果M©比那两个都大,那么说明C这个点的像素值是极大值像素点,保留下这个点,否则,把它干掉。那么接下来的问题,就是M(dtmp1), M(dtmp2)如何计算呢?
    • 这俩点的梯度其实是伪梯度,直接算没法算,只能通过与它相邻的两个点去估计, 比如算M(dtmp1)
      • 首先,我们能得到dtmp1两端点的像素值M(g1), M(g2)
      • 然后,我们通过这两点的像素值线性插出dtmp1的梯度值来, 怎么插呢? 线性插值公式如上面,这里的w表示的距离权重,很好理解应该
    • 对于dtmp2,同样按照上面的方式计算即可
方法二

这个方法比上面那个简单了, 这里会借助它周围的8个点:

  • 首先,把一个像素的梯度方向离散成周围8个方向,然后看看它离这个方向进,就用那个方向代替
  • 这个方向上,看看离着哪两个点近,就有这两个点的梯度,和中心这个点的梯度进行比较
  • 如果中心这个点的梯度比那两个点都大,那么保留,否则,抑制掉

双阈值检测


这里其实就是指定了一个最大值和一个最小值,然后看看中心点这个梯度值大小,根据右边的规则进行判断需要需要保留。 这里的连有边界的意思是看他是不是挨着一个边界, 如果它旁边那个点是边界了, 那么这个点也会保留下来,否则会去掉。

Canny算法的使用

上面就是Canny算法各个流程的细节部分,在opencv中用起来其实很简单,只需要一个cv2.Canny函数即可。

lena = cv2.imread('img/lena.jpg', cv2.IMREAD_GRAYSCALE)

v1 = cv2.Canny(lena, 80, 150)
v2 = cv2.Canny(lena, 50, 100)

res_lena = np.hstack((lena, v1, v2))
cv2.imshow('res', res_lena)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

这里的minval指定的如果比较小,会找到更加细致的边界, 纹理信息更多, 但可能会拿到很多误选择的边界
maxval指定的如果比较大, 会找到更加严格的边界,但可能会漏掉一些边界

  • minval和maxval都越小,那么检测到的信息就越丰富
  • maxval和maxval都越大, 检测到的信息就越稀疏, 很多边界可能检测不出来

图像金字塔

图像金字塔, 把图像组合成像金字塔一样的形状。图像金字塔的作用,比如我们要对一张图像进行特征提取, 我们可以把图像制作成一个金字塔, 对于金字塔里面的每张图片都进行特征提取, 每张图片可能提取的特征是不一样的,这样就可能增加了图像特征的丰富性。
图像金字塔是图像多尺度表达的一种,最主要用于图像分割,是一种以多分辨率来解释图像的有效但概念简单的结构,最早用于视觉和图像压缩。最底部是待处理图像的高分分辨率表示, 越高层,分辨率越低。

常见的两类金字塔:

  • 高斯金字塔:用来向下/降采样, 主要的图像金字塔
  • 拉普拉斯金字塔:用来从金字塔底层图像重建上层未采样图像, 可以对图像进行最大程度的还原,配合高斯金字塔一起使用

两者的简要区别:高斯金字塔用来向下降采样图像,注意降采样其实是由金字塔底部向上,分辨率降低,它和我们理解的金字塔概念相反(注意);而拉普拉斯金字塔则用来从金字塔底层图像中向上采样重建一个图像。

高斯金字塔

向下采样方法(缩小)

将level0级别的图像转换为 level1,level2,level3,level4,图像分辨率不断降低的过程称为向下取样

从金字塔下往上,是一个向下采样,越采样越小, 主要过程有两步:

  • 普通的卷积操作, 用的是上面这个卷积核, 我发现这里的卷积和卷积神经网络里面的那种卷积运算还不太一样吧, 这里怎么感觉是对图片的每个像素都用上面这个卷积核卷一遍,卷的过程就是覆盖区域元素相乘最后相加得到中心点的元素,这样最终的结果就是和原图片一样大小的了。
  • 但是金字塔是越往上越小的,所以这里的第二步,就是将所有偶数行和列去掉,这样得到的结果图像只有原图的四分之一 , 这就是下采样的过程,
向上采样方法(放大)

将level4级别的图像转换为 level3,level2,level1,leve0,图像分辨率不断增大的过程称为向上取样

金字塔上往下, 是一个向上采样, 越采样越大

它将图像在每个方向上扩大为原图像的2倍,新增的行和列均使用0来填充,并使用于“向下取样”相同的卷积核乘以4,再与放大后的图像进行卷积运算,以获得“新增像素”的新值

注意: 把图像先上采样,再下采样还原,或者是下采样再上采样还原,都会损失掉信息,虽然和原始图片一样大,但会比原始图像模糊,所以上采样和下采样都是非线性处理, 不可逆,会损失掉信息


img = cv2.imread('img/AM.png')     # (442, 340, 3)

# 先向上采样看看
up_sample = cv2.pyrUp(img)
cv2.imshow('up', up_sample)
print(up_sample.shape)   # (884, 680, 3)

# 向下采样
up_down = cv2.pyrDown(img)
cv2.imshow('down', up_down)
print(up_down.shape)   # (221, 170, 3)

# 先经历上采样再还原
cv2.imshow('up_down', np.hstack((img, cv2.pyrDown(up_sample))))

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

拉普拉斯金字塔


也就是说,拉普拉斯金字塔是通过源图像减去先缩小后再放大的图像的一系列图像构成的。保留的是残差!为图像还原做准备,实现非常简单。

down = cv2.pyrDown(img)
down_up = cv2.pyrUp(down)
l_1 = img - down_up
cv2.imshow('l1', l_1)
  • 1
  • 2
  • 3
  • 4

下面,就是把高斯上采样, 下采样, 拉普拉斯金字塔的这个过程,都写成了函数的形式,可以一键把整个金字塔干出来。

# 高斯降采样金字塔
def Gaussian_Down(img, sample_times):
    Gaussian_Down_pyramid = []
    for i in range(sample_times):
        if i == 0:
            down = cv2.pyrDown(img)
        else:
            down = cv2.pyrDown(down)
        Gaussian_Down_pyramid.append(down)
    return Gaussian_Down_pyramid

def Gaussian_up(img, sample_times):
    Gaussian_up_pyramid = []
    for i in range(sample_times):
        if i == 0:
            up = cv2.pyrUp(img)
        else:
            up = cv2.pyrUp(up)
        Gaussian_up_pyramid.append(up)
    return up

def Laplace(img, sample_times):
    laplace_pyramid = []
    for i in range(sample_times):
        if i == 0:
            down = cv2.pyrDown(img)
            laplace_pyramid.append(img-cv2.pyrUp(cv2.pyrDown(img)))
        else:
            up_down = cv2.pyrUp(cv2.pyrDown(down))
            # 注意这里由于取整的关系,导致这俩还不一样大
            if down.shape != up_down.shape:
                laplace_pyramid.append(down - up_down[:down.shape[0], :down.shape[1], :])
            else:
                laplace_pyramid.append(down - up_down)
            down = cv2.pyrDown(down)
    return laplace_pyramid

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37

图像轮廓检测

轮廓检测和边缘检测是不一样的概念,轮廓检测是只检测对象最外面的那个大轮廓,类似于用一个框圈起对象来,相当于锁定对象的位置,而边缘检测是检测对象内部的各种边界信息。所以不一样。

轮廓检测的作用是可以帮助我们做一些额外的数值特征, 比如图像轮廓的面积,周长, 这样就能反映出大小来了。

轮廓绘制

cv2.findContours(img, mode, method) → contours, hierarchy(返回的轮廓列表及层级信息)

  • mode: 轮廓检索模式
    • RETR_EXTERNAL: 只检索最外面的轮廓
    • RETR_LIST: 检索所有的轮廓,并将其保存到一条链表当中
    • RETR_CCOMP: 检索所有的轮廓, 并将他们组织成两层, 顶层是各部分的外部边界, 第二层是空洞的边界
    • RETR_TREE: 检索所有的轮廓, 并重构嵌套轮廓的整个层次(最常用), 多层字典?
  • method: 轮廓逼近方法:
    • CHAIN_APPROX_NONE: 以Freeman链码的方式输出轮廓, 所有其他方法输出多边形(顶点的序列)
    • CHAIN_APPROX_SIMPLE: 压缩水平的,垂直的和斜的部分, 也就是,函数只保留他们的终点部分
img = cv2.imread('img/car.png')

# 转成灰度, 这里为啥不直接读取的时候转?  因为有些人后序会用到彩色图,仅此而已
gray = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY)

# 下一步, 对上面的灰度图像进行二值处理
ret, thresh = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY)  # 像素小于127的弄成0, 大于127的弄成255,这样变成二值图像

# 下面绘制轮廓
# 最新版opencv只返回两个值了 3.2之后, 不会返回原来的二值图像了,直接返回轮廓信息和层级信息
contours, hierarchy = cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_NONE)


# 下面把轮廓进行可视化
draw_img = img.copy()    # 这里要copy一下, 否则会改变原始图像, 下面那哥们貌似是原子操作
# drawContours(image, contours, contourIdx, color[, thickness[, lineType[, hierarchy[, maxLevel[, offset]]]]]) -> image
# 这里的contourIdx要绘制的轮廓线,负数表示所有, 正是只画对应位置的那个点
# color是轮廓线的颜色, (0,0,255)表示红色,因为opencv里面通道排列是bgr吧应该--> 对的
# 最后面那个2是线条的宽度,值越大, 线条越宽
res = cv2.drawContours(draw_img, contours, -1, (0, 255, 0), 2)
cv2.imshow('res', res)

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

轮廓特征

def contours_feature(contour):
    features = []
    features.append(cv2.contourArea(contour))  # 轮廓面积
    features.append(cv2.arcLength(contours, True))  # 轮廓周长,后面那个是closed,表示这个曲线是否闭合
	return features

controus_feature(countours[0])
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

轮廓近似

# 读取图片 -> 转成灰度图 -> 二值化 -> 找轮廓
img = cv2.imread('img/contours2.png')

# 用上面的函数画一些轮廓
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
ret, thresh = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY)
contours, hierarchy = cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_NONE)
cnt = contours[0]

draw_img = img.copy()
res = cv2.drawContours(draw_img, [cnt], -1, (0, 0, 255), 2)
cv2.imshow('res', res)


# 下面进行轮廓近似
epsilon = 0.1 * cv2.arcLength(cnt, True)   # 这里是指定的阈值,一般按照周长的百分比设置
# 这个阈值越大,那么画出来的轮廓就越粗糙,因为很容易直接一条直线代替曲线, 如果阈值越小,那么近似出来的轮廓细腻,因为d很容易大于阈值,用多条直线近似
approx = cv2.approxPolyDP(cnt, epsilon, True)

draw_img2 = img.copy()
res2 = cv2.drawContours(draw_img2, [approx], -1, (0, 0, 255), 2)
cv2.imshow('res2', res2)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

图像模板匹配

模板匹配和卷积原理很像, 模板在原图像上从原点开始滑动,计算模板与(图像被模板覆盖的地方)的差别程度,这个差别程度的计算方法在opencv里面有6种, 然后将每次计算的结果放入一个矩阵里,作为结果输出。

假如原图是A ∗ B ABA∗B大小, 而模板是a ∗ b aba∗b大小, 则输出结果的矩阵( A − a + 1 ) ( B − b + 1 ) (A-a+1)(B-b+1)(A−a+1)(B−b+1), 这个感觉和卷积神经网络中的卷积操作很像了。 主要步骤如下:

  • 读入原始图像,一般是灰度图
  • 读入模板图像,也是灰度图
  • 模板匹配,这里直接使用模板匹配函数cv2.matchTemplate(img, template, mode), mode表示计算方式
    • TM_SQDIFF: 计算平方不同, 计算出来的值越小越相关
    • TM_CCORR: 计算相关性, 计算出来的值越大,越相关
    • TM_CCOEFF: 计算相关系数,计算出来的值越大,越相关
    • TM_SQDIFF_NORMED: 计算归一化平方不同,计算出来的值越接近0, 越相关
    • TM_CCORR_NORMED: 计算归一化相关性,计算出来的值越接近1, 越相关
    • TM_CCOEFF_NORMED: 计算归一化相关系数, 计算出来的值越接近1, 越相关

具体计算公式可以去查文档。尽量使用下面归一化的结果

  • 获取到匹配好的原始图像的起始位置cv2.minMaxLoc, 这里可以获取到最大值位置,也可以获取到最小值位置,但是由于上面计算方式的不同, 得先弄明白是最大的时候两个越相关,还是越小的时候, 两个越相关
# 1. 读入原始图像,灰度图模式读入 0表示灰度图,还有就是cv2.IMREAD_SCALE
img = cv2.imread('img/cat.jpg', 0)  # <==> cv2.imread('img/cat.jpg', cv2.IMREAD_GRAYSCALE)

# 2. 读入模板
template = cv2.imread('img/template.jpg', 0)

# 3. 模板匹配
res = cv2.matchTemplate(img, template, cv2.TM_SQDIFF)  # 值越小越相似
res.shape  # (548-122+1)(666-181+1)


# 4. 获取匹配好图像的起始位置
min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(res)

# 下面尝试把矩形框画出来
top_left = min_loc
bottom_right = (top_left[0]+template.shape[0], top_left[1]+template.shape[1])

# 画矩形
img2 = img.copy()
rect = cv2.rectangle(img2, top_left, bottom_right, 255, 2)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

假设图片里面不只是一个对象能匹配呢? 有没有办法把所有的模板图像都匹配出来, 那就需要自己最大或者最小位置的地方了,其实也简单:

img_rgb = cv2.imread('img/mario.jpg')
# 转成灰度图
img_gray = cv2.cvtColor(img_rgb, cv2.COLOR_BGR2GRAY)
template = cv2.imread('img/mario_coin.jpg', 0)

h, w = template.shape[:2]
res = cv2.matchTemplate(img_gray, template, cv2.TM_CCOEFF_NORMED)
threshold = 0.8  # 这里设置一个门限

# 取匹配程度大于百分之八十的坐标
loc = np.where(res >= threshold)
# 这个loc是横坐标一个数组,纵坐标一个数组的形式,所以下面得用可选参数的方式进行组合

for pt in zip(*loc[::-1]):
    bottom_right = (pt[0]+w, pt[1]+h)
    cv2.rectangle(img_rgb, pt, bottom_right, (0, 255, 0), 2)

cv2.imshow('img_rgb', img_rgb)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

图像直方图与均衡化

直方图

cv2.calcHist(images, channels, mask, histSize, ranges):

  • images: 原图像格式为uint8或者float32, 当传入时用[]括起来, 一般时灰度图像居多。
  • channels: 同样时中括号括起来, 指定彩色图片的哪个通道,如果时灰度图,它的值就是[0], 如果时彩色图, [0], [1], [2]表示BGR三个通道。
  • mask: 掩膜图像, 统计整个图的时候,就时None,如果想统计图像的某一部分,那么可以制作一个掩膜矩阵
  • histSize: bin的数目, 可以统计0-255,每个像素点,也可以按照范围, 比如0-10, 10-20, 等等的个数,这里指定
  • ranges: 像素值范围,一般默认[0,256)
img_bgr = cv2.imread('img/cat.jpg')
color = ('b', 'g', 'r')
for i, col in enumerate(color):
    histr = cv2.calcHist([img_bgr], [i], None, [256], [0, 256])
    plt.plot(histr, color=col)
    plt.xlim([0, 256])
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

mask掩码操作的使用, mask掩码矩阵传入之后,只会统计我们需要部分的直方图:

# 先创建mask
mask = np.zeros(img.shape[:2], np.uint8)

mask[100:300, 100:400] = 255
cv2.imshow('mask', mask)  # 这样就创造除了一个掩码矩阵, 只有0和255组成, 白色的地方是要显示的,黑色的地方是被遮挡

masked_img = cv2.bitwise_and(img, img, mask=mask)  # 与操作  等价img & mask

# 下面对比一下
hist_full = cv2.calcHist([img], [0], None, [256], [0, 256])
hist_mask = cv2.calcHist([img], [0], mask, [256], [0, 256])
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

均衡化

直方图的作用,就是可以对一些图像均衡化处理,使得图像的对比度拉高,更加清晰

  • 首先,先统计原始图像里面每个像素点出现的次数,然后归一化,计算出概率
  • 从小到大排序,然后就算累计概率,也非常简单,从上往下,概率的累计值
  • 用到的映射函数: 累计概率值*像素的取值范围(255-0)
  • 把上面计算得到的数取整
img = cv2.imread('img/clahe.jpg', 0)  
plt.hist(img.ravel(), 256)

# 均衡化
equ = cv2.equalizeHist(img)
plt.hist(equ.ravel(), 256)
plt.show()

res = np.hstack((img, equ))
cv2.imshow('res', res)  # 会发现均衡话之后,变得更加清晰了
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/我家小花儿/article/detail/554555
推荐阅读
相关标签
  

闽ICP备14008679号