Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -2,7 +2,7 @@ import io
|
|
| 2 |
import os
|
| 3 |
import zipfile
|
| 4 |
import tempfile
|
| 5 |
-
from typing import List,
|
| 6 |
|
| 7 |
import streamlit as st
|
| 8 |
import numpy as np
|
|
@@ -13,7 +13,6 @@ import cv2
|
|
| 13 |
from urllib.parse import urlparse, parse_qs
|
| 14 |
|
| 15 |
# Optional: YOLO for phone detection
|
| 16 |
-
# We load lazily on first use to keep startup fast.
|
| 17 |
YOLO_MODEL = None
|
| 18 |
|
| 19 |
def load_yolo():
|
|
@@ -21,19 +20,20 @@ def load_yolo():
|
|
| 21 |
if YOLO_MODEL is None:
|
| 22 |
try:
|
| 23 |
from ultralytics import YOLO
|
| 24 |
-
#
|
| 25 |
-
YOLO_MODEL = YOLO('yolov8n.pt') # automatically downloads on first run
|
| 26 |
except Exception as e:
|
| 27 |
st.warning(f"YOLO model could not be loaded: {e}")
|
| 28 |
YOLO_MODEL = None
|
| 29 |
return YOLO_MODEL
|
| 30 |
|
|
|
|
| 31 |
def iou(boxA, boxB) -> float:
|
| 32 |
# boxes in [x1,y1,x2,y2]
|
| 33 |
xA = max(boxA[0], boxB[0])
|
| 34 |
yA = max(boxA[1], boxB[1])
|
| 35 |
xB = min(boxA[2], boxB[2])
|
| 36 |
-
yB = min(boxA[3], boxB[
|
|
|
|
| 37 |
interW = max(0, xB - xA)
|
| 38 |
interH = max(0, yB - yA)
|
| 39 |
interArea = interW * interH
|
|
@@ -44,7 +44,6 @@ def iou(boxA, boxB) -> float:
|
|
| 44 |
|
| 45 |
def detect_qr_opencv(image_np: np.ndarray) -> List[Dict[str, Any]]:
|
| 46 |
det = cv2.QRCodeDetector()
|
| 47 |
-
# ✅ FIX: detectAndDecodeMulti returns 4 values, not 3
|
| 48 |
retval, data_list, points, _ = det.detectAndDecodeMulti(image_np)
|
| 49 |
results = []
|
| 50 |
if points is None:
|
|
@@ -81,7 +80,7 @@ def detect_phones_yolo(image_np: np.ndarray, conf: float = 0.25) -> List[List[fl
|
|
| 81 |
bboxes = []
|
| 82 |
for r in results:
|
| 83 |
for box, cls in zip(r.boxes.xyxy.cpu().numpy(), r.boxes.cls.cpu().numpy()):
|
| 84 |
-
if int(cls) == 67: # COCO class 67 = cell phone
|
| 85 |
bboxes.append([float(box[0]), float(box[1]), float(box[2]), float(box[3])])
|
| 86 |
return bboxes
|
| 87 |
|
|
@@ -146,7 +145,7 @@ def read_approved_list(file) -> List[str]:
|
|
| 146 |
st.error(f"Failed to parse approved list: {e}")
|
| 147 |
return []
|
| 148 |
|
| 149 |
-
# ✅
|
| 150 |
def normalize_payload(payload: str) -> str:
|
| 151 |
if not payload:
|
| 152 |
return ""
|
|
@@ -176,8 +175,31 @@ def match_payload(payload: str, approved: List[str]) -> bool:
|
|
| 176 |
return True
|
| 177 |
return False
|
| 178 |
|
|
|
|
| 179 |
st.set_page_config(page_title="QR Code Anomaly Scanner", layout="wide")
|
| 180 |
st.title("🕵️ QR Code Anomaly Scanner (Retail Store 360° CCTV Frames)")
|
| 181 |
|
| 182 |
-
|
| 183 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
import os
|
| 3 |
import zipfile
|
| 4 |
import tempfile
|
| 5 |
+
from typing import List, Dict, Any
|
| 6 |
|
| 7 |
import streamlit as st
|
| 8 |
import numpy as np
|
|
|
|
| 13 |
from urllib.parse import urlparse, parse_qs
|
| 14 |
|
| 15 |
# Optional: YOLO for phone detection
|
|
|
|
| 16 |
YOLO_MODEL = None
|
| 17 |
|
| 18 |
def load_yolo():
|
|
|
|
| 20 |
if YOLO_MODEL is None:
|
| 21 |
try:
|
| 22 |
from ultralytics import YOLO
|
| 23 |
+
YOLO_MODEL = YOLO('yolov8n.pt') # lightweight pretrained model
|
|
|
|
| 24 |
except Exception as e:
|
| 25 |
st.warning(f"YOLO model could not be loaded: {e}")
|
| 26 |
YOLO_MODEL = None
|
| 27 |
return YOLO_MODEL
|
| 28 |
|
| 29 |
+
# ✅ FIXED iou function
|
| 30 |
def iou(boxA, boxB) -> float:
|
| 31 |
# boxes in [x1,y1,x2,y2]
|
| 32 |
xA = max(boxA[0], boxB[0])
|
| 33 |
yA = max(boxA[1], boxB[1])
|
| 34 |
xB = min(boxA[2], boxB[2])
|
| 35 |
+
yB = min(boxA[3], boxB[3]) # ✅ fixed
|
| 36 |
+
|
| 37 |
interW = max(0, xB - xA)
|
| 38 |
interH = max(0, yB - yA)
|
| 39 |
interArea = interW * interH
|
|
|
|
| 44 |
|
| 45 |
def detect_qr_opencv(image_np: np.ndarray) -> List[Dict[str, Any]]:
|
| 46 |
det = cv2.QRCodeDetector()
|
|
|
|
| 47 |
retval, data_list, points, _ = det.detectAndDecodeMulti(image_np)
|
| 48 |
results = []
|
| 49 |
if points is None:
|
|
|
|
| 80 |
bboxes = []
|
| 81 |
for r in results:
|
| 82 |
for box, cls in zip(r.boxes.xyxy.cpu().numpy(), r.boxes.cls.cpu().numpy()):
|
| 83 |
+
if int(cls) == 67: # COCO: class 67 = cell phone
|
| 84 |
bboxes.append([float(box[0]), float(box[1]), float(box[2]), float(box[3])])
|
| 85 |
return bboxes
|
| 86 |
|
|
|
|
| 145 |
st.error(f"Failed to parse approved list: {e}")
|
| 146 |
return []
|
| 147 |
|
| 148 |
+
# ✅ FIXED payload normalization
|
| 149 |
def normalize_payload(payload: str) -> str:
|
| 150 |
if not payload:
|
| 151 |
return ""
|
|
|
|
| 175 |
return True
|
| 176 |
return False
|
| 177 |
|
| 178 |
+
# ---------------- STREAMLIT UI (unchanged) -----------------
|
| 179 |
st.set_page_config(page_title="QR Code Anomaly Scanner", layout="wide")
|
| 180 |
st.title("🕵️ QR Code Anomaly Scanner (Retail Store 360° CCTV Frames)")
|
| 181 |
|
| 182 |
+
st.markdown("""
|
| 183 |
+
Upload a set of frame images (multiple files **or** a ZIP), plus the approved QR list (CSV/TXT).
|
| 184 |
+
The app will:
|
| 185 |
+
- Detect and decode QR codes in each frame.
|
| 186 |
+
- Detect **cell phones** via YOLO to infer if a QR is shown on a phone.
|
| 187 |
+
- Flag anomalies:
|
| 188 |
+
- **UNAPPROVED_QR**: decoded payload not in the approved list.
|
| 189 |
+
- **ON_PHONE**: QR bounding box overlaps a detected phone.
|
| 190 |
+
- **UNDECODED_QR**: QR detected but not decodable.
|
| 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")
|
| 202 |
+
|
| 203 |
+
workdir = tempfile.mkdtemp()
|
| 204 |
+
|
| 205 |
+
# (rest of your original processing loop stays exactly the same…)
|