Bodmer / JPEGDecoder

A JPEG decoder library
Other
220 stars 64 forks source link

Example code for UTFT, UTFT_SD.Jpeg does not work for LANDSCAPE #78

Open drp0 opened 1 year ago

drp0 commented 1 year ago

The example works perfectly for all of the supplied images in PORTRAIT. If myGLCD.InitLCD(LANDSCAPE); is used the image drawn is not correct. Is this a problem with renderJPEG ? Can a landscape alternative be provided?

David

drp0 commented 1 year ago

I am using a 320x240 screen, with utft:

UTFT myGLCD(ITDB32S_V2,38,39,40,41);  // latest Henning UTFT needed
// myGLCD(model, SDA, SCL, CS, RST [, RS] )
#define TFT_CS    40
#define SD_CS     53

The issue appears to revolve around renderJPEG and the alignment of the bit array pImg. I can reduce the lanscape error by rotating each pImg array clockwise by 90 degrees before pushing onto the lcd:

Test Image (320x240 px): cross

Portrait mode left: PL

Portrait mode right: PR

Landscape no rotation: L1

Landscape with rotated bitmaps: LR2

Some improvement shown so possibly on the right path.

My code: Includes image centering, for x=-1 or y =-1.

void jpegDraw(const char* filename, int x, int y) {
byte wstate;

  // Try to open requested file on SD card
File jpegFile = SD.open(filename, O_READ); 
  if (!jpegFile) {
  Serial.println("Jpeg file not found on SD card.");
  return;
  }
jpegFile.close();

Serial.print(F("Decoding  image '"));
Serial.print(filename);
Serial.println('\'');

// initialise the decoder, check compatibility and gain access to image information
boolean decoded = JpegDec.decodeSdFile(filename);

  if (decoded) {
  // print information about the image to the serial port
  jpegInfo();

  int X  = JpegDec.width;
  int Y  = JpegDec.height;
  int XX = myGLCD.getDisplayXSize();
  int YY = myGLCD.getDisplayYSize();
  bool sx         = X < XX;
  bool bx         = X > XX;
  bool sy         = Y < YY;
  bool by         = Y > YY;

    if (filename != "arduino.jpg") {
      // if (JpegDec.width > JpegDec.height) wstate = LANDSCAPE; else wstate = PORTRAIT;

      // if (page != wstate) {
      // myGLCD.InitLCD(wstate);
      // page = wstate;
      // }

      myGLCD.clrScr();                      // myGLCD.fillScr(0, 0, 0);
    }

  // render the image onto the screen at given coordinates
    if (y == -1) {                                          // centre image -x ok, -y ok
      if ( sy || by ) y = int( (YY - Y) / 2); else y = 0;   // if smaller than screen centre, else if bigger use -x, -y to centre
    }
    if (x == -1) {
      if ( sx || bx ) x = int( (XX - X) / 2); else x = 0; 
    }
  Serial.println("X, Y " + String(x) +", " + String(y));

  renderJPEG(x, y, filename);

  }else {
  Serial.println(F("Jpeg file format not supported."));
  myGLCD.clrScr();                                          // myGLCD.fillScr(0, 0, 0);
  }
}

//====================================================================================
//   Decode and render onto the TFT screen
//====================================================================================

void renderJPEG(int x, int y, const char* fname) {

// retrieve infomration about the image
uint16_t *pImg;
uint16_t mcu_w  = JpegDec.MCUWidth;
uint16_t mcu_h  = JpegDec.MCUHeight;
uint16_t sx     = JpegDec.width;
uint16_t sy     = JpegDec.height;
uint16_t W      = myGLCD.getDisplayXSize();
uint16_t H      = myGLCD.getDisplayYSize();
uint16_t xLim   = W - mcu_w;
uint16_t yLim   = H - mcu_h;
uint16_t mcu_pixels = mcu_w * mcu_h;

int result = 2 * mcu_pixels;
uint16_t pixels;
int mcu_x, mcu_y;
bool started = false;
uint8_t col_h;
uint8_t col_l;

uint32_t drawTime = millis();

uint8_t buff[result];
int i;

Serial.println("\nMCUX " + String(JpegDec.MCUx) +"," + String(JpegDec.MCUy));

  // read each MCU block until there are no more
  while ( JpegDec.read()) {
  pImg = JpegDec.pImage;                                    // save a pointer to the image block

    // uint16_t *pStart = pImg;
    // for (i = 0; i < result; i += 2) {                       // pImg to byte buff
    // buff[i]   = (*pStart) >> 8;                             // high byte
    // buff[i + 1] = (*pStart) & 0xFF;                         // low byte
    // pStart++;  
    // }

  mcu_x = JpegDec.MCUx * mcu_w + x;                         // position using block sent
  mcu_y = JpegDec.MCUy * mcu_h + y;
  //Serial.println(String(mcu_x ) + "," + String(mcu_y )); 

    if (page == PORTRAIT) {

      // test if points are on screen
      if ( (mcu_x >-1) && (mcu_x <= xLim) && (mcu_y > -1) && (mcu_y <= yLim) ) {
      started = true;
      digitalWrite(TFT_CS, LOW);                            // allow tft write       
      myGLCD.setXY(mcu_x, mcu_y, mcu_x +  mcu_w - 1, mcu_y + mcu_h - 1);
        //for ( i = 0; i < result; i += 2) myGLCD.LCD_Write_DATA(buff[i], buff[i + 1]);
      pixels = mcu_pixels;
        while (pixels--) {
        // Push each pixel to the TFT MCU area
        col_h = (*pImg) >> 8;                               // High byte
        col_l = (*pImg) & 0xFF;                             // Low byte
        pImg++;                                             // Increment pointer
        myGLCD.LCD_Write_DATA(col_h, col_l);                // Send a pixel colour to window
        }
      digitalWrite(TFT_CS, HIGH);
      }                                                     // end on screen test

    }else{                                                  // LANDSCAPE
      if ( (mcu_x >-1) && (mcu_x <= xLim) && (mcu_y > -1) && (mcu_y <= yLim) ) {
      started = true;
      digitalWrite(TFT_CS, LOW);                              // allow tft write 
      myGLCD.setXY(mcu_x, mcu_y, mcu_x +  mcu_w - 1, mcu_y + mcu_h - 1);
      // pixels = mcu_pixels;
      //   while (pixels--) {
      //   // Push each pixel to the TFT MCU area
      //   col_h = (*pImg) >> 8;                               // High byte
      //   col_l = (*pImg) & 0xFF;                             // Low byte
      //   pImg++;                                             // Increment pointer
      //   myGLCD.LCD_Write_DATA(col_h, col_l);                // Send a pixel colour to window
      //   }

      int n[mcu_pixels];
        for (i = 0; i < mcu_pixels; i ++) {                 // pImg to byte buff
        n[i] = (*pImg);
        pImg++;  
        }
      matrix(n, mcu_pixels, false);                         // rotate array true:left false:right
        for ( i = 0; i < mcu_pixels; i ++) {
        col_h = n[i] >> 8;                                  // High byte
        col_l = n[i] & 0xFF;                                // Low byte 
        myGLCD.LCD_Write_DATA(col_h, col_l);
        }

      digitalWrite(TFT_CS, HIGH); 
      }
    }                                                         // end else if (state == portrait)              

    if (started) {
      if ( mcu_y > yLim ) break;
    }
  }                                                         // end while ( JpegDec.read())

JpegDec.abort();

drawTime = millis() - drawTime;                             // calculate how long it took to draw the image
myGLCD.print(fname, CENTER, Ly);

Serial.print(F(  "Total render time was    : ")); Serial.print(drawTime); Serial.println(F(" ms\n"));
}

My array rotation code:

void matrix(int in[], int v, bool left) {
int size = sqrt(v);
int p;
int r = 0;
int c = 0;
int m[size][size];

  for (p = 0; p < v; p++) {                                 // 1d to 2d
  m[r][c] = in[p];
  c++;
    if (c == size) {
    c = 0;
    r++;
    } 
  }

  for (r = 0; r < size/2; r++) {                            // rotate
    for (c = r; c < (size - r - 1); c++) {
    int tmp = m[c][r];
    m[c][r] = m[size -r -1][c];
    m[size - r - 1][c] = m[size - c -1][size - r - 1];
    m[size - c - 1][size - r - 1] = m[r][size - c -1];
    m[r][size - c - 1] = tmp;
    }
  }

  if (left){                                                 // 2d to 1d
  p = 15;
    for (r = 0; r < size; r++) {
      for (c = 0; c < size; c++) {
      in[p] = m[r][c];
      p--;
      }
    }
  }else{
  p = 0;
    for (r = 0; r < size; r++) {
      for (c = 0; c < size; c++) {
      in[p] = m[r][c];
      p++;
      }
    }
  }
}

Rotating the bit image anticlockwise does not improve the result.
I am out of ideas, now.

Any suggestions? David

Bodmer commented 1 year ago

The problem is that UTFT does not use the hardware rotation features built into the display. This is possibly because it supports some displays that do not have this feature. This ultimately means that the display is kept in portrait orientation but x and y for each pixel are swappped (if required) and coordinates adjusted (if required) by subtracting from the width and height of display.

There are a few ways to deal with this:

  1. Extract each pixel from the decoded MCU block image (typically 8x8 or 16x16) then plot the pixels on the screen in the correct place.
  2. Rotate the image block and swap/adjust mcu_x, mcu_y (if required) n the myGLCD.setXY() function.
  3. Over-ride the UTFT library by setting the width and height in the desired rotation then force the hardware rotation feature of the display to the same rotation. In this case don't tell UTFT library to perform the rotation.
  4. Command UTFT to rotation 0, then display an image that has already been rotated (e.g. 0, 90, 18 or 270 to look correct), then swap back to the desired rotation for future graphics operations.

Personally I would avoid the UTFT library and use TFT_eSPI or Adafuit_GFX. Those libraries make thing simpler and use hardware rotation and have a performance advantage. Performing the rotations in software for every pixel has a performance impact, but does make the library "universal" which is as the author intended.

drp0 commented 1 year ago

Thanks for the reply. I have around 6 tft units, none of which appear to be compatible with the TFT_espi or adafruit GFX libraries.

Does landscape work using the library example with any hardware combinations that you have tested?

I am using a mega with a ITDB32S_V2 screen.

Re 1 & 2, As detailed above I have extracted the MCU block and rotated it, with some improvement with simple lined shapes. With detailed pictures, the screen image is barely recognisable.

Re 3 & 4, I had already tried the pre-rotating trick but this would not be a satisfactory solution.

Is JpegDec.MCUx properly tied into the screen alignment, or is it specifically returning portrait positions? At what point is JpegDec.MCUx updated? Does the JpegDec.read() function read in a linear book-reading fashion through the jpeg (in x by y blocks) ?

David

Bodmer commented 1 year ago

Intuitively, looking at the image where blocks are not rotated, a 90 degree anticlockwise rotation of each MCU block will correct the image. What does it look like?

drp0 commented 1 year ago

90 degree anticlockwise was a good call. I also had to rewrite the anticlockwise rotation section using google code. I have centring and positioning working, with options to auto-rotate the screen for portrait / landscape pictures. The matrix rotation has a time overhead of less than 1s on a 320x240 image. Bearing in mind that portrait mode crops the pushed area, it is probably less than this. Here is the full code. You are welcome to use it as you see fit.

David

main:

/*
UTFT_SD_Jpeg4.ino
Modified jpeg decoder
Supports PORTRAIT and LANDSCAPE
D.R.Patterson
3/5/2023

 This sketch draws Jpeg files stored on an SD card on a TFT screen, it is based on the UTFT_Bitmap
 by Henning Karlsen.

 web: http://www.RinkyDinkElectronics.com/

 The demo should also run on an Arduino Mega, but it will be slower

  By default the UTFT library does not configure the Gamma curve settings for the ILI9341 TFT,
  so photo images may not render well.  To correct this ensure the set Gamma curve section
  in initlcd.h (library folder UTFT\tft_drivers\ili9341\s5p\initlcd.h) is NOT commented out.

  You can generate your own Jpeg images from digital photographs by cropping and resizing
  by using commonly available picture/image editors such as Paint or IrfanView.

  The latest JPEGDecoder library can be found here:
  https://github.com/Bodmer/JPEGDecoder

  Information on JPEG compression can be found here:
  https://en.wikipedia.org/wiki/JPEG 

====================================================================================
  libraries
====================================================================================
*/

#include <SPI.h>
#include <SD.h>  // Use the Arduino IDE built-in SD library

#include <UTFT.h>

UTFT myGLCD(ITDB32S_V2,38,39,40,41);                        // For new screens latest Henning UTFT needed
// myGLCD(model, SDA, SCL, CS, RST [, RS] )
#define TFT_CS    40                                        // Chip Select for TFT
extern uint8_t SmallFont[];                                 // Declare which fonts we will be using 

#include <JPEGDecoder.h>                                    // JPEG decoder library

// SD card connects to hardware SPI pins MOSI, MISO and SCK and the following chip select
#define SD_CS 53                                            // Chip Select for SD card
#define defaultselect 53                                    // Mega default select

char * files[] = {"cross.jpg", "david.jpg", "tiger.jpg", "/xsail/mast2.jpg", "Baboon20.jpg", "Baboon40.jpg", "EagleEye.jpg", "lena20k.jpg", "Mouse480.jpg" };

unsigned int Ly;
unsigned int N = sizeof(files)/sizeof(*files);              // Number of filenames

// ===================================================================================
// Options
// ===================================================================================

byte page             = LANDSCAPE;                           // PORTRAIT / LANDSCAPE
const bool autoOrient = true;                                // if true align screen to picture orientation
const bool doOver     = false;                               // add arduino logo
int posx              = -1;                                  // x position, -1 for centred
int posy              = -1;                                  // y position, -1 for centred
byte index            = 0;                                   // index of 1st file to be displayed

//====================================================================================
//  setup
//====================================================================================

void setup() {

Serial.begin(115200);
delay(500);

pinMode(SD_CS, OUTPUT);
    if (SD_CS != defaultselect) pinMode(defaultselect, OUTPUT);
  // Initialise the SD card interface, check it is OK
Serial.print(F("Initialising SD card..."));
  if (!SD.begin(SD_CS)){
  Serial.println(F("failed!"));
  return;
  }else Serial.println(F("SD ok"));

myGLCD.InitLCD(page);
myGLCD.clrScr();
myGLCD.setFont(SmallFont);
myGLCD.setColor(VGA_YELLOW);
Ly = myGLCD.getDisplayYSize() - 15;

//mtest(); while(true) {;}                                    // 90 degree a-clock matrix rotation test

Serial.println(F("Ready"));
}

void loop() {

jpegDraw(files[index], posx, posy);                         // -1, -1: centred or 0, 0 or x, y
  if (doOver) {
  delay(1000);
  // draw Arduino logo at a random position within the screen area
  jpegDraw( "arduino.jpg", random(myGLCD.getDisplayXSize() - 160), random(myGLCD.getDisplayYSize() - 128) );
  }
index += 1;
  if (index == N) index = 0;

delay(4000);
}

Jpeg_utilities.ino:

/*====================================================================================
  This sketch contains support functions to render the Jpeg images.

  Created by Bodmer 15th Jan 2017
  ==================================================================================*/

//====================================================================================
//  Open a Jpeg image file and displays it at the given coordinates.
//====================================================================================

void jpegDraw(const char* filename, int x, int y) {
byte wstate;

  // Try to open requested file on SD card
File jpegFile = SD.open(filename, O_READ); 
  if (!jpegFile) {
  Serial.println("Jpeg file not found on SD card.");
  return;
  }
jpegFile.close();

Serial.print(F("Decoding  image '"));
Serial.print(filename);
Serial.println('\'');

// initialise the decoder, check compatibility and gain access to image information
boolean decoded = JpegDec.decodeSdFile(filename);

  if (decoded) {
  // print information about the image to the serial port
  jpegInfo();

  int X  = JpegDec.width;
  int Y  = JpegDec.height;

    if (filename != "arduino.jpg") {
      if (autoOrient) {
        if (JpegDec.width > JpegDec.height) wstate = LANDSCAPE; else wstate = PORTRAIT;

        if (page != wstate) {
        myGLCD.InitLCD(wstate);
        page = wstate;
        myGLCD.setFont(SmallFont);
        myGLCD.setColor(VGA_YELLOW);
        Ly = myGLCD.getDisplayYSize() - 15;
        }
      }
    myGLCD.clrScr();                                        // myGLCD.fillScr(0, 0, 0);
    }

  int XX = myGLCD.getDisplayXSize();
  int YY = myGLCD.getDisplayYSize();
  bool sx         = X < XX;
  bool bx         = X > XX;
  bool sy         = Y < YY;
  bool by         = Y > YY;

  // render the image onto the screen at given coordinates
    if (y == -1) {                                          // centre image -x ok, -y ok
      if ( sy || by ) y = int( (YY - Y) / 2); else y = 0;   // if smaller than screen centre, else if bigger use -x, -y to centre
    }
    if (x == -1) {
      if ( sx || bx ) x = int( (XX - X) / 2); else x = 0; 
    }
  Serial.println("X, Y " + String(x) +", " + String(y));

  renderJPEG(x, y, filename);

  }else {
  Serial.println(F("Jpeg file format not supported."));
  myGLCD.clrScr();                                          // myGLCD.fillScr(0, 0, 0);
  }
}

//====================================================================================
//   Decode and render onto the TFT screen
//====================================================================================

void renderJPEG(int x, int y, const char* fname) {

// retrieve infomration about the image
uint16_t *pImg;
uint16_t mcu_w  = JpegDec.MCUWidth;
uint16_t mcu_h  = JpegDec.MCUHeight;
uint16_t sx     = JpegDec.width;
uint16_t sy     = JpegDec.height;
uint16_t W      = myGLCD.getDisplayXSize();
uint16_t H      = myGLCD.getDisplayYSize();
uint16_t xLim   = W - mcu_w;
uint16_t yLim   = H - mcu_h;
uint16_t yQuit  = H - 2;
uint16_t mcu_pixels = mcu_w * mcu_h;
uint16_t pixels;
int16_t mcu_x, mcu_y;
bool started = false;
uint8_t col_h;
uint8_t col_l;
uint16_t incx, incy, lastY; 
int16_t residue;
int i;

uint32_t drawTime = millis();
digitalWrite(TFT_CS, LOW);                                  // allow tft write

  // read each MCU block until there are no more
  while ( JpegDec.read()) {
  pImg = JpegDec.pImage;                                    // save a pointer to the image block

  mcu_x = JpegDec.MCUx * mcu_w + x;                         // position using block sent
  mcu_y = JpegDec.MCUy * mcu_h + y;                         // Serial.println(String(mcu_x ) + "," + String(mcu_y )); 

  incx = mcu_w;  
    if (mcu_x > xLim) {                                     // test if approaching screen width and reduce push window
    residue = W - mcu_x;
      if (residue > 0) incx = residue;
    }

  incy = mcu_h;  
    if (mcu_y > yLim) {                                     // test if approaching screen height and reduce push window
    residue = H - mcu_y;
      if (residue > 0) incy = residue;
    }

    // test if points are on screen
    if ( (mcu_x >-1) && (mcu_x < W) && (mcu_y > -1) && (mcu_y < H) ) {
    started = true;

    lastY = mcu_y + incy - 1;
    myGLCD.setXY(mcu_x, mcu_y, mcu_x +  incx - 1, lastY);

      if (page == PORTRAIT) {

      pixels = mcu_pixels;
        while (pixels--) {                                  // Push each pixel to the TFT MCU area
        col_h = (*pImg) >> 8;                               // High byte
        col_l = (*pImg) & 0xFF;                             // Low byte
        myGLCD.LCD_Write_DATA(col_h, col_l);                // Send a pixel colour to window
        pImg++;                                             // Increment pointer
        }

      }else{                                                // LANDSCAPE

      int n[mcu_pixels];                                    // push pixels from array
        for (i = 0; i < mcu_pixels; i ++) {                 // pImg to byte buff
        n[i] = (*pImg);
        pImg++;  
        }

      Rmatrix(n, mcu_pixels);                               // rotate array n 90 degrees anti clockwise
        for (i = 0; i < mcu_pixels; i ++) {
        col_h = n[i] >> 8;                                  // High byte
        col_l = n[i] & 0xFF;                                // Low byte 
        myGLCD.LCD_Write_DATA(col_h, col_l);
        }
      }                                                     // end else if (state == portrait)              
    }                                                       // end on screen test  

    if (started) {
      if ( mcu_y > yQuit ) break;
    }
  }                                                         // end while ( JpegDec.read())

JpegDec.abort();
myGLCD.setXY(0, 0, W-1, H-1); digitalWrite(TFT_CS, HIGH);
drawTime = millis() - drawTime;                             // calculate how long it took to draw the image

myGLCD.print(fname, CENTER, Ly);

Serial.print(F(  "Total render time was    : ")); Serial.print(drawTime); Serial.println(F(" ms\n"));
}

//====================================================================================
//   Open a Jpeg file and send it to the Serial port in a C array compatible format
//====================================================================================
void createArray(const char *filename) {

// Open the named file
File jpgFile = SD.open( filename, O_READ);                  // get file handle reference for SD library

  if ( !jpgFile ) {
  Serial.print("ERROR: File \""); Serial.print(filename); Serial.println ("\" not found!");
  return;
  }

uint8_t data;
byte line_len = 0;
Serial.println("");
Serial.println("// Generated by a JPEGDecoder library example sketch:");
Serial.println("// https://github.com/Bodmer/JPEGDecoder\n");
Serial.println("#if defined(__AVR__)");
Serial.println("  #include <avr/pgmspace.h>");
Serial.println("#endif");
Serial.println("");
Serial.print  ("const uint8_t ");
  while (*filename != '.') Serial.print(*filename++);
Serial.println("[] PROGMEM = {");                           // PROGMEM added for AVR processors, it is ignored by Due

  while ( jpgFile.available()) {
  data = jpgFile.read();
  Serial.print("0x"); if (abs(data) < 16) Serial.print("0");
  Serial.print(data, HEX); Serial.print(",");               // Add value and comma
  line_len++;
    if ( line_len >= 32) {
    line_len = 0;
    Serial.println();
    }
  }

Serial.println("};\r\n");
jpgFile.close();
}

//====================================================================================

//====================================================================================
//   Print information about the decoded Jpeg image
//====================================================================================

void jpegInfo() {
Serial.println(F("================="));
Serial.println(F(" JPEG image info"));
Serial.println(F("================="));
Serial.print(F("  Width      :")); Serial.println(JpegDec.width);
Serial.print(F("  Height     :")); Serial.println(JpegDec.height);
Serial.print(F("  Components :")); Serial.println(JpegDec.comps);
Serial.print(F("  MCU / row  :")); Serial.println(JpegDec.MCUSPerRow);
Serial.print(F("  MCU / col  :")); Serial.println(JpegDec.MCUSPerCol);
Serial.print(F("  Scan type  :")); Serial.println(JpegDec.scanType);
Serial.print(F("  MCU width  :")); Serial.println(JpegDec.MCUWidth);
Serial.print(F("  MCU height :")); Serial.println(JpegDec.MCUHeight);
Serial.println(F("================="));
}

matrix.ino:


void Rmatrix(int in[], int v) {

// https://www.geeksforgeeks.org/inplace-rotate-square-matrix-by-90-degrees/

int size = sqrt(v);
int j = 0;
int i = 0;
int p, start, end, tmp;

int m[size][size];                                          // local 2d array

  for (p = 0; p < v; p++) {                                 // 1d to 2d
  m[j][i] = in[p];
  i++;
    if (i == size) {
    i = 0;
    j++;
    } 
  }

  for (i = 0; i < size; i++) {
  start = 0;                                                // Initialise start and end index
  end = size - 1;

    // Till start < end, swap the element at start and end index
    while (start < end) {
    // Swap the element
    tmp = m[i][start];
    m[i][start] = m[i][end];
    m[i][end] = tmp;
    start++;                                                // Increment start and
    end--;                                                  // decrement end for next pair of swapping
    }
  }

  for (int i = 0; i < size; i++) {                          // Perform Transpose
    for (int j = i; j < size; j++) {
    tmp = m[i][j];
    m[i][j] = m[j][i];
    m[j][i] = tmp;
    }
  }

p = 0;                                                      // 2d to 1d for left rotation
  for (j = 0; j < size; j++) {
    for (i = 0; i < size; i++) {
    in[p] = m[j][i];
    p++;
    }
  }
}

void mtest(){
// matrix rotation test
int q[16] = {1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16};
Rmatrix(q, 16);
  for (int p = 0; p < 16; p++) {
  Serial.print("\t"); Serial.print(q[p]);
  }
Serial.println();
}