Jupyter远程服务器使用本地摄像头、WebRTC实现聊天室、实时视频处理

Jupyter远程使用摄像头、WebRTC聊天室、实时视频处理

前言

使用ipywebrtc组件,获取本地视频流并传输到远程Jupyter服务器,由服务器处理过视频后再回传到本地,最后由ipywidgets.Image组件展示。

效果体验

建议使用Chrome浏览器

效果展示
前往官方示例并启用摄像头,即可体现实际效果。
如果你想更深刻地体验示范代码,可以去Binder打开任意一个.ipynb文件,一步步运行即可。


准备工作

需要先安装ipywebrtc组件,有两种方法,简单的办法是直接通过pip安装(需要jupyter版本在5.3及以上):

pip install ipywebrtc

第二种是通过github安装最新预览版:

git clone https://github.com/maartenbreddels/ipywebrtc
cd ipywebrtc
pip install -e .
jupyter nbextension install --py --symlink --sys-prefix ipywebrtc
jupyter nbextension enable --py --sys-prefix ipywebrtc

如果你使用的是jupyter lab,那么只需要在终端运行以下语句:

jupyter labextension install jupyter-webrtc

使用方法

完成准备工作后,首先需要在Jupyter文件中引用ipywebrtc库,然后创建一个流,可用的流请见下文组件介绍部分,这里以CameraStream为例,使用本地前置摄像头:

from ipywebrtc import CameraStream
camera = CameraStream.facing_user(audio=False)
camera

不出意外的话,Chrome浏览器会弹窗询问是否允许网页使用摄像头,选择允许后,就可在输出区看到摄像头拍到的视频了。

如果Chrome不弹出提示,而是显示Error creating view for media stream: Only secure origins are allowed,则表示浏览器认为当前网站不安全(没有使用https连接),因此禁用了摄像头。最简单的解决方法是在Chrome快捷方式的“目标”一栏最后加上--unsafely-treat-insecure-origin-as-secure="http://host_ip:port"并重启即可(把host_ip:port修改为自己的服务器地址)

但这时候,视频还只是在本地显示,并没有上传到Python内核所在的服务器中(假设你使用的是远程服务器),因此也就没办法在Python上下文中获取视频内容。
所以下一步,我们需要创建一个ImageRecorder来记录流媒体,并以图片的形式发送给服务器上的Python内核:

from ipywebrtc import ImageRecorder
image_recorder = ImageRecorder(stream=camera)
image_recorder

运行这段代码,会显示ImageRecorder组件,点击组件上的相机图标,即可抓拍来自stream的画面。
ImageRecorder
之后,再通过访问image_recorder.image.value并转换成Pillow格式,即可在Python内获取该图片:

import PIL.Image
import io
im = PIL.Image.open(io.BytesIO(image_recorder.image.value))

如果不需要处理,仅需要预览,则直接显示image_recorder.image即可,它是一个ipywidgets.Image组件,本身就有展示图片的功能。
如果需要使用opencv-python处理图片,可以通过canvas = numpy.array(im)[..., :3]得到cv2能处理的图像数组,处理之后可以在matplotlib中展示图片,但我更推荐使用ipywidgets.Image组件:

from ipywidgets import Image
out = Image()
out.value = cv2.imencode('.jpg', cv2.cvtColor(canvas, cv2.COLOR_BGR2RGB))[1].tobytes() # canvas出处见上文说明部分
out

或者如果你不希望再引入cv2,也可以按照官方给出的方式:

from ipywidgets import Image
out = Image()
im_out = PIL.Image.fromarray(canvas)
f = io.BytesIO()
im_out.save(f, format='png')
out.value = f.getvalue()
out

处理视频

目前我们仅介绍了怎么抓取一张图片,处理并展示,这些都可以很容易地通过官方文档学会。那么我们怎样才能连续不停地抓取一个视频里的各个帧,对其处理后再一一展示呢?下面将介绍我总结出来的ImageRecorder连续抓取的方法。

之所以连续抓取依然使用ImageRecorder而不是VideoRecorder,是因为VideoRecorder抓取视频必须有开始和结束,只有结束后才能对抓取的整个视频片段做处理,这达不到“实时”的要求。

我分析了作者的代码后发现,并没有类似于recorder.record()recorder.take_photo()之类的函数,而ImageRecorder前端抓取到下一帧图像后,会通知Python内核把ImageRecorderrecording属性设置为True,然后在抓取一张图片后,recording属性会自动变为False。因此我就想尝试一种循环机制,在每次处理完上一帧图片后,自动设置recordingTrue以抓取下一帧图片。
然而经过实验,使用while循环或for循环都是行不通的,这大概是因为Jupyter的前端是由Javascript渲染,单纯在后端Python环境下改变属性而不通知前端,是无法继续抓取的。

这个原因仅是我的个人猜测,因为时间原因没有去细看前端文件与底层逻辑,所以理解可能不正确,欢迎在评论区指正。

最后参考了Github上martinRenou的帖子,想到了下面这种模式,来完成连续的实时视频处理:

import io
import PIL.Image
import numpy as np
from ipywidgets import Image, VBox, HBox, Widget, Button
from IPython.display import display
from ipywebrtc import CameraStream, ImageRecorder

VIDEO_WIDTH = 640 # 窗口宽度,按需调整
VIDEO_HEIGHT = 480 # 窗口高度,按需调整

camera = CameraStream(constraints=
                      {
    
    'facing_mode': 'user',	
                       'audio': False,	
                       'video': {
    
     'width': VIDEO_WIDTH, 'height': VIDEO_HEIGHT}	
                       })	# 另一种CameraStream创建方式,参考下文组件介绍部分
image_recorder = ImageRecorder(stream=camera)
out = Image(width=VIDEO_WIDTH, height=VIDEO_HEIGHT)

FLAG_STOP = False	# 终止标记

def cap_image(_):	# 处理ImageRecord抓取到的图片的过程
    if FLAG_STOP:
        return	# 停止处理
    im_in = PIL.Image.open(io.BytesIO(image_recorder.image.value))
    im_array = np.array(im_in)[..., :3]
    canvas = process(im_array)	# process是处理图像数组的函数,这里没写出来,各位按处理需要自己写即可
    im_out = PIL.Image.fromarray(canvas)
    f = io.BytesIO()
    im_out.save(f, format='png')
    out.value = f.getvalue()
    image_recorder.recording = True	# 重新设置属性,使ImageRecorder继续抓取

# 注册抓取事件,参考我另一篇Blog:https://qxsoftware.blog.csdn.net/article/details/86708381
image_recorder.image.observe(cap_image, names=['value'])

# 用于停止抓取的按钮
btn_stop = Button(description="Stop",
                  tooltip='click this to stop webcam',
                  button_style='danger')
# btn_stop的处理函数
def close_cam(_):
    FLAG_STOP= True
    Widget.close_all()
btn_stop.on_click(close_cam) # 注册单击事件
# Run this section and Press the Camera button to display demo
display(VBox([HBox([camera, image_recorder, btn_stop]), out]))

在Jupyter中运行这段代码后,会出现一个本地camera预览框、一个ImageRecorder抓取框、一个红色的Stop按钮,以及一个尚无图片的Image组件。
点击ImageRecorder上的相机按钮,会激活cap_image函数,之后处理过的图片就会在Image组件里展示,接下来直到点击Stop,这个过程都会重复进行。
只有在点击相机之后,更后面的Jupyter Cells才能正常访问image_recorder.image,不然会报错OSError: cannot identify image file

这里最关键的部分,就是在ImageRecorder.imageobserver事件注册函数里添加的image_recorder.recording = True语句。至于为何在注册函数外添加这句话无效,就需要研究一下前后端的联系了。
需要注意的是,注册函数cap_image里出现的Error不会引发中断,因此如果运行后Image组件不显示图像,可能是cap_image发生了Error,这可以通过把cap_image里的内容提出来放到新的Cell中去验证。


组件介绍

ipywebrtc中,可用的流媒体有:

  • VideoStream:可以由VideoStream.from_file(path)获取本地视频,或由VideoStream.from_url(url)获取网络视频

  • CameraStream:通过本地摄像设备或者网络摄像头(webcam)获取媒体流,创建方法有两种:

    • 第一种:
       	camera = CameraStream(constraints=
       	                      {
          
          'facing_mode': 'user',	# 'user表示前置摄像头,'environment'表示后置摄像头
       	                       'audio': False,	# 是否同时获取音频(需要硬件支持)
       	                       'video': {
          
           'width': 640, 'height': 480 }	# 获取视频的宽高
       	                       })
    
    • 第二种:
      front_camera = CameraStream.facing_user(audio=False):前置摄像头
      back_camera = CameraStream.facing_environment(audio=False):后置摄像头
  • AudioStream:音频流,与VideoStream的创建方式相同

  • WidgetStream:通过WidgetStream(widget=target)指定widget,可以将任何ipywidget实例的输出创建为媒体流

这些媒体流都继承自MediaStream

除流媒体组件外,还有记录器组件,用于记录前端Javascript获取到的流并发送到Python内核,其中ImageRecorder已经在示例里介绍得很详细,而VideoRecorder用法与之类似,可参阅官方文档
最后还有ipywebrtc.webrtc中的组件,经过测试目前还有些BUG,可参考Chat视频聊天室


更多内容(如ipyvolumn系列)会在以后更新。
原创于CSDN,转载请注明出处:https://qxsoftware.blog.csdn.net/article/details/89513815。有任何问题或想法请在下方留下评论~~

猜你喜欢

转载自blog.csdn.net/liuqixuan1994/article/details/89513815