使用 raspberry pi 搭建延迟摄影平台(上)

之前买了一块 200+W 的电池,驱动什么大的电器是不行的,小的电器也没什么意思,突然意识到也许作为 timelapse 的供电方案会比一个充电宝强大许多,于是考虑做一个复杂一点的东西。

硬件

由于驱动拍照本身可用的软件比较少,一定程度上限制了硬件的选择,这里的相机最好是 gphoto2 兼容的,除此以外我们需要

  • 相机:我这里可以使用的是 Nikon D80(在 octoprint 之前用过)或者索尼 a6000(二手大约 200$)
  • 三脚架:不用废话
  • 连接线:sony 据说可以使用一根 USB(micro USB),但是似乎我这里 a6000 可以,但是 RX100V 不晓得为啥不行,后面讨论一下不同连接方式的问题
  • 驱动:一台 raspberry pi 4(带有 wifi / BT 支持),可安装后面讨论的软件,提供了四个 USB,不加 hub 的情况下可以支持最多 4 台相机
  • 电源,我这里拿了个 EcoFlow River 2,256W(打折的时候 150-180$ 可以拿下):它的 USB type C 可以用来给 RPI 供电,其余的 USB 可以驱动额外的屏幕和相机
  • 屏幕与键盘(可选),在家里面用的话有 wifi,这些不是必要的;户外可以考虑另外的方案
  • DC-DC converter,这主要是相机的供电大约需要 7-8V,而电源输出的电压或者是 USB 的 5V 或者是车载的 12V,因此需要转换电压
  • 假电池,这个主要是插相机里面提供电力,新的相机也许可以直接从 USB 取电,会方便很多
  • 存储:RPI 本身的系统可以考虑使用比较便宜的 16G 的 TF 卡,而拍摄获得的照片可以考虑通过 USB 或者 wifi/ethernet 传输到另一个介质里,比如 SSD/NVMe 磁盘里面(现在 1T 的 NVMe 可能不到 50$)

软件

我们讨论一下基本的想法,提供一些基本的实现。最后讨论一些取舍。某些新的相机本身就提供了 timelapse 拍照的能力,我手头这部比较老的 a6000 仅仅在其 app store 里面提供了一个 10$ 的应用,这大概是最坑人的设计了吧(买了相机还要花钱买它家的 app)。仅仅通过相机本身也是可以获得不错的效果的,我们这里加入了这么多的硬件,自然希望获得更多的好处。

基本环境

在 RPI 上可以安装 gphoto2、对应的 python binding(现在 RPI 上提供的是 python 3.9 对一般性的用途应该是绰绰有余)。我们可以通过如下的命令测试相机是否连接成功,Sony 的连接需要在相机一方设置类型为 PC remote。连接成功后可以查看到

$ dmesg
[   60.467646] usb 1-1.1: new high-speed USB device number 3 using xhci_hcd
[   60.569174] usb 1-1.1: New USB device found, idVendor=054c, idProduct=094e, bcdDevice= 1.00
[   60.569205] usb 1-1.1: New USB device strings: Mfr=1, Product=2, SerialNumber=3
[   60.569222] usb 1-1.1: Product: ILCE-6000
[   60.569237] usb 1-1.1: Manufacturer: Sony
[   60.569251] usb 1-1.1: SerialNumber: ......

这时也可以使用

$ gphoto2 --auto-detect
Model                          Port
----------------------------------------------------------
Sony Alpha-A6000 (Control)     usb:001,003

如果需要拍照并下载可以

$ gphoto2 --auto-detect --capture-image-and-download --filename Test-%n.%C

因此如果你用 shell 脚本写个批处理之类的话,大概就是 while true 反复调用这个命令,中间加上 sleep 就行了。

轻量级可视化桌面

通常在家的时候 wifi + ssh 是一个比较便捷的组合,可是一旦跑到户外没有显示器,一种可能就是使用 VNC server,然后通过手机端的 VNC viewer 与其协同工作(用手机的屏幕查看结果)。手机端可以安装 JuiceSSH 作为 ssh 客户端,Real VNC Viewer 作为 VNC 的客户端即可。 RPI 上需要安装 tightvncserver + X window manager(我这里使用了轻量级的 Fluxbox + lxterminal)。我们需要进行如下配置 vncpasswd 设置登录密码,编辑 ~/.vnc/config

session=fluxbox
geometry=1920x1200
localhost
alwaysshared

然后测试一下,执行 vncserver -name pi,此时它占用 :1(对应 5901 端口),然后通过其他机器进行连接,验证可用后,我们需要将此程序通过 systemd 启用。

拍摄程序

我们这里先给出一个简单的实现,它仅仅考虑简单的定时拍摄

import contextlib
import datetime
import logging
import math
import os
import time

import click
import gphoto2 as gp

@contextlib.contextmanager
def default_camera():
  camera = gp.Camera()
  try:
    camera.init()
    summary = camera.get_summary()
    logging.info('Camera initialized, collecting camera summary and config')
    print(summary.text)
    config = camera.list_config()
    for i in range(len(config)):
      print(f'index={i:>3}: NAME={config.get_name(i)}, VALUE={config.get_value(i)}')
    yield camera
  finally:
    camera.exit()

@click.group()
@click.option('--log_level', default=logging.INFO, help='logging level')
@click.pass_context
def main(ctx, log_level):
  logging.basicConfig(format='%(levelname)s: %(name)s: %(message)s', level=logging.INFO)
  cb = gp.check_result(gp.use_python_logging(mapping = {
    gp.GP_LOG_ERROR: logging.DEBUG,
    gp.GP_LOG_VERBOSE: logging.INFO,
    gp.GP_LOG_DEBUG: logging.DEBUG,
    gp.GP_LOG_DATA: logging.DEBUG - 5,
  }))
  ctx.ensure_object(dict)
  ctx.obj['callback'] = cb  # keep this live

@main.command()
@click.option('--total_count', default=None, type=int, help='number of shots')
@click.option('--interval', default=5, type=int, help='shoot interval in seconds')
@click.option('--output_directory', default='/tmp', help='output photos in this directory')
@click.option('--photo_prefix', default='test', help='file name prefix')
@click.pass_context
def start_time_lapse(ctx, total_count, interval, output_directory, photo_prefix):
  count = 0
  last_start_time = None
  try:
    with default_camera() as camera:
      while True:
        start_time = datetime.datetime.now()
        if last_start_time:
          actual_interval = (start_time - last_start_time).total_seconds()
          if abs(actual_interval - interval) / interval > 0.1:
            logging.info('Tolerance violated when taking %d photo: %fs but expected %fs', count-1, actual_interval, interval)
        if count > 0:
          event_type, event_data = camera.wait_for_event(10)
          #if event_type != gp.GP_EVENT_CAPTURE_COMPLETE:
          #  raise RuntimeError(f'Unexpected event: {event_type}, {event_data}')
        camera_file_path = camera.capture(gp.GP_CAPTURE_IMAGE)
        camera_file = camera.file_get(camera_file_path.folder, camera_file_path.name, gp.GP_FILE_TYPE_NORMAL)
        file_suffix = os.path.splitext(camera_file_path.name)[-1]
        local_file_path = os.path.join(output_directory, f'{photo_prefix}-{count:0>5}{file_suffix}')
        camera_file.save(local_file_path)
        camera.file_delete(camera_file_path.folder, camera_file_path.name)
        end_take_time = datetime.datetime.now()
        count += 1
        actual_sleep_interval = (datetime.timedelta(seconds=interval) - (end_take_time - start_time)).total_seconds()
        if actual_sleep_interval < 0:
          missed_shot = math.ceil(-actual_sleep_interval / interval)
          actual_sleep_interval = -actual_sleep_interval - (missed_shot - 1)*interval
          logging.warn('Missed shots = %d', missed_shot)
        logging.info('Captured %s at %s, sleep for %f seconds', local_file_path, start_time.isoformat(), actual_sleep_interval)
        if total_count and count >= total_count:
          break
        last_start_time = start_time
        time.sleep(actual_sleep_interval)
  except KeyboardInterrupt as e:
    logging.info('Captured %d photos in total.', count)
    raise e
  logging.info('Captured %d photos in total.', count)


if __name__ == '__main__':
  main()

这主要是参看 gphoto 的例子写的。

后处理

Sony 相机会生成一个专有的 arw RAW 格式,如果想趁此时间也转换成为 DNG 方便后期处理一些事物的话可以使用 raw2dng 这个项目,下面的脚本给出了一个安装该程序的过程,供参考

$ git clone
$ cd
$ mkdir build && cd build
$ cmake -E env CXXFLAGS="-DkBigEndianHost=0 -DqDNGBigEndian=0" CFLAGS="-DkBigEndianHost=0 -DqDNGBigEndian=0" cmake ../
$ make
$ make install DESTDIR=$HOME

装好了之后可以使用 raw2dng 将 arw 转换到 dng 格式(大概需要 15s / 张 24M pixel 照片)。如果照片生成的频率较高,这个程序可能还是稍微嫌慢。

另外一种可能是将生成的 jpeg 文件利用上,直接添加到一个视频文件里面或者按照帧数播放一下,这样就可以获得预览了(拍摄过程中)。

连接方式

相机的连接方式无外两种:

  • USB tether:这个方式的好处是类似 gphoto 的软件可以比较容易控制,不占用控制器的连接方式
  • WIFI:通常不少 gimbal、手机 app 会选择 WIFI 来控制相机,这样减少了一根线,但是会有一些自己的问题
    • Sony 的实现(可能是相机比较老?)仅仅支持相机自己起个 AP 的模式下通过 app 或者 API 来进行控制,这样一来 RPI 就必须连接到相机的 WIFI,如果想通过 WIFI 控制 RPI 就比较麻烦
    • 传输速率低于 USB,特别是 raw 文件相对较大,

其实 RPI4 算是一个不错的小型控制器了,提供的连接方式也比较多样

  • WIFI / ethernet,这个在家里面搞一下应该是个不错的选择
  • 户外的一种可能是通过键盘和屏幕,比如这种小键盘和可移动的屏幕;或者使用一个可移动的无线路由

实操

与常规的延迟相比,星空摄影相对来说比较有意思:

  • 拍摄的环境较黑,需要相当大的曝光量才能将星空记录下来
  • 存在两种风格:
    • 无轨迹版:比较适合银河、星云这类,后者还需要特别的硬件赤道仪
    • 有轨迹:比较适合较少星星的拍摄情况

无轨迹基本上要上较高的 ISO (800-3200)和尽量大的光圈,这样将曝光时间控制在 15s 以内(也有用 400 / 135 等效焦距来计算时间的说法)。这样对软件来说是比较容易控制的。

有轨迹的需要使用较长的曝光时间(比如 120s),因此很多相机本身并不支持这个选项,需要使用 bulb mode 和定时器来完成。拍完之后为了避免“断轨”,需要继续以相同的设置立即继续拍摄。这种情况下可使用相对稍小的 ISO。

另外,如果我们以天为单位来拍摄,跨越多天来拍摄的话有意思的一点大约是夜晚和白天使用的曝光相距是较大的,如何调整 time lapse 的参数是一个有意思的事情。

使用 raspberry pi 搭建延迟摄影平台(上)

一个有关“使用 raspberry pi 搭建延迟摄影平台(上)”的想法

留下评论