product-detection/pipeline_diagram.html

470 lines
19 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Product Detection Pipeline</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=DM+Mono:wght@400;500&family=Syne:wght@700;800&display=swap');
:root {
--bg: #0d0f12;
--surface: #161a20;
--border: #252a33;
--text: #dde3ee;
--muted: #5a6070;
--accent: #00e5a0;
--c-gray: #353b47;
--c-purple: #3d3489;
--c-teal: #0f6e56;
--c-coral: #993c1d;
--c-amber: #7a4f0a;
--t-gray: #b4b2a9;
--t-purple: #afa9ec;
--t-teal: #5dcaa5;
--t-coral: #f0997b;
--t-amber: #fac775;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
background: var(--bg);
color: var(--text);
font-family: 'DM Mono', monospace;
min-height: 100vh;
padding: 32px 24px 48px;
}
h1 {
font-family: 'Syne', sans-serif;
font-size: 22px;
font-weight: 800;
color: var(--accent);
margin-bottom: 6px;
}
.subtitle {
font-size: 12px;
color: var(--muted);
margin-bottom: 32px;
}
/* legend */
.legend {
display: flex;
flex-wrap: wrap;
gap: 16px;
margin-bottom: 28px;
font-size: 11px;
color: var(--muted);
}
.legend span { display: flex; align-items: center; gap: 6px; }
.dot { width: 10px; height: 10px; border-radius: 3px; flex-shrink: 0; }
/* pipeline wrapper */
.pipe { display: flex; flex-direction: column; align-items: center; gap: 0; }
/* node */
.node {
border-radius: 8px;
padding: 10px 20px;
text-align: center;
cursor: pointer;
transition: filter .15s, transform .15s;
border: 1px solid rgba(255,255,255,.1);
}
.node:hover { filter: brightness(1.2); transform: translateY(-2px); }
.node .label { font-size: 13px; font-weight: 500; }
.node .sub { font-size: 11px; opacity: .7; margin-top: 3px; }
.n-gray { background: var(--c-gray); color: var(--t-gray); }
.n-teal { background: var(--c-teal); color: var(--t-teal); }
.n-purple { background: var(--c-purple); color: var(--t-purple); }
.n-coral { background: var(--c-coral); color: var(--t-coral); }
.n-amber { background: var(--c-amber); color: var(--t-amber); }
/* arrow */
.arrow {
width: 1px;
background: var(--border);
position: relative;
flex-shrink: 0;
}
.arrow::after {
content: '';
position: absolute;
bottom: -1px;
left: 50%;
transform: translateX(-50%);
border-left: 5px solid transparent;
border-right: 5px solid transparent;
border-top: 7px solid var(--muted);
}
.arrow.h20 { height: 20px; }
.arrow.h28 { height: 28px; }
/* split row */
.split-row {
display: grid;
width: 100%;
gap: 16px;
align-items: start;
}
.split-row.two { grid-template-columns: 1fr 1fr; }
.split-row.three { grid-template-columns: 1fr 1fr 1fr; }
/* split connector line */
.split-line {
position: relative;
width: 100%;
height: 28px;
flex-shrink: 0;
}
.split-line svg { width: 100%; height: 100%; overflow: visible; }
/* merge connector */
.merge-line {
position: relative;
width: 100%;
height: 28px;
flex-shrink: 0;
}
.merge-line svg { width: 100%; height: 100%; overflow: visible; }
/* column: node + arrow stacked */
.col { display: flex; flex-direction: column; align-items: center; }
/* loop banner */
.loop-banner {
margin-top: 32px;
width: 100%;
border: 1px dashed var(--border);
border-radius: 8px;
padding: 12px 20px;
font-size: 12px;
color: var(--muted);
text-align: center;
line-height: 1.7;
}
.loop-banner strong { color: var(--accent); }
/* tooltip popup */
.tip {
display: none;
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: var(--surface);
border: 1px solid var(--border);
border-radius: 10px;
padding: 20px 24px;
max-width: 380px;
width: 90vw;
z-index: 100;
font-size: 13px;
line-height: 1.7;
color: var(--text);
box-shadow: 0 24px 60px rgba(0,0,0,.6);
}
.tip.open { display: block; }
.tip h3 { font-family:'Syne',sans-serif; font-size:15px; margin-bottom:8px; color: var(--accent); }
.tip .cmd { background: var(--bg); border-radius: 6px; padding: 8px 12px; font-size: 11px; color: var(--muted); margin-top: 10px; white-space: pre; line-height: 1.6; }
.tip-close { position:absolute; top:12px; right:14px; background:none; border:none; color:var(--muted); cursor:pointer; font-size:18px; }
.overlay { display:none; position:fixed; inset:0; z-index:99; }
.overlay.open { display:block; }
/* section label */
.section-tag {
font-size: 10px;
letter-spacing: 1.2px;
text-transform: uppercase;
color: var(--muted);
margin-bottom: 8px;
align-self: flex-start;
width: 100%;
}
</style>
</head>
<body>
<h1>Product Detection Pipeline</h1>
<p class="subtitle">Click any block to see details and the command that runs it.</p>
<div class="legend">
<span><span class="dot" style="background:var(--c-gray)"></span> Data</span>
<span><span class="dot" style="background:var(--c-teal)"></span> Models</span>
<span><span class="dot" style="background:var(--c-purple)"></span> Scripts</span>
<span><span class="dot" style="background:var(--c-coral)"></span> Deploy</span>
<span><span class="dot" style="background:var(--c-amber)"></span> Manual step</span>
</div>
<div class="pipe" id="pipe">
<!-- source -->
<div class="node n-gray" style="width:220px" onclick="show('src')">
<div class="label">Shelf images</div>
<div class="sub">jpg / png / mp4 / webcam</div>
</div>
<div class="arrow h28"></div>
<!-- detector -->
<div class="node n-teal" style="width:280px" onclick="show('det')">
<div class="label">YOLOv8n detector</div>
<div class="sub">Detect &amp; crop every product</div>
</div>
<!-- fork label -->
<div class="split-line">
<svg viewBox="0 0 600 28" preserveAspectRatio="none">
<line x1="300" y1="0" x2="300" y2="28" stroke="#252a33" stroke-width="1"/>
<line x1="150" y1="28" x2="450" y2="28" stroke="#252a33" stroke-width="1"/>
<line x1="150" y1="28" x2="150" y2="28" stroke="#252a33" stroke-width="1"/>
<line x1="450" y1="28" x2="450" y2="28" stroke="#252a33" stroke-width="1"/>
<polygon points="146,24 154,24 150,29" fill="#5a6070"/>
<polygon points="446,24 454,24 450,29" fill="#5a6070"/>
<text x="140" y="18" text-anchor="middle" fill="#5a6070" font-family="DM Mono,monospace" font-size="10">Path A — first run</text>
<text x="462" y="18" text-anchor="middle" fill="#5a6070" font-family="DM Mono,monospace" font-size="10">Path B — iterative</text>
</svg>
</div>
<!-- path A / B -->
<div class="split-row two" style="max-width:640px; width:100%">
<!-- PATH A -->
<div class="col">
<div class="node n-purple" style="width:100%" onclick="show('gen')">
<div class="label">1_generate_crop_dataset.py</div>
<div class="sub">Crop only, no classifier</div>
</div>
<div class="arrow h20"></div>
<div class="node n-amber" style="width:100%" onclick="show('manual')">
<div class="label">Manual labelling</div>
<div class="sub">Move crops to class folders</div>
</div>
</div>
<!-- PATH B -->
<div class="col">
<div class="node n-purple" style="width:100%" onclick="show('auto')">
<div class="label">auto_label.py</div>
<div class="sub">Detect + auto-classify</div>
</div>
<div class="arrow h20"></div>
<div class="node n-coral" style="width:100%" onclick="show('review')">
<div class="label">Browser review UI</div>
<div class="sub">Confirm / fix / reject</div>
</div>
</div>
</div>
<!-- merge -->
<div class="merge-line">
<svg viewBox="0 0 600 28" preserveAspectRatio="none">
<line x1="150" y1="0" x2="150" y2="14" stroke="#252a33" stroke-width="1"/>
<line x1="450" y1="0" x2="450" y2="14" stroke="#252a33" stroke-width="1"/>
<line x1="150" y1="14" x2="450" y2="14" stroke="#252a33" stroke-width="1"/>
<line x1="300" y1="14" x2="300" y2="28" stroke="#252a33" stroke-width="1"/>
<polygon points="296,24 304,24 300,29" fill="#5a6070"/>
</svg>
</div>
<!-- data/ folder -->
<div class="node n-gray" style="width:240px" onclick="show('data')">
<div class="label">data/</div>
<div class="sub">Labelled crops by class folder</div>
</div>
<div class="arrow h28"></div>
<!-- splitter -->
<div class="node n-purple" style="width:280px" onclick="show('split')">
<div class="label">1b_split_dataset.py</div>
<div class="sub">Stratified train / val / test</div>
</div>
<div class="arrow h20"></div>
<!-- crops_dataset -->
<div class="node n-gray" style="width:240px" onclick="show('crops')">
<div class="label">crops_dataset/</div>
<div class="sub">train / val / test</div>
</div>
<!-- fork to local / kaggle -->
<div class="split-line">
<svg viewBox="0 0 600 28" preserveAspectRatio="none">
<line x1="300" y1="0" x2="300" y2="28" stroke="#252a33" stroke-width="1"/>
<line x1="150" y1="28" x2="450" y2="28" stroke="#252a33" stroke-width="1"/>
<polygon points="146,24 154,24 150,29" fill="#5a6070"/>
<polygon points="446,24 454,24 450,29" fill="#5a6070"/>
<text x="140" y="18" text-anchor="middle" fill="#5a6070" font-family="DM Mono,monospace" font-size="10">Local GPU / CPU</text>
<text x="462" y="18" text-anchor="middle" fill="#5a6070" font-family="DM Mono,monospace" font-size="10">Free cloud GPU</text>
</svg>
</div>
<div class="split-row two" style="max-width:640px; width:100%">
<div class="col">
<div class="node n-purple" style="width:100%" onclick="show('train')">
<div class="label">2_train_classifier.py</div>
<div class="sub">EfficientNet-B0 local</div>
</div>
</div>
<div class="col">
<div class="node n-purple" style="width:100%" onclick="show('kaggle')">
<div class="label">Kaggle notebook</div>
<div class="sub">T4 GPU — free tier</div>
</div>
</div>
</div>
<!-- merge to weights -->
<div class="merge-line">
<svg viewBox="0 0 600 28" preserveAspectRatio="none">
<line x1="150" y1="0" x2="150" y2="14" stroke="#252a33" stroke-width="1"/>
<line x1="450" y1="0" x2="450" y2="14" stroke="#252a33" stroke-width="1"/>
<line x1="150" y1="14" x2="450" y2="14" stroke="#252a33" stroke-width="1"/>
<line x1="300" y1="14" x2="300" y2="28" stroke="#252a33" stroke-width="1"/>
<polygon points="296,24 304,24 300,29" fill="#5a6070"/>
</svg>
</div>
<!-- classifier weights -->
<div class="node n-teal" style="width:280px" onclick="show('weights')">
<div class="label">Classifier weights</div>
<div class="sub">best.pt + class_names.json</div>
</div>
<!-- fork to inference / api -->
<div class="split-line">
<svg viewBox="0 0 600 28" preserveAspectRatio="none">
<line x1="300" y1="0" x2="300" y2="28" stroke="#252a33" stroke-width="1"/>
<line x1="150" y1="28" x2="450" y2="28" stroke="#252a33" stroke-width="1"/>
<polygon points="146,24 154,24 150,29" fill="#5a6070"/>
<polygon points="446,24 454,24 450,29" fill="#5a6070"/>
</svg>
</div>
<div class="split-row two" style="max-width:640px; width:100%">
<div class="col">
<div class="node n-coral" style="width:100%" onclick="show('inf')">
<div class="label">3_inference.py</div>
<div class="sub">Images · video · webcam</div>
</div>
</div>
<div class="col">
<div class="node n-coral" style="width:100%" onclick="show('api')">
<div class="label">api_server.py</div>
<div class="sub">REST + polling API</div>
</div>
</div>
</div>
<!-- loop -->
<div class="loop-banner">
&nbsp;<strong>Iterative loop</strong> — shoot new shelf photos → run <strong>auto_label.py</strong> → review → retrain → better auto-labelling next time
</div>
</div><!-- /pipe -->
<!-- overlay -->
<div class="overlay" id="overlay" onclick="close_tip()"></div>
<!-- tooltip -->
<div class="tip" id="tip">
<button class="tip-close" onclick="close_tip()"></button>
<h3 id="tip-title"></h3>
<div id="tip-body"></div>
</div>
<script>
const INFO = {
src: {
title: "Shelf images",
body: `Any image file, folder of images, video, or webcam stream.<br>Supported: jpg, jpeg, png, bmp, webp, tiff, mp4, avi, mov.<br>Pass a single file or an entire folder — the scripts handle both.`,
cmd: null
},
det: {
title: "YOLOv8n detector",
body: `Your fine-tuned YOLOv8n model runs on each frame and outputs bounding boxes for every product detected. Each box is cropped out (with configurable padding) and passed to the next step.`,
cmd: `# Used internally by all pipeline scripts\n# Fine-tuned weights live at: detector/best.pt`
},
gen: {
title: "1_generate_crop_dataset.py",
body: `<b>Path A — first time only.</b><br>Runs YOLOv8n on your images and saves every crop into <code>data/unknown/</code>. No classifier is used here. You then manually move crops into named class folders before splitting.`,
cmd: `python 1_generate_crop_dataset.py \\\n --source shelf_images/ \\\n --weights detector/best.pt \\\n --output_dir data \\\n --conf 0.25 \\\n --padding 10`
},
manual: {
title: "Manual labelling",
body: `After script 1, all crops land in <code>data/unknown/</code>. You move them into named subfolders:<br><br><code>data/cola_can/&nbsp;&nbsp;&nbsp;crop001.jpg<br>data/pepsi_can/&nbsp;&nbsp;crop002.jpg</code><br><br>Use any file manager or Label Studio. Only needed the first time.`,
cmd: null
},
auto: {
title: "auto_label.py",
body: `<b>Path B — iterative (recommended).</b><br>Runs YOLOv8n + the classifier together, proposes a label for every crop, and opens a browser review UI. Much faster than manual labelling after the first model is trained.`,
cmd: `python auto_label.py \\\n --source shelf_images/ \\\n --detector_weights detector/best.pt \\\n --classifier_weights runs/classify/best.pt \\\n --output_dir data \\\n --cls_conf 0.50 \\\n --port 5000`
},
review: {
title: "Browser review UI",
body: `Opens at <code>http://localhost:5000</code>. For each crop you see:<br>• The top-3 class predictions with confidence<br>• One-click confirm, reject, or reclassify<br>• Ctrl+click for multi-select bulk actions<br>• Confidence slider to hide uncertain crops<br><br><b>Keyboard:</b> C = confirm · R = reject · ← → = navigate`,
cmd: null
},
data: {
title: "data/ folder",
body: `The labelled dataset root. Each subfolder is one class.<br><br><code>data/<br>&nbsp;&nbsp;cola_can/<br>&nbsp;&nbsp;pepsi_can/<br>&nbsp;&nbsp;lays_chips/<br>&nbsp;&nbsp;_rejected_/&nbsp;&nbsp;← from review UI<br>&nbsp;&nbsp;_unreviewed_/</code><br><br>Every <code>auto_label.py</code> run on new images <b>adds</b> to this folder — it never overwrites existing crops.`,
cmd: null
},
split: {
title: "1b_split_dataset.py",
body: `Reads <code>data/</code> and produces a stratified train / val / test split — every class gets the same ratio across all splits, so rare classes are never accidentally lost from val or test.<br><br>Automatically skips <code>_rejected_/</code>, <code>_unreviewed_/</code>, <code>unknown/</code>, and any folder starting with <code>_</code>.`,
cmd: `python 1b_split_dataset.py \\\n --data_dir data \\\n --output_dir crops_dataset \\\n --val_split 0.15 \\\n --test_split 0.05`
},
crops: {
title: "crops_dataset/",
body: `The split dataset, ready for training.<br><br><code>crops_dataset/<br>&nbsp;&nbsp;train/ cola_can/ pepsi_can/ …<br>&nbsp;&nbsp;val/ cola_can/ pepsi_can/ …<br>&nbsp;&nbsp;test/ cola_can/ pepsi_can/ …</code><br><br>Aim for ≥ 50 train images and ≥ 10 val images per class before training.`,
cmd: null
},
train: {
title: "2_train_classifier.py",
body: `Trains EfficientNet-B0 (ImageNet pre-trained) on <code>crops_dataset/</code>.<br>• <b>WeightedRandomSampler</b> handles class imbalance automatically<br>• <b>AMP</b> (--amp) for faster GPU training<br>• <b>Early stopping</b> with configurable patience<br>• Saves best.pt, last.pt, class_names.json, training curves`,
cmd: `python 2_train_classifier.py \\\n --data_dir crops_dataset \\\n --model efficientnet_b0 \\\n --epochs 50 \\\n --batch_size 64 \\\n --amp`
},
kaggle: {
title: "Kaggle notebook",
body: `<code>train_classifier_kaggle.ipynb</code> — same training logic as the script but runs on Kaggle's free T4 GPU.<br><br>Extra cells: dataset diagnostics, live training curves, per-class accuracy bar chart, confusion matrix.<br><br>Download <code>best.pt</code> + <code>class_names.json</code> and place them in <code>runs/classify/</code>.`,
cmd: `# On Kaggle:\n# 1. Upload crops_dataset/ as a Kaggle Dataset\n# 2. Attach it to the notebook\n# 3. Set DATA_DIR in Cell 2\n# 4. Runtime → GPU T4 → Run All`
},
weights: {
title: "Classifier weights",
body: `Two files are all you need for inference and the API:<br><br><code>runs/classify/best.pt</code> — model weights<br><code>runs/classify/class_names.json</code> — index→class mapping<br><br>The dynamic batch sizer probes available VRAM at startup and picks the optimal batch size automatically.`,
cmd: null
},
inf: {
title: "3_inference.py",
body: `Full detect → classify pipeline on any source. Draws colour-coded bounding boxes with class name + confidence. Batch size is auto-calibrated to VRAM — handles 260+ products per frame safely.`,
cmd: `python 3_inference.py \\\n --detector_weights detector/best.pt \\\n --classifier_weights runs/classify/best.pt \\\n --source shelf_images/ \\\n --det_conf 0.30 \\\n --cls_conf 0.65 \\\n --show`
},
api: {
title: "api_server.py",
body: `FastAPI server with a polling pattern:<br>1. <code>POST /jobs</code> — submit base64 image + class filter → get <code>job_id</code><br>2. <code>GET /jobs/{job_id}</code> — poll every 3s until <code>status: "done"</code><br>3. Response includes annotated image (base64), counts per class, and per-detection bboxes.`,
cmd: `uvicorn api_server:app \\\n --host 0.0.0.0 --port 8000 --workers 1\n\n# Env vars:\nDETECTOR_WEIGHTS=detector/best.pt\nCLASSIFIER_WEIGHTS=runs/classify/best.pt`
}
};
function show(key) {
const d = INFO[key];
document.getElementById('tip-title').textContent = d.title;
document.getElementById('tip-body').innerHTML =
d.body + (d.cmd ? `<div class="cmd">${d.cmd}</div>` : '');
document.getElementById('tip').classList.add('open');
document.getElementById('overlay').classList.add('open');
}
function close_tip() {
document.getElementById('tip').classList.remove('open');
document.getElementById('overlay').classList.remove('open');
}
document.addEventListener('keydown', e => { if (e.key === 'Escape') close_tip(); });
</script>
</body>
</html>