Recent

Author Topic: My hobby project (image-to-character)  (Read 4454 times)

anyone

  • Guest
My hobby project (image-to-character)
« on: October 04, 2020, 06:32:52 pm »
Hello everybody. I have a hobby project that I am reluctant to upload to GitHub or make an entry in Free Pascal wiki page.

Anyway, thanks to the fcl-image example code in FPC folder, I managed to do conversion of image (JPEG in this case) to 16-color character in text mode. I did it in C# .NET before but now I am glad that I am able to port it to Free Pascal.

But I wish to know:
1) How to support reading of image types without defining the Reader (JPEG, BMP, GIF, PNG...)
2) Is there any built-in support for color quantization from RGB 24-bit down to 16 color?

Actually for the second question, I had my own algorithm (written 20 years ago), but just want to know if Free Pascal libraries have any.

Your feedback or criticism is welcomed.


UPDATE (as of 5-Oct-2020 6:55PM UTC+2): A big "Thank You" to @wp for his /her kind effort  :-* to fix my "img2chr.pp" program so that it now supports multiple image file format regardless of the incorrect file extension.

Code: Pascal  [Select][+][-]
  1. {$mode objfpc}{$h+}
  2. uses fpreadgif, fpreadpng, fpreadbmp, fpreadjpeg, fpimage, classes, sysutils, Crt;
  3.  
  4. var img : TFPMemoryImage;
  5.     ReadFile : string;
  6.     pixel : TFPColor;
  7.     x, y:integer;
  8.     ratioX, ratioY:integer;
  9.  
  10. function SimplifyColorComponent(Value:byte):byte;
  11. begin
  12.  if Value>=52 then
  13.    SimplifyColorComponent:=63
  14.  else
  15.  if Value>=32 then
  16.    SimplifyColorComponent:=42
  17.  else
  18.  if Value>=12 then
  19.    SimplifyColorComponent:=21
  20.  else
  21.    SimplifyColorComponent:=0;
  22. end;
  23.  
  24. function DecreaseColor256(Red,Green,Blue:byte):byte;
  25. const
  26.  Palette16:array [0..15,1..3] of byte=((0,0,0),(0,0,42),(0,42,0),(0,42,42),
  27.                                        (42,0,0),(42,0,42),(42,42,0),(42,42,42),
  28.                                        (0,0,21),(0,0,63),(0,42,21),(0,42,63),
  29.                                        (42,0,21),(42,0,63),(42,42,21),(42,42,63));
  30. var
  31.  Color,Component,Value:byte;
  32.  NewRed,NewGreen,NewBlue:byte;
  33.  
  34. begin
  35.  DecreaseColor256:=0;
  36.  Component:=1;
  37.  
  38.  repeat
  39.    case Component of
  40.      1:Value:=SimplifyColorComponent(Red div 4);
  41.      2:Value:=SimplifyColorComponent(Green div 4);
  42.      3:Value:=SimplifyColorComponent(Blue div 4);
  43.    end;
  44.  
  45.    Color:=0;
  46.  
  47.    while Value<>Palette16[Color,Component] do
  48.    begin
  49.      Inc(Color);
  50.  
  51.      if Color>15 then
  52.      begin
  53.        Dec(Value,21);
  54.        Color:=0;
  55.      end;
  56.    end;
  57.  
  58.    case Component of
  59.      1:NewRed:=Value;
  60.      2:NewGreen:=Value;
  61.      3:NewBlue:=Value;
  62.    end;
  63.  
  64.    Inc(Component);
  65.  
  66.  until Component>3;
  67.  
  68.  for Color:=0 to 15 do
  69.    if (Palette16[Color,1]=NewRed) and (Palette16[Color,2]=NewGreen)
  70.    and (Palette16[Color,3]=NewBlue) then
  71.    begin
  72.      DecreaseColor256:=Color;
  73.      Exit;
  74.    end;
  75. end;
  76.  
  77. procedure Init;
  78. begin
  79.   ReadFile := ParamStr(1);
  80.   img := TFPMemoryImage.Create(0,0);
  81. end;
  82.  
  83. procedure ReadImage;
  84. var
  85.   color16, r, g, b:byte;
  86.   readerClass: TFPCustomImageReaderClass;
  87.   reader: TFPCustomImageReader;
  88.   stream: TStream;
  89. begin
  90.    if FileExists(ReadFile) then
  91.    begin
  92.      stream := TFileStream.Create(ReadFile, fmOpenRead + fmShareDenyNone);
  93.      try
  94.        stream.Position := 0;
  95.        readerClass := TFPCustomImage.FindHandlerFromStream(stream).Reader;
  96.        if Assigned(readerClass) then begin
  97.          reader := readerClass.Create;
  98.          try
  99.            img.LoadFromStream(stream, reader);
  100.          finally
  101.            reader.Free;
  102.          end;
  103.  
  104.          ratioY := img.Height div 30;
  105.          ratioX := img.Width div 90;
  106.  
  107.          y:=0;
  108.          x:=0;
  109.  
  110.          while (y<img.Height) do
  111.          begin
  112.            while (x<img.Width) do
  113.            begin
  114.              pixel := img.Colors[x,y];
  115.              r := (pixel.Red shr 8) and $00ff;
  116.              g := (pixel.Green shr 8) and $00ff;
  117.              b := (pixel.Blue shr 8) and $00ff;
  118.              color16:=DecreaseColor256(r, g, b);
  119.  
  120.              TextBackground(color16);
  121.              Write(' ');
  122.  
  123.              x:=x+ratioX;
  124.            end;
  125.  
  126.            WriteLn;
  127.            x:=0;
  128.            y:=y+ratioY;
  129.          end;
  130.        end else
  131.          WriteLn('Unknown image format.');
  132.      finally
  133.        stream.Free;
  134.      end;
  135.    end
  136.    else
  137.       WriteLn (ReadFile,' file not found.');
  138. end;
  139.  
  140. procedure Dispose;
  141. begin
  142.   Img.Free;
  143. end;
  144.  
  145. begin
  146.   if (ParamCount=0) then
  147.   begin
  148.     WriteLn('img2chr ,developed by bookhanming@outlook.my');
  149.     WriteLn;
  150.     WriteLn('img2chr <image filename>');
  151.   end
  152.   else
  153.     try
  154.       Init;
  155.       ReadImage;
  156.       Dispose;
  157.     except
  158.       on e : exception do
  159.         writeln ('Error: ',e.message);
  160.     end;
  161.  
  162.   ReadLn;
  163. end.

Example screen shot is attached together. (Left: output of this img2chr program using .... Right: original Panda.jpg photo)
« Last Edit: October 05, 2020, 06:58:20 pm by anyone »

MarkMLl

  • Hero Member
  • *****
  • Posts: 6676
Re: My hobby project (image-to-character)
« Reply #1 on: October 04, 2020, 09:59:36 pm »
1) How to support reading of image types without defining the Reader (JPEG, BMP, GIF, PNG...)

I think you'd have to open it as a binary file and look to see if the first few bytes match a known fingerprint. On unix there is a program called  file  which uses (or at least used to use) what is known as the "magic database", that contains fingerprints for a large number of files based on (a) bytes at a known location relative to the start (i.e. in a header) and (b) bytes whose location in the file is given by indirection via a header field. Obviously you don't need that amount of flexibility for a program handling a limited number of filetypes.

It looks as though these days the magic database doesn't come as a simple text format, but https://en.wikipedia.org/wiki/List_of_file_signatures is a good alternative.

MarkMLl


MT+86 & Turbo Pascal v1 on CCP/M-86, multitasking with LAN & graphics in 128Kb.
Pet hate: people who boast about the size and sophistication of their computer.
GitHub repositories: https://github.com/MarkMLl?tab=repositories

Handoko

  • Hero Member
  • *****
  • Posts: 5131
  • My goal: build my own game engine using Lazarus
Re: My hobby project (image-to-character)
« Reply #2 on: October 05, 2020, 04:36:59 am »
1) How to support reading of image types without defining the Reader (JPEG, BMP, GIF, PNG...)

I think there is no simple solution for it. But I will solve the problem like this:

Code: Pascal  [Select][+][-]
  1. var
  2.   Reader : TFPCustomImageReader;
  3.   FileName: string;
  4.   Sig: array of char;
  5.   S: string;
  6. begin
  7.  
  8.   if ParamCount <= 0 then
  9.   begin
  10.     WriteLn('Usage: img2chr <image filename>');
  11.     Halt;
  12.   end;
  13.  
  14.   Filename := ParamStr(1);
  15.   Reader := nil;
  16.  
  17.   // Open based on file extension
  18.   S :=  ExtractFileExt(Filename.ToLower);
  19.   if S = '.jpg' then TryOpenAsJPG(Reader, Filename)
  20.   else
  21.     if S = '.png' then TryOpenAsPNG(Reader, Filename)
  22.     else
  23.       if S = '.bmp' then TryOpenAsBMP(Reader, Filename)
  24.       else
  25.         if S = '.gif' then TryOpenAsGIF(Reader, Filename));
  26.  
  27.   // Open based on magic number
  28.   if not(Assigned(Reader) then
  29.   begin
  30.     Sig := ReadFileSignature(Filename);
  31.     If SignatureIsJPG then TryOpenAsJPG(Reader, Filename)
  32.     else
  33.       if SignatureIsPNG then TryOpenAsPNG(Reader, Filename)
  34.       else
  35.         if SignatureIsBMP then TryOpenAsBMP(Reader, Filename)
  36.         else
  37.           if SignatureIsGIF then TryOpenAsGIF(Reader, Filename)
  38.   end;
  39.  
  40.   if not(Assgined(Reader)) then
  41.   begin
  42.     WriteLn('Unrecognizable file format.');
  43.     Halt;
  44.   end;
  45.  
  46. end;

You will need write all the TryOpenAs* and SignatureIs* and ReadFileSignature functions.
« Last Edit: October 05, 2020, 04:43:03 am by Handoko »

PascalDragon

  • Hero Member
  • *****
  • Posts: 5446
  • Compiler Developer
Re: My hobby project (image-to-character)
« Reply #3 on: October 05, 2020, 09:17:08 am »
All fpReadXXX functions register their reader in the global ImageHandlers of unit fpImage and TFPCustomImage makes use of that. So simply doing a TFPMemoryImage.LoadFromFile without any reader should work already.

Fred vS

  • Hero Member
  • *****
  • Posts: 3158
    • StrumPract is the musicians best friend
Re: My hobby project (image-to-character)
« Reply #4 on: October 05, 2020, 10:42:32 am »
Hello.


You can detect the format with BGRABitmap using DetectFileFormat functions of BGRABitmapTypes unit.

  {** Detect the file format of a given file }
 
Code: Pascal  [Select][+][-]
  1. function DetectFileFormat(AFilenameUTF8: string): TBGRAImageFormat

  {** Detect the file format of a given stream. ''ASuggestedExtensionUTF8'' can be provided to guess the format }
 
Code: Pascal  [Select][+][-]
  1.  function DetectFileFormat(AStream: TStream; ASuggestedExtensionUTF8: string = ''): BGRAImageFormat;

  {** Returns the file format that is most likely to be stored in the given filename (according to its extension) }
Code: Pascal  [Select][+][-]
  1.   function SuggestImageFormat(AFilenameOrExtensionUTF8: string): TBGRAImageFormat;

  {** Returns a likely image extension for the format }
 
Code: Pascal  [Select][+][-]
  1. function SuggestImageExtension(AFormat: TBGRAImageFormat): string;

  {** Create an image reader for the given format }
Code: Pascal  [Select][+][-]
  1.   function CreateBGRAImageReader(AFormat: TBGRAImageFormat): FPCustomImageReader;

  {** Create an image writer for the given format. ''AHasTransparentPixels'' specifies if alpha channel must be supported }
 
Code: Pascal  [Select][+][-]
  1.  function CreateBGRAImageWriter(AFormat: TBGRAImageFormat; AHasTransparentPixels: boolean): TFPCustomImageWriter;

Fre;D

PS: Thanks to Circular.
I use Lazarus 2.2.0 32/64 and FPC 3.2.2 32/64 on Debian 11 64 bit, Windows 10, Windows 7 32/64, Windows XP 32,  FreeBSD 64.
Widgetset: fpGUI, MSEgui, Win32, GTK2, Qt.

https://github.com/fredvs
https://gitlab.com/fredvs
https://codeberg.org/fredvs

MarkMLl

  • Hero Member
  • *****
  • Posts: 6676
Re: My hobby project (image-to-character)
« Reply #5 on: October 05, 2020, 01:07:16 pm »
Thanks for updating us Fred :-)

MarkMLl
MT+86 & Turbo Pascal v1 on CCP/M-86, multitasking with LAN & graphics in 128Kb.
Pet hate: people who boast about the size and sophistication of their computer.
GitHub repositories: https://github.com/MarkMLl?tab=repositories

anyone

  • Guest
Re: My hobby project (image-to-character)
« Reply #6 on: October 05, 2020, 01:28:17 pm »
It is out of my expectation today that I receive many helpful response from fellow coders.

@MarkMLI . Thanks, reading the signature of magic number is the universal way of handling file format.
@Handoko. Thank you for the sample code (although it is not completed), reading both the file extension and file header signature is certainly a secure way to identify the image file format.

But, surprisingly, if I didn't specify the Reader, as suggested by @PascalDragon, it can open multiple image file format (BMP, PNG, JPG, GIF)! However, it failed on one particular PNG file no matter I specify the Reader explicitly as PNG or not.

@Fred vS. Thank you for the useful information. BGRABitmap was what I want to try in the first place, though, I found this fcl-image example code instead of BGRABitmap example code in my FPC installation folder.

----

BTW, Has anyone tried to convert image to ASCII art before? To get the grey intensity, can I use back the same code, or use BGRABitmap?


All fpReadXXX functions register their reader in the global ImageHandlers of unit fpImage and TFPCustomImage makes use of that. So simply doing a TFPMemoryImage.LoadFromFile without any reader should work already.

Looks like TFPMemoryImage.LoadFromFile would auto-detect file format?

And yes, thank you, it works.
« Last Edit: October 05, 2020, 01:30:57 pm by anyone »

circular

  • Hero Member
  • *****
  • Posts: 4196
    • Personal webpage
Re: My hobby project (image-to-character)
« Reply #7 on: October 05, 2020, 02:57:32 pm »
However, it failed on one particular PNG file no matter I specify the Reader explicitly as PNG or not.
The file might not be in PNG format in fact.

Quote
BTW, Has anyone tried to convert image to ASCII art before? To get the grey intensity, can I use back the same code, or use BGRABitmap?
It does not really matter at this point. Though with BGRABitmap you could resample the image instead of getting individual pixels separated by some distance. The image may look better with resample.

I was wondering about adding ASCII art features to BGRABitmap. Though I was thinking of trying something else: that the shape of the character would try to match the pixels it covers. For example to use "/" for a diagonal line etc.
Conscience is the debugger of the mind

MarkMLl

  • Hero Member
  • *****
  • Posts: 6676
Re: My hobby project (image-to-character)
« Reply #8 on: October 05, 2020, 03:30:07 pm »
However, it failed on one particular PNG file no matter I specify the Reader explicitly as PNG or not.
The file might not be in PNG format in fact.

I don't have the book that discusses it any more, but note that the sequence of characters at the start of a PNG was carefully chosen to detect the transmission and storage errors which had bedevilled GIFs: lost MSB, endian swaps, disagreement regarding EOL, truncation at CP/M-style EOF and so on.

It is possible- I'm not saying it happens, but it's possible- that some of those could be accommodated by promiscuous reader software. So sometimes even if a file appears to be broken, it's worth looking carefully at it.

MarkMLl
MT+86 & Turbo Pascal v1 on CCP/M-86, multitasking with LAN & graphics in 128Kb.
Pet hate: people who boast about the size and sophistication of their computer.
GitHub repositories: https://github.com/MarkMLl?tab=repositories

anyone

  • Guest
Re: My hobby project (image-to-character)
« Reply #9 on: October 05, 2020, 03:45:03 pm »
However, it failed on one particular PNG file no matter I specify the Reader explicitly as PNG or not.
The file might not be in PNG format in fact.

That PNG file can be opened by LazPaint nonetheless. (The said PNG file has background transparency)

It does not really matter at this point. Though with BGRABitmap you could resample the image instead of getting individual pixels separated by some distance. The image may look better with resample.

I was wondering about adding ASCII art features to BGRABitmap. Though I was thinking of trying something else: that the shape of the character would try to match the pixels it covers. For example to use "/" for a diagonal line etc.

Interesting idea, that is also considered an ASCII art.

Besides this, I have a feature suggestion for LazPaint. I am not sure about its usefulness though.

If anyone of you remember Paint Shop Pro, there is a feature called "Increase color depth..." and "Decrease color depth...". This feature is called "color quantization" in Python programming.

As I have limited knowledge about mathematic calculation, I will leave it to you as to how to implement this feature (if you accept my suggestion).

Such as, 24-bit color reduced to 256 color or 16 color....etc.

anyone

  • Guest
Re: My hobby project (image-to-character)
« Reply #10 on: October 05, 2020, 04:05:18 pm »
Please find the attached PNG file which could not be opened with fcl-image, but can be opened using LazPaint (and of course, Windows Photo Editor)

This is the error message:
Code: Pascal  [Select][+][-]
  1. C:\FPC>img2chr panda.png
  2. Error: Error while reading stream: Wrong image format
« Last Edit: October 05, 2020, 04:06:55 pm by anyone »

Fred vS

  • Hero Member
  • *****
  • Posts: 3158
    • StrumPract is the musicians best friend
Re: My hobby project (image-to-character)
« Reply #11 on: October 05, 2020, 04:14:42 pm »

If anyone of you remember Paint Shop Pro, there is a feature called "Increase color depth..." and "Decrease color depth...". This feature is called "color quantization" in Python programming.


Yes, I remember and I liked that feature.
IMHO, increasing color depth will be easy but decreasing is more tricky (maybe with different ways to calculate the average of the new color).
But I vote for it.
I use Lazarus 2.2.0 32/64 and FPC 3.2.2 32/64 on Debian 11 64 bit, Windows 10, Windows 7 32/64, Windows XP 32,  FreeBSD 64.
Widgetset: fpGUI, MSEgui, Win32, GTK2, Qt.

https://github.com/fredvs
https://gitlab.com/fredvs
https://codeberg.org/fredvs

wp

  • Hero Member
  • *****
  • Posts: 11858
Re: My hobby project (image-to-character)
« Reply #12 on: October 05, 2020, 04:17:16 pm »
Please find the attached PNG file which could not be opened with fcl-image, but can be opened using LazPaint (and of course, Windows Photo Editor)

This is the error message:
Code: Pascal  [Select][+][-]
  1. C:\FPC>img2chr panda.png
  2. Error: Error while reading stream: Wrong image format
Trying to open this image with IrfanView I get the message that this is a jpg file with incorrect extension. The LCL TImage (which uses the fcl-image readers/writers) loads the image without any issues.
« Last Edit: October 05, 2020, 04:34:58 pm by wp »

Fred vS

  • Hero Member
  • *****
  • Posts: 3158
    • StrumPract is the musicians best friend
Re: My hobby project (image-to-character)
« Reply #13 on: October 05, 2020, 04:24:58 pm »
Please find the attached PNG file which could not be opened with fcl-image, but can be opened using LazPaint (and of course, Windows Photo Editor)

This is the error message:
Code: Pascal  [Select][+][-]
  1. C:\FPC>img2chr panda.png
  2. Error: Error while reading stream: Wrong image format
Trying to open this image with IrfanView I get the message that this is a jpg file with incorrect extension.

Indeed, with ImageMagick I get this (see picture):

I use Lazarus 2.2.0 32/64 and FPC 3.2.2 32/64 on Debian 11 64 bit, Windows 10, Windows 7 32/64, Windows XP 32,  FreeBSD 64.
Widgetset: fpGUI, MSEgui, Win32, GTK2, Qt.

https://github.com/fredvs
https://gitlab.com/fredvs
https://codeberg.org/fredvs

circular

  • Hero Member
  • *****
  • Posts: 4196
    • Personal webpage
Re: My hobby project (image-to-character)
« Reply #14 on: October 05, 2020, 04:35:13 pm »
Besides this, I have a feature suggestion for LazPaint. I am not sure about its usefulness though.

If anyone of you remember Paint Shop Pro, there is a feature called "Increase color depth..." and "Decrease color depth...". This feature is called "color quantization" in Python programming.

As I have limited knowledge about mathematic calculation, I will leave it to you as to how to implement this feature (if you accept my suggestion).

Such as, 24-bit color reduced to 256 color or 16 color....etc.
It is possible to save a file in 256 colors, which makes color quantization. You can as well use the palette toolbar. For example generate 16 colors and then apply dithering with it. The image can still have all 24-bit colors but you can get the effect if you like.
Conscience is the debugger of the mind

 

TinyPortal © 2005-2018