Spaces:
Sleeping
Sleeping
| import cv2 | |
| import numpy as np | |
| import os | |
| import re | |
| import glob | |
| def clean_filename(text): | |
| """清理文件名""" | |
| text = text.lower().strip() | |
| text = re.sub(r'[\s\W]+', '_', text) | |
| return text.strip('_') | |
| def sort_contours(cnts, method="left-to-right"): | |
| """ | |
| 对轮廓进行排序。 | |
| 对于 Grid 布局,我们需要 'top-to-bottom' 然后 'left-to-right' 的混合排序。 | |
| """ | |
| if not cnts: | |
| return [], [] | |
| # 获取每个轮廓的 Bounding Box | |
| boundingBoxes = [cv2.boundingRect(c) for c in cnts] | |
| # 将轮廓和bbox打包 | |
| cnts_boxes = list(zip(cnts, boundingBoxes)) | |
| # 1. 按照 Y 坐标(从上到下)进行初步排序 | |
| # key: y | |
| cnts_boxes.sort(key=lambda b: b[1][1]) | |
| # 2. 分行处理 | |
| # 由于手工画线或扫描误差,同一行的y坐标可能不完全相同。 | |
| # 我们需要设定一个阈值,认为y坐标相近的是“同一行”。 | |
| rows = [] | |
| current_row = [] | |
| if cnts_boxes: | |
| # 以第一个轮廓的高度作为参考阈值 | |
| ref_h = cnts_boxes[0][1][3] | |
| tolerance = ref_h * 0.5 # 容差设为高度的一半 | |
| last_y = cnts_boxes[0][1][1] | |
| for c, box in cnts_boxes: | |
| y = box[1] | |
| if y <= last_y + tolerance: | |
| current_row.append((c, box)) | |
| else: | |
| # 新的一行 | |
| rows.append(current_row) | |
| current_row = [(c, box)] | |
| last_y = y | |
| # 添加最后一行 | |
| if current_row: | |
| rows.append(current_row) | |
| # 3. 对每一行内部,按照 X 坐标(从左到右)排序 | |
| final_sorted = [] | |
| row_counts = [] | |
| for i, row in enumerate(rows): | |
| # key: x | |
| row.sort(key=lambda b: b[1][0]) | |
| row_counts.append(len(row)) | |
| for item in row: | |
| final_sorted.append(item[1]) # 只返回 bbox (x, y, w, h) | |
| return final_sorted, row_counts | |
| def uniform_grid_split(img, expected_cols=6, expected_rows=4, margin_percent=0.02): | |
| """ | |
| 均匀分割方法:直接按照预期的行列数均匀分割图像 | |
| 适用于网格线不连续或没有明显网格线的情况 | |
| Args: | |
| img: 输入图像 | |
| expected_cols: 期望的列数 | |
| expected_rows: 期望的行数 | |
| margin_percent: 边缘裁剪比例(去除可能的边框) | |
| Returns: | |
| 排序好的 (x, y, w, h) 列表 | |
| """ | |
| h_img, w_img = img.shape[:2] | |
| # 去除边缘 | |
| margin_x = int(w_img * margin_percent) | |
| margin_y = int(h_img * margin_percent) | |
| effective_width = w_img - 2 * margin_x | |
| effective_height = h_img - 2 * margin_y | |
| # 计算每个单元格的尺寸 | |
| cell_width = effective_width // expected_cols | |
| cell_height = effective_height // expected_rows | |
| boxes = [] | |
| for row in range(expected_rows): | |
| for col in range(expected_cols): | |
| x = margin_x + col * cell_width | |
| y = margin_y + row * cell_height | |
| boxes.append((x, y, cell_width, cell_height)) | |
| return boxes | |
| def detect_grid_cells_with_lines(img, expected_cols=6, expected_rows=4): | |
| """ | |
| 通过形态学操作检测网格线,并提取每个格子的坐标 | |
| """ | |
| gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) | |
| # 二值化 (反转:背景黑,内容/线白) | |
| # 使用自适应阈值来应对光照或颜色不均 | |
| thresh = cv2.adaptiveThreshold(gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, | |
| cv2.THRESH_BINARY_INV, 11, 2) | |
| # 定义结构元素 (Kernel) - 增大kernel以更好地检测断裂的线 | |
| h_img, w_img = img.shape[:2] | |
| # 水平线 Kernel: 宽度长,高度为1 | |
| horizontal_kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (w_img // 15, 1)) | |
| # 垂直线 Kernel: 宽度为1,高度长 | |
| vertical_kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (1, h_img // 15)) | |
| # 1. 提取水平线 | |
| detect_horizontal = cv2.morphologyEx(thresh, cv2.MORPH_OPEN, horizontal_kernel, iterations=2) | |
| # 2. 提取垂直线 | |
| detect_vertical = cv2.morphologyEx(thresh, cv2.MORPH_OPEN, vertical_kernel, iterations=2) | |
| # 3. 合并网格线 | |
| grid_mask = cv2.addWeighted(detect_horizontal, 0.5, detect_vertical, 0.5, 0) | |
| _, grid_mask = cv2.threshold(grid_mask, 0, 255, cv2.THRESH_BINARY) | |
| # 更强的膨胀操作,连接断裂的网格线 | |
| kernel_dilate = cv2.getStructuringElement(cv2.MORPH_RECT, (5, 5)) | |
| grid_mask = cv2.dilate(grid_mask, kernel_dilate, iterations=3) | |
| # 闭运算,进一步连接断裂 | |
| kernel_close = cv2.getStructuringElement(cv2.MORPH_RECT, (7, 7)) | |
| grid_mask = cv2.morphologyEx(grid_mask, cv2.MORPH_CLOSE, kernel_close, iterations=2) | |
| # 4. 寻找所有的“洞”(即单元格) | |
| # 我们通过查找 grid_mask 的轮廓,通常很难直接找到内部的矩形。 | |
| # 更好的方法是:找出网格线轮廓,画在全黑背景上,然后寻找连通组件,或者反转图片找白色方块。 | |
| # 这里我们采用“反转 mask”法:网格线是黑,格子是白 | |
| contours_mask = cv2.bitwise_not(grid_mask) | |
| contours, _ = cv2.findContours(contours_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) | |
| # 计算期望的单元格面积 | |
| expected_cell_area = (w_img * h_img) / (expected_cols * expected_rows) | |
| # 过滤微小的噪点轮廓,同时也过滤过大的轮廓 | |
| # 放宽过滤条件以捕获更多单元格 | |
| min_area = expected_cell_area / 20 # 单元格面积的 1/20 (之前是1/10) | |
| max_area = expected_cell_area * 3 # 单元格面积的3倍 (之前是2倍) | |
| # 调试信息 | |
| print(f" [调试] 图像尺寸: {w_img}x{h_img}, 检测到轮廓: {len(contours)}个") | |
| print(f" [调试] 期望单元格面积: {expected_cell_area:.0f}, 过滤范围: {min_area:.0f}-{max_area:.0f}") | |
| # 统计被过滤掉的轮廓 | |
| filtered_out = [] | |
| valid_contours = [] | |
| for c in contours: | |
| area = cv2.contourArea(c) | |
| if min_area < area < max_area: | |
| valid_contours.append(c) | |
| else: | |
| x, y, w, h = cv2.boundingRect(c) | |
| filtered_out.append((area, x, y, w, h)) | |
| if filtered_out: | |
| print(f" [调试] 被过滤掉 {len(filtered_out)} 个轮廓:") | |
| for area, x, y, w, h in sorted(filtered_out, key=lambda t: t[0], reverse=True)[:5]: | |
| print(f" - 面积={area:.0f}, 位置=({x},{y}), 尺寸={w}x{h}") | |
| # 排序:确保顺序是 左->右,上->下 | |
| sorted_boxes, row_counts = sort_contours(valid_contours) | |
| return sorted_boxes, row_counts | |
| def detect_grid_cells(img, expected_cols=6, expected_rows=4): | |
| """ | |
| 鲁棒的网格检测方法:首先尝试检测网格线,如果失败则使用均匀分割 | |
| """ | |
| expected_count = expected_cols * expected_rows | |
| # 方法1: 尝试检测网格线 | |
| sorted_boxes, row_counts = detect_grid_cells_with_lines(img, expected_cols, expected_rows) | |
| # 严格检查:必须恰好检测到期望数量的单元格 | |
| if len(sorted_boxes) != expected_count: | |
| print(f" 网格线检测不理想(检测到 {len(sorted_boxes)} 个单元格,期望 {expected_count} 个)") | |
| if row_counts: | |
| row_info = ", ".join([f"第{i+1}行: {count}个" for i, count in enumerate(row_counts)]) | |
| print(f" 检测到的行分布: {row_info}") | |
| print(f" 切换到均匀分割模式...") | |
| sorted_boxes = uniform_grid_split(img, expected_cols, expected_rows) | |
| # 均匀分割时,打印每行的单元格数 | |
| print(f" 均匀分割结果:每行 {expected_cols} 个单元格,共 {expected_rows} 行") | |
| else: | |
| print(f" ✓ 成功检测到 {len(sorted_boxes)} 个网格单元格(符合预期)") | |
| # 打印每行的单元格数量 | |
| if row_counts: | |
| row_info = ", ".join([f"第{i+1}行: {count}个" for i, count in enumerate(row_counts)]) | |
| print(f" 行分布: {row_info}") | |
| return sorted_boxes | |
| def is_likely_text_region(img_region, thresh_region): | |
| """ | |
| 判断一个区域是否可能是文字 | |
| 文字的特征: | |
| 1. 主要是黑色或深色 | |
| 2. 高度较小 | |
| 3. 像素密度适中(不是纯色块) | |
| """ | |
| if img_region.shape[0] == 0 or img_region.shape[1] == 0: | |
| return False | |
| # 转换为灰度(如果不是) | |
| if len(img_region.shape) == 3: | |
| gray_region = cv2.cvtColor(img_region, cv2.COLOR_BGR2GRAY) | |
| else: | |
| gray_region = img_region | |
| # 检查1:高度不能太大(文字通常较矮) | |
| height_ratio = img_region.shape[0] / img_region.shape[1] if img_region.shape[1] > 0 else 1 | |
| if height_ratio > 0.3: # 如果高度超过宽度的30%,可能不是单行文字 | |
| return False | |
| # 检查2:颜色是否偏暗(文字通常是黑色或深色) | |
| mean_brightness = np.mean(gray_region) | |
| if mean_brightness > 200: # 太亮,不像文字 | |
| return False | |
| # 检查3:内容像素占比(文字不会太密集也不会太稀疏) | |
| content_pixels = np.sum(thresh_region > 0) | |
| total_pixels = thresh_region.shape[0] * thresh_region.shape[1] | |
| density = content_pixels / total_pixels if total_pixels > 0 else 0 | |
| if density < 0.05 or density > 0.5: # 密度不在合理范围 | |
| return False | |
| return True | |
| def detect_and_remove_text(img, thresh, row_sums): | |
| """ | |
| 检测并移除图标上方或下方的文字标题 | |
| 返回: (top_crop, bottom_crop) - 需要裁剪的上下边界 | |
| """ | |
| h = len(row_sums) | |
| # 定义"空白行"的阈值(行和很小) | |
| empty_threshold = max(5, img.shape[1] * 0.01) # 至少5,或宽度的1% | |
| # 定义"间隙"的最小行数 | |
| min_gap_rows = max(2, int(h * 0.02)) # 至少2行,或高度的2% | |
| # 找到所有内容行(非空白行) | |
| content_rows = [i for i, val in enumerate(row_sums) if val > empty_threshold] | |
| if len(content_rows) == 0: | |
| return 0, h | |
| # 找到主要内容区域(最大的连续内容块) | |
| # 先找出所有的间隙 | |
| gaps = [] | |
| if len(content_rows) > 1: | |
| for i in range(len(content_rows) - 1): | |
| gap_size = content_rows[i + 1] - content_rows[i] - 1 | |
| if gap_size >= min_gap_rows: | |
| gap_start = content_rows[i] | |
| gap_end = content_rows[i + 1] | |
| gaps.append((gap_start, gap_end, gap_size)) | |
| top_crop = 0 | |
| bottom_crop = h | |
| # 如果存在明显的间隙,说明可能有分离的文字 | |
| if gaps: | |
| # 找到最大的间隙 | |
| largest_gap = max(gaps, key=lambda x: x[2]) | |
| gap_start, gap_end, gap_size = largest_gap | |
| # 计算间隙上方和下方的内容量和行数 | |
| top_rows = gap_start | |
| bottom_rows = h - gap_end | |
| top_content = sum(row_sums[:gap_start]) | |
| bottom_content = sum(row_sums[gap_end:]) | |
| # 判断哪一部分是主要图标,哪一部分是文字 | |
| # 文字的特征:1) 内容较少 2) 行数较少 3) 符合文字特征 | |
| # 检查上方区域 | |
| if top_rows > 0 and top_rows < h * 0.3: # 上方行数不超过30% | |
| if top_content < bottom_content * 0.4: # 上方内容明显少于下方 | |
| # 进一步检查是否像文字 | |
| top_region = img[:gap_start, :] | |
| top_thresh = thresh[:gap_start, :] | |
| if is_likely_text_region(top_region, top_thresh): | |
| top_crop = gap_end | |
| # 检查下方区域 | |
| if bottom_rows > 0 and bottom_rows < h * 0.3: # 下方行数不超过30% | |
| if bottom_content < top_content * 0.4: # 下方内容明显少于上方 | |
| # 进一步检查是否像文字 | |
| bottom_region = img[gap_end:, :] | |
| bottom_thresh = thresh[gap_end:, :] | |
| if is_likely_text_region(bottom_region, bottom_thresh): | |
| bottom_crop = gap_start | |
| return top_crop, bottom_crop | |
| def smart_crop_icon(img, padding=10): | |
| """ | |
| 单个 Icon 处理:去字、去空、加 Padding | |
| 增强版:可以检测并删除上方或下方的文字标题 | |
| """ | |
| h, w = img.shape[:2] | |
| # 1. 裁剪掉可能残留的网格边缘 (比如四周切掉 3px) | |
| margin = 3 | |
| if h > 2*margin and w > 2*margin: | |
| img = img[margin:-margin, margin:-margin] | |
| h, w = img.shape[:2] | |
| gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) | |
| _, thresh = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU) | |
| # 2. 使用改进的方法检测并移除文字 | |
| row_sums = np.sum(thresh, axis=1) | |
| top_crop, bottom_crop = detect_and_remove_text(img, thresh, row_sums) | |
| # 应用裁剪 | |
| if top_crop > 0 or bottom_crop < h: | |
| img = img[top_crop:bottom_crop, :] | |
| thresh = thresh[top_crop:bottom_crop, :] | |
| # 3. 寻找 Icon 的精确边界 | |
| coords = cv2.findNonZero(thresh) | |
| if coords is not None: | |
| x, y, w_box, h_box = cv2.boundingRect(coords) | |
| # 裁剪并添加 Padding | |
| # 创建一个新的白色画布 | |
| final_h = h_box + 2 * padding | |
| final_w = w_box + 2 * padding | |
| canvas = np.ones((final_h, final_w, 3), dtype=np.uint8) * 255 | |
| # 提取 icon 内容 | |
| icon_content = img[y:y+h_box, x:x+w_box] | |
| # 将 icon 贴到画布中心 | |
| canvas[padding:padding+h_box, padding:padding+w_box] = icon_content | |
| return canvas | |
| return img | |
| def process_image_robust(image_path, labels_data): | |
| if not os.path.exists(image_path): | |
| print(f"Error: {image_path} not found.") | |
| return | |
| print(f"Processing: {image_path} ...") | |
| img = cv2.imread(image_path) | |
| # 1. 检测网格 | |
| # 返回的是排序好的 (x, y, w, h) 列表 | |
| grid_boxes = detect_grid_cells(img, expected_cols=6, expected_rows=4) | |
| # 2. 准备文本数据 | |
| lines = [l.strip() for l in labels_data.strip().split('\n') if l.strip()] | |
| style = "flat" | |
| start_idx = 0 | |
| if lines[0].lower().startswith("style:"): | |
| style = clean_filename(lines[0].split(':')[1]) | |
| start_idx = 1 | |
| output_dir = "extracted_icons" | |
| if not os.path.exists(output_dir): | |
| os.makedirs(output_dir) | |
| # 3. 遍历并保存 | |
| for i, box in enumerate(grid_boxes): | |
| text_idx = start_idx + i | |
| if text_idx >= len(lines): | |
| break | |
| # 解析文本 | |
| line_text = lines[text_idx] | |
| parts = line_text.split(',', 1) | |
| if len(parts) == 2: | |
| category = clean_filename(parts[0]) | |
| name = clean_filename(parts[1]) | |
| else: | |
| category = "icon" | |
| name = clean_filename(parts[0]) | |
| filename = f"{category}-{name}-{style}.png" | |
| save_path = os.path.join(output_dir, filename) | |
| # 提取单元格 | |
| x, y, w, h = box | |
| cell_img = img[y:y+h, x:x+w] | |
| # 智能裁切 | |
| final_img = smart_crop_icon(cell_img, padding=10) | |
| cv2.imwrite(save_path, final_img) | |
| # print(f"Saved: {filename}") # 减少刷屏 | |
| print(f"Done. Extracted {len(grid_boxes)} icons to '{output_dir}/'.\n") | |
| def process_batch_range(start_batch, end_batch, base_dir="generated_icons"): | |
| """ | |
| 批量处理指定范围内的batch文件夹下的所有png文件 | |
| Args: | |
| start_batch: 起始batch编号 (例如: 1) | |
| end_batch: 结束batch编号 (例如: 10) | |
| base_dir: batch文件夹所在的基础目录 | |
| """ | |
| print(f"开始批量处理 batch_{start_batch:04d} 到 batch_{end_batch:04d} ...\n") | |
| total_processed = 0 | |
| failed_files = [] | |
| for batch_num in range(start_batch, end_batch + 1): | |
| batch_dir = os.path.join(base_dir, f"batch_{batch_num:04d}") | |
| # 检查batch文件夹是否存在 | |
| if not os.path.exists(batch_dir): | |
| print(f"Warning: {batch_dir} 不存在,跳过...") | |
| continue | |
| print(f"处理 {batch_dir} ...") | |
| # 查找该batch下的所有png文件 | |
| png_files = glob.glob(os.path.join(batch_dir, "*.png")) | |
| if not png_files: | |
| print(f" 未找到png文件,跳过...") | |
| continue | |
| # 处理每个png文件 | |
| for png_path in sorted(png_files): | |
| # 构造对应的txt文件路径 | |
| txt_path = png_path.rsplit('.', 1)[0] + '.txt' | |
| # 检查txt文件是否存在 | |
| if not os.path.exists(txt_path): | |
| print(f" Warning: {txt_path} 不存在,跳过 {os.path.basename(png_path)}") | |
| failed_files.append(png_path) | |
| continue | |
| # 读取txt文件内容 | |
| try: | |
| with open(txt_path, 'r', encoding='utf-8') as f: | |
| labels_data = f.read() | |
| # 处理图像 | |
| process_image_robust(png_path, labels_data) | |
| total_processed += 1 | |
| except Exception as e: | |
| print(f" Error processing {os.path.basename(png_path)}: {str(e)}") | |
| failed_files.append(png_path) | |
| # 输出总结 | |
| print(f"\n{'='*60}") | |
| print(f"批量处理完成!") | |
| print(f"成功处理: {total_processed} 个文件") | |
| if failed_files: | |
| print(f"失败/跳过: {len(failed_files)} 个文件") | |
| print("失败文件列表:") | |
| for f in failed_files: | |
| print(f" - {f}") | |
| print(f"{'='*60}") | |
| if __name__ == "__main__": | |
| # 设置要处理的batch范围 | |
| START_BATCH = 1 # 起始batch编号 | |
| END_BATCH = 200 # 结束batch编号 | |
| # 执行批量处理 | |
| process_batch_range(START_BATCH, END_BATCH) |