# 目标检测引擎

> DetectionEngine 的 YOLOX 风格推理流程：预处理、锚点解码、NMS 后处理与边界框输出

- Repository: sml2h3/ddddocr
- GitHub: https://github.com/sml2h3/ddddocr
- Human wiki: https://grok-wiki.com/public/wiki/sml2h3-ddddocr-10e2eae686bf
- Complete Markdown: https://grok-wiki.com/public/wiki/sml2h3-ddddocr-10e2eae686bf/llms-full.txt

## Source Files

- `ddddocr/core/detection_engine.py`
- `ddddocr/core/base.py`
- `ddddocr/models/model_loader.py`
- `ddddocr/utils/image_io.py`

---

<details>
<summary>相关源文件</summary>
以下文件用于生成此维基页面：
- [ddddocr/core/detection_engine.py](ddddocr/core/detection_engine.py)
- [ddddocr/core/base.py](ddddocr/core/base.py)
- [ddddocr/models/model_loader.py](ddddocr/models/model_loader.py)
- [ddddocr/utils/image_io.py](ddddocr/utils/image_io.py)
- [ddddocr/compat/v1.py](ddddocr/compat/v1.py)
- [ddddocr/utils/exceptions.py](ddddocr/utils/exceptions.py)
</details>

# 目标检测引擎

`DetectionEngine` 是 ddddocr 中负责验证码目标检测的核心模块。它实现了完整的 YOLOX 风格推理流程：从原始图像字节输入，经过 letterbox 预处理、ONNX 模型推理、锚点解码、NMS 后处理，最终输出裁剪后的整数边界框列表。该引擎主要用于在验证码图片中定位文字区域，为后续 OCR 识别提供精确的区域坐标。

本文档覆盖 `DetectionEngine` 的类结构、完整推理流水线、各阶段的算法细节，以及与上层调用方的集成方式。

## 类继承结构

`DetectionEngine` 继承自 `BaseEngine` 抽象基类，获得 ONNX 推理会话管理、设备切换和生命周期控制等通用能力。

```text
BaseEngine (ABC)
 ├── session: onnxruntime.InferenceSession
 ├── model_loader: ModelLoader
 ├── initialize() → abstract
 ├── predict() → abstract
 ├── is_ready() → bool
 ├── switch_device()
 └── cleanup()

DetectionEngine
 ├── initialize()        加载 common_det.onnx
 ├── predict()           统一入口，支持 bytes/str/Image
 ├── preproc()           letterbox 预处理
 ├── demo_postprocess()  YOLOX 锚点解码
 ├── nms()               单类别 NMS
 ├── multiclass_nms()    多类别 NMS（class-agnostic）
 └── get_bbox()          完整推理流水线
```

Sources: [ddddocr/core/base.py:15-113](ddddocr/core/base.py), [ddddocr/core/detection_engine.py:20-212](ddddocr/core/detection_engine.py)

## 模型加载与初始化

`DetectionEngine.__init__` 接受 `use_gpu` 和 `device_id` 参数，调用父类构造后立即执行 `initialize()`。初始化过程通过 `ModelLoader.load_detection_model()` 加载 `common_det.onnx` 模型文件，该文件位于 ddddocr 包根目录下。

`ModelLoader` 内部根据 GPU 配置选择 ONNX Runtime 执行提供者：GPU 模式优先使用 `CUDAExecutionProvider`（分配 2GB 显存上限），失败时回退到 `CPUExecutionProvider`。

| 参数 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| `use_gpu` | `bool` | `False` | 是否启用 GPU 推理 |
| `device_id` | `int` | `0` | CUDA 设备编号 |

Sources: [ddddocr/core/detection_engine.py:23-47](ddddocr/core/detection_engine.py), [ddddocr/models/model_loader.py:152-168](ddddocr/models/model_loader.py)

## 推理流水线概览

`get_bbox()` 方法是完整推理流水线的入口，从原始图像字节到最终边界框列表的完整流程如下：

```text
image_bytes (bytes)
    │
    ▼
cv2.imdecode → BGR numpy array
    │
    ▼
preproc(img, (416, 416))
    ├── letterbox 等比缩放 + 填充 (灰度 114)
    ├── 转置为 CHW 格式
    └── 返回 (padded_img, ratio)
    │
    ▼
ONNX Runtime 推理 → raw output
    │
    ▼
demo_postprocess(output, (416, 416))
    ├── 生成 3 级网格锚点 (stride 8/16/32)
    ├── 解码中心坐标: (offset + grid) × stride
    └── 解码宽高: exp(raw) × stride
    │
    ▼
boxes_xyxy 转换 (cxcywh → xyxy) + 除以 ratio
    │
    ▼
multiclass_nms(boxes, scores, nms_thr=0.45, score_thr=0.1)
    ├── 每个锚点取最高类别分数
    ├── 过滤低于 score_thr 的预测
    ├── 贪心 NMS 按 IoU 去重
    └── 返回 [x1, y1, x2, y2, score, class_id]
    │
    ▼
边界框裁剪到图像范围 → List[List[int]]
```

Sources: [ddddocr/core/detection_engine.py:173-212](ddddocr/core/detection_engine.py)

## 预处理：Letterbox 缩放

`preproc()` 方法将输入图像缩放到模型要求的固定尺寸（默认 416×416），同时保持原始宽高比：

1. 创建一个 416×416 的灰色画布（像素值 114），作为 YOLOX 系列模型的标准填充值。
2. 计算缩放比例 `r = min(416/H, 416/W)`，使用双线性插值等比缩放。
3. 将缩放后的图像粘贴到画布左上角，右侧和下方保留灰色填充。
4. 从 HWC 格式转置为 CHW 格式，转为 `float32` 连续数组。

返回值包含预处理后的张量和缩放比例 `ratio`，后者用于后续将检测坐标映射回原图尺寸。

```python
# ddddocr/core/detection_engine.py:89-104
def preproc(self, img, input_size, swap=(2, 0, 1)):
    padded_img = np.ones((input_size[0], input_size[1], 3), dtype=np.uint8) * 114
    r = min(input_size[0] / img.shape[0], input_size[1] / img.shape[1])
    resized_img = cv2.resize(
        img,
        (int(img.shape[1] * r), int(img.shape[0] * r)),
        interpolation=cv2.INTER_LINEAR,
    ).astype(np.uint8)
    padded_img[: int(img.shape[0] * r), : int(img.shape[1] * r)] = resized_img
    padded_img = padded_img.transpose(swap)
    padded_img = np.ascontiguousarray(padded_img, dtype=np.float32)
    return padded_img, r
```

Sources: [ddddocr/core/detection_engine.py:89-104](ddddocr/core/detection_engine.py)

## 锚点解码：demo_postprocess

YOLOX 模型输出的原始张量并非直接的边界框坐标，而是相对于网格锚点的偏移量。`demo_postprocess()` 负责将这些偏移量解码为实际的像素坐标。

### 多尺度网格生成

模型在三个特征图尺度上生成锚点，每个尺度对应不同的下采样步长：

| 尺度 | 步长 (stride) | 特征图大小 | 锚点数量 |
|------|---------------|------------|----------|
| P3 | 8 | 52×52 | 2704 |
| P4 | 16 | 26×26 | 676 |
| P5 | 32 | 13×13 | 169 |
| **合计** | | | **3549** |

对每个尺度，使用 `np.meshgrid` 生成二维网格坐标，展平后拼接为 `(1, 3549, 2)` 的锚点矩阵。同时生成等长的步长矩阵。

### 坐标解码公式

原始输出的形状为 `(1, 3549, N)`，其中前 4 列为边界框参数：

- **中心坐标** `cx, cy`：`decoded = (raw_offset + grid_anchor) × stride`
- **宽高** `w, h`：`decoded = exp(raw) × stride`

```python
# ddddocr/core/detection_engine.py:124-125
outputs[..., :2] = (outputs[..., :2] + grids) * expanded_strides
outputs[..., 2:4] = np.exp(outputs[..., 2:4]) * expanded_strides
```

解码后的输出格式为 `(1, 3549, N)`，其中前 4 列是 `cx, cy, w, h` 格式的边界框坐标（相对于 416×416 输入空间）。

Sources: [ddddocr/core/detection_engine.py:106-126](ddddocr/core/detection_engine.py)

## 坐标格式转换

`get_bbox()` 在锚点解码后执行两步坐标转换：

1. **cxcywh → xyxy**：将中心点+宽高格式转换为左上角+右下角格式。
2. **缩放回原图**：将 416×416 空间的坐标除以预处理阶段的缩放比例 `ratio`，映射回原图像素坐标。

```python
# ddddocr/core/detection_engine.py:182-187
boxes_xyxy[:, 0] = boxes[:, 0] - boxes[:, 2] / 2.  # x1
boxes_xyxy[:, 1] = boxes[:, 1] - boxes[:, 3] / 2.  # y1
boxes_xyxy[:, 2] = boxes[:, 0] + boxes[:, 2] / 2.  # x2
boxes_xyxy[:, 3] = boxes[:, 1] + boxes[:, 3] / 2.  # y2
boxes_xyxy /= ratio
```

Sources: [ddddocr/core/detection_engine.py:182-187](ddddocr/core/detection_engine.py)

## NMS 后处理

NMS（非极大值抑制）用于去除重叠的冗余检测框。`DetectionEngine` 实现了两层 NMS 逻辑。

### 单类别 NMS

`nms()` 方法实现标准的贪心 NMS 算法：

1. 按置信度降序排列所有候选框。
2. 取最高分框加入保留列表。
3. 计算该框与所有剩余框的 IoU（交并比）。
4. 移除 IoU 超过阈值的框。
5. 重复直到候选列表为空。

IoU 计算使用标准公式：`IoU = 交集面积 / (框A面积 + 框B面积 - 交集面积)`。

### 多类别 NMS（Class-Agnostic）

`multiclass_nms_class_agnostic()` 是实际调用的入口，流程如下：

1. 对每个锚点，取所有类别分数中最高的类别和分数。
2. 过滤掉最高分数低于 `score_thr`（默认 0.1）的预测。
3. 对剩余预测调用 `nms()`，IoU 阈值为 0.45。
4. 返回形状为 `(K, 6)` 的数组，每行包含 `[x1, y1, x2, y2, score, class_id]`。

| 参数 | 默认值 | 说明 |
|------|--------|------|
| `nms_thr` | 0.45 | IoU 重叠阈值，超过此值的框被抑制 |
| `score_thr` | 0.1 | 最低置信度阈值，低于此值的预测被丢弃 |

Sources: [ddddocr/core/detection_engine.py:128-171](ddddocr/core/detection_engine.py)

## 输出边界框裁剪

NMS 之后，`get_bbox()` 将浮点坐标裁剪到原图有效范围内并转为整数：

- `x1`、`y1` 负值修正为 0。
- `x2` 不超过原图宽度，`y2` 不超过原图高度。
- 若 NMS 返回空结果（`pred` 为 `None`），捕获异常后返回空列表 `[]`。

最终输出格式为 `List[List[int]]`，每个子列表为 `[x1, y1, x2, y2]`。

Sources: [ddddocr/core/detection_engine.py:189-212](ddddocr/core/detection_engine.py)

## 上层集成

`DetectionEngine` 通过 `compat.v1.DdddOcr` 类对外暴露。当用户以 `det=True` 初始化 `DdddOcr` 时，创建 `DetectionEngine` 实例并禁用 OCR 功能。调用 `doddocr.detection(img)` 即可触发完整检测流程。

```python
# ddddocr/compat/v1.py:73-76
if det:
    self.det = True
    self.detection_engine = DetectionEngine(use_gpu, device_id)
```

API 服务层（`api/server.py`）和 MCP 工具层（`api/mcp.py`）也支持检测功能的按需启用，通过 `ddddocr_detection` 工具名对外暴露。

Sources: [ddddocr/compat/v1.py:69-76](ddddocr/compat/v1.py), [ddddocr/compat/v1.py:129-148](ddddocr/compat/v1.py)

## 异常处理

`DetectionEngine` 在关键路径上定义了两类自定义异常：

| 异常类 | 触发场景 |
|--------|----------|
| `ModelLoadError` | 模型文件不存在、ONNX 会话创建失败、引擎未初始化时调用 `predict()` |
| `ImageProcessError` | 图像解码失败、预处理或后处理过程中出现运行时错误 |

OpenCV 导入失败时，`safe_import_opencv()` 会根据操作系统输出针对性的安装指导，而非直接崩溃。

Sources: [ddddocr/utils/exceptions.py:11-98](ddddocr/utils/exceptions.py), [ddddocr/core/detection_engine.py:34-47](ddddocr/core/detection_engine.py)

## 总结

`DetectionEngine` 实现了一条紧凑的 YOLOX 风格目标检测流水线：416×416 letterbox 预处理保持宽高比，三尺度网格锚点解码（stride 8/16/32）将模型输出转换为像素坐标，class-agnostic NMS 以 0.45 IoU 阈值去除重叠框，最终输出裁剪到原图范围的整数边界框。整个流程以 ONNX Runtime 为推理后端，支持 CPU/GPU 切换，通过 `ModelLoader` 管理模型生命周期。
