从零打造树莓派家庭监控 (二): 监控模块与前后端

January 20, 2020
iOS开发家庭监控项目树莓派Python

上一篇文章 从零打造树莓派家庭监控 (一): 伺服电机控制 中讲述了如何去控制伺服电机,这篇文章将会讲解监控模块的实现以及前端后端的实现。

监控模块

监控方面主要是用到了一个摄像头,树莓派应该是任何 USB Webcam 都兼容的,我使用了一个2015年购入的微软摄像头,现在估计已经买不到了。不过网上有很多其他牌子的

说到录像,Python 的 OpenCV 库对调用摄像头录像有着良好的支持所以这里直接使用 OpenCV 来获取摄像头的影响。
监控模块需要实现两个功能:

  • 一个是在后端为 App 提供实时串流的功能
  • 一个是自动在后台录制并且保存成视频文件

下面是监控模块的实现代码,也才100行:
整个 cctv.py 有两个类,其中一个 CCTV 类就是监控模块,RecordThread 类是后台自动录制视频的线程类。

  • CCTV类中使用了 openCV 的 VideoCapture 类进行视频捕捉
  • get_frame 函数是用来捕捉一帧画面,压缩成jpg并且以比特数组返回,用于串流
  • start_record 函数用来启动后台录制,函数会判断当前的时间并且以小时来保存视频,所以保存下来的视频文件是按照小时来保存的。每过一个小时函数就会产生一个新的 RecordThread 线程来录制视频,并且销毁上一个 RecordThread。
  • RecordThread 中的录制视频方法和 get_frame 类似,只是使用了一个 openCV 提供的 VideoWriter 类来进行文件写入。
  1. # Home_CCTV_Server
  2. # CCTV
  3. # Created by Yigang Zhou on 2020-01-09.
  4. # Copyright © 2020 Yigang Zhou. All rights reserved.
  5. import cv2
  6. import time
  7. import threading
  8. import datetime
  9. from pathlib import Path
  10. def draw_time_label(frame):
  11. """
  12. 为frame加入时间label
  13. :param frame:
  14. :return:
  15. """
  16. text = time.ctime()
  17. font_face = cv2.FONT_HERSHEY_SIMPLEX
  18. scale = 0.5
  19. color = (255, 0, 0)
  20. thickness = 2
  21. f = cv2.putText(frame, text, (10, 30), font_face, scale, color, thickness, cv2.LINE_AA)
  22. return f
  23. class CCTV():
  24. def __init__(self,save_path='/'):
  25. threading.Thread.__init__(self)
  26. self.cap = cv2.VideoCapture(0)
  27. self.frame_width = int(self.cap.get(3))
  28. self.frame_height = int(self.cap.get(4))
  29. self.current_hour = -1
  30. self.record_thread = None
  31. self.save_path = save_path
  32. print("CCTV初始化...")
  33. print("摄像头分辨率", self.frame_width, "x", self.frame_height)
  34. print("储存路径", self.save_path)
  35. def __del__(self):
  36. self.cap.release()
  37. def start_record(self):
  38. now = datetime.datetime.now()
  39. date = str(now.year)+'-'+str(now.month)+'-'+str(now.day)
  40. hour = now.hour
  41. if hour != self.current_hour:
  42. path = self.save_path + date + '/'
  43. file_path = path + str(hour) + '.mp4'
  44. Path(path).mkdir(parents=True, exist_ok=True)
  45. if self.record_thread != None:
  46. self.record_thread.stop()
  47. self.record_thread = RecordThread(self.cap, file_path)
  48. self.record_thread.start()
  49. self.current_hour = hour
  50. threading.Timer(1, self.start_record).start()
  51. def stop_record(self):
  52. self.record_thread.stop()
  53. def get_frame(self):
  54. """
  55. 得到帧,用于Web渲染
  56. :return:
  57. """
  58. success, image = self.cap.read()
  59. #self.cap.set(3, 320)
  60. #self.cap.set(4, 240)
  61. if success:
  62. image = draw_time_label(image)
  63. # We are using Motion JPEG, but OpenCV defaults to capture raw images,
  64. # so we must encode it into JPEG in order to correctly display the
  65. # video stream.
  66. ret, jpeg = cv2.imencode('.jpg', image)
  67. return jpeg.tobytes()
  68. else:
  69. return bytearray()
  70. class RecordThread(threading.Thread):
  71. def __init__(self, cap, file_path):
  72. threading.Thread.__init__(self)
  73. self.file_path = file_path
  74. self.cap = cap
  75. self.frame_width = int(self.cap.get(3))
  76. self.frame_height = int(self.cap.get(4))
  77. self.stopped = True
  78. out = None
  79. def run(self):
  80. print("CCTV开始录制", self.file_path)
  81. self.stopped = False
  82. fourcc = cv2.VideoWriter_fourcc(*'mp4v')
  83. frame_rate = 30
  84. out = cv2.VideoWriter(self.file_path, fourcc, frame_rate, (self.frame_width, self.frame_height))
  85. while True:
  86. ret, frame = self.cap.read()
  87. frame = draw_time_label(frame)
  88. if ret:
  89. out.write(frame)
  90. if self.stopped:
  91. # out.release()
  92. print("录制停止\n")
  93. break
  94. def stop(self):
  95. self.stopped = True
  96. self.join()

有了视频录制模块之后就要去写一个后端了,要么每次只能去服务器上查看录制的文件,不能实时的查看摄像头内容,并且旋转舵机也不方便。

后端开发

后端开发没有什么难的,我使用了Python 非常轻量级的 Flask 框架,第一次使用,很容易就能学会。

  1. 规定了一个 token 变量用于安全,所有的API请求必须包含这个token过来才会调用
  2. 初始化一个 刚才编写的 CCTV 的实例,调用 start_record 函数来启动后台录制
  3. 初始化两个之前文章中提到的两个 MyServo 实例用于电机控制
  4. 实现了一个 /reposition API接口供 App 调用,传入三个个参数,token,id和angle,其中id用于区分两个电机,angle为旋转的角度。
  5. video_feed 路由用于提供实时串流,可以直接用浏览器访问。
  6. 启动服务器
  1. from flask import Flask,request,Response,render_template
  2. from cctv import CCTV
  3. from servo import MyServo
  4. app = Flask(__name__)
  5. #1
  6. token = "zyg19960622"
  7. #2
  8. cctv = CCTV(save_path='/Users/mike/Desktop/cctv/')
  9. # cctv.start_record()
  10. #3
  11. s0 = MyServo(18)
  12. s1 = MyServo(23,default_postion=90)
  13. #4
  14. @app.route('/reposition')
  15. def servo_reposition():
  16. if request.args['token'] == token:
  17. servo_id = int(request.args['id'])
  18. angle = int(request.args['angle'])
  19. if servo_id == 0:
  20. print(0,'to',angle)
  21. s0.reposition(angle)
  22. elif servo_id == 1:
  23. s1.reposition(angle)
  24. print(1,"to",angle)
  25. return "OK"
  26. else:
  27. return "Wrong Token"
  28. #5
  29. @app.route('/video_feed')
  30. def video_feed():
  31. if request.args['token'] == token:
  32. return Response(gen(cctv), mimetype='multipart/x-mixed-replace; boundary=frame')
  33. else:
  34. return "Wrong Token"
  35. def gen(cctv):
  36. while True:
  37. frame = cctv.get_frame()
  38. yield (b'--frame\r\n'
  39. b'Content-Type: image/jpeg\r\n\r\n' + frame + b'\r\n\r\n')
  40. #6
  41. if __name__ == '__main__':
  42. app.run(host='0.0.0.0')

写完之后运行后端,Flask默认会跑在 5000 端口上,然后在浏览器上实验一下 video_feed 接口能不能使用,打开之后顺利的话就能看见摄像头的串流了。

之后在Postman上测试了一下reposition接口也一切正常

后端的代码也开源在我的GitHub: GitHub - Yigang0622/Home_CCTV_Server: Home CCTV Server using Raspberry Pi

然后就完事具备只欠 iOS 客户端了

iOS 客户端

iOS 端的基本功能就是调用 reposition 接口调整舵机方向和显示串流了,因为后端的接口已经做好了,所以如果有一定的 iOS 开发基础的话也没有什么难度。
第一个界面提供了一个对话框来输入服务器地址和token,之后的API请求需要这两个信息。
主界面的串流使用了一个 WebView 控件,两个舵机的控制使用了两个 SegmentControl 控件,仅此而已,因为iOS写的比较熟练的缘故用了20分钟就实现了。

具体的代码不在这里放了,有兴趣的同学可以直接去看我的GitHub: GitHub - Yigang0622/Home_CCTV_iOS: 家庭监控iOS端

Comments

作者
July 21, 2018 at 10:52 am

There are no comments

keyboard_arrow_up