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

@@ -24,7 +24,7 @@ jobs:
run: uv run --frozen pytest
- name: 构建扩展
run: uv run --frozen python pack.py
run: uv run --frozen pack
- name: 发布扩展包
uses: https://gitea.com/actions/gitea-release-action@v1.3.6

2
.gitignore vendored
View File

@@ -1,6 +1,7 @@
__pycache__/
*.py[cod]
.venv/
.env
.uv-cache/
dist/
build/
@@ -8,4 +9,5 @@ build/
.pytest_cache/
/models/
/.model-cache/
wheels/

127
AGENTS.md
View File

@@ -1,103 +1,90 @@
# AnyImage 开发约定
# AnyImage 项目规范
## 总则
- 说明性内容(提交说明、文档、注释、Mermaid 等)以中文为主,专有名词和约定俗成的表达可使用英文
- 动手前先读 `docs/index.md`;程序边界、调用流程和模块职责见 `docs/internals/index.md` 与 `docs/internals/architecture.md`
- 修改代码、依赖或执行方式后,同步更新 `README.md` 与对应主题文档,保持文档、测试和实现一致
- 优先选择依赖少、直接、易验证的实现;保留工作区中与当前任务无关的已有改动
**不管是代码还是文档,遵循「少就是多」的原则,以好理解为首要要求**
## 内部实现文档
- 说明性内容以中文为主,专有名词和约定俗成的表达可使用英文
- 修改代码、依赖或执行方式后,
- 不保留旧路径、转发函数、兼容层或胶水代码
- 仅过测试,不改同步改文档,不构建文档、产物
- 优先选择依赖少、直接、易验证的实现
- 复用来自 `common` 的函数,避免重复造轮子
- 确保文件名/文件夹名和内容边界一致
- [内部实现总览](docs/internals/index.md):内部实现文档的统一入口
## 查询文档
下列是重要文档
- [总览](docs/internals/index.md):内部实现的总揽
- [程序架构](docs/internals/architecture.md):进程边界、职责与调用主链
- [Operator 实现索引](docs/internals/operators/index.md):源码功能与实现页的对应关系
修改完整工作流时同步更新对应实现页。`operators/common/` 不单独建立页面,共享边界写入程序架构。
## 模块与目录边界
- 目录结构应能直接表达业务模块和职责;优先按用户可理解的功能或稳定能力组织,不按模糊的数据类型、临时步骤或界面摆放机械分组
- 一个模块只承担一个可描述的职责。功能出现多个相对独立的交互、算法或构建阶段时再建立子目录,并让同一抽象层的实现并列
- 公共层来自已经存在的复用,不来自对未来的猜测。没有真实共享消费者的代码留在所属模块;抽取后只暴露消费者需要的最小接口
- 模块边界优先服从运行进程、依赖方向、数据所有权和生命周期。只在一侧处理的数据不为形式上的“共享”跨越边界;确因加载机制产生的例外应在架构文档中说明
- 编排层只组合步骤和定义公开协议,原子能力放在所属领域模块。编排模块之间不互相导入实现,共享逻辑应下沉
- 公开任务与界面入口不要求一一对应;边界由可复用计算、输入输出协议和失败语义决定。名称必须描述实际动作与产物,不暗示未承担的职责
- 没有生产消费者的入口、兼容层和辅助协议应删除,不为测试便利单独保留
- 移动或拆分模块时同步更新注册、导入、打包、测试和文档,不留下旧路径转发层
- [Operator 索引](docs/internals/operators/index.md):Operator 实现功能和对应源码
- [节点资产](docs/internals/node-assets.md):节点组的构建与验证(节点组不在运行时新建)
## 常见命令
优先在项目根目录使用 Git Bash;Windows 路径在 Bash 命令中使用正斜杠。
```bash
# 安装或同步开发依赖
uv sync
# 本地 BlendJob wheel 内容变化后刷新锁文件
uv lock --refresh
# 运行全部测试
uv run pytest
# 运行插件注册、Manifest 与打包相关的快速测试
uv run pytest tests/test_addon.py tests/test_manifest.py tests/test_pack.py -v
# 构建 Windows、macOS、Linux 扩展包到 dist/
uv run python pack.py
uv run pack
# 本地 BlendJob wheel 内容变化后刷新锁文件
uv lock --refresh
# 构建并验证 Blender 节点资产
uv run build_node
# 准备、校验并同步 ONNX 模型到 R2
uv run sync_models
```
资产脚本必须由 Blender 执行;具体入口与验证范围见 [节点资产](docs/internals/node-assets.md),不要用普通 Python 解释器运行。
`tools/` 中的脚本按维护对象分组,并统一使用能够区分“构建”和“验证”等动作的命名。每个脚本都应有明确入口、文档或测试消费者;没有实际用途的脚本应删除。
## 命名规范
- 变量、函数、Docstring 使用英文;同一概念在代码和文档中使用统一表达
- 函数的动作前缀应表达产物边界:`generate_*` 表示 AI 生成,`build_*` 只在内存中构建值或几何,`create_*` 表示创建 Blender Image、Mesh、Material、Object 等真实数据
- 变量、函数、Docstring 必须使用英文;同一概念在代码和文档中使用统一表达
- 函数的动作前缀应表达产物边界:`generate_*` 表示 AI 生成。`build_*` 表示代码层构建。`create_*` 表示 Blender 内创建
- 名称应说明业务职责,并与用户概念、公开协议和界面术语保持一致;不要使用脱离上下文后无法判断用途的宽泛名称
- 文件和目录名应表达稳定的业务分类和模块职责,不直接沿用函数名,也不按临时执行步骤命名;集合层使用复数,单项分类使用单数,同一层级保持一致
- Python 类使用大驼峰,不添加项目名前缀。Operator 使用动宾结构,如 `ConvertToDepthPlane`、`RemoveImageBackground`;Menu、Panel、AddonPreferences、PropertyGroup 分别使用实际类型后缀,如 `AnyImageImageMenu`、`ServerPanel`、`AnyImagePreferences`、`AnyImageSettings`
- `bl_idname` 与挂载到公共 Blender RNA 类型的 Property 使用 `anyimage` 项目前缀:Operator 如 `anyimage.convert_to_depth_plane`,Menu 如 `ANYIMAGE_MT_image_context`,Panel 如 `ANYIMAGE_PT_server`,公共 Property 如 `Scene.anyimage_settings`
- 文件和目录名应表达稳定的业务分类和模块职责,不直接沿用函数名,也不按临时执行步骤命名
- 集合层使用复数,单项分类使用单数
- Operator Class 使用动宾结构,如 `ConvertToDepthPlane`、`RemoveImageBackground`
- Menu、Panel、AddonPreferences、PropertyGroup 分别使用实际类型后缀,如 `AnyImageImageMenu`、`ServerPanel`、`AnyImagePreferences`、`AnyImageSettings`
- `bl_idname` 使用 `anyimage` 项目前缀:
- Operator 如 `anyimage.convert_to_depth_plane`
- Menu 如 `ANYIMAGE_MT_image_context`
- Panel 如 `ANYIMAGE_PT_server`
- 公共 Property 如 `Scene.anyimage_settings`
- Operator 自身 Property 不加项目前缀,使用业务名称,如 `input_path`、`process_res`、`resolution_level`
- 扩展包名使用 Manifest 中的真实名称、版本和平台,例如 `AnyImage.v0.11.2.windows-x64.zip`;节点组名称沿用资产中的 `O DA3 Depth Plane`、`O MoGe2 Surface` 等既有名称
- 常量应表达领域约束、共享数值或稳定映射,不为字面量增加没有额外语义的同名别名;需要类型约束时使用集中映射或 Enum
- 同一发布单元的版本只保留一个事实来源。只有存在独立兼容周期和实际消费者时,才引入额外版本或迁移状态
## 代码规范
## 文档规范
- 避免不必要的嵌套,优先使用提前返回和小而明确的函数
- 只为特殊、非常规或存在兼容性原因的实现写注释,不复述代码本身
- 删除功能时同时删除无效参数、分支、测试和文档,不保留没有实际消费者的兼容层
- Blender 侧不载入 AI 依赖:UI、Operator 和结果导入放在 `src/anyimage/` 或 `src/anyimage/operators/`,模型下载、ONNX 推理和输出放在 `src/anyimage/server/`
- 扩展级设置及其读取入口统一放在 `preferences.py`;使用 `addon_preferences()` 读取,不把 Blender Add-on Preferences 的定位、默认值或路径解析放进专门管理 BlendJob 的 `runtime.py`
- `README.md` 面向用户,不在其中堆叠内部细节。实现原理放入 `docs/internals` 对应主题
- 文档只正面描述当前主题的职责与实现,不记录无关模块、未采用路径、历史差异、边界辩解、“不做什么”的排除性信息。
- 实现原理用流程图替代文字,Mermaid 默认使用从上到下
## Blender 开发规范
- 扩展级设置及其读取入口统一放在 `preferences.py`;使用 `addon_preferences()` 读取
- Job 参数使用可序列化的普通字典;Blender 数据只能在主线程的 Operator 响应阶段读写
- 新增或修改 Blender 类型时同步检查 `src/anyimage/__init__.py` 的注册与逆序注销,并补充对应测试
- 依赖版本、wheel 或 Manifest 改变时同步检查 `pyproject.toml`、`uv.lock`、`blender_manifest.toml` 和打包测试
- `server/` 是 blendjob 后端代码,无法引用来自它上级文件夹的内容。共享函数需单独实现。
## Blender 节点组规范
资产脚本必须由 Blender 执行;具体入口与验证范围见 [节点资产](docs/internals/node-assets.md),不要用普通 Python 解释器运行。
`tools/` 中的脚本按维护对象分组,并统一使用能够区分“构建”和“验证”等动作的命名。每个脚本都应有明确入口、文档或测试消费者;没有实际用途的脚本应删除。
- 生产运行时使用的可复用节点组必须构建并存储在 `src/anyimage/assets/O_AnyImage.blend`,由统一资产加载入口按名称加载;不得在 `src/anyimage/` 中通过 `bpy.data.node_groups.new()` 动态创建。节点组新增、修改或删除时同步更新资产构建、验证、测试和节点资产文档
- 不修改节点的 `name`,也不使用 `label` 覆盖节点标题;保留 Blender 为节点类型生成的默认名称和外观,避免读图时无法判断节点原型。用途应通过节点类型、参数、空间布局和必要的少量注释表达
- 节点图优先依靠清晰的空间层次表达流程,不用五颜六色的 Frame 做分类;仅在重复区、模拟区等确有包裹语义时少量使用 Frame,并保持配色克制
- 避免跨越大段节点图的连线;来自 Group Input 的参数可按使用区域放置多个 Group Input,并隐藏未连接的 sockets
- 主处理链尽量保持为一根从左到右连续可见的几何主线,辅助字段和控制分支放在主线上下两侧,就近汇入消费者
- 避免交错连线、节点重叠和不必要的回折;新增或重建节点组时检查节点坐标唯一性和主要连线方向
- Repeat、Simulation、For Each Geometry Element 等具有输入/输出“头尾”的成对结构,应先确定完整包裹范围,再放置内部节点,保证头尾顺序、配对关系和区域边界一眼可辨
- 作为修改器使用的 Geometry Nodes,除确实需要动态字段或其他结构的接口外,输入 socket 的 structure_type 应设为 SINGLE
- 为接口 socket 设置符合业务含义的 `subtype`;例如表示 Blender 场景实际长度的浮点输入使用 `DISTANCE`,明确限制为 `0–1` 的比例使用 `FACTOR`,以 `0–100` 表示的百分比使用 `PERCENTAGE`,角度使用对应 subtype。无上限倍率等不符合范围语义的数值保留普通 Float,不为显示形式强套 subtype
- 调整节点接口或布局后,应补充或更新 Blender 验证,检查 socket 顺序与结构、节点不重叠、Group Input 未使用输出已隐藏,以及几何主线可正常求值
## 文档与 Markdown 规范
- `README.md` 面向用户;实现原理放入 `docs/` 对应主题,不在 README 堆叠内部细节
- 不使用 `---` 分隔线;一级标题只在文档开头使用一次
- Mermaid 节点 ID 使用英文字符,节点显示文本可以使用中文
- 文档描述当前已实现行为;限制、默认值、命令和文件名必须能从代码或测试中验证
- 节点组统一存入 `assets/O_AnyImage.blend`,运行时按名称加载(`load_node_group(group_name)`)
- 节点组通过[节点资产](docs/internals/node-assets.md)说明构建,禁止运行时调用 `bpy.data.node_groups.new()` 新建
- 节点组变更须同步构建脚本、验证、测试和节点资产文档
- 保留节点默认 `name` 和标题,不用 `label` 覆盖
- 主链从左到右连续排列;辅助分支就近汇入,避免长线、交错、重叠和回折
- Group Input 可按区域重复放置,并隐藏未连接 sockets
- Frame 仅用于确有包裹语义的区域
- Repeat、Simulation、For Each Geometry Element 等成对节点,确保位置正确,没有遮挡其他节点
- 修改器 Geometry Nodes 的输入 socket 默认使用 `SINGLE`
- socket `subtype` 按业务语义设置:长度用 `DISTANCE`,`0–1` 比例用 `FACTOR`,`0–100` 用 `PERCENTAGE`;无对应范围语义时保留默认
- 接口或布局变更须验证 socket 顺序与结构、节点不重叠、未使用输出隐藏及几何求值

View File

@@ -1,75 +1,12 @@
# AnyImage
AnyImage 是一款 Blender Extension,用于把图片带入可编辑的 2.5D 工作流:粘贴和清理图片、创建普通 Plane 或 Depth Plane、从套索生成 Cutout Shape,并通过独立 AI Environment 去除背景或进行 4× 高清放大。
把图片里的空间感带进 Blender。
完整说明从[文档首页](docs/index.md)开始;首次使用请阅读[安装与首次配置](docs/getting-started/installation.md)和[五分钟上手](docs/getting-started/quick-start.md)。
AnyImage 是一款 Blender 扩展,可以粘贴和编辑图片、创建普通 Plane 或 Depth Plane、从局部选区生成 Cutout Shape,并在独立 AI Environment 中完成去背景与高清放大。
## 主要功能
- 在 3D View、Node Editor 与绘画模式中用 `Ctrl+V`(macOS 为 `Cmd+V`)粘贴并 Pack 剪贴板图片
- 将 Image Empty 转换为带 `O Mesh Plane` Geometry Nodes 的普通 Plane
- 使用 MoGe-2 将静态图转换为可调 Depth Plane
- 用 Cutout 套索在图片可见 Alpha 内生成 Surface、Balloon、Depth Balloon 或 Depth Surface,并通过统一的 `O Image Cutout` 修改器切换 Shape、按创建分辨率清理碎岛、根据 Region Alpha 校准浮雕基准和调整表面
- 用 Image Box、Lasso、Line、Polyline 和 Perspective 清理或矫正静态图片
- 用 BEN2 为单图、视频或序列帧去除背景
- 用三档 ONNX 模型将单图、视频或序列帧放大 4×,并独立重采样 Alpha
AI 推理运行在 Blender 之外的独立 Python 3.12 Environment 中。Blender 进程只负责 UI、任务提交和结果导入,不加载 ONNX Runtime 或模型权重。
**Maximum AI Input Size** 默认设为 2048 px,只限制 Upscale 可执行图片长边 以及 Depth、Normal Map、Fit to Foreground 的 AI 分析输入,不降低颜色贴图分辨率。Cutout 网格密度由默认 `1024` 的独立 **Maximum Cutout Mesh Resolution** 限制,因此本地与 AI Shape 使用同一套明确的网格上限。
## 安装
1. 下载与系统匹配的扩展包:
- `AnyImage.v<version>.windows-x64.zip`
- `AnyImage.v<version>.macos-arm64.zip`
- `AnyImage.v<version>.linux-x64.zip`
2. 在 Blender 打开 **Edit > Preferences > Extensions**,选择 **Install from Disk** 并直接安装 ZIP。
3. 选中 Image Empty,在右键 **AnyImage** 菜单中点击 **Set Up AI Environment…**。
4. 安装完成后,在扩展 Preferences 中确认默认 MoGe-2、BEN2 与 Real-ESRGAN WDN 模型显示 Ready。
普通 Plane、基础 Image 编辑,以及未启用 **Fit to Foreground** / **Generate Normal Map** 的 Cutout Surface 与 Balloon 无需 AI Environment。Cutout 的 Depth 与 Normal Map 统一使用 MoGe-2;两者同时启用时共享同一次 Prediction,不会重复运行模型。Cutout 的 **Generate Normal Map** 是启用开关;开启后,Depth Surface 自动使用 Object Space,其余 Shape 自动使用 Tangent Space。
Balloon 与 Depth Balloon 会用 `o_balloon` 驱动 `O Image Layer` 已有的 **Normal Scale**,使 Normal Map 强度在轮廓处回落。
## 快速使用
选中 Image Empty 后,右键 **AnyImage** 菜单提供:
- **Convert to Plane**:同步创建普通图片网格;
- **Convert to Depth Plane**:使用 Preferences 中选择的深度模型处理静态图;
- **Remove Background**:生成透明图片或序列;
- **Upscale**:使用默认高清模型把当前图片编辑为 4× 结果。
3D Viewport 工具栏提供 Image Mask、Perspective 与 Cutout。Remove Background、Upscale、Image Mask 和 Perspective 都只更新当前 Empty;共享 Image 会保留原数据,并为当前 Empty 创建独立结果。工作流对比和当前输入范围见[选择工作流](docs/guides/workflows.md)。
## 源码结构
```text
src/anyimage/
├── operators/ # Blender 交互、任务提交与结果导入
├── server/ # 无 Blender 依赖的 Job、媒体、Region、几何与模型能力
├── assets/ # Geometry Nodes 与 Shader 资产
├── runtime.py # BlendJob Runtime 配置
└── __init__.py # Blender 注册入口
```
生产功能使用的可复用节点组统一构建到 `assets/O_AnyImage.blend`,运行时只按名称加载,不动态创建节点组。
程序边界、功能文件结构和调用流程见[内部实现总览](docs/internals/index.md);开发新功能前先查看[Common 公共函数](docs/internals/common.md),复用已经存在的 Image、Depth 产物、Material 和对象替换入口。文档按开始使用、功能指南、参考和内部实现分层,不在 README 重复内部算法细节。
## 开发
仓库开发环境使用 CPython 3.11:
```bash
uv sync
uv run pytest
uv run python pack.py
```
节点资产脚本必须由 Blender 执行,具体命令和验证范围见[节点资产](docs/internals/node-assets.md)。完整开发、打包与 CI 说明见[开发与发布](docs/internals/development.md)。
## 许可证
AnyImage Extension 代码采用 GPL-3.0-or-later。模型权重使用各自上游许可证。
- 按 **Ctrl+V**(macOS 为 **Cmd+V**)把剪贴板图片粘贴到当前编辑器
- 用 **Plane** 创建可调厚度和细分的图片网格
- 用 **Depth Plane** 把静态图片转换为可调节的立体面片
- 用 **Cutout** 从套索选区生成 Surface、Balloon 与两种 Depth Shape
- 用 **Image Tools / Perspective** 直接清理或矫正 Image Empty
- 用 **Remove Background / Upscale** 处理单图、视频和序列帧

BIN
docs/assets/logo.png LFS Normal file

Binary file not shown.

View File

@@ -25,10 +25,10 @@ AnyImage 的扩展偏好设置按功能分组。Environment 区包含:
Environment 检测后显示 Diagnostics。Depth & Normal、Remove Background、Upscale 与 Cutout 是默认展开的可折叠功能区,各自包含对应设置和模型状态。其中:
- **Default Upscale Model**:Upscale 使用的模型,默认是 General WDN x4v3。
- **Maximum AI Input Size**:Upscale、Depth、Normal Map 与 Fit to Foreground 的 AI 输入长边上限,默认 2048 px;只缩放分析输入,不降低颜色贴图分辨率。
- **Maximum AI Input Size**:Upscale、Depth、Normal Map 与 Fit to Foreground 的 AI 输入长边上限,默认 2048 px;颜色贴图保留原始分辨率。
- **Maximum Cutout Mesh Resolution**:所有 Cutout Shape 的网格采样长边上限,默认 1024,独立于 AI 输入尺寸。
Depth & Normal 只列出三档 MoGe-2,Depth Plane 与 Cutout Depth / Normal Map 使用同一个显式选择,不存在生产回退。可选模型的 **Ready** 状态或 **Download** 按钮位于选择项下方;Cutout 区提供独立网格上限与 Balloon Profile。
Depth & Normal 提供三档 MoGe-2,Depth Plane 与 Cutout Depth / Normal Map 共用当前选择。可选模型的 **Ready** 状态或 **Download** 按钮位于选择项下方;Cutout 区提供独立网格上限与 Balloon Profile。
如果准备把 Storage Root 放到其他磁盘,建议在首次安装 Environment 之前修改。更换目录后,旧目录中的环境和模型不会自动移动。
@@ -51,11 +51,10 @@ Depth & Normal 只列出三档 MoGe-2,Depth Plane 与 Cutout Depth / Normal Ma
完成后可以用以下方式确认:
- **Job Server** 不再显示 “Environment is not installed”。
- AnyImage 扩展偏好设置中的 MoGe-2 ViT-S Normal、BEN2 Base 和 Real-ESRGAN General WDN x4v3 显示 **Ready**。
- 选中 Image Empty 后,右键菜单底部显示 **AI Environment Settings…**。
**Convert to Depth Plane**、**Remove Background** 和 **Upscale** 在 AI 未就绪时仍可点击,点击后会根据状态转入 **Set Up AI Environment…** 或 **Download Required Models…**。普通 Plane、Image 编辑,以及未启用 **Fit to Foreground** / **Generate Normal Map** 的 Cutout Surface 与 Balloon 不要求安装 AI Environment。
点击 **Convert to Depth Plane**、**Remove Background** 或 **Upscale** 时,AnyImage 会根据状态进入 **Set Up AI Environment…** 或 **Download Required Models…**。普通 Plane、Image 编辑和基础 Cutout Surface / Balloon 直接在 Blender 中执行。
## 下一步

View File

@@ -10,11 +10,11 @@
打开 **Edit > Preferences > Extensions > AnyImage**。第一次使用建议保持默认:
- **Mesh Depth Model**:MoGe-2 ViT-S Normal
- **Depth & Normal Model**:MoGe-2 ViT-S Normal
- **MoGe Resolution Level**:5
- **Device**:使用自动列出的首选设备
这些是扩展级默认值,转换时不会再弹出参数对话框。
转换直接使用这些扩展级默认值。
## 3. 开始转换
@@ -31,10 +31,3 @@
- **Depth Amount** 控制深度位移强度。
建议先用较低 Subdivide 调整 Depth Amount,再按成品需要增加细分。
## 接下来可以尝试
- 只需要普通图片卡片:[Plane](../guides/mesh-plane.md)
- 先获得透明前景:[去除背景](../guides/remove-background.md)
- 从局部选择生成透视表面:[Cutout](../guides/cutout.md)
- 了解深度数据如何进入节点:[Convert to Plane 实现原理](../internals/operators/convert-to-plane.md)

View File

@@ -1,33 +0,0 @@
# 视频与序列帧
AnyImage 对 Movie 与 Image Sequence 提供三类工作流:直接转换为普通 Plane、逐帧去除背景、逐帧高清放大。Depth Plane 与 Cutout 当前只支持静态图片。
## 支持范围
| 工作流 | 视频 / 序列帧 | 结果 |
| --- | --- | --- |
| Convert to Plane | 支持 | 保留源 Image 的播放设置 |
| Remove Background | 支持 | 透明 PNG 序列 |
| Upscale | 支持 | 4× PNG 序列 |
| Convert to Depth Plane | 不支持 | 仅静态图片 |
| Cutout / Image Tools | 不支持 | 仅静态图片 |
## 处理范围
逐帧 Job 从 Image Empty 的图片设置读取:
- **Image Frames**:处理帧数;
- **Offset**:源素材起始偏移;
- **Start**:结果在场景中的播放起点;
- **Cyclic**:是否循环播放。
显式把 Image Frames 设为 `1` 时,只处理一帧。Remove Background 与 Upscale 复用同一模型 Session 逐帧执行,内存峰值主要取决于单帧尺寸。
## 保存工程
Blender 无法把整组图片序列完整 Pack 到 `.blend`。Remove Background 与 Upscale 的动画结果会继续引用 Storage Root 中的 Job 目录。
!!! danger
删除或移动仍被工程引用的 Job 目录,会导致动画序列丢失。
长期保存时,可以连同对应 Job 目录一起复制工程,或先把序列移动到项目素材目录,再用 Blender 的外部数据工具重新定位路径。

View File

@@ -1,43 +0,0 @@
# 粘贴剪贴板图片
复制图片后,直接在 Blender 当前编辑器按 `CTRL + V`;macOS 使用 `CMD + V`。AnyImage 会根据上下文决定如何使用图片,并将它 Pack 到当前 `.blend`。
## 支持的上下文
| 上下文 | 结果 |
| --- | --- |
| 3D View / Object Mode | 创建 Reference Image 或带材质的 Plane |
| Sculpt | 设置为笔刷 Texture |
| Vertex Paint | 设置为笔刷 Texture |
| Texture Paint | 设置为笔刷 Texture |
| Shader Editor | 创建 Image Texture 节点 |
| Compositor | 创建 Image 节点 |
| Geometry Nodes | 创建 Image Texture 节点 |
## 在 3D View 中
图片会放在鼠标所在的屏幕位置,并使用当前 3D Cursor 作为视图深度参考。默认导入为 `Reference Image`。
如果在操作面板中切换为 `Plane`,会创建基础四边形和 `O Mesh Plane` Geometry Nodes 修改器。操作面板只保留:
- **Import As**:在 Reference Image 与 Plane 之间切换
- **Shadeless**:让图片不受场景灯光影响
`Thickness` 与 `Subdivide` 直接在 Geometry Nodes 修改器中调整,不作为粘贴参数。
## 在 Node Editor 中
节点创建在鼠标位置。不同节点编辑器会选择对应的图片节点类型,不需要先保存临时图片再手动浏览文件。
## 与 Blender 原生粘贴共存
如果刚刚使用 `CTRL + C` 复制了 Blender 内部数据,随后按 `CTRL + V` 时会优先让 Blender 处理原生粘贴。只有剪贴板中存在新的图片数据时,图片粘贴功能才接管操作。
## 平台说明
- Windows:直接支持
- macOS:使用 `CMD + V`
- Linux Wayland:需要 `wl-clipboard`
- Linux X11:需要 `xclip`
剪贴板中没有可用图片时,操作会放行,不阻断 Blender 的其他粘贴行为。

View File

@@ -1,61 +0,0 @@
# Cutout
Cutout 用套索从一张 Image Empty 中选择区域,再生成 Surface、Balloon、Depth Balloon 或 Depth Surface。它适合从图片局部制作薄片、鼓包或带透视深度的网格。
## 创建
1. 在 Object Mode 的 3D Viewport 左侧工具栏选择 **Cutout**。
2. 沿 Image Empty 按住左键绘制;双击图片可选择完整画面。
3. 松开左键后,从饼菜单选择 Shape。
绘制时按 `F` 切换反选,右键或 `Esc` 取消。Cutout 使用非零环绕填充:反向绘制的内环可以形成孔洞。
## 选择 Shape
| Shape | 结果特点 | 是否需要深度模型 |
| --- | --- | --- |
| Surface | 保持平面轮廓,可增加真实厚度 | 否 |
| Balloon | 按轮廓平滑鼓起 | 否 |
| Depth Balloon | 在 Balloon 上加入图片深度高度 | 是,固定使用 MoGe-2 |
| Depth Surface | 使用相机点形成透视表面 | 是,使用 Preferences 中的 Mesh Depth Model |
四种结果都保留源 Image Empty,并在同一位置创建新网格。它们共享选区轮廓、UV、材质和基础三角网格,因此可以从同一张图多次生成不同 Shape 进行比较。
无论是否启用 Fit to Foreground,套索超出图片的部分都不会生成网格,源图中 `Alpha <= 0.1` 的像素也始终从 Region 中排除。双击使用完整画面时同样只为 `Alpha > 0.1` 的 Region 生成网格。
## 工具设置
选项按执行意图排列:
- **Fit to Foreground**:用 BEN2 把几何套索贴合到其中的前景主体,默认关闭。
- **Mesh Sampling Spacing**:相邻网格采样点大约对应多少源图像素;数值越小,三角网格越密。
- **Generate Normal Map**:是否使用 MoGe-2 生成法线贴图,默认关闭;空间由 Shape 自动决定。
Surface 与 Balloon 在 **Generate Normal Map**、**Fit to Foreground** 都关闭时完全在 Blender 中生成,不需要 AI Environment。开启任一 AI 选项,或选择 Depth Shape 时,会提交后台任务。
Mesh Sampling Spacing 先按完整源图换算期望密度,再由 Cutout 区的 **Maximum Cutout Mesh Resolution** 限制裁切区域的最长采样边。这个网格上限对 Surface、Balloon 与 Depth Shape 完全相同。**Maximum AI Input Size** 只限制 Fit to Foreground、Depth 和 Normal Map 的模型分析图;颜色贴图始终保留原裁切分辨率。建议先用默认 `32` 观察形状,轮廓需要更细时再逐步减小。
## 调整修改器
四个 Shape 都使用同一个 `O Image Cutout` Geometry Nodes 修改器。创建时 Shape 菜单已经设为饼菜单所选类型;切换 Shape 后,Blender 只显示该类型实际使用的输入。常用参数包括:
- **Thickness**:Surface / Depth Surface 使用场景距离;Balloon / Depth Balloon 使用鼓起高度倍率。
- **Cleanup**:碎岛面积阈值;创建时根据 Mesh Sampling Spacing 和图片平面尺寸自动写入。为便于在修改器中调节,显示值放大了 `100` 倍,节点内部会还原为实际面积,因此这次显示缩放不会改变默认清理效果。
- **Smooth Iterations**:位于外层,控制平滑次数,默认 `0`;**Smooth** 位于 Options 中,控制每次平滑的混合强度。四种 Shape 都可使用。
- **Depth Scale**:Depth Shape 的深度强度;`0` 回到基础平面或 Balloon。
两个 Depth Shape 共用 **Base Plane Depth**;Depth Balloon 还提供 **Depth Direction**。生成前会检查最终 Region 输入的 Alpha:`Alpha < 0.1` 的像素达到 `5%` 时,根据有效 Region 自适应 Base Plane Depth,并为 Depth Balloon 拟合 Direction;不足 `5%` 时使用稳定的固定基准。这个判断不依赖双击全图或 Fit to Foreground。插件自动写入的 Depth Image 与 Uniform Scale 位于折叠的 Data 面板,一般无需手动修改。
Balloon 的轮廓高度算法由扩展 Preferences 中的 **Balloon Profile** 控制:Poisson 更平滑,Teddy 更强调轮廓骨架。修改默认值只影响之后创建的 Shape。
## Fit to Foreground 与 Normal Map
Fit to Foreground 只在“套索与源图可见 Alpha 的交集”内收紧最终 Region,不把 AI 生成的颜色写回原图。材质使用按最终 Region 包围矩形裁切的当前 Image Empty 源图,保留源颜色和原 Alpha,不把 RegionMask 写入 Alpha。
Normal Map 关闭时使用几何法线。开启后,Depth Surface 自动使用 Object Space,把模型法线编码到 Cutout 对象局部轴;Surface、Balloon 与 Depth Balloon 自动使用 Tangent Space,作为相对图片平面的浮雕细节。用户不直接选择 Normal Space,两种路径都不根据 Geometry Nodes 的成品网格再次烘焙。Normal Map 保持模型分析分辨率,Color Image 仍保留原始裁切分辨率;Depth 与 Normal Map 同时需要时共享同一次 Prediction,不会重复运行模型。
## 当前范围
Cutout 只支持静态图片。图片工具创建的透明结果和 Upscale 结果都可以继续作为输入;Cutout 始终以当前 Image Empty 的实际图片为颜色来源。
轮廓、深度场、节点资产与法线空间的实现见 [Cutout Tool 实现原理](../internals/operators/cutout-tool.md)。

View File

@@ -1,36 +0,0 @@
# Depth Plane
Depth Plane 将一张静态图片转换为带深度位移的网格,适合快速场景搭建、2.5D 合成和立体插画。
## 创建
1. 在 3D View 中选中 Image Empty。
2. 右键选择 **AnyImage > Convert to Depth Plane**。
3. 等待状态栏提示任务完成。
转换不会弹出设置对话框。深度模型和分辨率使用 **Edit > Preferences > Extensions > AnyImage > Depth & Normal** 中的默认值。
## 选择模型
### MoGe-2
默认推荐。MoGe-2 估算尺度一致的深度与相机信息,使用 **MoGe Resolution Level** 控制处理精度。第一次使用建议保持 `5`。
## 调整结果
生成对象使用扩展资产中的 `O Image Depth Plane` Geometry Nodes 修改器:
- **Subdivide**:控制基础平面细分级别;
- **Thickness**:增加背面和侧壁,`0` 输出单面;
- **Depth Amount**:控制图片深度位移强度。
结果颜色来自当前 Image Empty,深度由所选模型分析。转换会以一个 Undo 步骤替换源 Empty;如果需要保留原对象,请在转换前复制一份。
## 当前输入范围
Depth Plane 当前只支持静态图片。视频与序列帧可以转换为普通 [Plane](mesh-plane.md),或使用 [Remove Background / Upscale](animation.md) 逐帧处理。
实现中的深度场、坐标锚定和节点流程见 [Convert to Plane 实现原理](../internals/operators/convert-to-plane.md)。
## 分析分辨率与颜色
Depth 输入还受 Environment 区的 **Maximum AI Input Size** 约束,默认最大长边为 `2048 px`。超过时只会等比缩放模型分析输入;结果材质仍使用原始颜色图片,不会改用限制后的临时图。

View File

@@ -1,54 +0,0 @@
# 常见问题
## 找不到 AnyImage 右键菜单
AnyImage 菜单只会在选中 Image Empty 时出现。确认对象类型是 Empty,并且显示方式为 Image。普通 Mesh、图片编辑器中的图片或未选中的 Image Empty 都不会显示入口。
## 转换入口是灰色的
通常有以下原因:
- 另一个 AnyImage 任务正在运行。
- 当前对象不是 Image Empty。
Environment 或默认模型缺失不会让四个 AI 功能变灰;点击功能会根据状态打开 **Set Up AI Environment…** 或 **Download Required Models…**。Balloon 的 **Contour**、**Depth** 与 Depth Model 会在 AI 未就绪时单独禁用,同一位置提供安装按钮。
## Environment 安装失败
- 海外源访问不稳定时重新打开 **Set Up AI Environment…**,勾选 **Use Mirror**。
- 检查 Blender 的 **Online Access** 是否开启。
- 确认 Storage Root 至少有约 1 GB 可用空间且可写。
- 查看 Storage Root 下的 `install.log`。
安装模型失败不会删除已经成功创建的 Environment。可以稍后使用 **Download Required Models…** 补齐默认模型,或在 AnyImage 扩展偏好设置中逐项重试。
## 模型下载很慢或失败
模型会尝试 Hugging Face 和 OmooLab 备用来源。检查网络、代理和磁盘空间后重新点击 Download。模型直链连接或长时间无数据时会自动尝试下一来源。
## 第一次转换较慢
第一次任务需要启动 Job Server 并把模型载入内存。Server 运行且模型仍在缓存中时,后续同类任务通常会更快。
## Upscale 很卡或提示内存不足
三个高清模型都使用分块推理,但完整输出数组、PNG 编码和 Blender 图片载入仍取决于输入与 4× 结果尺寸。Environment 区的 **Maximum AI Input Size** 默认限制输入长边为 `2048 px`;可以降低该值,或改用 CPU。DirectML 或其他 provider 无法分配资源时,任务也会提示降低这个全局值。
## Depth Plane 太平或深度方向反了
在 Geometry Nodes 修改器中调整 **Depth Scale**。增大绝对值会加强深度,使用负值可以反转前后关系。
## Cutout Depth Surface 边缘破碎或细节太多
先增大 **Mesh Sampling Spacing** 降低基础网格密度,再调整 **Cleanup** 清理小块,并用 **Smooth** 稳定表面。必要时降低模型分辨率。
## 动画面片不播放
检查源 Image Empty 的 **Image Frames** 是否大于 `1`,并确认场景时间正在变化。生成后的动画还需要对应 Job 文件夹存在;文件被移动或删除时应重新定位序列。
## 可以删除 jobs 文件夹吗
静态图的部分结果会 Pack 到 `.blend`,但视频与序列帧会直接引用 Job 文件。只有确认没有工程继续引用对应目录时才应删除。
## 如何完全撤销一次转换
转换完成后直接按 `Ctrl+Z`。AnyImage 会把网格、材质、图片和修改器的创建合并为一次 Blender Undo 操作。

View File

@@ -1,82 +0,0 @@
# Image 编辑工具
Image Box、Lasso、Line、Polyline 与 Perspective 直接编辑静态 Image Empty,并把结果 Pack 到当前 `.blend`。前四个工具组成 Mask 工具组;Perspective 是紧随其后的独立工具。
## 选择工具
| 工具 | 用途 |
| --- | --- |
| **Image Box** | 用矩形保留或清理图片区域 |
| **Image Lasso** | 手绘自由选区 |
| **Image Line** | 用一条直线清理一侧 |
| **Image Polyline** | 逐点构建精确多边形 |
| **Image Perspective** | 将任意顺序输入的四个角矫正为矩形 |
在 3D Viewport 左侧工具栏长按 Image Mask 图标,可以在 Box、Lasso、Line 与 Polyline 之间切换。**Fit to Foreground** 只显示在 Box / Lasso;Line / Polyline 保持几何直线边界。Perspective 使用独立图标。
<div class="screenshot-placeholder" markdown>
**截图占位:Image 工具组与 Tool Settings**
显示包含四个工具的 Image Mask 组、独立 Perspective,以及 Box / Lasso 的 Fit to Foreground。
目标文件:`images/image-tools/tool-group.png`
</div>
## 选择图片
直接在当前 Active 图片上开始绘制。如果第一次点击的是另一张 Image Empty,这次点击只会选中它;下一次操作才开始编辑。点击空白处或非图片对象时,继续编辑当前 Active 图片,不会移动对象。
没有 Active Image Empty 时,工具会提示先选择图片。切换到其他工具会立即取消正在进行的绘制。
## 清理选区
### Box 与 Lasso
- Box:按住左键拖出矩形,松开后执行。
- Lasso:按住左键绘制轮廓,松开后执行。
### Line
从起点拖到终点,松开后保留直线左侧的内容。按住 `Ctrl` 以 5° 为单位吸附方向;需要另一侧时按 `F` 翻转。
### Polyline
逐点单击构建多边形。鼠标接近起点时会显示闭合圆环;单击圆环或按 `Enter` 完成,`Backspace` 删除最近一点。
绘制过程中按 `F` 切换反选。反选是一个持续到本次操作完成的状态,不需要按住:
- 关闭:保留选区内;
- 开启:保留选区外。
Cutout、Box 与 Lasso 分别保存各自的 **Fit to Foreground** 设置;在一个工具中切换不会改变另外两个工具。启用后,BEN2 会先把几何 Region 收紧到其中的前景,再应用反选。例如框住一个苹果并开启反选,最终会清理苹果本身、保留其余内容。Line / Polyline 不提供 Fit to Foreground,以保持直线边界和明确的裁切方向。
结果会直接更新当前 Empty 的图片,并把画布紧裁切到最终可见 Alpha。只有一个 Image Empty 使用该图片时,工具更新原 Image;两个或更多 Image Empty 共用它时,工具为当前 Empty 创建独立结果,其他 Empty 保持不变。对象 Transform 不变,显示尺寸与 Image Offset 会更新,使剩余像素保持在原图片平面上的位置。透明区只修改 Alpha,保留源 RGB 供纹理过滤使用。未形成有效区域的单击会静默结束;有效选区没有可见像素时只显示警告,不修改图片。
<div class="screenshot-placeholder" markdown>
**截图占位:Polyline 与反选**
显示半透明选区面、虚线边界,以及状态栏中的 `F toggles Invert`。
目标文件:`images/image-tools/polyline-invert.png`
</div>
## 四点透视
1. 任意顺序单击四个角点。
2. 第四点后,在 Viewport 中央检查矫正预览。
3. 左右移动鼠标调整宽高比;按住 `Ctrl` 可吸附到 `1:1`、`4:3`、`3:4`、`16:9`、`9:16`。
4. 左键确认。
`Backspace` 返回上一步,右键或 `Esc` 取消。四点会自动排序,点击顺序不会改变输出旋转。
Perspective 只执行图片 Homography 与紧裁切,不提供 Fit to Foreground,也不尝试从当前 View 推导真实相机。结果保持源 Empty 的 Transform,并按原四边形覆盖范围近似更新显示位置与尺寸。选区完全位于图片外或没有可见像素时不会执行。
<div class="screenshot-placeholder" markdown>
**截图占位:Perspective 比例确认**
显示中央图片预览、蓝色边框、当前比例与底部操作提示。
目标文件:`images/image-tools/perspective-aspect.png`
</div>
实现结构与像素规则见 [Image Tool 实现原理](../internals/operators/image-tool.md)。

View File

@@ -1,27 +0,0 @@
# Plane
Plane 把 Image Empty 转换为带图片材质的普通网格,不运行 AI,也不生成深度估算。它适合广告牌、图片卡片、背景板,以及视频或序列贴图。
## 创建
1. 在 3D View 中选中 Image Empty。
2. 右键选择 **AnyImage > Convert to Plane**。
3. 在 Last Operation 中按需调整 **Subdivide**。
转换会立即创建结果并替换源 Empty。新对象继承原对象的 Transform、图片显示比例和 Offset;需要保留源 Empty 时,请先复制对象。
## Geometry Nodes
结果使用 `O Mesh Plane` 修改器:
- **Subdivide** 使用二分级别控制网格密度,`3` 表示短边约 `8` 段;
- **Thickness** 沿平面法线增加厚度,`0` 输出真正的单面;
- **Material** 由 AnyImage 自动连接。
原始 Mesh 数据只有一个四边形,细分和厚度都由修改器实时生成,可以在转换后继续调整。
## 剪贴板 Plane
在 3D View 中粘贴剪贴板图片,并在 Last Operation 把 **Import As** 改为 **Plane**,也会创建同样的基础网格和 `O Mesh Plane` 修改器。该入口还可选择 **Shadeless**;Image Empty 的 Convert to Plane 入口不提供 Shadeless 选项。
模块与节点分工见 [Convert to Plane 实现原理](../internals/operators/convert-to-plane.md)。

View File

@@ -1,66 +0,0 @@
# 模型、设备与存储
AnyImage 把运行环境、模型和任务结果放在 Blender 之外,多个 Blender 版本可以共享同一套数据。
## 功能与模型设置
打开 **Edit > Preferences > Extensions > AnyImage**,可以查看和下载模型。右键菜单中的 **AI Environment Settings…** 会直接打开这里。Environment 检测后紧接 Diagnostics;其余设置分为默认展开、可折叠的 Depth & Normal、Remove Background、Upscale 和 Cutout 功能区,模型不会再集中显示在单独列表中。
- **Ready** 表示模型文件已经就绪。
- **Download** 用于按需下载模型。
- Depth & Normal 与 Upscale 的下载按钮位于当前模型选择项下方;切换选择后,可直接下载对应模型。
- Depth & Normal 只列出三档 MoGe-2;Depth Plane 与 Cutout 共用这个显式选择。
- Remove Background 使用的 BEN2 固定模型显示在对应功能区;Cutout 区只包含 Balloon Profile。
- 默认安装会准备 MoGe-2 ViT-S Normal、BEN2 Base 和 Real-ESRGAN General WDN x4v3。
- 更大的 MoGe-2,以及可选的 SAFMN Real x4、Real-ESRGAN x4plus 需要时再下载即可。
- 可公开取得的 ONNX 制品使用经过大小与 SHA-256 校验的原址,并以 OmooLab R2 为镜像;不会把源码仓库名称推测成 Hugging Face 下载地址。
- Real-ESRGAN General WDN x4v3 与 SAFMN Real x4 的上游只发布 PyTorch 权重,当前 ONNX 均从官方权重导出并仅从 R2 下载。
较大的模型不一定适合所有图片。建议先用默认模型完成构图,再针对细节与性能需求比较其他模型。
## AI 输入与 Cutout 网格尺寸
Environment 区的 **Maximum AI Input Size** 默认 `2048 px`,可在 `256–8192 px` 之间调整。它是 Upscale 的可执行源图长边上限,也是 Depth、Normal Map 与 Fit to Foreground 模型分析输入的最大长边;颜色贴图仍使用原始分辨率。Cutout 区的 **Maximum Cutout Mesh Resolution** 默认 `1024`,独立限制所有 Shape 的网格采样密度,不会因为某个 Shape 是否使用 AI 而改变含义。
## Device
扩展偏好设置中的 **Device** 控制 Depth Plane、Cutout 的 AI 增强、Remove Background 和 Upscale 等正式任务。
- Windows 通常优先使用 DirectML。
- Apple Silicon macOS 通常优先使用 CoreML。
- Provider 不可用时会回退到 CPU。
Debug 面板中的 **Debug Device** 只影响 Debug 任务,不会覆盖正式任务的 Device。
## Session 缓存
连续使用同一模型和 Device 时,Server 会复用已经加载的 ONNX Session。缓存分为 BEN2、几何模型和 Upscale 三个族,每个族只保留最近使用的一套;同族切换模型或 Device 时释放旧 Session。如果高清推理遇到 provider 资源错误,也会立即卸载该 Session。
## Storage Root
默认 Storage Root 是 `~/.anyimage`,主要包含:
```text
~/.anyimage/
├── .venv/ # 独立 Python Environment
├── models/ # 模型文件
├── jobs/ # 每次任务的结果与 job.log
├── install.log # Environment 安装日志
└── server.log # 带 Job ID 的任务主线与 Job Server 日志
```
更改 Storage Root 相当于切换到另一套环境、模型和任务数据。AnyImage 不会自动移动旧目录。
## Job 文件夹
每个任务会在 `jobs/` 下创建一个独立目录。单张图生成的关键贴图通常会 Pack 到 `.blend`;视频和序列帧则继续引用 Job 目录中的文件。
可以清理已经确认不再使用的任务目录,但清理前应检查是否有 `.blend` 仍引用其中的序列帧。
## 日志
遇到安装或 Server 问题时:
- 在扩展偏好设置中点击 **Open Server Log** 查看带时间、Job ID 与 Job Type 的运行主线。
- 具体 Job 的 Handler 细节和完整 traceback 查看 `jobs/<job-id>/job.log`。
- Environment 安装问题查看 Storage Root 下的 `install.log`。
- 修改 Storage Root 后,确认你检查的是当前目录中的日志。

View File

@@ -1,23 +0,0 @@
# 去除背景
Remove Background 使用 BEN2 为 Image Empty 生成透明前景,不创建网格。
## 使用
1. 选中 Image Empty。
2. 在 3D View 中右键选择 **AnyImage > Remove Background**。
3. 等待状态栏提示任务完成。
结果替换当前 Empty 的图片,对象 Transform、显示尺寸、Offset 和播放设置保持不变。单图结果会 Pack 到 `.blend`;视频或序列帧结果继续引用 Job 目录。
## Alpha 处理
推理前,AnyImage 会用输入 Alpha 遮住透明区域中的 RGB。结果颜色仍来自源图,只采用 BEN2 生成的前景 Alpha;源图已有透明度时,两者会相乘。因此 Image Mask 或外部软件已经清理的范围不会被重新扩张。
当前入口不提供 BEN2 的额外细化参数。需要手动控制范围时,可以先使用 [Image Box / Lasso](image-tools.md),也可以在结果材质中继续调整 Alpha。
## 图片序列
Remove Background 支持单图、Movie 和 Image Sequence。处理帧数、偏移和播放起点见[视频与序列帧](animation.md)。
任务与结果替换流程见 [Remove Background 实现原理](../internals/operators/remove-background.md)。

View File

@@ -1,27 +0,0 @@
# 高清放大
Upscale 用 ONNX 模型把 Image Empty 的 RGB 放大 4 倍,不创建网格。它支持单图、Movie 和 Image Sequence,并以 Lanczos 独立放大源 Alpha 后重新合并。
## 使用
1. 在 AnyImage 扩展偏好设置中选择 **Default Upscale Model**。
2. 按需要设置 Environment 区的 **Maximum AI Input Size**;默认 `2048 px`,最高 `8192 px`。
3. 选中 Image Empty,在右键 **AnyImage** 菜单中点击 **Upscale**。
菜单项只显示本次操作预计生成的 4× 尺寸。当前图片长边必须小于全局上限;达到上限后 Upscale 会置灰,因此默认设置下单次结果长边小于 `8192 px`。完成后,当前 Empty 会直接使用放大结果;原 Image 没有其他引用时沿用原名并移除旧数据,存在其他引用时则为当前 Empty 保留独立结果。
## 模型选择
- **Real-ESRGAN General WDN x4v3**:默认模型,速度快,细节处理较保守。
- **SAFMN Real x4**:速度与细节较均衡。
- **Real-ESRGAN x4plus**:生成的细节更明显。
后两项可在扩展偏好设置中按需下载。
AnyImage 不追踪高清结果的原图引用,Upscale 始终处理当前 Image。只要当前长边仍小于全局上限,就可以再次放大;达到上限后不能继续执行。
把高清结果继续转换为 Depth Plane 时,颜色材质和分析都以当前 Image 为来源,分析输入仍遵守全局上限;Cutout 同样只缩放裁切后的分析输入,不会把限制后的临时图用作 Color。
如果显存或内存不足,请降低 **Maximum AI Input Size**,或改用 CPU。
分块推理、Alpha 合并与输入限制见 [Upscale 实现原理](../internals/operators/upscale.md)。

View File

@@ -1,41 +0,0 @@
# 选择工作流
先确定需要的是图片数据、普通平面、整图深度,还是局部网格。
| 工作流 | 输入 | 结果 | AI |
| --- | --- | --- | --- |
| Clipboard Image | 剪贴板静态图 | Reference、Plane、节点或笔刷纹理 | 不需要 |
| Convert to Plane | 单图、Movie、Sequence | 带材质的普通网格 | 不需要 |
| Convert to Depth Plane | 静态图 | 带深度位移的整图面片 | MoGe-2 |
| Cutout | 静态图局部选区 | 四种局部 Shape | 按 Shape 与选项决定 |
| Remove Background | 单图、Movie、Sequence | 透明图片或序列 | BEN2 |
| Upscale | 单图、Movie、Sequence | 4× 图片或序列 | Upscale 模型 |
| Image Mask / Perspective | 静态图 | 原位更新的裁切或矫正图 | Fit to Foreground 时使用 BEN2 |
## 普通 Plane
只需要图片卡片、背景板或视频贴图时,选择 **Convert to Plane**。它不分析深度,Thickness 与 Subdivide 可以在 Geometry Nodes 中随时调整。
[阅读 Plane 指南](mesh-plane.md)
## Depth Plane
需要把整张静态图快速变成可调浮雕时,选择 **Convert to Depth Plane**。深度模型与分辨率在扩展 Preferences 中设置。
[阅读 Depth Plane 指南](depth-plane.md)
## Cutout
只需要图片局部,或希望按轮廓生成 Balloon / Depth Surface 时,使用工具栏中的 **Cutout**。普通 Surface 与 Balloon 可以完全在 Blender 内完成,Fit to Foreground、Generate Normal Map 与 Depth Shape 按需调用 AI。
[阅读 Cutout 指南](cutout.md)
## 图片处理
只想得到透明前景,不需要网格时使用 [Remove Background](remove-background.md)。需要增加像素尺寸时使用 [Upscale](upscale.md)。两者都支持 Movie 和 Image Sequence。
## Image 编辑
Box、Lasso、Line 和 Polyline 适合直接清理 Alpha;Perspective 适合把图片中的四边形内容矫正为矩形。结果都会写回当前 Image Empty,并保持对象 Transform。
[阅读 Image 编辑工具](image-tools.md)

View File

@@ -10,45 +10,3 @@ AnyImage 是一款 Blender 扩展,可以粘贴和编辑图片、创建普通 P
- 用 **Cutout** 从套索选区生成 Surface、Balloon 与两种 Depth Shape
- 用 **Image Tools / Perspective** 直接清理或矫正 Image Empty
- 用 **Remove Background / Upscale** 处理单图、视频和序列帧
!!! info "运行环境与 Blender 分离"
AI 计算运行在独立 Environment 中,不会把 ONNX Runtime 等依赖安装进 Blender Python。Blender 只负责界面、任务提交和结果导入。
## 从这里开始
<div class="grid cards" markdown>
- **安装 AnyImage**
下载与你的系统匹配的扩展包,完成 Environment 与默认模型配置。
[阅读安装指南](getting-started/installation.md)
- **五分钟上手**
从导入一张静态图片开始,创建第一个 Depth Plane。
[开始第一个作品](getting-started/quick-start.md)
- **选择工作流**
对比 Plane、Depth Plane、Cutout、去背景、高清放大和图片编辑。
[查看工作流](guides/workflows.md)
- **了解实现**
从项目主干、进程边界和功能模块了解内部实现。
[阅读内部实现总览](internals/index.md)
</div>
## 使用要求
- Blender 4.5 或更高版本
- Windows x64、macOS Apple Silicon 或 Linux x64
- 安装 Environment 与模型时需要网络连接
- AI 功能需要对应模型;普通 Plane、基础 Image 编辑与不带 AI 选项的 Cutout Surface / Balloon 可离线使用
遇到安装、模型或任务问题时,查看[常见问题](guides/faq.md)。

View File

@@ -1,6 +1,6 @@
# 程序架构
AnyImage 由 Blender 交互层、BlendJob Runtime 和独立 Job Server 三部分组成。这个分层让 Blender Python 不需要加载 ONNX Runtime、Pillow 或模型权重,同时保证所有 Blender 数据仍在主线程创建和修改。
AnyImage 由 Blender 交互层、BlendJob Runtime 和独立 Job Server 三部分组成。AI 依赖运行在 Server Environment 中,Blender 数据在主线程创建和修改。
## 对应文件
@@ -12,17 +12,18 @@ src/anyimage/
├── preferences.py # Storage Root、Device 与功能默认值
├── properties.py # Scene 状态、模型状态与动态枚举
├── runtime.py # Environment 和 JobRuntime
├── common/ # Blender 侧共享原子能力
├── operators/ # Blender 侧业务入口
└── server/ # 独立计算端
```
## 职责边界
## 职责
| 层次 | 负责 | 不负责 |
| --- | --- | --- |
| Blender 交互层 | UI、手势、RNA 状态、输入准备、结果导入、Undo | 在 Blender 进程加载 AI 依赖 |
| BlendJob Runtime | Environment、Server 生命周期、队列、进度、取消、Job 目录 | AnyImage 的图像与几何业务 |
| Job Server | 媒体解码、ONNX 推理、数组派生、文件输出 | 创建 Blender 对象或访问 `bpy` |
| 层次 | 职责 |
| --- | --- |
| Blender 交互层 | UI、手势、RNA 状态、输入准备、结果导入和 Undo |
| BlendJob Runtime | Environment、Server 生命周期、队列、进度、取消和 Job 目录 |
| Job Server | 媒体解码、ONNX 推理、数组派生和文件输出 |
Operator 向 Server 发送普通字典。Server 返回文件名和 Job 目录;Operator 的 `response()` 再读取这些文件,并在 Blender 主线程中创建 Image、Mesh、Material 和 Geometry Nodes Modifier。
@@ -58,12 +59,8 @@ sequenceDiagram
Op->>Op: 导入数据并提交 Undo
```
这条边界也决定了模块依赖方向:`server/` 不导入 `bpy` 或 `operators/`;Blender 侧可以读取 Server 产物,但不会复用 Server 中的 Blender 无关算法来创建 RNA 数据。
`server/` 保持纯 Python 实现;Operator 在 Blender 主线程读取产物并创建 RNA 数据。
## 共享能力 { #shared-capabilities }
## 共享能力
`operators/common/` 只保存多个生产功能真实复用的稳定原子能力;完整工作流、功能参数和专用算法留在所属模块。公共层不保留旧路径转发函数,也不为了测试或资产工具建立入口。
全部可由功能模块调用的函数见[Common 公共函数](common.md)。该索引只列 `common/` 外部的生产调用,不重复列出公共模块内部辅助函数;创建新功能前先核对该页,避免重新实现 Image 引用分流、像素方向、Job 输入生命周期、数据块命名、节点资产加载和对象激活逻辑。
新增公共函数前应确认至少有两个真实消费者,或它是两个公共流程共同依赖的原子能力。只有单一功能使用的逻辑应留在所属模块;删除或移动能力时同步清理注册、测试、工具脚本和文档,不保留兼容胶水。
`common/` 提供 Blender 侧复用的 Image 数据、Region、Depth、Object 和 AI Job 原子能力。公开函数见[Common 公共函数](common.md)。

View File

@@ -1,12 +1,10 @@
# Common 公共函数
`src/anyimage/operators/common/` 集中已经被多个功能复用的 Blender 侧原子能力,目的是让开发新功能时能够先查找并复用现有实现,避免重复造轮子。
本页只列功能模块直接调用的公共函数,不列 `common/` 内部辅助函数、测试或资产工具专用函数,也不列公共类型。
`src/anyimage/common/` 提供多个功能复用的 Blender 侧原子能力。
## Image 数据块
`image_data.py` 处理 Blender `Image` 数据块及其在 Image Empty 上的使用方式,包括像素读取、动画帧、Job 输入、结果 Image 创建和 Empty 引用替换。它不处理 Viewport 手势或 Region 算法。
`image_data.py` 处理 Blender `Image` 数据块及其在 Image Empty 上的使用方式,包括像素读取、动画帧、Job 输入、结果 Image 创建和 Empty 引用替换。
| 函数 | 职责 |
| --- | --- |
@@ -30,16 +28,14 @@
| `image_data.replace_empty_image()` | 执行 Image Empty 结果替换、共享引用分流、旧数据清理、帧设置继承和可选画布对齐。 |
| `image_data.image_empty_bounds()` | 计算 Image Empty 在对象局部坐标中的实际显示边界。 |
`load_*_result_image()` 和 `create_pixel_result_image()` 只创建结果 Image;`replace_empty_image()` 负责把结果提交给当前 Empty。两者分别表达“创建产物”和“替换引用”。
## Image Region 与视图交互
这组公共能力使用固定术语:
- Region 是用户在图片空间圈定的业务范围;`RegionPath` 保存路径点、反选状态和填充规则。
- `RegionMask` 是 Region 的栅格化结果,只包含局部像素权重 `values` 和源图 `bounds`。
- Mask 只表示二维像素权重或 Alpha 运算,不作为手势、协议或业务范围的别名。
- Selection 只用于 Blender 对象选择;图片业务范围统一称为 Region,细化动作称为 Fit to Foreground。
- Mask 表示二维像素权重或 Alpha 运算。
- Selection 表示 Blender 对象选择;图片业务范围称为 Region,细化动作称为 Fit to Foreground。
`image_interaction.py` 处理 3D Viewport 中的点击、坐标投影、`ImageRegionGesture`、工具设置和预览绘制;`image_region.py` 负责图片空间 Region 的值、栅格化和 Alpha 裁切。前者负责用户如何圈定 Region,后者负责 Region 如何变成 Mask。
@@ -71,7 +67,7 @@
## Depth 产物
`depth_data.py` 负责 Blender 侧共享的 Depth 文件加载和相机尺度换算;Server 已经写好 EXR,Blender 不再从点场创建第二张 Depth Image。
`depth_data.py` 负责 Blender 侧共享的 Depth 文件加载和相机尺度换算,直接加载 Server 写出的 EXR。
| 函数 | 职责 |
| --- | --- |
@@ -80,23 +76,22 @@
| `depth_data.camera_depth_values()` | 从已加载 Depth Image 读取用于本地标定的 Z 与有效区。 |
| `depth_data.depth_uniform_scale()` | 根据相机内参、参考深度和 Image Empty 范围计算统一尺度。 |
## Mesh 结果构建
## Object 构建
`mesh_result.py` 集中 Convert to Plane 与 Cutout 共同使用的结果构建能力:创建材质、加载节点资产、设置修改器输入,并完成结果对象替换。
`object.py` 集中 Convert to Plane 与 Cutout 共用的材质、Modifier 和结果 Object 构建能力;`node.py` 负责加载节点资产。
| 函数 | 职责 |
| --- | --- |
| `mesh_result.create_image_material()` | 从 Image Empty 创建包含颜色、位移、Normal Map、Alpha 和动画设置的完整材质。 |
| `mesh_result.load_asset_node_group()` | 从随扩展分发的 Blend 资产加载指定 Node Group。 |
| `mesh_result.modifier_input_identifier()` | 按接口名称解析 Geometry Nodes Modifier 输入 Identifier。 |
| `mesh_result.set_modifier_input()` | 跨受支持 Blender RNA 形式设置 Modifier 输入。 |
| `mesh_result.finalize_object_result()` | 激活结果对象,并按需替换或保留源 Empty。 |
| `node.load_node_group()` | 从随扩展分发的 Blend 资产加载指定 Node Group。 |
| `object.create_image_material()` | 从 Image Empty 创建包含颜色、位移、Normal Map、Alpha 和动画设置的完整材质。 |
| `object.modifier_input_identifier()` | 按接口名称解析 Geometry Nodes Modifier 输入 Identifier。 |
| `object.set_modifier_input()` | 跨受支持 Blender RNA 形式设置 Modifier 输入。 |
| `object.finalize_object_result()` | 激活结果对象,并按需替换或保留源 Empty。 |
节点资产构建和验证专用函数位于 `tools/node_assets/`,不属于运行时公共能力。
## AI 任务准备
`ai_job.py` 处理多个 AI 功能共同使用的任务准备,包括输入路径校验、Environment 与模型检查、Device 选择和模型请求参数组装。DA3 参数只供 Debug Operator 使用;正式几何流程只组装 MoGe-2 参数。它不包含业务 Operator、结果导入或 Undo 操作。
`ai_job.py` 处理多个 AI 功能共同使用的任务准备,包括输入路径校验、Environment 与模型检查、Device 选择和模型请求参数组装。
| 函数 | 职责 |
| --- | --- |

View File

@@ -1,6 +1,6 @@
# 开发与发布
AnyImage 的仓库环境用于测试 Blender 侧代码、Server 纯 Python 模块、Manifest 和打包逻辑。AI Runtime 使用另一套 Python 3.12 Environment,不应与仓库开发环境混为一谈。
AnyImage 的仓库环境用于测试 Blender 侧代码、Server 纯 Python 模块、Manifest 和打包逻辑。AI Runtime 使用独立的 Python 3.12 Environment。
## 对应文件
@@ -9,11 +9,14 @@ AnyImage 的仓库环境用于测试 Blender 侧代码、Server 纯 Python 模
├── pyproject.toml
├── uv.lock
├── .python-version
├── pack.py
├── sync_models.py
├── tools/
│ ├── pack.py
│ ├── model_exports.py
│ ├── sync_models.py
│ ├── build_node.py
│ └── node_assets/
├── src/anyimage/blender_manifest.toml
├── tests/
├── tools/node_assets/
└── .github/workflows/
├── test.yml
├── build.yml
@@ -40,10 +43,10 @@ uv run pytest tests/test_addon.py tests/test_manifest.py tests/test_pack.py -v
## 扩展打包
```bash
uv run python pack.py
uv run pack
```
`pack.py` 为 `windows-x64`、`macos-arm64` 和 `linux-x64` 分别解析依赖 wheel,生成平台专属 Manifest,再把 `src/anyimage/` 与 wheel 写入 `dist/`:
`tools/pack.py` 为 `windows-x64`、`macos-arm64` 和 `linux-x64` 分别解析依赖 wheel,生成平台专属 Manifest,再把 `src/anyimage/` 与 wheel 写入 `dist/`:
```text
dist/
@@ -54,21 +57,29 @@ dist/
本地 `src/anyimage/wheels/` 是 wheel 解析的优先来源,用于携带尚未发布的 BlendJob 构建。依赖、wheel 或 Manifest 变化后,需要同时检查 `pyproject.toml`、`uv.lock`、`blender_manifest.toml` 和打包测试;本地 wheel 内容变化后运行 `uv lock --refresh`。
## 模型发布
```bash
uv run sync_models
```
`tools/sync_models.py` 从公开原址下载已有 ONNX;Real-ESRGAN General WDN x4v3 与 SAFMN Real x4 则从官方 PyTorch 权重调用 `tools/model_exports.py` 构建。所有制品通过固定大小、SHA-256、ONNX Checker 和 PyTorch/ONNX Runtime 数值对比后,逐文件上传到 R2。添加 `--prepare-only` 可仅准备和验证制品。
## 文档
```bash
uv run --only-group docs mkdocs serve
uv run --only-group docs mkdocs build --strict
uv run mkdocs serve
uv run mkdocs build --strict
```
站点导航由 `mkdocs.yml` 显式维护,因此移动页面时必须同时更新导航和相对链接。Mike 仍可用于本地版本化发布;CI 则直接构建严格模式站点并部署到 Cloudflare Worker。
站点导航由 `mkdocs.yml` 显式维护,因此移动页面时必须同时更新导航和相对链接。本地版本化发布使用 Mike;CI 构建严格模式站点并部署到 Cloudflare Worker。
## CI
```mermaid
flowchart TD
Main["push main / Pull Request"] --> Test["uv run --frozen pytest"]
Tag["push v* tag"] --> Release["测试 → pack.py → Release ZIP"]
Tag["push v* tag"] --> Release["测试 → tools/pack.py → Release ZIP"]
Docs["main 上文档变更"] --> Site["mkdocs --strict → Cloudflare"]
```
@@ -76,6 +87,6 @@ flowchart TD
## 许可证
AnyImage Extension 代码采用 GPL-3.0-or-later,完整文本见仓库 `LICENSE`。模型权重保持各自上游许可证;模型目录声明下载来源、文件校验和许可信息,技术文档不复制可能变化的制品清单。
AnyImage Extension 代码采用 GPL-3.0-or-later,完整文本见仓库 `LICENSE`。模型权重保持各自上游许可证;模型目录声明下载来源、文件校验和许可信息。
Geometry Nodes 资产必须由 Blender 构建和验证,见[节点资产](node-assets.md)。

View File

@@ -1,6 +1,6 @@
# 内部实现总览
AnyImage 的实现围绕一条清晰边界展开:Blender 进程负责交互与数据创建,独立 Job Server 负责模型推理和文件处理。两侧通过 BlendJob 传递可序列化参数与 Job 目录中的产物,不跨进程传递 Blender RNA 数据。
Blender 进程负责交互与数据创建,独立 Job Server 负责模型推理和文件处理。两侧通过 BlendJob 传递可序列化参数与 Job 目录中的产物。
```mermaid
flowchart TD
@@ -16,8 +16,6 @@ flowchart TD
## 项目主干
总览只展示稳定边界;各功能包的具体文件在对应实现页中展开。
```text
.
├── src/anyimage/
@@ -26,24 +24,21 @@ flowchart TD
│ ├── preferences.py # 扩展级配置
│ ├── properties.py # Scene 状态与模型枚举
│ ├── runtime.py # BlendJob Runtime 配置
│ ├── common/ # Blender 侧共享原子能力
│ ├── operators/ # 交互、任务提交与结果导入
│ ├── server/ # 无 Blender 依赖的计算端
│ ├── assets/ # 随扩展分发的节点资产
│ └── wheels/ # 随扩展分发的 BlendJob wheel
├── tools/ # Blender 资产构建与验证
├── tests/ # Python 自动化测试
├── pack.py # 多平台扩展打包
└── sync_models.py # ONNX 模型同步
├── tools/ # 打包、模型同步与 Blender 节点资产
└── tests/ # Python 自动化测试
```
## 文档分层
- [程序架构](architecture.md):注册、配置、线程和模块边界。
- [Common 公共函数](common.md):仅列生产功能从 `operators/common/` 外部调用的函数。
- [Common 公共函数](common.md):仅列生产功能从 `common/` 外部调用的函数。
- [Job Runtime 与 Server](runtime.md):任务生命周期、媒体输入和模型 Session。
- [Operator 实现索引](operators/index.md):按 `src/anyimage/operators/` 中的完整功能拆分实现页。
- [Job 产物](outputs.md):文件用途和 Blender 导入关系。
- [开发与发布](development.md):本地环境、测试、打包和 CI。
- [节点资产](node-assets.md):Geometry Nodes 的构建、加载与验证。
内部实现文档描述模块之间的职责和主要数据流。[Common 公共函数](common.md)按真实外部调用列出开发新功能时应优先复用的稳定入口;函数级分支、参数校验与异常处理仍以源码和测试为准。

View File

@@ -1,60 +1,52 @@
# 节点资产
AnyImage 将生产功能使用的可复用 Geometry Nodes 和 Shader 节点组统一构建到 `O_AnyImage.blend`,运行时只按名称加载,不动态创建节点组。
生产节点组由脚本构建到 `src/anyimage/assets/O_AnyImage.blend`;运行时只加载,不创建节点组。
## 对应文件
## 构建
```text
src/anyimage/
├── assets/O_AnyImage.blend
├── operators/common/mesh_result.py # 运行时资产加载与 Modifier 输入
└── operators/convert_to_plane/nodes.py # Plane 节点资产名称与加载入口
tools/node_assets/
├── build.py # 统一构建入口
├── node_graph.py # Cutout 资产构建专用节点图函数
├── build_mesh_plane.py
├── build_depth_plane.py
├── build_material_layers.py
├── build_shadeless.py
├── build_cutout.py
├── validate.py # 资产清单与基础接口
├── validate_mesh_plane.py
└── validate_cutout.py
```
## 构建与加载
```mermaid
flowchart TD
Scripts["tools/node_assets/build_*.py"] --> Build["Blender 后台构建"]
Build --> Blend["assets/O_AnyImage.blend"]
Blend --> Loader["operators/common/mesh_result.py"]
Loader --> Modifier["运行时 Node Group / Modifier"]
```
使用 Blender 而不是普通 Python 执行:
在项目根目录执行:
```bash
BLENDER_BIN="/c/Program Files/Blender Foundation/Blender 4.5/blender.exe"
"$BLENDER_BIN" --background --python tools/node_assets/build.py
"$BLENDER_BIN" --background --python tools/node_assets/validate.py
"$BLENDER_BIN" --background --python tools/node_assets/validate_mesh_plane.py
"$BLENDER_BIN" --background --python tools/node_assets/validate_cutout.py
uv run build_node
```
`build.py` 从空 Factory 文件开始,依次构建 `O Mesh Plane`、`O Image Depth Plane`、`O Image Layer`、`O Image Depth Layer`、`O Shadeless`、四个 Cutout 内部节点组与公开修改器节点组 `O Image Cutout`,再保存唯一资产文件。`O Image Cutout` 用 Menu Switch 包装四个内部组;运行时 Loader 只加载这个稳定入口,Blender 4.5 根据 Shape 菜单依赖只显示当前分支实际使用的输入。
脚本依次构建资产、运行三项 Blender 验证和完整测试,任一步失败即退出。
## 验证范围
脚本按 `--blender`、环境变量 `BLENDER_BIN`、项目 `.env`、`PATH` 的顺序查找 Blender。无法自动找到时,在项目根目录的 `.env` 中配置:
基础验证检查完整发布资产清单、节点树类型、Modifier 标记、接口顺序和 Fake User。专项验证继续检查:
```dotenv
BLENDER_BIN="/absolute/path/to/blender"
```
- Mesh Plane 的接口、细分、Thickness 和几何求值;
- `O Image Cutout` 的公开接口、Shape 菜单和四个内部组的依赖关系;
- 节点不重叠、主要连线保持从左向右;
- 局部 Group Input 未使用输出隐藏;
- Cleanup 只在四条分支之前执行,Smooth 只在菜单选择之后执行;
- Repeat Zone 数量和四种 Shape 的关键几何求值稳定。
`.env` 已被 Git 忽略。临时指定路径或跳过 Python 测试:
节点资产接口变化时,应运行全部四个 Blender 入口和完整 Python 测试。只修改 Python、Markdown 或打包逻辑时无需重建 `.blend`。Blender 自动生成的 `.blend1` 等备份不属于发布资产。
```bash
uv run build_node --blender "/path/to/blender"
uv run build_node --skip-tests
```
## 资产清单
| 节点组 | 构建模块 |
| --- | --- |
| `O Mesh Plane` | `build_mesh_plane.py` |
| `O Image Depth Plane` | `build_depth_plane.py` |
| `O Image Layer`、`O Image Depth Layer` | `build_material_layers.py` |
| `O Shadeless` | `build_shadeless.py` |
| `O Image Cutout` | `build_cutout.py` |
| `O Image Surface`、`O Image Balloon` | `build_cutout.py` |
| `O Image Depth Balloon`、`O Image Depth Surface` | `build_cutout.py` |
构建从空 Factory 文件生成全部节点组,关闭 `.blend1` 备份,并覆盖唯一发布资产。
## 修改节点组
1. 修改 `tools/node_assets/build_*.py`;
2. 更新对应验证;
3. 运行一键构建脚本;
4. 搜索并清理旧名称、接口和路径;
5. 检查 `.blend`、代码、测试和文档差异。
接口、节点、连线、默认值或布局变化时必须重建。只修改运行时 Python、Markdown 或打包逻辑时无需重建。
`O_AnyImage.blend1` 等备份不应提交或打包。

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 为输入。

View File

@@ -1,6 +1,6 @@
# Job 产物
Job 目录是 Server 与 Blender Operator 的交换边界。Server 写入普通图片、JSON 或 Debug NPZ;Blender 只读取 Job 返回值明确声明的文件,再创建最终 Image、Mesh、Material 与 Object。
Server 在 Job 目录写入图片、JSON 或 Debug NPZ;Blender 根据 Job 返回值读取文件并创建 Image、Mesh、Material 与 Object。
## 正式 Job
@@ -12,7 +12,7 @@ Job 目录是 Server 与 Blender Operator 的交换边界。Server 写入普通
| `generate-depth-plane-geometry` | `image.0001.*`、`z-depth.exr`、`depth.json` | 创建 Depth Plane |
| `generate-cutout-artifacts` | 可选 Region、Depth 与 Normal 文件 | 创建 Cutout Shape 与材质 |
两个 Depth Job 都不输出 Blender 网格。Server 保存可由 Blender 直接加载的模型空间 EXR;轮廓提取、三角化、Depth 标定以及 Blender 数据创建仍在主线程完成。
Depth Job 保存可由 Blender 直接加载的模型空间 EXR;Blender 主线程负责轮廓提取、三角化、Depth 标定和数据创建。
## Depth 与 Normal 文件
@@ -26,7 +26,7 @@ MoGe-2 Prediction 只存在于 Server 内存。`geometry/prediction_artifacts.py
| `tangent-normal.png` | Tangent Space Normal Map |
| `object-normal.png` | Object Space Normal Map |
Blender 通过 `operators/common/depth_data.py` 直接加载 EXR,设为 Non-Color 并 Pack;不会从 NumPy 点场再创建一张 Depth Image。Job 文件名不改变 Blender 数据块命名:Depth 与 Normal 仍分别使用 `depth_data_name()` 和 `normal_data_name()`。Blender 读取 Z 与 Alpha 只用于最终 Region 的本地标定,Geometry Nodes 直接采样已加载的 EXR。
Blender 通过 `common/depth_data.py` 加载 EXR,设为 Non-Color 并 Pack。Depth 与 Normal 数据块分别由 `depth_data_name()` 和 `normal_data_name()` 命名。Blender 读取 Z 与 Alpha 进行最终 Region 的本地标定,Geometry Nodes 采样已加载的 EXR。
## Cutout Region 产物
@@ -37,7 +37,7 @@ Blender 先把 `RegionPath` 栅格化为 `RegionMask`,再把局部 RGBA 作为
- 需要 Z Depth 时请求 `z-depth.exr` 与 `depth.json`;
- Normal Map 按空间请求 `tangent-normal.png` 或 `object-normal.png`。
未启用 Fit to Foreground,且不需要 Depth 或 Normal Map 时,Surface 与 Balloon 不提交 Job。最终颜色图仍由 Blender 从源 Image 裁切,并把 Alpha 乘以最终 `RegionMask`。
Surface 与 Balloon 的本地路径直接使用 `RegionMask` 构建 Shape。最终颜色图由 Blender 按 Region 范围裁切源 Image,并保留源 RGBA。
## Debug 产物
@@ -48,4 +48,4 @@ Blender 先把 `RegionPath` 栅格化为 `RegionMask`,再把局部 RGBA 作为
| `debug-ben2` | `foreground*.png` |
| `debug-upscale` | `prediction*.png` |
Debug Job 只用于直接检查模型适配层,不参与正式对象创建流程。
Debug Job 保存模型适配层的直接输出。

View File

@@ -1,6 +1,6 @@
# Job Runtime 与 Server
AnyImage 使用 BlendJob 管理独立 Python Environment、Server 进程与单 Worker 队列。Blender 侧只准备输入、提交普通字典参数并导入结果;`server/` 不依赖 `bpy`。
AnyImage 使用 BlendJob 管理独立 Python Environment、Server 进程与单 Worker 队列。Blender 侧准备输入、提交普通字典参数并导入结果。
## 目录职责
@@ -20,7 +20,7 @@ src/anyimage/
└── outputs/ # Debug Prediction 序列化
```
公开 Job 不互相导入实现。`jobs/` 只编排,稳定的原子能力放在对应领域目录中。
`jobs/` 编排任务,领域目录提供媒体、Region、Geometry、模型和输出能力。
## 生命周期
@@ -52,15 +52,15 @@ Job 在阶段边界调用 `context.check_cancelled()`,并把稳定产物写入
## 尺寸约束
`server/media/resolution.py` 提供 **Maximum AI Input Size**,默认 `2048 px`。它限制 Upscale 的可提交源图长边,以及 Depth、Normal Map、Fit to Foreground 的模型分析图长边;不会降低材质颜色图分辨率。
`server/media/resolution.py` 提供 **Maximum AI Input Size**,默认 `2048 px`。它限制 Upscale 的可提交源图长边,以及 Depth、Normal Map、Fit to Foreground 的模型分析图长边;材质颜色图使用原始分辨率。
Cutout 另有 Blender 侧的 **Maximum Cutout Mesh Resolution**。它限制所有 Cutout Shape 的网格采样密度,包括完全不使用 AI 的 Surface 与 Balloon。两个设置没有隐式绑定。
Blender 侧的 **Maximum Cutout Mesh Resolution** 统一限制所有 Cutout Shape 的网格采样密度。
`generate-depth-plane-geometry` 与 `generate-cutout-artifacts` 只接受一帧,因此 Depth Plane 与 Cutout 是静态图工作流。Remove Background 与 Upscale 继续通过媒体入口处理单图、视频和序列帧。
## 模型边界
## 模型
正式 Depth Plane、Cutout Depth 与 Normal Map 都只使用 MoGe-2。BEN2 用于 Remove Background 与 Fit to Foreground,Upscale 使用所选高清模型。DA3 只保留 `debug-da3` 及其 Debug 参数、模型下载与适配代码,不进入任何生产 Operator、Job 或自动回退。
Depth Plane、Cutout Depth 与 Normal Map 使用 MoGe-2,Remove Background 与 Fit to Foreground 使用 BEN2,Upscale 使用所选高清模型。
`ModelManager` 按模型族管理下载目录与 ONNX Session,同一族只保留最近使用的模型和 Device,避免同类模型重复占用资源。

View File

@@ -1,55 +0,0 @@
# 功能参考
本页集中列出 AnyImage 当前实现的主要入口。操作建议见[工作流选择](../guides/workflows.md),内部设计见[内部实现总览](../internals/index.md)。
## Blender 入口
| 入口 | 位置 | 输入 |
| --- | --- | --- |
| Paste Clipboard Image | 3D View / oode Editor 的 `Ctrl+V` 或 `Cmd+V` | 剪贴板静态图 |
| Convert to Plane | Image Empty 右键菜单 | 单图、Movie、Sequence |
| Convert to Depth Plane | Image Empty 右键菜单 | 静态图 |
| Remove Background | Image Empty 右键菜单 | 单图、Movie、Sequence |
| Upscale | Image Empty 右键菜单 | 单图、Movie、Sequence |
| Image Box / Lasso / Line / Polyline | 3D Viewport 工具栏 | 静态 Image Empty |
| Image Perspective | 3D Viewport 工具栏 | 静态 Image Empty |
| Cutout | 3D Viewport 工具栏 | 静态 Image Empty |
右键菜单只在当前对象是 Image Empty 时出现。AI 功能会在 Environment 或默认模型未就绪时转入安装流程;普通 Plane、未启用 Fit to Foreground 的 Image Tools 与基础 Cutout Surface / Balloon 不依赖 Environment。
## 结果类型
### Plane
普通 Plane 使用资产中的 `O Mesh Plane` Geometry Nodes,提供 Subdivide、Thickness 和 Material。Depth Plane 使用同一资产文件中的 `O Image Depth Plane`,增加 Depth Amount 与内部 Depth Image。
### Cutout Shape
Cutout 提供四种结果:
| Shape | `O Image Cutout` 内部分支 | 深度 |
| --- | --- | --- |
| Surface | `O Image Surface` | 无 |
| Balloon | `O Image Balloon` | Balloon Profile |
| Depth Balloon | `O Image Depth Balloon` | MoGe-2 高度 |
| Depth Surface | `O Image Depth Surface` | MoGe-2 相机点 |
四种 Shape 共用 `O Image Cutout` 修改器、规则三角基础面、UV 和 `o_balloon` 属性。Shape 菜单选择内部几何分支,Cleanup 在分支前执行且使用便于调节的百倍显示值,Smooth 在分支后执行;Depth Shape 根据 Region 透明比例选择固定或自适应 Base Plane Depth,Generate Normal Map 是启用开关:Depth Surface 自动使用 Object Space,其余 Shape 自动使用 Tangent Space;Fit to Foreground 按需启用 BEN2。
### 图片结果
Image Mask 和 Perspective 直接更新当前 Empty 的图片并 Pack。Remove Background 输出透明颜色图或序列。Upscale 通过三档 ONNX 模型把当前 Image 输出为 4× RGBA;当前长边达到全局输入上限时不可执行。
## 模型
- MoGe-2 ViT-S / ViT-B / ViT-L Normal
- BEN2 Base
- Real-ESRGAN General WDN x4v3
- SAFMN Real x4
- Real-ESRGAN x4plus
默认安装准备 MoGe-2 ViT-S Normal、BEN2 Base 和 Real-ESRGAN General WDN x4v3。其余模型在扩展 Preferences 中按需下载。
## Debug
Server 面板提供 DA3、MoGe-2、BEN2 与 Upscale Debug Job。它们保存模型适配层的直接 Prediction,不创建正式功能对象。文件说明见 [Job 产物](../internals/outputs.md)。

View File

@@ -1,27 +1,28 @@
@import url("https://fontsapi.zeoseven.com/292/main/result.css");
@import url("https://fontsapi.zeoseven.com/293/main/result.css");
/* [data-md-color-scheme="slate"] {
--md-code-hl-string-color: #8b8b8b;
--md-code-hl-keyword-color: #ffe8a3;
--md-code-hl-number-color: #ffe8a3;
--md-typeset-a-color: #ffe8a3;
}
[data-md-color-scheme="default"] {
--md-code-hl-string-color: #444444;
--md-code-hl-keyword-color: #bd9100;
--md-code-hl-number-color: #bd9100;
}
[data-md-color-scheme=slate][data-md-color-primary=black] {
--md-typeset-a-color: #ffe8a3;
}
[data-md-color-scheme=default][data-md-color-primary=black] {
--md-typeset-a-color: #bd9100;
} */
:root {
--md-text-font: "Open Sans", "Noto Sans SC", "Microsoft YaHei", sans-serif;
--md-code-font: "Cascadia Code", "SFMono-Regular", Consolas, monospace;
}
.md-typeset h1 {
font-weight: 700;
letter-spacing: -0.02em;
}
.md-typeset .grid.cards > ul > li {
border-radius: 0.5rem;
}
.md-typeset .screenshot-placeholder {
margin: 1rem 0;
padding: 2.5rem 1.25rem;
border: 2px dashed var(--md-default-fg-color--lighter);
border-radius: 0.5rem;
color: var(--md-default-fg-color--light);
text-align: center;
background: var(--md-code-bg-color);
}
.md-typeset .screenshot-placeholder p {
margin: 0.35rem 0;
--md-text-font: "LXGW WenKai";
--md-code-font: "LXGW WenKai Mono";
}

View File

@@ -12,20 +12,6 @@ nav:
- 开始使用:
- 安装与首次配置: getting-started/installation.md
- 五分钟上手: getting-started/quick-start.md
- 粘贴剪贴板图片: guides/clipboard-image.md
- 使用指南:
- 选择工作流: guides/workflows.md
- Image 编辑工具: guides/image-tools.md
- Cutout: guides/cutout.md
- Plane: guides/mesh-plane.md
- Depth Plane: guides/depth-plane.md
- 去除背景: guides/remove-background.md
- 高清放大: guides/upscale.md
- 视频与序列帧: guides/animation.md
- 模型、设备与存储: guides/models-and-storage.md
- 常见问题: guides/faq.md
- 参考:
- 功能参考: reference/features.md
- 内部实现:
- 总览: internals/index.md
- 程序架构: internals/architecture.md
@@ -102,8 +88,6 @@ plugins:
- search
markdown_extensions:
- attr_list
- md_in_html
- mkdocs-click
- admonition
- pymdownx.highlight:
@@ -113,7 +97,9 @@ markdown_extensions:
- pymdownx.inlinehilite
- pymdownx.snippets
- pymdownx.details
- pymdownx.superfences
- pymdownx.superfences:
custom_fences:
- name: mermaid
class: mermaid
- pymdownx.tabbed:
alternate_style: true

View File

@@ -7,23 +7,33 @@ requires-python = ">=3.11,<3.12"
license = "GPL-3.0-or-later"
dependencies = ["blendjob==0.1.14"]
[project.scripts]
pack = "tools.pack:main"
build_node = "tools.build_node:main"
sync_models = "tools.sync_models:main"
docs = "tools.docs:main"
[build-system]
requires = ["setuptools>=77"]
build-backend = "setuptools.build_meta"
[dependency-groups]
dev = [
"av==17.0.0",
"bpy==4.5.3",
"huggingface-hub==1.4.1",
"mike>=2.1.3,<3",
"mkdocs>=1.6.1,<2",
"mkdocs-click==0.9.0",
"mkdocs-material>=9.6.16,<10",
"numpy>=1.26,<2.0",
"onnx==1.19.0",
"onnxruntime==1.24.4",
"pillow==12.1.1",
"pip>=25,<27",
"pytest",
"torch==2.13.0",
"trimesh==4.11.2",
]
docs = [
"mike>=2.1.3,<3",
"mkdocs>=1.6.1,<2",
"mkdocs-material>=9.6.16,<10",
]
[tool.uv]
package = false
[tool.setuptools]
packages = ["tools"]

Binary file not shown.

View File

@@ -0,0 +1 @@
"""Shared Blender-side image, node, object, interaction, and AI Job helpers."""

View File

@@ -2,9 +2,9 @@ from pathlib import Path
import bpy
from ...preferences import addon_preferences
from ...properties import ai_ready, model_ready, model_record
from ...runtime import runtime
from ..preferences import addon_preferences
from ..properties import ai_ready, model_ready, model_record
from ..runtime import runtime
BEN2_IMAGE_SUFFIXES = {".jpg", ".jpeg", ".png", ".webp"}
BEN2_VIDEO_SUFFIXES = {

View File

@@ -3,7 +3,7 @@ import math
import bpy
from ...properties import ai_ready, ai_setup_label, ai_status
from ..properties import ai_ready, ai_setup_label, ai_status
from .image_data import (
image_empty_bounds,
is_image_empty,

View File

@@ -0,0 +1,47 @@
from pathlib import Path
import bpy
NODE_ASSET_NAME = "O_AnyImage.blend"
def node_asset_path():
return Path(__file__).resolve().parents[1] / "assets" / NODE_ASSET_NAME
def load_node_group(group_name):
existing = bpy.data.node_groups.get(group_name)
if existing is not None:
return existing
asset_path = node_asset_path()
if not asset_path.is_file():
raise RuntimeError(f"Missing node asset: {asset_path}")
if bpy.app.version < (5, 0, 0):
directory = str(asset_path) + "/NodeTree/"
result = bpy.ops.wm.append(
filepath=directory + group_name,
directory=directory,
filename=group_name,
link=False,
do_reuse_local_id=True,
)
if "FINISHED" not in result:
raise RuntimeError("Unable to append the node asset")
node_group = bpy.data.node_groups.get(group_name)
if node_group is None:
raise RuntimeError("Appended node asset is unavailable")
node_group.use_fake_user = True
return node_group
with bpy.data.libraries.load(str(asset_path), link=True) as (source, target):
if group_name not in source.node_groups:
raise RuntimeError(f"Node asset is missing {group_name}")
target.node_groups = [group_name]
linked_group = target.node_groups[0]
node_group = bpy.data.pack_linked_ids_hierarchy(root_id=linked_group)
if linked_group != node_group and linked_group.users == 0:
bpy.data.node_groups.remove(linked_group)
node_group.use_fake_user = True
return node_group

View File

@@ -1,5 +1,3 @@
from pathlib import Path
import bpy
from .image_data import (
@@ -8,50 +6,7 @@ from .image_data import (
image_pixels,
is_animated_image,
)
NODE_ASSET_NAME = "O_AnyImage.blend"
def node_asset_path():
return Path(__file__).resolve().parents[2] / "assets" / NODE_ASSET_NAME
def load_asset_node_group(group_name):
existing = bpy.data.node_groups.get(group_name)
if existing is not None:
return existing
asset_path = node_asset_path()
if not asset_path.is_file():
raise RuntimeError(f"Missing node asset: {asset_path}")
if bpy.app.version < (5, 0, 0):
directory = str(asset_path) + "/NodeTree/"
result = bpy.ops.wm.append(
filepath=directory + group_name,
directory=directory,
filename=group_name,
link=False,
do_reuse_local_id=True,
)
if "FINISHED" not in result:
raise RuntimeError("Unable to append the node asset")
node_group = bpy.data.node_groups.get(group_name)
if node_group is None:
raise RuntimeError("Appended node asset is unavailable")
node_group.use_fake_user = True
return node_group
with bpy.data.libraries.load(str(asset_path), link=True) as (source, target):
if group_name not in source.node_groups:
raise RuntimeError(f"Node asset is missing {group_name}")
target.node_groups = [group_name]
linked_group = target.node_groups[0]
node_group = bpy.data.pack_linked_ids_hierarchy(root_id=linked_group)
if linked_group != node_group and linked_group.users == 0:
bpy.data.node_groups.remove(linked_group)
node_group.use_fake_user = True
return node_group
from .node import load_node_group
def modifier_input_identifier(node_group, name):
@@ -139,7 +94,7 @@ def material_node_group(depth_plane=False):
if depth_plane
else IMAGE_MATERIAL_NODE_GROUP_NAME
)
return load_asset_node_group(group_name)
return load_node_group(group_name)
def create_image_material(
@@ -224,7 +179,7 @@ def create_image_material(
links.new(normal_texture.outputs["Color"], material_inputs.inputs["Normal"])
if shadeless:
shader = nodes.new("ShaderNodeGroup")
shader.node_tree = load_asset_node_group(SHADELESS_NODE_GROUP_NAME)
shader.node_tree = load_node_group(SHADELESS_NODE_GROUP_NAME)
shader.name = SHADELESS_NODE_GROUP_NAME
transparent = nodes.new("ShaderNodeBsdfTransparent")
mix_shader = nodes.new("ShaderNodeMixShader")

View File

@@ -1,6 +1,6 @@
import bpy
from .operators.common.image_data import is_image_empty
from .common.image_data import is_image_empty
from .operators.upscale import upscale_output_size

View File

@@ -8,7 +8,7 @@ from ..properties import (
model_download_size,
model_record,
)
from .common.ai_job import require_environment
from ..common.ai_job import require_environment
def format_download_size(size):

View File

@@ -1 +0,0 @@
"""Shared Blender-side image data, interaction, mesh result, and AI Job helpers."""

View File

@@ -8,7 +8,7 @@ from ...server.media.resolution import (
MAX_MAX_AI_INPUT_SIZE,
MIN_MAX_AI_INPUT_SIZE,
)
from ..common.ai_job import (
from ...common.ai_job import (
invoke_ai_setup_if_needed,
moge2_parameters,
production_device,
@@ -16,7 +16,7 @@ from ..common.ai_job import (
require_input_path,
require_model,
)
from ..common.image_data import (
from ...common.image_data import (
cleanup_image_input,
image_user_settings,
is_image_empty,
@@ -24,7 +24,7 @@ from ..common.image_data import (
source_start_frame,
video_total_frames,
)
from ..common.mesh_result import create_image_material
from ...common.object import create_image_material
from .object import (
create_color_image,
create_depth_plane_from_result,

View File

@@ -2,8 +2,8 @@
import bpy
from ..common.mesh_result import modifier_input_identifier, set_modifier_input
from .nodes import mesh_plane_node_group
from ...common.object import modifier_input_identifier, set_modifier_input
from ...common.node import load_node_group
MESH_PLANE_NODE_GROUP_NAME = "O Mesh Plane"
@@ -41,7 +41,7 @@ def add_mesh_plane_processing(
subdivisions=0,
thickness=0.0,
):
node_group = mesh_plane_node_group()
node_group = load_node_group(MESH_PLANE_NODE_GROUP_NAME)
modifier = plane_object.modifiers.new(
name=MESH_PLANE_NODE_GROUP_NAME,
type="NODES",

View File

@@ -1,23 +0,0 @@
"""Asset-backed Geometry Nodes for Plane conversion."""
from ..common.mesh_result import load_asset_node_group
MESH_PLANE_NODE_GROUP_NAME = "O Mesh Plane"
DEPTH_PLANE_NODE_GROUP_NAME = "O Image Depth Plane"
def mesh_plane_node_group():
return load_asset_node_group(MESH_PLANE_NODE_GROUP_NAME)
def depth_plane_node_group():
return load_asset_node_group(DEPTH_PLANE_NODE_GROUP_NAME)
def plane_node_group(shape):
if shape == "PLANE":
return mesh_plane_node_group()
if shape == "DEPTH_PLANE":
return depth_plane_node_group()
raise ValueError(f"Unsupported Plane shape: {shape}")

View File

@@ -2,26 +2,29 @@
import bpy
from ..common.image_data import (
from ...common.image_data import (
color_data_name,
image_empty_bounds,
image_pixels,
load_color_result_image,
require_image_empty,
)
from ..common.depth_data import (
from ...common.depth_data import (
depth_uniform_scale,
load_depth_metadata,
load_depth_result_image,
)
from ..common.mesh_result import (
from ...common.object import (
create_image_material,
finalize_object_result,
modifier_input_identifier,
set_modifier_input,
)
from ...common.node import load_node_group
from .mesh_plane import create_mesh_plane_mesh, create_mesh_plane_object
from .nodes import depth_plane_node_group
DEPTH_PLANE_NODE_GROUP_NAME = "O Image Depth Plane"
def create_color_image(source_object):
@@ -71,7 +74,7 @@ def create_depth_plane_object(
plane.matrix_world = source_object.matrix_world.copy()
plane["anyimage_mesh_shape"] = "DEPTH_PLANE"
node_group = depth_plane_node_group()
node_group = load_node_group(DEPTH_PLANE_NODE_GROUP_NAME)
modifier = plane.modifiers.new(name=node_group.name, type="NODES")
modifier.node_group = node_group
modifier.show_group_selector = False

View File

@@ -5,7 +5,7 @@ from dataclasses import dataclass
from ...preferences import addon_preferences
from ...server.media.resolution import limited_image_size
from ..common.image_data import image_empty_bounds
from ...common.image_data import image_empty_bounds
@dataclass(frozen=True)

View File

@@ -4,9 +4,10 @@ from ...preferences import (
MAX_CUTOUT_MESH_RESOLUTION,
MIN_CUTOUT_MESH_RESOLUTION,
)
from ..common.image_data import image_rgba
from ..common.image_region import rasterize_region_path
from ...common.image_data import image_rgba
from ...common.image_region import rasterize_region_path
from .geometry import CUTOUT_ALPHA_THRESHOLD
from .shape import CUTOUT_SHAPE_LABELS
def validate_cutout_region(
@@ -76,7 +77,7 @@ def open_shape_pie(
_pie_button(
pie,
"SURFACE",
"Surface",
CUTOUT_SHAPE_LABELS["SURFACE"],
"MESH_GRID",
source_name,
region_path_json,
@@ -87,7 +88,7 @@ def open_shape_pie(
_pie_button(
pie,
"BALLOON",
"Balloon",
CUTOUT_SHAPE_LABELS["BALLOON"],
"MESH_UVSPHERE",
source_name,
region_path_json,
@@ -98,7 +99,7 @@ def open_shape_pie(
_pie_button(
pie,
"DEPTH_BALLOON",
"Depth Balloon",
CUTOUT_SHAPE_LABELS["DEPTH_BALLOON"],
"META_BALL",
source_name,
region_path_json,
@@ -109,7 +110,7 @@ def open_shape_pie(
_pie_button(
pie,
"DEPTH_SURFACE",
"Depth Surface",
CUTOUT_SHAPE_LABELS["DEPTH_SURFACE"],
"SURFACE_NCURVE",
source_name,
region_path_json,

View File

@@ -22,7 +22,7 @@ from ...server.media.resolution import (
MAX_MAX_AI_INPUT_SIZE,
MIN_MAX_AI_INPUT_SIZE,
)
from ..common.ai_job import (
from ...common.ai_job import (
invoke_ai_setup_if_needed,
moge2_parameters,
production_device,
@@ -30,7 +30,7 @@ from ..common.ai_job import (
require_input_path,
require_model,
)
from ..common.image_data import (
from ...common.image_data import (
cleanup_image_input,
image_rgba,
load_normal_result_image,
@@ -39,8 +39,8 @@ from ..common.image_data import (
source_start_frame,
video_total_frames,
)
from ..common.depth_data import load_depth_metadata, load_depth_result_image
from ..common.image_interaction import (
from ...common.depth_data import load_depth_metadata, load_depth_result_image
from ...common.image_interaction import (
ImageRegionGesture,
apply_region_settings,
draw_region_settings,
@@ -49,7 +49,7 @@ from ..common.image_interaction import (
resolve_image_edit_click,
serialize_matrix,
)
from ..common.image_region import (
from ...common.image_region import (
ImageEditWarning,
RegionMask,
RegionPath,

View File

@@ -4,19 +4,19 @@ from enum import StrEnum
import bpy
from ..common.image_data import (
from ...common.image_data import (
color_data_name,
image_rgba,
)
from ..common.depth_data import camera_depth_values, depth_uniform_scale
from ..common.mesh_result import (
from ...common.depth_data import camera_depth_values, depth_uniform_scale
from ...common.object import (
IMAGE_MATERIAL_NODE_GROUP_NAME,
create_image_material,
finalize_object_result,
load_asset_node_group,
modifier_input_identifier,
set_modifier_input,
)
from ...common.node import load_node_group
from .geometry import (
DEFAULT_DEPTH_BASELINE,
DEFAULT_DEPTH_DIRECTION,
@@ -123,7 +123,7 @@ def _cropped_color_image(source_object, region_mask, source_rgba):
def load_cutout_node_group():
return load_asset_node_group(CUTOUT_NODE_GROUP_NAME)
return load_node_group(CUTOUT_NODE_GROUP_NAME)
def _connect_balloon_normal_scale(material):

View File

@@ -4,7 +4,7 @@ import bpy
from ..runtime import JobOperatorBase
from ..properties import BEN2_MODEL_KEY, REALESRGAN_GENERAL_WDN_MODEL_KEY
from .common.ai_job import (
from ..common.ai_job import (
require_background_input_path,
da3_parameters,
require_input_path,

View File

@@ -1,6 +1,6 @@
import bpy
from ..common.image_interaction import draw_region_settings
from ...common.image_interaction import draw_region_settings
from .edit_mask import EditImageMask

View File

@@ -4,12 +4,12 @@ import bpy
from ...properties import BEN2_MODEL_KEY, refine_region_property
from ...runtime import JobOperatorBase
from ..common.ai_job import (
from ...common.ai_job import (
production_device,
require_environment,
require_model,
)
from ..common.image_data import (
from ...common.image_data import (
cleanup_image_input,
create_pixel_result_image,
image_pixels,
@@ -19,13 +19,13 @@ from ..common.image_data import (
require_image_empty,
video_total_frames,
)
from ..common.image_interaction import (
from ...common.image_interaction import (
ImageRegionGesture,
gesture_property,
image_edit_poll,
report_image_edit_exception,
)
from ..common.image_region import (
from ...common.image_region import (
ImageEditWarning,
RegionPath,
crop_pixels_to_alpha,

View File

@@ -1,6 +1,6 @@
import bpy
from ..common.image_interaction import draw_region_settings
from ...common.image_interaction import draw_region_settings
from .edit_mask import EditImageMask

View File

@@ -3,14 +3,14 @@ import math
import bpy
from ..common.image_interaction import (
from ...common.image_interaction import (
deserialize_matrix,
drawing_in_region,
screen_path_to_image_pixels,
serialize_matrix,
)
from ..common.image_region import crop_pixels_to_alpha
from ..common.image_data import (
from ...common.image_region import crop_pixels_to_alpha
from ...common.image_data import (
create_pixel_result_image,
image_base_name,
image_pixels,
@@ -19,7 +19,7 @@ from ..common.image_data import (
require_image_empty,
video_total_frames,
)
from ..common.image_interaction import (
from ...common.image_interaction import (
active_view3d_tool_id,
draw_dashed_line,
draw_polygon_fill,

View File

@@ -2,7 +2,7 @@ import bpy
from ..properties import BEN2_MODEL_KEY
from ..runtime import JobOperatorBase
from .common.ai_job import (
from ..common.ai_job import (
require_background_input_path,
invoke_ai_setup_if_needed,
production_device,
@@ -10,7 +10,7 @@ from .common.ai_job import (
require_model,
)
from .common.image_data import (
from ..common.image_data import (
cleanup_image_input,
default_extract_frames,
is_image_empty,
@@ -70,7 +70,7 @@ class RemoveImageBackground(bpy.types.Operator):
def apply_background_response(result, source_object_name):
from .common.image_data import (
from ..common.image_data import (
load_color_result_image,
replace_empty_image,
require_image_empty,

View File

@@ -8,14 +8,14 @@ from ..server.media.resolution import (
MIN_MAX_AI_INPUT_SIZE,
)
from ..runtime import JobOperatorBase
from .common.ai_job import (
from ..common.ai_job import (
require_input_path,
invoke_ai_setup_if_needed,
production_device,
require_environment,
require_model,
)
from .common.image_data import (
from ..common.image_data import (
cleanup_image_input,
default_extract_frames,
is_image_empty,

View File

@@ -1,173 +0,0 @@
"""Prepare AnyImage ONNX models and sync them to OmooLab R2."""
import argparse
import hashlib
import shutil
import subprocess
import sys
from dataclasses import dataclass
from pathlib import Path
from urllib.request import Request, urlopen
PROJECT_ROOT = Path(__file__).resolve().parent
MODELS_DIR = PROJECT_ROOT / "models"
SOURCE_ROOT = PROJECT_ROOT / "src" / "anyimage"
sys.path.insert(0, str(SOURCE_ROOT))
from server.model_catalog import DOWNLOADABLE_MODELS
R2_ENDPOINT = "https://models.omoolab.xyz"
CHUNK_SIZE = 1024 * 1024
@dataclass(frozen=True)
class ModelFile:
folder: str
size: int
sha256: str
sources: tuple[str, ...]
filename: str = "model.onnx"
@property
def destination(self):
return MODELS_DIR / self.folder / self.filename
def hf_url(repository, filename):
return f"https://huggingface.co/{repository}/resolve/main/{filename}"
def r2_url(folder, filename="model.onnx"):
return f"{R2_ENDPOINT}/{folder}/{filename}"
def catalog_model_files():
files = []
for model in DOWNLOADABLE_MODELS.values():
for filename, size, sha256 in model.r2_files:
relative_path = Path(filename)
folder = str(Path(model.r2_directory) / relative_path.parent)
sources = []
if model.huggingface_repository:
sources.append(
hf_url(model.huggingface_repository, filename)
)
sources.append(r2_url(model.r2_directory, filename))
files.append(
ModelFile(
folder,
size,
sha256,
tuple(sources),
relative_path.name,
)
)
return tuple(files)
MODEL_FILES = catalog_model_files()
def file_sha256(path):
digest = hashlib.sha256()
with Path(path).open("rb") as stream:
while chunk := stream.read(CHUNK_SIZE):
digest.update(chunk)
return digest.hexdigest()
def verify_existing(model):
path = model.destination
if not path.is_file():
return False
if path.stat().st_size != model.size:
raise RuntimeError(f"Existing model has an unexpected size: {path}")
if file_sha256(path) != model.sha256:
raise RuntimeError(f"Existing model has an unexpected SHA-256: {path}")
print(f"Skip existing: {path.relative_to(PROJECT_ROOT)}")
return True
def download_once(model, url):
destination = model.destination
destination.parent.mkdir(parents=True, exist_ok=True)
temporary = destination.with_suffix(".onnx.part")
temporary.unlink(missing_ok=True)
request = Request(url, headers={"User-Agent": "AnyImage model sync"})
print(f"Download: {url}")
downloaded = 0
digest = hashlib.sha256()
try:
with urlopen(request, timeout=60) as response, temporary.open("wb") as output:
while chunk := response.read(CHUNK_SIZE):
output.write(chunk)
digest.update(chunk)
downloaded += len(chunk)
print(
f"\r {downloaded / 1024**2:.1f} / "
f"{model.size / 1024**2:.1f} MiB",
end="",
flush=True,
)
print()
if downloaded != model.size:
raise RuntimeError(f"size mismatch: {downloaded} != {model.size}")
if digest.hexdigest() != model.sha256:
raise RuntimeError("SHA-256 mismatch")
temporary.replace(destination)
except Exception:
temporary.unlink(missing_ok=True)
raise
def prepare_model(model):
if verify_existing(model):
return
failures = []
for url in model.sources:
try:
download_once(model, url)
return
except Exception as error:
failures.append(f"{url}: {error}")
print(f"Source failed: {error}", file=sys.stderr)
raise RuntimeError("All sources failed:\n" + "\n".join(failures))
def sync_models():
executable = shutil.which("rclone")
if executable is None:
raise RuntimeError("rclone is not available on PATH")
subprocess.run(
[
executable,
"sync",
"models",
"omoolab-r2:ai-models",
"--progress",
],
cwd=PROJECT_ROOT,
check=True,
)
def parse_args():
parser = argparse.ArgumentParser()
parser.add_argument(
"--download-only",
action="store_true",
help="Prepare and verify models without running rclone",
)
return parser.parse_args()
def main():
args = parse_args()
for model in MODEL_FILES:
prepare_model(model)
if not args.download_only:
sync_models()
if __name__ == "__main__":
main()

File diff suppressed because it is too large Load Diff

View File

@@ -10,20 +10,20 @@ import numpy as np
SOURCE_ROOT = Path(__file__).parents[1] / "src"
sys.path.insert(0, str(SOURCE_ROOT))
from anyimage.operators.common.image_region import (
from anyimage.common.image_region import (
RegionMask,
RegionPath,
rasterize_region_path,
)
from anyimage.operators.common.depth_data import (
from anyimage.common.depth_data import (
depth_uniform_scale,
load_depth_metadata,
)
from anyimage.operators.cutout_tool import geometry as cutout_depth
from anyimage.operators.cutout_tool import geometry as cutout_profile
from anyimage.operators.cutout_tool import geometry as mesh
from anyimage.operators.cutout_tool import object as cutout_nodes
from anyimage.operators.cutout_tool import shape as shapes
from anyimage.operators.cutout_tool import geometry as cutout_geometry
from anyimage.operators.cutout_tool import object as cutout_object
from anyimage.operators.cutout_tool import shape as cutout_shape
from anyimage.operators.cutout_tool.geometry import (
RegionContours,
extract_region_contours,
@@ -33,9 +33,9 @@ from anyimage.server.models import moge2 as moge2_model
def test_cutout_mesh_detail_uses_the_cropped_analysis_scale():
assert mesh.limit_cutout_mesh_resolution(256, (4096, 1024), 2048) == 128
assert mesh.limit_cutout_mesh_resolution(256, (1024, 512), 2048) == 256
assert mesh.limit_cutout_mesh_resolution(8, (8192, 4096), 2048) == 4
assert cutout_geometry.limit_cutout_mesh_resolution(256, (4096, 1024), 2048) == 128
assert cutout_geometry.limit_cutout_mesh_resolution(256, (1024, 512), 2048) == 256
assert cutout_geometry.limit_cutout_mesh_resolution(8, (8192, 4096), 2048) == 4
def test_depth_metadata_loader_uses_one_schema_for_plane_and_cutout(tmp_path):
@@ -61,11 +61,11 @@ def test_depth_metadata_loader_uses_one_schema_for_plane_and_cutout(tmp_path):
def test_cutout_node_groups_are_loaded_from_the_asset():
loaded = object()
with patch.object(
cutout_nodes, "load_asset_node_group", return_value=loaded
cutout_object, "load_node_group", return_value=loaded
) as load:
for shape in shapes.CUTOUT_NODE_GROUP_NAMES:
assert cutout_nodes.load_cutout_node_group() is loaded
load.assert_called_with(shapes.CUTOUT_NODE_GROUP_NAME)
for shape in cutout_shape.CUTOUT_NODE_GROUP_NAMES:
assert cutout_object.load_cutout_node_group() is loaded
load.assert_called_with(cutout_shape.CUTOUT_NODE_GROUP_NAME)
assert load.call_count == 4
@@ -80,7 +80,7 @@ def test_base_plane_fit_is_stable_for_a_slender_diagonal_domain():
)
values = 0.4 * along + np.sign(across) * 0.05
plane = cutout_depth._stable_plane(coordinates, values)
plane = cutout_geometry._stable_plane(coordinates, values)
selected_plane = coordinates @ plane[:2] + plane[2]
corners = np.asarray(((-1.0, -1.0), (-1.0, 1.0), (1.0, -1.0), (1.0, 1.0)))
corner_plane = corners @ plane[:2] + plane[2]
@@ -101,7 +101,7 @@ def test_depth_balloon_plane_calibration_matches_cutout_axes():
uv.append((column * 0.5, 1.0 - row * 0.5))
camera_depth[row, column] = 2.0 + 0.2 * local_y + 0.1 * local_z
reference, direction = cutout_depth.depth_balloon_projection_parameters(
reference, direction = cutout_geometry.depth_balloon_projection_parameters(
vertices,
uv,
camera_depth,
@@ -131,7 +131,7 @@ def test_depth_balloon_reference_uses_the_shared_upper_depth_percentile():
vertices.append((0.0, float(x), float(size - 1 - y)))
uv.append((x / (size - 1), 1.0 - y / (size - 1)))
reference, direction = cutout_depth.depth_balloon_projection_parameters(
reference, direction = cutout_geometry.depth_balloon_projection_parameters(
vertices,
uv,
camera_depth,
@@ -185,7 +185,7 @@ def test_depth_surface_projection_uses_one_scale_for_all_camera_axes():
)
assert uniform_scale == 2.0
reference_depth = cutout_depth.metric_depth_reference(
reference_depth = cutout_geometry.metric_depth_reference(
points[..., 2],
np.ones((51, 101), dtype=bool),
uniform_scale,
@@ -271,7 +271,7 @@ def test_cutout_crop_bounds_map_to_the_source_plane():
empty_image_offset=(-0.5, -0.5),
)
bounds = mesh.crop_plane_bounds(
bounds = cutout_geometry.crop_plane_bounds(
source,
(50, 25, 150, 75),
(200, 100),
@@ -285,7 +285,7 @@ def test_depth_surface_cutout_uses_half_alpha_boundary():
alpha[2:10, 2:14] = 0.49
alpha[4:8, 5:11] = 0.51
region_contours = mesh.extract_cutout_contours(
region_contours = cutout_geometry.extract_cutout_contours(
RegionMask(alpha, (0, 0, 16, 12)),
64,
threshold=0.5,
@@ -328,9 +328,9 @@ def test_blender_cutout_contour_resolution_tracks_mesh_detail():
image_size=(1, 1),
)
region_mask = RegionMask(np.ones((2, 2), dtype=np.float32), (0, 0, 2, 2))
with patch.object(mesh, "extract_region_contours", return_value=expected) as trace:
assert mesh.extract_cutout_contours(region_mask, 32) == expected
assert mesh.extract_cutout_contours(region_mask, 256) == expected
with patch.object(cutout_geometry, "extract_region_contours", return_value=expected) as trace:
assert cutout_geometry.extract_cutout_contours(region_mask, 32) == expected
assert cutout_geometry.extract_cutout_contours(region_mask, 256) == expected
assert trace.call_args_list[0].kwargs["max_resolution"] == 512
assert trace.call_args_list[1].kwargs["max_resolution"] == 1024
@@ -339,28 +339,28 @@ def test_blender_cutout_contour_resolution_tracks_mesh_detail():
def test_cutout_point_spacing_uses_the_complete_source_plane():
source = SimpleNamespace(matrix_world=object())
with (
patch.object(mesh, "image_empty_bounds", return_value=(-2.0, 2.0, -1.0, 1.0)),
patch.object(cutout_geometry, "image_empty_bounds", return_value=(-2.0, 2.0, -1.0, 1.0)),
patch.object(
mesh,
cutout_geometry,
"world_plane_metric",
return_value=(((1.0, 0.0), (0.0, 1.0)), 1.0),
),
):
spacing = mesh.cutout_point_spacing(source, 200)
spacing = cutout_geometry.cutout_point_spacing(source, 200)
assert spacing == 0.02
def test_cutout_cleanup_value_scales_the_ui_without_changing_node_area():
assert np.isclose(
mesh.cutout_cleanup_value(
cutout_geometry.cutout_cleanup_value(
0.25,
((1.0, 0.0), (0.0, 1.0)),
),
6.25,
)
assert np.isclose(
mesh.cutout_cleanup_value(
cutout_geometry.cutout_cleanup_value(
0.25,
((4.0, 0.0), (0.0, 9.0)),
),
@@ -369,8 +369,8 @@ def test_cutout_cleanup_value_scales_the_ui_without_changing_node_area():
def test_cutout_depth_calibration_uses_five_percent_transparency_threshold():
assert not cutout_depth.use_adaptive_depth_calibration(0.0499)
assert cutout_depth.use_adaptive_depth_calibration(0.05)
assert not cutout_geometry.use_adaptive_depth_calibration(0.0499)
assert cutout_geometry.use_adaptive_depth_calibration(0.05)
def test_poisson_balloon_profile_is_zero_on_boundary_and_positive_inside():
@@ -429,11 +429,11 @@ def test_teddy_balloon_profile_uses_chordal_axis_radius():
nodes = np.asarray(((1, 1), (3, 1)), dtype=float)
with patch.object(
cutout_profile,
cutout_geometry,
"chordal_axis",
return_value=(nodes, [(0, 1)], np.asarray((1.0, 1.0))),
):
profile = cutout_profile.teddy_balloon_heights(
profile = cutout_geometry.teddy_balloon_heights(
points,
faces,
points[:4],
@@ -448,7 +448,7 @@ def test_teddy_balloon_profile_uses_chordal_axis_radius():
def test_cutout_domain_adapts_boundary_spacing_for_a_small_valid_region():
regions = ((((2046, 2046), (2050, 2046), (2050, 2050), (2046, 2050)),),)
domain = mesh.cutout_domain(
domain = cutout_geometry.cutout_domain(
RegionContours(regions, (4096, 4096)),
(-1.0, 1.0, -1.0, 1.0),
0.5,
@@ -462,7 +462,7 @@ def test_cutout_domain_adapts_boundary_spacing_for_a_small_valid_region():
def test_cutout_domain_builds_a_full_image_at_minimum_detail():
regions = ((((0, 0), (4096, 0), (4096, 4096), (0, 4096)),),)
domain = mesh.cutout_domain(
domain = cutout_geometry.cutout_domain(
RegionContours(regions, (4096, 4096)),
(-1.0, 1.0, -1.0, 1.0),
0.5,
@@ -476,7 +476,7 @@ def test_cutout_domain_builds_a_full_image_at_minimum_detail():
def test_cutout_domain_centers_vertices_on_crop_bounds():
regions = ((((0, 0), (100, 0), (100, 50), (0, 50)),),)
vertices, _faces, uv, _boundary, _planar, _profile = mesh.cutout_domain(
vertices, _faces, uv, _boundary, _planar, _profile = cutout_geometry.cutout_domain(
RegionContours(regions, (100, 50)),
(2.0, 6.0, -3.0, 1.0),
0.5,
@@ -497,7 +497,7 @@ def test_cutout_domain_keeps_overlapping_regions_disconnected():
),
(((0, 20), (40, 20), (40, 50), (0, 50)),),
)
domain = mesh.cutout_domain(
domain = cutout_geometry.cutout_domain(
RegionContours(regions, (120, 80)),
(0.0, 1.0, 0.0, 1.0),
0.025,
@@ -529,7 +529,7 @@ def test_region_calibration_uses_the_final_region_mask():
(0, 0, 2, 2),
)
valid, transparent_fraction = mesh.region_calibration(region_mask, (2, 2))
valid, transparent_fraction = cutout_geometry.region_calibration(region_mask, (2, 2))
assert np.array_equal(valid, ((True, True), (True, False)))
assert transparent_fraction == 0.25

View File

@@ -7,10 +7,10 @@ import bpy
SOURCE_ROOT = Path(__file__).parents[1] / "src"
sys.path.insert(0, str(SOURCE_ROOT))
from anyimage.operators.common.image_interaction import ImageRegionGesture
from anyimage.operators.common.image_region import RegionPath
from anyimage.common.image_interaction import ImageRegionGesture
from anyimage.common.image_region import RegionPath
from anyimage.operators.cutout_tool import interaction
from anyimage.operators.cutout_tool import main as cutout
from anyimage.operators.cutout_tool import main as cutout_main
class Pie:
@@ -106,11 +106,11 @@ def test_cutout_pie_passes_image_lasso_options_to_every_shape():
def test_cutout_normal_space_is_derived_from_shape():
assert cutout.normal_mode_for_shape("DEPTH_SURFACE", False) == "NONE"
assert cutout.normal_mode_for_shape("SURFACE", True) == "TANGENT"
assert cutout.normal_mode_for_shape("BALLOON", True) == "TANGENT"
assert cutout.normal_mode_for_shape("DEPTH_BALLOON", True) == "TANGENT"
assert cutout.normal_mode_for_shape("DEPTH_SURFACE", True) == "OBJECT"
assert cutout_main.normal_mode_for_shape("DEPTH_SURFACE", False) == "NONE"
assert cutout_main.normal_mode_for_shape("SURFACE", True) == "TANGENT"
assert cutout_main.normal_mode_for_shape("BALLOON", True) == "TANGENT"
assert cutout_main.normal_mode_for_shape("DEPTH_BALLOON", True) == "TANGENT"
assert cutout_main.normal_mode_for_shape("DEPTH_SURFACE", True) == "OBJECT"
def test_cutout_mesh_detail_tracks_the_source_long_edge_and_clamps():
@@ -130,30 +130,30 @@ def test_cutout_reuses_image_lasso_gesture_and_double_click_selects_all(monkeypa
context = cutout_context(window_manager)
monkeypatch.setattr(
cutout, "resolve_image_edit_click", lambda *_args: (source, True)
cutout_main, "resolve_image_edit_click", lambda *_args: (source, True)
)
monkeypatch.setattr(cutout, "video_total_frames", lambda _image: 1)
monkeypatch.setattr(cutout, "apply_region_settings", lambda *_args: None)
monkeypatch.setattr(cutout, "validate_cutout_region", lambda *_args: None)
monkeypatch.setattr(cutout_main, "video_total_frames", lambda _image: 1)
monkeypatch.setattr(cutout_main, "apply_region_settings", lambda *_args: None)
monkeypatch.setattr(cutout_main, "validate_cutout_region", lambda *_args: None)
operator = SimpleNamespace(
full_image=True,
refine_region=False,
report=lambda *_args: None,
)
operator.execute = MethodType(cutout.SelectCutoutRegion.execute, operator)
result = cutout.SelectCutoutRegion.invoke(
operator.execute = MethodType(cutout_main.SelectCutoutRegion.execute, operator)
result = cutout_main.SelectCutoutRegion.invoke(
operator, context, SimpleNamespace(value="DOUBLE_CLICK")
)
assert issubclass(cutout.SelectCutoutRegion, ImageRegionGesture)
assert issubclass(cutout_main.SelectCutoutRegion, ImageRegionGesture)
assert result == {"FINISHED"}
region_path = RegionPath.from_json(window_manager.pie.calls[0][2].region_path_json)
assert region_path.points == ((0, 0), (320, 0), (320, 180), (0, 180))
assert window_manager.pie.calls[0][2].mesh_resolution == 10
assert not hasattr(cutout, "pending_selection")
assert not hasattr(cutout_main, "pending_selection")
def test_cutout_tool_prioritizes_double_click_before_lasso_press():
assert cutout.CutoutTool.bl_keymap[0][1]["value"] == "DOUBLE_CLICK"
assert cutout.CutoutTool.bl_keymap[1][1]["value"] == "PRESS"
assert cutout_main.CutoutTool.bl_keymap[0][1]["value"] == "DOUBLE_CLICK"
assert cutout_main.CutoutTool.bl_keymap[1][1]["value"] == "PRESS"

View File

@@ -10,20 +10,20 @@ from mathutils import Vector
SOURCE_ROOT = Path(__file__).parents[1] / "src"
sys.path.insert(0, str(SOURCE_ROOT))
from anyimage.operators.common import image_data as common_image
from anyimage.operators.common import depth_data as common_depth
from anyimage.operators.common import mesh_result as material
from anyimage.operators.common.image_region import (
from anyimage.common import image_data
from anyimage.common import depth_data
from anyimage.common import node as node_asset
from anyimage.common import object as object_data
from anyimage.common.image_region import (
RegionMask,
RegionPath,
rasterize_region_path,
)
from anyimage.operators.convert_to_plane import nodes as plane_nodes
from anyimage.operators.convert_to_plane import object as plane_object
from anyimage.operators.cutout_tool import geometry as cutout_geometry
from anyimage.operators.cutout_tool import main as cutout_main
from anyimage.operators.cutout_tool import object as cutout_object
from anyimage.operators.cutout_tool import shape as shapes
from anyimage.operators.cutout_tool import shape as cutout_shape
from anyimage.server.geometry.depth_texture import write_vector_depth_texture
@@ -117,7 +117,7 @@ def test_depth_result_image_loads_server_vector_exr_and_keeps_data_name(tmp_path
write_vector_depth_texture(points, valid, path)
metadata = _depth_metadata(np.eye(3), (2, 2), 1.0)
image = common_depth.load_depth_result_image(path, source, metadata)
image = depth_data.load_depth_result_image(path, source, metadata)
packed = np.empty(16, dtype=np.float32)
image.pixels.foreach_get(packed)
@@ -131,14 +131,14 @@ def test_depth_result_image_loads_server_vector_exr_and_keeps_data_name(tmp_path
def test_cutout_modifier_wraps_four_menu_controlled_shape_groups():
bpy.ops.wm.read_factory_settings(use_empty=True)
plane = plane_nodes.plane_node_group("PLANE")
depth_plane = plane_nodes.plane_node_group("DEPTH_PLANE")
plane = node_asset.load_node_group("O Mesh Plane")
depth_plane = node_asset.load_node_group("O Image Depth Plane")
cutout = cutout_object.load_cutout_node_group()
assert plane.name == "O Mesh Plane"
assert cutout.name == shapes.CUTOUT_NODE_GROUP_NAME
assert cutout.name == cutout_shape.CUTOUT_NODE_GROUP_NAME
assert all(
cutout_object.load_cutout_node_group() == cutout
for shape in shapes.CUTOUT_SHAPES
for shape in cutout_shape.CUTOUT_SHAPES
)
assert _inputs(plane) == ["Geometry", "Subdivide", "Thickness", "Material"]
assert _inputs(depth_plane) == [
@@ -191,7 +191,7 @@ def test_cutout_modifier_wraps_four_menu_controlled_shape_groups():
)
labels = {
shape: shape.replace("_", " ").title()
for shape in shapes.CUTOUT_NODE_GROUP_NAMES
for shape in cutout_shape.CUTOUT_NODE_GROUP_NAMES
}
assert [item.name for item in menu_switch.enum_items] == list(labels.values())
assert any(
@@ -203,7 +203,7 @@ def test_cutout_modifier_wraps_four_menu_controlled_shape_groups():
child_groups = {
shape: bpy.data.node_groups[name]
for shape, name in shapes.CUTOUT_NODE_GROUP_NAMES.items()
for shape, name in cutout_shape.CUTOUT_NODE_GROUP_NAMES.items()
}
assert cutout.is_modifier
assert all(not group.is_modifier for group in child_groups.values())
@@ -256,7 +256,7 @@ def test_cutout_modifier_wraps_four_menu_controlled_shape_groups():
)
dependencies.setdefault(link.from_socket.name, set()).add(shape)
assert dependencies == {
"Thickness": set(shapes.CUTOUT_SHAPES),
"Thickness": set(cutout_shape.CUTOUT_SHAPES),
"Depth Scale": {"DEPTH_BALLOON", "DEPTH_SURFACE"},
"Double Sided": {"DEPTH_BALLOON"},
"Base Plane Depth": {"DEPTH_BALLOON", "DEPTH_SURFACE"},
@@ -370,8 +370,8 @@ def test_cutout_modifier_wraps_four_menu_controlled_shape_groups():
def test_image_layer_switches_between_tangent_and_object_space_normal():
bpy.ops.wm.read_factory_settings(use_empty=True)
assert material.IMAGE_MATERIAL_NODE_GROUP_NAME == "O Image Layer"
group = material.material_node_group()
assert object_data.IMAGE_MATERIAL_NODE_GROUP_NAME == "O Image Layer"
group = object_data.material_node_group()
normal = next(
item
for item in group.interface.items_tree
@@ -416,7 +416,7 @@ def test_image_layer_switches_between_tangent_and_object_space_normal():
color = bpy.data.images.new("Color", width=2, height=2)
normal_image = bpy.data.images.new("Normal", width=2, height=2)
object_material = material.create_image_material(
object_material = object_data.create_image_material(
SimpleNamespace(data=SimpleNamespace(name="Object Normal")),
color,
1.0,
@@ -426,7 +426,7 @@ def test_image_layer_switches_between_tangent_and_object_space_normal():
layer = _group_node(group, object_material.node_tree)
assert layer.inputs["Object Space"].default_value is True
tangent_material = material.create_image_material(
tangent_material = object_data.create_image_material(
SimpleNamespace(data=SimpleNamespace(name="Tangent Normal")),
color,
1.0,
@@ -434,7 +434,7 @@ def test_image_layer_switches_between_tangent_and_object_space_normal():
)
layer = _group_node(group, tangent_material.node_tree)
assert layer.inputs["Object Space"].default_value is False
reused = material.material_node_group()
reused = object_data.material_node_group()
assert {
node.space for node in reused.nodes if node.bl_idname == "ShaderNodeNormalMap"
} == {"TANGENT", "OBJECT"}
@@ -461,7 +461,7 @@ def test_build_plane_keeps_one_quad_and_generates_topology_in_geometry_nodes():
assert len(result.data.vertices) == 4
assert len(result.data.polygons) == 1
assert all(polygon.use_smooth for polygon in result.data.polygons)
assert shapes.BALLOON_ATTRIBUTE_NAME not in result.data.attributes
assert cutout_shape.BALLOON_ATTRIBUTE_NAME not in result.data.attributes
assert result.modifiers[0].node_group.name == "O Mesh Plane"
evaluated = result.evaluated_get(bpy.context.evaluated_depsgraph_get())
evaluated_mesh = evaluated.to_mesh()
@@ -488,8 +488,8 @@ def test_balloon_build_starts_from_the_unified_single_sheet():
assert all(abs(vertex.co.x) < 1e-8 for vertex in result.data.vertices)
assert len(result.data.vertices) > 4
assert len(result.data.polygons) > 1
assert shapes.BALLOON_ATTRIBUTE_NAME in result.data.attributes
profile_attribute = result.data.attributes[shapes.BALLOON_ATTRIBUTE_NAME]
assert cutout_shape.BALLOON_ATTRIBUTE_NAME in result.data.attributes
profile_attribute = result.data.attributes[cutout_shape.BALLOON_ATTRIBUTE_NAME]
assert profile_attribute.data_type == "FLOAT"
assert profile_attribute.domain == "POINT"
profile = np.empty(len(result.data.vertices), dtype=np.float32)
@@ -502,8 +502,8 @@ def test_balloon_build_starts_from_the_unified_single_sheet():
if node.bl_idname == "ShaderNodeAttribute"
)
assert normal_scale_attribute.attribute_type == "GEOMETRY"
assert normal_scale_attribute.attribute_name == shapes.BALLOON_ATTRIBUTE_NAME
image_layer = _group_node(material.material_node_group(), material_tree)
assert normal_scale_attribute.attribute_name == cutout_shape.BALLOON_ATTRIBUTE_NAME
image_layer = _group_node(object_data.material_node_group(), material_tree)
normal_scale_link = next(
link
for link in material_tree.links
@@ -512,7 +512,7 @@ def test_balloon_build_starts_from_the_unified_single_sheet():
)
assert normal_scale_link.from_node == normal_scale_attribute
assert normal_scale_link.from_socket == normal_scale_attribute.outputs["Fac"]
assert result.modifiers[0].node_group.name == shapes.CUTOUT_NODE_GROUP_NAME
assert result.modifiers[0].node_group.name == cutout_shape.CUTOUT_NODE_GROUP_NAME
inward = result.matrix_world.to_3x3() @ Vector((1.0, 0.0, 0.0))
assert np.allclose(tuple(inward), (0.0, 0.0, -1.0), atol=1e-6)
@@ -536,7 +536,7 @@ def test_teddy_preference_writes_the_shared_balloon_attribute():
8,
)
attribute = result.data.attributes[shapes.BALLOON_ATTRIBUTE_NAME]
attribute = result.data.attributes[cutout_shape.BALLOON_ATTRIBUTE_NAME]
profile = np.empty(len(result.data.vertices), dtype=np.float32)
attribute.data.foreach_get("value", profile)
assert profile.min() == 0.0 and profile.max() > 0.0
@@ -572,7 +572,7 @@ def test_cutout_material_uses_flat_tangent_normal():
color = bpy.data.images.new("Color", width=2, height=2)
normal = bpy.data.images.new("Normal", width=1, height=1)
result = material.create_image_material(
result = object_data.create_image_material(
source,
color,
0.0,
@@ -580,7 +580,7 @@ def test_cutout_material_uses_flat_tangent_normal():
scene=bpy.context.scene,
)
layer = _group_node(material.material_node_group(), result.node_tree)
layer = _group_node(object_data.material_node_group(), result.node_tree)
assert layer.inputs["Object Space"].default_value is False
@@ -616,39 +616,39 @@ def test_depth_surface_protected_smooth_evaluates():
)
assert all(abs(vertex.co.x) < 1e-8 for vertex in result.data.vertices)
assert shapes.BALLOON_ATTRIBUTE_NAME in result.data.attributes
assert cutout_shape.BALLOON_ATTRIBUTE_NAME in result.data.attributes
modifier = result.modifiers[0]
assert modifier.node_group.name == shapes.CUTOUT_NODE_GROUP_NAME
assert modifier.node_group.name == cutout_shape.CUTOUT_NODE_GROUP_NAME
assert (
modifier[material.modifier_input_identifier(modifier.node_group, "Shape")]
== shapes.CUTOUT_SHAPE_INDICES["DEPTH_SURFACE"]
modifier[object_data.modifier_input_identifier(modifier.node_group, "Shape")]
== cutout_shape.CUTOUT_SHAPE_INDICES["DEPTH_SURFACE"]
)
expected_cleanup = cutout_geometry.cutout_cleanup_value(
cutout_geometry.cutout_point_spacing(source, 8),
cutout_geometry.world_plane_metric(source.matrix_world)[0],
)
assert np.isclose(
modifier[material.modifier_input_identifier(modifier.node_group, "Cleanup")],
modifier[object_data.modifier_input_identifier(modifier.node_group, "Cleanup")],
expected_cleanup,
)
assert (
modifier[material.modifier_input_identifier(modifier.node_group, "Smooth")]
modifier[object_data.modifier_input_identifier(modifier.node_group, "Smooth")]
== 1.0
)
assert (
modifier[
material.modifier_input_identifier(modifier.node_group, "Smooth Iterations")
object_data.modifier_input_identifier(modifier.node_group, "Smooth Iterations")
]
== 0
)
reference_id = material.modifier_input_identifier(
reference_id = object_data.modifier_input_identifier(
modifier.node_group,
"Base Plane Depth",
)
assert modifier[reference_id] == 8.0
material.set_modifier_input(
object_data.set_modifier_input(
modifier,
material.modifier_input_identifier(modifier.node_group, "Smooth"),
object_data.modifier_input_identifier(modifier.node_group, "Smooth"),
0.0,
)
evaluated = result.evaluated_get(bpy.context.evaluated_depsgraph_get())
@@ -660,9 +660,9 @@ def test_depth_surface_protected_smooth_evaluates():
finally:
evaluated.to_mesh_clear()
material.set_modifier_input(
object_data.set_modifier_input(
modifier,
material.modifier_input_identifier(modifier.node_group, "Thickness"),
object_data.modifier_input_identifier(modifier.node_group, "Thickness"),
1.0,
)
result.update_tag(refresh={"DATA"})
@@ -677,14 +677,14 @@ def test_depth_surface_protected_smooth_evaluates():
finally:
evaluated.to_mesh_clear()
material.set_modifier_input(
object_data.set_modifier_input(
modifier,
material.modifier_input_identifier(modifier.node_group, "Smooth Iterations"),
object_data.modifier_input_identifier(modifier.node_group, "Smooth Iterations"),
4,
)
material.set_modifier_input(
object_data.set_modifier_input(
modifier,
material.modifier_input_identifier(modifier.node_group, "Smooth"),
object_data.modifier_input_identifier(modifier.node_group, "Smooth"),
1.0,
)
result.update_tag(refresh={"DATA"})
@@ -716,24 +716,24 @@ def _add_cutout_modifier(obj, shape):
group = cutout_object.load_cutout_node_group()
modifier = obj.modifiers.new(group.name, "NODES")
modifier.node_group = group
material.set_modifier_input(
object_data.set_modifier_input(
modifier,
material.modifier_input_identifier(group, "Shape"),
shapes.CUTOUT_SHAPE_INDICES[shape],
object_data.modifier_input_identifier(group, "Shape"),
cutout_shape.CUTOUT_SHAPE_INDICES[shape],
)
material.set_modifier_input(
object_data.set_modifier_input(
modifier,
material.modifier_input_identifier(group, "Cleanup"),
object_data.modifier_input_identifier(group, "Cleanup"),
0.0,
)
material.set_modifier_input(
object_data.set_modifier_input(
modifier,
material.modifier_input_identifier(group, "Smooth"),
object_data.modifier_input_identifier(group, "Smooth"),
0.0,
)
material.set_modifier_input(
object_data.set_modifier_input(
modifier,
material.modifier_input_identifier(group, "Smooth Iterations"),
object_data.modifier_input_identifier(group, "Smooth Iterations"),
1,
)
return modifier
@@ -753,14 +753,14 @@ def test_cutout_thickness_keeps_one_connected_shell():
bpy.context.collection.objects.link(obj)
modifier = _add_cutout_modifier(obj, "SURFACE")
material.set_modifier_input(
object_data.set_modifier_input(
modifier,
material.modifier_input_identifier(modifier.node_group, "Cleanup"),
object_data.modifier_input_identifier(modifier.node_group, "Cleanup"),
0.0,
)
material.set_modifier_input(
object_data.set_modifier_input(
modifier,
material.modifier_input_identifier(modifier.node_group, "Thickness"),
object_data.modifier_input_identifier(modifier.node_group, "Thickness"),
1.0,
)
@@ -789,9 +789,9 @@ def test_cutout_thickness_keeps_one_connected_shell():
finally:
evaluated.to_mesh_clear()
material.set_modifier_input(
object_data.set_modifier_input(
modifier,
material.modifier_input_identifier(modifier.node_group, "Thickness"),
object_data.modifier_input_identifier(modifier.node_group, "Thickness"),
2.4,
)
obj.update_tag(refresh={"DATA"})
@@ -828,21 +828,21 @@ def test_balloon_uses_injected_profile_at_default_height():
data.from_pydata(vertices, (), faces)
cutout_object._assign_float_point_attribute(
data,
shapes.BALLOON_ATTRIBUTE_NAME,
cutout_shape.BALLOON_ATTRIBUTE_NAME,
(0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0),
)
obj = bpy.data.objects.new("Balloon Profile", data)
bpy.context.collection.objects.link(obj)
modifier = _add_cutout_modifier(obj, "BALLOON")
material.set_modifier_input(
object_data.set_modifier_input(
modifier,
material.modifier_input_identifier(modifier.node_group, "Cleanup"),
object_data.modifier_input_identifier(modifier.node_group, "Cleanup"),
0.0,
)
material.set_modifier_input(
object_data.set_modifier_input(
modifier,
material.modifier_input_identifier(modifier.node_group, "Thickness"),
object_data.modifier_input_identifier(modifier.node_group, "Thickness"),
1.0,
)
@@ -861,9 +861,9 @@ def test_balloon_uses_injected_profile_at_default_height():
baseline_face_count = len(result.polygons)
finally:
evaluated.to_mesh_clear()
material.set_modifier_input(
object_data.set_modifier_input(
modifier,
material.modifier_input_identifier(modifier.node_group, "Thickness"),
object_data.modifier_input_identifier(modifier.node_group, "Thickness"),
2.0,
)
obj.update_tag(refresh={"DATA"})
@@ -877,9 +877,9 @@ def test_balloon_uses_injected_profile_at_default_height():
assert np.isclose(x.max(), 2.0, atol=1e-6)
finally:
evaluated.to_mesh_clear()
material.set_modifier_input(
object_data.set_modifier_input(
modifier,
material.modifier_input_identifier(modifier.node_group, "Thickness"),
object_data.modifier_input_identifier(modifier.node_group, "Thickness"),
0.0,
)
obj.update_tag(refresh={"DATA"})
@@ -908,13 +908,13 @@ def test_cutout_cleanup_uses_absolute_area_with_many_small_islands():
bpy.context.collection.objects.link(obj)
modifier = _add_cutout_modifier(obj, "SURFACE")
cleanup_id = material.modifier_input_identifier(modifier.node_group, "Cleanup")
cleanup_id = object_data.modifier_input_identifier(modifier.node_group, "Cleanup")
material.set_modifier_input(modifier, cleanup_id, 0.0)
object_data.set_modifier_input(modifier, cleanup_id, 0.0)
bpy.context.view_layer.update()
assert len(_evaluated_positions(obj)) == 45
material.set_modifier_input(modifier, cleanup_id, 0.1)
object_data.set_modifier_input(modifier, cleanup_id, 0.1)
obj.update_tag(refresh={"DATA"})
bpy.context.view_layer.update()
assert len(_evaluated_positions(obj)) == 9
@@ -964,7 +964,7 @@ def test_depth_balloon_uses_metric_plane_inputs_without_controller():
)
assert all(abs(vertex.co.x) < 1e-8 for vertex in result.data.vertices)
assert shapes.BALLOON_ATTRIBUTE_NAME in result.data.attributes
assert cutout_shape.BALLOON_ATTRIBUTE_NAME in result.data.attributes
assert "o_balloon_depth" not in result.data.attributes
assert "o_depth_offset" not in result.data.attributes
assert not result.children
@@ -973,7 +973,7 @@ def test_depth_balloon_uses_metric_plane_inputs_without_controller():
result.name,
}
modifier = result.modifiers[0]
depth_image_id = material.modifier_input_identifier(
depth_image_id = object_data.modifier_input_identifier(
modifier.node_group,
"Depth Image",
)
@@ -993,39 +993,39 @@ def test_depth_balloon_uses_metric_plane_inputs_without_controller():
atol=1e-5,
)
depth_reference_id = material.modifier_input_identifier(
depth_reference_id = object_data.modifier_input_identifier(
modifier.node_group,
"Base Plane Depth",
)
assert not np.isclose(modifier[depth_reference_id], 8.0)
depth_direction_id = material.modifier_input_identifier(
depth_direction_id = object_data.modifier_input_identifier(
modifier.node_group,
"Depth Direction",
)
direction = np.asarray(modifier[depth_direction_id], dtype=np.float64)
assert not np.allclose(direction, (0.0, 0.0, 1.0))
uniform_scale_id = material.modifier_input_identifier(
uniform_scale_id = object_data.modifier_input_identifier(
modifier.node_group,
"Uniform Scale",
)
assert modifier[uniform_scale_id] > 0.0
thickness_id = material.modifier_input_identifier(
thickness_id = object_data.modifier_input_identifier(
modifier.node_group,
"Thickness",
)
smooth_id = material.modifier_input_identifier(
smooth_id = object_data.modifier_input_identifier(
modifier.node_group,
"Smooth",
)
assert modifier[smooth_id] == 1.0
material.set_modifier_input(modifier, smooth_id, 0.0)
object_data.set_modifier_input(modifier, smooth_id, 0.0)
result.update_tag(refresh={"DATA"})
bpy.context.view_layer.update()
positions = _evaluated_positions(result)
center_distance = np.linalg.norm(positions[:, 1:] - expected_center, axis=1)
nearest_center = center_distance <= center_distance.min() + 1e-6
depth_at_thickness_one = positions[nearest_center, 0].min()
material.set_modifier_input(modifier, thickness_id, 2.0)
object_data.set_modifier_input(modifier, thickness_id, 2.0)
result.update_tag(refresh={"DATA"})
bpy.context.view_layer.update()
positions = _evaluated_positions(result)
@@ -1037,12 +1037,12 @@ def test_depth_balloon_uses_metric_plane_inputs_without_controller():
depth_at_thickness_two,
atol=1e-5,
)
material.set_modifier_input(modifier, thickness_id, 1.0)
object_data.set_modifier_input(modifier, thickness_id, 1.0)
result.update_tag(refresh={"DATA"})
bpy.context.view_layer.update()
before_positions = _evaluated_positions(result)
material.set_modifier_input(
object_data.set_modifier_input(
modifier,
depth_reference_id,
modifier[depth_reference_id] + 0.2,
@@ -1056,31 +1056,31 @@ def test_depth_balloon_uses_metric_plane_inputs_without_controller():
atol=1e-5,
)
depth_scale_id = material.modifier_input_identifier(
depth_scale_id = object_data.modifier_input_identifier(
modifier.node_group,
"Depth Scale",
)
material.set_modifier_input(modifier, depth_scale_id, 0.0)
object_data.set_modifier_input(modifier, depth_scale_id, 0.0)
result.update_tag(refresh={"DATA"})
bpy.context.view_layer.update()
zero_scale_before = _evaluated_positions(result)
material.set_modifier_input(
object_data.set_modifier_input(
modifier,
depth_reference_id,
modifier[depth_reference_id] + 0.15,
)
material.set_modifier_input(modifier, depth_direction_id, (0.2, 0.1, 1.0))
object_data.set_modifier_input(modifier, depth_direction_id, (0.2, 0.1, 1.0))
result.update_tag(refresh={"DATA"})
bpy.context.view_layer.update()
zero_scale_after = _evaluated_positions(result)
assert np.allclose(zero_scale_after, zero_scale_before, atol=1e-5)
material.set_modifier_input(modifier, depth_scale_id, 1.0)
object_data.set_modifier_input(modifier, depth_scale_id, 1.0)
double_sided_id = material.modifier_input_identifier(
double_sided_id = object_data.modifier_input_identifier(
modifier.node_group,
"Double Sided",
)
material.set_modifier_input(modifier, double_sided_id, True)
object_data.set_modifier_input(modifier, double_sided_id, True)
result.update_tag(refresh={"DATA"})
bpy.context.view_layer.update()
mirrored = _evaluated_positions(result)
@@ -1106,7 +1106,7 @@ def test_loaded_normal_image_uses_normal_texture_name(tmp_path):
Image.new("RGB", (1, 1), (128, 128, 255)).save(path)
source = SimpleNamespace(data=SimpleNamespace(name="Poster.png"))
image = common_image.load_normal_result_image(path, source)
image = image_data.load_normal_result_image(path, source)
assert image.name == "Poster_normal.png"
assert image.colorspace_settings.name == "Non-Color"
@@ -1166,14 +1166,14 @@ def test_local_cutout_builds_from_blender_image_pixels_without_a_job():
points=((-8, 8), (72, 8), (72, 40), (-8, 40)),
fill_rule="NONZERO",
)
source_bounds = common_image.image_empty_bounds(source)
source_bounds = image_data.image_empty_bounds(source)
with patch.object(
common_image,
image_data,
"image_rgba",
wraps=common_image.image_rgba,
wraps=image_data.image_rgba,
) as read_rgba:
rgba = common_image.image_rgba(image)
rgba = image_data.image_rgba(image)
region_mask = rasterize_region_path(
tuple(image.size),
region_path,
@@ -1197,26 +1197,26 @@ def test_local_cutout_builds_from_blender_image_pixels_without_a_job():
assert result.type == "MESH"
assert result["anyimage_mesh_shape"] == "SURFACE"
modifier = result.modifiers[0]
assert modifier.node_group.name == shapes.CUTOUT_NODE_GROUP_NAME
assert modifier.node_group.name == cutout_shape.CUTOUT_NODE_GROUP_NAME
assert (
modifier[material.modifier_input_identifier(modifier.node_group, "Shape")]
== shapes.CUTOUT_SHAPE_INDICES["SURFACE"]
modifier[object_data.modifier_input_identifier(modifier.node_group, "Shape")]
== cutout_shape.CUTOUT_SHAPE_INDICES["SURFACE"]
)
expected_cleanup = cutout_geometry.cutout_cleanup_value(
cutout_geometry.cutout_point_spacing(source, 16),
cutout_geometry.world_plane_metric(source.matrix_world)[0],
)
assert np.isclose(
modifier[material.modifier_input_identifier(modifier.node_group, "Cleanup")],
modifier[object_data.modifier_input_identifier(modifier.node_group, "Cleanup")],
expected_cleanup,
)
assert (
modifier[material.modifier_input_identifier(modifier.node_group, "Smooth")]
modifier[object_data.modifier_input_identifier(modifier.node_group, "Smooth")]
== 1.0
)
assert (
modifier[
material.modifier_input_identifier(modifier.node_group, "Smooth Iterations")
object_data.modifier_input_identifier(modifier.node_group, "Smooth Iterations")
]
== 0
)
@@ -1224,7 +1224,7 @@ def test_local_cutout_builds_from_blender_image_pixels_without_a_job():
assert len(result.data.polygons) > 1
assert all(polygon.use_smooth for polygon in result.data.polygons)
assert all(abs(vertex.co.x) < 1e-8 for vertex in result.data.vertices)
assert shapes.BALLOON_ATTRIBUTE_NAME in result.data.attributes
assert cutout_shape.BALLOON_ATTRIBUTE_NAME in result.data.attributes
source_inverse = source.matrix_world.inverted()
source_x = [
(source_inverse @ (result.matrix_world @ vertex.co)).x
@@ -1253,11 +1253,11 @@ def test_local_cutout_builds_from_blender_image_pixels_without_a_job():
def test_balloon_normal_scale_uses_existing_image_layer_input():
bpy.ops.wm.read_factory_settings(use_empty=True)
assert shapes.BALLOON_CUTOUT_SHAPES == frozenset(
assert cutout_shape.BALLOON_CUTOUT_SHAPES == frozenset(
{"BALLOON", "DEPTH_BALLOON"}
)
group = bpy.data.node_groups.new(
material.IMAGE_MATERIAL_NODE_GROUP_NAME,
object_data.IMAGE_MATERIAL_NODE_GROUP_NAME,
"ShaderNodeTree",
)
group.interface.new_socket(
@@ -1280,7 +1280,7 @@ def test_balloon_normal_scale_uses_existing_image_layer_input():
if node != image_layer
)
assert attribute.bl_idname == "ShaderNodeAttribute"
assert attribute.attribute_name == shapes.BALLOON_ATTRIBUTE_NAME
assert attribute.attribute_name == cutout_shape.BALLOON_ATTRIBUTE_NAME
link = next(iter(image_material.node_tree.links))
assert link.from_socket == attribute.outputs["Fac"]
assert link.to_socket == image_layer.inputs["Normal Scale"]

42
tests/test_docs.py Normal file
View File

@@ -0,0 +1,42 @@
import importlib.util
from pathlib import Path
SCRIPT_PATH = Path(__file__).resolve().parents[1] / "tools" / "docs.py"
SPEC = importlib.util.spec_from_file_location("docs_script", SCRIPT_PATH)
docs = importlib.util.module_from_spec(SPEC)
SPEC.loader.exec_module(docs)
def test_documentation_command_deploys_project_version(monkeypatch):
monkeypatch.setattr(docs, "project_version", lambda: "1.2.3")
assert docs.documentation_command() == [
"mike",
"deploy",
"--update-aliases",
"1.2.3",
"latest",
]
def test_documentation_command_previews_current_source():
assert docs.documentation_command(dev=True) == ["mkdocs", "serve"]
def test_main_runs_from_project_root(monkeypatch):
calls = []
monkeypatch.setattr(
docs.subprocess,
"run",
lambda *args, **kwargs: calls.append((args, kwargs)),
)
docs.main(["-dev"])
assert calls == [
(
(["mkdocs", "serve"],),
{"cwd": docs.PROJECT_ROOT, "check": True},
)
]

View File

@@ -7,7 +7,7 @@ PROJECT_ROOT = Path(__file__).resolve().parents[1]
MANIFEST_PATH = PROJECT_ROOT / "src" / "anyimage" / "blender_manifest.toml"
class ManifestTest(unittest.TestCase):
class ExtensionManifestTest(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.manifest = tomllib.loads(MANIFEST_PATH.read_text(encoding="utf-8"))

View File

@@ -12,14 +12,14 @@ PROJECT_ROOT = Path(__file__).resolve().parents[1]
def load_pack():
specification = importlib.util.spec_from_file_location(
"anyimage_pack",
PROJECT_ROOT / "pack.py",
PROJECT_ROOT / "tools" / "pack.py",
)
module = importlib.util.module_from_spec(specification)
specification.loader.exec_module(module)
return module
class PackTest(unittest.TestCase):
class ExtensionPackagingTest(unittest.TestCase):
def test_builds_platform_specific_extension_zips(self):
pack = load_pack()
with tempfile.TemporaryDirectory() as directory:
@@ -58,12 +58,13 @@ class PackTest(unittest.TestCase):
self.assertIn("blender_manifest.toml", names)
self.assertNotIn("assets/O_AnyImage.blend1", names)
for path in (
"operators/common/__init__.py",
"operators/common/ai_job.py",
"operators/common/image_data.py",
"operators/common/image_interaction.py",
"operators/common/image_region.py",
"operators/common/mesh_result.py",
"common/__init__.py",
"common/ai_job.py",
"common/image_data.py",
"common/image_interaction.py",
"common/image_region.py",
"common/node.py",
"common/object.py",
"operators/cutout_tool/__init__.py",
"operators/cutout_tool/main.py",
"operators/cutout_tool/interaction.py",
@@ -74,7 +75,6 @@ class PackTest(unittest.TestCase):
"operators/convert_to_plane/object.py",
"operators/convert_to_plane/conversion.py",
"operators/convert_to_plane/mesh_plane.py",
"operators/convert_to_plane/nodes.py",
"operators/image_tool/__init__.py",
"operators/image_tool/edit_mask.py",
"operators/image_tool/box.py",
@@ -87,6 +87,7 @@ class PackTest(unittest.TestCase):
"operators/remove_background.py",
):
self.assertIn(path, names)
self.assertNotIn("operators/convert_to_plane/nodes.py", names)
self.assertNotIn("operators/cutout/__init__.py", names)
self.assertNotIn("operators/image_mesh/__init__.py", names)
self.assertNotIn("operators/mesh/__init__.py", names)
@@ -97,7 +98,7 @@ class PackTest(unittest.TestCase):
names,
)
self.assertIn("server/media/input.py", names)
self.assertIn("operators/common/depth_data.py", names)
self.assertIn("common/depth_data.py", names)
self.assertIn("server/geometry/projection.py", names)
self.assertIn("server/models/geometry.py", names)
self.assertIn("server/jobs/remove_background.py", names)
@@ -111,7 +112,7 @@ class PackTest(unittest.TestCase):
self.assertNotIn(
"server/geometry/generate_geometry_and_normal.py", names
)
self.assertNotIn("operators/common/image_selection.py", names)
self.assertNotIn("common/image_selection.py", names)
self.assertNotIn("server/selection/cutout_images.py", names)
self.assertNotIn("server/selection/polygon_mask.py", names)
self.assertNotIn("server/jobs/prepare_geometry_data.py", names)

View File

@@ -24,22 +24,28 @@ def load_image_modules():
module_paths = (
(
"anyimage.operators.common.image_data",
"anyimage.common.image_data",
PROJECT_ROOT
/ "src"
/ "anyimage"
/ "operators"
/ "common"
/ "image_data.py",
),
(
"anyimage.operators.common.mesh_result",
"anyimage.common.node",
PROJECT_ROOT
/ "src"
/ "anyimage"
/ "operators"
/ "common"
/ "mesh_result.py",
/ "node.py",
),
(
"anyimage.common.object",
PROJECT_ROOT
/ "src"
/ "anyimage"
/ "common"
/ "object.py",
),
)
packages = {
@@ -47,7 +53,7 @@ def load_image_modules():
for name in (
"anyimage",
"anyimage.operators",
"anyimage.operators.common",
"anyimage.common",
"anyimage.operators.convert_to_plane",
)
}
@@ -74,12 +80,12 @@ def load_image_modules():
return namespace
class ImageTest(unittest.TestCase):
class ImageDataTest(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.image_depth = load_image_modules()
def test_material_asset_path_survives_package_split(self):
def test_node_asset_path_survives_package_split(self):
self.assertEqual(
self.image_depth.node_asset_path(),
PROJECT_ROOT / "src" / "anyimage" / "assets" / "O_AnyImage.blend",

View File

@@ -16,7 +16,7 @@ def load_module(name, relative_path):
return module
class ModelTest(unittest.TestCase):
class ModelCatalogTest(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.models = load_module(

View File

@@ -35,7 +35,7 @@ class FakeTqdm:
return True
class DownloadProgressTest(unittest.TestCase):
class ModelDownloadTest(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.download = load_download_module()

View File

@@ -60,7 +60,7 @@ class FakeSession:
return [FakeInput(name) for name in self.outputs]
class MogeOnnxTest(unittest.TestCase):
class Moge2OnnxTest(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.moge = load_module()

View File

@@ -0,0 +1,72 @@
import importlib.util
from pathlib import Path
from unittest.mock import patch
import pytest
PROJECT_ROOT = Path(__file__).resolve().parents[1]
SCRIPT_PATH = PROJECT_ROOT / "tools" / "build_node.py"
def load_launcher():
specification = importlib.util.spec_from_file_location(
"node_asset_launcher",
SCRIPT_PATH,
)
module = importlib.util.module_from_spec(specification)
specification.loader.exec_module(module)
return module
def test_blender_path_prefers_explicit_path(tmp_path):
launcher = load_launcher()
executable = tmp_path / "blender"
executable.touch()
with patch.dict("os.environ", {"BLENDER_BIN": "ignored"}):
assert launcher.blender_path(executable) == executable
def test_blender_path_reads_environment(tmp_path):
launcher = load_launcher()
executable = tmp_path / "blender"
executable.touch()
with patch.dict("os.environ", {"BLENDER_BIN": str(executable)}):
assert launcher.blender_path() == executable
def test_blender_path_reads_project_dotenv(tmp_path):
launcher = load_launcher()
executable = tmp_path / "Blender 4.5" / "blender.exe"
executable.parent.mkdir()
executable.touch()
env_path = tmp_path / ".env"
env_path.write_text(
f'BLENDER_BIN="{executable}"\n',
encoding="utf-8",
)
with patch.dict("os.environ", {}, clear=True), patch.object(
launcher,
"PROJECT_ROOT",
tmp_path,
):
assert launcher.blender_path() == executable
def test_blender_path_reports_missing_configuration(tmp_path):
launcher = load_launcher()
with patch.dict("os.environ", {}, clear=True), patch.object(
launcher.shutil,
"which",
return_value=None,
), patch.object(
launcher,
"PROJECT_ROOT",
tmp_path,
):
with pytest.raises(RuntimeError, match="Blender not found"):
launcher.blender_path()

View File

@@ -6,7 +6,7 @@ PROJECT_ROOT = Path(__file__).resolve().parents[1]
SOURCE_ROOT = PROJECT_ROOT / "src" / "anyimage"
class NodeAssetTest(unittest.TestCase):
class NodeAssetsTest(unittest.TestCase):
def test_production_code_does_not_create_node_groups(self):
offenders = []
for path in SOURCE_ROOT.rglob("*.py"):
@@ -15,16 +15,26 @@ class NodeAssetTest(unittest.TestCase):
self.assertEqual(offenders, [])
def test_runtime_node_group_entries_load_assets(self):
material_source = (
SOURCE_ROOT / "operators" / "common" / "mesh_result.py"
object_source = (
SOURCE_ROOT / "common" / "object.py"
).read_text(encoding="utf-8")
plane_source = (
SOURCE_ROOT / "operators" / "convert_to_plane" / "nodes.py"
mesh_plane_source = (
SOURCE_ROOT / "operators" / "convert_to_plane" / "mesh_plane.py"
).read_text(encoding="utf-8")
self.assertIn("return load_asset_node_group(group_name)", material_source)
depth_plane_source = (
SOURCE_ROOT / "operators" / "convert_to_plane" / "object.py"
).read_text(encoding="utf-8")
self.assertIn("return load_node_group(group_name)", object_source)
self.assertIn(
"return load_asset_node_group(DEPTH_PLANE_NODE_GROUP_NAME)",
plane_source,
"load_node_group(MESH_PLANE_NODE_GROUP_NAME)",
mesh_plane_source,
)
self.assertIn(
"load_node_group(DEPTH_PLANE_NODE_GROUP_NAME)",
depth_plane_source,
)
self.assertFalse(
(SOURCE_ROOT / "operators" / "convert_to_plane" / "nodes.py").exists()
)

View File

@@ -111,7 +111,7 @@ class FakeJobContext:
return None
class WorkerTest(unittest.TestCase):
class ServerJobsTest(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.media = load_server_module("media.input")

View File

@@ -38,7 +38,7 @@ class FakeCuda:
return None
class ServerTest(unittest.TestCase):
class ServerRuntimeTest(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.server_module = load_server_module()

99
tests/test_sync_models.py Normal file
View File

@@ -0,0 +1,99 @@
import importlib.util
import sys
import unittest
from pathlib import Path
from unittest.mock import patch
PROJECT_ROOT = Path(__file__).resolve().parents[1]
def load_sync_models():
path = PROJECT_ROOT / "tools" / "sync_models.py"
spec = importlib.util.spec_from_file_location("sync_models", path)
module = importlib.util.module_from_spec(spec)
sys.modules[spec.name] = module
spec.loader.exec_module(module)
return module
class SyncModelsTests(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.sync = load_sync_models()
def test_r2_is_only_an_upload_destination(self):
for model in self.sync.MODEL_FILES:
self.assertNotIn("models.omoolab.xyz", model.source_url)
if model.build:
self.assertFalse(model.source_url)
def test_local_exports_use_official_pytorch_weights(self):
builds = self.sync.MODEL_BUILDS
self.assertEqual(
set(builds),
{"REALESRGAN_GENERAL_WDN_X4V3", "SAFMN_REAL_X4"},
)
for build in builds.values():
self.assertTrue(build.source_url.startswith("https://github.com/"))
self.assertTrue(build.source_filename.endswith(".pth"))
self.assertEqual(len(build.source_sha256), 64)
def test_prepare_model_builds_missing_local_export(self):
model = next(model for model in self.sync.MODEL_FILES if model.build)
with (
patch.object(self.sync, "verify_file", return_value=False),
patch.object(self.sync, "build_model") as build,
patch.object(self.sync, "download_file") as download,
):
self.sync.prepare_model(model)
build.assert_called_once_with(model)
download.assert_not_called()
def test_prepare_model_downloads_upstream_onnx(self):
model = next(
model
for model in self.sync.MODEL_FILES
if model.source_url and not model.build
)
with (
patch.object(self.sync, "verify_file", return_value=False),
patch.object(self.sync, "download_file") as download,
):
self.sync.prepare_model(model)
download.assert_called_once_with(
model.source_url,
model.destination,
model.size,
model.sha256,
)
def test_upload_copies_declared_files_without_remote_deletion(self):
model = self.sync.ModelFile(
key="TEST",
folder="folder",
filename="model.onnx",
size=1,
sha256="hash",
)
with (
patch.object(self.sync.shutil, "which", return_value="rclone"),
patch.object(self.sync.subprocess, "run") as run,
):
self.sync.upload_models((model,))
command = run.call_args.args[0]
self.assertEqual(command[1], "copyto")
self.assertNotIn("sync", command)
self.assertEqual(command[-2], model.remote)
def test_verify_file_checks_size_and_sha256(self):
path = PROJECT_ROOT / "tools" / "sync_models.py"
size = path.stat().st_size
digest = self.sync.file_sha256(path)
self.assertTrue(self.sync.verify_file(path, size, digest))
with self.assertRaisesRegex(RuntimeError, "unexpected size"):
self.sync.verify_file(path, size - 1, digest)
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,26 @@
import tomllib
import unittest
from pathlib import Path
PROJECT_ROOT = Path(__file__).resolve().parents[1]
class ToolCommandsTests(unittest.TestCase):
def test_repository_tools_have_uv_commands(self):
project = tomllib.loads(
(PROJECT_ROOT / "pyproject.toml").read_text(encoding="utf-8")
)["project"]
self.assertEqual(
project["scripts"],
{
"pack": "tools.pack:main",
"build_node": "tools.build_node:main",
"sync_models": "tools.sync_models:main",
"docs": "tools.docs:main",
},
)
if __name__ == "__main__":
unittest.main()

1
tools/__init__.py Normal file
View File

@@ -0,0 +1 @@
"""AnyImage repository maintenance commands."""

91
tools/build_node.py Normal file
View File

@@ -0,0 +1,91 @@
import argparse
import os
import shutil
import subprocess
import sys
from pathlib import Path
PROJECT_ROOT = Path(__file__).resolve().parents[1]
BLENDER_SCRIPTS = (
"build.py",
"validate.py",
"validate_mesh_plane.py",
"validate_cutout.py",
)
def project_env_value(name, path=None):
env_path = path or PROJECT_ROOT / ".env"
if not env_path.is_file():
return None
for raw_line in env_path.read_text(encoding="utf-8").splitlines():
line = raw_line.strip()
if not line or line.startswith("#"):
continue
if line.startswith("export "):
line = line.removeprefix("export ").lstrip()
key, separator, value = line.partition("=")
if separator and key.strip() == name:
value = value.strip()
if len(value) >= 2 and value[0] == value[-1] and value[0] in "\"'":
value = value[1:-1]
return value or None
return None
def blender_path(explicit=None):
candidate = (
explicit
or os.environ.get("BLENDER_BIN")
or project_env_value("BLENDER_BIN")
or shutil.which("blender")
)
if not candidate:
raise RuntimeError(
"Blender not found: pass --blender, set BLENDER_BIN, add it to .env, "
"or add blender to PATH"
)
path = Path(candidate).expanduser()
if not path.is_file():
raise RuntimeError(f"Blender executable does not exist: {path}")
return path
def run(command):
print("+", subprocess.list2cmdline([str(part) for part in command]), flush=True)
subprocess.run(command, cwd=PROJECT_ROOT, check=True)
def main():
parser = argparse.ArgumentParser(
description="Build and validate O_AnyImage.blend.",
)
parser.add_argument(
"--blender",
help="Blender executable; overrides BLENDER_BIN, .env, and PATH.",
)
parser.add_argument(
"--skip-tests",
action="store_true",
help="Skip the Python test suite.",
)
options = parser.parse_args()
executable = blender_path(options.blender)
for script_name in BLENDER_SCRIPTS:
run(
(
executable,
"--factory-startup",
"--background",
"--python",
PROJECT_ROOT / "tools" / "node_assets" / script_name,
)
)
if not options.skip_tests:
run((sys.executable, "-m", "pytest"))
if __name__ == "__main__":
main()

48
tools/docs.py Normal file
View File

@@ -0,0 +1,48 @@
import argparse
import subprocess
import tomllib
from pathlib import Path
PROJECT_ROOT = Path(__file__).resolve().parents[1]
def project_version():
"""Return the documentation version declared by the project."""
project = tomllib.loads(
(PROJECT_ROOT / "pyproject.toml").read_text(encoding="utf-8")
)["project"]
return project["version"]
def documentation_command(dev=False):
"""Return the Mike or MkDocs command for the requested mode."""
if dev:
return ["mkdocs", "serve"]
return [
"mike",
"deploy",
"--update-aliases",
project_version(),
"latest",
]
def main(argv=None):
parser = argparse.ArgumentParser(description="Build or preview documentation.")
parser.add_argument(
"-dev",
"--dev",
action="store_true",
help="preview the current documentation with live reload",
)
args = parser.parse_args(argv)
subprocess.run(
documentation_command(args.dev),
cwd=PROJECT_ROOT,
check=True,
)
if __name__ == "__main__":
main()

204
tools/model_exports.py Normal file
View File

@@ -0,0 +1,204 @@
"""Export official PyTorch super-resolution models to ONNX."""
import numpy as np
OPSET_VERSION = 17
def _require_build_dependencies():
try:
import onnx
import onnxruntime
import torch
except ImportError as error:
raise RuntimeError(
"Model build dependencies are missing. Run with: "
"uv sync"
) from error
return onnx, onnxruntime, torch
def _load_weights(torch, checkpoint_path, key):
checkpoint = torch.load(checkpoint_path, map_location="cpu", weights_only=True)
return checkpoint[key]
def _validate_export(onnx, onnxruntime, torch, model, sample, destination):
onnx.checker.check_model(onnx.load(destination))
expected = model(sample).detach().numpy()
session = onnxruntime.InferenceSession(
str(destination),
providers=["CPUExecutionProvider"],
)
actual = session.run(None, {"input": sample.numpy()})[0]
np.testing.assert_allclose(actual, expected, rtol=1e-4, atol=1e-5)
def _export(torch, model, sample, destination, dynamic_axes=None):
model.eval()
torch.onnx.export(
model,
sample,
destination,
export_params=True,
opset_version=OPSET_VERSION,
do_constant_folding=True,
input_names=["input"],
output_names=["output"],
dynamic_axes=dynamic_axes,
dynamo=False,
)
def _restore_realesrgan_scope_names(onnx, destination):
model = onnx.load(destination)
for node in model.graph.node:
node.name = node.name.replace("/body.", "/body/body.")
for values in (node.input, node.output):
for index, value in enumerate(values):
values[index] = value.replace("/body.", "/body/body.")
onnx.save(model, destination)
def export_realesrgan_wdn_x4(checkpoint_path, destination):
onnx, onnxruntime, torch = _require_build_dependencies()
nn = torch.nn
class SRVGGNetCompact(nn.Module):
def __init__(self):
super().__init__()
self.upscale = 4
self.body = nn.ModuleList([nn.Conv2d(3, 64, 3, 1, 1), nn.PReLU(64)])
for _ in range(32):
self.body.extend([nn.Conv2d(64, 64, 3, 1, 1), nn.PReLU(64)])
self.body.append(nn.Conv2d(64, 3 * self.upscale**2, 3, 1, 1))
self.upsampler = nn.PixelShuffle(self.upscale)
def forward(self, value):
result = value
for layer in self.body:
result = layer(result)
result = self.upsampler(result)
return result + torch.nn.functional.interpolate(
value,
scale_factor=self.upscale,
mode="nearest",
)
model = SRVGGNetCompact()
model.load_state_dict(_load_weights(torch, checkpoint_path, "params"), strict=True)
sample = torch.rand(1, 3, 16, 16)
dynamic_axes = {
"input": {2: "height", 3: "width"},
"output": {2: "height4", 3: "width4"},
}
_export(torch, model, sample, destination, dynamic_axes)
_restore_realesrgan_scope_names(onnx, destination)
_validate_export(onnx, onnxruntime, torch, model, sample, destination)
def export_safmn_real_x4(checkpoint_path, destination):
onnx, onnxruntime, torch = _require_build_dependencies()
nn = torch.nn
functional = torch.nn.functional
class LayerNorm(nn.Module):
def __init__(self, channels):
super().__init__()
self.weight = nn.Parameter(torch.ones(channels))
self.bias = nn.Parameter(torch.zeros(channels))
self.eps = 1e-6
def forward(self, value):
mean = value.mean(1, keepdim=True)
variance = (value - mean).pow(2).mean(1, keepdim=True)
value = (value - mean) / torch.sqrt(variance + self.eps)
return self.weight[:, None, None] * value + self.bias[:, None, None]
class SAFM(nn.Module):
def __init__(self, channels):
super().__init__()
chunk_channels = channels // 4
self.mfr = nn.ModuleList(
nn.Conv2d(chunk_channels, chunk_channels, 3, 1, 1, groups=chunk_channels)
for _ in range(4)
)
self.aggr = nn.Conv2d(channels, channels, 1)
self.act = nn.GELU()
def forward(self, value):
height, width = value.shape[-2:]
outputs = []
for level, chunk in enumerate(value.chunk(4, dim=1)):
if level:
size = (height // 2**level, width // 2**level)
chunk = functional.adaptive_max_pool2d(chunk, size)
chunk = self.mfr[level](chunk)
if level:
chunk = functional.interpolate(
chunk,
size=(height, width),
mode="nearest",
)
outputs.append(chunk)
return self.act(self.aggr(torch.cat(outputs, dim=1))) * value
class CCM(nn.Module):
def __init__(self, channels):
super().__init__()
self.ccm = nn.Sequential(
nn.Conv2d(channels, channels * 2, 3, 1, 1),
nn.GELU(),
nn.Conv2d(channels * 2, channels, 1),
)
def forward(self, value):
return self.ccm(value)
class AttBlock(nn.Module):
def __init__(self, channels):
super().__init__()
self.norm1 = LayerNorm(channels)
self.norm2 = LayerNorm(channels)
self.safm = SAFM(channels)
self.ccm = CCM(channels)
def forward(self, value):
value = self.safm(self.norm1(value)) + value
return self.ccm(self.norm2(value)) + value
class SAFMN(nn.Module):
def __init__(self):
super().__init__()
self.to_feat = nn.Conv2d(3, 128, 3, 1, 1)
self.feats = nn.Sequential(*(AttBlock(128) for _ in range(16)))
self.to_img = nn.Sequential(
nn.Conv2d(128, 3 * 4**2, 3, 1, 1),
nn.PixelShuffle(4),
)
def forward(self, value):
value = self.to_feat(value)
return self.to_img(self.feats(value) + value)
model = SAFMN()
model.load_state_dict(
_load_weights(torch, checkpoint_path, "params_ema"),
strict=True,
)
sample = torch.rand(1, 3, 256, 256)
_export(torch, model, sample, destination)
_validate_export(onnx, onnxruntime, torch, model, sample, destination)
def export_model(exporter, checkpoint_path, destination):
exporters = {
"realesrgan_wdn_x4": export_realesrgan_wdn_x4,
"safmn_real_x4": export_safmn_real_x4,
}
try:
export = exporters[exporter]
except KeyError as error:
raise ValueError(f"Unknown model exporter: {exporter}") from error
export(checkpoint_path, destination)

View File

@@ -2,7 +2,7 @@
import bpy
from anyimage.operators.common.mesh_result import (
from anyimage.common.object import (
DEPTH_MATERIAL_NODE_GROUP_NAME,
IMAGE_MATERIAL_NODE_GROUP_NAME,
)

View File

@@ -10,19 +10,20 @@ sys.path.insert(
str(next((PROJECT_ROOT / "src" / "anyimage" / "wheels").glob("blendjob-*.whl"))),
)
from anyimage.operators.common.mesh_result import (
from anyimage.common.object import (
DEPTH_MATERIAL_NODE_GROUP_NAME,
IMAGE_MATERIAL_NODE_GROUP_NAME,
load_asset_node_group,
node_asset_path,
)
from anyimage.operators.convert_to_plane.nodes import DEPTH_PLANE_NODE_GROUP_NAME
from anyimage.common.node import load_node_group, node_asset_path
from anyimage.operators.cutout_tool.shape import (
CUTOUT_NODE_GROUP_NAME,
CUTOUT_NODE_GROUP_NAMES,
)
DEPTH_PLANE_NODE_GROUP_NAME = "O Image Depth Plane"
def input_names(group):
return [
item.name
@@ -33,12 +34,12 @@ def input_names(group):
def main():
bpy.ops.wm.read_factory_settings(use_empty=True)
mesh_plane = load_asset_node_group("O Mesh Plane")
depth_plane = load_asset_node_group(DEPTH_PLANE_NODE_GROUP_NAME)
image_layer = load_asset_node_group(IMAGE_MATERIAL_NODE_GROUP_NAME)
depth_layer = load_asset_node_group(DEPTH_MATERIAL_NODE_GROUP_NAME)
shadeless = load_asset_node_group("O Shadeless")
cutout = load_asset_node_group(CUTOUT_NODE_GROUP_NAME)
mesh_plane = load_node_group("O Mesh Plane")
depth_plane = load_node_group(DEPTH_PLANE_NODE_GROUP_NAME)
image_layer = load_node_group(IMAGE_MATERIAL_NODE_GROUP_NAME)
depth_layer = load_node_group(DEPTH_MATERIAL_NODE_GROUP_NAME)
shadeless = load_node_group("O Shadeless")
cutout = load_node_group(CUTOUT_NODE_GROUP_NAME)
cutout_groups = {
name: bpy.data.node_groups[name] for name in CUTOUT_NODE_GROUP_NAMES.values()
}

View File

@@ -10,7 +10,7 @@ sys.path.insert(
str(next((PROJECT_ROOT / "src" / "anyimage" / "wheels").glob("blendjob-*.whl"))),
)
from anyimage.operators.common.mesh_result import (
from anyimage.common.object import (
modifier_input_identifier,
set_modifier_input,
)

View File

@@ -11,7 +11,7 @@ sys.path.insert(
str(next((PROJECT_ROOT / "src" / "anyimage" / "wheels").glob("blendjob-*.whl"))),
)
from anyimage.operators.common.mesh_result import (
from anyimage.common.object import (
modifier_input_identifier,
set_modifier_input,
)

View File

@@ -7,7 +7,7 @@ from pathlib import Path
from zipfile import ZIP_DEFLATED, ZipFile
PROJECT_ROOT = Path(__file__).resolve().parent
PROJECT_ROOT = Path(__file__).resolve().parents[1]
SOURCE_DIR = PROJECT_ROOT / "src" / "anyimage"
DIST_DIR = PROJECT_ROOT / "dist"
PYTHON_VERSION = "3.11"
@@ -134,8 +134,12 @@ def build_all():
return [build_extension(platform) for platform in PLATFORMS]
if __name__ == "__main__":
def main():
extension_paths = build_all()
for extension_path in extension_paths:
size_mib = extension_path.stat().st_size / (1024 * 1024)
print(f"Built {extension_path} ({size_mib:.2f} MiB)")
if __name__ == "__main__":
main()

244
tools/sync_models.py Normal file
View File

@@ -0,0 +1,244 @@
"""Prepare AnyImage ONNX artifacts and upload them to OmooLab R2."""
import argparse
import hashlib
import shutil
import subprocess
import sys
from dataclasses import dataclass
from pathlib import Path
from urllib.request import Request, urlopen
PROJECT_ROOT = Path(__file__).resolve().parents[1]
MODELS_DIR = PROJECT_ROOT / "models"
MODEL_CACHE_DIR = PROJECT_ROOT / ".model-cache"
SOURCE_ROOT = PROJECT_ROOT / "src" / "anyimage"
sys.path.insert(0, str(SOURCE_ROOT))
from server.model_catalog import DOWNLOADABLE_MODELS
R2_REMOTE = "omoolab-r2:ai-models"
CHUNK_SIZE = 1024 * 1024
@dataclass(frozen=True)
class BuildSpec:
source_url: str
source_filename: str
source_size: int
source_sha256: str
exporter: str
@dataclass(frozen=True)
class ModelFile:
key: str
folder: str
filename: str
size: int
sha256: str
source_url: str = ""
build: BuildSpec | None = None
@property
def destination(self):
return MODELS_DIR / self.folder / self.filename
@property
def remote(self):
return f"{R2_REMOTE}/{self.folder}/{self.filename}"
MODEL_BUILDS = {
"REALESRGAN_GENERAL_WDN_X4V3": BuildSpec(
source_url=(
"https://github.com/xinntao/Real-ESRGAN/releases/download/"
"v0.2.5.0/realesr-general-wdn-x4v3.pth"
),
source_filename="realesr-general-wdn-x4v3.pth",
source_size=4885111,
source_sha256=(
"1641f8c4464b9f097c9fdda5589273713f67cf59f3d909e0bd688f0cee269dca"
),
exporter="realesrgan_wdn_x4",
),
"SAFMN_REAL_X4": BuildSpec(
source_url=(
"https://github.com/sunny2109/SAFMN/releases/download/"
"v0.1.0/SAFMN_L_Real_LSDIR_x4-v2.pth"
),
source_filename="SAFMN_L_Real_LSDIR_x4-v2.pth",
source_size=45002097,
source_sha256=(
"f1ac0ee3ee143fbbc49aff6584cb7b48b71ba8d1961dfb8ad076b98ca4799ae2"
),
exporter="safmn_real_x4",
),
}
def hf_url(repository, filename):
return f"https://huggingface.co/{repository}/resolve/main/{filename}"
def catalog_model_files():
files = []
for key, model in DOWNLOADABLE_MODELS.items():
for filename, size, sha256 in model.r2_files:
source_url = ""
if model.huggingface_repository:
source_url = hf_url(model.huggingface_repository, filename)
files.append(
ModelFile(
key=key,
folder=model.r2_directory,
filename=filename,
size=size,
sha256=sha256,
source_url=source_url,
build=MODEL_BUILDS.get(key),
)
)
return tuple(files)
MODEL_FILES = catalog_model_files()
def file_sha256(path):
digest = hashlib.sha256()
with Path(path).open("rb") as stream:
while chunk := stream.read(CHUNK_SIZE):
digest.update(chunk)
return digest.hexdigest()
def verify_file(path, size, sha256):
path = Path(path)
if not path.is_file():
return False
if path.stat().st_size != size:
raise RuntimeError(f"File has an unexpected size: {path}")
if file_sha256(path) != sha256:
raise RuntimeError(f"File has an unexpected SHA-256: {path}")
return True
def download_file(url, destination, size, sha256):
destination = Path(destination)
destination.parent.mkdir(parents=True, exist_ok=True)
temporary = destination.with_name(f"{destination.name}.part")
temporary.unlink(missing_ok=True)
request = Request(url, headers={"User-Agent": "AnyImage model build"})
print(f"Download: {url}")
downloaded = 0
digest = hashlib.sha256()
try:
with urlopen(request, timeout=60) as response, temporary.open("wb") as output:
while chunk := response.read(CHUNK_SIZE):
output.write(chunk)
digest.update(chunk)
downloaded += len(chunk)
print(
f"\r {downloaded / 1024**2:.1f} / {size / 1024**2:.1f} MiB",
end="",
flush=True,
)
print()
if downloaded != size:
raise RuntimeError(f"size mismatch: {downloaded} != {size}")
if digest.hexdigest() != sha256:
raise RuntimeError("SHA-256 mismatch")
temporary.replace(destination)
except Exception:
temporary.unlink(missing_ok=True)
raise
def prepare_source(build):
source = MODEL_CACHE_DIR / build.source_filename
if verify_file(source, build.source_size, build.source_sha256):
return source
download_file(
build.source_url,
source,
build.source_size,
build.source_sha256,
)
return source
def build_model(model):
from tools.model_exports import export_model
source = prepare_source(model.build)
destination = model.destination
destination.parent.mkdir(parents=True, exist_ok=True)
temporary = destination.with_name(f"{destination.name}.part")
temporary.unlink(missing_ok=True)
print(f"Build: {destination.relative_to(PROJECT_ROOT)}")
try:
export_model(model.build.exporter, source, temporary)
verify_file(temporary, model.size, model.sha256)
temporary.replace(destination)
except Exception:
temporary.unlink(missing_ok=True)
raise
def prepare_model(model):
if verify_file(model.destination, model.size, model.sha256):
print(f"Skip existing: {model.destination.relative_to(PROJECT_ROOT)}")
return
if model.build:
build_model(model)
return
if not model.source_url:
raise RuntimeError(f"No upstream source or builder for {model.key}")
download_file(
model.source_url,
model.destination,
model.size,
model.sha256,
)
def upload_models(models=MODEL_FILES):
executable = shutil.which("rclone")
if executable is None:
raise RuntimeError("rclone is not available on PATH")
for model in models:
print(f"Upload: {model.remote}")
subprocess.run(
[
executable,
"copyto",
str(model.destination),
model.remote,
"--progress",
],
cwd=PROJECT_ROOT,
check=True,
)
def parse_args():
parser = argparse.ArgumentParser()
parser.add_argument(
"--prepare-only",
action="store_true",
help="Prepare and verify models without uploading them",
)
return parser.parse_args()
def main():
args = parse_args()
for model in MODEL_FILES:
prepare_model(model)
if not args.prepare_only:
upload_models()
if __name__ == "__main__":
main()

348
uv.lock generated
View File

@@ -14,7 +14,7 @@ wheels = [
[[package]]
name = "anyimage"
version = "0.12.15"
source = { virtual = "." }
source = { editable = "." }
dependencies = [
{ name = "blendjob" },
]
@@ -24,18 +24,19 @@ dev = [
{ name = "av" },
{ name = "bpy" },
{ name = "huggingface-hub" },
{ name = "mike" },
{ name = "mkdocs" },
{ name = "mkdocs-click" },
{ name = "mkdocs-material" },
{ name = "numpy" },
{ name = "onnx" },
{ name = "onnxruntime" },
{ name = "pillow" },
{ name = "pip" },
{ name = "pytest" },
{ name = "torch" },
{ name = "trimesh" },
]
docs = [
{ name = "mike" },
{ name = "mkdocs" },
{ name = "mkdocs-material" },
]
[package.metadata]
requires-dist = [{ name = "blendjob", specifier = "==0.1.14" }]
@@ -45,18 +46,19 @@ dev = [
{ name = "av", specifier = "==17.0.0" },
{ name = "bpy", specifier = "==4.5.3" },
{ name = "huggingface-hub", specifier = "==1.4.1" },
{ name = "mike", specifier = ">=2.1.3,<3" },
{ name = "mkdocs", specifier = ">=1.6.1,<2" },
{ name = "mkdocs-click", specifier = "==0.9.0" },
{ name = "mkdocs-material", specifier = ">=9.6.16,<10" },
{ name = "numpy", specifier = ">=1.26,<2.0" },
{ name = "onnx", specifier = "==1.19.0" },
{ name = "onnxruntime", specifier = "==1.24.4" },
{ name = "pillow", specifier = "==12.1.1" },
{ name = "pip", specifier = ">=25,<27" },
{ name = "pytest" },
{ name = "torch", specifier = "==2.13.0" },
{ name = "trimesh", specifier = "==4.11.2" },
]
docs = [
{ name = "mike", specifier = ">=2.1.3,<3" },
{ name = "mkdocs", specifier = ">=1.6.1,<2" },
{ name = "mkdocs-material", specifier = ">=9.6.16,<10" },
]
[[package]]
name = "anyio"
@@ -204,6 +206,75 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
]
[[package]]
name = "cuda-bindings"
version = "13.3.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cuda-pathfinder" },
]
wheels = [
{ url = "https://files.pythonhosted.org/packages/51/6b/457ca12dad3ee9bfcc9a545cfd6b64b359ba49de40f776f6e028e678f262/cuda_bindings-13.3.1-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c5879712accf6e14bb01aa5e67440eb84998b8d104b509cc7a6dc0b8f656a474", size = 6053539, upload-time = "2026-05-29T23:11:43.19Z" },
{ url = "https://files.pythonhosted.org/packages/95/7a/c5e3c34a409b148f5c0f5a4ea374158f95d488862c1dffedf9aa5c639df9/cuda_bindings-13.3.1-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:04436a9364059c84b8f9636f359eccda1cf814341f5b670c71d80d2f79dbc708", size = 6674166, upload-time = "2026-05-29T23:11:45.478Z" },
]
[[package]]
name = "cuda-pathfinder"
version = "1.8.0"
source = { registry = "https://pypi.org/simple" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a1/b1/ef21259ec74fe0b265ed201379de1d0ef7c14178313ee03705952f1b7093/cuda_pathfinder-1.8.0-py3-none-any.whl", hash = "sha256:c44e574dc997fae2814721d1ae97d0fd6db76db82decbe9b753bf75de53f515e", size = 62539, upload-time = "2026-08-27T21:33:03.229Z" },
]
[[package]]
name = "cuda-toolkit"
version = "13.0.3.0"
source = { registry = "https://pypi.org/simple" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d1/c7/a79086a62c98befcdb8349656c6f114e2db3b8b2422f6e25c97a7f2a9a3c/cuda_toolkit-13.0.3.0-py2.py3-none-any.whl", hash = "sha256:d693caaa261214ddd7dbb60d68e71cbed884e68c2be7509778f3051da0b91c3f", size = 2512, upload-time = "2026-04-14T00:50:08.173Z" },
]
[package.optional-dependencies]
cublas = [
{ name = "nvidia-cublas", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine == 'AMD64' and sys_platform == 'win32')" },
{ name = "nvidia-cuda-nvrtc", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine == 'AMD64' and sys_platform == 'win32')" },
]
cudart = [
{ name = "nvidia-cuda-runtime", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine == 'AMD64' and sys_platform == 'win32')" },
]
cufft = [
{ name = "nvidia-cufft", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine == 'AMD64' and sys_platform == 'win32')" },
{ name = "nvidia-nvjitlink", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine == 'AMD64' and sys_platform == 'win32')" },
]
cufile = [
{ name = "nvidia-cufile", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" },
]
cupti = [
{ name = "nvidia-cuda-cupti", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine == 'AMD64' and sys_platform == 'win32')" },
]
curand = [
{ name = "nvidia-curand", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine == 'AMD64' and sys_platform == 'win32')" },
]
cusolver = [
{ name = "nvidia-cublas", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine == 'AMD64' and sys_platform == 'win32')" },
{ name = "nvidia-cusolver", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine == 'AMD64' and sys_platform == 'win32')" },
{ name = "nvidia-cusparse", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine == 'AMD64' and sys_platform == 'win32')" },
{ name = "nvidia-nvjitlink", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine == 'AMD64' and sys_platform == 'win32')" },
]
cusparse = [
{ name = "nvidia-cusparse", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine == 'AMD64' and sys_platform == 'win32')" },
{ name = "nvidia-nvjitlink", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine == 'AMD64' and sys_platform == 'win32')" },
]
nvjitlink = [
{ name = "nvidia-nvjitlink", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine == 'AMD64' and sys_platform == 'win32')" },
]
nvrtc = [
{ name = "nvidia-cuda-nvrtc", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine == 'AMD64' and sys_platform == 'win32')" },
]
nvtx = [
{ name = "nvidia-nvtx", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine == 'AMD64' and sys_platform == 'win32')" },
]
[[package]]
name = "cython"
version = "3.2.9"
@@ -467,6 +538,19 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/22/5b/dbc6a8cddc9cfa9c4971d59fb12bb8d42e161b7e7f8cc89e49137c5b279c/mkdocs-1.6.1-py3-none-any.whl", hash = "sha256:db91759624d1647f3f34aa0c3f327dd2601beae39a366d6e064c03468d35c20e", size = 3864451, upload-time = "2024-08-30T12:24:05.054Z" },
]
[[package]]
name = "mkdocs-click"
version = "0.9.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "click" },
{ name = "markdown" },
]
sdist = { url = "https://files.pythonhosted.org/packages/a1/c7/8c25f3a3b379def41e6d0bb5c4beeab7aa8a394b17e749f498504102cfa5/mkdocs_click-0.9.0.tar.gz", hash = "sha256:6050917628d4740517541422b607404d044117bc31b770c4f9e9e1939a50c908", size = 18720, upload-time = "2025-04-07T16:59:36.387Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e9/fc/9124ab36e2341e78d8d9c669511bd70f52ea0de8105760c31fabec1f9396/mkdocs_click-0.9.0-py3-none-any.whl", hash = "sha256:5208e828f4f68f63c847c1ef7be48edee9964090390afc8f5b3d4cbe5ea9bbed", size = 15104, upload-time = "2025-04-07T16:59:34.807Z" },
]
[[package]]
name = "mkdocs-get-deps"
version = "0.2.2"
@@ -512,6 +596,22 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/5b/54/662a4743aa81d9582ee9339d4ffa3c8fd40a4965e033d77b9da9774d3960/mkdocs_material_extensions-1.3.1-py3-none-any.whl", hash = "sha256:adff8b62700b25cb77b53358dad940f3ef973dd6db797907c49e3c2ef3ab4e31", size = 8728, upload-time = "2023-11-22T19:09:43.465Z" },
]
[[package]]
name = "ml-dtypes"
version = "0.5.4"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "numpy" },
]
sdist = { url = "https://files.pythonhosted.org/packages/0e/4a/c27b42ed9b1c7d13d9ba8b6905dece787d6259152f2309338aed29b2447b/ml_dtypes-0.5.4.tar.gz", hash = "sha256:8ab06a50fb9bf9666dd0fe5dfb4676fa2b0ac0f31ecff72a6c3af8e22c063453", size = 692314, upload-time = "2025-11-17T22:32:31.031Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c6/5e/712092cfe7e5eb667b8ad9ca7c54442f21ed7ca8979745f1000e24cf8737/ml_dtypes-0.5.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6c7ecb74c4bd71db68a6bea1edf8da8c34f3d9fe218f038814fd1d310ac76c90", size = 679734, upload-time = "2025-11-17T22:31:39.223Z" },
{ url = "https://files.pythonhosted.org/packages/4f/cf/912146dfd4b5c0eea956836c01dcd2fce6c9c844b2691f5152aca196ce4f/ml_dtypes-0.5.4-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bc11d7e8c44a65115d05e2ab9989d1e045125d7be8e05a071a48bc76eb6d6040", size = 5056165, upload-time = "2025-11-17T22:31:41.071Z" },
{ url = "https://files.pythonhosted.org/packages/a9/80/19189ea605017473660e43762dc853d2797984b3c7bf30ce656099add30c/ml_dtypes-0.5.4-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:19b9a53598f21e453ea2fbda8aa783c20faff8e1eeb0d7ab899309a0053f1483", size = 5034975, upload-time = "2025-11-17T22:31:42.758Z" },
{ url = "https://files.pythonhosted.org/packages/b4/24/70bd59276883fdd91600ca20040b41efd4902a923283c4d6edcb1de128d2/ml_dtypes-0.5.4-cp311-cp311-win_amd64.whl", hash = "sha256:7c23c54a00ae43edf48d44066a7ec31e05fdc2eee0be2b8b50dd1903a1db94bb", size = 210742, upload-time = "2025-11-17T22:31:44.068Z" },
{ url = "https://files.pythonhosted.org/packages/a0/c9/64230ef14e40aa3f1cb254ef623bf812735e6bec7772848d19131111ac0d/ml_dtypes-0.5.4-cp311-cp311-win_arm64.whl", hash = "sha256:557a31a390b7e9439056644cb80ed0735a6e3e3bb09d67fd5687e4b04238d1de", size = 160709, upload-time = "2025-11-17T22:31:46.557Z" },
]
[[package]]
name = "mpmath"
version = "1.3.0"
@@ -521,6 +621,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c", size = 536198, upload-time = "2023-03-07T16:47:09.197Z" },
]
[[package]]
name = "networkx"
version = "3.6.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/6a/51/63fe664f3908c97be9d2e4f1158eb633317598cfa6e1fc14af5383f17512/networkx-3.6.1.tar.gz", hash = "sha256:26b7c357accc0c8cde558ad486283728b65b6a95d85ee1cd66bafab4c8168509", size = 2517025, upload-time = "2025-12-08T17:02:39.908Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/9e/c9/b2622292ea83fbb4ec318f5b9ab867d0a28ab43c5717bb85b0a5f6b3b0a4/networkx-3.6.1-py3-none-any.whl", hash = "sha256:d47fbf302e7d9cbbb9e2555a0d267983d2aa476bac30e90dfbe5669bd57f3762", size = 2068504, upload-time = "2025-12-08T17:02:38.159Z" },
]
[[package]]
name = "numpy"
version = "1.26.4"
@@ -537,6 +646,178 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/3f/6b/5610004206cf7f8e7ad91c5a85a8c71b2f2f8051a0c0c4d5916b76d6cbb2/numpy-1.26.4-cp311-cp311-win_amd64.whl", hash = "sha256:cd25bcecc4974d09257ffcd1f098ee778f7834c3ad767fe5db785be9a4aa9cb2", size = 15811913, upload-time = "2024-02-05T23:54:53.933Z" },
]
[[package]]
name = "nvidia-cublas"
version = "13.1.1.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "nvidia-cuda-nvrtc" },
]
wheels = [
{ url = "https://files.pythonhosted.org/packages/a7/a1/0bd24ee8c8d03adac032fd2909426a00c88f8c57961b1277ded97f91119f/nvidia_cublas-13.1.1.3-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:b7a210458267ac818974c53038fbec2e969d5c99f305ab15c72522fa9f001dd5", size = 542848918, upload-time = "2026-04-08T18:46:22.985Z" },
{ url = "https://files.pythonhosted.org/packages/3b/cd/154ca20c38269e05eff77c1464e6c1da89f50a6390b565e9d82e06bc11e1/nvidia_cublas-13.1.1.3-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:37936a16db8fe4ac1f065c2139360608a543a09275cb1a1af612e08cfa065436", size = 423138758, upload-time = "2026-04-08T18:46:58.655Z" },
]
[[package]]
name = "nvidia-cuda-cupti"
version = "13.0.85"
source = { registry = "https://pypi.org/simple" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/2a/2a/80353b103fc20ce05ef51e928daed4b6015db4aaa9162ed0997090fe2250/nvidia_cuda_cupti-13.0.85-py3-none-manylinux_2_25_aarch64.whl", hash = "sha256:796bd679890ee55fb14a94629b698b6db54bcfd833d391d5e94017dd9d7d3151", size = 10310827, upload-time = "2025-09-04T08:26:42.012Z" },
{ url = "https://files.pythonhosted.org/packages/33/6d/737d164b4837a9bbd202f5ae3078975f0525a55730fe871d8ed4e3b952b0/nvidia_cuda_cupti-13.0.85-py3-none-manylinux_2_25_x86_64.whl", hash = "sha256:4eb01c08e859bf924d222250d2e8f8b8ff6d3db4721288cf35d14252a4d933c8", size = 10715597, upload-time = "2025-09-04T08:26:51.312Z" },
]
[[package]]
name = "nvidia-cuda-nvrtc"
version = "13.0.88"
source = { registry = "https://pypi.org/simple" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c3/68/483a78f5e8f31b08fb1bb671559968c0ca3a065ac7acabfc7cee55214fd6/nvidia_cuda_nvrtc-13.0.88-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:ad9b6d2ead2435f11cbb6868809d2adeeee302e9bb94bcf0539c7a40d80e8575", size = 90215200, upload-time = "2025-09-04T08:28:44.204Z" },
{ url = "https://files.pythonhosted.org/packages/b7/dc/6bb80850e0b7edd6588d560758f17e0550893a1feaf436807d64d2da040f/nvidia_cuda_nvrtc-13.0.88-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d27f20a0ca67a4bb34268a5e951033496c5b74870b868bacd046b1b8e0c3267b", size = 43015449, upload-time = "2025-09-04T08:28:20.239Z" },
]
[[package]]
name = "nvidia-cuda-runtime"
version = "13.0.96"
source = { registry = "https://pypi.org/simple" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/87/4f/17d7b9b8e285199c58ce28e31b5c5bbaa4d8271af06a89b6405258245de2/nvidia_cuda_runtime-13.0.96-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ef9bcbe90493a2b9d810e43d249adb3d02e98dd30200d86607d8d02687c43f55", size = 2261060, upload-time = "2025-10-09T08:55:15.78Z" },
{ url = "https://files.pythonhosted.org/packages/2e/24/d1558f3b68b1d26e706813b1d10aa1d785e4698c425af8db8edc3dced472/nvidia_cuda_runtime-13.0.96-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7f82250d7782aa23b6cfe765ecc7db554bd3c2870c43f3d1821f1d18aebf0548", size = 2243632, upload-time = "2025-10-09T08:55:36.117Z" },
]
[[package]]
name = "nvidia-cudnn-cu13"
version = "9.20.0.48"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "nvidia-cublas" },
]
wheels = [
{ url = "https://files.pythonhosted.org/packages/56/c5/83384d846b2fd17c44bd499b36c75a45ed4f095fbbb2252294e89cea5c5c/nvidia_cudnn_cu13-9.20.0.48-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:e31454ae00094b0c55319d9d15b6fa2fc50a9e1c0f5c8c80fb75258234e731e1", size = 444574296, upload-time = "2026-03-09T19:28:27.751Z" },
{ url = "https://files.pythonhosted.org/packages/6e/5e/edb9c0ae051602c3ccaffe424256463636d639e27d7f302dde9975ef9e7a/nvidia_cudnn_cu13-9.20.0.48-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:0c45dd8eeb50b603f07995b1b300c62ffe6a1980482b82b3bcf94a4ca9d49304", size = 366173588, upload-time = "2026-03-09T19:29:34.474Z" },
]
[[package]]
name = "nvidia-cufft"
version = "12.0.0.61"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "nvidia-nvjitlink" },
]
wheels = [
{ url = "https://files.pythonhosted.org/packages/8b/ae/f417a75c0259e85c1d2f83ca4e960289a5f814ed0cea74d18c353d3e989d/nvidia_cufft-12.0.0.61-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2708c852ef8cd89d1d2068bdbece0aa188813a0c934db3779b9b1faa8442e5f5", size = 214053554, upload-time = "2025-09-04T08:31:38.196Z" },
{ url = "https://files.pythonhosted.org/packages/a8/2f/7b57e29836ea8714f81e9898409196f47d772d5ddedddf1592eadb8ab743/nvidia_cufft-12.0.0.61-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6c44f692dce8fd5ffd3e3df134b6cdb9c2f72d99cf40b62c32dde45eea9ddad3", size = 214085489, upload-time = "2025-09-04T08:31:56.044Z" },
]
[[package]]
name = "nvidia-cufile"
version = "1.15.1.6"
source = { registry = "https://pypi.org/simple" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/3f/70/4f193de89a48b71714e74602ee14d04e4019ad36a5a9f20c425776e72cd6/nvidia_cufile-1.15.1.6-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:08a3ecefae5a01c7f5117351c64f17c7c62efa5fffdbe24fc7d298da19cd0b44", size = 1223672, upload-time = "2025-09-04T08:32:22.779Z" },
{ url = "https://files.pythonhosted.org/packages/ab/73/cc4a14c9813a8a0d509417cf5f4bdaba76e924d58beb9864f5a7baceefbf/nvidia_cufile-1.15.1.6-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:bdc0deedc61f548bddf7733bdc216456c2fdb101d020e1ab4b88d232d5e2f6d1", size = 1136992, upload-time = "2025-09-04T08:32:14.119Z" },
]
[[package]]
name = "nvidia-curand"
version = "10.4.0.35"
source = { registry = "https://pypi.org/simple" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/1e/72/7c2ae24fb6b63a32e6ae5d241cc65263ea18d08802aaae087d9f013335a2/nvidia_curand-10.4.0.35-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:133df5a7509c3e292aaa2b477afd0194f06ce4ea24d714d616ff36439cee349a", size = 61962106, upload-time = "2025-08-04T10:21:41.128Z" },
{ url = "https://files.pythonhosted.org/packages/a5/9f/be0a41ca4a4917abf5cb9ae0daff1a6060cc5de950aec0396de9f3b52bc5/nvidia_curand-10.4.0.35-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:1aee33a5da6e1db083fe2b90082def8915f30f3248d5896bcec36a579d941bfc", size = 59544258, upload-time = "2025-08-04T10:22:03.992Z" },
]
[[package]]
name = "nvidia-cusolver"
version = "12.0.4.66"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "nvidia-cublas" },
{ name = "nvidia-cusparse" },
{ name = "nvidia-nvjitlink" },
]
wheels = [
{ url = "https://files.pythonhosted.org/packages/c8/c3/b30c9e935fc01e3da443ec0116ed1b2a009bb867f5324d3f2d7e533e776b/nvidia_cusolver-12.0.4.66-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:02c2457eaa9e39de20f880f4bd8820e6a1cfb9f9a34f820eb12a155aa5bc92d2", size = 223467760, upload-time = "2025-09-04T08:33:04.222Z" },
{ url = "https://files.pythonhosted.org/packages/5f/67/cba3777620cdacb99102da4042883709c41c709f4b6323c10781a9c3aa34/nvidia_cusolver-12.0.4.66-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:0a759da5dea5c0ea10fd307de75cdeb59e7ea4fcb8add0924859b944babf1112", size = 200941980, upload-time = "2025-09-04T08:33:22.767Z" },
]
[[package]]
name = "nvidia-cusparse"
version = "12.6.3.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "nvidia-nvjitlink" },
]
wheels = [
{ url = "https://files.pythonhosted.org/packages/f8/94/5c26f33738ae35276672f12615a64bd008ed5be6d1ebcb23579285d960a9/nvidia_cusparse-12.6.3.3-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:80bcc4662f23f1054ee334a15c72b8940402975e0eab63178fc7e670aa59472c", size = 162155568, upload-time = "2025-09-04T08:33:42.864Z" },
{ url = "https://files.pythonhosted.org/packages/fa/18/623c77619c31d62efd55302939756966f3ecc8d724a14dab2b75f1508850/nvidia_cusparse-12.6.3.3-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2b3c89c88d01ee0e477cb7f82ef60a11a4bcd57b6b87c33f789350b59759360b", size = 145942937, upload-time = "2025-09-04T08:33:58.029Z" },
]
[[package]]
name = "nvidia-cusparselt-cu13"
version = "0.8.1"
source = { registry = "https://pypi.org/simple" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/46/e1/cdc1797eadf82d3a9a575a19b33fdc871a97edbec42c00b5b5e914f4aff4/nvidia_cusparselt_cu13-0.8.1-py3-none-manylinux2014_aarch64.whl", hash = "sha256:4dca476c50bf4780d46cd0bfbd82e2bc10a08e4fef7950917ce8d7578d22a23f", size = 221051344, upload-time = "2025-09-05T18:49:51.289Z" },
{ url = "https://files.pythonhosted.org/packages/34/7d/2661f2fb3ac4302f3a246f5fc030213ac60c1fe0bce84f9783dbd831dbb7/nvidia_cusparselt_cu13-0.8.1-py3-none-manylinux2014_x86_64.whl", hash = "sha256:786ce87568c303fadb5afcc7102d454cd3040d75f6f8626f5db460d1871f4dd0", size = 170148586, upload-time = "2025-09-05T18:50:50.248Z" },
]
[[package]]
name = "nvidia-nccl-cu13"
version = "2.29.7"
source = { registry = "https://pypi.org/simple" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/72/0d/daf50d44177ee0cbc7ff0a0c91eb5ff676c82be42f9a970bc7597f440c3a/nvidia_nccl_cu13-2.29.7-py3-none-manylinux_2_18_aarch64.whl", hash = "sha256:674a12383e3c38a1bcccae7d4f3633b37852230b6047883cb2f4c2d1b36d9bf5", size = 206014712, upload-time = "2026-03-03T05:34:20.843Z" },
{ url = "https://files.pythonhosted.org/packages/67/f4/58e4e91b6919367c7aafb8e36fce9aad1a3047e536bf7e2fd560927d3a4c/nvidia_nccl_cu13-2.29.7-py3-none-manylinux_2_18_x86_64.whl", hash = "sha256:edd81538446786ec3b73972543e53bb43bcaf0bfc8ef76cb679fcc390ffe136d", size = 205976000, upload-time = "2026-03-03T05:36:24.472Z" },
]
[[package]]
name = "nvidia-nvjitlink"
version = "13.3.33"
source = { registry = "https://pypi.org/simple" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f0/ee/580ca6f29dcab0221db8706badca1bbbb084f1975c4d4e83329c3a7e31f0/nvidia_nvjitlink-13.3.33-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:26a6de7fb4c8fdaa7703d3dad720d6d427ddfea5c48a528fd97c11733ad830e5", size = 40742423, upload-time = "2026-05-26T16:54:51.613Z" },
{ url = "https://files.pythonhosted.org/packages/69/30/45414e35ff2eee7db3da037e5707037ccf9d2b5218ffbdb055ea4d5aa98a/nvidia_nvjitlink-13.3.33-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ce48b37dfeb3cb1eae4cf85adacb47d7a6539ea2272870c9a3628ce275c2037e", size = 39168635, upload-time = "2026-05-26T16:54:13.906Z" },
]
[[package]]
name = "nvidia-nvshmem-cu13"
version = "3.4.5"
source = { registry = "https://pypi.org/simple" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/dc/0f/05cc9c720236dcd2db9c1ab97fff629e96821be2e63103569da0c9b72f19/nvidia_nvshmem_cu13-3.4.5-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6dc2a197f38e5d0376ad52cd1a2a3617d3cdc150fd5966f4aee9bcebb1d68fe9", size = 60215947, upload-time = "2025-09-06T00:32:20.022Z" },
{ url = "https://files.pythonhosted.org/packages/3c/35/a9bf80a609e74e3b000fef598933235c908fcefcef9026042b8e6dfde2a9/nvidia_nvshmem_cu13-3.4.5-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:290f0a2ee94c9f3687a02502f3b9299a9f9fe826e6d0287ee18482e78d495b80", size = 60412546, upload-time = "2025-09-06T00:32:41.564Z" },
]
[[package]]
name = "nvidia-nvtx"
version = "13.0.85"
source = { registry = "https://pypi.org/simple" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c2/f3/d86c845465a2723ad7e1e5c36dcd75ddb82898b3f53be47ebd429fb2fa5d/nvidia_nvtx-13.0.85-py3-none-manylinux1_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4936d1d6780fbe68db454f5e72a42ff64d1fd6397df9f363ae786930fd5c1cd4", size = 148047, upload-time = "2025-09-04T08:29:01.761Z" },
{ url = "https://files.pythonhosted.org/packages/a8/64/3708a90d1ebe202ffdeb7185f878a3c84d15c2b2c31858da2ce0583e2def/nvidia_nvtx-13.0.85-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cb7780edb6b14107373c835bf8b72e7a178bac7367e23da7acb108f973f157a6", size = 148878, upload-time = "2025-09-04T08:28:53.627Z" },
]
[[package]]
name = "onnx"
version = "1.19.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "ml-dtypes" },
{ name = "numpy" },
{ name = "protobuf" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/5b/bf/b0a63ee9f3759dcd177b28c6f2cb22f2aecc6d9b3efecaabc298883caa5f/onnx-1.19.0.tar.gz", hash = "sha256:aa3f70b60f54a29015e41639298ace06adf1dd6b023b9b30f1bca91bb0db9473", size = 11949859, upload-time = "2025-08-27T02:34:27.107Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/db/5c/b959b17608cfb6ccf6359b39fe56a5b0b7d965b3d6e6a3c0add90812c36e/onnx-1.19.0-cp311-cp311-macosx_12_0_universal2.whl", hash = "sha256:206f00c47b85b5c7af79671e3307147407991a17994c26974565aadc9e96e4e4", size = 18312580, upload-time = "2025-08-27T02:33:03.081Z" },
{ url = "https://files.pythonhosted.org/packages/2c/ee/ac052bbbc832abe0debb784c2c57f9582444fb5f51d63c2967fd04432444/onnx-1.19.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4d7bee94abaac28988b50da675ae99ef8dd3ce16210d591fbd0b214a5930beb3", size = 18029165, upload-time = "2025-08-27T02:33:05.771Z" },
{ url = "https://files.pythonhosted.org/packages/5c/c9/8687ba0948d46fd61b04e3952af9237883bbf8f16d716e7ed27e688d73b8/onnx-1.19.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7730b96b68c0c354bbc7857961bb4909b9aaa171360a8e3708d0a4c749aaadeb", size = 18202125, upload-time = "2025-08-27T02:33:09.325Z" },
{ url = "https://files.pythonhosted.org/packages/e2/16/6249c013e81bd689f46f96c7236d7677f1af5dd9ef22746716b48f10e506/onnx-1.19.0-cp311-cp311-win32.whl", hash = "sha256:7cb7a3ad8059d1a0dfdc5e0a98f71837d82002e441f112825403b137227c2c97", size = 16332738, upload-time = "2025-08-27T02:33:12.448Z" },
{ url = "https://files.pythonhosted.org/packages/6a/28/34a1e2166e418c6a78e5c82e66f409d9da9317832f11c647f7d4e23846a6/onnx-1.19.0-cp311-cp311-win_amd64.whl", hash = "sha256:d75452a9be868bd30c3ef6aa5991df89bbfe53d0d90b2325c5e730fbd91fff85", size = 16452303, upload-time = "2025-08-27T02:33:15.176Z" },
{ url = "https://files.pythonhosted.org/packages/e6/b7/639664626e5ba8027860c4d2a639ee02b37e9c322215c921e9222513c3aa/onnx-1.19.0-cp311-cp311-win_arm64.whl", hash = "sha256:23c7959370d7b3236f821e609b0af7763cff7672a758e6c1fc877bac099e786b", size = 16425340, upload-time = "2025-08-27T02:33:17.78Z" },
]
[[package]]
name = "onnxruntime"
version = "1.24.4"
@@ -767,6 +1048,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" },
]
[[package]]
name = "setuptools"
version = "84.0.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/6d/44/f5da03a8ef95d369145c5bb53050e7877c9f3d312e128605fd9504829143/setuptools-84.0.0.tar.gz", hash = "sha256:f4695c21257f0d9b537ec2692c941d02ee143b7cc1276941349a546573b2ef73", size = 1168449, upload-time = "2026-08-08T18:27:58.365Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/95/9c/c510029fc6ef33a6275cd2c5d3cecd6613dfd6aa401d57c54f1c18852ccf/setuptools-84.0.0-py3-none-any.whl", hash = "sha256:51a52592b3b99e102b609654876bd65f19f999935166d1352678931132b0c670", size = 818216, upload-time = "2026-08-08T18:27:56.719Z" },
]
[[package]]
name = "shellingham"
version = "1.5.4"
@@ -797,6 +1087,33 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl", hash = "sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5", size = 6299353, upload-time = "2025-04-27T18:04:59.103Z" },
]
[[package]]
name = "torch"
version = "2.13.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cuda-bindings", marker = "sys_platform == 'linux'" },
{ name = "cuda-toolkit", extra = ["cublas", "cudart", "cufft", "cufile", "cupti", "curand", "cusolver", "cusparse", "nvjitlink", "nvrtc", "nvtx"], marker = "sys_platform == 'linux'" },
{ name = "filelock" },
{ name = "fsspec" },
{ name = "jinja2" },
{ name = "networkx" },
{ name = "nvidia-cudnn-cu13", marker = "sys_platform == 'linux'" },
{ name = "nvidia-cusparselt-cu13", marker = "sys_platform == 'linux'" },
{ name = "nvidia-nccl-cu13", marker = "sys_platform == 'linux'" },
{ name = "nvidia-nvshmem-cu13", marker = "sys_platform == 'linux'" },
{ name = "setuptools" },
{ name = "sympy" },
{ name = "triton", marker = "sys_platform == 'linux'" },
{ name = "typing-extensions" },
]
wheels = [
{ url = "https://files.pythonhosted.org/packages/5b/fe/cba54dc58523434919b66f13a667e36e436deddd77ca519e96553617d4ec/torch-2.13.0-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:e76f9bcecc52b8ff711239a2f7547d5353df95878ab232f0773c1d95928b92f8", size = 111187938, upload-time = "2026-07-08T16:05:17.065Z" },
{ url = "https://files.pythonhosted.org/packages/c2/59/1e3160e18e12aa3038390efab3ce02b36a9d4d6a527ecdd8520dca2e68d8/torch-2.13.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:092790c696a760c729fd5722835f50b9d81fd7c8f141571f3f3cf4081a8f664c", size = 427199369, upload-time = "2026-07-08T16:04:51.054Z" },
{ url = "https://files.pythonhosted.org/packages/01/79/1f2d34ad7034ee1c7ffc1cf8bf0f8213af2a81df6ecdb3997ecec107c09d/torch-2.13.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:60fcdcb2f3876e21146cb4524ef06397d727ca9ad5f020818547e25075fe3cb7", size = 526574961, upload-time = "2026-07-08T16:04:07.075Z" },
{ url = "https://files.pythonhosted.org/packages/6c/fd/0f2ce40f58aefbdb3392f9acce3c8171940943ae2d661f70558bfa73befb/torch-2.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:a0d8b11f16a48d60e2015d8213aa0390744cbebb98e58b62b3514dddc656e330", size = 122015870, upload-time = "2026-07-08T16:05:27.59Z" },
]
[[package]]
name = "tqdm"
version = "4.70.0"
@@ -821,6 +1138,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/3c/b9/da09903ea53b677a58ba770112de6fe8b2acb8b4cd9bffae4ff6cfe7c072/trimesh-4.11.2-py3-none-any.whl", hash = "sha256:25e3ab2620f9eca5c9376168c67aabdd32205dad1c4eea09cd45cd4a3edf775a", size = 740328, upload-time = "2026-02-10T16:00:25.246Z" },
]
[[package]]
name = "triton"
version = "3.7.1"
source = { registry = "https://pypi.org/simple" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/7b/f9/19d842d06a08559534fa1eaab6ca551b1bcf40f06620bddec1babaa2772d/triton-3.7.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d4a0e1cd4c4a76370ed74a8432a53cea28716827d19e40ffc732233e35ceb3f6", size = 184664887, upload-time = "2026-06-17T20:03:42.913Z" },
{ url = "https://files.pythonhosted.org/packages/cd/5e/fce69606f7f240297f163e25539906732b199530d486ce67ae319877e821/triton-3.7.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6744957e9fd610a29680ec2346057d0c86948ed3812468670719f391e94b44a5", size = 197701306, upload-time = "2026-06-17T19:53:13.673Z" },
]
[[package]]
name = "typer"
version = "0.27.1"