13 KiB
合并图片智能拆分功能
功能概述
增强了智能拆图功能,现在可以识别并还原从"合并为图片"功能创建的透明背景图片。
技术实现
1. 透明度检测
新增 hasTransparency() 函数,用于检测图片是否包含透明度(Alpha 通道):
function hasTransparency(imageData: ImageData): boolean {
// 采样检测,每隔 10 个像素检测一次
// 如果超过 5% 的采样像素是透明的,认为图片有透明度
}
2. 透明分割线检测
新增透明分割线检测函数:
isTransparentPixel()- 检测单个像素是否透明getRowTransparentRatio()- 获取一行的透明像素比例getColTransparentRatio()- 获取一列的透明像素比例isHorizontalTransparentLine()- 检测水平透明分割线(要求 100% 透明)isVerticalTransparentLine()- 检测垂直透明分割线(要求 100% 透明)
重要:透明分割线检测要求 minTransparentRatio = 1.0(100% 透明),这是为了避免将图形/文字的细线误判为分割线。例如:
- ✅ 完全透明的分隔区域 → 识别为分割线
- ❌ 包含手绘线条的区域(部分透明)→ 不识别为分割线
- ❌ 包含文字笔画的区域(部分透明)→ 不识别为分割线
这比白色分割线检测(90% 白色即可)更严格,确保不会切断图形和文字。
3. 透明边框裁剪
新增 trimTransparentBorders() 函数,专门用于裁剪透明边框:
function trimTransparentBorders(imageData: ImageData): {
top: number;
right: number;
bottom: number;
left: number;
}
重要:对于透明背景的合并图片,禁用边框裁剪,因为:
- 透明分割线检测已经足够精确(要求 100% 透明)
- 如果再裁剪透明边框,会导致文字被截断
- 文字字符之间的透明间隙会被误判为边框
实现逻辑:
// 根据图片类型选择 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
- ✅ 直接使用检测到的分割线位置,保证内容完整
过滤完全透明的子图片:
// 检查是否完全透明(对于透明背景图片)
if (hasAlpha && isCompletelyTransparent(regionData)) {
console.log('[splitImageByLines] Skipping completely transparent region');
continue;
}
这样可以避免将贯通文字的透明分割线区域插入到画布,导致出现空白图片。
4. 智能检测流程
在 detectGridLinesInternal() 函数中,增强了分割线检测逻辑:
// 检测图片是否有透明度(合并图片特征)
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() 函数中,根据图片类型选择合适的输出格式:
// 对于透明背景图片,保存为 PNG 以保留透明度
const imageFormat = hasAlpha ? 'image/png' : 'image/jpeg';
const imageQuality = hasAlpha ? undefined : 0.92;
elements.push({
imageData: canvas.toDataURL(imageFormat, imageQuality),
// ...
});
使用场景
场景 1:合并多个元素
- 用户选中多个元素(图片、形状、文本等)
- 点击工具栏的"合并为图片"按钮
- 系统使用
toImage()生成透明背景的 PNG 图片 - 合并后的图片插入到画布
场景 2:还原合并图片
- 用户选中合并后的图片
- 点击工具栏的"智能拆图"按钮
- 系统检测到图片有透明度
- 使用透明区域作为分割线
- 还原出原始的独立图片元素
支持的图片格式
智能拆图功能现在支持三种格式:
- 网格分割线格式 - 白色分割线的宫格图
- 透明分割线格式 - 合并图片的透明区域(新增)
- 灵感图格式 - 灰色背景 + 白边框图片
技术优势
- 自动识别 - 无需用户手动指定图片类型
- 精确还原 - 使用透明区域精确定位原始元素边界
- 格式保留 - 输出 PNG 格式保留透明度
- 向后兼容 - 不影响现有的白色分割线检测逻辑
- 防止误判 - 透明图片只检测透明分割线,避免"切人头"问题
- 严格检测 - 要求 100% 透明,避免切断图形/文字的细线
- 禁用递归 - 透明图片只拆分一次,保持文本框完整
- 用户可控 - 如需进一步拆分,用户可手动再次操作
- 保持布局 - 拆分后的子图片保持原图的相对位置和空间关系
关键设计决策
互斥检测策略
当检测到图片有透明度时,系统会完全切换到透明分割线检测模式,不再检测白色分割线。这是因为:
- 合并图片特征:通过"合并为图片"功能创建的图片,使用透明背景分隔元素
- 避免误判:如果同时检测白色分割线,会将图片内容(如白色背景的人物、白色衣服等)误判为分割线
- 精确还原:透明区域是合并时自动生成的,能够精确标记原始元素边界
透明度标记传递机制
为了确保递归拆分时保持一致性,系统实现了透明度标记传递机制:
- 第一次检测:在
splitAndInsertImages中检测图片透明度 - 标记传递:将透明度标记
hasTransparency添加到每个SplitImageElement - 递归保持:在
recursiveSplitElement中,使用元素的透明度标记强制使用透明分割线检测 - 从一而终:整个拆分流程中,一旦检测到透明度,所有后续操作都使用透明分割线逻辑
关键代码流程:
// 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% | 合并图片 | 严格检测,避免切断图形/文字的细线 |
禁用递归拆分
对于透明背景的合并图片,系统会禁用递归拆分,只进行一次拆分:
问题场景:
合并图片(透明背景)
└── 文本框
├── 第一行文字
├── 透明间隙(行间距)
├── 第二行文字
├── 透明间隙
└── 第三行文字
如果启用递归拆分:
- ❌ 第一次拆分:按元素边界拆分,得到文本框
- ❌ 第二次递归:检测到行间距的透明间隙
- ❌ 结果:文本框被切成多个单行文字碎片
禁用递归拆分:
- ✅ 第一次拆分:按元素边界拆分,得到完整文本框
- ✅ 不再递归:保持文本框完整
- ✅ 结果:文本框作为一个整体保留
实现逻辑:
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]
实现逻辑:
// 基准位置:在源图片下方 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]);
优势:
- ✅ 直观布局 - 拆分后的布局与原图一致,用户容易理解
- ✅ 保持结构 - 元素之间的空间关系得以保留
- ✅ 精确还原 - 完全还原原图的布局结构
- ✅ 无需调整 - 用户不需要手动重新排列元素
- ✅ 避免覆盖 - 拆分结果在原图下方,不会覆盖原图
- ✅ 便于对比 - 用户可以同时看到原图和拆分结果
调试日志
功能包含详细的调试日志,方便排查问题:
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 个像素检测一次
- 只在检测到透明度时才使用透明分割线检测
- 保持原有白色分割线检测的高效性
未来改进
- 支持部分透明(半透明)的分割线检测
- 优化透明度检测的采样策略
- 支持更复杂的透明背景图片还原