计算机视觉中的图像分割是将图像划分为多个部分的过程,每个部分具有相同或相似的视觉特征。它是图像理解和计算机视觉高级任务的基础,常用于物体识别、人脸识别、医学图像分析等领域。
阈值法是计算机视觉中的一种简单图像分割方法。它选择一个阈值,大于该阈值的像素归为一类,小于阈值的像素归为另一类。
1. 计算图像的直方图,找到图像的谷值或峰值作为阈值;或者根据需求手动设定一个阈值。
2. 遍历图像中的每个像素,如果像素值大于阈值,则归类为前景;否则归类为背景。
3. 根据分类结果,前景像素可视为一个区域,背景像素可视为另一个区域,实现图像的二值分割。
有一幅包含明暗两块区域的图像,明暗两区域像素值差异较大,要实现两区域的分割。
1. 计算图像直方图,由于两区域像素值差距较大,直方图中存在两个峰值,选择两个峰值之间的谷值作为阈值。
2. 以阈值为标准,大于阈值的像素点归类为明区域,小于阈值的像素点归类为暗区域。
3. 根据分类结果,得到两块分割区域,实现图像的分割。
import cv2
import numpy as np
# 读取图像
img = cv2.imread('image.jpg')
# 转换为灰度图像
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# 计算图像直方图
hist = cv2.calcHist([gray], [0], None, [256], [0, 256])
# 找到直方图的两个峰值之间的谷值作为阈值
thresh = 50
# 设定阈值,大于阈值的为前景像素
binary = gray > thresh
# 根据二值图像绘制矩形框
contours, hierarchy = cv2.findContours(binary, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
for cnt in contours:
x, y, w, h = cv2.boundingRect(cnt)
cv2.rectangle(img, (x, y), (x + w, y + h), (0, 255, 0), 2)
# 显示图像和直方图
cv2.imshow('Image', img)
cv2.imshow('Histogram', hist)
cv2.imshow('Binary', binary)
cv2.waitKey(0)
1. 读取图像并转为灰度图像。
2. 计算灰度图像的直方图。找到两个峰值之间的谷值作为阈值。
3. 使用阈值对灰度图像进行二值化,得到分割结果。
4. 在原彩色图像上绘制矩形框,显示分割结果。
5. 显示原图像、直方图与二值化结果。
该示例利用简单阈值法实现了图像的二值分割。但是可以看到,对于复杂情况如同时包含三个以上区域,或者像素变化比较连续的图像,阈值法的效果会比较差,无法完成精细的语义分割。
阈值法的优点是简单易行,计算量小。但是它有几个显著缺点。
1. 无法处理像素值变化复杂的情况,只适合二值分割。
2. 阈值的选择会对结果有较大影响,阈值选取不当会导致分割错误。
3. 无法利用图像的纹理、颜色等信息,其分割结果比较粗糙。
4. 对噪声比较敏感,噪声会对结果产生较大影响。
阈值法属于一种较为初级的图像分割方法。对于高级视觉任务,更倾向于使用基于边缘检测、区域生长以及深度学习的分割方法,这些方法可以针对图像的语义与内容进行更精细的分割与理解。
检测图像中的边缘和轮廓,把边界内的区域分割出来。效果较好但边缘连接问题较多。
图像分割的边缘检测主要有灰度化、平滑化、计算梯度、非最大值抑制等步骤
将RGB图像转换为灰度图像,减少颜色信息对边缘检测的影响。
使用高斯平滑、中值滤波等方法平滑图像,去除噪音。平滑化可以使边缘更加连续清晰。
使用Sobel、Canny等 operator 计算图像梯度,检测图像明显变化的地方。梯度的大小表示边缘强度,梯度的方向表示边缘方向。
只保留局部最大的梯度值,抑制边缘方向垂直方向的梯度值。使得边缘变得更加清晰连续。
使用高低两个阈值检测真实边缘。高阈值检测出确定的边缘,低阈值检测出可能的边缘。
使用8连通或4连通方法连接高阈值检测出的边缘段。连接断开的边缘,形成闭合的边缘链。
使用一定的规则产生尽量连续的边缘线条。使分割结果看上去更加平滑清晰。
这是图像分割中经典的边缘检测流程,不同的算法在具体的步骤的实现上会有差异,但总体思路是相似的。边缘检测的好坏直接决定了图像分割的效果。
Canny算法是计算机视觉中最流行和最有效的边缘检测算法之一。这里给出一个基本的Canny边缘检测算法的代码示例,包括梯度计算、非最大值抑制和双阈值检测等。
import cv2
import numpy as np
def canny(image):
# 转换为灰度图像
gray = cv2.cvtColor(image, cv2.COLOR_RGB2GRAY)
# 高斯平滑
blur = cv2.GaussianBlur(gray, (5, 5), 0)
# 计算梯度
xgrad = cv2.Sobel(blur, cv2.CV_16SC1, 1, 0)
ygrad = cv2.Sobel(blur, cv2.CV_16SC1, 0, 1)
# 计算边缘梯度和方向
edge_grad = np.hypot(xgrad, ygrad)
edge_dir = np.arctan2(ygrad, xgrad)
# 非最大值抑制
edge_grad = non_max_suppression(edge_grad, edge_dir)
# 双阈值检测和连接边缘
low_threshold = 80
high_threshold = 120
edge = cv2.Canny(edge_grad, low_threshold, high_threshold)
return edge
def non_max_suppression(edge_grad, edge_dir):
...
src = cv2.imread('xxx.jpg')
edge = canny(src)
cv2.imshow('edge', edge)
cv2.waitKey(0)
选择图像中的种子点,根据相邻像素的相似性不断吸收周围像素进行区域生长,直到生长停止。效果较好但种子点选择关键。
区域生长法是一种基于连通性的图像分割方法。其基本思想是:
1. 选择一些种子点作为生长起点,这些种子点通常选择图像中的一些特征点。
2. 根据种子点周围像素与种子点的相似度,决定将哪些周围像素归为同一个区域。相似度高的像素会被归入种子区域。
3. 新归入的像素也可以成为生长点,继续吸收周围相似的像素。这个过程会不断重复,直到没有更多的像素可以被吸收为止。
4. 最后,图像被分割成多个由生长点连接的区域。
区域生长法的主要参数有:
1. 种子点:选择影响分割结果的关键。通常选择图像特征点作为种子点。
2. 相似度测量:决定像素是否被归入种子区域的关键。常用的有颜色相似度、灰度相似度等。
3. 生长策略:决定从种子点开始如何向外部生长。常见的有4连通、8连通生长等。
4. 生长终止条件:什么条件下停止生长,影响最终分割结果的粒度。
这里是一段区域生长法的代码示例。
import cv2
import numpy as np
def region_growing(image, seed_point, threshold):
'''
image: 输入图像
seed_point: 种子点坐标(行,列)
threshold: 相似度阈值
'''
height, width = image.shape
# 获取图像高度和宽度
seed_x, seed_y = seed_point # 获得种子点坐标
seed_value = image[seed_x, seed_y] # 获取种子点灰度值
region = np.zeros_like(image) # 创建一个与图像大小相同的图像,初始化为0
region[seed_x, seed_y] = 1 # 将种子点所在位置的值置为1
neighbors = [] # 创建一个列表来存储生长点坐标
neighbors.append((seed_x, seed_y)) # 将种子点添加到生长点列表
while len(neighbors) > 0: # 生长点列表不为空,则一直生长
current_point = neighbors.pop(0) # 获取列表中第一个生长点
x, y = current_point # 获得生长点坐标
for offset_x, offset_y in [(-1, 0), (1, 0), (0, -1), (0, 1)]:
# 4连通生长,Offsets代表生长方向
new_x = x + offset_x # 计算新点坐标
new_y = y + offset_y
if 0 <= new_x < height and 0 <= new_y < width: # 判断新点是否在图像范围内
diff = abs(image[new_x, new_y] - seed_value) # 计算新点与种子点的灰度差
if region[new_x, new_y] == 0 and diff < threshold:
# 如果新点未被访问且与种子点灰度差在阈值范围内
region[new_x, new_y] = 1 # 将新点设置为已访问
neighbors.append((new_x, new_y)) # 添加新点到生长点列表
return region
这个示例函数实现了区域生长法的所有步骤。我们选择一个种子点和阈值,从种子点开始4连通生长,不断吸收与种子点相似的新点,当生长点列表为空时生长结束。返回一个只包含生长区域的二值图像。
该算法简单直观,但生长结果很依赖于种子点选择和阈值设置,容易造成过分割或漏分割。所以通常将其与其他更稳定的算法结合使用。
区域生长法的优点是直观简单,比较符合人眼对图像区域的感知。但是很难保证能分割出我们想要的整体区域,容易由于种子点选取和相似度测量等问题而导致过分割或未分割的现象。所以,区域生长法常与其它分割方法结合使用。
区域生长法在医学图像处理、遥感图像处理等领域有较广泛的应用。但作为一种基本的图像分割方法,其思想对很多其它更复杂的图像分割算法也有重要影响。
通过少量像素的标注信息传播到未标注的像素,实现图像的自动分割。效果较好,但需要大量标注数据以训练模型。
标注传播是一种基于先验知识的图像分割方法。其基本思想是:
1. 首先,训练图像需要事先标注,也就是人工定义图像中每个像素属于的类别。这些标注作为模型的先验知识。
2. 然后,对于新输入的未标注图像,算法会先选择小部分像素的标注作为初始种子。这些种子像素可以随机选择,也可以基于一定特征选择。
3. 最后,算法从这些种子像素开始,根据图像中的局部特征和先验知识模型,预测其周围像素的类别,不断传播,直到图像全部被标注。
标注传播的关键在于构建一个好的先验知识模型。常用的方法有:
1. 最近邻分类:直接使用训练集中与当前像素最相似的像素的类别作为预测类别。
2. 贝叶斯分类:基于贝叶斯定理,计算各个类别在当前像素位置出现的概率,选择概率最大的类别。
3. 决策树:训练一个决策树,基于像素特征选择最可能的类别。
4. 随机森林:训练多个决策树,综合所有的树得到最终预测类别。
5. 神经网络:使用卷积神经网络等作为分类模型进行预测。
这里是一个简单的标注传播算法实现。
import cv2
import numpy as np
from sklearn.neighbors import KNeighborsClassifier
# 训练数据及标注
train_data = np.load('train_data.npy')
train_label = np.load('train_label.npy')
# KNN模型
knn = KNeighborsClassifier(n_neighbors=3)
knn.fit(train_data, train_label)
def propagation(image, seeds):
'''
image: 输入图像
seeds: 初始种子点及对应类别,如[(x1,y1,c1),(x2,y2,c2),...]
'''
height, width = image.shape # 获取图像大小
labeled = np.zeros((height, width)) # 初始化标注矩阵
# labeled[seeds[:,0], seeds[:,1]] = seeds[:,2] # 设置种子点类别
Q = []
for seed in seeds:
Q.append(seed)
labeled[seed[0], seed[1]] = seed[2] # 将种子点类别信息添加到标注矩阵
while Q: # 列表不为空则继续传播
current = Q.pop(0)
x, y, category = current # 获取当前点坐标及类别
# 使用KNN模型预测4邻域点的类别
for x_offset, y_offset in [(-1, 0), (1, 0), (0, -1), (0, 1)]:
x_neighbor, y_neighbor = x + x_offset, y + y_offset
if 0 <= x_neighbor < height and 0 <= y_neighbor < width:
neighbor = image[x_neighbor, y_neighbor]
pred = knn.predict([neighbor]) # 使用KNN预测类别
if labeled[x_neighbor, y_neighbor] == 0: # 邻域点未被标注
labeled[x_neighbor, y_neighbor] = pred # 标注赋值
Q.append((x_neighbor, y_neighbor, pred)) # 添加新点到传播列表
return labeled
src = cv2.imread('xxx.jpg') # 输入图像
seeds = [(50, 50, 1), (100, 100, 2)] # 选择两个种子点及类别
result = propagation(src, seeds) # 执行标注传播
cv2.imshow('result', result * 50)
cv2.waitKey(0)
这个示例完成了一个简单的标注传播实现。我们首先训练一个KNN模型作为分类器。然后选择两个种子点,设置对应的类别。从这两个种子点开始,使用KNN模型预测其4邻域点的类别,并不断传播,直到图像全部被标注。
可以看到,该方法的效果高度依赖于训练集的质量和种子点的选择。但当得到良好的模型和种子点时,其分割精度会非常高。
标注传播的优点是可以很好地利用图像先验知识,使分割结果具有更高的准确率。但是其结果也依赖于网络训练的数据集的质量。训练数据集标注不当会直接导致结果误差较大。
标注传播应用于医学图像分割、遥感图像分割以及一般场景图像的语义分割等任务,具有较高的实用价值。相比于其它无监督方法,标注传播可以达到较高的精度,这也是其优势所在。
使用深度学习网络如FCN、U-Net等,通过大量训练数据实现图像的自动分割。效果最好,是目前图像分割的主流方法。
深度学习在图像分割领域有着越来越广泛的应用,主要有以下几个方向:
1. 全卷积网络(FCN):将分类网络的全连接层替换为卷积层,可以产生像素级的预测结果,用于图像语义分割。FCN融合了分类网络的高层语义信息和低层位置信息,使得分割结果既能准确标注语义类别,也能很好地符合目标物体的形状。
2. 编码器-解码器网络:使用编码器层逐层抽取图像的语义特征,解码器层逐层恢复图像空间信息,输出精细的像素级分割结果。典型网络有U-Net、SegNet等。这类网络可以学习更加抽象的语义概念,产生更加准确的分割。
3. 空洞卷积网络(Dilated ConvNets):使用空洞卷积层来获取更大感受野,捕捉更高层次的语义上下文,用于细粒度的图像分割。典型网络如DeepLab系列等。
4. Attention机制:引入注意力机制,可以自适应地聚焦于输入图像的重要部分,用于场景理解和精细分割。典型网络如DANet、Attention U-Net等。
5. Conditional Random Field (CRF):将深度学习网络的输出作为CRF模型的输入,利用CRF层进一步优化分割结果的连续性和平滑性。
以上方法都是在深度卷积神经网络的框架下,通过设计更加高效的网络结构、损失函数、后处理等手段来解决图像分割任务。这些方法已经在许多数据集上达到最高的分割性能,并广泛应用于医疗影像、自动驾驶等领域。
深度学习使得图像分割比以往任何时候都更加智能和高效。但其结果的可解释性较差,对数据集依赖性较大,这也是其需要继续努力的方向。总体来说,深度学习是图像分割未来发展的主流方向。
这里以U-Net作为例子,给出深度学习图像分割的代码示例。
import torch
import torch.nn as nn
import torch.nn.functional as F
class UNet(nn.Module):
def __init__(self, n_channels, n_classes):
super(UNet, self).__init__()
self.inc = inconv(n_channels, 64)
self.down1 = down(64, 128)
self.down2 = down(128, 256)
self.down3 = down(256, 512)
self.down4 = down(512, 512)
self.up1 = up(1024, 256)
self.up2 = up(512, 128)
self.up3 = up(256, 64)
self.up4 = up(128, 64)
self.outc = outconv(64, n_classes)
def forward(self, x):
x1 = self.inc(x)
x2 = self.down1(x1)
x3 = self.down2(x2)
x4 = self.down3(x3)
x5 = self.down4(x4)
x = self.up1(x5, x4)
x = self.up2(x, x3)
x = self.up3(x, x2)
x = self.up4(x, x1)
x = self.outc(x)
return x
def inconv(in_channels, out_channels):
return nn.Sequential(
nn.Conv2d(in_channels, out_channels, 3, padding=1),
nn.BatchNorm2d(out_channels),
nn.ReLU(inplace=True)
)
def down(in_channels, out_channels):
return nn.Sequential(
nn.MaxPool2d(2),
nn.Conv2d(in_channels, out_channels, 3, padding=1),
nn.BatchNorm2d(out_channels),
nn.ReLU(inplace=True)
)
def up(in_channels, out_channels):
return nn.Sequential(
nn.Upsample(scale_factor=2),
nn.Conv2d(in_channels, out_channels, 3, padding=1),
nn.BatchNorm2d(out_channels),
nn.ReLU(inplace=True)
)
def outconv(in_channels, out_channels):
return nn.Conv2d(in_channels, out_channels, 1)
这个示例实现了UNet网络,一种典型的编码器-解码器网络结构。它通过多次下采样获得抽象语义特征,再通过上采样逐步恢复空间信息,输出精细的像素级分割结果。
该网络包含卷积层、批量归一化层、ReLU激活层、池化层、上采样层等模块。通过这种 Encoder-Decoder 的设计,可以有效利用高层和低层的特征,使得分割既准确又符合物体形状。
UNet已经在许多医学图像分割数据集上达到最优性能,是目前效果最好的通用图像分割网络之一。
我们有一幅包含狗和猫的图像,要实现二者的分割。
1. 阈值法
选择一个阈值,将阈值以上像素区分为猫,以下像素区分为狗。效果较差,无法准确分割。
2. 边缘检测
检测图像中的边缘,但是狗和猫边界不清晰,效果一般。
3. 区域生长法
选择狗和猫各一个种子点进行生长,但是生长过程中容易出现误连,效果受种子点影响较大。
4. 标注传播
给一小部分像素标注狗或猫的标签,通过传播分割整幅图像。需要大量训练数据,效果较好。
5. 深度学习
使用训练好的分割网络直接对图像进行分割,效果最优,可以准确分割狗和猫。
深度学习是图像分割最为主流和高效的方法。通过大数据训练深度网络,可以实现图像中的精细分割与语义理解。这是实现更高级视觉任务的基础。