10 KiB
Implementation Plan: Compass Image Loader
This document provides a step-by-step implementation plan for the CompassImageLoader ComfyUI custom node. Each task is a concrete, actionable item with a checkbox.
Phase 1: Project Bootstrap & File Structure
Goal: Set up the custom node package so ComfyUI recognizes and registers the node.
-
Create
__init__.py- Import
CompassImageLoaderfromcompass_image_loader. - Define
NODE_CLASS_MAPPINGS = {"CompassImageLoader": CompassImageLoader}. - Define
NODE_DISPLAY_NAME_MAPPINGS = {"CompassImageLoader": "Compass Image Loader"}.
- Import
-
Create
compass_image_loader.py- Define the
CompassImageLoaderclass. - Set
CATEGORY = "image/loaders".
- Define the
Phase 2: Backend Node Implementation
Goal: Implement the Python class that ComfyUI executes.
2.1 Input Types & Discovery
- Implement
CompassImageLoader.INPUT_TYPES(cls)classmethod.- Implement
_discover_directories()helper that:- Gets the base path via
folder_paths.get_input_directory(). - Recursively walks the base path using
os.walk. - Identifies valid direction names:
{"n", "ne", "e", "se", "s", "sw", "w", "nw"}. - Identifies valid modality names:
{"image", "depth", "openpose"}. - For each directory, if it contains any valid direction subfolder or any valid modality subfolder, add its relative path (from
input/) to the candidate list. - Return sorted, deduplicated list of strings.
- Gets the base path via
- Return dictionary with:
required:directory:(directories_list,)direction:(["", "n", "ne", "e", "se", "s", "sw", "w", "nw"],)modality:(["image", "depth", "openpose"],)frame:("STRING", {"default": ""})width:("INT", {"default": 0, "min": 0, "max": 16384, "step": 1})height:("INT", {"default": 0, "min": 0, "max": 16384, "step": 1})
- Implement
2.2 Return Types & Function Signature
- Set
RETURN_TYPES = ("IMAGE", "STRING", "INT", "INT", "INT"). - Set
RETURN_NAMES = ("IMAGE", "path", "width", "height", "frame_count"). - Set
FUNCTION = "load_images". - Set
OUTPUT_NODE = False.
2.3 Core Loading Logic
- Implement
load_images(self, directory, direction, modality, frame, width, height).
Path Resolution
- Resolve
base_dir = folder_paths.get_input_directory(). - Construct
target_dir:- If
direction.strip():os.path.join(base_dir, directory, direction, modality). - Else:
os.path.join(base_dir, directory, modality).
- If
- Verify
target_direxists; if not, raiseRuntimeError.
Image Discovery
- List files in
target_dirwithos.listdir. - Filter to supported image extensions:
png,jpg,jpeg,webp,bmp,gif,tiff. - Sort alphabetically:
sorted(files). - If list is empty, raise
RuntimeError.
Frame Selection
- If
frameisNoneorframe.strip() == "":- Select all images (
selected_files = all_files). - Set
output_path = target_dir.
- Select all images (
- Else:
- Try
index = int(frame.strip()). - If
index < 0orindex >= len(all_files), raiseRuntimeError. - Select single file:
selected_files = [all_files[index]]. - Set
output_path = os.path.join(target_dir, all_files[index]).
- Try
Image Processing Loop
- Initialize empty list
tensors = []. - For each file in
selected_files:- Open with
Image.open(path). - Convert to RGB:
image = image.convert("RGB"). - Read original dimensions:
orig_w, orig_h = image.size. - Determine final dimensions and resize:
- If
width == 0 and height == 0:final_w, final_h = orig_w, orig_h(no resize).
- Elif
width > 0 and height == 0:final_w = width.final_h = int(orig_h * (width / orig_w)).image = image.resize((final_w, final_h), Image.Resampling.LANCZOS).
- Elif
height > 0 and width == 0:final_h = height.final_w = int(orig_w * (height / orig_h)).image = image.resize((final_w, final_h), Image.Resampling.LANCZOS).
- Else (both non-zero):
scale = max(width / orig_w, height / orig_h).new_w = int(orig_w * scale).new_h = int(orig_h * scale).image = image.resize((new_w, new_h), Image.Resampling.LANCZOS).- Compute crop box:
left = (new_w - width) // 2top = (new_h - height) // 2right = left + widthbottom = top + height
image = image.crop((left, top, right, bottom)).final_w, final_h = width, height.
- If
- Convert to NumPy array:
np_image = np.array(image).astype(np.float32) / 255.0
- Convert to Torch tensor:
tensor = torch.from_numpy(np_image)[None,]# shape [1, H, W, 3]
- Append to
tensors.
- Open with
Batch Assembly
- If
len(tensors) == 1:image_batch = tensors[0]
- Else:
image_batch = torch.cat(tensors, dim=0)
Return
- Return
(image_batch, output_path, final_w, final_h, len(selected_files)).
2.4 Change Detection
- Implement
IS_CHANGED(self, directory, direction, modality, frame, width, height):- Compute
target_diras above. - If frame is empty, hash the sorted filenames list + mtime.
- If frame is specified, hash the single file content.
- Include
widthandheightin the hash (so resizing triggers re-execution).
- Compute
Phase 3: Frontend Extension
Goal: Provide an enhanced UX for the directory combo widget (typing to filter / autocomplete).
-
Create
js/compass_image_loader.js- Import
appfromscripts/app.js. - Import
apifromscripts/api.js(for event listeners if needed).
- Import
-
Register Extension
- Call
app.registerExtension({ name: "CompassImageLoader", ... }).
- Call
-
Override
beforeRegisterNodeDef- Check
passedNodeData.name === "CompassImageLoader". - Save reference to original
onNodeCreated. - Replace with a wrapper that:
- Calls the original
onNodeCreated. - Finds the
directorywidget by name. - Stores the original full list of combo values on the widget instance (e.g.,
widget._all_values). - Attaches a keyup/input listener to the widget's DOM element (
widget.inputElor equivalent) to implement filtering. - When the user types, filter
_all_valuescase-insensitively and updatewidget.options.values. - If the current
widget.valueis not in the filtered list, leave it as-is (allow free-form or preserve selection).
- Calls the original
- Check
-
Handle Refresh
- Listen for
fresh-node-defsevent fromapi. - On event, iterate over all nodes in
app.graph._nodes. - For each
CompassImageLoadernode:- Find the
directorywidget. - Update
widget._all_valuesandwidget.options.valueswith the new list from the refreshed node definition. - If the currently selected value still exists in the new list, keep it.
- If it does not exist, clear it to
""(or first available).
- Find the
- Listen for
-
State Preservation
- Ensure the directory value is correctly serialized in the workflow JSON (ComfyUI handles this automatically for widgets, but verify the filtered list does not override the saved value on
configure).
- Ensure the directory value is correctly serialized in the workflow JSON (ComfyUI handles this automatically for widgets, but verify the filtered list does not override the saved value on
Phase 4: Integration & Packaging
- Verify file layout:
ComfyUI/custom_nodes/ComfyUI-compass-paths/ __init__.py compass_image_loader.py js/ compass_image_loader.js - Ensure
__init__.pyis minimal and correct. - Restart ComfyUI and verify the node appears in the node search under
image/loaders.
Phase 5: Testing & Validation
Run the following manual tests in the ComfyUI interface.
-
Test 1: Single Frame Load
- Set
directory,direction=s,modality=depth,frame=0,width=0,height=0. - Queue. Verify output
IMAGEshape is[1, H, W, 3],frame_count=1,pathis the full file path.
- Set
-
Test 2: Batch Load
- Leave
frameblank. - Queue. Verify
IMAGEshape is[N, H, W, 3]whereN > 1,frame_count=N,pathis the directory path.
- Leave
-
Test 3: Cover + Crop Resize
- Set
width=512,height=768on a non-square source image. - Verify output
width=512,height=768, and image appears center-cropped.
- Set
-
Test 4: Proportional Resize (Width Only)
- Set
width=512,height=0. - Verify output width=512 and height is proportionally scaled (no cropping).
- Set
-
Test 5: Proportional Resize (Height Only)
- Set
width=0,height=768. - Verify output height=768 and width is proportionally scaled (no cropping).
- Set
-
Test 6: No Direction
- Set
directionto blank. - Verify node loads from
input/<directory>/<modality>/(no direction folder).
- Set
-
Test 7: Error — Missing Directory
- Set
directoryto a non-existent path. - Queue. Verify a clear
RuntimeErroris raised.
- Set
-
Test 8: Error — Empty Image Folder
- Point to a valid directory/modality path that has no images.
- Queue. Verify a clear
RuntimeErroris raised.
-
Test 9: Error — Out of Bounds Frame
- Set
frameto a number larger than the number of images minus one. - Queue. Verify a clear
RuntimeErroris raised.
- Set
-
Test 10: Refresh Updates Combo
- Add a new valid folder under
input/while ComfyUI is running. - Click the ComfyUI Refresh button.
- Open the node's
directorydropdown. Verify the new folder appears.
- Add a new valid folder under
-
Test 11: Autocomplete Filtering
- Click the
directorycombo and type a substring (e.g.,walk). - Verify the dropdown filters to show only matching entries.
- Click the
Future Enhancements (Out of Scope for Initial Implementation)
- Support for recursive image loading across subdirectories.
- Caching of directory scans to improve performance on very large
input/trees. - Support for additional modalities beyond
image,depth,openposevia a configuration file. - Drag-and-drop folder path into the directory widget.