From 11c5e062c2035fd8e580f7163b8163bc61662d2f Mon Sep 17 00:00:00 2001 From: SkyLi Date: Fri, 27 Mar 2026 00:47:33 +0800 Subject: [PATCH] feat: add ROI temperature feature histogram analysis script Analyzes validation set images using YOLO detection, extracts ROI temperature features, and plots histograms. --- scripts/analyze_temp_features.py | 317 +++++++++++++++++++++++++++++++ 1 file changed, 317 insertions(+) create mode 100644 scripts/analyze_temp_features.py diff --git a/scripts/analyze_temp_features.py b/scripts/analyze_temp_features.py new file mode 100644 index 0000000..712db90 --- /dev/null +++ b/scripts/analyze_temp_features.py @@ -0,0 +1,317 @@ +#!/usr/bin/env python3 +""" +验证集目标检测与ROI温度特征直方图分析 +对验证集图像进行YOLO检测,提取ROI温度特征,绘制直方图 +""" + +import sys +from pathlib import Path + +# 添加项目根目录到路径 +sys.path.insert(0, str(Path(__file__).parent.parent)) + +import cv2 +import numpy as np +import matplotlib.pyplot as plt +from tqdm import tqdm +from collections import defaultdict + +from src.detect import InfraredDetector +from src.extract_temp import TempFeatureExtractor, load_infrared_image + + +def analyze_validation_set( + image_dir: str, + model_path: str, + conf: float = 0.25, + output_dir: str = None +): + """ + 分析验证集,提取ROI温度特征并绘制直方图 + + Args: + image_dir: 验证集图像目录 + model_path: YOLO模型路径 + conf: 检测置信度阈值 + output_dir: 输出目录 + """ + image_dir = Path(image_dir) + if output_dir: + output_dir = Path(output_dir) + output_dir.mkdir(parents=True, exist_ok=True) + else: + output_dir = Path("output/temp_analysis") + output_dir.mkdir(parents=True, exist_ok=True) + + # 初始化检测器和温度特征提取器 + detector = InfraredDetector(model_path, conf=conf) + extractor = TempFeatureExtractor(model_path) + + # 获取所有图像 + image_paths = sorted([ + p for p in image_dir.glob('*') + if p.suffix.lower() in ['.jpg', '.jpeg', '.png', '.bmp', '.tiff'] + ]) + + print(f"找到 {len(image_paths)} 张图像") + + # 收集所有温度特征 + all_features = defaultdict(list) + class_features = defaultdict(lambda: defaultdict(list)) + + for img_path in tqdm(image_paths, desc="处理图像"): + try: + # 加载灰度图像(用于温度提取) + gray_img = load_infrared_image(str(img_path)) + + # 进行检测 + detections = detector.detect(str(img_path), conf=conf) + + for det in detections: + bbox = det['bbox'] + x1, y1, x2, y2 = map(int, bbox) + + # 确保边界在图像范围内 + x1, y1 = max(0, x1), max(0, y1) + x2, y2 = min(gray_img.shape[1], x2), min(gray_img.shape[0], y2) + + if x2 - x1 <= 0 or y2 - y1 <= 0: + continue + + roi = gray_img[y1:y2, x1:x2] + if roi.size == 0: + continue + + # 提取温度特征 + features = extractor.extract_from_roi(roi) + + # 收集全局特征 + all_features['temp_max'].append(features['temp_max']) + all_features['temp_min'].append(features['temp_min']) + all_features['temp_mean'].append(features['temp_mean']) + all_features['temp_std'].append(features['temp_std']) + all_features['temp_median'].append(features['temp_median']) + all_features['gradient_mean'].append(features['gradient_mean']) + all_features['gradient_std'].append(features['gradient_std']) + + # 计算方差(标准差的平方) + variance = features['temp_std'] ** 2 + all_features['temp_variance'].append(variance) + + # 按类别收集特征 + class_name = det['class_name'] + class_features[class_name]['temp_max'].append(features['temp_max']) + class_features[class_name]['temp_mean'].append(features['temp_mean']) + class_features[class_name]['temp_std'].append(features['temp_std']) + class_features[class_name]['temp_variance'].append(variance) + + except Exception as e: + print(f"处理 {img_path.name} 时出错: {e}") + continue + + # 打印统计信息 + print(f"\n总共提取了 {len(all_features['temp_max'])} 个ROI的温度特征") + + # 绘制全局直方图 + draw_global_histograms(all_features, output_dir) + + # 绘制各类别的温度STD直方图 + draw_class_histograms(class_features, output_dir) + + # 绘制温度散点图 + draw_temp_scatter(all_features, output_dir) + + # 保存特征统计到CSV + save_stats_csv(all_features, class_features, output_dir) + + print(f"\n分析完成! 结果保存至: {output_dir}") + + +def draw_global_histograms(features: dict, output_dir: Path): + """绘制全局温度特征直方图""" + fig, axes = plt.subplots(2, 4, figsize=(16, 8)) + fig.suptitle('ROI Temperature Features Distribution (All Classes)', fontsize=14) + + plot_configs = [ + ('temp_max', 'Max Temperature', 'red'), + ('temp_min', 'Min Temperature', 'blue'), + ('temp_mean', 'Mean Temperature', 'green'), + ('temp_std', 'Temperature Std Dev', 'orange'), + ('temp_median', 'Median Temperature', 'purple'), + ('temp_variance', 'Temperature Variance', 'brown'), + ('gradient_mean', 'Gradient Mean', 'pink'), + ('gradient_std', 'Gradient Std', 'gray'), + ] + + for idx, (key, title, color) in enumerate(plot_configs): + ax = axes[idx // 4, idx % 4] + data = features.get(key, []) + + if data: + ax.hist(data, bins=30, color=color, alpha=0.7, edgecolor='black') + ax.axvline(np.mean(data), color='red', linestyle='--', + linewidth=2, label=f'Mean: {np.mean(data):.2f}') + ax.axvline(np.median(data), color='blue', linestyle=':', + linewidth=2, label=f'Median: {np.median(data):.2f}') + + ax.set_title(title) + ax.set_xlabel('Value') + ax.set_ylabel('Count') + ax.legend(fontsize=8) + + plt.tight_layout() + plt.savefig(output_dir / 'global_temp_histograms.png', dpi=150) + plt.close() + print(f" 保存: global_temp_histograms.png") + + +def draw_class_histograms(class_features: dict, output_dir: Path): + """绘制各类别的温度标准差直方图""" + if not class_features: + return + + n_classes = len(class_features) + n_cols = 3 + n_rows = (n_classes + n_cols - 1) // n_cols + + fig, axes = plt.subplots(n_rows, n_cols, figsize=(15, 4 * n_rows)) + fig.suptitle('Temperature Std Dev Distribution by Class', fontsize=14) + axes = axes.flatten() if n_classes > 1 else [axes] + + for idx, (class_name, features) in enumerate(sorted(class_features.items())): + ax = axes[idx] + std_data = features.get('temp_std', []) + + if std_data: + ax.hist(std_data, bins=20, color='steelblue', alpha=0.7, edgecolor='black') + ax.axvline(np.mean(std_data), color='red', linestyle='--', + linewidth=2, label=f'Mean: {np.mean(std_data):.2f}') + + ax.set_title(f'{class_name} (n={len(std_data)})') + ax.set_xlabel('Temperature Std Dev') + ax.set_ylabel('Count') + ax.legend(fontsize=8) + + # 隐藏多余的子图 + for idx in range(n_classes, len(axes)): + axes[idx].axis('off') + + plt.tight_layout() + plt.savefig(output_dir / 'class_temp_std_histograms.png', dpi=150) + plt.close() + print(f" 保存: class_temp_std_histograms.png") + + +def draw_temp_scatter(features: dict, output_dir: Path): + """绘制温度特征散点图""" + fig, axes = plt.subplots(1, 2, figsize=(12, 5)) + + # temp_max vs temp_std + ax = axes[0] + ax.scatter(features['temp_max'], features['temp_std'], + alpha=0.3, s=10, c='red') + ax.set_xlabel('Max Temperature') + ax.set_ylabel('Temperature Std Dev') + ax.set_title('Max Temperature vs Std Dev') + ax.grid(True, alpha=0.3) + + # temp_mean vs temp_std + ax = axes[1] + ax.scatter(features['temp_mean'], features['temp_std'], + alpha=0.3, s=10, c='blue') + ax.set_xlabel('Mean Temperature') + ax.set_ylabel('Temperature Std Dev') + ax.set_title('Mean Temperature vs Std Dev') + ax.grid(True, alpha=0.3) + + plt.tight_layout() + plt.savefig(output_dir / 'temp_scatter_plots.png', dpi=150) + plt.close() + print(f" 保存: temp_scatter_plots.png") + + +def save_stats_csv(all_features: dict, class_features: dict, output_dir: Path): + """保存特征统计到CSV""" + import csv + + # 全局统计 + stats_file = output_dir / 'temp_feature_stats.csv' + with open(stats_file, 'w', newline='', encoding='utf-8') as f: + writer = csv.writer(f) + writer.writerow(['Feature', 'Count', 'Mean', 'Std', 'Min', 'Max', 'Median']) + + for key in ['temp_max', 'temp_min', 'temp_mean', 'temp_std', + 'temp_median', 'temp_variance', 'gradient_mean', 'gradient_std']: + data = all_features.get(key, []) + if data: + writer.writerow([ + key, + len(data), + f'{np.mean(data):.2f}', + f'{np.std(data):.2f}', + f'{np.min(data):.2f}', + f'{np.max(data):.2f}', + f'{np.median(data):.2f}' + ]) + + print(f" 保存: temp_feature_stats.csv") + + # 各类别统计 + class_stats_file = output_dir / 'class_temp_stats.csv' + with open(class_stats_file, 'w', newline='', encoding='utf-8') as f: + writer = csv.writer(f) + writer.writerow(['Class', 'Count', 'Mean_Max', 'Mean_Mean', 'Mean_Std', 'Mean_Variance']) + + for class_name in sorted(class_features.keys()): + features = class_features[class_name] + writer.writerow([ + class_name, + len(features['temp_std']), + f'{np.mean(features["temp_max"]):.2f}', + f'{np.mean(features["temp_mean"]):.2f}', + f'{np.mean(features["temp_std"]):.2f}', + f'{np.mean(features["temp_variance"]):.2f}' + ]) + + print(f" 保存: class_temp_stats.csv") + + +if __name__ == '__main__': + import argparse + + parser = argparse.ArgumentParser( + description='验证集ROI温度特征提取与直方图分析' + ) + parser.add_argument( + '--image-dir', + type=str, + default='data/valid/images', + help='验证集图像目录' + ) + parser.add_argument( + '--model', + type=str, + default='weights/best.pt', + help='YOLO模型路径' + ) + parser.add_argument( + '--conf', + type=float, + default=0.25, + help='检测置信度阈值' + ) + parser.add_argument( + '--output', + type=str, + default='output/temp_analysis', + help='输出目录' + ) + + args = parser.parse_args() + + analyze_validation_set( + image_dir=args.image_dir, + model_path=args.model, + conf=args.conf, + output_dir=args.output + ) \ No newline at end of file