译者 | 陈峻
审校 | 孙淑娟
如今,市场上的智能监控摄像头林林总总,它们往往对我们来说是一种看家护院的黑匣子,我们无法知晓其内部的工作机制。如果我们想一探究竟,则需要利用物联网的相关知识,去自行搭建监控系统。下面,我将从客户端、仪表板UI、以及服务器端等方面,从硬件组装和软件部署入手,和您深入讨论如何构建一个物联网安全摄像头。
1.构建的目标
我们希望新创建的家用智能摄像头监控系统,能够实现如下四个方面:1. 通过运动检测模块,系统会对检测到的运动物体进行拍照。2. 可将图像保存到远程服务器上。3. 通过访问服务器的仪表板,我们可以查看所有事件,包括照片和时间戳。4. 以滑动窗口的形式,保存最近20个事件,并清理所有旧的事件。
2.需要哪些组件?
硬件
一个Raspberry Pi(树莓派) 4、一个运动检测传感器、一个摄像头模块,以及如下物料清单(BOM)中的各种小组件。
软件
- 一个用于部署、保存和显示摄像头图像的服务器端Render.com帐户。
- Git、Python 3和一个代码编辑器。
3.配置Raspberry Pi
第1步:为Raspberry Pi插入可靠的电源。最好使用BOM中指定的官方版本。毕竟有消息称,一些旧的Raspberry Pi 4型号存在着一些USB-C电缆和电源的适配问题。
第2步:安装Raspberry Pi OS(树莓派操作系统)。从官网上获取相关的指南和工具,包括如何使用SD卡等。
由于并不需要图形化界面,因此我们可以安装仅供专家使用的Raspberry Pi OS Lite版本。不过,如果您是首次使用Raspberry Pi进行开发的话,也可选用带有桌面的64 位版本的Raspberry Pi OS。
第3步:测试PIR运动传感器,以检测和捕捉房间中的运动物体。注意,传感器上有三根线,其中两根用于电源(+5V和接地),第三根用于从传感器读取数值,即:如果传感器检测到移动,就读取1,否则读取0。请使用pinout命令,查看Raspberry每个引脚的完整说明。
在本例中,我们使用一根黑线将传感器的地线,连接到电路板的地线(PIN 6)上,一根红线连接到+5V(PIN 2)上,并将信号线连接到其中一个GPIO(PIN 11)上。下面的两张图像展示了组装的效果,当然,如果您不知道哪根线缆应当对应哪里的话,请取下传感器上的盖子,并仔细检查PCB上的标签。
4.检测运动
为了检测运动,我们需要通过软件来读取PIR的数值,并发送通知。GitHub上的 Python版本提供了针对此类应用的简单版本,请参考如下代码段:
Python
from gpiozero import MotionSensor
from datetime import datetime
from signal import pause
pir = MotionSensor(17)
def capture():
timestamp = datetime.now().isoformat()
print('%s Detected movement' % timestamp)
def not_moving():
timestamp = datetime.now().isoformat()
print('%s All clear' % timestamp)
pir.when_motion = capture
pir.when_no_motion = not_moving
pause()
注意,由于我们的运动检测器已插入GPIO17(尽管在物理板上,它对应的是引脚11),因此我们将17的值传递给MotionSensor(),并通过运行python pir_motion_sensor.py,来启动之,以实现对PIR时间的调整。
为了避免过于频繁地被运动触发,内部计时器会阻止系统持续发送运动信号,因此传感器存在着虽然能够每次检测到运动,但可能不会去通知系统的风险。由于计时器的范围是0-255秒(255是全部顺时针方向,0为所有逆时针),因此根据我的经验,只需将定时器配置在7-10秒之间,电位器便可以在几乎水平的位置,逆时针地转动。类似地,对于灵敏度电位器而言,顺时针方向表示灵敏度更高。其对应的命令输出会显示如下:
Plain Text
pi@raspberrypi:~/raspberry-pi-security-camera-client $ python pir_motion_sensor.py
2022-04-21T15:35:35.275947 Detected movement
2022-04-21T15:35:41.607265 All clear
5.添加摄像头
在Raspberry Pi处于关闭状态,以及断开了与任何电源的连接时,我们将摄像头安装在右侧。而在完成后,请重启Raspberry Pi,并确保已拥有最新的摄像头栈(camera stack)。
然后,请打开控制台并输入如下内容:
Shell
$ sudo raspi-config
请选择“接口选项”菜单。
选择“启用/禁用传统摄像头支持”并确保将其已禁用。
最后,保存并重新启动。
6.Picamera2与Picamera
Picamera2是libcamera的新式Python端口。其对应的旧项目--Picamera虽然基于不同的系统,但是其接受度颇高。
7.测试摄像头
为了测试摄像头,我使用Picamera2创建了一个简短的脚本。鉴于Picamera2项目仍处于预览阶段,其安装并不容易。下面,我们先运行example_picamera2.py脚本,来验证摄像头是否已设置正确:
Shell
$ python example_picamera2.py
而example_picamera2.py的具体内容如下:
Python
from gpiozeroimport MotionSensor
frompicamera2.picamera2 import *
fromdatetime import datetime
fromsignal import pause
pir = MotionSensor(17)
camera = Picamera2()
camera.start_preview(Preview.NULL)
config = camera.still_configuration()
camera.configure(config)
defcapture():
camera.start()
timestamp = datetime.now().isoformat()
print('%s Detected movement' % timestamp)
metadata = camera.capture_file('/home/pi/%s.jpg' % timestamp)
print(metadata)
camera.stop()
defnot_moving():
timestamp = datetime.now().isoformat()
print('%s All clear' % timestamp)
pir.when_motion = capture
pir.when_no_motion = not_moving
pause()
每当移动检测PIR传感器检测到物体移动时,该文件都会执行快照,并将图像放置在/home/pi目录中,并保持文件名与摄像头捕获图像的时间相一致。下图便是我的摄像头所拍摄到的图像:
至此,我们只完成了项目的一半,毕竟这些都是在本地实现的,并未通过物联网进行远程监控,更谈不上防止有人访问我们的Raspberry Pi、移除SD卡、并带走监控记录。
8.编写客户端代码并在本地进行测试
下面,我们准备在客户端上实现以下软件逻辑:
(1)使用Picamera2设置摄像头。
(2)初始化运动传感器。
(3) 当检测到物体运动时,读取事件并调用以下函数:
a. 捕获图像并将其保存到本地文件系统上的一个文件中。
b. 将图像上传到远程服务器上。
c. 如果上传正确,则删除本地文件,以避免填满Raspberry Pi上的所有空间。
(4) 当由于超时(在本例子中为6-7秒)而不再检测到运动时,开始读取事件,并打上带有“All clear”消息的时间戳。5. 等待下一个事件。
下面是对应的高级别(high-level)代码:
Python
def init(settings):
camera = setup_camera()
pir = MotionSensor(settings.get('PIR_GPIO'))
pir.when_motion = picture_when_motion(pir, camera, settings)
pir.when_no_motion = not_moving
pause()
其中,最复杂的函数是picture_when_motion。当设备从非运动状态变为运动状态时,when_motion便会开始执行。我们可以设置为不接受其他参数,或仅接受单个强制参数。我将通过下面的代码,将其转换为一个函数,并创建一个回调(callback)来返回它。
Python
defpicture_when_motion(pir, camera, settings):
setup_path(settings.get('IMG_PATH'))
def capture_and_upload_picture():
if camera:
file_path = capture(camera, settings.get('IMG_PATH'))
server_settings = settings.get('SERVER')
uploaded = upload_picture(file_path, server_settings)
if uploaded:
cleanup(file_path)
else:
print("Camera not defined")
return capture_and_upload_picture
上述代码中的捕获函数类似于前面用于测试摄像头的函数,而upload_picture函数是将软件从本地转换为物联网应用的核心。下面让我们来对其进行分析:Python
def upload_picture(file_path, server_settings):
if server_settings.get('base_url'):
url = urljoin(server_settings.get('base_url'), 'upload')
if server_settings.get('user') and server_settings.get('password'):
user = server_settings.get('user')
password = server_settings.get('password')
files = {'file': open(file_path, 'rb')}
print('Uploading file %s to URL: %s' %(file_path, url))
try:
r = requests.post(url, files=files, auth=HTTPBasicAuth(user, password))
image_path = r.json().get('path')
except e:
print(e)
if not image_path or not r.ok:
print('Error uploading image')
return False
print('Image available at: {}'.format(image_path))
return True
理想情况下,我们让服务器使用用户名和密码的验证方式,接受作为POST请求的负载文件。其对应的命令为:
Shell
curl \
-F "file=@/home/user/Desktop/test.jpg" \
http://localhost:5000/upload
由于它们是使用开源的MIT许可证发布的,因此您既可以随意复制它们,也可以使用python main.py来执行之。
9.创建一个服务器来存储图像
针对存储图像的服务器,我们希望:
- 支持Python代码,特别是Flask。
- 无需RDBMS或复杂的数据库,仅靠文件系统来存储图像。
- 提供如下简单API的REST接口:
/upload 上传图像。
/ 获取所有图像的列表。
/cleanup 删除旧的图像。
/download/<name>下载单个图像。
- 安全的TLS连接。
- 在完成身份验证的基础上,允许Raspberry Pi上传文件。
- 自动部署。
- 支持安全的环境变量,可用于存储用户凭据。
- 低成本(不超过几美元/月)。
GitHub上提供了在本地、或在服务器上运行代码的相关说明。
10.Flask应用
Flask是一个简单灵活的Python框架,可用于快速创建以REST API为主的Web应用。同时,我们可以将主要代码放在main.py文件。首先,我们需要初始化Flask应用,并声明身份验证的方法。对此,我会声明一个名为setup的函数,以读取本地机器上的各种可用环境变量。同时,我也会创建一个包含了所有环境变量的.env文件。接着,我声明了一个verify_password函数,来验证提供给服务器的密码是否正确。然后,我通过函数upload_file,来支持上传新的文件,并访问/upload端点,将图像存储在文件系统中,其具体内容如下:
Python
defupload_file():
if request.method == 'POST':
# check if the post request has the file part
if 'file' not in request.files:
flash('No file part')
return redirect(request.url)
file = request.files['file']
# If the user does not select a file, the browser submits an
# empty file without a filename.
if file.filename == '':
flash('No selected file')
return redirect(request.url)
if file and allowed_file(file.filename):
filename = secure_filename(file.filename)
file.save(os.path.join(app.config['UPLOAD_FOLDER'], filename))
return jsonify(success=True, filename=filename, path=urljoin(request.host_url, url_for('download_file', name=filename)))
return '''
<!doctype html>
<title>Upload new File</title>
<h2>Upload new File</h2>
<form method=post enctype=multipart/form-data>
<input type=file name=file>
<input type=submit value=Upload>
</form>
'''
该函数在GET和POST模式下均可有效。其中,运行在POST中时,我们可以从文本客户端、或其他应用程序处上传文件;而在GET模式下,我们则可以使用浏览器来实现。
11.本地测试
虽然您可以直接在Raspberry Pi 4中测试服务器,但是如果您有Linux或Mac系统,那么配置和启动它会更加容易。在本例中,我们首先需要创建一个.env文件,并将其放在与应用程序相同的目录中。.env文件将会存储服务器如下所需的信息:
- 用于管理Flask会话的密钥
- 保存图像的上传文件夹
- 可接受图像的最大尺寸
- 用于身份验证的用户名和密码
- 服务器本身的端点URL
下面展示的是.env-example-local文件的内容。您可以将其用作模板,复制、重命名、并按需予以修改。
属性文件
SECRET_KEY='change-this-to-something-unlikely-to-guess'
UPLOAD_FOLDER = './img'
MAX_CONTENT_LENGTH = 16000000
USERNAME = 'admin'
PASSWORD = 'change-this-to-your-unique-password'
SERVER='http://127.0.0.0:5001/'
通过运行python main.py,服务器将被启动,并进入主动调试模式,以便我们观察到后台发生的情况。
Shell
$ python main.py
* Serving Flask app 'main' (lazy loading)
* Environment: production
WARNING: This is a development server. Do not use it in a production deployment.
Use a production WSGI server instead.
* Debug mode: on
* Running on all addresses (0.0.0.0)
WARNING: This is a development server. Do not use it in a production deployment.
* Running on http://127.0.0.1:5001
* Running on http://192.168.123.228:5001 (Press CTRL+C to quit)
* Restarting with stat
* Debugger is active!
* Debugger PIN: 332-932-829
让我们首先通过CURL的方式来测试上传文件。您也可以使用Postman之类的工具来进行测试。假设您想从路径/Users/luca/Pictures/image.jpeg处上传图像,请使用如下命令:
Shell
curl \
-F "file=@/Users/luca/Pictures/image.jpeg" \
-u 'admin:password' \
'http://127.0.0.1:5001/upload'
{
"filename": "image.jpeg",
"path": "http://127.0.0.1:5001/download/image.jpeg",
"success": true
}
下图展示了上传图像的请求已被成功受理。
12.将服务器部署到Render处
至此,我们可以将服务器推送到一个真实、稳定且安全的环境中了。我们希望:
- 能够在磁盘上直接存储图像,而无需配置数据库。
- 为了避免在服务器上保留过多的图像,通过Cronjob运行某个API,以定期只保留最后20张图像。
- 简单地使用基于.env文件的密钥和变量
13.注册并配置首个服务
在注册到平台之前,我建议您通过单击屏幕右上角的“分叉(fork)”按钮,来分叉现有的GitHub存储库。
接着,您可以在Github上完成注册。
然后,请从仪表板中选择“新建Web服务”。
并搜索最近分叉的存储库(repo)。
为了配置服务器,您可以先选择一个免费的入门计划,并在后期按需选购永久性磁盘。其中会涉及到如下参数:
- 名称:可自由选择
- 环境:Python 3
- 地区:选择离您最近的一个
- 分支:main
- 构建命令 pip install -r requirements.txt
- 启动命令gunicorn main:app
现在让我们转到界面的高级部分,以设置密钥文件。您可以将其命名为.env,并粘贴以下的文本内容(您可以按需进行更改):
14.创建永久性磁盘
在Render上创建永久性磁盘并不难,我们完全可以使用界面来完成。您只需单击左侧的磁盘部分,为其选择名称和安装路径即可。例如:
- 名称:图像
- 挂载路径:/var/img
- 大小:1GB
我们将会在“事件”选项卡中收到有关其状态的通知。
如果我们点击一个特定的事件,将能够看到所有的细节。
完成后,您将在页面的顶部看到服务器的URL。
现在,是时候开始从我们的Raspberry Pi客户端处上传一些真实的图像了。首先,我们需要更改Raspberry客户端中的.env文件。下面展示了其环境变量的信息:
属性文件
PIR_GPIO=17
USERNAME='admin'
PASSWORD='change-me-with-a-real-password-please'
API_SERVER='https://your-api-address.onrender.com/'
IMG_PATH='img'
接着,请使用Python 3启动main.py的服务。
如果您在PIR传感器的区域内移动,摄像头将会拍摄照片并将其上传到服务器上。我们可以通过获取图像的URL,实现浏览器下载照片。
15.定期清理图像
为了避免在服务器上存储太多的图像,我人为地设定为最多保留20张。为此,我们需要创建一个额外的Cronjob服务,来定期调用API。
首先,我创建了一个名为/cleanup的服务器路由,它会调用keep_last_images()函数。该函数的定义如下:
Shell
$ curl-v -d '{"keep": "20"}' -H "Content-Type: application/json" -u 'username:password' -X POST http://127.0.0.1:5001/cleanup/
此函数会按照创建图像的时间对图像进行排序,并保留POST请求有效负载中所指示的X数量的图像。请使用如下命令测试CURL的执行效果:
Shell
$ curl-v -d '{"keep": "20"}' -H "Content-Type: application/json" -u 'username:password' -X POST http://127.0.0.1:5001/cleanup/
通过定期(如每周)调用上述函数,我们将能够清理所有比最近20张更旧的图像。
接着,我在Render的仪表板中创建了一个新的Cronjob服务。
下面是针对Cronjob的设置:
名称:清理旧文件
- 地区:法兰克福
- 时间表:4 5 * * 2(我使用https://crontab.guru/来创建正确的字符串。)
- 命令:Python 3 auto_cleanup.py 20(最后一个参数是设定保留图像的数量)
- 构建命令:pip install -r requirements.txt(这是安装所有依赖项所必需的)
- 分支:main
- 自动部署:是
- Cronjob失败通知:用户帐户的相关通知设置
为了测试其效果,我们可以在Cronjob上手动触发其运行,而无需等待真实的时间表,即:单击页面顶部的“触发运行”按钮即可。Render界面的仪表板会显示如下信息:
16.创建事件的仪表板
为了实现对摄像头的安全管理,我们可以使用list_files()函数查询文件系统,并按照创建日期列出所有的图像文件。请参考如下代码段:
Python
# List endpoint, get an HTML page listing all the uploaded files link
@app.route('/')
@auth.login_required
def list_files():
files = get_list_of_img_path(path=app.config['UPLOAD_FOLDER'], reverse=True)
images_url = []
for file in files:
images_url.append(urljoin(request.host_url, url_for('download_file', name=os.path.basename(file))))
return render_template('imglist.html', images_url=images_url)
上述函数会调用与操作系统相关的API,并返回按创建时间排序的文件列表。接着,它会使用jinja模板,将数据返回到imglist.html文件中。该文件的基本部分为:
HTML
<ul>
{% for image in images_url %}
<li><a href="{{image}}">{{image}}</a></li>
{% else %}
<li>No images uploaded yet</li>
{% endfor %}
</ul>
它会产生如下列表:
17.在外出时查看自己的仪表板
物联网的好处不仅在于您可以安全地远程存储图像,而且能够避免因有人窃取或损坏您的Raspberry Pi,而丢失数据。也就是说,您可以身处世界任何地方,通过使用服务器.env文件中记录的用户名和密码,访问并登录Render的完整URL,以查看照片数据,并及时捕获设备前的运动事物。下面的一组照片来自我家的摄像头。其中的最后一张记录了我爱人对摄像头进行测试的场景。
18.小结
在上文中,我向您介绍了如何以端到端的方式,从硬件设置到服务器部署,来创建一个廉价且实用的物联网摄像头应用。您可以使用Raspberry Pi 4、Python、Flask、Render等技术组件与服务,在短短几个小时内构建出具有远程图像上传功能的安全摄像头。
原文链接:https://dzone.com/articles/iot-security-camera-with-rasbperry-and-render
译者介绍
陈峻 (Julian Chen),51CTO社区编辑,具有十多年的IT项目实施经验,善于对内外部资源与风险实施管控,专注传播网络与信息安全知识与经验;持续以博文、专题和译文等形式,分享前沿技术与新知;经常以线上、线下等方式,开展信息安全类培训与授课。