Navy commited on
Commit
297eef6
·
1 Parent(s): 1a20d1e

feat: all features

Browse files
.gitattributes CHANGED
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ models/* filter=lfs diff=lfs merge=lfs -text
.gitignore CHANGED
@@ -9,4 +9,5 @@ __pycache__/
9
 
10
  outputs/
11
  videos/
12
- images/
 
 
9
 
10
  outputs/
11
  videos/
12
+ images/
13
+ testing/
app.py CHANGED
@@ -1,7 +1,103 @@
1
  from fastapi import FastAPI
 
 
2
 
3
- app = FastAPI()
4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5
  @app.get("/")
6
- def greet_json():
7
- return {"Hello": "World!"}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  from fastapi import FastAPI
2
+ from fastapi.middleware.cors import CORSMiddleware
3
+ from ultralytics import YOLO
4
 
5
+ from typing import Dict
6
 
7
+ from core.defect_detection import *
8
+ from utils import *
9
+
10
+ import uvicorn, asyncio
11
+
12
+ # ============================================================
13
+ # APP SETUP
14
+ # ============================================================
15
+ app = FastAPI(
16
+ title="AI Engine Dummy",
17
+ version="1.0.0",
18
+ description="""
19
+ ## 🧠 AI Engine Dummy API
20
+
21
+ API simulasi integrasi **AI Engine** untuk deteksi defect pada sistem monitoring.
22
+
23
+ ---
24
+ ### 🔹 Endpoint Utama
25
+ - `/start-detection` → Memulai simulasi deteksi untuk beberapa kamera.
26
+
27
+ ### 🔹 Webhook
28
+ - Gunakan https://webhook.site/ untuk menerima hasil deteksi.
29
+ - Pastikan mengisi `webhook_url` pada payload request.
30
+
31
+ ### 🔹 Simulasi
32
+ - Tiap kamera akan melakukan deteksi selama max 5 (Sesuai Waktu Timeout) detik.
33
+ - Jika ditemukan defect, hasil langsung dikirim ke webhook dan semua kamera berhenti.
34
+ - Jika semua kamera tidak menemukan defect setelah 5 (Sesuai Waktu Timeout) detik → status "OK" dikirim satu kali.
35
+ """,
36
+ )
37
+
38
+ # ============================================================
39
+ # CORS CONFIG (Hanya port 8899)
40
+ # ============================================================
41
+ allowed_origins = [
42
+ "http://localhost:8899",
43
+ "http://127.0.0.1:8899",
44
+ "http://0.0.0.0:8899",
45
+ ]
46
+
47
+ app.add_middleware(
48
+ CORSMiddleware,
49
+ allow_origins=allowed_origins,
50
+ allow_credentials=True,
51
+ allow_methods=["*"],
52
+ allow_headers=["*"],
53
+ )
54
+
55
+
56
+ # ============================================================
57
+ # ROUTES
58
+ # ============================================================
59
  @app.get("/")
60
+ def read_root():
61
+ return {"message": "Defect Detection API is running."}
62
+
63
+
64
+ @app.post("/start-detection")
65
+ async def start_detection(data: Dict):
66
+ station_id = data.get("station_id")
67
+ parts = data.get("parts")
68
+ webhook_url = data.get("webhook_url")
69
+ cameras = data.get("cameras", [])
70
+
71
+ if not station_id or not parts or not webhook_url or not cameras:
72
+ return {"status": "error", "message": "Missing required fields"}
73
+
74
+ logger.info(f"[INFO] Get metadata parts")
75
+ model_path = model_by_id_metadata(parts['id'])
76
+
77
+ logger.info(f"[INFO] Checking model_path")
78
+ if isinstance(model_path, str):
79
+ if not os.path.exists(model_path):
80
+ logger.info(f"[INFO] Model file not found")
81
+ return {"status": "error", "message": f"Model file not found: {model_path}"}
82
+ model = YOLO(model_path)
83
+ else:
84
+ model = model_path
85
+
86
+ logger.info(f"[START] Station {station_id} → {len(cameras)} kamera diproses")
87
+
88
+ # running background
89
+ asyncio.create_task(run_detection_group(station_id, cameras, webhook_url, model, parts))
90
+
91
+ return {
92
+ "status": "started",
93
+ "station_id": station_id,
94
+ "camera_count": len(cameras),
95
+ "message": "Detection is running in background."
96
+ }
97
+
98
+
99
+ # ============================================================
100
+ # ENTRY POINT
101
+ # ============================================================
102
+ if __name__ == "__main__":
103
+ uvicorn.run(app, host="0.0.0.0", port=7860)
core/defect_detection.py ADDED
@@ -0,0 +1,187 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os, cv2, time, base64, asyncio, httpx
2
+
3
+ from datetime import datetime
4
+ from dotenv import load_dotenv
5
+ from typing import Dict, List
6
+ from utils import *
7
+
8
+ load_dotenv()
9
+
10
+ MODEL_VERSION = os.getenv("MODEL_VERSION")
11
+ WEBHOOK_URL = os.getenv("WEBHOOK_URL")
12
+
13
+ MAX_RUNTIME_SEC = float(os.getenv("MAX_RUNTIME_SEC"))
14
+ FRAME_FAIL_SLEEP = float(os.getenv("FRAME_FAIL_SLEEP"))
15
+ DEFAULT_FPS = float(os.getenv("DEFAULT_FPS"))
16
+ WEBHOOK_TIMEOUT = float(os.getenv("WEBHOOK_TIMEOUT"))
17
+
18
+ # ============================================================
19
+ # DEFECT DETECTION FROM VIDEO URL
20
+ # ============================================================
21
+ def detect_defect_from_video_url(station_id, camera_id: str, video_url: str, model=None):
22
+ """
23
+ Detect defects sequentially from a video URL.
24
+ - Reads frames in order.
25
+ - Returns immediately when a defect is found.
26
+ - Returns OK if timeout or no detection.
27
+ - Always saves last processed image (OK or NG) to outputs/images/
28
+ """
29
+
30
+ cap = cv2.VideoCapture(video_url)
31
+ if not cap.isOpened():
32
+ logger.error(f"[ERROR] Cannot open video URL: {video_url}")
33
+ return {
34
+ "station_id": station_id,
35
+ "camera_id": camera_id,
36
+ "status": "error",
37
+ "detections": [],
38
+ "message": f"Cannot open video URL: {video_url}"
39
+ }
40
+
41
+ fps = DEFAULT_FPS
42
+ if fps == 0 or fps != fps: # handle NaN
43
+ fps = DEFAULT_FPS
44
+
45
+ start_time = time.time()
46
+ frame_index = 0
47
+ last_frame = None
48
+
49
+ while True:
50
+ elapsed = time.time() - start_time
51
+ if elapsed > MAX_RUNTIME_SEC:
52
+ logger.info(f"[OK] {camera_id} → Timeout reached ({MAX_RUNTIME_SEC}s), no defect detected.")
53
+ break
54
+
55
+ ret, frame = cap.read()
56
+ if not ret:
57
+ time.sleep(FRAME_FAIL_SLEEP)
58
+ continue
59
+
60
+ frame_index += 1
61
+ time.sleep(1 / fps)
62
+ last_frame = frame.copy()
63
+
64
+ # YOLO DETECTION
65
+ if model:
66
+ results = model.predict(source=frame, conf=0.4, imgsz=640, verbose=False)
67
+ boxes = results[0].boxes
68
+
69
+ if len(boxes) > 0:
70
+ for box in boxes:
71
+ cls = int(box.cls[0])
72
+ conf = float(box.conf[0])
73
+ xyxy = [int(x) for x in box.xyxy[0].tolist()]
74
+ defect_name = model.names.get(cls, f"class_{cls}").lower()
75
+
76
+ x1, y1, x2, y2 = xyxy
77
+
78
+ # Ambil warna berdasarkan defect
79
+ try :
80
+ color = color_defect(defect_name)
81
+ except Exception as e:
82
+ color = color_defect('other')
83
+
84
+ # Draw bounding box
85
+ cv2.rectangle(frame, (x1, y1), (x2, y2), color, 2)
86
+
87
+ # Label
88
+ label = f"{defect_name.upper()} {conf:.2f}"
89
+ (w, h), _ = cv2.getTextSize(label, cv2.FONT_HERSHEY_SIMPLEX, 0.6, 2)
90
+ cv2.rectangle(frame, (x1, y1 - 20), (x1 + w, y1), color, -1)
91
+ cv2.putText(frame, label, (x1, y1 - 5),
92
+ cv2.FONT_HERSHEY_SIMPLEX, 0.6, (255, 255, 255), 2)
93
+
94
+ # Convert frame to Base64
95
+ _, buffer = cv2.imencode(".jpg", frame)
96
+ frame_base64 = base64.b64encode(buffer).decode("utf-8")
97
+
98
+ # Save annotated image
99
+ output_dir = "outputs/images"
100
+ os.makedirs(output_dir, exist_ok=True)
101
+ filename = f"{station_id}_{camera_id}_NG_{datetime.now().strftime('%Y%m%d_%H%M%S')}.jpg"
102
+ filepath = os.path.join(output_dir, filename)
103
+ cv2.imwrite(filepath, frame)
104
+ logger.info(f"[SAVED] NG image saved to {filepath}")
105
+
106
+ cap.release()
107
+ logger.info(f"[DETECTED] Camera {camera_id} → {defect_name} ({conf:.2f})")
108
+
109
+ return {
110
+ "station_id": station_id,
111
+ "camera_id": camera_id,
112
+ "status_defect": "NG",
113
+ "image_base64": frame_base64,
114
+ "image_path": filepath,
115
+ "detections": [{
116
+ "class": defect_name,
117
+ "confidence": conf,
118
+ "bbox": xyxy
119
+ }]
120
+ }
121
+
122
+ # --- no defect detected ---
123
+ cap.release()
124
+
125
+ if last_frame is not None:
126
+ _, buffer = cv2.imencode(".jpg", last_frame)
127
+ frame_base64 = base64.b64encode(buffer).decode("utf-8")
128
+
129
+ # Save OK image (no bbox)
130
+ output_dir = "outputs/images"
131
+ os.makedirs(output_dir, exist_ok=True)
132
+ filename = f"{station_id}_{camera_id}_OK_{datetime.now().strftime('%Y%m%d_%H%M%S')}.jpg"
133
+ filepath = os.path.join(output_dir, filename)
134
+ cv2.imwrite(filepath, last_frame)
135
+ logger.info(f"[SAVED] OK image saved to {filepath}")
136
+ else:
137
+ frame_base64 = ""
138
+ filepath = None
139
+
140
+ return {
141
+ "station_id": station_id,
142
+ "camera_id": camera_id,
143
+ "status_defect": "OK",
144
+ "image_base64": frame_base64,
145
+ "image_path": filepath,
146
+ "detections": []
147
+ }
148
+
149
+
150
+ # ============================================================
151
+ # ASYNC WRAPPERS
152
+ # ============================================================
153
+ async def _detect_camera_video(station_id: str, camera: Dict, stop_flag: Dict, model=None):
154
+ """Run detection in thread (for async parallel)."""
155
+ return await asyncio.to_thread(detect_defect_from_video_url, station_id, camera["camera_id"], camera["rtsp_url"], model)
156
+
157
+
158
+ async def run_detection_group(station_id: str, cameras: List[Dict], webhook_url: str, model=None, parts=str):
159
+ """
160
+ Run detection for all cameras in parallel.
161
+ Send webhook with NG/OK status.
162
+ """
163
+ stop_flag = {"stop": False}
164
+ logger.info(f"[START] Station {station_id} → {len(cameras)} camera(s)")
165
+
166
+ results = await asyncio.gather(*[
167
+ _detect_camera_video(station_id, cam, stop_flag, model)
168
+ for cam in cameras
169
+ ])
170
+
171
+ payload = {
172
+ "status": "success",
173
+ "timestamp": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
174
+ "model_version": MODEL_VERSION,
175
+ "parts" : parts,
176
+ "data": results,
177
+ }
178
+
179
+ # Send webhook
180
+ try:
181
+ async with httpx.AsyncClient(timeout=WEBHOOK_TIMEOUT) as client:
182
+ await client.post(webhook_url, json=payload)
183
+ logger.info(f"[DONE] Station {station_id}")
184
+ except Exception as e:
185
+ logger.error(f"[ERROR] Webhook failed: {e}")
186
+
187
+ # return payload
metadata/defect.json ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [
2
+ { "class": "short_shoot", "color": "#FF0000" },
3
+ { "class": "scratch", "color": "#00B050" },
4
+ { "class": "burry", "color": "#FFFF00" },
5
+ { "class": "crack", "color": "#3282F6" },
6
+ { "class": "silver", "color": "#0023F5" },
7
+ { "class": "weldline", "color": "#7030A0" },
8
+ { "class": "ejectormark", "color": "#FFC000" },
9
+ { "class": "bubble", "color": "#EF88BE" },
10
+ { "class": "kontaminasi", "color": "#7EB5F7" },
11
+ { "class": "jetting", "color": "#B5E61D" },
12
+ { "class": "ng_collar", "color": "#C0C0C0" },
13
+ { "class": "minyak", "color": "#B97A57" },
14
+ { "class": "bending", "color": "#EFE4B0" },
15
+ { "class": "overcut", "color": "#99D9EA" },
16
+ { "class": "sinmark", "color": "#817F26" },
17
+ { "class": "flowmark", "color": "#C00000" },
18
+ { "class": "burnmark", "color": "#F08784" },
19
+ { "class": "gloss", "color": "#7092BE" },
20
+ { "class": "mutih", "color": "#C8BFE7" },
21
+ { "class": "blackspot", "color": "#75163F" },
22
+ { "class": "bintik", "color": "#FFFE91" },
23
+ { "class": "other", "color": "#FFFFFF" }
24
+ ]
metadata/product.json ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [
2
+ {
3
+ "id": "4",
4
+ "pin_api": "JI4ACL-GCSBYHBK03",
5
+ "name": "CASE BLOWER LHD_SFG - LH",
6
+ "sku": "002928",
7
+ "model_path": "models/short shot_case lhd.pt"
8
+ },
9
+ {
10
+ "id": "6",
11
+ "pin_api": "JI4ACL-GCB5Y4BK00",
12
+ "name": "CASE BLOWER JK017470-4150",
13
+ "sku": "005581",
14
+ "model_path": "models/short shot_case blower jk 2.pt"
15
+ }
16
+ ]
models/crack_duct.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:db9bd066179cb368c2300ffeb772b8a3616043801c5ee75fd16dbb9e16804a9a
3
+ size 6245539
models/short shot_case blower jk 2.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:d00c7c978e73e7752933920d617b72c9bb5e1f234dc84816f55b91338fbbe72c
3
+ size 6299811
models/short shot_case blower jk.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:bc3bad050ab2dcc8fda6b45802373c508f4da4066fb41e0e1ce582fa0bafb0ff
3
+ size 6301987
models/short shot_case lhd.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:94cb1c2bd2753fc9313afd02cb44bf4eb29fca14a49e9a49c7afcdd3ae9367a2
3
+ size 6245539
requirements.txt CHANGED
Binary files a/requirements.txt and b/requirements.txt differ
 
utils.py ADDED
@@ -0,0 +1,86 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging, sys, asyncio, random, json
2
+ from pathlib import Path
3
+
4
+ # ============================================================
5
+ # LOGGER SETUP
6
+ # ============================================================
7
+ logger = logging.getLogger("ai_engine_dummy")
8
+ logger.setLevel(logging.INFO)
9
+
10
+ # Handler untuk tampil di console
11
+ handler = logging.StreamHandler(sys.stdout)
12
+ formatter = logging.Formatter("[%(asctime)s] %(levelname)s → %(message)s", "%H:%M:%S")
13
+ handler.setFormatter(formatter)
14
+ logger.addHandler(handler)
15
+
16
+ # ============================================================
17
+ # HELPER FUNCTION (opsional untuk simulasi delay)
18
+ # ============================================================
19
+ async def async_sleep_random(min_s=0.2, max_s=0.8):
20
+ """
21
+ Helper untuk simulasi waktu inferensi secara acak.
22
+ """
23
+ durasi = random.uniform(min_s, max_s)
24
+ await asyncio.sleep(durasi)
25
+
26
+ # ============================================================
27
+ # HELPER
28
+ # ============================================================
29
+ def _metadata():
30
+ """
31
+ load file metadata.json into json data
32
+ """
33
+ path = Path("metadata/product.json")
34
+ if not path.exists():
35
+ return {"status": "error", "message": "metadata.json not found"}
36
+
37
+ with open(path, "r") as f:
38
+ data = json.load(f)
39
+ return data
40
+
41
+ def _color_map():
42
+ path = Path("metadata/defect.json")
43
+ if not path.exists():
44
+ return []
45
+
46
+ with open(path, "r") as f:
47
+ return json.load(f)
48
+
49
+ def model_by_id_metadata(part_id):
50
+ """
51
+ part_id = "4" # id part / product
52
+ get model_path from metadata by part_id
53
+ """
54
+ metadata = _metadata()
55
+ id_part = part_id
56
+ item = next((x for x in metadata if x["id"] == id_part), None)
57
+ model_path = item['model_path']
58
+ return model_path
59
+
60
+ def model_by_pin_metadata(pin_api):
61
+ """
62
+ pin_api = "JI4ACL-GCSBYHBK03" # PIN API from part / product
63
+ get model_path from metadata by pin_api
64
+ """
65
+ metadata = _metadata()
66
+ pin_api = pin_api
67
+ item = next((x for x in metadata if x["pin_api"] == pin_api), None)
68
+ model_path = item['model_path']
69
+ return model_path
70
+
71
+ def color_defect(defect_name):
72
+ """
73
+ Return BGR tuple color for OpenCV bounding box.
74
+ """
75
+ defect_name = defect_name.lower()
76
+ data = _color_map()
77
+
78
+ item = next((x for x in data if x["class"].lower() == defect_name), None)
79
+ if item is None:
80
+ return (0, 255, 0) # Default GREEN jika tidak ditemukan
81
+
82
+ hex_color = item["color"].lstrip('#')
83
+ r = int(hex_color[0:2], 16)
84
+ g = int(hex_color[2:4], 16)
85
+ b = int(hex_color[4:6], 16)
86
+ return (b, g, r) # Convert to BGR