Stabilize unit facing and movement feedback
@@ -70,9 +70,10 @@ def main() -> None:
|
|||||||
generic_only = "--generic-only" in sys.argv
|
generic_only = "--generic-only" in sys.argv
|
||||||
cleanup_only = "--cleanup-only" in sys.argv
|
cleanup_only = "--cleanup-only" in sys.argv
|
||||||
optimize_only = "--optimize-only" in sys.argv
|
optimize_only = "--optimize-only" in sys.argv
|
||||||
selected_modes = sum(1 for enabled in (generic_only, cleanup_only, optimize_only) if enabled)
|
stabilize_motion = "--stabilize-motion" in sys.argv
|
||||||
|
selected_modes = sum(1 for enabled in (generic_only, cleanup_only, optimize_only, stabilize_motion) if enabled)
|
||||||
if selected_modes > 1:
|
if selected_modes > 1:
|
||||||
raise ValueError("Use only one of --generic-only, --cleanup-only, or --optimize-only")
|
raise ValueError("Use only one of --generic-only, --cleanup-only, --optimize-only, or --stabilize-motion")
|
||||||
|
|
||||||
if generic_only:
|
if generic_only:
|
||||||
sheets = load_current_base_sheets()
|
sheets = load_current_base_sheets()
|
||||||
@@ -87,6 +88,10 @@ def main() -> None:
|
|||||||
optimize_current_unit_pngs()
|
optimize_current_unit_pngs()
|
||||||
return
|
return
|
||||||
|
|
||||||
|
if stabilize_motion:
|
||||||
|
stabilize_current_motion_sheets()
|
||||||
|
return
|
||||||
|
|
||||||
reference = Image.open(REFERENCE_SHEET).convert("RGBA")
|
reference = Image.open(REFERENCE_SHEET).convert("RGBA")
|
||||||
targets = reference_targets(reference)
|
targets = reference_targets(reference)
|
||||||
generated = 0
|
generated = 0
|
||||||
@@ -147,6 +152,126 @@ def optimize_current_unit_pngs() -> None:
|
|||||||
print(f"Optimized {optimized} unit PNGs with {PALETTE_COLORS}-color palettes")
|
print(f"Optimized {optimized} unit PNGs with {PALETTE_COLORS}-color palettes")
|
||||||
|
|
||||||
|
|
||||||
|
def stabilize_current_motion_sheets() -> None:
|
||||||
|
reference = Image.open(REFERENCE_SHEET).convert("RGBA")
|
||||||
|
targets = stable_reference_targets(reference_targets(reference))
|
||||||
|
reference_east_score = side_orientation_score(crop_frame(reference, 1, 0))
|
||||||
|
generated_sheets: dict[str, Image.Image] = {}
|
||||||
|
stabilized = 0
|
||||||
|
|
||||||
|
for source in sorted(UNIT_DIR.glob("unit-*.png")):
|
||||||
|
if source.name.endswith("-actions.png"):
|
||||||
|
continue
|
||||||
|
|
||||||
|
sheet = Image.open(source).convert("RGBA")
|
||||||
|
if sheet.size != (FRAME_SIZE * GRID_SIZE, FRAME_SIZE * GRID_SIZE):
|
||||||
|
raise ValueError(f"{source.name} has unexpected size {sheet.size}")
|
||||||
|
|
||||||
|
output = stabilize_motion_sheet(sheet, source.stem, targets, reference_east_score)
|
||||||
|
save_unit_png(output, source)
|
||||||
|
generated_sheets[source.stem] = output
|
||||||
|
stabilized += 1
|
||||||
|
|
||||||
|
write_template_variants(generated_sheets)
|
||||||
|
print(f"Stabilized directional motion in {stabilized} unit sheets")
|
||||||
|
|
||||||
|
|
||||||
|
def stable_reference_targets(targets: list[list[FrameTarget]]) -> list[list[FrameTarget]]:
|
||||||
|
stable_rows: list[list[FrameTarget]] = []
|
||||||
|
for row_targets in targets:
|
||||||
|
width = round(sum(target.width for target in row_targets) / len(row_targets))
|
||||||
|
height = round(sum(target.height for target in row_targets) / len(row_targets))
|
||||||
|
stable_rows.append(
|
||||||
|
[
|
||||||
|
FrameTarget(
|
||||||
|
center_x=target.center_x,
|
||||||
|
bottom_y=target.bottom_y,
|
||||||
|
width=width,
|
||||||
|
height=height,
|
||||||
|
)
|
||||||
|
for target in row_targets
|
||||||
|
]
|
||||||
|
)
|
||||||
|
return stable_rows
|
||||||
|
|
||||||
|
|
||||||
|
def stabilize_motion_sheet(
|
||||||
|
sheet: Image.Image,
|
||||||
|
stem: str,
|
||||||
|
targets: list[list[FrameTarget]],
|
||||||
|
reference_east_score: float,
|
||||||
|
) -> Image.Image:
|
||||||
|
style = style_for_unit(stem)
|
||||||
|
sources = {
|
||||||
|
0: canonical_row_frame(sheet, 0),
|
||||||
|
1: oriented_east_frame(canonical_row_frame(sheet, 1), reference_east_score),
|
||||||
|
2: canonical_row_frame(sheet, 2),
|
||||||
|
}
|
||||||
|
sources[3] = ImageOps.mirror(sources[1])
|
||||||
|
|
||||||
|
output = Image.new("RGBA", sheet.size, (0, 0, 0, 0))
|
||||||
|
for row in range(GRID_SIZE):
|
||||||
|
source = sources[row]
|
||||||
|
for col in range(GRID_SIZE):
|
||||||
|
frame = normalize_frame(source, targets[row][col], style)
|
||||||
|
output.alpha_composite(frame, (col * FRAME_SIZE, row * FRAME_SIZE))
|
||||||
|
return output
|
||||||
|
|
||||||
|
|
||||||
|
def canonical_row_frame(sheet: Image.Image, row: int) -> Image.Image:
|
||||||
|
preferred = remove_detached_artifacts(clean_alpha(crop_frame(sheet, row, 0)))
|
||||||
|
preferred_area = alpha_area(preferred)
|
||||||
|
candidates = [remove_detached_artifacts(clean_alpha(crop_frame(sheet, row, col))) for col in range(GRID_SIZE)]
|
||||||
|
best = max(candidates, key=alpha_area)
|
||||||
|
best_area = alpha_area(best)
|
||||||
|
if preferred_area >= max(1, best_area) * 0.55:
|
||||||
|
return preferred
|
||||||
|
return best
|
||||||
|
|
||||||
|
|
||||||
|
def oriented_east_frame(frame: Image.Image, reference_east_score: float) -> Image.Image:
|
||||||
|
score = side_orientation_score(frame)
|
||||||
|
if abs(score) < 1.25 or abs(reference_east_score) < 1.25:
|
||||||
|
return frame
|
||||||
|
return ImageOps.mirror(frame) if score * reference_east_score < 0 else frame
|
||||||
|
|
||||||
|
|
||||||
|
def alpha_area(image: Image.Image) -> int:
|
||||||
|
alpha = image.getchannel("A")
|
||||||
|
histogram = alpha.histogram()
|
||||||
|
return sum(count for value, count in enumerate(histogram) if value > 12)
|
||||||
|
|
||||||
|
|
||||||
|
def side_orientation_score(image: Image.Image) -> float:
|
||||||
|
image = remove_detached_artifacts(clean_alpha(image))
|
||||||
|
bbox = alpha_bbox(image)
|
||||||
|
if not bbox:
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
pixels = image.load()
|
||||||
|
left, top, right, bottom = bbox
|
||||||
|
center_x = (left + right) / 2
|
||||||
|
upper_bottom = top + round((bottom - top) * 0.68)
|
||||||
|
weighted_offset = 0.0
|
||||||
|
weight_total = 0.0
|
||||||
|
|
||||||
|
for y in range(top, upper_bottom):
|
||||||
|
for x in range(left, right):
|
||||||
|
red, green, blue, alpha = pixels[x, y]
|
||||||
|
if alpha <= 24:
|
||||||
|
continue
|
||||||
|
brightness = (red + green + blue) / 3
|
||||||
|
warmth = red * 1.1 + green * 0.45 - blue * 0.45
|
||||||
|
weight = max(0.0, brightness - 38) + max(0.0, warmth - 58) * 0.34
|
||||||
|
if weight <= 0:
|
||||||
|
continue
|
||||||
|
weight *= alpha / 255
|
||||||
|
weighted_offset += (x - center_x) * weight
|
||||||
|
weight_total += weight
|
||||||
|
|
||||||
|
return weighted_offset / weight_total if weight_total else 0.0
|
||||||
|
|
||||||
|
|
||||||
def write_template_variants(sheets: dict[str, Image.Image]) -> None:
|
def write_template_variants(sheets: dict[str, Image.Image]) -> None:
|
||||||
written = 0
|
written = 0
|
||||||
for source in sorted(UNIT_DIR.glob("unit-*.png")):
|
for source in sorted(UNIT_DIR.glob("unit-*.png")):
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 293 KiB After Width: | Height: | Size: 220 KiB |
|
Before Width: | Height: | Size: 279 KiB After Width: | Height: | Size: 100 KiB |
|
Before Width: | Height: | Size: 330 KiB After Width: | Height: | Size: 240 KiB |
|
Before Width: | Height: | Size: 340 KiB After Width: | Height: | Size: 108 KiB |
|
Before Width: | Height: | Size: 376 KiB After Width: | Height: | Size: 277 KiB |
|
Before Width: | Height: | Size: 381 KiB After Width: | Height: | Size: 120 KiB |
|
Before Width: | Height: | Size: 294 KiB After Width: | Height: | Size: 237 KiB |
|
Before Width: | Height: | Size: 259 KiB After Width: | Height: | Size: 102 KiB |
|
Before Width: | Height: | Size: 374 KiB After Width: | Height: | Size: 260 KiB |
|
Before Width: | Height: | Size: 392 KiB After Width: | Height: | Size: 122 KiB |
|
Before Width: | Height: | Size: 354 KiB After Width: | Height: | Size: 277 KiB |
|
Before Width: | Height: | Size: 355 KiB After Width: | Height: | Size: 120 KiB |
|
Before Width: | Height: | Size: 348 KiB After Width: | Height: | Size: 298 KiB |
|
Before Width: | Height: | Size: 349 KiB After Width: | Height: | Size: 122 KiB |
|
Before Width: | Height: | Size: 345 KiB After Width: | Height: | Size: 257 KiB |
|
Before Width: | Height: | Size: 350 KiB After Width: | Height: | Size: 114 KiB |
|
Before Width: | Height: | Size: 266 KiB After Width: | Height: | Size: 201 KiB |
|
Before Width: | Height: | Size: 249 KiB After Width: | Height: | Size: 88 KiB |
|
Before Width: | Height: | Size: 357 KiB After Width: | Height: | Size: 283 KiB |
|
Before Width: | Height: | Size: 359 KiB After Width: | Height: | Size: 124 KiB |
|
Before Width: | Height: | Size: 343 KiB After Width: | Height: | Size: 270 KiB |
|
Before Width: | Height: | Size: 354 KiB After Width: | Height: | Size: 118 KiB |
|
Before Width: | Height: | Size: 400 KiB After Width: | Height: | Size: 289 KiB |
|
Before Width: | Height: | Size: 408 KiB After Width: | Height: | Size: 123 KiB |
|
Before Width: | Height: | Size: 365 KiB After Width: | Height: | Size: 278 KiB |
|
Before Width: | Height: | Size: 369 KiB After Width: | Height: | Size: 124 KiB |
|
Before Width: | Height: | Size: 354 KiB After Width: | Height: | Size: 234 KiB |
|
Before Width: | Height: | Size: 355 KiB After Width: | Height: | Size: 105 KiB |
|
Before Width: | Height: | Size: 363 KiB After Width: | Height: | Size: 272 KiB |
|
Before Width: | Height: | Size: 366 KiB After Width: | Height: | Size: 116 KiB |
|
Before Width: | Height: | Size: 318 KiB After Width: | Height: | Size: 242 KiB |
|
Before Width: | Height: | Size: 318 KiB After Width: | Height: | Size: 242 KiB |
|
Before Width: | Height: | Size: 287 KiB After Width: | Height: | Size: 105 KiB |
|
Before Width: | Height: | Size: 318 KiB After Width: | Height: | Size: 242 KiB |
|
Before Width: | Height: | Size: 287 KiB After Width: | Height: | Size: 105 KiB |
|
Before Width: | Height: | Size: 287 KiB After Width: | Height: | Size: 105 KiB |
|
Before Width: | Height: | Size: 359 KiB After Width: | Height: | Size: 275 KiB |
|
Before Width: | Height: | Size: 359 KiB After Width: | Height: | Size: 275 KiB |
|
Before Width: | Height: | Size: 361 KiB After Width: | Height: | Size: 118 KiB |
|
Before Width: | Height: | Size: 359 KiB After Width: | Height: | Size: 275 KiB |
|
Before Width: | Height: | Size: 361 KiB After Width: | Height: | Size: 118 KiB |
|
Before Width: | Height: | Size: 361 KiB After Width: | Height: | Size: 118 KiB |
|
Before Width: | Height: | Size: 353 KiB After Width: | Height: | Size: 238 KiB |
|
Before Width: | Height: | Size: 353 KiB After Width: | Height: | Size: 238 KiB |
|
Before Width: | Height: | Size: 351 KiB After Width: | Height: | Size: 102 KiB |
|
Before Width: | Height: | Size: 353 KiB After Width: | Height: | Size: 238 KiB |
|
Before Width: | Height: | Size: 351 KiB After Width: | Height: | Size: 102 KiB |
|
Before Width: | Height: | Size: 351 KiB After Width: | Height: | Size: 102 KiB |
|
Before Width: | Height: | Size: 353 KiB After Width: | Height: | Size: 260 KiB |
|
Before Width: | Height: | Size: 369 KiB After Width: | Height: | Size: 121 KiB |
|
Before Width: | Height: | Size: 319 KiB After Width: | Height: | Size: 242 KiB |
|
Before Width: | Height: | Size: 277 KiB After Width: | Height: | Size: 214 KiB |
|
Before Width: | Height: | Size: 324 KiB After Width: | Height: | Size: 251 KiB |
|
Before Width: | Height: | Size: 307 KiB After Width: | Height: | Size: 107 KiB |
|
Before Width: | Height: | Size: 316 KiB After Width: | Height: | Size: 246 KiB |
|
Before Width: | Height: | Size: 305 KiB After Width: | Height: | Size: 106 KiB |
|
Before Width: | Height: | Size: 248 KiB After Width: | Height: | Size: 93 KiB |
|
Before Width: | Height: | Size: 295 KiB After Width: | Height: | Size: 247 KiB |
|
Before Width: | Height: | Size: 288 KiB After Width: | Height: | Size: 245 KiB |
|
Before Width: | Height: | Size: 290 KiB After Width: | Height: | Size: 103 KiB |
|
Before Width: | Height: | Size: 274 KiB After Width: | Height: | Size: 106 KiB |
|
Before Width: | Height: | Size: 322 KiB After Width: | Height: | Size: 239 KiB |
|
Before Width: | Height: | Size: 333 KiB After Width: | Height: | Size: 273 KiB |
|
Before Width: | Height: | Size: 321 KiB After Width: | Height: | Size: 114 KiB |
|
Before Width: | Height: | Size: 292 KiB After Width: | Height: | Size: 106 KiB |
|
Before Width: | Height: | Size: 342 KiB After Width: | Height: | Size: 267 KiB |
|
Before Width: | Height: | Size: 329 KiB After Width: | Height: | Size: 116 KiB |
|
Before Width: | Height: | Size: 284 KiB After Width: | Height: | Size: 104 KiB |
|
Before Width: | Height: | Size: 278 KiB After Width: | Height: | Size: 221 KiB |
|
Before Width: | Height: | Size: 278 KiB After Width: | Height: | Size: 221 KiB |
|
Before Width: | Height: | Size: 251 KiB After Width: | Height: | Size: 95 KiB |
|
Before Width: | Height: | Size: 278 KiB After Width: | Height: | Size: 221 KiB |
|
Before Width: | Height: | Size: 251 KiB After Width: | Height: | Size: 95 KiB |
|
Before Width: | Height: | Size: 251 KiB After Width: | Height: | Size: 95 KiB |
|
Before Width: | Height: | Size: 393 KiB After Width: | Height: | Size: 284 KiB |
|
Before Width: | Height: | Size: 393 KiB After Width: | Height: | Size: 284 KiB |
|
Before Width: | Height: | Size: 409 KiB After Width: | Height: | Size: 124 KiB |
|
Before Width: | Height: | Size: 393 KiB After Width: | Height: | Size: 284 KiB |
|
Before Width: | Height: | Size: 409 KiB After Width: | Height: | Size: 124 KiB |
|
Before Width: | Height: | Size: 409 KiB After Width: | Height: | Size: 124 KiB |
|
Before Width: | Height: | Size: 319 KiB After Width: | Height: | Size: 246 KiB |
|
Before Width: | Height: | Size: 319 KiB After Width: | Height: | Size: 246 KiB |
|
Before Width: | Height: | Size: 285 KiB After Width: | Height: | Size: 107 KiB |
|
Before Width: | Height: | Size: 319 KiB After Width: | Height: | Size: 246 KiB |
|
Before Width: | Height: | Size: 285 KiB After Width: | Height: | Size: 107 KiB |
|
Before Width: | Height: | Size: 285 KiB After Width: | Height: | Size: 107 KiB |
|
Before Width: | Height: | Size: 293 KiB After Width: | Height: | Size: 234 KiB |
|
Before Width: | Height: | Size: 293 KiB After Width: | Height: | Size: 234 KiB |
|
Before Width: | Height: | Size: 257 KiB After Width: | Height: | Size: 102 KiB |
|
Before Width: | Height: | Size: 293 KiB After Width: | Height: | Size: 234 KiB |
|
Before Width: | Height: | Size: 257 KiB After Width: | Height: | Size: 102 KiB |
|
Before Width: | Height: | Size: 257 KiB After Width: | Height: | Size: 102 KiB |
|
Before Width: | Height: | Size: 305 KiB After Width: | Height: | Size: 233 KiB |
|
Before Width: | Height: | Size: 305 KiB After Width: | Height: | Size: 233 KiB |
|
Before Width: | Height: | Size: 274 KiB After Width: | Height: | Size: 103 KiB |
|
Before Width: | Height: | Size: 305 KiB After Width: | Height: | Size: 233 KiB |
|
Before Width: | Height: | Size: 274 KiB After Width: | Height: | Size: 103 KiB |
|
Before Width: | Height: | Size: 274 KiB After Width: | Height: | Size: 103 KiB |
|
Before Width: | Height: | Size: 357 KiB After Width: | Height: | Size: 234 KiB |