Image Processing
Image Feathering  Lab Report
"Feathering" of foreground and background images


Albert Einstein Memorial Statue in Washington, DC
and my children several years ago

Purpose
The purpose of this project is to demonstrate how to merge a foreground and background image by fading from one image to the other, but only along a transition edge drawn with a simple drawing tool.  (The photography community calls this effect a vignette.)

Materials and Equipment

Software Requirements
Windows 95/98/NT/2000  [but see ExtCreatePen limitations in Windows 95/98]
Delphi 5 (to recompile)
Feathering.EXE

Hardware Requirements
Best with 800-by-600 pixel monitor with high color or true color.
Colors may not display correctly in 256-color mode.

Procedure

  1. Click on the Feathering.EXE icon to start the program.
  2. So you can experiment with the program without loading any images, the default foreground image is solid blue, while the default background image is solid red (or can be changed to a background pattern)..  A "round rectangle" around the perimeter of the foreground picture is automatically applied.  Use the green corner "handles" to move the round rectangle, or click and drag within the bounding rectangle to move it..  Experiment with the Mask settings and the resulting Feathering of the foreground and background images.
  3. Foreground Image.   Press the Read File button to read a BMP or JPG foreground image from a file (or a GIF file if TGIFImage is installed).  If there is a bitmap on the clipboard from some other program, press the Paste button to use that clipboard image as the foreground image.  The foreground image shown in Figure 1 was read from a file.   Whenever a new foreground image is displayed, a round rectangle is automatically added as the feathering mask around the perimeter of the image.

Figure 1.  Foreground image with elliptical area of interest.

  1. Press the Delete speed button eraser to delete the original round rectangle feathering mask, which was automatically selected when it was added.  Select the Ellipse tool and draw an ellipse around a region of interest in an image. Any number of objects can be drawn using the Simple Drawing Tool on the Foreground tabsheet.  Figure 2 shows two ellipses drawn on this same image.  Note that these ellipses are two pixels wide.

The width of the rectangle, round rectangle, or ellipse is used to determine the number of transition feathering bands.  There are 16 bands for a pixel width of 1.  There are 8 bands for a pixel width of 2.  There are 4 bands for a pixel width of 4.  In general, the transition area is about 16 pixels, and the number of bands is adjusted to make this true (with a minimum of 4 bands).  The line style, or the color of the line, makes no difference in creating the transition bands.  This transition feathering mask will be shown on the Mask tabsheet.

Figure 2.  Foreground image with two elliptical areas of interest.

In addition to using the mouse to stretch the corners of a selected image, press within the bounding rectangle and drag a selected object anywhere on the image.  If the Foreground tabsheet is selected, keystrokes can also be used to move the selected figures.  Press Shift-Click to select multiple drawing objects -- the object "handles" are changed to red from green when multiple objects are selected.  Press Ctrl-Arrow to move the selected figure(s), much like moving objects in the Delphi IDE.  Pressing an arrow key alone is a way to select drawing objects from the keyboard.

Once you have the desired foreground image, select the Background tabsheet.

  1. Background Image.  With the Style radiogroup box, you can select a background that is either a solid color, or a tiled bitmap.   When a Solid Color style is selected, click on the color square to change the color.  When Bitmap Tiling style is selected, a default bitmap tile is used (shown in Figure 3) until you read either a BMP or JPG  image tile from a file (or a GIF if TGIFImage is installed).  
    Alternately, a bitmap tile can also be pasted from the clipboard by selecting the Paste button.

The objects drawn on the foreground image are also drawn on the background image, but any interaction with the drawn objects must occur on the Foreground tabsheet.

Figure 3.  Background image using default bitmap tile.

Once a foreground and background image is set, you can change certain aspects of the feathering mask on the Mask tabsheet.

  1. Mask Image.  The width of the feathering band is determined when you draw an object using the Simple Drawing Tool on the foreground image.  By default the transition area is about 16 pixels, but you can increase or decrease the number of bands using the Bands SpinEdit control shown in Figure 4.  

The white areas of the mask show where the foreground image will be selected in the final image.  The black areas of the mask show where the background image will be selected in the final image.  The "feathered" transition area shows where the two image are blended in the final image.

Figure 4.  Portion of "Feathering" Mask.

When the band width is more than one pixel, sometimes the Blur checkbox will result in a more pleasant transition.

The Invert checkbox has the equivalent effect of switching foreground and background images.

  1. Feathered Image.   To save the whole feathered image to a BMP or JPG file, press the Save To File button. To save the whole feathered image to the clipboard, press the Copy button. 

If you want to crop out just a portion of the feathered image, select a "Marching Ants" bounding rectangle with the mouse as shown in Figure 5.  (Sorry, I should have implemented a way to stretch or move this marching ants rectangle.  Right now you must just redraw the rectangle if you want a new one.)

Figure 5.  Feathered Image.

You can copy and paste multiple selected areas of the feathered image.  Using the feathering masks from the two elliptical areas of interest from Figure 2, you can copy these areas (shown in Figure 6) to another application application, such as the Microsoft Photo Editor, shown in Figure 7.

Figure 6.   Feathered image from mask produced from objects drawn in Figure 2.

 

Figure 7.  Images Pasted to Photo Editor Application.

The objects drawn with the Simple Drawing Tool to form a feathering mask can overlap and form a fairly complicated area, such as the one shown in Figure 8.

Figure 8.  Multiple overlapping objects to form complex feathering mask.

One final point about the marching ants rectangle is that it can be used to flip or reverse a figure.  Normally, you'll want to draw the selection rectangle from the upper left to the lower right.  If the starting and ending points do not follow this expected order, the image may be reversed or flipped.  The cropped image will be reversed (left-to-right) if the starting point is to the right of the ending point.  Likewise, the cropped image will be flipped (top-to-bottom) if the starting point is below the ending point.

Discussion

Tabsheet updates.  Often a good, simple way to write a Windows program is to update everything whenever anything changes.  However, when the processing time for some operations is too slow, such as working with images, this may not be a good approach.  To keep good response time, and without adding too much complexity to the program, separate "update" methods were written for each of the tabsheets shown in Figure 1.  These updates were performed only when the corresponding tabsheet was displayed, or when anything on the tabsheet was changed.  Some extra flags (namely, UpdateFlagBackground and UpdateFlagMask) were necessary to make sure everything was updated when the tabsheets were selected in an arbitrary order.

Foreground Image.  The foreground image can be loaded from a file or pasted from the clipboard.  How a BMP or JPG image is loaded from a file is shown in Listing 1.

Listing 1.  Read foreground image from file.

procedure TFormFeathering.ButtonReadForegroundFileClick(Sender: TObject);
  VAR
    Picture:  TPicture;
begin
  IF   OpenPictureDialog.Execute
  THEN BEGIN
    IF   Assigned(BitmapForeground)
    THEN BitmapForeground.Free;
    BitmapForeground := TBitmap.Create;
    // Use polymorphic TPicture to load any registered file type.
    Picture := TPicture.Create;
    TRY
      Picture.LoadFromFile(OpenPictureDialog.Filename);
      // Try converting picture to a bitmap
      TRY
        BitmapForeground.Assign(Picture.Graphic)
      EXCEPT
        // Draw picture on bitmap since conversion not supported
        BitmapForeground.Width  := Picture.Graphic.Width;
        BitmapForeground.Height := Picture.Graphic.Height;
        BitmapForeground.PixelFormat := pf24bit;
        BitmapForeground.Canvas.Draw(0,0, Picture.Graphic)
      END
    FINALLY
      Picture.Free
    END;
    // In case pf8bit bitmap (or other variations) are loaded
    IF   BitmapForeground.PixelFormat <> pf24bit
    THEN BitmapForeground.PixelFormat := pf24bit;
    UpdateForeground
  END;
  // Set focus here so keyboard controls deal with
  // moving figures that are drawn on image
  PageControl.SetFocus
end;

Once the image has been loaded into a TPicture (so the polymorphic TPicture can read any registered file format), and converted to a TBitmap (BitmapForeground in this case), the UpdateForeground method is called, which is shown in Listing 2.

Listing 2.  Updates needed when foreground image changes.

PROCEDURE TFormFeathering.UpdateForeground;
    VAR
      Size:  INTEGER;
  BEGIN
    // Adjust size of TImage to same size as TBitmap to reduce flicker
    // while drawing.
    IF   BitmapForeground.Width < OriginalImageWidth
    THEN ImageForeground.Width := BitmapForeground.Width
    ELSE ImageForeground.Width := OriginalImageWidth;
    IF   BitmapForeground.Height < OriginalImageHeight
    THEN ImageForeground.Height := BitmapForeground.Height
    ELSE ImageForeground.Height := OriginalImageHeight;
    ImageForeground.Picture.Graphic := BitmapForeground;
    GraphicsList.Free;
    GraphicsList := TVectorGraphicsList.Create;
    Size := (SpinEditBands.Value+2) * (ComboBoxLineWidth.ItemIndex+1);
    PointA := Point(Size, Size);
    PointB := Point(BitmapForeground.Width  - Size,
                    BitmapForeground.Height - Size);
    // Create RoundRect node and add to image list                
    DrawingNode := TRoundRectangleNode.Create(ShapeLineColor.Brush.Color,
                                  TPenStyle(ComboBoxLineStyle.ItemIndex),
                                  ComboBoxLineWidth.ItemIndex+1 {PenWidth},
                                  PointA, PointB);
    DrawingNode.StandardizeOrder;      // Fix 25 Feb 2001
    GraphicsList.Add(DrawingNode);
    DrawingNode := NIL;
    GraphicsList.SetSelectedIndex(GraphicsList.Count - 1);
    DrawAllFigures (sfHighlightSelectedFigure);
    // Force update of background and mask
    UpdateFlagBackground := FALSE;
    UpdateFlagMask       := FALSE;
  END {UpdateForeground};

Listing 2 shows the in-memory BitmapForeground is displayed in the ImageForeground TImage.  The GraphicsList will be discussed in more detail below under Simple Drawing Tool, but briefly, a round rectangle (around the perimeter of the bitmap) DrawingNode is created and added to the GraphicsList.  With this single figure in the list, the DrawAllFigures method draws the round rectangle on top of the ImageForeground TImage.    The last two statements setting the update flags are necessary when the tabsheets are not selected in order from left to right.

[A problem was identified by John Clark that he could not select figures that had been drawn from right-to-left instead of from left-to-right.  To fix this problem, the StandardizeOrder method was added to the TVectorGraphicsNode class.  Whenever a new node is created or modified, the StandardizeOrder makes sure that point A is always the "upper left" point and that point B is always the "lower right" point.  -- efg, 25 Feb 2001.]

Simple Drawing Tool.  The VectorGraphicsNodeLibrary defines the various kinds of drawing "tools" shown along the top of the foreground image.  The base class for each of these drawing nodes is the TVectorGraphicsNode class shown in Listing 3.

Listing 3.  TVectorGraphicsNode base class.

// Each node in TVectorGraphicsList has a base class of TVectorGraphicsNode
TYPE
  TVectorGraphicsNode =
  CLASS(TObject)
    PROTECTED
      FHandleRadius:  INTEGER;
      FPenColor    :  TColor;
      FPenStyle    :  TPenStyle;
      FPenWidth    :  INTEGER;
      FPointA      :  TPoint;
      FPointB      :  TPoint;
      // Normally, the selected item is flagged TRUE.  When multiple
      // selections are allowed, more than one node can be flagged as
      // selected, however.
      FSelected   :  BOOLEAN;
    PUBLIC
      PROCEDURE DrawFigure (CONST Canvas:  TCanvas;
                            CONST Factor:  INTEGER = 0);      VIRTUAL; ABSTRACT;
      PROCEDURE Translate  (CONST TranslateVector:  TPoint);  VIRTUAL; ABSTRACT;
      PROCEDURE DrawHandles( CONST Canvas    :  TCanvas;
                             CONST PenColor  :  TColor;
                             CONST BrushColor:  TColor;
                             CONST Radius    :  INTEGER);   VIRTUAL; ABSTRACT;
      FUNCTION  GetHandleAtPoint(CONST x, y  :  INTEGER;
                                 CONST Radius:  INTEGER):  TDrawingHandle;
                                                            VIRTUAL; ABSTRACT;
      PROPERTY  PenColor   :  TColor        READ FPenColor    WRITE FPenColor;
      PROPERTY  PenStyle   :  TPenStyle     READ FPenStyle    WRITE FPenStyle;
      PROPERTY  PenWidth   :  INTEGER       READ FPenWidth    WRITE FPenWidth;
      PROPERTY  PointA     :  TPoint        READ FPointA      WRITE FPointA;
      PROPERTY  PointB     :  TPoint        READ FPointB      WRITE FPointB;
      PROPERTY  Selected   :  BOOLEAN       READ FSelected    WRITE FSelected;
  END;

When saving data to a database or a file, there is little advantage to using an object hierarchy to define the various drawing objects.  Often it's useful to just have a data field that defines the type of the object in the class definition as shown above, instead of deriving a new object for each drawing object.  A problem with the "object oriented" methodology is that only the data from an object is stored in a file or a database.  The data and the methods are stored in two separate places.

I used a TBoundingRectangle class derived from the TVectorGraphicsNode above and then derived separate "tool" classes for each of the drawing objects:  TRectangleNode, TRoundRectangleNode, and TEllipseNode.  Find all the details of this in the VectorGraphicsNodeLibrary unit.  Again, if I were saving the data to a file or a database, it is likely just as easy to derive all objects from the base class directly instead of introducing a three-level hierarchy. 

The TVectorGraphicsNodes are stored in a TVectorGraphicsList, which is derived from a TList and defined in the VectorGraphicsListLibrary unit shown in List 4. 

Listing 4.  TVectorGraphicsList class.

TYPE
  // Use TList of TVectorGraphicsNodes to store list of graphical objects.
  TVectorGraphicsList =
  CLASS(TList)
    PROTECTED
      FIndexOfSelected:  INTEGER;  // index of node in TList when only single
                                   // figure is selected
    PUBLIC
      CONSTRUCTOR Create;
      DESTRUCTOR  Destroy;  OVERRIDE;
      // Group of routines that manages "selected" flags for the figures.
      PROCEDURE SelectedFigureIncrementIndex (CONST increment:  INTEGER);
      FUNCTION  SelectedFigureCount:  INTEGER;
      FUNCTION  SelectedContainsPoint(CONST TargetPoint:  TPoint):  BOOLEAN;
      PROCEDURE DeleteSelectedFigures;
      PROCEDURE DrawAllFigures(CONST Canvas:  TCanvas);
      PROCEDURE DrawSelectedFigures(CONST Canvas:  TCanvas);
      PROCEDURE DrawBandAround(CONST Canvas:  TCanvas; CONST Factor:  INTEGER);
      FUNCTION  GetSelectedNode:  TVectorGraphicsNode;
      PROCEDURE TranslateSelectedFigures (CONST TranslateVector:  TPoint);
      PROCEDURE SelectFigures (CONST Shift:  TShiftState;
                               CONST X, Y:  Integer);
      FUNCTION  GetSelectedHandleAtPoint(CONST X,Y:  INTEGER):  TDrawingHandle;
      PROCEDURE SetSelectedFlags (CONST state:  BOOLEAN);
      PROCEDURE SetSelectedIndex (CONST index:  INTEGER);
      PROPERTY  SelectedIndex:  INTEGER  READ  FIndexOfSelected  WRITE  SetSelectedIndex;
  END;

As shown in Listing 2, a new GraphicsList with a single node is created whenever a new foreground image is defined.  A node is added to the GraphicsList on the MouseUp event after drawing an object.  The order of the nodes in the GraphicsList defines the "z" order for selecting or drawing the objects.

Selecting a drawing tool creates a new drawing node of the appropriate type, which is shown in part of the SpeedButtonToolClick method in Listing 5.  The MouseDown, MouseMove and MoveUp methods for ImageForeground also use the DrawingNode created in Listing 5.

Listing 5.  ToolClick method for Drawing SpeedButtons.

procedure TFormFeathering.SpeedButtonToolClick(Sender: TObject);
begin
  IF   Assigned(DrawingNode)
  THEN DrawingNode.Free;
  CASE (Sender AS TSpeedButton).Tag OF
    ORD(dtSelectTool):
      BEGIN
        DrawingTool := dtSelectTool;
        DrawingNode := NIL
      END;
    ORD(dtRectangleTool):
      BEGIN
        DrawingTool := dtRectangleTool;
        DrawingNode := TRectangleNode.Create(ShapeLineColor.Brush.Color,
                                  TPenStyle(ComboBoxLineStyle.ItemIndex),
                                  ComboBoxLineWidth.ItemIndex+1 {PenWidth},
                                  Point(-1,-1), Point(-1,-1)     );
      END;
   ...
end;

Find all the details of the Mouse events (such as TFormFeathering.ImageForegroundMouseDown)in the ScreenFeathering unit.  The MouseDown method defines a DrawingState variable, which is used by MouseMove and MouseUp, to determine whether a new figure is being drawn, or whether a figure is being moved (translated) to a new location.

In addition to using the mouse to manipulate the vector objects, keystrokes can also be used.  Listing 6 shows some of the details of processing the keystrokes to move the selected figure(s):

Listing 6.  Part of keystroke processing to manipulate vector objects.

// The form's KeyPreview must be set to TRUE for this to work.
procedure TFormFeathering.FormKeyDown(Sender: TObject; var Key: Word;
  Shift: TShiftState);
  VAR
    TranslateVector:  TPoint;  // "translate" vector to move selected objects
begin
  IF  PageControl.ActivePage = TabSheetForeground
  THEN BEGIN
    // We only care about keystrokes when figure(s) are selected.
    IF  GraphicsList.SelectedFigureCount > 0
    THEN BEGIN
      // Simulte clicking erase button when delete key is pressed
      IF   key = VK_DELETE
      THEN SpeedButtonEraseClick(Sender)
      ELSE BEGIN
        // Use Ctrl-shift keys to translate objects, just like Delphi's IDE
        IF   (ssCtrl IN Shift) AND
             (key IN [VK_LEFT, VK_RIGHT, VK_UP, VK_DOWN])
        THEN BEGIN
          CASE key OF
            VK_LEFT:   TranslateVector := Point(-1, 0);
            VK_RIGHT:  TranslateVector := Point( 1, 0);
            VK_UP:     TranslateVector := Point( 0,-1);
            VK_DOWN:   TranslateVector := Point( 0, 1)
          END;
          GraphicsList.TranslateSelectedFigures(TranslateVector);
          DrawAllFigures(sfHighlightSelectedFigure);
          // Make sure other controls don't see this key
          key := 0
        END
      END
    END;
...

Using the OnDrawItem for the two line-related comboboxes was more difficult than I anticipated since the default "round" endcaps for lines look simply awful.  Listing 7 shows how the line width combobox was drawn -- a similar routine was used to draw the line style combobox.

Listing 7.  OnDrawItem Method to draw Line Width ComboBox.

// The default pens in Delphi are "Cosmetic" and look awful when drawing
// short lines because of the rounded tips.  This example shows how to
// create a "Geometric" pen with square ends.
procedure TFormFeathering.ComboBoxLineWidthDrawItem(Control: TWinControl;
  Index: Integer; Rect: TRect; State: TOwnerDrawState);
 VAR
    BrushInfo:  TLogBrush;
    i1       :  INTEGER;
    i2       :  INTEGER;
begin
  WITH BrushInfo DO
  BEGIN
    lbStyle := BS_SOLID;
    lbColor := clBlack;
    lbHatch := 0
  END;
  (Control AS TComboBox).Canvas.Pen.Handle :=
    ExtCreatePen(PS_GEOMETRIC OR PS_ENDCAP_SQUARE OR PS_JOIN_MITER,
                 index+1,
                 BrushInfo, 0, NIL);
   WITH (Control AS TComboBox).Canvas DO
   BEGIN
     i1 := MulDiv(rect.Left + rect.Right, 1, 5);
     i2 := MulDiv(rect.Left + rect.Right, 4, 5);
     MoveTo(i1,  (rect.Top + rect.Bottom) DIV 2);
     LineTo(i2, (rect.Top + rect.Bottom) DIV 2)
   END
end;

See my UseNet Posting explaining why I don't think a DeleteObject call is needed above.

Thérèse Hanquet reports problems using  in Windows 95:  "...I found out that the use of ExtCreatePen is very limited when you use it with Windows 95 (apparently this applies also to Windows 98).  For example, there is no way to change the standard rounded line ends. Also, you cannot have a geometric pen with another brush than a solid brush (I had tried to use a pattern brush, in vain)."   Thérèse also gave this URL for additional information: 
http://msdn.microsoft.com/library/en-us/win32/chilimit_0ap1.asp [Thanks, Thérèse, for this information and update.  efg, 26 April 2001, updated 4 Dec 2001].

Background Image.  The UpdateBackground method, shown in Listing 8, is called whenever the background changes.

Listing 8.  Using Bitmap Tile to Create Background Image.
// Create background bitmap based on RadioGroupBackground setting
PROCEDURE TFormFeathering.UpdateBackground;
  VAR
    i:  Integer;
    j:  Integer;
BEGIN
  IF   Assigned(BitmapBackground)
  THEN BitmapBackground.Free;
  BitmapBackground := TBitmap.Create;
  BitmapBackground.Width  := BitmapForeground.Width;
  BitmapBackground.Height := BitmapForeground.Height;
  BitmapBackground.PixelFormat := pf24bit;
  CASE RadioGroupBackground.ItemIndex OF
    // fill bitmap with solid color
    0:  BEGIN
          BitmapBackground.Canvas.Brush.Color := ShapeBackground.Brush.Color;
          BitmapBackground.Canvas.FillRect(BitmapBackground.Canvas.ClipRect)
        END;
    // tile bitmap
    1:  BEGIN
          j := 0;
          WHILE j < BitmapBackground.Height DO
          BEGIN
            i := 0;
            WHILE i < BitmapBackground.Width DO
            BEGIN
              BitmapBackground.Canvas.Draw(i,j, BitmapTile);
              INC(i, BitmapTile.Width)
            END;
            INC (j, BitmapTile.Height)
          END
        END
  END;
  ImageBackground.Picture.Graphic := BitmapBackground;
  // Draw the same list of figures as on Foreground, but do
  // not show any handles here since no interaction is
  // allowed on this tabsheet with the graphics figures.
  GraphicsList.DrawAllFigures (ImageBackground.Canvas);
  UpdateFlagBackground := TRUE
END {UpdateBackground};

Feather Mask.   A drawing package would usually not need a DrawBandAround method, but this application did.  This method was added to the TVectorGraphicsList to deal with the feathering effect.  Listing 9 shows how the DrawBandAroundMethod was called whenever the feathering mask was updated.

Listing 9.  How the Feathering Mask is Created.
ROCEDURE TFormFeathering.UpdateMask;
  VAR
    grey   :  INTEGER;
    i      :  INTEGER;
    j      :  INTEGER;
    k      :  INTEGER;
    Row    :  pRGBTripleArray;
    RowLast:  pRGBTripleArray;
    RowNext:  pRGBTRipleArray;
BEGIN
  // This "IF" statement needed since processing is delayed until
  // absolutely necessary.
  IF   NOT UpdateFlagBackground
  THEN UpdateBackground;
  // Get rid of old result and prepare blank new bitmap.
  IF   Assigned(BitmapMask)
  THEN BitmapMask.Free;
  BitmapMask := TBitmap.Create;
  BitmapMask.Height := BitmapForeGround.Height;
  BitmapMask.Width  := BitmapBackground.Width;
  BitmapMask.PixelFormat := pf24bit;
  BitmapMask.Canvas.Brush.Color := clBlack;
  BitmapMask.Canvas.FillRect(BitmapMask.Canvas.ClipRect);
  // Draw several bands from black to white
  FOR k := 1 TO SpinEditBands.Value DO
  BEGIN
    grey := MulDiv(255, k, SpinEditBands.Value);  // last one is white
    BitmapMask.Canvas.Pen.Color   := RGB(grey, grey, grey);
    BitmapMask.Canvas.Brush.Color := BitmapMask.Canvas.Pen.Color;
    GraphicsList.DrawBandAround(BitmapMask.Canvas, SpinEditBands.Value-k);
  END;
  IF   CheckBoxBlur.Checked
  THEN BEGIN
    ...
  END;
  IF   CheckBoxInvert.Checked
  THEN BEGIN
    ...
  END;
  ImageMask.Picture.Graphic := BitmapMask;
  UpdateFlagMask := TRUE;
END {UpdateMask};

The GraphicsList.DrawBandAround call results in stepping though the list of vector objects and calling the appropriate DrawFigure method for each TVectorGraphicsNode.  The "layers" were added to each node by altering the class definition to have a default factor value of "0" (so all the existing calls worked just fine) -- see Listing 3 -- but allowed positive and negative factors to draw bands around the "normal" vector object.  For example, Listing 10 shows the details for the ellipse object:

Listing 10.  DrawFigure Method for TEllipseNode.

PROCEDURE TEllipseNode.DrawFigure (CONST Canvas:  TCanvas;
                                   CONST Factor:  INTEGER);
  VAR
    Delta:  TPoint;
BEGIN
  IF   Factor = 0
  THEN Canvas.Ellipse (FPointA.X, FPointA.Y, FPointB.X, FPointB.Y)
  ELSE BEGIN
    Delta := UnitDelta(FPointA, FPointB);
    Canvas.Ellipse (FPointA.X - Factor*FPenWidth*Delta.X,
                    FPointA.Y - Factor*FPenWidth*Delta.Y,
                    FPointB.X + Factor*FPenWidth*Delta.X,
                    FPointB.Y + Factor*FPenWidth*Delta.Y)
  END
END {DrawFigure};

Feathering.  Finally, with a foreground and background image, and any number of simple figures used to create the feathering mask, Listing 11 shows how to create the desired feathered image.

Listing 11.  Creating Feathered Image from Foreground, Background and Mask Bitmaps.
PROCEDURE TFormFeathering.UpdateFeathering;
  VAR
    i            :  INTEGER;
    j            :  INTEGER;
    RowBackground:  pRGBTripleArray;
    RowFeathering:  pRGBTripleArray;
    RowForeground:  pRGBTripleArray;
    RowMask      :  pRGBTripleArray;
    weight       :  INTEGER;
BEGIN
  // These "IF" statements needed since processing is delayed until
  // absolutely necessary.
  IF   NOT UpdateFlagBackground
  THEN UpdateBackground;
  IF   NOT UpdateFlagMask
  THEN UpdateMask;
  // Get rid of old result and prepare blank new bitmap.
  IF   Assigned(BitmapFeathering)
  THEN BitmapFeathering.Free;
  BitmapFeathering := TBitmap.Create;
  BitmapFeathering.Width  := BitmapForeground.Width;
  BitmapFeathering.Height := BitmapForeground.Height;
  BitmapFeathering.PixelFormat := pf24bit;
  FOR j := 0 TO BitmapForeground.Height - 1 DO
  BEGIN
    RowForeground := BitmapForeground.Scanline[j];
    RowBackground := BitmapBackground.Scanline[j];
    RowFeathering := BitmapFeathering.Scanline[j];
    RowMask       := BitmapMask.Scanline[j];
    // Normally "weight" is the same for all color planes
    FOR i := 0 TO BitmapForeground.Width - 1 DO
    BEGIN
      weight := RowMask[i].rgbtRed;
      RowFeathering[i].rgbtRed :=
        (weight      *RowForeground[i].rgbtRed +
         (255-weight)*RowBackground[i].rgbtRed) DIV 255;
      weight := RowMask[i].rgbtRed;
      RowFeathering[i].rgbtGreen :=
        (weight      *RowForeground[i].rgbtGreen +
         (255-weight)*RowBackground[i].rgbtGreen) DIV 255;
      weight := RowMask[i].rgbtRed;
      RowFeathering[i].rgbtBlue :=
        (weight      *RowForeground[i].rgbtBlue +
         (255-weight)*RowBackground[i].rgbtBlue) DIV 255;
    END
  END;
  ImageFeathering.Picture.Graphic := BitmapFeathering
END {UpdateFeathering};

Marching Ants.  Showing the selected area of the final feathered image with "Marching Ants" was much harder than I anticipated. Robert Vivrette's  wonderful "How to Draw Marching Ants" was a great resource and where I first learned about marching ants.  But putting marching ants on a Timage, or a TImage on a TTabsheet proved to be a bit more difficult.

Some time ago I created a sample project of how to use "Marching Ants" over a TImage.  I thought this would also work when the TImage was on a TTabSheet but some changes were needed.  The needed tricks included (1)  introducing a TControlCanvas and defining  MarchingAntsCanvas.Control in the FormCreate, and (2) using the TabSheetFeathering.Handle in the RemoveMarchingAnts method, as shown in Listing 12.

Listing 12.  Tricks Used for Marching Ants on TImage on TTabsheet.
TYPE
  TFormFeathering = class(TForm)
    ...
    MarchingAntsCanvas  :  TControlCanvas;
    ...
procedure TFormFeathering.FormCreate(Sender: TObject);
begin
  ...
  MarchingAntsCanvas := TControlCanvas.Create;
  MarchingAntsCanvas.Control := TControl(TabsheetFeathering);
end;
procedure TFormFeathering.FormDestroy(Sender: TObject);
begin
  ...
  MarchingAntsCanvas.Free
end;
PROCEDURE MarchingAnts(X,Y: Integer; Canvas: TCanvas); stdcall;
BEGIN
  MarchingAntsCounter := MarchingAntsCounter SHL 1; // Shift one bit left
  IF   MarchingAntsCounter = 0
  THEN MarchingAntsCounter := 1;
  IF   (MarchingAntsCounter AND $E0) > 0  // Any of the left 3 bits set?
  THEN Canvas.Pixels[X,Y] := clWhite      // Erase the pixel
  ELSE Canvas.Pixels[X,Y] := clBlack;     // Draw the pixel
end {MovingDots};
...
procedure TFormFeathering.DrawMarchingAnts;
begin
  MarchingAntsCounter := MarchingAntsCounterStart;
  // Use LineDDA to draw each of the 4 edges of the rectangle
  LineDDA(MarchingAntsPointA.X, MarchingAntsPointA.Y,
          MarchingAntsPointB.X, MarchingAntsPointA.Y,
          @MarchingAnts, LongInt(MarchingAntsCanvas));
  LineDDA(MarchingAntsPointB.X, MarchingAntsPointA.Y,
          MarchingAntsPointB.X, MarchingAntsPointB.Y,
          @MarchingAnts, LongInt(MarchingAntsCanvas));
  LineDDA(MarchingAntsPointB.X, MarchingAntsPointB.Y,
          MarchingAntsPointA.X, MarchingAntsPointB.Y,
          @MarchingAnts, LongInt(MarchingAntsCanvas));
  LineDDA(MarchingAntsPointA.X, MarchingAntsPointB.Y,
          MarchingAntsPointA.X, MarchingAntsPointA.Y,
          @MarchingAnts, LongInt(MarchingAntsCanvas));
end;
procedure TFormFeathering.RemoveMarchingAnts;
var
  R:  TRect;
begin
  R := NormalizeRect(Rect(MarchingAntsPointA.X, MarchingAntsPointA.Y,
                          MarchingAntsPointB.X, MarchingAntsPointB.Y));
  InflateRect(R,1,1);                // Make the rectangle 1 pixel larger
  InvalidateRect(TabsheetFeathering.Handle, @R, TRUE); // Mark as invalid
  InflateRect(R, -2, -2);            // Now shrink the rectangle 2 pixels
  ValidateRect(TabsheetFeathering.Handle, @R);    // Validate new rectangle
  // This leaves a 2 pixel band all the way around
  // the rectangle that will be erased and redrawn
  UpdateWindow(TabsheetFeathering.Handle);
end;
procedure TFormFeathering.ImageFeatheringMouseDown(Sender: TObject;
  Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
begin
  X := X + (Sender AS TImage).Left;
  Y := Y + (Sender AS TImage).Top;
  RemoveMarchingAnts;
  MarchingAntsPointA.X := X;
  MarchingAntsPointA.Y := Y;
  MarchingAntsPointB.X := X;
  MarchingAntsPointB.Y := Y;
  // Force mouse movement to stay within TImage
  RestrictCursorToDrawingArea( (Sender AS TImage) )
end;
...
procedure TFormFeathering.TimerMarchingAntsTimer(Sender: TObject);
begin
  // Use SHR 1 for slower moving ants
  MarchingAntsCounterStart := MarchingAntsCounterStart SHR 2;
  IF   MarchingAntsCounterStart = 0
  THEN MarchingAntsCounterStart := 128;
  DrawMarchingAnts
end;

Conclusion
Image feathering is an interesting way to merge two images together.  

This project shows a number of interesting techniques, including how to develop a Simple Drawing Tool to draw vector graphics over a TImage, and how to draw "marching ants" on a TImage on a TTabsheet.


Keywords
feathering, vignette, foreground image, background image, bitmap tiling, BMP, JPG, GIF, TGIFImage, TPicture, JPG to BMP conversion, BMP to JPG conversion, JPEG Quality, KeyPreview, rubber band line, MouseDown, MouseMove, MouseUp, object translation, ClientToScreen, ScreenToClient, Simple Drawing Tool, TList, TVectorGraphicsList, TVectorGraphicsNode, TRectangleNode, TRoundRectangleNode, TEllipseNode, TDrawingTool, TDrawingHandle,  object "handles", RestrictCursorToDrawingArea, Windows.ClipCursor, SquareContainsPoint, Windows.PtInRect, Marching Ants, LineDDA, TPenStyle, TLogBrush, Geometric Pens, ExtCreatePen, PS_GEOMETRIC, PS_ENDCAP_SQUARE, TCombBox, OnDrawItem, Scanline, TRGBTripleArray, blur, invert, Clipboard, CF_BITMAP, Clipboard.HasFormat, TControlCanvas, cropping, flip, reverse

References
Feathering a Selection:  Faded Edges Tutorial (Paint Shop Pro)
www.dumlao.cc/psptutorials/feather.htm 

Feathering (Adobe Photoshop)
www.arraich.com/ref/feathering.htm 


Download 
Delphi 5 Source and EXE (316 KB):  Feathering.ZIP

To use GIF images for the foreground image or the background tile:

  1. Download Anders Melander's TGIFImage.

  2. Change default NoGIF conditional compilation variable to GIF:  Project | Options | Directories/Conditionals | Conditionals | GIF

  3. Recompile the Feathering program.

Delphi 4 version by Allan Rowe.


Thanks to Gerhard Venter for asking how image feathering works.
Thanks to John Clark for identifying a bug in selecting areas when drawn right-to-left.


Updated 14 Jun 2009
Since 5 Jan 2001