Navy commited on
Commit
ae00b72
·
1 Parent(s): d5327ae

change video stream to base64

Browse files
Files changed (3) hide show
  1. app.py +111 -34
  2. core/defect_detection_image.py +228 -0
  3. utils.py +4 -2
app.py CHANGED
@@ -5,7 +5,8 @@ from ultralytics import YOLO
5
 
6
  from typing import Dict
7
 
8
- from core.defect_detection import *
 
9
  from utils import *
10
 
11
  import uvicorn, asyncio
@@ -54,7 +55,6 @@ app.add_middleware(
54
  allow_headers=["*"],
55
  )
56
 
57
-
58
  # ============================================================
59
  # ROUTES
60
  # ============================================================
@@ -70,26 +70,24 @@ async def start_detection(data: Dict):
70
  webhook_url = data.get("webhook_url")
71
  cameras = data.get("cameras", [])
72
 
 
 
 
73
  if not station_id or not parts or not webhook_url or not cameras:
74
  return JSONResponse(
75
- status_code=400,
76
- content={
77
- "status": "error",
78
- "station_id": station_id,
79
- "camera_count": len(cameras),
80
- "message": "Missing required fields"
81
- }
82
- )
83
 
84
  # -------------------------------
85
  # VALIDATION BEFORE EXECUTION
86
  # -------------------------------
87
- required_parts_fields = [
88
- "id",
89
- "pin_api",
90
- "name",
91
- "sku"
92
- ]
93
  validation_errors = validate_input(required_parts_fields, station_id, cameras, parts, webhook_url)
94
 
95
  if validation_errors:
@@ -97,40 +95,119 @@ async def start_detection(data: Dict):
97
  for err in validation_errors:
98
  logger.error(f" - {err}")
99
  return JSONResponse(
100
- status_code=400,
101
- content={
102
- "status": "error",
103
- "station_id": station_id,
104
- "camera_count": len(cameras),
105
- "message": " | ".join(validation_errors)
106
- }
107
- )
 
108
  logger.info(f"[INFO] Get metadata parts")
109
  model_path = model_by_id_metadata(parts['id'])
110
 
111
  logger.info(f"[INFO] Checking model_path")
112
  if isinstance(model_path, str):
113
  if not os.path.exists(model_path):
114
- logger.info(f"[INFO] Model file not found")
115
- return {"status": "error", "message": f"Model file not found: {model_path}"}
 
 
 
116
  model = YOLO(model_path)
117
  else:
118
  model = model_path
119
 
120
- logger.info(f"[START] Station {station_id} → {len(cameras)} kamera diproses")
 
 
 
121
 
122
- # running background
123
- asyncio.create_task(run_detection_group(station_id, cameras, webhook_url, model, parts))
 
 
124
 
125
  return JSONResponse(
126
- status_code=200,
127
- content={
128
  "status": "started",
129
  "station_id": station_id,
130
  "camera_count": len(cameras),
131
- "message": "Detection is running in background."
132
- }
133
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
134
 
135
 
136
  # ============================================================
 
5
 
6
  from typing import Dict
7
 
8
+ # from core.defect_detection import *
9
+ from core.defect_detection_image import *
10
  from utils import *
11
 
12
  import uvicorn, asyncio
 
55
  allow_headers=["*"],
56
  )
57
 
 
58
  # ============================================================
59
  # ROUTES
60
  # ============================================================
 
70
  webhook_url = data.get("webhook_url")
71
  cameras = data.get("cameras", [])
72
 
73
+ # -------------------------------
74
+ # BASIC VALIDATION
75
+ # -------------------------------
76
  if not station_id or not parts or not webhook_url or not cameras:
77
  return JSONResponse(
78
+ status_code=400,
79
+ content={
80
+ "status": "error",
81
+ "station_id": station_id,
82
+ "camera_count": len(cameras),
83
+ "message": "Missing required fields"
84
+ }
85
+ )
86
 
87
  # -------------------------------
88
  # VALIDATION BEFORE EXECUTION
89
  # -------------------------------
90
+ required_parts_fields = ["id", "pin_api", "name", "sku"]
 
 
 
 
 
91
  validation_errors = validate_input(required_parts_fields, station_id, cameras, parts, webhook_url)
92
 
93
  if validation_errors:
 
95
  for err in validation_errors:
96
  logger.error(f" - {err}")
97
  return JSONResponse(
98
+ status_code=400,
99
+ content={
100
+ "status": "error",
101
+ "station_id": station_id,
102
+ "camera_count": len(cameras),
103
+ "message": " | ".join(validation_errors)
104
+ }
105
+ )
106
+
107
  logger.info(f"[INFO] Get metadata parts")
108
  model_path = model_by_id_metadata(parts['id'])
109
 
110
  logger.info(f"[INFO] Checking model_path")
111
  if isinstance(model_path, str):
112
  if not os.path.exists(model_path):
113
+ logger.error(f"[ERROR] Model file not found: {model_path}")
114
+ return {
115
+ "status": "error",
116
+ "message": f"Model file not found: {model_path}"
117
+ }
118
  model = YOLO(model_path)
119
  else:
120
  model = model_path
121
 
122
+ # =====================================================
123
+ # BASE64 IMAGE DETECTION (NOT VIDEO STREAM)
124
+ # =====================================================
125
+ logger.info(f"[START] Station {station_id} → {len(cameras)} camera(s) with base64 images")
126
 
127
+ # Jalankan detection di background
128
+ asyncio.create_task(
129
+ run_detection_group(station_id, cameras, webhook_url, model, parts)
130
+ )
131
 
132
  return JSONResponse(
133
+ status_code=200,
134
+ content={
135
  "status": "started",
136
  "station_id": station_id,
137
  "camera_count": len(cameras),
138
+ "message": "Base64 image detection is running in background."
139
+ }
140
+ )
141
+
142
+
143
+ # @app.post("/start-detection") # live stream
144
+ # async def start_detection(data: Dict):
145
+ # station_id = data.get("station_id")
146
+ # parts = data.get("parts")
147
+ # webhook_url = data.get("webhook_url")
148
+ # cameras = data.get("cameras", [])
149
+
150
+ # if not station_id or not parts or not webhook_url or not cameras:
151
+ # return JSONResponse(
152
+ # status_code=400,
153
+ # content={
154
+ # "status": "error",
155
+ # "station_id": station_id,
156
+ # "camera_count": len(cameras),
157
+ # "message": "Missing required fields"
158
+ # }
159
+ # )
160
+
161
+ # # -------------------------------
162
+ # # VALIDATION BEFORE EXECUTION
163
+ # # -------------------------------
164
+ # required_parts_fields = [
165
+ # "id",
166
+ # "pin_api",
167
+ # "name",
168
+ # "sku"
169
+ # ]
170
+ # validation_errors = validate_input(required_parts_fields, station_id, cameras, parts, webhook_url)
171
+
172
+ # if validation_errors:
173
+ # logger.error("[VALIDATION FAILED] Input data is invalid.")
174
+ # for err in validation_errors:
175
+ # logger.error(f" - {err}")
176
+ # return JSONResponse(
177
+ # status_code=400,
178
+ # content={
179
+ # "status": "error",
180
+ # "station_id": station_id,
181
+ # "camera_count": len(cameras),
182
+ # "message": " | ".join(validation_errors)
183
+ # }
184
+ # )
185
+ # logger.info(f"[INFO] Get metadata parts")
186
+ # model_path = model_by_id_metadata(parts['id'])
187
+
188
+ # logger.info(f"[INFO] Checking model_path")
189
+ # if isinstance(model_path, str):
190
+ # if not os.path.exists(model_path):
191
+ # logger.info(f"[INFO] Model file not found")
192
+ # return {"status": "error", "message": f"Model file not found: {model_path}"}
193
+ # model = YOLO(model_path)
194
+ # else:
195
+ # model = model_path
196
+
197
+ # logger.info(f"[START] Station {station_id} → {len(cameras)} kamera diproses")
198
+
199
+ # # running background
200
+ # asyncio.create_task(run_detection_group(station_id, cameras, webhook_url, model, parts))
201
+
202
+ # return JSONResponse(
203
+ # status_code=200,
204
+ # content={
205
+ # "status": "started",
206
+ # "station_id": station_id,
207
+ # "camera_count": len(cameras),
208
+ # "message": "Detection is running in background."
209
+ # }
210
+ # )
211
 
212
 
213
  # ============================================================
core/defect_detection_image.py ADDED
@@ -0,0 +1,228 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os, cv2, time, base64, asyncio, httpx
2
+ import numpy as np
3
+
4
+ from datetime import datetime
5
+ from dotenv import load_dotenv
6
+ from typing import Dict, List
7
+ from utils import *
8
+
9
+ load_dotenv()
10
+
11
+ MODEL_VERSION = os.getenv("MODEL_VERSION","v1.0.0")
12
+ WEBHOOK_URL = os.getenv("WEBHOOK_URL")
13
+
14
+ WEBHOOK_TIMEOUT = float(os.getenv("WEBHOOK_TIMEOUT", "10.0"))
15
+
16
+ # ============================================================
17
+ # DEFECT DETECTION FROM BASE64 IMAGE
18
+ # ============================================================
19
+ def detect_defect_from_base64(station_id: str, camera_id: str, image_base64: str, model=None):
20
+ """
21
+ Detect defect from a single Base64 image.
22
+ - Decode Base64 → OpenCV image
23
+ - Run YOLO model once
24
+ - Return result (OK / NG) + annotated image base64
25
+ """
26
+
27
+ try:
28
+ img_data = base64.b64decode(image_base64)
29
+ np_arr = np.frombuffer(img_data, np.uint8)
30
+ frame = cv2.imdecode(np_arr, cv2.IMREAD_COLOR)
31
+ if frame is None:
32
+ raise ValueError("Decoded image is None")
33
+ except Exception as e:
34
+ logger.error(f"[ERROR] Cannot decode base64 image for camera {camera_id}: {e}")
35
+ return {
36
+ "station_id": station_id,
37
+ "camera_id": camera_id,
38
+ "status": "error",
39
+ "status_defect": "",
40
+ "image_base64": "",
41
+ "detections": [],
42
+ "message": f"Invalid base64 image"
43
+ }
44
+
45
+
46
+ # ========== YOLO DETECTION ==========
47
+ if model:
48
+ # Predict
49
+ results = model.predict(source=frame, conf=0.4, imgsz=640, verbose=False)
50
+ boxes = results[0].boxes
51
+
52
+ if len(boxes) > 0:
53
+ for box in boxes:
54
+ cls = int(box.cls[0])
55
+ conf = float(box.conf[0])
56
+ xyxy = [int(x) for x in box.xyxy[0].tolist()]
57
+ defect_name = model.names.get(cls, f"class_{cls}").lower()
58
+
59
+ x1, y1, x2, y2 = xyxy
60
+
61
+ # Ambil warna berdasarkan defect
62
+ try:
63
+ color = color_defect(defect_name)
64
+ except Exception:
65
+ color = color_defect('other')
66
+
67
+ # Draw bounding box di frame
68
+ cv2.rectangle(frame, (x1, y1), (x2, y2), color, 2)
69
+
70
+ # Label
71
+ label = f"{defect_name.upper()} {conf:.2f}"
72
+ (w, h), _ = cv2.getTextSize(label, cv2.FONT_HERSHEY_SIMPLEX, 0.6, 2)
73
+ cv2.rectangle(frame, (x1, y1 - 20), (x1 + w, y1), color, -1)
74
+ cv2.putText(frame, label, (x1, y1 - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (255, 255, 255), 2)
75
+
76
+
77
+ # Convert annotated image ke Base64
78
+ _, buffer = cv2.imencode(".jpg", frame)
79
+ frame_base64 = base64.b64encode(buffer).decode("utf-8")
80
+
81
+ # Save annotated image
82
+ # output_dir = "outputs/images"
83
+ # os.makedirs(output_dir, exist_ok=True)
84
+ # filename = f"{station_id}_{camera_id}_NG_{datetime.now().strftime('%Y%m%d_%H%M%S')}.jpg"
85
+ # filepath = os.path.join(output_dir, filename)
86
+ # cv2.imwrite(filepath, frame)
87
+
88
+ # logger.info(f"[SAVED] NG image saved to {filepath}")
89
+
90
+ logger.info(f"[DETECTED] Camera {camera_id} → {defect_name} ({conf:.2f})")
91
+
92
+ return {
93
+ "station_id": station_id,
94
+ "camera_id": camera_id,
95
+ "status": "success",
96
+ "status_defect": "NG",
97
+ "image_base64": frame_base64,
98
+ "detections": [{
99
+ "class": defect_name,
100
+ "confidence": conf,
101
+ "bbox": xyxy
102
+ }],
103
+ "message": f"Detected as defect"
104
+ }
105
+
106
+ # ========== NO DEFECT ==========
107
+ _, buffer = cv2.imencode(".jpg", frame)
108
+ frame_base64 = base64.b64encode(buffer).decode("utf-8")
109
+
110
+ # Save OK image (no bbox)
111
+ # output_dir = "outputs/images"
112
+ # os.makedirs(output_dir, exist_ok=True)
113
+ # filename = f"{station_id}_{camera_id}_OK_{datetime.now().strftime('%Y%m%d_%H%M%S')}.jpg"
114
+ # filepath = os.path.join(output_dir, filename)
115
+ # cv2.imwrite(filepath, frame)
116
+ # logger.info(f"[SAVED] OK image saved to {filepath}")
117
+
118
+ logger.info(f"[OK] Camera {camera_id} → No defect detected.")
119
+ return {
120
+ "station_id": station_id,
121
+ "camera_id": camera_id,
122
+ "status": "success",
123
+ "status_defect": "OK",
124
+ "image_base64": frame_base64,
125
+ "detections": [],
126
+ "message": f"Detected as normal (no defect)"
127
+ }
128
+
129
+ # ============================================================
130
+ # ASYNC WRAPPERS
131
+ # ============================================================
132
+ async def _detect_camera_image(station_id: str, camera: Dict, model=None):
133
+ """Run detect_defect_from_base64 in thread for async parallel."""
134
+ return await asyncio.to_thread(
135
+ detect_defect_from_base64,
136
+ station_id,
137
+ camera["camera_id"],
138
+ camera["image_base64"],
139
+ model
140
+ )
141
+
142
+ async def run_detection_group(
143
+ station_id: str,
144
+ cameras: List[Dict],
145
+ webhook_url: str,
146
+ model=None,
147
+ parts: Dict = None
148
+ ):
149
+ """
150
+ Run defect detection for multiple cameras in parallel and send webhook.
151
+ All results are serialized safely for JSON.
152
+ """
153
+ parts = parts or {}
154
+
155
+ logger.info(f"[START] Station {station_id} → {len(cameras)} camera(s)")
156
+
157
+ # Run detection async parallel
158
+ results = await asyncio.gather(
159
+ *[_detect_camera_image(station_id, cam, model) for cam in cameras],
160
+ return_exceptions=True
161
+ )
162
+
163
+ # Convert exceptions to dict
164
+ clean_results = []
165
+ for r in results:
166
+ if isinstance(r, Exception):
167
+ clean_results.append({
168
+ "status": "error",
169
+ "message": str(r)
170
+ })
171
+ else:
172
+ clean_results.append(r)
173
+
174
+ # Determine overall status
175
+ has_error = any(r.get("status") == "error" for r in clean_results)
176
+ all_error = all(r.get("status") == "error" for r in clean_results)
177
+
178
+ if all_error:
179
+ status = "error"
180
+ message = "All cameras failed during detection"
181
+ elif has_error:
182
+ status = "partial_error"
183
+ message = "Some cameras failed during detection"
184
+ else:
185
+ status = "success"
186
+ message = "Success detecting defects"
187
+
188
+ # Build payload
189
+ payload = {
190
+ "status": status,
191
+ "timestamp": time.strftime("%Y-%m-%dT%H:%M:%S", time.localtime()),
192
+ "model_version": MODEL_VERSION,
193
+ "message": message,
194
+ "parts": parts,
195
+ # "data": make_serializable(clean_results),
196
+ }
197
+
198
+ # Send webhook
199
+ try:
200
+ async with httpx.AsyncClient(timeout=WEBHOOK_TIMEOUT) as client:
201
+ await client.post(webhook_url, json=payload)
202
+ logger.info(f"[DONE] Station {station_id}")
203
+ except Exception as e:
204
+ logger.exception(f"[ERROR] Webhook failed for Station {station_id}: {e}")
205
+
206
+ return payload
207
+
208
+ # ============================================================
209
+ # JSON SERIALIZABLE HELPER
210
+ # ============================================================
211
+ def make_serializable(obj):
212
+ """Convert object to JSON-serializable format."""
213
+ if isinstance(obj, (int, float, str, bool)) or obj is None:
214
+ return obj
215
+ elif isinstance(obj, (list, tuple)):
216
+ return [make_serializable(i) for i in obj]
217
+ elif isinstance(obj, dict):
218
+ return {k: make_serializable(v) for k, v in obj.items()}
219
+ elif isinstance(obj, datetime):
220
+ return obj.isoformat()
221
+ elif isinstance(obj, np.integer):
222
+ return int(obj)
223
+ elif isinstance(obj, np.floating):
224
+ return float(obj)
225
+ elif isinstance(obj, np.ndarray):
226
+ return obj.tolist()
227
+ else:
228
+ return str(obj)
utils.py CHANGED
@@ -46,8 +46,10 @@ def validate_input(required_parts_fields, station_id, cameras, parts, webhook_ur
46
  for index, cam in enumerate(cameras):
47
  if "camera_id" not in cam or not str(cam["camera_id"]).strip():
48
  errors.append(f"camera[{index}].camera_id missing or empty")
49
- if "rtsp_url" not in cam or not str(cam["rtsp_url"]).strip():
50
- errors.append(f"camera[{index}].rtsp_url missing or empty")
 
 
51
 
52
  # Validate webhook
53
  if not webhook_url or not webhook_url.startswith("http"):
 
46
  for index, cam in enumerate(cameras):
47
  if "camera_id" not in cam or not str(cam["camera_id"]).strip():
48
  errors.append(f"camera[{index}].camera_id missing or empty")
49
+ if "image_base64" not in cam or not str(cam["image_base64"]).strip():
50
+ errors.append(f"camera[{index}].image_base64 missing or empty")
51
+ # if "rtsp_url" not in cam or not str(cam["rtsp_url"]).strip():
52
+ # errors.append(f"camera[{index}].rtsp_url missing or empty")
53
 
54
  # Validate webhook
55
  if not webhook_url or not webhook_url.startswith("http"):