mirror of
https://github.com/dbalsom/x86_microcode.git
synced 2026-06-09 13:04:17 +03:00
157 lines
5.2 KiB
Python
157 lines
5.2 KiB
Python
#!/usr/bin/env python3
|
||
"""
|
||
Export square crops around MaskROMTool bit coordinates into a single ZIP file.
|
||
|
||
Usage:
|
||
python export_bit_windows_zip.py \
|
||
--json bits.json \
|
||
--image die.jpg \
|
||
--zip_out bits.zip \
|
||
--window_size 38
|
||
|
||
Notes:
|
||
- The input JSON is expected to have a large "bits" array with fields: x, y, value, ambiguous.
|
||
- Bits are sorted *column-first*, *bottom-up*.
|
||
- Logical coordinates are assigned so that (0,0) is the top-left of the bit grid.
|
||
- Each crop (window_size × window_size) is saved as bit_X_Y.png inside the ZIP.
|
||
"""
|
||
|
||
import argparse
|
||
import json
|
||
import math
|
||
import os
|
||
import io
|
||
import zipfile
|
||
from dataclasses import dataclass
|
||
from statistics import mean
|
||
from typing import List, Tuple
|
||
|
||
from PIL import Image
|
||
|
||
|
||
@dataclass
|
||
class Bit:
|
||
x: float
|
||
y: float
|
||
value: bool | None = None
|
||
ambiguous: bool | None = None
|
||
gx: int | None = None
|
||
gy: int | None = None
|
||
|
||
|
||
def parse_args() -> argparse.Namespace:
|
||
p = argparse.ArgumentParser(description="Export bit-centered crops into a single ZIP archive.")
|
||
p.add_argument("--json", required=True, help="Path to MaskROMTool JSON file.")
|
||
p.add_argument("--image", required=True, help="Path to source JPEG (very high resolution).")
|
||
p.add_argument("--zip_out", required=True, help="Output ZIP filename (e.g. bits.zip).")
|
||
p.add_argument("--window_size", type=int, required=True, help="Window size (pixels).")
|
||
p.add_argument("--tail_frac", type=float, default=0.05,
|
||
help="Fraction of largest x-diffs used for detecting column jumps (default: 0.05).")
|
||
return p.parse_args()
|
||
|
||
|
||
def load_bits(json_path: str) -> List[Bit]:
|
||
with open(json_path, "r", encoding="utf-8") as f:
|
||
data = json.load(f)
|
||
bits_json = data.get("bits")
|
||
if not isinstance(bits_json, list):
|
||
raise ValueError("JSON missing 'bits' array.")
|
||
return [Bit(
|
||
x=float(b["x"]),
|
||
y=float(b["y"]),
|
||
value=bool(b.get("value")) if "value" in b else None,
|
||
ambiguous=bool(b.get("ambiguous")) if "ambiguous" in b else None,
|
||
) for b in bits_json]
|
||
|
||
|
||
def estimate_column_threshold(x_diffs: List[float], tail_frac: float) -> float:
|
||
if not x_diffs:
|
||
return 0.0
|
||
diffs = sorted(abs(d) for d in x_diffs)
|
||
n = len(diffs)
|
||
if n == 1:
|
||
return diffs[0] / 2 if diffs[0] > 0 else 0.0
|
||
cut = max(1, min(n - 1, int(math.floor((1.0 - tail_frac) * n))))
|
||
small = diffs[:cut]
|
||
large = diffs[cut:]
|
||
if not large:
|
||
return (mean(small) if small else 0.0) * 3.0
|
||
return (mean(small) + mean(large)) / 2.0
|
||
|
||
|
||
def split_into_columns(bits: List[Bit], tail_frac: float) -> List[List[Bit]]:
|
||
x_diffs = [bits[i].x - bits[i - 1].x for i in range(1, len(bits))]
|
||
threshold = estimate_column_threshold(x_diffs, tail_frac)
|
||
columns: List[List[Bit]] = []
|
||
current_col: List[Bit] = [bits[0]]
|
||
|
||
for i in range(1, len(bits)):
|
||
dx = abs(bits[i].x - bits[i - 1].x)
|
||
if dx > threshold:
|
||
columns.append(current_col)
|
||
current_col = [bits[i]]
|
||
else:
|
||
current_col.append(bits[i])
|
||
columns.append(current_col)
|
||
columns.sort(key=lambda col: mean(b.x for b in col))
|
||
return columns
|
||
|
||
|
||
def assign_logical_grid(columns: List[List[Bit]]) -> Tuple[int, int]:
|
||
grid_width = len(columns)
|
||
grid_height_max = 0
|
||
for gx, col in enumerate(columns):
|
||
col_sorted = sorted(col, key=lambda b: b.y) # top→bottom
|
||
for gy, b in enumerate(col_sorted):
|
||
b.gx = gx
|
||
b.gy = gy
|
||
grid_height_max = max(grid_height_max, len(col_sorted))
|
||
columns[gx] = col_sorted
|
||
return grid_width, grid_height_max
|
||
|
||
|
||
def crop_with_padding(img: Image.Image, cx: int, cy: int, size: int) -> Image.Image:
|
||
half = size // 2
|
||
left = cx - half
|
||
top = cy - half
|
||
right = left + size
|
||
bottom = top + size
|
||
src_w, src_h = img.size
|
||
src_box = (max(left, 0), max(top, 0), min(right, src_w), min(bottom, src_h))
|
||
out = Image.new("RGB", (size, size), (0, 0, 0))
|
||
dst_left = max(0, -left)
|
||
dst_top = max(0, -top)
|
||
if src_box[0] < src_box[2] and src_box[1] < src_box[3]:
|
||
region = img.crop(src_box)
|
||
out.paste(region, (dst_left, dst_top))
|
||
return out
|
||
|
||
|
||
def main():
|
||
args = parse_args()
|
||
bits = load_bits(args.json)
|
||
print(f"[info] Loaded {len(bits)} bits.")
|
||
|
||
columns = split_into_columns(bits, args.tail_frac)
|
||
grid_w, grid_h_max = assign_logical_grid(columns)
|
||
print(f"[info] Detected grid {grid_w}×{grid_h_max}")
|
||
|
||
img = Image.open(args.image).convert("RGB")
|
||
ws = args.window_size
|
||
ordered_bits = [b for col in columns for b in col]
|
||
|
||
with zipfile.ZipFile(args.zip_out, "w", compression=zipfile.ZIP_DEFLATED) as zf:
|
||
for i, b in enumerate(ordered_bits, 1):
|
||
cx, cy = int(round(b.x)), int(round(b.y))
|
||
crop = crop_with_padding(img, cx, cy, ws)
|
||
buf = io.BytesIO()
|
||
crop.save(buf, format="PNG")
|
||
zf.writestr(f"bit_{b.gx}_{b.gy}.png", buf.getvalue())
|
||
if i % 500 == 0 or i == len(ordered_bits):
|
||
print(f"[progress] {i}/{len(ordered_bits)} written")
|
||
|
||
print(f"[done] {len(ordered_bits)} images saved to {args.zip_out}")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main() |