First public commit.
This commit is contained in:
parent
2a48f52979
commit
4bac9d83ed
288 changed files with 18417 additions and 1076 deletions
31
lenser/atari/convert/__init__.py
Normal file
31
lenser/atari/convert/__init__.py
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
"""Atari conversion dispatch."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from ... import imageprep
|
||||
from .. import palette as apal
|
||||
from . import gr15
|
||||
|
||||
_MODULES = {"gr15": gr15}
|
||||
for _name in ("gr9", "gr8", "gr15dli", "mono"):
|
||||
try:
|
||||
_mod = __import__(f"lenser.atari.convert.{_name}", fromlist=[_name])
|
||||
_MODULES[_name] = _mod
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
MODES = list(_MODULES.keys())
|
||||
|
||||
|
||||
def convert_image(path_or_img, mode="gr15", palette_name="ntsc",
|
||||
dither_mode="floyd", intensive=False,
|
||||
prep_opt: imageprep.PrepOptions | None = None, base_color=None):
|
||||
prep_opt = prep_opt or imageprep.PrepOptions()
|
||||
module = _MODULES[mode]
|
||||
border_rgb = apal.get_palette(palette_name)[0]
|
||||
img_rgb = imageprep.prepare(
|
||||
path_or_img, module.WIDTH, module.HEIGHT, module.PIXEL_ASPECT,
|
||||
prep_opt, border_rgb=border_rgb,
|
||||
)
|
||||
return module.convert(img_rgb, palette_name, dither_mode, intensive,
|
||||
base_color=base_color)
|
||||
176
lenser/atari/convert/_common.py
Normal file
176
lenser/atari/convert/_common.py
Normal file
|
|
@ -0,0 +1,176 @@
|
|||
"""Shared helpers for the Atari encoders."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import numpy as np
|
||||
|
||||
DATA_ADDR = 0x4000 # bitmap base
|
||||
COLOR_ADDR = 0x6000 # colour data base (fixed, after the bitmap)
|
||||
SPLIT_LINE = 102 # lines that fit in the first 4K ($4000-$4FEF)
|
||||
BYTES_PER_LINE = 40
|
||||
LINES = 192
|
||||
|
||||
|
||||
def split_screen(line_bytes: list[bytes]) -> bytes:
|
||||
"""Lay out 192 screen lines with the 16-byte gap that pushes line 102 onto
|
||||
the $5000 boundary (so no ANTIC line crosses a 4K boundary), then pad up to
|
||||
COLOR_ADDR so colour data can follow at a fixed address."""
|
||||
first = b"".join(line_bytes[:SPLIT_LINE]) # 4080 bytes -> $4000
|
||||
second = b"".join(line_bytes[SPLIT_LINE:]) # 3600 bytes -> $5000
|
||||
body = first + bytes(0x1000 - len(first)) + second # gap fills to $5000
|
||||
pad = (COLOR_ADDR - DATA_ADDR) - len(body)
|
||||
return body + bytes(pad)
|
||||
|
||||
|
||||
def luminance_lab(img_rgb, plab):
|
||||
"""Return (image, palette) recast into luminance-only CIELAB (L, 0, 0), so
|
||||
matching is by brightness alone -- used by the single-hue modes."""
|
||||
from ...palette import srgb_to_lab
|
||||
L = srgb_to_lab(img_rgb)[..., 0]
|
||||
img_mono = np.zeros(img_rgb.shape[:2] + (3,))
|
||||
img_mono[..., 0] = L
|
||||
plab_mono = np.zeros_like(plab)
|
||||
plab_mono[:, 0] = plab[:, 0]
|
||||
return img_mono, plab_mono
|
||||
|
||||
|
||||
def choose_palette(img_lab: np.ndarray, plab: np.ndarray, k: int,
|
||||
iters: int = 12) -> list[int]:
|
||||
"""Pick the ``k`` palette register values (0..255) that best represent the
|
||||
image, by palette-constrained k-means in CIELAB."""
|
||||
flat = img_lab.reshape(-1, 3).astype(np.float32)
|
||||
D = np.sum((flat[:, None, :] - plab[None, :, :].astype(np.float32)) ** 2, axis=-1) # (N,256)
|
||||
|
||||
# k-means++-ish greedy init.
|
||||
chosen = [int(np.argmin(np.sum((plab - flat.mean(0)) ** 2, axis=-1)))]
|
||||
for _ in range(k - 1):
|
||||
md = D[:, chosen].min(axis=1)
|
||||
improv = np.maximum(0.0, md[:, None] - D).sum(axis=0)
|
||||
improv[chosen] = -1.0
|
||||
chosen.append(int(np.argmax(improv)))
|
||||
|
||||
# Lloyd refinement, each centroid snapped to its best palette colour.
|
||||
for _ in range(iters):
|
||||
assign = np.argmin(D[:, chosen], axis=1)
|
||||
new = []
|
||||
for j in range(k):
|
||||
mask = assign == j
|
||||
if not mask.any():
|
||||
new.append(chosen[j])
|
||||
else:
|
||||
new.append(int(np.argmin(D[mask].sum(axis=0))))
|
||||
# keep distinct where possible
|
||||
if new == chosen:
|
||||
break
|
||||
chosen = new
|
||||
return chosen
|
||||
|
||||
|
||||
def _seg_all(sub, c1all, c2):
|
||||
"""Distance from each ``sub`` pixel to the segment between every palette colour
|
||||
(c1all, shape (256,3)) and a fixed endpoint c2. Returns (256, Nsub)."""
|
||||
seg = c2 - c1all # (256,3)
|
||||
L = np.sum(seg * seg, axis=1) + 1e-9 # (256,)
|
||||
rel = sub[None, :, :] - c1all[:, None, :] # (256,Nsub,3)
|
||||
t = np.clip(np.sum(rel * seg[:, None, :], axis=2) / L[:, None], 0.0, 1.0)
|
||||
proj = c1all[:, None, :] + t[:, :, None] * seg[:, None, :]
|
||||
return np.sum((sub[None, :, :] - proj) ** 2, axis=2)
|
||||
|
||||
|
||||
def relevant_candidates(img_lab, plab):
|
||||
"""Palette colours that are the nearest match to some image pixel -- a small
|
||||
set (the image's own gamut) to restrict the dither-aware search to."""
|
||||
flat = img_lab.reshape(-1, 3).astype(np.float32)
|
||||
if len(flat) > 4000:
|
||||
flat = flat[::len(flat) // 4000]
|
||||
d = np.sum((flat[:, None, :] - plab[None, :, :].astype(np.float32)) ** 2, axis=-1)
|
||||
return np.unique(np.argmin(d, axis=1)).astype(np.int64)
|
||||
|
||||
|
||||
def choose_palette_dither(img_lab, plab, k, init=None, n_sample=900, iters=5,
|
||||
candidates=None):
|
||||
"""Dither-aware palette: pick the ``k`` colours whose pairwise *segment* blends
|
||||
(what error diffusion can reproduce) best cover the image -- so the colours
|
||||
span the gamut instead of sitting at k-means centroids. Vectorised local
|
||||
search (all candidates per slot at once) from a k-means start."""
|
||||
from itertools import combinations
|
||||
flat = img_lab.reshape(-1, 3)
|
||||
sub = flat[::max(1, len(flat) // n_sample)] if len(flat) > n_sample else flat
|
||||
colors = list(init) if init is not None else choose_palette(img_lab, plab, k)
|
||||
cand = np.asarray(candidates if candidates is not None else range(256), np.int64)
|
||||
cand_lab = plab[cand].astype(np.float64) # (C,3)
|
||||
for _ in range(iters):
|
||||
changed = False
|
||||
for i in range(k):
|
||||
others = [colors[j] for j in range(k) if j != i]
|
||||
fixed = None
|
||||
for x, y in combinations(others, 2):
|
||||
s = _seg_all(sub, plab[x][None], plab[y])[0]
|
||||
fixed = s if fixed is None else np.minimum(fixed, s)
|
||||
m = None
|
||||
for o in others:
|
||||
d = _seg_all(sub, cand_lab, plab[o]) # (C, Nsub)
|
||||
m = d if m is None else np.minimum(m, d)
|
||||
if fixed is not None:
|
||||
m = np.minimum(m, fixed[None, :])
|
||||
err = m.sum(axis=1) # (C,)
|
||||
for ci, c in enumerate(cand):
|
||||
if c in others:
|
||||
err[ci] = np.inf # avoid duplicate colours
|
||||
best = int(cand[np.argmin(err)])
|
||||
if best != colors[i]:
|
||||
colors[i] = best
|
||||
changed = True
|
||||
if not changed:
|
||||
break
|
||||
return colors
|
||||
|
||||
|
||||
def quantize_global(img_lab, plab, colors, dither_mode):
|
||||
"""Dither the whole image to a fixed global set of palette indices."""
|
||||
from ... import dither
|
||||
H, W, _ = img_lab.shape
|
||||
allowed = np.tile(np.array(colors, dtype=np.int64), (H, W, 1))
|
||||
return dither.quantize(img_lab, allowed, plab, dither_mode).astype(np.int64)
|
||||
|
||||
|
||||
def pack_2bpp(val_image: np.ndarray) -> list[bytes]:
|
||||
"""160-wide 2-bits-per-pixel -> list of 192 x 40-byte lines."""
|
||||
H, W = val_image.shape
|
||||
lines = []
|
||||
for y in range(H):
|
||||
row = val_image[y]
|
||||
out = bytearray()
|
||||
for x in range(0, W, 4):
|
||||
out.append((row[x] << 6) | (row[x + 1] << 4) | (row[x + 2] << 2) | row[x + 3])
|
||||
lines.append(bytes(out))
|
||||
return lines
|
||||
|
||||
|
||||
def pack_4bpp(val_image: np.ndarray) -> list[bytes]:
|
||||
"""80-wide 4-bits-per-pixel -> list of 192 x 40-byte lines."""
|
||||
H, W = val_image.shape
|
||||
lines = []
|
||||
for y in range(H):
|
||||
row = val_image[y]
|
||||
out = bytearray()
|
||||
for x in range(0, W, 2):
|
||||
out.append((row[x] << 4) | row[x + 1])
|
||||
lines.append(bytes(out))
|
||||
return lines
|
||||
|
||||
|
||||
def pack_1bpp(val_image: np.ndarray) -> list[bytes]:
|
||||
"""320-wide 1-bit-per-pixel -> list of 192 x 40-byte lines."""
|
||||
H, W = val_image.shape
|
||||
lines = []
|
||||
for y in range(H):
|
||||
row = val_image[y]
|
||||
out = bytearray()
|
||||
for x in range(0, W, 8):
|
||||
b = 0
|
||||
for i in range(8):
|
||||
b = (b << 1) | int(row[x + i])
|
||||
out.append(b)
|
||||
lines.append(bytes(out))
|
||||
return lines
|
||||
51
lenser/atari/convert/gr15.py
Normal file
51
lenser/atari/convert/gr15.py
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
"""Atari GR.15 (ANTIC mode E): 160x192, 4 colours chosen globally from 256.
|
||||
|
||||
No per-cell colour limit, so this is a clean 4-colour dithered image.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import numpy as np
|
||||
|
||||
from ... import palette as c64pal # for srgb_to_lab (shared)
|
||||
from ...convert.base import (Conversion, mean_error, perceptual_error,
|
||||
DIFFUSION_DITHERS)
|
||||
from .. import palette as apal
|
||||
from . import _common
|
||||
|
||||
WIDTH, HEIGHT = 160, 192
|
||||
PIXEL_ASPECT = 2.0
|
||||
|
||||
|
||||
def convert(img_rgb, palette_name="ntsc", dither_mode="floyd",
|
||||
intensive=False, base_color=None):
|
||||
plab = apal.palette_lab(palette_name)
|
||||
prgb = apal.get_palette(palette_name).astype(np.uint8)
|
||||
img_lab = c64pal.srgb_to_lab(img_rgb)
|
||||
|
||||
# Dither-aware palette for error-diffusion modes: pick 4 colours whose blends
|
||||
# span the image gamut (so dithering reproduces saturated/intermediate shades)
|
||||
# instead of k-means centroids the dither can't reach.
|
||||
if dither_mode in DIFFUSION_DITHERS:
|
||||
colors = _common.choose_palette_dither(img_lab, plab, k=4)
|
||||
else:
|
||||
colors = _common.choose_palette(img_lab, plab, k=4)
|
||||
colors.sort(key=lambda c: plab[c, 0]) # value 0 = darkest (background)
|
||||
|
||||
idx = _common.quantize_global(img_lab, plab, colors, dither_mode)
|
||||
value_of = {c: v for v, c in enumerate(colors)}
|
||||
val_image = np.vectorize(value_of.get)(idx).astype(np.uint8)
|
||||
|
||||
lines = _common.pack_2bpp(val_image)
|
||||
data = _common.split_screen(lines) + bytes(colors) # 4 colour regs at $6000
|
||||
|
||||
preview = np.repeat(prgb[idx], int(PIXEL_ASPECT), axis=1)
|
||||
|
||||
err = (perceptual_error if dither_mode in DIFFUSION_DITHERS else mean_error)
|
||||
return Conversion(
|
||||
mode="gr15", width=WIDTH, height=HEIGHT, pixel_aspect=PIXEL_ASPECT,
|
||||
index_image=idx.astype(np.uint16), data=data, data_addr=_common.DATA_ADDR,
|
||||
viewer="gr15", preview_rgb=preview,
|
||||
error=err(idx, img_lab, plab),
|
||||
meta={"palette": palette_name, "dither": dither_mode, "colors": colors},
|
||||
)
|
||||
84
lenser/atari/convert/gr15dli.py
Normal file
84
lenser/atari/convert/gr15dli.py
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
"""Atari GR.15 + DLI: 160x192, a fresh set of 4 colours every 2 scanlines.
|
||||
|
||||
A display-list interrupt rewrites the four colour registers for each 2-line band
|
||||
(96 bands). Every *single* scanline is impossible -- four register writes don't
|
||||
fit the inter-DLI window -- but every 2 lines leaves a comfortable budget, and
|
||||
96x4 colours is still far beyond flat GR.15. The display list (with DLI bits on
|
||||
the right lines) is generated here and shipped in the data block.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import numpy as np
|
||||
|
||||
from ... import dither, palette as c64pal
|
||||
from ...convert.base import (Conversion, mean_error, perceptual_error,
|
||||
DIFFUSION_DITHERS)
|
||||
from .. import palette as apal
|
||||
from . import _common
|
||||
|
||||
WIDTH, HEIGHT = 160, 192
|
||||
PIXEL_ASPECT = 2.0
|
||||
BAND_H = 2
|
||||
N_BANDS = HEIGHT // BAND_H # 96
|
||||
COLOR_ADDR = 0x6000
|
||||
DL_ADDR = 0x6400 # display list, after the colour table
|
||||
|
||||
|
||||
def make_dlist() -> bytes:
|
||||
"""ANTIC mode-E display list, 4K-split, DLI bit on the last line of each
|
||||
2-line band (odd lines 1..189) so the handler sets up the next band."""
|
||||
dl = bytearray([0x70, 0x70, 0x70]) # 24 blank lines
|
||||
dl += bytes([0x4e, 0x00, 0x40]) # line 0: LMS $4000 (no DLI)
|
||||
for ln in range(1, 102): # lines 1..101
|
||||
dl.append(0x8e if ln % 2 == 1 else 0x0e)
|
||||
dl += bytes([0x4e, 0x00, 0x50]) # line 102: LMS $5000 (no DLI)
|
||||
for ln in range(103, 192): # lines 103..191
|
||||
dl.append(0x8e if (ln % 2 == 1 and ln != 191) else 0x0e)
|
||||
dl += bytes([0x41, DL_ADDR & 0xFF, DL_ADDR >> 8]) # JVB -> start
|
||||
return bytes(dl)
|
||||
|
||||
|
||||
def convert(img_rgb, palette_name="ntsc", dither_mode="floyd",
|
||||
intensive=False, base_color=None):
|
||||
plab = apal.palette_lab(palette_name)
|
||||
prgb = apal.get_palette(palette_name).astype(np.uint8)
|
||||
img_lab = c64pal.srgb_to_lab(img_rgb)
|
||||
|
||||
band_sets = np.zeros((N_BANDS, 4), dtype=np.int64)
|
||||
aware = dither_mode in DIFFUSION_DITHERS
|
||||
iters = 10 if intensive else 5
|
||||
# restrict the per-band dither-aware search to the image's own gamut (fast).
|
||||
cand = _common.relevant_candidates(img_lab, plab) if aware else None
|
||||
for b in range(N_BANDS):
|
||||
block = img_lab[b * BAND_H:(b + 1) * BAND_H].reshape(-1, 3)
|
||||
cols = _common.choose_palette(block, plab, k=4, iters=iters)
|
||||
if aware: # span each band's gamut so dithering blends to the true shade
|
||||
cols = _common.choose_palette_dither(block, plab, k=4, init=cols,
|
||||
iters=4 if intensive else 3,
|
||||
candidates=cand)
|
||||
cols.sort(key=lambda c: plab[c, 0])
|
||||
band_sets[b] = cols
|
||||
|
||||
allowed = np.repeat((band_sets[np.arange(HEIGHT) // BAND_H])[:, None, :],
|
||||
WIDTH, axis=1)
|
||||
idx = dither.quantize(img_lab, allowed, plab, dither_mode).astype(np.int64)
|
||||
|
||||
val = np.zeros((HEIGHT, WIDTH), dtype=np.uint8)
|
||||
for y in range(HEIGHT):
|
||||
lut = {int(c): v for v, c in enumerate(band_sets[y // BAND_H])}
|
||||
val[y] = [lut.get(int(p), 0) for p in idx[y]]
|
||||
|
||||
bitmap = _common.split_screen(_common.pack_2bpp(val)) # 8192 -> $4000..$5FFF
|
||||
coltab = band_sets.astype(np.uint8).tobytes() # 384 -> $6000
|
||||
region = coltab + bytes((DL_ADDR - COLOR_ADDR) - len(coltab)) + make_dlist()
|
||||
data = bitmap + region
|
||||
|
||||
preview = np.repeat(prgb[idx], int(PIXEL_ASPECT), axis=1)
|
||||
return Conversion(
|
||||
mode="gr15dli", width=WIDTH, height=HEIGHT, pixel_aspect=PIXEL_ASPECT,
|
||||
index_image=idx.astype(np.uint16), data=data, data_addr=_common.DATA_ADDR,
|
||||
viewer="gr15dli", preview_rgb=preview,
|
||||
error=(perceptual_error if aware else mean_error)(idx, img_lab, plab),
|
||||
meta={"palette": palette_name, "dither": dither_mode},
|
||||
)
|
||||
40
lenser/atari/convert/gr8.py
Normal file
40
lenser/atari/convert/gr8.py
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
"""Atari GR.8 (ANTIC mode F): 320x192 hi-res, two tones of one hue.
|
||||
|
||||
Highest spatial resolution; carries tone by dithering between background and a
|
||||
foreground luminance. ``base_color`` picks the hue (None = greyscale).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import numpy as np
|
||||
|
||||
from ...convert.base import Conversion, perceptual_error
|
||||
from .. import palette as apal
|
||||
from . import _common
|
||||
|
||||
WIDTH, HEIGHT = 320, 192
|
||||
PIXEL_ASPECT = 1.0
|
||||
|
||||
|
||||
def convert(img_rgb, palette_name="ntsc", dither_mode="floyd",
|
||||
intensive=False, base_color=None):
|
||||
hue = 0 if base_color is None else (int(base_color) & 0x0F)
|
||||
bg_reg = (hue << 4) | 0x00 # darkest of the hue
|
||||
fg_reg = (hue << 4) | 0x0E # brightest of the hue
|
||||
plab = apal.palette_lab(palette_name)
|
||||
prgb = apal.get_palette(palette_name).astype(np.uint8)
|
||||
|
||||
img_mono, plab_mono = _common.luminance_lab(img_rgb, plab)
|
||||
idx = _common.quantize_global(img_mono, plab_mono, [bg_reg, fg_reg], dither_mode)
|
||||
val = (idx == fg_reg).astype(np.uint8)
|
||||
|
||||
data = _common.split_screen(_common.pack_1bpp(val)) + bytes([bg_reg, fg_reg])
|
||||
preview = prgb[idx] # already 320 wide
|
||||
|
||||
return Conversion(
|
||||
mode="gr8", width=WIDTH, height=HEIGHT, pixel_aspect=PIXEL_ASPECT,
|
||||
index_image=idx.astype(np.uint16), data=data, data_addr=_common.DATA_ADDR,
|
||||
viewer="gr8", preview_rgb=preview,
|
||||
error=perceptual_error(idx, img_mono, plab_mono),
|
||||
meta={"palette": palette_name, "dither": dither_mode, "hue": hue},
|
||||
)
|
||||
48
lenser/atari/convert/gr9.py
Normal file
48
lenser/atari/convert/gr9.py
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
"""Atari GR.9 (GTIA): 80x192, 16 luminance shades of one hue.
|
||||
|
||||
Excellent greyscale (hue 0) or tinted monochrome (any of 16 hues) -- 16 real
|
||||
shades, not just dithered. ``base_color`` selects the hue (0..15); None = grey.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import numpy as np
|
||||
|
||||
from ...convert.base import Conversion, perceptual_error
|
||||
from .. import palette as apal
|
||||
from . import _common
|
||||
|
||||
WIDTH, HEIGHT = 80, 192
|
||||
PIXEL_ASPECT = 4.0
|
||||
|
||||
|
||||
def convert(img_rgb, palette_name="ntsc", dither_mode="floyd",
|
||||
intensive=False, base_color=None):
|
||||
hue = 0 if base_color is None else (int(base_color) & 0x0F)
|
||||
plab = apal.palette_lab(palette_name)
|
||||
prgb = apal.get_palette(palette_name).astype(np.uint8)
|
||||
|
||||
img_mono, plab_mono = _common.luminance_lab(img_rgb, plab)
|
||||
ramp = apal.hue_ramp(hue) # 16 register values of this hue
|
||||
idx = _common.quantize_global(img_mono, plab_mono, ramp, dither_mode)
|
||||
val = (idx & 0x0F).astype(np.uint8) # GR.9 pixel = 4-bit luminance
|
||||
|
||||
# GTIA mode 9 takes the hue from COLBK and the luminance from each pixel. A
|
||||
# COLBK of exactly $00, though, blanks the whole playfield to black -- the
|
||||
# register must be non-zero to enable the display. For a tinted hue that is
|
||||
# automatic ((hue<<4) != 0); for greyscale (hue 0) force a non-zero luminance
|
||||
# nibble, which the mode ignores for output (luminance still comes from the
|
||||
# pixels) but which switches the 16-shade display on.
|
||||
colbk = (hue & 0x0F) << 4
|
||||
if colbk == 0:
|
||||
colbk = 0x0E
|
||||
data = _common.split_screen(_common.pack_4bpp(val)) + bytes([colbk])
|
||||
preview = np.repeat(prgb[idx], int(PIXEL_ASPECT), axis=1)
|
||||
|
||||
return Conversion(
|
||||
mode="gr9", width=WIDTH, height=HEIGHT, pixel_aspect=PIXEL_ASPECT,
|
||||
index_image=idx.astype(np.uint16), data=data, data_addr=_common.DATA_ADDR,
|
||||
viewer="gr9", preview_rgb=preview,
|
||||
error=perceptual_error(idx, img_mono, plab_mono),
|
||||
meta={"palette": palette_name, "dither": dither_mode, "hue": hue},
|
||||
)
|
||||
16
lenser/atari/convert/mono.py
Normal file
16
lenser/atari/convert/mono.py
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
"""Atari monochrome -- GR.9's 16 luminance shades, exposed as the standard
|
||||
``mono`` mode for cross-platform parity. Greyscale by default; ``--mono-base``
|
||||
tints it to one of the GTIA hues (16 real shades of that hue)."""
|
||||
from __future__ import annotations
|
||||
|
||||
from . import gr9
|
||||
|
||||
WIDTH, HEIGHT, PIXEL_ASPECT = gr9.WIDTH, gr9.HEIGHT, gr9.PIXEL_ASPECT
|
||||
|
||||
|
||||
def convert(img_rgb, palette_name="ntsc", dither_mode="floyd",
|
||||
intensive=False, base_color=None):
|
||||
conv = gr9.convert(img_rgb, palette_name, dither_mode, intensive,
|
||||
base_color=base_color)
|
||||
conv.mode = "mono"
|
||||
return conv
|
||||
Loading…
Add table
Add a link
Reference in a new issue