---
url: https://www.dreamlu.net/blog/2026-10-25-mica-ppocr-1.2.4-release.md
description: >-
  预处理堆内存砍掉一半、检测提速 2.6 倍。这一版还清掉了两类「写了却不生效」「关了却关不掉」的问题：PDF
  双通道的配置终于传到了引擎，关掉总开关不再启动失败，海报类版面也有了声明检测参数的口子。
---

> 上一版本 v1.2.3（2026-09-12）　·　发布于 2026-10-25

这一版把预处理内存砍掉一半、检测提速 2.6 倍，顺手清掉了两类「写了却不生效」「关了却关不掉」的问题：PDF 双通道的配置终于传到了引擎，`mica.ai.ppocr.enabled=false` 之后应用也能正常启动；海报类版面也有了声明检测参数的口子。

## 变更总览

| 模块 | 变更 | 类型 |
| --- | --- | --- |
| `mica-ppocr-core` | 预处理内存融合（堆 -50%、检测提速 2.6×）；`mica.ai.ppocr.pdf.*` 接通引擎；输入校验修复 | perf / feat / fix |
| `mica-ppocr-structured` | 热路径分配优化；新增 `detParams()` 钩子；拼多多福袋类型识别 | perf / feat |
| `mica-ppocr-spring-boot-starter` | 总开关闸门补齐；PDF 配置默认值对齐 | fix |
| `mica-ppocr-solon-plugin` | 总开关闸门对齐；补注册 `PdfAutoConfiguration` | fix |

***

## 一、预处理内存砍半，检测提速 2.6 倍

三处预处理（det / rec / doc_ori）改成「归一化结果直接写入 CHW / NCHW 输出数组」，不再 materialize 中间 HWC 数组，也不再生成 CV_32F 中间 Mat。

| 场景 | 堆分配 | 耗时 |
| --- | --- | --- |
| det 预处理（3000×4000 图） | 275 MB → 137 MB（-50%） | 110 ms → 42 ms（2.6×） |
| rec 预处理（50 框 × 1200px 宽） | 79.1 MB → 39.8 MB（-50%） | 21.1 ms → 15.9 ms（-25%） |

另省约 40 MB native 内存。

对使用者的实际影响：预处理一度是大图 OCR 的内存大头 —— 一张 3000×4000 的图，光归一化就吃掉 275 MB 堆。砍掉一半之后，Docker 里给容器的内存配额、或者本地跑批处理时的峰值占用都跟着松一截。

### 数值一个字节都没变

uint8 → CV_32F 与 uint8 → 提升为 float 在 0~255 上逐位相同，所以「先转 float 再算」与「直接提升再算」结果一致 —— 这正是省掉 CV_32F 中间 Mat 的依据。

三个预处理测试各保留了一个 `reference*` 方法（旧实现的副本）做 `assertArrayEquals` 逐位比对。后续再做优化时，按同样模式把参照实现留在测试里 —— 那是数值真相源，不是死代码。

### 顺手修掉的 native 内存滞留

文档方向预处理的中心裁剪子 Mat 视图与 resize 本体原先释放顺序颠倒（先放本体、后放视图），现改为「先视图、后本体」，引用计数 2 → 1 → 0 依次归零。

写反了不会立刻报错，只是 native 缓冲滞留到 GC —— 长时间跑批处理时，这是一笔看不见的账。

***

## 二、PDF 通道配置接通了

### 背景

v1.2.2 引入 PDF 双通道（文本层坐标抽取 + 渲染位图 OCR）时，`mica.ai.ppocr.pdf.*` 只停留在配置对象上，没有传到引擎。引擎的 PDF 入口始终用 `PdfOcrConfig.defaults()`。

实际表现是：`render-dpi` 调成 300 没反应，`min-text-chars` 调成 10 没反应 —— 参数写得对，行为纹丝不动。

### 现在

`PPOcrV6Config` 新增 `pdfOcrConfig` 字段，`PPOcrV6Engine` 构造期取它，PDF 入口（`run(byte[])` / `run(Path)` 嗅探到 `%PDF-` 时）实际消费。图片通道不受影响。

| 参数 | 默认值 | 作用 |
| --- | --- | --- |
| `render-dpi` | 200 | 渲染通道输出 DPI。扫描件内嵌位图有原始分辨率，超过原图信息量的重采样只会放大噪声，发票 / 证件类 200~300 为宜 |
| `min-text-chars` | 20 | 文本层可用所需的最小非空白字符数，低于则判定无文本层 |
| `min-readable-ratio` | 0.90 | 文本层可读字符占比下限。缺 ToUnicode 映射的 CID 字体会抽出一批私有区字符，拉低占比 |
| `force-ocr` | false | 跳过文本层探测，所有页面强制走渲染 + OCR，用于竖排、页面旋转等文本层坐标不可信的版式 |

YAML 写法不变：

```yaml
mica:
  ai:
    ppocr:
      pdf:
        enabled: true
        render-dpi: 200
        min-text-chars: 20
        min-readable-ratio: 0.90
        force-ocr: false
```

也可以直接用代码覆盖 —— 自定义一个 `PdfOcrConfig` Bean 即可（Spring `@Bean` / Solon `@Configuration`），**引擎会直接采用它**，无需再改 yml。

### 两处需要留意的行为变更

1. **`PdfOcrConfig` Bean 现在就是引擎使用的配置**，不再是只读副本。此前业务方自定义同名 Bean 会被静默忽略（容器里是自定义值、引擎仍用 yml 或默认值），现在会直接改变引擎行为。
2. **`pdf.enabled=false` 时 Bean 仍然存在**，取 `PdfOcrConfig.defaults()`（此前是不注册这个 Bean）。两端都容忍该 Bean 缺失（回落到 yml 派生值），不会因为排除自动配置类而启动失败。

### 两端默认值对齐了

v1.2.3 时 Starter 暴露的默认值是 `minTextChars=10` / `minReadableRatio=0.6`，而引擎实际在用的是 `20` / `0.90` —— 文档写 10、引擎跑 20。两端现已统一为 `20` / `0.90`，不配置即保持原有行为。

Solon 端此前还漏了 `PdfAutoConfiguration` 的注册，压根拿不到 `PdfOcrConfig` Bean，本次补上。

***

## 三、关掉总开关不再启动失败

### 背景

Spring 端的 `StructuredParserAutoConfiguration` 漏挂了 `mica.ai.ppocr.enabled` 这个闸门。

10 个解析器 Bean 都以 `PPOcrV6Engine` 为构造参数注入。主配置被关掉 ⇒ 引擎 Bean 不存在 ⇒ 抛 `NoSuchBeanDefinitionException` ⇒ **容器 refresh 直接失败**。

也就是说，想用这个开关在某些环境关掉 OCR，结果应用启动失败。

### 现在

三个配置类统一挂到同一个闸门：

* Spring：`@ConditionalOnProperty(prefix = "mica.ai.ppocr", name = "enabled", havingValue = "true", matchIfMissing = true)`
* Solon：`@Condition(onExpression = "${mica.ai.ppocr.enabled:true} == true")`

关闭时容器里不注册任何 PP-OCR 功能 Bean（引擎 / 10 个解析器 / `PPOcrTemplate` / 配置对象），应用照常启动，可用于「同一套代码在部分环境不启用 OCR」的场景。

⚠️ 行为变更：关闭时两端都不再留下 `PdfOcrConfig` Bean —— Spring 端此前会残留一个（它的 `PdfAutoConfiguration` 只挂在 `mica.ai.ppocr.pdf.enabled` 上），Solon 端此前压根没注册过 `PdfAutoConfiguration`。

例外：`OpenCVNativeLoader` 不受该开关约束，OpenCV 原生库仍会加载。业务方若自行使用 `org.opencv.*`，需知晓这一点。

***

## 四、按版面类型声明 DB 检测参数

### 一个容易被误判的现象

海报类版面（大字号稀疏数字 + 满屏背景水印），rec 单独跑同一张图完全正确，det 却一个框都没给。

![拼多多福袋分享图](/blog/pdd.bug.masked.jpg)

根因不在模型，而在 DB 后处理：`boxScore` 是框（minAreaRect）内**概率图的平均值**，本质是「框内文字像素占比」。这种版面上 DB 把 8 位大数字和背景水印连成一个整体轮廓，框内真正的笔画只占约 42%，低于默认 `detBoxThresh=0.6` 被直接丢弃。

**模型看见了，是后处理过滤掉的。** 排查时先把 det 的概率图 dump 成 PNG（目标区域纯白高响应即模型看见了），再复刻 `DbPostProcessor.extractBoxes` 逐条打印 `origRect / minSideLen / score / PASS|DROP(原因)`，就能定位到是哪一条判据把它过滤掉了。

### 解法：解析器自己声明检测参数

`BaseStructuredParser` 新增 `detParams()` 钩子（默认 `null` = 引擎默认），一站式 `parse(...)` 自动带上，显式传入的 `DbDetParams` 优先。解析器可以按自己的**版面类型**声明专属参数，不影响同引擎上的其它解析器。

```java
public class MyPosterParser extends BaseStructuredParser<MyResult> {

	@Override
	protected DbDetParams detParams() {
		// 放宽 boxThresh 让稀疏大字符框过关，同时收紧 unclipRatio 避免外扩把上下水印卷进来
		return DbDetParams.of(0.3f, 0.4f, 1.0f);
	}

	// ...
}
```

参数顺序是 `DbDetParams.of(thresh, boxThresh, unclipRatio)`。已落地在 `PddLuckyBagParser.DET_PARAMS`（`0.3f / 0.4f / 1.0f`）。

![对比图](/blog/pdd.compare-1.2.3-vs-1.2.4.png)

### 但别照抄这个参数

证件 / 票据类版面沿用引擎默认（`min/64` + `0.6/1.5`）。

* 不要改成 `detLimitType=max` + 大 `detLimitSideLen` —— 那会缩小整图，小字成片漏检（实测出租车票框数 55 → 26）。
* 不要靠调大 `DbDetParams.minSize` 过滤碎片 —— 会连小字标签一起丢（如行驶证「所有人」变 `null`）。

***

## 五、拼多多福袋类型识别

`PddLuckyBagResult` 新增 `getType()`，返回 `PddLuckyBagType`。两类分享图版式几乎一致（搜索邀请码 → 组队双方必得 → 大字号邀请码），仅靠文案区分：

| 枚举 | 含义 | 标题 | 口号 |
| --- | --- | --- | --- |
| `LUCKY_BAG` | 福袋券 | 百亿补贴 抽福袋 | 组队双方 必得现金或券 |
| `FRUIT` | 水果券 | 百亿补贴 水果分享券 | 组队双方 必得水果券 |
| `UNKNOWN` | 无法判定 | 特征文案缺失（如只识别出邀请码数字框） | — |

`fieldBoxes` 同步回填 `type` 关键框。

***

## 六、其他修复与优化

* **识别 / 文档方向预处理显式校验输入为 8bit 3 通道**：归一化按「每像素 3 字节」逐行读，而 `Mat.get(row, 0, byte[])` 是从 `(row, 0)` 起连续读满数组长度 —— 通道数 < 3 会**跨行读**（把相邻行数据当本行像素），> 3 会**通道步长错位**。OpenCV 虽有「缓冲长度须为通道数整数倍」的前置校验，但 1 通道（灰度图）恒能绕过。旧实现越界即抛 `ArrayIndexOutOfBoundsException`，故障模式由「立刻报错」退化为「静默产出错误张量」，现改为显式抛 `IllegalArgumentException`。
* **`MemoryLeakReproTest` 误报**：判据参考点由 round 5 改为 `max(round 5, round 10)`，并移除 `stabilizeJvmMemory()` 的早退守卫。round 5 紧跟强制 GC，是堆刚被 uncommit 的人为凹点，后续正常回升被算成「增长」；实测误报样本 round 20 仍比 baseline **低 26.8 MB**，却因以 round 5 为参考被判成 +87 MB 泄漏。
* **结构化解析热路径分配优化**：火车票解析器 3 处 `Pattern.compile` 提为类常量；户口本解析器标签片段数组提为类常量（消除逐框每次约 110 个 String 引用分配）；银行卡持卡人反查 O(n²) → O(n)（语义不变）。
* **死代码清理**：`Offset.toFloat`，以及 PDF 通道三个无 `detParams` 的私有重载。

***

## 升级指引

```xml
<dependency>
    <groupId>net.dreamlu</groupId>
    <artifactId>mica-ppocr-spring-boot-starter</artifactId>
    <version>1.2.4</version>
</dependency>
```

Solon 用户换 `mica-ppocr-solon-plugin`，纯 Java 用户换 `mica-ppocr-core` + `mica-ppocr-structured`。用法与依赖坐标详见 [mica-ppocr 文档](/mica-ppocr/)。

### 升级前必读：行为变更汇总

| 变更 | 影响 | 应对 |
| --- | --- | --- |
| `mica.ai.ppocr.pdf.*` 真的生效了 | 之前靠改 yml 调 PDF 参数的用户会发现行为变了 —— 之前根本没生效 | 不配置即保持原行为；默认值两端已对齐 |
| `PdfOcrConfig` Bean 会被引擎直接采用 | 自定义同名 Bean 会改变引擎行为，此前是静默忽略 | 检查自定义 Bean 是否符合预期 |
| `pdf.enabled=false` 时 Bean 仍存在 | 取默认值 | 如依赖 Bean 缺失做判断，需调整 |
| `mica.ai.ppocr.enabled=false` 时不再留下 `PdfOcrConfig` Bean | 此前 Spring 会残留一个 | 按需改成读配置属性而非读 Bean |
| 识别 / 文档方向预处理对非 8bit 3 通道 Mat 抛 `IllegalArgumentException` | 此前要么越界异常，要么静默产出错误张量 | 传入前先转成 `CV_8UC3` |

### 兼容性

* Java 8 兼容不变，运行依赖字节码均为 major 52。
* 依赖版本：onnxruntime 1.18.0 / opencv 4.9.0-0 / jts-core 1.20.0 / pdfbox 3.0.8 / slf4j 2.0.20 / spring-boot 2.7.18 / mica-auto 2.3.5 / solon 4.1.1。
* 本版实际升级的依赖只有两项：slf4j 2.0.19 → 2.0.20、solon 4.1.0 → 4.1.1（均为小版本，向后兼容）。其余依赖版本未动。
* Spring Boot Starter 与 Solon 插件能力对齐，类名一一对应。

***

## 验证情况

在 v1.2.4 发布提交（`8f13fc1`）上跑通全量构建与测试（`mvn package -Dmaven.compiler.release=8`），共 **311** 个测试用例，0 失败、0 错误（core 1 个跳过）。

本次新增测试：

* `PdfOcrConfigWiringTest`（core）、`PdfOcrConfigSpringWiringTest`、`PdfOcrConfigSolonWiringTest` —— PDF 配置接线
* `StructuredParserAutoConfigurationTest`（Spring / Solon 各一）—— 总开关闸门
* `DetectionPreprocessorTest` / `RecognitionPreprocessorTest` / `DocOrientationPreprocessorTest` 的 `reference*` 逐位比对
* `PddLuckyBagParserTest` / `PddLuckyBagIntegrationTest` —— 福袋类型与检测参数

***

## 相关链接

* [完整变更记录](https://gitee.com/dreamlu/mica-ppocr/blob/master/CHANGELOG.md)
* [README](https://gitee.com/dreamlu/mica-ppocr)
* [Gitee 仓库](https://gitee.com/dreamlu/mica-ppocr)
* [GitHub 仓库](https://github.com/lets-mica/mica-ppocr)
