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:
2
.github/workflows/build.yml
vendored
2
.github/workflows/build.yml
vendored
@@ -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
2
.gitignore
vendored
@@ -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
127
AGENTS.md
@@ -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 顺序与结构、节点不重叠、未使用输出隐藏及几何求值
|
||||
|
||||
79
README.md
79
README.md
@@ -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
BIN
docs/assets/logo.png
LFS
Normal file
Binary file not shown.
@@ -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 中执行。
|
||||
|
||||
## 下一步
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 的外部数据工具重新定位路径。
|
||||
@@ -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 的其他粘贴行为。
|
||||
@@ -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)。
|
||||
@@ -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`。超过时只会等比缩放模型分析输入;结果材质仍使用原始颜色图片,不会改用限制后的临时图。
|
||||
@@ -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 操作。
|
||||
@@ -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)。
|
||||
@@ -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)。
|
||||
@@ -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 后,确认你检查的是当前目录中的日志。
|
||||
@@ -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)。
|
||||
@@ -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)。
|
||||
@@ -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)
|
||||
@@ -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)。
|
||||
|
||||
@@ -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)。
|
||||
|
||||
@@ -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 选择和模型请求参数组装。
|
||||
|
||||
| 函数 | 职责 |
|
||||
| --- | --- |
|
||||
|
||||
@@ -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)。
|
||||
|
||||
@@ -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)按真实外部调用列出开发新功能时应优先复用的稳定入口;函数级分支、参数校验与异常处理仍以源码和测试为准。
|
||||
|
||||
@@ -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` 等备份不应提交或打包。
|
||||
|
||||
@@ -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 或模型依赖。
|
||||
|
||||
|
||||
@@ -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 数据。
|
||||
|
||||
@@ -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,再由节点根据相机元数据转换为局部位移。
|
||||
|
||||
## 节点说明
|
||||
|
||||
|
||||
@@ -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 不建立这条连接。
|
||||
|
||||
@@ -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 的目录。
|
||||
|
||||
@@ -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`。
|
||||
|
||||
@@ -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` 聚合注册类,扩展入口按顺序注册并逆序注销。
|
||||
|
||||
@@ -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。写回时保持中心和覆盖尺度近似一致。
|
||||
|
||||
@@ -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 步骤,并清理本次创建的临时输入。
|
||||
|
||||
@@ -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 为输入。
|
||||
|
||||
@@ -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 保存模型适配层的直接输出。
|
||||
|
||||
@@ -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,避免同类模型重复占用资源。
|
||||
|
||||
|
||||
@@ -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)。
|
||||
@@ -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";
|
||||
}
|
||||
22
mkdocs.yml
22
mkdocs.yml
@@ -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
|
||||
|
||||
|
||||
@@ -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.
1
src/anyimage/common/__init__.py
Normal file
1
src/anyimage/common/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Shared Blender-side image, node, object, interaction, and AI Job helpers."""
|
||||
@@ -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 = {
|
||||
@@ -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,
|
||||
47
src/anyimage/common/node.py
Normal file
47
src/anyimage/common/node.py
Normal 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
|
||||
@@ -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")
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
"""Shared Blender-side image data, interaction, mesh result, and AI Job helpers."""
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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}")
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
173
sync_models.py
173
sync_models.py
@@ -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
@@ -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
|
||||
@@ -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"
|
||||
@@ -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
42
tests/test_docs.py
Normal 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},
|
||||
)
|
||||
]
|
||||
@@ -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"))
|
||||
@@ -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)
|
||||
@@ -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",
|
||||
@@ -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(
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
72
tests/test_node_asset_launcher.py
Normal file
72
tests/test_node_asset_launcher.py
Normal 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()
|
||||
@@ -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()
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -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")
|
||||
@@ -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
99
tests/test_sync_models.py
Normal 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()
|
||||
26
tests/test_tool_commands.py
Normal file
26
tests/test_tool_commands.py
Normal 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
1
tools/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""AnyImage repository maintenance commands."""
|
||||
91
tools/build_node.py
Normal file
91
tools/build_node.py
Normal 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
48
tools/docs.py
Normal 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
204
tools/model_exports.py
Normal 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)
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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
244
tools/sync_models.py
Normal 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
348
uv.lock
generated
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user