

谷田部総合運動公園、つくば市
この写真、なんか違和感があるなぁー、とずっと気になっていたんですが、
いまやっと気づいた。
時計の時針が変なんですね。
Exif によれば、撮影時刻は午後3:30分なんですが、分針はあっているものの、
時針が4をさしている。3と4の間になくてはならないのに。



以上、谷田部、つくば市
個人営業の商店は、スーパーや郊外の大型店に押されて、いつのまにかなくなって
しまった。子供の頃には、普通に肉屋、魚屋、豆腐屋、八百屋、いろんな店が
あったものだが。谷田部の中心街には、まだすこしだが、これらの商店が
残っていて、なつかしいのと、すこしうれしかった。
前回に引き続き、Turbo Delphi で Rinkaku Application をつくる、の10回目。
今回は、前回までに得られた画像のコントラスト調整をする RinkakuContrast() を
作ってテストする。
RinkakuUtils.pas に以下の関数を追加する。
function RinkakuContrast(var bmp: TBitmap; factor: integer): Boolean; var w, h, x, y, i: integer; a: double; src: TBmpData8; d: array[0..255] of byte; begin result := false; if bmp.PixelFormat <> pf8bit then exit; if (factor > 80) or (factor < 0) then exit; w := bmp.Width; h := bmp.Height; a := (127.5 + factor * 1.8) / (127.5 * 127.5) ; for i := 0 to 255 do if i < 128 then d[i] := AdjustByte( -(i-127.5)*(i-127.5)*a + 127.5 - factor) else d[i] := AdjustByte((i-127.5)*(i-127.5)*a + 127.5 - factor); src := TBmpData8.Create(bmp); for y := 0 to h-1 do for x := 0 to w-1 do src[x,y]^ := d[ src[x,y]^]; src.Free; result := true; end;
テストコードをしめす。
procedure TForm1.Button1Click(Sender: TObject); var bmp, tmp: TBitmap; begin bmp := LoadPng('C:\Home\ImgWork\RaceQueen.png'); if not Assigned(bmp) then exit; bmp.PixelFormat := pf24bit; tmp := BmpClone(bmp); if Rinkaku(tmp, 25) and Contrast8(tmp, 105, 0.05) and AntiBlackOut(bmp, tmp, 55, 30, 0.5) and Kuwahara8Ex(tmp, 1, true) and Median8(tmp, 1) and SetTwoColorGrayScalePalette(tmp, RGB(44, 51, 0), RGB(255, 244, 238)) //and RinkakuContrast(tmp, 50) then begin Canvas.Draw(5, 35, tmp); Clipboard.Assign(tmp); end; bmp.Free; tmp.Free; end;
RinkakuContrast() なし。

これは、意図的に薄く塗ってある。
RinkakuContrast(tmp, 25)

RinkakuContrast(tmp, 50)

RinkakuContrast(tmp, 75)

今回つくった RinkakuContrast() の合理的な論理はない。
Rinkaku 画像は、エッジ抽出による線画に、元画像の濃淡を薄く重ねたものである。
したがって、輝度のヒストグラムは、線画部分の暗い領域がほんの少し、あとは
うすく塗りつぶした部分の比較的明るい部分が圧倒的に多い。コントラストを強調する
ためには、この比較的明るい部分の分布を拡大し、全体に暗いほうへ分布をシフトさせると
よい。これを簡単な数式と、パラメータを一つだけつかって実現した一例が
RinkakuContrast() フィルタである。
このフィルタの効果を PaintShopPro4 のヒストグラムで見てみよう。
RinkakuContrast() なし。

RinkakuContrast(tmp, 75)

だいたいねらったとおりになっているのが分かるだろう。
今回で Rinkaku Application を作るためのフィルタづくりは終わり。
次回はこのシリーズ最後で、Application を Turbo Delphi でつくる。
前回に引き続き、Turbo Delphi で Rinkaku Application をつくる、の9回目。
今回は、Kuwahara8() を拡張した Kuwahara8Ex() とグレースケール専用のメディアン
フィルタ Median8() をつくって、引き続きノイズの低減を目指す。さらに、
グレースケールのパレットの両端を任意の色に設定する SetTwoColorGrayScalePalette()
をつくる。
ここで説明したように、普通の Kuwahara() はカレントピクセルを含む斜め上下の
4区画について、最小の分散区画をもとめ、その区画の色の平均値をカレントの
色とする。この論理を拡張して、斜め上下と、直接の上下区画を4つ加えて、
カレントピクセルを囲む全部で8つの区画について同様にする Kuwahara8Ex() を
試してみよう。
以下の関数を RinkakuUtils.pas に追加する。
function Kuwahara8Ex(var bmp: TBitmap; nBlock: integer; bDetail: Boolean = true): Boolean; var w, h, x, y, i, ix, iy, block, sblock, numBlock: integer; indx, min, d: integer; t: double; tmp: TBitmap; src, dst: TBmpData8; sig: array[0..7] of integer; sum: array[0..7] of integer; Xini: array[0..7] of integer; Yini: array[0..7] of integer; begin result := false; if bmp.PixelFormat <> pf8bit then exit; if (nBlock > 8) or (nBlock < 1) then exit; w := bmp.Width; h := bmp.Height; block := nBlock * 2 + 1; sblock := block - 1; numBlock := block * block; tmp := TBitmap.Create; tmp.PixelFormat := pf24bit; tmp.Width := w + sblock * 2; tmp.Height := h + sblock * 2; tmp.Canvas.Draw(sblock, sblock, bmp); GrayScale(tmp); src := TBmpData8.Create(tmp); dst := TBmpData8.Create(bmp); for y := 0 to h-1 do for x := 0 to w-1 do begin Xini[0] := x - sblock; Yini[0] := y - sblock; // upper-left Xini[1] := x; Yini[1] := y - sblock; // upper-right Xini[2] := x; Yini[2] := y; // lower-right Xini[3] := x - sblock; Yini[3] := y; // lower-left; Xini[4] := x - nBlock; Yini[4] := y - sblock; // upper Xini[5] := x; Yini[5] := y - nBlock; // right Xini[6] := x - nBlock; Yini[6] := y; // lower Xini[7] := x - sblock; Yini[7] := y - nBlock; // left for i := 0 to 7 do begin sum[i] := 0; for ix := Xini[i] to Xini[i] + sblock do for iy := Yini[i] to Yini[i] + sblock do sum[i] := sum[i] + src[ix+sblock, iy+sblock]^; sum[i] := sum[i] div numBlock; sig[i] := 0; for ix := Xini[i] to Xini[i] + sblock do for iy := Yini[i] to Yini[i] + sblock do begin d := src[ix+sblock, iy+sblock]^; sig[i] := sig[i] + (sum[i] - d) * (sum[i] - d); end; sig[i] := sig[i] div numBlock; end; min := 90000; indx := 0; for i := 0 to 7 do if (sig[i] < min) then begin min := sig[i]; indx:= i; end; if (bDetail) then begin t := Max(0.5, 1.0 - Sqrt(sig[indx]) / 60); dst[x, y]^ := AdjustByte(t * sum[indx] + (1 - t) * dst[x, y]^); end else dst[x, y]^ := AdjustByte(sum[indx]); end; dst.Free; src.Free; tmp.Free; result := true; end;
テストコードは前回と同じで Kuwahara8() を Kuwahara8Ex() に変えただけである。
uses VCLImageUtils, RinkakuUtils, Clipbrd; procedure TForm1.Button1Click(Sender: TObject); var bmp, tmp: TBitmap; begin bmp := LoadPng('C:\Home\ImgWork\RaceQueen.png'); if not Assigned(bmp) then exit; bmp.PixelFormat := pf24bit; tmp := BmpClone(bmp); if Rinkaku(tmp, 15) and Contrast8(tmp, 100, 0.04) and AntiBlackOut(bmp, tmp, 60, 30, 0.8) and Kuwahara8Ex(tmp, 1, true) then begin Canvas.Draw(5, 35, tmp); Clipboard.Assign(tmp); end; bmp.Free; tmp.Free; end;
結果をしめす。

これは、Kuwahara8() よりわずかに良いようだ。
次に、ノイズ除去フィルタの定番である Median8() を試してみる。すでに、Delphi で
pf24bit のカラー画像用には、ここで作った。今回は、Rinkaku 画像用にグレースケール
専用の Median8() をつくる。
RinkakuUtils.pas に以下の関数を追加する。
function ByteSort(Item1, Item2: Pointer): Integer; begin result := byte(Item1)-byte(Item2); end; function Median8(var bmp: TBitmap; area: integer = 1):Boolean; var tmp:TBitmap; w, h, ix, iy, x, y, xx, yy: integer; src, dst: TBmpData8; ll: TList; md, num, indx: integer; begin result := false; if bmp.PixelFormat <> pf8bit then exit; if (area<1) or (area>4) then exit; w := bmp.Width; h := bmp.Height; num := (2*area+1)*(2*area+1); md := Round(num/2); ll := TList.Create; ll.Capacity := num; ll.Count := num; tmp := BmpClone(bmp); src := TBmpData8.Create(tmp); dst := TBmpData8.Create(bmp); for iy := 0 to h-1 do for ix := 0 to w-1 do begin indx := 0; for y := iy-area to iy+area do for x := ix-area to ix+area do begin if (y<0) or (y>h-1) then yy := iy else yy := y; if (x<0) or (x>w-1) then xx := ix else xx := x; ll[indx] := pointer(src[xx, yy]^); inc(indx); end; ll.Sort(ByteSort); dst[ix, iy]^ := byte(ll[md]); end; dst.Free; src.Free; ll.Free; tmp.Free; result := true; end;
前回のテストコードの Kuwahara8Ex() を Median8() に変えた結果を示す。

Median8() 単独でもかなりのノイズ低減効果がある。
Kuwahara8Ex() と Median8() をこの順序で適用した結果を以下にしめす。

もうほとんど完璧かな?
元画像が良質な場合は、Kuwahara8Ex() も Median8() も適用しなくてもよい。
画像の効果として、イラスト風、絵画風を求める場合は、ノイズの有無に関係なく
Kuwahara8Ex() を適用してもよいだろう。
最後に、任意の二色をパレットの両端に設定する関数をつくる。
RinkakuUtils.pas に以下を追加する。
function SetTwoColorGrayScalePalette(var bmp: TBitmap; Dark, Bright: TColor):Boolean; var i: integer; ct: array[0..255] of TRGBQuad; dr, dg, db, br, bg, bb: Byte; begin result := false; if bmp.PixelFormat <> pf8bit then exit; Dark := ColorToRGB(Dark); Bright := ColorToRGB(Bright); dr := GetRValue(Dark); dg := GetGValue(Dark); db := GetBValue(Dark); br := GetRValue(Bright); bg := GetGValue(Bright); bb := GetBValue(Bright); for i := 0 to 255 do begin ct[i].rgbRed := Round(dr+(br-dr)*i/255); ct[i].rgbGreen := Round(dg+(bg-dg)*i/255); ct[i].rgbBlue := Round(db+(bb-db)*i/255); ct[i].rgbReserved := 0; end; SetDIBColorTable(bmp.Canvas.Handle,0,256,ct); DeleteObject(bmp.ReleasePalette); result := true; end;
テストコードを以下にしめす。
uses VCLImageUtils, RinkakuUtils, Clipbrd; procedure TForm1.Button1Click(Sender: TObject); var bmp, tmp: TBitmap; begin bmp := LoadPng('C:\Home\ImgWork\RaceQueen.png'); if not Assigned(bmp) then exit; bmp.PixelFormat := pf24bit; tmp := BmpClone(bmp); if Rinkaku(tmp, 15) and Contrast8(tmp, 100, 0.04) and AntiBlackOut(bmp, tmp, 60, 30, 0.8) and Kuwahara8Ex(tmp, 1, true) and Median8(tmp, 1) and SetTwoColorGrayScalePalette(tmp, RGB(50, 20, 0), RGB(255, 230, 240)) then begin Canvas.Draw(5, 35, tmp); Clipboard.Assign(tmp); end; bmp.Free; tmp.Free; end;
この結果は、

となる。
今回はこれまで。
次回は、いよいよフィルタづくりの最後となる Rinkaku 専用のコントラスト調整のための
フィルタをつくる。
前回に引き続き、Turbo Delphi で Rinkaku Application をつくる、の8回目。
今回は、前回までで取りきれないかも知れないノイズを、エッジを保つ平均化である
Kuwahara() フィルタでつくって処理することを試す。
Kuwahara() 自体の論理は、C# ですでにここで説明したので繰り返すことはしない。
今回は、これをグレースケールに変換したここと同等なものを Turbo Delphi で
つくる。ただし、C# のときは手抜きしていた統計処理を今回は厳密に標本分散を
計算して、それが最小な区画から平均をとることにする。
さっそく実装しよう。RinkakuUtils.pas に以下の関数を追加する。
function Kuwahara8(var bmp: TBitmap; nBlock: integer; bDetail: Boolean = true): Boolean; var w, h, x, y, i, ix, iy, block, sblock, numBlock: integer; indx, min, d: integer; t: double; tmp: TBitmap; src, dst: TBmpData8; sum: array[0..3] of integer; sig: array[0..3] of integer; Xini: array[0..3] of integer; Yini: array[0..3] of integer; begin result := false; if bmp.PixelFormat <> pf8bit then exit; if (nBlock > 8) or (nBlock < 1) then exit; w := bmp.Width; h := bmp.Height; block := nBlock * 2 + 1; sblock := block - 1; numBlock := block * block; tmp := TBitmap.Create; tmp.PixelFormat := pf24bit; tmp.Width := w + sblock * 2; tmp.Height := h + sblock * 2; tmp.Canvas.Draw(sblock, sblock, bmp); GrayScale(tmp); src := TBmpData8.Create(tmp); dst := TBmpData8.Create(bmp); for y := 0 to h-1 do for x := 0 to w-1 do begin Xini[0] := x - sblock; Yini[0] := y - sblock; // upper-left Xini[1] := x; Yini[1] := y - sblock; // upper-right Xini[2] := x; Yini[2] := y; // lower-right Xini[3] := x - sblock; Yini[3] := y; // lower-left; for i := 0 to 3 do begin sum[i] := 0; for ix := Xini[i] to Xini[i] + sblock do for iy := Yini[i] to Yini[i] + sblock do sum[i] := sum[i] + src[ix+sblock, iy+sblock]^; sum[i] := sum[i] div numBlock; sig[i] := 0; for ix := Xini[i] to Xini[i] + sblock do for iy := Yini[i] to Yini[i] + sblock do begin d := src[ix+sblock, iy+sblock]^; sig[i] := sig[i] + (sum[i] - d) * (sum[i] - d); end; sig[i] := sig[i] div numBlock; end; min := 90000; indx := 0; for i := 0 to 3 do if (sig[i] < min) then begin min := sig[i]; indx:= i; end; if (bDetail) then begin t := Max(0.5, 1.0 - Sqrt(sig[indx]) / 60); dst[x, y]^ := AdjustByte(t * sum[indx] + (1 - t) * dst[x, y]^); end else dst[x, y]^ := AdjustByte(sum[indx]); end; dst.Free; src.Free; tmp.Free; result := true; end;
この実装の仕方は C# のときとは違う。カレントピクセルを含む小区画の
サイズは nBlock で設定するが、今回は nBlock*2 - 1 であり、nBlock = 1 は
C# のときの2、nBlock = 2 は C# のときの4に相当する。 通常は、nBlock = 1 で
十分である。
このテストコードを以下に示す。
uses VCLImageUtils, RinkakuUtils, Clipbrd; procedure TForm1.Button1Click(Sender: TObject); var bmp, tmp: TBitmap; begin bmp := LoadPng('C:\Home\ImgWork\RaceQueen.png'); if not Assigned(bmp) then exit; bmp.PixelFormat := pf24bit; tmp := BmpClone(bmp); if Rinkaku(tmp, 15) and Contrast8(tmp, 100, 0.04) and AntiBlackOut(bmp, tmp, 60, 30, 0.8) and Kuwahara8(tmp, 1, true) then begin Canvas.Draw(5, 35, tmp); Clipboard.Assign(tmp); end; bmp.Free; tmp.Free; end;
結果は

となる。Kuwahara8() を適用しない

と比べると、エッジの周辺、顔のぶつぶつなどが効果的に軽減されていることが分かるだろう。
なお、上の二つは少し同じだけコントラストを強めている。
bDetail = false のときには

となる。最初の画像と比べると、平均化が大きく、ノイズがより低減されているが
歯茎や毛先などのディテールが失われていて、より絵画的になっている。
今回はここまで。
次回は Kuwahara8() を拡張して、より効果的なフィルタを作って試す。