Spaces:
Sleeping
Sleeping
File size: 17,993 Bytes
273b8b1 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 | 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) |