pixelification 1.2.5__tar.gz → 1.2.8__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {pixelification-1.2.5 → pixelification-1.2.8}/PKG-INFO +52 -3
- {pixelification-1.2.5 → pixelification-1.2.8}/README.md +51 -2
- {pixelification-1.2.5 → pixelification-1.2.8}/src/pixelification/main.py +366 -3
- {pixelification-1.2.5 → pixelification-1.2.8}/.github/workflows/publish.yml +0 -0
- {pixelification-1.2.5 → pixelification-1.2.8}/.gitignore +0 -0
- {pixelification-1.2.5 → pixelification-1.2.8}/.python-version +0 -0
- {pixelification-1.2.5 → pixelification-1.2.8}/Cargo.lock +0 -0
- {pixelification-1.2.5 → pixelification-1.2.8}/Cargo.toml +0 -0
- {pixelification-1.2.5 → pixelification-1.2.8}/main.py +0 -0
- {pixelification-1.2.5 → pixelification-1.2.8}/pyproject.toml +0 -0
- {pixelification-1.2.5 → pixelification-1.2.8}/requirements.txt +0 -0
- {pixelification-1.2.5 → pixelification-1.2.8}/src/app/mod.rs +0 -0
- {pixelification-1.2.5 → pixelification-1.2.8}/src/config/mod.rs +0 -0
- {pixelification-1.2.5 → pixelification-1.2.8}/src/drag/mod.rs +0 -0
- {pixelification-1.2.5 → pixelification-1.2.8}/src/events/mod.rs +0 -0
- {pixelification-1.2.5 → pixelification-1.2.8}/src/main.rs +0 -0
- {pixelification-1.2.5 → pixelification-1.2.8}/src/pixelification/__init__.py +0 -0
- {pixelification-1.2.5 → pixelification-1.2.8}/src/pixelification/runtime.py +0 -0
- {pixelification-1.2.5 → pixelification-1.2.8}/src/state/mod.rs +0 -0
- {pixelification-1.2.5 → pixelification-1.2.8}/src/ui/animated.rs +0 -0
- {pixelification-1.2.5 → pixelification-1.2.8}/src/ui/bitmap.rs +0 -0
- {pixelification-1.2.5 → pixelification-1.2.8}/src/ui/brush.rs +0 -0
- {pixelification-1.2.5 → pixelification-1.2.8}/src/ui/color.rs +0 -0
- {pixelification-1.2.5 → pixelification-1.2.8}/src/ui/font.rs +0 -0
- {pixelification-1.2.5 → pixelification-1.2.8}/src/ui/mod.rs +0 -0
- {pixelification-1.2.5 → pixelification-1.2.8}/src/ui/render.rs +0 -0
- {pixelification-1.2.5 → pixelification-1.2.8}/src/webview/mod.rs +0 -0
- {pixelification-1.2.5 → pixelification-1.2.8}/src/win32.rs +0 -0
- {pixelification-1.2.5 → pixelification-1.2.8}/uv.lock +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pixelification
|
|
3
|
-
Version: 1.2.
|
|
3
|
+
Version: 1.2.8
|
|
4
4
|
Summary: Pixel Rearrangement Tool — Keyboard-Navigated Terminal UI
|
|
5
5
|
Requires-Python: >=3.10
|
|
6
6
|
Requires-Dist: numpy>=1.20.0
|
|
@@ -187,9 +187,15 @@ pipx run pixelification
|
|
|
187
187
|
|
|
188
188
|
---
|
|
189
189
|
|
|
190
|
-
## ◈ Usage
|
|
190
|
+
## ◈ Usage — TUI
|
|
191
191
|
|
|
192
|
-
|
|
192
|
+
Launch the interactive terminal UI (no arguments needed):
|
|
193
|
+
|
|
194
|
+
```bash
|
|
195
|
+
pixelification
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
A keyboard-navigated menu opens:
|
|
193
199
|
|
|
194
200
|
```
|
|
195
201
|
■ Pixel Rearrangement Tool
|
|
@@ -237,6 +243,49 @@ A keyboard-navigated terminal UI opens immediately:
|
|
|
237
243
|
|
|
238
244
|
---
|
|
239
245
|
|
|
246
|
+
## ◈ Usage — CLI
|
|
247
|
+
|
|
248
|
+
For scripting, batch processing, or headless environments, use the subcommand interface:
|
|
249
|
+
|
|
250
|
+
```bash
|
|
251
|
+
pixelification img2img <source> <target> [options]
|
|
252
|
+
pixelification vid2vid <source> <target> [options]
|
|
253
|
+
pixelification img2ascii <image> [options]
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
Output paths default to auto-named files in the current directory (e.g. `reconstructed_src_from_tgt.png`).
|
|
257
|
+
|
|
258
|
+
### Commands
|
|
259
|
+
|
|
260
|
+
```
|
|
261
|
+
pixelification img2img <source> <target>
|
|
262
|
+
Rearrange pixels from a source image to approximate a target image.
|
|
263
|
+
|
|
264
|
+
-o, --output FILE output image path (default: auto-named)
|
|
265
|
+
--show open an OpenCV window with the result
|
|
266
|
+
--cpu force CPU-only mode
|
|
267
|
+
|
|
268
|
+
pixelification vid2vid <source> <target>
|
|
269
|
+
Rearrange every frame of a source (video or still image) to match
|
|
270
|
+
a target video. Prints a progress bar to stderr.
|
|
271
|
+
|
|
272
|
+
-o, --output FILE output video path (default: auto-named)
|
|
273
|
+
--show play the result in an OpenCV window
|
|
274
|
+
--cpu force CPU-only mode
|
|
275
|
+
|
|
276
|
+
pixelification img2ascii <image>
|
|
277
|
+
Convert an image to ASCII art. Prints to stdout and saves to file.
|
|
278
|
+
|
|
279
|
+
-o, --output FILE output text path (default: auto-named)
|
|
280
|
+
-w, --width CHARS output width in characters (default: 120)
|
|
281
|
+
--no-dither disable Floyd-Steinberg dithering
|
|
282
|
+
|
|
283
|
+
pixelification --version / -v
|
|
284
|
+
Print the installed version.
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
---
|
|
288
|
+
|
|
240
289
|
## ◈ Requirements
|
|
241
290
|
|
|
242
291
|
```
|
|
@@ -175,9 +175,15 @@ pipx run pixelification
|
|
|
175
175
|
|
|
176
176
|
---
|
|
177
177
|
|
|
178
|
-
## ◈ Usage
|
|
178
|
+
## ◈ Usage — TUI
|
|
179
179
|
|
|
180
|
-
|
|
180
|
+
Launch the interactive terminal UI (no arguments needed):
|
|
181
|
+
|
|
182
|
+
```bash
|
|
183
|
+
pixelification
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
A keyboard-navigated menu opens:
|
|
181
187
|
|
|
182
188
|
```
|
|
183
189
|
■ Pixel Rearrangement Tool
|
|
@@ -225,6 +231,49 @@ A keyboard-navigated terminal UI opens immediately:
|
|
|
225
231
|
|
|
226
232
|
---
|
|
227
233
|
|
|
234
|
+
## ◈ Usage — CLI
|
|
235
|
+
|
|
236
|
+
For scripting, batch processing, or headless environments, use the subcommand interface:
|
|
237
|
+
|
|
238
|
+
```bash
|
|
239
|
+
pixelification img2img <source> <target> [options]
|
|
240
|
+
pixelification vid2vid <source> <target> [options]
|
|
241
|
+
pixelification img2ascii <image> [options]
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
Output paths default to auto-named files in the current directory (e.g. `reconstructed_src_from_tgt.png`).
|
|
245
|
+
|
|
246
|
+
### Commands
|
|
247
|
+
|
|
248
|
+
```
|
|
249
|
+
pixelification img2img <source> <target>
|
|
250
|
+
Rearrange pixels from a source image to approximate a target image.
|
|
251
|
+
|
|
252
|
+
-o, --output FILE output image path (default: auto-named)
|
|
253
|
+
--show open an OpenCV window with the result
|
|
254
|
+
--cpu force CPU-only mode
|
|
255
|
+
|
|
256
|
+
pixelification vid2vid <source> <target>
|
|
257
|
+
Rearrange every frame of a source (video or still image) to match
|
|
258
|
+
a target video. Prints a progress bar to stderr.
|
|
259
|
+
|
|
260
|
+
-o, --output FILE output video path (default: auto-named)
|
|
261
|
+
--show play the result in an OpenCV window
|
|
262
|
+
--cpu force CPU-only mode
|
|
263
|
+
|
|
264
|
+
pixelification img2ascii <image>
|
|
265
|
+
Convert an image to ASCII art. Prints to stdout and saves to file.
|
|
266
|
+
|
|
267
|
+
-o, --output FILE output text path (default: auto-named)
|
|
268
|
+
-w, --width CHARS output width in characters (default: 120)
|
|
269
|
+
--no-dither disable Floyd-Steinberg dithering
|
|
270
|
+
|
|
271
|
+
pixelification --version / -v
|
|
272
|
+
Print the installed version.
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
---
|
|
276
|
+
|
|
228
277
|
## ◈ Requirements
|
|
229
278
|
|
|
230
279
|
```
|
|
@@ -5,11 +5,13 @@ Rearrange image pixels or video frames via colour-sort optimal transport.
|
|
|
5
5
|
OpenCV window shows the result (animation for images, playback for video).
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
+
import argparse
|
|
8
9
|
import asyncio
|
|
9
10
|
import cv2
|
|
10
11
|
import numpy as np
|
|
11
12
|
import os
|
|
12
13
|
import shutil
|
|
14
|
+
import signal
|
|
13
15
|
import subprocess
|
|
14
16
|
import sys
|
|
15
17
|
import tempfile
|
|
@@ -27,6 +29,11 @@ from pixelification.runtime import RuntimeConfig, load_or_create_runtime_config
|
|
|
27
29
|
# ── GPU & Accelerator Support ────────────────────────────────────────
|
|
28
30
|
|
|
29
31
|
FORCE_CPU = False
|
|
32
|
+
_RUNNING = True
|
|
33
|
+
|
|
34
|
+
def _sigint_handler(signum, frame):
|
|
35
|
+
global _RUNNING
|
|
36
|
+
_RUNNING = False
|
|
30
37
|
|
|
31
38
|
try:
|
|
32
39
|
import cupy as cp
|
|
@@ -120,6 +127,7 @@ ASCII_ART = [
|
|
|
120
127
|
]
|
|
121
128
|
|
|
122
129
|
_IMAGE_EXTS = frozenset({'.png', '.jpg', '.jpeg', '.bmp', '.tiff', '.gif', '.webp'})
|
|
130
|
+
_VIDEO_EXTS = frozenset({'.mp4', '.avi', '.mov', '.mkv', '.webm'})
|
|
123
131
|
|
|
124
132
|
|
|
125
133
|
# ── Native File Dialog ───────────────────────────────────────────────
|
|
@@ -279,6 +287,31 @@ def compute_sort_keys(img, xp=np):
|
|
|
279
287
|
return lum, hsv[:, 0], hsv[:, 1]
|
|
280
288
|
|
|
281
289
|
|
|
290
|
+
def _compute_rearrangement(source_path: str, target_path: str) -> np.ndarray:
|
|
291
|
+
img_src = cv2.imread(source_path, cv2.IMREAD_COLOR)
|
|
292
|
+
img_tgt = cv2.imread(target_path, cv2.IMREAD_COLOR)
|
|
293
|
+
if img_src is None:
|
|
294
|
+
raise FileNotFoundError(f"Cannot read source: {source_path}")
|
|
295
|
+
if img_tgt is None:
|
|
296
|
+
raise FileNotFoundError(f"Cannot read target: {target_path}")
|
|
297
|
+
|
|
298
|
+
h, w = img_src.shape[:2]
|
|
299
|
+
img_tgt = cv2.resize(img_tgt, (w, h))
|
|
300
|
+
|
|
301
|
+
xp = get_xp()
|
|
302
|
+
s_l, s_h, s_s = compute_sort_keys(img_src, xp)
|
|
303
|
+
t_l, t_h, t_s = compute_sort_keys(img_tgt, xp)
|
|
304
|
+
s_order = xp_lexsort((s_s, s_h, s_l), xp)
|
|
305
|
+
t_order = xp_lexsort((t_s, t_h, t_l), xp)
|
|
306
|
+
|
|
307
|
+
out_flat = xp.empty_like(xp.array(img_src.reshape(-1, 3)), dtype=xp.uint8)
|
|
308
|
+
out_flat[t_order] = xp.array(img_src.reshape(-1, 3))[s_order]
|
|
309
|
+
out_img = out_flat.reshape(h, w, 3)
|
|
310
|
+
out_img = to_np(out_img)
|
|
311
|
+
|
|
312
|
+
return out_img
|
|
313
|
+
|
|
314
|
+
|
|
282
315
|
def get_screen_resolution():
|
|
283
316
|
try:
|
|
284
317
|
import ctypes
|
|
@@ -461,6 +494,120 @@ def letterbox_pad(img, target_w, target_h):
|
|
|
461
494
|
return canvas
|
|
462
495
|
|
|
463
496
|
|
|
497
|
+
def _compute_video_rearrangement(
|
|
498
|
+
source_path: str,
|
|
499
|
+
target_path: str,
|
|
500
|
+
progress_callback=None,
|
|
501
|
+
) -> str:
|
|
502
|
+
src_is_img = Path(source_path).suffix.lower() in _IMAGE_EXTS
|
|
503
|
+
|
|
504
|
+
if src_is_img:
|
|
505
|
+
img_src = cv2.imread(source_path, cv2.IMREAD_COLOR)
|
|
506
|
+
if img_src is None:
|
|
507
|
+
raise FileNotFoundError(f"Cannot read source image: {source_path}")
|
|
508
|
+
h_src, w_src = img_src.shape[:2]
|
|
509
|
+
cap_tgt = cv2.VideoCapture(target_path)
|
|
510
|
+
if not cap_tgt.isOpened():
|
|
511
|
+
raise FileNotFoundError(f"Cannot open target video: {target_path}")
|
|
512
|
+
total_tgt = int(cap_tgt.get(cv2.CAP_PROP_FRAME_COUNT))
|
|
513
|
+
total = total_tgt
|
|
514
|
+
if total == 0:
|
|
515
|
+
raise ValueError("Target video has no frames")
|
|
516
|
+
fps = cap_tgt.get(cv2.CAP_PROP_FPS)
|
|
517
|
+
else:
|
|
518
|
+
cap_src = cv2.VideoCapture(source_path)
|
|
519
|
+
cap_tgt = cv2.VideoCapture(target_path)
|
|
520
|
+
if not cap_src.isOpened():
|
|
521
|
+
raise FileNotFoundError(f"Cannot open source video: {source_path}")
|
|
522
|
+
if not cap_tgt.isOpened():
|
|
523
|
+
raise FileNotFoundError(f"Cannot open target video: {target_path}")
|
|
524
|
+
total_src = int(cap_src.get(cv2.CAP_PROP_FRAME_COUNT))
|
|
525
|
+
total_tgt = int(cap_tgt.get(cv2.CAP_PROP_FRAME_COUNT))
|
|
526
|
+
total = min(total_src, total_tgt)
|
|
527
|
+
if total == 0:
|
|
528
|
+
raise ValueError("One or both videos have no frames")
|
|
529
|
+
fps = cap_src.get(cv2.CAP_PROP_FPS)
|
|
530
|
+
h_src = int(cap_src.get(cv2.CAP_PROP_FRAME_WIDTH))
|
|
531
|
+
w_src = int(cap_src.get(cv2.CAP_PROP_FRAME_HEIGHT))
|
|
532
|
+
|
|
533
|
+
w_tgt = int(cap_tgt.get(cv2.CAP_PROP_FRAME_WIDTH))
|
|
534
|
+
h_tgt = int(cap_tgt.get(cv2.CAP_PROP_FRAME_HEIGHT))
|
|
535
|
+
|
|
536
|
+
if w_src == 0 or h_src == 0:
|
|
537
|
+
raise ValueError(f"source is not a valid video file (0x0 dimensions)")
|
|
538
|
+
if w_tgt == 0 or h_tgt == 0:
|
|
539
|
+
raise ValueError(f"target is not a valid video file (0x0 dimensions)")
|
|
540
|
+
|
|
541
|
+
ar_src = w_src / h_src
|
|
542
|
+
ar_tgt = w_tgt / h_tgt
|
|
543
|
+
ar_diff = abs(ar_src - ar_tgt) > 0.01
|
|
544
|
+
|
|
545
|
+
if ar_diff:
|
|
546
|
+
if ar_src >= ar_tgt:
|
|
547
|
+
out_w, out_h = w_src, h_src
|
|
548
|
+
pad_src, pad_tgt = False, True
|
|
549
|
+
else:
|
|
550
|
+
out_w, out_h = w_tgt, h_tgt
|
|
551
|
+
pad_src, pad_tgt = True, False
|
|
552
|
+
else:
|
|
553
|
+
out_w, out_h = w_src, h_src
|
|
554
|
+
pad_src, pad_tgt = False, False
|
|
555
|
+
|
|
556
|
+
fd, tmp_path = tempfile.mkstemp(suffix=".mp4")
|
|
557
|
+
os.close(fd)
|
|
558
|
+
|
|
559
|
+
fourcc = cv2.VideoWriter_fourcc(*'mp4v')
|
|
560
|
+
writer = cv2.VideoWriter(tmp_path, fourcc, fps, (out_w, out_h))
|
|
561
|
+
|
|
562
|
+
xp = get_xp()
|
|
563
|
+
|
|
564
|
+
for i in range(total):
|
|
565
|
+
if not _RUNNING:
|
|
566
|
+
raise KeyboardInterrupt()
|
|
567
|
+
|
|
568
|
+
if src_is_img:
|
|
569
|
+
src_frame = img_src.copy()
|
|
570
|
+
else:
|
|
571
|
+
ret_src, src_frame = cap_src.read()
|
|
572
|
+
if not ret_src:
|
|
573
|
+
break
|
|
574
|
+
|
|
575
|
+
ret_tgt, tgt_frame = cap_tgt.read()
|
|
576
|
+
if not ret_tgt:
|
|
577
|
+
break
|
|
578
|
+
|
|
579
|
+
if pad_src:
|
|
580
|
+
src_frame = letterbox_pad(src_frame, out_w, out_h)
|
|
581
|
+
if pad_tgt:
|
|
582
|
+
tgt_frame = letterbox_pad(tgt_frame, out_w, out_h)
|
|
583
|
+
if not pad_src and not pad_tgt and tgt_frame.shape[:2] != src_frame.shape[:2]:
|
|
584
|
+
tgt_frame = cv2.resize(tgt_frame, (out_w, out_h))
|
|
585
|
+
|
|
586
|
+
s_l, s_h, s_s = compute_sort_keys(src_frame, xp)
|
|
587
|
+
t_l, t_h, t_s = compute_sort_keys(tgt_frame, xp)
|
|
588
|
+
s_order = xp_lexsort((s_s, s_h, s_l), xp)
|
|
589
|
+
t_order = xp_lexsort((t_s, t_h, t_l), xp)
|
|
590
|
+
|
|
591
|
+
src_flat = xp.array(src_frame.reshape(-1, 3))
|
|
592
|
+
out_flat = xp.empty_like(src_flat, dtype=xp.uint8)
|
|
593
|
+
out_flat[t_order] = src_flat[s_order]
|
|
594
|
+
out_frame = out_flat.reshape(out_h, out_w, 3)
|
|
595
|
+
|
|
596
|
+
out_frame = to_np(out_frame)
|
|
597
|
+
|
|
598
|
+
writer.write(out_frame)
|
|
599
|
+
|
|
600
|
+
if progress_callback:
|
|
601
|
+
progress_callback(i + 1, total)
|
|
602
|
+
|
|
603
|
+
if not src_is_img:
|
|
604
|
+
cap_src.release()
|
|
605
|
+
cap_tgt.release()
|
|
606
|
+
writer.release()
|
|
607
|
+
|
|
608
|
+
return tmp_path
|
|
609
|
+
|
|
610
|
+
|
|
464
611
|
def rearrange_video(source_path: str, target_path: str, state: State) -> None:
|
|
465
612
|
try:
|
|
466
613
|
src_is_img = Path(source_path).suffix.lower() in _IMAGE_EXTS
|
|
@@ -692,6 +839,157 @@ def image_to_ascii(path: str, width: int = 120, dither: bool = True) -> str:
|
|
|
692
839
|
return "\n".join(lines)
|
|
693
840
|
|
|
694
841
|
|
|
842
|
+
# ── CLI Handlers ─────────────────────────────────────────────────────
|
|
843
|
+
|
|
844
|
+
def _progress_bar(current, total, bar_len=20):
|
|
845
|
+
filled = int(bar_len * current / total)
|
|
846
|
+
bar = "\u2588" * filled + "\u2591" * (bar_len - filled)
|
|
847
|
+
print(f"\r [{bar}] {current}/{total} ({100*current/total:.0f}%)", end="", file=sys.stderr)
|
|
848
|
+
if current == total:
|
|
849
|
+
print(file=sys.stderr)
|
|
850
|
+
|
|
851
|
+
|
|
852
|
+
def _cli_img2img(args):
|
|
853
|
+
try:
|
|
854
|
+
if args.cpu:
|
|
855
|
+
global FORCE_CPU; FORCE_CPU = True
|
|
856
|
+
|
|
857
|
+
src_ext = Path(args.source).suffix.lower()
|
|
858
|
+
tgt_ext = Path(args.target).suffix.lower()
|
|
859
|
+
if src_ext not in _IMAGE_EXTS:
|
|
860
|
+
print(f"Error: source must be an image ({', '.join(sorted(_IMAGE_EXTS))})", file=sys.stderr)
|
|
861
|
+
sys.exit(1)
|
|
862
|
+
if tgt_ext not in _IMAGE_EXTS:
|
|
863
|
+
print(f"Error: target must be an image ({', '.join(sorted(_IMAGE_EXTS))})", file=sys.stderr)
|
|
864
|
+
sys.exit(1)
|
|
865
|
+
|
|
866
|
+
out_path = args.output
|
|
867
|
+
if not out_path:
|
|
868
|
+
src_stem = Path(args.source).stem
|
|
869
|
+
tgt_stem = Path(args.target).stem
|
|
870
|
+
out_path = f"reconstructed_{src_stem}_from_{tgt_stem}.png"
|
|
871
|
+
|
|
872
|
+
result = _compute_rearrangement(args.source, args.target)
|
|
873
|
+
cv2.imwrite(out_path, result)
|
|
874
|
+
print(out_path)
|
|
875
|
+
|
|
876
|
+
if args.show:
|
|
877
|
+
img_src = cv2.imread(args.source, cv2.IMREAD_COLOR)
|
|
878
|
+
img_tgt = cv2.imread(args.target, cv2.IMREAD_COLOR)
|
|
879
|
+
h, w = img_src.shape[:2]
|
|
880
|
+
img_tgt = cv2.resize(img_tgt, (w, h))
|
|
881
|
+
sw, sh = get_screen_resolution()
|
|
882
|
+
canvas = np.full((sh, sw, 3), 32, dtype=np.uint8)
|
|
883
|
+
label_h = 22
|
|
884
|
+
pw = sw // 3
|
|
885
|
+
ph = sh - label_h
|
|
886
|
+
sc = min(pw / w, ph / h)
|
|
887
|
+
iw, ih = max(int(w * sc), 1), max(int(h * sc), 1)
|
|
888
|
+
src_s = cv2.resize(img_src, (iw, ih), interpolation=cv2.INTER_LANCZOS4)
|
|
889
|
+
tgt_s = cv2.resize(img_tgt, (iw, ih), interpolation=cv2.INTER_LANCZOS4)
|
|
890
|
+
cx = (pw - iw) // 2
|
|
891
|
+
cy = label_h + (ph - ih) // 2
|
|
892
|
+
canvas[cy:cy+ih, cx:cx+iw] = src_s
|
|
893
|
+
canvas[cy:cy+ih, pw+cx:pw+cx+iw] = tgt_s
|
|
894
|
+
rec_x = 2 * pw + cx
|
|
895
|
+
rec_region = canvas[cy:cy+ih, rec_x:rec_x+iw]
|
|
896
|
+
font = cv2.FONT_HERSHEY_SIMPLEX
|
|
897
|
+
for label, xo in [("Source", 0), ("Target", pw), ("Reconstruction", 2 * pw)]:
|
|
898
|
+
cv2.rectangle(canvas, (xo, 0), (xo + pw, label_h), (0, 0, 0), -1)
|
|
899
|
+
cv2.putText(canvas, label, (xo + 6, 16), font, 0.45, (200, 200, 200), 1)
|
|
900
|
+
wn = "Pixel Rearrangement (ESC/q anytime to quit)"
|
|
901
|
+
cv2.namedWindow(wn, cv2.WINDOW_NORMAL)
|
|
902
|
+
cv2.resizeWindow(wn, sw, sh)
|
|
903
|
+
rec_region[:] = cv2.resize(result, (iw, ih))
|
|
904
|
+
cv2.imshow(wn, canvas)
|
|
905
|
+
while True:
|
|
906
|
+
key = cv2.waitKey(100) & 0xFF
|
|
907
|
+
if key in (27, ord("q")):
|
|
908
|
+
break
|
|
909
|
+
cv2.destroyAllWindows()
|
|
910
|
+
except Exception as e:
|
|
911
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
912
|
+
sys.exit(1)
|
|
913
|
+
|
|
914
|
+
|
|
915
|
+
def _cli_vid2vid(args):
|
|
916
|
+
try:
|
|
917
|
+
if args.cpu:
|
|
918
|
+
global FORCE_CPU; FORCE_CPU = True
|
|
919
|
+
|
|
920
|
+
src_ext = Path(args.source).suffix.lower()
|
|
921
|
+
tgt_ext = Path(args.target).suffix.lower()
|
|
922
|
+
if src_ext not in _IMAGE_EXTS and src_ext not in _VIDEO_EXTS:
|
|
923
|
+
print(f"Error: source must be an image or video ({', '.join(sorted(_IMAGE_EXTS | _VIDEO_EXTS))})", file=sys.stderr)
|
|
924
|
+
sys.exit(1)
|
|
925
|
+
if tgt_ext not in _VIDEO_EXTS:
|
|
926
|
+
print(f"Error: target must be a video ({', '.join(sorted(_VIDEO_EXTS))})", file=sys.stderr)
|
|
927
|
+
sys.exit(1)
|
|
928
|
+
|
|
929
|
+
def progress(cur, total):
|
|
930
|
+
_progress_bar(cur, total)
|
|
931
|
+
|
|
932
|
+
result_path = _compute_video_rearrangement(args.source, args.target, progress)
|
|
933
|
+
|
|
934
|
+
out_path = args.output
|
|
935
|
+
if not out_path:
|
|
936
|
+
src_stem = Path(args.source).stem
|
|
937
|
+
tgt_stem = Path(args.target).stem
|
|
938
|
+
out_path = f"rearranged_{src_stem}_from_{tgt_stem}.mp4"
|
|
939
|
+
|
|
940
|
+
shutil.copy2(result_path, out_path)
|
|
941
|
+
print(out_path)
|
|
942
|
+
|
|
943
|
+
try:
|
|
944
|
+
os.unlink(result_path)
|
|
945
|
+
except Exception:
|
|
946
|
+
pass
|
|
947
|
+
|
|
948
|
+
if args.show:
|
|
949
|
+
cap = cv2.VideoCapture(out_path)
|
|
950
|
+
wn = "Video Rearrangement Result (ESC/q anytime to quit)"
|
|
951
|
+
cv2.namedWindow(wn, cv2.WINDOW_NORMAL)
|
|
952
|
+
cv2.resizeWindow(wn, 1280, 720)
|
|
953
|
+
while cv2.getWindowProperty(wn, cv2.WND_PROP_VISIBLE) >= 1:
|
|
954
|
+
ret, frame = cap.read()
|
|
955
|
+
if not ret:
|
|
956
|
+
cap.set(cv2.CAP_PROP_POS_FRAMES, 0)
|
|
957
|
+
continue
|
|
958
|
+
cv2.imshow(wn, frame)
|
|
959
|
+
key = cv2.waitKey(30) & 0xFF
|
|
960
|
+
if key in (27, ord("q")):
|
|
961
|
+
break
|
|
962
|
+
cap.release()
|
|
963
|
+
cv2.destroyAllWindows()
|
|
964
|
+
except Exception as e:
|
|
965
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
966
|
+
sys.exit(1)
|
|
967
|
+
|
|
968
|
+
|
|
969
|
+
def _cli_img2ascii(args):
|
|
970
|
+
try:
|
|
971
|
+
ext = Path(args.image).suffix.lower()
|
|
972
|
+
if ext not in _IMAGE_EXTS:
|
|
973
|
+
print(f"Error: input must be an image ({', '.join(sorted(_IMAGE_EXTS))})", file=sys.stderr)
|
|
974
|
+
sys.exit(1)
|
|
975
|
+
|
|
976
|
+
ascii_str = image_to_ascii(args.image, args.width, not args.no_dither)
|
|
977
|
+
if not ascii_str:
|
|
978
|
+
print("Error: could not read image", file=sys.stderr)
|
|
979
|
+
sys.exit(1)
|
|
980
|
+
|
|
981
|
+
print(ascii_str)
|
|
982
|
+
|
|
983
|
+
out_path = args.output
|
|
984
|
+
if not out_path:
|
|
985
|
+
stem = Path(args.image).stem
|
|
986
|
+
out_path = f"{stem}_ascii.txt"
|
|
987
|
+
Path(out_path).write_text(ascii_str, encoding="utf-8")
|
|
988
|
+
except Exception as e:
|
|
989
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
990
|
+
sys.exit(1)
|
|
991
|
+
|
|
992
|
+
|
|
695
993
|
# ── TUI Application ──────────────────────────────────────────────────
|
|
696
994
|
|
|
697
995
|
class PixelTUI:
|
|
@@ -1312,14 +1610,79 @@ class PixelTUI:
|
|
|
1312
1610
|
|
|
1313
1611
|
|
|
1314
1612
|
def main():
|
|
1315
|
-
|
|
1613
|
+
parser = argparse.ArgumentParser(
|
|
1614
|
+
prog="pixelification",
|
|
1615
|
+
description="Rearrange image pixels via colour-sort optimal transport.",
|
|
1616
|
+
)
|
|
1617
|
+
parser.add_argument("--version", "-v", action="store_true", help="print version")
|
|
1618
|
+
|
|
1619
|
+
_orig_error = parser.error
|
|
1620
|
+
def _parser_error(msg):
|
|
1621
|
+
if "invalid choice" in msg:
|
|
1622
|
+
_orig_error(
|
|
1623
|
+
"missing subcommand.\n\n"
|
|
1624
|
+
" Usage: pixelification <command> [arguments]\n\n"
|
|
1625
|
+
" Commands:\n"
|
|
1626
|
+
" img2img <source> <target> Rearrange pixels between two images\n"
|
|
1627
|
+
" vid2vid <source> <target> Rearrange frames between two videos\n"
|
|
1628
|
+
" img2ascii <image> Convert an image to ASCII art\n"
|
|
1629
|
+
" help Show this help message"
|
|
1630
|
+
)
|
|
1631
|
+
_orig_error(msg)
|
|
1632
|
+
parser.error = _parser_error
|
|
1633
|
+
|
|
1634
|
+
sub = parser.add_subparsers(dest="command", metavar="")
|
|
1635
|
+
|
|
1636
|
+
p_img2img = sub.add_parser("img2img", help="rearrange pixels between two images")
|
|
1637
|
+
p_img2img.add_argument("source", help="source image path")
|
|
1638
|
+
p_img2img.add_argument("target", help="target image path")
|
|
1639
|
+
p_img2img.add_argument("-o", "--output", help="output image path (default: auto-named)")
|
|
1640
|
+
p_img2img.add_argument("--show", action="store_true", help="show the result in an OpenCV window")
|
|
1641
|
+
p_img2img.add_argument("--cpu", action="store_true", help="force CPU mode")
|
|
1642
|
+
|
|
1643
|
+
p_vid2vid = sub.add_parser("vid2vid", help="rearrange frames between two videos (or image->video)")
|
|
1644
|
+
p_vid2vid.add_argument("source", help="source video or image path")
|
|
1645
|
+
p_vid2vid.add_argument("target", help="target video path")
|
|
1646
|
+
p_vid2vid.add_argument("-o", "--output", help="output video path (default: auto-named)")
|
|
1647
|
+
p_vid2vid.add_argument("--show", action="store_true", help="play the result in an OpenCV window")
|
|
1648
|
+
p_vid2vid.add_argument("--cpu", action="store_true", help="force CPU mode")
|
|
1649
|
+
|
|
1650
|
+
p_ascii = sub.add_parser("img2ascii", help="convert an image to ASCII art")
|
|
1651
|
+
p_ascii.add_argument("image", help="source image path")
|
|
1652
|
+
p_ascii.add_argument("-o", "--output", help="output text path (default: auto-named)")
|
|
1653
|
+
p_ascii.add_argument("-w", "--width", type=int, default=120, help="ASCII output width in characters (default: 120)")
|
|
1654
|
+
p_ascii.add_argument("--no-dither", action="store_true", help="disable Floyd-Steinberg dithering")
|
|
1655
|
+
|
|
1656
|
+
p_help = sub.add_parser("help", help="show this help message and exit")
|
|
1657
|
+
|
|
1658
|
+
args = parser.parse_args()
|
|
1659
|
+
|
|
1660
|
+
if args.version:
|
|
1316
1661
|
try:
|
|
1317
1662
|
print(f"pixelification {version('pixelification')}")
|
|
1318
1663
|
except PackageNotFoundError:
|
|
1319
1664
|
print("pixelification (local development)")
|
|
1320
1665
|
return
|
|
1321
|
-
|
|
1322
|
-
|
|
1666
|
+
|
|
1667
|
+
if not args.command:
|
|
1668
|
+
runtime_config = load_or_create_runtime_config(HAS_CUPY)
|
|
1669
|
+
PixelTUI(runtime_config).run()
|
|
1670
|
+
return
|
|
1671
|
+
|
|
1672
|
+
handlers = {
|
|
1673
|
+
"img2img": _cli_img2img,
|
|
1674
|
+
"vid2vid": _cli_vid2vid,
|
|
1675
|
+
"img2ascii": _cli_img2ascii,
|
|
1676
|
+
"help": lambda _: parser.print_help(),
|
|
1677
|
+
}
|
|
1678
|
+
signal.signal(signal.SIGINT, _sigint_handler)
|
|
1679
|
+
|
|
1680
|
+
try:
|
|
1681
|
+
handlers[args.command](args)
|
|
1682
|
+
except KeyboardInterrupt:
|
|
1683
|
+
print(file=sys.stderr)
|
|
1684
|
+
print("Interrupted.", file=sys.stderr)
|
|
1685
|
+
os._exit(1)
|
|
1323
1686
|
|
|
1324
1687
|
|
|
1325
1688
|
# ── Entry Point ──────────────────────────────────────────────────────
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|