Bodmer / TJpg_Decoder

Jpeg decoder library based on Tiny JPEG Decompressor
Other
227 stars 43 forks source link

Using the callback to convert to 1bit image #9

Closed robotzero1 closed 4 years ago

robotzero1 commented 4 years ago

I'm trying to use the tft_output callback to convert a jpg to a 1bit image but I don't get the 1s and 0s how I would expect. Am I doing something wrong?

This is the 128x64 image: colour

The code:

static bool tft_output(int16_t x, int16_t y, uint16_t w, uint16_t h, uint16_t* bitmap)
{
  if ( y >= 64 ) return 0;
  byte line_len = 0;
  for (int o = 0; o < w * h; o++) {
    uint16_t R = (((bitmap[o]) >> 11) & 0x1F) << 3; // extract R component
    uint16_t G = (((bitmap[o]) >> 5) & 0x3F) << 2; // extract G component
    uint16_t B = ((bitmap[o]) & 0x1F) << 3; // extract B component
    uint16_t gray = (R * 30 + G * 59 + B * 11) / 100; // formula from the link above
    uint8_t black_white = random(255) < gray ? 1 : 0;
    Serial.print(black_white);
    line_len++;
    if ( line_len >= 128) {
      line_len = 0;
      Serial.println();
    }
  }
  return 1;
}

void setup()
{
  Serial.begin(115200);

  if (!SPIFFS.begin()) {
    Serial.println("SPIFFS initialisation failed!");
    while (1) yield(); // Stay here twiddling thumbs waiting
  }

  TJpgDec.setCallback(tft_output);
  uint16_t w = 0, h = 0;
  TJpgDec.getFsJpgSize(&w, &h, "/colour.jpg"); // Note name preceded with "/"
  TJpgDec.drawFsJpg(0, 0, "/colour.jpg");
}

The result: binary

Bodmer commented 4 years ago

I can see the following problems:

  1. The variable gray will end up being in the range 0-255 so a reasonable brightness threshold would be 128. Why use a random number as the threshold? I suggest this alternative line:
    uint8_t black_white = 128 < gray ? 1 : 0;
  2. The Jpeg encodes either 8x8 or 16x16 image blocks called Minimum Coding Units (MCUs), these are like tiles, so you cannot print a simple raster scan output. Look at the width and height to see the MCU size.
robotzero1 commented 4 years ago

Ah, of course.. the data comes as tiles. I was looking at this and later forgot. The random number was just a cheap dithering method that I copied from somewhere. I have the code working now but it's a bit rubbish. I basically count positions and add the 1s and 0s to strings in an array as the bitmap is decoded. Tomorrow I'll try to make it a 2 dimensional array and add Floyd-Steinburg dithering.


String rows[64];
int bitmap_line = 0;
int current_line = 0;
int tiles_number = 0;
int tiles_line = 0;

static bool tft_output(int16_t x, int16_t y, uint16_t w, uint16_t h, uint16_t* bitmap)
{
  for (int o = 0; o < w * h; o++) {
    uint16_t R = (((bitmap[o]) >> 11) & 0x1F) << 3; // extract R component
    uint16_t G = (((bitmap[o]) >> 5) & 0x3F) << 2; // extract G component
    uint16_t B = ((bitmap[o]) & 0x1F) << 3; // extract B component
    uint16_t gray = (R * 30 + G * 59 + B * 11) / 100; // formula from the link above

    String black_white = random(255) < gray ? "1" : "0";

    if (o % 16 == 0 && o > 0) {
      bitmap_line++ ;
    }

    tiles_line = tiles_number / 8;

    current_line = bitmap_line + (tiles_line * 16);

    rows[current_line] += black_white;

    if (o == 255) {
      bitmap_line = 0;
      tiles_number++;
    }

    if (tiles_number >= 32) {
      return 0;
    }
  }
  return 1;
}
robotzero1 commented 4 years ago

I seem to have it working now. Probably not the best coding but it appears to be outputting a dithered raster scan...

#include <TJpg_Decoder.h>
#define FS_NO_GLOBALS
#include <FS.h>
#include "SPIFFS.h" // ESP32 only

int gray_array [128][64];
int bitmap_row = 0;
int bitmap_column = 0;
int output_row = 0;
int output_column = 0;

static bool tft_output(int16_t x, int16_t y, uint16_t w, uint16_t h, uint16_t* bitmap)
{
  if ( y >= 64 ) return 0;

  for (int p = 0; p < w * h; p++) { //pixels in current tile
    uint16_t R = (((bitmap[p]) >> 11) & 0x1F) << 3; // extract R component
    uint16_t G = (((bitmap[p]) >> 5) & 0x3F) << 2; // extract G component
    uint16_t B = ((bitmap[p]) & 0x1F) << 3; // extract B component
    uint16_t gray = (R * 30 + G * 59 + B * 11) / 100; // formula from the link above

    if (p % 16 == 0 && p > 0) { // 16 x 16 tile
      bitmap_row++ ;
    }

    bitmap_column = p - (bitmap_row * 16);

    gray_array[x + bitmap_column][y + bitmap_row] = gray;

    if (p == 255) {
      bitmap_row = 0;
    }
  }
  return 1;
}

void setup()
{
  Serial.begin(115200);
  if (!SPIFFS.begin()) {
    Serial.println("SPIFFS initialisation failed!");
    while (1) yield(); // Stay here twiddling thumbs waiting
  }

  TJpgDec.setCallback(tft_output);

  uint16_t w = 0, h = 0;
  TJpgDec.getFsJpgSize(&w, &h, "/colour.jpg"); // Note name preceded with "/"
  TJpgDec.drawFsJpg(0, 0, "/colour.jpg");

  for (int y = 0; y < 63; y++) {
    for (int x = 0; x < 127; x++) {

      int oldpixel = gray_array[x][y];
      int newpixel = (oldpixel > 128) ? 255 : 0;
      gray_array[x][y] = newpixel;
      int quant_error  = oldpixel - newpixel;

      gray_array[x + 1][y    ] = gray_array[x + 1][y    ] + (quant_error * 7 / 16);
      gray_array[x - 1][y + 1] = gray_array[x - 1][y + 1] + (quant_error * 3 / 16);
      gray_array[x    ][y + 1] = gray_array[x    ][y + 1] + (quant_error * 5 / 16);
      gray_array[x + 1][y + 1] = gray_array[x + 1][y + 1] + (quant_error * 1 / 16);

      int black_white = (gray_array[x][y] > 128) ? 0 : 1;
      Serial.print(black_white);
    }
    Serial.println();
  }
}

void loop(){}
robotzero1 commented 4 years ago

I've developed it a bit further and it now outputs dithered frames from a camera nicely on a ssd1306 OLED over i2c (using an OLED library)

However it only works properly if I include the eSPI library as below. If not the image is garbled.

#include <TFT_eSPI.h>
TFT_eSPI tft = TFT_eSPI();

Any idea why that would be?

Bodmer commented 4 years ago

I assume there are no compile errors, in which case I do not know why...