File size: 4,103 Bytes
1ca9b28
 
 
 
cfbaa51
 
1ca9b28
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4cdc77a
1ca9b28
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
import threading
from queue import Queue
import queue
import cv2

from CPR_Module.Common.logging_config import cpr_logger

class ThreadedCamera:
    def __init__(self, source, requested_fps = 30):

        # The constructor of OpenCV's VideoCapture class automatically opens the camera
        self.cap = cv2.VideoCapture(source)
        if not self.cap.isOpened():
            raise ValueError(f"[VIDEO CAPTURE] Unable to open camera source: {source}")
        cpr_logger.info(f"[VIDEO CAPTURE] Camera source opened: {source}")

        # Attempt to configure the camera to the requested FPS
        # Which is set to the value we have been working on with recorded videos
        # .set() returns True if the camera acknowledged the request, not if it actually achieved the FPS.
        set_success = self.cap.set(cv2.CAP_PROP_FPS, requested_fps)

        # Get the actual FPS from the camera
        # This is the FPS that the camera is actually using, which may differ from the requested FPS.
        actual_fps = self.cap.get(cv2.CAP_PROP_FPS)
        self.fps = actual_fps

        cpr_logger.info(f"[VIDEO CAPTURE] Requested FPS: {requested_fps}, Set Success: {set_success}, Actual FPS: {actual_fps}")

        # The buffer should be able to hold a lag of up to "number_of_seconds_to_buffer" seconds
        number_of_seconds_to_buffer = 5
        queue_size = int(actual_fps * number_of_seconds_to_buffer)
        self.q = Queue(maxsize=queue_size)
        cpr_logger.info(f"[VIDEO CAPTURE] Queue size: {queue_size}")

        # Set a flag to indicate that the camera is running
        self.running = threading.Event()
        self.running.set()  # Initial state = running
        cpr_logger.info(f"[VIDEO CAPTURE] Camera running: {self.running.is_set()}")

        self.number_of_total_frames = 0
        self.number_of_dropped_frames = 0

        self.thread = None

    def start_capture(self):
        # Clear any existing frames in queue
        while not self.q.empty():
            self.q.get()

        # threading.Thread() initialize a new thread
        # target=self._reader specify the method (_reader) the thread will execute
        self.thread = threading.Thread(target=self._reader)
        cpr_logger.info(f"[VIDEO CAPTURE] Thread initialized: {self.thread}")
        
        # Set the thread as a daemon thread:
        #   Daemon threads automatically exit when the main program exits
        #   They run in the background and don't block program termination
        self.thread.daemon = True
        cpr_logger.info(f"[VIDEO CAPTURE] Thread daemon: {self.thread.daemon}")

        # Start the thread execution:
        #   Call the _reader method in parallel with the main program
        self.thread.start()

    def _reader(self):
        while self.running.is_set():
            ret, frame = self.cap.read()
            if not ret:
                cpr_logger.info("Camera disconnected")
                self.q.put(None)  # Sentinel for clean exit
                break
                
            try:
                self.number_of_total_frames += 1
                self.q.put(frame, timeout=0.1)
            except queue.Full:
                cpr_logger.info("Frame dropped")
                self.number_of_dropped_frames += 1

    def read(self):
        return self.q.get()

    def release(self):
        #! Not an error
        cpr_logger.error(f"[VIDEO CAPTURE] Total frames: {self.number_of_total_frames}, Dropped frames: {self.number_of_dropped_frames}")
        
        self.running.clear()
        
        # First release the capture to unblock pending reads
        self.cap.release()  # MOVED THIS LINE UP
        
        # Then join the thread
        self.thread.join(timeout=1.0)
        
        if self.thread.is_alive():
            cpr_logger.info("Warning: Thread didn't terminate cleanly")
        # Removed redundant self.cap.release()

    def isOpened(self):
        return self.running.is_set() and self.cap.isOpened()

    def __del__(self):
        if self.running.is_set():  # Only release if not already done
            self.release()