ibdetect / app.py
hansaka1's picture
Update app.py
14ce970 verified
Raw
History Blame Contribute Delete
6.83 kB
import gradio as gr
import easyocr
import cv2
import numpy as np
from PIL import Image
# Initialize Deep Learning OCR once
reader = easyocr.Reader(['en'])
# Helper to calculate Intersection over Union (Overlap)
def calculate_iou(boxA, boxB):
# box = [x_left, y_top, x_right, y_bottom]
xA = max(boxA[0], boxB[0])
yA = max(boxA[1], boxB[1])
xB = min(boxA[2], boxB[2])
yB = min(boxA[3], boxB[3])
interArea = max(0, xB - xA) * max(0, yB - yA)
if interArea == 0: return 0.0
boxAArea = (boxA[2] - boxA[0]) * (boxA[3] - boxA[1])
boxBArea = (boxB[2] - boxB[0]) * (boxB[3] - boxB[1])
iou = interArea / float(boxAArea + boxBArea - interArea)
return iou
def validate_whatsapp_hybrid(image):
if image is None:
return {"valid": False, "reason": "No image provided"}
image_np = np.array(image.convert('RGB'))
height, width, _ = image_np.shape
center_x = width / 2
debug_log = []
# --- PHASE 1: DEEP OCR & HEADER BARRIER ---
try:
# detail=1 returns [ [[tl,tr,br,bl]], text, conf ]
ocr_results = reader.readtext(image_np, detail=1, paragraph=False)
except Exception as e:
return {"valid": False, "error": f"OCR Failed: {str(e)}"}
header_keywords = ["block", "add to contacts", "report", "business account",
"joined in", "not a contact", "encrypted", "end-to-end"]
safe_chat_start_y = 0
# Find the lowest header element
for (bbox, text, conf) in ocr_results:
if conf < 0.3: continue
bottom_y = bbox[2][1]
if any(k in text.lower() for k in header_keywords):
if bottom_y > safe_chat_start_y:
safe_chat_start_y = bottom_y
# Add buffer or set default
if safe_chat_start_y > 0:
safe_chat_start_y += 30
debug_log.append(f"Header barrier set at Y={int(safe_chat_start_y)}")
else:
safe_chat_start_y = height * 0.18
debug_log.append(f"No header keywords. Using default barrier Y={int(safe_chat_start_y)}")
# --- PHASE 2: OPENCV STICKER CANDIDATES ---
# Convert to grayscale
gray = cv2.cvtColor(image_np, cv2.COLOR_RGB2GRAY)
# Blur slightly to remove image noise
blurred = cv2.GaussianBlur(gray, (5, 5), 0)
# Adaptive Threshold (handles different background brightness)
thresh = cv2.adaptiveThreshold(blurred, 255, cv2.ADAPTIVE_THRESH_MEAN_C, cv2.THRESH_BINARY_INV, 25, 15)
# Find contours (blobs)
contours, _ = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
sticker_candidates = []
min_sticker_size = width * 0.12 # Sticker must be at least 12% of screen width
max_sticker_size = width * 0.60
for cnt in contours:
x, y, w, h = cv2.boundingRect(cnt)
# Filter A: Must be below header
if y < safe_chat_start_y: continue
# Filter B: Size criteria (Stickers aren't tiny dots or massive screens)
if w < min_sticker_size or h < min_sticker_size: continue
if w > max_sticker_size or h > max_sticker_size: continue
# Filter C: Aspect Ratio (Stickers are roughly square-ish, 0.6 to 1.6 ratio)
aspect = w / float(h)
if not (0.6 < aspect < 1.6): continue
# This is a potential sticker or a text bubble
sticker_candidates.append([x, y, x+w, y+h]) # Format: [left, top, right, bottom]
# --- PHASE 3: DE-DUPLICATION (Separating Text vs. Stickers) ---
final_objects = []
# 3a. Process OCR Text Objects
ocr_boxes_formatted = []
system_words = ["today", "yesterday", "messages", "calls", "unread"]
for (bbox, text, conf) in ocr_results:
top_y = bbox[0][1]
if top_y < safe_chat_start_y: continue
if conf < 0.4 or len(text) < 2: continue
# Center check for system dates
text_center = bbox[0][0] + ((bbox[1][0] - bbox[0][0]) / 2)
if abs(text_center - center_x) < (width * 0.12): continue
if any(s in text.lower() for s in system_words): continue
# Format: [left, top, right, bottom]
l, t, r, b = bbox[0][0], bbox[0][1], bbox[2][0], bbox[2][1]
ocr_boxes_formatted.append([l, t, r, b])
final_objects.append({
'type': 'text', 'text_content': text, 'left': l, 'top': t,
'center': l + (r-l)/2
})
# 3b. Process Sticker Candidates against Text Boxes
for s_box in sticker_candidates:
is_actually_text = False
# Check if this sticker candidate overlaps significantly with any text box
for t_box in ocr_boxes_formatted:
# If IoU > 15%, assume this blob is just the container for the text
if calculate_iou(s_box, t_box) > 0.15:
is_actually_text = True
break
if not is_actually_text:
# It's a blob with no text inside! Probably a sticker.
l, t, r, b = s_box
final_objects.append({
'type': 'sticker', 'text_content': 'STICKER/IMAGE', 'left': l, 'top': t,
'center': l + (r-l)/2
})
debug_log.append(f"Found Sticker Candidate at Y={t}, X={l}")
# --- PHASE 4: UNIFIED DECISION LOOP ---
# Sort everything by top Y position
final_objects.sort(key=lambda x: x['top'])
final_decision = None
stranger_limit = width * 0.20
me_limit = width * 0.40
for obj in final_objects:
# Double-check center alignment (redundant safety check)
if abs(obj['center'] - center_x) < (width * 0.10):
debug_log.append(f"Skipped Centered {obj['type']}")
continue
# --- ALIGNMENT CHECK ---
if obj['left'] < stranger_limit:
final_decision = True
debug_log.append(f"✅ VALID (Stranger): Found Left-Aligned {obj['type']} at X={int(obj['left'])}")
break
elif obj['left'] > me_limit:
final_decision = False
debug_log.append(f"❌ INVALID (Me): Found Right-Aligned {obj['type']} at X={int(obj['left'])}")
break
# --- RETURN ---
if final_decision is True:
return {"valid": True, "reason": "First message/sticker is from stranger.", "debug": debug_log}
elif final_decision is False:
return {"valid": False, "reason": "First message/sticker is from you.", "debug": debug_log}
else:
return {"valid": False, "reason": "No valid messages or stickers detected.", "debug": debug_log}
iface = gr.Interface(
fn=validate_whatsapp_hybrid,
inputs=gr.Image(type="pil"),
outputs=gr.JSON(),
title="WhatsApp Validator V13 (Hybrid Text + Sticker Detection)",
description="Uses Deep Learning OCR for text and OpenCV Blob Detection for stickers."
)
if __name__ == "__main__":
iface.launch()