import os
import subprocess
import tempfile
import numpy as np
from PIL import Image
from scipy import ndimage
WCELLS = 34
DARK = 0.55 MIN_BLOB = 60 PAD = 4
SEP = "\x0c"
SOURCES = {
"aris": {"kind": "gif", "path": "refs/aris2.gif"},
"motion_a": {"kind": "green", "path": "refs/yt_src.webm", "ss": 8.5, "to": 13, "fps": 12},
"motion_b": {"kind": "green", "path": "refs/yt_src3.mp4", "ss": 5.75, "to": 11.3, "fps": 12},
}
STATIC_FROM = "aris"
_DOTS = [(0, 0, 0), (1, 0, 1), (2, 0, 2), (0, 1, 3),
(1, 1, 4), (2, 1, 5), (3, 0, 6), (3, 1, 7)]
def green_alpha(rgb):
r, g, b = rgb[..., 0].astype(int), rgb[..., 1].astype(int), rgb[..., 2].astype(int)
green = (g > 100) & (g > r + 30) & (g > b + 30)
nong = ~green
lbl, _ = ndimage.label(nong) border = set(lbl[0, :]) | set(lbl[-1, :]) | set(lbl[:, 0]) | set(lbl[:, -1])
border.discard(0)
interior = nong & ~np.isin(lbl, list(border))
l2, n2 = ndimage.label(interior) alpha = np.zeros(rgb.shape[:2], bool)
minor = np.zeros(rgb.shape[:2], bool)
if n2 > 0:
sizes = ndimage.sum(np.ones_like(l2), l2, range(1, n2 + 1))
main = int(np.argmax(sizes)) + 1
for i, sz in enumerate(sizes, start=1):
if sz >= MIN_BLOB:
blob = l2 == i
alpha |= blob
if i != main:
minor |= blob
return (alpha * 255).astype("uint8"), (minor * 255).astype("uint8")
def load_source(spec):
if spec["kind"] == "gif":
im = Image.open(spec["path"])
out = []
for i in range(getattr(im, "n_frames", 1)):
im.seek(i)
f = im.convert("RGBA")
a = np.asarray(f)[..., 3]
out.append((f, a, np.zeros_like(a)))
return out
with tempfile.TemporaryDirectory() as td:
subprocess.run(["ffmpeg", "-y", "-ss", str(spec["ss"]), "-to", str(spec["to"]),
"-i", spec["path"], "-vf", f"fps={spec['fps']}", os.path.join(td, "f%04d.png")],
check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
out = []
for name in sorted(os.listdir(td)):
im = Image.open(os.path.join(td, name)).convert("RGB")
rgb = np.asarray(im)
alpha, minor = green_alpha(rgb)
r, g, b = rgb[..., 0].astype(int), rgb[..., 1].astype(int), rgb[..., 2].astype(int)
teal = (b > r + 25) & (g > r + 15) & (alpha > 0)
minor = np.maximum(minor, (teal * 255).astype("uint8"))
out.append((im.convert("RGBA"), alpha, minor))
return out
def union_box(frames):
x0 = y0 = 1 << 30
x1 = y1 = 0
for _, a, _ in frames:
ys, xs = np.where(a > 128)
if len(xs):
x0, x1 = min(x0, int(xs.min())), max(x1, int(xs.max()))
y0, y1 = min(y0, int(ys.min())), max(y1, int(ys.max()))
w, h = frames[0][0].size
return (max(0, x0 - PAD), max(0, y0 - PAD), min(w, x1 + PAD), min(h, y1 + PAD))
def to_braille(rgba, alpha, minor, box):
im = rgba.crop(box)
a = np.asarray(Image.fromarray(alpha).crop(box), np.float32) / 255.0
mn = np.asarray(Image.fromarray(minor).crop(box), np.float32) / 255.0
rgb = np.asarray(im.convert("RGBA"), np.float32)[..., :3] / 255.0
white = rgb * a[..., None] + (1 - a[..., None])
w, h = im.size
wpx = WCELLS * 2
hpx = max(4, round(wpx * h / w))
lum = Image.fromarray((white * 255).astype("uint8")).convert("L").resize((wpx, hpx), Image.Resampling.LANCZOS)
opa = Image.fromarray((a * 255).astype("uint8")).resize((wpx, hpx), Image.Resampling.LANCZOS)
mno = Image.fromarray((mn * 255).astype("uint8")).resize((wpx, hpx), Image.Resampling.LANCZOS)
g = np.asarray(lum, np.float32) / 255.0
op = np.asarray(opa, np.float32) / 255.0
mr = np.asarray(mno, np.float32) / 255.0
mask = ((op > 0.5) & (g < DARK)) | (mr > 0.5)
H = (hpx + 3) // 4 * 4
W = (wpx + 1) // 2 * 2
m = np.zeros((H, W), bool)
m[:hpx, :wpx] = mask
lines = []
for by in range(0, H, 4):
row = []
for bx in range(0, W, 2):
bits = sum((1 << bit) for r, c, bit in _DOTS if m[by + r, bx + c])
row.append(chr(0x2800 + bits))
lines.append("".join(row))
return "\n".join(lines)
if __name__ == "__main__":
os.makedirs("frames", exist_ok=True)
first = {}
for name, spec in SOURCES.items():
frames = load_source(spec)
box = union_box(frames)
arts = [to_braille(f, a, mn, box) for f, a, mn in frames]
open(f"frames/{name}.txt", "w").write(SEP.join(arts))
first[name] = arts[0]
print(f"{name}: {len(arts)} frames box={box}")
open("frames/static.txt", "w").write(first[STATIC_FROM])
print("static: 1 frame")