소스 검색

Ensure cv2.VideoCapture is only called once (#3619)

* ensure cv2.VideoCapture is only called once (see #2321)

* review

---------

Co-authored-by: Falko Schindler <falko@zauberzeug.com>
Rodja Trappe 8 달 전
부모
커밋
53cedb1add
1개의 변경된 파일57개의 추가작업 그리고 48개의 파일을 삭제
  1. 57 48
      examples/opencv_webcam/main.py

+ 57 - 48
examples/opencv_webcam/main.py

@@ -12,60 +12,69 @@ from nicegui import Client, app, core, run, ui
 # In case you don't have a webcam, this will provide a black placeholder image.
 black_1px = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAAXNSR0IArs4c6QAAAA1JREFUGFdjYGBg+A8AAQQBAHAgZQsAAAAASUVORK5CYII='
 placeholder = Response(content=base64.b64decode(black_1px.encode('ascii')), media_type='image/png')
-# OpenCV is used to access the webcam.
-video_capture = cv2.VideoCapture(0)
 
 
 def convert(frame: np.ndarray) -> bytes:
+    """Converts a frame from OpenCV to a JPEG image.
+
+    This is a free function (not in a class or inner-function),
+    to allow run.cpu_bound to pickle it and send it to a separate process.
+    """
     _, imencode_image = cv2.imencode('.jpg', frame)
     return imencode_image.tobytes()
 
 
-@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:
-    if not video_capture.isOpened():
-        return placeholder
-    # The `video_capture.read` call is a blocking function.
-    # So we run it in a separate thread (default executor) to avoid blocking the event loop.
-    _, frame = await run.io_bound(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 run.cpu_bound(convert, frame)
-    return Response(content=jpeg, media_type='image/jpeg')
-
-# For non-flickering image updates an interactive image is much better than `ui.image()`.
-video_image = ui.interactive_image().classes('w-full h-full')
-# A timer constantly updates the source of the image.
-# Because data from same paths are cached by the browser,
-# 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()}'))
-
-
-async def disconnect() -> None:
-    """Disconnect all clients from current running server."""
-    for client_id in Client.instances:
-        await core.sio.disconnect(client_id)
-
-
-def handle_sigint(signum, frame) -> None:
-    # `disconnect` is async, so it must be called from the event loop; we use `ui.timer` to do so.
-    ui.timer(0.1, disconnect, once=True)
-    # Delay the default handler to allow the disconnect to complete.
-    ui.timer(1, lambda: signal.default_int_handler(signum, frame), once=True)
-
-
-async def cleanup() -> None:
-    # This prevents ugly stack traces when auto-reloading on code change,
-    # because otherwise disconnected clients try to reconnect to the newly started server.
-    await disconnect()
-    # Release the webcam hardware so it can be used by other applications again.
-    video_capture.release()
-
-app.on_shutdown(cleanup)
-# We also need to disconnect clients when the app is stopped with Ctrl+C,
-# because otherwise they will keep requesting images which lead to unfinished subprocesses blocking the shutdown.
-signal.signal(signal.SIGINT, handle_sigint)
+def setup() -> None:
+    # OpenCV is used to access the webcam.
+    video_capture = cv2.VideoCapture(0)
+
+    @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:
+        if not video_capture.isOpened():
+            return placeholder
+        # The `video_capture.read` call is a blocking function.
+        # So we run it in a separate thread (default executor) to avoid blocking the event loop.
+        _, frame = await run.io_bound(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 run.cpu_bound(convert, frame)
+        return Response(content=jpeg, media_type='image/jpeg')
+
+    # For non-flickering image updates and automatic bandwidth adaptation an interactive image is much better than `ui.image()`.
+    video_image = ui.interactive_image().classes('w-full h-full')
+    # A timer constantly updates the source of the image.
+    # Because data from same paths is cached by the browser,
+    # 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()}'))
+
+    async def disconnect() -> None:
+        """Disconnect all clients from current running server."""
+        for client_id in Client.instances:
+            await core.sio.disconnect(client_id)
+
+    def handle_sigint(signum, frame) -> None:
+        # `disconnect` is async, so it must be called from the event loop; we use `ui.timer` to do so.
+        ui.timer(0.1, disconnect, once=True)
+        # Delay the default handler to allow the disconnect to complete.
+        ui.timer(1, lambda: signal.default_int_handler(signum, frame), once=True)
+
+    async def cleanup() -> None:
+        # This prevents ugly stack traces when auto-reloading on code change,
+        # because otherwise disconnected clients try to reconnect to the newly started server.
+        await disconnect()
+        # Release the webcam hardware so it can be used by other applications again.
+        video_capture.release()
+
+    app.on_shutdown(cleanup)
+    # We also need to disconnect clients when the app is stopped with Ctrl+C,
+    # because otherwise they will keep requesting images which lead to unfinished subprocesses blocking the shutdown.
+    signal.signal(signal.SIGINT, handle_sigint)
+
+
+# All the setup is only done when the server starts. This avoids the webcam being accessed
+# by the auto-reload main process (see https://github.com/zauberzeug/nicegui/discussions/2321).
+app.on_startup(setup)
 
 ui.run()