Pandas DataFrameでの表示列・行をすべて表示する(表示制限を解除する)

Jupyter NotebookでPandasのDataFrameを表示する際、行数・列数が多すぎると省略されてしまう場合があります。

制限を解除しましょう。

pd.set_option('display.max_rows', None)
pd.set_option('display.max_columns', None)

Noneに設定するだけで、すべての行・列を表示可能です。

ただし、行・列数が多すぎる場合、読み込みに時間がかかる場合がありますのでご注意ください!

Meisyoモバイル版(MeisyoⅢ)構想

こんばんは、れいです。

最近はCovision(進捗)にかかりきりです。ただ・・・Meisyoの続編を作りたいという気持ちはモヤモヤしてきました。

仕事にできる(=もっと時間がさけるくらいのモノにしていきたく)よう、もっと一般受けするようにしたいので、最後にアンケートも行います!皆さんの意見をいただきたいです。

アンケート

Meisyo3 アンケート2022/06

ストーリー(仮):ある人の思い

あの夏の大会のあと、さまざまな人々、多くの時間が過ぎ去りました。
私は彼らとの記憶を胸に、次なるミッションへ立ち向かいます…。

変わるもの、変わらないもの。良いとされるもの、良いとされたもの。さまざまな戦術が今まで生まれ試されてきました。
私は私のチームで、選手たちの才能を見つけたいのです。

(これまでプレイしたことがある方向け)
Meisyo+ではナビゲーターだった星谷さんが選手兼監督となり、高校野球を戦い抜きます。
彼女の高校入学からストーリーが始まります。
果たして、Meisyo2から進歩した連盟システムは、彼女に何をもたらすのでしょうか?
※MeisyoやMeisyo+で語られなかった内容が再登場するかもしれません。

ゲームシステム

(戦略)限られたリソースの分配を考える
MeisyoやMeisyo+はある程度監督Lvが上がるとリソースが余りがち、かつ初心者より圧倒的に有利だったので反省。
(戦術)状況に合った指揮で試合を覆す
指揮ができるようにする。「指揮で試合が変わる」「指揮の楽しさ」を体験できるように。

モバイル(当面はAndroid)専門。
高校野球シミュレーション・監督視点。簡易3Dモデルを採用予定。
試合操作(指示・交代)可能。1試合最長で15分。1人の指揮持ち時間は5分(将棋ライク)。
全国大会での優勝を目指す。都道府県大会あり。
1日プレイ時間を30分程度で想定。

戦術選択によって選手の動きが大きく変わる。試合中操作しなくても、過去の自分の動作を真似て指揮してくれる。
戦術は、投手・打者・守備。特に守備位置等、立体的に考えることができるように。
OB大会あり。(大会の種類は月ごとに変更。種類はチーム戦、連盟(クラン)戦、個人戦、都市対抗戦、レートマッチ、、、)
大会で勝利して、学校で野球部が使える敷地を拡げていく。監督能力ではなく、箱庭で練習効率UPを具現化する。(1か月くらいでカンスト予定)
アイテムはほぼ期限付きにして、新規・古参の差を減らす。残るのは「経験と知識」のみ。(追いつきやすく、復帰しやすく)

選手は卒業あり(OBあり)。
選手は3か月(リアル1か月=ゲーム内1年)で卒業。
選手はランダム生成であるものの、すべてが有名選手ベース。各選手に特徴あるスキルを実装予定。(仲間内で「(例)松坂を持ってるんだけど、どう使えばいいのやら…」という会話のきっかけを作りたい。今はユーザー間で共有できる情報があまりにも少ない。)
スキルは固有スキルが1つあり、残り二つは同年代の選手から継承可能。(スキル三國志真戦などのゲームを参考にする。)
選手は1か月で最初に10人、ゲーム内1年で30人は無料で獲得できる予定。(無料で1日1人(+半額)ガチャ。)レアリティは存在しないが、低出現選手(+ガチャ天井の考え方)は存在するようにする。強さはそこまで変わらないし、個別の選手でばらつきがあるようにする。
選手のアバターは男女含め購入 or 自動生成する予定。
選手(ゲーム内1年で2名)は入学前に事前にスカウトできるように。スカウトの能力も重要。
選手体力を使用して、練習・試合が可能。(試合は体力がなくても出場可能だが…)
選手の自動練習を個別に設定可能。(フル出場選手なら3年目の前半くらいで育成値カンストになるように調整。)
監督体力を使用して、試合・挑戦(Meisyo+改良版)・探索(Meisyo改良版)を行う。
(日常の)試合は地域対抗戦(都道府県内のみ)、シナリオ。試合は時間がかかる分、練習より経験値効率が高い。楽しんで1日3回くらいできるようにしたい。
チャンスの時は攻撃陣は大幅に能力が上がり、守備陣は大幅に能力が下がる。点差によってさらに大きくなる。(逆転しやすくする)
挑戦は特定条件をクリアするとオリジナル選手が獲得可能になる。
探索は特定エリア内のアイテムを獲得できる。(マップの特色を強く、複雑にする)
ログボあり。ログボは7マス中1マスがクリアマス。クリアマスを踏むと残りのアイテム獲得+クリアアイテム獲得。
データ分析機能は、戦術を決める際に利用できるようにする。

課金要素:育成速度向上、選手獲得、アバター(監督、選手(試合含む))、ゲーム内マネー獲得
試合の特性を追加。球場、天候、温度、湿度…etc。もちろん時間で移り変わる。(リアルからデータを持ってきて都道府県別天気に反映させる?)
選手に特性として、天候〇などが存在するように。
選手の特性は、同年代の選手を材料にして強化できる。(スキルボックスや気合の塊の廃止&リソースの限定)
選手の特性発動はアクティブスキルのみ。パッシブは常時発動。アクティブスキルは使用制限があるが能力値がとても上昇する。
連盟(クラン)を利用可能。ユーザー同士の交流を図れるよう、クラン戦などを企画可能に。
エキシビジョン:1VS1やユーザーが作るトーナメント、リーグ戦などを実施可能にする。1日何回か可能で、経験値はちょっと入る。

雑記

MeisyoはこれまでRTS的な要素が弱かったので再考しています。皆さんのコメントで内容が変わる可能性が多いにあります。
あとは、どうにかしてリアルタイム対戦を入れようと思っていましたが自分の技術的に難しかったので見送っていました。今ならできる。

・・・とはいえ、重めのソシャゲを作るつもりはありません。メインは、戦略⇔戦術を行き来して考える&操作が楽しい体験を作りたいです。
Covisionの対応で、Android開発がざっくりとわかったので作ってみたくなりました。Unityも触ってみようと思います。(あくまでCovisionがメインです)

最近、スマホでもRTSゲームならかなりいい感じにできることを三国志系のゲームで知りました。ブラウザ三国志ガチ勢だったので良くできていると思います。皆でやるゲームとしてとても面白い。

【Flutter】動画配信アプリの作成(2022年版・無料)

はじめに

とあるきっかけから動画撮影・個別配信アプリを作りたいなということでいろいろなサイトで調べていました。

が・・・配信アプリ作成がうまく行かない。

例)

  1. ローカルネットワークでライブ配信する(2020年11月11日)
  2. 【完全無料】Flutterを使って簡易ライブ配信アプリを構築する(2021.03.14に公開)

サーバー構築はubuntu上にlibnginx-mod-rtmpを使えば簡単にできますし、サーバーがあれば配信自体はOBS Studioでできます。

配信アプリだけエラーが出て、なぜかうまく行かない(理由がわからない)を解決していきましょう!

準備

上記の例にある記事を参考に、サーバー側はローカルネットワークでライブ配信する(2020年11月11日)、アプリ側を【完全無料】Flutterを使って簡易ライブ配信アプリを構築する(2021.03.14に公開)で実装してみてください。

すべての作業を完了させ、アプリを実行させるとエラーが出てしまうと思います。そこからスタートです。

以下のパッケージをpub getします。それぞれのpackage page(例)で必要な設定を行うことを忘れないでください。

dependencies:
  flutter:
    sdk: flutter
  # camera
  camera: ^0.9.4+19 # カメラ
  camera_with_rtmp: ^0.3.2 # 配信(RTMP)用
  path_provider: ^2.0.9 # パスを通す
  # video
  video_player: ^2.3.0 # 動画確認用
  wakelock: ^0.6.1+2 # 動画撮影中に画面をOFFにしない

camera_with_rtmpにエラーが出る場合はhttps://githubhot.com/repo/pinkfish/flutter_rtmppublisher/issues/36をご覧ください。

コードの作成

ベースとなるコードの取得

flutter_rtmppublisher/example/main.dartのコードをコピペし、lib/main.dartの中身をすべて書き換えます。

Null Safety対応を行う

ベースコードは下記の通り記載されていて、内部の変数がNull Safetyに対応していません。

  CameraController controller;
  String imagePath;
  String videoPath;
  String url;

そのため、下記のように変更を行います。

  CameraController? controller;
  String? imagePath;
  String? videoPath;
  String? url;

その他のコードにも対応していない部分が複数存在するので、対応します。

Null Safety対応のパッケージを取得する

まずは、lib/rtmp_publisherディレクトリを作成します。

続いて、そのディレクトリにflutter_rtmppublisher内のファイルをコピーします。

※こんな感じでディレクトリとファイルが存在しているようにします。

lib/main.dartの下記importを、

import 'package:camera_with_rtmp/camera.dart';

下記のように変更します。自分のパッケージ名については、適宜変更ください。

import 'package:自分のパッケージ名/rtmp_publisher/camera.dart';

上手くできたと思います。

配信の確認方法

アプリ上での再生はfijkplayer、PC上での再生はVLCなどをお使いください

再生はvideo_player(や派生したパッケージ)で可能とされていますが、試してみてうまくいったのはfijkplayerパッケージのみでした。

まとめ

今回は動画配信アプリを作ってみました。それを通じて、パッケージの利用やパッケージの改善方法を知ることができたと思います。

うまく行かなかった原因は、Flutterのコーディングルールに2021年後半に大きな内容変更があったようで、後方互換性がなくなっていました。(ツライ…)

疑問点などがあればコメントをお願いいたします。それでは!

【Flutter】Googleアカウント認証SHA-1キーのためにkeytoolを使えるようにする

FlutterのGoogleアカウントでの認証(Authentication)のためにSHA-1キーが必要です。ただし、簡単に取得ができません。そのため、下記を参考にして進めます。
Google Auth公式

上記のページに、下記のコマンドをコマンドプロンプトに入力するように書かれています。

keytool -list -v -alias androiddebugkey -keystore %USERPROFILE%\.android\debug.keystore

ただし、以下のような表示が出て実行できません。

'keytool' は、内部コマンドまたは外部コマンド、
操作可能なプログラムまたはバッチ ファイルとして認識されていません。

このエラーは、「パソコンにkeytoolが入っていないので実行できないよ!」(意訳)ということですね。

keytoolはJavaJDKに入っているので、JavaJDKのインストールをすれば対応が可能です。
Windows:https://www.oracle.com/java/technologies/downloads/#java8-windows

ダウンロードにはOracleの登録が必要です。なぜ会社情報が必要なんだろう・・・。

その後、パスを通します。
デフォルトでは下記のディレクトリにkeytoolが存在するので、まずは周辺のディレクトリを探してみてください。存在を確認してからパスを指定してください。

C:\Program Files\Java\jdk1.8.0_321\bin

では実行します。パスワードのデフォルト値はandroidです。

keytool -list -v -alias androiddebugkey -keystore %USERPROFILE%\.android\debug.keystore

下記のような表示が出ると思います。

SHA-1の値をコピーして終了です!

【Flutter】ToDoアプリの作成

StatefullWidgetの状態管理、データの受け渡し、デバイスへのToDoリスト保存の解説がなかったので例となるアプリを作成しました。

こちらのアプリを応用すれば、大体のアプリが作れるのではないかと思います。アプリ作成初心者の方は、このアプリを模写して作ってみると理解が進むと思います。

GitHub

R2002:flutter_todo_demo

機能

ToDo追加

ToDoリスト表示

ToDo編集

ToDo削除

ToDoリストをデバイスに保存

ToDoリストをデバイスから呼び出す

ファイル

home.dart

Todoリスト表示など中心的なファイル。

list_add.dart

リストにTodoを追加するためのファイル。

list_edit.dart

リストのTodoを編集するためのファイル。

main.dart

アプリ起動用のファイル。

利用パッケージ

shared_preferences: ^2.0.13

エラー対応

flutter: MissingPluginException(No implementation found for method getAll on channel plugins.flutter.io/shared_preferences)

仮想デバイスを一度再起動してください。再起動をしないとshared_preferencesパッケージを正しく読み込めません。

さいごに

Listをベースに、データのやり取り編集、削除まで、アプリで一般的に使われる機能の実装を行うことができました。表示などもサクッと作れるし、これでiOSとAndroidどちらも対応できるFlutterは便利ですね~。(^^

参考

Todoアプリ概要 | Flutterで始めるアプリ開発

【Flutter】(初心者向け)カメラ・動画アプリを理解しながら作る

今回の記事はモバイルのカメラ&動画撮影アプリをまず動かしてみて、何を実装しているのか解説・理解しながら、作成を進めるコンセプトで進めていきます。

なぜなら、この記事の著者はとあるきっかけからモバイル(iOS、Android)でカメラアプリを作りたいなと思って調べたところ・・・、他のサイトでは「コードは書いている。でも何で動いているかはわからない。各コードの説明もない。」(何が分からないかわからない…ツライ)状態でした。

1行ごとに解説が欲しい…と思いましたね。そのため、丁寧に解説したいと思います。

記事の前提

記事を書くにあたり、対象とする・しない内容を明示しておきます。

  1. Android Studioを使い、その環境設定は実施済みです。
  2. 開発はiOSとAndroid向けのみ行います。
  3. Flutterの特定のバージョンのみを対象に開発します。

各種バージョンは以下の通り(現行最新)です。バージョンが変動するとコーディングルールが変化する場合がありますのでご注意ください。

> flutter doctor -v
[√] Flutter (Channel stable, 2.10.2, on Microsoft Windows [Version 10.0.22000.556], locale ja-JP)
    • Flutter version 2.10.2 at C:\Users\saito\flutter
    • Upstream repository https://github.com/flutter/flutter.git
    • Framework revision 097d3313d8 (5 weeks ago), 2022-02-18 19:33:08 -0600
    • Engine revision a83ed0e5e3
    • Dart version 2.16.1
    • DevTools version 2.9.2

Flutterの開発言語はDart、DartのベースはJavaScriptです。もし、コーディングルールがよくわからない場合は、JavaScriptを少し勉強してから取り組むと効率的だと思われます。
また、アプリの完成版が欲しい方は、一番下のまとめにあるGithubページを確認ください。

参考サイト

Flutter camera plugin: A deep dive with examples
上記サイトのアプリをベースにカスタマイズしています。実際にアプリのコードを利用する(記事の作成、アプリリリース等)場合は、上記サイトの利用規約をご確認ください。

やることメニュー

  1. 新規プロジェクト作成と準備
  2. カメラプレビューの作成
  3. カメラ撮影を可能にする
  4. カメラ状態の変更と調整
  5. 動画撮影を可能にする

カメラプレビュー、カメラを撮影できるようにします。その後、カメラの状態を変更(カメラ画素、フラッシュ、通常とセルフィーの切り替え)できるようにし、最後に動画を撮影できるようにします。

▲ メニューに戻る

新規プロジェクト作成と準備

プロジェクト作成

下記をコマンドラインで実行するか、新規Flutterプロジェクト作成を行ってください。

flutter create camera_demo_rfarms

インストール

ルートディレクトリのpubspec.yamlにインストールするパッケージを記載しておきます。
dependenciesの後に記載してください。

dependencies:
camera: ^0.9.4+17
video_player: ^2.3.0
path_provider: ^2.0.9
permission_handler: ^9.2.0

各バージョンは最新版をググって入力しておきます。バージョンが低いと競合することが多いです。

インストールするには、Android Studioの左下にあるターミナル(下記画像参照)で下記のコマンドを実行するか、右上にあるPub Get(下記画像参照)ボタンを押してください。
(画像)

(インストールコマンド)

flutter pub get

ユーザー許可の取得

cameraはユーザー許可を取得する必要があります。詳細はcamera | Flutter Packageをご覧ください。
現行では、ios/Runner/Info.plistとandroid/app/build.gradleに変更が必要です。

以降でコーディングを行います。
コーディングの各ステップの流れとしては、まずはコードの内容を変更し、ビルドして画面の変化を確認します。その後、何をどう変更したのか確認しましょう。

▲ メニューに戻る

カメラプレビューの作成

カメラプレビューの表示

lib/screens/camera_screen.dartを作成し、下記の通りに変更します。


import 'package:camera/camera.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

import '../main.dart';

class CameraScreen extends StatefulWidget {
  @override
  _CameraScreenState createState() => _CameraScreenState();
}

class _CameraScreenState extends State<CameraScreen>
    with WidgetsBindingObserver {
  CameraController? controller;
  bool _isCameraInitialized = false;

  void onNewCameraSelected(CameraDescription cameraDescription) async {
    final previousCameraController = controller;
    // Instantiating the camera controller
    final CameraController cameraController = CameraController(
      cameraDescription,
      ResolutionPreset.high,
      imageFormatGroup: ImageFormatGroup.jpeg,
    );

    // Dispose the previous controller
    await previousCameraController?.dispose();

    // Replace with the new controller
    if (mounted) {
      setState(() {
        controller = cameraController;
      });
    }

    // Update UI if controller updated
    cameraController.addListener(() {
      if (mounted) setState(() {});
    });

    // Initialize controller
    try {
      await cameraController.initialize();
    } on CameraException catch (e) {
      throw ('Error initializing camera: $e');
    }

    // Update the Boolean
    if (mounted) {
      setState(() {
        _isCameraInitialized = controller!.value.isInitialized;
      });
    }
  }

  @override
  void initState() {
    // Hide the status bar
    // SystemChrome.setEnabledSystemUIOverlays([]);
    onNewCameraSelected(cameras[0]);
    super.initState();
  }

  @override
  void dispose() {
    controller?.dispose();
    super.dispose();
  }

  @override
  void didChangeAppLifecycleState(AppLifecycleState state) {
    final CameraController? cameraController = controller;

    // App state changed before we got the chance to initialize.
    if (cameraController == null || !cameraController.value.isInitialized) {
      return;
    }

    if (state == AppLifecycleState.inactive) {
      // Free up memory when camera not active
      cameraController.dispose();
    } else if (state == AppLifecycleState.resumed) {
      // Reinitialize the camera with same properties
      onNewCameraSelected(cameraController.description);
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: _isCameraInitialized
          ? AspectRatio(
              aspectRatio: 1 / controller!.value.aspectRatio,
              child: controller!.buildPreview(),
            )
          : Container(),
    );
  }
}

lib/main.dartを下記の通りに変更します。


import 'package:flutter/material.dart';

import 'screens/camera_screen.dart';
import 'package:camera/camera.dart';

List<CameraDescription> cameras = [];

Future<void> main() async {
  try {
    WidgetsFlutterBinding.ensureInitialized();
    cameras = await availableCameras();
  } on CameraException catch (e) {
    throw ('Error in fetching the cameras: $e');
  }
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      debugShowCheckedModeBanner: false,
      home: CameraScreen(),
    );
  }
}

ビルドを実行してみてください。カメラのプレビュー画面が表示されると思います。

カメラ視点の変更は、ALTボタンを押しながら、マウスやWASDキーをクリックしてみてください。

カメラプレビューの表示(解説)

大まかな流れとしては、元々あったlib/main.dartから、lib/screens/camera_screen.dartを呼び出してカメラのプレビューを表示しています。

lib/main.dart

まずは、lib/main.dartを確認します。
必要なパッケージをインポートします。

import 'package:flutter/material.dart';
import 'screens/camera_screen.dart';
import 'package:camera/camera.dart';

続いて、cameraのデータ形式(リスト)を定義します。

List<CameraDescription> cameras = [];

続いて、メイン関数を定義します。

Future<void> main() async {
  try {
    WidgetsFlutterBinding.ensureInitialized();
    cameras = await availableCameras();
  } on CameraException catch (e) {
    throw ('Error in fetching the cameras: $e');
  }
  runApp(MyApp());
}

async-awaitを利用し、同期している(非同期関数)ように見せます。
try-catchを利用し、カメラの初期化にエラーが出た場合は「Error in fetching the cameras: $e」を表示します。カメラの初期化では、WidgetsBindingを定義し、Flutter Engineと通信ができるようにします([Flutter] WidgetsBindingとは何か?)。カメラ(cameras)リストは利用可能な複数(0:通常画面、1:セルフィー画面)を取得してきます。
その後、下記のMyAppクラスを生成します。

MyAppクラスは状態を持たないStatelessWidgetを継承して定義します。

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      debugShowCheckedModeBanner: false,
      home: CameraScreen(),
    );
  }
}

Widget build(BuildContext context) {}には、BuildContext は Element であり、 これからつくる(buildする) Widget が実際にどんな場所でどんな風に使われるのかを知るためのものという解釈があります。現実問題として、これを外すと、もしWidgetの内容が変更された場合に前後でデータが一致せずエラーが出ます。
MaterialAppを設定する際、debugShowCheckedModeBannerでデバッグモード表示をfalseに設定します。

lib/screens/camera_screen.dart

続いて、lib/screens/camera_screen.dartを確認します。
まずは、モジュールをインポートします。

import 'package:camera/camera.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

import '../main.dart';

先ほど設定したmain.dartもインポートする必要があります。その理由は、camerasなどのデータを取得する必要があるためです。

CameraScreenクラスをStatefulWidgetを用いて定義します。

class CameraScreen extends StatefulWidget {
  @override
  _CameraScreenState createState() => _CameraScreenState();
}

続いて定義する_CameraScreenStateを利用し、createStateで状態を設定します。

_CameraScreenState

_CameraScreenStateはかなり情報量が多いため、いくつかに分割して解説します。
_CameraScreenStateはStateを継承して定義します。

class _CameraScreenState extends State<CameraScreen>
    with WidgetsBindingObserver {
  CameraController? controller;
  bool _isCameraInitialized = false;

WidgetsBindingObserverを使い、アプリの状態(復帰や待機、中止状態)を検知できるようにします(WidgetsBindingObserver class)。
CameraController型でcontrollerを定義します。型の後ろに?を記載することで、Null許容になります。
bool型で_isCameraInitializedを定義します。Cameraが初期化されたかどうかを判断します。_変数名の形式は、StatefulWidget内で状態をプライベート化するために利用します(Flutterの変数名の前のUnderscore “_”は何を意味しますか?)。

onNewCameraSelected

続いて、カメラの選択をするためにonNewCameraSelectedを定義します。こちらも長いため分けて解説します。

  void onNewCameraSelected(CameraDescription cameraDescription) async {
    final previousCameraController = controller;
    // Instantiating the camera controller
    final CameraController cameraController = CameraController(
      cameraDescription,
      ResolutionPreset.high,
      imageFormatGroup: ImageFormatGroup.jpeg,
    );

    // Dispose the previous controller
    await previousCameraController?.dispose();

こちらもasync-awaitを追加します。以前のカメラを停止させるときのdisposeに利用します。
finalでpreviousCameraControllerを定義します。finalとconstは定数という観点では似たようなものですが、定義のタイミングが違います(Dart finalとconstの違いについて)。finalはプログラムが実行され、変数が途中で定義(可変)されれば、そのあとは定数として扱われます。
cameraControllerは、cameraパッケージのCameraControllerを使って定義します。定義内容は、cameraDescription、ResolutionPreset.high、imageFormatGroupを利用します。実運用上変更する可能性があるのは、ResolutionPreset(カメラの画素数)でしょうか。それは後程変更できるようにします。

onNewCameraSelectedの最後です。各種状態を設定します。

    // Replace with the new controller
    if (mounted) {
      setState(() {
        controller = cameraController;
      });
    }

    // Update UI if controller updated
    cameraController.addListener(() {
      if (mounted) setState(() {});
    });

    // Initialize controller
    try {
      await cameraController.initialize();
    } on CameraException catch (e) {
      throw ('Error initializing camera: $e');
    }

    // Update the Boolean
    if (mounted) {
      setState(() {
        _isCameraInitialized = controller!.value.isInitialized;
      });
    }
  }

状態を変更する際に使うsetState()を多用しています。controller!(クラスの後に感嘆符)は、Nullにならないとコンパイラに教えるようです(TypeScriptの変数の末尾の”!”(エクスクラメーション/感嘆符)の意味)。Flutterではどうなんでしょうか。それ以外は説明済みなので割愛します。

続いて、initState(状態設定)とdispose(停止)を定義します。

  @override
  void initState() {
    // Hide the status bar
    // SystemChrome.setEnabledSystemUIOverlays([]);
    onNewCameraSelected(cameras[0]);
    super.initState();
  }

  @override
  void dispose() {
    controller?.dispose();
    super.dispose();
  }

SystemChrome.setEnabledSystemUIOverlays([])を使うと、ステータスバーを表示させません。
camerasはリスト形式(0:通常画面、1:セルフィー画面)で得られているので、0を設定します。
disposeは説明できる内容はありません。Nullを許容しているクラスや定数(今回はcontroller)には、?を後ろにつける必要があります。

SystemChrome.setEnabledSystemUIOverlaysは非推奨に変更されたようです。下記に入れ替えてください(Flutter 2.5リリース周りのAPIを研究してみる)。

SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky,);

続いて、カメラの状態(ライフサイクル)が変わった時のdidChangeAppLifecycleStateを定義します。

  @override
  void didChangeAppLifecycleState(AppLifecycleState state) {
    final CameraController? cameraController = controller;

    // App state changed before we got the chance to initialize.
    if (cameraController == null || !cameraController.value.isInitialized) {
      return;
    }

    if (state == AppLifecycleState.inactive) {
      // Free up memory when camera not active
      cameraController.dispose();
    } else if (state == AppLifecycleState.resumed) {
      // Reinitialize the camera with same properties
      onNewCameraSelected(cameraController.description);
    }
  }

こちらは、cameraControllerが定義できていなかったり、initializeがうまくいかない場合に再定義するために利用されます。

長かった_CameraScreenStateの最後です。ここは表示に利用されます。

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: _isCameraInitialized
          ? AspectRatio(
              aspectRatio: 1 / controller!.value.aspectRatio,
              child: controller!.buildPreview(),
            )
          : Container(),
    );
  }
}

表示内容をScaffoldのbody内に定義します。以降の表示の変更は、ここで追加されていきます。

これでカメラプレビューの表示ができるようになりました。お疲れさまでした。

一端休憩しましょう。

続いて、カメラを撮影できるようにします。

▲ メニューに戻る

カメラ撮影を可能にする

カメラを撮影できるようにする

撮影ボタンがなく、かつカメラを撮る関数がないので定義します。

まずは、カメラを撮る関数takePicture()を_CameraScreenState内に定義します。

Future<XFile?> takePicture() async {
  final CameraController? cameraController = controller;
  if (cameraController!.value.isTakingPicture) {
    // A capture is already pending, do nothing.
    return null;
  }
  try {
    XFile file = await cameraController.takePicture();
    return file;
  } on CameraException catch (e) {
    print('Error occured while taking picture: $e');
    return null;
  }
}

続いて、撮影ボタンを作成します。コードの最後に定義しているWidget build(BuildContext context) {}内を変更します。

    return SafeArea(
        child: Scaffold(
      body: _isCameraInitialized
          ? Column(children: [
              AspectRatio(
                aspectRatio: 1 / controller!.value.aspectRatio,
                child: Stack(children: [
                  CameraPreview(
                    controller!,
                  ),
                  Padding(
                    padding: const EdgeInsets.fromLTRB(
                      16.0,
                      8.0,
                      16.0,
                      8.0,
                    ),
                    child: Column(
                        crossAxisAlignment: CrossAxisAlignment.end,
                        children: [
                          InkWell(
                            onTap: () async {
                              XFile? rawImage = await takePicture();
                              File imageFile = File(rawImage!.path);

                              int currentUnix =
                                  DateTime.now().millisecondsSinceEpoch;
                              final directory =
                                  await getApplicationDocumentsDirectory();
                              String fileFormat =
                                  imageFile.path.split('.').last;

                              await imageFile.copy(
                                '${directory.path}/$currentUnix.$fileFormat',
                              );
                            },
                            child: Stack(
                              alignment: Alignment.center,
                              children: [
                                Icon(Icons.circle,
                                    color: Colors.white38, size: 80),
                                Icon(Icons.circle,
                                    color: Colors.white, size: 65),
                              ],
                            ),
                          ),
                        ]),
                  ),
                ]),
              ),
            ])
          : const Center(
              child: Text(
                'LOADING',
                style: TextStyle(color: Colors.white),
              ),
            ),
    ));

SafeAreaクラスはどのOSでも適切な場所に設定する機能を持っています(SafeArea class)。
bodyは、特殊な構文で定義されているので確認します。bool値 (trueなら実行) : (falseなら実行)となります。つまり、カメラが設定されていなければ、LOADINGが表示されるが、設定されていればカメラプレビューとボタンが表示されます。

実際に撮影ボタンがタップされた場合は下記のコードが実行されます。

                              XFile? rawImage = await takePicture();
                              File imageFile = File(rawImage!.path);

                              int currentUnix =
                                  DateTime.now().millisecondsSinceEpoch;
                              final directory =
                                  await getApplicationDocumentsDirectory();
                              String fileFormat =
                                  imageFile.path.split('.').last;

                              await imageFile.copy(
                                '${directory.path}/$currentUnix.$fileFormat',
                              );

takePicture()関数が実行され、rawImageが生成される。そのパスをimageFileが受け取る。保存の際、パスはファイルのベースパス/ミリセカンド.ファイルフォーマットと設定される。

最後に、パッケージが不足しているので最初に追加しておきましょう。

import 'dart:io';
import 'package:path_provider/path_provider.dart';

ビルドをして表示を確認します。

左上に撮影の白いボタンがあり、位置に違和感あり・・・ですが機能はします。
白いボタンをクリックするとAndroid Studio上のコンソールで下記のような表示がされます。一応動いているみたいですね。

撮影したファイルが確認できないと困るので、次は撮影した写真のプレビューを表示します。

撮影したファイルのプレビューを作成する

まずは、撮影したファイルを格納しておく変数を定義します。
CameraController? controller;直下に下記を定義しておきます。

  File? _imageFile;
  List<File> allFileList = [];

続いて、_CameraScreenState内のtakePicture()のすぐ下に、撮影した画像を取得する関数を定義します。

  refreshAlreadyCapturedImages() async {
    final directory = await getApplicationDocumentsDirectory();
    List<FileSystemEntity> fileList = await directory.list().toList();
    allFileList.clear();
    List<Map<int, dynamic>> fileNames = [];

    fileList.forEach((file) {
      if (file.path.contains('.jpg') || file.path.contains('.mp4')) {
        allFileList.add(File(file.path));

        String name = file.path.split('/').last.split('.').first;
        fileNames.add({0: int.parse(name), 1: file.path.split('/').last});
      }
    });

    if (fileNames.isNotEmpty) {
      final recentFile =
          fileNames.reduce((curr, next) => curr[0] > next[0] ? curr : next);
      String recentFileName = recentFile[1];
      if (recentFileName.contains('.mp4')) {
        _imageFile = null;
      } else {
        _imageFile = File('${directory.path}/$recentFileName');
      }

      setState(() {});
    }
  }

getApplicationDocumentsDirectory()でルートディレクトリを取得し、撮影した画像データを取得します。
そのファイルのうち、指定された拡張子(jpg:画像、mp4:動画)のみをallFileListに追加します。
allFileListのうち、最新のファイルを_imageFileとして取得します。動画の場合、nullになるように定義していますが、今は使いません。

関数が定義できたので、更新時と初期設定の際に画像を読み込みます。
まずは、更新時のために、Widget build(BuildContext context) {} SafeArea内のawait imageFile.copy()の後に下記を定義します。

refreshAlreadyCapturedImages();

続いて、関数initState()のonNewCameraSelected(cameras[0]);とsuper.initState();の間に定義します。

refreshAlreadyCapturedImages();

最後に、表示を追加します。
カメラボタンが定義されているInkWellと同列(Children内)に、下記のように定義します。

                          InkWell(
                            onTap: _imageFile != null
                                ? () {
                                    Navigator.of(context).push(
                                      MaterialPageRoute(
                                        builder: (context) => PreviewScreen(
                                          imageFile: _imageFile!,
                                          fileList: allFileList,
                                        ),
                                      ),
                                    );
                                  }
                                : null,
                            child: Container(
                              width: 60,
                              height: 60,
                              decoration: BoxDecoration(
                                color: Colors.black,
                                borderRadius: BorderRadius.circular(10.0),
                                border: Border.all(
                                  color: Colors.white,
                                  width: 2,
                                ),
                                image: _imageFile != null
                                    ? DecorationImage(
                                        image: FileImage(_imageFile!),
                                        fit: BoxFit.cover,
                                      )
                                    : null,
                              ),
                              child: Container(),
                            ),
                          ),

プレビュー画像を表示するために、さまざまな調整をしています。
そこに、他ファイルで定義したPreviewScreenクラスを利用します。PreviewScreenはほかのファイルを2つ使い定義します。
コードの内容は表示を定義するだけですので、読み飛ばしていただいてもかまいません。

PreviewScreenクラス

lib/screens/preview_screen.dartを作成し、下記の通りに変更します。
こちらは、個別の画像(動画)を一つずつ確認する際に使用します。

import 'dart:io';

import 'package:flutter/material.dart';
import './captures_screen.dart';

class PreviewScreen extends StatelessWidget {
  final File imageFile;
  final List<File> fileList;

  const PreviewScreen({
    required this.imageFile,
    required this.fileList,
  });

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.black,
      body: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Padding(
            padding: const EdgeInsets.all(8.0),
            child: TextButton(
              onPressed: () {
                Navigator.of(context).pushReplacement(
                  MaterialPageRoute(
                    builder: (context) => CapturesScreen(
                      imageFileList: fileList,
                    ),
                  ),
                );
              },
              child: Text('Go to all captures'),
              style: TextButton.styleFrom(
                primary: Colors.black,
                backgroundColor: Colors.white,
              ),
            ),
          ),
          Expanded(
            child: Image.file(imageFile),
          ),
        ],
      ),
    );
  }
}

処理の流れとしては、クラス内変数を定義し、const PreviewScreen({});でこのクラスが他で使用されたときの入力を定義します。
「Go to all captures」ボタンがクリックされた場合、lib/screens/captures_screen.dartに移動します。

lib/screens/captures_screen.dartを作成し、下記の通りに変更します。
こちらは、撮影したすべての画像(動画)を表示するために使用します。

import 'dart:io';

import 'package:flutter/material.dart';
import './preview_screen.dart';

class CapturesScreen extends StatelessWidget {
  final List<File> imageFileList;

  const CapturesScreen({required this.imageFileList});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.black,
      body: SingleChildScrollView(
        physics: BouncingScrollPhysics(),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Padding(
              padding: const EdgeInsets.all(16.0),
              child: Text(
                'Captures',
                style: TextStyle(
                  fontSize: 32.0,
                  color: Colors.white,
                ),
              ),
            ),
            GridView.count(
              shrinkWrap: true,
              physics: NeverScrollableScrollPhysics(),
              crossAxisCount: 2,
              children: [
                for (File imageFile in imageFileList)
                  Container(
                    decoration: BoxDecoration(
                      border: Border.all(
                        color: Colors.black,
                        width: 2,
                      ),
                    ),
                    child: InkWell(
                      onTap: () {
                        Navigator.of(context).pushReplacement(
                          MaterialPageRoute(
                            builder: (context) => PreviewScreen(
                              fileList: imageFileList,
                              imageFile: imageFile,
                            ),
                          ),
                        );
                      },
                      child: Image.file(
                        imageFile,
                        fit: BoxFit.cover,
                      ),
                    ),
                  ),
              ],
            ),
          ],
        ),
      ),
    );
  }
}

こちらは、特定の画像をクリックすることでlib/screens/preview_screen.dartに移動できるよう作成しています。

パッケージが不足しているので、lib/screens/camera_screen.dartの最初に追加しておきましょう。

import './preview_screen.dart';

ビルドして表示を確認してみましょう。

撮影ボタンの下にプレビューが追加されています。位置は後で補正するので気にしないでください。
撮影するか、またはアプリを立ち上げなおすことで、プレビューが正常に表示されることを確認ください。

撮影したすべての画像の表示も確認しておきます。プレビューをクリックし、Go to all cupturesをクリックしてください。
(少しカスタマイズしていますが、)撮影したファイルがすべて記録されています。

お疲れさまでした。一端休憩しましょう。
この後、カメラの状態の変更(カメラ画素、フラッシュ、通常⇔セルフィーなど)を設定し、最後に動画を撮影できるようにします。

▲ メニューに戻る

カメラ状態の変更と調整

カメラの状態の変更

画素調整

画素を調整できるようにしていきます。
まずは、変数を定義します。_CameraScreenStateのallFileList直下に下記を追記します。

  final resolutionPresets = ResolutionPreset.values;
  ResolutionPreset currentResolutionPreset = ResolutionPreset.high;

続いて、_CameraScreenStateのonNewCameraSelected関数内のcameraControllerの定義を下記に変更します。

final CameraController cameraController = CameraController(
  cameraDescription,
  currentResolutionPreset,
  imageFormatGroup: ImageFormatGroup.jpeg,
);

以前はResolutionPreset.highの固定値で定義していましたが、その値を最初に定義したcurrentResolutionPresetに変え、可変になるように変更しました。

最後に、表示を追加します。
カメラボタンが定義されているInkWellと同列(Children内)のうち最初に、下記のように定義します。

                                  Align(
                                    alignment: Alignment.topRight,
                                    child: Container(
                                      decoration: BoxDecoration(
                                        color: Colors.black87,
                                        borderRadius:
                                            BorderRadius.circular(10.0),
                                      ),
                                      child: Padding(
                                        padding: const EdgeInsets.only(
                                          left: 8.0,
                                          right: 8.0,
                                        ),
                                        child: DropdownButton<ResolutionPreset>(
                                          dropdownColor: Colors.black87,
                                          underline: Container(),
                                          value: currentResolutionPreset,
                                          items: [
                                            for (ResolutionPreset preset
                                                in resolutionPresets)
                                              DropdownMenuItem(
                                                child: Text(
                                                  preset
                                                      .toString()
                                                      .split('.')[1]
                                                      .toUpperCase(),
                                                  style: TextStyle(
                                                      color: Colors.white),
                                                ),
                                                value: preset,
                                              )
                                          ],
                                          onChanged: (value) {
                                            setState(() {
                                              currentResolutionPreset = value!;
                                              _isCameraInitialized = false;
                                            });
                                            onNewCameraSelected(
                                                controller!.description);
                                          },
                                          hint: Text("Select item"),
                                        ),
                                      ),
                                    ),
                                  ),

resolutionPresetsのリストにforを使うことで、ドロップダウンメニューを作っています。

では、表示を確認しましょう。

簡単にドロップダウンリストが右上に表示できましたね。

ズーム調整

まずは、変数を定義します。_CameraScreenStateのallFileList直下に下記を追記します。

double _minAvailableZoom = 1.0;
double _maxAvailableZoom = 1.0;
double _currentZoomLevel = 1.0;

設定変更のため、onNewCameraSelectedのawait cameraController.initialize();の直下に下記を定義します。

      await Future.wait([
        cameraController
            .getMaxZoomLevel()
            .then((value) => _maxAvailableZoom = value),
        cameraController
            .getMinZoomLevel()
            .then((value) => _minAvailableZoom = value),
      ]);

最後に、表示を追加します。
カメラボタンが定義されているInkWellと同列(Children内)に、下記のように定義します。上記で定義したDropdownButtonの後ろに設定するとよいでしょう。表示はあとで調整可能です。

                                  Row(
                                    children: [
                                      Expanded(
                                        child: Slider(
                                          value: _currentZoomLevel,
                                          min: _minAvailableZoom,
                                          max: _maxAvailableZoom,
                                          activeColor: Colors.white,
                                          inactiveColor: Colors.white30,
                                          onChanged: (value) async {
                                            setState(() {
                                              _currentZoomLevel = value;
                                            });
                                            await controller!
                                                .setZoomLevel(value);
                                          },
                                        ),
                                      ),
                                      Padding(
                                        padding:
                                            const EdgeInsets.only(right: 8.0),
                                        child: Container(
                                          decoration: BoxDecoration(
                                            color: Colors.black87,
                                            borderRadius:
                                                BorderRadius.circular(10.0),
                                          ),
                                          child: Padding(
                                            padding: const EdgeInsets.all(8.0),
                                            child: Text(
                                              _currentZoomLevel
                                                      .toStringAsFixed(1) +
                                                  'x',
                                              style: TextStyle(
                                                  color: Colors.white),
                                            ),
                                          ),
                                        ),
                                      ),
                                    ],
                                  ),
露光調整

まずは、変数を定義します。_CameraScreenStateのallFileList直下に下記を追記します。

double _minAvailableExposureOffset = 0.0;
double _maxAvailableExposureOffset = 0.0;
double _currentExposureOffset = 0.0;

Future.wait内に下記を定義します。

        cameraController
            .getMinExposureOffset()
            .then((value) => _minAvailableExposureOffset = value),
        cameraController
            .getMaxExposureOffset()
            .then((value) => _maxAvailableExposureOffset = value),

最後に、表示を追加します。
カメラボタンが定義されているInkWellと同列(Children内)に、下記のように定義します。上記で定義したRow()の後ろに設定します。

                                  Expanded(
                                    child: RotatedBox(
                                      quarterTurns: 3,
                                      child: Container(
                                        height: 30,
                                        child: Slider(
                                          value: _currentExposureOffset,
                                          min: _minAvailableExposureOffset,
                                          max: _maxAvailableExposureOffset,
                                          activeColor: Colors.white,
                                          inactiveColor: Colors.white30,
                                          onChanged: (value) async {
                                            setState(() {
                                              _currentExposureOffset = value;
                                            });
                                            await controller!
                                                .setExposureOffset(value);
                                          },
                                        ),
                                      ),
                                    ),
                                  ),

追加で、上記コードの上部(ROW()との間)に、下記コードを追記します。

                                  Padding(
                                    padding: const EdgeInsets.only(
                                        right: 8.0, top: 16.0),
                                    child: Container(
                                      decoration: BoxDecoration(
                                        color: Colors.white,
                                        borderRadius:
                                            BorderRadius.circular(10.0),
                                      ),
                                      child: Padding(
                                        padding: const EdgeInsets.all(8.0),
                                        child: Text(
                                          _currentExposureOffset
                                                  .toStringAsFixed(1) +
                                              'x',
                                          style: TextStyle(color: Colors.black),
                                        ),
                                      ),
                                    ),
                                  ),

それでは、表示を確認します。

ズーム調整(横)と露光調整(縦)が表示されました。スライダーを触ると、倍率を調整できることが分かると思います!
表示がおかしい場合、コーディングの順序を確認ください。Align, Padding, Expanded, Row, InkWell(撮影ボタン), InkWell(プレビュー)になっていると思います。

フラッシュ設定

まずは、変数を定義します。_CameraScreenStateのallFileList直下に下記を追記します。

FlashMode? _currentFlashMode;

onNewCameraSelectedのFuture.waitの後に下記を定義します。

_currentFlashMode = controller!.value.flashMode;

最後に、表示を追加します。
カメラボタンが定義されているInkWellと同列(Children内)に、下記のように定義します。

                      Expanded(
                        child: SingleChildScrollView(
                          physics: BouncingScrollPhysics(),
                          child: Column(
                            children: [
                              Padding(
                                padding: const EdgeInsets.only(top: 8.0),
                                child: Row(
                                  children: [
                                    Expanded(
                                      child: Padding(
                                        padding: const EdgeInsets.only(
                                          left: 8.0,
                                          right: 4.0,
                                        ),
                                        child: TextButton(
                                          onPressed: _isRecordingInProgress
                                              ? null
                                              : () {
                                                  if (_isVideoCameraSelected) {
                                                    setState(() {
                                                      _isVideoCameraSelected =
                                                          false;
                                                    });
                                                  }
                                                },
                                          style: TextButton.styleFrom(
                                            primary: _isVideoCameraSelected
                                                ? Colors.black54
                                                : Colors.black,
                                            backgroundColor:
                                                _isVideoCameraSelected
                                                    ? Colors.white30
                                                    : Colors.white,
                                          ),
                                          child: Text('IMAGE'),
                                        ),
                                      ),
                                    ),
                                    Expanded(
                                      child: Padding(
                                        padding: const EdgeInsets.only(
                                            left: 4.0, right: 8.0),
                                        child: TextButton(
                                          onPressed: () {
                                            if (!_isVideoCameraSelected) {
                                              setState(() {
                                                _isVideoCameraSelected = true;
                                              });
                                            }
                                          },
                                          style: TextButton.styleFrom(
                                            primary: _isVideoCameraSelected
                                                ? Colors.black
                                                : Colors.black54,
                                            backgroundColor:
                                                _isVideoCameraSelected
                                                    ? Colors.white
                                                    : Colors.white30,
                                          ),
                                          child: Text('VIDEO'),
                                        ),
                                      ),
                                    ),
                                  ],
                                ),
                              ),
                              Padding(
                                padding: const EdgeInsets.fromLTRB(
                                    16.0, 8.0, 16.0, 8.0),
                                child: Row(
                                  mainAxisAlignment:
                                      MainAxisAlignment.spaceBetween,
                                  children: [
                                    InkWell(
                                      onTap: () async {
                                        setState(() {
                                          _currentFlashMode = FlashMode.off;
                                        });
                                        await controller!.setFlashMode(
                                          FlashMode.off,
                                        );
                                      },
                                      child: Icon(
                                        Icons.flash_off,
                                        color:
                                            _currentFlashMode == FlashMode.off
                                                ? Colors.amber
                                                : Colors.white,
                                      ),
                                    ),
                                    InkWell(
                                      onTap: () async {
                                        setState(() {
                                          _currentFlashMode = FlashMode.auto;
                                        });
                                        await controller!.setFlashMode(
                                          FlashMode.auto,
                                        );
                                      },
                                      child: Icon(
                                        Icons.flash_auto,
                                        color:
                                            _currentFlashMode == FlashMode.auto
                                                ? Colors.amber
                                                : Colors.white,
                                      ),
                                    ),
                                    InkWell(
                                      onTap: () async {
                                        setState(() {
                                          _currentFlashMode = FlashMode.always;
                                        });
                                        await controller!.setFlashMode(
                                          FlashMode.always,
                                        );
                                      },
                                      child: Icon(
                                        Icons.flash_on,
                                        color: _currentFlashMode ==
                                                FlashMode.always
                                            ? Colors.amber
                                            : Colors.white,
                                      ),
                                    ),
                                    InkWell(
                                      onTap: () async {
                                        setState(() {
                                          _currentFlashMode = FlashMode.torch;
                                        });
                                        await controller!.setFlashMode(
                                          FlashMode.torch,
                                        );
                                      },
                                      child: Icon(
                                        Icons.highlight,
                                        color:
                                            _currentFlashMode == FlashMode.torch
                                                ? Colors.amber
                                                : Colors.white,
                                      ),
                                    ),
                                  ],
                                ),
                              )
                            ],
                          ),
                        ),
                      ),

カメラ/動画の切り替えボタンと、フラッシュ設定を切り替えるアイコンを設定しました。Flutterの条件文の構文、isA ? True : Falseが随所に出てきます。それさえ理解できれば、それ以上に複雑なことはないでしょう。

続いて、動画撮影で使うものの、定義できていない変数を定義します。

  bool _isVideoCameraSelected = false;
  bool _isRecordingInProgress = false;

さて、最後に表示を確認します。

表示はできているものの、何か違和感があります。これは後で対処します。

カメラを切り替える

通常のカメラと、セルフィーのカメラを切り替えます。
まずは、変数を定義します。_CameraScreenStateのallFileList直下に下記を追記します。

bool _isRearCameraSelected = true;

最後に、表示を追加します。
カメラボタンが定義されているInkWellと同列(Children内)に、InkWellのうちでは一番上部で下記のように定義します。

                                      InkWell(
                                        onTap: _isRecordingInProgress
                                            ? () async {
                                                if (controller!
                                                    .value.isRecordingPaused) {
                                                  //await resumeVideoRecording();
                                                } else {
                                                  //await pauseVideoRecording();
                                                }
                                              }
                                            : () {
                                                setState(() {
                                                  _isCameraInitialized = false;
                                                });
                                                onNewCameraSelected(cameras[
                                                    _isRearCameraSelected
                                                        ? 1
                                                        : 0]);
                                                setState(() {
                                                  _isRearCameraSelected =
                                                      !_isRearCameraSelected;
                                                });
                                              },
                                        child: Stack(
                                          alignment: Alignment.center,
                                          children: [
                                            Icon(
                                              Icons.circle,
                                              color: Colors.black38,
                                              size: 60,
                                            ),
                                            _isRecordingInProgress
                                                ? controller!
                                                        .value.isRecordingPaused
                                                    ? Icon(
                                                        Icons.play_arrow,
                                                        color: Colors.white,
                                                        size: 30,
                                                      )
                                                    : Icon(
                                                        Icons.pause,
                                                        color: Colors.white,
                                                        size: 30,
                                                      )
                                                : Icon(
                                                    _isRearCameraSelected
                                                        ? Icons.camera_front
                                                        : Icons.camera_rear,
                                                    color: Colors.white,
                                                    size: 30,
                                                  ),
                                          ],
                                        ),
                                      ),

最後にビルドしてください。

追加できました!

ただ、現在は表示に違和感があり、カメラ切り替え・カメラ撮影・プレビューが縦に並んでいます。表示が一致しない場合は、R2002/flutter_camera_demo_1をご覧ください。

次は、表示の不具合を修正します。

表示を修正する

縦並びから横並びに変更

下記の画像のように、カメラ切り替え・カメラ撮影・プレビューが縦に並んでいます。

現状、大ざっぱに考えると下記のように定義されています。

children: [
   ズームなどの表示,
   InkWell(カメラ切り替え),
   InkWell(カメラ撮影ボタン),
   InkWell(プレビュー表示),
   フラッシュなどの調整,
]

それらを横並びにするために、ROW()を使用します。

children: [
   ズームなどの表示,
   Row(
       mainAxisAlignment:
           MainAxisAlignment.spaceBetween,
           children: [
               InkWell(カメラ切り替え),
               InkWell(カメラ撮影ボタン),
               InkWell(プレビュー表示),
           ]
   ),
   フラッシュなどの調整,
]

mainAxisAlignment:MainAxisAlignment.spaceBetween,を定義することにより、オブジェクトごとにスペース(横軸)を開けています。

表示を確認してみましょう。

意図した通りに横並びになってくれました。

ただ、まだ表示に違和感があります。画面下に白いスペースが空いています・・・。そのため、次は縦の長さを調整します。

縦の長さを調整する

initState()の上に定義します。
こちらは、スクリーンの大きさによって設定が変動するようにします。

  void onViewFinderTap(TapDownDetails details, BoxConstraints constraints) {
    if (controller == null) {
      return;
    }

    final offset = Offset(
      details.localPosition.dx / constraints.maxWidth,
      details.localPosition.dy / constraints.maxHeight,
    );
    controller!.setExposurePoint(offset);
    controller!.setFocusPoint(offset);
  }

CameraPreview(controller!,)を変更します。

                  CameraPreview(
                    controller!,
                    child: LayoutBuilder(builder:
                        (BuildContext context, BoxConstraints constraints) {
                      return GestureDetector(
                        behavior: HitTestBehavior.opaque,
                        onTapDown: (details) =>
                            onViewFinderTap(details, constraints),
                      );
                    }),
                  ),

ただし、表示はまだ変わりません。

最後に、AspectRatio内のStackにchildrenとして定義されているExpanded(child: SingleChildScrollView(~))(カメラ/ビデオ切り替えとフラッシュ設定を変更する大きなボックス)を、AspectRatioと同列に定義しなおします。

下記から、

AspectRatio(
    child: Stack(children: [
        表示がいろいろと定義されている,
        Expanded(child: SingleChildScrollView(~)),
    ])
),

下記のように変更します。

AspectRatio(
    child: Stack(children: [
        表示がいろいろと定義されている,
    ])
),
Expanded(child: SingleChildScrollView(~)),

AspectRatioが、Columnのchildrenとして定義されているため、このように同列に並べる変更が可能です。

最後に、SafeAreaのchildに指定されているScaffoldに背景色を追加します。

  Widget build(BuildContext context) {
    return SafeArea(
        child: Scaffold(
      backgroundColor: Colors.black,
      body: _isCameraInitialized...()
      )
   )
  });

それでは表示を確認します。スライダーやカメラ切り替えをチェックしてみましょう。

触っていて切り替え時のバグを発見しました。下の露光スライダーをx10にして、カメラを切り替えるとエラーが出ます。対応方法を考えます。

どうやら、ExposureOffsetの最大値が通常とセルフィー画面で違うようです。そのため、カメラ切り替え時にリセットをかけます。
関数を作成します。

  void resetCameraValues() async {
    _currentZoomLevel = 1.0;
    _currentExposureOffset = 0.0;
  }

onNewCameraSelectedのawait previousCameraController?.dispose();の後に下記を追記します。

resetCameraValues();

これで表示の作成は完了しました!お疲れさまでした。

一端休憩しましょう。ここまでの過程をR2002/flutter_camera_demo_2に保存しておきました。自分のコードと比較したい場合はどうぞ。

▲ メニューに戻る

動画撮影を可能にする

動画を撮影できるようにする

では最後のステップです。
流れとしては、撮影開始、撮影終了、ポーズ、ポーズからの再開、動画プレイヤーの追加、撮影ボタンの編集を行います。

撮影開始
  Future<void> startVideoRecording() async {
    final CameraController? cameraController = controller;

    if (controller!.value.isRecordingVideo) {
      // A recording has already started, do nothing.
      return;
    }

    try {
      await cameraController!.startVideoRecording();
      setState(() {
        _isRecordingInProgress = true;
        print(_isRecordingInProgress);
      });
    } on CameraException catch (e) {
      print('Error starting to record video: $e');
    }
  }

cameraアプリのstartVideoRecording()を利用するために、いくつかの確認をしています。

撮影終了
  Future<XFile?> stopVideoRecording() async {
    if (!controller!.value.isRecordingVideo) {
      // Recording is already is stopped state
      return null;
    }

    try {
      XFile file = await controller!.stopVideoRecording();
      setState(() {
        _isRecordingInProgress = false;
      });
      return file;
    } on CameraException catch (e) {
      print('Error stopping video recording: $e');
      return null;
    }
  }

終了時に、ファイルをreturnしていることに注意してください。

ポーズ
  Future<void> pauseVideoRecording() async {
    if (!controller!.value.isRecordingVideo) {
      // Video recording is not in progress
      return;
    }

    try {
      await controller!.pauseVideoRecording();
    } on CameraException catch (e) {
      print('Error pausing video recording: $e');
    }
  }

ポーズを実施するためにいくつか確認をしています。

ポーズからの再開
  Future<void> resumeVideoRecording() async {
    if (!controller!.value.isRecordingVideo) {
      // No video recording was in progress
      return;
    }

    try {
      await controller!.resumeVideoRecording();
    } on CameraException catch (e) {
      print('Error resuming video recording: $e');
    }
  }

ポーズからの再開を実施するためにいくつか確認をしています。

こちらで、関数の定義は終了しました。

動画プレイヤーの追加

動画を撮影しても動画プレイヤーがないと確認ができません。そのため実装を行っていきます。
まずは、変数を定義します。_CameraScreenStateのallFileList直下に下記を追記します。

  VideoPlayerController? videoController;
  File? _videoFile;

ファイルの先頭で必要なパッケージをインポートします。

import 'package:video_player/video_player.dart';

関数を定義します。

  Future<void> _startVideoPlayer() async {
    if (_videoFile != null) {
      videoController = VideoPlayerController.file(_videoFile!);
      await videoController!.initialize().then((_) {
        // Ensure the first frame is shown after the video is initialized,
        // even before the play button has been pressed.
        setState(() {});
      });
      await videoController!.setLooping(true);
      await videoController!.play();
    }
  }
撮影ボタンの編集

コード上で一番上にあるInkWellのawait resumeVideoRecording();とawait pauseVideoRecording();のコメントアウトを外します。

続いて、上から2番目のInkWell(カメラ撮影ボタン)を下記に変更します。

                                      InkWell(
                                        onTap: _isVideoCameraSelected
                                            ? () async {
                                                if (_isRecordingInProgress) {
                                                  XFile? rawVideo =
                                                      await stopVideoRecording();
                                                  File videoFile =
                                                      File(rawVideo!.path);

                                                  int currentUnix = DateTime
                                                          .now()
                                                      .millisecondsSinceEpoch;

                                                  final directory =
                                                      await getApplicationDocumentsDirectory();

                                                  String fileFormat = videoFile
                                                      .path
                                                      .split('.')
                                                      .last;

                                                  _videoFile =
                                                      await videoFile.copy(
                                                    '${directory.path}/$currentUnix.$fileFormat',
                                                  );

                                                  _startVideoPlayer();
                                                } else {
                                                  await startVideoRecording();
                                                }
                                              }
                                            : () async {
                                                XFile? rawImage =
                                                    await takePicture();
                                                File imageFile =
                                                    File(rawImage!.path);

                                                int currentUnix = DateTime.now()
                                                    .millisecondsSinceEpoch;

                                                final directory =
                                                    await getApplicationDocumentsDirectory();

                                                String fileFormat = imageFile
                                                    .path
                                                    .split('.')
                                                    .last;

                                                print(fileFormat);

                                                await imageFile.copy(
                                                  '${directory.path}/$currentUnix.$fileFormat',
                                                );

                                                refreshAlreadyCapturedImages();
                                              },
                                        child: Stack(
                                          alignment: Alignment.center,
                                          children: [
                                            Icon(
                                              Icons.circle,
                                              color: _isVideoCameraSelected
                                                  ? Colors.white
                                                  : Colors.white38,
                                              size: 80,
                                            ),
                                            Icon(
                                              Icons.circle,
                                              color: _isVideoCameraSelected
                                                  ? Colors.red
                                                  : Colors.white,
                                              size: 65,
                                            ),
                                            _isVideoCameraSelected &&
                                                    _isRecordingInProgress
                                                ? Icon(
                                                    Icons.stop_rounded,
                                                    color: Colors.white,
                                                    size: 32,
                                                  )
                                                : Container(),
                                          ],
                                        ),
                                      ),

当初はカメラ撮影のみしか機能が存在しませんでしたが、動画撮影も加わったため、表示の調整をしています。

上から3番目のInkWell(プレビュー表示)も下記の通りに変更します。

                                InkWell(
                                  onTap:
                                      _imageFile != null || _videoFile != null
                                          ? () {
                                              Navigator.of(context).push(
                                                MaterialPageRoute(
                                                  builder: (context) =>
                                                      CapturesScreen(
                                                    imageFileList: allFileList,
                                                  ),
                                                ),
                                              );
                                            }
                                          : null,
                                  child: Container(
                                    width: 60,
                                    height: 60,
                                    decoration: BoxDecoration(
                                      color: Colors.black,
                                      borderRadius: BorderRadius.circular(10.0),
                                      border: Border.all(
                                        color: Colors.white,
                                        width: 2,
                                      ),
                                      image: _imageFile != null
                                          ? DecorationImage(
                                              image: FileImage(_imageFile!),
                                              fit: BoxFit.cover,
                                            )
                                          : null,
                                    ),
                                    child: videoController != null &&
                                            videoController!.value.isInitialized
                                        ? ClipRRect(
                                            borderRadius:
                                                BorderRadius.circular(8.0),
                                            child: AspectRatio(
                                              aspectRatio: videoController!
                                                  .value.aspectRatio,
                                              child:
                                                  VideoPlayer(videoController!),
                                            ),
                                          )
                                        : Container(),
                                  ),
                                ),

こちらも動画撮影に対応するために変更しました。

最後に、プレビュー表示変更用のrefreshAlreadyCapturedImages()を変更します。
if (fileNames.isNotEmpty) {}内にあるif(recentFileName.contains(‘.mp4’)){}内に下記を追記します。

        _videoFile = File('${directory.path}/$recentFileName');
        _startVideoPlayer();

else側に下記を追記します。

        _videoFile = null;

機能停止のためのdispose関数内に下記を追記します。

videoController?.dispose();

表示を確認します。

動画がうまく表示されないようです。

ユーザー許可を取得する画面を作成します。設定についてはpermission_handlerをご覧ください。
まずは、変数を定義します。_CameraScreenStateのallFileList直下に下記を追記します。

  bool _isCameraPermissionGranted = false;

続いて、許可の状態を確認する関数として下記を定義します。その他の関数と同列に定義してください。

  getPermissionStatus() async {
    await Permission.camera.request();
    var status = await Permission.camera.status;

    if (status.isGranted) {
      print('Camera Permission: GRANTED');
      setState(() {
        _isCameraPermissionGranted = true;
      });
      // Set and initialize the new camera
      onNewCameraSelected(cameras[0]);
      refreshAlreadyCapturedImages();
    } else {
      print('Camera Permission: DENIED');
    }
  }

initState()の下記をgetPermissionStatus();に変更します。

onNewCameraSelected(cameras[0]);
refreshAlreadyCapturedImages();

SafeArea(child: Scaffold())内にある下記を、、

      body: _isCameraInitialized ?

下記の通りに変更します。ユーザー許可がされていない場合は、続いて定義する内容を表示させます。

      body: _isCameraPermissionGranted
          ? _isCameraInitialized ?

構文としては、上記はif ~ if ~ else と同じなので、body: isA ? isB ? Column() : Column() : Column()と記載する必要があります。

上記で追加した? _isCameraPermissionGrantedに対応するように、最後にelseとして許可が取得できなかった内容を表示します。

            : Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  Row(),
                  const Text(
                    'Permission denied',
                    style: TextStyle(
                      color: Colors.white,
                      fontSize: 24,
                    ),
                  ),
                  SizedBox(height: 24),
                  ElevatedButton(
                    onPressed: () {
                      getPermissionStatus();
                    },
                    child: Padding(
                      padding: const EdgeInsets.all(8.0),
                      child: Text(
                        'Give permission',
                        style: TextStyle(
                          color: Colors.white,
                          fontSize: 24,
                        ),
                      ),
                    ),
                  ),
                ],
              ),

ここまででカメラ&動画撮影アプリの作成はほぼ終了です。

以降は、開発時によくあるのですが思わぬ不具合(バグ)があるため、そのバグを対処していきたいと思います。

最終課題

バグは2点存在します。それぞれ対処していきましょう。

  1. 動画が表示できない
  2. 動画を撮影しプレビューに反映すると、プレビューの挙動がおかしくなる

動画が表示できない

下記のように、画面にバツ印が出て動画を表示できません。

まずは何が問題か確認するため、動画が保存されているか確認します。

なぜなら、現状で考えられる問題のパターンは、1. 動画は正常に保存されているが表示に不具合があるのか、2. 動画は保存されているが正常な動画ファイルではないのか、3. 動画が保存されていないのかなどがあるでしょう。そのため、まずは動画が保存されているかどうかを確認します。

Android Studioのデバイスマネージャをクリックします。

そうすると、作成したデバイスが表示されます。そこで赤枠のディレクトリアイコンをクリックすると、デバイスのデータを確認できます。

データはアプリごとに分けて保存されているため、data/data/アプリ名/app_flutterを探してみてください。

探した後は、ファイルを右クリックして、開くコマンドを選択します。関連付けられたファイルで表示すると、どうやらデータの保存はうまくいっていることが分かりました。つまり、アプリの表示部分に課題があるということです。

今回の対応としては、ImageとVideoで条件を分けをして表示を切り分けました。パッケージとしては、ImageはImageで、VideoはVideoPlayer(screen/movie_screen.dartを新たに作成)を利用しました。完成した内容についてはまとめをご覧ください。

動画を撮影しプレビューに反映すると、プレビューの挙動がおかしくなる

画像のプレビューが無視されるようになります。アプリを再起動、リロードしたときだけは問題は出ません。

該当する表示部位を確認します。

                                          child: videoController != null &&
                                                  videoController!
                                                      .value.isInitialized
                                              ? ClipRRect(
                                                  borderRadius:
                                                      BorderRadius.circular(
                                                          8.0),
                                                  child: AspectRatio(
                                                    aspectRatio:
                                                        videoController!
                                                            .value.aspectRatio,
                                                    child: VideoPlayer(
                                                        videoController!),
                                                  ),
                                                )
                                              : Container(),

どうやらこのコードだと、画像を撮影した後にも、videoControllerが存在したままになっているため動画が表示されている可能性があります。

プレビューを変更する関数を下記から、

      if (recentFileName.contains('.mp4')) {
        _imageFile = null;
        _videoFile = File('${directory.path}/$recentFileName');
        _startVideoPlayer();
      } else {
        _imageFile = File('${directory.path}/$recentFileName');
        _videoFile = null;
      }

下記のように変更します。

      if (recentFileName.contains('.mp4')) {
        _imageFile = null;
        _videoFile = File('${directory.path}/$recentFileName');
        _startVideoPlayer();
      } else {
        _imageFile = File('${directory.path}/$recentFileName');
        _videoFile = null;
        videoController!.dispose(); # videoController = null;
      }

そうすることで、_videoFileが定義されていない場合は、videoControllerが初期化されます。

また、表示部分の下記を、

videoController != null && videoController!.value.isInitialized

下記の通り変更します。

_videoFile != null

そうすると、_videoFileがnullであれば動画を表示させないことが可能になります。

▲ メニューに戻る

まとめ

長い記事になりましたが、お疲れさまでした。読んでいただきありがとうございました。

カメラと動画を撮影できるアプリを、1から作成することができましたね!Flutterはいろいろな機能を組み合わせて、機能をサクサクと作ることが可能です。

今回の実装はR2002/flutter_camera_demo_lastに最終版が保存されています。ご確認ください。

また、コードの不具合や疑問等があればコメントいただければと思います。それではありがとうございました!

[Meisyo+] 新機能(リーグ戦)を試すとアイデアが出る

Meisyo+の機能、いろいろと案はあるもののテストもせずとりあえず作っています。
私事ですが、入社してすぐですが仕事は面白くなりそうでよかったです。まだまだ研修中です。

ある程度正確な指標予測モデル作成のために、リーグ戦(仮)を作成してデータ抽出を行っています。

これを見るに、リーグ戦を「特訓」に入れてもいいのでは?と思ってます。
仕様は、募集主がリーグ戦の相手(人数・能力制限あり)を募る形+1日1試合限定で、対戦相手名は伏せると・・・。募集も応募もできると。

Meisyoの連盟戦もリーグ戦でしたが、、そのまま実装するのもアレなんですよね・・・。
ゲームシステム・ストーリー的に、リーグ戦の優勝チームを予想する野球賭博的なこともあってもいい・・・のか?何か妙案が欲しい。

あとは、ゲームへの参加人数が頭打ちしているのでどうにかしたいですね。。。
継続率は高いものの、新規参加数が少なすぎる。

自然言語処理×教師なし学習での温故知新 PythonでBERT-MaskedLM実装

はじめに

自然言語処理(BERT、GPT-3)および画像認識(ViT)等で以前のState of The Artモデルを超える精度を発揮したTransformer(元論文:Attention Is All You Need)を元にしたモデルたちが存在します。

その中で、BERT: Pre-training of Deep Bidirectional Transformers for Language Understandingという、2022年現在では自然言語処理ベーシックとなった手法が存在します。

ベーシックになった一端として、MaskedLMと呼ばれる学習方法があると私は考えました。

なぜMaskedLMが重要なのか

大規模なデータセットから、モデルを用いて表現学習する際に、教師ラベルを付与する必要がなくても問題がないことを示したからです。

例えば、Deep Learningが注目されるきっかけとなったImageNet Large Scale Visual Recognition Challenge(ILSVRC)は、画像に物体のラベルを1:1で付与したデータセットImageNetを利用しています。

そこで問題となるのは、教師ラベルを付ける工数やその整合性です。

工数は、AIアノテーションの意味とは?タグ付け自動化ツール17選を比較のような製品群ランキングが出てくるように喫緊の課題とされています。
整合性については、現実問題、データ量があまりにも多すぎて検証しようがない状況ですが、近年、AIの専門誌ではないテック系メディアのWiredにAIの基盤となるデータに「ラベル付けの間違い」が蔓延、その影響の深刻度が掲載されるような状態です。

そのように、ラベルを付与する手間や正しさが分からないまま教師あり学習を行うとノイズが含まれる結果となり、ノイズなしのデータと比べてどうしても精度が落ちる(A Chat with Andrew on MLOps: From Model-centric to Data-centric AI)ということが分かっています。

その点、教師なし学習で表現学習ができるMaskedLMは、もともとのデータ(文章)が膨大なメンテナンスコストをかけて整備され続けてきたもの(例:Wikipedia)であれば、ノイズはほぼ含まれていないであろうと考えられます。以前は、教師なし学習でそのような大規模な学習は不可能でした。

そのため、MaskedLMは画期的であろうと私は考えました。しかも、画像処理より難易度の高いと考えられている自然言語処理て、事前の表現学習が可能となりました。

もちろん、画像処理においても、現在ではさまざまな教師なし学習(例:Contrastive Learningの2大手法SimCLR・MoCo、それぞれの進化 (画像の表現学習2020夏特集2))が提案されていることは忘れてはならないかと思います。それは、MaskedLMが提案された後です。

では、実装したコードを示します。

MaskedLMの実装

R2002/NLP_MaskedLM
・Pytorchでの実装です。
・利用される場合はrequirements.txtをpipでインストールしてください。
・学習用テキストは太宰治「走れメロス」を利用しています。

学習方法は、マスクしたトークンを周辺のトークンから推測します。
トークンとは、与えられた文を形態素解析し、1つ1つの意味(チャンク)に分けられた後の語のことを示します。

例えば、文をメロスは激怒したとすると、

形態素解析後にメロス/は/激怒/したのように分けられます。

そのうち、メロス、は、激怒、したというそれぞれの語をトークンと呼びます。

MaskedLMは、そのトークンをランダムにマスクし、下記のようなデータを生成します。

説明変数:
メロス は [MASK] した

目的変数:
激怒

そのため、教師データが必要ありません。学習用データを自分自身から作ることができます。

そのため、膨大でかつ品質が担保されたデータがあれば、人に比肩するような精度を持つモデルが作れた理由の一端として、私はMaskedLMが非常に重要なのではないかと考えています。

※正確な説明はBERTによる自然言語処理入門 ―Transformersを使った実践プログラミング―など専門書をご覧ください。

さいごに

今回はBERTでのMaskedLMの学習方法となぜ重要なのかを確認しました。

BERTの提案は2019年ですので、2022年まででまた大きく技術が進歩したと考えています。

ただ、MaskedLMをはじめとした学習方法が持ち込んだ問題点としては、膨大なデータがあれば、良いモデルは学習できる・・・が学習できる環境は整っているのだろうか?ということです。巨大な計算リソースがなければ大本となるモデルは作れない、つまり、大企業や潤沢な研究資金・資材が得られない環境であれば、新しいことができなくなるのではないかということです。その分、転移学習などで利用は簡単になりましたが・・・。

今回、GitHubで示したコードの条件下(CPU利用)でも、走れメロスのみで30分近くの学習時間がかかりました。計算資源がディープラーニングの制約にのような記事も散見されます。

研究の流れとして、精度がある程度満たされれば計算速度が追及され、計算速度が要求以上になれば精度を求める・・・のような揺り戻しは来ると思います。

機械学習系のコンサルタントとして、今後もBERTを追っていきたいと思います。

以上、れいでした。

おまけ

MaskedLMの手法改善

私が思うに、MaskedLMの改善手法として下記がありそうだなと考えていました。
1・Mask割合を変動させる
2・復元抽出を行って再度Maskしなおす
今回は、(1)が論文において実証されたようです。

【Meisyo】ステータスバランス調整

はじめに

Meisyo+ お知らせ ステータスバランス調整予定のお知らせの詳細を記載します。

下記の順番で対処を行いました。
②本塁打率が高すぎる。
①打率平均が低すぎる。
③長打(二塁打・三塁打)が少なすぎる。

問題対処

まずは②本塁打率が多すぎる問題に対処しました。

スイングスピード

一律に低下させました。
y = ax + bで定義しており、x=パワーです。
係数のaも下げましたが、切片のbも低下させました。その分、パワーが高い選手も低い選手も打球速度が下がり、打球が飛びにくくなりました。

球場

現在3球場あり(大きさは公式戦>練習試合リーグ用>その他)ます。
その他球場が小さかった(=本塁打が出やすい)ので、練習試合リーグ用に準ずる程度まで大きくしました。

守備速度

打球速度が下がると何が起こるか。

ヒットが出なくなります。(Meisyo+でも)統一球問題ですね。

お知らせの内容の通り、Meisyo+は①打率低い問題も抱えているので、これは困った・・・。そのため、守備速度を低下させました。

以前は外野手・内野手が同じ守備速度でした。
今回から全体的に速度を落とし、相対速度は外野手>内野手としました。内野手の方が守備範囲が比較的狭いため、そのようにしました。

その結果、打率が仕様調整以前より10%UPすることとなりました。
また、③長打(二塁打・三塁打)が少なすぎる。については、ある程度調整できたので大きく補正はしていません。

調整結果

変更後も三塁打は少ないものの、二塁打・本塁打はある程度順当に設定できたと思われます。

こうしてみてみると、甲子園はかなり打高なんですね・・・。

データ集計について

変更を行う際はデータ集計を行っています。

その方法は、オフライン上で、総合力上位のある高校 vs その他選択された高校たちを使った練習試合の繰り返しで求めています。

そのため、今回の調整は総合力B~Sのユーザーへの影響はある程度わかりますが、それ以外への影響は未知数です。

ただ、実験していて面白いなと思うことは、勝敗は総合力に相関すると考えられる結果ですが、個別では組み合わせによって全然違う結果となりました。総合力が高くても勝てない不思議です。実現したかった内容がある程度実現できていると思います。

※特にトップレベルのチームでは3または4すくみの状況となっていて、同等のあるチームには9割勝てるけど、他のあるチームには9割負けるみたいなことが観測されています。

おまけ

リアル表示の改善


チャンス時に表示速度低下を追加しました。

高速で飛ばしてみていても、チャンス時だけはちゃんと見たいという人がいるはずだということで実装しました。

併せて、リアル表示の関連ログを取得するようにしました。リアル表示のボタンをクリックしたか、どういう設定で見ているか等をログにとることで、さらなる改善を目指します。

ケガ状態の仕様変更

ケガ状態では、やる気と体力が自動的に0になっていました。

改善後は、やる気がケガ前の数値で保持されるようになります、

打球分析βの実装


打球分析データを取得するようにしました。

そのデータ(打球の1バウンド目のみ表示)を元に、守備位置(全体設定・対チーム・対個人)を設定できるようにする予定です。いつになるかわかりませんが。

とはいえ、試しで取得した全体の打球分布(上記画像)はかなり一様分布なので、パワーが低い選手では前に守らせるくらいしかなさそうな気もしますが…。

※捕手・一塁・三塁付近で打球が多くなっているのは、カットミスでの打球だと思われます。

ストーリーの追記

5章、7章にストーリー進行上重要な追記を行いました。

さいごに

今回のように仕様変更する際は、データを元に意思決定しています。
とはいえ、調整部分は多分に個人の意見や思い込みが入る余地がありますが…。

また、今回のアップデートはユーザー数増加のために、SEO(Search Engine Optimization)のためにも行いました。ログインしていなくても見れるページ数を多くしたため、検索クローラーに対する情報量が多くなり、検索のより上位に表示されることを試みました。

その他の方法を考えると、多くの人に、ブログやSNSで紹介してもらえれば知名度が上がるので、よりユーザー数(戦略の数)が増えて、攻略が楽しいです。ぜひ一言でもSNSなどにコメントいただきたいですね~。プラス・マイナスどちらでも歓迎です!

その他、変更してほしい点があればご連絡ください。

2022年 プログラミング初心者におすすめのPCと備品

はじめに

プログラミング始めてみたい(始めてる)けど、パソコン用意したいなあと思った時、ググってみるといろいろ情報がありすぎて困ったあなた。

PC・プログラミング歴20年以上私が見るに、アフィリエイト(個人で行う企業広告)に毒された記事群が散見されました。困りますよね。。

これ・・・初心者は判断できないのでは?ということでアフィリエイトなしの記事を書きました。

用意したい金額の前提として、下記のように読者を想定しています。

リーズナブルにプログラミングの初歩を学びたいなら。新品価格であれば3万円程度で十分です。

プログラミング楽しいな!プログラミングをより詳しく学びたいという方は、5万円くらいで十分です。10万円程度のPCも紹介していますが、ガッツリやりたい、趣味になってしまった人向けです。

注意点としては、皆が知っているような日系大手メーカーのPCは残念成分(価格、性能、ソフト他)が多分に含まれています。最近の店頭に出ている商品でも、不要なソフトがてんこ盛りなので買う意味を個人的には感じません。

話は少し変わりますが、PCには備品が必要です。なぜなら、備品によって作業効率が段違いになります。備品はソフトとデバイスを紹介します。

ソフトは、私の専門でデータサイエンス(統計・機械学習)で必要なものをピックアップしました。ふつうにプログラミングをする分には不要です。Excel(エクセルやCSVファイルに対応)、PowerPoint(分析では使わないが報告には必要)があればOKです。

デバイスは、作業や学習効率アップのために必要であれば購入ください。

それでは紹介します。

PC

まとめ
・OS:Windows
・大きさ:14型
・性能:要注意

OS

Windowsが実用上良いです。日本のITの現場において、利用環境がWindows準拠が多いからです。

バージョンとしてはWindows10でOKで、Windows11である必要はないです。

本格的なプログラミングをする場合は、MacやUbuntu等になる可能性もありますが、その際は知識がついているので判断が可能になります。

大きさ

14型かそれ以下がおすすめです。

14型はノートPCとしては個人の感想ですが大きめに感じます。17型は大きすぎる可能性があります。カバンによく入れるなら13型以下にすべきでしょう。

ここは特に、家電量販店で見て触れて決めてほしいところです。

性能

最重要ポイントです。高性能と銘打って売られているPCも実は・・・ということは多くあります。

CPU(処理装置)、メモリ(作業台の大きさ)、HDD(記憶装置)、グラフィックカード(画面描写の速さ)の視点で解説します。

CPU

インテルの第10世代i5で十分です。インテルHPに記載があれば問題ありません。比較サイトもありますので、よさそうなPCまたはCPUがあれば自分で調べてみてください。

メモリ

最低8GBは欲しいです。16GBはそこまで必要に感じません。ただし、4GBだとWindowsの動作だけでカクカクになる場合があります。

HDD(記憶装置)

最低250GB欲しいです。Windowsの場合、OSち基礎ソフトで150GBくらいは使います。

より読み込み・書き込みが高速な種類(SSD)もあり、立ち上げが早いなどのメリットがあります。ただ、プログラミング初心者がそこまでこだわる必要あるか?とは感じますね。筆者はこだわっているのでSSDですが。

グラフィックカード

ノートPCで一般的に採用されているオンボード型で問題ありません。
動画編集、深層学習のモデル作成、ゲームをしないのであればGPUは不要です。

おすすめPC(例)

IdeaPad Slim 350i 14

64,130円

上記に書いてある条件はほぼすべて満たしています。
詳しくは、レノボのHPをご覧ください。

THIRDWAVE DX-T7

96,580円

ちょっとハイエンドよりでプログラミングをガッツリする人向け。これ以上ははっきり言って不要です。よりローエンドのPCもあります。
詳しくは、ドスパラのHPをご覧ください。

より見た目重視なら、下記がおすすめです。ただ、値段が高いですね・・・。
Microsoft Surface
Huawei matebook X Pro

ソフト

基本的にMicrosoft Officeで良いですが、PCによって同梱品が変わります。基準価格を知ってお得に購入したいですね。

Microsoft Office Home

買い切り:38,284円
高い・・・。Word、Excel、PowerPointが含まれています。

Microsoft 365 Personal

サブスク:1,284円/月(12,840円/年)
Word、Excel、PowerPointが含まれています。

高いなあと思う方は、下記ソフトをお勧めします。

KING SOFT WPS Office 2

買い切り:5,690円
Word、Excel、PowerPointの代用アプリ。操作性は少し違いますが、機能は十分です。

Apache Open Office

はっきり言ってお勧めしません。
Officeと(互換性があると言っているが)互換性がなく使いづらい。エクセルやパワポをOffice純正からOpenOfficeに変換すると元に戻せないことも・・・。

デバイス

モニタ

デュアルモニタに利用します。作業効率UPならこれが一番です。作業机における大きさで安いモノで十分OK。

マウス

ノートPCにもあった方が良いです。キーボードのコマンドで高速に操作できる人はごくわずかです。

キーボード

不要です。気に入ったキーボードがついてるノートPCを買うべし。

こだわるならデスクトップPCにした方が安上がりかもしれません。

さいごに

PCのスペックはいろいろあって困りますよね。今回は最低限、プログラミングができる内容で設定しました。

最後は、店頭に行き、フィーリングが合ったものを買うといいと思います。長い時間使うので、「おすすめと言われていたから」では続かない可能性が高いです。ストレスも減りますし。

それでは皆様、良いプログラミングライフを!