Files
TrueGrowth/docs/MERGED_IMAGE_SPLITTING.md

13 KiB
Raw Blame History

合并图片智能拆分功能

功能概述

增强了智能拆图功能,现在可以识别并还原从"合并为图片"功能创建的透明背景图片。

技术实现

1. 透明度检测

新增 hasTransparency() 函数用于检测图片是否包含透明度Alpha 通道):

function hasTransparency(imageData: ImageData): boolean {
  // 采样检测,每隔 10 个像素检测一次
  // 如果超过 5% 的采样像素是透明的,认为图片有透明度
}

2. 透明分割线检测

新增透明分割线检测函数:

  • isTransparentPixel() - 检测单个像素是否透明
  • getRowTransparentRatio() - 获取一行的透明像素比例
  • getColTransparentRatio() - 获取一列的透明像素比例
  • isHorizontalTransparentLine() - 检测水平透明分割线(要求 100% 透明
  • isVerticalTransparentLine() - 检测垂直透明分割线(要求 100% 透明

重要:透明分割线检测要求 minTransparentRatio = 1.0100% 透明),这是为了避免将图形/文字的细线误判为分割线。例如:

  • 完全透明的分隔区域 → 识别为分割线
  • 包含手绘线条的区域(部分透明)→ 不识别为分割线
  • 包含文字笔画的区域(部分透明)→ 不识别为分割线

这比白色分割线检测90% 白色即可)更严格,确保不会切断图形和文字。

3. 透明边框裁剪

新增 trimTransparentBorders() 函数,专门用于裁剪透明边框:

function trimTransparentBorders(imageData: ImageData): {
  top: number;
  right: number;
  bottom: number;
  left: number;
}

重要:对于透明背景的合并图片,禁用边框裁剪,因为:

  1. 透明分割线检测已经足够精确(要求 100% 透明)
  2. 如果再裁剪透明边框,会导致文字被截断
  3. 文字字符之间的透明间隙会被误判为边框

实现逻辑

// 根据图片类型选择 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合并多个元素

  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. 从一而终:整个拆分流程中,一旦检测到透明度,所有后续操作都使用透明分割线逻辑

关键代码流程

// 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 个像素检测一次
  • 只在检测到透明度时才使用透明分割线检测
  • 保持原有白色分割线检测的高效性

未来改进

  1. 支持部分透明(半透明)的分割线检测
  2. 优化透明度检测的采样策略
  3. 支持更复杂的透明背景图片还原