A full-stack web application that detects objects in an uploaded image with Ultralytics YOLO26, draws bounding boxes over the image, and counts the objects by class.
πΉπ· TΓΌrkΓ§e README
FastAPI backend + React (Vite) frontend. There is no database β the app is fully stateless: an uploaded image lives in memory only for the duration of the request, is never written to disk, and no result is ever stored.
The user interface is in Turkish. All code, comments and this README document the behaviour in full.
Features Β· Technical highlights Β· Quick start Β· API Β· Configuration Β· Project structure Β· License
| Drag & drop upload | File picker, instant preview, client-side type and size validation |
| Canvas visualisation | Boxes are drawn over the image with label score% above each one |
| Per-class colours | Colour is derived from class_id, so a class always keeps the same colour |
| Confidence slider | 0β100 in 1% steps, debounced (400 ms) so a request fires only once the user stops |
| Per-class counting | Total detection count plus one coloured badge per class (β¦ car, β‘ person) |
| Class filter | Checkbox list; filtering happens entirely client-side, no extra backend request |
| Loading & error handling | Spinner, human-readable errors, retry action, backend status badge with auto-reconnect |
| PNG export | Downloads the annotated result at full image resolution |
| Responsive design | Single/two-column layout, dark mode support |
The model is loaded exactly once. Weights are loaded into memory inside the FastAPI
lifespan; /predict only runs inference. A warm-up pass runs at startup so even the first
real request is fast (~100 ms).
The event loop is never blocked. Ultralytics predict is blocking and not thread-safe,
so the call runs in a separate thread via asyncio.to_thread, guarded by a lock.
Label collision avoidance. So that labels of nearby boxes never overlap, each label gets a
list of candidate positions in order of preference (above the box β inside the top edge β
stepping down inside the box β below the box), and the first candidate that does not collide
with an already-placed label wins. If every candidate is taken, the one with the smallest
overlap area is used. The logic lives in utils/labelLayout.js as a pure, testable function.
Guaranteed coordinate alignment. Boxes are drawn into the same bitmap as the image, using
one shared scale factor. In CSS only width is set and the height follows from the aspect
ratio, so the browser scales that bitmap by a single uniform factor β boxes can never drift
away from the image, and no runtime scaleX/scaleY computation is needed.
Stale responses can never win. When the slider is dragged, the in-flight request is
cancelled with AbortController, so a late response cannot overwrite a newer one.
Layered structure. On the backend routers only deal with HTTP while services hold the
business logic; the frontend separates api / hooks / components / utils.
EXIF orientation is applied, so boxes stay aligned on photos taken with a phone.
- Pretrained model: trained on the COCO dataset, recognises 80 classes (person, car, dog, bus, chair β¦). No training required.
- The
s(small) variant: the sweet spot between accuracy and speed.nis faster but less accurate;m/lare more accurate but noticeably slower on CPU.sruns in roughly 100 ms per image even on CPU. - One-line API:
YOLO("yolo26s.pt")β weights download automatically on first run. - To try another variant, just set
MODEL_NAME=yolo26m.ptinbackend/.env.
Note
The yolo26s.pt weights file (~20 MB) is downloaded automatically by Ultralytics on first
startup and placed in backend/models/. The first launch therefore takes a few extra
seconds and needs an internet connection. Later launches take about 2 seconds.
Requirements: Python 3.10+ Β· Node.js 18+
git clone https://github.com/ismailbilalakbulut/object-detection-counter.git
cd object-detection-counterYou need two terminals: one for the backend, one for the frontend.
# Create and activate the virtual environment (from the project root)
python -m venv .venv
.\.venv\Scripts\Activate.ps1
# Dependencies
cd backend
pip install -r requirements.txt
# Start the server
uvicorn main:app --reloadLinux / macOS
python3 -m venv .venv
source .venv/bin/activate
cd backend && pip install -r requirements.txt
uvicorn main:app --reloadBackend β http://localhost:8000 Β· Interactive API docs β http://localhost:8000/docs
The API is ready once you see Model hazir (... 80 sinif, cihaz=cpu) in the console.
cd frontend
npm install
copy .env.example .env # Linux/macOS: cp .env.example .env
npm run devFrontend β http://localhost:5173
Open that address in your browser. If the badge in the top right reads "BaΔlΔ±" (connected), the backend is reachable.
- Drag an image in, or pick one with Dosya seΓ§ (choose file).
- Detection runs automatically at the default 70% threshold and boxes are drawn.
- Move the GΓΌven eΕiΔi (confidence threshold) slider β a new request fires ~0.4 s after you let go. Lowering it surfaces more detections, raising it keeps only confident ones.
- Use the SΔ±nΔ±f filtresi (class filter) checkboxes to choose which classes are drawn β instant, with no extra request.
- Save the annotated image with PNG indir (download PNG).
| Body | multipart/form-data, field name: file |
| Query | confidence_threshold β float, 0.0β1.0, default 0.7 |
curl -X POST "http://localhost:8000/predict?confidence_threshold=0.7" \
-F "file=@image.jpg"Box coordinates are in the original image's pixel space; scaling happens on the frontend.
| Code | Situation |
|---|---|
400 |
Empty file, corrupt or unreadable image |
413 |
File exceeds the 10 MB limit, or the resolution is too high |
415 |
Unsupported file type (JPG, PNG, WEBP, BMP are accepted) |
422 |
confidence_threshold outside the 0β1 range |
500 |
Model inference error |
503 |
Model not loaded yet |
| Endpoint | Description |
|---|---|
GET /health |
Service and model status (status, device, num_classes) |
GET /classes |
The 80 classes the model recognises |
GET /docs |
Swagger UI |
Both sides are configured through environment variables β no address is hardcoded.
backend/.env (optional β see .env.example)
| Variable | Default | Description |
|---|---|---|
MODEL_NAME |
yolo26s.pt |
Weights to use (yolo26n/s/m/l) |
DEVICE |
auto | cpu, cuda, 0 β¦ |
DEFAULT_CONFIDENCE_THRESHOLD |
0.7 |
Default threshold |
MAX_UPLOAD_BYTES |
10485760 |
Upload limit (10 MB) |
CORS_ORIGINS |
["http://localhost:5173", β¦] |
Allowed frontend origins |
frontend/.env
| Variable | Default | Description |
|---|---|---|
VITE_API_BASE_URL |
http://localhost:8000 |
Backend root URL |
Tip
If you move the frontend to another port, remember to add that origin to CORS_ORIGINS
in backend/.env as well.
objectCounter/
βββ backend/
β βββ main.py # FastAPI entry point, CORS, lifespan
β βββ requirements.txt
β βββ .env.example
β βββ models/ # Downloaded YOLO weights (created automatically)
β βββ app/
β βββ config.py # Settings (pydantic-settings, overridable via .env)
β βββ schemas.py # Request/response models
β βββ routers/
β β βββ predict.py # POST /predict
β β βββ health.py # GET /health, GET /classes
β βββ services/
β βββ detector.py # YOLO wrapper (model loaded once)
β βββ image_loader.py # File validation + conversion to PIL
βββ frontend/
β βββ .env.example # VITE_API_BASE_URL
β βββ src/
β βββ App.jsx # State management and layout
β βββ config.js # API address and client settings
β βββ api/detectionApi.js # fetch wrapper + error types
β βββ hooks/ # useDebouncedValue, useObjectDetection, useBackendHealth
β βββ utils/ # labelLayout (label placement), colour palette, formatting
β βββ components/ # ImageUploader, DetectionCanvas, ConfidenceSlider,
β # CountsPanel, ClassFilter, Spinner, ErrorBanner, Header
βββ docs/ # README assets
| Symptom | Fix |
|---|---|
| Badge shows "Backend kapalΔ±" | Is uvicorn main:app --reload running in Terminal 1, and is port 8000 free? The badge recovers on its own once the backend is up. |
| CORS error in the browser console | The frontend origin must be listed in backend/.env β CORS_ORIGINS |
| First startup takes very long | Weights are downloading (~20 MB); this happens only once |
| "Nesne bulunamadΔ±" (no objects found) | Lower the confidence threshold; the model only knows the 80 COCO classes |
| Vite opens on 5174 instead of 5173 | A previous dev server is still running: find it with Get-NetTCPConnection -State Listen -LocalPort 5173 and stop it |
.env is ignored |
The file must be UTF-8 without BOM; PowerShell's Set-Content -Encoding utf8 adds a BOM |
Backend: FastAPI 0.141 Β· Uvicorn Β· Ultralytics 8.4 (YOLO26s) Β· Pillow Β· pydantic-settings Frontend: React 19 Β· Vite 8 Β· CSS Modules Β· oxlint
This project is licensed under the GNU Affero General Public License v3.0 (AGPL-3.0).
That is not an arbitrary choice: Ultralytics YOLO and its pretrained weights are themselves AGPL-3.0, a strong copyleft licence whose network clause requires that anyone interacting with the software over a network can obtain its source code. Ultralytics reads this as covering any project that links to their models, so a project built on YOLO is released under AGPL-3.0 as well. Using YOLO inside a closed-source or commercial product instead requires a paid Ultralytics Enterprise License.

{ "detections": [ { "label": "car", "score": 0.9435, "box": { "xmin": 667.65, "ymin": 395.18, "xmax": 809.66, "ymax": 880.56 }, "class_id": 2 } ], "counts": { "car": 7, "person": 2, "traffic light": 2 }, "total": 11, "confidence_threshold": 0.7, "model_name": "yolo26s.pt", "inference_ms": 186.54, "image": { "filename": "image.jpg", "width": 740, "height": 493 } }