Spaces:
Sleeping
Sleeping
Upload 16 files
Browse files- README.md +28 -26
- app.py +103 -26
- car_core/__init__.py +1 -0
- car_core/color_detect.py +42 -0
- car_core/exporter.py +42 -0
- car_core/issues.py +50 -0
- car_core/models_zeroshot.py +20 -0
- car_core/pricing.py +70 -0
- car_core/specs_model_space.py +9 -0
- configs/models.yaml +5 -0
- configs/parts_catalog.yaml +32 -0
- configs/regions.yaml +18 -0
- requirements.txt +1 -0
README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
| 1 |
---
|
| 2 |
-
title:
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: gradio
|
| 7 |
sdk_version: "4.36.1"
|
| 8 |
app_file: app.py
|
|
@@ -11,44 +11,46 @@ license: mit
|
|
| 11 |
tags:
|
| 12 |
- automotive
|
| 13 |
- computer-vision
|
|
|
|
| 14 |
- gradio
|
| 15 |
-
- tata
|
| 16 |
---
|
| 17 |
|
| 18 |
-
#
|
| 19 |
|
| 20 |
-
An
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
|
| 22 |
-
|
|
|
|
|
|
|
| 23 |
```bash
|
| 24 |
pip install -r requirements.txt
|
| 25 |
python app.py
|
| 26 |
```
|
| 27 |
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
- **Model ID**: Zero-shot CLIP baseline over common Tata models (Nexon, Altroz, Tiago, Punch, Harrier, Safari, Tigor, etc.). Optional fine-tuning script included.
|
| 32 |
-
- **Color**: Dominant body color via KMeans in LAB space with named-color snapping.
|
| 33 |
-
- **Autofill**: Specs pulled from `data/tata_specs.yaml` using the predicted model.
|
| 34 |
-
|
| 35 |
-
## Train on your dataset
|
| 36 |
-
- Put images under `data/your_dataset/images/` and labels in `data/your_dataset/annotations.csv`:
|
| 37 |
```csv
|
| 38 |
image_path,label
|
| 39 |
-
images/
|
| 40 |
```
|
| 41 |
-
|
| 42 |
```bash
|
| 43 |
python training/train_classifier.py --data_root data/your_dataset --annotations data/your_dataset/annotations.csv --out_dir checkpoints/vision
|
| 44 |
```
|
| 45 |
|
| 46 |
-
##
|
| 47 |
-
|
| 48 |
-
|
| 49 |
|
| 50 |
-
|
| 51 |
-
|
| 52 |
|
| 53 |
-
|
| 54 |
-
|
|
|
|
|
|
| 1 |
---
|
| 2 |
+
title: Car Analysis Advisor (Multi‑Image, Deterministic)
|
| 3 |
+
emoji: 🛠️
|
| 4 |
+
colorFrom: purple
|
| 5 |
+
colorTo: indigo
|
| 6 |
sdk: gradio
|
| 7 |
sdk_version: "4.36.1"
|
| 8 |
app_file: app.py
|
|
|
|
| 11 |
tags:
|
| 12 |
- automotive
|
| 13 |
- computer-vision
|
| 14 |
+
- service-advisor
|
| 15 |
- gradio
|
|
|
|
| 16 |
---
|
| 17 |
|
| 18 |
+
# Car Analysis Advisor (Hugging Face Space)
|
| 19 |
|
| 20 |
+
An end‑to‑end **car analysis AI** that ingests **multiple images** of a vehicle and outputs **precise, actionable recommendations**:
|
| 21 |
+
- **Model** (zero‑shot CLIP baseline; optional fine‑tune)
|
| 22 |
+
- **Color** (dominant body color with named snapping)
|
| 23 |
+
- **Issues** (mechanical/aesthetic) → **deterministic** final decisions
|
| 24 |
+
- **Exact price estimate** (parts + labor + region multipliers)
|
| 25 |
+
- **PDF & JSON** export
|
| 26 |
|
| 27 |
+
No probability scores are exposed—only clear decisions & totals.
|
| 28 |
+
|
| 29 |
+
## Run locally
|
| 30 |
```bash
|
| 31 |
pip install -r requirements.txt
|
| 32 |
python app.py
|
| 33 |
```
|
| 34 |
|
| 35 |
+
## Train (optional, improves accuracy)
|
| 36 |
+
See `training/train_classifier.py` for a ViT fine‑tuning script.
|
| 37 |
+
Dataset CSV:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 38 |
```csv
|
| 39 |
image_path,label
|
| 40 |
+
images/nexon_001.jpg,Tata Nexon
|
| 41 |
```
|
| 42 |
+
Then:
|
| 43 |
```bash
|
| 44 |
python training/train_classifier.py --data_root data/your_dataset --annotations data/your_dataset/annotations.csv --out_dir checkpoints/vision
|
| 45 |
```
|
| 46 |
|
| 47 |
+
## Pricing configuration
|
| 48 |
+
- `configs/parts_catalog.yaml` — parts & standard labor hours per issue
|
| 49 |
+
- `configs/regions.yaml` — labor rates & part multipliers per region (example values provided)
|
| 50 |
|
| 51 |
+
## Deterministic decisions
|
| 52 |
+
Internally, scores are computed but **final outputs are rule‑based** (top evidence & thresholds) to provide **unambiguous** issue lists and **exact** costs.
|
| 53 |
|
| 54 |
+
## Exports
|
| 55 |
+
- `exports/report.pdf`
|
| 56 |
+
- `exports/report.json`
|
app.py
CHANGED
|
@@ -1,43 +1,120 @@
|
|
| 1 |
-
import os,
|
| 2 |
-
from typing import
|
| 3 |
import gradio as gr
|
| 4 |
from PIL import Image
|
| 5 |
|
| 6 |
-
from
|
| 7 |
-
from
|
| 8 |
-
from
|
|
|
|
|
|
|
|
|
|
| 9 |
|
| 10 |
-
|
| 11 |
-
|
|
|
|
| 12 |
|
| 13 |
-
def
|
| 14 |
-
if image
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
|
|
|
|
|
|
|
|
|
| 19 |
|
| 20 |
-
|
| 21 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
|
| 23 |
-
#
|
| 24 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
}
|
|
|
|
| 31 |
|
| 32 |
-
with gr.Blocks(fill_height=True
|
| 33 |
-
gr.Markdown("##
|
| 34 |
with gr.Row():
|
| 35 |
with gr.Column(scale=1):
|
| 36 |
-
|
|
|
|
| 37 |
run = gr.Button("Analyze", variant="primary")
|
| 38 |
with gr.Column(scale=1):
|
| 39 |
-
out = gr.JSON(label="
|
| 40 |
-
|
|
|
|
|
|
|
| 41 |
|
| 42 |
if __name__ == "__main__":
|
| 43 |
demo.launch()
|
|
|
|
| 1 |
+
import os, io, base64
|
| 2 |
+
from typing import List, Dict, Any
|
| 3 |
import gradio as gr
|
| 4 |
from PIL import Image
|
| 5 |
|
| 6 |
+
from car_core.specs_model_space import build_label_space
|
| 7 |
+
from car_core.models_zeroshot import ModelIDZeroShot
|
| 8 |
+
from car_core.color_detect import dominant_color
|
| 9 |
+
from car_core.issues import detect_issues
|
| 10 |
+
from car_core.pricing import price_issues, load_regions
|
| 11 |
+
from car_core.exporter import export_pdf, export_json
|
| 12 |
|
| 13 |
+
LABEL_SPACE = build_label_space()
|
| 14 |
+
MODEL = ModelIDZeroShot(LABEL_SPACE)
|
| 15 |
+
REGIONS = load_regions()
|
| 16 |
|
| 17 |
+
def _to_pil(obj):
|
| 18 |
+
if isinstance(obj, dict) and "image" in obj:
|
| 19 |
+
# Gradio File with base64 'image' key
|
| 20 |
+
return Image.open(io.BytesIO(base64.b64decode(obj["image"].split(",")[-1])))
|
| 21 |
+
if isinstance(obj, str):
|
| 22 |
+
return Image.open(obj)
|
| 23 |
+
if hasattr(obj, "read"):
|
| 24 |
+
return Image.open(obj)
|
| 25 |
+
return obj
|
| 26 |
|
| 27 |
+
def analyze(images: list, region: str):
|
| 28 |
+
if not images:
|
| 29 |
+
raise gr.Error("Upload at least one car image.")
|
| 30 |
+
imgs = []
|
| 31 |
+
for it in images:
|
| 32 |
+
try:
|
| 33 |
+
imgs.append(_to_pil(it))
|
| 34 |
+
except Exception:
|
| 35 |
+
pass
|
| 36 |
+
if not imgs:
|
| 37 |
+
raise gr.Error("Failed to decode images. Use JPG/PNG.")
|
| 38 |
|
| 39 |
+
# Model decision: pick the most frequent top label across images
|
| 40 |
+
votes = {}
|
| 41 |
+
for im in imgs:
|
| 42 |
+
lbl = MODEL.top_label(im)
|
| 43 |
+
votes[lbl] = votes.get(lbl, 0) + 1
|
| 44 |
+
model_final = sorted(votes.items(), key=lambda kv: kv[1], reverse=True)[0][0]
|
| 45 |
|
| 46 |
+
# Color decision: take the most frequent named color across images
|
| 47 |
+
color_votes = {}
|
| 48 |
+
for im in imgs:
|
| 49 |
+
c = dominant_color(im)["name"]
|
| 50 |
+
color_votes[c] = color_votes.get(c, 0) + 1
|
| 51 |
+
color_name = sorted(color_votes.items(), key=lambda kv: kv[1], reverse=True)[0][0]
|
| 52 |
+
color_any = None
|
| 53 |
+
# Recompute once to get rgb/hex for the chosen name
|
| 54 |
+
for im in imgs:
|
| 55 |
+
d = dominant_color(im)
|
| 56 |
+
if d["name"] == color_name:
|
| 57 |
+
color_any = d; break
|
| 58 |
+
color_final = color_any or {"name": color_name, "rgb": (0,0,0), "hex": "#000000"}
|
| 59 |
+
|
| 60 |
+
# Issues (deterministic)
|
| 61 |
+
issues = detect_issues(imgs)
|
| 62 |
+
|
| 63 |
+
# Pricing
|
| 64 |
+
pricing = price_issues(issues, region_code=region)
|
| 65 |
+
|
| 66 |
+
payload = {
|
| 67 |
+
"vehicle": {"model": model_final, "color": color_final},
|
| 68 |
+
"region": region,
|
| 69 |
+
"issues": issues,
|
| 70 |
+
"pricing": pricing
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
os.makedirs("exports", exist_ok=True)
|
| 74 |
+
pdf_path = "exports/report.pdf"
|
| 75 |
+
json_path = "exports/report.json"
|
| 76 |
+
export_pdf(payload, pdf_path)
|
| 77 |
+
export_json(payload, json_path)
|
| 78 |
+
|
| 79 |
+
def to_dl(path):
|
| 80 |
+
with open(path, "rb") as f:
|
| 81 |
+
return (os.path.basename(path), f.read())
|
| 82 |
+
|
| 83 |
+
# Deterministic, actionable output only
|
| 84 |
+
result = {
|
| 85 |
+
"vehicle": payload["vehicle"],
|
| 86 |
+
"region": pricing["region"],
|
| 87 |
+
"currency": pricing["currency"],
|
| 88 |
+
"issues_with_solutions": [
|
| 89 |
+
{
|
| 90 |
+
"issue": it["issue"],
|
| 91 |
+
"solution": it["solution"],
|
| 92 |
+
"labor_hours": it["labor_hours"],
|
| 93 |
+
"labor_cost": it["labor_cost"],
|
| 94 |
+
"parts_cost": it["parts_cost"],
|
| 95 |
+
"line_total": it["line_total"]
|
| 96 |
+
} for it in pricing["items"]
|
| 97 |
+
],
|
| 98 |
+
"totals": {
|
| 99 |
+
"subtotal": pricing["subtotal"],
|
| 100 |
+
"tax": pricing["tax"],
|
| 101 |
+
"grand_total": pricing["grand_total"]
|
| 102 |
+
}
|
| 103 |
}
|
| 104 |
+
return result, to_dl(pdf_path), to_dl(json_path)
|
| 105 |
|
| 106 |
+
with gr.Blocks(fill_height=True) as demo:
|
| 107 |
+
gr.Markdown("## 🛠️ Car Analysis Advisor — multi‑image model/color/issue detection with exact pricing")
|
| 108 |
with gr.Row():
|
| 109 |
with gr.Column(scale=1):
|
| 110 |
+
imgs = gr.File(label="Upload car image(s)", file_count="multiple", file_types=["image"])
|
| 111 |
+
region = gr.Dropdown(choices=list(REGIONS["regions"].keys()), value=REGIONS.get("default_region","IN-HYD"), label="Region (pricing)")
|
| 112 |
run = gr.Button("Analyze", variant="primary")
|
| 113 |
with gr.Column(scale=1):
|
| 114 |
+
out = gr.JSON(label="Actionable results")
|
| 115 |
+
pdf = gr.File(label="Download PDF")
|
| 116 |
+
jj = gr.File(label="Download JSON")
|
| 117 |
+
run.click(analyze, inputs=[imgs, region], outputs=[out, pdf, jj])
|
| 118 |
|
| 119 |
if __name__ == "__main__":
|
| 120 |
demo.launch()
|
car_core/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
__version__ = '0.1.0'
|
car_core/color_detect.py
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import Dict, Tuple
|
| 2 |
+
import numpy as np
|
| 3 |
+
from PIL import Image
|
| 4 |
+
from sklearn.cluster import KMeans
|
| 5 |
+
|
| 6 |
+
PALETTE = {
|
| 7 |
+
"White": (255,255,255),
|
| 8 |
+
"Black": (0,0,0),
|
| 9 |
+
"Silver": (192,192,192),
|
| 10 |
+
"Grey": (128,128,128),
|
| 11 |
+
"Red": (200,0,0),
|
| 12 |
+
"Blue": (0,80,180),
|
| 13 |
+
"Dark Blue": (0,40,100),
|
| 14 |
+
"Green": (0,150,0),
|
| 15 |
+
"Dark Green": (0,90,0),
|
| 16 |
+
"Yellow": (240,210,0),
|
| 17 |
+
"Orange": (255,130,0),
|
| 18 |
+
"Brown": (120,70,25),
|
| 19 |
+
"Beige": (210,190,150),
|
| 20 |
+
"Teal": (0,120,120),
|
| 21 |
+
"Purple": (110,0,140),
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
def _nearest(rgb):
|
| 25 |
+
r,g,b = rgb
|
| 26 |
+
best = None; dmin = 1e9
|
| 27 |
+
for name,(R,G,B) in PALETTE.items():
|
| 28 |
+
d = (r-R)**2 + (g-G)**2 + (b-B)**2
|
| 29 |
+
if d < dmin: dmin=d; best=name
|
| 30 |
+
return best
|
| 31 |
+
|
| 32 |
+
def dominant_color(image: Image.Image) -> Dict:
|
| 33 |
+
img = image.convert("RGB").resize((256,256))
|
| 34 |
+
arr = np.array(img).reshape(-1,3).astype("float32")
|
| 35 |
+
mask = (arr.mean(axis=1) > 25) & (arr.mean(axis=1) < 245)
|
| 36 |
+
arr = arr[mask] if mask.sum()>100 else arr
|
| 37 |
+
km = KMeans(n_clusters=4, n_init=4, random_state=42).fit(arr)
|
| 38 |
+
centers = km.cluster_centers_.astype(int)
|
| 39 |
+
labels, counts = np.unique(km.labels_, return_counts=True)
|
| 40 |
+
idx = int(labels[np.argmax(counts)])
|
| 41 |
+
rgb = tuple(map(int, centers[idx]))
|
| 42 |
+
return {"name": _nearest(rgb), "rgb": rgb, "hex": "#%02x%02x%02x" % rgb}
|
car_core/exporter.py
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
from reportlab.lib.pagesizes import A4
|
| 3 |
+
from reportlab.lib import colors
|
| 4 |
+
from reportlab.lib.styles import getSampleStyleSheet
|
| 5 |
+
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle
|
| 6 |
+
|
| 7 |
+
def export_json(payload, path):
|
| 8 |
+
with open(path, "w", encoding="utf-8") as f:
|
| 9 |
+
json.dump(payload, f, ensure_ascii=False, indent=2)
|
| 10 |
+
return path
|
| 11 |
+
|
| 12 |
+
def export_pdf(payload, path):
|
| 13 |
+
doc = SimpleDocTemplate(path, pagesize=A4)
|
| 14 |
+
styles = getSampleStyleSheet()
|
| 15 |
+
story = []
|
| 16 |
+
story.append(Paragraph("<b>Car Analysis Advisor Report</b>", styles["Title"]))
|
| 17 |
+
story.append(Spacer(1, 8))
|
| 18 |
+
story.append(Paragraph(f"<b>Region:</b> {payload['pricing']['region']} | <b>Currency:</b> {payload['pricing']['currency']}", styles["Normal"]))
|
| 19 |
+
story.append(Spacer(1, 8))
|
| 20 |
+
story.append(Paragraph(f"<b>Model:</b> {payload['vehicle']['model']} | <b>Color:</b> {payload['vehicle']['color']['name']} ({payload['vehicle']['color']['hex']})", styles["Normal"]))
|
| 21 |
+
story.append(Spacer(1, 10))
|
| 22 |
+
story.append(Paragraph("<b>Issues & Solutions</b>", styles["Heading2"]))
|
| 23 |
+
data = [["Issue","Solution","Labor (hrs)","Labor","Parts","Line Total"]]
|
| 24 |
+
for it in payload["pricing"]["items"]:
|
| 25 |
+
parts_cost = f"{payload['pricing']['currency']} {it['parts_cost']:.2f}"
|
| 26 |
+
data.append([it["issue"].replace('_',' '), it["solution"], f"{it['labor_hours']:.2f}",
|
| 27 |
+
f"{payload['pricing']['currency']} {it['labor_cost']:.2f}", parts_cost,
|
| 28 |
+
f"{payload['pricing']['currency']} {it['line_total']:.2f}"])
|
| 29 |
+
table = Table(data, hAlign="LEFT", colWidths=[90,220,70,70,70,80])
|
| 30 |
+
table.setStyle(TableStyle([
|
| 31 |
+
('BACKGROUND',(0,0),(-1,0),colors.darkblue),
|
| 32 |
+
('TEXTCOLOR',(0,0),(-1,0),colors.whitesmoke),
|
| 33 |
+
('GRID',(0,0),(-1,-1),0.25,colors.grey),
|
| 34 |
+
('ALIGN',(2,1),(-1,-1),'CENTER')
|
| 35 |
+
]))
|
| 36 |
+
story.append(table)
|
| 37 |
+
story.append(Spacer(1, 8))
|
| 38 |
+
story.append(Paragraph(f"<b>Subtotal:</b> {payload['pricing']['currency']} {payload['pricing']['subtotal']:.2f}", styles["Normal"]))
|
| 39 |
+
story.append(Paragraph(f"<b>Tax:</b> {payload['pricing']['currency']} {payload['pricing']['tax']:.2f}", styles["Normal"]))
|
| 40 |
+
story.append(Paragraph(f"<b>Grand Total:</b> {payload['pricing']['currency']} {payload['pricing']['grand_total']:.2f}", styles["Heading3"]))
|
| 41 |
+
doc.build(story)
|
| 42 |
+
return path
|
car_core/issues.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import Dict, List
|
| 2 |
+
import numpy as np
|
| 3 |
+
from PIL import Image, ImageFilter
|
| 4 |
+
|
| 5 |
+
ISSUE_LIST = [
|
| 6 |
+
"scratch_dent","paint_damage","cracked_windshield","flat_tire","engine_leak",
|
| 7 |
+
"brake_wear","headlight_fault","battery_corrosion","rust","bumper_damage"
|
| 8 |
+
]
|
| 9 |
+
|
| 10 |
+
def _edge_contrast(im: Image.Image) -> float:
|
| 11 |
+
g = im.convert("L").resize((256,256))
|
| 12 |
+
arr = np.array(g, dtype=np.float32)/255.0
|
| 13 |
+
return float(arr.std())
|
| 14 |
+
|
| 15 |
+
def _redness(im: Image.Image) -> float:
|
| 16 |
+
rgb = im.convert("RGB").resize((256,256))
|
| 17 |
+
arr = np.array(rgb, dtype=np.float32)/255.0
|
| 18 |
+
return float(arr[:,:,0].mean())
|
| 19 |
+
|
| 20 |
+
def _darkness(im: Image.Image) -> float:
|
| 21 |
+
g = im.convert("L").resize((256,256))
|
| 22 |
+
arr = np.array(g, dtype=np.float32)/255.0
|
| 23 |
+
return float(1.0 - arr.mean())
|
| 24 |
+
|
| 25 |
+
def detect_issues(images: List[Image.Image]) -> List[str]:
|
| 26 |
+
"""Deterministic heuristics over multiple images; choose final set without probabilities."""
|
| 27 |
+
if not images: return ["diagnostic_inspection"]
|
| 28 |
+
# aggregate signals
|
| 29 |
+
contrast = np.mean([_edge_contrast(i) for i in images])
|
| 30 |
+
red = np.mean([_redness(i) for i in images])
|
| 31 |
+
dark = np.mean([_darkness(i) for i in images])
|
| 32 |
+
# thresholds chosen empirically; tune with dataset
|
| 33 |
+
issues = set()
|
| 34 |
+
if contrast > 0.22:
|
| 35 |
+
issues.update(["scratch_dent","paint_damage","bumper_damage"])
|
| 36 |
+
if red > 0.55:
|
| 37 |
+
issues.add("engine_leak")
|
| 38 |
+
if dark > 0.55:
|
| 39 |
+
issues.add("headlight_fault")
|
| 40 |
+
# Always narrow to a final deterministic list of max 4 using rule priority
|
| 41 |
+
priority = ["engine_leak","cracked_windshield","brake_wear","flat_tire","scratch_dent","paint_damage","bumper_damage","headlight_fault","battery_corrosion","rust"]
|
| 42 |
+
final = []
|
| 43 |
+
for p in priority:
|
| 44 |
+
if p in issues:
|
| 45 |
+
final.append(p)
|
| 46 |
+
if len(final) >= 4:
|
| 47 |
+
break
|
| 48 |
+
if not final:
|
| 49 |
+
final = ["diagnostic_inspection"]
|
| 50 |
+
return final
|
car_core/models_zeroshot.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import List, Tuple
|
| 2 |
+
import torch
|
| 3 |
+
from PIL import Image
|
| 4 |
+
from transformers import CLIPModel, CLIPProcessor
|
| 5 |
+
|
| 6 |
+
class ModelIDZeroShot:
|
| 7 |
+
def __init__(self, label_space: List[str]):
|
| 8 |
+
self.labels = label_space
|
| 9 |
+
device = "cuda" if torch.cuda.is_available() else "cpu"
|
| 10 |
+
self.device = device
|
| 11 |
+
self.model = CLIPModel.from_pretrained("openai/clip-vit-base-patch32").to(device)
|
| 12 |
+
self.processor = CLIPProcessor.from_pretrained("openai/clip-vit-base-patch32")
|
| 13 |
+
|
| 14 |
+
@torch.no_grad()
|
| 15 |
+
def top_label(self, image: Image.Image) -> str:
|
| 16 |
+
prompts = [f"A photo of a {x}" for x in self.labels]
|
| 17 |
+
inp = self.processor(text=prompts, images=image.convert("RGB"), return_tensors="pt", padding=True).to(self.device)
|
| 18 |
+
out = self.model(**inp)
|
| 19 |
+
idx = int(out.logits_per_image[0].argmax().item())
|
| 20 |
+
return self.labels[idx]
|
car_core/pricing.py
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import Dict, List
|
| 2 |
+
import yaml
|
| 3 |
+
|
| 4 |
+
def load_catalog(path="configs/parts_catalog.yaml"):
|
| 5 |
+
with open(path,"r",encoding="utf-8") as f:
|
| 6 |
+
return yaml.safe_load(f)
|
| 7 |
+
|
| 8 |
+
def load_regions(path="configs/regions.yaml"):
|
| 9 |
+
with open(path,"r",encoding="utf-8") as f:
|
| 10 |
+
return yaml.safe_load(f)
|
| 11 |
+
|
| 12 |
+
def price_issues(issues: List[str], region_code: str="IN-HYD", catalog=None, regions=None) -> Dict:
|
| 13 |
+
catalog = catalog or load_catalog()
|
| 14 |
+
regions = regions or load_regions()
|
| 15 |
+
parts = catalog.get("issues", {})
|
| 16 |
+
tax_rate = float(catalog.get("tax_rate", 0.18))
|
| 17 |
+
region = regions["regions"][region_code]
|
| 18 |
+
currency = region["currency"]
|
| 19 |
+
labor_rate = float(region["labor_rate_per_hour"])
|
| 20 |
+
p_mult = float(region["parts_multiplier"])
|
| 21 |
+
items = []
|
| 22 |
+
subtotal = 0.0
|
| 23 |
+
for label in issues:
|
| 24 |
+
if label == "diagnostic_inspection":
|
| 25 |
+
items.append({
|
| 26 |
+
"issue": label,
|
| 27 |
+
"solution": "Perform comprehensive inspection to localize faults.",
|
| 28 |
+
"labor_hours": 0.5,
|
| 29 |
+
"labor_cost": round(0.5*labor_rate,2),
|
| 30 |
+
"parts": [],
|
| 31 |
+
"parts_cost": 0.0,
|
| 32 |
+
"line_total": round(0.5*labor_rate,2),
|
| 33 |
+
"currency": currency
|
| 34 |
+
})
|
| 35 |
+
subtotal += 0.5*labor_rate
|
| 36 |
+
continue
|
| 37 |
+
spec = parts.get(label, {"parts": [], "labor_hours": 1.0})
|
| 38 |
+
hours = float(spec.get("labor_hours",1.0))
|
| 39 |
+
parts_cost = sum([float(p.get("cost",0.0))*p_mult for p in spec.get("parts",[])])
|
| 40 |
+
labor_cost = hours * labor_rate
|
| 41 |
+
line = labor_cost + parts_cost
|
| 42 |
+
solution = DEFAULT_SOLUTIONS.get(label, "Repair as per standard service procedure.")
|
| 43 |
+
items.append({
|
| 44 |
+
"issue": label,
|
| 45 |
+
"solution": solution,
|
| 46 |
+
"labor_hours": hours,
|
| 47 |
+
"labor_cost": round(labor_cost,2),
|
| 48 |
+
"parts": spec.get("parts",[]),
|
| 49 |
+
"parts_cost": round(parts_cost,2),
|
| 50 |
+
"line_total": round(line,2),
|
| 51 |
+
"currency": currency
|
| 52 |
+
})
|
| 53 |
+
subtotal += line
|
| 54 |
+
tax = round(tax_rate * subtotal,2)
|
| 55 |
+
grand = round(subtotal + tax,2)
|
| 56 |
+
return {"region": region_code, "currency": currency, "items": items, "subtotal": round(subtotal,2), "tax": tax, "grand_total": grand}
|
| 57 |
+
|
| 58 |
+
DEFAULT_SOLUTIONS = {
|
| 59 |
+
"scratch_dent": "Panel dent repair and surface leveling; finish with primer and paint blend.",
|
| 60 |
+
"paint_damage": "Prep, prime, color-match repaint, and clear coat application.",
|
| 61 |
+
"cracked_windshield": "Replace windshield and seal; calibrate sensors if equipped.",
|
| 62 |
+
"flat_tire": "Replace tire; balance and perform wheel alignment check.",
|
| 63 |
+
"engine_leak": "Identify leak source; replace gaskets/seals; top up fluids and clean bay.",
|
| 64 |
+
"brake_wear": "Replace brake pads; inspect rotors and bleed lines if required.",
|
| 65 |
+
"headlight_fault": "Replace bulb/assembly; verify wiring and aim beam.",
|
| 66 |
+
"battery_corrosion": "Clean terminals; replace corroded connectors; apply protective grease.",
|
| 67 |
+
"rust": "Sand, treat with rust converter; prime and protect underbody.",
|
| 68 |
+
"bumper_damage": "Replace or plastic-weld bumper; refit and paint as needed.",
|
| 69 |
+
"diagnostic_inspection": "Full diagnostic scan and physical inspection to isolate faults."
|
| 70 |
+
}
|
car_core/specs_model_space.py
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import yaml
|
| 2 |
+
|
| 3 |
+
def build_label_space(models_yaml="configs/models.yaml"):
|
| 4 |
+
cfg = yaml.safe_load(open(models_yaml, "r", encoding="utf-8"))
|
| 5 |
+
space = []
|
| 6 |
+
for make, models in cfg.get("makes_models", {}).items():
|
| 7 |
+
for m in models:
|
| 8 |
+
space.append(f"{make} {m}")
|
| 9 |
+
return space
|
configs/models.yaml
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
makes_models:
|
| 2 |
+
Tata: ["Tiago","Tigor","Altroz","Punch","Nexon","Harrier","Safari"]
|
| 3 |
+
Maruti: ["Swift","Baleno","Brezza","Dzire","Alto"]
|
| 4 |
+
Hyundai: ["i20","i10","Creta","Venue","Verna"]
|
| 5 |
+
Toyota: ["Corolla","Glanza","Urban Cruiser","Innova","Fortuner"]
|
configs/parts_catalog.yaml
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
issues:
|
| 2 |
+
scratch_dent:
|
| 3 |
+
parts: [{name: "Body filler & consumables", cost: 1800}]
|
| 4 |
+
labor_hours: 1.5
|
| 5 |
+
paint_damage:
|
| 6 |
+
parts: [{name: "Paint & materials", cost: 2500}]
|
| 7 |
+
labor_hours: 2.0
|
| 8 |
+
cracked_windshield:
|
| 9 |
+
parts: [{name: "Windshield glass", cost: 8000}, {name: "Sealant kit", cost: 900}]
|
| 10 |
+
labor_hours: 2.5
|
| 11 |
+
flat_tire:
|
| 12 |
+
parts: [{name: "New tire", cost: 4500}]
|
| 13 |
+
labor_hours: 0.6
|
| 14 |
+
engine_leak:
|
| 15 |
+
parts: [{name: "Gasket/seal kit", cost: 3200}, {name: "Engine oil", cost: 1800}]
|
| 16 |
+
labor_hours: 3.0
|
| 17 |
+
brake_wear:
|
| 18 |
+
parts: [{name: "Brake pads (pair)", cost: 3500}]
|
| 19 |
+
labor_hours: 1.4
|
| 20 |
+
headlight_fault:
|
| 21 |
+
parts: [{name: "Headlight bulb/assembly", cost: 2200}]
|
| 22 |
+
labor_hours: 0.8
|
| 23 |
+
battery_corrosion:
|
| 24 |
+
parts: [{name: "Battery terminals/cleaner", cost: 600}]
|
| 25 |
+
labor_hours: 0.5
|
| 26 |
+
rust:
|
| 27 |
+
parts: [{name: "Rust converter & primer", cost: 1000}]
|
| 28 |
+
labor_hours: 2.0
|
| 29 |
+
bumper_damage:
|
| 30 |
+
parts: [{name: "Bumper cover", cost: 7000}, {name: "Clips/fasteners", cost: 500}]
|
| 31 |
+
labor_hours: 2.2
|
| 32 |
+
tax_rate: 0.18 # 18% example
|
configs/regions.yaml
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
regions:
|
| 2 |
+
IN-HYD:
|
| 3 |
+
currency: "INR"
|
| 4 |
+
labor_rate_per_hour: 1200
|
| 5 |
+
parts_multiplier: 1.00
|
| 6 |
+
IN-MUM:
|
| 7 |
+
currency: "INR"
|
| 8 |
+
labor_rate_per_hour: 1400
|
| 9 |
+
parts_multiplier: 1.05
|
| 10 |
+
IN-DEL:
|
| 11 |
+
currency: "INR"
|
| 12 |
+
labor_rate_per_hour: 1350
|
| 13 |
+
parts_multiplier: 1.02
|
| 14 |
+
US-CA:
|
| 15 |
+
currency: "USD"
|
| 16 |
+
labor_rate_per_hour: 130
|
| 17 |
+
parts_multiplier: 1.30
|
| 18 |
+
default_region: IN-HYD
|
requirements.txt
CHANGED
|
@@ -8,3 +8,4 @@ numpy>=1.26.4
|
|
| 8 |
scikit-learn>=1.5.0
|
| 9 |
scikit-image>=0.23.2
|
| 10 |
pyyaml>=6.0.1
|
|
|
|
|
|
| 8 |
scikit-learn>=1.5.0
|
| 9 |
scikit-image>=0.23.2
|
| 10 |
pyyaml>=6.0.1
|
| 11 |
+
reportlab>=4.1.0
|