need help troubleshooting RGB565 to RGB888 conversion

Hi All,

I’m taking data from an ArduCAM mini and saving it to an SD card. Native format for the camera is RGB565, but the display I wish to send the image to (Adafruit ST7735) only takes 24 bit images.

I’ve gotten the RGB565 images to save with reasonable integrity, and am now trying to do an RGB565 to 888 conversion using the tips found elsewhere on the forum (http://forum.arduino.cc/index.php?topic=285303.0) and a header stolen from an image of the right size and depth.

The closest I’ve been able to come is an image that is darkened and has a slight magenta shift. white spots in the image (such as light sources) show up bright magenta. I don’t understand the bit shift in much detail, and the read order and direction of shift is confusing me, so any help troubleshooting would be most appreciated.

The portion of the code that does the conversion and write is:

      // RGB565 to RGB88 Conversion
      b = (((VH & 0x1F) * 527) + 23) >> 6;
      g = ((((((VH & 0xE0) >> 5) | ((VL & 0x03) << 3)) & 0x3F) * 259) + 33) >> 6;
      r = ((((VL >> 3) & 0x1F) * 527) + 23) >> 6; 

      /*         B5 = tbi1 & 0x1F;
                 G5 = (((tbi1 & 0xE0) >> 5) | ((tbi2 & 0x03) << 3)) & 0x1F;
                 R5 = (tbi2 >> 2) & 0x1F;
*/
      
     
      //Write image data to file
      outFile.write(b);
      outFile.write(g);
      outFile.write(r);

Full code is as follows:

//saves empty file with partially modified header
//try full modified header

//http://forum.arduino.cc/index.php?topic=285778.0
// inspired by http://sauerwine.blogspot.fr/2013/07/an-arduino-time-lapse-camera-using.html
// and http://www.arducam.com/how-arducam-use-a-external-trigger-from-a-sensor/

#include <SD.h>
#include <Wire.h>
#include <ArduCAM.h>
#include <SPI.h>
#include "memorysaver.h"
#include <avr/pgmspace.h>
#include <ov2640_regs.h>

// Duemilanove, etc., pin 11 = MOSI, pin 12 = MISO, pin 13 = SCK

#define BMPIMAGEOFFSET 54
#define SD_CS 9 
const int CS1 = 4;

// Constants that define the format of the picture taken
const int bmp_mode = 0;
const int mode = bmp_mode;

const char bmp_header[54] PROGMEM =
//header from 24bit 320 x 240 image
{
0x42, 0x4D, 0x38, 0x84, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x36, 0x00, 0x00, 0x00, 0x28, 0x00,
0x00, 0x00, 0x40, 0x01, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x00, 0x01, 0x00, 0x18, 0x00, 0x00, 0x00,
0x00, 0x00, 0x02, 0x84, 0x03, 0x00, 0x47, 0x30, 0x00, 0x00, 0x47, 0x30, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00
};


// Instanciation of the class ArduCAM
ArduCAM myCAM1(OV2640, CS1);


// Used in takePicture() to transfer the buffer of the ArduCAM into a BMP file
// inspired by http://sauerwine.blogspot.fr/2013/07/an-arduino-time-lapse-camera-using.html
void writeBMP(File outFile){
  
  char VH, VL;
  uint8_t temp,temp_last;
  int i, j, posn, nextNum;
  byte r, g, b;
  
    //Write the BMP header
  for( i = 0; i < 54; i++)
  {
    char ch = pgm_read_byte(&bmp_header[i]);
    outFile.write((uint8_t*)&ch,1);
  }
  
  //Read the first dummy byte from FIFO
  temp = myCAM1.read_fifo();
  //Read 320x240x2 byte from FIFO
  for(i = 0; i < 240; i++)
    for(j = 0; j < 320; j++)
    {
      VH = myCAM1.read_fifo();
      VL = myCAM1.read_fifo();
    
     
      /*RGB565 to RGB555 Conversion
      if (false) {
        VL = (VH << 7) | ((VL & 0xC0) >> 1) | (VL & 0x1f);
        VH = VH >> 1;
      }*/

       // RGB565 to RGB88 Conversion
      b = (((VH & 0x1F) * 527) + 23) >> 6;
      g = ((((((VH & 0xE0) >> 5) | ((VL & 0x03) << 3)) & 0x3F) * 259) + 33) >> 6;
      r = ((((VL >> 3) & 0x1F) * 527) + 23) >> 6; 

      /*         B5 = tbi1 & 0x1F;
                 G5 = (((tbi1 & 0xE0) >> 5) | ((tbi2 & 0x03) << 3)) & 0x1F;
                 R5 = (tbi2 >> 2) & 0x1F;
*/
      
     
      //Write image data to file
      outFile.write(b);
      outFile.write(g);
      outFile.write(r);      
  }
}

// IMAGE FILE GENERATION
// generates a name and returns the opened file
File imageFile(){
 // generates the filename
 char filename[13];
File outFile;

  strcpy(filename, "IMAGE00.BMP");
  for (int i = 0; i < 100; i++) {
    filename[5] = '0' + i/10;
    filename[6] = '0' + i%10;
    // create if does not exist, do not open existing, write, sync after write
    if (! SD.exists(filename)) {
      break;
    }}
   Serial.println(filename); 
   
 // opens the file
 outFile = SD.open(filename,O_WRITE | O_CREAT | O_TRUNC);
 if (! outFile){ 
 Serial.println("open file failed");
 } else {
 Serial.println("File opened sucessfully");
 }

 return outFile;
}

// ========= TAKE PICTURE ========
// simple to use function, which takes a picture
// either in JPEG (bugged) or BMP format in function 
// of the 'mode' constant

void takePicture(){
  Serial.println("Taking picture...");
  File outFile;

  uint8_t start_capture = 0;
  
  myCAM1.write_reg(ARDUCHIP_MODE, 0x00);
  
  // Initialisation
  // Warning : if you use set_format(JPEG) before Init, it will freeze
  myCAM1.set_format(BMP);
  Serial.print("Init ? ");
  myCAM1.InitCAM();
  Serial.println("OK");
  
  
  myCAM1.flush_fifo(); // clean ArduCAM buffer
  myCAM1.clear_fifo_flag(); // start capture
  myCAM1.start_capture();
  Serial.print("Waiting for capture..."); //waiting until capture is done
  while (!(myCAM1.read_reg(ARDUCHIP_TRIG) & CAP_DONE_MASK)) {
   delay(10); 
  }
  Serial.println("OK");
  
  // open a file onto the SD card
  outFile = imageFile();
    
    // writes the content of the ArduCAM buffer into the file, with the right
    // function, defined by the 'mode' value
    Serial.print("Buffering...");
    writeBMP(outFile);
    Serial.println("OK");
    
    // close the file and clean the flags and the buffers
    outFile.close(); 
    Serial.println("Capture finished");
    myCAM1.clear_fifo_flag();
    start_capture = 0;
    myCAM1.InitCAM();
}

// Function that waits for n seconds, writing a countdown on the Serial console
void countdown(int n = 10){
 while (n>0){
    Serial.print(n);
    Serial.println("... ");
    n=n-1;
    delay(1000);
  }
}

// ======== SETUP ========

void setup(){
  uint8_t vid,pid;
  uint8_t temp; 
  #if defined (__AVR__)
    Wire.begin(); 
  #endif
  #if defined(__arm__)
    Wire1.begin(); 
  #endif
  
  // begins the Serial communication
  Serial.begin(115200);
  Serial.println("ArduCAM Start!");
  pinMode(CS1, OUTPUT);
  SPI.begin();

 
  myCAM1.write_reg(ARDUCHIP_TEST1, 0x55);
  temp = myCAM1.read_reg(ARDUCHIP_TEST1);
  if(temp != 0x55)
  {
   Serial.println("SPI interface Error!");
   while(1);
  } 
  else {
  Serial.println("SPI All Good");
  }  
  
  // change MCU mode (?)
  myCAM1.write_reg(ARDUCHIP_MODE, 0x00);
  myCAM1.InitCAM();
  
  // SD card initialisation
  if (!SD.begin(SD_CS)) 
  {
    while (1); //If failed, stop here
    Serial.println("SD Card Error");
  }
  else
 {   Serial.println("SD Card detected!");
}}


void loop(){
 takePicture();
 delay(20000);
 
}

Thanks in advance :slight_smile:

The 565 designation means that the value contains 5 bits that define red, 6 that define green, and 5 that define blue. The 888 designation means that the value contains 8 bits for red, green, and blue.

Why are the 5 bits for red not sufficient? A simple scaling would seem sufficient if the 5 bits are not adequate as-is. But, the first step is to extract the 5 red bits, the 6 green bits, and the 5 blue bits.

Thanks for your reply PaulS, I'm not sure I've understood your advice, though.

I need to have the data saved as 24 bit (8, 8, 8 ) so the tft display can read the saved file from the sd card. From what I understand with the sketch above, the camera saves the value as 5, 6, 5, which totals two bytes- it reads these as VH (high byte) and VL (low byte). What the conversion code is intended to do is read the first 5 bits of VH (corresponding to the stored b value) and store them in unsigned integer b, the last 3 bits of VH and first 3 bits of VL (corresponding to the stored g value) and store them in integer g, and the last 5 bits of VL (corresponding to the stored value of r) and store them in integer r, then scale them up to 8 bits and write them as b, g, r to be read as 3 bytes of info (24 bit instead of 16 bit). If this is what's happening with the code, then I would have isolated the bits for r in the phrase (VL >> 3) & 0x1F, and the bits for g and b in the matching phrases?

Please let me know if I've misunderstood :)

Please let me know if I've misunderstood

I don't think that you have. What I think is the problem is that you are trying to extract the values and scale them in one step, without knowing if you are extracting the data properly.

Do the extraction, then print the values. If they are reasonable, scale and print the scaled values instead.

This way, you know where the failure is happening.

hmm, good point :slight_smile:

I didn’t think the hex colour values for the whole image would do me much good (because I have no idea how to go about translating them), but i tried saving the r5 g6 b5 values to the SD like this:

void writeBMP(File outFile){
  
  char VH, VL;
  uint8_t temp,temp_last;
  int i, j, posn, nextNum;
  unsigned int r5, g6, b5;
  
    //Write the BMP header
  for( i = 0; i < 54; i++)
  {
    char ch = pgm_read_byte(&bmp_header[i]);
    outFile.write((uint8_t*)&ch,1);
  }
  
  //Read the first dummy byte from FIFO
  temp = myCAM1.read_fifo();
  //Read 320x240x2 byte from FIFO
  for(i = 0; i < 240; i++)
    for(j = 0; j < 320; j++)
    {
      VH = myCAM1.read_fifo();
      VL = myCAM1.read_fifo();
    
   
b5 = VH & 0x1F;
g6 = ((VH & 0xE0) >> 5 | (VL & 0x03) << 3) & 0x3F;
r5 = (VL >> 3) & 0x1F;


     
      //Write image data to file
 
      outFile.write(b5);
      outFile.write(g6);
      outFile.write(r5);
      
      }

Interestingly, it only worked with the 24-bit header to give IMAGE01.bmp (I assume this has something to do with the way it saves – The 16-bit header gave green and blue garbage). The image is darkened, but still has the magenta staining issue. IMAGE02.bmp shows the result with the original scaling code. Any ideas? Have I got the bits wrong or am I shifting them the incorrect amount?

IMAGE01.jpg

IMAGE02.jpg

keep it simple ?

VH = myCAM1.read_fifo();
VL = myCAM1.read_fifo();

unsigned int val565 = VH*256 + VL;

byte r = (val565 & 0xF800) >> 8;  //  >> 11 << 3
byte b = (val565 & 0x07E0) >> 3;  // >> 5 << 2
byte g = (val565 & 0x001F) << 3;  // >> 0 << 3

thanks rob, i wish i could :slight_smile:

running your code:

VH = myCAM1.read_fifo();
VL = myCAM1.read_fifo();

unsigned int val565 = VH*256 + VL;

byte r = (val565 & 0xF800) >> 8;  //  >> 11 << 3
byte b = (val565 & 0x07E0) >> 3;  // >> 5 << 2
byte g = (val565 & 0x001F) << 3;  // >> 0 << 3
     
      //Write image data to file
      outFile.write(r);
      outFile.write(b);
      outFile.write(g);

I get IMAGE80 (attached). The reference image at RGB565 is there for you to look at. Incidentally, I’m running the camera at 3.3V, I’m not sure if that makes a difference?

IMAGE80.jpg

REF IMAGE 565.jpg

looks quite artistic (but hopelessly wrong :)

3.3v should not make the that difference

Can you change unsigned int val565 = VH*256 + VL;

into unsigned int val565 = VL*256 + VH;

?

It might be a high byte low byte swap..(big endian little endian)

I thought that might be the issue too, so I’ve been through a bunch of different iterations of different byte reading, scaling, combining and outwriting combinations. The swap you suggested above gives IMAGE34, and the others give similar data to this or to IMAGE80 from the last post.

I also had a go at forming a word (VH, VL) and shifting 11 and 5 using the post-scaling from the code I posted earlier but with no luck:

VH = myCAM1.read_fifo();
VL = myCAM1.read_fifo();
    
  word RGB565 = (VH, VL);

  b = (((RGB565 & 0x1f)* 527) + 23) >> 6;
  g = ((((RGB565 >> 5) & 0x3f)* 259) + 33) >> 6;
  r = ((((RGB565 >> 11) & 0x1f)* 527) + 23) >> 6;
    
      //Write image data to file
      outFile.write(b);
      outFile.write(g);
      outFile.write(r);

Images generated through this come up yellow/blue or red/cyan depending on the outwrite order for the bytes (eg IMAGE28)

I notice that the code I came across and posted earlier scales the r and b bytes differently to the g bytes, but yours applies *256 across the raw data – what’s the motivation behind this?

IMAGE28.jpg

IMAGE34.jpg

feathersinthread: ... I notice that the code I came across and posted earlier scales the r and b bytes differently to the g bytes, but yours applies *256 across the raw data -- what's the motivation behind this?

I want to merge the two 8 bit bytes into one 16 bit integer. The reason to do this is that the 565 coding is effectively 16 bits.

lets approach the problem in steps.

VH = myCAM1.read_fifo();
VL = myCAM1.read_fifo();

unsigned int val565 = VH*256 + VL;

byte r = (val565 & 0xF800) >> 8; 
byte b = 0;
byte g = 0;

Make an image with only the red channel.

Do the same for a blue channel and green channel.

does that deliver some insights?

Hi again, sorry for the delay in reply, mind on other matters :slight_smile:

If you’re still interested, I had a go at writing each of the colour channels and it was supremely helpful – the red and blue channels write fine if I invert the read order (little/big endian) and save the r and b data in the opposite order. The green channel, however, is being strange, which leads me to think there’s a problem with how the VH and VL bytes come together, since G lies over the byte boundary. IMAGE01 is a series of tests.

At first I thought it might be that it was writing backwards, so i introduced another integer for green:

//Read 320x240x2 byte from FIFO
  for(i = 0; i < 240; i++)
    for(j = 0; j < 320; j++)
    {
      VH = myCAM1.read_fifo();
      VL = myCAM1.read_fifo();
    
unsigned int val565a = VL*256 + VH;
unsigned int val565b = VH*256 + VL;

byte r = (val565a & 0xF800) >> 8;  //  >> 11 << 3
//byte r = 0;
byte g = (val565b & 0x07E0) >> 3;  // >> 5 << 2
//byte g = 0;
byte b = (val565a & 0x001F) << 3;  // >> 0 << 3
//byte b = 0;

      //Write image data to file
      outFile.write(b);
      outFile.write(g);
      outFile.write(r);
  }

IMAGE02 is the result- but as you can see, the colour is in the wrong place. I had a look back at images from IMAGE01, and it seems like… how should I put this… although the green is showing in the right place, its intensity is flipped. So that in the shadows, where the value should be close to 0, it is writing F instead; and vice versa. I’m not sure if this is a thing, but I thought it might explain why the midtones display fairly well, whilst the highlights appear magenta and the shadows green.
I wondered… and this is a stab in the dark… whether, rather than an endian issue, it might be an issue on the bit level (LSB/MSB)? Perhaps, to back up my train of thought, that would explain why i needed to invert the bytes on the RGB888 version, but not the RGB565 version, which reads VH, VL and saves VH, VL.

I tried to logic it out like this:

what i want is (in numbered bytes):
16 15 14 13 12 11 10 9 | 8 7 6 5 4 3 2 1 (VL | VH)
16 15 14 13 12 | 11 10 9 8 7 6 | 5 4 3 2 1 (R|G|B)
if it’s saving with an endian issue, it comes out:
8 7 6 5 4 3 2 1 | 16 15 14 13 12 11 10 9 (VL | VH)
so flipping the bytes gives me:
16 15 14 13 12 11 10 9 | 8 7 6 5 4 3 2 1 (VL | VH)
and, I think, I shouldn’t have to invert the write order too (it should read better whensaving r g b, not b r g) because the positions match up:
16 15 14 13 12 | 11 10 9 8 7 6 | 5 4 3 2 1 (R|G|B)

if it’s saving with a bit order issue, it comes out:
9 10 11 12 13 14 15 16 | 1 2 3 4 5 6 7 8 (VL | VH)
flipping the bytes gives me:
1 2 3 4 5 6 7 8 | 9 10 11 12 13 14 15 16 (VL | VH)
and flipping the write order gives me:
12 13 14 15 16 | 6 7 8 9 10 11 | 1 2 3 4 5 (R|G|B)
which should give me consistent errors over all three channels when the bytes weren’t symmetrical?

So I didn’t get very far. I feel like I’m missing something. What do you reckon? Any thoughts on fixes?

IMAGE02.jpg

Any thoughts on fixes?

Can you take three pictures - one of a red piece of paper, one of a blue piece of paper, and one of a green piece of paper?

Then, have the code simply print the values of r, g, b, VL, and VH, in HEX, all on one line, with spaces between the values, and show us (some of) the output.

if only green needs to be inverted you might use

byte g = 255 - ( (val565b & 0x07E0) >> 3); // >> 5 << 2

Woop, I made it! Thanks for all your help guys!

In the end, the problem was at the byte boundary. I tried inverting the green channel with Rob’s simplified code, but it didn’t solve the problem, so I went for a selective inversion for the almost pure G values (if g1 > 224, g = 255 - g) and got reasonable results, but also had to shift some of the lower values up by 16 bytes to reduce the magenta patches.

After trying to shift the g channel using the &0x001F (which I think is some sort of reference byte?), I went back to the original more complicated code and started shifting it separately for the high byte and low byte. After a bunch of trial and error- whammo! magic number was 0x0F on the low byte for real(ish) data:

      VH = myCAM1.read_fifo();
      VL = myCAM1.read_fifo();
    
       // RGB565 to RGB88 Conversion
      b = (((VH & 0x1F) * 527) + 23) >> 6;
      //b = 0;
      r = ((((VL >> 3) & 0x1F) * 527) + 23) >> 6; 
      //r = 0;
      g = ((((((VH & 0xF0) >> 5) | ((VL & 0x0F) << 3)) & 0x3F) * 259) + 33) >> 6;
      //g = 0;
      
      //Write image data to file
      outFile.write(b);
      outFile.write(g);
      outFile.write(r);

Still some issues at the byte boundary, but these are present in the 565 data too, so I’m satisfied.
Thanks again, couldn’t have done it without you!

IMAGE01.jpg

That code looks completely nuts - but it apparently works…

b = (((VH & 0x1F) * 527) + 23) >> 6;
The + 23 will be wiped away by the shift 6
The multiplication by 527 should be 512 which is a Left shift 9
together with the Right shift 6 that leaves a Left shift 3.

can you try

b = int(VH & 0x1F) << 3;

r = int(VL & 0xF8);

The green one will take some more thinking. but please try

That code looks completely nuts - but it apparently works

I did some RGB565 to/from RGB888 conversions, those "magic numbers" are there for a reason. Unlike a simple left shift by 3, the result of this calculation is the most accurate 8 bits representation of a 5 bits color.

For example, with R5 = B11111, you expect R8 to be B11111111 (which is the result of the magic calculation), not B11111000 (which is the result of left shift by 3).