接觸數位影像處理最早是在高中,那時候PHOTOSHOP還是4.0,可能是因為先入為主的關係,到現在都沒有學3DMAX之類的興趣,2D到3D的飛躍估計是沒我什麼事了,捨不得那平方到立方的高薪....呵呵。
在上大學的時候,就和同學一起寫過一些圖像處理的程序,那時候編程還很隨意,考慮的只是如何實現,現在看來真正的技術是把握全局的能力,而不是靈光一現的神奇。前些日子接觸了一些國外的影像處理程序,在這裡算是作個總結,估計以後不會再針對性的研究影像處理方面的東西了。
以前的一個同學曾經跟我說過.net沒有指針,現在很多訓練課好像也是這麼講的,其實這是一個謬誤。只是framework不建議使用指針,尤其是在webservise,remoting等跨進程操作中,指針都是不安全的。但用過TC的各位都應該對指針的執行效率又深刻的印象,在批量運算大規模資料的需求下,指針是不二的選擇。因而.net聰明的保留的保留了指針,並將其列入不安全方法集中。合理的使用指針將大幅提高執行效率,我曾做過試驗,對640*480的圖像進行逐點運算,非指針運算要執行數分鐘,而指針運算幾乎是瞬間完成的。所以不要害怕使用指針。
其次就是數學,奉勸大家一定要弄清楚了再寫程序,數學課不是鬧著玩的......想不明白就要躺在床上反复的想,我總覺得數學能預防老年癡呆。
言歸正傳,說說程序結構吧 :
Imaging專案(濾鏡,紋理,影像模式)
Math專案(演算法,邊界,客製化。及常用計算方法)
主程式項目
各舉個例子來說明,我也來一回面向介面編程,
public interface IFilter
{
Bitmap Apply( Bitmap img );
}
舉例來說明,我也來一回面向接口編程,各濾鏡都要實現這個接口,接口定義還包括一個不生成實際圖像,只生成二進制對象的藉口定義,在這裡暫不作考慮。以取反色濾鏡為例
public Bitmap Apply( Bitmap srcImg )
{
// get source image size
int width = srcImg.Width;
int height = srcImg.Height;
PixelFormat fmt = ( srcImg.PixelFormat == PixelFormat.Format8bppIndexed ) ?
PixelFormat.Format8bppIndexed : PixelFormat.Format24bppRgb;
// lock source bitmap data
BitmapData srcData = srcImg.LockBits(
new Rectangle( 0, 0, width, height ),
ImageLockMode.ReadOnly, fmt );
// create new image
Bitmap dstImg = ( fmt == PixelFormat.Format8bppIndexed ) ?
AForge.Imaging.Image.CreateGrayscaleImage( width, height ) :
new Bitmap( width, height, fmt );
// lock destination bitmap data
BitmapData dstData = dstImg.LockBits(
new Rectangle( 0, 0, width, height ),
ImageLockMode.ReadWrite, fmt );
// copy image
Win32.memcpy( dstData.Scan0, srcData.Scan0, srcData.Stride * height );
// process the filter
ProcessFilter( dstData, fmt );
// unlock both images
dstImg.UnlockBits( dstData );
srcImg.UnlockBits( srcData );
return dstImg;
}
是此濾鏡方法的入口,完成了處理前的準備工作,ProcessFilter同時調用每個濾鏡類別中共有的ProcessFilter方法,而這個ProcessFilter就是實現功能的關鍵所在了逐點運算或模版運算。
// Process the filter
private unsafe void ProcessFilter( BitmapData data, PixelFormat fmt )
{
int width = data.Width;
int height = data.Height;
int lineSize = width * ( ( fmt == PixelFormat.Format8bppIndexed ) ? 1 : 3 );
int offset = data.Stride - lineSize;
// do the job
byte * ptr = (byte *) data.Scan0.ToPointer( );
// invert
for ( int y = 0; y < height; y++ )
{
for ( int x = 0; x < lineSize; x++, ptr ++ )
{
// ivert each pixel
*ptr = (byte)( 255 - *ptr );
}
ptr += offset;
}
}
其中Format8bppIndexed是不必太在意的,個人認為設計初期可以不用考慮相容它的問題。
下面來說說紋理,這個以前考慮得還不太多,但發現老外很喜歡玩這個,因為紋理在數學方面發揮的空間更大,我也不知道他們是怎麼想出來的,憑空想可能還真是有難度,可能是他們誰在玩數學建模軟體的時候發現這個玩法的,於是高數老師誰也不服誰,把演算法玩的火火的。反正我覺得是這麼回事。 。 。
public interface ITextureGenerator
{
/**//// <summary>
/// Generate texture
/// </summary>
float[,] Generate( int width, int height );
/**//// <summary>
/// Reset - regenerate internal random numbers
/// </summary>
void Reset( );
}
這是紋理產生器的實作接口,為了確保每次的紋理不同,也要更新隨機數以作為計算參數
private Math.PerlinNoise noise = new Math.PerlinNoise( 1.0 / 32, 0.05, 0.5, 8 );
實作紋理細節還需要靠noise實現,因而需要實現許多種noise。
// Constructors
public WoodTexture( ) : this( 12.0 ) { }
public WoodTexture( double rings )
{
this.rings = rings;
Reset( );
}
建構函數提供了預設值的設置,也就是單位紋理大小的限定。
// Generate texture
public float[,] Generate( int width, int height )
{
float[,] texture = new float[height, width];
int w2 = width / 2;
int h2 = height / 2;
for ( int y = 0; y < height; y++ )
{
for ( int x = 0; x < width; x++ )
{
double xv = (double) ( x - w2 ) / width;
double yv = (double) ( y - h2 ) / height;
texture[y, x] =
Math.Max( 0.0f, Math.Min( 1.0f, (float)
Math.Abs( Math.Sin(
( Math.Sqrt( xv * xv + yv * yv ) + noise.Function2D( x + r, y + r ) )
* Math.PI * 2 * rings
))
));
}
}
return texture;
}
這就是。 。 。我數學不好的下場。都不知道她在說什麼呢,最小值中選出最大值。演算法不難找,關鍵是要看結構如何整合他們。
public void Reset( )
{
r = rand.Next( 5000 );
}別忘了這個隨機數,數字的圖像也需要自然的美。
Math工程中物件導向的觀念不它容易貫徹,看看那個PerlinNoise吧,拋磚引玉。
public PerlinNoise( double initFrequency, double initAmplitude, double persistance, int octaves )
{
this.initFrequency = initFrequency;
this.initAmplitude = initAmplitude;
this.persistance = persistance;
this.octaves = octaves;
}
首先要收集數據,因為影像處理要涉及一維和二維兩種情況,因而像noise這種底層方法要分別對應著兩種情況給出對應的方法。
/**//// <summary>
/// 1-D Perlin noise function
/// </summary>
public double Function( double x )
{
double frequency = initFrequency;
double amplitude = initAmplitude;
double sum = 0;
// octaves
for ( int i = 0; i < octaves; i++ )
{
sum += SmoothedNoise( x * frequency ) * amplitude;
frequency *= 2;
amplitude *= persistance;
}
return sum;
}
/**//// <summary>
/// 2-D Perlin noise function
/// </summary>
public double Function2D( double x, double y )
{
double frequency = initFrequency;
double amplitude = initAmplitude;
double sum = 0;
// octaves
for ( int i = 0; i < octaves; i++ )
{
sum += SmoothedNoise( x * frequency, y * frequency ) * amplitude;
frequency *= 2;
amplitude *= persistance;
}
return sum;
}
一維跟二維的區別是什麼,上中學的時候知道了線的運動生成了面,上大學又知道了循環著變化著的線能代表面,但如果做過了邊緣識別和銳化以後話,又發現以前小看線了,其實它只是比面少一個參數而已。
/**//// <summary>
/// Ordinary noise function
/// </summary>
protected double Noise( int x )
{
int n = ( x << 13 ) ^ x;
return ( 1.0 - ( ( n * ( n * n * 15731 + 789221 ) + 1376312589 ) & 0x7fffffff ) / 1073741824.0 );
}
protected double Noise( int x, int y )
{
int n = x + y * 57;
n = ( n << 13 ) ^ n ;
return ( 1.0 - ( ( n * ( n * n * 15731 + 789221 ) + 1376312589 ) & 0x7fffffff ) / 1073741824.0 );
}又一次證明了前面那段話,個人感覺這個x+y*57有點投影的意思。取得對應的雜訊值。但雜訊不是直接就能拿來用的
/**//// <summary>
/// Smoothed noise
/// </summary>
protected double SmoothedNoise( double x )
{
int xInt = (int) x;
double xFrac = x - xInt;
return CosineInterpolate( Noise( xInt ) , Noise( xInt + 1 ), xFrac );
}
protected double SmoothedNoise( double x, double y )
{
int xInt = (int) x;
int yInt = (int) y;
double xFrac = x - xInt;
double yFrac = y - yInt;
// get four noise values
double x0y0 = Noise( xInt , yInt );
double x1y0 = Noise( xInt + 1, yInt );
double x0y1 = Noise( xInt , yInt + 1 );
double x1y1 = Noise( xInt + 1, yInt + 1) ;
// x interpolation
double v1 = CosineInterpolate( x0y0, x1y0, xFrac );
double v2 = CosineInterpolate( x0y1, x1y1, xFrac );
// y interpolation
return CosineInterpolate( v1, v2, yFrac );
}平滑的雜訊,這個稱呼似乎有點不協調,透過餘弦插值,而不是離散餘弦來運算。什麼是餘弦插值呢? /**//// <summary>
/// Cosine interpolation
/// </summary>
protected double CosineInterpolate( double x1, double x2, double a )
{
double f = ( 1 - Math.Cos( a * Math.PI ) ) * 0.5;
return x1 * ( 1 - f ) + x2 * f;
}就是這個,有些事情,大師知道就夠了,你就照著去做就行了,為什麼?因為你可能一輩子也不明白,自然有人會去弄清楚的,知識還在傳承。就像你不必知道自己的胃酸比例,也可以放心的吃香喝辣一樣,也不必擔心子孫後代會消化不良。有些事不必強求,有點消極了,呵呵。
畫面並不難,只要把握好呼叫關係就可以了,另外像photoshop那樣的懸浮窗體是最佳的選擇我認為, // Invert image
private void invertColorFiltersItem_Click(object sender, System.EventArgs e)
{
ApplyFilter(new Invert());
}
// Apply filter on the image
private void ApplyFilter(IFilter filter)
{
try
{
// set wait cursor
this.Cursor = Cursors.WaitCursor;
// apply filter to the image
Bitmap newImage = filter.Apply(image);
if (host.CreateNewDocumentOnChange)
{
// open new image in new document
host.NewDocument(newImage);
}
else
{
if (host.RememberOnChange)
{
// backup current image
if (backup != null)
backup.Dispose();
backup = image;
}
else
{
// release current image
image.Dispose();
}
image = newImage;
// update
UpdateNewImage();
}
}
catch (ArgumentException)
{
MessageBox.Show("Selected filter can not be applied to the image", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
finally
{
// restore cursor
this.Cursor = Cursors.Default;
}
}呼叫順暢的話,多少程式碼都不會覺得亂,對於初學者來說,要善用region。
這裡還有個DocumentsHost的概念,用它來承載圖像文件,並將圖像和窗體連接起來,很方便 /**//// <summary>
/// IDocumentsHost interface
/// Provides connectione between documents and the main widnow
/// </summary>
public interface IDocumentsHost
{
bool CreateNewDocumentOnChange{get;}
bool RememberOnChange{get;}
bool NewDocument(Bitmap image);
bool NewDocument(ComplexImage image);
Bitmap GetImage(object sender, String text, Size size, PixelFormat format);
}歡迎大家跟我討論