Rename project to ICRS and add folder browsing

This commit is contained in:
lm 2025-10-17 12:31:21 +02:00
parent d49cd08c23
commit f47e3925f3
8 changed files with 103 additions and 23 deletions

View File

@ -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. Finetune 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`:

View File

@ -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"]

View File

@ -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"]

View File

@ -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),

View File

@ -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",
]

View File

@ -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,

View File

@ -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:

View File

@ -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