目的:只扩大二值图像内部的黑色孔洞,而不改变白色物体的外部轮廓。
1 示例:
import cv2
import numpy as np
import matplotlib.pyplot as plt
# --- 解决方案:处理中文乱码 ---
plt.rcParams['font.sans-serif'] = ['SimHei'] # 指定支持中文的字体
plt.rcParams['axes.unicode_minus'] = False # 修正负号显示问题
# ------------------------------------
# --- 1. 创建一个用于演示的、包含多个孔洞的二值图像 ---
image = np.zeros((300, 300), dtype=np.uint8)
cv2.rectangle(image, (30, 30), (270, 270), 255, -1) # 白色主体区域
# 在中间添加多个不同形状和大小的孔洞
cv2.circle(image, (150, 100), 30, 0, -1) # 圆形孔洞
cv2.rectangle(image, (80, 180), (140, 220), 0, -1) # 矩形孔洞
cv2.ellipse(image, (220, 190), (40, 20), 0, 0, 360, 0, -1) # 椭圆形孔洞
# --------------------------------------------------
# --- 2. 核心处理逻辑 (无需改动) ---
def enlarge_holes(binary_image):
"""
将二值图像的内部黑色孔洞扩大一圈,同时保持外部轮廓不变。
此函数可以处理单个或多个孔洞。
"""
result = binary_image.copy()
# --- 步骤 1: 提取内部孔洞 ---
h, w = binary_image.shape
flood_mask = np.zeros((h + 2, w + 2), np.uint8)
im_floodfill = result.copy()
cv2.floodFill(im_floodfill, flood_mask, (0, 0), 255)
holes_mask = cv2.bitwise_not(im_floodfill)
# --- 步骤 2: 让孔洞“变大一圈” ---
kernel = np.ones((3, 3), np.uint8)
dilated_holes_mask = cv2.dilate(holes_mask, kernel, iterations=1)
# --- 步骤 3: 从原图中“减去”变大后的孔洞 ---
inverted_dilated_mask = cv2.bitwise_not(dilated_holes_mask)
final_result = cv2.bitwise_and(result, inverted_dilated_mask)
return final_result, holes_mask, dilated_holes_mask
# --- 3. 执行处理并显示结果 ---
final_image, original_holes, dilated_holes = enlarge_holes(image)
# 使用 matplotlib 显示结果,方便对比
plt.figure(figsize=(16, 4))
plt.subplot(1, 4, 1)
plt.title("原始图像 (含多个孔洞)")
plt.imshow(image, cmap='gray')
plt.subplot(1, 4, 2)
plt.title("提取出的所有孔洞")
plt.imshow(original_holes, cmap='gray')
plt.subplot(1, 4, 3)
plt.title("膨胀后的所有孔洞")
plt.imshow(dilated_holes, cmap='gray')
plt.subplot(1, 4, 4)
plt.title("最终结果")
plt.imshow(final_image, cmap='gray')
plt.tight_layout()
plt.show()
2 超大图:内部大量空洞处理
利用 cv2.findContours
及其返回的**层级(Hierarchy)**信息,快速筛选出所有父轮廓不为空的轮廓(即内部孔洞),然后对这些孔洞的蒙版进行膨胀,最后从原图中减去膨胀后的孔洞区域。
import cv2
import numpy as np
import time
import os
import random
def enlarge_holes_contours(binary_image):
"""
【高效】使用轮廓检测法,将图像内部的黑色孔洞扩大一圈。
"""
# 寻找所有轮廓及其层级关系
# RETR_TREE能构建完整的层级树,非常适合查找内部轮廓
contours, hierarchy = cv2.findContours(binary_image, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
# 创建一个与原图等大的空白蒙版,用于绘制所有识别出的孔洞
height, width = binary_image.shape
holes_mask = np.zeros((height, width), dtype=np.uint8)
# 检查hierarchy是否存在,以避免在没有轮廓时出错
if hierarchy is not None:
# 遍历所有轮廓,根据层级信息筛选出孔洞
# hierarchy[0][i][3] 代表第 i 个轮廓的父轮廓索引。
# 如果父轮廓存在(即索引不为-1),说明它是一个内部孔洞。
for i, contour in enumerate(contours):
if hierarchy[0][i][3] != -1:
# 在蒙版上将这个孔洞轮廓填充为白色
cv2.drawContours(holes_mask, [contour], 0, 255, -1)
# 定义一个3x3的结构元素用于膨胀(扩大一圈)
kernel = np.ones((3, 3), np.uint8)
# 对孔洞蒙版进行膨胀操作
dilated_holes_mask = cv2.dilate(holes_mask, kernel, iterations=1)
# 使用位运算,从原图中“减去”膨胀后的孔洞区域
# bitwise_not(dilated_holes_mask) 会得到一个孔洞区域为黑色的蒙版
final_result = cv2.bitwise_and(binary_image, cv2.bitwise_not(dilated_holes_mask))
return final_result
# --- 主程序 ---
if __name__ == "__main__":
image_size = 8000
num_holes_side = 15 # 每行/每列的孔洞数,总共 7x7=49 个
hole_radius = 30 # 孔洞半径
# --- 1. 创建5000x5000的测试图像 ---
print(f"正在创建 {image_size}x{image_size} 的测试图像,包含 {num_holes_side**2} 个半径为 {hole_radius} 的孔洞...")
large_image = np.zeros((image_size, image_size), dtype=np.uint8)
# 绘制一个大的白色主体区域,留出边框
cv2.rectangle(large_image, (200, 200), (image_size - 200, image_size - 200), 255, -1)
# 在主体区域内均匀生成几十个孔洞
step = (image_size - 1000) // num_holes_side
for i in range(num_holes_side):
for j in range(num_holes_side):
center_x = 500 + i * step + random.randint(-step//4, step//4)
center_y = 500 + j * step + random.randint(-step//4, step//4)
cv2.circle(large_image, (center_x, center_y), hole_radius, 0, -1)
print("测试图像创建完毕。")
cv2.imwrite("original_large_uniform_holes.png", large_image)
print("原始大图已保存为 'original_large_uniform_holes.png'")
# --- 2. 使用高效的轮廓检测法进行处理和计时 ---
print("\n--- 开始使用 'findContours' 方法处理大图 ---")
start_time = time.time()
result_image = enlarge_holes_contours(large_image)
end_time = time.time()
elapsed_time = end_time - start_time
print(f"✅ 'findContours' 方法处理5000x5000图像耗时: {elapsed_time:.4f} 秒")
cv2.imwrite("processed_large_uniform_holes.png", result_image)
print("处理后的大图已保存为 'processed_large_uniform_holes.png'")
# --- 3. 生成一个局部区域的视觉对比图 ---
print("\n--- 正在生成局部区域的对比效果图 ---")
# 截取图像中包含几个孔洞的区域以便观察
crop_x, crop_y, crop_w, crop_h = 450, 450, 800, 800
original_crop = large_image[crop_y:crop_y+crop_h, crop_x:crop_x+crop_w]
processed_crop = result_image[crop_y:crop_y+crop_h, crop_x:crop_x+crop_w]
# 将灰度图转为BGR以添加彩色文字
original_crop_bgr = cv2.cvtColor(original_crop, cv2.COLOR_GRAY2BGR)
processed_crop_bgr = cv2.cvtColor(processed_crop, cv2.COLOR_GRAY2BGR)
# 添加标签
cv2.putText(original_crop_bgr, "原始图像 (局部)", (20, 40), cv2.FONT_HERSHEY_SIMPLEX, 1.2, (0, 255, 0), 2)
cv2.putText(processed_crop_bgr, "处理结果 (局部)", (20, 40), cv2.FONT_HERSHEY_SIMPLEX, 1.2, (0, 255, 0), 2)
# 水平拼接图像
comparison_image = np.hstack([original_crop_bgr, processed_crop_bgr])
cv2.imwrite("comparison_crop_uniform_holes.png", comparison_image)
print("局部对比图已保存为 'comparison_crop_uniform_holes.png'")
1. 寻找所有轮廓及其层级
contours, hierarchy = cv2.findContours(binary_image, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
cv2.findContours
: 这是OpenCV的轮廓查找函数。binary_image
: 输入的必须是二值图像(只有黑白两色)。cv2.RETR_TREE
: 这是最关键的参数。它告诉函数去检测所有轮廓并重建一个完整的层级树。这棵树描述了轮廓间的嵌套关系。hierarchy
: 这是一个数组,存储了所有轮廓的层级信息。它的结构是[下一个轮廓, 上一个轮廓, 第一个子轮廓, 父轮廓]
。
2. 筛选出内部孔洞
# hierarchy[0][i][3] 代表第 i 个轮廓的父轮廓索引。
# 如果父轮廓存在(即索引不为-1),说明它是一个内部孔洞。
for i, contour in enumerate(contours):
if hierarchy[0][i][3] != -1:
# 在蒙版上将这个孔洞轮廓填充为白色
cv2.drawContours(holes_mask, [contour], 0, 255, -1)
代码遍历找到的每一个轮廓。
对于第
i
个轮廓,它会检查hierarchy[0][i][3]
的值。这个值代表了该轮廓的父轮廓的索引。如果一个轮廓有父轮廓(即这个值不是
-1
),就意味着它被另一个轮廓所包围,因此它是一个内部孔洞。一旦确定是孔洞,就使用
cv2.drawContours
将它填充为白色,绘制到一个新建的空白**蒙版 (holes_mask
)**上。
执行完这一步,holes_mask
就成了一张只包含所有孔洞形状的图像。
3. 扩大孔洞
kernel = np.ones((3, 3), np.uint8)
dilated_holes_mask = cv2.dilate(holes_mask, kernel, iterations=1)
这一步是对上一步生成的孔洞蒙版进行膨胀 (Dilation) 操作。
膨胀会使图像中的白色区域(在这里就是孔洞)向外扩张。使用一个3x3的核心并迭代1次,正好能让每个孔洞的边界向外扩大一圈(1个像素)。
4. 应用回原图
final_result = cv2.bitwise_and(binary_image, cv2.bitwise_not(dilated_holes_mask))
这是最后一步,将扩大后的孔洞应用回原始图像。
cv2.bitwise_not(dilated_holes_mask)
: 首先,将膨胀后的孔洞蒙版进行反色。现在,这张蒙版上,扩大后的孔洞区域是黑色,其他所有地方都是白色。cv2.bitwise_and(...)
: 然后,将原始图像与这张反色后的蒙版进行**“位与”**操作。这个操作的效果是:在蒙版为白色的区域(非孔洞区域),保留原始图像的像素。
在蒙版为黑色的区域(扩大后的孔洞区域),结果将变为黑色。