概要
表題の通り、以前からVector様にて公開している私のWindowsソフトに付属の
モザイクアートの作成機能をこの度
Google Playにて私の公開しているアプリに移植しました。
その際の
モザイクアートの作成機能の主要部分のコードを
私自身への備忘録も兼ねて記事にいたしました。
コード中のURLは、引用や参考にさせていただきましたサイト様のURLでございます。
この場にて厚く御礼申し上げます。
コードは丸っとコピーしても動くようにしていますが
簡略化しているので
解放などが省かれているので注意してください。
注意事項
※私は素人ですし
今回のコードは、あくまで最低限の動作する部分にとどめていますので、汚いです。
例外処理や、解放など至らぬ点が、かなり多々ございます。ご了承ください。
もし実際に参考にされる際は、必要個所を必ず訂正・加筆してからにしてください。
本題
Android C#環境の場合のコード
using Android.App;
using Android.Content;
using Android.OS;
using Android.Graphics;
using Android.Widget;
using System.Collections.Generic;
using AndroidX.AppCompat.App;
using System;
namespace App2
{
[Activity(Label = "モザイクアート", LaunchMode = Android.Content.PM.LaunchMode.SingleInstance, MainLauncher = true)]
public class MainActivity : AppCompatActivity
{
//保存しておく写真サイズ
int img_save_w = 1024;
//モザイクで集める画像のURIの格納
List<Android.Net.Uri> selected_fileuris = new List<Android.Net.Uri>();
Bitmap Mosaic_moto_img = null;
public async System.Threading.Tasks.Task collage_Start_Sub(Activity ac, List<Android.Net.Uri> selected_fileuris, Button lblsyori)
{ //モザイク画像を作成するところ
try
{
BitmapFactory.Options options = new BitmapFactory.Options();
lblsyori.Text = "再現元の画像の取得中";
await System.Threading.Tasks.Task.Delay(10);
//縮小サイズの取得
int load_int_chou = 128;
//元画像をリサイズする。
Bitmap bmp_main = bmp_size_format(Mosaic_moto_img, load_int_chou, load_int_chou, false);
options.Dispose();
//Bitmapのバイナリ置き場の準備
//https://qiita.com/aymikmts/items/7139fa6c4da3b57cb4fc
Java.Nio.ByteBuffer byteBuffer2 = Java.Nio.ByteBuffer.Allocate(bmp_main.ByteCount);
bmp_main.CopyPixelsToBuffer(byteBuffer2);
byteBuffer2.Flip();
//基礎Bitmapのバイナリの格納
byte[] bmparr = new byte[byteBuffer2.Capacity()];
byteBuffer2.Duplicate().Get(bmparr);
byteBuffer2.Clear();
lblsyori.Text = "再現元のピクセル取得中";
await System.Threading.Tasks.Task.Delay(10);
//画像の各ピクセルの色を格納
List<Android.Graphics.Color> moto_img_Color = new List<Android.Graphics.Color>();
moto_img_Color.Clear();
int base_cnt = 0;
do
{ //各ピクセルの色を取得していく
//安全装置
if (bmparr.Length == base_cnt) { break; }
//色を格納する
moto_img_Color.Add(Android.Graphics.Color.Rgb(bmparr[base_cnt], bmparr[base_cnt + 1], bmparr[base_cnt + 2]));
//次へ
base_cnt += 4;
} while (bmparr.Length > base_cnt); //最後までループ
//モザイク画として配置する画像の取得
List<Android.Graphics.Bitmap> selected_Bitmap = new List<Android.Graphics.Bitmap>();
selected_Bitmap.Clear();
//ARGB8888にて扱う。
Android.Graphics.Bitmap.Config bitmapConfig = Android.Graphics.Bitmap.Config.Argb8888;
//集める画像の小さなサイズのものを取得
int load_int_ippen = 32;
base_cnt = 0;
foreach (Android.Net.Uri selected_saki_imguri in selected_fileuris)
{ //各個小さな画像をリストにして入れている
try
{
using (var inputStream = ac.ContentResolver.OpenInputStream(selected_saki_imguri))
{
//事前準備
if (base_cnt % 3 == 0)
{
lblsyori.Text = "集める画像を取得中" + base_cnt.ToString();
await System.Threading.Tasks.Task.Delay(10);
}
//画像の読出し
Android.Graphics.Bitmap bmp_main2 = BitmapFactory.DecodeStream(inputStream, null, null);
options.Dispose();
inputStream.Close();
//元画像をリサイズしてリストに格納する。
bmp_main2 = bmp_size_format2(bmp_main2, load_int_ippen, load_int_ippen);
bmp_main2 = bmp_main2.Copy(bitmapConfig, true);
selected_Bitmap.Add(bmp_main2);
}
base_cnt += 1;
}
catch { }
}
//与えられた画像たちの平均色を設置する。
//モザイク画として配置する画像の色とした場合どんな色となるかを取得
List<Android.Graphics.Color> selected_Color = new List<Android.Graphics.Color>();
selected_Color.Clear();
int midx = 0;
foreach (Android.Graphics.Bitmap bmp_main2 in selected_Bitmap)
{
if (midx % 50 == 0)
{
lblsyori.Text = "集める画像のピクセル取得中" + midx.ToString();
await System.Threading.Tasks.Task.Delay(10);
}
//Bitmapのバイナリ置き場の準備
//https://qiita.com/aymikmts/items/7139fa6c4da3b57cb4fc
Java.Nio.ByteBuffer byteBuffer = Java.Nio.ByteBuffer.Allocate(bmp_main2.ByteCount);
bmp_main2.CopyPixelsToBuffer(byteBuffer);
byteBuffer.Flip();
//基礎Bitmapのバイナリの格納
bmparr = new byte[byteBuffer.Capacity()];
byteBuffer.Duplicate().Get(bmparr);
byteBuffer.Clear();
//初期化
base_cnt = 0;
long av_r = 0;
long av_g = 0;
long av_b = 0;
long av_cnt = 0;
do
{
//安全装置
if (bmparr.Length == base_cnt) { break; }
//色の合計を取得する
av_r += (long)bmparr[base_cnt];
av_g += (long)bmparr[base_cnt + 1];
av_b += (long)bmparr[base_cnt + 2];
//int color_a = bmparr[base_cnt + 3];
//次へ
base_cnt += 4;
av_cnt += 1; //ピクセル数をカウント
} while (bmparr.Length > base_cnt); //最後までループ
//この画像の平均色を取得
av_r = av_r / av_cnt;
av_g = av_g / av_cnt;
av_b = av_b / av_cnt;
selected_Color.Add(Android.Graphics.Color.Rgb((int)av_r, (int)av_g, (int)av_b));
}
//出力画像サイズの算出
int hw = bmp_main.Width * load_int_ippen; //画像幅
int hh = bmp_main.Height * load_int_ippen; //画像高さ
//元地の画像作成
Bitmap Haikei = Android.Graphics.Bitmap.CreateBitmap(hw, hh, bitmapConfig);
midx = 0;
using (Android.Graphics.Canvas canvas = new Android.Graphics.Canvas(Haikei))
{
using (var paint = new Paint())
{
foreach (Android.Graphics.Color mclr in moto_img_Color)
{ //元画像の各ピクセルに一番近い色の画像をはめ込んでいく
if (midx % 100 == 0)
{
lblsyori.Text = "集める画像で構成中" + midx.ToString();
await System.Threading.Tasks.Task.Delay(10);
}
//一番近い色を抽出する。
int min_span = 99999;
int min_span_idx = -1;
base_cnt = 0;
foreach (Android.Graphics.Color sclr in selected_Color)
{
int span = Math.Abs(mclr.R - sclr.R); //赤の色差
span += Math.Abs(mclr.G - sclr.G); //赤の色差
span += Math.Abs(mclr.G - sclr.B); //青の色差
if (span < min_span)
{
//より小さい色差が見つかった場合
min_span = span; //候補を変更する。
min_span_idx = base_cnt;
}
base_cnt += 1;
}
//最も近かった画像のサムネイルを下地に描画する。
int left = (midx % bmp_main.Width) * load_int_ippen;
int top = (midx / bmp_main.Width) * load_int_ippen;
paint.AntiAlias = true;
canvas.DrawBitmap(selected_Bitmap[min_span_idx], left, top, paint);
midx += 1;
}
}
}
lblsyori.Text = "処理終了中";
await System.Threading.Tasks.Task.Delay(10);
//画像などの解放
foreach (Android.Graphics.Bitmap bmp_main2 in selected_Bitmap)
{
bmp_main2.Dispose();
}
selected_Bitmap.Clear();
bmp_main.Dispose();
if (Haikei != null)
{ //画像を保存する場合
await bitmap_hontai_save(Haikei, "test.jpg", lblsyori);
Haikei.Dispose();
Mosaic_moto_img.Dispose();
}
}
catch { }
return;
}
protected override void OnCreate(Bundle savedInstanceState)
{
base.OnCreate(savedInstanceState);
SetContentView(Resource.Layout.layout2);
// ストレージの読み書き権限の確認
if (Build.VERSION.SdkInt > BuildVersionCodes.Q)
{ //Android11.0以上の場合のみ
System.Collections.Generic.List<string> Manifest_Permissions = new System.Collections.Generic.List<string>();
if (Build.VERSION.SdkInt >= Android.OS.BuildVersionCodes.Tiramisu)
{
//Android13.0以上の場合
//https://learn.microsoft.com/en-us/answers/questions/1354992/xamarin-android-13-popup-permission-notification-a
Manifest_Permissions.Add(Android.Manifest.Permission.PostNotifications);
Manifest_Permissions.Add(Android.Manifest.Permission.ReadMediaImages);
Manifest_Permissions.Add(Android.Manifest.Permission.AccessMediaLocation);
}
else
{
Manifest_Permissions.Add(Android.Manifest.Permission.WriteExternalStorage);
Manifest_Permissions.Add(Android.Manifest.Permission.ReadExternalStorage);
Manifest_Permissions.Add(Android.Manifest.Permission.AccessMediaLocation);
}
//各権限をループ2
foreach (System.String Permission_str in Manifest_Permissions)
{
//https://docs.microsoft.com/ja-jp/xamarin/android/app-fundamentals/permissions?tabs=windows
//https://www.petitmonte.com/java/android_fileprovider.html
if (ApplicationContext.CheckCallingOrSelfPermission(Permission_str) !=
Android.Content.PM.Permission.Granted)
{ //許可されていない場合
// ストレージの権限の許可を求めるダイアログを表示する
//https://qiita.com/khara_nasuo486/items/f23c91ccd37db885aefe
if (AndroidX.Core.App.ActivityCompat.ShouldShowRequestPermissionRationale(this,
Permission_str))
{
AndroidX.Core.App.ActivityCompat.RequestPermissions(this,
Manifest_Permissions.ToArray(), (int)Android.Content.PM.RequestedPermission.Required);
}
else
{
Toast toast =
Toast.MakeText(ApplicationContext, "アプリ実行の権限が必要です", ToastLength.Short);
toast.Show();
AndroidX.Core.App.ActivityCompat.RequestPermissions(this,
Manifest_Permissions.ToArray(),
(int)Android.Content.PM.RequestedPermission.Required);
}
}
}
}
Button btnMosaicstart = FindViewById<Button>(Resource.Id.btnMosaicstart);
btnMosaicstart.Click += delegate
{
Android.App.AlertDialog.Builder alert = new Android.App.AlertDialog.Builder(this);
alert.SetCancelable(false); //ダイアログ外クリックでのキャンセル禁止
alert.SetTitle("モザイクアート作成開始の確認");
alert.SetMessage("モザイクアートで集めて表現する元の画像素材を選択してください。");
alert.SetPositiveButton("はい", (senderAlert, args) =>
{
//素材画像の選択
image_select_main(0, 0, this);
});
alert.SetNegativeButton("いいえ", (senderAlert, args) =>
{ //何もしない
return;
});
Dialog dialog = alert.Create();
dialog.Show();
};
}
public void image_select_main(int no, int requestCode, Activity ac)
{ //画像選択時の処理
try
{
string caption = "";
//https://developer.xamarin.com/recipes/android/data/files/selecting_a_gallery_image/
var imageIntent = new Intent(Intent.ActionGetContent);
if (no == -1)
{ //素材画像
imageIntent.SetType("image/*");
caption = "モザイクアートの集合の素材となる画像を選択してください。";
}
else
{ //元画像
imageIntent.SetType("image/*");
caption = "モザイクアートの元となる画像を選択してください。";
}
//複数画像選択かどうか?0なら複数選択
//https://stackoverflow.com/questions/19585815/select-multiple-images-from-android-gallery
imageIntent.PutExtra(Intent.ExtraAllowMultiple, (no == -1));
imageIntent.SetAction(Intent.ActionGetContent);
ac.StartActivityForResult(
Intent.CreateChooser(imageIntent, caption), requestCode);
}
catch
{ //エラー時
Toast.MakeText(Application.Context, "選択ダイアログエラー", ToastLength.Long).Show();
}
}
public async System.Threading.Tasks.Task<Bitmap> Image_file_making(int image_no, int page_no, Activity ac, Intent data, Button lblsyori)
{ //画像差し込み処理
int err_cnt = 500;
lblsyori.Text = "[No." + err_cnt.ToString() + "]画像準備中....";
await System.Threading.Tasks.Task.Delay(10);
try
{ //画像を取得
//https://stackoverflow.com/questions/20013220/translate-android-java-to-xamarin-c-sharp
//https://stackoverflow.com/questions/43753989/xamarin-android-image-uri-to-byte-array
//jpegの場合⇒拡張子が付いてくるとは限らない。
//Exifに応じた回転情報を探る
lblsyori.Text = "[No." + err_cnt.ToString() + "]画像情報取得中";
await System.Threading.Tasks.Task.Delay(10);
Bitmap bmp_main = null; //初期化
using (var inputStream = ac.ContentResolver.OpenInputStream(data.Data))
{
//事前準備
lblsyori.Text = "[No." + err_cnt.ToString() + "]画像ロード中";
//画像の読出し
bmp_main = BitmapFactory.DecodeStream(inputStream, null, null);
inputStream.Close();
err_cnt = 503;
}
//リサイズする。
bmp_main = bmp_size_format(bmp_main, img_save_w, img_save_w, false);
err_cnt = 505;
lblsyori.Text = "[No." + err_cnt.ToString() + "]画像保存中";
await System.Threading.Tasks.Task.Delay(10);
return bmp_main;
}
catch
{
Toast.MakeText(Application.Context, "選択した画像は、存在していないか、読み込みに失敗しました。:" + err_cnt.ToString(), ToastLength.Long).Show();
return null;
}
}
public Bitmap bmp_size_format(Bitmap bmp, int max_w, int max_h, bool force_resize)
{ //画像の最大サイズを大きく超える画像は、
//動作不良防止のため、予め小さくして扱う
int w = bmp.Width;
int h = bmp.Height;
bool resize_flg = false; //リサイズが必要かどうか?
if (bmp.Width >= bmp.Height && (bmp.Width > max_w || force_resize))
{ //横長の画像で、(最大サイズを超えた場合、または、強制リサイズ時)
resize_flg = true;//リサイズ必要
w = max_w;
h = (w * bmp.Height) / bmp.Width;
if (h > max_h)
{ //上限値補正
h = max_h;
w = (h * bmp.Width) / bmp.Height;
}
}
else if (bmp.Height > max_h || force_resize)
{ //縦長の画像で、最大サイズを超えた場合、または、強制リサイズ時
resize_flg = true;//リサイズ必要
h = max_h;
w = (h * bmp.Width) / bmp.Height;
if (w > max_w)
{ //上限値補正
w = max_w;
h = (w * bmp.Height) / bmp.Width;
}
}
//リサイズが必要な場合
//安全装置付き
if (resize_flg && w > 0 && h > 0)
{ //リサイズ画像の作成
//http://seesaawiki.jp/w/moonlight_aska/d/BMP%B2%E8%C1%FC%A4%F2%A5%EA%A5%B5%A5%A4%A5%BA%A4%B9%A4%EB
try
{
//まずは正攻法での縮小を試みる
return Bitmap.CreateScaledBitmap(bmp, w, h, true);
}
catch
{ //失敗時→別の方法を試す
//元地の画像作成
Bitmap Haikei = Bitmap.CreateBitmap(w, h, Android.Graphics.Bitmap.Config.Argb8888);
//描画開始
using (Android.Graphics.Canvas canvas = new Android.Graphics.Canvas(Haikei))
{
using (var paint = new Paint())
{ //高画質で縮小させる。
//https://qiita.com/t2low/items/33606d6403226965b3bf
//http://anadreline.blogspot.com/2013/07/android_3.html
paint.FilterBitmap = true; //画像をきれいに縮小??
// 描画元の矩形イメージ
Rect src = new Rect(0, 0, bmp.Width, bmp.Height);
// 描画先の矩形イメージ
Rect dst = new Rect(0, 0, w, h);
//座標に指定のサイズで表示する
//https://dev.classmethod.jp/smartphone/xamarin-android-draw-image/
//そのままのDrawBitmapでのリサイズは、画質が劣化する。
//https://dev.classmethod.jp/smartphone/xamarin-android-draw-image/
canvas.DrawBitmap(bmp, src, dst, paint);
}
}
//ここで、Disposeはしてはいけない。
return Haikei;
}
}
else
{ //そのまま返す
return bmp;
}
}
public Bitmap bmp_size_format2(Bitmap bmp, int max_w, int max_h)
{ //単純に指定サイズにリサイズを行う
//安全装置付き
if (max_w > 30 && max_h > 30)
{ //リサイズ画像の作成
//http://seesaawiki.jp/w/moonlight_aska/d/BMP%B2%E8%C1%FC%A4%F2%A5%EA%A5%B5%A5%A4%A5%BA%A4%B9%A4%EB
try
{
//まずは正攻法での縮小を試みる
return Bitmap.CreateScaledBitmap(bmp, max_w, max_h, true);
}
catch
{ //失敗時→別の方法を試す
//元地の画像作成
Bitmap Haikei = Bitmap.CreateBitmap(max_w, max_h, Android.Graphics.Bitmap.Config.Argb8888);
//描画開始
using (Android.Graphics.Canvas canvas = new Android.Graphics.Canvas(Haikei))
{
using (var paint = new Paint())
{ //高画質で縮小させる。
//https://qiita.com/t2low/items/33606d6403226965b3bf
//http://anadreline.blogspot.com/2013/07/android_3.html
paint.FilterBitmap = true; //画像をきれいに縮小??
// 描画元の矩形イメージ
Rect src = new Rect(0, 0, bmp.Width, bmp.Height);
// 描画先の矩形イメージ
Rect dst = new Rect(0, 0, max_w, max_h);
//座標に指定のサイズで表示する
//https://dev.classmethod.jp/smartphone/xamarin-android-draw-image/
//そのままのDrawBitmapでのリサイズは、画質が劣化する。
//https://dev.classmethod.jp/smartphone/xamarin-android-draw-image/
canvas.DrawBitmap(bmp, src, dst, paint);
}
}
//ここで、Disposeはしてはいけない。
return Haikei;
}
}
else
{ //そのまま返す
return bmp;
}
}
protected override async void OnActivityResult(int requestCode, Result resultCode, Intent data)
{
base.OnActivityResult(requestCode, resultCode, data);
int err_cnt = 0;
try
{
Button btnMosaicstart = FindViewById<Button>(Resource.Id.btnMosaicstart);
string bf_text = btnMosaicstart.Text;
//◆◆◆以下、画像選択時のイベント◆◆◆
if (resultCode == Result.Ok)
{ //OK(ズドン)
if (requestCode == 0)
{ //元画像の選択
try
{ //画像の埋め込み処理
Mosaic_moto_img = await Image_file_making(0, 0, this, data, btnMosaicstart);
//文字を戻す
btnMosaicstart.Text = bf_text;
//素材画像の選択
image_select_main(-1, 1, this);
}
catch
{
Toast.MakeText(ApplicationContext, "選択した写真は、存在していないか、読み込みに失敗しました。:" + err_cnt.ToString(), ToastLength.Long).Show();
}
}
else if (requestCode == 1)
{ //集合画像の選択
selected_fileuris.Clear();
if (data.ClipData != null)
{ //複数選択された場合
int count = data.ClipData.ItemCount;
int currentItem = 0;
while (currentItem < count)
{ //各ファイルのURIを取得
currentItem = currentItem + 1;
selected_fileuris.Add(data.ClipData.GetItemAt(currentItem - 1).Uri);
}
await collage_Start_Sub(this, selected_fileuris, btnMosaicstart);
//文字を戻す
btnMosaicstart.Text = bf_text;
}
else if (data.Data != null)
{ //1つだけの選択時
Toast.MakeText(ApplicationContext, "単一の画像の選択ではモザイクアートは作成できません。", ToastLength.Long).Show();
}
}
}
}
catch
{
Toast.MakeText(ApplicationContext, "選択した写真は、存在していないか、読み込みに失敗しました。:" + err_cnt.ToString(), ToastLength.Long).Show();
}
}
public async System.Threading.Tasks.Task bitmap_hontai_save(Bitmap bitmap, string filePath, Button lblsyori)
{ //本体のPictureフォルダに画像の保存
try
{
//画像保存
//https://garakutatech.blogspot.com/2021/02/androidmedia-store.html
//https://codechacha.com/ja/android-mediastore-insert-media-files/
ContentValues values = new ContentValues();
// コンテンツ クエリの列名
values.Put(Android.Provider.MediaStore.Images.Media.InterfaceConsts.RelativePath, "Pictures/TageSP");
// ファイル名
values.Put(Android.Provider.MediaStore.Images.Media.InterfaceConsts.DisplayName, System.IO.Path.GetFileName(filePath));
// マイムの設定
//https://developer.mozilla.org/ja/docs/Web/Media/Formats/Image_types
//JPEG保存の場合
values.Put(Android.Provider.MediaStore.Images.Media.InterfaceConsts.MimeType, "image/jpeg");
// 書込み時にメディア ファイルに排他的にアクセスする
values.Put(Android.Provider.MediaStore.Images.Media.InterfaceConsts.IsPending, 1);
ContentResolver resolver = ContentResolver;
Android.Net.Uri collection = Android.Provider.MediaStore.Images.Media.GetContentUri(Android.Provider.MediaStore.VolumeExternalPrimary);
Android.Net.Uri item = resolver.Insert(collection, values);
//ファイル書き込み
lblsyori.Text = "画像保存中";
using (System.IO.Stream outstream = resolver.OpenOutputStream(item))
{
bitmap.Compress(Bitmap.CompressFormat.Jpeg, 100, outstream);
//https://stackoverflow.com/questions/71794933/how-to-save-a-bitmap-on-storage-in-android-q-and-later
outstream.Flush();
outstream.Close();
};
// 排他的にアクセスの解除
values.Clear();
values.Put(Android.Provider.MediaStore.Images.Media.InterfaceConsts.IsPending, 0);
resolver.Update(item, values, null, null);
await System.Threading.Tasks.Task.Delay(10);
lblsyori.Text = "画像保存完了";
return;
}
catch
{
return; //出る
}
}
}
}
レイアウト
上記のコードを動かすためのレイアウトは以下の通りです。
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
>
<Button
android:text="開始"
android:textSize="25.0dp"
android:layout_width="350.0dp"
android:layout_height="300.0dp"
android:id="@+id/btnMosaicstart"
android:layout_marginTop="10.0dp"
/>
</LinearLayout>
サンプル
上記のコードを
丸っと書き写して
実行すると、以下のような画面が表示されます。
※ファイルサイズの関係で縮小して掲載しています。
余談
今回の記事の作成に当たり
VisualStudio2022にてビルドの「ソリューションの配置」で
様子を毎回見ながら、最低限必要な機能だけ記述しながら
確認用にアプリ(apk)を作成しながら、訂正・加筆を行っていたのですが
ある時点で急に、エミュレータでも実機でもアプリの起動に失敗するようになりました。
そのため、その時点で加筆した部分をコメントアウトしましたのですが、治らず。
更にコメントアウトの部分を増やして、すべてのコードをコメントアウトした時点でも
アプリの起動に失敗するようになりました。
別の新規プロジェクトを作成してコードを移植しながら確認しても、
ある時、急に起動できなくなりました。
結局、一時間程度悩んだのですが
ビルドの「ソリューションのクリーン」を試したところ
解決いたしました。
原因は不明ですが、突然、アプリの起動に失敗して
その間に加筆した部分を戻しても症状が治らない場合は
ビルドの「ソリューションのクリーン」も一手かもしれないです。