"LTガチバトル 新郎新婦+3本勝負" 枠において、ワンオブ3本枠として発表した内容のディレクターズカット版です。スライドだけ公開してもなんのこっちゃとなりそうなので、時間があったらこういう内容を喋っていた…という内容を書いてみよう、という趣旨でやっていきます。
(ここで完成物を使ったデモとして記念写真を撮影した)
近年、オールドデジカメを使うとエモい写真が取れる、という流行というか風潮がありますね。
- Digital cameras back in fashion after online revival
- オールドコンデジが若者の間で流行しているらしいので自室から発掘してきた:荻窪圭のデジカメレビュープラス(1/9 ページ) - ITmedia NEWS
- いま、スマホではなく「オールドデジカメ」で撮る理由 | ギズモード・ジャパン
- 「平成レトロな写真が撮れる」コダックのコンデジが人気沸騰中
- Z世代に「オールドコンデジ」なぜ流行? 秋葉原のカメラ専門店で聞いてみた|Real Sound|リアルサウンド テック
デジカメ専用機だけでなく、ニンテンドーDS/3DSの内蔵カメラや、PSPのカメラユニット、あるいは古めのスマホに対しても同様の目線が向けられているように思います。
で、オールドデジカメの写真のどこがエモ成分なのか、最新のカメラで取った写真をそれっぽくすることもできるんじゃないのかと思うわけですが、単に画像ファイルの解像度を揃えるとか、JPEGの圧縮率を上げてみるくらいだとそこまでそれっぽくはならないことが多いでしょう。
自分が思うに、これはデジタルカメラの構成要素のすべてが進化し続けているためであり、画素数であるとかJPEGの圧縮ノイズの乗り具合ではない場所になんらかの味が存在しているということなのではないでしょうか。本気で再現するのであればレンズ・センサー・画像処理アルゴリズムすべてについて味を考慮する必要がありそうです。
今回は、ホンモノを持ち出すことでその味を手っ取り早く獲得することを考えました。クローゼットで眠っていたオールドデジカメ…ではなく、オールドWebカメラを使ってみます。
2011年発売のMicrosoft LifeCam Studioという機種になります。
Webカメラはパソコンで使うものなので、パソコンからいろいろ制御する仕組みが整っています。USBがまずあって、その上にUSB Video Device Class (通称UVC)という仕様が載っている、というような構成が一般的なのかなと思います。
OSの中でUSBとUVCがどのように扱われているかについてはそれぞれ異なると思いますが (上図の「何かしら」部分)、macOSであればUVC接続のカメラはAVFoundationというAPIを使って制御するのが一般的です。
で、今回は ぱ —— Perl陣営を代表してのLTということなので、もちろんPerlからこのスタックのどこかしらにアクセスしていくことで写真を撮っていきたいと思います。
一番最初に思いつく素朴なやり方は、system()
などの外部コマンドを呼び出す関数からffmpegなどを呼び出し、そこでカメラデバイスからの画像の取得を完結させてしまうことではないでしょうか。しかしながら、これは素朴すぎであり、「Perlで写真を撮った」とは言いにくいと思います。
(本番ではこのへんまで喋って時間切れになった気がする)
先に説明したように、macOSにおける正攻法はAVFoundationを呼び出すことなのですが、これにはSwiftまたはObjective-Cのどちらかを使わないといけません。
PerlにはXSというネイティブ拡張の仕組みが用意されているので、そこからObjective-CのAPIを呼び出すことは理論上可能なはず…なのですが、自分はObjective-CもXSも不慣れなのでちょっとなかなかしんどそうです。
ここで、libuvcというライブラリの存在を思い出します。普通のC言語製ライブラリなので、AVFoundationよりは簡単にXSから呼び出せるのではないかという魂胆でのチョイスです。
というわけで、minil new -p XS Acme::SuperUVC::XS
して開発開始です。
minil
コマンドとは、MinillaというCPANモジュールのオーサリングツールです。minil new
は、PHPでいうと composer create-project
、Rubyでいうと bundle gem
に相当する操作になります。-p XS
オプションを付けることで、XSを使ったプロジェクトの雛形が生成されます。
(bundle gem
のところを bundle init
と誤って紹介していました、失礼しました。指摘いただいた id:onk さんありがとうございました :pray:)
で、自分はXSを書いたことがなかったので、ここからは緊急入門しながらの開発となります。
6時間ほどの苦労の結果、libuvcのサンプルコードの断片をPerlから発動させるような素朴なライブラリの作成に成功しました。正直思ったよりも時間がかかってしまったのですが、ここでハマったポイントについては最後に言及します。
で、libuvcのサンプルコードの断片をPerlから発動させることはできたのですが、UVCデバイスを実際に開くuvc_open()
がなんかデバイスを開けない的なエラーで終了してしまいます。
なぜこのようなエラーになっているのか見当がつかないので、ログを増やしたいのですが、libuvcにはそういう設定がなさそうでした。隠しオプションみたいなのがあったりしないだろうかと思ってlibuvcのソースコードを眺めてみたところ、ビルド時の設定として ENABLE_UVC_DEBUGGING
というフラグを与えてやればよさそうなことがわかります。
CMakeLists.txt で定義されているフラグなので cmake -DENABLE_UVC_DEBUGGING=ON .
みたく指定したらよさそうです。
よさそうだったのですが、ここでまず -DENABLE_UVC_DEBUGGING=ON
を指定したときに、見慣れない名前のライブラリが存在しないというエラーでlibuvcのコンパイルが完走しなくなってしまいました。以下のpull requestで言及があるのですが、Android NDK由来のライブラリのようです。
brew install android-ndk
してからしばらくゴニョゴニョ試行錯誤していたのですが、NDKのどの部分が要求されているのか見当がつけられませんでした……。コードをざっとgrepしてみて、CMakeLists.txtの当該の記述部分を削除してもコンパイルできて、動きもするんじゃないかという予感がしたので、実際にそうしてみたところ、少なくとも自分が使った部分は普通に動いてしまいました……。
(今気づいたのですが、Android NDKないならないでもコンパイルできるようにするpull requestがマージされずにcloseされていたので、これがマージされていたらうっかり悩まずに済んだのかもしれません)
- Fix missing log library issue when no android ndk by lifei · Pull Request #225 · libuvc/libuvc · GitHub
libuvcのデバッグログ有効バージョンがコンパイルできた話に戻ると、以下のようなエラーメッセージが得られました!
not claiming interface 0: unable to detach kernel driver (Access denied)
わかりやすい!しかしながら、root権限が必要そうな操作をしているわけでもないはずなのにkernel driverがどうのでAccess denied、と言われているのはかなり不穏です。不穏ですが、そう言われてしまったときに考えることは一つ —— sudoつけたら動くってコト!?
sudoをつけたら動いてしまいました!下の方に callback! みたいなログが連なっていますが、これはlibuvcがカメラデバイスの初期化に成功し、実際に映像が得られたときのコールバック関数まで呼ばれていることを示しています。
さて困りました。カメラからの映像が得られたのは良いニュースですが、root権限でないと動かないのは、今回のようなめでたい席のLTにはあまり相応しくないように思います。なんでこんなことになってるんだ?と思って調べてみたところ、近年のmacOSのデバイスドライバ関連のAPIの変更によるものではないか、というissueがlibuvcやlibusbに複数立っていました!
- How to use the new macOS kernel detach feature for non-root user · Issue #1014 · libusb/libusb · GitHub
- libuvc didn't work on the macos12 · Issue #188 · libuvc/libuvc · GitHub
先に調べとけという話なのですが、こういう行き当たりばったりを楽しむのも人生ということで………。
というわけで、先程の図に戻ります。近年のmacOSにおいては、Objective-C + XSを避けてPerlからWebカメラにアクセスするのはなかなか難しそうなことがわかりました。
今度は本当に困りました。途方に暮れながら、ふとmetacpanで "avfoundation" と検索してみたら………
なんかそれっぽいパッケージがヒットする!!!!!
PDL::OpenCV::Videoio
!!名前もそれっぽすぎる!!!マジで??
調べてみたところ、これはPDL: Perl Data Languageというフレームワーク、Pythonでいうところのnumpyみたいなやつ、の一部のようです。数値計算したいときって画像処理の話であることも多いよね、的な感じ (推測) でPDLにOpenCVのバインディングが存在しており (PDL::OpenCV
)、そのOpenCVの一機能であるUVC撮影グッズに対応したPerlバインディングも用意されている (推測)、という状況のようです。
ウキウキで試そうと cpanm PDL::OpenCV
してみたところ、執筆時点でmetacpanにアップロードされているバージョン0.001はコンパイルに失敗しインストールできませんでした…が、GitHub上のmainブランチ (執筆時点で b307d7b
) の内容はインストールできました。
(人力bisectしてみたところ、おそらく以下の修正でコンパイルエラーの原因が修正されているみたいです)
さて、インストールできた PDL::OpenCV
と PDL::OpenCV::Videoio
を使ってみます。アプリケーションがカメラの権限を要求するあの見慣れたダイアログが、iTermという見慣れないアプリケーションから出てきているのが愉快です。もちろんroot権限不要で実行できています。
で、写真が撮れたのですが……なんか色味が変!あとなんかひっくり返っている!
最初は自分の顔とか手を適当に撮影していたのですが、フォトショのレタッチ機能では狙った色味に修正できないくらい変な色味だったので、テレビなどで用いるカラーバーを撮影してみています。自分の人生のなかでこれがガチの用途で役に立つことがあるなんて……。
見にくかったので上下反転してしまいました。下部の正解と見比べると……なんか右側がわかりやすいですね。赤と青がきれいに入れ替わっています。
ここで突然記憶が呼び起こされ、BGR形式の存在を思い出します。
教えに従い、適切に色変換をかますと…
正しい配色になりました!自分の部屋が暗くて色温度高めなのは実際こういう感じなので正解です。背景の生活感がヤバいのはすみません……。
ということで、冒頭に実施したPerl製記念撮影グッズはこうして完成したのだ、ということでした。
実装をベロっと貼ってしまうとこういう感じでした:
# Camera.pm use PDL; use PDL::OpenCV::Videoio; use PDL::OpenCV; use PDL::OpenCV::Imgproc qw(cvtColor); use PDL::IO::Pic; use PDL::Ufunc; # for max use File::Temp qw/tempfile/; use MIME::Base64; sub launch_camera { my ($class, %args) = @_; my $wb_temperature = $args{wb_temperature} || 100; my $exposure = $args{exposure} || -8; my $device_index = $args{device_index} || 0; my $save_dir = $args{save_dir} || $ENV{HOME}; my $obj = PDL::OpenCV::VideoCapture->new4($device_index); my $is_success = $obj->open3($device_index); die unless $is_success; # このあたりはOpenCVの設定そのまま $obj->set(PDL::OpenCV::Videoio::CAP_PROP_AUTO_WB, 1); $obj->set(PDL::OpenCV::Videoio::CAP_PROP_WB_TEMPERATURE, $wb_temperature); $obj->set(PDL::OpenCV::Videoio::CAP_PROP_AUTOFOCUS, 1); $obj->set(PDL::OpenCV::Videoio::CAP_PROP_AUTO_EXPOSURE, 0); $obj->set(PDL::OpenCV::Videoio::CAP_PROP_EXPOSURE, $exposure); while (1) { # reset terminal print "\33[H\33[2J"; my ($image,$res) = $obj->read; # BGR / RGB 変換 my $img2 = cvtColor($image, PDL::OpenCV::Imgproc::COLOR_BGR2RGB()); # 上下反転 my $img3 = flip($img2, 0); # ↓ の wim (write image) では拡張子からファイル形式を推定してくれるので jpg を指定しておく my $tmp = File::Temp->new(SUFFIX => '.jpg'); $img3->wim($tmp->filename); my $data; { local $/; open my $fh, '<', $tmp->filename; $data = <$fh>; close $fh; } # 今撮れた写真を iTerm2 Inline Image Protocol で出力 my $base64 = encode_base64($data, ""); print "\033]1337;File=inline=1:$base64\a\n"; my $in = <STDIN>; chomp $in; # `s` で始まる入力が得られたら画像を保存して終了 if ($in =~ /^s/) { # write $data to home { my $epoch = time; my $dst_filename = "$save_dir/$epoch.jpg"; open my $fh, '>', $dst_filename; print $fh $data; close $fh; warn "wrote to $dst_filename\n"; } last; } } $obj->release; }
# runcamera.pl use Camera; use Getopt::Long 'GetOptions'; my $wb_temperature = 100; my $exposure = -8; my $device_index = 0; my $save_dir = $ENV{HOME}; my $show_help = 0; GetOptions( 'wb-temperature=i' => \$wb_temperature, 'exposure=i' => \$exposure, 'device-index=i' => \$device_index, 'save-dir=s' => \$save_dir, 'help' => \$show_help, ); if ($show_help) { print <<HELP; $0 [options] --device-index=0 Index of device(s) --wb-temperature=100 ref. CAP_PROP_WB_TEMPERATURE --exposure=-8 ref. CAP_PROP_AUTO_EXPOSURE --save-dir=$ENV{HOME} Directory to save images --help show this message HELP exit; } launch_camera( wb_temperature => $wb_temperature, exposure => $exposure, device_index => $device_index, save_dir => $save_dir, );
実質OpenCV入門やんけ!
iTerm2の画像埋め込み機能を使ってプレビューを表示し、満足できる写真が撮れたら s
と入力すると指定したディレクトリに保存できる、という、いろいろ割り切りつつ意外と普通に使えてしまう感じに作れたのは良かったかなという感じです。
さて、XSの世界に飛び込むにあたり、豊富な日本語資料に大変助けられました。その一部を紹介します:
一つ気づいたことがあって、おそらくMinillaって今回でいうとlibuvcみたく外部のライブラリに依存する形のXSを雛形そのままには書き始められないのかな?ということです。今回は時間がなかったので、まずModule::Buildを用いる設定に変えて、minil build
で生成されるBuild.PLに直接 extra_linker_flagsオプションを記述し、その後は perl Build.PL
だけ実行する、という荒くれた手段をとってしまいました。これは追ってフィードバックできればと思っています。
おしまい!
というわけで、今回はめでたい席だったので、どう写真を撮ってもあとから見返したときにエモくなるのは確実だろうとは思うのですが、変なカメラで変な写真を撮ること自体が思い出になればいいかなと思ってこういうトークにしてみ……たのですが、デモの写真撮影で想定の10倍くらい時間を使ってしまったのが反省です。楽しんでもらえていたら幸いです!
画像処理の話はほぼしませんでしたが、デジカメがイメージセンサーから上がってきた画像をどう画像ファイルにしてんの?っていうちゃんとした話が気になる方には、こちらのエントリから始まる一連の連載が非常にわかりやすくオススメです。
ここまでで紹介した内容は、以下に示す環境で開発・検証を行ったものです:
- macOS Sequoia 15.1.1
- MacBook Pro (M1, 2020)
- Perl 5.40.0