main.py 2.4 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556
  1. #!/usr/bin/env python3
  2. import asyncio
  3. import base64
  4. import concurrent.futures
  5. import time
  6. from typing import Optional
  7. import cv2
  8. import numpy as np
  9. from fastapi import Response
  10. from nicegui import app, ui
  11. # we need two executors to schedule IO and CPU intensive tasks with loop.run_in_executor()
  12. process_pool_executor = concurrent.futures.ProcessPoolExecutor()
  13. thread_pool_executor = concurrent.futures.ThreadPoolExecutor()
  14. # in case you don't have a webcam, this will provide a black placeholder image
  15. black_1px = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAAXNSR0IArs4c6QAAAA1JREFUGFdjYGBg+A8AAQQBAHAgZQsAAAAASUVORK5CYII='
  16. placeholder = Response(content=base64.b64decode(black_1px.encode('ascii')), media_type='image/png')
  17. # OpenCV is used to access the webcam
  18. video_capture = cv2.VideoCapture(0)
  19. def convert(frame: np.ndarray) -> Optional[bytes]:
  20. _, imencode_image = cv2.imencode('.jpg', frame)
  21. return imencode_image.tobytes()
  22. @app.get('/video/frame')
  23. # thanks to FastAPI's "app.get" it is easy to create a web route which always provides the latest image from OpenCV
  24. async def grab_video_frame() -> Response:
  25. loop = asyncio.get_running_loop()
  26. if not video_capture.isOpened():
  27. return placeholder
  28. # video_capture.read() is a blocking function, so we run it in a separate thread it to avoid blocking the event loop
  29. _, frame = await loop.run_in_executor(thread_pool_executor, video_capture.read)
  30. if frame is None:
  31. return placeholder
  32. # convert() is a cpu intensive function, so we run it in a separate process to avoid blocking the event loop and GIL
  33. jpeg = await loop.run_in_executor(process_pool_executor, convert, frame)
  34. if not jpeg:
  35. return placeholder
  36. return Response(content=jpeg, media_type='image/jpeg')
  37. # For non-flickering image updates an interactive image is much better than ui.image().
  38. video_image = ui.interactive_image().classes('w-full h-full')
  39. # A timer constantly updates the source of the image.
  40. # But because the path is always the same, we must force an update by adding the current timestamp to the source.
  41. ui.timer(interval=0.01, callback=lambda: video_image.set_source(f'/video/frame?{time.time()}'))
  42. # the process pool executor must be shutdown when the app is closed, otherwise the process will not exit
  43. app.on_shutdown(lambda: process_pool_executor.shutdown(wait=True, cancel_futures=True))
  44. ui.run()