Offline-first leather pattern recognition for factory use (LAN-hosted web app). The system does not do classification. Instead, it extracts an embedding from an uploaded image and finds the closest reference pattern using cosine similarity.
The LeatherMatch system consists of two separate repositories:
| Repository | Stack | Role |
|---|---|---|
| LeatherMatch-AI (this repo) | Java · Python · React · Docker | AI inference server + web UI |
| LeatherMatch-Control | C# · WPF · .NET 8 · Windows | Desktop control panel for managing the AI service |
LeatherMatch-Control lets operators start, stop, and monitor the AI service without any technical knowledge — see the LeatherMatch Control section below.
- Java: JDK 17
- Backend build/run: Maven
- Python tooling: Python 3.10+ (only if you use
build_database.py) - Frontend dev: Node.js 18+
- Java backend (runtime): Spring Boot + ONNX Runtime for embedding extraction, REST API, SQLite persistence, admin panel endpoints.
- Python tooling (offline utilities): build the embedding database and validate similarity logic.
- Frontend: React (TypeScript) + Vite — admin panel + match page (under
Frontend/).
flowchart LR
Control["LeatherMatch Control\n(WPF · .NET 8 · Windows)"]
UserBrowser[UserBrowser]
Control -->|"docker compose up/down"| SpringBootAPI
Control -->|"GET /api/health"| SpringBootAPI
UserBrowser --> ReactUI[ReactUI]
ReactUI -->|"HTTP /api"| SpringBootAPI[SpringBootAPI]
ReactUI -->|"Basic Auth /api/admin"| AdminController[AdminController]
SpringBootAPI --> ImagePreprocessor[ImagePreprocessor]
ImagePreprocessor --> OnnxEmbedding[ONNX_Embedding]
OnnxEmbedding --> SimilarityEngine[SimilarityEngine]
PatternDB[("pattern_database.json\nRAM cache")] --> SimilarityEngine
AdminDB[("SQLite logs.db\npatterns + references\n+ settings")] --> SimilarityEngine
SpringBootAPI --> AdminDB
AdminController --> AdminDB
AdminController --> Filesystem[Leather_Images/]
graph LR
subgraph aiRepo [LeatherMatch-AI repo]
Backend["Spring Boot + ONNX"]
Frontend["React + Vite UI"]
PyTools["Python tooling"]
ExampleCompose["docker-compose.example.yml"]
end
subgraph ctrlRepo [LeatherMatch-Control repo]
WPF["WPF desktop app\n.NET 8 Windows"]
CtrlConfig["config.json\nDocker dir + health URL"]
end
ExampleCompose -->|"copy & rename\nto docker-compose.yml"| ctrlRepo
WPF -->|"docker compose up/down"| Backend
WPF -->|"GET /api/health"| Backend
- Reference embeddings are generated ahead of time (Python tooling) and stored in
data/pattern_database.json. New references can also be added at runtime via the admin panel — no restart needed. - At runtime, the backend:
- preprocesses the uploaded image (224×224, normalized),
- runs ONNX inference to get an embedding vector (dimension depends on the model),
- L2-normalizes only the query embedding,
- computes cosine similarity (dot product for normalized vectors),
- picks the best match using max similarity per pattern,
- applies a threshold (default:
0.70, adjustable at runtime) to decide MATCH vs UNCERTAIN.
- Purpose: offline-first leather pattern matching via embedding + cosine similarity (no classification).
- Roles:
operatoruses/match+ submit feedback;adminmanages/admin/**(patterns, feedback, logs, settings). - Quick start:
- Dev: run backend (Spring Boot) + frontend (Vite).
- LAN/Prod: run
docker compose up -dand accesshttp://HOST_IP:8080.
- Security: HTTP Basic Auth is enabled; use strong credentials configured in
docker-compose.yml(fail-fast password rules). - Matching decision summary:
thresholdandmarginare used to separateMATCHvsUNCERTAIN. - Critical admin flows: manage patterns, upload/import reference images + choose thumbnail, review feedback queue, adjust similarity settings at runtime.
- Control point: verify the service with
GET /api/health.
LeatherMatch-AI/
├─ Backend/ # Java Spring Boot backend (runtime)
│ └─ src/main/java/com/example/src/
│ ├─ config/ # SecurityConfig (HTTP Basic Auth)
│ ├─ controller/ # PatternMatchController + AdminController + SpaController (SPA forward)
│ ├─ dto/ # MatchResult, ErrorResponse, PatternDto, ReferenceImageDto, SettingsDto
│ ├─ exception/ # GlobalExceptionHandler
│ ├─ preprocessing/ # Image preprocessing (224x224, NCHW)
│ └─ service/ # ONNX loader, EmbeddingService, DatabaseService,
│ # SimilarityService, LogService,
│ # AdminDatabaseService, SettingsService
├─ Frontend/ # React + Vite admin UI
│ └─ src/
│ ├─ api/client.ts # Axios instance with Basic Auth interceptor
│ ├─ components/ # Layout (sidebar), ProtectedRoute
│ └─ pages/
│ ├─ LoginPage.tsx
│ ├─ MatchPage.tsx # Upload image → show match result
│ └─ admin/
│ ├─ PatternsPage.tsx # List + add + delete patterns
│ ├─ PatternDetailPage.tsx# Upload / import / preview reference images
│ ├─ LogsPage.tsx # Paginated match logs with filters
│ └─ SettingsPage.tsx # Threshold slider
├─ model/
│ └─ leather_model.onnx # ONNX feature extractor (not in repo — provide separately)
├─ Leather_Images/ # Reference images by pattern code (not in repo)
├─ data/
│ ├─ pattern_database.json # Legacy embedding database (built by Python tooling)
│ └─ logs.db # SQLite — match_logs, patterns, reference_images, settings
├─ build_database.py # Build embeddings JSON database (Python tooling)
├─ verify_setup.py # Verify local environment + paths
├─ test_similarity.py # Python reference matcher (ground truth)
├─ requirements.txt # Python deps for tooling
├─ Dockerfile # Multi-stage build (Node → Maven → JRE runtime)
├─ docker-compose.example.yml # Template for server deployment
└─ .gitignore
Goal: only the host machine runs the server; other users only need a browser.
Only needed if you add new patterns via Python tooling instead of the admin panel.
pip install -r requirements.txt
python verify_setup.py
python build_database.pyThis generates/updates data/pattern_database.json.
Patterns added here will be available for matching (in-memory cache and GET /api/patterns) on next server start.
Note: the admin panel pattern list (GET /api/admin/patterns) comes from SQLite patterns. If you want to manage a JSON-only pattern in the admin UI, create the same pattern code in the admin panel first, then use Import from disk to register its images into SQLite.
Open the Backend/ project in Eclipse and run LeatherMatchAiApplication as a Spring Boot app.
Or from a terminal:
mvn -f Backend/pom.xml spring-boot:runThe server binds to 0.0.0.0:8080 — accessible from any device on the LAN.
Requires Node.js v18+.
cd Frontend
npm install # first time only
npm run dev
# → http://localhost:5173Vite proxies all /api requests to http://localhost:8080 automatically.
LAN note: if you want other devices to open the dev server, start it with:
cd Frontend
npm run dev -- --hostFrom another device on the same network, open:
http://HOST_IP:8080(if serving the built frontend from Spring Boot — see below)http://HOST_IP:5173(if the Vite dev server is running and reachable)
Windows Firewall: allow inbound TCP on port 8080 (and 5173 for dev).
cd Frontend
npm run build
# Copy Frontend/dist/* → Backend/src/main/resources/static/After rebuilding and restarting Spring Boot, the UI is served at http://HOST_IP:8080.
Note: Files in
Frontend/public/(e.g.login-bg.png) are copied by Vite intodist/and then intostatic/during the Docker build. They are served without authentication. All SPA routes (/login,/match,/admin/**) are forwarded toindex.htmlbySpaController— no Basic-Auth popup on page refresh.
With this method you can run the application even on a server PC without Java, Maven, or Node.js installed. The only requirement is Docker Desktop.
- Host: Windows 10 / 11
- Docker Desktop must be installed and running
In the repo root (where the Dockerfile is located):
docker build -t leathermatch-ai:latest .This runs a three-stage build:
- Compiles the React/Vite frontend with Node 18.
- Compiles the Spring Boot backend with Maven + JDK 17; embeds the frontend output into the JAR.
- Produces a minimal runtime image containing only JRE 17 (the ONNX model is also copied into the image).
-
Create a working directory on the server PC:
C:\LeatherMatch\ ├── docker-compose.yml ← described below └── data\ ← empty folder; SQLite and feedback images are written here -
Copy
docker-compose.example.ymlto this folder and rename it todocker-compose.yml. -
SECURITY: Configure credentials and CORS in
docker-compose.yml:CRITICAL: The application will NOT start with weak or missing passwords (fail-fast security).
Edit the
environmentsection:environment: - SPRING_PROFILES_ACTIVE=docker # Replace with strong passwords (minimum 12 characters) - ADMIN_PASSWORD=YourVeryStrongPassword123!@# - OPERATOR_PASSWORD=AnotherStrongPassword456$%^ # Replace SERVER_IP with your actual LAN IP (e.g., 192.168.1.100) - CORS_ALLOWED_ORIGINS=http://192.168.1.100:8080 # Set to true only if using HTTPS - SSL_ENABLED=false
Password Requirements:
- Minimum 12 characters
- Cannot be weak passwords like:
changeme123,operator123,password123, etc. - Application will abort startup if passwords are weak
CORS Configuration:
- Replace
SERVER_IPwith the actual IP address of the server on your LAN - For multiple frontend origins, use comma-separated list:
http://IP1:PORT1,http://IP2:PORT2
-
Edit the
Leather_Imagesvolume line indocker-compose.ymlto point to the actual location on your server:# External drive example: - D:/LeatherMatchData/Leather_Images:/app/Leather_Images # Local drive example: - C:/LeatherMatchData/Leather_Images:/app/Leather_Images
-
Transfer the
leathermatch-ai:latestimage built on the development machine to the server:# On development machine — save image to file: docker save leathermatch-ai:latest -o leathermatch-ai.tar # On server machine — load image: docker load -i leathermatch-ai.tar
On the server PC in the C:\LeatherMatch\ folder:
docker compose up -dThe application is accessible at http://SERVER_IP:8080 from all devices on the LAN.
Windows Firewall: Allow inbound TCP on port 8080 on the server PC.
docker compose down-
Build a new image on the development machine:
docker build -t leathermatch-ai:latest .
-
Transfer the new image to the server (
docker save/docker load). -
Apply the update on the server:
docker compose up -dDocker Compose stops the old container and restarts it with the new image. The
data/andLeather_Images/volumes remain untouched.
Use docker-compose.local.yml to test the image on your development machine. This file mounts the existing data/ and Leather_Images/ folders into the container.
# In repo root:
docker build -t leathermatch-ai:latest .
docker compose -f docker-compose.local.yml up -dVerification:
# Health check:
curl http://localhost:8080/api/health
# Admin panel in browser:
# http://localhost:8080 → login with your configured admin credentialsTo stop:
docker compose -f docker-compose.local.yml downThe volume paths in
docker-compose.local.yml(E:/Proje/LeatherMatch-AI/...) are set for the development machine. Edit them if you are working from a different drive.
| URL | Description |
|---|---|
http://HOST_IP:8080/login |
Sign in with credentials configured in docker-compose.yml (see Security Configuration) |
/match |
Upload a leather image → get best pattern match |
/admin/feedback |
Review Queue — operator corrections (approve/reject/add as reference) |
/admin/patterns |
List all patterns, add new, delete |
/admin/patterns/:id |
Upload / import reference images, preview thumbnails, choose thumbnail via "Thumbnail yap" |
/admin/logs |
Paginated match log with pattern and result filters |
/admin/settings |
Change similarity threshold at runtime (no restart needed) |
Browser refresh / deep links: All SPA routes (/login, /match, /admin/**) are handled by SpaController on the backend, which forwards them to index.html. This means pressing F5 or navigating directly to any of these URLs never triggers a browser Basic-Auth dialog or a 500 error — React Router takes over on the client side.
Security Note: Default credentials (admin/changeme123, operator/changeme123) are rejected at startup. You must configure strong passwords in docker-compose.yml (see deployment instructions above).
Credentials are set in Backend/src/main/resources/application.properties (not tracked by git — copy from application.properties.example and set your own values):
# Admin: full access to /api/admin/** and /api/match
admin.username=admin
admin.password=changeme123
# Operator: match-only (example user)
operator.username=operator
operator.password=changeme123| User | Password | Access |
|---|---|---|
admin |
changeme123 |
Match + Admin |
operator |
changeme123 |
Match only |
Security note: change default passwords before using on a shared network.
Operator UI: Operators log in via /login and use /match (no admin nav). Admins see /admin/* after login. Operators who try /admin/* get 403.
Patterns that exist in SQLite (created via admin UI) can show their true embedding count (including JSON-loaded cache embeddings) even before their images are imported into SQLite.
If the reference images already exist on disk under Leather_Images/<PATTERN_CODE>/,
import them so they are registered in SQLite and usable for thumbnails / management.
On the pattern detail page a banner appears — click "Import from disk" to scan
Leather_Images/<PATTERN_CODE>/, run ONNX inference on each image, and register them.
This is a one-time action per pattern; it is idempotent (already-registered files are skipped).
On the pattern detail page, the "Thumbnail yap" button opens a modal to choose which reference image is used as the pattern thumbnail. The selected image is copied to Leather_Images/<CODE>/_thumbnail.jpg (or .png), and the Match screen serves this file. Thumbnails and reference images use a zoomable modal (double-click to zoom, pinch on mobile).
| Method | Path | Description |
|---|---|---|
| GET | /api/patterns |
List all pattern codes in memory |
| GET | /api/health |
Server health + loaded counts |
| GET | /api/stats |
Pattern/embedding counts + accuracy stats |
| Method | Path | Description |
|---|---|---|
| POST | /api/match |
Upload image → returns best pattern + score (operator or admin) |
| POST | /api/feedback |
Submit correction when match is wrong (operator or admin) |
| GET | /api/patterns/{code}/thumbnail |
Return thumbnail image for pattern (admin/operator auth; uses selected or first reference) |
| Method | Path | Description |
|---|---|---|
| GET | /api/admin/patterns |
List patterns with reference counts |
| POST | /api/admin/patterns |
Create pattern {"code": "XY-001"} |
| DELETE | /api/admin/patterns/{id} |
Delete pattern + all its references |
| GET | /api/admin/patterns/{id}/references |
List registered references |
| POST | /api/admin/patterns/{id}/references |
Upload image(s), generate embedding, persist |
| POST | /api/admin/patterns/{id}/import-from-disk |
Scan disk and import unregistered images |
| PUT | /api/admin/patterns/{id}/thumbnail-reference |
Set thumbnail reference {"referenceId": 123} |
| GET | /api/admin/references/{id}/image |
Stream image file (used by thumbnail UI) |
| DELETE | /api/admin/references/{id} |
Delete single reference |
| GET | /api/admin/settings |
Get current threshold and margin |
| PUT | /api/admin/settings/threshold |
Update threshold {"threshold": 0.75} |
| PUT | /api/admin/settings/margin |
Update top-2 margin {"margin": 0.03} (gap between best and second-best scores) |
| GET | /api/admin/feedback |
Paginated feedback list (?status&limit&offset) |
| POST | /api/admin/feedback/{id}/approve |
Approve feedback |
| POST | /api/admin/feedback/{id}/reject |
Reject feedback |
| POST | /api/admin/feedback/{id}/approve-and-add-reference |
Approve and add image as reference |
| GET | /api/admin/logs |
Paginated logs (?limit&offset&pattern&isMatch) |
| GET | /api/admin/metrics |
Log-derived metrics (match/uncertain rates, latency, score histogram, per-pattern stats) |
| What | Where |
|---|---|
| ONNX model | model/leather_model.onnx |
| Legacy embedding DB | data/pattern_database.json (read-only at runtime) |
| SQLite database | data/logs.db (match_logs, match_feedback, patterns, reference_images, settings) |
| Feedback images | data/feedback_images/ (operator corrections pending review) |
| Reference images | Leather_Images/<PATTERN_CODE>/<filename>.jpg |
- Chrome DevTools: Run
npm run dev, open the app in Chrome, pressCtrl+Shift+Mto toggle device toolbar. Select "iPhone SE" (375px) or "Responsive" and set width to 320px. - Real device: Run
npm run dev -- --host, find your host IP (e.g.ipconfig), openhttp://HOST_IP:5173on your phone. - Production: After
npm run buildand copyingdist/*toBackend/src/main/resources/static/, accesshttp://HOST_IP:8080from a phone.
LeatherMatch-Control is a separate Windows desktop application that provides a graphical control panel for managing the LeatherMatch-AI Docker service — no command line required.
Tech stack: C# 12 · WPF · .NET 8 (net8.0-windows) · pure BCL (no third-party packages)
Key features:
| Feature | Description |
|---|---|
| Server control | Start / stop the LeatherMatch-AI service via Docker Compose with a single click |
| Health monitoring | Color-coded status indicator (green / yellow / red / gray) via GET /api/health |
| Auto refresh | Configurable polling interval (default: 15 s) |
| Scheduled start/stop | Automatically start and stop the service at configured daily times |
| Settings UI | Configure Docker Compose directory, health check URL, and schedule |
Requirements: Windows 10 x64 · .NET 8 Runtime · Docker Desktop (Compose V2)
Configuration (config.json):
{
"ComposeWorkingDirectory": "C:\\LeatherMatch",
"HealthCheckUrl": "http://localhost:8080/api/health",
"AutoRefreshIntervalSeconds": 15,
"AutoStartEnabled": false,
"AutoStopEnabled": false,
"StartTime": "09:00",
"StopTime": "18:00"
}The ComposeWorkingDirectory must point to the folder containing your docker-compose.yml (copied from docker-compose.example.yml in this repo).
See the LeatherMatch-Control repository for full build and setup instructions.
Copyright (c) 2026
This source code is made available for portfolio and demonstration purposes only. Commercial use, redistribution, and production deployment are prohibited without prior written permission. See LICENSE for full terms.