20200516のC#に関する記事は8件です。

眺めて覚える C# Xamarin Forms(13) File 操作

Android上の任意のファイルアクセスするときにユーザーの許可が必要です。

主なAndroidのファイルディレクトリ

Android.OS.Environment ディレクトリ
DirectoryAlarms PRIVATE_EXTERNAL_STORAGE/Alarms
DirectoryDcim PRIVATE_EXTERNAL_STORAGE/DCIM
DirectoryDownloads PRIVATE_EXTERNAL_STORAGE/Download
DirectoryDocuments PRIVATE_EXTERNAL_STORAGE/Documents
DirectoryMovies PRIVATE_EXTERNAL_STORAGE/Movies
DirectoryMusic PRIVATE_EXTERNAL_STORAGE/Music
DirectoryNotifications PRIVATE_EXTERNAL_STORAGE/Notifications
DirectoryPodcasts PRIVATE_EXTERNAL_STORAGE/Podcasts
DirectoryRingtones PRIVATE_EXTERNAL_STORAGE/Ringtones
DirectoryPictures PRIVATE_EXTERNAL_STORAGE/Pictures

image.png

許可なしでアクセスすると下記のメッセージを出し例外として扱われます。

image.png

手順1 Xamarin formsのプロジェクトを作成します。

手順2 andoridプロジェクトのプロパティを開きます。

image.png

手順3 ファイルアクセスするためには、android マニフェストを開いて、必要なアクセス許可を与えます。

image.png
image.png
AndroidManifest.xmlにて確認します。
image.png

手順4 参照にMono.Abdroid.dllを追加します。

image.png

手順5MainActivity.csに許可ダイアログ要求を追加します。

MainActivity.cs
using System;

using Android.App;
using Android.Content.PM;
using Android.Runtime;
using Android.Views;
using Android.Widget;
using Android.OS;
using Android.Support.V4.App;
using Android.Support.V4.Content;
using Android;

namespace PCLFiles.Droid
{
    [Activity(Label = "PCLFiles", Icon = "@mipmap/icon", Theme = "@style/MainTheme", MainLauncher = true, ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation)]
    public class MainActivity : global::Xamarin.Forms.Platform.Android.FormsAppCompatActivity
    {
        protected override void OnCreate(Bundle savedInstanceState)
        {
            TabLayoutResource = Resource.Layout.Tabbar;
            ToolbarResource = Resource.Layout.Toolbar;

            base.OnCreate(savedInstanceState);
       //追加します。
            if (ContextCompat.CheckSelfPermission(this, Manifest.Permission.ReadExternalStorage) != (int)Permission.Granted)
            {
                ActivityCompat.RequestPermissions(this, new string[] { Manifest.Permission.ReadExternalStorage }, 0);
            }
            if (ContextCompat.CheckSelfPermission(this, Manifest.Permission.WriteExternalStorage) != (int)Permission.Granted)
            {
                ActivityCompat.RequestPermissions(this, new string[] { Manifest.Permission.WriteExternalStorage }, 0);
            }


            Xamarin.Essentials.Platform.Init(this, savedInstanceState);
            global::Xamarin.Forms.Forms.Init(this, savedInstanceState);
            LoadApplication(new App());
        }
        public override void OnRequestPermissionsResult(int requestCode, string[] permissions, [GeneratedEnum] Android.Content.PM.Permission[] grantResults)
        {
            Xamarin.Essentials.Platform.OnRequestPermissionsResult(requestCode, permissions, grantResults);

            base.OnRequestPermissionsResult(requestCode, permissions, grantResults);
        }
    }
}

image.png

手順6 MainPage.xamlにスタートボタンを設定します。

MainPage.xaml
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:d="http://xamarin.com/schemas/2014/forms/design"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
             mc:Ignorable="d"
             x:Class="PCLFiles.MainPage">

    <StackLayout>
        <!-- Place new controls here -->
        <Label Text="Welcome to Xamarin.Forms!" 
           HorizontalOptions="Center"
            />
        <Button Text="Start" Clicked="SetUp"/>
    </StackLayout>
</ContentPage>

手順7 ライブラリを追加しMainPage.xaml.csにファイル読み込み部を追加します。

image.png

MainPage.xaml.cs
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Xamarin.Forms;

using Android.OS;
using System.IO;
using Android.Support.V4.App;
using Android.Support.V4.Content;
using Android;
namespace PCLFiles
{
    public class Cam
    {
        public string Name { get; set; }
        public string Image { get; set; }
    }
    // Learn more about making custom code visible in the Xamarin.Forms previewer
    // by visiting https://aka.ms/xamarinforms-previewer
    [DesignTimeVisible(false)]
    public partial class MainPage : ContentPage
    {
        public MainPage()
        {
            InitializeComponent();

        }

        private void SetUp(object s,EventArgs e)
        {
            var tmp = new DataTemplate(() =>
            {
                var grid = new Grid() { Margin = 2 };
                grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(100) });
                grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Auto) });
                var name = new Label { FontSize = 16 };
                name.SetBinding(Label.TextProperty, "Name");
                var image = new Image() { HeightRequest = 200 };
                image.SetBinding(Image.SourceProperty, "Image");
                grid.Children.Add(image, 0, 0);
                grid.Children.Add(name, 1, 0);
                return new ViewCell { View = grid };
            });
            var path = Android.OS.Environment.GetExternalStoragePublicDirectory(Android.OS.Environment.DirectoryDcim).Path + "/Camera";
            var lst = from x in Directory.GetFiles(path).Take(10) select new Cam() { Name = Path.GetFileName(x), Image = x };
            var lv = new ListView() { ItemsSource = lst, ItemTemplate = tmp };
            Content = new Xamarin.Forms.ScrollView() { Margin = 2, Orientation = ScrollOrientation.Vertical, Content = lv };
        }
    }
}

手順8 実行する。

image.png

付録

nugetで追加したライブラリ

image.png

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Effective Java 第2版 関連事項調査まとめ

はじめに

新人のころEffective Java 第2版を購入したが2~3年積んだまま放置してしまった。
読み返してみると案外今でも知らなかったりふわっとしてて人に説明できない関連事項が結構多かったため、恥を忍んでこの記事に調査内容をまとめていく。
Javaの本であるが、本記事の筆者は現在C#を現場で使っているため、もしC#に同等の内容が存在すれば、C#の内容を記載することもある。
また、このシリーズは、Effective Javaの本筋を追うものではなく、理解の障壁となりうる関連事項の調査をまとめたものである。
Effective Javaの内容そのものに関しては、すでに先人たちの努力によって多様なサマリーがあちこち落ちているので参照されたし。

なお、本書は2020年5月現在第3版がすでに出版されているので、最新のEffective Javaが知りたい方はそちらを参照すること。
#Lambda式とかtry-with-resource文とか追加されたらしい

目次

第2章 オブジェクトの生成と消滅

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

インスタンス制御とは?

はじめに

Effective Java 第2版 P6に出てくる下記の内容について深堀する。

static ファクトリーメソッドが、何度呼び出されても同じオブジェクトを返すことができることは、ある時点でどのようなインスタンスが存在するかを厳密に制御するのにも使用できます。この制御をおこなうクラスは インスタンス制御されている (instance-controlled)と言われます。

目次

  1. 結局どんな状態?
  2. 具体例は?
  3. C#ではEnumでシングルトン作れないの?
  4. 参考文献

結局どんな状態?

  • 実行環境が、あるクラスのインスタンス数を制限している状態
    • 1個の場合もあるし、複数個の場合もあるが、「このクラスのインスタンスはn個以上存在することはありません!!」というのを保証している状態
  • 同じインスタンスが1つだけしか存在しない
    • インスタンスが同じ(a == b)であれば、インスタンスの内容も同一(a.equal(b))ということ
    • 上記の性質により、値比較の際equal(Object)の代わりに==演算子による比較が可能

具体例は?

シングルトン

インスタンスが1個であることが保証されているクラス。

下記はJavaでの記述例

Singleton.cs
// Enumで実装したパターン
// sealedで継承不可クラスになる
public class SingletonClass
{
    // 外部から変更できないようにする
    private static final SingletonClass INSTANCE = new SingletonClass();

    // 外部から生成できないようにする
    private SingletonClass() { //初期化処理 };

    // インスタンス取得
    public static SingletonClass GetInstance() {return INSTANCE;}

}

下記はC#での記述例

Singleton.cs
// Singletonパターン
public class SingletonClass
{
    // 外部から変更できないようにする
    private static SingletonClass _singleInstance = new SingletonClass();

    // インスタンス取得
    public static SingletonClass GetInstance(){ return _singleInstance; }

    // 外部から生成できないようにする
    private SingletonClass(){ //初期化処理 }
}

Enum

インスタンスがn個であることが保証されているクラス。 ※追記:Javaのみ

Enum.java
enum Season
{
    Spring,
    Summer,
    Autumn,
    Winter
}

追記:ただし、C#のEnumはインスタンスがn個であることが保証されて いない。
クラスとして実装されているJavaと違って、C#のEnumはCの列挙型のように、名前付きの整数に過ぎないので、Javaと同様の保証はされない。
詳細はコメント欄を参照。

C#ではEnumでシングルトン作れないの?

蛇足だが気になったので調査する。
Javaでは下記のようにしてEnumを使用してシングルトンを実装することができる。

EnumSingleton.java
public enum Singleton {
    INSTANCE;

    public void execute (String arg) {
        // インスタンス取得など、お目当ての処理を実装
    }
}

同様にしてC#でもEnumでシングルトンを実装できないのか?というのが疑問である。
調査の結果、 C#はEnumそのものにフィールドやメソッドを定義することができないため、Javaと同様にSingletonを作るのは難しいということが判明した。
仮にEnumを使って実装するとしたら、下記のように 拡張メソッド を使用する必要がある

追記:コメントで指摘があったが、そもそもC#のEnumは名前付き整数に過ぎないため、インスタンス数の制限を保証できない。
そのため、C#でEnumを利用したシングルトンの実装はできない

参考文献

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

サイゼリヤの間違い探しをロバストな画像処理で解く

はじめに

初投稿です。よろしくお願いします。

サイゼリヤの間違い探しを画像処理を用いて解くプログラムを作成しました。
基本的には画像の差分をとってなんやかんやして間違いを見つける、という手法ですが、それに加え今回はロバスト性に着目しました。
まずは実行結果をご覧ください。

※ロバスト性… 本義は外乱の影響の受けづらさ。差分で間違い箇所を抽出するときは全く同じ距離・角度から撮影した2枚の画像が必要となりますが(少しでもずれていると誤検出がめっちゃ増える)、少しずれた2枚の画像同士の間違いも検出できるという意味でロバスト性と言っています。

実行結果

通常、差分で間違いを探す手法では2枚の画像をきちんと位置合わせする必要があるのですが、斜めから見た画像同士からでも間違い箇所を検出できています。
最終目標は、スマホのカメラでサイゼの間違い探しを撮影し、その写真をもとに間違いを見つけることです。
0_01.png
0_02.png

アルゴリズムの概要

以下、使用した技術等をつらつらと書いていきます。
恥ずかしながら初めてGitHub使ったのでちゃんと見られるか不安ですが、一応ソース類をアップしたのでよかったらご覧ください。
https://github.com/ika-kk/SaizeriyaPj

実装と環境

C#
.NET Framework 4.6.1
OpenCvSharp4
Windows 10 Home
Intel Core i7-6700 @ 3.40GHz
RAM 16.0GB
グラボは非搭載

フローチャート

  1. 画像を2枚読み込む(以下SrcとTargetと記載します)
  2. OpenCVの特徴量マッチングでSrcとTargetの対応点を取得する
  3. 対応点の情報をもとにSrcに射影変換を適用し、Targetと座標を一致させる
  4. SrcとTargetの差分画像を取得する
  5. 差分画像の重要箇所のみを目立たせ、マスクを作成する
  6. マスクをSrcとTargetにかぶせ、間違い箇所を目立たせる

1. 画像を2枚読み込む

まずはサイゼリヤの間違い探しの画像を読み込みます。
今回はサイゼリヤのホームページから画像をいただきました。
なお、実際にサイゼに行って間違い探しの写真を撮るという状況を想定し、Photoshopで歪みを加えた画像を作成しました。

また、間違い探し以外の余分な情報を極力削るために、マスク機能(手動)を追加してあります。
これに関してはこちらのブログを参考にしました。
【C#】レイヤー機能を作る|いえひのプログラミング部屋

あと最近のスマホの写真は解像度が大きめなので、処理時間が結構がかかります。
そのため、次の処理に進む際に、画像サイズを800[px]*600[px]におさまるサイズに縮小するようにしました。
このサイズに縮小しても分解能はだいたい0.4[mm/px]になるため、幅・高さともに1~2mm以上の間違い箇所であれば検出できるはずです。

2. OpenCVの特徴量マッチングでSrcとTargetの対応点を取得する

画像同士のマッチングには、色々な種類があります。
テンプレートマッチング (画像の濃淡を主に用いたマッチング)
幾何形状マッチング (エッジ情報を用いたマッチング)
特徴点マッチング (局所的な特徴点を利用するマッチング)
・その他

色々比較した結果、今回は特徴量マッチングを使うことにしました。

方式 OpenCV 形状変化への強さ 色味の変化への強さ
テンプレートマッチング 対応 × ×
幾何形状マッチング 非対応 ×
特徴点マッチング 対応

幾何形状マッチングはOpenCVには非実装だったため、自動的に候補から除外されます。個人的にはかなり便利なマッチング方式だと思うので、実装してほしいんですけどね…。

次に、形状変化への強さは特徴点マッチングが優秀です。
テンプレートマッチングと幾何形状マッチングは、マッチングの元画像と対象画像が拡大・縮小・回転を用いて一致するものしか対応できません。
一方の特徴点マッチングは、拡大・縮小・回転に加え、せん断・歪みまで対応できます。冒頭にもあるように斜めから見た画像(=歪み変形した画像)同士を比較したいので、特徴点マッチングを採用しました。
ちなみに、拡大・縮小・回転・せん断が可能で、更に移動を実現できる変形をアフィン変換(変形)、このアフィン変換に歪み変形を加えたものを射影変換(変形)と呼びます。
変換.png

画像の多くの箇所が同時に色味の変化を起こすことはないだろうと予想し、特徴点マッチングで問題ないと判断しました。
ちなみに、色味が変わってもエッジさえ検出できればマッチングの精度に影響しないという意味で、幾何形状マッチングは優秀です。一方テンプレートマッチングは輝度情報がキモになるので、色が変わると検出できなくなったり、精度が悪くなったりします。

特徴点マッチングにはSIFT法やSURF法など色々な手法が存在しますが、OpenCVではAKAZEという方式が一般的だそうです。
実装にあたってはこちらの記事を参考にさせていただきました。
OpenCvSharpでAKAZEを用いて特徴量を検出する - Qiita

最終的に、以下の画像のように特徴点同士を対応付けすることができました。
対応する特徴点同士をつないだ線がおおむね平行になっているのがわかります。
誤った特徴点が対応付けされている箇所もいくつかありますが、これは次に行う変換の際に外れ値として無視されるので、あんまり気にしなくてもいいです。
ちなみに、今回使用した特徴点の数は、全体の10%です。つまり本来はこの10倍の特徴点が検出されているのですが、処理が重くなること、外れ値を多く含むため使用する意味がないことから、一致度上位10%のみを抽出しています。
特徴点マッチング.png

3. 対応点の情報をもとにSrcに射影変換を適用してTargetと座標を合わせる

拡大縮小回転とせん断であれば前述のようにアフィン変換で事足りますが、今回は歪みも想定しているため、射影変換を使用しました。SrcをTargetに合わせこむようなイメージです。処理の具体的なフローはこんな感じです。

  1. 特徴点の対応をベクトルで表現する
  2. ベクトルの始点と終点を一致させるような射影変換を実現したい
  3. そうなるような変換行列を作成する
  4. 変換行列にもとづき、画像の変形を行う

以下はイメージ画像です。簡単のために4隅のベクトルしか書いていませんが、実際は画像中の一致度上位10%の特徴点同士のベクトル全てを考慮し、かつ外れ値は無視しつつ変形が行われています。

射影変換.png
OpenCvSharpでの実装方法はこちらを参考にしました。
画像から特徴量を抽出し、透視変換行列を導出して画像を変形する - Qiita
OpenCvSharpで透視投影の補正 - SourceChord

実装(折りたたみ)
射影変換の実装部
Mat SrcMat, TargetMat; // 素材画像
public Mat WarpedSrcMat; // 射影変換後の画像
KeyPoint[] KeyPtsSrc, KeyPtsTarget; // 特徴量
IEnumerable<DMatch> SelectedMatched; // マッチング結果

public void FitSrcToTarget()
{
    // 使用する特徴点の量だけベクトル用意
    int size = SelectedMatched.Count();
    var getPtsSrc = new Vec2f[size];
    var getPtsTarget = new Vec2f[size];

    // SrcとTarget画像の対応する特徴点の座標を取得し、ベクトル配列に格納していく。
    int count = 0;
    foreach (var item in SelectedMatched)
    {
        var ptSrc = KeyPtsSrc[item.QueryIdx].Pt;
        var ptTarget = KeyPtsTarget[item.TrainIdx].Pt;
        getPtsSrc[count][0] = ptSrc.X;
        getPtsSrc[count][1] = ptSrc.Y;
        getPtsTarget[count][0] = ptTarget.X;
        getPtsTarget[count][1] = ptTarget.Y;
        count++;
    }

    // SrcをTargetにあわせこむ変換行列homを取得する。ロバスト推定法はRANZAC。
    var hom = Cv2.FindHomography(
        InputArray.Create(getPtsSrc),
        InputArray.Create(getPtsTarget),
        HomographyMethods.Ransac);

    // 行列homを用いてSrcに射影変換を適用する。
    WarpedSrcMat = new Mat();
    Cv2.WarpPerspective(
        SrcMat, WarpedSrcMat, hom,
        new OpenCvSharp.Size(TargetMat.Width, TargetMat.Height));
}

4. SrcとTargetの差分画像を取得する

ようやく2枚の画像を得ることができました。いよいよレガシーな画像処理の出番です。
とりあえず射影変換後の画像をPhotoshopで比較してみました(レイヤースタイル:差の絶対値を使用)。なかなかいい感じです。
差の絶対値.png
更に、チャンネルを別個に使用すれば検出精度が上がると思ったため、RGBとHSVの各チャンネル同士の差分画像を作成しました。ただしH(色相)、S(彩度)の差分はノイズが多くて使い物になりませんでした。変形時の補完に原因があるような気がします。
そのため、今回はRGBV(輝度)計4チャンネルのみを使用することにしました。これらの画像に対して処理を施し、間違い箇所のみを目立たせるマスクを作っていきます。
差分画像.png
余談ですが、OpenCVには画素にアクセスするメソッドSet/GetPixelが用意されています。最初はそれを使って実装したのですが、クソ遅かったです(800px*600pxの差分画像を1枚作るのに5秒くらいかかった)。
その後LockBitsを使ってメモリ領域を直接いじる方法を知って、ようやく6枚で1秒というギリ耐えられるかなって速度になりました。それでも遅いけど。
更にプログラム完成後に知ったんですけどPythonだとこんな感じで差分画像取れるんですね……便利……。C#やめよ。

Python
diff = src.astype(int) - target.astype(int)

2020/05/17追記
@albireo 氏からコメントを頂き、OpenCVにも差分画像を取得する cv::absdiff があることを知りました。当然 OpenCvSharpにも組み込まれているので、これを使用すれば処理時間の大幅な短縮が見込めそうです。情報ありがとうございました。
まずは何事も調べるのは大事ですね。ごめんなC#。

5. 差分画像の重要箇所のみを目立たせ、マスクを作成する

得られたRGBとVの計4枚の差分画像を2値化しますが、このままだとノイズが結構あるので、メディアンフィルタを適用してごま塩ノイズを取っています。
メディアンフィルタ 画像処理ソリューション

ノイズ除去の次は4枚の画像をBitwizeOrで統合します。すなわち、各画像の画素ごとにOR演算を行い、どれか1枚でも白の箇所があったらその画素は白とすることで1枚の統合画像を作成します。この処理によって、検出モレを防ぐことができます。
ピクセル毎の論理演算 AND NOT OR XOR | OpenCV画像解析入門

次にブロブ処理一定の大きさより小さい差分検出領域を省いて、ノイズ除去を行います。メディアンフィルタと被っているように思えますが、メディアンフィルタとブロブの違いは形状に依存するか否かというところです。また、ブロブ処理は、これはある大きさのかたまりをカウントすることができる、という利点があります。今回は実装できなかったしていませんが「○個の間違いを表示する」といったように指定することも応用次第でできると思います。
ブロブ解析~ヴィスコの画像処理技術 | ヴィスコ・テクノロジーズ株式会社

最後に、膨張処理を適用します。膨張処理とは、ある画素が白だったらその近傍の画素も白にするという処理のことです。
完成イメージとして、間違い箇所を囲むようなマスクを作りたかったので、差分検出領域を広げるためにこの処理をかませています。これで最終的なマスク調整を行います。
膨張・収縮・オープニング・クロージング 画像処理ソリューション

フローの概略図はこんな感じです(Vチャンネル描き忘れましたが、実際は前述のとおり4チャンネルの画像を使用しています)。
下処理.png

なお、メディアンフィルタのカーネルサイズは3px、2値化閾値は128、膨張は5px、ブロブ面積下限値は10pxとしました。この値でおおむね良さそうですが、実際には細かい調整をすることがあるため、こんな感じのGUIも一応作成しました。
2.gif

6. マスクをSrcとTargetにかぶせる

射影変換したSrcとTargetを並べて表示し、両方にマスクをかぶせます。
今回のプログラムでは、差分検出領域は透明色、それ以外は低透明度の黒とすることで、間違い箇所を際立たせています。
無事10個の間違いの周辺がハイライトされていますね。
両脇もハイライトされているのは、ダウンロードできる間違い探しの画像サイズがそもそも一致しておらず、端の方が削れてしまっているからです。
0_02.png

机上評価結果

適当に3種類の間違い探しを選んでプログラム実行したところ、いずれも10個の間違いが取得できています!
にしても本当に難しいですね。個人的にヤバいと思ったのは2つめの右下の焼き鳥の串の角度です。こいつはやばい。
2019.png
201912.png
2020.png

そして現実へ…

プログラムは完成した。抜かりはない、完璧だ。
いざ実戦といこう。

実際にサイゼで写真を撮ってみた結果

くどいようですが、今回ロバスト性を重視したのは、サイゼに行って間違い探しの写真を撮ってその流れで答えを見つけるというリアルタイム感の実現を目指してのことです。
そのため、実際にiPhone Xで写真を撮ってこのプログラムに突っ込んでみました。果たして結果やいかに。
実写3.png
デンッ!!
実写.png
ダメだったよ。

失敗の原因

この失敗の原因は、実際の対象はJpgでもPngでもなく厚紙に印刷されているというところにあるようです。つまり、画像によって3次元的な反りの具合が異なってしまっており、その歪みが補正しきれていません。射影変換ではこのタイプの歪みに対応できないのです。
その結果、端にいけばいくほど画像間のずれが大きくなり、結果として端の方で誤検出が増大しています。左端の女の子やおじさん、右端の羊なんかが顕著です。
反り.png
ただし、画像の中央付近はいい感じに検出できています。特に豆の違いが検出できているので個人的にはかなり達成感があります。この豆だけが自力(人力)で解けなかったんですよね……。
実写2.png

対策

単なる歪みであれば任意直線上の特徴点同士の距離の比は変わらないため射影変換で対応できますが、今回の場合は3次元的に反っているため、より高度な変形によって丹念にあわせ込む必要があります。
色々調べたところ、九州大学の情報系の研究室の資料がヒットしました。
【スライド】2次元ワープを用いた顔画像処理顔画像処理 - 内田誠一氏、他2名
【論文】粗密DPに基づく画像の弾性マッチングアルゴリズム - 宮崎洋光氏、他2名
一応概要を書いておきます。

  • 射影変換よりもフレキシブルに変形できる弾性マッチングというアルゴリズムがある
    • 対応する点同士を一致させるアルゴリズム。例えば、ある人の正面からの写真と斜めからの写真をマッチングして、斜めからの写真を正面からの写真に変換できる。
    • この原理で反りに対応できそう
  • ただし計算量が$O(N^{2}9^{2N})$らしい。こいつはやばい。
  • それを緩和するために粗密DPという動的計画法を適用する
    • これにより計算量は$O(N^{4})$となり、ある程度現実的なアルゴリズムとなる
    • 粗密DPを低解像度の画像に適用することが前提

なるほどわからん。ただ、本論文が執筆されたのは2004年とだいぶ前なので、技術自体は枯れてきているかもしれません。勉強していつか実装したいですね。

その他課題

極端に小さい間違いは検出できない

画像を合わせ込む段階で画像全体の特徴点を使っているため、当然ながら間違い箇所の特徴点も使用しています。現状、そういった箇所は閾値を設定して外れ値とすることで無視しています。
ただし、2枚の画像両方に似たような特徴点があり、かつその2点の距離が比較的近い場合、その2点は同じであるとみなされ、射影変換の精度に影響する可能性があります。サイゼの間違い探しには対象がずれているだけ、という間違いも結構あるので、この影響は無視できません。
実際、以下の画像の場合だと間違いの箇所がかなり細いため、プログラムを実行した結果ノイズとみなされてしまいました。
2006年8月の間違い探し - 左下の時計の短針に注目

この解決策として、「マッチングの閾値を追い込む」「画像の一部分のみの特徴点を用いて射影変換する」といった方法が考えられます。
ただし、前者は閾値を上回る間違い箇所の特徴点が無いことを証明できず、後者はその一部分に間違いがあった場合意味がない上に、一部分だけだと射影変換の精度が不安です。よって、これらの解決策は根本的なものではありません。
あとは紙自体の反りを補正した上での話になりますが、間違い探しの冊子のエッジを利用できるような気もします。要検討ですね。

サイゼで実行したい

「サイゼに行く→注文する→写真を撮る→実行する→料理が来るまでに間違いを全て見つける」というのが理想のフローです。
でもスマホアプリ作ったことないので諦めました。Xamarin勉強します。
また、スマホで動かすためにはもっと処理を軽くする必要がありますね。マシンパワーに頼らない実装……。

さいごに

このたび初めてOpenCV(OpenCvSharp)をまともにいじりましたが、思いの外色々なことが実現できて楽しかったです。

また、今回使用した実写画像は、サイゼにテイクアウトを買いに行ったときに撮影しました。
テイクアウトかなり良かったので是非みなさまもおうちでサイゼしましょう。
サイゼリヤトップページ|サイゼリヤ
takeout.png

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

コレクションクラスのListに対してLINQを用いて集約したい。GroupBY,Sum,OrderBy,Selectの書き方

LINQをList<コレクションクラス>にどう書くか?

LINQの記事では、string型やint型のListを扱っているものはよくある。しかし、独自で作成したコレクションクラスのListをどうLINQで処理するかの記事が発見しづらかった。そのため、ここにその仕方を記述する。
 元となるList<独自クラスで複数のフィールドを持つ>の値で、同一科目のものは集約しつつ、金額は合計値を記述するようなテキスト(メモ)を生成したい。

 public string WritingMemo( List<JournalizingMemo> MemoDrafts)
        {

            //メモの元MemoDraftsから同一会計名目や貸借項目は集約し金額は合計値を記述したいので
            var memoArranged = MemoDrafts.GroupBy(x => new { x.SlipDate, x.AccountingName,x.BalanceName, x.SubjectName })
                                    .Select(y => new
                                   {
                                       SlipDate = y.Key.SlipDate,
                                       Amount   = y.Sum(z => z.Amount),
                                       AccountingName = y.Key.AccountingName,
                                       BalanceName =    y.Key.BalanceName,
                                       SubjectName =    y.Key.SubjectName
                                   })
                                   .OrderBy(x => x.SlipDate)
                                   .ThenBy(x => x.AccountingName)
                                   .ThenBy(x => x.BalanceName)
                                   .ThenBy(x => x.SubjectName);

            //メモのヘッダー作成
            sb.AppendLine("出力日:" + DateTime.Now.Date.ToString("yyyy/mm/dd"));
            sb.AppendLine("");

            //集約結果からテキストに起こす
            var sb = new StringBuilder();

                foreach (var record in memoArranged)
                {
                        sb.AppendLine("伝票日付:" + record.SlipDate.ToString("yyyy/mm/dd"));
                        sb.AppendLine($"口座情報:{record.AccountingName}");

                        //...中略
                     }
             return sb.ToString();

 という書き方で、独自のコレクションクラスに対しても、LINQを用いれば簡単に集約した結果を記述できる。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

連想配列を説明する

じゃあまずC#で書く。

ex.cs
using System;
using System.Collections.Generic;
public class Ex{
    public static void Main(){
        var jcpmusicdotcom = new Dictionary<string,string>();
        jcpmusicdotcom["東方ロストワード"] = "ロストワードクロニカル";
        jcpmusicdotcom["俺はボーマンダ"] = "レプリカの恋";
        jcpmusicdotcom["ほのぼの神社"] = "SHAMAN QUEEN";
        foreach(var music in jcpmusicdotcom){
            Console.WriteLine($"{music.Value}は、{music.Key}を連想する音楽です。");
        }
    }
}

次にPythonで書いてみる。

ex.py
class Ex:
    def main():
        jcpmusicdotcom = {}
        jcpmusicdotcom["東方ロストワード"] = "ロストワードクロニカル"
        jcpmusicdotcom["俺はボーマンダ"] = "レプリカの恋"
        jcpmusicdotcom["ほのぼの神社"] = "SHAMAN QUEEN"
        for musickey in jcpmusicdotcom:
            print(f"{jcpmusicdotcom[musickey]}は、{musickey}を連想する音楽です。")

if __name__ == "__main__":
    Ex.main()

やはりどちらもC言語の影響があるので大して書き方が変わらないですね。私は、結構C#とかPythonとかの「汚い書き方ができないスタイル」が好きなのですが、皆さんはどうですか?コメントよろしくお願いします。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[C#] overrideとnewの違い

(5/15追記:@shiracamusさんより指摘コメントあり、内容に間違いがあったため図解を差し替えています。)

overrideとnewの違いで少し混乱したためまとめ。
解説サイトはいくつもあるけれども、図解しているものがなかったため、図で考えてみた。
下記のような、BクラスがAクラスを継承している場合を考えてみる。

    class A
    {
      public void Test_New() => Console.WriteLine("A::Test_New");
      public virtual void Test_Override() => Console.WriteLine("A::Test_Override");
    }
    class B : A
    {
      public new void Test_New() => Console.WriteLine("B::Test_New");
      public override void Test_Override() => Console.WriteLine("B::Test_Override");
    }

実行を以下の様に行なうと、

      A a = new B();
      a.Test_New();
      a.Test_Override();

      B b = new B();
      b.Test_New();
      b.Test_Override();

結果は以下になる。
image.png

つまり、図解(下記、@shiracamusさんのソースコードに基づいている)すると
image.png

となる。
overrideの場合、派生元のメソッドを文字通り上書きしに行くので、イメージとしてはAとBが重なり合う感じ。
newの場合も、文字通り追加するので、イメージとしてはインスタンス内に二つのメソッドが存在する。
この場合、呼ばれるのは型による。
(A.Test_New()やB.Test_New()とすると、型によって呼ばれる先が異なるのはイメージできると思う)

また、@shiracamusさんのソースコードについて考えてみると

class A
{
    public int value = 123;
    public void Test_New() {Console.WriteLine($"A::New : {value}");}
    public virtual void Test_Override() {Console.WriteLine($"A::Override : {value}");}
}

class B : A
{
    public new int value = 456;
    public new void Test_New() {Console.WriteLine($"B::New : {value}");}
    public override void Test_Override() {Console.WriteLine($"B::Override : {value}");}
}

について、図解の灰色の部分は呼ばれることはないので、

        Console.WriteLine("A a = new B()");
        A a = new B();
        a.Test_New();
        a.Test_Override();
        ((B)a).Test_New(); // b.Test_New()と同等

        Console.WriteLine();

        Console.WriteLine("B b = new B()");
        B b = new B();
        b.Test_New();
        b.Test_Override();
        ((A)b).Test_New(); // a.Test_New()と同等

の結果は、@shiracamusさんの示された下記の通りになる。

A a = new B()
A::New : 123
B::Override : 456
B::New : 456

B b = new B()
B::New : 456
B::Override : 456
A::New : 123

表にすると、
image.png
となる。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【C#】文字列結合の負荷について検証

概要

C#には文字列を結合する方法がいくつかあります。
今回はそのうち5つを試してみたので、その結果を記載します。

検証した方法

・+演算子
・StringBuilder
・文字列補間→$"Name:{name}"など($ - 文字列補間 - C# リファレンス | Microsoft Docs)
・string.Format
・String.Concat

検証の設定

・それぞれの方法を100,000,000回(1億回)繰り返し、処理時間を計測
・System.Diagnostics.Stopwatchクラスを使用して繰り返し文の前後で
 スタート・ストップをして時間を出力

検証

以下の関数を実行して計測

StringMixTestA
    private static void StringMixTestA()
    {
        Console.WriteLine("--- 計測開始A ---");

        string strA = "TestStringA";
        string strB = "TestStringB";
        var stopWatch = new System.Diagnostics.Stopwatch();

        // 通常の"a" + "b";
        stopWatch.Start();
        for (int i = 0; i < testCount; i++)
        {
            var result = "Test A:" + strA + " B:"+ strB;
        }
        stopWatch.Stop();
        Console.WriteLine("Plus:\t\t" + stopWatch.Elapsed.ToString());

        // StringBuilderのAppend"a" + "b";
        stopWatch.Restart();
        for (int i = 0; i < testCount; i++)
        {
            StringBuilder stringBuilder = new StringBuilder();
            stringBuilder.Append("Test A:");
            stringBuilder.Append(strA);
            stringBuilder.Append(" B:");
            stringBuilder.Append(strB);
        }
        stopWatch.Stop();
        Console.WriteLine("StringBuilder:\t" + stopWatch.Elapsed.ToString());

        // 文字列補間
        stopWatch.Restart();
        for (int i = 0; i < testCount; i++)
        {
            var result = $"Test A:{strA} Test B:{strB}";
        }
        stopWatch.Stop();
        Console.WriteLine("$:\t\t" + stopWatch.Elapsed.ToString());

        // string.Format()
        stopWatch.Restart();
        for (int i = 0; i < testCount; i++)
        {
            var result = string.Format("Test A:{0} Test B:{1}", strA, strB);
        }
        stopWatch.Stop();
        Console.WriteLine("string.Format:\t" + stopWatch.Elapsed.ToString());

        // String.Concat
        stopWatch.Restart();
        for (int i = 0; i < testCount; i++)
        {
            var result = String.Concat("Test A:", strA, " Test B:", strB);
        }
        stopWatch.Stop();
        Console.WriteLine("String.Concat:\t" + stopWatch.Elapsed.ToString());

        Console.WriteLine("--- 計測終了A ---");
    }

検証結果

方法 計測時間(時:分:秒)
Plus 00:00:08.8524457
StringBuilder 00:00:16.8160575
$ 00:00:08.7606260
string.Format 00:00:25.5426815
String.Concat 00:00:08.6294175

というわけでString.Concatが一番早いようですが、
調べてみるとStringBuilderが早いという記事もあったので
別パターンも書いて計測してみました。

検証B

以下の関数を実行して計測。
先に変数を定義し、+=で結合する形に変更。
※StringBuilderは初めの検証と同じ

StringMixTestB
    private static void StringMixTestB()
    {
        Console.WriteLine("--- 計測開始B ---");

        string strA = "TestStringA";
        string strB = "TestStringB";
        var stopWatch = new System.Diagnostics.Stopwatch();

        // 通常の"a" + "b";
        stopWatch.Start();
        for (int i = 0; i < testCount; i++)
        {
            var result = "";
            result += "Test A:" + strA + " B:" + strB;
        }
        stopWatch.Stop();
        Console.WriteLine("Plus:\t\t" + stopWatch.Elapsed.ToString());

        // StringBuilderのAppend"a" + "b";
        stopWatch.Restart();
        for (int i = 0; i < testCount; i++)
        {
            StringBuilder stringBuilder = new StringBuilder();
            stringBuilder.Append("Test A:");
            stringBuilder.Append(strA);
            stringBuilder.Append(" B:");
            stringBuilder.Append(strB);
        }
        stopWatch.Stop();
        Console.WriteLine("StringBuilder:\t" + stopWatch.Elapsed.ToString());

        // 文字列補間
        stopWatch.Restart();
        for (int i = 0; i < testCount; i++)
        {
            var result = "";
            result += $"Test A:{strA} Test B:{strB}";
        }
        stopWatch.Stop();
        Console.WriteLine("$:\t\t" + stopWatch.Elapsed.ToString());

        // string.Format()
        stopWatch.Restart();
        for (int i = 0; i < testCount; i++)
        {
            var result = "";
            result += string.Format("Test A:{0} Test B:{1}", strA, strB);
        }
        stopWatch.Stop();
        Console.WriteLine("string.Format:\t" + stopWatch.Elapsed.ToString());

        // String.Concat
        stopWatch.Restart();
        for (int i = 0; i < testCount; i++)
        {
            var result = "";
            result += String.Concat("Test A:", strA, " Test B:", strB);
        }
        stopWatch.Stop();
        Console.WriteLine("String.Concat:\t" + stopWatch.Elapsed.ToString());

        Console.WriteLine("--- 計測終了B ---");
    }

検証結果B

方法 計測時間(時:分:秒)
Plus 00:00:17.1915427
StringBuilder 00:00:16.7240321
$ 00:00:17.1817604
string.Format 00:00:26.1338767
String.Concat 00:00:17.0150580

この結果ではStringBuilderが一番早いですね。
すでに文字列があり、そこに足していくような場合はStringBuilderが良さそうです。

検証C

以下の関数を実行して計測。
先に変数を定義し、そのまま代入する形に変更。
※StringBuilderは初めの検証と同じ

StringMixTestC
    private static void StringMixTestC()
    {
        Console.WriteLine("--- 計測開始C ---");

        string strA = "TestStringA";
        string strB = "TestStringB";
        var stopWatch = new System.Diagnostics.Stopwatch();

        // 通常の"a" + "b";
        stopWatch.Start();
        for (int i = 0; i < testCount; i++)
        {
            var result = "";
            result = "Test A:" + strA + " B:" + strB;
        }
        stopWatch.Stop();
        Console.WriteLine("Plus:\t\t" + stopWatch.Elapsed.ToString());

        // StringBuilderのAppend"a" + "b";
        stopWatch.Restart();
        for (int i = 0; i < testCount; i++)
        {
            StringBuilder stringBuilder = new StringBuilder();
            stringBuilder.Append("Test A:");
            stringBuilder.Append(strA);
            stringBuilder.Append(" B:");
            stringBuilder.Append(strB);
        }
        stopWatch.Stop();
        Console.WriteLine("StringBuilder:\t" + stopWatch.Elapsed.ToString());

        // 文字列補間
        stopWatch.Restart();
        for (int i = 0; i < testCount; i++)
        {
            var result = "";
            result = $"Test A:{strA} Test B:{strB}";
        }
        stopWatch.Stop();
        Console.WriteLine("$:\t\t" + stopWatch.Elapsed.ToString());

        // string.Format()
        stopWatch.Restart();
        for (int i = 0; i < testCount; i++)
        {
            var result = "";
            result = string.Format("Test A:{0} Test B:{1}", strA, strB);
        }
        stopWatch.Stop();
        Console.WriteLine("string.Format:\t" + stopWatch.Elapsed.ToString());

        // String.Concat
        stopWatch.Restart();
        for (int i = 0; i < testCount; i++)
        {
            var result = "";
            result = String.Concat("Test A:", strA, " Test B:", strB);
        }
        stopWatch.Stop();
        Console.WriteLine("String.Concat:\t" + stopWatch.Elapsed.ToString());

        Console.WriteLine("--- 計測終了C ---");
    }

検証結果C

方法 計測時間(時:分:秒)
Plus 00:00:08.2999059
StringBuilder 00:00:16.2361791
$ 00:00:08.5158497
string.Format 00:00:25.3592823
String.Concat 00:00:08.5106941

この場合は+演算子が一番早かったですが、変数展開やString.Concatとは誤差程度ですね。

検証D

以下の関数を実行して計測。
StringBuilderが次々追加するのに対し、
Plusがいっぺんに合成しているのが不公平だと思ったので
+で一つの要素ごとに追加するようにしてみた。

StringMixTestC
    private static void StringMixTestD()
    {
        Console.WriteLine("--- 計測開始D ---");

        string strA = "TestStringA";
        string strB = "TestStringB";
        var stopWatch = new System.Diagnostics.Stopwatch();

        // 通常の"a" + "b";
        stopWatch.Start();
        for (int i = 0; i < testCount; i++)
        {
            var result = "";
            result += "Test A:";
            result += strA;
            result += " B:";
            result += strB;
        }
        stopWatch.Stop();
        Console.WriteLine("Plus:\t\t" + stopWatch.Elapsed.ToString());

        // StringBuilderのAppend"a" + "b";
        stopWatch.Restart();
        for (int i = 0; i < testCount; i++)
        {
            StringBuilder stringBuilder = new StringBuilder();
            stringBuilder.Append("Test A:");
            stringBuilder.Append(strA);
            stringBuilder.Append(" B:");
            stringBuilder.Append(strB);
        }
        stopWatch.Stop();
        Console.WriteLine("StringBuilder:\t" + stopWatch.Elapsed.ToString());

        Console.WriteLine("--- 計測終了D ---");
    }

検証結果D

方法 計測時間(時:分:秒)
Plus 00:00:16.1928413
StringBuilder 00:00:15.2582835

何度か合成を行う場合はStringBuilderの方が早いということがわかりました。

まとめ

例えば"100.0kg"のような数値+単位を表すような決まった文字列の場合は
速度の面で言えば+演算子、変数展開、String.Concatがよいでしょう。
より可読性が高いのは変数展開だと私は思っているので
文字列補間をおすすめしたいです。

何度も同じ変数に文字列を足していくような処理の場合は
StringBuilderがよさそうです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む