From 592fdf97a3f4bc8d8fab93a24405ec7773723717 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 31 Oct 2024 08:30:33 +0000 Subject: [PATCH 1/9] Update repo structure --- PROJECT_STRUCTURE.md | 9 +++++++-- repo_structure.txt | 9 +++++++-- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/PROJECT_STRUCTURE.md b/PROJECT_STRUCTURE.md index 23b5d82..d672dd6 100644 --- a/PROJECT_STRUCTURE.md +++ b/PROJECT_STRUCTURE.md @@ -225,19 +225,24 @@ │ ├── SQLFILE.sql │ ├── __pycache__/ │ │ ├── audio.cpython-311.pyc +│ │ ├── audio.cpython-312.pyc │ │ ├── detection.cpython-311.pyc -│ │ └── head_pose.cpython-311.pyc +│ │ ├── head_pose.cpython-311.pyc +│ │ └── head_pose.cpython-312.pyc │ ├── audio.py │ ├── detection.py │ ├── face-rec.py │ ├── graph.py │ ├── head_pose.py │ ├── logic.xlsx +│ ├── object_detection.py │ ├── peer_comparison_tool.py │ ├── processes.py │ ├── pyaudio_test.py │ ├── run.py -│ └── screen_recorder.py +│ ├── screen_recorder.py +│ ├── test-image.jpg +│ └── test-image2.jpg ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── GSSoC-Ext.png diff --git a/repo_structure.txt b/repo_structure.txt index 3864e1e..871b4aa 100644 --- a/repo_structure.txt +++ b/repo_structure.txt @@ -221,19 +221,24 @@ │ ├── SQLFILE.sql │ ├── __pycache__/ │ │ ├── audio.cpython-311.pyc +│ │ ├── audio.cpython-312.pyc │ │ ├── detection.cpython-311.pyc -│ │ └── head_pose.cpython-311.pyc +│ │ ├── head_pose.cpython-311.pyc +│ │ └── head_pose.cpython-312.pyc │ ├── audio.py │ ├── detection.py │ ├── face-rec.py │ ├── graph.py │ ├── head_pose.py │ ├── logic.xlsx +│ ├── object_detection.py │ ├── peer_comparison_tool.py │ ├── processes.py │ ├── pyaudio_test.py │ ├── run.py -│ └── screen_recorder.py +│ ├── screen_recorder.py +│ ├── test-image.jpg +│ └── test-image2.jpg ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── GSSoC-Ext.png From 0438ccaa2a2b4ba982c310b11d8e52afb322502e Mon Sep 17 00:00:00 2001 From: MANI Date: Thu, 31 Oct 2024 14:30:10 +0530 Subject: [PATCH 2/9] added the whole frontend --- frontend/index.html | 1003 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1003 insertions(+) create mode 100644 frontend/index.html diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..8e60c9b --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,1003 @@ + + + + + + SPROCTOR - Smart Proctoring System + + + + + + +
+
+
+
+
1
+ Video Feed +
+
+
+ + Live +
+ Live Camera Feed +
+
+
+
24
+
Active Students
+
+
+
3
+
Active Exams
+
+
+
+ +
+
+
2
+ Image Processing +
+
+
+
98%
+
Face Detection
+
+
+
95%
+
Eye Tracking
+
+
+
+ Monitoring for: Head Movement, Eye Direction, Multiple Faces +
+
+ +
+
+
3
+ Suspicious Detection +
+
+
+ +
+
+
4
+ Results +
+
+
+
12%
+
Avg. Suspicious
+
+
+
5
+
Flagged
+
+
+
+
+ +
+
+

Active Examination Sessions

+
+
+ + +
+ +
+
+ +
+
+
👥
+
+
24
+
Total Students
+
+
+
+
+
+
18
+
Active Now
+
+
+
+
⚠️
+
+
3
+
High Risk
+
+
+
+
+
+
6
+
Completed
+
+
+
+ +
+ + + + + + + + + + + + + + +
Student IDNameStatusDurationSuspicious LevelActions
+
+ + +
+ + + + + + \ No newline at end of file From f40998a7bcb328fb9063f314d990dd8b1db6235b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 31 Oct 2024 09:00:32 +0000 Subject: [PATCH 3/9] Update repo structure --- PROJECT_STRUCTURE.md | 2 ++ repo_structure.txt | 2 ++ 2 files changed, 4 insertions(+) diff --git a/PROJECT_STRUCTURE.md b/PROJECT_STRUCTURE.md index d672dd6..adeae4c 100644 --- a/PROJECT_STRUCTURE.md +++ b/PROJECT_STRUCTURE.md @@ -265,6 +265,8 @@ │ ├── contributor.css │ ├── contributor.html │ └── contributor.js +├── frontend/ +│ └── index.html ├── heatmap_combined_20241009_144503.png ├── login.py ├── modle.png diff --git a/repo_structure.txt b/repo_structure.txt index 871b4aa..c6dc528 100644 --- a/repo_structure.txt +++ b/repo_structure.txt @@ -261,6 +261,8 @@ │ ├── contributor.css │ ├── contributor.html │ └── contributor.js +├── frontend/ +│ └── index.html ├── heatmap_combined_20241009_144503.png ├── login.py ├── modle.png From 804032aad8f8148b37a7a504bca0f92be2df3bdb Mon Sep 17 00:00:00 2001 From: MANI Date: Thu, 31 Oct 2024 14:57:00 +0530 Subject: [PATCH 4/9] Adding object detection model --- Backend/proctor_core.py | 121 +++++++++++++++++++++++++++++++++++++++ Backend/run.py | 124 +++++++++++++++++++++++++++++++++------- requirements.txt | 27 +++++---- 3 files changed, 239 insertions(+), 33 deletions(-) create mode 100644 Backend/proctor_core.py diff --git a/Backend/proctor_core.py b/Backend/proctor_core.py new file mode 100644 index 0000000..ed48076 --- /dev/null +++ b/Backend/proctor_core.py @@ -0,0 +1,121 @@ +# Backend/proctor_core.py + +import cv2 +import mediapipe as mp +import numpy as np +from typing import Dict, List, Tuple +import logging +import os + +# Local imports +from .detection import run_detection +from .head_pose import pose +from .object_detection import detect_objects +from .audio import process_audio +from .screen_recorder import capture_screen + +class ProctorCore: + def __init__(self): + self.mp_face_detection = mp.solutions.face_detection + self.mp_pose = mp.solutions.pose + self.face_detection = self.mp_face_detection.FaceDetection(min_detection_confidence=0.7) + self.pose_detection = self.mp_pose.Pose(min_detection_confidence=0.7) + self.logger = self._setup_logger() + + def _setup_logger(self) -> logging.Logger: + """Configure logging for the proctoring system""" + logger = logging.getLogger('ProctorCore') + logger.setLevel(logging.INFO) + handler = logging.StreamHandler() + formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') + handler.setFormatter(formatter) + logger.addHandler(handler) + return logger + + def start_monitoring(self): + """Initialize and start all monitoring components""" + try: + # Start detection systems + detection_result = run_detection() + pose_result = pose() + screen_capture = capture_screen() + + # Process and combine results + combined_results = self._process_results( + detection_result, + pose_result, + screen_capture + ) + + return combined_results + + except Exception as e: + self.logger.error(f"Error in monitoring: {str(e)}") + raise + + def _process_results(self, detection_data, pose_data, screen_data) -> Dict: + """Process and combine results from different detection systems""" + results = { + 'timestamp': np.datetime64('now'), + 'detection': detection_data, + 'pose': pose_data, + 'screen': screen_data, + 'suspicious_level': 0.0 + } + + # Calculate suspicious level based on combined factors + suspicious_factors = [ + detection_data.get('suspicious_score', 0), + pose_data.get('deviation_score', 0), + screen_data.get('activity_score', 0) + ] + + results['suspicious_level'] = np.mean([x for x in suspicious_factors if x is not None]) + + return results + + def save_results(self, results: Dict, output_path: str = None): + """Save monitoring results to specified location""" + if output_path is None: + output_path = os.path.join( + os.path.dirname(__file__), + 'Dataset', + f'proctor_results_{np.datetime64("now")}.json' + ) + + try: + import json + with open(output_path, 'w') as f: + json.dump(results, f, indent=4, default=str) + self.logger.info(f"Results saved to {output_path}") + except Exception as e: + self.logger.error(f"Error saving results: {str(e)}") + + def analyze_behavior(self, results: Dict) -> Dict: + """Analyze monitored behavior and generate insights""" + analysis = { + 'timestamp': np.datetime64('now'), + 'overall_score': results.get('suspicious_level', 0), + 'warnings': [], + 'recommendations': [] + } + + # Generate warnings based on thresholds + if results.get('pose', {}).get('deviation_score', 0) > 0.7: + analysis['warnings'].append('Significant head movement detected') + + if results.get('detection', {}).get('suspicious_score', 0) > 0.7: + analysis['warnings'].append('Suspicious objects detected') + + if results.get('screen', {}).get('activity_score', 0) > 0.7: + analysis['warnings'].append('Unusual screen activity detected') + + return analysis + + def cleanup(self): + """Cleanup resources and close connections""" + try: + cv2.destroyAllWindows() + self.logger.info("Cleanup completed successfully") + except Exception as e: + self.logger.error(f"Error during cleanup: {str(e)}") \ No newline at end of file diff --git a/Backend/run.py b/Backend/run.py index 7fa04de..a6dad64 100644 --- a/Backend/run.py +++ b/Backend/run.py @@ -1,28 +1,110 @@ -import head_pose -import detection -import threading as th - -def run_threads(): - try: - # Create threads for each target function - head_pose_thread = th.Thread(target=head_pose.pose) - # audio_thread = th.Thread(target=audio.sound) # Uncomment if audio module is needed - detection_thread = th.Thread(target=detection.run_detection) +# Backend/run.py - # Start the threads - head_pose_thread.start() - # audio_thread.start() # Uncomment to start audio thread - detection_thread.start() +import threading as th +import logging +import os +from typing import Dict, List +import queue +from .proctor_core import ProctorCore - # Wait for the threads to complete - head_pose_thread.join() - # audio_thread.join() # Uncomment to wait for audio thread - detection_thread.join() +class ProctorManager: + def __init__(self): + self.result_queue = queue.Queue() + self.proctor = ProctorCore() + self.is_running = False + self.threads: List[th.Thread] = [] + self.logger = self._setup_logger() + + def _setup_logger(self): + logger = logging.getLogger('ProctorManager') + logger.setLevel(logging.INFO) + handler = logging.StreamHandler() + formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') + handler.setFormatter(formatter) + logger.addHandler(handler) + return logger + + def _monitoring_worker(self): + """Worker function for continuous monitoring""" + while self.is_running: + try: + results = self.proctor.start_monitoring() + self.result_queue.put(results) + except Exception as e: + self.logger.error(f"Error in monitoring worker: {str(e)}") + break + + def _analysis_worker(self): + """Worker function for analyzing results""" + while self.is_running: + try: + results = self.result_queue.get(timeout=1) + if results: + analysis = self.proctor.analyze_behavior(results) + self.proctor.save_results(analysis) + except queue.Empty: + continue + except Exception as e: + self.logger.error(f"Error in analysis worker: {str(e)}") + break + + def start(self): + """Start the proctoring system""" + try: + self.is_running = True + + # Create worker threads + monitoring_thread = th.Thread(target=self._monitoring_worker) + analysis_thread = th.Thread(target=self._analysis_worker) + + # Start threads + self.threads = [monitoring_thread, analysis_thread] + for thread in self.threads: + thread.start() + + self.logger.info("Proctoring system started successfully") + + except Exception as e: + self.logger.error(f"Error starting proctoring system: {str(e)}") + self.stop() + + def stop(self): + """Stop the proctoring system""" + self.is_running = False + + # Wait for threads to complete + for thread in self.threads: + thread.join() + + # Cleanup resources + self.proctor.cleanup() + self.logger.info("Proctoring system stopped") +def main(): + # Setup logging + logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' + ) + logger = logging.getLogger(__name__) + + try: + # Initialize and start the proctoring system + manager = ProctorManager() + manager.start() + + # Keep running until interrupted + while True: + pass + + except KeyboardInterrupt: + logger.info("Received shutdown signal") except Exception as e: - print(f"An error occurred: {e}") + logger.error(f"Unexpected error: {str(e)}") finally: - print("All threads have been joined.") + if 'manager' in locals(): + manager.stop() + logger.info("Application shutdown complete") if __name__ == "__main__": - run_threads() + main() \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 3850801..0a37db7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,16 @@ +numpy==2.1.1 +opencv-python==4.10.0.84 +opencv-contrib-python==4.10.0.84 +mediapipe==0.10.14 +PyAudio==0.2.14 +sounddevice==0.5.0 +tensorflow>=2.14.0 +torch>=2.1.0 +mediapipe==0.10.14 +protobuf==4.25.5 +scipy==1.14.1 +matplotlib==3.9.2 +pillow==10.4.0 absl-py==2.1.0 attrs==24.2.0 cffi==1.17.1 @@ -9,23 +22,13 @@ glob2==0.7 jax==0.4.33 jaxlib==0.4.33 kiwisolver==1.4.7 -matplotlib==3.9.2 -mediapipe==0.10.14 ml_dtypes==0.5.0 -numpy==2.1.1 -opencv-contrib-python==4.10.0.84 -opencv-python==4.10.0.84 opt_einsum==3.4.0 packaging==24.1 -pillow==10.4.0 -protobuf==4.25.5 -PyAudio==0.2.14 pycparser==2.22 pyparsing==3.1.4 python-dateutil==2.9.0.post0 -pywin32==306 -scipy==1.14.1 six==1.16.0 -sounddevice==0.5.0 +pywin32==306 WMI==1.5.1 -tensor \ No newline at end of file +psutil>=5.9.0 \ No newline at end of file From 0d55b4fa166fc3eb81fa4e2f554647a5c2072710 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 31 Oct 2024 09:27:28 +0000 Subject: [PATCH 5/9] Update repo structure --- PROJECT_STRUCTURE.md | 1 + repo_structure.txt | 1 + 2 files changed, 2 insertions(+) diff --git a/PROJECT_STRUCTURE.md b/PROJECT_STRUCTURE.md index adeae4c..6fb00ae 100644 --- a/PROJECT_STRUCTURE.md +++ b/PROJECT_STRUCTURE.md @@ -238,6 +238,7 @@ │ ├── object_detection.py │ ├── peer_comparison_tool.py │ ├── processes.py +│ ├── proctor_core.py │ ├── pyaudio_test.py │ ├── run.py │ ├── screen_recorder.py diff --git a/repo_structure.txt b/repo_structure.txt index c6dc528..2755cc2 100644 --- a/repo_structure.txt +++ b/repo_structure.txt @@ -234,6 +234,7 @@ │ ├── object_detection.py │ ├── peer_comparison_tool.py │ ├── processes.py +│ ├── proctor_core.py │ ├── pyaudio_test.py │ ├── run.py │ ├── screen_recorder.py From c7099f88b951eafb36e16f28221a0ba9716cc958 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 4 Nov 2024 06:15:36 +0000 Subject: [PATCH 6/9] Update repo structure --- PROJECT_STRUCTURE.md | 1 + repo_structure.txt | 1 + 2 files changed, 2 insertions(+) diff --git a/PROJECT_STRUCTURE.md b/PROJECT_STRUCTURE.md index 6fb00ae..397d1af 100644 --- a/PROJECT_STRUCTURE.md +++ b/PROJECT_STRUCTURE.md @@ -254,6 +254,7 @@ ├── LICENSE ├── PROJECT_STRUCTURE.md ├── ReadMe.md +├── SECURITY.md ├── Suggested-Issues.md ├── Userdb.sql ├── __pycache__/ diff --git a/repo_structure.txt b/repo_structure.txt index 2755cc2..5100cb8 100644 --- a/repo_structure.txt +++ b/repo_structure.txt @@ -250,6 +250,7 @@ ├── LICENSE ├── PROJECT_STRUCTURE.md ├── ReadMe.md +├── SECURITY.md ├── Suggested-Issues.md ├── Userdb.sql ├── __pycache__/ From 89089d01a0cc747b0be8a565e80183ee904088e0 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 4 Nov 2024 10:36:22 +0000 Subject: [PATCH 7/9] Update repo structure --- PROJECT_STRUCTURE.md | 4 +++- repo_structure.txt | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/PROJECT_STRUCTURE.md b/PROJECT_STRUCTURE.md index 397d1af..006de22 100644 --- a/PROJECT_STRUCTURE.md +++ b/PROJECT_STRUCTURE.md @@ -260,7 +260,9 @@ ├── __pycache__/ │ ├── audio.cpython-311.pyc │ ├── detection.cpython-311.pyc -│ └── head_pose.cpython-311.pyc +│ ├── head_pose.cpython-311.pyc +│ └── tutorial/ +│ └── tutorial.html ├── calenderApp/ │ └── calender.html ├── contributor/ diff --git a/repo_structure.txt b/repo_structure.txt index 5100cb8..038311e 100644 --- a/repo_structure.txt +++ b/repo_structure.txt @@ -256,7 +256,9 @@ ├── __pycache__/ │ ├── audio.cpython-311.pyc │ ├── detection.cpython-311.pyc -│ └── head_pose.cpython-311.pyc +│ ├── head_pose.cpython-311.pyc +│ └── tutorial/ +│ └── tutorial.html ├── calenderApp/ │ └── calender.html ├── contributor/ From 1211f17c8c1fdc49202d1615fca3dec26dde59fb Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 8 Nov 2024 17:31:46 +0000 Subject: [PATCH 8/9] Update repo structure --- PROJECT_STRUCTURE.md | 7 ++++++- repo_structure.txt | 7 ++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/PROJECT_STRUCTURE.md b/PROJECT_STRUCTURE.md index 006de22..0852838 100644 --- a/PROJECT_STRUCTURE.md +++ b/PROJECT_STRUCTURE.md @@ -235,15 +235,19 @@ │ ├── graph.py │ ├── head_pose.py │ ├── logic.xlsx +│ ├── model_training.py +│ ├── mtcnn_face_detection.py │ ├── object_detection.py │ ├── peer_comparison_tool.py │ ├── processes.py +│ ├── proctor_api.py │ ├── proctor_core.py │ ├── pyaudio_test.py │ ├── run.py │ ├── screen_recorder.py │ ├── test-image.jpg -│ └── test-image2.jpg +│ ├── test-image2.jpg +│ └── train.py ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── GSSoC-Ext.png @@ -255,6 +259,7 @@ ├── PROJECT_STRUCTURE.md ├── ReadMe.md ├── SECURITY.md +├── SECURITYPOLICY.md ├── Suggested-Issues.md ├── Userdb.sql ├── __pycache__/ diff --git a/repo_structure.txt b/repo_structure.txt index 038311e..29c87ac 100644 --- a/repo_structure.txt +++ b/repo_structure.txt @@ -231,15 +231,19 @@ │ ├── graph.py │ ├── head_pose.py │ ├── logic.xlsx +│ ├── model_training.py +│ ├── mtcnn_face_detection.py │ ├── object_detection.py │ ├── peer_comparison_tool.py │ ├── processes.py +│ ├── proctor_api.py │ ├── proctor_core.py │ ├── pyaudio_test.py │ ├── run.py │ ├── screen_recorder.py │ ├── test-image.jpg -│ └── test-image2.jpg +│ ├── test-image2.jpg +│ └── train.py ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── GSSoC-Ext.png @@ -251,6 +255,7 @@ ├── PROJECT_STRUCTURE.md ├── ReadMe.md ├── SECURITY.md +├── SECURITYPOLICY.md ├── Suggested-Issues.md ├── Userdb.sql ├── __pycache__/ From 7b13b2ca06cdcaa85df06ef36488b553599344ae Mon Sep 17 00:00:00 2001 From: MANI Date: Sat, 9 Nov 2024 17:06:13 +0530 Subject: [PATCH 9/9] Train and Deploy RCNN and CNN Model --- Backend/detection.py | 166 ++++------------------ Backend/model_training.py | 211 +++++++++++++++++++--------- Backend/mtcnn_face_detection.py | 238 ++++++++++++++++++++++++++------ Backend/processes.py | 182 +++++++++++++++++++----- 4 files changed, 522 insertions(+), 275 deletions(-) diff --git a/Backend/detection.py b/Backend/detection.py index a56dcfb..1387976 100644 --- a/Backend/detection.py +++ b/Backend/detection.py @@ -1,138 +1,32 @@ -import time -import audio -import head_pose -import matplotlib.pyplot as plt +from dataclasses import dataclass +from typing import List, Dict, Optional import numpy as np -import logging - -# Configure logging -logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') - -PLOT_LENGTH = 200 - -# Placeholders -GLOBAL_CHEAT = 0 -PERCENTAGE_CHEAT = 0 -CHEAT_THRESH = 0.6 -XDATA = list(range(200)) -YDATA = [0] * 200 - -# Global flag to check if window is open -is_running = True - -def avg(current, previous): - if previous > 1: - return 0.65 - if current == 0: - if previous < 0.01: - return 0.01 - return previous / 1.01 - if previous == 0: - return current - return 1 * previous + 0.1 * current - -def process(): - global GLOBAL_CHEAT, PERCENTAGE_CHEAT, CHEAT_THRESH - - try: - if GLOBAL_CHEAT == 0: - if head_pose.X_AXIS_CHEAT == 0: - if head_pose.Y_AXIS_CHEAT == 0: - if audio.AUDIO_CHEAT == 0: - PERCENTAGE_CHEAT = avg(0, PERCENTAGE_CHEAT) - else: - PERCENTAGE_CHEAT = avg(0.2, PERCENTAGE_CHEAT) - else: - if audio.AUDIO_CHEAT == 0: - PERCENTAGE_CHEAT = avg(0.2, PERCENTAGE_CHEAT) - else: - PERCENTAGE_CHEAT = avg(0.4, PERCENTAGE_CHEAT) - else: - if head_pose.Y_AXIS_CHEAT == 0: - if audio.AUDIO_CHEAT == 0: - PERCENTAGE_CHEAT = avg(0.1, PERCENTAGE_CHEAT) - else: - PERCENTAGE_CHEAT = avg(0.4, PERCENTAGE_CHEAT) - else: - if audio.AUDIO_CHEAT == 0: - PERCENTAGE_CHEAT = avg(0.15, PERCENTAGE_CHEAT) - else: - PERCENTAGE_CHEAT = avg(0.25, PERCENTAGE_CHEAT) - else: - if head_pose.X_AXIS_CHEAT == 0: - if head_pose.Y_AXIS_CHEAT == 0: - if audio.AUDIO_CHEAT == 0: - PERCENTAGE_CHEAT = avg(0, PERCENTAGE_CHEAT) - else: - PERCENTAGE_CHEAT = avg(0.55, PERCENTAGE_CHEAT) - else: - if audio.AUDIO_CHEAT == 0: - PERCENTAGE_CHEAT = avg(0.55, PERCENTAGE_CHEAT) - else: - PERCENTAGE_CHEAT = avg(0.85, PERCENTAGE_CHEAT) - else: - if head_pose.Y_AXIS_CHEAT == 0: - if audio.AUDIO_CHEAT == 0: - PERCENTAGE_CHEAT = avg(0.6, PERCENTAGE_CHEAT) - else: - PERCENTAGE_CHEAT = avg(0.85, PERCENTAGE_CHEAT) - else: - if audio.AUDIO_CHEAT == 0: - PERCENTAGE_CHEAT = avg(0.5, PERCENTAGE_CHEAT) - else: - PERCENTAGE_CHEAT = avg(0.85, PERCENTAGE_CHEAT) - - if PERCENTAGE_CHEAT > CHEAT_THRESH: - GLOBAL_CHEAT = 1 - print("CHEATING") - else: - GLOBAL_CHEAT = 0 - print("Cheat percent: ", PERCENTAGE_CHEAT, GLOBAL_CHEAT) +from datetime import datetime + +@dataclass +class Detection: + """Class for storing detection results""" + frame: Optional[np.ndarray] = None + faces: List[Dict] = None + timestamp: datetime = None + is_cheating: bool = False + confidence: float = 0.0 + detected_processes: List[Dict] = None - except Exception as e: - logging.error(f"Error in process: {e}") - print("An error occurred during processing. Please check the logs.") - -def on_close(event): - global is_running - is_running = False - # Set flag to False when the window is closed - -def run_detection(): - global XDATA, YDATA, is_running - - try: - fig, axes = plt.subplots() - - axes.set_xlim(0, 200) - axes.set_ylim(0, 1) - line, = axes.plot(XDATA, YDATA, 'r-') - plt.title("Suspicious Behaviour Detection") - plt.xlabel("Time") - plt.ylabel("Cheat Probability") - - # Connect the close event to the callback - fig.canvas.mpl_connect('close_event', on_close) - - while is_running: - YDATA.pop(0) - YDATA.append(PERCENTAGE_CHEAT) - line.set_xdata(XDATA) - line.set_ydata(YDATA) - plt.draw() - plt.pause(1e-17) - process() - time.sleep(1 / 5) - - plt.close(fig) - - except Exception as e: - logging.error(f"Error in run_detection: {e}") - print("An error occurred while running the detection. Please check the logs.") - -if __name__ == "__main__": - try: - run_detection() - except KeyboardInterrupt: - logging.info("Detection interrupted by user.") - print("Terminated detection.") + def __post_init__(self): + if self.faces is None: + self.faces = [] + if self.detected_processes is None: + self.detected_processes = [] + + def add_face(self, face: Dict): + """Add a face detection result""" + self.faces.append(face) + + def get_face_count(self) -> int: + """Return number of faces detected""" + return len(self.faces) + + def get_process_count(self) -> int: + """Return number of detected unauthorized processes""" + return len(self.detected_processes) \ No newline at end of file diff --git a/Backend/model_training.py b/Backend/model_training.py index f97a7ac..ea37822 100644 --- a/Backend/model_training.py +++ b/Backend/model_training.py @@ -1,3 +1,5 @@ +# Backend/Offline-capabilities/model_training.py + import torch import torch.nn as nn import torchvision @@ -7,9 +9,19 @@ import numpy as np from torch.utils.data import Dataset, DataLoader import pandas as pd +from pathlib import Path +import os class ExamDataset(Dataset): def __init__(self, image_paths, annotations, transform=None): + """ + Dataset class for exam monitoring images + + Args: + image_paths (list): List of paths to images + annotations (list): List of dictionaries containing boxes and labels + transform (callable, optional): Optional transform to be applied on images + """ self.image_paths = image_paths self.annotations = annotations self.transform = transform @@ -36,9 +48,31 @@ def __getitem__(self, idx): return image, target -class InvigilationSystem: - def __init__(self): - # Initialize FRCNN for student detection and behavior analysis +class SPROCTORModelTrainer: + def __init__(self, data_dir=None, known_faces_path=None): + """ + Initialize the SPROCTOR model trainer + + Args: + data_dir (str): Path to the dataset directory + known_faces_path (str): Path to known faces database + """ + self.device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') + self.data_dir = Path(data_dir) if data_dir else Path('Backend/Dataset') + self.known_faces = self._load_known_faces(known_faces_path) + + # Initialize models + self.initialize_models() + + def _load_known_faces(self, known_faces_path): + """Load known faces database""" + if known_faces_path and os.path.exists(known_faces_path): + return pd.read_csv(known_faces_path) + return pd.DataFrame() # Empty DataFrame if no database exists + + def initialize_models(self): + """Initialize FRCNN and Face Recognition models""" + # Initialize FRCNN for behavior detection self.frcnn = fasterrcnn_resnet50_fpn(pretrained=True) num_classes = 3 # background, cheating, not_cheating in_features = self.frcnn.roi_heads.box_predictor.cls_score.in_features @@ -49,16 +83,86 @@ def __init__(self): num_features = self.face_cnn.fc.in_features self.face_cnn.fc = nn.Linear(num_features, len(self.known_faces)) - self.device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') + # Move models to device self.frcnn.to(self.device) self.face_cnn.to(self.device) - - def train_models(self, train_loader, num_epochs=10): - # Training parameters - params = [p for p in self.frcnn.parameters() if p.requires_grad] - optimizer = torch.optim.SGD(params, lr=0.005, momentum=0.9, weight_decay=0.0005) + def prepare_data_loaders(self, train_ratio=0.8): + """Prepare train and validation data loaders""" + # Load and split dataset + image_paths = list(self.data_dir.glob('images/*.jpg')) + annotations_path = self.data_dir / 'annotations.json' + + if not annotations_path.exists(): + raise FileNotFoundError(f"Annotations file not found at {annotations_path}") + + # Load annotations + import json + with open(annotations_path) as f: + annotations = json.load(f) + + # Split dataset + num_train = int(len(image_paths) * train_ratio) + train_paths = image_paths[:num_train] + train_annotations = annotations[:num_train] + val_paths = image_paths[num_train:] + val_annotations = annotations[num_train:] + + # Create transforms + transform = torchvision.transforms.Compose([ + torchvision.transforms.ToTensor(), + torchvision.transforms.Normalize( + mean=[0.485, 0.456, 0.406], + std=[0.229, 0.224, 0.225] + ) + ]) + + # Create datasets + train_dataset = ExamDataset(train_paths, train_annotations, transform) + val_dataset = ExamDataset(val_paths, val_annotations, transform) + + # Create data loaders + train_loader = DataLoader( + train_dataset, + batch_size=2, + shuffle=True, + collate_fn=self.collate_fn + ) + + val_loader = DataLoader( + val_dataset, + batch_size=2, + shuffle=False, + collate_fn=self.collate_fn + ) + + return train_loader, val_loader + + @staticmethod + def collate_fn(batch): + """Custom collate function for data loader""" + images = [] + targets = [] + for image, target in batch: + images.append(image) + targets.append(target) + return images, targets + + def train_models(self, num_epochs=10, learning_rate=0.005): + """Train both FRCNN and Face Recognition models""" + # Prepare data loaders + train_loader, val_loader = self.prepare_data_loaders() + + # Optimizers + frcnn_params = [p for p in self.frcnn.parameters() if p.requires_grad] + face_params = [p for p in self.face_cnn.parameters() if p.requires_grad] + + frcnn_optimizer = torch.optim.SGD(frcnn_params, lr=learning_rate, momentum=0.9) + face_optimizer = torch.optim.Adam(face_params, lr=learning_rate) + + # Training loop for epoch in range(num_epochs): + # Train FRCNN self.frcnn.train() total_loss = 0 @@ -66,70 +170,47 @@ def train_models(self, train_loader, num_epochs=10): images = [image.to(self.device) for image in images] targets = [{k: v.to(self.device) for k, v in t.items()} for t in targets] + frcnn_optimizer.zero_grad() loss_dict = self.frcnn(images, targets) losses = sum(loss for loss in loss_dict.values()) - optimizer.zero_grad() losses.backward() - optimizer.step() + frcnn_optimizer.step() total_loss += losses.item() - - print(f"Epoch [{epoch+1}/{num_epochs}], Loss: {total_loss/len(train_loader):.4f}") - - def process_frame(self, frame): - self.frcnn.eval() - self.face_cnn.eval() - - # Transform frame - transform = torchvision.transforms.Compose([ - torchvision.transforms.ToTensor(), - torchvision.transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) - ]) + + # Validate + self.frcnn.eval() + val_loss = 0 + + with torch.no_grad(): + for images, targets in val_loader: + images = [image.to(self.device) for image in images] + targets = [{k: v.to(self.device) for k, v in t.items()} for t in targets] + + loss_dict = self.frcnn(images, targets) + losses = sum(loss for loss in loss_dict.values()) + val_loss += losses.item() + + print(f"Epoch [{epoch+1}/{num_epochs}]") + print(f"Training Loss: {total_loss/len(train_loader):.4f}") + print(f"Validation Loss: {val_loss/len(val_loader):.4f}") + + def save_models(self, frcnn_path='models/frcnn.pth', face_cnn_path='models/face_cnn.pth'): + """Save trained models""" + # Create models directory if it doesn't exist + os.makedirs('models', exist_ok=True) - frame_tensor = transform(frame).unsqueeze(0).to(self.device) + # Save models + torch.save(self.frcnn.state_dict(), frcnn_path) + torch.save(self.face_cnn.state_dict(), face_cnn_path) - with torch.no_grad(): - predictions = self.frcnn(frame_tensor) - - # Process predictions - boxes = predictions[0]['boxes'].cpu().numpy() - scores = predictions[0]['scores'].cpu().numpy() - labels = predictions[0]['labels'].cpu().numpy() - - results = [] - for box, score, label in zip(boxes, scores, labels): - if score > 0.5: # Confidence threshold - x1, y1, x2, y2 = box.astype(int) - face_crop = frame[y1:y2, x1:x2] - - # Face recognition - face_tensor = transform(face_crop).unsqueeze(0).to(self.device) - face_prediction = self.face_cnn(face_tensor) - student_id = torch.argmax(face_prediction).item() - - results.append({ - 'box': box, - 'score': score, - 'is_cheating': label == 1, - 'student_id': student_id - }) - - return results - - def generate_report(self, results): - report_data = [] - for result in results: - report_data.append({ - 'timestamp': pd.Timestamp.now(), - 'student_id': result['student_id'], - 'confidence': result['score'], - 'behavior': 'Suspicious' if result['is_cheating'] else 'Normal' - }) - - df = pd.DataFrame(report_data) - df.to_excel('invigilation_report.xlsx', index=False) - return df + def load_models(self, frcnn_path='models/frcnn.pth', face_cnn_path='models/face_cnn.pth'): + """Load trained models""" + if os.path.exists(frcnn_path): + self.frcnn.load_state_dict(torch.load(frcnn_path)) + if os.path.exists(face_cnn_path): + self.face_cnn.load_state_dict(torch.load(face_cnn_path)) class FastRCNNPredictor(nn.Module): def __init__(self, in_channels, num_classes): diff --git a/Backend/mtcnn_face_detection.py b/Backend/mtcnn_face_detection.py index 391fc13..6b924f8 100644 --- a/Backend/mtcnn_face_detection.py +++ b/Backend/mtcnn_face_detection.py @@ -1,48 +1,202 @@ import cv2 +import numpy as np from mtcnn import MTCNN +import logging +import os +from typing import Dict, List, Tuple, Optional -# Initialize the MTCNN face detector -detector = MTCNN() +# Import project-specific modules +from detection import Detection +from processes import Process +from proctor_core import ProctorCore -# Initialize the webcam (0 is the default camera) -cap = cv2.VideoCapture(0) +class FaceDetector: + """MTCNN-based face detection class for the SPROCTOR system.""" + + def __init__(self, confidence_threshold: float = 0.9): + """ + Initialize the MTCNN face detector. + + Args: + confidence_threshold (float): Minimum confidence threshold for face detection + """ + self.detector = MTCNN(min_face_size=20, scale_factor=0.709) + self.confidence_threshold = confidence_threshold + self.logger = self._setup_logger() + + def _setup_logger(self) -> logging.Logger: + """Configure logging for the face detector.""" + logger = logging.getLogger('SPROCTOR.FaceDetector') + logger.setLevel(logging.INFO) + + if not logger.handlers: + handler = logging.StreamHandler() + formatter = logging.Formatter( + '%(asctime)s - %(name)s - %(levelname)s - %(message)s' + ) + handler.setFormatter(formatter) + logger.addHandler(handler) + + return logger + + def detect_faces(self, frame: np.ndarray) -> Tuple[List[Dict], np.ndarray]: + """ + Detect faces in a frame and return detection results. + + Args: + frame (np.ndarray): Input frame in BGR format + + Returns: + Tuple containing list of face detections and annotated frame + """ + try: + # Convert BGR to RGB for MTCNN + rgb_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) + + # Detect faces + faces = self.detector.detect_faces(rgb_frame) + + # Filter faces based on confidence + valid_faces = [ + face for face in faces + if face['confidence'] >= self.confidence_threshold + ] + + # Annotate frame + annotated_frame = self._annotate_frame(frame.copy(), valid_faces) + + self.logger.debug(f"Detected {len(valid_faces)} faces") + return valid_faces, annotated_frame + + except Exception as e: + self.logger.error(f"Error in face detection: {str(e)}") + return [], frame + + def _annotate_frame(self, frame: np.ndarray, faces: List[Dict]) -> np.ndarray: + """ + Draw detection results on frame. + + Args: + frame (np.ndarray): Input frame + faces (List[Dict]): List of detected faces + + Returns: + np.ndarray: Annotated frame + """ + for face in faces: + # Get detection data + box = face['box'] + keypoints = face['keypoints'] + confidence = face['confidence'] + + # Draw bounding box + x, y, width, height = box + cv2.rectangle( + frame, + (x, y), + (x + width, y + height), + (0, 255, 0), + 2 + ) + + # Draw confidence + cv2.putText( + frame, + f"Conf: {confidence:.2f}", + (x, y - 10), + cv2.FONT_HERSHEY_SIMPLEX, + 0.5, + (0, 255, 0), + 2 + ) + + # Draw facial keypoints + for point in keypoints.values(): + cv2.circle(frame, point, 2, (0, 155, 255), 2) + + return frame -while cap.isOpened(): - # Capture frame-by-frame - ret, frame = cap.read() - if not ret: - print("Failed to grab frame.") - break +class MTCNNDetectionProcess(Process): + """Process class for running MTCNN face detection.""" + + def __init__(self, core: ProctorCore): + """ + Initialize the MTCNN detection process. + + Args: + core (ProctorCore): Reference to the main proctor core + """ + super().__init__() + self.core = core + self.face_detector = FaceDetector() + self.logger = logging.getLogger('SPROCTOR.MTCNNProcess') + + def process_frame(self, frame: np.ndarray) -> Detection: + """ + Process a single frame and return detection results. + + Args: + frame (np.ndarray): Input frame + + Returns: + Detection: Detection results including face locations and confidence + """ + faces, annotated_frame = self.face_detector.detect_faces(frame) + + # Create detection object + detection = Detection() + detection.frame = annotated_frame + detection.faces = faces + detection.timestamp = self.core.get_timestamp() + + return detection + + def run(self): + """Main process loop.""" + self.logger.info("Starting MTCNN detection process") + + try: + cap = cv2.VideoCapture(0) + + while not self.stop_flag.is_set(): + ret, frame = cap.read() + if not ret: + self.logger.error("Failed to grab frame") + continue + + # Process frame + detection = self.process_frame(frame) + + # Send detection to core + self.core.process_detection(detection) + + # Check for quit command + if cv2.waitKey(1) & 0xFF == ord('q'): + break + + except Exception as e: + self.logger.error(f"Error in MTCNN process: {str(e)}") + + finally: + cap.release() + cv2.destroyAllWindows() + self.logger.info("MTCNN detection process stopped") - # Convert frame to RGB (MTCNN requires RGB) - rgb_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) - - # Detect faces in the frame - faces = detector.detect_faces(rgb_frame) - - # Loop over each face detected - for face in faces: - # Get the bounding box and keypoints - x, y, width, height = face['box'] - keypoints = face['keypoints'] - - # Draw bounding box - cv2.rectangle(frame, (x, y), (x + width, y + height), (0, 255, 0), 2) - - # Draw keypoints - cv2.circle(frame, (keypoints['left_eye']), 2, (0, 155, 255), 2) - cv2.circle(frame, (keypoints['right_eye']), 2, (0, 155, 255), 2) - cv2.circle(frame, (keypoints['nose']), 2, (0, 155, 255), 2) - cv2.circle(frame, (keypoints['mouth_left']), 2, (0, 155, 255), 2) - cv2.circle(frame, (keypoints['mouth_right']), 2, (0, 155, 255), 2) - - # Display the resulting frame - cv2.imshow('MTCNN Real-Time Face Detection', frame) - - # Break the loop on 'q' key press - if cv2.waitKey(1) & 0xFF == ord('q'): - break - -# Release the capture and close any OpenCV windows -cap.release() -cv2.destroyAllWindows() +if __name__ == "__main__": + # Simple test code + detector = FaceDetector() + cap = cv2.VideoCapture(0) + + while cap.isOpened(): + ret, frame = cap.read() + if not ret: + break + + faces, annotated_frame = detector.detect_faces(frame) + cv2.imshow('MTCNN Face Detection', annotated_frame) + + if cv2.waitKey(1) & 0xFF == ord('q'): + break + + cap.release() + cv2.destroyAllWindows() \ No newline at end of file diff --git a/Backend/processes.py b/Backend/processes.py index 90175cb..8fb18b2 100644 --- a/Backend/processes.py +++ b/Backend/processes.py @@ -1,39 +1,157 @@ +import threading +from abc import ABC, abstractmethod +import logging +from typing import Optional, List, Dict +import numpy as np import wmi import time +from detection import Detection -# Start the timer -start = time.time() +class Process(ABC): + """Abstract base class for all detection processes""" + + def __init__(self): + self.stop_flag = threading.Event() + self.thread: Optional[threading.Thread] = None + self.logger = logging.getLogger(f'SPROCTOR.{self.__class__.__name__}') + + @abstractmethod + def process_frame(self, frame: np.ndarray) -> Detection: + """Process a single frame""" + pass + + @abstractmethod + def run(self): + """Main process loop""" + pass + + def start(self): + """Start the process thread""" + self.thread = threading.Thread(target=self.run) + self.thread.daemon = True + self.thread.start() + self.logger.info(f"Started {self.__class__.__name__}") + + def stop(self): + """Stop the process thread""" + self.stop_flag.set() + if self.thread: + self.thread.join() + self.logger.info(f"Stopped {self.__class__.__name__}") -# Initializing the WMI constructor -f = wmi.WMI() +class ProcessMonitor: + """Class for monitoring system processes""" + + NOT_ALLOWED_APPS = [ + "Discord", + "Whatsapp", + "Telegram", + "Zoom", + "Skype" + ] + + def __init__(self): + self.logger = logging.getLogger('SPROCTOR.ProcessMonitor') + self.wmi = wmi.WMI() + self.detected_processes: Dict[str, int] = {} + + def scan_processes(self) -> List[Dict[str, str]]: + """ + Scan for not allowed processes + Returns list of detected processes with their details + """ + start_time = time.time() + detected = [] + + try: + processes = self.wmi.Win32_Process() + + for process in processes: + for app in self.NOT_ALLOWED_APPS: + if app.lower() in process.Name.lower(): + process_info = { + 'name': process.Name, + 'pid': process.ProcessId, + 'path': process.ExecutablePath or 'Unknown' + } + detected.append(process_info) + self.detected_processes[process.Name] = process.ProcessId + self.logger.warning(f"Detected unauthorized application: {process.Name}") + + execution_time = time.time() - start_time + self.logger.info(f"Process scan completed in {execution_time:.2f} seconds") + return detected + + except Exception as e: + self.logger.error(f"Error scanning processes: {str(e)}") + return [] + + def is_cheating(self) -> bool: + """Check if any unauthorized applications are running""" + return len(self.detected_processes) > 0 + + def get_detected_apps(self) -> List[str]: + """Get list of currently detected unauthorized applications""" + return list(self.detected_processes.keys()) -# List of not allowed applications -notAllowed = [ - "Discord", - "Whatsapp", - "Telegram", - "Zoom", - "Skype" -] +class ProcessMonitoringProcess(Process): + """Process class for monitoring system processes""" + + def __init__(self, core): + super().__init__() + self.core = core + self.monitor = ProcessMonitor() + self.scan_interval = 5 # seconds + + def process_frame(self, frame: np.ndarray) -> Detection: + """Process a single frame (not used in process monitoring)""" + return Detection() + + def run(self): + """Main monitoring loop""" + self.logger.info("Starting process monitoring") + + while not self.stop_flag.is_set(): + try: + # Scan for unauthorized processes + detected = self.monitor.scan_processes() + + if detected: + # Create detection event + detection = Detection() + detection.timestamp = self.core.get_timestamp() + detection.is_cheating = True + detection.confidence = 0.9 + detection.detected_processes = detected + + # Send detection to core + self.core.process_detection(detection) + + # Wait for next scan + time.sleep(self.scan_interval) + + except Exception as e: + self.logger.error(f"Error in process monitoring: {str(e)}") + time.sleep(1) + + self.logger.info("Process monitoring stopped") -print("Name Id") -print("-------------------------------") +def main(): + """Test function for process monitoring""" + logging.basicConfig(level=logging.INFO) + + monitor = ProcessMonitor() + print("\nScanning for unauthorized applications...") + detected = monitor.scan_processes() + + if detected: + print("\nDetected unauthorized applications:") + print("Name PID") + print("-" * 40) + for process in detected: + print(f"{process['name']:<30} {process['pid']}") + else: + print("\nNo unauthorized applications detected") -# Fetching all running processes -try: - processes = f.Win32_Process() - # Loop through each process - for process in processes: - for name in notAllowed: - # Check if the process name matches any of the not allowed names - if name.lower() in process.Name.lower(): - print(f"{process.Name:<30} {process.ProcessId}") - - print("\nFound {} processes".format(len(processes))) - -except Exception as e: - print("An error occurred: ", str(e)) - -# Calculate execution time -end = time.time() -print("Executed in {:.2f} seconds".format(end - start)) +if __name__ == "__main__": + main() \ No newline at end of file