Fast crop .png images in Delphi - delphi

I don't know what to do here anymore, so I hope that somebody can help me.
I'm using Delphi 10.4 and Windows 10.
Basically, my problem is that cutting a part of the .png image with transparent background is to slow. I use scanline.
I have one background image (back.bmp) that is drawn on the form. That image can be also a .png (with no transparency) if that can help to solve this.
From the second image (frontsigns.bmp) I cat different parts and need to draw them to that background.
Old version of this program used .bmp as second image (with no transparent background) so that was very fast.
procedure TfrmMain.btnDrawBMPClick(Sender: TObject);
var
frontsigns : TBitmap;
begin
frontsigns := TBitmap.Create;
frontsigns.LoadFromFile('E:\frontsigns.bmp');
frmMain.Canvas.CopyRect(Rect(0,0,302,869), frontsigns.Canvas, Rect(0, yStartPos, 302, yEndPos)); // yStartPos and yEndPos are variables
end;
This draw part of the second image (303x870 px) on the background in the 0.415 ms. That is OK (probably can't be faster).
Now I need to use a second image with transparent backgrounds, so I use .png. Because I cut and draw different parts of the second image on the background my idea is that I use temp background image and draw part of the .png on that temp image and after that I draw it on the form.
Here is the code.
procedure TfrmMain.btnDrawBMPClick(Sender: TObject);
var
background, tmpbackground : TBitmap;
frontsigns, CroppedPng : TPngImage;
begin
background := TBitmap.Create;
background.LoadFromFile('E:\back.bmp');
frontsigns := TPngImage.Create;
frontsigns.LoadFromFile('E:\frontsigns.png');
tmpbackground := TBitmap.Create(303, 870);
tmpbackground.Canvas.CopyRect(Rect(0, 0, 302, 869), background.Canvas, Rect(0, 0, 302, 869));
CropPng(frontsigns, 0, yStartPos, 302, yEndPos, CroppedPng); // yStartPos and yEndPos are variables
tmpbackground.Canvas.Draw(0, 0, CroppedPng);
end;
This draw part of the second image (303x870 px) on the background in the 13.5 ms!!!!!!!
Reason is slow scanline I think. I should write here that frontsigns.png has only fully transparent background. There are not any semi-transparent pixels.
Here is my code for cropping .png images.
const
ColorTabMax = 10;
ColorTab : array[0..ColorTabMax-1] of TColor =
(ClBlack, ClMaroon, ClRed, ClWebDarkOrange, ClYellow, ClGreen, ClBlue, ClPurple, ClGray, ClWhite);
procedure CropPng(Source: TPngImage; Left, Top, Width, Height: Integer; out Target : TPngImage);
function ColorToTriple(Color: TColor): TRGBTriple;
begin
Color := ColorToRGB(Color);
Result.rgbtBlue := Color shr 16 and $FF;
Result.rgbtGreen := Color shr 8 and $FF;
Result.rgbtRed := Color and $FF;
end;
var
X, Y : Integer;
Bitmap : TBitmap;
BitmapLine : PRGBLine;
AlphaLineA, AlphaLineB : pngImage.PByteArray;
begin
if (Source.Width < (Left + Width)) or (Source.Height < (Top + Height)) then
raise Exception.Create('Invalid position/size');
Bitmap := TBitmap.Create;
try
Bitmap.Width := Width;
Bitmap.Height := Height;
Bitmap.PixelFormat := pf24bit;
for Y := 0 to Bitmap.Height - 1 do
begin
BitmapLine := Bitmap.Scanline[Y];
for X := 0 to Bitmap.Width - 1 do
BitmapLine^[X] := ColorToTriple(Source.Pixels[Left + X, Top + Y]);
end;
Target := TPngImage.Create;
Target.Assign(Bitmap);
if Source.Header.ColorType in [COLOR_GRAYSCALEALPHA, COLOR_RGBALPHA] then
begin
Target.CreateAlpha;
for Y := 0 to Target.Height - 1 do
begin
AlphaLineA := Source.AlphaScanline[Top + Y];
AlphaLineB := Target.AlphaScanline[Y];
for X := 0 to Target.Width - 1 do
AlphaLineB^[X] := AlphaLineA^[X + Left];
end;
end;
finally
Bitmap.Free;
end;
end;
I'm open for any ideas here. Can I make scanline works fatser? I don't have semi-transparent pixels so maybe I don't need to do all this.
I've tried with 32bit .bmp images with alpha channel, but haven't made it work with alphablend function.
I'me even open for third party libraries if there is no otehr option.
Thanks.....

In library PngComponents unit PngFunctions offers procedure SlicePNG, which allows to split a TPngImage into separate parts of equal size. As this has to be done only once it may significantly reduce the drawing time.

The problem with your approach is that you are reading your source image by accessing individual pixels using Source.Pixels and not using ScanLine
BitmapLine^[X] := ColorToTriple(Source.Pixels[Left + X, Top + Y]);
If you want to benefit properly by using ScanLine make sure you use ScanLine for both source and target images.
Also since your source and target images are both TPngImage you probably don't even need to create the temporary TBitmap.
And if color palettes of your PNG's match then you don't even need to do any color decoding/encoding but instead just copy data directly from one image to another. Of course you do need to make sure that color palette in your PNG's match each other in advance.
I remember reading about a tool that modifies a PNG's palette information to match with other files some years ago. Unfortunately I don't remember its name. I do remember reading about it in an article about creating of PNG based image atlases for games.

Here is my current progress thank you to the SilverWariors answer. I've just implemented first tip for now.
I was using information from:
https://delphi.cjcsoft.net/viewthread.php?tid=48996
https://en.wikipedia.org/wiki/BMP_file_format
I've replaced:
BitmapLine^[X] := ColorToTriple(Source.Pixels[Left + X, Top + Y]);
with:
BitmapLine^[X] := GetPixel(source, Left + X, Top + Y);
GetPixel function is bellow.
function GetPixel(Source: TPngImage; X, Y: Integer): TRGBTriple;
var
LineSource : pngImage.PByteArray;
begin
LineSource := Source.Scanline[y];
// Get blue value - stored in lowest order byte in a TColor
Result.rgbtBlue := PByteArray(LineSource)^[(x*3)+0];
// Get Green value - second lowest byte in TColor
Result.rgbtGreen := PByteArray(LineSource)^[(x*3)+1];
// Get Red value - third lowest byte in TColor
Result.rgbtRed := PByteArray(LineSource)^[(x*3)+2];
end;
I'm not sure why the color order is like this and not like in the article on the link above. Maybe because .png file is 32bit.
With this change I've decreased time from 13.5 ms to 6.44 ms. That is great, but I think it can be even much better.
Here is where is I see potential improvement.
Now I scan every line two times. One for the RGB colors and one for for ALPA information.
AlphaLineA := Source.AlphaScanline[Top + Y];
I think that I can get ALPHA info from scanline if I scanline returns all four bytes in a 32bit image. I'm I correct?
Maybe something like:
PByteArray(LineSource)^[(x*3)+3];
Another idea is that I can directly write to the final background. Now I cut part of the .png image and draw it on the background at the end. I must use draw because transparency that .png image that I got as result of croping original image will be lost if I use CopyRect.
But If I draw pixels directly to the background (that has ALPHA 255) that would be much faster. Maybe I can avoid that because the background is 32bit .bmp (it can be 32bit .png) without any transparency (ALPHA is 255 for all bits). Also ALPHA for .png that I'm cutting of can be only 255 (not transparent) and 0 (fully transparent).
I'm not sure how I can accomplish this.

Related

Applying transformations to Bitmap before calling Canvas.DrawBitmap (Firemonkey)

Good afternoon.
I'm working on a drawing program that allows the user to drag and drop TImages loaded with a Bitmap over a canvas. (In a Firemonkey HD application in RAD Studio XE2) The user can then change x and y scales and rotation before saving the image.
All TImages are kept in a list and this list is then written to the underlying canvas using this simple procedure:
for i := 0 to DroppedList.Count - 1 do
begin
AImage := DroppedList[i];
SourceRect.Left := 0;
SourceRect.Right := AImage.Bitmap.Width;
SourceRect.Top := 0;
Sourcerect.Bottom := AImage.Bitmap.Height;
TargetRect.Left := AImage.Position.X;
TargetRect.Right := AImage.Position.X + AImage.Bitmap.Width;
TargetRect.Top := AImage.Position.Y;
TargetRect.Bottom := AImage.Position.Y + AImage.Bitmap.Height;
with FImage.Bitmap do
begin
Canvas.BeginScene;
Canvas.DrawBitmap(AImage.Bitmap, SourceRect, TargetRect, 1, True);
Canvas.EndScene;
BitmapChanged
end;
end;
FImage.Bitmap.SaveToFile('test.bmp');
The problem with this is that transformations to the scale and rotation of the images that are visible in the window are not taken into account by DrawBitmap, and are lost when saving.
I am looking for a way to apply the transformations to the bitmap before drawing it to the background.
I was unable to find any info on this, so i was hoping someone here could help.
Thank you,
Daniël
the problem seems to be that the Scaling and the Rotation are applyed to a source TImage. In this "source TImage", the transformations are not done to the bitmap but rather at the TImage level (beause it's a TControl and as all TControl they can be scaled and rotated). Later you copy the source Bitmap elsewhere, but actually this Bitmap has never changed.
So would have to rotate and scale the bitmap in the loop, according to the settings in the source TImage:
with FImage.Bitmap do
begin
Canvas.BeginScene;
LBmp := TBitmap.Create;
try
// create a copy on which transformations will be applyed
LBmp.Assign(AImage.Bitmap);
// rotate the local bmp copy according to the source TImage.
if AImage.RotationAngle <> 0 then
LBmp.Rotate( AImage.RotationAngle);
// scale the local bmp copy...
If AImage.Scale.X <> 1
then ;
Canvas.DrawBitmap(LBmp, SourceRect, TargetRect, 1, True);
finally
LBmp.Free;
Canvas.EndScene;
BitmapChanged
end;
end;
This simple code sample explains well the problem. For example, RotatationAngle is a property of AImage and not of AImage.Bitmap.
A workaround that would avoid to implement the transformations would be to use TControl.MakeScreenshot(). (to be verified, this coulds fail)
with FImage.Bitmap do
begin
Canvas.BeginScene;
LBmpInclTranformations := AImage.MakeScreenShot;
Canvas.DrawBitmap(LBmpInclTranformations, SourceRect, TargetRect, 1, True);
Canvas.EndScene;
BitmapChanged
end;

How to load a transparent Image from ImageList?

I want to load a picture (32 bit-depth, transparent) from a TImageList to an TImage. The standard approach would be ImageList.GetBitmap(Index, Image.Picture.Bitmap);. However the GetBitmap method doesn't work with transparency, so I always get a non-transparent bitmap.
The workaround is rather simple - ImageList offers another method, GetIcon, which works OK with transparency. Code to load a transparent Image would be:
ImageList.GetIcon(Index, Image.Picture.Icon);
And don't forget to set proper ImageList properties:
ImageList.ColorDepth:=cd32bit;
ImageList.DrawingStyle:=dsTransparent;
I too have had various issues with passing in images from the a tImageList. So I have a simple wrapper routine that generally does the job and it enforces the transparency. The code below is Delphi 2005 and imlActiveView is the tImageList component that has my set of button glyph images.
procedure TfrmForm.LoadBitmap (Number : integer; bmp : tBitMap);
var
ActiveBitmap : TBitMap;
begin
ActiveBitmap := TBitMap.Create;
try
imlActiveView.GetBitmap (Number, ActiveBitmap);
bmp.Transparent := true;
bmp.Height := ActiveBitmap.Height;
bmp.Width := ActiveBitmap.Width;
bmp.Canvas.Draw (0, 0, ActiveBitmap);
finally
ActiveBitmap.Free;
end
end;
Here is an example of use where the 5th imlActiveView image is passed into the btnNavigate.Glyph.
LoadBitmap (5, btnNavigate.Glyph)

CopyRect (scaling) with the correct colors in Delphi

In this question I asked about the correct use of the CopyRect method. I got an answer which fixed my problem, but now the colors of the copied rectangle are wrong (limited to 256 values?).
This is the code:
var
Bmp: TBitmap;
begin
Image1.Picture.LoadFromFile(SomeJPGimage);
Bmp := TBitmap.Create;
try
Bmp.Assign(Image1.Picture.Graphic);
with Bmp do
Image2.Canvas.CopyRect(Image2.Canvas.ClipRect, Canvas, Canvas.ClipRect);
finally
Bmp.Free;
end;
end;
The inset with the false colors is Image2. The colors are right if I don't resize.
How do I get the 24 bit color of the source image (a JPG) when resizing?
edit
Draw is not an alternative; I want to copy a scaled version of part of the source image.
This is not caused because of color reduction, or a wrong pixelformat etc.. You're probably shrinking the image while copying and 'StretchBlt' compresses the image to fit in, and depending on the mode, produces some artifacts. For instance the below 128x128 image    
is displayed exactly the same if no resizing is applied. However if it is applied on a 90x100 image for instance, the output is   .
You can change the stretching mode for a slightly better result:
var
Bmp: TBitmap;
begin
Image1.Picture.LoadFromFile(SomeJPGimage);
Bmp := TBitmap.Create;
try
Bmp.Assign(Image1.Picture.Graphic);
SetStretchBltMode(Image2.Canvas.Handle, HALFTONE); // <- here
with Bmp do
Image2.Canvas.CopyRect(Image2.Canvas.ClipRect, Canvas, Canvas.ClipRect);
finally
Bmp.Free;
end;
end;
For the above source picture the output now becomes:
(Having browsed a little 'graphics.pas', the VCL seems to be using halftone only for 8-bit images. I may be wrong or right in this assessment, but in any case halftone stretching mode has no such constraint.)
For anything better, I believe, you have to use a proper graphics library.
Edited again:
Turns out the issue is going against the WRONG canvas (too easy with TImage if you're not used to it). Tried to save files on my last sample and got a huge file on the one I assigned. So I Started looking into some of the other values and found that you need to work against the Bitmap Canvas...
var
BMP: TBitmap;
MyClipRect: TRect;
begin
if OpenDialog1.Execute then
begin
Image1.Picture.LoadFromFile(OpenDialog1.FileName);
Bmp := TBitmap.Create;
try
Bmp.Assign(Image1.Picture.Graphic);
myClipRect.Left := (Bmp.Width div 2);
myClipRect.Top := (Bmp.Height div 2);
myClipRect.Right := (Bmp.Width);
myClipRect.Bottom := (Bmp.Height);
with Image2.Picture.Bitmap do
begin
Width := Bmp.Width div 2;
Height := Bmp.Height div 2;
Canvas.CopyRect(Canvas.ClipRect, Bmp.Canvas, MyClipRect);
end;
Image2.Picture.SaveToFile('image2.bmp');
finally
Bmp.Free;
end;
end;
end;
Hope that finally got it. Yeesh.

How to check if the PNG image loaded into FMX.TBitmap has an alpha channel?

I'm loading PNG images into FMX.Type.TBitmap in Delphi-XE2 Update3 FireMonkey HD application. How do I check if loaded PNG image has an alpha channel or not?
Currently if I load an image with an alpha channel it has alpha info in Bitmap.Scanline[Y]^[X] in a form of $AABBGGRR. However if I load PNG image without alpha the said record has only $00BBGGRR entries (AA = 0), just like an image with clear alpha. Hence the problem - how to determine if it is RGBA image with the alpha fully transparent or it is a RGB image (in this case I will process it to make the alpha fully opaque). Note: Checking through all pixels is not an option.
FMX TBitmap has no PixelFormat property, nor I could find HasAlpha flag.
You're probably not going to like this.
All bitmaps in FMX are 32-bit, and they are loaded and saved using code from the OS, which is all 32-bit.
So, the real answer is that all bitmaps have an alpha channel.
But, what you really want to know is whether the bitmap uses the alpha channel, and the only way to tell this would be to iterate over every pixel and see if any have an alpha channel which is <> 255.
I would recommmend something like the following (untested):
function TBitmap.IsAlpha(Bitmap: TBitmap): Boolean;
var
I, j: Integer;
Bits: PAlphaColorRecArray;
begin
Bits := PAlphaColorRecArray(StartLine);
for j := 0 to Height - 1 do
for I := 0 to Width - 1 do
begin
if Bits[I + (j * Width)].A <> 255 then
begin
Result := True;
EXIT;
end;
end;
Result := False;
end;
Following function checks if a PNG file has a transparency channel. This is easy, since the main PNG header has a fixed length and the data information block IHDR must be the first occurring block.
function PngHasAlphaLayer(f: String): Boolean;
var
fs: TFileStream;
colorType: Byte;
begin
fs := TFileStream.Create(f, fmOpenRead);
fs.Position := 25;
fs.Read(colorType, 1);
fs.Free;
Result := colorType and (1 shl 2) <> 0;
end;
So it is stored in the 26th byte (or 0x19 as hex), in the 3rd bit.
However, this function does not check for a valid file structure for simplicity reasons. So it should be used after the PNG image has been loaded to TBitmap and then its boolean value for transparency support could be stored e. g. in the Tag property of TImage (or wherever you want).

How to Read Pixels in WIC image

The code below (from this thread: How to use Delphi 2010's new WIC capability on Canon files?) opens a WIC image into a BitMap.
However, if the dynamic range of WIC pixel values is large, this code loses a lot of information, since it has to scale the wide dynamic range into the low range that a Bitmap pixel can accommodate.
procedure TForm116.Button1Click(Sender: TObject);
var
WIC: TWICImage;
begin
WIC := TWICImage.Create;
try
WIC.LoadFromFile('MyFilename.raw');
Image1.Picture.Graphic.Assign(WIC);
finally
WIC.Free;
end;
end;
Can anyone show me sample code that would let me read the pixel values directly from the TWICImage, so I can access the image data without losing information? I need the intensity (gray scale) values of each pixel which perhaps can be calculated from the RGB values if not directly available?
Something like:
var
PixelValue: Integer; // Grayscale
for Row := 0 to WIC.Width do
for Col := 0 to WIC.Height
PixelValue := WIC.GetPixelValue(Row, Col);
You should be able to assign() the TWICImage to a TBitmap, and then access the bitmap's Pixels or ScanLine property to get at the pixel data. Much like you'd do when loading a JPEG.
For example (untested pseudocode)
w := TWICImage.Create();
w.LoadFromFile(...)
b := TBitmap.Create;
b.assign( w);
ptr := b.ScanLine[...]

Resources