mirror of
https://github.com/BrianPugh/game-and-watch-patch.git
synced 2025-12-16 07:16:26 +01:00
146 lines
3.7 KiB
Python
146 lines
3.7 KiB
Python
from math import ceil
|
|
|
|
import numpy as np
|
|
from PIL import Image
|
|
|
|
_BLOCK_SIZE = 16
|
|
_BLOCK_PIXEL = _BLOCK_SIZE * _BLOCK_SIZE
|
|
|
|
PALETTE_OFFSETS = [
|
|
0xB_EC68,
|
|
0xB_EDA8,
|
|
0xB_EEE8,
|
|
0xB_F028,
|
|
0xB_F168,
|
|
]
|
|
|
|
|
|
def bytes_to_tilemap(data, palette=None, bpp=8, width=256):
|
|
"""
|
|
Parameters
|
|
----------
|
|
palette : bytes
|
|
320 long RGBA (80 colors). Alpha is ignored.
|
|
|
|
Returns
|
|
-------
|
|
PIL.Image
|
|
Rendered RGB image.
|
|
"""
|
|
|
|
# assert bpp in [4, 8]
|
|
|
|
if bpp < 8:
|
|
nibbles = bytearray()
|
|
# offset = 0x0
|
|
for b in data:
|
|
shift = 8 - bpp
|
|
while shift >= 0:
|
|
nibbles.append(b >> shift & (2 ** bpp - 1))
|
|
shift -= bpp
|
|
# nibbles.append((b >> 4) | (offset << 4))
|
|
# nibbles.append((b & 0xF) | (offset << 4))
|
|
data = bytes(nibbles)
|
|
del nibbles
|
|
|
|
# Assemble bytes into an index-image
|
|
h, w = int(ceil(len(data) / width / _BLOCK_SIZE) * _BLOCK_SIZE), width
|
|
canvas = np.zeros((h, w), dtype=np.uint8)
|
|
i_sprite = 0
|
|
for i in range(0, len(data), _BLOCK_PIXEL):
|
|
sprite = data[i : i + _BLOCK_PIXEL]
|
|
|
|
x = i_sprite * _BLOCK_SIZE % w
|
|
y = _BLOCK_SIZE * (i_sprite * _BLOCK_SIZE // w)
|
|
view = canvas[y : y + _BLOCK_SIZE, x : x + _BLOCK_SIZE]
|
|
sprite_block = np.frombuffer(sprite, dtype=np.uint8).reshape(
|
|
_BLOCK_SIZE, _BLOCK_SIZE
|
|
)
|
|
view[:] = sprite_block
|
|
|
|
i_sprite += 1
|
|
|
|
if palette is None:
|
|
return Image.fromarray(canvas, "L")
|
|
|
|
# Apply palette to index-image
|
|
p = np.frombuffer(palette, dtype=np.uint8).reshape((80, 4))
|
|
p = p[:, :3]
|
|
p = np.fliplr(p) # BGR->RGB
|
|
|
|
im = Image.fromarray(canvas, "P")
|
|
im.putpalette(p)
|
|
|
|
return im
|
|
|
|
|
|
def rgb_to_index(tilemap, palette):
|
|
if isinstance(tilemap, Image.Image):
|
|
tilemap = tilemap.convert("RGB")
|
|
tilemap = np.array(tilemap)
|
|
elif isinstance(tilemap, np.ndarray):
|
|
pass
|
|
else:
|
|
raise TypeError(f"Don't know how to handle tilemap type {type(tilemap)}")
|
|
|
|
# Convert rgb tilemap to index image
|
|
p = np.frombuffer(palette, dtype=np.uint8).reshape((80, 4))
|
|
p = p[:, :3]
|
|
p = np.fliplr(p) # BGR->RGB
|
|
p = p[None, None].transpose(0, 1, 3, 2) # (1, 1, 3, 80)
|
|
|
|
# Find closest color
|
|
diff = tilemap[..., None] - p
|
|
dist = np.linalg.norm(diff, axis=2)
|
|
tilemap = np.argmin(dist, axis=-1).astype(np.uint8)
|
|
|
|
return tilemap
|
|
|
|
|
|
def tilemap_to_bytes(tilemap, palette=None, bpp=8):
|
|
"""
|
|
Parameters
|
|
----------
|
|
tilemap : PIL.Image.Image or numpy.ndarray
|
|
RGB data
|
|
palette : bytes
|
|
320 long RGBA (80 colors). Alpha is ignored.
|
|
|
|
Returns
|
|
-------
|
|
bytes
|
|
Bytes representation of index image
|
|
"""
|
|
|
|
if isinstance(tilemap, Image.Image):
|
|
tilemap = tilemap.convert("RGB")
|
|
tilemap = np.array(tilemap)
|
|
elif isinstance(tilemap, np.ndarray):
|
|
pass
|
|
else:
|
|
raise TypeError(f"Don't know how to handle tilemap type {type(tilemap)}")
|
|
|
|
if palette is not None:
|
|
tilemap = rgb_to_index(tilemap, palette)
|
|
|
|
# Need to undo the tiling now.
|
|
out = []
|
|
for i in range(0, tilemap.shape[0], _BLOCK_SIZE):
|
|
for j in range(0, tilemap.shape[1], _BLOCK_SIZE):
|
|
sprite = tilemap[i : i + _BLOCK_SIZE, j : j + _BLOCK_SIZE]
|
|
sprite_bytes = sprite.tobytes()
|
|
out.append(sprite_bytes)
|
|
out = b"".join(out)
|
|
|
|
if bpp == 4:
|
|
out_packed = bytearray()
|
|
assert len(out) % 2 == 0
|
|
for i in range(0, len(out), 2):
|
|
b1, b2 = out[i], out[i + 1]
|
|
b1 &= 0xF
|
|
b2 &= 0xF
|
|
out_packed.append((b1 << 4) | b2)
|
|
out = bytes(out_packed)
|
|
|
|
return out
|