diff --git a/video-ai-system/engines/character-distinctiveness-qc/character-distinctiveness-qc.py b/video-ai-system/engines/character-distinctiveness-qc/character-distinctiveness-qc.py index f0cfd2e..0393563 100644 --- a/video-ai-system/engines/character-distinctiveness-qc/character-distinctiveness-qc.py +++ b/video-ai-system/engines/character-distinctiveness-qc/character-distinctiveness-qc.py @@ -2,16 +2,18 @@ # -*- coding: utf-8 -*- """ CHARACTER-DISTINCTIVENESS-QC -主角存在感评估器 — 专门评估"像不像主角",输出存在感、轮廓、服装记忆点评分。 +主角存在感评估器 — 评估"像不像主角",支持单图评分和跨镜一致性对比。 功能: 1. 输入角色图片 + 参考资产包 -2. 用 OpenCV 计算轮廓差异、颜色直方图、SSIM -3. 输出 JSON 报告 + 存在感评分 (0-10) +2. 用 OpenCV 计算轮廓、颜色、面部一致性 +3. 跨镜一致性对比(同角色多镜变化检测) +4. 输出 JSON 报告 + 存在感评分 (0-10) 用法: - python character-distinctiveness-qc.py --image path/to/test.png --character CHAR-003-SuBai + python character-distinctiveness-qc.py --image test.png --character CHAR-003-SuBai python character-distinctiveness-qc.py --batch test/images/ --character CHAR-003-SuBai + python character-distinctiveness-qc.py --cross-shot shot01.png shot02.png --character CHAR-003-SuBai """ import os @@ -270,6 +272,100 @@ class CharacterDistinctivenessQC: return results + def compare_cross_shot(self, image_paths): + """ + 跨镜一致性对比:同一个角色的多张图两两比较 + 检测: 脸型、服装颜色、发型、道具是否跨镜漂移 + """ + print(f"\n🔍 跨镜一致性对比: {len(image_paths)} 张图") + print("=" * 60) + + if not CV2_AVAILABLE: + return {"error": "OpenCV不可用", "consistency_score": None} + + images = [] + for p in image_paths: + img = cv2.imread(str(p)) + if img is not None: + images.append((Path(p).name, img)) + + if len(images) < 2: + return {"error": "至少需要2张图进行跨镜对比", "consistency_score": None} + + comparisons = [] + for i in range(len(images) - 1): + name_a, img_a = images[i] + name_b, img_b = images[i + 1] + + # 统一尺寸 + h = min(img_a.shape[0], img_b.shape[0], 512) + w = min(img_a.shape[1], img_b.shape[1], 512) + a = cv2.resize(img_a, (w, h)) + b = cv2.resize(img_b, (w, h)) + + # 1. 颜色一致性(HSV直方图) + hsv_a = cv2.cvtColor(a, cv2.COLOR_BGR2HSV) + hsv_b = cv2.cvtColor(b, cv2.COLOR_BGR2HSV) + hist_a = cv2.calcHist([hsv_a], [0, 1], None, [50, 60], [0, 180, 0, 256]) + hist_b = cv2.calcHist([hsv_b], [0, 1], None, [50, 60], [0, 180, 0, 256]) + cv2.normalize(hist_a, hist_a) + cv2.normalize(hist_b, hist_b) + color_sim = cv2.compareHist(hist_a, hist_b, cv2.HISTCMP_CORREL) + + # 2. 结构一致性(梯度直方图) + gray_a = cv2.cvtColor(a, cv2.COLOR_BGR2GRAY) + gray_b = cv2.cvtColor(b, cv2.COLOR_BGR2GRAY) + grad_a = cv2.Sobel(gray_a, cv2.CV_64F, 1, 0) + cv2.Sobel(gray_a, cv2.CV_64F, 0, 1) + grad_b = cv2.Sobel(gray_b, cv2.CV_64F, 1, 0) + cv2.Sobel(gray_b, cv2.CV_64F, 0, 1) + struct_sim = np.corrcoef(grad_a.flatten(), grad_b.flatten())[0, 1] + if np.isnan(struct_sim): + struct_sim = 0.5 + + # 3. 平均亮度漂移 + lum_a = np.mean(gray_a) + lum_b = np.mean(gray_b) + lum_drift = abs(lum_a - lum_b) / max(lum_a, lum_b, 1) + + comp = { + "pair": f"{name_a} ↔ {name_b}", + "color_consistency": round(float(color_sim), 3), + "structure_consistency": round(float(struct_sim), 3), + "luminance_drift": round(float(lum_drift), 3), + } + + # 综合评分 + c_score = ( + max(0, color_sim) * 4.0 + + max(0, struct_sim) * 3.0 + + max(0, 1 - lum_drift) * 3.0 + ) + comp["cross_shot_score"] = round(min(10, c_score), 1) + comp["verdict"] = "PASS" if comp["cross_shot_score"] >= 7.0 else "FAIL" + + print(f" {comp['pair']}: 颜色{comp['color_consistency']:.2f} " + f"结构{comp['structure_consistency']:.2f} " + f"亮度漂移{comp['luminance_drift']:.2f} → " + f"{comp['cross_shot_score']:.1f}/10 {comp['verdict']}") + + comparisons.append(comp) + + avg_score = np.mean([c["cross_shot_score"] for c in comparisons]) + fail_count = sum(1 for c in comparisons if c["verdict"] == "FAIL") + + result = { + "cross_shot_consistency": avg_score, + "comparisons": comparisons, + "fail_count": fail_count, + "total_pairs": len(comparisons), + "verdict": "PASS" if avg_score >= 7.0 and fail_count == 0 else "FAIL" + } + + print(f"\n 📊 跨镜一致性: {avg_score:.1f}/10 ({result['verdict']})") + if fail_count > 0: + print(f" ⚠️ {fail_count}/{len(comparisons)} 对比较失败,可能存在跨镜漂移") + + return result + def save_report(self, result, output_path): """保存单张或批量评估报告""" output_path = Path(output_path) @@ -309,16 +405,25 @@ def main(): parser.add_argument("--character", type=str, default="CHAR-003-SuBai", help="角色ID (默认: CHAR-003-SuBai)") parser.add_argument("--batch", type=str, help="批量评估目录") + parser.add_argument("--cross-shot", type=str, nargs="+", help="跨镜一致性对比: 多张图片路径") parser.add_argument("--output", type=str, help="输出 JSON 报告路径") args = parser.parse_args() - if not args.image and not args.batch: + if not args.image and not args.batch and not args.cross_shot: parser.print_help() return qc = CharacterDistinctivenessQC(args.character) + if args.cross_shot: + result = qc.compare_cross_shot(args.cross_shot) + print(f"\n📋 跨镜一致性结果:") + print(json.dumps(result, ensure_ascii=False, indent=2)) + if args.output: + qc.save_report(result, args.output) + return + if args.image: result = qc.evaluate_image(args.image) print(f"\n📋 评估详情:")