mirror of
				https://github.com/blakeblackshear/frigate.git
				synced 2025-10-27 10:52:11 +01:00 
			
		
		
		
	store the best recent person image and reconnect the RTSP stream if unable to grab several consecutive frames
This commit is contained in:
		
							parent
							
								
									2e3c9da650
								
							
						
					
					
						commit
						df7b90e367
					
				@ -2,6 +2,7 @@
 | 
				
			|||||||
This results in a MJPEG stream with objects identified that has a lower latency than directly viewing the RTSP feed with VLC.
 | 
					This results in a MJPEG stream with objects identified that has a lower latency than directly viewing the RTSP feed with VLC.
 | 
				
			||||||
- Prioritizes realtime processing over frames per second. Dropping frames is fine.
 | 
					- Prioritizes realtime processing over frames per second. Dropping frames is fine.
 | 
				
			||||||
- OpenCV runs in a separate process so it can grab frames as quickly as possible to ensure there aren't old frames in the buffer
 | 
					- OpenCV runs in a separate process so it can grab frames as quickly as possible to ensure there aren't old frames in the buffer
 | 
				
			||||||
 | 
					- Allows you to define specific regions (squares) in the image to look for motion/objects
 | 
				
			||||||
- Motion detection runs in a separate process per region and signals to object detection to avoid wasting CPU cycles to look for objects when there is no motion
 | 
					- Motion detection runs in a separate process per region and signals to object detection to avoid wasting CPU cycles to look for objects when there is no motion
 | 
				
			||||||
- Object detection with Tensorflow runs in a separate process per region and ignores frames that are more than 0.5 seconds old
 | 
					- Object detection with Tensorflow runs in a separate process per region and ignores frames that are more than 0.5 seconds old
 | 
				
			||||||
- Uses shared memory arrays for handing frames between processes
 | 
					- Uses shared memory arrays for handing frames between processes
 | 
				
			||||||
@ -45,16 +46,17 @@ Access the mjpeg stream at http://localhost:5000
 | 
				
			|||||||
- [x] Add last will and availability for MQTT
 | 
					- [x] Add last will and availability for MQTT
 | 
				
			||||||
- [ ] Build tensorflow from source for CPU optimizations
 | 
					- [ ] Build tensorflow from source for CPU optimizations
 | 
				
			||||||
- [ ] Add ability to turn detection on and off via MQTT
 | 
					- [ ] Add ability to turn detection on and off via MQTT
 | 
				
			||||||
- [ ] MQTT reconnect if disconnected
 | 
					- [ ] MQTT reconnect if disconnected (and resend availability message)
 | 
				
			||||||
- [ ] MQTT motion occasionally gets stuck ON
 | 
					- [ ] MQTT motion occasionally gets stuck ON
 | 
				
			||||||
- [ ] Output movie clips of people for notifications, etc.
 | 
					- [ ] Output movie clips of people for notifications, etc.
 | 
				
			||||||
 | 
					- [x] Store highest scoring person frame from most recent event
 | 
				
			||||||
- [x] Add a max size for motion and objects (height/width > 1.5, total area > 1500 and < 100,000)
 | 
					- [x] Add a max size for motion and objects (height/width > 1.5, total area > 1500 and < 100,000)
 | 
				
			||||||
- [x] Make motion less sensitive to rain
 | 
					- [x] Make motion less sensitive to rain
 | 
				
			||||||
- [x] Use Events or Conditions to signal between threads rather than polling a value
 | 
					- [x] Use Events or Conditions to signal between threads rather than polling a value
 | 
				
			||||||
- [x] Implement a debug option to save images with detected objects
 | 
					- [x] Implement a debug option to save images with detected objects
 | 
				
			||||||
- [x] Only report if x% of the recent frames have a person to avoid single frame false positives (maybe take an average of the person scores in the past x frames?)
 | 
					- [x] Only report if x% of the recent frames have a person to avoid single frame false positives (maybe take an average of the person scores in the past x frames?)
 | 
				
			||||||
- [x] Filter out detected objects that are not the right size
 | 
					- [x] Filter out detected objects that are not the right size
 | 
				
			||||||
- [ ] Make resilient to network drop outs
 | 
					- [x] Make RTSP resilient to network drop outs
 | 
				
			||||||
- [ ] Merge bounding boxes that span multiple regions
 | 
					- [ ] Merge bounding boxes that span multiple regions
 | 
				
			||||||
- [ ] Switch to a config file
 | 
					- [ ] Switch to a config file
 | 
				
			||||||
- [ ] Allow motion regions to be different than object detection regions
 | 
					- [ ] Allow motion regions to be different than object detection regions
 | 
				
			||||||
 | 
				
			|||||||
@ -11,12 +11,12 @@ import json
 | 
				
			|||||||
from contextlib import closing
 | 
					from contextlib import closing
 | 
				
			||||||
import numpy as np
 | 
					import numpy as np
 | 
				
			||||||
from object_detection.utils import visualization_utils as vis_util
 | 
					from object_detection.utils import visualization_utils as vis_util
 | 
				
			||||||
from flask import Flask, Response, make_response
 | 
					from flask import Flask, Response, make_response, send_file
 | 
				
			||||||
import paho.mqtt.client as mqtt
 | 
					import paho.mqtt.client as mqtt
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from frigate.util import tonumpyarray
 | 
					from frigate.util import tonumpyarray
 | 
				
			||||||
from frigate.mqtt import MqttMotionPublisher, MqttObjectPublisher
 | 
					from frigate.mqtt import MqttMotionPublisher, MqttObjectPublisher
 | 
				
			||||||
from frigate.objects import ObjectParser, ObjectCleaner
 | 
					from frigate.objects import ObjectParser, ObjectCleaner, BestPersonFrame
 | 
				
			||||||
from frigate.motion import detect_motion
 | 
					from frigate.motion import detect_motion
 | 
				
			||||||
from frigate.video import fetch_frames, FrameTracker
 | 
					from frigate.video import fetch_frames, FrameTracker
 | 
				
			||||||
from frigate.object_detection import detect_objects
 | 
					from frigate.object_detection import detect_objects
 | 
				
			||||||
@ -126,6 +126,11 @@ def main():
 | 
				
			|||||||
        recent_motion_frames, motion_changed, [region['motion_detected'] for region in regions])
 | 
					        recent_motion_frames, motion_changed, [region['motion_detected'] for region in regions])
 | 
				
			||||||
    frame_tracker.start()
 | 
					    frame_tracker.start()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # start a thread to store the highest scoring recent person frame
 | 
				
			||||||
 | 
					    best_person_frame = BestPersonFrame(objects_parsed, recent_motion_frames, DETECTED_OBJECTS, 
 | 
				
			||||||
 | 
					        motion_changed, [region['motion_detected'] for region in regions])
 | 
				
			||||||
 | 
					    best_person_frame.start()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # start a thread to parse objects from the queue
 | 
					    # start a thread to parse objects from the queue
 | 
				
			||||||
    object_parser = ObjectParser(object_queue, objects_parsed, DETECTED_OBJECTS)
 | 
					    object_parser = ObjectParser(object_queue, objects_parsed, DETECTED_OBJECTS)
 | 
				
			||||||
    object_parser.start()
 | 
					    object_parser.start()
 | 
				
			||||||
@ -168,6 +173,14 @@ def main():
 | 
				
			|||||||
    # create a flask app that encodes frames a mjpeg on demand
 | 
					    # create a flask app that encodes frames a mjpeg on demand
 | 
				
			||||||
    app = Flask(__name__)
 | 
					    app = Flask(__name__)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @app.route('/best_person.jpg')
 | 
				
			||||||
 | 
					    def best_person():
 | 
				
			||||||
 | 
					        frame = np.zeros(frame_shape, np.uint8) if best_person_frame.best_frame is None else best_person_frame.best_frame
 | 
				
			||||||
 | 
					        ret, jpg = cv2.imencode('.jpg', frame)
 | 
				
			||||||
 | 
					        response = make_response(jpg.tobytes())
 | 
				
			||||||
 | 
					        response.headers['Content-Type'] = 'image/jpg'
 | 
				
			||||||
 | 
					        return response
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @app.route('/')
 | 
					    @app.route('/')
 | 
				
			||||||
    def index():
 | 
					    def index():
 | 
				
			||||||
        # return a multipart response
 | 
					        # return a multipart response
 | 
				
			||||||
@ -219,6 +232,7 @@ def main():
 | 
				
			|||||||
    for motion_process in motion_processes:
 | 
					    for motion_process in motion_processes:
 | 
				
			||||||
        motion_process.join()
 | 
					        motion_process.join()
 | 
				
			||||||
    frame_tracker.join()
 | 
					    frame_tracker.join()
 | 
				
			||||||
 | 
					    best_person_frame.join()
 | 
				
			||||||
    object_parser.join()
 | 
					    object_parser.join()
 | 
				
			||||||
    object_cleaner.join()
 | 
					    object_cleaner.join()
 | 
				
			||||||
    mqtt_publisher.join()
 | 
					    mqtt_publisher.join()
 | 
				
			||||||
 | 
				
			|||||||
@ -1,7 +1,8 @@
 | 
				
			|||||||
import time
 | 
					import time
 | 
				
			||||||
import datetime
 | 
					import datetime
 | 
				
			||||||
import threading
 | 
					import threading
 | 
				
			||||||
 | 
					import cv2
 | 
				
			||||||
 | 
					from object_detection.utils import visualization_utils as vis_util
 | 
				
			||||||
class ObjectParser(threading.Thread):
 | 
					class ObjectParser(threading.Thread):
 | 
				
			||||||
    def __init__(self, object_queue, objects_parsed, detected_objects):
 | 
					    def __init__(self, object_queue, objects_parsed, detected_objects):
 | 
				
			||||||
        threading.Thread.__init__(self)
 | 
					        threading.Thread.__init__(self)
 | 
				
			||||||
@ -45,4 +46,78 @@ class ObjectCleaner(threading.Thread):
 | 
				
			|||||||
                    self._objects_parsed.notify_all()
 | 
					                    self._objects_parsed.notify_all()
 | 
				
			||||||
            
 | 
					            
 | 
				
			||||||
            # wait a bit before checking for more expired frames
 | 
					            # wait a bit before checking for more expired frames
 | 
				
			||||||
            time.sleep(0.2)
 | 
					            time.sleep(0.2)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Maintains the frame and person with the highest score from the most recent
 | 
				
			||||||
 | 
					# motion event
 | 
				
			||||||
 | 
					class BestPersonFrame(threading.Thread):
 | 
				
			||||||
 | 
					    def __init__(self, objects_parsed, recent_frames, detected_objects, motion_changed, motion_regions):
 | 
				
			||||||
 | 
					        threading.Thread.__init__(self)
 | 
				
			||||||
 | 
					        self.objects_parsed = objects_parsed
 | 
				
			||||||
 | 
					        self.recent_frames = recent_frames
 | 
				
			||||||
 | 
					        self.detected_objects = detected_objects
 | 
				
			||||||
 | 
					        self.motion_changed = motion_changed
 | 
				
			||||||
 | 
					        self.motion_regions = motion_regions
 | 
				
			||||||
 | 
					        self.best_person = None
 | 
				
			||||||
 | 
					        self.best_frame = None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def run(self):
 | 
				
			||||||
 | 
					        motion_start = 0.0
 | 
				
			||||||
 | 
					        motion_end = 0.0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        while True:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					             # while there is motion
 | 
				
			||||||
 | 
					            while len([r for r in self.motion_regions if r.is_set()]) > 0:
 | 
				
			||||||
 | 
					                # wait until objects have been parsed
 | 
				
			||||||
 | 
					                with self.objects_parsed:
 | 
				
			||||||
 | 
					                    self.objects_parsed.wait()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                # make a copy of detected objects
 | 
				
			||||||
 | 
					                detected_objects = self.detected_objects.copy()
 | 
				
			||||||
 | 
					                detected_people = [obj for obj in detected_objects if obj['name'] == 'person']
 | 
				
			||||||
 | 
					                # make a copy of the recent frames
 | 
				
			||||||
 | 
					                recent_frames = self.recent_frames.copy()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                # get the highest scoring person
 | 
				
			||||||
 | 
					                new_best_person = max(detected_people, key=lambda x:x['score'], default=self.best_person)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                # if there isnt a person, continue
 | 
				
			||||||
 | 
					                if new_best_person is None:
 | 
				
			||||||
 | 
					                    continue
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                # if there is no current best_person
 | 
				
			||||||
 | 
					                if self.best_person is None:
 | 
				
			||||||
 | 
					                    self.best_person = new_best_person
 | 
				
			||||||
 | 
					                # if there is already a best_person
 | 
				
			||||||
 | 
					                else:
 | 
				
			||||||
 | 
					                    now = datetime.datetime.now().timestamp()
 | 
				
			||||||
 | 
					                    # if the new best person is a higher score than the current best person 
 | 
				
			||||||
 | 
					                    # or the current person is more than 1 minute old, use the new best person
 | 
				
			||||||
 | 
					                    if new_best_person['score'] > self.best_person['score'] or (now - self.best_person['frame_time']) > 60:
 | 
				
			||||||
 | 
					                        self.best_person = new_best_person
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                if not self.best_person is None and self.best_person['frame_time'] in recent_frames:
 | 
				
			||||||
 | 
					                    best_frame = recent_frames[self.best_person['frame_time']]
 | 
				
			||||||
 | 
					                    best_frame = cv2.cvtColor(best_frame, cv2.COLOR_BGR2RGB)
 | 
				
			||||||
 | 
					                    # draw the bounding box on the frame
 | 
				
			||||||
 | 
					                    vis_util.draw_bounding_box_on_image_array(best_frame,
 | 
				
			||||||
 | 
					                        self.best_person['ymin'],
 | 
				
			||||||
 | 
					                        self.best_person['xmin'],
 | 
				
			||||||
 | 
					                        self.best_person['ymax'],
 | 
				
			||||||
 | 
					                        self.best_person['xmax'],
 | 
				
			||||||
 | 
					                        color='red',
 | 
				
			||||||
 | 
					                        thickness=2,
 | 
				
			||||||
 | 
					                        display_str_list=["{}: {}%".format(self.best_person['name'],int(self.best_person['score']*100))],
 | 
				
			||||||
 | 
					                        use_normalized_coordinates=False)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    # convert back to BGR
 | 
				
			||||||
 | 
					                    self.best_frame = cv2.cvtColor(best_frame, cv2.COLOR_RGB2BGR)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            motion_end = datetime.datetime.now().timestamp()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            # wait for the global motion flag to change
 | 
				
			||||||
 | 
					            with self.motion_changed:
 | 
				
			||||||
 | 
					                self.motion_changed.wait()
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            motion_start = datetime.datetime.now().timestamp()
 | 
				
			||||||
@ -16,6 +16,7 @@ def fetch_frames(shared_arr, shared_frame_time, frame_lock, frame_ready, frame_s
 | 
				
			|||||||
    # keep the buffer small so we minimize old data
 | 
					    # keep the buffer small so we minimize old data
 | 
				
			||||||
    video.set(cv2.CAP_PROP_BUFFERSIZE,1)
 | 
					    video.set(cv2.CAP_PROP_BUFFERSIZE,1)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    bad_frame_counter = 0
 | 
				
			||||||
    while True:
 | 
					    while True:
 | 
				
			||||||
        # check if the video stream is still open, and reopen if needed
 | 
					        # check if the video stream is still open, and reopen if needed
 | 
				
			||||||
        if not video.isOpened():
 | 
					        if not video.isOpened():
 | 
				
			||||||
@ -38,9 +39,20 @@ def fetch_frames(shared_arr, shared_frame_time, frame_lock, frame_ready, frame_s
 | 
				
			|||||||
                # Notify with the condition that a new frame is ready
 | 
					                # Notify with the condition that a new frame is ready
 | 
				
			||||||
                with frame_ready:
 | 
					                with frame_ready:
 | 
				
			||||||
                    frame_ready.notify_all()
 | 
					                    frame_ready.notify_all()
 | 
				
			||||||
 | 
					                bad_frame_counter = 0
 | 
				
			||||||
 | 
					            else:
 | 
				
			||||||
 | 
					                print("Unable to decode frame")
 | 
				
			||||||
 | 
					                bad_frame_counter += 1
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            print("Unable to grab a frame")
 | 
				
			||||||
 | 
					            bad_frame_counter += 1
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        if bad_frame_counter > 100:
 | 
				
			||||||
 | 
					            video.release()
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    video.release()
 | 
					    video.release()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Stores 2 seconds worth of frames when motion is detected so they can be used for other threads
 | 
				
			||||||
class FrameTracker(threading.Thread):
 | 
					class FrameTracker(threading.Thread):
 | 
				
			||||||
    def __init__(self, shared_frame, frame_time, frame_ready, frame_lock, recent_frames, motion_changed, motion_regions):
 | 
					    def __init__(self, shared_frame, frame_time, frame_ready, frame_lock, recent_frames, motion_changed, motion_regions):
 | 
				
			||||||
        threading.Thread.__init__(self)
 | 
					        threading.Thread.__init__(self)
 | 
				
			||||||
@ -78,8 +90,6 @@ class FrameTracker(threading.Thread):
 | 
				
			|||||||
                    if (now - k) > 2:
 | 
					                    if (now - k) > 2:
 | 
				
			||||||
                        del self.recent_frames[k]
 | 
					                        del self.recent_frames[k]
 | 
				
			||||||
                
 | 
					                
 | 
				
			||||||
                print(stored_frame_times)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            # wait for the global motion flag to change
 | 
					            # wait for the global motion flag to change
 | 
				
			||||||
            with self.motion_changed:
 | 
					            with self.motion_changed:
 | 
				
			||||||
                self.motion_changed.wait()
 | 
					                self.motion_changed.wait()
 | 
				
			||||||
		Loading…
	
		Reference in New Issue
	
	Block a user