如何使用CV2检测并提取印刷文本中的非连续字符?
解决非连续字符的提取问题
针对非连续字符(如i、j的上点,:、;的上点)无法被外接轮廓完整提取的问题,用OpenCV可以通过以下两种思路处理,不需要依赖OCR引擎:
方法一:连通域分析+距离阈值合并
这种方法先找出所有独立的连通区域(包括字符主体和附属的点),再通过距离判断将属于同一字符的区域合并,最终得到完整的字符 bounding box。
实现步骤:
- 获取所有连通域的统计信息,包括每个区域的坐标、尺寸、中心位置
- 筛选出可能是"主体"的连通域(通常面积更大、高度更高)和"附属点"(面积小、高度低)
- 计算附属点与每个主体的距离,当距离小于设定阈值时,将附属点的范围合并到主体的 bounding box 中
- 去重后,裁剪每个合并后的区域并保存
代码示例:
import cv2 import numpy as np # 假设已得到二值化图像(黑底白字,若为白底黑字需先反转) gray_image = cv2.imread("text_img.png", cv2.IMREAD_GRAYSCALE) binary_image = cv2.threshold(gray_image, 127, 255, cv2.THRESH_BINARY_INV)[1] # 获取连通域及统计信息 num_labels, labels, stats, centroids = cv2.connectedComponentsWithStats(binary_image, connectivity=8) # 筛选主体与附属点:主体高度大于平均高度的一半,附属点面积远小于主体 avg_height = np.mean(stats[1:, cv2.CC_STAT_HEIGHT]) # 跳过背景(label=0) main_components = [] dot_components = [] for i in range(1, num_labels): x, y, w, h, area = stats[i] if h > avg_height * 0.5: main_components.append((x, y, w, h, centroids[i])) else: dot_components.append((x, y, w, h, centroids[i])) # 设定距离阈值:取主体平均高度的1/3,可根据字体调整 distance_threshold = avg_height * 0.33 # 合并附属点到对应主体 merged_boxes = [] for main_x, main_y, main_w, main_h, main_centroid in main_components: min_x, min_y = main_x, main_y max_x, max_y = main_x + main_w, main_y + main_h for dot_x, dot_y, dot_w, dot_h, dot_centroid in dot_components: # 计算中心欧氏距离 dist = np.linalg.norm(np.array(main_centroid) - np.array(dot_centroid)) if dist < distance_threshold: min_x = min(min_x, dot_x) min_y = min(min_y, dot_y) max_x = max(max_x, dot_x + dot_w) max_y = max(max_y, dot_y + dot_h) merged_boxes.append((min_x, min_y, max_x - min_x, max_y - min_y)) # 去重避免重复合并 merged_boxes = list(set(merged_boxes)) # 裁剪并保存每个字符 for idx, (x, y, w, h) in enumerate(merged_boxes): # 确保坐标在图像范围内 x = max(0, x) y = max(0, y) w = min(binary_image.shape[1] - x, w) h = min(binary_image.shape[0] - y, h) char_img = binary_image[y:y+h, x:x+w] # 反转回白底黑字(可选) char_img = cv2.threshold(char_img, 127, 255, cv2.THRESH_BINARY_INV)[1] cv2.imwrite(f"char_{idx}.png", char_img)
方法二:局部膨胀+轮廓提取
如果文本排列整齐,可对每个字符的垂直区域做小范围横向膨胀,将字符主体和上方的点连接成一个连通区域,再提取轮廓。
实现步骤:
- 计算垂直投影,确定每个字符的列范围
- 对每个字符列区域做横向膨胀,让点和主体连通
- 对膨胀后的区域提取轮廓,得到完整字符的 bounding box
代码示例:
import cv2 import numpy as np gray_image = cv2.imread("text_img.png", cv2.IMREAD_GRAYSCALE) binary_image = cv2.threshold(gray_image, 127, 255, cv2.THRESH_BINARY_INV)[1] # 计算垂直投影:统计每列黑像素数量 vertical_proj = np.sum(binary_image, axis=0) # 找到有字符的列 char_cols = np.where(vertical_proj > 0)[0] if len(char_cols) == 0: exit() # 分割字符列:按连续非零投影列分组 char_regions = [] start_col = char_cols[0] for i in range(1, len(char_cols)): if char_cols[i] != char_cols[i-1] + 1: char_regions.append((start_col, char_cols[i-1])) start_col = char_cols[i] char_regions.append((start_col, char_cols[-1])) # 定义横向膨胀核:让点与主体连通(可根据字体调整尺寸) kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 1)) # 处理每个字符区域 for idx, (start_x, end_x) in enumerate(char_regions): # 裁剪字符列区域 char_roi = binary_image[:, start_x:end_x+1] # 横向膨胀 dilated_roi = cv2.dilate(char_roi, kernel, iterations=1) # 提取最大轮廓(完整字符) contours, _ = cv2.findContours(dilated_roi, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) if contours: max_contour = max(contours, key=cv2.contourArea) x, y, w, h = cv2.boundingRect(max_contour) final_char = char_roi[y:y+h, x:x+w] final_char = cv2.threshold(final_char, 127, 255, cv2.THRESH_BINARY_INV)[1] cv2.imwrite(f"char_{idx}.png", final_char)
注意事项:
- 两种方法都需要根据实际字体大小调整参数(如距离阈值、膨胀核尺寸),可通过统计图像中字符的平均高度/宽度自动适配
- 若图像有噪声,可先做
cv2.medianBlur或cv2.GaussianBlur去噪,避免误识别噪声点为字符附属部分
内容的提问来源于stack exchange,提问作者Terence Eden
相关产品推荐
相关产品推荐

