ソフトウェアで制御可能な電動フォーカス機能付き、Raspberry Pi用CSIインターフェイスカメラ "Arducam B0176"

コンピュータービジョン(CV)に欠かせないカメラ。Raspberry Piには多くのカメラモジュールが存在しますが、フォーカスをソフトウェアから操作できると、設置場所の自由度が向上します。

ソフトウェアで制御可能な電動フォーカス機能付き、Raspberry Pi用CSIインターフェイスカメラ "[Arducam B0176](https://www.arducam.com/product/raspberry-pi-camera-5mp-autofocus-motorized-focus-camera-b0176/)" の紹介と使い方です。

## Arducam B0176 の特徴

- フォーカスが電動、ソフトウェアから 1024 段階で設定可能
- 他のRaspberry Pi向けカメラと同様の形状とネジ穴の位置で、物理的に互換
- ソフトウェアも他のRaspberry Pi向けカメラと同様のCSIインターフェイス
- 500万画素(5MP)で十分な解像度
- 安い！ (約1700円)

電動フォーカスの様子はYouTubeを見るのが早いです。(40秒)
[![Arducam B0176](http://img.youtube.com/vi/NAlvdgab4-A/0.jpg)](http://www.youtube.com/watch?v=NAlvdgab4-A)

## Arducam B0176 の情報源

- 商品詳細: https://www.arducam.com/product/raspberry-pi-camera-5mp-autofocus-motorized-focus-camera-b0176/
- ハードウェア仕様やセットアップ方法: https://www.arducam.com/docs/cameras-for-raspberry-pi/native-raspberry-pi-cameras/5mp-ov5647-motorized-focus-camera-modules/
- サンプルコード: https://github.com/ArduCAM/RaspberryPi/tree/master/Motorized_Focus_Camera/
- 販売ページ(Amazon.co.jp): https://www.amazon.co.jp/dp/B07SN8GYGD/

※ 日本語情報がほとんど無いですね。

### ミスリードしそうな製品名 "Auto Focus"

商品詳細や販売ページでは "Auto Focus" と書いてありますが、正確には「電動フォーカス」です。
オートフォーカスは OpenCV 等を使って輪郭抽出をしながらソフトウェアでフォーカスを調整して実現しています。ですので、「できない事はないけれど、この製品自体にオートフォーカスが備わっているわけでは無い」のでお気を付けください。

## Arducam B0176 の仕組みとセットアップ

### 対象環境

* Raspberry Pi 4 model B
* Raspberry Pi OS 2021/1/11 (GUIがあるほうが動作確認が楽です)

### カメラのセットアップ

Arducam B0176はカメラ画像をCSIで、電動フォーカスはI2Cで動かしています。そのため、Raspberry Pi OSではCameraとI2Cの2つを有効化しておく必要があります。

```console
$ sudo raspi-config nonint do_camera 0
$ sudo raspi-config nonint do_i2c 0
$ sudo sed -i 's/i2c_arm/i2c_vc/' /boot/config.txt
$ sudo systemctl reboot
```

注意点はI2Cの有効化です。 `/boot/config.txt` では `dtparam=i2c_vc=on` というエントリーにする必要があります。
raspi-config で有効化した場合は `dtparam=i2c_arm=on` 設定されます。この状態で電動フォーカスを動かそうとすると `Failed to set I2C address` というエラーが発生します。
パラメータの意味は [/boot/overlays/README](https://github.com/raspberrypi/firmware/blob/master/boot/overlays/README) をご覧ください。

再起動後に `vcgencmd get_camera` で動作確認をしてください。 supported と detected がそれぞれ 1 になっていれば動作しています。

```console
$ sudo vcgencmd get_camera
supported=1 detected=1
```

この時点で `raspistill` 等のコマンドも動作するはずです。

### 電動フォーカスを動かすライブラリのインストール

電動フォーカスを動かすためのライブラリは [libarducam_vcm.so](https://github.com/ArduCAM/RaspberryPi/blob/master/Motorized_Focus_Camera/python/lib/libarducam_vcm.so)になります。
これをRaspberry Pi にダウンロードし、共有ライブラリとして読み込みます。

```console
$ curl -LO https://github.com/ArduCAM/RaspberryPi/raw/master/Motorized_Focus_Camera/python/lib/libarducam_vcm.so
$ sudo install -m 0644 libarducam_vcm.so /opt/vc/lib/ && rm libarducam_vcm.so
$ sudo ldconfig
```

sudo ldconfig 、もしくはOSを再起動すれば読み込まれます。
ldconfig -p で libarducam_vcm.so が読み込まれていることが確認できれば完了です。

```console
$ ldconfig -p | grep arducam_vcm
        libarducam_vcm.so (libc6,hard-float) => /opt/vc/lib/libarducam_vcm.so
```

## フォーカス操作をしてみる

libarducam_vcm.so を実行するPythonスクリプト([Arducam_B0176_focus_ctl.py](https://gist.github.com/ma2shita/a6ad200bea550f7211636783f17539b9)を用意しました。

```console
$ curl -O https://gist.githubusercontent.com/ma2shita/a6ad200bea550f7211636783f17539b9/raw/997f648d3c99fe5c6cb6313275ff3dcc5fe60403/Arducam_B0176_focus_ctl.py
$ chmod u+x Arducam_B0176_focus_ctl.py
$ ./Arducam_B0176_focus_ctl.py -h
usage: Arducam_B0176_focus_ctl.py [-h] --focus {Integer,0 <= N <= 1023}

optional arguments:
  -h, --help            show this help message and exit
  --focus {Integer,0 <= N <= 1023}
                        Camera focus 0(far)..1023(near)
```

動作確認ですが、GUIであれば、もう一つターミナルを開いて `raspistill -t 0` とプレビューウィンドウを表示させたまま、Arducam_B0176_focus_ctl.pyを動かすとフォーカスの変化がわかりやすいでしょう。

```
$ Arducam_B0176_focus_ctl.py --focus 0    # 焦点:近い (数センチレベル)
$ Arducam_B0176_focus_ctl.py --focus 1023 # 焦点:遠い
```

CLIの場合は一度 `raspistill -t 0` でカメラを動かしながらArducam_B0176_focus_ctl.py でフォーカスを変更し、改めて `raspistill` で画像を取る形になります。

電動フォーカスはカメラの動作中に操作可能です。そのため、動画撮影中でもフォーカス変更ができます。
※逆に、カメラが動作していないと電動フォーカスを動かすことはできません。

### 仕様

libarducam_vcm.so の利用可能なメソッドは以下の通りです。

* unsigned char vcm_init(void)
* unsigned char vcm_write(unsigned int focus_val)
    * focus_val: 0(遠距離) .. 1023(近接)

※ このリファレンスは [arducam_vcm.h](https://github.com/ArduCAM/RaspberryPi/blob/master/Motorized_Focus_Camera/C/arducam_vcm.h) と [Motorized_Focus_Camera_Preview.py](https://github.com/ArduCAM/RaspberryPi/blob/master/Motorized_Focus_Camera/python/Motorized_Focus_Camera_Preview.py#L33) の実装から推測と試験の結果です。


## 調査ノート

### GitHubに掲載されているセットアップについて

* `enable_i2c_vc.sh`
  * このshファイルは `/boot/config.txt` へ `dtparam=i2c_vc=on` を追加します。ですが、`/etc/modules` へ `i2c-dev` を追加してくれないので、本手順では raspi-config を使うようにしました。
* `python-opencv` パッケージのインストールは不要
  * このパッケージは [GitHub のセットアップ](https://github.com/ArduCAM/RaspberryPi/tree/master/Motorized_Focus_Camera/)ですが、OpenCVによる[オートフォーカスのサンプル](https://github.com/ArduCAM/RaspberryPi/blob/master/Motorized_Focus_Camera/python/Autofocus.py)を動かすためのものです。電動フォーカスを動かすだけなら不要です。

### libarducam_vcm.so のソース

libarducam_vcm.so は[ヘッダファイルはある](https://github.com/ArduCAM/RaspberryPi/blob/master/Motorized_Focus_Camera/C/arducam_vcm.h)のですが、本体のソースが見当たりません。そのためリファレンスもヘッダファイルや他の実装、そして strings コマンドの結果からのリエンジニアリングです。

Raspberry Pi 上でのlddとfileの結果を残しておきます。

```console
$ file libarducam_vcm.so
libarducam_vcm.so: ELF 32-bit LSB shared object, ARM, EABI5 version 1 (SYSV), dynamically linked, BuildID[sha1]=29831c10685a26166a127333e0413e97f586e1ae, not stripped
$ ldd libarducam_vcm.so
        linux-vdso.so.1 (0xbefac000)
        /usr/lib/arm-linux-gnueabihf/libarmmem-${PLATFORM}.so => /usr/lib/arm-linux-gnueabihf/libarmmem-v7l.so (0xb6f2a000)
        libpthread.so.0 => /lib/arm-linux-gnueabihf/libpthread.so.0 (0xb6eec000)
        libc.so.6 => /lib/arm-linux-gnueabihf/libc.so.6 (0xb6d9e000)
        /lib/ld-linux-armhf.so.3 (0xb6f54000)
```

ライセンスは [リポジトリがBSD-3-Clause](https://github.com/ArduCAM/RaspberryPi/blob/master/LICENCE.txt) なので、それに従うことになると思います。

また、現在のフォーカス状態を知る `vcm_read()` のような関数は、探す限りは見つけられませんでした。そのため、フォーカスの状況はプログラム側で把握しておく必要があります。

### i2cset / i2cget コマンドでの操作

センサーにArduCamには[IMX219を使った電動フォーカスカメラのプロダクト](https://www.arducam.com/docs/cameras-for-raspberry-pi/native-raspberry-pi-cameras/8mp-imx219-motorized-focus-camera-module/#10-software-setup)が存在します。そのJetson向けのコードに、[i2csetでフォーカスを設定している部分が存在](https://github.com/ArduCAM/Nvidia_Jetson/blob/master/IMX219_AutoFocus/Autofocus.py#L12)しました。

Arducam B0176でも動作します。

```console
## Focus = 0
$ sudo i2cset -y 0 0x0c 0 0
## Focus = 1023
$ sudo i2cset -y 0 0x0c 63 240
```

どうやらI2Cアドレスは `0x0c` のようです。フォーカスの値を2バイトで書き込むことで動きました。
また、i2cgetによる現在のフォーカス状況も読み出すことができました。

```console
$ sudo i2cset -y 0 0x0c 63 240
$ sudo i2cget -y 0 0x0c 0x00 w
0xf03f
```

この方法が正しいのか否かは不明ですの。ご自分の責任範囲でご利用ください。
一応、i2cset / i2cget を補助するPythonスクリプトを置いておきます。

```python:focusing.py
#!/bin/env python3

"""
Number to Focus for Arducam B0176
Focus control for Arducam B0176 "Motorized focus camera for Raspberry Pi"

Copyright (c) 2021 Kohei MATSUSHITA

This software is released under the The 3-Clause BSD License.
https://opensource.org/licenses/BSD-3-Clause

Usage:
$ sudo i2cset -y 0 0x0c $(./focusing.py 0)
$ sudo i2cset -y 0 0x0c $(./focusing.py 512)
$ sudo i2cset -y 0 0x0c $(./focusing.py 1023)
"""

import argparse
import sys

def focusing(val):
  value = (val << 4) & 0x3ff0
  data1 = (value >> 8) & 0x3f
  data2 = value & 0xf0
  return (data1, data2)

if __name__ == "__main__":
  parser = argparse.ArgumentParser()
  parser.add_argument('focus', metavar='N', type=int, help="Camera focus 0(far)..1023(near)")
  args = parser.parse_args()
  r = focusing(args.focus)
  print("{} {}".format(r[0], r[1]))
  sys.exit(0)
```

```python:focused.py
#!/bin/python3

"""
Number to Focus for Arducam B0176
Focus control for Arducam B0176 "Motorized focus camera for Raspberry Pi"

Copyright (c) 2021 Kohei MATSUSHITA

This software is released under the The 3-Clause BSD License.
https://opensource.org/licenses/BSD-3-Clause

Usage:
$ sudo i2cget -y 0 0x0c 0x00 w | ./focused.py
<Current Focus 0 to 1023>
"""

def focused(str):
  s = str[2:]
  u = int(s[:2], 16)
  d = int(s[2:], 16) << 8
  r = (u + d) >> 4
  return r

import sys
if __name__ == "__main__":
  str = input()
  r = focused(str)
  print(r)
  sys.exit(0)
```

### あまり派手にフォーカスを動かさない方が良いかもしれない

0 => 1023といった大きな数字の移動をすると、電動フォーカスが「カチッ」と動いている音が聞こえます。物理的な可動部分が存在するため、あまり極端な操作を繰り返すと壊れるかもしれません。
また、埃や湿度も気をつける必要がありそうです。

また、I2Cへのコマンド投入後、一定時間はコマンドを受け付けてくれないことがあるため、リトライを前提にしておいた方が無難です。

## あとがき

なんかハマってしまったが、帰ってこられて良かった。
というか、仕様を書いておいて欲しいっすな。

EoT