昨日は毎年参加している、地域の将棋大会だった。
今年も家庭が忙しくて準備不足で、特に今年は直前になっても実戦練習をほとんどやらなかったので、予選突破は完全に諦めていて、予選落ちして指導対局を受ける覚悟で行ったが、意外にも2連勝で予選突破した。決勝トーナメント1回戦敗退はいつも通りだが、予選突破がストレートの分、例年より少し良い結果だった。

決勝Tの1回戦は、悪いと思ってなかった局面から、一方的に攻められて受け方がわからず、そのままボロ負けするという、私にはよくある、指していて全く面白くない将棋になってしまったので、激指14を用いて少し研究してみる。


筆者は後手である。先手が3手目角交換から振り飛車にして、このように▲6六銀△6五歩▲5五銀とぶつけられた。△6三銀と引けば5五の銀が立ち往生して先手が困るのではないか、▲4五歩からの桂馬攻めが見えてるが大したことないだろうと思って引いたら、数分の長考の後、▲7七角とされた。

これを見ても、△4三金右で受かるだろうと思った。
相手は4年前の優勝者で、その年に私も当たって負けたのだが、圧倒的に強い感じではなかったし、長考してる時に意表を突かれて悩んでいる感じだったので、苦し紛れの手だろうと思った。
△4三金右▲4五歩

△同歩▲同桂△4四歩▲3三桂成△同金上▲4五歩△同歩▲4四銀打

となってこの局面になったが、まだ受け切れそうに思っていた。
そこから△4二飛▲4三銀成△同飛▲4四金△4二飛▲3三金△同桂▲4四銀

となって崩壊し、この後△6四角▲3七金△3二金▲5五金から角を取られて▲5一角とされて終了した。

何が悪かったのだろうと激指14で解析してみたら、42手目の△4三金右から58手目の△3三同桂までの間の悪手は56手目の△4二飛だけで、評価値は42手目の△4三金右で+380(Pro+3, 先手有利)、52手目くらいまで後手だけ最善を尽くしても+560、本譜の手順でも56手目の△4二飛の悪手が無くて代わりに△4一飛▲3三金△同桂▲4四銀△3二銀

とすれば+589なので54手目まで評価値がほぼ最善手順と変わらないという感じなので、42手目の△4三金右の時点でどうしようもなかったようだ。
しかし、上の△3二銀の局面の評価値が最善手順と大差ないといっても、52手目に△4二飛とするとこの局面まで一本道なのであり、51手目の段階でこの局面を想像できたとしても、筆者には危なっかしくてこの局面を選択することはできない。▲4四の銀が動くと△4六歩があるので、先手が悩みそうで、勝負手としては良さそうだが、後手がこれで耐えていると判断するのが、筆者には難しそうだ。

局後の感想戦では、相手の人がどこかで△6六歩とされるのが嫌だったと言っていた。▲同角に銀があれば△6五銀ということだろうか。筆者としては先手に1歩渡すリスクが大き過ぎてまず考えられないし、激指14の検討手順には出て来なかったが、先手としては角を狙われるのが一番嫌だったのだろうか。
また、52手目の△4二飛がまずく、△3一桂なら良かったのではないか、という話になった。激指14の検討手順には出て来なかった(52手目の候補手の上位は△5四銀、△4二金、△3二玉)が、進めてみると、△3一桂▲3三銀成△同桂▲4四銀△3二銀

▲3三銀成△同銀▲2五桂△4四銀打▲3三桂成△同銀

で激指14の評価値は+500〜+700くらいで極端に悪くはないし、あくまでも筆者の感性では、後手の手順がわかりやすいし、後手が受け切っている。後手からの攻め味が無いが、先手が焦っていればミスを誘発しやすい。上の手順中、先手には4三の金を取る機会が2回あるが、4三の金を取ると△同桂で先手の攻めが切れやすいと思う(局後の検討では△4三同桂で後手良し)。このようにしたかったと思った。

他に、激指14の候補手の中に、46手目から△5四歩▲3三桂成△同桂▲4四銀△6四角▲4六歩

というのが出てきた。46手目に△4四歩としないと先手から▲4四歩とされて困ると思ったので、ここで△5四歩は選ばなかったのだが、もしこの局面のように▲4六歩を強要できるなら、これを選択したい。ただ、この手順で▲4六歩を強要できるかどうかを考えるのは、筆者には難しすぎる。

少し前に画像認識できるTensorFlowの学習済みモデルを探していると、https://github.com/tensorflow/modelsのofficialの下に、ImageNetデータセットで学習済みの、ResNetのPre-trained modelというのを見つけた。
ResNet以外のモデルやPre-trained modelがほとんど無いので、"official"といってもあまり注目されていないサイトなのかな、TensorFlow Hubがメインストリームなのかなと思いつつも、"official"なので信頼できそうだし、メンテナンスされているだろうからクオリティが高いだろう、難なく動かせるだろうと思って、とりあえず動かしてみようと思った。

しかし、TensorFlowのofficialなものなので、サンプルコードがすぐに見つかるだろうと思ったが、直接的なものを全く見つけることができなかった。その時点でこれは興味を持つ人が少ない、あまり良いものではないのだろうなと確信したが、何せTensorFlowのofficialなものなので、一応動かしておこうと思った。筆者は"official"という言葉に弱いのである。
しかし、TensorFlowの使い方をほとんど知らないまま、巷のサンプルコードをつぎはぎしながらでは予想以上に難しく、見ても何が悪いのかがわからない同じエラーメッセージを何時間も見続けて嫌になったが、それでもofficialなものを動かせなくては敗北と思って、さらに何時間も費やしてしまった。
最終的にはよくわからない所があるまま何らか動いたので、一応そのコードを記録する。

使用したモデルは、"ResNet in TensorFlow"のPre-trained modelの"ResNet-50 v2 (fp32, ...)"のSavedModelの所にある、"(NHWC)"と"(NHWC, JPG)"というリンクの先の、以下の2つのファイルである。
[1] resnet_v2_fp32_savedmodel_NHWC.tar.gz
[2] resnet_v2_fp32_savedmodel_NHWC_jpg.tar.gz
これらを、カレントディレクトリに展開したものとする。 すると、それぞれ
resnet_v2_fp32_savedmodel_NHWC/1538687283/
resnet_v2_fp32_savedmodel_NHWC_jpg/1538687457/
の下に、
saved_model.pb
variables/
が展開される。

それぞれ、saved_model_cliコマンドを使って、入力テンソルと出力テンソルの情報を表示してみる。

$ saved_model_cli show --dir resnet_v2_fp32_savedmodel_NHWC/1538687283/ --all
MetaGraphDef with tag-set: 'serve' contains the following SignatureDefs:

signature_def['predict']:
  The given SavedModel SignatureDef contains the following input(s):
    inputs['input'] tensor_info:
        dtype: DT_FLOAT
        shape: (64, 224, 224, 3)
        name: input_tensor:0
  The given SavedModel SignatureDef contains the following output(s):
    outputs['classes'] tensor_info:
        dtype: DT_INT64
        shape: (64)
        name: ArgMax:0
    outputs['probabilities'] tensor_info:
        dtype: DT_FLOAT
        shape: (64, 1001)
        name: softmax_tensor:0
  Method name is: tensorflow/serving/predict
$ saved_model_cli show --dir resnet_v2_fp32_savedmodel_NHWC_jpg/1538687457/ --all
MetaGraphDef with tag-set: 'serve' contains the following SignatureDefs:

signature_def['predict']:
  The given SavedModel SignatureDef contains the following input(s):
    inputs['image_bytes'] tensor_info:
        dtype: DT_STRING
        shape: (-1)
        name: input_tensor:0
  The given SavedModel SignatureDef contains the following output(s):
    outputs['classes'] tensor_info:
        dtype: DT_INT64
        shape: (-1)
        name: ArgMax:0
    outputs['probabilities'] tensor_info:
        dtype: DT_FLOAT
        shape: (-1, 1001)
        name: softmax_tensor:0
  Method name is: tensorflow/serving/predict

まず、tf.contrib.predictorを使って、"(NHWC, JPG)"[2]についてやってみた。
これは割と簡単に動いた。

●コード例1
import tensorflow as tf
from tensorflow.contrib import predictor

img_path = 'elephant.jpg'
with open(img_path, 'rb') as F:
    jpeg_bytes = F.read()

predict_fn = predictor.from_saved_model("./resnet_v2_fp32_savedmodel_NHWC_jpg/1538687457/")
result = predict_fn({'image_bytes': [jpeg_bytes]})

cls = result['classes'][0]
prob = result['probabilities'][0, cls]
print('class={} probability={:.3f}'.format(cls, prob))
●出力例1
class=386 probability=0.988

動作確認した環境は、macOS 10.13.6 + TensorFlow 1.13.1である。Raspberry Pi 2 v1.2(Cortex A53) + TensorFlow 1.13.1では、import行で後述のエラーが出て動かなかった。
コード中の'image_bytes'というのは、上記のsaved_model_cliの出力にある。
'elephant.jpg'はhttps://keras.io/ja/applications/の"Classify ImageNet classes with ResNet50"のコード例に合わせたものだが、officialなelephant.jpgが見つからなかったので、All-free-download.comの中から選んだ、次の画像を使った。
elephant_in_kobe_zoo_514337_resized.jpg
class=386は、別途調べた(後述)所では"Indian_elephant"なので、正解である。

次に、同じくtf.contrib.predictorを使って、"(NHWC)"[1]についてやってみた。
これはかなり苦労した。

●コード例2
import numpy as np
from PIL import Image
import tensorflow as tf
from tensorflow.contrib import predictor

img_path = 'elephant.jpg'
img = Image.open(img_path).resize((224,224))
x = np.array(img)
x = x.astype(np.float32)

predict_fn = predictor.from_saved_model("./resnet_v2_fp32_savedmodel_NHWC/1538687283/")
result = predict_fn({'input': np.array([x] * 64)})

cls = result['classes'][0]
prob = result['probabilities'][0, cls]
print('class={} probability={:.3f}'.format(cls, prob))
●出力例2
class=386 probability=0.821

上記のsaved_model_cliの出力にあるように、入力テンソルのshapeが(64, 224, 224, 3)なので、1枚の画像だけを認識したくても、必ず64枚分渡す必要がある(認識処理も64枚分まとめてなされる)のである。そのことになかなか気付かなかったので、同じエラーメッセージを嫌になるほど目にする羽目になってしまった。一体そういう仕様にすることにどういう意味があるのだろうか。これでは使い勝手が悪すぎる。
さらに、同じ形状のニューラルネットワークと同じ入力画像を使ってるのに、probabilityの値が"(NHWC, JPG)"[2]よりも悪くなっている。これについて少し調べたことを後述する。

次に、predictorを使わず、TensorFlow APIだけを使って、"(NHWC, JPG)"[2]についてやってみた。

●コード例3
import tensorflow as tf

img_path = 'elephant.jpg'
with open(img_path, 'rb') as F:
    jpeg_bytes = F.read()

with tf.Session(graph=tf.Graph()) as sess:
    tf.saved_model.loader.load(sess, [tf.saved_model.tag_constants.SERVING], "./resnet_v2_fp32_savedmodel_NHWC_jpg/1538687457/")
    class_tensor = sess.graph.get_tensor_by_name('ArgMax:0')
    prob_tensor = sess.graph.get_tensor_by_name('softmax_tensor:0')
    classes, probabilities = sess.run([class_tensor, prob_tensor], {'input_tensor:0': [jpeg_bytes]})

cls = classes[0]
prob = probabilities[0, cls]
print('class={} probability={:.3f}'.format(cls, prob))
●出力例3
class=386 probability=0.988

これはRaspberry Pi 2 v1.2 + TensorFlow 1.13.1でも動いた。
コード中の'ArgMax:0', 'softmax_tensor:0', 'input_tensor:0'は、上記のsaved_model_cliの出力から拾って試行錯誤して見つけた。

最後に、同じくpredictorを使わず、TensorFlow APIだけを使って、"(NHWC)"[1]についてやってみた。

●コード例4
import numpy as np
from PIL import Image
import tensorflow as tf

img_path = 'elephant.jpg'
img = Image.open(img_path).resize((224,224))
x = np.array(img)
x = x.astype(np.float32)

with tf.Session(graph=tf.Graph()) as sess:
    tf.saved_model.loader.load(sess, [tf.saved_model.tag_constants.SERVING], "./resnet_v2_fp32_savedmodel_NHWC/1538687283/")
    class_tensor = sess.graph.get_tensor_by_name('ArgMax:0')
    prob_tensor = sess.graph.get_tensor_by_name('softmax_tensor:0')
    classess, probabilities = sess.run([class_tensor, prob_tensor], {'input_tensor:0': np.array([x] * 64)})

cls = classes[0]
prob = probabilities[0, cls]
print('class={} probability={:.3f}'.format(cls, prob))
●出力例4
class=386 probability=0.821

前回、TensorFlow Liteのモデルを使ってもTensorFlowのモデルと比べて全く速くならなかったのは、ひょっとしてTensorFlowが内部でuint8をfloat32に変換して処理してるとか、TensorFlowモデルと同じくらいのメモリを使うのでキャッシュのヒット率が上がらないとかが原因で、ニューラルネットワークの実行環境としてTensorFlowでなくTensorFlow Liteを使えば速くなるのではないかと思って、やってみたら、Inception_V3_quantのtfliteモデルの1フレーム当たりの処理時間が、シングルコア(1スレッド実行)で約4.2秒(Raspberry Pi 2 v1.2(Cortex A53)、前回のTensorFlowだと約4.7秒)だった。使用したコードは後述のコードとほとんど同じなので省略する。少し速くなったが、少なくとも、TensorFlow Liteでもfloat32をuint8にすれば2倍とかのレベルで計算が速くなるという訳では無いようだ。
また、マルチコア(4スレッド実行)にすると約1.7秒だった。4コアの並列実行でCPU負荷が3.5倍になっても、2.4倍しか速くなっていない。単純なモデルの変換や実行環境の変更による高速化はこれくらいが限界なのだろうか。

とりあえず、前回と同じく、TensorFlow LiteのガイドのObject Detectionのstarter modelを動かしてみたので、やったことを記録する。

●TensorFlow Liteのビルド
TensorFlow公式のBuild TensorFlow Lite for Raspberry Piのページに従って、C++ static libraryをビルドした。
(今日見ると"install just the Python interpreter API"というリンクがあるが、筆者が先月見た時は無かったか、気付かなかったので、C++用のlibtensorflow-lite.aを作るしかないと思った。)

git clone https://github.com/tensorflow/tensorflow
cd tensorflow
git checkout v1.13.1
sudo apt-get install crossbuild-essential-armhf
./tensorflow/lite/tools/make/download_dependencies.sh
./tensorflow/lite/tools/make/build_rpi_lib.sh
なお、スワップ領域が100MBだとメモリ不足で失敗したので、1GB追加した。
sudo su -
dd if=/dev/zero of=/swapfile bs=1024 count=1048576
mkswap /swapfile 
chmod 0600 /swapfile 
swapon /swapfile

●ヘッダファイルとライブラリのインストール
/home/pi/tensorflow-lite/にインストールすることにした。

mkdir -p /home/pi/tensorflow-lite/include/tensorflow/lite
cd tensorflow/lite/
cp --parents $(find . -name "*.h*") /home/pi/tensorflow-lite/include/tensorflow/lite
cd ../..
cp -r tensorflow/lite/tools/make/downloads/flatbuffers/include/flatbuffers /home/pi/tensorflow-lite/include/
mkdir /home/pi/tensorflow-lite/lib
cp tensorflow/lite/tools/make/gen/rpi_armv7l/lib/libtensorflow-lite.a /home/pi/tensorflow-lite/lib/

●今回作ったC++のプログラム(main.cc)

#include <iostream>
#include <fstream>
#include <sstream>
#include <vector>
#include <opencv2/opencv.hpp>
#include <cstdint>

#include <tensorflow/lite/model.h>
#include <tensorflow/lite/interpreter.h>
#include <tensorflow/lite/kernels/register.h>

const char* WINNAME = "Capture";
const int CAPTURE_WIDTH = 1280;
const int CAPTURE_HEIGHT = 720;
//const int CAPTURE_WIDTH = 640;
//const int CAPTURE_HEIGHT = 480;
const int DISPLAY_WIDTH = 480;
const int DISPLAY_HEIGHT = 480;
const int colors[10][3] = {
  {255, 255, 0}, {0, 255, 255}, {128, 256, 128}, {64, 192, 255}, {128, 128, 255},
  {255, 255, 0}, {0, 255, 255}, {128, 256, 128}, {64, 192, 255}, {128, 128, 255}};

int main(int argc, char *argv[])
{
  const char *model_file = "coco_ssd_mobilenet_v1_1.0_quant/detect.tflite";
  const char *label_file = "coco_ssd_mobilenet_v1_1.0_quant/labelmap.txt";

  // TensorFlow Lite things
  std::unique_ptr<tflite::FlatBufferModel> model =
    tflite::FlatBufferModel::BuildFromFile(model_file);
  if (!model) {
    std::cerr << "FlatBufferModel::BuildFromFile(\"" << model_file << "\") failed." << std::endl;
    return -1;
  }

  tflite::ops::builtin::BuiltinOpResolver resolver;
  std::unique_ptr<tflite::Interpreter> interpreter;
  tflite::InterpreterBuilder(*model, resolver)(&interpreter);

  interpreter->AllocateTensors();
  interpreter->SetNumThreads(4);

  // Read class labels
  std::ifstream ifs(label_file);
  if (!ifs) {
    std::cerr << "ifstream(\"" << label_file << "\") failed." << std::endl;
    return -1;
  }
  std::vector<std::string> class_names;
  std::string line;
  while (std::getline(ifs, line)) {
    class_names.push_back(line);
  }

  // Camera settings
  cv::VideoCapture cap;
  while (!cap.isOpened()) {
    cap.open(0);
  }
  std::cout << "Camera is opened." << std::endl;
  cap.set(CV_CAP_PROP_FRAME_WIDTH, CAPTURE_WIDTH);
  cap.set(CV_CAP_PROP_FRAME_HEIGHT, CAPTURE_HEIGHT);

  // Main loop
  int key;
  do {
    cv::Mat img;
    cap >> img;

    // Crop center square
    img = img(cv::Rect(
      (CAPTURE_WIDTH - CAPTURE_HEIGHT) / 2,
      0,
      CAPTURE_HEIGHT,
      CAPTURE_HEIGHT
    ));

    // Make resized image for input
    cv::Mat X(300, 300, img.type());
    cv::resize(img, X, X.size(), cv::INTER_CUBIC);

    // Copy X to input_tensor
    uint8_t *input_tensor = interpreter->typed_tensor<uint8_t>(interpreter->inputs()[0]);
    int i = 0;
    for (int y = 0; y < 300; y++) {
      for (int x = 0; x < 300; x++) {
        input_tensor[i++] = X.data[y * X.step + x * 3 + 2]; //BGR->RGB
        input_tensor[i++] = X.data[y * X.step + x * 3 + 1];
        input_tensor[i++] = X.data[y * X.step + x * 3 + 0];
      }
    }

    // Execute inference
    interpreter->Invoke();

    // Get result
    float *result1 = interpreter->typed_output_tensor<float>(0); //Locations (Top, Left, Bottom, Right)
    float *result2 = interpreter->typed_output_tensor<float>(1); //Classes (0=Person)
    float *result3 = interpreter->typed_output_tensor<float>(2); //Scores
    float *result4 = interpreter->typed_output_tensor<float>(3); //Number of detections

    // Draw result
    cv::resize(img, img, cv::Size(DISPLAY_WIDTH, DISPLAY_HEIGHT), cv::INTER_CUBIC);
    for (int i = result4[0] - 1; i >= 0; i--) {
      int top    = result1[10*i + 0] * 300;
      int left   = result1[10*i + 1] * 300;
      int bottom = result1[10*i + 2] * 300;
      int right  = result1[10*i + 3] * 300;
      #define SWAP(X,Y) {(X)+=(Y); (Y)=(X)-(Y); (X)-=(Y);}
      if (left > right) SWAP(left, right);
      if (top > bottom) SWAP(top, bottom);
      std::string class_name = class_names[result2[i]+1];
      float score = result3[i];
      if (score < 0.5) continue;
      std::cout << "Location=(" << left << "," << top << ")-(" << right << "," << bottom << "), ";
      std::cout << "Class=" << class_name << ", ";
      std::cout << "Score=" << score << ", ";
      std::cout << std::endl;
      left = left * DISPLAY_WIDTH / 300;
      right = right * DISPLAY_WIDTH / 300;
      top = top * DISPLAY_HEIGHT / 300;
      bottom = bottom * DISPLAY_HEIGHT / 300;
      cv::rectangle(img, cv::Point(left, top), cv::Point(right, bottom), cv::Scalar(colors[i][0], colors[i][1], colors[i][2]), 1);
      cv::rectangle(img, cv::Point(left, top+20), cv::Point(left+160, top), cv::Scalar(colors[i][0], colors[i][1], colors[i][2]), CV_FILLED);
      cv::putText(img, class_name + " (" + std::to_string(score).substr(0, 5) + ")",
        cv::Point(left, top+15), cv::FONT_HERSHEY_SIMPLEX, 0.5, cv::Scalar(0, 0, 0));
    }

    cv::imshow(WINNAME, img);
    cv::moveWindow(WINNAME, 0, 0);
    key = cv::waitKey(1);
    if (key == 's')
      cv::imwrite("result.jpg", img);

  } while (key != 'q');

  cv::destroyAllWindows();
  cap.release();
  return 0;
}
参考URL
TensorFlow Lite公式ガイドのAPI解説
https://www.tensorflow.org/lite/guide/inference
TensorFlow Lite C++ APIリファレンス
https://www.tensorflow.org/lite/api_docs/cc/namespace/tflite

●Makefile

all: main

TENSORFLOW_LITE_INCLUDE_DIR = $(HOME)/tensorflow-lite/include
TENSORFLOW_LITE_LIB_DIR = $(HOME)/tensorflow-lite/lib

main: main.o
	g++ -o main  -L$(TENSORFLOW_LITE_LIB_DIR) `pkg-config --libs opencv` main.o -ltensorflow-lite -lpthread -ldl

main.o: main.cc
	g++ -I$(TENSORFLOW_LITE_INCLUDE_DIR) `pkg-config --cflags opencv` -c main.cc

clean:
	rm -f main main.o

test: main
	./main

●実行結果
前回と同じなので省略

前回、Kerasを使ってTensorFlowの学習済みモデルをRaspberry Piで動かすとあまりにも遅かったので、次はTensorFlow Liteでやってみようと思っていた。それでTensorFlow LiteのガイドのHosted modelsにあるInception_V3_quantのtfliteモデルをがんばって動かしてみたら、何と1フレーム当たりの処理時間は、TensorFlowのモデルと全く変わらなかった(Raspberry Pi 2 v1.2(Cortex A53)で約4.7秒)。使用したコードは後述のコードとほとんど同じなので省略する。float32の演算がint8の演算に変わるんだから当然速くなるだろうと思っていたが、今時のCPUはfloat32とint8の処理時間が同じなのか。
それでも、起動が早い(TensorFlow Liteモデルのロードまでで15秒)し、メモリ負荷が小さくてメモリ不足で落ちることが無い点については、非常に軽いのを実感した。

折角TensorFlow Liteの動かし方を確保したので、TensorFlow LiteのガイドのObject Detectionのstarter modelを動かしてみる。

●コード
import numpy as np
import tensorflow as tf
import cv2

interpreter = tf.lite.Interpreter(model_path="coco_ssd_mobilenet_v1_1.0_quant/detect.tflite")
interpreter.allocate_tensors()

input_details = interpreter.get_input_details()
output_details = interpreter.get_output_details()

with open('coco_ssd_mobilenet_v1_1.0_quant/labelmap.txt', 'r') as F:
    class_names = F.readlines()


WINNAME = "Capture"
FRAME_INTERVAL = 30 # msec

CAPTURE_WIDTH = 1280
CAPTURE_HEIGHT = 720
CENTER_CROP_X1 = int((CAPTURE_WIDTH - CAPTURE_HEIGHT) / 2)
CENTER_CROP_X2 = CAPTURE_WIDTH - CENTER_CROP_X1
DISPLAY_WIDTH = 480
DISPLAY_HEIGHT = 480
colors = ((255, 255, 0), (0, 255, 255), (128, 256, 128), (64, 192, 255), (128, 128, 255)) * 2

ret = False
while ret == False:
    cap = cv2.VideoCapture(0)
    cap.set(cv2.cv.CV_CAP_PROP_FRAME_WIDTH, CAPTURE_WIDTH)
    cap.set(cv2.cv.CV_CAP_PROP_FRAME_HEIGHT, CAPTURE_HEIGHT)
    ret, img = cap.read()

key = 0
while key != ord('q'):
    ret, img = cap.read()
    img = img[:, CENTER_CROP_X1:CENTER_CROP_X2]  # crop center square
    x = cv2.resize(img, (300, 300))  # input size of coco ssd mobilenet?
    x = x[:, :, [2,1,0]]  # BGR -> RGB
    x = np.expand_dims(x, axis=0)

    interpreter.set_tensor(input_details[0]['index'], x)
    interpreter.invoke()

    tflite_results1 = interpreter.get_tensor(output_details[0]['index'])  # Locations (Top, Left, Bottom, Right)
    tflite_results2 = interpreter.get_tensor(output_details[1]['index'])  # Classes (0=Person)
    tflite_results3 = interpreter.get_tensor(output_details[2]['index'])  # Scores
    tflite_results4 = interpreter.get_tensor(output_details[3]['index'])  # Number of detections

    img = cv2.resize(img, (DISPLAY_WIDTH, DISPLAY_HEIGHT))
    for i in range(int(tflite_results4[0])):
        (top, left, bottom, right) = tflite_results1[0, i] * 300
        class_name = class_names[tflite_results2[0, i].astype(int) + 1].rstrip()
        prob = tflite_results3[0, i]
        if prob >= 0.5:
            print("Location=({},{})-({},{})".format(int(left), int(top), int(right), int(bottom)))
            print("Class={}".format(class_name))
            print("Probability={}".format(prob))
            left = int(left * DISPLAY_WIDTH / 300)
            right = int(right * DISPLAY_WIDTH / 300)
            top =  int(top * DISPLAY_HEIGHT / 300)
            bottom = int(bottom * DISPLAY_HEIGHT / 300)
            cv2.rectangle(img, (left, top), (right, bottom), colors[i], 1)
            cv2.rectangle(img, (left, top+20), (left+160, top), colors[i], cv2.cv.CV_FILLED)
            cv2.putText(img, "{} ({:.3f})".format(class_name, prob),
                        (left, top+15), cv2.cv.CV_FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 0))
    cv2.imshow(WINNAME, img)
    cv2.moveWindow(WINNAME, 0, 0)

    key = cv2.waitKey(FRAME_INTERVAL)
    if key == ord('s'):
        cv2.imwrite('result.jpg', img)

cap.release()
cv2.destroyAllWindows()

●結果(Raspberry Pi 2 + Webcam C270で実施)

このモデルには"penguin"の識別クラスが無いので、"bird"で正解である。

確信度が高い検出結果の枠とラベルが前になるよう、確信度が低い枠とラベルから順に描画するようにすれば良かった。

なお、TensorFlow公式サイトに記載されている手順でインストールしたTensorFlowだと、tf.lite.Interpreter()の呼び出しで次のようなエラーになった。

_tensorflow_wrap_interpreter_wrapper.so: undefined symbol: _ZN6tflite12tensor_utils24NeonVectorScalarMultiplyEPKaifPf

https://github.com/tensorflow/tensorflow/issues/21855を読むと、この問題は結構前からあるのにリリースを重ねても解決されていないようだ。筆者の環境では、同ページに紹介されているhttps://github.com/lhelontra/tensorflow-on-arm/releases/にあるtensorflow-1.13.1-cp27-none-linux_armv7l.whlをインストールすると、上記のエラーが解消した。

Deep Learningで画像認識してみようと、手持ちのRaspberry Pi 2にUSBカメラを接続してから半年以上、進捗が無いので、とにかく何か動かしてみる。

現在Deep Learningのプラットフォームとして最もメジャーなTensorFlowが公式にRaspberry Piをサポートしているので、まずはTensorFlowを使うことにする。
TensorFlowはpipでインストールできる。筆者のRaspberry Piでは機械学習関係のPython環境はPython 2.7で揃えており、しかしながらPython 3のpipもインストールしており、pipをアップデートするとPython 3のpipが壊れるので、virtualenvを使用して、Python 2.7用のTensorFlowをインストールする。

公式ページの手順に従って、

virtualenv --system-site-packages -p python2.7 ./venv
source ./venv/bin/activate
pip install --upgrade pip
pip install --upgrade tensorflow
とすると、.whlファイルのダウンロードが途中で失敗し、
THESE PACKAGES DO NOT MATCH THE HASHES FROM THE REQUIREMENTS FILE. If you have updated the package versions, please update the hashes. Otherwise, examine the package contents carefully; someone may have tampered with them.
というエラーになった。何度やっても、 pip install --no-cache-dir tensorflowとやっても、 pip install tensorflow==1.12などとバージョンを変えても、 時間帯を変えてやっても日を改めても駄目で、他に適当な方法が見つからなかったので、ダウンロードに失敗する.whlをwgetで取得してインストールした。
wget -t 0 https://www.piwheels.org/simple/tensorflow/tensorflow-1.13.1-cp27-none-linux_armv7l.whl
pip install tensorflow-1.13.1-cp27-none-linux_armv7l.whl 

今回はKeras DocumentationのApplicationのページを参考に、ImageNetで学習済みのモデルで画像認識させる。
同ページの"Classify ImageNet classes with ResNet50"の所にあるコードと、以前に作成したUSBカメラを動かすコードを組み合わせ、ResNet50をより少し小さく少し認識精度が高いとされるInceptionV3に変えて、次のコードにすると、無事に動いた。

from tensorflow.keras.applications.inception_v3 import InceptionV3
from tensorflow.keras.preprocessing import image
from tensorflow.keras.applications.inception_v3 import preprocess_input, decode_predictions
import numpy as np
import cv2

model = InceptionV3(weights='imagenet')

WINNAME = "Capture"
FRAME_INTERVAL = 30 # msec

CAPTURE_WIDTH = 1280
CAPTURE_HEIGHT = 720
CENTER_CROP_X1 = int((CAPTURE_WIDTH - CAPTURE_HEIGHT) / 2)
CENTER_CROP_X2 = CAPTURE_WIDTH - CENTER_CROP_X1
DISPLAY_WIDTH = 480
DISPLAY_HEIGHT = 480

ret = False
while ret == False:
    cap = cv2.VideoCapture(0)
    cap.set(cv2.cv.CV_CAP_PROP_FRAME_WIDTH, CAPTURE_WIDTH)
    cap.set(cv2.cv.CV_CAP_PROP_FRAME_HEIGHT, CAPTURE_HEIGHT)
    ret, img = cap.read()

key = 0
while key != ord('q'):
    ret, img = cap.read()
    img = img[:, CENTER_CROP_X1:CENTER_CROP_X2]  # crop center square
    cv2.moveWindow(WINNAME, 0, 0)
    x = cv2.resize(img, (299, 299))  # input size of InceptionV3
    x = image.img_to_array(x)
    x = x[:, :, [2,1,0]]  # BGR -> RGB
    x = np.expand_dims(x, axis=0)
    x = preprocess_input(x)

    preds = model.predict(x)
    preds = decode_predictions(preds, top=3)[0]

    img = cv2.resize(img, (DISPLAY_WIDTH, DISPLAY_HEIGHT))
    for i in range(3):
        _, class_name, accuracy = preds[i]
        if accuracy >= 0.2:
            print("{} ({:.3f})".format(class_name, accuracy))
            cv2.rectangle(img, (0, 20*(i+1)), (200, 20*i), (255, 255, 0), cv2.cv.CV_FILLED)
            cv2.putText(img, "{} ({:.3f})".format(class_name, accuracy),
                        (0, 20*(i+1)-5), cv2.cv.CV_FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 0))
    cv2.imshow(WINNAME, img)

    key = cv2.waitKey(FRAME_INTERVAL)
    if key == ord('s'):
        cv2.imwrite('result.jpg', img)

cap.release()
cv2.destroyAllWindows()

●結果(Raspberry Pi 2 + Webcam C270で実施)


遅いだろうとは思っていたが、2011年製のMacBook Air(Intel Core i5 1.6GHz)でも起動に30秒、画像認識に1フレーム当たり0.5秒くらいかかるが、Raspberry Pi 2(v1.2)だと起動に3分、画像認識5秒くらいかかる。

この間、Pandasの勉強をしていると、PandasのDataFrameクラスにはpivotとpivot_tableという2つのメソッドがあることに気付かず、違う方を使って自分で打ち込んだサンプルコードが動かなかったり、helpを読んで混乱することが度々あった。
pivotとpivot_tableが違うことに気付いた時にあまりにも悔しかったので、pivotとpivot_tableの違いに関してメモしておく。

以下、Jupyter Notebookで書いたコードと出力を使って記述する。

In [1]:
df = pd.DataFrame({'date': ['{}:{:02d}:{:02d}'.format(6, 30, s) for s in (2, 2, 2, 3, 3, 3, 4, 4, 4)],
                    'variable': ['A', 'B', 'C'] * 3,
                    'value': list('abcdefghi')})

df
Out [1]:
date variable value
0 6:30:02 A a
1 6:30:02 B b
2 6:30:02 C c
3 6:30:03 A d
4 6:30:03 B e
5 6:30:03 C f
6 6:30:04 A g
7 6:30:04 B h
8 6:30:04 C i

こういう、いわゆるlongフォーマットの時系列データを以下のwideフォーマットに変形することを考える。(筆者は最近実際にやることがあった)

In [2]:
df.pivot(index='date', columns='variable', values='value')
Out [2]:
variable A B C
date
6:30:02 a b c
6:30:03 d e f
6:30:04 g h i

上ではpivotを使ったが、pivot_tableを使っても同じことができる。
但し、pivot_tableはグループ毎(2次元の表にまとめられたセル毎)に集約関数が適用され、defaultの関数はnp.sumで、この例のようにセルの値が文字列だと適用できず、"DataError: No numeric types to aggregate"というエラーになる為、何か文字列を受け取れる関数を指定する必要がある。
(下記のようにaggfunc='first'とすると、DataFrameGroupBy.firstが用いられると思われる)

In [3]:
df.pivot_table(index='date', columns='variable', values='value', aggfunc='first')
Out [3]:
variable A B C
date
6:30:02 a b c
6:30:03 d e f
6:30:04 g h i

このように、pivot_tableはグループ毎に必ず集約関数が使われるのが、pivotとの主な違いのようである。
pivotでは集約ができないので、次のように結果が同じ行と同じ列に相当する行があると、それらが全く同じ値であってもエラーになる。(筆者は実際にそういうデータに遭遇して難儀した)

In [4]:
df = df.append(pd.DataFrame({'date': ['{}:{:02d}:{:02d}'.format(6, 30, s) for s in (6, 6, 6) * 2],
                    'variable': ['A', 'B', 'C'] * 2,
                    'value': list('jkl') * 2}))
df
Out [4]:
date variable value
0 6:30:02 A a
1 6:30:02 B b
2 6:30:02 C c
3 6:30:03 A d
4 6:30:03 B e
5 6:30:03 C f
6 6:30:04 A g
7 6:30:04 B h
8 6:30:04 C i
0 6:30:06 A j
1 6:30:06 B k
2 6:30:06 C l
3 6:30:06 A j
4 6:30:06 B k
5 6:30:06 C l
In [5]:
df.pivot(index='date', columns='variable', values='value')
Out [5]:
ValueError: Index contains duplicate entries, cannot reshape

pivot_tableであれば、この場合も成功する。

In [6]:
df.pivot_table(values='value', index='date', columns='variable', aggfunc='first')
Out [6]:
variable A B C
date
6:30:02 a b c
6:30:03 d e f
6:30:04 g h i
6:30:06 j k l

この間、Pandasを使った教材を流し読みしていると、DataFrameクラスのmeltというメソッドが出てきて、pivotの逆だと説明されていた。それを読んで、meltされたものをpivotして元に戻してみよう思って、すきま時間を集めてのべ1時間くらいがんばったが、できなかった。
ついでに、meltとstackの違いがわからなかった。

今月、1時間くらい連続してがんばったら成功して、meltとstackの違いも理解できたので、ここに控えておく。

以下、コード例はJupyter Notebookで書いたインタラクティブシェル向けの形式のものをそのまま貼り付けており、出力例はJupyter Notebookの出力を加工して作成している。

In [1]:
import pandas as pd
df = pd.DataFrame({'月': ['1月', '2月', '3月'],
                   '京都': [110, 115, 144],
                   '大阪': [263, 283, 309],
                   '奈良': [12, 13, 21],
                  })
df
Out [1]:
京都 大阪 奈良
0 1月 110 263 12
1 2月 115 283 13
2 3月 144 309 21

このようなDataFrameがあるとして、これをmeltして、pivotで元に戻してみる。

In [2]:
df_melted = df.melt(id_vars='月', var_name='地域', value_name='人数')
df_melted
Out [2]:
地域 人数
0 1月 京都 110
1 2月 京都 115
2 3月 京都 144
3 1月 大阪 263
4 2月 大阪 283
5 3月 大阪 309
6 1月 奈良 12
7 2月 奈良 13
8 3月 奈良 21
In [3]:
df_pivoted = df_melted.pivot(index='月', columns='地域', values='人数')
df_pivoted
Out [3]:
地域 京都 大阪 奈良
1月 110 263 12
2月 115 283 13
3月 144 309 21

meltしたものをpivotすると大体戻ったが、「月」がindexになっているのと、meltで加えた「地域」が残っているのが異なるので、修正する。
(ちなみに、reset_indexしただけでは「月」が「地域」に入っておかしなことになる)

In [4]:
df_pivoted = df_pivoted.reset_index()
df_pivoted.columns.name = None
df_pivoted
Out [4]:
京都 大阪 奈良
0 1月 110 263 12
1 2月 115 283 13
2 3月 144 309 21

次に、meltとstackの違いを見てみる。
上のdfをstackすると「月」と地域名が同列に処理されてしまうので、まず「月」をインデックスにしてからstackする。

In [5]:
df2 = df.set_index('月')
df2
Out [5]:
京都 大阪 奈良
1月 110 263 12
2月 115 283 13
3月 144 309 21

この場合はstackすると結果が1列なのでDataFrameでなくSeriesになるので、変数名の先頭をdf_でなくsr_にしている。

In [6]:
sr_stacked = df2.stack()
sr_stacked
Out [6]:
1月 京都 110
大阪 263
奈良 12
2月 京都 115
大阪 283
奈良 13
3月 京都 144
大阪 309
奈良 21

当然ながら、stackしたものをunstackすると元に戻る。

In [7]:
sr_stacked.unstack()
Out [7]:
京都 大阪 奈良
1月 110 263 12
2月 115 283 13
3月 144 309 21

meltでは元々column名だった列(「地域」列)がindexにならなかったが、stackではindexになっている。つまり、meltはcolumn名を新たに加えた通常の列に展開することによって表を変形し、stackはcolumn名をindexの新たな階層に展開することによって表を変形する。このことがmeltとstackの主な違いのようだ。
次のようにして、stackしたもののindexを通常の列に戻すと、meltした結果(Out [2])と同じになる。

In [8]:
df_stacked = sr_stacked.sort_index(level=1).reset_index()
df_stacked.columns = ['月', '地域', '人数']
df_stacked
Out [8]:
地域 人数
0 1月 京都 110
1 2月 京都 115
2 3月 京都 144
3 1月 大阪 263
4 2月 大阪 283
5 3月 大阪 309
6 1月 奈良 12
7 2月 奈良 13
8 3月 奈良 21

ピボットテーブルを理解した

15年くらい前にExcelのピボットテーブルを使ってみて以来ずっと、ピボットテーブルとは何なのかが理解できなかった。2年くらい前にも、ピボットテーブルを初心者向けに詳しく解説したITProの記事を読んで、今度こそ理解しようと意気込んで、何も見ずに演習問題を解けるようにもなったが、結局「ピボットテーブル」の意味を理解できず、ただ演習問題の解き方を丸暗記したような感じで終わってしまった。
筆者にとってピボットテーブルは、わかりそうなのにわからない、何故分からないのかがわからない、まるで眼の盲点のように、筆者の脳はそれが理解できない作りになっているかのように思えるものだった。

最近、久々にPandasを勉強する機会があって、ピボットテーブルが出てきて、また理解できなかった。しかし、何回かgroupbyの練習をした後、ふと、ピボットテーブルってgroupbyと似てるなと思い、Webで調べたらピボットテーブルはGroupByを2次元にしたようなものというような説明が見つかって、遂に理解に成功した。

GroupByの動作は、split-apply-combineと言われるように、データをキーによって分割し、分割した単位で何らかの処理を適用し、結合して1つのデータにするものである。
次の図は、データをkey列の値によって分割し、分割された組毎に合計値を計算して1つの値に「集約(aggregate)」し、1つのデータにまとめる、というGroupByの処理の例を表している。

●コード例
import pandas as pd
df = pd.DataFrame({
    'key': ['A', 'B', 'C'] * 3,
    'val': range(1, 10)})
print(df)
print(df.groupby('key').aggregate(sum))
●実行結果
  key  val
0   A    1
1   B    2
2   C    3
3   A    4
4   B    5
5   C    6
6   A    7
7   B    8
8   C    9
     val
key     
A     12
B     15
C     18

Applyは集約に限らず、単に表を変形したり、グループ毎の値を使って値を変換することなども含まれるが、グループ毎に集約するのがGroupByやピボットテーブルの醍醐味だと思うので、この記事の例では集約にしている。

ピボットテーブルは、split-apply-combineを行方向と列方向に同時に行うものと捉えることができる。
次の図は、データをkey1列の値によって行方向に、key2列の値によって列方向に分割し、同様に分割毎に合計値に集約し、1つのデータに結合する、というピボットテーブルの処理の例を表している。

●コード例(pivot_tableメソッド使用)
import pandas as pd
df = pd.DataFrame({
    'key1': ['A', 'B'] * 6,
    'key2': ['①', '②', '③'] * 4,
    'val': range(1, 13)})
print(df)
print(df.pivot_table(index='key1', columns='key2', values='val', aggfunc='sum'))
●実行結果
   key1 key2  val
0     A    ①    1
1     B    ②    2
2     A    ③    3
3     B    ①    4
4     A    ②    5
5     B    ③    6
6     A    ①    7
7     B    ②    8
8     A    ③    9
9     B    ①   10
10    A    ②   11
11    B    ③   12
key2   ①   ②   ③
key1            
A      8  16  12
B     14  10  18

ピボットテーブルはGroupByを2次元にしたようなもの、ということを確認する為に、pivot_tableの代わりにgroupbyを2つ使って同じ結果にしてみる。

●コード例(groupbyメソッドを2つ使用)
print(df.groupby('key1').apply(lambda x: x.groupby('key2')['val'].apply(sum)))
●実行結果
key2   ①   ②   ③
key1            
A      8  16  12
B     14  10  18

aptでpython-sklearnをインストールして、pythonで

from sklearn.neural_network import MLPClassifier

すると、見つからないというエラーになった。
Raspbian 8.0 (jessie)の現在最新のaptのpython-sklearnのバージョンは0.14.1-3だが、sklearn.neural_network.MLPClassifierはscikit-learn 0.18以降にしか無いようだ。

そこで、pipで新しいバージョンのscikit-learnをインストールしようとして、

sudo pip install --upgrade pip

するとpip3が壊れたりしたので、
sudo pip uninstall pip
sudo apt install --reinstall python-pip python3-pip

として修復し、virtualenvを使うことにした。

aptでpython-virtualenvをインストールして、

virtualenv (workdir)
cd (workdir)
. bin/activate
pip install --upgrade pip
pip install scikit-learn

とすると、scipyのmake中にblasやlapackが無いというエラーになった。
https://scikit-learn.org/stable/developers/advanced_installation.html
によると、scikit-learnのインストールにはlibatlas-devとlibatlas3-baseが必要とのことだが、scipyのmakeには他に、liblapack-devか、もしくはatlasに合わせるならlibatlas-base-devが必要のようだ(どちらでも成功した)。

aptでlibatlas-dev, libatlas3-base, libatlas-base-devをインストールして、改めて

pip install scikit-learn

すると、数十分後にRaspberry Pi 2からの応答が無くなった。scipyのmake中にメモリのスワップ領域が枯渇するようだった。

スワップ領域を1GB追加しても不足したので、2GB追加した。

su -
dd if=/dev/zero of=/swapfile bs=1024 count=2097152
chmod 0600 /swapfile
mkswap /swapfile
swapon /swapfile

これによって、数時間かかるが、scikit-learnのインストールができるようだ。

ただ、時間がかかるし、makeが失敗して時間を取られるリスクがあるので、やっぱりaptでインストールできるものはaptでインストールすることにした。MLPClassifierと合わせてmatplotlibを使いたいが、もはやpipでmatplotlibをインストールする気にはならない。
現時点で手っ取り早くscikit-learn 0.20.xをインストールするには、以下のようにすれば良いと思う。

sudo apt install python-numpy python-scipy python-virtualenv libatlas-dev libatlas3-base libatlas-base-dev
virtualenv --system-site-packages (dir)
cd (dir)
. bin/activate
pip install --upgrade pip
pip install scikit-learn

※(dir)は任意のディレクトリ名

Raspberry Pi 2にUSBカメラを接続

昨年辺りから、Raspberry Piにカメラを付けて、Deep Learningさせたニューラルネットワークを使って物体認識させるのが流行ってるというか、今年の夏にTensorFlowが公式にRaspberry Piをサポートしたことで、定番から基本になりつつあるようだ。
近い内にAIを勉強しようと思っており(思い始めてからだいぶ経つが)、かつ、Raspberry Piを所有している身としては、一度やっておかないといけないと思っていた。

しかし、Raspberry Piのカメラモジュールは4,000円くらいするので、二の足を踏んでいた。筆者は貧乏性で、確実に元が取れると思えない限りはお金を使わない、よくそれで損をするタイプなのである。
そんな折、昔Skype英会話用に買ったUSBカメラが10回くらい使っただけで眠っているのを思い出した。2010年に買った、Logicool HD Webcam C270というやつである。思い出してからも、どうせ消費電力的に無理だろうと思っていたが、先月ふと思い立って繋いだら動いて、今の所特に電源異常は起こっていない。少し調べた限りでは、このカメラの消費電力が1.0WくらいでRaspberry Pi 2の消費電力が最大4.5W、使用しているACアダプターの出力が9.0Wなので、大丈夫のようだ。

PythonとCでOpenCVを使って動かしたが、安定的に動かすまでには結構試行錯誤したので、それなりに動くようになったコードを貼っておく。

●Pythonのサンプルコード

#!/usr/bin/python
import cv2

WINNAME = "Capture"
FRAME_INTERVAL = 30 # msec

CAPTURE_WIDTH = 1280
CAPTURE_HEIGHT = 720

ret = False
while ret == False:
	cap = cv2.VideoCapture(0)
	cap.set(cv2.cv.CV_CAP_PROP_FRAME_WIDTH, CAPTURE_WIDTH)
	cap.set(cv2.cv.CV_CAP_PROP_FRAME_HEIGHT, CAPTURE_HEIGHT)
	ret, img = cap.read()

key = 0
while key != ord('q'):
	ret, img = cap.read()
	cv2.imshow(WINNAME, img)
	cv2.moveWindow(WINNAME, 0, 0)
	key = cv2.waitKey(FRAME_INTERVAL)
	if key == ord('s'):
		cv2.imwrite('capture.jpg', img)

cap.release()
cv2.destroyAllWindows()

実行する前に、

sudo apt install python-opencv
などとしてaptでpython-opencvをインストールしておく。

なお、Python3でも動かそうと思ったが、aptにpython3-opencvが無く、代わりにaptでpython3-pipをインストールして

sudo pip3 install opencv-python
とすれば良いと思うのだが、下のようなエラーになったので、今回は諦めた。
(pip3 install --upgrade pipとしてpip3を最新にしても同じ結果だった)
Could not find any downloads that satisfy the requirement opencv-python
Cleaning up...
No distributions at all found for opencv-python

●Cのサンプルコード

#include <cv.h>
#include <highgui.h>

#define WINNAME "Capture"
#define FRAME_INTERVAL 30 /* msec. */

#define CAPTURE_WIDTH 1280
#define CAPTURE_HEIGHT 720

int main(int argc, char *argv[])
{
	CvCapture *capture = NULL;
	IplImage *frame = NULL;
	int key;

	while (!capture)
	    capture = cvCreateCameraCapture(0);

	cvSetCaptureProperty(capture, CV_CAP_PROP_FRAME_WIDTH, CAPTURE_WIDTH);
	cvSetCaptureProperty(capture, CV_CAP_PROP_FRAME_HEIGHT, CAPTURE_HEIGHT);

	cvNamedWindow(WINNAME, CV_WINDOW_AUTOSIZE);
	cvMoveWindow(WINNAME, 0, 0);

	do {
		frame = cvQueryFrame(capture);
		cvShowImage(WINNAME, frame);
		key = cvWaitKey(FRAME_INTERVAL);
		if (key == 's')
			cvSaveImage("capture.jpg", frame, NULL);
	} while (key != 'q');

	cvDestroyWindow (WINNAME);
	cvReleaseCapture(&capture);
	return 0;
}

コンパイルする前に、

sudo apt install libopencv-dev
などとしてaptでlibopencv-devをインストールしておく。

コンパイルは、次のコマンドで行った。

gcc camera_test.c `pkg-config --cflags --libs opencv` -lm -o camera_test

いずれも、sを押すとカメラ画像がcapture.jpgに保存され、qを押すと終了する。