六、形态图像处理
在本章中,我们将讨论数学形态学和形态学图像处理。形态图像处理是与图像中特征的形状或形态相关的非线性操作的集合。这些操作特别适合于二值图像的处理(其中像素表示为 0 或 1,并且根据惯例,对象的前景=1 或白色,背景=0 或黑色),尽管它可以扩展到灰度图像。
在形态学运算中,使用结构元素(小模板图像)探测输入图像。该算法的工作原理是将结构元素定位在输入图像中所有可能的位置,并将其与输入图像进行比较。。。
scikit 图像形态学模块
在本节中,我们将演示如何使用 scikit image 的形态学模块中的函数来实现一些形态学操作,首先对二值图像进行形态学操作,然后对灰度图像进行形态学操作。
二进制运算
让我们从二值图像的形态学操作开始。在调用函数之前,我们需要创建一个二进制输入图像(例如,使用具有固定阈值的简单阈值)。
腐蚀
侵蚀是一种基本的形态学操作,可缩小前景对象的大小,平滑对象边界,并移除半岛、手指和小对象。以下代码块显示了如何使用binary_erosion()
函数计算二值图像的快速二值形态学侵蚀:
from skimage.io import imread
from skimage.color import rgb2gray
import matplotlib.pylab as pylab
from skimage.morphology import binary_erosion, rectangle
def plot_image(image, title=''):
pylab.title(title, size=20), pylab.imshow(image)
pylab.axis('off') # comment this line if you want axis ticks
im = rgb2gray(imread('../images/clock2.jpg'))
im[im <= 0.5] = 0 # create binary image with fixed threshold 0.5
im[im > 0.5] = 1
pylab.gray()
pylab.figure(figsize=(20,10))
pylab.subplot(1,3,1), plot_image(im, 'original')
im1 = binary_erosion(im, rectangle(1,5))
pylab.subplot(1,3,2), plot_image(im1, 'erosion with rectangle size (1,5)')
im1 = binary_erosion(im, rectangle(1,15))
pylab.subplot(1,3,3), plot_image(im1, 'erosion with rectangle size (1,15)')
pylab.show()
下面的屏幕截图显示了前面代码的输出。可以看出,将结构元素作为一个薄的、小的、垂直的矩形使用时,首先会删除二进制时钟图像中的小刻度。接下来,一个更高的垂直矩形也被用来腐蚀时钟指针:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NRZYdjHu-1681961425699)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/handson-imgproc-py/img/8cf9bc94-a0b4-4ae4-9100-27385f0e446e.png)]
扩张
膨胀是另一种基本的形态学操作,它扩展前景对象的大小,平滑对象边界,并关闭二值图像中的孔和间隙。这是侵蚀的双重作用。下面的代码片段展示了如何在泰戈尔的二进制图像上使用binary_dilation()
函数,并使用不同大小的磁盘结构元素:
from skimage.morphology import binary_dilation, diskfrom skimage import img_as_floatim = img_as_float(imread('../images/tagore.png'))im = 1 - im[...,3]im[im <= 0.5] = 0im[im > 0.5] = 1pylab.gray()pylab.figure(figsize=(18,9))pylab.subplot(131)pylab.imshow(im)pylab.title('original', size=20)pylab.axis('off')for d in range(1,3): pylab.subplot(1,3,d 1) im1 = binary_dilation(im, disk(2*d)) ...
开合
开放是一种形态学操作,可以表示为先侵蚀后扩张操作的组合;它从二值图像中移除小对象。关闭相反,是另一种形态学操作,可以表示为先膨胀然后腐蚀操作的组合;它从二值图像中去除小孔。这两个是双重操作。下面的代码片段显示了如何使用 scikit imagemorphology
模块的相应函数分别从二进制图像中移除小对象和小孔:
from skimage.morphology import binary_opening, binary_closing, binary_erosion, binary_dilation, disk
im = rgb2gray(imread('../images/circles.jpg'))
im[im <= 0.5] = 0
im[im > 0.5] = 1
pylab.gray()
pylab.figure(figsize=(20,10))
pylab.subplot(1,3,1), plot_image(im, 'original')
im1 = binary_opening(im, disk(12))
pylab.subplot(1,3,2), plot_image(im1, 'opening with disk size ' str(12))
im1 = binary_closing(im, disk(6))
pylab.subplot(1,3,3), plot_image(im1, 'closing with disk size ' str(6))
pylab.show()
下面的屏幕截图显示了前面代码块的输出,即使用不同大小的磁盘结构元素进行二进制打开和关闭操作生成的模式。正如预期的那样,打开操作仅保留较大的圆:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-c0lsGZQt-1681961425700)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/handson-imgproc-py/img/71eb6cd8-12c3-4871-8b31-54f426e96601.png)]
现在让我们比较一下打开和腐蚀,关闭和膨胀(分别用binary_erosion()
替换binary_opening()
,用binary_dilation()
替换binary_closing()
,结构元素与上一个代码块相同。下面的屏幕截图显示了用腐蚀和膨胀获得的输出图像:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-varowC14-1681961425700)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/handson-imgproc-py/img/050cf664-bbd7-4f89-9ad7-259ada462446.png)]
骨骼化
在该操作中,使用形态学细化操作将二值图像中的每个连接分量缩减为单个像素宽的骨架。以下代码块显示了如何对恐龙的二值图像进行骨架化:
代码语言:javascript复制def plot_images_horizontally(original, filtered, filter_name, sz=(18,7)): pylab.gray() pylab.figure(figsize = sz) pylab.subplot(1,2,1), plot_image(original, 'original') pylab.subplot(1,2,2), plot_image(filtered, filter_name) pylab.show()from skimage.morphology import skeletonizeim = img_as_float(imread('../images/dynasaur.png')[...,3])threshold = 0.5im[im <= threshold] = 0im[im > threshold] = 1skeleton = skeletonize(im)plot_images_horizontally(im, skeleton, 'skeleton',sz=(18,9))
下面的屏幕截图。。。
凸壳的计算
凸包由包围输入图像中所有前景(白色像素)的最小凸面多边形定义。下面的代码块演示如何计算二值图像的convex hull
:
from skimage.morphology import convex_hull_image
im = rgb2gray(imread('../images/horse-dog.jpg'))
threshold = 0.5
im[im < threshold] = 0 # convert to binary image
im[im >= threshold] = 1
chull = convex_hull_image(im)
plot_images_horizontally(im, chull, 'convex hull', sz=(18,9))
以下屏幕截图显示了输出:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wUAqEcUT-1681961425701)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/handson-imgproc-py/img/9c48d0bf-bd13-47be-acfd-f5805c486441.png)]
以下代码块绘制原始二值图像和计算的凸包图像的差异图像:
代码语言:javascript复制im = im.astype(np.bool)
chull_diff = img_as_float(chull.copy())
chull_diff[im] = 2
pylab.figure(figsize=(20,10))
pylab.imshow(chull_diff, cmap=pylab.cm.gray, interpolation='nearest')
pylab.title('Difference Image', size=20)
pylab.show()
以下屏幕截图显示了前面代码的输出:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6mieyUDh-1681961425701)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/handson-imgproc-py/img/03761b6e-b7ee-4154-90e4-5288ae3f22de.png)]
移除小对象
以下代码块显示了如何使用remove_small_objects()
功能删除小于指定最小大小阈值的对象指定阈值越高,删除的对象越多:
from skimage.morphology import remove_small_objectsim = rgb2gray(imread('../images/circles.jpg'))im[im > 0.5] = 1 # create binary image by thresholding with fixed threshold0.5im[im <= 0.5] = 0im = im.astype(np.bool)pylab.figure(figsize=(20,20))pylab.subplot(2,2,1), plot_image(im, 'original')i = 2for osz in [50, 200, 500]: im1 = remove_small_objects(im, osz, connectivity=1) pylab.subplot(2,2,i), plot_image(im1, 'removing small objects below size ' str(osz)) i = 1pylab.show()
下面的屏幕截图显示。。。
黑白顶帽
图像的白色顶帽计算出比结构元素小的亮点。它被定义为原始图像及其形态开口的差分图像。类似地,图像的黑顶帽计算出比结构元素小的黑点。定义为原始图像形态闭合图像的差分图像。原始图像中的黑点在黑顶帽操作后变为亮点。以下代码块演示如何使用 scikit imagemorphology
模块函数对泰戈尔的输入二进制图像使用这两种形态学操作:
from skimage.morphology import white_tophat, black_tophat, square
im = imread('../images/tagore.png')[...,3]
im[im <= 0.5] = 0
im[im > 0.5] = 1
im1 = white_tophat(im, square(5))
im2 = black_tophat(im, square(5))
pylab.figure(figsize=(20,15))
pylab.subplot(1,2,1), plot_image(im1, 'white tophat')
pylab.subplot(1,2,2), plot_image(im2, 'black tophat')
pylab.show()
以下屏幕截图显示了白色和黑色礼帽的输出:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eIJwi1eT-1681961425701)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/handson-imgproc-py/img/3805055b-fe4c-4165-a056-407ff85c46a6.png)]
边界提取
侵蚀操作可用于提取二值图像的边界,我们只需从输入的二值图像中减去侵蚀图像即可提取边界。以下代码块实现了这一点:
代码语言:javascript复制from skimage.morphology import binary_erosionim = rgb2gray(imread('../images/horse-dog.jpg'))threshold = 0.5im[im < threshold] = 0im[im >= threshold] = 1boundary = im - binary_erosion(im)plot_images_horizontally(im, boundary, 'boundary',sz=(18,9))
以下屏幕截图显示了上一个代码块的输出:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-P497MKAe-1681961425701)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/handson-imgproc-py/img/b4171771-b9d8-4eb0-85ee-5044a3480efc.png)]
打开和关闭指纹清洗
打开和关闭可顺序用于从二值图像中去除噪声(前景小对象)。这可以作为预处理步骤用于清洁指纹图像。下面的代码块演示了如何实现它:
代码语言:javascript复制im = rgb2gray(imread('../images/fingerprint.jpg'))
im[im <= 0.5] = 0 # binarize
im[im > 0.5] = 1
im_o = binary_opening(im, square(2))
im_c = binary_closing(im, square(2))
im_oc = binary_closing(binary_opening(im, square(2)), square(2))
pylab.figure(figsize=(20,20))
pylab.subplot(221), plot_image(im, 'original')
pylab.subplot(222), plot_image(im_o, 'opening')
pylab.subplot(223), plot_image(im_c, 'closing')
pylab.subplot(224), plot_image(im_oc, 'opening closing')
pylab.show()
下面的屏幕截图显示了前面代码的输出。可以看出,连续应用打开和关闭可清除噪声二值指纹图像:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-x69WoEhV-1681961425702)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/handson-imgproc-py/img/e100ac21-0ffd-4c7f-9453-7595eeb33991.png)]
灰度运算
下面几个代码块展示了如何在灰度图像上应用形态学操作。首先,让我们从灰度侵蚀开始:
代码语言:javascript复制from skimage.morphology import dilation, erosion, closing, opening, squareim = imread('../images/zebras.jpg')im = rgb2gray(im)struct_elem = square(5) eroded = erosion(im, struct_elem)plot_images_horizontally(im, eroded, 'erosion')
下面的屏幕截图显示了上一个代码块的输出。可以看出,黑色条纹因侵蚀而加宽:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6roQV2bk-1681961425702)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/handson-imgproc-py/img/2e6ef21f-0fbd-4754-8f0d-9d706c63fbc6.png)]
下面的代码块显示了如何在相同的输入灰度图像上应用dilation
:
dilated = dilation(im, struct_elem) ...
scikit image filter.rank 模块
scikit 图像的filter.rank
模块提供了实现形态滤波器的功能;例如,形态学中值滤波器和形态学对比度增强滤波器。以下各节将演示其中的几个过滤器。
形态对比增强
形态学对比度增强滤波器通过仅考虑由结构元素定义的邻域中的像素对每个像素进行操作。它用邻域中的局部最小或局部最大像素替换中心像素,具体取决于原始像素最接近的像素。以下代码块显示了使用形态对比度增强滤波器和曝光模块的自适应直方图均衡化获得的输出的比较,两个滤波器均为局部滤波器:
代码语言:javascript复制from skimage.filters.rank import enhance_contrastdef plot_gray_image(ax, image, title): ax.imshow(image, vmin=0, vmax=255, cmap=pylab.cm.gray), ax.set_title(title), ax.axis('off') ax.set_adjustable('box-forced') ...
用中值滤波去除噪声
下面的代码块显示了如何使用 scikit 图像filters.rank
模块的形态median
过滤器。通过将 10%的像素随机设置为255
(salt),将另外 10%的像素随机设置为0
(胡椒),将一些脉冲噪声添加到输入灰度Lena
图像中。所使用的结构元素是不同尺寸的圆盘,以便通过median
过滤器消除噪音:
from skimage.filters.rank import median
from skimage.morphology import disk
noisy_image = (rgb2gray(imread('../images/lena.jpg'))*255).astype(np.uint8)
noise = np.random.random(noisy_image.shape)
noisy_image[noise > 0.9] = 255
noisy_image[noise < 0.1] = 0
fig, axes = pylab.subplots(2, 2, figsize=(10, 10), sharex=True, sharey=True)
axes1, axes2, axes3, axes4 = axes.ravel()
plot_gray_image(axes1, noisy_image, 'Noisy image')
plot_gray_image(axes2, median(noisy_image, disk(1)), 'Median $r=1$')
plot_gray_image(axes3, median(noisy_image, disk(5)), 'Median $r=5$')
plot_gray_image(axes4, median(noisy_image, disk(20)), 'Median $r=20$')
下面的屏幕截图显示了上一个代码块的输出。可以看出,随着磁盘半径的增加,输出变得更加模糊,尽管同时会消除更多的噪声:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-b6LhCUGv-1681961425702)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/handson-imgproc-py/img/3f8b5e94-0fc3-4f4a-9b84-5db077398602.png)]
计算局部熵
熵是图像中不确定性或随机性的度量。其数学定义如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MMgUM67P-1681961425703)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/handson-imgproc-py/img/7692154c-3a03-4624-ba30-9c9731cd4707.png)]
在前面的公式中,pi是与灰度i相关联的概率(从图像的归一化直方图中获得)。此公式计算图像的全局熵。以类似的方式,我们也可以定义局部熵来定义局部图像复杂度,它可以从局部直方图中计算出来。
skimage.rank.entropy()
函数计算给定结构元素上图像的局部熵(编码局部灰度分布所需的最小位数)。。。
SciPy ndimage.MOTHORMATION 模块
SciPyndimage.morphology
模块还提供了前面讨论的用于对二值图像和灰度图像进行形态学操作的函数,其中一些函数将在以下部分中演示。
填充二进制对象中的漏洞
此函数用于填充二进制对象中的漏洞。以下代码块演示了在输入二进制图像上使用不同结构元素大小的函数的应用:
代码语言:javascript复制from scipy.ndimage.morphology import binary_fill_holesim = rgb2gray(imread('../images/text1.png'))im[im <= 0.5] = 0im[im > 0.5] = 1pylab.figure(figsize=(20,15))pylab.subplot(221), pylab.imshow(im), pylab.title('original', size=20),pylab.axis('off')i = 2for n in [3,5,7]: pylab.subplot(2, 2, i) im1 = binary_fill_holes(im, structure=np.ones((n,n))) pylab.imshow(im1), pylab.title('binary_fill_holes with structure square side ' str(n), size=20) pylab.axis('off') i = 1pylab.show()
下面的屏幕截图显示了。。。
使用开关消除噪音
以下代码块显示了灰度打开和关闭如何从灰度图像中去除椒盐噪声,以及连续应用打开和关闭如何从带噪的山楂灰度图像输入中去除椒盐(脉冲)噪声:
代码语言:javascript复制from scipy import ndimage
im = rgb2gray(imread('../images/mandrill_spnoise_0.1.jpg'))
im_o = ndimage.grey_opening(im, size=(2,2))
im_c = ndimage.grey_closing(im, size=(2,2))
im_oc = ndimage.grey_closing(ndimage.grey_opening(im, size=(2,2)), size=(2,2))
pylab.figure(figsize=(20,20))
pylab.subplot(221), pylab.imshow(im), pylab.title('original', size=20), pylab.axis('off')
pylab.subplot(222), pylab.imshow(im_o), pylab.title('opening (removes salt)', size=20), pylab.axis('off')
pylab.subplot(223), pylab.imshow(im_c), pylab.title('closing (removes pepper)', size=20),pylab.axis('off')
pylab.subplot(224), pylab.imshow(im_oc), pylab.title('opening closing (removes salt pepper)', size=20)
pylab.axis('off')
pylab.show()
下面显示了前面代码的输出,说明了打开和关闭如何从带有噪波的曼陀罗灰度图像中移除椒盐噪声:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5FEzElBn-1681961425703)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/handson-imgproc-py/img/e34cf345-4dc9-4cf7-935b-e6035ffb6819.png)]
形态 Beucher 梯度的计算
形态 Beucher 梯度可以计算为输入灰度图像的放大版和腐蚀版的差分图像。SciPyndimage
提供了计算灰度图像形态梯度的功能。下面的代码块显示了这两个函数如何为爱因斯坦图像生成相同的输出:
from scipy import ndimageim = rgb2gray(imread('../images/einstein.jpg'))im_d = ndimage.grey_dilation(im, size=(3,3))im_e = ndimage.grey_erosion(im, size=(3,3))im_bg = im_d - im_eim_g = ndimage.morphological_gradient(im, size=(3,3))pylab.gray()pylab.figure(figsize=(20,18))pylab.subplot(231), pylab.imshow(im), pylab.title('original', size=20),pylab.axis('off')pylab.subplot(232), ...
形态拉普拉斯的计算
下面的代码块演示了如何使用泰戈尔二值图像的相应ndimage
函数计算形态拉普拉斯,并将其与具有不同大小结构元素的形态梯度进行比较,尽管可以看出,对于该图像,具有梯度的较小结构元素和具有拉普拉斯的较大结构元素在提取的边缘方面产生更好的输出图像:
im = imread('../images/tagore.png')[...,3]
im_g = ndimage.morphological_gradient(im, size=(3,3))
im_l = ndimage.morphological_laplace(im, size=(5,5))
pylab.figure(figsize=(15,10))
pylab.subplot(121), pylab.title('ndimage morphological laplace', size=20), pylab.imshow(im_l)
pylab.axis('off')
pylab.subplot(122), pylab.title('ndimage morphological gradient', size=20),
pylab.imshow(im_g)
pylab.axis('off')
pylab.show()
以下屏幕截图显示了前面代码的输出:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CpHZaMuP-1681961425703)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/handson-imgproc-py/img/efd87a74-cc48-46bf-81df-ece24e32e7f8.png)]
总结
在本章中,我们讨论了基于数学形态学的不同图像处理技术。我们讨论了形态学上的二元操作,如腐蚀、膨胀、打开、关闭、骨骼化以及黑白顶帽。然后,我们讨论了一些应用,如计算凸包、去除小对象、提取边界、打开和关闭指纹清洗、填充二元对象中的孔洞以及使用打开和关闭去除噪声。然后,我们讨论了形态学运算到灰度运算的扩展,以及形态学对比度增强、中值滤波去噪和计算局部熵的应用。此外,我们还讨论了如何计算形态学参数。。。
问题
- 用二值图像显示形态打开和关闭是双重操作。(提示:在具有相同结构元素的图像前景上应用打开,在图像背景上应用关闭)
- 使用图像中对象的凸包自动裁剪图像(问题取自https://stackoverflow.com/questions/14211340/automatically-cropping-an-image-with-python-pil/51703287#51703287 )。使用以下图像并裁剪白色背景:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qxbyj6kF-1681961425703)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/handson-imgproc-py/img/5179ac65-509a-47e9-8239-98162d001f8d.png)]
所需的输出图像如下所示。将自动找到裁剪图像的边框:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QsRlhoyY-1681961425704)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/handson-imgproc-py/img/ad15e7a2-2613-449f-a932-93a20c55063d.png)]
- 使用
skimage.filters.rank
中的maximum()
和minimum()
功能,实现灰度图像的形态打开和关闭。
进一步阅读
- https://www.idi.ntnu.no/emner/tdt4265/lectures/lecture3b.pdf
- https://www.uio.no/studier/emner/matnat/ifi/INF4300/h11/undervisningsmateriale/morfologi2011.pdf
- https://www.cis.rit.edu/class/simg782/lectures/lecture_03/lec782_05_03.pdf
- http://www.math.tau.ac.il/~turkel/notes/segmentation_morphology.pdf
- https://courses.cs.washington.edu/courses/cse576/book/ch3.pdf
- http://www.cse.iitd.ernet.in/~pkalra/csl783/morphical.pdf
七、提取图像特征和描述符
在本章中,我们将讨论特征检测器和描述符,以及不同类型的特征检测器/提取器在图像处理中的各种应用。我们将从定义特征检测器和描述符开始。然后,我们将继续讨论一些流行的特征检测器,如 Harris 角点/SIFT 和 HOG,然后分别使用scikit-image
和python-opencv (cv2)
库函数讨论它们在图像匹配和目标检测等重要图像处理问题中的应用。
本章涉及的主题如下:
- 特征检测器与描述符,用于从图像中提取特征/描述符
- Harris 角点检测器和 Harris 角点特征在图像匹配中的应用(与 scikit 图像)
- 带有 LoG、DoG 和 DoH 的水滴探测器(带有
scikit-image
) - 方向梯度特征直方图的提取
- SIFT、ORB 和简短特征及其在图像匹配中的应用
- 类 Haar 特征及其在人脸检测中的应用
特征检测器与描述符
在图像处理中,(局部)特征是指与图像处理任务相关的一组关键/显著点或信息,它们创建了图像的抽象、更通用(通常更健壮)表示。基于某种标准(例如,检测/提取图像特征的转角、局部最大值/最小值等)从图像中选择一组兴趣点的一系列算法称为特征检测器/提取器。
相反,描述符由一组值组成,用于表示具有特征/兴趣点(例如,HOG 特征)的图像。特征提取也可以被认为是一种将图像转换为集合的操作。。。
哈里斯角检测器
该算法探索窗口在图像中的位置变化时窗口内的强度变化。与仅在一个方向上突然更改强度值的边不同,在所有方向的拐角处,强度值都会发生显著更改。因此,当窗口在拐角处沿任何方向移动(具有良好的定位)时,强度值应发生较大变化;哈里斯角点检测算法利用了这一事实。它不随旋转而变化,但不随缩放而变化(即,在图像进行旋转变换时,从图像中找到的角点保持不变,但在调整图像大小时会发生变化)。在本节中,我们将讨论如何使用scikit-image
实现 Harris 角点检测器。
使用 scikit 图像
下一个代码片段显示了如何使用 Harris 角点检测器和scikit-image
功能模块中的corner_harris()
功能检测图像中的角点:
image = imread('../images/chess_football.png') # RGB imageimage_gray = rgb2gray(image)coordinates = corner_harris(image_gray, k =0.001)image[coordinates>0.01*coordinates.max()]=[255,0,0,255]pylab.figure(figsize=(20,10))pylab.imshow(image), pylab.axis('off'), pylab.show()
下一个屏幕截图显示了代码的输出,其中角点被检测为红色点:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gI7L6KXr-1681961425704)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/handson-imgproc-py/img/f9e4e862-af3a-4d1e-b44b-901e4021b496.png)]
亚像素精度
有时,可能需要以最大精度找到拐角。通过 scikit 图像功能模块的corner_subpix()
功能,检测到的角点以亚像素精度进行细化。下面的代码演示如何使用此函数。巴黎卢浮宫金字塔作为输入图像,通常先用corner_peaks()
计算哈里斯角点,然后用corner_subpix()
函数计算角点的亚像素位置,该函数使用统计测试来决定是否接受/拒绝之前用corner_peaks()
计算的角点函数。我们需要定义函数用于搜索角点的邻域(窗口)的大小:
image = imread('../images/pyramids2.jpg')
image_gray = rgb2gray(image)
coordinates = corner_harris(image_gray, k =0.001)
coordinates[coordinates > 0.03*coordinates.max()] = 255 # threshold for an optimal value, depends on the image
corner_coordinates = corner_peaks(coordinates)
coordinates_subpix = corner_subpix(image_gray, corner_coordinates, window_size=11)
pylab.figure(figsize=(20,20))
pylab.subplot(211), pylab.imshow(coordinates, cmap='inferno')
pylab.plot(coordinates_subpix[:, 1], coordinates_subpix[:, 0], 'r.', markersize=5, label='subpixel')
pylab.legend(prop={'size': 20}), pylab.axis('off')
pylab.subplot(212), pylab.imshow(image, interpolation='nearest')
pylab.plot(corner_coordinates[:, 1], corner_coordinates[:, 0], 'bo', markersize=5)
pylab.plot(coordinates_subpix[:, 1], coordinates_subpix[:, 0], 'r ', markersize=10), pylab.axis('off')
pylab.tight_layout(), pylab.show()
接下来的两个屏幕截图显示了代码的输出。在第一个屏幕截图中,Harris 角点用黄色像素标记,精细的亚像素角点用红色像素标记。在第二个屏幕截图中,检测到的角点用蓝色像素和亚像素(同样是红色像素)绘制在原始输入图像的顶部:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-T81sKR7c-1681961425704)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/handson-imgproc-py/img/8ee80b1b-4005-43f2-b170-6b4fc6d55882.png)]
一种应用——图像匹配
一旦我们检测到图像中的兴趣点,最好知道如何在同一对象的不同图像上匹配这些点。例如,以下列表显示了匹配两个此类图像的一般方法:
- 计算感兴趣的点(例如,使用 Harris 角点检测器的角点)
- 考虑每个关键点周围的区域(窗口)
- 从区域中,为每个图像的每个关键点计算一个局部特征描述符,并进行规格化
- 匹配两幅图像中计算的局部描述符(例如,使用欧几里德距离)
哈里斯角点可以用来匹配两幅图像;下一节将给出一个示例
基于 RANSAC 算法和 Harris 角点特征的鲁棒图像匹配
在本例中,我们将匹配图像及其仿射变换版本;它们可以被视为是从不同的观点出发的。以下步骤描述了图像匹配算法:
- 首先,我们将计算两幅图像中的兴趣点或 Harris 角点。
- 将考虑点周围的小空间,然后使用平方差的加权和计算点之间的对应关系。此度量不是非常健壮,仅在视点发生轻微变化时可用。
- 一旦找到对应,将获得一组源坐标和相应的目标坐标;它们用于估计两幅图像之间的几何变换。
- 用坐标简单地估计参数是不够的,许多对应关系可能是错误的。
- 使用随机样本一致性(RANSAC)算法对参数进行稳健估计,首先将点分类为
inliers
和outliers
,然后在忽略outliers
的情况下将模型拟合到inliers
,以找到与仿射变换一致的匹配。
下一个代码块显示如何使用 Harris 角点功能实现图像匹配:
代码语言:javascript复制temple = rgb2gray(img_as_float(imread('../images/temple.jpg')))
image_original = np.zeros(list(temple.shape) [3])
image_original[..., 0] = temple
gradient_row, gradient_col = (np.mgrid[0:image_original.shape[0], 0:image_original.shape[1]] / float(image_original.shape[0]))
image_original[..., 1] = gradient_row
image_original[..., 2] = gradient_col
image_original = rescale_intensity(image_original)
image_original_gray = rgb2gray(image_original)
affine_trans = AffineTransform(scale=(0.8, 0.9), rotation=0.1, translation=(120, -20))
image_warped = warp(image_original, affine_trans .inverse, output_shape=image_original.shape)
image_warped_gray = rgb2gray(image_warped)
使用 Harris 角点度量提取角点:
代码语言:javascript复制coordinates = corner_harris(image_original_gray)
coordinates[coordinates > 0.01*coordinates.max()] = 1
coordinates_original = corner_peaks(coordinates, threshold_rel=0.0001, min_distance=5)
coordinates = corner_harris(image_warped_gray)
coordinates[coordinates > 0.01*coordinates.max()] = 1
coordinates_warped = corner_peaks(coordinates, threshold_rel=0.0001, min_distance=5)
确定亚像素角点位置:
代码语言:javascript复制coordinates_original_subpix = corner_subpix(image_original_gray, coordinates_original, window_size=9)
coordinates_warped_subpix = corner_subpix(image_warped_gray, coordinates_warped, window_size=9)
def gaussian_weights(window_ext, sigma=1):
y, x = np.mgrid[-window_ext:window_ext 1, -window_ext:window_ext 1]
g_w = np.zeros(y.shape, dtype = np.double)
g_w[:] = np.exp(-0.5 * (x**2 / sigma**2 y**2 / sigma**2))
g_w /= 2 * np.pi * sigma * sigma
return g_w
根据到中心像素的距离对像素进行加权,计算扭曲图像中所有角点的平方差之和,并使用具有最小 SSD 的角点作为对应:
代码语言:javascript复制def match_corner(coordinates, window_ext=3):
row, col = np.round(coordinates).astype(np.intp)
window_original = image_original[row-window_ext:row window_ext 1, col-window_ext:col window_ext 1, :]
weights = gaussian_weights(window_ext, 3)
weights = np.dstack((weights, weights, weights))
SSDs = []
for coord_row, coord_col in coordinates_warped:
window_warped = image_warped[coord_row-window_ext:coord_row window_ext 1,
coord_col-window_ext:coord_col window_ext 1, :]
if window_original.shape == window_warped.shape:
SSD = np.sum(weights * (window_original - window_warped)**2)
SSDs.append(SSD)
min_idx = np.argmin(SSDs) if len(SSDs) > 0 else -1
return coordinates_warped_subpix[min_idx] if min_idx >= 0 else [None]
使用平方差的简单加权和查找对应关系:
代码语言:javascript复制source, destination = [], []
for coordinates in coordinates_original_subpix:
coordinates1 = match_corner(coordinates)
if any(coordinates1) and len(coordinates1) > 0 and not all(np.isnan(coordinates1)):
source.append(coordinates)
destination.append(coordinates1)
source = np.array(source)
destination = np.array(destination)
使用所有坐标估计仿射变换模型:
代码语言:javascript复制model = AffineTransform()
model.estimate(source, destination)
使用 RANSAC 稳健估计仿射变换模型:
代码语言:javascript复制model_robust, inliers = ransac((source, destination), AffineTransform, min_samples=3, residual_threshold=2, max_trials=100)
outliers = inliers == False
比较True
和估计的变换参数:
print(affine_trans.scale, affine_trans.translation, affine_trans.rotation)
# (0.8, 0.9) [ 120. -20.] 0.09999999999999999
print(model.scale, model.translation, model.rotation)
# (0.8982412101241938, 0.8072777593937368) [ -20.45123966 114.92297156] -0.10225420334222493
print(model_robust.scale, model_robust.translation, model_robust.rotation)
# (0.9001524425730119, 0.8000362790749188) [ -19.87491292 119.83016533] -0.09990858564132575
将通信可视化:
代码语言:javascript复制fig, axes = pylab.subplots(nrows=2, ncols=1, figsize=(20,15))
pylab.gray()
inlier_idxs = np.nonzero(inliers)[0]
plot_matches(axes[0], image_original_gray, image_warped_gray, source, destination, np.column_stack((inlier_idxs, inlier_idxs)), matches_color='b')
axes[0].axis('off'), axes[0].set_title('Correct correspondences', size=20)
outlier_idxs = np.nonzero(outliers)[0]
plot_matches(axes[1], image_original_gray, image_warped_gray, source, destination, np.column_stack((outlier_idxs, outlier_idxs)), matches_color='row')
axes[1].axis('off'), axes[1].set_title('Faulty correspondences', size=20)
fig.tight_layout(), pylab.show()
下面的屏幕截图显示了代码块的输出。找到的正确对应用蓝线显示,而错误对应用红线显示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KdDHI9Mv-1681961425705)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/handson-imgproc-py/img/4ded7cfe-7442-4d7e-8b2d-57a591b6c0ab.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YMFMttiV-1681961425705)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/handson-imgproc-py/img/8145bb33-43ef-4a98-b090-c7b39aee3704.png)]
Blob 探测器,带有 LoG、DoG 和 DoH
在图像中,水滴被定义为暗区域上的亮区域或亮区域上的暗区域。在本节中,我们将讨论如何使用以下三种算法在图像中实现斑点特征检测。输入图像是彩色(RGB)蝴蝶图像。
高斯拉普拉斯(对数)
在第 3 章、卷积和频域滤波中,我们看到图像与滤波器的互相关可以看作是模式匹配;也就是说,将一个(小的)模板图像(我们想要找到的)与图像中的所有局部区域进行比较。斑点检测的关键思想来自这一事实。在上一章中,我们已经看到了带过零的对数滤波器如何用于边缘检测。LoG 还可以利用尺度空间的概念,通过搜索 LoG 的三维(位置 尺度)极值来寻找尺度不变区域。如果拉普拉斯函数的尺度(对数滤波器的σ)与 blob 的尺度匹配,则拉普拉斯函数响应的幅度在 blob 的中心达到最大值。采用这种方法,对数卷积图像以逐渐增加的σ进行计算,并堆叠成立方体。BLOB 对应于此多维数据集中的局部最大值。这种方法只检测黑暗背景上的明亮斑点。它是准确的,但速度很慢(特别是对于检测较大的斑点)。
高斯差分(DoG)
LoG 方法近似于 DoG 方法,因此速度更快。图像用增加的σ值进行平滑(使用高斯),两个连续平滑图像之间的差异叠加在一个立方体中。这种方法再次检测黑暗背景上的亮斑。它比日志更快,但精确度较低,尽管较大的斑点检测仍然昂贵。
黑森行列式(DoH)
DoH 方法是所有这些方法中最快的。它通过计算图像 Hessian 行列式矩阵的最大值来检测斑点。水滴的大小对检测速度没有任何影响。该方法既能检测出暗背景上的亮斑,又能检测出亮背景上的暗斑,但不能准确地检测出小斑。
下一个代码块演示如何使用 scikit image 实现上述三种算法:
代码语言:javascript复制from numpy import sqrt
from skimage.feature import blob_dog, blob_log, blob_doh
im = imread('../images/butterfly.png')
im_gray = rgb2gray(im)
log_blobs = blob_log(im_gray, max_sigma=30, num_sigma=10, threshold=.1)
log_blobs[:, 2] = sqrt(2) * log_blobs[:, 2] # Compute radius in the 3rd column
dog_blobs = blob_dog(im_gray, max_sigma=30, threshold=0.1)
dog_blobs[:, 2] = sqrt(2) * dog_blobs[:, 2]
doh_blobs = blob_doh(im_gray, max_sigma=30, threshold=0.005)
list_blobs = [log_blobs, dog_blobs, doh_blobs]
color, titles = ['yellow', 'lime', 'red'], ['Laplacian of Gaussian', 'Difference of Gaussian', 'Determinant of Hessian']
sequence = zip(list_blobs, colors, titles)
fig, axes = pylab.subplots(2, 2, figsize=(20, 20), sharex=True, sharey=True)
axes = axes.ravel()
axes[0].imshow(im, interpolation='nearest')
axes[0].set_title('original image', size=30), axes[0].set_axis_off()
for idx, (blobs, color, title) in enumerate(sequence):
axes[idx 1].imshow(im, interpolation='nearest')
axes[idx 1].set_title('Blobs with ' title, size=30)
for blob in blobs:
y, x, row = blob
col = pylab.Circle((x, y), row, color=color, linewidth=2, fill=False)
axes[idx 1].add_patch(col), axes[idx 1].set_axis_off()
pylab.tight_layout(), pylab.show()
下面的屏幕截图显示了代码的输出,即使用不同算法检测到的斑点:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EH8vRzRS-1681961425705)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/handson-imgproc-py/img/d2251034-c47d-4e24-ba3a-f0340af9a346.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HBc4ONQj-1681961425705)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/handson-imgproc-py/img/58f50753-29e9-4ec2-b337-c34dccbd179e.png)]
角点和斑点特征都具有重复性、显著性和局部性。
定向梯度直方图
用于目标检测的一种流行特征描述符是定向梯度的直方图(HOG。在本节中,我们将讨论如何从图像计算 HOG 描述符。
计算 HOG 描述符的算法
以下步骤描述了该算法:
- 如果愿意,可以对图像进行全局规格化
- 计算水平和垂直梯度图像
- 计算梯度直方图
- 跨块标准化
- 展平为特征描述符向量
HOG 描述符是使用该算法最终得到的归一化块描述符
使用 scikit 图像计算 HOG 描述符
现在,让我们使用 scikit 图像功能模块的hog()
函数计算 HOG 描述符,并将其可视化:
from skimage.feature import hogfrom skimage import exposureimage = rgb2gray(imread('../images/cameraman.jpg'))fd, hog_image = hog(image, orientations=8, pixels_per_cell=(16, 16), cells_per_block=(1, 1), visualize=True) print(image.shape, len(fd))# ((256L, 256L), 2048)fig, (axes1, axes2) = pylab.subplots(1, 2, figsize=(15, 10), sharex=True, sharey=True)axes1.axis('off'), axes1.imshow(image, cmap=pylab.cm.gray), axes1.set_title('Input image')
现在,让我们重新缩放直方图以更好地显示:
代码语言:javascript复制hog_image_rescaled = exposure.rescale_intensity(hog_image, in_range=(0, 10))axes2.axis('off'), ...
尺度不变特征变换
尺度不变特征变换(SIFT描述符)为图像区域提供了一种替代表示。它们对于匹配图像非常有用。如前所述,当要匹配的图像本质相似时(关于标度、方向等),简单的角点检测器工作良好。但是,如果它们具有不同的尺度和旋转,则需要使用 SIFT 描述符来匹配它们。SIFT 不仅具有尺度不变性,而且在图像的旋转、照明和视点也发生变化时,仍然可以获得良好的结果
让我们讨论 SIFT 算法中涉及的主要步骤,SIFT 算法将图像内容转换为对平移、旋转、缩放和其他成像参数不变的局部特征坐标。
SIFT 描述符的计算算法
- 尺度空间极值检测:搜索多个尺度和图像位置,位置和特征尺度由 DoG 检测器给出
- **关键点定位:**基于稳定性度量选择关键点,通过消除低对比度和边缘关键点,仅保留强兴趣点
- 方向分配:计算每个关键点区域的最佳方向,有助于匹配的稳定性
- 关键点描述符计算:使用选定比例和旋转的局部图像梯度来描述每个关键点区域
如前所述,SIFT 对于光照的微小变化(由于梯度和归一化)、姿势(由于。。。
使用 opencv 和 opencv contrib
为了能够与python-opencv
一起使用 SIFT 功能,我们首先需要按照此链接的说明安装opencv-contrib
:https://pypi.org/project/opencv-contrib-python/ 。下一个代码块演示如何检测 SIFT 关键点,并使用输入的蒙娜丽莎图像绘制它们
我们将首先构造一个 SIFT 对象,然后使用detect()
方法计算图像中的关键点。每个关键点都是一个特殊的特征,并且具有多个属性。例如,它的*(x,y)*坐标、角度(方向)、响应(关键点的强度)、有意义邻域的大小等等。
然后,我们将使用cv2
中的drawKeyPoints()
函数在检测到的关键点周围绘制小圆圈。如果将cv2.DRAW_MATCHES_FLAGS_DRAW_RICH_KEYPOINTS
标志应用于函数,它将绘制一个具有关键点大小的圆及其方向。为了同时计算关键点和描述符,我们将使用函数detectAndCompute()
:
# make sure the opencv version is 3.3.0 with
# pip install opencv-python==3.3.0.10 opencv-contrib-python==3.3.0.10
import cv2
print(cv2.__version__)
# 3.3.0
img = cv2.imread('../images/monalisa.jpg')
gray= cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)
sift = cv2.xfeatures2d.SIFT_create()
kp = sift.detect(gray,None) # detect SIFT keypoints
img = cv2.drawKeypoints(img,kp, None, flags=cv2.DRAW_MATCHES_FLAGS_DRAW_RICH_KEYPOINTS)
cv2.imshow("Image", img);
cv2.imwrite('me5_keypoints.jpg',img)
kp, des = sift.detectAndCompute(gray,None) # compute the SIFT descriptor
下面是代码的输出,输入蒙娜丽莎图像,以及在其上绘制的计算的 SIFT 关键点,以及方向:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-T5UtnDOC-1681961425706)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/handson-imgproc-py/img/d8c9c4b8-7ed2-4b28-b29f-a0bd1945c4b2.png)]
应用程序–使用简短、筛选和 ORB 匹配图像
在最后一节中,我们讨论了如何检测 SIFT 关键点。在本节中,我们将为图像引入更多的特征描述符,即简短(一个简短的二进制描述符)和 ORB(一个有效的 SIFT 替代品)。所有这些描述符也可以用于图像匹配和对象检测,我们将很快看到
使用简短的二进制描述符将图像与 scikit 图像匹配
简短描述符具有相对较少的位,可以使用一组强度差测试进行计算。作为一个较短的二进制描述符,它具有较低的内存占用,使用该描述符进行匹配在汉明距离度量下非常有效。简单地说,通过检测不同尺度下的特征可以获得所需的尺度不变性,尽管它不提供旋转不变性。下一个代码块演示如何使用 scikit 图像函数计算简短的二进制描述符。用于匹配的输入图像是灰度 Lena 图像及其仿射变换版本
现在让我们编写以下代码:
代码语言:javascript复制from skimage import transform as transform
from skimage.feature import (match_descriptors, corner_peaks, corner_harris, plot_matches, BRIEF)
img1 = rgb2gray(imread('../images/lena.jpg')) #data.astronaut())
affine_trans = transform.AffineTransform(scale=(1.2, 1.2), translation=(0,-100))
img2 = transform.warp(img1, affine_trans)
img3 = transform.rotate(img1, 25)
coords1, coords2, coords3 = corner_harris(img1), corner_harris(img2), corner_harris(img3)
coords1[coords1 > 0.01*coords1.max()] = 1
coords2[coords2 > 0.01*coords2.max()] = 1
coords3[coords3 > 0.01*coords3.max()] = 1
keypoints1 = corner_peaks(coords1, min_distance=5)
keypoints2 = corner_peaks(coords2, min_distance=5)
keypoints3 = corner_peaks(coords3, min_distance=5)
extractor = BRIEF()
extractor.extract(img1, keypoints1)
keypoints1, descriptors1 = keypoints1[extractor.mask], extractor.descriptors
extractor.extract(img2, keypoints2)
keypoints2, descriptors2 = keypoints2[extractor.mask], extractor.descriptors
extractor.extract(img3, keypoints3)
keypoints3, descriptors3 = keypoints3[extractor.mask], extractor.descriptors
matches12 = match_descriptors(descriptors1, descriptors2, cross_check=True)
matches13 = match_descriptors(descriptors1, descriptors3, cross_check=True)
fig, axes = pylab.subplots(nrows=2, ncols=1, figsize=(20,20))
pylab.gray(), plot_matches(axes[0], img1, img2, keypoints1, keypoints2, matches12)
axes[0].axis('off'), axes[0].set_title("Original Image vs. Transformed Image")
plot_matches(axes[1], img1, img3, keypoints1, keypoints3, matches13)
axes[1].axis('off'), axes[1].set_title("Original Image vs. Transformed Image"), pylab.show()
下面的屏幕截图显示了代码块的输出以及两幅图像之间的简短关键点如何匹配:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3bXZ7N6l-1681961425706)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/handson-imgproc-py/img/668443a7-57f2-408c-97ec-451170e86e5b.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gJThlqDd-1681961425706)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/handson-imgproc-py/img/e5c71d72-447c-457b-b2f8-819562781138.png)]
使用 scikit 图像匹配 ORB 特征检测器和二进制描述符
让我们编写一段代码来演示 ORB 特征检测和二进制描述符算法。该算法采用了一种面向对象的快速检测方法和旋转的简短描述子。与 BRIEF 相比,ORB 具有更大的尺度和旋转不变性,但即使这样,也应用了更有效的汉明距离度量进行匹配。因此,在考虑实时应用时,此方法优于简单方法:
代码语言:javascript复制from skimage import transform as transformfrom skimage.feature import (match_descriptors, ORB, plot_matches)img1 = rgb2gray(imread('../images/me5.jpg'))img2 = transform.rotate(img1, 180)affine_trans = transform.AffineTransform(scale=(1.3, 1.1), ...
使用蛮力匹配 python opencv 与 ORB 特性匹配
在本节中,我们将演示如何使用opencv
的蛮力匹配器匹配两个图像描述符。在这种情况下,来自一个图像的特征描述符与另一个图像中的所有特征匹配(使用一些距离度量),并返回最接近的特征。我们将使用带有 ORB 描述符的BFMatcher()
函数来匹配两个图书图像:
img1 = cv2.imread('../images/books.png',0) # queryImage
img2 = cv2.imread('../images/book.png',0) # trainImage
# Create a ORB detector object
orb = cv2.ORB_create()
# find the keypoints and descriptors
kp1, des1 = orb.detectAndCompute(img1,None)
kp2, des2 = orb.detectAndCompute(img2,None)
# create a BFMatcher object
bf = cv2.BFMatcher(cv2.NORM_HAMMING, crossCheck=True)
# Match descriptors.
matches = bf.match(des1, des2)
# Sort them in the order of their distance.
matches = sorted(matches, key = lambda x:x.distance)
# Draw first 20 matches.
img3 = cv2.drawMatches(img1,kp1,img2,kp2,matches[:20], None, flags=2)
pylab.figure(figsize=(20,10)), pylab.imshow(img3), pylab.show()
以下屏幕截图显示了代码块中使用的输入图像:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sYDi4qNc-1681961425706)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/handson-imgproc-py/img/de4cf7fa-9a47-4e09-830f-eafc83713beb.png)]
以下屏幕截图显示了代码块计算的前 20 个 ORB 关键点匹配:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4ZBTPX18-1681961425707)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/handson-imgproc-py/img/893a9fe3-2a53-4508-a81b-d7fda92cc449.png)]
使用 SIFT 描述符的蛮力匹配和使用 OpenCV 的比率测试
两幅图像之间的 SIFT 关键点通过识别其最近邻进行匹配。但在某些情况下,由于噪音等因素,第二个最接近的匹配可能更接近第一个。在这种情况下,我们计算最近距离与第二最近距离的比率,并检查其是否高于 0.8。如果比率大于 0.8,则表示它们被拒绝。
这有效地消除了大约 90%的错误匹配,只有大约 5%的正确匹配*(根据 SIFT 纸张)。让我们使用knnMatch()
功能来获得一个关键点的k=2
最佳匹配;我们还将应用比率测试:*
# make sure the opencv version is 3.3.0 with# pip install opencv-python==3.3.0.10 ...
类哈尔特征
类 Haar 特征是用于目标检测的非常有用的图像特征。Viola 和 Jones 在第一台实时人脸检测仪中引入了这种技术。利用积分图像,可以在恒定时间内有效地计算任意大小(尺度)的类 Haar 特征。计算速度是 Haar-like 特征相对于大多数其他特征的关键优势。这些特性就像第 3 章、卷积和频域滤波中介绍的卷积核(矩形滤波器)。每个特征对应于一个单独的值,该值通过从黑色矩形下的像素总和减去白色矩形下的像素总和计算得出。下图显示了不同类型的类 Haar 特征,以及用于人脸检测的重要类 Haar 特征:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PF4RM6TY-1681961425707)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/handson-imgproc-py/img/d2de5fbd-dbbe-4530-a4a3-44b77a8cb0eb.png)]
这里显示的人脸检测的第一个和第二个重要特征似乎集中在以下事实上:眼睛区域通常比鼻子和脸颊区域暗,眼睛也比鼻梁暗。下一节使用 scikit 图像可视化 Haar 样特征。
带有 scikit 图像的类 Haar 特征描述符
在本节中,我们将可视化不同类型的 Haar-like 特征描述符,其中有五种不同类型。描述符的值等于蓝色和红色强度值之和之间的差值。
下一个代码块显示了如何使用 scikit 图像特征模块的haar_like_feature_coord()
和draw_haar_like_feature()
函数来可视化不同类型的 Haar 特征描述符:
from skimage.feature import haar_like_feature_coordfrom skimage.feature import draw_haar_like_featureimages = [np.zeros((2, 2)), np.zeros((2, 2)), np.zeros((3, 3)),np.zeros((3, 3)), np.zeros((2, 2))]feature_types = ['type-2-x', 'type-2-y', 'type-3-x', ...
应用–具有类 Haar 特征的人脸检测
使用 Viola-Jones 人脸检测算法,可以使用这些类似 Haar 的特征在图像中检测人脸。每个 Haar-like 特征只是一个弱分类器,因此需要大量的 Haar-like 特征才能准确地检测人脸。使用积分图像计算每个类 Haar 内核的所有可能大小和位置的大量类 Haar 特征。然后使用 AdaBoost 集成分类器从大量特征中选择重要特征,并在训练阶段将其组合成强分类器模型。然后使用学习的模型对具有选定特征的人脸区域进行分类
图像中的大多数区域通常是非人脸区域。因此,首先检查窗口是否不是面区域。如果不是,则在一次拍摄中丢弃,并在可能发现人脸的不同区域进行检查。为了实现这一思想,引入了级联分类器的概念。与在一个窗口上应用大量的特征不同,这些特征被分组到分类器的不同阶段,并逐个应用。(前几个阶段包含的功能非常少)。如果某个窗口在第一阶段出现故障,它将被丢弃,并且不考虑其上的其余功能。如果通过,则应用特征的第二阶段,依此类推。面区域对应于通过所有阶段的窗口。这些概念将在第 9 章、图像处理中的经典机器学习方法中详细讨论。
基于预训练分类器和 Haar 级联特征的 OpenCV 人脸/眼睛检测
OpenCV 配有一个培训师和一个检测器。在本节中,我们将演示使用预先训练的人脸、眼睛、微笑等分类器进行检测(跳过训练模型)。OpenCV 已经包含了许多已经训练过的模型;我们将使用它们,而不是从头开始训练分类器。这些预先训练好的分类器被序列化为 XML 文件,并附带 OpenCV 安装(可以在opencv/data/haarcascades/
文件夹中找到)
为了从输入图像中检测人脸,首先需要加载所需的 XML 分类器,然后加载输入图像(在灰度模式下)。图像中的面可以是。。。
总结
在本章中,我们讨论了一些重要的特征检测和提取技术,以使用 Python 的scikit-image
和cv2 (python-opencv)
库从图像中计算不同类型的特征描述符。我们首先介绍了图像的局部特征检测器和描述符的基本概念,以及它们所需要的特性。然后,我们讨论了 Harris 角点检测器来检测图像的角点兴趣点,并使用它们匹配两幅图像(从不同的视点捕获相同的对象)。接下来,我们讨论了使用 LoG/DoG/DoH 过滤器的 blob 检测。接下来,我们讨论了 HOG、SIFT、ORB、简短的二进制检测器/描述符以及如何将图像与这些特征匹配。最后,我们讨论了类 Haar 特征和 Viola-Jones 算法的人脸检测。在本章结束时,您应该能够使用 Python 库计算图像的不同特征/描述符。此外,您还应该能够使用不同类型的特征描述符(例如,SIFT、ORB 等)匹配图像并使用 Python 从包含人脸的图像中检测人脸。
在下一章中,我们将讨论图像分割。
问题
- 使用
cv2
实现亚像素精度的 Harris 角点检测器。 - 使用
cv2
使用几个不同的预先训练好的 Haar Cascade 分类器,尝试从图像中检测多张人脸。 - 使用基于 FLANN 的近似最近邻匹配器代替
BFMatcher
将图像与具有cv2
的书籍进行匹配。 - 计算 SURF 关键点并使用它们与
cv2
进行图像匹配
进一步阅读
- http://scikit-image.org/docs/dev/api/skimage.feature.html
- https://docs.opencv.org/3.1.0/da/df5/tutorial_py_sift_intro.html
- https://sandipanweb.wordpress.com/2017/10/22/feature-detection-with-harris-corner-detector-and-matching-images-with-feature-descriptors-in-python/
- https://sandipanweb.wordpress.com/2018/06/30/detection-of-a-human-object-with-hog-descriptor-features-using-svm-primal-quadprog-implementation-using-cvxopt-in-python/
- http://vision.stanford.edu/teaching/cs231b_spring1213/slides/HOG_2011_Stanford.pdf
- http://cvgl.stanford.edu/teaching/cs231a_winter1415/lecture/lecture10_detector_descriptors_2015.pdf
- https://www.cis.rit.edu/~cnspci/references/dip/feature_extraction/harris1988.pdf
- https://www.cs.ubc.ca/~lowe/papers/ijcv04.pdf
- https://www.cs.cmu.edu/~efros/courses/LBMV07/Papers/viola-cvpr-01.pdf
- https://lear.inrialpes.fr/people/triggs/pubs/Dalal-cvpr05.pdf**
八、图像分割
在本章中,我们将讨论图像处理中的一个关键概念,即分割。我们将首先介绍图像分割的基本概念以及为什么它如此重要。我们将继续讨论许多不同的图像分割技术及其在scikit-image
和python-opencv
(cv2
库函数中的实现。
*本章涉及的主题如下:
- 图像中的 Hough 变换圆和线检测(带
scikit-image
- 阈值化和大津分割(带
scikit-image
- 基于边缘/基于区域的分割技术(带
scikit-image
) - Felzenszwalb、SLIC、QuickShift 和紧凑分水岭算法(带
scikit-image
) - 活动轮廓。。。
什么是图像分割?
图像分割是将图像划分为不同的区域或类别,对应于不同的对象或对象的部分。每个区域包含具有相似属性的像素,并且图像中的每个像素被分配到这些类别之一。好的分割通常是指同一类别中的像素具有相似的强度值并形成一个连接区域,而不同类别中的相邻像素具有不同的值。其目的是简化/更改图像的表示,使其更具意义且更易于分析
如果分割做得好,那么图像分析的所有其他阶段都会变得更简单。因此,分割的质量和可靠性决定了图像分析是否成功。但是将图像分割成正确的片段通常是一个非常具有挑战性的问题。
分割技术可以是非上下文的(不考虑图像中的特征之间的空间关系,而只考虑一些全局属性,例如颜色/灰度级)或上下文(另外利用空间关系;例如,在空间上相近的像素具有相似的灰度级)。。在本章中,我们将讨论不同的分段技术,并使用scikit-image
、python-opencv
(cv2
)和SimpleITK
库函数演示它们基于 Python 的实现。让我们从导入本章所需的库开始:
import numpy as np
from skimage.transform import (hough_line, hough_line_peaks, hough_circle, hough_circle_peaks)
from skimage.draw import circle_perimeter
from skimage.feature import canny
from skimage.data import astronaut
from skimage.io import imread, imsave
from skimage.color import rgb2gray, gray2rgb, label2rgb
from skimage import img_as_float
from skimage.morphology import skeletonize
from skimage import data, img_as_float
import matplotlib.pyplot as pylab
from matplotlib import cm
from skimage.filters import sobel, threshold_otsu
from skimage.feature import canny
from skimage.segmentation import felzenszwalb, slic, quickshift, watershed
from skimage.segmentation import mark_boundaries, find_boundaries
Hough 变换-检测直线和圆
在图像处理中,Hough 变换是一种特征提取技术,旨在使用在参数空间中执行的投票过程来查找特定形状对象的实例。在其最简单的形式中,经典的 Hough 变换可用于检测图像中的直线。我们可以用极参数(ρ,θ)表示一条直线,其中ρ是线段的长度,θ是直线与x轴之间的角度。为了探索(ρ,θ)参数空间,它首先创建一个二维直方图。然后,对于ρ和θ的每个值,它计算输入图像中靠近对应线的非零像素数,并相应地增加位置(ρ,θ)处的数组。。。
阈值化与大津分割
阈值化是指使用像素值作为阈值,从灰度图像创建二值图像(只有黑白像素的图像)的一系列算法。它提供了从图像背景中分割对象的最简单方法。可以手动(通过查看像素值的直方图)或使用算法自动选择阈值。在scikit-image
中,有两类阈值算法实现,即基于直方图(使用像素强度直方图,并对该直方图的属性进行一些假设,例如双峰)和局部(仅使用相邻像素处理像素;这使得这些算法的计算成本更高)。
在本节中,我们将只讨论一种流行的基于直方图的阈值方法,称为Otsu 方法(假设为双峰直方图)。该方法通过同时最大化类间方差和最小化两类像素之间的类内方差来计算最佳阈值(由该阈值分隔)。下一个代码块演示了大津对马输入图像进行分割的实现,并计算最佳阈值以将前景与背景分开:
代码语言:javascript复制image = rgb2gray(imread('../images/horse.jpg'))
thresh = threshold_otsu(image)
binary = image > thresh
fig, axes = pylab.subplots(nrows=2, ncols=2, figsize=(20, 15))
axes = axes.ravel()
axes[0], axes[1] = pylab.subplot(2, 2, 1), pylab.subplot(2, 2, 2)
axes[2] = pylab.subplot(2, 2, 3, sharex=axes[0], sharey=axes[0])
axes[3] = pylab.subplot(2, 2, 4, sharex=axes[0], sharey=axes[0])
axes[0].imshow(image, cmap=pylab.cm.gray)
axes[0].set_title('Original', size=20), axes[0].axis('off')
axes[1].hist(image.ravel(), bins=256, normed=True)
axes[1].set_title('Histogram', size=20), axes[1].axvline(thresh, color='r')
axes[2].imshow(binary, cmap=pylab.cm.gray)
axes[2].set_title('Thresholded (Otsu)', size=20), axes[2].axis('off')
axes[3].axis('off'), pylab.tight_layout(), pylab.show()
下一个屏幕截图显示上一个代码块的输出;通过大津方法计算的最佳阈值由直方图中的红线标记,如下所示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-w0d9ogIa-1681961425707)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/handson-imgproc-py/img/42cd61c4-55d1-42ae-b3e4-71393f3dc192.png)]
基于边缘/基于区域的分割
此示例取自scikit-image
文档中的示例,演示了如何首先使用基于边缘的分割算法,然后使用基于区域的分割算法,从背景中分割对象。skimage.data
中的硬币图像用作输入图像,显示了在较暗背景下勾勒出的几个硬币。下一个代码块显示灰度图像及其强度直方图:
coins = data.coins()hist = np.histogram(coins, bins=np.arange(0, 256), normed=True)fig, axes = pylab.subplots(1, 2, figsize=(20, 10))axes[0].imshow(coins, cmap=pylab.cm.gray, interpolation='nearest')axes[0].axis('off'), axes[1].plot(hist[1][:-1], hist[0], lw=2)axes[1].set_title('histogram of gray values') ...
基于边缘的分割
在本例中,我们将尝试使用基于边缘的分割来描绘硬币的轮廓。为此,第一步是使用 Canny 边缘检测器获取特征的边缘,如下代码块所示:
代码语言:javascript复制edges = canny(coins, sigma=2)
fig, axes = pylab.subplots(figsize=(10, 6))
axes.imshow(edges, cmap=pylab.cm.gray, interpolation='nearest')
axes.set_title('Canny detector'), axes.axis('off'), pylab.show()
下一个屏幕截图显示了早期代码的输出,即使用 Canny 边缘检测器获得的硬币轮廓:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ve7vRoYG-1681961425707)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/handson-imgproc-py/img/ad5e9144-91a4-418b-9697-4dcc2d2b5c3a.png)]
下一步是使用scipy ndimage
模块的morphological
功能binary_fill_holes()
填充这些轮廓,如下一个代码块所示:
from scipy import ndimage as ndi
fill_coins = ndi.binary_fill_holes(edges)
fig, axes = pylab.subplots(figsize=(10, 6))
axes.imshow(fill_coins, cmap=pylab.cm.gray, interpolation='nearest')
axes.set_title('filling the holes'), axes.axis('off'), pylab.show()
下一个屏幕截图显示了早期代码块的输出,即硬币的填充轮廓:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wl4WqFvZ-1681961425707)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/handson-imgproc-py/img/b38e8811-7294-4fbe-8852-2f9ba8de261f.png)]
从屏幕截图可以看出,有一枚硬币的轮廓没有被操作填满。在下一步中,通过设置有效对象的最小尺寸,并再次使用morphological
功能,这一次使用scikit-image
形态学模块中的remove_small_objects()
功能,移除像这样的小伪对象:
from skimage import morphology
coins_cleaned = morphology.remove_small_objects(fill_coins, 21)
fig, axes = pylab.subplots(figsize=(10, 6))
axes.imshow(coins_cleaned, cmap=pylab.cm.gray, interpolation='nearest')
axes.set_title('removing small objects'), axes.axis('off'), pylab.show()
下一个屏幕截图显示了前面代码块的输出:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7YlIq0CT-1681961425708)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/handson-imgproc-py/img/fccf6fc0-10f6-4d8f-94cd-12af1d0ec666.png)]
然而,这种方法不是很稳健,因为没有完全闭合的轮廓没有正确填充,就像一枚未填充硬币的情况一样,正如前面的屏幕截图所示。
基于区域的分割
在本节中,我们将使用形态学分水岭算法对同一图像应用基于区域的分割方法。首先,让我们直观地讨论分水岭算法的基本步骤。
形态分水岭算法
任何灰度图像都可以视为地形表面。如果该曲面从其最小值淹没,并且阻止来自不同来源的水合并,则图像将被分割为两个不同的集合,即集水区和集水区线。如果将此变换应用于图像渐变,则汇水盆地理论上应与此图像的均匀灰度区域(分段)相对应。
然而,在实际应用中,由于梯度图像中存在噪声或局部不规则性,使用该变换对图像进行过度分割。为了防止过度分割,使用一组预定义的标记,曲面的泛洪从这些标记开始。因此,以下是通过分水岭变换分割图像的步骤:
- 找到标记和分割标准(用于分割区域的函数,通常是对比度/梯度)
- 使用这两个元素运行标记控制的分水岭算法
现在,让我们使用形态学分水岭算法的scikit-image
实现将前景硬币与背景分开。第一步是使用图像的sobel
梯度查找高程地图,如以下代码块所示:
elevation_map = sobel(coins)
fig, axes = pylab.subplots(figsize=(10, 6))
axes.imshow(elevation_map, cmap=pylab.cm.gray, interpolation='nearest')
axes.set_title('elevation map'), axes.axis('off'), pylab.show()
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bi5osBA6-1681961425708)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/handson-imgproc-py/img/df077e7c-154b-420b-ba7f-53cf0087955e.png)]
接下来,根据灰度值直方图的极端部分,计算背景和硬币的标记,如以下代码块所示:
代码语言:javascript复制markers = np.zeros_like(coins)
markers[coins < 30] = 1
markers[coins > 150] = 2
print(np.max(markers), np.min(markers))
fig, axes = pylab.subplots(figsize=(10, 6))
a = axes.imshow(markers, cmap=plt.cm.hot, interpolation='nearest')
plt.colorbar(a)
axes.set_title('markers'), axes.axis('off'), pylab.show()
下一个屏幕截图显示先前代码块的输出,即 markers 数组的热图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-l8afFsmu-1681961425708)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/handson-imgproc-py/img/4c51f27d-0964-4b50-bd9f-ac854e1b2233.png)]
最后,使用分水岭变换从确定的标记开始填充高程地图的区域,如下一个代码块中所示:
代码语言:javascript复制segmentation = morphology.watershed(elevation_map, markers)
fig, axes = pylab.subplots(figsize=(10, 6))
axes.imshow(segmentation, cmap=pylab.cm.gray, interpolation='nearest')
axes.set_title('segmentation'), axes.axis('off'), pylab.show()
下图显示了代码块的输出,即使用形态学分水岭算法生成的分割二值图像:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4nPo6DE6-1681961425708)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/handson-imgproc-py/img/4ff585e7-1302-4df2-80ef-0d7db1f52f3a.png)]
最后一种方法效果更好,硬币可以单独分割和标记,如以下代码块所示:
代码语言:javascript复制segmentation = ndi.binary_fill_holes(segmentation - 1)
labeled_coins, _ = ndi.label(segmentation)
image_label_overlay = label2rgb(labeled_coins, image=coins)
fig, axes = pylab.subplots(1, 2, figsize=(20, 6), sharey=True)
axes[0].imshow(coins, cmap=pylab.cm.gray, interpolation='nearest')
axes[0].contour(segmentation, [0.5], linewidths=1.2, colors='y')
axes[1].imshow(image_label_overlay, interpolation='nearest')
for a in axes:
a.axis('off')
pylab.tight_layout(), pylab.show()
下一个屏幕截图分别显示了此代码块的输出、带分水岭线(等高线)的分段硬币和带标签的硬币:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vWcQvk9o-1681961425709)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/handson-imgproc-py/img/ba24d0c6-e2dc-4892-98b1-ce96791f11ad.png)]
Felzenszwalb、SLIC、QuickShift 和紧凑分水岭算法
在本节中,我们将讨论四种流行的低级图像分割方法,然后将这些方法获得的结果与输入图像进行比较。好分割的定义往往取决于应用,因此很难获得好分割。这些方法通常用于获得过分割,也称为超像素。这些超像素然后作为更复杂算法的基础,例如与区域邻接图或条件随机场合并。
Felzenszwalb 基于图的高效图像分割
Felzenszwalb 的算法采用基于图形的分割方法。它首先构造一个无向图,其中图像像素作为顶点(要分割的集合),两个顶点之间的边的权重是相异性的某种度量(例如,强度的差异)。在基于图的方法中,将图像划分为多个部分的问题转化为在构造的图中查找连接的组件。同一组件中两个顶点之间的边应具有相对较低的权重,而不同组件中顶点之间的边应具有较高的权重。
该算法在时间上运行,在图的边数上几乎是线性的,在实践中也很快。该技术保留了低变率图像区域的细节,而忽略了高变率图像区域的细节。该算法有一个影响分段大小的单一比例参数。根据局部对比度的不同,分段的实际大小和数量可能会有很大差异。下面的代码块演示了如何使用scikit-image
分割模块对该算法的实现以及使用少量输入图像获得的输出分割图像:
from matplotlib.colors import LinearSegmentedColormap
for imfile in ['../images/eagle.png', '../images/horses.png', '../images/flowers.png', '../images/bisons.png']:
img = img_as_float(imread(imfile)[::2, ::2, :3])
pylab.figure(figsize=(20,10))
segments_fz = felzenszwalb(img, scale=100, sigma=0.5, min_size=400)
borders = find_boundaries(segments_fz)
unique_colors = np.unique(segments_fz.ravel())
segments_fz[borders] = -1
colors = [np.zeros(3)]
for color in unique_colors:
colors.append(np.mean(img[segments_fz == color], axis=0))
cm = LinearSegmentedColormap.from_list('pallete', colors, N=len(colors))
pylab.subplot(121), pylab.imshow(img), pylab.title('Original', size=20), pylab.axis('off')
pylab.subplot(122), pylab.imshow(segments_fz, cmap=cm),
pylab.title('Segmented with Felzenszwalbs's method', size=20), pylab.axis('off')
pylab.show()
下一个屏幕截图显示了代码块的输出、输入图像以及使用算法的相应分段输出图像:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-T0rEAhWi-1681961425709)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/handson-imgproc-py/img/56cea97f-9e82-4212-9a25-e33f97229582.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-swBn2gCi-1681961425709)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/handson-imgproc-py/img/4830c835-fc56-44d1-bc9c-2e58ab48a206.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8ml4tkSS-1681961425709)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/handson-imgproc-py/img/1cf49ae8-bead-41e9-b5dc-a6a3dd18a17b.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rAwd1tFb-1681961425709)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/handson-imgproc-py/img/1ca70ec2-a54f-4193-83fb-853a3c3cfff3.png)]
下面的代码块演示了当比例参数发生变化时,算法的结果是如何变化的:
代码语言:javascript复制def plot_image(img, title):
pylab.imshow(img), pylab.title(title, size=20), pylab.axis('off')
img = imread('../images/fish.jpg')[::2, ::2, :3]
pylab.figure(figsize=(15,10))
i = 1
for scale in [50, 100, 200, 400]:
plt.subplot(2,2,i)
segments_fz = felzenszwalb(img, scale=scale, sigma=0.5, min_size=200)
plot_image(mark_boundaries(img, segments_fz, color=(1,0,0)), 'scale=' str(scale))
i = 1
pylab.suptitle('Felzenszwalbs's method', size=30), pylab.tight_layout(rect=[0, 0.03, 1, 0.95])
pylab.show()
下一个屏幕截图显示了用于分割的输入鱼图像:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YBwPowcg-1681961425710)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/handson-imgproc-py/img/744334e4-f2ef-4709-aeae-821edaa575ea.png)]
下一个屏幕截图显示了前面代码块的输出。可以看出,输出图像中的分段数随着scale
参数值的增加而减少:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BNwzKCHn-1681961425710)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/handson-imgproc-py/img/c6d71195-ee76-4642-8840-49dd109f63ce.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cGvOIDRd-1681961425710)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/handson-imgproc-py/img/13cf1992-2a05-4a89-b64a-9666bd9c360c.png)]
SLIC
SLIC 算法只是在颜色空间(RGB 或 Lab)和图像位置(即像素坐标:x的五维空间中进行 k 均值聚类(我们将在第 9 章、图像处理中的经典机器学习方法中对该聚类算法进行更多探讨),y。该算法非常有效,因为聚类方法更简单。要使用该算法获得良好的结果,必须在实验室颜色空间中工作。该算法已迅速获得势头,目前已被广泛使用。紧致度参数权衡颜色相似性和接近性,而n_segments
参数选择 k-means 的中心数。下一个代码块演示如何使用scikit-image ...
实现该算法
碎布合并
在本节中,我们将讨论如何使用区域邻接图(RAG)组合图像的过分割区域,以获得更好的分割。它使用 SLIC 算法首先分割输入图像并获得区域标签。然后,它构造一个 RAG 并逐步合并颜色相似的过度分割区域。合并两个相邻区域产生一个新区域,其中包含合并区域中的所有像素。合并区域,直到不再保留高度相似的区域对:
代码语言:javascript复制from skimage import segmentation
from skimage.future import graph
def _weight_mean_color(graph, src, dst, n):
diff = graph.node[dst]['mean color'] - graph.node[n]['mean color']
diff = np.linalg.norm(diff)
return {'weight': diff}
def merge_mean_color(graph, src, dst):
graph.node[dst]['total color'] = graph.node[src]['total color']
graph.node[dst]['pixel count'] = graph.node[src]['pixel count']
graph.node[dst]['mean color'] = (graph.node[dst]['total color'] / graph.node[dst]['pixel count'])
img = imread('../images/me12.jpg')
labels = segmentation.slic(img, compactness=30, n_segments=400)
g = graph.rag_mean_color(img, labels)
labels2 = graph.merge_hierarchical(labels, g, thresh=35, rag_copy=False,
in_place_merge=True,
merge_func=merge_mean_color,
weight_func=_weight_mean_color)
out = label2rgb(labels2, img, kind='avg')
out = segmentation.mark_boundaries(out, labels2, (0, 0, 0))
pylab.figure(figsize=(20,10))
pylab.subplot(121), pylab.imshow(img), pylab.axis('off')
pylab.subplot(122), pylab.imshow(out), pylab.axis('off')
pylab.tight_layout(), pylab.show()
下一个屏幕截图显示代码的输出、输入图像和使用 RAG 获得的输出图像,合并 SLIC 段:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4T92BNGW-1681961425710)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/handson-imgproc-py/img/5d2537ba-6969-447a-a459-d0aae8857b6f.png)]
快速换档
QuickShift是一种二维图像分割算法,基于核化均值漂移算法的近似值,相对较新。它属于局部(非参数)模式搜索算法家族(基于将每个数据点关联到潜在概率密度函数模式的思想),并应用于由颜色空间和图像位置组成的 5D 空间。
QuickShift 实际上同时计算多个尺度的分层分割,这是该算法的优点之一。QuickShift 有两个主要参数:参数 sigma 控制局部密度近似的比例,其中参数max_dist
选择分层分割中的一个级别。。。
紧凑流域
如前所述,分水岭算法计算从给定标记淹没的图像中的分水岭流域,将像素分配到标记流域中。该算法需要灰度梯度图像作为输入(将图像视为风景),其中明亮的像素表示区域之间的边界(形成高峰)。根据给定的标记,该景观随后被淹没,直到不同的洪水流域在山峰处汇合。每个不同的盆地形成不同的图像段。正如我们在 SLIC 中所做的那样,还有一个额外的紧凑性论证,使得标记更难淹没远处的像素。紧凑度值越高,流域区域的形状越规则。下一个代码块演示如何使用该算法的scikit-image
实现。它还显示了更改标记和紧致度参数对分割结果的影响:
from skimage.segmentation import watershed
gradient = sobel(rgb2gray(img))
pylab.figure(figsize=(15,10))
i = 1
for markers in [200, 1000]:
for compactness in [0.001, 0.0001]:
pylab.subplot(2,2,i)
segments_watershed = watershed(gradient, markers=markers, compactness=compactness)
plot_image(mark_boundaries(img, segments_watershed, color=(1,0,0), 'markers=' str(markers) '.compactness=' str(compactness))
i = 1
pylab.suptitle('Compact watershed', size=30), pylab.tight_layout(rect=[0, 0.03, 1, 0.95]), pylab.show()
下一个屏幕截图显示了代码块的输出。可以看出,紧凑度值越高,流域形状越规则,而markers
参数值越高,则会导致过度分割:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-m6FiF9Gk-1681961425711)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/handson-imgproc-py/img/c71ebb92-8cf2-4b4d-9136-7e585b42e518.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ikzLZTr4-1681961425711)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/handson-imgproc-py/img/218a6ad2-1fda-4128-88cb-802780326abe.png)]
用 simpletk 进行区域生长
区域增长指的是一类分割算法,其中,如果像素的邻域强度与当前像素相似,则将其视为在同一段中。相似性的定义因算法而异。初始像素集称为种子点,通常是手动选择的。下一个代码块演示如何使用SimpleITK
库实现ConnectedThreshold
(在区域增长分割算法的变体上)。使用颅骨 MRI 扫描(T1
医学图像作为输入图像。在ConnectedThreshold
算法的情况下,如果相邻体素的强度在。。。
活动轮廓、形态蛇和 GrabCut 算法
在本节中,我们将讨论一些更复杂的分割算法,并用scikit-image
或python-opencv
(cv2
)库函数演示它们。我们将从使用活动轮廓进行分割开始
活动等高线
活动轮廓模型(也称为蛇)是一种将开放或闭合样条曲线与图像中的线条或边缘相匹配的框架。蛇是一种能量最小化、可变形的样条曲线,受约束、图像和内力的影响。因此,它通过最小化部分由图像定义,部分由样条曲线的形状、长度和平滑度定义的能量来工作。约束力和图像力将蛇拉向对象轮廓,内力抵抗变形。该算法接受一条初始蛇(围绕感兴趣的对象),为了使闭合轮廓适合感兴趣的对象,它会收缩/扩展。最小化显式地在图像能量中进行,隐式地在形状能量中进行。作为。。。
形态蛇
形态学蛇是指一系列用于图像分割的方法(类似于活动轮廓)。然而,形态蛇比活动轮廓更快,数值更稳定,因为它们在二进制数组上使用形态算子(例如,膨胀/侵蚀),而活动轮廓在浮点数组上解决偏微分方程。在scikit-image
实现中有两种形态蛇方法可用,即形态测地活动轮廓*(形态 GAC带morphological_geodesic_active_contour()
和形态活动轮廓无边缘**(*形态蛇带morphological_chan_vese()
*MorphGAC 适用于轮廓可见(可能有噪声、杂乱或部分不清晰)的图像,需要对图像进行预处理以突出轮廓。此预处理可通过inverse_gaussian_gradient()
*功能完成。这个预处理步骤对 Morphac 分割的质量有很大的影响。相反,当要分割的对象的内部和外部区域的像素值具有不同的平均值时,MorphACWE 工作得很好。它与原始图像一起工作,不需要任何预处理,也不需要对对象的轮廓进行测量定义。因此,MorphACWE 比 MorphacGac 更易于使用和调整。下一个代码块演示了如何使用这些函数实现形态蛇。它还显示了算法的演变以及在不同迭代中获得的分段:
from skimage.segmentation import (morphological_chan_vese, morphological_geodesic_active_contour,
inverse_gaussian_gradient, checkerboard_level_set)
def store_evolution_in(lst):
"""Returns a callback function to store the evolution of the level sets in
the given list.
"""
def _store(x):
lst.append(np.copy(x))
return _store
# Morphological ACWE
image = imread('../images/me14.jpg')
image_gray = rgb2gray(image)
# initial level set
init_lvl_set = checkerboard_level_set(image_gray.shape, 6)
# list with intermediate results for plotting the evolution
evolution = []
callback = store_evolution_in(evolution)
lvl_set = morphological_chan_vese(image_gray, 30, init_level_set=init_lvl_set, smoothing=3, iter_callback=callback)
fig, axes = pylab.subplots(2, 2, figsize=(8, 6))
axes = axes.flatten()
axes[0].imshow(image, cmap="gray"), axes[0].set_axis_off(), axes[0].contour(lvl_set, [0.5], colors='r')
axes[0].set_title("Morphological ACWE segmentation", fontsize=12)
axes[1].imshow(lvl_set, cmap="gray"), axes[1].set_axis_off()
contour = axes[1].contour(evolution[5], [0.5], colors='g')
contour.collections[0].set_label("Iteration 5")
contour = axes[1].contour(evolution[10], [0.5], colors='y')
contour.collections[0].set_label("Iteration 10")
contour = axes[1].contour(evolution[-1], [0.5], colors='r')
contour.collections[0].set_label("Iteration " str(len(evolution)-1))
axes[1].legend(loc="upper right"), axes[1].set_title("Morphological ACWE evolution", fontsize=12)
# Morphological GAC
image = imread('images/fishes4.jpg')
image_gray = rgb2gray(image)
gimage = inverse_gaussian_gradient(image_gray)
# initial level set
init_lvl_set = np.zeros(image_gray.shape, dtype=np.int8)
init_lvl_set[10:-10, 10:-10] = 1
# list with intermediate results for plotting the evolution
evolution = []
callback = store_evolution_in(evolution)
lvl_set = morphological_geodesic_active_contour(gimage, 400, init_lvl_set, smoothing=1, balloon=-1,
threshold=0.7, iter_callback=callback)
axes[2].imshow(image, cmap="gray"), axes[2].set_axis_off(), axes[2].contour(lvl_set, [0.5], colors='r')
axes[2].set_title("Morphological GAC segmentation", fontsize=12)
axes[3].imshow(lvl_set, cmap="gray"), axes[3].set_axis_off()
contour = axes[3].contour(evolution[100], [0.5], colors='g')
contour.collections[0].set_label("Iteration 100")
contour = axes[3].contour(evolution[200], [0.5], colors='y')
contour.collections[0].set_label("Iteration 200")
contour = axes[3].contour(evolution[-1], [0.5], colors='r')
contour.collections[0].set_label("Iteration " str(len(evolution)-1))
axes[3].legend(loc="upper right"), axes[3].set_title("Morphological GAC evolution", fontsize=12)
fig.tight_layout(), pylab.show()
下一个屏幕截图显示了代码的输出:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-W281lbXI-1681961425711)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/handson-imgproc-py/img/5e97468f-6773-416a-87f5-1ad89622cc00.png)]
带 OpenCV 的 grabbut
GrabCut是一种交互式分割方法,使用图论最大流/最小割算法从图像背景中提取前景。在算法开始之前,用户首先需要提供一些提示,在输入图像中大致指定前景区域,尽可能减少交互(例如,在前景区域周围绘制一个矩形)。然后,该算法迭代分割图像以获得最佳结果。在某些情况下,分割可能不是期望的(例如,算法可能已将某些前景区域标记为背景,反之亦然)。
在这种情况下,用户需要通过在图像上进行一些笔划来再次进行精细润色。。。
总结
在本章中,我们讨论了图像分割,并用 Python 库演示了不同的算法,如scikit-image
、opencv (cv2)
和SimpleITK
。我们从使用 Hough 变换的图像中的直线和圆检测开始,并展示了如何将其用于图像分割的示例。接下来,我们讨论了 Otsu 的阈值算法,以找到分割的最佳阈值。然后介绍了基于边缘和基于区域的分割算法以及用于图像分割的形态学分水岭算法。在下一节中,将讨论更多的分割算法,如 Felzenszwalb 的基于图的算法、区域增长、SLIC 和 QuickShift,以及使用scikit-image
的实现。最后,我们讨论了一些更复杂的分割算法,如 GrabCut、活动轮廓和形态蛇
在下一章中,我们将讨论图像处理中的机器学习技术,并将更多地讨论使用 k-means 聚类和 meanshift 算法作为无监督机器学习算法的图像分割。我们还将在后面的深入学习章节中讨论语义分割技术。
问题
- 使用 Hough 变换从带有
scikit-image
椭圆的图像中检测椭圆。 - 使用
scikit-image
转换模块的probabilistic_hough_line()
功能从图像中检测线条。它与hough_line()
有何不同? - 使用
scikit-image
过滤器模块的try_all_threshold()
功能比较不同类型的局部阈值技术,将灰度图像分割为二值图像。 - 使用
ConfidenceConnected
和VectorConfidenceConnected
算法对使用SimpleITK
的 MRI 扫描图像进行分割。 - 在前景对象周围使用正确的边框,使用 GrabCut 算法分割鲸鱼图像。
- 使用
scikit-image
分割模块的random_walker()
功能分割图像开始。。。
进一步阅读
- http://scikit-image.org/docs/dev/user_guide/tutorial_segmentation.html
- https://www.scipy-lectures.org/packages/scikit-image/index.html
- http://scikit-image.org/docs/dev/auto_examples/
- https://web.stanford.edu/class/ee368/Handouts/Lectures/2014_Spring/Combined_Slides/6-Image-Segmentation-Combined.pdf
- https://courses.csail.mit.edu/6.869/lectnotes/lect19/lect19-slides-6up.pdf
- http://cmm.ensmp.fr/~beucher/publi/WTS_.pdf
- http://cmm.ensmp.fr/~beucher/wtshed.html
- https://sandipanweb.wordpress.com/2018/02/25/graph-based-image-segmentation-in-python/
- https://sandipanweb.wordpress.com/2018/02/11/interactive-image-segmentation-with-graph-cut/
- http://people.cs.uchicago.edu/~pff/papers/seg-ijcv.pdf
- http://www.kev-smith.com/papers/SMITH_TPAMI12.pdf**
九、图像处理中的经典机器学习方法
在本章中,我们将讨论机器学习技术在图像处理中的应用。我们将定义机器学习,并学习机器学习的两种算法,有监督和无监督。然后,我们将继续讨论一些流行的无监督机器学习技术的应用,如聚类,以及图像分割等问题。
我们还将研究监督机器学习技术在图像分类和目标检测等问题上的应用。我们将使用一个非常流行的库 scikit learn,以及 scikit image 和 Python OpenCV(cv2)来实现用于图像处理的机器学习算法。这
有监督与无监督学习
机器学习算法主要有两种类型:
- 监督学习:在这种类型的学习中,我们会得到一个带有正确标签的输入数据集,我们需要学习输入和输出之间的关系(作为函数)。手写数字分类问题是监督(分类)问题的一个例子。
- 无监督学习:在这种类型的学习中,我们几乎不知道或根本不知道我们的输出应该是什么样子。我们可以从不一定知道变量影响的数据中得出结构。在图像处理技术中,聚类就是一个例子,在这种技术中,我们不知道哪个像素属于哪个片段。
一个计算机程序据说是从经验中学习的,E,关于一些任务,T和一些性能度量,P,如果它在T上的性能,如P所测量的,随着经验的增加而提高,E。
例如,假设我们得到一组手写数字图像及其标签(数字从零到九),我们需要编写一个 Python 程序,学习图像和标签之间的关联(如经验E),然后自动标记一组新的手写数字图像。
在这种情况下,任务T是将标签分配给图像(即,对数字图像进行分类或识别)。正确识别的新图像集的比例将是性能P(准确度)。是节目的一部分。在这种情况下,该计划可以说是一个学习计划。
在本章中,我们将描述一些可以使用机器学习算法(无监督或有监督)解决的图像处理问题。我们将从学习两种无监督机器学习技术在解决图像处理问题中的应用开始。
无监督机器学习–聚类、PCA 和特征脸
在本节中,我们将讨论几种流行的机器学习算法及其在图像处理中的应用。让我们从两个聚类算法及其在颜色量化和图像分割中的应用开始。我们将使用 scikit 学习库的实现来实现这些集群算法。
基于颜色量化的 K-均值聚类图像分割
在本节中,我们将演示如何对 pepper 图像执行像素级矢量量化(VQ),将显示图像所需的颜色数量从 250 种唯一颜色减少到 4 种颜色,同时保持整体外观质量。在本例中,像素在三维空间中表示,k-均值用于查找四个颜色簇。
在图像处理文献中,码本是从 k-means(聚类中心)获得的,称为调色板。在调色板中,使用单个字节,最多可以寻址 256 种颜色,而 RGB 编码需要每个像素 3 个字节。GIF 文件格式使用这样的调色板。为了进行比较,我们还将看到使用随机码本(随机拾取的颜色)的量化图像。
让我们使用 k-means 聚类来分割图像,首先让我们加载所需的库和输入图像:
代码语言:javascript复制import numpy as np
import matplotlib.pyplot as plt
from sklearn.cluster import KMeans
from sklearn.metrics import pairwise_distances_argmin
from skimage.io import imread
from sklearn.utils import shuffle
from skimage import img_as_float
from time import time
pepper = imread("../images/pepper.jpg")
# Display the original image
plt.figure(1), plt.clf()
ax = plt.axes([0, 0, 1, 1])
plt.axis('off'), plt.title('Original image (%d colors)' %(len(np.unique(pepper)))), plt.imshow(pepper)
使用的原始输入胡椒图像如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-R7iWFXYB-1681961425711)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/handson-imgproc-py/img/4438ae95-62fc-408e-b4bd-b350c31c9d8d.png)]
现在,让我们用 k-均值聚类法分割图像:
代码语言:javascript复制n_colors = 64
# Convert to floats instead of the default 8 bits integer coding. Dividing by
# 255 is important so that plt.imshow behaves works well on float data (need to
# be in the range [0-1])
pepper = np.array(pepper, dtype=np.float64) / 255
# Load Image and transform to a 2D numpy array.
w, h, d = original_shape = tuple(pepper.shape)
assert d == 3
image_array = np.reshape(pepper, (w * h, d))
def recreate_image(codebook, labels, w, h):
"""Recreate the (compressed) image from the code book & labels"""
d = codebook.shape[1]
image = np.zeros((w, h, d))
label_idx = 0
for i in range(w):
for j in range(h):
image[i][j] = codebook[labels[label_idx]]
label_idx = 1
return image
# Display all results, alongside original image
plt.figure(1)
plt.clf()
ax = plt.axes([0, 0, 1, 1])
plt.axis('off')
plt.title('Original image (96,615 colors)')
plt.imshow(pepper)
plt.figure(2, figsize=(10,10))
plt.clf()
i = 1
for k in [64, 32, 16, 4]:
t0 = time()
plt.subplot(2,2,i)
plt.axis('off')
image_array_sample = shuffle(image_array, random_state=0)[:1000]
kmeans = KMeans(n_clusters=k, random_state=0).fit(image_array_sample)
print("done in %0.3fs." % (time() - t0))
# Get labels for all points
print("Predicting color indices on the full image (k-means)")
t0 = time()
labels = kmeans.predict(image_array)
print("done in %0.3fs." % (time() - t0))
plt.title('Quantized image (' str(k) ' colors, K-Means)')
plt.imshow(recreate_image(kmeans.cluster_centers_, labels, w, h))
i = 1
plt.show()
plt.figure(3, figsize=(10,10))
plt.clf()
i = 1
for k in [64, 32, 16, 4]:
t0 = time()
plt.subplot(2,2,i)
plt.axis('off')
codebook_random = shuffle(image_array, random_state=0)[:k 1]
print("Predicting color indices on the full image (random)")
t0 = time()
labels_random = pairwise_distances_argmin(codebook_random,
image_array,
axis=0)
print("done in %0.3fs." % (time() - t0))
plt.title('Quantized image (' str(k) ' colors, Random)')
plt.imshow(recreate_image(codebook_random, labels_random, w, h))
i = 1
plt.show()
下图显示了上述代码的输出。可以看出,就保留的图像质量而言,k-means 聚类在颜色量化方面总是比使用随机码本做得更好:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EIN9KYna-1681961425712)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/handson-imgproc-py/img/1223c30f-1575-4e53-9eb4-9aa0ad6d9724.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IMJVkEb3-1681961425712)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/handson-imgproc-py/img/ee481380-aca9-4f35-8cb0-2f078eff6f7a.png)]
用于图像分割的谱聚类方法
在本节中,我们将演示如何将光谱聚类技术用于图像分割。在这些设置中,光谱聚类方法解决了称为归一化图切割的问题。图像被视为连通像素的图,光谱聚类算法相当于选择定义区域的图切割,同时最小化沿切割的梯度与区域体积的比率。SpectralClustering()
从 scikit 学习集群模块将用于将图像分割为前景和背景。
将结果与使用 k-均值聚类获得的二进制分割进行比较,如以下代码所示:
代码语言:javascript复制from sklearn import cluster ...
PCA 与特征脸
主成分分析(PCA是一种统计/无监督机器学习技术,它使用正交变换将一组可能相关变量的观测值转换为一组称为主成分的线性不相关变量值,从而找到数据集中(沿主成分)的最大方差方向。
这可以用于(线性)降维(大多数时候只有少数主要主成分捕获数据集中几乎所有的方差)和具有多个维度的数据集的可视化(2D)。PCA 的一个应用是特征面,以找到一组可以(理论上)表示任何面(作为这些特征面的线性组合)的面。
基于 PCA 的降维与可视化
在本节中,我们将使用 scikit learn 的 digits 数据集,该数据集包含 1797 幅手写数字图像(每幅 8 x 8 像素)。每行表示数据矩阵中的一个图像。让我们首先使用以下代码块加载并显示数据集中的前 25 位数字:
代码语言:javascript复制import numpy as npimport matplotlib.pylab as plt from sklearn.datasets import load_digits from sklearn.preprocessing import StandardScalerfrom sklearn.decomposition import PCAfrom sklearn.pipeline import Pipelinedigits = load_digits() #print(digits.keys())print(digits.data.shape)j = 1np.random.seed(1)fig = plt.figure(figsize=(3,3)) fig.subplots_adjust(left=0, right=1, bottom=0, top=1, hspace=0.05, wspace=0.05) for i ...
二维投影与可视化
从加载的数据集可以看出,它是一个 64 维数据集。现在,让我们使用 scikit learn 的PCA()
函数找到该数据集的两个主要组成部分,并沿着这两个维度投影数据集,然后使用 Matplotlib 散点绘制投影数据,每个数据点代表一个图像(一个数字),数字标签由唯一的颜色表示,使用以下代码块:
pca_digits=PCA(2)
digits.data_proj = pca_digits.fit_transform(digits.data)
print(np.sum(pca_digits.explained_variance_ratio_))
# 0.28509364823696987
plt.figure(figsize=(15,10))
plt.scatter(digits.data_proj[:, 0], digits.data_proj[:, 1], lw=0.25, c=digits.target, edgecolor='k', s=100, cmap=plt.cm.get_cmap('cubehelix', 10))
plt.xlabel('PC1', size=20), plt.ylabel('PC2', size=20), plt.title('2D Projection of handwritten digits with PCA', size=25)
plt.colorbar(ticks=range(10), label='digit value')
plt.clim(-0.5, 9.5)
下面的屏幕截图显示了输出。可以看出,即使在沿 PCs 的 2D 投影中,数字也有些分离(尽管有一些重叠),相同的数字值出现在集群附近:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vJrpnaya-1681961425712)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/handson-imgproc-py/img/41ef42bb-a4bb-464f-874f-7791c8fa19ea.png)]
PCA 特征脸
让我们首先从 scikit learn 加载 olivetti 人脸数据集;它包含 400 张人脸图像,每张图像的尺寸为 64 x 64 像素。
以下代码块显示了数据集中的几个随机面:
代码语言:javascript复制from sklearn.datasets import fetch_olivetti_faces faces = fetch_olivetti_faces().dataprint(faces.shape) # there are 400 faces each of them is of 64x64=4096 pixelsfig = plt.figure(figsize=(5,5)) fig.subplots_adjust(left=0, right=1, bottom=0, top=1, hspace=0.05, wspace=0.05) # plot 25 random facesj = 1np.random.seed(0)for i in np.random.choice(range(faces.shape[0]), 25): ax = fig.add_subplot(5, 5, j, xticks=[], yticks=[]) ax.imshow(np.reshape(faces[i,:],(64,64)), cmap=plt.cm.bone, interpolation='nearest') j = 1plt.show() ...
特征脸
鉴于 PCA 的特性,计算出的 PC 相互正交,每个 PC 包含 4096 个像素,并且可以重塑为 64 x 64 图像。这些主分量称为特征面(因为它们也是特征向量)。
可以看出,它们表示面的某些属性。以下代码块显示一些计算出的特征面:
代码语言:javascript复制fig = plt.figure(figsize=(5,2))
fig.subplots_adjust(left=0, right=1, bottom=0, top=1, hspace=0.05, wspace=0.05)
# plot the first 10 eigenfaces
for i in range(10):
ax = fig.add_subplot(2, 5, i 1, xticks=[], yticks=[])
ax.imshow(np.reshape(pipeline.named_steps['pca'].components_[i,:], (64,64)), cmap=plt.cm.bone, interpolation='nearest')
以下屏幕截图显示了前 10 个特征面上前面代码块的输出:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zxeE6L1P-1681961425712)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/handson-imgproc-py/img/cfc9419a-c69f-4e10-9e3b-bf8cf7d36b4c.png)]
重建
下面的代码块演示了如何将每个面近似表示为仅 64 个主要特征面的线性组合。scikit learn 的inverse_transform()
函数用于返回原始空间,但仅使用这 64 个主要特征面,丢弃所有其他特征面:
# face reconstructionfaces_inv_proj = pipeline.named_steps['pca'].inverse_transform(faces_proj) #reshaping as 400 images of 64x64 dimension fig = plt.figure(figsize=(5,5)) fig.subplots_adjust(left=0, right=1, bottom=0, top=1, hspace=0.05, wspace=0.05) # plot the faces, each image is 64 by 64 dimension but 8x8 pixels j = 1np.random.seed(0)for i in np.random.choice(range(faces.shape[0]), 25):
代码语言:javascript复制 ax = fig.add_subplot(5, ...
特征分解
每个面可以表示为 64 个特征面的线性组合。对于不同的人脸图像,每个特征脸将具有不同的权重(载荷)。下面的屏幕截图显示了如何用特征面表示人脸,并显示了前几个相应的权重。代码留给读者作为练习:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5w6K84EA-1681961425713)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/handson-imgproc-py/img/f72c60b2-102e-4e9e-8ca4-c344a920df3b.png)]
监督机器学习-图像分类
在本节中,我们将讨论图像分类问题。我们将使用的输入数据集是 MNIST(http://yann.lecun.com/exdb/mnist/ ),这是机器学习中的经典数据集,由手写数字的 28 x 28 灰度图像组成。
原始训练数据集包含 60000 个示例(手写数字图像和标签用于训练机器学习模型),测试数据集包含 10000 个示例(手写数字图像和标签作为基本事实,用于测试学习模型的准确性)。给定一组手写数字和图像及其标签(0-9),目标将是学习一个机器学习模型,该模型可以。。。
下载 MNIST(手写数字)数据集
让我们从下载 MNIST 数据集开始。以下 Python 代码向您展示了如何下载培训和测试数据集:
代码语言:javascript复制# Function that downloads a specified MNIST data file from Yann Le Cun's website
def download(filename, source='http://yann.lecun.com/exdb/mnist/'):
print("Downloading %s" % filename)
urlretrieve(source filename, filename)
# Invokes download() if necessary, then reads in images
def load_mnist_images(filename):
if not os.path.exists(filename):
download(filename)
with gzip.open(filename, 'rb') as f:
data = np.frombuffer(f.read(), np.uint8, offset=16)
data = data.reshape(-1,784)
return data
def load_mnist_labels(filename):
if not os.path.exists(filename):
download(filename)
with gzip.open(filename, 'rb') as f:
data = np.frombuffer(f.read(), np.uint8, offset=8)
return data
## Load the training set
train_data = load_mnist_images('train-images-idx3-ubyte.gz')
train_labels = load_mnist_labels('train-labels-idx1-ubyte.gz')
## Load the testing set
test_data = load_mnist_images('t10k-images-idx3-ubyte.gz')
代码语言:javascript复制test_labels = load_mnist_labels('t10k-labels-idx1-ubyte.gz')
print(train_data.shape)
# (60000, 784) ## 60k 28x28 handwritten digits
print(test_data.shape)
# (10000, 784) ## 10k 2bx28 handwritten digits
可视化数据集
每个数据点存储为 784 维向量。要可视化数据点,我们首先需要将其重塑为 28 x 28 的图像。以下代码段显示了如何显示测试数据集中的几个手写数字:
代码语言:javascript复制## Define a function that displays a digit given its vector representationdef show_digit(x, label): pylab.axis('off') pylab.imshow(x.reshape((28,28)), cmap=pylab.cm.gray) pylab.title('Label ' str(label)) pylab.figure(figsize=(10,10))for i in range(25): pylab.subplot(5, 5, i 1) show_digit(test_data[i,], test_labels[i])pylab.tight_layout()pylab.show()
下面的屏幕截图显示了来自测试数据集的前 25 个手写数字以及它们的基本事实(true)标签。kNN 分类器。。。
训练 kNN、高斯贝叶斯和 SVM 模型对 MNIST 进行分类
我们将使用 scikit 学习库函数实现以下分类器集:
- K 近邻
- 高斯贝叶斯分类器(生成模型)
- 支持向量机(SVM分类器
让我们从 k-最近邻分类器开始
k-最近邻(KNN)分类器
在本节中,我们将构建一个分类器,用于获取手写数字的图像,并使用一种称为最近邻分类器的特别简单的策略输出标签(0-9)。预测看不见的测试数字图像的想法非常简单。首先,我们需要从训练数据集中找到最接近该测试图像的k实例。接下来,我们需要简单地使用多数投票来计算测试图像的标签,即,来自 k 个最近训练数据点的大多数数据点所具有的标签将被分配给测试图像(任意断开连接)。
欧氏距离的平方
要计算数据集中的最近邻,我们首先需要能够计算数据点之间的距离。自然距离函数是欧几里得距离;对于两个向量x,y∈ Rd,其欧氏距离定义如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jajx51KW-1681961425713)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/handson-imgproc-py/img/303feb25-c74c-4ef0-872f-cb2af8c96136.png)]
通常,我们忽略平方根,只计算平方欧氏距离。为了进行最近邻计算,这两个向量是等价的:对于三个向量 x,y,z∈ Rd我们有∥x−Y∥ ≤ ∥x−Z∥ 当且仅当∥x−Y∥ 2≤ ∥x−Z∥ 2。现在我们只需要计算平方欧几里德距离
计算最近邻
k-最近邻的简单实现将扫描每个测试图像的每个训练图像。以这种方式执行最近邻分类将需要完全通过训练集才能对单个点进行分类。如果Rd中有N个训练点,这需要*O(Nd)*时间,这将非常缓慢。幸运的是,如果我们愿意花一些时间预处理训练集,有更快的方法来执行最近邻查找。scikit 学习库快速实现了两种有用的最近邻数据结构:ball 树和 k-d 树。下面的代码演示如何在训练时创建 ball 树数据结构,然后将其用于。。。
评估分类器的性能
接下来,我们需要在测试数据集上评估分类器的性能。以下代码段显示了如何执行此操作:
代码语言:javascript复制# evaluate the classifier
t_accuracy = sum(test_predictions == test_labels) / float(len(test_labels))
t_accuracy
# 0.96909999999999996
import pandas as pd
import seaborn as sn
from sklearn import metrics
cm = metrics.confusion_matrix(test_labels,test_predictions)
df_cm = pd.DataFrame(cm, range(10), range(10))
sn.set(font_scale=1.2)#for label size
sn.heatmap(df_cm, annot=True,annot_kws={"size": 16}, fmt="g")
以下屏幕截图显示了分类的混淆矩阵;我们可以看到有一些错误分类的测试图像,训练数据集的总体准确率为 96.9%:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Zh8p17JK-1681961425713)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/handson-imgproc-py/img/7441888f-97d3-4812-8ffe-85356389cd0c.png)]
以下屏幕截图显示:
- 成功案例:1-NN 预测标签=真标签=0
- 故障案例:1-NN 预测标签=2,真标签=3
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7hOh1S5B-1681961425713)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/handson-imgproc-py/img/95df50c8-812b-4ec3-bce4-49e0074739e2.png)]
找到预测的成功和失败案例的代码留给读者作为练习。
贝叶斯分类器(高斯生成模型)
正如我们在上一节中所看到的,1-NN 分类器在 MNIST 手写数字数据集上产生了 3.09%的测试错误率。在本节中,我们将构建一个高斯生成模型,该模型几乎同样有效,同时速度更快,结构更紧凑。同样,我们需要首先加载 MNIST 训练和测试数据集,就像上次一样。接下来,让我们将高斯生成模型拟合到训练数据集。
培训生成模型-计算高斯参数的最大似然估计
下面的代码块定义了一个函数fit_generative_model()
,该函数接受一个训练集(x
数据和y
标签)作为输入,并将高斯生成模型拟合到它。它为每个标签返回此生成模型的以下参数,j=0,1,…,9,我们有以下内容:
- πj:标签的频率(即之前的频率)
- μj:784 维平均向量
- ∑ j:784 x 784 协方差矩阵
这意味着π是 10 x 1,μ是 10 x 784,∑ 是一个 10 x 784 x 784 矩阵。参数的最大似然估计(MLE为经验估计,如下图所示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hHlkqZtn-1681961425714)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/handson-imgproc-py/img/c72ddab5-de26-4160-98b4-43d4be1d87ad.png)]
经验协方差很可能是奇异的(或接近奇异的),这意味着我们无法用它们进行计算。因此,对这些矩阵进行正则化非常重要。这样做的标准方法是将cI添加到它们中,其中c是某个常数,I是 784 维单位矩阵(换句话说,我们计算经验协方差,然后将它们的对角项增加某个常数c*
这种修改保证为任何c>0产生非奇异协方差矩阵,无论多么小。现在,c成为(正则化)参数,通过适当设置,我们可以提高模型的性能。我们应该选择一个好的值c。重要的是,这需要单独使用训练集来完成,将训练集的一部分作为验证集或使用某种交叉验证,我们将此作为练习留给读者完成。特别是,display_char()
函数将用于可视化前三位的高斯平均值:
def display_char(image):
plt.imshow(np.reshape(image, (28,28)), cmap=plt.cm.gray)
plt.axis('off')
plt.show()
def fit_generative_model(x,y):
k = 10 # labels 0,1,...,k-1
d = (x.shape)[1] # number of features
mu = np.zeros((k,d))
sigma = np.zeros((k,d,d))
pi = np.zeros(k)
c = 3500 #10000 #1000 #100 #10 #0.1 #1e9
for label in range(k):
indices = (y == label)
pi[label] = sum(indices) / float(len(y))
mu[label] = np.mean(x[indices,:], axis=0)
sigma[label] = np.cov(x[indices,:], rowvar=0, bias=1) c*np.eye(d)
return mu, sigma, pi
mu, sigma, pi = fit_generative_model(train_data, train_labels)
display_char(mu[0])
display_char(mu[1])
display_char(mu[2])
以下屏幕截图显示了前面代码块的输出以及前三位的平均值 MLE:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2jjcaJU6-1681961425714)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/handson-imgproc-py/img/bb70235c-4433-4b32-a89f-47708aedada7.png)]
计算后验概率对试验数据和模型评估进行预测
为了预测新图像的标签x,我们需要找到标签j,其后验概率*Pr(y=j|x)*最大。可使用贝叶斯规则计算,如下所示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-usMGNd8q-1681961425714)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/handson-imgproc-py/img/1bc55df6-8499-48c5-ab61-4d2cdc800d62.png)]
下面的代码块显示了如何使用生成模型预测测试数据集的标签,以及如何计算模型在测试数据集上产生的错误数。可以看出,测试数据集的精度为 95.6%,略低于 1-NN 分类器:
代码语言:javascript复制# Compute log Pr(label|image) for each [test image,label] pair.k = 10score ...
分类器
在本节中,我们将使用 MNIST 训练数据集训练(多类)SVM 分类器,然后使用它从 MNIST 测试数据集预测图像的标签。
SVM 是一种非常复杂的二元分类器,它使用二次规划来最大化分离超平面之间的边界。利用 1-vs-all 或 1-vs-1 技术,将二元 SVM 分类器扩展到处理多类分类问题。我们将使用 scikit learn 的实现SVC()
,使用多项式核(2 次)拟合(训练)训练数据集的软边界(核化)SVM 分类器,然后使用score()
函数预测测试图像的标签。
下面的代码显示了如何使用 MNIST 数据集训练、预测和评估 SVM 分类器。可以看出,使用该分类器在测试数据集上获得的准确度已增加到 98%:
代码语言:javascript复制from sklearn.svm import SVC
clf = SVC(C=1, kernel='poly', degree=2)
clf.fit(train_data,train_labels)
print(clf.score(test_data,test_labels))
# 0.9806
test_predictions = clf.predict(test_data)
cm = metrics.confusion_matrix(test_labels,test_predictions)
df_cm = pd.DataFrame(cm, range(10), range(10))
sn.set(font_scale=1.2)
sn.heatmap(df_cm, annot=True,annot_kws={"size": 16}, fmt="g")
以下屏幕截图显示了分类混淆矩阵的输出:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-F96QhIQl-1681961425714)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/handson-imgproc-py/img/6a974cfa-9114-45ae-b709-07ad76516614.png)]
接下来,让我们找到一个 SVM 分类器预测了错误标签的测试图像(与地面真实值标签不同)。
以下代码查找此类图像,并将其与预测和真实标签一起显示:
代码语言:javascript复制wrong_indices = test_predictions != test_labels
wrong_digits, wrong_preds, correct_labs = test_data[wrong_indices], test_predictions[wrong_indices], test_labels[wrong_indices]
print(len(wrong_pred))
# 194
pylab.title('predicted: ' str(wrong_preds[1]) ', actual: ' str(correct_labs[1]))
display_char(wrong_digits[1])
下面的屏幕截图显示了输出。可以看出,测试图像具有真实标签 2,但图像看起来更像 7,因此 SVM 预测为 7:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mR1jh4iR-1681961425715)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/handson-imgproc-py/img/96c03dfd-6e0f-41d7-a830-b7a5dd669d47.png)]
监督机器学习-目标检测
到目前为止,我们已经演示了如何使用分类模型对图像进行分类,例如,使用二进制分类来确定图像是否包含手写数字 1。在下一节中,我们将看到如何使用有监督的机器学习模型,不仅检查对象是否在图像中,而且还可以找到对象在图像中的位置(例如,根据边界框,即对象所在的矩形)。
基于 Haar-like 特征的人脸检测和 AdaBoost-Viola-Jones 级联分类器
正如我们在第 7 章提取图像特征和描述符(在类似 Haar 的特征提取上下文中)中简要讨论的,Viola Jones 的目标检测技术可用于图像中的人脸检测。它是一种经典的机器学习方法,其中,通过使用从图像中提取的手工制作的类 Haar 特征,使用正图像和负图像的训练集训练级联函数。
Viola-Jones 算法通常使用基本面片大小(例如,24 x 24 像素),它在图像上来回滑动,并计算大量类似 Haar 的特征(24 x 24 面片的 160000 个可能特征,尽管通过适当选择 6000 个特征子集,它们已达到 95%的准确率);这些功能是可以提取的,但要在每个补丁上运行 6000 个功能需要付出很大的努力。因此,一旦确定并组装了这些特性,就会构建一些快速拒绝器。这是基于这样的想法,即在一张完整的图像中,我们检查的大多数可能位置都不会包含人脸,因此,总体而言,快速拒绝比在其上投入太多更快。如果该位置不包含面,我们将丢弃它并继续,然后再进行更多的计算检查。这是 Viola Jones 实现其性能的另一个技巧,如下图所示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PJCKxKZy-1681961425715)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/handson-imgproc-py/img/709f5482-6841-47c7-8f73-c4da2c57dbb8.png)]
不是在一个窗口上应用所有 6000 个特征,而是通过使用 AdaBoost 集成分类器将特征分组到弱分类器的不同阶段(以执行特征级联性能的自适应增强),并且这些弱分类器在级联中逐个运行。如果正在考虑的修补程序窗口在任何阶段失败,则该窗口将被拒绝。通过所有阶段的修补程序被视为有效检测
Haar-like 功能的一个很好的优点是计算速度非常快(由于积分图像技术),这导致了 Viola-Jones 性能的最后一个方面。基本面片本身可以缩放,最终级联中的特征可以非常快速地评估,以搜索不同大小的对象,而无需图像金字塔。综合起来,Viola-Jones 培训工作流程如下图所示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HTtQIikC-1681961425715)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/handson-imgproc-py/img/c006aca6-9d78-48b2-9bbf-e3d5484f926f.png)]
该算法速度惊人,但存在明显的精度限制
基于类 Haar 特征描述子的人脸分类
Viola Jones 使用类似 Haar 的特征描述符实现了第一个实时人脸检测器。以下示例取自 scikit 图像的示例,该示例演示了提取、选择和分类类 Haar 特征,以检测人脸与非人脸。显然,这是一个二值分类问题。
用于特征选择和分类的 scikit 学习库。虽然人脸检测的原始实现使用 AdaBoost 集成分类器,但在本例中,将使用不同的集成分类器 random forest,主要用于查找对分类有用的重要 Haar-like 特征
为了提取哈尔式的。。。
用随机森林集成分类器寻找人脸分类中最重要的类 Haar 特征
训练一个随机森林分类器,以选择最显著的特征进行人脸分类。这个想法是检查哪些特征是树集合最常用的。通过在后续步骤中仅使用最显著的特征,可以提高计算速度,同时保持准确性。以下代码片段显示了如何计算分类器的特征重要性,并显示了前 25 个最重要的 Haar-like 特征:
代码语言:javascript复制# For speed, only extract the two first types of features
feature_types = ['type-2-x', 'type-2-y']
# Build a computation graph using dask. This allows using multiple CPUs for
# the computation step
X = delayed(extract_feature_image(img, feature_types)
for img in images)
# Compute the result using the "processes" dask backend
t_start = time()
X = np.array(X.compute(scheduler='processes'))
time_full_feature_comp = time() - t_start
y = np.array([1] * 100 [0] * 100)
X_train, X_test, y_train, y_test = train_test_split(X, y, train_size=150, random_state=0, stratify=y)
print(time_full_feature_comp)
# 104.87986302375793
print(X.shape, X_train.shape)
# (200, 101088) (150, 101088)
from sklearn.metrics import roc_curve, auc, roc_auc_score
# Extract all possible features to be able to select the most salient.
feature_coord, feature_type =
haar_like_feature_coord(width=images.shape[2], height=images.shape[1],
feature_type=feature_types)
# Train a random forest classifier and check performance
clf = RandomForestClassifier(n_estimators=1000, max_depth=None,
max_features=100, n_jobs=-1, random_state=0)
t_start = time()
clf.fit(X_train, y_train)
time_full_train = time() - t_start
print(time_full_train)
# 1.6583366394042969
auc_full_features = roc_auc_score(y_test, clf.predict_proba(X_test)[:, 1])
print(auc_full_features)
# 1.0
# Sort features in order of importance, plot six most significant
idx_sorted = np.argsort(clf.feature_importances_)[::-1]
fig, axes = pylab.subplots(5, 5, figsize=(10,10))
for idx, ax in enumerate(axes.ravel()):
image = images[1]
image = draw_haar_like_feature(image, 0, 0, images.shape[2], images.shape[1],
[feature_coord[idx_sorted[idx]]])
ax.imshow(image), ax.set_xticks([]), ax.set_yticks([])
fig.suptitle('The most important features', size=30)
下面的屏幕截图显示了前面代码块的输出—用于人脸检测的前 25 个最重要的 Haar-like 特征:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5DyDwJus-1681961425716)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/handson-imgproc-py/img/90ff7afc-e545-4bd1-94be-27eaefc37fa4.png)]
通过仅保留少数最重要的特征(约占所有特征的 3%),可以保留大部分(约 70%)的特征重要性,并且通过仅使用这些特征训练RandomForest
分类器,我们应该能够保持验证数据集的准确性(我们通过使用所有特征训练分类器获得),但特征提取和分类器训练所需的时间要小得多。代码留给读者作为练习。
基于 HOG 特征的 SVM 目标检测
如第 7 章提取图像特征和描述符中所述,定向梯度的直方图(HOG是一种用于各种计算机视觉和图像处理应用中的特征描述符,用于目标检测。Navneet Dalal 和 Bill Triggs 首次将 HOG 描述符与 SVM 分类器一起用于行人检测。HOG 描述符的使用在检测人、动物、人脸和文本等方面尤其成功。例如,可以考虑对象检测系统来生成描述输入图像中对象特征的 HOG 描述符
我们已经描述了如何从…计算 HOG 描述符。。。
养猪
SVM 训练器选择最佳超平面,以从训练集中分离正示例和负示例。将这些块描述符串联起来,转换为 SVM 训练器的输入格式,并适当标记为正或负。训练器通常输出一组支持向量,即,训练集中最能描述超平面的示例。超平面是区分正例和负例的学习决策边界。SVM 模型随后使用这些支持向量对测试图像中的 HOG 描述符块进行分类,以检测对象的存在/不存在。
用支持向量机模型进行分类
这种 HOG 计算传统上是通过在测试图像帧上重复步进 64 像素宽×128 像素高的窗口并计算 HOG 描述符来执行的。由于 HOG 计算不包含固有的比例感,并且对象可以在图像中的多个比例下出现,因此 HOG 计算将在比例金字塔的每个级别上进行阶梯式重复。缩放棱锥体中每个级别之间的缩放因子通常在 1.05 和 1.2 之间,并且图像会反复缩放,直到缩放的源帧无法再容纳完整的 HOG 窗口。如果 SVM 分类器预测任何尺度下的对象检测,则返回相应的边界框。这个
用 HOG-SVM 计算包围盒
在本节中,我们将演示如何使用python-opencv
库函数使用 HOG-SVM 检测图像中的人物。下面的代码显示了如何从图像计算 HOG 描述符,并使用描述符输入预训练的 SVM 分类器(使用 cv2 的HOGDescriptor_getDefaultPeopleDetector()
),该分类器将使用python-opencv
中的detectMultiScale()
函数从多个尺度的图像块中预测人员的存在或不存在:
import numpy as np
import cv2
import matplotlib.pylab as pylab
img = cv2.imread("../images/me16.jpg")
# create HOG descriptor using default people (pedestrian) detector
hog = cv2.HOGDescriptor()
hog.setSVMDetector(cv2.HOGDescriptor_getDefaultPeopleDetector())
# run detection, using a spatial stride of 4 pixels (horizontal and vertical), a scale stride of 1.02, and # zero grouping of rectangles (to demonstrate that HOG will detect at potentially multiple places in the
# scale pyramid)
(foundBoundingBoxes, weights) = hog.detectMultiScale(img, winStride=(4, 4), padding=(8, 8), scale=1.02, finalThreshold=0)
print(len(foundBoundingBoxes)) # number of boundingboxes
# 357
# copy the original image to draw bounding boxes on it for now, as we'll use it again later
imgWithRawBboxes = img.copy()
for (hx, hy, hw, hh) in foundBoundingBoxes:
cv2.rectangle(imgWithRawBboxes, (hx, hy), (hx hw, hy hh), (0, 0, 255), 1)
pylab.figure(figsize=(20, 12))
imgWithRawBboxes = cv2.cvtColor(imgWithRawBboxes, cv2.COLOR_BGR2RGB)
pylab.imshow(imgWithRawBboxes, aspect='auto'), pylab.axis('off'), pylab.show()
以下屏幕截图显示了输出、图像以及边界框(红色矩形)指示的不同比例的检测对象:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VJh9bj4W-1681961425716)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/handson-imgproc-py/img/d624a77e-0f19-4951-9915-0804c033ae54.png)]
前面的屏幕截图说明了 HOG 的一些有趣特性和问题,我们可以在前面的屏幕截图中看到,有许多无关的检测(总共 357 个),需要使用非最大抑制将它们融合在一起。此外,我们还可能看到一些误报。
非最大值抑制
接下来,需要调用非最大抑制函数,以避免在多次和多次缩放时检测到同一对象。下面的代码块显示了如何使用cv2
库函数实现它:
from imutils.object_detection import non_max_suppression# convert our bounding boxes from format (x1, y1, w, h) to (x1, y1, x2, y2)rects = np.array([[x, y, x w, y h] for (x, y, w, h) in foundBoundingBoxes])# run non-max suppression on these based on an overlay op 65%nmsBoundingBoxes = non_max_suppression(rects, probs=None, overlapThresh=0.65)print(len(rects), len(nmsBoundingBoxes))# 357 1# draw the final bounding boxesfor (x1, y1, x2, y2) in nmsBoundingBoxes: cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), ...
总结
在本章中,我们讨论了一些经典的机器学习技术及其在解决图像处理问题中的应用。我们从无监督机器学习算法开始,如聚类和主成分分析。我们使用 scikit learn 演示了 k-means 和谱聚类算法,并向您展示了如何将它们用于矢量量化和分割。接下来,我们了解了 PCA 如何用于高维数据集(如 scikit 学习手写数字图像数据集)的降维和可视化。此外,还说明了 PCA 如何使用 scikit 学习人脸数据集实现特征脸。
然后,我们讨论了几种有监督机器学习分类模型,如 kNN、高斯贝叶斯生成模型和 SVM,以解决手写数字数据集的分类等问题。最后,我们讨论了两种用于图像中目标检测的经典机器学习技术,即 Viola-Jones 的 AdaBoost 级联分类器,用于人脸检测(并使用随机森林分类器查找最重要的特征)和用于行人检测的 HOG-SVM。
在下一章中,我们将开始讨论深度学习技术在图像处理方面的最新进展。
问题
- 使用 k 均值聚类对图像进行阈值化(使用
number of clusters=2
),并将结果与大津的结果进行比较。 - 使用 scikit learn 的
cluster.MeanShift()
和mixture.GaussianMixture()
函数,分别使用 mean shift 和 GMM-EM 聚类方法对图像进行分割,这是另外两种流行的聚类算法。 - 使用 Isomap(从
sklearn.manifold
开始)进行非线性降维,并可视化二维投影。它是否比 PCA 的线性降维更好?用 TSNE 重复练习(再次从sklearn.manifold
开始)。 - 编写一个 Python 程序来显示几个主要特征面的加权线性组合确实近似于一个面。
- 表明特征脸也可以用于原始人脸检测(和识别)。。。
进一步阅读
- Viola、Paul 和 Michael J.Jones,鲁棒实时人脸检测。国际计算机视觉杂志:http://www.merl.com/publications/docs/TR2004-043.pdf
- Navneet Dalal 和 Bill Triggs,人体检测方向梯度直方图:https://lear.inrialpes.fr/people/triggs/pubs/Dalal-cvpr05.pdf
十、图像处理中的深度学习——图像分类
在本章中,我们将通过深入学习讨论图像处理的最新进展。我们将首先区分经典和深度学习技术,然后是关于卷积神经网络(CNN)的概念部分,这是一种对图像处理特别有用的深度神经网络体系结构。然后,我们将继续讨论使用两个图像数据集的图像分类问题,以及如何使用 TensorFlow 和 Keras 这两个非常流行的深度学习库来实现它。此外,我们还将了解如何训练深层 CNN 架构并将其用于预测。
本章涉及的主题如下:
- 图像处理中的深度学习
图像处理中的深度学习
机器学习(ML的主要目标是泛化;也就是说,我们在一个训练数据集上训练一个算法,我们希望该算法在一个看不见的数据集上以高性能(精确性)工作。为了解决复杂的图像处理任务(如图像分类),我们拥有的训练数据越多,我们可能期望学习到的 ML 模型具有更好的泛化能力,前提是我们考虑过拟合(例如,正则化)。但是,使用传统的 ML 技术,不仅需要大量的训练数据,而且学习(泛化方面的改进)往往会在某一点停止。此外,传统的 ML 算法通常需要大量的领域专业知识和人工干预,并且它们只能够完成它们的设计目的。这就是深度学习模式非常有希望的地方。
什么是深度学习?
深度学习的一些广为人知和被广泛接受的定义如下:
- 它是 ML 的子集。
- 它使用多层(非线性)处理单元级联,称为人工神经网络(ANN),以及受大脑(神经元)结构和功能启发的算法。每个连续层使用前一层的输出作为输入。
- 它使用人工神经网络进行特征提取和转换,处理数据,发现模式,并开发抽象。
- 它可以是有监督的(例如,分类)或无监督的(例如,模式分析)。
- 它使用梯度下降算法来学习对应于不同抽象级别的多个表示级别,以及。。。
经典与深度学习
- 手工与自动特征提取:为了解决传统 ML 技术的图像处理问题,最重要的预处理步骤是手工特征(如 HOG 和 SIFT)提取,以降低图像的复杂性,并使模式更加可见,以便学习算法工作。深度学习算法的最大优点是,它们试图以增量方式从训练图像中学习低级和高级特征。这样就不需要在提取或工程中使用手工制作的功能。
- 分部分与端到端解决方案:传统的 ML 技术通过分解问题、先解决不同部分,然后将结果聚合到一起给出输出来解决问题陈述,而深度学习技术使用端到端方法解决问题。例如,在对象检测问题中,经典的 ML 算法(如 SVM)需要一个边界框对象检测算法,该算法将首先识别所有可能的对象,这些对象需要将 HOG 作为 ML 算法的输入,以便识别正确的对象。但一种深度学习方法,如 YOLO 网络,将图像作为输入,并提供对象的位置和名称作为输出。显然是端到端的,不是吗?
- 训练时间和先进的硬件:与传统的 ML 算法不同,深度学习算法由于参数数量庞大、数据集相对庞大,训练时间较长。因此,我们应该始终在高端硬件(如 GPU)上训练深度学习模型,并记住合理的训练时间,因为时间是有效训练模型的一个非常重要的方面。
- 适应性和可转移性:经典的 ML 技术非常有限,而深度学习技术可以应用于广泛的应用和各个领域。其中很大一部分用于转移学习,这使我们能够在同一领域内为不同的应用程序使用预先训练的深度网络。例如,在这里,在图像处理中,预训练的图像分类网络通常用作特征提取前端来检测对象和分割网络。
**现在,让我们以图解方式(猫和狗的图像)在图像分类中使用 ML 和深度学习模型时,看看它们之间的区别。
传统 ML 将具有特征提取和分类器,以解决任何问题:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YyhS5Ns7-1681961425716)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/handson-imgproc-py/img/5ccb7276-858b-46d5-8b83-da552d1994b6.png)]
通过深入学习,您可以看到我们讨论过的隐藏层和行动中的决策:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Qo2FpW4J-1681961425717)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/handson-imgproc-py/img/d4004f8e-f905-4402-a44b-daa51f56569a.png)]
为什么要深入学习?
如前所述,如果您有更多的数据,那么最好的选择是深度网络,该网络在数据充足的情况下性能更好。很多时候,使用的数据越多,结果就越准确。经典的最大似然法需要一组复杂的最大似然算法,更多的数据只会影响其准确性。然后需要采用复杂的方法来弥补精度较低的缺陷。此外,即使学习受到影响,当添加更多的训练数据来训练模型时,学习也几乎在某个时间点停止。
这是如何以图形方式描述的:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vv3nrQp2-1681961425717)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/handson-imgproc-py/img/dfeb5ffe-758b-4b88-858c-f5d0f871339b.png)]
CNNs
CNN是深度神经网络,主要用于输入图像。CNN 学习传统算法中手工设计的滤波器(特征)。这种独立于先验知识和人类在特征设计中的努力是一个主要优势。它们还通过共享权重体系结构减少了需要学习的参数数量,并具有平移不变性特征。在下一小节中,我们将讨论 CNN 的总体架构及其工作原理。
Conv 或池或 FC 层–CNN 体系结构及其工作原理
下一个屏幕截图显示了 CNN 的典型架构。它包括一个或多个卷积层,然后是非线性 ReLU 激活层、池层,最后是一个(或多个)完全连接的(FC)层,然后是 FC softmax 层,例如,在设计用于解决图像分类问题的 CNN 的情况下。
网络中可能存在多个层卷积 ReLU 池序列,使神经网络更深入,有助于解决复杂的图像处理任务,如下图所示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Spk2psxA-1681961425717)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/handson-imgproc-py/img/3ffb873b-b3c6-4dc8-9531-1e7918401a68.png)]
下面几节将介绍。。。
卷积层
CNN 的主要组成部分是卷积层。卷积层由一组卷积滤波器(内核)组成,我们已经在第 2 章、采样、傅里叶变换和卷积中详细讨论了这些滤波器。使用卷积滤波器对输入图像进行卷积,以生成特征映射。左侧是卷积层的输入;例如,输入图像。右边是卷积滤波器,也称为内核。通常,卷积运算是通过在输入上滑动该滤波器来执行的。在每个位置,元素矩阵乘法的和进入特征映射。卷积层由其宽度、高度(过滤器的大小为宽度 x 高度)和深度(过滤器数量)表示。步长指定卷积过滤器在每一步移动的量(默认值为 1)。填充是指环绕输入的零层(通常用于保持输入和输出图像大小相同,也称为相同填充。以下屏幕截图显示了如何在 RGB 图像上应用 3 x 3 x 3 卷积滤波器,第一个使用有效填充,第二个使用两个大小为步长=填充=1的此类滤波器进行计算:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-c1JdfK5z-1681961425718)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/handson-imgproc-py/img/64384b37-c61a-43ca-87a8-a581803b7ff7.png)]
池层
在卷积运算之后,通常执行池运算以减少维数和要学习的参数的数量,这缩短了训练时间,需要更少的数据进行训练,并防止过度拟合。池层独立地对每个特征地图进行下采样,减少高度和宽度,但保持深度不变。最常见的池类型是最大池,它只取池窗口中的最大值。与卷积运算相反,池没有参数。它将窗口滑动到其输入上,只需获取窗口中的最大值。与卷积类似,可以指定池的窗口大小和步长。
非线性-ReLU 层
任何一种神经网络要想强大,都需要包含非线性。因此,卷积运算的结果通过非线性激活函数传递。ReLU 激活通常用于实现非线性(以及用 sigmoid 激活解决消失梯度问题)。因此,最终特征图中的值实际上不是和,而是应用于它们的relu
函数。
FC 层
在卷积层和池层之后,通常会添加两个 FC 层来封装 CNN 架构。卷积层和池层的输出都是 3D 卷,但 FC 层需要 1D 数字向量。因此,最终池层的输出需要展平为一个向量,这将成为 FC 层的输入。展平就是简单地将三维体积的数字排列成一维向量。
辍学者
Dropout是深度神经网络最流行的正则化技术。Dropout 用于防止过度拟合,通常用于在看不见的数据集上提高深度学习任务的性能(准确性)。在训练期间,在每次迭代中,一个神经元以某种概率暂时丢失或禁用,p。这意味着在当前迭代中,该神经元的所有输入和输出都将被禁用。这个超参数p被称为退出率,它通常是一个 0.5 左右的数字,对应于 50%的神经元退出。
基于 TensorFlow 或 Keras 的图像分类
在本节中,我们将重新讨论手写数字分类问题(使用 MNIST 数据集),但这次是使用深层神经网络。我们将使用两个非常流行的深度学习库来解决这个问题,即 TensorFlow 和 Keras。TensorFlow(TF)是深度学习模型生产中使用的最著名的库。它有一个非常大和令人敬畏的社区。然而,TensorFlow 并不是那么容易使用。另一方面,Keras 是基于 TensorFlow 构建的高级 API。与 TF 相比,它更加用户友好且易于使用,尽管它对低级结构的控制较少。低级库提供了更大的灵活性。因此TF 可以调整得更多,因为。。。
*# TF 分类
首先,我们将从一个非常简单的深层神经网络开始,该网络只包含一个 FC 隐藏层(具有 ReLU 激活)和一个 softmax FC 层,没有卷积层。下一个屏幕截图显示了网络颠倒。输入是一个扁平图像,包含隐藏层中的 28 x 28 个节点和 1024 个节点,以及 10 个输出节点,对应于要分类的每个数字:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-O6wofc2V-1681961425718)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/handson-imgproc-py/img/48cd1b80-5f86-4636-b7d7-1ddc1def4204.png)]
现在,让我们用 TF 实现深度学习图像分类。首先,我们需要加载mnist
数据集,并将训练图像分为两部分,第一部分是较大的(我们使用 50k 图像)进行训练,第二部分(10k 图像)用于验证。让我们重新格式化标签,用一个热编码的二进制向量表示图像类。然后需要初始化tensorflow
图以及变量、常量和占位符张量。使用小批量随机梯度下降(SGD优化器)作为学习算法,批量大小为 256,在两个权重层(超参数值为λ1=λ2=1)上使用 L2 正则化器最小化 softmax 交叉熵 logit 损失函数。最后,TensorFlowsession
对象将运行 6k 步(小批量),并运行前向/反向传播以更新学习的模型(权重),随后在验证数据集上对模型进行评估。可以看出,最终批次完成后获得的精度为96.5%
:
%matplotlib inline
import numpy as np
# import data
from keras.datasets import mnist
import tensorflow as tf
# load data
(X_train, y_train), (X_test, y_test) = mnist.load_data()
np.random.seed(0)
train_indices = np.random.choice(60000, 50000, replace=False)
valid_indices = [i for i in range(60000) if i not in train_indices]
X_valid, y_valid = X_train[valid_indices,:,:], y_train[valid_indices]
X_train, y_train = X_train[train_indices,:,:], y_train[train_indices]
print(X_train.shape, X_valid.shape, X_test.shape)
# (50000, 28, 28) (10000, 28, 28) (10000, 28, 28)
image_size = 28
num_labels = 10
def reformat(dataset, labels):
dataset = dataset.reshape((-1, image_size * image_size)).astype(np.float32)
# one hot encoding: Map 1 to [0.0, 1.0, 0.0 ...], 2 to [0.0, 0.0, 1.0 ...]
labels = (np.arange(num_labels) == labels[:,None]).astype(np.float32)
return dataset, labels
X_train, y_train = reformat(X_train, y_train)
X_valid, y_valid = reformat(X_valid, y_valid)
X_test, y_test = reformat(X_test, y_test)
print('Training set', X_train.shape, X_train.shape)
print('Validation set', X_valid.shape, X_valid.shape)
print('Test set', X_test.shape, X_test.shape)
# Training set (50000, 784) (50000, 784) # Validation set (10000, 784) (10000, 784) # Test set (10000, 784) (10000, 784)
def accuracy(predictions, labels):
return (100.0 * np.sum(np.argmax(predictions, 1) == np.argmax(labels, 1)) / predictions.shape[0])
batch_size = 256
num_hidden_units = 1024
lambda1 = 0.1
lambda2 = 0.1
graph = tf.Graph()
with graph.as_default():
# Input data. For the training data, we use a placeholder that will be fed
# at run time with a training minibatch.
tf_train_dataset = tf.placeholder(tf.float32,
shape=(batch_size, image_size * image_size))
tf_train_labels = tf.placeholder(tf.float32, shape=(batch_size, num_labels))
tf_valid_dataset = tf.constant(X_valid)
tf_test_dataset = tf.constant(X_test)
# Variables.
weights1 = tf.Variable(tf.truncated_normal([image_size * image_size, num_hidden_units]))
biases1 = tf.Variable(tf.zeros([num_hidden_units]))
# connect inputs to every hidden unit. Add bias
layer_1_outputs = tf.nn.relu(tf.matmul(tf_train_dataset, weights1) biases1)
weights2 = tf.Variable(tf.truncated_normal([num_hidden_units, num_labels]))
biases2 = tf.Variable(tf.zeros([num_labels]))
# Training computation.
logits = tf.matmul(layer_1_outputs, weights2) biases2
loss = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(labels=tf_train_labels, logits=logits)
lambda1*tf.nn.l2_loss(weights1) lambda2*tf.nn.l2_loss(weights2))
# Optimizer.
optimizer = tf.train.GradientDescentOptimizer(0.008).minimize(loss)
# Predictions for the training, validation, and test data.
train_prediction = tf.nn.softmax(logits)
layer_1_outputs = tf.nn.relu(tf.matmul(tf_valid_dataset, weights1) biases1)
valid_prediction = tf.nn.softmax(tf.matmul(layer_1_outputs, weights2) biases2)
layer_1_outputs = tf.nn.relu(tf.matmul(tf_test_dataset, weights1) biases1)
test_prediction = tf.nn.softmax(tf.matmul(layer_1_outputs, weights2) biases2)
num_steps = 6001
ll = []
atr = []
av = []
import matplotlib.pylab as pylab
with tf.Session(graph=graph) as session:
#tf.global_variables_initializer().run()
session.run(tf.initialize_all_variables())
print("Initialized")
for step in range(num_steps):
# Pick an offset within the training data, which has been randomized.
# Note: we could use better randomization across epochs.
offset = (step * batch_size) % (y_train.shape[0] - batch_size)
# Generate a minibatch.
batch_data = X_train[offset:(offset batch_size), :]
batch_labels = y_train[offset:(offset batch_size), :]
# Prepare a dictionary telling the session where to feed the minibatch.
# The key of the dictionary is the placeholder node of the graph to be fed,
# and the value is the numpy array to feed to it.
feed_dict = {tf_train_dataset : batch_data, tf_train_labels : batch_labels}
_, l, predictions = session.run([optimizer, loss, train_prediction], feed_dict=feed_dict)
if (step % 500 == 0):
ll.append(l)
a = accuracy(predictions, batch_labels)
atr.append(a)
print("Minibatch loss at step %d: %f" % (step, l))
print("Minibatch accuracy: %.1f%%" % a)
a = accuracy(valid_prediction.eval(), y_valid)
av.append(a)
print("Validation accuracy: %.1f%%" % a)
print("Test accuracy: %.1f%%" % accuracy(test_prediction.eval(), y_test))
# Initialized
# Minibatch loss at step 0: 92091.781250
# Minibatch accuracy: 9.0%
# Validation accuracy: 21.6%
#
# Minibatch loss at step 500: 35599.835938
# Minibatch accuracy: 50.4%
# Validation accuracy: 47.4%
#
# Minibatch loss at step 1000: 15989.455078
# Minibatch accuracy: 46.5%
# Validation accuracy: 47.5%
#
# Minibatch loss at step 1500: 7182.631836
# Minibatch accuracy: 59.0%
# Validation accuracy: 54.7%
#
# Minibatch loss at step 2000: 3226.800781
# Minibatch accuracy: 68.4%
# Validation accuracy: 66.0%
#
# Minibatch loss at step 2500: 1449.654785
# Minibatch accuracy: 79.3%
# Validation accuracy: 77.7%
#
# Minibatch loss at step 3000: 651.267456
# Minibatch accuracy: 89.8%
# Validation accuracy: 87.7%
#
# Minibatch loss at step 3500: 292.560272
# Minibatch accuracy: 94.5%
# Validation accuracy: 91.3%
#
# Minibatch loss at step 4000: 131.462219
# Minibatch accuracy: 95.3%
# Validation accuracy: 93.7%
#
# Minibatch loss at step 4500: 59.149700
# Minibatch accuracy: 95.3%
# Validation accuracy: 94.3%
#
# Minibatch loss at step 5000: 26.656094
# Minibatch accuracy: 94.9%
# Validation accuracy: 95.5%
#
# Minibatch loss at step 5500: 12.033947
# Minibatch accuracy: 97.3%
# Validation accuracy: 97.0%
#
# Minibatch loss at step 6000: 5.521026
# Minibatch accuracy: 97.3%
# Validation accuracy: 96.6%
#
# Test accuracy: 96.5%
让我们使用以下代码块来可视化每个步骤的第 1 层权重:
代码语言:javascript复制images = weights1.eval()
pylab.figure(figsize=(18,18))
indices = np.random.choice(num_hidden_units, 225)
for j in range(225):
pylab.subplot(15,15,j 1)
pylab.imshow(np.reshape(images[:,indices[j]], (image_size,image_size)), cmap='gray')
pylab.xticks([],[]), pylab.yticks([],[])
pylab.subtitle('SGD after Step ' str(step) ' with lambda1=lambda2=' str(lambda1))
pylab.show()
前面显示了经过 4000 个步骤后为网络 FC 层 1 中的225
(随机选择)隐藏节点学习的权重。观察权重已从模型训练的输入图像中学习到一些特征:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qbZOYeAv-1681961425718)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/handson-imgproc-py/img/c51c4eaf-419f-4d4b-909f-3ebff28ea4a4.png)]
下面的代码片段在不同的步骤中绘制了training accuracy
和validation accuracy
:
pylab.figure(figsize=(8,12))
pylab.subplot(211)
pylab.plot(range(0,3001,500), atr, '.-', label='training accuracy')
pylab.plot(range(0,3001,500), av, '.-', label='validation accuracy')
pylab.xlabel('GD steps'), pylab.ylabel('Accuracy'), pylab.legend(loc='lower right')
pylab.subplot(212)
pylab.plot(range(0,3001,500), ll, '.-')
pylab.xlabel('GD steps'), pylab.ylabel('Softmax Loss')
pylab.show()
下一个屏幕截图显示上一个代码块的输出;观察到准确度总体上持续增加,但最终几乎保持不变,这意味着学习不再发生:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5AqkK6KV-1681961425718)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/handson-imgproc-py/img/06050da1-c7cd-4556-a73f-ed8d6df8691c.png)]
使用含 Keras 的致密 FC 层进行分类
让我们用 Keras 实现手写数字分类,同样只使用密集的 FC 层。这一次,我们将使用一个隐藏层和一个退出层。下一个代码块显示了如何使用keras.models Sequential()
函数用几行代码实现分类器。我们可以简单地将层顺序添加到模型中。引入了两个隐藏层,每个层都有 200 个节点,中间有一个辍学,辍学率为 15%。这次,让我们使用Adam优化器(它使用动量来加速 SGD)。让我们用 10epochs
(一次通过整个输入数据集)将模型拟合到训练数据集上。正如我们所看到的,随着。。。
网络可视化
让我们想象一下我们用 Keras 设计的神经网络的架构。以下代码片段将允许我们在图像中保存模型(网络)体系结构:
代码语言:javascript复制# pip install pydot_ng ## install pydot_ng if not already installed
import pydot_ng as pydot
from keras.utils import plot_model
plot_model(model, to_file='../images/model.png')
以下屏幕截图显示了前一个代码块(神经网络体系结构)的输出:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-j5cb4MIp-1681961425718)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/handson-imgproc-py/img/c13b0fee-c88f-44cd-8c03-4f0b5cabcafb.png)]
可视化中间层中的权重
现在,让我们可视化在中间层中学习的权重。以下 Python 代码可视化了在第一个密集层上为前 200 个隐藏单元学习的权重:
代码语言:javascript复制from keras.models import Modelimport matplotlib.pylab as pylabimport numpy as npW = model.get_layer('dense_1').get_weights()print(W[0].shape)print(W[1].shape)fig = pylab.figure(figsize=(20,20))fig.subplots_adjust(left=0, right=1, bottom=0, top=0.95, hspace=0.05, wspace=0.05) pylab.gray()for i in range(200): pylab.subplot(15, 14, i 1), pylab.imshow(np.reshape(W[0][:, i], (28,28))), pylab.axis('off')pylab.suptitle('Dense_1 Weights (200 hidden units)', size=20)pylab.show()
这将导致以下输出:。。。
CNN 使用 Keras 进行分类
现在,让我们用 Keras 实现一个 CNN。我们需要引入卷积层、池层和平坦层。下一小节将再次展示如何实现和使用 CNN 进行 MNIST 分类。正如我们将看到的那样,测试数据集的准确性提高了。
分类 MNIST
这一次,让我们介绍一个 5 x 5 的卷积层,其中有 64 个带有 ReLU 激活的过滤器,然后是一个 2 x 2 max 池层,步幅为 2。这需要紧跟一个扁平层,然后是一个包含 100 个节点的单个隐藏密集层,然后是输出 softmax 密集层。可以看出,在对模型进行了 10 个时期的训练后,测试数据集上的精度增加到了98.77%
:
import kerasfrom keras.models import Sequentialfrom keras.layers import Densefrom keras.utils import to_categoricalfrom keras.layers.convolutional import Conv2D # to add convolutional layersfrom keras.layers.convolutional import MaxPooling2D # to add pooling layersfrom keras.layers import Flatten # to flatten data for fully ...
可视化中间层
现在,让我们可视化图像特征映射(64 个特征和 64 个过滤器),使用以下代码块,通过卷积层学习两幅图像:
代码语言:javascript复制from keras.models import Model
import matplotlib.pylab as pylab
import numpy as np
intermediate_layer_model = Model(inputs=model.input, outputs=model.get_layer('conv2d_1').output)
intermediate_output = intermediate_layer_model.predict(X_train)
print(model.input.shape, intermediate_output.shape)
fig = pylab.figure(figsize=(15,15))
fig.subplots_adjust(left=0, right=1, bottom=0, top=1, hspace=0.05, wspace=0.05)
pylab.gray()
i = 1
for c in range(64):
pylab.subplot(8, 8, c 1), pylab.imshow(intermediate_output[i,:,:,c]), pylab.axis('off')
pylab.show()
以下屏幕截图显示了卷积层从训练数据集中学习的标签为 0 的手写数字图像的特征图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-f0aOzOae-1681961425719)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/handson-imgproc-py/img/742aac75-cb77-4b6c-bc50-41dd7ed1396d.png)]
通过将图像的索引从训练数据集更改为 2 并再次运行前面的代码,我们得到以下输出:
代码语言:javascript复制i = 2
以下屏幕截图显示了卷积层从 MNIST 训练数据集中学习的标签为 4 的手写数字图像的特征图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oYUGe7TI-1681961425719)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/handson-imgproc-py/img/18d78c5e-cd95-40b1-b798-cd127c61a51c.png)]
一些流行的深度 CNN
在本节中,让我们讨论用于图像分类的流行深度 CNN(例如,VGG-18/19、ResNet 和 InceptionNet)。下面的屏幕截图显示了单作物精度(top-1 精度:CNN 预测的正确标签具有最高概率的次数)在提交给 ImageNet 挑战赛的最相关的参赛作品中,从最左边的AlexNet(Krizhevsky 等人,2012),到表现最好的Inception-v4(Szegedy 等人,2016):
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-McG5cHPL-1681961425719)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/handson-imgproc-py/img/ef7a2ec2-ea59-483f-9cbd-85b9f822801c.png)]
此外,我们还将使用 Keras 训练 VGG-16 CNN,将猫图像与狗图像进行分类。
VGG-16/19
下面的屏幕截图显示了一个名为 VGG-16/19 的流行 CNN 的架构。关于 VGG-16 网络,值得注意的是,它不需要太多的超参数,而是让你使用一个简单得多的网络,你只需要使用 3 x 3 的卷积层,步长为 1,并且总是使用相同的填充,使所有最大池层的步长为 2 x 2。这是一个非常深入的网络。
该网络共有约 1.38 亿个参数,如下图所示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tNjJvoYT-1681961425720)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/handson-imgproc-py/img/f84e5774-a2b7-440e-8831-3fce499a371c.png)]
在 Keras 中用 VGG-16 对猫/狗图像进行分类
在本节中,让我们使用 Keras VGG-16 实现对 Kaggle狗与猫比赛中的猫和狗图像进行分类。让我们下载培训和测试图像数据集(https://www.kaggle.com/c/dogs-vs-cats 首先。然后,让我们在训练图像上从头开始训练 VGG-16 网络
**# 训练阶段
下面的代码块显示了如何在训练数据集上拟合模型。让我们使用来自训练数据集的 20k 图像来训练 VGG-16 模型,并使用 5k 图像作为验证数据集在训练时评估模型。weights=None
参数值必须传递给VGG16()
函数,以确保网络从头开始训练。注意,如果不在 GPU 上运行,这将花费很长时间,因此建议使用 GPU
For installing TensorFlow with a GPU, refer to this article: https://medium.com/@raza.shahzad/setting-up-tensorflow-gpu-keras-in-conda-on-windows-10-75d4fd498198.
对于 20 个时代,验证数据集的精度为78.38%.
,我们可以调整超参数以进一步提高模型的精度,这留给读者作为练习:
import os
import numpy as np
import cv2
from random import shuffle
from tqdm import tqdm # percentage bar for tasks.
# download the cats/dogs images compressed train and test datasets from here: https://www.kaggle.com/c/dogs-vs-cats/data
# unzip the train.zip images under the train folder and test.zip images under the test folder
train = './train'
test = './test'
lr = 1e-6 # learning rate
image_size = 50 # all the images will be resized to squaure images with this dimension
model_name = 'cats_dogs-{}-{}.model'.format(lr, 'conv2')
代码语言:javascript复制def label_image(image):
word_label = image.split('.')[-3]
if word_label == 'cat': return 0
elif word_label == 'dog': return 1
def create_training_data():
training_data = []
for image in tqdm(os.listdir(train)):
path = os.path.join(train, image)
label = label_image(image)
image = cv2.imread(path)
image = cv2.resize(image, (image_size, image_size))
training_data.append([np.array(image),np.array(label)])
shuffle(training_data)
np.save('train_data.npy', training_data)
return training_data
train_data = create_training_data()
# 100%|████████████████████████████████████████████████████████████████████████████████████████| 1100/1100 [00:00<00:00, 1133.86it/s]
train = train_data[:-5000] # 20k images for training
valid = train_data[-5000:] # 5k images for validation
X_train = np.array([i[0] for i in train]).reshape(-1,image_size,image_size,3)
y_train = [i[1] for i in train]
y_train = to_categorical(y_train)
print(X_train.shape, y_train.shape)
X_valid = np.array([i[0] for i in valid]).reshape(-1,image_size,image_size,3)
y_valid = [i[1] for i in valid]
y_valid = to_categorical(y_valid) # to one-hot encoding
num_classes = y_valid.shape[1] # number of categories
model = VGG16(weights=None, input_shape=(image_size,image_size,3), classes=num_classes) # train VGG16 model from scratch
model.compile(Adam(lr=lr), "categorical_crossentropy", metrics=["accuracy"]) # "adam"
model.summary()
代码语言:javascript复制# fit the model, it's going take a long time if not run on GPU
model.fit(X_train, y_train, validation_data=(X_valid, y_valid), epochs=20, batch_size=256, verbose=2)
# evaluate the model
scores = model.evaluate(X_valid, y_valid, verbose=0)
print("Accuracy: {} n Error: {}".format(scores[1], 100-scores[1]*100))
# _______________________________________________________________
#Layer (type) Output Shape Param
# =================================================================
# input_5 (InputLayer) (None, 50, 50, 3) 0
# _________________________________________________________________
# block1_conv1 (Conv2D) (None, 50, 50, 64) 1792
# _________________________________________________________________
# block1_conv2 (Conv2D) (None, 50, 50, 64) 36928
# _________________________________________________________________
# block1_pool (MaxPooling2D) (None, 25, 25, 64) 0
# _________________________________________________________________
# block2_conv1 (Conv2D) (None, 25, 25, 128) 73856
# _________________________________________________________________
# block2_conv2 (Conv2D) (None, 25, 25, 128) 147584
# _________________________________________________________________
# block2_pool (MaxPooling2D) (None, 12, 12, 128) 0
# _________________________________________________________________
# block3_conv1 (Conv2D) (None, 12, 12, 256) 295168
# _________________________________________________________________
# block3_conv2 (Conv2D) (None, 12, 12, 256) 590080
# _________________________________________________________________
# block3_conv3 (Conv2D) (None, 12, 12, 256) 590080
# _________________________________________________________________
# block3_pool (MaxPooling2D) (None, 6, 6, 256) 0
# _________________________________________________________________
# block4_conv1 (Conv2D) (None, 6, 6, 512) 1180160
# _________________________________________________________________
# block4_conv2 (Conv2D) (None, 6, 6, 512) 2359808
# _________________________________________________________________
# block4_conv3 (Conv2D) (None, 6, 6, 512) 2359808
# _________________________________________________________________
# block4_pool (MaxPooling2D) (None, 3, 3, 512) 0
# _________________________________________________________________
# block5_conv1 (Conv2D) (None, 3, 3, 512) 2359808
# _________________________________________________________________
# block5_conv2 (Conv2D) (None, 3, 3, 512) 2359808
# _________________________________________________________________
# block5_conv3 (Conv2D) (None, 3, 3, 512) 2359808
# _________________________________________________________________
# block5_pool (MaxPooling2D) (None, 1, 1, 512) 0
# _________________________________________________________________
# flatten (Flatten) (None, 512) 0
# ________________________________________________________________ *# fc1 (Dense) (None, 4096) 2101248* # _______________________________________________________________ *# fc2 (Dense) (None, 4096) 16781312* # ________________________________________________________________
# predictions (Dense) (None, 2) 8194
# =================================================================
# Total params: 33,605,442
# Trainable params: 33,605,442
# Non-trainable params: 0
# _________________________________________________________________
# Train on 20000 samples, validate on 5000 samples
# Epoch 1/10
# - 92s - loss: 0.6878 - acc: 0.5472 - val_loss: 0.6744 - val_acc: 0.5750
# Epoch 2/20
# - 51s - loss: 0.6529 - acc: 0.6291 - val_loss: 0.6324 - val_acc: 0.6534
# Epoch 3/20
# - 51s - loss: 0.6123 - acc: 0.6649 - val_loss: 0.6249 - val_acc: 0.6472
# Epoch 4/20
# - 51s - loss: 0.5919 - acc: 0.6842 - val_loss: 0.5902 - val_acc: 0.6828
# Epoch 5/20
# - 51s - loss: 0.5709 - acc: 0.6992 - val_loss: 0.5687 - val_acc: 0.7054
# Epoch 6/20
# - 51s - loss: 0.5564 - acc: 0.7159 - val_loss: 0.5620 - val_acc: 0.7142
# Epoch 7/20
# - 51s - loss: 0.5539 - acc: 0.7137 - val_loss: 0.5698 - val_acc: 0.6976
# Epoch 8/20
# - 51s - loss: 0.5275 - acc: 0.7371 - val_loss: 0.5402 - val_acc: 0.7298
# Epoch 9/20
# - 51s - loss: 0.5072 - acc: 0.7536 - val_loss: 0.5240 - val_acc: 0.7444
# Epoch 10/20
# - 51s - loss: 0.4880 - acc: 0.7647 - val_loss: 0.5127 - val_acc: 0.7544
# Epoch 11/20
# - 51s - loss: 0.4659 - acc: 0.7814 - val_loss: 0.5594 - val_acc: 0.7164
# Epoch 12/20
# - 51s - loss: 0.4584 - acc: 0.7813 - val_loss: 0.5689 - val_acc: 0.7124
# Epoch 13/20
# - 51s - loss: 0.4410 - acc: 0.7952 - val_loss: 0.4863 - val_acc: 0.7704
# Epoch 14/20
# - 51s - loss: 0.4295 - acc: 0.8022 - val_loss: 0.5073 - val_acc: 0.7596
# Epoch 15/20
# - 51s - loss: 0.4175 - acc: 0.8084 - val_loss: 0.4854 - val_acc: 0.7688
# Epoch 16/20
# - 51s - loss: 0.3914 - acc: 0.8259 - val_loss: 0.4743 - val_acc: 0.7794
# Epoch 17/20
# - 51s - loss: 0.3852 - acc: 0.8286 - val_loss: 0.4721 - val_acc: 0.7810
# Epoch 18/20
# - 51s - loss: 0.3692 - acc: 0.8364 - val_loss: 0.6765 - val_acc: 0.6826
# Epoch 19/20
# - 51s - loss: 0.3752 - acc: 0.8332 - val_loss: 0.4805 - val_acc: 0.7760
# Epoch 20/20
# - 51s - loss: 0.3360 - acc: 0.8586 - val_loss: 0.4711 - val_acc: 0.7838
# Accuracy: 0.7838
# Error: 21.61999999999999
下一个代码块使用第一个块的第二个卷积层中的前 64 个过滤器,可视化为狗图像学习的特征:
代码语言:javascript复制intermediate_layer_model = Model(inputs=model.input, outputs=model.get_layer('block1_conv2').output)
intermediate_output = intermediate_layer_model.predict(X_train)
fig = pylab.figure(figsize=(10,10))
fig.subplots_adjust(left=0, right=1, bottom=0, top=1, hspace=0.05, wspace=0.05)
pylab.gray()
i = 3
for c in range(64):
pylab.subplot(8, 8, c 1), pylab.imshow(intermediate_output[i,:,:,c]), pylab.axis('off')
pylab.show()
以下屏幕截图显示了前面代码的输出,即为模型的狗图像学习的特征图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4JbKrh0N-1681961425720)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/handson-imgproc-py/img/9a36f3d3-a07e-4098-bcf8-1e0880bb3695.png)]
通过更改前一代码段中的单行,我们可以可视化在第二个块的第二个卷积层中使用前 64 个过滤器为同一个狗图像学习的特征:
代码语言:javascript复制intermediate_layer_model = Model(inputs=model.input, outputs=model.get_layer('block2_conv2').output)
以下屏幕截图显示了之前代码的输出,即为模型的同一狗图像学习的特征图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-de7uQNxH-1681961425720)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/handson-imgproc-py/img/29d8597d-5545-45a3-97eb-c1bd083f8c7d.png)]
测试(预测)阶段
下一个代码块显示了如何使用所学的 VGG-16 模型,从test
图像数据集中预测图像是狗还是猫的概率:
test_data = process_test_data()len(test_data)X_test = np.array([i for i in test_data]).reshape(-1,IMG_SIZE,IMG_SIZE,3)probs = model.predict(X_test)probs = np.round(probs,2)pylab.figure(figsize=(20,20))for i in range(100): pylab.subplot(10,10,i 1), pylab.imshow(X_test[i,:,:,::-1]), pylab.axis('off') pylab.title("{}, prob={:0.2f}".format('cat' if probs[i][1] < 0.5 else 'dog', max(probs[i][0],probs[i][1])))pylab.show()
下一个屏幕截图显示了类预测的前 100 个测试图像以及预测概率。。。
接收网
在 CNN 分类器的发展中,初始网络是一个非常重要的里程碑。在 inception 网络出现之前,CNN 通常只将卷积层堆叠到最大深度,以获得更好的性能。Inception 网络使用复杂的技术和技巧来满足速度和准确性方面的性能要求
《盗梦空间》网络不断发展,并导致了网络的几个新版本的诞生。一些流行的版本是-Inception-v1、v2、v3、v4 和 Inception-ResNet。由于图像中的显著部分和信息位置可能存在巨大变化,因此为卷积运算选择正确的核大小变得非常困难。对于分布更为全局的信息,宜使用较大的核;对于分布更为局部的信息,宜使用较小的核。深层神经网络存在过度拟合和消失梯度问题。天真地堆叠大型卷积运算将招致大量费用。
inception 网络通过添加在同一级别上运行的多个大小的过滤器,解决了前面的所有问题。这导致网络变得更广而不是更深。下一个屏幕截图显示了具有降维功能的 inception 模块。它使用三种不同大小的过滤器(1 x 1、3 x 3 和 5 x 5)和额外的最大池对输入执行卷积。输出被级联并发送到下一个初始模块。为了降低成本,通过在 3 x 3 和 5 x 5 卷积之前添加额外的 1 x 1 卷积来限制输入通道的数量。利用降维初始模块,建立了神经网络结构。这就是众所周知的谷歌网(初始版本 v1。该体系结构如下图所示,GoogleNet 有九个这样的初始模块线性堆叠。它有 22 层(27 层,包括池层),在上一个初始模块结束时使用全局平均池:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vaI1A98c-1681961425721)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/handson-imgproc-py/img/b5b2d82a-559d-438c-b26f-61856074e91a.png)]
在撰写本文时,已经引入了几个版本的 inception net(V2、3 和 4),它们是对以前体系结构的扩展。Keras 提供了 Inception-v3 模型,可以从头开始训练,也可以使用预训练版本(通过在 ImageNet 上训练获得权重)。
ResNet
简单地堆叠层并不一定会增加网络深度。由于消失梯度问题,它们很难训练。这是一个梯度反向传播到前一层的问题,如果这种情况反复发生,梯度可能会变得无限小。因此,随着我们的深入,性能会受到严重影响
ResNet代表剩余网络,它在网络中引入了快捷方式,我们称之为身份快捷连接。快捷连接遵循其名称,并跳过一个或多个层,从而防止堆叠层降低性能。堆叠的标识层除了简单地堆叠标识映射之外,什么都不做。。。
总结
本章介绍了深度学习模型在图像处理方面的最新进展。我们首先讨论了深度学习的基本概念,它与传统的 ML 有什么不同,以及我们为什么需要它。然后,CNN 被介绍为深度神经网络,专门用于解决复杂的图像处理和计算机视觉任务。讨论了具有卷积层、池层和 FC 层的 CNN 体系结构。接下来,我们介绍了 TensorFlow 和 Keras,这是 Python 中两个流行的深度学习库。我们展示了如何使用 CNN 提高 MNIST 数据集上手写数字分类的测试精度,然后仅使用 FC 层。最后,我们讨论了一些流行的网络,如 VGG-16/19、GoogleNet 和 ResNet。Kera 的 VGG-16 模型在 Kaggle 的狗对猫比赛图像上进行了训练,我们展示了它如何以相当高的精度在验证图像数据集上执行。
在下一章中,我们将讨论如何使用深度学习模型解决更复杂的图像处理任务(例如,对象检测、分割和样式转换),以及如何使用转换学习来节省培训时间。
问题
- 对于使用带有 Keras 的 FC 层对
mnist
数据集进行分类,编写 Python 代码片段以可视化输出层(神经网络所看到的)。 - 对于仅使用带有 FC 层的神经网络和带有 Keras 的 CNN 对
mnist
数据集进行分类,我们在训练模型时直接使用测试数据集对模型进行评估。从训练图像中留出几千个图像,创建一个验证数据集,并在剩余的图像上训练模型。在培训时使用验证数据集评估模型。在培训结束时,使用所学的模型预测测试数据集的标签,并评估模型的准确性。它增加了吗? - 使用 VGG-16/19、Resnet-50 和 Inception。。。