Initial TrueGrowth source import

This commit is contained in:
2026-07-07 09:36:36 +08:00
commit 3b6781d695
2283 changed files with 691996 additions and 0 deletions

View File

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