【C#】MDIフォームにリサイズ可能なPanelを設置する

普通のFormに実行中に幅や高さを変えられるパネルを設置する場合は、SplitContainerを配置すればOKですが、MDIフォーム↓に

 

 

SplitContainerを配置すると、

 

 

のようになり、MDIフォームの子フォームの配置する領域(濃いグレーの部分)がPanelで埋まってしまうため、MDIフォームの機能をなしてくれません。

 

そこで、この場合のみSplitterの登場となります。

以下、MDIフォームの右側にリサイズ可能はPanelを配置する例で示します。

 

まず、MDIフォームにPanelを配置します。

 

 

このPanelを右側にDockします。

 

 

すると、MDIフォームの右側にPanelが配置されます。

 

 

この状態だと、まだ、リサイズしてくれないので、次にSplitterを配置します。

すると、Splitterはデフォルトでは左側にDockされてしまうため、Dockプロパティを右(Right)に設定します。

 

 

Splitterが右側に配置された様子↓

 

 

この状態で実行すると、リサイズ可能なPanelとなります。

 

 

しかしながら、MSDNを見るとSplitterは互換性のために残してあるだけで、新規にプログラムを作るときは使わないようにと書いてあります。

いつまで、この方法が使えるかは、かなり怪しいところではありますが、現状、MDIフォームにリサイズできるパネルを配置するには、この方法しか無いと思われ。。

 

C#へ戻る

【ImageDataクラス】画像の輝度値のCSVファイル保存

画像処理をしていると、画像の輝度値をCSVファイルに保存したいという要望はよくあります。

C#で画像の輝度値にアクセスしやすいようにしたImageDataクラスを作成しました。

(Pythonで画像の輝度値をCSVファイルに保存したい場合はこちらの記事を参照ください。)

 

このImageDataクラスではSave()メソッドに拡張子がCSVのファイルにも対応したので、輝度値をCSVファイルに保存するには

var img = new ImagingSolution.Imaging.ImageData("Mandrill.BMP");
img.Save("Mandrill.csv");

というコード量だけで、CSVファイルに保存できます。

ちなみに、CSVファイルに保存したいだけであれば、こちらで公開しているサンプルプログラムでも輝度値をCSVファイルに保存できるので、お試しください。

 

例えば、マンドリルのこの画像↓をCSVファイルに保存すると、

 

下図のように画像の左上から画像の輝度値がカンマ区切りで保存されます。

 

このCSVファイルをエクセルで表示すると、

 

エクセルにはセルの値に連動してセルの色を変える条件付き書式というのがあるのですが、この機能を使うとセルに画像を表示することができます。(ただし、モノクロ画像のみ)

 

やり方は、条件付き書式→カラースケール→その他のルールと選択します。

 

すると、最小値種類の部分を数値にして

 

同様に、最大値種類の部分を数値にして255に設定します。

 

すると、エクセル上に、こんな感じ↓で画像が表示されます。

 

ImageDataクラスライブラリへ戻る

【ImageDataクラス】Regionプロパティ

画像処理プログラムでは、画像の一部分のみを処理するROIRegion of Interest)という機能がよくあります。

このROIを使うことで、必要な部分のみを処理したり、異なる2つの画像において、同じ領域を処理するために、このROIを指定したりします。

 

ImageDataクラスでは、Regionプロパティを用意し、処理領域を.NET FrameworkのSystem.Drawing.Rectangle構造体を使って領域を指定します。

 

例えば、下記のようなコードで

// ImageDataクラスをファイル名を指定してインスタンスする
var img = new ImagingSolution.Imaging.ImageData("Lenna.bmp");
// Region範囲の指定
img.Region = new Rectangle(50, 20, 150, 180);
// 指定された範囲の画像を保存
img.Save("Lenna_ROI.bmp");

領域を解除するには

img.ResetRegion();

 

のようにResetRegionメソッドを呼び出して下さい。

 

有名どこのレナさんですが、領域を指定すると指定された領域を画像ファイルに切り出すことができます。

 

 

ImageDataクラスライブラリへ戻る

【ImageDataクラス】画像の輝度値(画素値)の取得/設定

BorderTypeプロパティInterpolationModeプロパティのページでも少し紹介していますが、画像の輝度値を取得/設定するには

var img = new ImagingSolution.Imaging.ImageData("image.bmp");
// 輝度値の取得
var bright = img[2, 3];
// 輝度値の設定
img[2, 3] = 234;

のようにImageDataクラスオブジェクトを、あたかも配列として値の取得/設定ができるようになっています。

 

また、画像の輝度値の値は通常8bit(0~255)なので、

 

img[2, 3] = -45;  や  img[4, 5] = 258;

 

のようには設定できないのですが、上記のように設定すると、クラス内部的には

 

img[2, 3] = -45;  →  img[2, 3] = 0;

img[4, 5] = 258;  →  img[4, 5] = 255;

 

として設定されるので、輝度値を設定するときに、if文で0~255の範囲チェックは必要ありません。

また、 img[2, 3] = -45;  と設定した場合ですが、MinusValueModeプロパティを使って

 

img.MinusValueMode = ImagingSolution.Imaging.ImageData.MinusValueModeEnum.Absolute;

 

のように設定すると img[2, 3] = -45; は  img[2, 3] = 45;  として設定されます。

MinusValueModeプロパティは何も設定しないとMinusValueModeEnum.Zeroに設定されています。

 

値の取得/設定にはC#のインデクサという機能を用いているのですが、このインデクサに下記4つのオーバーロードを用意しています。

int this[int row, int column] // 取得/設定
int this[int row, int column, int ch]  // 取得/設定
int this[float row, float column]  // 取得のみ
int this[float row, float column, int ch] // 取得のみ

この引数の部分の並びを[row, column]とするか、[column, row]にするか悩んだのですが、画像データを多次元配列で表したときに同じ並びになる[row, column]の順番なので、ご注意ください。

座標っぽく表示すると[y, x]という順番です。

 

row(行番号)とcolumn(列番号)の位置関係ですが、画像の左上を原点[0, 0]として、モノクロ画像(8bit,1channel)の場合、次のようになります。

 

カラー画像(24bit,3channel)の場合、

 this[int row, int column]

の形式で輝度値を参照すると、下図のようになります。

 

これだと、少し分かりづらいので、chの引数を用いて

 this[int row, int column, int ch]

の形式で参照すると、下図のようになります。

 

 

カラー画像(32bit,4channel)は、下図のようになります。

 

 

ImageDataクラスライブラリへ戻る

 

【ImageDataクラス】InterpolationModeプロパティ

BorderTypeプロパティではインデクサをつかって

var img = new ImagingSolution.Imaging.ImageData("image.bmp");
var bright = img[-1, -1];

のように配列の添え字に相当する部分に負の値を設定すると、画像の外側の輝度値(画素値)を参照できるのを紹介しました。

だったら、添え字相当の部分に少数も指定できるんじゃね?!的な発想で画素間の輝度値を取得できるようにしています。

 

下記のコード例ではX座標が1.3、Y座標が2.7の位置を輝度値を取得しています。

var img = new ImagingSolution.Imaging.ImageData("image.bmp");
var bright = img[2.7f, 1.3f];

 

この画素と画素の間の輝度値を周りの画素の輝度の値から算出する方法を補間というのですが、詳細は下記ページを参照ください。

画素の補間(Nearest neighbor,Bilinear,Bicubic)の計算方法

 

この補間方法に有名どころで、Nearest neighorBilinearBicubicの3があり、この補間方法をInterpolationMonoプロパティにInterpolationModeEnumで指定します。

Bilinearが初期値で設定されています。

 

コード例

var img = new ImagingSolution.Imaging.ImageData("image.bmp");

////////////////////////////////////////////////////
// 下記のいづれかを指定します
// NearestNeighbor
img.InterpolationMode = ImagingSolution.Imaging.ImageData.InterpolationModeEnum.NearestNeighbor;
// Bilinear(初期値)
img.InterpolationMode = ImagingSolution.Imaging.ImageData.InterpolationModeEnum.Bilinear;
// Bicubic
img.InterpolationMode = ImagingSolution.Imaging.ImageData.InterpolationModeEnum.Bicubic;
////////////////////////////////////////////////////
// 取得する輝度値
var bright = img[2.7f, 1.3f];

ただし、Bicubicは処理が重いです...

 

ImageDataクラスライブラリへ戻る

 

【ImageDataクラス】BorderTypeプロパティ

ImageDataクラスでは、C#のインデクサという機能を使って、new したImageDataクラスをオブジェクトをあたかも配列かのように扱うことができます。

 

インデクサの例

var img = new ImagingSolution.Imaging.ImageData("image.bmp");
var bright = img[2, 5];

のようにするだけで画像の輝度値を取得することができます。

 

インデクサを用いると、配列ではできない

var bright = img[-1, -1];

のように配列の添え字に相当する部分にマイナスの値を指定することもできます。

 

このインデクサの機能を使って、画像のフィルタ処理で、画像の外周部分において、画像の外側の輝度値を参照する方法をBorderTypeプロパティにより設定を行います。

 

3×3フィルタにおいて、画像の外側を参照してしまう例

 

設定はBorderTypeEnum列挙型を指定します。

 

BordeTypeEnum.Mirrorの場合(初期値)

画像の輪郭部分を基準に折り返すように輝度値を参照します。

img[-1, -1]の値は130となります。

 

BordeTypeEnum.Clampの場合

画像の輪郭部分の輝度値に固定し参照します。

 

img[-1, -1]の値は71となります。

 

BordeTypeEnum.ToZeroの場合

画像の外側の輝度値を0にします。

img[-1, -1]の値は0となります。

 

ImageDataクラスライブラリへ戻る

【ImageDataクラス】サンプルプログラム

ImageDataクラスライブラリ公開のページにて公開しているサンプルプログラムについて簡単に説明しておきます。

 

このサンプルプログラムの開発環境は

  • Visual Studio 2015 C#
  • .NET Framework 4.5.2

となります。

 

上記環境にて、サンプルプログラムを実行すると、下図のような画面が開きます。

 

 

このFileメニューよりOpenを選択し、画像ファイルを指定します。

 

 

このOpenの処理の中身は、ほぼ.NET FrameworkのBitmapクラスなので、開く事のできるファイルもBitmapクラスに従い、*.bmp、*.jpg、*.png、*.tifとなります。

保存(Save)は上記形式に加え、CSVファイルにも保存できます。

 

画像を開いたら、ウィンドウに等倍で画像が表示されます。

 

 

ステータスバー左側にはマウスポインタが指示している画像の座標と、その位置の輝度値(R, G, B)が表示されます。

また、ステータスバー右側には、フィルタ処理を行った時の処理時間が表示されます。

 

画像のフィルタ処理を実行するにはFileメニューより、GrayScaleGaussianSobelの3つの処理を選ぶことができます。

 

 

GrayScaleの処理結果

 

Gaussianフィルタの処理結果

 

Sobelフィルタの処理結果

 

の3つの処理を実装しています。

フィルタ処理の部分はソースを公開しているので、フィルタ処理プログラム作成の参考にしてみて下さい。

 

今回、この3つの処理を選んだのには

 

GrayScael・・・処理の前後で画像のビット数が変わる例

Gaussian・・・画像の外側の輝度値を参照する例

Sobel・・・計算結果が負になる場合がある例

 

の代表的な例として公開しています。

ソースコードを見て頂くと分かりますが、画像の外側も普通に参照しちゃってるっぽいし、フィルタ処理の計算結果が0~255の範囲を超える場合もありそう・・・なのですが、そこは、このImageDataクラスがなんとかしてくれてます。

 

ImageDataクラスライブラリへ戻る

ImageDataクラスライブラリの使用方法

ImageDataクラスライブラリは、知っている人向けにはライブラリ(サンプルプログラム)のダウンロードページよりファイルをダウンロードし、zipファイルを解凍したImageDataSampleフォルダ内にあるImagingSolution.Imaging.ImageData.dllを参照してください!

 

ということなのですが、このライブラリは画像処理プログラム初心者向けを目指しているので、念のため詳細の使用方法です。

 

まず、Visual Stusio 2015を起動し新しいプロジェクトを選択します。

 

次にテンプレート→Visual C#→Windows→Windowsフォームアプリケーション

 

 

と選択し、プロジェクトの名前および場所を指定し、参照をクリックします。

 

 

次にプロジェクト名(WindowsFormsApplication1、プロジェクトの作成時に付けた名前です)を右クリックし、追加参照と選択します。

 

 

次に表示されたウィンドウの参照をクリックします。

すると、参照するファイルの選択ウィンドウが表示されるので、ダウンロードページより取得したサンプルプログラムのファイルを解凍してできたImageDataSamleフォルダ内にある

 

ImagingSolution.Imaging.ImageData.dll

 

のファイルを選択し、追加をクリックします。

 

 

すると、ImagingSolution.Imaging.ImageData.dllの左側にチェックが入っている状態で、OKをクリックします。

 

 

最後にVisual Studioのソリューションエクスプローラー画面の参照の部分にImagingSolution.Imaging.ImageDataが追加されていれば、このライブラリを使用する準備が完了です。

 

 

ImageDataクラスライブラリへ戻る

【C#】ImageDataクラスライブラリ公開

画像処理プログラムの基本は画像の輝度値(画素値)を取得して、様々な処理をすることとなりますが、C#では輝度値を取得するメソッドにSetPixel/GetPixelのメソッドが用意されていますが、これは処理が遅いことで有名。

 

そこで、OpenCVのIplImage構造体やMatクラスのエッセンスを取り入れつつ、.NETのBitmapクラスっぽく、さらに画像データを簡単に扱えるようにすることを目指したImageDataクラスなるライブラリを作成してみました。

 

基本コンセプトは

●画像の輝度値の取得/設定が簡単

●SetPixel/GetPixelよりは高速

●シンプルなプログラムが書けること

 

といったところです。

 

画像処理をこれから学ぼうとする人、とりあえず簡単にアルゴリズムの評価プログラムを作って見たい人向けを想定しています。パフォーマンス重視の方はポインタ(Scan0)を参照してください。

 

というスタンスです。

今のところフリーですので、ご興味があれば使ってみてください。

ただし、本ライブラリを使用した事による不具合等の責任を負えませんので、ご了承願います。

 

ファイル:ImagingSolution.Imaging.ImageData.dll

.NET Frameworkバージョン:4.5.2

Visual Studio 2015 C#を使用

 

ダウンロード

公開日 バージョン サンプルプログラム
2016.3.19 Ver.0.1.1 ImageDataSample011.zip Bitmapファイル読込部分のバグ修正

Reaginプロパティに影響

2016.3.17 Ver.0.1.0 ImageDataSample010.zip 初版

バグなどが含まれる可能性があります。

今後、仕様が変更される可能性もあります。

 

コンストラクター

名前 説明
ImageData(int, int, int, int) 画像の幅、高さ、画像のビット数、メモリのビット数を指定して、ImageDataクラスの新しいインスタンスを初期化します。
ImageData(int, int, int, System.Drawing.Imaging.PixelFormat, System.IntPtr) 画像の幅、高さ、メモリの幅のバイト数、ピクセルフォーマット、外部メモリのポインタを指定して、ImageDataクラスの新しいインスタンスを初期化します。
ImageData(int, int, System.Drawing.Imaging.PixelFormat) 画像の幅、高さ、ピクセルフォーマットを指定して、ImageDataクラスの新しいインスタンスを初期化します。
ImageData(string) 画像ファイル名を指定して、ImageDataクラスの新しいインスタンスを初期化します。

 

プロパティ

名前 説明
BiCubicVal biCubiによる補間の計算時に用いる変数を取得/設定します。

初期値:-0.75

BorderType 画像の外側の輝度値の参照方法をBorderTypeEnumにより取得/設定します。

初期値:BorderTypeEnum.Mirror

BufferBit 画像を格納するメモリのビット数を取得します。

8、16、24、32のいづれを指定します。

ByteAlignment 画像を格納するメモリのアライメントのバイト数を取得します。
Channel 画像のチャンネル数(色数)を取得します。

1, 3, 4のいづれか

Height 画像の高さ(画素数)を取得します。
ImageBit 画像のビット数を取得します。

8~16、24、30、32のいづれを指定します。

InterpolationMode 画素間の輝度値の取得時の補間モードをInterpolationModeEnumにより取得/設定します。

初期値:InterpolationModeEnum.Bilinear

MinusValueMode 輝度値(画素値)に負の値を設定時の処理方法をMinusValueModeEnumにより取得/設定します。

初期値:MinusValueModeEnum.Zero

PixelFormat 画像のピクセルフォーマットをSystem.Drawing.Imaging.PixelFormatにより取得します。
Region 画像処理処理範囲(参照範囲)をSystem.Drawing.Rectangleにより取得/設定します。
Sacn0 画像データを格納するメモリのポインタをSystem.IntPtrにより取得します。
Stride 画像データを格納するメモリの幅をバイト数で取得します。
Width 画像の幅(画素数)を取得します。

 

インデクサ

名前 説明
this[float, float, int] 画像の行位置(Y座標)、列位置(X座標)、チャンネル番号(B[0]、G[1]、R[2])を指定して輝度値を取得します。
this[float, float] 画像データの行位置(Y座標)、列位置(X座標)を指定して輝度値を取得します。
this[int, int, int] 画像の行位置(Y座標)、列位置(X座標)、チャンネル番号(B[0]、G[1]、R[2])を指定して輝度値を取得/設定します。
this[int, int] 画像データの行位置(Y座標)、列位置(X座標)を指定して輝度値を取得/設定します。

 

列挙型

名前 説明
BorderTypeEnum BorderTypeプロパティを指定するのに用います。

Clamp:最外周の輝度値を用いる

Mirror:(初期値)画像の外側から折り返した輝度値を用いる

ToZero:画像の外側は輝度値0にする

InterpolationModeEnum InterpolationModeプロパティで画素間の輝度値を取得する際の補間方法を指定するのに用います。

Bicubic:バイキュービック

Bilinear:(初期値)バイリニア

NearestNeighbor:ニアレストネイバー

MinusValueModeEnum インデクサで負の値を指定した時の処理方法を指定します。

Absolute:負の場合は絶対値をとります。

Zero:(初期値)負の場合は0にします。

 

メソッド

名前 説明
Clone() ImageDataクラスオブジェクトのクローンを作成します。画像データもコピーされます。
Clone(bool) 画像データをコピーする(true)/しない(false)を指定してImageDataクラスオブジェクトのクローンを作成します。
CopyMemory(System.IntPtr, System.IntPtr, int) コピー先のポインタ、コピー元のポインタ、コピーサイズを指定して画像データをコピーします。
CopyTo(ImagingSolution.Imaging.ImageData) 画像データを指定したImageDataクラスオブジェクトへコピーします。
Dispose() ImageDataクラスで使用されているリソースを解放します。
ImageData.IsEqualImageSize(

ImagingSolution.Imaging.ImageData, ImagingSolution.Imaging.ImageData)

2つのImageDataクラスの画像サイズが等しい(true)か等しくない(false)かを確認します。
ResetRegion()  指定した領域(Regionプロパティ)を解除します。
Save(string) ImageDataクラスの画像データをファイルに保存します。
ToBitmap() ImageDataクラスオブジェクトをSystem.Drawing.Bitmapクラスへ変換します。
ZeroMemory(System.IntPtr, int) ポインタで示されているメモリから指定バイト数分、0に設定します。

 

サンプルプログラム

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace ImageDataSample
{
    public partial class Form1 : Form
    {

        /// 
        ImagingSolution.Imaging.ImageData _img;

        public Form1()
        {
            InitializeComponent();
        }

        /// 
        /// 
        private void mnuFileOpen_Click(object sender, EventArgs e)
        {
            //ファイルを開くダイアログボックスの作成  
            var ofd = new OpenFileDialog();
            //ファイルフィルタ  
            ofd.Filter = "Image File(*.bmp,*.jpg,*.png,*.tif)|*.bmp;*.jpg;*.png;*.tif|Bitmap(*.bmp)|*.bmp|Jpeg(*.jpg)|*.jpg|PNG(*.png)|*.png";
            //ダイアログの表示 (Cancelボタンがクリックされた場合は何もしない)
            if (ofd.ShowDialog() == DialogResult.Cancel) return;

            if (_img != null) _img.Dispose();

            _img = new ImagingSolution.Imaging.ImageData(ofd.FileName);

            picImage.Image = _img.ToBitmap();
        }

        /// 
        /// 
        private void mnuFileSave_Click(object sender, EventArgs e)
        {
            // 画像データが設定されていない場合は何もしない
            if (_img == null) return;

            //名前を付けて保存ダイアログボックスの作成  
            var sfd = new SaveFileDialog();
            //ファイルフィルタ  
            sfd.Filter = "Bitmap(*.bmp)|*.bmp|Jpeg(*.jpg)|*.jpg|PNG(*.png)|*.png|CSV(*.csv)|*.csv";
            //ダイアログの表示 (Cancelボタンがクリックされた場合は何もしない)
            if (sfd.ShowDialog() == DialogResult.Cancel) return;

            // 名前を付けて画像データを保存
            _img.Save(sfd.FileName);
        }

        /// 
        /// 
        /// /param>
        private void mnuFileExit_Click(object sender, EventArgs e)
        {
            this.Close();
        }

        /// 

 

ImageDataクラスライブラリへ戻る

ImageDataクラスライブラリ

画像データの取得/設定をできるだけ簡単にできることを目指したImageDataクラスライブラリに関する情報です。

 

 

 

 

画像処理のためのC#へ戻る

【C#】Chartを使ったヒストグラム表示

Chartコントロールを使いたかった理由の一つに画像処理ではおなじみのヒストグラムをChartコントロールで表示したかったのですが、その簡単なプログラムです。

 

フォームにはChartコントロールを配置し、Chartの名前がchart1とした事を前提として、

// /////////////////////////////////////////////////////
// Chartコントロール内のグラフ、凡例、目盛り領域を削除
chart1.Series.Clear();
chart1.Legends.Clear();
chart1.ChartAreas.Clear();

// /////////////////////////////////////////////////////
// 目盛り領域の設定
var ca = chart1.ChartAreas.Add("Histogram");

// X軸
ca.AxisX.Title = "Brightness";  // タイトル
ca.AxisX.Minimum = 0;           // 最小値
ca.AxisX.Maximum = 256;         // 最大値
ca.AxisX.Interval = 64;         // 目盛りの間隔
// Y軸
ca.AxisY.Title = "Count";
ca.AxisY.Minimum = 0;

// /////////////////////////////////////////////////////
// データの追加
var hist = new Point[] {
    new Point(32, 10),
    new Point(96, 30),
    new Point(160, 50),
    new Point(224, 20)
};

// グラフの系列を追加
var s = chart1.Series.Add("Histogram");
// 棒グラフの隙間を無くす
s.SetCustomProperty("PointWidth", "1.0");
// データ設定
for (int i = 0; i < hist.Length; i++) {
    s.Points.AddXY(hist[i].X, hist[i].Y);
}

と書くだけで、モノクロ画像用のヒストグラムが簡単に作成できます。

 

(実行結果)

 

ここでのポイントとしては、棒グラフ(ChartTypeColumn)の時に、棒グラフの間隔を無くすにはPointWidthプロパティを1.0にすればよい(デフォルトは0.8)のですが、各グラフ特有のプロパティ設定は以下のように設定する必要があります。

(直接プロパティの設定ができません。)

// 棒グラフの隙間を無くす
s.SetCustomProperty("PointWidth", "1.0");

モノクロ画像の時はこれでもよいのですが、カラーだと、R,G,Bの3つのグラフを書かないといけないので、折れ線では表現しにくいので、折れ線(ChartTypeLine)を使ってみたいと思います。

 

要領は同じで、SeriesにR,G,B用の3つを追加することぐらいです。

// /////////////////////////////////////////////////////
// Chartコントロール内のグラフ、凡例、目盛り領域を削除
chart1.Series.Clear();
chart1.Legends.Clear();
chart1.ChartAreas.Clear();

// /////////////////////////////////////////////////////
// 目盛り領域の設定
var ca = chart1.ChartAreas.Add("Histogram");

// X軸
ca.AxisX.Title = "Brightness";  // タイトル
ca.AxisX.Minimum = 0;           // 最小値
ca.AxisX.Maximum = 256;         // 最大値
ca.AxisX.Interval = 64;         // 目盛りの間隔
// Y軸
ca.AxisY.Title = "Count";
ca.AxisY.Minimum = 0;

// /////////////////////////////////////////////////////
// データの追加
var histR = new Point[] {
    new Point(0, 10),
    new Point(63, 30),
    new Point(127, 80),
    new Point(191, 30),
    new Point(255, 15)
};
var histG = new Point[] {
    new Point(0, 70),
    new Point(63, 60),
    new Point(127, 40),
    new Point(191, 20),
    new Point(255, 10)
};
var histB = new Point[] {
    new Point(0, 10),
    new Point(63, 20),
    new Point(127, 30),
    new Point(191, 70),
    new Point(255, 90)
};

// グラフの系列を追加
var sR = chart1.Series.Add("HistogramR");
var sG = chart1.Series.Add("HistogramG");
var sB = chart1.Series.Add("HistogramB");

// グラフの種類を折れ線に設定する
sR.ChartType = sG.ChartType = sB.ChartType 
    = System.Windows.Forms.DataVisualization.Charting.SeriesChartType.Line;

// データ設定
for (int i = 0; i < histR.Length; i++)
{
    sR.Points.AddXY(histR[i].X, histR[i].Y);
    sG.Points.AddXY(histG[i].X, histG[i].Y);
    sB.Points.AddXY(histB[i].X, histB[i].Y);
}

(実行結果)

 

これでカラーのヒストグラムも表示できる事ができます!

とか言っても、見た目があまりにもショボイので、もう少し手を入れます。

 

今度はグラフの種類にスプライン面グラフ(SeriesChartType.SplineArea)を使ってみます。

// /////////////////////////////////////////////////////
// Chartコントロール内のグラフ、凡例、目盛り領域を削除
chart1.Series.Clear();
chart1.Legends.Clear();
chart1.ChartAreas.Clear();

// /////////////////////////////////////////////////////
// 目盛り領域の設定
var ca = chart1.ChartAreas.Add("Histogram");

// X軸
ca.AxisX.Title = "Brightness";  // タイトル
ca.AxisX.Minimum = 0;           // 最小値
ca.AxisX.Maximum = 256;         // 最大値
ca.AxisX.Interval = 64;         // 目盛りの間隔
// Y軸
ca.AxisY.Title = "Count";
ca.AxisY.Minimum = 0;

// /////////////////////////////////////////////////////
// データの追加
var histR = new Point[] {
    new Point(0, 10),
    new Point(63, 30),
    new Point(127, 80),
    new Point(191, 30),
    new Point(255, 15)
};
var histG = new Point[] {
    new Point(0, 70),
    new Point(63, 60),
    new Point(127, 40),
    new Point(191, 20),
    new Point(255, 10)
};
var histB = new Point[] {
    new Point(0, 10),
    new Point(63, 20),
    new Point(127, 30),
    new Point(191, 70),
    new Point(255, 90)
};

// グラフの系列を追加
var sR = chart1.Series.Add("HistogramR");
var sG = chart1.Series.Add("HistogramG");
var sB = chart1.Series.Add("HistogramB");

// グラフの種類をスプライン面グラフに設定する
sR.ChartType = sG.ChartType = sB.ChartType
    = System.Windows.Forms.DataVisualization.Charting.SeriesChartType.SplineArea;

// 輪郭線の太さ
sR.BorderWidth = sG.BorderWidth = sB.BorderWidth = 2;

// 輪郭線の色
sR.BorderColor = Color.Red;
sG.BorderColor = Color.Green;
sB.BorderColor = Color.Blue;

// 塗りつぶしの色の設定(半透明)
sR.Color = Color.FromArgb(150, Color.Red);
sG.Color = Color.FromArgb(150, Color.Green);
sB.Color = Color.FromArgb(150, Color.Blue);

// データ設定
for (int i = 0; i < histR.Length; i++)
{
    sR.Points.AddXY(histR[i].X, histR[i].Y);
    sG.Points.AddXY(histG[i].X, histG[i].Y);
    sB.Points.AddXY(histB[i].X, histB[i].Y);
}

(実行結果)

 

これで、かなりそれっぽくないですか?

プログラム的に書くと、塗りつぶしの色に半透明の色を設定できるところがポイントでしょうかね。

 

Chartコントロールの使用方法へ戻る

【C#】Chartコントロールをとりあえず使ってみる

Chartコントロールは、本当に多くのプロパティがあるため、一つ一つを調べてからプログラムをするよりも、まずはVisual Studioのフォームエディタ上でいろいろ触ってみてから、プロパティの意味するところを覚えて、プログラムを組むようにした方が早そうです。

 

という事で、フォームエディタ上で、いろいろ触ってみました。

 

コントロールの追加

ツールボックスのデータの中にあるChartを選択し、フォームへ追加します。

 

 

グラフデータの追加

グラフのデータはSeriesプロパティで管理されています。

 

Series(コレクション)の部分をクリックし、  をクリックするとエディタが表示されます。

 

 

グラフのデータはデータPointsプロパティの(コレクション)の部分をクリックし、  をクリックすると各データを編集する画面↓が表示されます。

 

 

追加のボタンをクリックするたびにデータが追加されます。

下図は5個のデータを追加した様子

 

 

各データの値はデータのXValueとYValuesの値を設定します。

ここでYの値だけ複数形になっている?!と思えたら、少しChartコントロールに慣れてきている感じでしょうか?

複数形は複数の設定ができます。

 

この状態で5個のデータを表示したのがこちら↓

 

 

グラフの種類の設定

グラフの種類は各系列(Series)ごとに設定ができ(できない組み合わせもあります)、グラフのChartTypeで設定します。

 

 

ここで、縦の棒グラフ(Column)から折れ線グラフ(Line)に変更すると

 

 

上図のようになります。

 

マーカーの追加

マーカーはSeriesプロパティのMakerSyleというプロパティがあります。

 

デフォルトではマーカーなし(None)になっていますが、これをSquareにしてみます。

エクセルっぽい!

 

軸の名前の設定

グラフの軸を管理しているのはChartAreaプロパティになります。

ChartArea(コレクション)の部分をクリックし、  をクリックするとエディタが表示されます。

 

軸を管理しているのがAxesプロパティでさらにエディタを開きます。

 

 

Axisコレクションには最初から

X axis X軸(主軸)
Y(Value) axis Y軸(主軸)
Secondary X axis X軸(第二軸)
Secondary Y axis Y軸(第二軸)

の4つが用意されており、各軸のTitleプロパティで軸の名前を設定します。

 

 

X軸とY軸のTitleを設定した結果↓

 

 

グラフのタイトルの設定

グラフのタイトルはTitleプロパティになります。

Title(コレクション)の部分をクリックし、  をクリックするとエディタが表示されます。

 

 

(Text)の部分を編集するとグラフのタイトルが表示されます。

 

凡例領域の設定

凡例領域の設定はLegendsロパティになります。

Legends(コレクション)の部分をクリックし、  をクリックするとエディタが表示されます。

 

 

Legends1のTitleプロパティに凡例表示と入力してみると

 

 

ん??

Series1の部分が変わるかと思ったら、その上に表示されるだけ?

Legendsプロパティはあくまでも凡例表示全体(各系列の凡例を表示している領域)のプロパティ設定であって、各系列(Series)の設定ではありません。

 

Series1の部分の表示名を変更するにはSeriesプロパティのNameプロパティを設定します。

 

 

凡例の名前が変更されました↓

 

他にも様々なプロパティがあるので、いじりたおすと面白いと思います。

 

これで、この操作をプログラムで表現できれば、何とかなりそうな気がしてきた!

 

Chartコントロールの使用方法へ戻る

【C#】Chartコントロールの主なプロパティ

Chartコントロールを使うにはSeriesプロパティが最も重要になりますが、次にLegends,ChartAreas,Titlesの3つ、Annotationsは、ほとんど使わないと思います。

 

 

●Seriesプロパティ

グラフのデータやグラフの種類を取得、設定します。

エクセルのグラフでいうところのグラフの種類系列を追加した近い感じです。

●Legendsプロパティ

凡例表示領域に関する情報を取得、設定します。

●ChartAreasプロパティ

グラフの目盛り領域に関する情報を取得、設定します。

●Titlesプロパティ

グラスのタイトルに関する情報を取得、設定します。

●Annotationsプロパティ

グラフの注釈に関する情報を取得、設定します。

 

どのプロパティも□□□□□sというように複数形になっているからも想像できると思いますが、複数個設定する事ができます。

 

Chartコントロールの使用方法へ戻る

【C#】SplitContainerの境界線に描画する

SplitContainerの境界線をクリックすると片側のPanelを閉じるなどのGUIで、境界線の部分に三角形などを書きたい場合があります。

こんな感じ↓

 

その場合、どうするのか?

Splitterのオブジェクトを探してみても存在しないのですが、SplitContainerの構造を理解すると、Splitter部分に描画する事ができます。

 

上図のようにSplitContainerはContainerの上に2つのパネル(Panel1,Panel2)が張り付いているイメージです。

SplitterはPanel1とPanel2の隙間から見えるContainerそのものとなっています。

そのため、Splitterに描画するには、隙間から見えるContainerに描画すればOKです。

また、参考までに主なSplitContainerのプロパティは以下の通りです。

 

その事を理解したうえでSplitContainerのPaintイベントで三角印を書くサンプルは以下のようになります。

 private void splitContainer1_Paint(object sender, PaintEventArgs e)
 {
     Point[] p = new Point[3];

     p[0] = new Point(splitContainer1.SplitterDistance, splitContainer1.Height / 2 - splitContainer1.SplitterWidth * 2 / 3);
     p[1] = new Point(splitContainer1.SplitterDistance, splitContainer1.Height / 2 + splitContainer1.SplitterWidth * 2 / 3);
     p[2] = new Point(splitContainer1.SplitterDistance + splitContainer1.SplitterWidth, splitContainer1.Height / 2);

     e.Graphics.FillPolygon(Brushes.Gray, p);
 }

 private void splitContainer1_Resize(object sender, EventArgs e)
 {
     splitContainer1.Refresh();
 }

上記のプログラムはかなりいい加減なので、お好みで修正して下さい。

 

Paintイベントだけでは、SplitContainerのリサイズの時に描画してくれないので、ResizeイベントでコントロールをRefresh()してPaintイベントを発生させています。

 

C#へ戻る

【C#】グローバル変換を使ったアフィン変換

.NETで画像や線などを描画する時はGraphicsオブジェクトに対して描画を行いますが、このGraphicsオブジェクトの座標系をアフィン変換する処理をグローバル変換と言います。

 

グローバル変換された Graphicsオブジェクトに対し描画を行うと、アフィン変換された状態で、画像や線が描画されます。

 

ただ、一般的なアフィン変換では

 

 

のように表現される場合が多いかと多いと思いますが、マイクロソフトの仕様では、上記の行列を転置(行と列を反転する)した

 

 

のように表示されます。

また、アフィン変換の座標系は下図のように、左上が原点、Y軸は下方向、回転は時計周りが正の方向となります。

 

 

そのため、平行移動や拡大縮小、回転の変換行列も一般的な変換行列を転置した表現となります。

 

X軸方向へTx、Y軸方向へTyだけ移動する平行移動の変換行列は、

 

X軸方向へSx倍、Y軸方向へSy倍だけ拡大する変換行列は、

 

原点まわりにθ°回転移動する変換行列は

 

となります。

少し紛らわしいので、一般的なアフィン変換と混同しないように注意して下さい。

このように表現するのは、私の経験的にはマイクロソフトだけ?だと思います。

(参考)

http://msdn.microsoft.com/ja-jp/library/vstudio/c499ats3.aspx

http://msdn.microsoft.com/ja-jp/library/vstudio/8667dchf.aspx

 

しかし、.NET Frameworkでは特に変換行列を知らなくても、アフィン変換用のメソッドが用意されているので、それらを使うと間違いが少ないでしょう。

 

平行移動は TranslateTransform
拡大縮小は ScaleTransform
回転移動は RotateTransform

 

(参考)Graphicsメソッド

http://msdn.microsoft.com/ja-jp/library/system.drawing.graphics_methods%28v=vs.80%29.aspx

 

逆に、行列を使ってアフィン変換する事も可能なので、スキュー変換のような変換も行う事ができます。

(参考)

http://msdn.microsoft.com/ja-jp/library/system.drawing.drawing2d.matrix_methods%28v=vs.80%29.aspx

 

また、アフィン変換を行うと、変換後の座標から、変換前の座標を計算したい場合がありますが、System.Drawing.Graphics.TransformPointsというメソッドがあるので、これを使うと簡単に座標が求められます。

 

これらのメソッドを使って、アフィン変換のサンプルプログラムを作成しました。

 

(ダウンロード)GlobalTransformations.zip(Visual Studio 2008 Exress C#)

 

このサンプルでは見た目上では一般的に用いられる行列の表現に合わせています。
また、エラー処理などは行っていないので、ご了承下さい。

アフィン変換の勉強の手助けになれば...

 

C#へ戻る

64bit対応ユーザーコントロール作成方法

64bitに対応したユーザーコントロール(カスタムコントロール)を作成するには、前提条件として

 

Visual Studio はOSが64bitであっても32bitで動作している!

 

という点に注意しないといけません。

つまり、プラットフォームを64bitで作成したユーザーコントロールをVisual Studioのフォームデザイナでフォーム上に配置すると、32bitプログラムから64bitプログラムを呼び出す事になるので、フォームデザイナがエラーとなり、フォームを表示してくれません。

 

これを回避するためには、ユーザーコントロールのプラットフォームをAny CPUに設定して下さい。

 

この時点で、Any CPUの設定がないC++/CLIのユーザーコントロールは64bit対応ができないという事になります。

これを知らずにC++/CLIのユーザーコントロールを32bitである程度作成してしまい、64bitに対応させようと思った時に、エライ目に会いました...→結局、C#で作り直すハメに。

 

次に私がハマったのが、ユーザーコントロールからアンマネージのライブラリを呼ぶ場合の処理。

 

作成したプログラムのプラットフォームがAny CPUの時の挙動ですが、

32bitOSの場合:32bitのライブラリを呼び出す

64bitOSの場合:64bitのライブラリを呼び出す

となります。

 

つまり、64bitOS環境でユーザーコントロールを開発すると、Visual Studioで開発中にフォームデザイナでは、Any CPUのユーザーコントロールから、64bitのライブラリを呼び出そうとします。

ユーザーコントロールはフォームに配置した時点で、コントロールのプロパティやコンストラクタ、各種イベント処理が動いてしまうので、この時に64bitライブラリの関数が呼ばれてしまうと、フォームデザイナがエラーとなってしまいます。

 

これを回避するには、フォームがデザインモードかどうかを調べる

 System.ComponentModel.Component.DesignModeプロパティ

を用います。

この値がtrueの場合、64bitライブラリの関数を呼び出さないようにすると、回避できます。

 

さらに話をややこしくするのが、DesignModeプロパティはコンストラクタや入れ子となったユーザーコントロールでは正しい値が取得できません

そのため、DesignModeプロパティを取得する際には注意して下さい。

(参考)

http://support.microsoft.com/kb/839202/ja

http://social.msdn.microsoft.com/Forums/ja-JP/csharpexpressja/thread/d946a597-0ba4-4880-b99b-13c728e2f39f

 

まとめると、

●ユーザーコントロールはVB.NETもしくはC#でプラットフォームをAny CPUにして作成する。

●デザイン時に64bitライブラリの関数を呼び出さい。

 

という感じでしょうか?

 

VB.NETやC#だけで作るならプラットフォームは全てAny CPUにしておくと、あまり64bit対応の事は気にしなくても大丈夫ですね。

また、逆に32bit対応のライブラリしかなく、32bitで動くプログラムを作る場合は、64bit対応のライブラリが呼び出されないように、プラットフォームをAny CPUにはせずに、Win32に設定しておく必要があります。

 

Visual Studioへ戻る