doniramdani820 commited on
Commit
e7e7810
·
verified ·
1 Parent(s): a215ddc

Update main.py

Browse files
Files changed (1) hide show
  1. main.py +100 -136
main.py CHANGED
@@ -1,7 +1,8 @@
1
  #!/usr/bin/env python3
2
  """
3
- GeeTest4 Solver - Pure FastAPI Version
4
- 100% API endpoint without any UI dependencies
 
5
  """
6
 
7
  import os
@@ -19,143 +20,111 @@ import numpy as np
19
  from PIL import Image
20
  import cv2
21
 
22
- # Try to import ONNX (optional)
23
- ONNX_AVAILABLE = False
24
  try:
25
  import onnxruntime as ort
26
  ONNX_AVAILABLE = True
27
  except ImportError:
28
- pass
29
 
30
- # --- Configuration ---
 
 
31
  MODEL_PATH = "best_model.onnx"
32
- YAML_PATH = "data.yaml"
33
- CONFIDENCE_THRESHOLD = 0.3
34
- NMS_IOU_THRESHOLD = 0.5
35
  API_KEY = os.getenv("GEETEST4_API_KEY", "ADMINCKV005")
36
 
37
- # Global variables
 
 
 
 
38
  model_session = None
39
  CLASS_NAMES = []
40
 
41
- # --- Setup Logging ---
42
- logging.basicConfig(level=logging.INFO)
43
  logger = logging.getLogger(__name__)
44
 
45
- # --- Pydantic Models ---
46
  class PredictRequest(BaseModel):
47
  data: List[str]
48
-
49
- # MODIFIKASI: Tipe data untuk bounding box
50
  BoundingBox = Dict[str, int]
51
 
52
  def verify_api_key(api_key: str) -> bool:
53
- """Verify API key"""
54
  return api_key == API_KEY
55
 
56
- def smart_cv_model(image_np: np.ndarray) -> Tuple[int, float, Union[BoundingBox, None]]:
57
- """Computer Vision model - sekarang mengembalikan bounding box"""
58
- height, width = image_np.shape[:2]
59
- try:
60
- gray = cv2.cvtColor(image_np, cv2.COLOR_RGB2GRAY)
61
- _, thresh = cv2.threshold(gray, 100, 255, cv2.THRESH_BINARY_INV)
62
- contours, _ = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
63
-
64
- if contours:
65
- largest_contour = max(contours, key=cv2.contourArea)
66
- x, y, w, h = cv2.boundingRect(largest_contour)
67
- center_x = x + w // 2
68
-
69
- # MODIFIKASI: Siapkan data bounding box untuk dikembalikan
70
- bbox = {'x': x, 'y': y, 'w': w, 'h': h}
71
-
72
- center_x = max(int(width * 0.1), min(center_x, int(width * 0.9)))
73
- area_ratio = cv2.contourArea(largest_contour) / (width * height)
74
- confidence = min(0.9, max(0.6, area_ratio * 10))
75
-
76
- logger.info(f"CV Model: target at x={center_x}, confidence={confidence:.3f}")
77
- # MODIFIKASI: Kembalikan bbox
78
- return center_x, confidence, bbox
79
- else:
80
- # Fallback jika tidak ada kontur
81
- random.seed(hash(image_np.tobytes()) % 2**31)
82
- target_x = int(width * (0.45 + random.random() * 0.3))
83
- confidence = 0.65 + random.random() * 0.15
84
- logger.info(f"CV Model (rule-based): target at x={target_x}, confidence={confidence:.3f}")
85
- return target_x, confidence, None
86
-
87
- except Exception as e:
88
- logger.warning(f"CV processing failed, using safe fallback: {e}")
89
- center_x = int(width * 0.6)
90
- return center_x, 0.7, None
91
 
92
  def process_image_onnx(image_np: np.ndarray) -> Tuple[int, float, Union[BoundingBox, None]]:
93
- """Process image with ONNX model - sekarang mengembalikan bounding box"""
94
- height, width = image_np.shape[:2]
95
  try:
96
- max_size = 640
97
- ratio = min(max_size / width, max_size / height)
98
- new_width, new_height = int(width * ratio), int(height * ratio)
99
- resized = cv2.resize(image_np, (new_width, new_height))
100
-
101
- dw, dh = (max_size - new_width) // 2, (max_size - new_height) // 2
102
- padded = cv2.copyMakeBorder(resized, dh, max_size - new_height - dh, dw, max_size - new_width - dw, cv2.BORDER_CONSTANT, value=(114, 114, 114))
103
-
104
- input_tensor = (padded.astype(np.float32) / 255.0).transpose(2, 0, 1)
105
- input_tensor = np.expand_dims(input_tensor, axis=0)
106
 
 
107
  outputs = model_session.run(None, {model_session.get_inputs()[0].name: input_tensor})
108
- preds = outputs[0][0]
109
 
110
- if len(preds) == 0:
111
- return 0, 0.0, None
 
 
 
 
 
 
112
 
113
- box_scores = preds[:, 4:]
114
- max_scores = np.max(box_scores, axis=1)
115
 
116
- valid_preds = max_scores > CONFIDENCE_THRESHOLD
117
- if not np.any(valid_preds):
118
  return 0, 0.0, None
119
 
120
- preds = preds[valid_preds]
121
- max_scores = max_scores[valid_preds]
122
-
123
- boxes_raw = preds[:, :4]
124
- boxes_raw[:, 0] = (boxes_raw[:, 0] - dw) / ratio
125
- boxes_raw[:, 1] = (boxes_raw[:, 1] - dh) / ratio
126
- boxes_raw[:, 2] /= ratio
127
- boxes_raw[:, 3] /= ratio
128
-
129
- x1, y1 = boxes_raw[:, 0] - boxes_raw[:, 2] / 2, boxes_raw[:, 1] - boxes_raw[:, 3] / 2
130
- x2, y2 = boxes_raw[:, 0] + boxes_raw[:, 2] / 2, boxes_raw[:, 1] + boxes_raw[:, 3] / 2
131
-
132
- boxes_processed = np.column_stack((x1, y1, x2, y2)).astype(np.float32)
133
- indices = cv2.dnn.NMSBoxes(boxes_processed, max_scores, CONFIDENCE_THRESHOLD, NMS_IOU_THRESHOLD)
134
 
135
  if len(indices) == 0:
136
  return 0, 0.0, None
137
-
138
- best_idx = indices.flatten()[0]
139
- best_box = boxes_processed[best_idx]
140
- best_score = max_scores[best_idx]
141
- center_x = int((best_box[0] + best_box[2]) / 2)
142
 
143
- # MODIFIKASI: Konversi dari x1,y1,x2,y2 ke x,y,w,h
144
- x = int(best_box[0])
145
- y = int(best_box[1])
146
- w = int(best_box[2] - best_box[0])
147
- h = int(best_box[3] - best_box[1])
148
- bbox = {'x': x, 'y': y, 'w': w, 'h': h}
 
 
 
 
 
 
 
 
 
149
 
150
- # MODIFIKASI: Kembalikan bbox
151
  return center_x, float(best_score), bbox
152
 
153
  except Exception as e:
154
- logger.error(f"Error in ONNX processing: {e}")
155
  return 0, 0.0, None
156
 
157
  def load_model():
158
- """Load ONNX model and class names"""
159
  global model_session, CLASS_NAMES
160
  try:
161
  if os.path.exists(YAML_PATH):
@@ -163,68 +132,56 @@ def load_model():
163
  CLASS_NAMES = yaml.safe_load(f).get('names', ['Target'])
164
  else:
165
  CLASS_NAMES = ['Target']
166
- logger.info(f"Loaded {len(CLASS_NAMES)} classes: {CLASS_NAMES}")
167
 
168
  if ONNX_AVAILABLE and os.path.exists(MODEL_PATH):
169
  model_session = ort.InferenceSession(MODEL_PATH, providers=['CPUExecutionProvider'])
170
- logger.info("✅ ONNX model loaded successfully")
171
  else:
172
- logger.warning("ONNX model not found or ONNX runtime not available. Using CV model.")
173
  model_session = None
 
 
174
  except Exception as e:
175
- logger.error(f"Failed to load model: {e}")
176
  model_session = None
177
 
178
  def base64_to_numpy(base64_string: str) -> np.ndarray:
179
- """Convert base64 to numpy array"""
180
  try:
181
  if base64_string.startswith('data:image'):
182
  base64_string = base64_string.split(',')[1]
183
  image_data = base64.b64decode(base64_string)
184
  return np.array(Image.open(io.BytesIO(image_data)).convert('RGB'))
185
  except Exception as e:
186
- logger.error(f"Error converting base64: {e}")
187
- raise ValueError("Invalid image data")
188
 
189
  def solve_geetest4_api(background_image: str, api_key: str):
190
- """Pure API endpoint function - sekarang mengembalikan bounding box"""
191
  try:
192
  if not verify_api_key(api_key):
193
- # MODIFIKASI: Tambah None untuk konsistensi format
194
- return ["❌ Invalid API key", 0, 0.0, None]
195
 
196
  image_np = base64_to_numpy(background_image)
197
- bbox = None # Default value
198
-
199
- if model_session:
200
- # MODIFIKASI: Tangkap bbox dari return value
201
  target_x, confidence, bbox = process_image_onnx(image_np)
202
- if confidence < CONFIDENCE_THRESHOLD:
203
- logger.info("ONNX confidence too low, using CV fallback")
204
- target_x, confidence, bbox = smart_cv_model(image_np)
205
- model_type = "CV"
206
- else:
207
- model_type = "ONNX"
208
  else:
209
- # MODIFIKASI: Tangkap bbox dari return value
210
- target_x, confidence, bbox = smart_cv_model(image_np)
211
- model_type = "CV"
212
 
213
- if target_x > 0 and confidence >= 0.5:
214
- # MODIFIKASI: Sertakan bbox dalam respons sukses
215
- return [f"✅ Success! Target at x={target_x} (Model: {model_type})", target_x, confidence, bbox]
216
  else:
217
- fallback_x = int(image_np.shape[1] * 0.6)
218
- # MODIFIKASI: Tambah None untuk konsistensi format
219
- return [f"✅ Fallback position x={fallback_x}", fallback_x, 0.7, None]
220
 
221
  except Exception as e:
222
- logger.error(f"API Error: {e}")
223
- # MODIFIKASI: Tambah None untuk konsistensi format
224
- return [f"⚠️ Error, using fallback position", 200, 0.6, None]
225
 
 
226
  load_model()
227
- app = FastAPI(title="GeeTest4 Solver API", version="1.1.0", docs_url=None, redoc_url=None)
 
 
228
 
229
  @app.get("/")
230
  async def root():
@@ -232,22 +189,29 @@ async def root():
232
 
233
  @app.post("/api/predict")
234
  async def predict(request: PredictRequest):
235
- """Main API endpoint for GeeTest4 solving"""
236
  try:
237
  if len(request.data) < 2:
238
- raise HTTPException(status_code=400, detail="Invalid request format")
239
 
240
  background_image, api_key = request.data[0], request.data[1]
241
  result = solve_geetest4_api(background_image, api_key)
242
  return {"data": result}
 
243
  except Exception as e:
244
  logger.error(f"API Error: {e}")
245
- return JSONResponse(status_code=500, content={"data": ["❌ Server error", 0, 0.0, None]})
246
 
247
  @app.get("/health")
248
  async def health_check():
249
  return {"status": "healthy", "model_loaded": model_session is not None}
250
 
 
251
  if __name__ == "__main__":
252
- logger.info("🚀 Starting GeeTest4 Pure FastAPI Server")
253
- uvicorn.run(app, host="0.0.0.0", port=int(os.getenv("PORT", 7860)), log_level="info")
 
 
 
 
 
 
1
  #!/usr/bin/env python3
2
  """
3
+ GeeTest4 Solver - Pure FastAPI Version v1.2.0
4
+ - Updated with optimal thresholds from testing.
5
+ - Fixed YOLOv8 ONNX output transpose bug.
6
  """
7
 
8
  import os
 
20
  from PIL import Image
21
  import cv2
22
 
23
+ # Coba impor ONNX
 
24
  try:
25
  import onnxruntime as ort
26
  ONNX_AVAILABLE = True
27
  except ImportError:
28
+ ONNX_AVAILABLE = False
29
 
30
+ # ===================================================================
31
+ # KONFIGURASI
32
+ # ===================================================================
33
  MODEL_PATH = "best_model.onnx"
34
+ YAML_PATH = "data.yaml"
 
 
35
  API_KEY = os.getenv("GEETEST4_API_KEY", "ADMINCKV005")
36
 
37
+ # MODIFIKASI: Menggunakan nilai optimal dari hasil tes interaktif
38
+ CONFIDENCE_THRESHOLD = 0.25
39
+ NMS_IOU_THRESHOLD = 0.0
40
+
41
+ # Variabel Global
42
  model_session = None
43
  CLASS_NAMES = []
44
 
45
+ # Setup Logging
46
+ logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
47
  logger = logging.getLogger(__name__)
48
 
49
+ # Pydantic Models
50
  class PredictRequest(BaseModel):
51
  data: List[str]
 
 
52
  BoundingBox = Dict[str, int]
53
 
54
  def verify_api_key(api_key: str) -> bool:
 
55
  return api_key == API_KEY
56
 
57
+ def preprocess_for_onnx(image: np.ndarray, input_size: int = 640):
58
+ height, width, _ = image.shape
59
+ r = min(input_size / width, input_size / height)
60
+ new_width, new_height = int(width * r), int(height * r)
61
+ resized_image = cv2.resize(image, (new_width, new_height), interpolation=cv2.INTER_LINEAR)
62
+ d_w, d_h = (input_size - new_width) // 2, (input_size - new_height) // 2
63
+ padded_image = np.full((input_size, input_size, 3), 114, dtype=np.uint8)
64
+ padded_image[d_h:new_height + d_h, d_w:new_width + d_w, :] = resized_image
65
+ input_tensor = (padded_image.astype(np.float32) / 255.0).transpose(2, 0, 1)
66
+ return np.expand_dims(input_tensor, axis=0), r, d_w, d_h
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
67
 
68
  def process_image_onnx(image_np: np.ndarray) -> Tuple[int, float, Union[BoundingBox, None]]:
69
+ """Memproses gambar dengan model ONNX."""
 
70
  try:
71
+ input_tensor, ratio, dw, dh = preprocess_for_onnx(image_np)
 
 
 
 
 
 
 
 
 
72
 
73
+ # Jalankan inferensi
74
  outputs = model_session.run(None, {model_session.get_inputs()[0].name: input_tensor})
 
75
 
76
+ # ==================================================================
77
+ # PERBAIKAN KRUSIAL: Tambahkan .T untuk transpose output model YOLOv8
78
+ # ==================================================================
79
+ raw_predictions = outputs[0][0].T
80
+
81
+ # Ambil skor confidence (sekarang berada di kolom yang benar)
82
+ scores = raw_predictions[:, 4]
83
+ valid_indices = scores > CONFIDENCE_THRESHOLD
84
 
85
+ boxes_raw = raw_predictions[valid_indices, :4]
86
+ scores = scores[valid_indices]
87
 
88
+ if len(boxes_raw) == 0:
 
89
  return 0, 0.0, None
90
 
91
+ # Konversi box dari (center_x, center_y, w, h) ke (x1, y1, x2, y2)
92
+ x1 = boxes_raw[:, 0] - boxes_raw[:, 2] / 2
93
+ y1 = boxes_raw[:, 1] - boxes_raw[:, 3] / 2
94
+ x2 = boxes_raw[:, 0] + boxes_raw[:, 2] / 2
95
+ y2 = boxes_raw[:, 1] + boxes_raw[:, 3] / 2
96
+ boxes_for_nms = np.column_stack((x1, y1, x2, y2)).astype(np.float32)
97
+
98
+ # Terapkan Non-Max Suppression
99
+ indices = cv2.dnn.NMSBoxes(boxes_for_nms, scores, CONFIDENCE_THRESHOLD, NMS_IOU_THRESHOLD)
 
 
 
 
 
100
 
101
  if len(indices) == 0:
102
  return 0, 0.0, None
 
 
 
 
 
103
 
104
+ # Ambil deteksi terbaik (dengan skor tertinggi setelah NMS)
105
+ indices = indices.flatten()
106
+ best_idx = indices[np.argmax(scores[indices])]
107
+
108
+ best_box_coords = boxes_for_nms[best_idx]
109
+ best_score = scores[best_idx]
110
+
111
+ # Konversi koordinat kembali ke ukuran gambar asli
112
+ x1_orig = int((best_box_coords[0] - dw) / ratio)
113
+ y1_orig = int((best_box_coords[1] - dh) / ratio)
114
+ x2_orig = int((best_box_coords[2] - dw) / ratio)
115
+ y2_orig = int((best_box_coords[3] - dh) / ratio)
116
+
117
+ center_x = (x1_orig + x2_orig) // 2
118
+ bbox = {'x': x1_orig, 'y': y1_orig, 'w': x2_orig - x1_orig, 'h': y2_orig - y1_orig}
119
 
 
120
  return center_x, float(best_score), bbox
121
 
122
  except Exception as e:
123
+ logger.error(f"Error dalam pemrosesan ONNX: {e}")
124
  return 0, 0.0, None
125
 
126
  def load_model():
127
+ """Memuat model ONNX."""
128
  global model_session, CLASS_NAMES
129
  try:
130
  if os.path.exists(YAML_PATH):
 
132
  CLASS_NAMES = yaml.safe_load(f).get('names', ['Target'])
133
  else:
134
  CLASS_NAMES = ['Target']
 
135
 
136
  if ONNX_AVAILABLE and os.path.exists(MODEL_PATH):
137
  model_session = ort.InferenceSession(MODEL_PATH, providers=['CPUExecutionProvider'])
138
+ logger.info("✅ Model ONNX berhasil dimuat.")
139
  else:
 
140
  model_session = None
141
+ logger.critical("❌ GAGAL: Model ONNX tidak ditemukan atau onnxruntime tidak terinstal.")
142
+
143
  except Exception as e:
144
+ logger.error(f"FATAL: Gagal memuat model: {e}")
145
  model_session = None
146
 
147
  def base64_to_numpy(base64_string: str) -> np.ndarray:
 
148
  try:
149
  if base64_string.startswith('data:image'):
150
  base64_string = base64_string.split(',')[1]
151
  image_data = base64.b64decode(base64_string)
152
  return np.array(Image.open(io.BytesIO(image_data)).convert('RGB'))
153
  except Exception as e:
154
+ logger.error(f"Error saat konversi base64: {e}")
155
+ raise ValueError("Data gambar tidak valid")
156
 
157
  def solve_geetest4_api(background_image: str, api_key: str):
158
+ """Fungsi endpoint API utama."""
159
  try:
160
  if not verify_api_key(api_key):
161
+ return ["❌ Kunci API tidak valid", 0, 0.0, None]
 
162
 
163
  image_np = base64_to_numpy(background_image)
164
+
165
+ if model_session is not None:
 
 
166
  target_x, confidence, bbox = process_image_onnx(image_np)
167
+ model_type = "ONNX"
 
 
 
 
 
168
  else:
169
+ return ["❌ Model tidak dimuat", 0, 0.0, None]
 
 
170
 
171
+ if target_x > 0 and bbox is not None:
172
+ return [f"✅ Sukses! Target di x={target_x} (Model: {model_type})", target_x, confidence, bbox]
 
173
  else:
174
+ return [f"⚠️ Tidak ada target terdeteksi dengan threshold saat ini.", 0, 0.0, None]
 
 
175
 
176
  except Exception as e:
177
+ logger.error(f"Error API: {e}")
178
+ return [f"⚠️ Error server, menggunakan posisi fallback", 200, 0.6, None]
 
179
 
180
+ # Inisialisasi model saat startup
181
  load_model()
182
+
183
+ # --- Aplikasi FastAPI ---
184
+ app = FastAPI(title="GeeTest4 Solver API", version="1.2.0", docs_url=None, redoc_url=None)
185
 
186
  @app.get("/")
187
  async def root():
 
189
 
190
  @app.post("/api/predict")
191
  async def predict(request: PredictRequest):
192
+ """Endpoint utama untuk prediksi."""
193
  try:
194
  if len(request.data) < 2:
195
+ raise HTTPException(status_code=400, detail="Format request tidak valid")
196
 
197
  background_image, api_key = request.data[0], request.data[1]
198
  result = solve_geetest4_api(background_image, api_key)
199
  return {"data": result}
200
+
201
  except Exception as e:
202
  logger.error(f"API Error: {e}")
203
+ return JSONResponse(status_code=500, content={"data": ["❌ Error internal server", 0, 0.0, None]})
204
 
205
  @app.get("/health")
206
  async def health_check():
207
  return {"status": "healthy", "model_loaded": model_session is not None}
208
 
209
+ # Menjalankan aplikasi
210
  if __name__ == "__main__":
211
+ logger.info("🚀 Memulai Server FastAPI GeeTest4...")
212
+ uvicorn.run(
213
+ "__main__:app",
214
+ host="0.0.0.0",
215
+ port=int(os.getenv("PORT", 7860)),
216
+ log_level="info"
217
+ )