Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f541a2fc8c | ||
|
|
be1ffd431c | ||
|
|
5b3eb0d2c0 | ||
|
|
ce7d20df86 | ||
|
|
a2bcc41bcf | ||
|
|
7b60ad5bea | ||
|
|
57e2476fb5 | ||
|
|
12519a4447 | ||
|
|
c1ad6b7a14 | ||
|
|
3f37f2da3f | ||
|
|
e3c68d48ef | ||
|
|
5e000c56ff | ||
|
|
0a56ae8372 | ||
|
|
67b0d14b2d | ||
|
|
1101bd91b6 | ||
|
|
51822fe912 | ||
|
|
ee93e7133c | ||
|
|
f269c16ca5 | ||
|
|
c41cbc2dd0 | ||
|
|
3c053e09df | ||
|
|
5a617560a3 | ||
|
|
5389e93994 | ||
|
|
f4843d30d3 | ||
|
|
8eb38a81bf | ||
|
|
e9ff4c2390 | ||
|
|
1687e14d08 | ||
|
|
e28b9c3810 | ||
|
|
5d2e6c5b9c |
@@ -0,0 +1,5 @@
|
||||
# Left out of the package published to the Comfy Registry; they stay in the GitHub repository.
|
||||
# The test suite (fixtures, stubs and helper processes that the Registry's scanner reads as risky code)
|
||||
tests/
|
||||
# The scripts that build the docs' images, node cards and PDF guides
|
||||
docs/_tools/
|
||||
@@ -1,354 +1,184 @@
|
||||
<p align="center"><img src="docs/assets/banner.jpg" alt="NH-Nodes — image, mask, resolution and workflow nodes for ComfyUI" width="100%"></p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://registry.comfy.org/nodes/nh-nodes"><img src="https://img.shields.io/badge/ComfyUI_Registry-nh--nodes-2563eb" alt="ComfyUI Registry"></a>
|
||||
<a href="https://github.com/jetthuangai/NH-Nodes/actions/workflows/tests.yml"><img src="https://github.com/jetthuangai/NH-Nodes/actions/workflows/tests.yml/badge.svg" alt="Tests"></a>
|
||||
<a href="LICENSE"><img src="https://img.shields.io/badge/License-MIT_(see_notices)-16a34a" alt="License"></a>
|
||||
<a href="docs/en/README.md"><img src="https://img.shields.io/badge/Docs-EN_%7C_VI-0f766e" alt="Documentation"></a>
|
||||
</p>
|
||||
|
||||
<p align="center"><b>English</b> · <a href="docs/vi/README.md">Tiếng Việt</a></p>
|
||||
|
||||
# NH-Nodes for ComfyUI
|
||||
|
||||
Production-focused custom nodes for ComfyUI workflows: image loading, tiling,
|
||||
compositing, mask utilities, smart resize, logic routing, prompt/text tooling,
|
||||
batch helpers, indexed model loaders, garment/portrait segmentation, and VTON
|
||||
preprocessing.
|
||||
**76 production-focused nodes** for ComfyUI: load and save image sets with their metadata, tile and composite large
|
||||
images, pick the right resolution for current models and put results back into the original frame, refine masks,
|
||||
segment garments and faces (also in half-body and group photos), mask the skin, hair, lips, teeth and clothes of people
|
||||
by typing their names, get the mask of anything you name with SAM 3 from a short formula such as `person - bag` or
|
||||
`background`, build VTON masks, route workflows with lazy switches, generate prompts, and test many LoRAs or models from
|
||||
one node.
|
||||
|
||||
[](https://registry.comfy.org/nodes/nh-nodes)
|
||||
[](https://github.com/jetthuangai/NH-Nodes)
|
||||
[](LICENSE)
|
||||
Every node is documented with a visual card, its inputs and outputs, and examples — in
|
||||
[English](docs/en/README.md) and [Vietnamese](docs/vi/README.md).
|
||||
|
||||
- **Author:** [jetthuang.com](https://jetthuang.com)
|
||||
- **Hugging Face:** [nhathoangfoto](https://huggingface.co/nhathoangfoto)
|
||||
- **Support:** [PayPal](https://paypal.me/nhathoangfoto)
|
||||
## Highlights
|
||||
|
||||
---
|
||||
|
||||
## Why NH-Nodes
|
||||
|
||||
NH-Nodes is built for practical workflow construction. It focuses on reusable
|
||||
building blocks that remove repetitive graph work:
|
||||
|
||||
- Load images with filename/path metadata.
|
||||
- Split images into tiles, upscale or process tiles, then stitch them back.
|
||||
- Build image grids, layer composites, comparisons, and labeled previews.
|
||||
- Resize images and masks with predictable aspect-ratio behavior.
|
||||
- Route workflow branches with boolean logic and generic data switches.
|
||||
- Generate prompts from templates, regex, lists, schedules, and text utilities.
|
||||
- Pick indexed LoRA/diffusion models for larger configurable workflows.
|
||||
- Prepare VTON masks, agnostic images, DensePose/parsing outputs, and related masks.
|
||||
|
||||
---
|
||||
<table>
|
||||
<tr>
|
||||
<td width="50%"><a href="docs/en/resolution.md"><img src="docs/assets/examples/resize-modes.jpg" alt="Resize modes"></a><br>
|
||||
<b>Resolution done right</b> — presets for FLUX.2, Qwen-Image and Z-Image, cover / contain / stretch resizing, and an
|
||||
exact round trip back to the original frame.</td>
|
||||
<td width="50%"><a href="docs/en/image-compose.md"><img src="docs/assets/examples/tile-untile.jpg" alt="Tile and untile"></a><br>
|
||||
<b>Large images</b> — split into overlapping tiles, process them, and blend them back seamlessly; deep-zoom preview
|
||||
for images too big for the canvas.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="docs/en/vision.md"><img src="docs/assets/examples/garment-segment.jpg" alt="Garment segmentation"></a><br>
|
||||
<b>Segmentation</b> — name what you want (people, windows, the sky) or write a formula such as
|
||||
<code>person - bag</code> or <code>background</code>, and SAM 3 returns the mask, with crisp edges on small and
|
||||
medium objects even in 4K–8K photos, and shows it on the node; type <code>skin</code>, <code>hair, face</code> or
|
||||
<code>clothes - shoes</code> and Body Segment masks those parts of every person; garment and face-part masks from
|
||||
models that parse faces and people close up; VTON garment masks with pose and parsing.</td>
|
||||
<td><a href="docs/en/mask.md"><img src="docs/assets/examples/mask-morphology.jpg" alt="Mask morphology"></a><br>
|
||||
<b>Mask tools</b> — fast grow / shrink on the CPU or the GPU, hole filling, soft edges, bounding boxes and
|
||||
aspect-ratio matching.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="docs/en/image-compose.md"><img src="docs/assets/examples/layer-layout.jpg" alt="Layer layout"></a><br>
|
||||
<b>Compositing</b> — layouts, layer stacks with alpha, labelled grids and side-by-side comparisons.</td>
|
||||
<td><a href="docs/en/logic.md"><img src="docs/assets/examples/grid-composite.jpg" alt="Grid composite"></a><br>
|
||||
<b>Workflow logic</b> — lazy If/Else and switches that run only the chosen branch, safe math, counters, lists and
|
||||
prompt tools.</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
## Installation
|
||||
|
||||
### ComfyUI Manager
|
||||
**ComfyUI Manager** — search for `NH-Nodes`, install, restart ComfyUI.
|
||||
|
||||
Search for `NH-Nodes`, install, then restart ComfyUI.
|
||||
|
||||
### Comfy CLI
|
||||
**Comfy CLI**
|
||||
|
||||
```bash
|
||||
comfy node install nh-nodes
|
||||
```
|
||||
|
||||
### Manual
|
||||
**Manual**
|
||||
|
||||
```bash
|
||||
cd ComfyUI/custom_nodes
|
||||
git clone https://github.com/jetthuangai/NH-Nodes.git
|
||||
cd NH-Nodes
|
||||
pip install -r requirements.txt
|
||||
python -m pip install -r requirements.txt
|
||||
```
|
||||
|
||||
Restart ComfyUI after installation.
|
||||
Restart ComfyUI afterwards. Requirements: a recent ComfyUI (tested with 0.41) and Python 3.10+. **SAM3 Segment** and
|
||||
**Body Segment** have more requirements, listed below.
|
||||
|
||||
---
|
||||
### Optional: VTON and segmentation models
|
||||
|
||||
## Featured Workflows
|
||||
- **VTON Ultimate Processor** needs ONNX Runtime, which is not installed automatically (the CPU wheel would overwrite an
|
||||
existing GPU build). Install one of them into ComfyUI's Python:
|
||||
`python -m pip install onnxruntime-gpu` (NVIDIA) or `python -m pip install onnxruntime` (CPU).
|
||||
- **Garment Segment**, **Portrait Segment**, **Body Segment** and **VTON** download their models from Hugging Face on
|
||||
first use (pinned revisions, only the needed files): about 256 MB for Garment Segment, and 1.6 GB for Body Segment
|
||||
(3.3 GB and 5.9 GB for its larger sizes). Garment Segment, Portrait Segment and Body Segment also fetch YuNet, a
|
||||
230 KB face detector (MIT) that runs on the CPU. **Body Segment needs PyTorch 2.4 or newer.** To keep models
|
||||
elsewhere, add these keys to `extra_model_paths.yaml`:
|
||||
|
||||
### Tile, Process, Untile
|
||||
```yaml
|
||||
my_models:
|
||||
base_path: D:/ai-models/
|
||||
nh_nodes: NH-Nodes/ # garment, face and body-part models, and the YuNet face detector
|
||||
vton_mask: ComfyUI-Vton-Mask/ # VTON pose + parsing ONNX models
|
||||
sam3: sam3/ # SAM 3 checkpoint (you download sam3.pt yourself)
|
||||
```
|
||||
- **Image folders outside ComfyUI.** Load Images Folder, Load images matching and Save Image Path work inside
|
||||
ComfyUI's `input`, `output` and `temp` folders (saving: `output` only). To use a photo drive or a project folder,
|
||||
list it in the same file and restart ComfyUI
|
||||
([details](docs/en/image-io.md#folders-outside-comfyui)):
|
||||
|
||||
Use this when an image is too large for a model, or when every tile needs the
|
||||
same enhancement/upscale step.
|
||||
```yaml
|
||||
my_images:
|
||||
nh_image_folders: |
|
||||
D:/Photos/
|
||||
E:/Exports/
|
||||
```
|
||||
- **Requirements for SAM 3** (SAM3 Segment only): an NVIDIA GPU with CUDA, Meta's `sam3` package and the `sam3.pt`
|
||||
checkpoint. NH-Nodes installs and downloads none of them: you install the package yourself and request the weights
|
||||
from Meta. The steps, and what to do if another SAM 3 pack is already installed, are in
|
||||
[Before you start with SAM 3](docs/en/vision.md#before-you-start-with-sam-3).
|
||||
|
||||
```text
|
||||
Load Image -> Image Tile (NH) -> upscale/process tiles -> Image Untile (NH)
|
||||
```
|
||||
## Quick start
|
||||
|
||||
`Image Tile (NH)` supports:
|
||||
| Goal | Chain | Guide |
|
||||
|---|---|---|
|
||||
| Upscale or refine a huge image piece by piece | Load Image → **Image Tile** → your sampler/upscaler per tile → **Image Untile** | [image-compose](docs/en/image-compose.md#image-tile-nh) |
|
||||
| Generate at the model's native size, then restore the original framing | Load Image → **NH Smart Ratio Image Resize** → edit → **NH Restore Image Geometry** | [resolution](docs/en/resolution.md) |
|
||||
| Inpaint only the clothes | Load Image → **Garment Segment** → **Mask Morphology** (grow) → inpaint | [vision](docs/en/vision.md) |
|
||||
| Mask the skin, hair, lips, teeth or clothes of the people in a photo by name | Load Image → **Body Segment** (`regions`: `skin`, `hair, face` or `clothes - shoes`; Vietnamese works: `da`, `tóc, môi`) → mask → retouch or inpaint | [vision](docs/en/vision.md#body-segment-nh) |
|
||||
| Mask every person, window or the sky in a large photo, or everything except the person | Load Image → **SAM3 Segment** (`concepts`: `window`, `person - bag` or `background`) → mask → edit or inpaint; add **Segs Select** to pick single instances | [vision](docs/en/vision.md#sam-3-workflows) |
|
||||
| Compare many LoRAs, one per queue | **Counter** → **Load LoRA Model Index** (index) → sampler → **Image Label** (prints the LoRA name) → Save Image | [loaders](docs/en/loaders.md) |
|
||||
| Run one of two branches without executing the other | condition → **If/Else** (lazy) → … | [logic](docs/en/logic.md#ifelse-nh) |
|
||||
|
||||
- `original_ratio`: derive tile dimensions from the original image aspect ratio.
|
||||
- `custom`: use explicit tile width and height.
|
||||
- `square`: force square tiles, for example `1024 x 1024`.
|
||||
- Automatic overlap calculation.
|
||||
- Edge padding so square/custom tile outputs keep the requested tile size.
|
||||
## Node catalog
|
||||
|
||||
`Image Untile (NH)` uses `tile_data` from the tile node to stitch results back.
|
||||
It also handles tiles that were resized or upscaled, such as 2x or 3x per tile.
|
||||
| Group | Nodes | Guide |
|
||||
|---|---|---|
|
||||
| Image — load & save | Load Image Info, Load Images Folder, Load images matching, Save Image Path, NH Save Large Image | [EN](docs/en/image-io.md) · [VI](docs/vi/image-io.md) |
|
||||
| Image — compose | Image Tile, Image Untile, Image Grid Composite, Layer Layout Composite, Layer Stack Composite, Image Compare, Image Label, Image Resize Unit | [EN](docs/en/image-compose.md) · [VI](docs/vi/image-compose.md) |
|
||||
| Image — large preview | NH Large Image Preview, NH Large Image Compare, NH Large Preview Cache Manager | [EN](docs/en/image-preview.md) · [VI](docs/vi/image-preview.md) |
|
||||
| Image — mask-based retouch | NH Mask-Aware Resize, NH Agnostic Image Generator, NH Simple Face Paste | [EN](docs/en/image-retouch.md) · [VI](docs/vi/image-retouch.md) |
|
||||
| Resolution | NH Smart Resolution Picker, NH Smart Ratio Image Resize, NH Ratio Preset Image Resize, NH Remove Resize Padding, NH Restore Image Geometry | [EN](docs/en/resolution.md) · [VI](docs/vi/resolution.md) |
|
||||
| Mask | Mask Morphology, Mask Properties, NH Create Box Mask, NH Mask Aspect Ratio Match | [EN](docs/en/mask.md) · [VI](docs/vi/mask.md) |
|
||||
| Vision | Garment Segment, Portrait Segment, Body Segment, SAM3 Segment, Segs Select, Segs Compose, Segs To Masks, Segs To Impact SEGS, Segs Preview | [EN](docs/en/vision.md) · [VI](docs/vi/vision.md) |
|
||||
| VTON | VTON Ultimate Processor | [EN](docs/en/vton.md) · [VI](docs/vi/vton.md) |
|
||||
| Logic | Compare, Logic Gate, If/Else, Switch N, Any Switch, Any Branch Switch, Gate Switch, Value Match Index, Math Eval, Random Choice | [EN](docs/en/logic.md) · [VI](docs/vi/logic.md) |
|
||||
| Text | Prompt Template, Prompt Scheduler, String Operations, Prompt Join, Text Split, Regex Extract, Text Index, Text Concatenate, Text Split Lines, Text Random Line | [EN](docs/en/text.md) · [VI](docs/vi/text.md) |
|
||||
| Batch & lists | Batch Index, Batch Merge, Counter, List Create, List Index, List Filter, Any List Split | [EN](docs/en/batch-lists.md) · [VI](docs/vi/batch-lists.md) |
|
||||
| Loaders | Load LoRA Model Index, Load LoRA Clip Index, Load Diffusion Model Index, Load VAE Index, Load Clip Index | [EN](docs/en/loaders.md) · [VI](docs/vi/loaders.md) |
|
||||
| Utils | Multi-Slider (FLOAT/INT), Boolean Switch, Pack Universal, Unpack Universal, Universal Slider Builder *(deprecated)* | [EN](docs/en/utils.md) · [VI](docs/vi/utils.md) |
|
||||
|
||||
### Grid Contact Sheet
|
||||
All nodes live under **NH-Nodes** in the node menu; search by name or by node id (for example `NH_ImageTile`).
|
||||
|
||||
```text
|
||||
Image batch -> Image Grid Composite (NH) -> Preview Image
|
||||
```
|
||||
## Documentation
|
||||
|
||||
Use manual cell size or first-image cell size. Resize modes include `stretch`,
|
||||
`pad`, and `fill`.
|
||||
|
||||
### Metadata-Aware Image Load
|
||||
|
||||
```text
|
||||
Load Image Info (NH) -> image / mask / file_name / path
|
||||
```
|
||||
|
||||
Useful for naming outputs, logging source images, or routing by filename.
|
||||
|
||||
### Smart Resolution Resize
|
||||
|
||||
```text
|
||||
Load Image -> NH Smart Ratio Image Resize -> (your pipeline) -> NH Remove Resize Padding
|
||||
```
|
||||
|
||||
`NH Smart Ratio Image Resize` picks the closest model preset itself from
|
||||
`model_family` + `resolution_level`, so it does not need `NH Smart Resolution
|
||||
Picker` upstream. Resize with cover crop, contain pad, or stretch behavior.
|
||||
|
||||
In `contain_pad` mode it also emits `resize_data` describing the content region,
|
||||
padding, and scale factor. Feed that into `NH Remove Resize Padding` at the end
|
||||
of the pipeline to crop the padded border back off (`content_region`) or to
|
||||
restore the original input size (`original_size`).
|
||||
|
||||
For `cover_crop`, use `NH Restore Image Geometry` after processing. Connect the
|
||||
processed image to `image`, the source image to `original_image`, and the resize
|
||||
node's `resize_data` to the matching input. It returns an image at the exact
|
||||
original dimensions, with processed content placed back in its original region
|
||||
and cropped-away borders retained from the source. Its `mask` is white in the
|
||||
restored region and black in the retained borders; use it for masked blending
|
||||
with the original, optionally softening the mask edges first.
|
||||
|
||||
Processed images may be upscaled, including by a non-integer factor, provided
|
||||
their framing is unchanged. Additional crops, rotations, or model-generated
|
||||
changes in geometry cannot be reversed by this metadata. The restore node also
|
||||
accepts `contain_pad` and `stretch` metadata and supports matching batches or
|
||||
singleton batch broadcasting. Restoration uses bicubic (default), bilinear, or
|
||||
nearest sampling; it does not recover details lost during downsampling.
|
||||
|
||||
Use `NH Smart Resolution Picker` when you need target dimensions and a latent
|
||||
for text-to-image, instead of resizing an existing image.
|
||||
|
||||
### Garment and Portrait Segmentation
|
||||
|
||||
```text
|
||||
Load Image -> Garment Segment (NH) [part=upper_garment] -> mask / cutout_rgba / mask_image
|
||||
Load Image -> Portrait Segment (NH) [hair + face] -> mask / cutout_rgba / mask_image
|
||||
```
|
||||
|
||||
SegFormer-based masks at input resolution. `cutout_rgba` is the input image
|
||||
with the mask as alpha (RGB is left untouched outside the mask); `mask_image`
|
||||
is the mask as an RGB preview. Dresses and jumpsuits belong to `upper_garment`. Both nodes share `mask_expand`, `mask_blur`, and `fill_holes`.
|
||||
Choose `part=all_garments` to combine upper/lower garments, footwear and headwear
|
||||
in one mask, for example a shirt and pants together. `attach_parts` still controls
|
||||
whether touching pockets, zippers and other garment decorations are included.
|
||||
Models are auto-downloaded to `models/NH-Nodes/` on first use (existing
|
||||
`models/face_parsing` or `models/RMBG/segformer_fashion` copies are reused).
|
||||
|
||||
---
|
||||
|
||||
## Node Catalog
|
||||
|
||||
### Image Loading and Saving
|
||||
|
||||
| Node | Purpose |
|
||||
|---|---|
|
||||
| `Load Image Info (NH)` | Load one image and output `IMAGE`, `MASK`, file name, and path. |
|
||||
| `Save Image Path (NH)` | Save image batches to a chosen folder and output saved filenames/path/count. |
|
||||
| `NH Save Large Image` | Save full-resolution images without attaching heavy full-res previews to the canvas. |
|
||||
| `Load Images Folder (NH)` | Load images from a folder by index/step, with filename and path outputs. |
|
||||
| `Load images matching` | Load source/matching images by text, folder context, and matching rules. |
|
||||
|
||||
### Image Tiling, Grid, and Composition
|
||||
|
||||
| Node | Purpose |
|
||||
|---|---|
|
||||
| `Image Tile (NH)` | Split image batches into overlapping tiles with metadata for reconstruction. |
|
||||
| `Image Untile (NH)` | Rebuild full images from processed/upscaled tiles and tile metadata. |
|
||||
| `Image Grid Composite (NH)` | Combine an image batch into a configurable grid. |
|
||||
| `NH Large Image Preview` | Preview large images with lightweight canvas thumbnails and passthrough output. |
|
||||
| `NH Large Image Compare` | Compare large A/B images with lightweight canvas proxy and tiled slider/side/difference viewer. |
|
||||
| `NH Large Preview Cache Manager` | Inspect and clean the NH large preview tile cache by age, size, or cache id. |
|
||||
| `Layer Layout Composite (NH)` | Place layers into preset, grid, or custom layout slots. |
|
||||
| `Layer Stack Composite (NH)` | Paste multiple layers onto a background using explicit coordinates. |
|
||||
| `Image Resize Unit (NH)` | Resize by pixel/mm/cm using stretch, pad, crop, or lock-ratio modes. |
|
||||
| `Image Compare (NH)` | Compare two images side-by-side, top/bottom, split, difference, or overlay. |
|
||||
| `Image Label (NH)` | Add header/bottom labels with font, padding, color, and outline controls. |
|
||||
| `NH Agnostic Image Generator` | Create agnostic/masked/composite images for inpaint and VTON workflows. |
|
||||
| `NH Simple Face Paste` | Paste a source face region into a target using mask feathering. |
|
||||
|
||||
### Smart Resolution
|
||||
|
||||
| Node | Purpose |
|
||||
|---|---|
|
||||
| `NH Smart Resolution Picker` | Pick model/preset-aware dimensions and latent setup. |
|
||||
| `NH Smart Ratio Image Resize` | Auto-pick the closest model preset for the source aspect, then resize with cover, contain, or stretch; outputs `resize_data` for padding removal. |
|
||||
| `NH Ratio Preset Image Resize` | Resize to a chosen model preset ratio with crop, pad, or fill behavior. |
|
||||
| `NH Remove Resize Padding` | Remove the padded border from processed images using resize metadata. |
|
||||
| `NH Restore Image Geometry` | Restore processed or upscaled images to original coordinates with a blend mask; keep source pixels outside the crop. |
|
||||
|
||||
### Vision Segmentation
|
||||
|
||||
| Node | Purpose |
|
||||
|---|---|
|
||||
| `Garment Segment (NH)` | Mask `upper_garment` (tops, jackets, coats, dresses, ties, scarves, collars, sleeves), `lower_garment` (pants, shorts, skirts, belts, tights; no shoes), `footwear`, `headwear`, or combine all four with `all_garments`. Attached parts (pockets, zippers, bows...) merge only when touching the selected garments. |
|
||||
| `Portrait Segment (NH)` | Checklist mask of face, neck, hair, eyes, lips, nose, eyebrows, ears (+ optional mouth interior). `face` is the solid face region including features. |
|
||||
|
||||
### Mask Tools
|
||||
|
||||
| Node | Purpose |
|
||||
|---|---|
|
||||
| `Mask Morphology (NH)` | Expand/shrink masks, fill holes, and blur edges. |
|
||||
| `Mask Properties (NH)` | Output mask bounding box, dimensions, and related measurements. |
|
||||
| `NH Create Box Mask` | Convert a mask area into a rectangular box mask. |
|
||||
| `NH Mask Aspect Ratio Match` | Adjust one mask to match another mask's aspect ratio. |
|
||||
| `NH Mask-Aware Resize` | Resize an image/mask pair while preserving the masked subject region. |
|
||||
|
||||
### Logic and Flow Control
|
||||
|
||||
| Node | Purpose |
|
||||
|---|---|
|
||||
| `Compare (NH)` | Compare values with typed operators. |
|
||||
| `Logic Gate (NH)` | AND, OR, NOT, XOR, NAND, and NOR boolean logic. |
|
||||
| `If/Else (NH)` | Select between two inputs by condition. |
|
||||
| `Switch N (NH)` | Pick one of several generic inputs by index. |
|
||||
| `Any Switch (NH)` | Boolean switch helper. |
|
||||
| `Any Branch Switch (NH)` | Route generic data through selected branches. |
|
||||
| `Gate Switch (NH)` | Pass or block a generic input by boolean state. |
|
||||
| `Value Match Index (NH)` | Return index of matching configured value. |
|
||||
| `Math Eval (NH)` | Safe math expression evaluator with numeric/string outputs. |
|
||||
| `Random Choice (NH)` | Weighted random choice from generic inputs. |
|
||||
|
||||
### Text and Prompt Tools
|
||||
|
||||
| Node | Purpose |
|
||||
|---|---|
|
||||
| `String Operations (NH)` | Strip, case convert, replace, slice, inspect, and test strings. |
|
||||
| `Prompt Join (NH)` | Join text fragments with separators and empty-line handling. |
|
||||
| `Text Split (NH)` | Split text by delimiter and expose count/first/last values. |
|
||||
| `Regex Extract (NH)` | Extract, replace, split, and match with regex patterns. |
|
||||
| `Text Index (NH)` | Select a line/item from text by index. |
|
||||
| `Text Concatenate (NH)` | Concatenate multiple text inputs. |
|
||||
| `Text Split Lines (NH)` | Split multiline text into selected parts and metadata. |
|
||||
| `Text Random Line (NH)` | Pick a random line from multiline text. |
|
||||
| `Prompt Template (NH)` | Fill `{placeholder}` variables from connected text values. |
|
||||
| `Prompt Scheduler (NH)` | Select prompts by step, sequence, ping-pong, or seed. |
|
||||
|
||||
### Lists and Batch Helpers
|
||||
|
||||
| Node | Purpose |
|
||||
|---|---|
|
||||
| `List Create (NH)` | Build an `NH_LIST` from multiline or delimited text. |
|
||||
| `List Index (NH)` | Select an item from an `NH_LIST`. |
|
||||
| `List Filter (NH)` | Filter lists with contains, regex, equals, length, and related conditions. |
|
||||
| `Batch Index (NH)` | Extract one or more images from an image batch. |
|
||||
| `Batch Merge (NH)` | Merge image batches with resize handling. |
|
||||
| `Counter (NH)` | Stateful counter for repeated queue runs. |
|
||||
| `Any List Split (NH)` | Split any incoming list into an `All` output plus up to 10 indexed outputs. |
|
||||
|
||||
### Indexed Loaders
|
||||
|
||||
| Node | Purpose |
|
||||
|---|---|
|
||||
| `Load LoRA Model Index (NH)` | Apply one selected LoRA to `MODEL` by 1-based index. |
|
||||
| `Load LoRA Clip Index (NH)` | Apply one selected LoRA to `CLIP` by 1-based index. |
|
||||
| `Load Diffusion Model Index (NH)` | Load one diffusion model by 1-based index. |
|
||||
| `Load VAE Index (NH)` | Load one VAE by 1-based index or boolean toggle. |
|
||||
| `Load Clip Index (NH)` | Load one CLIP by 1-based index or boolean toggle, with CLIP type selection. |
|
||||
|
||||
### Workflow Utilities
|
||||
|
||||
| Node | Purpose |
|
||||
|---|---|
|
||||
| `Multi-Slider (FLOAT)` | Five float controls in one node. |
|
||||
| `Multi-Slider (INT)` | Five integer controls in one node. |
|
||||
| `Universal Slider Builder (NH)` | Generate dynamic slider outputs from text config. |
|
||||
| `Boolean Switch (NH)` | Simple boolean toggle. |
|
||||
| `Pack Universal (NH)` | Bundle generic values into an `NH_UNIVERSAL_PIPE`. |
|
||||
| `Unpack Universal (NH)` | Extract values from an `NH_UNIVERSAL_PIPE`. |
|
||||
|
||||
### VTON Preprocessing
|
||||
|
||||
| Node | Purpose |
|
||||
|---|---|
|
||||
| `VTON Ultimate Processor (NH)` | Generate masks, agnostic image data, DensePose/parsing outputs, hair/hands masks, and related VTON preprocessing assets. |
|
||||
|
||||
---
|
||||
|
||||
## Menu Structure
|
||||
|
||||
```text
|
||||
NH-Nodes/
|
||||
Image/
|
||||
Load/save helpers
|
||||
Tile/untile
|
||||
Grid and layer composition
|
||||
Resize, compare, label
|
||||
Mask/
|
||||
Morphology, properties, bbox, aspect matching
|
||||
Vision/
|
||||
Garment and portrait segmentation
|
||||
Logic/
|
||||
Compare, gates, switches, branching, math, random choice
|
||||
Text/
|
||||
String, regex, split, concatenate, templates, scheduling
|
||||
Batch/
|
||||
Image batch, list helpers, any-list split
|
||||
Resolution/
|
||||
Smart resolution picker, ratio resize, preset resize, padding removal
|
||||
Loaders/
|
||||
Indexed LoRA, diffusion model, VAE, and CLIP loaders
|
||||
Utils/
|
||||
Sliders, booleans, universal pipe
|
||||
VTON/
|
||||
VTON Ultimate Processor
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Requirements
|
||||
|
||||
```text
|
||||
numpy
|
||||
Pillow
|
||||
scipy
|
||||
opencv-python
|
||||
scikit-image>=0.18.0
|
||||
transformers>=4.20.0
|
||||
tqdm>=4.62.0
|
||||
huggingface_hub>=0.16.0
|
||||
```
|
||||
|
||||
`VTON Ultimate Processor (NH)` also needs ONNX Runtime, which is not installed
|
||||
automatically (the CPU wheel would overwrite an existing GPU build). Install one:
|
||||
`python -m pip install onnxruntime-gpu` (NVIDIA) or `python -m pip install onnxruntime` (CPU).
|
||||
|
||||
Most image/text/logic nodes use common ComfyUI dependencies. VTON and Vision
|
||||
nodes download model assets on first use (`transformers` SegFormer weights for
|
||||
Vision: ~190 MB fashion, ~340 MB face).
|
||||
|
||||
---
|
||||
|
||||
## Development Notes
|
||||
|
||||
- Layout: `core/` holds shared helpers and model backends (no nodes); `nodes/<category>/`
|
||||
mirrors the ComfyUI menu, and every module there that defines `NODE_CLASS_MAPPINGS`
|
||||
is registered automatically by `__init__.py` - drop a file in the right folder
|
||||
and restart. VTON's vendored preprocessing lives in `nodes/vton/preprocess/`.
|
||||
- Tests: `python -m pytest custom_nodes/NH-Nodes/tests -q` from the ComfyUI root.
|
||||
- Restart ComfyUI after installing or updating nodes.
|
||||
- If a node appears cached in the browser, refresh the ComfyUI page after restart.
|
||||
- `Image Tile (NH)` and `Image Untile (NH)` must pass `tile_data` directly through
|
||||
the workflow so reconstruction uses the original tile layout.
|
||||
|
||||
---
|
||||
- **Guide:** [English](docs/en/README.md) · [Tiếng Việt](docs/vi/README.md)
|
||||
- **PDF guides:** [English](https://github.com/jetthuangai/NH-Nodes/releases/latest/download/NH-Nodes-Guide-EN.pdf) ·
|
||||
[Tiếng Việt](https://github.com/jetthuangai/NH-Nodes/releases/latest/download/NH-Nodes-Huong-dan-VI.pdf)
|
||||
- **Changelog:** [docs/CHANGELOG.md](docs/CHANGELOG.md) — read it when updating: some fixes change outputs on purpose.
|
||||
|
||||
## License
|
||||
|
||||
NH-Nodes' own code is [MIT](LICENSE). `VTON Ultimate Processor (NH)` and
|
||||
`Portrait Segment (NH)` depend on third-party code or models that are licensed for
|
||||
**non-commercial use only**, and the model behind `Garment Segment (NH)` declares no
|
||||
license. See [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md) before using these
|
||||
three nodes commercially.
|
||||
NH-Nodes' own code is [MIT](LICENSE). Some features rely on third-party code or models with other terms — see
|
||||
[THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md):
|
||||
|
||||
If NH-Nodes helps your workflow, consider starring the repository:
|
||||
[github.com/jetthuangai/NH-Nodes](https://github.com/jetthuangai/NH-Nodes)
|
||||
- **VTON Ultimate Processor** and **Portrait Segment** are for **non-commercial use only** (CC BY-NC-SA 4.0 code and
|
||||
parsing models; non-commercial face-parsing model).
|
||||
- **Garment Segment** is for **non-commercial use only, and NVIDIA's licence defines that as research or evaluation
|
||||
purposes only**: its default model (FASHN Human Parser) is under the NVIDIA Source Code License for SegFormer. The
|
||||
other model, `fashionpedia`, declares no licence but is fine-tuned from NVIDIA's `mit-b3`, so treat it the same way.
|
||||
- **Body Segment** uses Meta's Sapiens2 under the Sapiens2 License: royalty-free, with no non-commercial clause, but
|
||||
it prohibits surveillance, biometric processing, identifying people, deepfakes and other uses, requires you to follow
|
||||
privacy, biometric and trade-control laws, and lets Meta audit your use, end the licence and change its terms. The
|
||||
model code is included with its licence; the weights are downloaded from Meta's Hugging Face repositories.
|
||||
- **SAM3 Segment** uses Meta's SAM 3 under the SAM License: royalty-free, with no non-commercial clause, but it bars
|
||||
military or warfare, nuclear, espionage, and gun or illegal-weapon uses. The weights and the `sam3` package are not
|
||||
included; you get them from Meta.
|
||||
|
||||
## Development
|
||||
|
||||
- Layout: `core/` holds shared helpers and model backends (no nodes); `nodes/<category>/` mirrors the menu, and every
|
||||
module there that defines `NODE_CLASS_MAPPINGS` is registered automatically; `web/` holds the frontend extensions.
|
||||
- Tests and lint (run from the ComfyUI root / the pack folder):
|
||||
|
||||
```bash
|
||||
python -m pytest custom_nodes/NH-Nodes/tests -q
|
||||
ruff check .
|
||||
```
|
||||
- Docs assets are generated from the live code: `docs/_tools/build_node_cards.py` (node cards) and
|
||||
`docs/_tools/build_examples.py` (example images), both run from the ComfyUI root.
|
||||
|
||||
## Author
|
||||
|
||||
[jetthuang.com](https://jetthuang.com) · [Hugging Face](https://huggingface.co/nhathoangfoto) ·
|
||||
[Support via PayPal](https://paypal.me/nhathoangfoto). If NH-Nodes helps your work, a ⭐ on GitHub is appreciated.
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
NH-Nodes' own code is released under the [MIT License](LICENSE). The components
|
||||
listed here come from other projects and keep their own licenses. Some of them are
|
||||
**non-commercial**, so the features that depend on them are not covered by the
|
||||
MIT license.
|
||||
**non-commercial** and others restrict how they may be used, so the features that
|
||||
depend on them are not covered by the MIT license.
|
||||
|
||||
> This is a good-faith summary, not legal advice. Check the upstream licenses
|
||||
> yourself before using these features commercially.
|
||||
@@ -15,7 +15,9 @@ MIT license.
|
||||
| Every node not listed below | Yes, under MIT | Original NH-Nodes code |
|
||||
| `VTON Ultimate Processor (NH)` | **No** | Its code and parsing models are CC BY-NC-SA 4.0 (FitDiT / OOTDiffusion) |
|
||||
| `Portrait Segment (NH)` | **No** | The face-parsing model card restricts use to non-commercial research and education |
|
||||
| `Garment Segment (NH)` | Unclear | The model uploader does not declare a license |
|
||||
| `Garment Segment (NH)` | **No** | Non-commercial, which its license defines as research or evaluation only (see below). The default model, FASHN Human Parser, is under the NVIDIA Source Code License for SegFormer. The `fashionpedia` model declares no license, but it is fine-tuned from `nvidia/mit-b3`, so the same license plausibly applies |
|
||||
| `Body Segment (NH)` | Yes, with use restrictions | Meta's Sapiens2 License is royalty-free and has no non-commercial clause, but it prohibits surveillance, biometric processing, identifying people, deepfakes and other uses (listed below), requires you to follow privacy, biometric and trade-control laws, and lets Meta audit your use, end the license and change its terms |
|
||||
| `SAM3 Segment (NH)` | Yes, within trade-control limits | Meta's SAM License is royalty-free and has no non-commercial clause, but it forbids ITAR-regulated activities and uses that trade controls prohibit (military or warfare, nuclear, espionage, guns and illegal weapons). The model and the `sam3` package are not part of NH-Nodes: you get them from Meta |
|
||||
|
||||
## Source code bundled in this repository
|
||||
|
||||
@@ -26,6 +28,8 @@ MIT license.
|
||||
| `core/utils_mask.py` | FitDiT `src/utils_mask.py`, received via ComfyUI-Vton-Mask | CC BY-NC-SA 4.0 |
|
||||
| `nodes/vton/vton_preprocessor_nodes.py` | NH-Nodes node built on ComfyUI-Vton-Mask's mask generator. That project's `LICENSE` is MIT and says it is based on FitDiT; its README says it follows FitDiT's licensing terms. | Treat as CC BY-NC-SA 4.0 together with the code above |
|
||||
| `core/utils.py` (`resize_image`, `tensor_to_pil`, `pil_to_tensor`) | Small helpers adapted from ComfyUI-Vton-Mask `utils.py` | MIT |
|
||||
| `core/sapiens2/` | Meta's [Sapiens2](https://github.com/facebookresearch/sapiens2) `sapiens/backbones/standalone/sapiens2.py` at commit `7e5bae8`, kept as `backbone.py` with a `Modified by NH-Nodes` header line | Sapiens2 License — the copy is [`core/sapiens2/LICENSE.md`](core/sapiens2/LICENSE.md) |
|
||||
| `core/sapiens2_backend.py` (`SegHead`) | NH-Nodes' inference version of Meta's `SegHead` (`sapiens/dense/src/models/heads/seg_head.py` at the same commit): the same layers and layer names, so it loads Meta's released weights | Treated like the file above: Sapiens2 License |
|
||||
|
||||
Every file NH-Nodes changed carries a `Modified by NH-Nodes` header line (for
|
||||
example, ONNX Runtime sessions in the vendored code are created through
|
||||
@@ -40,6 +44,7 @@ example, ONNX Runtime sessions in the vendored code are created through
|
||||
| [`licenses/Self-Correction-Human-Parsing-MIT.txt`](licenses/Self-Correction-Human-Parsing-MIT.txt) | MIT, Peike Li |
|
||||
| [`licenses/Microsoft-human-pose-estimation-MIT.txt`](licenses/Microsoft-human-pose-estimation-MIT.txt) | MIT, Microsoft (HRNet transforms) |
|
||||
| [`licenses/ComfyUI-Vton-Mask-MIT.txt`](licenses/ComfyUI-Vton-Mask-MIT.txt) | MIT, ComfyUI-Vton-Mask (`core/utils.py` helpers) |
|
||||
| [`core/sapiens2/LICENSE.md`](core/sapiens2/LICENSE.md) | Sapiens2 License (Meta): the bundled Sapiens2 model code and the Sapiens2 weights that Body Segment downloads |
|
||||
|
||||
## Models downloaded at runtime
|
||||
|
||||
@@ -53,10 +58,121 @@ They are looked up first in the folders registered as `vton_mask` and `nh_nodes`
|
||||
| VTON Ultimate Processor | [`kg-09/kg-vton-mask`](https://huggingface.co/kg-09/kg-vton-mask) @ `4648778` | `dwpose/yolox_l.onnx`, `dwpose/dw-ll_ucoco_384.onnx` | DWPose / YOLOX weights, Apache-2.0 upstream |
|
||||
| VTON Ultimate Processor | same | `humanparsing/parsing_atr.onnx`, `humanparsing/parsing_lip.onnx` | From the OOTDiffusion/FitDiT checkpoints (CC BY-NC-SA 4.0); trained on the ATR and LIP datasets, which are distributed for research use |
|
||||
| Portrait Segment | [`jonathandinu/face-parsing`](https://huggingface.co/jonathandinu/face-parsing) @ `758b82e` | SegFormer fine-tuned on CelebAMask-HQ | Model card: "non-commercial research and educational purposes" |
|
||||
| Garment Segment | [`1038lab/segformer_fashion`](https://huggingface.co/1038lab/segformer_fashion) @ `e5bae2d` | SegFormer trained for Fashionpedia classes | No license declared by the uploader (Fashionpedia annotations are CC BY 4.0) |
|
||||
| Garment Segment (`model`: `fashn`, the default) | [`fashn-ai/fashn-human-parser`](https://huggingface.co/fashn-ai/fashn-human-parser) @ `1f80c34` | SegFormer-B4 fine-tuned from `nvidia/mit-b4` for 18 human-parsing classes (about 256 MB) | NVIDIA Source Code License for SegFormer: research or evaluation only (see below). Model card: "This model inherits the NVIDIA Source Code License for SegFormer." |
|
||||
| Garment Segment (`model`: `fashionpedia`) | [`1038lab/segformer_fashion`](https://huggingface.co/1038lab/segformer_fashion) @ `e5bae2d` | SegFormer trained for Fashionpedia classes (about 190 MB); its `config.json` records `_name_or_path: nvidia/mit-b3` | No license declared by the uploader (Fashionpedia annotations are CC BY 4.0). The NVIDIA license plausibly applies, so treat it as research or evaluation only (see below) |
|
||||
| Body Segment | [`facebook/sapiens2-seg-0.4b`](https://huggingface.co/facebook/sapiens2-seg-0.4b) @ `449b3c5`, [`-0.8b`](https://huggingface.co/facebook/sapiens2-seg-0.8b) @ `196a627`, [`-1b`](https://huggingface.co/facebook/sapiens2-seg-1b) @ `60cf0b3` | `sapiens2_<size>_seg.safetensors` (1.6 / 3.3 / 5.9 GB): Sapiens2 body-part segmentation, 28 classes plus background | Sapiens2 License (see below) |
|
||||
| Portrait Segment, Garment Segment, Body Segment | [`opencv/face_detection_yunet`](https://huggingface.co/opencv/face_detection_yunet) @ `3cc26e7` | `yunet/face_detection_yunet_2023mar.onnx`, the YuNet face detector (about 230 KB), and its `LICENSE` file | MIT, Copyright (c) 2020 Shiqi Yu (OpenCV Zoo) |
|
||||
|
||||
YuNet is the face detector that Portrait Segment, Garment Segment and Body Segment use to find the
|
||||
people in a picture. It runs on the CPU through OpenCV, and its MIT license does not change the "At a
|
||||
glance" table: the terms of those nodes come from their models.
|
||||
|
||||
### NVIDIA Source Code License for SegFormer (Garment Segment)
|
||||
|
||||
Garment Segment's default model, FASHN Human Parser, is a SegFormer-B4 fine-tuned from
|
||||
`nvidia/mit-b4`. Its [model card](https://huggingface.co/fashn-ai/fashn-human-parser) says: "This
|
||||
model inherits the NVIDIA Source Code License for SegFormer." Section 3.3 of that
|
||||
[license](https://github.com/NVlabs/SegFormer/blob/master/LICENSE) reads:
|
||||
|
||||
> The Work and any derivative works thereof only may be used or intended for use non-commercially.
|
||||
> Notwithstanding the foregoing, NVIDIA and its affiliates may use the Work and any derivative works
|
||||
> commercially. As used herein, "non-commercially" means for research or evaluation purposes only.
|
||||
|
||||
This is narrower than the everyday meaning of "non-commercial": the license allows research or
|
||||
evaluation, and nothing else. NH-Nodes' MIT license does not change it.
|
||||
|
||||
The `fashionpedia` model (`1038lab/segformer_fashion`) has no model card and its uploader declares no
|
||||
license, so there is nothing to quote. Its `config.json` records `_name_or_path: nvidia/mit-b3`,
|
||||
which points to a fine-tune of NVIDIA's SegFormer encoder, so the same license plausibly applies.
|
||||
Treat it as research or evaluation only too.
|
||||
|
||||
### Sapiens2 License (Body Segment)
|
||||
|
||||
Body Segment runs Meta's Sapiens2 (April 2026). NH-Nodes bundles the model code with the license
|
||||
text (`core/sapiens2/`) and downloads the weights from Meta's repositories on first use. It does not
|
||||
redistribute the weights. The [Sapiens2 License](https://github.com/facebookresearch/sapiens2/blob/main/LICENSE.md)
|
||||
(`core/sapiens2/LICENSE.md` is identical to Meta's file at commit `7e5bae8`) is a non-exclusive,
|
||||
worldwide, non-transferable, royalty-free limited license to use, reproduce, distribute, copy, create
|
||||
derivative works of and modify the "Sapiens Materials": Meta's models, code and trained weights. It
|
||||
has no non-commercial clause. You agree to it by using or distributing any part of the materials.
|
||||
What matters to a user of NH-Nodes:
|
||||
|
||||
- **Prohibited uses (section 1.b.vi).** Your use of the materials must not involve or encourage
|
||||
others to use them:
|
||||
- for purposes of surveillance, including any research or development relating to surveillance;
|
||||
- for biometric processing;
|
||||
- in any manner that infringes, misappropriates or otherwise violates third-party rights;
|
||||
- for attempting to identify or re-identify any individual included in the Sapiens Materials;
|
||||
- to create deepfakes, surveillance, or other misleading, deceptive or inappropriate content,
|
||||
including impersonating any individual or facilitating false online engagement;
|
||||
- to collect, process, disclose, generate or infer health, demographic or other sensitive personal
|
||||
or private information about individuals without the rights and consents that applicable laws
|
||||
require;
|
||||
- in a pornographic or defamatory manner;
|
||||
- to engage in the unauthorized or unlicensed practice of any profession, including but not
|
||||
limited to financial, legal, medical/health, or related professional practices;
|
||||
- to engage in, promote, incite, facilitate, or assist in the planning or development of any
|
||||
activities subject to the International Traffic in Arms Regulations (ITAR) or end uses prohibited
|
||||
by trade controls, or any other activities that present a risk of death or bodily harm to
|
||||
individuals, including those related to military or warfare purposes, nuclear industries or
|
||||
applications, espionage, the development or use of guns or illegal weapons or illegal drugs, and
|
||||
the operation of critical infrastructure or machinery.
|
||||
- **Laws (sections 1.b.iii and 1.b.v).** Your use must comply with applicable laws, including privacy,
|
||||
data protection and security laws: the EU General Data Protection Regulation, the California
|
||||
Consumer Privacy Act, the US data security regulations at 28 C.F.R. Part 202, Brazil's Lei Geral de
|
||||
Proteção de Dados, and any law governing the processing of biometric information. It must comply
|
||||
with trade controls too, and you must not be the target of trade controls.
|
||||
- **No reverse engineering (1.b.iv).** Your use must not involve or encourage others to reverse
|
||||
engineer, decompile or discover the underlying components of the materials.
|
||||
- **What Meta may do.** If Meta reasonably believes that you are or are likely to be in violation, it
|
||||
may audit your use, storage and distribution of the materials and require you to delete every copy
|
||||
(1.c). It may terminate the license if you breach it, and you must then delete the materials and
|
||||
stop using them (6). It may also change the terms from time to time, provided the changes are
|
||||
similar in spirit to the current ones: they take effect immediately, and continuing to use the
|
||||
materials counts as agreeing (8).
|
||||
- **Redistribution and publications (1.b.i and 1.b.ii).** Whoever distributes the materials, or
|
||||
derivative works of them, must do so under this license and include a copy of it. Research
|
||||
publications that use the materials must acknowledge it.
|
||||
- **Liability (2 to 5).** The materials and any output or results from them are provided "as is", and
|
||||
you are solely responsible for deciding whether using them is appropriate. If you start proceedings
|
||||
against Meta or any entity alleging that the materials or their outputs infringe your intellectual
|
||||
property, the license ends, and you must indemnify Meta against third-party claims that arise from
|
||||
your use or distribution of the materials.
|
||||
- **Governing law (7).** California law applies, and the courts of California have exclusive
|
||||
jurisdiction.
|
||||
|
||||
## Software and models you install yourself
|
||||
|
||||
`SAM3 Segment (NH)` runs Meta's SAM 3. NH-Nodes neither bundles nor downloads it, and the
|
||||
nodes only load it when you use them. You get both parts from Meta and accept Meta's terms
|
||||
yourself.
|
||||
|
||||
| What | Where it comes from | Terms |
|
||||
|------|---------------------|-------|
|
||||
| `sam3` package (code) | [`facebookresearch/sam3`](https://github.com/facebookresearch/sam3), installed by the user with pip | SAM License |
|
||||
| SAM 3 checkpoint `sam3.pt` | [`facebook/sam3`](https://huggingface.co/facebook/sam3) on Hugging Face, a gated repository: access is granted after the user accepts the license. It goes in the `sam3` model folder (`models/sam3`, or any location added under `sam3` in `extra_model_paths.yaml`), which other SAM 3 packs share | SAM License |
|
||||
|
||||
The [SAM License](https://github.com/facebookresearch/sam3/blob/main/LICENSE) (Meta, last
|
||||
updated 19 November 2025) is a non-exclusive, worldwide, non-transferable, royalty-free
|
||||
limited license to use, reproduce, distribute, copy, modify and create derivative works of
|
||||
the SAM Materials. It does not mention commercial use. What matters to a user of NH-Nodes:
|
||||
|
||||
- **Trade controls.** You must not be a target of trade controls and must comply with them.
|
||||
The materials may not be used for activities subject to the International Traffic in Arms
|
||||
Regulations (ITAR), or for end uses that trade controls prohibit, including military or
|
||||
warfare purposes, nuclear industries or applications, espionage, and the development or
|
||||
use of guns or illegal weapons.
|
||||
- Whoever redistributes the SAM Materials must include a copy of the license. NH-Nodes does
|
||||
not redistribute them.
|
||||
- Research publications that use the SAM Materials must acknowledge it.
|
||||
- The materials may not be used to reverse engineer or decompile them, nor to encourage
|
||||
others to.
|
||||
- They are provided "as is", and Meta may terminate the license if you breach it.
|
||||
|
||||
## Python dependencies
|
||||
|
||||
Dependencies listed in `requirements.txt` are installed from PyPI and are not
|
||||
redistributed here; each keeps its own license. ONNX Runtime (MIT) is optional
|
||||
and only needed by the VTON node.
|
||||
and only needed by the VTON node. The `sam3` package (SAM License) and its extras
|
||||
(`timm`, `ftfy`, `regex`, `iopath`) are optional too: only `SAM3 Segment (NH)` needs
|
||||
them, and you install them yourself.
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
"""Which folders the image nodes and routes of NH-Nodes may read and write.
|
||||
|
||||
A workflow is data: it can come from anyone, and ComfyUI can be reachable from the network. So the folder text of a
|
||||
node (or of a request) only works inside
|
||||
|
||||
* reading: ComfyUI's ``input``, ``output`` and ``temp`` folders,
|
||||
* writing: ComfyUI's ``output`` folder,
|
||||
|
||||
and the folders the owner of the machine listed on disk under ``nh_image_folders`` in ``extra_model_paths.yaml``
|
||||
(both reading and writing)::
|
||||
|
||||
nh_nodes_images:
|
||||
nh_image_folders: |
|
||||
D:/Photos/
|
||||
E:/Exports/
|
||||
|
||||
A path is checked twice. First as written (``..`` resolved, no disk access), so a path outside every allowed folder
|
||||
is refused before anything touches it: a ``\\\\server\\share`` path never makes Windows contact that server. Then
|
||||
with links and junctions resolved, so a link inside an allowed folder cannot lead out of it.
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
import folder_paths
|
||||
|
||||
FOLDER_KEY = "nh_image_folders"
|
||||
|
||||
_declared = []
|
||||
|
||||
|
||||
class FolderNotAllowed(PermissionError):
|
||||
"""The path lies outside every folder NH-Nodes may use. An OSError, so callers that skip unreadable paths skip it."""
|
||||
|
||||
|
||||
def _absorb_declared():
|
||||
# ComfyUI turns every key of extra_model_paths.yaml into a model folder name, and its /models routes list the
|
||||
# files of every such name. These folders hold photos, not models: take the locations and drop the name.
|
||||
entry = folder_paths.folder_names_and_paths.pop(FOLDER_KEY, None)
|
||||
if entry:
|
||||
for path in entry[0]:
|
||||
if path and path not in _declared:
|
||||
_declared.append(path)
|
||||
|
||||
|
||||
def declared_folders():
|
||||
"""The folders listed under ``nh_image_folders``; [] when the key is not used."""
|
||||
_absorb_declared()
|
||||
return list(_declared)
|
||||
|
||||
|
||||
def read_roots():
|
||||
return [
|
||||
folder_paths.get_input_directory(),
|
||||
folder_paths.get_output_directory(),
|
||||
folder_paths.get_temp_directory(),
|
||||
*declared_folders(),
|
||||
]
|
||||
|
||||
|
||||
def write_roots():
|
||||
return [folder_paths.get_output_directory(), *declared_folders()]
|
||||
|
||||
|
||||
def _contains(root, path):
|
||||
try:
|
||||
return os.path.commonpath([root, path]) == root
|
||||
except ValueError: # another drive, or a drive path against a network share
|
||||
return False
|
||||
|
||||
|
||||
def _written(path):
|
||||
return os.path.normcase(os.path.abspath(path))
|
||||
|
||||
|
||||
def _resolved(path):
|
||||
return os.path.normcase(os.path.realpath(path))
|
||||
|
||||
|
||||
def is_inside(path, roots):
|
||||
"""True when ``path`` is one of ``roots`` or below one, both as written and with links resolved."""
|
||||
roots = [root for root in roots if root]
|
||||
resolved_roots = [_resolved(root) for root in roots] # the roots are the owner's: resolving them is safe
|
||||
written = _written(path)
|
||||
# As written, the path must lie in a root under either of the root's spellings (a root that is itself a link
|
||||
# may be typed by its real location).
|
||||
if not any(_contains(root, written) for root in [*map(_written, roots), *resolved_roots]):
|
||||
return False
|
||||
resolved = _resolved(path)
|
||||
return any(_contains(root, resolved) for root in resolved_roots)
|
||||
|
||||
|
||||
def _refusal(path, what, action, defaults):
|
||||
return FolderNotAllowed(
|
||||
f"NH-Nodes: {what} is outside the folders NH-Nodes may {action}: {str(path)!r}. Allowed are {defaults} and "
|
||||
f"the folders listed under '{FOLDER_KEY}' in extra_model_paths.yaml. To allow another folder, add it there, "
|
||||
f"for example\n nh_nodes_images:\n {FOLDER_KEY}: D:/Photos/\nand restart ComfyUI."
|
||||
)
|
||||
|
||||
|
||||
def require_readable(path, what="The folder"):
|
||||
"""``path`` as an absolute path; FolderNotAllowed (naming ``what``) when NH-Nodes may not read there."""
|
||||
if not is_inside(path, read_roots()):
|
||||
raise _refusal(path, what, "read", "ComfyUI's input, output and temp folders")
|
||||
return os.path.abspath(path)
|
||||
|
||||
|
||||
def require_writable(path, what="The folder"):
|
||||
"""``path`` as an absolute path; FolderNotAllowed (naming ``what``) when NH-Nodes may not write there."""
|
||||
if not is_inside(path, write_roots()):
|
||||
raise _refusal(path, what, "write to", "ComfyUI's output folder")
|
||||
return os.path.abspath(path)
|
||||
|
||||
|
||||
def can_read(path):
|
||||
return is_inside(path, read_roots())
|
||||
|
||||
|
||||
_absorb_declared()
|
||||
@@ -0,0 +1,108 @@
|
||||
"""Face boxes for Portrait Segment, from YuNet (OpenCV Zoo, MIT license).
|
||||
|
||||
YuNet is a 230 KB ONNX model that runs on the CPU through the OpenCV the pack already needs, so finding faces costs
|
||||
no VRAM and no extra package. The weights are fetched once, pinned to a fixed Hugging Face revision, into the same
|
||||
``nh_nodes`` model folder as the parsers (models/NH-Nodes/yunet by default).
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
|
||||
from .model_files import download_pinned, register_model_folder
|
||||
from .segformer_backend import FOLDER_EXTENSIONS, FOLDER_NAME, default_folder
|
||||
|
||||
YUNET = {
|
||||
"repo_id": "opencv/face_detection_yunet",
|
||||
"revision": "3cc26e7f1014a5ee5d74a42acee58bafc9d0a310",
|
||||
"file": "face_detection_yunet_2023mar.onnx",
|
||||
"folder": "yunet",
|
||||
}
|
||||
MIN_SCORE = 0.6 # YuNet's own samples use 0.9; a box that is not a face is turned down later by the face parser
|
||||
DETECT_SIDES = (2048, 640) # faces are looked for on copies at most this long: small faces show on the first, big or
|
||||
# grainy ones (scanned prints, close-ups) are often found only on the second
|
||||
MAX_FACES = 32 # the largest ones; each face costs two parses
|
||||
|
||||
_detector = None
|
||||
|
||||
|
||||
def model_path(download=True):
|
||||
"""Path of the YuNet weights, from any registered ``nh_nodes`` folder; downloaded into the default one if absent."""
|
||||
for base in register_model_folder(FOLDER_NAME, default_folder(), FOLDER_EXTENSIONS):
|
||||
candidate = os.path.join(base, YUNET["folder"], YUNET["file"])
|
||||
if os.path.isfile(candidate):
|
||||
return candidate
|
||||
if not download:
|
||||
return None
|
||||
target = os.path.join(default_folder(), YUNET["folder"])
|
||||
download_pinned(YUNET["repo_id"], YUNET["revision"], target, (YUNET["file"], "LICENSE"))
|
||||
path = os.path.join(target, YUNET["file"])
|
||||
if not os.path.isfile(path):
|
||||
raise RuntimeError(f"[NH-Nodes] Face detector missing after download: {path}")
|
||||
return path
|
||||
|
||||
|
||||
def _get_detector(size):
|
||||
"""The YuNet detector, set up for pictures of ``size`` (width, height). Loaded from bytes read by Python, so a
|
||||
model folder whose path has accents (which OpenCV cannot open on Windows) works too."""
|
||||
global _detector
|
||||
if _detector is None:
|
||||
weights = np.fromfile(model_path(), dtype=np.uint8)
|
||||
_detector = cv2.FaceDetectorYN.create("onnx", weights, np.empty(0, np.uint8), size, MIN_SCORE, 0.3, 5000)
|
||||
_detector.setInputSize(size)
|
||||
return _detector
|
||||
|
||||
|
||||
def _detect_at(image, max_side, min_score):
|
||||
"""Face boxes found on a copy of ``image`` at most ``max_side`` pixels long, in the image's pixels."""
|
||||
height, width = image.shape[:2]
|
||||
scale = min(1.0, max_side / max(height, width))
|
||||
small = image
|
||||
if scale < 1.0:
|
||||
small = cv2.resize(image, (max(1, round(width * scale)), max(1, round(height * scale))),
|
||||
interpolation=cv2.INTER_AREA)
|
||||
detector = _get_detector((small.shape[1], small.shape[0]))
|
||||
_, found = detector.detect(cv2.cvtColor(np.ascontiguousarray(small), cv2.COLOR_RGB2BGR))
|
||||
faces = []
|
||||
for row in [] if found is None else found:
|
||||
score = float(row[-1])
|
||||
if score < min_score:
|
||||
continue
|
||||
x, y, w, h = (float(v) / scale for v in row[:4])
|
||||
box = (max(0.0, x), max(0.0, y), min(float(width), x + w), min(float(height), y + h))
|
||||
if box[2] > box[0] and box[3] > box[1]:
|
||||
faces.append((*box, score))
|
||||
return faces
|
||||
|
||||
|
||||
def _area(box):
|
||||
return max(0.0, box[2] - box[0]) * max(0.0, box[3] - box[1])
|
||||
|
||||
|
||||
def _overlap(a, b):
|
||||
return _area((max(a[0], b[0]), max(a[1], b[1]), min(a[2], b[2]), min(a[3], b[3])))
|
||||
|
||||
|
||||
def merge_faces(faces):
|
||||
"""One box per face from boxes found at several scales: best score first, a box is dropped when it overlaps a
|
||||
kept one (IoU 0.3 or more) or lies mostly (60%) inside one, as a half face found at one scale does."""
|
||||
kept = []
|
||||
for face in sorted(faces, key=lambda f: f[4], reverse=True):
|
||||
if all(_overlap(face, other) < 0.3 * (_area(face) + _area(other) - _overlap(face, other))
|
||||
and _overlap(face, other) < 0.6 * min(_area(face), _area(other)) for other in kept):
|
||||
kept.append(face)
|
||||
return kept
|
||||
|
||||
|
||||
def detect_faces(image, min_score=MIN_SCORE, sides=DETECT_SIDES, limit=MAX_FACES):
|
||||
"""[(x1, y1, x2, y2, score)] of the faces in the uint8 RGB ``image``, in its pixels, largest first.
|
||||
|
||||
Looks on copies of each length in ``sides`` and merges what they find (see merge_faces), which finds faces from
|
||||
about 1/150 of the picture's long side up to close-ups. A face cut by the frame is often missed; the caller can
|
||||
look again on a copy with a margin, and parses the whole frame when nothing is found.
|
||||
"""
|
||||
found = [face for side in sides for face in _detect_at(image, side, min_score)]
|
||||
faces = merge_faces(found)
|
||||
faces.sort(key=_area, reverse=True)
|
||||
return faces[:limit]
|
||||
@@ -12,6 +12,7 @@ import os
|
||||
|
||||
from aiohttp import web
|
||||
|
||||
from .allowed_folders import FolderNotAllowed
|
||||
from .image_folders import collect_image_files, resolve_load_dir
|
||||
|
||||
COUNT_PATH = "/nh-nodes/load-images-folder/count"
|
||||
@@ -51,6 +52,9 @@ async def nh_load_images_folder_count(request):
|
||||
count = await asyncio.to_thread(_count_images, folder_path, recursive)
|
||||
except FileNotFoundError:
|
||||
return _error(404, "folder not found")
|
||||
except FolderNotAllowed:
|
||||
# Outside ComfyUI's own folders and the folders listed under nh_image_folders: refused before the disk is read.
|
||||
return _error(403, "folder is not allowed")
|
||||
except PermissionError:
|
||||
return _error(403, "folder is not readable")
|
||||
except Exception as exc:
|
||||
|
||||
@@ -10,6 +10,8 @@ from pathlib import Path
|
||||
|
||||
import folder_paths
|
||||
|
||||
from .allowed_folders import can_read, require_readable
|
||||
|
||||
IMAGE_EXTENSIONS = {".png", ".jpg", ".jpeg", ".webp", ".avif", ".bmp", ".tif", ".tiff"}
|
||||
|
||||
|
||||
@@ -22,13 +24,15 @@ def resolve_load_dir(folder_path, image="", upload_probe=""):
|
||||
"""The folder a loader reads.
|
||||
|
||||
``folder_path`` when given (relative to ComfyUI's input folder unless absolute; a file stands for its folder),
|
||||
else the folder of the uploaded ``image`` / ``upload_probe``, else the input folder.
|
||||
else the folder of the uploaded ``image`` / ``upload_probe``, else the input folder. FolderNotAllowed for a
|
||||
``folder_path`` outside the folders NH-Nodes may read, before the disk is touched.
|
||||
"""
|
||||
folder_path = (folder_path or "").strip()
|
||||
if folder_path:
|
||||
resolved = folder_path
|
||||
if not os.path.isabs(resolved):
|
||||
resolved = os.path.join(folder_paths.get_input_directory(), resolved)
|
||||
resolved = require_readable(resolved, "folder_path")
|
||||
if os.path.isfile(resolved):
|
||||
resolved = os.path.dirname(resolved)
|
||||
return resolved
|
||||
@@ -59,9 +63,14 @@ def collect_image_files(folder_path, recursive=False):
|
||||
seen = set(files)
|
||||
models_dir = os.path.normcase(os.path.abspath(folder_paths.models_dir))
|
||||
for current_root, dir_names, file_names in os.walk(folder_path):
|
||||
# Natural order, and never into the ComfyUI models folder (a walk from one of its parents would crawl it).
|
||||
# Natural order, never into the ComfyUI models folder (a walk from one of its parents would crawl it), and
|
||||
# never through a link or junction that leads out of the folders NH-Nodes may read.
|
||||
dir_names[:] = sorted(
|
||||
(name for name in dir_names if os.path.normcase(os.path.abspath(os.path.join(current_root, name))) != models_dir),
|
||||
(
|
||||
name for name in dir_names
|
||||
if os.path.normcase(os.path.abspath(os.path.join(current_root, name))) != models_dir
|
||||
and can_read(os.path.join(current_root, name))
|
||||
),
|
||||
key=natural_key,
|
||||
)
|
||||
for file_name in sorted(file_names, key=natural_key):
|
||||
|
||||
@@ -0,0 +1,360 @@
|
||||
"""Where to run a parsing model on a big picture, and how to put its answers back (model-free, unit tested).
|
||||
|
||||
Parsing models see a small fixed-size picture (512 x 512 for the SegFormer face and fashion parsers). Squeezing a
|
||||
whole 8K photo into that leaves a face a few dozen pixels wide, so the parsers also run on crops: a square around
|
||||
each face, or a box of the model's shape around each group of people. A crop only refines the parse of the whole
|
||||
frame: it is pasted inside its own region, away from the region's cut edges, so whatever it misses keeps the
|
||||
whole-frame answer and no straight crop edge shows. Regions are (x1, y1, x2, y2) in image pixels with exclusive ends,
|
||||
and may reach past the image; the missing part is padded with grey.
|
||||
"""
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
|
||||
PAD_VALUE = 114 # grey, for the parts of a crop outside the picture
|
||||
|
||||
|
||||
def square_around(box, scale, lift=0.0):
|
||||
"""Square region ``scale`` times the longer side of ``box``, centred on it and moved up by ``lift`` times its
|
||||
side (a head's hair sits above its face box)."""
|
||||
x1, y1, x2, y2 = box[:4]
|
||||
side = max(1.0, scale * max(x2 - x1, y2 - y1))
|
||||
cx, cy = (x1 + x2) / 2.0, (y1 + y2) / 2.0 - lift * side
|
||||
left, top = int(round(cx - side / 2.0)), int(round(cy - side / 2.0))
|
||||
size = int(round(side))
|
||||
return left, top, left + size, top + size
|
||||
|
||||
|
||||
def _span(lo, hi, size, limit):
|
||||
"""[start, start + size) holding [lo, hi), slid to stay inside [0, limit) when it fits there."""
|
||||
start = (lo + hi) / 2.0 - size / 2.0
|
||||
if size <= limit:
|
||||
start = min(max(0.0, start), limit - size)
|
||||
start = int(round(start))
|
||||
return start, start + int(size)
|
||||
|
||||
|
||||
def fit_box(box, aspect, margin, image_size):
|
||||
"""Region of the model's shape (``aspect`` = width / height) around ``box`` plus ``margin`` (a fraction of its
|
||||
longer side) on each side. It stays inside the image where it fits there, and is padded where it does not."""
|
||||
height, width = image_size
|
||||
x1, y1, x2, y2 = box[:4]
|
||||
pad = margin * max(x2 - x1, y2 - y1)
|
||||
w, h = (x2 - x1) + 2 * pad, (y2 - y1) + 2 * pad
|
||||
w, h = max(w, h * aspect), max(h, w / aspect)
|
||||
w, h = int(np.ceil(w)), int(np.ceil(h))
|
||||
left, right = _span(x1, x2, w, width)
|
||||
top, bottom = _span(y1, y2, h, height)
|
||||
return left, top, right, bottom
|
||||
|
||||
|
||||
SPLIT = 1.3 # a box this much wider than the parser's shape is covered by a row of regions instead of one
|
||||
|
||||
|
||||
def _centres(lo, hi, tile, overlap):
|
||||
count = max(2, int(np.ceil((hi - lo - tile) / (tile * (1.0 - overlap)))) + 1)
|
||||
return np.linspace(lo + tile / 2.0, hi - tile / 2.0, count)
|
||||
|
||||
|
||||
def cover_box(box, aspect, margin, image_size, overlap=0.2):
|
||||
"""Regions of the parser's shape (``aspect`` = width / height) that together cover ``box`` plus ``margin``.
|
||||
|
||||
One region (see fit_box) unless the box is much wider than that shape, as a group photo is: then a row of
|
||||
overlapping regions as tall as the box (plus a margin of its height), so each person keeps the parser's resolution
|
||||
instead of the group being squeezed into one region with grey bands above and below. A tall box keeps one region:
|
||||
splitting a standing person into halves would hide the rest of the body from the parser.
|
||||
"""
|
||||
x1, y1, x2, y2 = box[:4]
|
||||
pad = margin * (y2 - y1)
|
||||
bx1, by1, bx2, by2 = x1 - pad, y1 - pad, x2 + pad, y2 + pad
|
||||
if bx2 - bx1 <= (by2 - by1) * aspect * SPLIT:
|
||||
return [fit_box(box, aspect, margin, image_size)]
|
||||
tile = (by2 - by1) * aspect
|
||||
return [fit_box((c - tile / 2.0, by1, c + tile / 2.0, by2), aspect, 0.0, image_size)
|
||||
for c in _centres(bx1, bx2, tile, overlap)]
|
||||
|
||||
|
||||
def square_frame(image_size):
|
||||
"""The square region centred on the picture that holds all of it (the short side is padded)."""
|
||||
height, width = image_size
|
||||
side = max(height, width)
|
||||
left, top = (width - side) // 2, (height - side) // 2
|
||||
return left, top, left + side, top + side
|
||||
|
||||
|
||||
def visible(region, image_size):
|
||||
"""The part of ``region`` inside an image of ``image_size`` (height, width)."""
|
||||
height, width = image_size
|
||||
x1, y1, x2, y2 = region
|
||||
return max(0, x1), max(0, y1), min(width, x2), min(height, y2)
|
||||
|
||||
|
||||
def cut_padded(image, region):
|
||||
"""``image[region]`` (H x W x C), padded with grey where the region leaves the image."""
|
||||
height, width = image.shape[:2]
|
||||
x1, y1, x2, y2 = region
|
||||
vx1, vy1, vx2, vy2 = visible(region, (height, width))
|
||||
crop = image[vy1:vy2, vx1:vx2]
|
||||
pads = (vy1 - y1, y2 - vy2, vx1 - x1, x2 - vx2)
|
||||
if any(pads):
|
||||
crop = cv2.copyMakeBorder(np.ascontiguousarray(crop), *pads, cv2.BORDER_CONSTANT,
|
||||
value=(PAD_VALUE,) * image.shape[2])
|
||||
return np.ascontiguousarray(crop)
|
||||
|
||||
|
||||
def nearest_owner(region, boxes, regions, index):
|
||||
"""Bool mask over ``region`` (inside the image): the pixels that belong to ``boxes[index]`` because no other box
|
||||
whose region also covers them is nearer (distance from the box centre, in box sizes). Where the crops around two
|
||||
faces overlap, each face keeps the parse of its own crop."""
|
||||
x1, y1, x2, y2 = region
|
||||
overlapping = [other for other in range(len(boxes)) if other != index and regions[other][2] > x1
|
||||
and regions[other][0] < x2 and regions[other][3] > y1 and regions[other][1] < y2]
|
||||
if not overlapping:
|
||||
return np.ones((y2 - y1, x2 - x1), dtype=bool)
|
||||
# decided on a grid of at most OWNER_GRID points a side, then enlarged: the border between two faces runs through
|
||||
# hair or background, where a few pixels do not matter, and an 8K crop would otherwise cost a second per face
|
||||
step = max(1, int(np.ceil(max(x2 - x1, y2 - y1) / OWNER_GRID)))
|
||||
ys = np.arange(y1, y2, step, dtype=np.float32)[:, None] + step / 2.0
|
||||
xs = np.arange(x1, x2, step, dtype=np.float32)[None, :] + step / 2.0
|
||||
|
||||
def distance(box):
|
||||
bx1, by1, bx2, by2 = box[:4]
|
||||
size = max(1.0, float(max(bx2 - bx1, by2 - by1)))
|
||||
return np.hypot(xs - (bx1 + bx2) / 2.0, ys - (by1 + by2) / 2.0) / size
|
||||
|
||||
own = distance(boxes[index])
|
||||
keep = np.ones(own.shape, dtype=bool)
|
||||
for other in overlapping:
|
||||
ox1, oy1, ox2, oy2 = regions[other]
|
||||
covered = (xs >= ox1) & (xs < ox2) & (ys >= oy1) & (ys < oy2)
|
||||
keep &= ~covered | (own <= distance(boxes[other]))
|
||||
if step > 1:
|
||||
keep = cv2.resize(keep.astype(np.uint8), (x2 - x1, y2 - y1), interpolation=cv2.INTER_NEAREST) > 0
|
||||
return keep
|
||||
|
||||
|
||||
OWNER_GRID = 512
|
||||
|
||||
|
||||
CUT_BAND = 0.08 # share of a crop's side, along each of its edges that cuts the picture, left to the whole frame
|
||||
NEAR_PEOPLE = 0.02 # how far (share of the long side) from the whole-frame parse's people a crop may add labels
|
||||
|
||||
|
||||
def inner_mask(region, image_size, band=CUT_BAND):
|
||||
"""Bool mask over the part of ``region`` inside the picture: True except within ``band`` of the region's size
|
||||
along its edges that lie inside the picture. The parser sees things cut there, so its answer is less sure; an edge
|
||||
that is the picture's own edge (or beyond it) keeps everything."""
|
||||
height, width = image_size
|
||||
x1, y1, x2, y2 = region
|
||||
vx1, vy1, vx2, vy2 = visible(region, image_size)
|
||||
keep = np.ones((vy2 - vy1, vx2 - vx1), dtype=bool)
|
||||
cut_x, cut_y = int(round(band * (x2 - x1))), int(round(band * (y2 - y1)))
|
||||
if x1 > 0:
|
||||
keep[:, :max(0, x1 + cut_x - vx1)] = False
|
||||
if x2 < width:
|
||||
keep[:, max(0, (x2 - cut_x) - vx1):] = False
|
||||
if y1 > 0:
|
||||
keep[:max(0, y1 + cut_y - vy1), :] = False
|
||||
if y2 < height:
|
||||
keep[max(0, (y2 - cut_y) - vy1):, :] = False
|
||||
return keep
|
||||
|
||||
|
||||
def parse_region(predict, image, region):
|
||||
"""(``predict`` run on the padded crop, cut to the part of ``region`` inside the picture; that part). When
|
||||
``predict`` returns several maps (labels and their confidence), each is cut the same way."""
|
||||
seen = visible(region, image.shape[:2])
|
||||
result = predict(cut_padded(image, region))
|
||||
x1, y1 = region[:2]
|
||||
rows, cols = slice(seen[1] - y1, seen[3] - y1), slice(seen[0] - x1, seen[2] - x1)
|
||||
if isinstance(result, tuple):
|
||||
return tuple(part[rows, cols] for part in result), seen
|
||||
return result[rows, cols], seen
|
||||
|
||||
|
||||
GROW_ROUNDS = 3 # times a crop may grow when its people run past one of its cut edges
|
||||
GROW_STEP = 0.5 # how much it grows each time, as a share of its size
|
||||
|
||||
|
||||
def _cut_sides_with_people(labels, region, image_size, background=0, share=0.02):
|
||||
"""Edges of ``region`` that cut the picture and where the parse (of the part inside the picture) still finds
|
||||
people within CUT_BAND: there the crop sliced through somebody."""
|
||||
height, width = image_size
|
||||
x1, y1, x2, y2 = region
|
||||
vx1, vy1, vx2, vy2 = visible(region, image_size)
|
||||
band_x, band_y = max(1, int(round(CUT_BAND * (x2 - x1)))), max(1, int(round(CUT_BAND * (y2 - y1))))
|
||||
found = labels != background
|
||||
sides = []
|
||||
if x1 > 0 and found[:, :band_x].mean() > share:
|
||||
sides.append("left")
|
||||
if x2 < width and found[:, max(0, (vx2 - vx1) - band_x):].mean() > share:
|
||||
sides.append("right")
|
||||
if y1 > 0 and found[:band_y, :].mean() > share:
|
||||
sides.append("top")
|
||||
if y2 < height and found[max(0, (vy2 - vy1) - band_y):, :].mean() > share:
|
||||
sides.append("bottom")
|
||||
return sides
|
||||
|
||||
|
||||
def _grown(region, sides, aspect, image_size):
|
||||
"""``region`` enlarged by GROW_STEP of its size towards ``sides``, back in the parser's shape."""
|
||||
x1, y1, x2, y2 = region
|
||||
step_x, step_y = GROW_STEP * (x2 - x1), GROW_STEP * (y2 - y1)
|
||||
box = (x1 - step_x * ("left" in sides), y1 - step_y * ("top" in sides),
|
||||
x2 + step_x * ("right" in sides), y2 + step_y * ("bottom" in sides))
|
||||
return fit_box(box, aspect, 0.0, image_size)
|
||||
|
||||
|
||||
COARSE_TRUST = 0.5 # the first look's confidence counts this much against a crop's: a crop sees people sharper
|
||||
SURE = 0.9 # a parser's confidence counted as sure, for the floor of people_labels
|
||||
# Least overlap (IoU) of the first look's people with those of a look at the mirrored picture, when no face is found:
|
||||
# people seen without a face come back in the mirror (0.83 to 0.99 on 32 photos; edited copies and small people pasted
|
||||
# on a plain ground go down to 0.64), what a parser invents on a facade does not (0.02 to 0.03 in 50 variants).
|
||||
MIRROR_AGREEMENT = 0.6
|
||||
# A region shorter than this (px) is not parsed again: blown up to a parser's input (4 to 8 times) so little is mostly
|
||||
# made up (a speck of a held helmet came back as salt-and-pepper labels); the first look's labels stay there.
|
||||
MIN_CROP = 128
|
||||
|
||||
|
||||
def person_box(face, image_size):
|
||||
"""Rough box of the person a face (x1, y1, x2, y2[, score]) belongs to: five face widths wide, from just above
|
||||
the head to about eight face heights below its top (a standing person), inside the picture."""
|
||||
height, width = image_size
|
||||
x1, y1, x2, y2 = face[:4]
|
||||
face_w, face_h = x2 - x1, y2 - y1
|
||||
centre = (x1 + x2) / 2.0
|
||||
return (max(0, int(centre - 2.5 * face_w)), max(0, int(y1 - 0.6 * face_h)),
|
||||
min(width, int(np.ceil(centre + 2.5 * face_w))), min(height, int(np.ceil(y1 + 8.0 * face_h))))
|
||||
|
||||
|
||||
def people_labels(predict, image, aspect, margin=0.08, people=(), floor=None):
|
||||
"""Label map of the people in the uint8 RGB ``image``, from ``predict`` (picture -> (label map with 0 for
|
||||
background, confidence map)), a parser trained on pictures of the shape ``aspect`` (width / height).
|
||||
|
||||
A first look parses the whole frame and is kept as the answer to refine. Each group of people (those it found,
|
||||
and the ``people`` boxes found from faces, which it may have missed) gets regions of the parser's shape (a row of
|
||||
them for a wide group, see cover_box), parsed again at the parser's full resolution; a region whose people run past
|
||||
one of its cut edges grows that way and is parsed again. A region's label replaces the current one where it is
|
||||
surer and away from the region's cut edges, for the parts of it that lie on people already found (see anchored)
|
||||
and for background near them; so nothing is invented on an empty backdrop and overlapping regions meet where their
|
||||
answers do, not along a straight line.
|
||||
|
||||
With ``floor`` and no ``people`` boxes, a first look that is sure (SURE or more) of less than ``floor`` of what it
|
||||
labels means nobody is there, and the map is all background: a parser trained on people sees them in a picture
|
||||
without any, but is unsure of most of it (a facade or a cafe's shelves come out as faint arms and clothes), and its
|
||||
crops of such a picture would find more. So does a first look that a look at the mirrored picture does not repeat
|
||||
(MIRROR_AGREEMENT): what is invented is not where the mirror would put it.
|
||||
"""
|
||||
height, width = image.shape[:2]
|
||||
coarse, trust = predict(image)
|
||||
if floor is not None and not len(people):
|
||||
labelled = trust[coarse != 0]
|
||||
if not labelled.size or np.count_nonzero(labelled >= labelled.dtype.type(SURE)) < floor * labelled.size:
|
||||
return np.zeros_like(coarse)
|
||||
found, mirrored = coarse != 0, predict(np.ascontiguousarray(image[:, ::-1]))[0][:, ::-1] != 0
|
||||
if np.count_nonzero(found & mirrored) < MIRROR_AGREEMENT * np.count_nonzero(found | mirrored):
|
||||
return np.zeros_like(coarse)
|
||||
regions = [region for box in foreground_clusters(coarse, extra=people)
|
||||
for region in cover_box(box, aspect, margin, (height, width)) if region[3] - region[1] >= MIN_CROP]
|
||||
if not regions or regions == [(0, 0, width, height)]:
|
||||
return coarse # nobody found, or the first look already saw the whole picture in the parser's own shape
|
||||
anchor = coarse != 0 # where the first look (or a face) puts people
|
||||
for x1, y1, x2, y2 in people:
|
||||
anchor[y1:y2, x1:x2] = True
|
||||
near = near_people(anchor.astype(np.uint8))
|
||||
canvas, trust = coarse.copy(), trust.astype(np.float32) * COARSE_TRUST
|
||||
for region in regions:
|
||||
(labels, sure), seen = parse_region(predict, image, region)
|
||||
for _ in range(GROW_ROUNDS):
|
||||
sides = _cut_sides_with_people(labels, region, (height, width))
|
||||
if not sides:
|
||||
break
|
||||
region = _grown(region, sides, aspect, (height, width))
|
||||
(labels, sure), seen = parse_region(predict, image, region)
|
||||
if labels.size == 0:
|
||||
continue
|
||||
x1, y1, x2, y2 = seen
|
||||
sure = sure.astype(np.float32)
|
||||
allowed = anchored(labels, anchor[y1:y2, x1:x2]) | ((labels == 0) & near[y1:y2, x1:x2])
|
||||
keep = inner_mask(region, (height, width)) & allowed & (sure > trust[y1:y2, x1:x2])
|
||||
canvas[y1:y2, x1:x2][keep] = labels[keep]
|
||||
trust[y1:y2, x1:x2][keep] = sure[keep]
|
||||
return canvas
|
||||
|
||||
|
||||
ANCHOR_SHARE = 0.1 # share of a crop's part that must lie on people already found for the part to be kept
|
||||
|
||||
|
||||
def anchored(labels, anchor, share=ANCHOR_SHARE, grid=1024):
|
||||
"""Bool mask of the labelled pixels of a crop's ``labels`` whose part (the connected pixels of one label) lies on
|
||||
``anchor`` (people already found) for at least ``share`` of its area. A crop may so extend a person the first look
|
||||
saw only in part, but anything it finds on its own (clothes "seen" on a plain backdrop, even right next to
|
||||
somebody) is dropped. Worked out on a copy at most ``grid`` pixels long."""
|
||||
height, width = labels.shape
|
||||
scale = min(1.0, grid / max(height, width, 1))
|
||||
small, held = labels, anchor
|
||||
if scale < 1.0:
|
||||
size = (max(1, round(width * scale)), max(1, round(height * scale)))
|
||||
small = cv2.resize(labels.astype(np.int32), size, interpolation=cv2.INTER_NEAREST)
|
||||
held = cv2.resize(anchor.astype(np.uint8), size, interpolation=cv2.INTER_NEAREST) > 0
|
||||
kept = np.zeros(small.shape, dtype=bool)
|
||||
for value in np.unique(small):
|
||||
if value == 0:
|
||||
continue
|
||||
count, parts = cv2.connectedComponents((small == value).astype(np.uint8), connectivity=8)
|
||||
if count <= 1:
|
||||
continue
|
||||
areas = np.bincount(parts.ravel(), minlength=count)
|
||||
on_people = np.bincount(parts[held], minlength=count)
|
||||
good = on_people >= share * areas
|
||||
good[0] = False
|
||||
kept |= good[parts]
|
||||
if scale < 1.0:
|
||||
kept = cv2.resize(kept.astype(np.uint8), (width, height), interpolation=cv2.INTER_NEAREST) > 0
|
||||
return kept & (labels != 0)
|
||||
|
||||
|
||||
def near_people(labels, background=0, reach=NEAR_PEOPLE, grid=1024):
|
||||
"""Bool map of the pixels within ``reach`` (a share of the long side) of a labelled pixel. Worked out on a copy at
|
||||
most ``grid`` pixels long and enlarged, since a few pixels do not matter here and a 300 px dilation of an 8K map
|
||||
would take seconds."""
|
||||
height, width = labels.shape
|
||||
scale = min(1.0, grid / max(height, width))
|
||||
found = (labels != background).astype(np.uint8)
|
||||
if scale < 1.0:
|
||||
found = cv2.resize(found, (max(1, round(width * scale)), max(1, round(height * scale))),
|
||||
interpolation=cv2.INTER_AREA)
|
||||
found = (found > 0).astype(np.uint8)
|
||||
radius = max(1, int(round(reach * max(found.shape))))
|
||||
grown = cv2.dilate(found, cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (2 * radius + 1, 2 * radius + 1)))
|
||||
if scale < 1.0:
|
||||
grown = cv2.resize(grown, (width, height), interpolation=cv2.INTER_NEAREST)
|
||||
return grown > 0
|
||||
|
||||
|
||||
def foreground_clusters(labels, background=0, min_fraction=0.001, gap=0.05, extra=()):
|
||||
"""Boxes around the groups of labelled pixels of a label map (and the ``extra`` boxes): specks under
|
||||
``min_fraction`` of the picture are left out, and boxes closer to each other than ``gap`` of the picture's long
|
||||
side are merged, so people standing together form one group and people far apart get one box each."""
|
||||
found = (labels != background).astype(np.uint8)
|
||||
count, _, stats, _ = cv2.connectedComponentsWithStats(found, connectivity=8)
|
||||
boxes = [[int(stats[i, cv2.CC_STAT_LEFT]), int(stats[i, cv2.CC_STAT_TOP]),
|
||||
int(stats[i, cv2.CC_STAT_LEFT] + stats[i, cv2.CC_STAT_WIDTH]),
|
||||
int(stats[i, cv2.CC_STAT_TOP] + stats[i, cv2.CC_STAT_HEIGHT])]
|
||||
for i in range(1, count) if stats[i, cv2.CC_STAT_AREA] >= min_fraction * labels.size]
|
||||
boxes += [[int(v) for v in box[:4]] for box in extra]
|
||||
reach = gap * max(labels.shape)
|
||||
merged = True
|
||||
while merged:
|
||||
merged = False
|
||||
for i in range(len(boxes)):
|
||||
for j in range(i + 1, len(boxes)):
|
||||
a, b = boxes[i], boxes[j]
|
||||
if (max(a[0], b[0]) - min(a[2], b[2]) < reach) and (max(a[1], b[1]) - min(a[3], b[3]) < reach):
|
||||
boxes[i] = [min(a[0], b[0]), min(a[1], b[1]), max(a[2], b[2]), max(a[3], b[3])]
|
||||
del boxes[j]
|
||||
merged = True
|
||||
break
|
||||
if merged:
|
||||
break
|
||||
return [tuple(box) for box in sorted(boxes)]
|
||||
@@ -43,7 +43,7 @@ MODEL_SPECS = {
|
||||
"min_res": (512, 512),
|
||||
"max_res": (2560, 2560),
|
||||
"latent_channels": 16,
|
||||
"note": "Qwen tự match aspect input image",
|
||||
"note": "Qwen-Image matches the aspect ratio of the input image",
|
||||
},
|
||||
"z_image": {
|
||||
"name": "Z-Image",
|
||||
|
||||
@@ -0,0 +1,710 @@
|
||||
"""SAM 3 for the NH vision nodes: finding the model, loading it, and segmenting concepts in two passes.
|
||||
|
||||
NH-Nodes does not ship SAM 3. It uses Meta's ``sam3`` package, installed separately (see ``INSTALL_HINT``), and the
|
||||
``sam3.pt`` checkpoint, which Hugging Face hands out only after Meta's license is accepted, so it is never
|
||||
downloaded automatically. Checkpoints are looked up in the ``sam3`` model folder (``models/sam3`` by default, more
|
||||
locations in ``extra_model_paths.yaml``); other SAM 3 packs use the same folder, so one copy serves all of them.
|
||||
|
||||
Pass 1 looks at the whole image once: the image is resized to the model's square input, encoded once and asked for
|
||||
every concept. Each instance's mask is sampled back to full resolution inside its own box only.
|
||||
Pass 2 (``detail_level="balanced"``) cuts each instance out of the full-resolution image with a margin and segments it
|
||||
again with SAM 3's box-prompt decoder, the pass-1 mask given as a hint. Objects that are small or medium in the frame
|
||||
then get their edges from the original pixels instead of a ~1000 px view of the whole frame. SAM 3 draws masks on a
|
||||
grid of about 288 cells across whatever it looks at, so a subject that fills most of an 8K frame still has edges at
|
||||
about 1/300 of its size; hair-level edges need matting. A close-up that does not show the pass-1 instance is dropped
|
||||
and the pass-1 mask kept.
|
||||
|
||||
The official sam3 code builds some of its lookup tables on the GPU, so SAM 3 runs on CUDA only.
|
||||
"""
|
||||
|
||||
import contextlib
|
||||
import importlib
|
||||
import importlib.util
|
||||
import inspect
|
||||
import itertools
|
||||
import math
|
||||
import os
|
||||
import sys
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
import torch
|
||||
import torch.nn.functional as F
|
||||
|
||||
from .model_files import make_room_on_gpu, models_dir, register_model_folder
|
||||
from .segs import SegsFrame, make_segment
|
||||
|
||||
try:
|
||||
import folder_paths
|
||||
except Exception: # running outside ComfyUI (tests)
|
||||
folder_paths = None
|
||||
|
||||
try:
|
||||
import comfy.model_management as _mm
|
||||
except Exception:
|
||||
_mm = None
|
||||
|
||||
FOLDER_NAME = "sam3" # shared with the other SAM 3 packs
|
||||
# Listed in ComfyUI for the folder: SAM 3 checkpoints in the formats SAM 3 packs use, not Hugging Face cache files.
|
||||
FOLDER_EXTENSIONS = (".pt", ".pth", ".safetensors")
|
||||
CHECKPOINT_EXTENSIONS = (".pt", ".pth") # what sam3's own loader reads (torch.load)
|
||||
DEFAULT_CHECKPOINT = "sam3.pt"
|
||||
BPE_FILE = "bpe_simple_vocab_16e6.txt.gz" # sam3's tokenizer vocabulary, in the package's assets folder
|
||||
DETAIL_LEVELS = ("balanced", "fast")
|
||||
|
||||
SAM3_COMMIT = "b26a5f330e05d321afb39d01d3d4881f258f65ff" # the sam3 build NH-Nodes is tested with
|
||||
INSTALL_HINT = (
|
||||
"SAM 3 needs Meta's sam3 package, which NH-Nodes does not install for you. In ComfyUI's Python environment run:\n"
|
||||
f" pip install --no-deps git+https://github.com/facebookresearch/sam3@{SAM3_COMMIT}\n"
|
||||
" pip install timm ftfy regex iopath\n"
|
||||
"--no-deps keeps your numpy: sam3 asks for numpy<2, which ComfyUI does not need. "
|
||||
"Restart ComfyUI afterwards."
|
||||
)
|
||||
CHECKPOINT_HINT = (
|
||||
"Ask for access at https://huggingface.co/facebook/sam3 (Meta has you accept the SAM License first), "
|
||||
"download sam3.pt from that page and put it in {folder}. Then refresh ComfyUI (press R) and pick it."
|
||||
)
|
||||
|
||||
DEDUP_IOU = 0.9 # a second instance of the same concept overlapping a better one this much is a duplicate
|
||||
# A close-up replaces the pass-1 mask when it shows the same object: at least REFINE_MIN_INSIDE of it lies within the
|
||||
# pass-1 blur of the pass-1 mask, and it covers at least REFINE_MIN_CORE of the pass-1 core (see close_up_agrees).
|
||||
REFINE_MIN_INSIDE = 0.8
|
||||
REFINE_MIN_CORE = 0.6
|
||||
_PASS1_GRID = 288 # SAM 3's detector draws masks on a 288 x 288 grid of its square view of the whole image
|
||||
_AGREE_PIXELS = 1 << 21 # masks larger than this are compared at a reduced scale
|
||||
_WIDE_ASPECT = 4.0 # beyond this aspect ratio SAM 3's square view squeezes the picture enough to miss small things
|
||||
_CROP_MARGIN = 0.2 # pass-2 crop margin on each side, as a fraction of the instance's longer side
|
||||
_CROP_MIN_MARGIN = 16 # px
|
||||
_CROP_MAX_ASPECT = 2.0 # pass-2 crops are widened to at most 2:1, the model squeezes them into a square
|
||||
_SAMPLE_CHUNK = 1 << 22 # output pixels per grid_sample call when resampling masks
|
||||
# SAM 3's masks fade out along the border of the square view it segments: on a 1008 px view the last 3 rows and
|
||||
# columns hold logits near 0 even where the object runs off the picture (79% / 23% / 23% of an edge's object pixels
|
||||
# still positive), which left a ragged gap along the image edge. That band is lifted to the first row inside it, only
|
||||
# where it is still in the fade (see extend_to_border). A close-up fades over 2 to 3 rows of its own view. The price:
|
||||
# a thing that ends within that band (about 0.3% of the picture's side) above the edge is drawn down to the edge,
|
||||
# which costs far less than the ragged edge every person cut by the frame used to get.
|
||||
_BORDER_CELLS = 3
|
||||
_CLOSE_UP_CELLS = 3
|
||||
_FADE_FLOOR = -2.5 # band logits below this are a sure "no" and are left alone (the fade reaches about -1.6)
|
||||
_VIEW_SIZE = 1008 # side of SAM 3's square view
|
||||
NEAR_MISS = 0.1 # candidates scoring at least this (but under the threshold) are reported as near misses
|
||||
_WORKING_BYTES = 3 << 30 # VRAM for activations and the full-resolution image, on top of the weights
|
||||
|
||||
_API = None
|
||||
_CACHE = {}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Checkpoints
|
||||
# ---------------------------------------------------------------------------
|
||||
def default_folder():
|
||||
"""<models dir>/sam3, the folder ComfyUI and the other SAM 3 packs use."""
|
||||
return os.path.join(models_dir(), FOLDER_NAME)
|
||||
|
||||
|
||||
def checkpoint_folders():
|
||||
"""Every folder searched for checkpoints, best first (registers the folder name with ComfyUI)."""
|
||||
return register_model_folder(FOLDER_NAME, default_folder(), FOLDER_EXTENSIONS)
|
||||
|
||||
|
||||
def checkpoint_choices():
|
||||
"""Checkpoint names for the node's list: .pt/.pth files in the sam3 folders, or the default name if none yet."""
|
||||
names = set()
|
||||
folders = checkpoint_folders()
|
||||
if folder_paths is not None:
|
||||
try:
|
||||
names.update(folder_paths.get_filename_list(FOLDER_NAME))
|
||||
except Exception as exc:
|
||||
print(f"[NH-Nodes] Could not list the {FOLDER_NAME} model folder: {exc}")
|
||||
else:
|
||||
for folder in folders:
|
||||
if os.path.isdir(folder):
|
||||
names.update(os.listdir(folder))
|
||||
names = sorted(name for name in names if name.lower().endswith(CHECKPOINT_EXTENSIONS))
|
||||
return names or [DEFAULT_CHECKPOINT]
|
||||
|
||||
|
||||
def resolve_checkpoint(name):
|
||||
"""Full path of checkpoint ``name`` in the sam3 folders; a RuntimeError says where to get it when it is missing."""
|
||||
name = str(name or DEFAULT_CHECKPOINT)
|
||||
parts = name.replace("\\", "/").split("/")
|
||||
if os.path.isabs(name) or ".." in parts:
|
||||
raise RuntimeError(f"[NH-Nodes] SAM 3 checkpoint name must be a file in the {FOLDER_NAME} folder: {name!r}")
|
||||
if not name.lower().endswith(CHECKPOINT_EXTENSIONS):
|
||||
raise RuntimeError(
|
||||
f"[NH-Nodes] {name} is not a SAM 3 .pt checkpoint. The safetensors export on Hugging Face is for the "
|
||||
"transformers library and uses other weight names; download sam3.pt instead."
|
||||
)
|
||||
folders = checkpoint_folders()
|
||||
path = folder_paths.get_full_path(FOLDER_NAME, name) if folder_paths is not None else None
|
||||
if path is None:
|
||||
path = next((os.path.join(folder, *parts) for folder in folders
|
||||
if os.path.isfile(os.path.join(folder, *parts))), None)
|
||||
if path is None or not os.path.isfile(path):
|
||||
raise RuntimeError(
|
||||
f"[NH-Nodes] SAM 3 checkpoint {name} not found in: {', '.join(folders)}.\n"
|
||||
+ CHECKPOINT_HINT.format(folder=folders[0] if folders else default_folder())
|
||||
)
|
||||
return path
|
||||
|
||||
|
||||
# Make the folder name known to ComfyUI as soon as the pack loads.
|
||||
checkpoint_folders()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# The sam3 package and the model
|
||||
# ---------------------------------------------------------------------------
|
||||
def _tf32_flags():
|
||||
"""(matmul, cudnn) TF32 switches, or None when torch refuses to report them (its newer precision API in use)."""
|
||||
try:
|
||||
return torch.backends.cuda.matmul.allow_tf32, torch.backends.cudnn.allow_tf32
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def _restore_tf32(flags):
|
||||
if flags is not None:
|
||||
try:
|
||||
torch.backends.cuda.matmul.allow_tf32, torch.backends.cudnn.allow_tf32 = flags
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def _missing_parameters(func, names):
|
||||
"""Names from ``names`` that ``func`` does not accept (none when its signature cannot be read or takes **kwargs)."""
|
||||
try:
|
||||
parameters = inspect.signature(func).parameters
|
||||
except (TypeError, ValueError):
|
||||
return []
|
||||
if any(p.kind == p.VAR_KEYWORD for p in parameters.values()):
|
||||
return []
|
||||
return [name for name in names if name not in parameters]
|
||||
|
||||
|
||||
def ensure_sam3():
|
||||
"""(build_sam3_image_model, Sam3Processor) from the installed sam3 package.
|
||||
|
||||
A RuntimeError explains what to do when the package is missing, fails to import or lacks the image API
|
||||
NH-Nodes uses. Importing sam3's model builder switches TF32 on for the whole process; the previous setting is
|
||||
put back, so ComfyUI's own models keep theirs.
|
||||
"""
|
||||
global _API
|
||||
if _API is not None:
|
||||
return _API
|
||||
if importlib.util.find_spec("sam3") is None:
|
||||
raise RuntimeError("[NH-Nodes] " + INSTALL_HINT)
|
||||
flags = _tf32_flags()
|
||||
try:
|
||||
builder = importlib.import_module("sam3.model_builder")
|
||||
processors = importlib.import_module("sam3.model.sam3_image_processor")
|
||||
except Exception as exc:
|
||||
raise RuntimeError(f"[NH-Nodes] The sam3 package is installed but failed to load: {exc}\n{INSTALL_HINT}") from exc
|
||||
finally:
|
||||
_restore_tf32(flags)
|
||||
|
||||
build = getattr(builder, "build_sam3_image_model", None)
|
||||
processor_cls = getattr(processors, "Sam3Processor", None)
|
||||
missing = []
|
||||
if not callable(build):
|
||||
missing.append("build_sam3_image_model")
|
||||
else:
|
||||
missing += [f"build_sam3_image_model({name}=)" for name in _missing_parameters(
|
||||
build, ("bpe_path", "checkpoint_path", "load_from_HF", "enable_inst_interactivity", "device", "eval_mode"))]
|
||||
if processor_cls is None:
|
||||
missing.append("Sam3Processor")
|
||||
else:
|
||||
missing += [f"Sam3Processor({name}=)" for name in
|
||||
_missing_parameters(processor_cls, ("device", "confidence_threshold"))]
|
||||
missing += [f"Sam3Processor.{name}" for name in ("set_image", "set_text_prompt", "reset_all_prompts")
|
||||
if not callable(getattr(processor_cls, name, None))]
|
||||
if missing:
|
||||
raise RuntimeError(
|
||||
f"[NH-Nodes] The installed sam3 package lacks {', '.join(missing)}; it is older or newer than the "
|
||||
f"versions NH-Nodes supports. Reinstall it:\n{INSTALL_HINT}"
|
||||
)
|
||||
_API = (build, processor_cls)
|
||||
return _API
|
||||
|
||||
|
||||
def sam3_device():
|
||||
"""ComfyUI's torch device, which must be a CUDA GPU for SAM 3."""
|
||||
if _mm is not None:
|
||||
device = _mm.get_torch_device()
|
||||
else:
|
||||
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
||||
if device.type != "cuda":
|
||||
raise RuntimeError(
|
||||
f"[NH-Nodes] SAM 3 needs an NVIDIA GPU with CUDA (ComfyUI runs on {device}): the official sam3 code "
|
||||
"builds part of the model on the GPU."
|
||||
)
|
||||
return device
|
||||
|
||||
|
||||
def bpe_path():
|
||||
"""The tokenizer vocabulary inside the sam3 package that was imported, whichever copy that is.
|
||||
|
||||
Other packs bundle their own copy of sam3 and may put it first on sys.path (ComfyUI-RMBG does), so "sam3" is not
|
||||
always Meta's install. Such a copy can look for the vocabulary in the wrong folder when it is not given one
|
||||
(ComfyUI-RMBG's looks one level too high), so NH-Nodes always finds it here and passes it.
|
||||
"""
|
||||
package = sys.modules.get("sam3") or importlib.import_module("sam3")
|
||||
folders = [os.path.dirname(package.__file__)] if getattr(package, "__file__", None) else []
|
||||
folders += [folder for folder in getattr(package, "__path__", []) if folder not in folders]
|
||||
for folder in folders:
|
||||
for candidate in (os.path.join(folder, "assets", BPE_FILE), os.path.join(folder, os.pardir, "assets", BPE_FILE)):
|
||||
if os.path.isfile(candidate):
|
||||
return os.path.normpath(candidate)
|
||||
where = folders[0] if folders else "its folder"
|
||||
raise RuntimeError(f"[NH-Nodes] The sam3 package in {where} has no assets/{BPE_FILE}; reinstall it:\n{INSTALL_HINT}")
|
||||
|
||||
|
||||
def check_checkpoint(path):
|
||||
"""Fail early, with the download advice, when ``path`` is not a readable SAM 3 checkpoint.
|
||||
|
||||
sam3 loads weights non-strictly and only prints the names it misses, so another model's .pt in the shared folder
|
||||
would run with random weights, and a truncated download fails deep inside torch. Reading the names is quick:
|
||||
the file is memory-mapped, not loaded.
|
||||
"""
|
||||
advice = CHECKPOINT_HINT.format(folder=os.path.dirname(path))
|
||||
try:
|
||||
state = torch.load(path, map_location="cpu", weights_only=True, mmap=True)
|
||||
except Exception as exc:
|
||||
raise RuntimeError(f"[NH-Nodes] {path} cannot be read as a SAM 3 checkpoint ({exc}); it may be an "
|
||||
f"incomplete download.\n{advice}") from exc
|
||||
if isinstance(state, dict) and isinstance(state.get("model"), dict):
|
||||
state = state["model"]
|
||||
names = list(state.keys()) if isinstance(state, dict) else []
|
||||
del state
|
||||
if not any(name.startswith("detector.") for name in names) or not any(name.startswith("tracker.") for name in names):
|
||||
raise RuntimeError(f"[NH-Nodes] {path} is not a SAM 3 checkpoint (it has no SAM 3 detector and tracker "
|
||||
f"weights).\n{advice}")
|
||||
|
||||
|
||||
def load_model(checkpoint):
|
||||
"""The SAM 3 image model for ``checkpoint`` (a name in the sam3 folders); kept in RAM between runs."""
|
||||
path = resolve_checkpoint(checkpoint)
|
||||
stamp = (os.path.normcase(os.path.abspath(path)), os.path.getmtime(path), os.path.getsize(path))
|
||||
if _CACHE.get("model") is not None and _CACHE.get("stamp") == stamp:
|
||||
return _CACHE["model"]
|
||||
unload()
|
||||
build, _ = ensure_sam3()
|
||||
device = sam3_device()
|
||||
check_checkpoint(path)
|
||||
print(f"[NH-Nodes] Loading SAM 3 from {path}")
|
||||
with torch.cuda.device(device): # the tables sam3 builds on "cuda" go to ComfyUI's GPU
|
||||
model = build(bpe_path=bpe_path(), checkpoint_path=path, load_from_HF=False,
|
||||
enable_inst_interactivity=True, device="cpu", eval_mode=True)
|
||||
if not callable(getattr(model, "predict_inst", None)) or getattr(model, "inst_interactive_predictor", None) is None:
|
||||
raise RuntimeError("[NH-Nodes] This sam3 build has no box-prompt decoder (predict_inst). " + INSTALL_HINT)
|
||||
transforms = getattr(model.inst_interactive_predictor, "_transforms", None)
|
||||
if getattr(transforms, "max_hole_area", 0):
|
||||
# sam3 fills holes of close-up masks by default; keep real ones (between an arm and the body), as pass 1 does
|
||||
transforms.max_hole_area = 0.0
|
||||
model.requires_grad_(False)
|
||||
_CACHE.update(model=model, stamp=stamp)
|
||||
return model
|
||||
|
||||
|
||||
def _model_device(model):
|
||||
return next(model.parameters()).device
|
||||
|
||||
|
||||
def _is_on(current, wanted):
|
||||
"""True when ``current`` satisfies ``wanted``; an index-less "cuda" accepts any cuda:N."""
|
||||
return current.type == wanted.type and (wanted.index is None or current.index == wanted.index)
|
||||
|
||||
|
||||
_TABLE_ATTRIBUTES = ("freqs_cis", "freqs_cis_real", "freqs_cis_imag") # sam3's rotary tables, plain attributes
|
||||
|
||||
|
||||
def _all_on(model, device):
|
||||
return all(_is_on(p.device, device) for p in model.parameters())
|
||||
|
||||
|
||||
def _move_tables(model, device):
|
||||
"""Move sam3's rotary tables, which model.to() leaves behind because they are not buffers."""
|
||||
for module in model.modules():
|
||||
for name in _TABLE_ATTRIBUTES:
|
||||
value = getattr(module, name, None)
|
||||
if isinstance(value, torch.Tensor) and not _is_on(value.device, device):
|
||||
setattr(module, name, value.to(device))
|
||||
|
||||
|
||||
def move_model(model, device):
|
||||
"""Put the weights on ``device``, asking ComfyUI to free VRAM first; back to the CPU if the move fails."""
|
||||
if device.type == "cuda":
|
||||
weights = 0 if _all_on(model, device) else sum(
|
||||
t.numel() * t.element_size() for t in itertools.chain(model.parameters(), model.buffers()))
|
||||
make_room_on_gpu(weights + _WORKING_BYTES)
|
||||
try:
|
||||
model.to(device)
|
||||
_move_tables(model, device)
|
||||
except Exception:
|
||||
offload(model) # a half-moved model would fail every later run
|
||||
raise
|
||||
|
||||
|
||||
def offload(model):
|
||||
"""Move the weights and sam3's GPU-side tables to the CPU, so nothing holds VRAM between runs.
|
||||
|
||||
The position-encoding caches sam3 fills at build time (about 200 MB on the GPU) are emptied; sam3 rebuilds them
|
||||
on the GPU when it needs them again.
|
||||
"""
|
||||
cpu = torch.device("cpu")
|
||||
if not _all_on(model, cpu):
|
||||
model.to(cpu)
|
||||
_move_tables(model, cpu)
|
||||
for module in model.modules():
|
||||
if type(module).__name__ == "PositionEmbeddingSine" and isinstance(getattr(module, "cache", None), dict):
|
||||
module.cache.clear()
|
||||
if torch.cuda.is_available():
|
||||
torch.cuda.empty_cache()
|
||||
|
||||
|
||||
def unload():
|
||||
"""Forget the cached model (another checkpoint was chosen)."""
|
||||
model = _CACHE.pop("model", None)
|
||||
_CACHE.pop("stamp", None)
|
||||
if model is not None:
|
||||
offload(model)
|
||||
|
||||
|
||||
def _native_bf16():
|
||||
try:
|
||||
return torch.cuda.is_bf16_supported(including_emulation=False)
|
||||
except TypeError: # torch without the including_emulation switch
|
||||
return torch.cuda.is_bf16_supported()
|
||||
|
||||
|
||||
def _autocast(device):
|
||||
"""bfloat16 autocast on GPUs with native bfloat16 (Ampere and newer), as Meta runs SAM 3; full precision elsewhere."""
|
||||
if device.type == "cuda" and _native_bf16():
|
||||
return torch.autocast("cuda", dtype=torch.bfloat16)
|
||||
return contextlib.nullcontext()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Geometry helpers (model-free, unit tested)
|
||||
# ---------------------------------------------------------------------------
|
||||
def to_logits(masks):
|
||||
"""sam3 returns its masks after a sigmoid (under the key "masks_logits"); turn them back into logits.
|
||||
|
||||
Values outside [0, 1] are taken as logits already, in case a later sam3 returns them as the key says.
|
||||
"""
|
||||
masks = masks.float()
|
||||
if masks.numel() and (float(masks.min()) < 0.0 or float(masks.max()) > 1.0):
|
||||
return masks
|
||||
masks = masks.clamp(1e-6, 1.0 - 1e-6)
|
||||
return torch.log(masks) - torch.log1p(-masks)
|
||||
|
||||
|
||||
def sample_region(logits, image_size, region, out_size):
|
||||
"""Resample the square model-space map ``logits`` [S, S] over ``region`` of the image, bilinearly.
|
||||
|
||||
``region`` is (x1, y1, x2, y2) in pixels of an image of ``image_size`` (height, width), which the model saw
|
||||
squeezed into S x S; the result is [h, w] for ``out_size`` (h, w), each value taken at the centre of its output
|
||||
pixel. Runs in row chunks, so a full 8K region needs a few hundred MB at most.
|
||||
"""
|
||||
height, width = image_size
|
||||
size = logits.shape[-1]
|
||||
x1, y1, x2, y2 = region
|
||||
out_h, out_w = int(out_size[0]), int(out_size[1])
|
||||
device = logits.device
|
||||
xs = x1 + (torch.arange(out_w, device=device, dtype=torch.float32) + 0.5) * ((x2 - x1) / out_w)
|
||||
ys = y1 + (torch.arange(out_h, device=device, dtype=torch.float32) + 0.5) * ((y2 - y1) / out_h)
|
||||
# model-map pixel index of each centre, as grid_sample coordinates (align_corners=True: -1/+1 = first/last centre)
|
||||
grid_x = (xs * (size / width) - 0.5) * (2.0 / max(1, size - 1)) - 1.0
|
||||
grid_y = (ys * (size / height) - 0.5) * (2.0 / max(1, size - 1)) - 1.0
|
||||
source = logits.float()[None, None]
|
||||
result = torch.empty((out_h, out_w), device=device, dtype=torch.float32)
|
||||
rows = max(1, _SAMPLE_CHUNK // max(1, out_w))
|
||||
for start in range(0, out_h, rows):
|
||||
gy = grid_y[start:start + rows]
|
||||
grid = torch.stack(torch.broadcast_tensors(grid_x[None, :], gy[:, None]), dim=-1)[None]
|
||||
result[start:start + rows] = F.grid_sample(source, grid, mode="bilinear", padding_mode="border",
|
||||
align_corners=True)[0, 0]
|
||||
return result
|
||||
|
||||
|
||||
def _lift(band, inner):
|
||||
"""``band`` raised to ``inner`` where that is higher and ``band`` is still in the fade (above _FADE_FLOOR)."""
|
||||
if torch.is_tensor(band):
|
||||
return torch.where(band > _FADE_FLOOR, torch.maximum(band, inner), band)
|
||||
return np.where(band > _FADE_FLOOR, np.maximum(band, inner), band)
|
||||
|
||||
|
||||
def extend_to_border(logits, band_y, band_x, edges=("top", "bottom", "left", "right")):
|
||||
"""Copy of ``logits`` [..., H, W] (torch or numpy) whose outer ``band_y`` rows / ``band_x`` columns on the given
|
||||
``edges`` are lifted to the first row / column inside the band, where they are still in the fade. A mask can only
|
||||
grow there, and only where the object reaches the band from inside."""
|
||||
out = logits.clone() if torch.is_tensor(logits) else np.array(logits, copy=True)
|
||||
height, width = out.shape[-2:]
|
||||
band_y, band_x = min(int(band_y), (height - 1) // 2), min(int(band_x), (width - 1) // 2)
|
||||
if band_y > 0 and "top" in edges:
|
||||
out[..., :band_y, :] = _lift(out[..., :band_y, :], out[..., band_y:band_y + 1, :])
|
||||
if band_y > 0 and "bottom" in edges:
|
||||
out[..., height - band_y:, :] = _lift(out[..., height - band_y:, :],
|
||||
out[..., height - band_y - 1:height - band_y, :])
|
||||
if band_x > 0 and "left" in edges:
|
||||
out[..., :, :band_x] = _lift(out[..., :, :band_x], out[..., :, band_x:band_x + 1])
|
||||
if band_x > 0 and "right" in edges:
|
||||
out[..., :, width - band_x:] = _lift(out[..., :, width - band_x:], out[..., :, width - band_x - 1:width - band_x])
|
||||
return out
|
||||
|
||||
|
||||
def border_band(pixels, view=_VIEW_SIZE, cells=_BORDER_CELLS):
|
||||
"""Width in pixels of the faded band (``cells`` rows of SAM 3's view) along an edge ``pixels`` long that SAM 3
|
||||
saw ``view`` pixels long."""
|
||||
return math.ceil(cells * pixels / view)
|
||||
|
||||
|
||||
def mask_box(mask):
|
||||
"""Tight (x1, y1, x2, y2) box of a bool [H, W] tensor with exclusive ends; None when empty."""
|
||||
rows = torch.nonzero(mask.any(dim=1)).flatten()
|
||||
if rows.numel() == 0:
|
||||
return None
|
||||
cols = torch.nonzero(mask.any(dim=0)).flatten()
|
||||
return int(cols[0]), int(rows[0]), int(cols[-1]) + 1, int(rows[-1]) + 1
|
||||
|
||||
|
||||
def model_box_to_image(box, size, image_size, margin=1):
|
||||
"""An S x S model-space box as an image-space region, ``margin`` model pixels wider on each side, in the image."""
|
||||
height, width = image_size
|
||||
sx, sy = width / size, height / size
|
||||
x1, y1, x2, y2 = box
|
||||
return (max(0, int((x1 - margin) * sx)), max(0, int((y1 - margin) * sy)),
|
||||
min(width, int(math.ceil((x2 + margin) * sx))), min(height, int(math.ceil((y2 + margin) * sy))))
|
||||
|
||||
|
||||
def _fit_span(center, size, limit):
|
||||
size = int(round(min(size, limit)))
|
||||
start = int(round(center - size / 2.0))
|
||||
start = min(max(0, start), limit - size)
|
||||
return start, start + size
|
||||
|
||||
|
||||
def crop_region(box, image_size):
|
||||
"""Pass-2 crop around an image-space box: a margin on each side, widened to at most 2:1, kept inside the image."""
|
||||
height, width = image_size
|
||||
x1, y1, x2, y2 = box
|
||||
margin = max(_CROP_MIN_MARGIN, _CROP_MARGIN * max(x2 - x1, y2 - y1))
|
||||
crop_w, crop_h = (x2 - x1) + 2 * margin, (y2 - y1) + 2 * margin
|
||||
crop_w, crop_h = max(crop_w, crop_h / _CROP_MAX_ASPECT), max(crop_h, crop_w / _CROP_MAX_ASPECT)
|
||||
left, right = _fit_span((x1 + x2) / 2.0, crop_w, width)
|
||||
top, bottom = _fit_span((y1 + y2) / 2.0, crop_h, height)
|
||||
# the box itself always stays inside, whatever the rounding did
|
||||
return min(left, x1), min(top, y1), max(right, x2), max(bottom, y2)
|
||||
|
||||
|
||||
def drop_stray_parts(refined, coarse):
|
||||
"""``refined`` without the connected parts that do not touch ``coarse`` at all (specks around the object)."""
|
||||
count, labels = cv2.connectedComponents(refined.astype(np.uint8), connectivity=8)
|
||||
if count <= 2:
|
||||
return refined
|
||||
touching = np.bincount(labels[coarse], minlength=count) > 0
|
||||
touching[0] = False
|
||||
return touching[labels]
|
||||
|
||||
|
||||
def close_up_agrees(refined, coarse, tolerance):
|
||||
"""True when the close-up mask ``refined`` shows the same object as the pass-1 mask ``coarse`` (bool arrays).
|
||||
|
||||
The pass-1 mask is blurred by up to ``tolerance`` px, so plain IoU would turn down a sharp mask of a thin pole or
|
||||
cable. Instead most of the close-up must lie within ``tolerance`` of the pass-1 mask, and it must cover most of
|
||||
the pass-1 core (pixels deeper than twice ``tolerance`` inside it); thin objects have no core and need only the
|
||||
first. Large masks are compared at a reduced scale.
|
||||
"""
|
||||
if not refined.any() or not coarse.any():
|
||||
return False
|
||||
step = max(1, int(math.sqrt(refined.size / _AGREE_PIXELS)))
|
||||
if step > 1 and not refined[::step, ::step].any():
|
||||
step = 1 # something thinner than the step: compare at full scale
|
||||
refined, coarse = refined[::step, ::step], coarse[::step, ::step]
|
||||
tolerance = max(1.0, tolerance / step)
|
||||
to_coarse = cv2.distanceTransform((~coarse).astype(np.uint8), cv2.DIST_L2, 3)
|
||||
inside = np.count_nonzero(refined & (to_coarse <= tolerance)) / np.count_nonzero(refined)
|
||||
core = cv2.distanceTransform(coarse.astype(np.uint8), cv2.DIST_L2, 3) > 2.0 * tolerance
|
||||
covered = np.count_nonzero(refined & core) / np.count_nonzero(core) if core.any() else 1.0
|
||||
return inside >= REFINE_MIN_INSIDE and covered >= REFINE_MIN_CORE
|
||||
|
||||
|
||||
def _box_overlap(a, b):
|
||||
"""Intersection box of two (x1, y1, x2, y2) boxes, or None."""
|
||||
x1, y1, x2, y2 = max(a[0], b[0]), max(a[1], b[1]), min(a[2], b[2]), min(a[3], b[3])
|
||||
return (x1, y1, x2, y2) if x1 < x2 and y1 < y2 else None
|
||||
|
||||
|
||||
def find_instances(processor, state, concept, max_instances=0, threshold=0.0, misses=None):
|
||||
"""[(score, logits [S, S], model-space box)] for one concept on an encoded image, best first, duplicates dropped.
|
||||
|
||||
Candidates scoring under ``threshold`` are left out; the best of them goes to ``misses[concept]`` when a dict is
|
||||
given, so the node can say how close a concept it did not find came. The logits are extended to the border of
|
||||
the view first (SAM 3's masks fade out there, see extend_to_border).
|
||||
"""
|
||||
processor.reset_all_prompts(state)
|
||||
output = processor.set_text_prompt(concept, state)
|
||||
masks, scores = output["masks_logits"], output["scores"]
|
||||
if masks is None or len(scores) == 0:
|
||||
return []
|
||||
scores = scores.float().flatten()
|
||||
under = scores <= threshold # SAM 3 itself keeps only scores above the threshold
|
||||
if misses is not None and bool(under.any()):
|
||||
misses[concept] = max(misses.get(concept, 0.0), float(scores[under].max()))
|
||||
band = border_band(masks.shape[-1])
|
||||
found, kept = [], [] # kept: (bool mask, box, area) of accepted instances
|
||||
for i in torch.argsort(scores, descending=True, stable=True).tolist():
|
||||
if float(scores[i]) <= threshold:
|
||||
break
|
||||
logits = extend_to_border(to_logits(masks[i].reshape(masks.shape[-2], masks.shape[-1])), band, band)
|
||||
inside = logits > 0
|
||||
box = mask_box(inside)
|
||||
if box is None:
|
||||
continue
|
||||
area = int(inside.sum())
|
||||
duplicate = False
|
||||
for other, other_box, other_area in kept:
|
||||
overlap = _box_overlap(box, other_box)
|
||||
if overlap is None:
|
||||
continue
|
||||
ox1, oy1, ox2, oy2 = overlap
|
||||
inter = int((inside[oy1:oy2, ox1:ox2] & other[oy1:oy2, ox1:ox2]).sum())
|
||||
if inter / max(1, area + other_area - inter) >= DEDUP_IOU:
|
||||
duplicate = True
|
||||
break
|
||||
if duplicate:
|
||||
continue
|
||||
kept.append((inside, box, area))
|
||||
found.append((float(scores[i]), logits, box))
|
||||
if max_instances and len(found) >= max_instances:
|
||||
break
|
||||
return found
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Segmenting
|
||||
# ---------------------------------------------------------------------------
|
||||
def _hint_size(model):
|
||||
"""Side of the low-resolution mask the box-prompt decoder takes as a hint (288 for SAM 3)."""
|
||||
encoder = getattr(getattr(getattr(model, "inst_interactive_predictor", None), "model", None),
|
||||
"sam_prompt_encoder", None)
|
||||
size = getattr(encoder, "mask_input_size", (288, 288))
|
||||
return int(size[0]), int(size[1])
|
||||
|
||||
|
||||
def _upsample(concept, score, logits, box, image_size):
|
||||
"""Pass 1 only: the instance's mask at full resolution, inside its own region."""
|
||||
region = model_box_to_image(box, logits.shape[-1], image_size)
|
||||
x1, y1, x2, y2 = region
|
||||
mask = sample_region(logits, image_size, region, (y2 - y1, x2 - x1)) > 0
|
||||
return make_segment(concept, score, mask, origin=(x1, y1)), False
|
||||
|
||||
|
||||
def _refine(model, processor, image, concept, score, logits, box, image_size, hint_size):
|
||||
"""Pass 2: segment the instance again on a full-resolution crop, the pass-1 mask as a hint.
|
||||
|
||||
Returns (segment, refined) where ``refined`` is False when the pass-1 mask was kept.
|
||||
"""
|
||||
target = model_box_to_image(box, logits.shape[-1], image_size)
|
||||
cx1, cy1, cx2, cy2 = crop_region(target, image_size)
|
||||
coarse = (sample_region(logits, image_size, (cx1, cy1, cx2, cy2), (cy2 - cy1, cx2 - cx1)) > 0).cpu().numpy()
|
||||
hint = sample_region(logits, image_size, (cx1, cy1, cx2, cy2), hint_size).clamp(-20.0, 20.0)
|
||||
state = processor.set_image(image[:, cy1:cy2, cx1:cx2])
|
||||
x1, y1, x2, y2 = target
|
||||
prompt_box = np.array([x1 - cx1, y1 - cy1, x2 - cx1, y2 - cy1], dtype=np.float32)
|
||||
masks, _, _ = model.predict_inst(state, box=prompt_box, mask_input=hint[None], multimask_output=False,
|
||||
return_logits=True)
|
||||
logits = np.asarray(masks, dtype=np.float32).reshape(-1, cy2 - cy1, cx2 - cx1)[0]
|
||||
# the close-up fades out along its own border too, which matters where that border is the image's
|
||||
height, width = image_size
|
||||
edges = [edge for edge, at_image_edge in (("top", cy1 == 0), ("bottom", cy2 == height), ("left", cx1 == 0),
|
||||
("right", cx2 == width)) if at_image_edge]
|
||||
view = int(getattr(processor, "resolution", _VIEW_SIZE))
|
||||
refined = extend_to_border(logits, border_band(cy2 - cy1, view, _CLOSE_UP_CELLS),
|
||||
border_band(cx2 - cx1, view, _CLOSE_UP_CELLS), edges) > 0
|
||||
refined = drop_stray_parts(refined, coarse)
|
||||
accepted = close_up_agrees(refined, coarse, max(image_size) / _PASS1_GRID + 2.0)
|
||||
return make_segment(concept, score, refined if accepted else coarse, origin=(cx1, cy1)), accepted
|
||||
|
||||
|
||||
def _image_chw(image):
|
||||
"""One ComfyUI image [H, W, C] -> float [3, H, W] in [0, 1]."""
|
||||
channels = image.shape[-1]
|
||||
if channels == 1:
|
||||
image = image.expand(-1, -1, 3)
|
||||
elif channels not in (3, 4):
|
||||
raise ValueError(f"[NH-Nodes] SAM 3 expects an IMAGE with 1, 3 or 4 channels, got {channels}")
|
||||
return image[..., :3].permute(2, 0, 1).float().clamp(0.0, 1.0)
|
||||
|
||||
|
||||
def segment_images(model, make_processor, images, concepts, threshold=0.5, detail_level="balanced",
|
||||
max_per_concept=0, device=None, interrupt=None, progress=None, misses=None):
|
||||
"""NH_SEGS (one SegsFrame per image) for ``concepts`` found in the ComfyUI IMAGE batch ``images``.
|
||||
|
||||
``make_processor(threshold)`` returns a Sam3Processor for ``model`` on ``device``; the model must already be
|
||||
there. It is asked for candidates down to NEAR_MISS, so that ``misses`` (a dict, when given) learns the best score
|
||||
under ``threshold`` of each concept. Instances come in concept order, best score first within a concept.
|
||||
``interrupt()`` runs before each concept and each close-up (it raises to stop), ``progress(done, total)`` after
|
||||
each concept of each image.
|
||||
"""
|
||||
device = device or _model_device(model)
|
||||
refine = detail_level != "fast"
|
||||
hint_size = _hint_size(model) if refine else None
|
||||
frames, kept_pass1, warned_wide = [], 0, False
|
||||
done, total = 0, len(images) * len(concepts)
|
||||
with torch.inference_mode(), _autocast(device):
|
||||
for image in images:
|
||||
height, width = int(image.shape[0]), int(image.shape[1])
|
||||
if not warned_wide and max(height, width) > _WIDE_ASPECT * min(height, width):
|
||||
warned_wide = True
|
||||
print(f"[NH-Nodes] SAM 3 sees the whole {width}x{height} picture squeezed into a square, so on images "
|
||||
"wider than 4:1 it can miss small things; split such a picture into tiles for better results")
|
||||
pixels = _image_chw(image).to(device)
|
||||
processor = make_processor(min(threshold, NEAR_MISS))
|
||||
size = int(getattr(processor, "resolution", 1008))
|
||||
square = F.interpolate(pixels[None], size=(size, size), mode="bilinear", antialias=True,
|
||||
align_corners=False)[0].clamp_(0.0, 1.0)
|
||||
state = processor.set_image(square)
|
||||
segments = []
|
||||
for concept in concepts:
|
||||
if interrupt is not None:
|
||||
interrupt()
|
||||
for score, logits, box in find_instances(processor, state, concept, max_per_concept, threshold,
|
||||
misses):
|
||||
if refine:
|
||||
if interrupt is not None:
|
||||
interrupt()
|
||||
seg, refined = _refine(model, processor, pixels, concept, score, logits, box,
|
||||
(height, width), hint_size)
|
||||
kept_pass1 += 0 if refined else 1
|
||||
else:
|
||||
seg, _ = _upsample(concept, score, logits, box, (height, width))
|
||||
if seg is not None:
|
||||
segments.append(seg)
|
||||
done += 1
|
||||
if progress is not None:
|
||||
progress(done, total)
|
||||
frames.append(SegsFrame(height, width, tuple(segments)))
|
||||
del pixels, square, state
|
||||
if kept_pass1:
|
||||
print(f"[NH-Nodes] SAM 3: kept the first-pass mask for {kept_pass1} instance(s) whose close-up disagreed")
|
||||
return tuple(frames)
|
||||
|
||||
|
||||
def segment(images, concepts, threshold=0.5, detail_level="balanced", max_per_concept=0, checkpoint=DEFAULT_CHECKPOINT,
|
||||
keep_model_loaded=False, interrupt=None, progress=None, misses=None):
|
||||
"""Load SAM 3 and run ``segment_images`` on ComfyUI's GPU; the weights go back to RAM afterwards unless kept."""
|
||||
if images.shape[0] == 0:
|
||||
raise ValueError("[NH-Nodes] SAM 3 got an empty image batch")
|
||||
model = load_model(checkpoint)
|
||||
_, processor_cls = ensure_sam3()
|
||||
device = sam3_device()
|
||||
try:
|
||||
move_model(model, device)
|
||||
return segment_images(
|
||||
model, lambda value: processor_cls(model, device=device, confidence_threshold=value), images, concepts,
|
||||
threshold, detail_level, max_per_concept, device, interrupt, progress, misses,
|
||||
)
|
||||
finally:
|
||||
if not keep_model_loaded:
|
||||
offload(model)
|
||||
@@ -0,0 +1,79 @@
|
||||
# Sapiens2 License
|
||||
|
||||
## Definitions
|
||||
|
||||
**"Agreement"** means the terms and conditions for use, reproduction, distribution and modification of the Sapiens Materials set forth herein.
|
||||
|
||||
**"Sapiens Materials"** means, collectively, Documentation and the models, software and algorithms, including machine-learning model code, trained model weights, inference-enabling code, training-enabling code, fine-tuning enabling code, and other elements of the foregoing distributed by Meta and made available under this Agreement.
|
||||
|
||||
**"Documentation"** means the specifications, manuals and documentation accompanying Sapiens Materials distributed by Meta.
|
||||
|
||||
**"Licensee"** or **"you"** means you, or your employer or any other person or entity (if you are entering into this Agreement on such person or entity's behalf), of the age required under applicable laws, rules or regulations to provide legal consent and that has legal authority to bind your employer or such other person or entity if you are entering in this Agreement on their behalf.
|
||||
|
||||
**"Meta"** or **"we"** means Meta Platforms Ireland Limited (if you are located in or, if you are an entity, your principal place of business is in the EEA or Switzerland) or Meta Platforms, Inc. (if you are located outside of the EEA or Switzerland).
|
||||
|
||||
**"Sanctions"** means any economic or trade sanctions or restrictions administered or enforced by the United States (including the Office of Foreign Assets Control of the U.S. Department of the Treasury ("OFAC"), the U.S. Department of State and the U.S. Department of Commerce), the United Nations, the European Union, or the United Kingdom.
|
||||
|
||||
**"Trade Controls"** means any of the following: Sanctions and applicable export and import controls.
|
||||
|
||||
By clicking **"I Accept"** below or by using or distributing any portion or element of the Sapiens Materials, you agree to be bound by this Agreement.
|
||||
|
||||
## 1. License Rights and Redistribution
|
||||
|
||||
**a. Grant of Rights.** You are granted a non-exclusive, worldwide, non-transferable and royalty-free limited license under Meta's intellectual property or other rights owned by Meta embodied in the Sapiens Materials to use, reproduce, distribute, copy, create derivative works of, and make modifications to the Sapiens Materials.
|
||||
|
||||
**b. Redistribution and Use.**
|
||||
|
||||
i. Distribution of Sapiens Materials, and any derivative works thereof, are subject to the terms of this Agreement. If you distribute or make the Sapiens Materials, or any derivative works thereof, available to a third party, you may only do so under the terms of this Agreement and you shall provide a copy of this Agreement with any such Sapiens Materials.
|
||||
|
||||
ii. If you submit for publication the results of research you perform on, using, or otherwise in connection with Sapiens Materials, you must acknowledge the use of Sapiens Materials in your publication.
|
||||
|
||||
iii. Your use of the Sapiens Materials must comply with applicable laws and regulations, including Trade Control Laws and applicable privacy, data protection and security laws, rules, regulations, directives, or governmental requirements including the General Data Privacy Regulation (Regulation (EU) 2016/679), the California Consumer Privacy Act, U.S. data security regulations set forth at 28 C.F.R. Part 202, the Lei Geral de Proteção de Dados, and any and all laws governing the processing of biometric information, as well as all amendments and successor laws to any of the foregoing.
|
||||
|
||||
iv. Your use of the Sapiens Materials will not involve or encourage others to reverse engineer, decompile or discover the underlying components of the Sapiens Materials.
|
||||
|
||||
v. You are not the target of Trade Controls and your use of Sapiens Materials must comply with Trade Controls.
|
||||
|
||||
vi. Your use of the Sapiens Materials will not involve or encourage others to use for:
|
||||
|
||||
- (i) for purposes of surveillance, including any research or development relating to surveillance;
|
||||
- (ii) for biometric processing;
|
||||
- (iii) in any manner that infringes, misappropriates, or otherwise violates any third-party rights;
|
||||
- (iv) for attempting to identify or re-identify any individual included in the Sapiens Materials;
|
||||
- (v) to create deepfakes, surveillance, or other misleading, deceptive, or inappropriate content including but not limited to impersonating any individual or facilitating false online engagement;
|
||||
- (vi) to collect, process, disclose, generate, or infer health, demographic, or other sensitive personal or private information about individuals without rights and consents required by applicable laws;
|
||||
- (vii) in a pornographic or defamatory manner;
|
||||
- (viii) to engage in the unauthorized or unlicensed practice of any profession including but not limited to financial, legal, medical/health, or related professional practices; or
|
||||
- (ix) to engage in, promote, incite, facilitate, or assist in the planning or development of any activities subject to the International Traffic in Arms Regulations (ITAR) or end uses prohibited by Trade Controls or any other activities that present a risk of death or bodily harm to individuals, including those related to military or warfare purposes, nuclear industries or applications, espionage, the development or use of guns or illegal weapons or illegal drugs, and the operation of critical infrastructure or machinery.
|
||||
|
||||
**c.** If Meta reasonably believes, as determined by Meta in its sole discretion, that you are or are likely to be in violation of the terms of this Agreement, Meta or Meta's designee, at Meta's sole expense, may audit your use, storage, and distribution of the Sapiens Materials including, without limitation, any and all records and files associated with the Licensed Content and this Agreement. You hereby agree to cooperate with such an audit. Meta may also require You to delete all copies of the Sapiens Materials (in whole or in part) in Your possession and control. You will promptly comply with any and all such requests. Upon Meta's request, You shall provide Meta with written confirmation of Your compliance with such requirement.
|
||||
|
||||
## 2. User Support
|
||||
|
||||
Your use of the Sapiens Materials is done at your own discretion; Meta does not process any information nor provide any service in relation to such use. Meta is under no obligation to provide any support services for the Sapiens Materials. Any support provided is "as is", "with all faults", and without warranty of any kind.
|
||||
|
||||
## 3. Disclaimer of Warranty
|
||||
|
||||
UNLESS REQUIRED BY APPLICABLE LAW, THE SAPIENS MATERIALS AND ANY OUTPUT AND RESULTS THEREFROM ARE PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, AND META DISCLAIMS ALL WARRANTIES OF ANY KIND, BOTH EXPRESS AND IMPLIED, INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY, OR FITNESS FOR A PARTICULAR PURPOSE. YOU ARE SOLELY RESPONSIBLE FOR DETERMINING THE APPROPRIATENESS OF USING OR REDISTRIBUTING THE SAPIENS MATERIALS AND ASSUME ANY RISKS ASSOCIATED WITH YOUR USE OF THE SAPIENS MATERIALS AND ANY OUTPUT AND RESULTS.
|
||||
|
||||
## 4. Limitation of Liability
|
||||
|
||||
IN NO EVENT WILL META OR ITS AFFILIATES BE LIABLE UNDER ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, TORT, NEGLIGENCE, PRODUCTS LIABILITY, OR OTHERWISE, ARISING OUT OF THIS AGREEMENT, FOR ANY LOST PROFITS OR ANY DIRECT OR INDIRECT, SPECIAL, CONSEQUENTIAL, INCIDENTAL, EXEMPLARY OR PUNITIVE DAMAGES, EVEN IF META OR ITS AFFILIATES HAVE BEEN ADVISED OF THE POSSIBILITY OF ANY OF THE FOREGOING.
|
||||
|
||||
## 5. Intellectual Property
|
||||
|
||||
**a.** Subject to Meta's ownership of Sapiens Materials and derivatives made by or for Meta, with respect to any derivative works and modifications of the Sapiens Materials that are made by you, as between you and Meta, you are and will be the owner of such derivative works and modifications.
|
||||
|
||||
**b.** If you institute litigation or other proceedings against Meta or any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Sapiens Materials, outputs or results, or any portion of any of the foregoing, constitutes infringement of intellectual property or other rights owned or licensable by you, then any licenses granted to you under this Agreement shall terminate as of the date such litigation or claim is filed or instituted. You will indemnify and hold harmless Meta from and against any claim by any third party arising out of or related to your use or distribution of the Sapiens Materials.
|
||||
|
||||
## 6. Term and Termination
|
||||
|
||||
The term of this Agreement will commence upon your acceptance of this Agreement or access to the Sapiens Materials and will continue in full force and effect until terminated in accordance with the terms and conditions herein. Meta may terminate this Agreement if you are in breach of any term or condition of this Agreement. Upon termination of this Agreement, you shall delete and cease use of the Sapiens Materials. Sections 3, 4 and 7 shall survive the termination of this Agreement.
|
||||
|
||||
## 7. Governing Law and Jurisdiction
|
||||
|
||||
This Agreement will be governed and construed under the laws of the State of California without regard to choice of law principles, and the UN Convention on Contracts for the International Sale of Goods does not apply to this Agreement. The courts of California shall have exclusive jurisdiction of any dispute arising out of this Agreement.
|
||||
|
||||
## 8. Modifications and Amendments
|
||||
|
||||
Meta may modify this Agreement from time to time; provided that they are similar in spirit to the current version of the Agreement, but may differ in detail to address new problems or concerns. All such changes will be effective immediately. Your continued use of the Sapiens Materials after any modification to this Agreement constitutes your agreement to such modification. Except as provided in this Agreement, no modification or addition to any provision of this Agreement will be binding unless it is in writing and signed by an authorized representative of both you and Meta.
|
||||
@@ -0,0 +1 @@
|
||||
"""Meta Sapiens2 model code, vendored (see LICENSE.md here and backbone.py)."""
|
||||
@@ -0,0 +1,915 @@
|
||||
# Vendored from https://github.com/facebookresearch/sapiens2 (commit 7e5bae88456ac418ff0e58e74106c9fe192055d4),
|
||||
# file sapiens/backbones/standalone/sapiens2.py, under the Sapiens2 License (LICENSE.md in this folder).
|
||||
# Modified by NH-Nodes: this note, the numpy import that the training-only coordinate jitter uses, and the
|
||||
# line below, which keeps the linter off this upstream file.
|
||||
# ruff: noqa
|
||||
|
||||
# Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
# All rights reserved.
|
||||
#
|
||||
# This source code is licensed under the license found in the
|
||||
# LICENSE file in the root directory of this source tree.
|
||||
|
||||
import math
|
||||
from typing import Any, Dict, List, Literal, Optional, Sequence, Tuple, Union
|
||||
import numpy as np
|
||||
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
import torch.nn.functional as F
|
||||
from torch import Tensor
|
||||
from torch.nn.init import trunc_normal_
|
||||
from torch.utils.checkpoint import checkpoint
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
def to_2tuple(x):
|
||||
if isinstance(x, (str, bytes)):
|
||||
return (x, x)
|
||||
if isinstance(x, Sequence):
|
||||
x = tuple(x)
|
||||
if len(x) == 2:
|
||||
return x
|
||||
raise ValueError("Expected scalar or length-2 iterable")
|
||||
return (x, x)
|
||||
|
||||
|
||||
class RopePositionEmbedding(nn.Module):
|
||||
def __init__(
|
||||
self,
|
||||
embed_dim: int,
|
||||
*,
|
||||
num_heads: int,
|
||||
base: float | None = 100.0,
|
||||
min_period: float | None = None,
|
||||
max_period: float | None = None,
|
||||
normalize_coords: Literal["min", "max", "separate"] = "separate",
|
||||
shift_coords: float | None = None,
|
||||
jitter_coords: float | None = None,
|
||||
rescale_coords: float | None = None,
|
||||
dtype: torch.dtype | None = None,
|
||||
device: torch.device | None = None,
|
||||
):
|
||||
super().__init__()
|
||||
assert embed_dim % (4 * num_heads) == 0
|
||||
both_periods = min_period is not None and max_period is not None
|
||||
if (base is None and not both_periods) or (base is not None and both_periods):
|
||||
raise ValueError(
|
||||
"Either `base` or `min_period`+`max_period` must be provided."
|
||||
)
|
||||
|
||||
D_head = embed_dim // num_heads
|
||||
self.base = base
|
||||
self.min_period = min_period
|
||||
self.max_period = max_period
|
||||
self.D_head = D_head
|
||||
self.normalize_coords = normalize_coords
|
||||
self.shift_coords = shift_coords
|
||||
self.jitter_coords = jitter_coords
|
||||
self.rescale_coords = rescale_coords
|
||||
|
||||
# Needs persistent=True because we do teacher.load_state_dict(student.state_dict()) to initialize the teacher
|
||||
self.dtype = dtype or torch.float32 # Don't rely on self.periods.dtype
|
||||
self.register_buffer(
|
||||
"periods",
|
||||
torch.empty(D_head // 4, device=device, dtype=self.dtype),
|
||||
persistent=True,
|
||||
)
|
||||
self._init_weights()
|
||||
|
||||
def forward(self, *, H: int, W: int) -> tuple[Tensor, Tensor]:
|
||||
device = self.periods.device
|
||||
dtype = self.dtype
|
||||
dd = {"device": device, "dtype": dtype}
|
||||
# Prepare coords in range [-1, +1]
|
||||
if self.normalize_coords == "max":
|
||||
max_HW = max(H, W)
|
||||
coords_h = torch.arange(0.5, H, **dd) / max_HW # [H]
|
||||
coords_w = torch.arange(0.5, W, **dd) / max_HW # [W]
|
||||
elif self.normalize_coords == "min":
|
||||
min_HW = min(H, W)
|
||||
coords_h = torch.arange(0.5, H, **dd) / min_HW # [H]
|
||||
coords_w = torch.arange(0.5, W, **dd) / min_HW # [W]
|
||||
elif self.normalize_coords == "separate":
|
||||
coords_h = torch.arange(0.5, H, **dd) / H # [H]
|
||||
coords_w = torch.arange(0.5, W, **dd) / W # [W]
|
||||
else:
|
||||
raise ValueError(f"Unknown normalize_coords: {self.normalize_coords}")
|
||||
coords = torch.stack(
|
||||
torch.meshgrid(coords_h, coords_w, indexing="ij"), dim=-1
|
||||
) # [H, W, 2]
|
||||
coords = coords.flatten(0, 1) # [HW, 2]
|
||||
coords = 2.0 * coords - 1.0 # Shift range [0, 1] to [-1, +1]
|
||||
|
||||
# Shift coords by adding a uniform value in [-shift, shift]
|
||||
if self.training and self.shift_coords is not None:
|
||||
shift_hw = torch.empty(2, **dd).uniform_(
|
||||
-self.shift_coords, self.shift_coords
|
||||
)
|
||||
coords += shift_hw[None, :]
|
||||
|
||||
# Jitter coords by multiplying the range [-1, 1] by a log-uniform value in [1/jitter, jitter]
|
||||
if self.training and self.jitter_coords is not None:
|
||||
jitter_max = np.log(self.jitter_coords)
|
||||
jitter_min = -jitter_max
|
||||
jitter_hw = torch.empty(2, **dd).uniform_(jitter_min, jitter_max).exp()
|
||||
coords *= jitter_hw[None, :]
|
||||
|
||||
# Rescale coords by multiplying the range [-1, 1] by a log-uniform value in [1/rescale, rescale]
|
||||
if self.training and self.rescale_coords is not None:
|
||||
rescale_max = np.log(self.rescale_coords)
|
||||
rescale_min = -rescale_max
|
||||
rescale_hw = torch.empty(1, **dd).uniform_(rescale_min, rescale_max).exp()
|
||||
coords *= rescale_hw
|
||||
|
||||
# Prepare angles and sin/cos
|
||||
angles = (
|
||||
2 * math.pi * coords[:, :, None] / self.periods[None, None, :]
|
||||
) # [HW, 2, D//4]
|
||||
angles = angles.flatten(1, 2) # [HW, D//2]
|
||||
angles = angles.tile(2) # [HW, D]
|
||||
cos = torch.cos(angles) # [HW, D]
|
||||
sin = torch.sin(angles) # [HW, D]
|
||||
|
||||
return (sin, cos) # 2 * [HW, D]
|
||||
|
||||
def _init_weights(self):
|
||||
device = self.periods.device
|
||||
dtype = self.dtype
|
||||
if self.base is not None:
|
||||
periods = self.base ** (
|
||||
2
|
||||
* torch.arange(self.D_head // 4, device=device, dtype=dtype)
|
||||
/ (self.D_head // 2)
|
||||
) # [D//4]
|
||||
else:
|
||||
base = self.max_period / self.min_period
|
||||
exponents = torch.linspace(
|
||||
0, 1, self.D_head // 4, device=device, dtype=dtype
|
||||
) # [D//4] range [0, 1]
|
||||
periods = base**exponents # range [1, max_period / min_period]
|
||||
periods = periods / base # range [min_period / max_period, 1]
|
||||
periods = periods * self.max_period # range [min_period, max_period]
|
||||
self.periods.data = periods
|
||||
|
||||
|
||||
# -------------------------------------------------------------------------------
|
||||
class Tokenizer(nn.Module):
|
||||
"""Stacked window self‑attention that emits one token per window
|
||||
by re‑using TransformerEncoderLayer blocks."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
embed_dims: int,
|
||||
window_size: int = 4,
|
||||
num_heads: int = 4,
|
||||
num_tokenizer_layers: int = 1,
|
||||
qkv_bias: bool = True,
|
||||
use_qk_norm: bool = False,
|
||||
chunk_size: int = 1024, # max windows per chunk
|
||||
):
|
||||
super().__init__()
|
||||
self.ws = window_size
|
||||
self.chunk_size = chunk_size
|
||||
|
||||
# local absolute positional embeddings for [CLS] + patch tokens
|
||||
self.local_pos_embed = nn.Parameter(
|
||||
torch.zeros(1, 1 + window_size * window_size, embed_dims)
|
||||
)
|
||||
trunc_normal_(self.local_pos_embed, std=0.02)
|
||||
|
||||
# build N identical TransformerEncoderLayer blocks
|
||||
self.blocks = nn.ModuleList(
|
||||
[
|
||||
TransformerEncoderLayer2(
|
||||
embed_dims=embed_dims,
|
||||
num_heads=num_heads,
|
||||
feedforward_channels=embed_dims * 4, # standard FFN size
|
||||
qkv_bias=qkv_bias,
|
||||
use_qk_norm=use_qk_norm,
|
||||
)
|
||||
for _ in range(num_tokenizer_layers)
|
||||
]
|
||||
)
|
||||
|
||||
# shared CLS token for pooling
|
||||
self.w_cls = nn.Parameter(torch.zeros(1, 1, embed_dims))
|
||||
trunc_normal_(self.w_cls, std=0.02)
|
||||
|
||||
def forward(
|
||||
self,
|
||||
x: torch.Tensor,
|
||||
hw: Tuple[int, int],
|
||||
) -> Tuple[torch.Tensor, Tuple[int, int]]:
|
||||
"""Args:
|
||||
x : B, N, C (N = H*W)
|
||||
hw : (H, W) before reduction
|
||||
Returns:
|
||||
x_ : B, (H/ws)*(W/ws), C
|
||||
hw_: (H/ws, W/ws)
|
||||
"""
|
||||
B, N, C = x.shape
|
||||
H, W = hw
|
||||
ws = self.ws
|
||||
assert H % ws == 0 and W % ws == 0, (
|
||||
f"Image size {H}×{W} must be divisible by window {ws}."
|
||||
)
|
||||
|
||||
# reshape tokens → non‑overlapping windows
|
||||
x = x.view(B, H, W, C)
|
||||
|
||||
ph, pw = H // ws, W // ws ## ints in eager mode
|
||||
ph, pw = int(ph), int(pw) ## ints in scripting mode
|
||||
x = x.view(B, ph, ws, pw, ws, C) # B, H/ws, ws, W/ws, ws, C
|
||||
x = x.permute(0, 1, 3, 2, 4, 5) # B, H/ws, W/ws, ws, ws, C
|
||||
x = x.contiguous().view(B * ph * pw, ws * ws, C) # (B*H/ws*W/ws), ws², C))
|
||||
|
||||
total_windows = x.size(0)
|
||||
chunk_size = int(min(self.chunk_size, total_windows))
|
||||
token_out = x.new_empty(total_windows, C)
|
||||
|
||||
use_ckpt = self.training and torch.is_grad_enabled()
|
||||
|
||||
def _run_blocks(t: torch.Tensor) -> torch.Tensor:
|
||||
for blk in self.blocks:
|
||||
t = blk(t)
|
||||
return t
|
||||
|
||||
for i in range(0, total_windows, chunk_size):
|
||||
chunk = x[i : i + chunk_size] # (m, ws², C)
|
||||
m = chunk.size(0)
|
||||
cls = self.w_cls.expand(m, -1, -1) # (m, 1, C)
|
||||
chunk = torch.cat([cls, chunk], dim=1) # (m, 1+ws², C)
|
||||
chunk = chunk + self.local_pos_embed # add local PE
|
||||
|
||||
if use_ckpt:
|
||||
chunk = checkpoint(_run_blocks, chunk, use_reentrant=False)
|
||||
else:
|
||||
chunk = _run_blocks(chunk)
|
||||
|
||||
token_out[i : i + m] = chunk[:, 0] # take CLS out
|
||||
|
||||
token = token_out.view(B, ph * pw, C) # (B, (H/ws)*(W
|
||||
return token, (ph, pw)
|
||||
|
||||
|
||||
# -------------------------------------------------------------------------------
|
||||
class GroupedQueryAttention(nn.Module):
|
||||
def __init__(
|
||||
self,
|
||||
embed_dims,
|
||||
num_heads,
|
||||
num_kv_heads=None,
|
||||
input_dims=None,
|
||||
attn_drop=0.0,
|
||||
proj_drop=0.0,
|
||||
qkv_bias=True,
|
||||
qk_scale=None,
|
||||
proj_bias=True,
|
||||
use_qk_norm=True,
|
||||
v_shortcut=False,
|
||||
layer_scale_init_value=0.0,
|
||||
):
|
||||
super().__init__()
|
||||
# Core dims
|
||||
self.embed_dims = embed_dims
|
||||
self.num_heads = num_heads
|
||||
self.num_kv_heads = num_kv_heads or num_heads
|
||||
assert self.num_heads % self.num_kv_heads == 0, (
|
||||
"num_kv_heads must divide num_heads"
|
||||
)
|
||||
self.head_dim = embed_dims // num_heads
|
||||
self.input_dims = input_dims or embed_dims
|
||||
# Features
|
||||
self.attn_drop = attn_drop
|
||||
self.v_shortcut = v_shortcut
|
||||
self.use_qk_norm = use_qk_norm
|
||||
|
||||
# Attention operation selection
|
||||
if qk_scale is not None:
|
||||
scale = qk_scale
|
||||
else:
|
||||
scale = self.head_dim**-0.5
|
||||
|
||||
assert qk_scale is None, "qk_scale is not supported"
|
||||
self.attn_op = F.scaled_dot_product_attention
|
||||
|
||||
# Q/K/V projections
|
||||
self.wq = nn.Linear(self.input_dims, embed_dims, bias=qkv_bias)
|
||||
self.wk = nn.Linear(
|
||||
self.input_dims, self.num_kv_heads * self.head_dim, bias=qkv_bias
|
||||
)
|
||||
self.wv = nn.Linear(
|
||||
self.input_dims, self.num_kv_heads * self.head_dim, bias=qkv_bias
|
||||
)
|
||||
|
||||
if self.use_qk_norm:
|
||||
self.q_norm = nn.RMSNorm(self.head_dim, eps=1e-6)
|
||||
self.k_norm = nn.RMSNorm(self.head_dim, eps=1e-6)
|
||||
|
||||
# Output projection + dropout
|
||||
self.proj = nn.Linear(embed_dims, embed_dims, bias=proj_bias)
|
||||
self.proj_drop = nn.Dropout(proj_drop)
|
||||
|
||||
# Optional LayerScale
|
||||
if layer_scale_init_value > 0:
|
||||
self.gamma = LayerScale(embed_dims, scale=layer_scale_init_value)
|
||||
else:
|
||||
self.gamma = nn.Identity()
|
||||
|
||||
def apply_rope(
|
||||
self, q: Tensor, k: Tensor, rope: Tensor | Tuple[Tensor, Tensor]
|
||||
) -> Tuple[Tensor, Tensor]:
|
||||
# All operations will use the dtype of rope, the output is cast back to the dtype of q and k
|
||||
q_dtype = q.dtype
|
||||
k_dtype = k.dtype
|
||||
sin, cos = rope
|
||||
rope_dtype = sin.dtype
|
||||
q = q.to(dtype=rope_dtype)
|
||||
k = k.to(dtype=rope_dtype)
|
||||
N = q.shape[-2]
|
||||
prefix = N - sin.shape[-2] ## extra tokens
|
||||
assert prefix >= 0
|
||||
q_prefix = q[:, :, :prefix, :]
|
||||
q = self._rope_apply(q[:, :, prefix:, :], sin, cos) # [B, head, hw, D//head]
|
||||
q = torch.cat((q_prefix, q), dim=-2) # [B, head, N, D//head]
|
||||
k_prefix = k[:, :, :prefix, :]
|
||||
k = self._rope_apply(k[:, :, prefix:, :], sin, cos) # [B, head, hw, D//head]
|
||||
k = torch.cat((k_prefix, k), dim=-2) # [B, head, N, D//head]
|
||||
q = q.to(dtype=q_dtype)
|
||||
k = k.to(dtype=k_dtype)
|
||||
return q, k
|
||||
|
||||
def _rope_rotate_half(self, x: Tensor) -> Tensor:
|
||||
# x: [ x0 x1 x2 x3 x4 x5]
|
||||
# out: [-x3 -x4 -x5 x0 x1 x2]
|
||||
x1, x2 = x.chunk(2, dim=-1)
|
||||
return torch.cat([-x2, x1], dim=-1)
|
||||
|
||||
def _rope_apply(self, x: Tensor, sin: Tensor, cos: Tensor) -> Tensor:
|
||||
# x: [..., D], eg [x0, x1, x2, x3, x4, x5]
|
||||
# sin: [..., D], eg [sin0, sin1, sin2, sin0, sin1, sin2]
|
||||
# cos: [..., D], eg [cos0, cos1, cos2, cos0, cos1, cos2]
|
||||
return (x * cos) + (self._rope_rotate_half(x) * sin)
|
||||
|
||||
def forward(self, x, rope=None):
|
||||
B, N, _ = x.shape
|
||||
# Q: (B, N, num_heads, head_dim)
|
||||
q = self.wq(x).view(B, N, self.num_heads, self.head_dim)
|
||||
# K/V: (B, N, num_kv_heads, head_dim)
|
||||
k = self.wk(x).view(B, N, self.num_kv_heads, self.head_dim)
|
||||
v = self.wv(x).view(B, N, self.num_kv_heads, self.head_dim)
|
||||
|
||||
# (B, heads, N, head_dim)
|
||||
q = q.permute(0, 2, 1, 3)
|
||||
k = k.permute(0, 2, 1, 3)
|
||||
v = v.permute(0, 2, 1, 3)
|
||||
|
||||
if self.use_qk_norm:
|
||||
q = self.q_norm(q)
|
||||
k = self.k_norm(k)
|
||||
|
||||
# Repeat KV heads if group ratio >1
|
||||
if self.num_kv_heads != self.num_heads:
|
||||
factor = self.num_heads // self.num_kv_heads
|
||||
k = k.repeat_interleave(factor, dim=1)
|
||||
v = v.repeat_interleave(factor, dim=1)
|
||||
|
||||
if rope is not None:
|
||||
q, k = self.apply_rope(q, k, rope)
|
||||
|
||||
# Scaled dot-product attention
|
||||
attn_out = self.attn_op(
|
||||
q, k, v, dropout_p=self.attn_drop if self.training else 0.0
|
||||
) # (B, num_heads, N, head_dim)
|
||||
|
||||
# Merge heads -> (B, N, embed_dims)
|
||||
out = attn_out.permute(0, 2, 1, 3).reshape(B, N, self.embed_dims)
|
||||
|
||||
# Output projection + drop + layer scale
|
||||
out = self.proj(out)
|
||||
out = self.gamma(self.proj_drop(out))
|
||||
|
||||
# Optional V-shortcut (only when MQA)
|
||||
if self.v_shortcut and self.num_kv_heads == 1:
|
||||
raise NotImplementedError
|
||||
return out
|
||||
|
||||
|
||||
# -------------------------------------------------------------------------------
|
||||
class TransformerEncoderLayer2(nn.Module):
|
||||
def __init__(
|
||||
self,
|
||||
embed_dims,
|
||||
num_heads,
|
||||
num_kv_heads=None,
|
||||
feedforward_channels=None,
|
||||
drop_rate=0.0,
|
||||
attn_drop_rate=0.0,
|
||||
layer_scale_init_value=0.0,
|
||||
use_qk_norm=True,
|
||||
qkv_bias=True,
|
||||
):
|
||||
super(TransformerEncoderLayer2, self).__init__()
|
||||
|
||||
self.embed_dims = embed_dims
|
||||
self.ln1 = nn.RMSNorm(self.embed_dims, eps=1e-6)
|
||||
self.attn = GroupedQueryAttention(
|
||||
embed_dims=embed_dims,
|
||||
num_heads=num_heads,
|
||||
num_kv_heads=num_kv_heads,
|
||||
attn_drop=attn_drop_rate,
|
||||
proj_drop=drop_rate,
|
||||
qkv_bias=qkv_bias,
|
||||
layer_scale_init_value=layer_scale_init_value,
|
||||
use_qk_norm=use_qk_norm,
|
||||
)
|
||||
|
||||
self.ln2 = nn.RMSNorm(self.embed_dims, eps=1e-6)
|
||||
self.ffn = SwiGLUFFN(
|
||||
embed_dims=embed_dims,
|
||||
feedforward_channels=feedforward_channels,
|
||||
)
|
||||
|
||||
@property
|
||||
def norm1(self):
|
||||
return self.ln1
|
||||
|
||||
@property
|
||||
def norm2(self):
|
||||
return self.ln2
|
||||
|
||||
def forward(self, x, rope=None):
|
||||
x = x + self.attn(self.ln1(x), rope=rope)
|
||||
x = self.ffn(self.ln2(x), identity=x)
|
||||
return x
|
||||
|
||||
|
||||
##-----------------------------------
|
||||
class Sapiens2(nn.Module):
|
||||
arch_zoo = {
|
||||
**dict.fromkeys(
|
||||
["sapiens2_0.1b"],
|
||||
{
|
||||
"embed_dims": 768,
|
||||
"num_layers": 12,
|
||||
"num_heads": 12,
|
||||
"feedforward_channels": 768 * 4,
|
||||
"num_tokenizer_layers": 2,
|
||||
},
|
||||
),
|
||||
**dict.fromkeys(
|
||||
["sapiens2_0.4b"],
|
||||
{
|
||||
"embed_dims": 1024,
|
||||
"num_layers": 24,
|
||||
"num_heads": 16,
|
||||
"feedforward_channels": 1024 * 4,
|
||||
"num_tokenizer_layers": 2,
|
||||
},
|
||||
),
|
||||
**dict.fromkeys(
|
||||
["sapiens2_0.8b"],
|
||||
{
|
||||
"embed_dims": 1280,
|
||||
"num_layers": 32,
|
||||
"num_heads": 16,
|
||||
"feedforward_channels": 1280 * 4,
|
||||
"num_tokenizer_layers": 3,
|
||||
},
|
||||
),
|
||||
**dict.fromkeys(
|
||||
["sapiens2_1b"],
|
||||
{
|
||||
"embed_dims": 1536,
|
||||
"num_layers": 40,
|
||||
"num_heads": 24,
|
||||
"feedforward_channels": 1536 * 4,
|
||||
"num_tokenizer_layers": 4,
|
||||
},
|
||||
),
|
||||
**dict.fromkeys(
|
||||
["sapiens2_5b"],
|
||||
{
|
||||
"embed_dims": 2432,
|
||||
"num_layers": 56,
|
||||
"num_heads": 32,
|
||||
"feedforward_channels": 2432 * 4,
|
||||
"num_tokenizer_layers": 6,
|
||||
},
|
||||
),
|
||||
}
|
||||
|
||||
num_extra_tokens = 1 # class token
|
||||
OUT_TYPES = {"raw", "cls_token", "featmap"}
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
arch="sapiens2_1b",
|
||||
img_size=(1024, 768),
|
||||
patch_size=16,
|
||||
in_channels=3,
|
||||
out_indices=-1,
|
||||
drop_rate=0.0,
|
||||
window_size=4,
|
||||
use_tokenizer=False, ## 4k resolution
|
||||
use_qk_norm=True,
|
||||
qkv_bias=True,
|
||||
final_norm=True,
|
||||
out_type="raw",
|
||||
with_cls_token=True,
|
||||
layer_scale_init_value=1e-4, ## non zero init to activate layerscale
|
||||
frozen_stages=-1,
|
||||
patch_cfg=dict(),
|
||||
layer_cfgs=dict(),
|
||||
pos_embed_rope_base: float = 100.0,
|
||||
pos_embed_rope_min_period: float | None = None,
|
||||
pos_embed_rope_max_period: float | None = None,
|
||||
pos_embed_rope_normalize_coords: Literal["min", "max", "separate"] = "separate",
|
||||
pos_embed_rope_shift_coords: float | None = None,
|
||||
pos_embed_rope_jitter_coords: float | None = None,
|
||||
pos_embed_rope_rescale_coords: float | None = None,
|
||||
pos_embed_rope_dtype: str = "bf16",
|
||||
n_storage_tokens: int = 8,
|
||||
):
|
||||
super().__init__()
|
||||
|
||||
arch = arch.lower()
|
||||
assert arch in set(self.arch_zoo), (
|
||||
f"Arch {arch} is not in default archs {set(self.arch_zoo)}"
|
||||
)
|
||||
self.arch_settings = self.arch_zoo[arch]
|
||||
|
||||
self.embed_dims = self.arch_settings["embed_dims"]
|
||||
self.num_layers = self.arch_settings["num_layers"]
|
||||
self.patch_size = patch_size
|
||||
|
||||
self.window_size = window_size
|
||||
img_size = to_2tuple(img_size)
|
||||
encoder_img_size = (
|
||||
(img_size[0] // window_size, img_size[1] // window_size)
|
||||
if use_tokenizer
|
||||
else img_size
|
||||
)
|
||||
self.img_size = to_2tuple(encoder_img_size)
|
||||
|
||||
# Set patch embedding
|
||||
_patch_cfg = dict(
|
||||
in_channels=in_channels,
|
||||
input_size=self.img_size,
|
||||
embed_dims=self.embed_dims,
|
||||
kernel_size=patch_size,
|
||||
stride=patch_size,
|
||||
bias=True,
|
||||
)
|
||||
_patch_cfg.update(patch_cfg)
|
||||
self.patch_embed = PatchEmbed(**_patch_cfg)
|
||||
self.patch_resolution = self.patch_embed.init_out_size
|
||||
num_patches = self.patch_resolution[0] * self.patch_resolution[1]
|
||||
|
||||
self.rope_embed = RopePositionEmbedding(
|
||||
embed_dim=self.embed_dims,
|
||||
num_heads=self.arch_settings["num_heads"],
|
||||
base=pos_embed_rope_base,
|
||||
min_period=pos_embed_rope_min_period,
|
||||
max_period=pos_embed_rope_max_period,
|
||||
normalize_coords=pos_embed_rope_normalize_coords,
|
||||
shift_coords=pos_embed_rope_shift_coords,
|
||||
jitter_coords=pos_embed_rope_jitter_coords,
|
||||
rescale_coords=pos_embed_rope_rescale_coords,
|
||||
dtype=torch.bfloat16 if pos_embed_rope_dtype == "bf16" else torch.float32,
|
||||
)
|
||||
|
||||
# Set out type
|
||||
if out_type not in self.OUT_TYPES:
|
||||
raise ValueError(
|
||||
f"Unsupported `out_type` {out_type}, please "
|
||||
f"choose from {self.OUT_TYPES}"
|
||||
)
|
||||
self.out_type = out_type
|
||||
|
||||
if use_tokenizer == True:
|
||||
self.tokenizer = Tokenizer(
|
||||
embed_dims=self.embed_dims,
|
||||
window_size=self.window_size,
|
||||
num_heads=self.arch_settings["num_heads"],
|
||||
num_tokenizer_layers=self.arch_settings["num_tokenizer_layers"],
|
||||
qkv_bias=True,
|
||||
use_qk_norm=False,
|
||||
)
|
||||
else:
|
||||
self.tokenizer = None
|
||||
|
||||
# Set cls + storage tokens
|
||||
self.with_cls_token = with_cls_token
|
||||
if with_cls_token:
|
||||
self.cls_token = nn.Parameter(torch.zeros(1, 1, self.embed_dims))
|
||||
elif out_type != "cls_token":
|
||||
self.cls_token = None
|
||||
self.num_extra_tokens = 0
|
||||
else:
|
||||
raise ValueError('with_cls_token must be True when `out_type="cls_token"`.')
|
||||
|
||||
## registers
|
||||
self.n_storage_tokens = int(n_storage_tokens)
|
||||
self.storage_tokens = (
|
||||
nn.Parameter(torch.zeros(1, self.n_storage_tokens, self.embed_dims))
|
||||
if self.n_storage_tokens > 0
|
||||
else None
|
||||
)
|
||||
# how many non-patch tokens are at the front
|
||||
self.num_extra_tokens = (
|
||||
1 if self.cls_token is not None else 0
|
||||
) + self.n_storage_tokens
|
||||
|
||||
if isinstance(out_indices, int):
|
||||
out_indices = [out_indices]
|
||||
assert isinstance(out_indices, Sequence), (
|
||||
f'"out_indices" must by a sequence or int, get {type(out_indices)} instead.'
|
||||
)
|
||||
for i, index in enumerate(out_indices):
|
||||
if index < 0:
|
||||
out_indices[i] = self.num_layers + index
|
||||
assert 0 <= out_indices[i] <= self.num_layers, (
|
||||
f"Invalid out_indices {index}"
|
||||
)
|
||||
self.out_indices = out_indices
|
||||
|
||||
self.blocks = nn.Sequential()
|
||||
if isinstance(layer_cfgs, dict):
|
||||
layer_cfgs = [layer_cfgs] * self.num_layers
|
||||
|
||||
mhsa_early, mhsa_late = 8, 8
|
||||
for i in range(self.num_layers):
|
||||
if i < mhsa_early or i >= self.num_layers - mhsa_late:
|
||||
num_kv_heads = None ## use MHSA
|
||||
else:
|
||||
num_kv_heads = self.arch_settings["num_heads"] // 2 # Use GQA
|
||||
|
||||
_layer_cfg = dict(
|
||||
embed_dims=self.embed_dims,
|
||||
num_heads=self.arch_settings["num_heads"],
|
||||
num_kv_heads=num_kv_heads,
|
||||
feedforward_channels=self.arch_settings["feedforward_channels"],
|
||||
use_qk_norm=use_qk_norm,
|
||||
layer_scale_init_value=layer_scale_init_value,
|
||||
drop_rate=drop_rate,
|
||||
qkv_bias=qkv_bias,
|
||||
)
|
||||
_layer_cfg.update(layer_cfgs[i])
|
||||
self.blocks.append(TransformerEncoderLayer2(**_layer_cfg))
|
||||
|
||||
self.frozen_stages = frozen_stages
|
||||
|
||||
self.final_norm = final_norm
|
||||
if final_norm:
|
||||
self.ln1 = nn.RMSNorm(self.embed_dims, eps=1e-6)
|
||||
|
||||
# freeze stages only when self.frozen_stages > 0
|
||||
if self.frozen_stages > 0:
|
||||
self._freeze_stages()
|
||||
|
||||
## load init weights
|
||||
self.init_weights()
|
||||
|
||||
return
|
||||
|
||||
def init_weights(self):
|
||||
# Initialize class token and storagr token embeddings
|
||||
if self.with_cls_token:
|
||||
trunc_normal_(self.cls_token, std=0.02)
|
||||
|
||||
if self.storage_tokens is not None:
|
||||
trunc_normal_(self.storage_tokens, std=0.02)
|
||||
|
||||
# Apply custom initialization to all submodules
|
||||
self.apply(self._init_weights)
|
||||
|
||||
def _init_weights(self, m):
|
||||
if isinstance(m, nn.Linear):
|
||||
# Use a truncated normal distribution for linear layer weights
|
||||
trunc_normal_(m.weight, std=0.02)
|
||||
if m.bias is not None:
|
||||
nn.init.constant_(m.bias, 0)
|
||||
|
||||
elif isinstance(m, (nn.LayerNorm, nn.RMSNorm)):
|
||||
# Initialize normalization layers to act as an identity function
|
||||
if hasattr(m, "bias") and m.bias is not None:
|
||||
nn.init.constant_(m.bias, 0)
|
||||
if hasattr(m, "weight") and m.weight is not None:
|
||||
nn.init.constant_(m.weight, 1.0)
|
||||
|
||||
elif isinstance(m, nn.Conv2d):
|
||||
# Initialize conv layer weights like linear layers
|
||||
trunc_normal_(m.weight, std=0.02)
|
||||
if m.bias is not None:
|
||||
nn.init.constant_(m.bias, 0)
|
||||
|
||||
def _freeze_stages(self):
|
||||
## freeze tokenizer
|
||||
if self.frozen_stages >= 1 and self.tokenizer is not None:
|
||||
self.tokenizer.eval()
|
||||
for param in self.tokenizer.parameters():
|
||||
param.requires_grad = False
|
||||
|
||||
# freeze patch embedding
|
||||
self.patch_embed.eval()
|
||||
for param in self.patch_embed.parameters():
|
||||
param.requires_grad = False
|
||||
# freeze cls_token
|
||||
if self.cls_token is not None:
|
||||
self.cls_token.requires_grad = False
|
||||
if self.storage_tokens is not None:
|
||||
self.storage_tokens.requires_grad = False
|
||||
# freeze layers
|
||||
for i in range(1, self.frozen_stages + 1):
|
||||
m = self.blocks[i - 1]
|
||||
m.eval()
|
||||
for param in m.parameters():
|
||||
param.requires_grad = False
|
||||
|
||||
# freeze the last layer norm
|
||||
if self.frozen_stages == len(self.blocks):
|
||||
if self.final_norm:
|
||||
self.ln1.eval()
|
||||
for param in self.ln1.parameters():
|
||||
param.requires_grad = False
|
||||
|
||||
def forward(self, x):
|
||||
B = x.shape[0]
|
||||
|
||||
x, patch_resolution = self.patch_embed(x) # (B, 256*256, C)
|
||||
if self.tokenizer is not None:
|
||||
x, patch_resolution = self.tokenizer(x, patch_resolution)
|
||||
|
||||
# prepend [CLS] and storage tokens
|
||||
prepend = []
|
||||
if self.cls_token is not None:
|
||||
prepend.append(self.cls_token.expand(B, -1, -1))
|
||||
if self.storage_tokens is not None:
|
||||
prepend.append(self.storage_tokens.expand(B, -1, -1))
|
||||
if len(prepend) > 0:
|
||||
x = torch.cat(prepend + [x], dim=1)
|
||||
|
||||
rope_sincos = self.rope_embed(H=patch_resolution[0], W=patch_resolution[1])
|
||||
outs = []
|
||||
for i, layer in enumerate(self.blocks):
|
||||
x = layer(x, rope=rope_sincos)
|
||||
|
||||
if i == len(self.blocks) - 1 and self.final_norm:
|
||||
x = self.ln1(x)
|
||||
|
||||
if i in self.out_indices:
|
||||
outs.append(self._format_output(x, patch_resolution))
|
||||
|
||||
return tuple(outs)
|
||||
|
||||
def _format_output(self, x, hw):
|
||||
if self.out_type == "raw":
|
||||
return x
|
||||
if self.out_type == "cls_token":
|
||||
return x[:, 0]
|
||||
|
||||
patch_token = x[:, self.num_extra_tokens :]
|
||||
if self.out_type == "featmap":
|
||||
B = x.size(0)
|
||||
# (B, N, C) -> (B, H, W, C) -> (B, C, H, W)
|
||||
return patch_token.reshape(B, *hw, -1).permute(0, 3, 1, 2)
|
||||
|
||||
@property
|
||||
def norm1(self):
|
||||
return self.ln1
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
class LayerScale(nn.Module):
|
||||
def __init__(
|
||||
self,
|
||||
dim: int,
|
||||
inplace: bool = False,
|
||||
data_format: str = "channels_last",
|
||||
scale: float = 1e-5,
|
||||
):
|
||||
super().__init__()
|
||||
assert data_format in (
|
||||
"channels_last",
|
||||
"channels_first",
|
||||
), "'data_format' could only be channels_last or channels_first."
|
||||
self.inplace = inplace
|
||||
self.data_format = data_format
|
||||
self.weight = nn.Parameter(torch.ones(dim) * scale)
|
||||
|
||||
def forward(self, x) -> torch.Tensor:
|
||||
if self.data_format == "channels_first":
|
||||
shape = tuple((1, -1, *(1 for _ in range(x.dim() - 2))))
|
||||
else:
|
||||
shape = tuple((*(1 for _ in range(x.dim() - 1)), -1))
|
||||
if self.inplace:
|
||||
return x.mul_(self.weight.view(*shape))
|
||||
else:
|
||||
return x * self.weight.view(*shape)
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
class PatchEmbed(nn.Module):
|
||||
def __init__(
|
||||
self,
|
||||
in_channels=3,
|
||||
embed_dims=768,
|
||||
kernel_size=16,
|
||||
stride=16,
|
||||
padding="corner",
|
||||
dilation=1,
|
||||
bias=True,
|
||||
input_size=None,
|
||||
):
|
||||
super().__init__()
|
||||
|
||||
self.embed_dims = embed_dims
|
||||
if stride is None:
|
||||
stride = kernel_size
|
||||
|
||||
kernel_size = to_2tuple(kernel_size)
|
||||
stride = to_2tuple(stride)
|
||||
dilation = to_2tuple(dilation)
|
||||
padding = 0
|
||||
padding = to_2tuple(padding)
|
||||
|
||||
self.projection = nn.Conv2d(
|
||||
in_channels=in_channels,
|
||||
out_channels=embed_dims,
|
||||
kernel_size=kernel_size,
|
||||
stride=stride,
|
||||
padding=padding,
|
||||
dilation=dilation,
|
||||
bias=bias,
|
||||
)
|
||||
|
||||
if input_size:
|
||||
input_size = to_2tuple(input_size)
|
||||
self.init_input_size = input_size
|
||||
h_out = (
|
||||
input_size[0] + 2 * padding[0] - dilation[0] * (kernel_size[0] - 1) - 1
|
||||
) // stride[0] + 1
|
||||
w_out = (
|
||||
input_size[1] + 2 * padding[1] - dilation[1] * (kernel_size[1] - 1) - 1
|
||||
) // stride[1] + 1
|
||||
self.init_out_size = (h_out, w_out)
|
||||
else:
|
||||
self.init_input_size = None
|
||||
self.init_out_size = None
|
||||
|
||||
def forward(self, x):
|
||||
x = self.projection(x)
|
||||
out_size = (x.shape[2], x.shape[3])
|
||||
x = x.flatten(2).transpose(1, 2)
|
||||
return x, out_size
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
class SwiGLUFFN(nn.Module):
|
||||
"""SwiGLU FFN layer.
|
||||
https://github.com/facebookresearch/dinov2/blob/main/dinov2/layers/swiglu_ffn.py
|
||||
""" # noqa
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
embed_dims: int,
|
||||
feedforward_channels: Optional[int] = None,
|
||||
out_dims: Optional[int] = None,
|
||||
layer_scale_init_value: float = 0.0,
|
||||
bias: bool = True,
|
||||
add_identity: bool = True,
|
||||
) -> None:
|
||||
super().__init__()
|
||||
self.embed_dims = embed_dims
|
||||
self.out_dims = out_dims or embed_dims
|
||||
hidden_dims = feedforward_channels or embed_dims
|
||||
|
||||
self.w12 = nn.Linear(self.embed_dims, 2 * hidden_dims, bias=bias)
|
||||
self.w3 = nn.Linear(hidden_dims, self.out_dims, bias=bias)
|
||||
|
||||
if layer_scale_init_value > 0:
|
||||
self.gamma2 = LayerScale(dim=embed_dims, scale=layer_scale_init_value)
|
||||
else:
|
||||
self.gamma2 = nn.Identity()
|
||||
|
||||
self.add_identity = add_identity
|
||||
|
||||
def forward(
|
||||
self, x: torch.Tensor, identity: Optional[torch.Tensor] = None
|
||||
) -> torch.Tensor:
|
||||
x12 = self.w12(x)
|
||||
x1, x2 = x12.chunk(2, dim=-1)
|
||||
hidden = F.silu(x1) * x2
|
||||
out = self.w3(hidden)
|
||||
out = self.gamma2(out)
|
||||
|
||||
if self.out_dims != self.embed_dims or not self.add_identity:
|
||||
# due to the dimension inconsistence or user setting
|
||||
# not to apply residual operation
|
||||
return out
|
||||
|
||||
if identity is None:
|
||||
identity = x
|
||||
return identity + out
|
||||
@@ -0,0 +1,211 @@
|
||||
"""Sapiens2 body-part segmentation (Meta, April 2026) for Body Segment (NH).
|
||||
|
||||
The model labels every pixel of a person with one of 28 body parts and clothes (plus background), predicted at
|
||||
1024 x 768. The model code is Meta's standalone file, vendored in core/sapiens2/ with its licence (the Sapiens2
|
||||
License: royalty-free and commercial use allowed, with prohibited uses that include surveillance, biometric processing,
|
||||
identifying people, deepfakes and inferring sensitive data; see LICENSE.md there and THIRD_PARTY_NOTICES.md). The
|
||||
weights download once from Hugging Face, pinned to a fixed revision, into the ``nh_nodes`` model folder
|
||||
(models/NH-Nodes/sapiens2-seg-<size>/ by default), like the SegFormer parsers. It needs PyTorch 2.4 or newer.
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
import torch.nn.functional as F
|
||||
|
||||
from .model_files import download_pinned, make_room_on_gpu, register_model_folder
|
||||
from .segformer_backend import FOLDER_EXTENSIONS, FOLDER_NAME, default_folder
|
||||
|
||||
CLASSES = (
|
||||
"background", "apparel", "eyeglass", "face_neck", "hair", "left_foot", "left_hand", "left_lower_arm",
|
||||
"left_lower_leg", "left_shoe", "left_sock", "left_upper_arm", "left_upper_leg", "lower_clothing", "right_foot",
|
||||
"right_hand", "right_lower_arm", "right_lower_leg", "right_shoe", "right_sock", "right_upper_arm",
|
||||
"right_upper_leg", "torso", "upper_clothing", "lower_lip", "upper_lip", "lower_teeth", "upper_teeth", "tongue",
|
||||
)
|
||||
INPUT_SIZE = (1024, 768) # height, width the model was trained on (3:4)
|
||||
MODELS = {
|
||||
"0.4b": {"repo_id": "facebook/sapiens2-seg-0.4b", "revision": "449b3c5335e6722bb94990abdd1aa6e612432f22",
|
||||
"file": "sapiens2_0.4b_seg.safetensors", "arch": "sapiens2_0.4b", "dims": 1024},
|
||||
"0.8b": {"repo_id": "facebook/sapiens2-seg-0.8b", "revision": "196a627b928676c4429b738ed76f78a21d96c4eb",
|
||||
"file": "sapiens2_0.8b_seg.safetensors", "arch": "sapiens2_0.8b", "dims": 1280},
|
||||
"1b": {"repo_id": "facebook/sapiens2-seg-1b", "revision": "60cf0b3584d68dccca9adb3b59677b72a5611378",
|
||||
"file": "sapiens2_1b_seg.safetensors", "arch": "sapiens2_1b", "dims": 1536},
|
||||
}
|
||||
_MEAN = (123.675, 116.28, 103.53) # ImageNet, RGB on 0..255
|
||||
_STD = (58.395, 57.12, 57.375)
|
||||
_MAX_LOGIT_SIDE = 2048 # logits are resized at most to this size before argmax, like the SegFormer parsers
|
||||
|
||||
_LOADED = {}
|
||||
_said_cpu = False
|
||||
|
||||
|
||||
class SegHead(nn.Module):
|
||||
"""Meta's SegHead at inference: four 2x deconvolutions (16x, so 64 x 48 patches give 1024 x 768) and two 1x1
|
||||
convolutions, then one score per class. Layer names match the released checkpoints."""
|
||||
|
||||
def __init__(self, in_channels, deconv_channels=(512, 256, 128, 64), conv_channels=(64, 64), classes=29):
|
||||
super().__init__()
|
||||
layers = []
|
||||
for channels in deconv_channels:
|
||||
layers += [nn.ConvTranspose2d(in_channels, channels, 4, stride=2, padding=1, bias=False),
|
||||
nn.InstanceNorm2d(channels), nn.SiLU(inplace=True)]
|
||||
in_channels = channels
|
||||
self.deconv_layers = nn.Sequential(*layers)
|
||||
layers = []
|
||||
for channels in conv_channels:
|
||||
layers += [nn.Conv2d(in_channels, channels, 1), nn.InstanceNorm2d(channels), nn.SiLU(inplace=True)]
|
||||
in_channels = channels
|
||||
self.conv_layers = nn.Sequential(*layers)
|
||||
self.conv_seg = nn.Conv2d(in_channels, classes, 1)
|
||||
|
||||
def forward(self, features):
|
||||
return self.conv_seg(self.conv_layers(self.deconv_layers(features)))
|
||||
|
||||
|
||||
class Sapiens2Seg(nn.Module):
|
||||
def __init__(self, arch, dims):
|
||||
super().__init__()
|
||||
from .sapiens2.backbone import Sapiens2
|
||||
|
||||
self.backbone = Sapiens2(arch=arch, img_size=INPUT_SIZE, patch_size=16, final_norm=True, use_tokenizer=False,
|
||||
with_cls_token=True, out_type="featmap")
|
||||
self.decode_head = SegHead(dims, classes=len(CLASSES))
|
||||
|
||||
def forward(self, pixels):
|
||||
return self.decode_head(self.backbone(pixels)[0])
|
||||
|
||||
|
||||
def model_path(size, download=True):
|
||||
"""Path of the weights for ``size`` from any registered ``nh_nodes`` folder; downloaded if absent."""
|
||||
spec = MODELS[size]
|
||||
folder = f"sapiens2-seg-{size}"
|
||||
for base in register_model_folder(FOLDER_NAME, default_folder(), FOLDER_EXTENSIONS):
|
||||
candidate = os.path.join(base, folder, spec["file"])
|
||||
if os.path.isfile(candidate):
|
||||
return candidate
|
||||
if not download:
|
||||
return None
|
||||
target = os.path.join(default_folder(), folder)
|
||||
download_pinned(spec["repo_id"], spec["revision"], target, (spec["file"],))
|
||||
path = os.path.join(target, spec["file"])
|
||||
if not os.path.isfile(path):
|
||||
raise RuntimeError(f"[NH-Nodes] Sapiens2 weights missing after download: {path}")
|
||||
return path
|
||||
|
||||
|
||||
def torch_device(name):
|
||||
"""The torch device for the node's ``device`` choice; "cuda" without a CUDA GPU falls back to the CPU (said once)."""
|
||||
global _said_cpu
|
||||
if name == "cuda" and torch.cuda.is_available():
|
||||
return torch.device("cuda")
|
||||
if name == "cuda" and not _said_cpu:
|
||||
_said_cpu = True
|
||||
print("[NH-Nodes] CUDA not available, Body Segment runs on the CPU (slow)")
|
||||
return torch.device("cpu")
|
||||
|
||||
|
||||
def _native_bf16(device):
|
||||
if device.type != "cuda":
|
||||
return False
|
||||
try:
|
||||
return torch.cuda.is_bf16_supported(including_emulation=False)
|
||||
except TypeError: # PyTorch before 2.3 has no including_emulation
|
||||
return torch.cuda.get_device_capability(device)[0] >= 8
|
||||
|
||||
|
||||
def load(size, device):
|
||||
"""The model for ``size`` ("0.4b", "0.8b" or "1b") with the device it runs on; read from disk once and kept in
|
||||
system memory, one size at a time. Built without random initialisation: the layers are created empty and take the
|
||||
checkpoint's tensors, which a mismatching file fails loudly."""
|
||||
from safetensors.torch import load_file
|
||||
|
||||
if not hasattr(nn, "RMSNorm"):
|
||||
raise RuntimeError(f"[NH-Nodes] Body Segment needs PyTorch 2.4 or newer (this is {torch.__version__}).")
|
||||
bundle = _LOADED.get(size)
|
||||
if bundle is None:
|
||||
_LOADED.clear() # the 1b alone keeps 3 to 6 GB of system memory
|
||||
spec = MODELS[size]
|
||||
path = model_path(size)
|
||||
print(f"[NH-Nodes] Loading Sapiens2 {size} from {path}")
|
||||
try:
|
||||
state = load_file(path)
|
||||
except Exception as exc: # noqa: BLE001 - a damaged or cut-short download
|
||||
raise RuntimeError(f"[NH-Nodes] {path} cannot be read ({exc}); delete it so it is downloaded again.") \
|
||||
from None
|
||||
# own copies, in bfloat16 on GPUs that compute it natively (what it runs in there; half the system memory):
|
||||
# a tensor left on the file's memory map keeps the file open, and Windows then cannot replace or delete it
|
||||
dtype = torch.bfloat16 if _native_bf16(device) else None
|
||||
state = {name: tensor.to(dtype) if dtype is not None and tensor.is_floating_point() and tensor.dtype != dtype
|
||||
else tensor.clone() for name, tensor in state.items()}
|
||||
with torch.device("meta"):
|
||||
model = Sapiens2Seg(spec["arch"], spec["dims"])
|
||||
try:
|
||||
model.load_state_dict(state, strict=True, assign=True)
|
||||
except RuntimeError as exc:
|
||||
raise RuntimeError(f"[NH-Nodes] {path} is not the Sapiens2 {size} segmentation checkpoint; delete it so "
|
||||
f"the right one is downloaded.\n{exc}") from None
|
||||
model.eval().requires_grad_(False)
|
||||
bundle = {"model": model, "size": size}
|
||||
_LOADED[size] = bundle
|
||||
bundle["device"] = device
|
||||
return bundle
|
||||
|
||||
|
||||
def _ensure_on_device(bundle):
|
||||
"""Move the model to its device, in bfloat16 on GPUs that compute it natively and in float32 elsewhere."""
|
||||
model, device = bundle["model"], bundle["device"]
|
||||
dtype = torch.bfloat16 if _native_bf16(device) else torch.float32
|
||||
current = next(model.parameters())
|
||||
if (current.device.type == device.type and (device.index is None or current.device.index == device.index)
|
||||
and current.dtype == dtype):
|
||||
return
|
||||
if device.type == "cuda":
|
||||
make_room_on_gpu(sum(p.numel() for p in model.parameters()) * dtype.itemsize * 2)
|
||||
model.to(device=device, dtype=dtype)
|
||||
|
||||
|
||||
def offload(bundle):
|
||||
"""Weights back to system memory, so they never hold VRAM between runs."""
|
||||
model = bundle["model"]
|
||||
if next(model.parameters()).device.type != "cpu":
|
||||
model.cpu()
|
||||
torch.cuda.empty_cache()
|
||||
|
||||
|
||||
def predict_labels(bundle, image, confidence=False):
|
||||
"""Class index (into CLASSES) of every pixel of the uint8 RGB ``image``, at its size; with ``confidence`` also the
|
||||
float16 probability of each pixel's class, to merge crops by how sure each one is.
|
||||
|
||||
The picture is resized to 768 x 1024 as the model was trained (callers hand it crops of that 3:4 shape, see
|
||||
parse_crops.people_labels), so a crop gets the model's full resolution.
|
||||
"""
|
||||
_ensure_on_device(bundle)
|
||||
model = bundle["model"]
|
||||
parameter = next(model.parameters())
|
||||
device, dtype = parameter.device, parameter.dtype
|
||||
height, width = image.shape[:2]
|
||||
target_h, target_w = INPUT_SIZE
|
||||
shrink = height > target_h or width > target_w
|
||||
small = cv2.resize(image, (target_w, target_h), interpolation=cv2.INTER_AREA if shrink else cv2.INTER_LINEAR)
|
||||
pixels = torch.from_numpy(np.ascontiguousarray(small)).to(device).permute(2, 0, 1).float()
|
||||
mean = torch.tensor(_MEAN, device=device)[:, None, None]
|
||||
std = torch.tensor(_STD, device=device)[:, None, None]
|
||||
pixels = ((pixels - mean) / std)[None].to(dtype)
|
||||
scale = min(1.0, _MAX_LOGIT_SIDE / max(height, width))
|
||||
size = (max(1, round(height * scale)), max(1, round(width * scale)))
|
||||
with torch.inference_mode():
|
||||
logits = model(pixels).float()
|
||||
logits = F.interpolate(logits, size=size, mode="bilinear", align_corners=False)
|
||||
best, labels = logits.softmax(dim=1)[0].max(dim=0)
|
||||
labels = labels.to(torch.int32).cpu().numpy()
|
||||
best = best.cpu().numpy() if confidence else None
|
||||
if labels.shape != (height, width):
|
||||
labels = cv2.resize(labels, (width, height), interpolation=cv2.INTER_NEAREST)
|
||||
if best is not None:
|
||||
best = cv2.resize(best, (width, height), interpolation=cv2.INTER_LINEAR)
|
||||
if best is not None:
|
||||
return labels.astype(np.int64), best.astype(np.float16)
|
||||
return labels.astype(np.int64)
|
||||
@@ -50,6 +50,14 @@ MODEL_SPECS = {
|
||||
"legacy_dirs": ("face_parsing", "RMBG/segformer_face"),
|
||||
"signature": {1: "skin", 13: "hair", 17: "neck"},
|
||||
},
|
||||
# FASHN Human Parser: SegFormer-B4 for fashion, 18 classes (clothes, accessories and body parts), 576 x 384
|
||||
"fashn_human_parser": {
|
||||
"repo_id": "fashn-ai/fashn-human-parser",
|
||||
"revision": "1f80c34dbab321c5730dda5c3fea279fd3e97498",
|
||||
"legacy_dirs": (),
|
||||
"signature": {3: "top", 8: "bag", 17: "jewelry"},
|
||||
"area_resize": True, # trained on pictures brought to 384 x 576 with cv2.INTER_AREA
|
||||
},
|
||||
}
|
||||
|
||||
_LOADED = {}
|
||||
@@ -141,7 +149,7 @@ def load_segformer(model_name, device="cuda"):
|
||||
param.requires_grad = False
|
||||
id2label = {int(k): v for k, v in model.config.id2label.items()}
|
||||
_check_signature(model_name, id2label)
|
||||
bundle = {"processor": processor, "model": model, "id2label": id2label}
|
||||
bundle = {"processor": processor, "model": model, "id2label": id2label, "name": model_name}
|
||||
_LOADED[model_name] = bundle
|
||||
bundle["device"] = target_device
|
||||
return bundle
|
||||
@@ -170,20 +178,39 @@ def offload(bundle):
|
||||
torch.cuda.empty_cache()
|
||||
|
||||
|
||||
def predict_labels(bundle, image_uint8):
|
||||
"""Run SegFormer on one HxWx3 uint8 RGB array -> HxW int64 label map at input size."""
|
||||
def predict_labels(bundle, image_uint8, confidence=False):
|
||||
"""Run SegFormer on one HxWx3 uint8 RGB array -> HxW int64 label map at input size.
|
||||
|
||||
With ``confidence`` it returns (labels, HxW float16 probability of each pixel's label), which lets crops of the
|
||||
same picture be merged by how sure each one is (see parse_crops.people_labels).
|
||||
"""
|
||||
_ensure_on_device(bundle)
|
||||
processor, model = bundle["processor"], bundle["model"]
|
||||
height, width = image_uint8.shape[:2]
|
||||
scale = min(1.0, _MAX_LOGIT_SIDE / max(height, width))
|
||||
logit_size = (max(1, round(height * scale)), max(1, round(width * scale)))
|
||||
|
||||
# The processor squeezes the picture to its fixed size anyway; doing that here first, with area averaging, is
|
||||
# many times faster on a big photo than its own resize and keeps fine detail from aliasing. A model trained on
|
||||
# area-averaged pictures gets them at every size.
|
||||
size = getattr(processor, "size", None)
|
||||
if isinstance(size, dict) and "height" in size and "width" in size:
|
||||
target = (size["width"], size["height"])
|
||||
area = MODEL_SPECS.get(bundle.get("name"), {}).get("area_resize") and (width, height) != target
|
||||
if area or height > 2 * size["height"] or width > 2 * size["width"]:
|
||||
image_uint8 = cv2.resize(image_uint8, target, interpolation=cv2.INTER_AREA)
|
||||
inputs = processor(images=image_uint8, return_tensors="pt")
|
||||
inputs = {k: v.to(model.device) for k, v in inputs.items()}
|
||||
with torch.no_grad():
|
||||
logits = model(**inputs).logits
|
||||
logits = F.interpolate(logits, size=logit_size, mode="bilinear", align_corners=False)
|
||||
labels = logits.argmax(dim=1)[0].to(torch.int32).cpu().numpy()
|
||||
best, labels = logits.float().softmax(dim=1)[0].max(dim=0)
|
||||
labels = labels.to(torch.int32).cpu().numpy()
|
||||
best = best.cpu().numpy() if confidence else None
|
||||
if labels.shape != (height, width):
|
||||
labels = cv2.resize(labels, (width, height), interpolation=cv2.INTER_NEAREST)
|
||||
if best is not None:
|
||||
best = cv2.resize(best, (width, height), interpolation=cv2.INTER_LINEAR)
|
||||
if best is not None:
|
||||
return labels.astype(np.int64), best.astype(np.float16)
|
||||
return labels.astype(np.int64)
|
||||
|
||||
@@ -0,0 +1,856 @@
|
||||
"""NH_SEGS: the instances the NH vision nodes pass between each other.
|
||||
|
||||
An ``NH_SEGS`` value is a tuple with one ``SegsFrame`` per image of a batch. A frame holds the image size and the
|
||||
``Segment`` instances found in that image. A segment keeps its label, score and box, and only the mask pixels inside
|
||||
its box, so dozens of instances on an 8K image stay small. Full-size masks are built only when a node outputs a MASK.
|
||||
|
||||
Plain torch/numpy, no model: the select, compose, preview and convert nodes and their tests run anywhere.
|
||||
"""
|
||||
|
||||
import fnmatch
|
||||
import functools
|
||||
import unicodedata
|
||||
from collections import namedtuple
|
||||
from dataclasses import dataclass, replace
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
import torch
|
||||
import torch.nn.functional as F
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
|
||||
from .vi_words import translate
|
||||
|
||||
SEGS_TYPE = "NH_SEGS"
|
||||
|
||||
|
||||
@dataclass(frozen=True, eq=False)
|
||||
class Segment:
|
||||
"""One instance found in an image.
|
||||
|
||||
``box`` is (x1, y1, x2, y2) in image pixels with exclusive ends, so the instance covers ``image[y1:y2, x1:x2]``;
|
||||
it is the tight box around the mask. ``mask`` is a bool [y2 - y1, x2 - x1] CPU tensor over that box.
|
||||
"""
|
||||
|
||||
label: str
|
||||
score: float
|
||||
box: tuple
|
||||
mask: torch.Tensor
|
||||
source: str = "sam3"
|
||||
|
||||
@property
|
||||
def area(self):
|
||||
"""Number of mask pixels."""
|
||||
return int(self.mask.sum())
|
||||
|
||||
@property
|
||||
def center(self):
|
||||
x1, y1, x2, y2 = self.box
|
||||
return (x1 + x2) / 2.0, (y1 + y2) / 2.0
|
||||
|
||||
|
||||
@dataclass(frozen=True, eq=False)
|
||||
class SegsFrame:
|
||||
"""The instances found in one image of ``height`` x ``width`` pixels, in the order they were found. ``picture`` is
|
||||
a small grey print of that image (see picture_print), or None when the node that found them kept none."""
|
||||
|
||||
height: int
|
||||
width: int
|
||||
segments: tuple = ()
|
||||
picture: object = None
|
||||
|
||||
def with_segments(self, segments):
|
||||
return SegsFrame(self.height, self.width, tuple(segments), self.picture)
|
||||
|
||||
def _comfy_cache_tensors(self):
|
||||
"""The tensors this frame holds, so ComfyUI's RAM-pressure cache can size it."""
|
||||
return [seg.mask for seg in self.segments]
|
||||
|
||||
|
||||
def make_segment(label, score, mask, origin=(0, 0), source="sam3"):
|
||||
"""Segment for the True pixels of the bool [h, w] ``mask`` whose top-left pixel sits at ``origin`` (x, y) of the image.
|
||||
|
||||
The stored box is the tight box around those pixels and the stored mask a compact CPU copy of that box. Works on
|
||||
any device, cropping before the copy to the CPU. Returns None when the mask is empty.
|
||||
"""
|
||||
mask = torch.as_tensor(mask)
|
||||
if mask.dtype != torch.bool:
|
||||
mask = mask > 0.5
|
||||
rows = torch.nonzero(mask.any(dim=1)).flatten()
|
||||
if rows.numel() == 0:
|
||||
return None
|
||||
cols = torch.nonzero(mask.any(dim=0)).flatten()
|
||||
y1, y2 = int(rows[0]), int(rows[-1]) + 1
|
||||
x1, x2 = int(cols[0]), int(cols[-1]) + 1
|
||||
crop = mask[y1:y2, x1:x2].to("cpu").clone(memory_format=torch.contiguous_format)
|
||||
ox, oy = int(origin[0]), int(origin[1])
|
||||
return Segment(str(label), float(score), (ox + x1, oy + y1, ox + x2, oy + y2), crop, source)
|
||||
|
||||
|
||||
def frame_mask(frame, segments=None):
|
||||
"""Bool [H, W] union of ``segments`` (every segment of ``frame`` by default)."""
|
||||
canvas = torch.zeros((frame.height, frame.width), dtype=torch.bool)
|
||||
for seg in frame.segments if segments is None else segments:
|
||||
x1, y1, x2, y2 = seg.box
|
||||
canvas[y1:y2, x1:x2] |= seg.mask
|
||||
return canvas
|
||||
|
||||
|
||||
def segs_union(segs):
|
||||
"""MASK [B, H, W] float: the union of every instance, one mask per image."""
|
||||
return torch.stack([frame_mask(frame) for frame in segs]).float()
|
||||
|
||||
|
||||
def check_memory(nbytes, what):
|
||||
"""Raise MemoryError with advice when ``nbytes`` would take more than half of the RAM that is free now."""
|
||||
try:
|
||||
import psutil
|
||||
|
||||
available = psutil.virtual_memory().available
|
||||
except Exception: # no psutil: nothing to compare with
|
||||
return
|
||||
if nbytes > available // 2:
|
||||
raise MemoryError(
|
||||
f"[NH-Nodes] {what} would need {nbytes / 2**30:.1f} GiB but only {available / 2**30:.1f} GiB of RAM is "
|
||||
"free. Keep fewer instances with Segs Select, or combine them into one mask with Segs Compose."
|
||||
)
|
||||
|
||||
|
||||
def instance_masks(segs):
|
||||
"""(MASK [N, H, W] float with one full-size mask per instance of every image, in order; the labels).
|
||||
|
||||
With no instance at all the MASK holds a single empty mask, as nodes downstream may not accept an empty batch.
|
||||
Each full-size mask takes H x W x 4 bytes (about 145 MB on an 8K image), so a MemoryError explains what to do
|
||||
when they would not fit in RAM.
|
||||
"""
|
||||
labels = [seg.label for frame in segs for seg in frame.segments]
|
||||
height, width = segs[0].height, segs[0].width
|
||||
check_memory(max(1, len(labels)) * height * width * 4, f"{len(labels)} full-size instance masks")
|
||||
masks = torch.zeros((max(1, len(labels)), height, width))
|
||||
index = 0
|
||||
for frame in segs:
|
||||
for seg in frame.segments:
|
||||
x1, y1, x2, y2 = seg.box
|
||||
masks[index, y1:y2, x1:x2] = seg.mask.float()
|
||||
index += 1
|
||||
return masks, labels
|
||||
|
||||
|
||||
PRINT_SIDE = 32 # a frame keeps a PRINT_SIDE x PRINT_SIDE grey print of its picture
|
||||
# Least correlation between two prints of one picture. Tone, colour and blur edits keep it high (a strong
|
||||
# brightening that clips the highlights: 0.64, contrast 0.85, blur 0.87, on 230 photos), and most other photos of the
|
||||
# same size fall below it (36 of 40). It is a guard against wiring mistakes, not a proof: a copy shifted or zoomed a
|
||||
# little usually passes, and a new background behind the same person can fail (Segs Compose can turn it off).
|
||||
SAME_PICTURE = 0.5
|
||||
|
||||
|
||||
def picture_print(image):
|
||||
"""PRINT_SIDE x PRINT_SIDE float32 grey print of one picture (a ComfyUI [H, W, C] image or an HxWxC array): what
|
||||
a frame keeps to tell two photos of the same size apart."""
|
||||
pixels = image.detach().cpu().float().numpy() if torch.is_tensor(image) else np.asarray(image)
|
||||
if pixels.dtype not in (np.uint8, np.uint16, np.float32):
|
||||
pixels = pixels.astype(np.float32) # cv2 has no half precision
|
||||
small = cv2.resize(np.ascontiguousarray(pixels), (PRINT_SIDE, PRINT_SIDE), interpolation=cv2.INTER_AREA)
|
||||
small = np.nan_to_num(small.astype(np.float32), nan=0.0, posinf=1.0, neginf=0.0) # a broken pixel, not a photo
|
||||
return small[..., :3].mean(axis=2) if small.ndim == 3 else small
|
||||
|
||||
|
||||
def same_picture(a, b):
|
||||
"""Whether two picture prints come from the same photo (see SAME_PICTURE). A flat print (a blank picture) tells
|
||||
nothing, so it never refuses (flat up to the rounding of the area averaging)."""
|
||||
if any(float(p.std()) <= 1e-4 * float(np.abs(p).max()) for p in (a, b)):
|
||||
return True
|
||||
a, b = a - a.mean(), b - b.mean()
|
||||
return float((a * b).sum()) / float(np.sqrt((a * a).sum() * (b * b).sum())) >= SAME_PICTURE
|
||||
|
||||
|
||||
def stamped(segs, image):
|
||||
"""``segs`` with each frame keeping the print of its picture of the batch ``image`` (left as they are when the
|
||||
counts differ, so no frame is ever lost)."""
|
||||
if len(segs) != len(image):
|
||||
return tuple(segs)
|
||||
return tuple(replace(frame, picture=picture_print(picture)) for frame, picture in zip(segs, image))
|
||||
|
||||
|
||||
def merge_segs(*all_segs, check_prints=True):
|
||||
"""One NH_SEGS from several of the same pictures (None entries are skipped): each frame gets the instances of
|
||||
all of them, in order. Raises when they do not belong to the same pictures: another count or size of images, or
|
||||
(with ``check_prints``) the prints of different photos."""
|
||||
present = [segs for segs in all_segs if segs is not None]
|
||||
first = present[0]
|
||||
for other in present[1:]:
|
||||
if len(other) != len(first) or any((a.height, a.width) != (b.height, b.width) for a, b in zip(first, other)):
|
||||
raise ValueError("[NH-Nodes] The segs to mix come from different pictures: connect segs found on the same "
|
||||
"image batch.")
|
||||
for index in range(len(first) if check_prints else 0):
|
||||
prints = [segs[index].picture for segs in present if segs[index].picture is not None]
|
||||
if not all(same_picture(prints[0], other) for other in prints[1:]):
|
||||
which = f" (image {index + 1} of the batch)" if len(first) > 1 else ""
|
||||
raise ValueError(f"[NH-Nodes] The segs to mix look like different photos of the same size{which}. "
|
||||
"Connect segs found on the same image; if they are edits of one photo (a new "
|
||||
"background, a relight), turn check_same_photo off.")
|
||||
if len(present) == 1:
|
||||
return first
|
||||
return tuple(frame.with_segments([seg for segs in present for seg in segs[index].segments])
|
||||
for index, frame in enumerate(first))
|
||||
|
||||
|
||||
def count_segments(segs):
|
||||
return sum(len(frame.segments) for frame in segs)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Labels
|
||||
# ---------------------------------------------------------------------------
|
||||
def normalize_label(text):
|
||||
"""Lower case, '_' as a space, single spaces, accents in one form (keyboards differ): "Red_Car " and "red car"
|
||||
both become "red car"."""
|
||||
return " ".join(unicodedata.normalize("NFC", str(text)).replace("_", " ").lower().split())
|
||||
|
||||
|
||||
# Plurals the usual rules miss (see _singular), and "glasses" kept apart from a drinking glass
|
||||
_IRREGULAR = {"feet": "foot", "teeth": "tooth", "people": "person", "children": "child", "men": "man",
|
||||
"women": "woman", "mice": "mouse", "geese": "goose",
|
||||
"leaves": "leaf", "knives": "knife", "shelves": "shelf", "calves": "calf", "halves": "half",
|
||||
"scarves": "scarf", "wolves": "wolf", "wives": "wife", "loaves": "loaf", "thieves": "thief",
|
||||
"hoodies": "hoodie", "movies": "movie", "cookies": "cookie", "selfies": "selfie", "brownies": "brownie",
|
||||
"zombies": "zombie", "pies": "pie", "ties": "tie", "glasses": "glasses"}
|
||||
|
||||
|
||||
def _singular(word):
|
||||
"""The singular of one English word by the usual rules: arms -> arm, glasses -> glass, accessories -> accessory."""
|
||||
if word in _IRREGULAR:
|
||||
return _IRREGULAR[word]
|
||||
if len(word) > 4 and word.endswith("ies"):
|
||||
return word[:-3] + "y"
|
||||
if word.endswith(("sses", "shes", "ches", "xes", "zes")):
|
||||
return word[:-2]
|
||||
if len(word) > 3 and word.endswith("s") and not word.endswith(("ss", "us", "is")):
|
||||
return word[:-1]
|
||||
return word
|
||||
|
||||
|
||||
def fold_label(text):
|
||||
"""A label in lower case with every word made singular, so "Arms" and "arm" are the same label."""
|
||||
return " ".join(_singular(word) for word in normalize_label(text).split())
|
||||
|
||||
|
||||
def label_matches(label, pattern):
|
||||
"""True when ``label`` matches ``pattern``, ignoring case and singular or plural ("arm" finds "arms"); a pattern
|
||||
with * and ? wildcards is matched as written."""
|
||||
label, pattern = normalize_label(label), normalize_label(pattern)
|
||||
if fnmatch.fnmatchcase(label, pattern):
|
||||
return True
|
||||
return not any(ch in pattern for ch in "*?[") and fold_label(label) == fold_label(pattern)
|
||||
|
||||
|
||||
_ALIASES = {} # (source of the instances, folded word) -> folded labels of that source's parts the word stands for
|
||||
|
||||
|
||||
def add_aliases(source, words, parts):
|
||||
"""Let a node's own words (word -> class ids) name the parts of the segs it makes (part label -> class ids, with
|
||||
``source``): a word stands for the parts whose classes it covers. So Segs Compose and Segs Select read "skin" on
|
||||
Body Segment's segs as its face, torso, arms... parts. A word that is the name of one of the node's parts is left
|
||||
out: the part keeps its name, whatever else a picture holds."""
|
||||
named = {fold_label(part) for part in parts}
|
||||
for word, ids in words.items():
|
||||
labels = frozenset(fold_label(part) for part, part_ids in parts.items() if part_ids and set(part_ids) <= set(ids))
|
||||
if labels and fold_label(word) not in named:
|
||||
_ALIASES[(source, fold_label(word))] = labels
|
||||
|
||||
|
||||
def matching(segments, pattern, written=None):
|
||||
"""The instances a label pattern names, node by node (the instances one source made): that node's instances
|
||||
carrying the label (see label_matches), or when none does, the parts the pattern stands for among that node's own
|
||||
words (see add_aliases). ``written`` is the Vietnamese word the pattern was translated from, which a node may read
|
||||
its own way (Body Segment's "tay" is the arm with the hand, not the arm alone)."""
|
||||
if any(ch in str(pattern) for ch in "*?["):
|
||||
return [seg for seg in segments if label_matches(seg.label, pattern)]
|
||||
word, said = fold_label(pattern), fold_label(written) if written else None
|
||||
kept = set()
|
||||
for source in dict.fromkeys(seg.source for seg in segments):
|
||||
own = [seg for seg in segments if seg.source == source]
|
||||
parts = _ALIASES.get((source, said)) if said else None
|
||||
hits = [] if parts else [seg for seg in own if label_matches(seg.label, pattern)]
|
||||
if not hits:
|
||||
parts = parts or _ALIASES.get((source, word), ())
|
||||
hits = [seg for seg in own if fold_label(seg.label) in parts]
|
||||
kept.update(id(seg) for seg in hits)
|
||||
return [seg for seg in segments if id(seg) in kept]
|
||||
|
||||
|
||||
def parse_label_list(text):
|
||||
"""Commas and new lines separate entries: "person, red car" -> ["person", "red car"].
|
||||
|
||||
Blank entries and lines starting with '#' are dropped; a repeated entry (ignoring case and spacing) is kept once,
|
||||
with its first spelling.
|
||||
"""
|
||||
entries, seen = [], set()
|
||||
for line in str(text or "").splitlines():
|
||||
if line.strip().startswith("#"):
|
||||
continue
|
||||
for part in line.split(","):
|
||||
entry = " ".join(part.split())
|
||||
key = normalize_label(entry)
|
||||
if entry and key not in seen:
|
||||
seen.add(key)
|
||||
entries.append(entry)
|
||||
return entries
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Select
|
||||
# ---------------------------------------------------------------------------
|
||||
SORT_ORDERS = ("score", "area", "left_to_right", "right_to_left", "top_to_bottom", "bottom_to_top",
|
||||
"center_first", "as_found")
|
||||
|
||||
|
||||
def sort_segments(segments, order, frame):
|
||||
"""``segments`` in ``order`` (one of SORT_ORDERS); positions use each box's centre. Ties keep their found order."""
|
||||
segments = list(segments)
|
||||
if order == "score":
|
||||
return sorted(segments, key=lambda s: -s.score)
|
||||
if order == "area":
|
||||
return sorted(segments, key=lambda s: -s.area)
|
||||
if order == "left_to_right":
|
||||
return sorted(segments, key=lambda s: s.center[0])
|
||||
if order == "right_to_left":
|
||||
return sorted(segments, key=lambda s: -s.center[0])
|
||||
if order == "top_to_bottom":
|
||||
return sorted(segments, key=lambda s: s.center[1])
|
||||
if order == "bottom_to_top":
|
||||
return sorted(segments, key=lambda s: -s.center[1])
|
||||
if order == "center_first":
|
||||
cx, cy = frame.width / 2.0, frame.height / 2.0
|
||||
return sorted(segments, key=lambda s: (s.center[0] - cx) ** 2 + (s.center[1] - cy) ** 2)
|
||||
return segments
|
||||
|
||||
|
||||
def select_segments(frame, labels=(), min_score=0.0, min_area=0.0, max_area=1.0, order="score", start=0, count=0):
|
||||
"""Frame with the segments that pass the filters, sorted by ``order``, from position ``start`` (0 = first).
|
||||
|
||||
``labels`` are patterns (any one must match; none = every label). Areas are fractions of the image. ``count`` = 0
|
||||
keeps every segment from ``start`` on.
|
||||
"""
|
||||
total = max(1, frame.height * frame.width)
|
||||
named = {id(seg) for pattern in labels for seg in matching(frame.segments, pattern)}
|
||||
kept = []
|
||||
for seg in frame.segments:
|
||||
if labels and id(seg) not in named:
|
||||
continue
|
||||
if seg.score < min_score:
|
||||
continue
|
||||
if not min_area <= seg.area / total <= max_area:
|
||||
continue
|
||||
kept.append(seg)
|
||||
kept = sort_segments(kept, order, frame)
|
||||
start = max(0, int(start))
|
||||
kept = kept[start:] if count <= 0 else kept[start:start + int(count)]
|
||||
return frame.with_segments(kept)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Compose: a small boolean language over labels
|
||||
# ---------------------------------------------------------------------------
|
||||
class ExpressionError(ValueError):
|
||||
"""An NH_SEGS mask expression that cannot be read."""
|
||||
|
||||
|
||||
class EmptyExpressionError(ExpressionError):
|
||||
"""An expression with nothing in it (blank, or only commas and comments)."""
|
||||
|
||||
|
||||
_OPERATORS = {"+": "|", "|": "|", "-": "-", "&": "&", "~": "~", "!": "~", "(": "(", ")": ")"}
|
||||
_SEPARATORS = ",\n" # list items, joined like '|' but read last
|
||||
_WORD_OPERATORS = {"or": "|", "and": "&", "not": "~"}
|
||||
_KEYWORDS = ("all", "image", "none", "background")
|
||||
_QUOTES = "\"'"
|
||||
|
||||
|
||||
def _is_word_char(ch):
|
||||
return ch.isalnum() or ch == "_"
|
||||
|
||||
|
||||
def _word_tokens(text, word_operators):
|
||||
"""[(kind, value, position, written)] with kinds "op", "sep", "word" and "quoted"; positions count from 1.
|
||||
|
||||
A quote opens a quoted label only where a token starts, and a '-' between two word characters is part of the
|
||||
word, so ``women's`` and ``t-shirt`` are plain words while ``person - hair`` subtracts. A '#' where a token would
|
||||
start begins a comment that runs to the end of its line (inside a word, as in ``car#2``, it is part of the word).
|
||||
"""
|
||||
tokens, i, n = [], 0, len(text)
|
||||
while i < n:
|
||||
ch = text[i]
|
||||
if ch == "#":
|
||||
end = text.find("\n", i)
|
||||
i = n if end < 0 else end
|
||||
elif ch in _SEPARATORS:
|
||||
tokens.append(("sep", ",", i + 1, "a new line" if ch == "\n" else ch))
|
||||
i += 1
|
||||
elif ch.isspace():
|
||||
i += 1
|
||||
elif ch in _OPERATORS:
|
||||
tokens.append(("op", _OPERATORS[ch], i + 1, ch))
|
||||
i += 1
|
||||
elif ch in _QUOTES:
|
||||
end = text.find(ch, i + 1)
|
||||
if end < 0:
|
||||
raise ExpressionError(f"quote opened at position {i + 1} is never closed")
|
||||
tokens.append(("quoted", text[i + 1:end], i + 1, text[i:end + 1]))
|
||||
i = end + 1
|
||||
else:
|
||||
j = i
|
||||
while j < n and not text[j].isspace() and text[j] not in _SEPARATORS:
|
||||
hyphen_in_word = (text[j] == "-" and _is_word_char(text[j - 1])
|
||||
and j + 1 < n and _is_word_char(text[j + 1]))
|
||||
if text[j] in _OPERATORS and not hyphen_in_word:
|
||||
break
|
||||
j += 1
|
||||
word = text[i:j]
|
||||
if word_operators and word.lower() in _WORD_OPERATORS:
|
||||
tokens.append(("op", _WORD_OPERATORS[word.lower()], i + 1, word))
|
||||
else:
|
||||
tokens.append(("word", word, i + 1, word))
|
||||
i = j
|
||||
return tokens
|
||||
|
||||
|
||||
def _tokenize(text, word_operators=True):
|
||||
"""Tokens with each run of words joined into one "label" (or "keyword") token: ``red car`` is one label.
|
||||
|
||||
Labels are read through ``vi_words.translate``, so Vietnamese becomes the English the instances are named in;
|
||||
the token keeps (english, status) as its value. A run that translates to all, image, none or background, unquoted,
|
||||
is that keyword.
|
||||
"""
|
||||
raw = _word_tokens(text, word_operators)
|
||||
tokens = []
|
||||
for index, token in enumerate(raw):
|
||||
kind, value, position, written = token
|
||||
if kind == "sep" and written == "a new line":
|
||||
# a formula laid out over several lines: a line break next to an operator joins, it does not list
|
||||
after = next((t for t in raw[index + 1:] if not (t[0] == "sep" and t[3] == "a new line")), None)
|
||||
before = next((t for t in reversed(tokens) if t[0] != "sep"), None) if tokens and tokens[-1][0] != "sep" \
|
||||
else None
|
||||
if (before is not None and before[0] == "op" and before[1] != ")") or (
|
||||
after is not None and after[0] == "op" and after[1] in ("|", "-", "&", ")")):
|
||||
continue
|
||||
if kind in ("word", "quoted") and tokens and tokens[-1][0] == "run":
|
||||
_, parts, start, quoted = tokens[-1]
|
||||
tokens[-1] = ("run", parts + [value], start, quoted or kind == "quoted")
|
||||
elif kind in ("word", "quoted"):
|
||||
tokens.append(("run", [value], position, kind == "quoted"))
|
||||
else:
|
||||
tokens.append(token)
|
||||
result = []
|
||||
for token in tokens:
|
||||
if token[0] != "run":
|
||||
result.append(token)
|
||||
continue
|
||||
_, parts, position, quoted = token
|
||||
written = " ".join(parts)
|
||||
english, status = translate(written)
|
||||
if not quoted and english.lower() in _KEYWORDS:
|
||||
result.append(("keyword", english.lower(), position, written))
|
||||
else:
|
||||
result.append(("label", (english, status), position, written))
|
||||
return result
|
||||
|
||||
|
||||
class _Parser:
|
||||
"""list := expr? (',' expr?)* ; expr := term (('|' | '-') term)* ; term := unary ('&' unary)* ;
|
||||
unary := '~' unary | atom ; atom := label | keyword | '(' list ')'. Commas and new lines join the items of a list
|
||||
like '|', after every other operator. Builds a tuple tree evaluated by ``evaluate``: a label node is
|
||||
("label", english, written, status), a keyword node ("keyword", name, written)."""
|
||||
|
||||
def __init__(self, text, word_operators=True):
|
||||
self.tokens = _tokenize(text, word_operators)
|
||||
self.pos = 0
|
||||
|
||||
def _peek(self):
|
||||
return self.tokens[self.pos] if self.pos < len(self.tokens) else None
|
||||
|
||||
def _where(self):
|
||||
token = self._peek()
|
||||
return f"'{token[3]}' at position {token[2]}" if token else "the end"
|
||||
|
||||
def parse(self):
|
||||
items = self._list(nested=False)
|
||||
if not items:
|
||||
raise EmptyExpressionError("the expression is empty; use 'all' for every instance")
|
||||
return self._union(items)
|
||||
|
||||
@staticmethod
|
||||
def _union(items):
|
||||
tree = items[0]
|
||||
for item in items[1:]:
|
||||
tree = ("|", tree, item)
|
||||
return tree
|
||||
|
||||
def _list(self, nested):
|
||||
"""The items of a list up to the end (or the closing ')' when ``nested``); empty items are skipped."""
|
||||
items = []
|
||||
while True:
|
||||
token = self._peek()
|
||||
if token is not None and token[0] == "sep":
|
||||
self.pos += 1
|
||||
continue
|
||||
if token is None or (nested and token[:2] == ("op", ")")):
|
||||
return items
|
||||
items.append(self._expr())
|
||||
token = self._peek()
|
||||
if token is not None and token[0] != "sep" and not (nested and token[:2] == ("op", ")")):
|
||||
raise ExpressionError(f"unexpected {self._where()}")
|
||||
|
||||
def _expr(self):
|
||||
tree = self._term()
|
||||
while (token := self._peek()) is not None and token[0] == "op" and token[1] in ("|", "-"):
|
||||
self.pos += 1
|
||||
tree = (token[1], tree, self._term())
|
||||
return tree
|
||||
|
||||
def _term(self):
|
||||
tree = self._unary()
|
||||
while (token := self._peek()) is not None and token[0] == "op" and token[1] == "&":
|
||||
self.pos += 1
|
||||
tree = ("&", tree, self._unary())
|
||||
return tree
|
||||
|
||||
def _unary(self):
|
||||
token = self._peek()
|
||||
if token is not None and token[0] == "op" and token[1] == "~":
|
||||
self.pos += 1
|
||||
return ("~", self._unary())
|
||||
return self._atom()
|
||||
|
||||
def _atom(self):
|
||||
token = self._peek()
|
||||
if token is None:
|
||||
raise ExpressionError("the expression ends where a label, 'all', 'image', 'none' or '(' was expected")
|
||||
kind, value, position, written = token
|
||||
self.pos += 1
|
||||
if kind == "label":
|
||||
return ("label", value[0], written, value[1])
|
||||
if kind == "keyword":
|
||||
return ("keyword", value, written)
|
||||
if value == "(":
|
||||
items = self._list(nested=True)
|
||||
closing = self._peek()
|
||||
if closing is None or closing[:2] != ("op", ")"):
|
||||
raise ExpressionError(f"'(' at position {position} is not closed (found {self._where()})")
|
||||
if not items:
|
||||
raise ExpressionError(f"'(' at position {position} holds nothing")
|
||||
self.pos += 1
|
||||
return self._union(items)
|
||||
raise ExpressionError(f"unexpected '{written}' at position {position}")
|
||||
|
||||
|
||||
@functools.lru_cache(maxsize=64)
|
||||
def parse_expression(text, word_operators=True):
|
||||
"""Parse a mask expression (see ``evaluate``) into a tree; raises ExpressionError with the position on mistakes.
|
||||
|
||||
``word_operators=False`` reads or/and/not as words, for the natural phrases SAM 3 takes ("black and white dog").
|
||||
"""
|
||||
return _Parser(str(text), word_operators).parse()
|
||||
|
||||
|
||||
def parse_concepts(text):
|
||||
"""``parse_expression`` for SAM3 Segment's concepts: or/and/not are plain words there."""
|
||||
return parse_expression(text, word_operators=False)
|
||||
|
||||
|
||||
def expression_labels(tree):
|
||||
"""Label patterns (in English) used in a parsed expression, in order."""
|
||||
if tree[0] == "label":
|
||||
return [tree[1]]
|
||||
if tree[0] == "keyword":
|
||||
return []
|
||||
return [label for child in tree[1:] for label in expression_labels(child)]
|
||||
|
||||
|
||||
def expression_terms(tree):
|
||||
"""(written, english, status) of each label and keyword of a parsed expression, in order; status is "english",
|
||||
"translated" or "unknown" (Vietnamese not in vi_words) for labels and "keyword" for keywords."""
|
||||
if tree[0] == "label":
|
||||
return [(tree[2], tree[1], tree[3])]
|
||||
if tree[0] == "keyword":
|
||||
return [(tree[2], tree[1], "keyword")]
|
||||
return [term for child in tree[1:] for term in expression_terms(child)]
|
||||
|
||||
|
||||
def uses_keyword(tree, name):
|
||||
return any(status == "keyword" and english == name for _, english, status in expression_terms(tree))
|
||||
|
||||
|
||||
def evaluate(tree, frame):
|
||||
"""Bool [H, W] mask of a parsed expression for one frame.
|
||||
|
||||
A label (wildcards allowed; quotes or '_' for spaces; a '-' inside a word is part of it; words in a row make one
|
||||
label) is the union of the matching instances, ``all`` every instance, ``background`` everything outside them,
|
||||
``image`` the whole frame, ``none`` nothing. ``a | b`` (also ``+``, ``or``, a comma or a new line) is the union,
|
||||
``a - b`` the difference, ``a & b`` (``and``) the intersection, ``~a`` (``!``, ``not``) everything outside ``a``.
|
||||
"""
|
||||
kind = tree[0]
|
||||
if kind == "label":
|
||||
return frame_mask(frame, matching(frame.segments, tree[1], tree[2] if tree[3] == "translated" else None))
|
||||
if kind == "keyword":
|
||||
if tree[1] == "all":
|
||||
return frame_mask(frame)
|
||||
if tree[1] == "background":
|
||||
return ~frame_mask(frame)
|
||||
if tree[1] == "image":
|
||||
return torch.ones((frame.height, frame.width), dtype=torch.bool)
|
||||
return torch.zeros((frame.height, frame.width), dtype=torch.bool)
|
||||
if kind == "~":
|
||||
return ~evaluate(tree[1], frame)
|
||||
left, right = evaluate(tree[1], frame), evaluate(tree[2], frame)
|
||||
if kind == "|":
|
||||
return left | right
|
||||
if kind == "&":
|
||||
return left & right
|
||||
return left & ~right
|
||||
|
||||
|
||||
def expression_masks(tree, segs):
|
||||
"""MASK [B, H, W] float of a parsed expression, one mask per image of ``segs``."""
|
||||
return torch.stack([evaluate(tree, frame) for frame in segs]).float()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Impact Pack SEGS
|
||||
# ---------------------------------------------------------------------------
|
||||
# The field names of Impact Pack's SEG, so its detailers read these items; NH-Nodes does not import Impact Pack.
|
||||
IMPACT_SEG = namedtuple("SEG", ["cropped_image", "cropped_mask", "confidence", "crop_region", "bbox", "label",
|
||||
"control_net_wrapper"], defaults=[None])
|
||||
|
||||
|
||||
def _centred_span(lo, hi, factor, limit):
|
||||
"""[start, end) of ``factor`` times the length of [lo, hi), centred on it and slid inside [0, limit).
|
||||
|
||||
It is never shorter than [lo, hi) and only shorter than asked when the image is, so it always contains [lo, hi).
|
||||
"""
|
||||
size = min(limit, max(hi - lo, round((hi - lo) * factor)))
|
||||
start = min(max(0, round((lo + hi - size) / 2)), limit - size)
|
||||
return start, start + size
|
||||
|
||||
|
||||
def detail_crop_region(width, height, box, crop_factor):
|
||||
"""[x1, y1, x2, y2] a detailer works on around ``box``: ``crop_factor`` times its size, centred, inside the image."""
|
||||
rx1, rx2 = _centred_span(box[0], box[2], crop_factor, width)
|
||||
ry1, ry2 = _centred_span(box[1], box[3], crop_factor, height)
|
||||
return [rx1, ry1, rx2, ry2]
|
||||
|
||||
|
||||
def to_impact_segs(frame, crop_factor=3.0):
|
||||
"""Impact Pack SEGS ``((height, width), [SEG, ...])`` for one frame; each mask is placed in its crop region."""
|
||||
items = []
|
||||
for seg in frame.segments:
|
||||
region = detail_crop_region(frame.width, frame.height, seg.box, crop_factor)
|
||||
rx1, ry1, rx2, ry2 = region
|
||||
x1, y1, x2, y2 = seg.box
|
||||
cropped = np.zeros((ry2 - ry1, rx2 - rx1), dtype=np.float32)
|
||||
cropped[y1 - ry1:y2 - ry1, x1 - rx1:x2 - rx1] = seg.mask.numpy()
|
||||
items.append(IMPACT_SEG(None, cropped, float(seg.score), region, [x1, y1, x2, y2], seg.label, None))
|
||||
return (frame.height, frame.width), items
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Preview
|
||||
# ---------------------------------------------------------------------------
|
||||
_PALETTE = (
|
||||
(230, 25, 75), (60, 180, 75), (255, 225, 25), (0, 130, 200), (245, 130, 48), (145, 30, 180),
|
||||
(70, 240, 240), (240, 50, 230), (210, 245, 60), (250, 190, 212), (0, 128, 128), (170, 110, 40),
|
||||
)
|
||||
|
||||
|
||||
@functools.lru_cache(maxsize=16)
|
||||
def _label_font(size):
|
||||
for name in ("arial.ttf", "DejaVuSans.ttf"): # both cover Vietnamese
|
||||
try:
|
||||
return ImageFont.truetype(name, size)
|
||||
except Exception:
|
||||
continue
|
||||
try:
|
||||
return ImageFont.load_default(size=size)
|
||||
except TypeError: # Pillow < 10.1
|
||||
return ImageFont.load_default()
|
||||
|
||||
|
||||
def _free_spot(x, y, width, height, placed, limit):
|
||||
"""``y`` moved down (or up near the bottom ``limit``) until a ``width`` x ``height`` label at ``x`` overlaps none
|
||||
of the ``placed`` (x1, y1, x2, y2) rectangles; labels of instances sharing a top edge (person, hair) then stack."""
|
||||
for _ in range(len(placed)):
|
||||
hit = next((r for r in placed if x < r[2] and r[0] < x + width and y < r[3] and r[1] < y + height), None)
|
||||
if hit is None:
|
||||
break
|
||||
y = hit[3] + 1 if hit[3] + 1 + height <= limit else max(0, hit[1] - height - 1)
|
||||
return y
|
||||
|
||||
|
||||
def render_preview(image, frame, fill=0.45):
|
||||
"""uint8 HxWx3 copy of ``image`` with each instance tinted, outlined and labelled "label score"."""
|
||||
canvas = np.ascontiguousarray(image[:, :, :3]).copy()
|
||||
height, width = canvas.shape[:2]
|
||||
if not frame.segments:
|
||||
return canvas
|
||||
thickness = max(1, round(min(height, width) / 700))
|
||||
kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (2 * thickness + 1, 2 * thickness + 1))
|
||||
for index, seg in enumerate(frame.segments):
|
||||
color = np.array(_PALETTE[index % len(_PALETTE)], dtype=np.float32)
|
||||
x1, y1, x2, y2 = seg.box
|
||||
region = canvas[y1:y2, x1:x2]
|
||||
inside = seg.mask.numpy()
|
||||
region[inside] = (region[inside] * (1.0 - fill) + color * fill).astype(np.uint8)
|
||||
padded = np.pad(inside.astype(np.uint8), thickness) # so a box edge counts as an outline too
|
||||
edge = (padded > 0) & (cv2.erode(padded, kernel) == 0)
|
||||
region[edge[thickness:-thickness, thickness:-thickness]] = color.astype(np.uint8)
|
||||
|
||||
font = _label_font(max(12, round(min(height, width) / 45)))
|
||||
picture = Image.fromarray(canvas)
|
||||
draw = ImageDraw.Draw(picture)
|
||||
placed = [] # label rectangles drawn so far, so a label never hides another one
|
||||
for index, seg in enumerate(frame.segments):
|
||||
color = _PALETTE[index % len(_PALETTE)]
|
||||
text = f"{seg.label} {seg.score:.2f}"
|
||||
left, top, right, bottom = draw.textbbox((0, 0), text, font=font)
|
||||
pad = max(2, (bottom - top) // 6)
|
||||
tw, th = right - left + 2 * pad, bottom - top + 2 * pad
|
||||
# centred over the instance's topmost pixels (a head, a roof), not at a box corner that may be far from it
|
||||
top_columns = torch.nonzero(seg.mask[0]).flatten()
|
||||
anchor = seg.box[0] + int(top_columns[top_columns.numel() // 2]) if top_columns.numel() else seg.box[0]
|
||||
x = min(max(0, anchor - tw // 2), max(0, width - tw))
|
||||
y = _free_spot(x, seg.box[1] - th if seg.box[1] - th >= 0 else seg.box[1], tw, th, placed, height)
|
||||
placed.append((x, y, x + tw, y + th))
|
||||
draw.rectangle((x, y, x + tw, y + th), fill=color)
|
||||
dark_text = (0.299 * color[0] + 0.587 * color[1] + 0.114 * color[2]) > 150
|
||||
draw.text((x + pad - left, y + pad - top), text, font=font, fill=(0, 0, 0) if dark_text else (255, 255, 255))
|
||||
return np.asarray(picture)
|
||||
|
||||
|
||||
def scale_frame(frame, scale):
|
||||
"""The frame of the picture made ``scale`` times smaller (for previews): boxes scaled, masks area-resampled.
|
||||
|
||||
An instance too small to keep a pixel keeps its centre pixel, so it still shows up with its label.
|
||||
"""
|
||||
height, width = max(1, round(frame.height * scale)), max(1, round(frame.width * scale))
|
||||
segments = []
|
||||
for seg in frame.segments:
|
||||
x1, y1, x2, y2 = seg.box
|
||||
nx1, ny1 = min(width - 1, int(x1 * scale)), min(height - 1, int(y1 * scale))
|
||||
nx2 = max(nx1 + 1, min(width, int(np.ceil(x2 * scale))))
|
||||
ny2 = max(ny1 + 1, min(height, int(np.ceil(y2 * scale))))
|
||||
small = cv2.resize(seg.mask.numpy().astype(np.uint8) * 255, (nx2 - nx1, ny2 - ny1),
|
||||
interpolation=cv2.INTER_AREA) >= 128
|
||||
if not small.any():
|
||||
small[small.shape[0] // 2, small.shape[1] // 2] = True
|
||||
segments.append(make_segment(seg.label, seg.score, small, (nx1, ny1), seg.source))
|
||||
return SegsFrame(height, width, tuple(segments), frame.picture)
|
||||
|
||||
|
||||
def _caption(picture, notes):
|
||||
"""``picture`` (uint8 RGB) with ``notes`` written on a white strip below it, one line each, long lines wrapped."""
|
||||
width = picture.shape[1]
|
||||
font = _label_font(max(13, round(width / 70)))
|
||||
measure = ImageDraw.Draw(Image.new("RGB", (1, 1)))
|
||||
lines = []
|
||||
for note in notes:
|
||||
words, line = str(note).split(), ""
|
||||
for word in words:
|
||||
candidate = f"{line} {word}".strip()
|
||||
if line and measure.textlength(candidate, font=font) > width - 16:
|
||||
lines.append(line)
|
||||
line = word
|
||||
else:
|
||||
line = candidate
|
||||
lines.append(line)
|
||||
left, top, right, bottom = measure.textbbox((0, 0), "Ag", font=font)
|
||||
step = bottom - top + 6
|
||||
strip = Image.new("RGB", (width, step * len(lines) + 10), (255, 255, 255))
|
||||
draw = ImageDraw.Draw(strip)
|
||||
for index, line in enumerate(lines):
|
||||
draw.text((8, 5 + index * step - top), line, font=font, fill=(20, 20, 20))
|
||||
return np.concatenate([picture, np.asarray(strip)], axis=0)
|
||||
|
||||
|
||||
def preview_picture(image, frame, notes=(), max_side=1536, mask=None):
|
||||
"""uint8 RGB preview of one picture, at most ``max_side`` px, its instances drawn and ``notes`` written below.
|
||||
|
||||
``image`` is one ComfyUI image [H, W, C] (float, 0..1). With ``mask`` (the node's [H, W] output) everything
|
||||
outside it is darkened, so the preview shows what the mask holds ("background" lights up the background) while the
|
||||
instances keep their outline and label. The picture is shrunk first (area averaging), so an 8K image with a
|
||||
hundred instances needs a few MB more, not the full-size float preview Segs Preview makes.
|
||||
"""
|
||||
rgb = image[..., :3] if image.shape[-1] >= 3 else image[..., :1].expand(-1, -1, 3)
|
||||
scale = min(1.0, max_side / max(frame.height, frame.width))
|
||||
size = (max(1, round(frame.height * scale)), max(1, round(frame.width * scale)))
|
||||
if scale < 1.0: # shrink before anything else, so no full-size copy of an 8K picture is ever made
|
||||
rgb = F.interpolate(rgb.permute(2, 0, 1)[None].float(), size=size, mode="area")[0].permute(1, 2, 0)
|
||||
frame = scale_frame(frame, scale)
|
||||
pixels = (rgb.float().clamp(0.0, 1.0) * 255.0).round().to(torch.uint8).cpu().numpy()
|
||||
if mask is not None:
|
||||
held = mask.float()
|
||||
if scale < 1.0:
|
||||
held = F.interpolate(held[None, None], size=size, mode="area")[0, 0]
|
||||
outside = held.cpu().numpy() < 0.5
|
||||
pixels = pixels.copy()
|
||||
pixels[outside] = (pixels[outside] * _DIM_OUTSIDE).astype(np.uint8)
|
||||
picture = render_preview(pixels, frame, fill=0.2 if mask is not None else 0.45)
|
||||
return _caption(picture, notes) if notes else picture
|
||||
|
||||
|
||||
_DIM_OUTSIDE = 0.35 # brightness kept outside the mask in previews
|
||||
|
||||
|
||||
def describe_request(tree, segs, threshold=None, misses=None, added=()):
|
||||
"""Lines telling what each label and keyword of a parsed request found in ``segs``, for reports and previews.
|
||||
|
||||
``misses`` maps a concept to the best score its candidates reached under ``threshold``; ``added`` names concepts
|
||||
the node searched for on its own (person, for background).
|
||||
"""
|
||||
instances = [seg for frame in segs for seg in frame.segments]
|
||||
lines, seen = [], set()
|
||||
for written, english, status in expression_terms(tree):
|
||||
key = (status == "keyword", fold_label(english))
|
||||
if key in seen:
|
||||
continue
|
||||
seen.add(key)
|
||||
if status == "keyword":
|
||||
if english == "background":
|
||||
names = list(dict.fromkeys(seg.label for seg in instances))
|
||||
outside = ", ".join(names) if names else "nothing (no instance was found)"
|
||||
lines.append(f"background = everything outside {outside}")
|
||||
continue
|
||||
name = f"{written} -> {english}" if status == "translated" else written
|
||||
found = [seg for seg in instances if label_matches(seg.label, english)]
|
||||
if found:
|
||||
lines.append(f"{name}: {len(found)} found (best {max(seg.score for seg in found):.2f})")
|
||||
elif status == "unknown":
|
||||
lines.append(f"{written}: not understood; write it in English (SAM 3 reads English only)")
|
||||
elif misses and english in misses and threshold is not None:
|
||||
lines.append(f"{name}: none at threshold {threshold:.2f}; the best candidate scored "
|
||||
f"{misses[english]:.2f}, lower the threshold to keep it")
|
||||
else:
|
||||
lines.append(f"{name}: none found; try another English word")
|
||||
for name in added:
|
||||
found = [seg for seg in instances if label_matches(seg.label, name)]
|
||||
lines.append(f"{name} (searched for background): {len(found)} found")
|
||||
return lines
|
||||
|
||||
|
||||
def preview_batch(image, segs):
|
||||
"""IMAGE batch with every instance of ``segs`` drawn on its image (tinted, outlined, labelled)."""
|
||||
if len(segs) != image.shape[0]:
|
||||
raise ValueError(
|
||||
f"[NH-Nodes] The image batch has {image.shape[0]} image(s) but the segs belong to {len(segs)}; "
|
||||
"connect the image the segs were found in."
|
||||
)
|
||||
pictures = []
|
||||
for picture, frame in zip(image, segs):
|
||||
if (picture.shape[0], picture.shape[1]) != (frame.height, frame.width):
|
||||
raise ValueError(
|
||||
f"[NH-Nodes] The image is {picture.shape[1]}x{picture.shape[0]} but the segs were found on a "
|
||||
f"{frame.width}x{frame.height} image; connect the image the segs were found in."
|
||||
)
|
||||
rgb = picture[..., :3] if picture.shape[-1] >= 3 else picture[..., :1].expand(-1, -1, 3)
|
||||
pixels = (rgb.float().clamp(0.0, 1.0) * 255.0).round().to(torch.uint8).cpu().numpy()
|
||||
pictures.append(torch.from_numpy(render_preview(pixels, frame).astype(np.float32) / 255.0))
|
||||
return torch.stack(pictures)
|
||||
@@ -0,0 +1,33 @@
|
||||
"""Pictures a node shows on itself in the ComfyUI editor (its "ui" images), saved to ComfyUI's temp folder.
|
||||
|
||||
A node returns ``{"ui": {"images": save_temp_previews(...)}, "result": (...)}`` and the editor draws the pictures
|
||||
on the node, so nobody has to wire a Preview Image node to see what it did.
|
||||
"""
|
||||
|
||||
import os
|
||||
import uuid
|
||||
|
||||
from PIL import Image
|
||||
|
||||
MAX_PREVIEWS = 16 # pictures per run; a long batch shows its first ones
|
||||
|
||||
|
||||
def save_temp_previews(pictures, prefix):
|
||||
"""Save uint8 RGB arrays as JPEG files in ComfyUI's temp folder; the "ui" image entries for them.
|
||||
|
||||
Outside ComfyUI (tests, scripts) there is no temp folder to serve them from, and nothing is saved.
|
||||
"""
|
||||
try:
|
||||
import folder_paths
|
||||
|
||||
folder = folder_paths.get_temp_directory()
|
||||
except Exception:
|
||||
return []
|
||||
os.makedirs(folder, exist_ok=True)
|
||||
entries = []
|
||||
run = uuid.uuid4().hex[:12]
|
||||
for index, picture in enumerate(pictures[:MAX_PREVIEWS]):
|
||||
name = f"{prefix}_{run}_{index}.jpg"
|
||||
Image.fromarray(picture).save(os.path.join(folder, name), quality=90)
|
||||
entries.append({"filename": name, "subfolder": "", "type": "temp"})
|
||||
return entries
|
||||
@@ -0,0 +1,150 @@
|
||||
"""Vietnamese words for the things NH vision nodes look for, in the English SAM 3 and the parsers understand.
|
||||
|
||||
SAM 3 reads English only: "tóc" finds nothing where "hair" finds the hair. ``translate`` turns the Vietnamese words
|
||||
people type most for photos (people, body, clothes, accessories, rooms, buildings, nature) into English, including a
|
||||
thing followed by colours or sizes ("áo sơ mi trắng" -> "white shirt"). English passes through unchanged; Vietnamese
|
||||
it does not know is reported, so the node can say what to write instead.
|
||||
"""
|
||||
|
||||
import re
|
||||
import unicodedata
|
||||
|
||||
# Things. Longer phrases win over their first word ("cửa sổ" over "cửa"), so both may be listed.
|
||||
NOUNS = {
|
||||
# people and body
|
||||
"người": "person", "con người": "person", "người mẫu": "person", "mọi người": "person",
|
||||
"phụ nữ": "woman", "người phụ nữ": "woman", "đàn bà": "woman", "người đàn bà": "woman", "cô gái": "girl",
|
||||
"con gái": "girl", "bé gái": "girl", "đàn ông": "man", "người đàn ông": "man", "chàng trai": "man",
|
||||
"con trai": "boy", "bé trai": "boy",
|
||||
"trẻ em": "child", "em bé": "baby", "cô dâu": "bride", "chú rể": "groom",
|
||||
"cơ thể": "body", "thân người": "body", "đầu": "head",
|
||||
"mặt": "face", "khuôn mặt": "face", "gương mặt": "face",
|
||||
"tóc": "hair", "mái tóc": "hair", "tóc vàng": "blonde hair",
|
||||
"da": "skin", "làn da": "skin", "da mặt": "face skin", "da người": "skin",
|
||||
"mắt": "eye", "đôi mắt": "eye", "lông mày": "eyebrow", "chân mày": "eyebrow", "lông mi": "eyelash",
|
||||
"mũi": "nose", "miệng": "mouth", "môi": "lips", "đôi môi": "lips", "răng": "teeth", "lưỡi": "tongue",
|
||||
"tai": "ear", "cổ": "neck", "vai": "shoulder", "ngực": "chest", "bụng": "belly",
|
||||
"tay": "arm", "cánh tay": "arm", "bàn tay": "hand", "ngón tay": "finger", "móng tay": "fingernail",
|
||||
"đôi tay": "arm", "hai tay": "arm", "tay trái": "left arm", "tay phải": "right arm", "bắp tay": "upper arm",
|
||||
"cẳng tay": "forearm",
|
||||
"chân": "leg", "đôi chân": "leg", "bàn chân": "foot", "móng chân": "toenail",
|
||||
"hai chân": "leg", "chân trái": "left leg", "chân phải": "right leg", "đùi": "thigh", "bắp đùi": "thigh",
|
||||
"bắp chân": "calf", "cẳng chân": "lower leg",
|
||||
"râu": "beard", "ria mép": "mustache",
|
||||
# clothes and accessories
|
||||
"quần áo": "clothes", "trang phục": "outfit", "áo": "top", "áo sơ mi": "shirt", "áo thun": "t-shirt",
|
||||
"áo phông": "t-shirt", "áo khoác": "jacket", "áo vest": "suit jacket", "áo len": "sweater",
|
||||
"áo dài": "ao dai", "áo choàng": "coat", "áo măng tô": "coat", "áo ngực": "bra",
|
||||
"váy": "dress", "đầm": "dress", "váy cưới": "wedding dress", "chân váy": "skirt",
|
||||
"quần": "pants", "quần dài": "pants", "quần âu": "trousers", "quần jean": "jeans", "quần bò": "jeans",
|
||||
"quần short": "shorts", "quần đùi": "shorts",
|
||||
"giày": "shoe", "giày cao gót": "high heels", "dép": "sandal", "tất": "sock", "vớ": "sock",
|
||||
"mũ": "hat", "nón": "hat", "khăn": "scarf", "khăn quàng": "scarf", "cà vạt": "tie",
|
||||
"thắt lưng": "belt", "dây lưng": "belt", "túi": "bag", "túi xách": "handbag", "ví": "wallet",
|
||||
"ba lô": "backpack", "balo": "backpack", "kính": "glasses", "mắt kính": "glasses",
|
||||
"kính mát": "sunglasses", "kính râm": "sunglasses", "đồng hồ": "watch", "nhẫn": "ring",
|
||||
"vòng cổ": "necklace", "dây chuyền": "necklace", "vòng tay": "bracelet", "lắc tay": "bracelet",
|
||||
"bông tai": "earring", "khuyên tai": "earring", "hoa tai": "earring", "trang sức": "jewelry",
|
||||
"găng tay": "glove", "ô": "umbrella",
|
||||
# nature and outdoors
|
||||
"trời": "sky", "bầu trời": "sky", "mây": "cloud", "đám mây": "cloud", "mặt trời": "sun",
|
||||
"cây": "tree", "cây cối": "tree", "lá": "leaf", "lá cây": "leaf", "cỏ": "grass", "bãi cỏ": "lawn",
|
||||
"hoa": "flower", "bông hoa": "flower", "bụi cây": "bush", "núi": "mountain", "đồi": "hill",
|
||||
"biển": "sea", "bãi biển": "beach", "cát": "sand", "nước": "water", "sông": "river", "hồ": "lake",
|
||||
"đá": "rock", "đường": "road", "con đường": "road", "vỉa hè": "sidewalk", "cầu": "bridge",
|
||||
# buildings and rooms
|
||||
"nhà": "house", "ngôi nhà": "house", "tòa nhà": "building", "toà nhà": "building",
|
||||
"mái": "roof", "mái nhà": "roof", "tường": "wall", "bức tường": "wall", "cửa": "door",
|
||||
"cửa ra vào": "door", "cửa sổ": "window", "ban công": "balcony", "cầu thang": "stairs",
|
||||
"cột": "pillar", "hàng rào": "fence", "sàn": "floor", "sàn nhà": "floor", "nền nhà": "floor",
|
||||
"trần": "ceiling", "trần nhà": "ceiling", "đèn": "lamp", "đèn trần": "ceiling light",
|
||||
"rèm": "curtain", "rèm cửa": "curtain", "thảm": "rug", "tranh": "painting", "bức tranh": "painting",
|
||||
"gương": "mirror", "kệ": "shelf", "tủ": "cabinet", "bàn": "table", "ghế": "chair",
|
||||
"ghế sofa": "sofa", "giường": "bed", "gối": "pillow", "chăn": "blanket", "cây cảnh": "potted plant",
|
||||
"chậu cây": "potted plant", "lọ hoa": "vase", "bình hoa": "vase", "ti vi": "television",
|
||||
"tivi": "television", "máy tính": "computer", "điện thoại": "phone", "sách": "book",
|
||||
# vehicles, food, objects, animals
|
||||
"xe": "vehicle", "ô tô": "car", "xe hơi": "car", "xe máy": "motorcycle", "xe đạp": "bicycle",
|
||||
"xe buýt": "bus", "thuyền": "boat", "máy bay": "airplane",
|
||||
"cốc": "cup", "tách": "cup", "ly": "glass", "chai": "bottle", "đĩa": "plate", "bát": "bowl",
|
||||
"chén": "bowl", "bánh": "cake", "đồ ăn": "food", "thức ăn": "food", "hộp": "box",
|
||||
"sản phẩm": "product", "chữ": "text", "biển hiệu": "sign", "bảng hiệu": "sign",
|
||||
"chó": "dog", "mèo": "cat", "chim": "bird", "ngựa": "horse", "cá": "fish",
|
||||
# mask formula keywords
|
||||
"nền": "background", "phông": "background", "phông nền": "background", "hậu cảnh": "background",
|
||||
"tất cả": "all", "toàn ảnh": "image", "cả ảnh": "image",
|
||||
}
|
||||
|
||||
# Words that describe a thing and come after it in Vietnamese ("áo đỏ" = "red top"). "xanh" alone is left out on
|
||||
# purpose: it means blue or green, so the node asks for "xanh dương" / "xanh lá" (or English) instead.
|
||||
ADJECTIVES = {
|
||||
"đỏ": "red", "xanh dương": "blue", "xanh da trời": "blue", "xanh lam": "blue", "xanh lá": "green",
|
||||
"xanh lá cây": "green", "xanh lục": "green", "vàng": "yellow", "trắng": "white", "đen": "black",
|
||||
"hồng": "pink", "tím": "purple", "cam": "orange", "nâu": "brown", "xám": "gray", "ghi": "gray",
|
||||
"bạc": "silver", "be": "beige", "kem": "cream",
|
||||
"lớn": "large", "to": "large", "nhỏ": "small", "dài": "long", "ngắn": "short",
|
||||
}
|
||||
|
||||
# Words that only count or point ("các áo" = the tops) and the word for colour ("áo màu đỏ" = a red top).
|
||||
_LEADING = ("các", "những", "mấy", "cái", "chiếc", "con")
|
||||
_COLOUR_WORD = "màu"
|
||||
|
||||
_VIETNAMESE = re.compile("[àáạảãâầấậẩẫăằắặẳẵèéẹẻẽêềếệểễìíịỉĩòóọỏõôồốộổỗơờớợởỡùúụủũưừứựửữỳýỵỷỹđ]")
|
||||
|
||||
|
||||
def normalize(text):
|
||||
"""Lower case, composed accents (what IMEs and pasted text differ in), single spaces."""
|
||||
return " ".join(unicodedata.normalize("NFC", str(text)).lower().split())
|
||||
|
||||
|
||||
def is_vietnamese(text):
|
||||
"""True when ``text`` has a letter only Vietnamese uses (an accent, đ)."""
|
||||
return bool(_VIETNAMESE.search(normalize(text)))
|
||||
|
||||
|
||||
def _longest_prefix(words, table):
|
||||
"""(English, number of words) for the longest leading phrase of ``words`` found in ``table``, or (None, 0)."""
|
||||
for size in range(len(words), 0, -1):
|
||||
english = table.get(" ".join(words[:size]))
|
||||
if english is not None:
|
||||
return english, size
|
||||
return None, 0
|
||||
|
||||
|
||||
def translate(text):
|
||||
"""(English words, status) for one thing someone typed; status is "english", "translated" or "unknown".
|
||||
|
||||
A whole phrase is looked up first, then a thing followed by colours or sizes. Text without Vietnamese letters is
|
||||
taken as English and returned unchanged, as is Vietnamese that is not in the lists ("unknown").
|
||||
"""
|
||||
phrase = normalize(text)
|
||||
if not is_vietnamese(phrase) and phrase not in NOUNS:
|
||||
return str(text).strip(), "english"
|
||||
words = phrase.split()
|
||||
english = _phrase(words)
|
||||
while english is None and len(words) > 1 and words[0] in _LEADING: # "những chiếc xe": drop the counting words
|
||||
words = words[1:]
|
||||
english = _phrase(words)
|
||||
return (english, "translated") if english is not None else (str(text).strip(), "unknown")
|
||||
|
||||
|
||||
def _phrase(words):
|
||||
"""English for a thing followed by colours or sizes, or None."""
|
||||
if " ".join(words) in NOUNS:
|
||||
return NOUNS[" ".join(words)]
|
||||
noun, used = _longest_prefix(words, NOUNS)
|
||||
if noun is None:
|
||||
return None
|
||||
adjectives, rest = [], words[used:]
|
||||
while rest:
|
||||
if rest[0] == _COLOUR_WORD:
|
||||
rest = rest[1:]
|
||||
continue
|
||||
english, size = _longest_prefix(rest, ADJECTIVES)
|
||||
if english is None:
|
||||
return None
|
||||
adjectives.append(english)
|
||||
rest = rest[size:]
|
||||
if noun == "background":
|
||||
return "background" # "nền trắng": the keyword, which needs no colour
|
||||
return " ".join([*adjectives, noun])
|
||||
@@ -3,6 +3,137 @@
|
||||
All notable changes to NH-Nodes. Versions follow the Comfy Registry releases.
|
||||
"Behaviour change" marks fixes that can change the output of an existing workflow.
|
||||
|
||||
## 1.10.0 — Image folders are limited to ComfyUI's folders and the ones you list (2026-11-12)
|
||||
|
||||
No node is added (76 in total) and no input or output changes. The first entry is a behaviour change that stops workflows which read or save outside ComfyUI until you list their folders once, so read it before you update.
|
||||
|
||||
### Changed
|
||||
- (behaviour change) `Save Image Path (NH)`, `Load Images Folder (NH)` and `Load images matching` only work inside ComfyUI's own folders and the folders you list on disk. Reading is allowed in ComfyUI's `input`, `output` and `temp` folders, writing in its `output` folder. To use any other folder (a photo drive, a project folder), list it under the new key `nh_image_folders` in `extra_model_paths.yaml` and restart ComfyUI; every folder below a listed one is allowed, for reading and for writing:
|
||||
|
||||
```yaml
|
||||
my_images:
|
||||
nh_image_folders: |
|
||||
D:/Photos/
|
||||
E:/Exports/
|
||||
```
|
||||
|
||||
A path anywhere else stops the run with an error that names the input and shows these lines. Nothing is read, listed or created first, and a network path (`\\server\share`) outside the list is not even contacted. A relative path with `..` that leaves `input` or `output` is refused in the same way. So is a link or junction that leads out of the allowed folders: as the folder you type, or as part of `new_folder_name` or `filename_prefix`, it stops the run; as a sub-folder it is skipped when sub-folders are read or searched; an image file that is such a link is not opened. If a sub-folder of `output` or of a listed folder is a link to another disk, list the place it leads to as well. Before 1.10.0 an absolute path was used as it was, anywhere the ComfyUI process could reach, so a workflow from someone else could read or write there. Only the file on disk can widen the list: a workflow cannot. `NH Save Large Image` and `Load Image Info (NH)` are unchanged: they leave the choice of folder to ComfyUI's own save and input-file handling.
|
||||
- (behaviour change) The image count that `Load Images Folder (NH)` asks the server for (`GET /nh-nodes/load-images-folder/count`) follows the same rule and answers `403` for a folder outside the list, without touching it. On the node, `increment` and `decrement` then move the index without wrapping it, as they do for a folder that does not exist; the run itself stops with the error above.
|
||||
- (behaviour change) `Load images matching`: `source_folder_path` is read as text only and the node no longer looks at the disk for it. A source that ends in an image file name (`…/subject_01/portrait.jpg`) stands for the folder of that file; a source file with another extension is no longer replaced by its folder. With a root inside a context folder (`…/Nam`), the sibling context folder (`…/Nu`) is only searched when the folder that holds both is allowed.
|
||||
- `Image Label (NH)`: `font_name` only accepts a name from its list. A text wired into it used to be opened as a path below the Windows font folder, so `..` or a network path reached other places; any other value now falls back to the default font, as a font that is not installed always did.
|
||||
- `Image Label (NH)` reads the list of installed fonts when ComfyUI first asks for the node list, no longer while the pack is imported. The list and the result are the same.
|
||||
- `VTON Ultimate Processor (NH)`: the bundled DWPose code no longer sets the environment variable `KMP_DUPLICATE_LIB_OK` for the whole ComfyUI process.
|
||||
- The note that `NH Smart Resolution Picker` shows for Qwen-Image is in English ("Qwen-Image matches the aspect ratio of the input image"), and the comments in the code are in English throughout.
|
||||
|
||||
### Notes
|
||||
- Folders listed under `nh_image_folders` do not show up in ComfyUI's model lists: NH-Nodes takes them out of the model folder names when it loads.
|
||||
- Updating from 1.9.0 or older: if a workflow saves to or loads from a folder outside ComfyUI, add that folder (or a folder above it) to `nh_image_folders` first. The guide has a section on it: [Folders outside ComfyUI](en/image-io.md#folders-outside-comfyui).
|
||||
|
||||
## 1.9.0 — Vision: body parts by name, Garment Segment on FASHN, parts in segs (2026-10-18)
|
||||
|
||||
One node is added, `Body Segment (NH)` (76 nodes in total). `Garment Segment (NH)`, `Portrait Segment (NH)`, `Segs Compose (NH)` and `Segs Select (NH)` change. Entries marked "behaviour change" can change what a saved workflow produces, so read them before you update. Workflows saved with 1.8.x still load and run: every new input is optional and every new output comes last. The licence notes at the end matter: `Garment Segment (NH)` is now documented as non-commercial, and `Body Segment (NH)` comes with use restrictions.
|
||||
|
||||
### Added
|
||||
- `Body Segment (NH)` (NH-Nodes → Vision) masks the parts of the people in a picture that you name. Sapiens2 (Meta, 2026) labels every pixel of each person with one of 28 classes (face and neck, hair, torso, arms, hands, legs, feet, lips, teeth, tongue, glasses, upper and lower clothes, shoes, socks, accessories) or as background. It parses people like `Garment Segment (NH)` does: the whole picture first, then crops of the model's 3:4 shape around each person or group, parsed at 1024 × 768 (a person gets about 1024 px whatever the size of the photo), YuNet faces help to find people, and the work is done on a copy of at most 4096 px that is enlarged at the end.
|
||||
- `Body Segment (NH)` reads `regions` in the formula language of `SAM3 Segment (NH)`: commas or new lines list several words, `+` and `|` join, `-` removes, `&` keeps the overlap, `~` inverts, parentheses group, and `background` (everything outside the people), `all`, `image` and `none` are keywords: `skin`, `hair, face`, `clothes - shoes`, `~person`, `skin - hands`. `and`, `or` and `not` are not operators here: they are refused with advice. The words are English (`skin`, `hair`, `face`, `arms`, `hands`, `legs`, `feet`, `torso`, `lips`, `teeth`, `tongue`, `mouth`, `glasses`, `clothes`, `top`, `bottom`, `shoes`, `socks`, `accessories`, `person`, with plurals, synonyms and `left` / `right` forms) or the Vietnamese words of the shared word list (`da`, `tóc`, `mặt`, `môi`, `răng`, `áo`, `quần`, `giày`…). In Vietnamese, `tay`, `đôi tay`, `hai tay` and `chân`, `đôi chân`, `hai chân` are the whole limb with the hand and the foot, `tay trái`, `tay phải`, `chân trái` and `chân phải` the whole limb of one side, and `cánh tay`, `bàn tay`, `bàn chân`, `bắp tay`, `cẳng tay`, `đùi`, `bắp chân` and `cẳng chân` the parts. Singular and plural are the same word (`lower leg` or `lower legs`). A word that has no region (`eyes`, `hat`, `bag`, `shoulders`, a wildcard, a Vietnamese word that is not in the list…) is refused with an error before the model loads, and the error says what to write instead.
|
||||
- `Body Segment (NH)` returns `mask`, `cutout_rgba`, `mask_image`, `segs` and `report`. `segs` holds one instance per part found: hair, face, torso, arms, hands, legs, feet, lips, teeth, tongue, glasses, top, bottom, shoes, socks, accessories. `report` has one line per word with its share of the picture ("under 0.1%" for a tiny share, "of the pictures" for a batch), then notes where Sapiens2 differs from everyday use, and a line "no person found (none Sapiens2 was sure of, and no face)" for a picture with nobody in it (in a batch, "... in image(s) 2, 3"). The preview says it too. `tay - cánh tay` leaves the hands: the whole arm minus the arm alone. The node shows its result on itself (new optional input `show_preview`, on by default: the mask bright and the rest darkened, the parts it keeps outlined and labelled, and the report lines below; at most 16 pictures) and is an output node, so ComfyUI runs it even when nothing is connected to it.
|
||||
- `Body Segment (NH)` has three model sizes in `model_size`: `0.4b` (the default; 1.6 GB download, about 1.5 to 2 GB of VRAM at the peak), `0.8b` (3.3 GB) and `1b` (5.9 GB). The weights are downloaded once from Hugging Face (`facebook/sapiens2-seg-<size>`, pinned revisions) into `models/NH-Nodes/sapiens2-seg-<size>/`, and only one size is kept in memory at a time. It needs PyTorch 2.4 or newer.
|
||||
- `Body Segment (NH)` empties the map of a picture with nobody in it. When no face is found, two tests must pass: Sapiens2's first look must be sure (a confidence of 0.9 or more) of at least half of what it labels as people, and a second look at the mirrored picture must find the same people (the overlap, IoU, of the two looks must be at least 0.6). Otherwise the picture has nobody in it, and the report and the preview say "no person found (none Sapiens2 was sure of, and no face)". The second look costs one more look, only when no face is found. Before this, a facade or a café interior got faint "people", and everyday edits of a facade (exposure ×1.03 to ×1.15, contrast, saturation, sharpening) could lift the share of the first test above one half, which is why the second test was added. Measured with ComfyUI's decoder: people without a face overlapped 0.83 to 0.99 with their mirror (edited copies and small people on a plain background went down to 0.64), what the model invents on a facade 0.02 to 0.03. On 49 pictures without a face, 32 photos of people (backs, bodies without a head, a mannequin) were kept and 17 pictures of no one (a facade, a café, an elephant, flat lays of products, masks) were emptied. On 21 everyday edits each of the facade, the café and the elephant, the map was always empty. On 21 edits each of four faceless photos of people (a mannequin, a headless product shot, a dancer, a product on a body), it was always kept. A person whose face is found is never dropped.
|
||||
- `Garment Segment (NH)` has a new optional input `model`: `fashn` (the default) or `fashionpedia`. `fashn` is FASHN Human Parser, a SegFormer-B4 for fashion with 18 classes (256 MB): clean tops, dresses, skirts, pants, bags, hats, scarves, glasses and jewelry. `fashionpedia` is the model of 1.8.0 and before, with shoes, socks, headbands and small parts (sleeves, collars, pockets).
|
||||
- `Garment Segment (NH)` has new choices for `part`, next to `upper_garment`, `lower_garment`, `footwear`, `headwear` and `all_garments`: `dress`, `bag`, `belt`, `scarf`, `glasses` and `jewelry`. `jewelry` needs `fashn`: `fashionpedia` has no jewelry class, so the mask is empty and the console says why. With `fashn`, `footwear` is FASHN's "feet" class (the feet with whatever they wear, so bare feet too), `all_garments` is the upper, lower, footwear and headwear groups (bare feet included, as shoes were with Fashionpedia), `headwear` is the hat (there is no headband class) and `attach_parts` does nothing, because FASHN counts pockets, zips and patches as part of the garment. `attach_parts` works with `fashionpedia` only.
|
||||
- `Garment Segment (NH)` and `Portrait Segment (NH)` have a new output, `segs` (NH_SEGS, last), with one instance per part found. Garment with `fashn`: face, hair, top, dress, skirt, pants, belt, bag, hat, scarf, glasses, arms, hands, legs, feet, torso, jewelry; with `fashionpedia`: its class names (shirt, top, sweater, …, sleeve, pocket, …). Portrait: skin, nose, glasses, eyes, eyebrows, ears, mouth, lips, hair, hat, earrings, necklace, neck, clothes. An instance covers its part on every person in the picture.
|
||||
- `Segs Compose (NH)` has two new optional inputs, `segs_2` and `segs_3`: the instances of up to three nodes (`SAM3 Segment (NH)`, `Garment Segment (NH)`, `Portrait Segment (NH)`, `Body Segment (NH)`) of the same picture are mixed into one formula. For example, `bag` from SAM 3 in `segs`, the Garment segs in `segs_2` and the Body segs in `segs_3`, then `dress - bag - lips`.
|
||||
- `Segs Compose (NH)` refuses segs of different pictures: another image count or size, or what looks like a different photo of the same size. Every frame of segs keeps a small grey print of its picture for this check. Edits of the same photo (brightness, contrast, colour, blur) are still accepted. The check is a guard against wiring mistakes, not a proof: it catches most other photos of the same size, a copy shifted or zoomed a little usually passes, and a new background behind the same person can fail. The new optional input `check_same_photo` (on by default) turns it off for edits like that. Segs made by older versions have no print and are mixed on size alone.
|
||||
- `Segs Compose (NH)` and `Segs Select (NH)` understand each node's own words on that node's segs. Each node is read on its own: where a node has no instance carrying the word as its label, the word is read as one of that node's own words and takes the parts it stands for, and an instance of one node that carries the word does not hide another node's word (with the segs of `Portrait Segment (NH)` and `Body Segment (NH)` together, `skin` is the skin instance of the first and the skin parts of the second). Only group words stand for several parts. `Body Segment (NH)`: `skin`, `body skin`, `head`, `mouth`, `clothes`, `clothing`, `outfit`, `person`, `body`, and the exact other names `upper clothes`, `lower clothes`, `apparel` (`skin` is its face, torso, arms, hands, legs and feet parts, `clothes` its top, bottom, shoes, socks and accessories parts), their Vietnamese words (`da`, `quần áo`, `người`…), and, in `Segs Compose (NH)`, the whole limbs: `tay`, `đôi tay`, `hai tay` are the arms with the hands, `chân`, `đôi chân`, `hai chân` the legs with the feet, while `cánh tay` stays the arms alone (the side forms `tay trái`… and the pieces `left hand`, `thigh` find nothing there). `Garment Segment (NH)`: the groups of `part` (`upper_garment`, `lower_garment`, `footwear`, `headwear`, `all_garments`; with `fashn`, `upper_garment` is top, dress and scarf and `footwear` the feet). `Portrait Segment (NH)`: `face` (skin, nose, glasses, eyes, eyebrows, mouth, lips), `face skin` (the skin part) and `mouth interior`. A word that is the name of one of a node's parts never stands for other parts (Fashionpedia's `belt` is only its belt). The near synonyms of `Body Segment (NH)` (`dress`, `pants`, `skirt`, `jeans`, `shirt`, `neck`…) are not read: write `top` or `bottom`, or mix in the Garment segs, which label dress, skirt and pants. For example, `bag` from `SAM3 Segment (NH)` in `segs` and the Body segs in `segs_2`, then `skin - bag`.
|
||||
|
||||
### Changed
|
||||
- (behaviour change) `Garment Segment (NH)` runs `fashn` by default. A saved workflow or API prompt without `model` (everything made before 1.9.0) now runs `fashn`, so its masks change. They are better: `all_garments` on an 8K group photo went from 7% to 33% of the picture, because Fashionpedia missed the dresses. `footwear` is now the feet (bare feet included) instead of shoes and socks, `headwear` has no headbands, and `attach_parts` does nothing. The first run downloads 256 MB, so a machine that is offline fails until the model is downloaded. To get the 1.8.0 result, set `model` to `fashionpedia`.
|
||||
- (behaviour change) `Garment Segment (NH)`: a region shorter than 128 px is no longer parsed again close up, and the labels of the first look stay there. This removed the salt-and-pepper labels that the models gave on small, ambiguous things (a helmet held in the hand). `Body Segment (NH)` follows the same rule.
|
||||
- (behaviour change) `Segs Compose (NH)` and `Segs Select (NH)` treat singular and plural as the same label: `arm` finds `arms`, `foot` finds `feet`, `shoe` finds `shoes`, `scarf` finds `scarves`, `calf` finds `calves`. A pattern now also finds a label that differs from it only by its plural (a SAM 3 concept written `people` is found by `person`). `glasses` (eyewear) and `glass` (a drinking glass) stay two labels. Wildcards are matched as written.
|
||||
- `SAM3 Segment (NH)` asks SAM 3 once for a concept that a list names in singular and plural (`arm, arms`), and its report lists it once. `glass, glasses` stay two concepts.
|
||||
- The console message of `Segs Compose (NH)` for a label that finds nothing also lists the labels that the segs hold.
|
||||
- `Body Segment (NH)` refuses `beard` and `mustache` with "a beard is part of 'face' here, and no NH-Nodes parser has a beard class (SAM3 Segment can try 'beard')". Its report notes about the face class, a dress and a skirt appear only when the picture has some of that region.
|
||||
- The `fill_holes` tooltip now differs per node: off for `Garment Segment (NH)` (an arm across the torso would be filled in), on for `Portrait Segment (NH)` (a face stays solid where glasses, earrings or hair cut into it).
|
||||
|
||||
### Notes
|
||||
- Licences. Read `THIRD_PARTY_NOTICES.md` before you use these nodes for paid work.
|
||||
- `Garment Segment (NH)` is for non-commercial use only. FASHN Human Parser is under the NVIDIA Source Code License for SegFormer, whose section 3.3 says the work "only may be used or intended for use non-commercially" and defines "non-commercially" as "for research or evaluation purposes only". The `fashionpedia` model declares no licence, but it is fine-tuned from `nvidia/mit-b3`, so the same licence plausibly applies. The notices of 1.8.0 and before listed this node as "unclear"; they now say "No".
|
||||
- `Body Segment (NH)` uses Meta's Sapiens2 under the Sapiens2 License: royalty-free, with no non-commercial clause, but it prohibits surveillance, biometric processing, identifying or re-identifying people, deepfakes, impersonation and misleading content, inferring health, demographic or other sensitive data without the rights and consents the law requires, pornographic or defamatory use, unlicensed professional practice, and ITAR, military, weapons, drugs and critical-infrastructure uses. You must comply with privacy and biometric laws (GDPR, CCPA, LGPD, 28 CFR 202) and with trade controls, and you may not reverse engineer the materials. Meta may audit your use, require deletion, terminate the licence and change the terms, and California law governs. The model code is bundled in `core/sapiens2/` with its licence; the weights are downloaded from Meta's Hugging Face repositories.
|
||||
- `Portrait Segment (NH)` is unchanged: non-commercial research and education only.
|
||||
- Known limit: FASHN can miss a pale garment close to the colour of the skin (a pale pink sleeveless top was only partly found). The model itself misses it, not the crops. `top` or `clothes` in `Body Segment (NH)` covers it better. FASHN can also label things held near the body as a top (champagne glasses).
|
||||
- Known limit: `Body Segment (NH)` on old, faded or sepia scans. Sapiens2 can take the whole backdrop for clothes (a scanned group photo came out 96% "person"). Use `Garment Segment (NH)` with `fashn`, or `SAM3 Segment (NH)` with `person`, there.
|
||||
- Known limits of the "nobody" tests in `Body Segment (NH)`: a garment alone (a flat lay or a product shot without a body) counts as nobody, so use `Garment Segment (NH)` for that, and a very stylised or blurred figure without a face can be dropped. A person whose face is found is never dropped.
|
||||
- In `Body Segment (NH)`, `face` is one Sapiens2 class with the neck, the eyes, the eyebrows and the beard, and `skin` includes it, so `skin - face` also removes the neck and `face - neck` is empty. A dress is labelled `top` (upper clothes): `dress - top` is empty. Left and right are the person's own sides. The report adds a note when you use such a word (for the face class, a dress and a skirt only when the picture has some of that region).
|
||||
|
||||
## 1.8.0 — Vision: masks from one node, faces and clothes parsed close up (2026-10-18)
|
||||
|
||||
No node is added (75 in total). `SAM3 Segment (NH)`, `Segs Compose (NH)`, `Segs Select (NH)`, `Portrait Segment (NH)` and `Garment Segment (NH)` change. Entries marked "behaviour change" can change what a saved workflow produces, so read them before you update. Workflows saved with 1.7.x still load and run: the new input of `SAM3 Segment (NH)` is optional and its new output comes last.
|
||||
|
||||
### Added
|
||||
- `SAM3 Segment (NH)` returns the mask of a formula, so one node gives "the person without the bag" or "the background". In `concepts`, commas or new lines list several things (their union, applied after everything else); `+` and `|` join, `-` removes (put spaces around it), `&` keeps the overlap, `~` is everything except, parentheses group, and `background` is everything outside all the instances that were found: `person - bag`, `hair, face`, `~person`, `background`. Several words in a row are one label (`red car`, no quotes), and `#` starts a comment where a word could start. Here `or`, `and` and `not` are plain words, so a phrase such as `black and white dog` reaches SAM 3 as written, and wildcards are refused (they work in Segs Select and Segs Compose). When a request uses `background` and does not name `person`, SAM 3 also looks for `person` on its own; those instances are part of `segs` and `count`.
|
||||
- `SAM3 Segment (NH)` has a new output, `report` (STRING, last): for each concept the number found and the best score, the English a Vietnamese word became, a concept that was not found with the best score it reached under `threshold`, words that were not understood, and what `background` meant.
|
||||
- `SAM3 Segment (NH)` shows its result on the node (new optional input `show_preview`, on by default): the mask bright and the rest darkened, the instances outlined and labelled, and the report lines below. The preview is at most 1536 px and shows up to 16 pictures of a batch.
|
||||
- Common Vietnamese words work in `SAM3 Segment (NH)`, `Segs Compose (NH)` and `Segs Select (NH)`: people, body, clothes, accessories, nature, rooms, vehicles, food and animals. A thing followed by colours or sizes is reordered (`áo đỏ` is `red top`, `tóc dài` is `long hair`, `tóc vàng` is `blonde hair`). `nền`, `phông`, `phông nền` and `hậu cảnh` (also with a colour, as in `nền trắng`) mean `background`. `xanh` after a thing is ambiguous (blue or green) and is reported: write `xanh dương` or `xanh lá`. The labels of the instances are the English words. The word list is `core/vi_words.py`.
|
||||
- `Segs Compose (NH)` reads the same formula language: commas and line breaks list several items, `background` (the same as `~all`) is new, several words in a row are one label (`red car`; quotes are still allowed and keep `all`, `image`, `none` and `background` as ordinary labels), `#` starts a comment, and `or`, `and`, `not` and the wildcards work as before. When `person hair` finds no label but `person` and `hair` both exist, the node stops and suggests `person + hair`.
|
||||
- `Portrait Segment (NH)` and `Garment Segment (NH)` find people with YuNet, a face detector (about 230 KB, MIT licence, from OpenCV Zoo). It is downloaded once into `models/NH-Nodes/yunet` and runs on the CPU with the OpenCV that NH-Nodes already needs. If it cannot be loaded (offline on the first run, or an OpenCV that cannot run it), the console prints one warning and the nodes parse whole pictures.
|
||||
- `Portrait Segment (NH)` parses each face on close-ups. The whole picture is parsed first (as a padded square, no longer squeezed). Faces are looked for on two sizes of the picture (at most 32; when none is found, the picture gets a grey margin and is searched once more, which finds a face cut by the frame), and each one is confirmed by the parser on a tight crop. A wide crop (hair, neck) and the tight crop (eyes, eyebrows, nose, lips, only inside an ellipse around the face) then refine the whole-picture parse. A face that already fills most of the frame keeps the whole-picture parse. Half-body and group photos now get eyes, eyebrows, nose and lips, and group photos get every face.
|
||||
- `Garment Segment (NH)` parses groups of people on crops. The whole picture is parsed first and kept. Groups of people (found from that parse and from faces) get crops of the model's shape (a row of crops for a wide group) that are parsed at the model's full resolution, and a crop grows when people run past its edge. A crop's labels replace the first look only where they are surer, away from the crop's edges, and only for parts on people already found, so nothing is invented on an empty backdrop.
|
||||
|
||||
### Changed
|
||||
- (behaviour change) `SAM3 Segment (NH)`: a concept list that contains a keyword (`all`, `image`, `none`, `background`) or an operator character (`+ - & ~ ! | ( )`) now acts as a formula. Lists of plain words give the same result as before. `person, background` is the whole picture (a list is a union); `salt & pepper shaker` is the overlap of `salt` and `pepper shaker` (put it in quotes to keep one label); `person (adult)` is an error; in `car #2` the `#2` is a comment, so it finds `car`.
|
||||
- (behaviour change) `Segs Compose (NH)`: a line break between two labels now lists them (it used to count as a space, which made it an error). Next to an operator a line break still joins, so formulas laid out over several lines keep working. Words in a row are one label: `person hair` used to be an error and is now a label (with the hint above when both parts exist), and `red car` needs no quotes.
|
||||
- (behaviour change) `Portrait Segment (NH)` and `Garment Segment (NH)` masks change on photos where the subject does not fill the frame; they are better (see Added). The whole-picture parse of Portrait Segment, which is also the whole result when no face is found, is now a padded square instead of the picture squeezed into the model's square. A picture with a side longer than twice the model's 512 px is shrunk with area averaging before the model sees it, which is faster and keeps fine detail clean, so masks of big photos can differ slightly. On a photo longer than 4096 px the label map is worked out on a 4096 px copy and enlarged at the end (the models see 512 px at a time, so nothing is lost). Once faces are found, face parts that the whole-picture parse saw far from all of them (a reflection, a pattern) are dropped.
|
||||
- `SAM3 Segment (NH)` is an output node: ComfyUI runs it even when nothing is connected to it. A copy you no longer need keeps running SAM 3 whenever its inputs change, until you mute or delete it.
|
||||
- The `mask` of `SAM3 Segment (NH)` is the result of the formula. A plain list of words still gives the union of every instance. When nothing is found, the mask is empty for a plain list and the whole picture for `background` or `~person`.
|
||||
- `SAM3 Segment (NH)` asks SAM 3 for candidates down to a score of 0.1 and applies `threshold` itself, so the report can say how close a missed concept came. The instances kept are the same as before: a score exactly at `threshold` is dropped, as SAM 3 does. Instances with exactly equal scores keep the order SAM 3 gave them (the sort used to be unspecified).
|
||||
|
||||
### Fixed
|
||||
- (behaviour change) SAM 3 masks of things that run off the picture now reach the image edge. SAM 3 fades its masks along the border of the square view it works in (the last 3 of 1008 rows and columns), which left a ragged gap along the edge of a person cut by the frame. That band is now lifted where the object reaches it from inside, also in the close-ups of `balanced`. A thing that ends within the band (about 0.3% of the picture's side) above the edge is drawn down to the edge.
|
||||
- (behaviour change) `Portrait Segment (NH)` found few or no eyes, eyebrows, nose and lips on half-body shots, and missed faces in group photos, because the model saw a small face squeezed into 512 px. `Garment Segment (NH)` had the same problem with small people. See Added.
|
||||
|
||||
### Notes
|
||||
- ComfyUI's Unload Models button does not release SAM 3's VRAM while `keep_model_loaded` is on. Turn it off and run the node once, or restart ComfyUI.
|
||||
- Known limit: the Fashionpedia model behind `Garment Segment (NH)` misses some tops, for example a blouse under a big hat.
|
||||
|
||||
### Planned
|
||||
Not part of this release, and the names may change: a better garment model.
|
||||
|
||||
## 1.7.1 — SAM3 Segment with other SAM 3 packs installed (2026-10-18)
|
||||
|
||||
### Fixed
|
||||
- `SAM3 Segment (NH)` failed with `FileNotFoundError: ...bpe_simple_vocab_16e6.txt.gz` when another pack's bundled copy of `sam3` was imported first. ComfyUI-RMBG puts its copy ahead of the installed package, and that copy looks for its tokenizer vocabulary one folder too high when it is not told where it is. NH-Nodes now always finds the vocabulary inside the `sam3` package that was actually imported and passes it, so the node works with Meta's package and with the copies that SAM 3 packs bundle.
|
||||
|
||||
## 1.7.0 — SAM 3 segmentation and instance nodes (2026-10-18)
|
||||
|
||||
### Added
|
||||
Six nodes under **NH-Nodes → Vision** (75 nodes in total) and a new data type, `NH_SEGS`. Nothing in the existing nodes changes.
|
||||
- `SAM3 Segment (NH)`: finds every instance of the concepts you name (short English noun phrases, one per line or separated by commas) with Meta's SAM 3. Outputs the instances (`segs`), their union `mask` and the `count`; `Segs Preview (NH)` shows what it found. `detail_level` `balanced` (default) segments each instance a second time on a close-up cut from the full-resolution image, so small and medium objects get crisp edges even on 4K to 8K photos; `fast` keeps the whole-image pass only. Other inputs: `threshold`, `max_per_concept`, `checkpoint` and `keep_model_loaded`.
|
||||
- `NH_SEGS`: for every image of a batch, a list of instances with a label, a score, a tight box and the mask inside that box only, so dozens of instances on an 8K photo stay light. Full-size masks are built only when a node outputs a `MASK`.
|
||||
- `Segs Select (NH)`: keeps instances by label (`*` and `?` wildcards), score and size, sorts them (score, area, position, centre first, as found) and takes a slice, for example the second person from the left.
|
||||
- `Segs Compose (NH)`: one mask from a formula over labels: `|` `+` `or` (union), `-` (minus), `&` `and` (overlap), `~` `!` `not` (everything except), parentheses, and `all`, `image`, `none`. `~person` is the background, `(wall | ceiling) - window` the walls without the windows. A hyphen inside a word belongs to the label (`t-shirt` needs no quotes), so put spaces around a minus: `person - hair`.
|
||||
- `Segs To Masks (NH)`: one mask per instance, the union per image, the labels and the count. It stops with a message when the masks would need more than about half of the free RAM, for example hundreds of instances on an 8K photo.
|
||||
- `Segs To Impact SEGS (NH)`: Impact Pack's `SEGS` for one image of the batch (`crop_factor` 3.0 like Impact Pack). Impact Pack is not needed for anything else in NH-Nodes.
|
||||
- `Segs Preview (NH)`: draws the instances (tint, outline, label and score) on the picture they were found in.
|
||||
|
||||
### Notes
|
||||
- SAM3 Segment needs an NVIDIA GPU with CUDA: the official sam3 code builds part of the model on the GPU, so there is no CPU mode.
|
||||
- NH-Nodes does not install or download anything for it. You install Meta's `sam3` package yourself (the Vision guide gives the command, pinned to the commit NH-Nodes was tested with; newer commits with the same image API also work), and the weights are gated on Hugging Face: request access, accept Meta's SAM License and put `sam3.pt` in `models/sam3` (or a folder registered as `sam3` in `extra_model_paths.yaml`). The folder is shared with other SAM 3 packs, so an existing copy is found. The licence is in `THIRD_PARTY_NOTICES.md`.
|
||||
- The model stays in system memory between runs and moves to the GPU only while the node runs, unless `keep_model_loaded` is on.
|
||||
- `detail_level` has two levels for now, `balanced` and `fast`.
|
||||
- SAM 3 describes whatever it looks at on a grid of about 288 cells. A subject that fills most of an 8K frame therefore gets edges at about 1/300 of its size: fine for the outline of a person, soft on hair, lace and fur. Hair-level edges are not part of this release.
|
||||
- SAM 3 first sees the whole picture squeezed into a square, so on a picture wider or taller than 4:1 it can miss small things. The console says so; split such a picture into tiles with `Image Tile (NH)`.
|
||||
- A file in the `sam3` folder that is not a SAM 3 checkpoint, or an incomplete download, is refused with the download advice before anything runs.
|
||||
|
||||
### Planned
|
||||
Not part of this release, and the names may change: scene parsing that labels the whole picture (sky, wall, floor…), automatic concept tagging with ready presets and a report, matte refinement for hair and fur edges (it will add a `max` detail level), and parsing of each person's parts.
|
||||
|
||||
## 1.6.2 — Load images matching picks the right group
|
||||
|
||||
- `Load images matching` (behaviour change): when the source and the matching library share parent folders (for example
|
||||
`D:/Photos/Source/Nu/…` and `D:/Photos/Library/{Nam,Nu}/…`), the shared folders no longer outweigh the source's
|
||||
group, so a source in `Nu` picks `Nu/<match>` instead of the first folder in alphabetical order. Same-drive and
|
||||
cross-drive setups now rank identically, and folders elsewhere in the library no longer change which match wins.
|
||||
|
||||
## 1.6.1 — Cleanup and hardening
|
||||
|
||||
- Internal restructuring with no change to node ids, inputs or outputs: the large image-tools module is split into one
|
||||
|
||||
@@ -0,0 +1,241 @@
|
||||
"""Render a ComfyUI-style SVG card for every NH-Nodes node from its INPUT_TYPES / RETURN_TYPES.
|
||||
|
||||
Run with ComfyUI's Python from the ComfyUI root:
|
||||
python custom_nodes/NH-Nodes/docs/_tools/build_node_cards.py [output_dir]
|
||||
Cards are generated from the live node definitions, so they never drift from the code.
|
||||
"""
|
||||
|
||||
import importlib
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from html import escape
|
||||
|
||||
COMFY_ROOT = os.getcwd()
|
||||
sys.path[:0] = [COMFY_ROOT, os.path.join(COMFY_ROOT, "custom_nodes")]
|
||||
|
||||
import torch # noqa: E402
|
||||
|
||||
if not torch.cuda.is_available():
|
||||
from comfy.cli_args import args # noqa: E402
|
||||
|
||||
args.cpu = True
|
||||
|
||||
PACK_DIR = os.path.join(COMFY_ROOT, "custom_nodes", "NH-Nodes")
|
||||
OUT_DIR = sys.argv[1] if len(sys.argv) > 1 else os.path.join(PACK_DIR, "docs", "assets", "nodes")
|
||||
|
||||
# ComfyUI default dark palette (frontend 1.55) for link types; NH custom types get amber.
|
||||
TYPE_COLORS = {
|
||||
"IMAGE": "#64B5F6", "MASK": "#81C784", "LATENT": "#FF9CF9", "MODEL": "#B39DDB",
|
||||
"CLIP": "#FFD500", "VAE": "#FF6E6E", "CONDITIONING": "#FFA931", "CONTROL_NET": "#6EE7B7",
|
||||
"INT": "#9CC2E5", "FLOAT": "#9CC2E5", "STRING": "#C5E1A5", "BOOLEAN": "#F48FB1", "*": "#CFD8DC",
|
||||
}
|
||||
CUSTOM_TYPE_COLOR = "#E0B272"
|
||||
WIDGET_TYPES = {"INT", "FLOAT", "STRING", "BOOLEAN"}
|
||||
|
||||
W = 360
|
||||
PAD = 12
|
||||
TITLE_H = 46
|
||||
ROW_H = 22
|
||||
WIDGET_H = 24
|
||||
WIDGET_GAP = 5
|
||||
FONT = "Inter, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif"
|
||||
MONO = "'JetBrains Mono', Consolas, 'Courier New', monospace"
|
||||
|
||||
|
||||
FILE_EXTENSIONS = (".png", ".jpg", ".jpeg", ".webp", ".bmp", ".tif", ".tiff", ".gif", ".safetensors", ".ckpt",
|
||||
".pt", ".pth", ".bin", ".gguf", ".onnx", ".sft")
|
||||
|
||||
|
||||
def is_file_combo(options, opts):
|
||||
"""Combos listing files from the user's folders (inputs, models): never publish those names."""
|
||||
if opts.get("image_upload") or opts.get("video_upload"):
|
||||
return True
|
||||
return any(str(option).lower().endswith(FILE_EXTENSIONS) for option in options)
|
||||
|
||||
|
||||
def type_name(spec_type):
|
||||
if isinstance(spec_type, (list, tuple)):
|
||||
return "COMBO"
|
||||
return str(spec_type)
|
||||
|
||||
|
||||
def type_color(name):
|
||||
return TYPE_COLORS.get(name, CUSTOM_TYPE_COLOR)
|
||||
|
||||
|
||||
def short(value, limit=24):
|
||||
text = str(value).replace("\n", " ⏎ ")
|
||||
return text if len(text) <= limit else text[: limit - 1] + "…"
|
||||
|
||||
|
||||
def fmt_number(value, opts):
|
||||
if isinstance(value, float):
|
||||
step = opts.get("step", 0.01) or 0.01
|
||||
decimals = max(0, min(4, len(str(step).split(".")[1]) if "." in str(step) else 0))
|
||||
return f"{value:.{decimals}f}"
|
||||
return str(value)
|
||||
|
||||
|
||||
def collapse_numbered(entries):
|
||||
"""Fold long numbered families (lora_1..lora_64, enable_1..) into first two + a summary row."""
|
||||
families = {}
|
||||
for name, *_ in entries:
|
||||
match = re.match(r"^(.*?)(\d+)$", name)
|
||||
if match:
|
||||
families.setdefault(match.group(1), []).append(int(match.group(2)))
|
||||
folded = {prefix: max(nums) for prefix, nums in families.items() if len(nums) > 4}
|
||||
result, emitted = [], set()
|
||||
for entry in entries:
|
||||
match = re.match(r"^(.*?)(\d+)$", entry[0])
|
||||
if match and match.group(1) in folded:
|
||||
prefix, num = match.group(1), int(match.group(2))
|
||||
if num <= 2:
|
||||
result.append(entry)
|
||||
elif prefix not in emitted:
|
||||
emitted.add(prefix)
|
||||
result.append((f"{prefix}3 … {prefix}{folded[prefix]}", "FOLD", {}, entry[3]))
|
||||
continue
|
||||
result.append(entry)
|
||||
return result
|
||||
|
||||
|
||||
def node_entries(cls):
|
||||
spec = cls.INPUT_TYPES()
|
||||
entries = []
|
||||
for section in ("required", "optional"):
|
||||
for name, value in (spec.get(section) or {}).items():
|
||||
spec_type = value[0] if isinstance(value, (list, tuple)) and value else value
|
||||
opts = value[1] if isinstance(value, (list, tuple)) and len(value) > 1 and isinstance(value[1], dict) else {}
|
||||
entries.append((name, spec_type, opts, section == "optional"))
|
||||
return collapse_numbered(entries)
|
||||
|
||||
|
||||
def split_entries(entries):
|
||||
sockets, widgets = [], []
|
||||
for name, spec_type, opts, optional in entries:
|
||||
tname = type_name(spec_type)
|
||||
is_widget = tname == "COMBO" or tname == "FOLD" or (tname in WIDGET_TYPES and not opts.get("forceInput"))
|
||||
(widgets if is_widget else sockets).append((name, spec_type, opts, optional))
|
||||
return sockets, widgets
|
||||
|
||||
|
||||
def widget_value(spec_type, opts):
|
||||
tname = type_name(spec_type)
|
||||
if tname == "COMBO":
|
||||
options = list(spec_type)
|
||||
if is_file_combo(options, opts):
|
||||
return "(choose a file)", ""
|
||||
default = opts.get("default", options[0] if options else "")
|
||||
return (short(default) if default != "" else "—"), f"{len(options)} options" if len(options) > 1 else ""
|
||||
default = opts.get("default", "")
|
||||
if tname == "BOOLEAN":
|
||||
return ("true" if default else "false"), ""
|
||||
if tname in ("INT", "FLOAT"):
|
||||
return fmt_number(default, opts), ""
|
||||
if tname == "STRING":
|
||||
return short(default, 30) if default != "" else "", "multiline" if opts.get("multiline") else ""
|
||||
return short(default), ""
|
||||
|
||||
|
||||
def svg_card(node_id, display, category, cls):
|
||||
entries = node_entries(cls)
|
||||
sockets, widgets = split_entries(entries)
|
||||
return_types = [str(t) for t in getattr(cls, "RETURN_TYPES", ())]
|
||||
return_names = list(getattr(cls, "RETURN_NAMES", None) or return_types)
|
||||
is_list = list(getattr(cls, "OUTPUT_IS_LIST", ()) or [])
|
||||
output_node = getattr(cls, "OUTPUT_NODE", False)
|
||||
|
||||
io_rows = max(len(sockets), len(return_types))
|
||||
multiline_rows = sum(1 for _, t, o, _ in widgets if type_name(t) == "STRING" and o.get("multiline"))
|
||||
widgets_h = len(widgets) * (WIDGET_H + WIDGET_GAP) + multiline_rows * 22
|
||||
height = TITLE_H + 10 + io_rows * ROW_H + (8 if io_rows and widgets else 0) + widgets_h + PAD
|
||||
|
||||
parts = [
|
||||
f'<svg xmlns="http://www.w3.org/2000/svg" width="{W}" height="{height}" viewBox="0 0 {W} {height}" '
|
||||
f'font-family="{FONT}" role="img" aria-label="{escape(display)} node">',
|
||||
f'<rect x="0.5" y="0.5" width="{W - 1}" height="{height - 1}" rx="10" fill="#26282c" stroke="#45484f"/>',
|
||||
f'<path d="M0.5 10.5 a10 10 0 0 1 10 -10 h{W - 21} a10 10 0 0 1 10 10 v{TITLE_H - 10} h-{W - 1} z" fill="#34373d"/>',
|
||||
f'<circle cx="{PAD + 5}" cy="17" r="4.5" fill="{"#F6AD55" if output_node else "#8FD18F"}"/>',
|
||||
f'<text x="{PAD + 16}" y="22" font-size="14" font-weight="600" fill="#EEF0F3">{escape(short(display, 40))}</text>',
|
||||
f'<text x="{PAD + 16}" y="38" font-size="10.5" fill="#9AA0A8" font-family="{MONO}">'
|
||||
f'{escape(node_id)} · {escape(category.replace("NH-Nodes/", ""))}</text>',
|
||||
]
|
||||
|
||||
y = TITLE_H + 10
|
||||
for index in range(io_rows):
|
||||
cy = y + index * ROW_H + ROW_H / 2
|
||||
if index < len(sockets):
|
||||
name, spec_type, opts, optional = sockets[index]
|
||||
tname = type_name(spec_type)
|
||||
color = type_color(tname)
|
||||
parts.append(f'<circle cx="{PAD}" cy="{cy}" r="5" fill="{color}" stroke="#1b1c1f" stroke-width="1.5"/>')
|
||||
label = escape(name) + (' <tspan fill="#7d838c" font-style="italic">(optional)</tspan>' if optional else "")
|
||||
parts.append(f'<text x="{PAD + 12}" y="{cy + 4}" font-size="12" fill="#D7DADF">{label}</text>')
|
||||
if index < len(return_types):
|
||||
tname = return_types[index]
|
||||
color = type_color(tname)
|
||||
name = return_names[index] if index < len(return_names) else tname
|
||||
list_mark = " ≡" if index < len(is_list) and is_list[index] else ""
|
||||
parts.append(f'<text x="{W - PAD - 12}" y="{cy + 4}" font-size="12" fill="#D7DADF" text-anchor="end">'
|
||||
f'{escape(name)}{list_mark}</text>')
|
||||
parts.append(f'<circle cx="{W - PAD}" cy="{cy}" r="5" fill="{color}" stroke="#1b1c1f" stroke-width="1.5"/>')
|
||||
y += io_rows * ROW_H + (8 if io_rows and widgets else 0)
|
||||
|
||||
for name, spec_type, opts, optional in widgets:
|
||||
tname = type_name(spec_type)
|
||||
multiline = tname == "STRING" and opts.get("multiline")
|
||||
h = WIDGET_H + (22 if multiline else 0)
|
||||
parts.append(f'<rect x="{PAD}" y="{y}" width="{W - 2 * PAD}" height="{h}" rx="7" fill="#1d1f22" stroke="#3a3d43"/>')
|
||||
if tname == "FOLD":
|
||||
parts.append(f'<text x="{W / 2}" y="{y + 16}" font-size="11" fill="#9AA0A8" text-anchor="middle" '
|
||||
f'font-style="italic">{escape(name)}</text>')
|
||||
y += h + WIDGET_GAP
|
||||
continue
|
||||
value, hint = widget_value(spec_type, opts)
|
||||
label = escape(name) + (' <tspan fill="#6f747c" font-style="italic">opt</tspan>' if optional else "")
|
||||
if multiline:
|
||||
parts.append(f'<text x="{PAD + 10}" y="{y + 16}" font-size="11.5" fill="#9AA0A8">{label}</text>')
|
||||
parts.append(f'<text x="{PAD + 10}" y="{y + 36}" font-size="11.5" fill="#E6E8EB" font-family="{MONO}">'
|
||||
f'{escape(value) or "…"}</text>')
|
||||
elif tname == "BOOLEAN":
|
||||
on = value == "true"
|
||||
parts.append(f'<text x="{PAD + 10}" y="{y + 16}" font-size="11.5" fill="#9AA0A8">{label}</text>')
|
||||
tx = W - PAD - 40
|
||||
parts.append(f'<rect x="{tx}" y="{y + 6}" width="30" height="13" rx="6.5" fill="{"#4C8BF5" if on else "#4a4d53"}"/>')
|
||||
parts.append(f'<circle cx="{tx + (23 if on else 7)}" cy="{y + 12.5}" r="5" fill="#F5F6F7"/>')
|
||||
else:
|
||||
arrows = tname in ("INT", "FLOAT", "COMBO")
|
||||
if arrows:
|
||||
parts.append(f'<text x="{PAD + 8}" y="{y + 16}" font-size="10" fill="#6f747c">◀</text>')
|
||||
parts.append(f'<text x="{W - PAD - 16}" y="{y + 16}" font-size="10" fill="#6f747c">▶</text>')
|
||||
lx = PAD + (22 if arrows else 10)
|
||||
rx = W - PAD - (22 if arrows else 10)
|
||||
parts.append(f'<text x="{lx}" y="{y + 16}" font-size="11.5" fill="#9AA0A8">{label}</text>')
|
||||
parts.append(f'<text x="{rx}" y="{y + 16}" font-size="11.5" fill="#E6E8EB" text-anchor="end">{escape(value)}</text>')
|
||||
y += h + WIDGET_GAP
|
||||
|
||||
parts.append("</svg>")
|
||||
return "\n".join(parts) + "\n"
|
||||
|
||||
|
||||
def main():
|
||||
pack = importlib.import_module("NH-Nodes")
|
||||
os.makedirs(OUT_DIR, exist_ok=True)
|
||||
index = []
|
||||
for node_id, cls in sorted(pack.NODE_CLASS_MAPPINGS.items()):
|
||||
display = pack.NODE_DISPLAY_NAME_MAPPINGS.get(node_id, node_id)
|
||||
category = getattr(cls, "CATEGORY", "")
|
||||
svg = svg_card(node_id, display, category, cls)
|
||||
with open(os.path.join(OUT_DIR, f"{node_id}.svg"), "w", encoding="utf-8", newline="\n") as handle:
|
||||
handle.write(svg)
|
||||
index.append({"id": node_id, "name": display, "category": category,
|
||||
"deprecated": bool(getattr(cls, "DEPRECATED", False))})
|
||||
with open(os.path.join(OUT_DIR, "index.json"), "w", encoding="utf-8", newline="\n") as handle:
|
||||
json.dump(index, handle, ensure_ascii=False, indent=1)
|
||||
print(f"wrote {len(index)} cards to {OUT_DIR}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,82 @@
|
||||
"""Dump every NH node's live definition (inputs with defaults/ranges/options/tooltips, outputs) to JSON.
|
||||
Run with ComfyUI's Python from the ComfyUI root: python dump_node_spec.py <out.json>
|
||||
"""
|
||||
import importlib
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path[:0] = [os.getcwd(), os.path.join(os.getcwd(), "custom_nodes")]
|
||||
import torch # noqa: E402
|
||||
|
||||
if not torch.cuda.is_available():
|
||||
from comfy.cli_args import args # noqa: E402
|
||||
|
||||
args.cpu = True
|
||||
|
||||
pack = importlib.import_module("NH-Nodes")
|
||||
|
||||
|
||||
FILE_EXTENSIONS = (".png", ".jpg", ".jpeg", ".webp", ".bmp", ".tif", ".tiff", ".gif", ".safetensors", ".ckpt",
|
||||
".pt", ".pth", ".bin", ".gguf", ".onnx", ".sft")
|
||||
|
||||
|
||||
def is_file_combo(options, opts):
|
||||
"""Combos listing files from the user's folders (inputs, models): never publish those names."""
|
||||
if opts.get("image_upload") or opts.get("video_upload"):
|
||||
return True
|
||||
return any(str(option).lower().endswith(FILE_EXTENSIONS) for option in options)
|
||||
|
||||
|
||||
def jsonable(value):
|
||||
try:
|
||||
json.dumps(value)
|
||||
return value
|
||||
except TypeError:
|
||||
return str(value)
|
||||
|
||||
|
||||
spec = []
|
||||
for node_id, cls in sorted(pack.NODE_CLASS_MAPPINGS.items()):
|
||||
inputs = []
|
||||
for section, entries in (cls.INPUT_TYPES() or {}).items():
|
||||
if section == "hidden":
|
||||
continue
|
||||
for name, value in (entries or {}).items():
|
||||
spec_type = value[0] if isinstance(value, (list, tuple)) and value else value
|
||||
opts = value[1] if isinstance(value, (list, tuple)) and len(value) > 1 and isinstance(value[1], dict) else {}
|
||||
entry = {"name": name, "section": section}
|
||||
if isinstance(spec_type, (list, tuple)):
|
||||
entry["type"] = "COMBO"
|
||||
options = [str(o) for o in spec_type]
|
||||
if is_file_combo(options, opts):
|
||||
entry["options"] = ["(files from the user's folder)"]
|
||||
opts = {k: v for k, v in opts.items() if k != "default"}
|
||||
else:
|
||||
entry["options"] = options if len(options) <= 40 else options[:40] + [f"... ({len(options)} total)"]
|
||||
else:
|
||||
entry["type"] = str(spec_type)
|
||||
for key in ("default", "min", "max", "step", "multiline", "tooltip", "forceInput", "lazy"):
|
||||
if key in opts:
|
||||
entry[key] = jsonable(opts[key])
|
||||
inputs.append(entry)
|
||||
names = list(getattr(cls, "RETURN_NAMES", None) or getattr(cls, "RETURN_TYPES", ()))
|
||||
types = [str(t) for t in getattr(cls, "RETURN_TYPES", ())]
|
||||
is_list = list(getattr(cls, "OUTPUT_IS_LIST", ()) or [])
|
||||
outputs = [{"name": n, "type": t, "is_list": bool(is_list[i]) if i < len(is_list) else False}
|
||||
for i, (n, t) in enumerate(zip(names, types))]
|
||||
spec.append({
|
||||
"id": node_id,
|
||||
"name": pack.NODE_DISPLAY_NAME_MAPPINGS.get(node_id, node_id),
|
||||
"category": getattr(cls, "CATEGORY", ""),
|
||||
"module": cls.__module__.replace("NH-Nodes.", ""),
|
||||
"description": getattr(cls, "DESCRIPTION", "") or (cls.__doc__ or "").strip(),
|
||||
"output_node": bool(getattr(cls, "OUTPUT_NODE", False)),
|
||||
"deprecated": bool(getattr(cls, "DEPRECATED", False)),
|
||||
"inputs": inputs,
|
||||
"outputs": outputs,
|
||||
})
|
||||
|
||||
with open(sys.argv[1], "w", encoding="utf-8") as handle:
|
||||
json.dump(spec, handle, ensure_ascii=False, indent=1)
|
||||
print(f"{len(spec)} nodes -> {sys.argv[1]}")
|
||||
|
After Width: | Height: | Size: 147 KiB |
|
After Width: | Height: | Size: 156 KiB |
|
After Width: | Height: | Size: 198 KiB |
|
After Width: | Height: | Size: 115 KiB |
|
After Width: | Height: | Size: 136 KiB |
|
After Width: | Height: | Size: 173 KiB |
|
After Width: | Height: | Size: 100 KiB |
|
After Width: | Height: | Size: 131 KiB |
|
After Width: | Height: | Size: 125 KiB |
|
After Width: | Height: | Size: 137 KiB |
|
After Width: | Height: | Size: 179 KiB |
|
After Width: | Height: | Size: 81 KiB |
|
After Width: | Height: | Size: 147 KiB |
|
After Width: | Height: | Size: 77 KiB |
|
After Width: | Height: | Size: 170 KiB |
|
After Width: | Height: | Size: 193 KiB |
|
After Width: | Height: | Size: 80 KiB |
|
After Width: | Height: | Size: 84 KiB |
@@ -0,0 +1,42 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="360" height="287" viewBox="0 0 360 287" font-family="Inter, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" role="img" aria-label="NH Agnostic Image Generator node">
|
||||
<rect x="0.5" y="0.5" width="359" height="286" rx="10" fill="#26282c" stroke="#45484f"/>
|
||||
<path d="M0.5 10.5 a10 10 0 0 1 10 -10 h339 a10 10 0 0 1 10 10 v36 h-359 z" fill="#34373d"/>
|
||||
<circle cx="17" cy="17" r="4.5" fill="#8FD18F"/>
|
||||
<text x="28" y="22" font-size="14" font-weight="600" fill="#EEF0F3">NH Agnostic Image Generator</text>
|
||||
<text x="28" y="38" font-size="10.5" fill="#9AA0A8" font-family="'JetBrains Mono', Consolas, 'Courier New', monospace">NH_AgnosticImageGenerator · Image</text>
|
||||
<circle cx="12" cy="67.0" r="5" fill="#64B5F6" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="24" y="71.0" font-size="12" fill="#D7DADF">image</text>
|
||||
<text x="336" y="71.0" font-size="12" fill="#D7DADF" text-anchor="end">agnostic_img</text>
|
||||
<circle cx="348" cy="67.0" r="5" fill="#64B5F6" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<circle cx="12" cy="89.0" r="5" fill="#81C784" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="24" y="93.0" font-size="12" fill="#D7DADF">mask</text>
|
||||
<text x="336" y="93.0" font-size="12" fill="#D7DADF" text-anchor="end">masked_img</text>
|
||||
<circle cx="348" cy="89.0" r="5" fill="#64B5F6" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="336" y="115.0" font-size="12" fill="#D7DADF" text-anchor="end">composite</text>
|
||||
<circle cx="348" cy="111.0" r="5" fill="#64B5F6" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<rect x="12" y="130" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="20" y="146" font-size="10" fill="#6f747c">◀</text>
|
||||
<text x="332" y="146" font-size="10" fill="#6f747c">▶</text>
|
||||
<text x="34" y="146" font-size="11.5" fill="#9AA0A8">fill_mode</text>
|
||||
<text x="326" y="146" font-size="11.5" fill="#E6E8EB" text-anchor="end">gray</text>
|
||||
<rect x="12" y="159" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="20" y="175" font-size="10" fill="#6f747c">◀</text>
|
||||
<text x="332" y="175" font-size="10" fill="#6f747c">▶</text>
|
||||
<text x="34" y="175" font-size="11.5" fill="#9AA0A8">blur_radius</text>
|
||||
<text x="326" y="175" font-size="11.5" fill="#E6E8EB" text-anchor="end">65</text>
|
||||
<rect x="12" y="188" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="20" y="204" font-size="10" fill="#6f747c">◀</text>
|
||||
<text x="332" y="204" font-size="10" fill="#6f747c">▶</text>
|
||||
<text x="34" y="204" font-size="11.5" fill="#9AA0A8">noise_strength</text>
|
||||
<text x="326" y="204" font-size="11.5" fill="#E6E8EB" text-anchor="end">1.00</text>
|
||||
<rect x="12" y="217" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="20" y="233" font-size="10" fill="#6f747c">◀</text>
|
||||
<text x="332" y="233" font-size="10" fill="#6f747c">▶</text>
|
||||
<text x="34" y="233" font-size="11.5" fill="#9AA0A8">gray_value</text>
|
||||
<text x="326" y="233" font-size="11.5" fill="#E6E8EB" text-anchor="end">0.50</text>
|
||||
<rect x="12" y="246" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="20" y="262" font-size="10" fill="#6f747c">◀</text>
|
||||
<text x="332" y="262" font-size="10" fill="#6f747c">▶</text>
|
||||
<text x="34" y="262" font-size="11.5" fill="#9AA0A8">feathering</text>
|
||||
<text x="326" y="262" font-size="11.5" fill="#E6E8EB" text-anchor="end">0</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.3 KiB |
@@ -0,0 +1,36 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="360" height="347" viewBox="0 0 360 347" font-family="Inter, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" role="img" aria-label="Any List Split (NH) node">
|
||||
<rect x="0.5" y="0.5" width="359" height="346" rx="10" fill="#26282c" stroke="#45484f"/>
|
||||
<path d="M0.5 10.5 a10 10 0 0 1 10 -10 h339 a10 10 0 0 1 10 10 v36 h-359 z" fill="#34373d"/>
|
||||
<circle cx="17" cy="17" r="4.5" fill="#8FD18F"/>
|
||||
<text x="28" y="22" font-size="14" font-weight="600" fill="#EEF0F3">Any List Split (NH)</text>
|
||||
<text x="28" y="38" font-size="10.5" fill="#9AA0A8" font-family="'JetBrains Mono', Consolas, 'Courier New', monospace">NH_AnyListSplit · Batch</text>
|
||||
<circle cx="12" cy="67.0" r="5" fill="#CFD8DC" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="24" y="71.0" font-size="12" fill="#D7DADF">items</text>
|
||||
<text x="336" y="71.0" font-size="12" fill="#D7DADF" text-anchor="end">All ≡</text>
|
||||
<circle cx="348" cy="67.0" r="5" fill="#CFD8DC" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="336" y="93.0" font-size="12" fill="#D7DADF" text-anchor="end">index 1</text>
|
||||
<circle cx="348" cy="89.0" r="5" fill="#CFD8DC" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="336" y="115.0" font-size="12" fill="#D7DADF" text-anchor="end">index 2</text>
|
||||
<circle cx="348" cy="111.0" r="5" fill="#CFD8DC" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="336" y="137.0" font-size="12" fill="#D7DADF" text-anchor="end">index 3</text>
|
||||
<circle cx="348" cy="133.0" r="5" fill="#CFD8DC" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="336" y="159.0" font-size="12" fill="#D7DADF" text-anchor="end">index 4</text>
|
||||
<circle cx="348" cy="155.0" r="5" fill="#CFD8DC" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="336" y="181.0" font-size="12" fill="#D7DADF" text-anchor="end">index 5</text>
|
||||
<circle cx="348" cy="177.0" r="5" fill="#CFD8DC" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="336" y="203.0" font-size="12" fill="#D7DADF" text-anchor="end">index 6</text>
|
||||
<circle cx="348" cy="199.0" r="5" fill="#CFD8DC" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="336" y="225.0" font-size="12" fill="#D7DADF" text-anchor="end">index 7</text>
|
||||
<circle cx="348" cy="221.0" r="5" fill="#CFD8DC" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="336" y="247.0" font-size="12" fill="#D7DADF" text-anchor="end">index 8</text>
|
||||
<circle cx="348" cy="243.0" r="5" fill="#CFD8DC" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="336" y="269.0" font-size="12" fill="#D7DADF" text-anchor="end">index 9</text>
|
||||
<circle cx="348" cy="265.0" r="5" fill="#CFD8DC" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="336" y="291.0" font-size="12" fill="#D7DADF" text-anchor="end">index 10</text>
|
||||
<circle cx="348" cy="287.0" r="5" fill="#CFD8DC" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<rect x="12" y="306" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="20" y="322" font-size="10" fill="#6f747c">◀</text>
|
||||
<text x="332" y="322" font-size="10" fill="#6f747c">▶</text>
|
||||
<text x="34" y="322" font-size="11.5" fill="#9AA0A8">index_count</text>
|
||||
<text x="326" y="322" font-size="11.5" fill="#E6E8EB" text-anchor="end">4</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.0 KiB |
@@ -0,0 +1,26 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="360" height="237" viewBox="0 0 360 237" font-family="Inter, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" role="img" aria-label="Any Branch Switch (NH) node">
|
||||
<rect x="0.5" y="0.5" width="359" height="236" rx="10" fill="#26282c" stroke="#45484f"/>
|
||||
<path d="M0.5 10.5 a10 10 0 0 1 10 -10 h339 a10 10 0 0 1 10 10 v36 h-359 z" fill="#34373d"/>
|
||||
<circle cx="17" cy="17" r="4.5" fill="#8FD18F"/>
|
||||
<text x="28" y="22" font-size="14" font-weight="600" fill="#EEF0F3">Any Branch Switch (NH)</text>
|
||||
<text x="28" y="38" font-size="10.5" fill="#9AA0A8" font-family="'JetBrains Mono', Consolas, 'Courier New', monospace">NH_AnySwitch · Logic</text>
|
||||
<circle cx="12" cy="67.0" r="5" fill="#CFD8DC" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="24" y="71.0" font-size="12" fill="#D7DADF">input</text>
|
||||
<text x="336" y="71.0" font-size="12" fill="#D7DADF" text-anchor="end">output 0</text>
|
||||
<circle cx="348" cy="67.0" r="5" fill="#CFD8DC" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="336" y="93.0" font-size="12" fill="#D7DADF" text-anchor="end">output 1</text>
|
||||
<circle cx="348" cy="89.0" r="5" fill="#CFD8DC" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="336" y="115.0" font-size="12" fill="#D7DADF" text-anchor="end">output 2</text>
|
||||
<circle cx="348" cy="111.0" r="5" fill="#CFD8DC" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="336" y="137.0" font-size="12" fill="#D7DADF" text-anchor="end">output 3</text>
|
||||
<circle cx="348" cy="133.0" r="5" fill="#CFD8DC" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="336" y="159.0" font-size="12" fill="#D7DADF" text-anchor="end">output 4</text>
|
||||
<circle cx="348" cy="155.0" r="5" fill="#CFD8DC" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="336" y="181.0" font-size="12" fill="#D7DADF" text-anchor="end">index</text>
|
||||
<circle cx="348" cy="177.0" r="5" fill="#9CC2E5" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<rect x="12" y="196" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="20" y="212" font-size="10" fill="#6f747c">◀</text>
|
||||
<text x="332" y="212" font-size="10" fill="#6f747c">▶</text>
|
||||
<text x="34" y="212" font-size="11.5" fill="#9AA0A8">index</text>
|
||||
<text x="326" y="212" font-size="11.5" fill="#E6E8EB" text-anchor="end">0</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.2 KiB |
@@ -0,0 +1,19 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="360" height="149" viewBox="0 0 360 149" font-family="Inter, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" role="img" aria-label="Any Switch (NH) node">
|
||||
<rect x="0.5" y="0.5" width="359" height="148" rx="10" fill="#26282c" stroke="#45484f"/>
|
||||
<path d="M0.5 10.5 a10 10 0 0 1 10 -10 h339 a10 10 0 0 1 10 10 v36 h-359 z" fill="#34373d"/>
|
||||
<circle cx="17" cy="17" r="4.5" fill="#8FD18F"/>
|
||||
<text x="28" y="22" font-size="14" font-weight="600" fill="#EEF0F3">Any Switch (NH)</text>
|
||||
<text x="28" y="38" font-size="10.5" fill="#9AA0A8" font-family="'JetBrains Mono', Consolas, 'Courier New', monospace">NH_AnySwitchBoolean · Logic</text>
|
||||
<circle cx="12" cy="67.0" r="5" fill="#CFD8DC" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="24" y="71.0" font-size="12" fill="#D7DADF">input_true</text>
|
||||
<text x="336" y="71.0" font-size="12" fill="#D7DADF" text-anchor="end">result</text>
|
||||
<circle cx="348" cy="67.0" r="5" fill="#CFD8DC" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<circle cx="12" cy="89.0" r="5" fill="#CFD8DC" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="24" y="93.0" font-size="12" fill="#D7DADF">input_false</text>
|
||||
<text x="336" y="93.0" font-size="12" fill="#D7DADF" text-anchor="end">condition</text>
|
||||
<circle cx="348" cy="89.0" r="5" fill="#F48FB1" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<rect x="12" y="108" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="22" y="124" font-size="11.5" fill="#9AA0A8">condition</text>
|
||||
<rect x="308" y="114" width="30" height="13" rx="6.5" fill="#4C8BF5"/>
|
||||
<circle cx="331" cy="120.5" r="5" fill="#F5F6F7"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
@@ -0,0 +1,28 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="360" height="207" viewBox="0 0 360 207" font-family="Inter, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" role="img" aria-label="Batch Index (NH) node">
|
||||
<rect x="0.5" y="0.5" width="359" height="206" rx="10" fill="#26282c" stroke="#45484f"/>
|
||||
<path d="M0.5 10.5 a10 10 0 0 1 10 -10 h339 a10 10 0 0 1 10 10 v36 h-359 z" fill="#34373d"/>
|
||||
<circle cx="17" cy="17" r="4.5" fill="#8FD18F"/>
|
||||
<text x="28" y="22" font-size="14" font-weight="600" fill="#EEF0F3">Batch Index (NH)</text>
|
||||
<text x="28" y="38" font-size="10.5" fill="#9AA0A8" font-family="'JetBrains Mono', Consolas, 'Courier New', monospace">NH_BatchIndex · Batch</text>
|
||||
<circle cx="12" cy="67.0" r="5" fill="#64B5F6" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="24" y="71.0" font-size="12" fill="#D7DADF">batch</text>
|
||||
<text x="336" y="71.0" font-size="12" fill="#D7DADF" text-anchor="end">result</text>
|
||||
<circle cx="348" cy="67.0" r="5" fill="#64B5F6" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="336" y="93.0" font-size="12" fill="#D7DADF" text-anchor="end">original_count</text>
|
||||
<circle cx="348" cy="89.0" r="5" fill="#9CC2E5" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<rect x="12" y="108" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="20" y="124" font-size="10" fill="#6f747c">◀</text>
|
||||
<text x="332" y="124" font-size="10" fill="#6f747c">▶</text>
|
||||
<text x="34" y="124" font-size="11.5" fill="#9AA0A8">index</text>
|
||||
<text x="326" y="124" font-size="11.5" fill="#E6E8EB" text-anchor="end">0</text>
|
||||
<rect x="12" y="137" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="20" y="153" font-size="10" fill="#6f747c">◀</text>
|
||||
<text x="332" y="153" font-size="10" fill="#6f747c">▶</text>
|
||||
<text x="34" y="153" font-size="11.5" fill="#9AA0A8">end_index <tspan fill="#6f747c" font-style="italic">opt</tspan></text>
|
||||
<text x="326" y="153" font-size="11.5" fill="#E6E8EB" text-anchor="end">-1</text>
|
||||
<rect x="12" y="166" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="20" y="182" font-size="10" fill="#6f747c">◀</text>
|
||||
<text x="332" y="182" font-size="10" fill="#6f747c">▶</text>
|
||||
<text x="34" y="182" font-size="11.5" fill="#9AA0A8">step <tspan fill="#6f747c" font-style="italic">opt</tspan></text>
|
||||
<text x="326" y="182" font-size="11.5" fill="#E6E8EB" text-anchor="end">1</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.3 KiB |
@@ -0,0 +1,22 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="360" height="171" viewBox="0 0 360 171" font-family="Inter, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" role="img" aria-label="Batch Merge (NH) node">
|
||||
<rect x="0.5" y="0.5" width="359" height="170" rx="10" fill="#26282c" stroke="#45484f"/>
|
||||
<path d="M0.5 10.5 a10 10 0 0 1 10 -10 h339 a10 10 0 0 1 10 10 v36 h-359 z" fill="#34373d"/>
|
||||
<circle cx="17" cy="17" r="4.5" fill="#8FD18F"/>
|
||||
<text x="28" y="22" font-size="14" font-weight="600" fill="#EEF0F3">Batch Merge (NH)</text>
|
||||
<text x="28" y="38" font-size="10.5" fill="#9AA0A8" font-family="'JetBrains Mono', Consolas, 'Courier New', monospace">NH_BatchMerge · Batch</text>
|
||||
<circle cx="12" cy="67.0" r="5" fill="#64B5F6" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="24" y="71.0" font-size="12" fill="#D7DADF">batch_a</text>
|
||||
<text x="336" y="71.0" font-size="12" fill="#D7DADF" text-anchor="end">result</text>
|
||||
<circle cx="348" cy="67.0" r="5" fill="#64B5F6" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<circle cx="12" cy="89.0" r="5" fill="#64B5F6" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="24" y="93.0" font-size="12" fill="#D7DADF">batch_b <tspan fill="#7d838c" font-style="italic">(optional)</tspan></text>
|
||||
<text x="336" y="93.0" font-size="12" fill="#D7DADF" text-anchor="end">count</text>
|
||||
<circle cx="348" cy="89.0" r="5" fill="#9CC2E5" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<circle cx="12" cy="111.0" r="5" fill="#64B5F6" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="24" y="115.0" font-size="12" fill="#D7DADF">batch_c <tspan fill="#7d838c" font-style="italic">(optional)</tspan></text>
|
||||
<rect x="12" y="130" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="20" y="146" font-size="10" fill="#6f747c">◀</text>
|
||||
<text x="332" y="146" font-size="10" fill="#6f747c">▶</text>
|
||||
<text x="34" y="146" font-size="11.5" fill="#9AA0A8">resize_mode <tspan fill="#6f747c" font-style="italic">opt</tspan></text>
|
||||
<text x="326" y="146" font-size="11.5" fill="#E6E8EB" text-anchor="end">stretch</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.0 KiB |
@@ -0,0 +1,50 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="360" height="411" viewBox="0 0 360 411" font-family="Inter, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" role="img" aria-label="Body Segment (NH) node">
|
||||
<rect x="0.5" y="0.5" width="359" height="410" rx="10" fill="#26282c" stroke="#45484f"/>
|
||||
<path d="M0.5 10.5 a10 10 0 0 1 10 -10 h339 a10 10 0 0 1 10 10 v36 h-359 z" fill="#34373d"/>
|
||||
<circle cx="17" cy="17" r="4.5" fill="#F6AD55"/>
|
||||
<text x="28" y="22" font-size="14" font-weight="600" fill="#EEF0F3">Body Segment (NH)</text>
|
||||
<text x="28" y="38" font-size="10.5" fill="#9AA0A8" font-family="'JetBrains Mono', Consolas, 'Courier New', monospace">NH_BodySegment · Vision</text>
|
||||
<circle cx="12" cy="67.0" r="5" fill="#64B5F6" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="24" y="71.0" font-size="12" fill="#D7DADF">image</text>
|
||||
<text x="336" y="71.0" font-size="12" fill="#D7DADF" text-anchor="end">mask</text>
|
||||
<circle cx="348" cy="67.0" r="5" fill="#81C784" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="336" y="93.0" font-size="12" fill="#D7DADF" text-anchor="end">cutout_rgba</text>
|
||||
<circle cx="348" cy="89.0" r="5" fill="#64B5F6" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="336" y="115.0" font-size="12" fill="#D7DADF" text-anchor="end">mask_image</text>
|
||||
<circle cx="348" cy="111.0" r="5" fill="#64B5F6" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="336" y="137.0" font-size="12" fill="#D7DADF" text-anchor="end">segs</text>
|
||||
<circle cx="348" cy="133.0" r="5" fill="#E0B272" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="336" y="159.0" font-size="12" fill="#D7DADF" text-anchor="end">report</text>
|
||||
<circle cx="348" cy="155.0" r="5" fill="#C5E1A5" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<rect x="12" y="174" width="336" height="46" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="22" y="190" font-size="11.5" fill="#9AA0A8">regions</text>
|
||||
<text x="22" y="210" font-size="11.5" fill="#E6E8EB" font-family="'JetBrains Mono', Consolas, 'Courier New', monospace">skin</text>
|
||||
<rect x="12" y="225" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="20" y="241" font-size="10" fill="#6f747c">◀</text>
|
||||
<text x="332" y="241" font-size="10" fill="#6f747c">▶</text>
|
||||
<text x="34" y="241" font-size="11.5" fill="#9AA0A8">model_size</text>
|
||||
<text x="326" y="241" font-size="11.5" fill="#E6E8EB" text-anchor="end">0.4b</text>
|
||||
<rect x="12" y="254" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="20" y="270" font-size="10" fill="#6f747c">◀</text>
|
||||
<text x="332" y="270" font-size="10" fill="#6f747c">▶</text>
|
||||
<text x="34" y="270" font-size="11.5" fill="#9AA0A8">mask_expand</text>
|
||||
<text x="326" y="270" font-size="11.5" fill="#E6E8EB" text-anchor="end">0</text>
|
||||
<rect x="12" y="283" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="20" y="299" font-size="10" fill="#6f747c">◀</text>
|
||||
<text x="332" y="299" font-size="10" fill="#6f747c">▶</text>
|
||||
<text x="34" y="299" font-size="11.5" fill="#9AA0A8">mask_blur</text>
|
||||
<text x="326" y="299" font-size="11.5" fill="#E6E8EB" text-anchor="end">0</text>
|
||||
<rect x="12" y="312" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="22" y="328" font-size="11.5" fill="#9AA0A8">fill_holes</text>
|
||||
<rect x="308" y="318" width="30" height="13" rx="6.5" fill="#4a4d53"/>
|
||||
<circle cx="315" cy="324.5" r="5" fill="#F5F6F7"/>
|
||||
<rect x="12" y="341" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="20" y="357" font-size="10" fill="#6f747c">◀</text>
|
||||
<text x="332" y="357" font-size="10" fill="#6f747c">▶</text>
|
||||
<text x="34" y="357" font-size="11.5" fill="#9AA0A8">device</text>
|
||||
<text x="326" y="357" font-size="11.5" fill="#E6E8EB" text-anchor="end">cuda</text>
|
||||
<rect x="12" y="370" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="22" y="386" font-size="11.5" fill="#9AA0A8">show_preview <tspan fill="#6f747c" font-style="italic">opt</tspan></text>
|
||||
<rect x="308" y="376" width="30" height="13" rx="6.5" fill="#4C8BF5"/>
|
||||
<circle cx="331" cy="382.5" r="5" fill="#F5F6F7"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.0 KiB |
@@ -0,0 +1,13 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="360" height="127" viewBox="0 0 360 127" font-family="Inter, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" role="img" aria-label="Boolean Switch (NH) node">
|
||||
<rect x="0.5" y="0.5" width="359" height="126" rx="10" fill="#26282c" stroke="#45484f"/>
|
||||
<path d="M0.5 10.5 a10 10 0 0 1 10 -10 h339 a10 10 0 0 1 10 10 v36 h-359 z" fill="#34373d"/>
|
||||
<circle cx="17" cy="17" r="4.5" fill="#8FD18F"/>
|
||||
<text x="28" y="22" font-size="14" font-weight="600" fill="#EEF0F3">Boolean Switch (NH)</text>
|
||||
<text x="28" y="38" font-size="10.5" fill="#9AA0A8" font-family="'JetBrains Mono', Consolas, 'Courier New', monospace">NH_BooleanSwitch · Utils</text>
|
||||
<text x="336" y="71.0" font-size="12" fill="#D7DADF" text-anchor="end">boolean</text>
|
||||
<circle cx="348" cy="67.0" r="5" fill="#F48FB1" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<rect x="12" y="86" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="22" y="102" font-size="11.5" fill="#9AA0A8">boolean_switch</text>
|
||||
<rect x="308" y="92" width="30" height="13" rx="6.5" fill="#4C8BF5"/>
|
||||
<circle cx="331" cy="98.5" r="5" fill="#F5F6F7"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1,55 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="360" height="432" viewBox="0 0 360 432" font-family="Inter, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" role="img" aria-label="Load Clip Index (NH) node">
|
||||
<rect x="0.5" y="0.5" width="359" height="431" rx="10" fill="#26282c" stroke="#45484f"/>
|
||||
<path d="M0.5 10.5 a10 10 0 0 1 10 -10 h339 a10 10 0 0 1 10 10 v36 h-359 z" fill="#34373d"/>
|
||||
<circle cx="17" cy="17" r="4.5" fill="#8FD18F"/>
|
||||
<text x="28" y="22" font-size="14" font-weight="600" fill="#EEF0F3">Load Clip Index (NH)</text>
|
||||
<text x="28" y="38" font-size="10.5" fill="#9AA0A8" font-family="'JetBrains Mono', Consolas, 'Courier New', monospace">NH_ClipIndex · Loaders</text>
|
||||
<text x="336" y="71.0" font-size="12" fill="#D7DADF" text-anchor="end">clip</text>
|
||||
<circle cx="348" cy="67.0" r="5" fill="#FFD500" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="336" y="93.0" font-size="12" fill="#D7DADF" text-anchor="end">filename</text>
|
||||
<circle cx="348" cy="89.0" r="5" fill="#C5E1A5" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="336" y="115.0" font-size="12" fill="#D7DADF" text-anchor="end">index</text>
|
||||
<circle cx="348" cy="111.0" r="5" fill="#9CC2E5" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<rect x="12" y="130" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="20" y="146" font-size="10" fill="#6f747c">◀</text>
|
||||
<text x="332" y="146" font-size="10" fill="#6f747c">▶</text>
|
||||
<text x="34" y="146" font-size="11.5" fill="#9AA0A8">select_mode</text>
|
||||
<text x="326" y="146" font-size="11.5" fill="#E6E8EB" text-anchor="end">index</text>
|
||||
<rect x="12" y="159" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="20" y="175" font-size="10" fill="#6f747c">◀</text>
|
||||
<text x="332" y="175" font-size="10" fill="#6f747c">▶</text>
|
||||
<text x="34" y="175" font-size="11.5" fill="#9AA0A8">type</text>
|
||||
<text x="326" y="175" font-size="11.5" fill="#E6E8EB" text-anchor="end">stable_diffusion</text>
|
||||
<rect x="12" y="188" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="20" y="204" font-size="10" fill="#6f747c">◀</text>
|
||||
<text x="332" y="204" font-size="10" fill="#6f747c">▶</text>
|
||||
<text x="34" y="204" font-size="11.5" fill="#9AA0A8">index</text>
|
||||
<text x="326" y="204" font-size="11.5" fill="#E6E8EB" text-anchor="end">1</text>
|
||||
<rect x="12" y="217" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="20" y="233" font-size="10" fill="#6f747c">◀</text>
|
||||
<text x="332" y="233" font-size="10" fill="#6f747c">▶</text>
|
||||
<text x="34" y="233" font-size="11.5" fill="#9AA0A8">clip_count</text>
|
||||
<text x="326" y="233" font-size="11.5" fill="#E6E8EB" text-anchor="end">5</text>
|
||||
<rect x="12" y="246" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="20" y="262" font-size="10" fill="#6f747c">◀</text>
|
||||
<text x="332" y="262" font-size="10" fill="#6f747c">▶</text>
|
||||
<text x="34" y="262" font-size="11.5" fill="#9AA0A8">clip_1 <tspan fill="#6f747c" font-style="italic">opt</tspan></text>
|
||||
<text x="326" y="262" font-size="11.5" fill="#E6E8EB" text-anchor="end">(choose a file)</text>
|
||||
<rect x="12" y="275" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="22" y="291" font-size="11.5" fill="#9AA0A8">enable_1 <tspan fill="#6f747c" font-style="italic">opt</tspan></text>
|
||||
<rect x="308" y="281" width="30" height="13" rx="6.5" fill="#4a4d53"/>
|
||||
<circle cx="315" cy="287.5" r="5" fill="#F5F6F7"/>
|
||||
<rect x="12" y="304" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="20" y="320" font-size="10" fill="#6f747c">◀</text>
|
||||
<text x="332" y="320" font-size="10" fill="#6f747c">▶</text>
|
||||
<text x="34" y="320" font-size="11.5" fill="#9AA0A8">clip_2 <tspan fill="#6f747c" font-style="italic">opt</tspan></text>
|
||||
<text x="326" y="320" font-size="11.5" fill="#E6E8EB" text-anchor="end">(choose a file)</text>
|
||||
<rect x="12" y="333" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="22" y="349" font-size="11.5" fill="#9AA0A8">enable_2 <tspan fill="#6f747c" font-style="italic">opt</tspan></text>
|
||||
<rect x="308" y="339" width="30" height="13" rx="6.5" fill="#4a4d53"/>
|
||||
<circle cx="315" cy="345.5" r="5" fill="#F5F6F7"/>
|
||||
<rect x="12" y="362" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="180.0" y="378" font-size="11" fill="#9AA0A8" text-anchor="middle" font-style="italic">clip_3 … clip_64</text>
|
||||
<rect x="12" y="391" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="180.0" y="407" font-size="11" fill="#9AA0A8" text-anchor="middle" font-style="italic">enable_3 … enable_64</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.5 KiB |
@@ -0,0 +1,27 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="360" height="200" viewBox="0 0 360 200" font-family="Inter, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" role="img" aria-label="Compare (NH) node">
|
||||
<rect x="0.5" y="0.5" width="359" height="199" rx="10" fill="#26282c" stroke="#45484f"/>
|
||||
<path d="M0.5 10.5 a10 10 0 0 1 10 -10 h339 a10 10 0 0 1 10 10 v36 h-359 z" fill="#34373d"/>
|
||||
<circle cx="17" cy="17" r="4.5" fill="#8FD18F"/>
|
||||
<text x="28" y="22" font-size="14" font-weight="600" fill="#EEF0F3">Compare (NH)</text>
|
||||
<text x="28" y="38" font-size="10.5" fill="#9AA0A8" font-family="'JetBrains Mono', Consolas, 'Courier New', monospace">NH_Compare · Logic</text>
|
||||
<circle cx="12" cy="67.0" r="5" fill="#CFD8DC" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="24" y="71.0" font-size="12" fill="#D7DADF">a</text>
|
||||
<text x="336" y="71.0" font-size="12" fill="#D7DADF" text-anchor="end">result</text>
|
||||
<circle cx="348" cy="67.0" r="5" fill="#F48FB1" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<circle cx="12" cy="89.0" r="5" fill="#CFD8DC" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="24" y="93.0" font-size="12" fill="#D7DADF">b</text>
|
||||
<text x="336" y="93.0" font-size="12" fill="#D7DADF" text-anchor="end">a_passthrough</text>
|
||||
<circle cx="348" cy="89.0" r="5" fill="#CFD8DC" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="336" y="115.0" font-size="12" fill="#D7DADF" text-anchor="end">b_passthrough</text>
|
||||
<circle cx="348" cy="111.0" r="5" fill="#CFD8DC" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<rect x="12" y="130" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="20" y="146" font-size="10" fill="#6f747c">◀</text>
|
||||
<text x="332" y="146" font-size="10" fill="#6f747c">▶</text>
|
||||
<text x="34" y="146" font-size="11.5" fill="#9AA0A8">op</text>
|
||||
<text x="326" y="146" font-size="11.5" fill="#E6E8EB" text-anchor="end">is</text>
|
||||
<rect x="12" y="159" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="20" y="175" font-size="10" fill="#6f747c">◀</text>
|
||||
<text x="332" y="175" font-size="10" fill="#6f747c">▶</text>
|
||||
<text x="34" y="175" font-size="11.5" fill="#9AA0A8">type_cast <tspan fill="#6f747c" font-style="italic">opt</tspan></text>
|
||||
<text x="326" y="175" font-size="11.5" fill="#E6E8EB" text-anchor="end">auto</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.2 KiB |
@@ -0,0 +1,34 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="360" height="280" viewBox="0 0 360 280" font-family="Inter, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" role="img" aria-label="Counter (NH) node">
|
||||
<rect x="0.5" y="0.5" width="359" height="279" rx="10" fill="#26282c" stroke="#45484f"/>
|
||||
<path d="M0.5 10.5 a10 10 0 0 1 10 -10 h339 a10 10 0 0 1 10 10 v36 h-359 z" fill="#34373d"/>
|
||||
<circle cx="17" cy="17" r="4.5" fill="#8FD18F"/>
|
||||
<text x="28" y="22" font-size="14" font-weight="600" fill="#EEF0F3">Counter (NH)</text>
|
||||
<text x="28" y="38" font-size="10.5" fill="#9AA0A8" font-family="'JetBrains Mono', Consolas, 'Courier New', monospace">NH_Counter · Batch</text>
|
||||
<text x="336" y="71.0" font-size="12" fill="#D7DADF" text-anchor="end">current</text>
|
||||
<circle cx="348" cy="67.0" r="5" fill="#9CC2E5" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="336" y="93.0" font-size="12" fill="#D7DADF" text-anchor="end">is_done</text>
|
||||
<circle cx="348" cy="89.0" r="5" fill="#F48FB1" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="336" y="115.0" font-size="12" fill="#D7DADF" text-anchor="end">progress</text>
|
||||
<circle cx="348" cy="111.0" r="5" fill="#9CC2E5" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="336" y="137.0" font-size="12" fill="#D7DADF" text-anchor="end">remaining</text>
|
||||
<circle cx="348" cy="133.0" r="5" fill="#9CC2E5" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<rect x="12" y="152" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="20" y="168" font-size="10" fill="#6f747c">◀</text>
|
||||
<text x="332" y="168" font-size="10" fill="#6f747c">▶</text>
|
||||
<text x="34" y="168" font-size="11.5" fill="#9AA0A8">start</text>
|
||||
<text x="326" y="168" font-size="11.5" fill="#E6E8EB" text-anchor="end">0</text>
|
||||
<rect x="12" y="181" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="20" y="197" font-size="10" fill="#6f747c">◀</text>
|
||||
<text x="332" y="197" font-size="10" fill="#6f747c">▶</text>
|
||||
<text x="34" y="197" font-size="11.5" fill="#9AA0A8">step</text>
|
||||
<text x="326" y="197" font-size="11.5" fill="#E6E8EB" text-anchor="end">1</text>
|
||||
<rect x="12" y="210" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="20" y="226" font-size="10" fill="#6f747c">◀</text>
|
||||
<text x="332" y="226" font-size="10" fill="#6f747c">▶</text>
|
||||
<text x="34" y="226" font-size="11.5" fill="#9AA0A8">max_value</text>
|
||||
<text x="326" y="226" font-size="11.5" fill="#E6E8EB" text-anchor="end">9</text>
|
||||
<rect x="12" y="239" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="22" y="255" font-size="11.5" fill="#9AA0A8">reset</text>
|
||||
<rect x="308" y="245" width="30" height="13" rx="6.5" fill="#4a4d53"/>
|
||||
<circle cx="315" cy="251.5" r="5" fill="#F5F6F7"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.7 KiB |
@@ -0,0 +1,55 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="360" height="432" viewBox="0 0 360 432" font-family="Inter, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" role="img" aria-label="Load Diffusion Model Index (NH) node">
|
||||
<rect x="0.5" y="0.5" width="359" height="431" rx="10" fill="#26282c" stroke="#45484f"/>
|
||||
<path d="M0.5 10.5 a10 10 0 0 1 10 -10 h339 a10 10 0 0 1 10 10 v36 h-359 z" fill="#34373d"/>
|
||||
<circle cx="17" cy="17" r="4.5" fill="#8FD18F"/>
|
||||
<text x="28" y="22" font-size="14" font-weight="600" fill="#EEF0F3">Load Diffusion Model Index (NH)</text>
|
||||
<text x="28" y="38" font-size="10.5" fill="#9AA0A8" font-family="'JetBrains Mono', Consolas, 'Courier New', monospace">NH_DiffusionModelIndex · Loaders</text>
|
||||
<text x="336" y="71.0" font-size="12" fill="#D7DADF" text-anchor="end">model</text>
|
||||
<circle cx="348" cy="67.0" r="5" fill="#B39DDB" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="336" y="93.0" font-size="12" fill="#D7DADF" text-anchor="end">filename</text>
|
||||
<circle cx="348" cy="89.0" r="5" fill="#C5E1A5" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="336" y="115.0" font-size="12" fill="#D7DADF" text-anchor="end">index</text>
|
||||
<circle cx="348" cy="111.0" r="5" fill="#9CC2E5" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<rect x="12" y="130" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="20" y="146" font-size="10" fill="#6f747c">◀</text>
|
||||
<text x="332" y="146" font-size="10" fill="#6f747c">▶</text>
|
||||
<text x="34" y="146" font-size="11.5" fill="#9AA0A8">select_mode</text>
|
||||
<text x="326" y="146" font-size="11.5" fill="#E6E8EB" text-anchor="end">index</text>
|
||||
<rect x="12" y="159" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="20" y="175" font-size="10" fill="#6f747c">◀</text>
|
||||
<text x="332" y="175" font-size="10" fill="#6f747c">▶</text>
|
||||
<text x="34" y="175" font-size="11.5" fill="#9AA0A8">index</text>
|
||||
<text x="326" y="175" font-size="11.5" fill="#E6E8EB" text-anchor="end">1</text>
|
||||
<rect x="12" y="188" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="20" y="204" font-size="10" fill="#6f747c">◀</text>
|
||||
<text x="332" y="204" font-size="10" fill="#6f747c">▶</text>
|
||||
<text x="34" y="204" font-size="11.5" fill="#9AA0A8">model_count</text>
|
||||
<text x="326" y="204" font-size="11.5" fill="#E6E8EB" text-anchor="end">5</text>
|
||||
<rect x="12" y="217" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="20" y="233" font-size="10" fill="#6f747c">◀</text>
|
||||
<text x="332" y="233" font-size="10" fill="#6f747c">▶</text>
|
||||
<text x="34" y="233" font-size="11.5" fill="#9AA0A8">weight_dtype</text>
|
||||
<text x="326" y="233" font-size="11.5" fill="#E6E8EB" text-anchor="end">default</text>
|
||||
<rect x="12" y="246" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="20" y="262" font-size="10" fill="#6f747c">◀</text>
|
||||
<text x="332" y="262" font-size="10" fill="#6f747c">▶</text>
|
||||
<text x="34" y="262" font-size="11.5" fill="#9AA0A8">diffusion_model_1 <tspan fill="#6f747c" font-style="italic">opt</tspan></text>
|
||||
<text x="326" y="262" font-size="11.5" fill="#E6E8EB" text-anchor="end">—</text>
|
||||
<rect x="12" y="275" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="22" y="291" font-size="11.5" fill="#9AA0A8">enable_1 <tspan fill="#6f747c" font-style="italic">opt</tspan></text>
|
||||
<rect x="308" y="281" width="30" height="13" rx="6.5" fill="#4a4d53"/>
|
||||
<circle cx="315" cy="287.5" r="5" fill="#F5F6F7"/>
|
||||
<rect x="12" y="304" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="20" y="320" font-size="10" fill="#6f747c">◀</text>
|
||||
<text x="332" y="320" font-size="10" fill="#6f747c">▶</text>
|
||||
<text x="34" y="320" font-size="11.5" fill="#9AA0A8">diffusion_model_2 <tspan fill="#6f747c" font-style="italic">opt</tspan></text>
|
||||
<text x="326" y="320" font-size="11.5" fill="#E6E8EB" text-anchor="end">—</text>
|
||||
<rect x="12" y="333" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="22" y="349" font-size="11.5" fill="#9AA0A8">enable_2 <tspan fill="#6f747c" font-style="italic">opt</tspan></text>
|
||||
<rect x="308" y="339" width="30" height="13" rx="6.5" fill="#4a4d53"/>
|
||||
<circle cx="315" cy="345.5" r="5" fill="#F5F6F7"/>
|
||||
<rect x="12" y="362" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="180.0" y="378" font-size="11" fill="#9AA0A8" text-anchor="middle" font-style="italic">diffusion_model_3 … diffusion_model_64</text>
|
||||
<rect x="12" y="391" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="180.0" y="407" font-size="11" fill="#9AA0A8" text-anchor="middle" font-style="italic">enable_3 … enable_64</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.5 KiB |
@@ -0,0 +1,50 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="360" height="367" viewBox="0 0 360 367" font-family="Inter, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" role="img" aria-label="Garment Segment (NH) node">
|
||||
<rect x="0.5" y="0.5" width="359" height="366" rx="10" fill="#26282c" stroke="#45484f"/>
|
||||
<path d="M0.5 10.5 a10 10 0 0 1 10 -10 h339 a10 10 0 0 1 10 10 v36 h-359 z" fill="#34373d"/>
|
||||
<circle cx="17" cy="17" r="4.5" fill="#8FD18F"/>
|
||||
<text x="28" y="22" font-size="14" font-weight="600" fill="#EEF0F3">Garment Segment (NH)</text>
|
||||
<text x="28" y="38" font-size="10.5" fill="#9AA0A8" font-family="'JetBrains Mono', Consolas, 'Courier New', monospace">NH_GarmentSegment · Vision</text>
|
||||
<circle cx="12" cy="67.0" r="5" fill="#64B5F6" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="24" y="71.0" font-size="12" fill="#D7DADF">image</text>
|
||||
<text x="336" y="71.0" font-size="12" fill="#D7DADF" text-anchor="end">mask</text>
|
||||
<circle cx="348" cy="67.0" r="5" fill="#81C784" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="336" y="93.0" font-size="12" fill="#D7DADF" text-anchor="end">cutout_rgba</text>
|
||||
<circle cx="348" cy="89.0" r="5" fill="#64B5F6" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="336" y="115.0" font-size="12" fill="#D7DADF" text-anchor="end">mask_image</text>
|
||||
<circle cx="348" cy="111.0" r="5" fill="#64B5F6" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="336" y="137.0" font-size="12" fill="#D7DADF" text-anchor="end">segs</text>
|
||||
<circle cx="348" cy="133.0" r="5" fill="#E0B272" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<rect x="12" y="152" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="20" y="168" font-size="10" fill="#6f747c">◀</text>
|
||||
<text x="332" y="168" font-size="10" fill="#6f747c">▶</text>
|
||||
<text x="34" y="168" font-size="11.5" fill="#9AA0A8">part</text>
|
||||
<text x="326" y="168" font-size="11.5" fill="#E6E8EB" text-anchor="end">upper_garment</text>
|
||||
<rect x="12" y="181" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="22" y="197" font-size="11.5" fill="#9AA0A8">attach_parts</text>
|
||||
<rect x="308" y="187" width="30" height="13" rx="6.5" fill="#4C8BF5"/>
|
||||
<circle cx="331" cy="193.5" r="5" fill="#F5F6F7"/>
|
||||
<rect x="12" y="210" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="20" y="226" font-size="10" fill="#6f747c">◀</text>
|
||||
<text x="332" y="226" font-size="10" fill="#6f747c">▶</text>
|
||||
<text x="34" y="226" font-size="11.5" fill="#9AA0A8">mask_expand</text>
|
||||
<text x="326" y="226" font-size="11.5" fill="#E6E8EB" text-anchor="end">0</text>
|
||||
<rect x="12" y="239" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="20" y="255" font-size="10" fill="#6f747c">◀</text>
|
||||
<text x="332" y="255" font-size="10" fill="#6f747c">▶</text>
|
||||
<text x="34" y="255" font-size="11.5" fill="#9AA0A8">mask_blur</text>
|
||||
<text x="326" y="255" font-size="11.5" fill="#E6E8EB" text-anchor="end">0</text>
|
||||
<rect x="12" y="268" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="22" y="284" font-size="11.5" fill="#9AA0A8">fill_holes</text>
|
||||
<rect x="308" y="274" width="30" height="13" rx="6.5" fill="#4a4d53"/>
|
||||
<circle cx="315" cy="280.5" r="5" fill="#F5F6F7"/>
|
||||
<rect x="12" y="297" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="20" y="313" font-size="10" fill="#6f747c">◀</text>
|
||||
<text x="332" y="313" font-size="10" fill="#6f747c">▶</text>
|
||||
<text x="34" y="313" font-size="11.5" fill="#9AA0A8">device</text>
|
||||
<text x="326" y="313" font-size="11.5" fill="#E6E8EB" text-anchor="end">cuda</text>
|
||||
<rect x="12" y="326" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="20" y="342" font-size="10" fill="#6f747c">◀</text>
|
||||
<text x="332" y="342" font-size="10" fill="#6f747c">▶</text>
|
||||
<text x="34" y="342" font-size="11.5" fill="#9AA0A8">model <tspan fill="#6f747c" font-style="italic">opt</tspan></text>
|
||||
<text x="326" y="342" font-size="11.5" fill="#E6E8EB" text-anchor="end">fashn</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.9 KiB |
@@ -0,0 +1,15 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="360" height="127" viewBox="0 0 360 127" font-family="Inter, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" role="img" aria-label="Gate Switch (NH) node">
|
||||
<rect x="0.5" y="0.5" width="359" height="126" rx="10" fill="#26282c" stroke="#45484f"/>
|
||||
<path d="M0.5 10.5 a10 10 0 0 1 10 -10 h339 a10 10 0 0 1 10 10 v36 h-359 z" fill="#34373d"/>
|
||||
<circle cx="17" cy="17" r="4.5" fill="#8FD18F"/>
|
||||
<text x="28" y="22" font-size="14" font-weight="600" fill="#EEF0F3">Gate Switch (NH)</text>
|
||||
<text x="28" y="38" font-size="10.5" fill="#9AA0A8" font-family="'JetBrains Mono', Consolas, 'Courier New', monospace">NH_GateSwitch · Logic</text>
|
||||
<circle cx="12" cy="67.0" r="5" fill="#CFD8DC" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="24" y="71.0" font-size="12" fill="#D7DADF">input</text>
|
||||
<text x="336" y="71.0" font-size="12" fill="#D7DADF" text-anchor="end">output</text>
|
||||
<circle cx="348" cy="67.0" r="5" fill="#CFD8DC" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<rect x="12" y="86" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="22" y="102" font-size="11.5" fill="#9AA0A8">enabled</text>
|
||||
<rect x="308" y="92" width="30" height="13" rx="6.5" fill="#4C8BF5"/>
|
||||
<circle cx="331" cy="98.5" r="5" fill="#F5F6F7"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -0,0 +1,19 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="360" height="149" viewBox="0 0 360 149" font-family="Inter, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" role="img" aria-label="If/Else (NH) node">
|
||||
<rect x="0.5" y="0.5" width="359" height="148" rx="10" fill="#26282c" stroke="#45484f"/>
|
||||
<path d="M0.5 10.5 a10 10 0 0 1 10 -10 h339 a10 10 0 0 1 10 10 v36 h-359 z" fill="#34373d"/>
|
||||
<circle cx="17" cy="17" r="4.5" fill="#8FD18F"/>
|
||||
<text x="28" y="22" font-size="14" font-weight="600" fill="#EEF0F3">If/Else (NH)</text>
|
||||
<text x="28" y="38" font-size="10.5" fill="#9AA0A8" font-family="'JetBrains Mono', Consolas, 'Courier New', monospace">NH_IfElse · Logic</text>
|
||||
<circle cx="12" cy="67.0" r="5" fill="#CFD8DC" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="24" y="71.0" font-size="12" fill="#D7DADF">if_true</text>
|
||||
<text x="336" y="71.0" font-size="12" fill="#D7DADF" text-anchor="end">result</text>
|
||||
<circle cx="348" cy="67.0" r="5" fill="#CFD8DC" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<circle cx="12" cy="89.0" r="5" fill="#CFD8DC" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="24" y="93.0" font-size="12" fill="#D7DADF">if_false</text>
|
||||
<text x="336" y="93.0" font-size="12" fill="#D7DADF" text-anchor="end">condition_out</text>
|
||||
<circle cx="348" cy="89.0" r="5" fill="#F48FB1" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<rect x="12" y="108" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="22" y="124" font-size="11.5" fill="#9AA0A8">condition</text>
|
||||
<rect x="308" y="114" width="30" height="13" rx="6.5" fill="#4C8BF5"/>
|
||||
<circle cx="331" cy="120.5" r="5" fill="#F5F6F7"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
@@ -0,0 +1,33 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="360" height="236" viewBox="0 0 360 236" font-family="Inter, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" role="img" aria-label="Image Compare (NH) node">
|
||||
<rect x="0.5" y="0.5" width="359" height="235" rx="10" fill="#26282c" stroke="#45484f"/>
|
||||
<path d="M0.5 10.5 a10 10 0 0 1 10 -10 h339 a10 10 0 0 1 10 10 v36 h-359 z" fill="#34373d"/>
|
||||
<circle cx="17" cy="17" r="4.5" fill="#8FD18F"/>
|
||||
<text x="28" y="22" font-size="14" font-weight="600" fill="#EEF0F3">Image Compare (NH)</text>
|
||||
<text x="28" y="38" font-size="10.5" fill="#9AA0A8" font-family="'JetBrains Mono', Consolas, 'Courier New', monospace">NH_ImageCompare · Image</text>
|
||||
<circle cx="12" cy="67.0" r="5" fill="#64B5F6" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="24" y="71.0" font-size="12" fill="#D7DADF">image_a</text>
|
||||
<text x="336" y="71.0" font-size="12" fill="#D7DADF" text-anchor="end">image</text>
|
||||
<circle cx="348" cy="67.0" r="5" fill="#64B5F6" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<circle cx="12" cy="89.0" r="5" fill="#64B5F6" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="24" y="93.0" font-size="12" fill="#D7DADF">image_b</text>
|
||||
<rect x="12" y="108" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="20" y="124" font-size="10" fill="#6f747c">◀</text>
|
||||
<text x="332" y="124" font-size="10" fill="#6f747c">▶</text>
|
||||
<text x="34" y="124" font-size="11.5" fill="#9AA0A8">compare_mode</text>
|
||||
<text x="326" y="124" font-size="11.5" fill="#E6E8EB" text-anchor="end">side_by_side</text>
|
||||
<rect x="12" y="137" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="20" y="153" font-size="10" fill="#6f747c">◀</text>
|
||||
<text x="332" y="153" font-size="10" fill="#6f747c">▶</text>
|
||||
<text x="34" y="153" font-size="11.5" fill="#9AA0A8">split_position</text>
|
||||
<text x="326" y="153" font-size="11.5" fill="#E6E8EB" text-anchor="end">0.50</text>
|
||||
<rect x="12" y="166" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="20" y="182" font-size="10" fill="#6f747c">◀</text>
|
||||
<text x="332" y="182" font-size="10" fill="#6f747c">▶</text>
|
||||
<text x="34" y="182" font-size="11.5" fill="#9AA0A8">overlay_opacity</text>
|
||||
<text x="326" y="182" font-size="11.5" fill="#E6E8EB" text-anchor="end">0.50</text>
|
||||
<rect x="12" y="195" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="20" y="211" font-size="10" fill="#6f747c">◀</text>
|
||||
<text x="332" y="211" font-size="10" fill="#6f747c">▶</text>
|
||||
<text x="34" y="211" font-size="11.5" fill="#9AA0A8">resize_method</text>
|
||||
<text x="326" y="211" font-size="11.5" fill="#E6E8EB" text-anchor="end">lanczos</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.6 KiB |
@@ -0,0 +1,60 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="360" height="425" viewBox="0 0 360 425" font-family="Inter, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" role="img" aria-label="Image Grid Composite (NH) node">
|
||||
<rect x="0.5" y="0.5" width="359" height="424" rx="10" fill="#26282c" stroke="#45484f"/>
|
||||
<path d="M0.5 10.5 a10 10 0 0 1 10 -10 h339 a10 10 0 0 1 10 10 v36 h-359 z" fill="#34373d"/>
|
||||
<circle cx="17" cy="17" r="4.5" fill="#8FD18F"/>
|
||||
<text x="28" y="22" font-size="14" font-weight="600" fill="#EEF0F3">Image Grid Composite (NH)</text>
|
||||
<text x="28" y="38" font-size="10.5" fill="#9AA0A8" font-family="'JetBrains Mono', Consolas, 'Courier New', monospace">NH_ImageGridComposite · Image</text>
|
||||
<circle cx="12" cy="67.0" r="5" fill="#64B5F6" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="24" y="71.0" font-size="12" fill="#D7DADF">images</text>
|
||||
<text x="336" y="71.0" font-size="12" fill="#D7DADF" text-anchor="end">image</text>
|
||||
<circle cx="348" cy="67.0" r="5" fill="#64B5F6" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="336" y="93.0" font-size="12" fill="#D7DADF" text-anchor="end">slot_count</text>
|
||||
<circle cx="348" cy="89.0" r="5" fill="#9CC2E5" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="336" y="115.0" font-size="12" fill="#D7DADF" text-anchor="end">used_count</text>
|
||||
<circle cx="348" cy="111.0" r="5" fill="#9CC2E5" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="336" y="137.0" font-size="12" fill="#D7DADF" text-anchor="end">grid_info</text>
|
||||
<circle cx="348" cy="133.0" r="5" fill="#C5E1A5" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<rect x="12" y="152" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="20" y="168" font-size="10" fill="#6f747c">◀</text>
|
||||
<text x="332" y="168" font-size="10" fill="#6f747c">▶</text>
|
||||
<text x="34" y="168" font-size="11.5" fill="#9AA0A8">columns</text>
|
||||
<text x="326" y="168" font-size="11.5" fill="#E6E8EB" text-anchor="end">2</text>
|
||||
<rect x="12" y="181" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="20" y="197" font-size="10" fill="#6f747c">◀</text>
|
||||
<text x="332" y="197" font-size="10" fill="#6f747c">▶</text>
|
||||
<text x="34" y="197" font-size="11.5" fill="#9AA0A8">rows</text>
|
||||
<text x="326" y="197" font-size="11.5" fill="#E6E8EB" text-anchor="end">2</text>
|
||||
<rect x="12" y="210" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="20" y="226" font-size="10" fill="#6f747c">◀</text>
|
||||
<text x="332" y="226" font-size="10" fill="#6f747c">▶</text>
|
||||
<text x="34" y="226" font-size="11.5" fill="#9AA0A8">cell_size_mode</text>
|
||||
<text x="326" y="226" font-size="11.5" fill="#E6E8EB" text-anchor="end">manual</text>
|
||||
<rect x="12" y="239" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="20" y="255" font-size="10" fill="#6f747c">◀</text>
|
||||
<text x="332" y="255" font-size="10" fill="#6f747c">▶</text>
|
||||
<text x="34" y="255" font-size="11.5" fill="#9AA0A8">cell_width</text>
|
||||
<text x="326" y="255" font-size="11.5" fill="#E6E8EB" text-anchor="end">512</text>
|
||||
<rect x="12" y="268" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="20" y="284" font-size="10" fill="#6f747c">◀</text>
|
||||
<text x="332" y="284" font-size="10" fill="#6f747c">▶</text>
|
||||
<text x="34" y="284" font-size="11.5" fill="#9AA0A8">cell_height</text>
|
||||
<text x="326" y="284" font-size="11.5" fill="#E6E8EB" text-anchor="end">512</text>
|
||||
<rect x="12" y="297" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="20" y="313" font-size="10" fill="#6f747c">◀</text>
|
||||
<text x="332" y="313" font-size="10" fill="#6f747c">▶</text>
|
||||
<text x="34" y="313" font-size="11.5" fill="#9AA0A8">resize_mode</text>
|
||||
<text x="326" y="313" font-size="11.5" fill="#E6E8EB" text-anchor="end">stretch</text>
|
||||
<rect x="12" y="326" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="20" y="342" font-size="10" fill="#6f747c">◀</text>
|
||||
<text x="332" y="342" font-size="10" fill="#6f747c">▶</text>
|
||||
<text x="34" y="342" font-size="11.5" fill="#9AA0A8">gap_x</text>
|
||||
<text x="326" y="342" font-size="11.5" fill="#E6E8EB" text-anchor="end">0</text>
|
||||
<rect x="12" y="355" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="20" y="371" font-size="10" fill="#6f747c">◀</text>
|
||||
<text x="332" y="371" font-size="10" fill="#6f747c">▶</text>
|
||||
<text x="34" y="371" font-size="11.5" fill="#9AA0A8">gap_y</text>
|
||||
<text x="326" y="371" font-size="11.5" fill="#E6E8EB" text-anchor="end">0</text>
|
||||
<rect x="12" y="384" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="22" y="400" font-size="11.5" fill="#9AA0A8">background_color_hex</text>
|
||||
<text x="338" y="400" font-size="11.5" fill="#E6E8EB" text-anchor="end">#000000</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.6 KiB |
@@ -0,0 +1,70 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="360" height="526" viewBox="0 0 360 526" font-family="Inter, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" role="img" aria-label="Image Label (NH) node">
|
||||
<rect x="0.5" y="0.5" width="359" height="525" rx="10" fill="#26282c" stroke="#45484f"/>
|
||||
<path d="M0.5 10.5 a10 10 0 0 1 10 -10 h339 a10 10 0 0 1 10 10 v36 h-359 z" fill="#34373d"/>
|
||||
<circle cx="17" cy="17" r="4.5" fill="#8FD18F"/>
|
||||
<text x="28" y="22" font-size="14" font-weight="600" fill="#EEF0F3">Image Label (NH)</text>
|
||||
<text x="28" y="38" font-size="10.5" fill="#9AA0A8" font-family="'JetBrains Mono', Consolas, 'Courier New', monospace">NH_ImageLabel · Image</text>
|
||||
<circle cx="12" cy="67.0" r="5" fill="#64B5F6" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="24" y="71.0" font-size="12" fill="#D7DADF">image</text>
|
||||
<text x="336" y="71.0" font-size="12" fill="#D7DADF" text-anchor="end">image</text>
|
||||
<circle cx="348" cy="67.0" r="5" fill="#64B5F6" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<rect x="12" y="86" width="336" height="46" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="22" y="102" font-size="11.5" fill="#9AA0A8">label_text</text>
|
||||
<text x="22" y="122" font-size="11.5" fill="#E6E8EB" font-family="'JetBrains Mono', Consolas, 'Courier New', monospace">NH Label</text>
|
||||
<rect x="12" y="137" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="20" y="153" font-size="10" fill="#6f747c">◀</text>
|
||||
<text x="332" y="153" font-size="10" fill="#6f747c">▶</text>
|
||||
<text x="34" y="153" font-size="11.5" fill="#9AA0A8">position</text>
|
||||
<text x="326" y="153" font-size="11.5" fill="#E6E8EB" text-anchor="end">header</text>
|
||||
<rect x="12" y="166" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="20" y="182" font-size="10" fill="#6f747c">◀</text>
|
||||
<text x="332" y="182" font-size="10" fill="#6f747c">▶</text>
|
||||
<text x="34" y="182" font-size="11.5" fill="#9AA0A8">frame_height</text>
|
||||
<text x="326" y="182" font-size="11.5" fill="#E6E8EB" text-anchor="end">80</text>
|
||||
<rect x="12" y="195" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="22" y="211" font-size="11.5" fill="#9AA0A8">fill_enabled</text>
|
||||
<rect x="308" y="201" width="30" height="13" rx="6.5" fill="#4C8BF5"/>
|
||||
<circle cx="331" cy="207.5" r="5" fill="#F5F6F7"/>
|
||||
<rect x="12" y="224" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="22" y="240" font-size="11.5" fill="#9AA0A8">frame_color_hex</text>
|
||||
<text x="338" y="240" font-size="11.5" fill="#E6E8EB" text-anchor="end">#111111</text>
|
||||
<rect x="12" y="253" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="22" y="269" font-size="11.5" fill="#9AA0A8">text_color_hex</text>
|
||||
<text x="338" y="269" font-size="11.5" fill="#E6E8EB" text-anchor="end">#FFFFFF</text>
|
||||
<rect x="12" y="282" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="20" y="298" font-size="10" fill="#6f747c">◀</text>
|
||||
<text x="332" y="298" font-size="10" fill="#6f747c">▶</text>
|
||||
<text x="34" y="298" font-size="11.5" fill="#9AA0A8">font_name</text>
|
||||
<text x="326" y="298" font-size="11.5" fill="#E6E8EB" text-anchor="end">default</text>
|
||||
<rect x="12" y="311" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="20" y="327" font-size="10" fill="#6f747c">◀</text>
|
||||
<text x="332" y="327" font-size="10" fill="#6f747c">▶</text>
|
||||
<text x="34" y="327" font-size="11.5" fill="#9AA0A8">font_size</text>
|
||||
<text x="326" y="327" font-size="11.5" fill="#E6E8EB" text-anchor="end">32</text>
|
||||
<rect x="12" y="340" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="22" y="356" font-size="11.5" fill="#9AA0A8">fit_text</text>
|
||||
<rect x="308" y="346" width="30" height="13" rx="6.5" fill="#4C8BF5"/>
|
||||
<circle cx="331" cy="352.5" r="5" fill="#F5F6F7"/>
|
||||
<rect x="12" y="369" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="22" y="385" font-size="11.5" fill="#9AA0A8">overlay_on_image</text>
|
||||
<rect x="308" y="375" width="30" height="13" rx="6.5" fill="#4a4d53"/>
|
||||
<circle cx="315" cy="381.5" r="5" fill="#F5F6F7"/>
|
||||
<rect x="12" y="398" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="20" y="414" font-size="10" fill="#6f747c">◀</text>
|
||||
<text x="332" y="414" font-size="10" fill="#6f747c">▶</text>
|
||||
<text x="34" y="414" font-size="11.5" fill="#9AA0A8">align</text>
|
||||
<text x="326" y="414" font-size="11.5" fill="#E6E8EB" text-anchor="end">left</text>
|
||||
<rect x="12" y="427" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="20" y="443" font-size="10" fill="#6f747c">◀</text>
|
||||
<text x="332" y="443" font-size="10" fill="#6f747c">▶</text>
|
||||
<text x="34" y="443" font-size="11.5" fill="#9AA0A8">padding_x</text>
|
||||
<text x="326" y="443" font-size="11.5" fill="#E6E8EB" text-anchor="end">20</text>
|
||||
<rect x="12" y="456" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="20" y="472" font-size="10" fill="#6f747c">◀</text>
|
||||
<text x="332" y="472" font-size="10" fill="#6f747c">▶</text>
|
||||
<text x="34" y="472" font-size="11.5" fill="#9AA0A8">outline_width</text>
|
||||
<text x="326" y="472" font-size="11.5" fill="#E6E8EB" text-anchor="end">0</text>
|
||||
<rect x="12" y="485" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="22" y="501" font-size="11.5" fill="#9AA0A8">outline_color_hex</text>
|
||||
<text x="338" y="501" font-size="11.5" fill="#E6E8EB" text-anchor="end">#000000</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.3 KiB |
@@ -0,0 +1,48 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="360" height="345" viewBox="0 0 360 345" font-family="Inter, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" role="img" aria-label="Image Resize Unit (NH) node">
|
||||
<rect x="0.5" y="0.5" width="359" height="344" rx="10" fill="#26282c" stroke="#45484f"/>
|
||||
<path d="M0.5 10.5 a10 10 0 0 1 10 -10 h339 a10 10 0 0 1 10 10 v36 h-359 z" fill="#34373d"/>
|
||||
<circle cx="17" cy="17" r="4.5" fill="#8FD18F"/>
|
||||
<text x="28" y="22" font-size="14" font-weight="600" fill="#EEF0F3">Image Resize Unit (NH)</text>
|
||||
<text x="28" y="38" font-size="10.5" fill="#9AA0A8" font-family="'JetBrains Mono', Consolas, 'Courier New', monospace">NH_ImageResizeByUnit · Image</text>
|
||||
<circle cx="12" cy="67.0" r="5" fill="#64B5F6" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="24" y="71.0" font-size="12" fill="#D7DADF">image</text>
|
||||
<text x="336" y="71.0" font-size="12" fill="#D7DADF" text-anchor="end">image</text>
|
||||
<circle cx="348" cy="67.0" r="5" fill="#64B5F6" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="336" y="93.0" font-size="12" fill="#D7DADF" text-anchor="end">width_px</text>
|
||||
<circle cx="348" cy="89.0" r="5" fill="#9CC2E5" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="336" y="115.0" font-size="12" fill="#D7DADF" text-anchor="end">height_px</text>
|
||||
<circle cx="348" cy="111.0" r="5" fill="#9CC2E5" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<rect x="12" y="130" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="20" y="146" font-size="10" fill="#6f747c">◀</text>
|
||||
<text x="332" y="146" font-size="10" fill="#6f747c">▶</text>
|
||||
<text x="34" y="146" font-size="11.5" fill="#9AA0A8">unit</text>
|
||||
<text x="326" y="146" font-size="11.5" fill="#E6E8EB" text-anchor="end">pixel</text>
|
||||
<rect x="12" y="159" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="20" y="175" font-size="10" fill="#6f747c">◀</text>
|
||||
<text x="332" y="175" font-size="10" fill="#6f747c">▶</text>
|
||||
<text x="34" y="175" font-size="11.5" fill="#9AA0A8">width_value</text>
|
||||
<text x="326" y="175" font-size="11.5" fill="#E6E8EB" text-anchor="end">1024.0</text>
|
||||
<rect x="12" y="188" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="20" y="204" font-size="10" fill="#6f747c">◀</text>
|
||||
<text x="332" y="204" font-size="10" fill="#6f747c">▶</text>
|
||||
<text x="34" y="204" font-size="11.5" fill="#9AA0A8">height_value</text>
|
||||
<text x="326" y="204" font-size="11.5" fill="#E6E8EB" text-anchor="end">1024.0</text>
|
||||
<rect x="12" y="217" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="20" y="233" font-size="10" fill="#6f747c">◀</text>
|
||||
<text x="332" y="233" font-size="10" fill="#6f747c">▶</text>
|
||||
<text x="34" y="233" font-size="11.5" fill="#9AA0A8">dpi</text>
|
||||
<text x="326" y="233" font-size="11.5" fill="#E6E8EB" text-anchor="end">300</text>
|
||||
<rect x="12" y="246" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="20" y="262" font-size="10" fill="#6f747c">◀</text>
|
||||
<text x="332" y="262" font-size="10" fill="#6f747c">▶</text>
|
||||
<text x="34" y="262" font-size="11.5" fill="#9AA0A8">resize_mode</text>
|
||||
<text x="326" y="262" font-size="11.5" fill="#E6E8EB" text-anchor="end">stretch</text>
|
||||
<rect x="12" y="275" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="20" y="291" font-size="10" fill="#6f747c">◀</text>
|
||||
<text x="332" y="291" font-size="10" fill="#6f747c">▶</text>
|
||||
<text x="34" y="291" font-size="11.5" fill="#9AA0A8">resize_method</text>
|
||||
<text x="326" y="291" font-size="11.5" fill="#E6E8EB" text-anchor="end">nearest</text>
|
||||
<rect x="12" y="304" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="22" y="320" font-size="11.5" fill="#9AA0A8">pad_color_hex</text>
|
||||
<text x="338" y="320" font-size="11.5" fill="#E6E8EB" text-anchor="end">#000000</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.7 KiB |
@@ -0,0 +1,41 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="360" height="324" viewBox="0 0 360 324" font-family="Inter, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" role="img" aria-label="Image Tile (NH) node">
|
||||
<rect x="0.5" y="0.5" width="359" height="323" rx="10" fill="#26282c" stroke="#45484f"/>
|
||||
<path d="M0.5 10.5 a10 10 0 0 1 10 -10 h339 a10 10 0 0 1 10 10 v36 h-359 z" fill="#34373d"/>
|
||||
<circle cx="17" cy="17" r="4.5" fill="#8FD18F"/>
|
||||
<text x="28" y="22" font-size="14" font-weight="600" fill="#EEF0F3">Image Tile (NH)</text>
|
||||
<text x="28" y="38" font-size="10.5" fill="#9AA0A8" font-family="'JetBrains Mono', Consolas, 'Courier New', monospace">NH_ImageTile · Image</text>
|
||||
<circle cx="12" cy="67.0" r="5" fill="#64B5F6" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="24" y="71.0" font-size="12" fill="#D7DADF">image</text>
|
||||
<text x="336" y="71.0" font-size="12" fill="#D7DADF" text-anchor="end">tiles</text>
|
||||
<circle cx="348" cy="67.0" r="5" fill="#64B5F6" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="336" y="93.0" font-size="12" fill="#D7DADF" text-anchor="end">tile_data</text>
|
||||
<circle cx="348" cy="89.0" r="5" fill="#E0B272" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="336" y="115.0" font-size="12" fill="#D7DADF" text-anchor="end">tile_count</text>
|
||||
<circle cx="348" cy="111.0" r="5" fill="#9CC2E5" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="336" y="137.0" font-size="12" fill="#D7DADF" text-anchor="end">overlap_x</text>
|
||||
<circle cx="348" cy="133.0" r="5" fill="#9CC2E5" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="336" y="159.0" font-size="12" fill="#D7DADF" text-anchor="end">overlap_y</text>
|
||||
<circle cx="348" cy="155.0" r="5" fill="#9CC2E5" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="336" y="181.0" font-size="12" fill="#D7DADF" text-anchor="end">tile_info</text>
|
||||
<circle cx="348" cy="177.0" r="5" fill="#C5E1A5" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<rect x="12" y="196" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="20" y="212" font-size="10" fill="#6f747c">◀</text>
|
||||
<text x="332" y="212" font-size="10" fill="#6f747c">▶</text>
|
||||
<text x="34" y="212" font-size="11.5" fill="#9AA0A8">tile_mode</text>
|
||||
<text x="326" y="212" font-size="11.5" fill="#E6E8EB" text-anchor="end">original_ratio</text>
|
||||
<rect x="12" y="225" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="20" y="241" font-size="10" fill="#6f747c">◀</text>
|
||||
<text x="332" y="241" font-size="10" fill="#6f747c">▶</text>
|
||||
<text x="34" y="241" font-size="11.5" fill="#9AA0A8">tile_size</text>
|
||||
<text x="326" y="241" font-size="11.5" fill="#E6E8EB" text-anchor="end">1024</text>
|
||||
<rect x="12" y="254" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="20" y="270" font-size="10" fill="#6f747c">◀</text>
|
||||
<text x="332" y="270" font-size="10" fill="#6f747c">▶</text>
|
||||
<text x="34" y="270" font-size="11.5" fill="#9AA0A8">tile_width</text>
|
||||
<text x="326" y="270" font-size="11.5" fill="#E6E8EB" text-anchor="end">1024</text>
|
||||
<rect x="12" y="283" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="20" y="299" font-size="10" fill="#6f747c">◀</text>
|
||||
<text x="332" y="299" font-size="10" fill="#6f747c">▶</text>
|
||||
<text x="34" y="299" font-size="11.5" fill="#9AA0A8">tile_height</text>
|
||||
<text x="326" y="299" font-size="11.5" fill="#E6E8EB" text-anchor="end">1024</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.3 KiB |
@@ -0,0 +1,15 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="360" height="112" viewBox="0 0 360 112" font-family="Inter, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" role="img" aria-label="Image Untile (NH) node">
|
||||
<rect x="0.5" y="0.5" width="359" height="111" rx="10" fill="#26282c" stroke="#45484f"/>
|
||||
<path d="M0.5 10.5 a10 10 0 0 1 10 -10 h339 a10 10 0 0 1 10 10 v36 h-359 z" fill="#34373d"/>
|
||||
<circle cx="17" cy="17" r="4.5" fill="#8FD18F"/>
|
||||
<text x="28" y="22" font-size="14" font-weight="600" fill="#EEF0F3">Image Untile (NH)</text>
|
||||
<text x="28" y="38" font-size="10.5" fill="#9AA0A8" font-family="'JetBrains Mono', Consolas, 'Courier New', monospace">NH_ImageUntile · Image</text>
|
||||
<circle cx="12" cy="67.0" r="5" fill="#64B5F6" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="24" y="71.0" font-size="12" fill="#D7DADF">tiles</text>
|
||||
<text x="336" y="71.0" font-size="12" fill="#D7DADF" text-anchor="end">image</text>
|
||||
<circle cx="348" cy="67.0" r="5" fill="#64B5F6" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<circle cx="12" cy="89.0" r="5" fill="#E0B272" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="24" y="93.0" font-size="12" fill="#D7DADF">tile_data</text>
|
||||
<text x="336" y="93.0" font-size="12" fill="#D7DADF" text-anchor="end">untile_info</text>
|
||||
<circle cx="348" cy="89.0" r="5" fill="#C5E1A5" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -0,0 +1,64 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="360" height="432" viewBox="0 0 360 432" font-family="Inter, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" role="img" aria-label="NH Large Image Compare node">
|
||||
<rect x="0.5" y="0.5" width="359" height="431" rx="10" fill="#26282c" stroke="#45484f"/>
|
||||
<path d="M0.5 10.5 a10 10 0 0 1 10 -10 h339 a10 10 0 0 1 10 10 v36 h-359 z" fill="#34373d"/>
|
||||
<circle cx="17" cy="17" r="4.5" fill="#F6AD55"/>
|
||||
<text x="28" y="22" font-size="14" font-weight="600" fill="#EEF0F3">NH Large Image Compare</text>
|
||||
<text x="28" y="38" font-size="10.5" fill="#9AA0A8" font-family="'JetBrains Mono', Consolas, 'Courier New', monospace">NH_LargeImageCompare · Image</text>
|
||||
<circle cx="12" cy="67.0" r="5" fill="#64B5F6" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="24" y="71.0" font-size="12" fill="#D7DADF">image_a</text>
|
||||
<text x="336" y="71.0" font-size="12" fill="#D7DADF" text-anchor="end">image_a</text>
|
||||
<circle cx="348" cy="67.0" r="5" fill="#64B5F6" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<circle cx="12" cy="89.0" r="5" fill="#64B5F6" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="24" y="93.0" font-size="12" fill="#D7DADF">image_b</text>
|
||||
<text x="336" y="93.0" font-size="12" fill="#D7DADF" text-anchor="end">image_b</text>
|
||||
<circle cx="348" cy="89.0" r="5" fill="#64B5F6" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="336" y="115.0" font-size="12" fill="#D7DADF" text-anchor="end">manifest</text>
|
||||
<circle cx="348" cy="111.0" r="5" fill="#C5E1A5" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<rect x="12" y="130" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="20" y="146" font-size="10" fill="#6f747c">◀</text>
|
||||
<text x="332" y="146" font-size="10" fill="#6f747c">▶</text>
|
||||
<text x="34" y="146" font-size="11.5" fill="#9AA0A8">compare_mode</text>
|
||||
<text x="326" y="146" font-size="11.5" fill="#E6E8EB" text-anchor="end">slider</text>
|
||||
<rect x="12" y="159" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="22" y="175" font-size="11.5" fill="#9AA0A8">sync_pan_zoom</text>
|
||||
<rect x="308" y="165" width="30" height="13" rx="6.5" fill="#4C8BF5"/>
|
||||
<circle cx="331" cy="171.5" r="5" fill="#F5F6F7"/>
|
||||
<rect x="12" y="188" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="20" y="204" font-size="10" fill="#6f747c">◀</text>
|
||||
<text x="332" y="204" font-size="10" fill="#6f747c">▶</text>
|
||||
<text x="34" y="204" font-size="11.5" fill="#9AA0A8">preview_megapixels</text>
|
||||
<text x="326" y="204" font-size="11.5" fill="#E6E8EB" text-anchor="end">1.00</text>
|
||||
<rect x="12" y="217" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="20" y="233" font-size="10" fill="#6f747c">◀</text>
|
||||
<text x="332" y="233" font-size="10" fill="#6f747c">▶</text>
|
||||
<text x="34" y="233" font-size="11.5" fill="#9AA0A8">preview_format</text>
|
||||
<text x="326" y="233" font-size="11.5" fill="#E6E8EB" text-anchor="end">jpg</text>
|
||||
<rect x="12" y="246" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="20" y="262" font-size="10" fill="#6f747c">◀</text>
|
||||
<text x="332" y="262" font-size="10" fill="#6f747c">▶</text>
|
||||
<text x="34" y="262" font-size="11.5" fill="#9AA0A8">preview_quality</text>
|
||||
<text x="326" y="262" font-size="11.5" fill="#E6E8EB" text-anchor="end">90</text>
|
||||
<rect x="12" y="275" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="22" y="291" font-size="11.5" fill="#9AA0A8">title</text>
|
||||
<text x="338" y="291" font-size="11.5" fill="#E6E8EB" text-anchor="end">NH Large Image Compare</text>
|
||||
<rect x="12" y="304" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="20" y="320" font-size="10" fill="#6f747c">◀</text>
|
||||
<text x="332" y="320" font-size="10" fill="#6f747c">▶</text>
|
||||
<text x="34" y="320" font-size="11.5" fill="#9AA0A8">tile_size <tspan fill="#6f747c" font-style="italic">opt</tspan></text>
|
||||
<text x="326" y="320" font-size="11.5" fill="#E6E8EB" text-anchor="end">512</text>
|
||||
<rect x="12" y="333" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="20" y="349" font-size="10" fill="#6f747c">◀</text>
|
||||
<text x="332" y="349" font-size="10" fill="#6f747c">▶</text>
|
||||
<text x="34" y="349" font-size="11.5" fill="#9AA0A8">tile_format <tspan fill="#6f747c" font-style="italic">opt</tspan></text>
|
||||
<text x="326" y="349" font-size="11.5" fill="#E6E8EB" text-anchor="end">jpg</text>
|
||||
<rect x="12" y="362" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="20" y="378" font-size="10" fill="#6f747c">◀</text>
|
||||
<text x="332" y="378" font-size="10" fill="#6f747c">▶</text>
|
||||
<text x="34" y="378" font-size="11.5" fill="#9AA0A8">tile_quality <tspan fill="#6f747c" font-style="italic">opt</tspan></text>
|
||||
<text x="326" y="378" font-size="11.5" fill="#E6E8EB" text-anchor="end">90</text>
|
||||
<rect x="12" y="391" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="20" y="407" font-size="10" fill="#6f747c">◀</text>
|
||||
<text x="332" y="407" font-size="10" fill="#6f747c">▶</text>
|
||||
<text x="34" y="407" font-size="11.5" fill="#9AA0A8">cache_policy <tspan fill="#6f747c" font-style="italic">opt</tspan></text>
|
||||
<text x="326" y="407" font-size="11.5" fill="#E6E8EB" text-anchor="end">temp</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.1 KiB |
@@ -0,0 +1,56 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="360" height="381" viewBox="0 0 360 381" font-family="Inter, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" role="img" aria-label="NH Large Image Preview node">
|
||||
<rect x="0.5" y="0.5" width="359" height="380" rx="10" fill="#26282c" stroke="#45484f"/>
|
||||
<path d="M0.5 10.5 a10 10 0 0 1 10 -10 h339 a10 10 0 0 1 10 10 v36 h-359 z" fill="#34373d"/>
|
||||
<circle cx="17" cy="17" r="4.5" fill="#F6AD55"/>
|
||||
<text x="28" y="22" font-size="14" font-weight="600" fill="#EEF0F3">NH Large Image Preview</text>
|
||||
<text x="28" y="38" font-size="10.5" fill="#9AA0A8" font-family="'JetBrains Mono', Consolas, 'Courier New', monospace">NH_LargeImagePreview · Image</text>
|
||||
<circle cx="12" cy="67.0" r="5" fill="#64B5F6" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="24" y="71.0" font-size="12" fill="#D7DADF">image</text>
|
||||
<text x="336" y="71.0" font-size="12" fill="#D7DADF" text-anchor="end">image</text>
|
||||
<circle cx="348" cy="67.0" r="5" fill="#64B5F6" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="336" y="93.0" font-size="12" fill="#D7DADF" text-anchor="end">manifest</text>
|
||||
<circle cx="348" cy="89.0" r="5" fill="#C5E1A5" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<rect x="12" y="108" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="20" y="124" font-size="10" fill="#6f747c">◀</text>
|
||||
<text x="332" y="124" font-size="10" fill="#6f747c">▶</text>
|
||||
<text x="34" y="124" font-size="11.5" fill="#9AA0A8">preview_megapixels</text>
|
||||
<text x="326" y="124" font-size="11.5" fill="#E6E8EB" text-anchor="end">1.00</text>
|
||||
<rect x="12" y="137" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="20" y="153" font-size="10" fill="#6f747c">◀</text>
|
||||
<text x="332" y="153" font-size="10" fill="#6f747c">▶</text>
|
||||
<text x="34" y="153" font-size="11.5" fill="#9AA0A8">preview_format</text>
|
||||
<text x="326" y="153" font-size="11.5" fill="#E6E8EB" text-anchor="end">jpg</text>
|
||||
<rect x="12" y="166" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="20" y="182" font-size="10" fill="#6f747c">◀</text>
|
||||
<text x="332" y="182" font-size="10" fill="#6f747c">▶</text>
|
||||
<text x="34" y="182" font-size="11.5" fill="#9AA0A8">preview_quality</text>
|
||||
<text x="326" y="182" font-size="11.5" fill="#E6E8EB" text-anchor="end">90</text>
|
||||
<rect x="12" y="195" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="20" y="211" font-size="10" fill="#6f747c">◀</text>
|
||||
<text x="332" y="211" font-size="10" fill="#6f747c">▶</text>
|
||||
<text x="34" y="211" font-size="11.5" fill="#9AA0A8">viewer_mode</text>
|
||||
<text x="326" y="211" font-size="11.5" fill="#E6E8EB" text-anchor="end">modal</text>
|
||||
<rect x="12" y="224" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="22" y="240" font-size="11.5" fill="#9AA0A8">title</text>
|
||||
<text x="338" y="240" font-size="11.5" fill="#E6E8EB" text-anchor="end">NH Large Image Preview</text>
|
||||
<rect x="12" y="253" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="20" y="269" font-size="10" fill="#6f747c">◀</text>
|
||||
<text x="332" y="269" font-size="10" fill="#6f747c">▶</text>
|
||||
<text x="34" y="269" font-size="11.5" fill="#9AA0A8">tile_size <tspan fill="#6f747c" font-style="italic">opt</tspan></text>
|
||||
<text x="326" y="269" font-size="11.5" fill="#E6E8EB" text-anchor="end">512</text>
|
||||
<rect x="12" y="282" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="20" y="298" font-size="10" fill="#6f747c">◀</text>
|
||||
<text x="332" y="298" font-size="10" fill="#6f747c">▶</text>
|
||||
<text x="34" y="298" font-size="11.5" fill="#9AA0A8">tile_format <tspan fill="#6f747c" font-style="italic">opt</tspan></text>
|
||||
<text x="326" y="298" font-size="11.5" fill="#E6E8EB" text-anchor="end">jpg</text>
|
||||
<rect x="12" y="311" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="20" y="327" font-size="10" fill="#6f747c">◀</text>
|
||||
<text x="332" y="327" font-size="10" fill="#6f747c">▶</text>
|
||||
<text x="34" y="327" font-size="11.5" fill="#9AA0A8">tile_quality <tspan fill="#6f747c" font-style="italic">opt</tspan></text>
|
||||
<text x="326" y="327" font-size="11.5" fill="#E6E8EB" text-anchor="end">90</text>
|
||||
<rect x="12" y="340" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="20" y="356" font-size="10" fill="#6f747c">◀</text>
|
||||
<text x="332" y="356" font-size="10" fill="#6f747c">▶</text>
|
||||
<text x="34" y="356" font-size="11.5" fill="#9AA0A8">cache_policy <tspan fill="#6f747c" font-style="italic">opt</tspan></text>
|
||||
<text x="326" y="356" font-size="11.5" fill="#E6E8EB" text-anchor="end">temp</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.5 KiB |
@@ -0,0 +1,33 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="360" height="280" viewBox="0 0 360 280" font-family="Inter, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" role="img" aria-label="NH Large Preview Cache Manager node">
|
||||
<rect x="0.5" y="0.5" width="359" height="279" rx="10" fill="#26282c" stroke="#45484f"/>
|
||||
<path d="M0.5 10.5 a10 10 0 0 1 10 -10 h339 a10 10 0 0 1 10 10 v36 h-359 z" fill="#34373d"/>
|
||||
<circle cx="17" cy="17" r="4.5" fill="#F6AD55"/>
|
||||
<text x="28" y="22" font-size="14" font-weight="600" fill="#EEF0F3">NH Large Preview Cache Manager</text>
|
||||
<text x="28" y="38" font-size="10.5" fill="#9AA0A8" font-family="'JetBrains Mono', Consolas, 'Courier New', monospace">NH_LargePreviewCacheManager · Image</text>
|
||||
<text x="336" y="71.0" font-size="12" fill="#D7DADF" text-anchor="end">summary</text>
|
||||
<circle cx="348" cy="67.0" r="5" fill="#C5E1A5" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="336" y="93.0" font-size="12" fill="#D7DADF" text-anchor="end">removed</text>
|
||||
<circle cx="348" cy="89.0" r="5" fill="#9CC2E5" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="336" y="115.0" font-size="12" fill="#D7DADF" text-anchor="end">freed_mb</text>
|
||||
<circle cx="348" cy="111.0" r="5" fill="#9CC2E5" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="336" y="137.0" font-size="12" fill="#D7DADF" text-anchor="end">cache_root</text>
|
||||
<circle cx="348" cy="133.0" r="5" fill="#C5E1A5" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<rect x="12" y="152" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="20" y="168" font-size="10" fill="#6f747c">◀</text>
|
||||
<text x="332" y="168" font-size="10" fill="#6f747c">▶</text>
|
||||
<text x="34" y="168" font-size="11.5" fill="#9AA0A8">action</text>
|
||||
<text x="326" y="168" font-size="11.5" fill="#E6E8EB" text-anchor="end">info</text>
|
||||
<rect x="12" y="181" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="20" y="197" font-size="10" fill="#6f747c">◀</text>
|
||||
<text x="332" y="197" font-size="10" fill="#6f747c">▶</text>
|
||||
<text x="34" y="197" font-size="11.5" fill="#9AA0A8">max_age_hours</text>
|
||||
<text x="326" y="197" font-size="11.5" fill="#E6E8EB" text-anchor="end">24.0</text>
|
||||
<rect x="12" y="210" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="20" y="226" font-size="10" fill="#6f747c">◀</text>
|
||||
<text x="332" y="226" font-size="10" fill="#6f747c">▶</text>
|
||||
<text x="34" y="226" font-size="11.5" fill="#9AA0A8">max_total_mb</text>
|
||||
<text x="326" y="226" font-size="11.5" fill="#E6E8EB" text-anchor="end">2048.0</text>
|
||||
<rect x="12" y="239" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="22" y="255" font-size="11.5" fill="#9AA0A8">cache_id</text>
|
||||
<text x="338" y="255" font-size="11.5" fill="#E6E8EB" text-anchor="end"></text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.7 KiB |
@@ -0,0 +1,90 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="360" height="621" viewBox="0 0 360 621" font-family="Inter, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" role="img" aria-label="Layer Layout Composite (NH) node">
|
||||
<rect x="0.5" y="0.5" width="359" height="620" rx="10" fill="#26282c" stroke="#45484f"/>
|
||||
<path d="M0.5 10.5 a10 10 0 0 1 10 -10 h339 a10 10 0 0 1 10 10 v36 h-359 z" fill="#34373d"/>
|
||||
<circle cx="17" cy="17" r="4.5" fill="#8FD18F"/>
|
||||
<text x="28" y="22" font-size="14" font-weight="600" fill="#EEF0F3">Layer Layout Composite (NH)</text>
|
||||
<text x="28" y="38" font-size="10.5" fill="#9AA0A8" font-family="'JetBrains Mono', Consolas, 'Courier New', monospace">NH_LayerLayoutComposite · Image</text>
|
||||
<circle cx="12" cy="67.0" r="5" fill="#64B5F6" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="24" y="71.0" font-size="12" fill="#D7DADF">images</text>
|
||||
<text x="336" y="71.0" font-size="12" fill="#D7DADF" text-anchor="end">image</text>
|
||||
<circle cx="348" cy="67.0" r="5" fill="#64B5F6" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<circle cx="12" cy="89.0" r="5" fill="#64B5F6" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="24" y="93.0" font-size="12" fill="#D7DADF">background_image <tspan fill="#7d838c" font-style="italic">(optional)</tspan></text>
|
||||
<text x="336" y="93.0" font-size="12" fill="#D7DADF" text-anchor="end">slot_count</text>
|
||||
<circle cx="348" cy="89.0" r="5" fill="#9CC2E5" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="336" y="115.0" font-size="12" fill="#D7DADF" text-anchor="end">used_count</text>
|
||||
<circle cx="348" cy="111.0" r="5" fill="#9CC2E5" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="336" y="137.0" font-size="12" fill="#D7DADF" text-anchor="end">layout_info</text>
|
||||
<circle cx="348" cy="133.0" r="5" fill="#C5E1A5" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<rect x="12" y="152" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="20" y="168" font-size="10" fill="#6f747c">◀</text>
|
||||
<text x="332" y="168" font-size="10" fill="#6f747c">▶</text>
|
||||
<text x="34" y="168" font-size="11.5" fill="#9AA0A8">canvas_width</text>
|
||||
<text x="326" y="168" font-size="11.5" fill="#E6E8EB" text-anchor="end">2480</text>
|
||||
<rect x="12" y="181" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="20" y="197" font-size="10" fill="#6f747c">◀</text>
|
||||
<text x="332" y="197" font-size="10" fill="#6f747c">▶</text>
|
||||
<text x="34" y="197" font-size="11.5" fill="#9AA0A8">canvas_height</text>
|
||||
<text x="326" y="197" font-size="11.5" fill="#E6E8EB" text-anchor="end">3508</text>
|
||||
<rect x="12" y="210" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="22" y="226" font-size="11.5" fill="#9AA0A8">background_color</text>
|
||||
<text x="338" y="226" font-size="11.5" fill="#E6E8EB" text-anchor="end">#FFFFFF</text>
|
||||
<rect x="12" y="239" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="20" y="255" font-size="10" fill="#6f747c">◀</text>
|
||||
<text x="332" y="255" font-size="10" fill="#6f747c">▶</text>
|
||||
<text x="34" y="255" font-size="11.5" fill="#9AA0A8">layout_mode</text>
|
||||
<text x="326" y="255" font-size="11.5" fill="#E6E8EB" text-anchor="end">preset</text>
|
||||
<rect x="12" y="268" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="20" y="284" font-size="10" fill="#6f747c">◀</text>
|
||||
<text x="332" y="284" font-size="10" fill="#6f747c">▶</text>
|
||||
<text x="34" y="284" font-size="11.5" fill="#9AA0A8">layout_preset</text>
|
||||
<text x="326" y="284" font-size="11.5" fill="#E6E8EB" text-anchor="end">2x3</text>
|
||||
<rect x="12" y="297" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="20" y="313" font-size="10" fill="#6f747c">◀</text>
|
||||
<text x="332" y="313" font-size="10" fill="#6f747c">▶</text>
|
||||
<text x="34" y="313" font-size="11.5" fill="#9AA0A8">resize_mode</text>
|
||||
<text x="326" y="313" font-size="11.5" fill="#E6E8EB" text-anchor="end">cover</text>
|
||||
<rect x="12" y="326" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="20" y="342" font-size="10" fill="#6f747c">◀</text>
|
||||
<text x="332" y="342" font-size="10" fill="#6f747c">▶</text>
|
||||
<text x="34" y="342" font-size="11.5" fill="#9AA0A8">columns</text>
|
||||
<text x="326" y="342" font-size="11.5" fill="#E6E8EB" text-anchor="end">2</text>
|
||||
<rect x="12" y="355" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="20" y="371" font-size="10" fill="#6f747c">◀</text>
|
||||
<text x="332" y="371" font-size="10" fill="#6f747c">▶</text>
|
||||
<text x="34" y="371" font-size="11.5" fill="#9AA0A8">rows</text>
|
||||
<text x="326" y="371" font-size="11.5" fill="#E6E8EB" text-anchor="end">3</text>
|
||||
<rect x="12" y="384" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="20" y="400" font-size="10" fill="#6f747c">◀</text>
|
||||
<text x="332" y="400" font-size="10" fill="#6f747c">▶</text>
|
||||
<text x="34" y="400" font-size="11.5" fill="#9AA0A8">margin_x</text>
|
||||
<text x="326" y="400" font-size="11.5" fill="#E6E8EB" text-anchor="end">150</text>
|
||||
<rect x="12" y="413" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="20" y="429" font-size="10" fill="#6f747c">◀</text>
|
||||
<text x="332" y="429" font-size="10" fill="#6f747c">▶</text>
|
||||
<text x="34" y="429" font-size="11.5" fill="#9AA0A8">margin_y</text>
|
||||
<text x="326" y="429" font-size="11.5" fill="#E6E8EB" text-anchor="end">150</text>
|
||||
<rect x="12" y="442" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="20" y="458" font-size="10" fill="#6f747c">◀</text>
|
||||
<text x="332" y="458" font-size="10" fill="#6f747c">▶</text>
|
||||
<text x="34" y="458" font-size="11.5" fill="#9AA0A8">gap_x</text>
|
||||
<text x="326" y="458" font-size="11.5" fill="#E6E8EB" text-anchor="end">150</text>
|
||||
<rect x="12" y="471" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="20" y="487" font-size="10" fill="#6f747c">◀</text>
|
||||
<text x="332" y="487" font-size="10" fill="#6f747c">▶</text>
|
||||
<text x="34" y="487" font-size="11.5" fill="#9AA0A8">gap_y</text>
|
||||
<text x="326" y="487" font-size="11.5" fill="#E6E8EB" text-anchor="end">150</text>
|
||||
<rect x="12" y="500" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="20" y="516" font-size="10" fill="#6f747c">◀</text>
|
||||
<text x="332" y="516" font-size="10" fill="#6f747c">▶</text>
|
||||
<text x="34" y="516" font-size="11.5" fill="#9AA0A8">custom_units</text>
|
||||
<text x="326" y="516" font-size="11.5" fill="#E6E8EB" text-anchor="end">auto</text>
|
||||
<rect x="12" y="529" width="336" height="46" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="22" y="545" font-size="11.5" fill="#9AA0A8">custom_layout</text>
|
||||
<text x="22" y="565" font-size="11.5" fill="#E6E8EB" font-family="'JetBrains Mono', Consolas, 'Courier New', monospace">0.06,0.04,0.39,0.20 ⏎ 0.55,0.…</text>
|
||||
<rect x="12" y="580" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="20" y="596" font-size="10" fill="#6f747c">◀</text>
|
||||
<text x="332" y="596" font-size="10" fill="#6f747c">▶</text>
|
||||
<text x="34" y="596" font-size="11.5" fill="#9AA0A8">background_fit</text>
|
||||
<text x="326" y="596" font-size="11.5" fill="#E6E8EB" text-anchor="end">stretch</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 6.9 KiB |
@@ -0,0 +1,29 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="360" height="251" viewBox="0 0 360 251" font-family="Inter, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" role="img" aria-label="Layer Stack Composite (NH) node">
|
||||
<rect x="0.5" y="0.5" width="359" height="250" rx="10" fill="#26282c" stroke="#45484f"/>
|
||||
<path d="M0.5 10.5 a10 10 0 0 1 10 -10 h339 a10 10 0 0 1 10 10 v36 h-359 z" fill="#34373d"/>
|
||||
<circle cx="17" cy="17" r="4.5" fill="#8FD18F"/>
|
||||
<text x="28" y="22" font-size="14" font-weight="600" fill="#EEF0F3">Layer Stack Composite (NH)</text>
|
||||
<text x="28" y="38" font-size="10.5" fill="#9AA0A8" font-family="'JetBrains Mono', Consolas, 'Courier New', monospace">NH_LayerStackComposite · Image</text>
|
||||
<circle cx="12" cy="67.0" r="5" fill="#64B5F6" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="24" y="71.0" font-size="12" fill="#D7DADF">background_image</text>
|
||||
<text x="336" y="71.0" font-size="12" fill="#D7DADF" text-anchor="end">image</text>
|
||||
<circle cx="348" cy="67.0" r="5" fill="#64B5F6" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<circle cx="12" cy="89.0" r="5" fill="#64B5F6" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="24" y="93.0" font-size="12" fill="#D7DADF">layer_1 <tspan fill="#7d838c" font-style="italic">(optional)</tspan></text>
|
||||
<text x="336" y="93.0" font-size="12" fill="#D7DADF" text-anchor="end">used_count</text>
|
||||
<circle cx="348" cy="89.0" r="5" fill="#9CC2E5" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<circle cx="12" cy="111.0" r="5" fill="#64B5F6" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="24" y="115.0" font-size="12" fill="#D7DADF">layer_2 <tspan fill="#7d838c" font-style="italic">(optional)</tspan></text>
|
||||
<text x="336" y="115.0" font-size="12" fill="#D7DADF" text-anchor="end">layer_info</text>
|
||||
<circle cx="348" cy="111.0" r="5" fill="#C5E1A5" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<rect x="12" y="130" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="20" y="146" font-size="10" fill="#6f747c">◀</text>
|
||||
<text x="332" y="146" font-size="10" fill="#6f747c">▶</text>
|
||||
<text x="34" y="146" font-size="11.5" fill="#9AA0A8">layer_count</text>
|
||||
<text x="326" y="146" font-size="11.5" fill="#E6E8EB" text-anchor="end">1</text>
|
||||
<rect x="12" y="159" width="336" height="46" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="22" y="175" font-size="11.5" fill="#9AA0A8">positions</text>
|
||||
<text x="22" y="195" font-size="11.5" fill="#E6E8EB" font-family="'JetBrains Mono', Consolas, 'Courier New', monospace">0,0</text>
|
||||
<rect x="12" y="210" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="180.0" y="226" font-size="11" fill="#9AA0A8" text-anchor="middle" font-style="italic">layer_3 … layer_64</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.6 KiB |
@@ -0,0 +1,21 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="360" height="244" viewBox="0 0 360 244" font-family="Inter, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" role="img" aria-label="List Create (NH) node">
|
||||
<rect x="0.5" y="0.5" width="359" height="243" rx="10" fill="#26282c" stroke="#45484f"/>
|
||||
<path d="M0.5 10.5 a10 10 0 0 1 10 -10 h339 a10 10 0 0 1 10 10 v36 h-359 z" fill="#34373d"/>
|
||||
<circle cx="17" cy="17" r="4.5" fill="#8FD18F"/>
|
||||
<text x="28" y="22" font-size="14" font-weight="600" fill="#EEF0F3">List Create (NH)</text>
|
||||
<text x="28" y="38" font-size="10.5" fill="#9AA0A8" font-family="'JetBrains Mono', Consolas, 'Courier New', monospace">NH_ListCreate · Batch</text>
|
||||
<text x="336" y="71.0" font-size="12" fill="#D7DADF" text-anchor="end">items</text>
|
||||
<circle cx="348" cy="67.0" r="5" fill="#E0B272" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="336" y="93.0" font-size="12" fill="#D7DADF" text-anchor="end">count</text>
|
||||
<circle cx="348" cy="89.0" r="5" fill="#9CC2E5" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="336" y="115.0" font-size="12" fill="#D7DADF" text-anchor="end">first</text>
|
||||
<circle cx="348" cy="111.0" r="5" fill="#C5E1A5" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="336" y="137.0" font-size="12" fill="#D7DADF" text-anchor="end">last</text>
|
||||
<circle cx="348" cy="133.0" r="5" fill="#C5E1A5" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<rect x="12" y="152" width="336" height="46" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="22" y="168" font-size="11.5" fill="#9AA0A8">text</text>
|
||||
<text x="22" y="188" font-size="11.5" fill="#E6E8EB" font-family="'JetBrains Mono', Consolas, 'Courier New', monospace">item 1 ⏎ item 2 ⏎ item 3</text>
|
||||
<rect x="12" y="203" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="22" y="219" font-size="11.5" fill="#9AA0A8">delimiter <tspan fill="#6f747c" font-style="italic">opt</tspan></text>
|
||||
<text x="338" y="219" font-size="11.5" fill="#E6E8EB" text-anchor="end">\n</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.9 KiB |
@@ -0,0 +1,25 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="360" height="222" viewBox="0 0 360 222" font-family="Inter, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" role="img" aria-label="List Filter (NH) node">
|
||||
<rect x="0.5" y="0.5" width="359" height="221" rx="10" fill="#26282c" stroke="#45484f"/>
|
||||
<path d="M0.5 10.5 a10 10 0 0 1 10 -10 h339 a10 10 0 0 1 10 10 v36 h-359 z" fill="#34373d"/>
|
||||
<circle cx="17" cy="17" r="4.5" fill="#8FD18F"/>
|
||||
<text x="28" y="22" font-size="14" font-weight="600" fill="#EEF0F3">List Filter (NH)</text>
|
||||
<text x="28" y="38" font-size="10.5" fill="#9AA0A8" font-family="'JetBrains Mono', Consolas, 'Courier New', monospace">NH_ListFilter · Batch</text>
|
||||
<circle cx="12" cy="67.0" r="5" fill="#E0B272" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="24" y="71.0" font-size="12" fill="#D7DADF">items</text>
|
||||
<text x="336" y="71.0" font-size="12" fill="#D7DADF" text-anchor="end">passed</text>
|
||||
<circle cx="348" cy="67.0" r="5" fill="#E0B272" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="336" y="93.0" font-size="12" fill="#D7DADF" text-anchor="end">rejected</text>
|
||||
<circle cx="348" cy="89.0" r="5" fill="#E0B272" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="336" y="115.0" font-size="12" fill="#D7DADF" text-anchor="end">passed_count</text>
|
||||
<circle cx="348" cy="111.0" r="5" fill="#9CC2E5" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="336" y="137.0" font-size="12" fill="#D7DADF" text-anchor="end">rejected_count</text>
|
||||
<circle cx="348" cy="133.0" r="5" fill="#9CC2E5" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<rect x="12" y="152" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="22" y="168" font-size="11.5" fill="#9AA0A8">condition</text>
|
||||
<text x="338" y="168" font-size="11.5" fill="#E6E8EB" text-anchor="end">contains:dress</text>
|
||||
<rect x="12" y="181" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="20" y="197" font-size="10" fill="#6f747c">◀</text>
|
||||
<text x="332" y="197" font-size="10" fill="#6f747c">▶</text>
|
||||
<text x="34" y="197" font-size="11.5" fill="#9AA0A8">mode</text>
|
||||
<text x="326" y="197" font-size="11.5" fill="#E6E8EB" text-anchor="end">include</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.1 KiB |
@@ -0,0 +1,26 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="360" height="222" viewBox="0 0 360 222" font-family="Inter, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" role="img" aria-label="List Index (NH) node">
|
||||
<rect x="0.5" y="0.5" width="359" height="221" rx="10" fill="#26282c" stroke="#45484f"/>
|
||||
<path d="M0.5 10.5 a10 10 0 0 1 10 -10 h339 a10 10 0 0 1 10 10 v36 h-359 z" fill="#34373d"/>
|
||||
<circle cx="17" cy="17" r="4.5" fill="#8FD18F"/>
|
||||
<text x="28" y="22" font-size="14" font-weight="600" fill="#EEF0F3">List Index (NH)</text>
|
||||
<text x="28" y="38" font-size="10.5" fill="#9AA0A8" font-family="'JetBrains Mono', Consolas, 'Courier New', monospace">NH_ListIndex · Batch</text>
|
||||
<circle cx="12" cy="67.0" r="5" fill="#E0B272" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="24" y="71.0" font-size="12" fill="#D7DADF">items</text>
|
||||
<text x="336" y="71.0" font-size="12" fill="#D7DADF" text-anchor="end">item</text>
|
||||
<circle cx="348" cy="67.0" r="5" fill="#C5E1A5" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="336" y="93.0" font-size="12" fill="#D7DADF" text-anchor="end">is_first</text>
|
||||
<circle cx="348" cy="89.0" r="5" fill="#F48FB1" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="336" y="115.0" font-size="12" fill="#D7DADF" text-anchor="end">is_last</text>
|
||||
<circle cx="348" cy="111.0" r="5" fill="#F48FB1" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="336" y="137.0" font-size="12" fill="#D7DADF" text-anchor="end">count</text>
|
||||
<circle cx="348" cy="133.0" r="5" fill="#9CC2E5" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<rect x="12" y="152" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="20" y="168" font-size="10" fill="#6f747c">◀</text>
|
||||
<text x="332" y="168" font-size="10" fill="#6f747c">▶</text>
|
||||
<text x="34" y="168" font-size="11.5" fill="#9AA0A8">index</text>
|
||||
<text x="326" y="168" font-size="11.5" fill="#E6E8EB" text-anchor="end">0</text>
|
||||
<rect x="12" y="181" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="22" y="197" font-size="11.5" fill="#9AA0A8">wrap <tspan fill="#6f747c" font-style="italic">opt</tspan></text>
|
||||
<rect x="308" y="187" width="30" height="13" rx="6.5" fill="#4C8BF5"/>
|
||||
<circle cx="331" cy="193.5" r="5" fill="#F5F6F7"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.2 KiB |
@@ -0,0 +1,28 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="360" height="251" viewBox="0 0 360 251" font-family="Inter, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" role="img" aria-label="Load Image Info (NH) node">
|
||||
<rect x="0.5" y="0.5" width="359" height="250" rx="10" fill="#26282c" stroke="#45484f"/>
|
||||
<path d="M0.5 10.5 a10 10 0 0 1 10 -10 h339 a10 10 0 0 1 10 10 v36 h-359 z" fill="#34373d"/>
|
||||
<circle cx="17" cy="17" r="4.5" fill="#8FD18F"/>
|
||||
<text x="28" y="22" font-size="14" font-weight="600" fill="#EEF0F3">Load Image Info (NH)</text>
|
||||
<text x="28" y="38" font-size="10.5" fill="#9AA0A8" font-family="'JetBrains Mono', Consolas, 'Courier New', monospace">NH_LoadImageInfo · Image</text>
|
||||
<text x="336" y="71.0" font-size="12" fill="#D7DADF" text-anchor="end">image</text>
|
||||
<circle cx="348" cy="67.0" r="5" fill="#64B5F6" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="336" y="93.0" font-size="12" fill="#D7DADF" text-anchor="end">mask</text>
|
||||
<circle cx="348" cy="89.0" r="5" fill="#81C784" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="336" y="115.0" font-size="12" fill="#D7DADF" text-anchor="end">file_name</text>
|
||||
<circle cx="348" cy="111.0" r="5" fill="#C5E1A5" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="336" y="137.0" font-size="12" fill="#D7DADF" text-anchor="end">path</text>
|
||||
<circle cx="348" cy="133.0" r="5" fill="#C5E1A5" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<rect x="12" y="152" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="20" y="168" font-size="10" fill="#6f747c">◀</text>
|
||||
<text x="332" y="168" font-size="10" fill="#6f747c">▶</text>
|
||||
<text x="34" y="168" font-size="11.5" fill="#9AA0A8">image</text>
|
||||
<text x="326" y="168" font-size="11.5" fill="#E6E8EB" text-anchor="end">(choose a file)</text>
|
||||
<rect x="12" y="181" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="22" y="197" font-size="11.5" fill="#9AA0A8">filename_extension</text>
|
||||
<rect x="308" y="187" width="30" height="13" rx="6.5" fill="#4C8BF5"/>
|
||||
<circle cx="331" cy="193.5" r="5" fill="#F5F6F7"/>
|
||||
<rect x="12" y="210" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="22" y="226" font-size="11.5" fill="#9AA0A8">path_include_filename</text>
|
||||
<rect x="308" y="216" width="30" height="13" rx="6.5" fill="#4C8BF5"/>
|
||||
<circle cx="331" cy="222.5" r="5" fill="#F5F6F7"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.3 KiB |
@@ -0,0 +1,44 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="360" height="345" viewBox="0 0 360 345" font-family="Inter, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" role="img" aria-label="Load Images Folder (NH) node">
|
||||
<rect x="0.5" y="0.5" width="359" height="344" rx="10" fill="#26282c" stroke="#45484f"/>
|
||||
<path d="M0.5 10.5 a10 10 0 0 1 10 -10 h339 a10 10 0 0 1 10 10 v36 h-359 z" fill="#34373d"/>
|
||||
<circle cx="17" cy="17" r="4.5" fill="#8FD18F"/>
|
||||
<text x="28" y="22" font-size="14" font-weight="600" fill="#EEF0F3">Load Images Folder (NH)</text>
|
||||
<text x="28" y="38" font-size="10.5" fill="#9AA0A8" font-family="'JetBrains Mono', Consolas, 'Courier New', monospace">NH_LoadImagesFromFolder · Image</text>
|
||||
<text x="336" y="71.0" font-size="12" fill="#D7DADF" text-anchor="end">images</text>
|
||||
<circle cx="348" cy="67.0" r="5" fill="#64B5F6" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="336" y="93.0" font-size="12" fill="#D7DADF" text-anchor="end">filename</text>
|
||||
<circle cx="348" cy="89.0" r="5" fill="#C5E1A5" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="336" y="115.0" font-size="12" fill="#D7DADF" text-anchor="end">path</text>
|
||||
<circle cx="348" cy="111.0" r="5" fill="#C5E1A5" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<rect x="12" y="130" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="22" y="146" font-size="11.5" fill="#9AA0A8">folder_path</text>
|
||||
<text x="338" y="146" font-size="11.5" fill="#E6E8EB" text-anchor="end"></text>
|
||||
<rect x="12" y="159" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="20" y="175" font-size="10" fill="#6f747c">◀</text>
|
||||
<text x="332" y="175" font-size="10" fill="#6f747c">▶</text>
|
||||
<text x="34" y="175" font-size="11.5" fill="#9AA0A8">index</text>
|
||||
<text x="326" y="175" font-size="11.5" fill="#E6E8EB" text-anchor="end">0</text>
|
||||
<rect x="12" y="188" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="20" y="204" font-size="10" fill="#6f747c">◀</text>
|
||||
<text x="332" y="204" font-size="10" fill="#6f747c">▶</text>
|
||||
<text x="34" y="204" font-size="11.5" fill="#9AA0A8">step</text>
|
||||
<text x="326" y="204" font-size="11.5" fill="#E6E8EB" text-anchor="end">1</text>
|
||||
<rect x="12" y="217" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="20" y="233" font-size="10" fill="#6f747c">◀</text>
|
||||
<text x="332" y="233" font-size="10" fill="#6f747c">▶</text>
|
||||
<text x="34" y="233" font-size="11.5" fill="#9AA0A8">seed_mode</text>
|
||||
<text x="326" y="233" font-size="11.5" fill="#E6E8EB" text-anchor="end">fixed</text>
|
||||
<rect x="12" y="246" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="22" y="262" font-size="11.5" fill="#9AA0A8">recursive</text>
|
||||
<rect x="308" y="252" width="30" height="13" rx="6.5" fill="#4a4d53"/>
|
||||
<circle cx="315" cy="258.5" r="5" fill="#F5F6F7"/>
|
||||
<rect x="12" y="275" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="22" y="291" font-size="11.5" fill="#9AA0A8">filename_extension</text>
|
||||
<rect x="308" y="281" width="30" height="13" rx="6.5" fill="#4C8BF5"/>
|
||||
<circle cx="331" cy="287.5" r="5" fill="#F5F6F7"/>
|
||||
<rect x="12" y="304" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="20" y="320" font-size="10" fill="#6f747c">◀</text>
|
||||
<text x="332" y="320" font-size="10" fill="#6f747c">▶</text>
|
||||
<text x="34" y="320" font-size="11.5" fill="#9AA0A8">path_output</text>
|
||||
<text x="326" y="320" font-size="11.5" fill="#E6E8EB" text-anchor="end">file_path</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.4 KiB |
@@ -0,0 +1,58 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="360" height="520" viewBox="0 0 360 520" font-family="Inter, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" role="img" aria-label="Load images matching node">
|
||||
<rect x="0.5" y="0.5" width="359" height="519" rx="10" fill="#26282c" stroke="#45484f"/>
|
||||
<path d="M0.5 10.5 a10 10 0 0 1 10 -10 h339 a10 10 0 0 1 10 10 v36 h-359 z" fill="#34373d"/>
|
||||
<circle cx="17" cy="17" r="4.5" fill="#8FD18F"/>
|
||||
<text x="28" y="22" font-size="14" font-weight="600" fill="#EEF0F3">Load images matching</text>
|
||||
<text x="28" y="38" font-size="10.5" fill="#9AA0A8" font-family="'JetBrains Mono', Consolas, 'Courier New', monospace">NH_LoadImagesMatching · Image</text>
|
||||
<circle cx="12" cy="67.0" r="5" fill="#64B5F6" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="24" y="71.0" font-size="12" fill="#D7DADF">fallback_images <tspan fill="#7d838c" font-style="italic">(optional)</tspan></text>
|
||||
<text x="336" y="71.0" font-size="12" fill="#D7DADF" text-anchor="end">images</text>
|
||||
<circle cx="348" cy="67.0" r="5" fill="#64B5F6" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="336" y="93.0" font-size="12" fill="#D7DADF" text-anchor="end">filename</text>
|
||||
<circle cx="348" cy="89.0" r="5" fill="#C5E1A5" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="336" y="115.0" font-size="12" fill="#D7DADF" text-anchor="end">path</text>
|
||||
<circle cx="348" cy="111.0" r="5" fill="#C5E1A5" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="336" y="137.0" font-size="12" fill="#D7DADF" text-anchor="end">matched_text</text>
|
||||
<circle cx="348" cy="133.0" r="5" fill="#C5E1A5" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="336" y="159.0" font-size="12" fill="#D7DADF" text-anchor="end">found</text>
|
||||
<circle cx="348" cy="155.0" r="5" fill="#F48FB1" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<rect x="12" y="174" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="22" y="190" font-size="11.5" fill="#9AA0A8">source_folder_path</text>
|
||||
<text x="338" y="190" font-size="11.5" fill="#E6E8EB" text-anchor="end"></text>
|
||||
<rect x="12" y="203" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="22" y="219" font-size="11.5" fill="#9AA0A8">matching_root_path</text>
|
||||
<text x="338" y="219" font-size="11.5" fill="#E6E8EB" text-anchor="end"></text>
|
||||
<rect x="12" y="232" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="22" y="248" font-size="11.5" fill="#9AA0A8">match_text</text>
|
||||
<text x="338" y="248" font-size="11.5" fill="#E6E8EB" text-anchor="end"></text>
|
||||
<rect x="12" y="261" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="20" y="277" font-size="10" fill="#6f747c">◀</text>
|
||||
<text x="332" y="277" font-size="10" fill="#6f747c">▶</text>
|
||||
<text x="34" y="277" font-size="11.5" fill="#9AA0A8">match_mode</text>
|
||||
<text x="326" y="277" font-size="11.5" fill="#E6E8EB" text-anchor="end">contains</text>
|
||||
<rect x="12" y="290" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="22" y="306" font-size="11.5" fill="#9AA0A8">recursive</text>
|
||||
<rect x="308" y="296" width="30" height="13" rx="6.5" fill="#4C8BF5"/>
|
||||
<circle cx="331" cy="302.5" r="5" fill="#F5F6F7"/>
|
||||
<rect x="12" y="319" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="20" y="335" font-size="10" fill="#6f747c">◀</text>
|
||||
<text x="332" y="335" font-size="10" fill="#6f747c">▶</text>
|
||||
<text x="34" y="335" font-size="11.5" fill="#9AA0A8">index</text>
|
||||
<text x="326" y="335" font-size="11.5" fill="#E6E8EB" text-anchor="end">0</text>
|
||||
<rect x="12" y="348" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="20" y="364" font-size="10" fill="#6f747c">◀</text>
|
||||
<text x="332" y="364" font-size="10" fill="#6f747c">▶</text>
|
||||
<text x="34" y="364" font-size="11.5" fill="#9AA0A8">step</text>
|
||||
<text x="326" y="364" font-size="11.5" fill="#E6E8EB" text-anchor="end">1</text>
|
||||
<rect x="12" y="377" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="20" y="393" font-size="10" fill="#6f747c">◀</text>
|
||||
<text x="332" y="393" font-size="10" fill="#6f747c">▶</text>
|
||||
<text x="34" y="393" font-size="11.5" fill="#9AA0A8">repeat_count</text>
|
||||
<text x="326" y="393" font-size="11.5" fill="#E6E8EB" text-anchor="end">1</text>
|
||||
<rect x="12" y="406" width="336" height="46" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="22" y="422" font-size="11.5" fill="#9AA0A8">context_folder_names</text>
|
||||
<text x="22" y="442" font-size="11.5" fill="#E6E8EB" font-family="'JetBrains Mono', Consolas, 'Courier New', monospace">Nam ⏎ Nu</text>
|
||||
<rect x="12" y="457" width="336" height="46" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="22" y="473" font-size="11.5" fill="#9AA0A8">source_file_names <tspan fill="#6f747c" font-style="italic">opt</tspan></text>
|
||||
<text x="22" y="493" font-size="11.5" fill="#E6E8EB" font-family="'JetBrains Mono', Consolas, 'Courier New', monospace">…</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.8 KiB |
@@ -0,0 +1,22 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="360" height="185" viewBox="0 0 360 185" font-family="Inter, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" role="img" aria-label="Logic Gate (NH) node">
|
||||
<rect x="0.5" y="0.5" width="359" height="184" rx="10" fill="#26282c" stroke="#45484f"/>
|
||||
<path d="M0.5 10.5 a10 10 0 0 1 10 -10 h339 a10 10 0 0 1 10 10 v36 h-359 z" fill="#34373d"/>
|
||||
<circle cx="17" cy="17" r="4.5" fill="#8FD18F"/>
|
||||
<text x="28" y="22" font-size="14" font-weight="600" fill="#EEF0F3">Logic Gate (NH)</text>
|
||||
<text x="28" y="38" font-size="10.5" fill="#9AA0A8" font-family="'JetBrains Mono', Consolas, 'Courier New', monospace">NH_LogicGate · Logic</text>
|
||||
<text x="336" y="71.0" font-size="12" fill="#D7DADF" text-anchor="end">result</text>
|
||||
<circle cx="348" cy="67.0" r="5" fill="#F48FB1" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<rect x="12" y="86" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="22" y="102" font-size="11.5" fill="#9AA0A8">a</text>
|
||||
<rect x="308" y="92" width="30" height="13" rx="6.5" fill="#4a4d53"/>
|
||||
<circle cx="315" cy="98.5" r="5" fill="#F5F6F7"/>
|
||||
<rect x="12" y="115" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="20" y="131" font-size="10" fill="#6f747c">◀</text>
|
||||
<text x="332" y="131" font-size="10" fill="#6f747c">▶</text>
|
||||
<text x="34" y="131" font-size="11.5" fill="#9AA0A8">op</text>
|
||||
<text x="326" y="131" font-size="11.5" fill="#E6E8EB" text-anchor="end">AND</text>
|
||||
<rect x="12" y="144" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="22" y="160" font-size="11.5" fill="#9AA0A8">b <tspan fill="#6f747c" font-style="italic">opt</tspan></text>
|
||||
<rect x="308" y="150" width="30" height="13" rx="6.5" fill="#4a4d53"/>
|
||||
<circle cx="315" cy="156.5" r="5" fill="#F5F6F7"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
@@ -0,0 +1,66 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="360" height="483" viewBox="0 0 360 483" font-family="Inter, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" role="img" aria-label="Load LoRA Clip Index (NH) node">
|
||||
<rect x="0.5" y="0.5" width="359" height="482" rx="10" fill="#26282c" stroke="#45484f"/>
|
||||
<path d="M0.5 10.5 a10 10 0 0 1 10 -10 h339 a10 10 0 0 1 10 10 v36 h-359 z" fill="#34373d"/>
|
||||
<circle cx="17" cy="17" r="4.5" fill="#8FD18F"/>
|
||||
<text x="28" y="22" font-size="14" font-weight="600" fill="#EEF0F3">Load LoRA Clip Index (NH)</text>
|
||||
<text x="28" y="38" font-size="10.5" fill="#9AA0A8" font-family="'JetBrains Mono', Consolas, 'Courier New', monospace">NH_LoraClipIndex · Loaders</text>
|
||||
<circle cx="12" cy="67.0" r="5" fill="#B39DDB" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="24" y="71.0" font-size="12" fill="#D7DADF">model</text>
|
||||
<text x="336" y="71.0" font-size="12" fill="#D7DADF" text-anchor="end">model</text>
|
||||
<circle cx="348" cy="67.0" r="5" fill="#B39DDB" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<circle cx="12" cy="89.0" r="5" fill="#FFD500" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="24" y="93.0" font-size="12" fill="#D7DADF">clip</text>
|
||||
<text x="336" y="93.0" font-size="12" fill="#D7DADF" text-anchor="end">clip</text>
|
||||
<circle cx="348" cy="89.0" r="5" fill="#FFD500" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="336" y="115.0" font-size="12" fill="#D7DADF" text-anchor="end">filename</text>
|
||||
<circle cx="348" cy="111.0" r="5" fill="#C5E1A5" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="336" y="137.0" font-size="12" fill="#D7DADF" text-anchor="end">index</text>
|
||||
<circle cx="348" cy="133.0" r="5" fill="#9CC2E5" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<rect x="12" y="152" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="20" y="168" font-size="10" fill="#6f747c">◀</text>
|
||||
<text x="332" y="168" font-size="10" fill="#6f747c">▶</text>
|
||||
<text x="34" y="168" font-size="11.5" fill="#9AA0A8">select_mode</text>
|
||||
<text x="326" y="168" font-size="11.5" fill="#E6E8EB" text-anchor="end">index</text>
|
||||
<rect x="12" y="181" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="20" y="197" font-size="10" fill="#6f747c">◀</text>
|
||||
<text x="332" y="197" font-size="10" fill="#6f747c">▶</text>
|
||||
<text x="34" y="197" font-size="11.5" fill="#9AA0A8">index</text>
|
||||
<text x="326" y="197" font-size="11.5" fill="#E6E8EB" text-anchor="end">1</text>
|
||||
<rect x="12" y="210" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="20" y="226" font-size="10" fill="#6f747c">◀</text>
|
||||
<text x="332" y="226" font-size="10" fill="#6f747c">▶</text>
|
||||
<text x="34" y="226" font-size="11.5" fill="#9AA0A8">lora_count</text>
|
||||
<text x="326" y="226" font-size="11.5" fill="#E6E8EB" text-anchor="end">5</text>
|
||||
<rect x="12" y="239" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="20" y="255" font-size="10" fill="#6f747c">◀</text>
|
||||
<text x="332" y="255" font-size="10" fill="#6f747c">▶</text>
|
||||
<text x="34" y="255" font-size="11.5" fill="#9AA0A8">strength_model</text>
|
||||
<text x="326" y="255" font-size="11.5" fill="#E6E8EB" text-anchor="end">1.00</text>
|
||||
<rect x="12" y="268" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="20" y="284" font-size="10" fill="#6f747c">◀</text>
|
||||
<text x="332" y="284" font-size="10" fill="#6f747c">▶</text>
|
||||
<text x="34" y="284" font-size="11.5" fill="#9AA0A8">strength_clip</text>
|
||||
<text x="326" y="284" font-size="11.5" fill="#E6E8EB" text-anchor="end">1.00</text>
|
||||
<rect x="12" y="297" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="20" y="313" font-size="10" fill="#6f747c">◀</text>
|
||||
<text x="332" y="313" font-size="10" fill="#6f747c">▶</text>
|
||||
<text x="34" y="313" font-size="11.5" fill="#9AA0A8">lora_1 <tspan fill="#6f747c" font-style="italic">opt</tspan></text>
|
||||
<text x="326" y="313" font-size="11.5" fill="#E6E8EB" text-anchor="end">(choose a file)</text>
|
||||
<rect x="12" y="326" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="22" y="342" font-size="11.5" fill="#9AA0A8">enable_1 <tspan fill="#6f747c" font-style="italic">opt</tspan></text>
|
||||
<rect x="308" y="332" width="30" height="13" rx="6.5" fill="#4a4d53"/>
|
||||
<circle cx="315" cy="338.5" r="5" fill="#F5F6F7"/>
|
||||
<rect x="12" y="355" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="20" y="371" font-size="10" fill="#6f747c">◀</text>
|
||||
<text x="332" y="371" font-size="10" fill="#6f747c">▶</text>
|
||||
<text x="34" y="371" font-size="11.5" fill="#9AA0A8">lora_2 <tspan fill="#6f747c" font-style="italic">opt</tspan></text>
|
||||
<text x="326" y="371" font-size="11.5" fill="#E6E8EB" text-anchor="end">(choose a file)</text>
|
||||
<rect x="12" y="384" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="22" y="400" font-size="11.5" fill="#9AA0A8">enable_2 <tspan fill="#6f747c" font-style="italic">opt</tspan></text>
|
||||
<rect x="308" y="390" width="30" height="13" rx="6.5" fill="#4a4d53"/>
|
||||
<circle cx="315" cy="396.5" r="5" fill="#F5F6F7"/>
|
||||
<rect x="12" y="413" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="180.0" y="429" font-size="11" fill="#9AA0A8" text-anchor="middle" font-style="italic">lora_3 … lora_64</text>
|
||||
<rect x="12" y="442" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="180.0" y="458" font-size="11" fill="#9AA0A8" text-anchor="middle" font-style="italic">enable_3 … enable_64</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.3 KiB |
@@ -0,0 +1,57 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="360" height="432" viewBox="0 0 360 432" font-family="Inter, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" role="img" aria-label="Load LoRA Model Index (NH) node">
|
||||
<rect x="0.5" y="0.5" width="359" height="431" rx="10" fill="#26282c" stroke="#45484f"/>
|
||||
<path d="M0.5 10.5 a10 10 0 0 1 10 -10 h339 a10 10 0 0 1 10 10 v36 h-359 z" fill="#34373d"/>
|
||||
<circle cx="17" cy="17" r="4.5" fill="#8FD18F"/>
|
||||
<text x="28" y="22" font-size="14" font-weight="600" fill="#EEF0F3">Load LoRA Model Index (NH)</text>
|
||||
<text x="28" y="38" font-size="10.5" fill="#9AA0A8" font-family="'JetBrains Mono', Consolas, 'Courier New', monospace">NH_LoraModelIndex · Loaders</text>
|
||||
<circle cx="12" cy="67.0" r="5" fill="#B39DDB" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="24" y="71.0" font-size="12" fill="#D7DADF">model</text>
|
||||
<text x="336" y="71.0" font-size="12" fill="#D7DADF" text-anchor="end">model</text>
|
||||
<circle cx="348" cy="67.0" r="5" fill="#B39DDB" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="336" y="93.0" font-size="12" fill="#D7DADF" text-anchor="end">filename</text>
|
||||
<circle cx="348" cy="89.0" r="5" fill="#C5E1A5" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="336" y="115.0" font-size="12" fill="#D7DADF" text-anchor="end">index</text>
|
||||
<circle cx="348" cy="111.0" r="5" fill="#9CC2E5" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<rect x="12" y="130" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="20" y="146" font-size="10" fill="#6f747c">◀</text>
|
||||
<text x="332" y="146" font-size="10" fill="#6f747c">▶</text>
|
||||
<text x="34" y="146" font-size="11.5" fill="#9AA0A8">select_mode</text>
|
||||
<text x="326" y="146" font-size="11.5" fill="#E6E8EB" text-anchor="end">index</text>
|
||||
<rect x="12" y="159" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="20" y="175" font-size="10" fill="#6f747c">◀</text>
|
||||
<text x="332" y="175" font-size="10" fill="#6f747c">▶</text>
|
||||
<text x="34" y="175" font-size="11.5" fill="#9AA0A8">index</text>
|
||||
<text x="326" y="175" font-size="11.5" fill="#E6E8EB" text-anchor="end">1</text>
|
||||
<rect x="12" y="188" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="20" y="204" font-size="10" fill="#6f747c">◀</text>
|
||||
<text x="332" y="204" font-size="10" fill="#6f747c">▶</text>
|
||||
<text x="34" y="204" font-size="11.5" fill="#9AA0A8">lora_count</text>
|
||||
<text x="326" y="204" font-size="11.5" fill="#E6E8EB" text-anchor="end">5</text>
|
||||
<rect x="12" y="217" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="20" y="233" font-size="10" fill="#6f747c">◀</text>
|
||||
<text x="332" y="233" font-size="10" fill="#6f747c">▶</text>
|
||||
<text x="34" y="233" font-size="11.5" fill="#9AA0A8">strength_model</text>
|
||||
<text x="326" y="233" font-size="11.5" fill="#E6E8EB" text-anchor="end">1.00</text>
|
||||
<rect x="12" y="246" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="20" y="262" font-size="10" fill="#6f747c">◀</text>
|
||||
<text x="332" y="262" font-size="10" fill="#6f747c">▶</text>
|
||||
<text x="34" y="262" font-size="11.5" fill="#9AA0A8">lora_1 <tspan fill="#6f747c" font-style="italic">opt</tspan></text>
|
||||
<text x="326" y="262" font-size="11.5" fill="#E6E8EB" text-anchor="end">(choose a file)</text>
|
||||
<rect x="12" y="275" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="22" y="291" font-size="11.5" fill="#9AA0A8">enable_1 <tspan fill="#6f747c" font-style="italic">opt</tspan></text>
|
||||
<rect x="308" y="281" width="30" height="13" rx="6.5" fill="#4a4d53"/>
|
||||
<circle cx="315" cy="287.5" r="5" fill="#F5F6F7"/>
|
||||
<rect x="12" y="304" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="20" y="320" font-size="10" fill="#6f747c">◀</text>
|
||||
<text x="332" y="320" font-size="10" fill="#6f747c">▶</text>
|
||||
<text x="34" y="320" font-size="11.5" fill="#9AA0A8">lora_2 <tspan fill="#6f747c" font-style="italic">opt</tspan></text>
|
||||
<text x="326" y="320" font-size="11.5" fill="#E6E8EB" text-anchor="end">(choose a file)</text>
|
||||
<rect x="12" y="333" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="22" y="349" font-size="11.5" fill="#9AA0A8">enable_2 <tspan fill="#6f747c" font-style="italic">opt</tspan></text>
|
||||
<rect x="308" y="339" width="30" height="13" rx="6.5" fill="#4a4d53"/>
|
||||
<circle cx="315" cy="345.5" r="5" fill="#F5F6F7"/>
|
||||
<rect x="12" y="362" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="180.0" y="378" font-size="11" fill="#9AA0A8" text-anchor="middle" font-style="italic">lora_3 … lora_64</text>
|
||||
<rect x="12" y="391" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="180.0" y="407" font-size="11" fill="#9AA0A8" text-anchor="middle" font-style="italic">enable_3 … enable_64</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.7 KiB |
@@ -0,0 +1,30 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="360" height="214" viewBox="0 0 360 214" font-family="Inter, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" role="img" aria-label="Mask Morphology (NH) node">
|
||||
<rect x="0.5" y="0.5" width="359" height="213" rx="10" fill="#26282c" stroke="#45484f"/>
|
||||
<path d="M0.5 10.5 a10 10 0 0 1 10 -10 h339 a10 10 0 0 1 10 10 v36 h-359 z" fill="#34373d"/>
|
||||
<circle cx="17" cy="17" r="4.5" fill="#8FD18F"/>
|
||||
<text x="28" y="22" font-size="14" font-weight="600" fill="#EEF0F3">Mask Morphology (NH)</text>
|
||||
<text x="28" y="38" font-size="10.5" fill="#9AA0A8" font-family="'JetBrains Mono', Consolas, 'Courier New', monospace">NH_MaskAdvancedMorphology · Mask</text>
|
||||
<circle cx="12" cy="67.0" r="5" fill="#81C784" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="24" y="71.0" font-size="12" fill="#D7DADF">mask</text>
|
||||
<text x="336" y="71.0" font-size="12" fill="#D7DADF" text-anchor="end">MASK</text>
|
||||
<circle cx="348" cy="67.0" r="5" fill="#81C784" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<rect x="12" y="86" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="20" y="102" font-size="10" fill="#6f747c">◀</text>
|
||||
<text x="332" y="102" font-size="10" fill="#6f747c">▶</text>
|
||||
<text x="34" y="102" font-size="11.5" fill="#9AA0A8">horizontal_expand</text>
|
||||
<text x="326" y="102" font-size="11.5" fill="#E6E8EB" text-anchor="end">0</text>
|
||||
<rect x="12" y="115" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="20" y="131" font-size="10" fill="#6f747c">◀</text>
|
||||
<text x="332" y="131" font-size="10" fill="#6f747c">▶</text>
|
||||
<text x="34" y="131" font-size="11.5" fill="#9AA0A8">vertical_expand</text>
|
||||
<text x="326" y="131" font-size="11.5" fill="#E6E8EB" text-anchor="end">0</text>
|
||||
<rect x="12" y="144" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="22" y="160" font-size="11.5" fill="#9AA0A8">fill_holes</text>
|
||||
<rect x="308" y="150" width="30" height="13" rx="6.5" fill="#4a4d53"/>
|
||||
<circle cx="315" cy="156.5" r="5" fill="#F5F6F7"/>
|
||||
<rect x="12" y="173" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="20" y="189" font-size="10" fill="#6f747c">◀</text>
|
||||
<text x="332" y="189" font-size="10" fill="#6f747c">▶</text>
|
||||
<text x="34" y="189" font-size="11.5" fill="#9AA0A8">blur_radius</text>
|
||||
<text x="326" y="189" font-size="11.5" fill="#E6E8EB" text-anchor="end">0.0</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.3 KiB |
@@ -0,0 +1,18 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="360" height="149" viewBox="0 0 360 149" font-family="Inter, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" role="img" aria-label="NH Mask Aspect Ratio Match node">
|
||||
<rect x="0.5" y="0.5" width="359" height="148" rx="10" fill="#26282c" stroke="#45484f"/>
|
||||
<path d="M0.5 10.5 a10 10 0 0 1 10 -10 h339 a10 10 0 0 1 10 10 v36 h-359 z" fill="#34373d"/>
|
||||
<circle cx="17" cy="17" r="4.5" fill="#8FD18F"/>
|
||||
<text x="28" y="22" font-size="14" font-weight="600" fill="#EEF0F3">NH Mask Aspect Ratio Match</text>
|
||||
<text x="28" y="38" font-size="10.5" fill="#9AA0A8" font-family="'JetBrains Mono', Consolas, 'Courier New', monospace">NH_MaskAspectRatioMatch · Mask</text>
|
||||
<circle cx="12" cy="67.0" r="5" fill="#81C784" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="24" y="71.0" font-size="12" fill="#D7DADF">target_ratio_mask</text>
|
||||
<text x="336" y="71.0" font-size="12" fill="#D7DADF" text-anchor="end">MASK</text>
|
||||
<circle cx="348" cy="67.0" r="5" fill="#81C784" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<circle cx="12" cy="89.0" r="5" fill="#81C784" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="24" y="93.0" font-size="12" fill="#D7DADF">mask_to_adjust</text>
|
||||
<rect x="12" y="108" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="20" y="124" font-size="10" fill="#6f747c">◀</text>
|
||||
<text x="332" y="124" font-size="10" fill="#6f747c">▶</text>
|
||||
<text x="34" y="124" font-size="11.5" fill="#9AA0A8">mode</text>
|
||||
<text x="326" y="124" font-size="11.5" fill="#E6E8EB" text-anchor="end">stretch</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
@@ -0,0 +1,23 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="360" height="222" viewBox="0 0 360 222" font-family="Inter, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" role="img" aria-label="Mask Properties (NH) node">
|
||||
<rect x="0.5" y="0.5" width="359" height="221" rx="10" fill="#26282c" stroke="#45484f"/>
|
||||
<path d="M0.5 10.5 a10 10 0 0 1 10 -10 h339 a10 10 0 0 1 10 10 v36 h-359 z" fill="#34373d"/>
|
||||
<circle cx="17" cy="17" r="4.5" fill="#8FD18F"/>
|
||||
<text x="28" y="22" font-size="14" font-weight="600" fill="#EEF0F3">Mask Properties (NH)</text>
|
||||
<text x="28" y="38" font-size="10.5" fill="#9AA0A8" font-family="'JetBrains Mono', Consolas, 'Courier New', monospace">NH_MaskProperties · Mask</text>
|
||||
<circle cx="12" cy="67.0" r="5" fill="#81C784" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="24" y="71.0" font-size="12" fill="#D7DADF">mask</text>
|
||||
<text x="336" y="71.0" font-size="12" fill="#D7DADF" text-anchor="end">width</text>
|
||||
<circle cx="348" cy="67.0" r="5" fill="#9CC2E5" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="336" y="93.0" font-size="12" fill="#D7DADF" text-anchor="end">height</text>
|
||||
<circle cx="348" cy="89.0" r="5" fill="#9CC2E5" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="336" y="115.0" font-size="12" fill="#D7DADF" text-anchor="end">x1</text>
|
||||
<circle cx="348" cy="111.0" r="5" fill="#9CC2E5" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="336" y="137.0" font-size="12" fill="#D7DADF" text-anchor="end">y1</text>
|
||||
<circle cx="348" cy="133.0" r="5" fill="#9CC2E5" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="336" y="159.0" font-size="12" fill="#D7DADF" text-anchor="end">x2</text>
|
||||
<circle cx="348" cy="155.0" r="5" fill="#9CC2E5" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="336" y="181.0" font-size="12" fill="#D7DADF" text-anchor="end">y2</text>
|
||||
<circle cx="348" cy="177.0" r="5" fill="#9CC2E5" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="336" y="203.0" font-size="12" fill="#D7DADF" text-anchor="end">bbox</text>
|
||||
<circle cx="348" cy="199.0" r="5" fill="#E0B272" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.0 KiB |
@@ -0,0 +1,33 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="360" height="236" viewBox="0 0 360 236" font-family="Inter, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" role="img" aria-label="NH Mask-Aware Resize node">
|
||||
<rect x="0.5" y="0.5" width="359" height="235" rx="10" fill="#26282c" stroke="#45484f"/>
|
||||
<path d="M0.5 10.5 a10 10 0 0 1 10 -10 h339 a10 10 0 0 1 10 10 v36 h-359 z" fill="#34373d"/>
|
||||
<circle cx="17" cy="17" r="4.5" fill="#8FD18F"/>
|
||||
<text x="28" y="22" font-size="14" font-weight="600" fill="#EEF0F3">NH Mask-Aware Resize</text>
|
||||
<text x="28" y="38" font-size="10.5" fill="#9AA0A8" font-family="'JetBrains Mono', Consolas, 'Courier New', monospace">NH_MaskResizeImage · Image</text>
|
||||
<circle cx="12" cy="67.0" r="5" fill="#64B5F6" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="24" y="71.0" font-size="12" fill="#D7DADF">image</text>
|
||||
<text x="336" y="71.0" font-size="12" fill="#D7DADF" text-anchor="end">image</text>
|
||||
<circle cx="348" cy="67.0" r="5" fill="#64B5F6" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<circle cx="12" cy="89.0" r="5" fill="#81C784" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="24" y="93.0" font-size="12" fill="#D7DADF">mask</text>
|
||||
<text x="336" y="93.0" font-size="12" fill="#D7DADF" text-anchor="end">mask</text>
|
||||
<circle cx="348" cy="89.0" r="5" fill="#81C784" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<rect x="12" y="108" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="20" y="124" font-size="10" fill="#6f747c">◀</text>
|
||||
<text x="332" y="124" font-size="10" fill="#6f747c">▶</text>
|
||||
<text x="34" y="124" font-size="11.5" fill="#9AA0A8">width</text>
|
||||
<text x="326" y="124" font-size="11.5" fill="#E6E8EB" text-anchor="end">512</text>
|
||||
<rect x="12" y="137" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="20" y="153" font-size="10" fill="#6f747c">◀</text>
|
||||
<text x="332" y="153" font-size="10" fill="#6f747c">▶</text>
|
||||
<text x="34" y="153" font-size="11.5" fill="#9AA0A8">height</text>
|
||||
<text x="326" y="153" font-size="11.5" fill="#E6E8EB" text-anchor="end">512</text>
|
||||
<rect x="12" y="166" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="20" y="182" font-size="10" fill="#6f747c">◀</text>
|
||||
<text x="332" y="182" font-size="10" fill="#6f747c">▶</text>
|
||||
<text x="34" y="182" font-size="11.5" fill="#9AA0A8">mode</text>
|
||||
<text x="326" y="182" font-size="11.5" fill="#E6E8EB" text-anchor="end">crop</text>
|
||||
<rect x="12" y="195" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="22" y="211" font-size="11.5" fill="#9AA0A8">pad_color_hex</text>
|
||||
<text x="338" y="211" font-size="11.5" fill="#E6E8EB" text-anchor="end">#000000</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.6 KiB |
@@ -0,0 +1,11 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="360" height="90" viewBox="0 0 360 90" font-family="Inter, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" role="img" aria-label="NH Create Box Mask node">
|
||||
<rect x="0.5" y="0.5" width="359" height="89" rx="10" fill="#26282c" stroke="#45484f"/>
|
||||
<path d="M0.5 10.5 a10 10 0 0 1 10 -10 h339 a10 10 0 0 1 10 10 v36 h-359 z" fill="#34373d"/>
|
||||
<circle cx="17" cy="17" r="4.5" fill="#8FD18F"/>
|
||||
<text x="28" y="22" font-size="14" font-weight="600" fill="#EEF0F3">NH Create Box Mask</text>
|
||||
<text x="28" y="38" font-size="10.5" fill="#9AA0A8" font-family="'JetBrains Mono', Consolas, 'Courier New', monospace">NH_MaskToBBox · Mask</text>
|
||||
<circle cx="12" cy="67.0" r="5" fill="#81C784" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="24" y="71.0" font-size="12" fill="#D7DADF">mask</text>
|
||||
<text x="336" y="71.0" font-size="12" fill="#D7DADF" text-anchor="end">box_mask</text>
|
||||
<circle cx="348" cy="67.0" r="5" fill="#81C784" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1005 B |
@@ -0,0 +1,41 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="360" height="316" viewBox="0 0 360 316" font-family="Inter, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" role="img" aria-label="Math Eval (NH) node">
|
||||
<rect x="0.5" y="0.5" width="359" height="315" rx="10" fill="#26282c" stroke="#45484f"/>
|
||||
<path d="M0.5 10.5 a10 10 0 0 1 10 -10 h339 a10 10 0 0 1 10 10 v36 h-359 z" fill="#34373d"/>
|
||||
<circle cx="17" cy="17" r="4.5" fill="#8FD18F"/>
|
||||
<text x="28" y="22" font-size="14" font-weight="600" fill="#EEF0F3">Math Eval (NH)</text>
|
||||
<text x="28" y="38" font-size="10.5" fill="#9AA0A8" font-family="'JetBrains Mono', Consolas, 'Courier New', monospace">NH_MathEval · Logic</text>
|
||||
<text x="336" y="71.0" font-size="12" fill="#D7DADF" text-anchor="end">result_float</text>
|
||||
<circle cx="348" cy="67.0" r="5" fill="#9CC2E5" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="336" y="93.0" font-size="12" fill="#D7DADF" text-anchor="end">result_int</text>
|
||||
<circle cx="348" cy="89.0" r="5" fill="#9CC2E5" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="336" y="115.0" font-size="12" fill="#D7DADF" text-anchor="end">result_string</text>
|
||||
<circle cx="348" cy="111.0" r="5" fill="#C5E1A5" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<rect x="12" y="130" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="22" y="146" font-size="11.5" fill="#9AA0A8">expression</text>
|
||||
<text x="338" y="146" font-size="11.5" fill="#E6E8EB" text-anchor="end">a + b</text>
|
||||
<rect x="12" y="159" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="20" y="175" font-size="10" fill="#6f747c">◀</text>
|
||||
<text x="332" y="175" font-size="10" fill="#6f747c">▶</text>
|
||||
<text x="34" y="175" font-size="11.5" fill="#9AA0A8">a <tspan fill="#6f747c" font-style="italic">opt</tspan></text>
|
||||
<text x="326" y="175" font-size="11.5" fill="#E6E8EB" text-anchor="end">0.00</text>
|
||||
<rect x="12" y="188" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="20" y="204" font-size="10" fill="#6f747c">◀</text>
|
||||
<text x="332" y="204" font-size="10" fill="#6f747c">▶</text>
|
||||
<text x="34" y="204" font-size="11.5" fill="#9AA0A8">b <tspan fill="#6f747c" font-style="italic">opt</tspan></text>
|
||||
<text x="326" y="204" font-size="11.5" fill="#E6E8EB" text-anchor="end">0.00</text>
|
||||
<rect x="12" y="217" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="20" y="233" font-size="10" fill="#6f747c">◀</text>
|
||||
<text x="332" y="233" font-size="10" fill="#6f747c">▶</text>
|
||||
<text x="34" y="233" font-size="11.5" fill="#9AA0A8">c <tspan fill="#6f747c" font-style="italic">opt</tspan></text>
|
||||
<text x="326" y="233" font-size="11.5" fill="#E6E8EB" text-anchor="end">0.00</text>
|
||||
<rect x="12" y="246" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="20" y="262" font-size="10" fill="#6f747c">◀</text>
|
||||
<text x="332" y="262" font-size="10" fill="#6f747c">▶</text>
|
||||
<text x="34" y="262" font-size="11.5" fill="#9AA0A8">d <tspan fill="#6f747c" font-style="italic">opt</tspan></text>
|
||||
<text x="326" y="262" font-size="11.5" fill="#E6E8EB" text-anchor="end">0.00</text>
|
||||
<rect x="12" y="275" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="20" y="291" font-size="10" fill="#6f747c">◀</text>
|
||||
<text x="332" y="291" font-size="10" fill="#6f747c">▶</text>
|
||||
<text x="34" y="291" font-size="11.5" fill="#9AA0A8">round_to <tspan fill="#6f747c" font-style="italic">opt</tspan></text>
|
||||
<text x="326" y="291" font-size="11.5" fill="#E6E8EB" text-anchor="end">-1</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.4 KiB |
@@ -0,0 +1,29 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="360" height="273" viewBox="0 0 360 273" font-family="Inter, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" role="img" aria-label="Multi-Slider (FLOAT) node">
|
||||
<rect x="0.5" y="0.5" width="359" height="272" rx="10" fill="#26282c" stroke="#45484f"/>
|
||||
<path d="M0.5 10.5 a10 10 0 0 1 10 -10 h339 a10 10 0 0 1 10 10 v36 h-359 z" fill="#34373d"/>
|
||||
<circle cx="17" cy="17" r="4.5" fill="#8FD18F"/>
|
||||
<text x="28" y="22" font-size="14" font-weight="600" fill="#EEF0F3">Multi-Slider (FLOAT)</text>
|
||||
<text x="28" y="38" font-size="10.5" fill="#9AA0A8" font-family="'JetBrains Mono', Consolas, 'Courier New', monospace">NH_MultiSliderFloat · Utils</text>
|
||||
<text x="336" y="71.0" font-size="12" fill="#D7DADF" text-anchor="end">Float 1</text>
|
||||
<circle cx="348" cy="67.0" r="5" fill="#9CC2E5" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="336" y="93.0" font-size="12" fill="#D7DADF" text-anchor="end">Float 2</text>
|
||||
<circle cx="348" cy="89.0" r="5" fill="#9CC2E5" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="336" y="115.0" font-size="12" fill="#D7DADF" text-anchor="end">Float 3</text>
|
||||
<circle cx="348" cy="111.0" r="5" fill="#9CC2E5" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="336" y="137.0" font-size="12" fill="#D7DADF" text-anchor="end">Float 4</text>
|
||||
<circle cx="348" cy="133.0" r="5" fill="#9CC2E5" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="336" y="159.0" font-size="12" fill="#D7DADF" text-anchor="end">Float 5</text>
|
||||
<circle cx="348" cy="155.0" r="5" fill="#9CC2E5" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<rect x="12" y="174" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="20" y="190" font-size="10" fill="#6f747c">◀</text>
|
||||
<text x="332" y="190" font-size="10" fill="#6f747c">▶</text>
|
||||
<text x="34" y="190" font-size="11.5" fill="#9AA0A8">float_1</text>
|
||||
<text x="326" y="190" font-size="11.5" fill="#E6E8EB" text-anchor="end">0.00</text>
|
||||
<rect x="12" y="203" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="20" y="219" font-size="10" fill="#6f747c">◀</text>
|
||||
<text x="332" y="219" font-size="10" fill="#6f747c">▶</text>
|
||||
<text x="34" y="219" font-size="11.5" fill="#9AA0A8">float_2</text>
|
||||
<text x="326" y="219" font-size="11.5" fill="#E6E8EB" text-anchor="end">0.00</text>
|
||||
<rect x="12" y="232" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="180.0" y="248" font-size="11" fill="#9AA0A8" text-anchor="middle" font-style="italic">float_3 … float_5</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.4 KiB |
@@ -0,0 +1,29 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="360" height="273" viewBox="0 0 360 273" font-family="Inter, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" role="img" aria-label="Multi-Slider (INT) node">
|
||||
<rect x="0.5" y="0.5" width="359" height="272" rx="10" fill="#26282c" stroke="#45484f"/>
|
||||
<path d="M0.5 10.5 a10 10 0 0 1 10 -10 h339 a10 10 0 0 1 10 10 v36 h-359 z" fill="#34373d"/>
|
||||
<circle cx="17" cy="17" r="4.5" fill="#8FD18F"/>
|
||||
<text x="28" y="22" font-size="14" font-weight="600" fill="#EEF0F3">Multi-Slider (INT)</text>
|
||||
<text x="28" y="38" font-size="10.5" fill="#9AA0A8" font-family="'JetBrains Mono', Consolas, 'Courier New', monospace">NH_MultiSliderInt · Utils</text>
|
||||
<text x="336" y="71.0" font-size="12" fill="#D7DADF" text-anchor="end">Int 1</text>
|
||||
<circle cx="348" cy="67.0" r="5" fill="#9CC2E5" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="336" y="93.0" font-size="12" fill="#D7DADF" text-anchor="end">Int 2</text>
|
||||
<circle cx="348" cy="89.0" r="5" fill="#9CC2E5" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="336" y="115.0" font-size="12" fill="#D7DADF" text-anchor="end">Int 3</text>
|
||||
<circle cx="348" cy="111.0" r="5" fill="#9CC2E5" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="336" y="137.0" font-size="12" fill="#D7DADF" text-anchor="end">Int 4</text>
|
||||
<circle cx="348" cy="133.0" r="5" fill="#9CC2E5" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="336" y="159.0" font-size="12" fill="#D7DADF" text-anchor="end">Int 5</text>
|
||||
<circle cx="348" cy="155.0" r="5" fill="#9CC2E5" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<rect x="12" y="174" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="20" y="190" font-size="10" fill="#6f747c">◀</text>
|
||||
<text x="332" y="190" font-size="10" fill="#6f747c">▶</text>
|
||||
<text x="34" y="190" font-size="11.5" fill="#9AA0A8">int_1</text>
|
||||
<text x="326" y="190" font-size="11.5" fill="#E6E8EB" text-anchor="end">0</text>
|
||||
<rect x="12" y="203" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="20" y="219" font-size="10" fill="#6f747c">◀</text>
|
||||
<text x="332" y="219" font-size="10" fill="#6f747c">▶</text>
|
||||
<text x="34" y="219" font-size="11.5" fill="#9AA0A8">int_2</text>
|
||||
<text x="326" y="219" font-size="11.5" fill="#E6E8EB" text-anchor="end">0</text>
|
||||
<rect x="12" y="232" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="180.0" y="248" font-size="11" fill="#9AA0A8" text-anchor="middle" font-style="italic">int_3 … int_5</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.4 KiB |
@@ -0,0 +1,17 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="360" height="171" viewBox="0 0 360 171" font-family="Inter, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" role="img" aria-label="Pack Universal (NH) node">
|
||||
<rect x="0.5" y="0.5" width="359" height="170" rx="10" fill="#26282c" stroke="#45484f"/>
|
||||
<path d="M0.5 10.5 a10 10 0 0 1 10 -10 h339 a10 10 0 0 1 10 10 v36 h-359 z" fill="#34373d"/>
|
||||
<circle cx="17" cy="17" r="4.5" fill="#8FD18F"/>
|
||||
<text x="28" y="22" font-size="14" font-weight="600" fill="#EEF0F3">Pack Universal (NH)</text>
|
||||
<text x="28" y="38" font-size="10.5" fill="#9AA0A8" font-family="'JetBrains Mono', Consolas, 'Courier New', monospace">NH_PackUniversal · Utils/Pipe</text>
|
||||
<circle cx="12" cy="67.0" r="5" fill="#CFD8DC" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="24" y="71.0" font-size="12" fill="#D7DADF">input_0 <tspan fill="#7d838c" font-style="italic">(optional)</tspan></text>
|
||||
<text x="336" y="71.0" font-size="12" fill="#D7DADF" text-anchor="end">package</text>
|
||||
<circle cx="348" cy="67.0" r="5" fill="#E0B272" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<circle cx="12" cy="89.0" r="5" fill="#CFD8DC" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="24" y="93.0" font-size="12" fill="#D7DADF">input_1 <tspan fill="#7d838c" font-style="italic">(optional)</tspan></text>
|
||||
<circle cx="12" cy="111.0" r="5" fill="#CFD8DC" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="24" y="115.0" font-size="12" fill="#D7DADF">input_2 <tspan fill="#7d838c" font-style="italic">(optional)</tspan></text>
|
||||
<rect x="12" y="130" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="180.0" y="146" font-size="11" fill="#9AA0A8" text-anchor="middle" font-style="italic">input_3 … input_9</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
@@ -0,0 +1,72 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="360" height="541" viewBox="0 0 360 541" font-family="Inter, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" role="img" aria-label="Portrait Segment (NH) node">
|
||||
<rect x="0.5" y="0.5" width="359" height="540" rx="10" fill="#26282c" stroke="#45484f"/>
|
||||
<path d="M0.5 10.5 a10 10 0 0 1 10 -10 h339 a10 10 0 0 1 10 10 v36 h-359 z" fill="#34373d"/>
|
||||
<circle cx="17" cy="17" r="4.5" fill="#8FD18F"/>
|
||||
<text x="28" y="22" font-size="14" font-weight="600" fill="#EEF0F3">Portrait Segment (NH)</text>
|
||||
<text x="28" y="38" font-size="10.5" fill="#9AA0A8" font-family="'JetBrains Mono', Consolas, 'Courier New', monospace">NH_PortraitSegment · Vision</text>
|
||||
<circle cx="12" cy="67.0" r="5" fill="#64B5F6" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="24" y="71.0" font-size="12" fill="#D7DADF">image</text>
|
||||
<text x="336" y="71.0" font-size="12" fill="#D7DADF" text-anchor="end">mask</text>
|
||||
<circle cx="348" cy="67.0" r="5" fill="#81C784" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="336" y="93.0" font-size="12" fill="#D7DADF" text-anchor="end">cutout_rgba</text>
|
||||
<circle cx="348" cy="89.0" r="5" fill="#64B5F6" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="336" y="115.0" font-size="12" fill="#D7DADF" text-anchor="end">mask_image</text>
|
||||
<circle cx="348" cy="111.0" r="5" fill="#64B5F6" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="336" y="137.0" font-size="12" fill="#D7DADF" text-anchor="end">segs</text>
|
||||
<circle cx="348" cy="133.0" r="5" fill="#E0B272" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<rect x="12" y="152" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="22" y="168" font-size="11.5" fill="#9AA0A8">face</text>
|
||||
<rect x="308" y="158" width="30" height="13" rx="6.5" fill="#4C8BF5"/>
|
||||
<circle cx="331" cy="164.5" r="5" fill="#F5F6F7"/>
|
||||
<rect x="12" y="181" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="22" y="197" font-size="11.5" fill="#9AA0A8">neck</text>
|
||||
<rect x="308" y="187" width="30" height="13" rx="6.5" fill="#4C8BF5"/>
|
||||
<circle cx="331" cy="193.5" r="5" fill="#F5F6F7"/>
|
||||
<rect x="12" y="210" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="22" y="226" font-size="11.5" fill="#9AA0A8">hair</text>
|
||||
<rect x="308" y="216" width="30" height="13" rx="6.5" fill="#4C8BF5"/>
|
||||
<circle cx="331" cy="222.5" r="5" fill="#F5F6F7"/>
|
||||
<rect x="12" y="239" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="22" y="255" font-size="11.5" fill="#9AA0A8">eyes</text>
|
||||
<rect x="308" y="245" width="30" height="13" rx="6.5" fill="#4C8BF5"/>
|
||||
<circle cx="331" cy="251.5" r="5" fill="#F5F6F7"/>
|
||||
<rect x="12" y="268" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="22" y="284" font-size="11.5" fill="#9AA0A8">lips</text>
|
||||
<rect x="308" y="274" width="30" height="13" rx="6.5" fill="#4C8BF5"/>
|
||||
<circle cx="331" cy="280.5" r="5" fill="#F5F6F7"/>
|
||||
<rect x="12" y="297" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="22" y="313" font-size="11.5" fill="#9AA0A8">nose</text>
|
||||
<rect x="308" y="303" width="30" height="13" rx="6.5" fill="#4C8BF5"/>
|
||||
<circle cx="331" cy="309.5" r="5" fill="#F5F6F7"/>
|
||||
<rect x="12" y="326" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="22" y="342" font-size="11.5" fill="#9AA0A8">eyebrows</text>
|
||||
<rect x="308" y="332" width="30" height="13" rx="6.5" fill="#4C8BF5"/>
|
||||
<circle cx="331" cy="338.5" r="5" fill="#F5F6F7"/>
|
||||
<rect x="12" y="355" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="22" y="371" font-size="11.5" fill="#9AA0A8">ears</text>
|
||||
<rect x="308" y="361" width="30" height="13" rx="6.5" fill="#4C8BF5"/>
|
||||
<circle cx="331" cy="367.5" r="5" fill="#F5F6F7"/>
|
||||
<rect x="12" y="384" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="22" y="400" font-size="11.5" fill="#9AA0A8">mouth_interior</text>
|
||||
<rect x="308" y="390" width="30" height="13" rx="6.5" fill="#4a4d53"/>
|
||||
<circle cx="315" cy="396.5" r="5" fill="#F5F6F7"/>
|
||||
<rect x="12" y="413" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="20" y="429" font-size="10" fill="#6f747c">◀</text>
|
||||
<text x="332" y="429" font-size="10" fill="#6f747c">▶</text>
|
||||
<text x="34" y="429" font-size="11.5" fill="#9AA0A8">mask_expand</text>
|
||||
<text x="326" y="429" font-size="11.5" fill="#E6E8EB" text-anchor="end">0</text>
|
||||
<rect x="12" y="442" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="20" y="458" font-size="10" fill="#6f747c">◀</text>
|
||||
<text x="332" y="458" font-size="10" fill="#6f747c">▶</text>
|
||||
<text x="34" y="458" font-size="11.5" fill="#9AA0A8">mask_blur</text>
|
||||
<text x="326" y="458" font-size="11.5" fill="#E6E8EB" text-anchor="end">0</text>
|
||||
<rect x="12" y="471" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="22" y="487" font-size="11.5" fill="#9AA0A8">fill_holes</text>
|
||||
<rect x="308" y="477" width="30" height="13" rx="6.5" fill="#4C8BF5"/>
|
||||
<circle cx="331" cy="483.5" r="5" fill="#F5F6F7"/>
|
||||
<rect x="12" y="500" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="20" y="516" font-size="10" fill="#6f747c">◀</text>
|
||||
<text x="332" y="516" font-size="10" fill="#6f747c">▶</text>
|
||||
<text x="34" y="516" font-size="11.5" fill="#9AA0A8">device</text>
|
||||
<text x="326" y="516" font-size="11.5" fill="#E6E8EB" text-anchor="end">cuda</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.3 KiB |
@@ -0,0 +1,24 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="360" height="207" viewBox="0 0 360 207" font-family="Inter, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" role="img" aria-label="Prompt Join (NH) node">
|
||||
<rect x="0.5" y="0.5" width="359" height="206" rx="10" fill="#26282c" stroke="#45484f"/>
|
||||
<path d="M0.5 10.5 a10 10 0 0 1 10 -10 h339 a10 10 0 0 1 10 10 v36 h-359 z" fill="#34373d"/>
|
||||
<circle cx="17" cy="17" r="4.5" fill="#8FD18F"/>
|
||||
<text x="28" y="22" font-size="14" font-weight="600" fill="#EEF0F3">Prompt Join (NH)</text>
|
||||
<text x="28" y="38" font-size="10.5" fill="#9AA0A8" font-family="'JetBrains Mono', Consolas, 'Courier New', monospace">NH_PromptJoin · Text</text>
|
||||
<circle cx="12" cy="67.0" r="5" fill="#C5E1A5" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="24" y="71.0" font-size="12" fill="#D7DADF">text_1 <tspan fill="#7d838c" font-style="italic">(optional)</tspan></text>
|
||||
<text x="336" y="71.0" font-size="12" fill="#D7DADF" text-anchor="end">result</text>
|
||||
<circle cx="348" cy="67.0" r="5" fill="#C5E1A5" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<circle cx="12" cy="89.0" r="5" fill="#C5E1A5" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="24" y="93.0" font-size="12" fill="#D7DADF">text_2 <tspan fill="#7d838c" font-style="italic">(optional)</tspan></text>
|
||||
<text x="336" y="93.0" font-size="12" fill="#D7DADF" text-anchor="end">count</text>
|
||||
<circle cx="348" cy="89.0" r="5" fill="#9CC2E5" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<rect x="12" y="108" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="22" y="124" font-size="11.5" fill="#9AA0A8">separator</text>
|
||||
<text x="338" y="124" font-size="11.5" fill="#E6E8EB" text-anchor="end">, </text>
|
||||
<rect x="12" y="137" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="22" y="153" font-size="11.5" fill="#9AA0A8">skip_empty</text>
|
||||
<rect x="308" y="143" width="30" height="13" rx="6.5" fill="#4C8BF5"/>
|
||||
<circle cx="331" cy="149.5" r="5" fill="#F5F6F7"/>
|
||||
<rect x="12" y="166" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="180.0" y="182" font-size="11" fill="#9AA0A8" text-anchor="middle" font-style="italic">text_3 … text_5</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.1 KiB |
@@ -0,0 +1,36 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="360" height="309" viewBox="0 0 360 309" font-family="Inter, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" role="img" aria-label="Prompt Scheduler (NH) node">
|
||||
<rect x="0.5" y="0.5" width="359" height="308" rx="10" fill="#26282c" stroke="#45484f"/>
|
||||
<path d="M0.5 10.5 a10 10 0 0 1 10 -10 h339 a10 10 0 0 1 10 10 v36 h-359 z" fill="#34373d"/>
|
||||
<circle cx="17" cy="17" r="4.5" fill="#8FD18F"/>
|
||||
<text x="28" y="22" font-size="14" font-weight="600" fill="#EEF0F3">Prompt Scheduler (NH)</text>
|
||||
<text x="28" y="38" font-size="10.5" fill="#9AA0A8" font-family="'JetBrains Mono', Consolas, 'Courier New', monospace">NH_PromptScheduler · Text</text>
|
||||
<text x="336" y="71.0" font-size="12" fill="#D7DADF" text-anchor="end">current_prompt</text>
|
||||
<circle cx="348" cy="67.0" r="5" fill="#C5E1A5" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="336" y="93.0" font-size="12" fill="#D7DADF" text-anchor="end">progress</text>
|
||||
<circle cx="348" cy="89.0" r="5" fill="#9CC2E5" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="336" y="115.0" font-size="12" fill="#D7DADF" text-anchor="end">step_index</text>
|
||||
<circle cx="348" cy="111.0" r="5" fill="#9CC2E5" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<rect x="12" y="130" width="336" height="46" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="22" y="146" font-size="11.5" fill="#9AA0A8">prompts</text>
|
||||
<text x="22" y="166" font-size="11.5" fill="#E6E8EB" font-family="'JetBrains Mono', Consolas, 'Courier New', monospace">prompt 1 ⏎ prompt 2 ⏎ prompt 3</text>
|
||||
<rect x="12" y="181" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="20" y="197" font-size="10" fill="#6f747c">◀</text>
|
||||
<text x="332" y="197" font-size="10" fill="#6f747c">▶</text>
|
||||
<text x="34" y="197" font-size="11.5" fill="#9AA0A8">current_step</text>
|
||||
<text x="326" y="197" font-size="11.5" fill="#E6E8EB" text-anchor="end">0</text>
|
||||
<rect x="12" y="210" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="20" y="226" font-size="10" fill="#6f747c">◀</text>
|
||||
<text x="332" y="226" font-size="10" fill="#6f747c">▶</text>
|
||||
<text x="34" y="226" font-size="11.5" fill="#9AA0A8">total_steps</text>
|
||||
<text x="326" y="226" font-size="11.5" fill="#E6E8EB" text-anchor="end">10</text>
|
||||
<rect x="12" y="239" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="20" y="255" font-size="10" fill="#6f747c">◀</text>
|
||||
<text x="332" y="255" font-size="10" fill="#6f747c">▶</text>
|
||||
<text x="34" y="255" font-size="11.5" fill="#9AA0A8">mode</text>
|
||||
<text x="326" y="255" font-size="11.5" fill="#E6E8EB" text-anchor="end">sequential</text>
|
||||
<rect x="12" y="268" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="20" y="284" font-size="10" fill="#6f747c">◀</text>
|
||||
<text x="332" y="284" font-size="10" fill="#6f747c">▶</text>
|
||||
<text x="34" y="284" font-size="11.5" fill="#9AA0A8">seed <tspan fill="#6f747c" font-style="italic">opt</tspan></text>
|
||||
<text x="326" y="284" font-size="11.5" fill="#E6E8EB" text-anchor="end">0</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.0 KiB |
@@ -0,0 +1,23 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="360" height="229" viewBox="0 0 360 229" font-family="Inter, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" role="img" aria-label="Prompt Template (NH) node">
|
||||
<rect x="0.5" y="0.5" width="359" height="228" rx="10" fill="#26282c" stroke="#45484f"/>
|
||||
<path d="M0.5 10.5 a10 10 0 0 1 10 -10 h339 a10 10 0 0 1 10 10 v36 h-359 z" fill="#34373d"/>
|
||||
<circle cx="17" cy="17" r="4.5" fill="#8FD18F"/>
|
||||
<text x="28" y="22" font-size="14" font-weight="600" fill="#EEF0F3">Prompt Template (NH)</text>
|
||||
<text x="28" y="38" font-size="10.5" fill="#9AA0A8" font-family="'JetBrains Mono', Consolas, 'Courier New', monospace">NH_PromptTemplate · Text</text>
|
||||
<circle cx="12" cy="67.0" r="5" fill="#C5E1A5" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="24" y="71.0" font-size="12" fill="#D7DADF">var1 <tspan fill="#7d838c" font-style="italic">(optional)</tspan></text>
|
||||
<text x="336" y="71.0" font-size="12" fill="#D7DADF" text-anchor="end">result</text>
|
||||
<circle cx="348" cy="67.0" r="5" fill="#C5E1A5" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<circle cx="12" cy="89.0" r="5" fill="#C5E1A5" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="24" y="93.0" font-size="12" fill="#D7DADF">var2 <tspan fill="#7d838c" font-style="italic">(optional)</tspan></text>
|
||||
<text x="336" y="93.0" font-size="12" fill="#D7DADF" text-anchor="end">missing_vars</text>
|
||||
<circle cx="348" cy="89.0" r="5" fill="#C5E1A5" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<rect x="12" y="108" width="336" height="46" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="22" y="124" font-size="11.5" fill="#9AA0A8">template</text>
|
||||
<text x="22" y="144" font-size="11.5" fill="#E6E8EB" font-family="'JetBrains Mono', Consolas, 'Courier New', monospace">a {color} {garment}, {style},…</text>
|
||||
<rect x="12" y="159" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="22" y="175" font-size="11.5" fill="#9AA0A8">var_names</text>
|
||||
<text x="338" y="175" font-size="11.5" fill="#E6E8EB" text-anchor="end">color,garment,style</text>
|
||||
<rect x="12" y="188" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="180.0" y="204" font-size="11" fill="#9AA0A8" text-anchor="middle" font-style="italic">var3 … var6</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.2 KiB |
@@ -0,0 +1,29 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="360" height="229" viewBox="0 0 360 229" font-family="Inter, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" role="img" aria-label="Random Choice (NH) node">
|
||||
<rect x="0.5" y="0.5" width="359" height="228" rx="10" fill="#26282c" stroke="#45484f"/>
|
||||
<path d="M0.5 10.5 a10 10 0 0 1 10 -10 h339 a10 10 0 0 1 10 10 v36 h-359 z" fill="#34373d"/>
|
||||
<circle cx="17" cy="17" r="4.5" fill="#8FD18F"/>
|
||||
<text x="28" y="22" font-size="14" font-weight="600" fill="#EEF0F3">Random Choice (NH)</text>
|
||||
<text x="28" y="38" font-size="10.5" fill="#9AA0A8" font-family="'JetBrains Mono', Consolas, 'Courier New', monospace">NH_RandomChoice · Logic</text>
|
||||
<circle cx="12" cy="67.0" r="5" fill="#CFD8DC" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="24" y="71.0" font-size="12" fill="#D7DADF">input_0 <tspan fill="#7d838c" font-style="italic">(optional)</tspan></text>
|
||||
<text x="336" y="71.0" font-size="12" fill="#D7DADF" text-anchor="end">result</text>
|
||||
<circle cx="348" cy="67.0" r="5" fill="#CFD8DC" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<circle cx="12" cy="89.0" r="5" fill="#CFD8DC" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="24" y="93.0" font-size="12" fill="#D7DADF">input_1 <tspan fill="#7d838c" font-style="italic">(optional)</tspan></text>
|
||||
<text x="336" y="93.0" font-size="12" fill="#D7DADF" text-anchor="end">picked_index</text>
|
||||
<circle cx="348" cy="89.0" r="5" fill="#9CC2E5" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<circle cx="12" cy="111.0" r="5" fill="#CFD8DC" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="24" y="115.0" font-size="12" fill="#D7DADF">input_2 <tspan fill="#7d838c" font-style="italic">(optional)</tspan></text>
|
||||
<text x="336" y="115.0" font-size="12" fill="#D7DADF" text-anchor="end">probabilities</text>
|
||||
<circle cx="348" cy="111.0" r="5" fill="#C5E1A5" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<rect x="12" y="130" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="20" y="146" font-size="10" fill="#6f747c">◀</text>
|
||||
<text x="332" y="146" font-size="10" fill="#6f747c">▶</text>
|
||||
<text x="34" y="146" font-size="11.5" fill="#9AA0A8">seed</text>
|
||||
<text x="326" y="146" font-size="11.5" fill="#E6E8EB" text-anchor="end">0</text>
|
||||
<rect x="12" y="159" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="22" y="175" font-size="11.5" fill="#9AA0A8">weights</text>
|
||||
<text x="338" y="175" font-size="11.5" fill="#E6E8EB" text-anchor="end">1,1,1,1,1,1</text>
|
||||
<rect x="12" y="188" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="180.0" y="204" font-size="11" fill="#9AA0A8" text-anchor="middle" font-style="italic">input_3 … input_5</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.6 KiB |
@@ -0,0 +1,53 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="360" height="426" viewBox="0 0 360 426" font-family="Inter, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" role="img" aria-label="NH Ratio Preset Image Resize node">
|
||||
<rect x="0.5" y="0.5" width="359" height="425" rx="10" fill="#26282c" stroke="#45484f"/>
|
||||
<path d="M0.5 10.5 a10 10 0 0 1 10 -10 h339 a10 10 0 0 1 10 10 v36 h-359 z" fill="#34373d"/>
|
||||
<circle cx="17" cy="17" r="4.5" fill="#8FD18F"/>
|
||||
<text x="28" y="22" font-size="14" font-weight="600" fill="#EEF0F3">NH Ratio Preset Image Resize</text>
|
||||
<text x="28" y="38" font-size="10.5" fill="#9AA0A8" font-family="'JetBrains Mono', Consolas, 'Courier New', monospace">NH_RatioPresetImageResize · Resolution</text>
|
||||
<circle cx="12" cy="67.0" r="5" fill="#64B5F6" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="24" y="71.0" font-size="12" fill="#D7DADF">image</text>
|
||||
<text x="336" y="71.0" font-size="12" fill="#D7DADF" text-anchor="end">image</text>
|
||||
<circle cx="348" cy="67.0" r="5" fill="#64B5F6" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="336" y="93.0" font-size="12" fill="#D7DADF" text-anchor="end">width</text>
|
||||
<circle cx="348" cy="89.0" r="5" fill="#9CC2E5" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="336" y="115.0" font-size="12" fill="#D7DADF" text-anchor="end">height</text>
|
||||
<circle cx="348" cy="111.0" r="5" fill="#9CC2E5" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="336" y="137.0" font-size="12" fill="#D7DADF" text-anchor="end">info</text>
|
||||
<circle cx="348" cy="133.0" r="5" fill="#C5E1A5" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="336" y="159.0" font-size="12" fill="#D7DADF" text-anchor="end">source_aspect_ratio</text>
|
||||
<circle cx="348" cy="155.0" r="5" fill="#9CC2E5" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="336" y="181.0" font-size="12" fill="#D7DADF" text-anchor="end">target_aspect_ratio</text>
|
||||
<circle cx="348" cy="177.0" r="5" fill="#9CC2E5" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="336" y="203.0" font-size="12" fill="#D7DADF" text-anchor="end">selected_preset</text>
|
||||
<circle cx="348" cy="199.0" r="5" fill="#C5E1A5" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="336" y="225.0" font-size="12" fill="#D7DADF" text-anchor="end">resize_data</text>
|
||||
<circle cx="348" cy="221.0" r="5" fill="#E0B272" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<rect x="12" y="240" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="20" y="256" font-size="10" fill="#6f747c">◀</text>
|
||||
<text x="332" y="256" font-size="10" fill="#6f747c">▶</text>
|
||||
<text x="34" y="256" font-size="11.5" fill="#9AA0A8">model_family</text>
|
||||
<text x="326" y="256" font-size="11.5" fill="#E6E8EB" text-anchor="end">FLUX.2 Klein 9B</text>
|
||||
<rect x="12" y="269" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="20" y="285" font-size="10" fill="#6f747c">◀</text>
|
||||
<text x="332" y="285" font-size="10" fill="#6f747c">▶</text>
|
||||
<text x="34" y="285" font-size="11.5" fill="#9AA0A8">resolution_level</text>
|
||||
<text x="326" y="285" font-size="11.5" fill="#E6E8EB" text-anchor="end">1 MP</text>
|
||||
<rect x="12" y="298" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="20" y="314" font-size="10" fill="#6f747c">◀</text>
|
||||
<text x="332" y="314" font-size="10" fill="#6f747c">▶</text>
|
||||
<text x="34" y="314" font-size="11.5" fill="#9AA0A8">target_ratio</text>
|
||||
<text x="326" y="314" font-size="11.5" fill="#E6E8EB" text-anchor="end">1:1</text>
|
||||
<rect x="12" y="327" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="20" y="343" font-size="10" fill="#6f747c">◀</text>
|
||||
<text x="332" y="343" font-size="10" fill="#6f747c">▶</text>
|
||||
<text x="34" y="343" font-size="11.5" fill="#9AA0A8">resize_mode</text>
|
||||
<text x="326" y="343" font-size="11.5" fill="#E6E8EB" text-anchor="end">crop</text>
|
||||
<rect x="12" y="356" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="20" y="372" font-size="10" fill="#6f747c">◀</text>
|
||||
<text x="332" y="372" font-size="10" fill="#6f747c">▶</text>
|
||||
<text x="34" y="372" font-size="11.5" fill="#9AA0A8">resize_method</text>
|
||||
<text x="326" y="372" font-size="11.5" fill="#E6E8EB" text-anchor="end">lanczos</text>
|
||||
<rect x="12" y="385" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="22" y="401" font-size="11.5" fill="#9AA0A8">pad_color_hex</text>
|
||||
<text x="338" y="401" font-size="11.5" fill="#E6E8EB" text-anchor="end">#FFFFFF</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.3 KiB |
@@ -0,0 +1,29 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="360" height="302" viewBox="0 0 360 302" font-family="Inter, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" role="img" aria-label="Regex Extract (NH) node">
|
||||
<rect x="0.5" y="0.5" width="359" height="301" rx="10" fill="#26282c" stroke="#45484f"/>
|
||||
<path d="M0.5 10.5 a10 10 0 0 1 10 -10 h339 a10 10 0 0 1 10 10 v36 h-359 z" fill="#34373d"/>
|
||||
<circle cx="17" cy="17" r="4.5" fill="#8FD18F"/>
|
||||
<text x="28" y="22" font-size="14" font-weight="600" fill="#EEF0F3">Regex Extract (NH)</text>
|
||||
<text x="28" y="38" font-size="10.5" fill="#9AA0A8" font-family="'JetBrains Mono', Consolas, 'Courier New', monospace">NH_RegexExtract · Text</text>
|
||||
<text x="336" y="71.0" font-size="12" fill="#D7DADF" text-anchor="end">result</text>
|
||||
<circle cx="348" cy="67.0" r="5" fill="#C5E1A5" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="336" y="93.0" font-size="12" fill="#D7DADF" text-anchor="end">matched</text>
|
||||
<circle cx="348" cy="89.0" r="5" fill="#F48FB1" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="336" y="115.0" font-size="12" fill="#D7DADF" text-anchor="end">groups</text>
|
||||
<circle cx="348" cy="111.0" r="5" fill="#C5E1A5" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="336" y="137.0" font-size="12" fill="#D7DADF" text-anchor="end">count</text>
|
||||
<circle cx="348" cy="133.0" r="5" fill="#9CC2E5" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<rect x="12" y="152" width="336" height="46" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="22" y="168" font-size="11.5" fill="#9AA0A8">text</text>
|
||||
<text x="22" y="188" font-size="11.5" fill="#E6E8EB" font-family="'JetBrains Mono', Consolas, 'Courier New', monospace">…</text>
|
||||
<rect x="12" y="203" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="22" y="219" font-size="11.5" fill="#9AA0A8">pattern</text>
|
||||
<text x="338" y="219" font-size="11.5" fill="#E6E8EB" text-anchor="end">(\d+)</text>
|
||||
<rect x="12" y="232" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="20" y="248" font-size="10" fill="#6f747c">◀</text>
|
||||
<text x="332" y="248" font-size="10" fill="#6f747c">▶</text>
|
||||
<text x="34" y="248" font-size="11.5" fill="#9AA0A8">mode</text>
|
||||
<text x="326" y="248" font-size="11.5" fill="#E6E8EB" text-anchor="end">match</text>
|
||||
<rect x="12" y="261" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="22" y="277" font-size="11.5" fill="#9AA0A8">replacement <tspan fill="#6f747c" font-style="italic">opt</tspan></text>
|
||||
<text x="338" y="277" font-size="11.5" fill="#E6E8EB" text-anchor="end"></text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.5 KiB |
@@ -0,0 +1,29 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="360" height="222" viewBox="0 0 360 222" font-family="Inter, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" role="img" aria-label="NH Remove Resize Padding node">
|
||||
<rect x="0.5" y="0.5" width="359" height="221" rx="10" fill="#26282c" stroke="#45484f"/>
|
||||
<path d="M0.5 10.5 a10 10 0 0 1 10 -10 h339 a10 10 0 0 1 10 10 v36 h-359 z" fill="#34373d"/>
|
||||
<circle cx="17" cy="17" r="4.5" fill="#8FD18F"/>
|
||||
<text x="28" y="22" font-size="14" font-weight="600" fill="#EEF0F3">NH Remove Resize Padding</text>
|
||||
<text x="28" y="38" font-size="10.5" fill="#9AA0A8" font-family="'JetBrains Mono', Consolas, 'Courier New', monospace">NH_RemoveResizePadding · Resolution</text>
|
||||
<circle cx="12" cy="67.0" r="5" fill="#64B5F6" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="24" y="71.0" font-size="12" fill="#D7DADF">image</text>
|
||||
<text x="336" y="71.0" font-size="12" fill="#D7DADF" text-anchor="end">image</text>
|
||||
<circle cx="348" cy="67.0" r="5" fill="#64B5F6" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<circle cx="12" cy="89.0" r="5" fill="#E0B272" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="24" y="93.0" font-size="12" fill="#D7DADF">resize_data</text>
|
||||
<text x="336" y="93.0" font-size="12" fill="#D7DADF" text-anchor="end">width</text>
|
||||
<circle cx="348" cy="89.0" r="5" fill="#9CC2E5" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="336" y="115.0" font-size="12" fill="#D7DADF" text-anchor="end">height</text>
|
||||
<circle cx="348" cy="111.0" r="5" fill="#9CC2E5" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="336" y="137.0" font-size="12" fill="#D7DADF" text-anchor="end">info</text>
|
||||
<circle cx="348" cy="133.0" r="5" fill="#C5E1A5" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<rect x="12" y="152" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="20" y="168" font-size="10" fill="#6f747c">◀</text>
|
||||
<text x="332" y="168" font-size="10" fill="#6f747c">▶</text>
|
||||
<text x="34" y="168" font-size="11.5" fill="#9AA0A8">output_size</text>
|
||||
<text x="326" y="168" font-size="11.5" fill="#E6E8EB" text-anchor="end">content_region</text>
|
||||
<rect x="12" y="181" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="20" y="197" font-size="10" fill="#6f747c">◀</text>
|
||||
<text x="332" y="197" font-size="10" fill="#6f747c">▶</text>
|
||||
<text x="34" y="197" font-size="11.5" fill="#9AA0A8">resize_method</text>
|
||||
<text x="326" y="197" font-size="11.5" fill="#E6E8EB" text-anchor="end">lanczos</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.4 KiB |
@@ -0,0 +1,22 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="360" height="171" viewBox="0 0 360 171" font-family="Inter, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" role="img" aria-label="NH Restore Image Geometry node">
|
||||
<rect x="0.5" y="0.5" width="359" height="170" rx="10" fill="#26282c" stroke="#45484f"/>
|
||||
<path d="M0.5 10.5 a10 10 0 0 1 10 -10 h339 a10 10 0 0 1 10 10 v36 h-359 z" fill="#34373d"/>
|
||||
<circle cx="17" cy="17" r="4.5" fill="#8FD18F"/>
|
||||
<text x="28" y="22" font-size="14" font-weight="600" fill="#EEF0F3">NH Restore Image Geometry</text>
|
||||
<text x="28" y="38" font-size="10.5" fill="#9AA0A8" font-family="'JetBrains Mono', Consolas, 'Courier New', monospace">NH_RestoreImageGeometry · Resolution</text>
|
||||
<circle cx="12" cy="67.0" r="5" fill="#64B5F6" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="24" y="71.0" font-size="12" fill="#D7DADF">image</text>
|
||||
<text x="336" y="71.0" font-size="12" fill="#D7DADF" text-anchor="end">image</text>
|
||||
<circle cx="348" cy="67.0" r="5" fill="#64B5F6" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<circle cx="12" cy="89.0" r="5" fill="#64B5F6" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="24" y="93.0" font-size="12" fill="#D7DADF">original_image</text>
|
||||
<text x="336" y="93.0" font-size="12" fill="#D7DADF" text-anchor="end">mask</text>
|
||||
<circle cx="348" cy="89.0" r="5" fill="#81C784" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<circle cx="12" cy="111.0" r="5" fill="#E0B272" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="24" y="115.0" font-size="12" fill="#D7DADF">resize_data</text>
|
||||
<rect x="12" y="130" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="20" y="146" font-size="10" fill="#6f747c">◀</text>
|
||||
<text x="332" y="146" font-size="10" fill="#6f747c">▶</text>
|
||||
<text x="34" y="146" font-size="11.5" fill="#9AA0A8">resize_method</text>
|
||||
<text x="326" y="146" font-size="11.5" fill="#E6E8EB" text-anchor="end">bicubic</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
@@ -0,0 +1,48 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="360" height="389" viewBox="0 0 360 389" font-family="Inter, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" role="img" aria-label="SAM3 Segment (NH) node">
|
||||
<rect x="0.5" y="0.5" width="359" height="388" rx="10" fill="#26282c" stroke="#45484f"/>
|
||||
<path d="M0.5 10.5 a10 10 0 0 1 10 -10 h339 a10 10 0 0 1 10 10 v36 h-359 z" fill="#34373d"/>
|
||||
<circle cx="17" cy="17" r="4.5" fill="#F6AD55"/>
|
||||
<text x="28" y="22" font-size="14" font-weight="600" fill="#EEF0F3">SAM3 Segment (NH)</text>
|
||||
<text x="28" y="38" font-size="10.5" fill="#9AA0A8" font-family="'JetBrains Mono', Consolas, 'Courier New', monospace">NH_SAM3Segment · Vision</text>
|
||||
<circle cx="12" cy="67.0" r="5" fill="#64B5F6" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="24" y="71.0" font-size="12" fill="#D7DADF">image</text>
|
||||
<text x="336" y="71.0" font-size="12" fill="#D7DADF" text-anchor="end">segs</text>
|
||||
<circle cx="348" cy="67.0" r="5" fill="#E0B272" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="336" y="93.0" font-size="12" fill="#D7DADF" text-anchor="end">mask</text>
|
||||
<circle cx="348" cy="89.0" r="5" fill="#81C784" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="336" y="115.0" font-size="12" fill="#D7DADF" text-anchor="end">count</text>
|
||||
<circle cx="348" cy="111.0" r="5" fill="#9CC2E5" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="336" y="137.0" font-size="12" fill="#D7DADF" text-anchor="end">report</text>
|
||||
<circle cx="348" cy="133.0" r="5" fill="#C5E1A5" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<rect x="12" y="152" width="336" height="46" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="22" y="168" font-size="11.5" fill="#9AA0A8">concepts</text>
|
||||
<text x="22" y="188" font-size="11.5" fill="#E6E8EB" font-family="'JetBrains Mono', Consolas, 'Courier New', monospace">person</text>
|
||||
<rect x="12" y="203" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="20" y="219" font-size="10" fill="#6f747c">◀</text>
|
||||
<text x="332" y="219" font-size="10" fill="#6f747c">▶</text>
|
||||
<text x="34" y="219" font-size="11.5" fill="#9AA0A8">threshold</text>
|
||||
<text x="326" y="219" font-size="11.5" fill="#E6E8EB" text-anchor="end">0.50</text>
|
||||
<rect x="12" y="232" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="20" y="248" font-size="10" fill="#6f747c">◀</text>
|
||||
<text x="332" y="248" font-size="10" fill="#6f747c">▶</text>
|
||||
<text x="34" y="248" font-size="11.5" fill="#9AA0A8">detail_level</text>
|
||||
<text x="326" y="248" font-size="11.5" fill="#E6E8EB" text-anchor="end">balanced</text>
|
||||
<rect x="12" y="261" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="20" y="277" font-size="10" fill="#6f747c">◀</text>
|
||||
<text x="332" y="277" font-size="10" fill="#6f747c">▶</text>
|
||||
<text x="34" y="277" font-size="11.5" fill="#9AA0A8">max_per_concept</text>
|
||||
<text x="326" y="277" font-size="11.5" fill="#E6E8EB" text-anchor="end">0</text>
|
||||
<rect x="12" y="290" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="20" y="306" font-size="10" fill="#6f747c">◀</text>
|
||||
<text x="332" y="306" font-size="10" fill="#6f747c">▶</text>
|
||||
<text x="34" y="306" font-size="11.5" fill="#9AA0A8">checkpoint</text>
|
||||
<text x="326" y="306" font-size="11.5" fill="#E6E8EB" text-anchor="end">(choose a file)</text>
|
||||
<rect x="12" y="319" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="22" y="335" font-size="11.5" fill="#9AA0A8">keep_model_loaded</text>
|
||||
<rect x="308" y="325" width="30" height="13" rx="6.5" fill="#4a4d53"/>
|
||||
<circle cx="315" cy="331.5" r="5" fill="#F5F6F7"/>
|
||||
<rect x="12" y="348" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="22" y="364" font-size="11.5" fill="#9AA0A8">show_preview <tspan fill="#6f747c" font-style="italic">opt</tspan></text>
|
||||
<rect x="308" y="354" width="30" height="13" rx="6.5" fill="#4C8BF5"/>
|
||||
<circle cx="331" cy="360.5" r="5" fill="#F5F6F7"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.8 KiB |
@@ -0,0 +1,46 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="360" height="374" viewBox="0 0 360 374" font-family="Inter, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" role="img" aria-label="Save Image Path (NH) node">
|
||||
<rect x="0.5" y="0.5" width="359" height="373" rx="10" fill="#26282c" stroke="#45484f"/>
|
||||
<path d="M0.5 10.5 a10 10 0 0 1 10 -10 h339 a10 10 0 0 1 10 10 v36 h-359 z" fill="#34373d"/>
|
||||
<circle cx="17" cy="17" r="4.5" fill="#F6AD55"/>
|
||||
<text x="28" y="22" font-size="14" font-weight="600" fill="#EEF0F3">Save Image Path (NH)</text>
|
||||
<text x="28" y="38" font-size="10.5" fill="#9AA0A8" font-family="'JetBrains Mono', Consolas, 'Courier New', monospace">NH_SaveImagePath · Image</text>
|
||||
<circle cx="12" cy="67.0" r="5" fill="#64B5F6" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="24" y="71.0" font-size="12" fill="#D7DADF">images</text>
|
||||
<text x="336" y="71.0" font-size="12" fill="#D7DADF" text-anchor="end">filenames</text>
|
||||
<circle cx="348" cy="67.0" r="5" fill="#C5E1A5" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="336" y="93.0" font-size="12" fill="#D7DADF" text-anchor="end">path</text>
|
||||
<circle cx="348" cy="89.0" r="5" fill="#C5E1A5" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="336" y="115.0" font-size="12" fill="#D7DADF" text-anchor="end">saved_count</text>
|
||||
<circle cx="348" cy="111.0" r="5" fill="#9CC2E5" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<rect x="12" y="130" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="22" y="146" font-size="11.5" fill="#9AA0A8">folder_path</text>
|
||||
<text x="338" y="146" font-size="11.5" fill="#E6E8EB" text-anchor="end"></text>
|
||||
<rect x="12" y="159" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="22" y="175" font-size="11.5" fill="#9AA0A8">create_new_folder</text>
|
||||
<rect x="308" y="165" width="30" height="13" rx="6.5" fill="#4a4d53"/>
|
||||
<circle cx="315" cy="171.5" r="5" fill="#F5F6F7"/>
|
||||
<rect x="12" y="188" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="22" y="204" font-size="11.5" fill="#9AA0A8">new_folder_name</text>
|
||||
<text x="338" y="204" font-size="11.5" fill="#E6E8EB" text-anchor="end"></text>
|
||||
<rect x="12" y="217" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="22" y="233" font-size="11.5" fill="#9AA0A8">filename_prefix</text>
|
||||
<text x="338" y="233" font-size="11.5" fill="#E6E8EB" text-anchor="end">NH</text>
|
||||
<rect x="12" y="246" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="22" y="262" font-size="11.5" fill="#9AA0A8">number_suffix</text>
|
||||
<rect x="308" y="252" width="30" height="13" rx="6.5" fill="#4C8BF5"/>
|
||||
<circle cx="331" cy="258.5" r="5" fill="#F5F6F7"/>
|
||||
<rect x="12" y="275" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="22" y="291" font-size="11.5" fill="#9AA0A8">preview_image</text>
|
||||
<rect x="308" y="281" width="30" height="13" rx="6.5" fill="#4a4d53"/>
|
||||
<circle cx="315" cy="287.5" r="5" fill="#F5F6F7"/>
|
||||
<rect x="12" y="304" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="20" y="320" font-size="10" fill="#6f747c">◀</text>
|
||||
<text x="332" y="320" font-size="10" fill="#6f747c">▶</text>
|
||||
<text x="34" y="320" font-size="11.5" fill="#9AA0A8">file_format</text>
|
||||
<text x="326" y="320" font-size="11.5" fill="#E6E8EB" text-anchor="end">png</text>
|
||||
<rect x="12" y="333" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="20" y="349" font-size="10" fill="#6f747c">◀</text>
|
||||
<text x="332" y="349" font-size="10" fill="#6f747c">▶</text>
|
||||
<text x="34" y="349" font-size="11.5" fill="#9AA0A8">dpi</text>
|
||||
<text x="326" y="349" font-size="11.5" fill="#E6E8EB" text-anchor="end">300</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.6 KiB |
@@ -0,0 +1,32 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="360" height="258" viewBox="0 0 360 258" font-family="Inter, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" role="img" aria-label="NH Save Large Image node">
|
||||
<rect x="0.5" y="0.5" width="359" height="257" rx="10" fill="#26282c" stroke="#45484f"/>
|
||||
<path d="M0.5 10.5 a10 10 0 0 1 10 -10 h339 a10 10 0 0 1 10 10 v36 h-359 z" fill="#34373d"/>
|
||||
<circle cx="17" cy="17" r="4.5" fill="#F6AD55"/>
|
||||
<text x="28" y="22" font-size="14" font-weight="600" fill="#EEF0F3">NH Save Large Image</text>
|
||||
<text x="28" y="38" font-size="10.5" fill="#9AA0A8" font-family="'JetBrains Mono', Consolas, 'Courier New', monospace">NH_SaveLargeImage · Image</text>
|
||||
<circle cx="12" cy="67.0" r="5" fill="#64B5F6" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="24" y="71.0" font-size="12" fill="#D7DADF">image</text>
|
||||
<text x="336" y="71.0" font-size="12" fill="#D7DADF" text-anchor="end">image</text>
|
||||
<circle cx="348" cy="67.0" r="5" fill="#64B5F6" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="336" y="93.0" font-size="12" fill="#D7DADF" text-anchor="end">file_path</text>
|
||||
<circle cx="348" cy="89.0" r="5" fill="#C5E1A5" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="336" y="115.0" font-size="12" fill="#D7DADF" text-anchor="end">saved_count</text>
|
||||
<circle cx="348" cy="111.0" r="5" fill="#9CC2E5" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<rect x="12" y="130" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="22" y="146" font-size="11.5" fill="#9AA0A8">filename_prefix</text>
|
||||
<text x="338" y="146" font-size="11.5" fill="#E6E8EB" text-anchor="end">NH_Large</text>
|
||||
<rect x="12" y="159" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="20" y="175" font-size="10" fill="#6f747c">◀</text>
|
||||
<text x="332" y="175" font-size="10" fill="#6f747c">▶</text>
|
||||
<text x="34" y="175" font-size="11.5" fill="#9AA0A8">file_format</text>
|
||||
<text x="326" y="175" font-size="11.5" fill="#E6E8EB" text-anchor="end">png</text>
|
||||
<rect x="12" y="188" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="20" y="204" font-size="10" fill="#6f747c">◀</text>
|
||||
<text x="332" y="204" font-size="10" fill="#6f747c">▶</text>
|
||||
<text x="34" y="204" font-size="11.5" fill="#9AA0A8">quality</text>
|
||||
<text x="326" y="204" font-size="11.5" fill="#E6E8EB" text-anchor="end">95</text>
|
||||
<rect x="12" y="217" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="22" y="233" font-size="11.5" fill="#9AA0A8">embed_metadata</text>
|
||||
<rect x="308" y="223" width="30" height="13" rx="6.5" fill="#4C8BF5"/>
|
||||
<circle cx="331" cy="229.5" r="5" fill="#F5F6F7"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.6 KiB |
@@ -0,0 +1,22 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="360" height="222" viewBox="0 0 360 222" font-family="Inter, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" role="img" aria-label="Segs Compose (NH) node">
|
||||
<rect x="0.5" y="0.5" width="359" height="221" rx="10" fill="#26282c" stroke="#45484f"/>
|
||||
<path d="M0.5 10.5 a10 10 0 0 1 10 -10 h339 a10 10 0 0 1 10 10 v36 h-359 z" fill="#34373d"/>
|
||||
<circle cx="17" cy="17" r="4.5" fill="#8FD18F"/>
|
||||
<text x="28" y="22" font-size="14" font-weight="600" fill="#EEF0F3">Segs Compose (NH)</text>
|
||||
<text x="28" y="38" font-size="10.5" fill="#9AA0A8" font-family="'JetBrains Mono', Consolas, 'Courier New', monospace">NH_SegsCompose · Vision</text>
|
||||
<circle cx="12" cy="67.0" r="5" fill="#E0B272" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="24" y="71.0" font-size="12" fill="#D7DADF">segs</text>
|
||||
<text x="336" y="71.0" font-size="12" fill="#D7DADF" text-anchor="end">mask</text>
|
||||
<circle cx="348" cy="67.0" r="5" fill="#81C784" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<circle cx="12" cy="89.0" r="5" fill="#E0B272" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="24" y="93.0" font-size="12" fill="#D7DADF">segs_2 <tspan fill="#7d838c" font-style="italic">(optional)</tspan></text>
|
||||
<circle cx="12" cy="111.0" r="5" fill="#E0B272" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="24" y="115.0" font-size="12" fill="#D7DADF">segs_3 <tspan fill="#7d838c" font-style="italic">(optional)</tspan></text>
|
||||
<rect x="12" y="130" width="336" height="46" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="22" y="146" font-size="11.5" fill="#9AA0A8">expression</text>
|
||||
<text x="22" y="166" font-size="11.5" fill="#E6E8EB" font-family="'JetBrains Mono', Consolas, 'Courier New', monospace">all</text>
|
||||
<rect x="12" y="181" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="22" y="197" font-size="11.5" fill="#9AA0A8">check_same_photo <tspan fill="#6f747c" font-style="italic">opt</tspan></text>
|
||||
<rect x="308" y="187" width="30" height="13" rx="6.5" fill="#4C8BF5"/>
|
||||
<circle cx="331" cy="193.5" r="5" fill="#F5F6F7"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.0 KiB |
@@ -0,0 +1,13 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="360" height="112" viewBox="0 0 360 112" font-family="Inter, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" role="img" aria-label="Segs Preview (NH) node">
|
||||
<rect x="0.5" y="0.5" width="359" height="111" rx="10" fill="#26282c" stroke="#45484f"/>
|
||||
<path d="M0.5 10.5 a10 10 0 0 1 10 -10 h339 a10 10 0 0 1 10 10 v36 h-359 z" fill="#34373d"/>
|
||||
<circle cx="17" cy="17" r="4.5" fill="#8FD18F"/>
|
||||
<text x="28" y="22" font-size="14" font-weight="600" fill="#EEF0F3">Segs Preview (NH)</text>
|
||||
<text x="28" y="38" font-size="10.5" fill="#9AA0A8" font-family="'JetBrains Mono', Consolas, 'Courier New', monospace">NH_SegsPreview · Vision</text>
|
||||
<circle cx="12" cy="67.0" r="5" fill="#64B5F6" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="24" y="71.0" font-size="12" fill="#D7DADF">image</text>
|
||||
<text x="336" y="71.0" font-size="12" fill="#D7DADF" text-anchor="end">preview</text>
|
||||
<circle cx="348" cy="67.0" r="5" fill="#64B5F6" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<circle cx="12" cy="89.0" r="5" fill="#E0B272" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="24" y="93.0" font-size="12" fill="#D7DADF">segs</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1,48 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="360" height="345" viewBox="0 0 360 345" font-family="Inter, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" role="img" aria-label="Segs Select (NH) node">
|
||||
<rect x="0.5" y="0.5" width="359" height="344" rx="10" fill="#26282c" stroke="#45484f"/>
|
||||
<path d="M0.5 10.5 a10 10 0 0 1 10 -10 h339 a10 10 0 0 1 10 10 v36 h-359 z" fill="#34373d"/>
|
||||
<circle cx="17" cy="17" r="4.5" fill="#8FD18F"/>
|
||||
<text x="28" y="22" font-size="14" font-weight="600" fill="#EEF0F3">Segs Select (NH)</text>
|
||||
<text x="28" y="38" font-size="10.5" fill="#9AA0A8" font-family="'JetBrains Mono', Consolas, 'Courier New', monospace">NH_SegsSelect · Vision</text>
|
||||
<circle cx="12" cy="67.0" r="5" fill="#E0B272" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="24" y="71.0" font-size="12" fill="#D7DADF">segs</text>
|
||||
<text x="336" y="71.0" font-size="12" fill="#D7DADF" text-anchor="end">segs</text>
|
||||
<circle cx="348" cy="67.0" r="5" fill="#E0B272" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="336" y="93.0" font-size="12" fill="#D7DADF" text-anchor="end">mask</text>
|
||||
<circle cx="348" cy="89.0" r="5" fill="#81C784" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<text x="336" y="115.0" font-size="12" fill="#D7DADF" text-anchor="end">count</text>
|
||||
<circle cx="348" cy="111.0" r="5" fill="#9CC2E5" stroke="#1b1c1f" stroke-width="1.5"/>
|
||||
<rect x="12" y="130" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="22" y="146" font-size="11.5" fill="#9AA0A8">labels</text>
|
||||
<text x="338" y="146" font-size="11.5" fill="#E6E8EB" text-anchor="end"></text>
|
||||
<rect x="12" y="159" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="20" y="175" font-size="10" fill="#6f747c">◀</text>
|
||||
<text x="332" y="175" font-size="10" fill="#6f747c">▶</text>
|
||||
<text x="34" y="175" font-size="11.5" fill="#9AA0A8">min_score</text>
|
||||
<text x="326" y="175" font-size="11.5" fill="#E6E8EB" text-anchor="end">0.00</text>
|
||||
<rect x="12" y="188" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="20" y="204" font-size="10" fill="#6f747c">◀</text>
|
||||
<text x="332" y="204" font-size="10" fill="#6f747c">▶</text>
|
||||
<text x="34" y="204" font-size="11.5" fill="#9AA0A8">min_area</text>
|
||||
<text x="326" y="204" font-size="11.5" fill="#E6E8EB" text-anchor="end">0.0000</text>
|
||||
<rect x="12" y="217" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="20" y="233" font-size="10" fill="#6f747c">◀</text>
|
||||
<text x="332" y="233" font-size="10" fill="#6f747c">▶</text>
|
||||
<text x="34" y="233" font-size="11.5" fill="#9AA0A8">max_area</text>
|
||||
<text x="326" y="233" font-size="11.5" fill="#E6E8EB" text-anchor="end">1.0000</text>
|
||||
<rect x="12" y="246" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="20" y="262" font-size="10" fill="#6f747c">◀</text>
|
||||
<text x="332" y="262" font-size="10" fill="#6f747c">▶</text>
|
||||
<text x="34" y="262" font-size="11.5" fill="#9AA0A8">sort_by</text>
|
||||
<text x="326" y="262" font-size="11.5" fill="#E6E8EB" text-anchor="end">score</text>
|
||||
<rect x="12" y="275" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="20" y="291" font-size="10" fill="#6f747c">◀</text>
|
||||
<text x="332" y="291" font-size="10" fill="#6f747c">▶</text>
|
||||
<text x="34" y="291" font-size="11.5" fill="#9AA0A8">start</text>
|
||||
<text x="326" y="291" font-size="11.5" fill="#E6E8EB" text-anchor="end">0</text>
|
||||
<rect x="12" y="304" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
|
||||
<text x="20" y="320" font-size="10" fill="#6f747c">◀</text>
|
||||
<text x="332" y="320" font-size="10" fill="#6f747c">▶</text>
|
||||
<text x="34" y="320" font-size="11.5" fill="#9AA0A8">count</text>
|
||||
<text x="326" y="320" font-size="11.5" fill="#E6E8EB" text-anchor="end">0</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.7 KiB |