ディザ法(Dithering)
ディザによる二値化処理について、
画像を二値化するとき、元の画像を再現するのに、閾値の設定により、黒と白のパターンにより、中間色を再現しようとしたものです。
誤差拡散法も、同じですが、パターンの作成方法が違います。
カラーの三色で行えば、カラーも同様に再現することもできます。
基本的には、明と暗の二種類のドットで、ドットのパターンにより、目の錯覚を利用して中間色を再現しようとしたものです。
印刷や、プリンターに利用されています。
ディザ用フィルターの種類
1.ランダムフィルター
乱数を発生させ、乱数を閾値として、その閾値より大きければ、白、小さければ黒とする方法です。
2.マトリックスフィルター
N×Nのフィルターを用意して、二値化する方法です。
マトリックスへの値の与え方として、Bayer型、渦巻型、網点型があります。
Bayert型は、列の合計が同じ値になるようにし、更に隣同士の値が大小関係になるようにしたものです。
渦巻型は、その名のとおり、中心から外側に向かって渦巻き側に並べたものです。
網点型は、四個の組として、大と小に組み分けして、組として隣同士が大小となるようにしたものです。(点の集まりが網のようにみえます。)
3.平均誤差最小法
平均化は、閾値より大きかったら$FF、小さかったら$0とし、元の値から、決定した値 $FF 又は $0 を差し引いて、次の値に加算して、閾値により判別を繰り返して二値化する方法です。
この場合は、二バイトの符号付演算となります。
4.誤差拡散法
平均誤差最小法では、誤差を次の値にだけ与えていましたが、次の値だけでなく、左下、下、右下にも与えて、誤差を分散して滑らかさを再現したものです。
近いところには、大きく分配し、遠いところには小さく分配します。分配の合計値は差分と同じ値になるようにします。
サンプル画像
上サンプルには平均誤差最小法はありません。
実行結果は縦に筋が現れ、見た目には一番悪い結果となります。
二値化に関しては、ヒストグラム均一化、2値化 にもあります。
電光掲示板とか、印刷とかの特殊な場合を除いて、実際にディザが使用されることは無いでしょう。
最近では、電光掲示板も、輝度コントロールを採用しているので、利用しているのは印刷(一般プリンター含む)位かと思います。
サンプルプログラム
サンプルプログラムには、画像の保存はありません。
二値化後の画像を保存する場合は、TBitmatのフォーマットを モノクロ時 pf1bit カラー時は pf4bit
にして、ビットマップとして保存したほうが良いでしょう。
unit DitheringMain; interface uses Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics, Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.ExtCtrls, Vcl.StdCtrls, Vcl.ExtDlgs, system.Types, GDIPAPI, GDIPOBJ; type TForm1 = class(TForm) FileOpen: TButton; OpenPictureDialog1: TOpenPictureDialog; GrayScaleBtn: TButton; DitheringBtn: TButton; ColorDitheringBtn: TButton; RadioGroup1: TRadioGroup; VarianceBtn: TButton; ScrollBox1: TScrollBox; Image2: TImage; ScrollBox2: TScrollBox; Image1: TImage; RadioGroup2: TRadioGroup; MeanBtn: TButton; RadioGroup3: TRadioGroup; procedure FormCreate(Sender: TObject); procedure FileOpenClick(Sender: TObject); procedure FormDestroy(Sender: TObject); procedure GrayScaleBtnClick(Sender: TObject); procedure DitheringBtnClick(Sender: TObject); procedure ColorDitheringBtnClick(Sender: TObject); procedure RadioGroup1Click(Sender: TObject); procedure VarianceBtnClick(Sender: TObject); procedure RadioGroup2Click(Sender: TObject); procedure MeanBtnClick(Sender: TObject); procedure RadioGroup3Click(Sender: TObject); private { Private 宣言 } public { Public 宣言 } end; var Form1: TForm1; type TPrgbarry = array[0..0] of Trgbtriple; // 24ビットカラーレコード 32ビット用はTRGBQuadArray Prgbarray = ^TPrgbarry; // ポインター // 配列のポインターが必要なだけなので、長さは1 [0..0]で問題ありません。 TPSmallrgb = record // 三色計算用 16ビット red : smallint; green : smallint; blue : smallint; end; implementation {$R *.dfm} const OpenFileFilter = '画像ファイル|*.png;*.jpg;*.gif;*.bmp;*.tif;*.ico;*.wdp'+ '|*.png|*.png' + '|*.jpg|*.jpg' + '|*.gif|*.gif' + '|*.bmp|*.bmp' + '|*.tif|*.tif' + '|*.ico|*.ico' + '|*.wdp|*.wdp'; SaveFileFilter = '画像ファイル|*.png;*.jpg;*.gif;*.bmp;*.tif;*.wdp' + '|*.png|*.png' + '|*.jpg|*.jpg' + '|*.gif|*.gif' + '|*.bmp|*.bmp' + '|*.tif|*.tif' + '|*.wdp|*.wdp'; RK = 0.298912 * 4096; // グレースケール輝度係数 赤 GK = 0.586611 * 4096; // グレースケール輝度係数 緑 BK = 0.114478 * 4096; // グレースケール輝度係数 青 // ディザパターン // bayer型 bayerpattern : array[0..3, 0..3] of integer = (( 0, 8, 2, 10), // 20 小 大 小 大 (12, 4, 14, 6), // 36 大 小 大 小 ( 3, 11, 1, 9), // 24 小 大 小 大 (15, 7, 13, 5)); // 40 大 小 大 小 // 30 30 30 30 // 渦巻き型 spiralpattern: array[0..3, 0..3] of integer = (( 6, 7, 8, 9), ( 5, 0, 1, 10), ( 4, 3, 2, 11), (15, 14, 13, 12)); // 編点型 halftone_dot : array[0..3, 0..3] of integer = ((11, 4, 6, 9), // 大 小 小 大 (12, 0, 2, 14), // 大 小 小 大 ( 7, 8, 10, 5), // 小 大 大 小 ( 3, 15, 13, 1)); // 小 大 大 小 var InBitmap : TBitmap; // ビットマップ OutBitmap : TBitmap; GHeight, GWidth : integer; // ソース画像サイズ RGBOut : array of Prgbarray; // imagedata用配列 pattern: array[0..3, 0..3] of integer; // マスクパターン BtnF : byte; // ディザモード // ディザフィルターのセット 閾に変換 procedure pattern_Sub(a : integer); var II, JJ : Integer; begin for II := 0 to 3 do for JJ := 0 to 3 do case a of 0: pattern[II, JJ] := bayerpattern[II, JJ] * 16 + 8; // 閾値セット 1: pattern[II, JJ] := spiralpattern[II, JJ] * 16 + 8; 2: pattern[II, JJ] := halftone_dot[II, JJ] * 16 + 8; end; end; // NTSCグレースケール procedure GrayScale; var II, JJ : Integer; ARGB : Integer; IRGB : Byte; RGBT : Prgbarray; begin setlength(RGBOut , Gheight); // ポインター配列確保 for II := 0 to GHeight - 1 do begin RGBOut[II] := OutBitmap.ScanLine[II]; // ポインタの取得 RGBT := Inbitmap.ScanLine[II]; // ポインタの取得 for JJ := 0 to GWidth - 1 do begin ARGB := round( RK * RGBT[JJ].rgbtRed // グレースケール化 + GK * RGBT[JJ].rgbtGreen + BK * RGBT[JJ].rgbtBlue); IRGB := ARGB shr 12; // 12ビット右シフト RGBOut[II, JJ].rgbtBlue := IRGB; // 三色同じ値セット RGBOut[II, JJ].rgbtGreen := IRGB; // 三色同じ値セット RGBOut[II, JJ].rgbtRed := IRGB; // 三色同じ値セット end; end; end; // グレースケール表示 procedure TForm1.GrayScaleBtnClick(Sender: TObject); begin GrayScale; // グレースケール化 Image2.Picture.Bitmap := OutBitmap; // 出力枠 出力 end; // ディザパターン選択 procedure TForm1.RadioGroup1Click(Sender: TObject); begin case BtnF of 0 : ColorDitheringBtnClick(nil); // カラーディザ 1 : DitheringBtnClick(nil); // モノクロディザ end; end; // 誤差拡散カラー選択 procedure TForm1.RadioGroup2Click(Sender: TObject); begin if RadioGroup2.ItemIndex = -1 then RadioGroup2.ItemIndex := 0; VarianceBtnClick(nil); end; // 誤差平均化カラー procedure TForm1.RadioGroup3Click(Sender: TObject); begin if RadioGroup3.ItemIndex = -1 then RadioGroup3.ItemIndex := 0; MeanBtnClick(nil); end; // 誤差拡散カラー procedure TForm1.VarianceBtnClick(Sender: TObject); const threshold = 128; // 閾値 var HH, WW : Integer; DFR, DMR : Smallint; DFG, DMG : Smallint; DFB, DMB : Smallint; RGBT : Prgbarray; RGBTMP : array of array of TPSmallrgb; // Smallint x 3 配列 begin if RadioGroup2.ItemIndex = -1 then RadioGroup2.ItemIndex := 0; setlength(RGBTMP, Gheight, GWidth); // 処理用配列確保 // Gray 処理 データー用配列に グレー画像コピー if RadioGroup2.ItemIndex = 0 then begin GrayScale; // グレイスケール適用 for HH := 0 to GHeight - 1 do // 作業用配列に画像コピー for WW := 0 to GWidth - 1 do begin RGBTMP[HH, WW].red := RGBOut[HH,WW].rgbtRed; // ByteからSmallintに変換 RGBTMP[HH, WW].green := RGBOut[HH,WW].rgbtGreen; RGBTMP[HH, WW].blue := RGBOut[HH,WW].rgbtBlue; end; end; // color 処理 データー用配列に カラー画像コピー if RadioGroup2.ItemIndex = 1 then begin setlength(RGBOut, Gheight); // ポインター用配列確保 for HH := 0 to GHeight - 1 do begin // 作業用配列に画像コピー RGBT := Inbitmap.ScanLine[HH]; for WW := 0 to GWidth - 1 do begin RGBTMP[HH, WW].red := RGBT[WW].rgbtRed; // ByteからSmallintに変換 RGBTMP[HH, WW].green := RGBT[WW].rgbtGreen; RGBTMP[HH, WW].blue := RGBT[WW].rgbtBlue; end; end; end; // 誤差拡散処理 for HH := 0 to GHeight - 1 do begin for WW := 0 to GWidth - 1 do begin DMR := RGBTMP[HH, WW].red; // 画素値 DMG := RGBTMP[HH, WW].green; DMB := RGBTMP[HH, WW].blue; if DMR >= threshold then RGBTMP[HH, WW].red := $FF // 二値化 else RGBTMP[HH, WW].red := $00; if DMG >= threshold then RGBTMP[HH, WW].green := $FF else RGBTMP[HH, WW].green := $00; if DMB >= threshold then RGBTMP[HH, WW].blue := $FF else RGBTMP[HH, WW].blue := $00; DFR := DMR - RGBTMP[HH, WW].red; // 差分計算 DFG := DMG - RGBTMP[HH, WW].green; DFB := DMB - RGBTMP[HH, WW].blue; if WW < GWidth - 1 then begin // 右位置補正値 + 7/16 RGBTMP[HH, WW + 1].red := RGBTMP[HH, WW + 1].red + (DFR * 7) div 16; RGBTMP[HH, WW + 1].green := RGBTMP[HH, WW + 1].green + (DFG * 7) div 16; RGBTMP[HH, WW + 1].blue := RGBTMP[HH, WW + 1].blue + (DFB * 7) div 16; end; if (WW > 0) and (HH < GHeight - 1) then begin // 左下補正値 + 3/16 RGBTMP[HH + 1, WW - 1].red := RGBTMP[HH + 1, WW - 1].red + (DFR * 3) div 16; RGBTMP[HH + 1, WW - 1].green := RGBTMP[HH + 1, WW - 1].green + (DFG * 3) div 16; RGBTMP[HH + 1, WW - 1].blue := RGBTMP[HH + 1, WW - 1].blue + (DFB * 3) div 16; end; if HH < GHeight - 1 then begin // 下位置補正値 + 5/16 RGBTMP[HH + 1, WW].red := RGBTMP[HH + 1, WW].red + (DFR * 5) div 16; RGBTMP[HH + 1, WW].green := RGBTMP[HH + 1, WW].green + (DFG * 5) div 16; RGBTMP[HH + 1, WW].blue := RGBTMP[HH + 1, WW].blue + (DFB * 5) div 16; end; if (HH < GHeight - 1) and (WW < GWidth - 1) then begin // 右下補正値 + 1/16 RGBTMP[HH + 1, WW + 1].red := RGBTMP[HH + 1, WW + 1].red + DFR div 16; RGBTMP[HH + 1, WW + 1].green := RGBTMP[HH + 1, WW + 1].green + DFG div 16; RGBTMP[HH + 1, WW + 1].blue := RGBTMP[HH + 1, WW + 1].blue + DFB div 16; end; // 計 16/16 end; end; // 処理用配列から出力用ビットマップにコピー for HH := 0 to GHeight - 1 do begin RGBT := OutBitmap.ScanLine[HH]; for WW := 0 to GWidth - 1 do begin RGBT[WW].rgbtRed := RGBTMP[HH, WW].red; // Smallint からByteに変換 RGBT[WW].rgbtGreen := RGBTMP[HH, WW].green; RGBT[WW].rgbtBlue := RGBTMP[HH, WW].blue; end; end; Image2.Picture.Bitmap := OutBitmap; // 出力枠に出力 end; // グレー デザ法の適用 procedure TForm1.DitheringBtnClick(Sender: TObject); var II, JJ : Integer; begin if RadioGroup1.ItemIndex = -1 then RadioGroup1.ItemIndex := 0; GrayScale; pattern_Sub(RadioGroup1.ItemIndex); // ディザパターンの設定 for II := 0 to GHeight - 1 do // ディザの実行 for JJ := 0 to GWidth - 1 do begin if pattern[II mod 4, JJ mod 4] <= RGBOut[II, JJ].rgbtRed then begin // 三色同じ値なので赤を使用 RGBOut[II, JJ].rgbtBlue := 255; RGBOut[II, JJ].rgbtGreen := 255; RGBOut[II, JJ].rgbtRed := 255; end else begin RGBOut[II, JJ].rgbtBlue := 0; RGBOut[II, JJ].rgbtGreen := 0; RGBOut[II, JJ].rgbtRed := 0; end; end; Image2.Picture.Bitmap := OutBitmap; // 出力枠に出力 BtnF := 1; // ディザグレイに設定 end; // 平均値化(誤差拡散) procedure TForm1.MeanBtnClick(Sender: TObject); const threshold = 128; // 閾値 var HH, WW : Integer; DFR, DMR : Smallint; DFG, DMG : Smallint; DFB, DMB : Smallint; RGBT : Prgbarray; RGBTMP : array of array of TPSmallrgb; // Smallint x 3 配列 begin if RadioGroup3.ItemIndex = -1 then RadioGroup3.ItemIndex := 0; setlength(RGBTMP, Gheight, GWidth); // 処理用配列確保 // Gray 処理 データー用配列に グレー画像コピー if RadioGroup3.ItemIndex = 0 then begin GrayScale; // グレイスケール適用 for HH := 0 to GHeight - 1 do // 作業用配列に画像コピー for WW := 0 to GWidth - 1 do begin RGBTMP[HH, WW].red := RGBOut[HH,WW].rgbtRed; // ByteからSmallintに変換 RGBTMP[HH, WW].green := RGBOut[HH,WW].rgbtGreen; RGBTMP[HH, WW].blue := RGBOut[HH,WW].rgbtBlue; end; end; // color 処理 データー用配列に カラー画像コピー if RadioGroup3.ItemIndex = 1 then begin setlength(RGBOut, Gheight); // ポインター用配列確保 for HH := 0 to GHeight - 1 do begin // 作業用配列に画像コピー RGBT := Inbitmap.ScanLine[HH]; for WW := 0 to GWidth - 1 do begin RGBTMP[HH, WW].red := RGBT[WW].rgbtRed; // ByteからSmallintに変換 RGBTMP[HH, WW].green := RGBT[WW].rgbtGreen; RGBTMP[HH, WW].blue := RGBT[WW].rgbtBlue; end; end; end; // 平均化処理 for HH := 0 to GHeight - 1 do begin for WW := 0 to GWidth - 1 do begin DMR := RGBTMP[HH, WW].red; // 画素値 DMG := RGBTMP[HH, WW].green; DMB := RGBTMP[HH, WW].blue; if DMR >= threshold then RGBTMP[HH, WW].red := $FF // 二値化 else RGBTMP[HH, WW].red := $00; if DMG >= threshold then RGBTMP[HH, WW].green := $FF else RGBTMP[HH, WW].green := $00; if DMB >= threshold then RGBTMP[HH, WW].blue := $FF else RGBTMP[HH, WW].blue := $00; DFR := DMR - RGBTMP[HH, WW].red; // 差分計算 DFG := DMG - RGBTMP[HH, WW].green; DFB := DMB - RGBTMP[HH, WW].blue; if WW < GWidth - 1 then begin // 右位置補正値 + 7/16 RGBTMP[HH, WW + 1].red := RGBTMP[HH, WW + 1].red + DFR; RGBTMP[HH, WW + 1].green := RGBTMP[HH, WW + 1].green + DFG; RGBTMP[HH, WW + 1].blue := RGBTMP[HH, WW + 1].blue + DFB; end; end; end; // 処理用配列から出力用ビットマップにコピー for HH := 0 to GHeight - 1 do begin RGBT := OutBitmap.ScanLine[HH]; for WW := 0 to GWidth - 1 do begin RGBT[WW].rgbtRed := RGBTMP[HH, WW].red; // Smallint からByteに変換 RGBT[WW].rgbtGreen := RGBTMP[HH, WW].green; RGBT[WW].rgbtBlue := RGBTMP[HH, WW].blue; end; end; Image2.Picture.Bitmap := OutBitmap; // 出力枠に出力 end; // カラーディザ法の適用と表示 procedure TForm1.ColorDitheringBtnClick(Sender: TObject); var II, JJ : Integer; CompD : Integer; INPRGB, OUTRGB : Prgbarray; begin if RadioGroup1.ItemIndex = -1 then RadioGroup1.ItemIndex := 0; pattern_Sub(RadioGroup1.ItemIndex); // ディザパターンの設定 for II := 0 to GHeight - 1 do begin // ディザの実行 INPRGB := Inbitmap.ScanLine[II]; // 入力ピッマップラインポインタの取得 OUTRGB := OutBitmap.ScanLine[II]; // 出力ピッマップラインポインタの取得 for JJ := 0 to GWidth - 1 do begin CompD := pattern[II mod 4, JJ mod 4]; // ディザパターンデーター if CompD <= INPRGB[JJ].rgbtRed then // 赤の処理 OUTRGB[JJ].rgbtred := 255 else OUTRGB[JJ].rgbtred := 0; if CompD <= INPRGB[JJ].rgbtGreen then // 緑の処理 OUTRGB[JJ].rgbtGreen := 255 else OUTRGB[JJ].rgbtGreen := 0; if CompD <= INPRGB[JJ].rgbtBlue then // 青の処理 OUTRGB[JJ].rgbtBlue := 255 else OUTRGB[JJ].rgbtBlue := 0; end; end; Image2.Picture.Bitmap := OutBitmap; // 出力枠に出力 BtnF := 0; // ディザカラーに設定 end; // ファイルのオープン WIC がファイルの種類が多いので使用 procedure TForm1.FileOpenClick(Sender: TObject); var WIC : TWICImage; InFilename : String; begin OpenPictureDialog1.Filter := OpenFileFilter; // ファイルオープンフィルターの設定 if OpenPictureDialog1.Execute then // ファイルが指定されたら begin WIC := TWICImage.Create; // TWICImageの生成 try InFilename := OpenPictureDialog1.FileName; // ファイル名の取得 WIC.LoadFromFile(InFilename); // 画像の読み込み GHeight := WIC.Height; // 画像高さ取得 GWidth := WIC.Width; // 画像幅 InBitmap.Width := GWidth; // 画像高さ InBitmap.Height := GHeight; InBitmap.Canvas.Draw(0, 0, WIC); // DrawでInBitmapに入力画像設定フォーマット24ビットに変換されます OutBitmap.Width := GWidth; // 出力画像幅 OutBitmap.Height := GHeight; // 出力画像高さ image1.Picture.Bitmap := InBitmap; // 画像表示 finally WIC.Free; // TWICImage 解放 end; end else Exit; GrayScaleBtn.Enabled := True; DitheringBtn.Enabled := True; ColorDitheringBtn.Enabled := True; VarianceBtn.Enabled := True; MeanBtn.Enabled := True; RadioGroup1.ItemIndex := -1; // ディザラジオボタン選択なしにセット RadioGroup2.ItemIndex := -1; // 誤差拡散ラジオボタン選択なしにセット RadioGroup3.ItemIndex := -1; // 平均化ラジオボタン選択なしにセット end; // 初期設定 procedure TForm1.FormCreate(Sender: TObject); begin Top := (Screen.Height - Height) div 2; // 表示位置設定 Left := (Screen.Width - Width) div 2; InBitmap := TBitmap.Create; // 入力画像用 OutBitmap := TBitmap.Create; // 出力画像用 InBitmap.PixelFormat := pf24bit; // 24ビットカラーに設定 OutBitmap.PixelFormat := pf24bit; // 24ビットカラーに設定 GrayScaleBtn.Enabled := False; DitheringBtn.Enabled := False; ColorDitheringBtn.Enabled := False; VarianceBtn.Enabled := False; MeanBtn.Enabled := False; BtnF := 0; // ディザボタンフラグ カラーボタンにセット end; // ビットマップの解法 procedure TForm1.FormDestroy(Sender: TObject); begin InBitmap.Free; OutBitmap.Free; end; end.