Files
cmbot/docs/08-image-editor-design.md
T

603 lines
16 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. 文档定位
本文档定义合成预览区的实现规则,包括衣服底图显示、印花图层拖动、缩放、旋转、坐标换算、参数同步,以及预览和最终导出保持一致的规则。
后续任何涉及 `image_canvas.py`、印花变换、坐标换算、图片预览或最终合成一致性的任务,都必须阅读本文档。
## 2. 设计目标
- 用户可以在预览区直接拖动、缩放、旋转印花。
- 参数面板和预览区双向同步。
- 预览区缩放不影响真实合成参数。
- 所有合成参数以衣服底图原始像素坐标系保存。
- 最终导出结果与预览中的衣服、印花位置、尺寸、角度一致。
- 图片合成逻辑可以脱离 GUI 单独测试。
## 3. 核心原则
必须遵守:
- Qt 预览负责交互和显示。
- Pillow 负责最终图片合成和导出。
- 禁止截图预览区作为最终导出图片。
- 禁止把屏幕坐标直接保存为合成坐标。
- 禁止把预览缩放比例写入真实合成参数。
- `TransformState` 是唯一可信的印花变换状态。
## 4. 坐标系定义
图像编辑器涉及三套坐标系:
### 4.1 屏幕坐标
屏幕坐标是鼠标事件中的窗口坐标或控件坐标。
用途:
- 接收鼠标点击、拖动、滚轮事件。
- 判断用户操作了哪个控制点。
规则:
- 屏幕坐标只能作为交互输入。
- 屏幕坐标不能直接用于最终合成。
### 4.2 Scene 坐标
Scene 坐标是 `QGraphicsScene` 中的坐标。
用途:
- 显示衣服底图。
- 显示印花图层。
- 显示选中框、缩放手柄和旋转手柄。
推荐规则:
- `QGraphicsScene` 的坐标应尽量与衣服底图原始像素坐标保持一致。
- 衣服底图左上角放在 scene 坐标 `(0, 0)`。
- 衣服底图宽高等于原始图片像素宽高。
- 画布缩放通过 `QGraphicsView` 的 view transform 实现,不修改 scene 中图片真实尺寸。
### 4.3 原图像素坐标
原图像素坐标是衣服底图原始图片的像素坐标。
用途:
- 保存印花位置。
- 保存印花尺寸。
- 执行最终 Pillow 合成。
- 保存模板参数。
规则:
- `TransformState.x` 和 `TransformState.y` 使用原图像素坐标。
- `TransformState.width` 和 `TransformState.height` 使用原图像素尺寸。
- `TransformState.rotation` 使用角度,单位为度。
## 5. 图层结构
预览区使用 `QGraphicsView / QGraphicsScene`。
图层顺序:
```text
背景棋盘格
衣服底图 Item
印花图片 Item
印花选中框 Item
缩放控制点 Item
旋转控制点 Item
操作提示 Overlay
```
说明:
- 背景棋盘格可以由 view 背景或 scene 背景绘制。
- 衣服底图不可拖动。
- 印花图片可拖动、缩放、旋转。
- 选中框和控制点仅用于交互,不参与最终导出。
- 操作提示仅显示在 UI 中,不参与最终导出。
### 5.1 预览控制层显隐
预览区的印花选中框、缩放控制点、旋转连接线和旋转控制点属于交互控制层。该控制层只帮助用户识别和操作当前印花,不属于图片内容。
按键行为:
- 当预览区拥有键盘焦点时,按 `Esc` 应隐藏当前印花的交互控制层。
- `Esc` 只隐藏印花选中框、缩放控制点、旋转连接线和旋转控制点,不隐藏印花图片本身。
- `Esc` 不执行撤销,不回滚当前拖动、缩放或旋转结果,不修改 `TransformState`。
- 如果焦点在参数输入框、文件列表或其他控件中,预览区可以不响应该次 `Esc`。
焦点规则:
- `QGraphicsView` 应允许获得键盘焦点。
- 用户点击预览区时,预览区应获得焦点,以便后续 `Esc` 能被画布接收。
恢复规则:
- 控制层被隐藏后,用户再次点击预览区中的印花图片,应恢复控制层,并继续沿用原有拖动逻辑。
- 控制层隐藏期间,隐藏的缩放控制点和旋转控制点不需要响应命中测试;恢复后原有缩放、旋转行为保持不变。
- 重新加载印花图片或创建新的印花图层时,控制层默认显示。
一致性规则:
- 控制层显隐状态不参与模板保存、队列项保存、单张导出或批量导出。
- 最终导出仍只读取 `TransformState` 和原始图片,不读取控制层显隐状态。
## 6. 状态模型
印花变换状态使用统一模型表示:
```text
TransformState
x: float
y: float
width: float
height: float
rotation: float
keep_aspect_ratio: bool
```
字段含义:
- `x`:印花未旋转包围盒左上角在衣服原图中的 X 坐标。
- `y`:印花未旋转包围盒左上角在衣服原图中的 Y 坐标。
- `width`:印花缩放后的宽度。
- `height`:印花缩放后的高度。
- `rotation`:围绕印花中心旋转的角度,单位为度。
- `keep_aspect_ratio`:是否锁定宽高比例。
规则:
- UI 面板、预览区和导出逻辑必须共享同一个 `TransformState`。
- 预览区交互完成后更新 `TransformState`。
- 参数面板输入变化后更新 `TransformState`。
- `TransformState` 变化后统一刷新预览。
## 7. 印花 Item 设计
建议实现自定义印花图层:
```text
TransformablePrintItem
```
职责:
- 显示印花图片。
- 接收拖动、缩放、旋转交互。
- 绘制或管理选中框。
- 发出变换状态变化信号。
不负责:
- 不负责最终 Pillow 导出。
- 不负责批量任务。
- 不负责读取文件夹。
- 不负责保存模板文件。
## 8. 拖动设计
### 8.1 操作行为
用户按住印花主体区域并拖动,移动印花位置。
要求:
- 拖动过程中实时移动印花。
- 拖动过程中可以实时更新状态,也可以拖动结束后统一提交。
- 拖动结束后必须同步 X/Y 输入框。
- 拖动不改变宽度、高度和旋转角度。
### 8.2 坐标更新
拖动位移应在 scene 坐标中计算。
推荐公式:
```text
new_x = old_x + delta_scene_x
new_y = old_y + delta_scene_y
```
由于 scene 坐标与原图像素坐标一致,`new_x` 和 `new_y` 可以直接写入 `TransformState`。
规则:
- 不强制限制印花必须完全在衣服底图内。
- 导出时只保留衣服画布范围内的内容。
- 如需边界吸附,应作为后续增强功能,不作为第一阶段必需。
## 9. 缩放设计
### 9.1 控制点
选中印花时显示 8 个缩放控制点:
```text
左上、上中、右上、右中、右下、下中、左下、左中
```
第一阶段至少需要支持四角缩放:
```text
左上、右上、右下、左下
```
### 9.2 缩放行为
要求:
- 默认锁定宽高比例。
- 勾选 `锁定宽高比例` 时,拖动四角控制点按原始比例缩放。
- 取消锁定后,可以独立改变宽度和高度。
- 缩放后同步宽度和高度输入框。
- 缩放不改变旋转角度。
### 9.3 最小尺寸
必须设置最小尺寸,避免印花缩放为 0 或负数。
建议:
```text
min_width = 10 px
min_height = 10 px
```
### 9.4 缩放中心
第一阶段推荐使用对角固定缩放:
- 拖动右下角时,左上角保持不动。
- 拖动左上角时,右下角保持不动。
- 拖动右上角时,左下角保持不动。
- 拖动左下角时,右上角保持不动。
若实现复杂度过高,可第一阶段使用中心缩放,但必须保证参数和导出一致。
### 9.5 旋转状态下缩放
旋转后的缩放更复杂。第一阶段推荐规则:
- 控制点跟随旋转后的选中框显示。
- 计算缩放时先将鼠标点从 scene 坐标转换到印花本地坐标。
- 在印花本地坐标中计算新的宽高。
- 再更新 `TransformState`。
如果第一阶段暂不实现旋转状态下的精确控制点缩放,必须在实现说明中明确限制,并保证输入框缩放和导出仍然正确。
## 10. 旋转设计
### 10.1 控制点
旋转控制点位于印花选中框顶部中心外侧,通过一条短线连接选中框。
### 10.2 旋转中心
旋转中心为印花缩放后矩形的中心点。
计算:
```text
center_x = x + width / 2
center_y = y + height / 2
```
### 10.3 鼠标旋转
用户拖动旋转控制点时,根据鼠标当前位置和中心点计算角度。
推荐公式:
```text
angle = atan2(mouse_y - center_y, mouse_x - center_x)
rotation = angle_to_degrees(angle) + offset
```
`offset` 用于让控制点初始方向和视觉方向一致。
要求:
- 拖动时实时更新预览。
- 旋转结束后同步角度输入框。
- 角度可以为负数。
- 内部可以归一化到 `-180..180` 或 `0..360`,但 UI 表示要一致。
### 10.4 按钮旋转
右侧参数栏包含:
- `左旋 90°`
- `右旋 90°`
规则:
```text
左旋 90°:rotation = rotation - 90
右旋 90°:rotation = rotation + 90
```
更新后必须刷新预览并同步输入框。
## 11. 参数面板同步
参数面板包含:
- X 坐标。
- Y 坐标。
- 宽度。
- 高度。
- 锁定宽高比例。
- 角度。
同步规则:
```text
预览区交互 -> 更新 TransformState -> 更新参数面板
参数面板输入 -> 更新 TransformState -> 刷新预览区
模板选择 -> 生成 TransformState -> 刷新预览区和参数面板
队列项选择 -> 加载该项 TransformState -> 刷新预览区和参数面板
```
防循环规则:
- 参数面板程序化更新输入框时,应避免触发重复变更。
- 可以使用 `blockSignals(True/False)` 或内部更新标记。
## 12. 预览缩放
预览区支持画布缩放,例如:
```text
82%
```
规则:
- 预览缩放只改变 `QGraphicsView` 的显示变换。
- 预览缩放不改变 `TransformState.x/y/width/height`。
- 鼠标事件需要通过 Qt 的坐标映射转换回 scene 坐标。
推荐:
```text
view.mapToScene(mouse_pos)
```
## 13. 最终导出设计
最终导出由 Pillow 执行,不使用 Qt 截图。
### 13.1 单张合成输入
```text
garment_path
print_path
TransformState
ExportOptions
```
### 13.2 合成步骤
推荐流程:
```text
读取衣服底图为 RGBA
读取印花图为 RGBA
将印花缩放到 TransformState.width / height
按 TransformState.rotation 围绕中心旋转
计算旋转后图片应粘贴到衣服底图上的位置
使用 alpha_composite 合成
根据 ExportOptions 保存 PNG 或 JPG
```
### 13.3 旋转后粘贴位置
Pillow 旋转通常会产生新的包围盒。
规则:
- `TransformState.x/y/width/height` 表示旋转前矩形。
- 旋转中心为该矩形中心。
- 旋转后图片粘贴位置应保证旋转中心仍落在同一个中心点。
计算:
```text
center_x = x + width / 2
center_y = y + height / 2
paste_x = center_x - rotated_width / 2
paste_y = center_y - rotated_height / 2
```
### 13.4 输出格式
PNG:
- 保留 RGBA 或按需求输出不透明图。
JPG:
- JPG 不支持透明通道。
- 保存 JPG 前必须合成到 RGB 背景。
- 默认背景建议使用白色,除非后续有其他需求。
## 14. 预览与导出一致性
必须保证:
- Qt 中印花显示位置来自 `TransformState`。
- Pillow 导出也使用同一个 `TransformState`。
- 两边使用相同的旋转中心。
- 两边使用相同的宽度、高度和角度定义。
- 预览缩放比例不参与导出。
允许存在的差异:
- Qt 和 Pillow 的抗锯齿细节可能略有不同。
- 字节级像素不要求完全一致。
不允许存在的差异:
- 印花明显偏移。
- 印花尺寸明显不同。
- 旋转中心不同。
- 透明区域显示异常。
## 15. 队列项微调
UI 设计稿中存在 `已微调` 状态。
定义:
- 当用户选中队列中的某一项,并手动调整位置、尺寸或旋转后,该队列项标记为 `已微调`。
- `已微调` 队列项应保存自己的 `TransformState`。
- 重新应用模板时,是否覆盖 `已微调` 项需要用户确认或遵循明确规则。
第一阶段推荐规则:
- 当前队列项被手动调整后标记为 `已微调`。
- 批量导出时,已微调项使用自己的 `TransformState`。
- 未微调项使用当前模板生成的 `TransformState`。
## 16. 低对比组合筛选设计
### 16.1 问题定义
当衣服目标区域颜色和印花主要颜色接近时,用户即使完成合成,也可能看不清印花效果。软件应支持在导出前先筛选出这类组合,帮助用户决定是否跳过、替换印花或调整参数。
该功能是筛选和提示能力,不应自动修改素材,不应直接替用户删除队列项。
### 16.2 分析范围
衣服颜色分析不应使用整张衣服图,而应使用当前印花将要覆盖的衣服区域。
输入:
```text
garment_path
print_path
TransformState
```
推荐区域:
```text
garment_region = x, y, width, height
```
印花颜色分析应只统计有效像素:
```text
alpha > 20
```
这样可以避免透明 PNG 的透明区域干扰颜色判断。
### 16.3 第一阶段算法
第一阶段推荐使用简单稳定的组合指标:
```text
RGB 颜色距离
亮度差
```
示例判断:
```text
如果 RGB 距离 < 45 且亮度差 < 35
=> 标记为 不明显
```
可见度分级:
```text
正常
偏低
不明显
```
阈值应集中定义,避免散落在 UI 或批量任务代码中。
### 16.4 更准确的后续算法
后续可以在内存中模拟合成但不保存文件:
```text
按 TransformState 缩放、旋转印花
将印花在内存中叠加到衣服目标区域
比较合成前后的目标区域差异
如果差异很小,标记为不明显
```
这种方式比单纯比较主色更准确,因为它考虑透明度、旋转、图案面积和实际覆盖范围。
### 16.5 队列集成
低对比筛选应发生在队列生成后、批量导出前:
```text
生成合成队列
-> 对每个队列项计算可见度评分
-> 标记 正常 / 偏低 / 不明显
-> 用户筛选或跳过低对比项
-> 执行批量导出
```
要求:
- 单个组合分析失败不应中断整个队列。
- 分析失败应记录日志,并在队列项中显示可理解状态。
- 用户应能查看被标记为 `偏低` 或 `不明显` 的组合。
- 用户应能决定是否跳过这些组合。
## 17. 边界情况
必须处理:
- 印花部分超出衣服画布。
- 印花完全超出衣服画布。
- 印花透明 PNG。
- 衣服图和印花图尺寸很大。
- 文件路径包含中文。
- 宽度或高度输入为 0、负数或非数字。
- 旋转角度超过 360 度。
- 衣服目标区域和印花有效区域颜色接近,导致低对比。
- 印花有效像素过少,无法可靠判断主色。
处理规则:
- 部分超出画布允许导出,只保留画布内内容。
- 完全超出画布时可以导出空印花效果,但应提示用户确认或标记风险。
- 非法尺寸输入应阻止提交并提示。
- 角度可以归一化显示。
- 低对比组合应提示或标记,但不自动删除。
- 印花有效像素过少时应标记为无法判断,并记录日志。
## 18. 测试建议
核心测试:
- 未旋转印花合成位置正确。
- 缩放后导出尺寸正确。
- 旋转 90 度后中心不偏移。
- 旋转 -8 度后预览和导出大体一致。
- 印花部分超出画布时不会报错。
- 透明 PNG 不出现黑底或白底。
- 预览缩放到 50% 或 200% 后,拖动仍能得到正确原图坐标。
- 参数面板输入后预览同步。
- 预览拖动后参数面板同步。
- 点击预览区后按 `Esc`,印花选中框、缩放控制点和旋转控制点隐藏,但印花仍可见。
- 控制层隐藏后再次点击印花,控制层恢复,拖动、缩放、旋转仍按原有规则工作。
- 控制层显隐不改变 `TransformState`,也不影响导出结果。
- 白色衣服配浅色印花时能标记为低对比或不明显。
- 深色衣服配深色印花时能标记为低对比或不明显。
- 透明 PNG 的透明区域不参与印花主色判断。