利用树莓派进行延时摄影

Dec 1,2018   7895 words   29 min

Tags: RaspberryPi

1.目的与效果介绍

树莓派作为一个功能丰富的嵌入式开发平台可以结合各种硬件、传感器做出很多有意思的应用。这篇博客介绍的就是利用树莓派来实现延时摄影。首先先放实现的效果。

这个延时摄影拍摄的就是我机房窗外的景色。利用树莓派连续拍摄了24小时,每隔90秒拍一张,共拍了980张,视频帧率是11。由于拍摄当天的天气有雾,是阴天多云,只有中午有点太阳,所以太阳的光影变幻不是很明显,但也还行。

[2019-8-13更新]

利用上述代码在家里进行了为期两天(8月5日-8月7日)的延时摄影。每隔10秒拍摄一次,共拍摄了16225张照片,约32.4GB的数据量。观测了视频传到了油管,点击查看

[更新结束]

2.硬件准备

(1)树莓派一块

这个无需多说了,树莓派主板是必须的。我用的是最新的Model 3B+版本,四核A53,主频1.4Ghz。关于树莓派的电源问题,由于3B+版本硬件配置有所提升,所以耗电也有所增加。 最好的电源是5V~3A的电源,如果实在没有的话5V~2.5A也能运行,但是树莓派会提示电压不足。我用的是小米的手机快充头(5V~3A),也测试过小米移动电源(5V~2.5A),也是可以的。

(2)摄像头一枚

既然要用树莓派拍照,摄像头是必须的。摄像头类型不限,总之只要树莓派能识别的就可以了。可以使用树莓派官方的CSI接口的摄像头,也可以使用USB摄像头。 不过建议摄像头不要太渣,不然拍出来效果不是很好。这里我用的是树莓派官方摄像头Module V2,800万像素,官网说明文档点击查看,里面介绍了一些相机的参数等信息。 如果是CSI接口摄像头的话安装的时候稍微注意一下,把卡槽的两边往上提起后再将接线塞进去,然后再将卡槽按下去就行了。动作轻一点,防止损坏接口。

3.软件准备

(1)摄像头设置

如果你用的是USB摄像头,那么你可以跳过这一部分,可以直接使用。而如果你用的是CSI接口的摄像头,需要在树莓派中简单设置一下,开启摄像头。具体做法如下。 在树莓派中打开终端,然后输入sudo raspi-config,进入树莓派设置界面,如下。 然后在项目中找到”Interfacing Options”,然后在里面找到”Camera”条目,如下图。 选择”Enable”,重启即可开启相机。树莓派系统也提供了一些命令可供使用,如raspistill(拍照)、raspivid(拍视频)、raspiyuv(拍照返回原始数据)命令,每个命令都有很多参数可用,可以查看树莓派官方文档,点击查看

另一个可能出现的问题是相机硬件、连接都没有问题,打开相机出现select timeout错误,那么很有可能是相机供电不足导致的。我出现这个情况是在树莓派连接了一个HDMI显示器(并对其供电)、一个键盘、一个摄像头的情况下出现的。因此只需要把用不到的硬件拔掉就好了。

(2)环境配置

在软件方面由于是自己编写代码,所以并不需要额外准备什么,用系统自带的Python环境即可。 如果你使用CSI摄像头,你可以有两种拍照方式,一种是基于树莓派自带拍照命令raspistill命令的,一种是基于OpenCV的。 如果选用第一种方式,你不需要配置什么。如果你想用OpenCV调用CSI接口的树莓派摄像头,除了上面说的在系统中开启摄像头之外,还需要再做些设置,因为CSI接口的树莓派摄像头不是标准的驱动,所以OpenCV识别不了。 在控制台中输入sudo gedit /etc/modules-load.d/rpi-camera.conf,在打开的文件末尾添加bcm2835-v4l2,这个意思是系统在启动时加载bcm2835-v4l2,重启系统之后,就可以使用OpenCV的CaptureVideo(0)函数获取影像了。

此外,还需要给Python安装OpenCV库,建议源码安装。可以在OpenCV官网下载源码,然后一路cmake即可,切换到解压好的OpenCV文件夹后,输入如下命令(这些命令最好是以root权限运行,否则在make install的时候提示权限不够):

mkdir build
cd build
cmake ..
make -j4
make install

这样安装以后,C++和Python的OpenCV库就自动都装好了。 OpenCV虽然依赖的库比较多,但其实都比较好安装。一般情况下只要按着步骤一步步先把依赖库配好了,编译安装OpenCV基本不会有什么问题。 如果遇到问题可以参考这篇博客,写的挺详细的。 另一个可能出现的问题在之前的博客中也提到过,就是cmake的时候可能会下载一些文件,而由于是外网,所以经常下载失败。针对这种情况一定要根据出错提示进行解决,可以查看CMakeDownload的Log文件。

这里再做个小科普,关于如何卸载make install安装的库,方法很简单,还是在生成的文件夹内输入make uninstall即可。所以一般不建议安装完成后就把源码给删掉。 假如你已经把源码删掉了,那就只能再把源码下一遍,然后make install看看它都拷贝了哪些文件到什么地方,手动把那些文件删除。

还需要提示的是关于make的线程数问题,由于树莓派硬件资源实在有限,内存只有1G,所以在编译时需要关注一下内存使用情况。如果出现了编译到某个地方突然卡住了,看CPU使用率也不高,那十有八九就是内存用完了。 这时就不要用多线程编了,老老实实make就行了。因为多线程编就意味着会同时生成多份内容,所占内存与线程数成正比。

某些库的编译特别耗内存,之前在单线程编译G2O库的时候,875MB的内存,最高占用到了845MB。这还只是单线程编译,开多线程就直接卡死了。因为在电脑上make -j4写习惯了,所以到树莓派上还是这样写,结果一开始编译总是很慢,卡在某个地方就不动了,CPU使用率也很低。 当时一度怀疑是树莓派性能太差导致的。直到后来偶然打开了任务管理器,才发现原来是内存不足导致的。虽然可以通过增加虚拟内存的办法扩展内存(扩展内存方法可参考这里),但还是没有那么有效果。 关于如何降低编译时消耗内存较大问题,除了增加swap内存,另一个解决策略是,不编译示例代码,在CMake设置中取消”Build examples”,只编译核心代码,这样可以减少编译的工作量和消耗内存。 所以在用树莓派的时候,不能想当然的觉得它也是Linux系统,就可以一切都按照电脑的来,还是要多学,不能想当然,不然容易出错。

如果你使用USB摄像头,则可以使用OpenCV拍照方式,树莓派自带拍照命令好像只对CSI接口相机有用。 配置OpenCV读取摄像头和安装库和上面介绍的一样。

至此,需要用到的开发环境就准备完成了,下面就可以进入代码编写环节了。

4.代码编写

(1)基于OpenCV的拍照
# coding=utf-8
import cv2
import sys
import time
import datetime
import os

if __name__ == '__main__':
    dir = "observation/"
    if not os.path.exists(dir):
        os.mkdir(dir)
    print "save path:", dir

    start_time = int(time.time())
    if sys.argv.__len__() == 2:
        interval = int(sys.argv[1])
    else:
        interval = 10
    print "start time:", datetime.datetime.strftime(datetime.datetime.now(), '%Y-%m-%d %H:%M:%S')
    shot_time = start_time + interval

    while True:
        now_time = int(time.time())
        if now_time == shot_time:
            # windows下特定参数CAP_DSHOW,否则windows会报警告(但也可以运行),但在Linux下不需要此参数,否则没有数据
            cap = cv2.VideoCapture(0 + cv2.CAP_DSHOW)
            ret, frame = cap.read()
            save_str = datetime.datetime.strftime(datetime.datetime.now(), '%Y-%m-%d-%H-%M-%S')
            print "shot time:", save_str
            cv2.imwrite(dir + save_str + ".jpg", frame)
            shot_time = now_time + interval
            cap.release()

采用OpenCV实现的思路很简单,通过计时实现按时拍照功能,到时间就打开相机然后获取数据,完成后再释放。虽然在树莓派上配置OpenCV相对麻烦一些,但基于OpenCV的代码适用性比较广泛,不仅可以用于树莓派,只要是OpenCV能获取到摄像头数据,都可以使用。 脚本只有一个启动参数,就是拍摄间隔,若不指定,默认间隔为10s。

这里再简单提一下,os模块中有mkdirmkdirs两个函数。作用稍微有些区别。mkdir函数顾名思义就是根据给定的路径创建文件夹,但问题是如果当前要创建的文件夹的父目录都不存在的话,它会直接报错,但mkdirs就不会。因此简单来说mkdir只能在已有的目录下创建文件夹,而mkdirs可以在不存在的目录中递归地创建文件夹。

(2)基于Raspistill命令的拍照
# coding=utf-8
import sys
import time
import datetime
import os

if __name__ == '__main__':
    dir = "observation/"
    if not os.path.exists(dir):
        os.mkdir(dir)
    print "save path:", dir

    # 旋转角度
    rot = "180"
    # 饱和度,-100 - 100
    sa = "30"
    # 宽度
    width = "1024"
    # 高度
    height = "768"

    # 更多参数参见摄像头文档http://shumeipai.nxez.com/2014/09/21/raspicam-documentation.html

    start_time = int(time.time())
    if sys.argv.__len__() == 2:
        interval = int(sys.argv[1])
    else:
        interval = 10
    print "start time:", datetime.datetime.strftime(datetime.datetime.now(), '%Y-%m-%d %H:%M:%S')
    shot_time = start_time + interval

    while True:
        now_time = int(time.time())
        if now_time == shot_time:
            save_str = datetime.datetime.strftime(datetime.datetime.now(), '%Y-%m-%d-%H-%M-%S')
            print "shot time:", save_str
            os.system("raspistill -o " + dir + save_str + ".jpg " +
                      "-rot " + rot +
                      " -sa " + sa +
                      " -w " + width +
                      " -h " + height)
            shot_time = now_time + interval

相比于OpenCV的代码,这个代码的核心就是构造命令然后定时执行。当然raspistill命令有很多参数可选,想了解更多可以看上面提到的官方文档,很全面。 相比于OpenCV,这种方式可以拍摄到更加高清的影像,还可以指定饱和度等等,毕竟是系统自带的命令,应该是对摄像头专门做过适配的,用起来会比通用的OpenCV方式效果好一些。 脚本只有一个启动参数,就是拍摄间隔,若不指定,默认间隔为10s。其它的参数可以直接在脚本中修改。

(3)基于照片生成延时视频
# coding=utf-8
import cv2
import os.path

# 用户输入存放影像的文件夹目录,如E:\L0
rootdir = raw_input("Input the parent path of images:\n") + "\\"

# 输出视频路径
output = raw_input("Input output video path:\n")

# 考虑到适用性,增加了影像文件类型的选择,如tif、jpg、png等
type = raw_input("Input file type of images:\n")

# 考虑原始影像可能非常大,如4096*3072,直接拼接成视频可能不方便观看
# 因此设置了缩放因子,这样可以指定输出视频的大小
scale = input("Input scale of video(0-1):\n")

# 用于控制输出视频帧率
fps = input("Input fps of video:\n")

print "OK...Processing...\n"

# list,用于存放遍历得到的用户指定类型的影像文件
paths = []
names = []

# 遍历
for parent, dirname, filenames in os.walk(rootdir):
    for filename in filenames:
        # 判断,如果是用户指定的类型则添加到list,否则什么也不做
        # 这里用endswith更好一些,因为如果用contains,有可能有些文件是".tif.xml"
        # 这样即使不是tif,但是还是会被加进来,但用endswith就不会了
        if filename.endswith(type):
            paths.append(parent + '\\' + filename)
            names.append(filename)

if paths.__len__() is not 0:
    for path in paths:
        print path
print paths.__len__(), "frames were found."

# 读取第一张影像,获取其大小,然后计算输出视频的大小
tem = cv2.imread(paths[0])
width = int(scale * tem.shape[1])
height = int(scale * tem.shape[0])

# 指定输出视频的编码器以及相关参数
fourcc = cv2.VideoWriter_fourcc(*'XVID')
out = cv2.VideoWriter(output, fourcc, fps, (width, height))

# 用于统计进度的循环变量
count = 0

# 循环处理每一帧,组成视频
for i in range(paths.__len__()):
    frame = cv2.imread(paths[i])
    str_time = names[i].split(".")[0].split('-')
    year = str_time[0]
    month = str_time[1]
    day = str_time[2]
    hour = str_time[3]
    minute = str_time[4]
    second = str_time[5]
    frame = cv2.resize(frame, (width, height), interpolation=cv2.INTER_AREA)
    frame = cv2.putText(frame, year + "-" + month + "-" + day + " " + hour + ":" + minute + ":" + second,
                        (10, height - 25), cv2.FONT_HERSHEY_SIMPLEX, 0.5,
                        (255, 255, 255), 1, cv2.LINE_AA)
    out.write(frame)
    count += 1
    print paths[i] + "  " + ((count * 1.0 / paths.__len__()) * 100).__str__() + " %"

# 释放VideoWriter对象
out.release()

print "--------------------------------"
print "Output Video Information:"
print "Output path:" + output
print "Width:" + width.__str__()
print "Height:" + height.__str__()
print "FPS:" + fps.__str__()
print "Frames:" + paths.__len__().__str__()
print "Time:" + (paths.__len__() * 1.0 / fps * 1.0).__str__() + " s"
print "--------------------------------"

这部分代码来自于这篇博客,只是对每一帧进行了额外的处理,添加了拍摄时间,如果不需要时间的话,那就完全一样了。 可以控制输出视频大小、FPS等参数,按提示输入即可。

(4)打开摄像头预览

为了方便调试,写了个工具类的脚本,用于查看摄像头预览画面,以便合适地摆放位置,选择好角度。

# coding=utf-8
import cv2
import sys

if __name__ == '__main__':
    cap = cv2.VideoCapture(0)

    fps = 10
    waitTime = 20

    width = int(cap.get(3))
    height = int(cap.get(4))

    print width, height

    if sys.argv.__len__() == 1:
        while cap.isOpened():
            ret, frame = cap.read()
            if frame is None:
                break
            else:
                cv2.imshow("frames", frame)
                k = cv2.waitKey(waitTime) & 0xFF
                if k == 27:
                    break
        cap.release()
    elif sys.argv.__len__() == 2:
        angle = int(sys.argv[1])
        M = cv2.getRotationMatrix2D((width / 2, height / 2), angle, 1)
        while cap.isOpened():
            ret, frame = cap.read()
            if frame is None:
                break
            else:
                rotate = cv2.warpAffine(frame, M, (width, height))
                cv2.imshow("frames", rotate)
                k = cv2.waitKey(waitTime) & 0xFF
                if k == 27:
                    break
        cap.release()

脚本只有一个启动参数,就是旋转角度,若不指定则不旋转,若指定如180,则表示选择180度。

5.总结

总的来说使用树莓派进行延时摄影难度并不是很大,可能前期的难点在于树莓派上OpenCV环境的搭建。因为树莓派是ARM架构,直接用pip是找不到合适的安装包的,只能自己编译。 如果有兴趣,可以研究OpenCV的交叉编译,但我看了几天还是有些问题没有解决,所以并没有使用。最终还是老老实实选择了在树莓派上直接编译,虽说会慢一些,但也还行,编译完大约1个小时左右,还可以接受。

最后,把所有代码都放到了Github上,感兴趣可以查看,欢迎下载测试,拍摄属于自己的延时摄影。

本文作者原创,未经许可不得转载,谢谢配合

返回顶部