First public commit.
This commit is contained in:
parent
2a48f52979
commit
4bac9d83ed
288 changed files with 18417 additions and 1076 deletions
17
lenser/bbc/convert/__init__.py
Normal file
17
lenser/bbc/convert/__init__.py
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
"""BBC Micro conversion dispatch."""
|
||||
from __future__ import annotations
|
||||
from ... import imageprep
|
||||
from . import mode0, mode1, mode2, mode5, mono
|
||||
|
||||
_MODULES = {"mode0": mode0, "mode1": mode1, "mode2": mode2, "mode5": mode5,
|
||||
"mono": mono}
|
||||
MODES = list(_MODULES.keys())
|
||||
|
||||
|
||||
def convert_image(path_or_img, mode="mode2", palette_name="bbc",
|
||||
dither_mode="floyd", intensive=False, prep_opt=None, base_color=None):
|
||||
prep_opt = prep_opt or imageprep.PrepOptions()
|
||||
module = _MODULES[mode]
|
||||
img_rgb = imageprep.prepare(path_or_img, module.WIDTH, module.HEIGHT,
|
||||
module.PIXEL_ASPECT, prep_opt, border_rgb=(0, 0, 0))
|
||||
return module.convert(img_rgb, palette_name, dither_mode, intensive, base_color=base_color)
|
||||
72
lenser/bbc/convert/_common.py
Normal file
72
lenser/bbc/convert/_common.py
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
"""Shared helpers for the BBC Micro converters."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import itertools
|
||||
|
||||
import numpy as np
|
||||
|
||||
from ... import dither
|
||||
from ...convert.base import Conversion, perceptual_error, _box_blur
|
||||
from ...palette import srgb_to_lab
|
||||
from .. import palette as bpal
|
||||
|
||||
|
||||
def _choose_physicals(img_lab, n, dither_mode):
|
||||
"""Pick the n physical colours (of 8) that best reproduce the image, then
|
||||
dither once with that palette. Candidates are ranked by a fast vectorised
|
||||
proxy -- the perceptual error of the nearest-colour (un-dithered)
|
||||
reconstruction -- so we do NOT run the slow Floyd dither for all C(8,n)
|
||||
combinations (doing so made the 4-colour modes hang for a minute+); Floyd runs
|
||||
only on the winning palette. Returns (physical_indices, logical_idx, error)."""
|
||||
plab_all = bpal.phys_lab()
|
||||
H, W, _ = img_lab.shape
|
||||
target_blur = _box_blur(img_lab)
|
||||
best_combo, best_score = None, np.inf
|
||||
for combo in itertools.combinations(range(8), n):
|
||||
sub = plab_all[list(combo)]
|
||||
nidx = ((img_lab[:, :, None, :] - sub[None, None]) ** 2).sum(-1).argmin(-1)
|
||||
diff = _box_blur(sub[nidx]) - target_blur
|
||||
score = float(np.sqrt((diff ** 2).sum(-1)).mean())
|
||||
if score < best_score:
|
||||
best_score, best_combo = score, list(combo)
|
||||
sub = plab_all[best_combo]
|
||||
allowed = np.tile(np.arange(n), (H, W, 1))
|
||||
idx = dither.quantize(img_lab, allowed, sub, dither_mode).astype(np.uint8)
|
||||
return best_combo, idx, perceptual_error(idx, img_lab, sub)
|
||||
|
||||
|
||||
def build(img_rgb, *, mode, bbc_mode, ncol, bpp, width, height, base,
|
||||
dither_mode, mono=False):
|
||||
if mono:
|
||||
L = srgb_to_lab(img_rgb)[..., 0]
|
||||
img_lab = np.zeros((height, width, 3))
|
||||
img_lab[..., 0] = L
|
||||
plab = np.zeros((2, 3)); plab[:, 0] = bpal.mono_lab()[:, 0]
|
||||
allowed = np.tile(np.array([0, 1]), (height, width, 1))
|
||||
idx = dither.quantize(img_lab, allowed, plab, dither_mode).astype(np.uint8)
|
||||
physicals = [0, 7] # black, white
|
||||
err = perceptual_error(idx, img_lab, plab)
|
||||
prgb = bpal.PHYS[[0, 7]].astype(np.uint8)
|
||||
else:
|
||||
img_lab = srgb_to_lab(img_rgb)
|
||||
if ncol >= 8:
|
||||
physicals = list(range(8))
|
||||
plab = bpal.phys_lab()
|
||||
allowed = np.tile(np.arange(8), (height, width, 1))
|
||||
idx = dither.quantize(img_lab, allowed, plab, dither_mode).astype(np.uint8)
|
||||
err = perceptual_error(idx, img_lab, plab)
|
||||
else:
|
||||
physicals, idx, err = _choose_physicals(img_lab, ncol, dither_mode)
|
||||
prgb = bpal.PHYS[physicals].astype(np.uint8)
|
||||
|
||||
data = bpal.pack(idx, width, bpp)
|
||||
preview = prgb[idx]
|
||||
return Conversion(
|
||||
mode=mode, width=width, height=height,
|
||||
pixel_aspect=(4 / 3) / (width / height),
|
||||
index_image=idx.astype(np.uint16), data=data, data_addr=base,
|
||||
viewer="bbc", preview_rgb=preview, error=err,
|
||||
meta={"palette": "bbc", "dither": dither_mode, "bbc_mode": bbc_mode,
|
||||
"ncol": ncol, "physicals": physicals, "base": base},
|
||||
)
|
||||
9
lenser/bbc/convert/mode0.py
Normal file
9
lenser/bbc/convert/mode0.py
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
"""BBC MODE 0: 640x256, 2-colour (black & white). 20K screen at &3000."""
|
||||
from __future__ import annotations
|
||||
from . import _common
|
||||
WIDTH, HEIGHT = 640, 256
|
||||
PIXEL_ASPECT = (4 / 3) / (WIDTH / HEIGHT)
|
||||
def convert(img_rgb, palette_name="bbc", dither_mode="floyd", intensive=False, base_color=None):
|
||||
return _common.build(img_rgb, mode="mode0", bbc_mode=0, ncol=2, bpp=1,
|
||||
width=WIDTH, height=HEIGHT, base=0x3000,
|
||||
dither_mode=dither_mode, mono=True)
|
||||
8
lenser/bbc/convert/mode1.py
Normal file
8
lenser/bbc/convert/mode1.py
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
"""BBC MODE 1: 320x256, 4-colour (best 4 of 8). 20K screen at &3000."""
|
||||
from __future__ import annotations
|
||||
from . import _common
|
||||
WIDTH, HEIGHT = 320, 256
|
||||
PIXEL_ASPECT = (4 / 3) / (WIDTH / HEIGHT)
|
||||
def convert(img_rgb, palette_name="bbc", dither_mode="floyd", intensive=False, base_color=None):
|
||||
return _common.build(img_rgb, mode="mode1", bbc_mode=1, ncol=4, bpp=2,
|
||||
width=WIDTH, height=HEIGHT, base=0x3000, dither_mode=dither_mode)
|
||||
8
lenser/bbc/convert/mode2.py
Normal file
8
lenser/bbc/convert/mode2.py
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
"""BBC MODE 2: 160x256, 8-colour. 20K screen at &3000."""
|
||||
from __future__ import annotations
|
||||
from . import _common
|
||||
WIDTH, HEIGHT = 160, 256
|
||||
PIXEL_ASPECT = (4 / 3) / (WIDTH / HEIGHT)
|
||||
def convert(img_rgb, palette_name="bbc", dither_mode="floyd", intensive=False, base_color=None):
|
||||
return _common.build(img_rgb, mode="mode2", bbc_mode=2, ncol=8, bpp=4,
|
||||
width=WIDTH, height=HEIGHT, base=0x3000, dither_mode=dither_mode)
|
||||
8
lenser/bbc/convert/mode5.py
Normal file
8
lenser/bbc/convert/mode5.py
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
"""BBC MODE 5: 160x256, 4-colour (best 4 of 8). 10K screen at &5800 (fits a ROM)."""
|
||||
from __future__ import annotations
|
||||
from . import _common
|
||||
WIDTH, HEIGHT = 160, 256
|
||||
PIXEL_ASPECT = (4 / 3) / (WIDTH / HEIGHT)
|
||||
def convert(img_rgb, palette_name="bbc", dither_mode="floyd", intensive=False, base_color=None):
|
||||
return _common.build(img_rgb, mode="mode5", bbc_mode=5, ncol=4, bpp=2,
|
||||
width=WIDTH, height=HEIGHT, base=0x5800, dither_mode=dither_mode)
|
||||
15
lenser/bbc/convert/mono.py
Normal file
15
lenser/bbc/convert/mono.py
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
"""BBC monochrome -- MODE 0's 640x256 black & white, exposed as the standard
|
||||
``mono`` mode for cross-platform parity (tone carried by dithering)."""
|
||||
from __future__ import annotations
|
||||
|
||||
from . import mode0
|
||||
|
||||
WIDTH, HEIGHT, PIXEL_ASPECT = mode0.WIDTH, mode0.HEIGHT, mode0.PIXEL_ASPECT
|
||||
|
||||
|
||||
def convert(img_rgb, palette_name="bbc", dither_mode="floyd",
|
||||
intensive=False, base_color=None):
|
||||
conv = mode0.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