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

| 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')