2024/12/26 ファイル保存追加しました。
影消しプログラム
影、あるいは、暗い部分を明るくするプログラムです。
画像の明度を微分すると、明度の変化のない部分は、値が小さくなり、変化の大きい部分は、値が大きくなるのを利用します。
影や、暗い部分は、明度の変化が小さいので、小さい値となります。
微分した値を、明度の最大値 255 から差し引くと、明度の変化のない場所の明度値が大きくなります。
その明度値で、彩度、色相で色を再現すれば、影や、暗い部分のない明るい画像得ることができます。
しかし、そのままでは、陰影のない明るい画像が得られるだけなので、元の画像と、影を消した画像を適当な比率で、合成することで、綺麗な画像を作成します。
明度は、RGB から Hsu 色相 Saturation 彩度 Value 明度 に変換した明度値です。
R(赤)、G(緑)、B(青)の値のうち、一番大きい値が、明度の値となります。
微分に輝度値を使用しても良いと思いますが、明度を使用したほうが綺麗に仕上がるように思います。
どちらを使用するかは、好みでしょう。
上図は、元画像の、明度を微分した値を画像として表示したものです。
微分フィルターによって、それぞれの特色がありますので、どのフィルターを使用するかを決めます。
微分値に係数(Gain)を乗じて、微分値の調整をします、画像の微調整が可能です。
Gainの値はゼロ以上の値をしていします。
微分値を255から差し引いて、反転します。
微分値が255より大きいと、差分がゼロ以下になります、明度の値にゼロ以下はないので、ゼロとします。
この画像を明度として、彩度、色相により色を再現します。
元画像と適当な比率で合成します。
NextValue := 255 - Grad; //
255から微分値を引きます。
if NextValue < 0
then NextValue := 0; // ゼロ以下になったらゼロにします。
NextValue := Round((1 - ratio) * NextValue + ratio *
Value); // 指定された比率で合算します。
if NextValue > 255 then NextValue := 255; //
255を超えたら255にします。
ガンマ値で補正するよりも、暗い部分、影の部分を綺麗に、明るくできていると思います。
ここで取り上げた画像サンプルでは、綺麗に再現していますが、暗い部分のRGBの割合は、黒に近いので分からことがあり、明るくすると、意外な色になる事があります。
最初に、コントラストの調整をしてから、影消しを行うと、影の部分が明るい綺麗な画像がえられます。
処理の手順1
1. RGB HSV 変換
2. 明度の微分(Gainで値調整)
3. 微分値の反転(255との差分計算)
4. HSV RGB変換
5. 元画像と比率合成
処理の手順2
1. RGBの最大値配列作成(明度配列)
2.
明度の微分(Gainで値調整)
3. 微分値の反転(255との差分計算)
4. RGBの比率関係から、新RGB値計算
5. 元画像と比率合成
2つの手順のプログラムを作成してみました。
手順1
一般的は方法です。
手順2
RGBの値をHSVに変換する必要はなく、RGBの値の最大値が明度なので、最大値を取り出して、明度の配列を作成、微分、反転を行い、RGBの割合から、新しいRGB値を計算すれば、RGB
HSV 変換 HSV RGB 変換は不要です。
計算の結果は全く同じになります。
微分フィルターについては、輪郭抽出、細線化を参照してください。
微分を行う前に、ガウシアンフィルターを使用して、少しボカして、微分のかかり具合を調整しても良いかとおもいます。
ガウシアンフィルターに関しては、アンシャープマスキング その1を参照して下さい。
サンプルプログラム
// shadow_erase unit UtamaroMain; 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, system.Math, system.UITypes; type TForm1 = class(TForm) FileOpen: TButton; OpenPictureDialog1: TOpenPictureDialog; ScrollBox1: TScrollBox; Image2: TImage; ScrollBox2: TScrollBox; Image1: TImage; UtamaroBtn: TButton; LabeledEdit1: TLabeledEdit; RadioGroup1: TRadioGroup; GrayBtn: TButton; GradientBtn: TButton; CheckBox1: TCheckBox; RadioGroup2: TRadioGroup; LabeledEdit2: TLabeledEdit; SavePictureDialog1: TSavePictureDialog; procedure FormCreate(Sender: TObject); procedure FileOpenClick(Sender: TObject); procedure FormDestroy(Sender: TObject); procedure UtamaroBtnClick(Sender: TObject); procedure GrayBtnClick(Sender: TObject); procedure GradientBtnClick(Sender: TObject); procedure FileeSaveBtnClick(Sender: TObject); private { Private 宣言 } public { Public 宣言 } end; var Form1: TForm1; implementation {$R *.dfm} Type TDIarray = array of array of Double; 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'; // ガウシアンマスク Gaussian3X3 : array[0..8] of integer = (1, 2, 1, 2, 4, 2, 1, 2, 1); // 4 8 4 計 16 Total3 = 16; Gaussian5X5 : array[0..24] of integer = ( 1, 4, 6, 4, 1, 4, 16, 24, 16, 4, 6, 24, 36, 24, 6, 4, 16, 24, 16, 4, 1, 4, 6, 4, 1); // 16 64 96 64 16 計 256 total5 = 256; // NTSCグレースケール係数 kr = 0.299; // グレースケール輝度係数 赤 kg = 0.587; // グレースケール輝度係数 緑 kb = 0.114; // グレースケール輝度係数 青 // 一次微分 オペレーター cx0 : array[0..8] of integer = ( 0, 0, 0, // 一次微分 Gradient 通常の微分 オペレーター 0, 1,-1, 0, 0, 0); cy0 : array[0..8] of integer = ( 0, 0, 0, 0, 1, 0, 0,-1, 0); cx1 : array[0..8] of integer = ( 1, 0,-1, // 一次微分 Gradient Prewitt オペレーター 1, 0,-1, 1, 0,-1); cy1 : array[0..8] of integer = ( 1, 1, 1, 0, 0, 0, -1,-1,-1); cx2 : array[0..8] of integer = (-1,-2,-1, // 一次微分 Gradient Soble オペレーター 0, 0, 0, 1, 2, 1); cy2 : array[0..8] of integer = (-1, 0, 1, -2, 0, 2, -1, 0, 1); Laplacian4 : array[0..8] of integer = ( 0, 1, 0, // 二次微分 Laplacian オペレーター 1,-4, 1, 0, 1, 0); Laplacian8 : array[0..8] of integer = ( 1, 1, 1, // 二次微分 Laplacian オペレーター 1,-8, 1, 1, 1, 1); // 3 X 3 データー取り出し用 中心座標加算値 dy : array[0..8] of integer = (-1,-1,-1, 0, 0, 0, 1, 1, 1); dx : array[0..8] of integer = (-1, 0, 1, -1, 0, 1, -1, 0, 1); dgy : array[0..24] of integer = (-2,-2,-2,-2,-2, -1,-1,-1,-1,-1, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2); dgx : array[0..24] of integer = (-2,-1, 0, 1, 2, -2,-1, 0, 1, 2, -2,-1, 0, 1, 2, -2,-1, 0, 1, 2, -2,-1, 0, 1, 2); // 必要変数の宣言 var InBitmap : TBitmap; // ビットマップ OutBitmap : TBitmap; GHeight, GWidth : integer; // ソース画像サイズ y : TDIarray; // y 輝度 gr : TDIarray; // Gradient gy : TDIarray; // ガウシアンぼかし用 // 輝度(明度)値の取り出し function byte_data_read_gy(i, j: integer): double; begin if i < 0 then i := - i; if i > Gheight - 1 then i := Gheight + Gheight - 2 - i; // (Gheight - 1) - (i - (Gheight - 1)) if j < 0 then j := - j; if j > GWidth - 1 then j := GWidth + GWidth - 2 - j; // (GWidth - 1) - (j - (GWidth - 1)) Result := gy[i][j]; end; // RGBから輝度(明度)配列変換 // ガウシアンフィルターが指定されていたら、適用し輝度(明度)配列変換 procedure rgb_to_y; var i, j, k : Integer; fr, fg, fb : Double; bp : pbytearray; maxd : Double; begin setlength(y, GHeight, GWidth); // 輝度(明度)値配列確保 setlength(gr, GHeight, GWidth); // 微分結果配列確保 if Form1.CheckBox1.Checked then // ガウシアンぼかしが指定されていたら setlength(gy, GHeight, GWidth); // ガウシアン用配列確保 for i := 0 to GHeight - 1 do begin // 輝度(明度)配列データー作成 bp := InBitmap.ScanLine[i]; // 入力画像ラインポインタ取得 for j := 0 to GWidth - 1 do begin k := j * 3; // 1ピクセル3バイト fb := bp[k]; // Blue fg := bp[k + 1]; // Grren fr := bp[k + 2]; // Red maxd := max(fb, max(fg, fr)); // 最大値取り出し if Form1.CheckBox1.Checked then begin // ガウシアンぼかしが指定されていたら gy[i][j] := maxd; // ガウシアン実行のため別配列に保存 // gy[i][j] := (fb + fg + fr) / 3; // Y 平均値計算 // gy[i][j] := kr * fr + kg * fg + kb * fb; // Y NTSC輝度値計算 end else begin y[i][j] := maxd; // 指定されていなかったら明度配列に保存 // y[i][j] := (fb + fg + fr) / 3; // Y 平均値計算 // y[i][j] := kr * fr + kg * fg + kb * fb; // Y NTSC輝度値計算 end; end; end; // ガウシアンフィルターぼかしの実行 if Form1.CheckBox1.Checked then begin // ガウシアンぼかしが指定されていたら for i := 0 to GHeight - 1 do for j := 0 to GWidth - 1 do begin maxd := 0; if Form1.RadioGroup2.ItemIndex = 0 then begin // 3X3マスク選択時 for k := 0 to 8 do maxd := maxd + byte_data_read_gy(i + dy[k], j + dx[K]) * Gaussian3X3[k]; y[i][j] := maxd / Total3; // ぼかし明度保存 end; if Form1.RadioGroup2.ItemIndex = 1 then begin // 5X5マスク選択時 for k := 0 to 24 do maxd := maxd + byte_data_read_gy(i + dgy[k], j + dgx[K]) * Gaussian5X5[k]; y[i][j] := maxd / Total5; // ぼかし明度保存 end; end; end; end; // 輝度(明度)グレースケールの画像出力 procedure y_to_outBitmap; var i, j, k : integer; bp : pbytearray; fd : integer; begin for I := 0 to GHeight - 1 do begin bp := Outbitmap.ScanLine[i]; for j := 0 to GWidth - 1 do begin k := j * 3; fd := round(y[i][j]); bp[k] := fd; bp[k + 1] := fd; bp[k + 2] := fd; end; end; Form1.Image2.Picture.Bitmap := OutBitmap; // 画像表示 end; // 輝度値の取り出し function byte_data_read(i, j: integer): double; begin if i < 0 then i := - i; if i > Gheight - 1 then i := Gheight + Gheight - 2 - i; // (Gheight - 1) - (i - (Gheight - 1)) if j < 0 then j := - j; if j > GWidth - 1 then j := GWidth + GWidth - 2 - j; // (GWidth - 1) - (j - (GWidth - 1)) Result := y[i][j]; end; // 影消しプロセス // 一次微分と微分データー反転色付け // brightの値で、再現画像の明るさが変わります 最大値は255 // mf 0 影消し 1 微分値表示 procedure Gradient(percent, gain: double; mf : integer); const bright = 255; var i, j : integer; d : array[0..8] of double; k : integer; xx, yy : double; bp, inp : pbytearray; br, bg, bb : integer; fr, fg, fb, ff: double; maxd, ssd : double; begin for i := 0 to GHeight - 1 do begin for j := 0 to GWidth - 1 do begin for K := 0 to 8 do // 輝度(明度)グレーデーター取り出し d[k] := byte_data_read(i + dy[k],j + dx[k]); xx := 0; yy := 0; for k := 0 to 8 do begin case Form1.RadioGroup1.ItemIndex of // 微分フィルターの選択 0: begin // normal xx := xx + cx0[k] * d[k]; yy := yy + cy0[k] * d[k]; end; 1: begin // Prewitt xx := xx + cx1[k] * d[k]; yy := yy + cy1[k] * d[k]; end; 2: begin // Soble xx := xx + cx2[k] * d[k]; yy := yy + cy2[k] * d[k]; end; 3: xx := xx + Laplacian4[k] * d[k]; // Laplacian 4: xx := xx + Laplacian8[k] * d[k]; // Laplacian end; end; case Form1.RadioGroup1.ItemIndex of 0,1,2 : gr[i, j] := round(gain * sqrt(xx * xx + yy * yy)); // 微分値 3,4 : begin if XX < 0 then XX := 0; // プラスのみ採用 gr[i, j] := round(gain * XX); end; end; end; end; // 影消しルーチン if mf = 0 then begin for i := 0 to GHeight - 1 do begin bp := Outbitmap.ScanLine[i]; // 出力用ビットマップ inp := inbitmap.ScanLine[i]; // 入力画像ビットマップ for j := 0 to GWidth - 1 do begin k := j * 3; // 1ピクセル3バイト ff := bright - gr[i, j]; // 影消し輝度(明度)値計算 gr[]最大値は Soble時1442 if ff < 0 then ff := 0; // Soble Prewitt フィルター時はマイナスになる場合があますマイナス時は黒 fb := inp[k]; // 各色の取り出し blue fg := inp[k + 1]; // green fr := inp[k + 2]; // red maxd := max(fb, max(fg, fr)); // 最大値検索 if maxd > 0 then ssd := ff / maxd // 影消し輝度(明度)修正計数 maxd はゼロの時あり else ssd := 0; // maxd がゼロだつたらゼロをセット bb := round(fb * ssd * (1 - percent) + fb * percent); // blue 計算 影消し値と元の値の比率計算 bg := round(fg * ssd * (1 - percent) + fg * percent); // green br := round(fr * ssd * (1 - percent) + fr * percent); // red bp[k] := bb; // Outbitmapに保存 blue bp[k + 1] := bg; // green bp[k + 2] := br; // red end; end; end; // 微分値表示 if mf = 1 then begin for i := 0 to GHeight - 1 do begin bp := Outbitmap.ScanLine[i]; // 出力用ビットマップラインポインタ for j := 0 to GWidth - 1 do begin k := j * 3; // 1ピクセル3バイト bb := round(gr[i, j]); // 微分値 if bb > 255 then bb := 255; // gr[]最大値は Soble時1442 bp[k] := bb; // Outbitmapに保存 blue bp[k + 1] := bb; // green bp[k + 2] := bb; // red end; end; end; Form1.Image2.Picture.Bitmap := OutBitmap; // 画像表示 end; // 影消しルーチン実行 procedure TForm1.UtamaroBtnClick(Sender: TObject); var pp, pf : double; c : integer; gain : double; begin val(LabeledEdit1.Text, pp, c); if c <> 0 then begin application.MessageBox('%の値に間違いがあります。','元図割合%',0); exit; end; if pp > 100 then pp := 100; if pp < 0 then pp := 0; pf := pp / 100; val(LabeledEdit2.Text,gain,c); if c <> 0 then begin application.MessageBox('Gainの値に間違いがあります。','Gain',0); exit; end; if gain < 0 then gain := 0; rgb_to_y; // 輝度(明度)配列作成 // y_to_outBitmap; // 輝度(明度)表示 Gradient(pf, gain, 0); // 影消しプロセス end; // 微分値表示 procedure TForm1.GradientBtnClick(Sender: TObject); begin rgb_to_y; // 輝度(明度)配列作成 Gradient(0, 1); // 微分値表示 end; // 輝度(明度)表示 procedure TForm1.GrayBtnClick(Sender: TObject); begin rgb_to_y; // 輝度(明度)配列作成 y_to_outBitmap; // 影消し 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; UtamaroBtn.Enabled := True; GrayBtn.Enabled := True; GradientBtn.Enabled := True; end; // FileSave // TWICImage を使用してファイル保存 procedure TForm1.FileeSaveBtnClick(Sender: TObject); var WIC : TWicImage; WICF : TWicImageFormat; Fname : String; ExeStr : String; FnameTop: String; Findex : integer; function WFormatSet: Boolean; begin Result := false; ExeStr := LowerCase(ExeStr); if ExeStr = '.jpg' then begin WICF := Wifjpeg; Result := True; end; if ExeStr = '.jpeg' then begin WICF := Wifjpeg; Result := True; end; if ExeStr = '.tif' then begin WICF := Wiftiff; Result := True; end; if ExeStr = '.tiff' then begin WICF := Wiftiff; Result := True; end; if ExeStr = '.png' then begin WICF := Wifpng; Result := True; end; if ExeStr = '.gif' then begin WICF := Wifgif; Result := True; end; if ExeStr = '.bmp' then begin WICF := Wifbmp; Result := True; end; if ExeStr = '.wdp' then begin WICF := WifWMPhoto; Result := True; end; if ExeStr = '.hdp' then begin WICF := WifWMPhoto; Result := True; end; end; begin SavePictureDialog1.Filter := SaveFileFilter; // ファイルセーブフィルターの設定 if not SavePictureDialog1.Execute then exit; // ファイル名が設定されていなかったらここまで ExeStr := ExtractFileExt(SavePictureDialog1.FileName); if ExeStr = '' then begin Findex := SavePictureDialog1.FilterIndex; // FilterIndexによる拡張子の設定 case Findex of 1, 3 : Fname := ChangeFileExt(SavePictureDialog1.FileName,'.jpg'); // 拡張子の設定 2 : Fname := ChangeFileExt(SavePictureDialog1.FileName,'.png'); 4 : Fname := ChangeFileExt(SavePictureDialog1.FileName,'.gif'); 5 : Fname := ChangeFileExt(SavePictureDialog1.FileName,'.bmp'); 6 : Fname := ChangeFileExt(SavePictureDialog1.FileName,'.tif'); 7 : Fname := ChangeFileExt(SavePictureDialog1.FileName,'.wdp'); end; end else Fname := SavePictureDialog1.FileName; // 拡張子があったら ExeStr := ExtractFileExt(Fname); if not WFormatSet then begin application.MessageBox('ファイルの拡張子が間違っています。','注意', 0); exit; end; FnameTop := ExtractFileName(Fname); if Length(FnameTop) = Length(ExeStr) then begin application.MessageBox('ファイル名がありません。','注意', 0); exit; end; if FileExists(Fname) then if MessageDlg('既に同じ名前のファイルがあります上書きしますか ' + ExtractFileName(Fname) + '?', mtConfirmation, [mbYes, mbNo], 0, mbNo) = IDNo then exit; WIC := TWicImage.Create; try WIC.Assign(OutBitmap); WIC.ImageFormat := WICF; WIC.SaveTofile(Fname); // XE3,XE4 ではメモリーリークが発生します。 finally // アンシャープマスキングを参照して下さい。 WIC.Free; end; end; // 初期設定 procedure TForm1.FormCreate(Sender: TObject); begin UtamaroBtn.Enabled := False; GrayBtn.Enabled := False; GradientBtn.Enabled := False; 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ビットカラーに設定 end; // ビットマップの解法 procedure TForm1.FormDestroy(Sender: TObject); begin InBitmap.Free; OutBitmap.Free; end; end.