diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..5009789 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,5 @@ +# Use LF for Python files +*.py text eol=lf + +# Default everything to LF +* text=auto eol=lf diff --git a/__init__.py b/__init__.py index cee02d3..9f026c7 100644 --- a/__init__.py +++ b/__init__.py @@ -3,6 +3,7 @@ from .nodes import ( ApplyCircularConvPaddingVAE, C2EMaskedDiffNode, C2ENode, + CreateSeamMask, Crop360To180Node, CropImageWithCoordsNode, E2CNode, @@ -34,6 +35,7 @@ NODE_CLASS_MAPPINGS = { "Merge Monoscopic into Stereo": StereoToMonoScopicNode, "Apply Circular Padding VAE": ApplyCircularConvPaddingVAE, "Apply Circular Padding Model": ApplyCircularConvPaddingModel, + "Create Seam Mask": CreateSeamMask, } NODE_DISPLAY_NAME_MAPPINGS = { @@ -53,4 +55,5 @@ NODE_DISPLAY_NAME_MAPPINGS = { "Merge Monoscopic into Stereo": "Merge Monoscopic into Stereo", "Apply Circular Padding VAE": "Apply Circular Padding VAE", "Apply Circular Padding Model": "Apply Circular Padding Model", + "Create Seam Mask": "Create Seam Mask", } diff --git a/example_workflows/circular_padding.json b/example_workflows/circular_padding.json new file mode 100644 index 0000000..1b8570a --- /dev/null +++ b/example_workflows/circular_padding.json @@ -0,0 +1,502 @@ +{ + "id": "43787fee-fab6-4b11-a588-fe19868292a9", + "revision": 0, + "last_node_id": 12, + "last_link_id": 13, + "nodes": [ + { + "id": 8, + "type": "CLIPTextEncode", + "pos": [ + 4431.3701171875, + 1194.1214599609375 + ], + "size": [ + 422.84503173828125, + 164.31304931640625 + ], + "flags": { + "collapsed": true + }, + "order": 4, + "mode": 0, + "inputs": [ + { + "name": "clip", + "type": "CLIP", + "link": 6 + } + ], + "outputs": [ + { + "name": "CONDITIONING", + "type": "CONDITIONING", + "slot_index": 0, + "links": [ + 12 + ] + } + ], + "title": "CLIP Text Encode (Negative Prompt)", + "properties": { + "cnr_id": "comfy-core", + "ver": "0.3.33", + "Node name for S&R": "CLIPTextEncode" + }, + "widgets_values": [ + "" + ], + "color": "#322", + "bgcolor": "#533" + }, + { + "id": 4, + "type": "CheckpointLoaderSimple", + "pos": [ + 4076.524169921875, + 712.0999145507812 + ], + "size": [ + 270, + 98 + ], + "flags": {}, + "order": 0, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "MODEL", + "type": "MODEL", + "links": [ + 3 + ] + }, + { + "name": "CLIP", + "type": "CLIP", + "links": [ + 5, + 6 + ] + }, + { + "name": "VAE", + "type": "VAE", + "links": [ + 4 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.3.33", + "Node name for S&R": "CheckpointLoaderSimple" + }, + "widgets_values": [ + "flux1-dev-fp8.safetensors" + ] + }, + { + "id": 5, + "type": "Apply Circular Padding Model", + "pos": [ + 4425.533203125, + 679.6553344726562 + ], + "size": [ + 284.316650390625, + 82 + ], + "flags": {}, + "order": 2, + "mode": 0, + "inputs": [ + { + "name": "model", + "type": "MODEL", + "link": 3 + } + ], + "outputs": [ + { + "name": "MODEL", + "type": "MODEL", + "links": [ + 7 + ] + } + ], + "properties": { + "cnr_id": "comfyui-pytorch360convert", + "ver": "1.0.2", + "Node name for S&R": "Apply Circular Padding Model" + }, + "widgets_values": [ + true, + true + ] + }, + { + "id": 11, + "type": "VAEDecode", + "pos": [ + 5340.41162109375, + 680.2943115234375 + ], + "size": [ + 210, + 46 + ], + "flags": {}, + "order": 7, + "mode": 0, + "inputs": [ + { + "name": "samples", + "type": "LATENT", + "link": 9 + }, + { + "name": "vae", + "type": "VAE", + "link": 10 + } + ], + "outputs": [ + { + "name": "IMAGE", + "type": "IMAGE", + "slot_index": 0, + "links": [ + 8 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.3.33", + "Node name for S&R": "VAEDecode" + }, + "widgets_values": [] + }, + { + "id": 7, + "type": "KSampler", + "pos": [ + 4958.00390625, + 680.6325073242188 + ], + "size": [ + 315, + 474 + ], + "flags": {}, + "order": 6, + "mode": 0, + "inputs": [ + { + "name": "model", + "type": "MODEL", + "link": 7 + }, + { + "name": "positive", + "type": "CONDITIONING", + "link": 11 + }, + { + "name": "negative", + "type": "CONDITIONING", + "link": 12 + }, + { + "name": "latent_image", + "type": "LATENT", + "link": 13 + } + ], + "outputs": [ + { + "name": "LATENT", + "type": "LATENT", + "slot_index": 0, + "links": [ + 9 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.3.33", + "Node name for S&R": "KSampler" + }, + "widgets_values": [ + 1, + "randomize", + 20, + 1, + "euler", + "simple", + 1 + ] + }, + { + "id": 6, + "type": "Apply Circular Padding VAE", + "pos": [ + 4426.53759765625, + 816.3192749023438 + ], + "size": [ + 273.1499938964844, + 82 + ], + "flags": {}, + "order": 5, + "mode": 0, + "inputs": [ + { + "name": "vae", + "type": "VAE", + "link": 4 + } + ], + "outputs": [ + { + "name": "VAE", + "type": "VAE", + "links": [ + 10 + ] + } + ], + "properties": { + "cnr_id": "comfyui-pytorch360convert", + "ver": "1.0.2", + "Node name for S&R": "Apply Circular Padding VAE" + }, + "widgets_values": [ + true, + true + ] + }, + { + "id": 9, + "type": "CLIPTextEncode", + "pos": [ + 4428.75439453125, + 950.9507446289062 + ], + "size": [ + 295.62408447265625, + 151.8404083251953 + ], + "flags": {}, + "order": 3, + "mode": 0, + "inputs": [ + { + "name": "clip", + "type": "CLIP", + "link": 5 + } + ], + "outputs": [ + { + "name": "CONDITIONING", + "type": "CONDITIONING", + "slot_index": 0, + "links": [ + 11 + ] + } + ], + "title": "CLIP Text Encode (Positive Prompt)", + "properties": { + "cnr_id": "comfy-core", + "ver": "0.3.33", + "Node name for S&R": "CLIPTextEncode" + }, + "widgets_values": [ + "prompt text here" + ], + "color": "#232", + "bgcolor": "#353" + }, + { + "id": 10, + "type": "EmptySD3LatentImage", + "pos": [ + 4428.66162109375, + 1246.2977294921875 + ], + "size": [ + 315, + 106 + ], + "flags": {}, + "order": 1, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "LATENT", + "type": "LATENT", + "slot_index": 0, + "links": [ + 13 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.3.33", + "Node name for S&R": "EmptySD3LatentImage" + }, + "widgets_values": [ + 1536, + 768, + 1 + ], + "color": "#323", + "bgcolor": "#535" + }, + { + "id": 12, + "type": "SaveImage", + "pos": [ + 5597.923828125, + 680.36474609375 + ], + "size": [ + 985.3012084960938, + 1060.3828125 + ], + "flags": {}, + "order": 8, + "mode": 0, + "inputs": [ + { + "name": "images", + "type": "IMAGE", + "link": 8 + } + ], + "outputs": [], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.3.33" + }, + "widgets_values": [ + "ComfyUI" + ] + } + ], + "links": [ + [ + 3, + 4, + 0, + 5, + 0, + "MODEL" + ], + [ + 4, + 4, + 2, + 6, + 0, + "VAE" + ], + [ + 5, + 4, + 1, + 9, + 0, + "CLIP" + ], + [ + 6, + 4, + 1, + 8, + 0, + "CLIP" + ], + [ + 7, + 5, + 0, + 7, + 0, + "MODEL" + ], + [ + 8, + 11, + 0, + 12, + 0, + "IMAGE" + ], + [ + 9, + 7, + 0, + 11, + 0, + "LATENT" + ], + [ + 10, + 6, + 0, + 11, + 1, + "VAE" + ], + [ + 11, + 9, + 0, + 7, + 1, + "CONDITIONING" + ], + [ + 12, + 8, + 0, + 7, + 2, + "CONDITIONING" + ], + [ + 13, + 10, + 0, + 7, + 3, + "LATENT" + ] + ], + "groups": [], + "config": {}, + "extra": { + "ds": { + "scale": 1, + "offset": [ + -3500.2956000000004, + -505.54010000000017 + ] + }, + "node_versions": { + "comfy-core": "0.3.12" + }, + "frontendVersion": "1.18.9" + }, + "version": 0.4 +} \ No newline at end of file diff --git a/examples/crop_and_paste.json b/example_workflows/crop_and_paste.json similarity index 100% rename from examples/crop_and_paste.json rename to example_workflows/crop_and_paste.json diff --git a/examples/cubemap2equirectangular.json b/example_workflows/cubemap2equirectangular.json similarity index 100% rename from examples/cubemap2equirectangular.json rename to example_workflows/cubemap2equirectangular.json diff --git a/examples/equirectangular2equirectangular.json b/example_workflows/equirectangular2equirectangular.json similarity index 100% rename from examples/equirectangular2equirectangular.json rename to example_workflows/equirectangular2equirectangular.json diff --git a/examples/equirectangular2perspective.json b/example_workflows/equirectangular2perspective.json similarity index 100% rename from examples/equirectangular2perspective.json rename to example_workflows/equirectangular2perspective.json diff --git a/examples/example_basic_equi2cubemap2equi.png b/example_workflows/example_basic_equi2cubemap2equi.png similarity index 100% rename from examples/example_basic_equi2cubemap2equi.png rename to example_workflows/example_basic_equi2cubemap2equi.png diff --git a/examples/example_basic_equi2cubemap2split2equi.png b/example_workflows/example_basic_equi2cubemap2split2equi.png similarity index 100% rename from examples/example_basic_equi2cubemap2split2equi.png rename to example_workflows/example_basic_equi2cubemap2split2equi.png diff --git a/examples/roll_image.json b/example_workflows/roll_image.json similarity index 100% rename from examples/roll_image.json rename to example_workflows/roll_image.json diff --git a/examples/split_and_merge_faces.json b/example_workflows/split_and_merge_faces.json similarity index 100% rename from examples/split_and_merge_faces.json rename to example_workflows/split_and_merge_faces.json diff --git a/nodes.py b/nodes.py index df62aa9..fb84fd6 100644 --- a/nodes.py +++ b/nodes.py @@ -302,6 +302,13 @@ class RollImageNode: "image": ("IMAGE", {"default": None}), "roll_x": ("INT", {"default": 0}), "roll_y": ("INT", {"default": 0}), + "roll_x_by_50_percent": ( + "BOOLEAN", + { + "default": False, + "tooltip": "Ignores roll_x and roll_y. Shifts image horizontally by 50%.", + }, + ), }, } @@ -313,9 +320,18 @@ class RollImageNode: CATEGORY = "pytorch360convert" def roll_image( - self, image: torch.Tensor, roll_x: int = 0, roll_y: int = 0 + self, + image: torch.Tensor, + roll_x: int = 0, + roll_y: int = 0, + roll_x_by_50_percent: bool = False, ) -> Tuple[torch.Tensor]: assert image.dim() == 4, f"image should have 4 dimensions, got {image.dim()}" + if roll_x_by_50_percent: + _, H, W, _ = image.shape + px_half = W // 2 + roll_y = 0 + roll_x = px_half return (torch.roll(image, shifts=(roll_y, roll_x), dims=(1, 2)),) @@ -877,3 +893,63 @@ class ApplyCircularConvPaddingVAE: use_vae = copy.deepcopy(vae) _apply_circular_conv2d_padding(use_vae, True, x_axis_only) return (use_vae,) + + +def _create_center_seam_mask(x: torch.Tensor, frac_width: float = 0.10) -> torch.Tensor: + """ + For a ComfyUI-style mask: shape [B, H, W], values 0 or 1. + + Args: + x (torch.Tensor): input tensor with shape [B, H, W, C]. + frac_width (float, optional): fraction of input width for the vertical strip. + + Returns: + mask: torch.Tensor of shape [B, H, W] with float values 0.0 & 1.0. + """ + # Extract batch, height, and width from x + B, H, W, _ = x.shape + strip = max(1, int(W * frac_width)) + x0 = (W - strip) // 2 + x1 = x0 + strip + + # Create the mask with zeros + mask = torch.zeros((B, H, W), dtype=x.dtype, device=x.device) + + # Fill the vertical strip with ones + mask[:, :, x0:x1] = 1.0 + + return mask + + +class CreateSeamMask: + """ + Create a seam mask for inpainting on equirectangular images. + """ + + @classmethod + def INPUT_TYPES(s) -> Dict: + return { + "required": { + "image": ("IMAGE", {"default": None}), + "seam_mask_width": ("FLOAT", {"default": 0.10}), + }, + } + + RETURN_TYPES = ("MASK",) + RETURN_NAMES = ("Seam Mask",) + + FUNCTION = "run" + + CATEGORY = "pytorch360convert" + + def run( + self, + image: torch.Tensor, + seam_mask_width: float = 0.10, + ) -> Tuple[torch.Tensor]: + assert image.dim() == 4, "Image should have 4 dimensions" + _, H, W, _ = image.shape + px_half = W // 2 + image_rolled = torch.roll(image, shifts=(0, px_half), dims=(1, 2)) + seam_mask = _create_center_seam_mask(image, frac_width=seam_mask_width) + return (seam_mask,)