Skip to content

Commit 7230f69

Browse files
committed
Optimize color ASCII rendering and fix module name
Fix the OpenCV error message to reference dlclivegui.assets.ascii_art and refactor _color_ascii_lines for performance and clarity. Changes include: encode ASCII ramp once, clarify luminance comment, pack colors into 0xRRGGBB, use a prefix_cache for ANSI SGR color prefixes, emit a color sequence only when the color changes (using prev_ck), use memoryview for row access, append a single reset per line, and simplify character emission. Functionality and ANSI formatting are preserved while reducing per-character allocations and cache complexity.
1 parent 6d7a3e6 commit 7230f69

File tree

1 file changed

+32
-27
lines changed

1 file changed

+32
-27
lines changed

dlclivegui/assets/ascii_art.py

Lines changed: 32 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
import cv2 as cv
2828
except Exception as e: # pragma: no cover
2929
raise RuntimeError(
30-
"OpenCV (opencv-python) is required for dlclivegui.assets.ascii.\nInstall with: pip install opencv-python"
30+
"OpenCV (opencv-python) is required for dlclivegui.assets.ascii_art.\nInstall with: pip install opencv-python"
3131
) from e
3232

3333
# Character ramps (dense -> sparse)
@@ -205,56 +205,61 @@ def _map_luminance_to_chars(gray: np.ndarray, fine: bool) -> Iterable[str]:
205205

206206
def _color_ascii_lines(img_bgr: np.ndarray, fine: bool, invert: bool) -> Iterable[str]:
207207
ramp = ASCII_RAMP_FINE if fine else ASCII_RAMP_SIMPLE
208-
ramp_bytes = [c.encode("utf-8") for c in ramp] # 1-byte ASCII in practice
208+
# ramp is ASCII; encode once
209+
ramp_bytes = [c.encode("utf-8") for c in ramp]
210+
209211
reset = b"\x1b[0m"
210212

211-
# luminance in float32 like your current code
213+
# Luminance (same coefficients you used; keep exact behavior)
212214
b = img_bgr[..., 0].astype(np.float32)
213215
g = img_bgr[..., 1].astype(np.float32)
214216
r = img_bgr[..., 2].astype(np.float32)
215217
lum = 0.0722 * b + 0.7152 * g + 0.2126 * r
216218
if invert:
217219
lum = 255.0 - lum
218220

219-
idx = (lum / 255.0 * (len(ramp) - 1)).astype(np.uint16) # small dtype is fine
221+
idx = (lum / 255.0 * (len(ramp) - 1)).astype(np.uint16)
220222

221-
# Pack color into one int: 0xRRGGBB (faster dict key than tuple)
223+
# Pack color into 0xRRGGBB for fast comparisons
222224
rr = img_bgr[..., 2].astype(np.uint32)
223225
gg = img_bgr[..., 1].astype(np.uint32)
224226
bb = img_bgr[..., 0].astype(np.uint32)
225227
color_key = (rr << 16) | (gg << 8) | bb # (H,W) uint32
226228

227-
# Cache: (color_key<<8)|idx -> bytes for full colored char INCLUDING reset
228-
cache: dict[int, bytes] = {}
229+
# Cache SGR prefixes by packed color
230+
# e.g. 0xRRGGBB -> b"\x1b[38;2;R;G;Bm"
231+
prefix_cache: dict[int, bytes] = {}
229232

230233
h, w = idx.shape
231234
lines: list[str] = []
232235

233236
for y in range(h):
234237
ba = bytearray()
235-
ck_row = color_key[y]
236-
idx_row = idx[y]
237-
img_bgr[y] # for extracting r/g/b when cache miss
238+
239+
ck_row = memoryview(color_key[y])
240+
idx_row = memoryview(idx[y])
241+
242+
prev_ck: int | None = None
238243

239244
for x in range(w):
240-
ik = int(idx_row[x])
241245
ck = int(ck_row[x])
242-
subkey = (ck << 8) | ik
243-
244-
piece = cache.get(subkey)
245-
if piece is None:
246-
# Decode r,g,b from packed key (same as current rr,gg,bb)
247-
rr_i = (ck >> 16) & 255
248-
gg_i = (ck >> 8) & 255
249-
bb_i = ck & 255
250-
251-
# EXACT same formatting as before
252-
# \x1b[38;2;{rr};{gg};{bb}m{ch}\x1b[0m
253-
prefix = f"\x1b[38;2;{rr_i};{gg_i};{bb_i}m".encode("ascii")
254-
piece = prefix + ramp_bytes[ik] + reset
255-
cache[subkey] = piece
256-
257-
ba.extend(piece)
246+
247+
# Emit new color code only when color changes
248+
if ck != prev_ck:
249+
prefix = prefix_cache.get(ck)
250+
if prefix is None:
251+
rr_i = (ck >> 16) & 255
252+
gg_i = (ck >> 8) & 255
253+
bb_i = ck & 255
254+
prefix = f"\x1b[38;2;{rr_i};{gg_i};{bb_i}m".encode("ascii")
255+
prefix_cache[ck] = prefix
256+
ba.extend(prefix)
257+
prev_ck = ck
258+
259+
ba.extend(ramp_bytes[int(idx_row[x])])
260+
261+
# Reset once per line to prevent color bleed into subsequent terminal output
262+
ba.extend(reset)
258263

259264
lines.append(ba.decode("utf-8", errors="strict"))
260265

0 commit comments

Comments
 (0)