Flutterで2つの画像を合成する

Flutterで2枚の画像をこんな感じで合成したかったのですが、試行錯誤することになったので備忘録として残します。 f:id:chari_ngo:20200410225618p:plain

画像表示おさらい

Flutterはこんな感じにするだけで簡単に画像を表示することができるのでした。

Image.asset("assets/image.png")

f:id:chari_ngo:20200410230436p:plain
オリジナル画像

また画像に対して色を合成することもできます。 BlendModeに関しては一般的なものなので今回は詳しく触れませんが、公式ドキュメント が詳しいです。

Image.asset(
  "assets/gear.png",
  color: Colors.green,
  colorBlendMode: BlendMode.srcIn,
)

f:id:chari_ngo:20200410230553p:plain
色を合成した画像

正直ここまではAndoirdStudioのサジェストに従っていけば難なくこなせるでしょう。

画像どうしを合成するには

UIに凝ってくるとある画像に対して単色ではなくテクスチャを合成したい要求が出てくるかもしれません。先の例でいう歯車に対して緑色を合成するように以下のテクスチャの合成方法を考えます。

f:id:chari_ngo:20200410230945p:plain
合成したいテクスチャ

画像と色の合成に関しはFlutterのWidgetクラスの内部で行われているため、単純に考えると自前でCustomPainterなどを実装しCanvasに対する描画するロジックを書かねばなりません。 これははっきり言って面倒で運用コストが掛かるので他の方法を考える必要があります。

そこでFlutterが公式に提供しているShaderMaskというクラスを利用します。このクラスではシェーダを使ってマスク処理を行ってくれます。以下の動画の例ではカラーグラデーションやアニメーションなどを定義し、シェーダに変換することでWidgetに対してマスク処理を適用しています。 www.youtube.com

つまり画像データもカラーグラデーションなどのベクタデータ同様に、シェーダに変換できればShaderMaskクラスを利用して2つの画像を合成することができます。

画像を合成方法と結果

まず画像をbyteデータとして読み込み、画像としてデコードします。ここではassets配下にあるpng画像と取り込んでいます。

Future<ui.Image> _loadPngImage(String assetPath) async {
  final byteData = await rootBundle.load(assetPath);
  return decodeImageFromList(byteData.buffer.asUint8List());
}

画像がSVGの場合はflutter_svg を利用し以下のように画像を読み込むことができます。

Future<ui.Image> _loadSvgImage(String assetPath) async {
  final rawSvg = await rootBundle.loadString(assetPath);
  final DrawableRoot svgRoot = await svg.fromSvgString(rawSvg, rawSvg);
  final Picture picture = svgRoot.toPicture();
  // 元画像のサイズに合わせる
  return picture.toImage(32, 32);
}

画像をFutureクラスで受けとり、画像の読み込みに成功した場合はシェーダに変換し、ShaderMaskに設定します。ShaderMaskのblendModeは単色合成のときと同様に扱えます。シェーダに変換されたテクスチャが単色に相当します。

FutureBuilder(
    future: _loadPngImage("assets/texture.png"),
    builder: (context, AsyncSnapshot<ui.Image> image) {
      return (image.hasData)
          ? ShaderMask(
              child: Image.asset("assets/image.png"),
              shaderCallback: (bounds) => ImageShader(
                image.data,
                TileMode.repeated,
                TileMode.repeated,
                Matrix4.identity().storage,
              ),
              blendMode: BlendMode.srcIn,
            )
          : Container();
    }),

このように画像をシェーダに変換することで2つの画像を合成することが可能です。

f:id:chari_ngo:20200410232448p:plain
テクスチャ合成後の画像

全体のサンプルプロジェクトはこちらに置いていますので、参考にしていただければと思います。

github.com

おわりに

Android(Java)時代のつらさしか知らなかったのですが、Flutterはけっこう楽しいですね

kubectl applyでデプロイする際には容量制限に気をつける

kubenertesを使っていてハマったのでメモ。

kubectl apply -f でエラー

kubectl apply が便利なので何でもかんでもこれでデプロイをしていたが、とあるリソースファイルをデプロイしようとしたところ以下のようなエラーが

$ kubectl apply -f dashboard.yaml
The ConfigMap "dashboard" is invalid: metadata.annotations: Too long: must have at most 262144 characters

原因

kubectl apply ではロールバックなどのために更新前のリソースファイルをmetadata.annotationsに自動的に格納してくれている。metadata.annotationsの容量制限は256KBであるため、本来容量上限が1MBのConfigMapやSecretでも256KBを超えるとkubectl apply では単純にデプロイできないようだ。

github.com

解決策

kubectl applyではなくkubectl createkubectl replace を使おう。今回はCI/CDで使い回したかったので下記のようなコマンドを使うことにした。

$ kubectl replace -f dashboard.yaml || kubectl create -f dashboard.yaml

他にいい方法あったらぜひ教えて下さい 🙏

MavenマルチモジュールプロジェクトでJacocoを使ってカバレッジをまとめて集計

この記事では複数のモジュールから構成されるMavenプロジェクトで、Jacocoを用いテストカバレッジをまとめて集計する方法を紹介します。

Jacocoでカバレッジを取得する

JacocoとはJavaのテストカバレッジ集計ツールです。Mavenプロジェクトの構成が単体のモジュールからなる場合、pom.xml へJacocoプラグインを設定するだけでカバレッジの集計を行ってくれます。おおまかにはjacoco:prepare-agentjacoco.execという実行ファイルを生成、jacoco:reportではこの実行ファイルを用いて集計結果をhtmlなどで生成するという流れです。

<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.4</version>
<executions>
<!--実行ファイル jacoco.execの生成-->
    <execution>
        <id>prepare-agent</id>
        <goals>
            <goal>prepare-agent</goal>
        </goals>
    </execution>
<!--execファイルからレポートを生成-->     
    <execution>
        <id>report</id>
        <phase>prepare-package</phase>
        <goals>
            <goal>report</goal>
        </goals>
    </execution>
</executions>
</plugin>

www.baeldung.com

マルチモジュールプロジェクトでまとめてカバレッジ集計

マルチモジュールのMavenプロジェクトの場合、前項の設定を各モジュールの子pomで設定すると、出力されるレポートが各モジュールに散らばってしまうという問題があります。ここではマルチモジュールの構成として、以下のようなプロジェクトを考えます。

parent/
┣ pom.xml
┣ child1/pom.xml
┗ child2/pom.xml

子pomの設定

各子pomに以下の設定を追加します。jacoco:prepare-agentにより各子モジュールの target/ 以下に jacoco.exec を生成します。シングルモジュールプロジェクトの際に設定していたjacoco:reportがないことに注意してください。これはレポート出力を親pomで一括で実行するためです。

<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.4</version>
<executions>
<!--実行ファイル jacoco.execの生成-->
<!--子ではまだレポートは生成しない-->     
    <execution>
        <id>prepare-agent</id>
        <goals>
            <goal>prepare-agent</goal>
        </goals>
    </execution>
</executions>
</plugin>

親pomの設定

次に親pomの設定を行います。親pomでは子pomで生成した *.exec の実行ファイルからレポートを出力します。dependency内のgroupId, artifactId, versionは子pomで設定している値を入力し、各子モジュールを指定します。これにより、各子で生成したjacoco.execを参照することが可能となります。

次に子で実行していなかった jacoco:report に対応するコマンドの設定を親pomで行います。 親pomでは jacoco:reportに代わり、jacoco:report-aggregateを設定することで、プロジェクト全体のカバレッジレポートをまとめて集計することができます。

    <dependencies>

        <!--child1 の pom.xmlの設定を入力-->     
        <dependency>
            <groupId>com.chari_ngo.sample</groupId>
            <artifactId>child1</artifactId>
            <version>1.0.0-SNAPSHOT</version>
        </dependency>

        <!--child2 の pom.xmlの設定を入力-->     
        <dependency>
            <groupId>com.chari_ngo.sample</groupId>
            <artifactId>child2</artifactId>
            <version>1.0.0-SNAPSHOT</version>
        </dependency>
    </dependencies>

<!--各モジュールのexecファイルから統合されたレポートを生成-->     
    <build>
        <plugins>
            <plugin>
                <groupId>org.jacoco</groupId>
                <artifactId>jacoco-maven-plugin</artifactId>
                <version>0.8.4</version>
                <executions>
                    <execution>
                        <id>report-aggregate</id>
                        <phase>prepare-package</phase>
                        <goals>
                            <goal>report-aggregate</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

カバレッジの出力

pomの設定が完了したら、mvnコマンドを実行することでカバレッジを集計することができます。 parent/target/site/jacoco-aggregate にhtmlファイルとしてカバレッジレポートが生成されます。

$ mvn clean test jacoco:report-aggregate 

複数のGPSログからイイ感じな地図を生成する

はじめに

自転車旅行やトレッキングのような自分の足で移動するのが好きだ。TV番組でいうと水曜どうでしょうが好きなタイプ。観光地や名所などの特定のスポットに行くのも良いけど、個人的には移動そのものが好きだということに気づき位置情報をログとして残すようにしている。その結果大量のGPXファイル(ログデータのファイル)が溜まったので、今までの趣味を振り返るためにも1つの地図上に可視化してみることにしました。

GPSデータを用意する

まず表示する位置情報のデータが必要です。私はスマホやスマートウォッチで日頃から取得しておいてStravaなどで管理しています。最近だと格安かつGPS内臓のスマートウォッチが増えてきているのでおすすめです。

また過去の移動経路を覚えているけど、位置情報のデータがない場合はルートラボ等のWebサービスを利用することでルート情報をGPSで得られるものと同一のログファイルを作成することができます。今回は特にGPX形式の位置情報データを扱います。

Pythonでデータを可視化する

今回は可視化のためにPythonと関連ライブラリを用いて地図を生成します。スクリプトに必要な機能は以下の2点です。

  1. GPXファイルの読み込み
  2. 地図を画像やHTMLとして生成

GPXファイルの読み込み

GPXファイルの扱いですが、gpxpyというライブラリがあるのでこれを使います。

github.com

ライブラリはpipやconda経由で導入可能です。描画には緯度経度がわかれば良いので、単に読み込むだけのサンプルが以下です。

import gpxpy

points = []
gpx_file = open(filename, 'r')
gpx = gpxpy.parse(gpx_file)
for track in gpx.tracks:
    for segment in track.segments:
        for point in segment.points:
            points.append([point.latitude, point.longitude])

画像として地図を生成

次に地図の生成です。最も単純な方法はgpxpyで取得した点をすべてプロットする方法でしょうか。試しにmatplotlibで描画すると下の画像みたいな感じです。大量に位置情報があるのでそれっぽく見えてますが、できたら地図の上に重ねたりズームしたりしたいところです。

f:id:chari_ngo:20181217132303p:plain
matplotlibでGPXファイルを描画

import glob
import matplotlib.pyplot as plt
import gpxpy

lat, lon = [], []

fig = plt.figure(facecolor='black')
ax = plt.Axes(fig, [0., 0., 1., 1.], )
ax.set_aspect('equal')
ax.set_axis_off()
fig.add_axes(ax)

for filename in glob.glob('log/*'):
    gpx_file = open(filename, 'r')
    gpx = gpxpy.parse(gpx_file)
    for track in gpx.tracks:
        for segment in track.segments:
            for point in segmen.points[::50]:
                lat.append(point.latitude)
                lon.append(point.longitude)
    plt.plot(lon, lat, color='cyan', lw=0.5)
    lat, lon = [], []
plt.savefig("result.png", facecolor=fig.get_facecolor(), bbox_inches='tight', pad_inches=0, dpi=300)

イイ感じな地図を生成

画像ではなくHTMLなどでよりリッチな地図が描けないかなと悩んでいたところfoliumというライブラリを見つけました。こちらもpipやcondaで導入可能です。

github.com

一言で言えばGoogleMapのようにマウスで自由に動かせる地図をHTML形式で生成できるライブラリで、たった2行でそんな地図が生成できてしまいます。

map = folium.Map(location=[38.2586, 137.6850], zoom_start=6)
map.save("./map.html")

location が地図の中心, zoom_startが地図の拡大倍率です。

また単純に地図を生成するだけでなく地図に対してマーカや経路などをはじめとしたデータの可視化を簡単に描画することができます。詳細はドキュメントを参照してください。今回は位置情報を線として描画したいのでPolylineというメソッドを使用します。pointsはgpxpyのサンプルで読み込んでいるpointsと同じデータ形式です。

folium.PolyLine(points).add_to(map)

これを使って描画するとこんな感じになります。

f:id:chari_ngo:20181217135546p:plain
foliumでGPSデータを可視化

ソースコードはこんな感じです。マップタイルを変更できるように序盤で設定している以外は先程紹介したgpxpyとfoliumのサンプルを組み合わせるだけです。4重ループがあり頭を抱えたくなりますが、頻繁に実行するスクリプトでもないので今回は目を瞑ってください…。

import glob
import gpxpy
import folium

points = []
# create map
map = folium.Map(location=[38.2586, 137.6850], zoom_start=6)
# add map tiling options
folium.TileLayer('Mapbox Bright').add_to(map)
folium.TileLayer('cartodbdark_matter').add_to(map)
folium.TileLayer('openstreetmap').add_to(map)
folium.LayerControl().add_to(map)
# draw log data
for filename in glob.glob('log/*'):
    gpx_file = open(filename, 'r')
    gpx = gpxpy.parse(gpx_file)
    for track in gpx.tracks:
        for segment in track.segments:
            for point in segment.points:
                points.append([point.latitude, point.longitude])
    folium.PolyLine(points).add_to(my_map)
    points = []
map.save("./map.html")

できた地図を眺める

ここ数年で自転車旅行したルートを可視化してみました。Github Pagesにアップしたのでぜひ見てください。

https://ryoheinagao.github.io/bikelog/

例えば日本列島全体を見てみましょう。淡路島・佐渡ヶ島能登半島などは海岸をなぞるとくっきりとその形が現れています。一方でルートが引かれていない中部地方や九州地方はあまり行っていないことが地図からもわかります。数万kmは自転車で国内を旅行して大体の場所を知り尽くした気になっていましたが、まだまだ行ったところの無い場所が日本にたくさんあるということが実感できます。

f:id:chari_ngo:20181217141050p:plain
全国バージョン

自転車で走ってめちゃくちゃしんどかった長野県の付近をズームしてみると、道がかなりうねうねして険しい山岳であることも位置情報のログから読み取ることもできます。

f:id:chari_ngo:20181217141720p:plain
長野県のあたりは山しかなくて熱中症でぶっ倒れた記憶

北海道のオホーツク海沿いなどは非常に美しい直線です。だんだん書道のように感じてきました。

f:id:chari_ngo:20181217142752p:plain
実際はずっと向かい風で死にそうになったオホーツク海沿いの道

おわりに

いままでの位置情報ログをPythonを使って可視化することができました。自転車旅行のログを可視化することで当時の辛さや景色を思い出すことができ再訪したくなったり、まだ行ったことのない場所を発見できました。是非旅行好きの人同士で地図にログを可視化して、互いに見せあったり当時の思い出を語って欲しいです。