Add comprehensive tests for image processing and mask functionalities

- Introduced tests for color image handling, ensuring source preservation and premultiplied alpha usage.
- Implemented tests for brush stroke operations, validating segment boundaries and brush anti-aliasing.
- Added tests for frame anti-aliasing, confirming correct rendering behavior with various alpha values and depth settings.
- Created tests for frame projection, ensuring accurate overlap calculations and half-plane clipping.
- Developed tests for image premultiplied retention, verifying behavior during loading and packing processes.
- Added undo functionality tests for mask operations, ensuring single transaction behavior during edits.
This commit is contained in:
2026-09-05 21:33:11 +08:00
parent bbaa4bff46
commit 1994d38ccd
111 changed files with 7691 additions and 2341 deletions

View File

@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-09-04

View File

@@ -0,0 +1,51 @@
## Context
Frame 使用逐像素有效投影和稳定深度顺序。结果 RGB 需要保存前景到最底层原色的完整渐变,以便 Mask Add 恢复 Alpha 后仍能自然显示。
## Goals / Non-Goals
**Goals:**
- 最底层原始 RGB 打底,前景按自身 Alpha 混合。
- Alpha 独立沿用现有 source-over 与数值判定。
- 复用当前投影、排序、分块采样和 Packed Image 生命周期。
**Non-Goals:**
交付范围为 Frame 的 RGB 合成及测试;Mask、Rectify 与输出 Canvas 延续既有规则。
## Decisions
### 1. 逐像素确定底层
复用 frame_source_coordinates 有效性和现有 order,第一个有效样本为底层。相同深度 active 在最上层,其余源沿用名称顺序。正交负深度、透视有效前方交点和局部覆盖按现有规则处理。
### 2. RGB 与 Alpha 独立累积
底层 RGB 使用未乘 Alpha 的双线性采样初始化。每个更近的前景使用预乘后插值的 P 与 A,执行 C = P + C × (1 - A)。底层自身不再叠加一次。
Alpha 从零开始,对包含底层的每一层执行 A_out = A_layer + A_out × (1 - A_layer)。最终 RGB 不除以该 Alpha。
```mermaid
flowchart TD
Input[源 RGBA 与有效投影] --> Order[逐像素从远到近排序]
Order --> Base[底层原始 RGB 初始化 C]
Order --> Alpha[所有层按 source-over 累积 Alpha]
Base --> Foreground[前景预乘样本叠加到 C]
Foreground --> Result[独立的 RGB 与 Alpha]
Alpha --> Result
Result --> Pack[边缘扩色与 Packed Image]
```
单源只有底层,保留其原始 RGB 采样。多源中前景的透明黑色邻居通过预乘采样排除;半透明前景持续混入底图白色,而不是等 Alpha 为零才切换颜色。
### 3. 源外扩色使用最终颜色
将合成颜色乘以最终 Alpha 交给现有 straight_rgba 处理 Alpha 数值判定与一圈扩色,再在全部有效覆盖位置写回合成颜色。这样源外扩色来自新的合成 RGB,源内透明色也得以保留。
## Risks / Trade-offs
- [底层重复叠加导致变暗] → RGB 从第二层开始叠加,Alpha 从第一层开始。
- [透明前景的黑背景污染插值] → 前景坚持预乘后插值,测试黑色透明邻居与白色底图。
- [恢复 Alpha 前的半透明显示也会改变] → RGB 已融合底色,Alpha 仍单独控制可见性;这是本次确认的颜色语义。
- [已合并图片无法找回分层信息] → 用户须用原始分层图片重新执行 Frame。

View File

@@ -0,0 +1,28 @@
## Why
Mask 保留了被剔除区域的 RGB,但后续 Frame 的预乘 Alpha 处理会丢失这些颜色,使用户再通过 Mask Add 恢复 Alpha 时无法找回图片内容。Frame 需要在透明结果中保留最底层图片的 RGB,同时保持前景渐变和既有 Alpha 合成。
## What Changes
- 对每个输出像素,以现有逐像素深度顺序中最底层的有效图片样本作为隐藏 RGB 来源。
- 以底层原始 RGB 打底,前景按自身 Alpha 连续混入底色,覆盖完全透明与半透明区域。
- RGB 与 Alpha 分开合成:前景使用预乘采样叠加到底色,Alpha 独立沿用原有 source-over;恢复 Alpha 后边缘保持渐变。
- 图片投射外的透明 Canvas 与现有边缘扩色继续保留;扩色仅填充没有有效源覆盖的透明区域。
- 补充单图 Mask → Frame → Mask Add、多图透明叠放、半透明混合和有效投影边界测试。
## Capabilities
### New Capabilities
- `frame-transparent-rgb`: 定义 Frame 透明像素的底层 RGB 来源、保留规则及其与 Alpha 合成、边缘扩色的关系。
### Modified Capabilities
无。
## Impact
- `src/anyimage/operators/image_edit_tool/frame.py`:保留原始颜色采样入口,复用当前深度排序和投影有效性,补全透明结果 RGB。
- `src/anyimage/common/image.py`:复用公共双线性采样与 RGBA 转换能力,保持其他调用方的现有结果。
- `tests/test_blender_addon.py`、`tests/test_cutout_objects.py`:像素计算及 Packed Image 编辑链路验证。
- 仅涉及 Blender 本地像素处理与测试;实施后运行测试,不构建文档或扩展包。

View File

@@ -0,0 +1,91 @@
## ADDED Requirements
### Requirement: Frame selects the bottom valid source for hidden RGB
Frame SHALL 在每个输出像素复用当前有效投影和从远到近的稳定深度顺序,以第一个几何有效样本作为底层 RGB 来源。底层判定 SHALL 独立于 Alpha,并 SHALL 沿用相同深度时 active 位于最上层、其他对象按名称稳定排序的规则。
#### Scenario: Transparent foreground overlaps a transparent background
- **WHEN** 蓝色底层和红色前景覆盖同一像素,且两者 Alpha 都为零
- **THEN** 输出 RGB SHALL 为底层蓝色
- **AND** 输出 Alpha SHALL 为零
#### Scenario: The farthest image does not cover a pixel
- **WHEN** 最远图片在某像素没有有效投影,而其他图片具有有效投影
- **THEN** 该像素的底层来源 SHALL 为实际覆盖它的最远有效图片
#### Scenario: Transparent images share the same depth
- **WHEN** active 与另一透明图片在相同深度覆盖同一像素
- **THEN** 底层 RGB SHALL 来自非 active 图片
- **AND** 多个非 active 同深度图片 SHALL 沿用现有名称排序确定底层
#### Scenario: Source planes cross
- **WHEN** 两张透明图片的平面相交,并在不同像素交换前后关系
- **THEN** 各像素的隐藏 RGB SHALL 分别来自该像素处最底层的有效图片
#### Scenario: Projection validity depends on the view mode
- **WHEN** 一个候选底层在透视视图中位于观察方向后方,或无法取得有限且位于源边界内的交点
- **THEN** 该样本 SHALL 不作为底层来源
- **AND** 正交视图中有限且位于源边界内的非正深度交点 SHALL 继续具有底层资格
### Requirement: Frame preserves bottom RGB in transparent output
Frame SHALL 在最终 straight RGBA 转换后 Alpha 为零且具有有效源覆盖的像素中,保存底层源未乘 Alpha 的 RGB 双线性采样结果。采样 SHALL 使用现有 Frame 投影坐标和边界约定;系统 SHALL 保持当前 Alpha 可见性数值判定与最终 Alpha 结果。
#### Scenario: A masked single image is framed without resampling displacement
- **WHEN** 单张图片经 Mask 剔除 Alpha 后,以原尺寸和像素中心对齐的映射执行 Frame
- **THEN** 透明区域的 RGB SHALL 与源图片对应像素一致
- **AND** Alpha SHALL 保持剔除后的结果
#### Scenario: Transparent source colors require interpolation
- **WHEN** Frame 缩放或透视映射在底层完全透明区域取得非整数像素采样位置
- **THEN** RGB SHALL 为底层原始 RGB 的双线性插值结果
- **AND** Alpha SHALL 保持零
#### Scenario: Mask Add follows a packed Frame result
- **WHEN** 用户对 Mask 剔除后经 Frame 生成的单图 Packed Image 执行 Mask Add
- **THEN** 提升 Alpha 的区域 SHALL 显示 Frame 保留的底层颜色
- **AND** 该颜色 SHALL 在 Packed Image 像素中实际存在
### Requirement: Frame blends foreground RGB over bottom color with independent alpha
Frame SHALL 以最底层有效源的原始 RGB 双线性样本初始化颜色,随后从远到近对每个前景计算 C = P_front + C × (1 - A_front),其中 P_front 为预乘后插值的 RGB。Alpha SHALL 从零开始对包括底层在内的全部有效层独立执行 source-over,并沿用现有数值判定。最终 RGB SHALL 不除以合成 Alpha。
#### Scenario: Both layers are partially transparent
- **WHEN** 蓝色底层 Alpha 为 0.5,红色前景 Alpha 为 0.25
- **THEN** 输出 Alpha SHALL 为 0.625
- **AND** straight RGB SHALL 为红色 0.25、绿色 0、蓝色 0.75
#### Scenario: A visible foreground overlaps a transparent bottom layer
- **WHEN** 蓝色底层 Alpha 为零,红色前景 Alpha 为 0.25
- **THEN** 输出 SHALL 为 RGB (0.25, 0, 0.75) 和 Alpha 0.25
- **AND** 恢复 Alpha 后 SHALL 保留相同 RGB
#### Scenario: Hidden neighbor colors meet a visible sampling edge
- **WHEN** 前景插值邻域同时含有可见红色像素和完全透明绿色像素
- **THEN** 前景采样 SHALL 沿用预乘插值结果并按采样 Alpha 混入底色
- **AND** 隐藏绿色 SHALL 不污染可见红色
#### Scenario: Restoring alpha reveals a continuous transition into white
- **WHEN** 前景原图的黑色背景被 Alpha 隐藏,前景边缘 Alpha 逐渐下降,且底层原始 RGB 为白色、Alpha 为零
- **THEN** RGB SHALL 随前景 Alpha 连续过渡到白色
- **AND** Packed Image 经 Mask Add 恢复 Alpha 后 SHALL 保留该过渡
### Requirement: Source coverage takes priority over edge color extension
Frame SHALL 保持完整 Canvas 和源外透明边缘扩色。存在有效源覆盖且最终透明的像素 SHALL 使用底层 RGB;没有有效源覆盖的像素 SHALL 保持 Alpha 零,并沿用紧邻可见内容的一圈 RGB 扩色,其余区域保持透明黑色。
#### Scenario: Transparent source pixels border visible content
- **WHEN** 源内部透明像素紧邻可见像素,且隐藏 RGB 与相邻可见颜色不同
- **THEN** 源内部透明像素 SHALL 保留底层原始 RGB
- **AND** 该 RGB SHALL 不被边缘扩色覆盖
#### Scenario: The frame extends beyond all source images
- **WHEN** Frame 包含所有源投影之外的区域
- **THEN** 外部像素 Alpha SHALL 为零
- **AND** 一圈扩色之外的无覆盖像素 SHALL 为透明黑色
- **AND** Canvas 尺寸 SHALL 保持原有 Frame 规则
#### Scenario: A visible edge crosses a processing chunk boundary
- **WHEN** 可见边缘跨越 Frame 行块边界
- **THEN** 透明边缘颜色 SHALL 与使用同一规则处理完整画布的结果一致

View File

@@ -0,0 +1,16 @@
## 1. RGB 合成修正
- [x] 1.1 复核有效投影、最底层选择和公共采样入口,保留已有几何行为。
- [x] 1.2 以底层原始 RGB 初始化,前景逐层按 Alpha 叠加;Alpha 独立沿用原公式,源外扩色使用最终颜色。
## 2. 验证
- [x] 2.1 更新半透明预期,补充三层合成、白色底图上的前景渐变、预乘插值和单源原始 RGB 测试。
- [x] 2.2 验证实际 Packed Image 在 Mask Add 后保留前景到白色的连续渐变。
- [x] 2.3 运行定向与全量测试,校验 OpenSpec;不改产品文档,不构建产物。
## 验证结果
- 定向测试:51 passed;全量 uv run pytest:440 passed。
- 实际 Packed PNG 与 Mask Add 结果均验证了前景到白色的连续 RGB 渐变,Alpha 合成保持独立。
- 未做 Blender 视口人工验收,未构建产物。