Bodmer / TJpg_Decoder

Jpeg decoder library based on Tiny JPEG Decompressor
Other
222 stars 42 forks source link

JPEG Decode to BMP and Save to SD - Color Decoding Issue #61

Closed RSViewMan closed 1 year ago

RSViewMan commented 1 year ago

Bodmer - Your library is awesome!! -- Thank you for publishing it.

I am attempting to save a decoded JPEG to BMP565 directly to SD and am having an issue with the color mapping not being correct. Refer to the sample screen shots below. The original JPEG is 1200x1600 and the Scale factor is 4. I have tried different scales w/o any change in the output (other than size). I have hard coded everything below to alleviate any possibility of a looping error or boundary miscalculation. I thought maybe there was a byte misalignment in the file but the BMP Row Data ends on an even 4 byte boundary -- which appears to be a necessity of the BMP format. The tjpgdcnf.h file is set for 565 Decoding (default setting).

The JPEG renders to a TFT display w/o any issues using your TFT Library -- the quality is great and the picture is clear.

Any input you could give regarding how to correct the BMP file conversion would be appreciated. Thanks.

image image

//======================================================================
#include <TJpg_Decoder.h>
#include "FS.h"
#include "SD.h"
#include "SPI.h"

#define HSPI_SCK  14
#define HSPI_MISO  12
#define HSPI_MOSI  13
#define HSPI_CS  15

SPIClass hSPI = SPIClass(HSPI);

// Support funtion prototypes
bool tft_output(int16_t x, int16_t y, uint16_t w, uint16_t h, uint16_t* bitmap);
void loadFile(const char *name);

#define FIRMWARE_VERSION "v0.0.1"

unsigned long currentMillis = millis();
unsigned long previousMillis = 0;  // will store last time LED was updated
const long interval = 10000;  // interval at which to blink (milliseconds)
bool firstScanComplete = false;
File root;
File file;
unsigned long tftRenderLoops = 0;
bool decodeToBMPFile = false;
File bmpFile;
//unsigned int bmparray[304][402];
unsigned char decoderRow[4][300*2]; //Hold rows for decoding
//unsigned char decoderRow1[612*2]; //Hold rows for decoding
//unsigned char decoderRow2[612*2]; //Hold rows for decoding
//unsigned char decoderRow3[612*2]; //Hold rows for decoding
//unsigned char decoderRow4[612*2]; //Hold rows for decoding
int16_t xPrevious=0;
String sourceFile = "/IMG_20230129_144628.jpg";
String destFile = "/test3.bmp";

// variables
bool shouldReboot = false;            // schedule a reboot

// function defaults
//String listFiles(bool ishtml = false);
//String testFunction(bool ishtml = false);

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

  Serial.print("Firmware: "); Serial.println(FIRMWARE_VERSION);

  Serial.println("Booting ...");

  Serial.println("Configuring HSPI Port");
  hSPI.begin(HSPI_SCK, HSPI_MISO, HSPI_MOSI, HSPI_CS);
  delay(2000);    

  Serial.println("==============================");
  Serial.println("SD Card Configuration Start");
  Serial.println("==============================");
  Serial.println("Mounting SD Card...");
  if (!SD.begin(HSPI_CS,hSPI,80000000)) {
    Serial.println("Card Mount Failed");
    return;
  }

  Serial.println("==============================");
  Serial.println("SD Card Configuration End");
  Serial.println("==============================");

 //=======================================================================
 // Processing Steps:
 // 1) Open Jpeg File
 //  2) Request decode
 //  3) 
 //=======================================================================

  TJpgDec.setJpgScale(1);
  TJpgDec.setSwapBytes(true);
  //TJpgDec.setSwapBytes(false);
  TJpgDec.setCallback(bmp_output);

  //File jpegFile = SD.open("/0301141150.jpg");
  File jpegFile = SD.open(sourceFile);
  if (SD.exists(destFile)) {
    SD.remove(destFile);
    Serial.println("BMP Existed - File Deleted");
    bmpFile = SD.open(destFile,FILE_WRITE);  
    Serial.println("BMP Existed - File Recreated");
  } else {
    Serial.println("BMP Did not Exist - Created");
    bmpFile = SD.open(destFile,FILE_WRITE);
  }

  String strname = jpegFile.name();
  strname = "/" + strname;
  Serial.print("Loading JPEG Filename: ");Serial.println(strname);

  Serial.println("Starting JPEG Decoding");
  loadFile(strname.c_str());
  Serial.println("JPEG Decoding Complete");

  Serial.println("Closing JPEG and BMP File");  
  jpegFile.close();
  bmpFile.close();
  Serial.println("=================================");  
  Serial.println("Function Complete");  
  Serial.println("=================================");  
  }

void loop() {}

//====================================================================================
//                                    load_file
//====================================================================================
void loadFile(const char *name){
  // Time recorded for test purposes
  uint32_t t = millis();
  int i,j;
  unsigned char bytes[2] = {0,0};
  // Get the width and height in pixels of the jpeg if you wish
  uint16_t w = 0, h = 0, tws = 0, ths = 0;
  uint8_t scale;
  TJpgDec.getSdJpgSize(&w, &h, name); // Note name preceded with "/"

  scale = 4;
  TJpgDec.setJpgScale(scale);
  writeBMPFileHeader("str Not Implemented",w/scale,h/scale);  //Create the BMP File Header

    // Draw the image, top left at 0,0
  // This will trigger the callback function to write the BMP file
  TJpgDec.drawSdJpg(0, 0, name);

  Serial.println("JPEG to BMP Conversion Complete.");
  // How much time did rendering take
  t = millis() - t;

  //char buf[120];
  //sprintf(buf, "%s %dx%d 1:%d %u ms  Render Cycles: %u", name, w, h, scale, t,tftRenderLoops);
  //Serial.println(buf);
  tftRenderLoops = 0;
}

//====================================================================================
//                                    bmp_output
//====================================================================================
// This next function will be called during decoding of the jpeg file to
// render each block to the BMP File.
bool bmp_output(int16_t x, int16_t y, uint16_t w, uint16_t h, uint16_t* bitmap){
  int i,j;
  int bmpIndex=0;
  unsigned char bmbytes[2];
  unsigned char byte0[4],byte1[4];
  // Stop further decoding as image is running off bottom of screen
  //if ( y >= tft.height() ) return 0;

  //int mcu_pixels = w * h; //Number of Pixels in MCU
  //Serial.print("x = ");Serial.print(x);Serial.print("  y = ");Serial.print(y);Serial.print("  w = ");Serial.print(w);Serial.print("  h = ");Serial.println(h);

  decoderRow[0][(x*2)] = *bitmap >> 8;        //Pixel 0
  decoderRow[0][(x*2)+1] = *bitmap & 0xFF;    //Pixel 0
  decoderRow[0][(x*2)+2] = *(bitmap+1) >> 8;  //Pixel 1
  decoderRow[0][(x*2)+3] = *(bitmap+1) & 0xFF;//Pixel 1
  decoderRow[0][(x*2)+4] = *(bitmap+2) >> 8;  //Pixel 2
  decoderRow[0][(x*2)+5] = *(bitmap+2) & 0xFF;//Pixel 2
  decoderRow[0][(x*2)+6] = *(bitmap+3) >> 8;  //Pixel 3
  decoderRow[0][(x*2)+7] = *(bitmap+3) & 0xFF;//Pixel 3

  decoderRow[1][(x*2)] = *(bitmap+4) >> 8;    //Pixel 4
  decoderRow[1][(x*2)+1] = *(bitmap+4) & 0xFF;//Pixel 4
  decoderRow[1][(x*2)+2] = *(bitmap+5) >> 8;  //Pixel 5
  decoderRow[1][(x*2)+3] = *(bitmap+5) & 0xFF;//Pixel 5
  decoderRow[1][(x*2)+4] = *(bitmap+6) >> 8;  //Pixel 6
  decoderRow[1][(x*2)+5] = *(bitmap+6) & 0xFF;//Pixel 6
  decoderRow[1][(x*2)+6] = *(bitmap+7) >> 8;  //Pixel 7
  decoderRow[1][(x*2)+7] = *(bitmap+7) & 0xFF;//Pixel 7

  decoderRow[2][(x*2)] = *(bitmap+8) >> 8;    //Pixel 8
  decoderRow[2][(x*2)+1] = *(bitmap+8) & 0xFF;//Pixel 8
  decoderRow[2][(x*2)+2] = *(bitmap+9) >> 8;  //Pixel 9
  decoderRow[2][(x*2)+3] = *(bitmap+9) & 0xFF;//Pixel 9
  decoderRow[2][(x*2)+4] = *(bitmap+10) >> 8;  //Pixel 10
  decoderRow[2][(x*2)+5] = *(bitmap+10) & 0xFF;//Pixel 10
  decoderRow[2][(x*2)+6] = *(bitmap+11) >> 8;  //Pixel 11
  decoderRow[2][(x*2)+7] = *(bitmap+11) & 0xFF;//Pixel 11

  decoderRow[3][(x*2)] = *(bitmap+12) >> 8;    //Pixel 12
  decoderRow[3][(x*2)+1] = *(bitmap+12) & 0xFF;//Pixel 12
  decoderRow[3][(x*2)+2] = *(bitmap+13) >> 8;  //Pixel 13
  decoderRow[3][(x*2)+3] = *(bitmap+13) & 0xFF;//Pixel 13
  decoderRow[3][(x*2)+4] = *(bitmap+14) >> 8;  //Pixel 14
  decoderRow[3][(x*2)+5] = *(bitmap+14) & 0xFF;//Pixel 14
  decoderRow[3][(x*2)+6] = *(bitmap+15) >> 8;  //Pixel 15
  decoderRow[3][(x*2)+7] = *(bitmap+15) & 0xFF;//Pixel 15

  if(x == 296){
    bmpFile.write(decoderRow[0],sizeof(decoderRow[0]));
    bmpFile.write(decoderRow[1],sizeof(decoderRow[1]));
    bmpFile.write(decoderRow[2],sizeof(decoderRow[2]));
    bmpFile.write(decoderRow[3],sizeof(decoderRow[3]));

    if(y>=150 && y<= 200){
      Serial.println("");Serial.print("y:");Serial.print(y);Serial.print("  ");
      for (i=0;i<sizeof(decoderRow[0]);i++){
        Serial.printf("Byte[%d]: 0x%02x  ",i,decoderRow[0][i]);
      }
    }
  }

  xPrevious = x;  //Store the last value  

  // Return 1 to decode next block
  return 1;
}

void writeBMPFileHeader(char *str,int w,int h){
  byte VH, VL;
  int i, j = 0;

  //Create the File
  //bmpFile = SD.open(str, FILE_WRITE);

  unsigned char bmFlHdr[14] = {
    'B', 'M', 0, 0, 0, 0, 0, 0, 0, 0, 54, 0, 0, 0
  };
  // 54 = std total "old" Windows BMP file header size = 14 + 40

  unsigned char bmInHdr[40] = {
    40, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 16, 0
  };   
  // 40 = info header size
  //  1 = num of color planes
  // 16 = bits per pixel
  // all other header info = 0, including RI_RGB (no compr), DPI resolution

  unsigned long fileSize = 2ul * h * w + 54; // pix data + 54 byte hdr

  bmFlHdr[ 2] = (unsigned char)(fileSize      ); // all ints stored little-endian
  bmFlHdr[ 3] = (unsigned char)(fileSize >>  8); // i.e., LSB first
  bmFlHdr[ 4] = (unsigned char)(fileSize >> 16);
  bmFlHdr[ 5] = (unsigned char)(fileSize >> 24);

  bmInHdr[ 4] = (unsigned char)(       w      );
  bmInHdr[ 5] = (unsigned char)(       w >>  8);
  bmInHdr[ 6] = (unsigned char)(       w >> 16);
  bmInHdr[ 7] = (unsigned char)(       w >> 24);
  bmInHdr[ 8] = (unsigned char)(       h      );
  bmInHdr[ 9] = (unsigned char)(       h >>  8);
  bmInHdr[10] = (unsigned char)(       h >> 16);
  bmInHdr[11] = (unsigned char)(       h >> 24);

  bmpFile.write(bmFlHdr, sizeof(bmFlHdr));
  bmpFile.write(bmInHdr, sizeof(bmInHdr));
}

IMG_20230129_144628

Bodmer commented 1 year ago

I have not looked in detail at your sketch, but the bad output image suggests the image render program is correctly identifying the pixels as 16 bit and the pixel count is correct. However the colours are either 1. not being interpretted correctly by the image display program or 2. pixels have not beem endcoded correctly.

Black is being correctly interpretted but this is a value of 0 and ths to be expected. I would check if your display program can handle 565 encoded pixels as it may be interpretting them as 4444 ARGB. There online tools that will convert an image to 565 BMP format so maybe test your image display program with the converted output file from that: https://online-converting.com/image/convert2bmp/

If those steps do not reveal a problem then I would do a binary compare of a simple small image composed of solid red, green, blue, black and white blocks. You could then compare the output of your image converter with the output from the online tool.

RSViewMan commented 1 year ago

Bodmer - Thanks for the quick reply. I did a test using Red, Orange, Purple, Cyan and Blue JPEG files -- All tests created valid BMP files on the SD that accurately represented the source. I will continue to look at the data output. Thanks again.