OpenCV计算机视觉整理

原创
2021/11/12 15:31
阅读数 1.5K

图像、视频加载与显示

创建显示窗口

import cv2

if __name__ == "__main__":

    # 创建窗口
    cv2.namedWindow('new', cv2.WINDOW_NORMAL)
    # 调整窗口大小
    cv2.resizeWindow('new', 640, 480)
    # 显示窗口
    cv2.imshow('new', 0)
    # 显示时长
    key = cv2.waitKey(0)
    if key == 'q':
        exit()
    # 销毁窗口
    cv2.destroyWindow()

运行结果

载入图片

import cv2

if __name__ == "__main__":

    cv2.namedWindow('img', cv2.WINDOW_NORMAL)
    img = cv2.imread("/Users/admin/Documents/WechatIMG12.jpeg")
    cv2.imshow('img', img)
    while True:
        key = cv2.waitKey(0)
        if key & 0xFF == ord('q'):
            break
    cv2.destroyAllWindows()

运行结果

保存文件

import cv2

if __name__ == "__main__":

    cv2.namedWindow('img', cv2.WINDOW_NORMAL)
    img = cv2.imread("/Users/admin/Documents/WechatIMG12.jpeg")
    cv2.imshow('img', img)
    while True:
        key = cv2.waitKey(0)
        if key & 0xFF == ord('q'):
            break
        elif key & 0xFF == ord('s'):
            cv2.imwrite("/Users/admin/Documents/帅照.png", img)
    cv2.destroyAllWindows()

当我们点击键盘"s"键的时候,运行结果

进入/Users/admin/Documents文件夹

(base) -bash-3.2$ ls | grep 帅照
帅照.png

摄像头视频采集

import cv2

if __name__ == "__main__":

    # 创建窗口
    cv2.namedWindow('video', cv2.WINDOW_NORMAL)
    cv2.resizeWindow('video', 640, 480)
    # 获取视频设备
    cap = cv2.VideoCapture(0)
    while True:
        # 从摄像头读视频桢
        ret, frame = cap.read()
        # 将视频帧在窗口中显示
        cv2.imshow('video', frame)
        key = cv2.waitKey(1)
        if key & 0xFF == ord('q'):
            break
    # 释放资源
    cap.release()
    cv2.destroyAllWindows()

运行结果

这里可以看到摄像头已经打开,并开始采集视频。

读取视频文件

我们这里使用一段鹦鹉的视频,使用命令ffplay查看每秒播放帧数

./ffplay cockatoo.mp4
ffplay version N-104454-gd92fdc7144-tessus  https://evermeet.cx/ffmpeg/  Copyright (c) 2003-2021 the FFmpeg developers
  built with Apple clang version 11.0.0 (clang-1100.0.33.17)
  configuration: --cc=/usr/bin/clang --prefix=/opt/ffmpeg --extra-version=tessus --enable-avisynth --enable-fontconfig --enable-gpl --enable-libaom --enable-libass --enable-libbluray --enable-libdav1d --enable-libfreetype --enable-libgsm --enable-libmodplug --enable-libmp3lame --enable-libmysofa --enable-libopencore-amrnb --enable-libopencore-amrwb --enable-libopenh264 --enable-libopenjpeg --enable-libopus --enable-librubberband --enable-libshine --enable-libsnappy --enable-libsoxr --enable-libspeex --enable-libtheora --enable-libtwolame --enable-libvidstab --enable-libvmaf --enable-libvo-amrwbenc --enable-libvorbis --enable-libvpx --enable-libwebp --enable-libx264 --enable-libx265 --enable-libxavs --enable-libxvid --enable-libzimg --enable-libzmq --enable-libzvbi --enable-version3 --pkg-config-flags=--static --enable-librtmp --enable-ffplay --enable-sdl2 --disable-ffmpeg --disable-ffprobe
  libavutil      57.  7.100 / 57.  7.100
  libavcodec     59. 12.100 / 59. 12.100
  libavformat    59.  8.100 / 59.  8.100
  libavdevice    59.  0.101 / 59.  0.101
  libavfilter     8. 16.100 /  8. 16.100
  libswscale      6.  1.100 /  6.  1.100
  libswresample   4.  0.100 /  4.  0.100
  libpostproc    56.  0.100 / 56.  0.100
Input #0, mov,mp4,m4a,3gp,3g2,mj2, from 'cockatoo.mp4':
  Metadata:
    major_brand     : isom
    minor_version   : 512
    compatible_brands: isomiso2avc1mp41
    encoder         : Lavf56.4.101
  Duration: 00:00:14.00, start: 0.000000, bitrate: 416 kb/s
  Stream #0:0[0x1](und): Video: h264 (High 4:4:4 Predictive) (avc1 / 0x31637661), yuv444p(progressive), 1280x720, 387 kb/s, 20 fps, 20 tbr, 10240 tbn (default)
    Metadata:
      handler_name    : VideoHandler
      vendor_id       : [0][0][0][0]
  Stream #0:1[0x2](und): Audio: mp3 (mp4a / 0x6134706D), 16000 Hz, mono, fltp, 24 kb/s (default)
    Metadata:
      handler_name    : SoundHandler
      vendor_id       : [0][0][0][0]
  13.63 A-V: -0.031 fd=   0 aq=    0KB vq=    0KB sq=    0B f=0/0   

我们可以看到有这么一段

Stream #0:0[0x1](und): Video: h264 (High 4:4:4 Predictive) (avc1 / 0x31637661), yuv444p(progressive), 1280x720, 387 kb/s, 20 fps, 20 tbr, 10240 tbn (default)

这里有一个20 fps,说明该视频是每秒播放20桢

import cv2

if __name__ == "__main__":

    # 创建窗口
    cv2.namedWindow('video', cv2.WINDOW_NORMAL)
    cv2.resizeWindow('video', 640, 480)
    # 获取视频文件
    cap = cv2.VideoCapture("/Users/admin/Downloads/cockatoo.mp4")
    while True:
        # 从文件读视频桢
        ret, frame = cap.read()
        # 将视频帧在窗口中显示
        cv2.imshow('video', frame)
        # 此处不能设为1,否则会过快,可以设的比播放视频每秒帧数长一点
        key = cv2.waitKey(40)
        if key & 0xFF == ord('q'):
            break
    # 释放资源
    cap.release()
    cv2.destroyAllWindows()

运行结果

摄像头采集数据输出为媒体文件

import cv2

if __name__ == "__main__":

    fourcc = cv2.VideoWriter_fourcc(*'MJPG')
    # 25为帧率,(1280, 720)为分辨率,该分辨率必须与设备摄像头分辨率保持一致
    vw = cv2.VideoWriter("/Users/admin/Documents/out.mp4", fourcc, 25, (1280, 720))
    # 创建窗口
    cv2.namedWindow('video', cv2.WINDOW_NORMAL)
    cv2.resizeWindow('video', 640, 480)
    # 获取摄像头资源
    cap = cv2.VideoCapture(0)
    # 判断摄像头是否打开
    while cap.isOpened():
        # 从摄像头读视频桢
        ret, frame = cap.read()
        if ret:
            # 将视频帧在窗口中显示
            cv2.imshow('video', frame)
            # 写数据到多媒体文件
            vw.write(frame)
            key = cv2.waitKey(1)
            if key & 0xFF == ord('q'):
                break
        else:
            break
    # 释放资源
    cap.release()
    vw.release()
    cv2.destroyAllWindows()

运行结果

(base) -bash-3.2$ ls | grep out
out.mp4

控制鼠标

import cv2
import numpy as np

def mouse_callback(event, x, y, flags, userdata):
    print(event, x, y, flags, userdata)

if __name__ == "__main__":

    cv2.namedWindow('mouse', cv2.WINDOW_NORMAL)
    cv2.resizeWindow('mouse', 640, 360)
    # 设置鼠标回调
    cv2.setMouseCallback('mouse', mouse_callback, '123')
    # 设置背景为黑色
    img = np.zeros((360, 640, 3), np.uint8)
    while True:
        cv2.imshow('mouse', img)
        key = cv2.waitKey(1)
        if key & 0xFF == ord('q'):
            break
    cv2.destroyAllWindows()

运行结果

当鼠标在黑色区域滑动的时候,控制台会将鼠标的坐标给打印出来

0 272 156 0 123
0 272 155 0 123
0 272 155 0 123
0 271 155 0 123
0 271 155 0 123

TrackBar的使用

TrackBar就是一种滑动条,滑动到不同的位置,获取相应的值做不同的处理。

import cv2
import numpy as np

def callback():
    pass

if __name__ == "__main__":

    cv2.namedWindow('trackbar', cv2.WINDOW_NORMAL)
    # 创建trackbar,R是trackbar的名字,0是默认当前值,255是最大值
    cv2.createTrackbar('R', 'trackbar', 0, 255, callback)
    cv2.createTrackbar('G', 'trackbar', 0, 255, callback)
    cv2.createTrackbar('B', 'trackbar', 0, 255, callback)
    # 纯黑色背景
    img = np.zeros((480, 640, 3), np.uint8)
    while True:
        cv2.imshow('trackbar', img)
        r = cv2.getTrackbarPos('R', 'trackbar')
        g = cv2.getTrackbarPos('G', 'trackbar')
        b = cv2.getTrackbarPos('B', 'trackbar')
        img[:] = [b, g, r]
        key = cv2.waitKey(10)
        if key & 0xFF == ord('q'):
            break
    cv2.destroyAllWindows()

运行结果

trackbar取不同的值会有不同的背景色

OpenCV的色彩空间

RGB人眼的色彩空间

每一个像素有三种颜色——红色、绿色和蓝色。通过不同光源的组合,形成真彩色,有暗的,有明亮的。

上图中每一个方格都代表一个像素。

OpenCV默认使用的是BGR,BGR跟RGB的区别就是排列顺序的不同。电脑上一般的排列顺序都是RGB。

HSV/HSB/HSL

HSV代表的是色相、饱和度、明亮度。HSB和HSV是一个意思。

  1. Hue:色相,即色彩,如红色、蓝色
  2. Saturation:饱和度,颜色的纯度,值越大,纯度越高,最开始的时候是灰的,逐渐增大就纯度越高,如果是红色就是纯红,蓝色就是纯蓝
  3. Value:明度,代表更暗一些还是更亮一些,当更暗的时候,黑色的程度越高;更亮一些就黑色成分少一些。

该图中旋转一圈的过程中代表了不同的颜色。对于饱和度来说,以中心点为基础,底下是黑色,上面是白色,中间是黑与白之间的灰。越靠近于圆柱边缘的地方,颜色的纯度越高。而对于纵轴,底下是黑色的,越往上越来越亮,这个就是明亮度。

对于OpenCV来说更喜欢使用HSV,使用HSV在背景判断上要好过RGB,因为在一个背景中可能有各种绿色,使用HSV就可以统一将背景判断为绿色,而使用RGB就不太好判断,每一种成分都有。

判断背景是通过色度来进行判断的,上图中0度就是纯红,60度就是黄色,120度为绿色,180度为青色,240度为蓝色,300度为粉红。这里是不考虑从圆心到边缘的渐变的一些因素的。

HSL

  1. Hue:色相,即色彩,如红色、蓝色
  2. Saturation:饱和度
  3. Ligthness:亮度

HSL与HSV看起来差不多,但存在着不同。

这里左图是HSL的,右图是HSV的,对于HSL到最顶成的时候就是纯白,无论色相是什么,饱和度是什么。而HSV就没有这么夸张。我们基本上使用的都是HSV,HSL几乎是不使用的。

YUV

YUV主要用在视频领域。Y代表的是灰色图像,UV代表的是颜色。YUV来自于电视节目,以前的电视只有黑白电视,就只有这个Y,后来有了彩色电视,但是要兼容黑白电视剧,当彩色电视机播放黑白电视剧的时候就只播放这个Y。一般的YUV包含YUV4:2:0、YUV4:2:2、YUV4:4:4。

YUV4:2:0

上图中,4个Y对应2个U或者V。不同的间隔,U或者V都是不一定的。

色彩空间转换

import cv2

def callback():
    pass

if __name__ == "__main__":

    cv2.namedWindow('color', cv2.WINDOW_NORMAL)
    img = cv2.imread("/Users/admin/Documents/111.jpeg")
    colorspaces = [cv2.COLOR_BGR2RGBA, cv2.COLOR_BGR2BGRA, cv2.COLOR_BGR2GRAY,
                   cv2.COLOR_BGR2HSV_FULL, cv2.COLOR_BGR2YUV]
    cv2.createTrackbar('curcolor', 'color', 0, len(colorspaces) - 1, callback)
    while True:
        index = cv2.getTrackbarPos('curcolor', 'color')
        # 颜色空间转换
        cvt_img = cv2.cvtColor(img, colorspaces[index])
        cv2.imshow('color', cvt_img)
        key = cv2.waitKey(10)
        if key & 0xFF == ord('q'):
            break
    cv2.destroyAllWindows()

运行结果

trackbar取值为0的时候

trackbar取值为1的时候

trackbar取值为2的时候

trackbar取值为3的时候

trackbar取值为4的时候

ROI(Region of Image)

roi的意思是对图像的一个区域进行提取

import cv2

if __name__ == "__main__":

    cv2.namedWindow('roi', cv2.WINDOW_NORMAL)
    img = cv2.imread("/Users/admin/Documents/111.jpeg")
    print(img.shape)
    roi = img[0:550, 750:1350]
    while True:
        # 颜色空间转换
        cv2.imshow('roi', roi)
        key = cv2.waitKey()
        if key & 0xFF == ord('q'):
            break
    cv2.destroyAllWindows()

运行结果

(1080, 1920, 3)

我们这里需要注意的是img是一个numpy三维矩阵,它的第一个维度是图像的高,第二个维度是图像的宽,第三个维度是图像的通道数。

Mat

Mat就是矩阵,它的结构如下

对于Header头部来说,存放的是一些属性,包括维度、行数,列数。而Data是存放数据的地方,就是图像中的实际像素。总体如下

字段 说明 字段 说明
dims 维度 channels 通道数 RGB是3
rows 行数 size 矩阵大小
cols 列数 type dep+dt+chs CV_8UC3
depth 像素的位深 data 存放数据

Mat拷贝

这里Mat A是第一个创建的Mat,Mat B是拷贝Mat A,这里我们可以看到Mat A和Mat B的Header是两部分,而Data是它们公用的,也就是说Mat A和Mat B的Header的指针指向的是同一块内存空间。所以当我们用Mat B来拷贝Mat A的时候,默认情况下属于浅拷贝。有关深浅拷贝的概念请参考浅析克隆

import cv2

if __name__ == "__main__":

    cv2.namedWindow('img1', cv2.WINDOW_NORMAL)
    cv2.namedWindow('img2', cv2.WINDOW_NORMAL)
    img1 = cv2.imread("/Users/admin/Documents/111.jpeg")
    # 浅拷贝
    img2 = img1
    # 将图像左上角变成红色小方块
    img1[10:100, 10:100] = [0, 0, 255]
    while True:
        cv2.imshow('img1', img1)
        cv2.imshow('img2', img2)
        key = cv2.waitKey()
        if key & 0xFF == ord('q'):
            break
    cv2.destroyAllWindows()

运行结果

上面的两张图是两个不同的窗口,它们的左上角都有一小块红色的方块,说明,这种拷贝方式属于浅拷贝。

import cv2

if __name__ == "__main__":

    cv2.namedWindow('img1', cv2.WINDOW_NORMAL)
    cv2.namedWindow('img2', cv2.WINDOW_NORMAL)
    img1 = cv2.imread("/Users/admin/Documents/111.jpeg")
    # 深拷贝
    img2 = img1.copy()
    # 将图像左上角变成红色小方块
    img1[10:100, 10:100] = [0, 0, 255]
    while True:
        cv2.imshow('img1', img1)
        cv2.imshow('img2', img2)
        key = cv2.waitKey()
        if key & 0xFF == ord('q'):
            break
    cv2.destroyAllWindows()

运行结果

这里我们可以看到img2的左上角是没有红色小方块的,说明这是深拷贝。

Mat的属性

import cv2

if __name__ == "__main__":

    img = cv2.imread("/Users/admin/Documents/111.jpeg")
    print(img.shape)
    # 图像占用内存空间数,高度*宽度*通道数
    print(img.size)
    # 图像中每个元素的位深
    print(img.dtype)

运行结果

(1080, 1920, 3)
6220800
uint8

这里我们可以看到每个元素的类型是uint8,说明它是一个无符号8位整型,是从0~255的范围。

通道的分割与合并

这里我们需要明白一个概念,任何的单通道图像都是灰色的,而任何彩色图像都必须是三通道的。

import cv2

if __name__ == "__main__":

    cv2.namedWindow('img', cv2.WINDOW_NORMAL)
    img = cv2.imread("/Users/admin/Documents/111.jpeg")
    # 分割图像的三个通道
    b, g, r = cv2.split(img)
    while True:
        cv2.imshow('img', b)
        key = cv2.waitKey()
        if key & 0xFF == ord('q'):
            break
    cv2.destroyAllWindows()

运行结果

那么我们显示的是蓝色通道,为什么是黑白的呢?其实要显示蓝色通道的图像依然要合并另外两个通道,即红色通道和绿色通道,只不过这两个通道我们需要设置成纯黑。

import cv2
import numpy as np

if __name__ == "__main__":

    cv2.namedWindow('img', cv2.WINDOW_NORMAL)
    img = cv2.imread("/Users/admin/Documents/111.jpeg")
    # 分割图像的三个通道
    b, g, r = cv2.split(img)
    filt = np.zeros((1080, 1920), np.uint8)
    # 合并三个通道,但只保留蓝色通道信息
    imgnew = cv2.merge((b, filt, filt))
    while True:
        cv2.imshow('img', imgnew)
        key = cv2.waitKey()
        if key & 0xFF == ord('q'):
            break
    cv2.destroyAllWindows()

运行结果

如果我们要一张纯蓝色的图片呢?

import cv2
import numpy as np

if __name__ == "__main__":

    cv2.namedWindow('img', cv2.WINDOW_NORMAL)
    filt = np.zeros((1080, 1920), np.uint8)
    b = np.full((1080, 1920), 255, np.uint8)
    # 合并三个通道,但只保留蓝色通道信息
    imgnew = cv2.merge((b, filt, filt))
    while True:
        cv2.imshow('img', imgnew)
        key = cv2.waitKey()
        if key & 0xFF == ord('q'):
            break
    cv2.destroyAllWindows()

运行结果

对比两张图片,我们可以看到,在纯蓝图片中,蓝色通道中的所有像素值都是255,而从111.jpeg中蓝色通道的矩阵应该就是各不相同的像素大小最终显示出来的效果。

OpenCV图形绘制

画线

import cv2

if __name__ == "__main__":

    cv2.namedWindow('img', cv2.WINDOW_NORMAL)
    img = cv2.imread("/Users/admin/Documents/111.jpeg")
    # 画一条白线(750, 550)为起始点坐标,(1350, 550)为终止点坐标
    # (255, 255, 255)为颜色,用三通道表示,5为线宽
    cv2.line(img, (750, 550), (1350, 550), (255, 255, 255), 5)
    while True:
        cv2.imshow('img', img)
        key = cv2.waitKey()
        if key & 0xFF == ord('q'):
            break
    cv2.destroyAllWindows()

运行结果

现在我们来画一条斜线,并增加锯齿感

import cv2

if __name__ == "__main__":

    cv2.namedWindow('img', cv2.WINDOW_NORMAL)
    img = cv2.imread("/Users/admin/Documents/111.jpeg")
    # 画一条白线(750, 550)为起始点坐标,(1350, 550)为终止点坐标
    # (255, 255, 255)为颜色,用三通道表示,8为线宽,
    # 1为锯齿度,越小锯齿感越强,越大越平滑
    cv2.line(img, (759, 550), (1350, 900), (255, 255, 255), 8, 1)
    while True:
        cv2.imshow('img', img)
        key = cv2.waitKey()
        if key & 0xFF == ord('q'):
            break
    cv2.destroyAllWindows()

运行结果

在视频中画线

import cv2

if __name__ == "__main__":

    cv2.namedWindow('video', cv2.WINDOW_NORMAL)
    cv2.resizeWindow('video', 640, 480)
    # 获取视频文件
    cap = cv2.VideoCapture("/Users/admin/Downloads/cockatoo.mp4")
    while True:
        # 从文件读视频桢
        ret, frame = cap.read()
        if ret:
            cv2.line(frame, (0, 600), (1280, 600), (0, 0, 255), 5)
            # 将视频帧在窗口中显示
            cv2.imshow('video', frame)
            # 此处不能设为1,否则会过快,可以设的比播放视频每秒帧数长一点
            key = cv2.waitKey(40)
            if key & 0xFF == ord('q'):
                break
        else:
            key = cv2.waitKey(40)
            if key & 0xFF == ord('q'):
                break
    # 释放资源
    cap.release()
    cv2.destroyAllWindows()

运行结果

画矩形

import cv2

if __name__ == "__main__":

    cv2.namedWindow('img', cv2.WINDOW_NORMAL)
    img = cv2.imread("/Users/admin/Documents/111.jpeg")
    # 画一条白线(750, 550)为起始点坐标,(1350, 550)为终止点坐标
    # (255, 255, 255)为颜色,用三通道表示,8为线宽,
    # 1为锯齿度,越小锯齿感越强,越大越平滑
    # cv2.line(img, (759, 550), (1350, 900), (255, 255, 255), 8, 1)
    # 画一个红框(750, 0)为起始点坐标,(1350, 550)为终止点坐标
    cv2.rectangle(img, (750, 0), (1350, 550), (0, 0, 255), 8)
    while True:
        cv2.imshow('img', img)
        key = cv2.waitKey()
        if key & 0xFF == ord('q'):
            break
    cv2.destroyAllWindows()

运行结果

画实心矩形

import cv2

if __name__ == "__main__":

    cv2.namedWindow('img', cv2.WINDOW_NORMAL)
    img = cv2.imread("/Users/admin/Documents/111.jpeg")
    # 画一条白线(750, 550)为起始点坐标,(1350, 550)为终止点坐标
    # (255, 255, 255)为颜色,用三通道表示,8为线宽,
    # 1为锯齿度,越小锯齿感越强,越大越平滑
    # cv2.line(img, (759, 550), (1350, 900), (255, 255, 255), 8, 1)
    # 画一个红框(750, 0)为起始点坐标,(1350, 550)为终止点坐标
    # -1表示线宽无限大,即为实心
    cv2.rectangle(img, (750, 0), (1350, 550), (0, 0, 255), -1)
    while True:
        cv2.imshow('img', img)
        key = cv2.waitKey()
        if key & 0xFF == ord('q'):
            break
    cv2.destroyAllWindows()

运行结果

画圆

import cv2

if __name__ == "__main__":

    cv2.namedWindow('img', cv2.WINDOW_NORMAL)
    img = cv2.imread("/Users/admin/Documents/111.jpeg")
    # 画一条白线(750, 550)为起始点坐标,(1350, 550)为终止点坐标
    # (255, 255, 255)为颜色,用三通道表示,8为线宽,
    # 1为锯齿度,越小锯齿感越强,越大越平滑
    # cv2.line(img, (759, 550), (1350, 900), (255, 255, 255), 8, 1)
    # 画一个红框(750, 0)为起始点坐标,(1350, 550)为终止点坐标
    # -1表示线宽无限大,即为实心
    # cv2.rectangle(img, (750, 0), (1350, 550), (0, 0, 255), -1)
    # 画一个红色的圆,(1050, 275)为圆心坐标,275为半径,5为线宽,16的锯齿度数
    cv2.circle(img, (1050, 275), 275, (0, 0, 255), 5, 16)
    while True:
        cv2.imshow('img', img)
        key = cv2.waitKey()
        if key & 0xFF == ord('q'):
            break
    cv2.destroyAllWindows()

运行结果

画实心圆

import cv2

if __name__ == "__main__":

    cv2.namedWindow('img', cv2.WINDOW_NORMAL)
    img = cv2.imread("/Users/admin/Documents/111.jpeg")
    # 画一条白线(750, 550)为起始点坐标,(1350, 550)为终止点坐标
    # (255, 255, 255)为颜色,用三通道表示,8为线宽,
    # 1为锯齿度,越小锯齿感越强,越大越平滑
    # cv2.line(img, (759, 550), (1350, 900), (255, 255, 255), 8, 1)
    # 画一个红框(750, 0)为起始点坐标,(1350, 550)为终止点坐标
    # -1表示线宽无限大,即为实心
    # cv2.rectangle(img, (750, 0), (1350, 550), (0, 0, 255), -1)
    # 画一个红色的圆,(1050, 275)为圆心坐标,275为半径,-1为线宽无限大,即为实心,16的锯齿度数
    cv2.circle(img, (1050, 275), 275, (0, 0, 255), -1, 16)
    while True:
        cv2.imshow('img', img)
        key = cv2.waitKey()
        if key & 0xFF == ord('q'):
            break
    cv2.destroyAllWindows()

运行结果

画椭圆

import cv2

if __name__ == "__main__":

    cv2.namedWindow('img', cv2.WINDOW_NORMAL)
    img = cv2.imread("/Users/admin/Documents/111.jpeg")
    # 画一条白线(750, 550)为起始点坐标,(1350, 550)为终止点坐标
    # (255, 255, 255)为颜色,用三通道表示,8为线宽,
    # 1为锯齿度,越小锯齿感越强,越大越平滑
    # cv2.line(img, (759, 550), (1350, 900), (255, 255, 255), 8, 1)
    # 画一个红框(750, 0)为起始点坐标,(1350, 550)为终止点坐标
    # -1表示线宽无限大,即为实心
    # cv2.rectangle(img, (750, 0), (1350, 550), (0, 0, 255), -1)
    # 画一个红色的圆,(1050, 275)为圆心坐标,275为半径,-1为线宽无限大,即为实心,16的锯齿度数
    # cv2.circle(img, (1050, 275), 275, (0, 0, 255), -1, 16)
    # 画一个红色的椭圆,(1050, 275)为中心点坐标,(500, 275)为长宽的一半
    # 第一个0为长方体角度起始值,第二个0为长方体角度终止值
    # 360是椭圆的画线部分的度数
    cv2.ellipse(img, (1050, 275), (500, 275), 0, 0, 360, (0, 0, 255), 5, 16)
    while True:
        cv2.imshow('img', img)
        key = cv2.waitKey()
        if key & 0xFF == ord('q'):
            break
    cv2.destroyAllWindows()

运行结果

如果我们要绘制椭圆的下半部分

import cv2

if __name__ == "__main__":

    cv2.namedWindow('img', cv2.WINDOW_NORMAL)
    img = cv2.imread("/Users/admin/Documents/111.jpeg")
    # 画一条白线(750, 550)为起始点坐标,(1350, 550)为终止点坐标
    # (255, 255, 255)为颜色,用三通道表示,8为线宽,
    # 1为锯齿度,越小锯齿感越强,越大越平滑
    # cv2.line(img, (759, 550), (1350, 900), (255, 255, 255), 8, 1)
    # 画一个红框(750, 0)为起始点坐标,(1350, 550)为终止点坐标
    # -1表示线宽无限大,即为实心
    # cv2.rectangle(img, (750, 0), (1350, 550), (0, 0, 255), -1)
    # 画一个红色的圆,(1050, 275)为圆心坐标,275为半径,-1为线宽无限大,即为实心,16的锯齿度数
    # cv2.circle(img, (1050, 275), 275, (0, 0, 255), -1, 16)
    # 画一个红色的椭圆,(1050, 275)为中心点坐标,(500, 275)为长宽的一半
    # 第一个0为长方体角度起始值,第二个0为长方体角度终止值
    # 180是椭圆的画线部分的度数
    cv2.ellipse(img, (1050, 275), (500, 275), 0, 0, 180, (0, 0, 255), 5, 16)
    while True:
        cv2.imshow('img', img)
        key = cv2.waitKey()
        if key & 0xFF == ord('q'):
            break
    cv2.destroyAllWindows()

运行结果

如果我们要绘制椭圆的上半部分

import cv2

if __name__ == "__main__":

    cv2.namedWindow('img', cv2.WINDOW_NORMAL)
    img = cv2.imread("/Users/admin/Documents/111.jpeg")
    # 画一条白线(750, 550)为起始点坐标,(1350, 550)为终止点坐标
    # (255, 255, 255)为颜色,用三通道表示,8为线宽,
    # 1为锯齿度,越小锯齿感越强,越大越平滑
    # cv2.line(img, (759, 550), (1350, 900), (255, 255, 255), 8, 1)
    # 画一个红框(750, 0)为起始点坐标,(1350, 550)为终止点坐标
    # -1表示线宽无限大,即为实心
    # cv2.rectangle(img, (750, 0), (1350, 550), (0, 0, 255), -1)
    # 画一个红色的圆,(1050, 275)为圆心坐标,275为半径,-1为线宽无限大,即为实心,16的锯齿度数
    # cv2.circle(img, (1050, 275), 275, (0, 0, 255), -1, 16)
    # 画一个红色的椭圆,(1050, 275)为中心点坐标,(500, 275)为长宽的一半
    # 第一个0为长方体角度起始值,第二个0为长方体角度终止值
    # 180是椭圆的画线部分的度数
    cv2.ellipse(img, (1050, 275), (500, 275), 180, 0, 180, (0, 0, 255), 5, 16)
    while True:
        cv2.imshow('img', img)
        key = cv2.waitKey()
        if key & 0xFF == ord('q'):
            break
    cv2.destroyAllWindows()

运行结果

如果我们要绘制一些斜的椭圆

import cv2

if __name__ == "__main__":

    cv2.namedWindow('img', cv2.WINDOW_NORMAL)
    img = cv2.imread("/Users/admin/Documents/111.jpeg")
    # 画一条白线(750, 550)为起始点坐标,(1350, 550)为终止点坐标
    # (255, 255, 255)为颜色,用三通道表示,8为线宽,
    # 1为锯齿度,越小锯齿感越强,越大越平滑
    # cv2.line(img, (759, 550), (1350, 900), (255, 255, 255), 8, 1)
    # 画一个红框(750, 0)为起始点坐标,(1350, 550)为终止点坐标
    # -1表示线宽无限大,即为实心
    # cv2.rectangle(img, (750, 0), (1350, 550), (0, 0, 255), -1)
    # 画一个红色的圆,(1050, 275)为圆心坐标,275为半径,-1为线宽无限大,即为实心,16的锯齿度数
    # cv2.circle(img, (1050, 275), 275, (0, 0, 255), -1, 16)
    # 画一个红色的椭圆,(1050, 275)为中心点坐标,(500, 275)为长宽的一半
    # 第一个0为长方体角度起始值,第二个0为长方体角度终止值
    # 180是椭圆的画线部分的度数
    cv2.ellipse(img, (1050, 275), (500, 275), 60, 0, 360, (0, 0, 255), 5, 16)
    while True:
        cv2.imshow('img', img)
        key = cv2.waitKey()
        if key & 0xFF == ord('q'):
            break
    cv2.destroyAllWindows()

运行结果

如果我们要绘制一个与第一个椭圆垂直的椭圆

import cv2

if __name__ == "__main__":

    cv2.namedWindow('img', cv2.WINDOW_NORMAL)
    img = cv2.imread("/Users/admin/Documents/111.jpeg")
    # 画一条白线(750, 550)为起始点坐标,(1350, 550)为终止点坐标
    # (255, 255, 255)为颜色,用三通道表示,8为线宽,
    # 1为锯齿度,越小锯齿感越强,越大越平滑
    # cv2.line(img, (759, 550), (1350, 900), (255, 255, 255), 8, 1)
    # 画一个红框(750, 0)为起始点坐标,(1350, 550)为终止点坐标
    # -1表示线宽无限大,即为实心
    # cv2.rectangle(img, (750, 0), (1350, 550), (0, 0, 255), -1)
    # 画一个红色的圆,(1050, 275)为圆心坐标,275为半径,-1为线宽无限大,即为实心,16的锯齿度数
    # cv2.circle(img, (1050, 275), 275, (0, 0, 255), -1, 16)
    # 画一个红色的椭圆,(1050, 275)为中心点坐标,(500, 275)为长宽的一半
    # 第一个0为长方体角度起始值,第二个0为长方体角度终止值
    # 180是椭圆的画线部分的度数
    cv2.ellipse(img, (1050, 275), (500, 275), 90, 0, 360, (0, 0, 255), 5, 16)
    while True:
        cv2.imshow('img', img)
        key = cv2.waitKey()
        if key & 0xFF == ord('q'):
            break
    cv2.destroyAllWindows()

运行结果

如果我们要画一个实体扇形

import cv2

if __name__ == "__main__":

    cv2.namedWindow('img', cv2.WINDOW_NORMAL)
    img = cv2.imread("/Users/admin/Documents/111.jpeg")
    # 画一条白线(750, 550)为起始点坐标,(1350, 550)为终止点坐标
    # (255, 255, 255)为颜色,用三通道表示,8为线宽,
    # 1为锯齿度,越小锯齿感越强,越大越平滑
    # cv2.line(img, (759, 550), (1350, 900), (255, 255, 255), 8, 1)
    # 画一个红框(750, 0)为起始点坐标,(1350, 550)为终止点坐标
    # -1表示线宽无限大,即为实心
    # cv2.rectangle(img, (750, 0), (1350, 550), (0, 0, 255), -1)
    # 画一个红色的圆,(1050, 275)为圆心坐标,275为半径,-1为线宽无限大,即为实心,16的锯齿度数
    # cv2.circle(img, (1050, 275), 275, (0, 0, 255), -1, 16)
    # 画一个红色的椭圆,(1050, 275)为中心点坐标,(500, 275)为长宽的一半
    # 第一个0为长方体角度起始值,第二个0为长方体角度终止值
    # 180是椭圆的画线部分的度数
    cv2.ellipse(img, (1050, 275), (500, 275), 120, 330, 360, (0, 0, 255), -1, 16)
    while True:
        cv2.imshow('img', img)
        key = cv2.waitKey()
        if key & 0xFF == ord('q'):
            break
    cv2.destroyAllWindows()

运行结果

画多边形

import cv2
import numpy as np

if __name__ == "__main__":

    cv2.namedWindow('img', cv2.WINDOW_NORMAL)
    img = cv2.imread("/Users/admin/Documents/111.jpeg")
    # 画一条白线(750, 550)为起始点坐标,(1350, 550)为终止点坐标
    # (255, 255, 255)为颜色,用三通道表示,8为线宽,
    # 1为锯齿度,越小锯齿感越强,越大越平滑
    # cv2.line(img, (759, 550), (1350, 900), (255, 255, 255), 8, 1)
    # 画一个红框(750, 0)为起始点坐标,(1350, 550)为终止点坐标
    # -1表示线宽无限大,即为实心
    # cv2.rectangle(img, (750, 0), (1350, 550), (0, 0, 255), -1)
    # 画一个红色的圆,(1050, 275)为圆心坐标,275为半径,-1为线宽无限大,即为实心,16的锯齿度数
    # cv2.circle(img, (1050, 275), 275, (0, 0, 255), -1, 16)
    # 画一个红色的椭圆,(1050, 275)为中心点坐标,(500, 275)为长宽的一半
    # 第一个0为长方体角度起始值,第二个0为长方体角度终止值
    # 180是椭圆的画线部分的度数
    # cv2.ellipse(img, (1050, 275), (500, 275), 120, 330, 360, (0, 0, 255), -1, 16)
    pts = np.array([(750, 550), (1350, 550), (1050, 0)], np.int32)
    # 画一个多边形(三角形),pts是三个点的坐标
    cv2.polylines(img, [pts], True, (0, 0, 255), 8, 16)
    while True:
        cv2.imshow('img', img)
        key = cv2.waitKey()
        if key & 0xFF == ord('q'):
            break
    cv2.destroyAllWindows()

运行结果

画实心多边形

import cv2
import numpy as np

if __name__ == "__main__":

    cv2.namedWindow('img', cv2.WINDOW_NORMAL)
    img = cv2.imread("/Users/admin/Documents/111.jpeg")
    # 画一条白线(750, 550)为起始点坐标,(1350, 550)为终止点坐标
    # (255, 255, 255)为颜色,用三通道表示,8为线宽,
    # 1为锯齿度,越小锯齿感越强,越大越平滑
    # cv2.line(img, (759, 550), (1350, 900), (255, 255, 255), 8, 1)
    # 画一个红框(750, 0)为起始点坐标,(1350, 550)为终止点坐标
    # -1表示线宽无限大,即为实心
    # cv2.rectangle(img, (750, 0), (1350, 550), (0, 0, 255), -1)
    # 画一个红色的圆,(1050, 275)为圆心坐标,275为半径,-1为线宽无限大,即为实心,16的锯齿度数
    # cv2.circle(img, (1050, 275), 275, (0, 0, 255), -1, 16)
    # 画一个红色的椭圆,(1050, 275)为中心点坐标,(500, 275)为长宽的一半
    # 第一个0为长方体角度起始值,第二个0为长方体角度终止值
    # 180是椭圆的画线部分的度数
    # cv2.ellipse(img, (1050, 275), (500, 275), 120, 330, 360, (0, 0, 255), -1, 16)
    pts = np.array([(750, 550), (1350, 550), (1050, 0)], np.int32)
    # 画一个多边形(三角形),pts是三个点的坐标
    # cv2.polylines(img, [pts], True, (0, 0, 255), 8, 16)
    # 画一个实心多边形(三角形),pts是三个点的坐标
    cv2.fillPoly(img, [pts], (0, 0, 255))
    while True:
        cv2.imshow('img', img)
        key = cv2.waitKey()
        if key & 0xFF == ord('q'):
            break
    cv2.destroyAllWindows()

运行结果

这里需要注意的是,画实心多边形和普通多边形是两个不同的API。

绘制文本

import cv2
import numpy as np

if __name__ == "__main__":

    cv2.namedWindow('img', cv2.WINDOW_NORMAL)
    img = cv2.imread("/Users/admin/Documents/111.jpeg")
    # 画一条白线(750, 550)为起始点坐标,(1350, 550)为终止点坐标
    # (255, 255, 255)为颜色,用三通道表示,8为线宽,
    # 1为锯齿度,越小锯齿感越强,越大越平滑
    # cv2.line(img, (759, 550), (1350, 900), (255, 255, 255), 8, 1)
    # 画一个红框(750, 0)为起始点坐标,(1350, 550)为终止点坐标
    # -1表示线宽无限大,即为实心
    # cv2.rectangle(img, (750, 0), (1350, 550), (0, 0, 255), -1)
    # 画一个红色的圆,(1050, 275)为圆心坐标,275为半径,-1为线宽无限大,即为实心,16的锯齿度数
    # cv2.circle(img, (1050, 275), 275, (0, 0, 255), -1, 16)
    # 画一个红色的椭圆,(1050, 275)为中心点坐标,(500, 275)为长宽的一半
    # 第一个0为长方体角度起始值,第二个0为长方体角度终止值
    # 180是椭圆的画线部分的度数
    # cv2.ellipse(img, (1050, 275), (500, 275), 120, 330, 360, (0, 0, 255), -1, 16)
    # pts = np.array([(750, 550), (1350, 550), (1050, 0)], np.int32)
    # 画一个多边形(三角形),pts是三个点的坐标
    # cv2.polylines(img, [pts], True, (0, 0, 255), 8, 16)
    # 画一个实心多边形(三角形),pts是三个点的坐标
    # cv2.fillPoly(img, [pts], (0, 0, 255))
    # 绘制一段文本,(750, 275)为起始坐标,cv2.FONT_ITALIC为字体,8为字体大小,(0, 0, 0)为黑色,10为字体粗细
    cv2.putText(img, "Beauty", (750, 275), cv2.FONT_ITALIC, 8, (0, 0, 0), 10)
    while True:
        cv2.imshow('img', img)
        key = cv2.waitKey()
        if key & 0xFF == ord('q'):
            break
    cv2.destroyAllWindows()

运行结果

鼠标绘制图形

import cv2
import math

if __name__ == "__main__":

    curshape = 0
    startpos = (0, 0)

    def mouse_callback(event, x, y, flags, userdata):
        global startpos
        if event & cv2.EVENT_LBUTTONDOWN == cv2.EVENT_LBUTTONDOWN:
            startpos = (x, y)
        elif event & cv2.EVENT_LBUTTONUP == cv2.EVENT_LBUTTONUP:
            if curshape == 0:
                cv2.line(img, startpos, (x, y), (0, 0, 255), 8, 16)
            elif curshape == 1:
                cv2.rectangle(img, startpos, (x, y), (0, 0, 255), 8, 16)
            elif curshape == 2:
                a = x - startpos[0]
                b = y - startpos[1]
                r = int(math.sqrt(a**2 + b**2))
                cv2.circle(img, startpos, r, (0, 0, 255), 8, 16)
            else:
                print("无效类型")

    cv2.namedWindow('drawshape', cv2.WINDOW_NORMAL)
    cv2.setMouseCallback('drawshape', mouse_callback, "123")
    img = cv2.imread("/Users/admin/Documents/111.jpeg")
    while True:
        cv2.imshow('drawshape', img)
        key = cv2.waitKey(1) & 0xFF
        if key == ord('q'):
            break
        elif key == ord('l'):
            # 画线
            curshape = 0
        elif key == ord('r'):
            # 画矩形
            curshape = 1
        elif key == ord('c'):
            # 画圆
            curshape = 2
    cv2.destroyAllWindows()

运行结果

这上面的形状都是用鼠标绘制出来的。

OpenCV的算术与位运算

图像加法运算

import cv2
import numpy as np

if __name__ == "__main__":

    cv2.namedWindow('imgadd', cv2.WINDOW_NORMAL)
    img1 = cv2.imread("/Users/admin/Documents/111.jpeg")
    img2 = np.full((1080, 1920, 3), 50, np.uint8)
    result = cv2.add(img1, img2)
    while True:
        cv2.imshow('imgadd', result)
        key = cv2.waitKey()
        if key & 0xFF == ord('q'):
            break
    cv2.destroyAllWindows()

运行结果

这里我们可以看到图像好像是增加了曝光。

图像减法运算

import cv2
import numpy as np

if __name__ == "__main__":

    cv2.namedWindow('imgsub', cv2.WINDOW_NORMAL)
    img1 = cv2.imread("/Users/admin/Documents/111.jpeg")
    img2 = np.full((1080, 1920, 3), 100, np.uint8)
    result = cv2.subtract(img1, img2)
    while True:
        cv2.imshow('imgsub', result)
        key = cv2.waitKey()
        if key & 0xFF == ord('q'):
            break
    cv2.destroyAllWindows()

运行结果

这里需要注意的是,图像的减法运算,两个参数的位置不可调换,但是加法就没有这个要求。上图中就像是原图得到了锐化。

图像乘法运算

import cv2
import numpy as np

if __name__ == "__main__":

    cv2.namedWindow('imgmul', cv2.WINDOW_NORMAL)
    img1 = cv2.imread("/Users/admin/Documents/111.jpeg")
    img2 = np.full((1080, 1920, 3), 2, np.uint8)
    result = cv2.multiply(img1, img2)
    while True:
        cv2.imshow('imgmul', result)
        key = cv2.waitKey()
        if key & 0xFF == ord('q'):
            break
    cv2.destroyAllWindows()

运行结果

图像的乘法运算如果乘的像素点太大,基本上整个图像就看不清楚了。

图像除法运算

import cv2
import numpy as np

if __name__ == "__main__":

    cv2.namedWindow('imgdiv', cv2.WINDOW_NORMAL)
    img1 = cv2.imread("/Users/admin/Documents/111.jpeg")
    img2 = np.full((1080, 1920, 3), 3, np.uint8)
    result = cv2.divide(img1, img2)
    while True:
        cv2.imshow('imgdiv', result)
        key = cv2.waitKey()
        if key & 0xFF == ord('q'):
            break
    cv2.destroyAllWindows()

运行结果

 图像融合

我们这里有一张新的图片

现在我们要将这张图片与之前的美女的图片进行融合。

import cv2

if __name__ == "__main__":

    cv2.namedWindow('imgadd', cv2.WINDOW_NORMAL)
    img1 = cv2.imread("/Users/admin/Documents/111.jpeg")
    img2 = cv2.imread("/Users/admin/Documents/222.jpeg")
    print(img1.shape)
    print(img2.shape)
    # 融合两张图片,0.3和0.7分别表示两张图片融合的权重,0表示融合
    # 后的图片的所有元素都加0,表示静态权重
    result = cv2.addWeighted(img1, 0.3, img2, 0.7, 0)
    while True:
        cv2.imshow('imgadd', result)
        key = cv2.waitKey()
        if key & 0xFF == ord('q'):
            break
    cv2.destroyAllWindows()

运行结果

(1080, 1920, 3)
(1080, 1920, 3)

这里需要注意的是,要融合的两张图片的高和宽以及通道数必须相等,才可以进行融合。这里两张图片都是1080*1920*3的图像。

图像位运算

  • 非运算

非运算就是将图像中的像素进行255-x操作(x为原像素值)

import cv2

if __name__ == "__main__":

    cv2.namedWindow('imgbit', cv2.WINDOW_NORMAL)
    img = cv2.imread("/Users/admin/Documents/111.jpeg")
    new_img = cv2.bitwise_not(img)
    while True:
        cv2.imshow('imgbit', new_img)
        key = cv2.waitKey()
        if key & 0xFF == ord('q'):
            break
    cv2.destroyAllWindows()

运行结果

这个有点像彩色照片的底片

import cv2
import numpy as np

if __name__ == "__main__":

    cv2.namedWindow('imgbit', cv2.WINDOW_NORMAL)
    img1 = cv2.imread("/Users/admin/Documents/111.jpeg")
    img2 = np.full((1080, 1920, 3), 255, np.uint8)
    new_img = cv2.subtract(img2, img1)
    while True:
        cv2.imshow('imgbit', new_img)
        key = cv2.waitKey()
        if key & 0xFF == ord('q'):
            break
    cv2.destroyAllWindows()

使用255-原图像像素值与非运算效果一样

  • 与运算

与运算就是将图像各个通道像素点值转成二进制数按位与进行运算。

import cv2

if __name__ == "__main__":

    cv2.namedWindow('imgbit', cv2.WINDOW_NORMAL)
    img1 = cv2.imread("/Users/admin/Documents/111.jpeg")
    img2 = cv2.imread("/Users/admin/Documents/222.jpeg")
    new_img = cv2.bitwise_and(img1, img2)
    while True:
        cv2.imshow('imgbit', new_img)
        key = cv2.waitKey()
        if key & 0xFF == ord('q'):
            break
    cv2.destroyAllWindows()

运行结果

上面的代码等同与

import cv2
import numpy as np

if __name__ == "__main__":

    cv2.namedWindow('imgbit', cv2.WINDOW_NORMAL)
    img1 = cv2.imread("/Users/admin/Documents/111.jpeg")
    img2 = cv2.imread("/Users/admin/Documents/222.jpeg")
    result = np.bitwise_and(img1, img2)
    while True:
        cv2.imshow('imgbit', result)
        key = cv2.waitKey()
        if key & 0xFF == ord('q'):
            break
    cv2.destroyAllWindows()
  • 或运算

或运算就是将图像各个通道像素点值转成二进制数按位或进行运算。

import cv2

if __name__ == "__main__":

    cv2.namedWindow('imgbit', cv2.WINDOW_NORMAL)
    img1 = cv2.imread("/Users/admin/Documents/111.jpeg")
    img2 = cv2.imread("/Users/admin/Documents/222.jpeg")
    new_img = cv2.bitwise_or(img1, img2)
    while True:
        cv2.imshow('imgbit', new_img)
        key = cv2.waitKey()
        if key & 0xFF == ord('q'):
            break
    cv2.destroyAllWindows()

运行结果

上面的代码等同于

import cv2
import numpy as np

if __name__ == "__main__":

    cv2.namedWindow('imgbit', cv2.WINDOW_NORMAL)
    img1 = cv2.imread("/Users/admin/Documents/111.jpeg")
    img2 = cv2.imread("/Users/admin/Documents/222.jpeg")
    result = np.bitwise_or(img1, img2)
    while True:
        cv2.imshow('imgbit', result)
        key = cv2.waitKey()
        if key & 0xFF == ord('q'):
            break
    cv2.destroyAllWindows()
  • 异或运算

异或运算就是将图像各个通道像素点值转成二进制数按位异或进行运算。

import cv2

if __name__ == "__main__":

    cv2.namedWindow('imgbit', cv2.WINDOW_NORMAL)
    img1 = cv2.imread("/Users/admin/Documents/111.jpeg")
    img2 = cv2.imread("/Users/admin/Documents/222.jpeg")
    new_img = cv2.bitwise_xor(img1, img2)
    while True:
        cv2.imshow('imgbit', new_img)
        key = cv2.waitKey()
        if key & 0xFF == ord('q'):
            break
    cv2.destroyAllWindows()

运行结果

上面的代码等同于

import cv2
import numpy as np

if __name__ == "__main__":

    cv2.namedWindow('imgbit', cv2.WINDOW_NORMAL)
    img1 = cv2.imread("/Users/admin/Documents/111.jpeg")
    img2 = cv2.imread("/Users/admin/Documents/222.jpeg")
    result = np.bitwise_xor(img1, img2)
    while True:
        cv2.imshow('imgbit', result)
        key = cv2.waitKey()
        if key & 0xFF == ord('q'):
            break
    cv2.destroyAllWindows()

添加水印

我们先把logo给画出来,看看是什么样子的

import cv2
import numpy as np

if __name__ == "__main__":

    cv2.namedWindow('imlog', cv2.WINDOW_NORMAL)
    img1 = cv2.imread("/Users/admin/Documents/111.jpeg")
    logo = np.zeros((200, 200, 3), np.uint8)
    mask = np.zeros((200, 200), np.uint8)

    logo[20:120, 20:120] = [0, 0, 255]
    logo[80:180, 80:180] = [0, 255, 0]
    while True:
        cv2.imshow('imlog', logo)
        key = cv2.waitKey()
        if key & 0xFF == ord('q'):
            break
    cv2.destroyAllWindows()

运行结果

现在我们要将该logo放入到图片里面去

import cv2
import numpy as np

if __name__ == "__main__":

    cv2.namedWindow('imlog', cv2.WINDOW_NORMAL)
    # 导入图片
    img = cv2.imread("/Users/admin/Documents/111.jpeg")
    # 创建logo和mask
    logo = np.zeros((200, 200, 3), np.uint8)
    mask = np.full((200, 200), 255, np.uint8)
    # 绘制logo
    logo[20:120, 20:120] = [0, 0, 255]  # 该区域为红色
    logo[80:180, 80:180] = [0, 255, 0]  # 该区域为绿色
    mask[20:120, 20:120] = 0  # 该区域为黑色
    mask[80:180, 80:180] = 0  # 该区域为黑色

    # 选择图像添加logo的位置,并提取该部分的图像像素(3通道)
    roi = img[0:200, 0:200]
    # 对mask进行位与操作(0与任何值位与都是0,即为黑色,而任何值与255(二进制11111111))
    # 进行位与操作都是该值本身
    # 这里roi是三通道,而mask为单通道,
    # 则这里为roi的每个通道都与mask进行位与操作
    # 最终roi的位置与mask为0的部分变为0,其他部分保留roi其像素本身
    tmp = cv2.bitwise_and(roi, roi, mask=mask)
    # 0加任何数等于任何数,所以这里roi为0的位置变成logo的像素
    # 而logo为0的部分保留roi的像素值
    dst = cv2.add(tmp, logo)
    # 将得到的图像放入原始图像中
    img[0:200, 0:200] = dst
    while True:
        cv2.imshow('imlog', img)
        key = cv2.waitKey()
        if key & 0xFF == ord('q'):
            break
    cv2.destroyAllWindows()

运行结果

图像基本变换

图像放大与缩小

import cv2

if __name__ == "__main__":

    cv2.namedWindow('img', cv2.WINDOW_NORMAL)
    img = cv2.imread("/Users/admin/Documents/111.jpeg")
    new_img = cv2.resize(img, (200, 200))
    while True:
        cv2.imshow('img', new_img)
        key = cv2.waitKey()
        if key & 0xFF == ord('q'):
            break
    cv2.destroyAllWindows()

运行结果

我们可以看到在进行缩小的过程中,图像是有失真的。我们也可以直接使用比例缩放

import cv2

if __name__ == "__main__":

    cv2.namedWindow('img', cv2.WINDOW_NORMAL)
    img = cv2.imread("/Users/admin/Documents/111.jpeg")
    new_img = cv2.resize(img, None, fx=0.3, fy=0.3)
    while True:
        cv2.imshow('img', new_img)
        key = cv2.waitKey()
        if key & 0xFF == ord('q'):
            break
    cv2.destroyAllWindows()

运行结果

我们来看一下一张小图的放大会是什么效果,原图如下

import cv2

if __name__ == "__main__":

    cv2.namedWindow('img', cv2.WINDOW_AUTOSIZE)
    img = cv2.imread("/Users/admin/Documents/333.jpeg")
    # 图像缩放,cv2.INTER_AREA表示效果最好
    # cv2.INTER_NEAREST表示临近插值
    new_img = cv2.resize(img, None, fx=3, fy=3, interpolation=cv2.INTER_AREA)
    while True:
        cv2.imshow('img', new_img)
        key = cv2.waitKey()
        if key & 0xFF == ord('q'):
            break
    cv2.destroyAllWindows()

运行结果

图像翻转

上下翻转

import cv2

if __name__ == "__main__":

    cv2.namedWindow('img', cv2.WINDOW_NORMAL)
    img = cv2.imread("/Users/admin/Documents/111.jpeg")
    # 上下翻转
    new_img = cv2.flip(img, 0)
    while True:
        cv2.imshow('img', new_img)
        key = cv2.waitKey()
        if key & 0xFF == ord('q'):
            break
    cv2.destroyAllWindows()

运行结果

左右翻转

import cv2

if __name__ == "__main__":

    cv2.namedWindow('img', cv2.WINDOW_NORMAL)
    img = cv2.imread("/Users/admin/Documents/111.jpeg")
    # 0上下翻转,大于0左右翻转
    new_img = cv2.flip(img, 1)
    while True:
        cv2.imshow('img', new_img)
        key = cv2.waitKey()
        if key & 0xFF == ord('q'):
            break
    cv2.destroyAllWindows()

运行结果

上下+左右翻转

import cv2

if __name__ == "__main__":

    cv2.namedWindow('img', cv2.WINDOW_NORMAL)
    img = cv2.imread("/Users/admin/Documents/111.jpeg")
    # 0上下翻转,大于0左右翻转,小于0上下+左右翻转
    new_img = cv2.flip(img, -1)
    while True:
        cv2.imshow('img', new_img)
        key = cv2.waitKey()
        if key & 0xFF == ord('q'):
            break
    cv2.destroyAllWindows()

运行结果

图像旋转

import cv2

if __name__ == "__main__":

    cv2.namedWindow('img', cv2.WINDOW_NORMAL)
    img = cv2.imread("/Users/admin/Documents/111.jpeg")
    # 0上下翻转,大于0左右翻转,小于0上下+左右翻转
    # new_img = cv2.flip(img, -1)
    # 顺时针旋转90度
    new_img = cv2.rotate(img, cv2.ROTATE_90_CLOCKWISE)
    while True:
        cv2.imshow('img', new_img)
        key = cv2.waitKey()
        if key & 0xFF == ord('q'):
            break
    cv2.destroyAllWindows()

运行结果

import cv2

if __name__ == "__main__":

    cv2.namedWindow('img', cv2.WINDOW_NORMAL)
    img = cv2.imread("/Users/admin/Documents/111.jpeg")
    # 0上下翻转,大于0左右翻转,小于0上下+左右翻转
    # new_img = cv2.flip(img, -1)
    # 顺时针旋转180度
    new_img = cv2.rotate(img, cv2.ROTATE_180)
    while True:
        cv2.imshow('img', new_img)
        key = cv2.waitKey()
        if key & 0xFF == ord('q'):
            break
    cv2.destroyAllWindows()

运行结果

import cv2

if __name__ == "__main__":

    cv2.namedWindow('img', cv2.WINDOW_NORMAL)
    img = cv2.imread("/Users/admin/Documents/111.jpeg")
    # 0上下翻转,大于0左右翻转,小于0上下+左右翻转
    # new_img = cv2.flip(img, -1)
    # 逆时针旋转90度
    new_img = cv2.rotate(img, cv2.ROTATE_90_COUNTERCLOCKWISE)
    while True:
        cv2.imshow('img', new_img)
        key = cv2.waitKey()
        if key & 0xFF == ord('q'):
            break
    cv2.destroyAllWindows()

运行结果

这里需要注意的是,cv2.rotate无法旋转任意一个角度,只有这么三个角度可以旋转。

图像的仿射变换

仿射变换是图像旋转、缩放、平移的总称

import cv2
import numpy as np

if __name__ == "__main__":

    cv2.namedWindow('img', cv2.WINDOW_NORMAL)
    img = cv2.imread("/Users/admin/Documents/111.jpeg")
    h, w, ch = img.shape
    # 变换矩阵之右平移100像素,下平移100像素
    M = np.float32([[1, 0, 100], [0, 1, 100]])
    # 图像的仿射变换,M是变换矩阵,(w, h)是图像大小
    new_img = cv2.warpAffine(img, M, (w, h))
    while True:
        cv2.imshow('img', new_img)
        key = cv2.waitKey()
        if key & 0xFF == ord('q'):
            break
    cv2.destroyAllWindows()

运行结果

这里需要说明的是,变换矩阵是线性代数中的基础,可以参考线性代数整理 中的图形变换矩阵

之前我们在看图形的旋转中,只能旋转3个角度,无法任意旋转,现在我们就自己定义旋转的变换矩阵来让图片旋转任意角度

这个是使图形旋转的变换矩阵公式,现在我们来让图形逆时针旋转15度(围绕(0,0)旋转)

import cv2
import numpy as np
import math

if __name__ == "__main__":

    cv2.namedWindow('img', cv2.WINDOW_NORMAL)
    img = cv2.imread("/Users/admin/Documents/111.jpeg")
    h, w, ch = img.shape
    # 变换矩阵之右平移100像素,下平移100像素
    # M = np.float32([[1, 0, 100], [0, 1, 100]])
    theta = math.pi / 12
    # 将图像逆时针旋转15度    
    M = np.float32([[math.cos(theta), math.sin(theta), 0], [-math.sin(theta), math.cos(theta), 0]])
    # 图像的仿射变换,M是变换矩阵,(w, h)是图像大小
    new_img = cv2.warpAffine(img, M, (w, h))
    while True:
        cv2.imshow('img', new_img)
        key = cv2.waitKey()
        if key & 0xFF == ord('q'):
            break
    cv2.destroyAllWindows()

运行结果

又比如是将图像进行拉伸的变换矩阵

import cv2
import numpy as np

if __name__ == "__main__":

    cv2.namedWindow('img', cv2.WINDOW_NORMAL)
    img = cv2.imread("/Users/admin/Documents/111.jpeg")
    h, w, ch = img.shape
    # 变换矩阵之右平移100像素,下平移100像素
    # M = np.float32([[1, 0, 100], [0, 1, 100]])
    # theta = math.pi / 12
    # 将图像逆时针旋转15度
    # M = np.float32([[math.cos(theta), math.sin(theta), 0], [-math.sin(theta), math.cos(theta), 0]])
    # 将图像进行拉伸
    M = np.float32([[1, 0.3, 0], [0, 1, 0]])
    # 图像的仿射变换,M是变换矩阵,(w, h)是图像大小
    new_img = cv2.warpAffine(img, M, (w, h))
    while True:
        cv2.imshow('img', new_img)
        key = cv2.waitKey()
        if key & 0xFF == ord('q'):
            break
    cv2.destroyAllWindows()

运行结果

获取变换矩阵

由于上述变换矩阵需要特定的变换矩阵公式,OpenCV提供了一种获取该变换矩阵的方法

import cv2

if __name__ == "__main__":

    cv2.namedWindow('img', cv2.WINDOW_NORMAL)
    img = cv2.imread("/Users/admin/Documents/111.jpeg")
    h, w, ch = img.shape
    # 变换矩阵之右平移100像素,下平移100像素
    # M = np.float32([[1, 0, 100], [0, 1, 100]])
    # theta = math.pi / 12
    # 将图像逆时针旋转15度
    # M = np.float32([[math.cos(theta), math.sin(theta), 0], [-math.sin(theta), math.cos(theta), 0]])
    # 将图像进行拉伸
    # M = np.float32([[1, 0.3, 0], [0, 1, 0]])
    # 获取变换矩阵,(w / 2, h / 2)为图像中心点,逆时针旋转15度,1.0表示不缩放
    M = cv2.getRotationMatrix2D((w / 2, h / 2), 15, 1.0)
    # 图像的仿射变换,M是变换矩阵,(w, h)是图像大小
    new_img = cv2.warpAffine(img, M, (w, h))
    while True:
        cv2.imshow('img', new_img)
        key = cv2.waitKey()
        if key & 0xFF == ord('q'):
            break
    cv2.destroyAllWindows()

运行结果

现在我们再将图像旋转后进行缩放

import cv2

if __name__ == "__main__":

    cv2.namedWindow('img', cv2.WINDOW_NORMAL)
    img = cv2.imread("/Users/admin/Documents/111.jpeg")
    h, w, ch = img.shape
    # 变换矩阵之右平移100像素,下平移100像素
    # M = np.float32([[1, 0, 100], [0, 1, 100]])
    # theta = math.pi / 12
    # 将图像逆时针旋转15度
    # M = np.float32([[math.cos(theta), math.sin(theta), 0], [-math.sin(theta), math.cos(theta), 0]])
    # 将图像进行拉伸
    # M = np.float32([[1, 0.3, 0], [0, 1, 0]])
    # 获取变换矩阵,(w / 2, h / 2)为图像中心点,逆时针旋转15度,0。6表示缩小为60%
    M = cv2.getRotationMatrix2D((w / 2, h / 2), 15, 0.6)
    # 图像的仿射变换,M是变换矩阵,(w, h)是图像大小
    new_img = cv2.warpAffine(img, M, (w, h))
    while True:
        cv2.imshow('img', new_img)
        key = cv2.waitKey()
        if key & 0xFF == ord('q'):
            break
    cv2.destroyAllWindows()

运行结果

变换矩阵二

在上图中由三个绿点来获取变换矩阵。注意,这里左右两个图的三个绿点都要给出,左图的三个绿点叫做src,右图的三个绿点叫做dst。

import cv2
import numpy as np

if __name__ == "__main__":

    cv2.namedWindow('img', cv2.WINDOW_NORMAL)
    img = cv2.imread("/Users/admin/Documents/111.jpeg")
    h, w, ch = img.shape
    # 变换矩阵之右平移100像素,下平移100像素
    # M = np.float32([[1, 0, 100], [0, 1, 100]])
    # theta = math.pi / 12
    # 将图像逆时针旋转15度
    # M = np.float32([[math.cos(theta), math.sin(theta), 0], [-math.sin(theta), math.cos(theta), 0]])
    # 将图像进行拉伸
    # M = np.float32([[1, 0.3, 0], [0, 1, 0]])
    # 获取变换矩阵,(w / 2, h / 2)为图像中心点,逆时针旋转15度,0。6表示缩小为60%
    # M = cv2.getRotationMatrix2D((w / 2, h / 2), 15, 0.6)
    src = np.float32([[400, 300], [800, 300], [400, 1000]])
    dst = np.float32([[200, 400], [600, 500], [150, 1100]])
    M = cv2.getAffineTransform(src, dst)
    # 图像的仿射变换,M是变换矩阵,(w, h)是图像大小
    new_img = cv2.warpAffine(img, M, (w, h))
    while True:
        cv2.imshow('img', new_img)
        key = cv2.waitKey()
        if key & 0xFF == ord('q'):
            break
    cv2.destroyAllWindows()

运行结果

透视变换

透视变换是将拍摄的比较倾斜的图转换成比较方正的图,这里我们使用一张非常倾斜的图

import cv2
import numpy as np

if __name__ == "__main__":

    cv2.namedWindow('img', cv2.WINDOW_NORMAL)
    img = cv2.imread("/Users/admin/Documents/444.jpeg")
    print(img.shape)
    # 源数据图取大概一个梯形形状
    src = np.float32([[474, 100], [1659, 100], [0, 1200], [1896, 1200]])
    dst = np.float32([[0, 0], [2300, 0], [0, 2400], [2300, 2400]])
    # 获取转换矩阵
    M = cv2.getPerspectiveTransform(src, dst)
    # 进行透视变换
    new_img = cv2.warpPerspective(img, M, (2300, 2400))
    while True:
        cv2.imshow('img', new_img)
        key = cv2.waitKey()
        if key & 0xFF == ord('q'):
            break
    cv2.destroyAllWindows()

运行结果

OpenCV中的滤波器

图像滤波

一副图像通过滤波器得到另一幅图像。其中滤波器又称为卷积核,滤波的过程称为卷积。有关卷积核的内容请参考Tensorflow深度学习算法整理 ,这里不再赘述。

低通滤波与高通滤波

  • 低通滤波可以去除噪音或平滑图像。比方说这样一个卷积核(一般可以用于美颜)

import cv2
import numpy as np

if __name__ == "__main__":

    cv2.namedWindow('img', cv2.WINDOW_NORMAL)
    img = cv2.imread("/Users/admin/Documents/111.jpeg")
    # 5*5的卷积核
    kernal = np.ones((5, 5), np.float32) / 25
    # 低通滤波处理(卷积操作),-1表示不改变卷积后的图像大小
    dst = cv2.filter2D(img, -1, kernal)
    while True:
        cv2.imshow('img', dst)
        key = cv2.waitKey()
        if key & 0xFF == ord('q'):
            break
    cv2.destroyAllWindows()

运行结果

这张图可能看起来不是特别明显,不过没关系。

  • 高通滤波可以帮助查找图像的边缘。(一般可以用于抠图)

方盒滤波与均值滤波

对于卷积核,如果我们自己去手工创建卷积核可能比较困难,OpenCV为我们提供了一些常用的卷积核作为滤波器。

  • 方盒滤波

参数a的作用:

  1. normalize=True,a = 1/(W*H),这里W、H是滤波器的宽和高,这是一个均值滤波
  2. normalize=False,a = 1

当normalize==True的时候,方盒滤波==均值滤波,一般情况下,我们使用的都是均值滤波。

import cv2

if __name__ == "__main__":

    cv2.namedWindow('img', cv2.WINDOW_NORMAL)
    img = cv2.imread("/Users/admin/Documents/111.jpeg")
    # 5*5的卷积核
    # kernal = np.ones((5, 5), np.float32) / 25
    # 低通滤波处理(卷积操作),-1表示不改变卷积后的图像大小
    # dst = cv2.filter2D(img, -1, kernal)
    # 均值滤波,效果跟上面相同
    dst = cv2.blur(img, (5, 5))
    while True:
        cv2.imshow('img', dst)
        key = cv2.waitKey()
        if key & 0xFF == ord('q'):
            break
    cv2.destroyAllWindows()

我们再来看一下方盒滤波

import cv2

if __name__ == "__main__":

    cv2.namedWindow('img', cv2.WINDOW_NORMAL)
    img = cv2.imread("/Users/admin/Documents/111.jpeg")
    # 5*5的卷积核
    # kernal = np.ones((5, 5), np.float32) / 25
    # 低通滤波处理(卷积操作),-1表示不改变卷积后的图像大小
    # dst = cv2.filter2D(img, -1, kernal)
    # 方盒滤波
    dst = cv2.boxFilter(img, -1, (5, 5), normalize=False)
    while True:
        cv2.imshow('img', dst)
        key = cv2.waitKey()
        if key & 0xFF == ord('q'):
            break
    cv2.destroyAllWindows()

运行结果

高斯滤波

高斯滤波是一种呈现正态分布的滤波器,当图像中的噪音呈正态分布的时候,可以使用高斯滤波来矫正。

它的原理就是在我们的卷积核中,中心点不是最大的,但是比重是最大的,而周围的点可能比较大,但比重比较低。总之就是越靠边上,比重越低;越靠近中心,比重越高

  • 高斯权重

在上图中,我们可以看到,中心点25的权重为14.7%,最高,靠近中心点的上下左右的权重为11.83%,而四个角的权重只有9.47%,最低。

我们先来给美女图像添加高斯噪声。

import cv2
import numpy as np

if __name__ == "__main__":

    def gasuss_noise(image, mean=0, var=0.001):
        '''
            添加高斯噪声
            mean : 均值
            var : 方差
        '''
        image = np.array(image / 255, dtype=np.float)
        noise = np.random.normal(mean, var ** 0.5, image.shape)
        out = image + noise
        if out.min() < 0:
            low_clip = -1.
        else:
            low_clip = 0.
        out = np.clip(out, low_clip, 1.0)
        out = np.uint8(out * 255)
        return out

    cv2.namedWindow('img', cv2.WINDOW_NORMAL)
    img = cv2.imread("/Users/admin/Documents/111.jpeg")
    img_out = gasuss_noise(img, mean=0, var=0.001)
    # 5*5的卷积核
    # kernal = np.ones((5, 5), np.float32) / 25
    # 低通滤波处理(卷积操作),-1表示不改变卷积后的图像大小
    # dst = cv2.filter2D(img, -1, kernal)
    # 方盒滤波
    # dst = cv2.boxFilter(img, -1, (5, 5), normalize=False)
    while True:
        cv2.imshow('img', img_out)
        key = cv2.waitKey()
        if key & 0xFF == ord('q'):
            break
    cv2.destroyAllWindows()

运行结果

现在我们再使用高斯滤波器来进行处理

import cv2
import numpy as np

if __name__ == "__main__":

    def gasuss_noise(image, mean=0, var=0.001):
        '''
            添加高斯噪声
            mean : 均值
            var : 方差
        '''
        image = np.array(image / 255, dtype=np.float)
        noise = np.random.normal(mean, var ** 0.5, image.shape)
        out = image + noise
        if out.min() < 0:
            low_clip = -1.
        else:
            low_clip = 0.
        out = np.clip(out, low_clip, 1.0)
        out = np.uint8(out * 255)
        return out

    cv2.namedWindow('img', cv2.WINDOW_NORMAL)
    img = cv2.imread("/Users/admin/Documents/111.jpeg")
    img_out = gasuss_noise(img, mean=0, var=0.001)
    # 5*5的卷积核
    # kernal = np.ones((5, 5), np.float32) / 25
    # 低通滤波处理(卷积操作),-1表示不改变卷积后的图像大小
    # dst = cv2.filter2D(img, -1, kernal)
    # 方盒滤波
    # dst = cv2.boxFilter(img, -1, (5, 5), normalize=False)
    # 高斯滤波,sigmaX和sigmaY是辐射半径
    dst = cv2.GaussianBlur(img_out, (33, 33), sigmaX=50, sigmaY=50)
    while True:
        cv2.imshow('img', dst)
        key = cv2.waitKey()
        if key & 0xFF == ord('q'):
            break
    cv2.destroyAllWindows()

运行结果

中值滤波

假设我们使用3*3的卷积核去遍历图像,每取得一个区域的像素的时候,就将该区域的图像像素进行排序,比如[1,5,5,5,6,7,8,9,11],取中间值作为卷积后的结果值。这里就是6.

中值滤波的优点是对胡椒噪音(在图像中分布随机的噪音点)效果明显。

我们先来对美女图像生成胡椒噪音

import cv2
import numpy as np
import random

if __name__ == "__main__":

    def sp_noise(image, prob):
        '''
        添加椒盐噪声
        prob:噪声比例
        '''
        output = np.zeros(image.shape, np.uint8)
        thres = 1 - prob
        for i in range(image.shape[0]):
            for j in range(image.shape[1]):
                rdn = random.random()
                if rdn < prob:
                    output[i][j] = 0
                elif rdn > thres:
                    output[i][j] = 255
                else:
                    output[i][j] = image[i][j]
        return output

    cv2.namedWindow('img', cv2.WINDOW_NORMAL)
    img = cv2.imread("/Users/admin/Documents/111.jpeg")
    out_img = sp_noise(img, 0.02)
    while True:
        cv2.imshow('img', out_img)
        key = cv2.waitKey()
        if key & 0xFF == ord('q'):
            break
    cv2.destroyAllWindows()

运行结果

现在我们再使用中值滤波器来进行处理

import cv2
import numpy as np
import random

if __name__ == "__main__":

    def sp_noise(image, prob):
        '''
        添加椒盐噪声
        prob:噪声比例
        '''
        output = np.zeros(image.shape, np.uint8)
        thres = 1 - prob
        for i in range(image.shape[0]):
            for j in range(image.shape[1]):
                rdn = random.random()
                if rdn < prob:
                    output[i][j] = 0
                elif rdn > thres:
                    output[i][j] = 255
                else:
                    output[i][j] = image[i][j]
        return output

    cv2.namedWindow('img', cv2.WINDOW_NORMAL)
    img = cv2.imread("/Users/admin/Documents/111.jpeg")
    out_img = sp_noise(img, 0.02)
    # 中值滤波
    dst = cv2.medianBlur(out_img, 5)
    while True:
        cv2.imshow('img', dst)
        key = cv2.waitKey()
        if key & 0xFF == ord('q'):
            break
    cv2.destroyAllWindows()

运行结果

我们再来看另外一张图

import cv2
import numpy as np
import random

if __name__ == "__main__":

    def sp_noise(image, prob):
        '''
        添加椒盐噪声
        prob:噪声比例
        '''
        output = np.zeros(image.shape, np.uint8)
        thres = 1 - prob
        for i in range(image.shape[0]):
            for j in range(image.shape[1]):
                rdn = random.random()
                if rdn < prob:
                    output[i][j] = 0
                elif rdn > thres:
                    output[i][j] = 255
                else:
                    output[i][j] = image[i][j]
        return output

    cv2.namedWindow('img', cv2.WINDOW_NORMAL)
    img = cv2.imread("/Users/admin/Documents/555.jpeg")
    # out_img = sp_noise(img, 0.02)
    # 中值滤波
    dst = cv2.medianBlur(img, 15)
    while True:
        cv2.imshow('img', dst)
        key = cv2.waitKey()
        if key & 0xFF == ord('q'):
            break
    cv2.destroyAllWindows()

运行结果

双边滤波

双边滤波的优点:可以保留边缘,同时可以对边缘内的区域进行平滑处理。他的主要作用是进行美颜

  • 双边滤波的原理

在上图中的a输入中,有一个高度,这个高度代表颜色的边缘,颜色的落差特别大。上下两块代表边缘的两部分。对于双边滤波来说,对于边缘不会做处理,但是对于边缘的两部分的突起的部分给抹平。最终输出的就是d图。对于b空域核和c值域核,它们影响的是不同的地点的。b空域核影响的是颜色的落差边缘,c值域核影响的是边缘之外的平滑效果的。

现在依然来对美女图片进行双边滤波,先看一下原图,头发和脸的颜色都略显暗淡。

import cv2

if __name__ == "__main__":

    cv2.namedWindow('img', cv2.WINDOW_NORMAL)
    img = cv2.imread("/Users/admin/Documents/111.jpeg")
    # 双边滤波,7代表边缘直径,20表示空域核的值,意思为颜色的变化范围
    # 在这个范围认为都是相同的颜色
    # 50为值域核的值,意思为在平面上进行平滑处理的范围
    dst = cv2.bilateralFilter(img, 7, 20, 50)
    while True:
        cv2.imshow('img', dst)
        key = cv2.waitKey()
        if key & 0xFF == ord('q'):
            break
    cv2.destroyAllWindows()

运行结果

经过双边滤波后,我们可以看到,头发和脸都变得更加红润有光泽,眼睛也更加明亮。

高通滤波——索贝尔算子

前面的都是低通滤波,主要作用是消除噪音,平滑图像。OpenCV也提供了很多高通滤波。

常见的高通滤波:

  1. Sobel(索贝尔),对噪音适应性很强,在内部实现中使用了高斯滤波,对噪音首先进行了过滤,之后通过一阶导数求得图像的边缘。有很多算法都是使用索贝尔为基础的。
  2. Scharr(沙尔),卷积核不可改变的,尺寸是固定的,3*3大小的一个卷积核,如果索贝尔的size设成-1,则自动使用的是沙尔滤波算法,所以一般情况下使用的是索贝尔,很少使用沙尔。对于3*3的卷积核来说,索贝尔的效果是没有沙尔好的,因为沙尔可以检测出更细的边缘线。而索贝尔就比较粗糙一些,但它可以改变卷积核大小。对于索贝尔和沙尔来说,在计算边缘的时候,只能求一个方向的,要么是横轴要么是纵轴。最终的结果我们需要将横轴检测的边缘与纵轴检测的边缘加在一起,做一次加法运算才能得出最终的结果。
  3. Laplacian(拉普拉斯),不用单独求横轴或者纵轴边缘,它一下就能将横轴和纵轴的边缘全部检测出来。但是对于噪音比较敏感,在内部没有降噪的功能,所以在使用拉普拉斯之前还需要手工降噪。这样才能更好的体验出拉普拉斯的效果。

Sobel算子:先向x方向求导,然后在y方向求导,最终结果:|G|=|Gx|+|Gy|

我们先来看一下只求一个方向导数的结果,这里先向y方向求导。

import cv2

if __name__ == "__main__":

    cv2.namedWindow('img', cv2.WINDOW_NORMAL)
    img = cv2.imread("/Users/admin/Documents/111.jpeg")
    # 索贝尔滤波,cv2.CV_64F是位深,即数据类型,这里是64位float类型,1表示向y方向求导
    # 0表示不向x方向求导,ksize为卷积核大小
    d1 = cv2.Sobel(img, cv2.CV_64F, 1, 0, ksize=5)
    while True:
        cv2.imshow('img', d1)
        key = cv2.waitKey()
        if key & 0xFF == ord('q'):
            break
    cv2.destroyAllWindows()

运行结果

我们再来看一下只向x方向求导

import cv2

if __name__ == "__main__":

    cv2.namedWindow('img', cv2.WINDOW_NORMAL)
    img = cv2.imread("/Users/admin/Documents/111.jpeg")
    # 索贝尔滤波,cv2.CV_64F是位深,即数据类型,这里是64位float类型,1表示向y方向求导
    # 0表示不向x方向求导,ksize为卷积核大小
    d1 = cv2.Sobel(img, cv2.CV_64F, 1, 0, ksize=5)
    # 向x方向求导
    d2 = cv2.Sobel(img, cv2.CV_64F, 0, 1, ksize=5)
    while True:
        cv2.imshow('img', d2)
        key = cv2.waitKey()
        if key & 0xFF == ord('q'):
            break
    cv2.destroyAllWindows()

运行结果

最终我们将两个方向求导的结果加起来

import cv2

if __name__ == "__main__":

    cv2.namedWindow('img', cv2.WINDOW_NORMAL)
    img = cv2.imread("/Users/admin/Documents/111.jpeg")
    # 索贝尔滤波,cv2.CV_64F是位深,即数据类型,这里是64位float类型,1表示向y方向求导
    # 0表示不向x方向求导,ksize为卷积核大小
    d1 = cv2.Sobel(img, cv2.CV_64F, 1, 0, ksize=3)
    # 向x方向求导
    d2 = cv2.Sobel(img, cv2.CV_64F, 0, 1, ksize=3)
    dst = cv2.add(d1, d2)
    while True:
        cv2.imshow('img', dst)
        key = cv2.waitKey()
        if key & 0xFF == ord('q'):
            break
    cv2.destroyAllWindows()

运行结果

我们再来看一张这个图,效果会更加明显

import cv2

if __name__ == "__main__":

    cv2.namedWindow('img', cv2.WINDOW_NORMAL)
    img = cv2.imread("/Users/admin/Documents/888.jpg")
    # 索贝尔滤波,cv2.CV_64F是位深,即数据类型,这里是64位float类型,1表示向y方向求导
    # 0表示不向x方向求导,ksize为卷积核大小
    d1 = cv2.Sobel(img, cv2.CV_64F, 1, 0, ksize=3)
    # 向x方向求导
    d2 = cv2.Sobel(img, cv2.CV_64F, 0, 1, ksize=3)
    dst = cv2.add(d1, d2)
    while True:
        cv2.imshow('img', dst)
        key = cv2.waitKey()
        if key & 0xFF == ord('q'):
            break
    cv2.destroyAllWindows()

运行结果

沙尔算子

import cv2

if __name__ == "__main__":

    cv2.namedWindow('img', cv2.WINDOW_NORMAL)
    img = cv2.imread("/Users/admin/Documents/111.jpeg")
    # 索贝尔滤波,cv2.CV_64F是位深,即数据类型,这里是64位float类型,1表示向y方向求导
    # 0表示不向x方向求导,ksize为卷积核大小
    # d1 = cv2.Sobel(img, cv2.CV_64F, 1, 0, ksize=3)
    # 向x方向求导
    # d2 = cv2.Sobel(img, cv2.CV_64F, 0, 1, ksize=3)
    # 沙尔滤波,它的卷积核大小固定为3*3,求y方向导数
    d1 = cv2.Scharr(img, cv2.CV_64F, 1, 0)
    # dst = cv2.add(d1, d2)
    while True:
        cv2.imshow('img', d1)
        key = cv2.waitKey()
        if key & 0xFF == ord('q'):
            break
    cv2.destroyAllWindows()

运行结果

import cv2

if __name__ == "__main__":

    cv2.namedWindow('img', cv2.WINDOW_NORMAL)
    img = cv2.imread("/Users/admin/Documents/111.jpeg")
    # 索贝尔滤波,cv2.CV_64F是位深,即数据类型,这里是64位float类型,1表示向y方向求导
    # 0表示不向x方向求导,ksize为卷积核大小
    # d1 = cv2.Sobel(img, cv2.CV_64F, 1, 0, ksize=3)
    # 向x方向求导
    # d2 = cv2.Sobel(img, cv2.CV_64F, 0, 1, ksize=3)
    # 沙尔滤波,它的卷积核大小固定为3*3,求y方向导数
    d1 = cv2.Scharr(img, cv2.CV_64F, 1, 0)
    # 求x方向导数
    d2 = cv2.Scharr(img, cv2.CV_64F, 0, 1)
    # dst = cv2.add(d1, d2)
    while True:
        cv2.imshow('img', d2)
        key = cv2.waitKey()
        if key & 0xFF == ord('q'):
            break
    cv2.destroyAllWindows()

运行结果

import cv2

if __name__ == "__main__":

    cv2.namedWindow('img', cv2.WINDOW_NORMAL)
    img = cv2.imread("/Users/admin/Documents/111.jpeg")
    # 索贝尔滤波,cv2.CV_64F是位深,即数据类型,这里是64位float类型,1表示向y方向求导
    # 0表示不向x方向求导,ksize为卷积核大小
    # d1 = cv2.Sobel(img, cv2.CV_64F, 1, 0, ksize=3)
    # 向x方向求导
    # d2 = cv2.Sobel(img, cv2.CV_64F, 0, 1, ksize=3)
    # 沙尔滤波,它的卷积核大小固定为3*3,求y方向导数
    d1 = cv2.Scharr(img, cv2.CV_64F, 1, 0)
    # 求x方向导数
    d2 = cv2.Scharr(img, cv2.CV_64F, 0, 1)
    dst = cv2.add(d1, d2)
    while True:
        cv2.imshow('img', dst)
        key = cv2.waitKey()
        if key & 0xFF == ord('q'):
            break
    cv2.destroyAllWindows()

运行结果

拉普拉斯算子

import cv2

if __name__ == "__main__":

    cv2.namedWindow('img', cv2.WINDOW_NORMAL)
    img = cv2.imread("/Users/admin/Documents/111.jpeg")
    # 索贝尔滤波,cv2.CV_64F是位深,即数据类型,这里是64位float类型,1表示向y方向求导
    # 0表示不向x方向求导,ksize为卷积核大小
    # d1 = cv2.Sobel(img, cv2.CV_64F, 1, 0, ksize=3)
    # 向x方向求导
    # d2 = cv2.Sobel(img, cv2.CV_64F, 0, 1, ksize=3)
    # 沙尔滤波,它的卷积核大小固定为3*3,求y方向导数
    # d1 = cv2.Scharr(img, cv2.CV_64F, 1, 0)
    # 求x方向导数
    # d2 = cv2.Scharr(img, cv2.CV_64F, 0, 1)
    # dst = cv2.add(d1, d2)
    # 拉普拉斯滤波
    dst = cv2.Laplacian(img, cv2.CV_64F, ksize=5)
    while True:
        cv2.imshow('img', dst)
        key = cv2.waitKey()
        if key & 0xFF == ord('q'):
            break
    cv2.destroyAllWindows()

运行结果

边缘检测Canny

Canny是图像边缘检测的终极大法,由于之前的三个算子有着这样那样的问题,我们来看看Canny的优势

  1. 使用5*5高斯滤波消除噪音
  2. 计算图像梯度的方向(0°/45°/90°/135°)
  3. 取局部极大值
  4. 阈值计算

如果超过来maxVal最大值,肯定是边缘;如果低于minVal最小值,肯定不是边缘。如果在最大值和最小值之间,则要看所确定的值与之前是否是连贯的,比如说A是一个边缘,C与A在连线上,所以C也是一个边缘。对于B来说,也在最大值和最小值之间,由于它不在边缘的一条线上,所以B不是边缘。

import cv2

if __name__ == "__main__":

    cv2.namedWindow('img', cv2.WINDOW_NORMAL)
    img = cv2.imread("/Users/admin/Documents/111.jpeg")
    # Canny滤波,50为最小值,160为最大值
    dst = cv2.Canny(img, 50, 160)
    while True:
        cv2.imshow('img', dst)
        key = cv2.waitKey()
        if key & 0xFF == ord('q'):
            break
    cv2.destroyAllWindows()

运行结果

这里随着最小值和最大值的调整,它检测出来的边缘是不一样的,比如我调小最大值

import cv2

if __name__ == "__main__":

    cv2.namedWindow('img', cv2.WINDOW_NORMAL)
    img = cv2.imread("/Users/admin/Documents/111.jpeg")
    # Canny滤波,50为最小值,100为最大值
    dst = cv2.Canny(img, 50, 100)
    while True:
        cv2.imshow('img', dst)
        key = cv2.waitKey()
        if key & 0xFF == ord('q'):
            break
    cv2.destroyAllWindows()

运行结果

我们可以看到它会把更多地方视为边缘。在后面进行目标检测的时候就会使用Canny进行轮廓的查找。

OpenCV中的形态学

形态学概述

  • 什么是形态学处理:
  1. 基于图像形态进行处理的一些基本方法。比如说图片中有一个杯子,形态学可以帮我们找到这个杯子在哪,它并不关心我们要找的是什么。
  2. 这些处理方法基本是对二进制图像进行处理(即黑白图像)。如果我们拿到的是一个彩色图像,则需要先进行转换成灰色图像。
  3. 卷积核决定着图像处理后的效果
  • 形态学图像处理方式:
  1. 腐蚀与膨胀。腐蚀的意思就是将一个区域变小。膨胀的意思就是将一个区域变大。
  2. 开运算,就是先做腐蚀再做膨胀。
  3. 闭运算,就是先做膨胀再做腐蚀。
  4. 顶帽
  5. 黑帽

顶帽和黑帽都是一些插值处理

图像全局二值化

什么是二值化:

  1. 将图像的每个像素变成两种值,如0,255。对于灰色图像,它是有灰色程度的,它是一层一层由黑到白(即非0和255的中间值),有梯度的。我们在进行形态处理的时候,如果有梯度处理起来就比较麻烦。
  2. 全局二值化,我们选出一个阈值,所有的像素都和这个阈值做对比,如果低于这个阈值,就变成0;如果高于这个阈值,就变成255。
  3. 局部二值化,将图形分成很多的域,很多的小块,在每一个小块里再做二值化。这样就可以对一些光线的图像,特别暗的部分处理起来就会有特别的效果。
import cv2

if __name__ == "__main__":

    cv2.namedWindow('img', cv2.WINDOW_NORMAL)
    img = cv2.imread("/Users/admin/Documents/111.jpeg")
    # 将彩色图像转成灰色图像
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    # 二值化,180为阈值,255是超过阈值的转化值
    # cv2.THRESH_BINARY是类型,还有一个反向的cv2.THRESH_BINARY_INV
    # 低于阈值的变成255,高于阈值的变成0
    ret, dst = cv2.threshold(gray, 180, 255, cv2.THRESH_BINARY)
    while True:
        cv2.imshow('img', dst)
        key = cv2.waitKey()
        if key & 0xFF == ord('q'):
            break
    cv2.destroyAllWindows()

运行结果

我们来看一下相反值的情况

import cv2

if __name__ == "__main__":

    cv2.namedWindow('img', cv2.WINDOW_NORMAL)
    img = cv2.imread("/Users/admin/Documents/111.jpeg")
    # 将彩色图像转成灰色图像
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    # 二值化,180为阈值,255是超过阈值的转化值
    # cv2.THRESH_BINARY是类型,还有一个反向的cv2.THRESH_BINARY_INV
    # 低于阈值的变成255,高于阈值的变成0
    ret, dst = cv2.threshold(gray, 180, 255, cv2.THRESH_BINARY_INV)
    while True:
        cv2.imshow('img', dst)
        key = cv2.waitKey()
        if key & 0xFF == ord('q'):
            break
    cv2.destroyAllWindows()

运行结果

我们也可以通过改变阈值来看一下变化

import cv2

if __name__ == "__main__":

    cv2.namedWindow('img', cv2.WINDOW_NORMAL)
    img = cv2.imread("/Users/admin/Documents/111.jpeg")
    # 将彩色图像转成灰色图像
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    # 二值化,180为阈值,255是超过阈值的转化值
    # cv2.THRESH_BINARY是类型,还有一个反向的cv2.THRESH_BINARY_INV
    # 低于阈值的变成255,高于阈值的变成0
    ret, dst = cv2.threshold(gray, 60, 255, cv2.THRESH_BINARY)
    while True:
        cv2.imshow('img', dst)
        key = cv2.waitKey()
        if key & 0xFF == ord('q'):
            break
    cv2.destroyAllWindows()

运行结果

阈值类型

前面我们只介绍了两种类型的阈值类型,就是THRESH_BINARY以及THRESH_BINARY_INV,也就是上图中的第二项和第三项。而第一项是一个原始图,表示像素点的不同值,它可能有峰值和谷值。这里是以中间的线为阈值的,所以对于THRESH_BINARY来说就是其原始图像素转化图。而对于THRESH_BINARY_INV来说与THRESH_BINARY来说刚好相反。

对于第四项TRUNCATE来说,实际就是进行了削峰操作,大于阈值的值都变成了阈值本身,小于阈值的值保留原值。对于第五项THRESH_TO_ZERO_INV和第六项THRESH_TO_ZERO来说也是一个相反的过程,THRESH_TO_ZERO是将小于阈值的值变成0,保留大于阈值的原值。THRESH_TO_ZERO_INV是将大于阈值的值变成0,保留小于阈值的原值。TRUNCATE、THRESH_TO_ZERO、THRESH_TO_ZERO_INV是用的比较少的类型,我们一般使用的是THRESH_BINARY和THRESH_BINARY_INV。

自适应阈值二值化

由于光照不均匀以及阴影的存在,只有一个阈值会使得在阴影处的白色被二值化成黑色

我们现在用一张儿童思维导图来看一下

import cv2

if __name__ == "__main__":

    cv2.namedWindow('img', cv2.WINDOW_NORMAL)
    img = cv2.imread("/Users/admin/Documents/999.jpeg")
    # 将彩色图像转成灰色图像
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    # 二值化,180为阈值,255是超过阈值的转化值
    # cv2.THRESH_BINARY是类型,还有一个反向的cv2.THRESH_BINARY_INV
    # 低于阈值的变成255,高于阈值的变成0
    ret, dst = cv2.threshold(gray, 120, 255, cv2.THRESH_BINARY_INV)
    while True:
        cv2.imshow('img', dst)
        key = cv2.waitKey()
        if key & 0xFF == ord('q'):
            break
    cv2.destroyAllWindows()

运行结果

对比两张图,我们可以看见虽然二值图保留了原图的绝大部分信息,但还是有很多的信息丢失了,现在我们使用自适应的二值化来处理

import cv2

if __name__ == "__main__":

    cv2.namedWindow('img', cv2.WINDOW_NORMAL)
    img = cv2.imread("/Users/admin/Documents/999.jpeg")
    # 将彩色图像转成灰色图像
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    # 二值化,180为阈值,255是超过阈值的转化值
    # cv2.THRESH_BINARY是类型,还有一个反向的cv2.THRESH_BINARY_INV
    # 低于阈值的变成255,高于阈值的变成0
    # ret, dst = cv2.threshold(gray, 120, 255, cv2.THRESH_BINARY_INV)
    # 自适应二值化,255为最大值,cv2.ADAPTIVE_THRESH_GAUSSIAN_C为高斯窗口加权平均值
    # 即越在中心点权值越重,越靠近边缘权值越低,
    # 还有一种为cv2.ADAPTIVE_THRESH_MEAN_C,表示计算临近区域的平均值
    # 11为邻近区域的大小,0为从计算的平均值或加权平均值重减去的值
    dst = cv2.adaptiveThreshold(gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
                                cv2.THRESH_BINARY_INV, 11, 0)
    while True:
        cv2.imshow('img', dst)
        key = cv2.waitKey()
        if key & 0xFF == ord('q'):
            break
    cv2.destroyAllWindows()

运行结果

这样我们可以看到所有该保留的信息的纹路。我们也可以将颜色给反过来

import cv2

if __name__ == "__main__":

    cv2.namedWindow('img', cv2.WINDOW_NORMAL)
    img = cv2.imread("/Users/admin/Documents/999.jpeg")
    # 将彩色图像转成灰色图像
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    # 二值化,180为阈值,255是超过阈值的转化值
    # cv2.THRESH_BINARY是类型,还有一个反向的cv2.THRESH_BINARY_INV
    # 低于阈值的变成255,高于阈值的变成0
    # ret, dst = cv2.threshold(gray, 120, 255, cv2.THRESH_BINARY_INV)
    # 自适应二值化,255为最大值,cv2.ADAPTIVE_THRESH_GAUSSIAN_C为高斯窗口加权平均值
    # 即越在中心点权值越重,越靠近边缘权值越低,
    # 还有一种为cv2.ADAPTIVE_THRESH_MEAN_C,表示计算临近区域的平均值
    # 11为邻近区域的大小,0为从计算的平均值或加权平均值重减去的值
    dst = cv2.adaptiveThreshold(gray, 255, cv2.ADAPTIVE_THRESH_MEAN_C,
                                cv2.THRESH_BINARY, 11, 0)
    while True:
        cv2.imshow('img', dst)
        key = cv2.waitKey()
        if key & 0xFF == ord('q'):
            break
    cv2.destroyAllWindows()

运行结果

无论是哪种方式,我们都希望除了纹路之外,其他部分都是纯白色或者纯黑色的,而不是有这么多的噪点,这个可以后面来处理。

腐蚀

腐蚀的原理

上图的背景是一个黑底,中间有白色区域的原始图像。左上角是一个5*5的卷积核,其中的每一项都是1。经过这个卷积核扫描过的图像部分,如果图像部分都是黑色(像素都是0),那么图像的部分不会发生改变,因为0乘以任何数依然为0。当卷积核经过白色区域进行扫描的时候,我们知道卷积操作会将一个输入图像变成一个输出图像,而5*5=25个像素会变成一个像素,该像素会保留在卷积核中心点的位置上,这个值就是255,卷积操作不会修改原图,它会将这个255放到一个新图中,位置就在原图的卷积核中心的位置。当然在这个卷积操作过程中会采用padding技术,所以新图和原图会保持尺寸相等。新图的初始化状态我们可以认为是一个全0的二维数组,即全黑的。在卷积核不断对原图进行卷积操作的过程中,将卷积的结果像素值不断填充到新图的过程。这里需要注意的是图中的外围虚线部分的白色区域在原图中是不存在的,只是使用了padding技术对原图的外围进行了扩充,全部填充了0罢了。

一般腐蚀的卷积核大小有3*3、5*5、7*7,它的重点是卷积核内的值都是1.

它的腐蚀过程如上图所示,A的区域为全白的区域,经过B这个3*3的腐蚀卷积核腐蚀后,就变成了右边的的区域。

我们来看一个例子,原图如下

import cv2
import numpy as np

if __name__ == "__main__":

    cv2.namedWindow('img', cv2.WINDOW_NORMAL)
    img = cv2.imread("/Users/admin/Documents/1212.jpg")
    # 3*3的卷积核
    kernel = np.ones((3, 3), np.uint8)
    # 腐蚀操作,iterations = 8为卷积次数
    dst = cv2.erode(img, kernel, iterations=8)
    while True:
        cv2.imshow('img', dst)
        key = cv2.waitKey()
        if key & 0xFF == ord('q'):
            break
    cv2.destroyAllWindows()

运行结果

我们可以看到这个i瘦成了一道闪电。

 卷积核的类型

之前我们用numpy自己创建了一个卷积核,但其实OpenCV为我们提供了几种卷积核。

  1. MORPH_RECT,这种卷积核就是跟我们自己创建的卷积核是一样的,正方形的卷积核,大小自己设定。
  2. MORPH_ELLIPSE,这种卷积核是椭圆形的卷积核,它的四个角都是0.
  3. MORPH_CROSS,这种卷积核是一种交叉的卷积核

但我们经常用的还是MORPH_RECT

import cv2
import numpy as np

if __name__ == "__main__":

    cv2.namedWindow('img', cv2.WINDOW_NORMAL)
    img = cv2.imread("/Users/admin/Documents/1212.jpg")
    # 3*3的正方形卷积核
    kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3))
    # 腐蚀操作,iterations = 8为卷积次数
    dst = cv2.erode(img, kernel, iterations=8)
    while True:
        cv2.imshow('img', dst)
        key = cv2.waitKey()
        if key & 0xFF == ord('q'):
            break
    cv2.destroyAllWindows()

运行结果与之前相同。现在我们来看一下几种不同的卷积核的样子

kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (7, 7))
print(kernel)

运行结果

[[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 1 1 1]
 [1 1 1 1 1 1 1]
 [1 1 1 1 1 1 1]
 [1 1 1 1 1 1 1]]
kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (7, 7))
print(kernel)

运行结果

[[0 0 0 1 0 0 0]
 [0 1 1 1 1 1 0]
 [1 1 1 1 1 1 1]
 [1 1 1 1 1 1 1]
 [1 1 1 1 1 1 1]
 [0 1 1 1 1 1 0]
 [0 0 0 1 0 0 0]]
kernel = cv2.getStructuringElement(cv2.MORPH_CROSS, (7, 7))
print(kernel)

运行结果

[[0 0 0 1 0 0 0]
 [0 0 0 1 0 0 0]
 [0 0 0 1 0 0 0]
 [1 1 1 1 1 1 1]
 [0 0 0 1 0 0 0]
 [0 0 0 1 0 0 0]
 [0 0 0 1 0 0 0]]

膨胀

膨胀刚好与腐蚀相反

上图中中间的虚线部分是原图A,A经过卷积核B卷积之后,变成了实线部分,进行了扩张。

import cv2

if __name__ == "__main__":

    cv2.namedWindow('img', cv2.WINDOW_NORMAL)
    img = cv2.imread("/Users/admin/Documents/1212.jpg")
    # 3*3的正方形卷积核
    kernel = cv2.getStructuringElement(cv2.MORPH_CROSS, (3, 3))
    # 膨胀操作
    dst = cv2.dilate(img, kernel, iterations=12)
    while True:
        cv2.imshow('img', dst)
        key = cv2.waitKey()
        if key & 0xFF == ord('q'):
            break
    cv2.destroyAllWindows()

运行结果

这里我们可以看到这个i膨胀成了一道闪电。

开运算

开运算= 腐蚀 + 膨胀

开运算可以去除上面这张图中的黑色区域的白色噪点。

import cv2

if __name__ == "__main__":

    cv2.namedWindow('img', cv2.WINDOW_NORMAL)
    img = cv2.imread("/Users/admin/Documents/1313.jpg")
    # 13*13的正方形卷积核
    kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (13, 13))
    # 开运算,以cv2.MORPH_OPEN标识
    dst = cv2.morphologyEx(img, cv2.MORPH_OPEN, kernel)
    while True:
        cv2.imshow('img', dst)
        key = cv2.waitKey()
        if key & 0xFF == ord('q'):
            break
    cv2.destroyAllWindows()

运行结果

这里我们需要注意的是,由于开运算只是做一次腐蚀和一次膨胀,所以我们的卷积核的尺寸需要设的大一点,如果只是3*3的是无法达到效果消除噪点的。当然如果我们单独使用腐蚀和膨胀的话,可以进行多次卷积来消除噪点,这样就可以使用3*3的卷积核。

闭运算

闭运算 = 膨胀 + 腐蚀

闭运算可以消除上图中白色区域的黑色噪点。

import cv2

if __name__ == "__main__":

    cv2.namedWindow('img', cv2.WINDOW_NORMAL)
    img = cv2.imread("/Users/admin/Documents/1414.jpg")
    # 13*13的正方形卷积核
    kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (13, 13))
    # 闭运算,以cv2.MORPH_CLOSE标识
    dst = cv2.morphologyEx(img, cv2.MORPH_CLOSE, kernel)
    while True:
        cv2.imshow('img', dst)
        key = cv2.waitKey()
        if key & 0xFF == ord('q'):
            break
    cv2.destroyAllWindows()

运行结果

形态学梯度

梯度 = 原图 - 腐蚀

它主要是用来计算白色区域的边缘的。以此来获取图像边缘。

我们还是以这张图为例

import cv2

if __name__ == "__main__":

    cv2.namedWindow('img', cv2.WINDOW_NORMAL)
    img = cv2.imread("/Users/admin/Documents/1212.jpg")
    # 13*13的正方形卷积核
    kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (13, 13))
    # 梯度,以cv2.MORPH_GRADIENT标识
    dst = cv2.morphologyEx(img, cv2.MORPH_GRADIENT, kernel)
    while True:
        cv2.imshow('img', dst)
        key = cv2.waitKey()
        if key & 0xFF == ord('q'):
            break
    cv2.destroyAllWindows()

运行结果

顶帽运算

顶帽 = 原图 - 开运算

顶帽可以去掉大的白色区域,留下小的白色区域

我们以这张图为例

import cv2

if __name__ == "__main__":

    cv2.namedWindow('img', cv2.WINDOW_NORMAL)
    img = cv2.imread("/Users/admin/Documents/1515.jpg")
    # 33*33的正方形卷积核
    kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (33, 33))
    # 顶帽,以cv2.MORPH_TOPHAT标识
    dst = cv2.morphologyEx(img, cv2.MORPH_TOPHAT, kernel)
    while True:
        cv2.imshow('img', dst)
        key = cv2.waitKey()
        if key & 0xFF == ord('q'):
            break
    cv2.destroyAllWindows()

运行结果

这里我们可以看到,为了在腐蚀过程中彻底消除小的白色块,我们需要把卷积核设的大一点,否则顶帽后就是全部都是黑色的。

黑帽运算

黑帽 = 原图 - 闭运算

黑帽可以取出图中的黑色噪点,我们还是以这张图为例

import cv2

if __name__ == "__main__":

    cv2.namedWindow('img', cv2.WINDOW_NORMAL)
    img = cv2.imread("/Users/admin/Documents/1414.jpg")
    # 13*13的正方形卷积核
    kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (13, 13))
    # 黑帽,以cv2.MORPH_BLACKHAT标识
    dst = cv2.morphologyEx(img, cv2.MORPH_BLACKHAT, kernel)
    while True:
        cv2.imshow('img', dst)
        key = cv2.waitKey()
        if key & 0xFF == ord('q'):
            break
    cv2.destroyAllWindows()

运行结果

目标识别

图像轮廓

图像轮廓指的是具有相同颜色强度连续点的曲线。

这也是需要进行二值化的原因,因为对于一个灰色图像来说,可能一个图像轮廓的边缘它们像素点的灰度值是不一致的,所以为了更好辨认,需要它们具有相同的强度。

在上面这张图中,我们就识别出了三个轮廓,它们都具有相同的颜色或强度的连续点。

  • 图像轮廓的作用
  1. 可以用于图形分析,提取ROI
  2. 物体的识别和检测
  • 注意点
  1. 为了检测的准确性,需要先对图像进行二值化Canny操作。检测轮廓是目的,二值化或Canny操作是技术手段
  2. 画轮廓时会修改输入的图像。如果不想改变原始图像,需要先进行深拷贝。一般我们在进行二值化的时候需要把图像变成黑底白线,这样更加有利于轮廓的查找。

我们来看一下如何查找这个图形的轮廓,这里需要注意的是,该图并不是完全的黑白二色图,而是具有一定灰度的。

import cv2

if __name__ == "__main__":

    cv2.namedWindow('img', cv2.WINDOW_NORMAL)
    img = cv2.imread("/Users/admin/Documents/2121.jpg")
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    ret, binary = cv2.threshold(gray, 150, 255, cv2.THRESH_BINARY)
    # 轮廓查找
    # cv2.RETR_EXTERNAL查找最外围的轮廓
    # cv2.CHAIN_APPROX_SIMPLE轮廓记录方式,这里是压缩记录
    # contours, hierarchy是两个返回值,contours查到的所有的轮廓列表
    # hierarchy表示轮廓的层级
    contours, hierarchy = cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
    # 绘制轮廓,-1表示绘制所有轮廓
    cv2.drawContours(img, contours, -1, (0, 0, 255), 5)
    while True:
        cv2.imshow('img', img)
        key = cv2.waitKey()
        if key & 0xFF == ord('q'):
            break
    cv2.destroyAllWindows()

运行结果

这里我们看到,它就把图像外围的轮廓给查找出来了。由于轮廓查找函数的参数比较多,我们来一个个看

import cv2

if __name__ == "__main__":

    cv2.namedWindow('img', cv2.WINDOW_NORMAL)
    img = cv2.imread("/Users/admin/Documents/2121.jpg")
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    ret, binary = cv2.threshold(gray, 150, 255, cv2.THRESH_BINARY)
    # 轮廓查找
    # cv2.RETR_TREE查找所有的轮廓,并按照树形结构存储
    # cv2.CHAIN_APPROX_SIMPLE轮廓记录方式,这里是压缩记录
    # contours, hierarchy是两个返回值,contours查到的所有的轮廓列表
    # hierarchy表示轮廓的层级
    contours, hierarchy = cv2.findContours(binary, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
    # 绘制轮廓,-1表示绘制所有轮廓
    cv2.drawContours(img, contours, -1, (0, 0, 255), 5)
    while True:
        cv2.imshow('img', img)
        key = cv2.waitKey()
        if key & 0xFF == ord('q'):
            break
    cv2.destroyAllWindows()

运行结果

这里我们可以看到它查找到了3个轮廓,并且都给绘制了出来。

除了上面这两种查找轮廓的类型还有

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

这里cv2.RETR_LIST表示检测的轮廓不建立等级关系,它们都是平级的。

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

这里的cv2.RETR_CCOMP表示检测的轮廓建立层级关系,但每层最多两级。

这些类型的不同主要体现在返回值contours, hierarchy,它们有不同的数据结构,我们来看一下这些返回数据

contours, hierarchy = cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
print(contours)
print(hierarchy)

运行结果

[array([[[  0,   0]],

       [[  0, 787]],

       [[697, 787]],

       [[697,   0]]], dtype=int32)]
[[[-1 -1 -1 -1]]]

这说明返回的是四个绘制节点的坐标值,并且没有层级。

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

运行结果

[array([[[  0,   0]],

       [[  0, 787]],

       [[697, 787]],

       [[697,   0]]], dtype=int32), array([[[ 72, 235]],

       [[ 73, 234]],

       [[624, 234]],

       [[626, 236]],

       [[626, 726]],

       [[624, 728]],

       [[622, 728]],

       [[621, 727]],

       [[620, 728]],

       [[619, 728]],

       [[618, 727]],

       [[ 78, 727]],

       [[ 77, 728]],

       [[ 76, 727]],

       [[ 72, 727]],

       [[ 71, 726]],

       [[ 71, 583]],

       [[ 72, 582]],

       [[ 72, 580]],

       [[ 71, 579]],

       [[ 71, 560]],

       [[ 72, 559]],

       [[ 72, 556]],

       [[ 71, 555]],

       [[ 71, 488]],

       [[ 72, 487]],

       [[ 71, 486]],

       [[ 71, 481]],

       [[ 72, 480]],

       [[ 71, 479]],

       [[ 71, 251]],

       [[ 72, 250]],

       [[ 72, 248]],

       [[ 71, 247]],

       [[ 72, 246]]], dtype=int32), array([[[ 72,  61]],

       [[ 73,  60]],

       [[108,  60]],

       [[109,  61]],

       [[110,  61]],

       [[111,  60]],

       [[112,  61]],

       [[583,  61]],

       [[584,  60]],

       [[585,  61]],

       [[586,  61]],

       [[587,  60]],

       [[588,  60]],

       [[589,  61]],

       [[590,  60]],

       [[621,  60]],

       [[623,  62]],

       [[623,  69]],

       [[622,  70]],

       [[ 73,  70]],

       [[ 71,  68]],

       [[ 72,  67]],

       [[ 71,  66]],

       [[ 72,  65]],

       [[ 72,  64]],

       [[ 71,  63]],

       [[ 72,  62]]], dtype=int32)]
[[[-1 -1  1 -1]
  [ 2 -1 -1  0]
  [-1  1 -1  0]]]
contours, hierarchy = cv2.findContours(binary, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)
print(contours)
print(hierarchy)

运行结果

[array([[[ 72, 235]],

       [[ 73, 234]],

       [[624, 234]],

       [[626, 236]],

       [[626, 726]],

       [[624, 728]],

       [[622, 728]],

       [[621, 727]],

       [[620, 728]],

       [[619, 728]],

       [[618, 727]],

       [[ 78, 727]],

       [[ 77, 728]],

       [[ 76, 727]],

       [[ 72, 727]],

       [[ 71, 726]],

       [[ 71, 583]],

       [[ 72, 582]],

       [[ 72, 580]],

       [[ 71, 579]],

       [[ 71, 560]],

       [[ 72, 559]],

       [[ 72, 556]],

       [[ 71, 555]],

       [[ 71, 488]],

       [[ 72, 487]],

       [[ 71, 486]],

       [[ 71, 481]],

       [[ 72, 480]],

       [[ 71, 479]],

       [[ 71, 251]],

       [[ 72, 250]],

       [[ 72, 248]],

       [[ 71, 247]],

       [[ 72, 246]]], dtype=int32), array([[[ 72,  61]],

       [[ 73,  60]],

       [[108,  60]],

       [[109,  61]],

       [[110,  61]],

       [[111,  60]],

       [[112,  61]],

       [[583,  61]],

       [[584,  60]],

       [[585,  61]],

       [[586,  61]],

       [[587,  60]],

       [[588,  60]],

       [[589,  61]],

       [[590,  60]],

       [[621,  60]],

       [[623,  62]],

       [[623,  69]],

       [[622,  70]],

       [[ 73,  70]],

       [[ 71,  68]],

       [[ 72,  67]],

       [[ 71,  66]],

       [[ 72,  65]],

       [[ 72,  64]],

       [[ 71,  63]],

       [[ 72,  62]]], dtype=int32), array([[[  0,   0]],

       [[  0, 787]],

       [[697, 787]],

       [[697,   0]]], dtype=int32)]
[[[ 1 -1 -1 -1]
  [ 2  0 -1 -1]
  [-1  1 -1 -1]]]
contours, hierarchy = cv2.findContours(binary, cv2.RETR_CCOMP, cv2.CHAIN_APPROX_SIMPLE)
print(contours)
print(hierarchy)

运行结果

[array([[[  0,   0]],

       [[  0, 787]],

       [[697, 787]],

       [[697,   0]]], dtype=int32), array([[[ 72, 235]],

       [[ 73, 234]],

       [[624, 234]],

       [[626, 236]],

       [[626, 726]],

       [[624, 728]],

       [[622, 728]],

       [[621, 727]],

       [[620, 728]],

       [[619, 728]],

       [[618, 727]],

       [[ 78, 727]],

       [[ 77, 728]],

       [[ 76, 727]],

       [[ 72, 727]],

       [[ 71, 726]],

       [[ 71, 583]],

       [[ 72, 582]],

       [[ 72, 580]],

       [[ 71, 579]],

       [[ 71, 560]],

       [[ 72, 559]],

       [[ 72, 556]],

       [[ 71, 555]],

       [[ 71, 488]],

       [[ 72, 487]],

       [[ 71, 486]],

       [[ 71, 481]],

       [[ 72, 480]],

       [[ 71, 479]],

       [[ 71, 251]],

       [[ 72, 250]],

       [[ 72, 248]],

       [[ 71, 247]],

       [[ 72, 246]]], dtype=int32), array([[[ 72,  61]],

       [[ 73,  60]],

       [[108,  60]],

       [[109,  61]],

       [[110,  61]],

       [[111,  60]],

       [[112,  61]],

       [[583,  61]],

       [[584,  60]],

       [[585,  61]],

       [[586,  61]],

       [[587,  60]],

       [[588,  60]],

       [[589,  61]],

       [[590,  60]],

       [[621,  60]],

       [[623,  62]],

       [[623,  69]],

       [[622,  70]],

       [[ 73,  70]],

       [[ 71,  68]],

       [[ 72,  67]],

       [[ 71,  66]],

       [[ 72,  65]],

       [[ 72,  64]],

       [[ 71,  63]],

       [[ 72,  62]]], dtype=int32)]
[[[-1 -1  1 -1]
  [ 2 -1 -1  0]
  [-1  1 -1  0]]]

然后我们来看一下对美女图片进行轮廓查找的样子

import cv2

if __name__ == "__main__":

    cv2.namedWindow('img', cv2.WINDOW_NORMAL)
    img = cv2.imread("/Users/admin/Documents/111.jpeg")
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    ret, binary = cv2.threshold(gray, 120, 255, cv2.THRESH_BINARY)
    # 轮廓查找
    # cv2.RETR_TREE查找所有的轮廓,并按照树形结构存储
    # cv2.CHAIN_APPROX_SIMPLE轮廓记录方式,这里是压缩记录
    # contours, hierarchy是两个返回值,contours查到的所有的轮廓列表
    # hierarchy表示轮廓的层级
    contours, hierarchy = cv2.findContours(binary, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
    # 绘制轮廓,-1表示绘制所有轮廓
    cv2.drawContours(img, contours, -1, (0, 0, 255), 5)
    while True:
        cv2.imshow('img', img)
        key = cv2.waitKey()
        if key & 0xFF == ord('q'):
            break
    cv2.destroyAllWindows()

运行结果

轮廓的面积与周长

在我们查找到轮廓之后有很多碎小的轮廓,这些轮廓可能不是我们的计算范围之内,我们需要过滤掉它们。这个时候就可以通过面积的大小,来过滤获取我们真正需要的那个轮廓。又比如我们在一张图片中,我们知道一个物体的实际面积是多大,那么计算这个面积就可以通过实际面积计算出一个比例值来,通过这个比例值我们就可以知道这个图片中其他物体的大小。

import cv2

if __name__ == "__main__":

    cv2.namedWindow('img', cv2.WINDOW_NORMAL)
    img = cv2.imread("/Users/admin/Documents/2121.jpg")
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    ret, binary = cv2.threshold(gray, 120, 255, cv2.THRESH_BINARY)
    # 轮廓查找
    # cv2.RETR_TREE查找所有的轮廓,并按照树形结构存储
    # cv2.CHAIN_APPROX_SIMPLE轮廓记录方式,这里是压缩记录
    # contours, hierarchy是两个返回值,contours查到的所有的轮廓列表
    # hierarchy表示轮廓的层级
    contours, hierarchy = cv2.findContours(binary, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
    # 计算面积
    area = cv2.contourArea(contours[0])
    print(area)
    # 计算周长,True表示轮廓为闭合的,False表示轮廓是打开的
    len = cv2.arcLength(contours[0], True)
    print(len)
    # 绘制轮廓,-1表示绘制所有轮廓
    cv2.drawContours(img, contours, -1, (0, 0, 255), 5)
    while True:
        cv2.imshow('img', img)
        key = cv2.waitKey()
        if key & 0xFF == ord('q'):
            break
    cv2.destroyAllWindows()

运行结果

548539.0
2968.0

这样我们就得到了最外层的轮廓的面积和周长。

多边形逼近与凸包

在上图中有两个手型的图案,左边的轮廓就叫做多边形逼近,而右边的轮廓就叫做凸包。对于多边形逼近来说,它的手型是可以调整的。正常来说,当我们在查找轮廓的时候,对于这个手型轮廓是严格按照像素边缘点来进行绘制,但是这样会带来一个问题,当图形比较复杂的时候,它的轮廓边缘线会非常多,数据量会非常大。对于有些应用其实没必要存这么多数据,只需要存一些特征点,将手型会描述出来就可以了。比如说左边的图形中,我们只需要存一些关键点就可以将这个手型给描述出来。这就是多边形逼近的作用,减少了存储的数据量,同时通过特征点把这个手型给描述出来。这个特征点的选取是可以调整的,比如在小拇指到左下端中可以增加特征点来描绘左边手型的形状。但如果我们不想要这么严格,就可以把精度调低一些,这样存储的数据就会减少。

而凸包就是描述一个轮廓,而且是凸出的,不会往下凹。我们就用这个手型为例来看看这两个操作

我们来看一下,当我们不使用多边形逼近和凸包时的效果

import cv2

if __name__ == "__main__":

    cv2.namedWindow('img', cv2.WINDOW_NORMAL)
    img = cv2.imread("/Users/admin/Documents/hand.png")
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    ret, binary = cv2.threshold(gray, 120, 255, cv2.THRESH_BINARY)
    # 轮廓查找
    # cv2.RETR_TREE查找所有的轮廓,并按照树形结构存储
    # cv2.CHAIN_APPROX_SIMPLE轮廓记录方式,这里是压缩记录
    # contours, hierarchy是两个返回值,contours查到的所有的轮廓列表
    # hierarchy表示轮廓的层级
    contours, hierarchy = cv2.findContours(binary, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
    # 计算面积
    # area = cv2.contourArea(contours[0])
    # print(area)
    # 计算周长,True表示轮廓为闭合的,False表示轮廓是打开的
    # len = cv2.arcLength(contours[0], True)
    # print(len)
    # 绘制轮廓,-1表示绘制所有轮廓,3表示绘制框的粗细
    cv2.drawContours(img, contours, -1, (0, 255, 0), 3)
    while True:
        cv2.imshow('img', img)
        key = cv2.waitKey()
        if key & 0xFF == ord('q'):
            break
    cv2.destroyAllWindows()

运行结果

现在我们来增加多边形逼近

import cv2

if __name__ == "__main__":

    def drawShape(src, points):
        i = 0
        while i < len(points):
            if i == len(points) - 1:
                x, y = points[i][0]
                x1, y1 = points[0][0]
                cv2.line(src, (x, y), (x1, y1), (0, 0, 255), 3)
            else:
                x, y = points[i][0]
                x1, y1 = points[i + 1][0]
                cv2.line(src, (x, y), (x1, y1), (0, 0, 255), 3)
            i = i + 1

    cv2.namedWindow('img', cv2.WINDOW_NORMAL)
    img = cv2.imread("/Users/admin/Documents/hand.png")
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    ret, binary = cv2.threshold(gray, 120, 255, cv2.THRESH_BINARY)
    # 轮廓查找
    # cv2.RETR_TREE查找所有的轮廓,并按照树形结构存储
    # cv2.CHAIN_APPROX_SIMPLE轮廓记录方式,这里是压缩记录
    # contours, hierarchy是两个返回值,contours查到的所有的轮廓列表
    # hierarchy表示轮廓的层级
    contours, hierarchy = cv2.findContours(binary, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
    # 计算面积
    # area = cv2.contourArea(contours[0])
    # print(area)
    # 计算周长,True表示轮廓为闭合的,False表示轮廓是打开的
    # len = cv2.arcLength(contours[0], True)
    # print(len)
    # 绘制轮廓,-1表示绘制所有轮廓,3表示绘制框的粗细
    cv2.drawContours(img, contours, -1, (0, 255, 0), 3)
    e = 20
    # 获取多边形逼近的特征点,e为精度,True表示虽否闭合
    approx = cv2.approxPolyDP(contours[0], e, True)
    # 将多边形逼近的特征点给连接起来
    drawShape(img, approx)
    while True:
        cv2.imshow('img', img)
        key = cv2.waitKey()
        if key & 0xFF == ord('q'):
            break
    cv2.destroyAllWindows()

运行结果

现在来增加凸包

import cv2

if __name__ == "__main__":

    def drawShape(src, points):
        i = 0
        while i < len(points):
            if i == len(points) - 1:
                x, y = points[i][0]
                x1, y1 = points[0][0]
                cv2.line(src, (x, y), (x1, y1), (0, 0, 255), 3)
            else:
                x, y = points[i][0]
                x1, y1 = points[i + 1][0]
                cv2.line(src, (x, y), (x1, y1), (0, 0, 255), 3)
            i = i + 1

    cv2.namedWindow('img', cv2.WINDOW_NORMAL)
    img = cv2.imread("/Users/admin/Documents/hand.png")
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    ret, binary = cv2.threshold(gray, 120, 255, cv2.THRESH_BINARY)
    # 轮廓查找
    # cv2.RETR_TREE查找所有的轮廓,并按照树形结构存储
    # cv2.CHAIN_APPROX_SIMPLE轮廓记录方式,这里是压缩记录
    # contours, hierarchy是两个返回值,contours查到的所有的轮廓列表
    # hierarchy表示轮廓的层级
    contours, hierarchy = cv2.findContours(binary, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
    # 计算面积
    # area = cv2.contourArea(contours[0])
    # print(area)
    # 计算周长,True表示轮廓为闭合的,False表示轮廓是打开的
    # len = cv2.arcLength(contours[0], True)
    # print(len)
    # 绘制轮廓,-1表示绘制所有轮廓,3表示绘制框的粗细
    cv2.drawContours(img, contours, -1, (0, 255, 0), 3)
    e = 20
    # 获取多边形逼近的特征点,e为精度,True表示虽否闭合
    # approx = cv2.approxPolyDP(contours[0], e, True)
    # 将多边形逼近的特征点给连接起来
    # drawShape(img, approx)
    # 获取凸包的特征点
    hull = cv2.convexHull(contours[0])
    # 将凸包的特征点给连接起来
    drawShape(img, hull)
    while True:
        cv2.imshow('img', img)
        key = cv2.waitKey()
        if key & 0xFF == ord('q'):
            break
    cv2.destroyAllWindows()

运行结果

我们也可以查找美女图片的最大面积的轮廓,来进行多边形逼近

import cv2

if __name__ == "__main__":

    def drawShape(src, points):
        i = 0
        while i < len(points):
            if i == len(points) - 1:
                x, y = points[i][0]
                x1, y1 = points[0][0]
                cv2.line(src, (x, y), (x1, y1), (0, 0, 255), 3)
            else:
                x, y = points[i][0]
                x1, y1 = points[i + 1][0]
                cv2.line(src, (x, y), (x1, y1), (0, 0, 255), 3)
            i = i + 1

    cv2.namedWindow('img', cv2.WINDOW_NORMAL)
    img = cv2.imread("/Users/admin/Documents/111.jpeg")
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    ret, binary = cv2.threshold(gray, 120, 255, cv2.THRESH_BINARY)
    # 轮廓查找
    # cv2.RETR_TREE查找所有的轮廓,并按照树形结构存储
    # cv2.CHAIN_APPROX_SIMPLE轮廓记录方式,这里是压缩记录
    # contours, hierarchy是两个返回值,contours查到的所有的轮廓列表
    # hierarchy表示轮廓的层级
    contours, hierarchy = cv2.findContours(binary, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
    # 计算面积
    area = 0
    find = 0
    for i in range(len(contours)):
        areatemp = cv2.contourArea(contours[i])
        if areatemp > area:
            area = areatemp
            find = i
    # print(area)
    # print(find)
    # area = cv2.contourArea(contours[0])
    # print(area)
    # 计算周长,True表示轮廓为闭合的,False表示轮廓是打开的
    # len = cv2.arcLength(contours[0], True)
    # print(len)
    # 绘制轮廓,-1表示绘制所有轮廓,3表示绘制框的粗细
    cv2.drawContours(img, contours, -1, (0, 255, 0), 3)
    e = 30
    # 获取多边形逼近的特征点,e为精度,True表示虽否闭合
    approx = cv2.approxPolyDP(contours[find], e, True)
    # 将多边形逼近的特征点给连接起来
    drawShape(img, approx)
    # 获取凸包的特征点
    # hull = cv2.convexHull(contours[0])
    # 将凸包的特征点给连接起来
    # drawShape(img, hull)
    while True:
        cv2.imshow('img', img)
        key = cv2.waitKey()
        if key & 0xFF == ord('q'):
            break
    cv2.destroyAllWindows()

运行结果

外接矩形

这个功能是轮廓中一个更为重要的功能。它包括两种类型

  1. 最小外接矩形
  2. 最大外接矩形

在上图中,对于这个白色的闪电来说,红色框是最小外接矩形,绿色框是最大外接矩形。

我们以这张图为例,来看一下最小外接矩形和最大外接矩形

我们也一样,先来看一下该图像的轮廓

import cv2

if __name__ == "__main__":

    cv2.namedWindow('img', cv2.WINDOW_NORMAL)
    img = cv2.imread("/Users/admin/Documents/1010.jpg")
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    ret, binary = cv2.threshold(gray, 120, 255, cv2.THRESH_BINARY)
    contours, hierarchy = cv2.findContours(binary, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
    cv2.drawContours(img, contours, -1, (0, 255, 0), 3)
    while True:
        cv2.imshow('img', img)
        key = cv2.waitKey()
        if key & 0xFF == ord('q'):
            break
    cv2.destroyAllWindows()

运行结果

现在我们来看一下中间这个框的最小外接矩形

import cv2
import numpy as np

if __name__ == "__main__":

    cv2.namedWindow('img', cv2.WINDOW_NORMAL)
    img = cv2.imread("/Users/admin/Documents/1010.jpg")
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    ret, binary = cv2.threshold(gray, 120, 255, cv2.THRESH_BINARY)
    # 轮廓查找
    contours, hierarchy = cv2.findContours(binary, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
    # 获取中间Hello框的最小外接矩形
    r = cv2.minAreaRect(contours[1])
    # 获取矩形四个顶点(浮点型),并转化为整形
    box = np.int0(cv2.boxPoints(r))
    # 绘制轮廓
    cv2.drawContours(img, [box], 0, (0, 0, 255), 5)
    while True:
        cv2.imshow('img', img)
        key = cv2.waitKey()
        if key & 0xFF == ord('q'):
            break
    cv2.destroyAllWindows()

运行结果

现在我们来对中间这个框绘制最大外接矩形

import cv2
import numpy as np

if __name__ == "__main__":

    cv2.namedWindow('img', cv2.WINDOW_NORMAL)
    img = cv2.imread("/Users/admin/Documents/1010.jpg")
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    ret, binary = cv2.threshold(gray, 120, 255, cv2.THRESH_BINARY)
    # 轮廓查找
    contours, hierarchy = cv2.findContours(binary, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
    # 获取中间Hello框的最小外接矩形
    r = cv2.minAreaRect(contours[1])
    # 获取矩形四个顶点(浮点型),并转化为整形
    box = np.int0(cv2.boxPoints(r))
    # 绘制最小外接矩形
    cv2.drawContours(img, [box], 0, (0, 0, 255), 5)
    # 获取中间Hello框的最大外接矩形
    x, y, w, h = cv2.boundingRect(contours[1])
    # 绘制最大外接矩形
    cv2.rectangle(img, (x, y), (x + w, y + h), (255, 0, 0), 5)
    while True:
        cv2.imshow('img', img)
        key = cv2.waitKey()
        if key & 0xFF == ord('q'):
            break
    cv2.destroyAllWindows()

运行结果

车辆统计

这里我们需要对前面的内容进行一个全面的整理,原视频如下

我们来统计来往的车辆总数

import cv2

if __name__ == "__main__":

    # 最小宽高
    min_w = 90
    min_h = 90
    # 画线高度
    line_high = 550
    # 偏移量
    off_set = 7

    def center(x, y, w, h):
        # 计算车辆中心点
        x1 = int(w / 2)
        y1 = int(h / 2)
        cx = x + x1
        cy = y + y1
        return cx, cy

    # 创建窗口
    cv2.namedWindow('video', cv2.WINDOW_NORMAL)
    # 获取视频文件
    cap = cv2.VideoCapture("/Users/admin/Documents/video.mp4")
    # 去背景
    bgsubmog = cv2.createBackgroundSubtractorMOG2()
    # 腐蚀卷积核
    kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (7, 7))
    # 膨胀卷积核
    kernel1 = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3))
    # 通过的车辆数
    car_num = 0
    cars = []
    while True:
        # 从文件读视频桢
        ret, frame = cap.read()
        if ret:
            # 将每一帧图像转成灰度图像
            gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
            # 高斯滤波器去除高斯噪音
            gaussian = cv2.GaussianBlur(gray, (3, 3), sigmaX=25, sigmaY=25)
            # 去背景应用
            mask = bgsubmog.apply(gaussian)
            # 腐蚀去除高斯滤波器剩下的噪点
            erode = cv2.erode(mask, kernel)
            # 进行三次膨胀抵消腐蚀缩小的图形
            dilate = cv2.dilate(erode, kernel1, iterations=3)
            # 进行两次闭运算,消除车辆内部的噪点
            close = cv2.morphologyEx(dilate, cv2.MORPH_CLOSE, kernel)
            close = cv2.morphologyEx(close, cv2.MORPH_CLOSE, kernel)
            # 寻找每一帧的所有轮廓
            contours, hierarchy = cv2.findContours(close, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
            # 在图像底部画一条红线
            cv2.line(frame, (0, line_high), (1280, line_high), (0, 0, 255), 5)
            # 遍历所有的轮廓
            for i, c in enumerate(contours):
                # 获取该轮廓的最大外接矩形
                x, y, w, h = cv2.boundingRect(c)
                # 如果该外接矩形的宽和高小于最小宽高,视为噪音,过滤掉
                noValid = (w < min_w) and (h < min_h)
                if noValid:
                    continue
                # 画出目标轮廓的最大外接矩形
                cv2.rectangle(frame, (x, y), (x + w, y + h), (255, 0, 0), 5)
                # 获取该外接矩形的中心点,并放入列表中
                cpoint = center(x, y, w, h)
                cars.append(cpoint)
                for x, y in cars:
                    if (y > line_high - off_set) and (y < line_high + off_set):
                        # 如果该矩形中心点在红线偏移量范围内,车辆记数+1
                        # 并在列表中移除该中心点
                        car_num += 1
                        cars.remove((x, y))
            # 在图像上打印车辆统计数量
            cv2.putText(frame, "pass cars count:" + str(car_num), (500, 60), cv2.FONT_HERSHEY_SIMPLEX,
                        2, (0, 0, 0), 8)
            # 将视频帧在窗口中显示
            cv2.imshow('video', frame)
        # 此处不能设为1,否则会过快,可以设的比播放视频每秒帧数长一点
        key = cv2.waitKey(20)
        if key & 0xFF == ord('q'):
            break
    # 释放资源
    cap.release()
    cv2.destroyAllWindows()

运行结果

特征点检测与匹配

特征检测的基本概念

  • OpenCV特征的场景
  1. 图像搜索,如以图搜图。如果我们要在互联网上搜索一张图片,如果以全像素来搜索的话,那么这个计算量是难以估算的。实际上我们会把图的一些特征点给提取出来,可能就是很小的字节。这么少的数据再去进行搜索的时候就非常方便。比如对于Google来说,他每天从全世界获取到大量的图片,会搜索提取图像的特征点,把这些图片的主要的特征点给提取出来之后存储到数据库中,当用户进行搜索的时候,也会对用户提交的图像进行特征检测。检测出特征点后再去特征库中进行搜索。这时候就能找到多匹配的图片了。
  2. 拼图游戏,我们将人物照切成很多小的方块,在每个方块里都有一些特征值。只要我们看到了这些特征值就可以把它们拼接到一起。比方说一个头部的图像分成了四块,当我们找到了其中的一块的时候,另一块其实非常容易找到。比如说它是以鼻子分界的,左半边鼻子和右半边鼻子看到以后就可以把它们拼接到一起。这个就是通过特征查找。当我们组装头部图片的时候就会去找头部信息就不会去找手部信息,当我们组装手部的时候就会去找手部信息。对于我们人类来说也是通过特征来进行拼图的。
  3. 图像拼接,将两张有关联的图拼接到一起。当我们去看一番景色的时候,人眼看到的范围要比镜头看到的景色要广阔。那我们如何能让拍到的景象与人眼看到的景象一致呢?其实就用到了全景图像,就是用相机连续的拍照,在不同的角度上进行拍照,比如180度,360度。拍完了之后将这些图像拼接到一起,这样就形成了一个全景的图像。它与我们人眼看到的景色是一致的。有的全景图像甚至超过了人眼,它可以360度。它其实也是通过OpenCV的特征点检测。当相机拍照的每一幅图像,它们中间都是有一定重合度的,那么在重合的这一块是有特征值的,如果将两张图片的特征值都找到之后,将它们重合点拼接到一起,这样就形成了一个全景图像。
  • 寻找特征

在上面的这张图中取出了6块,这6块可以分成3组,A、B是一组,C、D是一组,E、F是一组。我们通过第一组特征,是否能知道它在哪呢?从目前来看,我们很难区分它在哪,因为很多地方是有这个信息的。对于这种平坦的图像不是唯一的,我们很难进行识别。对于第二组来说是边缘,虽然我们大概能知道它在哪里,但是我们依然无法确切知道它的具体位置。第三组是两个角,这时候我们就很容易判别出来。通过这张图,我们就知道,对于角来说它的特征足够明显,边次之,平坦就无法区分了。

  • 什么是特征

图像特征就是指有意义的图像区域,具有独特性、易于识别性,比如角点、斑点以及高密度区。

在特征中最重要的是角点,角点就是

  1. 灰度梯度的最大值对应的像素。
  2. 两条线的交点
  3. 极值点(一阶导数最大值,但二阶导数为0)

这些条件对于计算机来说是可识别的,但对于人来说是不会去这么计算的。

哈里斯(Harris)角点检测

在上图中,粉红色的小块是一个卷积核。这三张图分别代表平坦、边缘和角点的区域检测。在第一张图中,如果在卷积核的移动范围内(上下左右各个方向移动),卷积核提取的像素没有任何的变化,说明这是一个平坦的图片。在第二张图中,如果卷积核沿着边缘平行移动的时候,提取的像素是没有变化的;如果卷积核沿着边缘垂直移动的时候,那么提取的像素就会有剧烈的变化,这个时候harris会认为这是一条边缘。在第三张图中,无论卷积核朝任何一个方向移动的时候,提取的像素都会产生变化,这个时候就检测到了一个角点。总结如下

  1. 光滑地区,无论卷积核向哪里移动,衡量系数不变。
  2. 边缘地区,卷积核垂直边缘移动时,衡量系数变化剧烈。
  3. 在交点处,无论卷积核朝哪个方向移动,衡量系数都变化剧烈。

我们以这张图为例来进行角点检测说明

import cv2

if __name__ == "__main__":

    cv2.namedWindow('img', cv2.WINDOW_NORMAL)
    img = cv2.imread("/Users/admin/Documents/888.jpg")
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    # Harris角点检测,2为检测窗口大小,3为索贝尔卷积核大小,
    # 0.04为权重系数,经验值,一般取0.02~0.04之间
    dst = cv2.cornerHarris(gray, 2, 3, 0.04)
    # Harris角点的展示
    img[dst > 0.01 * dst.max()] = [0, 0, 255]
    while True:
        cv2.imshow('img', img)
        key = cv2.waitKey()
        if key & 0xFF == ord('q'):
            break
    cv2.destroyAllWindows()

运行结果

这里我们可以看到在每个交叉的地方都有红色的小点,一些数字拐角的地方也有这些红色的小点。

Shi-Tomasi角点检测

Shi-Tomasi是Harris角点检测的改进,前面我们看到Harris角点检测有一个权重系数经验值,这个值对不同的图片的角点检测可能会不同,在0.02~0.04之间。而Tomasi就不用设置这个权重系数了。

import cv2
import numpy as np

if __name__ == "__main__":

    cv2.namedWindow('img', cv2.WINDOW_NORMAL)
    img = cv2.imread("/Users/admin/Documents/888.jpg")
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    # Harris角点检测,2为检测窗口大小,3为索贝尔卷积核大小,
    # 0.04为权重系数,经验值,一般取0.02~0.04之间
    # dst = cv2.cornerHarris(gray, 2, 3, 0.04)
    # Harris角点的展示
    # img[dst > 0.01 * dst.max()] = [0, 255, 0]
    # Tomasi角点检测,1000为角点的最大数,值为0表示无限制
    # 0.01这个参数一般取0.01~1之间的数
    # 10表示角之间的最小欧式距离,忽略小于此距离的点
    corners = cv2.goodFeaturesToTrack(gray, 1000, 0.01, 10)
    corners = np.int0(corners)
    # Tomasi绘制角点
    for i in corners:
        x, y = i.ravel()
        cv2.circle(img, (x, y), 3, (255, 0, 0), -1)
    while True:
        cv2.imshow('img', img)
        key = cv2.waitKey()
        if key & 0xFF == ord('q'):
            break
    cv2.destroyAllWindows()

运行结果

SIFT关键点检测

  • SIFT出现的原因
  1. Harris角点具有旋转不变的特性,无论图像如何旋转,角点是不会发生变化的。
  2. 缩放后,原来的角点有可能就不是角点了。比如说图放大后

最左边的区域在我们用卷积核检测时发现这里是一个角,但当该图放大后,我们再使用卷积核去检测的时候,会发现它变成了一个边缘。这就是Harris角点检测时存在的一个巨大的问题。

  • 关键点与描述子
  1. 关键点:位置、大小和方向。如果有的关键点特别小,实际我们是需要给它过滤掉,减少计算量。因为这些小的关键点对于特征的查找和匹配意义不大。
  2. 关键点描述子:记录了关键点周围对其有贡献的像素点的一组向量,其不受仿射变换、光照变换等影响。
import cv2

if __name__ == "__main__":

    cv2.namedWindow('img', cv2.WINDOW_NORMAL)
    img = cv2.imread("/Users/admin/Documents/888.jpg")
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    # 实例化SIFT对象
    sift = cv2.SIFT_create()
    # 获取灰度图像中的关键点,返回值kp为关键点
    # 关键点包含角点,一阶导数极值点
    kp = sift.detect(gray, None)
    # 计算描述子des
    kp, des = sift.compute(img, kp)
    # 上面两步可以合并成一步
    # kp, des = sift.detectAndCompute(gray, None)
    # 打印第0个描述子
    print(des[0])
    # 绘出关键点
    cv2.drawKeypoints(gray, kp, img)
    while True:
        cv2.imshow('img', img)
        key = cv2.waitKey()
        if key & 0xFF == ord('q'):
            break
    cv2.destroyAllWindows()

运行结果

[  0.  12. 156.   2.   0.   0.   0.   0.  10.  87.  87.   0.   0.   0.
   0.   0.   7.  18.   2.   0.   0.   0.   0.   0.   0.   0.   0.   0.
   0.   0.   0.   0.  18.  42. 156.  13.  13.  17.   5.   1. 156. 156.
  79.   1.   1.   0.   0.   5.  97.  26.   0.   0.   0.   0.   0.   1.
   2.   0.   0.   0.   0.   0.   0.   0.  38.   5.   5.  35.  88.  35.
   9.  78. 156.  14.   1.   0.   2.   1.   1. 156. 105.   1.   0.   0.
   0.   0.   0.  24.   1.   0.   0.   0.   0.   0.   0.   1.   9.   0.
   0.  30.  13.   0.   3. 156.  45.   0.   0.   0.   0.   0.   0. 156.
  14.   0.   0.   0.   0.   0.   0.  20.   1.   0.   0.   0.   0.   0.
   0.   0.]

通过打印出来的描述子我们可以看到,它是某个关键点周围提供贡献的像素值,在进行匹配的时候才能根据这些描述子来进行匹配。

SURF(Speeded-Up Robust Features)特征检测

SIFT进行特征检测的时候非常准确,描述子也描述的非常详细。但是SIFT最大的问题是速度慢,因此才有SURF。如果我们想对一系列的图片快速的进行特征检测获取描述子的话,使用SIFT会非常的慢。SURF保留了SIFT的优点,而且检测速度也比较快。但是由于专利原因,暂不可用。

ORB特征检测

ORB可以做到实时监测,它对SIFT和SURF做了两点改进,一块是对特征点检测做了改进,另一块是对描述子的计算做了改进。

ORB = Oriented FAST + Rotated BRIEF

ORB有优点,也有缺点。要做到实时检测,实际上就是对数据量的缩减,在区域划分的时候,就要抛弃一些没有必要的点。对描述子的计算也是抛弃了大量的数据。虽然ORB快了,但是它的准确率就不如SIFT和SURF。所以对检测数量不多的图片,且对准确率要求较高时,我们应该使用SIFT;而要检测的图片的数量非常庞大的时候,就应该要使用ORB了。

  1. FAST:可以做到特征点的实时检测。它进行检测的时候,检测出的特征点是不带方向的。所以又给它增加了方向的属性,所以成了Oriented FAST
  2. BRIEF:是对已经检测到的特征点进行描述。它加快了特征描述符建立的速度,同时也极大的降低了特征匹配的时间。实际是对特征点描述子的计算。对于BRIEF来说对图像的旋转处理的不太好,所以增加了Rotated BRIEF,对它进行了改进,对旋转具有鲁棒性。
import cv2

if __name__ == "__main__":

    cv2.namedWindow('img', cv2.WINDOW_NORMAL)
    img = cv2.imread("/Users/admin/Documents/888.jpg")
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    # 实例化ORB对象
    orb = cv2.ORB_create()
    # 获取灰度图像中的关键点和描述子,返回值kp为关键点
    # 关键点包含角点,一阶导数极值点,des为描述子
    kp, des = orb.detectAndCompute(gray, None)
    # 打印第0个描述子
    print(des[0])
    # 绘出关键点
    cv2.drawKeypoints(gray, kp, img)
    while True:
        cv2.imshow('img', img)
        key = cv2.waitKey()
        if key & 0xFF == ord('q'):
            break
    cv2.destroyAllWindows()

运行结果

[130 205  38 216  21 133 172 103 148  18  80  18 168   8  52  65  77  35
  19 129 242 172  22 255  33  26 222 192  21  12 241 136]

对比ORB和SIFT的结果,我们会发现无论是描述子还是画出的关键点,ORB的数量都相对较少。

暴力特征匹配

  • 特征匹配方法
  1. BF(Brute-Force),暴力特征匹配方法
  2. FLANN,最快近邻区特征匹配方法
  • 暴力特征匹配原理

它使用第一组中的每个特征的描述子,与第二组中的所有特征描述子进行匹配,计算它们之间的差距(相似度),然后将最接近一个匹配返回。

这里我们要进行匹配的这样两幅图

以及

import cv2

if __name__ == "__main__":

    cv2.namedWindow('img', cv2.WINDOW_NORMAL)
    img1 = cv2.imread("/Users/admin/Documents/opencv_search.png")
    img2 = cv2.imread("/Users/admin/Documents/opencv_orig.png")
    g1 = cv2.cvtColor(img1, cv2.COLOR_BGRA2BGR)
    g2 = cv2.cvtColor(img2, cv2.COLOR_BGRA2BGR)
    sift = cv2.SIFT_create()
    kp1, des1 = sift.detectAndCompute(g1, None)
    kp2, des2 = sift.detectAndCompute(g2, None)
    # 创建匹配器,cv2.NORM_L1为匹配类型,对描述子取绝对值进行加法运算,还有
    # cv2.NORM_L2对描述子取平方和开方,这两种都是为SIFT和SURF提供的类型
    # 还有cv2.NORM_HAMMING判断二进制位在第几位开始不同,如果相同的越多,则速度越高
    # 和cv2.NORM_HAMMING2,是为ORB提供的类型
    # 还有一个参数为crossCheck,默认False,表示是否进行交叉匹配,即两幅图互相查找特征
    bf = cv2.BFMatcher(cv2.NORM_L1)
    # 进行特征匹配
    match = bf.match(des1, des2)
    # 绘制匹配点
    img3 = cv2.drawMatches(img1, kp1, img2, kp2, match, None)
    while True:
        cv2.imshow('img', img3)
        key = cv2.waitKey()
        if key & 0xFF == ord('q'):
            break
    cv2.destroyAllWindows()

运行结果

这里我们可以看到,它会把具有相似的特征点通过直线给串联起来直观的展示出来。但是也会有匹配有误的地方。我们可以利用这一特性来查找我们需要的图片

import cv2
import os

if __name__ == '__main__':

    file_list = []
    path = '/Users/admin/Downloads/youyi/'
    files = os.listdir(path)
    for file in files:
        file_path = os.path.join(path, file)
        if os.path.isfile(file_path) and file_path.endswith('.jpg'):
            file_list.append(file_path)
    img_query = cv2.imread(path + 'query/query.jpg')
    sift = cv2.SIFT_create()
    kp1, des1 = sift.detectAndCompute(img_query, None)
    bf = cv2.BFMatcher(cv2.NORM_L1)
    distance_threshold = 300.0
    for file in file_list:
        file_name = file.split('/')[-1]
        print(file_name)
        img = cv2.imread(file)
        kp2, des2 = sift.detectAndCompute(img, None)
        matches = bf.match(des1, des2)
        for m in matches:
            if m.distance < distance_threshold:
                cv2.imwrite(path + 'result/' + file_name, img)

FLANN特征匹配

相比暴力匹配,FLANN在进行批量特征匹配时,速度更快。但由于FLANN使用的是邻近近似值,所以精度较差。如果我们要进行图像的精度匹配,我们应该使用暴力特征匹配。

import cv2

if __name__ == "__main__":

    cv2.namedWindow('img', cv2.WINDOW_NORMAL)
    img1 = cv2.imread("/Users/admin/Documents/opencv_search.png")
    img2 = cv2.imread("/Users/admin/Documents/opencv_orig.png")
    g1 = cv2.cvtColor(img1, cv2.COLOR_BGRA2BGR)
    g2 = cv2.cvtColor(img2, cv2.COLOR_BGRA2BGR)
    sift = cv2.SIFT_create()
    kp1, des1 = sift.detectAndCompute(g1, None)
    kp2, des2 = sift.detectAndCompute(g2, None)
    index_params = dict(algorithm=1, trees=5)
    search_params = dict(checks=50)
    # 创建匹配器,index_params为一个字典,它有两种算法KDTREE和LSH
    # KDTREE是给SIFT和SURF使用的,LSH是给ORB使用的,如果调换会报错
    # search_params为一个字典,指定KDTREE算法中遍历树的次数
    flann = cv2.FlannBasedMatcher(index_params, search_params)
    # 对描述子进行特征匹配,k表示欧式距离最近的前k个关键点
    # 返回的是DMatch对象,它包含了distance,描述子之间的距离(近似度),值越低越好
    # 以及queryIdx,第一个图像的描述子索引值,trainIdx,第二个图像的描述子索引
    # imgIdx,第二个图的索引值
    matchs = flann.knnMatch(des1, des2, k=2)
    # 创建一个最好距离的列表
    good = []
    # 遍历返回结果的所有距离
    for i, (m, n) in enumerate(matchs):
        if m.distance < 0.7 * n.distance:
            good.append(m)
    # 绘制匹配点
    img3 = cv2.drawMatchesKnn(img1, kp1, img2, kp2, [good], None)
    while True:
        cv2.imshow('img', img3)
        key = cv2.waitKey()
        if key & 0xFF == ord('q'):
            break
    cv2.destroyAllWindows()

运行结果

相比于暴力匹配,我们会发现,它的匹配结果比较少。也就是说它的匹配效果不如暴力匹配。

图像查找

对于批量图像查找来说,使用FLANN更好一些,当我们获取特征匹配之后,把它们输入单应性矩阵,拿到单应性矩阵再经过透视变换就可以获取到最终的图像了。即特征匹配+单应性矩阵+透视变换

  • 单应性矩阵

在上图所示,有两个相机对底层的相同物品进行拍照,但是这两个相机的角度不同,这样第一个相机拍到的照片就是image1,第二个相机拍到的照片就是image2。对于物品的同一个点X,image1上对应的是x,image2上对应的是x'。单应性矩阵与image1进行运算,此时的x点就可以得到image2中x'的位置。同样道理,单应性矩阵与image2进行运算可以得到原始物体的X的位置;单应性矩阵与image1进行运算也可以得到原始物体的X的位置。它其实是一种线性变换,有关线性变换的内容可以参考线性代数整理(二)

  • 单应性的应用

比方说,我们从一个角度拍摄的银行卡就可以通过单应性矩阵给转成一张正面照。

这里左边的是一张原始图,它其中有一副巨大的广告牌,此时我们可以使用单应性矩阵将该广告牌给替换成右图中我们自己的广告牌,而无需去手动抠图,修改。

import cv2
import numpy as np

if __name__ == "__main__":

    cv2.namedWindow('img', cv2.WINDOW_NORMAL)
    img1 = cv2.imread("/Users/admin/Documents/666.jpg")
    img2 = cv2.imread("/Users/admin/Documents/8888.jpg")
    g1 = cv2.cvtColor(img1, cv2.COLOR_BGRA2BGR)
    g2 = cv2.cvtColor(img2, cv2.COLOR_BGRA2BGR)
    sift = cv2.SIFT_create()
    kp1, des1 = sift.detectAndCompute(g1, None)
    kp2, des2 = sift.detectAndCompute(g2, None)
    # 创建匹配器,cv2.NORM_L1为匹配类型,对描述子取绝对值进行加法运算,还有
    # cv2.NORM_L2对描述子取平方和开方,这两种都是为SIFT和SURF提供的类型
    # 还有cv2.NORM_HAMMING判断二进制位在第几位开始不同,如果相同的越多,则速度越高
    # 和cv2.NORM_HAMMING2,是为ORB提供的类型
    # 还有一个参数为crossCheck,默认False,表示是否进行交叉匹配,即两幅图互相查找特征
    bf = cv2.BFMatcher(cv2.NORM_L2)
    # 进行特征匹配
    matchs = bf.knnMatch(des1, des2, k=2)
    # 创建一个最好距离的列表
    good = []
    # 遍历返回结果的所有距离
    for i, (m, n) in enumerate(matchs):
        if m.distance < 0.7 * n.distance:
            good.append(m)
    if len(good) >= 4:
        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)
        # 获取单应性矩阵H,src_pts为原图的关键点坐标,dst_pts为查找图的关键点坐标
        # cv2.RANSAC表示对错误的匹配点做一个过滤,具体含义为随机抽样抑制算法
        # 5.0是一个阈值,是一个经验值,在1~10之间
        # 单应性矩阵即为查找图的区域坐标
        H, _ = cv2.findHomography(src_pts, dst_pts, cv2.RANSAC, 5.0)
        # 获取原图的高和宽
        h, w = img1.shape[:2]
        # 获取原图区域
        pts = np.float32([[0, 0], [0, h - 1], [w - 1, h - 1], [w - 1, 0]]).reshape(-1, 1, 2)
        # 进行透视变换,得到变换后的图像范围
        dst = np.int32(cv2.perspectiveTransform(pts, H))
        # 在查找图中画出查找范围的矩形
        cv2.polylines(img2, [dst], True, (0, 0, 255), 5)
    else:
        print("好的数据点太少,必须不少于4")
        exit()
    # 绘制匹配点
    img3 = cv2.drawMatchesKnn(img1, kp1, img2, kp2, matchs, None)
    while True:
        cv2.imshow('img', img3)
        key = cv2.waitKey()
        if key & 0xFF == ord('q'):
            break
    cv2.destroyAllWindows()

运行结果

图像拼接

这里我们会对前面的内容做一个整理,我们要拼接的两张图片如下

以及

这里我们可以看到,这两张图像是具有非常多的相同的特征点的。我们先将这两张图原始拼接在一起。

import cv2
import numpy as np

if __name__ == "__main__":

    cv2.namedWindow('img', cv2.WINDOW_NORMAL)
    img1 = cv2.imread("/Users/admin/Documents/map1.png")
    img2 = cv2.imread("/Users/admin/Documents/map2.png")
    img1 = cv2.resize(img1, (640, 480))
    img2 = cv2.resize(img2, (640, 480))
    img3 = np.hstack((img1, img2))
    while True:
        cv2.imshow('img', img3)
        key = cv2.waitKey()
        if key & 0xFF == ord('q'):
            break
    cv2.destroyAllWindows()

运行结果

现在来根据寻找到的两张图匹配的特征点完成图像的拼接

import cv2
import numpy as np

if __name__ == "__main__":

    cv2.namedWindow('img', cv2.WINDOW_NORMAL)
    img1 = cv2.imread("/Users/admin/Documents/map1.png")
    img2 = cv2.imread("/Users/admin/Documents/map2.png")
    img1 = cv2.resize(img1, (640, 480))
    img2 = cv2.resize(img2, (640, 480))
    img3 = np.hstack((img1, img2))

    def get_homo(img1, img2):
        '''
        获取单应性矩阵
        :param img1:
        :param img2:
        :return:
        '''
        # 获取特征转换对象
        sift = cv2.SIFT_create()
        g1 = cv2.cvtColor(img1, cv2.COLOR_BGRA2BGR)
        g2 = cv2.cvtColor(img2, cv2.COLOR_BGRA2BGR)
        # 获取关键点和描述子
        kp1, des1 = sift.detectAndCompute(g1, None)
        kp2, des2 = sift.detectAndCompute(g2, None)
        # 创建暴力匹配器
        bf = cv2.BFMatcher(cv2.NORM_L2)
        # 进行特征匹配
        matchs = bf.knnMatch(des1, des2, k=2)
        # 设置特征比例
        verify_ratio = 0.8
        # 有效特征点
        verify_matches = []
        # 过滤无效匹配点,获取有效匹配点
        for m1, m2 in matchs:
            if m1.distance < verify_ratio * m2.distance:
                verify_matches.append(m1)
        # 设置最小应匹配的个数
        min_matches = 8
        # 有效匹配点的数量必须达到最小值
        if len(verify_matches) > min_matches:
            img1_pts = []
            img2_pts = []
            # 获取图1、图2的匹配的特征点坐标
            for m in verify_matches:
                img1_pts.append(kp1[m.queryIdx].pt)
                img2_pts.append(kp2[m.trainIdx].pt)
            img1_pts = np.float32(img1_pts).reshape(-1, 1, 2)
            img2_pts = np.float32(img2_pts).reshape(-1, 1, 2)
            # 根据两幅图匹配的特征点坐标获取单应性矩阵
            H, _ = cv2.findHomography(img1_pts, img2_pts, cv2.RANSAC, 5.0)
            return H
        else:
            print("好的匹配点太少,必须不少于8")
            exit()

    def stitch_image(img1, img2, H):
        '''
        拼接图像
        :param img1:
        :param img2:
        :param H:
        :return:
        '''
        # 获取两张图像的高和宽
        h1, w1 = img1.shape[:2]
        h2, w2 = img2.shape[:2]
        # 获取两张图片的四个顶点
        img1_dims = np.float32([[0, 0], [0, h1], [w1, h1], [w1, 0]]).reshape(-1, 1, 2)
        img2_dims = np.float32([[0, 0], [0, h2], [w2, h2], [w2, 0]]).reshape(-1, 1, 2)
        # 获取第一张图的透视变换后的图像范围
        img1_transform = cv2.perspectiveTransform(img1_dims, H)
        # 将第一张图像变换后的图像范围与第二张图像的范围给拼接起来
        result_dims = np.concatenate((img1_transform, img2_dims), axis=0)
        # 获取联合范围的最小值坐标和最大值坐标
        [x_min, y_min] = np.int32(result_dims.min(axis=0).ravel() - 0.5)
        [x_max, y_max] = np.int32(result_dims.max(axis=0).ravel() + 0.5)
        # 由于第一张图透视变换后的图像范围超出边界,
        # 则联合范围的最小值的x、y坐标都是负数
        # 现在将其转成正数
        transform_dist = [-x_min, -y_min]
        # 设置平移矩阵,可以将超出边框的图像移动到边框内
        transform_array = np.array([[1, 0, transform_dist[0]],
                                    [0, 1, transform_dist[1]],
                                    [0, 0, 1]])
        # 对图一进行透视变换,转换矩阵为平移矩阵与单应性矩阵的点积表示图一不仅进行了单应性转换还进行了平移
        result_img = cv2.warpPerspective(img1, transform_array.dot(H), (x_max - x_min, y_max - y_min))
        # 将图二放入对接的区域中
        result_img[transform_dist[1]:transform_dist[1] + h2, transform_dist[0]:transform_dist[0] + w2] = img2
        return result_img

    H = get_homo(img1, img2)
    result_img = stitch_image(img1, img2, H)
    while True:
        cv2.imshow('img', result_img)
        key = cv2.waitKey()
        if key & 0xFF == ord('q'):
            break
    cv2.destroyAllWindows()

运行结果

现在我们来看一下两幅图的匹配的特征点以及拼接以后的图像查找的区域

import cv2
import numpy as np

if __name__ == "__main__":

    cv2.namedWindow('img', cv2.WINDOW_NORMAL)
    cv2.namedWindow('img3', cv2.WINDOW_NORMAL)
    img1 = cv2.imread("/Users/admin/Documents/map1.png")
    img2 = cv2.imread("/Users/admin/Documents/map2.png")
    img1 = cv2.resize(img1, (640, 480))
    img2 = cv2.resize(img2, (640, 480))

    def get_homo(img1, img2):
        '''
        获取单应性矩阵
        :param img1:
        :param img2:
        :return:
        '''
        # 获取特征转换对象
        sift = cv2.SIFT_create()
        g1 = cv2.cvtColor(img1, cv2.COLOR_BGRA2BGR)
        g2 = cv2.cvtColor(img2, cv2.COLOR_BGRA2BGR)
        # 获取关键点和描述子
        kp1, des1 = sift.detectAndCompute(g1, None)
        kp2, des2 = sift.detectAndCompute(g2, None)
        # 创建暴力匹配器
        bf = cv2.BFMatcher(cv2.NORM_L2)
        # 进行特征匹配
        matchs = bf.knnMatch(des1, des2, k=2)
        # 设置特征比例
        verify_ratio = 0.4
        # 有效特征点
        verify_matches = []
        matchs_new = []
        # 过滤无效匹配点,获取有效匹配点
        for i, (m1, m2) in enumerate(matchs):
            if m1.distance < verify_ratio * m2.distance:
                verify_matches.append(m1)
                matchs_new.append(matchs[i])
        # 设置最小应匹配的个数
        min_matches = 8
        # 有效匹配点的数量必须达到最小值
        if len(verify_matches) > min_matches:
            img1_pts = []
            img2_pts = []
            # 获取图1、图2的匹配的特征点坐标
            for m in verify_matches:
                img1_pts.append(kp1[m.queryIdx].pt)
                img2_pts.append(kp2[m.trainIdx].pt)
            img1_pts = np.float32(img1_pts).reshape(-1, 1, 2)
            img2_pts = np.float32(img2_pts).reshape(-1, 1, 2)
            # 根据两幅图匹配的特征点坐标获取单应性矩阵
            H, _ = cv2.findHomography(img1_pts, img2_pts, cv2.RANSAC, 5.0)
            H1, _ = cv2.findHomography(img2_pts, img1_pts, cv2.RANSAC, 5.0)
            img3 = cv2.drawMatchesKnn(img1, kp1, img2, kp2, matchs_new, None)
            h1, w1 = img1.shape[:2]
            h2, w2 = img2.shape[:2]
            # 获取原图区域
            pts1 = np.float32([[0, 0], [0, h1 - 1], [w1 - 1, h1 - 1], [w1 - 1, 0]]).reshape(-1, 1, 2)
            pts2 = np.float32([[0, 0], [0, h2 - 1], [w2 - 1, h2 - 1], [w2 - 1, 0]]).reshape(-1, 1, 2)
            # 获取透视变换的转换范围
            dst1 = np.int32(cv2.perspectiveTransform(pts1, H))
            dst2 = np.int32(cv2.perspectiveTransform(pts2, H1))
            # 在查找图中画出查找范围的矩形
            cv2.polylines(img2, [dst1], True, (0, 0, 255), 5)
            cv2.polylines(img1, [dst2], True, (0, 0, 255), 5)
            return H, img3
        else:
            print("好的匹配点太少,必须不少于8")
            exit()

    def stitch_image(img1, img2, H):
        '''
        拼接图像
        :param img1:
        :param img2:
        :param H:
        :return:
        '''
        # 获取两张图像的高和宽
        h1, w1 = img1.shape[:2]
        h2, w2 = img2.shape[:2]
        # 获取两张图片的四个顶点
        img1_dims = np.float32([[0, 0], [0, h1], [w1, h1], [w1, 0]]).reshape(-1, 1, 2)
        img2_dims = np.float32([[0, 0], [0, h2], [w2, h2], [w2, 0]]).reshape(-1, 1, 2)
        # 获取第一张图的透视变换后的图像范围
        img1_transform = cv2.perspectiveTransform(img1_dims, H)
        # 将第一张图像变换后的图像范围与第二张图像的范围给拼接起来
        result_dims = np.concatenate((img1_transform, img2_dims), axis=0)
        # 获取联合范围的最小值坐标和最大值坐标
        [x_min, y_min] = np.int32(result_dims.min(axis=0).ravel() - 0.5)
        [x_max, y_max] = np.int32(result_dims.max(axis=0).ravel() + 0.5)
        # 由于第一张图透视变换后的图像范围超出边界,
        # 则联合范围的最小值的x、y坐标都是负数
        # 现在将其转成正数
        transform_dist = [-x_min, -y_min]
        # 设置平移矩阵,可以将超出边框的图像移动到边框内
        transform_array = np.array([[1, 0, transform_dist[0]],
                                    [0, 1, transform_dist[1]],
                                    [0, 0, 1]])
        # 对图一进行透视变换,转换矩阵为平移矩阵与单应性矩阵的点积表示图一不仅进行了单应性转换还进行了平移
        result_img = cv2.warpPerspective(img1, transform_array.dot(H), (x_max - x_min, y_max - y_min))
        # 将图二放入对接的区域中
        result_img[transform_dist[1]:transform_dist[1] + h2, transform_dist[0]:transform_dist[0] + w2] = img2
        return result_img

    H, img3 = get_homo(img1, img2)
    result_img = stitch_image(img1, img2, H)
    while True:
        cv2.imshow('img', result_img)
        cv2.imshow('img3', img3)
        key = cv2.waitKey()
        if key & 0xFF == ord('q'):
            break
    cv2.destroyAllWindows()

运行结果

不过OpenCV有自带的图像拼接器

import cv2

if __name__ == "__main__":

    cv2.namedWindow('img', cv2.WINDOW_NORMAL)
    img1 = cv2.imread("/Users/admin/Documents/map1.png")
    img2 = cv2.imread("/Users/admin/Documents/map2.png")
    img1 = cv2.resize(img1, (640, 480))
    img2 = cv2.resize(img2, (640, 480))
    # 创建一个图像拼接器对象
    stitcher = cv2.Stitcher.create()
    # 进行图像拼接,返回的结果是一个元组,包含了索引和拼接后的图像
    result_img = stitcher.stitch([img1, img2])
    while True:
        cv2.imshow('img', result_img[1])
        key = cv2.waitKey()
        if key & 0xFF == ord('q'):
            break
    cv2.destroyAllWindows()

运行结果

展开阅读全文
加载中
点击引领话题📣 发布并加入讨论🔥
0 评论
3 收藏
2
分享
返回顶部
顶部