MaskCropMaster commited on
Commit ·
bfbe8cd
0
Parent(s):
Initial release: MASK CROP MASTER - always-square mask crop for ComfyUI
Browse files- README.md +71 -0
- __init__.py +86 -0
README.md
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# MASK CROP MASTER — ComfyUI Custom Node
|
| 2 |
+
|
| 3 |
+
**Always-square mask crop, even at image edges.**
|
| 4 |
+
|
| 5 |
+
## The Problem
|
| 6 |
+
|
| 7 |
+
When you mask a region at the **edge** of an image, the standard `Mask Crop Region` node from was-ns produces a **rectangular** crop instead of a square. This happens because it clamps the crop dimensions when the square extends beyond the image bounds.
|
| 8 |
+
|
| 9 |
+
## The Solution
|
| 10 |
+
|
| 11 |
+
`MASK CROP MASTER` **repositions** the square instead of clamping it. When the crop would extend past an image edge, it slides the square inward so it always fits completely inside the image.
|
| 12 |
+
|
| 13 |
+
```
|
| 14 |
+
was-ns (rectangle): MASK CROP MASTER (square):
|
| 15 |
+
┌────────────────────┐ ┌────────────────────┐
|
| 16 |
+
│████████████░░░░░░░░│ │░░░░░░░░░░░░░░░░░░░░│
|
| 17 |
+
│████████████░░░░░░░░│ │░░████████░░░░░░░░░░│
|
| 18 |
+
│░░░░░░░░░░░░░░░░░░░░│ │░░████████░░░░░░░░░░│
|
| 19 |
+
│░░░░░░░░░░░░░░░░░░░░│ │░░░░░░░░░░░░░░░░░░░░│
|
| 20 |
+
└────────────────────┘ └────────────────────┘
|
| 21 |
+
400×200 (broken) 400×400 (correct)
|
| 22 |
+
```
|
| 23 |
+
|
| 24 |
+
## Installation
|
| 25 |
+
|
| 26 |
+
```bash
|
| 27 |
+
cd ComfyUI/custom_nodes
|
| 28 |
+
git clone https://huggingface.co/YOUR_USERNAME/MaskCropMaster.git
|
| 29 |
+
```
|
| 30 |
+
|
| 31 |
+
Restart ComfyUI after cloning.
|
| 32 |
+
|
| 33 |
+
## Usage
|
| 34 |
+
|
| 35 |
+
Drop-in replacement for `Mask Crop Region` from was-ns:
|
| 36 |
+
|
| 37 |
+
1. **Remove** the old `Mask Crop Region` node from your workflow
|
| 38 |
+
2. **Add** `MASK CROP MASTER` from the `WAS Suite/Image/Masking` category
|
| 39 |
+
3. **Connect** the same inputs: `mask`, `padding`, `region_type`
|
| 40 |
+
4. **Connect** the same outputs: `cropped_mask`, `crop_data`, coordinates, dimensions
|
| 41 |
+
|
| 42 |
+
## Inputs
|
| 43 |
+
|
| 44 |
+
| Input | Type | Default | Description |
|
| 45 |
+
|-------|------|---------|-------------|
|
| 46 |
+
| `mask` | MASK | — | Input mask |
|
| 47 |
+
| `padding` | INT | 24 | Padding around the bounding box before squaring |
|
| 48 |
+
| `region_type` | COMBO | "dominant" | "dominant" or "minority" (was-ns compatible) |
|
| 49 |
+
|
| 50 |
+
## Outputs
|
| 51 |
+
|
| 52 |
+
| Output | Type | Description |
|
| 53 |
+
|--------|------|-------------|
|
| 54 |
+
| `cropped_mask` | MASK | Square-cropped mask |
|
| 55 |
+
| `crop_data` | CROP_DATA | Compatible with `Mask Paste Region` |
|
| 56 |
+
| `top_int` | INT | Y position of the square |
|
| 57 |
+
| `left_int` | INT | X position of the square |
|
| 58 |
+
| `right_int` | INT | Bottom edge |
|
| 59 |
+
| `bottom_int` | INT | Right edge |
|
| 60 |
+
| `width_int` | INT | Width (= side length) |
|
| 61 |
+
| `height_int` | INT | Height (= side length) |
|
| 62 |
+
|
| 63 |
+
## Compatibility
|
| 64 |
+
|
| 65 |
+
- Works with `Mask Paste Region` from was-ns (same `crop_data` format)
|
| 66 |
+
- Same output names → minimal rewiring needed
|
| 67 |
+
- Tested with ComfyUI latest version
|
| 68 |
+
|
| 69 |
+
## License
|
| 70 |
+
|
| 71 |
+
Apache 2.0
|
__init__.py
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import torch
|
| 2 |
+
import numpy as np
|
| 3 |
+
from PIL import Image, ImageOps
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
class MaskCropMaster:
|
| 7 |
+
"""Mask Crop Region replacement that ALWAYS outputs a square crop.
|
| 8 |
+
|
| 9 |
+
Based on WAS_Mask_Crop_Region from was-ns, with the key fix:
|
| 10 |
+
when the square extends beyond the image bounds, it REPOSITIONS
|
| 11 |
+
the crop instead of clamping dimensions (which produces a rectangle).
|
| 12 |
+
|
| 13 |
+
Compatible with Mask Paste Region (same crop_data format).
|
| 14 |
+
"""
|
| 15 |
+
|
| 16 |
+
@classmethod
|
| 17 |
+
def INPUT_TYPES(cls):
|
| 18 |
+
return {
|
| 19 |
+
"required": {
|
| 20 |
+
"mask": ("MASK",),
|
| 21 |
+
"padding": ("INT", {"default": 24, "min": 0, "max": 4096, "step": 1}),
|
| 22 |
+
"region_type": (["dominant", "minority"],),
|
| 23 |
+
}
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
RETURN_TYPES = ("MASK", "CROP_DATA", "INT", "INT", "INT", "INT", "INT", "INT")
|
| 27 |
+
RETURN_NAMES = ("cropped_mask", "crop_data", "top_int", "left_int", "right_int", "bottom_int", "width_int", "height_int")
|
| 28 |
+
FUNCTION = "mask_crop_master"
|
| 29 |
+
CATEGORY = "WAS Suite/Image/Masking"
|
| 30 |
+
DESCRIPTION = "Mask Crop Region that always outputs a square, even at image edges."
|
| 31 |
+
|
| 32 |
+
def mask_crop_master(self, mask, padding=24, region_type="dominant"):
|
| 33 |
+
mask_np = mask.cpu().squeeze().numpy()
|
| 34 |
+
mask_pil = Image.fromarray(np.clip(255.0 * mask_np, 0, 255).astype(np.uint8))
|
| 35 |
+
|
| 36 |
+
img_w, img_h = mask_pil.size
|
| 37 |
+
|
| 38 |
+
bbox = mask_pil.getbbox()
|
| 39 |
+
if bbox is None:
|
| 40 |
+
empty = Image.new("L", (img_w, img_h), 0)
|
| 41 |
+
empty_tensor = torch.from_numpy(np.array(empty).astype(np.float32) / 255.0).unsqueeze(0).unsqueeze(1)
|
| 42 |
+
crop_data = ((img_w, img_h), (0, 0, 0, 0))
|
| 43 |
+
return (empty_tensor, crop_data, 0, 0, 0, 0, img_w, img_h)
|
| 44 |
+
|
| 45 |
+
bbox_x1, bbox_y1, bbox_x2, bbox_y2 = bbox
|
| 46 |
+
bbox_w = bbox_x2 - bbox_x1
|
| 47 |
+
bbox_h = bbox_y2 - bbox_y1
|
| 48 |
+
|
| 49 |
+
side = max(bbox_w, bbox_h) + 2 * padding
|
| 50 |
+
side = min(side, img_w, img_h)
|
| 51 |
+
|
| 52 |
+
cx = (bbox_x1 + bbox_x2) / 2.0
|
| 53 |
+
cy = (bbox_y1 + bbox_y2) / 2.0
|
| 54 |
+
|
| 55 |
+
crop_x = round(cx - side / 2.0)
|
| 56 |
+
crop_y = round(cy - side / 2.0)
|
| 57 |
+
|
| 58 |
+
if crop_x < 0:
|
| 59 |
+
crop_x = 0
|
| 60 |
+
if crop_y < 0:
|
| 61 |
+
crop_y = 0
|
| 62 |
+
if crop_x + side > img_w:
|
| 63 |
+
crop_x = img_w - side
|
| 64 |
+
if crop_y + side > img_h:
|
| 65 |
+
crop_y = img_h - side
|
| 66 |
+
|
| 67 |
+
crop_x2 = crop_x + side
|
| 68 |
+
crop_y2 = crop_y + side
|
| 69 |
+
|
| 70 |
+
cropped_mask = mask_pil.crop((crop_x, crop_y, crop_x2, crop_y2))
|
| 71 |
+
|
| 72 |
+
region_tensor = torch.from_numpy(
|
| 73 |
+
np.array(cropped_mask).astype(np.float32) / 255.0
|
| 74 |
+
).unsqueeze(0).unsqueeze(1)
|
| 75 |
+
|
| 76 |
+
crop_data = (cropped_mask.size, (crop_x, crop_y, crop_x2, crop_y2))
|
| 77 |
+
|
| 78 |
+
return (region_tensor, crop_data, crop_y, crop_x, crop_y2, crop_x2, side, side)
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
NODE_CLASS_MAPPINGS = {
|
| 82 |
+
"MaskCropMaster": MaskCropMaster,
|
| 83 |
+
}
|
| 84 |
+
NODE_DISPLAY_NAME_MAPPINGS = {
|
| 85 |
+
"MaskCropMaster": "MASK CROP MASTER",
|
| 86 |
+
}
|