pixelification 0.1.0__py3-none-any.whl

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.
File without changes
pixelification/main.py ADDED
@@ -0,0 +1,531 @@
1
+ """
2
+ Pixel Rearrangement Tool — Keyboard-Navigated Terminal UI
3
+
4
+ Select images via a native file dialog, then watch the pixel rearrangement
5
+ animate row-by-row in an OpenCV window. No new pixels are created; only
6
+ existing pixels from the source image are reordered to match the target.
7
+
8
+ Usage:
9
+ python rearrange_pixels_tui.py
10
+ """
11
+
12
+ import asyncio
13
+ import cv2
14
+ import numpy as np
15
+ import os
16
+ import subprocess
17
+ import sys
18
+ import tempfile
19
+ import threading
20
+ from dataclasses import dataclass
21
+ from pathlib import Path
22
+
23
+ from prompt_toolkit import Application
24
+ from prompt_toolkit.key_binding import KeyBindings
25
+ from prompt_toolkit.layout import Layout, Window, FormattedTextControl
26
+ from prompt_toolkit.styles import Style
27
+
28
+
29
+ # ── Styling ──────────────────────────────────────────────────────────
30
+
31
+ STYLE = Style([
32
+ ("title", "bold #00d787"),
33
+ ("path-label", "bold #5f87ff"),
34
+ ("path-value", "#87afff"),
35
+ ("path-empty", "#585858 italic"),
36
+ ("divider", "#3a3a3a"),
37
+ ("menu-item", "bold #ffffff"),
38
+ ("menu-desc", "#6c6c6c"),
39
+ ("menu-cursor", "bold #000000 bg:#00d787"),
40
+ ("status", "#5faf5f"),
41
+ ("status-info", "#878787 italic"),
42
+ ("status-error", "#ff5f5f"),
43
+ ("status-warn", "#ffaf5f"),
44
+ ("help", "#585858 italic"),
45
+ ])
46
+
47
+
48
+ # ── Native File Dialog ───────────────────────────────────────────────
49
+
50
+ def _powershell_open_file(title: str) -> str | None:
51
+ """Open the native Windows file dialog via a temp PowerShell script.
52
+ Avoids command-line quoting issues with -Command."""
53
+ script = """Add-Type -AssemblyName System.Windows.Forms
54
+ $f = New-Object System.Windows.Forms.OpenFileDialog
55
+ $f.Filter = "Image Files (*.png;*.jpg;*.jpeg;*.bmp;*.tiff;*.gif;*.webp)|*.png;*.jpg;*.jpeg;*.bmp;*.tiff;*.gif;*.webp|All Files (*.*)|*.*"
56
+ $f.FilterIndex = 1
57
+ $f.RestoreDirectory = $true
58
+ if ($f.ShowDialog() -eq "OK") { $f.FileName }
59
+ """
60
+ tmp = None
61
+ try:
62
+ with tempfile.NamedTemporaryFile(
63
+ mode="w", suffix=".ps1", delete=False, encoding="utf-8",
64
+ ) as f:
65
+ f.write(script)
66
+ tmp = f.name
67
+ r = subprocess.run(
68
+ ["powershell", "-NoProfile", "-File", tmp],
69
+ capture_output=True, text=True, timeout=60,
70
+ creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0x08000000),
71
+ )
72
+ out = r.stdout.strip()
73
+ return out if out else None
74
+ except Exception:
75
+ return None
76
+ finally:
77
+ if tmp and os.path.exists(tmp):
78
+ try:
79
+ os.unlink(tmp)
80
+ except Exception:
81
+ pass
82
+
83
+
84
+ def _tkinter_open_file(title: str) -> str | None:
85
+ """Fallback: open file dialog via tkinter."""
86
+ try:
87
+ import tkinter as tk
88
+ from tkinter import filedialog
89
+ root = tk.Tk()
90
+ root.withdraw()
91
+ root.attributes("-topmost", True)
92
+ path = filedialog.askopenfilename(
93
+ parent=root, title=title,
94
+ filetypes=[
95
+ ("Image files", "*.png *.jpg *.jpeg *.bmp *.tiff *.gif *.webp"),
96
+ ("All files", "*.*"),
97
+ ],
98
+ )
99
+ root.destroy()
100
+ return path if path else None
101
+ except Exception:
102
+ return None
103
+
104
+
105
+ def select_file(title: str = "Select Image") -> str | None:
106
+ """Open a native file-selection dialog. Uses PowerShell on Windows,
107
+ tkinter elsewhere. Returns the selected path or *None* if cancelled."""
108
+ if os.name == "nt":
109
+ path = _powershell_open_file(title)
110
+ if path:
111
+ return path
112
+ path = _tkinter_open_file(title)
113
+ return path if path else None
114
+
115
+
116
+ # ── Application State ────────────────────────────────────────────────
117
+
118
+ @dataclass
119
+ class State:
120
+ source: str = ""
121
+ target: str = ""
122
+ status: str = "Ready"
123
+ status_style: str = "status"
124
+ info: str = ""
125
+ cursor: int = 0 # 0-3 → source / target / run / quit
126
+ running: bool = False
127
+ done: bool = False
128
+
129
+ MENU = [
130
+ ("Select Source Image", "choose the image whose pixels will be rearranged"),
131
+ ("Select Target Image", "choose the image whose layout will be approximated"),
132
+ ("Run Rearrangement", "execute the sort-based pixel-matching algorithm"),
133
+ ("Quit", "exit the application"),
134
+ ]
135
+
136
+
137
+ # ── Rearrangement Engine ─────────────────────────────────────────────
138
+
139
+ def compute_sort_keys(img):
140
+ h, w = img.shape[:2]
141
+ flat = img.reshape(-1, 3).astype(np.float32)
142
+ lum = 0.299 * flat[:, 2] + 0.587 * flat[:, 1] + 0.114 * flat[:, 0]
143
+ hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV_FULL).reshape(-1, 3).astype(np.float32)
144
+ return lum, hsv[:, 0], hsv[:, 1]
145
+
146
+
147
+ def get_screen_resolution():
148
+ """Attempt to get screen resolution across platforms."""
149
+ try:
150
+ import ctypes
151
+ sw = ctypes.windll.user32.GetSystemMetrics(0)
152
+ sh = ctypes.windll.user32.GetSystemMetrics(1)
153
+ if sw > 0 and sh > 0:
154
+ return sw, sh
155
+ except Exception:
156
+ pass
157
+
158
+ try:
159
+ import tkinter as tk
160
+ root = tk.Tk()
161
+ root.withdraw()
162
+ sw = root.winfo_screenwidth()
163
+ sh = root.winfo_screenheight()
164
+ root.destroy()
165
+ if sw > 0 and sh > 0:
166
+ return sw, sh
167
+ except Exception:
168
+ pass
169
+
170
+ return 1920, 1080
171
+
172
+
173
+ def rearrange(source_path: str, target_path: str, state: State) -> None:
174
+ """Run the pixel-rearrangement algorithm and show the OpenCV window.
175
+ Called from a background thread."""
176
+ try:
177
+ img_src = cv2.imread(source_path, cv2.IMREAD_COLOR)
178
+ img_tgt = cv2.imread(target_path, cv2.IMREAD_COLOR)
179
+
180
+ if img_src is None:
181
+ state.status = f"Can't read source: {Path(source_path).name}"
182
+ state.status_style = "status-error"; return
183
+ if img_tgt is None:
184
+ state.status = f"Can't read target: {Path(target_path).name}"
185
+ state.status_style = "status-error"; return
186
+
187
+ h, w = img_src.shape[:2]
188
+ img_tgt = cv2.resize(img_tgt, (w, h))
189
+
190
+ # ── optimal transport via colour sorting ──
191
+ s_l, s_h, s_s = compute_sort_keys(img_src)
192
+ t_l, t_h, t_s = compute_sort_keys(img_tgt)
193
+ s_order = np.lexsort((s_s, s_h, s_l))
194
+ t_order = np.lexsort((t_s, t_h, t_l))
195
+
196
+ out = np.empty_like(img_src.reshape(-1, 3), dtype=np.uint8)
197
+ out[t_order] = img_src.reshape(-1, 3)[s_order]
198
+ out_img = out.reshape(h, w, 3)
199
+
200
+ # ── Display setup (screen-resolution canvas) ──
201
+ sw, sh = get_screen_resolution()
202
+ canvas = np.full((sh, sw, 3), 32, dtype=np.uint8)
203
+
204
+ label_h = 22
205
+ pw = sw // 3 # panel width (equal thirds)
206
+ ph = sh - label_h # panel height
207
+ sc = min(pw / w, ph / h) # as large as possible, no crop
208
+ iw, ih = max(int(w * sc), 1), max(int(h * sc), 1)
209
+
210
+ src_s = cv2.resize(img_src, (iw, ih), interpolation=cv2.INTER_LANCZOS4)
211
+ tgt_s = cv2.resize(img_tgt, (iw, ih), interpolation=cv2.INTER_LANCZOS4)
212
+
213
+ cx = (pw - iw) // 2
214
+ cy = label_h + (ph - ih) // 2
215
+ canvas[cy:cy+ih, cx:cx+iw] = src_s
216
+ canvas[cy:cy+ih, pw+cx:pw+cx+iw] = tgt_s
217
+
218
+ rec_x = 2 * pw + cx
219
+ rec_region = canvas[cy:cy+ih, rec_x:rec_x+iw]
220
+
221
+ font = cv2.FONT_HERSHEY_SIMPLEX
222
+ for label, xo in [("Source", 0), ("Target", pw), ("Reconstruction", 2 * pw)]:
223
+ cv2.rectangle(canvas, (xo, 0), (xo + pw, label_h), (0, 0, 0), -1)
224
+ cv2.putText(canvas, label, (xo + 6, 16), font, 0.45, (200, 200, 200), 1)
225
+
226
+ wn = "Pixel Rearrangement (ESC/q anytime to quit)"
227
+ cv2.namedWindow(wn, cv2.WINDOW_NORMAL)
228
+ cv2.resizeWindow(wn, sw, sh)
229
+ cv2.imshow(wn, canvas)
230
+ cv2.waitKey(300)
231
+
232
+ # ── True pixel-sliding animation ──
233
+ total = h * w
234
+ forward = np.empty(total, dtype=np.int32)
235
+ forward[s_order] = t_order
236
+
237
+ scx = iw / w
238
+ scy = ih / h
239
+
240
+ s_idx_x = (np.arange(iw, dtype=np.float32) * w / iw).clip(0, w - 1).round().astype(np.int32)
241
+ s_idx_y = (np.arange(ih, dtype=np.float32) * h / ih).clip(0, h - 1).round().astype(np.int32)
242
+ gx, gy = np.meshgrid(s_idx_x, s_idx_y)
243
+ src_lin = gy * w + gx
244
+
245
+ tgt_lin = forward[src_lin]
246
+ tgt_dx = (tgt_lin % w).astype(np.float32) * scx
247
+ tgt_dy = (tgt_lin // w).astype(np.float32) * scy
248
+
249
+ src_dx = gx.astype(np.float32) * scx
250
+ src_dy = gy.astype(np.float32) * scy
251
+
252
+ colors = src_s.reshape(-1, 3).astype(np.float32)
253
+
254
+ num_frames = 60
255
+ for fi in range(num_frames):
256
+ t = (fi + 1) / num_frames
257
+
258
+ curr_x = np.clip((1 - t) * src_dx.ravel() + t * tgt_dx.ravel(), 0, iw - 1)
259
+ curr_y = np.clip((1 - t) * src_dy.ravel() + t * tgt_dy.ravel(), 0, ih - 1)
260
+ rx = np.round(curr_x).astype(np.int32)
261
+ ry = np.round(curr_y).astype(np.int32)
262
+
263
+ accum = np.zeros((ih, iw, 3), dtype=np.float32)
264
+ cnt = np.zeros((ih, iw), dtype=np.float32)
265
+ np.add.at(accum, (ry, rx), colors)
266
+ np.add.at(cnt, (ry, rx), 1.0)
267
+ mask = cnt > 0
268
+ accum[mask] /= cnt[mask, None]
269
+
270
+ rec_region[:] = accum.astype(np.uint8)
271
+ cv2.imshow(wn, canvas)
272
+ if cv2.waitKey(25) & 0xFF in (27, ord("q")):
273
+ break
274
+ else:
275
+ rec_region[:] = cv2.resize(
276
+ img_src.reshape(-1, 3)[s_order][t_order.argsort()].reshape(h, w, 3),
277
+ (iw, ih),
278
+ )
279
+ cv2.imshow(wn, canvas)
280
+
281
+ cv2.waitKey(0)
282
+ cv2.destroyAllWindows()
283
+
284
+ state.status = "Rearrangement finished!"
285
+ state.status_style = "status"
286
+
287
+ except Exception as e:
288
+ state.status = f"Error: {e}"
289
+ state.status_style = "status-error"
290
+ finally:
291
+ state.running = False
292
+ state.done = True
293
+
294
+
295
+ # ── TUI Application ──────────────────────────────────────────────────
296
+
297
+ class PixelTUI:
298
+ def __init__(self):
299
+ self.state = State()
300
+
301
+ self.kb = KeyBindings()
302
+ self._register_bindings()
303
+
304
+ self._app: Application | None = None
305
+
306
+ # ── Key Bindings ─────────────────────────────────────────────
307
+
308
+ def _register_bindings(self):
309
+ kb = self.kb
310
+
311
+ @kb.add("up")
312
+ def _(event):
313
+ if not self.state.running:
314
+ self.state.cursor = (self.state.cursor - 1) % 4
315
+ self._invalidate()
316
+
317
+ @kb.add("down")
318
+ def _(event):
319
+ if not self.state.running:
320
+ self.state.cursor = (self.state.cursor + 1) % 4
321
+ self._invalidate()
322
+
323
+ @kb.add("enter")
324
+ def _(event):
325
+ if not self.state.running:
326
+ self._dispatch(self.state.cursor)
327
+
328
+ for key, idx in [("1", 0), ("2", 1), ("3", 2), ("4", 3)]:
329
+ @kb.add(key)
330
+ def _(event, idx=idx):
331
+ if not self.state.running:
332
+ self.state.cursor = idx
333
+ self._dispatch(idx)
334
+
335
+ @kb.add("escape")
336
+ @kb.add("q")
337
+ def _(event):
338
+ if not self.state.running:
339
+ self._quit()
340
+
341
+ @kb.add("c-c")
342
+ def _(event):
343
+ self._quit()
344
+
345
+ # ── Dispatch ─────────────────────────────────────────────────
346
+
347
+ def _dispatch(self, idx: int):
348
+ {0: self._select_source,
349
+ 1: self._select_target,
350
+ 2: self._run,
351
+ 3: self._quit}[idx]()
352
+
353
+ # ── Actions ──────────────────────────────────────────────────
354
+
355
+ def _select_source(self):
356
+ path = select_file("Select Source Image (pixels to rearrange)")
357
+ if path:
358
+ self.state.source = path
359
+ self._refresh_info()
360
+ self.state.status = f"Source: {Path(path).name}"
361
+ self.state.status_style = "status"
362
+ else:
363
+ self.state.status = "Selection cancelled"
364
+ self.state.status_style = "status-info"
365
+ self._invalidate()
366
+
367
+ def _select_target(self):
368
+ path = select_file("Select Target Image (layout to approximate)")
369
+ if path:
370
+ self.state.target = path
371
+ self._refresh_info()
372
+ self.state.status = f"Target: {Path(path).name}"
373
+ self.state.status_style = "status"
374
+ else:
375
+ self.state.status = "Selection cancelled"
376
+ self.state.status_style = "status-info"
377
+ self._invalidate()
378
+
379
+ def _run(self):
380
+ if not self.state.source:
381
+ self.state.status = "Select a source image first!"
382
+ self.state.status_style = "status-error"; self._invalidate(); return
383
+ if not self.state.target:
384
+ self.state.status = "Select a target image first!"
385
+ self.state.status_style = "status-error"; self._invalidate(); return
386
+
387
+ self.state.running = True
388
+ self.state.done = False
389
+ self.state.status = "Rearrangement running in OpenCV window…"
390
+ self.state.status_style = "status-warn"
391
+ self._invalidate()
392
+
393
+ t = threading.Thread(
394
+ target=rearrange,
395
+ args=(self.state.source, self.state.target, self.state),
396
+ daemon=True,
397
+ )
398
+ t.start()
399
+
400
+ # Poll thread completion in the background
401
+ async def waiter():
402
+ while t.is_alive():
403
+ await asyncio.sleep(0.5)
404
+ self._invalidate()
405
+ self._invalidate()
406
+
407
+ if self._app:
408
+ asyncio.create_task(waiter())
409
+
410
+ def _refresh_info(self):
411
+ parts = []
412
+ for path in (self.state.source, self.state.target):
413
+ if not path:
414
+ continue
415
+ try:
416
+ img = cv2.imread(path, cv2.IMREAD_COLOR)
417
+ if img is not None:
418
+ h, w = img.shape[:2]
419
+ kb = Path(path).stat().st_size / 1024
420
+ parts.append(f"{Path(path).name} {w}×{h} ({kb:.0f} KB)")
421
+ except Exception:
422
+ parts.append(Path(path).name)
423
+ if self.state.source and self.state.target:
424
+ parts.append("Ready!")
425
+ self.state.info = " | ".join(parts) if parts else ""
426
+
427
+ def _quit(self):
428
+ cv2.destroyAllWindows()
429
+ if self._app:
430
+ self._app.exit()
431
+ sys.exit(0)
432
+
433
+ # ── UI Rendering ─────────────────────────────────────────────
434
+
435
+ def _build_text(self):
436
+ s = self.state
437
+ F: list[tuple[str, str]] = []
438
+
439
+ def push(style, text):
440
+ if text:
441
+ F.append((style, text))
442
+
443
+ # Header
444
+ push("bold #00d787", " ■ Pixel Rearrangement Tool")
445
+ push("", "\n")
446
+ push("#3a3a3a", " ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")
447
+ push("", "\n")
448
+
449
+ # Source / Target paths
450
+ push("bold #5f87ff", "\n Source: ")
451
+ push("#87afff" if s.source else "#585858 italic",
452
+ s.source if s.source else "— not selected —")
453
+
454
+ push("bold #5f87ff", "\n Target: ")
455
+ push("#87afff" if s.target else "#585858 italic",
456
+ s.target if s.target else "— not selected —")
457
+
458
+ # Info line
459
+ if s.info:
460
+ push("", "\n ")
461
+ push("#878787 italic", s.info)
462
+
463
+ # Menu divider
464
+ push("", "\n\n")
465
+ push("#3a3a3a", " ───────────────────────────────────────────────────────────")
466
+ push("", "\n")
467
+
468
+ # Menu items
469
+ for i, (label, desc) in enumerate(State.MENU):
470
+ cursor = "●" if i == s.cursor else "○"
471
+ sel = i == s.cursor
472
+ push("bold #000000 bg:#00d787" if sel else "bold #ffffff",
473
+ f" {cursor} {label} ")
474
+ push("", " ")
475
+ push("#6c6c6c", f"{desc}\n")
476
+
477
+ # Status divider
478
+ push("", "\n")
479
+ push("#3a3a3a", " ───────────────────────────────────────────────────────────")
480
+ push("", "\n ")
481
+
482
+ # Status line
483
+ c = {"status": "#5faf5f", "status-error": "#ff5f5f",
484
+ "status-warn": "#ffaf5f", "status-info": "#878787 italic"}
485
+ push(c.get(s.status_style, "#878787 italic"), s.status)
486
+ push("", "\n")
487
+
488
+ # Help
489
+ push("#585858 italic", "↑↓ navigate • Enter select • 1-4 shortcut • q quit")
490
+ push("", "\n")
491
+
492
+ return F
493
+
494
+ def _build_layout(self):
495
+ control = FormattedTextControl(
496
+ text=self._build_text,
497
+ show_cursor=False,
498
+ )
499
+ return Layout(Window(content=control, dont_extend_height=False))
500
+
501
+ def _invalidate(self):
502
+ try:
503
+ if self._app:
504
+ self._app.invalidate()
505
+ except Exception:
506
+ pass
507
+
508
+ # ── Run ──────────────────────────────────────────────────────
509
+
510
+ def run(self):
511
+ self._app = Application(
512
+ layout=self._build_layout(),
513
+ key_bindings=self.kb,
514
+ style=STYLE,
515
+ mouse_support=False,
516
+ full_screen=True,
517
+ )
518
+ try:
519
+ self._app.run()
520
+ except KeyboardInterrupt:
521
+ self._quit()
522
+
523
+
524
+ def main():
525
+ PixelTUI().run()
526
+
527
+
528
+ # ── Entry Point ──────────────────────────────────────────────────────
529
+
530
+ if __name__ == "__main__":
531
+ main()
@@ -0,0 +1,88 @@
1
+ Metadata-Version: 2.4
2
+ Name: pixelification
3
+ Version: 0.1.0
4
+ Summary: Pixel Rearrangement Tool — Keyboard-Navigated Terminal UI
5
+ Requires-Python: >=3.10
6
+ Requires-Dist: numpy>=1.20.0
7
+ Requires-Dist: opencv-python>=4.0.0
8
+ Requires-Dist: prompt-toolkit>=3.0.0
9
+ Description-Content-Type: text/markdown
10
+
11
+ # Pixelification
12
+
13
+ A terminal tool that rearranges pixels from a source image to approximate a target image using optimal transport via color sorting — then animates each pixel physically sliding to its new position.
14
+
15
+ No new pixels are created. Every pixel in the output comes from the source image, just rearranged.
16
+
17
+ ## How it works
18
+
19
+ ```mermaid
20
+ flowchart LR
21
+ A[Source Image] --> C[Color sort<br/>lexsort by<br/>lum→hue→sat]
22
+ B[Target Image] --> D[Color sort<br/>lexsort by<br/>lum→hue→sat]
23
+ C --> E["s_order[i]"]
24
+ D --> F["t_order[i]"]
25
+ E --> G["forward[s_order] = t_order"]
26
+ F --> G
27
+ G --> H[Per-pixel<br/>position lerp]
28
+ H --> I[Scatter render<br/>np.add.at]
29
+ I --> J[60 frames<br/>slide animation]
30
+ ```
31
+
32
+ 1. **Sort by colour** — every pixel in both images is sorted by luminance, then hue, then saturation. The darkest source pixel gets rank 0, the lightest gets rank *N*−1. Same for the target.
33
+
34
+ 2. **Map by rank** — a source pixel with rank *i* maps to the target position with rank *i*. This is the optimal transport: the *i*th darkest pixel in the source ends up where the *i*th darkest pixel was in the target.
35
+
36
+ ```
37
+ Source pixels (sorted) Target positions (sorted)
38
+ ┌─────────────────────┐ ┌─────────────────────┐
39
+ │ rank 0 (darkest) │──────→│ rank 0 │
40
+ │ rank 1 │──────→│ rank 1 │
41
+ │ rank 2 │──────→│ rank 2 │
42
+ │ ... │ │ ... │
43
+ │ rank N−1 (lightest)│──────→│ rank N−1 │
44
+ └─────────────────────┘ └─────────────────────┘
45
+ ```
46
+
47
+ 3. **Animate** — each pixel slides from its original position to its mapped position over 60 frames using linear interpolation. All pixels move simultaneously. When multiple pixels land on the same display cell, their colours are averaged.
48
+
49
+ ```
50
+ Frame 0 Frame 30 Frame 60
51
+ ┌────────┐ ┌────────┐ ┌────────┐
52
+ │• • │ │ •• │ │ •• │
53
+ │ •• │ ───────→ │ • •• │ ───────→ │ •• │
54
+ │ • • │ lerp │ • • │ lerp │ • • │
55
+ └────────┘ └────────┘ └────────┘
56
+ source positions midway target positions
57
+ ```
58
+
59
+ ## Usage
60
+
61
+ ```bash
62
+ python rearrange_pixels_tui.py
63
+ ```
64
+
65
+ A keyboard-navigated terminal interface opens. Use arrow keys to select images, press Enter to run.
66
+
67
+ ```
68
+ ↑↓ navigate • Enter select • 1-4 shortcut • q quit
69
+ ```
70
+
71
+ An OpenCV window opens with three panels:
72
+
73
+ | Source | Target | Reconstruction |
74
+ |--------|--------|----------------|
75
+ | Your image | Layout to approximate | Pixels sliding into place |
76
+
77
+ Press `ESC` or `q` during the animation to quit.
78
+
79
+ ## Requirements
80
+
81
+ - Python 3.10+
82
+ - OpenCV (`cv2`)
83
+ - NumPy
84
+ - `prompt_toolkit`
85
+
86
+ ```bash
87
+ pip install opencv-python numpy prompt_toolkit
88
+ ```
@@ -0,0 +1,6 @@
1
+ pixelification/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ pixelification/main.py,sha256=qsTp-vgKzQSLnwL1m8uYBrpL_3oWHZjhmEWHSNw4zZc,18748
3
+ pixelification-0.1.0.dist-info/METADATA,sha256=iLbwh5dDEEs0Xd_V4lxtpJ0qSrF8_Q7TIp7IcgIf_hk,3701
4
+ pixelification-0.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
5
+ pixelification-0.1.0.dist-info/entry_points.txt,sha256=ZHzodclL9j9F7sSnTZilwYGQLJCpAeHu-8_ZOABJ-cA,60
6
+ pixelification-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ pixelification = pixelification.main:main