mirror of
https://github.com/blakeblackshear/frigate.git
synced 2024-11-21 19:07:46 +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