An end-to-end AI pipeline that detects, segments, validates, and scores car damage from a single photograph — running locally on GPU via a Flask web interface.
- Overview
- Tech Stack
- System Requirements
- Quick Start
- Project Structure
- Detailed Pipeline Flow
- Configuration Reference
- Models
- Car Part Classes
- Damage Classes & Severity Rules
- API Endpoints
- Validation Logic
- Health Score Formula
- Evaluation
- User Interface
- Acknowledgements
AutoScan AI is a Project which takes a photograph of a car and performs:
- Car detection — locates the vehicle using YOLOv8
- Parts segmentation — identifies up to 29 named car parts using RF-DETR
- Damage segmentation — detects 6 types of damage using RF-DETR with tile inference
- Validation — filters spatially impossible damage-part combinations
- Analytics — generates a 0–100 health score with per-part severity breakdown
- Visualization — overlays color-coded masks and labels on the original image
The entire inference pipeline runs on a local GPU (tested on RTX 5070 8 GB).
The frontend is a single-page dark-themed web app served by Flask.
| Layer | Technology |
|---|---|
| Web Framework | Flask |
| Car Detection | YOLOv8 (Ultralytics) |
| Parts & Damage Segmentation | RF-DETR (Roboflow) |
| Image Processing | OpenCV, Pillow, NumPy |
| GPU Inference | CUDA via PyTorch |
| Frontend | Vanilla HTML / CSS / JavaScript |
| Model Optimization | torch.jit.trace via optimize_for_inference() |
| Component | Minimum | Recommended |
|---|---|---|
| GPU VRAM | 6 GB | 8 GB |
| RAM | 8 GB | 16 GB |
| Python | 3.10+ | 3.13 |
| CUDA | 12.7+ | 12.9 |
| OS | Windows / Linux | Windows 11 |
Note: Input images are automatically resized to a max of 1920 px on the longest side before inference to prevent CUDA out-of-memory errors on high-resolution camera photos (e.g. 4608×3072). Configurable via
MAX_INPUT_RESOLUTIONinconfig.py.
# 1. Install dependencies
pip install -r requirements.txt
# 2. Edit config/config.py — set your model checkpoint paths
# YOLO_CHECKPOINT = r"path\to\yolov8m.pt"
# PARTS_CHECKPOINT = r"path\to\checkpoint_parts.pth"
# DAMAGE_CHECKPOINT = r"path\to\checkpoint_damage.pth"
# 3. Run
python app.py
# → Open http://localhost:5000car-damage-and-parts-segmentation/
├── app.py ← Flask entry point; registers blueprint, cleans uploads on start
├── requirements.txt
├── README.md ← Main README File
├── config/
│ └── config.py ← ALL tunable settings (paths, thresholds, resolutions, etc.)
├── uploads/ ← Uploaded images saved here; auto-cleaned on startup
│ └── processed/
├── base/
│ ├── templates/
│ │ └── car_analysis/
│ │ └── index.html ← Single-page frontend (dark industrial UI)
│ ├── static/
│ │ └── UI_Image/
│ │ └── UI_Screenshot.png ← App UI screenshot (used in README)
│ └── com/
│ ├── controller/
│ │ └── car_analysis_controller.py ← /analysis/upload and /analysis/render endpoints
│ └── service/
│ ├── image_analysis.py ← Stage 1: YOLO car bounding-box detection
│ ├── car_parts_segmentation.py ← Stage 2: RF-DETR 29-class parts segmentation
│ ├── car_damage_segmentation.py ← Stage 3: RF-DETR 6-class damage + tile inference
│ ├── damage_validation.py ← Stage 4: Spatial false-positive filter
│ ├── damage_analytics.py ← Stage 5: Health score + per-part severity report
│ ├── output_handler.py ← Pipeline orchestrator (calls stages 1–5)
│ └── coco_annotations.py ← Optional COCO JSON export
└── evaluation/
├── README.md ← Top-level evaluation overview
├── Car Damage/
│ ├── README.md ← CarDD dataset & damage model evaluation
│ ├── Testing/
│ │ └── evaluate_rfdetr_cardd_full.py ← Full test-set evaluation script
│ └── Training and Validation/
│ └── plot_training_metrics.py ← Training chart generator
└── Car Parts/
├── README.md ← Car parts dataset & model evaluation
├── Testing/
│ └── evaluate_rfdetr_carparts.py ← Full test-set evaluation script
└── Training and Validation/
└── plot_parts_metrics.py ← Training chart generator
User Uploads Image
│
▼
[Auto-Resize] ──── if > 1920px longest side → downscale (LANCZOS4)
│
▼
[Stage 1] YOLOv8 Car Detection
→ Finds the largest car bounding box
→ Adds 2% padding around it
→ Output: coords (x1, y1, x2, y2)
│
▼
[Stage 2] RF-DETR Parts Segmentation (RFDETRSegNano, 960px)
→ Pre-NMS: collapse overlapping duplicate raw-class masks (IoU > 0.30)
→ Converts 19 raw classes → 29 Left/Right named classes using car side
→ Singleton enforcement: drop duplicate detections for classes
that can only appear once (e.g. Left_Headlight)
→ Output: det_parts (masks + class IDs + labels)
│
▼
[Stage 3] RF-DETR Damage Segmentation (RFDETRSegMedium, 960px)
→ Full-ROI pass: detects damage on the entire car crop
→ Tile pass: 2×2 grid with 35% overlap → finds small/fine damage
(only crack, dent, scratch on tiles)
→ Smart merge NMS: merges overlapping/touching tile + full detections
→ Output: det_damage (masks + class IDs + confidence)
│
▼
[Stage 4] Damage Validation (Spatial Filter)
→ Cross-references damage masks against part masks
→ First, a rule that applies to EVERY damage type:
• if more than 60% of the damage mask lies off every named part
→ dropped as "mostly unknown region" (crack included)
→ Then type-specific rules, over the parts covering >= 20% of the mask:
• dent → must touch a part that is not glass / tyre / lamp
• glass shatter → must touch a glass part
• lamp broken → must touch a lamp AND nothing else
• tire flat → must touch the tyre
• scratch → must touch a part, and must touch NO glass
• crack → no part rule beyond the unknown-region check
→ Removes spatially impossible detections
→ Output: filtered det_damage
│
▼
[Stage 5] Analytics Engine
→ For each damage, finds which part(s) it overlaps by ≥15% AND
are permitted for that damage type
→ Computes damage_percent = overlap_pixels / part_pixels × 100
→ Applies severity rules per damage type and percentage
→ Sums severity penalties → Health Score = max(0, 100 − Σ penalties)
→ Output: JSON report (health score, per-part severity rows)
│
▼
[Drawing Pass]
→ Draws part masks on canvas (color-coded, with smooth contours)
→ Draws damage masks on canvas (semi-transparent overlay)
→ Adds labels at mask centroids
│
▼
[Response]
→ original_image (base64 JPEG)
→ annotated_image (base64 JPEG)
→ parts_detections + damage_detections (metadata for legend)
→ analytics (health score + damage table)
→ Detections stored in server session for re-render
│
▼
[Re-Render (no re-inference)]
User toggles visibility / opacity →
Frontend calls /analysis/render →
Server re-draws from stored session detections (no model called again)
Image filename checked for prefix
│
Has prefix? ──── YES ──→ Use prefix (e.g. "front_left_car.jpg" → "front_left")
│
NO
│
User selected from dropdown? ──── YES ──→ Use dropdown value as fallback
│
NO
│
Block analysis — return 422 error
"Car side could not be determined"
Valid side prefixes: front, rear, left, right, front_left, front_right, rear_left, rear_right
Raw model output (19 classes)
│
▼
Pre-NMS: For each raw class group
→ Sort detections by confidence (highest first)
→ Compute mask IoU between detections of same class
→ Suppress if IoU > 0.30 (keep highest confidence)
│
▼
Side-aware class conversion (19 → 29 classes)
→ Fixed classes (Grill, Hood, Bumpers, etc.) pass through as-is
→ Side-dependent classes (Fender, Door, Headlight, etc.)
get Left_ / Right_ prefix based on car side + box center X
→ Special: if rear view and 2 taillights detected →
left-most = Left_Taillight, right-most = Right_Taillight
│
▼
Singleton enforcement (post-conversion)
→ For each class that can appear once per image:
if count > 1 → keep only highest-confidence detection
Car ROI (cropped from original image)
│
├──→ Full-ROI pass → detect all 6 damage types
│
└──→ Tile pass (2×2 grid, 35% overlap):
For each tile:
→ Crop tile from ROI
→ Run RF-DETR (higher threshold = 0.55)
→ Only keep: crack, dent, scratch
→ Offset detections back to full image coords
│
▼
Smart Merge NMS across all detections:
→ Sort by confidence descending
→ For each detection, merge with others of same class if:
box IoU > 0.35 OR
mask IoU > 0.25 OR
mask coverage > 0.80 OR
masks touch within 3 px gap
→ Merged box = union of boxes, merged mask = union of masks
Most settings live in config/config.py. Three validation and attribution
thresholds are defined in the service files instead, because they are part of the
filtering logic rather than deployment configuration:
| Constant | Value | File |
|---|---|---|
OVERLAP_THRESHOLD |
0.20 | damage_validation.py |
UNKNOWN_DOMINANCE_THRESHOLD |
0.60 | damage_validation.py |
MIN_ATTRIBUTION_OVERLAP |
0.15 | damage_analytics.py |
# ── PATHS ─────────────────────────────────────────────────────────────
YOLO_CHECKPOINT = r"path\to\yolov8m.pt"
PARTS_CHECKPOINT = r"path\to\parts_model.pth"
DAMAGE_CHECKPOINT = r"path\to\damage_model.pth"
# ── INPUT LIMITS ──────────────────────────────────────────────────────
MAX_CONTENT_LENGTH = 20 * 1024 * 1024 # 20 MB max upload
MAX_INPUT_RESOLUTION = 1920 # Longest side cap before inference
# (set 0 to disable)
# ── MODEL RESOLUTIONS ─────────────────────────────────────────────────
PARTS_RESOLUTION = 960 # RF-DETR internal resolution for parts model
DAMAGE_RESOLUTION = 960 # RF-DETR internal resolution for damage model
# ── DETECTION THRESHOLDS ──────────────────────────────────────────────
PARTS_THRESHOLD = 0.45 # Minimum confidence to accept a parts detection
DAMAGE_THRESHOLD = 0.40 # Minimum confidence (full-ROI pass)
TILE_THRESHOLD = 0.55 # Minimum confidence (tile pass, higher = stricter)
# ── TILE INFERENCE ────────────────────────────────────────────────────
USE_TILE_INFERENCE = True
GRID_ROWS = 2
GRID_COLS = 2
OVERLAP_RATIO = 0.35 # 35% tile overlap to avoid missing edge damage
MIN_TILE_SIZE = 300 # Minimum tile size in pixels
# ── MERGE NMS ─────────────────────────────────────────────────────────
BOX_IOU_THRESHOLD = 0.35
MASK_IOU_THRESHOLD = 0.25
MASK_COVERAGE_THRESHOLD = 0.80
TOUCH_GAP_PIXELS = 3 # Masks within 3px are treated as touching
# ── ANALYTICS ─────────────────────────────────────────────────────────
MIN_EXTERNAL_PIXELS = 150 # Min damage pixels for "unknown" (unmatched) damage
PADDING_RATIO = 0.02 # 2% padding added around YOLO car bounding box
SEVERITY_RULES = {...} # Per-damage-type percentage bands (table below)
SEVERITY_PENALTY = {...} # Points deducted per severity level
# ── FLASK ───────────────────────────────────────────────────────────────────
SECRET_KEY = "..." # Session signing key - change before deploying
DEBUG = True # Development only; see the warning below
HOST = "0.0.0.0"
PORT = 5000
# ── MISC ────────────────────────────────────────────────────────────────────
ALLOWED_EXTENSIONS = {"png", "jpg", "jpeg", "webp"}
TILE_DAMAGE_NAMES = {"dent", "scratch", "crack"} # Kept from the tile pass
SAVE_COCO_DEFAULT = False # Write a COCO JSON alongside each analysisWarning — development defaults. The repository ships
DEBUG = TruewithHOST = "0.0.0.0". Flask's debugger allows arbitrary code execution through the browser, and0.0.0.0binds every network interface, so running as-is exposes a shell to anyone who can reach the port. SetDEBUG = Falseand replace the hard-codedSECRET_KEYbefore running this anywhere but your own machine.
| Model | Architecture | Task | Classes |
|---|---|---|---|
| YOLOv8m | YOLOv8 Medium | Car bounding box detection | COCO class 2 (car) |
| RFDETRSegNano | RF-DETR Nano + Segmentation | Car parts instance segmentation | 19 raw → 29 final |
| RFDETRSegMedium | RF-DETR Medium + Segmentation | Car damage instance segmentation | 6 damage types |
Both RF-DETR models are optimized at startup using model.optimize_for_inference(compile=True, batch_size=1, dtype=torch.float32) which applies torch.jit.trace for faster GPU kernel execution with zero quality loss.
The parts model outputs 29 final classes (19 raw model classes converted via side-aware logic):
Fixed (side-independent):
Diggi_Back_Door, Diggi_Back_Door_Glass, Front_Bumper,
Front_Windshield_Glass, Grill, Hood_Bonnet, Rear_Bumper, Roof, tyre
Left side:
Left_Fender, Left_Front_Door, Left_Front_Door_Glass,
Left_Headlight, Left_Quarter_Panel, Left_Rear_Door,
Left_Rear_Door_Glass, Left_Running_Board, Left_Side_Mirror, Left_Taillight
Right side:
Right_Fender, Right_Front_Door, Right_Front_Door_Glass,
Right_Headlight, Right_Quarter_Panel, Right_Rear_Door,
Right_Rear_Door_Glass, Right_Running_Board, Right_Side_Mirror, Right_Taillight
Singleton enforcement ensures the following 22 classes appear at most once per image:
all Left_* and Right_* classes + Diggi_Back_Door_Glass + Front_Windshield_Glass
| Class | Color |
|---|---|
| crack | Red #FF0000 |
| dent | Blue #0000FF |
| glass shatter | Cyan #00FFFF |
| lamp broken | Orange #FFA500 |
| scratch | Green #00FF00 |
| tire flat | Purple #800080 |
| Damage Type | Minor | Moderate | Major | Critical |
|---|---|---|---|---|
| scratch | ≤ 1.5% | ≤ 4% | ≤ 8% | > 8% |
| dent | ≤ 2% | ≤ 5% | ≤ 10% | > 10% |
| crack | ≤ 1% | ≤ 3% | ≤ 6% | > 6% |
| glass shatter | — | — | ≤ 5% | > 5% |
| lamp broken | — | — | ≤ 5% | > 5% |
| tire flat | — | — | — | Always |
| Severity | Penalty |
|---|---|
| Minor | −3 |
| Moderate | −6 |
| Major | −10 |
| Critical | −15 |
| Method | URL | Description |
|---|---|---|
| GET | / |
Main frontend page |
| POST | /analysis/upload |
Upload image → full inference pipeline |
| POST | /analysis/render |
Re-render stored detections (no inference) |
Form fields:
| Field | Type | Description |
|---|---|---|
image |
File | Car photograph (JPG/PNG/WEBP, max 20 MB) |
car_side |
String | Optional: front, rear, left, right, front_left, front_right, rear_left, rear_right |
show_parts |
1/0 |
Draw parts masks on annotated image |
show_damage |
1/0 |
Draw damage masks on annotated image |
parts_filled |
1/0 |
Filled vs. outline-only parts masks |
damage_filled |
1/0 |
Filled vs. outline-only damage masks |
parts_labels |
1/0 |
Show part name labels |
damage_labels |
1/0 |
Show damage type + confidence labels |
mask_alpha |
Float | Mask transparency 0.05–0.90 |
Important: Damage inference always runs regardless of
show_damage. The toggle only controls drawing. This ensures the health score and legend are always accurate even if you upload with damage overlay hidden.
Response:
{
"success": true,
"original_image": "<base64 JPEG>",
"annotated_image": "<base64 JPEG>",
"coords": [x1, y1, x2, y2],
"parts_warning": "",
"parts_detections": [
{ "class_name": "Hood_Bonnet", "confidence": 0.91, "color": "#FF3838" }
],
"damage_detections": [
{ "class_name": "dent", "confidence": 0.78, "color": "#0000FF" }
],
"analytics": {
"overall_health_score": 75,
"total_damages": 3,
"summary": "3 grouped damages found",
"parts": [
{
"part_name": "Hood_Bonnet",
"damage_type": "dent",
"severity": "Moderate",
"damage_percent": 4.2,
"confidence": 0.78,
"affected_pixels": 18340
}
]
}
}Re-renders the canvas with new toggle/opacity settings without re-running any model inference. Requires a previous /upload call in the same session.
JSON body:
{
"show_parts": true,
"parts_filled": true,
"parts_labels": true,
"show_damage": true,
"damage_filled": false,
"damage_labels": true,
"mask_alpha": 0.40
}Response:
{
"success": true,
"annotated_image": "<base64 JPEG>"
}The damage_validation.py service removes damage detections that cannot
physically sit where the parts model says they are.
Step 1 — the unknown-region check, applied to every damage type including
crack. A part counts as "overlapping" only if it covers at least
OVERLAP_THRESHOLD (20%) of the damage mask. If less than 40% of the mask lies
on any named part — i.e. more than UNKNOWN_DOMINANCE_THRESHOLD (60%) of it is
on unsegmented background — the detection is dropped outright. This check runs
before any type-specific rule, so it can discard a damage of any class.
Step 2 — the type-specific rule. Note that these are not symmetric: only
lamp broken is exclusive, meaning it is the only rule that rejects a detection
for also touching something else.
| Damage Type | Rule as implemented | Exclusive? |
|---|---|---|
| dent | must touch at least one part that is not glass, tyre, headlight or taillight | no |
| glass shatter | must touch at least one glass part | no — may also overlap other parts |
| lamp broken | must touch a lamp part and nothing else | yes |
| tire flat | must touch the tyre | no — may also overlap other parts |
| scratch | must touch a named part, and must touch no glass part | no |
| crack | no type rule; only the Step 1 check applies | — |
Glass parts are the six *_Glass classes; lamp parts are the four
Left_/Right_Headlight and Left_/Right_Taillight classes.
Health Score = max(0, 100 − Σ(penalty per damage row))
Where each damage row's penalty depends on its severity:
Minor → −3 points
Moderate → −6 points
Major → −10 points
Critical → −15 points
Severity is determined by:
damage_percent = (overlap_pixels / part_mask_pixels) × 100
→ looked up in per-damage-type severity threshold table
Unmatched external damage — damage for which no part was credited, meaning no
part both covered at least MIN_ATTRIBUTION_OVERLAP (15%) of the damage mask *and*
was permitted for that damage type — is handled as follows:
below MIN_EXTERNAL_PIXELS (150 px) → discarded; it does not appear at all
at or above 150 px → listed as an "Unknown_External" row
with severity "—" and penalty 0
Either way it never affects the health score: with no reference part area there
is no meaningful percentage, so no severity is assigned and no penalty applies.
Example:
- Left_Fender → scratch → 3.2% → Moderate → −6
- Hood_Bonnet → dent → 6.1% → Major → −10
- Left_Headlight → lamp broken → 7.3% → Critical → −15
Health Score = 100 − 6 − 10 − 15 = 69
The evaluation/ directory contains all scripts, charts, and results used to benchmark both RF-DETR models on their respective test sets. Full metric tables are kept inside the evaluation READMEs.
Download pre-trained model checkpoints (checkpoint_best_total.pth) and the corresponding test-set images + COCO annotations for both tasks from the link above.
| Folder | Dataset | Description |
|---|---|---|
evaluation/Car Damage/ |
CarDD | 374 test images · 785 annotations · 6 damage classes |
evaluation/Car Parts/ |
Eight public datasets, combined | 540 test images · 4,591 annotations · 19 part classes |
Both models were trained on images resized to 1152 × 1152, but evaluation does not repeat that resize. RF-DETR interpolates its mask logits to the size of the image it is handed and thresholds them there, so feeding the original image returns masks already in original coordinates — no re-projection, and nothing lost at the mask boundary.
Each sub-directory has its own README.md with full metric tables, per-category AP breakdowns, usage instructions for the evaluation scripts, and training/validation charts.
X. Wang, W. Li and Z. Wu, "CarDD: A New Dataset for Vision-Based Car Damage Detection," IEEE Transactions on Intelligent Transportation Systems, vol. 24, no. 7, pp. 7202-7214, July 2023, doi: 10.1109/TITS.2023.3258480. Project page: https://cardd-ustc.github.io/
No single public dataset covers every part class this pipeline needs, so the parts model was trained on a composite of eight public car-part segmentation datasets — 11,547 images pooled, 7,708 retained after de-duplication and remapping onto a unified 19-class taxonomy.
| # | dataset | platform | images | classes | licence |
|---|---|---|---|---|---|
| 1 | Car Parts Dataset (DSMLR, IT-KMITL) | GitHub | 500 | 18 | citation requested |
| 2 | Car parts - Segmentation | Roboflow Universe | 1,755 | 9 | CC BY 4.0 |
| 3 | Car Parts Segmentation - Person Detector | Roboflow Universe | 603 | 19 | CC BY 4.0 |
| 4 | Car parts - FleetBlox | Roboflow Universe | 1,862 | 33 | CC BY 4.0 |
| 5 | car-parts - Axion Technical Service | Roboflow Universe | 819 | 30 | CC BY 4.0 |
| 6 | car parts - Habibullah | Roboflow Universe | 2,866 | 16 | CC BY 4.0 |
| 7 | car-seg - Gianmarco Russo | Roboflow Universe | 2,255 | 21 | CC BY 4.0 |
| 8 | car-parts - Atheer Algarni | Roboflow Universe | 887 | 20 | CC BY 4.0 |
The DSMLR dataset sets no formal licence but requests citation of its paper:
K. Pasupa, P. Kittiworapanya, N. Hongngern and K. Woraratpanya, "Evaluation of deep learning algorithms for semantic segmentation of car parts," Complex & Intelligent Systems, 2021, pp. 1-13, doi: 10.1007/s40747-021-00397-8. Dataset: https://github.com/dsmlr/Car-Parts-Segmentation
The seven Roboflow Universe datasets, in table order: 2 · 3 · 4 · 5 · 6 · 7 · 8
- RF-DETR (Roboflow, Apache-2.0) — both segmentation models.
I. Robinson, P. Robicheaux, M. Popov, D. Ramanan and N. Peri, "RF-DETR: Neural Architecture Search for Real-Time Detection Transformers," arXiv:2511.09554, 2025. https://github.com/roboflow/rf-detr
- YOLOv8 (Ultralytics, AGPL-3.0) — vehicle localisation.
G. Jocher, A. Chaurasia and J. Qiu, "Ultralytics YOLOv8," 2023. https://github.com/ultralytics/ultralytics
- RF-DETR segmentation guide and fine-tuning notebook
