| """
|
| 浒苔预测脚本 - 256x256图像尺寸
|
| 使用训练好的模型进行浒苔分割预测 - 简化版本
|
| """
|
| import os
|
| import torch
|
| import numpy as np
|
| from pathlib import Path
|
| import json
|
| import time
|
| from datetime import datetime
|
| import cv2
|
| from tqdm import tqdm
|
|
|
|
|
| from dinov3_deeplabv3plus import DinoV3DeepLabV3Plus
|
|
|
| def load_model(model_path, config, device):
|
| """加载训练好的模型"""
|
| print(f"正在加载模型: {model_path}")
|
|
|
|
|
| model = DinoV3DeepLabV3Plus(
|
| num_classes=config['num_classes'],
|
| backbone_name=config['backbone_name'],
|
| pretrained=False,
|
| weights=config['backbone_weights'],
|
| use_4channel=config['use_4channel']
|
| ).to(device)
|
|
|
|
|
| checkpoint = torch.load(model_path, map_location=device)
|
|
|
|
|
| if 'model_state_dict' in checkpoint:
|
| model.load_state_dict(checkpoint['model_state_dict'])
|
| print(f"加载模型权重 (epoch {checkpoint.get('epoch', 'unknown')})")
|
| elif 'state_dict' in checkpoint:
|
| model.load_state_dict(checkpoint['state_dict'])
|
| print("加载模型权重")
|
| else:
|
|
|
| model.load_state_dict(checkpoint)
|
| print("加载模型权重")
|
|
|
| model.eval()
|
| print("模型加载完成,进入评估模式")
|
| return model
|
|
|
| def process_image_for_model(image_path, target_size=256, use_4channel=True):
|
| """处理图像用于模型预测 - 使用与训练相同的方式"""
|
|
|
| img_16bit = cv2.imread(image_path, cv2.IMREAD_UNCHANGED)
|
|
|
| if img_16bit is None:
|
| raise ValueError(f"无法读取图像文件: {image_path}")
|
|
|
|
|
| try:
|
| import rasterio
|
| with rasterio.open(image_path) as src:
|
|
|
| image = src.read()
|
|
|
| image = np.transpose(image, (1, 2, 0))
|
| except Exception as e:
|
| print(f"rasterio读取失败,使用OpenCV: {e}")
|
|
|
| image = img_16bit
|
|
|
|
|
| if use_4channel and image.shape[2] >= 4:
|
|
|
| processed_image = image[:, :, :4]
|
| else:
|
|
|
| if image.shape[2] >= 4:
|
|
|
| processed_image = image[:, :, [3, 2, 1]]
|
| else:
|
|
|
| processed_image = image[:, :, :3]
|
| if image.shape[2] < 3:
|
|
|
| while processed_image.shape[2] < 3:
|
| processed_image = np.concatenate([processed_image, processed_image[:, :, -1:]], axis=2)
|
|
|
|
|
| if processed_image.shape[0] != target_size or processed_image.shape[1] != target_size:
|
| processed_image = cv2.resize(processed_image, (target_size, target_size), interpolation=cv2.INTER_LINEAR)
|
|
|
|
|
| processed_image = processed_image.astype(np.float32)
|
| if processed_image.max() > 1.0:
|
| processed_image = processed_image / 65535.0
|
|
|
|
|
| if use_4channel:
|
|
|
| mean = np.array([0.430, 0.411, 0.296, 0.350])
|
| std = np.array([0.213, 0.156, 0.143, 0.180])
|
| else:
|
|
|
| mean = np.array([0.430, 0.411, 0.296])
|
| std = np.array([0.213, 0.156, 0.143])
|
|
|
|
|
| for i in range(processed_image.shape[2]):
|
| processed_image[:, :, i] = (processed_image[:, :, i] - mean[i]) / std[i]
|
|
|
|
|
| img_tensor = torch.from_numpy(processed_image).permute(2, 0, 1).float()
|
|
|
| return img_tensor.unsqueeze(0)
|
|
|
| def predict_image(model, image_path, device, config, threshold=0.5):
|
| """
|
| 预测单张图像
|
|
|
| Args:
|
| model: 训练好的模型
|
| image_path: 图像路径
|
| device: 设备
|
| config: 配置字典
|
| threshold: 浒苔预测阈值,只有当浒苔概率超过此阈值时才预测为浒苔(默认0.5)
|
| """
|
|
|
| image_tensor = process_image_for_model(
|
| image_path,
|
| target_size=config['image_size'],
|
| use_4channel=config['use_4channel']
|
| ).to(device)
|
|
|
|
|
| with torch.no_grad():
|
| start_time = time.time()
|
| output = model(image_tensor)
|
| inference_time = time.time() - start_time
|
|
|
|
|
| if isinstance(output, dict):
|
| output = output['out']
|
|
|
|
|
| pred = torch.softmax(output, dim=1)
|
|
|
|
|
| seaweed_prob = pred[0, 1].squeeze(0).cpu().numpy()
|
|
|
|
|
|
|
| pred_class = (seaweed_prob > threshold).astype(np.uint8)
|
|
|
| return {
|
| 'prediction': pred_class,
|
| 'seaweed_probability': seaweed_prob,
|
| 'inference_time': inference_time
|
| }
|
|
|
| def save_prediction_results(prediction, seaweed_prob, output_path, base_name):
|
| """保存预测结果到文件"""
|
|
|
| pred_mask = prediction.astype(np.uint8)
|
| np.save(str(output_path / f"{base_name}_prediction.npy"), pred_mask)
|
|
|
|
|
| np.save(str(output_path / f"{base_name}_probability.npy"), seaweed_prob)
|
|
|
|
|
| pred_png = (prediction * 255).astype(np.uint8)
|
| prob_png = (seaweed_prob * 255).astype(np.uint8)
|
|
|
| cv2.imwrite(str(output_path / f"{base_name}_prediction.png"), pred_png)
|
| cv2.imwrite(str(output_path / f"{base_name}_probability.png"), prob_png)
|
|
|
| def calculate_metrics(prediction, target, num_classes=2):
|
| """计算评估指标 - 精确率、召回率、IoU、F1"""
|
|
|
| if isinstance(prediction, torch.Tensor):
|
| prediction = prediction.cpu().numpy()
|
| if isinstance(target, torch.Tensor):
|
| target = target.cpu().numpy()
|
|
|
|
|
| pred_flat = prediction.flatten()
|
| target_flat = target.flatten()
|
|
|
|
|
| confusion_matrix = np.zeros((num_classes, num_classes))
|
| for i in range(num_classes):
|
| for j in range(num_classes):
|
| confusion_matrix[i, j] = np.sum((pred_flat == i) & (target_flat == j))
|
|
|
|
|
| metrics_per_class = []
|
| for i in range(num_classes):
|
| tp = confusion_matrix[i, i]
|
| fp = confusion_matrix[i, :].sum() - tp
|
| fn = confusion_matrix[:, i].sum() - tp
|
| tn = confusion_matrix.sum() - tp - fp - fn
|
|
|
|
|
| precision = tp / (tp + fp) if (tp + fp) > 0 else 0.0
|
| recall = tp / (tp + fn) if (tp + fn) > 0 else 0.0
|
| iou = tp / (tp + fp + fn) if (tp + fp + fn) > 0 else 0.0
|
| f1 = 2 * (precision * recall) / (precision + recall) if (precision + recall) > 0 else 0.0
|
|
|
| metrics_per_class.append({
|
| 'precision': precision,
|
| 'recall': recall,
|
| 'iou': iou,
|
| 'f1': f1,
|
| 'tp': int(tp),
|
| 'fp': int(fp),
|
| 'fn': int(fn),
|
| 'tn': int(tn)
|
| })
|
|
|
|
|
| foreground_metrics = metrics_per_class[1] if len(metrics_per_class) > 1 else metrics_per_class[0]
|
|
|
|
|
| accuracy = np.sum(pred_flat == target_flat) / len(pred_flat)
|
|
|
| return {
|
| 'accuracy': float(accuracy),
|
| 'foreground_precision': foreground_metrics['precision'],
|
| 'foreground_recall': foreground_metrics['recall'],
|
| 'foreground_iou': foreground_metrics['iou'],
|
| 'foreground_f1': foreground_metrics['f1'],
|
| 'foreground_tp': foreground_metrics['tp'],
|
| 'foreground_fp': foreground_metrics['fp'],
|
| 'foreground_fn': foreground_metrics['fn'],
|
| 'foreground_tn': foreground_metrics['tn'],
|
| 'metrics_per_class': metrics_per_class
|
| }
|
|
|
| def calculate_statistics(prediction, seaweed_prob):
|
| """计算统计信息"""
|
| total_pixels = prediction.size
|
| seaweed_pixels = np.sum(prediction == 1)
|
| seaweed_ratio = seaweed_pixels / total_pixels
|
|
|
|
|
| avg_probability = np.mean(seaweed_prob)
|
| max_probability = np.max(seaweed_prob)
|
|
|
| return {
|
| 'total_pixels': int(total_pixels),
|
| 'seaweed_pixels': int(seaweed_pixels),
|
| 'seaweed_ratio': float(seaweed_ratio),
|
| 'avg_probability': float(avg_probability),
|
| 'max_probability': float(max_probability)
|
| }
|
|
|
| def load_label_mask(mask_path, target_size=256):
|
| """加载标签掩码"""
|
| try:
|
|
|
| mask = cv2.imread(mask_path, cv2.IMREAD_UNCHANGED)
|
| if mask is None:
|
|
|
| import rasterio
|
| with rasterio.open(mask_path) as src:
|
| mask = src.read(1)
|
| else:
|
|
|
| if len(mask.shape) > 2:
|
| mask = mask[:, :, 0]
|
|
|
|
|
| if mask.shape[0] != target_size or mask.shape[1] != target_size:
|
| mask = cv2.resize(mask, (target_size, target_size), interpolation=cv2.INTER_NEAREST)
|
|
|
|
|
| mask = (mask > 0).astype(np.uint8)
|
|
|
| return mask
|
|
|
| except Exception as e:
|
| print(f"加载标签失败 {mask_path}: {str(e)}")
|
| return None
|
|
|
| def main():
|
| """主预测函数"""
|
| print("=" * 60)
|
| print("浒苔预测系统 - 256x256图像 (带评估指标)")
|
| print("=" * 60)
|
|
|
|
|
| model_path = "outputs/seaweed_segmentation_improved_epoch500/best_checkpoint.pth"
|
| config_path = "outputs/seaweed_segmentation_improved_epoch500/config.json"
|
| test_image_dir = "data/test/images"
|
| test_mask_dir = "data/test/masks"
|
| output_dir = "outputs/predictions_256x256_results"
|
|
|
|
|
|
|
| prediction_threshold = 0.5
|
|
|
|
|
| device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
|
| print(f"使用设备: {device}")
|
|
|
|
|
| with open(config_path, 'r', encoding='utf-8') as f:
|
| config = json.load(f)
|
|
|
| print(f"模型配置:")
|
| print(f" - 输入通道: {4 if config['use_4channel'] else 3}")
|
| print(f" - 图像尺寸: {config['image_size']}x{config['image_size']}")
|
| print(f" - 类别数: {config['num_classes']}")
|
| print(f" - 预测阈值: {prediction_threshold}")
|
|
|
|
|
| model = load_model(model_path, config, device)
|
|
|
|
|
| output_path = Path(output_dir)
|
| output_path.mkdir(parents=True, exist_ok=True)
|
|
|
|
|
| test_images = [f for f in os.listdir(test_image_dir) if f.endswith('.TIF')]
|
| print(f"\n找到 {len(test_images)} 张测试图像")
|
|
|
|
|
| has_labels = os.path.exists(test_mask_dir)
|
| if has_labels:
|
| print(f"找到标签目录,将进行评估指标计算")
|
| else:
|
| print(f"未找到标签目录,仅进行预测")
|
|
|
|
|
| results = []
|
| evaluation_results = []
|
| total_inference_time = 0
|
|
|
| print("\n开始预测...")
|
|
|
|
|
| for i, image_file in enumerate(tqdm(test_images, desc="预测进度"), 1):
|
| image_path = os.path.join(test_image_dir, image_file)
|
|
|
| try:
|
|
|
| result = predict_image(model, image_path, device, config, threshold=prediction_threshold)
|
|
|
|
|
| stats = calculate_statistics(result['prediction'], result['seaweed_probability'])
|
|
|
|
|
| if has_labels:
|
|
|
| mask_file = image_file.replace('.TIF', '.png')
|
| mask_path = os.path.join(test_mask_dir, mask_file)
|
|
|
| if os.path.exists(mask_path):
|
|
|
| true_mask = load_label_mask(mask_path, config['image_size'])
|
|
|
| if true_mask is not None:
|
|
|
| metrics = calculate_metrics(result['prediction'], true_mask)
|
|
|
|
|
| eval_result = {
|
| 'image_file': image_file,
|
| 'accuracy': metrics['accuracy'],
|
| 'precision': metrics['foreground_precision'],
|
| 'recall': metrics['foreground_recall'],
|
| 'iou': metrics['foreground_iou'],
|
| 'f1': metrics['foreground_f1'],
|
| 'tp': metrics['foreground_tp'],
|
| 'fp': metrics['foreground_fp'],
|
| 'fn': metrics['foreground_fn'],
|
| 'tn': metrics['foreground_tn']
|
| }
|
| evaluation_results.append(eval_result)
|
|
|
|
|
| stats.update({
|
| 'accuracy': metrics['accuracy'],
|
| 'precision': metrics['foreground_precision'],
|
| 'recall': metrics['foreground_recall'],
|
| 'iou': metrics['foreground_iou'],
|
| 'f1': metrics['foreground_f1']
|
| })
|
|
|
|
|
| base_name = Path(image_file).stem
|
| save_prediction_results(result['prediction'], result['seaweed_probability'], output_path, base_name)
|
|
|
|
|
| result_info = {
|
| 'image_file': image_file,
|
| 'inference_time': float(result['inference_time']),
|
| 'statistics': stats
|
| }
|
| results.append(result_info)
|
|
|
| total_inference_time += result['inference_time']
|
|
|
| except Exception as e:
|
| print(f" - 预测失败: {str(e)}")
|
| continue
|
|
|
|
|
| results_file = output_path / "prediction_results.json"
|
| with open(results_file, 'w', encoding='utf-8') as f:
|
| json.dump(results, f, indent=2, ensure_ascii=False, default=str)
|
|
|
|
|
| if evaluation_results:
|
| eval_file = output_path / "evaluation_results.json"
|
| with open(eval_file, 'w', encoding='utf-8') as f:
|
| json.dump(evaluation_results, f, indent=2, ensure_ascii=False, default=str)
|
|
|
|
|
| if results:
|
| avg_inference_time = total_inference_time / len(results)
|
|
|
|
|
| all_ratios = [r['statistics']['seaweed_ratio'] for r in results]
|
| avg_seaweed_ratio = np.mean(all_ratios)
|
|
|
| print("\n" + "=" * 60)
|
| print("预测完成总结")
|
| print("=" * 60)
|
| print(f"总图像数: {len(results)}")
|
| print(f"平均推理时间: {avg_inference_time:.3f}s")
|
| print(f"平均浒苔比例: {avg_seaweed_ratio:.2%}")
|
|
|
|
|
| if evaluation_results:
|
| avg_accuracy = np.mean([r['accuracy'] for r in evaluation_results])
|
| avg_precision = np.mean([r['precision'] for r in evaluation_results])
|
| avg_recall = np.mean([r['recall'] for r in evaluation_results])
|
| avg_iou = np.mean([r['iou'] for r in evaluation_results])
|
| avg_f1 = np.mean([r['f1'] for r in evaluation_results])
|
|
|
| print(f"\n评估指标:")
|
| print(f" - 准确率 (Accuracy): {avg_accuracy:.4f}")
|
| print(f" - 精确率 (Precision): {avg_precision:.4f}")
|
| print(f" - 召回率 (Recall): {avg_recall:.4f}")
|
| print(f" - IoU: {avg_iou:.4f}")
|
| print(f" - F1分数: {avg_f1:.4f}")
|
|
|
|
|
| total_tp = sum([r['tp'] for r in evaluation_results])
|
| total_fp = sum([r['fp'] for r in evaluation_results])
|
| total_fn = sum([r['fn'] for r in evaluation_results])
|
| total_tn = sum([r['tn'] for r in evaluation_results])
|
|
|
| print(f"\n总体混淆矩阵:")
|
| print(f" - 真正例 (TP): {total_tp:,}")
|
| print(f" - 假正例 (FP): {total_fp:,}")
|
| print(f" - 假负例 (FN): {total_fn:,}")
|
| print(f" - 真负例 (TN): {total_tn:,}")
|
|
|
| print(f"\n结果保存目录: {output_path}")
|
| print(f"详细结果文件: {results_file}")
|
|
|
|
|
| summary = {
|
| 'timestamp': datetime.now().isoformat(),
|
| 'total_images': len(results),
|
| 'avg_inference_time': avg_inference_time,
|
| 'avg_seaweed_ratio': float(avg_seaweed_ratio),
|
| 'results': results
|
| }
|
|
|
|
|
| if evaluation_results:
|
| summary.update({
|
| 'avg_accuracy': float(avg_accuracy),
|
| 'avg_precision': float(avg_precision),
|
| 'avg_recall': float(avg_recall),
|
| 'avg_iou': float(avg_iou),
|
| 'avg_f1': float(avg_f1),
|
| 'total_confusion_matrix': {
|
| 'tp': total_tp,
|
| 'fp': total_fp,
|
| 'fn': total_fn,
|
| 'tn': total_tn
|
| },
|
| 'evaluation_results': evaluation_results
|
| })
|
|
|
| summary_file = output_path / "prediction_summary.json"
|
| with open(summary_file, 'w', encoding='utf-8') as f:
|
| json.dump(summary, f, indent=2, ensure_ascii=False, default=str)
|
|
|
| print(f"总结报告: {summary_file}")
|
|
|
|
|
| ratios = [r['statistics']['seaweed_ratio'] for r in results]
|
| print(f"\n浒苔分布统计:")
|
| print(f" - 最小浒苔比例: {min(ratios):.2%}")
|
| print(f" - 最大浒苔比例: {max(ratios):.2%}")
|
| print(f" - 浒苔比例标准差: {np.std(ratios):.2%}")
|
|
|
|
|
| print(f"\n保存的文件类型:")
|
| print(f" - .npy文件: NumPy数组格式的预测结果")
|
| print(f" - .png文件: 可视化的图像格式")
|
| print(f" - .json文件: 预测结果统计信息和评估指标")
|
|
|
| print("\n🎉 浒苔预测和评估完成!")
|
| print(f"预测结果保存在: {output_dir}")
|
|
|
| if __name__ == "__main__":
|
| main()
|
|
|