Files
TrueGrowth/docs/MERGED_IMAGE_SPLITTING.md

385 lines
13 KiB
Markdown
Raw 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.
# 合并图片智能拆分功能
## 功能概述
增强了智能拆图功能,现在可以识别并还原从"合并为图片"功能创建的透明背景图片。
## 技术实现
### 1. 透明度检测
新增 `hasTransparency()` 函数用于检测图片是否包含透明度Alpha 通道):
```typescript
function hasTransparency(imageData: ImageData): boolean {
// 采样检测,每隔 10 个像素检测一次
// 如果超过 5% 的采样像素是透明的,认为图片有透明度
}
```
### 2. 透明分割线检测
新增透明分割线检测函数:
- `isTransparentPixel()` - 检测单个像素是否透明
- `getRowTransparentRatio()` - 获取一行的透明像素比例
- `getColTransparentRatio()` - 获取一列的透明像素比例
- `isHorizontalTransparentLine()` - 检测水平透明分割线(**要求 100% 透明**
- `isVerticalTransparentLine()` - 检测垂直透明分割线(**要求 100% 透明**
**重要**:透明分割线检测要求 `minTransparentRatio = 1.0`100% 透明),这是为了避免将图形/文字的细线误判为分割线。例如:
- ✅ 完全透明的分隔区域 → 识别为分割线
- ❌ 包含手绘线条的区域(部分透明)→ 不识别为分割线
- ❌ 包含文字笔画的区域(部分透明)→ 不识别为分割线
这比白色分割线检测90% 白色即可)更严格,确保不会切断图形和文字。
### 3. 透明边框裁剪
新增 `trimTransparentBorders()` 函数,专门用于裁剪透明边框:
```typescript
function trimTransparentBorders(imageData: ImageData): {
top: number;
right: number;
bottom: number;
left: number;
}
```
**重要**:对于透明背景的合并图片,**禁用边框裁剪**,因为:
1. 透明分割线检测已经足够精确(要求 100% 透明)
2. 如果再裁剪透明边框,会导致文字被截断
3. 文字字符之间的透明间隙会被误判为边框
**实现逻辑**
```typescript
// 根据图片类型选择 padding
// 透明图片:不使用 padding因为透明分割线检测已经足够精确100% 透明)
// 普通图片:使用较大的 padding 以确保裁剪干净
const splitLinePadding = hasAlpha ? 0 : 8;
if (hasAlpha) {
// 合并图片:不裁剪边框,保持完整
borders = { top: 0, right: sw - 1, bottom: sh - 1, left: 0 };
} else {
// 普通图片:裁剪白边和灰边
borders = trimBorders(regionData, 0.5, 0.15);
}
```
**为什么不使用 padding**
- ❌ 如果使用 padding跳过分割线附近的像素会导致文字边缘被截断
- ✅ 透明分割线检测要求 100% 透明,已经足够精确,不需要额外的 padding
- ✅ 直接使用检测到的分割线位置,保证内容完整
**过滤完全透明的子图片**
```typescript
// 检查是否完全透明(对于透明背景图片)
if (hasAlpha && isCompletelyTransparent(regionData)) {
console.log('[splitImageByLines] Skipping completely transparent region');
continue;
}
```
这样可以避免将贯通文字的透明分割线区域插入到画布,导致出现空白图片。
### 4. 智能检测流程
`detectGridLinesInternal()` 函数中,增强了分割线检测逻辑:
```typescript
// 检测图片是否有透明度(合并图片特征)
const hasAlpha = hasTransparency(imageData);
if (hasAlpha) {
// 合并图片:只检测透明分割线,避免误判白色内容为分割线
for (let y = marginY; y < height - marginY; y++) {
if (isHorizontalTransparentLine(imageData, y)) {
horizontalLines.push(y);
}
}
} else {
// 普通图片:检测白色分割线
for (let y = marginY; y < height - marginY; y++) {
if (isHorizontalSplitLine(imageData, y)) {
horizontalLines.push(y);
}
}
}
```
**重要**:当检测到透明度时,**只使用透明分割线检测**,不再检测白色分割线。这样可以避免将图片内容(如白色背景的人物)误判为分割线,防止"切人头"的问题。
### 5. 图片格式保留
`splitImageByLines()` 函数中,根据图片类型选择合适的输出格式:
```typescript
// 对于透明背景图片,保存为 PNG 以保留透明度
const imageFormat = hasAlpha ? 'image/png' : 'image/jpeg';
const imageQuality = hasAlpha ? undefined : 0.92;
elements.push({
imageData: canvas.toDataURL(imageFormat, imageQuality),
// ...
});
```
## 使用场景
### 场景 1合并多个元素
1. 用户选中多个元素(图片、形状、文本等)
2. 点击工具栏的"合并为图片"按钮
3. 系统使用 `toImage()` 生成透明背景的 PNG 图片
4. 合并后的图片插入到画布
### 场景 2还原合并图片
1. 用户选中合并后的图片
2. 点击工具栏的"智能拆图"按钮
3. 系统检测到图片有透明度
4. 使用透明区域作为分割线
5. 还原出原始的独立图片元素
## 支持的图片格式
智能拆图功能现在支持三种格式:
1. **网格分割线格式** - 白色分割线的宫格图
2. **透明分割线格式** - 合并图片的透明区域(新增)
3. **灵感图格式** - 灰色背景 + 白边框图片
## 技术优势
1. **自动识别** - 无需用户手动指定图片类型
2. **精确还原** - 使用透明区域精确定位原始元素边界
3. **格式保留** - 输出 PNG 格式保留透明度
4. **向后兼容** - 不影响现有的白色分割线检测逻辑
5. **防止误判** - 透明图片只检测透明分割线,避免"切人头"问题
6. **严格检测** - 要求 100% 透明,避免切断图形/文字的细线
7. **禁用递归** - 透明图片只拆分一次,保持文本框完整
8. **用户可控** - 如需进一步拆分,用户可手动再次操作
9. **保持布局** - 拆分后的子图片保持原图的相对位置和空间关系
## 关键设计决策
### 互斥检测策略
当检测到图片有透明度时,系统会**完全切换**到透明分割线检测模式,不再检测白色分割线。这是因为:
1. **合并图片特征**:通过"合并为图片"功能创建的图片,使用透明背景分隔元素
2. **避免误判**:如果同时检测白色分割线,会将图片内容(如白色背景的人物、白色衣服等)误判为分割线
3. **精确还原**:透明区域是合并时自动生成的,能够精确标记原始元素边界
### 透明度标记传递机制
为了确保递归拆分时保持一致性,系统实现了透明度标记传递机制:
1. **第一次检测**:在 `splitAndInsertImages` 中检测图片透明度
2. **标记传递**:将透明度标记 `hasTransparency` 添加到每个 `SplitImageElement`
3. **递归保持**:在 `recursiveSplitElement` 中,使用元素的透明度标记强制使用透明分割线检测
4. **从一而终**:整个拆分流程中,一旦检测到透明度,所有后续操作都使用透明分割线逻辑
**关键代码流程**
```typescript
// 1. 第一次检测获取透明度信息
const { detection, hasTransparency } = await detectGridLinesInternal(imageUrl);
// 2. 传递给 splitImageByLines
const elements = await splitImageByLines(imageUrl, detection, hasTransparency);
// 3. 每个子元素都带有透明度标记
elements.push({
imageData: canvas.toDataURL(imageFormat, imageQuality),
hasTransparency: hasAlpha, // 传递给子元素
// ...
});
// 4. 递归拆分时使用元素的透明度标记
const detection = await detectGridLines(imageUrl, element.hasTransparency);
const subElements = await splitImageByLines(imageUrl, detection, element.hasTransparency);
```
这样可以避免以下问题:
- ❌ 第一次拆分检测到透明度,使用透明分割线
- ❌ 第二次递归拆分时,子图片丢失透明度信息
- ❌ 回退到白色分割线检测,导致"切人头"
正确流程:
- ✅ 第一次拆分检测到透明度,使用透明分割线
- ✅ 透明度标记传递给所有子元素
- ✅ 第二次递归拆分时,强制使用透明分割线检测
- ✅ 完整还原所有图片,不会"切人头"
### 示例场景
**错误做法**(同时检测两种分割线):
```
合并图片(透明背景)
├── 人物图片(白色背景)← 被误判为分割线
└── 卡通图片
结果:人物的白色背景被当作分割线,导致人物被切碎
```
**正确做法**(只检测透明分割线):
```
合并图片(透明背景)
├── 人物图片(白色背景)← 保持完整
└── 卡通图片
结果:根据透明区域精确还原两张完整图片
```
### 严格的透明分割线检测
对于包含图形和文字的合并图片,透明分割线检测采用**100% 透明**的严格标准:
**问题场景**
```
合并图片(透明背景)
├── 手绘图形(细线条)
├── 文字内容(笔画)
└── 火柴人图形
```
如果使用 90% 透明的宽松标准:
- ❌ 手绘线条穿过的行/列会被误判为分割线
- ❌ 文字笔画穿过的行/列会被误判为分割线
- ❌ 结果:图形和文字被从中间切断
使用 100% 透明的严格标准:
- ✅ 只有完全透明的行/列才是分割线
- ✅ 任何包含内容的行/列都不会被切断
- ✅ 结果:图形和文字保持完整
**对比**
| 检测类型 | 阈值 | 适用场景 | 原因 |
|---------|------|---------|------|
| 白色分割线 | 90% | 宫格图 | 允许一定容差,适应扫描图片的噪点 |
| 透明分割线 | 100% | 合并图片 | 严格检测,避免切断图形/文字的细线 |
### 禁用递归拆分
对于透明背景的合并图片,系统会**禁用递归拆分**,只进行一次拆分:
**问题场景**
```
合并图片(透明背景)
└── 文本框
├── 第一行文字
├── 透明间隙(行间距)
├── 第二行文字
├── 透明间隙
└── 第三行文字
```
如果启用递归拆分:
- ❌ 第一次拆分:按元素边界拆分,得到文本框
- ❌ 第二次递归:检测到行间距的透明间隙
- ❌ 结果:文本框被切成多个单行文字碎片
禁用递归拆分:
- ✅ 第一次拆分:按元素边界拆分,得到完整文本框
- ✅ 不再递归:保持文本框完整
- ✅ 结果:文本框作为一个整体保留
**实现逻辑**
```typescript
if (hasTransparency) {
// 透明背景的合并图片:禁用递归拆分
console.log('[splitAndInsertImages] Transparent image detected, disabling recursive split');
elements = initialElements;
} else if (isStandardGrid) {
// 标准宫格:直接使用拆分结果
elements = initialElements;
} else {
// 非标准宫格:允许递归拆分
// 递归处理每个子元素...
}
```
**用户体验**
- ✅ 合并图片拆分后,文本框保持完整
- ✅ 如果用户需要进一步拆分文本框,可以手动再次点击"智能拆图"
- ✅ 避免自动过度拆分,导致碎片化
### 保持原图相对位置
拆分后的子图片会保持在原图中的相对位置,而不是重新排列成网格:
**传统网格布局**
```
原图布局:
[图1] [图2]
[图3] [图4]
拆分后(网格布局):
[图1] [图2]
[图3] [图4]
```
**保持相对位置**
```
原图布局:
[图1] [图2]
[图3] [图4]
拆分后(保持位置,整体下移):
原图位置
↓ +20px
[图1] [图2]
[图3] [图4]
```
**实现逻辑**
```typescript
// 基准位置:在源图片下方 20px
baseX = sourceRect.x;
baseY = sourceRect.y + sourceRect.height + 20;
// 计算子图片在原图中的相对位置(缩放后)
const relativeX = element.sourceX * scale;
const relativeY = element.sourceY * scale;
// 计算最终插入位置
const insertX = baseX + relativeX;
const insertY = baseY + relativeY;
// 插入图片
DrawTransforms.insertImage(board, imageItem, [insertX, insertY]);
```
**优势**
-**直观布局** - 拆分后的布局与原图一致,用户容易理解
-**保持结构** - 元素之间的空间关系得以保留
-**精确还原** - 完全还原原图的布局结构
-**无需调整** - 用户不需要手动重新排列元素
-**避免覆盖** - 拆分结果在原图下方,不会覆盖原图
-**便于对比** - 用户可以同时看到原图和拆分结果
## 调试日志
功能包含详细的调试日志,方便排查问题:
```typescript
console.log('[splitImageByLines] Image transparency detection:', { hasAlpha });
console.log('[splitImageByLines] Trimming transparent borders:', { row, col, borders });
console.log('[hasSplitLines] Detected grid lines:', { rows, cols });
```
## 性能优化
- 透明度检测使用采样算法,每隔 10 个像素检测一次
- 只在检测到透明度时才使用透明分割线检测
- 保持原有白色分割线检测的高效性
## 未来改进
1. 支持部分透明(半透明)的分割线检测
2. 优化透明度检测的采样策略
3. 支持更复杂的透明背景图片还原