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にします。



合成
Sobel

 ガンマ値で補正するよりも、暗い部分、影の部分を綺麗に、明るくできていると思います。

ここで取り上げた画像サンプルでは、綺麗に再現していますが、暗い部分の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.

    download RGB_HSV.zip

画像処理一覧へ戻る

      最初に戻る