ancient-ocr-viewer/docs/三大关键问题.md
Yuuko 1018416a7a Initial commit: Ancient OCR Viewer
- 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>
2025-11-19 16:59:40 +08:00

191 lines
5.0 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 古籍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 → 网格列60+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 的正确结果
根据你的描述:
**物理列1line_ids: 0,1,2,3**
- 小字[左|手] 从第2行开始 → `1180000...`
- l2和l3中间有1个空格 → 需要检测l2最后一字和l3第一字的行号差
**物理列2line_id: 4**
- 前面1个空格 → `10000...`
**物理列3line_id: 5**
- 前面2个空格 → `110000...`
**物理列4line_id: 6和物理列5line_id: 7**
- 都是顶格 → `0000...`
**物理列6line_ids: 8,9**
- 前面1个空格 → `10000...`
- l8和l9之间1个空格 → 需要检测
**物理列7line_id: 10**
- 前面1个空格 → `10000...`
---
## 待实现的算法
1. **y坐标 → 行号** 的准确计算
2. **x坐标 → 网格列** 的映射(含空列检测)
3. **空行检测** 逻辑
4. **双行小字** 按line_id配对不是按x分左右