385 lines
13 KiB
Markdown
385 lines
13 KiB
Markdown
# 合并图片智能拆分功能
|
||
|
||
## 功能概述
|
||
|
||
增强了智能拆图功能,现在可以识别并还原从"合并为图片"功能创建的透明背景图片。
|
||
|
||
## 技术实现
|
||
|
||
### 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. 支持更复杂的透明背景图片还原
|