今回は画像の中にあるノイズを除去する方法についてご紹介したいと思います。
画像処理を行っていると、どうしても細かいノイズが入ってしまい、ノイズのせいで判定などに影響を及ぼしてしまうことが多々あります。
・輪郭抽出の際に検出したい箇所以外にも細かいノイズを拾ってしまう
・コーナー検出させる際に、ノイズにを拾ってしまう
などといった際に、問題を解決してくれるのが、ノイズ除去です。
「ココナラ」でC#、OpenCvSharpを使った画像処理アプリの作成を行っています。
もし、作成依頼やお困りごとがあればお気軽にご相談ください!

ノイズ除去
今回は2種類の方法で2値化画像のノイズ除去を試してみます。
収縮(Erode)・膨張処理(Dilate)
最初に収縮を行い、白い部分を収縮させます。それにより、細かいノイズが消されます。
その後、膨張処理を行い、白い部分を元に戻す処理を行います。この際に収縮で消えた部分に関しては戻らないため、細かいノイズのみ消えることになります。
平準化処理
平準化処理は画像をぼかし、滑らかにする処理になります。周りの値(カーネルサイズ)により、中心の画素値を平均/中央値に変更することにより、ノイズが除去されます。
OpenCvには4つの平準化処理があります。
・平均(Blur)
・ガウシアン(GaussianBlur)
・中央値(MedianBlur)
・バイラテラルフィルタ(BilateralFilter)
今回はごま塩ノイズの除去に使われる 中央値(MedianBlur) を使用してテストを行います。
ソースコードと解説
処理内容
カメラでキャプチャした画像を
・2値化して面積を算出
・2値化した画像を収縮/膨張処理をし、面積を算出
・2値化した画像を平準化処理をし、面積を算出

テキストボックスに書かれている数値が、検出した輪郭の面積となります。
2値化した画像では0が並んでいますが、目に見えないサイズのノイズとなります。
収縮/膨張処理と平準化処理した画像では0が消えており、ノイズが除去されているのが分かると思います。
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.Threading.Tasks;
using System.Windows.Forms;
using OpenCvSharp;
using OpenCvSharp.Extensions;
using System.IO;
namespace ImageProcessing
{
public partial class Form1 : Form
{
private Mat _flame;
private Mat _thresholdMat;
private int _thresholdValue;
public Form1()
{
InitializeComponent();
//PictureBoxのサイズに合わせて表示
pictureBox1.SizeMode = PictureBoxSizeMode.StretchImage;
pictureBox2.SizeMode = PictureBoxSizeMode.StretchImage;
pictureBox3.SizeMode = PictureBoxSizeMode.StretchImage;
pictureBox4.SizeMode = PictureBoxSizeMode.StretchImage;
textBox1.Text = "120";
_thresholdValue = int.Parse(textBox1.Text);
}
private void button1_Click_1(object sender, EventArgs e)
{
{
//VideoCapture作成
using (var capture = new VideoCapture())
{
//カメラの起動
capture.Open(0);
if (!capture.IsOpened())
{
throw new Exception("capture initialization failed");
}
//画像取得用のMatを作成
_flame = new Mat();
while (true)
{
try
{
capture.Read(_flame);
if (_flame.Empty())
{
break;
}
if (_flame.Size().Width > 0)
{
//PictureBoxに表示 MatをBitMapに変換
pictureBox1.Image = BitmapConverter.ToBitmap(_flame);
}
int key = Cv2.WaitKey();
if (this.IsDisposed)
{
break;
}
}
catch (Exception)
{
break;
}
}
}
}
}
private void button2_Click_1(object sender, EventArgs e)
{
textBox2.Text = null;
var capture = _flame;
var grayMat = capture.CvtColor(ColorConversionCodes.BGR2GRAY);
_thresholdMat = grayMat.Threshold(_thresholdValue, 255, ThresholdTypes.BinaryInv);
//ジャグ配列
OpenCvSharp.Point[][] contours;
OpenCvSharp.HierarchyIndex[] hierarchyIndexes;
_thresholdMat.FindContours(out contours, out hierarchyIndexes, RetrievalModes.External, ContourApproximationModes.ApproxSimple);
pictureBox2.Image = BitmapConverter.ToBitmap(_thresholdMat);
string areas = null;
var count = 1;
foreach (var contour in contours)
{
//面積を算出
var area = Cv2.ContourArea(contour);
if (count < contours.Count())
{
areas = areas + area + " ";
}
else
{
areas = areas + area;
}
count++;
}
textBox2.Text = areas;
}
/// <summary>
/// 収縮・膨張処理
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void button3_Click(object sender, EventArgs e)
{
textBox3.Text = null;
var noise = new Mat(new OpenCvSharp.Size(3, 3), MatType.CV_8UC1);
var erodeMat = new Mat();
var dilateMat = new Mat();
//収縮処理
Cv2.Erode(_thresholdMat, erodeMat, noise);
//膨張処理
Cv2.Dilate(erodeMat, dilateMat, noise);
//ジャグ配列
OpenCvSharp.Point[][] contours;
OpenCvSharp.HierarchyIndex[] hierarchyIndexes;
dilateMat.FindContours(out contours, out hierarchyIndexes, RetrievalModes.External, ContourApproximationModes.ApproxSimple);
pictureBox3.Image = BitmapConverter.ToBitmap(dilateMat);
string areas = null;
var count = 1;
foreach (var contour in contours)
{
//面積を算出
var area = Cv2.ContourArea(contour);
if (count < contours.Count())
{
areas = areas + area + " ";
}
else
{
areas = areas + area;
}
count++;
}
textBox3.Text = areas;
}
/// <summary>
/// 平準化処理
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void button4_Click(object sender, EventArgs e)
{
textBox4.Text = null;
var mediaBlur = new Mat();
//平準化(中央値フィルタ)
Cv2.MedianBlur(_thresholdMat, mediaBlur, 3);
//ジャグ配列
OpenCvSharp.Point[][] contours;
OpenCvSharp.HierarchyIndex[] hierarchyIndexes;
mediaBlur.FindContours(out contours, out hierarchyIndexes, RetrievalModes.External, ContourApproximationModes.ApproxSimple);
pictureBox4.Image = BitmapConverter.ToBitmap(mediaBlur);
string areas = null;
var count = 1;
foreach (var contour in contours)
{
//面積を算出
var area = Cv2.ContourArea(contour);
if (count < contours.Count())
{
areas = areas + area + " ";
}
else
{
areas = areas + area;
}
count++;
}
textBox4.Text = areas;
}
private void trackBar1_ValueChanged(object sender, EventArgs e)
{
textBox1.Text = trackBar1.Value.ToString();
_thresholdValue = trackBar1.Value;
}
}
}
膨張/収縮処理
var noise = new Mat(new OpenCvSharp.Size(3, 3), MatType.CV_8UC1);
var erodeMat = new Mat();
var dilateMat = new Mat();
//収縮処理
Cv2.Erode(_thresholdMat, erodeMat, noise);
//膨張処理
Cv2.Dilate(erodeMat, dilateMat, noise);
noise変数にカーネルサイズを指定します。今回は3×3とします。
収縮:Cv2.Erode(入力画像,出力画像,カーネルサイズ)
膨張:Cv2.Dilate(入力画像,出力画像,カーネルサイズ)
平準化処理
var mediaBlur = new Mat();
//平準化(中央値フィルタ)
Cv2.MedianBlur(_thresholdMat, mediaBlur, 3);
Cv2.MedianBlur( 入力画像,出力画像,カーネルサイズ )
まとめ
今回は膨張/収縮と中央値にてノイズ除去を試してみました。
結果としてはごま塩ノイズの除去に適した、中央値の方がより元画像と近い面積になりました。
ノイズにより適した処理が違うので、それぞれに合ったフィルターや処理を使うことにより、よい検出結果をだすことができると思います。
今回使っていないフィルターも是非試してみてください!
最後までご覧いただきありがとうございました!
C#で画像処理を学ぶためのおススメの書籍
C#でOpenCvを扱う方法を詳しく解説してくれています。
C#で画像処理を解説してくれている本がほとんどない中、こちらの書籍はいろいろなメソッドの使い方等を事例を交えて解説してくれているため、非常に参考になります。
私はこちらの書籍を参考に画像処理を実装しました。
是非参考にしてみてください。
C#の基本が学習したいという方におススメのスクール:侍エンジニア塾のエキスパートコース