HUSKING - kotteri

技術系Note

【C#】判別分析法を用いて画像2値化の閾値を算出してみる

前回の続き

husk.hatenablog.com


今回は対象の画像から閾値を動的に算出するメソッドを作成してみた

        /// <summary>
        /// 判別分析法により閾値を求める
        /// </summary>
        /// <param name="img"></param>
        /// <returns></returns>
        public int GetThreshold(Bitmap img)
        {
            BitmapData imgData = null;
            try
            {
                //=====================================================================
                // 変換する画像の1ピクセルあたりのバイト数を取得
                //=====================================================================
                PixelFormat pixelFormat = img.PixelFormat;
                int pixelSize = Image.GetPixelFormatSize(pixelFormat) / 8;

                //=====================================================================
                // 変換する画像データをアンマネージ配列にコピー
                //=====================================================================
                imgData = img.LockBits(
                    new Rectangle(0, 0, img.Width, img.Height),
                    ImageLockMode.ReadWrite,
                    pixelFormat);
                byte[] buf = new byte[imgData.Stride * imgData.Height];
                Marshal.Copy(imgData.Scan0, buf, 0, buf.Length);

                //=====================================================================
                // ヒストグラム算出
                //=====================================================================
                int[] hist = new int[256];
                int sum = 0;
                int cnt = 0;

                for (int y = 0; y < imgData.Height; y++)
                {
                    for (int x = 0; x < imgData.Width; x++)
                    {
                        // ピクセルで考えた場合の開始位置を計算する
                        int pos = y * imgData.Stride + x * pixelSize;

                        // ピクセルの輝度を算出
                        int gray = (int)(0.299 * buf[pos + 2] + 0.587 * buf[pos + 1] + 0.114 * buf[pos]);

                        hist[gray]++;
                        sum += gray;
                        cnt++;
                    }
                }

                // 全体の輝度の平均値
                double ave = sum / cnt;

                //=====================================================================
                // 閾値算出
                //=====================================================================
                int sh = 0;
                double sMax = 0;

                for (int i = 0; i < 256; i++)
                {
                    // クラス1とクラス2のピクセル数とピクセル値の合計値を算出
                    int n1 = 0;
                    int n2 = 0;
                    int sum1 = 0;
                    int sum2 = 0;

                    for (int j = 0; j < 256; j++)
                    {
                        if (j <= i)
                        {
                            n1 += hist[j];
                            sum1 += hist[j] * j;
                        }
                        else
                        {
                            n2 += hist[j];
                            sum2 += hist[j] * j;
                        }
                    }

                    // クラス1とクラス2のピクセル値の平均を計算
                    double ave1 = sum1 == 0 ? 0 : sum1 / n1;
                    double ave2 = sum2 == 0 ? 0 : sum2 / n2;

                    // クラス間分散の分子を計算
                    double s = n1 * n2 * Math.Pow((ave1 - ave2), 2);

                    // クラス間分散の分子が最大のとき、クラス間分散の分子と閾値を記録
                    if (s > sMax)
                    {
                        sh = i;
                        sMax = s;
                    }
                }
                return sh;
            }
            catch (Exception ex)
            {
                MessageBox.Show(ex.Message);
                return 0;
            }
            finally
            {
                if (img != null && imgData != null)
                {
                    img.UnlockBits(imgData);
                }
            }
        }

このメソッドの戻り値で閾値が取得できる
<例>194


前回のプログラムと今回のを組み合わせれば、画像に応じて最適な閾値を取得して、うまく2値化できる・・・はず。

【C#】画像の2値化(閾値固定)

画像処理として、2値化をやってみる
閾値を固定にしています。いつか判別分析で求めてみたい。

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using System.IO;
using System.Drawing;
using System.Drawing.Imaging;
using System.Runtime.InteropServices;

namespace TestImage
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }
        
        /// <summary>
        /// Formロード時処理
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void Form1_Load(object sender, EventArgs e)
        {
            // 元の画像を表示(左)
            string imagePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "test.jpg");
            pictureBox1.SizeMode = PictureBoxSizeMode.Zoom;
            pictureBox1.ImageLocation = imagePath;

            // 変換後の画像を表示(右)
            pictureBox2.SizeMode = PictureBoxSizeMode.Zoom;
            Bitmap img = new Bitmap(imagePath);
            CreateImage(img);
            pictureBox2.Image = img;
        }

        /// <summary>
        /// 画像を2値化する(24ビット画像のみ)
        /// </summary>
        /// <param name="srcImg">変換する画像</param>
        public void CreateImage(Bitmap srcImg)
        {
            BitmapData srcData = null;
            try
            {
                //=====================================================================
                // 変換する画像の1ピクセルあたりのバイト数を取得
                //=====================================================================
                PixelFormat pixelFormat = srcImg.PixelFormat;
                int pixelSize = Image.GetPixelFormatSize(pixelFormat) / 8;

                //=====================================================================
                // 変換する画像データをアンマネージ配列にコピー
                //=====================================================================
                srcData = srcImg.LockBits(
                    new Rectangle(0, 0, srcImg.Width, srcImg.Height),
                    ImageLockMode.ReadWrite,
                    pixelFormat);
                byte[] buf = new byte[srcData.Stride * srcData.Height];
                Marshal.Copy(srcData.Scan0, buf, 0, buf.Length);

                //=====================================================================
                // 2値化
                //=====================================================================
                for (int y = 0; y < srcData.Height; y++)
                {
                    for (int x = 0; x < srcData.Width; x++)
                    {
                        // ピクセルで考えた場合の開始位置を計算する
                        int pos = y * srcData.Stride + x * pixelSize;

                        // ピクセルの輝度を算出
                        int gray = (int)(0.299 * buf[pos + 2] + 0.587 * buf[pos + 1] + 0.114 * buf[pos]);

                        if (gray > 127)
                        {
                            // 閾値を超えた場合、白
                            buf[pos] = 0xFF;
                            buf[pos + 1] = 0xFF;
                            buf[pos + 2] = 0xFF;
                        }
                        else
                        {
                            // 閾値以下の場合、黒
                            buf[pos] = 0x0;
                            buf[pos + 1] = 0x0;
                            buf[pos + 2] = 0x0;
                        }
                    }
                }

                Marshal.Copy(buf, 0, srcData.Scan0, buf.Length);
            }
            catch (Exception ex)
            {
                MessageBox.Show(ex.Message);
            }
            finally
            {
                if (srcImg != null && srcData != null)
                {
                    srcImg.UnlockBits(srcData);
                }
            }
        }
    }

}

結果は以下の通り
f:id:huskworks53:20180821225651p:plain

閾値をちゃんとしないとダメですね。。

【C#】XMLを扱ってみる

ほとんどJSONの時と同じ

husk.hatenablog.com

1. XMLファイル作成用メソッドを用意

using System.IO; // ←追加
using System.Xml.Serialization; // ← 追加

namespace XMLTestApp
{
    public static class XmlUtils
    {
        /// <summary>
        /// XMLデータ書き込み
        /// </summary>
        /// <param name="obj">XMLへ変換するオブジェクト</param>
        /// <param name="filePath">作成するXMLファイルのパス</param>
        public static void writeXml (object obj, string filePath)
        {
            using (FileStream fs = new FileStream(filePath.ToString(), FileMode.Create))
            using (StreamWriter sw = new StreamWriter(fs, Encoding.UTF8))
            {
                XmlSerializer serializer = new XmlSerializer(typeof(XML.XmlData));
                serializer.Serialize(sw, xmlData);
            }
        }
    }
}

2. XMLファイル読込用メソッドを用意

using System.IO; // ←追加
using System.Xml.Serialization; // ← 追加

namespace XMLTestApp
{
    public static class XmlUtils
    {
        /// <summary>
        /// XMLデータ読み込み
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="filePath">読み込むXMLファイルのパス</param>
        /// <returns>XMLから変換されたオブジェクト</returns>
        public static T readXml<T> (string filePath)
        {
            if (!File.Exists(filePath.TOString()))
            {
                // ファイルが存在しない場合終了
                return null;
            }
            using (FileStream fs = new FileStream(filePath.ToString(), FileMode.Open))
            {
                 XmlSerializer serializer = new XmlSerializer(typeof(XML.XmlData));
                 return (T)serializer.Deserialize(fs);
            }
        }
    }
}

3. 実際の使用例

まずXMLへ変換するためのオブジェクトクラスを作成

using System.Xml.Serialization; // ←追加

namespace XMLTestApp
{
    [XmlRoot("PersonList")]
    public class SampleData
    {
        [XmlElement("person")]
        public List<Person> person { get; set; }
    }
    
    public class Person
    {
        [XmlAttribute("name")]
        public string name { get; set; }
        
        [XmlElement("age")]
        public string age { get; set; }
        
        [XmlElement("sex")]
        public string sex { get; set; }
    }
}

①オブジェクトからXMLへ変換

static void Main(string[] args)
{
    // サンプルデータを準備
    SampleData data = new SampleData();
    
    // 一人目の顧客情報
    Person person1 = new Person();
    person1.name = "大泉 洋";
    person1.age = "44歳";
    person1.sex = "男";
    
    // 二人目の顧客情報
    Person person2 = new Person();
    person2.name = "安田 顕";
    person2.age = "43歳";
    person2.sex = "男";
    
    // 三人目の顧客情報
    Person person3 = new Person();
    person3.name = "鈴井 貴之";
    person3.age = "55歳";
    person3.sex = "男";
    
    data.add(person1);
    data.add(person2);
    data.add(person3);
    
    // オブジェクトからXMLへ変換
    string filePath = "C:\sample.xml"
    XmlUtils.writeXml(data, filePath);
}

↓結果、以下の内容のXMLファイルが作成される

<?xml version="1.0" encoding="utf-8"?>
<PersonList xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
  <person name="大泉 洋">
    <age>44歳</age>
    <sex></sex>
  </person>
  <person name="安田 顕">
    <age>43歳</age>
    <sex></sex>
  </person>
  <person name="鈴井 貴之">
    <age>55歳</age>
    <sex></sex>
  </person>
</PersonList>


XMLからオブジェクトへ変換 
XMLファイルは上記で作成したものを使用

static void Main(string[] args)
{
    // XMLからオブジェクトへ変換
    string filePath = "C:\sample.xml"
    SampleData data = XmlUtils.readXml<SampleData>(filePath);
    
    foreach (Person p in data.person)
    {
        Console.WriteLine(p.name);
        Console.WriteLine(p.age);
        Console.WriteLine(p.sex);
        Console.WriteLine("\n");
    }
}

↓コンソール出力

大泉 洋
44歳
男

安田 顕
43歳
男

鈴井 貴之
55歳
男

【XAMARIN】グラフを描いてみる

OxyPlotなど色々なプラグインがあったのですが、デザイン的に
「Microcharts」
を採用してみました

1. 共通プロジェクト、iOSプロジェクト、Androidプロジェクトにライブラリを追加

Nugetにて以下のライブラリを全てのプロジェクトに追加

  • Microcharts
  • Microcharts.Forms

2. 共通プロジェクトの 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:microcharts="clr-namespace:Microcharts.Forms;assembly=Microcharts.Forms"
             xmlns:local="clr-namespace:TestProject" 
             x:Class="TestProject.MainPage">
    <StackLayout>
        <microcharts:ChartView x:Name="chartView" HorizontalOptions="FillAndExpand" VerticalOptions="FillAndExpand"/>
    </StackLayout>
</ContentPage>

3. 共通プロジェクトの MainPage.xaml.cs を編集する

public MainPage()
{
    InitializeComponent();

    List<Microcharts.Entry> entries = new List<Microcharts.Entry>
    {
        new Microcharts.Entry(200)
        {
            Label = "Data 1",
            ValueLabel = "200",
            Color = SkiaSharp.SKColor.Parse("#266489")
        },
        new Microcharts.Entry(400)
        {
            Label = "Data 2",
            ValueLabel = "400",
            Color = SkiaSharp.SKColor.Parse("#68B9C0")
        },
        new Microcharts.Entry(-200)
        {
            Label = "Data 3",
            ValueLabel = "-200",
            Color = SkiaSharp.SKColor.Parse("#90D585")
        }
    };

    this.chartView.Chart = new Microcharts.BarChart { Entries = entries };
}

4. 動かす

棒グラフが表示された。
※画像はFontawesomeのテストも表示されてしまっています
f:id:huskworks53:20180810015149p:plain:w100


棒グラフの他に、円グラフや線グラフなども簡単にできそう

あと、ラベルの大きさはどう変えるんだろうか・・・

【XAMARIN】FontAwesomeを使ってみる(Iconizeプラグイン使用)

Webページ作成でよくお世話になる「FontAwesome」をXamarinでも使ってみる

調べてみると既にプラグインが存在。簡単そうなのでこれを試す

github.com

1. Xamarin.Formプロジェクト(共通プロジェクト)

共通プロジェクトにNugetより以下の二つをインストール

  • Xam.Plugin.Iconize
  • Xam.FormsPlugin.Iconize

2. Xamarin.iOSプロジェクト

iOSプロジェクトにNugetより以下の二つをインストール

  • Xam.Plugin.Iconize
  • Xam.Plugin.Iconize.FontAwesome


次に、AppDelegete.cs に以下を追記する

public override bool FinishedLaunching(UIApplication app, NSDictionary options)
{
    // 追加1
    Plugin.Iconize.Iconize.With(new Plugin.Iconize.Fonts.FontAwesomeModule());

    global::Xamarin.Forms.Forms.Init();

    // 追加2
    FormsPlugin.Iconize.iOS.IconControls.Init();

    LoadApplication(new App());

    return base.FinishedLaunching(app, options);
}


最後に、Info.plistをテキストエディタで開いて、以下を追記visual studio for macからでは直編集できないのが残念)

<dict>
    ・・・省略
    <key>UIAppFonts</key>
    <array>
          <string>iconize-fontawesome.ttf</string>
    </array>
</dict>

3. Xamarin.Androidプロジェクト

AndroidプロジェクトにNugetより以下の二つをインストール

  • Xam.Plugin.Iconize
  • Xam.Plugin.Iconize.FontAwesome

次に、MainActivity.cs に以下を追記する

protected override void OnCreate(Bundle bundle)
{
    TabLayoutResource = Resource.Layout.Tabbar;
    ToolbarResource = Resource.Layout.Toolbar;

    base.OnCreate(bundle);

    // 追加1
    Plugin.Iconize.Iconize.With(new Plugin.Iconize.Fonts.FontAwesomeModule());

    global::Xamarin.Forms.Forms.Init(this, bundle);

    // 追加2
    FormsPlugin.Iconize.Droid.IconControls.Init(Resource.Id.toolbar);

    LoadApplication(new App());
}

4. 実際に試す

今回はナビゲーションページのツールバーと、ラベルにアイコンを表示してみる
(ここから共通プロジェクトのみの話)

App.xaml.cs を以下のように書き直す

public App()
{
    InitializeComponent();

    //MainPage = new MainPage();
    MainPage = new FormsPlugin.Iconize.IconNavigationPage(new MainPage());
}

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:local="clr-namespace:TestIcon"
             xmlns:iconize="clr-namespace:FormsPlugin.Iconize;assembly=FormsPlugin.Iconize"
             x:Class="TestIcon.MainPage">
    <ContentPage.ToolbarItems>
        <iconize:IconToolbarItem Icon="fa-github" IconColor="Red" />
    </ContentPage.ToolbarItems>
    <StackLayout>
        <iconize:IconLabel Text="fa-github" TextColor="Red" HorizontalOptions="Center" VerticalOptions="CenterAndExpand"/>
    </StackLayout>
</ContentPage>


これでツールバーと画面中央に赤色でGithubのアイコンがそれぞれ表示された:)
f:id:huskworks53:20180810013445p:plain:w100

【C#】アプリケーションの2重起動防止(Mutex)

※これが本当に正しいかは自信ありません。。

Windowsフォームアプリケーションの場合

static class Program
{
    // 重複起動チェック用
    private static Mutex mutex;
    
    static void Main()
    {

        bool hasHandle = false;

        // 初期所有権なしでMutexを生成
        // Mutex名を固有にするためGUIDを使用する
        // GUIDはVisual studio > [ツール] > [GUIDの生成]から生成
        using (mutex = new Mutex(false, "{D2B320CD-4CDB-404D-BB95-1D2E0090DBE5}"))
        {
            try
            {
                //---------------------------------------------------------
                // 二重起動のチェック
                //---------------------------------------------------------
                try
                {
                    // Mutexの所有権を要求
                    hasHandle = mutex.WaitOne(0, false);
                }
                catch (AbandonedMutexException)
                {
                    // 別アプリがMutexオブジェクトを開放しないで終了した場合
                    hasHandle = true;
                }
                
                if (hasHandle == false)
                {
                    // Mutexの所有権が得られなかったため、起動済みと判断して終了
                    MessageBox.Show("二重起動のため、プログラムは終了します。");
                    return;
                }

                //---------------------------------------------------------
                // アプリケーション開始
                //---------------------------------------------------------
                Application.EnableVisualStyles();
                Application.SetCompatibleTextRenderingDefault(false);
                Application.Run(new Form1());
            }
            catch (Exception ex)
            {
                // アプリケーション例外処理
            }
            finally
            {
                if (hasHandle)
                {
                    mutex.ReleaseMutex();
                }
                mutex.Close();
            }
        }
    }


WPFアプリケーションの場合

public partial class App : Application
{
    // 重複起動チェック用
    private static Mutex mutex;

    /// <summary>
    /// アプリケーションが終了する時のイベント
    /// </summary>
    /// <param name="e"></param>
    protected override void OnExit(ExitEventArgs e)
    {
        if (mutex == null)
        {
            return;
        }
        // ミューテックスの開放
        mutex.ReleaseMutex();
        mutex.Close();
        mutex = null;
    }

    /// <summary>
    /// アプリケーションが開始される時のイベント
    /// </summary>
    /// <param name="e"></param>
    protected override void OnStartup(StartupEventArgs e)
    {
        try
        {
            bool hasHandle = false;

            // 初期所有権なしでMutexを生成
            // Mutex名を固有にするためGUIDを使用する
            // GUIDはVisual studio > [ツール] > [GUIDの生成]から生成
            mutex = new Mutex(false, "{D2B320CD-4CDB-404D-BB95-1D2E0090DBE5}");

            //---------------------------------------------------------
            // 二重起動のチェック
            //---------------------------------------------------------
            try
            {
                // Mutexの所有権を要求
                hasHandle = mutex.WaitOne(0, false);
            }
            catch (AbandonedMutexException)
            {
                // 別アプリがMutexオブジェクトを開放しないで終了した場合
                hasHandle = true;
            }

            if (hasHandle == false)
            {
                // 所有権が得られなかった場合、起動済みと判断して終了
                MessageBox.Show("二重起動のため、プログラムを終了します");
                mutex.Close();
                mutex = null;
                this.Shutdown();
                return;
            }

            //---------------------------------------------------------
            // アプリケーション開始
            //---------------------------------------------------------
            MainWindow window = new MainWindow();
            window.Show();
        }
        catch (Exception)
        {
            // アプリケーション例外処理
            MessageBox.Show("エラーが発生した為、プログラムを終了します");
            this.Shutdown();
        }
    }
}

【C#】WPFアプリケーションにて起動および終了を制御する

App.xamlを編集

アプリ起動時に呼ばれる画面の設定を削除
・編集前

<Application x:Class="TimeCard.App"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:TimeCard"
    StartupUri="MainWindow.xaml">

↓ StartupUri を削除

・編集後

<Application x:Class="TimeCard.App"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:TimeCard">

App.xaml.csを編集

アプリ起動および終了時の処理を追記

/// <summary>
/// アプリケーションが終了する時のイベント
/// </summary>
/// <param name="e"></param>
protected override void OnStartup(StartupEventArgs e)
{
    // 初期表示画面を呼び出し
    MainWindow window = new MainWindow();
    window.Show();
}

/// <summary>
/// アプリケーションが終了する時のイベント
/// </summary>
/// <param name="e"></param>
protected override void OnExit(ExitEventArgs e)
{
    // 終了時なにかしたければ記入
}