ハフ変換

ハフ変換そのものは座標の変換処理なのですが、画像処理では、ハフ変換を用いて画像中の直線部分を抽出するのに用いられます。

ハフ変換と言うだけで、あんに直線検出を指している事が多くあります。

また、ハフ変換を拡張して、円の検出に用いられる場合もあります。

ハフ変換で出来ること

画像の中から、直線が途切れていても、直線らしき部分を抽出したり、前処理でエッジ抽出を行い、輪郭部分を検出する事もできます。

(元画像)

(直線検出)

(元画像)

(輪郭検出)

直線検出のしくみ

まず初めに直線として検出したい場所を二値化して抽出します。

被写体の輪郭を検出したい場合は、SobelCannyなどの前処理を行います。

二値化された座標(XY座標)に関して、X座標をいくつかに分割し、Y軸方向に二値化された座標をカウントします。

このカウントを点を原点に基点に回転させながら、繰り返しカウントを行います。

すると、点が直線的に並んだ時にカウント数が最大となります。

直線なので、逆向き(回転角度が180度反対)の時もカウント数が最大となります。

このカウント数を横軸を回転角度、縦軸をX座標にして、二次元的に表示すると、下図のようになり、カウント値がピークとなるXとθの値から、直線を検出することができます。

ハフ変換のアルゴリズム

点を回転して特定方向に積算することで、直線を見つける事もできるのですが、ハフ変換では、各点を通る直線を回転させ、直線が重なりあう場所をみつける事で、直線を検出します。

点(x, y)を通る線を、下図のようにρθで表すと

$$\rho=x cos\theta+y sin\theta$$

となります。

これは、(x, y)座標が決まっていて、θを与えると、ρが計算できる事になります。

点(x,y)を通る直線をθを0~360°の範囲で刻むと下図のようになります。

この直線をρとθで表すと、下図のようにsinカーブとなります。

 

この処理を各点に関して行うと、点が直線的に並んでいる部分では特定のρとθに集中します。

このρとθの値が集中している点を抽出し、ρとθの値から直線を検出します。

実際のハフ変換

ハフ変換の角度は0~360°で計算すると、必ず180°離れた2か所でρとθが集中するので、0~180°までを計算します。

実際のハフ変換では、エッジ上のxy座標から、θの値を例えば1°おきに0~180°までに対応したρの値を計算し、ρの値の分解能を例えば1などに落として、ρーθの座標系へ投票し、投票した値のカウント数が高い部分が直線となる部分のρ、θとなります。

OpenCV(Python)のサンプルプログラム

import cv2
import math
import numpy as np

# 画像読込
src = cv2.imread('keyboard.jpg', cv2.IMREAD_GRAYSCALE);
# 二値化
_, src = cv2.threshold(src, 200, 255, cv2.THRESH_BINARY)

cv2.imshow("edge image", src)

# ハフ変換
lines = cv2.HoughLines(src, 3, np.pi / 180, 400)

# 結果表示用の画像を作成
dst = cv2.cvtColor(src, cv2.COLOR_GRAY2BGR);

# 直線を描画
line_length = 1000
for line in lines:
    rho = line[0][0]
    theta = line[0][1]
    a = math.cos(theta)
    b = math.sin(theta)
    x0 = a * rho
    y0 = b * rho
    cv2.line(
        dst, 
        (int(x0 - line_length * b), int(y0 + line_length * a)), 
        (int(x0 + line_length * b), int(y0 - line_length * a)), 
        (0, 0, 255), thickness=2, lineType=cv2.LINE_4 )

cv2.imshow("result", dst)

cv2.waitKey()

(処理前の画像)

(処理後画像)

注意事項

ハフ変換を行うと、直線らしい部分が抽出できますが、この直線の位置の精度を高めようと、ハフ変換時のθとρの分解能を高め過ぎると、直線検出の安定性が悪くなります。

そのため、直線の位置精度を高めるには、ハフ変換で直線付近の座標を抽出し、その座標から回帰直線などを求めるようにします。

画像処理100本ノックをC#でやってみた

画像処理100本ノック

https://github.com/yoyoyo-yo/Gasyori100knock

https://qiita.com/yoyoyo_/items/2ef53f47f87dcf5d1e14

 

というページがあるのをご存じでしょうか?

最近の画像処理と言えば、OpenCVをPythonでやってみた!

という情報にあふれているのですが、この画像処理100本ノックでは、画像処理の処理部分はOpenCVに頼らずにベタにPythonやC++でプログラムしています。

 

実際、今どきは画像処理のプログラムをベタに組んだところで、車輪の再開発になるだけで意味があるのか??という意見もあろうかと思いますが、1度はやっておくと画像処理の理解が深まるので、おススメです。

 

そこで私も画像処理100本ノックの問題をC#で作ってみました。

ソースコードはGitHubに置いてあります。

https://github.com/ImagingSolution/ImagingDotNet

 

表向きは画像処理100本ノックに沿ってプログラムしていますが、目指すところはC#だけで、そこそこ処理が速くて、OpenCVのPython版のように簡単に使えるC#の画像処理ライブラリ(ImagingDotNet)を作るのが目標です。

本気で使うとなると、エラー処理は甘いし、処理もすごく速い訳ではないので、あくまでも、C#で作る画像処理プログラムのお勉強用です。

 

Q.1.チャンネル入れ替え

var src = new Bitmap("Lena.bmp");
var dst = ImagingDotNet.CvtColor(src, ImagingDotNet.COLOR_BGR2RGB);
入力 出力

C# >> CvtColor.cs cvt_COLOR_BGR2RGB部分

 

Q.2.グレースケール化

var src = new Bitmap("Lena.bmp");
var dst = ImagingDotNet.CvtColor(src, ImagingDotNet.COLOR_BGR2GRAY);
入力 出力

C# >> CvtColor.cs cvt_COLOR_BGR2GRAY部分

 

Q.3.二値化

(参考)https://imagingsolution.net/imaging/binarization/

var src = new Bitmap("Lena.bmp");
var dst = (Bitmap)src.Clone();
ImagingDotNet.Threshold(src, dst, 128, 255, ImagingDotNet.THRESH_BINARY);
入力 出力

C# >> Threshold.cs

 

Q.4.大津の二値化

(参考)https://imagingsolution.net/imaging/discriminant-analysis-method/

var src = new Bitmap("Lena.bmp");
var gray = ImagingDotNet.CvtColor(src, ImagingDotNet.COLOR_BGR2GRAY);
var dst = (Bitmap)src.Clone();
ImagingDotNet.Threshold(gray, dst, 128, 255, ImagingDotNet.THRESH_OTSU);
入力 出力

C# >> Threshold.cs

 

 

4問やったところで、力尽きました…

100問分、問題を考えてプログラムを作成した100本ノックの著者は、ほんとスゴイ!!

 

画像処理のためのC#テクニック へ戻る

【C#】画像の輝度値の取得設定速度の比較

C#で画像の輝度値を取得/設定を行う場合は、GetPixel、SetPixelメソッドを使うと遅いのでLockBits~UnlockBitsでポインタをむき出して輝度値の取得/設定を行うのが定番となっていますが、自分自身でGetPixel、SetPixelメソッドを使った事が無かったので、いくつかの方法で処理時間を比較してみました。

 

【評価環境】

OS Windows10 64bit ver.1809
CPU Intel Core i7-7700K 4.2GHz
メモリ 32GB
.NET Framework 4.5.2
プラットフォーム Any CPU(32ビットを優先)
評価画像 jpeg 6000×4000画素 24bitカラー

です。

 

評価プログラムとしては、カラー画像の輝度値をR,G,Bそれぞれ取得し、値を明暗反転して輝度値を入れなおすプログラムを作成しています。

 

処理結果は、以下のようにポジ→ネガ→ポジ→ネガと繰り返すようになっています。

 

処理時間は処理を5回行い平均した時間となります。

 

 

GetPixel、SetPixelを使った方法

平均処理時間は27439msec

本当に遅かった。。

しかもGetPixel、SetPixelではモノクロ画像(Format8bppIndexed)に対応していないので、モノクロ画像も多く扱う画像処理には不向きとなります。

 

LockBits~UnlockBitsでポインタ(Scan0)を取得し、データを配列にコピーしてから処理を行い、結果を元にコピーしなおす方法

平均処理時間は101msec

さすがにGetPixel、SetPixelよりはぜんぜん速いです。

 

MarshalクラスのReadByte、WriteByteメソッドでポインタ(Scan0)に直接、値を読み書きする方法

平均処理時間は273.6msec

この方法に少し期待していたのですが、配列にコピーした方が速かった。。

 

unsafeを使ったポインタ(Scan0)を直接読み書きする方法

平均処理時間は25.4msec

やっぱりunsafeはできれば使いたくないのですが、ここまで速いと使うのもあり??

ポインタを使うなら、C言語のライブラリにしておきたい気もしますが、C#だけで完結できるのもちょっと惹かれます。

 

ポインタで読み書きし、Parallel.Forをつかって処理を並列化する方法

平均処理時間は11.4msec

もう、unsafeは使うしかないでしょ!というレベルですが、やっぱり抵抗がある。。

 

処理を呼び出す側のプログラム

 

まとめ

それぞれの処理時間をまとめると以下の通りでした。

 

方法 処理時間(msec)
GetPixel、SetPixel 27439
LockBits、UnlockBitsで配列を介して処理 101
MarshalクラスのReadByte、WriteByte 273.6
unsafeのポインタで参照 25.4
unsafeのポインタの並列処理 11.4

 

結局GetPixel、SetPixelは論外でした。

あとは好みというか、ポリシーというか、意見の分かれるところだと思いますが、ポインタで処理するのは、やはり魅力です。

 

(2019.11.8追記)

.NET Framework4.5.232ビットを優先で評価を行っていたので、この部分を変えると処理速度に差が出るか?確認しました。

 

.NET Framework 4.7.2  32ビットを優先

方法 処理時間(msec)
GetPixel、SetPixel 27293.4
LockBits、UnlockBitsで配列を介して処理 101.8
MarshalクラスのReadByte、WriteByte 290.8
unsafeのポインタで参照 24.4
unsafeのポインタの並列処理 9.8

 

.NET Framework 4.7.2  32ビットを優先なし

方法 処理時間(msec)
GetPixel、SetPixel 21281.2
LockBits、UnlockBitsで配列を介して処理 85.4
MarshalクラスのReadByte、WriteByte 213.6
unsafeのポインタで参照 27.2
unsafeのポインタの並列処理 9.2

 

となりました。

ということで、.NET Frameworkの4.5.2と4.7.2の差はほとんどありませんでしたが、「32ビットを優先」のあるなしでは優先しない(64bitで動作)方が少し速い結果となりました。

 

関連記事

【C#】Bitmap画像データのメモリ構造

 

画像処理のためのC#テクニックへ戻る

C#から使える画像処理ライブラリ

画像処理のプログラムでは、当然ながら画像の表示や、操作するボタンなどが欲しくなるので、GUIのプログラム作成が簡単なC#が割とよく用いられています。

しかし、画像処理そのものをC#でやるには処理速度に不満もあるので、GUIはC#、画像処理部はC言語で処理が書かれた画像処理ライブラリを用いる事となります。

 

そこで、C#から使える画像処理ライブラリにはどんなものがあるのか?紹介したいと思います。

 

個人で使うなら

ほぼ、OpenCvSharpで決まりだと思います。

USBカメラも使えるし、バージョンアップもされているし、作者は日本人なので。。

OpenCVをラップしているので、画像処理時間も普通にプログラムを組むよりは圧倒的に速いかと思います。

 

インストール方法については、GitHubに少し書かれていますが、NuGetからインストールするのが基本のよう。

詳細は「OpenCvSharp インストール」で検索してみて、新しめの記事を参照して頂ければわかると思います。

 

マニュアル

https://shimat.github.io/opencvsharp_docs/index.html

GitHub

https://github.com/shimat/opencvsharp

 

会社で使うなら

「会社で使う」というか、使用するカメラが工業用のカメラを使うなら、以下の2つの画像処理ライブラリが、よく使われているように感じます。

 

●Cognex Vision Pro

●MVTec HALCON

 

両方とも数十万円するので、個人で買えるようなレベルではありませんが、OpenCVと何か違うか?というと、私が思うにはテンプレートマッチングの性能と工業用のカメラに対応している部分だと思います。

 

テンプレートマッチングについては、OpenCVのテンプレートマッチングでは回転やスケール変動には対応していませんが、これらのライブラリは対応しています。

さらには、袋の表面に印刷された画像のように、画像が歪んだ状態でもマッチングしてくれる機能もあります。OpenCVにも特徴点ベースのマッチングはありますが、やっぱりこっちの方が良いかと思います。

 

工業用のカメラを使うと何がいいか?という部分についてですが、カメラの画素数やフレームレートなどもそうですが、一番大きいのは同期撮影が出来る点だと思います。

例えば、ベルトコンベア上を流れる製品を撮影するような場面では、被写体は毎回画像の中央で撮影したくなりますが、カメラのフレーム単位で撮影タイミングを制御できないと、毎回撮影位置がばらついてしまいます。

 

OpenCVからは工業用のカメラを制御することは、ほとんどの場合、出来ないのですが、カメラ専用のSDK(もしくは画像入力ボードのSDK)を用いて画像を撮影し、撮影したデータをOpenCVに渡して画像処理を行う事も可能なので、やっぱり、一番差が大きいのはテンプレートマッチングの性能でしょうかね。

 

ちなみに、マシンビジョン業界でPythonが使われる事はほとんどありません。(聞いた事がありません。)

Pythonが出来ない私。。

 

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

画像の拡大

例えば、下図のように2x2画素の画像を4x4の画像に拡大する場合、アフィン変換を使えばいいんでしょ!と、安易に考えていると、思わぬ落とし穴があったりもします。

大事なポイントとして、

●画像の座標の原点は左上の画素の中心が原点(0.0、0.0)となる。(例外もあります)

●アフィン変換の拡大縮小は原点を基準として拡大縮小される。

 

となります。

これを気にせず、ただ、アフィン変換で画像を2倍に拡大すると、左上の画素の中心を基点に画像が拡大されます。

これで、一見良さそうにも感じるのですが、拡大後の画像において、画素の中心が原点であることから、4x4画素の領域は下図の四角で示した領域であり、画像全体が左上に0.5画素ズレた状態になっていまいます。

 

 

アフィン変換で画像を拡大する時の変換前と変換後の状態は、以下のようになるのが正解です。

 

この変換をアフィン変換で実現するには以下のように行います。

 

変換前の状態

 

①画像全体を右下に(+0.5,+0.5)画素移動
$$\begin{pmatrix} 1 & 0 & 0.5 \\ 0 & 1 & 0.5 \\ 0 & 0 & 1 \end{pmatrix}$$
 

②画像を2倍に拡大

$$\begin{pmatrix} 2 & 0 & 0 \\ 0 & 2 & 0 \\ 0 & 0 & 1 \end{pmatrix}$$
 

③画像全体を左上に(-0.5,-0.5)画素移動

$$\begin{pmatrix} 1 & 0 & -0.5 \\ 0 & 1 & -0.5 \\ 0 & 0 & 1 \end{pmatrix}$$

 

となります。

この一連の変換をアフィン変換行列であらわすと

$$\begin{pmatrix} { x }^{ ‘ } \\ { y }^{ ‘ } \\ 1 \end{pmatrix}=\begin{pmatrix} 1 & 0 & -0.5 \\ 0 & 1 & -0.5 \\ 0 & 0 & 1 \end{pmatrix}\begin{pmatrix} 2 & 0 & 0 \\ 0 & 2 & 0 \\ 0 & 0 & 1 \end{pmatrix}\begin{pmatrix} 1 & 0 & 0.5 \\ 0 & 1 & 0.5 \\ 0 & 0 & 1 \end{pmatrix}\begin{pmatrix} x \\ y \\ 1 \end{pmatrix}\\ \\ $$

$$\begin{pmatrix} { x }^{ ‘ } \\ { y }^{ ‘ } \\ 1 \end{pmatrix}=\begin{pmatrix} 2 & 0 & 0.5 \\ 0 & 2 & 0.5 \\ 0 & 0 & 1 \end{pmatrix}\begin{pmatrix} x \\ y \\ 1 \end{pmatrix}\\ \\ $$

 

となり、単に拡大のアフィン変換行列だけを掛ければOKでは無いことが分かります。

 

ちなみに、C#で2x2画素の画像をPictureBoxのSizeModeプロパティをZoomにして、ImageプロパティにBitmapを設定すると、このようになります。

 

なんとなく、画像が左上にズレているようで、なんか怪しい!!

 

【関連記事】

【C#】画像の座標系

アフィン変換(平行移動、拡大縮小、回転、スキュー行列)

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

画像の回転

 

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

【C#】Bitmapクラスへのポインタ渡し

画像処理のプログラムを作成するときは、GUIはC#、画像処理はC、C++で書かれたライブラリで作成するというのが、私の定番となっているのですが、C言語で処理された画像データをC#へ渡す場合は、画像データのコピーや画像ファイルを介せずとも、画像データのポインタをC#側へ渡す事で画像データを扱うことが可能となります。

 

これを可能にするのには、Bitmapクラスのコンストラクタに

public Bitmap(
    int width,
    int height,
    int stride,
    PixelFormat format,
    IntPtr scan0
)

というのがあるので、scan0の部分にC言語で扱っている画像データのポインタを渡す事で、C#のビットマップオブジェクトとして扱うことが可能となります。

ただし、画像データのメモリ構造はトップダウンで画像1行あたりのサイズが4バイト単位となります。

(参考)

【C#】Bitmap画像データのメモリ構造

 

scan0に渡されたポインタはBitmapクラス内ではメモリを参照するだけ(Bitmapクラス内でメモリを確保していない)なので、BitmapオブジェクトをDisposeするまでscan0で示されたメモリは解放しないように注意が必要です。

逆にBitmapオブジェクトをDisposeしてもメモリは解放されないので、メモリの解放も忘れずに。

 

メモリが参照されているだけかどうか?を評価するのに、下記のようなプログラムを作成してみました。

// 元のBitmap
var bmp1 = new Bitmap(@"C:\Temp\Lenna.bmp");

// ロックしてポインタ参照できるようにする
var bmpData1 = bmp1.LockBits(new Rectangle(0, 0, bmp1.Width, bmp1.Height),
System.Drawing.Imaging.ImageLockMode.ReadWrite, bmp1.PixelFormat);

// ポインタを渡してBitmapクラスを作成
var bmp2 = new Bitmap(
    bmp1.Width,
    bmp1.Height,
    bmpData1.Stride,
    bmp1.PixelFormat,
    bmpData1.Scan0 // bmp1の画像データのポインタ
);

// アンロック
bmp1.UnlockBits(bmpData1);

// ロックしてポインタ参照できるようにする
var bmpData2 = bmp2.LockBits(new Rectangle(0, 0, bmp2.Width, bmp2.Height),
System.Drawing.Imaging.ImageLockMode.ReadWrite, bmp2.PixelFormat);

var data2 = new byte[Math.Abs(bmpData2.Stride) * bmp2.Height];

// bmpData2の輝度値を配列へコピーする
System.Runtime.InteropServices.Marshal.Copy(
    bmpData2.Scan0,
    data2,
    0,
    data2.Length
);

// 画像の輝度値を+50する
for (int i = 0; i < data2.Length; i++)
{
    data2[i] = (byte)Math.Min(data2[i] + 50, 255);
}

// 元のポインタへコピーして、データを戻す
System.Runtime.InteropServices.Marshal.Copy(
    data2,
    0,
    bmpData2.Scan0,
    data2.Length
);

// アンロック
bmp2.UnlockBits(bmpData2);
//
bmp2.Dispose();

// bmp1を保存(※bmp1は直接輝度値の変更はしていない)
bmp1.Save(@"C:\Temp\Lenna1.bmp", System.Drawing.Imaging.ImageFormat.Bmp);
// 解放
bmp1.Dispose();

上記のプログラムではbmp1でBitmapオブジェクトを作成(画像のメモリを確保)し、bmp1の画像データのポインタをbmp2へ渡しています。

この時点でbmp1とbmp2の画像データのメモリは同じをメモリを参照しているはずなので、bmp2の画像の輝度値を変更すると、bmp1の画像も変更されているか?を確認しています。

 

元の画像(bmp1の画像)

 

bmp2を処理した後のbmp1の画像

 

以上のようになり、思惑通りでした!

 

画像処理のためのC#テクニックへ戻る

【C#】Bitmap画像データのメモリ構造

Bitmap画像の輝度値を参照するには、こちらのページ↓

【C#】Bitmap画像の輝度値の参照設定

 

で、Bitmap画像のメモリの値を参照するにはBitmapオブジェクトをLockBitsでロックし、BitmapDataScan0プロパティを参照すれば、良いと書きましたが、このScan0で示されたメモリの構造のお話です。

 

C言語で画像の表示を行った事がある人には、WindowsのDIB(Device Independent Bitmaps)の画像データの構造とほぼ同じで、データの並びがトップダウンになります。

で済むのですが、私自身もC言語で画像の表示をすることは少なくなり、これで通じる人は少なくなった気がします。。

 

簡単に画像データのメモリ構造を説明すると

 

●画像の左上から順にメモリに配置されている(トップダウン)

●画像1行あたりのメモリは4の倍数のバイトになるように調整されている

●カラーの場合、B,G,R,B,G,R,・・・もしくは B,G,R,A,B,G,R,A,・・・の順で並んでいる

 

より具体的に3x3画素の画像を例にとって説明します。

 

上図のように3x3の画像であってもメモリ上は下図のように一列にデータが並んでおり、BitmapDataのSacn0がメモリの先頭アドレスを差しています。

 

上図は8ビットのモノクロ画像の場合ですが、1行あたりのメモリサイズが4の倍数のバイトになるように余計にメモリが追加(上図のグレーの部分)されいます。

 

この1行あたりのメモリのサイズはBitmapDataStrideプロパティで取得できますが、自分で計算すると

 

stride = (width * bitCount + 31) / 32 * 4;

 

という計算で求めることができ、幅が3画素、8ビットの場合は

 

24ビットカラーの場合は、1画素あたり3バイト、メモリを使用し、画像の左上からB,G,R,B,G,R,・・・の順番でメモリに画像の輝度値が格納され

 

のようになっています。

同様に32ビットの場合は、1画素あたり4バイト、メモリを使用し、画像の左上からB,G,R,A,B,G,R,A,・・・の順番でメモリに画像の輝度値が格納され

 

となり、32ビットの場合は1画素あたり4バイトなので、画像の幅の画素数に関係なく余計なメモリが追加されることはありません。

 

画像を表示して斜めに表示される場合は、このStrideあたり(メモリの確保や輝度値の参照方法)がおかしい場合が多いので、確認してみて下さい。

 

参考までに、ビットマップファイル(*.bmp)の画像データ部分は、幅方向については、上記の通りですが、画像の左下から上へ(ボトムアップ)メモリに格納されています。

 

画像処理のためのC#テクニックへ戻る

【C#】Bitmapのカラーパレットの設定

8bitのグレースケールのBitmap(PixelFormatがFormat8bppIndexed)を新規で作成する場合、カラーパレットを設定する必要があるのですが、安易に下記のようなコード

// これは設定できない
var bmp = new Bitmap(256, 64, System.Drawing.Imaging.PixelFormat.Format8bppIndexed);
for (int i = 0; i < 256; i++)
{
    bmp.Palette.Entries[i] = Color.FromArgb(i, i, i);
}

のようにするとちゃんと設定できていそうで、実は設定できていません。

 

これはBitmapのPaletteプロパティで値の取得が呼ばれたときに内部でカラーパレットのメモリが確保され、実際にGDI+で描画に用いているカラーパレットをメモリに格納するようにしているためで、上記のコードではコピーしてきたカラーパレットに設定しているだけで、実際に用いているカラーパレットには設定できていません。

 

そこで正しく修正したのがこちら↓

// これなら設定できる
var bmp = new Bitmap(256, 64, System.Drawing.Imaging.PixelFormat.Format8bppIndexed);
var pal = bmp.Palette;
for (int i = 0; i < 256; i++)
{
    pal.Entries[i] = Color.FromArgb(i, i, i);
}
bmp.Palette = pal;

 

ちなみに、画像の輝度値が0~255に変化する、こんな画像↓

を作るのにカラーパレットの設定を失敗すると、こんな感じ↓になっていました。

 

画像処理では8ビットのグレースケールの画像を用いることは多いかと思いますが、カラーパレットの設定は、だた、グレースケールを設定するだけではつまらないので、少し変わった使い方を紹介します。

 

二値化のプレビューとして

指定した範囲の輝度値(val1~val2)に色を付けるメソッド(SetPaletteGrayScal)を作りました。

/// <summary>
/// 8bitグレースケールのカラーパレットを設定
/// </summary>
/// <param name="bmp">カラーパレットを設定するBitmapオブジェクト</param>
/// <param name="val1">val1~val2の範囲を指定した色(col)にする</param>
/// <param name="val2">val1~val2の範囲を指定した色(col)にする</param>
/// <param name="col">指定色</param>
public void SetPaletteGrayScal(Bitmap bmp, int val1 = -1, int val2 = -1, Color? col = null)
{
    var pal = bmp.Palette;

    // グレースケールのパレットの設定
    for (int i = 0; i < 256; i++) { pal.Entries[i] = Color.FromArgb(i, i, i); } if (col == null) col = Color.Red; // 値が設定されているとき if ( (val1 >= 0) &&
        (val1 < 256) && (val2 >= 0) &&
        (val2 < 256)
    )
    {
        if (val1 <= val2)
        {
            // val1~val2の範囲を指定した色に設定する
            for (int i = val1; i <= val2; i++)
            {
                pal.Entries[i] = (Color)col;
            }
        }
        else
        {
            // 0~val2、val1~255の範囲を指定した色に設定する
            for (int i = 0; i <= val2; i++)
            {
                pal.Entries[i] = (Color)col;
            }
            for (int i = val1; i < 256; i++)
            {
                pal.Entries[i] = (Color)col;
            }
        }
    }

    // カラーパレットの設定
    bmp.Palette = pal;
}

 

使い方は、こんな感じ↓

using (var bmp = new Bitmap("Grayscale.bmp"))
{
    // グレースケールの設定(64~128を赤くする)
    SetPaletteGrayScal(bmp, 64, 128);
    bmp.Save("GrayscaleRed.bmp", System.Drawing.Imaging.ImageFormat.Bmp);

    // 元に戻す場合は
    // グレースケールの設定(通常)
    SetPaletteGrayScal(bmp);
}

 

カラーパレット設定前

カラーパレット設定後

 

他の画像で輝度値127~255を赤く表示している例↓

  

 

この方法だと実際に二値化の処理は行っていないので、結果が表示されるまでの時間が殆どかかりません。

二値化のしきい値をどの程度にするか?プレビューする場合などは便利です。

 

疑似カラー画像

黒~白の画像を青~緑~赤と色を付けて表示している、いわゆる疑似カラーのカラーパレットを設定するメソッドを作成しました。

/// <summary>
/// 疑似カラーのパレットを設定
/// </summary>
/// <param name="bmp">カラーパレットを設定するBitmapオブジェクト</param>
public void SetPalettePseudoColor(Bitmap bmp)
{
    var pal = bmp.Palette;

    int r, g, b;

    // 疑似カラー
    for (int i = 0; i < 52; i++)
    {
        r = 0;
        g = (int)((Math.Cos(Math.PI * (i * 900 / 255.0 - 180) / 180.0) + 1) * 110.0);
        b = 255;
        pal.Entries[i] = Color.FromArgb(r, g, b);
    }
    for (int i = 52; i < 102; i++)
    {
        r = 0;
        g = 220;
        b = (int)((Math.Cos(Math.PI * (i * 900 / 255.0 - 180) / 180.0) + 1) * 127.5);
        pal.Entries[i] = Color.FromArgb(r, g, b);
    }
    for (int i = 102; i < 154; i++)
    {
        r = (int)((Math.Cos(Math.PI * (i * 900 / 255.0 - 180) / 180.0) + 1) * 127.5);
        g = 220;
        b = 0;
        pal.Entries[i] = Color.FromArgb(r, g, b);
    }
    for (int i = 154; i < 204; i++)
    {
        r = 255;
        g = (int)((Math.Cos(Math.PI * (i * 900 / 255.0 - 180) / 180.0) + 1) * 110.0);
        b = 0;
        pal.Entries[i] = Color.FromArgb(r, g, b);
    }
    for (int i = 204; i < 256; i++)
    {
        r = 255;
        g = 0;
        b = (int)((Math.Cos(Math.PI * (i * 900 / 255.0 - 180) / 180.0) + 1) * 127.5);
        pal.Entries[i] = Color.FromArgb(r, g, b);
    }
    // カラーパレットの設定
    bmp.Palette = pal;
}

使い方は、こんな感じ↓

using (var bmp = new Bitmap("Grayscale.bmp"))
{
    // 疑似カラーの設定
    SetPalettePseudoColor(bmp);
    bmp.Save("SetColorPalette_PseudoColor.bmp", System.Drawing.Imaging.ImageFormat.Bmp);
}

カラーパレット設定前

カラーパレット設定後

 

他の画像では

  

 

サーモグラフィ―っぽい画像

サーモグラフィ―でよく目にする画像では温度の高い部分が赤く、低い部分が青く表示されている場合が多いかと思いますが、実際にサーモグラフィーのカメラで撮影した画像データは8ビット(それ以上の場合もあるかも?)の場合は、8ビットの画像をカラーの24ビットに変換して色を付けるまでもなく、カラーパレットを設定することで、それっぽい画像になってくれます。

/// <summary>
/// サーモグラフィーっぽいカラーパレットを設定
/// </summary>
/// <param name="bmp">カラーパレットを設定するBitmapオブジェクト</param>
public void SetPaletteThermo(Bitmap bmp)
{
    var pal = bmp.Palette;

    // カラーパレットの設定

    int r, g, b;

    for (int i = 0; i < 256; i++)
    {
        if (i < 32)
        {
            r = 0;
        }
        else if (i < 164)
        {
            r = (int)((Math.Cos(Math.PI * (i * 360 / 255.0 - 225) / 180.0) + 1) * 127.5);
        }
        else
        {
            r = 255;
        }

        if (i < 96)
        {
            g = 0;
        }
        else if (i < 224)
        {
            g = (int)((Math.Cos(Math.PI * (i * 360 / 255.0 + 45) / 180.0) + 1) * 127.5);
        }
        else
        {
            g = 255;
        }

        if (i < 128)
        {
            b = (int)((Math.Cos(Math.PI * (i * 720 / 255.0 - 180) / 180.0) + 1) * 127.5);
        }
        else if (i < 192)
        {
            b = 0;
        }
        else
        {
            b = (int)((Math.Cos(Math.PI * (i * 720 / 255.0) / 180.0) + 1) * 127.5);
        }

        pal.Entries[i] = Color.FromArgb(r, g, b);
    }
    // カラーパレットの設定
    bmp.Palette = pal;
}

使い方は、こんな感じ↓

using (var bmp = new Bitmap("Grayscale.bmp"))
{
    // グレースケールの設定(64~128を赤くする)
    SetPaletteThermo(bmp);
    bmp.Save("SetColorPalette_Thermo.bmp", System.Drawing.Imaging.ImageFormat.Bmp);
}

カラーパレット設定前

カラーパレット設定後

 

他の画像では

  

 

画像処理のためのC#テクニックへ戻る

【C#】Bitmap画像のビット数の取得

Bitmapクラスのプロパティに Format24bppRgb などを取得できる PixelFormatプロパティ はありますが、1画素あたりのビット数を取得するにはどうすればよいのか??

 

画像処理で使うPixelFormatは Format8bppIndexed(8bit)、Format24bppRgb(24bit)、Format32bppArgb(32bit)ぐらいしかないので、if文とかで拾ってもいいかもしれませんが、Bitmapクラス(Imageクラスでも同じ)にGetPixelFormatSizeメソッドというのがあるので、これを使います。

 

(コード例)

// Bitmapオブジェクトの作成
var bmp = new Bitmap(@"C:\Temp\Lenna.bmp");

// 画像の1画素あたりのビット数の取得(8,24,32など)
var bitCount = Bitmap.GetPixelFormatSize(bmp.PixelFormat);

画像処理のためのC#テクニックへ戻る

【C#】画像(Bitmapクラス)のPictureBoxへの描画

画像(Bitmapオブジェクト)を描画するには、DrawImageメソッドを用いますが、DrawImageメソッドは30個も定義があり、使うと意図しない動きをする定義も含まれています。

DrawImage(Image image, PointF point);
DrawImage(Image image, Rectangle rect);
DrawImage(Image image, PointF[] destPoints);
DrawImage(Image image, Point[] destPoints);
DrawImage(Image image, RectangleF rect);
DrawImage(Image image, Point point);
DrawImage(Image image, float x, float y);
DrawImage(Image image, int x, int y);
DrawImage(Image image, Rectangle destRect, Rectangle srcRect, GraphicsUnit srcUnit);
DrawImage(Image image, RectangleF destRect, RectangleF srcRect, GraphicsUnit srcUnit);
DrawImage(Image image, PointF[] destPoints, RectangleF srcRect, GraphicsUnit srcUnit);
DrawImage(Image image, Point[] destPoints, Rectangle srcRect, GraphicsUnit srcUnit);
DrawImage(Image image, float x, float y, float width, float height);
DrawImage(Image image, Point[] destPoints, Rectangle srcRect, GraphicsUnit srcUnit, ImageAttributes imageAttr);
DrawImage(Image image, int x, int y, int width, int height);
DrawImage(Image image, int x, int y, Rectangle srcRect, GraphicsUnit srcUnit);
DrawImage(Image image, PointF[] destPoints, RectangleF srcRect, GraphicsUnit srcUnit, ImageAttributes imageAttr);
DrawImage(Image image, float x, float y, RectangleF srcRect, GraphicsUnit srcUnit);
DrawImage(Image image, PointF[] destPoints, RectangleF srcRect, GraphicsUnit srcUnit, ImageAttributes imageAttr, DrawImageAbort callback);
DrawImage(Image image, Point[] destPoints, Rectangle srcRect, GraphicsUnit srcUnit, ImageAttributes imageAttr, DrawImageAbort callback);
DrawImage(Image image, Rectangle destRect, float srcX, float srcY, float srcWidth, float srcHeight, GraphicsUnit srcUnit);
DrawImage(Image image, Point[] destPoints, Rectangle srcRect, GraphicsUnit srcUnit, ImageAttributes imageAttr, DrawImageAbort callback, int callbackData);
DrawImage(Image image, Rectangle destRect, int srcX, int srcY, int srcWidth, int srcHeight, GraphicsUnit srcUnit);
DrawImage(Image image, PointF[] destPoints, RectangleF srcRect, GraphicsUnit srcUnit, ImageAttributes imageAttr, DrawImageAbort callback, int callbackData);
DrawImage(Image image, Rectangle destRect, int srcX, int srcY, int srcWidth, int srcHeight, GraphicsUnit srcUnit, ImageAttributes imageAttr);
DrawImage(Image image, Rectangle destRect, float srcX, float srcY, float srcWidth, float srcHeight, GraphicsUnit srcUnit, ImageAttributes imageAttrs);
DrawImage(Image image, Rectangle destRect, float srcX, float srcY, float srcWidth, float srcHeight, GraphicsUnit srcUnit, ImageAttributes imageAttrs, DrawImageAbort callback);
DrawImage(Image image, Rectangle destRect, int srcX, int srcY, int srcWidth, int srcHeight, GraphicsUnit srcUnit, ImageAttributes imageAttr, DrawImageAbort callback);
DrawImage(Image image, Rectangle destRect, float srcX, float srcY, float srcWidth, float srcHeight, GraphicsUnit srcUnit, ImageAttributes imageAttrs, DrawImageAbort callback, IntPtr callbackData);
DrawImage(Image image, Rectangle destRect, int srcX, int srcY, int srcWidth, int srcHeight, GraphicsUnit srcUnit, ImageAttributes imageAttrs, DrawImageAbort callback, IntPtr callbackData);

DrawImageメソッドには、大まかには

●描画先の左上の座標を指定するもの

●描画先の左上の座標、幅、高さを指定するもの

●描画先の左上の座標、描画元の領域を指定するもの

●描画先の左上の座標、幅、高さ、描画元の領域を指定するもの

●上記の座標にint型で指定するもの

●上記の座標にfloat型で指定するもの

●上記にDrawImageAbort を指定するもの

 

の組み合わせとなります。

DrawImageAbortに関しては、使った事がないので、いまいち理解できていないのですが、描画先の左上の座標のみを指定する場合は、画像ファイルのDPI情報(dot per inch, 画像の解像度)に合わせて表示されるので、注意が必要です。(というより使わない方が良いです)

 

例えば、下記のようなコードで72dpiと96dpiのファイルをそれぞれ開くと、

pictureBox1.Image = new Bitmap(pictureBox1.Width, pictureBox1.Height);

using (var bmp = new Bitmap(@"C:\Temp\Lenna.jpg"))
using (var g = Graphics.FromImage(pictureBox1.Image))
{
    g.DrawImage(bmp, 0, 0);
}

(ファイルが72dpiのとき)

(ファイルが96dpiのとき)

 

のように2つの画像とも画素数は同じなのですが、表示される大きさが異なります。

Windowsでは96dpi基準なので、96dpiのファイルだけを表示していると気が付きませんが、macが72dpiのため、たまに表示サイズがおかしくなる場合があるので、意図的にdpiに基づいて表示する場合以外は必ず

  左上の座標、画像の幅、高さ

を指定するメソッドを用いるようにして下さい。

 

(修正したコード例)

pictureBox1.Image = new Bitmap(pictureBox1.Width, pictureBox1.Height);

using (var bmp = new Bitmap(@"C:\Temp\Lenna.jpg"))
using (var g = Graphics.FromImage(pictureBox1.Image))
{
    g.DrawImage(bmp, 0, 0, bmp.Width, bmp.Height);
}

さらに画像を拡大/縮小して表示する場合は、描画先の幅、高さに表示倍率を掛けることで、拡大縮小することが出来ますが、下記のようなコードで拡大表示すると、画像の上と左側が0.5画素分切れて表示されてしまいます。

 

(拡大表示例)

pictureBox1.Image = new Bitmap(pictureBox1.Width, pictureBox1.Height);

using (var bmp = new Bitmap(@"C:\Temp\test.jpg"))  // 6x6画素の画像
using (var g = Graphics.FromImage(pictureBox1.Image))
{
    // 補間モードの設定(各画素が見えるように) 
    g.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.NearestNeighbor; 
    // 50倍で描画  
    g.DrawImage(bmp, 0, 0, bmp.Width * 50, bmp.Height * 50);
}

実行結果

 

このようにならたいためには、こちらのページ↓

【C#】画像の座標系

 

でも書いていますが、PixelOffsetModeを指定するか、元の画像の座標を0.5画素ズラすかのいづれかの方法となります。

ここでは、元の画像の座標を0.5画素ずらす方法の例を示すと、

 

(修正したコード例)

pictureBox1.Image = new Bitmap(pictureBox1.Width, pictureBox1.Height);

using (var bmp = new Bitmap(@"C:\Temp\test.jpg"))  // 6x6画素の画像
using (var g = Graphics.FromImage(pictureBox1.Image))
{
    // 補間モードの設定(各画素が見えるように) 
    g.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.NearestNeighbor; 
    // 描画元を0.5画素ずらして50倍で描画  
  g.DrawImage(
        bmp, 
        new RectangleF(0f, 0f, bmp.Width * 50f, bmp.Height * 50f), 
        new RectangleF(-0.5f, -0.5f, bmp.Width, bmp.Height), 
        GraphicsUnit.Pixel);
}

実行結果

 

さらに面白い使い方として、描画先の座標に画像の左上、右上、左下の座標を指定することで、画像の平行移動、拡大縮小、拡大、せん断までのアフィン変換を実現することもできます。

(コード例)

pictureBox1.Image = new Bitmap(pictureBox1.Width, pictureBox1.Height);

using (var bmp = new Bitmap(@"C:\Temp\Lenna.jpg"))
using (var g = Graphics.FromImage(pictureBox1.Image))
{
    // 補間モードの設定(各画素が見えるように)    
    g.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.NearestNeighbor;
    // 描画元の領域
    var srcRect = new RectangleF(-0.5f, -0.5f, bmp.Width, bmp.Height);

    // 描画先の座標の初期値(左上、右上、左下の順)
    var points = new PointF[]
    {
        new PointF(0, 0),
        new PointF(bmp.Width, 0),
        new PointF(0, bmp.Height),
    };

    // 描画先の座標をアフィン変換で求める
    var mat = new System.Drawing.Drawing2D.Matrix();
    // 画像の中心を基点に回転
    mat.RotateAt(
        30f,
        //new PointF((bmp.Width - 1) / 2f, (bmp.Height - 1) / 2f),
        new PointF(bmp.Width / 2f, bmp.Height / 2f),
        System.Drawing.Drawing2D.MatrixOrder.Append
        );
    // 拡大
    mat.Scale(1.5f, 1.5f, System.Drawing.Drawing2D.MatrixOrder.Append);
    // 3点のアフィン変換
    mat.TransformPoints(points);

    // 描画
    g.DrawImage(
        bmp,
        points,
        srcRect,
        GraphicsUnit.Pixel
        );
}

実行結果

 

まとめ

●描画先の座標指定では、左上の座標のみの指定は用いないこと
必ず左上の座標、幅、高さを指定すること。

●画像を拡大する場合は、0.5画素分、表示がずれる事を考慮すること

●描画先の座標に画像の左上、右上、左下を指定することで、画像のアフィン変換も実現できる。

 

関連ページ

【C#】画像の座標系

【C#】グローバル変換とローカル変換

 

画像処理のためのC#テクニックへ戻る

【C#】Bitmapのファイル保存

Bitmapオブジェクトをファイルに保存するにはSaveメソッドを用います。

以下にBitmapファイル(*.bmp)を開き、Jpegファイル(*.jpg)に保存する例を示します。

var bmp = new Bitmap(@"C:\Temp\Mandrill.bmp");

bmp.Save(
    @"C:\Temp\Mandrill.jpg", 
    System.Drawing.Imaging.ImageFormat.Jpeg
    );

コード的には簡単なのですが、注意したいのが、必ずImageFormatを指定することです。

もし指定しないと、上記の例では、Mandrill.jpgファイルが保存されるのですが、ファイルの中身はpngフォーマットで保存されてしまいます。

(参考)

https://msdn.microsoft.com/ja-jp/library/ktx83wah(v=vs.110).aspx

 

一見するとjpegファイルに保存されているように見えるため、分かりづらいのでご注意を!

 

画像処理のためのC#テクニックへ戻る

【C#】Graphicsオブジェクトの作成

フォームのピクチャボックスなどに画像や線を描画するには、Graphicsオブジェクトに対して描画を行いますが、Graphicsオブジェクトの作成方法はいくつかあり、描画の際の挙動も異なります。

 

Graphicsオブジェクトの作成方法は以下の通り

 

●Imageオブジェクトから作成

●PaintイベントのPaintEventArgsから取得

●CreateGraphicsメソッドによる作成

●FromHwndメソッドによる作成

 

以下にフォームのPictureBoxをDockしたときにPictureBoxに描画するためのGraphicsオブジェクトの作成方法を例にとって説明したいと思います。

 

Imageオブジェクト(Bitmapオブジェクト)から作成

以下の例ではフォームのリサイズイベント内でピクチャボックスと同じサイズのBitmapオブジェクトを作成し、ピクチャボックスのImageプロパティへBitmapオブジェクトを設定し、BitmapオブジェクトからGraphicオブジェクトを作成し、線を描画しています。

private void Form1_Resize(object sender, EventArgs e)
{
    if ((pictureBox1.Width == 0) || (pictureBox1.Height == 0)) return;

    var bmp = pictureBox1.Image as Bitmap;

    if (bmp != null)
    {
        bmp.Dispose();
    }

    bmp = new Bitmap(pictureBox1.Width, pictureBox1.Height,
        System.Drawing.Imaging.PixelFormat.Format24bppRgb);

    pictureBox1.Image = bmp;

    using (var g = Graphics.FromImage(bmp))
    {
        g.Clear(Color.White);

        g.DrawLine(Pens.Red, 0, 0, pictureBox1.Width, pictureBox1.Height);
        pictureBox1.Refresh();
    }
}

実行例

 

ここではピクチャボックスのリサイズイベントではPictureBoxの大きさの取得に失敗する場合があるため、Formのリサイズイベントで処理を行っています。

 

PaintイベントのPaintEventArgsから取得

PictureBoxなどのコントロールのPaintイベントのPaintEventArgsからGraphicsオブジェクトを作成します。

private void pictureBox1_Paint(object sender, PaintEventArgs e)
{
    var g = e.Graphics;

    g.Clear(Color.White);

    g.DrawLine(Pens.Red, 0, 0, pictureBox1.Width, pictureBox1.Height);

}

実は上記コードだけでは問題があり、PictureBoxがリサイズされると、領域が更新された部分のみ線が描画されてしまうため、以下のようになってしまいます。

 

このようにならないようにするには、コントロールのResizeイベントでコントロールのInvalidate()を実行することで、コントロール全体の領域が更新され、正しく描画されるようになります。

private void pictureBox1_Resize(object sender, EventArgs e)
{
    pictureBox1.Invalidate();
}

CreateImageメソッドで作成

var g = pictureBox1.CreateGraphics();

g.Clear(Color.White);

g.DrawLine(Pens.Red, 0, 0, pictureBox1.Width, pictureBox1.Height);

FromHwndメソッドで作成

var g = Graphics.FromHwnd(pictureBox1.Handle);

g.Clear(Color.White);

g.DrawLine(Pens.Red, 0, 0, pictureBox1.Width, pictureBox1.Height);

まとめ

基本的にはImageオブジェクトからFromImageメソッドによりGraphicsオブジェクトを作成する方法がおススメです。

この方法ではGraphicsオブジェクトへ画像や線を描画した内容はBitmapオブジェクトへ反映されるので、使い勝手も良いかと思います。

この方法で描画速度が遅く感じる場合は手動ダブルバッファという方法があるので、こちらのページを参照下さい。

 

【C#】手動ダブルバッファによる高速描画

 

PaintイベントでGraphicsオブジェクトを取得する方法はプログラムが簡単なので、ちょっとした評価用のプログラムなどでは使用することはありますが、描画タイミングなど複雑なプログラムを作成しようとすると、Paintイベント内だけで描画するプログラムを作成するのは難しくなるので、私のブログでPaintイベントでGraphicsオブジェクトを取得しているコードがあったら、「手を抜いているんだな!」と思って下さい。

 

CreateImage、FromHwndメソッドを用いた方法では、なにせ描画が遅い!

線などたくさん描画するようなプログラムでは、線一本一本が描画される様子が見えるぐらいに遅いです。そのため、私は、この方法をほとんど使いません。

 

画像処理のためのC#テクニックへ戻る

【C#】画像の結合

画像の結合は画像の一部領域の切り出しで行った方法とよく似ています。

処理の手順は

  1. 結合後の大きさと同じ大きさのBitmapオブジェクトの作成
  2. BitmapオブジェクトからGraphicsオブジェクトの作成
  3. 作成したGraphicsオブジェクトへ描画先を指定して描画

 

 

画像を縦方向に結合する例をメソッドにまとめました。

/// <summary>
/// 画像を縦に結合する
/// </summary>
/// <param name="src">結合するBitmapの配列</param>
/// <returns>結合されたBitmapオブジェクト</returns>
public Bitmap ImageCombineV(Bitmap[] src)
{
    // 結合後のサイズを計算
    int dstWidth = 0, dstHeight = 0;
    System.Drawing.Imaging.PixelFormat dstPixelFormat = System.Drawing.Imaging.PixelFormat.Format8bppIndexed;

    for (int i = 0; i < src.Length; i++)
    {
        if (dstWidth < src[i].Width) dstWidth = src[i].Width;
        dstHeight += src[i].Height;

        // 最大のビット数を検索
        if (Bitmap.GetPixelFormatSize(dstPixelFormat)
            < Bitmap.GetPixelFormatSize(src[i].PixelFormat))
        {
            dstPixelFormat = src[i].PixelFormat;
        }
    }

    var dst = new Bitmap(dstWidth, dstHeight, dstPixelFormat);
    var dstRect = new Rectangle();

    using (var g = Graphics.FromImage(dst)) {
        for (int i = 0; i < src.Length; i++)
        {
            dstRect.Width = src[i].Width;
            dstRect.Height = src[i].Height;

            // 描画
            g.DrawImage(src[i], dstRect, 0, 0, src[i].Width, src[i].Height, GraphicsUnit.Pixel);

            // 次の描画先
            dstRect.Y = dstRect.Bottom;
        }
    }
    return dst;
}

使用方法は

var images = new Bitmap[] {
    new Bitmap("Lenna.bmp"),
    new Bitmap("Mandrill.bmp"),
    new Bitmap("Parrots.bmp")
};
            
// 画像の結合
var combinedImage = ImageCombineV(images);

// 結合画像の保存
combinedImage.Save("combinedImage.bmp", System.Drawing.Imaging.ImageFormat.Bmp);

// 解放
foreach (var bmp in images)
{
    bmp.Dispose();
}

のような感じで。

画像処理のためのC#テクニックへ戻る

【C#】画像の一部領域の切り出し

画像の一部領域を切り出すには以下の手順で行います。

 

  1. 切り出す領域と同じ大きさのBitmapオブジェクトの作成
  2. BitmapオブジェクトからGraphicsオブジェクトの作成
  3. 作成したGraphicsオブジェクトへ元の部分領域を指定して描画

 

 

この一連の処理をメソッドにまとめました。

/// <summary>
/// Bitmapの一部を切り出したBitmapオブジェクトを返す
/// </summary>
/// <param name="srcRect">元のBitmapクラスオブジェクト</param>
/// <param name="roi">切り出す領域</param>
/// <returns>切り出したBitmapオブジェクト</returns>
public Bitmap ImageRoi(Bitmap src, Rectangle roi)
{
    //////////////////////////////////////////////////////////////////////
    // srcRectとroiの重なった領域を取得(画像をはみ出した領域を切り取る)

    // 画像の領域
    var imgRect = new Rectangle(0, 0, src.Width, src.Height);
    // はみ出した部分を切り取る(重なった領域を取得)
    var roiTrim = Rectangle.Intersect(imgRect, roi);
    // 画像の外の領域を指定した場合
    if (roiTrim.IsEmpty == true) return null;

    //////////////////////////////////////////////////////////////////////
    // 画像の切り出し

    // 切り出す大きさと同じサイズのBitmapオブジェクトを作成
    var dst = new Bitmap(roiTrim.Width, roiTrim.Height, src.PixelFormat);
    // BitmapオブジェクトからGraphicsオブジェクトの作成
    var g = Graphics.FromImage(dst);
    // 描画先
    var dstRect = new Rectangle(0, 0, roiTrim.Width, roiTrim.Height);
    // 描画
    g.DrawImage(src, dstRect, roiTrim, GraphicsUnit.Pixel);
    // 解放
    g.Dispose();

    return dst;
}

 

使用方法は

var bmpRoi = ImageRoi(bmpSrc, new Rectangle(150, 80, 100, 100));

のような感じで。

画像処理のためのC#テクニックへ戻る

【C#】画像ファイルビューア(ソースコード付き)

(2019.09.03追記)本記事の内容を再度、再構成し、下記の記事を作成しました。

【C#】アフィン変換を用いて画像ビューアを作ろう!

 

以下は以前、書いた記事


画像ファイルを開いて、マウスホイールの上下で拡大縮小するソフトは、以前にも作り、旧ブログで公開しているのですが、これはGraphicクラスのTransformプロパティにアフィン変換行列を指定することで、Graphicオブジェクトの描画した画像や線は勝手に拡大縮小表示してくれるという、ワールド変換という機能を使っていました。

 

【C#】マウスホイールで画像の拡大縮小

http://imagingsolution.blog107.fc2.com/blog-entry-287.html

 

しかし、この方法では拡大した画像の上に線を引くと、線も太く表示されるので、少し使いにくい部分がありました。

そこで、今回は描画する元の領域と、描画先の左上、右上、左下の3点の座標を指定し、この3点からなる四角形(平行四辺形も含む)に合わせて画像を表示してくれるメソッド

public void DrawImage(
	Image image,
	PointF[] destPoints,
	RectangleF srcRect,
	GraphicsUnit srcUnit
)

を使って画像ファイルのビューアを作成しました。

 

動作イメージ

 

主な機能

  • ドラッグ&ドロップでファイルを開く
  • マウスホイールの上下による画像の拡大縮小表示
  • マウスのボタンを押しながら画像の移動
  • マウス左ボタンのダブルクリックで画像全体表示
  • マウス右ボタンのダブルクリックで画像等倍表示
  • 画像を開いたあと、矢印キーの左右ボタン(←、→)で同一フォルダ内の画像ファイル切替
  • ウィンドウ左下にマウスポインタ位置の画像上の座標および輝度値の表示
  • ウィンドウ右下に画像の幅x高さxビット数を表示

 

ダウンロード

公開日 バージョン ファイル 備考
2020.07.25 Ver.1.1.0 ImageViewer_V110.zip マウスダブルクリックで発生する例外を修正
2018.02.12 Ver.1.0.0 ImageViewer_V100.zip 初版

 

使用方法

プログラムを実行するだけなら、ファイルを解凍し、exeフォルダ内の ImageViewer.exe をダブルクリックすることで、実行できます。

ソースコード(Visual Studio 2015 C#版)はsourceフォルダに格納しています。

 

画像処理のためのC#テクニックへ戻る

【C#】画像処理プログラム向け基本プログラム

C#で画像処理を行うための最低限のプログラムを作成しました。

 

 

サンプルプログラムのダウンロードはこちら↓

ImagingBaseSample.zip(Visual Studio 2015 C#)

※Windows7では画像表示が消えてしまうようですので、Windows10でお試し下さい。

 

このプログラムは画像の輝度値へのアクセスを容易にするためのImageDataクラス(自作)と、

ImageDataクラスライブラリ

 

と、Graphicsクラスオブジェクトをコントロールのプロパティで拾えるようにしたGraphicsBoxコントロール(自作)

【.NET】GraphicsBoxコントロール

 

のサンプルとなっています。

 

現状では、むしろ面倒くさいだけのサンプルになっていますが、とりあえずヒストグラムの機能は付けました。

右上の空いたスペースにいろいろ肉付けをしていこうと思います。

 

画像処理のためのC#テクニックへ戻る