Downscaling with Lanczos lazarus / 39-Graphics ---------------------------------------------------------------------- domasz · 2024-05-25 07:14 I am trying to implement Lanczos resampling. Just the simplest possible algorithm, no optimizations. Works fine when upscaling but when downscaling the resulting image is too sharp. My input image is 1500x2000 px. I am downscaling to 50x50. For each output pixel the algo finds a source pixel and its 48 neighbors (7x7 block). This means some source pixels are ignored by design (50x50 * 7x7 = 350x350 pixels of source image). What am I missing? Code: Pascal  [Select][+][-]window.onload = function(){var x1 = document.getElementById("main_content_section"); if (x1) { var x = document.getElementsByClassName("geshi");for (var i = 0; i unit Unit1; {$mode objfpc}{$H+} interface uses  Classes, SysUtils, Forms, Controls, Graphics, Dialogs, StdCtrls, Math; type  TResampleFilter = function(Value: Single): Single;  { TForm1 }   TForm1 = class(TForm)  Button1: TButton;    procedure Button1Click(Sender: TObject);  private   public   end; var  Form1: TForm1; implementation {$R *.lfm} { TForm1 } function Sinc(Value: Single): Single;begin  if Value <> 0.0 then begin    Value := Value * Pi;    Result := Sin(Value) / Value;  end  else Result := 1.0;end; function Lanczos3Filter(Value: Single): Single;begin  if Value < 0.0 then Value := -Value;   if Value < 3.0 then    Result := SinC(Value) * SinC(Value / 3.0)  else    Result := 0.0;end; function Limit(Min, Max, Val: Integer): Integer;begin  if Val < Min then Exit(Min);  if Val > Max then Exit(Max);  Exit(Val);end; function Clip(Val: Extended): Byte;begin  if Val > 255 then Exit(255);  if Val < 0 then Exit(0);  Result := Round(Val);end; function Resample(Bmp: TBitmap; AWidth, AHeight: Integer; Filter: TResampleFilter): TBitmap;var x,y: Integer;    P: TColor;    PP: array[0..3] of Byte absolute P;     i,j: Integer;    ratioX,ratioY: Extended;    SumR, SumG, SumB: Extended;    xx,yy: Extended;    xxx,yyy: Integer;    xxxx,yyyy: Integer;    Tmp: Single;    RadX, RadY: Single;begin  Result := TBitmap.Create;  Result.PixelFormat := pf32bit;  Result.SetSize(AWidth, AHeight);   ratioX := AWidth/ Bmp.Width;  ratioY := AHeight/ Bmp.Height;   for y:=0 to AHeight-1 do begin    for x:=0 to AWidth-1 do begin       xx := x / ratioX;      yy := y / ratioY;       xxx := Floor(xx);      yyy := Floor(yy);       SumR := 0;      SumG := 0;      SumB := 0;       for j:=-3 to 3 do        for i:=-3 to 3 do begin          RadX := xx - (xxx + i);          RadY := yy - (yyy + j);          Tmp := Filter(RadX) * Filter(RadY);           xxxx := Limit(0, Bmp.Width-1, xxx+i);          yyyy := Limit(0, Bmp.Height-1, yyy+j);           P := Bmp.Canvas.Pixels[xxxx, yyyy];           SumR := SumR + PP[0]*Tmp ;        SumG := SumG + PP[1]*Tmp ;          SumB := SumB + PP[2]*Tmp ;      end;       Result.Canvas.Pixels[x,y] := RGBToColor( Clip(SumR), Clip(SumG), Clip(SumB) );    end;  end;end;  procedure TForm1.Button1Click(Sender: TObject);var InB, OutB: TBitmap;begin  InB := TBitmap.Create;  InB.LoadFromFile('1.bmp'); //should be 32bpp   OutB := Resample(InB, 50, 50, @Lanczos3Filter);  OutB.SaveToFile('2.bmp');  OutB.Free;  InB.Free;end; end. ---------------------------------------------------------------------- domasz · 2024-05-25 07:29 Here are test images. 1.jpg is the source image. You should convert it to bmp. result.png is the result of the program. The image is too sharp. should.png is how an image viewer downscaled 1.bmp using Lanczos. As you can see the image is smooth. ---------------------------------------------------------------------- KodeZwerg · 2024-05-25 08:26 If a different approach is welcomed not using custom interpolation with the result you wanted: Code: Pascal  [Select][+][-]window.onload = function(){var x1 = document.getElementById("main_content_section"); if (x1) { var x = document.getElementsByClassName("geshi");for (var i = 0; i procedure AntiAliasedStretchDrawBitmap(SourceBitmap, DestBitmap: TCustomBitmap);var  DestIntfImage, SourceIntfImage: TLazIntfImage;  DestWidth, DestHeight: Integer;  DestCanvas: TLazCanvas;begin  DestWidth := DestBitmap.Width;  DestHeight := DestBitmap.Height;  DestIntfImage := TLazIntfImage.Create(0, 0);  try    DestIntfImage.LoadFromBitmap(DestBitmap.Handle, DestBitmap.MaskHandle);    DestCanvas := TLazCanvas.Create(DestIntfImage);    try      SourceIntfImage := SourceBitmap.CreateIntfImage;      try        DestCanvas.Interpolation := TFPBaseInterpolation.Create;        try          DestCanvas.StretchDraw(0, 0, DestWidth, DestHeight, SourceIntfImage);          DestBitmap.LoadFromIntfImage(DestIntfImage);        finally        DestCanvas.Interpolation.Free;        end;      finally        SourceIntfImage.Free;      end;    finally      DestCanvas.Free;    end;  finally    DestIntfImage.Free;  end;end; procedure TForm1.Button1Click(Sender: TObject);var  Source,  Dest: TBitmap;begin  Source := TBitmap.Create;  try    Source.Assign(Image2.Picture.Bitmap); // image2 got "1.jpg" assigned in designtime    Dest := TBitmap.Create;    try      Dest.SetSize(50, 50);      AntiAliasedStretchDrawBitmap(Source, Dest);      Image1.Picture.Assign(Dest);    finally      Dest.Free;    end;  finally    Source.Free;  end;end; ---------------------------------------------------------------------- domasz · 2024-05-25 09:27 Thanks for the code, KodeZwerg, might be handy but I really need to know what I did wrong to sleep well ---------------------------------------------------------------------- wp · 2024-05-25 12:11 I've never done this myself, so this answer may be nonsense. But anyway: I am not sure whether you apply the filter function correctly. You have i and j running over x and y and you calculate the filter function at the integer values -3, -2, -1, 0, 1, 2, 3 (for i and j). Now let's look at a plot of the Lanczos filter - see screenshot, where you can find that these values are always near the zero-crossings and thus close to zero, except for the center value which is 1. In total, only the center value really contributes to the sum - a complicated way of just selecting the center value in the filter interval. In my opinion you should map the intervals [-3..3] to the original image (i.e. multiply i,j by the ratio of source image width to destination image width, height accordingly), and iterate over all original pixels in this area, apply the Lanczos function to them and sum them up. As a consequence you'll notice that the program will probably become much slower (because many more pixels have to be evaluated). This is because accessing Canvas.Pixels is a very slow process. You should convert the bitmap to a TLazIntfImage, or load it into an even simpler TFPMemoryImage, where the access to individual pixels is much faster. And doing so, you might consider using the interpolation functions built into fcl-image - there's even a TLanczosInterpolation... Here is some code how to apply it: Code: Pascal  [Select][+][-]window.onload = function(){var x1 = document.getElementById("main_content_section"); if (x1) { var x = document.getElementsByClassName("geshi");for (var i = 0; i uses  fpimage, fpcanvas, fpimgCanv, fpreadjpeg, fpwritebmp, extinterpolation; procedure TForm1.Button2Click(Sender: TObject);var  srcImg, destImg: TFPMemoryImage;  canv: TFPImageCanvas;begin  // Create and load the source image  srcImg := TFPMemoryImage.Create(0, 0);  srcImg.LoadFromFile('1.jpg');   // Create the destination image in the requested size  destimg := TFPMemoryImage.Create(50, 50);   // Create a canvas for the destination image ...  canv := TFPImageCanvas.Create(destImg);   // ... and an interpolation  canv.Interpolation := TLanczosInterpolation.Create;   // Use the interpolation to stretch the source image to the new size  canv.StretchDraw(0, 0, destImg.Width, destImg.Height, srcImg);   // Save destination image to file...  destImg.SaveToFile('2a.bmp');  // ... and display it in a TImage component  Image2.Picture.Assign(destImg);   // Clean up  canv.Interpolation.Free;  canv.Free;  destImg.Free;  srcImg.Free;end; P.S. Ah, sorry, KodeZwerg, I did not read your post before sending mine because you are doing essentially the same... ---------------------------------------------------------------------- domasz · 2024-05-25 16:14 Quote from: wp on May 25, 2024, 02:11:37 pmIn my opinion you should map the intervals [-3..3] to the original image (i.e. multiply i,j by the ratio of source image width to destination image width, height accordingly), and iterate over all original pixels in this area, apply the Lanczos function to them and sum them up. Thanks, wp! ---------------------------------------------------------------------- domasz · 2024-05-26 11:22 This is a problem with today scientific articles, presentations and the alike. I have seen dozens about Lanczos interpolation but they rarely give any code, pseudocode or even describe the nuances. Just focus on comparing Lanczos to Bilinear and other filters and how to use it in a given language. Here's what I found in the book from 1992 (Graphic Gems 3): "During magnification, we are stretching our reconstructed signal, lowering its component frequencies. However, during minification, we are shrinking our reconstructed signal, raising its component frequencies, and possibly exceeding the Nyquist frequency of our new sampling rate. To create proper samples, we must eliminate all frequency components above the resampling Nyquist frequency. This can be accomplished by stretching the filter function by the image reduction factor." So basically my algo is fine for upsampling but for downsampling what wp said is right. ---------------------------------------------------------------------- runewalsh · 2024-05-26 17:03 Downscale in several steps, each 7× at most (not sure if 2× will be better or not). ---------------------------------------------------------------------- KodeZwerg · 2024-05-26 18:06 Quote from: domasz on May 26, 2024, 01:22:09 pmThis is a problem with today scientific articles, presentations and the alike. I have seen dozens about Lanczos interpolation but they rarely give any code, pseudocode or even describe the nuances. Just focus on comparing Lanczos to Bilinear and other filters and how to use it in a given language. Here's what I found in the book from 1992 (Graphic Gems 3): "During magnification, we are stretching our reconstructed signal, lowering its component frequencies. However, during minification, we are shrinking our reconstructed signal, raising its component frequencies, and possibly exceeding the Nyquist frequency of our new sampling rate. To create proper samples, we must eliminate all frequency components above the resampling Nyquist frequency. This can be accomplished by stretching the filter function by the image reduction factor." So basically my algo is fine for upsampling but for downsampling what wp said is right. Take a look at Lánczos interpolation explained, its pretty in-depth with math formulas and some demo images to understand differences at usage. For sourcecode you can take a look at ImageMagick (C Language), watch at resize, it offer a lot interpolation "filters". ---------------------------------------------------------------------- domasz · 2024-05-26 18:13 Thx, runewalsh. That might be an idea. Generally speaking - the quality of code around us is total shit. In 1992 Dale Schumacher published in Graphics Gems III (p. 8-16) his algorithm with source code (filter.c) which resamples images using Lanczos, Bilinear and a few other filters. About 95% lanczos resamplers out there are just ports of this code. In 1997-98  Anders Melander released a Delphi port (resample.pas). GraphicEx, Alcinoe, JEDI JCL, GXScene and many others all use that code (or ported filter.c themselves). Lazarus also comes with a Lanczos resampler (extinterpolation.pas) but seems it's a different algorithm. All of these claim to support also other filters (Bilinear, Nearest Neighbor, Bell, Mitchell). Just look at attachments of some quick tests... ---------------------------------------------------------------------- domasz · 2024-05-26 18:18 And here's Lazarus (extinterpolation.pas) ---------------------------------------------------------------------- domasz · 2024-05-26 18:25 And my slow code from the first post (which doesn't handle well downscaling too much). So this is why you should reinvent the wheel sometimes. ---------------------------------------------------------------------- wp · 2024-05-26 21:47 I would not swear that the fcl-image interpolation is bug-free. Some time ago I wrote a small test application which up- and downscales some test images using all built-in interpolation filters (see attachment). I have the impression that most of these filters do not support upscaling at all because they do not produce a smoothed image. And some images look really strange, such as the small Lena image upscaled to 600x600 using the Hermite filter. ---------------------------------------------------------------------- Thaddy · 2024-05-27 14:41 Isn't graphics32 an option? It has fast and has also good filters. And it works on Lazarus. ---------------------------------------------------------------------- jamie · 2024-05-27 17:44 has anyone questioned the popup doc for the canvas.Draw(.....) ? It states it calls the StretchDraw and so on, read the rest. ---------------------------------------------------------------------- dsiders · 2024-05-27 17:53 Quote from: jamie on May 27, 2024, 07:44:02 pmhas anyone questioned the popup doc for the canvas.Draw(.....) ? It states it calls the StretchDraw and so on, read the rest. What is it specifically that *you* question? Give a fellow a goal... ---------------------------------------------------------------------- jamie · 2024-05-27 18:18 it's a misleading description of what happens in the end. you call DRAW, which does not resize the image but the docs leads you to believe the image is going to be resized because it's calling the StretchDraw.  In reality, the image does not get resized at all, it just gets clipped off. in any case, if you are on windows and want to natively use StretchDraw for a faster experience for a real stretch of an image. Code: Pascal  [Select][+][-]window.onload = function(){var x1 = document.getElementById("main_content_section"); if (x1) { var x = document.getElementsByClassName("geshi");for (var i = 0; i   SetStretchBltMode(hDC,HALFTONE);                StretchBlt(hDC,630,500,200,200, hDC, 0,0,400,400, SRCCOPY); ---------------------------------------------------------------------- dsiders · 2024-05-27 18:28 Quote from: jamie on May 27, 2024, 08:18:50 pmit's a misleading description of what happens in the end. you call DRAW, which does not resize the image but the docs leads you to believe the image is going to be resized because it's calling the StretchDraw.  In reality, the image does not get resized at all, it just gets clipped off. Thank you. That I can address. ---------------------------------------------------------------------- lainz · 2024-05-27 21:11 Try with BGRABitmap: https://forum.lazarus.freepascal.org/index.php/topic,41745.msg290230.h tml#msg290230 Code: Pascal  [Select][+][-]window.onload = function(){var x1 = document.getElementById("main_content_section"); if (x1) { var x = document.getElementsByClassName("geshi");for (var i = 0; i var  bgra: TBGRABitmap;begin  bgra := TBGRABitmap.Create('file.jpg');  BGRAReplace(bgra, bgra.Resample...) end; ---------------------------------------------------------------------- dsiders · 2024-05-28 01:30 Quote from: jamie on May 27, 2024, 08:18:50 pmit's a misleading description of what happens in the end. you call DRAW, which does not resize the image but the docs leads you to believe the image is going to be resized because it's calling the StretchDraw.  In reality, the image does not get resized at all, it just gets clipped off. [/code] I am planning to patch the topic to the following: Code: Text  [Select][+][-]window.onload = function(){var x1 = document.getElementById("main_content_section"); if (x1) { var x = document.getElementsByClassName("geshi");for (var i = 0; i Draws the image content in SrcGraphic on the canvas at the specified position. Draw is reintroduced method in TCanvas; it reimplements the inherited method to use the TGraphic instance in SrcGraphic as the container with the image content drawn in the method. No actions are performed in the method if SrcGraphic has not been assigned. Draw calls the StretchDraw method to render the content in SrcGraphic on the canvas at the position specified in X and Y. The Width and Height in SrcGraphic set the bounds for the image rectangle applied to the canvas using StretchDraw. Since a stretch mode is not enabled when StretchDraw is called, the source graphic is not enlarged or shrunk to the canvas size. The source graphic is clipped if its bounds exceeds the canvas bounds. The Draw method in SrcGraphic performs the actions needed to position and rendering the content to the Pixels in the canvas. CopyMode determines the raster operation performed for the Pixels affected on the canvas. Calling Draw causes the OnChanging and OnChange events to be signalled (when assigned). OnChanging occurs prior to calling Draw in the TGraphic instance. OnChange occurs after calling Draw in the TGraphic instance.  Is that more accurate? ---------------------------------------------------------------------- jamie · 2024-05-28 10:53 Yes, that looks much better. Thank you ---------------------------------------------------------------------- dsiders · 2024-05-28 16:44 Quote from: jamie on May 28, 2024, 12:53:33 pmYes, that looks much better. Thank you Thanks for the heads-up.