Add tools and tests for AnyImage model management and documentation

- Implement sync_models.py for preparing and uploading ONNX artifacts.
- Create build_node.py for building and validating Blender scripts.
- Add docs.py for building and previewing documentation with Mike or MkDocs.
- Introduce model_exports.py for exporting PyTorch models to ONNX format.
- Develop pack.py for packaging the project with dependencies for different platforms.
- Add test_sync_models.py to validate sync_models functionality.
- Add test_tool_commands.py to ensure repository tools have correct command scripts.
- Initialize tools package with __init__.py.
This commit is contained in:
2026-08-31 15:15:33 +08:00
parent 377eee43f5
commit e7aeeb21d4
99 changed files with 1953 additions and 1676 deletions

View File

@@ -23,11 +23,11 @@ flowchart TD
入口位于 `src/anyimage/operators/ai_setup.py`。`SetupAIEnvironment` 通过 `properties.py` 中的 `ai_status()` 读取环境和模型状态,并根据结果结束、安装环境或调用 `DownloadRequiredModels`。对话框同时显示预计下载量和缺失模型名称。
`runtime.py` 提供当前 Environment 与 Server 状态;Server 正忙时不会启动新的安装流程。`OpenAIEnvironmentSettings` 也在 `ai_setup.py` 中,它只负责打开 AnyImage 的扩展首选项。
`runtime.py` 提供当前 Environment 与 Server 状态;Server 正忙时不会启动新的安装流程。`OpenAIEnvironmentSettings` 打开 AnyImage 的扩展首选项。
### 安装独立环境
环境尚未就绪时,`SetupAIEnvironment` 调用 Runtime 注册的安装 Operator。普通源与中国镜像使用同一条安装流程,`use_mirror` 只决定 uv、Python 和 Python 包的下载源,不改变模型来源。
环境尚未就绪时,`SetupAIEnvironment` 调用 Runtime 注册的安装 Operator。`use_mirror` 决定 uv、Python 和 Python 包的下载源,模型使用模型目录中声明的来源。
安装完成后继续进入模型下载,不在 Blender Python 中加载 ONNX Runtime 或模型依赖。

View File

@@ -16,7 +16,7 @@ flowchart TD
Target --> Paint["Brush Texture"]
```
整条流程运行在 Blender 进程中,不使用 Job Server。快捷键会先尊重 Blender 自己的 Copy/Paste;只有系统剪贴板出现新的图片内容时,AnyImage 才接管粘贴。
快捷键优先处理 Blender 自己的 Copy/Paste;系统剪贴板出现新的图片内容时,AnyImage 接管粘贴。
## 节点说明
@@ -28,7 +28,7 @@ flowchart TD
### 读取系统图片
`clipboard.py` 负责平台差异。Windows 读取 DIB 或 DIBV5 并补成 BMP;macOS 和 Linux 调用系统提供的图片导出命令。无论来源平台,输出都收敛为图片字节和扩展名,后续步骤不再判断操作系统。
`clipboard.py` 负责平台差异。Windows 读取 DIB 或 DIBV5 并补成 BMP;macOS 和 Linux 调用系统提供的图片导出命令。各平台输出统一为图片字节和扩展名,再交给后续步骤。
### 创建或复用 Packed Image
@@ -38,4 +38,4 @@ flowchart TD
落点仍在 `actions.py` 中完成:3D View 创建 Reference 或 Plane,Node Editor 创建与当前节点树匹配的 Image 节点,绘制模式把 Image 配置为当前画笔的 Texture。鼠标位置和视图位置也在这里换算。
Plane 路径调用 `convert_to_plane/mesh_plane.py` 创建基础网格,并使用 `convert_to_plane/nodes.py` 提供的 `O Mesh Plane` 节点组。`clipboard_image/__init__.py` 只负责入口和快捷键,具体 Blender 数据始终由 `actions.py` 创建。
Plane 路径调用 `convert_to_plane/mesh_plane.py` 创建基础网格,并从节点资产加载 `O Mesh Plane`。`actions.py` 创建 Blender 数据。

View File

@@ -15,7 +15,7 @@ flowchart TD
Import --> Depth["创建 Depth Plane Object"]
```
两条路径共用 Image Empty 的显示范围、颜色材质和基础 Plane。Depth Plane 多出一次 Server Job,并由节点根据相机元数据把 Z Depth 转换为局部位移。正式流程只使用 MoGe-2;DA3 只保留 Debug 入口。
两条路径共用 Image Empty 的显示范围、颜色材质和基础 Plane。Depth Plane 通过一次 MoGe-2 Server Job 生成 Z Depth,再由节点根据相机元数据转换为局部位移。
## 节点说明

View File

@@ -21,11 +21,7 @@ flowchart TD
Ready --> Build --> Create
```
Surface 与 Balloon 可以完全在 Blender 中完成;启用 Fit to Foreground、生成 Depth Shape 或 Normal Map 时,中间会经过一次 Server Job。
两条路径的区别只在中间是否需要 Server 产物。回到 Blender 后,它们都使用最终的 `RegionMask` 构建同一种 `BaseShape`,再由同一入口创建对象。Server 不创建 Blender Mesh,也不访问 `bpy`。
下文中的 Blender 侧文件相对 `src/anyimage/operators/cutout_tool/`,Server 侧文件相对 `src/anyimage/server/`。
Surface 与 Balloon 直接使用 `RegionMask` 构建 `BaseShape`;Fit to Foreground、Depth Shape 和 Normal Map 先通过 Server Job 生成所需产物。
## 节点说明
@@ -33,13 +29,13 @@ Surface 与 Balloon 可以完全在 Blender 中完成;启用 Fit to Foreground
Cutout 工具由 `__init__.py` 注册,交互从 `main.py` 中的 `SelectCutoutRegion` 开始。它接收 Lasso 或双击整图手势,把图片空间路径保存为 `RegionPath`,然后调用 `interaction.py` 打开 Shape 饼菜单。
`interaction.py` 在这里完成 Region 有效性检查、读取三个工具选项,并根据 **Mesh Sampling Spacing** 换算期望网格分辨率;它不定义 Operator。菜单中的 Surface、Balloon、Depth Balloon 和 Depth Surface,以及它们对应的节点分支索引,统一来自 `shape.py`。
`interaction.py` 完成 Region 有效性检查、读取三个工具选项,并根据 **Mesh Sampling Spacing** 换算期望网格分辨率。菜单中的 Surface、Balloon、Depth Balloon 和 Depth Surface,以及它们对应的节点分支索引,统一来自 `shape.py`。
### 栅格化 Region
用户选定 Shape 后,`main.py` 中的 `CutoutRegionToShape` 接过主线。它使用 `operators/common/image_region.py` 把 `RegionPath` 栅格化为 `RegionMask`:`values` 保存局部像素权重,`bounds` 保存它在源图中的位置。套索外和源图透明区域不会进入最终 Region。
用户选定 Shape 后,`main.py` 中的 `CutoutRegionToShape` 接过主线。它使用 `common/image_region.py` 把 `RegionPath` 栅格化为 `RegionMask`:`values` 保存局部像素权重,`bounds` 保存它在源图中的位置。套索外和源图透明区域不会进入最终 Region。
此时也会应用 **Maximum Cutout Mesh Resolution**。它限制裁切区域的最长采样边,对四种 Shape 一视同仁,与是否使用 AI 无关。
此时也会应用 **Maximum Cutout Mesh Resolution**,统一限制四种 Shape 裁切区域的最长采样边。
### 需要后台生成数据?
@@ -49,23 +45,23 @@ Cutout 工具由 `__init__.py` 注册,交互从 `main.py` 中的 `SelectCutout
- 选择 Depth Balloon 或 Depth Surface;
- 开启 **Generate Normal Map**。
没有开启两个 AI 选项的 Surface 和 Balloon 会直接沿用本地 `RegionMask`。
Surface 和 Balloon 的本地路径直接沿用 `RegionMask`。
### 执行 Fit to Foreground,生成 Depth / Normal 产物
`main.py` 把请求提交给 `server/jobs/cutout.py`。这个 Job 只生成本次 Shape 实际需要的产物:
`main.py` 把请求提交给 `server/jobs/cutout.py`,生成本次 Shape 需要的产物:
- **Fit to Foreground** 调用 `server/region/refine_region.py`,写出收紧后的 `region.png` 和 `region_bounds.json`;
- 需要 Vector Depth 时写出相机空间 XYZ 组成的 `vector-depth.exr`,需要 Z Depth 时写出 `z-depth.exr`,两者都配套 `depth.json`;
- **Generate Normal Map** 根据既定空间写出 `tangent-normal.png` 或 `object-normal.png`。
Depth 与 Normal Map 同时需要时,共享同一次 MoGe-2 Prediction。Prediction 只存在于 Server 内存,Job 目录只保留 Blender 可直接加载的图片和 JSON。Depth Surface 的 Normal Map 编码为 Cutout 对象空间,其正面是对象局部 `-X`;其余 Shape 使用 Tangent Space。Normal Space 由 `main.normal_mode_for_shape()` 根据 Shape 决定,不是单独的用户选项。
Depth 与 Normal Map 共享同一次 MoGe-2 Prediction。Depth Surface 的 Normal Map 编码为 Cutout 对象空间,其正面是对象局部 `-X`;其余 Shape 使用 Tangent Space。`main.normal_mode_for_shape()` 根据 Shape 确定 Normal Space。
**Maximum AI Input Size** 只限制 BEN2 和 MoGe-2 看到的分析图长边。它不改变网格密度,也不降低最终颜色图分辨率。DA3 不进入这条正式路径,只保留 Debug 功能。
**Maximum AI Input Size** 约束 BEN2 和 MoGe-2 的分析图长边;网格密度使用 **Maximum Cutout Mesh Resolution**,最终颜色图使用源图裁切分辨率。
### 构建 BaseShape
Job 完成后,`main.py` 在 Blender 主线程读取可选的 RegionMask,直接加载并 Pack Depth 与 Normal 图片;本地路径则直接使用已有数据。两者最终都进入 `main.create_cutout_shape()`,再调用 `geometry.py` 中的 `build_base_shape()`:
Job 完成后,`main.py` 在 Blender 主线程读取可选的 RegionMask,加载并 Pack Depth 与 Normal 图片。`main.create_cutout_shape()` 调用 `geometry.py` 中的 `build_base_shape()`:
1. 从最终 `RegionMask` 提取外轮廓和孔洞;
2. 按 Mesh Sampling Spacing 生成轮廓与内部采样点;
@@ -77,7 +73,7 @@ Job 完成后,`main.py` 在 Blender 主线程读取可选的 RegionMask,直
### 创建 Blender 对象
`main.create_cutout_shape()` 把 `BaseShape` 和可选产物交给 `object.py` 的 `create_shape_object()`。这里创建裁切后的 Color Image、Material、Mesh Object,并挂上统一的 `O Image Cutout` Modifier。材质通过 `operators/common/mesh_result.py` 创建,Cutout 使用 `1.2` 的 IOR。Color Image 是按最终 Region 包围矩形裁切的源图 RGBA;RegionMask 不写入颜色图 Alpha,源图原有 Alpha 会原样保留。
`main.create_cutout_shape()` 把 `BaseShape` 和可选产物交给 `object.py` 的 `create_shape_object()`。这里创建裁切后的 Color Image、Material、Mesh Object,并挂上统一的 `O Image Cutout` Modifier。材质通过 `common/object.py` 创建,Cutout 使用 `1.2` 的 IOR。Color Image 按最终 Region 包围矩形裁切源图并保留 RGBA。
| Shape | Modifier 如何形成结果 | 必需后台产物 |
| --- | --- | --- |
@@ -88,6 +84,6 @@ Job 完成后,`main.py` 在 Blender 主线程读取可选的 RegionMask,直
Depth Shape 的 Base Plane Depth,以及 Depth Balloon 的 Direction,都由 `object.py` 调用 `geometry.py` 的标定函数,根据最终 Region Alpha 计算。透明像素达到 `5%` 时使用有效 Region 自适应标定,否则使用稳定的固定基准。
Normal Map 是材质细节,不改变上述四种 Shape 的几何分类。关闭时材质使用几何法线;开启时,`object.py` 把生成的图片和已经确定的 Normal Space 交给材质节点。最后 `main.py` 提交 Undo,整条 Cutout 流程结束。
Normal Map 关闭时材质使用几何法线;开启时,`object.py` 把生成的图片和已经确定的 Normal Space 交给材质节点。最后 `main.py` 提交 Undo,整条 Cutout 流程结束。
Balloon 与 Depth Balloon 的材质会创建 Geometry Attribute 节点,把 `o_balloon` 的 Float 输出直接连接到 `O Image Layer` 已有的 **Normal Scale**;Surface 与 Depth Surface 不建立这条连接。

View File

@@ -1,6 +1,6 @@
# Debug Operator
Debug Operator 从面板中的模型参数发起一次原始推理,把结果文件留在 Job 目录中供开发者检查,不创建用户场景对象。
Debug Operator 从面板中的模型参数发起一次原始推理,把结果文件写入 Job 目录并打开目录供开发者检查。
## 执行过程
@@ -13,7 +13,7 @@ flowchart TD
Output --> Directory["打开 Job 目录"]
```
DA3、MoGe-2、BEN2 和 Upscale 都走这条调试路径。它验证模型、设备、请求参数和输出格式,但不代表正式功能的对象创建流程。
DA3、MoGe-2、BEN2 和 Upscale 都走这条调试路径,用于验证模型、设备、请求参数和输出格式。
## 节点说明
@@ -25,8 +25,8 @@ DA3、MoGe-2、BEN2 和 Upscale 都走这条调试路径。它验证模型、设
`debug.py` 分别提交 `debug-da3`、`debug-moge2`、`debug-ben2` 和 `debug-upscale` Job。Server 侧由 `server/jobs/debug_predictions.py` 接收请求,再调用相应模型适配器。
DA3 和 MoGe-2 的可视化与数组输出由 `server/outputs/debug_da3.py`、`server/outputs/debug_moge2.py` 写出;BEN2 与 Upscale 保留模型返回的图片结果。这里允许 DA3 继续存在,但它不会被 Plane、Depth Plane 或 Cutout 的正式 Operator 调用。
DA3 和 MoGe-2 的可视化与数组输出由 `server/outputs/debug_da3.py`、`server/outputs/debug_moge2.py` 写出;BEN2 与 Upscale 保存模型返回的图片结果。
### 查看结果
任务完成后,`debug.py` 的 `_open_job_directory()` 调用 Blender 的路径打开操作,直接显示该 Job 的目录。响应阶段不导入 Image,不创建 Mesh、Material 或 Modifier,也不修改当前场景。
任务完成后,`debug.py` 的 `_open_job_directory()` 调用 Blender 的路径打开操作,直接显示该 Job 的目录。

View File

@@ -24,13 +24,13 @@ flowchart TD
`operators/image_tool/__init__.py` 汇总注册类型;`box.py`、`lasso.py`、`line.py` 和 `polyline.py` 分别定义四个 WorkspaceTool。它们都把操作交给 `edit_mask.py` 中的 `EditImageMask`。
屏幕坐标投射、点击选择、轨迹预览和状态切换来自 `operators/common/image_interaction.py`。Lasso 按移动距离采点并合并近似共线点;Line 扩展为有向半平面;Polyline 在确认时闭合路径。
屏幕坐标投射、点击选择、轨迹预览和状态切换来自 `common/image_interaction.py`。Lasso 按移动距离采点并合并近似共线点;Line 扩展为有向半平面;Polyline 在确认时闭合路径。
### RegionPath → RegionMask
`EditImageMask` 通过 `operators/common/image_region.py` 把手势整理为 `RegionPath`,再栅格化为 `RegionMask`。凹多边形使用三角剖分,自交路径使用 `EVEN_ODD` 填充。预览缓存只在轨迹变化时更新。
`EditImageMask` 通过 `common/image_region.py` 把手势整理为 `RegionPath`,再栅格化为 `RegionMask`。凹多边形使用三角剖分,自交路径使用 `EVEN_ODD` 填充。预览缓存只在轨迹变化时更新。
Box 和 Lasso 分别读取自己的 **Fit to Foreground** 设置。Line 与 Polyline 强制关闭该选项,因此不会改变直线边界。
Box 和 Lasso 分别读取自己的 **Fit to Foreground** 设置。Line 与 Polyline 直接使用几何边界。
### Fit to Foreground
@@ -40,6 +40,6 @@ Box 和 Lasso 分别读取自己的 **Fit to Foreground** 设置。Line 与 Poly
### 源 Alpha × RegionMask
`edit_mask.py` 保留源 RGB,只把最终 `RegionMask` 乘入源 Alpha,再按非零 Alpha 紧裁切。像素读取、结果 Image 创建和共享引用处理使用 `operators/common/image_data.py`。
`edit_mask.py` 保留源 RGB,只把最终 `RegionMask` 乘入源 Alpha,再按非零 Alpha 紧裁切。像素读取、结果 Image 创建和共享引用处理使用 `common/image_data.py`。
公共 Image Empty 替换入口会同步更新 `empty_display_size` 和 `empty_image_offset`,因此裁切后的内容仍对齐原图片平面,对象 `matrix_world` 不变。Perspective 使用相同的像素写回能力,但属于独立工具和状态。
公共 Image Empty 替换入口同步更新 `empty_display_size` 和 `empty_image_offset`,使裁切后的内容对齐原图片平面并保持对象 `matrix_world`。

View File

@@ -1,6 +1,6 @@
# Operator 实现索引
`src/anyimage/operators/` 负责 Blender 侧交互、Job 请求组装和结果导入。内部实现按完整功能包或顶层业务模块拆页;`common/` 的边界见[程序架构](../architecture.md),实际公开入口见[Common 公共函数](../common.md)。
`src/anyimage/operators/` 负责 Blender 侧交互、Job 请求组装和结果导入。
| Operator 路径 | 实现页 | 主要职责 |
| --- | --- | --- |
@@ -15,4 +15,4 @@
| `operators/remove_background.py` | [Remove Background](remove-background.md) | BEN2 去背与结果替换 |
| `operators/upscale.py` | [Upscale](upscale.md) | 当前 Image 的 4× 高清与输入长边检查 |
`operators/__init__.py` 只聚合注册类,不承载独立业务流程。具体类的注册和逆序注销由扩展入口统一管理,整体边界见[程序架构](../architecture.md)。
`operators/__init__.py` 聚合注册类,扩展入口按顺序注册并逆序注销。

View File

@@ -15,7 +15,7 @@ flowchart TD
Crop --> Write["Pack 并写回 Empty"]
```
预览只在第四点确认后生成一次;之后拖动鼠标只改变目标宽高比。用户确认时才执行全分辨率 Warp。整个流程在 Blender 进程内完成,不使用 Job Server。
第四点确认后生成预览,拖动鼠标调整目标宽高比,用户确认后执行全分辨率 Warp。整个流程在 Blender 进程内完成。
## 节点说明
@@ -23,18 +23,18 @@ flowchart TD
`operators/perspective_tool/__init__.py` 导出注册类型,交互和计算集中在 `edit_perspective.py`。`ImagePerspectiveTool` 启动 `EditImagePerspective`,后者接受任意顺序的四个图片角点,再根据源图 X/Y 方向整理成不交错的凸 Quad。
图片坐标投射和首次点击选择复用 `operators/common/image_interaction.py`。Perspective 始终把四点称为 Quad,不使用 Image Tool 的 Region 或 Mask 状态。
图片坐标投射和首次点击选择复用 `common/image_interaction.py`。四个角点在该流程中统一表示为 Quad。
### 预览并调整比例
第四点确定后,`edit_perspective.py` 创建一次 512 × 512 预览纹理。鼠标移动只更新目标宽高比和 HUD,可吸附到常用比例,不会重复重采样源图。
第四点确定后,`edit_perspective.py` 创建 512 × 512 预览纹理。鼠标移动更新目标宽高比和 HUD,并可吸附到常用比例。
输出尺寸根据 Quad 边长和目标比例计算,最长边不超过 8192 px,总像素不超过 16,777,216。
### 执行 Homography
确认后,`edit_perspective.py` 计算 Homography,并按 256 行分块处理全分辨率像素。插值使用预乘 Alpha,避免透明边缘混入黑色。Perspective 只做形变,不提供 Fit to Foreground 或反选。
确认后,`edit_perspective.py` 计算 Homography,并按 256 行分块处理全分辨率像素。插值使用预乘 Alpha,避免透明边缘混入黑色。
### 裁切并写回
Warp 结果按可见 Alpha 紧裁切,再通过 `operators/common/image_data.py` 创建 Packed Image 并替换当前 Empty。结果宽高比可能不同于原 Quad 的轴对齐范围,因此写回时保持中心和覆盖尺度近似一致,而不是复用原图片像素范围。
Warp 结果按可见 Alpha 紧裁切,再通过 `common/image_data.py` 创建 Packed Image 并替换当前 Empty。写回时保持中心和覆盖尺度近似一致。

View File

@@ -14,13 +14,13 @@ flowchart TD
Import --> Replace["替换 Empty.data"]
```
Blender 负责媒体输入准备和结果导入,BEN2 推理留在 Server。Packed 图片和内存中修改过的单图会先导出临时文件;可直接读取的文件图片、视频和序列沿用原路径。
Packed 图片和内存中修改过的单图会先导出临时文件;文件图片、视频和序列沿用原路径。
## 节点说明
### 准备媒体输入
入口是 `operators/remove_background.py` 中的 `RemoveImageBackground`。它检查当前对象、AI Environment 和帧数,再调用 `operators/common/image_data.py` 的 `prepare_image_input()`。多帧输入会先写入 Scene 中的抽帧设置。
入口是 `operators/remove_background.py` 中的 `RemoveImageBackground`。它检查当前对象、AI Environment 和帧数,再调用 `common/image_data.py` 的 `prepare_image_input()`。多帧输入会先写入 Scene 中的抽帧设置。
准备完成后,内部 Operator `RunBackgroundRemoval` 保存输入路径、源对象名和临时文件清理标记。
@@ -32,6 +32,6 @@ BEN2 推理前先用源 Alpha 屏蔽透明区域,模型输出的前景 Alpha
### 导入并替换结果
Job 返回后,`remove_background.py` 的 `apply_background_response()` 调用 `operators/common/image_data.py` 创建单图或序列 Image,并恢复源 Image User 的帧起点、偏移和循环设置。
Job 返回后,`remove_background.py` 的 `apply_background_response()` 调用 `common/image_data.py` 创建单图或序列 Image,并恢复源 Image User 的帧起点、偏移和循环设置。
同一公共入口随后替换 `Empty.data`。旧 Image 没有其他用户且未启用 Fake User 时会被清理,结果继承原名;否则保留旧数据块。最后提交一个 Remove Background Undo 步骤,并清理本次创建的临时输入。
`replace_empty_image()` 随后替换 `Empty.data`,提交一个 Remove Background Undo 步骤,并清理本次创建的临时输入。

View File

@@ -25,20 +25,20 @@ flowchart TD
入口位于 `operators/upscale.py`。`UpscaleImage.poll()` 读取 `preferences.py` 中的 **Maximum AI Input Size**;当前 Image 长边必须严格小于该值。菜单显示的预计尺寸来自同一文件的 `upscale_output_size()`。
执行时,`upscale.py` 冻结当前模型和输入上限,并通过 `operators/common/image_data.py` 准备单图、视频或序列输入。Packed Image 与内存修改结果会导出临时文件,多帧输入保留帧起点和抽帧设置。
执行时,`upscale.py` 冻结当前模型和输入上限,并通过 `common/image_data.py` 准备单图、视频或序列输入。Packed Image 与内存修改结果会导出临时文件,多帧输入保留帧起点和抽帧设置。
### 分块放大 RGB
`UpscaleImage` 提交 `upscale-image` Job。`server/jobs/upscale.py` 展开媒体帧并逐帧调用模型;`server/models/upscale.py` 负责输入约束、RGB/Alpha 分离和结果合并,`server/models/onnx_upscale.py` 以 256 × 256 分块执行 4× ONNX 推理并带入相邻上下文。
`server/media/resolution.py` 统一解释输入长边上限。正式 Upscale 在 Blender 侧直接禁用超限图片,因此不会进入预缩放路径,输出长边小于上限的四倍;模型层仍保留同一限制作为任务边界。
`server/media/resolution.py` 统一解释输入长边上限。Blender 侧要求源图长边小于该值,结果长边小于该值的四倍。
### 合并 Alpha
Alpha 不进入模型,由 `server/models/upscale.py` 使用 Lanczos 放大到目标尺寸,再与 4× RGB 合并为 RGBA。Provider 资源不足时,Server 释放当前 Upscale Session,并返回降低输入上限或改用 CPU 的提示。
`server/models/upscale.py` 使用 Lanczos 将 Alpha 放大到目标尺寸,再与 4× RGB 合并为 RGBA。Provider 资源不足时,Server 释放当前 Upscale Session,并返回降低输入上限或改用 CPU 的提示。
### 导入并替换结果
响应阶段回到 `operators/upscale.py`,通过 `operators/common/image_data.py` 读取 `upscale.*` 结果序列并替换当前 Empty。源 Image 没有其他用户且未启用 Fake User 时会被清理并由结果继承名称;存在其他用户时只替换当前 Empty 的引用。
响应阶段回到 `operators/upscale.py`,通过 `common/image_data.py` 读取 `upscale.*` 结果序列并替换当前 Empty。
结果不添加 Upscale 专属数据块后缀,也不保留来源引用。后续再次 Upscale 或转换 Depth Plane 时,读取的都是当前结果 Image。
后续 Upscale 和 Depth Plane 都以当前结果 Image 为输入。