第８回　Raspberry Pi で監視カメラを作ろう！　～静止画編～

Raspberry Piと専用のカメラモジュールを使用し、ONVIF対応の監視カメラを作成する[シリーズ記事](https://qiita.com/lis-hanzomon/items/cc1f7f316fbeb9579b28)です。
本記事は[リンク情報システム株式会社](https://www.lis.co.jp/)の有志が作成しています。

---
前回の記事では監視カメラ映像配信アプリのビルド方法について説明したので、今回からはプログラムの中からポイントとなる場所を解説していきます。

#静止画撮影（カメラ制御）
###カメラのセットアップ
事前にraspi-configでカメラモジュールを有効化し、再起動しておきます。
[第４回で設定済み](https://qiita.com/lis-hanzomon/items/917c199695caba15e144)です。

##MMAL (Multi-Media Abstraction Layer) を使って静止画撮影
Raspbery Piには、 MMAL というライブラリがあり、これを使用して、カメラモジュールから静止画や動画のデータを取得する事ができます。
また、第４回で静止画取得に使った raspistill も MMAL を使用して静止画を取得しています。
そこで、raspistillのソースファイル RaspiStill.c を元にポイントとなる処理を抜粋して解説します。

参考元：[userland/RaspiStill.c at master · raspberrypi/userland · GitHub](https://github.com/raspberrypi/userland/blob/master/host_applications/linux/apps/raspicam/RaspiStill.c)

静止画取得処理の流れは以下のようになります。
　1.カメラ設定
　2.エンコーダー設定
　3.カメラとエンコーダーの接続
　4.キャプチャ開始
　5.データ受信

####1.カメラ設定 (create_camera_component関数)
カメラから取得する画像のサイズ等の設定を行います。

```c
/* コンポーネント作成 */
mmal_component_create(MMAL_COMPONENT_DEFAULT_CAMERA, &camera);

/* ポート取得 */
camera_still_port = state.camera_component->output[MMAL_CAMERA_CAPTURE_PORT];

/* カメラの撮影条件を設定 */
MMAL_PARAMETER_CAMERA_CONFIG_T cam_config =
{
	{ MMAL_PARAMETER_CAMERA_CONFIG, sizeof(cam_config) },
	.max_stills_w = state->width,　　		/* 画像の幅 */
	.max_stills_h = state->height,　 		/* 画像の高さ */
	.stills_yuv422 = 0,
	.one_shot_stills = 1,
	.max_preview_video_w = state->width,
	.max_preview_video_h = state->height,
	.num_preview_video_frames = 3,
	.stills_capture_circular_buffer_height = 0,
	.fast_preview_resume = 0,
	.use_stc_timestamp = MMAL_PARAM_TIMESTAMP_MODE_RAW_STC
};
mmal_port_parameter_set(camera->control, &cam_config.hdr);

/* ポートを有効化 */
mmal_port_enable(camera->control, camera_control_callback);
```

####2.エンコーダー設定 (create_encoder_component関数)
カメラから取得した画像をエンコードする為の設定を行います。
JPEGで圧縮する場合は以下のように設定します。

```c
/* コンポーネント作成 */
mmal_component_create(MMAL_COMPONENT_DEFAULT_IMAGE_ENCODER, &encoder);

/* ポート取得 */
encoder_input = encoder->input[0];
encoder_output = encoder->output[0];

/* 条件設定 */
encoder_output->format->encoding = MMAL_ENCODING_JPEG;
mmal_port_format_commit(encoder_output);

/* ポートを有効化 */
status = mmal_port_enable(encoder_output_port, encoder_buffer_callback);
```

####3.カメラとエンコーダーの接続 (main関数)
カメラからの画像出力をエンコーダーの入力に接続します。

```c
connect_ports(camera_still_port, encoder_input_port, &state.encoder_connection);
```

####4.キャプチャ開始 (main関数)
画像のキャプチャを開始します。作業用バッファを通知することで、そのバッファに生成された画像データが格納されます。
作業バッファをキューで管理し使い回す事で、メモリの断片化を防ぐ事に繋がります。

```c
/* ファイルオープン */
output_file = fopen(use_filename, "wb");
callback_data.file_handle = output_file;

/* 受信用バッファを出力ポートに渡す */
num = mmal_queue_length(state.encoder_pool->queue);

for (q=0;q<num;q++)
{
	MMAL_BUFFER_HEADER_T *buffer = mmal_queue_get(state.encoder_pool->queue);

	if (!buffer)
	vcos_log_error("Unable to get a required buffer %d from pool queue", q);

	if (mmal_port_send_buffer(encoder_output_port, buffer)!= MMAL_SUCCESS)
	vcos_log_error("Unable to send a buffer to encoder output port (%d)", q);
}


/* 画像受信終了を待つ */
vcos_semaphore_wait(&callback_data.complete_semaphore);

callback_data.file_handle = NULL;

/* ファイルクローズ */
fclose(output_file);
```

####5.データ受信 (main関数)
バッファに画像が格納されると、mmal_port_enableで設定しておいた、encoder_buffer_callback関数が呼び出されます。
１枚の画像データに対して複数回分割して関数が呼び出される場合があります。
その為、buffer->flagsで1枚分の画像データの受信が終了したか判断します。

```c
static void encoder_buffer_callback(MMAL_PORT_T *port, MMAL_BUFFER_HEADER_T *buffer)
{
	int complete = 0;

	// We pass our file handle and other stuff in via the userdata field.

	PORT_USERDATA *pData = (PORT_USERDATA *)port->userdata;

	if (pData)
	{
		int bytes_written = buffer->length;

		if (buffer->length && pData->file_handle)
		{
			mmal_buffer_header_mem_lock(buffer);

			/* 受信データ(JPEG形式の画像データ)を出力  */
			bytes_written = fwrite(buffer->data, 1, buffer->length, pData->file_handle);

			mmal_buffer_header_mem_unlock(buffer);
		}

		// We need to check we wrote what we wanted - it's possible we have run out of storage.
		if (bytes_written != buffer->length)
		{
			vcos_log_error("Unable to write buffer to file - aborting");
			complete = 1;
		}

		// Now flag if we have completed
		if (buffer->flags & (MMAL_BUFFER_HEADER_FLAG_FRAME_END | MMAL_BUFFER_HEADER_FLAG_TRANSMISSION_FAILED))
		complete = 1;		/* 出力終了 */
	   }
	else
	{
		vcos_log_error("Received a encoder buffer callback with no state");
	}

	// release buffer back to the pool
	mmal_buffer_header_release(buffer);

	// and send one back to the port (if still open)
	if (port->is_enabled)
	{
		MMAL_STATUS_T status = MMAL_SUCCESS;
		MMAL_BUFFER_HEADER_T *new_buffer;

		new_buffer = mmal_queue_get(pData->pstate->encoder_pool->queue);

		if (new_buffer)
		{
			status = mmal_port_send_buffer(port, new_buffer);
		}
		if (!new_buffer || status != MMAL_SUCCESS)
		vcos_log_error("Unable to return a buffer to the encoder port");
	}

	if (complete)
	vcos_semaphore_post(&(pData->complete_semaphore));
}
```

###まとめ
出力されたファイルはペイントブラシなどで開いて確認してみましょう。
画像が表示されればOKです。

また、今回紹介したRaspiStill.c は RaspiCamControl.c と RaspiCLI.c にあるカメラやエンコーダーを制御する関数を経由してMMALドライバにアクセスしています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/399637/2a4a2180-d350-ed88-e995-6ba16bf3e077.png)

監視カメラ映像配信アプリでも、RaspiCamControl.c や RaspiCLI.c を使用して静止画取得処理を実現してます。

---
[インデックス記事へ](https://qiita.com/lis-hanzomon/items/cc1f7f316fbeb9579b28)
[第７回記事へ](https://qiita.com/lis-hanzomon/items/6cf0f0759d7a6120a29d)
[第９回記事へ](https://qiita.com/lis-hanzomon/items/9e975477bf9f255ed374)

---
[リンク情報システム株式会社](https://www.lis.co.jp)では一緒に働く仲間を[随時募集](https://www.lis.co.jp/employ_top)しています！
また、お仕事のご依頼、ビジネスパートナー様も募集しております。[お気軽にご連絡](https://www.lis.co.jp/inquiry)ください。