Dockerfile CHANGED
@@ -1,26 +1,28 @@
1
  FROM python:3.10-slim
2
-
 
3
  RUN useradd -m -u 1000 user
4
-
 
 
 
 
 
5
  USER root
6
  RUN apt-get update && apt-get install -y --no-install-recommends \
7
  libgl1 libglib2.0-0 libsm6 libxrender1 libxext6 \
8
  && rm -rf /var/lib/apt/lists/*
9
-
10
  USER user
11
- ENV PATH="/home/user/.local/bin:$PATH"
12
- # Force Python to print immediately β€” critical for HF health checks
13
- ENV PYTHONUNBUFFERED=1
14
-
15
- WORKDIR /app
16
-
17
  COPY --chown=user requirements.txt .
18
  RUN pip install --no-cache-dir -r requirements.txt
19
-
 
20
  COPY --chown=user server.py .
21
  COPY --chown=user models/ ./models/
22
-
 
23
  EXPOSE 7860
24
-
25
  CMD ["python", "server.py"]
26
-
 
1
  FROM python:3.10-slim
2
+
3
+ # HF Spaces runs as a non-root user β€” this satisfies that requirement
4
  RUN useradd -m -u 1000 user
5
+ USER user
6
+ ENV PATH="/home/user/.local/bin:$PATH"
7
+
8
+ WORKDIR /app
9
+
10
+ # Install system deps needed by OpenCV
11
  USER root
12
  RUN apt-get update && apt-get install -y --no-install-recommends \
13
  libgl1 libglib2.0-0 libsm6 libxrender1 libxext6 \
14
  && rm -rf /var/lib/apt/lists/*
 
15
  USER user
16
+
17
+ # Install Python deps
 
 
 
 
18
  COPY --chown=user requirements.txt .
19
  RUN pip install --no-cache-dir -r requirements.txt
20
+
21
+ # Copy app source and model files
22
  COPY --chown=user server.py .
23
  COPY --chown=user models/ ./models/
24
+
25
+ # HF Spaces expects the app to listen on port 7860
26
  EXPOSE 7860
27
+
28
  CMD ["python", "server.py"]
 
models/fruit_scaler.pkl β†’ fruit_scaler.pkl RENAMED
@@ -1,3 +1,3 @@
1
  version https://git-lfs.github.com/spec/v1
2
- oid sha256:6d1cd1cb11554763b0f4faf6929a1f51f7f9df02a9922bc0a43a1733784fe59b
3
  size 1727
 
1
  version https://git-lfs.github.com/spec/v1
2
+ oid sha256:3bb0916407d9add3c50e3c21702469ceb4876d36396f98817fb8b611f15c2cc6
3
  size 1727
models/fruit_svm_model.pkl β†’ fruit_svm_model.pkl RENAMED
@@ -1,3 +1,3 @@
1
  version https://git-lfs.github.com/spec/v1
2
- oid sha256:1232a94b63165c7b28a015bcd4d590b680e3e66046631b13625497aec433111d
3
- size 1520567
 
1
  version https://git-lfs.github.com/spec/v1
2
+ oid sha256:ea2d01f044eda5da5d9c96545a0f455a9a08b39af321ecb355e37a0c6b29eb3a
3
+ size 1736471
server.py CHANGED
@@ -9,7 +9,7 @@ from flask import Flask, request, jsonify, render_template
9
 
10
  app = Flask(__name__)
11
 
12
- # ─── Konfigurasi Path Model (Gunakan Absolute Path agar aman) ────────────────
13
  BASE_DIR = os.path.dirname(os.path.abspath(__file__))
14
  MODEL_PATH = os.path.join(BASE_DIR, "models", "fruit_svm_model.pkl")
15
  SCALER_PATH = os.path.join(BASE_DIR, "models", "fruit_scaler.pkl")
@@ -55,92 +55,54 @@ def map_kaggle_label(raw_label):
55
  return f"{ft}_{rs}", ft, rs
56
 
57
 
58
- # ─── Feature extraction β€” exact copy dari notebook (GrabCut version) ─────────
59
  def extract_features_from_array(img_array, size=(128, 128)):
60
  img_resized = cv2.resize(img_array, size)
61
  gray = cv2.cvtColor(img_resized, cv2.COLOR_BGR2GRAY)
62
  blurred = cv2.GaussianBlur(gray, (5, 5), 0)
63
 
64
- # ── Segmentation: GrabCut (matches notebook) ──────────────────────────────
65
- h, w = blurred.shape[:2]
66
- margin = int(min(h, w) * 0.085)
67
- rect = (margin, margin, w - margin * 2, h - margin * 2)
68
- mask = np.zeros(blurred.shape[:2], np.uint8)
69
- bgd_model = np.zeros((1, 65), np.float64)
70
- fgd_model = np.zeros((1, 65), np.float64)
71
- cv2.grabCut(img_resized, mask, rect, bgd_model, fgd_model,
72
- iterCount=20, mode=cv2.GC_INIT_WITH_RECT)
73
- mask2 = np.where((mask == 2) | (mask == 0), 0, 255).astype('uint8')
74
-
75
- # ── Shape features ────────────────────────────────────────────────────────
76
- contours, _ = cv2.findContours(mask2, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
77
  aspect_ratio, extent = 0, 0
78
  if contours:
79
  c = max(contours, key=cv2.contourArea)
80
- x, y, bw, bh = cv2.boundingRect(c)
81
- aspect_ratio = float(bw) / bh if bh != 0 else 0
82
- area = cv2.contourArea(c)
83
- rect_area = bw * bh
84
- extent = float(area) / rect_area if rect_area != 0 else 0
85
 
86
- fp = mask2 > 0
87
 
88
- # ── HSV color features (6) ────────────────────────────────────────────────
89
  hsv = cv2.cvtColor(img_resized, cv2.COLOR_BGR2HSV)
90
  h_ch, s_ch, v_ch = cv2.split(hsv)
91
  hsv_feats = [
92
- np.mean(h_ch[fp]) if fp.any() else 0,
93
- np.mean(s_ch[fp]) if fp.any() else 0,
94
- np.mean(v_ch[fp]) if fp.any() else 0,
95
- np.std(h_ch[fp]) if fp.any() else 0,
96
- np.std(s_ch[fp]) if fp.any() else 0,
97
- np.std(v_ch[fp]) if fp.any() else 0,
98
  ]
99
 
100
- # ── LAB color features (5) ────────────────────────────────────────────────
101
  lab = cv2.cvtColor(img_resized, cv2.COLOR_BGR2LAB)
102
  l_ch, a_ch, b_ch = cv2.split(lab)
103
  lab_feats = [
104
- np.mean(l_ch[fp]) if fp.any() else 0,
105
- np.mean(a_ch[fp]) if fp.any() else 0,
106
- np.mean(b_ch[fp]) if fp.any() else 0,
107
- np.std(a_ch[fp]) if fp.any() else 0,
108
  np.std(b_ch[fp]) if fp.any() else 0,
109
  ]
110
 
111
- # ── Hue histogram 18 bins (18) ────────────────────────────────────────────
112
- # NOTE: uses mask2 (uint8 0/255) as the cv2.calcHist mask β€” matches notebook
113
- h_hist = cv2.calcHist([h_ch], [0], mask2, [18], [0, 180])
114
  h_hist = cv2.normalize(h_hist, h_hist).flatten().tolist()
115
 
116
- # ── GLCM texture (6) ──────────────────────────────────────────────────────
117
- # Crop to bounding box of mask, inpaint background pixels, then quantise.
118
- # This exactly replicates the notebook's GLCM pipeline.
119
- x, y, bw, bh = cv2.boundingRect(mask2)
120
- if bw > 0 and bh > 0:
121
- gray_crop = gray[y:y + bh, x:x + bw]
122
- mask_crop = mask2[y:y + bh, x:x + bw]
123
- masked_gray_raw = np.where(mask_crop > 0, gray_crop, 0).astype(np.uint8)
124
- inv_mask_crop = cv2.bitwise_not(mask_crop)
125
- if np.count_nonzero(inv_mask_crop) > 0:
126
- inpainted = cv2.inpaint(masked_gray_raw, inv_mask_crop,
127
- inpaintRadius=1, flags=cv2.INPAINT_TELEA)
128
- masked_gray = inpainted if inpainted is not None else masked_gray_raw
129
- else:
130
- masked_gray = masked_gray_raw
131
- else:
132
- # GrabCut returned empty mask β€” fall back to full grayscale
133
- masked_gray = gray
134
-
135
- masked_gray_q = (masked_gray // 8).astype(np.uint8)
136
- valid_pixels = masked_gray_q[masked_gray_q > 0]
137
- if valid_pixels.size < 100:
138
- # Fallback: use full unmasked grayscale
139
- glcm_input = (gray // 8).astype(np.uint8)
140
- else:
141
- glcm_input = masked_gray_q
142
-
143
- glcm = graycomatrix(glcm_input, distances=[1, 3, 5],
144
  angles=[0, np.pi/4, np.pi/2, 3*np.pi/4],
145
  levels=32, symmetric=True, normed=True)
146
  glcm_feats = [
@@ -148,23 +110,23 @@ def extract_features_from_array(img_array, size=(128, 128)):
148
  graycoprops(glcm, 'correlation').mean(),
149
  graycoprops(glcm, 'energy').mean(),
150
  graycoprops(glcm, 'homogeneity').mean(),
151
- graycoprops(glcm, 'dissimilarity').mean(),
152
- graycoprops(glcm, 'ASM').mean(),
153
  ]
154
 
155
- # ── LBP texture 10 bins (10) ──────────────────────────────────────────────
156
  lbp = local_binary_pattern(gray, P=8, R=1, method='uniform')
157
  lbp_pixels = lbp[fp] if fp.any() else lbp.ravel()
158
  lbp_hist, _ = np.histogram(lbp_pixels, bins=10, range=(0, 10), density=True)
159
 
160
  features = hsv_feats + lab_feats + h_hist + glcm_feats + lbp_hist.tolist() + [aspect_ratio, extent]
161
-
162
- # raw dict for frontend display
163
  raw = {
164
  'h_mean': hsv_feats[0], 's_mean': hsv_feats[1], 'v_mean': hsv_feats[2],
165
  'h_std': hsv_feats[3], 's_std': hsv_feats[4], 'v_std': hsv_feats[5],
166
  'contrast': glcm_feats[0], 'correlation': glcm_feats[1],
167
- 'energy': glcm_feats[2], 'homogeneity': glcm_feats[3],
168
  'aspect_ratio': aspect_ratio, 'extent': extent,
169
  }
170
  return features, raw
@@ -277,10 +239,10 @@ def predict():
277
  return jsonify({'error': str(e)}), 500
278
 
279
 
280
- # ─── Run ────────────────────────────────────────────────────────────────────
281
  if __name__ == '__main__':
282
  print("=" * 60)
283
  print("RIPE.AI β€” Flask API Server")
284
  print("=" * 60)
285
  print(f"Model loaded: {model_loaded}")
286
- app.run(host='0.0.0.0', port=7860, debug=False)
 
9
 
10
  app = Flask(__name__)
11
 
12
+ # ─── Konfigurasi Path Model (Gunakan Absolute Path agar aman) ─────────────────
13
  BASE_DIR = os.path.dirname(os.path.abspath(__file__))
14
  MODEL_PATH = os.path.join(BASE_DIR, "models", "fruit_svm_model.pkl")
15
  SCALER_PATH = os.path.join(BASE_DIR, "models", "fruit_scaler.pkl")
 
55
  return f"{ft}_{rs}", ft, rs
56
 
57
 
58
+ # ─── Feature extraction β€” exact copy dari Cell 2 notebook ────────────────────
59
  def extract_features_from_array(img_array, size=(128, 128)):
60
  img_resized = cv2.resize(img_array, size)
61
  gray = cv2.cvtColor(img_resized, cv2.COLOR_BGR2GRAY)
62
  blurred = cv2.GaussianBlur(gray, (5, 5), 0)
63
 
64
+ _, mask = cv2.threshold(blurred, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
65
+
66
+ contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
 
 
 
 
 
 
 
 
 
 
67
  aspect_ratio, extent = 0, 0
68
  if contours:
69
  c = max(contours, key=cv2.contourArea)
70
+ x, y, w, h = cv2.boundingRect(c)
71
+ aspect_ratio = float(w) / h if h != 0 else 0
72
+ area = cv2.contourArea(c)
73
+ rect_area = w * h
74
+ extent = float(area) / rect_area if rect_area != 0 else 0
75
 
76
+ fp = mask > 0
77
 
78
+ # HSV (6)
79
  hsv = cv2.cvtColor(img_resized, cv2.COLOR_BGR2HSV)
80
  h_ch, s_ch, v_ch = cv2.split(hsv)
81
  hsv_feats = [
82
+ np.mean(h_ch[fp]) if fp.any() else 0, np.mean(s_ch[fp]) if fp.any() else 0,
83
+ np.mean(v_ch[fp]) if fp.any() else 0, np.std(h_ch[fp]) if fp.any() else 0,
84
+ np.std(s_ch[fp]) if fp.any() else 0, np.std(v_ch[fp]) if fp.any() else 0,
 
 
 
85
  ]
86
 
87
+ # LAB (5) ← NEW
88
  lab = cv2.cvtColor(img_resized, cv2.COLOR_BGR2LAB)
89
  l_ch, a_ch, b_ch = cv2.split(lab)
90
  lab_feats = [
91
+ np.mean(l_ch[fp]) if fp.any() else 0, np.mean(a_ch[fp]) if fp.any() else 0,
92
+ np.mean(b_ch[fp]) if fp.any() else 0, np.std(a_ch[fp]) if fp.any() else 0,
 
 
93
  np.std(b_ch[fp]) if fp.any() else 0,
94
  ]
95
 
96
+ # Hue histogram 18 bins (18) ← NEW
97
+ h_hist = cv2.calcHist([h_ch], [0], mask, [18], [0, 180])
 
98
  h_hist = cv2.normalize(h_hist, h_hist).flatten().tolist()
99
 
100
+ # GLCM with distances=[1,3,5], 4 angles, 6 props (6) ← EXPANDED
101
+ # Quantise to 32 levels: reduces sparsity and speeds up computation.
102
+ # ⚠️ Must match the notebook β€” retrain if you change this value.
103
+ masked_gray = cv2.bitwise_and(gray, gray, mask=mask).astype(np.uint8)
104
+ masked_gray = (masked_gray // 8).astype(np.uint8) # 256 β†’ 32 levels
105
+ glcm = graycomatrix(masked_gray, distances=[1, 3, 5],
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
106
  angles=[0, np.pi/4, np.pi/2, 3*np.pi/4],
107
  levels=32, symmetric=True, normed=True)
108
  glcm_feats = [
 
110
  graycoprops(glcm, 'correlation').mean(),
111
  graycoprops(glcm, 'energy').mean(),
112
  graycoprops(glcm, 'homogeneity').mean(),
113
+ graycoprops(glcm, 'dissimilarity').mean(), # NEW
114
+ graycoprops(glcm, 'ASM').mean(), # NEW
115
  ]
116
 
117
+ # LBP histogram 10 bins (10) ← NEW
118
  lbp = local_binary_pattern(gray, P=8, R=1, method='uniform')
119
  lbp_pixels = lbp[fp] if fp.any() else lbp.ravel()
120
  lbp_hist, _ = np.histogram(lbp_pixels, bins=10, range=(0, 10), density=True)
121
 
122
  features = hsv_feats + lab_feats + h_hist + glcm_feats + lbp_hist.tolist() + [aspect_ratio, extent]
123
+
124
+ # raw dict for frontend display (keep the same keys you already use)
125
  raw = {
126
  'h_mean': hsv_feats[0], 's_mean': hsv_feats[1], 'v_mean': hsv_feats[2],
127
  'h_std': hsv_feats[3], 's_std': hsv_feats[4], 'v_std': hsv_feats[5],
128
  'contrast': glcm_feats[0], 'correlation': glcm_feats[1],
129
+ 'energy': glcm_feats[2], 'homogeneity': glcm_feats[3],
130
  'aspect_ratio': aspect_ratio, 'extent': extent,
131
  }
132
  return features, raw
 
239
  return jsonify({'error': str(e)}), 500
240
 
241
 
242
+ # ─── Run ─────────────────────────────────────────────────────────────────────
243
  if __name__ == '__main__':
244
  print("=" * 60)
245
  print("RIPE.AI β€” Flask API Server")
246
  print("=" * 60)
247
  print(f"Model loaded: {model_loaded}")
248
+ app.run(host='0.0.0.0', port=5000, debug=False)