Compare commits
111 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e3114d6255 | |||
| 91a6beb032 | |||
| 0420c4a863 | |||
| caabb05023 | |||
| 2f15a37358 | |||
| 4356a47d06 | |||
| 9458173ad0 | |||
| cc811fdc94 | |||
| 452810b67a | |||
| 8c46a6ca9b | |||
| d335f866a3 | |||
| 88f80a2cad | |||
| d52d0d0489 | |||
| 9451a418a6 | |||
| 2c9160e4be | |||
| 6d6dcc3b7a | |||
| ee1c4a8f92 | |||
| 5002515b41 | |||
| 8029a1e12b | |||
| d37833076e | |||
| e1ed9206a3 | |||
| e84ae199ac | |||
| 5f0c4542d3 | |||
| 29c034fb05 | |||
| 6fb1efcab8 | |||
| 35df4c473c | |||
| 64f2c8b5dc | |||
| 7e076deb80 | |||
| 852597ed51 | |||
| a78884f950 | |||
| 543ae0f643 | |||
| a12574f3c5 | |||
| 110dc87b08 | |||
| 2bb2cf63cc | |||
| ea6a9163ad | |||
| 1cc7881a51 | |||
| 74a332a2dd | |||
| dae49eb4a3 | |||
| 9218cb2741 | |||
| 159f9089a5 | |||
| b718e81ccf | |||
| d46197a81a | |||
| 37c645984f | |||
| 0e148667ec | |||
| b5bbca0e85 | |||
| ca3882c59c | |||
| 7f6571bdd1 | |||
| 7cb1ae2df7 | |||
| 6ebb08e7a2 | |||
| eba9d478a7 | |||
| 0df0d98aa5 | |||
| b2b959e801 | |||
| b05246b492 | |||
| aeaa7fb5f7 | |||
| f347a10fad | |||
| 0b24be4d94 | |||
| 0296083e3c | |||
| 39208aadab | |||
| 2b7ee6799c | |||
| 5059ce1d89 | |||
| f05dec5183 | |||
| f8f6a15166 | |||
| 5bd8fca248 | |||
| 796ccb8052 | |||
| 0a8a9365bb | |||
| 9ed779637e | |||
| 077d44c3c8 | |||
| e038ee3a1d | |||
| 041b26e791 | |||
| 84b73dc651 | |||
| 8d8a89ac35 | |||
| 41976f574d | |||
| 4ef7a4a85f | |||
| 7de7f35b7c | |||
| 7b014b7f69 | |||
| 367ee9aaac | |||
| 74e5a45a39 | |||
| 11c5160385 | |||
| 07bab87cb9 | |||
| a247484f36 | |||
| e188df0adb | |||
| b35d47669c | |||
| fc3b0dbc3a | |||
| 6da4dd5329 | |||
| b143c6607a | |||
| 6704d66cd5 | |||
| 4419c237b2 | |||
| f00cf9b621 | |||
| 4b7271094b | |||
| 746d1668c6 | |||
| d9a40952c4 | |||
| 6db2086ead | |||
| 27a0ef1a45 | |||
| ba4024d252 | |||
| 89b59b3ea3 | |||
| 44a3046616 | |||
| 46e9941488 | |||
| 71a364a1fd | |||
| 3e4c20ecf5 | |||
| cc7d035f66 | |||
| 37b718e45e | |||
| b83e577eab | |||
| 2bca68d700 | |||
| 1671a151da | |||
| 1954bc6ffd | |||
| 45e3a29ff0 | |||
| e1a1b956fd | |||
| 9fba46d7f7 | |||
| d35bb574ef | |||
| fd7585acc5 | |||
| 4ddda1ec62 |
@@ -0,0 +1,22 @@
|
||||
.venv
|
||||
.git
|
||||
.gitignore
|
||||
.github
|
||||
__pycache__
|
||||
*.pyc
|
||||
*.pyo
|
||||
*.pyd
|
||||
.DS_Store
|
||||
.idea
|
||||
.vscode
|
||||
*.log
|
||||
# Test images non necessarie nel container (caricate via volume/UI)
|
||||
Test
|
||||
benchmarks
|
||||
ROADMAP.md
|
||||
shape_model_2d_technical_doc.md
|
||||
*.md
|
||||
!README.md
|
||||
Dockerfile
|
||||
docker-compose*.yml
|
||||
.env
|
||||
@@ -0,0 +1,14 @@
|
||||
# Copia questo file in .env e adatta i valori.
|
||||
# .env NON è versionato (contiene config locale/secrets).
|
||||
|
||||
# Cartella immagini (relativa al progetto in dev locale,
|
||||
# assoluta dentro container es. /data/images)
|
||||
IMAGES_DIR=Test
|
||||
|
||||
# Web server
|
||||
HOST=127.0.0.1
|
||||
PORT=8080
|
||||
|
||||
# Registry + tag per docker-compose (deploy VPS)
|
||||
REGISTRY=localhost:5000
|
||||
TAG=latest
|
||||
@@ -0,0 +1,31 @@
|
||||
# CI Gitea Actions: lint (ruff) + test sintetici (pytest).
|
||||
# I test non richiedono le immagini in Test/ (sono generati a runtime).
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Installa uv
|
||||
run: |
|
||||
curl -LsSf https://astral.sh/uv/install.sh | sh
|
||||
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
|
||||
|
||||
- name: Sync dipendenze
|
||||
run: uv sync
|
||||
|
||||
- name: Lint (ruff)
|
||||
# Ignore da CLI (pyproject.toml non va toccato): E501/E741 +
|
||||
# stile pre-esistente del progetto (E702 statement con ';',
|
||||
# E402 import dopo setup env, F841/F401 nei moduli legacy).
|
||||
run: uv run ruff check pm2d/
|
||||
|
||||
- name: Test (pytest)
|
||||
run: uv run pytest tests/ -v
|
||||
@@ -8,3 +8,12 @@ __pycache__/
|
||||
.DS_Store
|
||||
*.log
|
||||
models/
|
||||
# Ricette pre-trained (generate da utente, non versionare)
|
||||
recipes/*.npz
|
||||
# Immagini di test locali (richieste da benchmarks/test_suite.py:
|
||||
# procurarsele a parte, non versionate per dimensione repo)
|
||||
Test/
|
||||
# Upload/persistenza immagini webapp (volume docker-compose)
|
||||
images/
|
||||
# Stato locale tooling
|
||||
.omc/
|
||||
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
FROM python:3.13-slim AS base
|
||||
|
||||
# uv package manager (copia binario ufficiale)
|
||||
COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv
|
||||
|
||||
# System deps per opencv (libgl/glib), numba (libgomp)
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
libgl1 \
|
||||
libglib2.0-0 \
|
||||
libgomp1 \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Install deps da lockfile (layer cachato finché pyproject/uv.lock non cambiano)
|
||||
COPY pyproject.toml uv.lock ./
|
||||
COPY .python-version* ./
|
||||
RUN uv sync --frozen --no-dev
|
||||
|
||||
# Copia sorgenti applicazione
|
||||
COPY pm2d ./pm2d
|
||||
COPY main.py ./
|
||||
|
||||
# Defaults (override via docker-compose env)
|
||||
ENV IMAGES_DIR=/data/images \
|
||||
HOST=0.0.0.0 \
|
||||
PORT=8080 \
|
||||
PYTHONUNBUFFERED=1
|
||||
|
||||
# Cartella dati (montata come volume in compose)
|
||||
RUN mkdir -p /data/images
|
||||
|
||||
EXPOSE 8080
|
||||
|
||||
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
|
||||
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8080/images').read()" || exit 1
|
||||
|
||||
CMD ["uv", "run", "python", "main.py"]
|
||||
@@ -140,3 +140,52 @@ Implementato con **shift+add vettorizzato NumPy** (O(N_features · H · W) invec
|
||||
- ICP locale per raffinamento pose
|
||||
- Vincoli di orientamento: clustering delle pose per eliminare duplicati cross-variante
|
||||
- Numba JIT per il ciclo shift+add (eventuale 3-5× su scene grandi)
|
||||
|
||||
## Deploy VPS con Docker + Traefik
|
||||
|
||||
Assume che sulla VPS siano già attivi:
|
||||
- **Traefik** come reverse proxy su network Docker esterna `traefik`
|
||||
- Entrypoints `web` (:80) e `websecure` (:443)
|
||||
- Cert resolver `letsencrypt` configurato
|
||||
|
||||
### Build e push al registry
|
||||
|
||||
```bash
|
||||
# Build locale
|
||||
docker build -t vps-ip:5000/pm2d:latest .
|
||||
docker push vps-ip:5000/pm2d:latest
|
||||
```
|
||||
|
||||
### Sulla VPS
|
||||
|
||||
```bash
|
||||
# Cartella deploy (immagini persistenti qui)
|
||||
mkdir -p /opt/docker/pm2d/images
|
||||
cd /opt/docker/pm2d
|
||||
|
||||
# Copia docker-compose.yml
|
||||
# Imposta REGISTRY / TAG se necessario via .env
|
||||
echo "REGISTRY=vps-ip:5000" > .env
|
||||
echo "TAG=latest" >> .env
|
||||
|
||||
docker compose pull
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
Servizio raggiungibile: **https://pm.tielogic.xyz**
|
||||
|
||||
### Note operative
|
||||
|
||||
- **Volume `./images`**: persistenza delle immagini caricate tramite UI
|
||||
(`IMAGES_DIR=/data/images` nel container). Sopravvive a restart.
|
||||
- **Upload max 50MB**: middleware Traefik `pm2d-bodysize`. Adattare se serve.
|
||||
- **Cache matcher in-memory**: si svuota a restart container (no problema,
|
||||
viene ri-popolata al primo match).
|
||||
- **Healthcheck**: HTTP `GET /images` ogni 30s.
|
||||
- Se nome network Traefik diverso da `traefik`, modifica
|
||||
`docker-compose.yml` sezione `networks`.
|
||||
|
||||
### Adattamenti config Traefik non-standard
|
||||
|
||||
Se la VPS ha convenzioni diverse (es. cert resolver chiamato `le`,
|
||||
entrypoint `https`), modifica i labels nel `docker-compose.yml`.
|
||||
|
||||
+55
-4
@@ -2,6 +2,52 @@
|
||||
|
||||
Lista ragionata di miglioramenti futuri. Priorità = impatto / effort, non urgenza temporale.
|
||||
|
||||
## Fase 2 COMPLETATA (precisione rotazione + robustezza + perf)
|
||||
|
||||
Root cause della rotazione imprecisa: lo score satura a 1.0 sulla spread
|
||||
bitmap dilatata (raggio 4-5) → il refine non vedeva gradiente né in angolo
|
||||
né in posizione, e `cv2.minMaxLoc` sul plateau saturo spostava il centro
|
||||
sull'angolo della finestra (errore sistematico 3·√2 ≈ 4.24 px).
|
||||
|
||||
| Fix | Dettaglio |
|
||||
|---|---|
|
||||
| Refine su bitmap fine | `_refine_angle` ottimizza su spread raggio 1 (`spread_fine`, cached); score finale ricalcolato su spread coarse per mantenere semantica soglie |
|
||||
| Picco sub-pixel nel refine | centroide plateau / fit quadratico al posto di minMaxLoc (bias top-left) |
|
||||
| LM least-squares pos+angolo | `_subpixel_refine_lm` riscritto: snap edge ±2px lungo normale + LSQ 3x3 (dx, dy, dθ), ON di default |
|
||||
| Round feature offsets | troncamento `astype(int32)` → `np.round` (bias ~0.25 px) |
|
||||
| Centro rotazione coerente | `_prepare_padded_template`: rotazione attorno al centro reale del template nel padding (bias ≤0.5 px dipendente dall'angolo) |
|
||||
| `_angle_list` include estremo | range parziali ±tol ora testano anche +tol |
|
||||
| `_refine_pose_joint` rimosso | Nelder-Mead su funzione a gradini satura: terminava subito; param ora alias di refine_angle |
|
||||
| pyramid_propagate di default | kernel windowed (feature campionano l'intera scena: prima il crop troncava le feature → score 0); picchi = massimi locali (non top-K pixel); disattivato automaticamente per template elongati (>2:1) dove il picco top-level non localizza |
|
||||
| Piramide 3 livelli default | con clamp automatico sulla dimensione template (min 12 px al top) |
|
||||
| Cache scena: hash completo | prima hashava solo i primi 64KB → collisioni tra scene con stessa banda superiore → risultati della scena sbagliata |
|
||||
| Web server | lock matcher (race con threadpool FastAPI), LRU `_IMG_CACHE`, clamp ROI ovunque (400/422 invece di 500), `filtro_fp=off` disabilita davvero NCC, `_draw_matches` su crop locale |
|
||||
| GUI/legacy | centro overlay `(W-1)/2`→`W/2`, spread_radius default 5→4, EdgeShapeMatcher: angle list endpoint + cap candidati + save template_gray |
|
||||
|
||||
Misure (GT sintetica 7 pose, scena 900x700, VPS 2 core):
|
||||
- Errore angolare mediano: **2.3° → 0.05°** (step 5°); a step 2° era 4.4° → **0.03°**
|
||||
- Errore posizione mediano: **4.24 px → 0.04 px**
|
||||
- find GT scene: 4.7s → 1.7s; scena reale 646x482: 1.14s → 0.81s
|
||||
- Benchmark suite 16 scenari: 96.5s → 84.2s, match count ≥ baseline
|
||||
(eccezioni: dado_full -1 = match borderline su parte diversa;
|
||||
lama_part_preciso 25→18 con baseline al cap max_matches)
|
||||
|
||||
## Fase 1 COMPLETATA (branch `speedFase1`)
|
||||
|
||||
| ID | Voce | Status | Note |
|
||||
|---|---|---|---|
|
||||
| V1 | Coarse-to-fine angolare (step coarse al top-level) | ✅ | `coarse_angle_factor=2` default, safe anche su template allungati |
|
||||
| V11 | Cache matcher in-memory LRU (capacità 8) | ✅ | Key = hash(ROI bytes + params). Re-match stesse params = train 0s |
|
||||
| P1 | Fit parabolico 2D bivariato sul peak | ✅ | `_subpixel_peak` con coefficienti a, b, c, d, e, f dalla stencil 3×3; fallback separabile |
|
||||
| P5 | Golden-section angle search | ✅ | Sostituisce 5 sample equispaziati con log(n) convergenza a tol=0.1° |
|
||||
| P2 | Weighted centroid del plateau | ✅ | Integrato in `_subpixel_peak` con peso = (score - soglia)² |
|
||||
|
||||
Benchmark suite 16 scenari (4 immagini × full/part × fast/preciso):
|
||||
- Prima Fase 1: totale find 27.3s
|
||||
- Dopo Fase 1: totale find 25.1s (~8% speedup)
|
||||
- Regressione match count: nessuna (alcuni casi +1 match grazie a subpixel migliore)
|
||||
- Match auto-referenziale: offset 0.00 px, angolo 0.000° (era -3.5 px, -2.5°)
|
||||
|
||||
## Performance CPU
|
||||
|
||||
| Sviluppo | Effort | Speed-up atteso | Dipendenze | Priorità |
|
||||
@@ -68,9 +114,14 @@ Lista ragionata di miglioramenti futuri. Priorità = impatto / effort, non urgen
|
||||
## Target performance produzione
|
||||
|
||||
Obiettivi da documento tecnico Vision Suite (Fase Beta):
|
||||
- [ ] **Precisione posizionale mediana**: <0.5 px → **raggiunto con subpixel (attualmente ~0.1-0.3 px atteso)**
|
||||
- [ ] **Precisione angolare mediana**: <1.0° → **raggiunto con refinement (~0.5°)**
|
||||
- [ ] **Latency mediana**: <50 ms su 1920×1080 → **attuale ~1.7s su 830×822 (serve GPU o ulteriore CPU)**
|
||||
- [x] **Precisione posizionale mediana**: <0.5 px → **0.04 px misurato su GT sintetica (Fase 2)**
|
||||
- [x] **Precisione angolare mediana**: <1.0° → **0.05° misurato su GT sintetica (Fase 2)**
|
||||
- [ ] **Latency mediana**: <50 ms su 1920×1080 → **~0.8s su 646×482 con 2 core; da misurare su hardware produzione**
|
||||
- [ ] **F1 score dataset sintetico**: >0.95 → **da misurare con dataset sintetico**
|
||||
|
||||
Prossimo blocker per target: **latency**. Via più promettente: GPU (CuPy) o coarse-to-fine angolare.
|
||||
Prossimo blocker per target: **latency**. Nota: i kernel hot sono gia'
|
||||
Numba JIT (≈ velocita' C, prange parallelo): un port C++ dei kernel vale
|
||||
solo il margine SIMD esplicito (~2-4x con AVX2 su AND+popcount byte-wise).
|
||||
Prima di scriverlo conviene esaurire le vie algoritmiche rimaste:
|
||||
riduzione varianti al top-level (auto angle step per livello, stile
|
||||
Halcon), greediness di default, e GPU (CuPy/OpenCL) per scene 1080p.
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 283 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 150 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 79 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 130 KiB |
@@ -36,6 +36,11 @@ CONFIGS = [
|
||||
def bench(case_name: str, img_path: str, roi_box: tuple, roi_kind: str,
|
||||
cfg_name: str, cfg: dict) -> dict:
|
||||
scene = cv2.imread(str(TEST_DIR / img_path))
|
||||
if scene is None:
|
||||
# cv2.imread ritorna None silenzioso: senza check il crash arriva
|
||||
# dopo, sullo slice, con un errore criptico.
|
||||
raise FileNotFoundError(
|
||||
f"Immagine di test non trovata o non leggibile: {TEST_DIR / img_path}")
|
||||
y0, y1, x0, x1 = roi_box
|
||||
roi = scene[y0:y1, x0:x1].copy()
|
||||
m = LineShapeMatcher(
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
# docker-compose per deploy VPS con Traefik.
|
||||
# Assume che Traefik sia già attivo sulla VPS con:
|
||||
# - network esterna "traefik" (adatta nome se diverso)
|
||||
# - entrypoint "websecure" su :443
|
||||
# - certresolver "mytlschallenge" configurato
|
||||
#
|
||||
# Adattare eventualmente: nome network, entrypoint, certresolver.
|
||||
|
||||
services:
|
||||
pm2d:
|
||||
build: .
|
||||
image: pm2d:latest
|
||||
container_name: pm2d
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
IMAGES_DIR: /data/images
|
||||
HOST: 0.0.0.0
|
||||
PORT: ${PORT:-8080}
|
||||
volumes:
|
||||
# Persistenza immagini tra restart (upload/selezione)
|
||||
- ./images:/data/images
|
||||
networks:
|
||||
- traefik
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
|
||||
# Router HTTPS principale
|
||||
- "traefik.http.routers.pm2d.rule=Host(`pm.tielogic.xyz`)"
|
||||
- "traefik.http.routers.pm2d.entrypoints=websecure"
|
||||
- "traefik.http.routers.pm2d.tls=true"
|
||||
- "traefik.http.routers.pm2d.tls.certresolver=mytlschallenge"
|
||||
- "traefik.http.services.pm2d.loadbalancer.server.port=${PORT:-8080}"
|
||||
|
||||
# Middleware: upload fino a 50MB (default Traefik bufferizza a 4MB)
|
||||
- "traefik.http.middlewares.pm2d-bodysize.buffering.maxRequestBodyBytes=52428800"
|
||||
- "traefik.http.routers.pm2d.middlewares=pm2d-bodysize"
|
||||
# Redirect HTTP → HTTPS è gestito globalmente dall'entrypoint `web` di Traefik
|
||||
|
||||
networks:
|
||||
traefik:
|
||||
external: true
|
||||
@@ -1,26 +1,14 @@
|
||||
"""Entry-point standalone Pattern Matching 2D shape-based.
|
||||
"""Entry-point PM2D — webapp HTML.
|
||||
|
||||
Esegui: uv run python main.py
|
||||
Esegui locale: uv run python main.py (default 127.0.0.1:8080)
|
||||
Container: HOST=0.0.0.0 PORT=8080 python main.py
|
||||
"""
|
||||
from pathlib import Path
|
||||
import os
|
||||
|
||||
from pm2d.gui import run
|
||||
from pm2d.web.server import serve
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_dir = Path(__file__).parent / "Test"
|
||||
run(
|
||||
initial_dir=str(test_dir) if test_dir.is_dir() else None,
|
||||
angle_range_deg=(0.0, 360.0),
|
||||
angle_step_deg=5.0,
|
||||
scale_range=(1.0, 1.0),
|
||||
scale_step=0.1,
|
||||
num_features=96,
|
||||
weak_grad=30.0,
|
||||
strong_grad=60.0,
|
||||
spread_radius=5,
|
||||
pyramid_levels=3,
|
||||
min_score=0.55,
|
||||
max_matches=25,
|
||||
backend="line",
|
||||
)
|
||||
host = os.environ.get("HOST", "127.0.0.1")
|
||||
port = int(os.environ.get("PORT", "8080"))
|
||||
serve(host=host, port=port)
|
||||
|
||||
+588
-2
@@ -110,6 +110,385 @@ if HAS_NUMBA:
|
||||
acc[y, x] *= inv
|
||||
return acc
|
||||
|
||||
@nb.njit(cache=True, parallel=True, fastmath=True, boundscheck=False)
|
||||
def _jit_score_bitmap_rescored_strided(
|
||||
spread: np.ndarray,
|
||||
dx: np.ndarray, dy: np.ndarray, bins: np.ndarray,
|
||||
bit_active: np.uint8,
|
||||
bg: np.ndarray,
|
||||
stride: nb.int32,
|
||||
) -> np.ndarray:
|
||||
"""Variante con sub-sampling: valuta solo pixel su griglia stride×stride.
|
||||
Score restituito ha stessa shape (H, W); celle non valutate = 0.
|
||||
|
||||
4× speed-up con stride=2 (NMS recupera precisione in full-res).
|
||||
Numba prange richiede step costante: itero su indici griglia e
|
||||
moltiplico per stride dentro il body.
|
||||
"""
|
||||
H, W = spread.shape
|
||||
N = dx.shape[0]
|
||||
acc = np.zeros((H, W), dtype=np.float32)
|
||||
ny = (H + stride - 1) // stride
|
||||
nx = (W + stride - 1) // stride
|
||||
for yi in nb.prange(ny):
|
||||
y = yi * stride
|
||||
for i in range(N):
|
||||
b = bins[i]
|
||||
mask = np.uint8(1) << b
|
||||
if (bit_active & mask) == 0:
|
||||
continue
|
||||
ddy = dy[i]
|
||||
yy = y + ddy
|
||||
if yy < 0 or yy >= H:
|
||||
continue
|
||||
ddx = dx[i]
|
||||
x_lo = 0 if ddx >= 0 else -ddx
|
||||
x_hi = W if ddx <= 0 else W - ddx
|
||||
rem = x_lo % stride
|
||||
if rem != 0:
|
||||
x_lo += stride - rem
|
||||
x = x_lo
|
||||
while x < x_hi:
|
||||
if spread[yy, x + ddx] & mask:
|
||||
acc[y, x] += 1.0
|
||||
x += stride
|
||||
if N > 0:
|
||||
inv = 1.0 / N
|
||||
for yi in nb.prange(ny):
|
||||
y = yi * stride
|
||||
for xi in range(nx):
|
||||
x = xi * stride
|
||||
v = acc[y, x] * inv
|
||||
bgv = bg[y, x]
|
||||
if bgv < 1.0:
|
||||
r = (v - bgv) / (1.0 - bgv + 1e-6)
|
||||
acc[y, x] = r if r > 0.0 else 0.0
|
||||
else:
|
||||
acc[y, x] = 0.0
|
||||
return acc
|
||||
|
||||
@nb.njit(cache=True, parallel=True, fastmath=True, boundscheck=False)
|
||||
def _jit_score_bitmap_greedy(
|
||||
spread: np.ndarray,
|
||||
dx: np.ndarray, dy: np.ndarray, bins: np.ndarray,
|
||||
bit_active: np.uint8,
|
||||
min_score: nb.float32,
|
||||
greediness: nb.float32,
|
||||
) -> np.ndarray:
|
||||
"""Score bitmap con early-exit greedy (no rescore background).
|
||||
|
||||
Per ogni pixel iteriamo le N feature; abortiamo non appena diventa
|
||||
impossibile raggiungere `min_required` count anche aggiungendo
|
||||
tutte le feature rimanenti. min_required = greediness * min_score * N.
|
||||
|
||||
greediness=0 → nessun early-exit (equivalente a kernel base).
|
||||
greediness=1 → exit non appena hits + remaining < min_score * N.
|
||||
Tipico: 0.7-0.9 → 2-4x speed-up senza perdere match.
|
||||
"""
|
||||
H, W = spread.shape
|
||||
N = dx.shape[0]
|
||||
acc = np.zeros((H, W), dtype=np.float32)
|
||||
if N == 0:
|
||||
return acc
|
||||
min_req = greediness * min_score * N
|
||||
inv_N = nb.float32(1.0 / N)
|
||||
for y in nb.prange(H):
|
||||
for x in range(W):
|
||||
hits = 0
|
||||
for i in range(N):
|
||||
b = bins[i]
|
||||
mask = np.uint8(1) << b
|
||||
if (bit_active & mask) == 0:
|
||||
if hits + (N - i - 1) < min_req:
|
||||
break
|
||||
continue
|
||||
ddy = dy[i]
|
||||
yy = y + ddy
|
||||
if yy < 0 or yy >= H:
|
||||
if hits + (N - i - 1) < min_req:
|
||||
break
|
||||
continue
|
||||
ddx = dx[i]
|
||||
xx = x + ddx
|
||||
if xx < 0 or xx >= W:
|
||||
if hits + (N - i - 1) < min_req:
|
||||
break
|
||||
continue
|
||||
if spread[yy, xx] & mask:
|
||||
hits += 1
|
||||
else:
|
||||
if hits + (N - i - 1) < min_req:
|
||||
break
|
||||
acc[y, x] = nb.float32(hits) * inv_N
|
||||
return acc
|
||||
|
||||
@nb.njit(cache=True, parallel=True, fastmath=True, boundscheck=False)
|
||||
def _jit_score_bitmap_rescored(
|
||||
spread: np.ndarray, # uint8 (H, W)
|
||||
dx: np.ndarray, # int32 (N,)
|
||||
dy: np.ndarray, # int32 (N,)
|
||||
bins: np.ndarray, # int8 (N,)
|
||||
bit_active: np.uint8,
|
||||
bg: np.ndarray, # float32 (H, W) background density normalizzata
|
||||
) -> np.ndarray:
|
||||
"""score+rescore in un singolo pass: evita allocazione intermedia.
|
||||
|
||||
Equivalente a:
|
||||
score = _jit_score_bitmap(...)
|
||||
out = max(0, (score - bg) / (1 - bg + 1e-6))
|
||||
ma fonde la seconda passata dentro la normalizzazione finale
|
||||
(cache-friendly, risparmia ~15% sul totale find).
|
||||
"""
|
||||
H, W = spread.shape
|
||||
N = dx.shape[0]
|
||||
acc = np.zeros((H, W), dtype=np.float32)
|
||||
for y in nb.prange(H):
|
||||
for i in range(N):
|
||||
b = bins[i]
|
||||
mask = np.uint8(1) << b
|
||||
if (bit_active & mask) == 0:
|
||||
continue
|
||||
ddy = dy[i]
|
||||
yy = y + ddy
|
||||
if yy < 0 or yy >= H:
|
||||
continue
|
||||
ddx = dx[i]
|
||||
x_lo = 0 if ddx >= 0 else -ddx
|
||||
x_hi = W if ddx <= 0 else W - ddx
|
||||
for x in range(x_lo, x_hi):
|
||||
if spread[yy, x + ddx] & mask:
|
||||
acc[y, x] += 1.0
|
||||
if N > 0:
|
||||
inv = 1.0 / N
|
||||
for y in nb.prange(H):
|
||||
for x in range(W):
|
||||
v = acc[y, x] * inv
|
||||
bgv = bg[y, x]
|
||||
if bgv < 1.0:
|
||||
r = (v - bgv) / (1.0 - bgv + 1e-6)
|
||||
acc[y, x] = r if r > 0.0 else 0.0
|
||||
else:
|
||||
acc[y, x] = 0.0
|
||||
return acc
|
||||
|
||||
@nb.njit(cache=True, parallel=True, fastmath=True, boundscheck=False)
|
||||
def _jit_score_bitmap_rescored_window(
|
||||
spread: np.ndarray, # uint8 (H, W) - scena INTERA
|
||||
dx: np.ndarray, dy: np.ndarray, bins: np.ndarray,
|
||||
bit_active: np.uint8,
|
||||
bg: np.ndarray, # float32 (H, W) - scena intera
|
||||
y0: nb.int64, x0: nb.int64,
|
||||
wh: nb.int64, ww: nb.int64,
|
||||
) -> np.ndarray:
|
||||
"""Score rescored valutato SOLO nella finestra (y0, x0, wh, ww).
|
||||
|
||||
Le feature campionano lo spread dell'intera scena (bounds-checked
|
||||
sui bordi scena): a differenza di chiamare il kernel su un crop,
|
||||
le feature che escono dalla finestra NON contano come miss.
|
||||
Usato dal path pyramid_propagate: costo ∝ area finestra.
|
||||
"""
|
||||
H, W = spread.shape
|
||||
N = dx.shape[0]
|
||||
acc = np.zeros((wh, ww), dtype=np.float32)
|
||||
for yi in nb.prange(wh):
|
||||
y = y0 + yi
|
||||
for i in range(N):
|
||||
b = bins[i]
|
||||
mask = np.uint8(1) << b
|
||||
if (bit_active & mask) == 0:
|
||||
continue
|
||||
yy = y + dy[i]
|
||||
if yy < 0 or yy >= H:
|
||||
continue
|
||||
ddx = dx[i]
|
||||
xi_lo = 0
|
||||
xi_hi = ww
|
||||
lo = -(x0 + ddx)
|
||||
if lo > xi_lo:
|
||||
xi_lo = lo
|
||||
hi = W - (x0 + ddx)
|
||||
if hi < xi_hi:
|
||||
xi_hi = hi
|
||||
for xi in range(xi_lo, xi_hi):
|
||||
if spread[yy, x0 + xi + ddx] & mask:
|
||||
acc[yi, xi] += 1.0
|
||||
if N > 0:
|
||||
inv = 1.0 / N
|
||||
for yi in nb.prange(wh):
|
||||
for xi in range(ww):
|
||||
v = acc[yi, xi] * inv
|
||||
bgv = bg[y0 + yi, x0 + xi]
|
||||
if bgv < 1.0:
|
||||
r = (v - bgv) / (1.0 - bgv + 1e-6)
|
||||
acc[yi, xi] = r if r > 0.0 else 0.0
|
||||
else:
|
||||
acc[yi, xi] = 0.0
|
||||
return acc
|
||||
|
||||
@nb.njit(cache=True, parallel=True, fastmath=True, boundscheck=False)
|
||||
def _jit_score_bitmap_rescored_window_u16(
|
||||
spread: np.ndarray, # uint16 (H, W)
|
||||
dx: np.ndarray, dy: np.ndarray, bins: np.ndarray,
|
||||
bit_active: np.uint16,
|
||||
bg: np.ndarray,
|
||||
y0: nb.int64, x0: nb.int64,
|
||||
wh: nb.int64, ww: nb.int64,
|
||||
) -> np.ndarray:
|
||||
"""Versione uint16 (polarity 16-bin) del kernel windowed."""
|
||||
H, W = spread.shape
|
||||
N = dx.shape[0]
|
||||
acc = np.zeros((wh, ww), dtype=np.float32)
|
||||
for yi in nb.prange(wh):
|
||||
y = y0 + yi
|
||||
for i in range(N):
|
||||
b = bins[i]
|
||||
mask = np.uint16(1) << b
|
||||
if (bit_active & mask) == 0:
|
||||
continue
|
||||
yy = y + dy[i]
|
||||
if yy < 0 or yy >= H:
|
||||
continue
|
||||
ddx = dx[i]
|
||||
xi_lo = 0
|
||||
xi_hi = ww
|
||||
lo = -(x0 + ddx)
|
||||
if lo > xi_lo:
|
||||
xi_lo = lo
|
||||
hi = W - (x0 + ddx)
|
||||
if hi < xi_hi:
|
||||
xi_hi = hi
|
||||
for xi in range(xi_lo, xi_hi):
|
||||
if spread[yy, x0 + xi + ddx] & mask:
|
||||
acc[yi, xi] += 1.0
|
||||
if N > 0:
|
||||
inv = 1.0 / N
|
||||
for yi in nb.prange(wh):
|
||||
for xi in range(ww):
|
||||
v = acc[yi, xi] * inv
|
||||
bgv = bg[y0 + yi, x0 + xi]
|
||||
if bgv < 1.0:
|
||||
r = (v - bgv) / (1.0 - bgv + 1e-6)
|
||||
acc[yi, xi] = r if r > 0.0 else 0.0
|
||||
else:
|
||||
acc[yi, xi] = 0.0
|
||||
return acc
|
||||
|
||||
@nb.njit(cache=True, parallel=True, fastmath=True, boundscheck=False)
|
||||
def _jit_top_max_per_variant(
|
||||
spread: np.ndarray, # uint8 (H, W)
|
||||
dx_flat: np.ndarray, # int32 (sum_N,)
|
||||
dy_flat: np.ndarray, # int32 (sum_N,)
|
||||
bins_flat: np.ndarray, # int8 (sum_N,)
|
||||
offsets: np.ndarray, # int32 (n_vars+1,) prefix sum
|
||||
bit_active: np.uint8,
|
||||
bg_per_variant: np.ndarray, # float32 (n_vars, H, W) - 1 per scala
|
||||
scale_idx: np.ndarray, # int32 (n_vars,) idx in bg_per_variant
|
||||
) -> np.ndarray:
|
||||
"""Batch: per ogni variante calcola max score (rescored bg), ritorna
|
||||
array float32 (n_vars,). Parallelismo prange ESTERNO sulle varianti
|
||||
elimina overhead di n_vars chiamate JIT separate (avg ~20us per
|
||||
chiamata su template piccoli) + pool thread Python.
|
||||
|
||||
Pensato per fase TOP del pruning quando n_vars >> n_threads.
|
||||
"""
|
||||
n_vars = offsets.shape[0] - 1
|
||||
H, W = spread.shape
|
||||
out = np.zeros(n_vars, dtype=np.float32)
|
||||
for vi in nb.prange(n_vars):
|
||||
i0 = offsets[vi]; i1 = offsets[vi + 1]
|
||||
N = i1 - i0
|
||||
if N == 0:
|
||||
out[vi] = -1.0
|
||||
continue
|
||||
si = scale_idx[vi]
|
||||
inv = nb.float32(1.0 / N)
|
||||
best = nb.float32(-1.0)
|
||||
for y in range(H):
|
||||
for x in range(W):
|
||||
s = nb.float32(0.0)
|
||||
for k in range(N):
|
||||
b = bins_flat[i0 + k]
|
||||
mask = np.uint8(1) << b
|
||||
if (bit_active & mask) == 0:
|
||||
continue
|
||||
ddy = dy_flat[i0 + k]
|
||||
yy = y + ddy
|
||||
if yy < 0 or yy >= H:
|
||||
continue
|
||||
ddx = dx_flat[i0 + k]
|
||||
xx = x + ddx
|
||||
if xx < 0 or xx >= W:
|
||||
continue
|
||||
if spread[yy, xx] & mask:
|
||||
s += nb.float32(1.0)
|
||||
s *= inv
|
||||
bgv = bg_per_variant[si, y, x]
|
||||
if bgv < 1.0:
|
||||
r = (s - bgv) / (1.0 - bgv + 1e-6)
|
||||
if r > best:
|
||||
best = r
|
||||
out[vi] = best if best > 0.0 else 0.0
|
||||
return out
|
||||
|
||||
@nb.njit(cache=True, parallel=True, fastmath=True, boundscheck=False)
|
||||
def _jit_score_bitmap_rescored_u16(
|
||||
spread: np.ndarray, # uint16 (H, W) - 16 bit di polarity-aware
|
||||
dx: np.ndarray, dy: np.ndarray, bins: np.ndarray,
|
||||
bit_active: np.uint16,
|
||||
bg: np.ndarray,
|
||||
) -> np.ndarray:
|
||||
"""Versione uint16 di _jit_score_bitmap_rescored per polarity 16-bin.
|
||||
|
||||
Identica logica ma mask = uint16(1) << b dove b in [0..15]
|
||||
(orientamento mod 2π invece di mod π).
|
||||
"""
|
||||
H, W = spread.shape
|
||||
N = dx.shape[0]
|
||||
acc = np.zeros((H, W), dtype=np.float32)
|
||||
for y in nb.prange(H):
|
||||
for i in range(N):
|
||||
b = bins[i]
|
||||
mask = np.uint16(1) << b
|
||||
if (bit_active & mask) == 0:
|
||||
continue
|
||||
ddy = dy[i]
|
||||
yy = y + ddy
|
||||
if yy < 0 or yy >= H:
|
||||
continue
|
||||
ddx = dx[i]
|
||||
x_lo = 0 if ddx >= 0 else -ddx
|
||||
x_hi = W if ddx <= 0 else W - ddx
|
||||
for x in range(x_lo, x_hi):
|
||||
if spread[yy, x + ddx] & mask:
|
||||
acc[y, x] += 1.0
|
||||
if N > 0:
|
||||
inv = 1.0 / N
|
||||
for y in nb.prange(H):
|
||||
for x in range(W):
|
||||
v = acc[y, x] * inv
|
||||
bgv = bg[y, x]
|
||||
if bgv < 1.0:
|
||||
r = (v - bgv) / (1.0 - bgv + 1e-6)
|
||||
acc[y, x] = r if r > 0.0 else 0.0
|
||||
else:
|
||||
acc[y, x] = 0.0
|
||||
return acc
|
||||
|
||||
@nb.njit(cache=True, parallel=True, fastmath=True, boundscheck=False)
|
||||
def _jit_popcount_density_u16(spread: np.ndarray) -> np.ndarray:
|
||||
"""Popcount per uint16 (16 bin polarity)."""
|
||||
H, W = spread.shape
|
||||
out = np.zeros((H, W), dtype=np.float32)
|
||||
for y in nb.prange(H):
|
||||
for x in range(W):
|
||||
v = spread[y, x]
|
||||
cnt = 0
|
||||
for b in range(16):
|
||||
if v & (np.uint16(1) << b):
|
||||
cnt += 1
|
||||
out[y, x] = float(cnt)
|
||||
return out
|
||||
|
||||
@nb.njit(cache=True, parallel=True, fastmath=True, boundscheck=False)
|
||||
def _jit_popcount_density(spread: np.ndarray) -> np.ndarray:
|
||||
"""Conta bit set per pixel: ritorna (H, W) float32 in [0..8]."""
|
||||
@@ -134,7 +513,30 @@ if HAS_NUMBA:
|
||||
_jit_score_by_shift(resp, dx, dy, b, ba)
|
||||
spread = np.zeros((32, 32), dtype=np.uint8)
|
||||
_jit_score_bitmap(spread, dx, dy, b, np.uint8(0xFF))
|
||||
bg = np.zeros((32, 32), dtype=np.float32)
|
||||
_jit_score_bitmap_rescored(spread, dx, dy, b, np.uint8(0xFF), bg)
|
||||
_jit_score_bitmap_rescored_strided(
|
||||
spread, dx, dy, b, np.uint8(0xFF), bg, np.int32(2),
|
||||
)
|
||||
_jit_score_bitmap_greedy(
|
||||
spread, dx, dy, b, np.uint8(0xFF),
|
||||
np.float32(0.5), np.float32(0.8),
|
||||
)
|
||||
offsets = np.array([0, 1], dtype=np.int32)
|
||||
scale_idx = np.zeros(1, dtype=np.int32)
|
||||
bg_pv = np.zeros((1, 32, 32), dtype=np.float32)
|
||||
_jit_top_max_per_variant(
|
||||
spread, dx, dy, b, offsets, np.uint8(0xFF), bg_pv, scale_idx,
|
||||
)
|
||||
_jit_score_bitmap_rescored_window(
|
||||
spread, dx, dy, b, np.uint8(0xFF), bg, 4, 4, 8, 8,
|
||||
)
|
||||
_jit_popcount_density(spread)
|
||||
spread16 = np.zeros((32, 32), dtype=np.uint16)
|
||||
_jit_score_bitmap_rescored_u16(
|
||||
spread16, dx, dy, b, np.uint16(0xFFFF), bg,
|
||||
)
|
||||
_jit_popcount_density_u16(spread16)
|
||||
|
||||
else: # pragma: no cover
|
||||
|
||||
@@ -144,6 +546,33 @@ else: # pragma: no cover
|
||||
def _jit_score_bitmap(spread, dx, dy, bins, bit_active):
|
||||
raise RuntimeError("numba non disponibile")
|
||||
|
||||
def _jit_score_bitmap_rescored(spread, dx, dy, bins, bit_active, bg):
|
||||
raise RuntimeError("numba non disponibile")
|
||||
|
||||
def _jit_score_bitmap_rescored_strided(spread, dx, dy, bins, bit_active, bg, stride):
|
||||
raise RuntimeError("numba non disponibile")
|
||||
|
||||
def _jit_score_bitmap_rescored_window(spread, dx, dy, bins, bit_active, bg, y0, x0, wh, ww):
|
||||
raise RuntimeError("numba non disponibile")
|
||||
|
||||
def _jit_score_bitmap_rescored_window_u16(spread, dx, dy, bins, bit_active, bg, y0, x0, wh, ww):
|
||||
raise RuntimeError("numba non disponibile")
|
||||
|
||||
def _jit_score_bitmap_greedy(spread, dx, dy, bins, bit_active, min_score, greediness):
|
||||
raise RuntimeError("numba non disponibile")
|
||||
|
||||
def _jit_top_max_per_variant(
|
||||
spread, dx_flat, dy_flat, bins_flat, offsets, bit_active,
|
||||
bg_per_variant, scale_idx,
|
||||
):
|
||||
raise RuntimeError("numba non disponibile")
|
||||
|
||||
def _jit_score_bitmap_rescored_u16(spread, dx, dy, bins, bit_active, bg):
|
||||
raise RuntimeError("numba non disponibile")
|
||||
|
||||
def _jit_popcount_density_u16(spread):
|
||||
raise RuntimeError("numba non disponibile")
|
||||
|
||||
def _jit_popcount_density(spread):
|
||||
raise RuntimeError("numba non disponibile")
|
||||
|
||||
@@ -172,10 +601,167 @@ def score_bitmap(
|
||||
return _numpy_score_by_shift(resp, dx, dy, bins, None)
|
||||
|
||||
|
||||
def score_bitmap_rescored(
|
||||
spread: np.ndarray, dx: np.ndarray, dy: np.ndarray, bins: np.ndarray,
|
||||
bit_active: int, bg: np.ndarray, stride: int = 1,
|
||||
) -> np.ndarray:
|
||||
"""Score bitmap + rescore fusi in un solo pass (JIT).
|
||||
|
||||
Dispatch per dtype: uint16 → kernel polarity 16-bin, uint8 → kernel
|
||||
standard 8-bin (con eventuale stride > 1 per coarse top-level).
|
||||
"""
|
||||
if HAS_NUMBA and len(dx) > 0:
|
||||
dx_c = np.ascontiguousarray(dx, dtype=np.int32)
|
||||
dy_c = np.ascontiguousarray(dy, dtype=np.int32)
|
||||
bins_c = np.ascontiguousarray(bins, dtype=np.int8)
|
||||
bg_c = np.ascontiguousarray(bg, dtype=np.float32)
|
||||
if spread.dtype == np.uint16:
|
||||
spread_c = np.ascontiguousarray(spread, dtype=np.uint16)
|
||||
return _jit_score_bitmap_rescored_u16(
|
||||
spread_c, dx_c, dy_c, bins_c, np.uint16(bit_active), bg_c,
|
||||
)
|
||||
spread_c = np.ascontiguousarray(spread, dtype=np.uint8)
|
||||
if stride > 1:
|
||||
return _jit_score_bitmap_rescored_strided(
|
||||
spread_c, dx_c, dy_c, bins_c, np.uint8(bit_active), bg_c,
|
||||
np.int32(stride),
|
||||
)
|
||||
return _jit_score_bitmap_rescored(
|
||||
spread_c, dx_c, dy_c, bins_c, np.uint8(bit_active), bg_c,
|
||||
)
|
||||
# Fallback: chiamate separate (stride ignorato in fallback)
|
||||
score = score_bitmap(spread, dx, dy, bins, bit_active)
|
||||
out = (score - bg) / (1.0 - bg + 1e-6)
|
||||
return np.maximum(0.0, out).astype(np.float32)
|
||||
|
||||
|
||||
def score_bitmap_rescored_window(
|
||||
spread: np.ndarray, dx: np.ndarray, dy: np.ndarray, bins: np.ndarray,
|
||||
bit_active: int, bg: np.ndarray,
|
||||
y0: int, x0: int, wh: int, ww: int,
|
||||
) -> np.ndarray:
|
||||
"""Score rescored solo nella finestra (y0, x0, wh, ww) della scena.
|
||||
|
||||
Le feature campionano l'INTERA scena: feature fuori finestra ma dentro
|
||||
scena contano correttamente (chiamare il kernel su un crop le tratta
|
||||
come miss e azzera lo score — il bug che rendeva inutilizzabile il
|
||||
path pyramid_propagate). Fallback no-numba: kernel pieno + slice.
|
||||
"""
|
||||
if HAS_NUMBA and len(dx) > 0:
|
||||
dx_c = np.ascontiguousarray(dx, dtype=np.int32)
|
||||
dy_c = np.ascontiguousarray(dy, dtype=np.int32)
|
||||
bins_c = np.ascontiguousarray(bins, dtype=np.int8)
|
||||
bg_c = np.ascontiguousarray(bg, dtype=np.float32)
|
||||
if spread.dtype == np.uint16:
|
||||
return _jit_score_bitmap_rescored_window_u16(
|
||||
np.ascontiguousarray(spread, dtype=np.uint16),
|
||||
dx_c, dy_c, bins_c, np.uint16(bit_active), bg_c,
|
||||
int(y0), int(x0), int(wh), int(ww),
|
||||
)
|
||||
return _jit_score_bitmap_rescored_window(
|
||||
np.ascontiguousarray(spread, dtype=np.uint8),
|
||||
dx_c, dy_c, bins_c, np.uint8(bit_active), bg_c,
|
||||
int(y0), int(x0), int(wh), int(ww),
|
||||
)
|
||||
# Fallback (lento, solo senza numba): score full-frame + slice finestra
|
||||
full = score_bitmap_rescored(spread, dx, dy, bins, bit_active, bg)
|
||||
return full[y0:y0 + wh, x0:x0 + ww]
|
||||
|
||||
|
||||
def score_bitmap_greedy(
|
||||
spread: np.ndarray, dx: np.ndarray, dy: np.ndarray, bins: np.ndarray,
|
||||
bit_active: int, min_score: float, greediness: float,
|
||||
) -> np.ndarray:
|
||||
"""Score bitmap con early-exit greedy. Per coarse-pass aggressivo.
|
||||
|
||||
Non applica rescore background: usare quando la scena ha basso clutter
|
||||
o quando si vuole mass-prune varianti via top-level rapidamente.
|
||||
"""
|
||||
if HAS_NUMBA and len(dx) > 0:
|
||||
return _jit_score_bitmap_greedy(
|
||||
np.ascontiguousarray(spread, dtype=np.uint8),
|
||||
np.ascontiguousarray(dx, dtype=np.int32),
|
||||
np.ascontiguousarray(dy, dtype=np.int32),
|
||||
np.ascontiguousarray(bins, dtype=np.int8),
|
||||
np.uint8(bit_active),
|
||||
np.float32(min_score), np.float32(greediness),
|
||||
)
|
||||
# Fallback: kernel base senza early-exit
|
||||
return score_bitmap(spread, dx, dy, bins, bit_active)
|
||||
|
||||
|
||||
def top_max_per_variant(
|
||||
spread: np.ndarray,
|
||||
dx_list: list, dy_list: list, bin_list: list,
|
||||
bg_per_scale: dict,
|
||||
variant_scales: list,
|
||||
bit_active: int,
|
||||
) -> np.ndarray:
|
||||
"""Wrapper: prepara buffer flat e chiama kernel batch su tutte le varianti.
|
||||
|
||||
Parallelismo Numba prange-esterno sulle varianti (n_vars >> n_threads
|
||||
tipicamente per top-pruning) → meglio del thread-pool Python che paga
|
||||
overhead di n_vars chiamate JIT separate.
|
||||
"""
|
||||
if not HAS_NUMBA or len(dx_list) == 0:
|
||||
return np.array([], dtype=np.float32)
|
||||
n_vars = len(dx_list)
|
||||
sizes = [len(d) for d in dx_list]
|
||||
offsets = np.zeros(n_vars + 1, dtype=np.int32)
|
||||
offsets[1:] = np.cumsum(sizes)
|
||||
total = int(offsets[-1])
|
||||
dx_flat = np.empty(total, dtype=np.int32)
|
||||
dy_flat = np.empty(total, dtype=np.int32)
|
||||
bins_flat = np.empty(total, dtype=np.int8)
|
||||
for vi, (dx, dy, bn) in enumerate(zip(dx_list, dy_list, bin_list)):
|
||||
i0 = int(offsets[vi]); i1 = int(offsets[vi + 1])
|
||||
dx_flat[i0:i1] = dx
|
||||
dy_flat[i0:i1] = dy
|
||||
bins_flat[i0:i1] = bn
|
||||
# bg per variante: indicizzato per scala
|
||||
scales_unique = sorted(bg_per_scale.keys())
|
||||
scale_to_idx = {s: i for i, s in enumerate(scales_unique)}
|
||||
H, W = spread.shape
|
||||
bg_pv = np.empty((len(scales_unique), H, W), dtype=np.float32)
|
||||
for s, idx in scale_to_idx.items():
|
||||
bg_pv[idx] = bg_per_scale[s]
|
||||
scale_idx = np.array(
|
||||
[scale_to_idx[s] for s in variant_scales], dtype=np.int32,
|
||||
)
|
||||
return _jit_top_max_per_variant(
|
||||
np.ascontiguousarray(spread, dtype=np.uint8),
|
||||
dx_flat, dy_flat, bins_flat, offsets, np.uint8(bit_active),
|
||||
bg_pv, scale_idx,
|
||||
)
|
||||
|
||||
|
||||
_HAS_NP_BITCOUNT = hasattr(np, "bitwise_count")
|
||||
|
||||
|
||||
def popcount_density(spread: np.ndarray) -> np.ndarray:
|
||||
"""Conta bit set per pixel.
|
||||
|
||||
Order:
|
||||
1) Numba JIT parallel (preferito: piu veloce su 1080p, 0.5ms vs 1.6ms)
|
||||
2) numpy.bitwise_count (NumPy 2.0+, SIMD ma single-thread)
|
||||
3) Fallback numpy bit-shift puro
|
||||
"""
|
||||
if spread.dtype == np.uint16:
|
||||
spread_c = np.ascontiguousarray(spread, dtype=np.uint16)
|
||||
if HAS_NUMBA:
|
||||
return _jit_popcount_density_u16(spread_c)
|
||||
if _HAS_NP_BITCOUNT:
|
||||
return np.bitwise_count(spread_c).astype(np.float32, copy=False)
|
||||
H, W = spread_c.shape
|
||||
out = np.zeros((H, W), dtype=np.float32)
|
||||
for b in range(16):
|
||||
out += ((spread_c >> b) & 1).astype(np.float32)
|
||||
return out
|
||||
spread_c = np.ascontiguousarray(spread, dtype=np.uint8)
|
||||
if HAS_NUMBA:
|
||||
return _jit_popcount_density(np.ascontiguousarray(spread, dtype=np.uint8))
|
||||
# Fallback
|
||||
return _jit_popcount_density(spread_c)
|
||||
if _HAS_NP_BITCOUNT:
|
||||
return np.bitwise_count(spread_c).astype(np.float32, copy=False)
|
||||
H, W = spread.shape
|
||||
out = np.zeros((H, W), dtype=np.float32)
|
||||
for b in range(8):
|
||||
|
||||
+200
-18
@@ -14,6 +14,9 @@ Ritorna dict con i key esatti del form `edit_params`.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
from collections import OrderedDict
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
|
||||
@@ -24,17 +27,33 @@ def _to_gray(img: np.ndarray) -> np.ndarray:
|
||||
return img
|
||||
|
||||
|
||||
# Cache in-memory (LRU) dei risultati auto_tune per stesso input ROI.
|
||||
_TUNE_CACHE: OrderedDict[str, dict] = OrderedDict()
|
||||
_TUNE_CACHE_SIZE = 32
|
||||
|
||||
|
||||
def detect_rotational_symmetry(
|
||||
gray: np.ndarray, step_deg: float = 5.0, corr_thresh: float = 0.75,
|
||||
) -> dict:
|
||||
"""Rileva simmetria rotazionale su edge map (più robusto a sfondo uniforme).
|
||||
|
||||
Downsample a max 128 px prima di correlare per abbattere il costo
|
||||
O(n_angles · H · W) senza perdere precisione (la simmetria rotazionale
|
||||
è invariante a subsampling moderato).
|
||||
|
||||
Ritorna dict con:
|
||||
- order: int, 1=nessuna, 2=180°, 3=120°, 4=90°, 6=60°, 8=45°
|
||||
- period_deg: float, periodo minimo di simmetria (360/order)
|
||||
- confidence: float [0..1], correlazione minima tra rotazioni equivalenti
|
||||
"""
|
||||
h, w = gray.shape
|
||||
target = 128
|
||||
if max(h, w) > target:
|
||||
sf = target / max(h, w)
|
||||
new_w = max(32, int(w * sf))
|
||||
new_h = max(32, int(h * sf))
|
||||
gray = cv2.resize(gray, (new_w, new_h), interpolation=cv2.INTER_AREA)
|
||||
h, w = gray.shape
|
||||
# Usa magnitude gradiente (rotation-invariant rispetto a bg uniforme)
|
||||
gx = cv2.Sobel(gray, cv2.CV_32F, 1, 0, ksize=3)
|
||||
gy = cv2.Sobel(gray, cv2.CV_32F, 0, 1, ksize=3)
|
||||
@@ -42,6 +61,8 @@ def detect_rotational_symmetry(
|
||||
|
||||
center = (w / 2.0, h / 2.0)
|
||||
ref = mag
|
||||
# ref è costante nel loop sugli angoli: centra una volta sola
|
||||
rm = ref - ref.mean()
|
||||
|
||||
correlations: list[tuple[float, float]] = []
|
||||
for ang in np.arange(step_deg, 360.0, step_deg):
|
||||
@@ -49,7 +70,6 @@ def detect_rotational_symmetry(
|
||||
rot = cv2.warpAffine(
|
||||
mag, M, (w, h), borderValue=0.0,
|
||||
)
|
||||
rm = ref - ref.mean()
|
||||
rs = rot - rot.mean()
|
||||
denom = np.sqrt((rm * rm).sum() * (rs * rs).sum()) + 1e-9
|
||||
c = float((rm * rs).sum() / denom)
|
||||
@@ -88,9 +108,12 @@ def analyze_gradients(gray: np.ndarray) -> dict:
|
||||
gy = cv2.Sobel(gray, cv2.CV_32F, 0, 1, ksize=3)
|
||||
mag = cv2.magnitude(gx, gy)
|
||||
|
||||
# Percentili magnitude
|
||||
# Percentili magnitude: p55/p85 usati per soglie weak/strong (più aderenti
|
||||
# alla distribuzione reale rispetto a p50/p80 + clamp).
|
||||
p50 = float(np.percentile(mag, 50))
|
||||
p55 = float(np.percentile(mag, 55))
|
||||
p80 = float(np.percentile(mag, 80))
|
||||
p85 = float(np.percentile(mag, 85))
|
||||
p95 = float(np.percentile(mag, 95))
|
||||
mag_max = float(mag.max())
|
||||
|
||||
@@ -112,7 +135,8 @@ def analyze_gradients(gray: np.ndarray) -> dict:
|
||||
ent = 0.0
|
||||
|
||||
return {
|
||||
"p50": p50, "p80": p80, "p95": p95, "mag_max": mag_max,
|
||||
"p50": p50, "p55": p55, "p80": p80, "p85": p85, "p95": p95,
|
||||
"mag_max": mag_max,
|
||||
"strong_pct": strong_pct, "weak_pct": weak_pct,
|
||||
"orient_entropy": ent,
|
||||
"n_pixels": mag.size,
|
||||
@@ -120,11 +144,138 @@ def analyze_gradients(gray: np.ndarray) -> dict:
|
||||
}
|
||||
|
||||
|
||||
def auto_tune(template_bgr: np.ndarray, mask: np.ndarray | None = None) -> dict:
|
||||
def _cache_key(template_bgr: np.ndarray, mask: np.ndarray | None) -> str:
|
||||
h = hashlib.md5()
|
||||
h.update(np.ascontiguousarray(template_bgr).tobytes())
|
||||
h.update(f"shape={template_bgr.shape}".encode())
|
||||
if mask is not None:
|
||||
h.update(np.ascontiguousarray(mask).tobytes())
|
||||
return h.hexdigest()
|
||||
|
||||
|
||||
def _self_validate(template_bgr: np.ndarray, params: dict,
|
||||
mask: np.ndarray | None = None) -> dict:
|
||||
"""Halcon-style self-validation: train il matcher coi parametri tentativi
|
||||
e verifica che il template stesso sia trovato con recall ≥ 1.0.
|
||||
|
||||
Se recall < target o score basso, regola i parametri:
|
||||
- alza weak_grad se troppi edge spuri (recall solido ma molti picchi falsi)
|
||||
- abbassa strong_grad se troppe feature scartate (low feature count)
|
||||
- riduce pyramid_levels se variants[0].levels[top] ha <8 feature
|
||||
|
||||
Halcon usa internamente questo loop in inspect_shape_model. Costo: 1
|
||||
train + 1 find sul template (~50ms su template 100x100). Ne vale la
|
||||
pena se evita match-time errors su scene reali.
|
||||
|
||||
Mutates `params` in place e ritorna lo stesso dict per chaining.
|
||||
"""
|
||||
# Import lazy: evita ciclo (line_matcher importa nulla da auto_tune)
|
||||
from pm2d.line_matcher import LineShapeMatcher
|
||||
|
||||
# Caso degenerato: troppe poche feature pre-validation → riduci soglia
|
||||
if params.get("_n_strong_pixels", 0) < 30:
|
||||
params["weak_grad"] = max(15.0, params["weak_grad"] * 0.6)
|
||||
params["strong_grad"] = max(30.0, params["strong_grad"] * 0.6)
|
||||
|
||||
# Train minimale: 1 sola pose orientazione 0 (range degenerato che
|
||||
# produce comunque 1 variante via fallback in _angle_list).
|
||||
m = LineShapeMatcher(
|
||||
num_features=params["num_features"],
|
||||
weak_grad=params["weak_grad"],
|
||||
strong_grad=params["strong_grad"],
|
||||
angle_range_deg=(0.0, 0.0), # fallback _angle_list = [0.0]
|
||||
angle_step_deg=10.0,
|
||||
scale_range=(1.0, 1.0),
|
||||
spread_radius=params["spread_radius"],
|
||||
pyramid_levels=params["pyramid_levels"],
|
||||
)
|
||||
n_var = m.train(template_bgr, mask=mask)
|
||||
if n_var == 0:
|
||||
# Soglie troppo alte: nessuna variante generata → dimezza
|
||||
params["weak_grad"] = max(15.0, params["weak_grad"] * 0.5)
|
||||
params["strong_grad"] = max(30.0, params["strong_grad"] * 0.5)
|
||||
params["_validation"] = "fallback: soglie dimezzate (no variants)"
|
||||
return params
|
||||
|
||||
# Verifica densita' feature al top-level (rischio collasso)
|
||||
top_lvl = m.variants[0].levels[-1]
|
||||
if top_lvl.n < 8 and params["pyramid_levels"] > 1:
|
||||
params["pyramid_levels"] = max(1, params["pyramid_levels"] - 1)
|
||||
params["_validation"] = (
|
||||
f"pyramid_levels ridotto a {params['pyramid_levels']} "
|
||||
f"(top aveva {top_lvl.n} feature)"
|
||||
)
|
||||
return params
|
||||
|
||||
# Self-find: cerca il template stesso nella propria immagine
|
||||
h, w = template_bgr.shape[:2]
|
||||
# Embed template in scena leggermente più grande per evitare bordo
|
||||
pad = 20
|
||||
canvas = np.full(
|
||||
(h + 2 * pad, w + 2 * pad, 3 if template_bgr.ndim == 3 else 1),
|
||||
128, dtype=np.uint8,
|
||||
)
|
||||
canvas[pad:pad + h, pad:pad + w] = template_bgr
|
||||
matches = m.find(
|
||||
canvas, min_score=0.3, max_matches=5,
|
||||
verify_ncc=False, # template stesso → NCC = 1 sempre, skip per velocita'
|
||||
refine_angle=False, subpixel=False,
|
||||
nms_iou_threshold=0.3,
|
||||
)
|
||||
if not matches:
|
||||
# Nessun match sul proprio template: parametri troppo restrittivi
|
||||
params["weak_grad"] = max(15.0, params["weak_grad"] * 0.7)
|
||||
params["strong_grad"] = max(30.0, params["strong_grad"] * 0.7)
|
||||
params["num_features"] = max(48, int(params["num_features"] * 0.8))
|
||||
params["_validation"] = "soglie/feature ridotte (no self-match)"
|
||||
return params
|
||||
|
||||
# Misura score top match
|
||||
top_score = float(matches[0].score)
|
||||
params["_self_score"] = round(top_score, 3)
|
||||
if top_score < 0.7:
|
||||
# Score basso sul template stesso = parametri davvero subottimali
|
||||
params["weak_grad"] = max(15.0, params["weak_grad"] * 0.85)
|
||||
params["_validation"] = (
|
||||
f"weak_grad ridotto (self-score era {top_score:.2f})"
|
||||
)
|
||||
else:
|
||||
params["_validation"] = f"OK (self-score {top_score:.2f})"
|
||||
return params
|
||||
|
||||
|
||||
def auto_tune(
|
||||
template_bgr: np.ndarray,
|
||||
mask: np.ndarray | None = None,
|
||||
angle_tolerance_deg: float | None = None,
|
||||
angle_center_deg: float = 0.0,
|
||||
self_validate: bool = True,
|
||||
) -> dict:
|
||||
"""Analizza template e ritorna dict parametri suggeriti.
|
||||
|
||||
Chiavi compatibili con edit_params PARAM_SCHEMA.
|
||||
|
||||
angle_tolerance_deg: se != None, restringe angle_range a
|
||||
(center - tol, center + tol). Usare quando l'orientamento del
|
||||
pezzo e' noto a priori (feeder con guida, posizionamento
|
||||
meccanico): training molto piu rapido (24x meno varianti per
|
||||
tol=15° vs 360° pieno).
|
||||
|
||||
self_validate: se True (default), dopo la stima dei parametri
|
||||
esegue un dry-run del matching sul template stesso e regola
|
||||
weak_grad/strong_grad/pyramid_levels se i parametri tentativi
|
||||
non garantiscono auto-match (Halcon-style inspect_shape_model).
|
||||
|
||||
Risultato cachato in-memory (LRU): ri-chiamare con stessa ROI è O(1).
|
||||
"""
|
||||
ck = _cache_key(template_bgr, mask)
|
||||
if angle_tolerance_deg is not None:
|
||||
ck = f"{ck}|tol={angle_tolerance_deg}|c={angle_center_deg}"
|
||||
cached = _TUNE_CACHE.get(ck)
|
||||
if cached is not None:
|
||||
_TUNE_CACHE.move_to_end(ck)
|
||||
return dict(cached)
|
||||
|
||||
gray = _to_gray(template_bgr)
|
||||
h, w = gray.shape
|
||||
if mask is not None:
|
||||
@@ -136,16 +287,22 @@ def auto_tune(template_bgr: np.ndarray, mask: np.ndarray | None = None) -> dict:
|
||||
stats = analyze_gradients(gray_for_stats)
|
||||
sym = detect_rotational_symmetry(gray_for_stats)
|
||||
|
||||
# Soglie magnitude: usa percentili per robustezza illuminazione.
|
||||
# Target: strong_grad ~= valore a percentile 80-90 in assoluto, ma
|
||||
# clamp per compatibilità uint8 (Sobel può sforare).
|
||||
strong_grad = float(np.clip(stats["p80"], 20.0, 100.0))
|
||||
weak_grad = float(np.clip(strong_grad * 0.5, 10.0, 60.0))
|
||||
# Soglie magnitude: usa percentili reali (p85/p55) senza clamp duro a 100.
|
||||
# Sobel ksize=3 su uint8 può arrivare a ~1020, quindi clamp massimo 400
|
||||
# evita saturazione del threshold su template ad alto contrasto.
|
||||
strong_grad = float(np.clip(stats["p85"], 30.0, 400.0))
|
||||
weak_grad = float(np.clip(stats["p55"], 15.0, strong_grad * 0.7))
|
||||
|
||||
# num_features: 1 feature ogni ~25 px forti, clamp 48..192
|
||||
target_feat = int(np.clip(stats["n_strong"] / 25, 48, 192))
|
||||
# num_features: ibrido perimetro + densità. Target = min(perimeter_budget,
|
||||
# density_budget) per non generare più feature di quante edge nitide siano
|
||||
# disponibili, ma neanche meno di quante il perimetro possa tracciare.
|
||||
perim_budget = int(2 * (h + w) * 0.4) # ~40% dei pixel di perimetro
|
||||
density_budget = int(stats["n_strong"] / 20) # 1 feature ogni ~20 px forti
|
||||
target_feat = int(np.clip(min(perim_budget, density_budget), 64, 192))
|
||||
|
||||
# pyramid_levels in base alla dimensione minima
|
||||
# pyramid_levels in base a dimensione minima E densità feature: un template
|
||||
# grande ma povero di feature non deve scendere troppi livelli (rischio
|
||||
# collasso a <16 feature al top level).
|
||||
min_side = min(h, w)
|
||||
if min_side < 60:
|
||||
pyr = 1
|
||||
@@ -155,12 +312,20 @@ def auto_tune(template_bgr: np.ndarray, mask: np.ndarray | None = None) -> dict:
|
||||
pyr = 3
|
||||
else:
|
||||
pyr = 4
|
||||
# Cap: non scendere sotto ~16 feature al top level (feature ÷ 4^(pyr-1))
|
||||
max_pyr_from_feat = max(1, int(np.floor(np.log2(max(1, target_feat / 16.0)) / 2.0)) + 1)
|
||||
pyr = min(pyr, max_pyr_from_feat)
|
||||
|
||||
# spread_radius proporzionale a risoluzione + pyramid (tolleranza ~1% dim)
|
||||
spread_radius = int(np.clip(max(3, min_side * 0.02), 3, 8))
|
||||
|
||||
# angle range ridotto se simmetria rotazionale
|
||||
angle_max = 360.0 / sym["order"] if sym["order"] > 1 else 360.0
|
||||
# angle range: priorita' a tolerance hint utente, poi simmetria rotazionale.
|
||||
if angle_tolerance_deg is not None:
|
||||
angle_min = float(angle_center_deg - angle_tolerance_deg)
|
||||
angle_max = float(angle_center_deg + angle_tolerance_deg)
|
||||
else:
|
||||
angle_min = 0.0
|
||||
angle_max = 360.0 / sym["order"] if sym["order"] > 1 else 360.0
|
||||
|
||||
# min_score: se entropia orient alta → template distintivo → soglia alta ok
|
||||
# se entropia bassa → template ambiguo → soglia più permissiva
|
||||
@@ -171,12 +336,15 @@ def auto_tune(template_bgr: np.ndarray, mask: np.ndarray | None = None) -> dict:
|
||||
else:
|
||||
min_score = 0.45
|
||||
|
||||
# angle step: 5° default; se simmetria, mantengo step ma range ridotto
|
||||
angle_step = 5.0
|
||||
# angle step adattivo (Halcon-style): atan(2/max_side) deg, clampato.
|
||||
# Template grande → step fine (rotazione minima visibile su perimetro).
|
||||
# Template piccolo → step grosso (over-sampling = sprecato).
|
||||
max_side = max(h, w)
|
||||
angle_step = float(np.clip(np.degrees(np.arctan2(2.0, max_side)), 1.0, 8.0))
|
||||
|
||||
return {
|
||||
result = {
|
||||
"backend": "line",
|
||||
"angle_min": 0.0,
|
||||
"angle_min": angle_min,
|
||||
"angle_max": angle_max,
|
||||
"angle_step": angle_step,
|
||||
"scale_min": 1.0,
|
||||
@@ -195,7 +363,21 @@ def auto_tune(template_bgr: np.ndarray, mask: np.ndarray | None = None) -> dict:
|
||||
"_symmetry_order": sym["order"],
|
||||
"_symmetry_conf": round(sym["confidence"], 2),
|
||||
"_orient_entropy": round(stats["orient_entropy"], 2),
|
||||
"_n_strong_pixels": stats["n_strong"],
|
||||
}
|
||||
# Halcon-style self-validation: dry-run training+find sul template per
|
||||
# auto-correggere parametri tentativi che non garantirebbero match.
|
||||
if self_validate:
|
||||
result = _self_validate(template_bgr, result, mask=mask)
|
||||
# Round numerici dopo eventuali aggiustamenti
|
||||
result["weak_grad"] = round(result["weak_grad"], 1)
|
||||
result["strong_grad"] = round(result["strong_grad"], 1)
|
||||
# Store in LRU cache
|
||||
_TUNE_CACHE[ck] = dict(result)
|
||||
_TUNE_CACHE.move_to_end(ck)
|
||||
while len(_TUNE_CACHE) > _TUNE_CACHE_SIZE:
|
||||
_TUNE_CACHE.popitem(last=False)
|
||||
return result
|
||||
|
||||
|
||||
def summarize(tune: dict) -> str:
|
||||
|
||||
+179
@@ -0,0 +1,179 @@
|
||||
"""Benchmark suite per LineShapeMatcher.
|
||||
|
||||
Usage:
|
||||
python -m pm2d.bench [--quick]
|
||||
|
||||
Misura tempi find() su 3 template-tipo × 3 scene-tipo × N config:
|
||||
- Template: rettangolo 80×80, L-shape 120×120, cerchio 150×150
|
||||
- Scene: pulita 800×600, cluttered 1080×1920, multi-pezzo 1080×1920
|
||||
- Config: baseline, polarity, gpu, pyramid_propagate, greediness=0.7
|
||||
|
||||
Per ogni config stampa: ms/find, ms per fase (profile), n. match.
|
||||
Output tabellare per detectare regressioni in CI.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import time
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
|
||||
from pm2d.line_matcher import LineShapeMatcher, opencl_available
|
||||
|
||||
|
||||
# ---------- Sintetizzatori template/scena ----------
|
||||
|
||||
def _tpl_rect() -> np.ndarray:
|
||||
t = np.zeros((80, 80, 3), np.uint8)
|
||||
cv2.rectangle(t, (15, 15), (65, 65), (255, 255, 255), 3)
|
||||
return t
|
||||
|
||||
|
||||
def _tpl_lshape() -> np.ndarray:
|
||||
t = np.zeros((120, 120, 3), np.uint8)
|
||||
cv2.rectangle(t, (20, 20), (50, 100), (255, 255, 255), -1)
|
||||
cv2.rectangle(t, (20, 70), (100, 100), (255, 255, 255), -1)
|
||||
return t
|
||||
|
||||
|
||||
def _tpl_circle() -> np.ndarray:
|
||||
t = np.zeros((150, 150, 3), np.uint8)
|
||||
cv2.circle(t, (75, 75), 60, (255, 255, 255), 4)
|
||||
return t
|
||||
|
||||
|
||||
def _scene_clean(W: int, H: int, n_pieces: int = 1) -> np.ndarray:
|
||||
np.random.seed(0)
|
||||
s = np.zeros((H, W, 3), np.uint8)
|
||||
for _ in range(n_pieces):
|
||||
cx = np.random.randint(80, W - 80)
|
||||
cy = np.random.randint(80, H - 80)
|
||||
cv2.rectangle(s, (cx - 25, cy - 25), (cx + 25, cy + 25), (255, 255, 255), 3)
|
||||
return s
|
||||
|
||||
|
||||
def _scene_cluttered(W: int, H: int) -> np.ndarray:
|
||||
np.random.seed(0)
|
||||
s = np.random.randint(50, 200, (H, W, 3), np.uint8)
|
||||
cv2.rectangle(s, (300, 200), (350, 250), (255, 255, 255), 3)
|
||||
cv2.rectangle(s, (1500, 800), (1550, 850), (255, 255, 255), 3)
|
||||
return s
|
||||
|
||||
|
||||
# ---------- Single benchmark ----------
|
||||
|
||||
def _bench_config(template, scene, config_name: str,
|
||||
init_kw: dict, find_kw: dict,
|
||||
n_iter: int = 5) -> dict:
|
||||
m = LineShapeMatcher(**init_kw)
|
||||
t0 = time.perf_counter()
|
||||
n_var = m.train(template)
|
||||
t_train = time.perf_counter() - t0
|
||||
|
||||
# Warmup (Numba JIT)
|
||||
m.find(scene, **find_kw)
|
||||
m.find(scene, **find_kw)
|
||||
|
||||
# Run
|
||||
times_ms = []
|
||||
for _ in range(n_iter):
|
||||
t0 = time.perf_counter()
|
||||
matches = m.find(scene, **find_kw)
|
||||
times_ms.append((time.perf_counter() - t0) * 1000.0)
|
||||
|
||||
# Profile (1 iter)
|
||||
m.find(scene, profile=True, **find_kw)
|
||||
prof = m.get_last_profile() or {}
|
||||
|
||||
return {
|
||||
"config": config_name,
|
||||
"n_variants": n_var,
|
||||
"t_train_s": round(t_train, 3),
|
||||
"ms_avg": round(float(np.mean(times_ms)), 1),
|
||||
"ms_min": round(float(np.min(times_ms)), 1),
|
||||
"ms_max": round(float(np.max(times_ms)), 1),
|
||||
"n_matches": len(matches),
|
||||
"profile_ms": {k: round(v, 1) for k, v in prof.items()},
|
||||
}
|
||||
|
||||
|
||||
# ---------- Suite ----------
|
||||
|
||||
CONFIGS = [
|
||||
("baseline",
|
||||
{"angle_step_deg": 10, "pyramid_levels": 2},
|
||||
{"min_score": 0.4, "verify_threshold": 0.2}),
|
||||
("polarity",
|
||||
{"angle_step_deg": 10, "pyramid_levels": 2, "use_polarity": True},
|
||||
{"min_score": 0.4, "verify_threshold": 0.2}),
|
||||
("propagate",
|
||||
{"angle_step_deg": 10, "pyramid_levels": 3},
|
||||
{"min_score": 0.4, "verify_threshold": 0.2,
|
||||
"pyramid_propagate": True, "propagate_topk": 4}),
|
||||
("greedy_07",
|
||||
{"angle_step_deg": 10, "pyramid_levels": 2},
|
||||
{"min_score": 0.4, "verify_threshold": 0.2, "greediness": 0.7}),
|
||||
("stride2",
|
||||
{"angle_step_deg": 10, "pyramid_levels": 2},
|
||||
{"min_score": 0.4, "verify_threshold": 0.2, "coarse_stride": 2}),
|
||||
]
|
||||
|
||||
if opencl_available():
|
||||
CONFIGS.append(
|
||||
("gpu_umat",
|
||||
{"angle_step_deg": 10, "pyramid_levels": 2, "use_gpu": True},
|
||||
{"min_score": 0.4, "verify_threshold": 0.2})
|
||||
)
|
||||
|
||||
|
||||
SCENARIOS = [
|
||||
("rect_80 vs scene_800x600", _tpl_rect, lambda: _scene_clean(800, 600, 1)),
|
||||
("lshape_120 vs scene_1080x1920_clutter",
|
||||
_tpl_lshape, lambda: _scene_cluttered(1920, 1080)),
|
||||
("circle_150 vs scene_clean_3pieces",
|
||||
_tpl_circle, lambda: _scene_clean(1920, 1080, 3)),
|
||||
]
|
||||
|
||||
|
||||
def run(quick: bool = False) -> int:
|
||||
n_iter = 2 if quick else 5
|
||||
print(f"=== PM2D Benchmark Suite ({len(SCENARIOS)} scenarios x "
|
||||
f"{len(CONFIGS)} configs, n_iter={n_iter}) ===\n")
|
||||
rows = []
|
||||
for sc_name, tpl_fn, scn_fn in SCENARIOS:
|
||||
template = tpl_fn()
|
||||
scene = scn_fn()
|
||||
print(f"--- Scenario: {sc_name} (tpl={template.shape}, "
|
||||
f"scn={scene.shape}) ---")
|
||||
for cfg_name, init_kw, find_kw in CONFIGS:
|
||||
r = _bench_config(template, scene, cfg_name, init_kw, find_kw,
|
||||
n_iter=n_iter)
|
||||
r["scenario"] = sc_name
|
||||
rows.append(r)
|
||||
prof_str = " ".join(
|
||||
f"{k}={v:.1f}" for k, v in r["profile_ms"].items()
|
||||
)
|
||||
print(f" {cfg_name:14s} {r['ms_avg']:6.1f}ms "
|
||||
f"(min {r['ms_min']:.1f} max {r['ms_max']:.1f}) "
|
||||
f"vars={r['n_variants']:3d} "
|
||||
f"matches={r['n_matches']:2d}")
|
||||
if prof_str:
|
||||
print(f" profile: {prof_str}")
|
||||
print()
|
||||
print("=== Done ===")
|
||||
return 0
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
p = argparse.ArgumentParser(description="PM2D benchmark suite")
|
||||
p.add_argument("--quick", action="store_true",
|
||||
help="2 iterazioni per config invece di 5 (smoke test)")
|
||||
args = p.parse_args(argv)
|
||||
return run(quick=args.quick)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
sys.exit(main())
|
||||
+119
@@ -0,0 +1,119 @@
|
||||
"""Rasterizzazione DXF → immagine template per il matcher shape-based.
|
||||
|
||||
Il matcher lavora sui gradienti degli edge: un line-drawing pulito
|
||||
(sfondo grigio scuro, tratti chiari) è un template perfettamente valido.
|
||||
Questo modulo converte un file DXF (CAD 2D) in una bitmap grayscale
|
||||
centrata e scalata, pronta per train().
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import io
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
|
||||
# Valori di rendering: sfondo scuro / tratto chiaro → gradiente netto
|
||||
BG_GRAY = 60
|
||||
LINE_GRAY = 220
|
||||
|
||||
|
||||
def _read_doc(data: bytes):
|
||||
"""Parse DXF da bytes con gestione encoding.
|
||||
|
||||
Prima prova ezdxf.read su StringIO (DXF ASCII utf-8 / cp1252),
|
||||
poi fallback su ezdxf.recover che auto-rileva encoding e tollera
|
||||
file malformati.
|
||||
"""
|
||||
import ezdxf
|
||||
from ezdxf import recover
|
||||
|
||||
for enc in ("utf-8", "cp1252"):
|
||||
try:
|
||||
text = data.decode(enc)
|
||||
return ezdxf.read(io.StringIO(text))
|
||||
except Exception:
|
||||
# UnicodeDecodeError, DXFStructureError e simili: prossimo tentativo
|
||||
continue
|
||||
# Ultimo tentativo: recover lavora direttamente sui bytes
|
||||
try:
|
||||
doc, _auditor = recover.read(io.BytesIO(data))
|
||||
return doc
|
||||
except Exception as e:
|
||||
raise ValueError(f"DXF illeggibile o corrotto: {e}") from e
|
||||
|
||||
|
||||
def _extract_polylines(doc, flatten_dist: float = 0.05) -> tuple[list[np.ndarray], int]:
|
||||
"""Converte le entità del modelspace in polilinee (liste di punti XY).
|
||||
|
||||
Entità non convertibili (non supportate da make_path) vengono saltate
|
||||
silenziosamente ma conteggiate. Ritorna (polilinee, n_saltate).
|
||||
"""
|
||||
from ezdxf import path as ezpath
|
||||
|
||||
polylines: list[np.ndarray] = []
|
||||
skipped = 0
|
||||
for entity in doc.modelspace():
|
||||
try:
|
||||
p = ezpath.make_path(entity)
|
||||
pts = np.array(
|
||||
[(v.x, v.y) for v in p.flattening(distance=flatten_dist)],
|
||||
dtype=np.float64,
|
||||
)
|
||||
if len(pts) >= 2:
|
||||
polylines.append(pts)
|
||||
except Exception:
|
||||
skipped += 1
|
||||
return polylines, skipped
|
||||
|
||||
|
||||
def dxf_to_image(data: bytes, target_size: int = 512,
|
||||
line_thickness: int = 2, margin: int = 16) -> np.ndarray:
|
||||
"""Rasterizza un DXF in immagine grayscale (H, W) uint8.
|
||||
|
||||
- Scala uniforme: il lato lungo del disegno = target_size - 2*margin.
|
||||
- Disegno centrato, asse Y CAD (su) ribaltato in convenzione immagine.
|
||||
- Sfondo grigio scuro (60), tratti chiari (220), antialiased.
|
||||
|
||||
Solleva ValueError se il DXF è vuoto o illeggibile.
|
||||
"""
|
||||
doc = _read_doc(data)
|
||||
|
||||
# Distanza di flattening provvisoria in unità CAD: raffinata sotto
|
||||
# una volta nota la scala (qui serve solo per il bounding box).
|
||||
polylines, skipped = _extract_polylines(doc)
|
||||
if not polylines:
|
||||
raise ValueError(
|
||||
"DXF vuoto: nessuna entità convertibile in polilinea nel "
|
||||
f"modelspace ({skipped} entità non supportate saltate)")
|
||||
|
||||
all_pts = np.vstack(polylines)
|
||||
min_xy = all_pts.min(axis=0)
|
||||
max_xy = all_pts.max(axis=0)
|
||||
extent = max_xy - min_xy
|
||||
long_side = float(extent.max())
|
||||
if long_side <= 0:
|
||||
raise ValueError("DXF degenere: bounding box con estensione nulla")
|
||||
|
||||
# Ri-flattening con distanza adattiva: ~0.25 px di errore alla scala
|
||||
# finale (il primo pass usava una tolleranza in unità CAD arbitraria).
|
||||
avail = max(1, target_size - 2 * margin)
|
||||
scale = avail / long_side
|
||||
polylines, _ = _extract_polylines(doc, flatten_dist=max(1e-9, 0.25 / scale))
|
||||
|
||||
canvas = np.full((target_size, target_size), BG_GRAY, dtype=np.uint8)
|
||||
# Offset per centrare il disegno (anche sul lato corto)
|
||||
draw_w = extent[0] * scale
|
||||
draw_h = extent[1] * scale
|
||||
off_x = (target_size - draw_w) / 2.0
|
||||
off_y = (target_size - draw_h) / 2.0
|
||||
|
||||
for pts in polylines:
|
||||
px = (pts[:, 0] - min_xy[0]) * scale + off_x
|
||||
# Y CAD verso l'alto → Y immagine verso il basso
|
||||
py = (max_xy[1] - pts[:, 1]) * scale + off_y
|
||||
ipts = np.stack([px, py], axis=1).round().astype(np.int32)
|
||||
cv2.polylines(canvas, [ipts], isClosed=False,
|
||||
color=LINE_GRAY, thickness=line_thickness,
|
||||
lineType=cv2.LINE_AA)
|
||||
|
||||
return canvas
|
||||
+217
@@ -0,0 +1,217 @@
|
||||
"""CLI validation harness per LineShapeMatcher.
|
||||
|
||||
Usage:
|
||||
python -m pm2d.eval dataset.json [opzioni]
|
||||
|
||||
Formato dataset (JSON):
|
||||
{
|
||||
"template": "path/to/template.png",
|
||||
"mask": "path/to/mask.png", # opzionale
|
||||
"params": { # opzionali, override su matcher init
|
||||
"use_polarity": true,
|
||||
"angle_step_deg": 5,
|
||||
...
|
||||
},
|
||||
"find_params": { # opzionali, passati a find()
|
||||
"min_score": 0.6,
|
||||
"use_soft_score": true,
|
||||
...
|
||||
},
|
||||
"scenes": [
|
||||
{
|
||||
"image": "path/to/scene1.png",
|
||||
"ground_truth": [
|
||||
{"cx": 320.0, "cy": 240.0, "angle_deg": 12.0,
|
||||
"scale": 1.0, "tolerance_px": 5.0,
|
||||
"tolerance_deg": 3.0}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Output: report precision/recall/IoU/timing per ogni scena + aggregati.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import math
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
|
||||
from pm2d.line_matcher import LineShapeMatcher, _poly_iou, _oriented_bbox_polygon
|
||||
|
||||
|
||||
def _load_image(path: str | Path) -> np.ndarray:
|
||||
img = cv2.imread(str(path), cv2.IMREAD_UNCHANGED)
|
||||
if img is None:
|
||||
raise FileNotFoundError(f"Immagine non trovata: {path}")
|
||||
if img.ndim == 2:
|
||||
img = cv2.cvtColor(img, cv2.COLOR_GRAY2BGR)
|
||||
return img
|
||||
|
||||
|
||||
def _gt_to_poly(gt: dict, tw: int, th: int) -> np.ndarray:
|
||||
"""Costruisce bbox poligonale per un ground truth."""
|
||||
s = float(gt.get("scale", 1.0))
|
||||
return _oriented_bbox_polygon(
|
||||
float(gt["cx"]), float(gt["cy"]),
|
||||
tw * s, th * s, float(gt["angle_deg"]),
|
||||
)
|
||||
|
||||
|
||||
def _match_to_gt(match, gt: dict, tw: int, th: int,
|
||||
iou_thr: float = 0.3) -> bool:
|
||||
"""True se il match corrisponde al ground truth.
|
||||
|
||||
Criterio: distanza centro <= tolerance_px AND |angle_deg - gt| <= tolerance_deg
|
||||
OR IoU bbox >= iou_thr (fallback per pose con tolerance ampie).
|
||||
"""
|
||||
tol_px = float(gt.get("tolerance_px", 5.0))
|
||||
tol_deg = float(gt.get("tolerance_deg", 3.0))
|
||||
dx = match.cx - float(gt["cx"])
|
||||
dy = match.cy - float(gt["cy"])
|
||||
dist = math.hypot(dx, dy)
|
||||
da = abs((match.angle_deg - float(gt["angle_deg"]) + 180) % 360 - 180)
|
||||
if dist <= tol_px and da <= tol_deg:
|
||||
return True
|
||||
# Fallback IoU
|
||||
poly_gt = _gt_to_poly(gt, tw, th)
|
||||
poly_m = match.bbox_poly
|
||||
if _poly_iou(poly_m, poly_gt) >= iou_thr:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def evaluate_scene(matcher: LineShapeMatcher, scene_bgr: np.ndarray,
|
||||
gt_list: list[dict], find_params: dict,
|
||||
tw: int, th: int) -> dict:
|
||||
"""Esegue match e calcola TP/FP/FN per una scena."""
|
||||
t0 = time.time()
|
||||
matches = matcher.find(scene_bgr, **find_params)
|
||||
elapsed = time.time() - t0
|
||||
|
||||
gt_matched = [False] * len(gt_list)
|
||||
match_is_tp = [False] * len(matches)
|
||||
iou_per_match = [0.0] * len(matches)
|
||||
for i, m in enumerate(matches):
|
||||
for j, gt in enumerate(gt_list):
|
||||
if gt_matched[j]:
|
||||
continue
|
||||
if _match_to_gt(m, gt, tw, th):
|
||||
gt_matched[j] = True
|
||||
match_is_tp[i] = True
|
||||
# Calcolo IoU per metrica
|
||||
poly_gt = _gt_to_poly(gt, tw, th)
|
||||
iou_per_match[i] = _poly_iou(m.bbox_poly, poly_gt)
|
||||
break
|
||||
tp = sum(match_is_tp)
|
||||
fp = len(matches) - tp
|
||||
fn = len(gt_list) - sum(gt_matched)
|
||||
return {
|
||||
"n_matches": len(matches),
|
||||
"n_gt": len(gt_list),
|
||||
"tp": tp, "fp": fp, "fn": fn,
|
||||
"find_time_s": elapsed,
|
||||
"iou_mean": float(np.mean([i for i, t in zip(iou_per_match, match_is_tp) if t])
|
||||
if tp > 0 else 0.0),
|
||||
"diag": (matcher.get_last_diag()
|
||||
if hasattr(matcher, "get_last_diag") else None),
|
||||
}
|
||||
|
||||
|
||||
def run(dataset_path: str, scene_filter: str | None = None,
|
||||
verbose: bool = False) -> dict:
|
||||
"""Esegue eval su dataset, ritorna report aggregato."""
|
||||
dataset_path = Path(dataset_path)
|
||||
base = dataset_path.parent
|
||||
with open(dataset_path) as f:
|
||||
ds = json.load(f)
|
||||
|
||||
template = _load_image(base / ds["template"])
|
||||
mask = None
|
||||
if ds.get("mask"):
|
||||
mask_img = cv2.imread(str(base / ds["mask"]), cv2.IMREAD_GRAYSCALE)
|
||||
if mask_img is not None:
|
||||
mask = (mask_img > 128).astype(np.uint8) * 255
|
||||
init_params = ds.get("params", {})
|
||||
find_params = ds.get("find_params", {})
|
||||
|
||||
matcher = LineShapeMatcher(**init_params)
|
||||
n_var = matcher.train(template, mask=mask)
|
||||
tw, th = matcher.template_size
|
||||
print(f"Template: {ds['template']} ({tw}x{th}), {n_var} varianti")
|
||||
print(f"Param matcher: {init_params}")
|
||||
print(f"Param find: {find_params}")
|
||||
print()
|
||||
|
||||
scenes = ds["scenes"]
|
||||
if scene_filter:
|
||||
scenes = [s for s in scenes if scene_filter in s["image"]]
|
||||
|
||||
rows = []
|
||||
tot_tp = tot_fp = tot_fn = 0
|
||||
tot_time = 0.0
|
||||
for sc in scenes:
|
||||
scene = _load_image(base / sc["image"])
|
||||
gt = sc.get("ground_truth", [])
|
||||
result = evaluate_scene(matcher, scene, gt, find_params, tw, th)
|
||||
rows.append({"scene": sc["image"], **result})
|
||||
tot_tp += result["tp"]; tot_fp += result["fp"]; tot_fn += result["fn"]
|
||||
tot_time += result["find_time_s"]
|
||||
prec = result["tp"] / max(1, result["tp"] + result["fp"])
|
||||
rec = result["tp"] / max(1, result["tp"] + result["fn"])
|
||||
line = (f" {sc['image']:30s} "
|
||||
f"TP={result['tp']} FP={result['fp']} FN={result['fn']} "
|
||||
f"P={prec:.2f} R={rec:.2f} "
|
||||
f"IoU={result['iou_mean']:.2f} "
|
||||
f"t={result['find_time_s']*1000:.0f}ms")
|
||||
print(line)
|
||||
if verbose and result["diag"] and hasattr(matcher, "_format_diag"):
|
||||
print(f" diag: {matcher._format_diag(result['diag'])}")
|
||||
|
||||
# Aggregati
|
||||
precision = tot_tp / max(1, tot_tp + tot_fp)
|
||||
recall = tot_tp / max(1, tot_tp + tot_fn)
|
||||
f1 = 2 * precision * recall / max(1e-9, precision + recall)
|
||||
print()
|
||||
print(f"AGGREGATO: precision={precision:.3f} recall={recall:.3f} "
|
||||
f"F1={f1:.3f} TP={tot_tp} FP={tot_fp} FN={tot_fn}")
|
||||
print(f"TIME: total={tot_time:.2f}s avg={tot_time / max(1, len(scenes)) * 1000:.0f}ms/scene")
|
||||
|
||||
return {
|
||||
"precision": precision, "recall": recall, "f1": f1,
|
||||
"tp": tot_tp, "fp": tot_fp, "fn": tot_fn,
|
||||
"total_time_s": tot_time, "n_scenes": len(scenes),
|
||||
"per_scene": rows,
|
||||
}
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
p = argparse.ArgumentParser(
|
||||
description="pm2d-eval: validation harness per LineShapeMatcher"
|
||||
)
|
||||
p.add_argument("dataset", help="JSON dataset (template + scenes + GT)")
|
||||
p.add_argument("--scene-filter", default=None,
|
||||
help="Filtro substring sui nomi scena (debug)")
|
||||
p.add_argument("--verbose", "-v", action="store_true",
|
||||
help="Stampa diag dict per ogni scena")
|
||||
p.add_argument("--out", default=None,
|
||||
help="Salva report JSON su file")
|
||||
args = p.parse_args(argv)
|
||||
report = run(args.dataset, scene_filter=args.scene_filter,
|
||||
verbose=args.verbose)
|
||||
if args.out:
|
||||
with open(args.out, "w") as f:
|
||||
json.dump(report, f, indent=2)
|
||||
print(f"Report salvato: {args.out}")
|
||||
return 0 if report["f1"] > 0.5 else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
+144
-82
@@ -12,7 +12,6 @@ Tutta la logica algoritmica vive in pm2d.matcher.EdgeShapeMatcher.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from tkinter import Tk, filedialog
|
||||
import tkinter as tk
|
||||
@@ -196,8 +195,10 @@ def _warp_template_edges_to_scene(
|
||||
edge = cv2.Canny(template_gray, canny_low, canny_high)
|
||||
# Matrice affine: scala + rotazione attorno al centro template, poi traslazione
|
||||
Ht, Wt = h, w
|
||||
cx_t = (Wt - 1) / 2.0
|
||||
cy_t = (Ht - 1) / 2.0
|
||||
# Centro coerente con la convenzione train (center = w / 2.0, no -1):
|
||||
# (Wt-1)/2 introduceva uno shift di 0.5px per template di lato pari.
|
||||
cx_t = Wt / 2.0
|
||||
cy_t = Ht / 2.0
|
||||
M = cv2.getRotationMatrix2D((cx_t, cy_t), angle_deg, scale)
|
||||
# Traslazione per portare centro template a (cx, cy) della scena
|
||||
M[0, 2] += cx - cx_t
|
||||
@@ -261,33 +262,33 @@ def draw_matches(
|
||||
return out
|
||||
|
||||
|
||||
def build_info_panel(
|
||||
def _put_text(img: np.ndarray, s: str, x: int, y: int,
|
||||
size: float = 0.5, color: tuple = (220, 220, 220),
|
||||
thick: int = 1) -> None:
|
||||
cv2.putText(img, s, (x, y), cv2.FONT_HERSHEY_SIMPLEX,
|
||||
size, color, thick, cv2.LINE_AA)
|
||||
|
||||
|
||||
def build_left_panel(
|
||||
template_bgr: np.ndarray,
|
||||
params: dict,
|
||||
matches: list[Match],
|
||||
panel_width: int = 380,
|
||||
panel_height: int | None = None,
|
||||
panel_width: int = 300,
|
||||
panel_height: int = 900,
|
||||
) -> np.ndarray:
|
||||
"""Costruisce pannello laterale: thumbnail modello + parametri + legenda
|
||||
numerata dei match + hotkey."""
|
||||
if panel_height is None:
|
||||
panel_height = panel_width * 2
|
||||
"""Pannello sinistro: thumbnail modello + legenda risultati (senza parametri)."""
|
||||
panel = np.full((panel_height, panel_width, 3), 28, dtype=np.uint8)
|
||||
pad = 12
|
||||
y = pad
|
||||
|
||||
def _text(img, s, y, size=0.5, color=(220, 220, 220), thick=1, x=None):
|
||||
cv2.putText(img, s, (x if x is not None else pad, y),
|
||||
cv2.FONT_HERSHEY_SIMPLEX, size, color, thick, cv2.LINE_AA)
|
||||
|
||||
# Titolo
|
||||
_text(panel, "MODELLO", y + 18, size=0.7, color=(0, 200, 255), thick=2)
|
||||
# Titolo MODELLO
|
||||
_put_text(panel, "MODELLO", pad, y + 18, size=0.7,
|
||||
color=(0, 200, 255), thick=2)
|
||||
y += 34
|
||||
|
||||
# Thumbnail modello
|
||||
# Thumbnail
|
||||
th_h, th_w = template_bgr.shape[:2]
|
||||
max_tw = panel_width - 2 * pad
|
||||
max_th = 150
|
||||
max_th = 160
|
||||
sc = min(max_tw / th_w, max_th / th_h)
|
||||
tw = max(1, int(th_w * sc)); th = max(1, int(th_h * sc))
|
||||
thumb = cv2.resize(template_bgr, (tw, th), interpolation=cv2.INTER_AREA)
|
||||
@@ -297,74 +298,123 @@ def build_info_panel(
|
||||
panel[y:y + th, tx:tx + tw] = thumb
|
||||
cv2.rectangle(panel, (tx - 1, y - 1), (tx + tw, y + th),
|
||||
(90, 90, 90), 1, cv2.LINE_AA)
|
||||
y += th + 12
|
||||
y += th + 16
|
||||
|
||||
# Parametri
|
||||
_text(panel, "PARAMETRI", y, size=0.55, color=(0, 200, 255), thick=2)
|
||||
y += 20
|
||||
for k, v in params.items():
|
||||
_text(panel, f"{k}: {v}", y, size=0.42)
|
||||
y += 16
|
||||
|
||||
y += 6
|
||||
_text(panel, f"RISULTATI ({len(matches)})", y,
|
||||
size=0.55, color=(0, 200, 255), thick=2)
|
||||
y += 20
|
||||
# Risultati
|
||||
_put_text(panel, f"RISULTATI ({len(matches)})", pad, y,
|
||||
size=0.6, color=(0, 200, 255), thick=2)
|
||||
y += 22
|
||||
if matches:
|
||||
scores = [m.score for m in matches]
|
||||
scales = [m.scale for m in matches]
|
||||
_text(panel, f"score: {min(scores):.2f}..{max(scores):.2f}", y,
|
||||
size=0.42); y += 16
|
||||
_put_text(panel, f"score: {min(scores):.2f}..{max(scores):.2f}",
|
||||
pad, y, size=0.42); y += 16
|
||||
if max(scales) != min(scales):
|
||||
_text(panel, f"scale: {min(scales):.2f}..{max(scales):.2f}", y,
|
||||
size=0.42); y += 16
|
||||
|
||||
# Legenda numerata con colore per ogni match
|
||||
max_rows = max(1, (panel_height - y - 120) // 16)
|
||||
_put_text(panel, f"scale: {min(scales):.2f}..{max(scales):.2f}",
|
||||
pad, y, size=0.42); y += 16
|
||||
y += 4
|
||||
max_rows = max(1, (panel_height - y - 12) // 16)
|
||||
shown = matches[:max_rows]
|
||||
for i, m in enumerate(shown):
|
||||
color = _color_for(i)
|
||||
# Pallino di colore
|
||||
cv2.circle(panel, (pad + 6, y - 4), 5, color, -1, cv2.LINE_AA)
|
||||
txt = (f"#{i+1} ({int(m.cx)},{int(m.cy)}) "
|
||||
f"{m.angle_deg:.0f}d s={m.scale:.2f} {m.score:.3f}")
|
||||
_text(panel, txt, y, size=0.40, x=pad + 18)
|
||||
_put_text(panel, txt, pad + 18, y, size=0.40)
|
||||
y += 16
|
||||
if len(matches) > len(shown):
|
||||
_text(panel, f"... +{len(matches) - len(shown)} altri",
|
||||
y, size=0.40, color=(150, 150, 150)); y += 16
|
||||
|
||||
# Hotkey in fondo
|
||||
footer_y = panel_height - 92
|
||||
_text(panel, "HOTKEY", footer_y, size=0.55, color=(0, 200, 255), thick=2)
|
||||
fy = footer_y + 18
|
||||
for line in [
|
||||
"r modifica parametri",
|
||||
"o nuovo ROI (stesso modello)",
|
||||
"m nuovo file modello",
|
||||
"s nuova scena",
|
||||
"q / Esc esci",
|
||||
]:
|
||||
_text(panel, line, fy, size=0.40, color=(180, 180, 180))
|
||||
fy += 14
|
||||
_put_text(panel, f"... +{len(matches) - len(shown)} altri",
|
||||
pad, y, size=0.40, color=(150, 150, 150))
|
||||
return panel
|
||||
|
||||
|
||||
def compose_result(
|
||||
scene_annotated: np.ndarray,
|
||||
panel: np.ndarray,
|
||||
def build_right_panel(
|
||||
params: dict,
|
||||
panel_width: int = 320,
|
||||
panel_height: int = 900,
|
||||
) -> np.ndarray:
|
||||
"""Affianca pannello a sinistra + scena a destra, altezza uniforme."""
|
||||
sH, sW = scene_annotated.shape[:2]
|
||||
pH, pW = panel.shape[:2]
|
||||
if pH != sH:
|
||||
sc = sH / pH
|
||||
new_pW = max(1, int(pW * sc))
|
||||
panel = cv2.resize(panel, (new_pW, sH), interpolation=cv2.INTER_AREA)
|
||||
pW = new_pW
|
||||
out = np.zeros((sH, pW + sW, 3), dtype=np.uint8)
|
||||
out[:, :pW] = panel
|
||||
out[:, pW:] = scene_annotated
|
||||
"""Pannello destro: parametri correnti + hotkey."""
|
||||
panel = np.full((panel_height, panel_width, 3), 28, dtype=np.uint8)
|
||||
pad = 12
|
||||
y = pad
|
||||
|
||||
_put_text(panel, "PARAMETRI", pad, y + 18, size=0.7,
|
||||
color=(0, 200, 255), thick=2)
|
||||
y += 36
|
||||
|
||||
# Tempi in alto, evidenziati
|
||||
for k in ("train_time", "find_time", "num_variants"):
|
||||
if k in params:
|
||||
v = params[k]
|
||||
_put_text(panel, f"{k}:", pad, y,
|
||||
size=0.45, color=(160, 220, 160))
|
||||
_put_text(panel, str(v), pad + 140, y,
|
||||
size=0.45, color=(200, 255, 200), thick=2)
|
||||
y += 18
|
||||
y += 6
|
||||
|
||||
# Altri parametri
|
||||
skip = {"train_time", "find_time", "num_variants"}
|
||||
for k, v in params.items():
|
||||
if k in skip:
|
||||
continue
|
||||
_put_text(panel, f"{k}:", pad, y, size=0.42, color=(180, 180, 180))
|
||||
_put_text(panel, str(v), pad + 140, y, size=0.42,
|
||||
color=(220, 220, 220))
|
||||
y += 16
|
||||
|
||||
# Hotkey in fondo
|
||||
footer_y = panel_height - 110
|
||||
_put_text(panel, "HOTKEY", pad, footer_y, size=0.6,
|
||||
color=(0, 200, 255), thick=2)
|
||||
fy = footer_y + 22
|
||||
for line in [
|
||||
"r modifica parametri",
|
||||
"o nuovo ROI",
|
||||
"m nuovo modello",
|
||||
"s nuova scena",
|
||||
"q / Esc esci",
|
||||
]:
|
||||
_put_text(panel, line, pad, fy, size=0.42, color=(200, 200, 200))
|
||||
fy += 16
|
||||
return panel
|
||||
|
||||
|
||||
def _fit_scene_center(
|
||||
scene: np.ndarray, target_w: int, target_h: int,
|
||||
) -> np.ndarray:
|
||||
"""Scala scena a fit (target_w, target_h) mantenendo aspect; padding bg."""
|
||||
h, w = scene.shape[:2]
|
||||
sc = min(target_w / w, target_h / h)
|
||||
new_w = max(1, int(w * sc)); new_h = max(1, int(h * sc))
|
||||
resized = cv2.resize(scene, (new_w, new_h), interpolation=cv2.INTER_AREA)
|
||||
out = np.full((target_h, target_w, 3), 20, dtype=np.uint8)
|
||||
y0 = (target_h - new_h) // 2
|
||||
x0 = (target_w - new_w) // 2
|
||||
out[y0:y0 + new_h, x0:x0 + new_w] = resized
|
||||
return out
|
||||
|
||||
|
||||
def compose_fixed_layout(
|
||||
scene_annotated: np.ndarray,
|
||||
left_panel: np.ndarray,
|
||||
right_panel: np.ndarray,
|
||||
window_w: int = 1600,
|
||||
window_h: int = 900,
|
||||
) -> np.ndarray:
|
||||
"""Layout fisso: [left | scena fit-scaled | right]."""
|
||||
lH, lW = left_panel.shape[:2]
|
||||
rH, rW = right_panel.shape[:2]
|
||||
# Altezza uniforme (pannelli dovrebbero essere già window_h)
|
||||
if lH != window_h:
|
||||
left_panel = cv2.resize(left_panel, (lW, window_h),
|
||||
interpolation=cv2.INTER_AREA)
|
||||
if rH != window_h:
|
||||
right_panel = cv2.resize(right_panel, (rW, window_h),
|
||||
interpolation=cv2.INTER_AREA)
|
||||
center_w = window_w - lW - rW
|
||||
center = _fit_scene_center(scene_annotated, center_w, window_h)
|
||||
out = np.concatenate([left_panel, center, right_panel], axis=1)
|
||||
return out
|
||||
|
||||
|
||||
@@ -382,28 +432,38 @@ def show_results(
|
||||
matches: list[Match],
|
||||
template_bgr: np.ndarray | None = None,
|
||||
params: dict | None = None,
|
||||
window_w: int = 1600,
|
||||
window_h: int = 900,
|
||||
) -> str:
|
||||
"""Visualizza risultati. Ritorna 'rematch' se l'utente preme 'r', altrimenti 'quit'."""
|
||||
"""Visualizza risultati in layout fisso [SX panel | scena scalata | DX panel].
|
||||
|
||||
Ritorna 'rematch'/'new_roi'/'new_model'/'new_scene'/'quit'.
|
||||
"""
|
||||
print(f"\n=== {len(matches)} match trovati ===")
|
||||
for i, m in enumerate(matches):
|
||||
print(f" #{i+1}: cx={m.cx:.1f} cy={m.cy:.1f} "
|
||||
f"angle={m.angle_deg:.1f}d scale={m.scale:.2f} score={m.score:.3f}")
|
||||
|
||||
template_gray = None
|
||||
if template_bgr is not None:
|
||||
template_gray = (template_bgr if template_bgr.ndim == 2
|
||||
else cv2.cvtColor(template_bgr, cv2.COLOR_BGR2GRAY))
|
||||
annotated = draw_matches(scene, matches, template_gray=template_gray)
|
||||
if template_bgr is not None and params is not None:
|
||||
panel = build_info_panel(template_bgr, params, matches,
|
||||
panel_height=annotated.shape[0])
|
||||
composed = compose_result(annotated, panel)
|
||||
|
||||
if template_bgr is not None:
|
||||
left = build_left_panel(template_bgr, matches, panel_height=window_h)
|
||||
else:
|
||||
composed = annotated
|
||||
disp = _fit_for_display(composed, max_side=1600)
|
||||
cv2.namedWindow(WINDOW_RESULT, cv2.WINDOW_NORMAL)
|
||||
cv2.resizeWindow(WINDOW_RESULT, min(disp.shape[1], 1600),
|
||||
min(disp.shape[0], 900))
|
||||
cv2.imshow(WINDOW_RESULT, disp)
|
||||
left = np.full((window_h, 300, 3), 28, dtype=np.uint8)
|
||||
if params is not None:
|
||||
right = build_right_panel(params, panel_height=window_h)
|
||||
else:
|
||||
right = np.full((window_h, 320, 3), 28, dtype=np.uint8)
|
||||
|
||||
composed = compose_fixed_layout(
|
||||
annotated, left, right, window_w=window_w, window_h=window_h,
|
||||
)
|
||||
cv2.namedWindow(WINDOW_RESULT, cv2.WINDOW_AUTOSIZE)
|
||||
cv2.imshow(WINDOW_RESULT, composed)
|
||||
print("\n[r] parametri [o] nuovo ROI [m] nuovo modello [s] nuova scena [q/Esc] chiudi")
|
||||
action = "quit"
|
||||
while True:
|
||||
@@ -433,7 +493,9 @@ def run(
|
||||
num_features: int = 96,
|
||||
weak_grad: float = 30.0,
|
||||
strong_grad: float = 60.0,
|
||||
spread_radius: int = 5,
|
||||
# 4 allineato col default del matcher: raggio 5 peggiora la precisione
|
||||
# di rotazione (spread troppo largo appiattisce il picco angolare).
|
||||
spread_radius: int = 4,
|
||||
pyramid_levels: int = 3,
|
||||
min_score: float = 0.55,
|
||||
max_matches: int = 25,
|
||||
|
||||
+1562
-192
File diff suppressed because it is too large
Load Diff
+26
-2
@@ -91,8 +91,16 @@ class EdgeShapeMatcher:
|
||||
a0, a1 = self.angle_range_deg
|
||||
if self.angle_step_deg <= 0 or a0 >= a1:
|
||||
return [float(a0)]
|
||||
n = int(np.floor((a1 - a0) / self.angle_step_deg))
|
||||
return [float(a0 + i * self.angle_step_deg) for i in range(n)]
|
||||
# n+1 valori per includere l'estremo superiore del range: con il
|
||||
# solo floor un range [0, 90] step 5 si fermava a 85° (off-by-one).
|
||||
n = int(np.floor((a1 - a0) / self.angle_step_deg)) + 1
|
||||
angles = [float(a0 + i * self.angle_step_deg) for i in range(n)]
|
||||
if a1 - a0 >= 360.0:
|
||||
# Range che copre il giro completo: a0+360° è la stessa pose di
|
||||
# a0, escludi il duplicato (variante inutile in train/find).
|
||||
eps = 1e-6
|
||||
angles = [a for a in angles if a < a0 + 360.0 - eps]
|
||||
return angles
|
||||
|
||||
def train(self, template_bgr: np.ndarray) -> int:
|
||||
"""Genera varianti per tutte le combinazioni (angolo, scala)."""
|
||||
@@ -222,6 +230,14 @@ class EdgeShapeMatcher:
|
||||
for y, x in zip(ys, xs):
|
||||
candidates.append((float(res[y, x]), int(x), int(y), ti))
|
||||
|
||||
# Cap candidati top-level: senza limite np.where con soglia bassa
|
||||
# può generare migliaia di candidati, ognuno con un matchTemplate
|
||||
# full-res nel refinement. Tieni solo i migliori per score.
|
||||
max_candidates = max(1, max_matches * 10)
|
||||
if len(candidates) > max_candidates:
|
||||
candidates.sort(key=lambda c: -c[0])
|
||||
candidates = candidates[:max_candidates]
|
||||
|
||||
# Refinement a risoluzione piena: per ogni candidato top, finestra locale
|
||||
refined: list[tuple[float, int, int, int]] = []
|
||||
margin = sf + 4
|
||||
@@ -294,6 +310,10 @@ class EdgeShapeMatcher:
|
||||
)
|
||||
arrays = {f"edge_{i}": t.edge for i, t in enumerate(self.templates)}
|
||||
arrays.update({f"mask_{i}": t.mask for i, t in enumerate(self.templates)})
|
||||
# Persisti anche il grayscale originale: senza, l'overlay edge
|
||||
# spariva dopo load() (template_gray restava None).
|
||||
if self.template_gray is not None:
|
||||
arrays["template_gray"] = self.template_gray
|
||||
np.savez_compressed(path, params=params, meta=meta, **arrays)
|
||||
|
||||
@classmethod
|
||||
@@ -312,6 +332,10 @@ class EdgeShapeMatcher:
|
||||
top_score_factor=float(p[12]) if len(p) > 12 else 0.6,
|
||||
)
|
||||
m.template_size = (int(p[8]), int(p[9]))
|
||||
# Retrocompatibilità: modelli salvati prima non hanno template_gray
|
||||
# (resta None: overlay edge non disponibile ma find() funziona).
|
||||
if "template_gray" in z.files:
|
||||
m.template_gray = z["template_gray"]
|
||||
meta = z["meta"]
|
||||
for i in range(len(meta)):
|
||||
m.templates.append(
|
||||
|
||||
+1161
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,986 @@
|
||||
// Pattern Matching 2D - frontend (UI semplificata operator-friendly)
|
||||
|
||||
// Parametri avanzati (sezione collassabile)
|
||||
const ADV_PARAMS = [
|
||||
["num_features", "Num feature", "int", ""],
|
||||
["weak_grad", "Weak grad", "float", ""],
|
||||
["strong_grad", "Strong grad", "float", ""],
|
||||
["spread_radius", "Spread radius", "int", ""],
|
||||
["pyramid_levels", "Pyramid levels", "int", ""],
|
||||
["verify_threshold", "Verify NCC thr", "float", 0.4],
|
||||
["nms_radius", "NMS radius (0=auto)", "int", 0],
|
||||
];
|
||||
|
||||
const PALETTE = [
|
||||
"#00ff00", "#ffc800", "#ff6464", "#ffc800", "#c800ff",
|
||||
"#64ffc8", "#ff0000", "#00ffff",
|
||||
];
|
||||
|
||||
const state = {
|
||||
model: null, scene: null, roi: null, drag: null,
|
||||
matches: [], annotatedImg: null,
|
||||
active_recipe: null, // V: ricetta caricata (string nome) o null
|
||||
// ROI poligonale: vertici [x, y] in coordinate immagine modello
|
||||
polyMode: false, polyPts: [], polyClosed: false,
|
||||
// Export JSON: ultimo match completo (params + risposta)
|
||||
lastMatch: null,
|
||||
};
|
||||
|
||||
// ---------- Forms ----------
|
||||
function buildAdvancedForm() {
|
||||
const form = document.getElementById("adv-form");
|
||||
form.innerHTML = "";
|
||||
for (const [key, label, , def] of ADV_PARAMS) {
|
||||
const lbl = document.createElement("label");
|
||||
lbl.textContent = label;
|
||||
const inp = document.createElement("input");
|
||||
inp.id = `adv-${key}`;
|
||||
inp.type = "text";
|
||||
inp.placeholder = "auto";
|
||||
inp.value = def === "" ? "" : String(def);
|
||||
inp.addEventListener("keydown", (e) => {
|
||||
if (e.key === "Enter") doMatch();
|
||||
});
|
||||
form.appendChild(lbl);
|
||||
form.appendChild(inp);
|
||||
}
|
||||
}
|
||||
|
||||
function readUserParams() {
|
||||
return {
|
||||
tipo: document.getElementById("p-tipo").value,
|
||||
simmetria: document.getElementById("p-simmetria").value,
|
||||
scala: document.getElementById("p-scala").value,
|
||||
precisione: document.getElementById("p-precisione").value,
|
||||
filtro_fp: document.getElementById("p-filtro-fp").value,
|
||||
penalita_scala: parseFloat(
|
||||
document.getElementById("p-penalita-scala").value),
|
||||
min_score: parseFloat(document.getElementById("p-min-score").value),
|
||||
max_matches: parseInt(document.getElementById("p-max-matches").value, 10),
|
||||
...readEdgeOverrides(),
|
||||
...readHalconFlags(),
|
||||
};
|
||||
}
|
||||
|
||||
function readEdgeOverrides() {
|
||||
// Override edge dal pannello "Anteprima edge". Settati = utente li ha
|
||||
// toccati (anche se uguali al default attuale). Vengono propagati a
|
||||
// _simple_to_technical e usati identici sia in training sia in find.
|
||||
// Inoltre salvati nella ricetta cosi' si replicano al load.
|
||||
const _v = (id, parser) => {
|
||||
const el = document.getElementById(id);
|
||||
if (!el) return null;
|
||||
const v = parser(el.value);
|
||||
return Number.isFinite(v) ? v : null;
|
||||
};
|
||||
// Sempre passa i valori correnti degli slider: e' la richiesta utente
|
||||
// che i param di pulizia rumore vengano usati anche nel find/ricetta.
|
||||
const polCb = document.getElementById("hc-use-polarity");
|
||||
return {
|
||||
edge_weak_grad: _v("ep-weak", parseFloat),
|
||||
edge_strong_grad: _v("ep-strong", parseFloat),
|
||||
edge_num_features: _v("ep-nf", parseInt),
|
||||
edge_min_feature_spacing: _v("ep-sp", parseInt),
|
||||
use_polarity: polCb?.checked || document.getElementById("ep-pol")?.checked,
|
||||
};
|
||||
}
|
||||
|
||||
function readHalconFlags() {
|
||||
// Halcon-mode toggle: tutti i flag default-off, esposti via "Modalità Halcon"
|
||||
const $cb = (id) => document.getElementById(id)?.checked ?? false;
|
||||
const $num = (id, def) => {
|
||||
const v = parseFloat(document.getElementById(id)?.value);
|
||||
return Number.isFinite(v) ? v : def;
|
||||
};
|
||||
const $int = (id, def) => {
|
||||
const v = parseInt(document.getElementById(id)?.value, 10);
|
||||
return Number.isFinite(v) ? v : def;
|
||||
};
|
||||
const roiStr = document.getElementById("hc-search-roi")?.value.trim() ?? "";
|
||||
let search_roi = null;
|
||||
if (roiStr) {
|
||||
const p = roiStr.split(/[ ,;]+/).map((x) => parseInt(x, 10));
|
||||
if (p.length === 4 && p.every((v) => Number.isFinite(v))) search_roi = p;
|
||||
}
|
||||
return {
|
||||
use_polarity: $cb("hc-use-polarity"),
|
||||
use_gpu: $cb("hc-use-gpu"),
|
||||
use_soft_score: $cb("hc-soft-score"),
|
||||
subpixel_lm: $cb("hc-subpixel-lm"),
|
||||
refine_pose_joint: $cb("hc-refine-joint"),
|
||||
pyramid_propagate: $cb("hc-pyr-propagate"),
|
||||
min_recall: $num("hc-min-recall", 0),
|
||||
nms_iou_threshold: $num("hc-nms-iou", 0.3),
|
||||
greediness: $num("hc-greediness", 0),
|
||||
coarse_stride: $int("hc-coarse-stride", 1),
|
||||
search_roi: search_roi,
|
||||
};
|
||||
}
|
||||
|
||||
function readAdvancedOverrides() {
|
||||
const out = {};
|
||||
for (const [key, , type] of ADV_PARAMS) {
|
||||
const v = document.getElementById(`adv-${key}`).value.trim();
|
||||
if (v === "") continue;
|
||||
out[key] = type === "int" ? parseInt(v, 10) : parseFloat(v);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// ---------- Image loading from folder ----------
|
||||
async function loadFromFolder(filename) {
|
||||
const r = await fetch("/load_from_folder", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ filename }),
|
||||
});
|
||||
if (!r.ok) throw new Error(await r.text());
|
||||
return await r.json();
|
||||
}
|
||||
|
||||
async function fetchImagesList() {
|
||||
const r = await fetch("/images");
|
||||
if (!r.ok) return { files: [], dir: "" };
|
||||
return await r.json();
|
||||
}
|
||||
|
||||
async function uploadToFolder(file) {
|
||||
const fd = new FormData();
|
||||
fd.append("file", file);
|
||||
const r = await fetch("/upload_to_folder", { method: "POST", body: fd });
|
||||
if (!r.ok) throw new Error(await r.text());
|
||||
return await r.json();
|
||||
}
|
||||
|
||||
async function uploadDxf(file) {
|
||||
// DXF: rasterizzato server-side in template grayscale (vedi pm2d/dxf.py)
|
||||
const fd = new FormData();
|
||||
fd.append("file", file);
|
||||
const r = await fetch("/upload_dxf", { method: "POST", body: fd });
|
||||
if (!r.ok) throw new Error(await r.text());
|
||||
return await r.json();
|
||||
}
|
||||
|
||||
async function refreshPickers() {
|
||||
const {files, dir} = await fetchImagesList();
|
||||
buildThumbPicker("picker-model", files, onSelectModel);
|
||||
buildThumbPicker("picker-scene", files, onSelectScene);
|
||||
return {files, dir};
|
||||
}
|
||||
|
||||
function buildThumbPicker(pickerId, files, onSelect) {
|
||||
const picker = document.getElementById(pickerId);
|
||||
const current = picker.querySelector(".picker-current");
|
||||
const list = picker.querySelector(".picker-list");
|
||||
const text = current.querySelector(".picker-text");
|
||||
// Rimuovi eventuale vecchia thumbnail
|
||||
const oldImg = current.querySelector("img");
|
||||
if (oldImg) oldImg.remove();
|
||||
list.innerHTML = "";
|
||||
|
||||
files.forEach((f) => {
|
||||
const item = document.createElement("div");
|
||||
item.className = "picker-item";
|
||||
const img = document.createElement("img");
|
||||
img.src = `/folder_image/${encodeURIComponent(f)}?w=120`;
|
||||
img.loading = "lazy";
|
||||
const name = document.createElement("span");
|
||||
name.className = "name"; name.textContent = f;
|
||||
item.appendChild(img); item.appendChild(name);
|
||||
item.addEventListener("click", () => {
|
||||
// Aggiorna la visual del "current"
|
||||
let thumb = current.querySelector("img");
|
||||
if (!thumb) {
|
||||
thumb = document.createElement("img");
|
||||
current.insertBefore(thumb, text);
|
||||
}
|
||||
thumb.src = `/folder_image/${encodeURIComponent(f)}?w=80`;
|
||||
text.textContent = f;
|
||||
picker.classList.remove("open");
|
||||
onSelect(f);
|
||||
});
|
||||
list.appendChild(item);
|
||||
});
|
||||
|
||||
current.onclick = () => {
|
||||
// Chiudi altri picker aperti
|
||||
document.querySelectorAll(".thumb-picker.open")
|
||||
.forEach((p) => { if (p !== picker) p.classList.remove("open"); });
|
||||
picker.classList.toggle("open");
|
||||
};
|
||||
}
|
||||
|
||||
// Close picker on outside click
|
||||
document.addEventListener("click", (e) => {
|
||||
if (!e.target.closest(".thumb-picker")) {
|
||||
document.querySelectorAll(".thumb-picker.open")
|
||||
.forEach((p) => p.classList.remove("open"));
|
||||
}
|
||||
});
|
||||
|
||||
function loadImage(src) {
|
||||
return new Promise((res, rej) => {
|
||||
const img = new Image();
|
||||
img.onload = () => res(img);
|
||||
img.onerror = rej;
|
||||
img.src = src;
|
||||
});
|
||||
}
|
||||
|
||||
async function onSelectModel(filename) {
|
||||
if (!filename) return;
|
||||
setStatus("Caricamento modello...");
|
||||
try {
|
||||
const meta = await loadFromFolder(filename);
|
||||
const img = await loadImage(`/image/${meta.id}/raw`);
|
||||
state.model = { id: meta.id, w: meta.width, h: meta.height, img };
|
||||
state.roi = null;
|
||||
state.polyPts = []; state.polyClosed = false; // B: scarta poligono stale
|
||||
document.getElementById("roi-info").textContent = "ROI: (nessuna)";
|
||||
setStatus(`Modello: ${filename} ${meta.width}x${meta.height} — trascina ROI`);
|
||||
renderModel();
|
||||
} catch (e) {
|
||||
setStatus(`Errore modello: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function onSelectScene(filename) {
|
||||
if (!filename) return;
|
||||
setStatus("Caricamento scena...");
|
||||
try {
|
||||
const meta = await loadFromFolder(filename);
|
||||
const img = await loadImage(`/image/${meta.id}/raw`);
|
||||
state.scene = { id: meta.id, w: meta.width, h: meta.height, img };
|
||||
state.matches = []; state.annotatedImg = null;
|
||||
setStatus(`Scena: ${filename} ${meta.width}x${meta.height}`);
|
||||
renderScene();
|
||||
} catch (e) {
|
||||
setStatus(`Errore scena: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- Rendering ----------
|
||||
function fitToCanvas(img, cw, ch) {
|
||||
const sc = Math.min(cw / img.width, ch / img.height);
|
||||
const dw = img.width * sc, dh = img.height * sc;
|
||||
return { sc, ox: (cw - dw) / 2, oy: (ch - dh) / 2, dw, dh };
|
||||
}
|
||||
|
||||
function renderModel() {
|
||||
const cnv = document.getElementById("c-model");
|
||||
const ctx = cnv.getContext("2d");
|
||||
ctx.fillStyle = "#141414";
|
||||
ctx.fillRect(0, 0, cnv.width, cnv.height);
|
||||
if (!state.model) return;
|
||||
const fit = fitToCanvas(state.model.img, cnv.width, cnv.height);
|
||||
state.model.scale = fit.sc;
|
||||
state.model.ox = fit.ox; state.model.oy = fit.oy;
|
||||
ctx.drawImage(state.model.img, fit.ox, fit.oy, fit.dw, fit.dh);
|
||||
if (state.roi && !state.polyMode) {
|
||||
const [x, y, w, h] = state.roi;
|
||||
ctx.strokeStyle = "#00ff80"; ctx.lineWidth = 2;
|
||||
ctx.strokeRect(fit.ox + x * fit.sc, fit.oy + y * fit.sc,
|
||||
w * fit.sc, h * fit.sc);
|
||||
}
|
||||
// ROI poligonale: path aperto giallo, chiuso verde con fill semitrasparente
|
||||
if (state.polyMode && state.polyPts.length > 0) {
|
||||
ctx.beginPath();
|
||||
state.polyPts.forEach(([px, py], i) => {
|
||||
const cx = fit.ox + px * fit.sc;
|
||||
const cy = fit.oy + py * fit.sc;
|
||||
if (i === 0) ctx.moveTo(cx, cy); else ctx.lineTo(cx, cy);
|
||||
});
|
||||
if (state.polyClosed) {
|
||||
ctx.closePath();
|
||||
ctx.fillStyle = "rgba(0, 255, 128, 0.18)";
|
||||
ctx.fill();
|
||||
ctx.strokeStyle = "#00ff80";
|
||||
} else {
|
||||
ctx.strokeStyle = "#ffff00";
|
||||
}
|
||||
ctx.lineWidth = 2;
|
||||
ctx.stroke();
|
||||
// Vertici come quadratini
|
||||
ctx.fillStyle = state.polyClosed ? "#00ff80" : "#ffff00";
|
||||
for (const [px, py] of state.polyPts) {
|
||||
ctx.fillRect(fit.ox + px * fit.sc - 2, fit.oy + py * fit.sc - 2, 4, 4);
|
||||
}
|
||||
}
|
||||
if (state.drag) {
|
||||
ctx.strokeStyle = "#ffff00";
|
||||
ctx.setLineDash([4, 2]); ctx.lineWidth = 2;
|
||||
ctx.strokeRect(
|
||||
Math.min(state.drag.x0, state.drag.x1),
|
||||
Math.min(state.drag.y0, state.drag.y1),
|
||||
Math.abs(state.drag.x1 - state.drag.x0),
|
||||
Math.abs(state.drag.y1 - state.drag.y0));
|
||||
ctx.setLineDash([]);
|
||||
}
|
||||
}
|
||||
|
||||
function renderScene() {
|
||||
const cnv = document.getElementById("c-scene");
|
||||
const ctx = cnv.getContext("2d");
|
||||
ctx.fillStyle = "#141414";
|
||||
ctx.fillRect(0, 0, cnv.width, cnv.height);
|
||||
const img = state.annotatedImg || (state.scene && state.scene.img);
|
||||
if (!img) return;
|
||||
const fit = fitToCanvas(img, cnv.width, cnv.height);
|
||||
ctx.drawImage(img, fit.ox, fit.oy, fit.dw, fit.dh);
|
||||
}
|
||||
|
||||
// ---------- ROI drag ----------
|
||||
function canvasPos(cnv, ev) {
|
||||
const r = cnv.getBoundingClientRect();
|
||||
return { x: ev.clientX - r.left, y: ev.clientY - r.top };
|
||||
}
|
||||
|
||||
function setupROI() {
|
||||
const cnv = document.getElementById("c-model");
|
||||
cnv.addEventListener("mousedown", (e) => {
|
||||
if (!state.model) return;
|
||||
if (state.polyMode) return; // poly mode: gestito da click/dblclick
|
||||
const p = canvasPos(cnv, e);
|
||||
state.drag = { x0: p.x, y0: p.y, x1: p.x, y1: p.y };
|
||||
renderModel();
|
||||
});
|
||||
// ROI poligonale: click aggiunge vertice, doppio click chiude
|
||||
cnv.addEventListener("click", (e) => {
|
||||
if (!state.model || !state.polyMode || state.polyClosed) return;
|
||||
const m = state.model;
|
||||
const p = canvasPos(cnv, e);
|
||||
const ix = (p.x - m.ox) / m.scale;
|
||||
const iy = (p.y - m.oy) / m.scale;
|
||||
if (ix < 0 || iy < 0 || ix > m.w || iy > m.h) return; // fuori immagine
|
||||
const last = state.polyPts[state.polyPts.length - 1];
|
||||
// Dedup: il dblclick genera anche 2 click ravvicinati
|
||||
if (last && Math.hypot(ix - last[0], iy - last[1]) < 3) return;
|
||||
state.polyPts.push([
|
||||
Math.max(0, Math.min(Math.round(ix), m.w - 1)),
|
||||
Math.max(0, Math.min(Math.round(iy), m.h - 1)),
|
||||
]);
|
||||
document.getElementById("roi-info").textContent =
|
||||
`Poligono: ${state.polyPts.length} vertici (doppio click o "Chiudi" per chiudere)`;
|
||||
renderModel();
|
||||
});
|
||||
cnv.addEventListener("dblclick", (e) => {
|
||||
if (!state.polyMode) return;
|
||||
e.preventDefault();
|
||||
closePoly();
|
||||
});
|
||||
cnv.addEventListener("mousemove", (e) => {
|
||||
if (!state.drag) return;
|
||||
const p = canvasPos(cnv, e);
|
||||
state.drag.x1 = p.x; state.drag.y1 = p.y;
|
||||
renderModel();
|
||||
});
|
||||
cnv.addEventListener("mouseup", () => {
|
||||
if (!state.drag || !state.model) return;
|
||||
const d = state.drag; state.drag = null;
|
||||
if (Math.abs(d.x1 - d.x0) < 4 || Math.abs(d.y1 - d.y0) < 4) return;
|
||||
const m = state.model;
|
||||
const ix0 = Math.round((Math.min(d.x0, d.x1) - m.ox) / m.scale);
|
||||
const iy0 = Math.round((Math.min(d.y0, d.y1) - m.oy) / m.scale);
|
||||
const iw = Math.round(Math.abs(d.x1 - d.x0) / m.scale);
|
||||
const ih = Math.round(Math.abs(d.y1 - d.y0) / m.scale);
|
||||
const cx0 = Math.max(0, Math.min(ix0, m.w - 1));
|
||||
const cy0 = Math.max(0, Math.min(iy0, m.h - 1));
|
||||
const cw = Math.max(1, Math.min(iw, m.w - cx0));
|
||||
const ch = Math.max(1, Math.min(ih, m.h - cy0));
|
||||
state.roi = [cx0, cy0, cw, ch];
|
||||
document.getElementById("roi-info").textContent =
|
||||
`ROI: ${cw}x${ch} @ (${cx0}, ${cy0})`;
|
||||
renderModel();
|
||||
});
|
||||
}
|
||||
|
||||
// ---------- ROI poligonale ----------
|
||||
function closePoly() {
|
||||
if (!state.polyMode || state.polyClosed) return;
|
||||
if (state.polyPts.length < 3) {
|
||||
setStatus("Servono almeno 3 vertici per chiudere il poligono");
|
||||
return;
|
||||
}
|
||||
state.polyClosed = true;
|
||||
// ROI = bounding box del poligono (il server riceve anche roi_poly)
|
||||
const xs = state.polyPts.map((p) => p[0]);
|
||||
const ys = state.polyPts.map((p) => p[1]);
|
||||
const x0 = Math.min(...xs), y0 = Math.min(...ys);
|
||||
const w = Math.max(...xs) - x0, h = Math.max(...ys) - y0;
|
||||
state.roi = [x0, y0, Math.max(1, w), Math.max(1, h)];
|
||||
document.getElementById("roi-info").textContent =
|
||||
`Poligono: ${state.polyPts.length} vertici, bbox ${w}x${h} @ (${x0}, ${y0})`;
|
||||
renderModel();
|
||||
}
|
||||
|
||||
function resetPoly() {
|
||||
state.polyPts = [];
|
||||
state.polyClosed = false;
|
||||
state.roi = null;
|
||||
document.getElementById("roi-info").textContent = state.polyMode
|
||||
? "Poligono: clicca sul modello per aggiungere vertici"
|
||||
: "ROI: (nessuna)";
|
||||
renderModel();
|
||||
}
|
||||
|
||||
function getRoiPoly() {
|
||||
// Poligono valido solo se in modalità poly e chiuso
|
||||
return (state.polyMode && state.polyClosed && state.polyPts.length >= 3)
|
||||
? state.polyPts : null;
|
||||
}
|
||||
|
||||
// ---------- Match action ----------
|
||||
async function doMatchRecipe() {
|
||||
if (!state.scene) { setStatus("Carica scena"); return; }
|
||||
setStatus(`Match ricetta ${state.active_recipe}...`);
|
||||
const hc = readHalconFlags();
|
||||
const body = {
|
||||
recipe: state.active_recipe,
|
||||
scene_id: state.scene.id,
|
||||
min_score: parseFloat(document.getElementById("p-min-score").value),
|
||||
max_matches: parseInt(document.getElementById("p-max-matches").value, 10),
|
||||
verify_threshold: 0.50,
|
||||
...hc,
|
||||
};
|
||||
const r = await fetch("/match_recipe", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
if (!r.ok) { setStatus(`Errore: ${await r.text()}`); return; }
|
||||
const data = await r.json();
|
||||
state.matches = data.matches;
|
||||
// C: salva tutto per "Esporta JSON"
|
||||
state.lastMatch = {
|
||||
endpoint: "/match_recipe", params: body, response: data,
|
||||
image_id: state.scene.id,
|
||||
};
|
||||
document.getElementById("btn-export-json").disabled = false;
|
||||
state.annotatedImg = await loadImage(
|
||||
`/image/${data.annotated_id}/raw?t=${Date.now()}`);
|
||||
renderScene();
|
||||
renderLegend();
|
||||
document.getElementById("t-train").textContent = "—";
|
||||
document.getElementById("t-find").textContent = `${data.find_time.toFixed(2)}s`;
|
||||
document.getElementById("t-var").textContent = data.num_variants;
|
||||
document.getElementById("t-match").textContent = data.matches.length;
|
||||
renderDiag(data.diag, data.matches.length);
|
||||
setStatus(`${data.matches.length} match trovati (ricetta ${state.active_recipe})`);
|
||||
}
|
||||
|
||||
async function doMatch() {
|
||||
// Path V: ricetta caricata → bypass training, solo find su scena
|
||||
if (state.active_recipe) {
|
||||
return doMatchRecipe();
|
||||
}
|
||||
if (!state.model) { setStatus("Carica modello"); return; }
|
||||
if (!state.scene) { setStatus("Carica scena"); return; }
|
||||
if (state.polyMode && !state.polyClosed) {
|
||||
setStatus("Chiudi il poligono (doppio click o bottone Chiudi)"); return;
|
||||
}
|
||||
if (!state.roi) { setStatus("Seleziona ROI sul modello"); return; }
|
||||
const roiPoly = getRoiPoly();
|
||||
const user = readUserParams();
|
||||
const adv = readAdvancedOverrides();
|
||||
setStatus("Match in corso...");
|
||||
|
||||
// Se utente ha fornito override avanzati → usa /match (tecnico)
|
||||
// altrimenti /match_simple (operator mode)
|
||||
const hasAdv = Object.keys(adv).length > 0;
|
||||
const url = hasAdv ? "/match" : "/match_simple";
|
||||
let body;
|
||||
if (hasAdv) {
|
||||
// Merge simple → tecnici base, poi overrides
|
||||
const SYM_MAP = {invariante:0, nessuna:360, bilaterale:180, rot_3:120,
|
||||
rot_4:90, rot_6:60, rot_8:45};
|
||||
const SCALE_MAP = {fissa:[1,1,0.1], mini:[0.9,1.1,0.05],
|
||||
medio:[0.75,1.25,0.05], max:[0.5,1.5,0.05]};
|
||||
const PREC_MAP = {veloce:10, normale:5, preciso:2};
|
||||
// Allineato a FILTRO_FP_MAP server-side (server.py)
|
||||
const FP_MAP = {off:0, leggero:0.30, medio:0.50, forte:0.70};
|
||||
const [smin, smax, sstep] = SCALE_MAP[user.scala];
|
||||
// NB: SYM_MAP[invariante]=0 e' valido (zero rotazioni). Uso ?? per
|
||||
// distinguere "chiave mancante" da "valore zero": altrimenti 0 || 360
|
||||
// collassa invariante a 360 = bug "simmetria non ha effetto".
|
||||
const angMax = SYM_MAP[user.simmetria] ?? 360;
|
||||
body = {
|
||||
model_id: state.model.id, scene_id: state.scene.id, roi: state.roi,
|
||||
roi_poly: roiPoly,
|
||||
angle_min: 0, angle_max: angMax,
|
||||
angle_step: PREC_MAP[user.precisione] ?? 5,
|
||||
scale_min: smin, scale_max: smax, scale_step: sstep,
|
||||
min_score: user.min_score, max_matches: user.max_matches,
|
||||
num_features: adv.num_features ?? 96,
|
||||
weak_grad: adv.weak_grad ?? 30,
|
||||
strong_grad: adv.strong_grad ?? 60,
|
||||
spread_radius: adv.spread_radius ?? 5,
|
||||
pyramid_levels: adv.pyramid_levels ?? 3,
|
||||
verify_threshold: adv.verify_threshold ?? (FP_MAP[user.filtro_fp] ?? 0.50),
|
||||
nms_radius: adv.nms_radius ?? 0,
|
||||
};
|
||||
} else {
|
||||
body = {
|
||||
model_id: state.model.id, scene_id: state.scene.id, roi: state.roi,
|
||||
roi_poly: roiPoly,
|
||||
...user,
|
||||
};
|
||||
}
|
||||
|
||||
const r = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
if (!r.ok) {
|
||||
setStatus(`Errore: ${await r.text()}`); return;
|
||||
}
|
||||
const data = await r.json();
|
||||
state.matches = data.matches;
|
||||
// C: salva tutto per "Esporta JSON"
|
||||
state.lastMatch = {
|
||||
endpoint: url, params: body, response: data,
|
||||
image_id: state.scene.id,
|
||||
};
|
||||
document.getElementById("btn-export-json").disabled = false;
|
||||
state.annotatedImg = await loadImage(
|
||||
`/image/${data.annotated_id}/raw?t=${Date.now()}`);
|
||||
renderScene();
|
||||
renderLegend();
|
||||
document.getElementById("t-train").textContent = `${data.train_time.toFixed(2)}s`;
|
||||
document.getElementById("t-find").textContent = `${data.find_time.toFixed(2)}s`;
|
||||
document.getElementById("t-var").textContent = data.num_variants;
|
||||
document.getElementById("t-match").textContent = data.matches.length;
|
||||
renderDiag(data.diag, data.matches.length);
|
||||
setStatus(`${data.matches.length} match trovati${hasAdv ? " (avanzato)" : ""}`);
|
||||
}
|
||||
|
||||
function renderLegend() {
|
||||
const el = document.getElementById("legend");
|
||||
el.innerHTML = "";
|
||||
state.matches.forEach((m, i) => {
|
||||
const div = document.createElement("div");
|
||||
div.className = "legend-item";
|
||||
const dot = document.createElement("span");
|
||||
dot.className = "legend-dot";
|
||||
dot.style.background = PALETTE[i % PALETTE.length];
|
||||
div.appendChild(dot);
|
||||
const txt = document.createElement("span");
|
||||
txt.textContent = `#${i+1} cx=${Math.round(m.cx)} cy=${Math.round(m.cy)} `
|
||||
+ `${m.angle_deg.toFixed(1)}° s=${m.scale.toFixed(2)} `
|
||||
+ `score=${m.score.toFixed(3)}`;
|
||||
div.appendChild(txt);
|
||||
el.appendChild(div);
|
||||
});
|
||||
}
|
||||
|
||||
function setStatus(s) {
|
||||
document.getElementById("status").textContent = s;
|
||||
}
|
||||
|
||||
// ---------- C: Export JSON risultati ----------
|
||||
function exportMatchJSON() {
|
||||
if (!state.lastMatch) {
|
||||
alert("Nessun match da esportare: esegui prima un MATCH.");
|
||||
return;
|
||||
}
|
||||
const lm = state.lastMatch;
|
||||
const payload = {
|
||||
timestamp: new Date().toISOString(),
|
||||
image_id: lm.image_id,
|
||||
endpoint: lm.endpoint,
|
||||
params: lm.params,
|
||||
matches: lm.response.matches.map((m) => ({
|
||||
cx: m.cx, cy: m.cy, angle_deg: m.angle_deg,
|
||||
scale: m.scale, score: m.score, bbox: m.bbox_poly,
|
||||
})),
|
||||
train_time: lm.response.train_time,
|
||||
find_time: lm.response.find_time,
|
||||
num_variants: lm.response.num_variants,
|
||||
};
|
||||
const blob = new Blob([JSON.stringify(payload, null, 2)],
|
||||
{ type: "application/json" });
|
||||
const a = document.createElement("a");
|
||||
a.href = URL.createObjectURL(blob);
|
||||
const ts = new Date().toISOString().replace(/[:.]/g, "-");
|
||||
a.download = `pm2d_match_${ts}.json`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
a.remove();
|
||||
URL.revokeObjectURL(a.href);
|
||||
}
|
||||
|
||||
// ---------- Init ----------
|
||||
// ---------- Edge preview (clean rumore) ----------
|
||||
let _epDebounce = null;
|
||||
let _epLastImg = null;
|
||||
|
||||
async function fetchEdgePreview() {
|
||||
if (!state.model || !state.roi) {
|
||||
document.getElementById("edge-preview-info").textContent =
|
||||
"Disegna prima la ROI sul modello";
|
||||
return;
|
||||
}
|
||||
const body = {
|
||||
model_id: state.model.id,
|
||||
roi: state.roi,
|
||||
weak_grad: parseFloat(document.getElementById("ep-weak").value),
|
||||
strong_grad: parseFloat(document.getElementById("ep-strong").value),
|
||||
num_features: parseInt(document.getElementById("ep-nf").value, 10),
|
||||
min_feature_spacing: parseInt(document.getElementById("ep-sp").value, 10),
|
||||
use_polarity: document.getElementById("ep-pol").checked,
|
||||
};
|
||||
try {
|
||||
const r = await fetch("/preview_edges", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
if (!r.ok) throw new Error(await r.text());
|
||||
const j = await r.json();
|
||||
_epLastImg = await loadImage(`/image/${j.preview_id}/raw?t=${Date.now()}`);
|
||||
drawEdgePreview();
|
||||
const ucs = j.ucs_baricentro
|
||||
? ` | UCS=(${j.ucs_baricentro.cx},${j.ucs_baricentro.cy})`
|
||||
: "";
|
||||
document.getElementById("edge-preview-info").innerHTML =
|
||||
`<b>${j.n_features}</b> feature scelte (di ${j.n_edge_after_hysteresis} edge totali)<br>` +
|
||||
`mag: max=${j.mag_max.toFixed(0)} p50=${j.mag_p50.toFixed(0)} ` +
|
||||
`p85=${j.mag_p85.toFixed(0)}${ucs}`;
|
||||
} catch (e) {
|
||||
document.getElementById("edge-preview-info").textContent =
|
||||
`Errore preview: ${e.message}`;
|
||||
}
|
||||
}
|
||||
|
||||
function drawEdgePreview() {
|
||||
const cnv = document.getElementById("c-edge-preview");
|
||||
if (!_epLastImg) return;
|
||||
const ctx = cnv.getContext("2d");
|
||||
// Fit-contain
|
||||
const r = Math.min(cnv.width / _epLastImg.width,
|
||||
cnv.height / _epLastImg.height);
|
||||
const w = _epLastImg.width * r;
|
||||
const h = _epLastImg.height * r;
|
||||
const ox = (cnv.width - w) / 2;
|
||||
const oy = (cnv.height - h) / 2;
|
||||
ctx.fillStyle = "#000"; ctx.fillRect(0, 0, cnv.width, cnv.height);
|
||||
ctx.imageSmoothingEnabled = false;
|
||||
ctx.drawImage(_epLastImg, ox, oy, w, h);
|
||||
}
|
||||
|
||||
function scheduleEdgePreview() {
|
||||
if (_epDebounce) clearTimeout(_epDebounce);
|
||||
_epDebounce = setTimeout(fetchEdgePreview, 200);
|
||||
}
|
||||
|
||||
function bindEdgePreviewControls() {
|
||||
const slid = (id, valEl) => {
|
||||
const el = document.getElementById(id);
|
||||
const v = document.getElementById(valEl);
|
||||
el.addEventListener("input", () => {
|
||||
v.textContent = el.value;
|
||||
scheduleEdgePreview();
|
||||
});
|
||||
};
|
||||
slid("ep-weak", "ep-weak-v");
|
||||
slid("ep-strong", "ep-strong-v");
|
||||
slid("ep-nf", "ep-nf-v");
|
||||
slid("ep-sp", "ep-sp-v");
|
||||
document.getElementById("ep-pol").addEventListener("change",
|
||||
scheduleEdgePreview);
|
||||
// Auto-refresh quando il pannello viene aperto
|
||||
document.getElementById("edge-preview-panel").addEventListener("toggle",
|
||||
(e) => { if (e.target.open) fetchEdgePreview(); });
|
||||
document.getElementById("btn-edge-apply").addEventListener("click", () => {
|
||||
// Copia i valori correnti nei campi avanzati
|
||||
const map = {
|
||||
"ep-weak": "adv-weak_grad",
|
||||
"ep-strong": "adv-strong_grad",
|
||||
"ep-nf": "adv-num_features",
|
||||
"ep-sp": "adv-min_feature_spacing",
|
||||
};
|
||||
for (const [src, dst] of Object.entries(map)) {
|
||||
const dstEl = document.getElementById(dst);
|
||||
if (dstEl) dstEl.value = document.getElementById(src).value;
|
||||
}
|
||||
// use_polarity: alla checkbox della modalita Halcon
|
||||
const polCb = document.getElementById("hc-use-polarity");
|
||||
if (polCb) polCb.checked = document.getElementById("ep-pol").checked;
|
||||
// Apri pannello Avanzate per feedback
|
||||
const advDetails = document.querySelectorAll("#col-params details");
|
||||
advDetails.forEach((d) => { d.open = true; });
|
||||
alert("Parametri edge applicati. Esegui MATCH per usare i valori scelti.");
|
||||
});
|
||||
}
|
||||
|
||||
// ---------- CC: Diagnostica match ----------
|
||||
function renderDiag(diag, n_matches) {
|
||||
const el = document.getElementById("diag-content");
|
||||
if (!diag) {
|
||||
el.innerHTML = '<em style="color:#888">Diagnostica non disponibile</em>';
|
||||
return;
|
||||
}
|
||||
const dropTotal = (diag.drop_ncc_low || 0) + (diag.drop_min_score_post_avg || 0)
|
||||
+ (diag.drop_recall_low || 0) + (diag.drop_bbox_out_of_scene || 0)
|
||||
+ (diag.drop_nms_iou || 0);
|
||||
// Hint contestuali se 0 match
|
||||
let hint = "";
|
||||
if (n_matches === 0) {
|
||||
if (diag.n_after_pre_nms === 0) {
|
||||
hint = `<div style="color:#f88; margin-top:6px">⚠ Nessun candidato sopra soglia.
|
||||
Prova: ↓ <b>min_score</b> o ↓ <b>top_thresh</b> (currently ${diag.top_thresh_used.toFixed(2)})</div>`;
|
||||
} else if (diag.drop_ncc_low > 0 && dropTotal === diag.drop_ncc_low) {
|
||||
hint = `<div style="color:#f88; margin-top:6px">⚠ ${diag.drop_ncc_low} candidati droppati da NCC.
|
||||
Prova: ↓ <b>verify_threshold</b> (filtro_fp più leggero)</div>`;
|
||||
} else if (diag.drop_min_score_post_avg > 0) {
|
||||
hint = `<div style="color:#f88; margin-top:6px">⚠ ${diag.drop_min_score_post_avg} match sotto min_score post-NCC.
|
||||
Prova: ↓ <b>min_score</b></div>`;
|
||||
} else if (diag.drop_recall_low > 0) {
|
||||
hint = `<div style="color:#f88; margin-top:6px">⚠ ${diag.drop_recall_low} match con recall < ${diag.min_recall_used}.
|
||||
Prova: ↓ <b>min_recall</b></div>`;
|
||||
} else if (diag.drop_bbox_out_of_scene > 0) {
|
||||
hint = `<div style="color:#f88; margin-top:6px">⚠ ${diag.drop_bbox_out_of_scene} match con bbox fuori scena.
|
||||
Centro derivato male: aumenta <b>min_score</b> o restringi <b>search_roi</b></div>`;
|
||||
}
|
||||
}
|
||||
const flags = [];
|
||||
if (diag.use_polarity) flags.push("polarity");
|
||||
if (diag.use_soft_score) flags.push("soft");
|
||||
if (diag.subpixel_lm) flags.push("subpix-LM");
|
||||
el.innerHTML = `
|
||||
<div><b>Pipeline pruning:</b></div>
|
||||
<div>varianti: ${diag.n_variants_total} → top_eval=${diag.n_variants_top_evaluated}
|
||||
→ top_pass=${diag.n_variants_top_passed} → full_eval=${diag.n_variants_full_evaluated}</div>
|
||||
<div><b>Candidati:</b> raw=${diag.n_raw_candidates}
|
||||
→ pre_nms=${diag.n_after_pre_nms} → final=${diag.n_final}</div>
|
||||
<div><b>Drop reasons:</b> NCC=${diag.drop_ncc_low}, score=${diag.drop_min_score_post_avg},
|
||||
recall=${diag.drop_recall_low}, bbox=${diag.drop_bbox_out_of_scene}, NMS=${diag.drop_nms_iou}</div>
|
||||
<div><b>Soglie:</b> top=${diag.top_thresh_used.toFixed(2)},
|
||||
min_score=${diag.min_score_used.toFixed(2)},
|
||||
NCC=${diag.verify_threshold_used.toFixed(2)},
|
||||
recall=${diag.min_recall_used.toFixed(2)}</div>
|
||||
${flags.length ? `<div><b>Flag attivi:</b> ${flags.join(", ")}</div>` : ""}
|
||||
${hint}
|
||||
`;
|
||||
// Auto-apri pannello se 0 match (segnala problema)
|
||||
if (n_matches === 0) {
|
||||
document.getElementById("diag-panel").open = true;
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- Auto-tune (Halcon-style) ----------
|
||||
async function doAutoTune() {
|
||||
if (!state.model || !state.roi) {
|
||||
alert("Seleziona modello e disegna ROI prima di Auto-tune.");
|
||||
return;
|
||||
}
|
||||
const status = document.getElementById("status");
|
||||
status.textContent = "Analisi ROI in corso...";
|
||||
try {
|
||||
const r = await fetch("/auto_tune", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
model_id: state.model.id,
|
||||
roi: state.roi,
|
||||
}),
|
||||
});
|
||||
if (!r.ok) throw new Error(await r.text());
|
||||
const t = await r.json();
|
||||
// Applica ai campi avanzati (override automatico)
|
||||
for (const [key] of ADV_PARAMS) {
|
||||
const el = document.getElementById(`adv-${key}`);
|
||||
if (el && t[key] !== undefined) el.value = String(t[key]);
|
||||
}
|
||||
// Espandi la sezione Avanzate per mostrare i valori applicati
|
||||
const advDetails = document.querySelector("#col-params details:last-of-type");
|
||||
if (advDetails) advDetails.open = true;
|
||||
// Feedback diagnostico
|
||||
const lines = [
|
||||
`weak/strong: ${t.weak_grad} / ${t.strong_grad}`,
|
||||
`feature: ${t.num_features}, piramide: ${t.pyramid_levels}`,
|
||||
`angle: [${t.angle_min}..${t.angle_max}]@${t.angle_step}°`,
|
||||
];
|
||||
if (t._symmetry_order > 1) {
|
||||
lines.push(`simmetria rotaz. ${t._symmetry_order}x (conf ${t._symmetry_conf})`);
|
||||
}
|
||||
if (t._self_score !== undefined) {
|
||||
lines.push(`self-validation: ${t._validation}`);
|
||||
}
|
||||
status.textContent = `Auto-tune OK — ${lines[0]}`;
|
||||
alert("Auto-tune completato:\n\n" + lines.join("\n"));
|
||||
} catch (e) {
|
||||
status.textContent = `Auto-tune errore: ${e.message}`;
|
||||
alert(`Errore auto-tune: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- V: Recipe load/list/unload ----------
|
||||
async function refreshRecipeList() {
|
||||
try {
|
||||
const r = await fetch("/recipes");
|
||||
if (!r.ok) return;
|
||||
const j = await r.json();
|
||||
const sel = document.getElementById("hc-recipe-list");
|
||||
const cur = sel.value;
|
||||
sel.innerHTML = '<option value="">— ricette disponibili —</option>';
|
||||
for (const f of j.files) {
|
||||
const o = document.createElement("option");
|
||||
o.value = f.name;
|
||||
o.textContent = `${f.name} (${(f.size / 1024).toFixed(1)} KB)`;
|
||||
sel.appendChild(o);
|
||||
}
|
||||
if (cur) sel.value = cur;
|
||||
} catch (e) { /* silent */ }
|
||||
}
|
||||
|
||||
async function loadRecipe() {
|
||||
const sel = document.getElementById("hc-recipe-list");
|
||||
const name = sel.value;
|
||||
if (!name) {
|
||||
alert("Seleziona una ricetta dalla lista.");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const r = await fetch(`/recipes/${encodeURIComponent(name)}/load`, {
|
||||
method: "POST",
|
||||
});
|
||||
if (!r.ok) throw new Error(await r.text());
|
||||
const j = await r.json();
|
||||
state.active_recipe = j.name;
|
||||
document.getElementById("recipe-status").textContent =
|
||||
`Caricata: ${j.name} — ${j.n_variants} varianti, ` +
|
||||
`${j.template_size[0]}x${j.template_size[1]} px` +
|
||||
(j.use_polarity ? " (polarity)" : "");
|
||||
document.getElementById("recipe-status").style.color = "#0c0";
|
||||
document.getElementById("btn-unload-recipe").disabled = false;
|
||||
} catch (e) {
|
||||
alert(`Errore caricamento: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
function unloadRecipe() {
|
||||
state.active_recipe = null;
|
||||
document.getElementById("recipe-status").textContent = "Nessuna ricetta caricata";
|
||||
document.getElementById("recipe-status").style.color = "#888";
|
||||
document.getElementById("btn-unload-recipe").disabled = true;
|
||||
}
|
||||
|
||||
// ---------- V: Save recipe ----------
|
||||
async function saveRecipe() {
|
||||
if (!state.model || !state.roi) {
|
||||
alert("Seleziona modello e disegna ROI prima di salvare la ricetta.");
|
||||
return;
|
||||
}
|
||||
const name = document.getElementById("hc-recipe-name").value.trim();
|
||||
if (!name) {
|
||||
alert("Inserisci un nome per la ricetta.");
|
||||
return;
|
||||
}
|
||||
const user = readUserParams();
|
||||
const body = {
|
||||
model_id: state.model.id,
|
||||
scene_id: state.scene?.id || state.model.id,
|
||||
roi: state.roi,
|
||||
roi_poly: getRoiPoly(),
|
||||
tipo: user.tipo,
|
||||
simmetria: user.simmetria,
|
||||
scala: user.scala,
|
||||
precisione: user.precisione,
|
||||
use_polarity: user.use_polarity,
|
||||
use_gpu: user.use_gpu,
|
||||
edge_weak_grad: user.edge_weak_grad,
|
||||
edge_strong_grad: user.edge_strong_grad,
|
||||
edge_num_features: user.edge_num_features,
|
||||
edge_min_feature_spacing: user.edge_min_feature_spacing,
|
||||
name: name,
|
||||
};
|
||||
try {
|
||||
const r = await fetch("/recipes", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
if (!r.ok) throw new Error(await r.text());
|
||||
const j = await r.json();
|
||||
alert(`Ricetta salvata: ${j.name}\n${j.n_variants} varianti, ${j.size} bytes`);
|
||||
refreshRecipeList();
|
||||
} catch (e) {
|
||||
alert(`Errore salvataggio: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener("DOMContentLoaded", async () => {
|
||||
buildAdvancedForm();
|
||||
setupROI();
|
||||
// Popola picker immagini da IMAGES_DIR (con thumbnail)
|
||||
const {files, dir} = await refreshPickers();
|
||||
if (files.length === 0) {
|
||||
setStatus(`Nessuna immagine in ${dir} (carica file o configura IMAGES_DIR)`);
|
||||
} else {
|
||||
setStatus(`${files.length} immagini in ${dir}`);
|
||||
}
|
||||
|
||||
// Upload file nella folder
|
||||
const upEl = document.getElementById("file-upload");
|
||||
upEl.addEventListener("change", async (e) => {
|
||||
const f = e.target.files[0];
|
||||
if (!f) return;
|
||||
// A: file DXF → rasterizza server-side e usa direttamente come modello
|
||||
if (f.name.toLowerCase().endsWith(".dxf")) {
|
||||
setStatus(`Rasterizzazione DXF ${f.name}...`);
|
||||
try {
|
||||
const meta = await uploadDxf(f);
|
||||
const img = await loadImage(`/image/${meta.id}/raw`);
|
||||
state.model = { id: meta.id, w: meta.width, h: meta.height, img };
|
||||
state.roi = null;
|
||||
resetPoly();
|
||||
setStatus(`DXF ${f.name} rasterizzato ` +
|
||||
`${meta.width}x${meta.height} — disegna ROI sul modello`);
|
||||
renderModel();
|
||||
} catch (err) {
|
||||
setStatus(`Errore DXF: ${err.message}`);
|
||||
}
|
||||
e.target.value = "";
|
||||
return;
|
||||
}
|
||||
setStatus(`Caricamento ${f.name} nella cartella...`);
|
||||
try {
|
||||
const res = await uploadToFolder(f);
|
||||
await refreshPickers();
|
||||
setStatus(`Salvato come ${res.saved_as} (${res.files.length} file totali)`);
|
||||
} catch (err) {
|
||||
setStatus(`Errore upload: ${err.message}`);
|
||||
}
|
||||
e.target.value = ""; // consente re-upload stesso file
|
||||
});
|
||||
document.getElementById("btn-match").addEventListener("click", doMatch);
|
||||
document.getElementById("btn-autotune").addEventListener("click", doAutoTune);
|
||||
// B: ROI poligonale (toggle + chiudi + reset)
|
||||
document.getElementById("roi-poly-toggle").addEventListener("change", (e) => {
|
||||
state.polyMode = e.target.checked;
|
||||
document.getElementById("btn-poly-close").disabled = !state.polyMode;
|
||||
document.getElementById("btn-poly-reset").disabled = !state.polyMode;
|
||||
resetPoly();
|
||||
});
|
||||
document.getElementById("btn-poly-close").addEventListener("click", closePoly);
|
||||
document.getElementById("btn-poly-reset").addEventListener("click", resetPoly);
|
||||
// C: export JSON ultimo match
|
||||
document.getElementById("btn-export-json").addEventListener("click",
|
||||
exportMatchJSON);
|
||||
document.getElementById("btn-save-recipe").addEventListener("click",
|
||||
saveRecipe);
|
||||
document.getElementById("btn-load-recipe").addEventListener("click",
|
||||
loadRecipe);
|
||||
document.getElementById("btn-unload-recipe").addEventListener("click",
|
||||
unloadRecipe);
|
||||
refreshRecipeList();
|
||||
bindEdgePreviewControls();
|
||||
const slider = document.getElementById("p-min-score");
|
||||
slider.addEventListener("input", (e) => {
|
||||
document.getElementById("v-score").textContent =
|
||||
parseFloat(e.target.value).toFixed(2);
|
||||
});
|
||||
renderModel();
|
||||
renderScene();
|
||||
});
|
||||
@@ -0,0 +1,285 @@
|
||||
<!doctype html>
|
||||
<html lang="it">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Pattern Matching 2D</title>
|
||||
<link rel="stylesheet" href="/static/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<h1>Pattern Matching 2D</h1>
|
||||
<div class="toolbar">
|
||||
<label class="tb-label">Modello:</label>
|
||||
<div class="thumb-picker" id="picker-model">
|
||||
<div class="picker-current" tabindex="0">
|
||||
<span class="picker-text">-- seleziona --</span>
|
||||
<span class="caret">▾</span>
|
||||
</div>
|
||||
<div class="picker-list"></div>
|
||||
</div>
|
||||
<label class="tb-label">Scena:</label>
|
||||
<div class="thumb-picker" id="picker-scene">
|
||||
<div class="picker-current" tabindex="0">
|
||||
<span class="picker-text">-- seleziona --</span>
|
||||
<span class="caret">▾</span>
|
||||
</div>
|
||||
<div class="picker-list"></div>
|
||||
</div>
|
||||
<button class="btn btn-go" id="btn-match">▶ MATCH</button>
|
||||
<button class="btn" id="btn-autotune"
|
||||
title="Analizza ROI e derivata parametri ottimali (Halcon-style)">
|
||||
⚙ Auto-tune
|
||||
</button>
|
||||
<label class="btn" title="Carica nuovo file nella cartella immagini (immagine o DXF)">
|
||||
⬆ Carica file
|
||||
<input type="file" id="file-upload" accept="image/*,.dxf" hidden>
|
||||
</label>
|
||||
<span id="status">Seleziona modello, disegna ROI, seleziona scena</span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<section class="col" id="col-model">
|
||||
<h2>MODELLO</h2>
|
||||
<div class="canvas-wrap">
|
||||
<canvas id="c-model" width="380" height="420"></canvas>
|
||||
</div>
|
||||
<div id="roi-info">ROI: (nessuna)</div>
|
||||
<div id="roi-poly-bar" style="display:flex; gap:6px; align-items:center; margin-top:6px">
|
||||
<label style="display:flex; gap:4px; align-items:center; font-size:12px; cursor:pointer">
|
||||
<input type="checkbox" id="roi-poly-toggle"> ROI poligonale
|
||||
</label>
|
||||
<button class="btn" id="btn-poly-close" type="button" disabled
|
||||
title="Chiude il poligono (equivale al doppio click)">Chiudi</button>
|
||||
<button class="btn" id="btn-poly-reset" type="button" disabled
|
||||
title="Cancella i vertici del poligono">Reset</button>
|
||||
</div>
|
||||
<details id="edge-preview-panel" style="margin-top:10px">
|
||||
<summary>🔬 Anteprima edge / pulizia rumore</summary>
|
||||
<div style="font-size:11px; color:#aaa; margin:4px 0">
|
||||
Regola le soglie per togliere edge spuri (sporcizie). UCS rosso/verde
|
||||
sul baricentro feature.
|
||||
</div>
|
||||
<div class="ep-grid">
|
||||
<label class="ep-row">weak_grad <span id="ep-weak-v">30</span>
|
||||
<input type="range" id="ep-weak" min="5" max="200" value="30" step="1">
|
||||
</label>
|
||||
<label class="ep-row">strong_grad <span id="ep-strong-v">60</span>
|
||||
<input type="range" id="ep-strong" min="10" max="400" value="60" step="1">
|
||||
</label>
|
||||
<label class="ep-row">num_features <span id="ep-nf-v">96</span>
|
||||
<input type="range" id="ep-nf" min="16" max="300" value="96" step="1">
|
||||
</label>
|
||||
<label class="ep-row">spacing <span id="ep-sp-v">3</span>
|
||||
<input type="range" id="ep-sp" min="1" max="15" value="3" step="1">
|
||||
</label>
|
||||
<label class="ep-row" style="flex-direction:row; gap:6px">
|
||||
<input type="checkbox" id="ep-pol"> polarity
|
||||
</label>
|
||||
<button class="btn" id="btn-edge-apply" type="button"
|
||||
style="grid-column:1/-1">
|
||||
✓ Applica ai parametri Avanzate
|
||||
</button>
|
||||
</div>
|
||||
<div class="canvas-wrap" style="margin-top:6px">
|
||||
<canvas id="c-edge-preview" width="380" height="380"></canvas>
|
||||
</div>
|
||||
<div id="edge-preview-info" style="font-size:11px; color:#888; margin-top:4px">
|
||||
Disegna ROI e apri questo pannello per generare anteprima
|
||||
</div>
|
||||
</details>
|
||||
</section>
|
||||
|
||||
<section class="col" id="col-scene">
|
||||
<h2>SCENA</h2>
|
||||
<div class="canvas-wrap">
|
||||
<canvas id="c-scene" width="820" height="620"></canvas>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="col" id="col-params">
|
||||
<h2>IMPOSTAZIONI</h2>
|
||||
|
||||
<div class="field">
|
||||
<label>Tipo modello</label>
|
||||
<select id="p-tipo">
|
||||
<option value="intero">Oggetto intero</option>
|
||||
<option value="parziale">Parte di oggetto</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label>Simmetria</label>
|
||||
<select id="p-simmetria">
|
||||
<option value="nessuna" selected>Nessuna (0..360°)</option>
|
||||
<option value="invariante">Invariante (cerchi — no rotazione)</option>
|
||||
<option value="bilaterale">Bilaterale (speculare 180°)</option>
|
||||
<option value="rot_3">Rotazionale 3× (120°)</option>
|
||||
<option value="rot_4">Rotazionale 4× (90°)</option>
|
||||
<option value="rot_6">Rotazionale 6× (60°)</option>
|
||||
<option value="rot_8">Rotazionale 8× (45°)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label>Variazione scala</label>
|
||||
<select id="p-scala">
|
||||
<option value="fissa">Fissa (setup calibrato)</option>
|
||||
<option value="mini">±10%</option>
|
||||
<option value="medio">±25%</option>
|
||||
<option value="max">±50%</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label>Precisione angolare</label>
|
||||
<select id="p-precisione">
|
||||
<option value="veloce">Veloce (10°)</option>
|
||||
<option value="normale" selected>Normale (5°)</option>
|
||||
<option value="preciso">Preciso (2°)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label>Filtro falsi positivi
|
||||
<span class="hint">(verifica intensità colori)</span>
|
||||
</label>
|
||||
<select id="p-filtro-fp">
|
||||
<option value="off">Disattivato (massimo recall)</option>
|
||||
<option value="leggero">Leggero (tollera illuminazione)</option>
|
||||
<option value="medio" selected>Medio (consigliato)</option>
|
||||
<option value="forte">Forte (massima selettività)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label>Peso dimensione nel score
|
||||
<span class="hint">(penalizza scala ≠ 1.0)</span>
|
||||
</label>
|
||||
<select id="p-penalita-scala">
|
||||
<option value="0" selected>Nessuno (score shape puro)</option>
|
||||
<option value="0.3">Leggero (−30% max)</option>
|
||||
<option value="0.5">Medio (−50% max)</option>
|
||||
<option value="0.8">Forte (−80% max)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label>Score minimo <span id="v-score">0.65</span>
|
||||
<span class="hint">(più basso = più match anche incerti)</span>
|
||||
</label>
|
||||
<input type="range" id="p-min-score" min="0.30" max="0.95" step="0.05" value="0.65">
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label>Max match</label>
|
||||
<input type="number" id="p-max-matches" value="25" min="1" max="200">
|
||||
</div>
|
||||
|
||||
<details>
|
||||
<summary>Modalità Halcon</summary>
|
||||
<div class="halcon-grid">
|
||||
<label class="hc-row" title="16-bin orientation polarity-aware (mod 2π)">
|
||||
<input type="checkbox" id="hc-use-polarity">
|
||||
<span>Polarity 16-bin (F)</span>
|
||||
</label>
|
||||
<label class="hc-row" title="Score continuo cos(θ_t-θ_s) invece di bin">
|
||||
<input type="checkbox" id="hc-soft-score">
|
||||
<span>Soft-margin score (Y)</span>
|
||||
</label>
|
||||
<label class="hc-row" title="Sub-pixel refinement gradient field LM">
|
||||
<input type="checkbox" id="hc-subpixel-lm">
|
||||
<span>Sub-pixel LM 0.05 px (Z)</span>
|
||||
</label>
|
||||
<label class="hc-row" title="Refine congiunto Nelder-Mead (cx,cy,θ)">
|
||||
<input type="checkbox" id="hc-refine-joint">
|
||||
<span>Refine pose joint</span>
|
||||
</label>
|
||||
<label class="hc-row" title="Pyramid candidates propagation">
|
||||
<input type="checkbox" id="hc-pyr-propagate">
|
||||
<span>Pyramid propagate</span>
|
||||
</label>
|
||||
<label class="hc-row" title="OpenCL GPU offload (silent fallback CPU)">
|
||||
<input type="checkbox" id="hc-use-gpu">
|
||||
<span>GPU OpenCL (R)</span>
|
||||
</label>
|
||||
|
||||
<div class="hc-row hc-num">
|
||||
<label>Min recall (M)</label>
|
||||
<input type="number" id="hc-min-recall" value="0.0" min="0" max="1" step="0.05">
|
||||
</div>
|
||||
<div class="hc-row hc-num">
|
||||
<label>NMS IoU thr (A)</label>
|
||||
<input type="number" id="hc-nms-iou" value="0.3" min="0" max="1" step="0.05">
|
||||
</div>
|
||||
<div class="hc-row hc-num">
|
||||
<label>Greediness</label>
|
||||
<input type="number" id="hc-greediness" value="0.0" min="0" max="1" step="0.1">
|
||||
</div>
|
||||
<div class="hc-row hc-num">
|
||||
<label>Coarse stride</label>
|
||||
<input type="number" id="hc-coarse-stride" value="1" min="1" max="4" step="1">
|
||||
</div>
|
||||
<div class="hc-row hc-num" style="grid-column:1/-1">
|
||||
<label title="Limita area di ricerca scena: x,y,w,h (vuoto = tutta scena)">
|
||||
Search ROI (x,y,w,h)
|
||||
</label>
|
||||
<input type="text" id="hc-search-roi" placeholder="es. 100,50,800,400">
|
||||
</div>
|
||||
|
||||
<div class="hc-row" style="grid-column:1/-1; border-top:1px solid #444; padding-top:8px">
|
||||
<label>Ricetta pre-trained (V)</label>
|
||||
<div style="display:flex; gap:6px; margin-top:4px">
|
||||
<input type="text" id="hc-recipe-name" placeholder="nome_ricetta" style="flex:1">
|
||||
<button class="btn" id="btn-save-recipe" type="button">💾 Salva</button>
|
||||
</div>
|
||||
<div style="display:flex; gap:6px; margin-top:6px; align-items:center">
|
||||
<select id="hc-recipe-list" style="flex:1">
|
||||
<option value="">— ricette disponibili —</option>
|
||||
</select>
|
||||
<button class="btn" id="btn-load-recipe" type="button">📂 Carica</button>
|
||||
<button class="btn" id="btn-unload-recipe" type="button" disabled>✖ Stacca</button>
|
||||
</div>
|
||||
<div id="recipe-status" style="margin-top:4px; font-size:11px; color:#888">
|
||||
Nessuna ricetta caricata
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Avanzate</summary>
|
||||
<div id="adv-form"></div>
|
||||
</details>
|
||||
|
||||
<h2 style="margin-top:14px">TEMPI</h2>
|
||||
<div class="kv"><span>train:</span><span id="t-train">-</span></div>
|
||||
<div class="kv"><span>find:</span><span id="t-find">-</span></div>
|
||||
<div class="kv"><span>varianti:</span><span id="t-var">-</span></div>
|
||||
<div class="kv"><span>match:</span><span id="t-match">-</span></div>
|
||||
<button class="btn" id="btn-export-json" type="button" disabled
|
||||
style="margin-top:8px; width:100%"
|
||||
title="Scarica i risultati dell'ultimo match in formato JSON">
|
||||
⬇ Esporta JSON
|
||||
</button>
|
||||
|
||||
<details id="diag-panel" style="margin-top:10px">
|
||||
<summary>🔍 Diagnostica (CC)</summary>
|
||||
<div id="diag-content" style="font-family:monospace; font-size:11px;
|
||||
background:#1a1a1a; padding:8px;
|
||||
border-radius:3px; margin-top:6px;
|
||||
line-height:1.5">
|
||||
<em style="color:#888">Esegui un MATCH per vedere la diagnostica</em>
|
||||
</div>
|
||||
</details>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<footer>
|
||||
<h2>LEGENDA</h2>
|
||||
<div id="legend"></div>
|
||||
</footer>
|
||||
|
||||
<script src="/static/app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,190 @@
|
||||
* { box-sizing: border-box; }
|
||||
html, body {
|
||||
margin: 0; padding: 0; background: #1a1a1a; color: #dcdcdc;
|
||||
font-family: -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||
font-size: 14px;
|
||||
min-height: 100vh;
|
||||
}
|
||||
header {
|
||||
background: #111; padding: 10px 16px; border-bottom: 1px solid #333;
|
||||
position: sticky; top: 0; z-index: 10;
|
||||
}
|
||||
header h1 {
|
||||
margin: 0 0 8px; font-size: 18px; color: #00c8ff;
|
||||
}
|
||||
.toolbar {
|
||||
display: flex; gap: 10px; align-items: center; flex-wrap: wrap;
|
||||
}
|
||||
.btn {
|
||||
display: inline-block; padding: 6px 14px; background: #2c2c2c;
|
||||
border: 1px solid #444; color: #dcdcdc; cursor: pointer;
|
||||
border-radius: 4px; font-size: 13px; line-height: 1.4;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
.btn:hover { background: #3a3a3a; }
|
||||
.btn-go {
|
||||
background: #0a7d3a; border-color: #0d9c48; color: #fff; font-weight: bold;
|
||||
}
|
||||
.btn-go:hover { background: #0d9c48; }
|
||||
.tb-label { color: #b0b0b0; font-size: 12px; margin-left: 8px; }
|
||||
|
||||
.thumb-picker { position: relative; display: inline-block; }
|
||||
.picker-current {
|
||||
display: flex; align-items: center; gap: 8px;
|
||||
background: #2a2a2a; color: #dcdcdc; border: 1px solid #444;
|
||||
padding: 4px 8px; border-radius: 3px; font-size: 13px;
|
||||
cursor: pointer; min-width: 200px; min-height: 32px;
|
||||
}
|
||||
.picker-current:hover { background: #353535; }
|
||||
.picker-current img {
|
||||
width: 36px; height: 36px; object-fit: contain;
|
||||
background: #141414; border-radius: 2px;
|
||||
}
|
||||
.picker-text { flex: 1; white-space: nowrap; overflow: hidden;
|
||||
text-overflow: ellipsis; }
|
||||
.caret { color: #888; font-size: 10px; }
|
||||
.picker-list {
|
||||
display: none; position: absolute; top: 100%; left: 0;
|
||||
background: #232323; border: 1px solid #444; border-radius: 3px;
|
||||
margin-top: 2px; z-index: 100; max-height: 360px; overflow-y: auto;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.6); min-width: 280px;
|
||||
}
|
||||
.thumb-picker.open .picker-list { display: block; }
|
||||
.picker-item {
|
||||
display: flex; align-items: center; gap: 10px;
|
||||
padding: 6px 10px; cursor: pointer; border-bottom: 1px solid #2a2a2a;
|
||||
}
|
||||
.picker-item:hover { background: #2e2e2e; }
|
||||
.picker-item img {
|
||||
width: 60px; height: 60px; object-fit: contain;
|
||||
background: #141414; border-radius: 2px;
|
||||
}
|
||||
.picker-item .name { color: #dcdcdc; font-size: 13px; }
|
||||
#status {
|
||||
color: #00c8ff; margin-left: 12px; font-weight: 500;
|
||||
}
|
||||
|
||||
main {
|
||||
display: grid;
|
||||
grid-template-columns: 420px 1fr 360px;
|
||||
gap: 12px;
|
||||
padding: 12px 16px;
|
||||
}
|
||||
.col {
|
||||
background: #232323; padding: 10px;
|
||||
border: 1px solid #333; border-radius: 4px;
|
||||
}
|
||||
.col h2 {
|
||||
margin: 0 0 8px; font-size: 13px; color: #00c8ff;
|
||||
letter-spacing: 1px; text-transform: uppercase;
|
||||
}
|
||||
.canvas-wrap {
|
||||
background: #141414; border: 1px solid #444;
|
||||
display: inline-block; position: relative;
|
||||
}
|
||||
canvas {
|
||||
display: block; cursor: crosshair;
|
||||
image-rendering: pixelated;
|
||||
}
|
||||
#roi-info {
|
||||
margin-top: 6px; font-size: 12px; color: #aaa;
|
||||
}
|
||||
|
||||
.field {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.field label {
|
||||
display: block; font-size: 12px; color: #b0b0b0;
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
.field select, .field input {
|
||||
width: 100%; background: #2a2a2a; color: #dcdcdc;
|
||||
border: 1px solid #444; padding: 5px 6px; border-radius: 3px;
|
||||
font-size: 12px;
|
||||
}
|
||||
.field input[type="range"] {
|
||||
padding: 0; height: 26px;
|
||||
}
|
||||
.field select:focus, .field input:focus {
|
||||
outline: 1px solid #00c8ff;
|
||||
}
|
||||
|
||||
#v-score { color: #00c8ff; font-weight: bold; }
|
||||
.hint { color: #777; font-size: 10px; font-weight: normal; margin-left: 4px; }
|
||||
|
||||
details { margin-top: 10px; font-size: 12px; }
|
||||
details summary {
|
||||
cursor: pointer; padding: 4px 0; color: #00c8ff;
|
||||
}
|
||||
#adv-form {
|
||||
display: grid; grid-template-columns: 1fr 80px; gap: 3px 6px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
#adv-form label { font-size: 11px; color: #999; }
|
||||
#adv-form input {
|
||||
background: #2a2a2a; color: #dcdcdc; border: 1px solid #444;
|
||||
padding: 3px 5px; border-radius: 3px; font-size: 11px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.kv {
|
||||
display: flex; justify-content: space-between;
|
||||
padding: 3px 0; font-size: 12px; border-bottom: 1px dotted #333;
|
||||
}
|
||||
.kv span:last-child { color: #80ff80; font-weight: bold; }
|
||||
|
||||
footer {
|
||||
padding: 10px 16px; border-top: 1px solid #333;
|
||||
min-height: 120px;
|
||||
}
|
||||
footer h2 {
|
||||
margin: 0 0 8px; font-size: 13px; color: #00c8ff;
|
||||
letter-spacing: 1px; text-transform: uppercase;
|
||||
}
|
||||
#legend {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||
gap: 6px;
|
||||
}
|
||||
.legend-item {
|
||||
display: flex; align-items: center; gap: 8px;
|
||||
padding: 4px 6px; background: #232323;
|
||||
border-radius: 3px; font-size: 12px; font-family: monospace;
|
||||
}
|
||||
.legend-dot {
|
||||
width: 12px; height: 12px; border-radius: 50%; flex-shrink: 0;
|
||||
}
|
||||
|
||||
#col-model, #col-scene { min-width: 0; }
|
||||
|
||||
/* Halcon-mode panel */
|
||||
.halcon-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 6px 12px;
|
||||
margin-top: 6px;
|
||||
font-size: 12px;
|
||||
}
|
||||
.hc-row {
|
||||
display: flex; align-items: center; gap: 6px;
|
||||
}
|
||||
.hc-row.hc-num {
|
||||
flex-direction: column; align-items: flex-start;
|
||||
}
|
||||
.hc-row.hc-num label { font-size: 11px; color: #aaa; }
|
||||
.hc-row.hc-num input { width: 100%; }
|
||||
|
||||
/* Edge preview panel */
|
||||
.ep-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 6px 12px;
|
||||
margin-top: 6px;
|
||||
font-size: 12px;
|
||||
}
|
||||
.ep-row {
|
||||
display: flex; flex-direction: column; gap: 2px;
|
||||
font-size: 11px; color: #aaa;
|
||||
}
|
||||
.ep-row input[type="range"] { width: 100%; }
|
||||
.ep-row span { color: #fff; font-weight: bold; font-family: monospace; }
|
||||
@@ -3,7 +3,31 @@ name = "shape-model-2d"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.13"
|
||||
dependencies = [
|
||||
"fastapi>=0.115",
|
||||
"numba>=0.65.0",
|
||||
"numpy>=1.24",
|
||||
"opencv-python>=4.8",
|
||||
"pillow>=12.2.0",
|
||||
"python-multipart>=0.0.26",
|
||||
"uvicorn[standard]>=0.34",
|
||||
"ezdxf>=1.3",
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
pm2d-eval = "pm2d.eval:main"
|
||||
pm2d-bench = "pm2d.bench:main"
|
||||
|
||||
[dependency-groups]
|
||||
dev = [
|
||||
"httpx>=0.28.1",
|
||||
"pytest>=8.0",
|
||||
"ruff>=0.8",
|
||||
]
|
||||
|
||||
[tool.ruff]
|
||||
line-length = 100
|
||||
|
||||
[tool.ruff.lint]
|
||||
select = ["E", "F"]
|
||||
# E702 (a; b) ed E402 (import dopo codice) sono idiomi voluti del codebase
|
||||
ignore = ["E501", "E741", "E702", "E731", "E402"]
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
"""Fixture condivise: template e scene sintetiche con ground-truth nota.
|
||||
|
||||
Tutti i test sono sintetici (nessuna dipendenza dalle immagini Test/,
|
||||
non versionate): generano scene con pose note e verificano recall e
|
||||
precisione del matcher. Runtime totale atteso: ~2-4 min su 2 core.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
|
||||
def make_template(tw: int = 160, th: int = 120) -> np.ndarray:
|
||||
"""Forma a L asimmetrica con foro circolare, contrasto netto.
|
||||
|
||||
Asimmetrica per evitare ambiguita' rotazionali nei confronti GT.
|
||||
"""
|
||||
img = np.full((th, tw), 60, np.uint8)
|
||||
cv2.rectangle(img, (20, 20), (60, th - 20), 200, -1)
|
||||
cv2.rectangle(img, (20, th - 55), (tw - 25, th - 20), 200, -1)
|
||||
cv2.circle(img, (tw - 45, 40), 16, 200, -1)
|
||||
return cv2.GaussianBlur(img, (3, 3), 0)
|
||||
|
||||
|
||||
# Pose ground-truth: (cx, cy, angle_deg) - angoli volutamente lontani
|
||||
# dalla griglia di step 5/2 gradi per misurare il refine.
|
||||
GT_POSES: list[tuple[float, float, float]] = [
|
||||
(150.0, 150.0, 0.0),
|
||||
(450.0, 140.0, 7.3),
|
||||
(740.0, 170.0, 33.7),
|
||||
(160.0, 420.0, 91.2),
|
||||
(460.0, 430.0, 158.4),
|
||||
(750.0, 480.0, 246.9),
|
||||
(300.0, 590.0, 312.6),
|
||||
]
|
||||
|
||||
|
||||
def make_scene(
|
||||
template: np.ndarray,
|
||||
poses: list[tuple[float, float, float]],
|
||||
W: int = 900, H: int = 700,
|
||||
noise: float = 4.0, seed: int = 7,
|
||||
) -> np.ndarray:
|
||||
"""Incolla il template warpato alle pose date su sfondo rumoroso.
|
||||
|
||||
Convenzione di rotazione identica al matcher (cv2.getRotationMatrix2D
|
||||
attorno al centro template, poi traslazione del centro su (cx, cy)).
|
||||
"""
|
||||
rng = np.random.default_rng(seed)
|
||||
scene = np.full((H, W), 60, np.float32)
|
||||
th, tw = template.shape
|
||||
for (cx, cy, ang) in poses:
|
||||
M = cv2.getRotationMatrix2D((tw / 2.0, th / 2.0), ang, 1.0)
|
||||
M[0, 2] += cx - tw / 2.0
|
||||
M[1, 2] += cy - th / 2.0
|
||||
warped = cv2.warpAffine(template.astype(np.float32), M, (W, H),
|
||||
flags=cv2.INTER_LINEAR, borderValue=-1)
|
||||
scene = np.where(warped >= 0, warped, scene)
|
||||
scene += rng.normal(0, noise, scene.shape)
|
||||
return np.clip(scene, 0, 255).astype(np.uint8)
|
||||
|
||||
|
||||
def ang_diff(a: float, b: float) -> float:
|
||||
"""Differenza angolare firmata in (-180, 180]."""
|
||||
d = (a - b) % 360.0
|
||||
return d - 360.0 if d > 180.0 else d
|
||||
|
||||
|
||||
def match_errors(matches, poses, radius: float = 20.0):
|
||||
"""Associa match a pose GT per distanza; ritorna (err_ang, err_pos, n_miss)."""
|
||||
errs_a: list[float] = []
|
||||
errs_p: list[float] = []
|
||||
miss = 0
|
||||
for (cx, cy, ang) in poses:
|
||||
cands = [
|
||||
(math.hypot(m.cx - cx, m.cy - cy), m)
|
||||
for m in matches
|
||||
if math.hypot(m.cx - cx, m.cy - cy) < radius
|
||||
]
|
||||
if not cands:
|
||||
miss += 1
|
||||
continue
|
||||
d, m = min(cands, key=lambda t: t[0])
|
||||
errs_a.append(abs(ang_diff(m.angle_deg, ang)))
|
||||
errs_p.append(d)
|
||||
return errs_a, errs_p, miss
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def template() -> np.ndarray:
|
||||
return make_template()
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def scene(template) -> np.ndarray:
|
||||
return make_scene(template, GT_POSES)
|
||||
@@ -0,0 +1,84 @@
|
||||
"""Unit test rapidi su componenti del matcher (no matching pesante)."""
|
||||
from __future__ import annotations
|
||||
|
||||
import numpy as np
|
||||
import cv2
|
||||
import pytest
|
||||
|
||||
from pm2d import LineShapeMatcher
|
||||
from tests.conftest import GT_POSES, make_scene, match_errors
|
||||
|
||||
|
||||
def test_angle_list_includes_range_end():
|
||||
# Range parziale ±15: l'estremo +15 deve essere testato (era escluso).
|
||||
m = LineShapeMatcher(angle_range_deg=(-15.0, 15.0), angle_step_deg=5.0)
|
||||
angles = m._angle_list()
|
||||
assert -15.0 in angles and 15.0 in angles
|
||||
assert len(angles) == 7
|
||||
|
||||
|
||||
def test_angle_list_full_circle_no_duplicate():
|
||||
# (0, 360): 360 coincide con 0 → escluso, niente variante duplicata.
|
||||
m = LineShapeMatcher(angle_range_deg=(0.0, 360.0), angle_step_deg=5.0)
|
||||
angles = m._angle_list()
|
||||
assert len(angles) == 72
|
||||
assert 360.0 not in angles
|
||||
|
||||
|
||||
def test_pyramid_clamp_small_template():
|
||||
# Template 40px di lato minimo: al top /4 le feature collassano →
|
||||
# i livelli vengono clampati (40/2=20 >= 12, 40/4=10 < 12 → 2 livelli).
|
||||
m = LineShapeMatcher(pyramid_levels=4, angle_range_deg=(0.0, 10.0),
|
||||
angle_step_deg=5.0)
|
||||
tpl = np.full((40, 200), 60, np.uint8)
|
||||
cv2.rectangle(tpl, (30, 8), (170, 32), 200, -1)
|
||||
m.train(tpl)
|
||||
assert m.pyramid_levels == 2
|
||||
|
||||
|
||||
def test_save_load_roundtrip(tmp_path, template, scene):
|
||||
m = LineShapeMatcher(angle_step_deg=10.0)
|
||||
m.train(template)
|
||||
path = str(tmp_path / "model.npz")
|
||||
m.save_model(path)
|
||||
m2 = LineShapeMatcher.load_model(path)
|
||||
assert len(m2.variants) == len(m.variants)
|
||||
matches = m2.find(scene, min_score=0.5, max_matches=10)
|
||||
_, _, miss = match_errors(matches, GT_POSES)
|
||||
assert miss == 0
|
||||
|
||||
|
||||
def test_scene_cache_no_collision(template):
|
||||
# Due scene IDENTICHE nella banda superiore ma diverse sotto: la cache
|
||||
# (che prima hashava solo i primi 64KB) non deve restituire i risultati
|
||||
# della scena sbagliata.
|
||||
poses_a = [GT_POSES[0], (450.0, 560.0, 33.7)]
|
||||
poses_b = [GT_POSES[0], (700.0, 560.0, 91.2)]
|
||||
scene_a = make_scene(template, poses_a)
|
||||
scene_b = make_scene(template, poses_b)
|
||||
# Stessa banda superiore (le pose extra sono in basso, y >= 430)
|
||||
assert np.array_equal(scene_a[:80], scene_b[:80])
|
||||
m = LineShapeMatcher(angle_step_deg=10.0)
|
||||
m.train(template)
|
||||
ma = m.find(scene_a, min_score=0.5, max_matches=5)
|
||||
mb = m.find(scene_b, min_score=0.5, max_matches=5)
|
||||
_, _, miss_a = match_errors(ma, poses_a)
|
||||
_, _, miss_b = match_errors(mb, poses_b)
|
||||
assert miss_a == 0 and miss_b == 0
|
||||
|
||||
|
||||
def test_train_mask_polygonal(template, scene):
|
||||
# ROI poligonale: mask che copre solo la L verticale del template.
|
||||
mask = np.zeros_like(template)
|
||||
cv2.rectangle(mask, (10, 10), (70, template.shape[0] - 10), 255, -1)
|
||||
m = LineShapeMatcher(angle_step_deg=10.0)
|
||||
n = m.train(template, mask=mask)
|
||||
assert n > 0
|
||||
matches = m.find(scene, min_score=0.5, max_matches=10)
|
||||
assert len(matches) >= 1
|
||||
|
||||
|
||||
def test_untrained_find_raises():
|
||||
m = LineShapeMatcher()
|
||||
with pytest.raises(RuntimeError):
|
||||
m.find(np.zeros((100, 100), np.uint8))
|
||||
@@ -0,0 +1,56 @@
|
||||
"""Test di non-regressione su precisione e recall (GT sintetica).
|
||||
|
||||
Soglie derivate dalle misure di Fase 2 (errore mediano ~0.05 deg /
|
||||
~0.08 px) con margine 3-4x per assorbire rumore tra run/macchine.
|
||||
Una regressione del refine (es. score saturo, minMaxLoc sul plateau)
|
||||
riporterebbe gli errori a 2-4 deg / 4 px e fa fallire i test con
|
||||
margine enorme.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import numpy as np
|
||||
|
||||
from pm2d import LineShapeMatcher
|
||||
from tests.conftest import GT_POSES, match_errors
|
||||
|
||||
|
||||
def _find(template, scene, step, **kw):
|
||||
m = LineShapeMatcher(angle_step_deg=step, num_features=96)
|
||||
m.train(template)
|
||||
return m.find(scene, min_score=0.5, max_matches=10, **kw)
|
||||
|
||||
|
||||
def test_recall_and_precision_step5(template, scene):
|
||||
matches = _find(template, scene, 5.0)
|
||||
errs_a, errs_p, miss = match_errors(matches, GT_POSES)
|
||||
assert miss == 0, f"{miss} pose GT non trovate"
|
||||
assert float(np.median(errs_a)) < 0.2, f"err angolo mediano {np.median(errs_a):.3f} deg"
|
||||
assert float(np.max(errs_a)) < 0.5, f"err angolo max {np.max(errs_a):.3f} deg"
|
||||
assert float(np.median(errs_p)) < 0.3, f"err posizione mediano {np.median(errs_p):.3f} px"
|
||||
assert float(np.max(errs_p)) < 1.0, f"err posizione max {np.max(errs_p):.3f} px"
|
||||
|
||||
|
||||
def test_recall_and_precision_step2(template, scene):
|
||||
# Step fine: storicamente il caso peggiore (plateau con piu' varianti
|
||||
# dentro la tolleranza spread → scelta variante arbitraria).
|
||||
matches = _find(template, scene, 2.0)
|
||||
errs_a, errs_p, miss = match_errors(matches, GT_POSES)
|
||||
assert miss == 0, f"{miss} pose GT non trovate"
|
||||
assert float(np.median(errs_a)) < 0.2
|
||||
assert float(np.max(errs_a)) < 0.5
|
||||
assert float(np.median(errs_p)) < 0.3
|
||||
|
||||
|
||||
def test_no_false_positives(template, scene):
|
||||
# max_matches alto: non devono comparire match spuri oltre le 7 pose.
|
||||
matches = _find(template, scene, 5.0)
|
||||
assert len(matches) <= len(GT_POSES) + 1, (
|
||||
f"{len(matches)} match per {len(GT_POSES)} oggetti reali"
|
||||
)
|
||||
|
||||
|
||||
def test_full_scan_path_equivalent(template, scene):
|
||||
# Il path full-scan (propagate off) deve trovare le stesse pose.
|
||||
matches = _find(template, scene, 5.0, pyramid_propagate=False)
|
||||
_, _, miss = match_errors(matches, GT_POSES)
|
||||
assert miss == 0
|
||||
@@ -2,6 +2,215 @@ version = 1
|
||||
revision = 3
|
||||
requires-python = ">=3.13"
|
||||
|
||||
[[package]]
|
||||
name = "annotated-doc"
|
||||
version = "0.0.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288, upload-time = "2025-11-10T22:07:42.062Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "annotated-types"
|
||||
version = "0.7.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anyio"
|
||||
version = "4.13.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "idna" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/19/14/2c5dd9f512b66549ae92767a9c7b330ae88e1932ca57876909410251fe13/anyio-4.13.0.tar.gz", hash = "sha256:334b70e641fd2221c1505b3890c69882fe4a2df910cba14d97019b90b24439dc", size = 231622, upload-time = "2026-03-24T12:59:09.671Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/da/42/e921fccf5015463e32a3cf6ee7f980a6ed0f395ceeaa45060b61d86486c2/anyio-4.13.0-py3-none-any.whl", hash = "sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708", size = 114353, upload-time = "2026-03-24T12:59:08.246Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "certifi"
|
||||
version = "2026.4.22"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/25/ee/6caf7a40c36a1220410afe15a1cc64993a1f864871f698c0f93acb72842a/certifi-2026.4.22.tar.gz", hash = "sha256:8d455352a37b71bf76a79caa83a3d6c25afee4a385d632127b6afb3963f1c580", size = 137077, upload-time = "2026-04-22T11:26:11.191Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/22/30/7cd8fdcdfbc5b869528b079bfb76dcdf6056b1a2097a662e5e8c04f42965/certifi-2026.4.22-py3-none-any.whl", hash = "sha256:3cb2210c8f88ba2318d29b0388d1023c8492ff72ecdde4ebdaddbb13a31b1c4a", size = 135707, upload-time = "2026-04-22T11:26:09.372Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "click"
|
||||
version = "8.3.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/bb/63/f9e1ea081ce35720d8b92acde70daaedace594dc93b693c869e0d5910718/click-8.3.3.tar.gz", hash = "sha256:398329ad4837b2ff7cbe1dd166a4c0f8900c3ca3a218de04466f38f6497f18a2", size = 328061, upload-time = "2026-04-22T15:11:27.506Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ae/44/c1221527f6a71a01ec6fbad7fa78f1d50dfa02217385cf0fa3eec7087d59/click-8.3.3-py3-none-any.whl", hash = "sha256:a2bf429bb3033c89fa4936ffb35d5cb471e3719e1f3c8a7c3fff0b8314305613", size = 110502, upload-time = "2026-04-22T15:11:25.044Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "colorama"
|
||||
version = "0.4.6"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ezdxf"
|
||||
version = "1.4.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "fonttools" },
|
||||
{ name = "numpy" },
|
||||
{ name = "pyparsing" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/5e/d7/1b7be8db364f1c4838dfc1a40ca96577aba405deabf896a4eb3aaeb15a62/ezdxf-1.4.4.tar.gz", hash = "sha256:da5a5e0e6bdbb6656f9c017b47edc7eafceb419d61a2b5de64ffb344c168e593", size = 1866886, upload-time = "2026-05-14T09:19:19.511Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/be/ed/97aa3ba1ba923e4098169de5bca380ee1430e2b34d0dd85b73007e34df16/ezdxf-1.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:278a09a845d67a4f893aebe9ef8dcd8894ece9f255d7d8bb21719ffd902109b8", size = 3555283, upload-time = "2026-05-14T09:25:37.346Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4e/b1/8ca9408a2b382a806837e72d35f10931fdd9f53fe66cf208a6340358fbc5/ezdxf-1.4.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b0a787f8ebcbdcb9798c6fcfe90c23afd1a0993c8d4302b6cce5d8f19ae052ae", size = 3012646, upload-time = "2026-05-14T09:25:39.01Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8b/aa/1e8d3130eaabb780c5e8adfdc1ead023bb04468779b18169d60f7ce5b291/ezdxf-1.4.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c451d1c20f08b30c735ca4d650fee5b44147402f4b5662f8550053ed8a3009c2", size = 2999048, upload-time = "2026-05-14T09:25:40.348Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cd/ca/1dd390b79df9c104c2d4b5964e80db25362edf704ac742c9db9b279658a5/ezdxf-1.4.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3d3d9a6663993fc644751647fd7843057b51742ffac27c48ce28ea9f81239613", size = 5747890, upload-time = "2026-05-14T09:27:36.028Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8f/1e/226a6e636ae533e0bc0afe4435d1058fac173564e4af4102862f055a46d9/ezdxf-1.4.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82c022cf094d21ad3db68557aa83628e4152355e8a8bef0fffd4f71a9ea325df", size = 5774707, upload-time = "2026-05-14T09:27:17.625Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/24/a3/7e33c9036de944b5446982bb629d698356e48b00c4d173768f52bdeeff4b/ezdxf-1.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3be59c2a3a93585412a2a37af29b3ad7574fa3e429e624924c3285d0573388b3", size = 5713753, upload-time = "2026-05-14T09:27:37.55Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/36/9e/ac0cdc3a8623fc38aa16670f6537aa810bc1381217a51ac3481299bbc986/ezdxf-1.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:44a9716860b59dddd49a3708c04ff4580ad1e7de92ac93bb3213d85fba5ac93f", size = 5813895, upload-time = "2026-05-14T09:27:19.514Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dd/22/b511a8d9ea8f23447a67fe8977213fac5e4e69df30130a3ef31043613eaf/ezdxf-1.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:207eed544417464ffaf2570880d58a792ecd2534c9c2dede45e895f3200b77e6", size = 2960937, upload-time = "2026-05-14T09:23:49.361Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a0/09/5ecb6f82d35a2f4a4334d0a608fcf764827fa69dccdf5987ce309c16b6c1/ezdxf-1.4.4-py3-none-any.whl", hash = "sha256:666edda631ba717270293b734f5d58dd97a1d1aba4787187f09d0cc584645865", size = 1331217, upload-time = "2026-05-14T09:19:26.601Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fastapi"
|
||||
version = "0.136.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "annotated-doc" },
|
||||
{ name = "pydantic" },
|
||||
{ name = "starlette" },
|
||||
{ name = "typing-extensions" },
|
||||
{ name = "typing-inspection" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/5d/45/c130091c2dfa061bbfe3150f2a5091ef1adf149f2a8d2ae769ecaf6e99a2/fastapi-0.136.1.tar.gz", hash = "sha256:7af665ad7acfa0a3baf8983d393b6b471b9da10ede59c60045f49fbc89a0fa7f", size = 397448, upload-time = "2026-04-23T16:49:44.046Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/ff/2e4eca3ade2c22fe1dea7043b8ee9dabe47753349eb1b56a202de8af6349/fastapi-0.136.1-py3-none-any.whl", hash = "sha256:a6e9d7eeada96c93a4d69cb03836b44fa34e2854accb7244a1ece36cd4781c3f", size = 117683, upload-time = "2026-04-23T16:49:42.437Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fonttools"
|
||||
version = "4.63.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/84/69/c97f2c18e0db87d2c7b15da1974dace76ae938f1cfa22e2727a648b7ed43/fonttools-4.63.0.tar.gz", hash = "sha256:caeb583deeb5168e694b65cda8b4ee62abedfa66cf88488734466f2366b9c4e0", size = 3597189, upload-time = "2026-05-14T12:04:30.958Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/0f/8d/d8fec3dcde2963f8c908fb315e5ff2cd0ac34f82394bbbf73a2aa5145ce3/fonttools-4.63.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:cd7e9857e5e63738b9d9fd707bc1f59c8b09e5177726d23664db393c59bb08bd", size = 2876062, upload-time = "2026-05-14T12:03:32.554Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ef/71/d935dc54e4ff121bfdd11e08702db63a7e6f25af21d8a3d7b7212df53641/fonttools-4.63.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c2a2a42198b696a6f48fad91709afb55176e66a5e566131219dba372fb7f8c59", size = 2424594, upload-time = "2026-05-14T12:03:34.86Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8e/40/e76320afa1df918e146155ef239b1719ee266092e96f5423bfd075affba1/fonttools-4.63.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1e874792a8212b44583ea02189d9e693906b2f78b261f372f95d6c563210ac1d", size = 5024840, upload-time = "2026-05-14T12:03:36.745Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ce/36/0b805d8c485f872f65a509cbe3b58a5d0d17bee855333b54a150c79d3061/fonttools-4.63.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:22135da48a348785c5e2d5d2d9d6bec5ed44adacbaeb9db12d9493bf6c6bfa68", size = 4975801, upload-time = "2026-05-14T12:03:38.833Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c8/26/2cee03d0aa083ab022da5c07aff9ed3f689da1defb81ad6917c9627896da/fonttools-4.63.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ccf41f2efdf56994d22d73bef4ced1052161958169428d06ba9724ea9e9a64be", size = 4965009, upload-time = "2026-05-14T12:03:41.494Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7e/48/cc4b66d9058c0d0982c833fad10127c4b0e9324606aafa41382295ca4102/fonttools-4.63.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9ced0bd02ac751dd6319b0da88aaef24414e3b0dbc32bb4f24944821a3741a27", size = 5105892, upload-time = "2026-05-14T12:03:43.525Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d8/1f/a98a30a814b9ddef3a2e706025f90b9e0bc94890e6cb15254bc86547d11a/fonttools-4.63.0-cp313-cp313-win32.whl", hash = "sha256:85be818f5506e8a7753153def2c9550178f0ecae6a47b5e0e8dbb23f7cc90380", size = 2291313, upload-time = "2026-05-14T12:03:45.594Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/92/46/5177b01f3b4abfdd4409f31cca4ab279c9343a26efbe9ec78c97fc612e02/fonttools-4.63.0-cp313-cp313-win_amd64.whl", hash = "sha256:ba04cb5891d4c0c21b6da95eda8d7b090021508a294fff33464fc7d241e0856b", size = 2342299, upload-time = "2026-05-14T12:03:47.414Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/27/d2/23d25e3f247b328be58d04a4c9f894178a0d1eda7d42867cfb388adaf416/fonttools-4.63.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:fd1e3094f42d806d3d7c79162fc59e5910fcbe3a7360c385b8da969bc4493745", size = 2875338, upload-time = "2026-05-14T12:03:50.052Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cd/58/7dfa0c761cb3b2964e2a84c4dc986c926a87de0cb9fb60d5b28ded3f2914/fonttools-4.63.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:6e528da43bc3791085f8cb6141b1d13e459226790240340fcbb4625649238b03", size = 2422661, upload-time = "2026-05-14T12:03:52.154Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dd/87/64cfa18a7a1621d17b7f4502b2b0ed8a135a90c3db51ea590ee99043e76b/fonttools-4.63.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b2248c5decb223562f7902ff6325077a073f608ee8e33e88ad88db734eb9f49", size = 5010526, upload-time = "2026-05-14T12:03:54.647Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/36/e1/a8933a72c45a87177fbde2696e0d0755c8c9062f8c077a961c6215fa27b1/fonttools-4.63.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:308f957cdeaf8abe4e5f2f124902ef405448af92c90f80e302a3b771c2e6116b", size = 4923946, upload-time = "2026-05-14T12:03:56.984Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/27/60/872e6e233b8c5e8b41413796ff18b7fe479661bd40147e071b450dfad7a1/fonttools-4.63.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:bf00f21eb5fb721dbaf73d1e9da6d02a1af7768f2ebcf9798be98beab8ba90f6", size = 4962489, upload-time = "2026-05-14T12:03:59.443Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/30/c4/83c24f2ec38b90cfda84bf4b1a1f49df80e84a1db4e7ac6e0d41bf23bc39/fonttools-4.63.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c1aaa4b9c75798400ac043ce04d74e7830376c85095a5a6ed7cba2f17a266bf4", size = 5071870, upload-time = "2026-05-14T12:04:02.122Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/de/40/3ae22b60ff1d41ce0bd044b31238cdc72cef99f28b976f1e128ebd618c9b/fonttools-4.63.0-cp314-cp314-win32.whl", hash = "sha256:22693918177bd9ceabec4736d338045f357769416fc6b0b2508eefef75b08616", size = 2295026, upload-time = "2026-05-14T12:04:04.47Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c3/d4/98078064ccc76b45cb0f6c002452011e93c4bd26f6850344f0951cc1fe89/fonttools-4.63.0-cp314-cp314-win_amd64.whl", hash = "sha256:7d782fac32985914c351556f68ac0855391572bcd87de50e05970d3cd4c96fc5", size = 2347454, upload-time = "2026-05-14T12:04:06.752Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/49/4e/652d1580c5f4e39f7d103b0c793e4773129ad633dce4addd0cf4dfebde02/fonttools-4.63.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:6db5140a60a5d731d21ec076745b40a310607731b0a565b50776393188649001", size = 2958152, upload-time = "2026-05-14T12:04:08.706Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0e/55/ad864c9a9b219f552eb46b32cd7906c466e5a578ba0c3abfcc0fe7413eb6/fonttools-4.63.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7d76edbff9014094dbf03bd2d074709dfa6ec7aba13d838c937a2b33d2d6a86e", size = 2460809, upload-time = "2026-05-14T12:04:10.783Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ea/2b/0aa8db70f18cf52e49b4ed5ecec68547f981160bf5ded3b5aed6faa0a6f9/fonttools-4.63.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0eac00b9118c3c2f87d272e45341871c5b3066baa3c86897fa634a7c3fb59096", size = 5148649, upload-time = "2026-05-14T12:04:12.747Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7f/63/18e4369c25043096f1048e0c9915951adc4f842bd81c6b18155824d6fa99/fonttools-4.63.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:51394295f1a51de8b5f30bdb1e1b9a4231536c7064ef5c6e211eec19fa36036f", size = 4932147, upload-time = "2026-05-14T12:04:14.806Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a1/3f/67f3eac2ffd8a98446c5022f8ed3864eac878a5ff7af8df4c8286dba16cc/fonttools-4.63.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9e12f105d2b6342c559c298afb674006bb2893afc7102dcf8a1b55b0486b4e40", size = 5027237, upload-time = "2026-05-14T12:04:17.675Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/ba/4e6214cb38a7b04779e97bb7636de9a5c7f20af7018d03dee0b64c08510a/fonttools-4.63.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:796f27556dbe094c4824f75ca85267e4df776c79036c8441469a4df37038c196", size = 5053933, upload-time = "2026-05-14T12:04:20.818Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/34/3b/214dcc19ee31d3d38fb5ad2755c11ef0514e5dc300bbaf41c0b69f393799/fonttools-4.63.0-cp314-cp314t-win32.whl", hash = "sha256:948428a275741f0b64b113c955425a953314f4b9ab9997f73a72c83e68e569c8", size = 2359326, upload-time = "2026-05-14T12:04:24.22Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dd/1e/3ff1a9b523058c2eeb6a9d50f5574e2a738200d0d94107d5bc4105e8da3f/fonttools-4.63.0-cp314-cp314t-win_amd64.whl", hash = "sha256:6d4741eb179121cab9eea4cb2393d24492373a260d7945006358c08cfbf45419", size = 2425829, upload-time = "2026-05-14T12:04:26.829Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2c/47/c99d5268f354002ce80f8d029cd9d7d872969da1de8b93d32de4dc56d6f4/fonttools-4.63.0-py3-none-any.whl", hash = "sha256:445af2eab030a16b9171ea8bdda7ebf7d96bda2df88ee182a464252f6e05e20d", size = 1164562, upload-time = "2026-05-14T12:04:29.092Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "h11"
|
||||
version = "0.16.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "httpcore"
|
||||
version = "1.0.9"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "certifi" },
|
||||
{ name = "h11" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "httptools"
|
||||
version = "0.7.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b5/46/120a669232c7bdedb9d52d4aeae7e6c7dfe151e99dc70802e2fc7a5e1993/httptools-0.7.1.tar.gz", hash = "sha256:abd72556974f8e7c74a259655924a717a2365b236c882c3f6f8a45fe94703ac9", size = 258961, upload-time = "2025-10-10T03:55:08.559Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/09/8f/c77b1fcbfd262d422f12da02feb0d218fa228d52485b77b953832105bb90/httptools-0.7.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6babce6cfa2a99545c60bfef8bee0cc0545413cb0018f617c8059a30ad985de3", size = 202889, upload-time = "2025-10-10T03:54:47.089Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0a/1a/22887f53602feaa066354867bc49a68fc295c2293433177ee90870a7d517/httptools-0.7.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:601b7628de7504077dd3dcb3791c6b8694bbd967148a6d1f01806509254fb1ca", size = 108180, upload-time = "2025-10-10T03:54:48.052Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/32/6a/6aaa91937f0010d288d3d124ca2946d48d60c3a5ee7ca62afe870e3ea011/httptools-0.7.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:04c6c0e6c5fb0739c5b8a9eb046d298650a0ff38cf42537fc372b28dc7e4472c", size = 478596, upload-time = "2025-10-10T03:54:48.919Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6d/70/023d7ce117993107be88d2cbca566a7c1323ccbaf0af7eabf2064fe356f6/httptools-0.7.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:69d4f9705c405ae3ee83d6a12283dc9feba8cc6aaec671b412917e644ab4fa66", size = 473268, upload-time = "2025-10-10T03:54:49.993Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/32/4d/9dd616c38da088e3f436e9a616e1d0cc66544b8cdac405cc4e81c8679fc7/httptools-0.7.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:44c8f4347d4b31269c8a9205d8a5ee2df5322b09bbbd30f8f862185bb6b05346", size = 455517, upload-time = "2025-10-10T03:54:51.066Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1d/3a/a6c595c310b7df958e739aae88724e24f9246a514d909547778d776799be/httptools-0.7.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:465275d76db4d554918aba40bf1cbebe324670f3dfc979eaffaa5d108e2ed650", size = 458337, upload-time = "2025-10-10T03:54:52.196Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fd/82/88e8d6d2c51edc1cc391b6e044c6c435b6aebe97b1abc33db1b0b24cd582/httptools-0.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:322d00c2068d125bd570f7bf78b2d367dad02b919d8581d7476d8b75b294e3e6", size = 85743, upload-time = "2025-10-10T03:54:53.448Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/34/50/9d095fcbb6de2d523e027a2f304d4551855c2f46e0b82befd718b8b20056/httptools-0.7.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:c08fe65728b8d70b6923ce31e3956f859d5e1e8548e6f22ec520a962c6757270", size = 203619, upload-time = "2025-10-10T03:54:54.321Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/07/f0/89720dc5139ae54b03f861b5e2c55a37dba9a5da7d51e1e824a1f343627f/httptools-0.7.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7aea2e3c3953521c3c51106ee11487a910d45586e351202474d45472db7d72d3", size = 108714, upload-time = "2025-10-10T03:54:55.163Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/cb/eea88506f191fb552c11787c23f9a405f4c7b0c5799bf73f2249cd4f5228/httptools-0.7.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0e68b8582f4ea9166be62926077a3334064d422cf08ab87d8b74664f8e9058e1", size = 472909, upload-time = "2025-10-10T03:54:56.056Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e0/4a/a548bdfae6369c0d078bab5769f7b66f17f1bfaa6fa28f81d6be6959066b/httptools-0.7.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:df091cf961a3be783d6aebae963cc9b71e00d57fa6f149025075217bc6a55a7b", size = 470831, upload-time = "2025-10-10T03:54:57.219Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4d/31/14df99e1c43bd132eec921c2e7e11cda7852f65619bc0fc5bdc2d0cb126c/httptools-0.7.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f084813239e1eb403ddacd06a30de3d3e09a9b76e7894dcda2b22f8a726e9c60", size = 452631, upload-time = "2025-10-10T03:54:58.219Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/22/d2/b7e131f7be8d854d48cb6d048113c30f9a46dca0c9a8b08fcb3fcd588cdc/httptools-0.7.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7347714368fb2b335e9063bc2b96f2f87a9ceffcd9758ac295f8bbcd3ffbc0ca", size = 452910, upload-time = "2025-10-10T03:54:59.366Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/53/cf/878f3b91e4e6e011eff6d1fa9ca39f7eb17d19c9d7971b04873734112f30/httptools-0.7.1-cp314-cp314-win_amd64.whl", hash = "sha256:cfabda2a5bb85aa2a904ce06d974a3f30fb36cc63d7feaddec05d2050acede96", size = 88205, upload-time = "2025-10-10T03:55:00.389Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "httpx"
|
||||
version = "0.28.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "anyio" },
|
||||
{ name = "certifi" },
|
||||
{ name = "httpcore" },
|
||||
{ name = "idna" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "3.13"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ce/cc/762dfb036166873f0059f3b7de4565e1b5bc3d6f28a414c13da27e442f99/idna-3.13.tar.gz", hash = "sha256:585ea8fe5d69b9181ec1afba340451fba6ba764af97026f92a91d4eef164a242", size = 194210, upload-time = "2026-04-22T16:42:42.314Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/5d/13/ad7d7ca3808a898b4612b6fe93cde56b53f3034dcde235acb1f0e1df24c6/idna-3.13-py3-none-any.whl", hash = "sha256:892ea0cde124a99ce773decba204c5552b69c3c67ffd5f232eb7696135bc8bb3", size = 68629, upload-time = "2026-04-22T16:42:40.909Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iniconfig"
|
||||
version = "2.3.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "llvmlite"
|
||||
version = "0.47.0"
|
||||
@@ -114,19 +323,479 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e9/a5/1be1516390333ff9be3a9cb648c9f33df79d5096e5884b5df71a588af463/opencv_python-4.13.0.92-cp37-abi3-win_amd64.whl", hash = "sha256:423d934c9fafb91aad38edf26efb46da91ffbc05f3f59c4b0c72e699720706f5", size = 40212062, upload-time = "2026-02-05T07:02:12.724Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "packaging"
|
||||
version = "26.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pillow"
|
||||
version = "12.2.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/8c/21/c2bcdd5906101a30244eaffc1b6e6ce71a31bd0742a01eb89e660ebfac2d/pillow-12.2.0.tar.gz", hash = "sha256:a830b1a40919539d07806aa58e1b114df53ddd43213d9c8b75847eee6c0182b5", size = 46987819, upload-time = "2026-04-01T14:46:17.687Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/4a/01/53d10cf0dbad820a8db274d259a37ba50b88b24768ddccec07355382d5ad/pillow-12.2.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:8297651f5b5679c19968abefd6bb84d95fe30ef712eb1b2d9b2d31ca61267f4c", size = 4100837, upload-time = "2026-04-01T14:43:41.506Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0f/98/f3a6657ecb698c937f6c76ee564882945f29b79bad496abcba0e84659ec5/pillow-12.2.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:50d8520da2a6ce0af445fa6d648c4273c3eeefbc32d7ce049f22e8b5c3daecc2", size = 4176528, upload-time = "2026-04-01T14:43:43.773Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/69/bc/8986948f05e3ea490b8442ea1c1d4d990b24a7e43d8a51b2c7d8b1dced36/pillow-12.2.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:766cef22385fa1091258ad7e6216792b156dc16d8d3fa607e7545b2b72061f1c", size = 3640401, upload-time = "2026-04-01T14:43:45.87Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/34/46/6c717baadcd62bc8ed51d238d521ab651eaa74838291bda1f86fe1f864c9/pillow-12.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5d2fd0fa6b5d9d1de415060363433f28da8b1526c1c129020435e186794b3795", size = 5308094, upload-time = "2026-04-01T14:43:48.438Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/71/43/905a14a8b17fdb1ccb58d282454490662d2cb89a6bfec26af6d3520da5ec/pillow-12.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:56b25336f502b6ed02e889f4ece894a72612fe885889a6e8c4c80239ff6e5f5f", size = 4695402, upload-time = "2026-04-01T14:43:51.292Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/73/dd/42107efcb777b16fa0393317eac58f5b5cf30e8392e266e76e51cff28c3d/pillow-12.2.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f1c943e96e85df3d3478f7b691f229887e143f81fedab9b20205349ab04d73ed", size = 6280005, upload-time = "2026-04-01T14:43:54.242Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a8/68/b93e09e5e8549019e61acf49f65b1a8530765a7f812c77a7461bca7e4494/pillow-12.2.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:03f6fab9219220f041c74aeaa2939ff0062bd5c364ba9ce037197f4c6d498cd9", size = 8090669, upload-time = "2026-04-01T14:43:57.335Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4b/6e/3ccb54ce8ec4ddd1accd2d89004308b7b0b21c4ac3d20fa70af4760a4330/pillow-12.2.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5cdfebd752ec52bf5bb4e35d9c64b40826bc5b40a13df7c3cda20a2c03a0f5ed", size = 6395194, upload-time = "2026-04-01T14:43:59.864Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/67/ee/21d4e8536afd1a328f01b359b4d3997b291ffd35a237c877b331c1c3b71c/pillow-12.2.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eedf4b74eda2b5a4b2b2fb4c006d6295df3bf29e459e198c90ea48e130dc75c3", size = 7082423, upload-time = "2026-04-01T14:44:02.74Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/78/5f/e9f86ab0146464e8c133fe85df987ed9e77e08b29d8d35f9f9f4d6f917ba/pillow-12.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:00a2865911330191c0b818c59103b58a5e697cae67042366970a6b6f1b20b7f9", size = 6505667, upload-time = "2026-04-01T14:44:05.381Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ed/1e/409007f56a2fdce61584fd3acbc2bbc259857d555196cedcadc68c015c82/pillow-12.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1e1757442ed87f4912397c6d35a0db6a7b52592156014706f17658ff58bbf795", size = 7208580, upload-time = "2026-04-01T14:44:08.39Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/23/c4/7349421080b12fb35414607b8871e9534546c128a11965fd4a7002ccfbee/pillow-12.2.0-cp313-cp313-win32.whl", hash = "sha256:144748b3af2d1b358d41286056d0003f47cb339b8c43a9ea42f5fea4d8c66b6e", size = 6375896, upload-time = "2026-04-01T14:44:11.197Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3f/82/8a3739a5e470b3c6cbb1d21d315800d8e16bff503d1f16b03a4ec3212786/pillow-12.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:390ede346628ccc626e5730107cde16c42d3836b89662a115a921f28440e6a3b", size = 7081266, upload-time = "2026-04-01T14:44:13.947Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c3/25/f968f618a062574294592f668218f8af564830ccebdd1fa6200f598e65c5/pillow-12.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:8023abc91fba39036dbce14a7d6535632f99c0b857807cbbbf21ecc9f4717f06", size = 2463508, upload-time = "2026-04-01T14:44:16.312Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4d/a4/b342930964e3cb4dce5038ae34b0eab4653334995336cd486c5a8c25a00c/pillow-12.2.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:042db20a421b9bafecc4b84a8b6e444686bd9d836c7fd24542db3e7df7baad9b", size = 5309927, upload-time = "2026-04-01T14:44:18.89Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9f/de/23198e0a65a9cf06123f5435a5d95cea62a635697f8f03d134d3f3a96151/pillow-12.2.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:dd025009355c926a84a612fecf58bb315a3f6814b17ead51a8e48d3823d9087f", size = 4698624, upload-time = "2026-04-01T14:44:21.115Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/01/a6/1265e977f17d93ea37aa28aa81bad4fa597933879fac2520d24e021c8da3/pillow-12.2.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:88ddbc66737e277852913bd1e07c150cc7bb124539f94c4e2df5344494e0a612", size = 6321252, upload-time = "2026-04-01T14:44:23.663Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/83/5982eb4a285967baa70340320be9f88e57665a387e3a53a7f0db8231a0cd/pillow-12.2.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d362d1878f00c142b7e1a16e6e5e780f02be8195123f164edf7eddd911eefe7c", size = 8126550, upload-time = "2026-04-01T14:44:26.772Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4e/48/6ffc514adce69f6050d0753b1a18fd920fce8cac87620d5a31231b04bfc5/pillow-12.2.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2c727a6d53cb0018aadd8018c2b938376af27914a68a492f59dfcaca650d5eea", size = 6433114, upload-time = "2026-04-01T14:44:29.615Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/36/a3/f9a77144231fb8d40ee27107b4463e205fa4677e2ca2548e14da5cf18dce/pillow-12.2.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:efd8c21c98c5cc60653bcb311bef2ce0401642b7ce9d09e03a7da87c878289d4", size = 7115667, upload-time = "2026-04-01T14:44:32.773Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c1/fc/ac4ee3041e7d5a565e1c4fd72a113f03b6394cc72ab7089d27608f8aaccb/pillow-12.2.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9f08483a632889536b8139663db60f6724bfcb443c96f1b18855860d7d5c0fd4", size = 6538966, upload-time = "2026-04-01T14:44:35.252Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c0/a8/27fb307055087f3668f6d0a8ccb636e7431d56ed0750e07a60547b1e083e/pillow-12.2.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dac8d77255a37e81a2efcbd1fc05f1c15ee82200e6c240d7e127e25e365c39ea", size = 7238241, upload-time = "2026-04-01T14:44:37.875Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ad/4b/926ab182c07fccae9fcb120043464e1ff1564775ec8864f21a0ebce6ac25/pillow-12.2.0-cp313-cp313t-win32.whl", hash = "sha256:ee3120ae9dff32f121610bb08e4313be87e03efeadfc6c0d18f89127e24d0c24", size = 6379592, upload-time = "2026-04-01T14:44:40.336Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/c4/f9e476451a098181b30050cc4c9a3556b64c02cf6497ea421ac047e89e4b/pillow-12.2.0-cp313-cp313t-win_amd64.whl", hash = "sha256:325ca0528c6788d2a6c3d40e3568639398137346c3d6e66bb61db96b96511c98", size = 7085542, upload-time = "2026-04-01T14:44:43.251Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/00/a4/285f12aeacbe2d6dc36c407dfbbe9e96d4a80b0fb710a337f6d2ad978c75/pillow-12.2.0-cp313-cp313t-win_arm64.whl", hash = "sha256:2e5a76d03a6c6dcef67edabda7a52494afa4035021a79c8558e14af25313d453", size = 2465765, upload-time = "2026-04-01T14:44:45.996Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bf/98/4595daa2365416a86cb0d495248a393dfc84e96d62ad080c8546256cb9c0/pillow-12.2.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:3adc9215e8be0448ed6e814966ecf3d9952f0ea40eb14e89a102b87f450660d8", size = 4100848, upload-time = "2026-04-01T14:44:48.48Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0b/79/40184d464cf89f6663e18dfcf7ca21aae2491fff1a16127681bf1fa9b8cf/pillow-12.2.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:6a9adfc6d24b10f89588096364cc726174118c62130c817c2837c60cf08a392b", size = 4176515, upload-time = "2026-04-01T14:44:51.353Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b0/63/703f86fd4c422a9cf722833670f4f71418fb116b2853ff7da722ea43f184/pillow-12.2.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:6a6e67ea2e6feda684ed370f9a1c52e7a243631c025ba42149a2cc5934dec295", size = 3640159, upload-time = "2026-04-01T14:44:53.588Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/71/e0/fb22f797187d0be2270f83500aab851536101b254bfa1eae10795709d283/pillow-12.2.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2bb4a8d594eacdfc59d9e5ad972aa8afdd48d584ffd5f13a937a664c3e7db0ed", size = 5312185, upload-time = "2026-04-01T14:44:56.039Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ba/8c/1a9e46228571de18f8e28f16fabdfc20212a5d019f3e3303452b3f0a580d/pillow-12.2.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:80b2da48193b2f33ed0c32c38140f9d3186583ce7d516526d462645fd98660ae", size = 4695386, upload-time = "2026-04-01T14:44:58.663Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/70/62/98f6b7f0c88b9addd0e87c217ded307b36be024d4ff8869a812b241d1345/pillow-12.2.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:22db17c68434de69d8ecfc2fe821569195c0c373b25cccb9cbdacf2c6e53c601", size = 6280384, upload-time = "2026-04-01T14:45:01.5Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5e/03/688747d2e91cfbe0e64f316cd2e8005698f76ada3130d0194664174fa5de/pillow-12.2.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7b14cc0106cd9aecda615dd6903840a058b4700fcb817687d0ee4fc8b6e389be", size = 8091599, upload-time = "2026-04-01T14:45:04.5Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f6/35/577e22b936fcdd66537329b33af0b4ccfefaeabd8aec04b266528cddb33c/pillow-12.2.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cbeb542b2ebc6fcdacabf8aca8c1a97c9b3ad3927d46b8723f9d4f033288a0f", size = 6396021, upload-time = "2026-04-01T14:45:07.117Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/11/8d/d2532ad2a603ca2b93ad9f5135732124e57811d0168155852f37fbce2458/pillow-12.2.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4bfd07bc812fbd20395212969e41931001fd59eb55a60658b0e5710872e95286", size = 7083360, upload-time = "2026-04-01T14:45:09.763Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5e/26/d325f9f56c7e039034897e7380e9cc202b1e368bfd04d4cbe6a441f02885/pillow-12.2.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9aba9a17b623ef750a4d11b742cbafffeb48a869821252b30ee21b5e91392c50", size = 6507628, upload-time = "2026-04-01T14:45:12.378Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/f7/769d5632ffb0988f1c5e7660b3e731e30f7f8ec4318e94d0a5d674eb65a4/pillow-12.2.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:deede7c263feb25dba4e82ea23058a235dcc2fe1f6021025dc71f2b618e26104", size = 7209321, upload-time = "2026-04-01T14:45:15.122Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6a/7a/c253e3c645cd47f1aceea6a8bacdba9991bf45bb7dfe927f7c893e89c93c/pillow-12.2.0-cp314-cp314-win32.whl", hash = "sha256:632ff19b2778e43162304d50da0181ce24ac5bb8180122cbe1bf4673428328c7", size = 6479723, upload-time = "2026-04-01T14:45:17.797Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cd/8b/601e6566b957ca50e28725cb6c355c59c2c8609751efbecd980db44e0349/pillow-12.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:4e6c62e9d237e9b65fac06857d511e90d8461a32adcc1b9065ea0c0fa3a28150", size = 7217400, upload-time = "2026-04-01T14:45:20.529Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d6/94/220e46c73065c3e2951bb91c11a1fb636c8c9ad427ac3ce7d7f3359b9b2f/pillow-12.2.0-cp314-cp314-win_arm64.whl", hash = "sha256:b1c1fbd8a5a1af3412a0810d060a78b5136ec0836c8a4ef9aa11807f2a22f4e1", size = 2554835, upload-time = "2026-04-01T14:45:23.162Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b6/ab/1b426a3974cb0e7da5c29ccff4807871d48110933a57207b5a676cccc155/pillow-12.2.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:57850958fe9c751670e49b2cecf6294acc99e562531f4bd317fa5ddee2068463", size = 5314225, upload-time = "2026-04-01T14:45:25.637Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/19/1e/dce46f371be2438eecfee2a1960ee2a243bbe5e961890146d2dee1ff0f12/pillow-12.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d5d38f1411c0ed9f97bcb49b7bd59b6b7c314e0e27420e34d99d844b9ce3b6f3", size = 4698541, upload-time = "2026-04-01T14:45:28.355Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/55/c3/7fbecf70adb3a0c33b77a300dc52e424dc22ad8cdc06557a2e49523b703d/pillow-12.2.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5c0a9f29ca8e79f09de89293f82fc9b0270bb4af1d58bc98f540cc4aedf03166", size = 6322251, upload-time = "2026-04-01T14:45:30.924Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1c/3c/7fbc17cfb7e4fe0ef1642e0abc17fc6c94c9f7a16be41498e12e2ba60408/pillow-12.2.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1610dd6c61621ae1cf811bef44d77e149ce3f7b95afe66a4512f8c59f25d9ebe", size = 8127807, upload-time = "2026-04-01T14:45:33.908Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/c3/a8ae14d6defd2e448493ff512fae903b1e9bd40b72efb6ec55ce0048c8ce/pillow-12.2.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a34329707af4f73cf1782a36cd2289c0368880654a2c11f027bcee9052d35dd", size = 6433935, upload-time = "2026-04-01T14:45:36.623Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6e/32/2880fb3a074847ac159d8f902cb43278a61e85f681661e7419e6596803ed/pillow-12.2.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e9c4f5b3c546fa3458a29ab22646c1c6c787ea8f5ef51300e5a60300736905e", size = 7116720, upload-time = "2026-04-01T14:45:39.258Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/46/87/495cc9c30e0129501643f24d320076f4cc54f718341df18cc70ec94c44e1/pillow-12.2.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fb043ee2f06b41473269765c2feae53fc2e2fbf96e5e22ca94fb5ad677856f06", size = 6540498, upload-time = "2026-04-01T14:45:41.879Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/18/53/773f5edca692009d883a72211b60fdaf8871cbef075eaa9d577f0a2f989e/pillow-12.2.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f278f034eb75b4e8a13a54a876cc4a5ab39173d2cdd93a638e1b467fc545ac43", size = 7239413, upload-time = "2026-04-01T14:45:44.705Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c9/e4/4b64a97d71b2a83158134abbb2f5bd3f8a2ea691361282f010998f339ec7/pillow-12.2.0-cp314-cp314t-win32.whl", hash = "sha256:6bb77b2dcb06b20f9f4b4a8454caa581cd4dd0643a08bacf821216a16d9c8354", size = 6482084, upload-time = "2026-04-01T14:45:47.568Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ba/13/306d275efd3a3453f72114b7431c877d10b1154014c1ebbedd067770d629/pillow-12.2.0-cp314-cp314t-win_amd64.whl", hash = "sha256:6562ace0d3fb5f20ed7290f1f929cae41b25ae29528f2af1722966a0a02e2aa1", size = 7225152, upload-time = "2026-04-01T14:45:50.032Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/6e/cf826fae916b8658848d7b9f38d88da6396895c676e8086fc0988073aaf8/pillow-12.2.0-cp314-cp314t-win_arm64.whl", hash = "sha256:aa88ccfe4e32d362816319ed727a004423aab09c5cea43c01a4b435643fa34eb", size = 2556579, upload-time = "2026-04-01T14:45:52.529Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pluggy"
|
||||
version = "1.6.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pydantic"
|
||||
version = "2.13.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "annotated-types" },
|
||||
{ name = "pydantic-core" },
|
||||
{ name = "typing-extensions" },
|
||||
{ name = "typing-inspection" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d9/e4/40d09941a2cebcb20609b86a559817d5b9291c49dd6f8c87e5feffbe703a/pydantic-2.13.3.tar.gz", hash = "sha256:af09e9d1d09f4e7fe37145c1f577e1d61ceb9a41924bf0094a36506285d0a84d", size = 844068, upload-time = "2026-04-20T14:46:43.632Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f3/0a/fd7d723f8f8153418fb40cf9c940e82004fce7e987026b08a68a36dd3fe7/pydantic-2.13.3-py3-none-any.whl", hash = "sha256:6db14ac8dfc9a1e57f87ea2c0de670c251240f43cb0c30a5130e9720dc612927", size = 471981, upload-time = "2026-04-20T14:46:41.402Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pydantic-core"
|
||||
version = "2.46.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/2a/ef/f7abb56c49382a246fd2ce9c799691e3c3e7175ec74b14d99e798bcddb1a/pydantic_core-2.46.3.tar.gz", hash = "sha256:41c178f65b8c29807239d47e6050262eb6bf84eb695e41101e62e38df4a5bc2c", size = 471412, upload-time = "2026-04-20T14:40:56.672Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/9b/3c/9b5e8eb9821936d065439c3b0fb1490ffa64163bfe7e1595985a47896073/pydantic_core-2.46.3-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:12bc98de041458b80c86c56b24df1d23832f3e166cbaff011f25d187f5c62c37", size = 2102109, upload-time = "2026-04-20T14:41:24.219Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/91/97/1c41d1f5a19f241d8069f1e249853bcce378cdb76eec8ab636d7bc426280/pydantic_core-2.46.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:85348b8f89d2c3508b65b16c3c33a4da22b8215138d8b996912bb1532868885f", size = 1951820, upload-time = "2026-04-20T14:42:14.236Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/30/b4/d03a7ae14571bc2b6b3c7b122441154720619afe9a336fa3a95434df5e2f/pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1105677a6df914b1fb71a81b96c8cce7726857e1717d86001f29be06a25ee6f8", size = 1977785, upload-time = "2026-04-20T14:42:31.648Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ae/0c/4086f808834b59e3c8f1aa26df8f4b6d998cdcf354a143d18ef41529d1fe/pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:87082cd65669a33adeba5470769e9704c7cf026cc30afb9cc77fd865578ebaad", size = 2062761, upload-time = "2026-04-20T14:40:37.093Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fa/71/a649be5a5064c2df0db06e0a512c2281134ed2fcc981f52a657936a7527c/pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:60e5f66e12c4f5212d08522963380eaaeac5ebd795826cfd19b2dfb0c7a52b9c", size = 2232989, upload-time = "2026-04-20T14:42:59.254Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a2/84/7756e75763e810b3a710f4724441d1ecc5883b94aacb07ca71c5fb5cfb69/pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b6cdf19bf84128d5e7c37e8a73a0c5c10d51103a650ac585d42dd6ae233f2b7f", size = 2303975, upload-time = "2026-04-20T14:41:32.287Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6c/35/68a762e0c1e31f35fa0dac733cbd9f5b118042853698de9509c8e5bf128b/pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:031bb17f4885a43773c8c763089499f242aee2ea85cf17154168775dccdecf35", size = 2095325, upload-time = "2026-04-20T14:42:47.685Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/77/bf/1bf8c9a8e91836c926eae5e3e51dce009bf495a60ca56060689d3df3f340/pydantic_core-2.46.3-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:bcf2a8b2982a6673693eae7348ef3d8cf3979c1d63b54fca7c397a635cc68687", size = 2133368, upload-time = "2026-04-20T14:41:22.766Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e5/50/87d818d6bab915984995157ceb2380f5aac4e563dddbed6b56f0ed057aba/pydantic_core-2.46.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:28e8cf2f52d72ced402a137145923a762cbb5081e48b34312f7a0c8f55928ec3", size = 2173908, upload-time = "2026-04-20T14:42:52.044Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/91/88/a311fb306d0bd6185db41fa14ae888fb81d0baf648a761ae760d30819d33/pydantic_core-2.46.3-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:17eaface65d9fc5abb940003020309c1bf7a211f5f608d7870297c367e6f9022", size = 2186422, upload-time = "2026-04-20T14:43:29.55Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8f/79/28fd0d81508525ab2054fef7c77a638c8b5b0afcbbaeee493cf7c3fef7e1/pydantic_core-2.46.3-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:93fd339f23408a07e98950a89644f92c54d8729719a40b30c0a30bb9ebc55d23", size = 2332709, upload-time = "2026-04-20T14:42:16.134Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/21/795bf5fe5c0f379308b8ef19c50dedab2e7711dbc8d0c2acf08f1c7daa05/pydantic_core-2.46.3-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:23cbdb3aaa74dfe0837975dbf69b469753bbde8eacace524519ffdb6b6e89eb7", size = 2372428, upload-time = "2026-04-20T14:41:10.974Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/45/b3/ed14c659cbe7605e3ef063077680a64680aec81eb1a04763a05190d49b7f/pydantic_core-2.46.3-cp313-cp313-win32.whl", hash = "sha256:610eda2e3838f401105e6326ca304f5da1e15393ae25dacae5c5c63f2c275b13", size = 1965601, upload-time = "2026-04-20T14:41:42.128Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ef/bb/adb70d9a762ddd002d723fbf1bd492244d37da41e3af7b74ad212609027e/pydantic_core-2.46.3-cp313-cp313-win_amd64.whl", hash = "sha256:68cc7866ed863db34351294187f9b729964c371ba33e31c26f478471c52e1ed0", size = 2071517, upload-time = "2026-04-20T14:43:36.096Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/52/eb/66faefabebfe68bd7788339c9c9127231e680b11906368c67ce112fdb47f/pydantic_core-2.46.3-cp313-cp313-win_arm64.whl", hash = "sha256:f64b5537ac62b231572879cd08ec05600308636a5d63bcbdb15063a466977bec", size = 2035802, upload-time = "2026-04-20T14:43:38.507Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7f/db/a7bcb4940183fda36022cd18ba8dd12f2dff40740ec7b58ce7457befa416/pydantic_core-2.46.3-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:afa3aa644f74e290cdede48a7b0bee37d1c35e71b05105f6b340d484af536d9b", size = 2097614, upload-time = "2026-04-20T14:44:38.374Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/24/35/e4066358a22e3e99519db370494c7528f5a2aa1367370e80e27e20283543/pydantic_core-2.46.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ced3310e51aa425f7f77da8bbbb5212616655bedbe82c70944320bc1dbe5e018", size = 1951896, upload-time = "2026-04-20T14:40:53.996Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/87/92/37cf4049d1636996e4b888c05a501f40a43ff218983a551d57f9d5e14f0d/pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e29908922ce9da1a30b4da490bd1d3d82c01dcfdf864d2a74aacee674d0bfa34", size = 1979314, upload-time = "2026-04-20T14:41:49.446Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d8/36/9ff4d676dfbdfb2d591cf43f3d90ded01e15b1404fd101180ed2d62a2fd3/pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0c9ff69140423eea8ed2d5477df3ba037f671f5e897d206d921bc9fdc39613e7", size = 2056133, upload-time = "2026-04-20T14:42:23.574Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bc/f0/405b442a4d7ba855b06eec8b2bf9c617d43b8432d099dfdc7bf999293495/pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b675ab0a0d5b1c8fdb81195dc5bcefea3f3c240871cdd7ff9a2de8aa50772eb2", size = 2228726, upload-time = "2026-04-20T14:44:22.816Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e7/f8/65cd92dd5a0bd89ba277a98ecbfaf6fc36bbd3300973c7a4b826d6ab1391/pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0087084960f209a9a4af50ecd1fb063d9ad3658c07bb81a7a53f452dacbfb2ba", size = 2301214, upload-time = "2026-04-20T14:44:48.792Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fd/86/ef96a4c6e79e7a2d0410826a68fbc0eccc0fd44aa733be199d5fcac3bb87/pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ed42e6cc8e1b0e2b9b96e2276bad70ae625d10d6d524aed0c93de974ae029f9f", size = 2099927, upload-time = "2026-04-20T14:41:40.196Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6d/53/269caf30e0096e0a8a8f929d1982a27b3879872cca2d917d17c2f9fdf4fe/pydantic_core-2.46.3-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:f1771ce258afb3e4201e67d154edbbae712a76a6081079fe247c2f53c6322c22", size = 2128789, upload-time = "2026-04-20T14:41:15.868Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/00/b0/1a6d9b6a587e118482910c244a1c5acf4d192604174132efd12bf0ac486f/pydantic_core-2.46.3-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a7610b6a5242a6c736d8ad47fd5fff87fcfe8f833b281b1c409c3d6835d9227f", size = 2173815, upload-time = "2026-04-20T14:44:25.152Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/87/56/e7e00d4041a7e62b5a40815590114db3b535bf3ca0bf4dca9f16cef25246/pydantic_core-2.46.3-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:ff5e7783bcc5476e1db448bf268f11cb257b1c276d3e89f00b5727be86dd0127", size = 2181608, upload-time = "2026-04-20T14:41:28.933Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e8/22/4bd23c3d41f7c185d60808a1de83c76cf5aeabf792f6c636a55c3b1ec7f9/pydantic_core-2.46.3-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:9d2e32edcc143bc01e95300671915d9ca052d4f745aa0a49c48d4803f8a85f2c", size = 2326968, upload-time = "2026-04-20T14:42:03.962Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/24/ac/66cd45129e3915e5ade3b292cb3bc7fd537f58f8f8dbdaba6170f7cabb74/pydantic_core-2.46.3-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:6e42d83d1c6b87fa56b521479cff237e626a292f3b31b6345c15a99121b454c1", size = 2369842, upload-time = "2026-04-20T14:41:35.52Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a2/51/dd4248abb84113615473aa20d5545b7c4cd73c8644003b5259686f93996c/pydantic_core-2.46.3-cp314-cp314-win32.whl", hash = "sha256:07bc6d2a28c3adb4f7c6ae46aa4f2d2929af127f587ed44057af50bf1ce0f505", size = 1959661, upload-time = "2026-04-20T14:41:00.042Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/20/eb/59980e5f1ae54a3b86372bd9f0fa373ea2d402e8cdcd3459334430f91e91/pydantic_core-2.46.3-cp314-cp314-win_amd64.whl", hash = "sha256:8940562319bc621da30714617e6a7eaa6b98c84e8c685bcdc02d7ed5e7c7c44e", size = 2071686, upload-time = "2026-04-20T14:43:16.471Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8c/db/1cf77e5247047dfee34bc01fa9bca134854f528c8eb053e144298893d370/pydantic_core-2.46.3-cp314-cp314-win_arm64.whl", hash = "sha256:5dcbbcf4d22210ced8f837c96db941bdb078f419543472aca5d9a0bb7cddc7df", size = 2026907, upload-time = "2026-04-20T14:43:31.732Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/57/c0/b3df9f6a543276eadba0a48487b082ca1f201745329d97dbfa287034a230/pydantic_core-2.46.3-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:d0fe3dce1e836e418f912c1ad91c73357d03e556a4d286f441bf34fed2dbeecf", size = 2095047, upload-time = "2026-04-20T14:42:37.982Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/66/57/886a938073b97556c168fd99e1a7305bb363cd30a6d2c76086bf0587b32a/pydantic_core-2.46.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9ce92e58abc722dac1bf835a6798a60b294e48eb0e625ec9fd994b932ac5feee", size = 1934329, upload-time = "2026-04-20T14:43:49.655Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0b/7c/b42eaa5c34b13b07ecb51da21761297a9b8eb43044c864a035999998f328/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a03e6467f0f5ab796a486146d1b887b2dc5e5f9b3288898c1b1c3ad974e53e4a", size = 1974847, upload-time = "2026-04-20T14:42:10.737Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e6/9b/92b42db6543e7de4f99ae977101a2967b63122d4b6cf7773812da2d7d5b5/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2798b6ba041b9d70acfb9071a2ea13c8456dd1e6a5555798e41ba7b0790e329c", size = 2041742, upload-time = "2026-04-20T14:40:44.262Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0f/19/46fbe1efabb5aa2834b43b9454e70f9a83ad9c338c1291e48bdc4fecf167/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9be3e221bdc6d69abf294dcf7aff6af19c31a5cdcc8f0aa3b14be29df4bd03b1", size = 2236235, upload-time = "2026-04-20T14:41:27.307Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/77/da/b3f95bc009ad60ec53120f5d16c6faa8cabdbe8a20d83849a1f2b8728148/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f13936129ce841f2a5ddf6f126fea3c43cd128807b5a59588c37cf10178c2e64", size = 2282633, upload-time = "2026-04-20T14:44:33.271Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cc/6e/401336117722e28f32fb8220df676769d28ebdf08f2f4469646d404c43a3/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28b5f2ef03416facccb1c6ef744c69793175fd27e44ef15669201601cf423acb", size = 2109679, upload-time = "2026-04-20T14:44:41.065Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fc/53/b289f9bc8756a32fe718c46f55afaeaf8d489ee18d1a1e7be1db73f42cc4/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:830d1247d77ad23852314f069e9d7ddafeec5f684baf9d7e7065ed46a049c4e6", size = 2108342, upload-time = "2026-04-20T14:42:50.144Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/10/5b/8292fc7c1f9111f1b2b7c1b0dcf1179edcd014fc3ea4517499f50b829d71/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0793c90c1a3c74966e7975eaef3ed30ebdff3260a0f815a62a22adc17e4c01c", size = 2157208, upload-time = "2026-04-20T14:42:08.133Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/9e/f80044e9ec07580f057a89fc131f78dda7a58751ddf52bbe05eaf31db50f/pydantic_core-2.46.3-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:d2d0aead851b66f5245ec0c4fb2612ef457f8bbafefdf65a2bf9d6bac6140f47", size = 2167237, upload-time = "2026-04-20T14:42:25.412Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/84/6781a1b037f3b96be9227edbd1101f6d3946746056231bf4ac48cdff1a8d/pydantic_core-2.46.3-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:2f40e4246676beb31c5ce77c38a55ca4e465c6b38d11ea1bd935420568e0b1ab", size = 2312540, upload-time = "2026-04-20T14:40:40.313Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3e/db/19c0839feeb728e7df03255581f198dfdf1c2aeb1e174a8420b63c5252e5/pydantic_core-2.46.3-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:cf489cf8986c543939aeee17a09c04d6ffb43bfef8ca16fcbcc5cfdcbed24dba", size = 2369556, upload-time = "2026-04-20T14:41:09.427Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e0/15/3228774cb7cd45f5f721ddf1b2242747f4eb834d0c491f0c02d606f09fed/pydantic_core-2.46.3-cp314-cp314t-win32.whl", hash = "sha256:ffe0883b56cfc05798bf994164d2b2ff03efe2d22022a2bb080f3b626176dd56", size = 1949756, upload-time = "2026-04-20T14:41:25.717Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b8/2a/c79cf53fd91e5a87e30d481809f52f9a60dd221e39de66455cf04deaad37/pydantic_core-2.46.3-cp314-cp314t-win_amd64.whl", hash = "sha256:706d9d0ce9cf4593d07270d8e9f53b161f90c57d315aeec4fb4fd7a8b10240d8", size = 2051305, upload-time = "2026-04-20T14:43:18.627Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0b/db/d8182a7f1d9343a032265aae186eb063fe26ca4c40f256b21e8da4498e89/pydantic_core-2.46.3-cp314-cp314t-win_arm64.whl", hash = "sha256:77706aeb41df6a76568434701e0917da10692da28cb69d5fb6919ce5fdb07374", size = 2026310, upload-time = "2026-04-20T14:41:01.778Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pygments"
|
||||
version = "2.20.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyparsing"
|
||||
version = "3.3.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f3/91/9c6ee907786a473bf81c5f53cf703ba0957b23ab84c264080fb5a450416f/pyparsing-3.3.2.tar.gz", hash = "sha256:c777f4d763f140633dcb6d8a3eda953bf7a214dc4eff598413c070bcdc117cbc", size = 6851574, upload-time = "2026-01-21T03:57:59.36Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl", hash = "sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d", size = 122781, upload-time = "2026-01-21T03:57:55.912Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pytest"
|
||||
version = "9.0.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||
{ name = "iniconfig" },
|
||||
{ name = "packaging" },
|
||||
{ name = "pluggy" },
|
||||
{ name = "pygments" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "python-dotenv"
|
||||
version = "1.2.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/82/ed/0301aeeac3e5353ef3d94b6ec08bbcabd04a72018415dcb29e588514bba8/python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3", size = 50135, upload-time = "2026-03-01T16:00:26.196Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "python-multipart"
|
||||
version = "0.0.26"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/88/71/b145a380824a960ebd60e1014256dbb7d2253f2316ff2d73dfd8928ec2c3/python_multipart-0.0.26.tar.gz", hash = "sha256:08fadc45918cd615e26846437f50c5d6d23304da32c341f289a617127b081f17", size = 43501, upload-time = "2026-04-10T14:09:59.473Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/9a/22/f1925cdda983ab66fc8ec6ec8014b959262747e58bdca26a4e3d1da29d56/python_multipart-0.0.26-py3-none-any.whl", hash = "sha256:c0b169f8c4484c13b0dcf2ef0ec3a4adb255c4b7d18d8e420477d2b1dd03f185", size = 28847, upload-time = "2026-04-10T14:09:58.131Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyyaml"
|
||||
version = "6.0.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.15.17"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/8c/a9/3abdf488f1bf3d24c699415e454ed554a6350d5d89ce183be1ee0a3361ac/ruff-0.15.17.tar.gz", hash = "sha256:2ec446937fd16c8c4de2674a209cc5af64d9c6f17d21fbf1151054fa0bcf5219", size = 4743346, upload-time = "2026-06-11T17:54:47.663Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/db/4d/e11259f5da07cb6afb2d074c31bf09da9671993f7329d4f15d2fdc458301/ruff-0.15.17-py3-none-linux_armv6l.whl", hash = "sha256:d9feddb927fc68bd295f5eebc587a7e42cfaf9b65f60ca4a2386febff575da8f", size = 10856677, upload-time = "2026-06-11T17:54:49.533Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/29/3e/772d679e1a0dc058e58875bd2c0cb713a0530877b4a76fee3c7966df0d49/ruff-0.15.17-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:25805a226d741c47d274a35ad5c10a7dde175fcddfa511d7cf3da0a21eb3eab7", size = 11223443, upload-time = "2026-06-11T17:55:00.573Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/68/58/bd41f7688b2fd5623012605130ed70e60aa7f2244baa3d5066bdd61530c8/ruff-0.15.17-py3-none-macosx_11_0_arm64.whl", hash = "sha256:f6ad73b14c2d18a3bf8ad7cb6974294d7f613a7898604826058e6ac64918ef4d", size = 10566458, upload-time = "2026-06-11T17:55:07.52Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d8/5b/733371013fcf1ec339e477ece6ab42bfe10bdd9bba8ee88a9516aa56bfc0/ruff-0.15.17-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ba0c1e4f95bcb3869d0d30cbd5917071ef2e28665abfec970cdab0492c713ed", size = 10914483, upload-time = "2026-06-11T17:55:05.501Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bd/cc/6f24251cc0252f7239391ccb85833f320efad14ebe5b443943f37ced6332/ruff-0.15.17-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:81647960f10bff57d2e51cadd0c3950fe598400c852863a038720ef5b8cca91e", size = 10647497, upload-time = "2026-06-11T17:54:57.733Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/68/dd/0d10c17ce1a1624d6fc3156309c3f834fdb5dfaad026ec90c85684f3990e/ruff-0.15.17-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0e01a84ddbc8c16c23055ba3924476850f1bbc1917cebbb9376665a63e74260d", size = 11416967, upload-time = "2026-06-11T17:54:51.461Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2f/91/556bfb156f6144f355e831c23db00b2fc4120f86b3ce81cc5f7fd2df51f3/ruff-0.15.17-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:84fe9f653152f8f294f9f7e03bf3a453d8b4a27f7a59c78c8666167f2b17b96c", size = 12335770, upload-time = "2026-06-11T17:54:45.793Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/88/82/8b5999aa13355e926f06d9f42a32dcca862f623bf0363785ff89d607dffd/ruff-0.15.17-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c0fe88a7676e7a05b73174d4d4a59cb2ac21ff8263583f87a81a6018475a978", size = 11575441, upload-time = "2026-06-11T17:54:32.661Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/11/93/f10377bb04109ca0e8cbc483ff1982c54b6d418210041776f93e8cdc7fa9/ruff-0.15.17-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ecfc3c7878fff94633ab0348524e093f9ce3243080416dd7d14f8ba400174719", size = 11557614, upload-time = "2026-06-11T17:54:34.698Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/a6/eeeae7f7d5493df41649ab3db92f086b2d0a30199e4efdf8e3dd7a033f24/ruff-0.15.17-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:b8461180b22420b1bdc289909410930761629fddf2a5aaf60fae1ab26cedc4c4", size = 11544450, upload-time = "2026-06-11T17:54:39.042Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/32/88/5991ce565129a24dd4a00db1254b3b5db2e53018cbe4018ea5a89738e727/ruff-0.15.17-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:6eccbe50a038b503e7140b441aa9c7fc8c1f36edf23ebef9f4165c2f28f568b7", size = 10892524, upload-time = "2026-06-11T17:55:09.432Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f5/1d/0fdd248313425f55223968af04b0a42125466a8d88d21c1d99c6af0a51e8/ruff-0.15.17-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:382fc0521025f5a8ad447d8bdd523545d0d7646adb718eb1c2dac5065ec27c0f", size = 10659573, upload-time = "2026-06-11T17:54:36.824Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9e/0e/072e8260deb9461062ce9311ced27a8e541229a6ffd483013dd37661e43e/ruff-0.15.17-py3-none-musllinux_1_2_i686.whl", hash = "sha256:456d41fcd1b2777ad63f09a6e7121d43f7b688bbc76a800c10f7f8fb1f912c3f", size = 11127818, upload-time = "2026-06-11T17:55:03.124Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ab/b4/55060a34163121498014696b5f656db5b8c6963768f227dbf0d76b311073/ruff-0.15.17-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:b1a04bcc94ae6194e9db05d16ad31f298a7194bfbcb08258bbe589cee1d587b8", size = 11655901, upload-time = "2026-06-11T17:54:53.562Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/49/71/9b29d6b87cef468d697f43c6a91e3fae4a80185779d7d5a4ef27d173439f/ruff-0.15.17-py3-none-win32.whl", hash = "sha256:596065960ab1ff593f744220c9fe6580eda00a95003cffa9f4048bb5b1bf0392", size = 10925574, upload-time = "2026-06-11T17:54:55.723Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/b2/8fc77f3723228836fa5d12497eb71c808f83782e10d058d2b15cfa14640b/ruff-0.15.17-py3-none-win_amd64.whl", hash = "sha256:6769e5fa1710b179b92e0bfa5a51735b35baea9013dadb06d5f44cbcf9547084", size = 12058788, upload-time = "2026-06-11T17:54:41.042Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2d/c7/c53e8dbff9c9dc4b7928773421ae294a5d28fcb8dcda1a089579d3a7e510/ruff-0.15.17-py3-none-win_arm64.whl", hash = "sha256:f3be1fbb34bcdfd146240d8fb92a709d4c2c8191348580a3c044ec60fa0b4456", size = 11355275, upload-time = "2026-06-11T17:54:43.635Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shape-model-2d"
|
||||
version = "0.1.0"
|
||||
source = { virtual = "." }
|
||||
dependencies = [
|
||||
{ name = "ezdxf" },
|
||||
{ name = "fastapi" },
|
||||
{ name = "numba" },
|
||||
{ name = "numpy" },
|
||||
{ name = "opencv-python" },
|
||||
{ name = "pillow" },
|
||||
{ name = "python-multipart" },
|
||||
{ name = "uvicorn", extra = ["standard"] },
|
||||
]
|
||||
|
||||
[package.dev-dependencies]
|
||||
dev = [
|
||||
{ name = "httpx" },
|
||||
{ name = "pytest" },
|
||||
{ name = "ruff" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "ezdxf", specifier = ">=1.3" },
|
||||
{ name = "fastapi", specifier = ">=0.115" },
|
||||
{ name = "numba", specifier = ">=0.65.0" },
|
||||
{ name = "numpy", specifier = ">=1.24" },
|
||||
{ name = "opencv-python", specifier = ">=4.8" },
|
||||
{ name = "pillow", specifier = ">=12.2.0" },
|
||||
{ name = "python-multipart", specifier = ">=0.0.26" },
|
||||
{ name = "uvicorn", extras = ["standard"], specifier = ">=0.34" },
|
||||
]
|
||||
|
||||
[package.metadata.requires-dev]
|
||||
dev = [
|
||||
{ name = "httpx", specifier = ">=0.28.1" },
|
||||
{ name = "pytest", specifier = ">=8.0" },
|
||||
{ name = "ruff", specifier = ">=0.8" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "starlette"
|
||||
version = "1.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "anyio" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/81/69/17425771797c36cded50b7fe44e850315d039f28b15901ab44839e70b593/starlette-1.0.0.tar.gz", hash = "sha256:6a4beaf1f81bb472fd19ea9b918b50dc3a77a6f2e190a12954b25e6ed5eea149", size = 2655289, upload-time = "2026-03-22T18:29:46.779Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/0b/c9/584bc9651441b4ba60cc4d557d8a547b5aff901af35bda3a4ee30c819b82/starlette-1.0.0-py3-none-any.whl", hash = "sha256:d3ec55e0bb321692d275455ddfd3df75fff145d009685eb40dc91fc66b03d38b", size = 72651, upload-time = "2026-03-22T18:29:45.111Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typing-extensions"
|
||||
version = "4.15.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typing-inspection"
|
||||
version = "0.4.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "uvicorn"
|
||||
version = "0.46.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "click" },
|
||||
{ name = "h11" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/1f/93/041fca8274050e40e6791f267d82e0e2e27dd165627bd640d3e0e378d877/uvicorn-0.46.0.tar.gz", hash = "sha256:fb9da0926999cc6cb22dc7cd71a94a632f078e6ae47ff683c5c420750fb7413d", size = 88758, upload-time = "2026-04-23T07:16:00.151Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/31/a3/5b1562db76a5a488274b2332a97199b32d0442aca0ed193697fd47786316/uvicorn-0.46.0-py3-none-any.whl", hash = "sha256:bbebbcbed972d162afca128605223022bedd345b7bc7855ce66deb31487a9048", size = 70926, upload-time = "2026-04-23T07:15:58.355Z" },
|
||||
]
|
||||
|
||||
[package.optional-dependencies]
|
||||
standard = [
|
||||
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||
{ name = "httptools" },
|
||||
{ name = "python-dotenv" },
|
||||
{ name = "pyyaml" },
|
||||
{ name = "uvloop", marker = "platform_python_implementation != 'PyPy' and sys_platform != 'cygwin' and sys_platform != 'win32'" },
|
||||
{ name = "watchfiles" },
|
||||
{ name = "websockets" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "uvloop"
|
||||
version = "0.22.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/06/f0/18d39dbd1971d6d62c4629cc7fa67f74821b0dc1f5a77af43719de7936a7/uvloop-0.22.1.tar.gz", hash = "sha256:6c84bae345b9147082b17371e3dd5d42775bddce91f885499017f4607fdaf39f", size = 2443250, upload-time = "2025-10-16T22:17:19.342Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/89/8c/182a2a593195bfd39842ea68ebc084e20c850806117213f5a299dfc513d9/uvloop-0.22.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:561577354eb94200d75aca23fbde86ee11be36b00e52a4eaf8f50fb0c86b7705", size = 1358611, upload-time = "2025-10-16T22:16:36.833Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d2/14/e301ee96a6dc95224b6f1162cd3312f6d1217be3907b79173b06785f2fe7/uvloop-0.22.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1cdf5192ab3e674ca26da2eada35b288d2fa49fdd0f357a19f0e7c4e7d5077c8", size = 751811, upload-time = "2025-10-16T22:16:38.275Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b7/02/654426ce265ac19e2980bfd9ea6590ca96a56f10c76e63801a2df01c0486/uvloop-0.22.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e2ea3d6190a2968f4a14a23019d3b16870dd2190cd69c8180f7c632d21de68d", size = 4288562, upload-time = "2025-10-16T22:16:39.375Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/15/c0/0be24758891ef825f2065cd5db8741aaddabe3e248ee6acc5e8a80f04005/uvloop-0.22.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0530a5fbad9c9e4ee3f2b33b148c6a64d47bbad8000ea63704fa8260f4cf728e", size = 4366890, upload-time = "2025-10-16T22:16:40.547Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d2/53/8369e5219a5855869bcee5f4d317f6da0e2c669aecf0ef7d371e3d084449/uvloop-0.22.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bc5ef13bbc10b5335792360623cc378d52d7e62c2de64660616478c32cd0598e", size = 4119472, upload-time = "2025-10-16T22:16:41.694Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/ba/d69adbe699b768f6b29a5eec7b47dd610bd17a69de51b251126a801369ea/uvloop-0.22.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1f38ec5e3f18c8a10ded09742f7fb8de0108796eb673f30ce7762ce1b8550cad", size = 4239051, upload-time = "2025-10-16T22:16:43.224Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/90/cd/b62bdeaa429758aee8de8b00ac0dd26593a9de93d302bff3d21439e9791d/uvloop-0.22.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3879b88423ec7e97cd4eba2a443aa26ed4e59b45e6b76aabf13fe2f27023a142", size = 1362067, upload-time = "2025-10-16T22:16:44.503Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0d/f8/a132124dfda0777e489ca86732e85e69afcd1ff7686647000050ba670689/uvloop-0.22.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4baa86acedf1d62115c1dc6ad1e17134476688f08c6efd8a2ab076e815665c74", size = 752423, upload-time = "2025-10-16T22:16:45.968Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a3/94/94af78c156f88da4b3a733773ad5ba0b164393e357cc4bd0ab2e2677a7d6/uvloop-0.22.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:297c27d8003520596236bdb2335e6b3f649480bd09e00d1e3a99144b691d2a35", size = 4272437, upload-time = "2025-10-16T22:16:47.451Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b5/35/60249e9fd07b32c665192cec7af29e06c7cd96fa1d08b84f012a56a0b38e/uvloop-0.22.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c1955d5a1dd43198244d47664a5858082a3239766a839b2102a269aaff7a4e25", size = 4292101, upload-time = "2025-10-16T22:16:49.318Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/02/62/67d382dfcb25d0a98ce73c11ed1a6fba5037a1a1d533dcbb7cab033a2636/uvloop-0.22.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b31dc2fccbd42adc73bc4e7cdbae4fc5086cf378979e53ca5d0301838c5682c6", size = 4114158, upload-time = "2025-10-16T22:16:50.517Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f0/7a/f1171b4a882a5d13c8b7576f348acfe6074d72eaf52cccef752f748d4a9f/uvloop-0.22.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:93f617675b2d03af4e72a5333ef89450dfaa5321303ede6e67ba9c9d26878079", size = 4177360, upload-time = "2025-10-16T22:16:52.646Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/79/7b/b01414f31546caf0919da80ad57cbfe24c56b151d12af68cee1b04922ca8/uvloop-0.22.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:37554f70528f60cad66945b885eb01f1bb514f132d92b6eeed1c90fd54ed6289", size = 1454790, upload-time = "2025-10-16T22:16:54.355Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d4/31/0bb232318dd838cad3fa8fb0c68c8b40e1145b32025581975e18b11fab40/uvloop-0.22.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:b76324e2dc033a0b2f435f33eb88ff9913c156ef78e153fb210e03c13da746b3", size = 796783, upload-time = "2025-10-16T22:16:55.906Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/42/38/c9b09f3271a7a723a5de69f8e237ab8e7803183131bc57c890db0b6bb872/uvloop-0.22.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:badb4d8e58ee08dad957002027830d5c3b06aea446a6a3744483c2b3b745345c", size = 4647548, upload-time = "2025-10-16T22:16:57.008Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c1/37/945b4ca0ac27e3dc4952642d4c900edd030b3da6c9634875af6e13ae80e5/uvloop-0.22.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b91328c72635f6f9e0282e4a57da7470c7350ab1c9f48546c0f2866205349d21", size = 4467065, upload-time = "2025-10-16T22:16:58.206Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/97/cc/48d232f33d60e2e2e0b42f4e73455b146b76ebe216487e862700457fbf3c/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:daf620c2995d193449393d6c62131b3fbd40a63bf7b307a1527856ace637fe88", size = 4328384, upload-time = "2025-10-16T22:16:59.36Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e4/16/c1fd27e9549f3c4baf1dc9c20c456cd2f822dbf8de9f463824b0c0357e06/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6cde23eeda1a25c75b2e07d39970f3374105d5eafbaab2a4482be82f272d5a5e", size = 4296730, upload-time = "2025-10-16T22:17:00.744Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "watchfiles"
|
||||
version = "1.1.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "anyio" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c2/c9/8869df9b2a2d6c59d79220a4db37679e74f807c559ffe5265e08b227a210/watchfiles-1.1.1.tar.gz", hash = "sha256:a173cb5c16c4f40ab19cecf48a534c409f7ea983ab8fed0741304a1c0a31b3f2", size = 94440, upload-time = "2025-10-14T15:06:21.08Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/bb/f4/f750b29225fe77139f7ae5de89d4949f5a99f934c65a1f1c0b248f26f747/watchfiles-1.1.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:130e4876309e8686a5e37dba7d5e9bc77e6ed908266996ca26572437a5271e18", size = 404321, upload-time = "2025-10-14T15:05:02.063Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/f9/f07a295cde762644aa4c4bb0f88921d2d141af45e735b965fb2e87858328/watchfiles-1.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5f3bde70f157f84ece3765b42b4a52c6ac1a50334903c6eaf765362f6ccca88a", size = 391783, upload-time = "2025-10-14T15:05:03.052Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bc/11/fc2502457e0bea39a5c958d86d2cb69e407a4d00b85735ca724bfa6e0d1a/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:14e0b1fe858430fc0251737ef3824c54027bedb8c37c38114488b8e131cf8219", size = 449279, upload-time = "2025-10-14T15:05:04.004Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e3/1f/d66bc15ea0b728df3ed96a539c777acfcad0eb78555ad9efcaa1274688f0/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f27db948078f3823a6bb3b465180db8ebecf26dd5dae6f6180bd87383b6b4428", size = 459405, upload-time = "2025-10-14T15:05:04.942Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/be/90/9f4a65c0aec3ccf032703e6db02d89a157462fbb2cf20dd415128251cac0/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:059098c3a429f62fc98e8ec62b982230ef2c8df68c79e826e37b895bc359a9c0", size = 488976, upload-time = "2025-10-14T15:05:05.905Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/37/57/ee347af605d867f712be7029bb94c8c071732a4b44792e3176fa3c612d39/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bfb5862016acc9b869bb57284e6cb35fdf8e22fe59f7548858e2f971d045f150", size = 595506, upload-time = "2025-10-14T15:05:06.906Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a8/78/cc5ab0b86c122047f75e8fc471c67a04dee395daf847d3e59381996c8707/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:319b27255aacd9923b8a276bb14d21a5f7ff82564c744235fc5eae58d95422ae", size = 474936, upload-time = "2025-10-14T15:05:07.906Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/62/da/def65b170a3815af7bd40a3e7010bf6ab53089ef1b75d05dd5385b87cf08/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c755367e51db90e75b19454b680903631d41f9e3607fbd941d296a020c2d752d", size = 456147, upload-time = "2025-10-14T15:05:09.138Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/57/99/da6573ba71166e82d288d4df0839128004c67d2778d3b566c138695f5c0b/watchfiles-1.1.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c22c776292a23bfc7237a98f791b9ad3144b02116ff10d820829ce62dff46d0b", size = 630007, upload-time = "2025-10-14T15:05:10.117Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a8/51/7439c4dd39511368849eb1e53279cd3454b4a4dbace80bab88feeb83c6b5/watchfiles-1.1.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:3a476189be23c3686bc2f4321dd501cb329c0a0469e77b7b534ee10129ae6374", size = 622280, upload-time = "2025-10-14T15:05:11.146Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/95/9c/8ed97d4bba5db6fdcdb2b298d3898f2dd5c20f6b73aee04eabe56c59677e/watchfiles-1.1.1-cp313-cp313-win32.whl", hash = "sha256:bf0a91bfb5574a2f7fc223cf95eeea79abfefa404bf1ea5e339c0c1560ae99a0", size = 272056, upload-time = "2025-10-14T15:05:12.156Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1f/f3/c14e28429f744a260d8ceae18bf58c1d5fa56b50d006a7a9f80e1882cb0d/watchfiles-1.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:52e06553899e11e8074503c8e716d574adeeb7e68913115c4b3653c53f9bae42", size = 288162, upload-time = "2025-10-14T15:05:13.208Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dc/61/fe0e56c40d5cd29523e398d31153218718c5786b5e636d9ae8ae79453d27/watchfiles-1.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:ac3cc5759570cd02662b15fbcd9d917f7ecd47efe0d6b40474eafd246f91ea18", size = 277909, upload-time = "2025-10-14T15:05:14.49Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/79/42/e0a7d749626f1e28c7108a99fb9bf524b501bbbeb9b261ceecde644d5a07/watchfiles-1.1.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:563b116874a9a7ce6f96f87cd0b94f7faf92d08d0021e837796f0a14318ef8da", size = 403389, upload-time = "2025-10-14T15:05:15.777Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/15/49/08732f90ce0fbbc13913f9f215c689cfc9ced345fb1bcd8829a50007cc8d/watchfiles-1.1.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3ad9fe1dae4ab4212d8c91e80b832425e24f421703b5a42ef2e4a1e215aff051", size = 389964, upload-time = "2025-10-14T15:05:16.85Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/27/0d/7c315d4bd5f2538910491a0393c56bf70d333d51bc5b34bee8e68e8cea19/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce70f96a46b894b36eba678f153f052967a0d06d5b5a19b336ab0dbbd029f73e", size = 448114, upload-time = "2025-10-14T15:05:17.876Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c3/24/9e096de47a4d11bc4df41e9d1e61776393eac4cb6eb11b3e23315b78b2cc/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cb467c999c2eff23a6417e58d75e5828716f42ed8289fe6b77a7e5a91036ca70", size = 460264, upload-time = "2025-10-14T15:05:18.962Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cc/0f/e8dea6375f1d3ba5fcb0b3583e2b493e77379834c74fd5a22d66d85d6540/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:836398932192dae4146c8f6f737d74baeac8b70ce14831a239bdb1ca882fc261", size = 487877, upload-time = "2025-10-14T15:05:20.094Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ac/5b/df24cfc6424a12deb41503b64d42fbea6b8cb357ec62ca84a5a3476f654a/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:743185e7372b7bc7c389e1badcc606931a827112fbbd37f14c537320fca08620", size = 595176, upload-time = "2025-10-14T15:05:21.134Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8f/b5/853b6757f7347de4e9b37e8cc3289283fb983cba1ab4d2d7144694871d9c/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:afaeff7696e0ad9f02cbb8f56365ff4686ab205fcf9c4c5b6fdfaaa16549dd04", size = 473577, upload-time = "2025-10-14T15:05:22.306Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e1/f7/0a4467be0a56e80447c8529c9fce5b38eab4f513cb3d9bf82e7392a5696b/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f7eb7da0eb23aa2ba036d4f616d46906013a68caf61b7fdbe42fc8b25132e77", size = 455425, upload-time = "2025-10-14T15:05:23.348Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8e/e0/82583485ea00137ddf69bc84a2db88bd92ab4a6e3c405e5fb878ead8d0e7/watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:831a62658609f0e5c64178211c942ace999517f5770fe9436be4c2faeba0c0ef", size = 628826, upload-time = "2025-10-14T15:05:24.398Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/28/9a/a785356fccf9fae84c0cc90570f11702ae9571036fb25932f1242c82191c/watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:f9a2ae5c91cecc9edd47e041a930490c31c3afb1f5e6d71de3dc671bfaca02bf", size = 622208, upload-time = "2025-10-14T15:05:25.45Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c3/f4/0872229324ef69b2c3edec35e84bd57a1289e7d3fe74588048ed8947a323/watchfiles-1.1.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:d1715143123baeeaeadec0528bb7441103979a1d5f6fd0e1f915383fea7ea6d5", size = 404315, upload-time = "2025-10-14T15:05:26.501Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7b/22/16d5331eaed1cb107b873f6ae1b69e9ced582fcf0c59a50cd84f403b1c32/watchfiles-1.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:39574d6370c4579d7f5d0ad940ce5b20db0e4117444e39b6d8f99db5676c52fd", size = 390869, upload-time = "2025-10-14T15:05:27.649Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b2/7e/5643bfff5acb6539b18483128fdc0ef2cccc94a5b8fbda130c823e8ed636/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7365b92c2e69ee952902e8f70f3ba6360d0d596d9299d55d7d386df84b6941fb", size = 449919, upload-time = "2025-10-14T15:05:28.701Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/51/2e/c410993ba5025a9f9357c376f48976ef0e1b1aefb73b97a5ae01a5972755/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bfff9740c69c0e4ed32416f013f3c45e2ae42ccedd1167ef2d805c000b6c71a5", size = 460845, upload-time = "2025-10-14T15:05:30.064Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8e/a4/2df3b404469122e8680f0fcd06079317e48db58a2da2950fb45020947734/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b27cf2eb1dda37b2089e3907d8ea92922b673c0c427886d4edc6b94d8dfe5db3", size = 489027, upload-time = "2025-10-14T15:05:31.064Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ea/84/4587ba5b1f267167ee715b7f66e6382cca6938e0a4b870adad93e44747e6/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:526e86aced14a65a5b0ec50827c745597c782ff46b571dbfe46192ab9e0b3c33", size = 595615, upload-time = "2025-10-14T15:05:32.074Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6a/0f/c6988c91d06e93cd0bb3d4a808bcf32375ca1904609835c3031799e3ecae/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:04e78dd0b6352db95507fd8cb46f39d185cf8c74e4cf1e4fbad1d3df96faf510", size = 474836, upload-time = "2025-10-14T15:05:33.209Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b4/36/ded8aebea91919485b7bbabbd14f5f359326cb5ec218cd67074d1e426d74/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c85794a4cfa094714fb9c08d4a218375b2b95b8ed1666e8677c349906246c05", size = 455099, upload-time = "2025-10-14T15:05:34.189Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/98/e0/8c9bdba88af756a2fce230dd365fab2baf927ba42cd47521ee7498fd5211/watchfiles-1.1.1-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:74d5012b7630714b66be7b7b7a78855ef7ad58e8650c73afc4c076a1f480a8d6", size = 630626, upload-time = "2025-10-14T15:05:35.216Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/84/a95db05354bf2d19e438520d92a8ca475e578c647f78f53197f5a2f17aaf/watchfiles-1.1.1-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:8fbe85cb3201c7d380d3d0b90e63d520f15d6afe217165d7f98c9c649654db81", size = 622519, upload-time = "2025-10-14T15:05:36.259Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1d/ce/d8acdc8de545de995c339be67711e474c77d643555a9bb74a9334252bd55/watchfiles-1.1.1-cp314-cp314-win32.whl", hash = "sha256:3fa0b59c92278b5a7800d3ee7733da9d096d4aabcfabb9a928918bd276ef9b9b", size = 272078, upload-time = "2025-10-14T15:05:37.63Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c4/c9/a74487f72d0451524be827e8edec251da0cc1fcf111646a511ae752e1a3d/watchfiles-1.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:c2047d0b6cea13b3316bdbafbfa0c4228ae593d995030fda39089d36e64fc03a", size = 287664, upload-time = "2025-10-14T15:05:38.95Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/df/b8/8ac000702cdd496cdce998c6f4ee0ca1f15977bba51bdf07d872ebdfc34c/watchfiles-1.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:842178b126593addc05acf6fce960d28bc5fae7afbaa2c6c1b3a7b9460e5be02", size = 277154, upload-time = "2025-10-14T15:05:39.954Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/47/a8/e3af2184707c29f0f14b1963c0aace6529f9d1b8582d5b99f31bbf42f59e/watchfiles-1.1.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:88863fbbc1a7312972f1c511f202eb30866370ebb8493aef2812b9ff28156a21", size = 403820, upload-time = "2025-10-14T15:05:40.932Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c0/ec/e47e307c2f4bd75f9f9e8afbe3876679b18e1bcec449beca132a1c5ffb2d/watchfiles-1.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:55c7475190662e202c08c6c0f4d9e345a29367438cf8e8037f3155e10a88d5a5", size = 390510, upload-time = "2025-10-14T15:05:41.945Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d5/a0/ad235642118090f66e7b2f18fd5c42082418404a79205cdfca50b6309c13/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f53fa183d53a1d7a8852277c92b967ae99c2d4dcee2bfacff8868e6e30b15f7", size = 448408, upload-time = "2025-10-14T15:05:43.385Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/df/85/97fa10fd5ff3332ae17e7e40e20784e419e28521549780869f1413742e9d/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6aae418a8b323732fa89721d86f39ec8f092fc2af67f4217a2b07fd3e93c6101", size = 458968, upload-time = "2025-10-14T15:05:44.404Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/47/c2/9059c2e8966ea5ce678166617a7f75ecba6164375f3b288e50a40dc6d489/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f096076119da54a6080e8920cbdaac3dbee667eb91dcc5e5b78840b87415bd44", size = 488096, upload-time = "2025-10-14T15:05:45.398Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/94/44/d90a9ec8ac309bc26db808a13e7bfc0e4e78b6fc051078a554e132e80160/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:00485f441d183717038ed2e887a7c868154f216877653121068107b227a2f64c", size = 596040, upload-time = "2025-10-14T15:05:46.502Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/95/68/4e3479b20ca305cfc561db3ed207a8a1c745ee32bf24f2026a129d0ddb6e/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a55f3e9e493158d7bfdb60a1165035f1cf7d320914e7b7ea83fe22c6023b58fc", size = 473847, upload-time = "2025-10-14T15:05:47.484Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4f/55/2af26693fd15165c4ff7857e38330e1b61ab8c37d15dc79118cdba115b7a/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c91ed27800188c2ae96d16e3149f199d62f86c7af5f5f4d2c61a3ed8cd3666c", size = 455072, upload-time = "2025-10-14T15:05:48.928Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/66/1d/d0d200b10c9311ec25d2273f8aad8c3ef7cc7ea11808022501811208a750/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:311ff15a0bae3714ffb603e6ba6dbfba4065ab60865d15a6ec544133bdb21099", size = 629104, upload-time = "2025-10-14T15:05:49.908Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e3/bd/fa9bb053192491b3867ba07d2343d9f2252e00811567d30ae8d0f78136fe/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:a916a2932da8f8ab582f242c065f5c81bed3462849ca79ee357dd9551b0e9b01", size = 622112, upload-time = "2025-10-14T15:05:50.941Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "websockets"
|
||||
version = "16.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/04/24/4b2031d72e840ce4c1ccb255f693b15c334757fc50023e4db9537080b8c4/websockets-16.0.tar.gz", hash = "sha256:5f6261a5e56e8d5c42a4497b364ea24d94d9563e8fbd44e78ac40879c60179b5", size = 179346, upload-time = "2026-01-10T09:23:47.181Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/cc/9c/baa8456050d1c1b08dd0ec7346026668cbc6f145ab4e314d707bb845bf0d/websockets-16.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:878b336ac47938b474c8f982ac2f7266a540adc3fa4ad74ae96fea9823a02cc9", size = 177364, upload-time = "2026-01-10T09:22:59.333Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7e/0c/8811fc53e9bcff68fe7de2bcbe75116a8d959ac699a3200f4847a8925210/websockets-16.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:52a0fec0e6c8d9a784c2c78276a48a2bdf099e4ccc2a4cad53b27718dbfd0230", size = 175039, upload-time = "2026-01-10T09:23:01.171Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/aa/82/39a5f910cb99ec0b59e482971238c845af9220d3ab9fa76dd9162cda9d62/websockets-16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e6578ed5b6981005df1860a56e3617f14a6c307e6a71b4fff8c48fdc50f3ed2c", size = 175323, upload-time = "2026-01-10T09:23:02.341Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bd/28/0a25ee5342eb5d5f297d992a77e56892ecb65e7854c7898fb7d35e9b33bd/websockets-16.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:95724e638f0f9c350bb1c2b0a7ad0e83d9cc0c9259f3ea94e40d7b02a2179ae5", size = 184975, upload-time = "2026-01-10T09:23:03.756Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/66/27ea52741752f5107c2e41fda05e8395a682a1e11c4e592a809a90c6a506/websockets-16.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0204dc62a89dc9d50d682412c10b3542d748260d743500a85c13cd1ee4bde82", size = 186203, upload-time = "2026-01-10T09:23:05.01Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/37/e5/8e32857371406a757816a2b471939d51c463509be73fa538216ea52b792a/websockets-16.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:52ac480f44d32970d66763115edea932f1c5b1312de36df06d6b219f6741eed8", size = 185653, upload-time = "2026-01-10T09:23:06.301Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9b/67/f926bac29882894669368dc73f4da900fcdf47955d0a0185d60103df5737/websockets-16.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6e5a82b677f8f6f59e8dfc34ec06ca6b5b48bc4fcda346acd093694cc2c24d8f", size = 184920, upload-time = "2026-01-10T09:23:07.492Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/a1/3d6ccdcd125b0a42a311bcd15a7f705d688f73b2a22d8cf1c0875d35d34a/websockets-16.0-cp313-cp313-win32.whl", hash = "sha256:abf050a199613f64c886ea10f38b47770a65154dc37181bfaff70c160f45315a", size = 178255, upload-time = "2026-01-10T09:23:09.245Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6b/ae/90366304d7c2ce80f9b826096a9e9048b4bb760e44d3b873bb272cba696b/websockets-16.0-cp313-cp313-win_amd64.whl", hash = "sha256:3425ac5cf448801335d6fdc7ae1eb22072055417a96cc6b31b3861f455fbc156", size = 178689, upload-time = "2026-01-10T09:23:10.483Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f3/1d/e88022630271f5bd349ed82417136281931e558d628dd52c4d8621b4a0b2/websockets-16.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8cc451a50f2aee53042ac52d2d053d08bf89bcb31ae799cb4487587661c038a0", size = 177406, upload-time = "2026-01-10T09:23:12.178Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f2/78/e63be1bf0724eeb4616efb1ae1c9044f7c3953b7957799abb5915bffd38e/websockets-16.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:daa3b6ff70a9241cf6c7fc9e949d41232d9d7d26fd3522b1ad2b4d62487e9904", size = 175085, upload-time = "2026-01-10T09:23:13.511Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bb/f4/d3c9220d818ee955ae390cf319a7c7a467beceb24f05ee7aaaa2414345ba/websockets-16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:fd3cb4adb94a2a6e2b7c0d8d05cb94e6f1c81a0cf9dc2694fb65c7e8d94c42e4", size = 175328, upload-time = "2026-01-10T09:23:14.727Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/63/bc/d3e208028de777087e6fb2b122051a6ff7bbcca0d6df9d9c2bf1dd869ae9/websockets-16.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:781caf5e8eee67f663126490c2f96f40906594cb86b408a703630f95550a8c3e", size = 185044, upload-time = "2026-01-10T09:23:15.939Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ad/6e/9a0927ac24bd33a0a9af834d89e0abc7cfd8e13bed17a86407a66773cc0e/websockets-16.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:caab51a72c51973ca21fa8a18bd8165e1a0183f1ac7066a182ff27107b71e1a4", size = 186279, upload-time = "2026-01-10T09:23:17.148Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b9/ca/bf1c68440d7a868180e11be653c85959502efd3a709323230314fda6e0b3/websockets-16.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:19c4dc84098e523fd63711e563077d39e90ec6702aff4b5d9e344a60cb3c0cb1", size = 185711, upload-time = "2026-01-10T09:23:18.372Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c4/f8/fdc34643a989561f217bb477cbc47a3a07212cbda91c0e4389c43c296ebf/websockets-16.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a5e18a238a2b2249c9a9235466b90e96ae4795672598a58772dd806edc7ac6d3", size = 184982, upload-time = "2026-01-10T09:23:19.652Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dd/d1/574fa27e233764dbac9c52730d63fcf2823b16f0856b3329fc6268d6ae4f/websockets-16.0-cp314-cp314-win32.whl", hash = "sha256:a069d734c4a043182729edd3e9f247c3b2a4035415a9172fd0f1b71658a320a8", size = 177915, upload-time = "2026-01-10T09:23:21.458Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/f1/ae6b937bf3126b5134ce1f482365fde31a357c784ac51852978768b5eff4/websockets-16.0-cp314-cp314-win_amd64.whl", hash = "sha256:c0ee0e63f23914732c6d7e0cce24915c48f3f1512ec1d079ed01fc629dab269d", size = 178381, upload-time = "2026-01-10T09:23:22.715Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/06/9b/f791d1db48403e1f0a27577a6beb37afae94254a8c6f08be4a23e4930bc0/websockets-16.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:a35539cacc3febb22b8f4d4a99cc79b104226a756aa7400adc722e83b0d03244", size = 177737, upload-time = "2026-01-10T09:23:24.523Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bd/40/53ad02341fa33b3ce489023f635367a4ac98b73570102ad2cdd770dacc9a/websockets-16.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:b784ca5de850f4ce93ec85d3269d24d4c82f22b7212023c974c401d4980ebc5e", size = 175268, upload-time = "2026-01-10T09:23:25.781Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/74/9b/6158d4e459b984f949dcbbb0c5d270154c7618e11c01029b9bbd1bb4c4f9/websockets-16.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:569d01a4e7fba956c5ae4fc988f0d4e187900f5497ce46339c996dbf24f17641", size = 175486, upload-time = "2026-01-10T09:23:27.033Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e5/2d/7583b30208b639c8090206f95073646c2c9ffd66f44df967981a64f849ad/websockets-16.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:50f23cdd8343b984957e4077839841146f67a3d31ab0d00e6b824e74c5b2f6e8", size = 185331, upload-time = "2026-01-10T09:23:28.259Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/45/b0/cce3784eb519b7b5ad680d14b9673a31ab8dcb7aad8b64d81709d2430aa8/websockets-16.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:152284a83a00c59b759697b7f9e9cddf4e3c7861dd0d964b472b70f78f89e80e", size = 186501, upload-time = "2026-01-10T09:23:29.449Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/19/60/b8ebe4c7e89fb5f6cdf080623c9d92789a53636950f7abacfc33fe2b3135/websockets-16.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bc59589ab64b0022385f429b94697348a6a234e8ce22544e3681b2e9331b5944", size = 186062, upload-time = "2026-01-10T09:23:31.368Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/88/a8/a080593f89b0138b6cba1b28f8df5673b5506f72879322288b031337c0b8/websockets-16.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:32da954ffa2814258030e5a57bc73a3635463238e797c7375dc8091327434206", size = 185356, upload-time = "2026-01-10T09:23:32.627Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/b6/b9afed2afadddaf5ebb2afa801abf4b0868f42f8539bfe4b071b5266c9fe/websockets-16.0-cp314-cp314t-win32.whl", hash = "sha256:5a4b4cc550cb665dd8a47f868c8d04c8230f857363ad3c9caf7a0c3bf8c61ca6", size = 178085, upload-time = "2026-01-10T09:23:33.816Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9f/3e/28135a24e384493fa804216b79a6a6759a38cc4ff59118787b9fb693df93/websockets-16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b14dc141ed6d2dde437cddb216004bcac6a1df0935d79656387bd41632ba0bbd", size = 178531, upload-time = "2026-01-10T09:23:35.016Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/28/258ebab549c2bf3e64d2b0217b973467394a9cea8c42f70418ca2c5d0d2e/websockets-16.0-py3-none-any.whl", hash = "sha256:1637db62fad1dc833276dded54215f2c7fa46912301a24bd94d45d46a011ceec", size = 171598, upload-time = "2026-01-10T09:23:45.395Z" },
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user