Androidアプリ＆Kotlinでハマった点の技術的原因と対応方法（３．Androidの画像に関する処理）

# ３．*Android*の画像に関する処理
*Android*の画像は、基本的にBitmapを生成・収縮・管理していますが、本アプリのイメージ画像の加工やシームレス合成では、*OpenCV*の機能を利用しています。
*Android*アプリで*OpenCV*を利用するためには、2つの方法が存在します。
1つ目は、*OpenCV Manager*をユーザにインストールさせる方法で、2つ目は、*OpenCV Manager*なしで*OpenCV*の共有ライブラリを静的ビルドする方法です。
1つ目のメリットとしては、共有ライブラリを含まない分、アプリケーションパッケージ（*apk*ファイル）のサイズは小さくなります。デメリットは、*OpenCV Manager*をユーザにインストールさせること、最新バージョンのOpenCVに対応していないことです。
一方で、1つ目と逆になることで2つ目の*apk*ファイルのサイズ問題は、昨今のネット環境でそれほど問題にならないと判断して共有ライブラリを含む方法を採用しました。（クレームが来たら考えます）

本アプリでは、*OpenCV*の*Mat*（1行の2次元行列）を利用するため、*Bitmap*と*Mat*間変換が必要になります。
以前、画面遷移（フラグメント）処理の個所で、端末回転を禁止する代わりに画像を回転させる処理を組み込むことにした旨をお伝えしましたが、イメージ画像のミラーリング処理で課題が発生しました。

## ３．１ *File*クラスの*createTempFile()*(一意なファイル名)処理

### ＜背景＞
本アプリでは、ギャラリーの画像もしくはカメラ撮影のイメージ画像を一時的にファイルとして保存しています。

### ＜課題＞
*API 24/Android 7.0/Nougat*以上で本アプリを実行すると*createTempFile*(一意なファイル名)処理で例外が発生します。

### ＜原因＞
*API 24/Android 7.0/Nougat*以上では、*File.createTempFile()*内の*isInvalid()*以降の処理で例外が発生します。

### ＜対応＞
当初は、*Files.createTempFile()*を利用しようとしましたが、*Files*クラスは*API 26/Android 8.0/Oreo*（*Java SE SDK 7*）以降しか実装されていません。
別に*createTempFile()*に拘ることはなく、*File*クラスをインスタンスする際に絶対時刻を付加して一意なファイル名を作成します。

```Kotlin:一意なファイル名
// ..... 略
	fun getImageFile(context: Context, child: String): File {
		context.filesDir.let { path ->
			if (!path.exists()) path.mkdir()
			return File("$path$child-${System.currentTimeMillis()}$SUFFIX_IMAGE") // $SUFFIX_IMAGE → ".jpg"
		}
	}
// ..... 略
```

## ３．２ *OpenCV*を利用したイメージ画像のミラー処理

### ＜背景＞
本アプリでは、画面遷移後にイメージ画像を貼り付けた後、イメージ画像表示（加工）画面で[ミラー]ボタンをタッチするとイメージ画像のミラー処理を行います。

### ＜課題＞
ミラー処理では、*OpenCV*の*Core.flip()*を利用しますが、何回か繰り返してミラーリングすると画像が劣化します。

### ＜原因＞
イメージ画像読み込み→*bitmapToMat(Bitmap→Mat)*→*Core.flip()*→*matToBitmap(Mat→Bitmap)*→イメージ画像保存時に*Bitmap*の圧縮フォーマットが*JPEG*だと非可逆圧縮により、一部のデータは欠落します。
通常だと人間の目では判断が困難なほど劣化を目立たせなくしますが、何回か繰り返してイメージ画像を圧縮して保存したことで、劣化が目立つようになりました。

### ＜対応＞
イメージ画像は、*Bitmap*の圧縮フォーマットを*WEBP*（可逆圧縮対応）に変更して保存しました。

```Kotlin:Bitmapの圧縮フォーマット（WEBP）
// ..... 略
	ByteArrayOutputStream().let { outStream ->
		bitMap.compress(Bitmap.CompressFormat.WEBP, 100, outStream)
		FileOutputStream(引数).apply {
			outStream.writeTo(this)
			close()
		}
	}
// ..... 略
```

## ３．３ *OpenCV*を利用したシームレス合成処理

### ＜背景＞
本アプリでは、2つのイメージ画像を貼り付けた後、イメージ画像表示（加工）画面で[決定]ボタンをタッチするとイメージ画像のシームレス合成処理を行います。

### ＜課題＞
シームレス合成処理では、*OpenCV*の*Photo.seamlessClone()*を利用しますが、合成（**したい**）元の画像と合成（**させたい**）先の画像とマスク用の画像を指定するとエラー（*assertion failed*）が発生します。

### ＜原因＞
合成（**したい**）元の画像と合成（**させたい**）先の画像のサイズが同一にしないとエラーが発生します。また、透過含む4チャンネルの画像ではエラーが発生します。
（最終的に*OpenCV*のソースと*Photo.seamlessClone()*のドキュメントで気付きました）

### ＜対応＞
合成（**したい**）元の画像と合成（**させたい**）先の画像のサイズが同一にします。
また、それぞれの画像は、8ビットの3チャンネルで指定します。

```Kotlin:同一サイズの画像変換
// ..... 略
	private fun createThumbnailBitmap(
		bitMap: Bitmap, reqWidth: Int, reqHeight: Int, filter: Boolean
	): Bitmap = Bitmap.createScaledBitmap(bitMap, reqWidth, reqHeight, filter)
// ..... 略
```

```Kotlin:BitmaptoMat変換
// ..... 略
	fun bitmapToMat(bitMap: Bitmap?, colorCode: Int): Mat {
		return Mat(bitMap?.height ?: 0, bitMap?.width ?: 0, CvType.CV_8UC3).apply {
			Utils.bitmapToMat(bitMap,this, false)
			Imgproc.cvtColor(this, this, colorCode, depth())
		}
	}
// ..... 略
```

## ３．４ シームレス合成画像のギャラリー登録処理

### ＜背景＞
本アプリでは、2つのイメージ画像を貼り付けた後、イメージ画像表示（加工）画面で[決定]ボタンをタッチするとイメージ画像のシームレス合成処理を行います。
画面遷移後にシームレス合成結果画面で[保存]ボタンをタッチするとギャラリー登録します。

### ＜課題＞
シームレス合成した画像を外部領域に保存しても*MediaScannerConnection.scanFile()*で認識されず、ギャラリー登録されません。

### ＜原因＞
シームレス合成した画像を外部データ領域（*Context.getExternalFilesDir()*）に保存すると*MediaScannerConnection.scanFile()*で認識されません。

### ＜対応＞
シームレス合成した画像を外部公開共有領域（*Environment.getExternalStoragePublicDirectory()*）に保存することで、*MediaScannerConnection.scanFile()*で認識されました。

```Kotlin:外部公開共有領域の保存場所
// ..... 略
	fun getMixingExternalImageFile(): File {
		// 外部公開共有領域以外はギャラリー登録不可のため、内部データ領域は使用しない
		File(
			Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES),
			PATH_MIXING_IMAGE // → "SeamlessCollage"
		).let { file ->
			if (!file.exists()) file.mkdir()
			return File("$file$FILE_MIXING_IMAGE-${System.currentTimeMillis()}$SUFFIX_IMAGE")
		}
	}
// ..... 略
```

```Kotlin:
// ..... 略
	MediaScannerConnection.scanFile(
		context,
		arrayOf(strUri),
		arrayOf("image/jpeg"),
		this // MediaScannerConnection.OnScanCompletedListener Callback
	)
// ..... 略
```
  
## リンク
[初めてのAndroidアプリ＆Kotlinでハマった点の技術的原因と対応方法](https://qiita.com/schenlybiz/items/24d97c10f62c55334f6a)
[１．Androidの画面遷移（フラグメント）処理](https://qiita.com/schenlybiz/items/2aa4d4d2420ea73bef50)
[２．Androidのカメラ機能に関する処理](https://qiita.com/schenlybiz/items/65e60ed39331e494ee23)