シャープネス

 画像のシャープさを改善するのにアンシャープマスクによる方法、デコンボリューションによる方法がありますが、デコンボリューションはFFTを使用しても、演算回数が膨大でデジタルカメラの写真画像の場合、非常に時間がかかり実用的でないので、演算回数の少ない、シャープネスを検討してみました。

シャープネスマスク

Sharpnessmask
 一番左は整数演算用のフィルター値ですが、シャープ化の制御をする為に、係数Kを適用したのが左から二番目です。
三番目は、フィルター値をどの様に設定すれば良いかを表しています。
微分をするために、マスク中央値以外をマイナスのフィルター値に設定
マスクの中央値を、周囲の合計値の符号を反転した値に1を加算した値にすればシャープ化マスクのフィルター値となります。
ここで紹介しているプログラムでは、先に中央値を決め、1を引いた値を反転して周囲に配分する方式をとっています。

  シャープネスの場合、微分値に元の画像を加算した形になります。
一回のマスク計算(カラーの場合三回)で済むので、高速演算が可能となります。
  計算結果は、アンシャープと同じです。
アンシャープ の場合、ボケ画像を作成、元画像との差分に元画像を加算していますが、ここでのシャープネスの計算は、フィルター値をボケ画像差分+元画像の値にして、一回のマスク計算で済もせているだけです。
シャープネス

 整数で演算した方が早いのですが、現在は浮動小数点演算を使用しても十分に早いので、ストレスを感じることはないでしょう。
上記のマスクの値は、中央値を決め、中央値から1を引いて、残った値を中央値の周囲にマイナスとして配分しています。
中央からの距離の自乗に反比例するようにしています。
マスクの値の合計値は1になります。
3×3、5×5、7×7のマスクを作成してみましたが、マスクを大きくしても、効果は上がりません。
3×3のシャープネスマスクだけで良いようです。
 中央値に1以下を設定するめと、平均化され、ボケ画像となります。

シャープネスサンプル 3×3のシャープネスマスクの実行例です。
実際に解像度が上がっているわけではないのですが、見た目には、かなり鮮明な画像になっています。



プログラム例

unit Main;

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, Vcl.grids,
  System.UITypes, System.Math;

type
  B2array = array of array of Byte;
  D2array = array of array of Double;

  TForm1 = class(TForm)
    FileOpenBtn: TButton;
    ScrollBox1: TScrollBox;
    Image1: TImage;
    OpenPictureDialog1: TOpenPictureDialog;
    magnificationEdit: TLabeledEdit;
    SharpnessGrid: TStringGrid;
    SharpnessEdit: TLabeledEdit;
    Timer1: TTimer;
    RepaintBtn: TButton;
    MaskingBtn: TButton;
    sourceBtn: TButton;
    FileSaveBtn: TButton;
    SavePictureDialog1: TSavePictureDialog;
    RadioGroup1: TRadioGroup;
    procedure FileOpenBtnClick(Sender: TObject);
    procedure FormCreate(Sender: TObject);
    procedure FormDestroy(Sender: TObject);
    procedure Timer1Timer(Sender: TObject);
    procedure RepaintBtnClick(Sender: TObject);
    procedure MaskingBtnClick(Sender: TObject);
    procedure FileSaveBtnClick(Sender: TObject);
    procedure sourceBtnClick(Sender: TObject);
    procedure SharpnessEditChange(Sender: TObject);
    procedure RadioGroup1Click(Sender: TObject);
  private
    { Private 宣言 }
    procedure Imageout(Image: TBitmap);
    procedure SharpnessMask;
    procedure Masking;
    procedure ScanArray;
  public
    { Public 宣言 }
  end;

var
  Form1: TForm1;

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';

  ImageHWC   = 440;    // 表示枠サイズ

var
  InputDBitmap  : TBitmap;                    // 入力データー表示用ビットマップ
  OutputBitmap  : TBitmap;                    // 回転画像表示用ビットマップ
  GHeight       : Integer;                    // 入力データー画像高さ
  GWidth        : Integer;                    // 入力データー画像幅
  MGHeight      : Integer;                    // 入力データー画像高さ
  MGWidth       : Integer;                    // 入力データー画像幅

  VRect         : Trect;                      // 表示サイズ設定用
  InFilename    : string;                     // 入力ファイル名

  RedMat        : B2array;
  GrnMat        : B2array;
  BluMat        : B2array;
  sharparray    : D2array;
  arBlu         : D2array;                    // データ実数部(入出力兼用)
  aiBlu         : D2array;                    // データ虚数部(入出力兼用)
  TimerF        : Byte;
  sharp         : integer;
  scand         : array of integer;

// 配列スキャン用配列の作成
procedure TForm1.ScanArray;
var
  MDIV2, x: integer;
begin
  case RadioGroup1.ItemIndex of
    0: sharp := 3;
    1: sharp := 5;
    2: sharp := 7;
  end;
  setlength(scand, sharp);
  MDIV2 := sharp div 2;
  // -----スキャン配列の作成-------
  for x := - MDIV2 to MDIV2 do scand[x + MDIV2] := x;
end;

//-------------------------------------------
// シャープネスフィルター作成
// フィルターの値の合計が1になるようにします
// a の値は負数に設定をします。
// a a a
// a c a
// a a a
// a = - (c - 1) / 8
//-------------------------------------------
procedure TForm1.SharpnessMask;
var
  x, y  : integer;
  MDIV2 : integer;
  ad    : double;
  C, at : Double;
  check : integer;
  Tsharp : Double;
begin
  setlength(sharparray, sharp, sharp);
  val(SharpnessEdit.Text, C, check);
  if check <> 0 then begin
    application.MessageBox('Sharpness 中心値入力に間違いがあります。','注意',0);
    exit;
  end;
  if C < 0 then begin
    application.MessageBox('Sharpness 中心値 0以下ではいけません。','注意',0);
    exit;
  end;
  // ----シャープネス配列の作成----
  MDIV2 := sharp div 2;
  Tsharp := 0;
  for y := - MDIV2 to MDIV2 do
    for x := - MDIV2 to MDIV2 do begin
      // 距離の自乗分の一計算 配列の中央値は0
      if (x = 0) and (y = 0) then ad := 0
                             else ad := 1 / (y * y +  x * x);
      // 配列に保存
      sharparray[y + MDIV2 , X + MDIV2] := ad;
      // 合計値計算
      Tsharp := Tsharp + ad;
    end;
  // 合計値が-(C-1)になるように補正
  at := - (C - 1) / Tsharp;
  for y := 0 to sharp - 1 do
    for x := 0 to sharp - 1 do
      sharparray[x, y] := sharparray[x, y] * at;
  // 配列の中央値 C をセット
  sharparray[MDIV2 , MDIV2] := C;
  // ------------------------------
  // ストリンググリッドの設定
  SharpnessGrid.ClientWidth  := (SharpnessGrid.DefaultColWidth + 1)  * (sharp + 1) - 1;
  SharpnessGrid.ColCount := sharp + 1;
  SharpnessGrid.ClientHeight := (SharpnessGrid.DefaultRowHeight + 1) * (sharp + 1) - 1;
  SharpnessGrid.RowCount := sharp + 1;
  // 固定セルにナンバーリング
  for y := - MDIV2 to MDIV2 do SharpnessGrid.Cells[0 , Y + MDIV2 + 1] := intTostr(Y);
  for x := - MDIV2 to MDIV2 do SharpnessGrid.Cells[X + MDIV2 + 1,  0] := intTostr(X);
  // シャープネスフィルター値の表示
  for x := 0 to sharp - 1 do
    for y := 0 to sharp - 1 do begin
      SharpnessGrid.Cells[x + 1, y + 1] := FloatTostrF(sharparray[x , y], ffFixed, 5,4);
    end;
  application.ProcessMessages;
end;

//------------------------
// 変倍出力
//------------------------
procedure TForm1.Imageout(Image: TBitmap);
var
  Rect0           : Trect;
  magnification   : Double;
  Check           : Integer;
  MW, MH          : Integer;
begin
  Val(magnificationEdit.Text,magnification,Check);
  if Check <> 0 then begin
    application.MessageBox('表示倍率入力に間違いがあります。','注意',0);
    exit;
  end;
  MW := Round(GWidth * magnification);
  MH := Round(GHeight * magnification);
  Rect0 := Rect(0, 0, MW, MH);
  Image1.Width := MW;
  Image1.Height := MH;
  Image1.Picture.Bitmap.SetSize(MW, MH);
  Image1.Canvas.StretchDraw(Rect0, Image);                // 出力枠に変倍出力
end;

//----------------------------------
// シャープネス処理
//----------------------------------
procedure TForm1.Masking;
var
  x, y       : integer;
  mx, my     : integer;
  dx, dy     : integer;
  Step       : integer;
  data       : Double;
  PBA        : PByteArray;
  DI         : Byte;
  WP         : integer;
begin
  ScanArray;
  SharpnessMask;                            // シャープネスフィルターの生成
  for Step := 0 to 2 do begin
    for y := 0 to MGHeight do
      for x := 0 to MGWidth do begin
        case Step of
          0 : arBlu[y, x] := BluMAT[y][x];
          1 : arBlu[y, x] := GrnMat[y][x];
          2 : arBlu[y, x] := RedMat[y][x];
        end;
        aiBlu[y, x] := 0.0;
      end;
    // ----- シャープネス計算 -------
    for y := 0 to MGHeight do
      for x := 0 to MGWidth do begin
        for my := 0 to sharp - 1 do begin
          dy := y + scand[my];
          if dy < 0        then dy := - dy;
          if dy > MGHeight then dy := MGheight - scand[my];
          for mx := 0 to sharp - 1 do begin
            dx := x + scand[mx];
            if dx < 0       then dx := - dx;
            if dx > MGWidth then dx := MGWidth - scand[mx];
            aiBlu[y, x] := aiBlu[y, x] + arBlu[dy, dx] * sharparray[my, mx];
          end;
        end;
    end;
    // ------------------------------
    for Y := 0 to MGheight do begin
      PBA := OutputBitmap.ScanLine[Y];
      for X := 0 to MGWidth do begin
        WP := X * 3;
        data := aiBlu[y, x];
        if data > 255 then data := 255;
        if data <   0 then data := 0;
        DI := Round(data);
        case Step of
          0 : PBA[WP]     := DI;
          1 : PBA[WP + 1] := DI;
          2 : PBA[WP + 2] := DI;
        end;
      end;
    end;
  end;
  Imageout(OutputBitmap);               // 出力枠に変倍出力
  FileSaveBtn.Enabled := True;
  RepaintBtn.Enabled := True;
  sourceBtn.Enabled := True;
  MaskingBtn.Enabled := False;
end;

//-------------------------------------------------------------------------------
// シャープネス実行
// ボタンのデスエブルアニメション実行の待ち合わせの為タイマーで演算開始遅延します
//-------------------------------------------------------------------------------
procedure TForm1.MaskingBtnClick(Sender: TObject);
begin
  FileSaveBtn.Enabled := False;
  RepaintBtn.Enabled := False;
  MaskingBtn.Enabled := False;
  sourceBtn.Enabled := False;
  Timer1.Enabled := True;
end;

//----------------------------
// 計算結果の再表示
//----------------------------
procedure TForm1.RepaintBtnClick(Sender: TObject);
begin
  Imageout(OutputBitmap);               // 出力枠に変倍出力
end;

//----------------------------------
// ソース画像の表示
//----------------------------------
procedure TForm1.sourceBtnClick(Sender: TObject);
begin
  Imageout(InputDBitmap);               // 出力枠に変倍出力
end;

//-----------------------------------
// 最計算の為 マスクボタンイネーブル
//-----------------------------------
procedure TForm1.SharpnessEditChange(Sender: TObject);
begin
  MaskingBtn.Enabled := True;
end;

procedure TForm1.RadioGroup1Click(Sender: TObject);
begin
  MaskingBtn.Enabled := True;
end;

//-------------------------------
// 表示計算タイミングの制御
//-------------------------------
procedure TForm1.Timer1Timer(Sender: TObject);
begin
  Timer1.Enabled := False;
  case TimerF of
    0 : begin
          ScanArray;
          SharpnessMask;
          TimerF := 1;
        end;
    1: Masking;
  end;
end;

//---------------------------------------------------
// ファイルのオープン カラーデーターの生成
//---------------------------------------------------
procedure TForm1.FileOpenBtnClick(Sender: TObject);
var
  WIC         : TWICImage;
  X, Y        : Integer;
  PBA         : PBytearray;
  WP          : Integer;
begin
  VRect := Rect(0, 0, Image1.Width, Image1.Height);
  Image1.Canvas.Brush.Style := bsSolid;
  Image1.Canvas.Brush.Color := clBtnface;
  Image1.Canvas.FillRect(VRect);                              // Canvas 画像消去
  OpenPictureDialog1.Filter := OpenFileFilter;                // ファイルオープンフィルターの設定
  if OpenPictureDialog1.Execute = true then                   // ファイルが指定されたら
    begin
      WIC := TWICImage.Create;                                // TWICImageの生成
      try
        InFilename := OpenPictureDialog1.FileName;            // ファイル名の取得
        WIC.LoadFromFile(InFilename);                         // 画像の読み込み
        GHeight := WIC.Height;                                // 画像高さ取得
        GWidth  := WIC.Width;                                 // 画像幅
        Image1.Width := GWidth;
        Image1.Height:= GHeight;
        Image1.Picture.Bitmap.SetSize(GWidth, GHeight);
        InputDBitmap.Width := GWidth;
        InputDBitmap.Height := GHeight;
        InputDBitmap.Canvas.Draw(0, 0, WIC);                  // ビットマップに描画フォーマット変換
//        InputDBitmap.Assign(WIC);                             // Tbitmap に TWICImageをコピー
      finally
        WIC.Free;                                             // TWICImage 解放
      end;
    end
    else exit;
  MGHeight := GHeight - 1;                                    // 入力データー画像高さ
  MGWidth  := GWidth  - 1;                                    // 入力データー画像幅

  OutputBitmap.Width  := GWidth;
  OutputBitmap.Height := GHeight;
  // 作業用配列の確保
  setLength(RedMat, GHeight, GWidth);
  setLength(GrnMat, GHeight, GWidth);
  setLength(BluMat, GHeight, GWidth);
  setlength(arBlu, GHeight, GWidth);
  setlength(aiBlu, GHeight, GWidth);

  // カラー分解
  for Y := 0 to MGheight do begin
    PBA := InputDBitmap.ScanLine[Y];
    for X := 0 to MGWidth do begin
      WP := X * 3;
      // ラプラシアン用グレイ画像生成
      // カラー用三色分解
      BluMAT[Y, X] := PBA[WP];
      GrnMAT[Y, X] := PBA[WP + 1];
      RedMAT[Y, X] := PBA[WP + 2];
    end;
  end;
  Imageout(InputDBitmap);               // 出力枠に変倍出力
  MaskingBtn.Enabled := True;
  sourceBtn.Enabled := True;
  RadioGroup1.Enabled := True;
end;

//------------------------------
// 画像のファイルへの保存
//------------------------------
procedure TForm1.FileSaveBtnClick(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;
//  SavePictureDialog1.DefaultExt := GraphicExtension(TWicImage);
  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;                                                 // TWicImage生成
  try
    WIC.Assign(OutputBitmap);                                              // TWicImageにビットマップデーター割り付け
    WIC.ImageFormat := WICF;                                               // 保存フォーマットセット
    WIC.SaveTofile(Fname);                                                 // ファイルの書き出し
  finally
    WIC.Free;                                                              // TWicImage解放
  end;
end;

//----------------
// 初期設定
//----------------
procedure TForm1.FormCreate(Sender: TObject);
begin
  Timer1.Enabled := False;
  Image1.Width  := ImageHWC div 2;
  Image1.Height := ImageHWC div 2;
  ScrollBox1.Height := ImageHWC;
  ScrollBox1.Width  := ImageHWC;
  Image1.Top := 0;
  Image1.Left := 0;
  InputDBitmap  := TBitmap.Create;
  OutputBitmap  := TBitmap.Create;
  InputDBitmap.PixelFormat := pf24bit;
  OutputBitmap.PixelFormat := pf24bit;
  MaskingBtn.Enabled := False;
  FileSaveBtn.Enabled := False;
  RepaintBtn.Enabled := False;
  sourceBtn.Enabled := False;
  RadioGroup1.Enabled := False;
  TimerF := 0;
  Timer1.Enabled := True;
end;

//----------------
// 終了処理
//----------------
procedure TForm1.FormDestroy(Sender: TObject);
begin
  InputDBitmap.Free;
  OutputBitmap.Free;
end;

end.

  Sharpness.zip ファイルの中には、FFTを使用した シャープネスもあります。
FFTのプログラムは、アンシャープマスク、デコンボリューションのプログラムにシャープネスを追加したもので、各プロセスによる比較が出来ます。

download Sharpness.zip

画像処理一覧へ戻る

    最初に戻る