虚苦心観察ブログ

ブログ管理者である虚苦心が私利私欲に基づいて書いているブログです。主にガジェットのレビューだったり、画像処理のことだったりを記事にしています。

FlutterのWidgetについて勉強を始める

FlutterのUI作成の特徴

画面を表示

  • すべてコードで書く
  • Widgetの構成単位が細かい
  • AndroidでいうところのLayoutに当たるものがほとんど。
  • StatelessWidgetとStateWidgetがある
    • まだ理解していない
  • Androidでいうところのlayout_gravityやgravityもWidgetとして提供されている
  • とてもシンプルなデコレーションパターンな印象
  • それ故にコード量は多い
  • しかし、それを補って余りある強力なUI表現が得られる
  • UIをコードに落とし込むときも単純な考え方で記述できるので苦労しなさそう
  • Androidでよくある「え、標準UIでこれができないの?」がほとんどない気がする
    • Androidの標準のViewは機能が豊富過ぎるわりに必要な機能を網羅できていなくて使いにくかった

画像を表示

  • ローカルで表示する場合
    • Image.asset(<画像のパス>)で表示
    • プロジェクト直下にディレクトリを作成。名前はなんでもOK
    • 表示したい画像が入ったディレクトリはpubspec.yamlに記載
    • ディレクトリだけでなくファイルも指定可能
flutter:
  assets:
    - assets/ # ディレクトリ指定
    - assets/images/image.jpg
  • ネットワーク経由で表示する場合
    • Image.network()で表示できる
  • よくある角丸はClipRRect Widgetの子に指定することで簡単に

リスト表示

ListView.builder(
  itemBuilder: (buildContext, index) => Text("current $index"),
  itemCount: 10,
)
  • 書き方はRecyclerViewととても似ている。

Flutterを始める

つまづく

UIをぽちぽちしてプロジェクトを作ってRunしただけなのに動かない・・・

やったこと

Androidアプリ開発で陥りやすいバグやあまり知られてないけど有益な実装方法をまとめる。

忘れることが多いのでちょくちょくメモしていく。

  • ダウンロードは別スレッドで行う。メインスレッドを止めてしまうためANRが発生する可能性が高い。(activity,service)
  • 画像表示は縮小した画像で行う。Android8から大きすぎる画像を表示しようとすると例外が発生する場合がある。また縮小することで大幅な高速化が可能。ただし、縮小する方法は考えること。単純な実装だとむしろ重くなってしまう場合がある。
  • recyclerviewの各ViewHolderには直接listenerを実装せずにリストの各要素にそれぞれリスナーを保持する
  • ViewHolderで画像表示やネットワーク通信をするときはViewHolderが再利用されるときに画像の解放やネットワーク通信のキャンセルをすること。
  • mvpパターンはandroidフレームワークとロジックを分離するための雛形。
  • ネットワーク通信は通信内容の処理のほかに通信失敗の処理も必須。ネットワーク通信が失敗する可能性は0ではない。
  • FcmIdRecieverServiceはDeprecated。FirebaseMessageRecieverServiceでFCMトークンの更新処理を行うこと。
  • PendingIntentに設定したTaskBackStackが反映されない場合はアプリを再インストールすると治ることがある。アプリのキャッシュやデータを削除しても治らない。(このことからわかるようにAndroidにはアプリから触れることのできないアプリにかかわる領域がある)
  • Retrofit2のマルチパートでファイルとその他をアップロードする場合はPartとPartMapだけしか使えない。PartMapはBodyにハッシュを指定する感覚で使える。
  • KotlinからRetrofit2でPartMapを使う場合、RequestBodyがワイルドカードで表現されてしまう場合がある。その時はPartMapのValueに@JvmSuppressWildcardsを付ける。参照
  • EditTextのimeOptionsにactionSearchを指定してはいけない。ソフトウェアキーボードからのエンターキーのイベントはsetOnEditorActionListenerで設定できるが、ハードウェアキーボードからのイベントは処理できずアプリが落ちてしまう。単純に検索窓として使いたいならSupport LibraryのSearchViewを使おう。継承することで見た目をカスタマイズすることもできる。どうしてもEditTextを使いたい場合はimeOptionsにactionDoneを設定すること。見た目は検索と異なってしまうが、落ちることはない。
  • RecyclerViewのOnScrollListenerに依存した初期化処理を書いてはいけない。タイミングによってイベントが発火しないときがある。なんとかして初期化処理は能動的に実行すること
  • RecyclerViewのスクロール位置復元は明示的に行う必要がある。RecyclerViewのスクロール位置を保存したいに書かれていることをonSaveInstanceStateで保存し、onRestoreInstanceStateで復元する。
  • RecyclerViewの無限スクロールにはこれがよい。無限スクロールの状態をリセットし、一旦停止した無限スクロールを再開することもできる。使い方もしっかり書かれている。
  • アスペクト比を固定するにはConstraintLayoutが使える。stackoverflow よくあるアスペクト比固定のImaveViewもImageViewを拡張せずに使うことができる。
  • デバッグ時はアプリの起動と再起動を開発者オプションの「アクティビティを保持しない」の有効無効を切り替えて確認すること。アクティビティが一時的に破棄され、復元された場合のチェックが抜けていることがよくある。
  • Maps SDK for Androidで地図をLite Modeで表示するにはViewのサイズが完全に固定されている必要がある。固定されない場合、地図が表示されない、タップしたときだけ地図が表示される、など表示崩れが発生する。サンプルコードのLayoutをそのまま使ってもうまく動作しないことがある。その時は、Viewのサイズが変わらないよう、不要な要素を排除、もしくはサイズが変わらないように変更しよう。
  • Activity間のデータやイベントのやり取りはやってはいけない。短時間での使用ではモンダイなさそうでも、長期間利用した場合にAndroidのライフサイクルと合わず、おかしな動作をしてしまう可能性が高い。
  • 逆の考えでActivityの実装を制限してデータやイベントのやり取りを可能にすることはできるかもしれないが、普通ではない実装であるが故に普通の実装をしようとしたときに後悔することになりかねない。
  • FLAG_ACTIVITY_REORDER_TO_FRONTを使って複数のActivityを切り替えて表示すると、オーバービュー画面(アプリの使用履歴が見れる画面)で最後に表示されたActivityのスクリーンショットが表示されず、どこかのタイミングで表示されたActivityの画面になってしまうことがある。Android 8のNexus 5Xで確認。改修方法はわからず。
  • RecyclerView and java.lang.IndexOutOfBoundsException: Inconsistency detected. Invalid view holder adapter positionViewHolder のようなエラーが出た場合はAdapterの更新でnotifyItemRangeChangedが呼び出されているところをチェックしよう。StackOverFlowで説明されているエラーとは異なる。このエラーはsupport library のバージョンが23.1.1で発生していたもので、すでに修正済みである。ではなぜ同様のエラーが発生しているかというと、おそらくRecyclerView.AdapterのnotifyItemRangeChangedを呼び出していながら表示要素が減っていることが原因だ。notifyItemRangeChangedは表示要素が増えた場合は対応できるが、減った場合は対応できていないようだ。
  • Activity間での変更通知はLocalBroadCastManagerを使うことができる。しかし、これは本来的な使い方ではない。本来はActivityとServiceの通信で使うものである。よく検討し、ライフサイクルを考慮した実装をしなければただのバグの温床にしかならない。あるデータの変更通知を行いたいのであれば、データベースに変更日時を記録すれば、データの取得日時がそれよりも後か前かで更新すべきかどうか判断できる。もしくはデータをServiceなどで一括管理するなどが必要だろう。
  • 通知からCustomChromeTabを直接起動するとBackStackBuilderで構築したバックスタックが反映されず、CustomChromeTabが終了するとアプリも終了してしまう場合がある。Android API Level 25のバーチャルマシンで確認した。21、23、25、27と確認してのことである。解決方法はIntent.ACTIVITY_FLAG_NO_HISTORYで起動した何もしないActivityからCustomChromeTabを起動する。これでAPI LEVEL 25のバーチャルマシンでも問題なくBackstackが構築された。
  • IntentはParcelを継承しているのでそのままIntentにputExtraで保存することができる。これを利用すると何もしないACTIVITY_FLAG_NO_HISTORYで起動するActivityにIntentをExtraとして渡すことでRedirectのような動作を実現することができる。
  • Activity遷移のAnimationはNULLにしない。空のAnimationをセットすること。端末によってはNULLにしたことによってActivityの表示前に黒い画面が一瞬表示されてしまう。
  • RecyclerViewの各アイテムにもAnimationがある。ItemAnimatorを使って実現している。

sklearnのChange Logまとめ

バージョンアップでモジュールのパスなり引数の名前が変わってしまっていて、本を使って勉強をしているときに困ることが多いのでメモ。

  • パス
old path new path changed
sklearn.cross_validation sklearn.model_selection 0.18.0
sklearn.grid_search sklearn.model_selection 0.18.0
sklearn.learning_curve sklearn.model_selection 0.18.0
  • 引数 | function | old argument | new argument | changed | | :-- | :-- | :-- |

随時追加していこうかと思います。 載っていないものがあればコメントいただければ追記します。

Visual Studio 2015 で LLVM/Clang 3.6.2 をコンパイルする

LLVM/Clangはコンパイルのハードルが高いと思っていたのですが、そこまで大変ではないらしいことを知りました。

KMC Staff Blog:LLVM/Clang 3.6.2をVisualStudio 2013 Expressでビルド

上記の記事よりVisual Studioのバージョンが新しいのですが、コンパイルを試みました。 そしたらなぜかコンパイルエラーが。

Severity Code Description Project File Line Error C2664 'void llvm::BitstreamWriter::EmitRecordWithBlob<uint64_t>(unsigned int,llvm::SmallVectorImpl<uint64_t> &,llvm::StringRef)': cannot convert argument 3 from 'clang::serialization::DeclOffset *' to 'llvm::StringRef' clangSerialization  C:\Users\kyokushin\ProgramingLibraries\llvm-3.6.2.src\tools\clang\lib\Serialization\ASTWriter.cpp   2815

Visual Studioのバグでdataというテンプレート関数が別のインクルードファイルに含まれているテンプレート関数を呼び出してしまっているらしい。
エラー文からいくつかキーワードを引っ張ってググってみるとそれっぽいバグ報告を見つけた。

Bug 25650 – Cannot compile clang 3.7.0 with msvc 14 (2015)

このバグ報告によれば新しいClangでは直っているようです。なのでClang3.8.1のソースをダウンロードしてどのような変更があったのかを直接みました。 Clang3.8.1ではdataというテンプレート関数をbytesという名前に変更していることがわかり、同様の変更を加えてみました。

変更するファイルは一つ。
clang/lib/Serialization/ASTWriter.cpp

ファイル先頭にあるテンプレート関数dataを引数などはそのままで関数名のみをbytesに変更します。 同じファイル内でテンプレート関数dataを呼び出しているところをbytesに変更します。 一括置換で簡単に変更することができますが、dataというメソッド名も存在するので注意。 先頭にスペースを入れた" data("を検索文字列にすると対象となるテンプレート関数だけが置換できます。

変更後コンパイルしてみましたが、エラーなくコンパイルが終了しました。
こういうことがあると本当にVisual Studioはクソだなーって感じますね。

へっぽこ知識で作る画像処理プログラム 「自炊本のページ抜けチェック」 3.5日目

はじめに

2.5日目につづき今回もPython版を作成したので解説していこうと思います。 ただし、アルゴリズムについてはC++版を参考にしてもらうことにして、ここではC++版とPython版での違いについて紹介していきます。

C++Pythonでのピクセルアクセス方法の違い

C++ではOpenCVのクラスcv::Matに画像データを保持しますが、Pythonにはcv::Matは存在しません。
では何を使うのかというと、Python数値計算ライブラリであるnumpyのndarrayを利用します。

numpyのデータ構造の一つであるndarrayはC++のcv::Matに比べてかなり柔軟な書き方ができます。
例えば今回の場合だと、文字のない範囲を塗りつぶす、という処理を行っているわけですが、C++版とPython版を比べてみましょう。

C++

//文字のない範囲を3チャンネルの原画像に書き込む
    for (size_t i = 0; i < verticalRanges.size(); i++){
        Range& r = verticalRanges[i];
        //文字のない範囲を3チャンネルの原画像から切り出す
        cv::Rect rect(0, r.start, verticalRangeDst.cols, r.end - r.start + 1);
        cv::Mat roi(verticalRangeDst, rect);
        //切り出した画像を1色で塗りつぶす
        roi = cv::Scalar(0, 0, 255);
    }

Python

    for r in verticalRanges:
        verticalRangeDst[r.start:r.end, :, :] = (0, 0, 255)

C++版はちょっと冗長な書き方をしていますが、、、どうでしょう。Python版はものすごくシンプルで、なんと矩形の塗りつぶしだけなら1行で書けてしまいます!
ndarrayでの各ピクセルへのアクセス方法はPythonの配列と同等の方法になります。
そして":"はPythonのスライスと呼ばれる書き方です。 スライスを使うことでアクセスしたい配列の範囲を指定することができます。 スライスで指定した範囲に対して上記のように代入することができるため、C++に比べて非常に柔軟な書き方が可能です。

別の例として1チャンネルの画像から3チャンネルの画像を作る方法。cv::mergeを使わずに実装することができます。

color = numpy.zeros((gray.shape[0], gray.shape[1], 3), dtype=gray.dtype)
color[:,:,0] = gray
color[:,:,1] = gray
color[:,:,2] = gray

ただPython版のOpenCVにはcv2.merge関数が用意されていてそっちの方が簡単です。あくまで参考。

Pythonではrangeという組み込み関数が存在するので注意

Python版を作るうえで今回注意した点が一つあります。 それはrangeという名前の関数や変数を作らないことです。 なぜかというと、Pythonにはrangeという組み込み関数が存在し、誤って上書きしてしまうとrange関数を使っている場所で予期しない動作をする可能性があります。 今回の場合はrangeという変数名の代わりに頭文字のrをつかって対処しました。 PythonC++に比べて柔軟な書き方が出来て便利な反面、こういった落とし穴もあるので注意が必要です。

出来上がったプログラム

今回もタグを用意しています。

github.com

今回はここまで。

へっぽこ知識で作る画像処理プログラム 「自炊本のページ抜けチェック」 3日目

はじめに

前回の2日目では小説の画像に二値化を行い、積分画像を生成しました。
生成したところ、なんだか文字のある領域とない領域が判別出来そうだ、というところまでわかりました。

今回は前回生成した積分画像から文字のある領域とない領域を実際に判別してみようと思います。

文字のない領域の見つけ方

2日目の復習

2日目でもある程度説明しましたが、作成した積分画像からは以下のことがわかります f:id:ysuzuki116:20160605232954p:plain:w250

積分画像の下端(青)の横方向の変化を調べ、値が変化していない領域を調べれば以下のようになり、
f:id:ysuzuki116:20160608213645p:plain:w200
積分画像の右端(赤)の縦方向の変化を調べ、値が変化していない領域を調べれば以下のようになるはずです。
f:id:ysuzuki116:20160608213719p:plain:w200

さらにこの2つの調べた結果を合わせると、
f:id:ysuzuki116:20160608214131p:plain
となって文字のある領域と文字のない領域を判別できるはず!

なんとなく出来そうなのでプログラムを書いてみましょう。 書かなきゃわからないですし、2日目の結果からそれっぽい結果も得られていますし、何とかなるでしょう。

はじめてのピクセル

2日目ではOpenCVに用意されている関数を組み合わせてプログラムを書いていました。 しかし今回は画像を構成しているピクセルを直接触ります。

ポインタを扱うため、アクセスする範囲に注意が必要です。

プログラム自体はとても簡単で、難しいアルゴリズムは使わず、配列の1個前の要素の値と現在の要素の値を比較するだけです。
1個前の要素の値と現在の要素の値を比較し、値が同じなら白(255)、値が異なれば黒(0)とします。 値が同じということは変化のない領域、つまり文字のない領域のはずです。逆に値が異なるということは変化がある、つまり文字のある領域となるはずです。

まずは横方向だけ調べてみる

早速ですがプログラムです。ピクセルにアクセスするプログラムは長く読みにくくなりがちなので関数を定義しています。

//横方向をしらべる
//値が同じ連続領域を255、それ以外を0とするcv::Matを返す。
cv::Mat findSameValueHorizontal(const cv::Mat& src)
{
    //積分画像が欲しい
    CV_Assert(src.type() == CV_32SC1);

    //結果は白黒でいいから8bitの1チャンネル。しかも1行だけ
    cv::Mat dst(1, src.cols, CV_8UC1);
    //積分画像はintなのでint型のポインタを取得。下端なので位置はsrc.rows - 1
    const int* srcLine = src.ptr<int>(src.rows - 1);
    //結果は白黒の8bitなのでそのまま受け取っている。1行しかないので位置は0
    unsigned char* dstLine = dst.ptr(0);

    //とりあえず結果画像の一番左だけは右隣と比較する
    dstLine[0] = (srcLine[0] == srcLine[1]) * 255;
    for (int i = 1; i < src.cols; i++){
        //左隣と比較
        //比較結果(0または1)なので255を掛けて見えるように
        dstLine[i] = (srcLine[i] == srcLine[i-1]) * 255;
    }

    return dst;
}

ポインタの理解が浅いと読むのが大変かもしれませんが、難しい処理はないと思います。
ポインタを取得するメソッドptrは引数に与えた行の先頭を指すポインタを返します。 難しく書いてしまっていますが、要は積分画像の下端の1行分の配列と結果画像の1行分の配列を取ってきている感じです。

そして取ってきたのは配列なのでfor文で値を端から端まで比較し、ひとつ前の値と同じだったら白(255)、違かったら黒(0)に結果画像のピクセルをするだけです。
結果画像は以下になります。わかりやすいように枠をつけています。

f:id:ysuzuki116:20160610233509p:plain

バグではありません。結果画像のdstは1行の画像なのでこうなってしまいます。さすがに分かりにくいですね。 修正したものが以下になります。パワポで引き伸ばしました。

f:id:ysuzuki116:20160610233525p:plain

処理している原画像はというとこれでした。

f:id:ysuzuki116:20160610230658j:plain

非常にそれっぽい!(?)成功してそうです。プログラムを修正して原画像に重ね合わせて表示してみましょう。

先ほど載せたプログラムは画像を作るのではなく、文字のない領域を返すように変更しました。

//横方向をしらべる
//文字のない範囲をvectorで返す
void findSameValueHorizontal(const cv::Mat& src, std::vector<Range>& ranges)
{
    //積分画像が欲しい
    CV_Assert(src.type() == CV_32SC1);

    //値が入っているかもしれないので空にする
    ranges.clear();

    //積分画像はintなのでint型のポインタを取得。下端なので位置はsrc.rows - 1
    const int* srcLine = src.ptr<int>(src.rows - 1);

    Range range;
    for (int i = 1; i < src.cols; i++){
        //左隣と同じ値
        bool sameValue = srcLine[i] == srcLine[i - 1];
        //左隣と同じ値 かつ 範囲のstartが初期値(-1)のとき
        if (sameValue && range.start < 0){
            //文字のない範囲の始まり
            range.start = i - 1;
        }
        //左隣と違う値 かつ 範囲のstartが代入済み
        else if (!sameValue && range.start >= 0){
            
            //文字のない範囲の終わり
            range.end = i - 1;
            //結果として保存
            ranges.push_back(range);
            //文字のない範囲を初期値に戻す
            range.start = -1;
            range.end = -1;
        }
    }
    //最後の範囲が画像の右端まである場合はfor文を抜けてから結果を保存する
    //文字のない範囲のstartは代入済み かつ 範囲のendは初期値のとき
    if (range.start >= 0 && range.end < 0){
        range.end = src.cols - 1;
        ranges.push_back(range);
    }
}

さらに文字のない範囲を使って原画像に色を塗ります。

 //文字のない範囲を受け取る変数
    vector<Range> horizontalRanges;
    findSameValueHorizontal(integral, horizontalRanges);

    //文字のない範囲を書き込む画像
    cv::Mat horizontalRangeDst;
    //1チャンネルの原画像から3チャンネルの画像を作る
    cv::Mat srcArray[] = {image, image, image};
    cv::merge(srcArray, 3, horizontalRangeDst);

    //文字のない範囲を3チャンネルの原画像に書き込む
    for (size_t i = 0; i < horizontalRanges.size(); i++){
        Range& r = horizontalRanges[i];
        //文字のない範囲を3チャンネルの原画像から切り出す
        cv::Rect rect(r.start, 0, r.end - r.start + 1, horizontalRangeDst.rows);
        cv::Mat roi(horizontalRangeDst, rect);
        //切り出した画像を1色で塗りつぶす
        roi = cv::Scalar(240, 176, 0);
    }

結果は。

f:id:ysuzuki116:20160611001819p:plain

出来てるみたいですね!

縦方向も調べてみる

同様に縦方向も調べてみます。

 //文字のない範囲を受け取る変数
    vector<Range> verticalRanges;
    findSameValueVertival(integral, verticalRanges);

    //文字のない範囲を書き込む画像
    cv::Mat verticalRangeDst;
    //1チャンネルの原画像から3チャンネルの画像を作る
    cv::merge(srcArray, 3, verticalRangeDst);

    //文字のない範囲を3チャンネルの原画像に書き込む
    for (size_t i = 0; i < verticalRanges.size(); i++){
        Range& r = verticalRanges[i];
        //文字のない範囲を3チャンネルの原画像から切り出す
        cv::Rect rect(0, r.start, verticalRangeDst.cols, r.end - r.start + 1);
        cv::Mat roi(verticalRangeDst, rect);
        //切り出した画像を1色で塗りつぶす
        roi = cv::Scalar(0, 0, 255);
    }

結果はこちら。

f:id:ysuzuki116:20160615205102p:plain

縦方向も大丈夫そう!

縦横の結果を重ねる

せっかくなので縦方向の結果と横方向の結果を重ね合わせてみましょう。 横方向の結果画像に縦方向の結果を上書きしてしまいましょう。

//横方向の結果と縦方向の結果を合わせる
    for (size_t i = 0; i < verticalRanges.size(); i++){
        Range& r = verticalRanges[i];
        //文字のない範囲を3チャンネルの原画像から切り出す
        cv::Rect rect(0, r.start, horizontalRangeDst.cols, r.end - r.start + 1);
        cv::Mat roi(horizontalRangeDst, rect);
        //切り出した画像を1色で塗りつぶす
        roi = cv::Scalar(0, 0, 255);
    }

結果はこちら。

f:id:ysuzuki116:20160615205803p:plain

色合いが悪くて目が痛くなりそうですが、うまくいってるみたいです。 よしよし。

3日目まとめ

今回は実際に文字のない領域を検出するところまでプログラムを作成することができました。 これが出来ないとページの抜けをチェックするためのページ番号の抜き出しができません。だからこれができることは重要です。 出来なかったら処理方法を見直さなければなりませんからね。

今回の成果物はこちら

github.com

予告

次回は実際にページ番号を切り抜くところまで作ってみようと思います。

今回までのプログラムをそのまま使って作れそうですが、そうもいかない気がしています。 まずはリファクタリングを行い、そのあとに本題のページ番号の切り抜きを行おうと思います。

更新2週間に一回になりそう。。