画像の合成
画像の合成は、二枚以上の画像を重ね合わせることですが、一番良く使われるのは、人の画像を違う背景に貼り付けることですが、一番有名な合成画像は、UFO画像です。
今まで、沢山のUFO画像が公開せれていますが、未だにUFOの存在を証明するものはありません。
丹念に時間をかけて、合成画像を作成すれば、誰でもかなりレペルの高い合成画像を作成することが出来ます。
映像の場合は、合成する画像の数が多いので、プログラムに頼る必要があります。
合成画像作成のためのツールは、インターネットで検索すれば、出てきますが、ここではダウンロードツールを利用するのではなく、合成の仕組みと、プログラムついて検討します。
2つの画像を重ね合わせる場合、前景になる画像と、背景になる画像の二画像となりますが、前景画像の必要な部分をだけを残した画像が必要となります。
実際には、前景画像を残す部分を白として、背景を透過させる部分を黒とした、クロマキー(Chroma-Key)画像を作成します。
1. ペイントによる画像の合成
前景画像 クロマキー 合成画像
一番左側が、前景となる画像で、合成するのに不要となる前景画像の背景部分を黒で塗りつぶします。これが中央の画像です。
この作業は、Windows標準のペイントで行いましたので、必要な部分を丹念に黒で縁取り後、不必要な部分をブラシで塗りつぶしをしています。
ペイントの場合、透過色を、第二色(背景色)として、色の指定が出来ます。
背景となる画像を開いておき、透過色を黒に指定して、貼り付ければ、一番右の画像のような画像を作成することが出来ます。
現在はデジタル処理なので、簡単に合成が出来ますが、アナログの時代は大変でした。
ブルー・スクリーンを使用して、青い背景の画像を作成し、青だけを透過するフィルターにより、不必要な画像部が黒となるフィルムとそれを反転したフィルムを作成
次に、背景用クロマキーフィルムにより、前景部が露光されないようにし、次に前景用クロマキーフィルムにより、前景だけ露光します。
手順は逆でも構いませんが。
ブルースクリーンの使用は、人の顔の色と対比状態にあるのは、青だからですが、必要に応じて違う色を使用しても良いでしょう。
2.プログラムによる画像の合成
デジタル画像の合成の場合、どの色を透過色にするかを決定し、透過色を黒(値 0)にします。
(単にキー画像なので、赤でも青でも構いませんが、黒だと説明がし易いので黒にしています。)
2-1 輝度値によるクロマキーの作成
輝度値 =
赤 × 赤系数 + 緑 × 緑系数 + 青 × 青系数
輝度値の値により、透過させるか、画像として残すかの判別をします。
前景画像のバック色の色に対して、マイナスの係数を与えると良い結果が得られます。
画像の条件により、色係数の値と、閾値の調整をする事により、目的とするクロマーキーを作成します。
2値化キー合成
各ピクセルの値 青には、-1.35を乗じ、赤には1を乗じ 緑には1.2を乗じて 合計、2値化キーの場合閾値より大きかったら、255とし、小さかったら0としクロマキーを作成します。
値は、255 と 0 の2値となります。
最大値、最小値は、全ピクセルをキー色係数で計算した場合の最大値と最小値を表しています。
参考表示です。
2値化キーによるクロマキーと、合成画像ですが、一応それなりに透明感は出ていますが、滑らかさは有りません。
多値化キー合成
上限値と下限値を設け、その間を中間濃度として、中間の透過率とします。
色に対する係数は一応2値化キーと同じです。
多値化キーによるクロマキーと合成画像は、半透明画像部が出来 前景画像と背景画像が透過度に応じて、加算されるので、合成画像が、柔らかい感じの画像となります。
透過度は、255がゼロパーセントで0が百パーセントとなります。
前景用画像として、赤をバックにしたものを用意してみました。
赤のキー係数をマイナスにして、多値のクロマキーを作成、合成してみました。
グラスの色が赤みがかっていますが、綺麗に合成ができています。
2-2 色相によるクロマキーの作成
色相を指定し、その色相に近い色を透過色にすることに簡単にクロマキーの作成が出来るのではないかと思い、プログラムを作成してみました。
最初の前傾画像のバック色の色相を取り出すと、上図の様になります。
色相誤差は、透過色として判別する時の許容誤差です。
グラスの透明な部分が、消えすぎて、あまり良い結果が得られませんでした。
透明な部分は、バックの色相と同じで、明度の差のみのだからだと思われます。
クロマキーの作成は、各色にたいする係数を調整して計算した輝度値で判別して作成するのが良いようです。
サンプルプログラム
unit Main; interface uses Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics, Vcl.Controls, Vcl.Forms, Vcl.Dialogs, system.Math, Vcl.ExtDlgs, Vcl.ExtCtrls, System.UIConsts, Vcl.StdCtrls, System.UITypes, Vcl.Imaging.GIFImg; type TForm1 = class(TForm) FileOpenBtn: TButton; ScrollBox1: TScrollBox; ScrollBox2: TScrollBox; Image1: TImage; Image2: TImage; OpenPictureDialog1: TOpenPictureDialog; SavePictureDialog1: TSavePictureDialog; FileSaveBtn: TButton; GroupBox1: TGroupBox; hard_maskBtn: TButton; LabeledEdit1: TLabeledEdit; GroupBox2: TGroupBox; LabeledEdit3: TLabeledEdit; LabeledEdit2: TLabeledEdit; Soft_maskBtn: TButton; ScrollBox3: TScrollBox; Image3: TImage; ScrollBox4: TScrollBox; Image4: TImage; Label1: TLabel; Label2: TLabel; Label3: TLabel; Label4: TLabel; GroupBox3: TGroupBox; RedEdit: TLabeledEdit; GreenEdit: TLabeledEdit; BlueEdit: TLabeledEdit; BakFileOpenBtn: TButton; LabeledEdit4: TLabeledEdit; LabeledEdit5: TLabeledEdit; procedure FormCreate(Sender: TObject); procedure FormDestroy(Sender: TObject); procedure FileOpenBtnClick(Sender: TObject); procedure FileSaveBtnClick(Sender: TObject); procedure hard_maskBtnClick(Sender: TObject); procedure Soft_maskBtnClick(Sender: TObject); procedure BakFileOpenBtnClick(Sender: TObject); private { Private 宣言 } procedure FileOpen(InF : Boolean); procedure hard_mask; procedure soft_mask; procedure composit; function key_coefficient: Boolean; procedure MaxMin; public { Public 宣言 } end; var Form1: TForm1; implementation {$R *.dfm} type TPrgbarry = array[0..0] of Trgbtriple; // 24ビットカラーレコード 32ビット用はTRGBQuadArray Prgbarray = ^TPrgbarry; // ポインター 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'; var InpBitmap : TBitmap; // 前景ビットマップ BakBitmap : TBitmap; // 背景ビットマップ OutBitmap : TBitmap; // 出力用ビットマップ GHeight, GWidth : Integer; // 前景画像サイズ BHeight, BWidth : Integer; // 背景画像サイズ ImageKey : array of array of byte; // マスク用キー配列 FImage : Byte; // イメージフラグ 1bit 前景 2bit 背景 Rkey, Gkey, Bkey: double; // キー作成用色係数 Dmax, Dmin : smallint; // 最大値最小値 //============================== // キー作成用色係数 設定 //============================== function TForm1.key_coefficient: Boolean; var C : Integer; begin Result := True; Val(Rededit.Text, Rkey, C); if C <> 0 then begin application.MessageBox('赤キーの値 数値ではありません。','注意', 0); Result := False; exit; end; val(Greenedit.Text, Gkey, C); if C <> 0 then begin application.MessageBox('緑キーの値 数値ではありません。','注意', 0); Result := False; exit; end; val(Blueedit.Text, Bkey, C); if C <> 0 then begin application.MessageBox('青キーの値 数値ではありません。','注意', 0); Result := False; exit; end; end; //========================== // キー最大値最小値表示 //========================== procedure TForm1.MaxMin; begin LabeledEdit4.Text := intTostr(Dmax); LabeledEdit5.Text := intTostr(Dmin); end; //======================================================= // 合成 // Key = 0 の場合 背景 255の場合前景 // 値に応じて、前景の値と背景の値を比率で加算 //======================================================= procedure TForm1.composit; var PinpB : Prgbarray; PbakB : Prgbarray; PoutB : Prgbarray; YY, XX : Integer; Key, Mkey : Byte; begin for YY := 0 to GHeight - 1 do begin PinpB := InpBitmap.ScanLine[YY]; PbakB := BakBitmap.ScanLine[YY]; PoutB := OutBitmap.ScanLine[YY]; for XX := 0 to GWidth - 1 do begin // 合成用キーデーター Key := ImageKey[YY, XX]; MKey := MaxByte - Key; // Keyの値で、各色の値設定 PoutB[XX].rgbtBlue := (PinpB[XX].rgbtBlue * Key + PbakB[XX].rgbtBlue * MKey) div MaxByte; PoutB[XX].rgbtGreen := (PinpB[XX].rgbtGreen * Key + PbakB[XX].rgbtGreen * MKey) div MaxByte; PoutB[XX].rgbtRed := (PinpB[XX].rgbtRed * Key + PbakB[XX].rgbtRed * MKey) div MaxByte; end; end; image4.Picture.Bitmap := OutBitmap; // 画像表示 FileSaveBtn.Enabled := True; end; //======================== // 二値化キーマスクの生成 //======================== procedure TForm1.hard_mask; var thresh : Smallint; C : Integer; PinpB : Prgbarray; PoutB : Prgbarray; YY, XX : Integer; Data : Smallint; begin val(LabeledEdit1.Text, thresh, C); if C <> 0 then begin application.MessageBox('二値化キーの値 整数の数値ではありません。','注意', 0); exit; end; Dmin := 255 * 3; DMax := -255 * 3; for YY := 0 to GHeight - 1 do begin PinpB := InpBitmap.ScanLine[YY]; PoutB := OutBitmap.ScanLine[YY]; for XX := 0 to GWidth - 1 do begin Data := round(PinpB[XX].rgbtRed * Rkey + PinpB[XX].rgbtGreen * Gkey+ PinpB[XX].rgbtBlue * Bkey); if Dmin > Data then Dmin := Data; if DMax < Data then Dmax := Data; if Data >= thresh then ImageKey[YY, XX] := 255 else ImageKey[YY, XX] := 0; PoutB[XX].rgbtBlue := ImageKey[YY, XX]; PoutB[XX].rgbtGreen := ImageKey[YY, XX]; PoutB[XX].rgbtRed := ImageKey[YY, XX]; end; end; image2.Picture.Bitmap := OutBitmap; // 画像表示 MaxMin; end; //========================== // 二値化キーによる合成 //========================== procedure TForm1.hard_maskBtnClick(Sender: TObject); begin if not key_coefficient then exit; hard_mask; composit; end; //======================== // 多値化キーマスクの生成 //======================== procedure TForm1.soft_mask; var threshHi : Smallint; threshLow : Smallint; C : Integer; PinpB : Prgbarray; PoutB : Prgbarray; YY, XX : Integer; Data : Integer; DK : Integer; begin val(LabeledEdit2.Text, threshHi, C); if C <> 0 then begin application.MessageBox('多値化キー閾値上 整数の数値ではありません。','注意', 0); exit; end; val(LabeledEdit3.Text, threshLow, C); if C <> 0 then begin application.MessageBox('多値化キー閾値下 整数の数値ではありません。','注意', 0); exit; end; if threshHi <= threshLow then begin application.MessageBox('閾値の値、上と下が同じか、下が大きいです。','注意', 0); exit; end; Dmin := 255 * 3; DMax := -255 * 3; for YY := 0 to GHeight - 1 do begin PinpB := InpBitmap.ScanLine[YY]; PoutB := OutBitmap.ScanLine[YY]; for XX := 0 to GWidth - 1 do begin // 色係数による三色の値加算 Data := Round(PinpB[XX].rgbtRed * Rkey + PinpB[XX].rgbtGreen * GKey + PinpB[XX].rgbtBlue * Bkey); if Dmin > Data then Dmin := Data; if DMax < Data then Dmax := Data; DK := Round((Data - threshLow) * 255 / (threshHi - threshLow)); if DK > 255 then DK := 255; if DK < 0 then DK := 0; ImageKey[YY, XX] := DK; PoutB[XX].rgbtBlue := DK; PoutB[XX].rgbtGreen := DK; PoutB[XX].rgbtRed := DK; end; end; image2.Picture.Bitmap := OutBitmap; // 画像表示 MaxMin; end; //================================ // 多値化キー合成 //================================ procedure TForm1.Soft_maskBtnClick(Sender: TObject); begin if not key_coefficient then exit; soft_mask; composit; end; //======================================================== // ファイルの指定オープン // オープン画像表示 Treuで前景オープン Fasleで背景オープン // 前景と背景の画像サイズを同じにしておく必要があります //======================================================== procedure TForm1.FileOpen(InF : Boolean); 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); // 画像の読み込み // 前景画像読み込み if InF then begin GHeight := WIC.Height; // 画像高さ取得 GWidth := WIC.Width; // 画像幅 InpBitmap.Width := GWidth; // 画像幅 InpBitmap.Height := GHeight; // 画像高さ InpBitmap.Canvas.Draw(0, 0, WIC); // DrawでInBitmapに入力画像設定フォーマット32ビットに変換されます image1.Picture.Bitmap := InpBitmap; // 前景画像表示 FImage := FImage or 1; // イメージフラグ 1bit 前景 2bit 背景 end // 背景画像読み込み else Begin BHeight := WIC.Height; // 画像高さ取得 BWidth := WIC.Width; // 画像幅 BakBitmap.Width := BWidth; // 画像幅 BakBitmap.Height := BHeight; // 画像高さ BakBitmap.Canvas.Draw(0, 0, WIC); // DrawでInBitmapに入力画像設定フォーマット32ビットに変換されます image3.Picture.Bitmap := BakBitmap; // 背景画像表示 FImage := FImage or 2; // イメージフラグ 1bit 前景 2bit 背景 end; finally WIC.Free; // TWICImage 解放 end; end else Exit; // 前景画像と背景画像のサイズ確認 if (FImage and 3 = 3) and (GHeight = BHeight) and (GWidth = BWidth) then begin hard_maskBtn.Enabled := True; Soft_maskBtn.Enabled := True; OutBitmap.Width := GWidth; // 出力画像幅 OutBitmap.Height := GHeight; // 出力画像高さ Setlength(ImageKey, GHeight, GWidth); end else begin hard_maskBtn.Enabled := False; Soft_maskBtn.Enabled := False; if FImage and 3 = 3 then application.MessageBox('前景画像のサイズと背景画像のサイズが違います', '注意', 0); end; end; //================================== // 前景画像読み込み //================================== procedure TForm1.FileOpenBtnClick(Sender: TObject); begin FileOpen(True); end; //================================== // 背景画像読み込み //================================== procedure TForm1.BakFileOpenBtnClick(Sender: TObject); begin FileOpen(False); 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(OutBitmap); // TWicImageにビットマップデーター割り付け WIC.ImageFormat := WICF; // 保存フォーマットセット WIC.SaveTofile(Fname); // ファイルの書き出し finally // XE3,XE4の場合はアンシャープマスキングを参照してください WIC.Free; // TWicImage解放 end; end; //============ // 初期設定 //============ procedure TForm1.FormCreate(Sender: TObject); begin Top := (Screen.Height - Height) div 2; // 表示位置設定 Left := (Screen.Width - Width) div 2; InpBitmap := TBitmap.Create; // 前景画像用 BakBitmap := TBitmap.Create; // 背景画像用 OutBitmap := TBitmap.Create; // 出力画像用 InpBitmap.PixelFormat := pf24bit; // 24ビットカラーに設定 BakBitmap.PixelFormat := pf24bit; // 24ビットカラーに設定 OutBitmap.PixelFormat := pf24bit; // 24ビットカラーに設定 FImage := 0; // イメージフラグ 1bit 前景 2bit 背景 FileSaveBtn.Enabled := False; // 画像入力フラグリセット hard_maskBtn.Enabled := False; Soft_maskBtn.Enabled := False; end; //=============== // 終了処理 //=============== procedure TForm1.FormDestroy(Sender: TObject); begin InpBitmap.Free; BakBitmap.Free; OutBitmap.Free; end; end.
RGBtoHSL変換を利用して色相の値から、透過色を決める例
//========================== // 前景画像から透過色抽出 //========================== procedure TForm1.Image1MouseDown(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer); var Pcolor: TAlphacolor; S, L : Single; Spos : Integer; begin Pcolor := image1.Canvas.Pixels[X, Y]; RGBtoHsl(Pcolor, huein, S, L); Spos := Round(huein * 360); if Spos >= 360 then Spos := 0; ScrollBar1.Position := Spos; Maxhue := huein + 0.05; if Maxhue >= 1 then Maxhue := Maxhue - 1; MinHue := huein - 0.05; if MinHue < 0 then MinHue := Minhue + 1; end; //======================== // 二値化キーマスクの生成 //======================== procedure TForm1.hard_mask; var C : Integer; PinpB : Prgbarray; PoutB : Prgbarray; YY, XX : Integer; DColor : Tcolor; Data : Single; S, L : Single; MaXL : Single; GosaHu : Single; begin if ScrollBar1.Position < 0 then begin application.MessageBox('透明色の指定がされていません。', '注意', 0); exit; end; val(LabeledEdit1.Text, MaXL, C); if C<>0 then begin application.MessageBox('閾値入力が数値ではありません', '注意', 0); exit; end; val(LabeledEdit4.Text, GosaHu, C); if C<>0 then begin application.MessageBox('色相誤差値が数値ではありません', '注意', 0); exit; end; maxhue := huein + GosaHu; minhue := huein - GosaHu; for YY := 0 to GHeight - 1 do begin PinpB := InpBitmap.ScanLine[YY]; PoutB := OutBitmap.ScanLine[YY]; for XX := 0 to GWidth - 1 do begin DColor := RGB(PinpB[XX].rgbtRed, PinpB[XX].rgbtGreen, PinpB[XX].rgbtBlue); RGBtoHSL(DColor, Data, S, L); if Data = 1 then Data := 0; if maxhue < minhue then begin if ((Data < maxhue) or (Data > minhue)) and (L <= MaXL) then ImageKey[YY, XX] := 0 else ImageKey[YY, XX] := 255; end else begin if (Data < maxhue) and (Data > minhue) and (L <= MaXL) then ImageKey[YY, XX] := 0 else ImageKey[YY, XX] := 255; end; PoutB[XX].rgbtBlue := ImageKey[YY, XX]; PoutB[XX].rgbtGreen := ImageKey[YY, XX]; PoutB[XX].rgbtRed := ImageKey[YY, XX]; end; end; image2.Picture.Bitmap := OutBitmap; // 画像表示 composit; end;