Kaynağa Gözat

improved webcam example respect IO and CPU
fixes #490

Rodja Trappe 2 yıl önce
ebeveyn
işleme
4fd0791c46
1 değiştirilmiş dosya ile 26 ekleme ve 6 silme
  1. 26 6
      examples/opencv_webcam/main.py

+ 26 - 6
examples/opencv_webcam/main.py

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