bitbank2 / JPEGDEC

An optimized JPEG decoder for Arduino
Apache License 2.0
366 stars 47 forks source link

Nearly there. #26

Closed Sarah-C closed 2 years ago

Sarah-C commented 2 years ago

Hi Larry, Thanks for your great help last time - the wiki details were very clear.

I made the corrections you showed me, and made some space for decodeDither to do its stuff with: uint8_t ditherSpace[15360]; .......... (magic number = 960x * 16 bytes)

I'm tantalisingly close!

There's a 16bit value coming back, which I thought was 4 x 4 bits at first, but it wasn't. I think it's 2 x 8 bits. But the image cube stride still appears to be out, and I'm getting spurious long strips on the far right.

image

image

The task far exceeds my intelligence as I've spent all morning and a couple of hours last night working on it. If you could help, I'd really appreciate it!

I share all these M5Stack programs online, and I'll be sure to upload this to my github with a mention that it's an example of dithered e-ink so other people can see it - as a way of getting it out there before you get time to do your wiki. I checked your hover-over out, and it said you do optimising of systems, sweet! I love doing that too on my codepen pages, but it's more of a hobby than a profession.

I feel awkward putting this here, it's not a bug of your library - but I was desperate to see where I was going wrong, and couldn't find a more suitable channel of comms.

xx

.
.
       // Dithered decode!
        jpeg.open((const char *)file.name(), myOpen, myClose, myRead, mySeek, JPEGDraw);
        jpeg.setPixelType(FOUR_BIT_DITHERED);
        jpeg.decodeDither(ditherSpace, 0); // JPEG_DIV_NONE
.
.

int JPEGDraw(JPEGDRAW *pDraw)
{
  int x = pDraw->x;
  int y = pDraw->y;
  int w = pDraw->iWidth;
  int h = pDraw->iHeight;

  for (int16_t i = 0; i < w; i++){
    for (int16_t j = 0; j < h; j++){
      uint16_t col = pDraw->pPixels[(i + j * w) >> 2]; // Totally wrong, we get blocky results but correct pixel placement.

      /* It's not 4 bits x 4 moved to the most significant bits of a byte then......

      // 00001111 00001111
      //          1111<<<<  
      uint8_t col1 = (col << 4) & 0xf0;

      // 00001111 00001111
      //          0000    
      uint8_t col2 = col & 0xf0;

      // 00001111 00001111
      //     >>>> 1111
      uint8_t col3 = (col >> 4) & 0xf0;

      // 00001111 00001111
      // >>>>>>>> 0000
      uint8_t col4 = (col >> 8) & 0xf0;
      */

      uint8_t col1 = col;      
      canvas.drawPixel(x + i, y + j, 255 - col1);  // M5Stack 255 = white, not black.

    } // for j
  } // for i
  return 1;
} /* JPEGDraw() */
Sarah-C commented 2 years ago

Ohhh, line 3149 says: // pack new pixels into a byte That means it's putting two 4bit values into a byte, and the line: *d++ = cOut; is all uint8_t.

So it looks like I'm getting two 4 bit values packed into a byte, and the second byte I'm seeing data in is spurious memory contents as pPixels is always a uint8_t.

That just leaves me to work out if the width is a packed physical 2 pixels, or a logical 1.... I'm in work so I can't test it!

So for two packed 4 bit values, the two pixel colors then for the paper white (which needs a pixel color between 0 and 255) would be: uint8_t col1 = (col << 4) & 0x0f; uint8_t col2 = col & 0xf0;

Closer!

Sarah-C commented 2 years ago

back home..... about to have a look.

Sarah-C commented 2 years ago

Ooooooo - jpeg.decodeDither(ditherSpace, 3); decodes a 1/4 sized version of it just fine...... changing to jpeg.decodeDither(ditherSpace, 0);....... causes artefacts...

Sarah-C commented 2 years ago

Sorted! In JPEGDEC.h.... I had to increase the buffer size to deal with the 960 pixel width! I need to check what it needs to be after bed. 2800? I don't know. But this certainly works:

define MAX_BUFFERED_PIXELS 4096

So this is now the actual working version of the dithered decoding! Yay! =D (Oddly enough it was 2 bytes, 4 pixels worth in pPixels, after I was sure I'd seen a uint8_t..... )


/*
  https://github.com/wizche/flip-pics
  https://github.com/bitbank2/JPEGDEC
*/
#include <M5EPD.h>
#include <memory>
#include <stdexcept>
#include <SD.h>

#include <iostream>
#include <vector>
#include <sstream>
#include <string>
#include <JPEGDEC.h>

#define SLEEP_HOURS 1
bool dither = true;
int blockNumber = 0;

JPEGDEC jpeg;

M5EPD_Canvas canvas(&M5.EPD);
//M5EPD_Canvas canvasGrey(&M5.EPD);
//M5EPD_Canvas canvasDithered(&M5.EPD);

const char *DATA_FILE = "/data.txt";
uint32_t lastCount;
//uint8_t ditherSpace[15360];
uint8_t ditherSpace[30360];

//###############
// Callbacks for the jpeg decoding.

File myfile;

void * myOpen(const char *filename, int32_t *size) {
  myfile = SD.open(filename);
  *size = myfile.size();
  return &myfile;
}
void myClose(void *handle) {
  if (myfile) myfile.close();
}
int32_t myRead(JPEGFILE *handle, uint8_t *buffer, int32_t length) {
  if (!myfile) return 0;
  return myfile.read(buffer, length);
}
int32_t mySeek(JPEGFILE *handle, int32_t position) {
  if (!myfile) return 0;
  return myfile.seek(position);
}

//###############

bool has_suffix(const std::string &str, const std::string &suffix) {
  return str.size() >= suffix.size() &&
         str.compare(str.size() - suffix.size(), suffix.size(), suffix) == 0;
}

bool is_jpg(const std::string &filename) {
  return (has_suffix(filename, ".jpg") || has_suffix(filename, ".jpeg"));
}

bool is_valid_image(const std::string &filename) {
  return is_jpg(filename);
}

std::vector<std::string> split(const std::string &s, char delimiter) {
  std::vector<std::string> tokens;
  std::string token;
  std::istringstream tokenStream(s);
  while (std::getline(tokenStream, token, delimiter)) {
    tokens.push_back(token);
  }
  return tokens;
}

void storeCountSD(uint32_t c) {
  auto f = SD.open(DATA_FILE, "wb");
  uint8_t buf[4];
  buf[0] = c;
  buf[1] = c >> 8;
  buf[2] = c >> 16;
  buf[3] = c >> 24;
  auto bytes = f.write(&buf[0], 4);
  f.close();
}

uint32_t getCountSD() {
  uint32_t val;
  if (SD.exists(DATA_FILE)) {
    auto f = SD.open(DATA_FILE, "rb");
    val = f.read();
    f.close();
  }
  else {
    val = 0;
  }
  return val;
}

void drawTempHumidityBattery() {
  char batteryBuffer[20];
  uint32_t vol = M5.getBatteryVoltage();
  if (vol < 3300) {
    vol = 3300;
  }
  else if (vol > 4350) {
    vol = 4350;
  }
  float battery = (float)(vol - 3300) / (float)(4350 - 3300);
  if (battery <= 0.01) {
    battery = 0.01;
  }
  if (battery > 1) {
    battery = 1;
  }
  sprintf(batteryBuffer, "%d%%", (int)(battery * 100));
  char statusBuffer[256] = "CHARGING";
  M5.SHT30.UpdateData();
  float tem = M5.SHT30.GetTemperature();
  float hum = M5.SHT30.GetRelHumidity();
  sprintf(statusBuffer, "%2.2fC | %0.2f%% | %s", tem, hum, batteryBuffer);
  canvas.drawRightString(statusBuffer, 960, 0, 1);
}

int JPEGDraw(JPEGDRAW *pDraw){
  int x = pDraw->x;
  int y = pDraw->y;
  int w = pDraw->iWidth;
  int h = pDraw->iHeight;
  for (int16_t i = 0; i < w; i += 4) {
    for (int16_t j = 0; j < h; j++) {
      uint16_t col = pDraw->pPixels[ (i + (j * w) ) >> 2 ];
      uint16_t col1 = col & 0xf;
      uint16_t col2 = (col >> 4) & 0xf;
      uint16_t col3 = (col >> 8) & 0xf;
      uint16_t col4 = (col >> 12) & 0xf;
      canvas.drawPixel(x + i, y + j, 15 - col1);
      canvas.drawPixel(x + i + 1, y + j, 15 - col2);
      canvas.drawPixel(x + i + 2, y + j, 15 - col3);
      canvas.drawPixel(x + i + 3, y + j, 15 - col4);
    } // for j
  } // for i
  return 1;
} /* JPEGDraw() */

void load_image() {
  File root = SD.open("/");
  if (!root) {
    Serial.printf("Failed to open directory");
    return;
  }
  if (!root.isDirectory()) {
    Serial.printf("Not a directory");
    return;
  }

  String lastFile;
  lastCount = getCountSD();

  uint32_t currentCount = 0;
  while (true) {
    File file = root.openNextFile();
    if (!file) {
      Serial.printf("Reached end of directory, restart!\n");
      lastCount = 0;
      currentCount = 0;
      root.rewindDirectory();
      continue;
    }
    if (is_valid_image(file.name())) {
      currentCount++;
    }
    else {
      continue;
    }

    if (currentCount > lastCount) {
      if (is_jpg(file.name())) {
        Serial.printf("Filename %s\n", file.name());
        //canvas.drawJpgFile(SD, file.name(), 0, 0, 960, 540, 0, 0, JPEG_DIV_NONE);
        myfile = file;
        jpeg.open((const char *)file.name(), myOpen, myClose, myRead, mySeek, JPEGDraw);
        jpeg.setPixelType(FOUR_BIT_DITHERED);
        /*Decoder options
          JPEG_AUTO_ROTATE 1
          JPEG_SCALE_HALF 2
          JPEG_SCALE_QUARTER 4
          JPEG_SCALE_EIGHTH 8
          JPEG_LE_PIXELS 16
          JPEG_EXIF_THUMBNAIL 32
          JPEG_LUMA_ONLY 64
         */
        jpeg.decodeDither(ditherSpace, 0);
        jpeg.close();
      }
      else {
        Serial.printf("Something went wrong!\n");
        break;
      }
      lastCount = currentCount;
      //canvas.drawRightString(basename.c_str(), 960, 540, 1);
      canvas.drawRightString(file.name(), 960, 540, 1);
      break;
    }
  }
  storeCountSD(lastCount);
  drawTempHumidityBattery();
  canvas.pushCanvas(0, 0, UPDATE_MODE_GC16);
}

void setup() {
  M5.begin();
  M5.EPD.SetRotation(0);
  M5.EPD.Clear(1);
  M5.RTC.begin();
  canvas.createCanvas(960, 540);
  canvas.setTextSize(2);
  load_image();
}

//void loop() {}

void loop() {
  int tick = 200;
  while ((--tick) > 0) {
    delay(500);
    M5.update();

    if (M5.BtnL.wasPressed()) {
      Serial.println("Previous image.");
      tick = 0;
      uint32_t lastCount = getCountSD();
      lastCount -= 2;
      if ((lastCount) < 0) lastCount = 0;
      storeCountSD(lastCount);
    }

    if (M5.BtnP.wasPressed()) {
      Serial.println("Turning off.");
      canvas.drawRightString("Off", 960, 540, 1);
      canvas.pushCanvas(0, 0, UPDATE_MODE_GC16);
      M5.shutdown();
    }

    if (M5.BtnR.wasPressed()) {
      Serial.println("Next image.");
      tick = 0;
    }
  }
  blockNumber = 0;
  load_image();
}
bitbank2 commented 2 years ago

oh - so glad you got it sorted out. Can you share a photo of the correct output?

Sarah-C commented 2 years ago

Sure, just like this but on the M5Paper, you've seen the dwarf on the eagle's quality so I was lazy and figured the source image was going to be indistinguishable from the M5Paper image =) : test1

I noticed on some pictures a "fluffy" edge, and suspected pixel ordering in the packing was different to my codes.... I was able to confirm it with this image of 1 pixel wide lines at different angles. The line drawing algorithm never puts pixels adjacent unless horizontal or vertical. Making it easy to spot miss-aligned pixel ordering with a microscope.

test2

I realised at this point I made several mistakes reading through your dithering/packing code. It's interesting - I can plough through .Net API code with ease, and yet back to C and C++ I realise I'm missing the "detailed eye" needed of bits, bytes, and the plethora of byte/word/nibble sub-types. =) So what I read in your code form a uint8_t ACTUALLY ended up in the good-old uint16_t array the code sends out to the user calling it. I'm slowly getting my head back into it. Hours ago I has incorrectly assumed that second byte of data was just RAM noise. Incorrect for two reasons - 1: an array's nearly always going to never have "noise" bytes from unassigned locations in a situation like this. That would be highly odd. and 2: Data consistency in output would suggest the output's going to be the same format as other rendering modes. (for the most part, 565, 555, 22222222, 4 4 4 4, obviously ignoring 8 8 8.)