morefaat69 commited on
Commit
7119cd3
·
verified ·
1 Parent(s): e634d51

Upload 4 files

Browse files
Files changed (4) hide show
  1. Banned_travelers22.csv +0 -0
  2. Dockerfile +35 -0
  3. main.py +271 -0
  4. requirements.txt +16 -0
Banned_travelers22.csv ADDED
The diff for this file is too large to render. See raw diff
 
Dockerfile ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ FROM python:3.10-slim
3
+
4
+ # ── System deps ──
5
+ RUN apt-get update && apt-get install -y --no-install-recommends \
6
+ libgl1 \
7
+ libglib2.0-0 \
8
+ libsm6 \
9
+ libxrender1 \
10
+ libxext6 \
11
+ && rm -rf /var/lib/apt/lists/*
12
+
13
+ # ── Non-root user required by Hugging Face Spaces ──
14
+ RUN useradd -m -u 1000 user
15
+ USER user
16
+
17
+ ENV HOME=/home/user \
18
+ PATH=/home/user/.local/bin:$PATH \
19
+ PYTHONUNBUFFERED=1
20
+
21
+ WORKDIR /home/user/app
22
+
23
+ # ── Install Python deps ──
24
+ COPY --chown=user requirements.txt ./
25
+ RUN pip install --no-cache-dir --upgrade pip \
26
+ && pip install --no-cache-dir -r requirements.txt
27
+
28
+ # Copy app source
29
+ COPY --chown=user . .
30
+
31
+ # Expose port (Hugging Face Spaces uses 7860) ──
32
+ EXPOSE 7860
33
+
34
+ # ── Start server ──
35
+ CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7860"]
main.py ADDED
@@ -0,0 +1,271 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import io
2
+ import os
3
+ import logging
4
+ import numpy as np
5
+ import pandas as pd
6
+ import cv2
7
+ from pathlib import Path
8
+ from contextlib import asynccontextmanager
9
+
10
+ import uvicorn
11
+ from fastapi import FastAPI, File, UploadFile, HTTPException
12
+ from fastapi.responses import JSONResponse
13
+ from fastapi.middleware.cors import CORSMiddleware
14
+ from PIL import Image
15
+ from sklearn.preprocessing import LabelEncoder
16
+ import tensorflow as tf
17
+
18
+ # ──────────────────────────────────────────────
19
+ # Logging
20
+ # ──────────────────────────────────────────────
21
+ logging.basicConfig(level=logging.INFO)
22
+ logger = logging.getLogger(__name__)
23
+
24
+ # ──────────────────────────────────────────────
25
+ # Paths (relative — works inside the Space)
26
+ # ──────────────────────────────────────────────
27
+ MODEL_PATH = os.getenv("MODEL_PATH", "IrisRecognizer95.h5")
28
+ CSV_PATH = os.getenv("CSV_PATH", "Banned_travelers22.csv")
29
+
30
+ # ──────────────────────────────────────────────
31
+ # Image config (must match training)
32
+ # ──────────────────────────────────────────────
33
+ IMG_HEIGHT = 150
34
+ IMG_WIDTH = 150
35
+ NUM_CHANNELS = 1
36
+
37
+ # ──────────────────────────────────────────────
38
+ # Globals (loaded once at startup)
39
+ # ──────────────────────────────────────────────
40
+ model: tf.keras.Model | None = None
41
+ banned_df: pd.DataFrame | None = None
42
+ label_encoder: LabelEncoder | None = None
43
+
44
+
45
+ # ──────────────────────────────────────────────
46
+ # Preprocessing helpers
47
+ # ──────────────────────────────────────────────
48
+ def resize_keep_aspect_ratio(
49
+ img: np.ndarray,
50
+ target_h: int = IMG_HEIGHT,
51
+ target_w: int = IMG_WIDTH,
52
+ pad_value: int = 255,
53
+ ) -> np.ndarray:
54
+ """Resize grayscale image while preserving aspect ratio (white padding)."""
55
+ aspect = img.shape[1] / img.shape[0]
56
+ if aspect > target_w / target_h:
57
+ new_w = target_w
58
+ new_h = int(target_w / aspect)
59
+ else:
60
+ new_h = target_h
61
+ new_w = int(target_h * aspect)
62
+
63
+ resized = cv2.resize(img, (new_w, new_h))
64
+ padded = np.full((target_h, target_w), pad_value, dtype=np.uint8)
65
+ x_off = (target_w - new_w) // 2
66
+ y_off = (target_h - new_h) // 2
67
+ padded[y_off:y_off + new_h, x_off:x_off + new_w] = resized
68
+ return padded
69
+
70
+
71
+ def preprocess_image_bytes(image_bytes: bytes) -> np.ndarray:
72
+ """
73
+ Convert raw image bytes → model-ready numpy array (1, 150, 150, 1).
74
+ Steps:
75
+ 1. Decode to grayscale
76
+ 2. Resize with aspect-ratio padding
77
+ 3. Normalise to [0, 1]
78
+ 4. Expand dims for batch + channel
79
+ """
80
+ # Decode via PIL (handles JPEG / PNG / BMP …)
81
+ pil_img = Image.open(io.BytesIO(image_bytes)).convert("L") # grayscale
82
+ img_np = np.array(pil_img, dtype=np.uint8)
83
+
84
+ img_resized = resize_keep_aspect_ratio(img_np)
85
+ img_norm = img_resized.astype(np.float32) / 255.0
86
+ img_expanded = img_norm.reshape(1, IMG_HEIGHT, IMG_WIDTH, NUM_CHANNELS)
87
+ return img_expanded
88
+
89
+
90
+ # ──────────────────────────────────────────────
91
+ # Startup / shutdown
92
+ # ──────────────────────────────────────────────
93
+ @asynccontextmanager
94
+ async def lifespan(app: FastAPI):
95
+ global model, banned_df, label_encoder
96
+
97
+ # ── Load model ──
98
+ if not Path(MODEL_PATH).exists():
99
+ logger.error(f"Model file not found: {MODEL_PATH}")
100
+ raise FileNotFoundError(f"Model not found: {MODEL_PATH}")
101
+
102
+ logger.info(f"Loading model from {MODEL_PATH} …")
103
+ model = tf.keras.models.load_model(MODEL_PATH)
104
+ logger.info("Model loaded ✓")
105
+
106
+ # ── Load banned-traveler CSV ──
107
+ if not Path(CSV_PATH).exists():
108
+ logger.error(f"CSV file not found: {CSV_PATH}")
109
+ raise FileNotFoundError(f"CSV not found: {CSV_PATH}")
110
+
111
+ logger.info(f"Loading banned-traveler list from {CSV_PATH} …")
112
+ banned_df = pd.read_csv(CSV_PATH)
113
+
114
+ # Normalise column names (strip spaces / lower)
115
+ banned_df.columns = banned_df.columns.str.strip()
116
+
117
+ required_cols = {"Label", "person_id", "Status"}
118
+ missing = required_cols - set(banned_df.columns)
119
+ if missing:
120
+ raise ValueError(f"CSV is missing columns: {missing}")
121
+
122
+ banned_df["Label"] = banned_df["Label"].astype(str).str.strip()
123
+ banned_df["person_id"] = banned_df["person_id"].astype(str).str.strip()
124
+ banned_df["Status"] = banned_df["Status"].astype(str).str.strip()
125
+
126
+ # ── Build LabelEncoder from CSV labels (same as training) ──
127
+ label_encoder = LabelEncoder()
128
+ label_encoder.fit(banned_df["Label"].unique())
129
+ logger.info(
130
+ f"LabelEncoder fitted on {len(label_encoder.classes_)} classes ✓"
131
+ )
132
+ logger.info("Startup complete — API ready.")
133
+
134
+ yield # ── app is running ──
135
+
136
+ logger.info("Shutting down …")
137
+
138
+
139
+ # ──────────────────────────────────────────────
140
+ # FastAPI app
141
+ # ──────────────────────────────────────────────
142
+ app = FastAPI(
143
+ title="Iris Recognition — Banned Traveler Detection",
144
+ description=(
145
+ "Upload an iris image and the API will tell you "
146
+ "whether the person is banned from travelling or not."
147
+ ),
148
+ version="1.0.0",
149
+ lifespan=lifespan,
150
+ )
151
+
152
+ app.add_middleware(
153
+ CORSMiddleware,
154
+ allow_origins=["*"],
155
+ allow_methods=["*"],
156
+ allow_headers=["*"],
157
+ )
158
+
159
+
160
+ # ──────────────────────────────────────────────
161
+ # Routes
162
+ # ──────────────────────────────────────────────
163
+ @app.get("/", tags=["Health"])
164
+ def root():
165
+ return {
166
+ "message": "Iris Recognition API is running 🚀",
167
+ "endpoints": {
168
+ "predict": "/predict [POST] — upload iris image",
169
+ "health": "/health [GET] — service status",
170
+ "docs": "/docs [GET] — Swagger UI",
171
+ },
172
+ }
173
+
174
+
175
+ @app.get("/health", tags=["Health"])
176
+ def health():
177
+ return {
178
+ "status": "ok",
179
+ "model_loaded": model is not None,
180
+ "csv_loaded": banned_df is not None,
181
+ "num_classes": int(len(label_encoder.classes_)) if label_encoder else 0,
182
+ "banned_records": int(len(banned_df)) if banned_df is not None else 0,
183
+ }
184
+
185
+
186
+ @app.post("/predict", tags=["Prediction"])
187
+ async def predict(file: UploadFile = File(..., description="Iris image (JPEG/PNG)")):
188
+ """
189
+ Upload an iris image → returns:
190
+ - `person_id` : predicted person identifier
191
+ - `predicted_label`: predicted label (e.g. '437-R')
192
+ - `status` : 'Banned' | 'Allowed' | 'Unknown'
193
+ - `confidence` : model confidence score [0-1]
194
+ - `is_banned` : boolean
195
+ """
196
+ # ── Validate content type ──
197
+ if file.content_type not in ("image/jpeg", "image/png", "image/jpg", "image/bmp"):
198
+ raise HTTPException(
199
+ status_code=415,
200
+ detail=f"Unsupported image format: {file.content_type}. Use JPEG or PNG.",
201
+ )
202
+
203
+ # ── Read file bytes ──
204
+ image_bytes = await file.read()
205
+ if len(image_bytes) == 0:
206
+ raise HTTPException(status_code=400, detail="Empty file uploaded.")
207
+
208
+ # ── Preprocess ──
209
+ try:
210
+ img_array = preprocess_image_bytes(image_bytes)
211
+ except Exception as e:
212
+ logger.error(f"Image preprocessing failed: {e}")
213
+ raise HTTPException(status_code=422, detail=f"Could not process image: {str(e)}")
214
+
215
+ # ── Inference ──
216
+ try:
217
+ probabilities = model.predict(img_array, verbose=0) # (1, 2000)
218
+ pred_class_idx = int(np.argmax(probabilities[0]))
219
+ confidence = float(np.max(probabilities[0]))
220
+ predicted_label = str(label_encoder.classes_[pred_class_idx])
221
+ except Exception as e:
222
+ logger.error(f"Model inference failed: {e}")
223
+ raise HTTPException(status_code=500, detail=f"Model inference error: {str(e)}")
224
+
225
+ # ── Lookup in banned CSV ──
226
+ try:
227
+ match = banned_df[banned_df["Label"] == predicted_label]
228
+
229
+ if not match.empty:
230
+ row = match.iloc[0]
231
+ person_id = str(row["person_id"])
232
+ status = str(row["Status"])
233
+ else:
234
+ # Label predicted but not in CSV → report as unknown
235
+ person_id = predicted_label.split("-")[0]
236
+ status = "Unknown"
237
+
238
+ is_banned = status.lower() == "banned"
239
+
240
+ except Exception as e:
241
+ logger.error(f"CSV lookup failed: {e}")
242
+ raise HTTPException(status_code=500, detail=f"Database lookup error: {str(e)}")
243
+
244
+ logger.info(
245
+ f"[PREDICT] label={predicted_label} | person_id={person_id} "
246
+ f"| status={status} | confidence={confidence:.4f}"
247
+ )
248
+
249
+ return JSONResponse(
250
+ content={
251
+ "person_id": person_id,
252
+ "predicted_label": predicted_label,
253
+ "status": status,
254
+ "confidence": round(confidence, 4),
255
+ "is_banned": is_banned,
256
+ "message": (
257
+ f"⚠️ BANNED — Person {person_id} is NOT allowed to travel."
258
+ if is_banned
259
+ else f"✅ ALLOWED — Person {person_id} is cleared to travel."
260
+ if status == "Allowed"
261
+ else f"❓ UNKNOWN — Person {person_id} not found in records."
262
+ ),
263
+ }
264
+ )
265
+
266
+
267
+ # ──────────────────────────────────────────────
268
+ # Run locally
269
+ # ──────────────────────────────────────────────
270
+ if __name__ == "__main__":
271
+ uvicorn.run("main:app", host="0.0.0.0", port=7860, reload=False)
requirements.txt ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Web framework
2
+ fastapi==0.111.0
3
+ uvicorn[standard]==0.30.1
4
+ python-multipart==0.0.9
5
+
6
+ # Deep Learning
7
+ tensorflow==2.15.0
8
+
9
+ # Image processing
10
+ Pillow==10.3.0
11
+ opencv-python-headless==4.9.0.80
12
+
13
+ # Data
14
+ numpy==1.26.4
15
+ pandas==2.2.2
16
+ scikit-learn==1.5.0