http.py 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268
  1. import base64
  2. import datetime
  3. import logging
  4. import os
  5. import time
  6. from functools import reduce
  7. import cv2
  8. import numpy as np
  9. from flask import (Blueprint, Flask, Response, current_app, jsonify,
  10. make_response, request)
  11. from peewee import SqliteDatabase, operator, fn, DoesNotExist
  12. from playhouse.shortcuts import model_to_dict
  13. from frigate.models import Event
  14. from frigate.stats import stats_snapshot
  15. from frigate.util import calculate_region
  16. from frigate.version import VERSION
  17. logger = logging.getLogger(__name__)
  18. bp = Blueprint('frigate', __name__)
  19. def create_app(frigate_config, database: SqliteDatabase, stats_tracking, detected_frames_processor):
  20. app = Flask(__name__)
  21. @app.before_request
  22. def _db_connect():
  23. database.connect()
  24. @app.teardown_request
  25. def _db_close(exc):
  26. if not database.is_closed():
  27. database.close()
  28. app.frigate_config = frigate_config
  29. app.stats_tracking = stats_tracking
  30. app.detected_frames_processor = detected_frames_processor
  31. app.register_blueprint(bp)
  32. return app
  33. @bp.route('/')
  34. def is_healthy():
  35. return "Frigate is running. Alive and healthy!"
  36. @bp.route('/events/summary')
  37. def events_summary():
  38. has_clip = request.args.get('has_clip', type=int)
  39. has_snapshot = request.args.get('has_snapshot', type=int)
  40. clauses = []
  41. if not has_clip is None:
  42. clauses.append((Event.has_clip == has_clip))
  43. if not has_snapshot is None:
  44. clauses.append((Event.has_snapshot == has_snapshot))
  45. if len(clauses) == 0:
  46. clauses.append((1 == 1))
  47. groups = (
  48. Event
  49. .select(
  50. Event.camera,
  51. Event.label,
  52. fn.strftime('%Y-%m-%d', fn.datetime(Event.start_time, 'unixepoch', 'localtime')).alias('day'),
  53. Event.zones,
  54. fn.COUNT(Event.id).alias('count')
  55. )
  56. .where(reduce(operator.and_, clauses))
  57. .group_by(
  58. Event.camera,
  59. Event.label,
  60. fn.strftime('%Y-%m-%d', fn.datetime(Event.start_time, 'unixepoch', 'localtime')),
  61. Event.zones
  62. )
  63. )
  64. return jsonify([e for e in groups.dicts()])
  65. @bp.route('/events/<id>')
  66. def event(id):
  67. try:
  68. return model_to_dict(Event.get(Event.id == id))
  69. except DoesNotExist:
  70. return "Event not found", 404
  71. @bp.route('/events/<id>/thumbnail.jpg')
  72. def event_snapshot(id):
  73. format = request.args.get('format', 'ios')
  74. thumbnail_bytes = None
  75. try:
  76. event = Event.get(Event.id == id)
  77. thumbnail_bytes = base64.b64decode(event.thumbnail)
  78. except DoesNotExist:
  79. # see if the object is currently being tracked
  80. try:
  81. for camera_state in current_app.detected_frames_processor.camera_states.values():
  82. if id in camera_state.tracked_objects:
  83. tracked_obj = camera_state.tracked_objects.get(id)
  84. if not tracked_obj is None:
  85. thumbnail_bytes = tracked_obj.get_jpg_bytes()
  86. except:
  87. return "Event not found", 404
  88. if thumbnail_bytes is None:
  89. return "Event not found", 404
  90. # android notifications prefer a 2:1 ratio
  91. if format == 'android':
  92. jpg_as_np = np.frombuffer(thumbnail_bytes, dtype=np.uint8)
  93. img = cv2.imdecode(jpg_as_np, flags=1)
  94. thumbnail = cv2.copyMakeBorder(img, 0, 0, int(img.shape[1]*0.5), int(img.shape[1]*0.5), cv2.BORDER_CONSTANT, (0,0,0))
  95. ret, jpg = cv2.imencode('.jpg', thumbnail)
  96. thumbnail_bytes = jpg.tobytes()
  97. response = make_response(thumbnail_bytes)
  98. response.headers['Content-Type'] = 'image/jpg'
  99. return response
  100. @bp.route('/events')
  101. def events():
  102. limit = request.args.get('limit', 100)
  103. camera = request.args.get('camera')
  104. label = request.args.get('label')
  105. zone = request.args.get('zone')
  106. after = request.args.get('after', type=int)
  107. before = request.args.get('before', type=int)
  108. has_clip = request.args.get('has_clip', type=int)
  109. has_snapshot = request.args.get('has_snapshot', type=int)
  110. clauses = []
  111. if camera:
  112. clauses.append((Event.camera == camera))
  113. if label:
  114. clauses.append((Event.label == label))
  115. if zone:
  116. clauses.append((Event.zones.cast('text') % f"*\"{zone}\"*"))
  117. if after:
  118. clauses.append((Event.start_time >= after))
  119. if before:
  120. clauses.append((Event.start_time <= before))
  121. if not has_clip is None:
  122. clauses.append((Event.has_clip == has_clip))
  123. if not has_snapshot is None:
  124. clauses.append((Event.has_snapshot == has_snapshot))
  125. if len(clauses) == 0:
  126. clauses.append((1 == 1))
  127. events = (Event.select()
  128. .where(reduce(operator.and_, clauses))
  129. .order_by(Event.start_time.desc())
  130. .limit(limit))
  131. return jsonify([model_to_dict(e) for e in events])
  132. @bp.route('/config')
  133. def config():
  134. return jsonify(current_app.frigate_config.to_dict())
  135. @bp.route('/version')
  136. def version():
  137. return VERSION
  138. @bp.route('/stats')
  139. def stats():
  140. stats = stats_snapshot(current_app.stats_tracking)
  141. return jsonify(stats)
  142. @bp.route('/<camera_name>/<label>/best.jpg')
  143. def best(camera_name, label):
  144. if camera_name in current_app.frigate_config.cameras:
  145. best_object = current_app.detected_frames_processor.get_best(camera_name, label)
  146. best_frame = best_object.get('frame')
  147. if best_frame is None:
  148. best_frame = np.zeros((720,1280,3), np.uint8)
  149. else:
  150. best_frame = cv2.cvtColor(best_frame, cv2.COLOR_YUV2BGR_I420)
  151. crop = bool(request.args.get('crop', 0, type=int))
  152. if crop:
  153. box = best_object.get('box', (0,0,300,300))
  154. region = calculate_region(best_frame.shape, box[0], box[1], box[2], box[3], 1.1)
  155. best_frame = best_frame[region[1]:region[3], region[0]:region[2]]
  156. height = int(request.args.get('h', str(best_frame.shape[0])))
  157. width = int(height*best_frame.shape[1]/best_frame.shape[0])
  158. best_frame = cv2.resize(best_frame, dsize=(width, height), interpolation=cv2.INTER_AREA)
  159. ret, jpg = cv2.imencode('.jpg', best_frame)
  160. response = make_response(jpg.tobytes())
  161. response.headers['Content-Type'] = 'image/jpg'
  162. return response
  163. else:
  164. return "Camera named {} not found".format(camera_name), 404
  165. @bp.route('/<camera_name>')
  166. def mjpeg_feed(camera_name):
  167. fps = int(request.args.get('fps', '3'))
  168. height = int(request.args.get('h', '360'))
  169. draw_options = {
  170. 'bounding_boxes': request.args.get('bbox', type=int),
  171. 'timestamp': request.args.get('timestamp', type=int),
  172. 'zones': request.args.get('zones', type=int),
  173. 'mask': request.args.get('mask', type=int),
  174. 'motion_boxes': request.args.get('motion', type=int),
  175. 'regions': request.args.get('regions', type=int),
  176. }
  177. if camera_name in current_app.frigate_config.cameras:
  178. # return a multipart response
  179. return Response(imagestream(current_app.detected_frames_processor, camera_name, fps, height, draw_options),
  180. mimetype='multipart/x-mixed-replace; boundary=frame')
  181. else:
  182. return "Camera named {} not found".format(camera_name), 404
  183. @bp.route('/<camera_name>/latest.jpg')
  184. def latest_frame(camera_name):
  185. draw_options = {
  186. 'bounding_boxes': request.args.get('bbox', type=int),
  187. 'timestamp': request.args.get('timestamp', type=int),
  188. 'zones': request.args.get('zones', type=int),
  189. 'mask': request.args.get('mask', type=int),
  190. 'motion_boxes': request.args.get('motion', type=int),
  191. 'regions': request.args.get('regions', type=int),
  192. }
  193. if camera_name in current_app.frigate_config.cameras:
  194. # max out at specified FPS
  195. frame = current_app.detected_frames_processor.get_current_frame(camera_name, draw_options)
  196. if frame is None:
  197. frame = np.zeros((720,1280,3), np.uint8)
  198. height = int(request.args.get('h', str(frame.shape[0])))
  199. width = int(height*frame.shape[1]/frame.shape[0])
  200. frame = cv2.resize(frame, dsize=(width, height), interpolation=cv2.INTER_AREA)
  201. ret, jpg = cv2.imencode('.jpg', frame)
  202. response = make_response(jpg.tobytes())
  203. response.headers['Content-Type'] = 'image/jpg'
  204. return response
  205. else:
  206. return "Camera named {} not found".format(camera_name), 404
  207. def imagestream(detected_frames_processor, camera_name, fps, height, draw_options):
  208. while True:
  209. # max out at specified FPS
  210. time.sleep(1/fps)
  211. frame = detected_frames_processor.get_current_frame(camera_name, draw_options)
  212. if frame is None:
  213. frame = np.zeros((height,int(height*16/9),3), np.uint8)
  214. width = int(height*frame.shape[1]/frame.shape[0])
  215. frame = cv2.resize(frame, dsize=(width, height), interpolation=cv2.INTER_LINEAR)
  216. ret, jpg = cv2.imencode('.jpg', frame)
  217. yield (b'--frame\r\n'
  218. b'Content-Type: image/jpeg\r\n\r\n' + jpg.tobytes() + b'\r\n\r\n')