mobileapp / CrimeSceneAnalyzer.py
gcrn2318
Fix classify_scene: handle BaseModelOutputWithPooling from newer transformers
4b84b46
Raw
History Blame Contribute Delete
24 kB
import torch
import requests
from PIL import Image, ImageDraw, ImageFont
import matplotlib.pyplot as plt
from transformers import (
DetrImageProcessor, DetrForObjectDetection,
ViTImageProcessor, ViTForImageClassification,
AutoModelForCausalLM, AutoTokenizer,
pipeline
)
from ultralytics import YOLO
import cv2
import numpy as np
from datetime import datetime
from fpdf import FPDF
import os
import io
import traceback # Import traceback for better error logging
import torch
import joblib
import numpy as np
from transformers import CLIPProcessor, CLIPModel
from config import DEVICE, MODEL_SAVE_PATH, LABEL_ENCODER_PATH
import google.generativeai as genai
# ======================
# 1. INITIALIZATION
# ======================
class CrimeSceneAnalyzer:
def __init__(self):
self.yolo_model = YOLO('yolov8x.pt') # Automatically downloads if missing
# Load CLIP model and processor
self.clip_model = CLIPModel.from_pretrained("openai/clip-vit-base-patch32").to(DEVICE)
self.clip_processor = CLIPProcessor.from_pretrained("openai/clip-vit-base-patch32")
# Load crime scene classification model and label encoder
self.crime_scene_model = joblib.load(MODEL_SAVE_PATH)
self.label_encoder = joblib.load(LABEL_ENCODER_PATH)
# Object Detection (DETR)
self.detr_processor = DetrImageProcessor.from_pretrained("facebook/detr-resnet-50")
self.detr_model = DetrForObjectDetection.from_pretrained("facebook/detr-resnet-50")
self.detr_model.config.auxiliary_loss = True
self.detr_model.config.num_queries = 150
# Evidence Classification (ViT)
self.vit_processor = ViTImageProcessor.from_pretrained('google/vit-base-patch16-224')
self.vit_model = ViTForImageClassification.from_pretrained('google/vit-base-patch16-224')
# Report Generation (GPT-2 fine-tuned)
gemini_api_key = os.environ.get("GEMINI_API_KEY", "AIzaSyBG6e9rH2tPinqRQay2QrXTkMphkYEjyeY")
if not gemini_api_key:
print("⚠️ GEMINI_API_KEY environment variable not set. Report/Summary generation will be affected.")
self.gemini_model = None
else:
genai.configure(api_key=gemini_api_key)
# Choose the model best suited for report generation.
# gemini-1.5-flash is fast and cost-effective.
# gemini-1.0-pro or gemini-1.5-pro for potentially more detailed/nuanced reports.
self.gemini_model = genai.GenerativeModel('gemini-2.5-flash-lite')
# Evidence Mapping (your existing map)
self.EVIDENCE_MAP = {
"knife": {"type": "weapon", "priority": 1}, "gun": {"type": "weapon", "priority": 1},
"pistol": {"type": "weapon", "priority": 1}, "revolver": {"type": "weapon", "priority": 1},
"rifle": {"type": "weapon", "priority": 1}, "shotgun": {"type": "weapon", "priority": 1},
"sword": {"type": "weapon", "priority": 1}, "machete": {"type": "weapon", "priority": 1},
"brass knuckles": {"type": "weapon", "priority": 1}, "taser": {"type": "weapon", "priority": 1},
"pepper spray": {"type": "weapon", "priority": 1}, "crossbow": {"type": "weapon", "priority": 1},
"axe": {"type": "weapon", "priority": 1}, "hammer": {"type": "weapon", "priority": 1},
"scissors": {"type": "weapon", "priority": 1},
"phone": {"type": "electronic", "priority": 2}, "laptop": {"type": "electronic", "priority": 2},
"tablet": {"type": "electronic", "priority": 2}, "camera": {"type": "electronic", "priority": 2},
"usb": {"type": "electronic", "priority": 2}, "hard drive": {"type": "electronic", "priority": 2},
"sd card": {"type": "electronic", "priority": 2}, "dvr": {"type": "electronic", "priority": 2},
"router": {"type": "electronic", "priority": 2}, "sim card": {"type": "electronic", "priority": 2},
"blood": {"type": "biological", "priority": 1}, "hair": {"type": "biological", "priority": 1},
"fingerprint": {"type": "biological", "priority": 1}, "dna": {"type": "biological", "priority": 1},
"saliva": {"type": "biological", "priority": 1}, "semen": {"type": "biological", "priority": 1},
"tissue": {"type": "biological", "priority": 1}, "bone": {"type": "biological", "priority": 1},
"tooth": {"type": "biological", "priority": 1},
"bottle": {"type": "container", "priority": 3}, "syringe": {"type": "container", "priority": 1},
"needle": {"type": "drug", "priority": 1}, "pill": {"type": "drug", "priority": 1},
"powder": {"type": "drug", "priority": 1}, "marijuana": {"type": "drug", "priority": 1},
"cocaine": {"type": "drug", "priority": 1}, "heroin": {"type": "drug", "priority": 1},
"meth": {"type": "drug", "priority": 1}, "pipe": {"type": "drug", "priority": 1},
"scale": {"type": "drug", "priority": 1},
"paper": {"type": "document", "priority": 2}, "key": {"type": "tool", "priority": 2},
"id": {"type": "document", "priority": 2}, "passport": {"type": "document", "priority": 2},
"license": {"type": "document", "priority": 2}, "credit card": {"type": "document", "priority": 2},
"money": {"type": "document", "priority": 2}, "note": {"type": "document", "priority": 2},
"letter": {"type": "document", "priority": 2}, "diary": {"type": "document", "priority": 2},
"map": {"type": "document", "priority": 2}, "blueprint": {"type": "document", "priority": 2},
"shoe": {"type": "clothing", "priority": 3}, "glove": {"type": "clothing", "priority": 3},
"mask": {"type": "clothing", "priority": 3}, "hat": {"type": "clothing", "priority": 3},
"jacket": {"type": "clothing", "priority": 3}, "backpack": {"type": "clothing", "priority": 3},
"watch": {"type": "clothing", "priority": 3}, "jewelry": {"type": "clothing", "priority": 3},
"eyeglasses": {"type": "clothing", "priority": 3}, "crowbar": {"type": "tool", "priority": 2},
"screwdriver": {"type": "tool", "priority": 2}, "wrench": {"type": "tool", "priority": 2},
"pliers": {"type": "tool", "priority": 2}, "lockpick": {"type": "tool", "priority": 2},
"shovel": {"type": "tool", "priority": 2}, "rope": {"type": "tool", "priority": 2},
"duct tape": {"type": "tool", "priority": 2}, "wire": {"type": "tool", "priority": 2},
"car": {"type": "vehicle", "priority": 3}, "bicycle": {"type": "vehicle", "priority": 3},
"motorcycle": {"type": "vehicle", "priority": 3}, "license plate": {"type": "vehicle", "priority": 2},
"key": {"type": "vehicle", "priority": 3},
"person": {"type": "person", "priority": 3}
}
# Visualization
try:
self.font = ImageFont.truetype("arial.ttf", 12)
except:
try:
self.font = ImageFont.truetype("LiberationSans-Regular.ttf", 12)
except:
self.font = ImageFont.load_default()
self.colors = {
"weapon": "red", "electronic": "blue", "biological": "green", "drug":"orange",
"person": "purple", "document":"black", "tool":"brown", "clothing":"pink",
"vehicle":"gold", "default": "yellow"
}
# ======================
# 2. CORE FUNCTIONALITY
# ======================
def classify_scene(self, image: Image.Image):
"""
Classifies the crime scene image using the CLIP model and the trained crime scene classification model.
"""
try:
inputs = self.clip_processor(images=image, return_tensors="pt").to(DEVICE)
with torch.no_grad():
image_features = self.clip_model.get_image_features(**inputs)
# Newer transformers may return BaseModelOutputWithPooling
if hasattr(image_features, 'pooler_output'):
image_features = image_features.pooler_output
image_features = image_features / image_features.norm(p=2, dim=-1, keepdim=True)
features = image_features.cpu().numpy()
pred = self.crime_scene_model.predict(features)
label = self.label_encoder.inverse_transform(pred)
return label[0]
except Exception as e:
print(f"Error classifying scene: {e}")
traceback.print_exc()
return "Undetermined"
def analyze_scene(self, image_pil: Image.Image):
"""
Analyzes the crime scene image, generates analysis data, and a PDF report.
Returns a dictionary containing both for API consumption.
"""
try:
image = image_pil
# Classify the scene
crime_type = self.classify_scene(image)
# Detection and classification
detections = self._detect_objects(image)
evidence = self._classify_evidence(image, detections)
# Generate report text (for GPT-2 and PDF)
if evidence:
report_text = self._generate_report(evidence, image.size)
else:
report_text = "No significant forensic evidence detected. Detected objects:\n" + \
"\n".join(f"- {d['label']} (confidence: {d['score']:.2f})" for d in detections if d['score'] > 0.4)
# Generate visualization (for PDF embedding)
visualization = self._visualize_results(image.copy(), evidence if evidence else detections)
# Prepare data for React Native Frontend (AnalysisResult type)
scene_summary = "AI analysis complete. Refer to the 'Digital Forensic Analysis' section or the full PDF report for detailed findings."
frontend_evidence = []
if evidence:
for i, item in enumerate(evidence):
# For `item.image` in React Native, you would typically save
# a cropped thumbnail or provide a URL to a pre-generated visualization.
# For this example, we'll leave it empty.
frontend_evidence.append({
"id": i,
"type": item['type'].capitalize(),
"confidence": item['score'],
# Approximate location string from bounding box
"location": f"X:{int(item['box'][0])}-Y:{int(item['box'][1])} (W:{int(item['box'][2]-item['box'][0])}, H:{int(item['box'][3]-item['box'][1])})",
"image": "" # Placeholder: No cropped image sent directly in this example
})
else:
scene_summary = "No primary forensic evidence detected. General object detection was performed. Detailed analysis is in the report."
# Determine crime type and recommendations based on evidence and scene classification
crime_type_primary = crime_type
crime_type_confidence = 0.8 # Adjust confidence based on model performance
crime_type_secondary = ["General Investigation", "Scene Documentation"]
if any(item.get("type") == "weapon" for item in evidence):
crime_type_primary = "Assault/Homicide Related"
crime_type_confidence = 0.9
crime_type_secondary.insert(0, "Weapon Related Incident")
if any(item.get("type") == "drug" for item in evidence):
crime_type_primary = "Drug Related Incident"
crime_type_confidence = max(crime_type_confidence, 0.8)
crime_type_secondary.insert(0, "Substance Abuse Investigation")
if any(item.get("type") == "electronic" for item in evidence):
if "Digital Forensics" not in crime_type_secondary:
crime_type_secondary.append("Digital Forensics Required")
recommendations = [
"Ensure scene is fully secured and all personnel wear appropriate PPE.",
"Prioritize collection of high-priority evidence (weapons, biological).",
"Carefully document and photograph all evidence 'in situ' before collection.",
"Establish a strict chain of custody for every item collected.",
"Consider requesting specialized forensic units for biological or digital evidence.",
"Review immediate vicinity for additional evidence or potential witnesses/CCTV."
]
analysis_data_for_frontend = {
"sceneSummary": report_text, # Using the generated report text as scene summary
"evidenceDetected": frontend_evidence,
"crimeType": {
"primary": crime_type_primary,
"confidence": crime_type_confidence,
"secondary": list(set(crime_type_secondary)) # Remove duplicates
},
"recommendations": recommendations,
}
# Generate PDF bytes in memory
pdf_bytes = self._generate_pdf_report_in_memory(
evidence=evidence or [],
report_text=report_text,
visualization_img=visualization
)
return {
"analysis_data": analysis_data_for_frontend,
"pdf_bytes": pdf_bytes
}
except Exception as e:
print(f"Analysis failed in CrimeSceneAnalyzer: {str(e)}")
traceback.print_exc()
return {"error": f"Analysis failed: {str(e)}", "analysis_data": None, "pdf_bytes": None}
# ======================
# 3. HELPER METHODS (These remain largely unchanged)
# ======================
def _load_image(self, source): # Not directly used by the API endpoint, but good to keep
if source.startswith(('http:', 'https:')):
return Image.open(requests.get(source, stream=True).raw)
return Image.open(source)
def _detect_objects(self, image):
img_array = np.array(image)
results = self.yolo_model(img_array)
detections = []
for result in results:
for detection in result.boxes.data.tolist():
x1, y1, x2, y2, confidence, class_id = detection
label = self.yolo_model.names[int(class_id)]
detections.append({
"label": label,
"score": confidence,
"box": [x1, y1, x2, y2]
})
print(f"\nDetection results (showing >40% confidence):")
for obj in detections:
if obj["score"] > 0.4:
print(f"- {obj['label']}: {obj['score']:.2f} at {[round(x,1) for x in obj['box']]}")
return detections
def _classify_evidence(self, image, detections):
# Using the instance's EVIDENCE_MAP
evidence = []
for obj in detections:
obj_name = obj["label"].lower()
matched = False
for key in self.EVIDENCE_MAP: # Use self.EVIDENCE_MAP
if key in obj_name:
evidence.append({
**obj,
**self.EVIDENCE_MAP[key], # Use self.EVIDENCE_MAP
"exact_match": key == obj_name
})
matched = True
break
if not matched and obj["score"] > 0.1:
print(f"⚠️ Unclassified object: {obj_name} (score: {obj['score']:.2f})")
return sorted(evidence, key=lambda x: (-x["priority"], -x["score"]))
def _generate_report(self, evidence, image_size): # Renamed for clarity if you keep separate summary
if not self.gemini_model:
return "Report generation skipped: Gemini API key not configured."
if not evidence:
return "No evidence provided to generate a report."
evidence_text = "\n".join(
f"- Object: {e.get('label', 'Unknown')}\n Type: {e.get('type', 'N/A').upper()}\n Confidence: {e.get('score', 0):.0%}\n Assessed Priority: {e.get('priority', 'N/A')}"
for e in evidence
)
# You can add image_size or other context to the prompt if needed
# image_context = f"The analysis was performed on an image of size: {image_size[0]}x{image_size[1]} pixels."
prompt = f"""
You are a professional detective providing a detailed forensic analysis report.
Based *only* on the evidence items listed below, generate a plausible narrative of what could have happened at the scene.
Structure your report clearly. Be objective and stick to the facts presented by the evidence.
Do not invent evidence not listed.
EVIDENCE FOUND:
{evidence_text}
ANALYSIS OF WHAT COULD HAVE HAPPENED:
"""
try:
print("Generating detailed report with Gemini...")
response = self.gemini_model.generate_content(prompt, request_options={'timeout': 15})
report_text = ""
if response.parts:
report_text = "".join(part.text for part in response.parts if hasattr(part, 'text'))
elif response.candidates and response.candidates[0].content and response.candidates[0].content.parts:
report_text = "".join(part.text for part in response.candidates[0].content.parts if hasattr(part, 'text'))
if not report_text.strip():
return "Gemini generated an empty report for the provided evidence."
# The prompt is already part of the generated text by Gemini,
# so we usually don't need to prepend it.
# If Gemini's response *only* contains the analysis part, and not the "EVIDENCE FOUND" preamble,
# then you might want to structure the final return differently.
# For now, let's assume Gemini's response is the full text you need.
return report_text
except Exception as e:
print(f"‼️ Error calling Gemini API for report generation: {str(e)}")
return f"Detailed report generation failed due to an API error: {str(e)}"
def _visualize_results(self, image, items):
draw = ImageDraw.Draw(image)
for item in items:
box = [int(b) for b in item["box"]] # Ensure integers for drawing
if len(box) != 4: continue # Skip malformed boxes
if "type" in item:
color = self.colors.get(item["type"], self.colors["default"])
label = f"{item['label']} ({item['score']:.0%})"
else:
color = "gray"
label = f"{item['label']} ({item['score']:.2f})"
draw.rectangle(box, outline=color, width=3)
try: # Use textbbox for modern Pillow
bbox = draw.textbbox((0, 0), label, font=self.font)
text_width = bbox[2] - bbox[0]
text_height = bbox[3] - bbox[1]
except AttributeError: # Fallback for older versions
text_width, text_height = draw.textsize(label, font=self.font)
draw.rectangle(
[box[0], box[1], box[0] + text_width + 4, box[1] + text_height],
fill=color
)
draw.text(
(box[0] + 2, box[1]),
label,
fill="white",
font=self.font
)
return image
def _generate_pdf_report_in_memory(self, evidence, report_text, visualization_img):
try:
pdf = FPDF(format='A4', unit='mm')
pdf.set_auto_page_break(auto=True, margin=15)
pdf.add_page()
# Header
logo_path = "SceneX_logo.png" # Make sure this file exists in the same directory or provide full path
if os.path.exists(logo_path):
pdf.image(logo_path, x=10, y=8, w=25, h=25)
pdf.set_font("Courier", 'B', 18)
pdf.set_text_color(0, 51, 102)
pdf.cell(0, 20, "OFFICIAL CRIME SCENE REPORT", ln=1, align='C')
pdf.set_font("Courier", '', 12)
pdf.set_text_color(0, 0, 0)
pdf.cell(0, 6, f"Case Reference: CSR-{datetime.now().strftime('%Y%m%d-%H%M%S')}", ln=1)
pdf.cell(0, 6, f"Report Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}", ln=1)
pdf.ln(15)
# Visualization
img = visualization_img.convert('RGB')
img_buffer = io.BytesIO()
img.save(img_buffer, format="JPEG", quality=95)
img_buffer.seek(0)
pdf.image(img_buffer, x=12, y=pdf.get_y()+2, w=186)
pdf.ln(125)
# Evidence Catalog
pdf.set_font("Courier", 'B', 14)
pdf.set_fill_color(220, 230, 242)
pdf.cell(0, 10, "EVIDENCE CATALOG", ln=1, fill=True)
pdf.ln(5)
if evidence:
pdf.set_font("Courier", 'B', 12)
col_widths = [70, 40, 40, 40]
headers = ["Item Description", "Evidence Type", "Confidence", "Priority"]
for i, header in enumerate(headers):
pdf.cell(col_widths[i], 8, header, border=1, fill=True)
pdf.ln()
pdf.set_font("Courier", '', 10)
row_fill = False
for item in evidence:
pdf.set_fill_color(255, 255, 255) if row_fill else pdf.set_fill_color(240, 240, 240)
pdf.cell(col_widths[0], 8, item['label'], border=1, fill=True)
pdf.cell(col_widths[1], 8, item['type'].upper(), border=1, fill=True)
pdf.cell(col_widths[2], 8, f"{item['score']:.0%}", border=1, fill=True)
pdf.cell(col_widths[3], 8, str(item['priority']), border=1, ln=1, fill=True)
row_fill = not row_fill
else:
pdf.set_font("Courier", 'I', 12)
pdf.set_text_color(150, 150, 150)
pdf.cell(0, 8, "No specific forensic evidence categorized by the AI model.", ln=1, align='C')
pdf.set_text_color(0, 0, 0)
pdf.ln(10)
pdf.set_font("Courier", 'B', 14)
pdf.set_text_color(0, 51, 102)
pdf.cell(0, 10, "DIGITAL FORENSIC ANALYSIS", ln=1)
pdf.set_draw_color(200, 200, 200)
pdf.line(10, pdf.get_y(), 200, pdf.get_y())
pdf.ln(5)
pdf.set_font("Courier", '', 12)
cleaned_report_text = report_text.encode('latin-1', 'replace').decode('latin-1')
paragraphs = cleaned_report_text.split('\n')
for para in paragraphs:
if para.strip():
pdf.multi_cell(0, 6, para)
pdf.ln(3)
# Footer
pdf.set_y(-20)
pdf.set_font("Courier", 'I', 8)
pdf.set_text_color(100, 100, 100)
pdf.cell(0, 5, "CONFIDENTIAL - Law Enforcement Use Only", 0, 0, 'L')
pdf.cell(0, 5, f"Page {pdf.page_no()}", 0, 0, 'R')
print("✓ Professional report generated in-memory.")
return pdf.output()
except Exception as e:
print(f"!! Critical PDF generation error: {str(e)}")
traceback.print_exc()
return None
def _add_watermark(self, img):
try:
draw = ImageDraw.Draw(img)
font = ImageFont.load_default()
text = f"SceneX Analysis {datetime.now().strftime('%Y-%m-%d')}"
for i in range(0, img.width, 200):
for j in range(0, img.height, 200):
draw.text((i, j), text, fill=(200, 200, 200, 128), font=font)
return img
except:
return img