OpenCV计算机视觉实战(6)——经典计算机视觉算法

发布于:2025-05-20 ⋅ 阅读:(18) ⋅ 点赞:(0)

0. 前言

计算机视觉是一个多学科交叉领域,目标是使机器能够理解和解释来自图像和视频的视觉信息。本节中,将介绍计算机视觉中使用的基础经典算法,即使是在深度学习技术的出现之后,这些算法仍然具有重要意义,为图像分析、特征提取、分割、运动估计和目标检测奠定了基础。

1. 计算机视觉

在深入了解经典计算机视觉算法之前,我们快速回顾一下计算机视觉中的主要任务,包括图像分类、目标检测和定位、分割、字符识别、人脸检测、人脸识别、深度感知等,介绍用来解决这些问题的经典算法。

1.1 经典解决方案

经典的计算机视觉算法基于传统技术和启发式方法,重点在于显式手工设计的特征和基于规则的方法上,提供了明确定义的步骤和可解释性,用于边缘检测、角点检测和图像滤波等任务。然而,这些算法在处理复杂和大规模数据集时需要进行大量的参数调整,缺乏泛化能力,对手工设计的特征和显式的基于规则的方法的依赖限制了经典算法对不同数据集的适应性。虽然经典算法为计算机视觉任务提供了坚实的基础,但在面对复杂的现实场景时可能表现不佳。

1.2 现代解决方案

现代计算机视觉算法利用深度学习和神经网络,具有自动从数据中学习特征的能力,在图像分类、目标检测、语义分割和图像合成等任务中表现出色。通过从数据中学习分层表示,可以有效处理复杂模式,并实现最先进的性能。然而,这些算法需要大量标记数据进行训练,涉及计算密集型的训练和推理过程,并且由于它们的黑盒特性,缺乏可解释性。这些算法具备学习复杂和抽象特征的能力,能够处理各种数据并具有很好的泛化能力。深度学习已经彻底改变了计算机视觉,在各种任务中具备出色的性能表现,但在数据需求和计算资源等方面的挑战仍然存在。

2. 计算机视觉算法

在本节中,将介绍计算机视觉领域流行的经典算法,用于解决基础性任务。

2.1 形态学操作

形态学操作是一组用于分析和操作图像中对象形状和结构的图像处理技术,基于数学形态学原理,主要作用于二值或灰度图像。基本的形态学操作包括膨胀和腐蚀。膨胀通过向对象的边界添加像素来扩展对象的形状,从而产生更大更连通的区域;腐蚀通过从对象的边界移除像素来收缩对象,从而使对象变得更小和不连通。这些基本操作可以结合起来形成更高级的形态学操作,开运算是先进行腐蚀然后进行膨胀的过程,可以去除小对象并平滑边界;而闭运算则相反,先进行膨胀后进行腐蚀,可以填补间隙并去除对象中的小孔。
结构元素在形态学操作中起着至关重要的作用。结构元素是定义操作行为的核,它确定了操作过程中考虑的邻域的大小、形状和方向。通过选择不同的结构元素,可以根据特定的图像特征和目标调整形态学操作的效果。
形态学操作中的一个重要概念是连通性,连通性定义了像素或区域是否是相连或相邻的。它能够影响形态学操作的行为和结果,特别是在处理复杂或不规则形状的对象时。高级形态学操作包括形态学梯度、顶帽和底帽变换。形态学梯度突出显示对象的边界,而顶帽和底帽变换则分别强调与背景相比的明亮和暗区域。

2.1.1 腐蚀和膨胀

在给定图像上执行腐蚀操作:

import cv2
import numpy as np

# Read the input image
image = cv2.imread("1.jpeg")  

# Define the structuring element for erosion
kernel_size = 5
structuring_element = cv2.getStructuringElement(cv2.MORPH_RECT, (kernel_size, kernel_size))

# Perform image erosion
eroded_image = cv2.erode(image, structuring_element)

stacked_results = np.hstack((image, eroded_image)) 
# Display the original image and the eroded image
cv2.imshow('Erosion', stacked_results)

cv2.waitKey(0)
cv2.destroyAllWindows()
cv2.imwrite("erosion.jpg", stacked_results)

读取输入图像,并定义一个用于腐蚀的结构元素,在本节中,使用大小为 5x5 的矩形结构元素,可以调整 kernel_size 变量以更改结构元素的大小和形状。然后,使用 OpenCVerode() 函数应用腐蚀操作,传递输入图像和结构元素作为参数。最后,使用 OpenCVimshow() 函数显示原始图像和腐蚀后的图像。
腐蚀会从对象的边界移除像素,导致对象变得更小和不连通。腐蚀的效果取决于结构元素的大小、形状及其与图像中对象的关系,输出结果如下所示。

输出结果

接下来,对图像执行膨胀操作:

import cv2
import numpy as np

# Read the input image
image = cv2.imread("1.jpeg")

# Define the structuring element for dilation
kernel_size = 5
structuring_element = cv2.getStructuringElement(cv2.MORPH_RECT, (kernel_size, kernel_size))

# Perform image dilation
dilated_image = cv2.dilate(image, structuring_element)

stacked_results = np.hstack((image, dilated_image)) 
# Display the original image and the dilated image
cv2.imshow('Dilation', stacked_results)

# Wait for key press and then close all windows
cv2.waitKey(0)
cv2.destroyAllWindows()
cv2.imwrite("dilation.jpg", stacked_results)

读取输入图像,并定义一个用于膨胀的结构元素,在本节中,使用大小为 5x5 的矩形结构元素,可以调整 kernel_size 变量以更改结构元素的大小和形状。然后,使用 OpenCVdilate() 函数应用膨胀操作,传递输入图像和结构元素作为参数。最后,使用 OpenCVimshow() 函数显示原始图像和膨胀后的图像。
膨胀会向对象的边界添加像素,导致对象变得更大和更连通。膨胀的效果取决于结构元素的大小、形状及其与图像中对象的关系,输出结果如下所示:

输出结果

2.1.2 开运算与闭运算

开运算需要依次执行腐蚀和膨胀操作,有助于消除图像中的噪声:

import cv2
import numpy as np

# Read the input image
image = cv2.imread("1.jpeg")  

# Define the structuring element for erosion
kernel_size = 5
structuring_element = cv2.getStructuringElement(cv2.MORPH_RECT, (kernel_size, kernel_size))

# Perform image erosion
eroded_image = cv2.erode(image, structuring_element)

# Perform image dilation
opened_image = cv2.dilate(eroded_image, structuring_element)

# Display the original image and the opened image
stacked_results = np.hstack((image, opened_image)) 
cv2.imshow('Opened Image', stacked_results)

# Wait for key press and then close all windows
cv2.waitKey(0)
cv2.destroyAllWindows()
cv2.imwrite("opened.jpg", stacked_results)

读取输入图像,并定义了一个用于开运算的结构元素,在本节中,使用大小为 5x5 的矩形结构元素,可以调整 kernel_size 变量以更改结构元素的大小和形状。然后,通过先腐蚀图像,然后膨胀腐蚀后的图像来应用开运算。最后,使用 OpenCVimshow() 函数显示原始图像和执行开运算后的图像。
开运算通过依次执行腐蚀和膨胀来消除图像中的次要部分。开运算有助于平滑对象边界并移除图像中的孤立区域。开运算的有效性取决于结构元素的大小、形状及其与图像中对象的关系,代码输出结果如下所示。

输出

闭运算是开运算的反向操作,先进行膨胀,然后再进行腐蚀,可以消除图像前景对象中的小孔:

import cv2
import numpy as np

# Read the input image
image = cv2.imread("1.jpeg")  

# Define the structuring element for erosion
kernel_size = 5
structuring_element = cv2.getStructuringElement(cv2.MORPH_RECT, (kernel_size, kernel_size))

# Perform image dilation
dilated_image = cv2.dilate(image, structuring_element)

# Perform image erosion
closed_image = cv2.erode(dilated_image, structuring_element)

# Display the original image and the closed image
stacked_results = np.hstack((image, closed_image)) 
cv2.imshow('Closed Image', stacked_results)

# Wait for key press and then close all windows
cv2.waitKey(0)
cv2.destroyAllWindows()
cv2.imwrite("closed.jpg", stacked_results)

读取输入图像,并定义闭运算的结构元素。在本节中,使用大小为 5x5 的矩形结构元素,可以调整 kernel_size 变量以更改结构元素的大小和形状。然后,通过先膨胀图像,然后腐蚀膨胀后的图像来应用闭运算。最后,使用 OpenCVimshow() 函数显示原始图像和闭合后的图像。
闭运算结合了膨胀和腐蚀,以填充间隙并移除图像中的小孔,有助于平滑对象边界并移除图像中的孤立区域。闭运算的效果取决于结构元素的大小、形状及其与图像中对象的关系,代码输出结果如下图所示。

输出结果

形态学操作相对简单,但在计算机视觉中,它们是形状分析和操作的强大工具。它们的多功能性、保持对象边界的能力以及简单直观的实现使它们在各种图像处理任务中不可或缺,有助于提取有意义的信息并增强图像理解。

2.2 阈值化

阈值化是计算机视觉中的一种基本技术,用于根据像素强度将图像中的对象或感兴趣区域分离出来,是一种简单而强大的方法,根据指定的阈值确定像素属于前景还是背景。在阈值化中,图像中的每个像素都与阈值进行比较,如果像素强度高于阈值,则属于前景,否则,属于背景,从而创建一个二值图像,其中前景像素表示对象或感兴趣区域。阈值化在图像处理任务中应用广泛,如图像分割、目标检测和特征提取。当感兴趣区域与背景相比具有明显不同的强度特征时,阈值化技术十分有效。选择适当的阈值对于准确的结果至关重要,常用阈值化技术包括全局阈值化、自适应阈值化和大津法阈值化,这些方法根据局部或全局图像特征自动确定阈值,提高了分割的准确性。虽然阈值化是一种简单的技术,但它也有局限性,其假设前景和背景像素的强度分布能够很好地分隔,这在复杂图像中并不总是成立。光照变化、噪声和不均匀的背景可能会影响阈值化过程,导致不准确的结果。为了克服这些局限性,需要采用高级技术,如多级阈值化和多通道阈值化,这些方法利用额外的信息来改进分割结果并处理更复杂的情况。
接下来,对输入图像应用不同的阈值化算法(二值化、反二值化、截断、置零、反转置零和大津法),并可视化阈值图像:

import cv2
import numpy as np

# Read the input image
image = cv2.imread("1.jpeg", 0)

# Get the thresholding level from command line argument
threshold_level = int(127)

# Apply different thresholding algorithms
ret, thresh_binary = cv2.threshold(image, threshold_level, 255, cv2.THRESH_BINARY)
ret, thresh_binary_inv = cv2.threshold(image, threshold_level, 255, cv2.THRESH_BINARY_INV)
ret, thresh_trunc = cv2.threshold(image, threshold_level, 255, cv2.THRESH_TRUNC)
ret, thresh_tozero = cv2.threshold(image, threshold_level, 255, cv2.THRESH_TOZERO)
ret, thresh_tozero_inv = cv2.threshold(image, threshold_level, 255, cv2.THRESH_TOZERO_INV)
ret, thresh_otsu = cv2.threshold(image, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)

stacked_results = np.hstack((thresh_binary, thresh_binary_inv, thresh_trunc, thresh_tozero, thresh_tozero_inv, thresh_otsu))
# Create a window to display the thresholded images
cv2.namedWindow('Thresholding', cv2.WINDOW_NORMAL)
cv2.imshow('Thresholding', stacked_results)

# Wait for a key press and then close the window
cv2.waitKey(0)
cv2.destroyAllWindows()
cv2.imwrite("thresholding.jpg", stacked_results)

程序输出结果如下所示,从左到右使用的算法依次为二值化、反二值化、截断、阈值置零、反转阈值置零和大津法:

输出结果

将阈值设为 64,执行阈值化结果如下所示,从左到右使用的算法依次为二值化、反二值化、截断、阈值置零、反转阈值置零和大津法:

输出结果

阈值化是计算机视觉中的一种强大的技术,根据像素强度将图像中的对象或感兴趣区域分离出来,广泛应用于各种图像处理任务,同时也是更高级分割算法的基础。

2.3 边缘与角点检测

检测边缘和角点是用于识别和定位图像中重要特征的基本技术,这些特征在各种图像处理任务中起着关键作用,如对象识别、图像拼接和 3D 重建。边缘检测旨在识别像素强度的突变,通常对应于对象边缘或显著的图像结构,可以用于提取图像中对象的轮廓,常用的边缘检测算法包括 Canny 边缘检测器、Sobel 算子和高斯拉普拉斯算子。
而角点检测着重于识别图像梯度的急剧变化,表示边缘的交叉点或物体的角点。角点是可以用于图像跟踪和匹配的独特特征,常用的角点检测算法包括 Harris 角点检测器、Shi-Tomasi 角点检测器和 FAST 角点检测器。
边缘检测专注于捕获连续边界,角点检测则用于识别离散的、局部化的特征。因此,边缘检测算法通常对噪声更敏感,可能会产生较粗的边缘,而角点检测算法对噪声更具鲁棒性,可以提供精确的角点位置。
边缘和角点检测算法都依赖于图像梯度和局部图像特性的分析,通常涉及导数、卷积和阈值操作的计算,以识别所需的特征,能够为下游任务(如分割、对象识别和跟踪等)提供特征,但在复杂场景、遮挡和不同的光照条件下可能性能不佳;此外,参数选择和调整会显著影响检测结果。

import cv2
import numpy as np

# Read the input image
image = cv2.imread("1.jpeg")  

# Convert the image to grayscale
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)

# Apply different edge detection algorithms
canny_edges = cv2.Canny(gray, 100, 200)
sobel_x = cv2.Sobel(gray, cv2.CV_64F, 1, 0, ksize=3)
sobel_y = cv2.Sobel(gray, cv2.CV_64F, 0, 1, ksize=3)
laplacian_edges = cv2.Laplacian(gray, cv2.CV_64F, ksize=3)

# Create a window to display the images
cv2.namedWindow('Edge Detection', cv2.WINDOW_NORMAL)

# Display the original image and edge detection results side by side
stacked_results = np.hstack((canny_edges, sobel_x, sobel_y, laplacian_edges))
cv2.imshow('Edge Detection', stacked_results)

# Wait for a key press and then close the window
cv2.waitKey(0)
cv2.destroyAllWindows()
cv2.imwrite("edge_detect.jpg", stacked_results)

在输入图像上使用 Canny 边缘检测、Sobel 边缘检测和拉普拉斯边缘检测算法。程序读取输入图像,将其转换为灰度图像,并应用不同的边缘检测算法,包括 CannySobel (xy 梯度)和拉普拉斯。然后,使用 addWeighted() 将检测到的边缘叠加在原始图像上以组合图像。最后,使用 OpenCVimshow() 函数显示结果,显示在原始图像上叠加了边缘的图像。
程序输出结果如下所示,从左到右使用的算法依次为 Canny、在 x 轴上的 Sobel 导数、在 y 轴上的 Sobel 导数和拉普拉斯:

输出

边缘和角点检测是计算机视觉中重要的技术,用于识别重要的图像特征,为图像处理应用程序奠定了基础。

2.4 图像变换

图像转换是计算机视觉和图像处理中的基本操作,修改图像的外观或几何属性,可用于实现如纠正失真、改变透视、增强或操纵图像内容等目标。几何变换通常用于修改图像中像素的空间排列,包括缩放、旋转、平移和剪切,可用于修改图像中对象的大小、方向、位置和形状。仿射变换是一种保持平行线和距离比例的几何变换,仿射变换广泛用于图像对齐和透视校正等任务。非仿射变换,如投影变换,引入透视失真,可以进行更复杂的图像几何修改。投影变换适用于图像变形、3D 重建和虚拟现实应用等任务。除了几何变换外,还有其它图像增强和操纵技术,如对比度调整、亮度校正、颜色操纵和滤波,可用于提高视觉质量、增强特定的图像特征,或从图像中提取相关信息。
图像变换使用数学操作和算法操作图像的像素值实现,OpenCV 提供了内置函数和方法来高效地执行图像变换。

2.5 区域生长

区域生长是一种基于像素相似性进行图像分割的技术,将具有相似特征(如颜色或强度)的相邻像素组合成有意义的区域。区域生长算法从一个或一组初始种子像素开始,根据预定义的相似性标准迭代地将相邻像素添加到区域中,直到没有更多的像素符合相似性标准或达到停止条件。算法利用像素的空间连接性来形成连贯的区域,利用了图像中对象通常具有的空间连续性,即相邻像素往往具有相似的特性。
区域生长可用于各种应用,包括图像分割、对象提取和边界检测,通过将具有相似特征的像素分组从图像中提取有意义的结构。区域生长的结果取决于种子像素的选择和相似性标准的定义,选择合适的种子和相似性度量对于实现准确和可靠的结果至关重要。区域生长算法可以处理不同类型的图像,例如灰度或彩色图像,还可以结合额外的约束条件,例如梯度信息或纹理特征,以提升分割质量。
然而,区域生长算法在处理复杂场景、包含噪声或弱边界的图像时可能性能不佳,使用不合适的种子或相似性标准可能导致欠分割或过分割。算法的性能受到多种因素的影响,包括图像分辨率、对象大小以及遮挡或重叠对象的存在。为了提高区域生长的有效性,可以采用自适应区域生长方法。
OpenCV 中没有内置的算法支持区域生长,但可以使用诸如 k-Means 之类的统计方法来识别属于不同区域的像素聚类。

2.6 聚类

聚类是一种将相似数据点分组在一起的技术,通过根据它们在特征空间中的相似性将数据集分割成不同的簇,从而发现数据集中的模式或结构。聚类算法根据数据点在特征空间中的接近程度将数据点分配到不同簇中,特征的选择(如颜色、纹理或形状)取决于特定的应用和数据的特征。
聚类是一个迭代过程,旨在优化某个目标函数,例如最小化簇内距离或最大化簇间差异,聚类结果的收敛性和稳定性是重要的考虑因素。
流行的聚类算法包括 k-Means 聚类、层次聚类和谱聚类,这些算法采用不同的方法来定义簇的相似性并相应地分配数据点。其中一些属于无监督算法,即不依赖于标记数据,一些属于半监督算法,利用少量标记数据来引导聚类过程。聚类算法的有效性取决于多个因素,包括所使用特征的质量、距离或相似性度量的选择以及算法的参数。
确定要簇的数量是聚类的关键,可以基于先前的知识预定义,也可以使用诸如轮廓分析等技术自动确定。对图像执行 k-Means 聚类:

import cv2
import numpy as np
# Read the input image
image = cv2.imread("1.jpeg")  

# Reshape the image to a 2D array of pixels
pixels = image.reshape((-1, 3))

# Convert the pixel values to float
pixels = np.float32(pixels)

# Define the parameters for k-means clustering
num_clusters = 5
criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 10, 1.0)
flags = cv2.KMEANS_RANDOM_CENTERS

# Apply k-means clustering
_, labels, centers = cv2.kmeans(pixels, num_clusters, None, criteria, 10, flags)

# Convert the centers to 8-bit values
centers = np.uint8(centers)

# Map each pixel to its corresponding cluster center
segmented_image = centers[labels.flatten()]
segmented_image = segmented_image.reshape(image.shape)

# Convert the segmented image to RGB for visualization
segmented_image_rgb = cv2.cvtColor(segmented_image, cv2.COLOR_BGR2RGB)

# Display the original image and segmented image side by side
stacked_results = np.hstack((image, segmented_image_rgb))
cv2.imshow('Clustering', stacked_results)

# Wait for a key press and then close the window
cv2.waitKey(0)
cv2.destroyAllWindows()
cv2.imwrite("clustering.jpg", stacked_results)

读取输入图像并将其重塑为像素的二维数组,然后应用 k-Means 算法,使用生成的标签来创建一个图像,在原始图像上叠加聚类区域。最后,使用 OpenCVimshow() 函数显示原始图像和聚类结果。

输出

聚类用于将相似数据点分组并发现有意义的模式,可以用于图像分割和物体识别等任务,从而实现对视觉数据的有效分析和解释。聚类算法可以处理各种图像数据,但在处理嘈杂或模糊的数据、重叠的簇或非凸形状时可能性能不佳。

2.7 模板匹配

模板匹配是计算机视觉中的一种技术,涉及在较大的搜索图像中查找模板图像,通常用于在图像中定位特定对象或模式的实例。
模板匹配过程将模板图像(也称为模式或参考图像)与搜索图像的不同区域进行比较,目标是在模板和搜索图像中的相应区域之间找到最佳匹配。匹配过程可以使用不同度量标准(如相关性、平方差的总和或归一化互相关)来测量模板和图像区域之间的相似性,这些度量标准表示模板与搜索图像中每个区域相似性的数值分数。可以通过缩放或旋转模板图像,在不同的比例和方向上执行模板匹配,能够在搜索图像中检测大小或方向变化的对象。模板匹配具有多种应用,包括对象识别、对象跟踪和图像匹配,广泛应用于人脸识别、字符识别和文档分析等任务中。例如在下图中,如果要计算左图中圆的数量,就可以使用模板匹配算法,使用右图作为模板,编写模板匹配程序:

模板匹配

import cv2
import numpy as np

# Read the search image and the template image
search_image = cv2.imread('SearchImage.jpg', cv2.IMREAD_COLOR)  
template_image = cv2.imread('Template.jpg', cv2.IMREAD_COLOR)  
# Convert the images to grayscale
search_gray = cv2.cvtColor(search_image, cv2.COLOR_BGR2GRAY)
template_gray = cv2.cvtColor(template_image, cv2.COLOR_BGR2GRAY)

# Perform template matching
result = cv2.matchTemplate(search_gray, template_gray, cv2.TM_CCOEFF_NORMED)

# Set a threshold for the match score
threshold = 0.6

# Find the locations where the match score is above the threshold
locations = np.where(result >= threshold)

# Draw rectangles around the matched regions
for pt in zip(*locations[::-1]):
    bottom_right = (pt[0] + template_gray.shape[1], pt[1] + template_gray.shape[0])
    cv2.rectangle(search_image, pt, bottom_right, (0, 255, 0), 2)

# Display the search image with the matched regions
cv2.imshow('Template Matching Result', search_image)

# Wait for key press and then close the window
cv2.waitKey(0)
cv2.destroyAllWindows()
cv2.imwrite("template_matching.jpg", search_image)

使用 cv2.imread() 读取搜索图像和模板图像,然后,使用 cv2.cvtColor() 将图像转换为灰度图像。接下来,程序使用 cv2.matchTemplate() 执行模板匹配,将灰度搜索图像和模板图像作为参数传递,得到表示模板与搜索图像不同区域之间相似性的相关性图。设置阈值来确定基于匹配得分的匹配项,在本节中,阈值设置为 0.6,可以根据需要进行调整。程序使用 np.where() 找到匹配得分高于阈值的位置,然后,使用 cv2.rectangle() 在搜索图像上绘制匹配区域的矩形,最后,使用 cv2.imshow() 显示带有匹配区域的搜索图像。
运行程序后,可以看到在匹配区域周围绘制了矩形的搜索图像,如下图所示,可以通过修改阈值参数改进运行结果:

输出

模板匹配对光照条件、噪声和遮挡的变化敏感,在复杂场景中,可能会出现误报或不准确的匹配。为了解决这些挑战,通常采用多尺度模板匹配、基于特征的方法和机器学习方法等方法进行改进。这些技术旨在提高模板匹配任务的稳健性、准确性和效率。

2.8 分水岭算法

分水岭算法是一种基于分水岭线或边界概念的图像分割技术,模拟了水在地形图中流动的行为,其中流域代表图像中的不同区域或对象。
分水岭算法首先将灰度图像或梯度图像视为地形图,使用强度值表示海拔高度。然后,算法识别图像中的局部极小值,这些极小值作为区域的初始标记或种子。接下来,算法执行一种流动过程,流域从标记开始被水填满,随着水填满流域,自然地根据分水岭线分隔相邻区域,分水岭线代表图像中不同对象或区域之间的边界。分水岭算法根据实现过程的不同可以分为两类,包括基于溢流的算法和基于标记的算法。基于标记的算法能够通过手动或自动放置标记来更好地控制分割过程。
分水岭算法能够处理复杂的图像结构,包括具有不规则形状和重叠边界的对象,特别适用于对象之间的边界不明确或存在强烈的强度梯度的情况。然而,分水岭算法可能会产生过度分割,即边界过于碎片化,为了解决这个问题,可以应用后处理步骤,如区域合并技术。
接下来,应用分水岭算法分割输入图像:

import numpy as np
import cv2

image = cv2.imread("1.jpeg")

gray = cv2.cvtColor(image,cv2.COLOR_BGR2GRAY)
_, thresh = cv2.threshold(gray,0,255,cv2.THRESH_BINARY_INV+cv2.THRESH_OTSU)

kernel_size = 5
kernel = np.ones((kernel_size,kernel_size),np.uint8)

# Find area which is surely background
sure_bg = cv2.dilate(thresh,kernel,iterations=1)

# Find are which is surely foreground
dist_transform = cv2.distanceTransform(sure_bg,cv2.DIST_L2,3)
_, sure_fg = cv2.threshold(dist_transform,0.05*dist_transform.max(),255,0)
sure_fg = np.uint8(sure_fg)

# Find the region which is neither surely foreground nor surely background
unknown = cv2.subtract(sure_bg,sure_fg)

# Marker labelling
_, markers = cv2.connectedComponents(sure_fg)

# Add one to all labels so that sure background is not 0, but 1
markers = markers+1

# Now, mark the region of unknown with zero
markers[unknown==255] = 0

markers = cv2.watershed(image,markers)
image[markers == -1] = [0,0,0]

# The next 3 steps are needed only for better visibilty in publishing. 
structuring_element = cv2.getStructuringElement(cv2.MORPH_RECT, (kernel_size, kernel_size))
image = cv2.erode(image, structuring_element)
image = cv2.erode(image, structuring_element)

cv2.imshow('watershed', image)
cv2.waitKey(0)
cv2.destroyAllWindows()
cv2.imwrite("watershed.jpg",image)

使用 cv2.imread() 读取输入图像。然后,使用 cv2.cvtColor() 将图像转换为灰度图像。接下来,使用 cv2.threshold()Otsu 阈值应用于灰度图像,使用 cv2.dilate() 和定义的核对阈值图像进行膨胀操作,这些预处理步骤有助于识别背景区域。接着,使用 cv2.distanceTransform() 对背景图像应用距离变换。利用距离变换,使用 cv2.threshold() 对前景区域进行阈值处理,阈值设置为最大距离变换值的 0.05 倍。然后,使用 cv2.subtract() 从背景中减去前景,以识别未知区域。接着,对前景进行标记,使用 cv2.connectedComponents() 将标签分配给连接的组件。经过一些数学运算以清晰区分前景、背景和未知区域后,使用 cv2.watershed() 对输入图像应用分水岭算法,传递图像和标记作为参数。结果存储在标记中,在分水岭结果中,标记为 -1 的区域表示边界区域。程序使用布尔索引 (markers == -1) 将输入图像中相应的像素设置为黑色 [0,0,0],并将其分配给图像。为了更好地显示,可以使用 cv2.erode() 和由 cv2.getStructuringElement() 定义的结构元素对图像进行腐蚀。最后,使用 cv2.imshow() 显示结果图像,使用 cv2.imwrite() 将分水岭结果图像保存为 watershed.jpg

输出

对图像进行预处理增强边界或梯度,使用形态学运算或梯度算子等技术,是为了获得准确的分割结果。然而,并不能保证相同的操作在不同的图像上能够得到类似的提升。

2.9 前景和背景检测

在计算机视觉中,分离图像的前景和背景是一个基本问题,需要将图像分割,以区分对象或感兴趣区域(前景)与周围环境(背景)。
OpenCV 提供了 GrabCut 算法能够实现自动前景/背景分割。GrabCut 结合了图像数据和用户提供的信息,迭代地改进分割结果。GrabCut 算法从前景和背景区域的初始种子区域开始,用户可以通过边界框或一组标记来提供这个初始种子,以指示前景和背景区域。基于初始种子,GrabCut 将图像建模为马尔可夫随机场,并使用能量最小化方法迭代更新前景和背景。算法根据图像中的颜色相似性、空间接近性和像素连接性调整估计值。通过多次迭代,GrabCut 通过优化能量函数和调整分割边界来改进分割结果。当分割结果稳定时,表明前景和背景之间的分离完成,算法收敛。GrabCut 可以处理复杂的图像结构,包括具有复杂边界和不同外观的对象,算法还能够适应不同的图像,并且相对于光照条件、颜色和纹理的变化相对稳健。然而,GrabCut 的性能在很大程度上依赖于用户提供的初始种子,准确初始化前景和背景区域对于获得令人满意的结果至关重要,错误的初始种子可能导致过分割或欠分割,前景的部分可能被错误分类为背景,反之亦然。接下来,使用 Grabcut 算法执行前景/背景分割:

import cv2
import numpy as np

# Read the image
image = cv2.imread('1.jpeg')  

# Create a mask to indicate the areas of the image to be classified (foreground, background, etc.)
mask = np.zeros(image.shape[:2], np.uint8)

# Define the rectangle enclosing the foreground object (top left and bottom right coordinates)
rect = (225, 225, 850, 850)  # Adjust the coordinates based on the region of interest

# Perform the GrabCut algorithm
bgd_model = np.zeros((1, 65), np.float64)
fgd_model = np.zeros((1, 65), np.float64)
cv2.grabCut(image, mask, rect, bgd_model, fgd_model, 5, cv2.GC_INIT_WITH_RECT)

# Create a mask where all probable foreground and foreground pixels are set to 1
foreground_mask = np.where((mask == 2) | (mask == 0), 0, 1).astype('uint8')

# Apply the mask to the original image
segmented_image = image * foreground_mask[:, :, np.newaxis]

# Display the original image and the segmented image side by side
image = cv2.rectangle(image, (rect[0],rect[1]), (rect[2],rect[3]), (0,0,0), 3)
combined_image = np.hstack((image, segmented_image))
cv2.imshow('Original vs Segmented', combined_image)
cv2.waitKey(0)
cv2.destroyAllWindows()
cv2.imwrite("grabcut.jpg", combined_image)

首先使用 cv2.imread() 读取图像,然后创建一个初始掩码,用于指示图像中要分类为前景、背景等的区域。接下来,定义一个矩形来界定前景对象,可以根据图像中感兴趣的区域调整矩形坐标。使用 cv2.grabCut() 执行 GrabCut 算法,接受图像、掩码、矩形、背景模型、前景模型、迭代次数和初始化模式作为参数。算法更新掩码以将像素分类为前景、背景等,根据更新后的掩码创建一个二进制掩码 foreground_mask,将前景和前景像素设置为 1,而背景和背景像素设置为 0。最后,使用逐元素乘法将掩码应用于原始图像,从而得到分割后的图像,最后,使用 cv2.imshow() 显示原始图像和分割后的图像。
运行程序,结果如下图所示,可以看到左侧是原始图像,右侧是分割后的图像,前景对象突出显示。此外,在左侧图像中,绘制了一个黑色矩形,用于显示初始种子区域。

结果

2.10 超像素

超像素用来将像素分组成有意义的原子区域,旨在通过提供图像的更高级表示来减少图像处理任务的复杂性。原子区域是根据空间接近性和颜色相似性来分组像素形成的,这种分组过程允许创建紧凑且同质的区域,保留图像中重要的边界和结构。通过将像素分组成超像素,可以显着减少要分析的图像基元的数量,使后续算法更加健壮,对噪声和小变化不太敏感。超像素封装了像素之间的上下文信息和空间关系,减少了图像处理任务的复杂性,保留了边界,并提供了图像的更高级表示。
超像素可以使用不同的算法生成,如简单线性迭代聚类、QuickShiftWatershed。这些算法使用不同的标准(如颜色、空间接近性和图像梯度)定义超像素边界。超像素的大小和形状可以调整,在捕获细节和保留图像整体结构之间取得平衡,这种灵活性用于适应不同的任务和图像特征,选择超像素算法取决于任务的具体要求和图像的特征。
超像素可以用作特征提取、基于区域的目标检测和图像分类算法的输入,提供信息丰富的表示,可以用于图像分割、目标跟踪和图像增强等任务。通过提供图像的更有效表示,可以加快处理速度并降低计算成本。

2.11 图像金字塔

图像金字塔用于创建不同尺度的一系列图像,提供了图像的多分辨率表示,允许在不同级别的细节上进行高效的处理和分析。在图像金字塔中,原始图像重复地下采样或上采样,以创建一系列分辨率递减或递增的图像,通过应用预定义的缩放因子或使用特定的插值技术实现。金字塔结构能够处理由于尺度变化、视点变化和遮挡等因素引起的图像变化,为在不同细节级别上匹配和比较特征提供了一个稳健的框架。可以单独访问金字塔级别,以根据所需的细节级别进行选择性处理,这种灵活性使得图像金字塔适用于自适应算法,根据可用的图像分辨率调整其行为。
图像金字塔可以使用多种算法构建,包括层次化方法和小波变换。基于金字塔的技术,如拉普拉斯金字塔和高斯金字塔,通常用于图像表示和处理。拉普拉斯金字塔将图像分解为一系列带通滤波的图层,而高斯金字塔则创建一系列平滑的和下采样图像。金字塔的大小、缩放因子和插值方法的选择取决于特定的应用和输入图像的特征,根据需要进行调整以在保留重要图像特征和减少计算开销之间取得平衡。接下来,创建图像金字塔:

import cv2

# Read the input image
image = cv2.imread("1.jpeg")  

# Display the original image
cv2.imshow('Original Image', image)

# Generate and display the image pyramid
pyramid_image = image.copy()
pyramid_images = [pyramid_image]

while pyramid_image.shape[0] > 100 and pyramid_image.shape[1] > 100:  # Adjust the condition to control the pyramid size
    pyramid_image = cv2.pyrDown(pyramid_image)
    pyramid_images.append(pyramid_image)

for i, image_level in enumerate(pyramid_images):
    cv2.imshow(f'Pyramid Level {i}', image_level)

# Wait for key press and then close all windows
cv2.waitKey(0)
cv2.destroyAllWindows()

读取输入图像,然后通过重复使用 OpenCVpyrDown() 函数对图像进行下采样来生成金字塔。金字塔的每个级别都存储在 pyramid_images 列表中。程遍历金字塔图像并显示,while 循环中的条件控制金字塔的大小。运行程序,可以看到原始图像以及表示图像金字塔不同级别的一系列图像。

结果

图像金字塔通过在不同尺度上执行操作并捕获不同大小的对象,实现了有效的对象检测、识别和跟踪,在图像融合、图像压缩和特征提取等应用中应用广泛,能够在不同分辨率下无缝融合图像,并实现图像的高效存储和传输。

2.12 卷积

卷积是一种将两个模拟信号组合成新输出信号的基本操作,利用数学运算测量固定大小窗口(核)与输入信号段之间的重叠部分。核在输入信号上移动,在每个位置,相应的值逐像素相乘并求和以产生一个输出值。能够从信号中提取特定的模式或特征,从而对原始信号进行分析、增强和操纵。
卷积利用数学运算,将一个小矩阵(称为核或滤波器)与图像组合,以产生一个新的输出图像。核通常是一个带有数值的小正方形或矩形矩阵,卷积操作通过在图像上滑动核执行,计算每个位置的核邻域中像素值的加权和,然后将加权和结果分配给输出图像中的相应像素。卷积能够用于执行各种图像处理任务,如平滑、锐化、边缘检测和特征提取。通过选择不同类型的核,可以实现不同的图像增强和转换。卷积过程对图像中的每个像素应用局部操作,从而实现了对局部图像特征的提取和空间关系的保留。
OpenCV内置了用于执行卷积的函数,接下来,使用卷积进行图像去噪:

import cv2
import numpy as np

# Read the input image
image = cv2.imread("1.jpeg")  

# Define the kernel for de-noising convolution
kernel = np.array([[1, 1, 1],
                   [1, -7, 1],
                   [1, 1, 1]])

# Perform image convolution
convolved_image = cv2.filter2D(image, -1, kernel)

stacked_results = np.hstack((image, convolved_image)) 
# Display the original image and the convolved output image
cv2.imshow('Convolution', stacked_results)

# Wait for key press and then close all windows
cv2.waitKey(0)
cv2.destroyAllWindows()
cv2.imwrite("convolution.jpg", stacked_results)

读取带噪声的输入图像,并为卷积定义了一个卷积核,在本节中,使用了一个增强边缘并抑制噪声的核,可以使用不同的核尝试不同的去噪效果。然后,使用 OpenCVfilter2D() 函数应用去噪卷积操作。最后,使用 OpenCVimshow() 函数显示带噪声的输入图像和去噪输出图像。
运行程序,可以看到带噪声的输入图像和去噪输出图像。需要注意的是,去噪的有效性取决于噪声的特性和所使用的卷积核。需要根据具体图像尝试不同的核或去噪技术,以获得最佳结果。

结果

卷积是基于深度学习的图像处理中的基本操作。其具有的特征提取并保持空间关系、平移不变性的能力使其成为构建强大有效的深度学习模型的关键组成部分。

小结

经典算法在计算机视觉领域方面发挥了关键作用,为理解和分析视觉数据提供了坚实的基础,使机器能够从图像和视频中解释和提取有意义的信息。

系列链接

OpenCV计算机视觉实战(1)——计算机视觉简介
OpenCV计算机视觉实战(2)——环境搭建与OpenCV简介
OpenCV计算机视觉实战(3)——计算机图像处理基础
OpenCV计算机视觉实战(4)——计算机视觉核心技术全解析
OpenCV计算机视觉实战(5)——图像基础操作全解析