SuriRaja commited on
Commit
032e63d
Β·
verified Β·
1 Parent(s): 3e09b48

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +60 -58
app.py CHANGED
@@ -8,7 +8,6 @@ import streamlit as st
8
  import numpy as np
9
  import pandas as pd
10
  from PIL import Image, ImageDraw, ImageFont
11
-
12
  import cv2
13
 
14
  # Optional: YOLO for phone detection
@@ -40,7 +39,6 @@ def iou(boxA, boxB) -> float:
40
 
41
  def detect_qr_opencv(image_np: np.ndarray) -> List[Dict[str, Any]]:
42
  det = cv2.QRCodeDetector()
43
- # βœ… Correct unpack
44
  retval, data_list, points, _ = det.detectAndDecodeMulti(image_np)
45
  results = []
46
  if points is None:
@@ -56,11 +54,7 @@ def detect_qr_opencv(image_np: np.ndarray) -> List[Dict[str, Any]]:
56
  })
57
  return results
58
 
59
- if isinstance(data_list, (list, tuple)):
60
- decoded_list = data_list
61
- else:
62
- decoded_list = [data_list] * len(points)
63
-
64
  for i, quad in enumerate(points):
65
  pts = np.array(quad, dtype=np.float32).reshape(-1,2)
66
  x1, y1 = np.min(pts[:,0]), np.min(pts[:,1])
@@ -141,40 +135,36 @@ def read_approved_list(file) -> List[str]:
141
  vals = df.iloc[:,0].dropna().astype(str).tolist()
142
  else:
143
  content = file.read().decode("utf-8", errors="ignore")
144
- file.seek(0) # βœ… reset pointer so file can be reused
145
  vals = [line.strip() for line in content.splitlines() if line.strip()]
146
  return [v.strip() for v in vals if v.strip()]
147
  except Exception as e:
148
  st.error(f"Failed to parse approved list: {e}")
149
  return []
150
 
151
- # πŸ”Ή FIX START: robust normalization + exact matching
 
152
  def normalize_payload(payload: str) -> str:
153
  if not payload:
154
  return ""
155
  p = payload.strip().lower()
156
-
157
  if p.startswith("upi://"):
158
  try:
159
- from urllib.parse import urlparse, parse_qs
160
  parsed = urlparse(p)
161
  qs = parse_qs(parsed.query)
162
  if "pa" in qs:
163
  return qs["pa"][0].strip().lower()
164
  except Exception:
165
  pass
166
-
167
  if "pa=" in p:
168
  try:
169
  part = p.split("pa=")[1].split("&")[0]
170
  return part.strip().lower()
171
  except Exception:
172
  pass
173
-
174
  for prefix in ["upi://", "http://", "https://"]:
175
  if p.startswith(prefix):
176
  p = p[len(prefix):]
177
-
178
  return p
179
 
180
  def match_payload(payload: str, approved: List[str]) -> bool:
@@ -186,32 +176,29 @@ def match_payload(payload: str, approved: List[str]) -> bool:
186
  if norm_payload == norm_a:
187
  return True
188
  return False
189
- # πŸ”Ή FIX END
190
-
191
- st.set_page_config(page_title="QR Code Anomaly Scanner", layout="wide")
192
- st.title("πŸ•΅οΈ QR Code Anomaly Scanner (Retail Store 360Β° CCTV Frames)")
193
-
194
- st.markdown("""
195
- Upload a set of frame images (multiple files **or** a ZIP), plus the approved QR list (CSV/TXT).
196
- The app will:
197
- - Detect and decode QR codes in each frame.
198
- - Detect **cell phones** via YOLO to infer if a QR is shown on a phone.
199
- - Flag anomalies:
200
- - **UNAPPROVED_QR**: decoded payload not in the approved list.
201
- - **ON_PHONE**: QR bounding box overlaps a detected phone.
202
- - **UNDECODED_QR**: QR detected but not decodable.
203
- Download the annotated images and a consolidated CSV report at the end.
204
- """)
205
 
206
  with st.sidebar:
207
- st.header("Inputs")
208
- approved_file = st.file_uploader("Approved QR List (CSV/TXT)", type=["csv","txt"])
209
- frames = st.file_uploader("Frames (images)", type=["jpg","jpeg","png","bmp","webp"], accept_multiple_files=True)
210
- frames_zip = st.file_uploader("Or upload a ZIP of frames", type=["zip"])
211
- run_phone_detection = st.checkbox("Detect phones (YOLO)", value=True)
212
- phone_conf = st.slider("Phone detection confidence", 0.1, 0.8, 0.25, 0.05)
213
- iou_threshold = st.slider("QR–Phone overlap IoU threshold", 0.05, 0.8, 0.2, 0.05)
214
- process_btn = st.button("Run Scan")
215
 
216
  workdir = tempfile.mkdtemp()
217
 
@@ -224,7 +211,7 @@ if process_btn:
224
  if not approved_list:
225
  st.warning("Approved list is empty or failed to parse. All decoded QR payloads will be treated as UNAPPROVED.")
226
  else:
227
- st.success(f"Loaded {len(approved_list)} approved entries.")
228
 
229
  img_paths = []
230
  for f in frames or []:
@@ -305,28 +292,49 @@ if process_btn:
305
 
306
  progress.progress((idx+1)/len(img_paths))
307
 
308
- status.text("Completed.")
309
  df = pd.DataFrame(rows)
310
 
311
- st.subheader("Results")
312
- st.dataframe(df, use_container_width=True)
313
-
314
- st.markdown("### Summary")
315
  total_frames = len(img_paths)
316
  total_qr = int((df["qr_index"] >= 0).sum())
317
  unapproved = int((df["anomalies"].str.contains("UNAPPROVED_QR", na=False)).sum())
318
  on_phone = int((df["anomalies"].str.contains("ON_PHONE", na=False)).sum())
319
  undecoded = int((df["anomalies"].str.contains("UNDECODED_QR", na=False)).sum())
320
  no_qr = int((df["anomalies"] == "NO_QR_FOUND").sum())
321
- st.write({
322
- "frames_processed": total_frames,
323
- "qr_detections": total_qr,
324
- "unapproved_qr": unapproved,
325
- "qr_on_phone": on_phone,
326
- "undecoded_qr": undecoded,
327
- "frames_with_no_qr": no_qr
328
- })
329
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
330
  csv_bytes = df.to_csv(index=False).encode("utf-8")
331
  st.download_button("⬇️ Download CSV Report", data=csv_bytes,
332
  file_name="qr_anomaly_report.csv", mime="text/csv")
@@ -341,9 +349,3 @@ if process_btn:
341
 
342
  else:
343
  st.info("Upload inputs on the left and click **Run Scan** to begin.")
344
- st.markdown("""
345
- **Tips**
346
- - Approved list can be **TXT** (one payload per line) or **CSV** (use a `payload` column or first column).
347
- - For QR-on-phone detection, keep **Detect phones (YOLO)** enabled.
348
- - Name frames with timestamps to correlate events later.
349
- """)
 
8
  import numpy as np
9
  import pandas as pd
10
  from PIL import Image, ImageDraw, ImageFont
 
11
  import cv2
12
 
13
  # Optional: YOLO for phone detection
 
39
 
40
  def detect_qr_opencv(image_np: np.ndarray) -> List[Dict[str, Any]]:
41
  det = cv2.QRCodeDetector()
 
42
  retval, data_list, points, _ = det.detectAndDecodeMulti(image_np)
43
  results = []
44
  if points is None:
 
54
  })
55
  return results
56
 
57
+ decoded_list = data_list if isinstance(data_list, (list, tuple)) else [data_list] * len(points)
 
 
 
 
58
  for i, quad in enumerate(points):
59
  pts = np.array(quad, dtype=np.float32).reshape(-1,2)
60
  x1, y1 = np.min(pts[:,0]), np.min(pts[:,1])
 
135
  vals = df.iloc[:,0].dropna().astype(str).tolist()
136
  else:
137
  content = file.read().decode("utf-8", errors="ignore")
138
+ file.seek(0) # reset pointer
139
  vals = [line.strip() for line in content.splitlines() if line.strip()]
140
  return [v.strip() for v in vals if v.strip()]
141
  except Exception as e:
142
  st.error(f"Failed to parse approved list: {e}")
143
  return []
144
 
145
+ # --- FIXED match with normalization ---
146
+ from urllib.parse import urlparse, parse_qs
147
  def normalize_payload(payload: str) -> str:
148
  if not payload:
149
  return ""
150
  p = payload.strip().lower()
 
151
  if p.startswith("upi://"):
152
  try:
 
153
  parsed = urlparse(p)
154
  qs = parse_qs(parsed.query)
155
  if "pa" in qs:
156
  return qs["pa"][0].strip().lower()
157
  except Exception:
158
  pass
 
159
  if "pa=" in p:
160
  try:
161
  part = p.split("pa=")[1].split("&")[0]
162
  return part.strip().lower()
163
  except Exception:
164
  pass
 
165
  for prefix in ["upi://", "http://", "https://"]:
166
  if p.startswith(prefix):
167
  p = p[len(prefix):]
 
168
  return p
169
 
170
  def match_payload(payload: str, approved: List[str]) -> bool:
 
176
  if norm_payload == norm_a:
177
  return True
178
  return False
179
+
180
+ # --- UI ---
181
+ st.set_page_config(page_title="QR Code Anomaly Scanner", page_icon="πŸ•΅οΈ", layout="wide")
182
+
183
+ st.markdown(
184
+ """
185
+ <div style="background-color:#4B8BBE;padding:15px;border-radius:10px;margin-bottom:20px;">
186
+ <h1 style="color:white;text-align:center;">πŸ•΅οΈ QR Code Anomaly Scanner</h1>
187
+ <p style="color:white;text-align:center;">AI-Powered Surveillance for Retail Store 360Β° CCTV Frames</p>
188
+ </div>
189
+ """,
190
+ unsafe_allow_html=True
191
+ )
 
 
 
192
 
193
  with st.sidebar:
194
+ st.header("βš™οΈ Inputs")
195
+ approved_file = st.file_uploader("πŸ“‘ Approved QR List (CSV/TXT)", type=["csv","txt"])
196
+ frames = st.file_uploader("πŸ–ΌοΈ Frames (images)", type=["jpg","jpeg","png","bmp","webp"], accept_multiple_files=True)
197
+ frames_zip = st.file_uploader("πŸ“¦ Or upload a ZIP of frames", type=["zip"])
198
+ run_phone_detection = st.checkbox("πŸ“± Detect phones (YOLO)", value=True)
199
+ phone_conf = st.slider("πŸ“ Phone detection confidence", 0.1, 0.8, 0.25, 0.05)
200
+ iou_threshold = st.slider("🎯 QR–Phone overlap IoU threshold", 0.05, 0.8, 0.2, 0.05)
201
+ process_btn = st.button("πŸš€ Run Scan", use_container_width=True)
202
 
203
  workdir = tempfile.mkdtemp()
204
 
 
211
  if not approved_list:
212
  st.warning("Approved list is empty or failed to parse. All decoded QR payloads will be treated as UNAPPROVED.")
213
  else:
214
+ st.success(f"βœ… Loaded {len(approved_list)} approved entries.")
215
 
216
  img_paths = []
217
  for f in frames or []:
 
292
 
293
  progress.progress((idx+1)/len(img_paths))
294
 
295
+ status.text("βœ… Completed.")
296
  df = pd.DataFrame(rows)
297
 
298
+ # --- Summary metrics ---
299
+ st.markdown("### πŸ“Š Summary Dashboard")
300
+ col1, col2, col3, col4, col5, col6 = st.columns(6)
 
301
  total_frames = len(img_paths)
302
  total_qr = int((df["qr_index"] >= 0).sum())
303
  unapproved = int((df["anomalies"].str.contains("UNAPPROVED_QR", na=False)).sum())
304
  on_phone = int((df["anomalies"].str.contains("ON_PHONE", na=False)).sum())
305
  undecoded = int((df["anomalies"].str.contains("UNDECODED_QR", na=False)).sum())
306
  no_qr = int((df["anomalies"] == "NO_QR_FOUND").sum())
 
 
 
 
 
 
 
 
307
 
308
+ col1.metric("Frames Processed", total_frames)
309
+ col2.metric("QR Detections", total_qr)
310
+ col3.metric("❌ Unapproved", unapproved)
311
+ col4.metric("πŸ“± On Phone", on_phone)
312
+ col5.metric("⚠️ Undecoded", undecoded)
313
+ col6.metric("🚫 No QR Found", no_qr)
314
+
315
+ # --- Results Table ---
316
+ st.markdown("### πŸ“ Detailed Results")
317
+ def highlight_anomalies(val):
318
+ if "UNAPPROVED" in str(val):
319
+ return "background-color: #FFB6C1; color: black;"
320
+ elif "ON_PHONE" in str(val):
321
+ return "background-color: #FFD580; color: black;"
322
+ elif "UNDECODED" in str(val):
323
+ return "background-color: #B0C4DE; color: black;"
324
+ elif "NO_QR_FOUND" in str(val):
325
+ return "background-color: #D3D3D3; color: black;"
326
+ return ""
327
+
328
+ st.dataframe(df.style.applymap(highlight_anomalies, subset=["anomalies"]), use_container_width=True)
329
+
330
+ # --- Image Gallery ---
331
+ st.markdown("### πŸ–ΌοΈ Annotated Frames Preview")
332
+ cols = st.columns(3)
333
+ for idx, fname in enumerate(sorted(os.listdir(annotated_dir))):
334
+ with cols[idx % 3]:
335
+ st.image(os.path.join(annotated_dir, fname), caption=fname, use_container_width=True)
336
+
337
+ # --- Downloads ---
338
  csv_bytes = df.to_csv(index=False).encode("utf-8")
339
  st.download_button("⬇️ Download CSV Report", data=csv_bytes,
340
  file_name="qr_anomaly_report.csv", mime="text/csv")
 
349
 
350
  else:
351
  st.info("Upload inputs on the left and click **Run Scan** to begin.")