diff --git a/README.md b/README.md index ee3d2ca..ce6fd5a 100644 --- a/README.md +++ b/README.md @@ -2,17 +2,32 @@ A ComfyUI custom node for converting images into IRC art by processing them in pixel blocks and generating both image and text outputs with optimal color selection from a 99-color IRC palette. -This node is used with [aibird](https://github.com/birdneststream/aibird) to convert images to IRC art. +This node is used with [aibird](https://github.com/birdneststream/aibird) to convert text prompts to IRC art that is scrolled in the IRC client. -![Workflow Example](workflows/screenshot.png) +## Limitations + +- As we have a limited color palette of 99 colors, at times some colors may not match perfect. Overall it matches good enough. I've found the `perceptual_weighted` followed by `weighted_manhattan` to be best. +- 16 colors don't work too well, but I've added this for legacy compatibility with some IRC clients that do not support 99 colors. +- IRC has a `512 - (ident + host)` byte limit per line. If there's too many half blocks rendered, it may go over this limit and break the display of the ASCII art. Rendering the image with a solid background color can help with this, or using fullblock mode. +- I haven't tried with many widths and heights. The current `640px x 448px` is a good fit for making ASCII art that is 80 character blocks wide and 30 character blocks hight. A smaller aspect ratio would fit a lot of half blocks better. ## Workflow -An example workflow is provided in `workflows/mircify_example_workflow.json`. At the moment this example is using qwen image lightning, however any model that is decent at making pixel ansi style art can suffice. +![Workflow Example](workflows/screenshot.png) + +An example workflow is provided in `workflows/mircify_example_workflow.json`. + +At the moment this example is using qwen image (gguf), lightning 4 step lora with RES4LYF samplers. However any model that is decent at making pixel ansi style art can suffice. + +## IRC Output + +Pasting the results of the created `.txt` file into IRC, we should be able to see the IRC art rendered in the client. + +![IRC output](workflows/screenshot_irc.png) ## Features -- **Universal Image Support**: Works with any image dimensions +- **Universal Image Support**: Works with any image dimensions, but remember the IRC 512 byte limit per line. - **Color Transfer Integration**: Uses KMeans clustering with multiple distance methods for accurate color mapping - **Block Processing**: Configurable block sizes (default 8x15 pixels) - **Half Block Mode**: Option for 8x7.5 pixel blocks for finer resolution @@ -59,7 +74,7 @@ Converts images into IRC art with both visual and text output. - **processed_image**: Visual representation of the IRC art conversion - **irc_text**: mIRC-formatted text with color codes ready for IRC clients -### IRC Text Saver (still not working too good with color matching) +### IRC Text Saver **Category**: `text/output` Saves IRC-formatted text to files for easy copying. @@ -102,7 +117,7 @@ Saves IRC-formatted text to files for easy copying. The node uses a two-stage approach: -1. **Color Transfer Phase**: Applies KMeans clustering with configurable distance methods to map the entire image to the 99-color IRC palette +1. **Color Transfer Phase**: Applies KMeans clustering with configurable distance methods to map the entire image to the IRC color palette 2. **Block Processing Phase**: Divides the color-corrected image into blocks using dominant color extraction and generates the final IRC art output ## IRC Color Palette diff --git a/src/nodes.py b/src/nodes.py index 23057af..6c3f11d 100644 --- a/src/nodes.py +++ b/src/nodes.py @@ -180,21 +180,59 @@ class IRCArtConverter: line_text = "" i = 0 while i < len(line_colors): - color_code = line_colors[i] - count = 1 + color_entry = line_colors[i] - # Count consecutive identical colors - while i + count < len(line_colors) and line_colors[i + count] == color_code: - count += 1 - - # Format color code - if color_code <= 9: - color_format = f"\x03{color_code},{color_code}" + # Check if this is half-block mode (tuple) or full-block mode (int) + if isinstance(color_entry, tuple): + # Half-block mode: (top_color, bottom_color) + top_color, bottom_color = color_entry + + if top_color == bottom_color: + # Same color - use space with background color, check for compression + count = 1 + # Count consecutive identical same-color tuples + while (i + count < len(line_colors) and + isinstance(line_colors[i + count], tuple) and + line_colors[i + count] == color_entry): + count += 1 + + # Format color code once + if top_color < 10: + color_format = f"\x03{top_color},{top_color}" + else: + color_format = f"\x03{top_color:02d},{top_color:02d}" + + line_text += color_format + (" " * count) + i += count + else: + # Different colors - use half block character (no compression for mixed colors) + if top_color < 10 and bottom_color < 10: + color_format = f"\x03{top_color},{bottom_color}" + elif top_color < 10: + color_format = f"\x03{top_color},{bottom_color:02d}" + elif bottom_color < 10: + color_format = f"\x03{top_color:02d},{bottom_color}" + else: + color_format = f"\x03{top_color:02d},{bottom_color:02d}" + line_text += color_format + "▀" + i += 1 else: - color_format = f"\x03{color_code:02d},{color_code:02d}" - - line_text += color_format + (" " * count) - i += count + # Full-block mode: single color + color_code = color_entry + count = 1 + + # Count consecutive identical colors + while i + count < len(line_colors) and not isinstance(line_colors[i + count], tuple) and line_colors[i + count] == color_code: + count += 1 + + # Format color code + if color_code < 10: + color_format = f"\x03{color_code},{color_code}" + else: + color_format = f"\x03{color_code:02d},{color_code:02d}" + + line_text += color_format + (" " * count) + i += count return line_text @@ -257,13 +295,12 @@ class IRCArtConverter: actual_block_height, width, height, use_16_colors, distance_method ) - # Apply color to image and store for text + # Apply color to image irc_color = self.IRC_COLORS[color_idx] irc_color_tensor = torch.tensor(irc_color, dtype=torch.float32) / 255.0 output[start_y:end_y, x * block_width:min((x + 1) * block_width, width), :] = irc_color_tensor - line_colors.append(color_idx + 1) # IRC codes are 1-based - # Process second half-block if it exists + # Process second half-block if it exists if y + 1 < blocks_y: color_idx2, start_y2, end_y2 = self.process_block( color_transferred_img, x, y + 1, block_width, block_height, @@ -274,6 +311,12 @@ class IRCArtConverter: irc_color2 = self.IRC_COLORS[color_idx2] irc_color_tensor2 = torch.tensor(irc_color2, dtype=torch.float32) / 255.0 output[start_y2:end_y2, x * block_width:min((x + 1) * block_width, width), :] = irc_color_tensor2 + + # Store both colors as tuple (top, bottom) + line_colors.append((color_idx, color_idx2)) + else: + # Last row, only one half-block - use same color for both + line_colors.append((color_idx, color_idx)) text_lines.append(self.format_irc_line(line_colors)) else: @@ -291,7 +334,7 @@ class IRCArtConverter: irc_color = self.IRC_COLORS[color_idx] irc_color_tensor = torch.tensor(irc_color, dtype=torch.float32) / 255.0 output[start_y:end_y, x * block_width:min((x + 1) * block_width, width), :] = irc_color_tensor - line_colors.append(color_idx + 1) # IRC codes are 1-based + line_colors.append(color_idx) # Use same index as image text_lines.append(self.format_irc_line(line_colors)) diff --git a/workflows/screenshot.png b/workflows/screenshot.png index 6e39429..36735f8 100644 Binary files a/workflows/screenshot.png and b/workflows/screenshot.png differ diff --git a/workflows/screenshot_irc.png b/workflows/screenshot_irc.png new file mode 100644 index 0000000..2a2a49c Binary files /dev/null and b/workflows/screenshot_irc.png differ