跳转至

一、过滤器的乐趣

本章的目的是开发许多图像处理过滤器,并将其实时应用于网络摄像头的视频流。 这些过滤器将依靠各种 OpenCV 函数来通过拆分,合并,算术运算以及为复杂函数应用查找表来操纵矩阵。

这三种效果如下:

  • 黑白铅笔素描:要使产生此效果,我们将使用两种图像融合技术,即,淡化加深
  • 加热/冷却过滤器:为创建这些效果,我们将使用查找表实现我们自己的曲线过滤器
  • 卡通化器:要创建这种效果,我们将结合使用双边过滤器中值过滤器自适应阈值

OpenCV 是这样的高级工具链,通常的问题不是如何从头开始实现某些东西,而是要选择哪种预定义的实现来满足您的需求。 如果您有很多可用的计算资源,则生成复杂的效果并不难。 挑战通常在于寻找一种不仅可以完成工作而且还要及时完成的​​方法。

与其通过理论课程教授图像处理的基本概念,我们将采用一种实用的方法,并开发一个集成了多种图像过滤技术的端到端应用。 我们将运用我们的理论知识来寻求一个不仅可以工作而且可以加快看似复杂的效果的解决方案,以便笔记本电脑可以实时生产它们。

以下屏幕截图显示了在笔记本电脑上运行的三种效果的最终结果:

Fun with Filters

注意

本书中的所有代码均针对 OpenCV 2.4.9,并已在 Ubuntu 14.04 上进行了测试。 在本书中,我们将广泛使用 NumPy 包。 此外,本章还需要 SciPy 包的UnivariateSpline模块以及 wxPython 2.8 图形化用户界面,用于跨平台的 GUI 应用。 我们将尽可能地避免进一步的依赖。

规划应用

最终的应用将包含以下模块和脚本:

  • filters:模块包含针对三种不同图像效果的不同类别。 模块化方法将使我们能够独立使用任何图形用户界面GUI)的过滤器。
  • filters.PencilSketch:用于将铅笔素描效果应用于 RGB 彩色图像的类。
  • filters.WarmingFilter:类别,用于将预热过滤器应用于 RGB 彩色图像。
  • filters.CoolingFilter:类别,用于将冷却过滤器应用于 RGB 彩色图像。
  • filters.Cartoonizer:一种用于将卡通化效果应用于 RGB 彩色图像的方法。
  • gui:提供 wxPython GUI 应用以访问网络摄像头并显示摄像头提要的模块,我们将在本书中广泛使用该模块。
  • gui.BaseLayout:可以从中构建更复杂布局的通用布局。
  • chapter1:本章的主要脚本。
  • chapter1.FilterLayout:基于gui.BaseLayout的自定义布局,用于显示摄像机源和一行单选按钮,允许用户从可用的图像过滤器中进行选择,以将其应用于摄像机源的每一帧。
  • chapter1.main:用于启动 GUI 应用和访问网络摄像头的main函数例程。

创建黑白铅笔素描

为了获得相机帧的铅笔素描(即黑白图),我们将使用两种图像融合技术,分别是,淡化加深。 这些术语是指在传统摄影的打印过程中使用的技术; 摄影师可以控制暗室打印物中某个区域的曝光时间,以使其变暗或变暗。 淡化使图像变亮,而加深使图像变暗。

不应该进行更改的区域由遮罩保护。 如今,现代的图像编辑程序,例如 Photoshop 和 Gimp,提供了在数字图像中模拟这些效果的方法。 例如,掩模仍然被用来模仿改变图像曝光时间的效果,其中具有相对强的值的掩模区域将更多地暴露图像,从而使图像变亮。 OpenCV 没有提供实现这些技术的本机功能,但是有了一点见识和一些技巧,我们将得出我们自己的有效实现,可以用来产生漂亮的铅笔素描效果。

如果在互联网上进行搜索,则可能会发现以下常见过程无法从 RGB 彩色图像中获得铅笔素描:

  1. 将彩色图像转换为灰度。
  2. 反转灰度图像得到负片。
  3. 将高斯模糊应用于步骤 2 中的负片。
  4. 使用彩色减淡功能将步骤 1 的灰度图像与步骤 3 的模糊负片混合。

第 1 步到第 3 步很简单,而第 4 步可能有些棘手。 让我们先解决这个问题。

注意

OpenCV 3 附带了铅笔素描效果。 cv2.pencilSketch函数使用由 Eduardo Gastal 和 Manuel Oliveira 在 2011 年论文《域变换中引入的域过滤器进行边缘感知的图像和视频处理》。 但是,出于本书的目的,我们将开发自己的过滤器。

在 OpenCV 中实现淡化和加深

在现代图像编辑工具(例如 Photoshop)中,对带有掩膜B的图像A的颜色减淡实现为以下三元语句,作用于称为idx的每个像素索引:

((B[idx] == 255) ? B[idx] : min(255, ((A[idx] << 8) / (255-B[idx]))))

本质上,将A[idx]图像像素的值除以B[idx]遮罩像素值的倒数,同时确保所得的像素值在[0, 255]的范围内,并且不被零除。

我们可以将其转换为以下朴素的 Python 函数,该函数接受两个 OpenCV 矩阵(imagemask)并返回混合图像:

def dodgeNaive(image, mask):
    # determine the shape of the input image
    width,height = image.shape[:2]

    # prepare output argument with same size as image
    blend = np.zeros((width,height), np.uint8)

    for col in xrange(width):
        for row in xrange(height):

            # shift image pixel value by 8 bits
            # divide by the inverse of the mask
            tmp = (image[c,r] << 8) / (255.-mask)

            # make sure resulting value stays within bounds
            if tmp > 255:
                tmp = 255
            blend[c,r] = tmp
    return blend

您可能已经猜到了,尽管这段代码在功能上可能是正确的,但无疑会非常慢。 首先,该函数使用for循环,这在 Python 中几乎总是一个坏主意。 其次,NumPy 数组(Python 中 OpenCV 图像的基本格式)已针对数组计算进行了优化,因此分别访问和修改每个image[c,r]像素将非常慢。

相反,我们应该认识到<<8操作与将像素值乘以2 ^ 8 = 256相同,并且可以通过cv2.divide函数实现按像素划分。 因此,我们的淡化函数的改进版本可能如下所示:

import cv2

def dodgeV2(image, mask):
    return cv2.divide(image, 255-mask, scale=256)

我们将淡化函数减少到了一行! dodgeV2函数产生的结果与dodgeNaive相同,但是速度要快几个数量级。 此外,cv2.divide自动处理零除,使255-mask为零的结果0

现在,的实现很简单,可以实现类似的加深函数,该函数将反转的图像除以反转的遮罩并反转结果:

import cv2

def burnV2(image, mask):
    return 255 – cv2.divide(255-image, 255-mask, scale=256)

铅笔素描变换

有了这些技巧,我们现在就可以看看整个过程了。 最终代码将在filters模块中位于其自己的类中。 将彩色图像转换为灰度后,我们的目标是将该图像与其模糊的负片混合:

  1. 我们导入 OpenCV 和numpy模块:

    py import cv2 import numpy as np

  2. 实例化PencilSketch类:

    py class PencilSketch: def __init__(self, (width, height), bg_gray='pencilsketch_bg.jpg'):

    此类的构造器将接受图像尺寸以及可选的背景图像,我们将在稍后使用它。 如果文件存在,我们将打开它并缩放到合适的大小:

    ```py self.width = width self.height = height

    try to open background canvas (if it exists)

    self.canvas = cv2.imread(bg_gray, cv2.CV_8UC1) if self.canvas is not None: self.canvas = cv2.resize(self.canvas, (self.width, self.height)) ```

  3. 添加将执行铅笔素描的渲染方法:

    py def renderV2(self, img_rgb):

  4. 将 RGB 图像(imgRGB)转换为灰度很简单:

    py img_gray = cv2.cvtColor(img_rgb, cv2.COLOR_RGB2GRAY)

    请注意,输入图像是 RGB 还是 BGR 都没有关系。

  5. 然后,我们反转图像并使用大小为(21,21)的高斯高斯核模糊它:

    py img_gray_inv = 255 – img_gray img_blur = cv2.GaussianBlur(img_gray_inv, (21,21), 0, 0)

  6. 我们使用上述代码中的dodgeV2淡化函数将原始灰度图像与模糊逆混合:

    py img_blend = dodgeV2(mg_gray, img_blur) return cv2.cvtColor(img_blend, cv2.COLOR_GRAY2RGB)

生成的图像如下所示:

Pencil sketch transformation

您是否注意到我们的代码可以进一步优化?

高斯模糊本质上是具有高斯函数的卷积。 卷积的优点之一是它们的关联属性。 这意味着我们先反转图像然后对其进行模糊处理,还是先模糊图像然后对其进行反转都没有关系。

“那有什么关系呢?” 你可能会问。 好吧,如果我们从模糊的图像开始并将其逆数传递给dodgeV2函数,则在该函数内,图像将再次反转(255-mask部分),实质上产生了原始图像。 如果我们摆脱了这些多余的操作,经过优化的render方法将如下所示:

def render(img_rgb):
    img_gray = cv2.cvtColor(img_rgb, cv2.COLOR_BGR2GRAY)
    img_blur = cv2.GaussianBlur(img_gray, (21,21), 0, 0)
    img_blend = cv2.divide(img_gray, img_blur, scale=256)
    return img_blend

对于踢脚和傻笑,我们想将转换后的图像(img_blend)与背景图像(self.canvas)轻轻混合,使其看起来就像在画布上绘制了图像一样:

if self.canvas is not None:
    img_blend = cv2.multiply(img_blend, self.canvas, scale=1./256)
return cv2.cvtColor(img_blend, cv2.COLOR_GRAY2BGR)

我们完成了! 最终输出如下所示:

Pencil sketch transformation

生成加热/冷却过滤器

当我们感知图像时,我们的大脑会从许多微妙的线索中推断出有关场景的重要细节。 例如,在明亮的日光下,高光可能会因为在阳光直射下而具有淡黄色的色彩,而阴影可能由于蓝天的环境光而显得略带蓝色。 当我们查看具有这种颜色属性的图像时,我们可能会立即想到晴天

对于有时会故意操纵图像的白平衡以传达某种情绪的摄影师而言,这种效果并不神秘。 暖色通常被认为更令人愉悦,而冷色则与夜晚和昏暗有关。

为了操纵图像的感知到的色温,我们将实现和曲线过滤器。 这些过滤器控制颜色过渡如何在图像的不同区域之间出现,从而使我们可以巧妙地改变色谱图,而不会给图像增加看起来不自然的整体色调。

通过曲线移动的色彩操作

曲线过滤器本质上是一个函数,y = f(x),其将输入像素值x映射到输出像素值y。 通过一组n + 1锚点对曲线进行参数化,如下所示: {(x[0], y[0]), ..., (x[n], y[n])}

每个锚点都是一对数字,分别代表输入和输出像素值。 例如,对(30, 90)表示将输入像素值 30 增加到输出值 90。沿着平滑曲线对锚点之间的值进行插值(因此,名称曲线过滤器) 。

这种过滤器可以应用于任何图像通道,无论是单个灰度通道还是 RGB 彩色图像的 R,G 和 B 通道。 因此,出于我们的目的,xy的所有值必须保持在 0 到 255 之间。

例如,如果我们想使灰度图像稍微亮一些,可以使用带有以下控制点集的曲线过滤器: {(0, 0), (128, 192), (255, 255)}。 这意味着除 0 和 255 以外的所有输入像素值都会略微增加,从而导致图像的整体增亮效果。

如果我们希望这样的过滤器产生看起来自然的图像,则必须遵守以下两个规则:

  • 每组锚点都应包括(0, 0)(255, 255)。 这对于防止图像看起来好像具有整体色调非常重要,因为黑色保持黑色,白色保持白色。
  • 函数f(x)应该单调增加。 换句话说,随着x的增加, f(x)保持不变或增加(即,从不减少)。 这对于确保阴影仍然是阴影而高光仍然是高光非常重要。

使用查找表实现曲线过滤器

曲线过滤器在计算上是昂贵,因为每当x与预定锚点之一不一致时,都必须插值f(x)的值 。 对我们遇到的每个图像帧的每个像素执行此计算将对性能产生巨大影响。

相反,我们使用查找表。 由于出于我们的目的,只有 256 个可能的像素值,因此我们只需要为x的所有 256 个可能值计算f(x)。 插值由scipy.interpolate模块的UnivariateSpline函数处理,如以下代码片段所示:

from scipy.interpolate import UnivariateSpline

def _create_LUT_8UC1(self, x, y):
  spl = UnivariateSpline(x, y)
  return spl(xrange(256))

函数的return自变量是一个 256 个元素的列表,其中包含x的每个可能值的内插的f(x)值。

现在,我们需要做的所有事情都带有一组锚点(x[i], y[i]),我们准备将过滤器应用于灰度输入图像(img_gray):

import cv2
import numpy as np

x = [0, 128, 255]
y = [0, 192, 255]
myLUT = _create_LUT_8UC1(x, y)
img_curved = cv2.LUT(img_gray, myLUT).astype(np.uint8)

结果看起来像这样(原始图像在左侧,转换后的图像在右侧):

Implementing a curve filter by using lookup tables

设计加热/冷却效果

通过机制,可以将通用曲线过滤器快速应用于适当的任何图像通道,现在我们来探讨如何操纵图像的感知色温的问题。 同样,最终代码将在filters模块中具有其自己的类。

如果您有空余时间,建议您暂时尝试一下不同的曲线设置。 您可以选择任意数量的锚点,并将曲线过滤器应用于您可以想到的任何图像通道(红色,绿色,蓝色,色相,饱和度,亮度,亮度等)。 您甚至可以合并多个通道,或者减少一个通道,然后将另一个通道转移到所需区域。 结果会是什么样?

但是,如果各种可能性使您眼花缭乱,请采取更为保守的方法。 首先,通过使用前面步骤中开发的_create_LUT_8UC1函数,我们定义两个通用曲线过滤器,一个(按趋势)增加通道的所有像素值,而一个通常降低它们:

class WarmingFilter:

  def __init__(self):
    self.incr_ch_lut = _create_LUT_8UC1([0, 64, 128, 192, 256],
      [0, 70, 140, 210, 256])
    self.decr_ch_lut = _create_LUT_8UC1([0, 64, 128, 192, 256],
      [0, 30,  80, 120, 192])

使图像看起来像是在炎热的晴天(可能接近日落)拍摄时,最简单的方法是增加图像中的红色并通过增加色彩饱和度使颜色显得鲜艳。 我们将分两步实现:

  1. 使用incr_ch_lutdecr_ch_lut分别增加 RGB 彩色图像的 R 通道中的像素值和 B 通道中的像素值:

    py def render(self, img_rgb): c_r, c_g, c_b = cv2.split(img_rgb) c_r = cv2.LUT(c_r, self.incr_ch_lut).astype(np.uint8) c_b = cv2.LUT(c_b, self.decr_ch_lut).astype(np.uint8) img_rgb = cv2.merge((c_r, c_g, c_b))

  2. 将图像转换为和 HSV 色彩空间(H 表示色相,S 表示饱和度,V 表示值), 并使用incr_ch_lut增加 S 通道。 这可以通过以下功能来实现,需要 RGB 彩色图像作为输入:

    ```py c_b = cv2.LUT(c_b, decrChLUT).astype(np.uint8)

    increase color saturation

    c_h, c_s, c_v = cv2.split(cv2.cvtColor(img_rgb, cv2.COLOR_RGB2HSV)) c_s = cv2.LUT(c_s, self.incr_ch_lut).astype(np.uint8) return cv2.cvtColor(cv2.merge((c_h, c_s, c_v)), cv2.COLOR_HSV2RGB) ```

结果如下所示:

Designing the warming/cooling effect

类似地,我们可以定义一个冷却过滤器,该过滤器可以增加 RGB 图像的 B 通道中的像素值,减小 R 通道中的像素值,将图像转换为 HSV 色彩空间,并通过 S 降低色彩饱和度通道:

class CoolingFilter:

    def render(self, img_rgb):

        c_r, c_g, c_b = cv2.split(img_rgb)
        c_r = cv2.LUT(c_r, self.decr_ch_lut).astype(np.uint8)
        c_b = cv2.LUT(c_b, self.incr_ch_lut).astype(np.uint8)
        img_rgb = cv2.merge((c_r, c_g, c_b))

        # decrease color saturation
        c_h, c_s, c_v = cv2.split(cv2.cvtColor(img_rgb, cv2.COLOR_RGB2HSV))
        c_s = cv2.LUT(c_s, self.decr_ch_lut).astype(np.uint8)
        return cv2.cvtColor(cv2.merge((c_h, c_s, c_v)), cv2.COLOR_HSV2RGB)

现在,结果看起来像这样:

Designing the warming/cooling effect

将图像卡通化

在过去的几年中,专业的卡通化软件迅速出现。 为了实现基本的卡通效果,我们需要的是双边过滤器和一些边缘检测。 双边过滤器将减少调色板或图像中使用的颜色数。 这模仿了卡通图画,其中漫画家通常只有很少的颜色可以使用。 然后,我们可以对生成的图像进行边缘检测,以生成粗体轮廓。 然而,真正的挑战在于双边过滤器的计算成本。 因此,我们将使用一些技巧来实时产生可接受的卡通效果。

我们将遵循以下过程将 RGB 彩色图像转换为卡通图像:

  1. 应用双边过滤器来减少图像的调色板。
  2. 将原始彩色图像转换为灰度。
  3. 应用中值模糊来减少图像噪声。
  4. 使用自适应阈值检测并强调边缘遮罩中的边缘。
  5. 将步骤 1 中的彩色图像与步骤 4 中的边缘遮罩合并。

使用双边过滤器进行边缘感知的平滑

强大的双边过滤器非常适合将 RGB 图像转换为彩色绘画或卡通,因为它可以平滑平坦区域,同时保持边缘清晰。 过滤器的唯一缺点似乎是其计算成本,因为它比其他平滑操作(例如高斯模糊)要慢几个数量级。

当我们需要减少计算成本时,要采取的第一个措施是对低分辨率的图像执行操作。 为了将 RGB 图像(imgRGB)缩小到其尺寸的四分之一(将宽度和高度减小到一半),我们可以使用cv2.resize

import cv2

img_small = cv2.resize(img_rgb, (0,0), fx=0.5, fy=0.5)

调整大小后的图像中的像素值将对应于原始图像中小邻域的像素平均值。 但是,此过程可能会产生图像伪影,这也称为混叠。 尽管这本身就够糟糕的,但可以通过后续处理(例如边缘检测)来增强效果。

更好的替代方法可能是使用高斯金字塔进行缩放(缩小至原始大小的四分之一)。 高斯金字塔由模糊操作组成,该操作在对图像重新采样之前执行,从而减少了混叠效果:

img_small = cv2.pyrDown(img_rgb)

但是,即使在这种规模下,双边过滤器仍然可能太慢而无法实时运行。 另一个技巧是重复(例如五次)对图像应用一个小的双边过滤器,而不是一次应用一个大的双边过滤器:

num_iter = 5
for _ in xrange(num_iter):
    img_small = cv2.bilateralFilter(img_small, d=9, sigmaColor=9, sigmaSpace=7)

cv2.bilateralFilter中的三个参数控制像素邻域(d)的直径以及过滤器在色彩空间(sigmaColor)和坐标空间(sigmaSpace)中的标准差。

不要忘记将图像恢复为原始大小:

img_rgb = cv2.pyrUp(img_small)

结果看起来像是一个令人毛骨悚然的程序员的彩色绘画,如下所示:

Using a bilateral filter for edge-aware smoothing

检测并突出边缘

同样,当用于边缘检测时,挑战通常不在于底层算法如何工作,而在于为手头任务选择哪种特定算法。 您可能已经熟悉各种边缘检测器。 例如,Canny 边缘检测cv2.Canny)提供了一种相对简单有效的方法来检测图像中的边缘,但是它容易受到噪声的影响。

Sobel 运算符(cv2.Sobel)可以减少此类伪像,但它不是旋转对称的。 Scharr 运算符(cv2.Scharr)的目标是对此进行校正,但仅查看第一个图像导数。 如果您感兴趣,还有更多运算符,例如拉普拉斯算子山脊运算符(包括二阶导数),但它们却更加复杂。 最后,出于我们的特定目的,它们可能看起来并不好,也许是因为它们像其他任何算法一样容易受到光照条件的影响。

出于本项目的目的,我们将选择甚至与常规边缘检测都不相关的函数-cv2.adaptiveThreshold。 像cv2.threshold一样,此函数使用阈值像素值将灰度图像转换为二进制图像。 也就是说,如果原始图像中的像素值大于阈值,则最终图像中的像素值将为 255。否则,它将为 0。但是,自适应阈值的优点在于它不会查看图像的整体属性。 取而代之的是,它独立于每个小邻域中检测最显着的特征,而不考虑全局图像的最优性。 这使算法在光照条件下具有极强的鲁棒性,这正是我们试图在卡通物体和人物周围绘制粗体黑色轮廓时所需要的。

但是,这也使算法易受噪声影响。 为了解决这个问题,我们将使用中值过滤器对图像进行预处理。 中值过滤器按照其名称的含义执行; 它将每个像素值替换为小像素邻域中所有像素的中值。 我们首先将 RGB 图像(img_rgb)转换为灰度(img_gray),然后应用具有七个像素局部邻域的中值模糊:

# convert to grayscale and apply median blur
img_gray = cv2.cvtColor(img_rgb, cv2.COLOR_RGB2GRAY)
img_blur = cv2.medianBlur(img_gray, 7)

降低噪声后,现在可以使用自适应阈值检测和增强边缘。 即使剩下一些图像噪声,带blockSize=9cv2.ADAPTIVE_THRESH_MEAN_C算法也可以确保将阈值应用于9 x 9邻域平均值C=2的平均值:

img_edge = cv2.adaptiveThreshold(img_blur, 255, cv2.ADAPTIVE_THRESH_MEAN_C, cv2.THRESH_BINARY, 9, 2)

注意

下载示例代码

您可以从这个页面上的帐户下载示例代码文件,以获取已购买的所有 Packt Publishing 图书。 如果您在其他地方购买了此书,则可以访问这个页面并注册以将文件直接通过电子邮件发送给您。

自适应阈值的结果如下所示:

Detecting and emphasizing prominent edges

组合颜色和轮廓来制作卡通

最后一步是将两者合并。 只需使用cv2.bitwise_and将两种效果融合在一起成为一张图像。 完整的函数如下:

def render(self, img_rgb):
    numDownSamples = 2 # number of downscaling steps
    numBilateralFilters = 7  # number of bilateral filtering steps

    # -- STEP 1 --
    # downsample image using Gaussian pyramid
    img_color = img_rgb
    for _ in xrange(numDownSamples):
        img_color = cv2.pyrDown(img_color)

    # repeatedly apply small bilateral filter instead of applying
    # one large filter
    for _ in xrange(numBilateralFilters):
        img_color = cv2.bilateralFilter(img_color, 9, 9, 7)

    # upsample image to original size
    for _ in xrange(numDownSamples):
        img_color = cv2.pyrUp(img_color)

    # -- STEPS 2 and 3 --
    # convert to grayscale and apply median blur
    img_gray = cv2.cvtColor(img_rgb, cv2.COLOR_RGB2GRAY)
    img_blur = cv2.medianBlur(img_gray, 7)

    # -- STEP 4 --
    # detect and enhance edges
    img_edge = cv2.adaptiveThreshold(img_blur, 255,
        cv2.ADAPTIVE_THRESH_MEAN_C, cv2.THRESH_BINARY, 9, 2)

    # -- STEP 5 --
    # convert back to color so that it can be bit-ANDed
    # with color image
    img_edge = cv2.cvtColor(img_edge, cv2.COLOR_GRAY2RGB)
    return cv2.bitwise_and(img_color, img_edge)

结果如下所示:

Combining colors and outlines to produce a cartoon

全部放在一起

在以交互方式使用设计的图像过滤器效果之前,我们需要设置主脚本并设计 GUI 应用。

运行应用

要运行应用,我们将转到chapter1.py.脚本,我们将从导入所有必需的模块开始:

import numpy as np

import wx
import cv2

我们还必须导入通用的 GUI 布局(从gui)和所有设计的图像效果(从filters):

from gui import BaseLayout
from filters import PencilSketch, WarmingFilter, CoolingFilter, Cartoonizer

OpenCV 提供了一种直接的方法来访问计算机的网络摄像头或摄像头设备。 以下代码段使用cv2.VideoCapture打开计算机的默认摄像机 ID(0):

def main():
    capture = cv2.VideoCapture(0)

在某些平台上,对cv2.VideoCapture的首次调用无法打开通道。 在这种情况下,我们可以通过自己打开渠道来提供解决方法:

if not(capture.isOpened()):
    capture.open() 

为了给我们的应用一个实时运行的机会,我们将视频流的大小限制为640 x 480像素:

capture.set(cv2.cv.CV_CAP_PROP_FRAME_WIDTH, 640)
capture.set(cv2.cv.CV_CAP_PROP_FRAME_HEIGHT, 480)

注意

如果使用的是 OpenCV 3,则所要查找的常量可能称为cv3.CAP_PROP_FRAME_WIDTHcv3.CAP_PROP_FRAME_HEIGHT

然后,可以将capture流传递到我们的 GUI 应用,该应用是FilterLayout类的实例:

    # start graphical user interface
    app = wx.App()
    layout = FilterLayout(None, -1, 'Fun with Filters', capture)
    layout.Show(True)
    app.MainLoop()

现在唯一要做的就是设计上述 GUI。

GUI 基类

FilterLayout GUI 将基于称为BaseLayout的通用平面布局类,我们也将在以后的章节中使用它。

BaseLayout类被设计为抽象基类。 您可以将此类视为可以应用于我们尚未设计的所有布局的蓝图或秘籍-如果您愿意,可以将其作为骨架类,用作将来所有 GUI 代码的基础。 为了使用抽象类,我们需要以下import语句:

from abc import ABCMeta, abstractmethod

我们还包括其他一些有用的模块,特别是wx Python 模块和 OpenCV(当然):

import time

import wx
import cv2

该类被设计为从蓝图或框架派生,即wx.Frame类。 我们还通过添加__metaclass__属性将类标记为抽象类:

class BaseLayout(wx.Frame):
    __metaclass__ = ABCMeta

稍后,当我们编写自己的自定义布局(FilterLayout)时,将使用相同的符号来指定该类基于BaseLayout蓝图(或骨架)类,例如,在class FilterLayout(BaseLayout):中。 现在,让我们集中讨论BaseLayout类。

抽象类至少具有一个抽象方法。 抽象方法类似于指定某种方法必须存在,但是我们不确定当时的外观。 例如,假设BaseLayout包含如下指定的方法:

@abstractmethod
def _init_custom_layout(self):pass

然后,派生自该类的任何类,例如FilterLayout,都必须指定具有该确切签名的方法的完全充实的实现。 稍后您将看到,这将使我们能够创建自定义布局。

但是首先,让我们进入 GUI 构造器。

GUI 构造器

BaseLayout构造器接受一个 I​​D(-1),一个标题字符串('Fun with Filters'),一个视频捕获对象以及一个可选参数,该参数指定每秒的帧数。 然后,在构造器中要做的第一件事是尝试从捕获的对象中读取一个帧以确定图像大小:

def __init__(self, parent, id, title, capture, fps=10):
    self.capture = capture
    # determine window size and init wx.Frame
    _, frame = self.capture.read()
    self.imgHeight,self.imgWidth = frame.shape[:2]

我们将使用图像大小准备一个缓冲区,该缓冲区将每个视频帧存储为位图,并设置 GUI 的大小。 因为我们想在当前视频帧下方显示一堆控制按钮,所以我们将 GUI 的高度设置为self.imgHeight+20

self.bmp = wx.BitmapFromBuffer(self.imgWidth, 
    self.imgHeight, frame)
wx.Frame.__init__(self, parent, id, title,size=(self.imgWidth, self.imgHeight+20))

然后,我们提供了两种方法来初始化更多参数并创建 GUI 的实际布局:

self._init_base_layout()
self._create_base_layout()

处理视频流

网络摄像头的视频流由_init_base_layout方法开始的一系列步骤处理。 这些步骤起初看起来可能过于复杂,但是它们是使视频即使在更高的帧速率下也能平稳运行的必要条件(也就是说,可以防止闪烁)。

wxPython模块可用于事件和回调方法。 当某个事件被触发时,它可以导致某个类方法被执行(换句话说,一种方法可以将绑定到事件)。 我们将利用这种机制来发挥优势,并经常通过以下步骤来显示新的框架:

  1. 我们创建了一个计时器,只要经过 1000./fps 毫秒,它就会生成wx.EVT_TIMER事件:

    py def _init_base_layout(self): self.timer = wx.Timer(self) self.timer.Start(1000./self.fps)

  2. 每当计时器启动时,我们都希望调用_on_next_frame方法。 它将尝试获取新的视频帧:

    py self.Bind(wx.EVT_TIMER, self._on_next_frame)

  3. _on_next_frame方法将处理新的视频帧并将处理后的帧存储在位图中。 这将触发另一个事件wx.EVT_PAINT。 我们想将此事件绑定到_on_paint方法,该方法将在显示器上绘制新的帧:

    py self.Bind(wx.EVT_PAINT, self._on_paint)

_on_next_frame方法抓取一个新帧,完成后,将该帧发送到另一种方法__process_frame进行进一步处理:

def _on_next_frame(self, event):
    ret, frame = self.capture.read()
    if ret:
        frame = self._process_frame(cv2.cvtColor(frame,cv2.COLOR_BGR2RGB))

然后将已处理的帧(frame)存储在位图缓冲区(self.bmp)中:

self.bmp.CopyFromBuffer(frame)

调用Refresh会触发上述wx.EVT_PAINT事件,该事件绑定到_on_paint

self.Refresh(eraseBackground=False)

然后paint方法从缓冲区中抓取帧并将其显示:

def _on_paint(self, event):
    deviceContext = wx.BufferedPaintDC(self.pnl)
    deviceContext.DrawBitmap(self.bmp, 0, 0)

基本的 GUI 布局

通用布局的创建通过称为_create_base_layout的方法完成。 最基本的布局仅由一个大的黑色面板组成,该面板提供了足够的空间来显示视频供稿:

def _create_base_layout(self):
    self.pnl = wx.Panel(self, -1,
                        size=(self.imgWidth, self.imgHeight))
    self.pnl.SetBackgroundColour(wx.BLACK)

为了使布局可扩展,我们将其添加到垂直排列的wx.BoxSizer对象中:

self.panels_vertical = wx.BoxSizer(wx.VERTICAL)
self.panels_vertical.Add(self.pnl, 1, flag=wx.EXPAND)

接下来,我们指定一个抽象方法_create_custom_layout,我们将不填写任何代码。 相反,我们基类的任何用户都可以对基本布局进行自己的自定义修改:

self._create_custom_layout()

然后,我们只需要设置结果布局的最小尺寸并将其居中即可:

self.SetMinSize((self.imgWidth, self.imgHeight))
self.SetSizer(self.panels_vertical)
self.Centre()

自定义过滤器布局

现在我们即将完成! 如果要使用BaseLayout类,则需要为以前留空的三种方法提供代码:

  • _init_custom_layout:这是我们可以初始化特定于任务的参数的
  • _create_custom_layout:这是,我们可以在其中对 GUI 布局进行特定于任务的修改
  • _process_frame:这是,我们在其中对摄像机供稿的每个捕获帧执行特定于任务的处理

在这一点上,初始化图像过滤器是不言自明的,因为它只需要我们实例化相应的类:

def _init_custom_layout(self):
    self.pencil_sketch = PencilSketch((self.imgWidth, 
        self.imgHeight))
    self.warm_filter = WarmingFilter()
    self.cool_filter = CoolingFilter()
    self.cartoonizer = Cartoonizer()

为了自定义布局,我们水平排列了多个单选按钮,每个图像效果模式一个按钮:

def _create_custom_layout(self):
    # create a horizontal layout with all filter modes
    pnl = wx.Panel(self, -1 )
    self.mode_warm = wx.RadioButton(pnl, -1, 'Warming Filter',
        (10, 10), style=wx.RB_GROUP)
    self.mode_cool = wx.RadioButton(pnl, -1, 'Cooling Filter', 
        (10, 10))
    self.mode_sketch = wx.RadioButton(pnl, -1, 'Pencil Sketch',
        (10, 10))
    self.mode_cartoon = wx.RadioButton(pnl, -1, 'Cartoon',
        (10, 10))
    hbox = wx.BoxSizer(wx.HORIZONTAL)
    hbox.Add(self.mode_warm, 1)
    hbox.Add(self.mode_cool, 1)
    hbox.Add(self.mode_sketch, 1)
    hbox.Add(self.mode_cartoon, 1)
    pnl.SetSizer(hbox)

在此,style=wx.RB_GROUP选项可确保一次只能选择这些单选按钮之一。

为了使这些更改生效,需要将pnl添加到现有面板列表中:

self.panels_vertical.Add(pnl, flag=wx.EXPAND | wx.BOTTOM | wx.TOP, border=1)

最后要指定的方法是_process_frame。 回想一下,只要接收到新的相机帧,就会触发此方法。 我们需要做的就是选择要应用的正确图像效果,这取决于单选按钮的配置。 我们只需检查当前选择了哪个按钮,然后调用相应的render方法:

def _process_frame(self, frame_rgb):
    if self.mode_warm.GetValue():
        frame = self.warm_filter.render(frame_rgb)
    elif self.mode_cool.GetValue():
        frame = self.cool_filter.render(frame_rgb)
    elif self.mode_sketch.GetValue():
        frame = self.pencil_sketch.render(frame_rgb)
    elif self.mode_cartoon.GetValue():
        frame = self.cartoonizer.render(frame_rgb)

不要忘记返回已处理的帧:

return frame

我们完成了!

这是结果:

A custom filter layout

总结

在本章中,我们探讨了许多有趣的图像处理效果。 我们使用淡化和刻录来创建黑白铅笔素描效果,探索了查找表以实现曲线过滤器的有效实现,并具有创造卡通效果的创造力。

在下一章中,我们将稍作调整,并探索使用深度传感器(例如 Microsoft Kinect 3D)来实时识别手势。



回到顶部