This commit is contained in:
2026-03-09 09:22:21 -07:00
parent 8cfae8dea2
commit dd4899b8f9
29 changed files with 1014 additions and 1 deletions

212
tools/draw_polygon.py Normal file
View File

@@ -0,0 +1,212 @@
#!/usr/bin/env python3
"""Draw a polygon on an image using Pillow."""
import argparse
import sys
from pathlib import Path
try:
from PIL import Image, ImageDraw, ImageFilter
except ImportError:
print("Error: Pillow is required. Install with: pip install Pillow")
sys.exit(1)
COLOR_PALETTE = {
"red": "#FF0000",
"green": "#00FF00",
"blue": "#0000FF",
"yellow": "#FFFF00",
"cyan": "#00FFFF",
"magenta": "#FF00FF",
"white": "#FFFFFF",
"black": "#000000",
"orange": "#FFA500",
"purple": "#800080",
"brown": "#A52A2A",
"pink": "#FFC0CB",
"gray": "#808080",
"grey": "#808080",
"lime": "#00FF00",
"navy": "#000080",
"teal": "#008080",
"maroon": "#800000",
}
def parse_color(color_str: str) -> str:
"""Parse color string to hex."""
color_lower = color_str.lower()
if color_lower in COLOR_PALETTE:
return COLOR_PALETTE[color_lower]
if color_str.startswith("#") and len(color_str) in [7, 9]:
return color_str
raise ValueError(
f"Invalid color '{color_str}'. Use a name (e.g., red, blue) or hex code (e.g., #FF0000)."
)
def parse_points(points_str: str) -> list[tuple[float, float]]:
"""Parse points string into list of (x, y) tuples."""
points = []
for point in points_str.split():
if "," not in point:
raise ValueError(f"Invalid point format: '{point}'. Use 'x,y' format.")
try:
x, y = point.split(",")
points.append((float(x.strip()), float(y.strip())))
except ValueError:
raise ValueError(f"Invalid point format: '{point}'. Use 'x,y' format.")
return points
def convert_to_pixels(
points: list[tuple[float, float]], width: int, height: int, absolute: bool
) -> list[tuple[int, int]]:
"""Convert points to pixel coordinates."""
if absolute:
return [(int(x), int(y)) for x, y in points]
else:
return [
(int(x * width), int(y * height))
for x, y in points
if 0 <= x <= 1 and 0 <= y <= 1
]
def draw_polygon_on_image(
image_path: Path,
points: list[tuple[float, float]],
color: str,
thickness: int,
fill: bool,
absolute: bool,
save_path: Path | None,
) -> None:
"""Draw polygon on image."""
image = Image.open(image_path).convert("RGBA")
width, height = image.size
pixel_points = convert_to_pixels(points, width, height, absolute)
if len(pixel_points) < 3:
raise ValueError("Need at least 3 points to draw a polygon.")
draw = ImageDraw.Draw(image)
hex_color = parse_color(color)
draw.polygon(pixel_points, fill=hex_color if fill else None, outline=hex_color, width=thickness)
if fill:
alpha = 128
fill_color = hex_color[:7] + f"{alpha:02X}"
draw.polygon(pixel_points, fill=fill_color, outline=hex_color, width=thickness)
if save_path:
image.save(save_path)
print(f"Saved: {save_path}")
else:
image.show()
def main():
parser = argparse.ArgumentParser(
description="Draw a polygon on an image using Pillow.",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
# Draw a triangle using percentages (default)
python draw_polygon.py scene.png "0.5,0.1 0.9,0.9 0.1,0.9"
# Draw with pixel coordinates (absolute mode)
python draw_polygon.py scene.png "100,50 400,50 250,300" --absolute
# Save to file with blue color
python draw_polygon.py scene.png "0.2,0.2 0.8,0.2 0.8,0.8 0.2,0.8" --color blue --save output.png
# Fill polygon semi-transparently
python draw_polygon.py scene.png "0.3,0.3 0.7,0.3 0.7,0.7 0.3,0.7" --fill --color red
# Draw with custom hex color and thickness
python draw_polygon.py scene.png "0.1,0.1 0.9,0.1 0.9,0.9 0.1,0.9" --color #00FF00 --thickness 5
Coordinate Formats:
Percentage (default): 0.0 to 1.0, where 0.0,0.0 is top-left and 1.0,1.0 is bottom-right
Absolute (with --absolute): Actual pixel coordinates from 0 to image width/height
Color Formats:
Named colors: red, green, blue, yellow, cyan, magenta, white, black, orange, purple, brown, pink, gray, grey, lime, navy, teal, maroon
Hex codes: #FF0000, #00FF00FF, etc.
""",
)
parser.add_argument(
"image",
type=Path,
help="Path to input image file",
)
parser.add_argument(
"points",
type=str,
help="Space-separated list of x,y coordinates (e.g., '0.1,0.1 0.9,0.1 0.5,0.9'). Use percentages (0.0-1.0) by default, or --absolute for pixel coordinates.",
)
parser.add_argument(
"--color",
type=str,
default="red",
help="Polygon color (default: red). Use named colors (red, blue, green, etc.) or hex codes (#FF0000).",
)
parser.add_argument(
"--absolute",
action="store_true",
help="Use pixel coordinates instead of percentages",
)
parser.add_argument(
"--save",
type=Path,
default=None,
help="Save output to file path instead of displaying",
)
parser.add_argument(
"--thickness",
type=int,
default=2,
help="Line thickness in pixels (default: 2)",
)
parser.add_argument(
"--fill",
action="store_true",
help="Fill polygon with semi-transparent color",
)
args = parser.parse_args()
if not args.image.exists():
print(f"Error: Image not found: {args.image}", file=sys.stderr)
sys.exit(1)
try:
points = parse_points(args.points)
if len(points) < 3:
print(f"Error: Need at least 3 points. Got {len(points)}.", file=sys.stderr)
sys.exit(1)
draw_polygon_on_image(
image_path=args.image,
points=points,
color=args.color,
thickness=args.thickness,
fill=args.fill,
absolute=args.absolute,
save_path=args.save,
)
except ValueError as e:
print(f"Error: {e}", file=sys.stderr)
sys.exit(1)
except Exception as e:
print(f"Error: {e}", file=sys.stderr)
sys.exit(1)
if __name__ == "__main__":
main()

View File

@@ -125,7 +125,7 @@ def extract_mask(
with open(workflow_path, "r") as f:
workflow = json.load(f)
prompt_text = f"Create a black and white alpha mask of {subject}"
prompt_text = f"Create a black and white alpha mask of {subject}, leaving everything else black"
print(f"Encoding input image...")
base64_image = encode_image_base64(input_image)

154
tools/make_ora.py Normal file
View File

@@ -0,0 +1,154 @@
import argparse
import os
import zipfile
import tempfile
import shutil
import numpy as np
from PIL import Image
import xml.etree.ElementTree as ET
NUM_LAYERS = 4
MASKS_PER_LAYER = 3
NOISE_SCALES = [4, 16, 64]
def noise_mask(w, h, scale):
sw = max(1, w // scale)
sh = max(1, h // scale)
noise = np.random.rand(sh, sw)
img = Image.fromarray((noise * 255).astype(np.uint8), "L")
img = img.resize((w, h), Image.BILINEAR)
return img
def build_stack_xml(w, h, groups):
image = ET.Element(
"image",
{"version": "0.0.3", "w": str(w), "h": str(h)}
)
root_stack = ET.SubElement(image, "stack")
for group in groups:
group_el = ET.SubElement(
root_stack,
"stack",
{"name": group["name"]}
)
for layer in group["layers"]:
ET.SubElement(
group_el,
"layer",
{
"name": layer["name"],
"src": layer["src"],
"opacity": "1.0"
}
)
return ET.tostring(image, encoding="utf-8", xml_declaration=True)
def build_ora(input_png, output_ora):
base = Image.open(input_png).convert("RGBA")
w, h = base.size
tmp = tempfile.mkdtemp()
try:
data_dir = os.path.join(tmp, "data")
thumb_dir = os.path.join(tmp, "Thumbnails")
os.makedirs(data_dir)
os.makedirs(thumb_dir)
groups = []
for i in range(NUM_LAYERS):
group_layers = []
base_path = f"data/layer_{i}.png"
base.save(os.path.join(tmp, base_path))
group_layers.append({
"name": "base",
"src": base_path
})
for j in range(MASKS_PER_LAYER):
mask = noise_mask(w, h, NOISE_SCALES[j])
mask_path = f"data/layer_{i}_mask_{j}.png"
mask.save(os.path.join(tmp, mask_path))
group_layers.append({
"name": f"mask {j}",
"src": mask_path
})
groups.append({
"name": f"group {i}",
"layers": group_layers
})
stack_xml = build_stack_xml(w, h, groups)
with open(os.path.join(tmp, "stack.xml"), "wb") as f:
f.write(stack_xml)
with open(os.path.join(tmp, "mimetype"), "w") as f:
f.write("image/openraster")
base.save(os.path.join(tmp, "mergedimage.png"))
thumb = base.copy()
thumb.thumbnail((256, 256))
thumb.save(os.path.join(thumb_dir, "thumbnail.png"))
with zipfile.ZipFile(output_ora, "w") as z:
z.write(
os.path.join(tmp, "mimetype"),
"mimetype",
compress_type=zipfile.ZIP_STORED
)
for root, _, files in os.walk(tmp):
for file in files:
if file == "mimetype":
continue
full = os.path.join(root, file)
rel = os.path.relpath(full, tmp)
z.write(full, rel)
finally:
shutil.rmtree(tmp)
def main():
parser = argparse.ArgumentParser(
description="Generate ORA with grouped layers and mask layers"
)
parser.add_argument("input_png")
parser.add_argument("output_ora")
args = parser.parse_args()
build_ora(args.input_png, args.output_ora)
if __name__ == "__main__":
main()

View File

@@ -1,3 +1,4 @@
opencv-python
numpy
shapely
Pillow>=10.0.0