#!/usr/bin/env python3
"""
Subtitle Safe Area QC · 字幕安全区域质检器
==========================​===================
自动检查字幕有没有遮脸、遮身体表演、贴边、看不清。

依赖：
  pip install opencv-python pillow numpy
  # 可选：pip install face-recognition dlib  # 人脸识别
  # 可选：pip install ultralytics  # YOLO 人体检测

用法：
  # 检查单张帧
  python subtitle-safe-area-qc.py --frame frame.png --subtitle-box x,y,w,h --output qc-report.json

  # 批量检查视频帧
  python subtitle-safe-area-qc.py --video input.mp4 --subtitle-ass subtitles.ass --output qc-report.json

  # 交互式标注模式（手动框选人脸/身体区域）
  python subtitle-safe-area-qc.py --frame frame.png --interactive

  # 生成质检报告（包含问题帧截图）
  python subtitle-safe-area-qc.py --video input.mp4 --subtitle-ass subtitles.ass --generate-report --output-dir ./qc-report/

输出 JSON 格式：
  {
    "frame": "frame_00123.png",
    "timestamp": 12.34,
    "issues": [
      {"type": "face_occlusion", "severity": "high", "bbox": [x,y,w,h]},
      {"type": "body_occlusion", "severity": "medium", "bbox": [x,y,w,h]},
      {"type": "edge_too_close", "severity": "low", "distance": 5},
      {"type": "low_contrast", "severity": "medium", "contrast_ratio": 2.1}
    ],
    "safe": false
  }

路径：
  video-ai-system/engines/subtitle-pipeline/qc-tools/subtitle-safe-area-qc.py
"""

import argparse
import json
import os
import sys
from pathlib import Path

try:
    import cv2
    import numpy as np
    from PIL import Image
except ImportError as e:
    print(f"[ERROR] 缺少依赖：{e}")
    print("请先安装：pip install opencv-python pillow numpy")
    sys.exit(1)


# 尝试导入可选依赖
try:
    import face_recognition
    FACE_RECOGNITION_AVAILABLE = True
except ImportError:
    FACE_RECOGNITION_AVAILABLE = False
    print("[WARN] 未安装 face-recognition，将使用简化人脸检测")

try:
    from ultralytics import YOLO
    YOLO_AVAILABLE = True
except ImportError:
    YOLO_AVAILABLE = False
    print("[WARN] 未安装 ultralytics，将使用简化身体检测")


def detect_faces(image: np.ndarray) -> list:
    """
    检测人脸区域
    
    :param image: 图片数组
    :return: 人脸边界框列表 [[x,y,w,h], ...]
    """
    if FACE_RECOGNITION_AVAILABLE:
        # 使用 face_recognition 库（基于 dlib）
        rgb_image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
        face_locations = face_recognition.face_locations(rgb_image)
        
        # 转换为 [x,y,w,h] 格式
        boxes = []
        for top, right, bottom, left in face_locations:
            x = left
            y = top
            w = right - left
            h = bottom - top
            boxes.append([x, y, w, h])
        
        return boxes
    else:
        # 简化方案：使用 OpenCV Haar Cascade
        cascade_path = cv2.data.haarcascades + "haarcascade_frontalface_default.xml"
        face_cascade = cv2.CascadeClassifier(cascade_path)
        
        gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
        faces = face_cascade.detectMultiScale(gray, 1.1, 4)
        
        return faces.tolist() if len(faces) > 0 else []


def detect_body(image: np.ndarray) -> list:
    """
    检测身体区域
    
    :param image: 图片数组
    :return: 身体边界框列表 [[x,y,w,h], ...]
    """
    if YOLO_AVAILABLE:
        # 使用 YOLO 检测人体
        model = YOLO("yolov8n.pt")  # 自动下载
        results = model(image)
        
        boxes = []
        for result in results:
            for box in result.boxes:
                cls = int(box.cls[0])
                # COCO 数据集中，person 的类别 ID 是 0
                if cls == 0:
                    x1, y1, x2, y2 = box.xyxy[0].tolist()
                    boxes.append([int(x1), int(y1), int(x2-x1), int(y2-y1)])
        
        return boxes
    else:
        # 简化方案：使用背景减除或轮廓检测
        print("[WARN] 未安装 YOLO，使用简化身体检测（可能不准确）")
        
        # 策略：检测图像中的大轮廓（假设身体是大区域）
        gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
        _, binary = cv2.threshold(gray, 128, 255, cv2.THRESH_BINARY_INV)
        
        contours, _ = cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
        
        boxes = []
        for cnt in contours:
            area = cv2.contourArea(cnt)
            if area > image.shape[0] * image.shape[1] * 0.1:  # 面积 > 10%
                x, y, w, h = cv2.boundingRect(cnt)
                boxes.append([x, y, w, h])
        
        return boxes


def detect_subtitle_box(image: np.ndarray) -> list:
    """
    检测字幕框位置
    
    :param image: 图片数组
    :return: 字幕边界框 [x,y,w,h]
    """
    # 策略：检测底部区域的白色文本
    height, width = image.shape[:2]
    
    # 裁剪底部区域
    bottom_region = image[int(height * 0.85):, :]
    
    # 转为灰度图
    gray = cv2.cvtColor(bottom_region, cv2.COLOR_BGR2GRAY)
    
    # 二值化（检测白色文本）
    _, binary = cv2.threshold(gray, 200, 255, cv2.THRESH_BINARY)
    
    # 查找轮廓
    contours, _ = cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
    
    if not contours:
        return [0, int(height * 0.85), width, int(height * 0.12)]  # 默认底部区域
    
    # 收集所有轮廓的边界框
    all_x = []
    all_y = []
    all_w = []
    all_h = []
    
    for cnt in contours:
        x, y, w, h = cv2.boundingRect(cnt)
        all_x.append(x)
        all_y.append(y)
        all_w.append(w)
        all_h.append(h)
    
    # 合并所有轮廓的边界框
    # 先在 bottom_region 坐标系里算完，最后再加偏移
    x_bottom = min(all_x)
    y_bottom = min(all_y)
    max_x_bottom = max([all_x[i] + all_w[i] for i in range(len(all_x))])
    max_y_bottom = max([all_y[i] + all_h[i] for i in range(len(all_y))])
    w = max_x_bottom - x_bottom
    h = max_y_bottom - y_bottom
    
    # 最后统一加偏移
    x = x_bottom
    y = y_bottom + int(height * 0.85)
    # w, h 不需要加偏移（它们是在bottom_region里的尺寸）
    
    return [x, y, w, h]
    
    
def calculate_iou(box1: list, box2: list) -> float:
    """
    计算两个边界框的 IOU（交并比）
    
    :param box1: 边界框1 [x,y,w,h]
    :param box2: 边界框2 [x,y,w,h]
    :return: IOU 值（0-1）
    """
    x1 = max(box1[0], box2[0])
    y1 = max(box1[1], box2[1])
    x2 = min(box1[0] + box1[2], box2[0] + box2[2])
    y2 = min(box1[1] + box1[3], box2[1] + box2[3])
    
    if x2 < x1 or y2 < y1:
        return 0.0
    
    intersection = (x2 - x1) * (y2 - y1)
    area1 = box1[2] * box1[3]
    area2 = box2[2] * box2[3]
    union = area1 + area2 - intersection
    
    return intersection / union if union > 0 else 0.0


def check_subtitle_safe_area(image_path: str, subtitle_box: list = None, interactive: bool = False) -> dict:
    """
    检查字幕安全区域
    
    :param image_path: 图片路径
    :param subtitle_box: 字幕框 [x,y,w,h]（如果为 None，自动检测）
    :param interactive: 是否交互式标注
    :return: 质检报告字典
    """
    # 读取图片
    img = cv2.imread(image_path)
    if img is None:
        print(f"[ERROR] 无法读取图片：{image_path}")
        return {}
    
    height, width = img.shape[:2]
    print(f"[INFO] 检查图片：{image_path} ({width}x{height})")
    
    # 检测字幕框
    if subtitle_box is None:
        if interactive:
            print("[INFO] 交互式标注模式：请手动框选字幕区域")
            roi = cv2.selectROI("Select Subtitle Region", img, showCrosshair=True)
            cv2.destroyAllWindows()
            subtitle_box = [int(roi[0]), int(roi[1]), int(roi[2]), int(roi[3])]
        else:
            subtitle_box = detect_subtitle_box(img)
    
    print(f"[INFO] 字幕框：x={subtitle_box[0]}, y={subtitle_box[1]}, w={subtitle_box[2]}, h={subtitle_box[3]}")
    
    # 检测人脸
    print("[INFO] 检测人脸...")
    face_boxes = detect_faces(img)
    print(f"[INFO] 找到 {len(face_boxes)} 个人脸")
    
    # 检测身体
    print("[INFO] 检测身体...")
    body_boxes = detect_body(img)
    print(f"[INFO] 找到 {len(body_boxes)} 个身体区域")
    
    # 检查问题
    issues = []
    
    # 1. 检查是否遮挡脸部
    for i, face_box in enumerate(face_boxes):
        iou = calculate_iou(subtitle_box, face_box)
        if iou > 0.1:  # IOU > 10% 认为有遮挡
            severity = "high" if iou > 0.5 else "medium"
            issues.append({
                "type": "face_occlusion",
                "severity": severity,
                "bbox": face_box,
                "iou": round(iou, 2),
                "message": f"字幕遮挡脸部（IOU={iou:.2f}）"
            })
            print(f"[ISSUE] 字幕遮挡脸部（IOU={iou:.2f}）")
    
    # 2. 检查是否遮挡身体
    for i, body_box in enumerate(body_boxes):
        iou = calculate_iou(subtitle_box, body_box)
        if iou > 0.2:  # IOU > 20% 认为有遮挡
            severity = "high" if iou > 0.6 else "medium"
            issues.append({
                "type": "body_occlusion",
                "severity": severity,
                "bbox": body_box,
                "iou": round(iou, 2),
                "message": f"字幕遮挡身体（IOU={iou:.2f}）"
            })
            print(f"[ISSUE] 字幕遮挡身体（IOU={iou:.2f}）")
    
    # 3. 检查是否贴边
    edge_threshold = 20  # 像素
    if subtitle_box[0] < edge_threshold:
        issues.append({
            "type": "edge_too_close",
            "severity": "low",
            "distance": subtitle_box[0],
            "message": f"字幕距离左边缘太近（{subtitle_box[0]}px）"
        })
        print(f"[ISSUE] 字幕距离左边缘太近（{subtitle_box[0]}px）")
    
    if (subtitle_box[0] + subtitle_box[2]) > (width - edge_threshold):
        distance = (subtitle_box[0] + subtitle_box[2]) - width
        issues.append({
            "type": "edge_too_close",
            "severity": "low",
            "distance": abs(distance),
            "message": f"字幕距离右边缘太近（{abs(distance)}px）"
        })
        print(f"[ISSUE] 字幕距离右边缘太近（{abs(distance)}px）")
    
    # 4. 检查对比度（字幕是否看不清）
    # 简化方案：计算字幕区域和背景的平均亮度差
    subtitle_region = img[
        subtitle_box[1]:subtitle_box[1]+subtitle_box[3],
        subtitle_box[0]:subtitle_box[0]+subtitle_box[2]
    ]
    
    if subtitle_region.size > 0:
        # 计算字幕区域的平均亮度
        subtitle_gray = cv2.cvtColor(subtitle_region, cv2.COLOR_BGR2GRAY)
        subtitle_brightness = np.mean(subtitle_gray)
        
        # 计算背景区域的平均亮度（字幕框上方的区域）
        background_region = img[
            max(0, subtitle_box[1]-subtitle_box[3]):subtitle_box[1],
            subtitle_box[0]:subtitle_box[0]+subtitle_box[2]
        ]
        
        if background_region.size > 0:
            background_gray = cv2.cvtColor(background_region, cv2.COLOR_BGR2GRAY)
            background_brightness = np.mean(background_gray)
            
            # 计算对比度（亮度差）
            contrast = abs(subtitle_brightness - background_brightness)
            contrast_ratio = contrast / max(subtitle_brightness, background_brightness)
            
            if contrast_ratio < 0.3:  # 对比度 < 30% 认为看不清
                severity = "high" if contrast_ratio < 0.1 else "medium"
                issues.append({
                    "type": "low_contrast",
                    "severity": severity,
                    "contrast_ratio": round(contrast_ratio, 2),
                    "message": f"字幕对比度太低（{contrast_ratio:.2f}），可能看不清"
                })
                print(f"[ISSUE] 字幕对比度太低（{contrast_ratio:.2f}），可能看不清")
    
    # 生成报告
    report = {
        "image_path": image_path,
        "video_width": width,
        "video_height": height,
        "subtitle_box": subtitle_box,
        "face_boxes": face_boxes,
        "body_boxes": body_boxes,
        "issues": issues,
        "safe": len(issues) == 0,
        "issue_count": len(issues)
    }
    
    if report["safe"]:
        print(f"[OK] 字幕安全区域检查通过")
    else:
        print(f"[WARN] 发现 {len(issues)} 个问题")
    
    return report


def batch_check_video(video_path: str, ass_path: str, output_dir: str, sample_interval: float = 1.0):
    """
    批量检查视频帧
    
    :param video_path: 视频文件路径
    :param ass_path: ASS 字幕文件路径
    :param output_dir: 输出目录
    :param sample_interval: 采样间隔（秒）
    """
    # 打开视频
    cap = cv2.VideoCapture(video_path)
    if not cap.isOpened():
        print(f"[ERROR] 无法打开视频：{video_path}")
        return
    
    fps = cap.get(cv2.CAP_PROP_FPS)
    total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
    
    print(f"[INFO] 视频信息：{total_frames} 帧，{fps} FPS")
    
    # 创建输出目录
    os.makedirs(output_dir, exist_ok=True)
    frames_dir = os.path.join(output_dir, "problem-frames/")
    os.makedirs(frames_dir, exist_ok=True)
    
    # 读取 ASS 字幕文件（获取字幕时间点）
    # 简化方案：假设字幕在底部，直接检测
    # TODO: 解析 ASS 文件，获取准确的字幕时间点
    
    reports = []
    frame_count = 0
    
    while True:
        ret, frame = cap.read()
        if not ret:
            break
        
        timestamp = frame_count / fps
        
        # 按采样间隔检查
        if timestamp % sample_interval < (1.0 / fps):
            # 保存帧为临时文件
            temp_frame_path = os.path.join(output_dir, f"temp_frame_{frame_count:06d}.png")
            cv2.imwrite(temp_frame_path, frame)
            
            # 检查字幕安全区域
            report = check_subtitle_safe_area(temp_frame_path)
            
            if report and not report["safe"]:
                # 保存问题帧
                problem_frame_path = os.path.join(frames_dir, f"problem_{frame_count:06d}.png")
                cv2.imwrite(problem_frame_path, frame)
                
                report["timestamp"] = timestamp
                report["problem_frame"] = problem_frame_path
                reports.append(report)
                
                print(f"[WARN] 发现问题的帧：{timestamp:.2f}s")
            
            # 删除临时文件
            os.remove(temp_frame_path)
        
        frame_count += 1
    
    cap.release()
    
    # 保存报告
    report_path = os.path.join(output_dir, "qc-report.json")
    with open(report_path, "w", encoding="utf-8") as f:
        json.dump(reports, f, ensure_ascii=False, indent=2)
    
    print(f"\n[OK] 批量检查完成")
    print(f"[INFO] 共检查 {frame_count} 帧，发现 {len(reports)} 个有问题帧")
    print(f"[INFO] 报告已保存：{report_path}")


def main():
    parser = argparse.ArgumentParser(
        description="Subtitle Safe Area QC · 字幕安全区域质检器",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog="""
示例：
  python subtitle-safe-area-qc.py --frame frame.png --subtitle-box 100,1750,880,120 --output qc-report.json
  python subtitle-safe-area-qc.py --video input.mp4 --subtitle-ass subtitles.ass --output qc-report.json
  python subtitle-safe-area-qc.py --frame frame.png --interactive
"""
    )
    parser.add_argument("--frame", help="单张帧图片路径")
    parser.add_argument("--video", help="视频文件路径（批量检查）")
    parser.add_argument("--subtitle-box", help="字幕框坐标 x,y,w,h（逗号分隔）")
    parser.add_argument("--subtitle-ass", help="ASS 字幕文件路径（用于批量检查）")
    parser.add_argument("--output", help="输出报告文件路径")
    parser.add_argument("--interactive", action="store_true", help="交互式标注模式")
    parser.add_argument("--generate-report", action="store_true", help="生成质检报告（包含问题帧截图）")
    parser.add_argument("--output-dir", help="输出目录（用于批量检查）")
    parser.add_argument("--sample-interval", type=float, default=1.0, help="采样间隔（秒，用于批量检查）")
    
    args = parser.parse_args()
    
    if args.frame:
        # 单张帧检查
        subtitle_box = None
        if args.subtitle_box:
            subtitle_box = [int(x) for x in args.subtitle_box.split(",")]
        
        report = check_subtitle_safe_area(args.frame, subtitle_box, args.interactive)
        
        if not report:
            sys.exit(1)
        
        if args.output:
            with open(args.output, "w", encoding="utf-8") as f:
                json.dump(report, f, ensure_ascii=False, indent=2)
            print(f"[OK] 质检报告已保存：{args.output}")
    
    elif args.video:
        # 批量检查
        if not args.subtitle_ass:
            print("[ERROR] 批量检查需要指定 --subtitle-ass")
            sys.exit(1)
        
        output_dir = args.output_dir or "./qc-report/"
        batch_check_video(args.video, args.subtitle_ass, output_dir, args.sample_interval)
    
    else:
        print("[ERROR] 请指定 --frame 或 --video")
        sys.exit(1)


if __name__ == "__main__":
    main()
