Haiss123 commited on
Commit
9adeec5
ยท
verified ยท
1 Parent(s): df68c18

Upload 5 files

Browse files
Files changed (5) hide show
  1. detection_api.py +795 -0
  2. main.py +1840 -0
  3. models/best.pt +3 -0
  4. models/last.pt +3 -0
  5. requirements.txt +19 -0
detection_api.py ADDED
@@ -0,0 +1,795 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI, File, UploadFile, HTTPException, BackgroundTasks, Form
2
+ from fastapi.responses import FileResponse
3
+ from fastapi.middleware.cors import CORSMiddleware
4
+ from pydantic import BaseModel, Field
5
+ from typing import List, Optional, Dict, Any, Union
6
+ import cv2
7
+ import numpy as np
8
+ from datetime import datetime
9
+ import aiofiles
10
+ import json
11
+ from pathlib import Path
12
+ import uuid
13
+ import traceback
14
+ from concurrent.futures import ThreadPoolExecutor
15
+ import logging
16
+
17
+ from main import ContentModerator
18
+
19
+ # Setup logging
20
+ logging.basicConfig(
21
+ level=logging.INFO,
22
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
23
+ )
24
+ logger = logging.getLogger(__name__)
25
+
26
+ # Initialize FastAPI app
27
+ app = FastAPI(
28
+ title="Weapon & NSFW Detection API",
29
+ description="API for detecting knives/dao, guns, fights and NSFW content in images and videos",
30
+ version="2.0.0",
31
+ docs_url="/docs",
32
+ redoc_url="/redoc"
33
+ )
34
+
35
+ # Add CORS middleware
36
+ app.add_middleware(
37
+ CORSMiddleware,
38
+ allow_origins=["*"],
39
+ allow_credentials=True,
40
+ allow_methods=["*"],
41
+ allow_headers=["*"],
42
+ )
43
+
44
+
45
+ # Configuration
46
+ class Config:
47
+ UPLOAD_DIR = Path("uploads")
48
+ RESULTS_DIR = Path("results")
49
+ PROCESSED_DIR = Path("processed")
50
+ MAX_IMAGE_SIZE = 50 * 1024 * 1024 # 50MB for images
51
+ MAX_VIDEO_SIZE = 500 * 1024 * 1024 # 500MB for videos
52
+ ALLOWED_IMAGE_EXTENSIONS = {'.jpg', '.jpeg', '.png', '.bmp', '.gif', '.webp'}
53
+ ALLOWED_VIDEO_EXTENSIONS = {'.mp4', '.avi', '.mov', '.mkv', '.webm', '.flv', '.wmv'}
54
+ VIDEO_FRAME_SKIP = 5 # Process every 5th frame for performance
55
+ CLEANUP_AFTER_HOURS = 24
56
+ ENABLE_ANNOTATED_OUTPUT = True
57
+ MAX_WORKERS = 4
58
+
59
+
60
+ config = Config()
61
+
62
+ # Create necessary directories
63
+ for directory in [config.UPLOAD_DIR, config.RESULTS_DIR, config.PROCESSED_DIR]:
64
+ directory.mkdir(exist_ok=True)
65
+ (directory / "images").mkdir(exist_ok=True)
66
+ (directory / "videos").mkdir(exist_ok=True)
67
+
68
+ # Global moderator instance (initialized on startup)
69
+ moderator: Optional[ContentModerator] = None
70
+
71
+ # Thread pool for background processing
72
+ executor = ThreadPoolExecutor(max_workers=config.MAX_WORKERS)
73
+
74
+
75
+ # ============== Response Models ==============
76
+
77
+ class BoundingBox(BaseModel):
78
+ x1: int = Field(..., description="Top-left x coordinate")
79
+ y1: int = Field(..., description="Top-left y coordinate")
80
+ x2: int = Field(..., description="Bottom-right x coordinate")
81
+ y2: int = Field(..., description="Bottom-right y coordinate")
82
+
83
+
84
+ class WeaponDetection(BaseModel):
85
+ type: str = Field(..., description="Detection type (weapon)")
86
+ class_name: str = Field(..., description="Weapon class (knife/dao/gun)")
87
+ weapon_type: str = Field(..., description="Weapon category (blade/firearm)")
88
+ confidence: float = Field(..., ge=0, le=1, description="Detection confidence")
89
+ bbox: BoundingBox
90
+ threat_level: str = Field(..., description="Threat level (low/medium/high/critical)")
91
+ detection_method: str = Field(..., description="Detection method used")
92
+
93
+
94
+ class NSFWDetection(BaseModel):
95
+ type: str = Field(..., description="Detection type (nsfw)")
96
+ class_name: str = Field(..., description="NSFW class")
97
+ confidence: float = Field(..., ge=0, le=1, description="Detection confidence")
98
+ bbox: BoundingBox
99
+ method: str = Field(..., description="Detection method (classification/skin_detection/pose_analysis)")
100
+ skin_ratio: Optional[float] = Field(None, description="Skin exposure ratio if applicable")
101
+
102
+
103
+ class FightDetection(BaseModel):
104
+ type: str = Field(default="fight", description="Detection type")
105
+ confidence: float = Field(..., ge=0, le=1, description="Detection confidence")
106
+ bbox: BoundingBox
107
+ persons_involved: int = Field(..., description="Number of persons detected in fight")
108
+ threat_level: str = Field(..., description="Threat level")
109
+
110
+
111
+ class ImageDetectionResponse(BaseModel):
112
+ success: bool
113
+ request_id: str
114
+ timestamp: str
115
+ image_info: Dict[str, Any]
116
+ detections: Dict[str, List[Union[WeaponDetection, NSFWDetection, FightDetection]]]
117
+ summary: Dict[str, Any]
118
+ risk_level: str
119
+ action_required: bool
120
+ annotated_image_url: Optional[str] = None
121
+ processing_time_ms: float
122
+
123
+
124
+ class VideoDetectionResponse(BaseModel):
125
+ success: bool
126
+ request_id: str
127
+ timestamp: str
128
+ video_info: Dict[str, Any]
129
+ total_frames_processed: int
130
+ frame_detections: List[Dict[str, Any]]
131
+ summary: Dict[str, Any]
132
+ risk_level: str
133
+ action_required: bool
134
+ processed_video_url: Optional[str] = None
135
+ processing_time_ms: float
136
+
137
+
138
+ class ErrorResponse(BaseModel):
139
+ success: bool = False
140
+ error: str
141
+ error_code: str
142
+ timestamp: str
143
+ request_id: Optional[str] = None
144
+
145
+
146
+ # ============== Utility Functions ==============
147
+
148
+ def generate_request_id() -> str:
149
+ """Generate unique request ID"""
150
+ return f"req_{datetime.now().strftime('%Y%m%d%H%M%S')}_{uuid.uuid4().hex[:8]}"
151
+
152
+
153
+ def validate_file_extension(filename: str, allowed_extensions: set) -> bool:
154
+ """Validate file extension"""
155
+ return Path(filename).suffix.lower() in allowed_extensions
156
+
157
+
158
+ def validate_file_size(file_size: int, max_size: int) -> bool:
159
+ """Validate file size"""
160
+ return file_size <= max_size
161
+
162
+
163
+ async def save_upload_file(upload_file: UploadFile, destination: Path) -> Path:
164
+ """Save uploaded file to destination"""
165
+ try:
166
+ async with aiofiles.open(destination, 'wb') as f:
167
+ content = await upload_file.read()
168
+ await f.write(content)
169
+ return destination
170
+ except Exception as e:
171
+ logger.error(f"Error saving file: {e}")
172
+ raise
173
+
174
+
175
+ def detect_fight_in_frame(image: np.ndarray, persons: List[Dict]) -> Optional[FightDetection]:
176
+ """
177
+ Detect potential fight based on person proximity and poses
178
+ This is a simplified implementation - you may want to enhance this
179
+ """
180
+ if len(persons) < 2:
181
+ return None
182
+
183
+ # Check for overlapping or very close person bounding boxes
184
+ for i in range(len(persons)):
185
+ for j in range(i + 1, len(persons)):
186
+ bbox1 = persons[i]['bbox']
187
+ bbox2 = persons[j]['bbox']
188
+
189
+ # Calculate center points
190
+ center1_x = (bbox1[0] + bbox1[2]) / 2
191
+ center1_y = (bbox1[1] + bbox1[3]) / 2
192
+ center2_x = (bbox2[0] + bbox2[2]) / 2
193
+ center2_y = (bbox2[1] + bbox2[3]) / 2
194
+
195
+ # Calculate distance between centers
196
+ distance = np.sqrt((center1_x - center2_x) ** 2 + (center1_y - center2_y) ** 2)
197
+
198
+ # Calculate average person width
199
+ avg_width = ((bbox1[2] - bbox1[0]) + (bbox2[2] - bbox2[0])) / 2
200
+
201
+ # If persons are very close (distance less than average width)
202
+ if distance < avg_width * 1.5:
203
+ # Create combined bounding box
204
+ min_x = min(bbox1[0], bbox2[0])
205
+ min_y = min(bbox1[1], bbox2[1])
206
+ max_x = max(bbox1[2], bbox2[2])
207
+ max_y = max(bbox1[3], bbox2[3])
208
+
209
+ return FightDetection(
210
+ type="fight",
211
+ confidence=0.7, # Simplified confidence
212
+ bbox=BoundingBox(x1=min_x, y1=min_y, x2=max_x, y2=max_y),
213
+ persons_involved=2,
214
+ threat_level="high"
215
+ )
216
+
217
+ return None
218
+
219
+
220
+ def process_detections(raw_detections: List[Dict]) -> Dict[str, List]:
221
+ """Process and categorize raw detections"""
222
+ processed = {
223
+ 'weapons': [],
224
+ 'nsfw': [],
225
+ 'fights': []
226
+ }
227
+
228
+ for det in raw_detections:
229
+ if det['type'] == 'weapon':
230
+ processed['weapons'].append(WeaponDetection(
231
+ type=det['type'],
232
+ class_name=det['class'],
233
+ weapon_type=det.get('weapon_type', 'unknown'),
234
+ confidence=det['confidence'],
235
+ bbox=BoundingBox(
236
+ x1=det['bbox'][0],
237
+ y1=det['bbox'][1],
238
+ x2=det['bbox'][2],
239
+ y2=det['bbox'][3]
240
+ ),
241
+ threat_level=det.get('threat_level', 'medium'),
242
+ detection_method=det.get('detection_method', 'yolo')
243
+ ))
244
+ elif det['type'] == 'nsfw':
245
+ processed['nsfw'].append(NSFWDetection(
246
+ type=det['type'],
247
+ class_name=det['class'],
248
+ confidence=det['confidence'],
249
+ bbox=BoundingBox(
250
+ x1=det['bbox'][0],
251
+ y1=det['bbox'][1],
252
+ x2=det['bbox'][2],
253
+ y2=det['bbox'][3]
254
+ ),
255
+ method=det.get('method', 'classification'),
256
+ skin_ratio=det.get('skin_ratio')
257
+ ))
258
+ elif det['type'] == 'fight':
259
+ processed['fights'].append(det)
260
+
261
+ return processed
262
+
263
+
264
+ # ============== API Endpoints ==============
265
+
266
+ @app.on_event("startup")
267
+ async def startup_event():
268
+ """Initialize moderator on startup"""
269
+ global moderator
270
+ try:
271
+ logger.info("Initializing Content Moderator...")
272
+
273
+ # Custom configuration for API
274
+ custom_config = {
275
+ 'weapon_detection': {
276
+ 'enabled': True,
277
+ 'confidence_threshold': 0.5,
278
+ 'knife_confidence': 0.25,
279
+ 'model_size': 'yolo11n',
280
+ 'classes': ['knife', 'dao', 'gun', 'rifle', 'pistol', 'weapon', 'fight'],
281
+ 'use_enhancement': True,
282
+ 'multi_pass': True,
283
+ 'boost_knife_detection': True
284
+ },
285
+ 'nsfw_detection': {
286
+ 'enabled': True,
287
+ 'confidence_threshold': 0.7,
288
+ 'skin_detection': True,
289
+ 'pose_analysis': False, # Disabled for performance
290
+ 'region_analysis': True
291
+ },
292
+ 'performance': {
293
+ 'image_size': 640,
294
+ 'batch_size': 1,
295
+ 'half_precision': True,
296
+ 'use_flash_attention': False,
297
+ 'cpu_optimization': False
298
+ },
299
+ 'output': {
300
+ 'save_detections': True,
301
+ 'draw_boxes': True,
302
+ 'log_results': True
303
+ }
304
+ }
305
+
306
+ moderator = ContentModerator(config=custom_config)
307
+ logger.info("โœ… Content Moderator initialized successfully")
308
+
309
+ # Log model status
310
+ status = moderator.get_model_status()
311
+ logger.info(f"Model Status: {json.dumps(status, indent=2)}")
312
+
313
+ except Exception as e:
314
+ logger.error(f"Failed to initialize Content Moderator: {e}")
315
+ logger.error(traceback.format_exc())
316
+
317
+
318
+ @app.on_event("shutdown")
319
+ async def shutdown_event():
320
+ """Cleanup on shutdown"""
321
+ executor.shutdown(wait=True)
322
+ logger.info("API shutdown complete")
323
+
324
+
325
+ @app.get("/", response_model=Dict[str, Any])
326
+ async def root():
327
+ """API root endpoint with status information"""
328
+ if moderator:
329
+ status = moderator.get_model_status()
330
+ return {
331
+ "service": "Weapon & NSFW Detection API",
332
+ "version": "2.0.0",
333
+ "status": "operational",
334
+ "models": status,
335
+ "endpoints": {
336
+ "image_detection": "/detect_n_k_f_g/images",
337
+ "video_detection": "/detect_n_k_f_g/videos",
338
+ "documentation": "/docs"
339
+ }
340
+ }
341
+ else:
342
+ return {
343
+ "service": "Weapon & NSFW Detection API",
344
+ "version": "2.0.0",
345
+ "status": "initializing",
346
+ "message": "Models are being loaded..."
347
+ }
348
+
349
+
350
+ @app.post("/detect_n_k_f_g/images", response_model=ImageDetectionResponse)
351
+ async def detect_image(
352
+ file: UploadFile = File(..., description="Image file to analyze"),
353
+ enable_fight_detection: bool = Form(True, description="Enable fight detection"),
354
+ return_annotated: bool = Form(True, description="Return annotated image")
355
+ ):
356
+ """
357
+ Detect weapons (knife/dao/gun), fights, and NSFW content in images
358
+
359
+ Supports: JPG, JPEG, PNG, BMP, GIF, WEBP
360
+ Max size: 50MB
361
+ """
362
+ request_id = generate_request_id()
363
+ start_time = datetime.now()
364
+
365
+ try:
366
+ # Validate file extension
367
+ if not validate_file_extension(file.filename, config.ALLOWED_IMAGE_EXTENSIONS):
368
+ raise HTTPException(
369
+ status_code=400,
370
+ detail=f"Invalid file type. Allowed: {', '.join(config.ALLOWED_IMAGE_EXTENSIONS)}"
371
+ )
372
+
373
+ # Check file size
374
+ file_content = await file.read()
375
+ file_size = len(file_content)
376
+
377
+ if not validate_file_size(file_size, config.MAX_IMAGE_SIZE):
378
+ raise HTTPException(
379
+ status_code=400,
380
+ detail=f"File too large. Maximum size: {config.MAX_IMAGE_SIZE / (1024 * 1024):.1f}MB"
381
+ )
382
+
383
+ # Save uploaded file
384
+ upload_path = config.UPLOAD_DIR / "images" / f"{request_id}_{file.filename}"
385
+ async with aiofiles.open(upload_path, 'wb') as f:
386
+ await f.write(file_content)
387
+
388
+ # Read image with OpenCV
389
+ nparr = np.frombuffer(file_content, np.uint8)
390
+ image = cv2.imdecode(nparr, cv2.IMREAD_COLOR)
391
+
392
+ if image is None:
393
+ raise HTTPException(status_code=400, detail="Invalid or corrupted image file")
394
+
395
+ # Get image info
396
+ height, width, channels = image.shape
397
+ image_info = {
398
+ "filename": file.filename,
399
+ "width": width,
400
+ "height": height,
401
+ "channels": channels,
402
+ "size_bytes": file_size,
403
+ "size_mb": round(file_size / (1024 * 1024), 2)
404
+ }
405
+
406
+ # Process image with ContentModerator
407
+ logger.info(f"Processing image {request_id}")
408
+ result = moderator.process_image(image)
409
+
410
+ if not result:
411
+ raise HTTPException(status_code=500, detail="Detection processing failed")
412
+
413
+ # Detect persons for potential fight detection
414
+ persons = moderator.detect_persons(image)
415
+
416
+ # Check for fights if enabled
417
+ fight_detection = None
418
+ if enable_fight_detection and len(persons) >= 2:
419
+ fight_detection = detect_fight_in_frame(image, persons)
420
+
421
+ # Process detections
422
+ processed = process_detections(result['detections'])
423
+
424
+ # Add fight detection if found
425
+ if fight_detection:
426
+ processed['fights'].append(fight_detection)
427
+
428
+ # Save annotated image if requested
429
+ annotated_url = None
430
+ if return_annotated and config.ENABLE_ANNOTATED_OUTPUT:
431
+ if 'annotated_image' in result:
432
+ annotated_path = config.PROCESSED_DIR / "images" / f"{request_id}_annotated.jpg"
433
+ cv2.imwrite(str(annotated_path), result['annotated_image'])
434
+ annotated_url = f"/results/images/{request_id}_annotated.jpg"
435
+ else:
436
+ # Draw annotations manually if not provided
437
+ annotated_image = moderator.draw_detections(image.copy(), result['detections'])
438
+ annotated_path = config.PROCESSED_DIR / "images" / f"{request_id}_annotated.jpg"
439
+ cv2.imwrite(str(annotated_path), annotated_image)
440
+ annotated_url = f"/results/images/{request_id}_annotated.jpg"
441
+
442
+ # Calculate summary
443
+ total_weapons = len(processed['weapons'])
444
+ total_nsfw = len(processed['nsfw'])
445
+ total_fights = len(processed['fights'])
446
+
447
+ knife_count = sum(
448
+ 1 for w in processed['weapons'] if 'knife' in w.class_name.lower() or 'dao' in w.class_name.lower())
449
+ gun_count = sum(1 for w in processed['weapons'] if
450
+ 'gun' in w.class_name.lower() or 'pistol' in w.class_name.lower() or 'rifle' in w.class_name.lower())
451
+
452
+ summary = {
453
+ "total_detections": total_weapons + total_nsfw + total_fights,
454
+ "weapons": {
455
+ "total": total_weapons,
456
+ "knives": knife_count,
457
+ "guns": gun_count
458
+ },
459
+ "nsfw": total_nsfw,
460
+ "fights": total_fights,
461
+ "persons_detected": len(persons)
462
+ }
463
+
464
+ # Determine overall risk level
465
+ if total_weapons > 0 or total_fights > 0:
466
+ risk_level = "critical" if gun_count > 0 else "high"
467
+ elif total_nsfw > 0:
468
+ risk_level = "medium"
469
+ else:
470
+ risk_level = "safe"
471
+
472
+ # Calculate processing time
473
+ processing_time = (datetime.now() - start_time).total_seconds() * 1000
474
+
475
+ return ImageDetectionResponse(
476
+ success=True,
477
+ request_id=request_id,
478
+ timestamp=datetime.now().isoformat(),
479
+ image_info=image_info,
480
+ detections=processed,
481
+ summary=summary,
482
+ risk_level=risk_level,
483
+ action_required=(summary["total_detections"] > 0),
484
+ annotated_image_url=annotated_url,
485
+ processing_time_ms=processing_time
486
+ )
487
+
488
+ except HTTPException:
489
+ raise
490
+ except Exception as e:
491
+ logger.error(f"Error processing image {request_id}: {e}")
492
+ logger.error(traceback.format_exc())
493
+ raise HTTPException(
494
+ status_code=500,
495
+ detail=f"Internal server error: {str(e)}"
496
+ )
497
+
498
+
499
+ @app.post("/detect_n_k_f_g/videos", response_model=VideoDetectionResponse)
500
+ async def detect_video(
501
+ file: UploadFile = File(..., description="Video file to analyze"),
502
+ frame_skip: int = Form(5, ge=1, le=30, description="Process every Nth frame"),
503
+ max_frames: int = Form(1000, ge=10, le=5000, description="Maximum frames to process"),
504
+ enable_fight_detection: bool = Form(True, description="Enable fight detection"),
505
+ save_processed: bool = Form(False, description="Save processed video with annotations")
506
+ ):
507
+ """
508
+ Detect weapons (knife/dao/gun), fights, and NSFW content in videos
509
+
510
+ Supports: MP4, AVI, MOV, MKV, WEBM, FLV, WMV
511
+ Max size: 500MB
512
+ """
513
+ request_id = generate_request_id()
514
+ start_time = datetime.now()
515
+
516
+ try:
517
+ # Validate file extension
518
+ if not validate_file_extension(file.filename, config.ALLOWED_VIDEO_EXTENSIONS):
519
+ raise HTTPException(
520
+ status_code=400,
521
+ detail=f"Invalid file type. Allowed: {', '.join(config.ALLOWED_VIDEO_EXTENSIONS)}"
522
+ )
523
+
524
+ # Save uploaded video
525
+ upload_path = config.UPLOAD_DIR / "videos" / f"{request_id}_{file.filename}"
526
+ await save_upload_file(file, upload_path)
527
+
528
+ # Get file size
529
+ file_size = upload_path.stat().st_size
530
+ if not validate_file_size(file_size, config.MAX_VIDEO_SIZE):
531
+ upload_path.unlink() # Delete the file
532
+ raise HTTPException(
533
+ status_code=400,
534
+ detail=f"File too large. Maximum size: {config.MAX_VIDEO_SIZE / (1024 * 1024):.1f}MB"
535
+ )
536
+
537
+ # Open video
538
+ cap = cv2.VideoCapture(str(upload_path))
539
+ if not cap.isOpened():
540
+ raise HTTPException(status_code=400, detail="Invalid or corrupted video file")
541
+
542
+ # Get video info
543
+ fps = cap.get(cv2.CAP_PROP_FPS)
544
+ total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
545
+ width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
546
+ height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
547
+ duration = total_frames / fps if fps > 0 else 0
548
+
549
+ video_info = {
550
+ "filename": file.filename,
551
+ "width": width,
552
+ "height": height,
553
+ "fps": fps,
554
+ "total_frames": total_frames,
555
+ "duration_seconds": round(duration, 2),
556
+ "size_bytes": file_size,
557
+ "size_mb": round(file_size / (1024 * 1024), 2)
558
+ }
559
+
560
+ # Prepare output video if requested
561
+ out_writer = None
562
+ processed_video_path = None
563
+ if save_processed:
564
+ processed_video_path = config.PROCESSED_DIR / "videos" / f"{request_id}_processed.mp4"
565
+ fourcc = cv2.VideoWriter_fourcc(*'mp4v')
566
+ out_writer = cv2.VideoWriter(
567
+ str(processed_video_path),
568
+ fourcc,
569
+ fps,
570
+ (width, height)
571
+ )
572
+
573
+ # Process video frames
574
+ logger.info(f"Processing video {request_id}: {total_frames} frames, skip={frame_skip}")
575
+
576
+ frame_detections = []
577
+ frame_count = 0
578
+ processed_count = 0
579
+
580
+ # Aggregated statistics
581
+ all_weapons = []
582
+ all_nsfw = []
583
+ all_fights = []
584
+
585
+ while True:
586
+ ret, frame = cap.read()
587
+ if not ret:
588
+ break
589
+
590
+ frame_count += 1
591
+
592
+ # Skip frames according to frame_skip parameter
593
+ if frame_count % frame_skip != 0:
594
+ continue
595
+
596
+ # Limit maximum frames processed
597
+ if processed_count >= max_frames:
598
+ logger.info(f"Reached max frames limit: {max_frames}")
599
+ break
600
+
601
+ processed_count += 1
602
+
603
+ # Process frame
604
+ result = moderator.process_image(frame)
605
+
606
+ if result and result['detections']:
607
+ # Get persons for fight detection
608
+ persons = moderator.detect_persons(frame)
609
+
610
+ # Check for fights
611
+ fight_detection = None
612
+ if enable_fight_detection and len(persons) >= 2:
613
+ fight_detection = detect_fight_in_frame(frame, persons)
614
+
615
+ # Process detections
616
+ processed = process_detections(result['detections'])
617
+
618
+ if fight_detection:
619
+ processed['fights'].append(fight_detection)
620
+
621
+ # Store frame detection info
622
+ if len(processed['weapons']) > 0 or len(processed['nsfw']) > 0 or len(processed['fights']) > 0:
623
+ frame_info = {
624
+ "frame_number": frame_count,
625
+ "timestamp_seconds": frame_count / fps if fps > 0 else 0,
626
+ "detections": {
627
+ "weapons": [w.dict() for w in processed['weapons']],
628
+ "nsfw": [n.dict() for n in processed['nsfw']],
629
+ "fights": [f.dict() for f in processed['fights']]
630
+ }
631
+ }
632
+ frame_detections.append(frame_info)
633
+
634
+ # Aggregate statistics
635
+ all_weapons.extend(processed['weapons'])
636
+ all_nsfw.extend(processed['nsfw'])
637
+ all_fights.extend(processed['fights'])
638
+
639
+ # Write annotated frame if saving video
640
+ if out_writer and 'annotated_image' in result:
641
+ out_writer.write(result['annotated_image'])
642
+ elif out_writer:
643
+ # Write original frame if no detections
644
+ out_writer.write(frame)
645
+
646
+ # Log progress every 100 frames
647
+ if processed_count % 100 == 0:
648
+ logger.info(f"Processed {processed_count} frames...")
649
+
650
+ # Release resources
651
+ cap.release()
652
+ if out_writer:
653
+ out_writer.release()
654
+
655
+ # Calculate summary
656
+ knife_count = sum(1 for w in all_weapons if 'knife' in w.class_name.lower() or 'dao' in w.class_name.lower())
657
+ gun_count = sum(1 for w in all_weapons if 'gun' in w.class_name.lower() or 'pistol' in w.class_name.lower())
658
+
659
+ summary = {
660
+ "total_frames_analyzed": processed_count,
661
+ "frames_with_detections": len(frame_detections),
662
+ "total_detections": len(all_weapons) + len(all_nsfw) + len(all_fights),
663
+ "weapons": {
664
+ "total": len(all_weapons),
665
+ "knives": knife_count,
666
+ "guns": gun_count,
667
+ "unique_frames": len(set(f["frame_number"] for f in frame_detections if f["detections"]["weapons"]))
668
+ },
669
+ "nsfw": {
670
+ "total": len(all_nsfw),
671
+ "unique_frames": len(set(f["frame_number"] for f in frame_detections if f["detections"]["nsfw"]))
672
+ },
673
+ "fights": {
674
+ "total": len(all_fights),
675
+ "unique_frames": len(set(f["frame_number"] for f in frame_detections if f["detections"]["fights"]))
676
+ }
677
+ }
678
+
679
+ # Determine overall risk level
680
+ if gun_count > 0 or len(all_fights) > 5:
681
+ risk_level = "critical"
682
+ elif knife_count > 0 or len(all_fights) > 0:
683
+ risk_level = "high"
684
+ elif len(all_nsfw) > 0:
685
+ risk_level = "medium"
686
+ else:
687
+ risk_level = "safe"
688
+
689
+ # Calculate processing time
690
+ processing_time = (datetime.now() - start_time).total_seconds() * 1000
691
+
692
+ # Prepare processed video URL if saved
693
+ processed_video_url = None
694
+ if save_processed and processed_video_path and processed_video_path.exists():
695
+ processed_video_url = f"/results/videos/{request_id}_processed.mp4"
696
+
697
+ return VideoDetectionResponse(
698
+ success=True,
699
+ request_id=request_id,
700
+ timestamp=datetime.now().isoformat(),
701
+ video_info=video_info,
702
+ total_frames_processed=processed_count,
703
+ frame_detections=frame_detections,
704
+ summary=summary,
705
+ risk_level=risk_level,
706
+ action_required=(summary["total_detections"] > 0),
707
+ processed_video_url=processed_video_url,
708
+ processing_time_ms=processing_time
709
+ )
710
+
711
+ except HTTPException:
712
+ raise
713
+ except Exception as e:
714
+ logger.error(f"Error processing video {request_id}: {e}")
715
+ logger.error(traceback.format_exc())
716
+ raise HTTPException(
717
+ status_code=500,
718
+ detail=f"Internal server error: {str(e)}"
719
+ )
720
+ finally:
721
+ # Cleanup uploaded file if needed
722
+ if upload_path.exists() and not save_processed:
723
+ try:
724
+ upload_path.unlink()
725
+ except:
726
+ pass
727
+
728
+
729
+ @app.get("/results/images/{filename}")
730
+ async def get_processed_image(filename: str):
731
+ """Get processed/annotated image"""
732
+ file_path = config.PROCESSED_DIR / "images" / filename
733
+ if not file_path.exists():
734
+ raise HTTPException(status_code=404, detail="File not found")
735
+ return FileResponse(file_path)
736
+
737
+
738
+ @app.get("/results/videos/{filename}")
739
+ async def get_processed_video(filename: str):
740
+ """Get processed/annotated video"""
741
+ file_path = config.PROCESSED_DIR / "videos" / filename
742
+ if not file_path.exists():
743
+ raise HTTPException(status_code=404, detail="File not found")
744
+ return FileResponse(file_path)
745
+
746
+
747
+ @app.get("/health")
748
+ async def health_check():
749
+ """Health check endpoint"""
750
+ if moderator:
751
+ status = moderator.get_model_status()
752
+ return {
753
+ "status": "healthy",
754
+ "models_loaded": True,
755
+ "model_details": status
756
+ }
757
+ else:
758
+ return {
759
+ "status": "initializing",
760
+ "models_loaded": False
761
+ }
762
+
763
+
764
+ @app.delete("/cleanup")
765
+ async def cleanup_old_files(hours: int = 24):
766
+ """Clean up old files from upload and results directories"""
767
+ try:
768
+ from datetime import timedelta
769
+ cutoff_time = datetime.now() - timedelta(hours=hours)
770
+
771
+ deleted_count = 0
772
+ for directory in [config.UPLOAD_DIR, config.RESULTS_DIR, config.PROCESSED_DIR]:
773
+ for subdir in ["images", "videos"]:
774
+ path = directory / subdir
775
+ if path.exists():
776
+ for file in path.iterdir():
777
+ if file.is_file():
778
+ file_time = datetime.fromtimestamp(file.stat().st_mtime)
779
+ if file_time < cutoff_time:
780
+ file.unlink()
781
+ deleted_count += 1
782
+
783
+ return {
784
+ "success": True,
785
+ "deleted_files": deleted_count,
786
+ "message": f"Deleted {deleted_count} files older than {hours} hours"
787
+ }
788
+ except Exception as e:
789
+ logger.error(f"Cleanup error: {e}")
790
+ return {
791
+ "success": False,
792
+ "error": str(e)
793
+ }
794
+
795
+
main.py ADDED
@@ -0,0 +1,1840 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import cv2
2
+ import numpy as np
3
+ import torch
4
+ import os
5
+ import json
6
+ import warnings
7
+
8
+ warnings.filterwarnings('ignore')
9
+
10
+ # Import required libraries
11
+ try:
12
+ from ultralytics import YOLO
13
+ from transformers import pipeline
14
+ from PIL import Image, ImageDraw, ImageFont
15
+ import requests
16
+ from datetime import datetime
17
+
18
+ # MediaPipe import with fallback
19
+ try:
20
+ import mediapipe as mp
21
+
22
+ MEDIAPIPE_AVAILABLE = True
23
+ print("โœ… MediaPipe imported successfully")
24
+ except ImportError:
25
+ MEDIAPIPE_AVAILABLE = False
26
+ print("โš ๏ธ MediaPipe not available - pose detection disabled")
27
+ except Exception as e:
28
+ MEDIAPIPE_AVAILABLE = False
29
+ print(f"โš ๏ธ MediaPipe import error: {e} - pose detection disabled")
30
+
31
+ except ImportError as e:
32
+ print(f"Missing dependency: {e}")
33
+ print("Please install: pip install ultralytics transformers pillow requests")
34
+ print("For MediaPipe: pip install mediapipe==0.10.18")
35
+
36
+
37
+ class ContentModerator:
38
+ def __init__(self, config=None):
39
+ default_cfg = self.get_default_config()
40
+ self.config = self.deep_merge_dicts(default_cfg, config) if config else default_cfg
41
+
42
+ self.device = 'cuda' if torch.cuda.is_available() else 'cpu'
43
+
44
+ # CPU optimizations
45
+ if self.device == 'cpu':
46
+ print("๐Ÿ’ป CPU mode detected - applying optimizations...")
47
+ torch.set_num_threads(4)
48
+ self.config['performance']['half_precision'] = False
49
+ self.config['nsfw_detection']['pose_analysis'] = False
50
+
51
+ # Initialize models
52
+ self.weapon_model = None # Primary weapon model
53
+ self.weapon_model_custom = None # Custom model for dao + sรบng + fight
54
+ self.weapon_model_general = None # General model for person + backup weapons
55
+ self.nsfw_classifier = None
56
+ self.pose_detector = None
57
+
58
+ # Performance optimization
59
+ self.detection_cache = {}
60
+ self.cache_ttl = 2 # Cache for 2 seconds
61
+
62
+ # Results storage
63
+ self.detection_history = []
64
+
65
+ print(f"๐Ÿš€ Content Moderator initialized on {self.device}")
66
+ if self.device == 'cpu':
67
+ print("โšก CPU optimizations enabled")
68
+
69
+ self.setup_models()
70
+
71
+ def deep_merge_dicts(self, a: dict, b: dict) -> dict:
72
+ """Merge dict b into a (non-destructive). Returns merged copy.
73
+ - Keeps keys from a when missing in b.
74
+ - Recursively merges nested dicts.
75
+ """
76
+ if not isinstance(a, dict):
77
+ return b if isinstance(b, dict) else a
78
+ result = dict(a) # shallow copy
79
+ if not b:
80
+ return result
81
+ for k, v in b.items():
82
+ if k in result and isinstance(result[k], dict) and isinstance(v, dict):
83
+ result[k] = self.deep_merge_dicts(result[k], v)
84
+ else:
85
+ result[k] = v
86
+ return result
87
+
88
+ def get_default_config(self):
89
+ """Default configuration optimized for CPU/GPU with enhanced knife and fight detection"""
90
+ # Auto-detect optimal settings
91
+ is_cuda = torch.cuda.is_available()
92
+
93
+ return {
94
+ 'weapon_detection': {
95
+ 'enabled': True,
96
+ 'confidence_threshold': 0.45, # For guns
97
+ 'knife_confidence': 0.45, # Lower threshold for knives
98
+ 'fight_confidence': 0.40, # Lower threshold for fights (behavioral)
99
+ 'model_size': 'yolo11n',
100
+ 'classes': ['knife', 'gun', 'rifle', 'pistol', 'weapon', 'fight'],
101
+ 'use_enhancement': True, # Enable image enhancement for knives
102
+ 'multi_pass': True, # Enable multi-pass detection
103
+ 'boost_knife_detection': True, # Enable knife confidence boosting
104
+ 'fight_detection': True, # Enable fight-specific detection
105
+ 'fight_analysis': True # Enable advanced fight behavior analysis
106
+ },
107
+ 'fight_detection': {
108
+ 'enabled': True,
109
+ 'confidence_threshold': 0.40,
110
+ 'pose_analysis': True, # Analyze poses for fighting
111
+ 'motion_analysis': False, # Motion-based fight detection (for video)
112
+ 'aggression_keywords': ['fight', 'violence', 'aggression', 'combat'],
113
+ 'threat_escalation': True, # Escalate threat level for fights
114
+ 'multi_person_analysis': True # Analyze interactions between people
115
+ },
116
+ 'nsfw_detection': {
117
+ 'enabled': True,
118
+ 'confidence_threshold': 0.7,
119
+ 'skin_detection': True,
120
+ 'pose_analysis': False,
121
+ 'region_analysis': True
122
+ },
123
+ 'performance': {
124
+ 'image_size': 416 if is_cuda else 320,
125
+ 'batch_size': 1,
126
+ 'half_precision': is_cuda,
127
+ 'use_flash_attention': False,
128
+ 'cpu_optimization': not is_cuda
129
+ },
130
+ 'output': {
131
+ 'save_detections': True,
132
+ 'draw_boxes': True,
133
+ 'log_results': True
134
+ }
135
+ }
136
+
137
+ def setup_models(self):
138
+ """Initialize all detection models"""
139
+ try:
140
+ # Clear GPU cache
141
+ if torch.cuda.is_available():
142
+ torch.cuda.empty_cache()
143
+
144
+ # 1. Setup Weapon Detection (now includes fight detection)
145
+ if self.config['weapon_detection']['enabled']:
146
+ self.setup_weapon_detector()
147
+
148
+ # 2. Setup NSFW Detection
149
+ if self.config['nsfw_detection']['enabled']:
150
+ self.setup_nsfw_detector()
151
+
152
+ print("โœ… All models loaded successfully!")
153
+
154
+ except Exception as e:
155
+ print(f"โŒ Error setting up models: {e}")
156
+
157
+ def setup_weapon_detector(self):
158
+ """Setup dual model system: Custom for weapons + fights + General for person detection"""
159
+ try:
160
+ print("๐Ÿ”ซ Loading weapon and fight detection models...")
161
+
162
+ # Model 1: Custom YOLO11 for weapons (dao + sรบng + fight)
163
+ custom_model_path = "models/best.pt"
164
+ project_root = os.path.dirname(os.path.abspath(__file__))
165
+ full_model_path = os.path.join(project_root, custom_model_path)
166
+
167
+ if os.path.exists(full_model_path):
168
+ print(f"โœ… Loading custom weapon+fight model: {full_model_path}")
169
+ self.weapon_model_custom = YOLO(full_model_path)
170
+ print("๐ŸŽฏ Custom weapon+fight model (dao + sรบng + fight) loaded!")
171
+
172
+ # Show custom model classes
173
+ if hasattr(self.weapon_model_custom, 'names'):
174
+ classes = list(self.weapon_model_custom.names.values())
175
+ print(f"๐Ÿ“Š Custom classes: {classes}")
176
+
177
+ # Check if fight class is available
178
+ if any('fight' in str(cls).lower() for cls in classes):
179
+ print("๐Ÿ‘Š Fight detection enabled in custom model")
180
+ else:
181
+ print("โš ๏ธ Fight class not found in custom model")
182
+ else:
183
+ print("โš ๏ธ Custom weapon+fight model not found")
184
+ self.weapon_model_custom = None
185
+
186
+ # Model 2: General YOLO11n for person detection and fight fallback
187
+ print("๐Ÿ‘ค Loading general model for person detection...")
188
+ self.weapon_model_general = YOLO('yolo11n.pt')
189
+ print("โœ… General YOLO11n loaded for person detection")
190
+
191
+ # Set primary weapon model
192
+ self.weapon_model = self.weapon_model_custom if self.weapon_model_custom else self.weapon_model_general
193
+
194
+ # Optimize models for performance
195
+ if self.device == 'cuda' and self.config['performance']['half_precision']:
196
+ try:
197
+ if self.weapon_model_custom:
198
+ self.weapon_model_custom.model.half()
199
+ self.weapon_model_general.model.half()
200
+ print("โœ… Half precision enabled for both models")
201
+ except:
202
+ print("โš ๏ธ Half precision not supported")
203
+
204
+ print("๐Ÿ”ฅ Dual model system ready with fight detection!")
205
+
206
+ except Exception as e:
207
+ print(f"โŒ Error loading weapon+fight models: {e}")
208
+ self.weapon_model = None
209
+ self.weapon_model_custom = None
210
+ self.weapon_model_general = None
211
+
212
+ def detect_weapons(self, image):
213
+ """Enhanced dual model weapon and fight detection"""
214
+ detections = []
215
+
216
+ try:
217
+ imgsz = self.config['performance']['image_size']
218
+ use_half = self.config['performance']['half_precision'] and self.device == 'cuda'
219
+
220
+ # Prepare multiple versions of the image
221
+ images_to_process = [(image, 1.0, "original")]
222
+
223
+ if self.config['weapon_detection']['use_enhancement']:
224
+ enhanced_image = self.enhance_knife_detection(image)
225
+ images_to_process.append((enhanced_image, 1.15, "enhanced"))
226
+
227
+ # Process each image version
228
+ for img, weight_multiplier, img_type in images_to_process:
229
+ if self.weapon_model_custom:
230
+ # Use different confidence thresholds for different detection types
231
+ knife_conf = self.config['weapon_detection']['knife_confidence']
232
+ gun_conf = self.config['weapon_detection']['confidence_threshold']
233
+ fight_conf = self.config['weapon_detection']['fight_confidence']
234
+
235
+ # Multi-pass detection with different thresholds
236
+ passes = [
237
+ (knife_conf, "knife_pass"), # Low threshold for knives
238
+ (gun_conf, "gun_pass"), # Normal threshold for guns
239
+ (fight_conf, "fight_pass") # Low threshold for fights
240
+ ] if self.config['weapon_detection']['multi_pass'] else [
241
+ (min(knife_conf, fight_conf), "single_pass")]
242
+
243
+ for conf_threshold, pass_type in passes:
244
+ try:
245
+ results = self.weapon_model_custom(
246
+ img,
247
+ imgsz=imgsz,
248
+ conf=conf_threshold,
249
+ device=self.device,
250
+ half=use_half,
251
+ verbose=False,
252
+ augment=True # Enable test-time augmentation
253
+ )
254
+
255
+ for result in results:
256
+ boxes = result.boxes
257
+ if boxes is not None:
258
+ for box in boxes:
259
+ class_id = int(box.cls[0])
260
+
261
+ if hasattr(result, 'names') and class_id in result.names:
262
+ class_name = result.names[class_id].lower()
263
+ else:
264
+ class_name = f"detection_{class_id}"
265
+
266
+ x1, y1, x2, y2 = box.xyxy[0].cpu().numpy()
267
+ confidence = float(box.conf[0]) * weight_multiplier
268
+
269
+ # Determine detection type and apply appropriate processing
270
+ if self.is_fight_detection(class_name):
271
+ # Fight detection processing
272
+ confidence = self.boost_fight_confidence(
273
+ img, [x1, y1, x2, y2], confidence, class_name
274
+ )
275
+
276
+ detection_type = 'fight'
277
+ min_conf = fight_conf
278
+ threat_level = self.assess_fight_threat(confidence, img, [x1, y1, x2, y2])
279
+
280
+ else:
281
+ # Weapon detection processing
282
+ if self.config['weapon_detection']['boost_knife_detection']:
283
+ if 'dao' in class_name or 'knife' in class_name or 'blade' in class_name:
284
+ confidence = self.boost_knife_confidence(
285
+ img, [x1, y1, x2, y2], confidence, class_name
286
+ )
287
+
288
+ detection_type = 'weapon'
289
+ weapon_type = self.classify_weapon_type(class_name)
290
+ min_conf = knife_conf if weapon_type == 'blade' else gun_conf
291
+ threat_level = self.assess_weapon_threat(weapon_type, confidence)
292
+
293
+ if confidence >= min_conf:
294
+ detection_data = {
295
+ 'type': detection_type,
296
+ 'class': class_name,
297
+ 'confidence': min(confidence, 0.99),
298
+ 'bbox': [int(x1), int(y1), int(x2), int(y2)],
299
+ 'threat_level': threat_level,
300
+ 'detection_method': f'custom_model_{img_type}_{pass_type}'
301
+ }
302
+
303
+ # Add type-specific fields
304
+ if detection_type == 'weapon':
305
+ detection_data['weapon_type'] = weapon_type
306
+ elif detection_type == 'fight':
307
+ detection_data['fight_type'] = self.classify_fight_type(class_name)
308
+ detection_data['aggression_level'] = self.assess_aggression_level(
309
+ confidence)
310
+
311
+ detections.append(detection_data)
312
+
313
+ icon = "๐Ÿ‘Š" if detection_type == 'fight' else "๐ŸŽฏ"
314
+ print(
315
+ f" {icon} Detected: {class_name} (conf: {confidence:.3f}, method: {img_type}_{pass_type})")
316
+
317
+ except Exception as e:
318
+ print(f"โš ๏ธ Detection pass error ({pass_type}): {e}")
319
+
320
+ # Fallback: General model for backup detection (only if no custom detections)
321
+ if self.weapon_model_general and len(detections) == 0:
322
+ detections.extend(self.fallback_detection(image, imgsz, use_half))
323
+
324
+ # Remove duplicate detections
325
+ detections = self.remove_duplicate_detections(detections)
326
+
327
+ # Additional fight analysis if enabled
328
+ # Additional fight analysis if enabled (safe access)
329
+ try:
330
+ # weapon_detection may contain a simple boolean or a dict for fight_detection
331
+ wd = self.config.get('weapon_detection', {})
332
+ wd_fd = wd.get('fight_detection', None)
333
+
334
+ # Determine whether fight analysis is enabled
335
+ if isinstance(wd_fd, dict):
336
+ fight_enabled = wd_fd.get('enabled', False)
337
+ fight_multi_person = wd_fd.get('multi_person_analysis', False)
338
+ else:
339
+ # wd_fd may be boolean (legacy); consult top-level fight_detection dict for details
340
+ fight_enabled = bool(wd_fd)
341
+ fight_multi_person = bool(
342
+ self.config.get('fight_detection', {}).get('multi_person_analysis', False))
343
+
344
+ if fight_enabled and fight_multi_person:
345
+ fight_detections = [d for d in detections if d.get('type') == 'fight']
346
+ if fight_detections:
347
+ try:
348
+ enhanced_fights = self.analyze_fight_context(image, fight_detections)
349
+ # Replace original fight detections with enhanced ones
350
+ detections = [d for d in detections if d.get('type') != 'fight'] + enhanced_fights
351
+ except Exception as e:
352
+ print(f"โš ๏ธ Fight context enhancement error: {e}")
353
+ except Exception as e:
354
+ # Defensive: never allow missing config to break detection pipeline
355
+ import traceback
356
+ traceback.print_exc()
357
+ print(f"โš ๏ธ Skipping fight context analysis due to config error: {e}")
358
+
359
+ return detections
360
+
361
+ except Exception as e:
362
+ print(f"โŒ Weapon and fight detection error: {e}")
363
+ return []
364
+
365
+ def is_fight_detection(self, class_name):
366
+ """Check if detection is fight-related"""
367
+ fight_keywords = ['fight', 'fighting', 'combat', 'violence', 'aggression', 'brawl', 'scuffle']
368
+ return any(keyword in class_name.lower() for keyword in fight_keywords)
369
+
370
+ def classify_fight_type(self, class_name):
371
+ """Classify type of fight detected"""
372
+ class_name = class_name.lower()
373
+
374
+ if any(word in class_name for word in ['punch', 'boxing', 'fist']):
375
+ return 'physical_combat'
376
+ elif any(word in class_name for word in ['kick', 'martial', 'karate']):
377
+ return 'martial_arts'
378
+ elif any(word in class_name for word in ['wrestle', 'grapple']):
379
+ return 'wrestling'
380
+ elif any(word in class_name for word in ['group', 'mob', 'crowd']):
381
+ return 'group_violence'
382
+ else:
383
+ return 'general_fight'
384
+
385
+ def boost_fight_confidence(self, image, bbox, initial_confidence, class_name):
386
+ """Boost confidence for fight detection based on contextual analysis"""
387
+ try:
388
+ x1, y1, x2, y2 = [int(coord) for coord in bbox]
389
+
390
+ # Ensure bbox is within image bounds
391
+ x1 = max(0, x1)
392
+ y1 = max(0, y1)
393
+ x2 = min(image.shape[1], x2)
394
+ y2 = min(image.shape[0], y2)
395
+
396
+ roi = image[y1:y2, x1:x2]
397
+
398
+ if roi.size == 0:
399
+ return initial_confidence
400
+
401
+ boost = 0
402
+
403
+ # 1. Motion blur analysis (indicates rapid movement)
404
+ gray = cv2.cvtColor(roi, cv2.COLOR_BGR2GRAY)
405
+ blur_variance = cv2.Laplacian(gray, cv2.CV_64F).var()
406
+ if blur_variance < 100: # Low variance indicates blur/motion
407
+ boost += 0.10
408
+
409
+ # 2. Edge density (chaotic scenes have more edges)
410
+ edges = cv2.Canny(gray, 50, 150)
411
+ edge_density = np.count_nonzero(edges) / edges.size
412
+ if edge_density > 0.15:
413
+ boost += 0.08
414
+
415
+ # 3. Color analysis (fights often have varied, chaotic colors)
416
+ hsv = cv2.cvtColor(roi, cv2.COLOR_BGR2HSV)
417
+ color_variance = np.var(hsv[:, :, 1]) # Saturation variance
418
+ if color_variance > 1000:
419
+ boost += 0.05
420
+
421
+ # 4. Texture analysis (complex textures indicate multiple overlapping objects)
422
+ gray_f = np.float32(gray)
423
+ texture_response = cv2.cornerHarris(gray_f, 2, 3, 0.04)
424
+ texture_strength = np.mean(texture_response)
425
+ if texture_strength > 0.01:
426
+ boost += 0.07
427
+
428
+ # 5. Aspect ratio analysis (fights often have irregular bounding boxes)
429
+ height = y2 - y1
430
+ width = x2 - x1
431
+ if height > 0 and width > 0:
432
+ aspect_ratio = max(width, height) / min(width, height)
433
+ if 1.2 < aspect_ratio < 3.0: # Moderate irregularity
434
+ boost += 0.05
435
+
436
+ final_confidence = min(initial_confidence + boost, 0.95)
437
+
438
+ if boost > 0:
439
+ print(f" ๐Ÿ‘Š Fight boost applied: +{boost:.2f} (blur:{blur_variance:.0f}, edge:{edge_density:.2f})")
440
+
441
+ return final_confidence
442
+
443
+ except Exception as e:
444
+ print(f"โš ๏ธ Fight confidence boost error: {e}")
445
+ return initial_confidence
446
+
447
+ def assess_fight_threat(self, confidence, image, bbox):
448
+ """Assess threat level of detected fight"""
449
+ base_threat = 'medium' # Fights start at medium threat
450
+
451
+ # Escalate based on confidence
452
+ if confidence >= 0.85:
453
+ base_threat = 'critical'
454
+ elif confidence >= 0.70:
455
+ base_threat = 'high'
456
+ elif confidence >= 0.50:
457
+ base_threat = 'medium'
458
+ else:
459
+ base_threat = 'low'
460
+
461
+ # Additional context-based escalation
462
+ try:
463
+ x1, y1, x2, y2 = bbox
464
+ fight_area = (x2 - x1) * (y2 - y1)
465
+ image_area = image.shape[0] * image.shape[1]
466
+ area_ratio = fight_area / image_area
467
+
468
+ # Large fights are more dangerous
469
+ if area_ratio > 0.5: # Fight covers >50% of image
470
+ if base_threat == 'medium':
471
+ base_threat = 'high'
472
+ elif base_threat == 'high':
473
+ base_threat = 'critical'
474
+
475
+ except Exception as e:
476
+ print(f"โš ๏ธ Fight threat assessment error: {e}")
477
+
478
+ return base_threat
479
+
480
+ def assess_aggression_level(self, confidence):
481
+ """Assess aggression level based on confidence"""
482
+ if confidence >= 0.80:
483
+ return 'extreme'
484
+ elif confidence >= 0.65:
485
+ return 'high'
486
+ elif confidence >= 0.45:
487
+ return 'moderate'
488
+ else:
489
+ return 'low'
490
+
491
+ def analyze_fight_context(self, image, fight_detections):
492
+ """Enhanced analysis of fight context with multi-person detection"""
493
+ enhanced_fights = []
494
+
495
+ try:
496
+ # Detect all persons in the image
497
+ persons = self.detect_persons(image)
498
+
499
+ for fight in fight_detections:
500
+ enhanced_fight = fight.copy()
501
+
502
+ # Count people involved in or near the fight
503
+ fight_bbox = fight['bbox']
504
+ people_in_fight = 0
505
+ people_nearby = 0
506
+
507
+ for person in persons:
508
+ person_bbox = person['bbox']
509
+
510
+ # Calculate overlap with fight area
511
+ overlap = self.calculate_bbox_overlap(fight_bbox, person_bbox)
512
+
513
+ if overlap > 0.3: # Person is directly involved
514
+ people_in_fight += 1
515
+ elif overlap > 0.1: # Person is nearby
516
+ people_nearby += 1
517
+
518
+ # Update fight information based on context
519
+ enhanced_fight['people_involved'] = people_in_fight
520
+ enhanced_fight['people_nearby'] = people_nearby
521
+ enhanced_fight['total_people'] = people_in_fight + people_nearby
522
+
523
+ # Escalate threat based on number of people
524
+ if people_in_fight >= 3:
525
+ if enhanced_fight['threat_level'] == 'medium':
526
+ enhanced_fight['threat_level'] = 'high'
527
+ elif enhanced_fight['threat_level'] == 'high':
528
+ enhanced_fight['threat_level'] = 'critical'
529
+ enhanced_fight['fight_type'] = 'group_violence'
530
+
531
+ # Add context flags
532
+ enhanced_fight['context_flags'] = []
533
+ if people_in_fight >= 3:
534
+ enhanced_fight['context_flags'].append('multi_person_fight')
535
+ if people_nearby >= 2:
536
+ enhanced_fight['context_flags'].append('crowd_present')
537
+
538
+ enhanced_fights.append(enhanced_fight)
539
+
540
+ print(f" ๐Ÿ‘ฅ Fight context: {people_in_fight} involved, {people_nearby} nearby")
541
+
542
+ except Exception as e:
543
+ print(f"โš ๏ธ Fight context analysis error: {e}")
544
+ return fight_detections
545
+
546
+ return enhanced_fights
547
+
548
+ def calculate_bbox_overlap(self, bbox1, bbox2):
549
+ """Calculate overlap ratio between two bounding boxes"""
550
+ x1_min, y1_min, x1_max, y1_max = bbox1
551
+ x2_min, y2_min, x2_max, y2_max = bbox2
552
+
553
+ # Calculate intersection
554
+ intersect_xmin = max(x1_min, x2_min)
555
+ intersect_ymin = max(y1_min, y2_min)
556
+ intersect_xmax = min(x1_max, x2_max)
557
+ intersect_ymax = min(y1_max, y2_max)
558
+
559
+ if intersect_xmax < intersect_xmin or intersect_ymax < intersect_ymin:
560
+ return 0.0
561
+
562
+ intersect_area = (intersect_xmax - intersect_xmin) * (intersect_ymax - intersect_ymin)
563
+ bbox1_area = (x1_max - x1_min) * (y1_max - y1_min)
564
+
565
+ return intersect_area / bbox1_area if bbox1_area > 0 else 0
566
+
567
+ def fallback_detection(self, image, imgsz, use_half):
568
+ """Fallback detection using general model"""
569
+ detections = []
570
+
571
+ try:
572
+ general_results = self.weapon_model_general(
573
+ image,
574
+ imgsz=imgsz,
575
+ conf=0.4,
576
+ device=self.device,
577
+ half=use_half,
578
+ verbose=False
579
+ )
580
+
581
+ for result in general_results:
582
+ boxes = result.boxes
583
+ if boxes is not None:
584
+ for box in boxes:
585
+ class_id = int(box.cls[0])
586
+ class_name = result.names[class_id].lower()
587
+
588
+ # Filter for weapon-like objects
589
+ weapon_keywords = ['knife', 'scissors', 'fork']
590
+
591
+ if any(keyword in class_name for keyword in weapon_keywords):
592
+ x1, y1, x2, y2 = box.xyxy[0].cpu().numpy()
593
+ confidence = float(box.conf[0])
594
+
595
+ detections.append({
596
+ 'type': 'weapon',
597
+ 'class': class_name,
598
+ 'weapon_type': 'blade',
599
+ 'confidence': confidence,
600
+ 'bbox': [int(x1), int(y1), int(x2), int(y2)],
601
+ 'threat_level': self.assess_weapon_threat('blade', confidence),
602
+ 'detection_method': 'general_model_fallback'
603
+ })
604
+
605
+ except Exception as e:
606
+ print(f"โš ๏ธ General detection error: {e}")
607
+
608
+ return detections
609
+
610
+ # ... (rest of the existing methods remain the same) ...
611
+
612
+ def enhance_knife_detection(self, image):
613
+ """Enhance image specifically for better knife/dao detection"""
614
+ try:
615
+ # 1. Increase contrast and brightness for metallic objects
616
+ enhanced = cv2.convertScaleAbs(image, alpha=1.4, beta=25)
617
+
618
+ # 2. Apply sharpening kernel to highlight edges
619
+ kernel_sharpen = np.array([[-1, -1, -1],
620
+ [-1, 9, -1],
621
+ [-1, -1, -1]])
622
+ sharpened = cv2.filter2D(enhanced, -1, kernel_sharpen)
623
+
624
+ # 3. Apply CLAHE for better local contrast
625
+ lab = cv2.cvtColor(sharpened, cv2.COLOR_BGR2LAB)
626
+ l, a, b = cv2.split(lab)
627
+ clahe = cv2.createCLAHE(clipLimit=3.0, tileGridSize=(8, 8))
628
+ l = clahe.apply(l)
629
+ enhanced_final = cv2.merge([l, a, b])
630
+ enhanced_final = cv2.cvtColor(enhanced_final, cv2.COLOR_LAB2BGR)
631
+
632
+ return enhanced_final
633
+ except Exception as e:
634
+ print(f"โš ๏ธ Enhancement failed: {e}")
635
+ return image
636
+
637
+ def boost_knife_confidence(self, image, bbox, initial_confidence, class_name):
638
+ """Boost confidence for knife/dao based on geometric and visual features"""
639
+ try:
640
+ x1, y1, x2, y2 = [int(coord) for coord in bbox]
641
+
642
+ # Ensure bbox is within image bounds
643
+ x1 = max(0, x1)
644
+ y1 = max(0, y1)
645
+ x2 = min(image.shape[1], x2)
646
+ y2 = min(image.shape[0], y2)
647
+
648
+ roi = image[y1:y2, x1:x2]
649
+
650
+ if roi.size == 0:
651
+ return initial_confidence
652
+
653
+ boost = 0
654
+
655
+ # 1. Check aspect ratio (knives are typically elongated)
656
+ height = y2 - y1
657
+ width = x2 - x1
658
+ if height > 0 and width > 0:
659
+ aspect_ratio = max(width, height) / min(width, height)
660
+ if aspect_ratio > 2.5: # Elongated shape
661
+ boost += 0.15
662
+ elif aspect_ratio > 2.0:
663
+ boost += 0.10
664
+
665
+ # 2. Check for metallic reflection (brightness)
666
+ gray = cv2.cvtColor(roi, cv2.COLOR_BGR2GRAY)
667
+ mean_brightness = np.mean(gray)
668
+ std_brightness = np.std(gray)
669
+
670
+ if mean_brightness > 140: # Bright (metallic)
671
+ boost += 0.10
672
+ if std_brightness > 50: # High contrast (blade edge)
673
+ boost += 0.05
674
+
675
+ # 3. Edge detection (knives have strong edges)
676
+ edges = cv2.Canny(gray, 50, 150)
677
+ edge_ratio = np.count_nonzero(edges) / edges.size
678
+ if edge_ratio > 0.15: # Strong edges
679
+ boost += 0.10
680
+ elif edge_ratio > 0.10:
681
+ boost += 0.05
682
+
683
+ # 4. Check for blade-like gradient
684
+ if height > width: # Vertical orientation
685
+ gradient = np.gradient(gray, axis=0)
686
+ else: # Horizontal orientation
687
+ gradient = np.gradient(gray, axis=1)
688
+
689
+ gradient_strength = np.mean(np.abs(gradient))
690
+ if gradient_strength > 10:
691
+ boost += 0.05
692
+
693
+ # Apply boost with class-specific multiplier
694
+ if 'dao' in class_name.lower() or 'knife' in class_name.lower():
695
+ boost *= 1.2 # Extra boost for knife/dao classes
696
+
697
+ final_confidence = min(initial_confidence + boost, 0.95)
698
+
699
+ if boost > 0:
700
+ print(
701
+ f" ๐Ÿ”ช Knife boost applied: +{boost:.2f} (AR:{aspect_ratio:.1f}, Bright:{mean_brightness:.0f}, Edge:{edge_ratio:.2f})")
702
+
703
+ return final_confidence
704
+
705
+ except Exception as e:
706
+ print(f"โš ๏ธ Confidence boost error: {e}")
707
+ return initial_confidence
708
+
709
+ def detect_persons(self, image):
710
+ """Detect persons using general model (needed for NSFW and fight analysis)"""
711
+ persons = []
712
+
713
+ if not self.weapon_model_general:
714
+ return persons
715
+
716
+ try:
717
+ imgsz = self.config['performance']['image_size']
718
+ use_half = self.config['performance']['half_precision'] and self.device == 'cuda'
719
+
720
+ results = self.weapon_model_general(
721
+ image,
722
+ imgsz=imgsz,
723
+ conf=0.3,
724
+ device=self.device,
725
+ half=use_half,
726
+ verbose=False
727
+ )
728
+
729
+ for result in results:
730
+ boxes = result.boxes
731
+ if boxes is not None:
732
+ for box in boxes:
733
+ class_id = int(box.cls[0])
734
+ class_name = result.names[class_id].lower()
735
+
736
+ if class_name == 'person':
737
+ x1, y1, x2, y2 = box.xyxy[0].cpu().numpy()
738
+ confidence = float(box.conf[0])
739
+
740
+ persons.append({
741
+ 'class': 'person',
742
+ 'confidence': confidence,
743
+ 'bbox': [int(x1), int(y1), int(x2), int(y2)]
744
+ })
745
+
746
+ return persons
747
+
748
+ except Exception as e:
749
+ print(f"โŒ Person detection error: {e}")
750
+ return []
751
+
752
+ def classify_weapon_type(self, class_name):
753
+ """Classify weapon type from class name"""
754
+ class_name = class_name.lower()
755
+
756
+ # Knife/Blade keywords (expanded)
757
+ knife_keywords = ['knife', 'dao', 'blade', 'dagger', 'sword', 'machete', 'katana', 'cutter']
758
+ if any(keyword in class_name for keyword in knife_keywords):
759
+ return 'blade'
760
+
761
+ # Gun/Firearm keywords
762
+ gun_keywords = ['gun', 'pistol', 'rifle', 'firearm', 'revolver', 'shotgun', 'sรบng']
763
+ if any(keyword in class_name for keyword in gun_keywords):
764
+ return 'firearm'
765
+
766
+ # Other weapons
767
+ other_keywords = ['axe', 'hammer', 'club', 'bat']
768
+ if any(keyword in class_name for keyword in other_keywords):
769
+ return 'blunt_weapon'
770
+
771
+ # Check for numbered weapon classes
772
+ if 'weapon' in class_name:
773
+ try:
774
+ weapon_id = int(class_name.split('_')[-1])
775
+ if weapon_id in [0, 1]: # Assuming 0,1 are firearms
776
+ return 'firearm'
777
+ elif weapon_id in [2, 3]: # Assuming 2,3 are blades
778
+ return 'blade'
779
+ else:
780
+ return 'unknown_weapon'
781
+ except:
782
+ pass
783
+
784
+ return 'unknown_weapon'
785
+
786
+ def assess_weapon_threat(self, weapon_type, confidence):
787
+ """Assess threat level of detected weapon"""
788
+ threat_levels = {
789
+ 'firearm': 'critical',
790
+ 'blade': 'high',
791
+ 'blunt_weapon': 'medium',
792
+ 'unknown_weapon': 'medium'
793
+ }
794
+
795
+ base_threat = threat_levels.get(weapon_type, 'medium')
796
+
797
+ # Adjust based on confidence
798
+ if confidence >= 0.9:
799
+ if base_threat == 'medium':
800
+ return 'high'
801
+ elif base_threat == 'high':
802
+ return 'critical'
803
+ else:
804
+ return base_threat
805
+ elif confidence >= 0.7:
806
+ return base_threat
807
+ elif confidence >= 0.5:
808
+ if base_threat == 'critical':
809
+ return 'high'
810
+ elif base_threat == 'high':
811
+ return 'medium'
812
+ else:
813
+ return base_threat
814
+ else:
815
+ if base_threat == 'critical':
816
+ return 'medium'
817
+ elif base_threat == 'high':
818
+ return 'low'
819
+ else:
820
+ return 'low'
821
+
822
+ def remove_duplicate_detections(self, detections, iou_threshold=0.4):
823
+ """Remove duplicate detections using Non-Maximum Suppression"""
824
+ if len(detections) <= 1:
825
+ return detections
826
+
827
+ # Sort by confidence (highest first)
828
+ detections = sorted(detections, key=lambda x: x['confidence'], reverse=True)
829
+
830
+ keep = []
831
+ for i, det1 in enumerate(detections):
832
+ should_keep = True
833
+ for det2 in keep:
834
+ # Check if same type and overlapping
835
+ if det1['type'] == det2['type']:
836
+ iou = self.calculate_iou(det1['bbox'], det2['bbox'])
837
+ if iou > iou_threshold:
838
+ should_keep = False
839
+ break
840
+
841
+ if should_keep:
842
+ keep.append(det1)
843
+
844
+ return keep
845
+
846
+ def calculate_iou(self, box1, box2):
847
+ """Calculate Intersection over Union between two bounding boxes"""
848
+ x1_min, y1_min, x1_max, y1_max = box1
849
+ x2_min, y2_min, x2_max, y2_max = box2
850
+
851
+ # Calculate intersection
852
+ intersect_xmin = max(x1_min, x2_min)
853
+ intersect_ymin = max(y1_min, y2_min)
854
+ intersect_xmax = min(x1_max, x2_max)
855
+ intersect_ymax = min(y1_max, y2_max)
856
+
857
+ if intersect_xmax < intersect_xmin or intersect_ymax < intersect_ymin:
858
+ return 0.0
859
+
860
+ intersect_area = (intersect_xmax - intersect_xmin) * (intersect_ymax - intersect_ymin)
861
+
862
+ # Calculate union
863
+ box1_area = (x1_max - x1_min) * (y1_max - y1_min)
864
+ box2_area = (x2_max - x2_min) * (y2_max - y2_min)
865
+ union_area = box1_area + box2_area - intersect_area
866
+
867
+ return intersect_area / union_area if union_area > 0 else 0
868
+
869
+ # ... (continue with remaining NSFW detection methods) ...
870
+
871
+ def detect_nsfw_content(self, image):
872
+ """Enhanced NSFW detection with person detection first"""
873
+ detections = []
874
+
875
+ try:
876
+ if len(image.shape) == 3 and image.shape[2] == 3:
877
+ rgb_image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
878
+ else:
879
+ rgb_image = image
880
+
881
+ # Stage 1: Detect persons first (optimization)
882
+ persons = self.detect_persons(image)
883
+
884
+ if not persons:
885
+ # No persons detected, skip detailed NSFW analysis
886
+ return detections
887
+
888
+ print(f"๐Ÿ‘ค Found {len(persons)} person(s), analyzing for NSFW content...")
889
+
890
+ # Stage 2: Overall NSFW Classification
891
+ if self.nsfw_classifier:
892
+ try:
893
+ pil_image = Image.fromarray(rgb_image)
894
+ nsfw_result = self.nsfw_classifier(pil_image)
895
+
896
+ if nsfw_result[0]['label'] == 'nsfw':
897
+ confidence = nsfw_result[0]['score']
898
+ if confidence > self.config['nsfw_detection']['confidence_threshold']:
899
+ detections.append({
900
+ 'type': 'nsfw',
901
+ 'class': 'inappropriate_content',
902
+ 'confidence': confidence,
903
+ 'bbox': [0, 0, image.shape[1], image.shape[0]],
904
+ 'method': 'classification'
905
+ })
906
+ except Exception as e:
907
+ print(f"โš ๏ธ NSFW classifier error: {e}")
908
+
909
+ # Stage 3: Person-specific skin analysis
910
+ if self.config['nsfw_detection']['skin_detection']:
911
+ for person in persons:
912
+ person_detections = self.analyze_person_skin(image, person)
913
+ detections.extend(person_detections)
914
+
915
+ # Stage 4: Regional skin analysis (if no person-specific detections)
916
+ if self.config['nsfw_detection']['region_analysis'] and len(detections) == 0:
917
+ skin_detections = self.detect_skin_regions(image)
918
+ detections.extend(skin_detections)
919
+
920
+ return detections
921
+
922
+ except Exception as e:
923
+ print(f"โŒ NSFW detection error: {e}")
924
+ return []
925
+
926
+ def analyze_person_skin(self, image, person):
927
+ """Analyze skin exposure for a specific person"""
928
+ detections = []
929
+
930
+ try:
931
+ x1, y1, x2, y2 = person['bbox']
932
+ person_region = image[y1:y2, x1:x2]
933
+
934
+ if person_region.size == 0:
935
+ return detections
936
+
937
+ # Convert to HSV for skin detection
938
+ hsv_person = cv2.cvtColor(person_region, cv2.COLOR_BGR2HSV)
939
+
940
+ # Skin color range
941
+ lower_skin = np.array([0, 20, 70], dtype=np.uint8)
942
+ upper_skin = np.array([20, 255, 255], dtype=np.uint8)
943
+
944
+ # Create skin mask
945
+ skin_mask = cv2.inRange(hsv_person, lower_skin, upper_skin)
946
+
947
+ # Calculate skin percentage
948
+ total_person_pixels = person_region.shape[0] * person_region.shape[1]
949
+ skin_pixels = cv2.countNonZero(skin_mask)
950
+ skin_ratio = skin_pixels / total_person_pixels if total_person_pixels > 0 else 0
951
+
952
+ # Threshold for suspicious skin exposure
953
+ if skin_ratio > 0.4: # 40% of person region is skin
954
+ confidence = min(skin_ratio * 2, 1.0)
955
+
956
+ detections.append({
957
+ 'type': 'nsfw',
958
+ 'class': 'excessive_skin_exposure',
959
+ 'confidence': confidence,
960
+ 'bbox': [x1, y1, x2, y2],
961
+ 'method': 'person_skin_analysis',
962
+ 'skin_ratio': skin_ratio
963
+ })
964
+
965
+ print(f"๐Ÿšจ Excessive skin exposure detected: {skin_ratio:.2f} ratio")
966
+
967
+ return detections
968
+
969
+ except Exception as e:
970
+ print(f"โŒ Person skin analysis error: {e}")
971
+ return []
972
+
973
+ def detect_skin_regions(self, image):
974
+ """Detect large skin-colored regions"""
975
+ try:
976
+ hsv = cv2.cvtColor(image, cv2.COLOR_BGR2HSV)
977
+
978
+ # Define skin color range
979
+ lower_skin = np.array([0, 20, 70], dtype=np.uint8)
980
+ upper_skin = np.array([20, 255, 255], dtype=np.uint8)
981
+
982
+ # Create skin mask
983
+ skin_mask = cv2.inRange(hsv, lower_skin, upper_skin)
984
+
985
+ # Apply morphological operations
986
+ kernel = np.ones((3, 3), np.uint8)
987
+ skin_mask = cv2.morphologyEx(skin_mask, cv2.MORPH_OPEN, kernel)
988
+ skin_mask = cv2.morphologyEx(skin_mask, cv2.MORPH_CLOSE, kernel)
989
+
990
+ # Find contours
991
+ contours, _ = cv2.findContours(skin_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
992
+
993
+ detections = []
994
+ image_area = image.shape[0] * image.shape[1]
995
+
996
+ for contour in contours:
997
+ area = cv2.contourArea(contour)
998
+
999
+ # If skin region is too large
1000
+ if area > image_area * 0.3:
1001
+ x, y, w, h = cv2.boundingRect(contour)
1002
+ confidence = min(area / image_area, 1.0)
1003
+
1004
+ detections.append({
1005
+ 'type': 'nsfw',
1006
+ 'class': 'large_skin_region',
1007
+ 'confidence': confidence,
1008
+ 'bbox': [x, y, x + w, y + h],
1009
+ 'method': 'skin_detection'
1010
+ })
1011
+
1012
+ return detections
1013
+
1014
+ except Exception as e:
1015
+ print(f"โŒ Skin detection error: {e}")
1016
+ return []
1017
+
1018
+ def setup_nsfw_detector(self):
1019
+ """Setup NSFW detection components (Optimized for CPU)"""
1020
+ try:
1021
+ print("๐Ÿ”ž Loading NSFW detection components...")
1022
+
1023
+ # 1. NSFW Classifier (Optimized for CPU)
1024
+ try:
1025
+ device_id = 0 if self.device == 'cuda' else -1
1026
+ self.nsfw_classifier = pipeline(
1027
+ "image-classification",
1028
+ model="Falconsai/nsfw_image_detection",
1029
+ device=device_id,
1030
+ use_fast=True
1031
+ )
1032
+ print("โœ… NSFW classifier loaded")
1033
+ except Exception as nsfw_error:
1034
+ print(f"โš ๏ธ NSFW classifier failed: {nsfw_error}")
1035
+ print(" Trying backup method...")
1036
+ try:
1037
+ # Fallback without specifying use_fast
1038
+ self.nsfw_classifier = pipeline(
1039
+ "image-classification",
1040
+ model="Falconsai/nsfw_image_detection",
1041
+ device=device_id
1042
+ )
1043
+ print("โœ… NSFW classifier loaded (fallback)")
1044
+ except:
1045
+ print("โŒ NSFW classifier completely failed")
1046
+ self.nsfw_classifier = None
1047
+
1048
+ # 2. Pose Detection (Fixed import with fallbacks)
1049
+ if self.config['nsfw_detection']['pose_analysis'] and MEDIAPIPE_AVAILABLE:
1050
+ try:
1051
+ import mediapipe as mp
1052
+ try:
1053
+ mp_pose = mp.solutions.pose
1054
+ self.pose_detector = mp_pose.Pose(
1055
+ static_image_mode=True,
1056
+ model_complexity=0,
1057
+ min_detection_confidence=0.5
1058
+ )
1059
+ print("โœ… Pose detector loaded (legacy API)")
1060
+ except AttributeError:
1061
+ print("โš ๏ธ MediaPipe API not available")
1062
+ self.pose_detector = None
1063
+ self.config['nsfw_detection']['pose_analysis'] = False
1064
+
1065
+ except Exception as pose_error:
1066
+ print(f"โš ๏ธ Pose detection failed: {pose_error}")
1067
+ self.pose_detector = None
1068
+ self.config['nsfw_detection']['pose_analysis'] = False
1069
+ else:
1070
+ self.pose_detector = None
1071
+ if not MEDIAPIPE_AVAILABLE:
1072
+ print("โš ๏ธ MediaPipe not available - pose analysis disabled")
1073
+
1074
+ except Exception as e:
1075
+ print(f"โŒ Error loading NSFW components: {e}")
1076
+ print("๐Ÿ’ก Falling back to skin detection only")
1077
+
1078
+ def process_image(self, image_path):
1079
+ """Process single image with enhanced detection including fights"""
1080
+ try:
1081
+ # Load image
1082
+ if isinstance(image_path, str):
1083
+ image = cv2.imread(image_path)
1084
+ if image is None:
1085
+ raise ValueError(f"Could not load image: {image_path}")
1086
+ cache_key = f"file_{image_path}"
1087
+ else:
1088
+ image = image_path
1089
+ cache_key = f"array_{hash(image.tobytes())}"
1090
+
1091
+ # Check cache
1092
+ import time
1093
+ current_time = time.time()
1094
+ if cache_key in self.detection_cache:
1095
+ cached_result, timestamp = self.detection_cache[cache_key]
1096
+ if current_time - timestamp < self.cache_ttl:
1097
+ return cached_result
1098
+
1099
+ print(f"๐Ÿ“ธ Processing image: {image.shape}")
1100
+
1101
+ # Run detections
1102
+ all_detections = []
1103
+
1104
+ # Weapon and fight detection
1105
+ if self.config['weapon_detection']['enabled']:
1106
+ weapon_fight_detections = self.detect_weapons(image)
1107
+ all_detections.extend(weapon_fight_detections)
1108
+
1109
+ weapon_detections = [d for d in weapon_fight_detections if d['type'] == 'weapon']
1110
+ fight_detections = [d for d in weapon_fight_detections if d['type'] == 'fight']
1111
+
1112
+ print(f"๐Ÿ”ซ Found {len(weapon_detections)} weapon(s)")
1113
+ print(f"๐Ÿ‘Š Found {len(fight_detections)} fight(s)")
1114
+
1115
+ # Show detailed breakdown
1116
+ if weapon_detections:
1117
+ knife_detections = [d for d in weapon_detections if d['weapon_type'] == 'blade']
1118
+ if knife_detections:
1119
+ print(f" ๐Ÿ”ช Including {len(knife_detections)} knife/dao detection(s)")
1120
+
1121
+ if fight_detections:
1122
+ for fight in fight_detections:
1123
+ fight_type = fight.get('fight_type', 'unknown')
1124
+ aggression = fight.get('aggression_level', 'unknown')
1125
+ print(f" ๐Ÿ‘Š Fight: {fight_type} (aggression: {aggression})")
1126
+
1127
+ # NSFW detection
1128
+ if self.config['nsfw_detection']['enabled']:
1129
+ nsfw_detections = self.detect_nsfw_content(image)
1130
+ all_detections.extend(nsfw_detections)
1131
+ print(f"๐Ÿ”ž Found {len(nsfw_detections)} NSFW detection(s)")
1132
+
1133
+ # Generate result
1134
+ result = {
1135
+ 'timestamp': datetime.now().isoformat(),
1136
+ 'image_path': image_path if isinstance(image_path, str) else 'array',
1137
+ 'detections': all_detections,
1138
+ 'total_threats': len(all_detections),
1139
+ 'risk_level': self.calculate_risk_level(all_detections),
1140
+ 'action_required': len(all_detections) > 0,
1141
+ 'processing_method': 'enhanced_dual_model_with_fight',
1142
+ 'detection_breakdown': {
1143
+ 'weapons': len([d for d in all_detections if d['type'] == 'weapon']),
1144
+ 'fights': len([d for d in all_detections if d['type'] == 'fight']),
1145
+ 'nsfw': len([d for d in all_detections if d['type'] == 'nsfw'])
1146
+ }
1147
+ }
1148
+
1149
+ # Cache result
1150
+ self.detection_cache[cache_key] = (result, current_time)
1151
+
1152
+ # Clean old cache entries
1153
+ self.clean_cache(current_time)
1154
+
1155
+ # Save detection history
1156
+ self.detection_history.append(result)
1157
+
1158
+ # Draw detections
1159
+ if self.config['output']['draw_boxes'] and all_detections:
1160
+ annotated_image = self.draw_detections(image.copy(), all_detections)
1161
+ result['annotated_image'] = annotated_image
1162
+
1163
+ return result
1164
+
1165
+ except Exception as e:
1166
+ print(f"โŒ Error processing image: {e}")
1167
+ return None
1168
+
1169
+ def clean_cache(self, current_time):
1170
+ """Clean expired cache entries"""
1171
+ try:
1172
+ expired_keys = []
1173
+ for key, (_, timestamp) in self.detection_cache.items():
1174
+ if current_time - timestamp > self.cache_ttl:
1175
+ expired_keys.append(key)
1176
+
1177
+ for key in expired_keys:
1178
+ del self.detection_cache[key]
1179
+
1180
+ except Exception as e:
1181
+ print(f"โš ๏ธ Cache cleanup error: {e}")
1182
+
1183
+ def get_model_status(self):
1184
+ """Get status of all models (safe access to config keys)."""
1185
+ # weapon_detection subtree
1186
+ wd = self.config.get('weapon_detection', {})
1187
+ # fight_detection may be a bool in weapon_detection (legacy) or a dict (detailed).
1188
+ wd_fd = wd.get('fight_detection', None)
1189
+ if isinstance(wd_fd, dict):
1190
+ fight_enabled = wd_fd.get('enabled', True)
1191
+ else:
1192
+ fight_enabled = bool(wd_fd)
1193
+
1194
+ # fight analysis flag (either in weapon_detection or top-level fight_detection)
1195
+ fight_analysis_flag = wd.get('fight_analysis', False) or \
1196
+ bool(self.config.get('fight_detection', {}).get('multi_person_analysis', False))
1197
+
1198
+ status = {
1199
+ 'fight_detection': fight_enabled,
1200
+ 'custom_weapon_fight_model': self.weapon_model_custom is not None,
1201
+ 'general_model': self.weapon_model_general is not None,
1202
+ 'nsfw_classifier': self.nsfw_classifier is not None,
1203
+ 'pose_detector': self.pose_detector is not None,
1204
+ 'device': self.device,
1205
+ 'cache_size': len(self.detection_cache),
1206
+ 'knife_enhancement': wd.get('use_enhancement', False),
1207
+ 'knife_boost': wd.get('boost_knife_detection', False),
1208
+ 'fight_analysis': fight_analysis_flag
1209
+ }
1210
+
1211
+ if self.weapon_model_custom and hasattr(self.weapon_model_custom, 'names'):
1212
+ status['custom_classes'] = list(self.weapon_model_custom.names.values())
1213
+
1214
+ return status
1215
+
1216
+ def calculate_risk_level(self, detections):
1217
+ """Calculate overall risk level including fights"""
1218
+ if not detections:
1219
+ return 'safe'
1220
+
1221
+ max_confidence = max(det['confidence'] for det in detections)
1222
+ threat_types = set(det['type'] for det in detections)
1223
+
1224
+ # Check for critical combinations
1225
+ has_weapons = 'weapon' in threat_types
1226
+ has_fights = 'fight' in threat_types
1227
+ has_nsfw = 'nsfw' in threat_types
1228
+
1229
+ # Fights + weapons = critical
1230
+ if has_weapons and has_fights:
1231
+ return 'critical'
1232
+
1233
+ # High confidence fights are critical
1234
+ fight_detections = [d for d in detections if d['type'] == 'fight']
1235
+ if fight_detections:
1236
+ max_fight_confidence = max(f['confidence'] for f in fight_detections)
1237
+ if max_fight_confidence > 0.8:
1238
+ return 'critical'
1239
+ elif max_fight_confidence > 0.65:
1240
+ return 'high'
1241
+
1242
+ # Existing weapon logic
1243
+ if has_weapons and max_confidence > 0.8:
1244
+ return 'critical'
1245
+ elif has_weapons or has_fights or max_confidence > 0.9:
1246
+ return 'high'
1247
+ elif max_confidence > 0.7:
1248
+ return 'medium'
1249
+ else:
1250
+ return 'low'
1251
+
1252
+ def draw_detections(self, image, detections):
1253
+ """Draw detection boxes and labels with enhanced visualization for fights"""
1254
+ try:
1255
+ colors = {
1256
+ 'weapon': (0, 0, 255), # Red
1257
+ 'fight': (0, 165, 255), # Orange for fights
1258
+ 'nsfw': (255, 0, 255), # Magenta
1259
+ }
1260
+
1261
+ # Special colors for weapon types
1262
+ weapon_colors = {
1263
+ 'blade': (0, 100, 255), # Orange-red for knives
1264
+ 'firearm': (0, 0, 255), # Red for guns
1265
+ 'blunt_weapon': (100, 0, 255) # Purple for blunt weapons
1266
+ }
1267
+
1268
+ # Special colors for fight types
1269
+ fight_colors = {
1270
+ 'physical_combat': (0, 140, 255), # Orange
1271
+ 'martial_arts': (0, 200, 255), # Light orange
1272
+ 'wrestling': (0, 165, 255), # Medium orange
1273
+ 'group_violence': (0, 69, 255), # Dark orange
1274
+ 'general_fight': (0, 165, 255) # Default orange
1275
+ }
1276
+
1277
+ for det in detections:
1278
+ x1, y1, x2, y2 = det['bbox']
1279
+
1280
+ # Choose color based on type
1281
+ if det['type'] == 'weapon' and 'weapon_type' in det:
1282
+ color = weapon_colors.get(det['weapon_type'], colors['weapon'])
1283
+ elif det['type'] == 'fight' and 'fight_type' in det:
1284
+ color = fight_colors.get(det['fight_type'], colors['fight'])
1285
+ else:
1286
+ color = colors.get(det['type'], (0, 255, 0))
1287
+
1288
+ # Draw rectangle with thicker line for high-threat detections
1289
+ thickness = 4 if det.get('threat_level') == 'critical' else 3 if det['type'] in ['weapon',
1290
+ 'fight'] else 2
1291
+ cv2.rectangle(image, (x1, y1), (x2, y2), color, thickness)
1292
+
1293
+ # Create detailed label
1294
+ if det['type'] == 'weapon':
1295
+ label = f"{det['class']} ({det['confidence']:.2f})"
1296
+ if 'threat_level' in det:
1297
+ label += f" [{det['threat_level']}]"
1298
+ elif det['type'] == 'fight':
1299
+ label = f"FIGHT: {det['class']} ({det['confidence']:.2f})"
1300
+ if 'threat_level' in det:
1301
+ label += f" [{det['threat_level']}]"
1302
+ if 'aggression_level' in det:
1303
+ label += f" {det['aggression_level']}"
1304
+ else:
1305
+ label = f"{det['type']}: {det['class']} ({det['confidence']:.2f})"
1306
+
1307
+ # Draw label background
1308
+ label_size = cv2.getTextSize(label, cv2.FONT_HERSHEY_SIMPLEX, 0.5, 2)[0]
1309
+ cv2.rectangle(image, (x1, y1 - 25), (x1 + label_size[0] + 5, y1), color, -1)
1310
+
1311
+ # Draw label text
1312
+ cv2.putText(image, label, (x1 + 2, y1 - 7),
1313
+ cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 255, 255), 2)
1314
+
1315
+ # Add additional context for fights
1316
+ if det['type'] == 'fight':
1317
+ context_text = []
1318
+ if 'people_involved' in det and det['people_involved'] > 0:
1319
+ context_text.append(f"People: {det['people_involved']}")
1320
+ if 'context_flags' in det and det['context_flags']:
1321
+ context_text.append(f"Flags: {', '.join(det['context_flags'])}")
1322
+
1323
+ if context_text:
1324
+ context_label = " | ".join(context_text)
1325
+ cv2.putText(image, context_label, (x1, y2 + 15),
1326
+ cv2.FONT_HERSHEY_SIMPLEX, 0.3, color, 1)
1327
+
1328
+ # Add detection method indicator (small text)
1329
+ if 'detection_method' in det:
1330
+ method = det['detection_method'].split('_')[-1]
1331
+ cv2.putText(image, method, (x1, y2 + 30),
1332
+ cv2.FONT_HERSHEY_SIMPLEX, 0.3, color, 1)
1333
+
1334
+ return image
1335
+
1336
+ except Exception as e:
1337
+ print(f"โŒ Error drawing detections: {e}")
1338
+ return image
1339
+
1340
+ def process_video(self, video_path, output_path=None, frame_skip=2):
1341
+ """Process video file with enhanced detection including fights - optimized frame processing"""
1342
+ try:
1343
+ cap = cv2.VideoCapture(video_path)
1344
+ frame_count = 0
1345
+ total_detections = []
1346
+ fight_timeline = [] # Track fights over time
1347
+ recent_detections = [] # Track recent detections for adaptive processing
1348
+
1349
+ if output_path:
1350
+ fourcc = cv2.VideoWriter_fourcc(*'mp4v')
1351
+ fps = cap.get(cv2.CAP_PROP_FPS)
1352
+ width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
1353
+ height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
1354
+ out = cv2.VideoWriter(output_path, fourcc, fps, (width, height))
1355
+
1356
+ while True:
1357
+ ret, frame = cap.read()
1358
+ if not ret:
1359
+ break
1360
+
1361
+ frame_count += 1
1362
+
1363
+ # Adaptive frame processing based on recent detections
1364
+ should_process = False
1365
+
1366
+ # Always process if recent threats detected (within last 10 frames)
1367
+ if any(det['frame'] > frame_count - 10 for det in recent_detections[-5:]):
1368
+ should_process = True
1369
+ # Or process based on reduced skip rate
1370
+ elif frame_count % max(1, frame_skip) == 0:
1371
+ should_process = True
1372
+
1373
+ if not should_process:
1374
+ if output_path:
1375
+ out.write(frame)
1376
+ continue
1377
+
1378
+ # Process frame
1379
+ result = self.process_image(frame)
1380
+ if result and result['detections']:
1381
+ # Add frame number to each detection for tracking
1382
+ for detection in result['detections']:
1383
+ detection['frame'] = frame_count
1384
+
1385
+ total_detections.extend(result['detections'])
1386
+ recent_detections.append({'frame': frame_count, 'count': len(result['detections'])})
1387
+
1388
+ # Track fight timeline
1389
+ fight_detections = [d for d in result['detections'] if d['type'] == 'fight']
1390
+ if fight_detections:
1391
+ timestamp = frame_count / cap.get(cv2.CAP_PROP_FPS)
1392
+ fight_timeline.append({
1393
+ 'timestamp': timestamp,
1394
+ 'frame': frame_count,
1395
+ 'fights': len(fight_detections),
1396
+ 'max_aggression': max(f.get('aggression_level', 'low') for f in fight_detections)
1397
+ })
1398
+
1399
+ # Reduce frame_skip temporarily when fight detected
1400
+ frame_skip = max(1, frame_skip // 2)
1401
+
1402
+ print(f"โš ๏ธ Frame {frame_count}: {len(result['detections'])} threats detected")
1403
+
1404
+ breakdown = result.get('detection_breakdown', {})
1405
+ if breakdown.get('fights', 0) > 0:
1406
+ print(f" ๐Ÿ‘Š Fights: {breakdown['fights']}")
1407
+
1408
+ if output_path and 'annotated_image' in result:
1409
+ out.write(result['annotated_image'])
1410
+ elif output_path:
1411
+ out.write(frame)
1412
+ else:
1413
+ # No detections - can increase frame_skip for efficiency
1414
+ if len(recent_detections) > 5 and all(det['count'] == 0 for det in recent_detections[-5:]):
1415
+ frame_skip = min(5, frame_skip + 1)
1416
+
1417
+ if output_path:
1418
+ out.write(frame)
1419
+
1420
+ cap.release()
1421
+ if output_path:
1422
+ out.release()
1423
+
1424
+ # Analysis of fight patterns
1425
+ fight_analysis = {}
1426
+ if fight_timeline:
1427
+ fight_analysis = {
1428
+ 'total_fight_incidents': len(fight_timeline),
1429
+ 'first_fight_time': fight_timeline[0]['timestamp'],
1430
+ 'last_fight_time': fight_timeline[-1]['timestamp'],
1431
+ 'peak_aggression_time': max(fight_timeline, key=lambda x: x['max_aggression'])['timestamp'],
1432
+ 'fight_duration_coverage': fight_timeline[-1]['timestamp'] - fight_timeline[0]['timestamp'] if len(
1433
+ fight_timeline) > 1 else 0
1434
+ }
1435
+
1436
+ return {
1437
+ 'total_frames_processed': frame_count // frame_skip,
1438
+ 'total_detections': len(total_detections),
1439
+ 'detections': total_detections,
1440
+ 'fight_timeline': fight_timeline,
1441
+ 'fight_analysis': fight_analysis,
1442
+ 'detection_breakdown': {
1443
+ 'weapons': len([d for d in total_detections if d['type'] == 'weapon']),
1444
+ 'fights': len([d for d in total_detections if d['type'] == 'fight']),
1445
+ 'nsfw': len([d for d in total_detections if d['type'] == 'nsfw'])
1446
+ }
1447
+ }
1448
+
1449
+ except Exception as e:
1450
+ print(f"โŒ Error processing video: {e}")
1451
+ return None
1452
+
1453
+ def save_report(self, filename="detection_report.json"):
1454
+ """Save detection history to file"""
1455
+ try:
1456
+ with open(filename, 'w') as f:
1457
+ json.dump(self.detection_history, f, indent=2, default=str)
1458
+ print(f"๐Ÿ“Š Report saved to {filename}")
1459
+ except Exception as e:
1460
+ print(f"โŒ Error saving report: {e}")
1461
+
1462
+ def get_memory_usage(self):
1463
+ """Get current GPU memory usage"""
1464
+ if torch.cuda.is_available():
1465
+ allocated = torch.cuda.memory_allocated() / 1024 ** 3
1466
+ cached = torch.cuda.memory_reserved() / 1024 ** 3
1467
+ return f"GPU Memory: {allocated:.2f}GB allocated, {cached:.2f}GB cached"
1468
+ return "CPU mode"
1469
+
1470
+
1471
+ def main():
1472
+ """Enhanced example usage with knife and fight detection improvements"""
1473
+
1474
+ # Initialize the system
1475
+ moderator = ContentModerator()
1476
+
1477
+ # Show enhanced system information
1478
+ print("\n" + "=" * 60)
1479
+ print("๐ŸŽฏ ENHANCED DUAL MODEL SYSTEM WITH FIGHT DETECTION")
1480
+ print("=" * 60)
1481
+
1482
+ status = moderator.get_model_status()
1483
+
1484
+ if status['custom_weapon_fight_model']:
1485
+ print("โœ… Custom YOLO11 Model (dao + sรบng + fight): LOADED")
1486
+ if 'custom_classes' in status:
1487
+ print(f"๐Ÿ“Š Custom classes: {status['custom_classes']}")
1488
+ else:
1489
+ print("โŒ Custom weapon+fight model: NOT FOUND")
1490
+
1491
+ if status['general_model']:
1492
+ print("โœ… General YOLO11n Model (person detection): LOADED")
1493
+ else:
1494
+ print("โŒ General model: FAILED")
1495
+
1496
+ if status['nsfw_classifier']:
1497
+ print("โœ… NSFW Classifier: LOADED")
1498
+ else:
1499
+ print("โŒ NSFW Classifier: FAILED")
1500
+
1501
+ print(f"๐Ÿ–ฅ๏ธ Device: {status['device']}")
1502
+ print(f"๐Ÿ—„๏ธ Cache system: ENABLED")
1503
+ print(f"๐Ÿ”ช Knife enhancement: {'ENABLED' if status['knife_enhancement'] else 'DISABLED'}")
1504
+ print(f"๐Ÿ“ˆ Knife confidence boost: {'ENABLED' if status['knife_boost'] else 'DISABLED'}")
1505
+ print(f"๐Ÿ‘Š Fight detection: {'ENABLED' if status['fight_detection'] else 'DISABLED'}")
1506
+ print(f"๐Ÿง  Fight analysis: {'ENABLED' if status['fight_analysis'] else 'DISABLED'}")
1507
+
1508
+ # Enhanced features info
1509
+ print("\n" + "=" * 60)
1510
+ print("โœจ ENHANCED DETECTION FEATURES")
1511
+ print("=" * 60)
1512
+ print("๐Ÿ”ง Image Enhancement:")
1513
+ print(" - Contrast & brightness optimization")
1514
+ print(" - Edge sharpening for metallic objects")
1515
+ print(" - CLAHE for local contrast")
1516
+ print("๐Ÿ“Š Confidence Boosting:")
1517
+ print(" - Geometric analysis (knives)")
1518
+ print(" - Motion blur analysis (fights)")
1519
+ print(" - Edge strength analysis")
1520
+ print("๐ŸŽฏ Multi-pass Detection:")
1521
+ print(" - Low threshold pass for knives (0.45)")
1522
+ print(" - Normal threshold for guns (0.45)")
1523
+ print(" - Low threshold for fights (0.40)")
1524
+ print("๐Ÿ‘Š Fight Analysis:")
1525
+ print(" - Multi-person fight detection")
1526
+ print(" - Aggression level assessment")
1527
+ print(" - Context-aware threat escalation")
1528
+
1529
+ # Example 1: Process single image
1530
+ print("\n" + "=" * 50)
1531
+ print("๐Ÿ–ผ๏ธ SINGLE IMAGE PROCESSING")
1532
+ print("=" * 50)
1533
+
1534
+ test_image = "test_image.jpg"
1535
+
1536
+ if os.path.exists(test_image):
1537
+ result = moderator.process_image(test_image)
1538
+ if result:
1539
+ print(f"\n๐Ÿ“Š DETECTION RESULTS:")
1540
+ print(f"Risk Level: {result['risk_level']}")
1541
+ print(f"Total Threats: {result['total_threats']}")
1542
+ print(f"Processing Method: {result.get('processing_method', 'standard')}")
1543
+
1544
+ breakdown = result.get('detection_breakdown', {})
1545
+ if breakdown:
1546
+ print(f"\n๐Ÿ“ˆ BREAKDOWN:")
1547
+ print(f" Weapons: {breakdown.get('weapons', 0)}")
1548
+ print(f" Fights: {breakdown.get('fights', 0)}")
1549
+ print(f" NSFW: {breakdown.get('nsfw', 0)}")
1550
+
1551
+ # Show weapon-specific results
1552
+ weapon_detections = [d for d in result['detections'] if d['type'] == 'weapon']
1553
+ if weapon_detections:
1554
+ print(f"\n๐Ÿ”ซ WEAPON DETECTIONS: {len(weapon_detections)}")
1555
+ for i, detection in enumerate(weapon_detections):
1556
+ method = detection.get('detection_method', 'unknown')
1557
+ print(f" Weapon {i + 1} ({method}):")
1558
+ print(f" Class: {detection['class']}")
1559
+ print(f" Type: {detection['weapon_type']}")
1560
+ print(f" Confidence: {detection['confidence']:.3f}")
1561
+ print(f" Threat Level: {detection['threat_level']}")
1562
+
1563
+ # Show fight-specific results
1564
+ fight_detections = [d for d in result['detections'] if d['type'] == 'fight']
1565
+ if fight_detections:
1566
+ print(f"\n๐Ÿ‘Š FIGHT DETECTIONS: {len(fight_detections)}")
1567
+ for i, detection in enumerate(fight_detections):
1568
+ method = detection.get('detection_method', 'unknown')
1569
+ print(f" Fight {i + 1} ({method}):")
1570
+ print(f" Class: {detection['class']}")
1571
+ print(f" Type: {detection.get('fight_type', 'unknown')}")
1572
+ print(f" Confidence: {detection['confidence']:.3f}")
1573
+ print(f" Threat Level: {detection['threat_level']}")
1574
+ print(f" Aggression: {detection.get('aggression_level', 'unknown')}")
1575
+ if 'people_involved' in detection:
1576
+ print(f" People Involved: {detection['people_involved']}")
1577
+ if 'context_flags' in detection and detection['context_flags']:
1578
+ print(f" Context: {', '.join(detection['context_flags'])}")
1579
+
1580
+ # Show NSFW results
1581
+ nsfw_detections = [d for d in result['detections'] if d['type'] == 'nsfw']
1582
+ if nsfw_detections:
1583
+ print(f"\n๐Ÿ”ž NSFW DETECTIONS: {len(nsfw_detections)}")
1584
+ for i, detection in enumerate(nsfw_detections):
1585
+ method = detection.get('method', 'unknown')
1586
+ print(f" NSFW {i + 1} ({method}):")
1587
+ print(f" Class: {detection['class']}")
1588
+ print(f" Confidence: {detection['confidence']:.3f}")
1589
+ if 'skin_ratio' in detection:
1590
+ print(f" Skin Ratio: {detection['skin_ratio']:.2f}")
1591
+ else:
1592
+ print(f"โš ๏ธ Test image not found: {test_image}")
1593
+ print("Creating a test pattern to demonstrate detection...")
1594
+
1595
+ # Create a synthetic test image
1596
+ test_img = np.ones((640, 640, 3), dtype=np.uint8) * 128
1597
+ cv2.putText(test_img, "Test Pattern", (200, 320),
1598
+ cv2.FONT_HERSHEY_SIMPLEX, 2, (255, 255, 255), 3)
1599
+
1600
+ result = moderator.process_image(test_img)
1601
+ print("โœ… Test pattern processed successfully")
1602
+
1603
+ # Example 2: Enhanced webcam processing with fight detection
1604
+ print("\n" + "=" * 60)
1605
+ print("๐Ÿ“น ENHANCED WEBCAM PROCESSING WITH FIGHT DETECTION")
1606
+ print("=" * 60)
1607
+ print("Starting enhanced detection on webcam...")
1608
+ print("๐ŸŽฎ Controls:")
1609
+ print(" - Press 'q' to quit")
1610
+ print(" - Press 's' to save frame")
1611
+ print(" - Press 'i' to show model info")
1612
+ print(" - Press 'e' to toggle enhancement")
1613
+ print(" - Press 'b' to toggle knife confidence boost")
1614
+ print(" - Press 'f' to toggle fight analysis")
1615
+ print(" - Press 'h' for help")
1616
+
1617
+ try:
1618
+ cap = cv2.VideoCapture(0)
1619
+
1620
+ if not cap.isOpened():
1621
+ print("โŒ Cannot open webcam. Check if camera is connected.")
1622
+ else:
1623
+ print("โœ… Enhanced webcam processing started")
1624
+
1625
+ frame_count = 0
1626
+ detection_stats = {
1627
+ 'weapons': 0,
1628
+ 'knives': 0,
1629
+ 'guns': 0,
1630
+ 'fights': 0,
1631
+ 'nsfw': 0,
1632
+ 'total_frames': 0,
1633
+ 'fight_incidents': 0
1634
+ }
1635
+
1636
+ # Adaptive processing variables
1637
+ process_interval = 2 # Start with every 2nd frame
1638
+ last_detection_frame = 0
1639
+ consecutive_safe_frames = 0
1640
+
1641
+ while True:
1642
+ ret, frame = cap.read()
1643
+ if not ret:
1644
+ print("โŒ Cannot read from webcam")
1645
+ break
1646
+
1647
+ frame_count += 1
1648
+ detection_stats['total_frames'] = frame_count
1649
+ frame = cv2.flip(frame, 1)
1650
+
1651
+ # Add status overlay
1652
+ y_offset = frame.shape[0] - 120
1653
+ cv2.putText(frame,
1654
+ f"Enhancement: {'ON' if moderator.config['weapon_detection']['use_enhancement'] else 'OFF'}",
1655
+ (10, y_offset), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 255, 255), 1)
1656
+
1657
+ cv2.putText(frame,
1658
+ f"Knife Boost: {'ON' if moderator.config['weapon_detection']['boost_knife_detection'] else 'OFF'}",
1659
+ (10, y_offset + 20), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 255, 255), 1)
1660
+
1661
+ cv2.putText(frame,
1662
+ f"Fight Analysis: {'ON' if moderator.config['weapon_detection']['fight_analysis'] else 'OFF'}",
1663
+ (10, y_offset + 40), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 255, 255), 1)
1664
+
1665
+ model_info = "Models: Custom+General" if moderator.weapon_model_custom else "General Only"
1666
+ cv2.putText(frame, model_info, (10, y_offset + 60),
1667
+ cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 255, 255), 1)
1668
+
1669
+ # Adaptive frame processing - process more frequently when threats detected
1670
+ should_process = False
1671
+
1672
+ # Always process if recent threats (within last 30 frames)
1673
+ if frame_count - last_detection_frame <= 30:
1674
+ should_process = (frame_count % 1 == 0) # Process every frame
1675
+ cv2.putText(frame, "HIGH ALERT MODE", (10, 60), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 0, 255), 2)
1676
+ # Normal processing with reduced interval
1677
+ elif frame_count % process_interval == 0:
1678
+ should_process = True
1679
+
1680
+ if should_process:
1681
+ result = moderator.process_image(frame)
1682
+
1683
+ if result and result['action_required']:
1684
+ last_detection_frame = frame_count # Update last detection frame
1685
+ consecutive_safe_frames = 0
1686
+ process_interval = 1 # Process every frame when threats detected
1687
+
1688
+ # Count detections by type
1689
+ for detection in result['detections']:
1690
+ if detection['type'] == 'weapon':
1691
+ detection_stats['weapons'] += 1
1692
+ if detection['weapon_type'] == 'blade':
1693
+ detection_stats['knives'] += 1
1694
+ elif detection['weapon_type'] == 'firearm':
1695
+ detection_stats['guns'] += 1
1696
+ elif detection['type'] == 'fight':
1697
+ detection_stats['fights'] += 1
1698
+ if detection.get('aggression_level') in ['high', 'extreme']:
1699
+ detection_stats['fight_incidents'] += 1
1700
+ elif detection['type'] == 'nsfw':
1701
+ detection_stats['nsfw'] += 1
1702
+
1703
+ print(
1704
+ f"โš ๏ธ Frame {frame_count}: {result['risk_level']} risk - {result['total_threats']} threats!")
1705
+
1706
+ # Show specific detections with fight info
1707
+ for detection in result['detections']:
1708
+ if detection['type'] == 'weapon':
1709
+ icon = "๐Ÿ”ช" if detection['weapon_type'] == 'blade' else "๐Ÿ”ซ"
1710
+ method = detection.get('detection_method', 'unknown').split('_')[-1]
1711
+ print(f" {icon} {detection['class']} ({detection['confidence']:.3f}) [{method}]")
1712
+ elif detection['type'] == 'fight':
1713
+ fight_type = detection.get('fight_type', 'general')
1714
+ aggression = detection.get('aggression_level', 'unknown')
1715
+ people = detection.get('people_involved', 0)
1716
+ method = detection.get('detection_method', 'unknown').split('_')[-1]
1717
+ print(f" ๐Ÿ‘Š FIGHT: {fight_type} ({detection['confidence']:.3f}) [{method}]")
1718
+ print(f" Aggression: {aggression}, People: {people}")
1719
+
1720
+ # Use annotated frame
1721
+ if 'annotated_image' in result:
1722
+ cv2.imshow('Enhanced Detection System (Weapons + Fights)', result['annotated_image'])
1723
+ else:
1724
+ # Add threat counter
1725
+ breakdown = result.get('detection_breakdown', {})
1726
+ threat_text = f"THREATS: W:{breakdown.get('weapons', 0)} F:{breakdown.get('fights', 0)} N:{breakdown.get('nsfw', 0)}"
1727
+ cv2.putText(frame, threat_text, (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 255), 2)
1728
+ cv2.imshow('Enhanced Detection System (Weapons + Fights)', frame)
1729
+ else:
1730
+ consecutive_safe_frames += 1
1731
+ # Gradually increase processing interval when safe (up to max 3)
1732
+ if consecutive_safe_frames > 30:
1733
+ process_interval = min(3, process_interval + 1)
1734
+ consecutive_safe_frames = 0
1735
+
1736
+ cv2.putText(frame, "SAFE", (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 0), 2)
1737
+ cv2.putText(frame, f"Process Interval: {process_interval}", (10, 90),
1738
+ cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 255, 255), 1)
1739
+ cv2.imshow('Enhanced Detection System (Weapons + Fights)', frame)
1740
+ else:
1741
+ cv2.putText(frame, "SAFE", (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 0), 2)
1742
+ cv2.putText(frame, f"Process Interval: {process_interval}", (10, 90),
1743
+ cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 255, 255), 1)
1744
+ cv2.imshow('Enhanced Detection System (Weapons + Fights)', frame)
1745
+
1746
+ # Handle key presses
1747
+ key = cv2.waitKey(1) & 0xFF
1748
+ if key == ord('q'):
1749
+ print("๐Ÿ›‘ Webcam stopped by user")
1750
+ break
1751
+ elif key == ord('s'):
1752
+ filename = f"enhanced_detection_{frame_count}.jpg"
1753
+ cv2.imwrite(filename, frame)
1754
+ print(f"๐Ÿ’พ Frame saved as {filename}")
1755
+ elif key == ord('i'):
1756
+ print(f"\n๐Ÿ“Š Model Status:")
1757
+ current_status = moderator.get_model_status()
1758
+ for k, v in current_status.items():
1759
+ print(f" {k}: {v}")
1760
+ elif key == ord('e'):
1761
+ # Toggle enhancement
1762
+ moderator.config['weapon_detection']['use_enhancement'] = \
1763
+ not moderator.config['weapon_detection']['use_enhancement']
1764
+ print(
1765
+ f"๐Ÿ”ง Enhancement: {'ON' if moderator.config['weapon_detection']['use_enhancement'] else 'OFF'}")
1766
+ elif key == ord('b'):
1767
+ # Toggle knife boost
1768
+ moderator.config['weapon_detection']['boost_knife_detection'] = \
1769
+ not moderator.config['weapon_detection']['boost_knife_detection']
1770
+ print(
1771
+ f"๐Ÿ“ˆ Knife Boost: {'ON' if moderator.config['weapon_detection']['boost_knife_detection'] else 'OFF'}")
1772
+ elif key == ord('f'):
1773
+ # Toggle fight analysis
1774
+ moderator.config['weapon_detection']['fight_analysis'] = \
1775
+ not moderator.config['weapon_detection']['fight_analysis']
1776
+ print(
1777
+ f"๐Ÿ‘Š Fight Analysis: {'ON' if moderator.config['weapon_detection']['fight_analysis'] else 'OFF'}")
1778
+ elif key == ord('h'):
1779
+ print("\n๐ŸŽฎ Controls:")
1780
+ print(" 'q': quit")
1781
+ print(" 's': save frame")
1782
+ print(" 'i': model info")
1783
+ print(" 'e': toggle enhancement")
1784
+ print(" 'b': toggle knife confidence boost")
1785
+ print(" 'f': toggle fight analysis")
1786
+ print(" 'h': help")
1787
+
1788
+ # Show comprehensive session statistics
1789
+ print(f"\n๐Ÿ“ˆ Session Statistics:")
1790
+ print(f" Total frames: {detection_stats['total_frames']}")
1791
+ print(f" Total weapon detections: {detection_stats['weapons']}")
1792
+ print(f" - Knives/Dao: {detection_stats['knives']}")
1793
+ print(f" - Guns: {detection_stats['guns']}")
1794
+ print(f" Total fight detections: {detection_stats['fights']}")
1795
+ print(f" - High-aggression incidents: {detection_stats['fight_incidents']}")
1796
+ print(f" NSFW detections: {detection_stats['nsfw']}")
1797
+
1798
+ if detection_stats['total_frames'] > 0:
1799
+ total_detections = detection_stats['weapons'] + detection_stats['fights'] + detection_stats['nsfw']
1800
+ detection_rate = (total_detections / detection_stats['total_frames'] * 100)
1801
+ print(f" Overall detection rate: {detection_rate:.1f}%")
1802
+
1803
+ if detection_stats['weapons'] > 0:
1804
+ knife_ratio = detection_stats['knives'] / detection_stats['weapons'] * 100
1805
+ print(f" Knife detection ratio: {knife_ratio:.1f}% of weapons")
1806
+
1807
+ if detection_stats['fights'] > 0:
1808
+ incident_ratio = detection_stats['fight_incidents'] / detection_stats['fights'] * 100
1809
+ print(f" High-aggression fight ratio: {incident_ratio:.1f}% of fights")
1810
+
1811
+ cap.release()
1812
+ cv2.destroyAllWindows()
1813
+ print("โœ… Enhanced webcam session completed")
1814
+
1815
+ except Exception as e:
1816
+ print(f"โŒ Webcam error: {e}")
1817
+
1818
+ # Show final system status
1819
+ print(f"\n๐Ÿ’พ {moderator.get_memory_usage()}")
1820
+ print(f"๐Ÿ—„๏ธ Final cache size: {len(moderator.detection_cache)} entries")
1821
+
1822
+ # Save enhanced report
1823
+ moderator.save_report("enhanced_detection_with_fights_report.json")
1824
+
1825
+ print("\nโœ… Enhanced Content Moderation System with Fight Detection completed!")
1826
+ print("๐Ÿ’ก New fight detection capabilities:")
1827
+ print(" - Behavioral fight pattern recognition")
1828
+ print(" - Multi-person fight analysis")
1829
+ print(" - Aggression level assessment")
1830
+ print(" - Context-aware threat escalation")
1831
+ print(" - Fight timeline tracking for videos")
1832
+ print("๐Ÿ’ก Enhanced weapon detection:")
1833
+ print(" - Image enhancement preprocessing")
1834
+ print(" - Dynamic confidence thresholds")
1835
+ print(" - Geometric feature analysis")
1836
+ print(" - Multi-pass detection strategy")
1837
+
1838
+
1839
+ if __name__ == "__main__":
1840
+ main()
models/best.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:715928fb224aec3661a91a0008a033dd9d45043cc158538da6be0c2f27ff584a
3
+ size 19017171
models/last.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:122487e3289c2ae86fbce554a657c41193af0f1a07ce3bba98a578bd7f37846f
3
+ size 19017171
requirements.txt ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Core ML Libraries (Latest 2025 versions)
2
+ torch>=2.8.0
3
+ torchvision>=0.23.0
4
+ ultralytics>=8.3.0
5
+ transformers>=4.55.0
6
+
7
+ # Computer Vision (Latest versions)
8
+ opencv-python>=4.12.0
9
+ pillow>=10.0.0
10
+ flask>=5.13.0
11
+ Flask-SocketIO>=5.5.1
12
+ flask-cors>=6.0.1
13
+ python-socketio>=5.13.0
14
+ # Utilities (Updated)
15
+ numpy>=1.26.0
16
+ requests>=2.31.0
17
+ tqdm>=4.66.0
18
+ fastapi-events>=0.12.2
19
+ accelerate>=1.0.0