Files
pifu/wanzheng.py
T
2026-09-11 14:42:12 +08:00

654 lines
27 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
皮肤分析工具 - 后端 API 版
输出 JSON 数据到文件夹,完成后回调后端接口
"""
import cv2
import numpy as np
from ultralytics import YOLO
from pathlib import Path
import os
import argparse
import threading
import time
from concurrent.futures import ThreadPoolExecutor, as_completed
from tqdm import tqdm
import logging
from typing import Dict, List, Optional, Tuple
import requests
from config.settings import API_HOST, API_CALLBACK
# ==================== 日志配置 ====================
def setup_logging(verbose: bool = False):
level = logging.DEBUG if verbose else logging.INFO
logging.basicConfig(
level=level,
format='%(asctime)s | %(levelname)s | %(message)s',
datefmt='%H:%M:%S'
)
return logging.getLogger(__name__)
logger = setup_logging()
# ==================== 支持中文路径的读写函数 ====================
def cv2_imread_chinese(file_path: str) -> Optional[np.ndarray]:
if not os.path.exists(file_path):
logger.warning(f"文件不存在:{file_path}")
return None
try:
img = cv2.imdecode(np.fromfile(file_path, dtype=np.uint8), cv2.IMREAD_COLOR)
return img
except Exception as e:
logger.error(f"读取失败 {file_path}: {e}")
return None
def cv2_imwrite_chinese(file_path: str, img: np.ndarray) -> bool:
try:
ext = os.path.splitext(file_path)[1]
if ext.lower() not in ['.jpg', '.jpeg', '.png', '.bmp']:
ext = '.jpg'
cv2.imencode(ext, img)[1].tofile(file_path)
return True
except Exception as e:
logger.error(f"写入失败 {file_path}: {e}")
return False
# ==================== YOLO 痤疮检测 ====================
def detect_acne(img: np.ndarray, image_path: str, model: YOLO, output_path: str) -> Tuple[bool, Dict, str]:
try:
# 直接全图检测
results = model(img, conf=0.2, verbose=False)
annotated_img = img.copy()
class_counts = {}
for result in results:
if result.boxes is not None:
for box in result.boxes:
cls_id = int(box.cls[0])
class_name = model.names[cls_id]
x1, y1, x2, y2 = map(int, box.xyxy[0])
conf = float(box.conf[0])
color = (0, 0, 255)
cv2.rectangle(annotated_img, (x1, y1), (x2, y2), color, 2)
label = f"{class_name} {conf:.2f}"
(text_width, text_height), baseline = cv2.getTextSize(
label, cv2.FONT_HERSHEY_SIMPLEX, 0.5, 2
)
cv2.rectangle(annotated_img, (x1, y1 - text_height - baseline),
(x1 + text_width, y1), color, -1)
cv2.putText(annotated_img, label, (x1, y1 - baseline),
cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 255, 255), 2)
class_counts[class_name] = class_counts.get(class_name, 0) + 1
if cv2_imwrite_chinese(output_path, annotated_img):
logger.debug(f"YOLO 完成:{os.path.basename(output_path)}")
return True, class_counts, ""
else:
return False, {}, "写入失败"
except Exception as e:
logger.error(f"YOLO 失败:{e}")
return False, {}, str(e)
def build_lut(anchors: np.ndarray) -> np.ndarray:
"""从锚点构建自定义 256 级颜色映射 LUT"""
lut = np.zeros((256, 3), dtype=np.uint8)
for c in range(3):
lut[:, c] = np.interp(np.arange(256), anchors[:, 0], anchors[:, c + 1])
return lut.reshape(256, 1, 3)
def detect_skin_region(img: np.ndarray):
"""多色彩空间皮肤检测,返回 (skin_soft, skin_binary, a_float)"""
lab = cv2.cvtColor(img, cv2.COLOR_BGR2LAB)
_, a_ch, _ = cv2.split(lab)
a_float = a_ch.astype(np.float32)
mask_lab = cv2.inRange(lab, (40, 120, 105), (235, 185, 175))
ycrcb = cv2.cvtColor(img, cv2.COLOR_BGR2YCrCb)
mask_ycrcb = cv2.inRange(ycrcb, (40, 133, 77), (240, 175, 135))
skin_mask = cv2.bitwise_or(mask_lab, mask_ycrcb)
kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (11, 11))
skin_mask = cv2.morphologyEx(skin_mask, cv2.MORPH_CLOSE, kernel, iterations=3)
skin_mask = cv2.morphologyEx(skin_mask, cv2.MORPH_OPEN, kernel, iterations=1)
skin_mask = cv2.dilate(skin_mask, cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (7, 7)), iterations=1)
contours, _ = cv2.findContours(skin_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
if contours:
largest = max(contours, key=cv2.contourArea)
skin_mask = np.zeros_like(skin_mask)
cv2.drawContours(skin_mask, [largest], -1, 255, -1)
hull = cv2.convexHull(largest)
cv2.drawContours(skin_mask, [hull], -1, 255, -1)
skin_soft = cv2.GaussianBlur(skin_mask.astype(np.float32), (21, 21), 0) / 255.0
return skin_soft, skin_mask, a_float
def compute_heatmap_idx(a_float: np.ndarray, skin_mask: np.ndarray,
baseline_percentile: float, sensitivity: float) -> np.ndarray:
"""计算红度热力图索引(0-255)"""
skin_pixels = a_float[skin_mask > 128]
if len(skin_pixels) > 100:
baseline = np.percentile(skin_pixels, baseline_percentile)
else:
baseline = np.median(a_float)
redness = np.maximum(0, a_float - baseline) * sensitivity
vmax = np.percentile(redness, 98)
if vmax > 0:
idx = np.clip(redness, 0, vmax) / vmax * 255.0
else:
idx = np.zeros_like(a_float)
return np.clip(idx, 0, 255).astype(np.uint8)
def add_dark_details(result: np.ndarray, img: np.ndarray,
intensity: float = 0.50) -> np.ndarray:
"""叠加原图暗部细节(眉毛/头发/眼睛),保持五官可见"""
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
dark = (255.0 - gray.astype(np.float32)) / 255.0
dark = np.clip(dark * intensity, 0, 1)
dark = cv2.GaussianBlur(dark, (5, 5), 0)
return (result.astype(np.float32) * (1 - np.dstack([dark] * 3))).astype(np.uint8)
# ==================== RBX 红区分析 ====================
def analyze_red_zone(img: np.ndarray, output_path: str) -> Tuple[bool, Dict, str]:
try:
# 直接全图皮肤检测
lab = cv2.cvtColor(img, cv2.COLOR_BGR2LAB)
_, a_ch, _ = cv2.split(lab)
a_float = a_ch.astype(np.float32)
mask_lab = cv2.inRange(lab, (40, 120, 105), (235, 185, 175))
ycrcb = cv2.cvtColor(img, cv2.COLOR_BGR2YCrCb)
mask_ycrcb = cv2.inRange(ycrcb, (40, 133, 77), (240, 175, 135))
skin_mask = cv2.bitwise_or(mask_lab, mask_ycrcb)
kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (11, 11))
skin_mask = cv2.morphologyEx(skin_mask, cv2.MORPH_CLOSE, kernel, iterations=3)
skin_mask = cv2.morphologyEx(skin_mask, cv2.MORPH_OPEN, kernel, iterations=1)
skin_mask = cv2.dilate(skin_mask, cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (7, 7)), iterations=1)
contours, _ = cv2.findContours(skin_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
if contours:
largest = max(contours, key=cv2.contourArea)
skin_mask = np.zeros_like(skin_mask)
cv2.drawContours(skin_mask, [largest], -1, 255, -1)
hull = cv2.convexHull(largest)
cv2.drawContours(skin_mask, [hull], -1, 255, -1)
skin_soft = cv2.GaussianBlur(skin_mask.astype(np.float32), (31, 31), 0) / 255.0
skin_pixels = a_float[skin_mask > 128]
if len(skin_pixels) > 100:
baseline_a = np.percentile(skin_pixels, 15)
else:
baseline_a = np.median(a_float)
redness_map = np.maximum(0, a_float - baseline_a) * 1.8
vmax = np.percentile(redness_map, 98)
if vmax > 0:
redness_map = np.clip(redness_map, 0, vmax)
heatmap_idx = (redness_map / vmax) * 255.0
else:
heatmap_idx = np.zeros_like(a_float)
heatmap_idx = np.clip(heatmap_idx, 0, 255).astype(np.uint8)
heatmap_idx = cv2.bilateralFilter(heatmap_idx, 9, 75, 75)
heatmap_idx = cv2.GaussianBlur(heatmap_idx, (7, 7), 0)
# 皮肤健康度统计
skin_area = np.sum(skin_mask > 128)
if skin_area > 0:
within_skin = heatmap_idx[skin_mask > 128]
normal_pct = float(np.sum(within_skin < 55) / skin_area * 100)
mild_pct = float(np.sum((within_skin >= 55) & (within_skin < 110)) / skin_area * 100)
warning_pct = float(np.sum((within_skin >= 110) & (within_skin < 170)) / skin_area * 100)
severe_pct = float(np.sum(within_skin >= 170) / skin_area * 100)
health_score = float(normal_pct * 1.0 + mild_pct * 0.7 + warning_pct * 0.4 + severe_pct * 0.1)
else:
normal_pct = mild_pct = warning_pct = severe_pct = health_score = 0.0
health_stats = {
'health_score': health_score, 'normal_pct': normal_pct,
'mild_pct': mild_pct, 'warning_pct': warning_pct, 'severe_pct': severe_pct,
}
alpha_red = heatmap_idx.astype(np.float32) / 255.0
alpha_red = np.clip(alpha_red * 1.5, 0, 1)
alpha_red = cv2.GaussianBlur(alpha_red, (5, 5), 0)
anchors = np.array([
[0, 232, 228, 242],
[3, 205, 170, 220],
[6, 150, 90, 215],
[10, 70, 30, 210],
[15, 35, 15, 225],
[20, 18, 14, 242],
[255, 2, 2, 97 ],
], dtype=np.float32)
heatmap_color = cv2.applyColorMap(heatmap_idx, build_lut(anchors))
heatmap_color = cv2.GaussianBlur(heatmap_color, (3, 3), 0)
hsv = cv2.cvtColor(heatmap_color, cv2.COLOR_BGR2HSV).astype(np.float32)
hsv[:, :, 1] = np.clip(hsv[:, :, 1] * 1.5, 0, 255)
heatmap_color = cv2.cvtColor(hsv.astype(np.uint8), cv2.COLOR_HSV2BGR)
heatmap_float = heatmap_color.astype(np.float32) / 255.0
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY).astype(np.float32)
skin_texture = cv2.GaussianBlur(gray, (5, 5), 0)
skin_texture = np.clip(215 + skin_texture * 0.12, 215, 252)
skin_texture = np.dstack([skin_texture] * 3) / 255.0
alpha_3 = np.dstack([alpha_red] * 3)
result = skin_texture * (1 - alpha_3 * 0.8) + heatmap_float * (alpha_3 * 0.8)
skin_alpha = np.dstack([skin_soft] * 3)
highpass = gray - cv2.GaussianBlur(gray, (11, 11), 0)
highpass = np.clip(highpass * 0.08, -8, 8)
highpass = cv2.GaussianBlur(highpass, (3, 3), 0)
highpass_3 = np.dstack([highpass] * 3) / 255.0
result = np.clip(result + highpass_3 * skin_alpha * 0.5, 0, 1)
dark_mask = np.clip(1.0 - gray / 55.0, 0, 1)
dark_mask = cv2.GaussianBlur(dark_mask, (11, 11), 0)
dark_3 = np.dstack([dark_mask] * 3)
white = np.ones_like(heatmap_float)
result = result * (1 - dark_3) + white * dark_3
result = (result * 255).astype(np.uint8)
if cv2_imwrite_chinese(output_path, result):
logger.debug(f"红区完成:{os.path.basename(output_path)}")
return True, health_stats, ""
return False, {}, "写入失败"
except Exception as e:
logger.error(f"红区失败:{e}")
return False, {}, str(e)
# ==================== 人脸红区检测 ====================
def analyze_face_red(img: np.ndarray, output_path: str) -> Tuple[bool, str]:
"""交叉偏振人脸皮肤分析:红度 + 色素"""
try:
# 直接全图皮肤检测
lab = cv2.cvtColor(img, cv2.COLOR_BGR2LAB)
l_ch, a_ch, b_ch = cv2.split(lab)
a_float = a_ch.astype(np.float32)
l_float = l_ch.astype(np.float32)
mask_lab = cv2.inRange(lab, (40, 120, 105), (235, 185, 175))
ycrcb = cv2.cvtColor(img, cv2.COLOR_BGR2YCrCb)
mask_ycrcb = cv2.inRange(ycrcb, (40, 133, 77), (240, 175, 135))
skin = cv2.bitwise_or(mask_lab, mask_ycrcb)
k = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (11, 11))
skin = cv2.morphologyEx(skin, cv2.MORPH_CLOSE, k, iterations=3)
contours, _ = cv2.findContours(skin, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
if contours:
skin[:] = 0
cv2.drawContours(skin, [max(contours, key=cv2.contourArea)], -1, 255, -1)
skin_soft = cv2.GaussianBlur(skin.astype(np.float32), (15, 15), 0) / 255.0
# 3. 红度分析
skin_pixels = a_float[skin > 128]
baseline = np.percentile(skin_pixels, 20) if len(skin_pixels) > 100 else np.median(a_float)
redness = np.maximum(0, a_float - baseline) * 2.0
vmax = np.percentile(redness[skin > 128], 97) if np.sum(skin > 128) > 100 else 0
red_map = np.clip(redness / vmax * 255.0, 0, 255).astype(np.uint8) if vmax > 0 else np.zeros_like(a_float, dtype=np.uint8)
red_map = cv2.bilateralFilter(red_map, 7, 50, 50)
# 4. 色素分析
l_blur = cv2.GaussianBlur(l_float, (31, 31), 0)
pigment = np.abs(l_float - l_blur)
p_max = np.percentile(pigment[skin > 128], 95) if np.sum(skin > 128) > 100 else 0
pig_map = np.clip(pigment / p_max * 255.0, 0, 255).astype(np.uint8) if p_max > 0 else np.zeros_like(l_float, dtype=np.uint8)
# 5. 可视化:原图 + 红度(红) + 色素(蓝)
img_f = img.astype(np.float32)
skin_3 = np.dstack([skin_soft] * 3)
red_alpha = np.clip(red_map.astype(np.float32) / 200.0, 0, 1)
red_alpha = cv2.GaussianBlur(red_alpha, (5, 5), 0)
red_ol = np.zeros_like(img)
red_ol[:, :, 2] = 180
red_3 = np.dstack([red_alpha] * 3)
result = img_f * (1 - red_3 * 0.5) + red_ol.astype(np.float32) * (red_3 * 0.5)
pig_alpha = np.clip(pig_map.astype(np.float32) / 200.0, 0, 1)
pig_alpha = cv2.GaussianBlur(pig_alpha, (5, 5), 0)
pig_ol = np.zeros_like(img)
pig_ol[:, :, 0] = 160
pig_3 = np.dstack([pig_alpha] * 3)
result = result * (1 - pig_3 * 0.4) + pig_ol.astype(np.float32) * (pig_3 * 0.4)
# 非皮肤压暗
dark = img_f * 0.3 + 50
result = result * skin_3 + dark * (1 - skin_3)
result = np.clip(result, 0, 255).astype(np.uint8)
if cv2_imwrite_chinese(output_path, result):
logger.debug(f"交叉偏振分析完成:{os.path.basename(output_path)}")
return True, ""
return False, "写入失败"
except Exception as e:
logger.error(f"交叉偏振分析失败:{e}")
return False, str(e)
# ==================== 热力敏感图分析 ====================
def analyze_heat_sensitivity(img: np.ndarray, output_path: str) -> Tuple[bool, str]:
try:
# 直接全图皮肤检测
skin_soft, skin_binary, a_float = detect_skin_region(img)
heatmap_idx = compute_heatmap_idx(a_float, skin_binary, 30, 1.8)
heatmap_idx = cv2.GaussianBlur(heatmap_idx, (7, 7), 0)
heatmap_idx = cv2.bilateralFilter(heatmap_idx, 7, 60, 60)
anchors = np.array([
[0, 192, 97, 16 ],
[1, 233, 241, 86 ],
[60, 8, 213, 204],
[100, 0, 250, 245],
[160, 0, 150, 255],
[245, 33, 38, 200],
[255, 4, 38, 152],
], dtype=np.float32)
heatmap_color = cv2.applyColorMap(heatmap_idx, build_lut(anchors))
heatmap_color = cv2.convertScaleAbs(heatmap_color, alpha=1.05, beta=0)
result = (heatmap_color.astype(np.float32) * 0.92 +
np.full_like(img, 255, dtype=np.float32) * 0.08)
result = np.clip(result, 0, 255).astype(np.uint8)
result = add_dark_details(result, img)
if cv2_imwrite_chinese(output_path, result):
logger.debug(f"热力敏感图完成:{os.path.basename(output_path)}")
return True, ""
return False, "写入失败"
except Exception as e:
logger.error(f"热力敏感图失败:{e}")
return False, str(e)
# ==================== UV 荧光分析 ====================
def detect_skin_uv(img: np.ndarray):
"""UV 照片皮肤检测 — 亮度阈值 + 形态学"""
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# UV 照片: 皮肤有微弱的 UV 反射/荧光, 背景几乎全黑
_, skin = cv2.threshold(gray, 18, 255, cv2.THRESH_BINARY)
kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (11, 11))
skin = cv2.morphologyEx(skin, cv2.MORPH_CLOSE, kernel, iterations=3)
skin = cv2.morphologyEx(skin, cv2.MORPH_OPEN, kernel, iterations=1)
skin = cv2.dilate(skin, cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (7, 7)), iterations=1)
contours, _ = cv2.findContours(skin, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
if contours:
largest = max(contours, key=cv2.contourArea)
skin = np.zeros_like(skin)
cv2.drawContours(skin, [largest], -1, 255, -1)
hull = cv2.convexHull(largest)
cv2.drawContours(skin, [hull], -1, 255, -1)
skin_soft = cv2.GaussianBlur(skin.astype(np.float32), (21, 21), 0) / 255.0
return skin_soft, skin
def analyze_pigment(img: np.ndarray, output_path: str) -> Tuple[bool, str]:
"""UV 荧光分析 — 黑底 + 皮肤灰度 + 荧光绿点"""
try:
skin_soft, skin_binary = detect_skin_uv(img)
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY).astype(np.float32)
clahe = cv2.createCLAHE(clipLimit=2.5, tileGridSize=(16, 16))
skin_base = clahe.apply(gray.astype(np.uint8)).astype(np.float32)
skin_base = skin_base * skin_soft
skin_base = np.power(skin_base / 255.0, 1.2) * 255.0
skin_base_3 = np.dstack([skin_base / 255.0] * 3)
result_u8 = (np.clip(skin_base_3, 0, 1) * 255).astype(np.uint8)
if cv2_imwrite_chinese(output_path, result_u8):
logger.debug(f"UV 荧光完成:{os.path.basename(output_path)}")
return True, ""
else:
return False, "写入失败"
except Exception as e:
logger.error(f"UV 荧光失败:{e}")
return False, str(e)
# ==================== 单张图片处理 ====================
def process_single_image(args: Tuple) -> Dict:
img_path, model, output_base, verbose = args
IMAGE_EXTENSIONS = {'.jpg', '.jpeg', '.png', '.bmp', '.webp'}
if img_path.suffix.lower() not in IMAGE_EXTENSIONS:
return {'status': 'skip', 'filename': img_path.name}
logger.info(f"处理:{img_path.name}")
img = cv2_imread_chinese(str(img_path))
if img is None:
return {'status': 'error', 'filename': img_path.name, 'error': '读取失败'}
img = cv2.rotate(img, cv2.ROTATE_90_COUNTERCLOCKWISE)
result = {
'status': 'success',
'filename': img_path.name,
'stem': img_path.stem,
'acne_counts': {},
'health_score': 0,
'normal_pct': 0,
'mild_pct': 0,
'warning_pct': 0,
'severe_pct': 0,
'errors': []
}
# 1. YOLO 痤疮检测
try:
yolo_out = output_base / "yolo" / f"yolo_{img_path.stem}.jpg"
success, acne_counts, error = detect_acne(img, str(img_path), model, str(yolo_out))
if success:
result['acne_counts'] = acne_counts
else:
result['errors'].append(f"YOLO: {error}")
except Exception as e:
result['errors'].append(f"YOLO: {e}")
# 3. RBX 红区分析
try:
red_out = output_base / "redzone" / f"red_{img_path.stem}.jpg"
success, health_stats, error = analyze_red_zone(img, str(red_out))
if success:
result.update(health_stats)
else:
result['errors'].append(f"红区:{error}")
except Exception as e:
result['errors'].append(f"红区:{e}")
# 4. 人脸红区检测
try:
face_red_out = output_base / "face_red" / f"face_red_{img_path.stem}.jpg"
success, error = analyze_face_red(img, str(face_red_out))
if not success:
result['errors'].append(f"人脸红区:{error}")
except Exception as e:
result['errors'].append(f"人脸红区:{e}")
# 6. 热力敏感图分析
try:
sensitivity_out = output_base / "sensitivity" / f"sensitivity_{img_path.stem}.jpg"
success, error = analyze_heat_sensitivity(img, str(sensitivity_out))
if not success:
result['errors'].append(f"热力敏感图:{error}")
except Exception as e:
result['errors'].append(f"热力敏感图:{e}")
# 7. 色素铅图分析
try:
pig_out = output_base / "pigment" / f"pigment_{img_path.stem}.jpg"
success, error = analyze_pigment(img, str(pig_out))
if not success:
result['errors'].append(f"色素:{error}")
except Exception as e:
result['errors'].append(f"色素:{e}")
if result['errors']:
result['status'] = 'partial'
logger.warning(f"{img_path.name} 部分失败:{result['errors']}")
else:
logger.info(f"{img_path.name} 完成")
return result
# ==================== 保存文本报告 ====================
def save_text_report(results: List[Dict], texts_dir: Path):
try:
# 确保texts目录存在
if not texts_dir.exists():
texts_dir.mkdir(parents=True, exist_ok=True)
text_path = texts_dir / "analysis_report.txt"
valid = [r for r in results if r.get('health_score', 0) > 0]
if valid:
avg_health = np.mean([r['health_score'] for r in valid])
avg_normal = np.mean([r['normal_pct'] for r in valid])
avg_mild = np.mean([r['mild_pct'] for r in valid])
avg_warning = np.mean([r['warning_pct'] for r in valid])
avg_severe = np.mean([r['severe_pct'] for r in valid])
else:
avg_health = avg_normal = avg_mild = avg_warning = avg_severe = 0.0
with open(text_path, 'w', encoding='utf-8') as f:
f.write(f"Health Score::{avg_health:.1f} / 100;;\n")
f.write(f"Normal area::{avg_normal:.1f}%;;\n")
f.write(f"Mild abnormality::{avg_mild:.1f}%;;\n")
f.write(f"Moderate abnormality::{avg_warning:.1f}%;;\n")
f.write(f"Severe abnormality::{avg_severe:.1f}%;;\n")
logger.info(f"文本报告已保存:{text_path}")
return True
except Exception as e:
logger.error(f"文本报告生成失败:{e}")
return False
# ==================== 回调后端接口 ====================
def callback_api(task_id: str, status: int, api_url: str = API_HOST+API_CALLBACK):
"""完成后回调后端接口"""
try:
payload = {
"task_id": task_id,
"status": status # 1=成功,0=失败
}
response = requests.post(api_url, json=payload, timeout=10)
if response.status_code == 200:
logger.info(f"回调成功:{task_id}")
return True
else:
logger.warning(f"回调失败:{response.status_code, api_url}")
return False
except Exception as e:
logger.error(f"回调异常:{e}")
return False
class SafeYOLO:
"""One initialized model; serialize prediction while OpenCV runs in parallel."""
def __init__(self, model_path):
self._lock = threading.Lock()
started = time.perf_counter()
self._model = YOLO(str(model_path))
self.names = self._model.names
# Complete predictor creation, layer fusion and device setup before threads.
self(np.zeros((640, 640, 3), dtype=np.uint8), conf=0.2, verbose=False)
logger.info("模型加载及预热完成:%.3f 秒,设备:%s",
time.perf_counter() - started, self._model.predictor.device)
def __call__(self, *args, **kwargs):
with self._lock:
return self._model(*args, **kwargs)
def analyze_directory(input_dir, task_id, model, workers=4, api_url=None):
"""Reuse a warmed model. Return structured status to the resident parent."""
started = time.perf_counter()
input_dir = Path(input_dir)
results = []
error = None
try:
if not input_dir.is_dir():
raise RuntimeError(f"输入目录不存在:{input_dir}")
image_files = sorted(p for p in input_dir.iterdir() if p.is_file()
and p.suffix.lower() in {'.jpg', '.jpeg', '.png', '.bmp', '.webp'})
if not image_files:
raise RuntimeError("输入目录没有可处理的图片")
output_base = input_dir.parent / 'output'
for subdir in ['yolo', 'redzone', 'face_red', 'sensitivity', 'pigment', 'texts']:
(output_base / subdir).mkdir(parents=True, exist_ok=True)
logger.info("任务 %s:%d 张图片,%d 个图片线程,复用已预热模型", task_id, len(image_files), workers)
with ThreadPoolExecutor(max_workers=workers) as executor:
futures = [executor.submit(process_single_image, (p, model, output_base, False))
for p in image_files]
for future in as_completed(futures):
results.append(future.result())
if not save_text_report(results, output_base / 'texts'):
raise RuntimeError("文本报告保存失败")
failed = [r for r in results if r['status'] != 'success']
if failed:
error = "; ".join(f"{r['filename']}: {r.get('errors') or r.get('error') or r['status']}"
for r in failed)
except Exception as exc:
logger.exception("分析任务失败:%s", task_id)
error = str(exc) or type(exc).__name__
analysis_seconds = time.perf_counter() - started
logger.info("任务 %s 分析结束:%.3f 秒,成功 %d,未完整成功 %d", task_id, analysis_seconds,
sum(r['status'] == 'success' for r in results),
sum(r['status'] != 'success' for r in results))
callback_ok = None
if api_url:
callback_ok = callback_api(task_id, 0 if error else 1, api_url)
return {'task_id': task_id, 'error': error, 'images': results,
'analysis_seconds': analysis_seconds,
'total_seconds': time.perf_counter() - started, 'callback_ok': callback_ok}
def main():
parser = argparse.ArgumentParser(description='皮肤分析工具')
parser.add_argument('input_dir')
parser.add_argument('--task-id')
parser.add_argument('--model', '-m', default='best.pt')
parser.add_argument('--workers', '-w', type=int, default=4)
parser.add_argument('--verbose', '-v', action='store_true')
parser.add_argument('--api-url', default=API_HOST + API_CALLBACK)
args = parser.parse_args()
setup_logging(args.verbose)
if args.workers < 1:
parser.error('--workers 必须大于 0')
try:
model = SafeYOLO(args.model)
except Exception:
logger.exception("模型加载或预热失败")
callback_api(args.task_id, 0, args.api_url)
return 1
result = analyze_directory(args.input_dir, args.task_id, model, args.workers, args.api_url)
return 1 if result['error'] else 0
if __name__ == "__main__":
raise SystemExit(main())