Browse Source

refactor: 优化代码结构并移除调试输出

docs: 更新README文档,添加详细技术说明

chore: 更新.gitignore和requirements.txt配置

style: 清理代码中的调试信息和注释

feat: 改进模型推理和后处理逻辑
Hannnk 2 weeks ago
parent
commit
1a4d68d145

+ 1 - 0
.gitignore

@@ -33,6 +33,7 @@ env/
 
 # Project specific
 Output/
+models/
 Data
 *.onnx
 *.pkl

+ 269 - 2
README.md

@@ -2,15 +2,40 @@
 
 基于 ONNX 的无人机检测系统,支持单张图片和批量处理。
 
+## 概述
+
+本项目实现了多种UAV(无人机)检测模型的推理系统,支持三种不同的模型架构和相应的后处理方法。每种模型都有其特定的输入输出格式、后处理流程和可视化方式。
+
 ## 功能特点
 
 - 支持 ONNX 模型推理
 - 支持 CUDA 加速
 - 支持批量处理图片
 - 自动生成检测报告(CSV格式)
-- 支持误报过滤
 - 支持检测框面积比例限制
 - 支持保存未检测到目标的图片
+- 支持三种不同架构的模型自动识别
+- 集成边界框验证机制
+
+## 支持的模型类型
+
+### 1. Anti_UAV 模型
+- **文件名**: `250411_Anti_UAV.onnx`
+- **识别特征**: 输入包含 `scale_factor`,输出名称以 `multiclass_nms3` 开头
+- **输入尺寸**: 640×640
+- **类别**: 单类别检测(UAV)
+
+### 2. UAV-250411 模型
+- **文件名**: `UAV-250411.onnx`
+- **识别特征**: 输入包含 `scale_factor`,输出为2个张量且第一个以 `tmp_` 开头
+- **输入尺寸**: 640×640
+- **类别**: 单类别检测(UAV)
+
+### 3. uav_and_bird 模型
+- **文件名**: `uav_and_bird.onnx`
+- **识别特征**: 输入不包含 `scale_factor`,输出张量数量大于2
+- **输入尺寸**: 640×640
+- **类别**: 双类别检测(Bird: 0, UAV: 1)
 
 ## 环境要求
 
@@ -41,6 +66,15 @@ python -m src.core.inference --input path/to/image.jpg --threshold 0.6 --max-bbo
 
 # 保存未检测到目标的图片
 python -m src.core.inference --input path/to/images_dir --save-empty
+
+# 自动识别模型类型
+python inference.py --input /path/to/images --threshold 0.5
+
+# 指定模型类型
+python inference.py --input /path/to/images --model-type uav_and_bird
+
+# 批量处理
+python inference.py --input /path/to/directory --output /path/to/results
 ```
 
 ### 图形界面模式
@@ -49,6 +83,23 @@ python -m src.core.inference --input path/to/images_dir --save-empty
 python -m src.core.inference --gui
 ```
 
+### 编程接口
+
+```python
+# 初始化检测器
+detector = ONNXDetector(
+    input_dir="/path/to/images",
+    model_type="uav_and_bird",
+    prob_threshold=0.5
+)
+
+# 处理单张图像
+detections, img_out, detection_list = detector.process_image("/path/to/image.jpg")
+
+# 批量处理
+total_detections = detector.process_directory()
+```
+
 ## 参数说明
 
 - `--input`: 输入图像路径或目录(必需)
@@ -57,12 +108,209 @@ python -m src.core.inference --gui
 - `--max-bbox-ratio`: 检测框最大面积比例阈值,默认0.05
 - `--save-empty`: 是否保存未检测到目标的图片
 - `--gui`: 启用图形界面选择输入目录
+- `--model-type`: 指定模型类型(可选,系统可自动识别)
+
+## 模型结构分析
+
+### 输入预处理
+
+所有模型都采用相同的预处理流程:
+
+```python
+# 图像预处理步骤
+1. 颜色空间转换: BGR → RGB
+2. 尺寸调整: 原图 → 640×640
+3. 数据类型转换: uint8 → float32
+4. 归一化: [0,255] → [0,1]
+5. 标准化: 使用ImageNet均值和标准差
+   - mean = [0.485, 0.456, 0.406]
+   - std = [0.229, 0.224, 0.225]
+6. 维度调整: HWC → CHW
+7. 批次维度: CHW → NCHW
+```
+
+### 模型输入格式
+
+#### Anti_UAV 和 UAV-250411 模型
+```python
+inputs = {
+    'image': img[None, :, :, :],        # (1, 3, 640, 640)
+    'scale_factor': scale_factor[None, :] # (1, 2)
+}
+```
+
+#### uav_and_bird 模型
+```python
+inputs = {
+    'images': img[None, :, :, :]  # (1, 3, 640, 640)
+}
+```
+
+## 后处理方法详解
+
+### 1. Anti_UAV 模型后处理
+
+**特点**: 模型内置NMS,输出已经过滤的检测结果
+
+```python
+# 输出格式
+bbox: (N, 4)      # 边界框坐标 [x1, y1, x2, y2]
+confidence: (N, 1) # 置信度分数
+
+# 后处理流程
+1. 置信度过滤: confidence > threshold
+2. 坐标缩放: 模型输出 → 原图尺寸
+3. 边界框验证: 面积比例检查
+4. 结果保存和可视化
+```
+
+### 2. UAV-250411 模型后处理
+
+**特点**: 需要手动实现NMS处理
+
+```python
+# 输出格式
+output[0]: (N, 4)  # 边界框坐标
+output[1]: (N, 1)  # 置信度分数
+
+# 后处理流程
+1. 置信度过滤: confidence > threshold
+2. 坐标缩放: 模型输出 → 原图尺寸
+3. NMS处理: IoU阈值 = 0.4
+4. 边界框验证
+5. 结果保存和可视化
+```
+
+### 3. uav_and_bird 模型后处理
+
+**特点**: 多尺度输出,使用Distribution Focal Loss,需要复杂的解码过程
+
+```python
+# 输出格式(3个尺度)
+small_output: (1, 80, 80, 21)   # 小目标检测
+medium_output: (1, 40, 40, 21)  # 中等目标检测
+large_output: (1, 20, 20, 21)   # 大目标检测
+
+# 每个输出的通道分布
+# 21 = 17(bbox) + 2(classes) + 2(quality)
+
+# 后处理流程
+1. 多尺度处理:
+   - 小尺度: stride=8, 网格80×80
+   - 中尺度: stride=16, 网格40×40  
+   - 大尺度: stride=32, 网格20×20
+
+2. Distribution Focal Loss解码:
+   - bbox_pred: 17维距离分布 → 4个距离值
+   - 使用softmax + 期望值计算实际距离
+
+3. 网格坐标生成(带0.5偏移):
+   grid_x, grid_y = np.meshgrid(range(W), range(H))
+   grid_x = (grid_x.flatten() + 0.5)
+   grid_y = (grid_y.flatten() + 0.5)
+
+4. 边界框计算:
+   x1 = (grid_x - d_left) * stride
+   y1 = (grid_y - d_top) * stride
+   x2 = (grid_x + d_right) * stride
+   y2 = (grid_y + d_bottom) * stride
+
+5. 置信度计算:
+   final_scores = cls_pred * quality_pred
+
+6. 多尺度结果合并和NMS处理
+```
+
+## 可视化和画框方式
+
+### 1. 边界框绘制
+
+所有模型都使用相同的绘制方式:
+
+```python
+# 边界框样式
+cv2.rectangle(img_out, (x1, y1), (x2, y2), (255, 0, 0), 4)  # 蓝色框,线宽4
+
+# 标签样式
+label = f'{class_name} {confidence:.2f}'
+cv2.putText(img_out, label, (x1, y1 - 10), 
+            cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 0, 255), 2)  # 红色文字
+```
+
+### 2. 类别标签映射
+
+```python
+# Anti_UAV 和 UAV-250411 模型
+class_names = {0: 'UAV'}
+
+# uav_and_bird 模型
+class_names = {0: 'Bird', 1: 'UAV'}
+```
+
+### 3. 检测结果保存
+
+```python
+# 保存检测到的目标区域
+target_filename = f"{base_name}_{detection_count}.jpg"
+cv2.imwrite(os.path.join(targets_dir, target_filename), roi)
+
+# 保存带标注的完整图像
+output_filename = f"detected_{base_name}"
+cv2.imwrite(os.path.join(output_dir, output_filename), img_out)
+```
+
+## 性能优化和质量控制
+
+### 1. 边界框验证
+
+```python
+# 面积比例检查
+bbox_area = (x2 - x1) * (y2 - y1)
+image_area = orig_w * orig_h
+if bbox_area / image_area > max_bbox_ratio:  # 默认0.05
+    continue
+
+# 尺寸有效性检查
+if x2 <= x1 or y2 <= y1:
+    continue
+```
+
+### 2. NMS参数
+
+```python
+# 所有模型统一使用的NMS参数
+conf_threshold = 0.5    # 置信度阈值
+iou_threshold = 0.4     # IoU阈值
+```
+
+## 模型自动识别机制
+
+系统通过分析ONNX模型的输入输出结构自动识别模型类型:
+
+```python
+def get_model_type(model_path: str) -> str:
+    model = onnx.load(model_path)
+    input_names = [i.name for i in model.graph.input]
+    output_names = [o.name for o in model.graph.output]
+    
+    if 'scale_factor' in input_names:
+        if any(name.startswith('multiclass_nms3') for name in output_names):
+            return 'Anti_UAV'
+        else:
+            return 'UAV-250411'
+    else:
+        if len(output_names) > 2:
+            return 'uav_and_bird'
+    
+    return 'unknown'
+```
 
 ## 输出说明
 
 程序会在输出目录中生成以下内容:
 
 - 检测结果图片(带检测框)
+- 检测到的目标区域图片
 - `detection_report.csv`: 检测报告,包含以下信息:
   - 图片路径
   - 检测时间
@@ -76,10 +324,29 @@ python -m src.core.inference --gui
 2. 如果使用GPU加速,请确保CUDA环境配置正确
 3. 批量处理时建议使用相对较小的图片尺寸以提高处理速度
 4. 检测报告会自动覆盖同名文件,请注意备份
+5. 系统会自动识别模型类型,无需手动指定
+6. 支持多种模型架构,可根据实际需求选择合适的模型
+
+## 项目特点总结
+
+本项目实现了一个灵活的多模型UAV检测系统,具有以下特点:
+
+1. **模型兼容性**: 支持三种不同架构的ONNX模型
+2. **自动识别**: 根据模型结构自动选择对应的后处理方法
+3. **质量控制**: 集成边界框验证机制
+4. **可扩展性**: 易于添加新的模型类型和后处理方法
+5. **性能优化**: 支持CUDA加速和批量处理
+6. **用户友好**: 支持命令行和图形界面两种使用方式
+7. **完整输出**: 自动生成检测报告和可视化结果
+
+每种模型都有其特定的应用场景和优势,用户可以根据实际需求选择合适的模型进行部署。
 
 ## 更新日志
 
 ### 2024-03-29
 - 移除Excel报告生成功能
 - 优化检测报告生成逻辑
-- 修复模型加载和属性访问问题
+- 修复模型加载和属性访问问题
+- 移除ORB特征匹配误报过滤功能
+- 清理调试信息,优化代码结构
+- 合并技术文档到README,提供完整的使用和技术说明

+ 5 - 4
requirements.txt

@@ -1,8 +1,9 @@
-onnxruntime-gpu==1.15.1
+numpy>=1.21.0
+Pillow>=9.0.0
+opencv-python>=4.5.0
+onnxruntime>=1.8.0
 onnx==1.17.0
-opencv-python==4.11.0.86
-numpy==1.24.3
 tqdm==4.66.1
-openpyxl==3.1.2
+openpyxl==3.1.5
 pytest==7.4.0
 pytest-cov==4.1.0 

+ 1 - 1
src/analysis/analysis_report.py

@@ -90,4 +90,4 @@ if __name__ == "__main__":
     report = generate_analysis_report(df)
     save_report(report, args.output)
     
-    print(f"分析完成!报告已保存至:{args.output}")
+    # 分析报告生成完成

+ 7 - 10
src/core/ali_image_validation.py

@@ -18,10 +18,7 @@ class AliImageValidator:
         if not self.client.api_key:
             raise ValueError("未找到API密钥,请设置环境变量DASHSCOPE_API_KEY或通过参数传递")
 
-        logging.basicConfig(
-            format='%(asctime)s - %(levelname)s - %(message)s',
-            level=logging.DEBUG
-        )
+        # 日志配置已移除
 
 
     @retry(tries=3, delay=1, backoff=2, max_delay=5, exceptions=(Exception,))
@@ -53,7 +50,7 @@ class AliImageValidator:
         try:
             start_time = time.time()
             model_name = "qwen-vl-max-latest"
-            logging.info(f"发送请求到阿里云 | 端点: {self.client.base_url} | 模型: {model_name}")
+            # 发送请求到阿里云
             response = self.client.chat.completions.create(
                 model=model_name,
                 messages=[
@@ -77,7 +74,7 @@ class AliImageValidator:
             
             try:
                 result = response.choices[0].message.content
-                logging.debug(f"原始响应内容: {result}")
+                # 获取响应内容
                 try:
                     result_json = json.loads(result.split('```json')[1].split('```')[0].strip())  # 提取markdown代码块中的JSON
                     
@@ -89,7 +86,7 @@ class AliImageValidator:
                         'response_time': response_time
                     }
                 except (IndexError, json.JSONDecodeError, KeyError) as e:
-                    logging.error(f"响应解析失败 | 错误类型: {type(e).__name__} | 原始响应: {result}")
+                    # 响应解析失败
                     return {
                         'is_false_positive': "误报" in result,
                         'uav_detected': False,
@@ -98,7 +95,7 @@ class AliImageValidator:
                         'response_time': response_time
                     }
             except (json.JSONDecodeError, ValueError) as e:
-                logging.error(f"响应解析失败: {str(e)}")
+                # 响应解析失败
                 return {
                     'is_false_positive': "误报" in result,
                     'uav_detected': False,
@@ -110,7 +107,7 @@ class AliImageValidator:
         except Exception as e:
             end_time = time.time()
             response_time = end_time - start_time
-            logging.error(f"API调用失败 | 端点: {self.client.base_url} | 模型: {model_name} | 错误类型: {type(e).__name__} | 错误详情: {str(e)}")
+            # API调用失败
             return {
                 'is_false_positive': False,
                 'uav_detected': False,
@@ -129,4 +126,4 @@ if __name__ == "__main__":
     validator = AliImageValidator()
     result = validator.analyze_image(args.image_url)
     
-    print(f"综合分析结果:\n误报概率: {result['probability']:.2f}\n无人机识别: {'是' if result['uav_detected'] else '否'}\n判定结果: {'误报' if result['is_false_positive'] else '真实威胁'}\n响应时间: {result['response_time']:.2f}秒\n分析理由: {result['reason']}")
+    # 分析完成

+ 4 - 9
src/core/compare_outputs.py

@@ -42,25 +42,20 @@ def compare_directories(dir1, dir2, output_dir):
         src_file = os.path.join(dir1, dir1_base_files[base_name])
         dst_file = os.path.join(dir1_only_dir, dir1_base_files[base_name])
         shutil.copy2(src_file, dst_file)
-        print(f"复制 {src_file} 到 {dst_file}")
+        # 复制文件
     
     for base_name in only_in_dir2:
         src_file = os.path.join(dir2, dir2_base_files[base_name])
         dst_file = os.path.join(dir2_only_dir, dir2_base_files[base_name])
         shutil.copy2(src_file, dst_file)
-        print(f"复制 {src_file} 到 {dst_file}")
+        # 复制文件
     
     # 打印统计信息
-    print(f"\n统计信息:")
-    print(f"目录1中的文件总数: {len(dir1_files)}")
-    print(f"目录2中的文件总数: {len(dir2_files)}")
-    print(f"只在目录1中存在的文件数: {len(only_in_dir1)}")
-    print(f"只在目录2中存在的文件数: {len(only_in_dir2)}")
-    print(f"共同存在的文件数: {len(dir1_files) - len(only_in_dir1)}")
+    # 文件比较完成
 
 if __name__ == "__main__":
     dir1 = r"D:\PythonProject\Model\output_20250329_140816_results"
     dir2 = r"D:\PythonProject\Model\Output\output_20250329_140816_results"
     output_dir = r"D:\PythonProject\Model\output_differences"
     
-    compare_directories(dir1, dir2, output_dir) 
+    compare_directories(dir1, dir2, output_dir)

+ 6 - 6
src/core/feature_extractor.py

@@ -18,24 +18,24 @@ class FalsePositiveFeatureExtractor:
     def extract_features(self, img_path):
         # 检查文件是否存在
         if not os.path.exists(img_path):
-            print(f"警告:文件不存在 {img_path}")
+            # 文件不存在
             return None
             
         # 读取图片并检查有效性
         img = cv2.imread(img_path, cv2.IMREAD_GRAYSCALE)
         if img is None:
-            print(f"警告:无法读取图像 {img_path}")
+            # 无法读取图像
             return None
             
         # 检查图片尺寸和内容
         if img.size == 0:
-            print(f"警告:空图像 {img_path}")
+            # 空图像
             return None
             
         # 计算图像清晰度(拉普拉斯方差)
         blur_value = cv2.Laplacian(img, cv2.CV_64F).var()
         if blur_value < 50:  # 阈值可根据实际情况调整
-            print(f"警告:图像模糊 {img_path} (清晰度: {blur_value:.2f})")
+            # 图像模糊
             return None
             
         # 提取特征
@@ -43,7 +43,7 @@ class FalsePositiveFeatureExtractor:
         
         # 检查特征数量和质量
         if des is None or len(des) < 10:
-            print(f"警告:特征不足 {img_path} (特征数: {len(kp) if kp else 0})")
+            # 特征不足
             return None
             
         return des
@@ -73,4 +73,4 @@ if __name__ == '__main__':
     
     extractor = FalsePositiveFeatureExtractor()
     count = extractor.build_feature_db(args.input, args.output)
-    print(f'成功提取{count}个误报样本的特征')
+    # 特征提取完成

+ 5 - 8
src/core/image_validation.py

@@ -16,10 +16,7 @@ class ImageValidator:
         if not self.client.api_key:
             raise ValueError("未找到API密钥,请设置环境变量ARK_API_KEY或通过参数传递")
 
-        logging.basicConfig(
-            format='%(asctime)s - %(levelname)s - %(message)s',
-            level=logging.INFO
-        )
+        # 日志配置已移除
 
     def _image_to_base64(self, image_path):
         try:
@@ -28,7 +25,7 @@ class ImageValidator:
                 img.convert('RGB').save(buffered, format="JPEG")
                 return base64.b64encode(buffered.getvalue()).decode('utf-8')
         except Exception as e:
-            logging.error(f"图片处理失败: {str(e)}")
+            # 图片处理失败
             return None
 
     def analyze_image(self, image_path):
@@ -93,7 +90,7 @@ class ImageValidator:
                     'response_time': response_time
                 }
             except (json.JSONDecodeError, ValueError) as e:
-                logging.error(f"响应解析失败: {str(e)}")
+                # 响应解析失败
                 return {
                     'is_false_positive': "误报" in result,
                     'uav_detected': False,
@@ -105,7 +102,7 @@ class ImageValidator:
         except Exception as e:
             end_time = time.time()
             response_time = end_time - start_time
-            logging.error(f"API调用失败: {str(e)}")
+            # API调用失败
             return {
                 'is_false_positive': False,
                 'uav_detected': False,
@@ -126,4 +123,4 @@ if __name__ == "__main__":
     validator = ImageValidator(args.api_key)
     is_false_positive = validator.analyze_image(args.image_path)
     
-    print(f"综合分析结果:\n误报概率: {is_false_positive['probability']:.2f}\n无人机识别: {'是' if is_false_positive['uav_detected'] else '否'}\n判定结果: {'误报' if is_false_positive['is_false_positive'] else '真实威胁'}\n响应时间: {is_false_positive['response_time']:.2f}秒\n分析理由: {is_false_positive['reason']}")
+    # 分析完成

+ 3 - 3
src/core/infer.py

@@ -119,7 +119,7 @@ def process_image(detector: UAVDetector, image_path: str, output_dir: str) -> in
     """处理单张图像"""
     srcimg = cv2.imread(image_path)
     if srcimg is None:
-        print(f"无法读取图像: {image_path}")
+        # 无法读取图像
         return 0
         
     processed_img, detections = detector.detect(srcimg)
@@ -162,7 +162,7 @@ if __name__ == '__main__':
         for img_file in tqdm(image_files, desc='Processing images'):
             total_detections += process_image(detector, img_file, args.output)
             
-        print(f'批量处理完成!共检测到 {total_detections} 个目标')
+        # 批量处理完成
     else:
         detections = process_image(detector, args.input, args.output)
-        print(f'处理完成!检测到 {detections} 个目标') 
+        # 处理完成

+ 159 - 100
src/core/inference.py

@@ -3,7 +3,6 @@ import onnxruntime as ort
 import cv2
 import os
 import argparse
-import pickle
 from glob import glob
 from typing import List, Dict, Optional
 import time
@@ -46,8 +45,7 @@ class ONNXDetector:
                 self.model_path = r"D:\PythonProject\R360-UAVmodelTool\src\models\UAV-250411.onnx"
             else:
                 self.model_path = r"D:\PythonProject\R360-UAVmodelTool\src\models\uav_and_bird.onnx"
-        print(f"模型类型: {self.model_type}")
-        print(f"模型路径: {self.model_path}")
+        # 模型初始化完成
         
         # 初始化ONNX会话
         so = ort.SessionOptions()
@@ -74,15 +72,12 @@ class ONNXDetector:
             self.output_name = 'conv2d_308.tmp_1'
         
         # 类别映射
-        self.class_names = {0: 'UAV'}
+        if self.model_type == 'uav_and_bird':
+            self.class_names = {0: 'Bird', 1: 'UAV'}
+        else:
+            self.class_names = {0: 'UAV'}
 
-        # 加载误报特征库
-        self.false_positive_features = {}
-        try:
-            with open(os.path.join(os.path.dirname(__file__), 'false_positive_features.pkl'), 'rb') as f:
-                self.false_positive_features = pickle.load(f)
-        except FileNotFoundError:
-            print("未找到误报特征库文件,跳过加载")
+        # 误报特征库相关代码已移除
         
         # 创建输出目录
         output_base = os.path.join(os.path.dirname(__file__), '..', '..', 'Output')
@@ -109,7 +104,7 @@ class ONNXDetector:
         if self.use_cuda:
             self.gpu_frame.upload(cv2.imread(image_path))
             if self.gpu_frame.empty():
-                print(f"无法加载图像:{image_path}")
+                # 图像加载失败
                 return None
             
             # GPU预处理流水线
@@ -123,7 +118,7 @@ class ONNXDetector:
             # CPU回退路径
             image_orig = cv2.imread(image_path)
             if image_orig is None:
-                print(f"无法加载图像:{image_path}")
+                # 图像加载失败
                 return None
             
             self.orig_h, self.orig_w = image_orig.shape[:2]
@@ -165,83 +160,60 @@ class ONNXDetector:
     def inference(self, input_data: np.ndarray):
         if self.model_type == 'uav_and_bird':
             # 返回所有输出
-            return self.session.run(None, {self.input_name: input_data})
+            outputs = self.session.run(None, {self.input_name: input_data})
+            # 模型推理完成
+            return outputs
         elif self.model_type in ['Anti_UAV', 'UAV-250411']:
             scale_factor = np.array([[1, 1]], dtype=np.float32)
-            return self.session.run(
+            output = self.session.run(
                 [self.output_name],
                 {self.input_name: input_data, 'scale_factor': scale_factor}
             )[0]
+            # 模型推理完成
+            return output
         else:
-            return self.session.run(
+            output = self.session.run(
                 [self.output_name],
                 {self.input_name: input_data}
             )[0]
+            # 模型推理完成
+            return output
 
-    def _is_false_positive(self, roi: np.ndarray) -> bool:
-        """使用ORB特征匹配验证是否误报"""
-        if not self.false_positive_features:
-            return False
-
-        # 初始化特征检测器
-        detector = cv2.ORB_create()
-        _, des = detector.detectAndCompute(roi, None)
-
-        # 与特征库进行匹配
-        for fp_feature in self.false_positive_features.values():
-            if des is None or fp_feature['features'] is None:
-                continue
-
-            # 使用FLANN匹配器
-            flann = cv2.FlannBasedMatcher(dict(algorithm=6, table_number=6), dict())
-            matches = flann.knnMatch(des, fp_feature['features'], k=2)
-
-            # 处理空匹配情况
-            if not matches:
-                continue
-
-            # 安全验证匹配结果
-            good_matches = []
-            for match_group in matches:
-                # 确保match_group有足够元素防止索引错误
-                if len(match_group) < 2:
-                    continue
-                m, n = match_group
-                if m.distance < 0.7 * n.distance:
-                    good_matches.append(m)
-
-            # 计算优质匹配数量
-            if len(good_matches) > 15:  # 匹配阈值
-                return True
-        return False
+    # ORB特征匹配误报检测方法已移除
 
     def postprocess(self, detections, image_orig: np.ndarray, image_path: str) -> tuple:
+        # 开始后处理
+        
         valid_detections = 0
         img_out = image_orig.copy()
         detections_list = []
+        
         if self.model_type == 'Anti_UAV':
             # Anti_UAV模型的后处理逻辑
             keep_idx = (detections[:, 1] > self.confThreshold)
             detections = detections[keep_idx]
+            
             if len(detections) == 0:
                 return 0, img_out, []
             ratioh = self.orig_h / self.input_size[1]
             ratiow = self.orig_w / self.input_size[0]
+            
             detections[:, 2:6] *= np.array([ratiow, ratioh, ratiow, ratioh])
             keep = self.nms(detections[:, 2:6], detections[:, 1:2], self.confThreshold, 0.4)
+            
             for idx in keep:
                 class_id = int(detections[idx, 0])
                 confidence = detections[idx, 1]
                 x1, y1, x2, y2 = detections[idx, 2:6].astype(int)
                 bbox_area = (x2 - x1) * (y2 - y1)
                 image_area = self.orig_w * self.orig_h
+                
                 if bbox_area / image_area > self.max_bbox_ratio:
                     continue
                 roi = image_orig[y1:y2, x1:x2]
                 if roi.size == 0:
                     continue
-                if self._is_false_positive(roi):
-                    continue
+                # 误报过滤已移除
                 target_filename = f"{os.path.splitext(os.path.basename(image_path))[0]}_{valid_detections}.jpg"
                 cv2.imwrite(os.path.join(self.targets_dir, target_filename), roi)
                 label = f'{self.class_names[class_id]} {confidence:.2f}'
@@ -260,25 +232,28 @@ class ONNXDetector:
             # UAV-250411模型的后处理逻辑
             keep_idx = (detections[:, 1] > self.confThreshold)
             detections = detections[keep_idx]
+            
             if len(detections) == 0:
                 return 0, img_out, []
             ratioh = self.orig_h / self.input_size[1]
             ratiow = self.orig_w / self.input_size[0]
+            
             detections[:, 2:6] *= np.array([ratiow, ratioh, ratiow, ratioh])
             keep = self.nms(detections[:, 2:6], detections[:, 1:2], self.confThreshold, 0.4)
+            
             for idx in keep:
                 class_id = int(detections[idx, 0])
                 confidence = detections[idx, 1]
                 x1, y1, x2, y2 = detections[idx, 2:6].astype(int)
                 bbox_area = (x2 - x1) * (y2 - y1)
                 image_area = self.orig_w * self.orig_h
+                
                 if bbox_area / image_area > self.max_bbox_ratio:
                     continue
                 roi = image_orig[y1:y2, x1:x2]
                 if roi.size == 0:
                     continue
-                if self._is_false_positive(roi):
-                    continue
+                # 误报过滤已移除
                 target_filename = f"{os.path.splitext(os.path.basename(image_path))[0]}_{valid_detections}.jpg"
                 cv2.imwrite(os.path.join(self.targets_dir, target_filename), roi)
                 label = f'{self.class_names[class_id]} {confidence:.2f}'
@@ -294,58 +269,136 @@ class ONNXDetector:
                     'orig_h': self.orig_h
                 })
         elif self.model_type == 'uav_and_bird':
-            # Netron顺序: [bbox20, cls20, obj20, bbox40, cls40, obj40, bbox80, cls80, obj80]
-            all_boxes, all_scores, all_classes = [], [], []
+            # 根据用户提供的详细说明,模型输出9个张量,按3个尺度分组:
+            # 小尺度 (20x20): 输出0(bbox), 输出1(cls), 输出2(quality)
+            # 中等尺度 (40x40): 输出3(bbox), 输出4(cls), 输出5(quality) 
+            # 大尺度 (80x80): 输出6(bbox), 输出7(cls), 输出8(quality)
+            if len(detections) != 9:
+                return 0, img_out, []
+            
+            # 定义尺度信息:(bbox_idx, cls_idx, quality_idx, grid_size, stride)
             scales = [
-                (0, 1, 2, 20, 20),
-                (3, 4, 5, 40, 40),
-                (6, 7, 8, 80, 80),
+                (0, 1, 2, 20, 32),  # 小尺度特征图,用于检测大物体
+                (3, 4, 5, 40, 16),  # 中等尺度特征图,用于检测中等物体
+                (6, 7, 8, 80, 8),   # 大尺度特征图,用于检测小物体
             ]
-            for bbox_idx, cls_idx, obj_idx, H, W in scales:
-                bbox_pred = detections[bbox_idx][0]  # (68, H, W)
-                cls_pred = detections[cls_idx][0]    # (2, H, W)
-                obj_pred = detections[obj_idx][0]    # (1, H, W)
-                bbox_pred = bbox_pred.reshape(68, -1).T  # (H*W, 68)
-                cls_pred = cls_pred.reshape(2, -1).T     # (H*W, 2)
-                obj_pred = obj_pred.reshape(-1)          # (H*W,)
-                scores = obj_pred[:, None] * (1 / (1 + np.exp(-cls_pred)))
-                max_scores = np.max(scores, axis=1)
-                class_ids = np.argmax(scores, axis=1)
+            
+            all_boxes, all_scores, all_classes = [], [], []
+            
+            for scale_idx, (bbox_idx, cls_idx, quality_idx, grid_size, stride) in enumerate(scales):
+                # 获取当前尺度的输出
+                bbox_output = detections[bbox_idx]      # (1, 68, H, W)
+                cls_output = detections[cls_idx]        # (1, 2, H, W) 
+                quality_output = detections[quality_idx] # (1, 1, H, W)
+                
+                # 移除batch维度
+                bbox_pred = bbox_output[0]    # (68, H, W)
+                cls_pred = cls_output[0]      # (2, H, W)
+                quality_pred = quality_output[0] # (1, H, W)
+                
+                H, W = bbox_pred.shape[1], bbox_pred.shape[2]
+                
+                # 创建网格坐标 - 添加0.5偏移表示像素中心
+                grid_y, grid_x = np.meshgrid(np.arange(H), np.arange(W), indexing='ij')
+                grid_x = (grid_x + 0.5).flatten()  # (H*W,) - 像素中心偏移
+                grid_y = (grid_y + 0.5).flatten()  # (H*W,) - 像素中心偏移
+                
+                # 重塑张量为 (H*W, channels)
+                bbox_pred = bbox_pred.reshape(68, -1).T    # (H*W, 68)
+                cls_pred = cls_pred.reshape(2, -1).T       # (H*W, 2)
+                quality_pred = quality_pred.reshape(-1)    # (H*W,)
+                
+                # 解码边界框 - Distribution Focal Loss解码
+                # 68个通道 = 4个边界 * 17个分布值
+                bbox_pred = bbox_pred.reshape(-1, 4, 17)  # (H*W, 4, 17)
+                
+                # 对每个边界的17个值应用softmax
+                bbox_pred_softmax = np.exp(bbox_pred) / np.sum(np.exp(bbox_pred), axis=2, keepdims=True)
+                
+                # 创建范围向量 [0, 1, 2, ..., 16]
+                range_vector = np.arange(17).reshape(1, 1, 17)
+                
+                # 计算期望距离值
+                distances = np.sum(bbox_pred_softmax * range_vector, axis=2)  # (H*W, 4)
+                d_left, d_top, d_right, d_bottom = distances[:, 0], distances[:, 1], distances[:, 2], distances[:, 3]
+                
+                # 计算最终边界框坐标
+                x1 = (grid_x - d_left) * stride
+                y1 = (grid_y - d_top) * stride
+                x2 = (grid_x + d_right) * stride
+                y2 = (grid_y + d_bottom) * stride
+                
+                # 组合边界框
+                boxes = np.stack([x1, y1, x2, y2], axis=1)  # (H*W, 4)
+                
+                # 计算最终置信度分数
+                # cls_pred已经是sigmoid激活后的结果,quality_pred是clip后的结果
+                final_scores = cls_pred * quality_pred[:, None]  # (H*W, 2)
+                
+                # 获取最大置信度和对应类别
+                max_scores = np.max(final_scores, axis=1)  # (H*W,)
+                class_ids = np.argmax(final_scores, axis=1)  # (H*W,)
+                
+                # 置信度过滤
                 mask = max_scores > self.confThreshold
-                boxes = bbox_pred[mask]
-                scores = max_scores[mask]
-                classes = class_ids[mask]
-                all_boxes.append(boxes)
-                all_scores.append(scores)
-                all_classes.append(classes)
+                
+                if np.sum(mask) > 0:
+                    filtered_boxes = boxes[mask]
+                    filtered_scores = max_scores[mask]
+                    filtered_classes = class_ids[mask]
+                    
+                    all_boxes.append(filtered_boxes)
+                    all_scores.append(filtered_scores)
+                    all_classes.append(filtered_classes)
+            
+            # 检查是否有有效检测
             if len(all_boxes) == 0 or all([len(b) == 0 for b in all_boxes]):
                 return 0, img_out, []
-            boxes = np.concatenate(all_boxes, axis=0)
-            scores = np.concatenate(all_scores, axis=0)
-            classes = np.concatenate(all_classes, axis=0)
-            # 坐标缩放
+            
+            # 合并所有尺度的检测结果
+            final_boxes = np.concatenate(all_boxes, axis=0)
+            final_scores = np.concatenate(all_scores, axis=0)
+            final_classes = np.concatenate(all_classes, axis=0)
+            
+            # 坐标缩放到原图尺寸
             ratioh = self.orig_h / self.input_size[1]
             ratiow = self.orig_w / self.input_size[0]
-            boxes[:, [0, 2]] *= ratiow
-            boxes[:, [1, 3]] *= ratioh
-            # NMS
-            keep = self.nms(boxes, scores[:, None], self.confThreshold, 0.4)
+            
+            final_boxes[:, [0, 2]] *= ratiow  # x坐标
+            final_boxes[:, [1, 3]] *= ratioh  # y坐标
+            
+            # NMS处理
+            keep = self.nms(final_boxes, final_scores[:, None], self.confThreshold, 0.4)
+            
+            # 处理最终检测结果
             for idx in keep:
-                x1, y1, x2, y2 = boxes[idx][:4].astype(int)
-                confidence = scores[idx]
-                class_id = int(classes[idx])
+                x1, y1, x2, y2 = final_boxes[idx].astype(int)
+                confidence = final_scores[idx]
+                class_id = int(final_classes[idx])
+                
+                # 确保坐标在图像范围内
+                x1 = max(0, min(x1, self.orig_w - 1))
+                y1 = max(0, min(y1, self.orig_h - 1))
+                x2 = max(0, min(x2, self.orig_w - 1))
+                y2 = max(0, min(y2, self.orig_h - 1))
+                
                 bbox_area = (x2 - x1) * (y2 - y1)
                 image_area = self.orig_w * self.orig_h
+                
                 if bbox_area / image_area > self.max_bbox_ratio:
                     continue
+                    
+                if x2 <= x1 or y2 <= y1:
+                    continue
+                    
                 roi = image_orig[y1:y2, x1:x2]
                 if roi.size == 0:
                     continue
-                if self._is_false_positive(roi):
-                    continue
+                    
+                # 误报过滤已移除
                 target_filename = f"{os.path.splitext(os.path.basename(image_path))[0]}_{valid_detections}.jpg"
                 cv2.imwrite(os.path.join(self.targets_dir, target_filename), roi)
-                label = f'class {class_id} {confidence:.2f}'
+                label = f'{self.class_names[class_id]} {confidence:.2f}'
                 cv2.rectangle(img_out, (x1, y1), (x2, y2), (255, 0, 0), 4)
                 cv2.putText(img_out, label, (x1, y1 - 10), 
                             cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 0, 255), 2)
@@ -357,6 +410,7 @@ class ONNXDetector:
                     'orig_w': self.orig_w,
                     'orig_h': self.orig_h
                 })
+                
         return valid_detections, img_out, detections_list
 
     def process_image(self, image_path: str) -> int:
@@ -384,8 +438,11 @@ class ONNXDetector:
         # 根据设置保存图片
         if valid_count > 0 or self.save_empty:
             cv2.imwrite(output_path, processed_img)
+            # 输出图像已保存
         else:
             output_path = None
+            # 未保存图像 (无检测且save_empty=False)
+            
         self.image_count += 1
         
         # 记录检测信息
@@ -393,6 +450,7 @@ class ONNXDetector:
             record['detections'] = detections_list
         
         self.detection_records.append(record)
+        # 处理完成
         return valid_count
 
 def get_model_type(model_path: str) -> str:
@@ -422,7 +480,8 @@ def get_model_type(model_path: str) -> str:
                 return 'uav_and_bird'
         return 'unknown'
     except Exception as e:
-        print(f"模型类型识别失败: {e}")
+        # 模型类型识别失败
+        pass
         return 'unknown'
 
 # 命令行接口
@@ -432,13 +491,13 @@ if __name__ == '__main__':
     parser.add_argument('--threshold', type=float, default=0.5, help='检测置信度阈值')
     parser.add_argument('--output', type=str, default=None, help='输出目录路径,默认为输入目录名+_results')
     parser.add_argument('--max-bbox-ratio', type=float, default=0.05,
-                      help='检测框最大面积比例阈值,默认0.05')
+                        help='检测框最大面积比例阈值,默认0.05')
     parser.add_argument('--save-empty', action='store_true',
-                      help='是否保存未检测到目标的图片')
+                        help='是否保存未检测到目标的图片')
     parser.add_argument('--gui', action='store_true',
-                      help='启用图形界面选择输入目录')
+                        help='启用图形界面选择输入目录')
     parser.add_argument('--model-type', type=str, choices=['Anti_UAV', 'UAV-250411', 'uav_and_bird'],
-                      help='指定模型类型,不指定则自动识别')
+                        help='指定模型类型,不指定则自动识别')
     parser.add_argument('--model-path', type=str, help='指定模型路径,不指定则根据模型类型自动选择')
     args = parser.parse_args()
 
@@ -454,7 +513,7 @@ if __name__ == '__main__':
         )
         input_dir = detector.select_input_directory()
         if not input_dir:
-            print("未选择目录,程序退出")
+            # 未选择目录,程序退出
             exit()
         args.input = input_dir
     
@@ -471,7 +530,7 @@ if __name__ == '__main__':
 
     def process_single(image_path: str):
         detections = detector.process_image(image_path)
-        print(f'处理 {os.path.basename(image_path)} 完成,检测到 {detections} 个目标')
+        # 图像处理完成
 
     if os.path.isdir(args.input):
         total = 0
@@ -480,11 +539,11 @@ if __name__ == '__main__':
             image_files.extend([os.path.join(root, f) for f in files if f.lower().endswith('.jpg')])
         for img_file in tqdm(image_files, desc='Processing images'):
             total += detector.process_image(img_file)
-        print(f'批量处理完成!共检测到 {total} 个目标')
+        # 批量处理完成
     
         # 生成CSV报告
         csv_path = os.path.join(detector.output_dir, f'detection_report_{detector.model_type}.csv')
         ReportGenerator(detector).generate_csv(csv_path)
-        print(f'CSV报告已生成: {csv_path}')
+        # CSV报告已生成
     else:
         detections = detector.process_image(args.input)

+ 3 - 6
src/utils/split_dataset.py

@@ -14,12 +14,12 @@ os.makedirs(TEST_DIR, exist_ok=True)
 def split_dataset():
     # 1. 扫描源目录
     if not os.path.exists(SRC_DIR):
-        print(f"源目录不存在: {SRC_DIR}")
+        # 源目录不存在
         return
     
     all_files = [f for f in os.listdir(SRC_DIR) if f.lower().endswith('.jpg')]
     if not all_files:
-        print("未找到JPG文件")
+        # 未找到JPG文件
         return
 
     # 2. 按前缀分组
@@ -52,10 +52,7 @@ def split_dataset():
                 total_copied += 1
 
     # 输出统计信息
-    print(f"处理完成:\n"
-          f"- 共发现 {len(groups)} 个分组\n"
-          f"- 总计复制 {total_copied} 张测试图片\n"
-          f"- 输出目录: {TEST_DIR}")
+    # 数据集分割完成
 
 if __name__ == '__main__':
     split_dataset()

+ 2 - 6
src/utils/update_labels.py

@@ -27,13 +27,9 @@ df['Confirmed Positive'] = df['Image File'].apply(lambda x: os.path.basename(x)
 # 保存更新后的CSV文件
 df.to_csv(output_csv, index=False)
 
-print(f"处理完成!已更新 {len(confirmed_positives)} 个确认的阳性样本。")
-print(f"更新后的CSV文件已保存至:{output_csv}")
+# 标签更新完成
 
 # 显示统计信息
 total_images = len(df)
 confirmed_positive_count = df['Confirmed Positive'].sum()
-print(f"\n统计信息:")
-print(f"总图像数:{total_images}")
-print(f"确认阳性数:{confirmed_positive_count}")
-print(f"确认阳性比例:{confirmed_positive_count/total_images*100:.2f}%") 
+# 统计信息已生成