added tools
This commit is contained in:
parent
b0eb7e0751
commit
98be4b5aee
64
tools/recolor/__init__.py
Normal file
64
tools/recolor/__init__.py
Normal file
@ -0,0 +1,64 @@
|
||||
from dataclasses import dataclass
|
||||
from PIL import Image
|
||||
import importlib
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Color:
|
||||
RED: int
|
||||
GREEN: int
|
||||
BLUE: int
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ColorMask:
|
||||
target_color: Color
|
||||
replacement_color: Color
|
||||
|
||||
|
||||
def load_color_map(name: str):
|
||||
module_path = f"tools.recolor.color_maps.{name}"
|
||||
module = importlib.import_module(module_path)
|
||||
|
||||
if not hasattr(module, "COLOR_MAP"):
|
||||
raise RuntimeError(f"color map '{name}' has no COLOR_MAP")
|
||||
|
||||
return module.COLOR_MAP
|
||||
|
||||
|
||||
def recolor_image(img: Image.Image, color_map: list[ColorMask]) -> Image.Image:
|
||||
img = img.convert("RGBA")
|
||||
pixels = img.load()
|
||||
|
||||
lookup = {
|
||||
(m.target_color.RED, m.target_color.GREEN, m.target_color.BLUE):
|
||||
(m.replacement_color.RED, m.replacement_color.GREEN, m.replacement_color.BLUE)
|
||||
for m in color_map
|
||||
}
|
||||
|
||||
width, height = img.size
|
||||
|
||||
for y in range(height):
|
||||
for x in range(width):
|
||||
r, g, b, a = pixels[x, y]
|
||||
if (r, g, b) in lookup:
|
||||
nr, ng, nb = lookup[(r, g, b)]
|
||||
pixels[x, y] = (nr, ng, nb, a)
|
||||
|
||||
return img
|
||||
|
||||
|
||||
def extract_unique_colors(img: Image.Image) -> set[Color]:
|
||||
img = img.convert("RGBA")
|
||||
pixels = img.load()
|
||||
|
||||
width, height = img.size
|
||||
colors: set[Color] = set()
|
||||
|
||||
for y in range(height):
|
||||
for x in range(width):
|
||||
r, g, b, a = pixels[x, y]
|
||||
if a != 0:
|
||||
colors.add(Color(r, g, b))
|
||||
|
||||
return colors
|
||||
128
tools/recolor/__main__.py
Normal file
128
tools/recolor/__main__.py
Normal file
@ -0,0 +1,128 @@
|
||||
import argparse
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from PIL import Image
|
||||
|
||||
from . import load_color_map, recolor_image, extract_unique_colors
|
||||
|
||||
|
||||
def collect_files(dir_arg: str) -> list[Path]:
|
||||
path = Path(dir_arg)
|
||||
|
||||
if "*" in dir_arg:
|
||||
parent = path.parent
|
||||
pattern = path.name
|
||||
|
||||
if not parent.exists() or not parent.is_dir():
|
||||
print("directory does not exist", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
return [
|
||||
f for f in parent.iterdir()
|
||||
if f.is_file()
|
||||
and f.suffix.lower() == ".png"
|
||||
and f.match(pattern)
|
||||
]
|
||||
|
||||
if not path.exists() or not path.is_dir():
|
||||
print("directory does not exist", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
return [
|
||||
f for f in path.iterdir()
|
||||
if f.is_file() and f.suffix.lower() == ".png"
|
||||
]
|
||||
|
||||
|
||||
def process_file(path: Path, color_map):
|
||||
img = Image.open(path)
|
||||
img = recolor_image(img, color_map)
|
||||
img.save(path)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("-i", "--input")
|
||||
parser.add_argument("-dir", "--directory")
|
||||
parser.add_argument("-o", "--output")
|
||||
parser.add_argument("-r", "--replace", action="store_true")
|
||||
parser.add_argument("-m", "--map")
|
||||
parser.add_argument("-ext", "--extract", action="store_true")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if not args.input and not args.directory:
|
||||
print("either --input or --directory is required", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# -------- directory / glob mode --------
|
||||
if args.directory:
|
||||
files = collect_files(args.directory)
|
||||
|
||||
if args.extract:
|
||||
all_colors = set()
|
||||
|
||||
for file in files:
|
||||
img = Image.open(file)
|
||||
all_colors |= extract_unique_colors(img)
|
||||
|
||||
for c in sorted(all_colors, key=lambda x: (x.RED, x.GREEN, x.BLUE)):
|
||||
print(f"{c.RED:3}, {c.GREEN:3}, {c.BLUE:3}")
|
||||
|
||||
sys.exit(0)
|
||||
|
||||
if not args.map:
|
||||
print("color map required for directory mode", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
try:
|
||||
color_map = load_color_map(args.map)
|
||||
except Exception as e:
|
||||
print(str(e), file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
for file in files:
|
||||
process_file(file, color_map)
|
||||
|
||||
sys.exit(0)
|
||||
|
||||
# -------- single file mode --------
|
||||
input_path = Path(args.input)
|
||||
if not input_path.exists():
|
||||
print("input file does not exist", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
if args.extract:
|
||||
img = Image.open(input_path)
|
||||
colors = extract_unique_colors(img)
|
||||
|
||||
for c in sorted(colors, key=lambda x: (x.RED, x.GREEN, x.BLUE)):
|
||||
print(f"{c.RED:3}, {c.GREEN:3}, {c.BLUE:3}")
|
||||
|
||||
sys.exit(0)
|
||||
|
||||
if args.replace:
|
||||
output_path = input_path
|
||||
else:
|
||||
if not args.output:
|
||||
print("output file required unless --replace is set", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
output_path = Path(args.output)
|
||||
|
||||
if not args.map:
|
||||
print("color map required unless --extract is set", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
try:
|
||||
color_map = load_color_map(args.map)
|
||||
except Exception as e:
|
||||
print(str(e), file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
img = Image.open(input_path)
|
||||
img = recolor_image(img, color_map)
|
||||
img.save(output_path)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
35
tools/recolor/color_maps/chipper_orange.py
Normal file
35
tools/recolor/color_maps/chipper_orange.py
Normal file
@ -0,0 +1,35 @@
|
||||
from tools.recolor import Color, ColorMask
|
||||
|
||||
COLOR_MAP = [
|
||||
# ---- item/armor (teal -> orange) ----
|
||||
ColorMask(Color( 8, 37, 32), Color(100, 45, 10)),
|
||||
ColorMask(Color( 14, 62, 53), Color(130, 60, 15)),
|
||||
ColorMask(Color( 26, 168, 165), Color(160, 80, 20)),
|
||||
ColorMask(Color( 32, 195, 179), Color(190, 100, 25)),
|
||||
ColorMask(Color( 73, 234, 214), Color(220, 120, 30)),
|
||||
ColorMask(Color(159, 248, 229), Color(235, 140, 40)),
|
||||
ColorMask(Color(252, 252, 252), Color(255, 220, 180)),
|
||||
|
||||
# ---- model/armor (old teal -> orange) ----
|
||||
ColorMask(Color( 44, 224, 216), Color(130, 60, 15)),
|
||||
ColorMask(Color( 45, 196, 178), Color(160, 80, 20)),
|
||||
ColorMask(Color( 48, 208, 190), Color(190, 100, 25)),
|
||||
ColorMask(Color( 74, 237, 217), Color(220, 120, 30)),
|
||||
ColorMask(Color(107, 243, 227), Color(235, 140, 40)),
|
||||
ColorMask(Color(154, 248, 240), Color(245, 160, 60)),
|
||||
ColorMask(Color(161, 251, 232), Color(255, 180, 100)),
|
||||
ColorMask(Color(180, 253, 238), Color(255, 200, 140)),
|
||||
ColorMask(Color(229, 255, 250), Color(255, 220, 180)),
|
||||
|
||||
# ---- already orange (identity, makes it safe to rerun) ----
|
||||
ColorMask(Color(100, 45, 10), Color(100, 45, 10)),
|
||||
ColorMask(Color(130, 60, 15), Color(130, 60, 15)),
|
||||
ColorMask(Color(160, 80, 20), Color(160, 80, 20)),
|
||||
ColorMask(Color(190, 100, 25), Color(190, 100, 25)),
|
||||
ColorMask(Color(220, 120, 30), Color(220, 120, 30)),
|
||||
ColorMask(Color(235, 140, 40), Color(235, 140, 40)),
|
||||
ColorMask(Color(245, 160, 60), Color(245, 160, 60)),
|
||||
ColorMask(Color(255, 180, 100), Color(255, 180, 100)),
|
||||
ColorMask(Color(255, 200, 140), Color(255, 200, 140)),
|
||||
ColorMask(Color(255, 220, 180), Color(255, 220, 180)),
|
||||
]
|
||||
Loading…
x
Reference in New Issue
Block a user