A two-stage computer vision system for detecting, classifying, and tracking assembly defects on PCB boards, built on two YOLO models (board segmentation + defect detection) with multi-frame tracking support (ByteTrack). The project includes a shared core pipeline, a REST API built with FastAPI, and an interactive dashboard built with Streamlit, all runnable from a single Docker image.
- Architecture & Pipeline Logic
- Project Structure
- Prerequisites
- Preparing Model Weights
- Running Locally (without Docker)
- Running with Docker
- API Documentation
- Environment Variables
- Streamlit Dashboard
- Common Troubleshooting
- Roadmap
The pipeline (src/pipeline.py) works in two stages:
Input frame (image/video/webcam)
│
▼
┌────────────────────────────┐
│ Model 1: PCB-SEG (Segment) │ → locates and masks the PCB board in the frame
└────────────────────────────┘
│ (crop the board region)
▼
┌────────────────────────────┐
│ Model 2: Defect Detection │ → detects and classifies defects + ByteTrack
└────────────────────────────┘
│
▼
Structured result (DetectionResult)
→ annotated frame + downloadable JSON
The system doesn't run a single model over the whole frame. First, Model 1 (PCB-SEG) finds and segments the PCB board itself within the frame. Then, instead of running the second model on the entire image, we crop just the board region that Model 1 found and run Model 2 (Defect Detection) only on that cropped region to look for defects inside it. This two-stage design was chosen specifically for higher speed and lower computational cost — processing a small cropped region is far cheaper than running defect detection over the full frame every time. If no PCB board is found in the frame, the second model falls back to running on the whole frame so no defect is missed. The core class PCBPipeline supports three usage modes:
| Method | Purpose |
|---|---|
process_image(path) |
Process a single image, returns a DetectionResult |
process(progress_callback=...) |
Process a full video/webcam stream, saving the output video + JSON |
_draw_results(frame, result) |
Draws boxes/mask on a frame for display |
PCB-Defect-Detection/
├── app/
│ ├── api/
│ │ ├── API_app.py # FastAPI service (this project)
│ │ └── models/ # ⚠️ Model weights go here (git-ignored)
│ └── streamlit_app/
│ ├── app.py # Interactive dashboard (FA/EN)
│ └── models/ # Model weights for the dashboard
├── src/
│ └── pipeline.py # Core pipeline (shared by the API and dashboard)
├── requirements.txt
├── Dockerfile
├── docker-compose.yml # Run the API + dashboard together
├── .dockerignore
├── .gitignore
├── .env.example
└── README.md
- Python 3.11+ — for local (non-Docker) execution. Check your version:
python --version # or: python3 --version - pip (comes with Python) and, ideally, the venv module (also built-in)
- Git (optional, only needed if you clone the repo instead of downloading a zip)
- Docker 24+ and, optionally, Docker Compose v2 — only needed for the containerized setup
- Weights for both models:
best-pcb.ptandbest_detect2.pt— see Preparing Model Weights - (Optional) A CUDA-capable GPU + drivers for faster inference — the default is
cpu, which works everywhere
Model weights are not included in the repo or the Docker image (which is why they're excluded via .gitignore/.dockerignore — they're typically large and often proprietary). Before running the project, copy the weight files into all four of these paths (the API and the dashboard each load their own copy):
app/api/models/best-pcb.pt
app/api/models/best_detect2.pt
app/streamlit_app/models/best-pcb.pt
app/streamlit_app/models/best_detect2.pt
You can also change the paths/filenames via the MODEL1_PATH / MODEL2_PATH environment variables (for the API) or from the dashboard's sidebar settings panel (for Streamlit) if your weights live somewhere else or use different names.
⚠️ Run every command below from the project's root folder (the folder that containsrequirements.txt,app/, andsrc/) — both the API and the dashboard import fromsrc.pipeline, and running them from a different folder is the most common source ofModuleNotFoundError: No module named 'src'.
Either clone it with Git:
git clone <your-repo-url>
cd PCB-Defect-Detectionor unzip the downloaded archive and open a terminal inside the extracted PCB-Defect-Detection folder.
A virtual environment keeps this project's Python packages separate from the rest of your system.
Windows — PowerShell
python -m venv .venv
.venv\Scripts\Activate.ps1(If PowerShell blocks the script with an execution-policy error, run once as your user: Set-ExecutionPolicy -Scope CurrentUser RemoteSigned, then try activating again.)
Windows — Command Prompt (cmd.exe)
python -m venv .venv
.venv\Scripts\activate.batLinux / macOS — bash/zsh
python3 -m venv .venv
source .venv/bin/activateYour shell prompt should now start with (.venv). Every pip/python command below should be run with this environment active.
python -m pip install --upgrade pip
pip install -r requirements.txtThis installs FastAPI, Uvicorn, Streamlit, OpenCV, Ultralytics (YOLO), and everything else needed by both services. It can take a few minutes the first time (PyTorch/Ultralytics are large downloads).
Verify the install worked:
python -c "import fastapi, streamlit, cv2, ultralytics; print('All good ✅')"Copy your .pt files into the four paths described in Preparing Model Weights above. Without them, both services will still start, but inference endpoints will report an error until the correct paths are set.
From the project root:
uvicorn app.api.API_app:app --host 0.0.0.0 --port 8000 --reloadThen open http://localhost:8000/docs and confirm GET /health returns "status": "ok".
Open a new terminal, activate the same virtual environment again (Step 2), make sure you're still in the project root, then:
streamlit run app/streamlit_app/app.pyStreamlit will print a local URL (usually http://localhost:8501) and normally opens it in your browser automatically.
| Step | Command |
|---|---|
| Create venv (Windows PowerShell) | python -m venv .venv && .venv\Scripts\Activate.ps1 |
| Create venv (Linux/macOS) | python3 -m venv .venv && source .venv/bin/activate |
| Install deps | pip install -r requirements.txt |
| Run API | uvicorn app.api.API_app:app --host 0.0.0.0 --port 8000 --reload |
| Run dashboard | streamlit run app/streamlit_app/app.py |
| Deactivate venv | deactivate |
Once running:
- API:
http://localhost:8000 - Interactive Swagger docs:
http://localhost:8000/docs - Streamlit dashboard:
http://localhost:8501
Once running:
- API:
http://localhost:8000 - Interactive Swagger docs:
http://localhost:8000/docs - Streamlit dashboard:
http://localhost:8501
# Build the image
docker build -t pcb-defect-detection:latest .
# Run the FastAPI service (model weights are mounted from the host, not baked into the image)
docker run -d \
--name pcb-api \
-p 8000:8000 \
-v "$(pwd)/app/api/models:/app/app/api/models:ro" \
-v "$(pwd)/outputs:/app/outputs" \
pcb-defect-detection:latest
# Run the Streamlit dashboard instead of FastAPI (same image, different CMD)
docker run -d \
--name pcb-dashboard \
-p 8501:8501 \
-v "$(pwd)/app/streamlit_app/models:/app/app/streamlit_app/models:ro" \
pcb-defect-detection:latest \
streamlit run app/streamlit_app/app.py --server.address=0.0.0.0 --server.port=8501Check service health:
curl http://localhost:8000/healthdocker compose up --buildThis spins up two containers:
| Service | Address | Description |
|---|---|---|
api |
http://localhost:8000/docs | FastAPI service |
dashboard |
http://localhost:8501 | Streamlit dashboard |
Stop with:
docker compose downInteractive, always up-to-date documentation (Swagger UI) is available at
/docs— also/redoc.
| Method | Path | Description |
|---|---|---|
GET |
/ |
Basic service info |
GET |
/health |
Service health status and whether models loaded successfully |
| Method | Path | Input | Output |
|---|---|---|---|
POST |
/predict/image |
Image file (multipart/form-data) |
JSON result (defect count, bboxes, confidence, ...) |
POST |
/predict/image/annotated |
Image file + return_format=png|jpg |
The annotated image (binary) + a summary header |
Video processing can take a while, so it's designed as an asynchronous job: the request immediately returns a job_id, and the actual processing runs in the background (a thread pool).
| Method | Path | Description |
|---|---|---|
POST |
/predict/video |
Upload a video + optional params (conf_threshold, iou_threshold, imgsz, enable_tracking) → job_id |
GET |
/predict/video/{job_id}/status |
Processing status: queued → processing → done/failed + progress percentage |
GET |
/predict/video/{job_id}/download/video |
Download the annotated video (only once done) |
GET |
/predict/video/{job_id}/download/json |
Download the full JSON result (stats + per-frame defects) |
DELETE |
/predict/video/{job_id} |
Delete a job and its output files |
# Process an image → JSON
curl -X POST http://localhost:8000/predict/image \
-F "file=@sample_pcb.jpg"
# Process an image → get the annotated image back
curl -X POST "http://localhost:8000/predict/image/annotated?return_format=png" \
-F "file=@sample_pcb.jpg" \
-o annotated_result.png
# Start video processing
curl -X POST "http://localhost:8000/predict/video?conf_threshold=0.3" \
-F "file=@line_stream.mp4"
# → {"job_id": "a1b2c3...", "status": "queued", ...}
# Poll status
curl http://localhost:8000/predict/video/a1b2c3.../status
# Download the final video (once status=done)
curl -o result.mp4 http://localhost:8000/predict/video/a1b2c3.../download/videoimport requests, time
BASE = "http://localhost:8000"
# --- Image ---
with open("sample_pcb.jpg", "rb") as f:
r = requests.post(f"{BASE}/predict/image", files={"file": f})
print(r.json())
# --- Video ---
with open("line_stream.mp4", "rb") as f:
r = requests.post(f"{BASE}/predict/video", files={"file": f})
job_id = r.json()["job_id"]
while True:
status = requests.get(f"{BASE}/predict/video/{job_id}/status").json()
print(status["status"], status.get("progress"))
if status["status"] in ("done", "failed"):
break
time.sleep(2)
if status["status"] == "done":
video = requests.get(f"{BASE}/predict/video/{job_id}/download/video")
open("result.mp4", "wb").write(video.content)All of these are optional and have sensible defaults (see the full list in .env.example):
| Variable | Default | Description |
|---|---|---|
MODEL1_PATH |
app/api/models/best-pcb.pt |
Path to the board segmentation model weights |
MODEL2_PATH |
app/api/models/best_detect2.pt |
Path to the defect detection model weights |
CONF_THRESHOLD |
0.25 |
Default confidence threshold |
IOU_THRESHOLD |
0.45 |
Default IoU/NMS threshold |
IMG_SIZE |
416 |
Inference input resolution |
DEVICE |
cpu |
cpu or cuda:0 |
OUTPUT_DIR |
/tmp/pcb_api_outputs |
Where temporary job output (video/JSON) is stored |
MAX_UPLOAD_MB |
200 |
Maximum upload file size |
MAX_WORKERS |
2 |
Number of threads processing videos concurrently |
CORS_ORIGINS |
* |
Allowed CORS origins (comma-separated) |
The dashboard (app/streamlit_app/app.py) offers Persian/English support, single-image analysis, and full video processing with metrics and downloadable outputs — it runs independently of FastAPI (calling src.pipeline directly) and can run alongside the API or on its own. It uses Streamlit's native theme (.streamlit/config.toml) rather than custom CSS, so the UI stays consistent and doesn't clash with Streamlit's built-in widgets.
| Issue | Fix |
|---|---|
ModuleNotFoundError: No module named 'src' |
The dashboard already handles this automatically (it adds the project root to sys.path at startup), but if you still see it, make sure you're running streamlit run app/streamlit_app/app.py from the project root, with the venv active |
/health returns "status": "degraded" |
Check the MODEL1_PATH/MODEL2_PATH paths; in Docker, make sure the models folder is mounted |
ImportError: ultralytics not installed |
Run pip install -r requirements.txt from the project root, inside the activated virtual environment |
'python' is not recognized... (Windows) |
Python isn't on your PATH; reinstall Python from python.org and check "Add Python to PATH", or use the py launcher instead: py -m venv .venv |
| PowerShell blocks venv activation | Run once: Set-ExecutionPolicy -Scope CurrentUser RemoteSigned, then activate again |
| Output video won't play in the browser | ffmpeg must be installed in the system/container (already installed in the Dockerfile); locally, pip install imageio-ffmpeg provides one automatically |
| Inference is slow | Lower IMG_SIZE, or set DEVICE=cuda:0 on a GPU-equipped machine |
| 413 error on upload | Increase MAX_UPLOAD_MB |
| Port already in use | Pick a different port, e.g. uvicorn app.api.API_app:app --port 8001 or streamlit run app/streamlit_app/app.py --server.port 8502 |
This project was developed by a team of AI engineers with different areas of expertise:
| Member | Role | Contact |
|---|---|---|
| Mhajirezaei | Demo & Project Development | GitHub |
| Hossein Heydari | Backend Development & API (FastAPI) | GitHub |
| Amir mohammad Hatamzadeh | Computer Vision Development | GitHub |
Contributions are welcome! If you would like to improve this project, add new features, fix bugs, or optimize the pipeline, feel free to contribute.
- Fork this repository.
- Create a new branch:
git checkout -b feature/your-feature-name- Make your changes.
- Commit your changes:
git commit -m "Add new feature"- Push your branch:
git push origin feature/your-feature-name- Open a Pull Request and describe your changes.
If you have any questions, suggestions, or issues related to this project, feel free to contact the team.
For bug reports, please open an issue in the repository with:
- A clear description of the problem
- Steps to reproduce the issue
- Error logs or screenshots (if available)
For direct communication:
- API & Backend: Hossein Heydari
- Computer Vision: Amir mohammad Hatamzadeh
We appreciate your feedback and contributions ❤️
