Created
April 2, 2025 15:30
-
-
Save imaurer/85c937514664cf1183b50af55cd88596 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env -S uv --quiet run --script | |
# /// script | |
# requires-python = ">=3.11" | |
# dependencies = [ | |
# "pyahocorasick", | |
# ] | |
# /// | |
""" | |
Icon Slicer Script | |
Processes an input image by converting its background to transparent using ImageMagick | |
and then slicing it into individual icons based on contiguous non-transparent regions. | |
Requirements: | |
- Python 3.11+ | |
- opencv-python | |
- numpy | |
- ImageMagick (the "magick" command must be in your PATH) | |
""" | |
import cv2 | |
import numpy as np | |
import subprocess | |
import sys | |
from pathlib import Path | |
import argparse | |
import shutil | |
def check_imagemagick(): | |
"""Check if the ImageMagick 'magick' command is available.""" | |
if shutil.which("magick") is None: | |
sys.exit( | |
"Error: ImageMagick 'magick' command not found. Please install ImageMagick and ensure it's in your PATH." | |
) | |
def process_image( | |
input_path: Path, | |
output_folder: Path, | |
fuzz: str = "5%", | |
min_area: int = 100, | |
dilation_size: int = 3, | |
): | |
""" | |
Processes an input image: makes its background transparent, saves the transparent version, | |
and slices it into individual icons based on opaque regions. | |
Args: | |
input_path (Path): Path to the input image file. | |
output_folder (Path): Path to the folder where output files will be saved. | |
fuzz (str): Fuzz factor for background color matching (default: "5%"). | |
min_area (int): Minimum area (in pixels) for a contour to be considered an icon (default: 100). | |
dilation_size (int): Kernel size for the dilation operation (default: 3). | |
""" | |
output_folder.mkdir(parents=True, exist_ok=True) | |
# Read the input image | |
img = cv2.imread(str(input_path)) | |
if img is None: | |
raise FileNotFoundError(f"Image not found at {input_path}") | |
# Get the background color from the top-left pixel (BGR format) | |
b, g, r = img[0, 0] | |
color = f"rgb({r},{g},{b})" # Convert to RGB for ImageMagick | |
# Define output path for the full transparent image | |
transparent_path = output_folder / "transparent_full.png" | |
# Use ImageMagick to make the background transparent | |
try: | |
subprocess.run( | |
[ | |
"magick", | |
str(input_path), | |
"-fuzz", | |
fuzz, | |
"-transparent", | |
color, | |
str(transparent_path), | |
], | |
check=True, | |
) | |
except subprocess.CalledProcessError as e: | |
raise RuntimeError("ImageMagick command failed") from e | |
# Read the transparent image with the alpha channel | |
transparent_img = cv2.imread(str(transparent_path), cv2.IMREAD_UNCHANGED) | |
if transparent_img is None: | |
raise FileNotFoundError(f"Transparent image not found at {transparent_path}") | |
if transparent_img.shape[2] < 4: | |
raise ValueError( | |
"The processed image does not contain an alpha channel. Background transparency may have failed." | |
) | |
# Extract the alpha channel and create a binary mask (opaque = 255, transparent = 0) | |
alpha = transparent_img[:, :, 3] | |
_, binary = cv2.threshold(alpha, 1, 255, cv2.THRESH_BINARY) | |
# Dilate the mask to connect nearby opaque regions | |
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (dilation_size, dilation_size)) | |
dilated = cv2.dilate(binary, kernel, iterations=1) | |
# Find contours of the opaque regions | |
contours, _ = cv2.findContours(dilated, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) | |
# Sort contours top-to-bottom, then left-to-right | |
contours = sorted( | |
contours, key=lambda c: (cv2.boundingRect(c)[1], cv2.boundingRect(c)[0]) | |
) | |
# Crop and save each detected icon | |
icon_count = 1 | |
for cnt in contours: | |
x, y, w, h = cv2.boundingRect(cnt) | |
if w * h >= min_area: | |
cropped = transparent_img[y : y + h, x : x + w] | |
output_filename = output_folder / f"icon_{icon_count:02d}.png" | |
cv2.imwrite(str(output_filename), cropped) | |
icon_count += 1 | |
return transparent_path, icon_count - 1 | |
def parse_args(): | |
"""Parse command-line arguments.""" | |
parser = argparse.ArgumentParser( | |
description="Process an image to make its background transparent and slice it into icons." | |
) | |
parser.add_argument("input", type=Path, help="Path to the input image file") | |
parser.add_argument("output", type=Path, help="Path to the output folder") | |
parser.add_argument( | |
"--fuzz", default="5%", help="Fuzz factor for ImageMagick (default: 5%)" | |
) | |
parser.add_argument( | |
"--min-area", | |
type=int, | |
default=100, | |
help="Minimum area for a contour to be considered an icon (default: 100)", | |
) | |
parser.add_argument( | |
"--dilation-size", | |
type=int, | |
default=3, | |
help="Kernel size for dilation (default: 3)", | |
) | |
return parser.parse_args() | |
def main(): | |
args = parse_args() | |
check_imagemagick() | |
try: | |
transparent_path, icon_count = process_image( | |
args.input, args.output, args.fuzz, args.min_area, args.dilation_size | |
) | |
print(f"Processed image saved to {transparent_path}") | |
print(f"Extracted {icon_count} icon(s) in {args.output}") | |
except Exception as e: | |
print(f"Error: {e}", file=sys.stderr) | |
sys.exit(1) | |
if __name__ == "__main__": | |
main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment