feat(selection): separate selection geometry from image content processing
- Introduced a new design for selection rasterization that decouples the selection geometry from the image content, ensuring that the SelectionMask is independent of the source image's alpha and color. - Updated the `rasterize_selection_path` function to only accept selection paths and image dimensions, removing parameters related to source alpha and alpha thresholds. - Implemented a new approach for handling selection paths using non-zero winding fill, ensuring overlapping areas are preserved. - Enhanced performance by utilizing a scanline algorithm for rasterization, significantly reducing processing time for high-resolution images. - Updated internal documentation and added comprehensive tests to validate the new behavior and performance improvements. - Ensured backward compatibility by maintaining existing interfaces where necessary while introducing breaking changes in the selection processing logic.
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-09-01
|
||||
78
openspec/changes/simplify-selection-rasterization/design.md
Normal file
78
openspec/changes/simplify-selection-rasterization/design.md
Normal file
@@ -0,0 +1,78 @@
|
||||
## Context
|
||||
|
||||
Selection 手势当前构建带 `points`、`invert`、`fill_rule` 和 JSON 方法的 `SelectionPath`。Image Tool 默认使用 `EVEN_ODD`,Cutout 使用 `NONZERO`;两者只在自交或重复绕行时不同。产品决定统一保留同方向重叠区域,因此删除 `EVEN_ODD`,同时保留 `SelectionPath` 及其 JSON 方法,继续把路径点和反选状态作为不可分割的值。
|
||||
|
||||
当前栅格器已经取得 Path bounds,却仍创建整图 Mask,并对每条路径边构造“扫描行块 × 选区宽度”的二维布尔数组。实测 5120×2880、1000 点 Lasso 的完整处理约为 4.07 秒。局部 non-zero scanline 原型通过每条边只记录实际穿过扫描行的区间端点,再对每行累计一次,在 960 组路径、Invert、抗锯齿、Alpha 和 bounds 组合中与目标 non-zero 结果逐值一致,同一 5K 用例约为 0.16 秒。
|
||||
|
||||
Cutout Pie Menu 的按钮会启动新的 Blender Operator,普通 Python 对象不能直接跨该 RNA 边界。现有完整 Path JSON 已同时保存 Points 与 Invert,千点路径编码实测约 0.6 ms;保留 `to_json()` / `from_json()` 比拆分多个 Property 或增加 RNA Collection 更直接。
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
|
||||
- 所有 Selection 使用 non-zero winding,保留同方向重叠区域。
|
||||
- 运行时传递只包含普通二维点数组和 `invert` 的精简 `SelectionPath`,删除 `fill_rule`。
|
||||
- Cutout 只在 Pie Menu 到 Shape Operator 的必要边界编码一次完整 Selection Path。
|
||||
- 将 Path-to-Mask 的计算量从逐边二维像素广播改为扫描行交点累计,并尽可能限制在局部画布。
|
||||
- 保持现有像素中心、Invert、抗锯齿核、Alpha 阈值、空 Selection、最终 bounds 和下游输出语义。
|
||||
|
||||
**Non-Goals:**
|
||||
|
||||
- 不缓存 Blender 图片像素或 Selection,不引入令牌和全局状态。
|
||||
- 不改变 Lasso 采样与简化容差,不增加 Pillow、OpenCV 或 GPU 路径。
|
||||
- 不优化 Blender `image.pixels.foreach_get()`、图片写回、Pack、Cutout 网格或 AI Job。
|
||||
- 不保留旧 `fill_rule` 或兼容填充分支。
|
||||
|
||||
## Decisions
|
||||
|
||||
### 1. Selection Path 只保存 Points 与 Invert
|
||||
|
||||
`SelectionPath` 保留为不可变 dataclass,只包含 `points: tuple[tuple[float, float], ...]` 与 `invert: bool`,以及完整值的 `to_json()` / `from_json()`。构建时继续把点规范化为有限浮点二维数组、检查至少三个点,并固定反选值。共享手势完成边界提交一个完整 `SelectionPath`,Image Tool 在同一 Operator 调用栈内直接消费它。
|
||||
|
||||
`invert` 决定路径内部还是外部属于 Selection,与 `points` 共同定义同一个 Selection Path。保留二者的值对象及现有序列化边界可以避免调用链漏传或错配反选状态;删除 `fill_rule` 后,该类型不再承担工具填充策略。
|
||||
|
||||
### 2. Cutout 序列化完整 Selection Path
|
||||
|
||||
`open_shape_pie` 对 `selection_path` 调用一次 `to_json()`,把同一个字符串写入各 Shape Operator 的 `selection_path_json`。`CutoutSelectionToShape` 通过 `SelectionPath.from_json()` 恢复 Points 与 Invert 后交给栅格器。JSON 保留 `points` 和 `invert`,只删除 `fill_rule`。
|
||||
|
||||
相比拆分 Points JSON 与 Bool Property,此方案不会产生两个必须同步的传输值;相比 RNA Collection,它不增加 PropertyGroup、注册顺序和每按钮逐点复制;相比令牌或全局表,它没有生命周期与菜单取消清理问题。
|
||||
|
||||
### 3. 使用 non-zero scanline 端点累计
|
||||
|
||||
栅格器先裁切路径的几何 bounds,并以像素中心 `x + 0.5`、`y + 0.5` 判断边界。每条非水平边只处理其实际跨越的扫描行,计算交点对应的水平区间终点;按边方向在该行的起点和终点写入正负 winding 变化。全部边处理完成后,每行只执行一次水平累计,累计值非零的像素即为 Selection。
|
||||
|
||||
这与当前 `NONZERO` 的射线 crossing 定义相同,但不再为每条边生成二维 `selected` 数组。相比 Pillow,此实现可以明确固定 winding、严格不等号与像素中心语义,并且无需为自交行为依赖第三方未公开约定。
|
||||
|
||||
### 4. 非 Invert 使用局部画布
|
||||
|
||||
普通 Selection 只创建裁切到图片范围的 Path bounds;启用抗锯齿时在四周增加核半径 2 px,使局部卷积与整图卷积等价。源 Alpha 只切片到同一局部范围。由于 Invert 的有效区域可能覆盖路径外的整图,Invert 继续使用完整图片范围,但仍采用单次 scanline 累计。
|
||||
|
||||
最终可见 bounds 使用按行和按列的 `any()` 后查找首尾非空索引,不再通过 `np.nonzero()` 为每个命中像素创建两组坐标。返回值继续是局部 `SelectionMask(values, bounds)`。
|
||||
|
||||
### 5. 用结果等价和工作边界验证性能
|
||||
|
||||
小尺寸参考实现覆盖普通、凹、自交、重复绕行、越界、水平边、Invert、抗锯齿、Alpha 阈值和空 Selection,并逐值比较 Mask 与 bounds。高分辨率测试验证中间工作数组只按目标画布建立、每条边只产生其跨行事件,不设置依赖 CPU 的秒数断言。
|
||||
|
||||
开发验证继续记录代表性 5K/1000 点 Lasso 的基准,用于确认实际数量级改善,但不把机器耗时写成测试契约。
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- [Image Tool 自交路径结果改变] → 这是明确的产品决策;更新预览和最终 Mask 测试,使重叠区始终保留。
|
||||
- [局部抗锯齿在边缘与旧结果不同] → 按固定核半径扩展局部画布,并用逐值测试覆盖图片边缘和越界路径。
|
||||
- [Invert 无法取得同等局部内存收益] → 保留整图画布以维持语义,但仍消除逐边二维广播和巨大 `nonzero` 坐标数组。
|
||||
- [扫描线交点在整数顶点重复计数] → 沿用半开 crossing 条件和像素中心判定,以参考实现测试水平边、共顶点与自交路径。
|
||||
- [Path JSON 仍包含已删除的填充规则] → 更新序列化测试,验证载荷只包含 Points 与 Invert,且不保留旧填充分支。
|
||||
|
||||
## Migration Plan
|
||||
|
||||
1. 将预览与 Mask 参考语义统一为 non-zero,调整差异测试为重叠区保留测试。
|
||||
2. 将 `SelectionPath` 精简为 Points 与 Invert,删除 `fill_rule` 并更新 JSON 载荷,迁移共享手势、Image Tool 与 Cutout 的参数边界。
|
||||
3. 保持 Cutout 的单一 Path JSON Property,由其恢复精简 Path,删除旧填充字段解析。
|
||||
4. 实现局部 scanline、局部抗锯齿/Alpha 和低内存 bounds,迁移所有调用方。
|
||||
5. 更新测试和内部文档,运行相关测试、完整测试及代表性 5K 基准。
|
||||
|
||||
回退时整体恢复旧 Selection Path 字段和栅格器,不保留两套入口或兼容层。
|
||||
|
||||
## Open Questions
|
||||
|
||||
无。
|
||||
@@ -0,0 +1,28 @@
|
||||
## Why
|
||||
|
||||
当前 Selection 栅格化按路径的每条边重复扫描二维像素区域,5K 图片上的千点 Lasso 单次处理约需 4 秒并产生大量整图临时数组。`SelectionPath` 需要共同保存路径点与反选状态,但其中的两种填充规则没有继续保留的价值;产品实际期望所有重叠圈选区域都被保留。
|
||||
|
||||
## What Changes
|
||||
|
||||
- **BREAKING**:所有 Selection 统一使用 non-zero winding 填充,删除 `EVEN_ODD`、`fill_rule` 及其分支;自交或重复绕行的同方向重叠区域始终保留。
|
||||
- **BREAKING**:将 `SelectionPath` 精简为只包含普通二维点数组和 `invert` 的不可变值,删除 `fill_rule`,保留完整 Path 的 `to_json()` / `from_json()`。
|
||||
- Cutout Pie Menu 继续把完整 `SelectionPath` 编码一次并交给 Shape Operator,不拆分 Points 与 Invert,也不引入 RNA Collection、令牌或额外状态。
|
||||
- 使用按扫描行交点累计的局部 scanline 算法替换逐边二维像素广播,并保持现有像素中心判定、Invert、抗锯齿、Alpha 阈值、空 Selection 与最终 bounds 语义。
|
||||
- 抗锯齿、源 Alpha 合并和可见 bounds 计算只处理必要的局部画布;Invert Selection 仍按完整图片范围处理。
|
||||
- 增加逐像素等价性、不同路径规模的复杂度边界和高分辨率性能回归测试,不使用机器相关的固定耗时阈值。
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
|
||||
- `selection-rasterization`: 规定精简 Selection Path 的统一 non-zero 填充语义、局部 scanline 栅格化结果和高分辨率复杂度边界。
|
||||
|
||||
### Modified Capabilities
|
||||
|
||||
无。
|
||||
|
||||
## Impact
|
||||
|
||||
- 影响 `common/selection.py`、`common/viewport.py`、Image Tool、Cutout Tool 及其交互测试和内部文档。
|
||||
- Cutout Operator 的内部 Selection 参数会改变;它不是公开用户 API,但旧 Operator 参数不再兼容。
|
||||
- 不新增运行时依赖,不修改 AI Job 协议、Cutout Shape、Mask 抗锯齿核或最终图片与几何处理链。
|
||||
@@ -0,0 +1,64 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Selection uses one overlap-preserving fill
|
||||
系统 SHALL 使用 non-zero winding 栅格化所有 Selection Path,使同方向自交或重复绕行的重叠区域保持选中,并且不得暴露可切换的填充规则。
|
||||
|
||||
#### Scenario: Lasso overlaps itself
|
||||
- **WHEN** Image Tool 或 Cutout 的 Selection 路径以同一方向重复包围一个区域
|
||||
- **THEN** 该重叠区域在 Viewport 预览和最终 Selection Mask 中均保持选中
|
||||
|
||||
#### Scenario: Simple path is selected
|
||||
- **WHEN** Selection 路径是普通矩形、半平面、凹多边形或不自交 Lasso
|
||||
- **THEN** 系统产生与既有 non-zero 填充相同的选中区域
|
||||
|
||||
### Requirement: Selection Path contains its inversion state
|
||||
系统 SHALL 使用只包含普通二维点数组和 Invert 的不可变 Selection Path 值在用户手势与实际 Image 或 Cutout 操作之间传递 Selection;该类型 SHALL 保留完整值的 JSON 编解码,但不得包含填充规则。
|
||||
|
||||
#### Scenario: Image Tool submits a Selection
|
||||
- **WHEN** 用户完成 Image Tool 手势
|
||||
- **THEN** 同一 Operator 把包含 Points 与 Invert 的 Selection Path 直接交给实际图片编辑
|
||||
|
||||
#### Scenario: Cutout menu creates Shape operations
|
||||
- **WHEN** Cutout Pie Menu 为当前点数组创建 Shape 按钮
|
||||
- **THEN** 菜单将包含 Points 与 Invert 的完整 Path 编码一次,并为每个 Shape Operator 复用同一字符串
|
||||
|
||||
#### Scenario: Cutout Shape operation restores the Selection
|
||||
- **WHEN** 用户点击任一 Cutout Shape 按钮
|
||||
- **THEN** Shape Operator 从单一 Path JSON 恢复 Points 与 Invert 后再进行栅格化
|
||||
|
||||
### Requirement: Selection rasterization uses scanline events
|
||||
系统 SHALL 通过路径边与扫描行的交点事件构建 non-zero winding Mask,每条路径边不得重复创建扫描行块乘以 Selection 宽度的二维像素比较数组。
|
||||
|
||||
#### Scenario: High-resolution Lasso is rasterized
|
||||
- **WHEN** 系统处理高分辨率图片上的多点 Lasso
|
||||
- **THEN** 每条边只为实际跨越的扫描行产生端点事件,所有端点完成后每行执行一次水平累计
|
||||
|
||||
#### Scenario: Path point count increases
|
||||
- **WHEN** Selection 画布不变而路径点数增加
|
||||
- **THEN** 栅格器增加交点事件处理,不按每个新增点重新扫描完整二维 Selection 像素区域
|
||||
|
||||
### Requirement: Non-inverted Selection processing stays local
|
||||
系统 SHALL 对非 Invert Selection 只在裁切后的路径范围内执行填充、抗锯齿、源 Alpha 合并和可见 bounds 计算;抗锯齿范围 MUST 包含固定卷积核所需的边缘 padding。
|
||||
|
||||
#### Scenario: Local Selection is rasterized
|
||||
- **WHEN** 非 Invert Selection 只覆盖源图的一部分
|
||||
- **THEN** 中间 Mask 和 Alpha 运算范围限制在路径 bounds 及必要的抗锯齿 padding
|
||||
|
||||
#### Scenario: Inverted Selection is rasterized
|
||||
- **WHEN** Selection 启用 Invert
|
||||
- **THEN** 系统使用完整图片范围表达路径外区域,但仍只累计一次 scanline 事件
|
||||
|
||||
### Requirement: Optimized rasterization preserves Mask semantics
|
||||
系统 MUST 保持现有像素中心判定、Invert、抗锯齿核、Alpha 阈值、空 Selection 警告和最终局部 Selection Mask bounds 语义。
|
||||
|
||||
#### Scenario: Selection has source Alpha
|
||||
- **WHEN** Selection Path 与源图 Alpha 一起栅格化
|
||||
- **THEN** 系统只保留 Alpha 高于阈值且 non-zero winding Mask 有效的像素,并返回其紧致 bounds
|
||||
|
||||
#### Scenario: Selection has no visible pixels
|
||||
- **WHEN** Selection 与图片范围或可见源 Alpha 没有交集
|
||||
- **THEN** 系统报告空 Selection,且不向后续 Image、Cutout 或 AI 操作提交结果
|
||||
|
||||
#### Scenario: Equivalent Selection is processed
|
||||
- **WHEN** 相同 Selection Path、源 Alpha、阈值和抗锯齿设置由参考实现与优化实现处理
|
||||
- **THEN** 两者产生逐值一致的 Mask values 和相同 bounds
|
||||
20
openspec/changes/simplify-selection-rasterization/tasks.md
Normal file
20
openspec/changes/simplify-selection-rasterization/tasks.md
Normal file
@@ -0,0 +1,20 @@
|
||||
## 1. 简化 Selection 值与填充语义
|
||||
|
||||
- [x] 1.1 将 Viewport 预览和最终 Mask 统一为 non-zero winding,删除 `EVEN_ODD`、`fill_rule` 参数与分支,并用自交和重复绕行测试验证重叠区始终保留
|
||||
- [x] 1.2 将 `SelectionPath` 精简为只包含 Points 与 Invert 的不可变值,保留完整值的 `to_json()` / `from_json()`,使共享手势向 Image Tool 和 Cutout Tool 提交完整 Path,并迁移反选相关测试
|
||||
- [x] 1.3 保持 Cutout Pie Menu 到 `CutoutSelectionToShape` 的单一 `selection_path_json` 边界,用交互测试验证只编码一次、各 Shape 按钮复用同一字符串且恢复相同 Points 与 Invert
|
||||
- [x] 1.4 删除 `fill_rule` 的序列化、解析和所有兼容入口,使用定向搜索验证业务代码与 Path JSON 均不再包含填充规则
|
||||
|
||||
## 2. 局部 Scanline 栅格化
|
||||
|
||||
- [x] 2.1 实现 non-zero scanline 端点累计,使每条边只处理实际跨越的像素中心扫描行,并用整数顶点、水平边、凹路径、自交与越界路径测试验证结果
|
||||
- [x] 2.2 对非 Invert Selection 只建立裁切后的局部画布,抗锯齿时按 5-tap 核增加 2 px padding;对 Invert 保留完整图片画布并验证路径外语义
|
||||
- [x] 2.3 将源 Alpha 合并限制到当前画布切片,并以按行、按列归约计算紧致 bounds,删除整图 `np.where` 与逐命中像素坐标数组
|
||||
- [x] 2.4 保持 `SelectionMask`、空 Selection 警告、Image 编辑、AI 输入和 Cutout 几何调用边界不变,用现有像素与几何测试验证下游结果
|
||||
|
||||
## 3. 等价性、性能边界与文档
|
||||
|
||||
- [x] 3.1 增加小尺寸参考栅格器测试,组合覆盖普通与随机路径、Invert、抗锯齿、随机 Alpha、阈值和空结果,逐值比较 Mask values 与 bounds
|
||||
- [x] 3.2 增加 5K 多点 Lasso 的工作边界测试,验证不出现逐边的扫描行块乘以宽度数组、局部 Selection 不建立整图 Mask,且不使用机器相关耗时阈值
|
||||
- [x] 3.3 更新 `docs/internals` 的 Selection 值、Cutout 传输边界、统一 non-zero 语义与局部 scanline 流程,不记录旧兼容路径
|
||||
- [x] 3.4 运行 Image Selection、Image Tool、Cutout 交互与几何相关测试,运行代表性 5K 基准,再运行 `uv run pytest` 确认全部通过
|
||||
Reference in New Issue
Block a user