跳转至

七、对图像进行卡通化

在本章中,我们将学习如何将图像转换为卡通图像。 我们将学习如何在实时视频流中访问网络摄像头并进行键盘/鼠标输入。 我们还将学习一些高级图像过滤器,并了解如何使用它们对图像进行卡通化。

在本章结束时,您将了解:

  • 如何访问网络摄像头
  • 如何在实时视频流中进行键盘和鼠标输入
  • 如何创建一个交互式应用
  • 如何使用高级图像过滤器
  • 如何将图像卡通化

访问网络摄像头

我们可以使用网络摄像头的实时视频流构建非常有趣的应用。 OpenCV 提供了一个视频捕获对象,该对象可以处理与打开和关闭网络摄像头有关的所有事情。 我们需要做的就是创建该对象并不断读取其框架。

以下代码将打开网络摄像头,捕获帧,将其缩小 2 倍,然后在窗口中显示它们。 您可以按Esc键退出。

import cv2

cap = cv2.VideoCapture(0)

# Check if the webcam is opened correctly
if not cap.isOpened():
    raise IOError("Cannot open webcam")

while True:
    ret, frame = cap.read()
    frame = cv2.resize(frame, None, fx=0.5, fy=0.5, interpolation=cv2.INTER_AREA)
    cv2.imshow('Input', frame)

    c = cv2.waitKey(1)
    if c == 27:
        break

cap.release()
cv2.destroyAllWindows()

底层原理

从前面的代码中可以看到,我们使用 OpenCV 的VideoCapture函数来创建视频捕获对象上限。 创建完毕后,我们将开始无限循环并不断从网络摄像头读取帧,直到遇到键盘中断为止。 在 while 循环的第一行中,我们有以下行:

ret, frame = cap.read()

此处,ret是由read函数返回的布尔值,它指示是否成功捕获了帧。 如果正确捕获了帧,则将其存储在变量frame中。 该循环将一直运行,直到我们按Esc键。 因此,我们在下面的行中继续检查键盘中断:

if c == 27:

众所周知,Esc的 ASCII 值为 27。一旦遇到它,我们就会中断循环并释放视频捕获对象。 cap.release()行很重要,因为它可以正常关闭网络摄像头。

键盘输入

现在我们知道如何从网络摄像头捕获实时视频流,让我们看看如何使用键盘与显示视频流的窗口进行交互。

import argparse

import cv2

def argument_parser():
    parser = argparse.ArgumentParser(description="Change color space of the \
            input video stream using keyboard controls. The control keys are: \
            Grayscale - 'g', YUV - 'y', HSV - 'h'")
    return parser

if __name__=='__main__':
    args = argument_parser().parse_args()

    cap = cv2.VideoCapture(0)

    # Check if the webcam is opened correctly
    if not cap.isOpened():
        raise IOError("Cannot open webcam")

    cur_char = -1
    prev_char = -1

    while True:
        # Read the current frame from webcam
        ret, frame = cap.read()

        # Resize the captured image
        frame = cv2.resize(frame, None, fx=0.5, fy=0.5, interpolation=cv2.INTER_AREA)

        c = cv2.waitKey(1)

        if c == 27:
            break

        if c > -1 and c != prev_char:
            cur_char = c
        prev_char = c

        if cur_char == ord('g'):
            output = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
        elif cur_char == ord('y'):
            output = cv2.cvtColor(frame, cv2.COLOR_BGR2YUV)

        elif cur_char == ord('h'):
            output = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV)

        else:
            output = frame

        cv2.imshow('Webcam', output)

    cap.release()
    cv2.destroyAllWindows()

与应用交互

该程序将显示输入视频流,并等待键盘输入更改色彩空间。 如果运行先前的程序,则将看到一个窗口,显示来自网络摄像头的输入视频流。 如果按G,您将看到输入流的色彩空间已转换为灰度。 如果按Y,则输入流将转换为 YUV 色彩空间。 同样,如果按H,您将看到图像正在转换为 HSV 色彩空间。

众所周知,我们使用函数waitKey()来监听键盘事件。 当我们遇到不同的按键时,我们将采取适当的措施。 我们使用函数ord()的原因是因为waitKey()返回键盘输入的 ASCII 值。 因此,我们需要先将字符转换为 ASCII 形式,然后再检查其值。

鼠标输入

在本节中,我们将看到如何使用鼠标与显示窗口进行交互。 让我们从简单的事情开始。 我们将编写一个程序,该程序将检测在其中检测到鼠标单击的象限。 一旦检测到它,我们将突出显示该象限。

import cv2
import numpy as np

def detect_quadrant(event, x, y, flags, param):
    if event == cv2.EVENT_LBUTTONDOWN:
        if x > width/2:
            if y > height/2:
                point_top_left = (int(width/2), int(height/2))
                point_bottom_right = (width-1, height-1)
            else:
                point_top_left = (int(width/2), 0)
                point_bottom_right = (width-1, int(height/2))

        else:
            if y > height/2:
                point_top_left = (0, int(height/2))
                point_bottom_right = (int(width/2), height-1)
            else:
                point_top_left = (0, 0)
                point_bottom_right = (int(width/2), int(height/2))

        cv2.rectangle(img, (0,0), (width-1,height-1), (255,255,255), -1)
        cv2.rectangle(img, point_top_left, point_bottom_right, (0,100,0), -1)

if __name__=='__main__':
    width, height = 640, 480
    img = 255 * np.ones((height, width, 3), dtype=np.uint8)
    cv2.namedWindow('Input window')
    cv2.setMouseCallback('Input window', detect_quadrant)

    while True:
        cv2.imshow('Input window', img)
        c = cv2.waitKey(10)
        if c == 27:
            break

    cv2.destroyAllWindows()

输出看起来像,如下图所示:

Mouse inputs

下面发生了什么?

让我们从该程序的main函数开始。 我们创建一个白色图像,将使用鼠标单击该图像。 然后,我们创建一个命名窗口并将鼠标回调函数绑定到该窗口。 基本上,鼠标回调函数是在检测到鼠标事件时将调用的函数。 鼠标事件有很多种,例如单击,双击,拖动等等。 在我们的例子中,我们只想检测鼠标单击。 在函数detect_quadrant中,我们检查第一个输入参数事件以查看执行了什么操作。 OpenCV 提供了一组预定义的事件,我们可以使用特定的关键字来调用它们。 如果要查看所有鼠标事件的列表,可以转到 Python shell 并键入以下内容:

>>> import cv2
>>> print [x for x in dir(cv2) if x.startswith('EVENT')]

函数detect_quadrant中的第二个和第三个参数提供了鼠标单击事件的 X 和 Y 坐标。 一旦知道了这些坐标,就可以很容易地确定所在的象限。有了这些信息,我们就可以继续使用cv2.rectangle()绘制具有指定颜色的矩形。 这是一个非常方便的函数,它使用左上角和右下角在具有指定颜色的图像上绘制矩形。

与实时视频流互动

让我们看看我们如何使用鼠标与网络摄像头中的实时视频流进行交互。 我们可以使用鼠标选择一个区域,然后在该区域上应用“负片”效果,如下所示:

Interacting with a live video stream

在下面的程序中,我们将捕获来自网络摄像头的视频流,用鼠标选择一个兴趣区域,然后应用效果:

import cv2
import numpy as np

def draw_rectangle(event, x, y, flags, params):
    global x_init, y_init, drawing, top_left_pt, bottom_right_pt

    if event == cv2.EVENT_LBUTTONDOWN:
        drawing = True
        x_init, y_init = x, y

    elif event == cv2.EVENT_MOUSEMOVE:
        if drawing:
            top_left_pt = (min(x_init, x), min(y_init, y))
            bottom_right_pt = (max(x_init, x), max(y_init, y))
            img[y_init:y, x_init:x] = 255 - img[y_init:y, x_init:x]

    elif event == cv2.EVENT_LBUTTONUP:
        drawing = False
        top_left_pt = (min(x_init, x), min(y_init, y))
        bottom_right_pt = (max(x_init, x), max(y_init, y))
        img[y_init:y, x_init:x] = 255 - img[y_init:y, x_init:x]

if __name__=='__main__':
    drawing = False
    top_left_pt, bottom_right_pt = (-1,-1), (-1,-1)

    cap = cv2.VideoCapture(0)

    # Check if the webcam is opened correctly
    if not cap.isOpened():
        raise IOError("Cannot open webcam")

    cv2.namedWindow('Webcam')
    cv2.setMouseCallback('Webcam', draw_rectangle)

    while True:
        ret, frame = cap.read()
        img = cv2.resize(frame, None, fx=0.5, fy=0.5, interpolation=cv2.INTER_AREA)
        (x0,y0), (x1,y1) = top_left_pt, bottom_right_pt
        img[y0:y1, x0:x1] = 255 - img[y0:y1, x0:x1]
        cv2.imshow('Webcam', img)

        c = cv2.waitKey(1)
        if c == 27:
            break

    cap.release()
    cv2.destroyAllWindows()

如果运行前面的程序,将会看到一个显示视频流的窗口。 您可以使用鼠标在窗口上绘制一个矩形,然后您会看到该区域被转换为“负”区域。

我们是怎么做到的?

正如我们在程序的main函数中看到的一样,我们初始化了一个视频捕获对象。 然后,我们在下面的行中将函数draw_rectangle与鼠标回调绑定在一起:

cv2.setMouseCallback('Webcam', draw_rectangle)

然后,我们开始无限循环并开始捕获视频流。 让我们看看函数draw_rectangle中发生了什么。 每当我们使用鼠标绘制矩形时,我们基本上都必须检测三种类型的鼠标事件:鼠标单击,鼠标移动和鼠标按钮释放。 这正是我们在此函数中所做的。 每当我们检测到鼠标单击事件时,我们都会初始化矩形的左上角。 当我们移动鼠标时,我们通过将当前位置保持为矩形的右下角来选择兴趣区域。

一旦有了兴趣区域,我们只需反转像素即可应用“负片”效果。 我们从 255 中减去当前像素值,这给了我们想要的效果。 当鼠标移动停止并且检测到按下按钮事件时,我们将停止更新矩形的右下位置。 我们只是一直显示该图像,直到检测到另一个鼠标单击事件为止。

卡通化图像

现在我们知道了如何处理网络摄像头和键盘/鼠标输入,让我们继续前进,看看如何将图片转换为卡通图像。 我们可以将图像转换为草图或彩色卡通图像。

以下是草图的示例:

Cartoonizing an image

如果将卡通化效果应用于彩色图像,它将看起来像下一个图像:

Cartoonizing an image

让我们看看如何实现这一目标:

import cv2
import numpy as np

def cartoonize_image(img, ds_factor=4, sketch_mode=False):
    # Convert image to grayscale
    img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

    # Apply median filter to the grayscale image
    img_gray = cv2.medianBlur(img_gray, 7)

    # Detect edges in the image and threshold it
    edges = cv2.Laplacian(img_gray, cv2.CV_8U, ksize=5)
    ret, mask = cv2.threshold(edges, 100, 255, cv2.THRESH_BINARY_INV)

    # 'mask' is the sketch of the image
    if sketch_mode:
        return cv2.cvtColor(mask, cv2.COLOR_GRAY2BGR)

    # Resize the image to a smaller size for faster computation
    img_small = cv2.resize(img, None, fx=1.0/ds_factor, fy=1.0/ds_factor, interpolation=cv2.INTER_AREA)
    num_repetitions = 10
    sigma_color = 5
    sigma_space = 7
    size = 5

    # Apply bilateral filter the image multiple times
    for i in range(num_repetitions):
        img_small = cv2.bilateralFilter(img_small, size, sigma_color, sigma_space)

    img_output = cv2.resize(img_small, None, fx=ds_factor, fy=ds_factor, interpolation=cv2.INTER_LINEAR)

    dst = np.zeros(img_gray.shape)

    # Add the thick boundary lines to the image using 'AND' operator
    dst = cv2.bitwise_and(img_output, img_output, mask=mask)
    return dst

if __name__=='__main__':
    cap = cv2.VideoCapture(0)

    cur_char = -1
    prev_char = -1

    while True:
        ret, frame = cap.read()
        frame = cv2.resize(frame, None, fx=0.5, fy=0.5, interpolation=cv2.INTER_AREA)

        c = cv2.waitKey(1)
        if c == 27:
            break

        if c > -1 and c != prev_char:
            cur_char = c
        prev_char = c

        if cur_char == ord('s'):
            cv2.imshow('Cartoonize', cartoonize_image(frame, sketch_mode=True))
        elif cur_char == ord('c'):
            cv2.imshow('Cartoonize', cartoonize_image(frame, sketch_mode=False))
        else:
            cv2.imshow('Cartoonize', frame)

    cap.release()
    cv2.destroyAllWindows()

解构代码

当您运行前面的程序时,您将看到一个窗口,其中包含来自网络摄像头的视频流。 如果按S,视频流将变为草图模式,并且您将看到其铅笔状的轮廓。 如果按C,您将看到输入流的彩色卡通版。 如果按任何其他键,它将返回到正常模式。

让我们看一下函数cartoonize_image,看看我们是如何做到的。 我们首先将图像转换为灰度图像,然后通过中值过滤器对其进行处理。 中值过滤器非常擅长消除盐和胡椒粉噪音。 当您在图像中看到孤立的黑色或白色像素时,就是这种噪声。 它在网络摄像头和移动相机中很常见,因此我们需要将其过滤掉,然后再继续进行。 举个例子,看下面的图片:

Deconstructing the code

正如我们在输入图像中看到的,有很多孤立的绿色像素。 它们降低了图像的质量,我们需要摆脱它们。 这是中值过滤器派上用场的地方。 我们只查看每个像素周围的NxN邻域,然后选择这些数字的中值。 由于在这种情况下孤立的像素具有较高的值,因此取中值将摆脱这些值,并使图像平滑。 正如您在输出图像中看到的那样,中值过滤器消除了所有这些孤立的像素,并且图像看起来很干净。 以下是执行此操作的代码:

import cv2
import numpy as np

img = cv2.imread('input.png')
output = cv2.medianBlur(img, 7)
cv2.imshow('Input', img)
cv2.imshow('Median filter', output)
cv2.waitKey()

该代码非常简单。 我们仅使用函数medianBlur将中值过滤器应用于输入图像。 此函数中的第二个参数指定我们正在使用的核的大小。 核的大小与我们需要考虑的邻域大小有关。 您可以试用此参数,并查看它如何影响输出。

回到cartoonize_image,我们继续检测灰度图像上的边缘。 我们需要知道边缘在哪里,以便创建铅笔线效果。 一旦检测到边缘,就对它们进行阈值处理,以使事物在字面上和隐喻上都变成黑色和白色!

在下一步中,我们检查草图模式是否已启用。 如果是这样,那么我们只需将其转换为彩色图像并返回即可。 如果我们希望线变粗怎么办? 假设我们想看到类似下图的内容:

Deconstructing the code

如您所见,行比以前更粗。 为此,将if代码块替换为以下代码:

if sketch_mode:
    img_sketch = cv2.cvtColor(mask, cv2.COLOR_GRAY2BGR)
    kernel = np.ones((3,3), np.uint8)
    img_eroded = cv2.erode(img_sketch, kernel, iterations=1)
    return cv2.medianBlur(img_eroded, 5)

我们在这里使用带有3x3核的erode函数。 之所以要使用此选项,是因为它使我们有机会使用线图的粗细。 现在您可能会问,如果我们要增加某些物体的厚度,我们是否应该使用膨胀? 嗯,推理是正确的,但是这里有一个小小的转折。 请注意,前景为黑色,背景为白色。 侵蚀和膨胀将白色像素视为前景,将黑色像素视为背景。 因此,如果要增加黑色前景的厚度,则需要使用腐蚀。 施加腐蚀后,我们仅使用中值过滤器清除噪声并获得最终输出。

在下一步中,我们将使用双边过滤来平滑图像。 双边滤波是一个有趣的概念,它的表现比高斯过滤器好得多。 关于双边过滤的好处是,它保留了边缘,而高斯过滤器则使所有内容均等地平滑。 为了进行比较和对比,让我们看下面的输入图像:

Deconstructing the code

让我们将高斯过滤器应用于上一张图片:

Deconstructing the code

现在,让我们将双边过滤器应用于输入图像:

Deconstructing the code

如您所见,如果使用双边过滤器,质量会更好。 图像看起来很平滑,边缘看起来也很清晰! 接下来给出实现此目的的代码:

import cv2
import numpy as np

img = cv2.imread('input.jpg')

img_gaussian = cv2.GaussianBlur(img, (13,13), 0)
img_bilateral = cv2.bilateralFilter(img, 13, 70, 50)

cv2.imshow('Input', img)
cv2.imshow('Gaussian filter', img_gaussian)
cv2.imshow('Bilateral filter', img_bilateral)
cv2.waitKey()

如果仔细观察两个输出,可以看到高斯滤波图像中的边缘看起来模糊。 通常,我们只想平滑图像中的粗糙区域并保持边缘完整。 这是双边过滤器派上用场的地方。 高斯过滤器仅查看紧邻区域,并使用高斯核对像素值求平均。 双边过滤器通过仅对强度彼此相似的那些像素求平均,将这一概念提升到了一个新的水平。 它还使用颜色邻域度量标准来查看它是否可以替换强度相似的当前像素。 如果您看函数调用:

img_small = cv2.bilateralFilter(img_small, size, sigma_color, sigma_space)

这里的最后两个参数指定颜色和空间邻域。 这就是在双边过滤器的输出中边缘看起来清晰的原因。 我们在图像上多次运行此过滤器以使其平滑化,使其看起来像卡通漫画。 然后,我们将铅笔状的遮罩叠加在此彩色图像的顶部,以创建类似卡通的效果。

总结

在本章中,我们学习了如何访问网络摄像头。 我们讨论了在实时视频流中如何获取键盘和鼠标输入。 我们使用此知识来创建交互式应用。 我们讨论了中值和双边过滤器,并讨论了双边过滤器相对于高斯过滤器的优势。 我们使用所有这些原理将输入图像转换为素描图像,然后将其卡通化。

在下一章中,我们将学习如何在静态图像以及实时视频中检测不同的身体部位。


我们一直在努力

apachecn/AiLearning

【布客】中文翻译组