Initial commit
Browse filesThis view is limited to 50 files because it contains too many changes.
See raw diff
- .DS_Store +0 -0
- Dockerfile +17 -0
- README.md +4 -4
- app.log +0 -0
- app.py +824 -0
- dataset/.DS_Store +0 -0
- dataset/images/.DS_Store +0 -0
- dataset/images/train/02_JPG.rf.d6063f8ca200e543da7becc1bf260ed5.jpg +0 -0
- dataset/images/train/03_JPG.rf.2ca107348e11cdefab68044dba66388d.jpg +0 -0
- dataset/images/train/04_JPG.rf.b0b546ecbc6b70149b8932018e69fef0.jpg +0 -0
- dataset/images/train/05_jpg.rf.46241369ebb0749c40882400f82eb224.jpg +0 -0
- dataset/images/train/08_JPG.rf.1f81e954a3bbfc49dcd30e3ba0eb5e98.jpg +0 -0
- dataset/images/train/09_JPG.rf.9119efd8c174f968457a893669209835.jpg +0 -0
- dataset/images/train/10_JPG.rf.6745a7b3ea828239398b85182acba199.jpg +0 -0
- dataset/images/train/11_JPG.rf.3aa3109a1838549cf273cdbe8b2cafeb.jpg +0 -0
- dataset/images/train/12_jpg.rf.357643b374df92f81f9dee7c701b2315.jpg +0 -0
- dataset/images/train/14_jpg.rf.d91472c724e7c34da4d96ac5e504044c.jpg +0 -0
- dataset/images/train/15_jpg.rf.284413e4432b16253b4cd19f0c4f01e2.jpg +0 -0
- dataset/images/train/15r_jpg.rf.2da1990173346311d3a3555e23a9164a.jpg +0 -0
- dataset/images/train/16_jpg.rf.9fdb4f56ec8596ddcc31db5bbffc26a0.jpg +0 -0
- dataset/images/train/18_jpg.rf.4d241aab78af17171d83f3a50f1cf1aa.jpg +0 -0
- dataset/images/train/20_jpg.rf.4a45f799ba16b5ff81ab1929f12a12b1.jpg +0 -0
- dataset/images/train/21_jpg.rf.d1d6dd254d2e5f396589ccc68a3c8536.jpg +0 -0
- dataset/images/train/22_jpg.rf.a72964a78ea36c7bebe3a09cf27ef6ba.jpg +0 -0
- dataset/images/train/25_jpg.rf.893f4286e0c8a3cef2efb7612f248147.jpg +0 -0
- dataset/images/train/26_jpg.rf.a03c550707ff22cd50ff7f54bebda7ab.jpg +0 -0
- dataset/images/train/29_jpg.rf.931769b58ae20d18d1f09d042bc44176.jpg +0 -0
- dataset/images/train/31_jpg.rf.f31137f793efde0462ed560d426dcd24.jpg +0 -0
- dataset/images/train/7-Figure14-1_jpg.rf.1c6cb204ed1f37c8fed44178a02e9058.jpg +0 -0
- dataset/images/train/LU-F_mod_jpg.rf.fc594179772346639512f891960969bb.jpg +0 -0
- dataset/images/train/Solder_Voids_jpg.rf.d40f1b71d8a801f084067fde7f33fb08.jpg +0 -0
- dataset/images/train/gc10_lake_voids_260-31_jpg.rf.479f3d9dda8dd22097d3d93c78f7e11d.jpg +0 -0
- dataset/images/train/images_jpg.rf.675b31c5e1ba2b77f0fa5ca92e2391b0.jpg +0 -0
- dataset/images/train/qfn-voiding_0_jpg.rf.2945527db158e9ff4943febaf9cd3eab.jpg +0 -0
- dataset/images/train/techtips_3_jpg.rf.ad88af637816f0999f4df0b18dfef293.jpg +0 -0
- dataset/images/val/025_JPG.rf.b2cdc2d984adff593dc985f555b8d280.jpg +0 -0
- dataset/images/val/06_jpg.rf.a94e0a678df372f5ea1395a8d888a388.jpg +0 -0
- dataset/images/val/07_JPG.rf.324d17a87726bd2a9614536c687c6e68.jpg +0 -0
- dataset/images/val/23_jpg.rf.8e9afa6b3b471e10c26637d47700f28b.jpg +0 -0
- dataset/images/val/24_jpg.rf.4caa996d97e35f6ce4f27a527ea43465.jpg +0 -0
- dataset/images/val/27_jpg.rf.3475fce31d283058f46d9f349c04cb1a.jpg +0 -0
- dataset/images/val/28_jpg.rf.50e348d807d35667583137c9a6c162ca.jpg +0 -0
- dataset/images/val/30_jpg.rf.ed72622e97cf0d884997585686cfe40a.jpg +0 -0
- dataset/test/.DS_Store +0 -0
- dataset/test/images/17_jpg.rf.ec31940ea72d0cf8b9f38dba68789fcf.jpg +0 -0
- dataset/test/images/19_jpg.rf.2c5ffd63bd0ce6b9b0c80fef69d101dc.jpg +0 -0
- dataset/test/images/32_jpg.rf.f3e33dcf611a8754c0765224f7873d8b.jpg +0 -0
- dataset/test/images/normal-reflow_jpg.rf.2c4fbc1fda915b821b85689ae257e116.jpg +0 -0
- dataset/test/images/techtips_31_jpg.rf.673cd3c7c8511e534766e6dbc3171b39.jpg +0 -0
- dataset/test/labels/.DS_Store +0 -0
.DS_Store
ADDED
|
Binary file (6.15 kB). View file
|
|
|
Dockerfile
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Utiliser une image de base Python légère
|
| 2 |
+
FROM python:3.9-slim
|
| 3 |
+
|
| 4 |
+
# Définir le répertoire de travail
|
| 5 |
+
WORKDIR /app
|
| 6 |
+
|
| 7 |
+
# Copier les fichiers nécessaires dans le conteneur
|
| 8 |
+
COPY . /app
|
| 9 |
+
|
| 10 |
+
# Installer les dépendances
|
| 11 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 12 |
+
|
| 13 |
+
# Exposer le port 7860 pour le serveur Flask
|
| 14 |
+
EXPOSE 7860
|
| 15 |
+
|
| 16 |
+
# Commande pour démarrer Flask
|
| 17 |
+
CMD ["python", "app.py"]
|
README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
| 1 |
---
|
| 2 |
-
title: Project
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: docker
|
| 7 |
pinned: false
|
| 8 |
---
|
|
|
|
| 1 |
---
|
| 2 |
+
title: Segmentation Project
|
| 3 |
+
emoji: 😻
|
| 4 |
+
colorFrom: red
|
| 5 |
+
colorTo: purple
|
| 6 |
sdk: docker
|
| 7 |
pinned: false
|
| 8 |
---
|
app.log
ADDED
|
File without changes
|
app.py
ADDED
|
@@ -0,0 +1,824 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from flask import Flask, render_template, request, jsonify
|
| 2 |
+
from flask_socketio import SocketIO
|
| 3 |
+
import sys
|
| 4 |
+
import os
|
| 5 |
+
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
|
| 6 |
+
import shutil
|
| 7 |
+
import numpy as np
|
| 8 |
+
from PIL import Image
|
| 9 |
+
from sam2.build_sam import build_sam2
|
| 10 |
+
from sam2.sam2_image_predictor import SAM2ImagePredictor
|
| 11 |
+
|
| 12 |
+
class Predictor:
|
| 13 |
+
def __init__(self, model_cfg, checkpoint, device):
|
| 14 |
+
self.device = device
|
| 15 |
+
self.model = build_sam2(model_cfg, checkpoint, device=device)
|
| 16 |
+
self.predictor = SAM2ImagePredictor(self.model)
|
| 17 |
+
self.image_set = False
|
| 18 |
+
|
| 19 |
+
def set_image(self, image):
|
| 20 |
+
"""Set the image for SAM prediction."""
|
| 21 |
+
self.image = image
|
| 22 |
+
self.predictor.set_image(image)
|
| 23 |
+
self.image_set = True
|
| 24 |
+
|
| 25 |
+
def predict(self, point_coords, point_labels, multimask_output=False):
|
| 26 |
+
"""Run SAM prediction."""
|
| 27 |
+
if not self.image_set:
|
| 28 |
+
raise RuntimeError("An image must be set with .set_image(...) before mask prediction.")
|
| 29 |
+
return self.predictor.predict(
|
| 30 |
+
point_coords=point_coords,
|
| 31 |
+
point_labels=point_labels,
|
| 32 |
+
multimask_output=multimask_output
|
| 33 |
+
)
|
| 34 |
+
from utils.helpers import (
|
| 35 |
+
blend_mask_with_image,
|
| 36 |
+
save_mask_as_png,
|
| 37 |
+
convert_mask_to_yolo,
|
| 38 |
+
)
|
| 39 |
+
import torch
|
| 40 |
+
from ultralytics import YOLO
|
| 41 |
+
import threading
|
| 42 |
+
from threading import Lock
|
| 43 |
+
import subprocess
|
| 44 |
+
import time
|
| 45 |
+
import logging
|
| 46 |
+
import multiprocessing
|
| 47 |
+
import json
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
# Initialize Flask app and SocketIO
|
| 51 |
+
app = Flask(__name__)
|
| 52 |
+
socketio = SocketIO(app)
|
| 53 |
+
|
| 54 |
+
# Define Base Directory
|
| 55 |
+
BASE_DIR = os.path.abspath(os.path.dirname(__file__))
|
| 56 |
+
|
| 57 |
+
# Folder structure with absolute paths
|
| 58 |
+
UPLOAD_FOLDERS = {
|
| 59 |
+
'input': os.path.join(BASE_DIR, 'static/uploads/input'),
|
| 60 |
+
'segmented_voids': os.path.join(BASE_DIR, 'static/uploads/segmented/voids'),
|
| 61 |
+
'segmented_chips': os.path.join(BASE_DIR, 'static/uploads/segmented/chips'),
|
| 62 |
+
'mask_voids': os.path.join(BASE_DIR, 'static/uploads/mask/voids'),
|
| 63 |
+
'mask_chips': os.path.join(BASE_DIR, 'static/uploads/mask/chips'),
|
| 64 |
+
'automatic_segmented': os.path.join(BASE_DIR, 'static/uploads/segmented/automatic'),
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
HISTORY_FOLDERS = {
|
| 68 |
+
'images': os.path.join(BASE_DIR, 'static/history/images'),
|
| 69 |
+
'masks_chip': os.path.join(BASE_DIR, 'static/history/masks/chip'),
|
| 70 |
+
'masks_void': os.path.join(BASE_DIR, 'static/history/masks/void'),
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
DATASET_FOLDERS = {
|
| 74 |
+
'train_images': os.path.join(BASE_DIR, 'dataset/train/images'),
|
| 75 |
+
'train_labels': os.path.join(BASE_DIR, 'dataset/train/labels'),
|
| 76 |
+
'val_images': os.path.join(BASE_DIR, 'dataset/val/images'),
|
| 77 |
+
'val_labels': os.path.join(BASE_DIR, 'dataset/val/labels'),
|
| 78 |
+
'temp_backup': os.path.join(BASE_DIR, 'temp_backup'),
|
| 79 |
+
'models': os.path.join(BASE_DIR, 'models'),
|
| 80 |
+
'models_old': os.path.join(BASE_DIR, 'models/old'),
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
# Ensure all folders exist
|
| 84 |
+
for folder_name, folder_path in {**UPLOAD_FOLDERS, **HISTORY_FOLDERS, **DATASET_FOLDERS}.items():
|
| 85 |
+
os.makedirs(folder_path, exist_ok=True)
|
| 86 |
+
logging.info(f"Ensured folder exists: {folder_name} -> {folder_path}")
|
| 87 |
+
|
| 88 |
+
training_process = None
|
| 89 |
+
|
| 90 |
+
|
| 91 |
+
def initialize_training_status():
|
| 92 |
+
"""Initialize global training status."""
|
| 93 |
+
global training_status
|
| 94 |
+
training_status = {'running': False, 'cancelled': False}
|
| 95 |
+
|
| 96 |
+
def persist_training_status():
|
| 97 |
+
"""Save training status to a file."""
|
| 98 |
+
with open(os.path.join(BASE_DIR, 'training_status.json'), 'w') as status_file:
|
| 99 |
+
json.dump(training_status, status_file)
|
| 100 |
+
|
| 101 |
+
def load_training_status():
|
| 102 |
+
"""Load training status from a file."""
|
| 103 |
+
global training_status
|
| 104 |
+
status_path = os.path.join(BASE_DIR, 'training_status.json')
|
| 105 |
+
if os.path.exists(status_path):
|
| 106 |
+
with open(status_path, 'r') as status_file:
|
| 107 |
+
training_status = json.load(status_file)
|
| 108 |
+
else:
|
| 109 |
+
training_status = {'running': False, 'cancelled': False}
|
| 110 |
+
|
| 111 |
+
load_training_status()
|
| 112 |
+
|
| 113 |
+
os.environ["TORCH_CUDNN_SDPA_ENABLED"] = "0"
|
| 114 |
+
|
| 115 |
+
# Initialize SAM Predictor
|
| 116 |
+
MODEL_CFG = r"sam2/sam2_hiera_l.yaml"
|
| 117 |
+
CHECKPOINT = r"sam2/checkpoints/sam2.1_hiera_large.pt"
|
| 118 |
+
DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
| 119 |
+
predictor = Predictor(MODEL_CFG, CHECKPOINT, DEVICE)
|
| 120 |
+
|
| 121 |
+
# Initialize YOLO-seg
|
| 122 |
+
YOLO_CFG = os.path.join(DATASET_FOLDERS['models'], "best.pt")
|
| 123 |
+
yolo_model = YOLO(YOLO_CFG)
|
| 124 |
+
|
| 125 |
+
# Configure logging
|
| 126 |
+
logging.basicConfig(
|
| 127 |
+
level=logging.INFO,
|
| 128 |
+
format='%(asctime)s [%(levelname)s] %(message)s',
|
| 129 |
+
handlers=[
|
| 130 |
+
logging.StreamHandler(),
|
| 131 |
+
logging.FileHandler(os.path.join(BASE_DIR, "app.log")) # Log to a file
|
| 132 |
+
]
|
| 133 |
+
)
|
| 134 |
+
|
| 135 |
+
|
| 136 |
+
@app.route('/')
|
| 137 |
+
def index():
|
| 138 |
+
"""Serve the main UI."""
|
| 139 |
+
return render_template('index.html')
|
| 140 |
+
|
| 141 |
+
@app.route('/upload', methods=['POST'])
|
| 142 |
+
def upload_image():
|
| 143 |
+
"""Handle image uploads."""
|
| 144 |
+
if 'file' not in request.files:
|
| 145 |
+
return jsonify({'error': 'No file uploaded'}), 400
|
| 146 |
+
file = request.files['file']
|
| 147 |
+
if file.filename == '':
|
| 148 |
+
return jsonify({'error': 'No file selected'}), 400
|
| 149 |
+
|
| 150 |
+
# Save the uploaded file to the input folder
|
| 151 |
+
input_path = os.path.join(UPLOAD_FOLDERS['input'], file.filename)
|
| 152 |
+
file.save(input_path)
|
| 153 |
+
|
| 154 |
+
# Set the uploaded image in the predictor
|
| 155 |
+
image = np.array(Image.open(input_path).convert("RGB"))
|
| 156 |
+
predictor.set_image(image)
|
| 157 |
+
|
| 158 |
+
# Return a web-accessible URL instead of the file system path
|
| 159 |
+
web_accessible_url = f"/static/uploads/input/{file.filename}"
|
| 160 |
+
print(f"Image uploaded and set for prediction: {input_path}")
|
| 161 |
+
return jsonify({'image_url': web_accessible_url})
|
| 162 |
+
|
| 163 |
+
@app.route('/segment', methods=['POST'])
|
| 164 |
+
def segment():
|
| 165 |
+
"""
|
| 166 |
+
Perform segmentation and return the blended image URL.
|
| 167 |
+
"""
|
| 168 |
+
try:
|
| 169 |
+
# Extract data from request
|
| 170 |
+
data = request.json
|
| 171 |
+
points = np.array(data.get('points', []))
|
| 172 |
+
labels = np.array(data.get('labels', []))
|
| 173 |
+
current_class = data.get('class', 'voids') # Default to 'voids' if class not provided
|
| 174 |
+
|
| 175 |
+
# Ensure predictor has an image set
|
| 176 |
+
if not predictor.image_set:
|
| 177 |
+
raise ValueError("No image set for prediction.")
|
| 178 |
+
|
| 179 |
+
# Perform SAM prediction
|
| 180 |
+
masks, _, _ = predictor.predict(
|
| 181 |
+
point_coords=points,
|
| 182 |
+
point_labels=labels,
|
| 183 |
+
multimask_output=False
|
| 184 |
+
)
|
| 185 |
+
|
| 186 |
+
# Check if masks exist and have non-zero elements
|
| 187 |
+
if masks is None or masks.size == 0:
|
| 188 |
+
raise RuntimeError("No masks were generated by the predictor.")
|
| 189 |
+
|
| 190 |
+
# Define output paths based on class
|
| 191 |
+
mask_folder = UPLOAD_FOLDERS.get(f'mask_{current_class}')
|
| 192 |
+
segmented_folder = UPLOAD_FOLDERS.get(f'segmented_{current_class}')
|
| 193 |
+
|
| 194 |
+
if not mask_folder or not segmented_folder:
|
| 195 |
+
raise ValueError(f"Invalid class '{current_class}' provided.")
|
| 196 |
+
|
| 197 |
+
os.makedirs(mask_folder, exist_ok=True)
|
| 198 |
+
os.makedirs(segmented_folder, exist_ok=True)
|
| 199 |
+
|
| 200 |
+
# Save the raw mask
|
| 201 |
+
mask_path = os.path.join(mask_folder, 'raw_mask.png')
|
| 202 |
+
save_mask_as_png(masks[0], mask_path)
|
| 203 |
+
|
| 204 |
+
# Generate blended image
|
| 205 |
+
blend_color = [34, 139, 34] if current_class == 'voids' else [30, 144, 255] # Green for voids, blue for chips
|
| 206 |
+
blended_image = blend_mask_with_image(predictor.image, masks[0], blend_color)
|
| 207 |
+
|
| 208 |
+
# Save blended image
|
| 209 |
+
blended_filename = f"blended_{current_class}.png"
|
| 210 |
+
blended_path = os.path.join(segmented_folder, blended_filename)
|
| 211 |
+
Image.fromarray(blended_image).save(blended_path)
|
| 212 |
+
|
| 213 |
+
# Return URL for frontend access
|
| 214 |
+
segmented_url = f"/static/uploads/segmented/{current_class}/{blended_filename}"
|
| 215 |
+
logging.info(f"Segmentation completed for {current_class}. Points: {points}, Labels: {labels}")
|
| 216 |
+
return jsonify({'segmented_url': segmented_url})
|
| 217 |
+
|
| 218 |
+
except ValueError as ve:
|
| 219 |
+
logging.error(f"Value error during segmentation: {ve}")
|
| 220 |
+
return jsonify({'error': str(ve)}), 400
|
| 221 |
+
|
| 222 |
+
except Exception as e:
|
| 223 |
+
logging.error(f"Unexpected error during segmentation: {e}")
|
| 224 |
+
return jsonify({'error': 'Segmentation failed', 'details': str(e)}), 500
|
| 225 |
+
|
| 226 |
+
@app.route('/automatic_segment', methods=['POST'])
|
| 227 |
+
def automatic_segment():
|
| 228 |
+
"""Perform automatic segmentation using YOLO."""
|
| 229 |
+
if 'file' not in request.files:
|
| 230 |
+
return jsonify({'error': 'No file uploaded'}), 400
|
| 231 |
+
file = request.files['file']
|
| 232 |
+
if file.filename == '':
|
| 233 |
+
return jsonify({'error': 'No file selected'}), 400
|
| 234 |
+
|
| 235 |
+
input_path = os.path.join(UPLOAD_FOLDERS['input'], file.filename)
|
| 236 |
+
file.save(input_path)
|
| 237 |
+
|
| 238 |
+
try:
|
| 239 |
+
# Perform YOLO segmentation
|
| 240 |
+
results = yolo_model.predict(input_path, save=False, save_txt=False)
|
| 241 |
+
output_folder = UPLOAD_FOLDERS['automatic_segmented']
|
| 242 |
+
os.makedirs(output_folder, exist_ok=True)
|
| 243 |
+
|
| 244 |
+
chips_data = []
|
| 245 |
+
chips = []
|
| 246 |
+
voids = []
|
| 247 |
+
|
| 248 |
+
# Process results and save segmented images
|
| 249 |
+
for result in results:
|
| 250 |
+
annotated_image = result.plot()
|
| 251 |
+
result_filename = f"{file.filename.rsplit('.', 1)[0]}_pred.jpg"
|
| 252 |
+
result_path = os.path.join(output_folder, result_filename)
|
| 253 |
+
Image.fromarray(annotated_image).save(result_path)
|
| 254 |
+
|
| 255 |
+
# Separate chips and voids
|
| 256 |
+
for i, label in enumerate(result.boxes.cls): # YOLO labels
|
| 257 |
+
label_name = result.names[int(label)] # Get label name (e.g., 'chip' or 'void')
|
| 258 |
+
box = result.boxes.xyxy[i].cpu().numpy() # Bounding box (x1, y1, x2, y2)
|
| 259 |
+
area = float((box[2] - box[0]) * (box[3] - box[1])) # Calculate area
|
| 260 |
+
|
| 261 |
+
if label_name == 'chip':
|
| 262 |
+
chips.append({'box': box, 'area': area, 'voids': []})
|
| 263 |
+
elif label_name == 'void':
|
| 264 |
+
voids.append({'box': box, 'area': area})
|
| 265 |
+
|
| 266 |
+
# Assign voids to chips based on proximity
|
| 267 |
+
for void in voids:
|
| 268 |
+
void_centroid = [
|
| 269 |
+
(void['box'][0] + void['box'][2]) / 2, # x centroid
|
| 270 |
+
(void['box'][1] + void['box'][3]) / 2 # y centroid
|
| 271 |
+
]
|
| 272 |
+
for chip in chips:
|
| 273 |
+
# Check if void centroid is within chip bounding box
|
| 274 |
+
if (chip['box'][0] <= void_centroid[0] <= chip['box'][2] and
|
| 275 |
+
chip['box'][1] <= void_centroid[1] <= chip['box'][3]):
|
| 276 |
+
chip['voids'].append(void)
|
| 277 |
+
break
|
| 278 |
+
|
| 279 |
+
# Calculate metrics for each chip
|
| 280 |
+
for idx, chip in enumerate(chips):
|
| 281 |
+
chip_area = chip['area']
|
| 282 |
+
total_void_area = sum([float(void['area']) for void in chip['voids']])
|
| 283 |
+
max_void_area = max([float(void['area']) for void in chip['voids']], default=0)
|
| 284 |
+
|
| 285 |
+
void_percentage = (total_void_area / chip_area) * 100 if chip_area > 0 else 0
|
| 286 |
+
max_void_percentage = (max_void_area / chip_area) * 100 if chip_area > 0 else 0
|
| 287 |
+
|
| 288 |
+
chips_data.append({
|
| 289 |
+
"chip_number": int(idx + 1),
|
| 290 |
+
"chip_area": round(chip_area, 2),
|
| 291 |
+
"void_percentage": round(void_percentage, 2),
|
| 292 |
+
"max_void_percentage": round(max_void_percentage, 2)
|
| 293 |
+
})
|
| 294 |
+
|
| 295 |
+
# Return the segmented image URL and table data
|
| 296 |
+
segmented_url = f"/static/uploads/segmented/automatic/{result_filename}"
|
| 297 |
+
return jsonify({
|
| 298 |
+
"segmented_url": segmented_url, # Use the URL for frontend access
|
| 299 |
+
"table_data": {
|
| 300 |
+
"image_name": file.filename,
|
| 301 |
+
"chips": chips_data
|
| 302 |
+
}
|
| 303 |
+
})
|
| 304 |
+
|
| 305 |
+
except Exception as e:
|
| 306 |
+
print(f"Error in automatic segmentation: {e}")
|
| 307 |
+
return jsonify({'error': 'Segmentation failed.'}), 500
|
| 308 |
+
|
| 309 |
+
@app.route('/save_both', methods=['POST'])
|
| 310 |
+
def save_both():
|
| 311 |
+
"""Save both the image and masks into the history folders."""
|
| 312 |
+
data = request.json
|
| 313 |
+
image_name = data.get('image_name')
|
| 314 |
+
|
| 315 |
+
if not image_name:
|
| 316 |
+
return jsonify({'error': 'Image name not provided'}), 400
|
| 317 |
+
|
| 318 |
+
try:
|
| 319 |
+
# Ensure image_name is a pure file name
|
| 320 |
+
image_name = os.path.basename(image_name) # Strip any directory path
|
| 321 |
+
print(f"Sanitized Image Name: {image_name}")
|
| 322 |
+
|
| 323 |
+
# Correctly resolve the input image path
|
| 324 |
+
input_image_path = os.path.join(UPLOAD_FOLDERS['input'], image_name)
|
| 325 |
+
if not os.path.exists(input_image_path):
|
| 326 |
+
print(f"Input image does not exist: {input_image_path}")
|
| 327 |
+
return jsonify({'error': f'Input image not found: {input_image_path}'}), 404
|
| 328 |
+
|
| 329 |
+
# Copy the image to history/images
|
| 330 |
+
image_history_path = os.path.join(HISTORY_FOLDERS['images'], image_name)
|
| 331 |
+
os.makedirs(os.path.dirname(image_history_path), exist_ok=True)
|
| 332 |
+
shutil.copy(input_image_path, image_history_path)
|
| 333 |
+
print(f"Image saved to history: {image_history_path}")
|
| 334 |
+
|
| 335 |
+
# Backup void mask
|
| 336 |
+
void_mask_path = os.path.join(UPLOAD_FOLDERS['mask_voids'], 'raw_mask.png')
|
| 337 |
+
if os.path.exists(void_mask_path):
|
| 338 |
+
void_mask_history_path = os.path.join(HISTORY_FOLDERS['masks_void'], f"{os.path.splitext(image_name)[0]}.png")
|
| 339 |
+
os.makedirs(os.path.dirname(void_mask_history_path), exist_ok=True)
|
| 340 |
+
shutil.copy(void_mask_path, void_mask_history_path)
|
| 341 |
+
print(f"Voids mask saved to history: {void_mask_history_path}")
|
| 342 |
+
else:
|
| 343 |
+
print(f"Voids mask not found: {void_mask_path}")
|
| 344 |
+
|
| 345 |
+
# Backup chip mask
|
| 346 |
+
chip_mask_path = os.path.join(UPLOAD_FOLDERS['mask_chips'], 'raw_mask.png')
|
| 347 |
+
if os.path.exists(chip_mask_path):
|
| 348 |
+
chip_mask_history_path = os.path.join(HISTORY_FOLDERS['masks_chip'], f"{os.path.splitext(image_name)[0]}.png")
|
| 349 |
+
os.makedirs(os.path.dirname(chip_mask_history_path), exist_ok=True)
|
| 350 |
+
shutil.copy(chip_mask_path, chip_mask_history_path)
|
| 351 |
+
print(f"Chips mask saved to history: {chip_mask_history_path}")
|
| 352 |
+
else:
|
| 353 |
+
print(f"Chips mask not found: {chip_mask_path}")
|
| 354 |
+
|
| 355 |
+
return jsonify({'message': 'Image and masks saved successfully!'}), 200
|
| 356 |
+
|
| 357 |
+
except Exception as e:
|
| 358 |
+
print(f"Error saving files: {e}")
|
| 359 |
+
return jsonify({'error': 'Failed to save files.', 'details': str(e)}), 500
|
| 360 |
+
|
| 361 |
+
@app.route('/get_history', methods=['GET'])
|
| 362 |
+
def get_history():
|
| 363 |
+
try:
|
| 364 |
+
saved_images = os.listdir(HISTORY_FOLDERS['images'])
|
| 365 |
+
return jsonify({'status': 'success', 'images': saved_images}), 200
|
| 366 |
+
except Exception as e:
|
| 367 |
+
return jsonify({'status': 'error', 'message': f'Failed to fetch history: {e}'}), 500
|
| 368 |
+
|
| 369 |
+
|
| 370 |
+
@app.route('/delete_history_item', methods=['POST'])
|
| 371 |
+
def delete_history_item():
|
| 372 |
+
data = request.json
|
| 373 |
+
image_name = data.get('image_name')
|
| 374 |
+
|
| 375 |
+
if not image_name:
|
| 376 |
+
return jsonify({'error': 'Image name not provided'}), 400
|
| 377 |
+
|
| 378 |
+
try:
|
| 379 |
+
image_path = os.path.join(HISTORY_FOLDERS['images'], image_name)
|
| 380 |
+
if os.path.exists(image_path):
|
| 381 |
+
os.remove(image_path)
|
| 382 |
+
|
| 383 |
+
void_mask_path = os.path.join(HISTORY_FOLDERS['masks_void'], f"{os.path.splitext(image_name)[0]}.png")
|
| 384 |
+
if os.path.exists(void_mask_path):
|
| 385 |
+
os.remove(void_mask_path)
|
| 386 |
+
|
| 387 |
+
chip_mask_path = os.path.join(HISTORY_FOLDERS['masks_chip'], f"{os.path.splitext(image_name)[0]}.png")
|
| 388 |
+
if os.path.exists(chip_mask_path):
|
| 389 |
+
os.remove(chip_mask_path)
|
| 390 |
+
|
| 391 |
+
return jsonify({'message': f'{image_name} and associated masks deleted successfully.'}), 200
|
| 392 |
+
except Exception as e:
|
| 393 |
+
return jsonify({'error': f'Failed to delete files: {e}'}), 500
|
| 394 |
+
|
| 395 |
+
# Lock for training status updates
|
| 396 |
+
status_lock = Lock()
|
| 397 |
+
|
| 398 |
+
def update_training_status(key, value):
|
| 399 |
+
"""Thread-safe update for training status."""
|
| 400 |
+
with status_lock:
|
| 401 |
+
training_status[key] = value
|
| 402 |
+
|
| 403 |
+
@app.route('/retrain_model', methods=['POST'])
|
| 404 |
+
def retrain_model():
|
| 405 |
+
"""Handle retrain model workflow."""
|
| 406 |
+
global training_status
|
| 407 |
+
|
| 408 |
+
if training_status.get('running', False):
|
| 409 |
+
return jsonify({'error': 'Training is already in progress'}), 400
|
| 410 |
+
|
| 411 |
+
try:
|
| 412 |
+
# Update training status
|
| 413 |
+
update_training_status('running', True)
|
| 414 |
+
update_training_status('cancelled', False)
|
| 415 |
+
logging.info("Training status updated. Starting training workflow.")
|
| 416 |
+
|
| 417 |
+
# Backup masks and images
|
| 418 |
+
backup_masks_and_images()
|
| 419 |
+
logging.info("Backup completed successfully.")
|
| 420 |
+
|
| 421 |
+
# Prepare YOLO labels
|
| 422 |
+
prepare_yolo_labels()
|
| 423 |
+
logging.info("YOLO labels prepared successfully.")
|
| 424 |
+
|
| 425 |
+
# Start YOLO training in a separate thread
|
| 426 |
+
threading.Thread(target=run_yolo_training).start()
|
| 427 |
+
return jsonify({'message': 'Training started successfully!'}), 200
|
| 428 |
+
|
| 429 |
+
except Exception as e:
|
| 430 |
+
logging.error(f"Error during training preparation: {e}")
|
| 431 |
+
update_training_status('running', False)
|
| 432 |
+
return jsonify({'error': f"Failed to start training: {e}"}), 500
|
| 433 |
+
|
| 434 |
+
def prepare_yolo_labels():
|
| 435 |
+
"""Convert all masks into YOLO-compatible labels and copy images to the dataset folder."""
|
| 436 |
+
images_folder = HISTORY_FOLDERS['images'] # Use history images as the source
|
| 437 |
+
train_labels_folder = DATASET_FOLDERS['train_labels']
|
| 438 |
+
train_images_folder = DATASET_FOLDERS['train_images']
|
| 439 |
+
val_labels_folder = DATASET_FOLDERS['val_labels']
|
| 440 |
+
val_images_folder = DATASET_FOLDERS['val_images']
|
| 441 |
+
|
| 442 |
+
# Ensure destination directories exist
|
| 443 |
+
os.makedirs(train_labels_folder, exist_ok=True)
|
| 444 |
+
os.makedirs(train_images_folder, exist_ok=True)
|
| 445 |
+
os.makedirs(val_labels_folder, exist_ok=True)
|
| 446 |
+
os.makedirs(val_images_folder, exist_ok=True)
|
| 447 |
+
|
| 448 |
+
try:
|
| 449 |
+
all_images = [img for img in os.listdir(images_folder) if img.endswith(('.jpg', '.png'))]
|
| 450 |
+
random.shuffle(all_images) # Shuffle the images for randomness
|
| 451 |
+
|
| 452 |
+
# Determine split index
|
| 453 |
+
split_idx = int(len(all_images) * 0.8) # 80% for training, 20% for validation
|
| 454 |
+
|
| 455 |
+
# Split images into train and validation sets
|
| 456 |
+
train_images = all_images[:split_idx]
|
| 457 |
+
val_images = all_images[split_idx:]
|
| 458 |
+
|
| 459 |
+
# Process training images
|
| 460 |
+
for image_name in train_images:
|
| 461 |
+
process_image_and_mask(
|
| 462 |
+
image_name,
|
| 463 |
+
source_images_folder=images_folder,
|
| 464 |
+
dest_images_folder=train_images_folder,
|
| 465 |
+
dest_labels_folder=train_labels_folder
|
| 466 |
+
)
|
| 467 |
+
|
| 468 |
+
# Process validation images
|
| 469 |
+
for image_name in val_images:
|
| 470 |
+
process_image_and_mask(
|
| 471 |
+
image_name,
|
| 472 |
+
source_images_folder=images_folder,
|
| 473 |
+
dest_images_folder=val_images_folder,
|
| 474 |
+
dest_labels_folder=val_labels_folder
|
| 475 |
+
)
|
| 476 |
+
|
| 477 |
+
logging.info("YOLO labels prepared, and images split into train and validation successfully.")
|
| 478 |
+
|
| 479 |
+
except Exception as e:
|
| 480 |
+
logging.error(f"Error in preparing YOLO labels: {e}")
|
| 481 |
+
raise
|
| 482 |
+
|
| 483 |
+
import random
|
| 484 |
+
|
| 485 |
+
def prepare_yolo_labels():
|
| 486 |
+
"""Convert all masks into YOLO-compatible labels and copy images to the dataset folder."""
|
| 487 |
+
images_folder = HISTORY_FOLDERS['images'] # Use history images as the source
|
| 488 |
+
train_labels_folder = DATASET_FOLDERS['train_labels']
|
| 489 |
+
train_images_folder = DATASET_FOLDERS['train_images']
|
| 490 |
+
val_labels_folder = DATASET_FOLDERS['val_labels']
|
| 491 |
+
val_images_folder = DATASET_FOLDERS['val_images']
|
| 492 |
+
|
| 493 |
+
# Ensure destination directories exist
|
| 494 |
+
os.makedirs(train_labels_folder, exist_ok=True)
|
| 495 |
+
os.makedirs(train_images_folder, exist_ok=True)
|
| 496 |
+
os.makedirs(val_labels_folder, exist_ok=True)
|
| 497 |
+
os.makedirs(val_images_folder, exist_ok=True)
|
| 498 |
+
|
| 499 |
+
try:
|
| 500 |
+
all_images = [img for img in os.listdir(images_folder) if img.endswith(('.jpg', '.png'))]
|
| 501 |
+
random.shuffle(all_images) # Shuffle the images for randomness
|
| 502 |
+
|
| 503 |
+
# Determine split index
|
| 504 |
+
split_idx = int(len(all_images) * 0.8) # 80% for training, 20% for validation
|
| 505 |
+
|
| 506 |
+
# Split images into train and validation sets
|
| 507 |
+
train_images = all_images[:split_idx]
|
| 508 |
+
val_images = all_images[split_idx:]
|
| 509 |
+
|
| 510 |
+
# Process training images
|
| 511 |
+
for image_name in train_images:
|
| 512 |
+
process_image_and_mask(
|
| 513 |
+
image_name,
|
| 514 |
+
source_images_folder=images_folder,
|
| 515 |
+
dest_images_folder=train_images_folder,
|
| 516 |
+
dest_labels_folder=train_labels_folder
|
| 517 |
+
)
|
| 518 |
+
|
| 519 |
+
# Process validation images
|
| 520 |
+
for image_name in val_images:
|
| 521 |
+
process_image_and_mask(
|
| 522 |
+
image_name,
|
| 523 |
+
source_images_folder=images_folder,
|
| 524 |
+
dest_images_folder=val_images_folder,
|
| 525 |
+
dest_labels_folder=val_labels_folder
|
| 526 |
+
)
|
| 527 |
+
|
| 528 |
+
logging.info("YOLO labels prepared, and images split into train and validation successfully.")
|
| 529 |
+
|
| 530 |
+
except Exception as e:
|
| 531 |
+
logging.error(f"Error in preparing YOLO labels: {e}")
|
| 532 |
+
raise
|
| 533 |
+
|
| 534 |
+
|
| 535 |
+
def process_image_and_mask(image_name, source_images_folder, dest_images_folder, dest_labels_folder):
|
| 536 |
+
"""
|
| 537 |
+
Process a single image and its masks, saving them in the appropriate YOLO format.
|
| 538 |
+
"""
|
| 539 |
+
try:
|
| 540 |
+
image_path = os.path.join(source_images_folder, image_name)
|
| 541 |
+
label_file_path = os.path.join(dest_labels_folder, f"{os.path.splitext(image_name)[0]}.txt")
|
| 542 |
+
|
| 543 |
+
# Copy image to the destination images folder
|
| 544 |
+
shutil.copy(image_path, os.path.join(dest_images_folder, image_name))
|
| 545 |
+
|
| 546 |
+
# Clear the label file if it exists
|
| 547 |
+
if os.path.exists(label_file_path):
|
| 548 |
+
os.remove(label_file_path)
|
| 549 |
+
|
| 550 |
+
# Process void mask
|
| 551 |
+
void_mask_path = os.path.join(HISTORY_FOLDERS['masks_void'], f"{os.path.splitext(image_name)[0]}.png")
|
| 552 |
+
if os.path.exists(void_mask_path):
|
| 553 |
+
convert_mask_to_yolo(
|
| 554 |
+
mask_path=void_mask_path,
|
| 555 |
+
image_path=image_path,
|
| 556 |
+
class_id=0, # Void class
|
| 557 |
+
output_path=label_file_path
|
| 558 |
+
)
|
| 559 |
+
|
| 560 |
+
# Process chip mask
|
| 561 |
+
chip_mask_path = os.path.join(HISTORY_FOLDERS['masks_chip'], f"{os.path.splitext(image_name)[0]}.png")
|
| 562 |
+
if os.path.exists(chip_mask_path):
|
| 563 |
+
convert_mask_to_yolo(
|
| 564 |
+
mask_path=chip_mask_path,
|
| 565 |
+
image_path=image_path,
|
| 566 |
+
class_id=1, # Chip class
|
| 567 |
+
output_path=label_file_path,
|
| 568 |
+
append=True # Append chip annotations
|
| 569 |
+
)
|
| 570 |
+
|
| 571 |
+
logging.info(f"Processed {image_name} into YOLO format.")
|
| 572 |
+
except Exception as e:
|
| 573 |
+
logging.error(f"Error processing {image_name}: {e}")
|
| 574 |
+
raise
|
| 575 |
+
|
| 576 |
+
def backup_masks_and_images():
|
| 577 |
+
"""Backup current masks and images from history folders."""
|
| 578 |
+
temp_backup_paths = {
|
| 579 |
+
'voids': os.path.join(DATASET_FOLDERS['temp_backup'], 'masks/voids'),
|
| 580 |
+
'chips': os.path.join(DATASET_FOLDERS['temp_backup'], 'masks/chips'),
|
| 581 |
+
'images': os.path.join(DATASET_FOLDERS['temp_backup'], 'images')
|
| 582 |
+
}
|
| 583 |
+
|
| 584 |
+
# Prepare all backup directories
|
| 585 |
+
for path in temp_backup_paths.values():
|
| 586 |
+
if os.path.exists(path):
|
| 587 |
+
shutil.rmtree(path)
|
| 588 |
+
os.makedirs(path, exist_ok=True)
|
| 589 |
+
|
| 590 |
+
try:
|
| 591 |
+
# Backup images from history
|
| 592 |
+
for file in os.listdir(HISTORY_FOLDERS['images']):
|
| 593 |
+
src_image_path = os.path.join(HISTORY_FOLDERS['images'], file)
|
| 594 |
+
dst_image_path = os.path.join(temp_backup_paths['images'], file)
|
| 595 |
+
shutil.copy(src_image_path, dst_image_path)
|
| 596 |
+
|
| 597 |
+
# Backup void masks from history
|
| 598 |
+
for file in os.listdir(HISTORY_FOLDERS['masks_void']):
|
| 599 |
+
src_void_path = os.path.join(HISTORY_FOLDERS['masks_void'], file)
|
| 600 |
+
dst_void_path = os.path.join(temp_backup_paths['voids'], file)
|
| 601 |
+
shutil.copy(src_void_path, dst_void_path)
|
| 602 |
+
|
| 603 |
+
# Backup chip masks from history
|
| 604 |
+
for file in os.listdir(HISTORY_FOLDERS['masks_chip']):
|
| 605 |
+
src_chip_path = os.path.join(HISTORY_FOLDERS['masks_chip'], file)
|
| 606 |
+
dst_chip_path = os.path.join(temp_backup_paths['chips'], file)
|
| 607 |
+
shutil.copy(src_chip_path, dst_chip_path)
|
| 608 |
+
|
| 609 |
+
logging.info("Masks and images backed up successfully from history.")
|
| 610 |
+
except Exception as e:
|
| 611 |
+
logging.error(f"Error during backup: {e}")
|
| 612 |
+
raise RuntimeError("Backup process failed.")
|
| 613 |
+
|
| 614 |
+
def run_yolo_training(num_epochs=10):
|
| 615 |
+
"""Run YOLO training process."""
|
| 616 |
+
global training_process
|
| 617 |
+
|
| 618 |
+
try:
|
| 619 |
+
device = "cuda" if torch.cuda.is_available() else "cpu"
|
| 620 |
+
data_cfg_path = os.path.join(BASE_DIR, "models/data.yaml") # Ensure correct YAML path
|
| 621 |
+
|
| 622 |
+
logging.info(f"Starting YOLO training on {device} with {num_epochs} epochs.")
|
| 623 |
+
logging.info(f"Using dataset configuration: {data_cfg_path}")
|
| 624 |
+
|
| 625 |
+
training_command = [
|
| 626 |
+
"yolo",
|
| 627 |
+
"train",
|
| 628 |
+
f"data={data_cfg_path}",
|
| 629 |
+
f"model={os.path.join(DATASET_FOLDERS['models'], 'best.pt')}",
|
| 630 |
+
f"device={device}",
|
| 631 |
+
f"epochs={num_epochs}",
|
| 632 |
+
"project=runs",
|
| 633 |
+
"name=train"
|
| 634 |
+
]
|
| 635 |
+
|
| 636 |
+
training_process = subprocess.Popen(
|
| 637 |
+
training_command,
|
| 638 |
+
stdout=subprocess.PIPE,
|
| 639 |
+
stderr=subprocess.STDOUT,
|
| 640 |
+
text=True,
|
| 641 |
+
env=os.environ.copy(),
|
| 642 |
+
)
|
| 643 |
+
|
| 644 |
+
# Display and log output in real time
|
| 645 |
+
for line in iter(training_process.stdout.readline, ''):
|
| 646 |
+
print(line.strip())
|
| 647 |
+
logging.info(line.strip())
|
| 648 |
+
socketio.emit('training_update', {'message': line.strip()}) # Send updates to the frontend
|
| 649 |
+
|
| 650 |
+
training_process.wait()
|
| 651 |
+
|
| 652 |
+
if training_process.returncode == 0:
|
| 653 |
+
finalize_training() # Finalize successfully completed training
|
| 654 |
+
else:
|
| 655 |
+
raise RuntimeError("YOLO training process failed. Check logs for details.")
|
| 656 |
+
except Exception as e:
|
| 657 |
+
logging.error(f"Training error: {e}")
|
| 658 |
+
restore_backup() # Restore the dataset and masks
|
| 659 |
+
|
| 660 |
+
# Emit training error event to the frontend
|
| 661 |
+
socketio.emit('training_status', {'status': 'error', 'message': f"Training failed: {str(e)}"})
|
| 662 |
+
finally:
|
| 663 |
+
update_training_status('running', False)
|
| 664 |
+
training_process = None # Reset the process
|
| 665 |
+
|
| 666 |
+
|
| 667 |
+
@socketio.on('cancel_training')
|
| 668 |
+
def handle_cancel_training():
|
| 669 |
+
"""Cancel the YOLO training process."""
|
| 670 |
+
global training_process, training_status
|
| 671 |
+
|
| 672 |
+
if not training_status.get('running', False):
|
| 673 |
+
socketio.emit('button_update', {'action': 'retrain'}) # Update button to retrain
|
| 674 |
+
return
|
| 675 |
+
|
| 676 |
+
try:
|
| 677 |
+
training_process.terminate()
|
| 678 |
+
training_process.wait()
|
| 679 |
+
training_status['running'] = False
|
| 680 |
+
training_status['cancelled'] = True
|
| 681 |
+
|
| 682 |
+
restore_backup()
|
| 683 |
+
cleanup_train_val_directories()
|
| 684 |
+
|
| 685 |
+
# Emit button state change
|
| 686 |
+
socketio.emit('button_update', {'action': 'retrain'})
|
| 687 |
+
socketio.emit('training_status', {'status': 'cancelled', 'message': 'Training was canceled by the user.'})
|
| 688 |
+
except Exception as e:
|
| 689 |
+
logging.error(f"Error cancelling training: {e}")
|
| 690 |
+
socketio.emit('training_status', {'status': 'error', 'message': str(e)})
|
| 691 |
+
|
| 692 |
+
def finalize_training():
|
| 693 |
+
"""Finalize training by promoting the new model and cleaning up."""
|
| 694 |
+
try:
|
| 695 |
+
# Locate the most recent training directory
|
| 696 |
+
runs_dir = os.path.join(BASE_DIR, 'runs')
|
| 697 |
+
if not os.path.exists(runs_dir):
|
| 698 |
+
raise FileNotFoundError("Training runs directory does not exist.")
|
| 699 |
+
|
| 700 |
+
# Get the latest training run folder
|
| 701 |
+
latest_run = max(
|
| 702 |
+
[os.path.join(runs_dir, d) for d in os.listdir(runs_dir)],
|
| 703 |
+
key=os.path.getmtime
|
| 704 |
+
)
|
| 705 |
+
weights_dir = os.path.join(latest_run, 'weights')
|
| 706 |
+
best_model_path = os.path.join(weights_dir, 'best.pt')
|
| 707 |
+
|
| 708 |
+
if not os.path.exists(best_model_path):
|
| 709 |
+
raise FileNotFoundError(f"'best.pt' not found in {weights_dir}.")
|
| 710 |
+
|
| 711 |
+
# Backup the old model
|
| 712 |
+
old_model_folder = DATASET_FOLDERS['models_old']
|
| 713 |
+
os.makedirs(old_model_folder, exist_ok=True)
|
| 714 |
+
existing_best_model = os.path.join(DATASET_FOLDERS['models'], 'best.pt')
|
| 715 |
+
|
| 716 |
+
if os.path.exists(existing_best_model):
|
| 717 |
+
timestamp = time.strftime("%Y%m%d_%H%M%S")
|
| 718 |
+
shutil.move(existing_best_model, os.path.join(old_model_folder, f"old_{timestamp}.pt"))
|
| 719 |
+
logging.info(f"Old model backed up to {old_model_folder}.")
|
| 720 |
+
|
| 721 |
+
# Move the new model to the models directory
|
| 722 |
+
new_model_dest = os.path.join(DATASET_FOLDERS['models'], 'best.pt')
|
| 723 |
+
shutil.move(best_model_path, new_model_dest)
|
| 724 |
+
logging.info(f"New model saved to {new_model_dest}.")
|
| 725 |
+
|
| 726 |
+
# Notify frontend that training is completed
|
| 727 |
+
socketio.emit('training_status', {
|
| 728 |
+
'status': 'completed',
|
| 729 |
+
'message': 'Training completed successfully! Model saved as best.pt.'
|
| 730 |
+
})
|
| 731 |
+
|
| 732 |
+
# Clean up train/val directories
|
| 733 |
+
cleanup_train_val_directories()
|
| 734 |
+
logging.info("Train and validation directories cleaned up successfully.")
|
| 735 |
+
|
| 736 |
+
except Exception as e:
|
| 737 |
+
logging.error(f"Error finalizing training: {e}")
|
| 738 |
+
# Emit error status to the frontend
|
| 739 |
+
socketio.emit('training_status', {'status': 'error', 'message': f"Error finalizing training: {str(e)}"})
|
| 740 |
+
|
| 741 |
+
def restore_backup():
|
| 742 |
+
"""Restore the dataset and masks from the backup."""
|
| 743 |
+
try:
|
| 744 |
+
temp_backup = DATASET_FOLDERS['temp_backup']
|
| 745 |
+
shutil.copytree(os.path.join(temp_backup, 'masks/voids'), UPLOAD_FOLDERS['mask_voids'], dirs_exist_ok=True)
|
| 746 |
+
shutil.copytree(os.path.join(temp_backup, 'masks/chips'), UPLOAD_FOLDERS['mask_chips'], dirs_exist_ok=True)
|
| 747 |
+
shutil.copytree(os.path.join(temp_backup, 'images'), UPLOAD_FOLDERS['input'], dirs_exist_ok=True)
|
| 748 |
+
logging.info("Backup restored successfully.")
|
| 749 |
+
except Exception as e:
|
| 750 |
+
logging.error(f"Error restoring backup: {e}")
|
| 751 |
+
|
| 752 |
+
@app.route('/cancel_training', methods=['POST'])
|
| 753 |
+
def cancel_training():
|
| 754 |
+
global training_process
|
| 755 |
+
|
| 756 |
+
if training_process is None:
|
| 757 |
+
logging.error("No active training process to terminate.")
|
| 758 |
+
return jsonify({'error': 'No active training process to cancel.'}), 400
|
| 759 |
+
|
| 760 |
+
try:
|
| 761 |
+
training_process.terminate()
|
| 762 |
+
training_process.wait()
|
| 763 |
+
training_process = None # Reset the process after termination
|
| 764 |
+
|
| 765 |
+
# Update training status
|
| 766 |
+
update_training_status('running', False)
|
| 767 |
+
update_training_status('cancelled', True)
|
| 768 |
+
|
| 769 |
+
# Check if the model is already saved as best.pt
|
| 770 |
+
best_model_path = os.path.join(DATASET_FOLDERS['models'], 'best.pt')
|
| 771 |
+
if os.path.exists(best_model_path):
|
| 772 |
+
logging.info(f"Model already saved as best.pt at {best_model_path}.")
|
| 773 |
+
socketio.emit('button_update', {'action': 'revert'}) # Notify frontend to revert button state
|
| 774 |
+
else:
|
| 775 |
+
logging.info("Training canceled, but no new model was saved.")
|
| 776 |
+
|
| 777 |
+
# Restore backup if needed
|
| 778 |
+
restore_backup()
|
| 779 |
+
cleanup_train_val_directories()
|
| 780 |
+
|
| 781 |
+
# Emit status update to frontend
|
| 782 |
+
socketio.emit('training_status', {'status': 'cancelled', 'message': 'Training was canceled by the user.'})
|
| 783 |
+
return jsonify({'message': 'Training canceled and data restored successfully.'}), 200
|
| 784 |
+
|
| 785 |
+
except Exception as e:
|
| 786 |
+
logging.error(f"Error cancelling training: {e}")
|
| 787 |
+
return jsonify({'error': f"Failed to cancel training: {e}"}), 500
|
| 788 |
+
|
| 789 |
+
@app.route('/clear_history', methods=['POST'])
|
| 790 |
+
def clear_history():
|
| 791 |
+
try:
|
| 792 |
+
for folder in [HISTORY_FOLDERS['images'], HISTORY_FOLDERS['masks_chip'], HISTORY_FOLDERS['masks_void']]:
|
| 793 |
+
shutil.rmtree(folder, ignore_errors=True)
|
| 794 |
+
os.makedirs(folder, exist_ok=True) # Recreate the empty folder
|
| 795 |
+
return jsonify({'message': 'History cleared successfully!'}), 200
|
| 796 |
+
except Exception as e:
|
| 797 |
+
return jsonify({'error': f'Failed to clear history: {e}'}), 500
|
| 798 |
+
|
| 799 |
+
@app.route('/training_status', methods=['GET'])
|
| 800 |
+
def get_training_status():
|
| 801 |
+
"""Return the current training status."""
|
| 802 |
+
if training_status.get('running', False):
|
| 803 |
+
return jsonify({'status': 'running', 'message': 'Training in progress.'}), 200
|
| 804 |
+
elif training_status.get('cancelled', False):
|
| 805 |
+
return jsonify({'status': 'cancelled', 'message': 'Training was cancelled.'}), 200
|
| 806 |
+
return jsonify({'status': 'idle', 'message': 'No training is currently running.'}), 200
|
| 807 |
+
|
| 808 |
+
def cleanup_train_val_directories():
|
| 809 |
+
"""Clear the train and validation directories."""
|
| 810 |
+
try:
|
| 811 |
+
for folder in [DATASET_FOLDERS['train_images'], DATASET_FOLDERS['train_labels'],
|
| 812 |
+
DATASET_FOLDERS['val_images'], DATASET_FOLDERS['val_labels']]:
|
| 813 |
+
shutil.rmtree(folder, ignore_errors=True) # Remove folder contents
|
| 814 |
+
os.makedirs(folder, exist_ok=True) # Recreate empty folders
|
| 815 |
+
logging.info("Train and validation directories cleaned up successfully.")
|
| 816 |
+
except Exception as e:
|
| 817 |
+
logging.error(f"Error cleaning up train/val directories: {e}")
|
| 818 |
+
|
| 819 |
+
|
| 820 |
+
if __name__ == '__main__':
|
| 821 |
+
multiprocessing.set_start_method('spawn') # Required for multiprocessing on Windows
|
| 822 |
+
app.run(debug=True, use_reloader=False)
|
| 823 |
+
|
| 824 |
+
|
dataset/.DS_Store
ADDED
|
Binary file (8.2 kB). View file
|
|
|
dataset/images/.DS_Store
ADDED
|
Binary file (6.15 kB). View file
|
|
|
dataset/images/train/02_JPG.rf.d6063f8ca200e543da7becc1bf260ed5.jpg
ADDED
|
dataset/images/train/03_JPG.rf.2ca107348e11cdefab68044dba66388d.jpg
ADDED
|
dataset/images/train/04_JPG.rf.b0b546ecbc6b70149b8932018e69fef0.jpg
ADDED
|
dataset/images/train/05_jpg.rf.46241369ebb0749c40882400f82eb224.jpg
ADDED
|
dataset/images/train/08_JPG.rf.1f81e954a3bbfc49dcd30e3ba0eb5e98.jpg
ADDED
|
dataset/images/train/09_JPG.rf.9119efd8c174f968457a893669209835.jpg
ADDED
|
dataset/images/train/10_JPG.rf.6745a7b3ea828239398b85182acba199.jpg
ADDED
|
dataset/images/train/11_JPG.rf.3aa3109a1838549cf273cdbe8b2cafeb.jpg
ADDED
|
dataset/images/train/12_jpg.rf.357643b374df92f81f9dee7c701b2315.jpg
ADDED
|
dataset/images/train/14_jpg.rf.d91472c724e7c34da4d96ac5e504044c.jpg
ADDED
|
dataset/images/train/15_jpg.rf.284413e4432b16253b4cd19f0c4f01e2.jpg
ADDED
|
dataset/images/train/15r_jpg.rf.2da1990173346311d3a3555e23a9164a.jpg
ADDED
|
dataset/images/train/16_jpg.rf.9fdb4f56ec8596ddcc31db5bbffc26a0.jpg
ADDED
|
dataset/images/train/18_jpg.rf.4d241aab78af17171d83f3a50f1cf1aa.jpg
ADDED
|
dataset/images/train/20_jpg.rf.4a45f799ba16b5ff81ab1929f12a12b1.jpg
ADDED
|
dataset/images/train/21_jpg.rf.d1d6dd254d2e5f396589ccc68a3c8536.jpg
ADDED
|
dataset/images/train/22_jpg.rf.a72964a78ea36c7bebe3a09cf27ef6ba.jpg
ADDED
|
dataset/images/train/25_jpg.rf.893f4286e0c8a3cef2efb7612f248147.jpg
ADDED
|
dataset/images/train/26_jpg.rf.a03c550707ff22cd50ff7f54bebda7ab.jpg
ADDED
|
dataset/images/train/29_jpg.rf.931769b58ae20d18d1f09d042bc44176.jpg
ADDED
|
dataset/images/train/31_jpg.rf.f31137f793efde0462ed560d426dcd24.jpg
ADDED
|
dataset/images/train/7-Figure14-1_jpg.rf.1c6cb204ed1f37c8fed44178a02e9058.jpg
ADDED
|
dataset/images/train/LU-F_mod_jpg.rf.fc594179772346639512f891960969bb.jpg
ADDED
|
dataset/images/train/Solder_Voids_jpg.rf.d40f1b71d8a801f084067fde7f33fb08.jpg
ADDED
|
dataset/images/train/gc10_lake_voids_260-31_jpg.rf.479f3d9dda8dd22097d3d93c78f7e11d.jpg
ADDED
|
dataset/images/train/images_jpg.rf.675b31c5e1ba2b77f0fa5ca92e2391b0.jpg
ADDED
|
dataset/images/train/qfn-voiding_0_jpg.rf.2945527db158e9ff4943febaf9cd3eab.jpg
ADDED
|
dataset/images/train/techtips_3_jpg.rf.ad88af637816f0999f4df0b18dfef293.jpg
ADDED
|
dataset/images/val/025_JPG.rf.b2cdc2d984adff593dc985f555b8d280.jpg
ADDED
|
dataset/images/val/06_jpg.rf.a94e0a678df372f5ea1395a8d888a388.jpg
ADDED
|
dataset/images/val/07_JPG.rf.324d17a87726bd2a9614536c687c6e68.jpg
ADDED
|
dataset/images/val/23_jpg.rf.8e9afa6b3b471e10c26637d47700f28b.jpg
ADDED
|
dataset/images/val/24_jpg.rf.4caa996d97e35f6ce4f27a527ea43465.jpg
ADDED
|
dataset/images/val/27_jpg.rf.3475fce31d283058f46d9f349c04cb1a.jpg
ADDED
|
dataset/images/val/28_jpg.rf.50e348d807d35667583137c9a6c162ca.jpg
ADDED
|
dataset/images/val/30_jpg.rf.ed72622e97cf0d884997585686cfe40a.jpg
ADDED
|
dataset/test/.DS_Store
ADDED
|
Binary file (6.15 kB). View file
|
|
|
dataset/test/images/17_jpg.rf.ec31940ea72d0cf8b9f38dba68789fcf.jpg
ADDED
|
dataset/test/images/19_jpg.rf.2c5ffd63bd0ce6b9b0c80fef69d101dc.jpg
ADDED
|
dataset/test/images/32_jpg.rf.f3e33dcf611a8754c0765224f7873d8b.jpg
ADDED
|
dataset/test/images/normal-reflow_jpg.rf.2c4fbc1fda915b821b85689ae257e116.jpg
ADDED
|
dataset/test/images/techtips_31_jpg.rf.673cd3c7c8511e534766e6dbc3171b39.jpg
ADDED
|
dataset/test/labels/.DS_Store
ADDED
|
Binary file (6.15 kB). View file
|
|
|