- Canvas-based dual display (image + text) - Grid rendering system with layout support - Uniform font size rendering - Double-line small character handling - Comprehensive documentation of OCR rules and algorithms 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
191 lines
5.0 KiB
Markdown
191 lines
5.0 KiB
Markdown
# 古籍OCR渲染的三大关键问题
|
||
|
||
## 问题1:逻辑行到物理行的划分(换行问题)
|
||
|
||
### 场景
|
||
一个逻辑列(line_id)有100个字,但版式只有20行,如何换行填充?
|
||
|
||
### 答案
|
||
**根据y坐标计算每个字符的物理行号**
|
||
|
||
```javascript
|
||
const cellHeight = canvasHeight / rowsPerColumn; // 每行的高度
|
||
const row = Math.round(char.yCenter / cellHeight); // 字符中心y坐标 / 行高
|
||
```
|
||
|
||
### 关键点
|
||
1. **不是顺序填充**:不能简单地从第0行开始依次填充
|
||
2. **基于坐标计算**:每个字符的行号 = y坐标 ÷ 行高(四舍五入)
|
||
3. **人工校对保证**:人工校对后的数据,每列字符数不会超过版式限制
|
||
|
||
### 示例
|
||
```
|
||
版式:20行,行高 = 6000px / 20 = 300px
|
||
|
||
字符A: y=1500 → row = 1500/300 = 5 → 第5行
|
||
字符B: y=1800 → row = 1800/300 = 6 → 第6行
|
||
字符C: y=2100 → row = 2100/300 = 7 → 第7行
|
||
```
|
||
|
||
---
|
||
|
||
## 问题2:空列的判断(物理列到网格列的映射)
|
||
|
||
### 场景
|
||
- line_id=0 对应物理第1列(最右)
|
||
- line_id=1 对应物理第8列(最左)
|
||
- 中间的2-7列是空的
|
||
|
||
如何正确映射?
|
||
|
||
### 错误做法(当前程序)
|
||
```javascript
|
||
// 按物理列顺序映射
|
||
物理列0 → 网格列0
|
||
物理列1 → 网格列1
|
||
```
|
||
**问题**:忽略了空列,导致位置错误
|
||
|
||
### 正确做法1:基于x坐标计算
|
||
```javascript
|
||
const cellWidth = canvasWidth / totalColumns; // 每列宽度
|
||
const gridCol = Math.round((canvasWidth - xCenter) / cellWidth);
|
||
```
|
||
|
||
**问题**:如果页面有左右边距,计算会不准
|
||
|
||
### 正确做法2:基于列间距推断(推荐)
|
||
|
||
**步骤**:
|
||
1. 计算所有物理列的x中心:`[2900, 2600, 2300, ...]`
|
||
2. 计算相邻物理列的间距:`[300, 300, 300, ...]`
|
||
3. 识别**异常大的间距**(如900px),说明中间有空列
|
||
4. 根据间距计算空列数:`空列数 = floor(大间距 / 平均间距) - 1`
|
||
|
||
**示例**:
|
||
```
|
||
物理列1: x=2900
|
||
物理列2: x=900 间距=2000px
|
||
平均间距: 300px
|
||
→ 中间空列数 = floor(2000/300) - 1 = 5列
|
||
→ 物理列1 → 网格列0
|
||
→ 物理列2 → 网格列6(0+1+5空列)
|
||
```
|
||
|
||
---
|
||
|
||
## 问题3:起始位置和列内空格
|
||
|
||
### 3.1 列的起始行(是否顶格)
|
||
|
||
**判断方法**:看该列第一个字符的y坐标
|
||
|
||
```javascript
|
||
const firstChar = column.chars[0]; // 按y排序后的第一个字符
|
||
const startRow = Math.round(firstChar.yCenter / cellHeight);
|
||
|
||
if (startRow === 0) {
|
||
console.log("顶格开始");
|
||
} else {
|
||
console.log(`从第${startRow}行开始,前面有${startRow}个空格`);
|
||
}
|
||
```
|
||
|
||
### 3.2 同一物理列内逻辑列之间的空格
|
||
|
||
**场景**:一个物理列包含多个逻辑列
|
||
```
|
||
物理列1:
|
||
line_id=0: 3个大字
|
||
line_id=1: 2个小字(配成1格)
|
||
line_id=2: 5个大字
|
||
```
|
||
|
||
**如何判断 line_id=0 和 line_id=1 之间有没有空格?**
|
||
|
||
**方法**:按y坐标排序后,检查相邻字符的行号差
|
||
|
||
```javascript
|
||
// 合并所有字符,按y排序
|
||
allChars.sort(by y);
|
||
|
||
for (let i = 0; i < allChars.length - 1; i++) {
|
||
const currentRow = Math.round(allChars[i].yCenter / cellHeight);
|
||
const nextRow = Math.round(allChars[i+1].yCenter / cellHeight);
|
||
|
||
const gap = nextRow - currentRow - 1; // 减1是因为相邻字符行号差1是正常的
|
||
|
||
if (gap > 0) {
|
||
console.log(`第${currentRow}行到第${nextRow}行之间有${gap}个空格`);
|
||
}
|
||
}
|
||
```
|
||
|
||
### 3.3 双行小字内部的处理
|
||
|
||
**重要**:同一格内的双行小字(左右两个字),它们的y坐标可能略有不同
|
||
|
||
**处理**:
|
||
1. 按line_id分组(右列一个line_id,左列一个line_id)
|
||
2. 右列第i个字 配 左列第i个字(都按y排序)
|
||
3. 配对后,取**右字的y坐标**作为这一格的位置
|
||
|
||
```javascript
|
||
// 右列: [字A(y=1500), 字B(y=1800)]
|
||
// 左列: [字C(y=1510), 字D(y=1790)]
|
||
|
||
配对1: [A|C] → y=1500 → row=5
|
||
配对2: [B|D] → y=1800 → row=6
|
||
```
|
||
|
||
---
|
||
|
||
## 完整处理流程
|
||
|
||
```
|
||
1. 按line_id分组 → 逻辑列
|
||
2. 按x坐标聚合 → 物理列
|
||
3. 计算列间距 → 识别空列 → 映射到网格列
|
||
4. 处理每个物理列:
|
||
4.1 合并所有字符(大字+小字配对)
|
||
4.2 按y坐标排序
|
||
4.3 为每个字符/配对计算行号
|
||
4.4 检测空行
|
||
4.5 填充到grid[col][row]
|
||
```
|
||
|
||
---
|
||
|
||
## 0019A.json 的正确结果
|
||
|
||
根据你的描述:
|
||
|
||
**物理列1(line_ids: 0,1,2,3)**
|
||
- 小字[左|手] 从第2行开始 → `1180000...`
|
||
- l2和l3中间有1个空格 → 需要检测l2最后一字和l3第一字的行号差
|
||
|
||
**物理列2(line_id: 4)**
|
||
- 前面1个空格 → `10000...`
|
||
|
||
**物理列3(line_id: 5)**
|
||
- 前面2个空格 → `110000...`
|
||
|
||
**物理列4(line_id: 6)和物理列5(line_id: 7)**
|
||
- 都是顶格 → `0000...`
|
||
|
||
**物理列6(line_ids: 8,9)**
|
||
- 前面1个空格 → `10000...`
|
||
- l8和l9之间1个空格 → 需要检测
|
||
|
||
**物理列7(line_id: 10)**
|
||
- 前面1个空格 → `10000...`
|
||
|
||
---
|
||
|
||
## 待实现的算法
|
||
|
||
1. **y坐标 → 行号** 的准确计算
|
||
2. **x坐标 → 网格列** 的映射(含空列检测)
|
||
3. **空行检测** 逻辑
|
||
4. **双行小字** 按line_id配对(不是按x分左右)
|