Stabilize unit facing and movement feedback
This commit is contained in:
@@ -70,9 +70,10 @@ def main() -> None:
|
||||
generic_only = "--generic-only" in sys.argv
|
||||
cleanup_only = "--cleanup-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:
|
||||
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:
|
||||
sheets = load_current_base_sheets()
|
||||
@@ -87,6 +88,10 @@ def main() -> None:
|
||||
optimize_current_unit_pngs()
|
||||
return
|
||||
|
||||
if stabilize_motion:
|
||||
stabilize_current_motion_sheets()
|
||||
return
|
||||
|
||||
reference = Image.open(REFERENCE_SHEET).convert("RGBA")
|
||||
targets = reference_targets(reference)
|
||||
generated = 0
|
||||
@@ -147,6 +152,126 @@ def optimize_current_unit_pngs() -> None:
|
||||
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:
|
||||
written = 0
|
||||
for source in sorted(UNIT_DIR.glob("unit-*.png")):
|
||||
|
||||
Reference in New Issue
Block a user