Compare commits
6 Commits
4d1b97f196
...
f47e3925f3
| Author | SHA1 | Date |
|---|---|---|
|
|
f47e3925f3 | |
|
|
d49cd08c23 | |
|
|
91df0b8ddd | |
|
|
e3c3fc7254 | |
|
|
0bf947b0c2 | |
|
|
91cc96a631 |
|
|
@ -1,6 +1,7 @@
|
|||
# ---> Python
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
__pycache__
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
|
|
|
|||
143
README.md
143
README.md
|
|
@ -1,118 +1,63 @@
|
|||
# ColorCalc β Interactive Colour Range Analyzer
|
||||
# ICRS (Interactive Color Range Analyzer)
|
||||
|
||||
ColorCalc is a desktop app for Windows/macOS/Linux that helps you identify and highlight colours in images. It is built with Python, Tkinter, and Pillow and is tuned for fullβHD screens. Instead of focusing on a single hue (like purple), you can target any colour range, tweak saturation/value thresholds, and export overlays in seconds.
|
||||
|
||||
---
|
||||
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`
|
||||
|
||||
- π― **Configurable hue detection** β adjust hue, saturation, and value windows or pick colours directly from the image/preset swatches.
|
||||
- πͺ **Responsive dual-preview UI** β original image on the left, processed overlay on the right, sized for 1080p.
|
||||
- π±οΈ **Interactive masks** β draw exclusion rectangles to ignore specific regions when calculating results.
|
||||
- π **Light & dark mode** β rounded toolbar buttons adapt to the current theme; toggle any time.
|
||||
- π¨ **Realβtime stats** β centre-aligned labels show match ratios with/without exclusions plus filenames and dimensions.
|
||||
- πΎ **Overlay export** β save PNG overlays blended with the source image for reporting or further editing.
|
||||
- βοΈ **Config file defaults** β populate `config.toml` to ship different starting values across machines.
|
||||
- π¨ **Preset palette** β one-click access to common colours (red, cyan, grey, black, β¦).
|
||||
|
||||
---
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Requirements
|
||||
|
||||
- Python 3.11+ (includes the standard `tomllib`; for 3.10 install `tomli`)
|
||||
- Tkinter (ships with most Python distributions; on Linux install `python3-tk`)
|
||||
- Pillow (`pip install pillow`)
|
||||
|
||||
### Installation
|
||||
## Requirements
|
||||
- Python 3.11+ (3.10 works with `tomli`)
|
||||
- [uv](https://github.com/astral-sh/uv) for dependency management
|
||||
- Tkinter (install separately on some Linux distros)
|
||||
|
||||
## Setup with uv
|
||||
```bash
|
||||
git clone https://github.com/<your-org>/ColorCalc.git
|
||||
cd ColorCalc
|
||||
python3 -m venv .venv
|
||||
source .venv/bin/activate # Windows: .venv\Scripts\activate
|
||||
pip install -r requirements.txt # or pip install pillow
|
||||
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 icrs # launches the GUI
|
||||
```
|
||||
|
||||
### Launch
|
||||
|
||||
To include the optional ttkbootstrap theme pack:
|
||||
```bash
|
||||
python3 main.py
|
||||
uv pip install '.[ui]'
|
||||
```
|
||||
|
||||
The window opens maximized; load an image, tweak sliders, and watch the overlay update in real time.
|
||||
|
||||
---
|
||||
|
||||
## Usage Tips
|
||||
|
||||
- **Pick colours quickly:** use the π¨ button to open a colour chooser or click directly inside the left preview (enable π±οΈ first).
|
||||
- **Fine-tune defaults:** edit `config.toml` and set keys under `[defaults]`. Restart the app to apply changes.
|
||||
- **Reset & compare:** `π Slider zurΓΌcksetzen` reverts to defaults and clears the status message.
|
||||
- **Exclude areas:** right-drag on the original preview to mark rectangles; use `β©οΈ` to undo or `π§Ή` to clear all.
|
||||
- **Copy stats:** right-click the filename or ratio labels to copy.
|
||||
- **Export overlay:** `πΎ` saves a PNG with your overlay merged onto the source image.
|
||||
- **Theme switch:** `π` toggles between light/dark; button palettes update instantly.
|
||||
|
||||
---
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
ColorCalc/
|
||||
βββ app/
|
||||
β βββ app.py # ColorCalcApp composition root
|
||||
β βββ __init__.py # Package exports
|
||||
β βββ gui/
|
||||
β β βββ color_picker.py # Colour selection & presets
|
||||
β β βββ exclusions.py # Exclusion rectangle handlers
|
||||
β β βββ theme.py # Theme detection & style updates
|
||||
β β βββ ui.py # Tkinter layout and custom widgets
|
||||
β βββ logic/
|
||||
β βββ constants.py # Config defaults & preview sizing
|
||||
β βββ image_processing.py # Loading, overlay creation, stats
|
||||
β βββ reset.py # Slider reset mixin
|
||||
βββ config.toml # Default HSV/alpha overrides
|
||||
βββ images/ # Optional sample inputs
|
||||
βββ main.py # CLI entry point (`python3 main.py`)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Configuration (`config.toml`)
|
||||
## Workflow
|
||||
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. Move through folder images with `β¬
οΈ` / `β‘οΈ`.
|
||||
5. Draw exclusions with right drag; reset or save when ready.
|
||||
|
||||
## Config Defaults
|
||||
Optional `config.toml`:
|
||||
```toml
|
||||
[defaults]
|
||||
hue_min = 250.0 # 0..360Β°
|
||||
hue_min = 250.0
|
||||
hue_max = 310.0
|
||||
sat_min = 15.0 # percentage 0..100
|
||||
sat_min = 15.0
|
||||
val_min = 15.0
|
||||
val_max = 100.0
|
||||
alpha = 120 # overlay opacity 0..255
|
||||
alpha = 120
|
||||
```
|
||||
|
||||
All values are optional; omit them to fall back to built-in defaults. Hue min/max support wrap-around (e.g. 350 to 20).
|
||||
## Project Layout
|
||||
```
|
||||
app/
|
||||
app.py # main app assembly
|
||||
gui/ # UI, theme, picker mixins
|
||||
logic/ # image ops, defaults, reset
|
||||
config.toml # optional defaults
|
||||
main.py # entry point
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Development Workflow
|
||||
|
||||
- Format: project sticks to standard Python style; no formatter enforced.
|
||||
- Testing: primary quick check is compilation via `python3 -m compileall app main.py`.
|
||||
- Platform quirks: some theme detection uses Windows registry; errors are swallowed when unavailable.
|
||||
|
||||
---
|
||||
|
||||
## Contributing
|
||||
|
||||
1. Fork + clone.
|
||||
2. Create a feature branch.
|
||||
3. Make changes and run `python3 -m compileall app main.py`.
|
||||
4. Submit a PR with a clear description and screenshots if UI changes are visible.
|
||||
|
||||
---
|
||||
|
||||
## License
|
||||
|
||||
MIT License Β© 2024 ColorCalc contributors. See `LICENSE` (add one if missing) for details.
|
||||
## Development
|
||||
- Quick check: `uv run python -m compileall app main.py`
|
||||
- Contributions welcome; include screenshots for UI tweaks.
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -0,0 +1,20 @@
|
|||
[project]
|
||||
name = "icrs"
|
||||
version = "0.1.0"
|
||||
description = "Interactive Color Range Analyzer (ICRS) for Tkinter"
|
||||
readme = "README.md"
|
||||
authors = [{ name = "ICRS contributors" }]
|
||||
license = { text = "MIT" }
|
||||
requires-python = ">=3.10"
|
||||
dependencies = [
|
||||
"pillow>=10.0.0",
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
ui = ["ttkbootstrap>=1.10.0"]
|
||||
|
||||
[project.scripts]
|
||||
icrs = "app.app:start_app"
|
||||
|
||||
[tool.uv]
|
||||
package = false
|
||||
Loadingβ¦
Reference in New Issue