|
27 | 27 | import cv2 as cv |
28 | 28 | except Exception as e: # pragma: no cover |
29 | 29 | 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" |
31 | 31 | ) from e |
32 | 32 |
|
33 | 33 | # Character ramps (dense -> sparse) |
@@ -205,56 +205,61 @@ def _map_luminance_to_chars(gray: np.ndarray, fine: bool) -> Iterable[str]: |
205 | 205 |
|
206 | 206 | def _color_ascii_lines(img_bgr: np.ndarray, fine: bool, invert: bool) -> Iterable[str]: |
207 | 207 | 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 | + |
209 | 211 | reset = b"\x1b[0m" |
210 | 212 |
|
211 | | - # luminance in float32 like your current code |
| 213 | + # Luminance (same coefficients you used; keep exact behavior) |
212 | 214 | b = img_bgr[..., 0].astype(np.float32) |
213 | 215 | g = img_bgr[..., 1].astype(np.float32) |
214 | 216 | r = img_bgr[..., 2].astype(np.float32) |
215 | 217 | lum = 0.0722 * b + 0.7152 * g + 0.2126 * r |
216 | 218 | if invert: |
217 | 219 | lum = 255.0 - lum |
218 | 220 |
|
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) |
220 | 222 |
|
221 | | - # Pack color into one int: 0xRRGGBB (faster dict key than tuple) |
| 223 | + # Pack color into 0xRRGGBB for fast comparisons |
222 | 224 | rr = img_bgr[..., 2].astype(np.uint32) |
223 | 225 | gg = img_bgr[..., 1].astype(np.uint32) |
224 | 226 | bb = img_bgr[..., 0].astype(np.uint32) |
225 | 227 | color_key = (rr << 16) | (gg << 8) | bb # (H,W) uint32 |
226 | 228 |
|
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] = {} |
229 | 232 |
|
230 | 233 | h, w = idx.shape |
231 | 234 | lines: list[str] = [] |
232 | 235 |
|
233 | 236 | for y in range(h): |
234 | 237 | 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 |
238 | 243 |
|
239 | 244 | for x in range(w): |
240 | | - ik = int(idx_row[x]) |
241 | 245 | 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) |
258 | 263 |
|
259 | 264 | lines.append(ba.decode("utf-8", errors="strict")) |
260 | 265 |
|
|
0 commit comments