在计算机视觉和图像处理领域,图像分割是一项基础而关键的技术。所谓分割掩膜(Segmentation Mask),就是用一个与原始图像尺寸相同的二维矩阵,其中每个像素点的值代表该像素属于哪个物体或类别。这种技术广泛应用于医学影像分析、自动驾驶、工业质检等领域。
举个例子,在医学影像中,医生可能需要标记肿瘤区域;在自动驾驶系统中,需要区分道路、行人、车辆等不同对象。传统的手动标注方式效率低下,而借助Python和相关工具库,我们可以实现半自动化甚至全自动化的掩膜绘制流程。
本文将详细介绍如何使用Python生态中的主流工具(如OpenCV、scikit-image、matplotlib等)完成从基础到进阶的掩膜绘制操作。无论你是需要为机器学习项目准备训练数据,还是进行学术研究中的图像分析,这些方法都能提供实用参考。
绘制分割掩膜主要依赖以下几个核心库:
安装这些库只需一行命令:
bash复制pip install opencv-python numpy matplotlib scikit-image pillow
理解掩膜的数据结构至关重要。本质上,掩膜是一个与图像同尺寸的NumPy数组:
python复制import cv2
import numpy as np
# 创建一个全黑的掩膜(所有像素为0)
height, width = 480, 640
mask = np.zeros((height, width), dtype=np.uint8)
最常见的需求是手动标注图像中的特定区域。OpenCV的cv2.fillPoly()函数非常适合这种场景:
python复制def draw_polygon_mask(image_path, output_path):
# 读取图像
img = cv2.imread(image_path)
height, width = img.shape[:2]
# 创建空白掩膜
mask = np.zeros((height, width), dtype=np.uint8)
# 定义多边形顶点(示例为矩形)
pts = np.array([[100,50], [400,50], [400,300], [100,300]])
# 填充多边形区域(值为1)
cv2.fillPoly(mask, [pts], color=1)
# 保存掩膜
cv2.imwrite(output_path, mask*255) # 乘以255使掩膜可见
提示:在实际应用中,通常会结合交互式界面让用户点击选择多边形顶点。可以考虑使用matplotlib的
ginput()函数实现交互式顶点采集。
对于颜色特征明显的对象,阈值法是最简单的自动分割方法:
python复制def threshold_mask(image_path, output_path):
img = cv2.imread(image_path)
hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
# 定义颜色范围(示例为绿色区域)
lower_green = np.array([35, 50, 50])
upper_green = np.array([85, 255, 255])
# 生成掩膜
mask = cv2.inRange(hsv, lower_green, upper_green)
mask = (mask > 0).astype(np.uint8) # 转换为0/1格式
cv2.imwrite(output_path, mask*255)
当对象边界复杂但前景/背景可区分时,GrabCut算法能提供很好的平衡:
python复制def grabcut_mask(image_path, output_path, rect):
"""
rect: (x,y,w,h)格式的矩形框,大致框住前景对象
"""
img = cv2.imread(image_path)
mask = np.zeros(img.shape[:2], np.uint8)
# 初始化GrabCut
bgdModel = np.zeros((1,65), np.float64)
fgdModel = np.zeros((1,65), np.float64)
cv2.grabCut(img, mask, rect, bgdModel, fgdModel, 5, cv2.GC_INIT_WITH_RECT)
# 处理结果掩膜
mask = np.where((mask==2)|(mask==0), 0, 1).astype(np.uint8)
cv2.imwrite(output_path, mask*255)
对于最先进的分割效果,可以集成预训练模型如Segment Anything Model (SAM):
python复制from segment_anything import SamPredictor, sam_model_registry
def sam_mask(image_path, output_path, point_coords, point_labels):
"""
point_coords: 用户点击的坐标点 [[x1,y1],[x2,y2],...]
point_labels: 对应点的标签 1=前景, 0=背景
"""
sam = sam_model_registry["vit_b"](checkpoint="sam_vit_b_01ec64.pth")
predictor = SamPredictor(sam)
image = cv2.imread(image_path)
image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
predictor.set_image(image)
masks, _, _ = predictor.predict(
point_coords=np.array(point_coords),
point_labels=np.array(point_labels),
multimask_output=False,
)
cv2.imwrite(output_path, masks[0].astype(np.uint8)*255)
生成的掩膜往往需要后处理来消除噪声和优化边界:
python复制def refine_mask(mask_path, output_path):
mask = cv2.imread(mask_path, cv2.IMREAD_GRAYSCALE)
# 开运算去除小噪声
kernel = np.ones((5,5), np.uint8)
mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel)
# 闭运算填充小孔洞
mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel)
# 边界平滑
mask = cv2.GaussianBlur(mask, (5,5), 0)
_, mask = cv2.threshold(mask, 127, 255, cv2.THRESH_BINARY)
cv2.imwrite(output_path, mask)
清晰的展示效果对于验证掩膜质量至关重要:
python复制def visualize_mask(image_path, mask_path, output_path):
img = cv2.imread(image_path)
mask = cv2.imread(mask_path, cv2.IMREAD_GRAYSCALE)
# 创建彩色掩膜(红色)
color_mask = np.zeros_like(img)
color_mask[mask > 0] = [0, 0, 255] # BGR格式的红色
# 叠加显示(50%透明度)
blended = cv2.addWeighted(img, 0.7, color_mask, 0.3, 0)
cv2.imwrite(output_path, blended)
当背景杂乱时,可以尝试以下策略:
python复制def multi_channel_threshold(img):
# 转换为LAB颜色空间
lab = cv2.cvtColor(img, cv2.COLOR_BGR2LAB)
# 分别处理各通道
l_channel = lab[:,:,0]
a_channel = lab[:,:,1]
b_channel = lab[:,:,2]
# 组合多个通道的条件
mask = ((l_channel > 50) & (l_channel < 200)
& (a_channel > 120) & (b_channel < 150)).astype(np.uint8)
return mask
对于需要标注大量图像的情况,建议:
python复制def auto_preannotate(image_dir, output_dir):
for img_file in os.listdir(image_dir):
if not img_file.lower().endswith(('.png', '.jpg', '.jpeg')):
continue
img_path = os.path.join(image_dir, img_file)
img = cv2.imread(img_path)
# 使用简单的自动方法生成初始掩膜
initial_mask = threshold_mask(img) # 使用前面定义的阈值方法
# 保存供人工修正
output_path = os.path.join(output_dir, f"pre_{img_file}")
cv2.imwrite(output_path, initial_mask*255)
处理高分辨率图像时,这些方法可以提升性能:
python复制from multiprocessing import Pool
def process_image(args):
img_path, output_path = args
# 处理单个图像的掩膜生成
img = cv2.imread(img_path)
mask = generate_mask(img) # 假设已定义generate_mask函数
cv2.imwrite(output_path, mask)
def batch_process(image_paths, output_paths, workers=4):
with Pool(workers) as p:
p.map(process_image, zip(image_paths, output_paths))
考虑以下因素选择存储格式:
| 格式 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| PNG | 无损压缩,支持透明度 | 文件较大 | 精确标注 |
| JPEG | 文件小 | 有损压缩 | 快速预览 |
| NPZ | 保留原始数组数据 | 需要Python读取 | 中间处理结果 |
python复制def save_mask_optimized(mask, path):
if path.endswith('.png'):
# PNG格式使用最高压缩比
cv2.imwrite(path, mask, [cv2.IMWRITE_PNG_COMPRESSION, 9])
elif path.endswith('.npy'):
np.save(path, mask)
else:
cv2.imwrite(path, mask)
症状:生成的掩膜与原始图像位置偏移
可能原因:
解决方案:
python复制def check_alignment(img_path, mask_path):
img = cv2.imread(img_path)
mask = cv2.imread(mask_path, cv2.IMREAD_GRAYSCALE)
assert img.shape[:2] == mask.shape[:2], "尺寸不匹配"
# 可视化检查
plt.subplot(121); plt.imshow(img)
plt.subplot(122); plt.imshow(mask)
plt.show()
症状:掩膜边界出现明显锯齿状
可能原因:
改进方案:
python复制def smooth_mask_edges(mask):
# 先进行高斯模糊
blurred = cv2.GaussianBlur(mask.astype(np.float32), (5,5), 0)
# 自适应阈值
smoothed = np.zeros_like(mask)
smoothed[blurred > 0.5] = 1
# 小区域去除
contours, _ = cv2.findContours(smoothed, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
for cnt in contours:
if cv2.contourArea(cnt) < 100:
cv2.drawContours(smoothed, [cnt], -1, 0, -1)
return smoothed
python复制def end_to_end_pipeline(image_path, output_path):
# 1. 读取并预处理
img = cv2.imread(image_path)
img = cv2.medianBlur(img, 3)
# 2. 自动生成初始掩膜
initial_mask = grabcut_mask(img, rect=(50,50,400,400))
# 3. 此处应有交互式编辑(示例省略)
refined_mask = initial_mask # 假设已经交互修正
# 4. 后处理
final_mask = refine_mask(refined_mask)
# 5. 保存结果
cv2.imwrite(output_path, final_mask)
对于需要频繁标注的场景,建议开发专用工具:
python复制# 简化的标注工具框架示例
import matplotlib.pyplot as plt
from matplotlib.widgets import PolygonSelector
class MaskAnnotator:
def __init__(self, image_path):
self.fig, self.ax = plt.subplots()
self.img = plt.imread(image_path)
self.ax.imshow(self.img)
self.selector = PolygonSelector(self.ax, self.onselect)
self.mask = np.zeros(self.img.shape[:2], dtype=np.uint8)
def onselect(self, verts):
# 将多边形转换为掩膜
from matplotlib.path import Path
path = Path(verts)
x, y = np.meshgrid(np.arange(self.img.shape[1]),
np.arange(self.img.shape[0]))
points = np.vstack((x.flatten(), y.flatten())).T
mask = path.contains_points(points)
self.mask = mask.reshape(self.img.shape[:2]).astype(np.uint8)
def show_mask(self):
self.ax.clear()
self.ax.imshow(self.img)
self.ax.imshow(self.mask, alpha=0.3)
plt.draw()
将静态图像技术扩展到视频序列:
python复制def video_segmentation(video_path, output_dir):
cap = cv2.VideoCapture(video_path)
ret, prev_frame = cap.read()
prev_mask = generate_mask(prev_frame) # 首帧需要手动或自动生成
frame_count = 0
while True:
ret, frame = cap.read()
if not ret:
break
# 使用光流估计运动
flow = cv2.calcOpticalFlowFarneback(
cv2.cvtColor(prev_frame, cv2.COLOR_BGR2GRAY),
cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY),
None, 0.5, 3, 15, 3, 5, 1.2, 0)
# 根据光流变换掩膜
h, w = flow.shape[:2]
flow_map = -flow.copy()
flow_map[:,:,0] += np.arange(w)
flow_map[:,:,1] += np.arange(h)[:,np.newaxis]
new_mask = cv2.remap(prev_mask, flow_map, None, cv2.INTER_LINEAR)
# 保存结果
cv2.imwrite(f"{output_dir}/frame_{frame_count:04d}.png", new_mask*255)
prev_frame = frame.copy()
prev_mask = new_mask.copy()
frame_count += 1
对于CT/MRI等医学影像,需要处理3D体数据:
python复制def process_3d_volume(dicom_dir, output_path):
import pydicom
from skimage.measure import marching_cubes
# 读取DICOM序列
slices = [pydicom.dcmread(f) for f in sorted(os.listdir(dicom_dir))]
volume = np.stack([s.pixel_array for s in slices])
# 生成3D掩膜(示例为简单阈值)
mask_3d = (volume > threshold_value).astype(np.uint8)
# 提取等值面
verts, faces, _, _ = marching_cubes(mask_3d, level=0.5)
# 保存为3D模型
save_as_obj(verts, faces, output_path) # 需要实现OBJ文件保存函数
在实际项目中,我发现掩膜质量对后续分析影响巨大。一个实用的技巧是:在处理前先进行直方图分析,了解图像的灰度/颜色分布特征,这能帮助选择更合适的阈值或分割算法。另外,对于重要项目,建议至少由两人独立标注关键样本,然后计算标注一致性,这能有效评估掩膜质量。