闲人阿发伯的业余编程心得

闲云野鹤般的生活

Delphi图像处理 -- 图像色阶调整

阅读提示:

    《Delphi图像处理》系列以效率为侧重点,一般代码为PASCAL,核心代码采用BASM。

    《C++图像处理》系列以代码清晰,可读性为主,全部使用C++代码。

    尽可能保持二者内容一致,可相互对照。

   本文代码必须包括文章《Delphi图像处理 -- 数据类型及公用过程》中的ImageData.pas单元


    在Photoshop中,图像色阶调整应用很广泛,本文介绍的图像色阶调整过程与Photoshop处理效果基本一致。

    Photoshop的色阶调整分输入色阶调整和输出色阶调整,其中输入色阶调整有3个调整点,即通常所说的黑场、白场及灰场调整。

    输入色阶调整的基本算法并不复杂,首先计算出白场与黑场的离差Diff,然后计算出像素各份量值与黑场的离差rgbDiff,如果rgbDiff<=0,像素各份量值等于0,否则,计算以rgbDiff与Diff的比值为底的灰场倒数的幂。用公式表示:

    Diff = Highlight -Shadow

    rgbDiff = RGB - Shadow

    clRGB = Power(rgbDiff / Diff,  1 / Midtones)

    其中Shadow为输入色阶低端数据(黑场),Highlight为输入色阶高端数据(白场), Midtones为输入色阶中间数据(灰场),Diff为二者的离差(必须大于1),RGB为调整前的像素分量值,clRGB为调整输入色阶后的像素分量值。

    输出色阶调整更简单,首先计算输出色阶白场与黑场的离差与255的比值系数,然后用输入色阶调整后的像素分量值乘上这个系数,再加上输出黑场值即可。用公式表示:

    outClRGB = clRGB * (outHighlight - outShadow) / 255 + outShadow

    其中,outShadow为输出黑场,outHighlight为输出白场,outClRGB为全部色阶调整后的像素分量值。

    前面已经提到输入色阶黑白场的离差必须大于1,而输入色阶并没有这个限制,输出黑白场的离差可以为负数,当输出黑场与白场完全颠倒时,输出色阶调整后的图片为原图片的负片。

    色阶调整涉及四个通道,即R、G、B各分量通道及整体颜色通道,调整如果每个通道单独调整,将是比较麻烦和耗时的,本文采用色阶表替换法,可一次性完成所有四个通道的色阶调整。

    下面直接给出一个完整的图像色阶调整例子源代码,其中包含了色阶调整和灰度计算函数:

unit main;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, StdCtrls, ExtCtrls, ComCtrls, ImageData, Gdiplus;

type
  // 色阶项结构
  PColorLevelItem = ^TColorLevelItem;
  TColorLevelItem = packed record
    Shadow: LongWord;
    Midtones: Single;
    Highlight: LongWord;
    OutShadow: LongWord;
    OutHighlight: LongWord;
  end;

  // 色阶通道结构
  PColorLevelData = ^TColorLevelData;
  TColorLevelData = record
    Blue: TColorLevelItem;
    Green: TColorLevelItem;
    Red: TColorLevelItem;
    RGB: TColorLevelItem;
  end;

  // 256色灰度统计数组,每个元素表示该下标对应的颜色个数
  PGrayArray = ^TGrayArray;
  TGrayArray = array[0..255] of LongWord;
  // 灰度信息结构
  PImageGrayInfo = ^TImageGrayInfo;
  TImageGrayInfo = packed record
    Grays: TGrayArray;          // 灰度数组
    Total: int64;               // 总的灰度值
    Count: LongWord;            // 总的像素点数
    MaxValue: LongWord;         // 像素点最多的灰度值
    MinValue: LongWord;         // 像素点最少的灰度值
    Average: LongWord;          // 平均灰度值(Total / Count)
  end;

  TMainForm = class(TForm)
    LBar: TTrackBar;
    HBar: TTrackBar;
    Button1: TButton;
    Label3: TLabel;
    GrayMap: TPaintBox;
    ComboBox1: TComboBox;
    Label1: TLabel;
    LLabel: TLabel;
    HLabel: TLabel;
    MBar: TTrackBar;
    MLabel: TLabel;
    PaintBox1: TPaintBox;
    OLBar: TTrackBar;
    OHBar: TTrackBar;
    OLLabel: TLabel;
    OHLabel: TLabel;
    Label5: TLabel;
    procedure FormCreate(Sender: TObject);
    procedure FormDestroy(Sender: TObject);
    procedure GrayMapPaint(Sender: TObject);
    procedure LBarChange(Sender: TObject);
    procedure HBarChange(Sender: TObject);
    procedure Button1Click(Sender: TObject);
    procedure ComboBox1Change(Sender: TObject);
    procedure MBarChange(Sender: TObject);
    procedure PaintBox1Paint(Sender: TObject);
    procedure OLBarChange(Sender: TObject);
    procedure OHBarChange(Sender: TObject);
  private
    { Private declarations }
    FBitmap: TGpBitmap;
    FSource: TImageData;
    FDest: TImageData;
    FLevelData: TColorLevelData;
    FLevelItems: array[0..3] of PColorLevelItem;
    FGrayInfos: array[0..3] of TImageGrayInfo;
    FLock: Boolean;
    FIsRun: Boolean;
    FAbort: Boolean;
  public
    { Public declarations }
    procedure AdjustmentImage;
    procedure GetGrayInfos;
  end;

var
  MainForm: TMainForm;

implementation

uses Math;

{$R *.dfm}

const
  GRAY_MMX: TMMType = (3735, 19235, 9798, 0); // [0.114,0.587,0.229] * 32768

// 获取图像灰度信息
function GetGrayInfo(const Data: TImageData; var GrayInfo: TImageGrayInfo;
  isGrayImage: Boolean = False): Integer;
asm
    push      ebp
    push      esi
    push      edi
    push      ebx
    push      ecx
    push      eax
    mov       edi, edx
    mov       esi, edx
    xor       eax, eax
    mov       ecx, 256
    rep       stosd               // init Grays
    pop       eax
    call      _SetDataRegs
    mov       eax, ecx
    imul      eax, edx
    xchg      eax, [esp]          // pixel count
    test      al, al
    jnz       @@yGrayLoop

    // 建立灰度数组
    pxor      mm7, mm7
    movq      mm2, GRAY_MMX
@@yLoop:
    push      ecx
@@xLoop:
    movd      mm0, [edi]
    punpcklbw mm0, mm7
    pmaddwd   mm0, mm2
    movq      mm1, mm0
    psrlq     mm1, 32
    paddd     mm0, mm1
    movd      eax, mm0
    add       eax, 16384
    shr       eax, 15
    inc       [esi].TImageGrayInfo.Grays[eax*4].Integer
    add       edi, 4              // grayData.Grays[gray] ++
    loop      @@xLoop
    pop       ecx
    add       edi, ebx
    dec       edx
    jnz       @@yLoop
    emms
    jmp       @@SumStart
    // 建立灰度图的灰度数组
@@yGrayLoop:
    push      ecx
@@xGrayLoop:
    movzx     eax, [edi].TARGBQuad.Blue       // gray = *edi
    inc       [esi].TImageGrayInfo.Grays[eax*4].Integer// grayData.Grays[gray] ++
    add       edi, 4
    loop      @@xGrayLoop
    pop       ecx
    add       edi, ebx
    dec       edx
    jnz       @@yGrayLoop
    // 计算总的灰度值、最大灰度值及最小灰度值
@@SumStart:
    push      esi
    mov       edi, esi            // esi = ebx = &GrayData.Grays[0]
    mov       ebx, esi
    xor       eax, eax            // edx:eax = 0 (GrayData.Total)
    xor       edx, edx
    xor       ecx, ecx            // for (index = 0; index < 256; index ++)
@@SumLoop:                      // {
    mov       ebp, [edi]
    cmp       [esi], ebp
    cmovb     esi, edi            //   if (*esi < *edi) esi = edi
    cmp       [ebx], ebp
    cmova     ebx, edi            //   if (*ebx > *edi) ebx = edi
    imul      ebp, ecx            //   ebp = *edi * index
    add       eax, ebp            //   edx:eax += ebp
    adc       edx, 0
    add       edi, 4              //   edi += 4
    inc       ecx
    cmp       ecx, 255
    jle       @@SumLoop           // }
    pop       edi
    sub       ebx, edi
    shr       ebx, 2              // min = (ebx - &GrayData.Grays[0]) / 4
    mov       [edi].TImageGrayInfo.MinValue, ebx
    sub       esi, edi
    shr       esi, 2              // max = (esi - &GrayData.Grays[0]) / 4
    mov       [edi].TImageGrayInfo.MaxValue, esi
    pop       ebx                 // count = data.Width * data.Height
    mov       [edi].TImageGrayInfo.Count, ebx
    mov       dword ptr[edi].TImageGrayInfo.Total, eax // total = edx:eax
    mov       dword ptr[edi].TImageGrayInfo.Total+4, edx
    mov       ecx, ebx
    shr       ecx, 1
    add       eax, ecx
    adc       edx, 0
    div       ebx                 // average = (total + count / 2) / count
    mov       [edi].TImageGrayInfo.Average, eax
@@Exit:                           // return GrayData.Average
    pop       ebx
    pop       edi
    pop       esi
    pop       ebp
end;

// 用色阶表替换Source像素值到Dest
procedure _DoColorLevel(var Dest: TImageData; const Source: TImageData;
  const Table: PGrayTable);
var
  height, dstOffset, srcOffset: Integer;
asm
    push    ecx
    call    _SetCopyRegs
    mov     height, edx
    mov     dstOffset, ebx
    mov     srcOffset, eax
    pop     ebx
@@yLoop:
    push    ecx
@@xLoop:
    movzx   eax, [esi].TARGBQuad.Blue
    movzx   edx, [esi].TARGBQuad.Green
    mov     al, [ebx+eax]
    mov     dl, [ebx+edx+256]
    mov     [edi].TARGBQuad.Blue, al
    mov     [edi].TARGBQuad.Green, dl
    movzx   eax, [esi].TARGBQuad.Red
    mov     al, [ebx+eax+512]
    mov     ah, [esi].TARGBQuad.Alpha
    mov     [edi].TARGBQuad.Red.Word, ax
    add     esi, 4
    add     edi, 4
    loop    @@xLoop
    pop     ecx
    add     esi, srcOffset
    add     edi, dstOffset
    dec     height
    jnz     @@yLoop
end;

// 拷贝Source到Dest
procedure _DoCopyImageData(var Dest: TImageData; const Source: TImageData);
asm
    call    _SetCopyRegs
@@yLoop:
    push    ecx
    rep     movsd
    pop     ecx
    add     esi, eax
    add     edi, ebx
    dec     edx
    jnz     @@yLoop
end;

// 如果色阶项Item参数非初始值,计算色阶表Table并返回真
function GetColorLevelTable(Item: TColorLevelItem; var Table: TGrayTable): Boolean;
var
  i, v: Integer;
  outDiff, diff: Integer;
  outCoef, coef: double;
  exponent: double;
  isMidtones: Boolean;
begin
  outDiff := Integer(Item.OutHighlight - Item.OutShadow);
  diff := Integer(Item.Highlight - Item.Shadow);
  isMidtones := (Item.Midtones <> 1.0) and not ((Item.Midtones > 9.99) or (Item.Midtones < 0.1));

  Result := ((Item.Highlight <= 255) and (diff < 255) and (diff >= 2)) or
            ((Item.OutHighlight <= 255) and (Item.OutShadow <= 255) and (outDiff < 255)) or
            isMidtones;
  if not Result then Exit;

  Coef := 255 / diff;
  outCoef := outDiff / 255;
  exponent := 1 / Item.Midtones;
  for i := 0 to 255 do
  begin
    // 计算输入色阶黑白场
    if Table[i] <= Item.Shadow then v := 0
    else
    begin
      v := Round((Table[i] - Item.Shadow) * coef);
      if v > 255 then v := 255;
    end;
    // 计算输入色阶灰场
    v := Round(Power(v / 255, exponent) * 255);
    // 计算输出色阶
    Table[i] := Round(v * outCoef + Item.OutShadow);
  end;
end;

// 如果色阶通道数据clData参数非初始值,计算所有通道色阶表并返回真
function _CheckColorLevelData(const clData: TColorLevelData;
  var Tables: array of TGrayTable): Boolean;
type
  PColorLevels = ^TColorLevels;
  TColorLevels = array[0..2] of TColorLevelItem;
var
  i, j: Integer;
begin
  Result := False;
  for i := 0 to 2 do      // 初始化R、G、B通道色阶表
  begin
    for j := 0 to 255 do
      Tables[i, j] := j;
  end;
  for i := 0 to 2 do      // 计算R、G、B通道色阶表
  begin
    if GetColorLevelTable(PColorLevels(@clData)^[i], Tables[i]) then
      Result := True;
  end;
  for i := 0 to 2 do      // 计算整个RGB图像色阶表
  begin
    if not GetColorLevelTable(clData.RGB, Tables[i]) then
      Break;
    Result := True;
  end;
end;

// 初始化色阶通道数据
procedure InitColorLevelData(var clData: TColorLevelData);

  procedure InitTColorLevelItem(var Item: TColorLevelItem);
  begin
    Item.Shadow := 0;
    Item.Midtones := 1.0;
    Item.Highlight := 255;
    Item.OutShadow := 0;
    Item.OutHighlight := 255;
  end;

begin
  InitTColorLevelItem(clData.Blue);
  InitTColorLevelItem(clData.Green);
  InitTColorLevelItem(clData.Red);
  InitTColorLevelItem(clData.RGB);
end;

// 按clData拷贝Source的色阶调整数据到Dest
procedure ImageColorLevel(var Dest: TImageData; const Source: TImageData;
  const clData: TColorLevelData);
var
  Tables: array[0..2] of TGrayTable;
begin
  if _CheckColorLevelData(clData, Tables) then
    _DoColorLevel(Dest, Source, @Tables)
  else
    _DoCopyImageData(Dest, Source);
end;

procedure TMainForm.AdjustmentImage;
begin
  if not FIsRun then
  begin
    FIsRun := True;
    FAbort := False;
    ImageColorLevel(FDest, FSource, FLevelData);
    PaintBox1Paint(nil);
    FIsRun := False;
  end
  else
    FAbort := True;
end;

procedure TMainForm.Button1Click(Sender: TObject);
begin
  Close;
end;

procedure TMainForm.ComboBox1Change(Sender: TObject);
var
  x: Integer;
begin
  FLock := True;
  x := ComboBox1.ItemIndex;
  LBar.Position := FLevelItems[x].Shadow;
  MBar.Position := 46 - Round(Ln(FLevelItems[x].Midtones * 10) * 10);
  HBar.Position := FLevelItems[x].Highlight;
  FLock := False;
  GrayMap.Invalidate;
end;

procedure TMainForm.FormCreate(Sender: TObject);
begin
  FBitmap := TGpBitmap.Create('..\..\media\source1.jpg');
  FSource := LockGpBitmap(FBitmap);
  FDest := NewImageData(FSource.Width, FSource.Height);
  InitColorLevelData(FLevelData);
  FLevelItems[0] := @FLevelData.RGB;
  FLevelItems[1] := @FLevelData.Red;
  FLevelItems[2] := @FLevelData.Green;
  FLevelItems[3] := @FLevelData.Blue;
  GetGrayInfos;
  ComboBox1.ItemIndex := 0;
  AdjustmentImage;
end;

procedure TMainForm.FormDestroy(Sender: TObject);
begin
  FreeImageData(FDest);
  UnlockGpBitmap(FBitmap, FSource);
  FBitmap.Free;
end;

procedure TMainForm.GetGrayInfos;
var
  i, j: Integer;
  maxIdx, minIdx: Integer;
begin
  for i := 1 to 3 do
  begin
    GetGrayInfo(FSource, FGrayInfos[i], True);
    for j := 0 to 255 do
      Inc(FGrayInfos[0].Grays[j], FGrayInfos[i].Grays[j]);
    Inc(Integer(FSource.Scan0), 1);
  end;
  Dec(Integer(FSource.Scan0), 3);
  maxIdx := 0;
  minIdx := 0;
  for i := 0 to 255 do
  begin
    FGrayInfos[0].Grays[i] := Round(FGrayInfos[0].Grays[i] / 3);
    Inc(FGrayInfos[0].Total, FGrayInfos[0].Grays[i] * i);
    if (FGrayInfos[0].Grays[minIdx] > FGrayInfos[0].Grays[i]) then
      minIdx := i;
    if (FGrayInfos[0].Grays[maxIdx] < FGrayInfos[0].Grays[i]) then
      maxIdx := i;
  end;
  FGrayInfos[0].MaxValue := maxIdx;
  FGrayInfos[0].MinValue := minIdx;
  FGrayInfos[0].Count := FGrayInfos[1].Count;
  FGrayInfos[0].Average := FGrayInfos[0].Total div FGrayInfos[0].Count;
end;

procedure TMainForm.GrayMapPaint(Sender: TObject);
const
  PenColor: array[0..3] of TColor = ($000000, $0000FF, $008000, $FF0000);
var
  I, v, x: Integer;
begin
  x := ComboBox1.ItemIndex;
  GrayMap.Canvas.Brush.Color := clSkyBlue;
  GrayMap.Canvas.FillRect(GrayMap.ClientRect);
  GrayMap.Canvas.Pen.Color := PenColor[x];
  for I := 0 to 255 do
  begin
    v := Round(FGrayInfos[x].Grays[i] / FGrayInfos[x].Grays[FGrayInfos[x].MaxValue] * GrayMap.Height);
    GrayMap.Canvas.MoveTo(I, GrayMap.Height);
    GrayMap.Canvas.LineTo(I, GrayMap.Height - v);
  end;
end;

procedure TMainForm.HBarChange(Sender: TObject);
begin
  HLabel.Caption := IntToStr(HBar.Position);
  if FLock then Exit;
  if HBar.Position - LBar.Position < 2 then
  begin
    FLock := True;
    HBar.Position := LBar.Position + 2;
    FLock := False;
  end;
  FLevelItems[ComboBox1.ItemIndex].Highlight := HBar.Position;
  AdjustmentImage;
end;

procedure TMainForm.LBarChange(Sender: TObject);
begin
  LLabel.Caption := IntToStr(LBar.Position);
  if FLock then Exit;
  if HBar.Position - LBar.Position < 2 then
  begin
    FLock := True;
    LBar.Position := HBar.Position - 2;
    FLock := False;
  end;
  FLevelItems[ComboBox1.ItemIndex].Shadow := LBar.Position;
  AdjustmentImage;
end;

procedure TMainForm.MBarChange(Sender: TObject);
var
  v: Single;
begin
  v := Power(Exp(1), (MBar.Max - MBar.Position) / 10.0) / 10;
  MLabel.Caption := Format('%.1f', [v]);
  if FLock then Exit;
  FLevelItems[ComboBox1.ItemIndex].Midtones := StrToFloat(MLabel.Caption);
  AdjustmentImage;
end;

procedure TMainForm.OHBarChange(Sender: TObject);
begin
  OHLabel.Caption := IntToStr(OHBar.Position);
  if FLock then Exit;
  FLevelItems[ComboBox1.ItemIndex].OutHighlight := OHBar.Position;
  AdjustmentImage;
end;

procedure TMainForm.OLBarChange(Sender: TObject);
begin
  OLLabel.Caption := IntToStr(OLBar.Position);
  if FLock then Exit;
  FLevelItems[ComboBox1.ItemIndex].OutShadow := OLBar.Position;
  AdjustmentImage;
end;

procedure TMainForm.PaintBox1Paint(Sender: TObject);
var
  dstBmp, srcBmp: TGpBitmap;
  g: TGpGraphics;
begin
  g := TGpGraphics.Create(PaintBox1.Canvas.Handle);
  dstBmp := TGpBitmap.Create(FDest.Width, FDest.Height, FDest.Stride,
    pf32bppArgb, FDest.Scan0);
  srcBmp := TGpBitmap.Create(FSource.Width, FSource.Height, FSource.Stride,
    pf32bppArgb, FSource.Scan0);
  try
    g.DrawImage(dstBmp, 0, 0);
    g.DrawImage(srcBmp, 0, FDest.Height);
  finally
    srcBmp.Free;
    dstBmp.Free;
    g.Free;
  end;
end;

end.


    下面是2张运行效果图,第一张效果图绿色通道色阶调整,第二张效果图是RGB输出色阶调整到完全颠倒时的负片图:

 

    PhotoShop中的色阶调整只用了2个滑条分别进行输入、输出色阶调整,我没有这种滑槽组件,只好用了5个滑条组件,不太美观。当然在实用时完全可写一个与PhotoShop类似的元件,并不复杂。

    说明:本文章里的灰度计算和色阶调整源代码可以在http://download.csdn.net/detail/maozefa/8323289下载,但其中的输出色阶调整和输入色阶调整一样,是不允许黑白场颠倒的,而且黑白场离差最小允许值是4而不是2,可以按本文代码修正过来。


    《Delphi图像处理》系列使用GDI+单元下载地址和说明见文章《GDI+ for VCL基础 -- GDI+ 与 VCL》。

    因水平有限,错误在所难免,欢迎指正和指导。邮箱地址:maozefa@hotmail.com

    这里可访问《Delphi图像处理 -- 文章索引》。

 



阅读更多
想对作者说点什么? 我来说一句

用OpenCV实现Photoshop色阶调整算法

2016年09月11日 109KB 下载

没有更多推荐了,返回首页

加入CSDN,享受更精准的内容推荐,与500万程序员共同成长!
关闭
关闭