OpenCV and Motion JPEG

要在 Windows 作業系統上將 OpenCV 的影像以 Motion JPEG 串流形式送到瀏覽器顯示時,無法使用 CGI 方式,因為在 Windows 中所有資料,包含 HTTP 表頭資料都必須在 CGI 程式結束後才會傳到瀏覽器,所以只要 CGI 程式沒結束,瀏覽器就收不到任何訊息。這行為跟 UNIX 系統不同,UNIX 上的 CGI 程式只要有資料送出,不必等 CGI 程式結束瀏覽器就可以收到。所以在 Windows 上想要將 OpenCV 影像送到瀏覽器上,需自己動手 Web Server 端相關程式碼。這裡的 Web Server 使用的是 python3 內建的,程式碼參考 https://picamera.readthedocs.io/en/release-1.13/recipes2.html#web-streaming。這段程式碼原本是在樹莓派上使用 PyCamera 函數庫抓樹莓派的軟排線攝影機影像,我稍微修改了ㄧ點點,改成從 OpenCV 抓影像。

# source code from 
# https://picamera.readthedocs.io/en/release-1.13/recipes2.html#web-streaming

import io
#import picamera
import logging
import socketserver
from threading import Condition
from http import server
import cv2
import threading

PAGE="""\
<html>
<head>
<title>picamera MJPEG streaming demo</title>
</head>
<body>
<h1>PiCamera MJPEG Streaming Demo</h1>
<img src="stream.mjpg" width="640" height="480" />
</body>
</html>
"""

class StreamingOutput(object):
    def __init__(self):
        self.frame = None
        self.buffer = io.BytesIO()
        self.condition = Condition()

    def write(self, buf):
        if buf.startswith(b'\xff\xd8'):
            # New frame, copy the existing buffer's content and notify all
            # clients it's available
            self.buffer.truncate()
            with self.condition:
                self.frame = self.buffer.getvalue()
                self.condition.notify_all()
            self.buffer.seek(0)
        return self.buffer.write(buf)

class StreamingHandler(server.BaseHTTPRequestHandler):
    def do_GET(self):
        if self.path == '/':
            self.send_response(301)
            self.send_header('Location', '/index.html')
            self.end_headers()
        elif self.path == '/index.html':
            content = PAGE.encode('utf-8')
            self.send_response(200)
            self.send_header('Content-Type', 'text/html')
            self.send_header('Content-Length', len(content))
            self.end_headers()
            self.wfile.write(content)
        elif self.path == '/stream.mjpg':
            self.send_response(200)
            self.send_header('Age', 0)
            self.send_header('Cache-Control', 'no-cache, private')
            self.send_header('Pragma', 'no-cache')
            self.send_header('Content-Type', 'multipart/x-mixed-replace; boundary=FRAME')
            self.end_headers()
            try:
                while True:
                    with output.condition:
                        output.condition.wait()
                        frame = output.frame
                    self.wfile.write(b'--FRAME\r\n')
                    self.send_header('Content-Type', 'image/jpeg')
                    self.send_header('Content-Length', len(frame))
                    self.end_headers()
                    self.wfile.write(frame)
                    self.wfile.write(b'\r\n')
            except Exception as e:
                logging.warning(
                    'Removed streaming client %s: %s',
                    self.client_address, str(e))
        else:
            self.send_error(404)
            self.end_headers()

class StreamingServer(socketserver.ThreadingMixIn, server.HTTPServer):
    allow_reuse_address = True
    daemon_threads = True

output = StreamingOutput()    
address = ('', 8000)
server = StreamingServer(address, StreamingHandler)
def server_run():
    server.serve_forever()
threading.Thread(target=server_run).start()

cap = cv2.VideoCapture(0)
cap.set(3, 640)
cap.set(4, 480)
while True:
    ret, frame = cap.read()
    
    ### modify frame here ###

    data = cv2.imencode('.jpg', frame)[1].tobytes()
    output.write(data)

請自行在倒數第三行的位置修改 frame 內容,例如加上人臉辨識或是其他想要的影像處理效果。這段 python 程式執行後在瀏覽器輸入網址 http://localhost:8000 就可以看到串流影像了。

順道一提,欲使用 CGI 進行串流的程式碼,請參考 https://github.com/kirkchu/excv/tree/main/mjpg/cgi-bin 中的 video.py 與 mjpglib.py 這兩個檔案。

發表迴響