D144 · 角色存在感QC升级: 新增跨镜一致性对比
- 新增 --cross-shot 模式: 多张同角色图两两对比 - 检测项: 颜色一致性(HSV直方图) + 结构一致性(梯度) + 亮度漂移 - 跨镜评分规则: 颜色40% + 结构30% + 亮度30% 铸渊 ICE-GL-ZY001 · D144 · 2026-06-24
This commit is contained in:
parent
e403a41992
commit
679a57b791
@ -2,16 +2,18 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
"""
|
"""
|
||||||
CHARACTER-DISTINCTIVENESS-QC
|
CHARACTER-DISTINCTIVENESS-QC
|
||||||
主角存在感评估器 — 专门评估"像不像主角",输出存在感、轮廓、服装记忆点评分。
|
主角存在感评估器 — 评估"像不像主角",支持单图评分和跨镜一致性对比。
|
||||||
|
|
||||||
功能:
|
功能:
|
||||||
1. 输入角色图片 + 参考资产包
|
1. 输入角色图片 + 参考资产包
|
||||||
2. 用 OpenCV 计算轮廓差异、颜色直方图、SSIM
|
2. 用 OpenCV 计算轮廓、颜色、面部一致性
|
||||||
3. 输出 JSON 报告 + 存在感评分 (0-10)
|
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 --batch test/images/ --character CHAR-003-SuBai
|
||||||
|
python character-distinctiveness-qc.py --cross-shot shot01.png shot02.png --character CHAR-003-SuBai
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
@ -270,6 +272,100 @@ class CharacterDistinctivenessQC:
|
|||||||
|
|
||||||
return results
|
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):
|
def save_report(self, result, output_path):
|
||||||
"""保存单张或批量评估报告"""
|
"""保存单张或批量评估报告"""
|
||||||
output_path = Path(output_path)
|
output_path = Path(output_path)
|
||||||
@ -309,16 +405,25 @@ def main():
|
|||||||
parser.add_argument("--character", type=str, default="CHAR-003-SuBai",
|
parser.add_argument("--character", type=str, default="CHAR-003-SuBai",
|
||||||
help="角色ID (默认: CHAR-003-SuBai)")
|
help="角色ID (默认: CHAR-003-SuBai)")
|
||||||
parser.add_argument("--batch", type=str, help="批量评估目录")
|
parser.add_argument("--batch", type=str, help="批量评估目录")
|
||||||
|
parser.add_argument("--cross-shot", type=str, nargs="+", help="跨镜一致性对比: 多张图片路径")
|
||||||
parser.add_argument("--output", type=str, help="输出 JSON 报告路径")
|
parser.add_argument("--output", type=str, help="输出 JSON 报告路径")
|
||||||
|
|
||||||
args = parser.parse_args()
|
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()
|
parser.print_help()
|
||||||
return
|
return
|
||||||
|
|
||||||
qc = CharacterDistinctivenessQC(args.character)
|
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:
|
if args.image:
|
||||||
result = qc.evaluate_image(args.image)
|
result = qc.evaluate_image(args.image)
|
||||||
print(f"\n📋 评估详情:")
|
print(f"\n📋 评估详情:")
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user