"""Measured mask QA and previews made only from the original character pixels.""" import colorsys import html import json import math import re from pathlib import Path import numpy as np import cv2 from PIL import Image, ImageDraw, ImageFont def assess_candidate(mask, alpha, bbox, points): mask = np.asarray(mask, dtype=bool) if mask.shape != alpha.shape: raise ValueError('Mask dimensions do not match the source') clipped = mask & (alpha > 0) _, _, stats, _ = cv2.connectedComponentsWithStats(clipped.astype(np.uint8), 8) area, foreground_area = int(mask.sum()), int(clipped.sum()) x0, y0, x1, y1 = bbox hit = sum(bool(clipped[y, x]) for x, y in points) return { 'raw_area_px': area, 'area_px': foreground_area, 'outside_alpha_ratio': (area - foreground_area) / max(1, area), 'inside_roi_fraction': int(clipped[y0:y1, x0:x1].sum()) / max(1, foreground_area), 'seed_hit_fraction': hit / max(1, len(points)), 'components_at_least_4px': int((stats[1:, cv2.CC_STAT_AREA] >= 4).sum()), } def render_outputs(image, parts, out_dir): out = Path(out_dir) (out / 'parts').mkdir(parents=True, exist_ok=True) (out / 'masks').mkdir(parents=True, exist_ok=True) rgba = np.array(image.convert('RGBA')) h, w = rgba.shape[:2] coverage = np.zeros((h, w), np.uint16) white = Image.new('RGBA', (w, h), 'white') overlay = Image.alpha_composite(white, image.convert('RGBA')) records, crops, clipped_masks = [], [], [] seen = set() for index, part in enumerate(parts): name = part['id'] if not re.fullmatch('[a-z][a-z0-9_-]{0,63}', name) or name in seen: raise ValueError(f'Invalid or duplicate part ID: {name}') seen.add(name) mask = np.asarray(part['mask'], dtype=bool) if mask.shape != (h, w): raise ValueError(f'{name}: mask dimensions mismatch') mask = mask & (rgba[:, :, 3] > 0) clipped_masks.append(mask) ys, xs = np.nonzero(mask) if not len(xs): raise ValueError(f'{name}: empty foreground mask') coverage += mask bbox = [int(xs.min()), int(ys.min()), int(xs.max()) + 1, int(ys.max()) + 1] color = [round(v * 255) for v in colorsys.hsv_to_rgb((index * .61803398875) % 1, .75, .95)] cut = rgba.copy() cut[~mask] = 0 crop = Image.fromarray(cut).crop(bbox) crop.save(out / 'parts' / f'{name}.png') Image.fromarray(mask.astype(np.uint8) * 255).save(out / 'masks' / f'{name}.png') tint = np.zeros_like(rgba) tint[mask, :3] = color tint[mask, 3] = 100 overlay = Image.alpha_composite(overlay, Image.fromarray(tint)) records.append({k: v for k, v in part.items() if k != 'mask'} | { 'color_rgb': color, 'crop_xyxy': bbox, 'area_px': int(mask.sum()), 'image': f'parts/{name}.png', 'mask': f'masks/{name}.png', }) crops.append(crop) near_duplicates = [] for i, a in enumerate(clipped_masks): for j in range(i + 1, len(clipped_masks)): b = clipped_masks[j] iou = int((a & b).sum()) / max(1, int((a | b).sum())) if iou >= .8: near_duplicates.append({'part_a': records[i]['id'], 'part_b': records[j]['id'], 'iou': iou}) for index in [i, j]: warnings = records[index].setdefault('warnings', []).copy() if 'NEAR_DUPLICATE_MASK' not in warnings: warnings.append('NEAR_DUPLICATE_MASK') records[index]['warnings'] = warnings # Keep native scale, separated by padding and a label band. No rotation or # resampling of source pixels is used in the exploded sheet. pad, label = 24, 26 cell_widths = [max(c.width, len(r['id']) * 8 + 8) for c, r in zip(crops, records)] sheet_width = max(640, max(cell_widths, default=1) + pad * 2, int(math.sqrt(sum((cw + pad) * (c.height + label + pad) for cw, c in zip(cell_widths, crops)) * 1.5))) x, y, row_height = pad, pad, 0 for record, crop, cw in zip(records, crops, cell_widths): if x + cw + pad > sheet_width and x != pad: x, y, row_height = pad, y + row_height + pad, 0 record['sheet_xyxy'] = [x, y + label, x + crop.width, y + label + crop.height] x += cw + pad row_height = max(row_height, label + crop.height) sheet = Image.new('RGB', (sheet_width, max(100, y + row_height + pad)), 'white') draw = ImageDraw.Draw(sheet) font = ImageFont.load_default(size=14) for record, crop in zip(records, crops): x0, y0, _, _ = record['sheet_xyxy'] draw.rectangle((x0, y0 - label + 4, x0 + 10, y0 - label + 14), fill=tuple(record['color_rgb'])) draw.text((x0 + 16, y0 - label + 1), record['id'], fill='black', font=font) sheet.paste(crop, (x0, y0), crop) overlay.convert('RGB').save(out / 'annotated.png') sheet.save(out / 'parts-sheet.png') Image.fromarray(((rgba[:, :, 3] > 0) & (coverage == 0)).astype(np.uint8) * 255).save(out / 'unassigned-mask.png') Image.fromarray((coverage > 1).astype(np.uint8) * 255).save(out / 'overlap-mask.png') total = int((rgba[:, :, 3] > 0).sum()) return {'parts': records, 'near_duplicate_masks': near_duplicates, 'coverage': { 'foreground_pixels': total, 'covered_pixels': int((coverage > 0).sum()), 'coverage_ratio': int((coverage > 0).sum()) / max(1, total), 'overlap_pixels': int((coverage > 1).sum()), 'unassigned_pixels': int(((rgba[:, :, 3] > 0) & (coverage == 0)).sum()), }, 'outputs': {'annotated': 'annotated.png', 'parts_sheet': 'parts-sheet.png'}} def write_report(out_dir, result): out = Path(out_dir) rows = ''.join(f'   {html.escape(p["id"])}{html.escape(p["semantic"])}{p["model_score"]:.3f}{html.escape(", ".join(p.get("warnings", [])))}' for p in result['parts']) body = f'''SAM 部件分割

SAM 本地分割结果

颜色表示候选部件 mask。白底拆解图保留原始可见像素,尚未补全遮挡部分。重叠 mask 保留并计入诊断,不代表已经获得互斥、语义正确的最终拆件。

原图半透明标注

白底拆解图

部件

{rows}
ID / 颜色语义模型分数需复核

未生成的部件

{html.escape(json.dumps(result['skipped'], ensure_ascii=False, indent=2))}

覆盖与重叠

覆盖率只表示有 mask 的像素比例,不是分割准确率。模型分数也不代表部件语义已验证。

漏分区域(白色) · 重叠区域(白色)

{html.escape(json.dumps(result['coverage'], indent=2))}

疑似重复分割

以下不同部件的 mask IoU ≥ 0.8,需要检查是否分到了同一物体。

{html.escape(json.dumps(result['near_duplicate_masks'], indent=2))}
''' (out / 'report.html').write_text(body, encoding='utf-8')