Raspberry Pi x Flutter x Firebaseでホームカメラを製作(実践編)
Raspberry PiとUSBカメラを使ったホームカメラの製作の実践編です。
前回の内容はこちら。
- システム概要
- 環境
- Python opencvでカメラのデータを読み込む
- Cloud Storage for Firebaseへの画像アップロードテスト
- Firebase Authの設定
- Flutterアプリの製作
システム概要
Raspberry PiでPythonを実行し、USBカメラで画像を撮影します。 撮影された画像をadmin権限でCloud Storage for Firebaseにアップします。 アップされた画像をFlutterアプリで受け取ります。
環境
今回、各ツールのインストールは紹介しません。
項目 | 内容 |
---|---|
Raspberry Pi | 3 Model B+ |
USBカメラ | Buffalo BSWHD06MBK |
macbook air | m1 |
Python | 3.9.1 |
Flutter | 3.0.4 |
Python opencvでカメラのデータを読み込む
opencvをインポートします。 必要に応じて他のモジュールもインポートしてください。
pip install opencv-python
opencvを使って映像が流れるかテストします。
import cv2 def main(): # キャプチャの初期化 cap = cv2.VideoCapture(0) while True: # frameの読み込み ret, frame = cap.read() # 表示 cv2.imshow('frame', frame # qボタンを押すと再生停止 if cv2.waitKey(1) & 0xFF == ord('q'): break # キャプチャ終了 cap.release() if __name__ == '__main__': main()
参考
ちなみに、本当はDockerで実行するつもりでしたが、USBカメラの読み込みがいまいちうまくいかなかったので、今回はローカル環境で実行しました。
Cloud Storage for Firebaseへの画像アップロードテスト
画像とFirebaseの準備が終了したら、Cloud Storageに画像をアップできるかテストします。 Firebase consoleからStorageを始めます。
セキュリティルールは本番ルール、リージョンはasia-northeast2(大阪)を選択。
セキュリティルールを以下のように変更。
rules_version = '2'; service firebase.storage { match /b/{bucket}/o { match /{allPaths=**} { allow read, write: if request.auth != null; } } }
python側に戻り、アップロード用のコードを記述します。
<BUCKET_NAME>
にはFirebase consoleにあるURLを記述しますが、gs://
を含めない形にしないと、404エラーになります。
import firebase_admin from firebase_admin import credentials from firebase_admin import storage cred = credentials.Certificate('serviceAccountKey.json') firebase_admin.initialize_app(cred, { 'storageBucket': '<BUCKET_NAME>.appspot.com' }) bucket = storage.bucket()
実行してFirebase consoleで画像があるか確認します。
Cloud Storage for Firebaseの公式ドキュメントにAPIの使い方が記載されています。
Cloud Storage for Firebaseのアップロードとダウンロードの操作は、Cloud Storageと同じなのでGoogle Cloudの公式ドキュメントも参考にしました。
Firebase Authの設定
Cloud Storageを使用するためにFirebase Authを使用します。
Firebase consoleからAuthenticationを開始し、メールとパスワードを選択
今回はメールアドレスとパスワードを事前に作成しておくため、ユーザーを追加を選択し、適当にアカウントを作成します。
ユーザーが作成されていたら、OKです。
Flutterアプリの製作
Flutterアプリの初期化します。
flutter create home_camera
Firebaseの使用に必要なコマンドラインツールをインストール。
firebase login dart pub global activate flutterfire_cli
アプリをFirebaseに接続
flutterfire configure
選択内容は以下の通り。
# プロジェクトを選択 ? Select a Firebase project to configure your Flutter application with › ❯ *** # プラットフォームの選択。全てにチェックが入っているか確認 ? Which platforms should your configuration support (use arrow keys & space to select)? › ✔ android ✔ ios ✔ macos ✔ web # android/build.gradleの更新を行うかどうか ? The files android/build.gradle & android/app/build.gradle will be updated to apply Firebase configuration and gradle build plugins. Do you want to continue? (y/n) › yes
pubspe.yaml
にfirebase_core
とfirebase_storage
を追加します。
dependencies: firebase_core: ^1.19.1 firebase_storage: ^10.3.1 firebase_auth: ^3.4.1
main.dart
にFirebaseのパッケージを導入し、初期化します。
import 'package:firebase_core/firebase_core.dart'; import 'firebase_options.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); await Firebase.initializeApp( options: DefaultFirebaseOptions.currentPlatform, ); runApp(const MyApp()); }
Authの認証設定です。 今回はFirebase console上ですでに作成したアカウントに自動的にサインインします。
void signIn() async { try { /// credential にはアカウント情報が記録される final credential = await FirebaseAuth.instance.signInWithEmailAndPassword( email: id, // メールアドレス password: pass, // パスワード ); } /// サインインに失敗した場合のエラー処理 on FirebaseAuthException catch (e) { print(e.code); } }
Cloud Storageから画像をダウンロードするコードを実装します。
/// 写真のデータ Uint8List? imageData; /// 画像のダウンロード void downloadPic() async { try { /// 参照の作成 String downloadName = 'Jul_07_1657594686'; final storageRef = FirebaseStorage.instance.ref().child(downloadName); /// 画像をメモリに保存し、Uint8Listへ変換 const oneMegabyte = 2048 * 2048; imageData = await storageRef.getData(oneMegabyte); } catch (e) { print(e); } }
webでは動作しないので、androidまたはiosからテストします。
完成したコード
Raspberry Pi側のpythonコード
import cv2 import time import datetime import firebase_admin from firebase_admin import credentials from firebase_admin import storage def main(): # キャプチャの初期化 cap = cv2.VideoCapture(0) # firebaseとstorage初期化 cred = credentials.Certificate("serviceAccountKey.json") app = firebase_admin.initialize_app(cred, {'storageBucket': '***.appspot.com'} ) bucket = storage.bucket() while True: # frameの読み込み ret, frame = cap.read() # 保存 nowtime =datetime.datetime.now().strftime('%h_%m_%s') strfilename = 'nowtime.jpg' cv2.imwrite(strfilename, frame, [int(cv2.IMWRITE_JPEG_QUALITY), 90]) # アップロードの名前を決定しアップロード blob=bucket.blob('nowtime.jpg') blob.upload_from_filename(strfilename) # 停止タイマー time.sleep(1) # qボタンを押すと再生停止 if cv2.waitKey(1) & 0xFF == ord('q'): break # キャプチャ終了 cap.release() if __name__ == '__main__': main()
Flutter側コード
riverpodを使用しています。
import 'package:flutter/material.dart'; import 'dart:async'; import 'dart:typed_data'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:firebase_core/firebase_core.dart'; import 'package:firebase_auth/firebase_auth.dart'; import 'package:firebase_storage/firebase_storage.dart'; import 'firebase_options.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); await Firebase.initializeApp( options: DefaultFirebaseOptions.currentPlatform, ); /// riverpod runApp(const ProviderScope(child: MyApp())); } /// 画像表示用Provider final imageStateProvider = StateProvider<Uint8List?>((ref) => null); class MyApp extends StatelessWidget { const MyApp({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return MaterialApp( title: 'Flutter Demo', theme: ThemeData( primarySwatch: Colors.blue, ), home: const MyHomePage(), debugShowCheckedModeBanner: false, ); } } /// ページ class MyHomePage extends ConsumerStatefulWidget { const MyHomePage({Key? key}) : super(key: key); @override MyHomePageState createState() => MyHomePageState(); } class MyHomePageState extends ConsumerState<MyHomePage> { /// 認証情報 String id = '***@example.com'; String pass = '***'; /// authのサインイン処理 void signIn() async { try { /// credential にはアカウント情報が記録される final credential = await FirebaseAuth.instance.signInWithEmailAndPassword( email: id, password: pass, ); } /// サインインに失敗した場合のエラー処理 on FirebaseAuthException catch (e) { print(e.code); } } /// 画像のダウンロード void downloadPic() async { try { /// 参照の作成 String downloadName = 'nowtime.jpg'; final storageRef = FirebaseStorage.instance.ref().child(downloadName); /// 画像をメモリに保存し、Uint8Listへ変換 const oneMegabyte = 1024 * 1024; /// 書込み ref.read(imageStateProvider.state).state = await storageRef.getData(oneMegabyte); } catch (e) { print(e); } } /// タイマー関数用 Timer? timer; /// 再生中 bool isPlaying = false; @override Widget build(BuildContext context) { /// authにサインイン signIn(); var imageData = ref.watch(imageStateProvider); return Scaffold( appBar: AppBar( title: const Text('ホームカメラ'), ), body: ListView( padding: const EdgeInsets.all(10), children: <Widget>[ /// 再生中なら表示変更 Center( child: isPlaying ? const Text('再生中') : const Text('停止'), ), /// ボタン Container( padding: const EdgeInsets.all(10), child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ /// 再生開始ボタン Container( padding: const EdgeInsets.all(10), child: ElevatedButton( style: ButtonStyle( backgroundColor: MaterialStateProperty.all(Colors.green), padding: MaterialStateProperty.all(const EdgeInsets.all(20)), ), onPressed: () { /// 再生中 isPlaying = true; /// 以前のタイマーがあれば停止 timer != null ? timer!.cancel() : null; /// 1回目のダウンロード downloadPic(); /// 2回目以降のダウンロード timer = Timer.periodic( const Duration(seconds: 1), (Timer t) { downloadPic(); }, ); }, child: const Tooltip( message: '再生開始', child: Icon(Icons.play_arrow), ), ), ), /// 再生停止 Container( padding: const EdgeInsets.all(10), child: ElevatedButton( style: ButtonStyle( backgroundColor: MaterialStateProperty.all(Colors.red), padding: MaterialStateProperty.all(const EdgeInsets.all(20)), ), onPressed: () { /// 再生中OFF isPlaying = false; /// タイマー停止 timer!.cancel(); }, child: const Tooltip( message: '再生停止', child: Icon(Icons.pause), ), ), ), ], ), ), /// 画像の表示 Container( alignment: Alignment.center, padding: const EdgeInsets.all(10), child: imageData == null ? const Text('No Image') : Image.memory(imageData), ), ], ), ); } }
ビルド&テスト
作成したpythonコードをラズパイへ転送し、実行します。 また、Flutterアプリは自分の所有しているモバイル端末にあわせてビルドします。
実際に動作させてみた結果はこちら。 (いい感じに撮影できなかったので、モザイク入れています)
課題
見切り発車で作ってみたはいいけど、いくつか課題が出てきたので、まとめます。
- Cloud Storage for Firebaseの無料枠がすぐに無くなる
画像を1秒に約1枚アップロード&ダウンロードするため、1日あたりの通信量制限にすぐに引っかかります。
対策として、Firestoreでアップロードの制御を行う、PythonまたはCloud Functionでアップロード枚数の制限をかけるなどがありますが、根本的には解決しません。
Firebaseを使わず、ライブ配信アプリのように別サーバーを立てるのが無難でしょうか。 参考サイトはこちら。
- セキュリティ上の問題
PythonからCloud Storageに画像をアップロードしていますが、private keyを使ってadmin権限でアップロードしているため、個人的にはあまりいい方法ではないと考えています。
- カメラが水平垂直方向に動かず、通話もできない
Amazonで市販品を見ていると、カメラに首振り機能があり、通話もできるようです。
今回の自作ホームカメラには、残念ながら、そのような機能を付けられませんでした。
正直、市販品を購入したい方がいいと思いました。
まとめ
実質数時間程度でホームカメラを製作できました。 まだまだ機能が少ないですが、作っているときは楽しかったので、今回はよしとしましょう。
参考
Raspberry Pi x Flutter x Firebaseでホームカメラを製作(準備編)
はじめに
子どもの様子を見に行くための部屋の移動が面倒だったので、移動しなくてもいいように、余っていたラズパイとUSBカメラを使ってホームカメラを製作しました。 今回の内容はホームカメラ製作の準備編です。
システム概要
Raspberry PiでPythonを実行し、USBカメラで画像を撮影します。 撮影された画像をadmin権限でCloud Storage for Firebaseにアップします。 アップされた画像をFlutterアプリで受け取ります。
環境
今回、各ツールのインストールは紹介しません。
項目 | 内容 |
---|---|
Raspberry Pi | 3 Model B+ |
USBカメラ | Buffalo BSWHD06MBK |
macbook air | m1 |
Python | 3.9.1 |
Flutter | 3.0.4 |
Raspberry Piの初期設定
OSはRaspberry Pi OSが推奨されています。
Raspberry Pi 3 以降は64bitのCPUなので、64bit版のOSを選択します。 なお、Raspberry Pi OSのwith desktopはGUI付のOSイメージ、LiteはCLIのみのOSイメージです。
今回はフォーマットもできる公式imagerを使用してインストールします。
imagerをインストールした後、まずは既存のsdカードをフォーマットします。
完了したら、64bit版のRaspberry Pi OS with desktopをインストールします。
完了しました。
問題ないか実機で確認します。
ここでは、ssh接続やリモートデスクトップ接続(VNC Viewer)の導入についての説明は省略します。
USBカメラの動作確認
lsusb
コマンドでUSBカメラが認識されているか確認します。
問題なければ、Rasberry Pi OSに標準で同梱されているVLC Playerを使ってカメラデバイスの映像を画面に映します。 使用したUSBカメラはBuffaloのBSWHD06MBKです。
VLCを起動し、メディア > キャプチャデバイスを開き、デバイス設定します。
映像が出たら確認完了です。
Firebaseの初期設定
今回はFirebaseを使用するので、Firebaseの設定をします。 ここではアカウントの作成がすでに終了している状態から設定開始します。
まず、Firebaseコンソールから新しくプロジェクトを作成します。
プロジェクトの設定 > サービスアカウントからAdmin SDKの新しい秘密鍵を生成。
ダウンロードした鍵を任意の場所に設置します。 このとき、鍵ファイルは公開リポジトリにアップしてはいけません。
次に、firebase_admin
をインストール。
pip install firebase_admin
「構成スニペット」通りにpythonスクリプトに記述します。
import firebase_admin from firebase_admin import credentials cred = credentials.Certificate("serviceAccountKey.json") firebase_admin.initialize_app(cred)
完了後、動作チェックし、エラーが出なければOKです。
続きは実践編にて紹介します。
参考
iOSアプリ(電気設備計算アシスタント)をリリースした
はじめに
以前、電気エンジニア向けのアプリをGoogle Playに公開しました。
あれから何度かアップデートを重ね、今回App Storeでもリリースをしました。
apps.apple.comアプリの機能
電気容量、電圧、力率からケーブルを選定し、ケーブルの長さから電圧降下と電力損失を計算
電圧、電流、力率から電力の値(皮相、有効、無効)やsinφを計算
選択されたケーブルから電線管を選定
配線リストで敷設する配線を管理
設定でダークモードを選択できます。
アプリの構成
言語はDart、アプリフレームワークはFlutterを使用しています。
計算部分はfreezed + Riverpod(StateNotifierProvider)ベースで製作しています。
計算結果の保存はshared_preferencesを使用しています。 このアプリではクラウドでの保存はしていません。
Appleへのお布施のため広告を表示していますが、広告にはadmobを使用しています。
App Storeでアプリを公開するにあたり苦戦したところ
アカウントの開設で躓いた
お金を支払ったのにアカウントの承認に時間がかかり、何もできない日が数日続きました。 これは普通に審査が長かっただけでした。
ちなみに、アプリの公開時の審査は数時間でした。
ビルドとリリースの方法がわからなかった
いろんなサイトにいろんな方法が書いてあり、最初どうすればいいかわかりませんでした。 とりあえず理解できたのは、
- ビルドにはいくつか方法があり、主にxcodeだけで行う方法とflutterでビルドしてtranspoterでアップする方法がある
- アップロードするためにはarchiveファイルをビルドする
結局、公式のヘルプを読んで理解しました。
https://help.apple.com/app-store-connecthelp.apple.com
なお、今回はxcodeだけでリリースできました。
今後について
継続的に開発を行い、アップデートを行っていきたいと思います。 今後、実装していきたい機能はこちらに記載しています。
また、自分にとってまだ使ったことのない技術やプログラムの改善も継続的に実施していきたいと思います。