diff --git a/cmake/Modules/BuildFLTK.cmake b/cmake/Modules/BuildFLTK.cmake index 5262052a9..83489cc50 100644 --- a/cmake/Modules/BuildFLTK.cmake +++ b/cmake/Modules/BuildFLTK.cmake @@ -7,11 +7,10 @@ include( ExternalProject ) # The cutting EDGE! #set( FLTK_GIT_TAG master ) -# Adds label_image_spacing(). -#set(FLTK_GIT_TAG 05c91b287f16d87f335a8cc375074d712cb8511a) +# Wayland fix. (latest stable) +#set(FLTK_GIT_TAG 382d6b2fbdb441ef8cbbe52a7dc07032aa733188) -# Wayland fix. -set(FLTK_GIT_TAG 382d6b2fbdb441ef8cbbe52a7dc07032aa733188) +set(FLTK_GIT_TAG a333817f4178d944685fc787bd389097e8d85aca) if(MRV2_PYFLTK OR FLTK_BUILD_SHARED) # If we are building pyFLTK compile shared diff --git a/src/ComfyUI/custom_nodes/mrv2/draw/mrv2_annotations_image.py b/src/ComfyUI/custom_nodes/mrv2/draw/mrv2_annotations_image.py new file mode 100644 index 000000000..da0805878 --- /dev/null +++ b/src/ComfyUI/custom_nodes/mrv2/draw/mrv2_annotations_image.py @@ -0,0 +1,145 @@ +from PIL import Image, ImageDraw +import json + +import folder_paths +import logging +import numpy as np +import os +import time +import torch + +# Function to scale points +def _scale_points(points, scale_x, scale_y): + return [(x * scale_x, y * scale_y) for x, y in points] + +# Function to draw a circle with a specific width +def _draw_circle(draw, center, radius, width, color): + outer_radius = radius + inner_radius = radius - width / 2 + + # Draw the outer circle + draw.ellipse( + (center[0] - outer_radius, center[1] - outer_radius, center[0] + outer_radius, center[1] + outer_radius), + outline=255, + width=width + ) + +class mrv2AnnotationsImageNode: + @classmethod + def INPUT_TYPES(s): + input_directory = folder_paths.get_input_directory() + files = [f for f in os.listdir(input_directory) \ + if os.path.isfile(os.path.join(input_directory, f))] + return {"required": + { "annotations": (sorted(files), {"image_upload": True})}, + } + + + RETURN_TYPES = ("IMAGE", "MASK") + OUTPUT_TOOLTIPS = ("The decoded image and mask.",) + + FUNCTION = "create_mask" + + CATEGORY = "mrv2/mask" + DESCRIPTION = "Loads a mrv2 annotation .json file and creates a mask for it." + + @classmethod + def VALIDATE_INPUTS(s, annotations): + if not folder_paths.exists_annotated_filepath(annotations): + return "Invalid image file: {}".format(annotations) + + return True + + @classmethod + def IS_CHANGED(s, annotations): + return time.time() + + def create_mask(self, annotations): + input_directory = folder_paths.get_input_directory() + annotations_path = os.path.join(input_directory, annotations) + + logging.debug(f"Reading annotation {annotations_path}") + # Load the JSON file + with open(annotations_path) as f: + data = json.load(f) + + # Outputs + output_images = [] + output_masks = [] + + # Image size + image_width, image_height = data['render_size'] + + scale_factor = 4 # Drawing on a canvas 4x larger for antialiasing + high_res_width = image_width * scale_factor + high_res_height = image_height * scale_factor + + # Create a high-resolution black image + image = Image.new('L', (high_res_width, high_res_height), 0) + draw = ImageDraw.Draw(image) + + # Process the paths from the JSON + for annotation in data['annotations']: + for shape in annotation['shapes']: + shape_type = shape['type'] + pen_size = int(shape.get('pen_size', 1.0) * scale_factor) + if shape.get('pts', None): + points = [(point['x'], image_height - point['y']) + for point in shape['pts']] + + # Scale points to the higher-resolution canvas + points = _scale_points(points, scale_factor, scale_factor) + + if shape_type == 'DrawPath' or shape_type == 'Rectangle': + # Draw the path (white color, width defined by 'pen_size') + draw.line(points, fill=255, width=int(pen_size)) + elif shape_type == 'ErasePath': + # Draw the path (white color, width defined by 'pen_size') + draw.line(points, fill=0, width=int(pen_size * 1.5)) + elif shape_type == 'Arrow': + left_side = [points[1], points[2]] + draw.line(left_side, fill=255, width=int(pen_size)) + right_side = [points[1], points[4]] + draw.line(right_side, fill=255, width=int(pen_size)) + root = [points[0], points[1]] + draw.line(root, fill=255, width=int(pen_size)) + elif shape_type == 'Text': + continue + elif shape_type == 'Circle': + # Center and radius for the circle + center = shape['center'] # Center of the image + center[1] = image_height - center[1] + radius = int(shape['radius'] * scale_factor) # Outer radius + + center = _scale_points([center], scale_factor, scale_factor)[0] + + # Draw the circle with the specified stroke width + _draw_circle(draw, center, radius, pen_size, 255) + + + # Downscale to the target resolution + mask = image.resize((image_width, image_height), + Image.Resampling.LANCZOS) + image = mask.convert('RGB') + + # Convert the PIL mask to a NumPy array + mask = np.array(mask).astype(np.float32) / 255.0 + image = np.array(image).astype(np.float32) / 255.0 + + # Convert the NumPy array to torch data + image = torch.from_numpy(image)[None,] + mask = torch.from_numpy(mask).unsqueeze(0) + + output_images.append(image) + output_masks.append(mask) + + if len(output_images) > 1: + output_image = torch.cat(output_images, dim=0) + output_mask = torch.cat(output_masks, dim=0) + else: + output_image = output_images[0] + output_mask = output_masks[0] + + print("Annotation Image", output_image.shape) + print("Annotation Mask", output_mask.shape) + return (output_image, output_mask) diff --git a/src/ComfyUI/custom_nodes/mrv2/nodes.py b/src/ComfyUI/custom_nodes/mrv2/nodes.py index 801b80d0e..7ce97a659 100644 --- a/src/ComfyUI/custom_nodes/mrv2/nodes.py +++ b/src/ComfyUI/custom_nodes/mrv2/nodes.py @@ -1,10 +1,13 @@ from .save.mrv2_save_exr_image import mrv2SaveEXRImage +from .draw.mrv2_annotations_image import mrv2AnnotationsImageNode NODE_CLASS_MAPPINGS = { - "mrv2SaveEXRImage": mrv2SaveEXRImage + "mrv2SaveEXRImage": mrv2SaveEXRImage, + "mrv2AnnotationsImageNode": mrv2AnnotationsImageNode, } NODE_DISPLAY_NAME_MAPPINGS = { "mrv2SaveEXRImage": "mrv2 Save EXR Image", + "mrv2AnnotationsImageNode": "mrv2 Annotations Image Node", } diff --git a/src/ComfyUI/custom_nodes/mrv2/requirements.txt b/src/ComfyUI/custom_nodes/mrv2/requirements.txt index 665c0b45f..94d10bf2c 100644 --- a/src/ComfyUI/custom_nodes/mrv2/requirements.txt +++ b/src/ComfyUI/custom_nodes/mrv2/requirements.txt @@ -1 +1,2 @@ openexr +pillow diff --git a/src/ComfyUI/custom_nodes/mrv2/save/mrv2_save_exr_image.py b/src/ComfyUI/custom_nodes/mrv2/save/mrv2_save_exr_image.py index d07a86dc2..9ed773391 100644 --- a/src/ComfyUI/custom_nodes/mrv2/save/mrv2_save_exr_image.py +++ b/src/ComfyUI/custom_nodes/mrv2/save/mrv2_save_exr_image.py @@ -107,6 +107,7 @@ def INPUT_TYPES(s): CATEGORY = "mrv2/save" + @classmethod def IS_CHANGED(s, images): return time.time() @@ -215,10 +216,10 @@ def save_images(self, images, masks = [], # NOTE: names should be globally unique NODE_CLASS_MAPPINGS = { - "mrv2SaveEXRImage": mrv2SaveEXRImage + "mrv2SaveEXRImage": mrv2SaveEXRImage, } # A dictionary that contains the friendly/humanly readable titles for the nodes NODE_DISPLAY_NAME_MAPPINGS = { - "mrv2SaveEXRImage": "mrv2 Save EXR Node" + "mrv2SaveEXRImage": "mrv2 Save EXR Node", } diff --git a/src/lib/mrvFl/mrvCallbacks.cpp b/src/lib/mrvFl/mrvCallbacks.cpp index 5aaad81a3..fc8a59664 100644 --- a/src/lib/mrvFl/mrvCallbacks.cpp +++ b/src/lib/mrvFl/mrvCallbacks.cpp @@ -666,7 +666,8 @@ namespace mrv void save_annotations_as_json_cb(Fl_Menu_* w, ViewerUI* ui) { - auto player = ui->uiView->getTimelinePlayer(); + auto view = ui->uiView; + auto player = view->getTimelinePlayer(); if (!player) return; @@ -679,6 +680,8 @@ namespace mrv return; Message j; + j["render_size"] = view->getRenderSize(); + std::vector< draw::Annotation > flatAnnotations; for (const auto& annotation : annotations) { diff --git a/tlRender b/tlRender index 2a8756f37..34e4c1255 160000 --- a/tlRender +++ b/tlRender @@ -1 +1 @@ -Subproject commit 2a8756f37d84daeeb80eecc73c2ef89d968fb5ba +Subproject commit 34e4c125513f3375894f629fd03267f8a908031c