Rename project to ICRS and add folder browsing
This commit is contained in:
		
							parent
							
								
									d49cd08c23
								
							
						
					
					
						commit
						f47e3925f3
					
				
							
								
								
									
										16
									
								
								README.md
								
								
								
								
							
							
						
						
									
										16
									
								
								README.md
								
								
								
								
							|  | @ -1,12 +1,13 @@ | |||
| # ColorCalc | ||||
| # ICRS (Interactive Color Range Analyzer) | ||||
| 
 | ||||
| ColorCalc is a small Tkinter tool for analysing colour ranges in images. You load a picture, pick or click a reference colour, adjust hue/saturation/value sliders, and the app marks matching pixels while showing quick stats. | ||||
| ICRS is a small Tkinter tool for analysing colour ranges in images. You load a picture, pick or click a reference colour, adjust hue/saturation/value sliders, and the app marks matching pixels while showing quick stats. | ||||
| 
 | ||||
| ## Features | ||||
| - Two synced previews (original + overlay) | ||||
| - Hue/Sat/Value sliders with presets and image colour picker | ||||
| - Exclusion rectangles to ignore regions | ||||
| - Theme toggle (light/dark) with rounded toolbar buttons | ||||
| - Folder support with previous/next navigation | ||||
| - Quick overlay export (PNG) and optional defaults via `config.toml` | ||||
| 
 | ||||
| ## Requirements | ||||
|  | @ -16,12 +17,12 @@ ColorCalc is a small Tkinter tool for analysing colour ranges in images. You loa | |||
| 
 | ||||
| ## Setup with uv | ||||
| ```bash | ||||
| git clone https://github.com/<your-org>/ColorCalc.git | ||||
| cd ColorCalc | ||||
| git clone https://github.com/<your-org>/ICRS.git | ||||
| cd ICRS | ||||
| uv venv | ||||
| source .venv/bin/activate         # Windows: .venv\Scripts\activate | ||||
| uv pip sync                       # installs Pillow + optional deps from pyproject | ||||
| uv run colorcalc                  # launches the GUI | ||||
| uv run icrs                       # launches the GUI | ||||
| ``` | ||||
| To include the optional ttkbootstrap theme pack: | ||||
| ```bash | ||||
|  | @ -29,10 +30,11 @@ uv pip install '.[ui]' | |||
| ``` | ||||
| 
 | ||||
| ## Workflow | ||||
| 1. Load an image (`📂`). | ||||
| 1. Load an image (`📂`) or a folder (`📁`). | ||||
| 2. Pick a colour (`🎨` dialog, `🖱️` image click, or preset swatch). | ||||
| 3. Fine‑tune sliders; watch the overlay update on the right. | ||||
| 4. Draw exclusions with right drag; reset or save when ready. | ||||
| 4. Move through folder images with `⬅️` / `➡️`. | ||||
| 5. Draw exclusions with right drag; reset or save when ready. | ||||
| 
 | ||||
| ## Config Defaults | ||||
| Optional `config.toml`: | ||||
|  |  | |||
|  | @ -1,5 +1,5 @@ | |||
| """Application package.""" | ||||
| 
 | ||||
| from .app import ColorCalcApp, start_app | ||||
| from .app import ICRSApp, start_app | ||||
| 
 | ||||
| __all__ = ["ColorCalcApp", "start_app"] | ||||
| __all__ = ["ICRSApp", "start_app"] | ||||
|  |  | |||
							
								
								
									
										10
									
								
								app/app.py
								
								
								
								
							
							
						
						
									
										10
									
								
								app/app.py
								
								
								
								
							|  | @ -8,7 +8,7 @@ from .gui import ColorPickerMixin, ExclusionMixin, ThemeMixin, UIBuilderMixin | |||
| from .logic import DEFAULTS, ImageProcessingMixin, ResetMixin | ||||
| 
 | ||||
| 
 | ||||
| class ColorCalcApp( | ||||
| class ICRSApp( | ||||
|     ThemeMixin, | ||||
|     UIBuilderMixin, | ||||
|     ImageProcessingMixin, | ||||
|  | @ -20,7 +20,7 @@ class ColorCalcApp( | |||
| 
 | ||||
|     def __init__(self, root: tk.Tk): | ||||
|         self.root = root | ||||
|         self.root.title("ColorCalc — Bild + Overlay") | ||||
|         self.root.title("ICRS — Interactive Color Range Analyzer") | ||||
|         try: | ||||
|             self.root.state("zoomed") | ||||
|         except Exception: | ||||
|  | @ -56,6 +56,8 @@ class ColorCalcApp( | |||
|         self.preview_img = None | ||||
|         self.preview_tk = None | ||||
|         self.overlay_tk = None | ||||
|         self.image_paths = [] | ||||
|         self.current_image_index = -1 | ||||
| 
 | ||||
|         # Build UI | ||||
|         self.setup_ui() | ||||
|  | @ -66,8 +68,8 @@ class ColorCalcApp( | |||
| def start_app() -> None: | ||||
|     """Entry point used by the CLI script.""" | ||||
|     root = tk.Tk() | ||||
|     app = ColorCalcApp(root) | ||||
|     app = ICRSApp(root) | ||||
|     root.mainloop() | ||||
| 
 | ||||
| 
 | ||||
| __all__ = ["ColorCalcApp", "start_app"] | ||||
| __all__ = ["ICRSApp", "start_app"] | ||||
|  |  | |||
|  | @ -15,6 +15,9 @@ class UIBuilderMixin: | |||
|         toolbar.pack(fill=tk.X, padx=12, pady=8) | ||||
|         buttons = [ | ||||
|             ("📂 Bild laden", self.load_image), | ||||
|             ("📁 Ordner laden", self.load_folder), | ||||
|             ("⬅️ Bild zurück", self.show_previous_image), | ||||
|             ("➡️ Nächstes Bild", self.show_next_image), | ||||
|             ("🎨 Farbe wählen", self.choose_color), | ||||
|             ("🖱️Farbe aus Bild klicken", self.enable_pick_mode), | ||||
|             ("💾 Overlay speichern", self.save_overlay), | ||||
|  |  | |||
|  | @ -1,6 +1,6 @@ | |||
| """Logic utilities and mixins for processing and configuration.""" | ||||
| 
 | ||||
| from .constants import BASE_DIR, DEFAULTS, IMAGES_DIR, PREVIEW_MAX_SIZE | ||||
| from .constants import BASE_DIR, DEFAULTS, IMAGES_DIR, PREVIEW_MAX_SIZE, SUPPORTED_IMAGE_EXTENSIONS | ||||
| from .image_processing import ImageProcessingMixin | ||||
| from .reset import ResetMixin | ||||
| 
 | ||||
|  | @ -9,6 +9,7 @@ __all__ = [ | |||
|     "DEFAULTS", | ||||
|     "IMAGES_DIR", | ||||
|     "PREVIEW_MAX_SIZE", | ||||
|     "SUPPORTED_IMAGE_EXTENSIONS", | ||||
|     "ImageProcessingMixin", | ||||
|     "ResetMixin", | ||||
| ] | ||||
|  |  | |||
|  | @ -29,6 +29,8 @@ _DEFAULTS_BASE = { | |||
|     "alpha": 120, | ||||
| } | ||||
| 
 | ||||
| SUPPORTED_IMAGE_EXTENSIONS = (".webp", ".png", ".jpg", ".jpeg", ".bmp") | ||||
| 
 | ||||
| _DEFAULT_TYPES: dict[str, Callable[[Any], Any]] = { | ||||
|     "hue_min": float, | ||||
|     "hue_max": float, | ||||
|  |  | |||
|  | @ -4,13 +4,13 @@ from __future__ import annotations | |||
| 
 | ||||
| import colorsys | ||||
| from pathlib import Path | ||||
| from typing import Iterable, Tuple | ||||
| from typing import Iterable, Sequence, Tuple | ||||
| 
 | ||||
| from tkinter import filedialog, messagebox | ||||
| 
 | ||||
| from PIL import Image, ImageDraw, ImageTk | ||||
| 
 | ||||
| from .constants import IMAGES_DIR, PREVIEW_MAX_SIZE | ||||
| from .constants import IMAGES_DIR, PREVIEW_MAX_SIZE, SUPPORTED_IMAGE_EXTENSIONS | ||||
| 
 | ||||
| 
 | ||||
| class ImageProcessingMixin: | ||||
|  | @ -22,6 +22,9 @@ class ImageProcessingMixin: | |||
|     preview_tk: ImageTk.PhotoImage | None | ||||
|     overlay_tk: ImageTk.PhotoImage | None | ||||
| 
 | ||||
|     image_paths: list[Path] | ||||
|     current_image_index: int | ||||
| 
 | ||||
|     def load_image(self) -> None: | ||||
|         default_dir = IMAGES_DIR if IMAGES_DIR.exists() else Path.cwd() | ||||
|         path = filedialog.askopenfilename( | ||||
|  | @ -31,21 +34,88 @@ class ImageProcessingMixin: | |||
|         ) | ||||
|         if not path: | ||||
|             return | ||||
|         self.image_path = Path(path) | ||||
|         self._set_image_collection([Path(path)], 0) | ||||
| 
 | ||||
|     def load_folder(self) -> None: | ||||
|         default_dir = IMAGES_DIR if IMAGES_DIR.exists() else Path.cwd() | ||||
|         directory = filedialog.askdirectory( | ||||
|             title="Ordner mit Bildern wählen", | ||||
|             initialdir=str(default_dir), | ||||
|         ) | ||||
|         if not directory: | ||||
|             return | ||||
|         folder = Path(directory) | ||||
|         if not folder.exists(): | ||||
|             messagebox.showerror("Fehler", "Der Ordner wurde nicht gefunden.") | ||||
|             return | ||||
|         image_files = sorted( | ||||
|             ( | ||||
|                 path | ||||
|                 for path in folder.iterdir() | ||||
|                 if path.suffix.lower() in SUPPORTED_IMAGE_EXTENSIONS and path.is_file() | ||||
|             ), | ||||
|             key=lambda item: item.name.lower(), | ||||
|         ) | ||||
|         if not image_files: | ||||
|             messagebox.showinfo("Info", "Keine unterstützten Bilder im Ordner gefunden.") | ||||
|             return | ||||
|         self._set_image_collection(image_files, 0) | ||||
| 
 | ||||
|     def show_next_image(self, event=None) -> None: | ||||
|         if not getattr(self, "image_paths", None): | ||||
|             return | ||||
|         next_index = getattr(self, "current_image_index", -1) + 1 | ||||
|         if next_index < len(self.image_paths): | ||||
|             self._display_image_by_index(next_index) | ||||
| 
 | ||||
|     def show_previous_image(self, event=None) -> None: | ||||
|         if not getattr(self, "image_paths", None): | ||||
|             return | ||||
|         prev_index = getattr(self, "current_image_index", -1) - 1 | ||||
|         if prev_index >= 0: | ||||
|             self._display_image_by_index(prev_index) | ||||
| 
 | ||||
|     def _set_image_collection(self, paths: Sequence[Path], start_index: int) -> None: | ||||
|         self.image_paths = list(paths) | ||||
|         if not self.image_paths: | ||||
|             return | ||||
|         self.current_image_index = -1 | ||||
|         self._display_image_by_index(max(0, start_index)) | ||||
| 
 | ||||
|     def _display_image_by_index(self, index: int) -> None: | ||||
|         if not self.image_paths: | ||||
|             return | ||||
|         if index < 0 or index >= len(self.image_paths): | ||||
|             return | ||||
|         path = self.image_paths[index] | ||||
|         if not path.exists(): | ||||
|             messagebox.showerror("Fehler", f"Datei nicht gefunden: {path}") | ||||
|             return | ||||
|         try: | ||||
|             image = Image.open(path).convert("RGBA") | ||||
|         except Exception as exc: | ||||
|             messagebox.showerror("Fehler", f"Bild konnte nicht geladen werden: {exc}") | ||||
|             return | ||||
| 
 | ||||
|         self.image_path = path | ||||
|         self.orig_img = image | ||||
|         self.exclude_rects = [] | ||||
|         self._rubber_start = None | ||||
|         self._rubber_id = None | ||||
|         self.pick_mode = False | ||||
| 
 | ||||
|         self.prepare_preview() | ||||
|         self.update_preview() | ||||
| 
 | ||||
|         dimensions = f"{self.orig_img.width}x{self.orig_img.height}" | ||||
|         status_text = f"Geladen: {self.image_path.name} — {dimensions}" | ||||
|         suffix = f" [{index + 1}/{len(self.image_paths)}]" if len(self.image_paths) > 1 else "" | ||||
|         status_text = f"Geladen: {path.name} — {dimensions}{suffix}" | ||||
|         self.status.config(text=status_text) | ||||
|         self.status_default_text = status_text | ||||
|         if hasattr(self, "filename_label"): | ||||
|             self.filename_label.config(text=f"{self.image_path.name} — {dimensions}") | ||||
|             self.filename_label.config(text=f"{path.name} — {dimensions}{suffix}") | ||||
| 
 | ||||
|         self.current_image_index = index | ||||
| 
 | ||||
|     def save_overlay(self) -> None: | ||||
|         if self.orig_img is None: | ||||
|  |  | |||
|  | @ -1,9 +1,9 @@ | |||
| [project] | ||||
| name = "colorcalc" | ||||
| name = "icrs" | ||||
| version = "0.1.0" | ||||
| description = "Interactive colour range analyser for Tkinter" | ||||
| description = "Interactive Color Range Analyzer (ICRS) for Tkinter" | ||||
| readme = "README.md" | ||||
| authors = [{ name = "ColorCalc contributors" }] | ||||
| authors = [{ name = "ICRS contributors" }] | ||||
| license = { text = "MIT" } | ||||
| requires-python = ">=3.10" | ||||
| dependencies = [ | ||||
|  | @ -14,7 +14,7 @@ dependencies = [ | |||
| ui = ["ttkbootstrap>=1.10.0"] | ||||
| 
 | ||||
| [project.scripts] | ||||
| colorcalc = "app.app:start_app" | ||||
| icrs = "app.app:start_app" | ||||
| 
 | ||||
| [tool.uv] | ||||
| package = false | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue