From a9412fe58bf43806d7c971d73e4174953d8c1106 Mon Sep 17 00:00:00 2001 From: Steve Briggs Date: Sat, 23 May 2026 21:32:55 -0400 Subject: [PATCH] initial commit --- README.md | 108 ++++++++++++ labelstore.py | 55 ++++++ playback.py | 17 ++ screenshot.png | Bin 0 -> 126625 bytes sprite_tagger.py | 423 +++++++++++++++++++++++++++++++++++++++++++++ test_labelstore.py | 70 ++++++++ test_playback.py | 38 ++++ 7 files changed, 711 insertions(+) create mode 100644 README.md create mode 100644 labelstore.py create mode 100644 playback.py create mode 100644 screenshot.png create mode 100755 sprite_tagger.py create mode 100644 test_labelstore.py create mode 100644 test_playback.py diff --git a/README.md b/README.md new file mode 100644 index 0000000..693d28d --- /dev/null +++ b/README.md @@ -0,0 +1,108 @@ +# ๐ŸŽฎ Sprite Tagger + +[![WARN-LLM GENERATED](https://img.shields.io/badge/WARN-LLM%20GENERATED-FF6347)](https://github.com/40ants/ai-badges) + +A fast, keyboard-driven tool for sorting sprite animation frames into labeled directories. Load a folder of frames, tag them as `idle`, `walk`, `run`, whatever โ€” then export them into organized subfolders in one shot. + +![Python](https://img.shields.io/badge/Python-3.9+-blue?style=flat-square&logo=python) +![Platform](https://img.shields.io/badge/Platform-macOS%20%7C%20Linux-lightgrey?style=flat-square) +![License](https://img.shields.io/badge/License-MIT-green?style=flat-square) + +![Sprite Tagger screenshot](screenshot.png) + +--- + +## โœจ Features + +- ๐Ÿ–ผ๏ธ **Pixel-perfect display** โ€” small sprites zoom in with nearest-neighbor scaling; transparency shows on a checkerboard background +- โŒจ๏ธ **Fully keyboard-driven** โ€” tag and advance without touching the mouse +- โ–ถ๏ธ **Live animation preview** โ€” `Space` plays through all frames; `P` plays back *only the current frame's label* on a loop, so you can confirm a walk cycle or attack animation reads right +- ๐Ÿท๏ธ **Label up to 9 categories** โ€” one keypress per tag, auto-advances to the next frame; add new labels on the fly with `N` +- ๐Ÿ“ **Non-destructive** โ€” copies originals into labeled subdirectories, leaves your source untouched +- ๐Ÿ’พ **Autosave & resume** โ€” every label is written to a `.sprite_tags.json` sidecar as you go, so you can quit mid-batch and pick up exactly where you left off +- ๐ŸŽจ **Dark theme UI** โ€” easy on the eyes during long tagging sessions + +--- + +## ๐Ÿš€ Quick Start + +### Prerequisites + +```bash +pip install Pillow +``` + +> tkinter is included with most Python installs. On macOS with Homebrew: `brew install python-tk` + +### Run it + +```bash +python sprite_tagger.py /path/to/frames idle walk run jump attack +``` + +With a custom output directory: + +```bash +python sprite_tagger.py ./sprites idle walk run --out ./sorted_frames +``` + +--- + +## โŒจ๏ธ Controls + +| Key | Action | +|-----|--------| +| `โ†` `โ†’` | Previous / next frame | +| `1` โ€“ `9` | Assign label and advance to next frame | +| `Space` | Play / pause all frames | +| `P` | Play / pause only the current frame's label, looping | +| `[` `]` | Decrease / increase playback speed (1โ€“30 fps) | +| `X` | Clear label on current frame | +| `N` | Add a new label on the fly | +| `Enter` | Export all labeled frames | + +--- + +## ๐Ÿ“‚ Output Structure + +After exporting, your labeled frames are copied into subdirectories named after each label: + +``` +output/ +โ”œโ”€โ”€ idle/ +โ”‚ โ”œโ”€โ”€ frame_000.png +โ”‚ โ”œโ”€โ”€ frame_001.png +โ”‚ โ””โ”€โ”€ frame_002.png +โ”œโ”€โ”€ walk/ +โ”‚ โ”œโ”€โ”€ frame_003.png +โ”‚ โ””โ”€โ”€ frame_004.png +โ””โ”€โ”€ run/ + โ”œโ”€โ”€ frame_005.png + โ””โ”€โ”€ frame_006.png +``` + +If no `--out` path is specified, output goes to `/labeled/`. + +--- + +## ๐ŸŽฏ Workflow + +1. Point it at a directory of exported sprite frames +2. Use `โ†` `โ†’` to step through frames, or hit `Space` to play them back and get a feel for the animation +3. When you land on a frame you want to tag, press the number key for that label โ€” it tags and jumps to the next frame automatically +4. Made a mistake? Navigate back and press the same or different number to reassign, or `X` to clear +5. Hit `Enter` when done โ€” a summary dialog confirms what was copied where + +--- + +## ๐Ÿ› ๏ธ Supported Formats + +`.png` ยท `.jpg` ยท `.jpeg` ยท `.gif` ยท `.bmp` ยท `.webp` ยท `.tga` + +--- + +## ๐Ÿ“‹ Requirements + +- Python 3.9+ +- [Pillow](https://python-pillow.org/) +- tkinter (standard library, included with most Python distributions) diff --git a/labelstore.py b/labelstore.py new file mode 100644 index 0000000..65963d6 --- /dev/null +++ b/labelstore.py @@ -0,0 +1,55 @@ +"""Persistence for sprite frame label assignments. + +Assignments are stored in a JSON sidecar keyed by filename (not frame index), +so resuming a session survives frames being added, removed, or reordered. +""" + +import json +from pathlib import Path + +SIDECAR_NAME = '.sprite_tags.json' +SCHEMA_VERSION = 1 + + +def sidecar_path(frames_dir: Path) -> Path: + return frames_dir / SIDECAR_NAME + + +def save_labels(path: Path, assignments_by_name: dict, labels: list) -> None: + data = { + 'version': SCHEMA_VERSION, + 'labels': list(labels), + 'assignments': dict(assignments_by_name), + } + Path(path).write_text(json.dumps(data, indent=2)) + + +def load_labels(path: Path): + """Return (assignments_by_name, labels). Missing or invalid file โ†’ ({}, []).""" + path = Path(path) + if not path.exists(): + return {}, [] + try: + data = json.loads(path.read_text()) + except (json.JSONDecodeError, OSError, ValueError): + return {}, [] + assignments = data.get('assignments', {}) + labels = data.get('labels', []) + if not isinstance(assignments, dict) or not isinstance(labels, list): + return {}, [] + return assignments, labels + + +def to_name_keyed(assignments_by_index: dict, frames: list) -> dict: + """{index: label} โ†’ {filename: label}.""" + return {frames[i].name: label for i, label in assignments_by_index.items()} + + +def to_index_keyed(assignments_by_name: dict, frames: list) -> dict: + """{filename: label} โ†’ {index: label}, dropping files not in `frames`.""" + name_to_index = {p.name: i for i, p in enumerate(frames)} + return { + name_to_index[name]: label + for name, label in assignments_by_name.items() + if name in name_to_index + } diff --git a/playback.py b/playback.py new file mode 100644 index 0000000..9302f46 --- /dev/null +++ b/playback.py @@ -0,0 +1,17 @@ +"""Playback helpers โ€” selecting and cycling through frames by label.""" + + +def frames_with_label(assignments: dict, label: str) -> list: + """Sorted frame indices assigned the given label.""" + return sorted(i for i, lbl in assignments.items() if lbl == label) + + +def next_in_cycle(indices: list, current: int, delta: int) -> int: + """Next index in `indices` after `current`, wrapping around. + + If `current` isn't in the set, returns the first index. + """ + if current in indices: + pos = indices.index(current) + return indices[(pos + delta) % len(indices)] + return indices[0] diff --git a/screenshot.png b/screenshot.png new file mode 100644 index 0000000000000000000000000000000000000000..c459c43fbeff8a60042e550c617b92cc61761eb9 GIT binary patch literal 126625 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 J)JT|w{2$3Apt1k} literal 0 HcmV?d00001 diff --git a/sprite_tagger.py b/sprite_tagger.py new file mode 100755 index 0000000..d70d46c --- /dev/null +++ b/sprite_tagger.py @@ -0,0 +1,423 @@ +#!/opt/homebrew/bin/python3.13 +"""Sprite frame tagger โ€” navigate frames, assign labels, copy to labeled dirs. + +Usage: + sprite_tagger.py [label2 ...] [--out ] + +Example: + sprite_tagger.py ./sprites idle walk run jump + +Keys: + โ† โ†’ previous / next frame + 1-9 assign label and advance + N add a new label + Space play/pause all frames + P play/pause only the current frame's label (looping) + [ ] slower / faster playback + X clear label on current frame + Enter export labeled frames to output dir +""" + +import sys +import shutil +import tkinter as tk +from tkinter import messagebox +from pathlib import Path +from PIL import Image, ImageDraw, ImageTk + +import labelstore +import playback + +IMAGE_EXTENSIONS = {'.png', '.jpg', '.jpeg', '.gif', '.bmp', '.webp', '.tga'} + +BG = '#1a1a2e' +BG_PANEL = '#16213e' +ACCENT = '#4ecca3' +FG = '#e0e0e0' +FG_DIM = '#556677' +BTN_BG = '#0f3460' +BTN_ACT = '#e94560' +FONT_MONO = ('Courier', 11) +FONT_TINY = ('Courier', 9) + +CANVAS_W = 860 +CANVAS_H = 620 +FPS_DEFAULT = 10 +FPS_MIN = 1 +FPS_MAX = 30 + + +def load_frames(directory: Path) -> list: + return sorted( + p for p in directory.iterdir() + if p.suffix.lower() in IMAGE_EXTENSIONS + ) + + +class SpriteTagger: + def __init__(self, root: tk.Tk, frames: list, labels: list, output_dir: Path, + store_path: Path, assignments: dict = None): + self.root = root + self.frames = frames + self.labels = labels + self.output_dir = output_dir + self.store_path = store_path + self.current = 0 + self.assignments: dict = assignments or {} # frame index โ†’ label name + self._photo = None # ImageTk ref โ€” must not be GC'd + self._playing = False + self._fps = FPS_DEFAULT + self._play_job = None + self._preview_label = None # None = play all; label str = preview that label + + self._build_ui() + self._bind_keys() + self._show_frame() + + # โ”€โ”€ UI construction โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + + def _build_ui(self): + self.root.title('Sprite Tagger') + self.root.configure(bg=BG) + self.root.resizable(True, True) + + # canvas + self.canvas = tk.Canvas( + self.root, bg=BG_PANEL, + width=CANVAS_W, height=CANVAS_H, + highlightthickness=1, highlightbackground='#2a2a4e', + ) + self.canvas.pack(fill=tk.BOTH, expand=True, padx=10, pady=(10, 0)) + self.canvas.bind('', lambda _e: self._show_frame()) + + # status row + status = tk.Frame(self.root, bg=BG) + status.pack(fill=tk.X, padx=10, pady=4) + + self._var_info = tk.StringVar() + self._var_label = tk.StringVar() + self._var_fps = tk.StringVar() + + tk.Label(status, textvariable=self._var_info, bg=BG, fg=FG, font=FONT_MONO).pack(side=tk.LEFT) + tk.Label(status, textvariable=self._var_fps, bg=BG, fg=FG_DIM, font=FONT_TINY).pack(side=tk.LEFT, padx=10) + tk.Label(status, textvariable=self._var_label, bg=BG, fg=ACCENT, font=(*FONT_MONO, 'bold')).pack(side=tk.RIGHT) + + # label buttons row + self._btn_row = tk.Frame(self.root, bg=BG) + self._btn_row.pack(fill=tk.X, padx=10, pady=(0, 4)) + + self._label_btns = {} + for label in self.labels: + self._add_label_button(label) + + self._make_utility_buttons() + + # hint row + tk.Label( + self.root, + text='โ† โ†’ navigate 1-9 label+advance N new label Space play all P preview label [ ] speed X clear โ†ต export', + bg=BG, fg=FG_DIM, font=FONT_TINY, + ).pack(pady=(0, 6)) + + def _add_label_button(self, label: str): + idx = len(self._label_btns) + key = str(idx + 1) if idx < 9 else '-' + # tk.Label used instead of tk.Button โ€” macOS ignores fg/bg on native buttons + lbl = tk.Label( + self._btn_row, text=f'[{key}] {label}', + bg=BTN_BG, fg='white', + padx=8, pady=4, font=FONT_MONO, + cursor='hand2', relief=tk.FLAT, + ) + lbl.pack(side=tk.LEFT, padx=2) + lbl.bind('', lambda _e, l=label: self._assign(l)) + lbl.bind('', lambda _e, w=lbl: w.configure(bg='#1a5a9a')) + lbl.bind('', lambda _e, w=lbl: self._restore_btn_color(w, label)) + self._label_btns[label] = lbl + + def _make_utility_buttons(self): + self._make_action_label(self._btn_row, '[X] clear', '#5a1a3a', '#ff6688', self._clear, side=tk.LEFT, padx=(8, 2)) + self._make_action_label(self._btn_row, '[N] + label', '#3a3a1a', '#dddd44', self._prompt_new_label, side=tk.LEFT, padx=2) + self._make_action_label(self._btn_row, '[โ†ต] export', '#0a3a1a', ACCENT, self._export, side=tk.RIGHT, padx=2, bold=True) + + def _make_action_label(self, parent, text, bg, hover_bg, command, side, padx, bold=False): + font = (*FONT_MONO, 'bold') if bold else FONT_MONO + w = tk.Label(parent, text=text, bg=bg, fg='white', padx=8, pady=4, font=font, cursor='hand2', relief=tk.FLAT) + w.pack(side=side, padx=padx) + w.bind('', lambda _e: command()) + w.bind('', lambda _e: w.configure(bg=hover_bg, fg='black')) + w.bind('', lambda _e: w.configure(bg=bg, fg='white')) + + def _restore_btn_color(self, widget: tk.Label, label: str): + current_label = self.assignments.get(self.current) + if label == current_label: + widget.configure(bg=ACCENT, fg='black') + else: + widget.configure(bg=BTN_BG, fg='white') + + def _bind_keys(self): + self.root.bind('', lambda _e: self._navigate(1)) + self.root.bind('', lambda _e: self._navigate(-1)) + self.root.bind('', lambda _e: self._export()) + self.root.bind('', lambda _e: self._toggle_play()) + self.root.bind('p', lambda _e: self._toggle_preview()) + self.root.bind('P', lambda _e: self._toggle_preview()) + self.root.bind('[', lambda _e: self._change_fps(-2)) + self.root.bind(']', lambda _e: self._change_fps(+2)) + self.root.bind('x', lambda _e: self._clear()) + self.root.bind('X', lambda _e: self._clear()) + self.root.bind('n', lambda _e: self._prompt_new_label()) + self.root.bind('N', lambda _e: self._prompt_new_label()) + self._rebind_number_keys() + + def _rebind_number_keys(self): + for i, label in enumerate(self.labels): + if i < 9: + self.root.bind(str(i + 1), lambda _e, l=label: self._assign(l)) + + # โ”€โ”€ Frame display โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + + def _show_frame(self): + path = self.frames[self.current] + img = Image.open(path).convert('RGBA') + + cw = self.canvas.winfo_width() if self.canvas.winfo_width() > 1 else CANVAS_W + ch = self.canvas.winfo_height() if self.canvas.winfo_height() > 1 else CANVAS_H + + if img.width <= cw // 2 and img.height <= ch // 2: + # small sprite โ€” zoom with nearest-neighbor to preserve pixels + zoom = min(cw // img.width, ch // img.height) + zoom = max(zoom, 1) + img = img.resize((img.width * zoom, img.height * zoom), Image.NEAREST) + elif img.width > cw or img.height > ch: + # large image โ€” scale down with lanczos + scale = min(cw / img.width, ch / img.height) + img = img.resize((int(img.width * scale), int(img.height * scale)), Image.LANCZOS) + + # checkerboard background for transparency + check_size = max(8, min(img.width, img.height) // 16) + bg = Image.new('RGBA', img.size, (40, 40, 60, 255)) + draw = ImageDraw.Draw(bg) + for y in range(0, img.height, check_size): + for x in range(0, img.width, check_size): + if (x // check_size + y // check_size) % 2 == 0: + draw.rectangle( + [x, y, min(x + check_size, img.width) - 1, min(y + check_size, img.height) - 1], + fill=(50, 50, 70, 255), + ) + bg.paste(img, mask=img.split()[3]) + + self._photo = ImageTk.PhotoImage(bg) + self.canvas.delete('all') + self.canvas.create_image(cw // 2, ch // 2, image=self._photo, anchor='center') + + self._update_status() + + def _update_status(self): + path = self.frames[self.current] + labeled = len(self.assignments) + current_label = self.assignments.get(self.current) + + self._var_info.set(f'{path.name} [{self.current + 1}/{len(self.frames)}] labeled: {labeled}') + self._var_label.set(f'โ–ถ {current_label}' if current_label else 'โ€”') + play_sym = 'โธ' if self._playing else 'โ–ถ' + if self._preview_label is not None: + n = len(playback.frames_with_label(self.assignments, self._preview_label)) + self._var_fps.set(f'{play_sym} preview: {self._preview_label} ({n}) โŸณ {self._fps}fps') + else: + self._var_fps.set(f'{play_sym} {self._fps}fps') + + for label, btn in self._label_btns.items(): + if label == current_label: + btn.configure(bg=ACCENT, fg='black') + else: + btn.configure(bg=BTN_BG, fg='white') + + # โ”€โ”€ Navigation โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + + def _navigate(self, delta: int): + self.current = (self.current + delta) % len(self.frames) + self._show_frame() + + # โ”€โ”€ Labeling โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + + def _assign(self, label: str): + self.assignments[self.current] = label + self._save() + self._navigate(1) + + def _clear(self): + self.assignments.pop(self.current, None) + self._save() + self._update_status() + + def _save(self): + by_name = labelstore.to_name_keyed(self.assignments, self.frames) + labelstore.save_labels(self.store_path, by_name, self.labels) + + # โ”€โ”€ Playback โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + + def _toggle_play(self): + self._preview_label = None # Space always plays all frames + if self._playing: + self._stop_play() + else: + self._start_play() + + def _toggle_preview(self): + if self._preview_label is not None: + self._stop_play() + self._preview_label = None + self._update_status() + return + label = self.assignments.get(self.current) + if label is None: + self._flash('tag this frame first to preview its label') + return + self._preview_label = label + indices = playback.frames_with_label(self.assignments, label) + self.current = indices[0] + self._show_frame() + self._start_play() + + def _start_play(self): + self._playing = True + delay = max(1, 1000 // self._fps) + self._play_job = self.root.after(delay, self._tick) + self._update_status() + + def _stop_play(self): + self._playing = False + if self._play_job: + self.root.after_cancel(self._play_job) + self._play_job = None + self._update_status() + + def _tick(self): + if not self._playing: + return + if self._preview_label is not None: + self._step_preview(1) + else: + self._navigate(1) + delay = max(1, 1000 // self._fps) + self._play_job = self.root.after(delay, self._tick) + + def _step_preview(self, delta: int): + indices = playback.frames_with_label(self.assignments, self._preview_label) + if not indices: + self._stop_play() + self._preview_label = None + return + self.current = playback.next_in_cycle(indices, self.current, delta) + self._show_frame() + + def _change_fps(self, delta: int): + self._fps = max(FPS_MIN, min(FPS_MAX, self._fps + delta)) + self._update_status() + + def _flash(self, msg: str): + self._var_label.set(msg) + self.root.after(1500, self._update_status) + + # โ”€โ”€ Label management โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + + def _prompt_new_label(self): + from tkinter import simpledialog + name = simpledialog.askstring('New label', 'Label name:', parent=self.root) + if not name: + return + name = name.strip().lower().replace(' ', '_') + if not name or name in self._label_btns: + return + self.labels.append(name) + self._add_label_button(name) + self._rebind_number_keys() + self._save() + + # โ”€โ”€ Export โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + + def _export(self): + if self._playing: + self._toggle_play() + + if not self.assignments: + messagebox.showinfo('Nothing to export', 'No frames have been labeled yet.') + return + + counts: dict = {} + for idx, label in self.assignments.items(): + dest = self.output_dir / label + dest.mkdir(parents=True, exist_ok=True) + shutil.copy2(self.frames[idx], dest / self.frames[idx].name) + counts[label] = counts.get(label, 0) + 1 + + summary = '\n'.join(f' {label}/: {n} frame{"s" if n != 1 else ""}' for label, n in sorted(counts.items())) + messagebox.showinfo('Export complete', f'Copied to:\n{self.output_dir}\n\n{summary}') + + +# โ”€โ”€ Entry point โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +def _parse_args(argv: list): + if len(argv) < 3: + print(__doc__) + sys.exit(1) + + args = list(argv[1:]) + output_dir = None + + if '--out' in args: + i = args.index('--out') + if i + 1 >= len(args): + print('Error: --out requires a path argument') + sys.exit(1) + output_dir = Path(args[i + 1]) + args = args[:i] + args[i + 2:] + + frames_dir = Path(args[0]) + labels = args[1:] + + if not frames_dir.is_dir(): + print(f'Error: {frames_dir} is not a directory') + sys.exit(1) + if not labels: + print('Error: provide at least one label') + sys.exit(1) + + if output_dir is None: + output_dir = frames_dir / 'labeled' + + return frames_dir, labels, output_dir + + +def main(): + frames_dir, labels, output_dir = _parse_args(sys.argv) + + frames = load_frames(frames_dir) + if not frames: + print(f'No image files found in {frames_dir}') + print(f'Supported formats: {", ".join(sorted(IMAGE_EXTENSIONS))}') + sys.exit(1) + + store_path = labelstore.sidecar_path(frames_dir) + saved_assignments, saved_labels = labelstore.load_labels(store_path) + for lbl in saved_labels: + if lbl not in labels: + labels.append(lbl) + assignments = labelstore.to_index_keyed(saved_assignments, frames) + + print(f'Loaded {len(frames)} frames') + print(f'Labels : {", ".join(labels)}') + print(f'Output : {output_dir}') + if assignments: + print(f'Resumed {len(assignments)} labels from {store_path.name}') + + root = tk.Tk() + root.geometry(f'{CANVAS_W + 20}x{CANVAS_H + 130}') + root.minsize(400, 400) + SpriteTagger(root, frames, labels, output_dir, store_path, assignments) + root.mainloop() + + +if __name__ == '__main__': + main() diff --git a/test_labelstore.py b/test_labelstore.py new file mode 100644 index 0000000..5ba1e7b --- /dev/null +++ b/test_labelstore.py @@ -0,0 +1,70 @@ +import json +import unittest +import tempfile +from pathlib import Path + +import labelstore + + +class SaveLoadRoundTrip(unittest.TestCase): + def setUp(self): + self._tmp = tempfile.TemporaryDirectory() + self.dir = Path(self._tmp.name) + self.path = self.dir / 'tags.json' + + def tearDown(self): + self._tmp.cleanup() + + def test_save_then_load_round_trips_assignments(self): + assignments = {'frame_000.png': 'idle', 'frame_001.png': 'walk'} + labelstore.save_labels(self.path, assignments, ['idle', 'walk']) + loaded, _labels = labelstore.load_labels(self.path) + self.assertEqual(loaded, assignments) + + def test_save_persists_label_list(self): + labelstore.save_labels(self.path, {}, ['idle', 'walk', 'crouch']) + _assignments, labels = labelstore.load_labels(self.path) + self.assertEqual(labels, ['idle', 'walk', 'crouch']) + + def test_save_writes_filenames_not_indices(self): + labelstore.save_labels(self.path, {'frame_007.png': 'jump'}, ['jump']) + raw = json.loads(self.path.read_text()) + self.assertIn('frame_007.png', raw['assignments']) + self.assertEqual(raw['assignments']['frame_007.png'], 'jump') + + def test_load_missing_file_returns_empty(self): + loaded, labels = labelstore.load_labels(self.dir / 'does_not_exist.json') + self.assertEqual(loaded, {}) + self.assertEqual(labels, []) + + def test_load_invalid_json_returns_empty(self): + self.path.write_text('{not valid json at all') + loaded, labels = labelstore.load_labels(self.path) + self.assertEqual(loaded, {}) + self.assertEqual(labels, []) + + +class IndexNameConversion(unittest.TestCase): + def setUp(self): + self.frames = [Path('a.png'), Path('b.png'), Path('c.png')] + + def test_to_name_keyed_maps_index_to_filename(self): + result = labelstore.to_name_keyed({0: 'idle', 2: 'run'}, self.frames) + self.assertEqual(result, {'a.png': 'idle', 'c.png': 'run'}) + + def test_to_index_keyed_maps_filename_to_index(self): + result = labelstore.to_index_keyed({'a.png': 'idle', 'c.png': 'run'}, self.frames) + self.assertEqual(result, {0: 'idle', 2: 'run'}) + + def test_to_index_keyed_skips_files_no_longer_present(self): + result = labelstore.to_index_keyed({'a.png': 'idle', 'gone.png': 'walk'}, self.frames) + self.assertEqual(result, {0: 'idle'}) + + def test_to_index_keyed_remaps_after_reorder(self): + reordered = [Path('c.png'), Path('a.png'), Path('b.png')] + result = labelstore.to_index_keyed({'a.png': 'idle'}, reordered) + self.assertEqual(result, {1: 'idle'}) + + +if __name__ == '__main__': + unittest.main() diff --git a/test_playback.py b/test_playback.py new file mode 100644 index 0000000..16445d5 --- /dev/null +++ b/test_playback.py @@ -0,0 +1,38 @@ +import unittest + +import playback + + +class FramesWithLabel(unittest.TestCase): + def test_returns_sorted_indices_matching_label(self): + assignments = {5: 'walk', 1: 'walk', 3: 'idle', 8: 'walk'} + self.assertEqual(playback.frames_with_label(assignments, 'walk'), [1, 5, 8]) + + def test_ignores_other_labels(self): + assignments = {0: 'idle', 1: 'walk', 2: 'run'} + self.assertEqual(playback.frames_with_label(assignments, 'idle'), [0]) + + def test_no_matches_returns_empty(self): + assignments = {0: 'idle', 1: 'walk'} + self.assertEqual(playback.frames_with_label(assignments, 'jump'), []) + + +class NextInCycle(unittest.TestCase): + def test_steps_forward(self): + self.assertEqual(playback.next_in_cycle([1, 5, 8], 1, 1), 5) + + def test_wraps_forward_at_end(self): + self.assertEqual(playback.next_in_cycle([1, 5, 8], 8, 1), 1) + + def test_steps_backward(self): + self.assertEqual(playback.next_in_cycle([1, 5, 8], 5, -1), 1) + + def test_wraps_backward_at_start(self): + self.assertEqual(playback.next_in_cycle([1, 5, 8], 1, -1), 8) + + def test_current_not_in_set_returns_first(self): + self.assertEqual(playback.next_in_cycle([1, 5, 8], 3, 1), 1) + + +if __name__ == '__main__': + unittest.main()