Java 图像处理秘籍(二)
三、成像技术
最完美的技巧是根本不会被注意到的。
帕布罗·卡萨尔斯
前一章介绍了 Origami 以及如何在简单的垫子和图像上进行单步处理操作。
虽然这已经很好地展示了该库的易用性,但第三章希望通过将简单的处理步骤结合起来,向更大的目标迈进一步。从执行内容分析、轮廓检测、形状发现和形状移动,一直到基于计算机的素描和景观艺术,只要你能想到的,这里都有许多冒险等待着你。
我们将从熟悉的地方开始,通过在字节级操作 OpenCV mats,更详细地掌握图像操作的细节。
学习将分为两大部分。首先将是一个稍微侧重于艺术的部分,在这里我们用线条、渐变和 OpenCV 函数从现有的图像中创建新的图像。你将会使用已知的 origami/opencv 函数,但是一些其他的函数也将会根据需要引入到创作流程中。
这是 Origami 术最初的计划之一,用来创作图画。碰巧的是,为了理解简单的概念是如何组合在一起的,我不得不玩图像合成和线框,它们实际上比我想象的要好。更重要的是,添加你自己的风格并在以后重复使用这些作品是很容易的。所以第一部分是为了分享这个经验。
然后,在第二部分,我们将转移到更侧重于图像处理的技术。在回顾了来自艺术部门的即时反馈后,处理步骤在那个阶段将更容易掌握。
OpenCV 中的处理步骤大部分时间都很简单,但是 C++中的原始样本使得阅读指针行变得相当困难。我个人发现,即使包括 Clojure 学习曲线,Origami 也是一种更容易开始使用 OpenCV 的方法:您可以专注于代码行的直接影响,并尝试以不同的方式编写每一步,而不必每次都通过获得即时反馈来重新开始,直到它最终很好地到位。希望这一章的第二部分会让你足够舒服,你会想去挑战这些例子。
请注意,线性阅读本章可能是一个好主意,这样您就不会错过新功能或新技巧。然而,当然,没有什么能阻止你跳进你喜欢的地方。它毕竟是一本食谱!
3.1 玩颜色
问题
在前一章中,你已经看到了改变垫子颜色的各种技术。
你想要控制如何指定和影响颜色,例如,通过在垫子上应用特定的因素或功能来增加或减少它们的强度。
解决办法
在这里,您将了解以下内容:如何组合操作,如使用已知的 cvt-color 转换图像颜色通道;如何使用其他 OpenCV 函数像 threshold 来限制通道值;如何创建遮罩并使用功能设置为;以及如何使用函数来组合不同版本的 mat。
您还将更详细地回顾如何使用**变换!**创建基本艺术效果的功能。
它是如何工作的
为了玩垫子,我们将使用另一套猫和花,但是你当然可以随时尝试在你自己的照片上应用这些功能。
本章的名称空间头,以及所有的名称空间依赖项,将使用上一章中所需的相同名称空间,即 opencv3.core 和 opencv3.utils 以及来自 origami 的 opencv3 原始名称空间的 opencv3.colors.rgb。
所需的部分类似于下面的代码片段。
(ns opencv3.chapter03 (:require [opencv3.core :refer :all] [opencv3.colors.rgb :as rgb] [opencv3.utils :as u]))
通常为每个实验创建一个新的笔记本,并分别保存它们是一个好主意。
在彩色垫上应用阈值
回到基础。您还记得如何在 mat 上设置阈值,并且只保留矩阵中大于 150 的值吗?
是的,你是正确的:使用阈值函数。
(-> (u/matrix-to-mat [[100 255 200] [100 255 200] [100 255 200]]) (threshold! 150 255 THRESH_BINARY) (dump))
输入矩阵包含各种值,有些低于阈值 150,有些高于阈值 150。应用阈值时,下面的值设置为 0,上面的值设置为阈值的第二个参数值 255。
这导致了以下矩阵(图 3-1 ):
[0 255 255] [0 255 255] [0 255 255]
这是一个单通道的垫子,但如果我们在三通道垫子上做同样的事情会怎么样呢?
(-> (u/matrix-to-mat [[0 0 170] [0 0 170] [100 100 0]]) (cvt-color! COLOR_GRAY2BGR) (threshold! 150 255 THRESH_BINARY) (dump))
将颜色转换为 BGR 会将单通道贴图的每个值复制为同一像素上的相同三个值。
之后立即应用 OpenCV 阈值函数,将阈值应用于每个通道上的所有值。因此得到的 mat 丢失了原始 mat 的 100 个值,只保留了 255 个值。
[0 0 0 0 0 0 255 255 255] [0 0 0 0 0 0 255 255 255] [0 0 0 0 0 0 0 0 0]
一个 3×3 的矩阵太小了,不能在屏幕上显示,所以让我们先在输入矩阵上使用 resize。
(-> (u/matrix-to-mat [[0 0 170] [0 0 170] [100 100 0]]) (cvt-color! COLOR_GRAY2BGR) (resize! (new-size 50 50) 1 1 INTER_AREA) (u/mat-view)) Figure 3-1
黑白垫子
对前面的遮罩应用类似的阈值会保持浅灰色,浅灰色的值高于阈值,但会通过将深灰色变为黑色来移除深灰色。
(-> (u/matrix-to-mat [[0 0 170] [0 0 170] [100 100 0]]) (cvt-color! COLOR_GRAY2BGR) (threshold! 150 255 THRESH_BINARY) (resize! (new-size 50 50) 0 0 INTER_AREA) (u/mat-view)) This gives us Figure 3-2. Figure 3-2
阈值!
请注意,resize、 **INTER_AREA、**使用了一个特定的插值参数,它很好地清晰地切割了形状,而不是插值和强制模糊。
Just for some extra info, the default resize method gives something like Figure 3-3, which can be used in other circumstances , but this is not what we want here. Figure 3-3
使用默认插值调整大小
无论如何,回到这个练习,你可能已经做到了:应用一个标准的阈值来推动鲜艳的颜色。
让我们看看它是如何在一个从图像加载的垫子上工作的,让我们加载该章节的第一个图像(图 3-4 )。
(def rose (imread "resources/chapter03/rose.jpg" IMREAD_REDUCED_COLOR_4)) Figure 3-4
有人说爱是一条河
我们首先应用从矩阵加载的 mat 上应用的相同阈值,但这次是在 rose 图像上。
(-> original (clone) (threshold! 100 255 THRESH_BINARY) (u/mat-view)) You get a striking result! (Figure 3-5) Figure 3-5
鲜艳的颜色
在一张拍得很好的照片中,这实际上给了你一种艺术感,你可以在此基础上制作卡片和圣诞礼物!
现在让我们在一个完全不同的图像上应用类似的技术。我们先把图片转成黑白,看看效果如何。
这次的图片是贪玩的小猫,如图 3-6 所示。
(-> "resources/chapter03/ai6.jpg" (imread IMREAD_REDUCED_COLOR_2) (u/mat-view)) Figure 3-6
顽皮的猫
如果你应用一个相似的阈值,但是在灰度版本上,会发生一些有趣的事情。
(-> "resources/chapter03/ai6.jpg" (imread IMREAD_REDUCED_GRAYSCALE_2) (threshold! 100 255 THRESH_BINARY) (u/mat-view)) The two cats are actually standing out and being highlighted (Figure 3-7). Figure 3-7
顽皮、突出的猫
爽;这意味着我们想要突出的形状已经被突出显示。
类似这样的东西可以用来找出形状和移动的物体;更多信息请见配方 3-6 和 3-7。
现在,为了保持事物的艺术性,让我们开发一个小函数,将低于给定阈值的所有颜色转换为一种颜色,将高于阈值的所有值转换为另一种颜色。
We can achieve this by
-
首先,转向不同的颜色空间,即 HSV
-
从应用了 THRESH_BINARY 设置的阈值创建遮罩
-
从应用了 THRESH_BINARY_INV 设置的阈值创建第二个掩码,从而创建与第一个掩码具有相反值的掩码
-
将两个遮罩转换为灰色,因此它们仅由一个通道组成
-
使用 set-to 设置工作垫的颜色,遵循第一个遮罩
-
再次使用设置来设置工作垫的颜色,但是遵循第二个遮罩
-
就是这样!
在编码快乐时,我们将创建一个**低-高!**执行上述算法的函数。
**低-高!**功能由 cvt-color 组成!、threshold 和 set-to,所有的函数你都已经见过了。
(defn low-high! ([image t1 color1 color2 ] (let [_copy (-> image clone (cvt-color! COLOR_BGR2HSV)) _work (clone image) _thresh-1 (new-mat) _thresh-2 (new-mat)] (threshold _copy _thresh-1 t1 255 THRESH_BINARY) (cvt-color! _thresh-1 COLOR_BGR2GRAY) (set-to _work color1 _thresh-1) (threshold _copy _thresh-2 t1 255 THRESH_BINARY_INV) (cvt-color! _thresh-2 COLOR_BGR2GRAY) (set-to _work color2 _thresh-2) _work)))
我们将在玫瑰图上调用它,阈值为 150,白烟到浅蓝色分裂。
(-> (imread "resources/chapter02/rose.jpg" IMREAD_REDUCED_COLOR_4) (low-high! 150 rgb/white-smoke- rgb/lightblue-1) (u/mat-view)) Executing the preceding snippet gives us Figure 3-8. Figure 3-8
浅蓝色玫瑰上的白色
太好了。但是,你会问,我们真的需要为此创建两个遮罩吗?事实上,你不知道。你可以在第一个遮罩上完美地进行位元运算。为此,只需注释掉第二个遮罩创建并使用**位非!**第二次调用 set-to 之前。
;(threshold _copy _thresh-2 t1 255 THRESH_BINARY_INV) ;(cvt-color! _thresh-2 COLOR_BGR2GRAY) (set-to _work color2 (bitwise-not! _thresh-1))
在此基础上,您还可以对不同的颜色映射应用阈值,或者创建用作阈值的范围。
很明显,这里的另一个想法是对任何图片进行热空间皇后化。
如果你想知道,下面的代码片段可以帮你做到。
(def freddie-red (new-scalar 26 48 231)) (def freddie-blue (new-scalar 132 46 71)) (def bryan-yellow (new-scalar 56 235 255)) (def bryan-grey (new-scalar 186 185 181)) (def john-blue (new-scalar 235 169 0)) (def john-red (new-scalar 32 87 233)) (def roger-green (new-scalar 72 157 53)) (def roger-pink (new-scalar 151 95 226)) (defn queen-ize [mat thresh] (vconcat! [ (hconcat! [(-> mat clone (low-high! thresh freddie-red freddie-blue)) (-> mat clone (low-high! thresh john-blue john-red))]) (hconcat! [(-> mat clone (low-high! thresh roger-pink roger-green )) (-> mat clone (low-high! thresh bryan-yellow bryan-grey))] )]))
这真的是叫低-高!四次,每次都是 1982 年皇后乐队专辑 Hot Space 中的颜色。
And the old-fashioned result is shown in Figure 3-9. Figure 3-9
猫和皇后
你真会设置 的情绪
你真的进入了最佳状态
时尚弄潮儿
女王——“时尚弄潮儿”
手动通道
每当你要处理一个垫子的通道时,记住 opencv split 函数。该功能将通道分隔在一系列独立的垫子中,因此您可以完全只关注其中一个。
然后,您可以将变换应用到该特定的 mat,而不触及其他 mat,完成后,您可以使用 merge 函数返回到多通道 mat,该函数执行相反的操作,获取一个 mat 列表,每个通道一个,并创建一个目标 mat,将所有通道合并到一个 mat 中。
为了看到这一点,假设你有一个简单的橙色垫子(图 3-10 )。
(def orange-mat (new-mat 3 3 CV_8UC3 rgb/orange-2)) Figure 3-10
橙色 mat
如果你想把橙色的垫子变成红色的,你只需要把绿色通道的所有值都设置为 0。
所以,你从把 RGB 通道分成三个垫子开始;然后,将第二个 mat 的所有值设置为 0,并将所有三个 mat 合并为一个。
首先,让我们把垫子分成几个通道,看看每个通道的内容。
在快乐编码中,这给出了
(def channels (new-arraylist)) (split orange-mat channels)
这三个频道现在被分成列表中的三个元素。只需使用 dump 就可以查看每个频道的内容。
例如,转储蓝色通道:
(dump (nth channels 0)) ; no blue ;[0 0 0] ;[0 0 0] ;[0 0 0]
或者转储绿色通道:
(dump (nth channels 1)) ; quite a bit of green ;[154 154 154] ;[154 154 154] ;[154 154 154]
最后,转储红色通道:
(dump (nth channels 2)) ; almost max of red ;[238 238 238] ;[238 238 238] ;[238 238 238]
接下来,让我们将绿色通道中的所有 154 个值都变为 0。
(set-to (nth channels 1) (new-scalar 0.0))
然后,让我们将所有不同的垫子合并成一个垫子,得到图 3-11 。
(merge channels red-mat) Figure 3-11
红马特
mat 中所有像素上的绿色强度都统一设置为 0,因此所有蓝色通道值都已设置为 0,结果 mat 是一个完全红色的 mat。
我们可以将这个小练习的所有不同步骤结合起来,创建函数 **update-channel!,**它接受一个 mat、一个函数和应用该函数的通道,然后返回结果 mat。
让我们尝试使用 u/mat-to-bytes 和 **u/bytes-to-mat 的第一个版本!**在 mat 和 byte 数组之间来回转换。
这变得很复杂,但实际上是我能想到的解释转换流程的最简单的版本。
The code flow will be as follows:
-
将频道分成一个列表
-
检索目标通道的 mat
-
将 mat 转换为字节
-
将该函数应用于通道模板的每个字节
-
将字节数组转换回 mat
-
将 mat 设置到列表中相应频道
-
将通道合并到生成的垫子中
现在,至少应该按如下顺序阅读:
(defn update-channel! [mat fnc chan] (let [ channels (new-arraylist)] (split mat channels) (let [ old-ch (nth channels chan) new-ch (u/bytes-to-mat! (new-mat (.height mat) (.width mat) (.type old-ch) ) (byte-array (map fnc (u/mat-to-bytes old-ch) )))] (.set channels chan new-ch) (merge channels mat) mat)))
现在让我们回到我姐姐的猫,它已经在沙发上睡了一段时间了。是时候逗逗他,叫醒他了。
(def my-sister-cat (-> "resources/chapter03/emilie1.jpg" (imread IMREAD_REDUCED_COLOR_8)))
在更新频道的帮助下!函数,让我们将所有的蓝色和绿色通道值转换为它们的最大可能值 255。我们本来可以编写一个同时应用多个函数的函数,但是现在让我们一个接一个地调用同一个函数。
(-> my-sister-cat clone (update-channel! (fn [x] 255) 1) (update-channel! (fn [x] 255) 0) u/mat-view) This is not very useful as far as imaging goes, nor very useful for my sister’s cat either, but by maxing out all the values of the blue and green channels, we get a picture that is all cyan (Figure 3-12). Figure 3-12
青色 cat
这个新创建的函数也可以与转换色彩空间结合使用。
因此,在调用 **update-channel 之前切换到 HSV 色彩空间!**让您完全控制垫子的颜色。
(-> my-sister-cat clone (cvt-color! COLOR_RGB2HSV) (update-channel! (fn [x] 10) 0) ; blue filter (cvt-color! COLOR_HSV2RGB) (u/mat-view))
前面的代码应用蓝色滤镜,保持饱和度和亮度不变,从而仍然保持图像的动态。
当然,您可以尝试使用粉红色滤镜,将滤镜的值设置为 150,或者红色滤镜,将滤镜的值设置为 120 或任何其他可能的值。试试吧!
For now, enjoy the blue variation in Figure 3-13. Figure 3-13
蓝滤猫
就个人而言,我也喜欢 YUV 开关与最大化所有亮度值(Y)相结合。
(-> my-sister-cat clone (cvt-color! COLOR_BGR2YUV) (update-channel! (fn [x] 255) 0) (cvt-color! COLOR_YUV2BGR) (u/mat-view)) This gives a kind of watercolor feel to the image (Figure 3-14). Figure 3-14
巧妙的猫
改变
如果您还记得 transform,您还可以使用 opencv transform 函数应用不同种类的转换。
为了稍微了解一下 transform 的背景,让我们再一次回到通常的逐字节矩阵操作,首先是单通道 3×3 矩阵,我们想让它稍微暗一点。
(def s-mat (new-mat 3 3 CV_8UC1)) (.put s-mat 0 0 (byte-array [100 255 200 100 255 200 100 255 200]))
这可以通过以下代码查看(图 3-15 )。
(u/mat-view (-> s-mat clone (resize! (new-size 30 30) 1 1 INTER_AREA))) Figure 3-15
黑白旗帜
然后我们定义一个 1×1 的变换矩阵,一个值为 0.7。
(def t-mat (new-mat 1 1 CV_32F (new-scalar 0.7))
接下来,我们就地应用转换,并转储结果以查看转换的结果。
(-> s-mat (transform! t-mat) (dump))
调用 transform 函数的效果是将输入矩阵的所有值转换为其原始值乘以 0.7。
结果如下表所示:
[70 178 140] [70 178 140] [70 178 140]
这也意味着垫子的视觉效果变暗了(图 3-16 ):
(u/mat-view (-> s-mat (resize! (new-size 30 30) 1 1 INTER_AREA))) Figure 3-16
深色旗帜
This is a simple matrix computation, but it already shows two things:
-
源 mat 的字节都乘以 1×1 mat 中的值;
-
应用自定义转换实际上很容易。
对于具有多个通道的垫子,这些变换的工作方式大致相同。因此,让我们抓住一个例子,并使用 cvt-color 移动到一个彩色的颜色空间(是的,我知道)!
(def s-mat (new-mat 3 3 CV_8UC1)) (.put s-mat 0 0 (byte-array [100 255 200 100 255 200 100 255 200])) (cvt-color! s-mat COLOR_GRAY2BGR)
因为 mat 现在由三个通道组成,所以我们现在需要一个 3×3 的变换矩阵。
下面的变形垫子会给蓝色通道更多的力量。
[ 2 0 0 ; B -> B G R 0 1 0 ; G -> B G R 0 0 1] ; R -> B G R The transformation matrix is made of lines constructed as input-channel -> output channel, so three values per row, one for each output value of each channel, and three rows , one for each input.
-
[2 0 0]将蓝色通道的值提高 2 倍,不影响绿色或红色输出值
-
[0 1 0]保持绿色通道不变,不会影响输出中的其他通道
-
[0 0 1]保持红色通道不变,同样不会影响输出中的其他通道
(def t-mat (new-mat 3 3 CV_32F)) (.put t-mat 0 0 (float-array [2 0 0 0 1 0 0 0 1])) Applying the transformation to the newly colored mat gives you Figure 3-17, where blue is prominently standing out. Figure 3-17
蓝色标志
既然我们肯定没有办法让我姐姐的猫安静下来,那就让我们对它应用一个类似的变换。
该代码与前面的小 mat 示例完全相同,但是应用于一个图像。
(-> my-sister-cat clone (transform! (u/matrix-to-mat [ [2 0 0] [0 1 0] [0 0 1]]))) And Figure 3-18 shows a blue version of a usually white cat. Figure 3-18
蓝米乌夫
如果您想让输入中的蓝色也影响输出中的红色,您可以使用与下图稍微类似的矩阵:
[2 0 1.1 0 1 0 0 0 1 ]
你现在应该明白为什么了吧?[2 0 1.1]表示输入中的蓝色强度增加,但它也会增加输出中红色的强度。
您可能应该自己尝试几个转换矩阵来感受一下。
那么,现在,你如何使用类似的技术来增加垫子的亮度呢?
是的,没错:首先将矩阵转换到 HSV 色彩空间,然后乘以第三个通道,并保持其他通道不变。
下面的示例以同样的方式将亮度增加 1.5。
(-> my-sister-cat clone (cvt-color! COLOR_BGR2HSV) (transform! (u/matrix-to-mat [ [1 0 0] [0 1 0] [0 0 1.5]])) (cvt-color! COLOR_HSV2BGR) u/mat-view) Figure 3-19 shows the image output of the preceding snippet. Figure 3-19
夜光猫
巧妙的转变
总结这个食谱,让我们玩一点亮度和轮廓来创造一点艺术。
我们希望通过最大化亮度来创建输入图片的水彩版本。我们还想创建一个“轮廓”版本的图像,通过使用 opencv 的轮廓检测的精明的快速功能。最后,我们将结合这两个垫的铅笔水彩效果。
首先,让我们在背景上工作。背景是通过连续执行两个变换来创建的:一个是最大化 YUV 颜色空间中的亮度,另一个是通过增加蓝色和红色来使其更加生动。
(def usui-cat (-> my-sister-cat clone (cvt-color! COLOR_BGR2YUV) (transform! (u/matrix-to-mat [ [20 0 0] [0 1 0] [0 0 1]])) (cvt-color! COLOR_YUV2BGR) (transform! (u/matrix-to-mat [[3 0 0] [0 1 0] [0 0 2]]))))
如果你得到一个太透明的结果,你也可以在流水线的末端添加另一个变换来增加对比度;这在另一个色彩空间 HSV 中很容易做到。
(cvt-color! COLOR_BGR2HSV) (transform! (u/matrix-to-mat [[1 0 0] [0 3 0] [0 0 1]])) (cvt-color! COLOR_HSV2BGR) This gives us a nice pink-y background (Figure 3-20). Figure 3-20
背景是粉红色的猫
接下来是前景。前面的猫是通过调用 opencv 的 canny 函数创建的。这一次,这是在单通道灰色空间中完成的。
(def line-cat (-> my-sister-cat clone (cvt-color! COLOR_BGR2GRAY) (canny! 100.0 150.0 3 true) (cvt-color! COLOR_GRAY2BGR) (bitwise-not!))) The canny version of my sister’s cat gives the following (Figure 3-21): Figure 3-21
卡通猫
然后,使用对函数按位 and 的简单调用将两个 mat 组合在一起,该函数通过简单的“and”位操作将两个 mat 合并在一起。
(def target (new-mat)) (bitwise-and usui-cat line-cat target) This gives the nice artful cat in Figure 3-22. Figure 3-22
粉色、艺术和猫
虽然粉红色可能不是你最喜欢的颜色,但你现在有了所有的工具来根据你的喜好修改这个食谱中的流程,以创建许多不同的巧妙的猫,有不同的背景颜色,也有不同的前景。
但是拜托了。没有狗。
3.2 创作漫画
做你自己。没人能说你做错了。
问题
你已经看到了使用 canny 制作卡通艺术作品的非常简单的方法,但是你想掌握更多制作卡通艺术作品的变化。
解决办法
大多数卡通外观的转换可以使用灰度、模糊、canny 和通道滤镜功能的变化来创建,这些功能在前面的配方中已经介绍过。
它是如何工作的
您已经看到了 canny 函数,它以在图片中轻松突出显示形状而闻名。它实际上也可以用来画一点漫画。让我们看看我的朋友约翰。
Johan is a sharp Belgian guy who sometimes gets tricked into having a glass of good Pinot Noir (Figure 3-23). Figure 3-23
乔安
在这个菜谱中,Johan 加载了以下代码片段:
(def source (-> "resources/chapter03/johan.jpg" (imread IMREAD_REDUCED_GRAYSCALE_8)))
一个天真的 canny 调用应该是这样的,其中 10.0 和 90.0 是 canny 函数的底部和顶部阈值,3 是光圈,true/false 表示基本上是超高亮度模式或标准(false)。
(-> source clone (canny! 10.0 90.0 3 false)) Johan has now been turned into a canny version of himself (Figure 3-24). Figure 3-24
天真的 canny 用法
你已经知道我们可以使用 canny 函数的结果作为蒙版,例如在白色上复制蓝色(图 3-25 )。
(def colored (u/mat-from source)) (set-to colored rgb/blue-2) (def target (u/mat-from source)) (set-to target rgb/white) (copy-to colored target c) Figure 3-25
将蓝色复印在白色上
图中显示了相当多的线条。通过减小两个阈值之间的范围,我们可以使图片明显更清晰,看起来不那么杂乱。
(canny! 70.0 90.0 3 false) This indeed makes Johan a bit clearer (Figure 3-26). Figure 3-26
更清晰的约翰
结果不错,但似乎还是多了不少不该画的线。
通常用来移除这些多余线条的技术是在调用 canny 函数之前应用一个中值模糊或者一个高斯模糊。
高斯模糊通常更有效;毫不犹豫地放大,将模糊的大小增加到至少 13×13,甚至 21×21,如下所示:
(-> source clone (cvt-color! COLOR_BGR2GRAY) (gaussian-blur! (new-size 13 13) 1 1) (canny! 70.0 90.0 3 false)) That code snippet gives a neatly clearer picture (Figure 3-27). Figure 3-27
更好的约翰
你还记得双边过滤功能吗?如果你在调用 canny 函数后使用它*,它也会给出一些有趣的卡通形状,通过在 canny 效果中出现更多线条的地方进行强调。*
(-> source clone (cvt-color! COLOR_BGR2GRAY) (canny! 70.0 90.0 3 false) (bilateral-filter! 10 80 30))) Figure 3-28 shows the bilateral-filter! applied through a similar processing pipeline. Figure 3-28
应用双边过滤器
你应该记得双边过滤器的重点是加强轮廓。事实上,这就是我们在这里取得的成就。
Note also that the bilateral filter parameters are very sensitive, increasing the second parameter to 120; this gives a Picasso-like rendering (Figure 3-29). Figure 3-29
约翰松
所以,试试参数,看看什么对你有用。无论如何,整个 Origami 设置都是为了提供即时反馈。
还有,canny 不是唯一的选择。再来看看其他实现动漫效果的技巧。
双边卡通
双边过滤器实际上做了大量的卡通工作,所以让我们看看我们是否可以跳过狡猾的处理,坚持只使用双边过滤器的步骤。
We will create a new function called cartoon-0. That new function will
-
将输入图像变成灰色
-
应用非常大的双边过滤器
-
应用连续平滑函数
-
然后转回到一个 RGB 垫
一种可能的实现如下所示:
(defn cartoon-0! [buffer] (-> buffer (cvt-color! COLOR_RGB2GRAY) (bilateral-filter! 10 250 30) (median-blur! 7) (adaptive-threshold! 255 ADAPTIVE_THRESH_MEAN_C THRESH_BINARY 9 3) (cvt-color! COLOR_GRAY2BGR)))
卡通的输出-0!应用于 Johan 使其达到图 3-30 。
(-> "resources/chapter03/johan.jpg" (imread IMREAD_REDUCED_COLOR_8) cartoon-0! u/mat-view) Figure 3-30
没有精明的卡通
同样,双边滤波器的参数几乎可以完成所有工作。
改变**(双边过滤!10 250 30)** 到**(双边-过滤!9 9 7)** 给人完全不同的感觉。
(defn cartoon-1! [buffer] (-> buffer (cvt-color! COLOR_RGB2GRAY) (bilateral-filter! 9 9 7) (median-blur! 7) (adaptive-threshold! 255 ADAPTIVE_THRESH_MEAN_C THRESH_BINARY 9 3) (cvt-color! COLOR_GRAY2BGR))) And Johan now looks even more artistic and thoughtful (Figure 3-31). Figure 3-31
体贴的约翰
更新频道变灰
这个食谱的最后一个技巧将带我们回到使用**更新频道!**前一个配方中编写的功能。
This new method uses update-channel with a function that
-
如果原始值小于 70,则将灰色通道的值变为 0;
-
如果原始值大于 80 但小于 180,则将其转换为 100;和
-
否则就变成 255。
这给出了以下稍微长但简单的流水线:
(-> "resources/chapter03/johan.jpg" (imread IMREAD_REDUCED_COLOR_8) (median-blur! 1) (cvt-color! COLOR_BGR2GRAY) (update-channel! (fn[x] (cond (< x 70) 0 (< x 180) 100 :else 255)) 0) (bitwise-not!) (cvt-color! COLOR_GRAY2BGR) (u/mat-view)) This is nothing you would not understand by now, but the pipeline is quite a pleasure to write and its result even more so, because it gives more depth to the output than the other techniques used up to now (Figure 3-32). Figure 3-32
深度约翰
流水线的输出看起来很棒,但是像素经过了相当多的处理,所以很难判断在这个阶段每个像素内部有什么,之后的后处理需要一些小心。
比如你想增加前面输出的亮度或者改变颜色;通常,在对颜色进行任何更改之前,最好再次切换到 HSV 颜色空间并增加亮度,如下所示:
(-> "resources/chapter03/shinji.jpg" (imread IMREAD_REDUCED_COLOR_4) (cartoon! 70 180 false) (cvt-color! COLOR_BGR2HSV) (update-channel! (fn [x] 250) 1) (update-channel! (fn [x] 5) 0) (cvt-color! COLOR_HSV2BGR) (bitwise-not!) (flip! 1) (u/mat-view)) The final processing pipeline gives us a shining blue Johan (Figure 3-33). The overall color is blue due to channel 0’s value set to 5 in HSV range, and the luminosity set to 250, almost the maximum value. Figure 3-33
翻转和蓝色
作为奖励,我们也只是水平翻转图像,以前瞻性的图片结束这个食谱!
3.3 创建铅笔草图
问题
你已经看到了如何为肖像做一些漫画,但想通过结合正面素描和深背景色来赋予它更多的艺术感。
解决办法
为了创建有冲击力的背景,你将会看到如何使用 pyr-down 和 pyr-up 结合你已经看到的平滑方法。
为了合并结果,我们将再次使用位与。
它是如何工作的
My hometown is in the French Alps, near the Swiss border, and there is a very nice canal flowing between the houses right in the middle of the old town (Figure 3-34). Figure 3-34
夏天的法国安纳西
这里的目标是创建该图片的绘画版本。
The plan is to proceed in three phases.
没有计划的目标只是一个愿望。
第一阶段:我们通过平滑边缘和循环降低图片的分辨率来完全去除图片的所有轮廓。这将是背景图片。
第二阶段:我们反其道而行之,也就是说我们把注意力放在轮廓上,运用和卡通食谱中相似的技术,把图片变成灰色,找到所有的边缘,并赋予它们尽可能多的深度。这将是前面的部分。
第三阶段:最后,我们结合第一阶段和第二阶段的结果,以获得我们正在寻找的绘画效果。
背景
**pyr-down!**对你来说可能是新的。这会降低图像的分辨率。让我们比较应用以下代码片段所做的分辨率更改前后的地垫。
(def factor 1) (def work (clone img)) (dotimes [_ factor] (pyr-down! work))
之前:
#object[org.opencv.core.Mat 0x3f133cac "Mat [ 431431CV_8UC3...]"]
之后:
#object[org.opencv.core.Mat 0x3f133cac "Mat [ 216216CV_8UC3...]"]
基本上,mat 的分辨率除以 2,四舍五入到像素。(是的,我以前听过 1/2 像素的故事,但是要注意……那些都不是真的!!)
Using a factor of 4, and thus applying the resolution downgrade four times, we get a mat that is now 27×27 and looks like the mat in Figure 3-35. Figure 3-35
更改分辨率
为了创建背景效果,我们实际上需要一个与原始大小相同的垫子,因此需要将输出的大小调整为原始大小。
第一个想法当然是简单地尝试通常的调整大小!功能:
(resize! work (.size img)) But that does result in something not very satisfying to the eyes. Figure 3-36 indeed shows some quite visible weird pixelization of the resized mat. Figure 3-36
嗯…调整大小
让我们试试别的东西。有一个 pyr-down 的反向函数,名为 pyr-up,可以将一个 mat 的分辨率提高一倍。为了有效地使用它,我们可以在一个循环中应用 pyr-up,并像使用 pyr-down 一样循环相同的次数。
(dotimes [_ factor] (pyr-up! work)) The resulting mat is similar to Figure 3-36, but is much smoother, as shown in Figure 3-37. Figure 3-37
平滑模糊
通过在 pyr-down 和 pyr-up 舞蹈之间的垫子中应用模糊来最终确定背景。
所以:
(dotimes [_ factor] (pyr-down! work)) (bilateral-filter! work 11 11 7) (dotimes [_ factor] (pyr-up! work))
输出留到以后,后台就这样了;让我们移动到前景的边缘发现部分。
前景和结果
前景将主要是前一个食谱的复制粘贴练习。你当然可以在这个阶段创造自己的变体;这里我们将使用一个由中值模糊和自适应阈值步骤组成的卡通化函数。
(def edge (-> img clone (resize! (new-size (.cols output) (.rows output))) (cvt-color! COLOR_RGB2GRAY) (median-blur! 7) (adaptive-threshold! 255 ADAPTIVE_THRESH_MEAN_C THRESH_BINARY 9 7) (cvt-color! COLOR_GRAY2RGB))) Using the old town image as input, this time you get a mat showing only the prominent edges, as shown in Figure 3-38. Figure 3-38
到处都是边缘
为了完成这个练习,我们现在使用按位 and 将两个垫子组合起来。基本上,由于边缘是黑色的,按位 and 运算使它们保持黑色,它们的值将被复制到输出 mat。
这将具有将未改变的边缘复制到目标结果上的结果,并且由于边缘垫的剩余部分是由白色构成的,按位与将是另一个垫的值,因此背景垫的颜色将优先。
(let [result (new-mat) ] (bitwise-and work edge result) (u/mat-view result)) This gives you the sketching effect of Figure 3-39. Figure 3-39
像专业人士一样素描
使用“自适应阈值”步骤,可以调整正面草图的外观。
(adaptive-threshold! 255 ADAPTIVE_THRESH_MEAN_C THRESH_BINARY edges-thickness edges-number)
在第一张草图中,我们用 9 作为边厚,7 作为边数;让我们看看如果我们把这两个参数设为 5 会发生什么。
This gives more space to the color of the background, by reducing the thickness of the edges (Figure 3-40). Figure 3-40
较薄的边缘
现在就看你的发挥了,从那里随机应变!
摘要
Finally, let’s get you equipped with a ready-to-use sketch! function. This is an exact copy of the code that has been used up to now, with places for the most important parameters for this sketching technique:
-
用于降低分辨率然后再次提高分辨率的因素,例如舞蹈中的循环次数
-
背景双边滤波器的参数
-
前景的自适应阈值的参数
素描!函数是由平滑构成的!还有棱角!。首先,让我们使用平滑!来创造背景。
(defn smoothing! [img factor filter-size filter-value] (let [ work (clone img) output (new-mat)] (dotimes [_ factor] (pyr-down! work)) (bilateral-filter work output filter-size filter-size filter-value) (dotimes [_ factor] (pyr-up! output)) (resize! output (new-size (.cols img) (.rows img)))))
然后边缘!来创造前景。
(defn edges! [img e1 e2 e3] (-> img clone (cvt-color! COLOR_RGB2GRAY) (median-blur! e1) (adaptive-threshold! 255 ADAPTIVE_THRESH_MEAN_C THRESH_BINARY e2 e3) (cvt-color! COLOR_GRAY2RGB)))
终于可以用素描了!,背景和前景的结合。
(defn sketch! [ img s1 s2 s3 e1 e2 e3] (let [ output (smoothing! img s1 s2 s3) edge (edges! img e1 e2 e3)] (bitwise-and output edge output) output))
召唤素描!是比较容易的。您可以尝试下面的代码片段:
(sketch! 6 9 7 7 9 11) And instantly turn the landscape picture of Figure 3-41 … Figure 3-41
树
into the sketched version of Figure 3-42. Figure 3-42
风景素描
其他几个已经放入了示例中,但是现在确实是时候拍摄您自己的照片并尝试这些功能和参数了。
3.4 创建画布效果
问题
创造景观艺术对你来说似乎没有更多的秘密,但你想在它上面浮雕一块画布,使它更像一幅画。
解决办法
这个简短的食谱将重复使用你已经看到的技术,以及两个新的 mat 函数:乘和除。
使用 divide,可以创建一个垫子的燃烧和躲闪效果,我们将使用它们来创建想要的效果。
有了乘,就有可能将垫子组合成一个漂亮的深度效果,因此通过使用一个看起来像纸一样的背景垫子,就有可能在画布上产生一个特殊的绘制输出。
它是如何工作的
我们将在法国阿尔卑斯山再拍一张照片——我是说为什么不呢!—因为我们想让它看起来有点复古,我们将使用一个古老城堡的图像。
(def img (-> "resources/chapter03/montrottier.jpg" (imread IMREAD_REDUCED_COLOR_4))) Figure 3-43 shows the castle of Montrottier, which you should probably visit when you have the time, or vacation (I do not even know what the second word means anymore). Figure 3-43
向星星许愿
我们首先从应用一个位非开始!,然后在源图片的灰色克隆上进行高斯模糊;用 Origami 流水线很容易做到这一点。
我们将需要一个灰色的版本,所以让我们保持两个垫子灰色和 gaussed 分开。
(def gray (-> img clone (cvt-color! COLOR_BGR2GRAY))) (def gaussed (-> gray clone bitwise-not! (gaussian-blur! (new-size 21 21) 0.0 0.0))) Figure 3-44 shows the gaussed mat, which looks like a spooky version of the input image. Figure 3-44
幽灵城堡
我们将使用这个 gaussed 垫作为一个面具。神奇的事情发生在函数道奇!,在原图上使用 opencv 函数 divide,以及 gaussed mat 的反转版本。
(defn dodge! [img_ mask] (let [ output (clone img_) ] (divide img_ (bitwise-not! (-> mask clone)) output 256.0) output))
嗯……好吧。除法是做什么的?我的意思是,你知道它划分事物,但是在字节水平上,真正发生的是什么?
我们举两个矩阵,a 和 b,对它们调用 divide 作为例子。
(def a (u/matrix-to-mat [[1 1 1]])) (def b (u/matrix-to-mat [[0 1 2]])) (def c (new-mat)) (divide a b c 10.0) (dump c)
divide 调用的输出是
[0 10 5]
哪个是
[ (a0 / b0) * 10.0, (a1 / b1) * 10.0, (a2 / b2) * 10.0]
这给了
[ 1 / 0 * 10.0, 1 / 1 * 10.0, 1 / 2 * 10.0]
那么,鉴于 OpenCV 认为除以 0 等于 0:
[0, 10, 5]
现在,让我们打电话给道奇!在灰色垫子和格纹垫子上:
(u/mat-view (dodge! gray gaussed)) And see the sharp result of Figure 3-45. Figure 3-45
锋利的铅笔
应用画布
现在,主画面已经变成了蜡笔风格的艺术形式,把它放在一个看起来像帆布的垫子上会很好。如前所述,这是使用 OpenCV 中的乘函数完成的。
We want the canvas to look like a very old parchment, and we will use the one from Figure 3-46. Figure 3-46
旧羊皮纸
现在我们将创建**应用画布!**函数,接受前端草图和画布,并在它们之间应用乘法函数。(/ 1 256.0)是用于相乘的值;因为这里是灰色字节,值越大越白,所以这里(/ 1 256.0)使得最终结果上的黑线非常明显。
(defn apply-canvas! [ sketch canvas] (let [ out (new-mat)] (resize! canvas (new-size (.cols sketch) (.rows sketch))) (multiply (-> sketch clone (cvt-color! COLOR_GRAY2RGB)) canvas out (/ 1 256.0)) out))
呜呼。差不多了;现在让我们调用这个新创建的函数
(u/mat-view (apply-canvas! sketch canvas)) And enjoy the drawing on the canvas (Figure 3-47). Figure 3-47
旧羊皮纸上的城堡
现在显然是你去寻找/扫描你自己的旧论文的时候了,用这种技术尝试一些事情;或者为什么不重用以前食谱中的卡通功能来覆盖不同的纸张呢?
3.5 突出显示线条和圆圈
问题
这个食谱是关于如何在一个加载的垫子中找到并高亮显示线条、圆圈和线段的。
解决办法
通常需要一点预处理来准备图像,以便用一些谨慎和平滑的操作进行分析。
一旦第一步准备工作完成,就可以用 opencv 函数 hough-circles 找到圆。
查找线条的版本称为 hough-lines,其兄弟 hough-lines-p 使用概率来查找更好的线条。
最后,我们将看到如何使用线段检测器来绘制找到的线段。
它是如何工作的
用霍夫线寻找网球场的线
本教程的第一部分展示了如何在图像中寻找线条。我们将以网球场为例。
(def tennis (-> "resources/chapter03/tennis_ground.jpg" imread )) You have probably seen a tennis court before, and this one is not so different from the others (Figure 3-48). If you have never seen a tennis court before, this is a great introduction all the same, but you should probably stop reading and go play a game already. Figure 3-48
网球场
为 hough-lines 函数准备目标是通过将原始网球场图片转换为灰色,然后应用简单的 canny 变换来完成的。
(def can (-> tennis clone (cvt-color! COLOR_BGR2GRAY) (canny! 50.0 180.0 3 false))) With the expected result of the lines standing out on a black background, as shown in Figure 3-49. Figure 3-49
漂亮的网球场
在 opencv 的底层 Java 版本中,行被收集在一个 mat 中,因此,无法避免这一点,我们也将准备一个 mat 来接收结果行。
hough-lines 函数本身是用一组参数调用的。可以在 OpenCV 网站上找到霍夫变换的完整的基本极坐标系统解释:
docs . opencv . org/3 . 3 . 1/d9/db0/tutorial _ Hough _ lines . html
你真的不需要现在就阅读所有的东西,但是意识到什么可以做什么不可以做是很好的。
现在,我们将只应用链接教程中建议的相同参数。
(def lines (new-mat)) (hough-lines can lines 1 (/ Math/PI 180) 100)
得到的线条矩阵由一列行组成,每行有两个值 rho 和 theta。
创建从 rho 和 theta 画线所需的两个点有点复杂,但在 opencv 教程中有描述。
现在,下面的函数为您完成了工作。
(def result (clone parking)) (dotimes [ i (.rows lines)] (let [ val_ (.get lines i 0) rho (nth val_ 0) theta (nth val_ 1) a (Math/cos theta) b (Math/sin theta) x0 (* a rho) y0 (* b rho) pt1 (new-point (Math/round (+ x0 (* 1000 (* -1 b)))) (Math/round (+ y0 (* 1000 a)))) pt2 (new-point (Math/round (- x0 (* 1000 (* -1 b)))) (Math/round (- y0 (* 1000 a)))) ] (line result pt1 pt2 color/black 1))) Drawing the found lines on top of the tennis court mat creates the image in Figure 3-50. Figure 3-50
霍夫线结果
请注意,当调用 hough-lines 时,将值为 1 的参数更改为值为 2 会得到更多的行,但是您可能需要在之后自己过滤这些行。
同样根据经验,将数学/圆周率舍入从 180°更改为 90°会产生更少的行,但结果会更好。
霍夫线 P
hough-lines 函数的另一个变体,名为 hough-lines-p,是一个添加了概率数学的增强版本,它通常通过执行猜测来给出一组更好的线。
为了用 P 来尝试霍夫线,我们这次将以…一个足球场为例。
(def soccer-field (-> "resources/chapter03/soccer-field.jpg" (imread IMREAD_REDUCED_COLOR_4))) (u/mat-view soccer-field)
按照最初的 hough-lines 示例,我们将足球场变成灰色,并应用轻微的高斯模糊来消除源图像中可能的缺陷。
(def gray (-> soccer-field clone (cvt-color! COLOR_BGR2GRAY) (gaussian-blur! (new-size 1 1) 0 ) )) The resulting grayed version of the soccer field is shown in Figure 3-51. Figure 3-51
灰色足球场
现在让我们制作一个巧妙的球场来创建边缘。
(def edges (-> gray clone (canny! 100 220)))
现在,我们调用 hough-lines-p. 在下面的代码片段中解释了所使用的参数。预计将从新创建的边垫中收集线。
; distance resolution in pixels of the Hough grid (def rho 1) ; angular resolution in radians of the Hough grid (def theta (/ Math/PI 180)) ; minimum number of votes (intersections in Hough grid cell) (def min-intersections 30) ; minimum number of pixels making up a line (def min-line-length 10) ; maximum gap in pixels between connectable line segments (def max-line-gap 50)
参数准备好了;让我们调用 hough-lines-p,结果存储在 lines mat 中。
(def lines (new-mat)) (hough-lines-p edges lines rho theta min-intersections min-line-length max-line-gap)
这一次,线条比常规的 hough-lines 函数更容易绘制。结果矩阵的每一行都由四个值组成,这四个值对应于绘制该行所需的两个点。
(def result (clone soccer-field)) (dotimes [ i (.rows lines)] (let [ val (.get lines i 0)] (line result (new-point (nth val 0) (nth val 1)) (new-point (nth val 2) (nth val 3)) color/black 1))) The result of drawing the results of hough-lines-p is displayed in Figure 3-52. Figure 3-52
足球场上的线
在台球桌上找口袋
不再在球场上跑来跑去;让我们移动到…台球桌!
以类似的方式,opencv 有一个名为 hough-circles 的函数来寻找看起来像圆的形状。更重要的是,这个功能很容易实现。
This time, let’s try to find the ball pockets of a billiard table. The exercise is slightly difficult because it is easy to wrongly count the regular balls as pockets.
你不能在没有准备好的情况下敲开机会的大门。
布鲁诺·马尔斯
我们先把台球桌准备好。
(def pool (-> "resources/chapter03/pooltable.jpg" (imread IMREAD_REDUCED_COLOR_2)))
使用 hough-circles,似乎可以通过绕过预处理中的谨慎步骤来获得更好的结果。
下面的代码片段显示了要在源 mat 中查找的圆的最小和最大半径值的位置。
(def gray (-> pool clone (cvt-color! COLOR_BGR2GRAY))) (def minRadius 13) (def maxRadius 18) (def circles (new-mat)) (hough-circles gray circles CV_HOUGH_GRADIENT 1 minRadius 120 10 minRadius maxRadius)
这里,圆被收集在一个垫子中,每条线包含圆心的 x 和 y 位置及其半径。
最后,我们简单地用 opencv circle 函数在结果 mat 上画圆。
(def output (clone pool)) (dotimes [i (.cols circles)] (let [ _circle (.get circles 0 i) x (nth _circle 0) y (nth _circle 1) r (nth _circle 2) p (new-point x y)] (circle output p (int r) color/white 3))) All the pockets are now highlighted in white in Figure 3-53. Figure 3-53
白色台球桌的口袋!
Note that if you put the minRadius value too low, you quickly get false positives with the regular balls, as shown in Figure 3-54. Figure 3-54
假口袋
因此,精确定义要搜索的内容是大多数 OpenCV 工作(也许还有其他工作)成功的秘诀。
因此,为了避免这里的假阳性,在接受和画线之前过滤颜色可能也是一个好主意。接下来看看怎么做。
寻找圆圈
在这个简短的例子中,我们将寻找垫子中的红色圆圈,在垫子中可以找到多种颜色的圆圈。
(def bgr-image (-> "resources/detect/circles.jpg" imread (u/resize-by 0.5) )) The bgr-image is shown in Figure 3-55. Figure 3-55
彩色圆圈
如果你直接阅读这本书的黑白版本,你可能看不到它,但我们将把注意力集中在左下角的大圆圈上,它是一个鲜艳的红色。
如果你还记得以前食谱中的经验,你已经知道我们需要将色彩空间转换为 HSV,然后过滤 0 到 10 之间的色调范围。
下面的代码片段展示了如何使用一些额外的模糊来实现这一点,以方便以后的处理。
(def ogr-image (-> bgr-image (clone) (median-blur! 3) (cvt-color! COLOR_BGR2HSV) (in-range! (new-scalar 0 100 100) (new-scalar 10 255 255)) (gaussian-blur! (new-size 9 9) 2 2))) All the circles we are not looking for have disappeared from the mat resulting from the small pipeline, and the only circle we are looking for is now standing out nicely (Figure 3-56). Figure 3-56
红色圆圈显示为白色
现在,我们可以应用与之前看到的相同的霍夫圆调用;同样,圆圈将被收集在圆圈垫中,这将是一个 1×1 的垫,具有三个通道。
(def circles (new-mat)) (hough-circles ogr-image circles CV_HOUGH_GRADIENT 1 (/ (.rows bgr-image) 8) 100 20 0 0) (dotimes [i (.cols circles)] (let [ _circle (.get circles 0 i) x (nth _circle 0) y (nth _circle 1) r (nth _circle 2) p (new-point x y)] (circle bgr-image p (int r) rgb/greenyellow 5))) The result of drawing the circle with a border is shown in Figure 3-57. The red circle has been highlighted with a green-yellow color and a thickness of 5. Figure 3-57
突出显示的红色圆圈
使用绘制线段
有时,最简单的方法可能是简单地使用所提供的片段检测器。它对 Origami 不太友好,因为使用的方法是直接的 Java 方法调用(所以前缀是点“.”),但是片段比较自成一体。
让我们在之前看到的足球场上试试。这次我们将它直接加载到 gray,并观察分段检测器的行为。
(def soccer-field (-> "resources/chapter03/soccer-field.jpg" (imread IMREAD_REDUCED_GRAYSCALE_4))) (def det (create-line-segment-detector)) (def lines (new-mat)) (def result (clone soccer-field))
我们在线段检测器上调用 detect ,现在使用 Clojure Java Interop。
(.detect det soccer-field lines)
在这个阶段,lines mat 元数据是 1611CV_32FC4,意味着 161 行,每行由 1 列和每个点的 4 个通道组成,意味着每个值 2 个点。
检测器有一个有用的 drawSegments 函数,我们可以调用它来获得结果 mat。
(.drawSegments det result lines) The soccer field mat is now showing in Figure 3-58, this time with all the lines highlighted, including circles and semicircles. Figure 3-58
第一季第一集
3.6 查找并绘制轮廓和边界框
问题
由于识别和计算形状是 OpenCV 使用的前沿,您可能想知道如何在 Origami 中使用轮廓查找技术。
解决办法
除了传统的清理和图像准备,这个方法将引入 find-contours 函数来填充轮廓列表。
一旦找到轮廓,我们需要应用一个简单的过滤器来去除非常大的轮廓,如整个图片,以及实在太小而无用的轮廓。
一旦过滤完成,我们可以使用手工绘制的圆形和矩形或者提供的函数绘制轮廓来绘制轮廓。
它是如何工作的
索尼耳机
它们不再那么新了,但我爱我的索尼耳机。我只是带着它们到处走,你可以满足你的自恋,并通过简单地穿着它们获得你需要的所有关注。无论是在火车上还是在飞机上,它们都能带给你最好的声音…
让我们来玩一个快速寻找耳机轮廓的游戏。
(def headphones (-> "resources/chapter03/sonyheadphones.jpg" (imread IMREAD_REDUCED_COLOR_4)))
我的耳机仍然有一根电缆,因为我更喜欢声音,不管一些大公司怎么说。
Anyway, the headphones are shown in Figure 3-59. Figure 3-59
带线缆的索尼耳机
首先,我们需要准备耳机,以便更容易分析。为此,我们创建了一个有趣的部分,耳机本身的面具。
(def mask (-> headphones (cvt-color! COLOR_BGR2GRAY) (clone) (threshold! 250 255 THRESH_BINARY_INV) (median-blur! 7))) The inverted thresh binary output is shown in Figure 3-60. Figure 3-60
蒙面耳机
然后,通过使用蒙版,我们创建了一个蒙版输入 mat,它将用于简化轮廓查找步骤。
(def masked-input (clone headphones)) (set-to masked-input (new-scalar 0 0 0) mask) (set-to masked-input (new-scalar 255 255 255) (bitwise-not! mask))
你注意到了吗?是的,创建输入有一种更简单的方法,首先简单地创建一个非反转遮罩,但第二种方法为准备输入垫提供了更多的控制。
所以这里我们基本上分两步走。首先,当蒙版的相同像素值为 1 时,将原始 mat 的所有像素设置为黑色。下一步,设置所有其他的值为白色,在相反版本的面具。
The prepared result mat is in Figure 3-61. Figure 3-61
输入垫的准备
现在,用于查找轮廓的 mat 已经准备好了,您几乎可以直接在它上面调用 find-contours。
find-contours 有几个明显的参数,还有两个,最后两个,有点模糊。
RETR _ 列表是最简单的一种,将所有轮廓作为列表返回,而RETR _ 树是最常用的,表示轮廓是分层有序的。
CHAIN_APPROX_NONE 表示找到的轮廓的所有点被存储。不过,通常在绘制这些轮廓时,并不需要定义它们的所有点。如果不需要所有的点,可以使用 CHAIN_APPROX_SIMPLE ,减少定义轮廓的点数。
这最终取决于你如何处理之后的轮廓。但是现在,让我们保持所有的点!
(def contours (new-arraylist)) (find-contours masked-input contours (new-mat) ; mask RETR_TREE CHAIN_APPROX_NONE)
好的,现在让我们画矩形来突出每个找到的轮廓。我们在轮廓列表上循环,对于每个轮廓,我们使用 bounding-rect 函数来获得一个包围轮廓本身的矩形。
从 bounding-rect 调用中获取的矩形几乎可以原样使用,我们将用它来绘制我们的第一个轮廓。
(def exercise-1 (clone headphones)) (doseq [c contours] (let [ rect (bounding-rect c)] (rectangle exercise-1 (new-point (.x rect) (.y rect)) (new-point (+ (.width rect) (.x rect)) (+ (.y rect) (.height rect))) (color/->scalar "#ccffcc") 2))) Contours are now showing in Figure 3-62. Figure 3-62
头电话轮廓
没错。还不错。从图中可以很明显地看出,覆盖整个画面的大矩形并不是很有用。这就是为什么我们需要一点过滤。
Let’s filter the contours, by making sure they are
-
不要太小,这意味着它们应该覆盖的面积至少为 10,000,也就是 125×80 的表面。
-
也不能太大,也就是说高度不能覆盖整个画面。
下面的代码片段完成了过滤。
(def interesting-contours (filter #(and (> (contour-area %) 10000 ) (< (.height (bounding-rect %)) (- (.height headphones) 10))) contours))
所以,这次只画出有趣的轮廓就相当准确了。
(def exercise-1 (clone headphones)) (doseq [c interesting-contours] ...) Figure 3-63 this time shows only useful contours. Figure 3-63
耳机有趣的外形
画圆而不是矩形应该不会太难,所以我们在有趣的轮廓上做同样的循环,但是这次,基于边界矩形画一个圆。
(def exercise-2 (clone headphones)) (doseq [c interesting-contours] (let [ rect (bounding-rect c) center (u/center-of-rect rect) ] (circle exercise-2 center (u/distance-of-two-points center (.tl rect)) (color/->scalar "#ccffcc") 2))) The resulting mat, exercise-2, is shown in Figure 3-64. Figure 3-64
在它上面盘旋
最后,虽然它很难用于检测处理,但您也可以使用 opencv 函数 draw-contours 来很好地绘制轮廓的自由形状。
我们仍将在有趣轮廓列表上循环。请注意,参数可能感觉有点奇怪,因为绘制轮廓使用的是索引和列表,而不是轮廓本身,所以在使用绘制轮廓时要小心。
(def exercise-3 (clone headphones)) (dotimes [ci (.size interesting-contours)] (draw-contours exercise-3 interesting-contours ci (color/->scalar "#cc66cc") 3)) And finally, the resulting mat can be found in Figure 3-65. Figure 3-65
耳机和粉色轮廓
事情并不总是那么容易,那我们再举一个天上掉馅饼的例子吧!
在天空中
第二个例子以天空中的热气球为例,希望在上面绘制轮廓。
The picture of hot-air balloons in Figure 3-66 seems very innocent and peaceful. Figure 3-66
热气球
不幸的是,使用与前面所示相同的技术来准备图片并不能达到非常性感的效果。
(def wrong-mask (-> kikyu clone (cvt-color! COLOR_BGR2GRAY) (threshold! 250 255 THRESH_BINARY) (median-blur! 7))) It’s pretty pitch-black in Figure 3-67. Figure 3-67
有人在上面吗?
所以,让我们试试另一种技术。为了得到更好的口罩,你会怎么做?
是的——为什么不呢?让我们过滤所有这些蓝色,并从中创建一个模糊的面具。这将为您提供以下代码片段。
(def mask (-> kikyu (clone) (cvt-color! COLOR_RGB2HSV) (in-range! (new-scalar 10 30 30) (new-scalar 30 255 255)) (median-blur! 7))) Nice! Figure 3-68 shows that this actually worked out pretty neatly. Figure 3-68
有用的面具
我们现在将使用补充版本的面具来寻找轮廓。
(def work (-> mask bitwise-not!))
使用查找轮廓功能,没有更多的秘密向你隐藏。或许是吧?参数表中的新点在做什么?不用担心;它只是一个偏移量,这里我们没有指定偏移量,所以 0 0。
(def contours (new-arraylist)) (find-contours work contours (new-mat) RETR_LIST CHAIN_APPROX_SIMPLE (new-point 0 0))
轮廓在里面!让我们过滤尺寸,并在它们周围画圆。这只是上一个例子的重复。
(def output_ (clone kikyu)) (doseq [c contours] (if (> (contour-area c) 50 ) (let [ rect (bounding-rect c)] (if (and (> (.height rect) 40) (> (.width rect) 60)) (circle output_ (new-point (+ (/ (.width rect) 2) (.x rect)) (+ (.y rect) (/ (.height rect) 2))) 100 rgb/tan 5))))) Nice. You are getting pretty good at those things. Look at and enjoy the result of Figure 3-69. Figure 3-69
在热气球上盘旋
接下来,让我们在绘图之前进行过滤,并再次使用 bounding-rect 来绘制矩形。
(def my-contours (filter #(and (> (contour-area %) 50 ) (> (.height (bounding-rect %)) 40) (> (.width (bounding-rect %)) 60)) contours))
的确,如果你检查它的内容,我的轮廓只有三个元素。
(doseq [c my-contours] (let [ rect (bounding-rect c)] (rectangle output (new-point (.x rect) (.y rect)) (new-point (+ (.width rect) (.x rect)) (+ (.y rect) (.height rect))) rgb/tan 5))) Now drawing those rectangles results in Figure 3-70. Figure 3-70
热气球上的长方形
3.7 关于轮廓的更多信息:使用形状
问题
继续上一个食谱,您将会看到函数 find-contours 返回了什么。用所有的点绘制轮廓是不错的,但是如果你想用不同的颜色突出不同的形状呢?
此外,如果形状是手绘的,或者在源 mat 中显示不正确怎么办?
解决办法
我们仍然要像到目前为止所做的那样使用查找轮廓和绘制轮廓,但是我们要在绘制它们之前对每个轮廓做一些预处理,以找出它们有多少条边。
近似-多边形- dp 是用于近似形状的函数,从而减少点的数量,只保留多边形形状中最重要的点。我们将创建一个小函数,**approximate,**将形状转换成多边形,并计算它们的边数。
我们还将看看填充凸多边形,看看我们如何绘制手写形状的近似轮廓。
最后,另一个名为折线的 opencv 函数将用于只绘制找到的轮廓的线框。
它是如何工作的
突出轮廓
We will use a picture with many shapes for the first part of this exercise, like the one in Figure 3-71. Figure 3-71
形状
这里的目标是根据每个形状的边数,用不同的颜色绘制每个形状的轮廓。
shapes mat 只需加载以下代码片段:
(def shapes (-> "resources/morph/shapes3.jpg" (imread IMREAD_REDUCED_COLOR_2)))
正如在前面的配方中所做的,我们首先通过将输入的克隆转换为灰色来准备一个 thresh mat,然后应用一个简单的阈值来突出显示形状。
(def thresh (-> shapes clone (cvt-color! COLOR_BGR2GRAY) (threshold! 210 240 1))) (def contours (new-arraylist)) Looking closely, we can see that the shapes are nicely highlighted, and if you look at Figure 3-72, the thresh is indeed nicely showing the shapes. Figure 3-72
功能脱粒机
好了,thresh 准备好了,所以你现在可以调用 find-contours 了。
(find-contours thresh contours (new-mat) RETR_LIST CHAIN_APPROX_SIMPLE)
为了绘制轮廓,我们首先编写一个 dump 函数,它在轮廓列表上循环,并用洋红色绘制每个轮廓。
(defn draw-contours! [img contours] (dotimes [i (.size contours)] (let [c (.get contours i)] (draw-contours img contours i rgb/magenta-2 3))) img) (-> shapes (draw-contours! contours) (u/mat-view)) The function works as expected, and the result is shown in Figure 3-73. Figure 3-73
洋红色轮廓
但是,正如我们已经说过的,我们希望为每个轮廓使用不同的颜色,所以让我们编写一个函数,根据轮廓的边来选择颜色。
(defn which-color[c] (condp = (how-many-sides c) 1 rgb/pink 2 rgb/magenta- 3 rgb/green 4 rgb/blue 5 rgb/yellow-1- 6 rgb/cyan-2 rgb/orange))
不幸的是,即使将 CHAIN_APPROX_SIMPLE 作为参数传递给 find-contours,每个形状的点数仍然太高,没有任何意义。
8, 70, 132, 137...
因此,让我们通过将形状转换为近似值来减少点数。
opencv 中使用了两个函数:弧长函数和近似多边形函数。因子 0.02 是 opencv 提出的默认值;稍后我们将在这个食谱中看到不同值的影响。
(defn approx [c] (let[m2f (new-matofpoint2f (.toArray c)) len (arc-length m2f true) ret (new-matofpoint2f) app (approx-poly-dp m2f ret (* 0.02 len) true)] ret))
使用这个新的近似值函数,我们现在可以通过计算近似值的点数来计算边数。
下面是一个简单的多少边函数。
(defn how-many-sides[c] (let[nb-sides (.size (.toList c))] nb-sides))
一切就绪;让我们重写愚蠢的轮廓图!使用 which-color 函数变成稍微进化的东西。
(defn draw-contours! [img contours] (dotimes [i (.size contours)] (let [c (.get contours i)] (draw-contours img contours i (which-color c) 3))) img) And now calling the updated function properly highlights the polygons, counting the number of sides on an approximation of each of the found shapes (Figure 3-74). Figure 3-74
不同形状,不同颜色
请注意这个圆仍然有点过分,有太多的边,但这是意料之中的。
手绘形状
但也许你会说,形状已经很好地显示出来了,所以你仍然对近似是否真的有用有些怀疑。因此,让我们来看一幅美丽的手绘艺术作品,它正是为了这个例子而准备的。
(def shapes2 (-> "resources/chapter03/hand_shapes.jpg" (imread IMREAD_REDUCED_COLOR_2))) Figure 3-75 shows the newly loaded shapes. Figure 3-75
一件艺术品
首先,让我们调用 find-contours 并画出由它们定义的形状。
Reusing the same draw-contours! function and drawing over the art itself gives Figure 3-76. Figure 3-76
艺术上的轮廓
现在这一次,让我们尝试一些不同的东西,使用核心 opencv 包中的函数 fill-convex-poly 。
这与绘制轮廓没有太大的不同,我们实际上只是在列表上循环,并在每个轮廓上使用填充凸多边形。
(def drawing (u/mat-from shapes2)) (set-to drawing rgb/white) (let[ contours (new-arraylist)] (find-contours thresh contours (new-mat) RETR_LIST CHAIN_APPROX_SIMPLE) (doseq [c contours] (fill-convex-poly drawing c rgb/blue-3- LINE_4 1))) And so, we get the four shapes turned to blue (Figure 3-77). Figure 3-77
一件艺术品变成了蓝色
正如我们所看到的,轮廓和形状被发现并且可以被画出来。
另一种绘制等高线的方法是使用函数折线。幸运的是,函数折线隐藏了轮廓每个元素上的循环,您可以直接将轮廓列表作为参数传入。
(set-to drawing rgb/white) (let[ contours (new-arraylist)] (find-contours thresh contours (new-mat) RETR_LIST CHAIN_APPROX_SIMPLE) (polylines drawing contours true rgb/magenta-2)) (-> drawing clone (u/resize-by 0.5) u/mat-view) And this time, we nicely get the wireframe only of the contours (Figure 3-78). Figure 3-78
艺术线框
好的,但是现在这些形状都有太多的点。
让我们再次使用创建的近似函数,并增强它,以便我们可以指定近似聚合 dp 使用的因子。
(defn approx_ ([c] (approx_ c 0.02)) ([c factor] (let[m2f (new-matofpoint2f (.toArray c)) len (arc-length m2f true) ret (new-matofpoint2f)] (approx-poly-dp m2f ret (* factor len) true) (new-matofpoint (.toArray ret)))))
更高的系数意味着我们在更大程度上强制减少点数。因此,为了达到这个效果,让我们将通常的值 0.02 增加到 0.03。
(set-to drawing rgb/white) (let[ contours (new-arraylist)] (find-contours thresh contours (new-mat) RETR_LIST CHAIN_APPROX_SIMPLE) (doseq [c contours] (fill-convex-poly drawing (approx_ c 0.03) (which-color c) LINE_AA 1))) The shapes have been greatly simplified, and the number of sides has quite diminished: the shapes are now easier to identify (Figure 3-79). Figure 3-79
具有简单形状的艺术
3.8 移动形状
问题
这是基于堆栈溢出时发现的一个问题。
stack overflow . com/questions/32590277/move-area-of-a-image-using-opencv
问题是“将图像区域移动到中心”,基本图片如图 3-80 所示。
The goal is to move the yellow shape and the black mark inside to the center of the mat. Figure 3-80
移动形状
解决办法
我非常喜欢这个食谱,因为它引入了许多 Origami 功能,共同致力于一个目标,这也是本章的主题。
The plan to achieve our goal is as follows:
-
首先,给原图加边框,看边界
-
切换到 HSV 颜色空间
-
通过只选择黄色范围内的颜色来创建蒙版
-
从前一个蒙版的边界矩形在原始图片中创建一个 submat
-
创建与原始大小相同的目标结果材质
-
在目标 mat 中创建一个 submat 来放置内容。该子矩阵必须具有相同的大小,并且它将位于中心。
-
将目标垫的其余部分设置为任意颜色…
-
我们完了!
让我们开始吧。
它是如何工作的
好的,所以第一步是突出垫子的边界,因为我们不能真正看到它延伸到哪里。
我们将开始加载图片,同时添加边框。
(def img (-> "resources/morph/cjy6M.jpg" (imread IMREAD_REDUCED_COLOR_2) (copy-make-border! 1 1 1 1 BORDER_CONSTANT (->scalar "#aabbcc")))) Bordered input with the rounded yellow mark is now shown in Figure 3-81. Figure 3-81
黄色标记和边框
然后,我们切换到 hsv 颜色空间,并在黄色标记上创建一个遮罩,这就是 Origami 流水线使一个接一个地传递函数变得容易得多的地方。
(def mask-on-yellow (-> img (clone) (cvt-color! COLOR_BGR2HSV) (in-range! (new-scalar 20 100 100) (new-scalar 30 255 255)))) Our yellow mask is ready (Figure 3-82). Figure 3-82
黄色标记上的遮罩
下一步是在新创建的遮罩中找到轮廓。请注意 RETR _ 外部的用法,这意味着我们只对外部轮廓感兴趣,因此黄色标记内的线将不包括在返回的轮廓列表中。
(def contours (new-arraylist)) (find-contours mask-on-yellow contours (new-mat) RETR_EXTERNAL CHAIN_APPROX_SIMPLE)
现在让我们创建一个项目 mat,原始图片的子 mat,其中定义它的矩形是由轮廓的边界矩形构成的。
(def background-color (->scalar "#000000")) ; mask type CV_8UC1 is important !! (def mask (new-mat (rows img) (cols img) CV_8UC1 background-color)) (def box (bounding-rect (first contours))) (def item (submat img box)) The item submat is shown in Figure 3-83. Figure 3-83
由轮廓的边界矩形组成的 Submat
我们现在创建一个全新的 mat,与 submat 项大小相同,并复制到分段项的内容中。背景颜色必须与结果 mat 的背景颜色相同。
(def segmented-item (new-mat (rows item) (cols item) CV_8UC3 background-color)) (copy-to item segmented-item (submat mask box) ) The newly computed segmented item is shown in Figure 3-84. Figure 3-84
分段项目
现在让我们找到将作为复制目标的 rect 的位置。我们希望该项目被移到中心,rect 应该与原来的小盒垫大小相同。
(def center (new-point (/ (.cols img ) 2 ) (/ (.rows img) 2))) (def center-box (new-rect (- (.-x center ) (/ (.-width box) 2)) (- (.-y center ) (/ (.-height box) 2)) (.-width box) (.-height box)))
好了,一切就绪;现在,我们创建结果 mat,并通过 submat 在前面计算的中心位置复制分段项的内容。
(def result (new-mat (rows img) (cols img) CV_8UC3 background-color)) (def final (submat result center-box)) (copy-to segmented-item final (new-mat))
仅此而已。
The yellow shape has been moved to the center of a new mat. We made sure the white color of the original mat was not copied over, by specifically using a black background for the final result mat (Figure 3-85). Figure 3-85
获胜
3.9 看树
问题
这是另一个基于堆栈溢出问题的方法。这一次的兴趣是集中在一个树木种植园,在数树木之前,能够在航拍照片中突出显示它们。
参考问题在这里:
解决办法
像往常一样,通过调用范围内的来识别树。但是结果,正如我们将看到的,仍然是相互关联的,这使得实际计数变得非常困难。
我们就来介绍一下**形态学的用法——ex!**来来回回地腐蚀所创建的掩模,从而形成更好的预处理垫,为计数做好准备。
它是如何工作的
We will use a picture of a hazy morning forest to work on (Figure 3-86). Figure 3-86
朦胧的树木
最终,你会想要数一数这些树,但是现在甚至很难用肉眼看到它们。(周围有机器人吗?)
让我们从在绿色的树木上创建一个遮罩开始。
(def in-range-pict (-> trees clone (in-range! (new-scalar 100 80 100) (new-scalar 120 255 255)) (bitwise-not!))) We get a mask of dots … as shown in Figure 3-87. Figure 3-87
白纸黑字
这个食谱的诀窍就在这里。我们将在图片范围内的 mat 上应用 MORPH_ERODE,然后是 MORPH_OPEN。这将有清理森林的效果,并给每棵树自己的空间。
变形是通过准备一个 mat 来传递一个由小椭圆创建的内核矩阵作为参数来完成的。
(def elem (get-structuring-element MORPH_ELLIPSE (new-size 3 3)))
如果在 elem 上调用 dump ,会发现它的内部表示。
[0 1 0] [1 1 1] [0 1 0]
然后我们使用这个内核矩阵,把它传递给 morpholy-ex!。
(morphology-ex! in-range-pict MORPH_ERODE elem (new-point -1 -1) 1) (morphology-ex! in-range-pict MORPH_OPEN elem) This has the desired effect of reducing the size of each tree dot, thus reducing the overlap between the trees (Figure 3-88). Figure 3-88
变形后树不重叠
最后,我们只需在原始垫子上应用简单的颜色来突出人眼看到的树的位置。(周围还是没有机器人?)
(def mask (-> in-range-pict clone (in-range! (new-scalar 0 255 255) (new-scalar 0 0 0)))) (def target (new-mat (.size trees) CV_8UC3)) (set-to target rgb/greenyellow) (copy-to original target mask)
这在视频流中实时进行是很棒的。
你也已经知道接下来等待你的是什么练习。通过快速调用查找轮廓来计算森林中的树木数量…
这当然是留给读者的自由练习!
3.10 检测模糊
问题
您有大量的图片要分类,并且您希望有一个自动化的过程来丢弃那些模糊的图片。
解决办法
该解决方案的灵感来自于 pyimagesearch 网站条目pyimagesearch . com/2015/09/07/blur-detection-with-opencv/,该条目本身指向了 Pech-Pacheco 等人的论文拉普拉斯的变体“硅藻在明场显微镜中的自动聚焦:比较研究”
它确实突出了将 OpenCV 和这里的 origami 快速转化为有用的东西的酷方法。
基本上,你需要对你的图像的单通道版本应用拉普拉斯过滤器。然后,计算结果与前面结果的偏差,并检查偏差是否低于给定的阈值。
滤镜本身应用了 filter-2-d!,而方差是用均值-标准差计算的。
它是如何工作的
用于滤波器的拉普拉斯矩阵/核将重点放在中心像素上,并减少对左/右上/下像素的强调。
这是我们将要使用的拉普拉斯核。
(def laplacian-kernel (u/matrix-to-mat [ [ 0 -1 0] [-1 4 -1] [ 0 -1 0] ]))
让我们用 filter-2d 来应用这个内核吧!,然后调用 mean-std-dev 来计算中值和偏差。
(filter-2-d! img -1 laplacian-kernel) (def std (new-matofdouble)) (def median (new-matofdouble)) (mean-std-dev img median std)
处理图片时,您可以使用 dump 查看平均值的结果,因为它们是矩阵。这显示在下面:
(dump median) ; [19.60282552083333] (dump std) ; [45.26957788759024]
最后,用于比较以检测模糊的值将是偏差的 2 次方。
(Math/pow (first (.get std 0 0)) 2)
然后我们将得到一个与 50 相比较的值。低于 50 表示图像模糊。大于 50 表示图像显示不模糊。
让我们创造一个模糊的图像?由前面所有步骤组成的函数:
(defn std-laplacian [img] (let [ std (new-matofdouble)] (filter-2-d! img -1 laplacian-kernel) (mean-std-dev img (new-matofdouble) std) (Math/pow (first (.get std 0 0)) 2))) (defn is-image-blurred?[img] (< (std-laplacian (clone img)) 50))
现在让我们把这个函数应用到一些图片上。
(-> "resources/chapter03/cat-bg-blurred.jpg" (imread IMREAD_REDUCED_GRAYSCALE_4) (is-image-blurred?)) And … our first test passes! The cat of Figure 3-89 indeed gives a deserved blurred result. Figure 3-89
模糊的猫
And what about one of the most beautiful cat on this planet? That worked too. The cat from Figure 3-90 is recognized as sharp! Figure 3-90
敏锐但困倦的猫
现在,也许是时候去整理你所有的海边夏日照片了…
但是,是的,当然,是的,同意,并不是所有模糊的图片都是垃圾。
3.11 制作照片拼版
问题
大约 20 年前,在一个项目实验室里,我看到了一张巨大的《星球大战》海报,由第一部电影《??:新的希望》的多个小场景组成。
这张海报很大,从稍远的地方看,它实际上是一张达斯·维德向卢克伸出手的照片。
海报给我留下了很好的印象,我一直想自己做一张。最近,我还知道这种创作的图片有一个名字:照片马赛克。
解决办法
这个概念比我最初想的要简单得多。基本上,最难的是下载图片。
你主要需要两个输入,一个最终的图片,和一组用作 subs 的图片。
这项工作包括计算每张图片的 RGB 通道的平均值,并从中创建一个索引。
第一步准备工作完成后,在要复制的图片上创建一个网格,然后对于网格中的每个单元格,计算两个平均值之间的范数:一个来自单元格,一个来自索引的每个文件。
最后,用具有最低平均值的索引中的图片替换大图片的子图片,这意味着该图片在视觉上更接近子图片。
让我们付诸行动吧!
它是如何工作的
第一步是编写一个函数来计算一个垫子颜色的平均值。为此,我们再次使用了 mean-std-dev ,因为我们只对这个练习的平均值感兴趣,所以这是函数返回的结果。
(defn mean-average-bgr [mat] (let [_mean (new-matofdouble)] (-> mat clone (median-blur! 3) (mean-std-dev _mean (new-matofdouble))) _mean))
让我们在任何图片上调用这个,看看会发生什么。
(-> "resources/chapter03/emilie1.jpg" (imread IMREAD_REDUCED_COLOR_8) get-averages-bgr-mat dump)
返回值如下所示。这些值是三个 RGB 通道的平均值。
[123.182] [127.38] [134.128]
让我们稍微回避一下,比较一下三个矩阵的范数:ex1、ex2 和 ex3。看它们的内容,你可以“感觉”到 ex1 和 ex2 比 ex1 和 ex3 更接近。
(def ex1 (u/matrix-to-mat [[0 1 2]])) (def ex2 (u/matrix-to-mat [[0 1 3]])) (def ex3 (u/matrix-to-mat [[0 1 7]])) (norm ex1 ex2) ; 1.0 (norm ex1 ex3) ; 5.0
计算矩阵之间距离的范数函数的输出结果证实了这一点。
这就是我们要用的。首先,我们创建所有可用文件的索引。该索引是通过将每个图像加载为 mat 并计算其均值-平均值-bgr 而创建的图。
(defn indexing [files for-size] (zipmap files (map #(-> % imread (resize! for-size) mean-average-bgr) files)))
该函数的输出是一个映射,其中每个元素是一组键,val 类似 filepath -> mean-average-bgr。
为了找到最接近的图像,现在我们有了一个索引,我们计算所考虑的 mat(或以后的 submat)的范数,以及我们的索引的所有可能的均值-bgr 矩阵。
然后我们进行排序,取尽可能低的值。这就是 find-closest 所做的。
(defn find-closest [ target indexed ] (let [mean-bgr-target (get-averages-bgr-mat target)] (first (sort-by val < (apply-to-vals indexed #(norm mean-bgr-target %))))))
apply-to-vals 是一个函数,它接受一个 hashmap 和一个函数,将一个函数应用于 map 中的所有值,其余的保持不变。
(defn apply-to-vals [m f] (into {} (for [[k v] m] [k (f v)])))
最难的部分完成了;让我们来看看照片拼图算法的实质。
tile 函数是创建输入图片的网格并检索子 mat 的函数,每个子 mat 对应网格的一个 tile。
然后,它逐个遍历所有子 mat,使用相同的函数计算子 mat 的平均颜色平均值,然后使用该平均值和之前创建的索引调用 find-closest 。
对 find-closest 的调用返回一个文件路径,我们从该路径加载一个 submat,然后替换目标图片中图块的 submat,只需用通常的 copy-to 复制加载的 mat。
在这里写的函数 tile 里看到这个。
(defn tile [org indexed ^long grid-x ^long grid-y] (let[ dst (u/mat-from org) width (/ (.cols dst) grid-x) height (/ (.rows dst) grid-y) total (* grid-x grid-y) cache (java.util.HashMap.) ] (doseq [^long i (range 0 grid-y)] (doseq [^long j (range 0 grid-x)] (let [ square (submat org (new-rect (* j width) (* i height) width height )) best (first (find-closest square indexed)) img (get-cache-image cache best width height) sub (submat dst (new-rect (* j width) (* i height) width height )) ] (copy-to img sub)))) dst))
主入口点是一个名为 photomosaic 的函数,它通过预先创建平均值的索引并将其传递给 tile 函数来调用 tile 算法。
(defn photomosaic [images-folder target-image output grid-x grid-y ] (let [files (collect-pictures images-folder) indexed (indexing (collect-pictures images-folder) (new-size grid-x grid-y)) target (imread target-image )] (tile target indexed grid-x grid-y))) Whoo-hoo. It’s all there. Creating the photomosaic is now as simple as calling the function of the same name with the proper parameters:
-
jpg 图像的文件夹
-
我们要镶嵌的图片
-
网格的大小
下面是一个简单的例子:
(def lechat (photomosaic "resources/cat_photos" "resources/chapter03/emilie5.jpg" 100 100)) And the first photomosaic ever of Marcel the cat is shown in Figure 3-91. Figure 3-91
一只熟睡的猫的马赛克
Another photomosaic input/output, this from Kenji’s cat, is in Figure 3-92. Figure 3-92
古贺圣猫
And, a romantic mosaic in Figure 3-93. Figure 3-93
福冈的猫
图片中使用的猫都包括在例子中,没有一只猫受到伤害,所以现在可能是你创造自己的令人敬畏的马赛克的时候了…享受吧!
四、实时视频
到目前为止,这本书一直专注于让读者快速处理图像和生成的图形艺术。您现在应该对介绍的方法非常有信心,并且您有很多想法的空间。
太好了!
我们可以继续扩展和解释 OpenCV 的其他方法,但我们将在第四章中做一些其他事情,因为我们切换到实时视频分析,将前几章学到的知识应用到视频流领域。
你可能会问:什么是实时视频分析,我为什么要这么做?OpenCV 使查看视频流和关注视频内容变得轻而易举。例如,现在有多少人在视频流上显示?这个视频里有猫吗?这是一场网球比赛吗,所有问题的根源,今天是晴天吗?
OpenCV 已经为您实现了许多这样的算法,更好的是,Origami 添加了一点甜蜜的糖,所以您可以直接开始,并以一种简单的方式将块放在一起。
在这一章中,我们将从第一个配方开始,它将向您展示为视频流做好准备需要多少东西。
然后,我们转移到更实质性的主题,像人脸识别,背景区分,寻找橙子,最重要的是,身体肥皂。
4.1 视频流入门
问题
你有图像处理的 Origami 装置;现在,你想知道视频处理的 Origami 设置。
解决办法
坏消息是没有额外的项目设置。所以,我们几乎已经可以关闭这个食谱了。
好消息是 Origami 提供了两个功能,但是在使用它们之前,我们将介绍底层处理是如何工作的。
首先,我们将从 origami opencv3.video 包中创建一个 videocapture 对象,并用它开始/停止一个流。
第二,既然我们认为这肯定更容易使用,我们将介绍为您做所有事情的函数: u/simple-cam-window 。
最后,我们将回顾 **u/cams-window,**它可以轻松地组合来自不同来源的多个流。
它是如何工作的
自己动手视频流
你可以跳过食谱中的这一小部分,但是知道幕后是什么实际上是非常有益的。
视频流操作的简单思想始于创建一个 opencv videocapture 对象来访问可用的视频设备。
然后,该对象可以返回一个 mat 对象,就像您到目前为止使用的所有 mat 对象一样。然后可以对 mat 对象进行操作,在最简单的情况下,在屏幕上的一个框架中显示 mat。
Origami 使用类似于 u/imshow 的东西来显示视频中的垫子,但是对于第一个例子,让我们简单地使用 u/imshow 来显示垫子。
这里,我们确实需要另一个名称空间: [opencv3.video :as v] ,但是稍后您将会看到这一步并不是必需的,只有在直接使用 opencv video 函数时,您才会需要额外的 video 名称空间。
让我们通过下面的代码示例来看看它是如何进行的。
首先,我们创建 videocapture 对象,它可以访问您的主机系统的所有网络摄像头。
然后我们打开 ID 为 0 的相机。这可能是您的环境中的默认设置,但是我们将在后面看到如何使用多个设备。
(def capture (v/new-videocapture)) (.open capture 0)
我们需要一个窗口来显示从设备记录的帧,毫无疑问,我们将创建一个名为 window 的绑定。该窗口将被设置为黑色背景。
(def window (u/show (new-mat 200 200 CV_8UC3 rgb/black)))
然后,我们创建一个缓冲区来接收视频数据,就像一个普通的 OpenCV mat 一样。
(def buffer (new-mat))
核心视频循环将使用捕获对象上的 read 函数将内容复制到缓冲区,然后使用 u/re-show 函数在窗口中显示缓冲区。
(dotimes [_ 100] (.read capture buffer) (u/re-show window buffer)) At this stage, you should see frames showing up in a window on your screen, as in Figure 4-1. Figure 4-1
我最喜欢的沐浴露
最后,当循环结束时,使用捕获对象上的释放功能释放网络摄像头。
(.release capture)
这也应该有关闭你的计算机的照相机 LED 的效果。在这个小练习的最后要考虑的一件事是…是的,这是一个标准的 mat 对象,它被用作显示循环中的缓冲区,所以,是的,你可以在显示它之前插入一些文本或颜色转换。
单功能网络摄像头
既然你已经理解了底层的网络摄像头处理是如何完成的,这里有另一个稍微短一点的方法让你得到同样的结果,使用 u/simple-cam-window 。
在这一小节中,我们想快速回顾一下如何获取流并使用该函数对其进行操作。
在其最简单的形式中,simple-cam-window 与作为参数的身份函数一起使用。如您所知,identity 接受一个元素并按原样返回它。
(u/simple-cam-window identity)
如果您连接了网络摄像头,这将启动相同的流视频,流的内容显示在一个框架中。
该函数采用单个参数,该参数是在垫子显示在框架内之前应用于垫子的函数。
太好了。我们将在几秒钟后回到它,但是现在,这里是你将发现的:简单地将记录帧转换成不同的颜色图,你可以只使用 apply-color-map 传递一个匿名函数!。
(u/simple-cam-window #(apply-color-map! % COLORMAP_HOT)) With the immediate result showing in Figure 4-2. Figure 4-2
热身体皂
在第二个版本的 u/simple-cam-window 中,您可以指定帧和视频录制的设置,所有这些都是作为第一个参数传递给 simple-cam-window 的简单映射。
例如:
(u/simple-cam-window {:frame {:color "#ffcc88", :title "video", :width 350, :height 300} :video {:device 0, :width 100, :height 120}} identity)
在映射中,视频键指定设备 ID、获取流的设备以及要记录的帧的大小。请注意,如果大小不符合设备的能力,设置将被忽略。
在同一个参数映射中,帧键可以指定参数,如前一章所见,包括背景颜色、标题和窗口大小。
好,太好了;一切都准备好了。让我们玩一会儿。
变换函数
identity 函数接受一个元素并按原样返回它。通过返回 opencv 框架记录的 mat,我们看到了 identity 在第一次使用 cam 时是如何工作的。
Now, say you would like to write a function that
-
拿个垫子
-
以 0.5 为因子调整垫子的大小
-
将颜色贴图更改为冬季
-
将当前日期添加为白色覆盖
以你目前所掌握的知识来看,这并不困难。我们在一个函数 **my-fn 里写一个小 Origami 流水线吧!**进行图像转换:
(defn my-fn![mat] (-> mat (u/resize-by 0.5) (apply-color-map! COLORMAP_WINTER) (put-text! (str (java.util.Date.)) (new-point 10 50) FONT_HERSHEY_PLAIN 1 rgb/white 1) ))
请注意,此处流水线返回转换后的 mat。现在让我们在一个静态图像上使用这个新创建的流水线。
(-> "resources/chapter03/ai5.jpg" imread my-fn! u/mat-view) And let’s enjoy a simple winter feline output (Figure 4-3). Figure 4-3
酷猫
然后,如果你在星巴克使用笔记本电脑摄像头,你可以使用新功能 my-fn!通过将它作为一个参数传递给 simple-cam-window。
(u/simple-cam-window my-fn!) Which would give you something like Figure 4-4. Figure 4-4
星巴克冰咖啡笔芯
来自同一输入源的两帧或更多帧
当试图从同一个源应用两个或多个函数时,这是一种方便的方法。这实际上只是使用克隆函数来避免与源缓冲区的内存冲突的问题。
这里,我们创建了一个函数,它将缓冲区作为输入,然后连接从同一个缓冲区创建的两个图像。左边的第一个图像将是流的黑白版本,而右边的图像将是缓冲区的翻转版本。
(u/simple-cam-window (fn [buffer] (vconcat! [ (-> buffer clone (cvt-color! COLOR_RGB2GRAY) (cvt-color! COLOR_GRAY2RGB)) (-> buffer clone (flip! -1)) ]))) Note that we use the clone twice for each side of the concatenation (Figure 4-5). Figure 4-5
灰色左,翻转右,但它仍然是身体肥皂
您可以通过任意多次克隆输入缓冲区来进一步推广这个方法;为了突出这一点,下面是另一个在同一个输入缓冲区上应用不同颜色映射三次的例子。
(u/simple-cam-window (fn [buffer] (hconcat! [ (-> buffer clone (apply-color-map! COLORMAP_JET)) (-> buffer clone (apply-color-map! COLORMAP_BONE)) (-> buffer clone (apply-color-map! COLORMAP_PARULA))]))) And the result is shown in Figure 4-6. Figure 4-6
黑玉,骨头和帕鲁拉,但这仍然是沐浴露
4.2 组合多个视频流
问题
您尝试从同一个缓冲区创建许多输出,但如果能够插入多个摄像机并将它们的缓冲区组合在一起,那就更好了。
解决办法
Origami 附带了一个 u/simple-cam-window 的兄弟函数,名为 u/cams-window,,这是一个增强版本,您可以合并来自相同或多个源的多个流。
它是如何工作的
u/cams-window 是一个获取设备列表的函数,每个设备从一个 ID 定义一个设备,并且通常是一个转换函数。
该函数还采用一个视频函数来连接两个或多个设备输出,最后采用一个帧元素来定义窗口的常用参数,如大小和标题。
(u/cams-window {:devices [ {:device 0 :width 300 :height 200 :fn identity} {:device 1 :width 300 :height 200 :fn identity}] :video { :fn #(hconcat! [ (-> %1 (resize! (new-size 300 200))) (-> %2 (resize! (new-size 300 200))) ])} :frame {:width 650 :height 250 :title "OneOfTheSame"}})
图 4-7 显示了两种针对同一种沐浴露的设备,但角度不同。
The left frame takes input from the device with ID 0, and the right frame input from the device with ID 1. Figure 4-7
更多人体香皂图片
请注意,即使为每个设备指定了大小,实际上仍然需要调整大小,因为设备可以使用非常特定的高度和宽度组合,因此使用不同的设备可能有点困难。
尽管如此,调整大小!调用合成视频功能并不觉得不合适,之后一切都很顺利。
4.3 扭曲视频
问题
这个方法是关于使用变换来扭曲视频流的缓冲区,但也是关于实时更新变换。
解决办法
扭曲转换本身将使用 opencv 的get-perspective-transform从核心名称空间完成。
实时更新将使用 Clojure atom 和软件事务内存来完成,在这里很适合更新进行转换所需的矩阵值,而显示循环正在读取该矩阵的内容,因此总是获得最新的值。
它是如何工作的
为了执行透视变换,我们需要一个扭曲矩阵。扭曲矩阵包含在一个原子中,并首先初始化为零。
(def mt (atom nil))
用于进行变换的扭曲矩阵可以从四个点创建,具有它们在变换之前和之后的位置。
我们将使用 reset 来更新 atom 值,而不是对本地绑定进行操作!。
(def points1 [[100 10] [200 100] [28 200] [389 390]]) (def points2 [[70 10] [200 140] [20 200] [389 390]]) (reset! mt (get-perspective-transform (u/matrix-to-matofpoint2f points1) (u/matrix-to-matofpoint2f points2)))
请记住,您仍然可以转储 warp matrix,它是一个常规的 3×3 mat,方法是对它使用一个解引用调用,使用@或 deref 。
(dump @mt)
利用前面定义的点,这给出了下面的双精度矩阵。
[1.789337561985906 0.3234215275201738 -94.5799621372129] [0.7803091692375479 1.293303360247406 -78.45137776386103] [0.002543030309135725 -3.045754676722361E-4 1]
现在让我们使用保存在 mt atom 中的矩阵创建一个扭曲垫子的函数。
(defn warp! [ buffer ] (-> buffer (warp-perspective! @mt (size buffer ))))
记住这个函数仍然可以应用于标准图像;例如,如果您想要扭曲猫,您可以编写以下 Origami 流水线:
(-> "resources/chapter03/ai5.jpg" imread (u/resize-by 0.7) warp! u/imshow) And the two cats from before would be warping as in Figure 4-8. Figure 4-8
扭曲的猫
现在让我们使用 **warp 将该功能应用于视频流!**作为 u/simple-cam 窗口的参数。
(u/simple-cam-window warp!)
身体皂已经翘了!(图 4-9
Obviously, the book is not doing too much to express the difference between a still cat image and the body soap stream , so you can plug in your own stream there. Figure 4-9
扭曲的身体肥皂
4.4 使用人脸识别
问题
虽然 OpenCV 人脸识别功能在静态图片上工作得非常好,但在视频流上工作就不同了,因为要寻找实时显示的移动人脸,以及计算人数等。
解决办法
第一步是加载一个分类器:opencv 对象,它将能够找出 mat 上的匹配元素。
使用 origami 函数 new-cascadeclassifier 从 xml 定义中加载分类器。
然后,使用该分类器和 mat 调用 detectMultiScale 将返回匹配 rect 对象的列表。
然后,这些 rect 对象可以用于用矩形突出显示找到的面,或者用于创建 submat。
它是如何工作的
完成这项工作不需要额外的 Clojure 名称空间,因为 new-cascadeclassifier 函数已经在核心名称空间中。
如果 xml 文件在文件系统上,那么可以用
(def detector (new-cascadeclassifier "resources/lbpcascade_frontalface.xml"))
如果 xml 作为资源存储在 jar 文件中,那么可以用
(def detector (new-cascadeclassifier (.getPath (clojure.java.io/resource "lbpcascade_frontalface.xml"))))
需要绘制由分类器找到的矩形对象。分类器的 detect 函数返回一个矩形列表,所以让我们编写一个函数,简单地遍历 rect 对象列表,并在每个 rect 上绘制一个蓝色的边框。
(defn draw-rects! [buffer rects] (doseq [rect (.toArray rects)] (rectangle buffer (new-point (.-x rect) (.-y rect)) (new-point (+ (.-width rect) (.-x rect)) (+ (.-height rect) (.-y rect))) rgb/blue 5)) buffer)
然后让我们定义第二个函数, find-faces! **,**调用分类器上的 detectMultiScale 方法并使用 draw-rects 绘制矩形!前面定义的函数。
(defn find-faces![buffer] (let [rects (new-matofrect)] (.detectMultiScale detector buffer rects) (-> buffer (draw-rects! rects) (u/resize-by 0.7))))
我们又把所有的积木都放在这里了,现在只需要简单地调用 find-faces 就可以了!通过u/简单凸轮窗口。
(u/simple-cam-window find-faces!) And if you find yourself in Starbucks one morning on a terrace, the image could be something like Figure 4-10. Figure 4-10
安静令人印象深刻的早晨咖啡脸
抽屉!函数实际上可以是任何东西,因为你可以访问一个缓冲对象。
比如这个第二版的 **draw-rects!**对找到的面的矩形创建的子贴图应用不同的颜色贴图。
(defn draw-rects! [buffer rects] (doseq [r (.toArray rects)] (-> buffer (submat r) (apply-color-map! COLORMAP_COOL) (copy-to (submat buffer r)))) (put-text! buffer (str (count (.toArray rects) ) ) (new-point 30 100) FONT_HERSHEY_PLAIN 5 rgb/magenta-2 2)) And reusing the created building blocks, this gives the cool face from Figure 4-11. Figure 4-11
凉爽的早晨咖啡脸
这最后一个画人脸的例子采用第一个找到的人脸,在视频流的右边画一个大特写。
(defn draw-rects! [buffer rects] (if (> (count (.toArray rects)) 0) (let [r (first (.toArray rects)) s (-> buffer clone (submat r) (resize! (.size buffer)))] (hconcat! [buffer s])) buffer)) Obviously, Figure 4-12 will quickly get you convinced that this should really only be used for house BBQs, in order to show everyone who has been eating all the meat. Figure 4-12
同一视频窗口上的概览和特写
4.5 与基础图像的差异
问题
您想要获取一个 mat 映像,将其定义为一个基础映像,并发现对该基础映像所做的更改。
解决办法
这是一个非常短的配方,但它本身对理解后面更复杂的运动配方很有帮助。
为了创建一个图像和它的基底的 diff,我们在这里首先创建两段视频回调代码:一个将背景图片存储在 Clojure 原子中,另一个将使用该基底原子进行 diff。
然后,结果的灰色版本将通过简单的阈值函数,以准备用于附加形状识别和/或进一步处理的结果。
它是如何工作的
为了计算一个图像与另一个图像的差异,你需要两个垫子:一个用于基底,另一个是更新版本,其中有(我们希望)新的额外形状。
我们首先定义 Clojure 原子,并启动一个视频流来创建一个引用背景图像的原子。
只要 cam-window 在运行,来自视频流的最新缓冲垫就会存储在 atom 中。
(def base-image (atom nil)) (u/simple-cam-window (fn [buffer] (swap! base-image (fn[_] buffer) ) ))
一旦你对背景满意了,你可以停止 cam 窗口,用 imshow 和 atom 的一个 deref 版本检查当前存储的图片背景。
(u/imshow @base-image) This time, the image is a typical one of a busy home workplace (Figure 4-13). Figure 4-13
心形和扬声器
现在,下一步是定义一个新的流回调来使用 simple-cam-window,它将与 Clojure 原子中存储的 mat 不同。
diff 是通过 opencv 函数 absdiff 完成的,它需要三个 mat,即 diff 的两个输入和输出。
(defn diff-with-bg [buffer] (let[ output (new-mat)] (absdiff buffer @base-image output) output)) (u/simple-cam-window diff-with-bg)
显然,在开始第二个流和引入新的形状之前,您应该停止第一个记录流。
This would give something like Figure 4-14, where the added body soap is clearly being recognized. Figure 4-14
办公室里的沐浴露!
现在通常,下一步是清理显示在背景顶部的形状,方法是将差异贴图变为灰色,并在模糊后应用非常高的阈值。
; diff in gray (defn diff-in-gray [buffer] (-> buffer clone (cvt-color! COLOR_RGB2GRAY) (median-blur! 7) (threshold! 10 255 1)))
对于同一个缓冲区,我们有两个处理函数,在 Clojure 中,将它们与 comp 结合起来实际上是相当容易的,所以现在让我们尝试一下。
记住 comp 从右到左组合函数,意味着第一个被应用的函数是最右边的一个。
(u/simple-cam-window (comp diff-in-gray diff-with-bg )) See the composition result and the shape of the body soap showing in Figure 4-15. Figure 4-15
添加的形状有助于更多处理
在这里,您可以编译所有的步骤,从前面添加的 shape mat 创建一个简单的遮罩,并使用该遮罩来突出显示不同的部分。
所有这些都不奇怪,除了按位非!来电,总结在亮点——新!功能。
(defn highlight-new! [buffer] (let [output (u/mat-from buffer) w (-> buffer clone diff-with-bg (cvt-color! COLOR_RGB2GRAY) (median-blur! 7) (threshold! 10 255 1) (bitwise-not!))] (set-to output rgb/black) (copy-to buffer output w) output)) And the body soap output shows in Figure 4-16. Figure 4-16
回到沐浴露
这些溪流是在凌晨 3 点左右的严重时差中拍摄的,因此光线条件会在身体肥皂的底部产生一点噪音,但你可以通过更新遮罩以不包括桌子木材的颜色来尝试消除这种噪音。轮到你了!
4.6 寻找运动
问题
您希望识别并突出显示视频流中的移动和移动形状。
解决办法
在清理缓冲区之后,我们从累加缓冲区的浮点值开始。这是通过函数累加加权完成的。
然后,我们在缓冲区的灰色版本和计算的平均 mat 之间做一个 diff,我们检索一个 delta 的 mask mat,就像在前面的配方中快速呈现的那样。
最后,我们在 delta 上应用一个阈值,用一点膨胀清理结果,并将 mat 转换回彩色模式以显示在屏幕上。
这其实比听起来容易!
它是如何工作的
在这里,我们想在垫子上展示运动产生的三角洲。
在黑白中寻找运动
第一步是取一个缓冲区,创建一个干净的(通过模糊)灰色版本。
我们对展示这张垫子不感兴趣,而只是对它进行算术运算;我们将 mat 转换为 32 位浮点 mat,或者用 opencv 语言 CV_32F 。
(defn gray-clean! [buffer] (-> buffer clone (cvt-color! COLOR_BGR2GRAY) (gaussian-blur! (new-size 3 3) 0) (convert-to! CV_32F)))
此功能将用于准备灰色版本的垫子。现在让我们来计算累计平均值以及平均值和最近缓冲区之间的差值。
我们将创建另一个函数, find-movement ,它将以黑白方式突出显示图片中最近的移动。
该函数将获得一个 Clojure 原子, avg ,作为跟踪视频传入 mat 对象平均值的参数。第二个参数是传递给回调的常用缓冲垫。该功能将显示帧增量。
在第一个 if 开关中,我们确保存储在 atom 中的 average mat 用来自传入流的适当值进行初始化。
然后使用 absdiff 计算 diff,我们在其上应用一个短的 threshold-explain-CVT-color 流水线来直接显示运动。
(defn find-movement [ avg buffer] (let [gray (gray-clean! buffer) frame-delta (new-mat)] (if (nil? @avg) (reset! avg gray)) ; compute the absolute diff on the weighted average (accumulate-weighted gray @avg 0.05 (new-mat)) (absdiff gray @avg frame-delta) ; apply threshold and convert back to RGB for display (-> frame-delta (threshold! 35 255 THRESH_BINARY) (dilate! (new-mat)) (cvt-color! COLOR_GRAY2RGB) (u/resize-by 0.8))))
我们最后定义一个函数,用一个内嵌的 Clojure 原子包装 find-movement 函数。该原子将包含 mat 对象的平均值。
(def find-movements! (partial find-movement (atom nil)))
是时候用 u/simple-cam-window 来实现这些功能了。
(u/simple-cam-window find-movements!) This is shown in Figure 4-17. Figure 4-17
检测到移动!
我们想在这里显示运动,但因为打印所需的黑色墨水量会吓到出版商,所以让我们添加一个按位运算来进行黑白转换,看看实时进展如何。
让我们用一个按位非来更新查找移动函数!呼叫框架-三角垫。在此之前,我们需要使用 opencv 的 convert-to 将矩阵转换回我们可以处理的内容。函数,类型 target CV_8UC3,这是我们通常使用的。
(defn find-movement [ avg buffer] (let [gray (gray-clean! buffer) frame-delta (new-mat)] ... (-> frame-delta (threshold! 35 255 THRESH_BINARY) (dilate! (new-mat)) (convert-to! CV_8UC3) (bitwise-not!) (cvt-color! COLOR_GRAY2RGB) (u/resize-by 0.8)))) Good; let’s call simple-cam again. Wow. Figure 4-18 now looks a bit scary. Figure 4-18
可怕的黑白运动
And if you stop getting agitated in front of your computer, the movement highlights are stabilizing and slowly moving to a fully white mat, as shown in the progression of Figure 4-19. Figure 4-19
稳定运动
查找并绘制轮廓
在这个阶段,很容易找到并绘制轮廓来突出显示原始彩色缓冲区上的运动。
让我们来看看你做的漂亮的运动垫的轮廓。
在查找移动函数中增加了几行,特别是在 delta mat 上查找轮廓和在 color mat 上绘图。
在前一章中,您已经看到了所有这些寻找轮廓的动作,所以让我们来看看更新后的代码。
(defn find-movement [ avg buffer] (let [ gray (base-gray! buffer) frame-delta (new-mat) contours (new-arraylist)] (if (nil? @avg) (reset! avg gray)) (accumulate-weighted gray @avg 0.05 (new-mat)) (absdiff gray @avg frame-delta) (-> frame-delta (threshold! 35 255 THRESH_BINARY) (dilate! (new-mat)) (convert-to! CV_8UC3) (find-contours contours (new-mat) RETR_EXTERNAL CHAIN_APPROX_SIMPLE)) (-> frame-delta (bitwise-not!) (cvt-color! COLOR_GRAY2RGB) (u/resize-by 0.8)) (-> buffer ; (u/draw-contours-with-rect! contours ) (u/draw-contours-with-line! contours) (u/resize-by 0.8)) (hconcat! [frame-delta buffer]) )) Calling this new version of the find-movement function gives something like Figure 4-20, but you can probably be way more creative from there. Figure 4-20
用蓝色突出显示移动部件
4.7 使用 Grabcut 将前景与背景分离
问题
Grabcut 是另一种 opencv 方法,可用于将图像的前景与背景分开。但它能像在视频流上一样实时使用吗?
解决办法
确实有一个抓取-剪切功能,可以轻松地将正面与背景分开。这个函数只需要一点点的理解就可以看到它运行所需要的不同的遮罩,所以我们将首先关注于理解静止图像上的事情是如何工作的。
然后,我们将继续讨论实时流解决方案。这将很快导致速度问题,因为抓取-剪切比实时处理花费更多的时间。
因此,我们将使用一个小技巧,通过调低工作区的分辨率来将 grab-cut 所用的时间降到最低;然后,我们将在执行其余处理时使用全分辨率,产生一个 grabcut。
它是如何工作的
在静止图像上
这里我们想调用 grabcut,将一个深度的图层从其他图层中分离出来。
grabcut 的想法是准备在输入图片上使用矩形或遮罩,并将其传递给 grabcut 函数。
存储在单个输出掩膜中的结果将包含一组 1.0、2.0 或 3.0 标量值,具体取决于 grabcut 认为哪些是每个不同图层的一部分。
然后我们使用 opencv 比较这个遮罩和我们想要检索的层的标量值的另一个固定的 1×1 遮罩上的。我们只为感兴趣的层获得一个掩模。
最后,我们使用步骤 2 中创建的蒙版在输出垫上复制原始图像。
准备好了吗?让我们举一个猫的例子。
首先,我们加载一个我们非常喜欢的猫图片,并将其转换为一个合适的工作大小的 mat 对象。
(def source "resources/chapter03/ai6.jpg") (def img (-> source imread (u/resize-by 0.5))) The loaded cat picture is shown in Figure 4-21. Figure 4-21
给你一个猫吻
然后,我们定义一个 mask mat,它将接收 grabcut 调用的输出,即关于图层信息的每像素信息。
我们还为感兴趣的区域(ROI)定义了一个矩形,我们希望在这个矩形中执行 grabcut,这里几乎是完整的图片,大部分只是删除了边界。
(def mask (new-mat)) (def rect (new-rect (new-point 10 10) (new-size (- (.width img) 30) (- (.height img) 30 ))))
现在我们有了 grabcut 所需的所有输入,让我们用掩码、ROI 和 grabcut 初始化参数来调用它,这里是 GC_INIT_WITH_RECT。另一个可用的方法是使用 GC_INIT_WITH_MASK ,正如您可能已经猜到的,它是用 MASK 而不是 rect 初始化的。
(grab-cut img mask rect (new-mat) (new-mat) 11 GC_INIT_WITH_RECT)
Grabcut 已被调用。为了了解输出中检索到的内容,让我们快速查看一下 mask 的一个小 submat 上的矩阵内容。
(dump (submat mask (new-rect (new-point 10 10) (new-size 5 5))))
如果你亲自尝试,你会看到这样的价值观
[2 2 2 2 2] [2 2 2 2 2] [2 2 2 2 2] [2 2 2 2 2] [2 2 2 2 2]
mat 中其他地方的另一个 submat 转储给出了不同的结果:
(dump (submat mask (new-rect (new-point 150 150) (new-size 5 5))))
反过来,这给了
[3 3 3 3 3] [3 3 3 3 3] [3 3 3 3 3] [3 3 3 3 3] [3 3 3 3 3]
我们可以从这个不同的矩阵中猜测出层是不同的。
这里的想法是检索由所有相同值组成的遮罩,所以现在让我们从第 3 层中包含的所有像素创建一个遮罩,这意味着它们由 3.0 值组成。
我们称之为 fg-mask,前景遮罩。
(def fg-mask (clone mask)) (def source1 (new-mat 1 1 CV_8U (new-scalar 3.0))) (compare mask source1 fg-mask CMP_EQ) (u/mat-view fg-mask) The cat foreground mask is shown in Figure 4-22. Figure 4-22
前景遮罩
然后,我们可以使用复制到原始输入图像,并在与输入相同大小的新黑色垫上使用 fg-mask。
(def fg_foreground (-> img (u/mat-from) (set-to rgb/black))) (copy-to img fg_foreground fg-mask) (u/mat-view fg_foreground) And we get the mat of Figure 4-23. Figure 4-23
只有猫吻的前景
请注意,我们是如何得到两只小猫相互交配的近似结果的,但总的来说,结果是相当有效的。
在继续之前,让我们通过聚焦在标量值为 2.0 的层上来快速检索互补蒙版,即背景蒙版。
首先,我们再次创建一个掩码来接收输出,这次是 bg-mask 。
(def bg-mask (clone mask)) (def source2 (new-mat 1 1 CV_8U (new-scalar 2.0))) (compare mask source2 bg-mask CMP_EQ) (u/mat-view bg-mask) The result for the background mask is shown in Figure 4-24. Figure 4-24
背景遮罩
然后,简单地复制一个类似的前景。
(def bg_foreground (-> img (u/mat-from) (set-to (new-scalar 0 0 0)))) (copy-to img bg_foreground bg-mask) (u/mat-view bg_foreground) And the result is shown in Figure 4-25. Figure 4-25
背景层的 Mat
既然你已经看到了如何分离静止图像上的不同层,让我们继续视频流。
在视频流上
您可能已经注意到,前面示例中的 grabcut 步骤非常慢,主要是因为为了实现不同层的清晰分离而进行了大量繁重的计算。但是有多糟糕呢?
让我们快速尝试一下第一个哑版本的实时 grabcut。
我们将调用这个函数在前面- 慢,基本上只是在一个函数中编译我们刚刚在静态例子中看到的步骤。
(defn in-front-slow [buffer] (let [ img (clone buffer) rect (new-rect (new-point 5 5) (new-size (- (.width buffer) 5) (- (.height buffer) 5 ))) mask (new-mat) pfg-mask (new-mat) source1 (new-mat 1 1 CV_8U (new-scalar 3.0)) pfg_foreground (-> buffer (u/mat-from) (set-to rgb/black))] (grab-cut img mask rect (new-mat) (new-mat) 7 GC_INIT_WITH_RECT) (compare mask source1 pfg-mask CMP_EQ) (copy-to buffer pfg_foreground pfg-mask) pfg_foreground))
然后,让我们使用这个函数作为对我们现在熟悉的 u/simple-cam-window 的回调。
(u/simple-cam-window in-front-slow) This slowly gives the output seen in Figure 4-26. Figure 4-26
慢点,慢点,慢点
您很快就会意识到,这不像在视频流中那样非常有用。
这里的技巧实际上是降低输入缓冲区的分辨率,在分辨率较低的 mat 上进行 grabcut,并获得 grabcut 掩码。然后,使用全尺寸图片和从 grabcut 获取的低分辨率蒙版进行复制。
这一次,我们将创建一个前置函数,这将是前面的一个略微更新的版本,但是现在包括一个围绕 grabcut 调用的 pyr-down-pyr-up 舞蹈(图 4-27 )。
为了使这更容易,我们将设置舞蹈的迭代次数作为回调的参数。
(defn in-front [resolution-factor buffer] (let [ img (clone buffer) rect (new-rect (new-point 5 5) (new-size (- (.width buffer) 5) (- (.height buffer) 5 ))) mask (new-mat) pfg-mask (new-mat) source1 (new-mat 1 1 CV_8U (new-scalar 3.0)) pfg_foreground (-> buffer (u/mat-from) (set-to (new-scalar 0 0 0)))] (dotimes [_ resolution-factor] (pyr-down! img)) (grab-cut img mask rect (new-mat) (new-mat) 7 GC_INIT_WITH_RECT) (dotimes [_ resolution-factor] (pyr-up! mask)) (compare mask source1 pfg-mask CMP_EQ) (copy-to buffer pfg_foreground pfg-mask) pfg_foreground))
然后,用这个新的回调函数调用 simple-cam-window。
(u/simple-cam-window (partial in-front 2))
仅仅通过阅读是很难获得速度感的,所以一定要在本地尝试一下。
Usually, a factor of 2 for the resolution-down dance is enough, but it depends on both your video hardware and the speed of the underlying processor. Figure 4-27
想多快就多快,宝贝
4.8 实时寻找橙子
问题
你想检测和跟踪视频流中的桔子。它也可以是一个柠檬,但是作者用完了柠檬,所以我们将使用一个橙子。
解决办法
在这里,我们将使用你以前见过的技术,像霍夫圆或寻找轮廓,并将它们应用于实时流。我们将在实时流上绘制移动的橙子的形状。
对于这两种解决方案,您可能还记得缓冲区需要一些小的预处理来检测橙色。这里,为了简单起见,我们将在 hsv 颜色空间中进行简单的范围内处理。
它是如何工作的
使用霍夫圆
首先,我们将通过拍摄橙子的一张照片来集中寻找合适的 hsv 范围。
First, let’s put the orange on the table (Figure 4-28). Figure 4-28
餐桌上的橘子,法国安纳西
我们首先切换到 hsv 颜色空间,然后应用范围内函数,最后将找到的橙色形状放大一点,以便更容易进行霍夫圆调用。
在 Origami 艺术中,这给了
(def hsv (-> img clone (cvt-color! COLOR_RGB2HSV))) (def thresh-image (new-mat)) (in-range hsv (new-scalar 70 100 100) (new-scalar 103 255 255) thresh-image) (dotimes [_ 1] (dilate! thresh-image (new-mat)))
现在,你会记得如何从第三章开始做霍夫循环,所以这里不需要花太多时间。这一部分最重要的是要有合适的半径范围,这里我们用 10-50 像素的直径来识别橙色。
(def circles (new-mat)) (def minRadius 10) (def maxRadius 50) (hough-circles thresh-image circles CV_HOUGH_GRADIENT 1 minRadius 120 15 minRadius maxRadius)
在这个阶段,你应该只有一个橙色的匹配圆。这一步非常重要,直到找到一个圆。
作为检查,打印圆形垫应得到 1×1 垫,如下所示:
#object[org.opencv.core.Mat 0x3547aa31 Mat [ 11CV_32FC3, isCont=true, isSubmat=false, nativeObj=0x7ff097ca7460, dataAddr=0x7ff097c4b980 ]]
一旦你把垫子钉好了,让我们在原图上画一个粉红色的圆圈(图 4-29 )。
(def output (clone img)) (dotimes [i (.cols circles)] (let [ circle (.get circles 0 i) x (nth circle 0) y (nth circle 1) r (nth circle 2) p (new-point x y)] (opencv3.core/circle output p (int r) color/ color/magenta- 3))) Figure 4-29
橙色和洋红色
所有的东西都在那里,所以让我们把我们的发现作为一个单一的函数来处理来自视频流的缓冲区;我们称这个函数为 my-orange!,是前面步骤的回顾。
(defn my-orange! [img] (u/resize-by img 0.5) (let [ hsv (-> img clone (cvt-color! COLOR_RGB2HSV)) thresh-image (new-mat) circles (new-mat) minRadius 10 maxRadius 50 output (clone img)] (in-range hsv (new-scalar 70 100 100) (new-scalar 103 255 255) thresh-image) (dotimes [_ 1] (dilate! thresh-image (new-mat))) (hough-circles thresh-image circles CV_HOUGH_GRADIENT 1 minRadius 120 15 minRadius maxRadius) (dotimes [i (.cols circles)] (let [ circle (.get circles 0 0) x (nth circle 0) y (nth circle 1) r (nth circle 2) p (new-point x y)] (opencv3.core/circle output p (int r) color/magenta- 3))) output))
现在只需再次将回调函数传递给简单的 cam 窗口。
(u/simple-cam-window my-orange!) Figures 4-30 and 4-31 show how the orange is found properly, even in low-light conditions. Winter in the French Alps after a storm did indeed make the evening light, and everything under it, a bit orange. Figure 4-30
打印机上的橙色
Figure 4-31
梅和橘子
使用查找轮廓
不是寻找一个完美的圆,你可能会寻找一个稍微扭曲的形状,这是当使用查找轮廓实际上比霍夫圆给出更好的结果。
这里,我们结合几分钟前发现的相同 hsv 范围来选择橙色,并应用第三章中的轮廓查找技术。
寻找我的橘子!回调带回了熟悉的查找轮廓和绘制轮廓函数调用。请注意,只有当找到的形状比预期的最小橙子大时,我们才绘制它们的轮廓。
(defn find-my-orange! [img ] (let[ hsv (-> img clone (cvt-color! COLOR_RGB2HSV)) thresh-image (new-mat) contours (new-arraylist) output (clone img)] (in-range hsv (new-scalar 70 100 100) (new-scalar 103 255 255) thresh-image) (find-contours thresh-image contours (new-mat) ; mask RETR_LIST CHAIN_APPROX_SIMPLE) (dotimes [ci (.size contours)] (if (> (contour-area (.get contours ci)) 100 ) (draw-contours output contours ci color/pink-1 FILLED))) output)) Giving this callback to simple-cam-window shows Mei playing around with a pink-colored orange in Figure 4-32. Figure 4-32
梅和粉橙,在附近的剧院演出
4.9 在视频流中查找图像
问题
您希望在流中找到图像的精确副本。
解决办法
OpenCV 附带了您可以使用的特性检测功能。不幸的是,这些特性大多是面向 Java 的。
这个菜谱将展示如何在 Java 和 Origami 之间架起桥梁,以及使用 Clojure 如何通过减少样板代码有所帮助。
Here we will use three main OpenCV objects :
-
特征检测器,
-
DescriptorExtractor,
-
描述符匹配器。
特征提取通过使用特征检测器找到输入图片和待发现图像的关键点来工作。然后,使用描述符提取器从两组点中的每一组计算描述符。
一旦有了描述符,就可以将这些描述符作为输入传递给描述符匹配器,描述符匹配器给出一组匹配结果,每个匹配都通过一个距离属性给出一个分数。
然后,我们最终可以筛选出最相关的点,并将它们画在流上。
代码清单比通常要长一点,但是让我们把最后一个方法也用在您的机器上吧!
它是如何工作的
在这个例子中,我们将在静态图像和实时图像中寻找我最喜欢的沐浴露,桉树香水。
Figure 4-33 shows the concerned body soap. Figure 4-33
小马赛人
静止图像
The first test is to be able to find the body soap in a simple still picture, like the one in Figure 4-34. Figure 4-34
卡门,我的沐浴露在哪里?
首先,我们还需要几个 Java 对象导入,即检测器和提取器,我们将在进行任何处理之前直接初始化它们。
(ns wandering-moss (:require [opencv3.core :refer :all] [opencv3.utils :as u]) (:import [org.opencv.features2d Features2d DescriptorExtractor DescriptorMatcher FeatureDetector])) (def detector (FeatureDetector/create FeatureDetector/AKAZE)) (def extractor (DescriptorExtractor/create DescriptorExtractor/AKAZE))
基本设置完成;然后,我们通过一个短的 Origami 流水线加载人体肥皂背景,并要求检测器检测其上的点。
(def original (-> "resources/chapter04/bodysoap_bg.png" imread (u/resize-by 0.3))) (def mat1 (clone original)) (def points1 (new-matofkeypoint)) (.detect detector mat1 points1)
接下来的步骤并不是必须的,但是画出找到的关键点可以让我们知道匹配者认为重要的点在哪里。
(def show-keypoints1 (new-mat)) (Features2d/drawKeypoints mat1 points1 show-keypoints1 (new-scalar 255 0 0) 0) (u/mat-view show-keypoints1) This gives a bunch of blue circles, as shown in Figure 4-35. Figure 4-35
沐浴露背景要点
当然,在检索关键点之前清理和移除缺陷可能是有用的,但是让我们检查一下匹配是如何在 raw mat 上工作的。
注意点的强度在身体肥皂本身上已经相当强了。
我们现在对仅含沐浴露的垫子重复同样的步骤。
(def mat2 (-> "resources/chapter04/bodysoap.png" imread (u/resize-by 0.3))) (def points2 (new-matofkeypoint)) (.detect detector mat2 points2)
同样,这个绘图点部分不是必需的,但它有助于更好地理解正在发生的事情。
(def show-keypoints2 (new-mat)) (Features2d/drawKeypoints mat2 points2 show-keypoints2 (new-scalar 255 0 0) 0) (u/mat-view show-keypoints2) The detector result is in Figure 4-36, and again, the keypoints look to be focused on the label of the body soap. Figure 4-36
香皂的检测结果
下一步是提取两个特征集,然后用于匹配器。
这只是用上一步中找到的点集在提取器上调用 compute 的问题。
(def desc1 (new-mat)) (.compute extractor mat1 points1 desc1) (def desc2 (new-mat)) (.compute extractor mat2 points2 desc2)
现在,进入匹配步骤。我们通过 DescriptorMatcher 创建一个匹配器,并给它一个找出匹配的方法。
在这种情况下,强力总是寻找解决方案的推荐方法。尝试每一种解决方案,看看是否有匹配的。
(def matcher (DescriptorMatcher/create DescriptorMatcher/BRUTEFORCE_HAMMINGLUT)) (def matches (new-matofdmatch)) (.match matcher desc1 desc2 matches)
正如解决方案摘要中所述,每个匹配都是通过其距离值来评定的。
如果打印出来,每个匹配看起来如下所示:
#object[org.opencv.core.DMatch 0x38dedaa8 "DMatch [queryIdx=0, trainIdx=82, imgIdx=0, distance=136.0]"]
对于距离值,匹配本身的分数通常显示为 0 到 300 之间的值。
所以现在,让我们创建一个快速的 Clojure 函数来排序和过滤好的匹配。这可以简单地通过过滤它们的距离属性来实现。我们将过滤低于 50 的匹配。根据录制的质量,您可以根据需要减少或增加该值。
(defn best-n-dmatches2[dmatches] (new-matofdmatch (into-array org.opencv.core.DMatch (filter #(< (.-distance %) 50) (.toArray dmatches)))))
draw-matches 方法是一场编码噩梦,但它可以被看作是 OpenCV 中噩梦般的 drawMatches Java 方法的包装。
我们通常使用 Java interop 和对每个参数的一些清理来传递参数。我们还创建了更大的输出 mat,这样我们可以将背景图片和人体肥皂放在同一个 mat 上。
(defn draw-matches [_mat1 _points1 _mat2 _points2 _matches] (let[ output (new-mat (* 2 (.rows _mat1)) (* 2 (.cols _mat1)) (.type _mat1)) _sorted-matches (best-n-dmatches2 _matches)] (Features2d/drawMatches _mat1 _points1 _mat2 _points2 _sorted-matches output (new-scalar 255 0 0) (new-scalar 0 0 255) (new-matofbyte) Features2d/NOT_DRAW_SINGLE_POINTS) output))
现在,有了所有这些,我们可以使用前面的函数绘制匹配器找到的匹配。
我们将第一个和第二个垫子,以及它们各自找到的关键点和匹配集传递给它。
(u/mat-view (draw-matches mat1 points1 mat2 points2 matches)) This, surprisingly after all the obscure coding, works very well, as shown in Figure 4-37. Figure 4-37
绘画比赛
视频流
与你刚刚经历的相比,视频流版本会让你感觉像呼吸了一口新鲜空气。
我们将创造一个我的身体在哪里的肥皂!函数将重用前面定义的匹配器,并在缓冲垫上的流回调中运行检测器、提取器和匹配。
之前定义的 draw-matches 函数也被重用来绘制实时流上的匹配。
(defn where-is-my-body-soap! [buffer] (let[ mat1 (clone buffer) points1 (new-matofkeypoint) desc1 (new-mat) matches (new-matofdmatch)] (.detect detector mat1 points1) (.compute extractor mat1 points1 desc1) (.match matcher desc1 desc2 matches) (draw-matches mat1 points1 mat2 points2 matches)))
你可以用这个回调函数来调用 simple-cam-window,但是……啊!似乎梅在配方特征检测可以运行之前已经找到了沐浴露!
Figure 4-38 shows both on the video stream. Figure 4-38
谢谢你找到沐浴露,梅!
这使得这份食谱,这一章,这本书的卑微的结束。我们真的希望这给了你很多想法,让你通过玩 Origami 框架来尝试,并为你的创作带来光明。
For now, “Hello Goodnight”:
搜索天空
我发誓我看到影子 坠落
可能是幻觉
一声暗藏的警告
名声将永远让我欲罢不能
想要
好了,没事了
我一直很好
你好你好晚安
妈妈枪“你好晚安”