Home
JAQForum Ver 24.01
Log In or Join  
Active Topics
Local Time 09:26 15 Oct 2025 Privacy Policy
Jump to

Notice. New forum software under development. It's going to miss a few functions and look a bit ugly for a while, but I'm working on it full time now as the old forum was too unstable. Couple days, all good. If you notice any issues, please contact me.

Forum Index : Microcontroller and PC projects : Dittering Pictures to PicoMite

     Page 2 of 2    
Author Message
Martin H.

Guru

Joined: 04/06/2022
Location: Germany
Posts: 1293
Posted: 08:17am 24 Dec 2022
Copy link to clipboard 
Print this post

I Found a Video where javidx9 (One lone Coder) explains, how to Program Dithering Floyd-Steinberg Dithering. Seems to be not much, so I'II give it a try next week.

Dithering with Floyd-Steinberg in C++
'no comment
 
Martin H.

Guru

Joined: 04/06/2022
Location: Germany
Posts: 1293
Posted: 08:22am 24 Dec 2022
Copy link to clipboard 
Print this post

  scruss said  
Here are the BMPs generated from the above two commands:
dithering.zip

Thank you for showing another option.
Very nice Output Pictures. Good Job
For using them on the Pico, they have to be transfered back to 24Bit BMP or to Jpg.
Cheers
Mart!n
Edited 2022-12-24 18:23 by Martin H.
'no comment
 
homa

Guru

Joined: 05/11/2021
Location: Germany
Posts: 481
Posted: 08:52pm 30 Dec 2022
Copy link to clipboard 
Print this post

Hello,
I have found the following tool: http://christoph.laeubrich.de/files/uploads/19/grafikkonverterv1_4.zip
Detail here: https://www.mikrocontroller.net/topic/92623   or also here https://github.com/laeubi/grafikkonverter-tool
Unfortunately you have to enter the 16 colors once as a palette at the beginning, this can also not be saved :-(
Maybe someone can do something with the sources ...
Results see attached.
London extracted from the input post. The bird and a test picture of me ...











 
homa

Guru

Joined: 05/11/2021
Location: Germany
Posts: 481
Posted: 08:59pm 30 Dec 2022
Copy link to clipboard 
Print this post

 
Martin H.

Guru

Joined: 04/06/2022
Location: Germany
Posts: 1293
Posted: 03:29pm 31 Dec 2022
Copy link to clipboard 
Print this post

Nice,
Unfortunately I don't know enough about Java to find the part, where you could store or load the palette.
The results are really great.

btw: Happy New Year


Edited 2023-01-01 01:30 by Martin H.
'no comment
 
homa

Guru

Joined: 05/11/2021
Location: Germany
Posts: 481
Posted: 09:00pm 31 Dec 2022
Copy link to clipboard 
Print this post

If then true to style ;-)
Happy New Year!


 
Martin H.

Guru

Joined: 04/06/2022
Location: Germany
Posts: 1293
Posted: 03:05pm 02 Jan 2023
Copy link to clipboard 
Print this post

  homa said   _

I Think I've found the(by now) best solution.
Here is an Online Ditherer, where you can save (copy and Past) a Custom Color Palette and it dithers in many different ways:
https://ditherit.com/


the Custom Palette for the Pico to past in looks like this:

[{"hex":"#000000"},
{"hex":"#FF0000"},
{"hex":"#0000FF"},
{"hex":"#FF00FF"},
{"hex":"#004000"},
{"hex":"#FF4000"},
{"hex":"#0040FF"},
{"hex":"#FF40FF"},
{"hex":"#008000"},
{"hex":"#FF8000"},
{"hex":"#0080FF"},
{"hex":"#FF80FF"},
{"hex":"#00FF00"},
{"hex":"#FFFF00"},
{"hex":"#00FFFF"},
{"hex":"#FFFFFF"}]




Here the london png with Atkinson dithering looks like this





Cheers
Mart!n
Edited 2023-01-03 01:25 by Martin H.
'no comment
 
Volhout
Guru

Joined: 05/03/2018
Location: Netherlands
Posts: 5345
Posted: 07:32pm 02 Jan 2023
Copy link to clipboard 
Print this post

Nice tool, this will help create better graphics.

Volhout
PicomiteVGA PETSCII ROBOTS
 
Martin H.

Guru

Joined: 04/06/2022
Location: Germany
Posts: 1293
Posted: 08:46pm 13 Oct 2025
Copy link to clipboard 
Print this post

Working on the illustration for Colossal Cave Adventures has resulted in significant changes to my MMB 4 Windows tool.
The program now determines the color thresholds itself.
First, the statistical mean for each color (RGB) is calculated for 8x8 pixel blocks, then the median of this list is determined from all the values.
These values are used as the threshold values.
These values can be changed later using buttons.


becomes

'MMB4W Modes etc. Loads a source PNG and translatet it to 121 RGB BMP
'
'Enter Source Filename here
FN$="testapple"
'---------------------------------

Mode 1
dim w, h as integer
open FN$ + ".png" for input as #1
s$ = INPUT$(24, #1) ' Erste 24 Bytes holen (bis Ende IHDR-Header)

' Breite aus Byte 17-20 (Index 16–19 in MMBasic, da 0-basiert)
w = ASC(MID$(s$,17,1))*16777216 + ASC(MID$(s$,18,1))*65536 + ASC(MID$(s$,19,1))*256 + ASC(MID$(s$,20,1))

' Höhe aus Byte 21–24 (Index 20–23)
h = ASC(MID$(s$,21,1))*16777216 + ASC(MID$(s$,22,1))*65536 + ASC(MID$(s$,23,1))*256 + ASC(MID$(s$,24,1))
close #1
BL%=(h*w)/64
Dim integer listR(BL%)
Dim integer listG(BL%)
Dim integer listB(BL%)
cls
load png FN$+".png"
Prescan
Scan
ui
save image FN$+".bmp",0,0,w,h
end
sub Scan
for y=0 to h
   for x=0 to w
       cl%=0
       n=Pixel(x,y)AND &HFFFFFF
       '-->get Red
       r=n AND &HFF
       'Check red thresholds if value is in then dither
       if r>LR and r<UR then
        if (y mod 2)then
        R=255*not (x mod 2)
        else
        R=255*(x mod 2)
        end if
       end if
       inc cl%,255*(r>UR):n=int(n/256)
       '-->get Green
       g=n AND &HFF
       'Check green thresholds, no dithering
       select case g
       case >UG
       inc cl%,&HFF00
       case >MG
       inc cl%,&H8000
       case >LG
       inc cl%,&H4000
       end select
       '-->get Blue
       n=int(n/256)
       b=n AND &HFF
       'Check Blue thresholds, if value is in then dither
       if b>LB and r<UB then
        if (y mod 2)then
        B=255*(x mod 2)
        else
        B=255*not (x mod 2)
        end if
       end if
       inc cl%,&Hff0000*(b>UB)
       pixel x,y,cl%
   next
next
End sub
sub ui
Font 2
px=w+6
do
Print@(px,0);"           threshold values"
Print@(px,20);"      RED       GREEN        Blue"
Print@(px,40);"    Q ";make$(LR);" W    E ";make$(LG);
PRINT " R      O ";make$(LB);" P"
Print@(px,60);"    A ";make$(UR);" S    D ";make$(MG);
PRINT " F      K ";make$(UB);" L"
Print@(px,80);"               C ";make$(UG);" V"
Print@(px,100);
Print@(PX,120);" SPACE = Repaint"
Print@(PX,140);" Enter = Save&Exit"
Print@(PX,160);"   ESC = Exit without Save"
do :k$=inkey$:Loop while k$=""
k$=Ucase$(k$)
select case k$
case "Q"
if LR>0 then inc LR, -1
case "W"
if LR<UR then inc LR
case "E"
if LG>0 then inc LG, -1
case "R"
if LG<MG then inc LG
case "O"
if LB>0 then inc LB, -1
case "P"
if LB<UB then inc LB
case "A"
if UR>LR then inc UR, -1
 case "S"
 if UR<255 then inc UR
  case "K"
if UB>LB then inc UB, -1
 case "L"
 if UB<255 then inc UB
   case "C"
if UG>MG then inc UG, -1
 case "V"
 if UG<255 then inc UG
     case "D"
if MG>LG then inc MG, -1
 case "F"
 if MG<UG then inc MG
 case " "
 load png FN$+".png"
  Scan
case chr$(13) ,chr$(10)
  Exit SUB
 case chr$(27)
  End
end select
loop
end sub
Function  make$(Value)
make$=STR$(Value)
do while (len(make$)<3)
 make$="0"+make$
loop
end Function
sub Prescan
FOR y = 0 TO h - 8 STEP 8
 FOR x = 0 TO w - 8 STEP 8
   sumR = 0 : sumG = 0 : sumB = 0
   FOR dy = 0 TO 7
     FOR dx = 0 TO 7
       n = Pixel(x + dx, y + dy) AND &HFFFFFF
       r = n AND &HFF
       n = INT(n / 256)
       g = n AND &HFF
       n = INT(n / 256)
       b = n AND &HFF
       sumR = sumR + r
       sumG = sumG + g
       sumB = sumB + b
     NEXT dx
   NEXT dy
   ' Mittelwert pro Block
   blockR = INT(sumR / 64)
   blockG = INT(sumG / 64)
   blockB = INT(sumB / 64)
   ' In Medianliste schreiben
   listR(blockIndex) = blockR
   listG(blockIndex) = blockG
   listB(blockIndex) = blockB
   blockIndex = blockIndex + 1
 NEXT x
NEXT y
pass = 0
do
   tausch = 0
   For ss = 0 To BL% - 2 - pass
       If listR(ss) > listR(ss + 1) Then
           tt = listR(ss):listR(ss) = listR(ss + 1):listR(ss + 1) = tt:tausch = 1
       End If
   Next
   Inc pass = pass
Loop Until tausch = 0
pass = 0
do
   tausch = 0
   For ss = 0 To BL% - 2 - pass
       If ListG(ss) > ListG(ss + 1) Then
           tt = ListG(ss):ListG(ss) = ListG(ss + 1):ListG(ss + 1) = tt:tausch = 1
       End If
   Next
   Inc pass = pass
Loop Until tausch = 0
pass = 0
do
   tausch = 0
   For ss = 0 To BL% - 2 - pass
       If ListB(ss) > ListB(ss + 1) Then
           tt = ListB(ss):ListB(ss) = ListB(ss + 1):ListB(ss + 1) = tt:tausch = 1
       End If
   Next
Inc pass = pass
Loop Until tausch = 0
LR=listR(BL%/3)      'Lower threshold Red
UR=listR(2*(BL%/3))  'Upper threshold

LG=listG(BL%/4)'Lower threshold Green
MG=ListG(2*(BL%/4)) 'Mid threshold
UG=ListG(3*(BL%/4)) 'Upper threshold

LB=listB(BL%/3)      'Lower threshold Blue
UB=listB(2*(BL%/3))  'Upper threshold
end sub

Maybe I'll give Atkinson a try after all.
Cheers
 Martin
Edited 2025-10-14 07:52 by Martin H.
'no comment
 
dddns
Guru

Joined: 20/09/2024
Location: Germany
Posts: 608
Posted: 10:15pm 13 Oct 2025
Copy link to clipboard 
Print this post

Fantastic thread, thank you all! I'm new to grafics and didn't have an idea how to get into that.

Special thanks @scruss:

  scruss said  A couple of other ways to make pre-dithered images using the venerable (okay, maybe just old) Netpbm toolkit:

1.) Ordered dither

djpeg london.jpg | ppmdither -red=2 -green=4 -blue=2 | ppmtobmp > london_ordered.bmp


Result:





Under Linux all there is to do:
apt install libjpeg-progs
djpeg bird_orig.jpg | pnmremap -floyd -mapfile=<(echo P3 16 1 3 0 0 0 0 2 0 0 2 3 3 3 3 0 3 3 3 0 3 3 0 0 3 1 0 3 2 3 3 2 0 3 1 3 0 3 0 3 3 0 0 1 3 0 0 3 0 1 0) | ppmtobmp > bird_orig_FS.bmp


to have that wonderful result and that is exactly how it look on my unmodified VGA:
load image "b:bird_orig_fs.bmp" :  save image "pico_org.bmp"




Thanks a lot!!


Edit:
Weils soo schön ist:



Mode 3 @800x600
Edited 2025-10-14 08:47 by dddns
 
Martin H.

Guru

Joined: 04/06/2022
Location: Germany
Posts: 1293
Posted: 09:38am 14 Oct 2025
Copy link to clipboard 
Print this post

sub Prescan Accelerated many times (from 3-4S to 200ms) over and greatly shortened (Sometimes it doesn't hurt to look in the manual)
please replace with:

sub Prescan
   'Determine the average values of each color in the image
   'to auto calculate the thresholds
   '--------------------------------------------------
   'pass1: Sample and store the color-values of 8x8 blocks.
   local integer r,g,b,n,dx,dy,ss
   FOR y = 0 TO h - 8 STEP 8
     FOR x = 0 TO w - 8 STEP 8
       sumR = 0 : sumG = 0 : sumB = 0
       FOR dy = y TO y+7
         FOR dx = x TO x+7
           n = Pixel(dx, dy) AND &HFFFFFF
           r=n AND &HFF:g=n AND &HFF00:b=n AND &HFF0000
           inc sumR,r:inc sumG,g:inc sumB,b
         NEXT
       NEXT
       ' Values *64 (8x8)pro Block
       listR(blockIndex)=sumR:listG(blockIndex)=sumG:listB(blockIndex)=sumB
       inc blockIndex
     NEXT
   NEXT
   '2.Pass sort the sampled values for each color to find the median
   SORT ListR(), , , ,BL%:SORT ListG(), , , ,BL%:SORT ListB(), , , ,BL%
   LR=listR(BL%/3)>>6      'Lower threshold Red
   UR=listR(2*(BL%/3))>>6  'Upper threshold
   LG=listG(BL%/4)>>14     'Lower threshold Green
   MG=ListG(2*(BL%/4))>>14 'Mid threshold
   UG=ListG(3*(BL%/4))>>14 'Upper threshold
   LB=listB(BL%/3)>>22     'Lower threshold Blue
   UB=listB(2*(BL%/3))>>22 'Upper threshold
end sub

If you can't sleep, you might as well use the time wisely.
'no comment
 
matherp
Guru

Joined: 11/12/2012
Location: United Kingdom
Posts: 10481
Posted: 11:11am 14 Oct 2025
Copy link to clipboard 
Print this post

Please don't take this the wrong way....
Here is an image on PICOMITE VGA RP2350 Mode 3 so 640x480 RGB121 standard palette
using the new command
timer=0:load dithered "b:/tiger640":?timer
As you can see it took 1.3 seconds to load and draw.



Here is the genesis of the command using CLAUDE
  Quote  write a function to read a bmp file. Assume the file fnbr is already open and you can read it using the function int IMG_FREAD(int fnbr, void *buff, int count, unsigned int *read)
buffer no more than 5 lines at a time. The maximum filesize is 1920x1080.
Write the file to the display using a dithering algorithm to optimise the visual effect. Accept as an input to the function a parameter that specifies whether the display is RGB121 or RGB332. Assume the bmp file uses standard non-compressed RGB888


  Quote  You can use the function DrawPixel(x,y,c) to write to the screen. No start or end frame calls are needed. The display size is HRes *VRes. Add parameters to indicate the x and y coordinates of the image that should map to 0,0 of the display. Ignore pixels in the image that would fall outside of the display.
DrawPixel takes an RGB888 colour so the RGB121 or RGB332 pixels should be expanded to RGB888 in order to call it.
Use (void *)GetMemory(size) to allocate buffer space and (void)FreeMemory(void *) to free it


  Quote  cast all the freememory functions to void * to avoid compiler warnings. Check for the correct bmp file format and raise an error if incorrect using the function void error(char *)


  Quote  To optimise writing you can use the function void DrawBuffer(int x1, int y1, int x2, int y2, unsigned char *p);
to output to the screen. In this case char *p is a pointer to an array of colour triplets R0,G0,B0,R1,G1,B1...Rn,Bn,Gn


  Quote  apologies it should have been colour triplets B0,G0,R0,B1,G1,R1...Bn,Gn,Rn


  Quote  Finally add parameters x_display and y_display that indicate the top left coordinates on the physical display where the image will be drawn


Total time to create the fully working new capability which will be in the next release - less than 1 hour. The world really has changed

Here is the code

// BMP structures
typedef struct __attribute__((packed))
{
       uint16_t bfType;
       uint32_t bfSize;
       uint16_t bfReserved1;
       uint16_t bfReserved2;
       uint32_t bfOffBits;
} BITMAPFILEHEADER;

typedef struct __attribute__((packed))
{
       uint32_t biSize;
       int32_t biWidth;
       int32_t biHeight;
       uint16_t biPlanes;
       uint16_t biBitCount;
       uint32_t biCompression;
       uint32_t biSizeImage;
       int32_t biXPelsPerMeter;
       int32_t biYPelsPerMeter;
       uint32_t biClrUsed;
       uint32_t biClrImportant;
} BITMAPINFOHEADER;

// Convert RGB121 to RGB888
void rgb121_to_rgb888_components(uint8_t color, uint8_t *r, uint8_t *g, uint8_t *b)
{
       uint8_t r1 = (color >> 3) & 1;
       uint8_t g2 = (color >> 1) & 3;
       uint8_t b1 = color & 1;

       *r = r1 * 255;
       *g = g2 * 85; // 255/3
       *b = b1 * 255;
}

// Convert RGB332 to RGB888
void rgb332_to_rgb888_components(uint8_t color, uint8_t *r, uint8_t *g, uint8_t *b)
{
       uint8_t r3 = (color >> 5) & 7;
       uint8_t g3 = (color >> 2) & 7;
       uint8_t b2 = color & 3;

       *r = (r3 * 255) / 7;
       *g = (g3 * 255) / 7;
       *b = (b2 * 255) / 3;
}

// Convert RGB888 to RGB121 with dithering
uint8_t rgb888_to_rgb121_dither(int16_t r, int16_t g, int16_t b)
{
       // Clamp values
       r = (r < 0) ? 0 : (r > 255) ? 255
                                   : r;
       g = (g < 0) ? 0 : (g > 255) ? 255
                                   : g;
       b = (b < 0) ? 0 : (b > 255) ? 255
                                   : b;

       // Convert to RGB121 (1 bit R, 2 bits G, 1 bit B)
       uint8_t r1 = (r >= 128) ? 1 : 0;
       uint8_t g2 = (g * 3 + 127) / 255;
       uint8_t b1 = (b >= 128) ? 1 : 0;

       return (r1 << 3) | (g2 << 1) | b1;
}

// Convert RGB888 to RGB332 with dithering
uint8_t rgb888_to_rgb332_dither(int16_t r, int16_t g, int16_t b)
{
       // Clamp values
       r = (r < 0) ? 0 : (r > 255) ? 255
                                   : r;
       g = (g < 0) ? 0 : (g > 255) ? 255
                                   : g;
       b = (b < 0) ? 0 : (b > 255) ? 255
                                   : b;

       // Convert to RGB332 (3 bits R, 3 bits G, 2 bits B)
       uint8_t r3 = (r * 7 + 127) / 255;
       uint8_t g3 = (g * 7 + 127) / 255;
       uint8_t b2 = (b * 3 + 127) / 255;

       return (r3 << 5) | (g3 << 2) | b2;
}

int ReadAndDisplayBMP(int fnbr, int display_mode, int img_x_offset, int img_y_offset,
                     int x_display, int y_display)
{
       BITMAPFILEHEADER fileHeader;
       BITMAPINFOHEADER infoHeader;
       unsigned int bytes_read;
       int result;

       // Read file header
       result = IMG_FREAD(fnbr, &fileHeader, sizeof(BITMAPFILEHEADER), &bytes_read);
       if (result != 0 || bytes_read != sizeof(BITMAPFILEHEADER))
       {
               error("BMP: Failed to read file header");
               return -1;
       }

       // Verify BMP signature
       if (fileHeader.bfType != 0x4D42)
       { // "BM"
               error("BMP: Invalid file signature (not a BMP file)");
               return -2;
       }

       // Read info header
       result = IMG_FREAD(fnbr, &infoHeader, sizeof(BITMAPINFOHEADER), &bytes_read);
       if (result != 0 || bytes_read != sizeof(BITMAPINFOHEADER))
       {
               error("BMP: Failed to read info header");
               return -3;
       }

       // Verify format - must be 24-bit RGB
       if (infoHeader.biBitCount != 24)
       {
               error("BMP: Must be 24-bit RGB888 format");
               return -4;
       }

       // Verify no compression
       if (infoHeader.biCompression != 0)
       {
               error("BMP: Compressed format not supported");
               return -5;
       }

       // Verify biPlanes is 1 (standard requirement)
       if (infoHeader.biPlanes != 1)
       {
               error("BMP: Invalid planes value (must be 1)");
               return -6;
       }

       int width = infoHeader.biWidth;
       int height = infoHeader.biHeight;
       int is_bottom_up = (height > 0);
       if (height < 0)
               height = -height;

       // Check size limits
       if (width > 1920 || height > 1080 || width <= 0 || height <= 0)
       {
               error("BMP: Image dimensions invalid or exceed 1920x1080");
               return -7;
       }

       // Calculate row size (rows are padded to 4-byte boundary)
       int row_size = ((width * 3 + 3) / 4) * 4;

       // Allocate buffers for Floyd-Steinberg dithering
       // We need to store error values for current and next line
       int16_t *error_buffer_r = (int16_t *)GetMemory(width * 2 * sizeof(int16_t));
       int16_t *error_buffer_g = (int16_t *)GetMemory(width * 2 * sizeof(int16_t));
       int16_t *error_buffer_b = (int16_t *)GetMemory(width * 2 * sizeof(int16_t));

       if (!error_buffer_r || !error_buffer_g || !error_buffer_b)
       {
               if (error_buffer_r)
                       FreeMemory((void *)error_buffer_r);
               if (error_buffer_g)
                       FreeMemory((void *)error_buffer_g);
               if (error_buffer_b)
                       FreeMemory((void *)error_buffer_b);
               error("BMP: Failed to allocate error buffers");
               return -8;
       }

       // Initialize error buffers to zero
       for (int i = 0; i < width * 2; i++)
       {
               error_buffer_r[i] = 0;
               error_buffer_g[i] = 0;
               error_buffer_b[i] = 0;
       }

       int16_t *curr_error_r = error_buffer_r;
       int16_t *next_error_r = error_buffer_r + width;
       int16_t *curr_error_g = error_buffer_g;
       int16_t *next_error_g = error_buffer_g + width;
       int16_t *curr_error_b = error_buffer_b;
       int16_t *next_error_b = error_buffer_b + width;

       // Buffer for reading pixel data (5 lines max)
       int lines_per_buffer = 5;
       uint8_t *line_buffer = (uint8_t *)GetMemory(row_size * lines_per_buffer);
       if (!line_buffer)
       {
               FreeMemory((void *)error_buffer_r);
               FreeMemory((void *)error_buffer_g);
               FreeMemory((void *)error_buffer_b);
               error("BMP: Failed to allocate line buffer");
               return -9;
       }

       // Buffer for output BGR triplets (maximum width for visible portion)
       int max_visible_width = (HRes < width) ? HRes : width;
       uint8_t *output_buffer = (uint8_t *)GetMemory(max_visible_width * 3);
       if (!output_buffer)
       {
               FreeMemory((void *)line_buffer);
               FreeMemory((void *)error_buffer_r);
               FreeMemory((void *)error_buffer_g);
               FreeMemory((void *)error_buffer_b);
               error("BMP: Failed to allocate output buffer");
               return -10;
       }

       // Process image line by line
       for (int y = 0; y < height; y += lines_per_buffer)
       {
               int lines_to_read = (y + lines_per_buffer > height) ? (height - y) : lines_per_buffer;
               int bytes_to_read = row_size * lines_to_read;

               // Read lines
               result = IMG_FREAD(fnbr, line_buffer, bytes_to_read, &bytes_read);
               if (result != 0 || bytes_read != bytes_to_read)
               {
                       FreeMemory((void *)output_buffer);
                       FreeMemory((void *)line_buffer);
                       FreeMemory((void *)error_buffer_r);
                       FreeMemory((void *)error_buffer_g);
                       FreeMemory((void *)error_buffer_b);
                       error("BMP: Failed to read image data");
                       return -11;
               }

               // Process each line in the buffer
               for (int line = 0; line < lines_to_read; line++)
               {
                       int current_y = y + line;
                       int img_y = is_bottom_up ? (height - 1 - current_y) : current_y;
                       int screen_y = img_y - img_y_offset + y_display;

                       // Skip if line is outside display bounds
                       if (screen_y < 0 || screen_y >= VRes)
                       {
                               continue;
                       }

                       // Clear next line error buffer
                       for (int i = 0; i < width; i++)
                       {
                               next_error_r[i] = 0;
                               next_error_g[i] = 0;
                               next_error_b[i] = 0;
                       }

                       // Track visible pixels
                       int visible_pixels = 0;
                       int first_screen_x = -1;

                       // Process each pixel in the line
                       for (int x = 0; x < width; x++)
                       {
                               int pixel_offset = line * row_size + x * 3;
                               uint8_t b = line_buffer[pixel_offset];
                               uint8_t g = line_buffer[pixel_offset + 1];
                               uint8_t r = line_buffer[pixel_offset + 2];

                               // Apply accumulated error from previous pixels
                               int16_t old_r = r + curr_error_r[x];
                               int16_t old_g = g + curr_error_g[x];
                               int16_t old_b = b + curr_error_b[x];

                               // Convert to display format
                               uint8_t display_color;
                               uint8_t new_r, new_g, new_b;

                               if (display_mode == DISPLAY_RGB121)
                               {
                                       display_color = rgb888_to_rgb121_dither(old_r, old_g, old_b);
                                       rgb121_to_rgb888_components(display_color, &new_r, &new_g, &new_b);
                               }
                               else
                               { // DISPLAY_RGB332
                                       display_color = rgb888_to_rgb332_dither(old_r, old_g, old_b);
                                       rgb332_to_rgb888_components(display_color, &new_r, &new_g, &new_b);
                               }

                               // Clamp old values for error calculation
                               old_r = (old_r < 0) ? 0 : (old_r > 255) ? 255
                                                                       : old_r;
                               old_g = (old_g < 0) ? 0 : (old_g > 255) ? 255
                                                                       : old_g;
                               old_b = (old_b < 0) ? 0 : (old_b > 255) ? 255
                                                                       : old_b;

                               // Calculate error
                               int16_t err_r = old_r - new_r;
                               int16_t err_g = old_g - new_g;
                               int16_t err_b = old_b - new_b;

                               // Distribute error using Floyd-Steinberg dithering
                               if (x + 1 < width)
                               {
                                       curr_error_r[x + 1] += (err_r * 7) / 16;
                                       curr_error_g[x + 1] += (err_g * 7) / 16;
                                       curr_error_b[x + 1] += (err_b * 7) / 16;
                               }
                               if (x > 0)
                               {
                                       next_error_r[x - 1] += (err_r * 3) / 16;
                                       next_error_g[x - 1] += (err_g * 3) / 16;
                                       next_error_b[x - 1] += (err_b * 3) / 16;
                               }
                               next_error_r[x] += (err_r * 5) / 16;
                               next_error_g[x] += (err_g * 5) / 16;
                               next_error_b[x] += (err_b * 5) / 16;
                               if (x + 1 < width)
                               {
                                       next_error_r[x + 1] += (err_r * 1) / 16;
                                       next_error_g[x + 1] += (err_g * 1) / 16;
                                       next_error_b[x + 1] += (err_b * 1) / 16;
                               }

                               // Calculate screen position
                               int screen_x = x - img_x_offset + x_display;

                               // Store pixel if it's visible on screen
                               if (screen_x >= 0 && screen_x < HRes)
                               {
                                       if (first_screen_x == -1)
                                       {
                                               first_screen_x = screen_x;
                                       }
                                       // Store as BGR triplets
                                       output_buffer[visible_pixels * 3] = new_b;
                                       output_buffer[visible_pixels * 3 + 1] = new_g;
                                       output_buffer[visible_pixels * 3 + 2] = new_r;
                                       visible_pixels++;
                               }
                       }

                       // Draw the line if any pixels are visible
                       if (visible_pixels > 0)
                       {
                               int last_screen_x = first_screen_x + visible_pixels - 1;
                               DrawBuffer(first_screen_x, screen_y, last_screen_x, screen_y, output_buffer);
                       }

                       // Swap error buffers
                       int16_t *temp;
                       temp = curr_error_r;
                       curr_error_r = next_error_r;
                       next_error_r = temp;
                       temp = curr_error_g;
                       curr_error_g = next_error_g;
                       next_error_g = temp;
                       temp = curr_error_b;
                       curr_error_b = next_error_b;
                       next_error_b = temp;
               }
       }

       // Cleanup
       FreeMemory((void *)output_buffer);
       FreeMemory((void *)line_buffer);
       FreeMemory((void *)error_buffer_r);
       FreeMemory((void *)error_buffer_g);
       FreeMemory((void *)error_buffer_b);

       return 0;
}

Edited 2025-10-14 21:26 by matherp
 
Martin H.

Guru

Joined: 04/06/2022
Location: Germany
Posts: 1293
Posted: 07:12pm 14 Oct 2025
Copy link to clipboard 
Print this post

Atkinson Dithering

During the mid-1980’s, dithering became increasingly popular as computer hardware advanced to support more powerful video drivers and displays. One of the best dithering algorithms from this era was developed by Bill Atkinson, a Apple employee who worked on everything from MacPaint (which he wrote from scratch for the original Macintosh) to HyperCard and QuickDraw.

Atkinson’s formula is a bit different from others in this list, because it only propagates a fraction of the error instead of the full amount. This technique is sometimes offered by modern graphics applications as a “reduced color bleed” option. By only propagating part of the error, speckling is reduced, but contiguous dark or bright sections of an image may become washed out.

       X   1   1
   1   1   1
       1
https://tannerhelland.com/2012/12/28/dithering-eleven-algorithms-source-code.html

FN$="TestApple"
'---------------------------------
' Atkinson Dithering v 0.9
'Converts PNG Source Files
'     to RGB121 BMP
'   by Martin H. 2025
'---------------------------------
Mode 1
'Header vom Quellbild "PNG" einlesen
dim integer w, h
open FN$ + ".png" for input as #1
s$ = INPUT$(24, #1) ' Erste 24 Bytes holen (bis Ende IHDR-Header)
' Breite aus Byte 17-20 (Index 16–19 in MMBasic, da 0-basiert)
w = ASC(MID$(s$,17,1))*16777216 + ASC(MID$(s$,18,1))*65536 + ASC(MID$(s$,19,1))*256 + ASC(MID$(s$,20,1))
' Höhe aus Byte 21–24 (Index 20–23)
h = ASC(MID$(s$,21,1))*16777216 + ASC(MID$(s$,22,1))*65536 + ASC(MID$(s$,23,1))*256 + ASC(MID$(s$,24,1))
close #1
BL%=(h*w)/64
Dim integer listR(BL%),listG(BL%),listB(BL%)
cls
'Quellbild laden
load png FN$+".png"
'Atkinson Dithering, see
' https://tannerhelland.com/2012/12/28/dithering-eleven-algorithms-source-code.html
Dim integer RLine(W+3,3),BLine(W+3,3),GLine(W+3,3)
'Scan beginn
for Ypos=0 to H-1
   for Xpos=1 to W
   '  
       n=Pixel(Xpos,Ypos)AND &HFFFFFF
       R = (N AND 255):RV=R+RLine(Xpos,1):'Pixel Xpos,Ypos,R
       B = ((N >> 16) AND 255)
       G=  ((N >> 8)  AND 255)
       'Green
       GV = G + GLine(Xpos, 1)
       if GV > 255 then GV = 255
       if GV < 0 then GV = 0
       Index = INT(GV / 64 + 0.5)
       'Calculate pallet value (G_Pallete). (0, 64, 128, 192, 255)
       G_Palette = Index * 64
       if G_Palette > 255 then G_Palette = 255
       GE = GV - G_Palette
       'Red
       if RV>128 then
         RE=RV-255:CR=255
       Else
           RE=RV:CR=0 'Pixel Xpos,Ypos+h,0:CR=0
       end if
       'Blue
       BV=B+BLine(Xpos,1)
       if BV>128 then
         BE=BV-255:CB=255
       Else
           BE=BV:CB=0
       end if
       Pixel Xpos,Ypos+h,RGB(CR,G_Palette,CB)
       
       'Add to error list
       GE=GE/8
       RE=RE/8:BE=BE/8:GE=GE/8
       inc GLine(Xpos+1,1),GE:inc GLine(Xpos+2,1),GE
       inc GLine(Xpos-1,2),GE:inc GLine(Xpos+1,2),GE:inc GLine(Xposs,3),GE
       inc RLine(Xpos+1,1),RE:inc RLine(Xpos+2,1),RE:
       if Xpos>0 then inc RLine(Xpos-1,2),RE:inc BLine(Xpos-1,2),BE:inc GLine(Xpos-1,2),GE
       inc RLine(Xposs,2),RE:inc RLine(Xpos+1,2),RE:inc RLine(Xposs,3),RE
       inc BLine(Xpos+1,1),BE:inc BLine(Xpos+2,1),BE:
       inc BLine(Xposs,2),BE:inc BLine(Xpos+1,2),BE:inc BLine(Xposs,3),BE
     Next
    'Next line, scrolling through the 3 lines of the error list
   for f=0 to W+3:RLine(F,1)=RLine(F,2):RLine(F,2)=RLine(F,3):RLine(F,3)=0:
   BLine(F,1)=BLine(F,2):BLine(F,2)=BLine(F,3):BLine(F,3)=0:Next
Next
save image FN$+".bmp",0,h,w,h



'no comment
 
     Page 2 of 2    
Print this page


To reply to this topic, you need to log in.

The Back Shed's forum code is written, and hosted, in Australia.
© JAQ Software 2025