Files
pifu/wanzheng.py
2026-09-10 10:32:43 +08:00

713 lines
28 KiB
Python
Raw Permalink 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
from concurrent.futures import ThreadPoolExecutor, as_completed
from tqdm import tqdm
import logging
import threading
from typing import Dict, List, Optional, Tuple
import requests
from config.settings import API_HOST, API_CALLBACK
# YOLO 模型非线程安全(fuse/BN 会竞态报 AttributeError: bn),推理需串行
_yolo_infer_lock = threading.Lock()
# ==================== 日志配置 ====================
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:
# 共享模型下串行推理,避免多线程 fuse/BN 竞态
with _yolo_infer_lock:
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
# ==================== 主函数 ====================
def main():
global logger
parser = argparse.ArgumentParser(description='皮肤分析工具 - 后端 API 版')
parser.add_argument('--task-id', type=str,
help='任务 ID')
parser.add_argument('input_dir', type=str,
help='输入目录完整路径(后端传入的 folder_name)')
parser.add_argument('--model', '-m', type=str, default='best.pt',
help='YOLO 痤疮模型路径 (默认:cuochuang.pt)')
parser.add_argument('--workers', '-w', type=int, default=3,
help='并发工作线程数 (默认:3)')
parser.add_argument('--verbose', '-v', action='store_true',
help='详细输出模式')
parser.add_argument('--api-url', type=str, default=API_HOST+API_CALLBACK,
help='回调 API 地址')
args = parser.parse_args()
# 初始化日志
logger = setup_logging(args.verbose)
# 输入输出路径
input_dir = Path(args.input_dir)
# task_id = input_dir.name # 从文件夹名称自动提取 task_id // 修改为不从目录获取任务ID
task_id = args.task_id
output_base = input_dir.parent / 'output'
logger.info("=" * 60)
logger.info("皮肤分析工具 - 后端 API 版")
logger.info("=" * 60)
logger.info(f"任务 ID: {task_id}")
logger.info(f"输入目录:{input_dir}")
logger.info(f"输出目录:{output_base}")
logger.info(f"模型路径:{args.model}")
logger.info(f"并发线程:{args.workers}")
logger.info(f"回调地址:{args.api_url}")
model_path = args.model
if not input_dir.exists():
logger.error(f"输入目录不存在:{input_dir}")
callback_api(task_id, status=0, api_url=args.api_url)
return
# 创建输出目录
texts_dir = output_base / 'texts'
try:
# 首先创建输出目录
output_base.mkdir(exist_ok=True, parents=True)
logger.info(f"输出目录创建成功:{output_base}")
# 然后创建子目录
for subdir in ['yolo', 'redzone', 'face_red', 'sensitivity', 'pigment', 'texts']:
subdir_path = output_base / subdir
subdir_path.mkdir(exist_ok=True)
logger.info(f"子目录创建成功:{subdir_path}")
except Exception as e:
logger.error(f"目录创建失败:{e}")
callback_api(task_id, status=0, api_url=args.api_url)
return
# 加载痤疮模型
logger.info("加载 YOLO 痤疮模型...")
try:
model = YOLO(model_path)
# 主线程预 fuse,避免首批多线程推理时争抢 BN
try:
if hasattr(model, "fuse"):
model.fuse()
elif hasattr(model, "model") and hasattr(model.model, "fuse"):
model.model.fuse()
except Exception as fuse_err:
logger.warning(f"YOLO 预 fuse 跳过:{fuse_err}")
logger.info("痤疮模型加载成功")
except Exception as e:
logger.error(f"痤疮模型加载失败:{e}")
callback_api(task_id, status=0, api_url=args.api_url)
return
# 获取图片列表
try:
image_files = list(input_dir.iterdir())
total_images = len(image_files)
logger.info(f"发现 {total_images} 个文件")
except Exception as e:
logger.error(f"获取图片列表失败:{e}")
callback_api(task_id, status=0, api_url=args.api_url)
return
# 并发处理
results = []
tasks = [(img_path, model, output_base, args.verbose)
for img_path in image_files]
logger.info("开始并发处理...")
try:
with ThreadPoolExecutor(max_workers=args.workers) as executor:
futures = {executor.submit(process_single_image, task): task
for task in tasks}
for future in tqdm(as_completed(futures), total=len(futures),
desc="处理进度", unit="图"):
result = future.result()
results.append(result)
except Exception as e:
logger.error(f"并发处理失败:{e}")
callback_api(task_id, status=0, api_url=args.api_url)
return
# 保存文本报告
logger.info("\n保存文本报告中...")
logger.info(f"文本目录:{texts_dir}")
try:
save_text_report(results, texts_dir)
logger.info("文本报告保存成功")
except Exception as e:
logger.error(f"保存数据失败:{e}")
callback_api(task_id, status=0, api_url=args.api_url)
return
# 统计总结
success = sum(1 for r in results if r['status'] == 'success')
partial = sum(1 for r in results if r['status'] == 'partial')
errors = sum(1 for r in results if r['status'] == 'error')
logger.info("\n" + "=" * 60)
logger.info("全部完成!")
logger.info(f"成功:{success} | 部分:{partial} | 失败:{errors}")
logger.info(f"输出目录:{output_base}")
logger.info(f"文本报告:{texts_dir / 'analysis_report.txt'}")
logger.info("=" * 60)
# 回调后端接口(状态 1=成功)
status = 1 if errors == 0 or (success + partial) > 0 else 0
callback_api(task_id, status=status, api_url=args.api_url)
if __name__ == "__main__":
main()