How to take a snapshot?

Description

I’ve been following the documentation as I want to take a snapshot on device and save it to an SD card.
So far I’ve tried to use lv_snapshot_take but it returns NULL all the time as there is not enough RAM in the region it is running in on the Teensy (RAM1)
So I tried to use lv_snapshot_take_to_buf but the watchdog on the Teensy is resetting the device due to a null pointer.

Below is a function I wrote to take a snapshot and save it to SD.
I call it where I want to take a snapshot and pass the lv_scr_act() object to it to take a full screen snapshot.

Can anyone provide an example of how they used lv_snapshot_take_to_buf to make sure i am using it correctly?

What MCU/Processor/Board and compiler are you using?

Teensy MicroMod (IMXRT1062)

What LVGL version are you using?

v8.1

Code to reproduce

#include "../lib/lvgl/lvgl.h"
#include "SdFat.h"
#include "Arduino.h"
SdFs sd;
FsFile file;
// Use Teensy SDIO
#define SD_CONFIG  SdioConfig(FIFO_SDIO)
// Size to log 10 byte lines at 25 kHz for more than ten minutes.

#define LOG_FILE_SIZE (480*320*3) // 480px * 320px * 3bytes per pixle
#define LOG_FILENAME "snapshot.c"

DMAMEM uint16_t buff[LOG_FILE_SIZE/2]; //Buffer in RAM2 to place the snapshot
lv_img_dsc_t * img;

void takeSnapshot(lv_obj_t * screenshot){
    arm_dcache_flush((uint16_t*)buff, sizeof(buff)); // always flush cache after writing to DMAMEM variable that will be accessed by DMA
    lv_res_t snap = lv_snapshot_take_to_buf(screenshot, LV_IMG_CF_TRUE_COLOR_ALPHA, img, &buff, sizeof(buff));
    if (snap == LV_RES_INV){
        Serial.println("Snapshot failed");
    }
    else{
    // Initialize the SD.
  if (!sd.begin(SD_CONFIG)) {
    sd.initErrorHalt(&Serial);
  }
  // Open or create file - truncate existing file.
  if (!file.open(LOG_FILENAME, O_RDWR | O_CREAT | O_TRUNC)) {
    Serial.println("open failed\n");
    return;
  }
  // File must be pre-allocated to avoid huge
  // delays searching for free clusters.
  if (!file.preAllocate(LOG_FILE_SIZE)) {
     Serial.println("preAllocate failed\n");
     file.close();
     return;
  }
  file.write((uint16_t*)buff);
  file.close();
  Serial.println("Snapshot saved");
    }

}



1 Like

Anyone? No…?

Hi @reso ,

I am not familiar with the Teensy or the lv_snapshot feature but looking at your code I think I can see an error, the image descriptor lv_img_dsc_t * img; shouldn’t be a pointer so this will cause your NULL pointer crash. Taking your code it might be worth trying it like this…

#include "../lib/lvgl/lvgl.h"
#include "SdFat.h"
#include "Arduino.h"
SdFs sd;
FsFile file;
// Use Teensy SDIO
#define SD_CONFIG  SdioConfig(FIFO_SDIO)
// Size to log 10 byte lines at 25 kHz for more than ten minutes.

#define LOG_FILE_SIZE (480*320*3) // 480px * 320px * 3bytes per pixle
#define LOG_FILENAME "snapshot.c"

DMAMEM uint16_t buff[LOG_FILE_SIZE/2]; //Buffer in RAM2 to place the snapshot
lv_img_dsc_t img;  // I believe this shouldn't be a pointer!!

void takeSnapshot(lv_obj_t * screenshot){
    arm_dcache_flush((uint16_t*)buff, sizeof(buff)); // always flush cache after writing to DMAMEM variable that will be accessed by DMA
 // In this line we add address of operator '&' to 'img'
    lv_res_t snap = lv_snapshot_take_to_buf(screenshot, LV_IMG_CF_TRUE_COLOR_ALPHA, &img, &buff, sizeof(buff)); 
    if (snap == LV_RES_INV){
        Serial.println("Snapshot failed");
    }
    else{
    // Initialize the SD.
  if (!sd.begin(SD_CONFIG)) {
    sd.initErrorHalt(&Serial);
  }
  // Open or create file - truncate existing file.
  if (!file.open(LOG_FILENAME, O_RDWR | O_CREAT | O_TRUNC)) {
    Serial.println("open failed\n");
    return;
  }
  // File must be pre-allocated to avoid huge
  // delays searching for free clusters.
  if (!file.preAllocate(LOG_FILE_SIZE)) {
     Serial.println("preAllocate failed\n");
     file.close();
     return;
  }
  file.write((uint16_t*)buff);
  file.close();
  Serial.println("Snapshot saved");
    }

}

I would also check the buffer size carefully as it looks like it might be incorrect, if it is lv_snapshot_take_to_buf() should return LV_RES_INV so if that happens I would adjust the size of the buffer accordingly. If you have a debugger you can step into the lv_snapshot_take_to_buf() and see what value it is expecting for length if you are unsure about the size of the buffer.

I hope that is the answer.

Kind Regards,

Pete

@pete-pjb thank you for your response and suggestions.
I checked the size of the object that should be returned by calling lv_snapshot_buf_size_needed and it returned 460.8Kb. Being that the buffer value is a 2 byte unsigned integer, it needs to be half of that, no? Or should I just save an an uint8_t?
It’s not clear what kind of object is retuned to the buffer.

Hi @reso,

Yes I agree with your comments. The buffer can be specified anyway you want as long as the number of bytes is correct :slight_smile:

Kind Regards,

Pete

Thanks @pete-pjb that worked!
Now I need to tinker with getting it converted to a bitmap

I have been able to write it as a bitmap to the SD card but the image is flipped and mirrored, as well as mostly blue.
I know the bitmap config is wrong as it’s set up for RGB555 and not 565, and the pixels are definitely being read in the incorrect order

1 Like

You can set the bitmap height to the negative value to make the client render it mirrored. The BMP spec orders the pixel rows bottom to top by default, but you can also put them top to bottom if you specify a negative height.

I’m using the BITMAPV2INFOHEADER BMP format while setting BI_BITFIELDS to 3 for RGB bitmasks like so:

struct bmp_header_t
{
    uint32_t bfSize;
    uint32_t bfReserved;
    uint32_t bfOffBits;

    uint32_t biSize;
    int32_t biWidth;
    int32_t biHeight;
    uint16_t biPlanes;
    uint16_t biBitCount;
    uint32_t biCompression;
    uint32_t biSizeImage;
    int32_t biXPelsPerMeter;
    int32_t biYPelsPerMeter;
    uint32_t biClrUsed;
    uint32_t biClrImportant;

    uint32_t bdMask[3]; // RGB
};

/** Set a Bitmap Header.
 *
 * Initializes a header in BMP format for the size of the screen.
 *
 * @note: send header before refreshing the whole screen
 *
 **/
static void gui_get_bitmap_header(uint8_t* buffer, size_t bufsize)
{
    lv_obj_t* scr     = lv_disp_get_scr_act(NULL);
    lv_coord_t width  = lv_obj_get_width(scr);
    lv_coord_t height = lv_obj_get_height(scr);

    const char* bm = "BM";
    memcpy(buffer, bm, strlen(bm));
    buffer += strlen(bm);

    // Bitmap file header
    bmp_header_t* bmp = (bmp_header_t*)buffer;
    bmp->bfSize       = (uint32_t)(width * height * LV_COLOR_DEPTH / 8);
    bmp->bfReserved   = 0;
    bmp->bfOffBits    = bufsize;

    // Bitmap information header
    bmp->biSize          = 40;
    bmp->biWidth         = width;
    bmp->biHeight        = -height;
    bmp->biPlanes        = 1;
    bmp->biBitCount      = LV_COLOR_DEPTH;
    bmp->biCompression   = 3; // BI_BITFIELDS
    bmp->biSizeImage     = bmp->bfSize;
    bmp->biXPelsPerMeter = 2836;
    bmp->biYPelsPerMeter = 2836;
    bmp->biClrUsed       = 0; // zero defaults to 2^n
    bmp->biClrImportant  = 0;

    // BI_BITFIELDS
    bmp->bdMask[0] = 0xF800; // Red bitmask  : 1111 1000 | 0000 0000
    bmp->bdMask[1] = 0x07E0; // Green bitmask: 0000 0111 | 1110 0000
    bmp->bdMask[2] = 0x001F; // Blue bitmask : 0000 0000 | 0001 1111
}

If you write such header before dumping the LVGL pixels to the file, it should render fine on the client.
I’m using it to take local screenshots on flash and also send the screenshot to a browser via HTTP.

1 Like

Thanks for the tip!
How do you suggest I dump the snapshot code to the card?
Just write the full array as is? Or loop through it with something like this?

uint16_t readPixA(int x, int y) { // get pixel color code in rgb565 format
    static long pos = 0;
    uint16_t r = (buff[pos+1] << 8) | buff[pos];
    pos+=3;
    return r;
}


void writeImageToFile(){
 byte VH, VL;
 int w = 480;
 int h = 320;
 int i, j = 0;

 for (i = h; i > 0; i--) {
            for (j = 0; j < w; j++) {
            
            uint16_t rgb = readPixA(j,i); // get pix color in rgb565 format
            VH = ((uint8_t)rgb & 0xFF00) >> 8; // High Byte
            VL = (uint8_t)rgb & 0x00FF;        // Low Byte
            
            //RGB565 to RGB555 conversion... 555 is default for uncompressed BMP
            //this conversion is from ...topic=177361.0 and has not been verified
            VL = (VH << 7) | ((VL & 0xC0) >> 1) | (VL & 0x1f);
            VH = VH >> 1;
            
            //Write image data to file, low byte first
            file.write(VL);
            file.write(VH);
            }
        }
......
}
1 Like

Here is my full snapshot code with the function+struct provided by @fvanroie

void takeSnapshot(lv_obj_t * screenshot){
    //arm_dcache_flush((uint8_t*)buff, sizeof(buff)); // always flush cache after writing to DMAMEM variable that will be accessed by DMA
 // In this line we add address of operator '&' to 'img'
    Serial.printf("Buffer size: %d, Snapshot size required: %u \n", LOG_FILE_SIZE, lv_snapshot_buf_size_needed(lv_scr_act(), LV_IMG_CF_TRUE_COLOR_ALPHA));
    lv_res_t snap = lv_snapshot_take_to_buf(screenshot, LV_IMG_CF_TRUE_COLOR_ALPHA, &snapshot_img, &buff, sizeof(buff)); 
    if (snap == LV_RES_INV){
        Serial.println("Snapshot failed");
    }
    else{
        Serial.println("Snapshot Success");
    uint8_t header[128];
    Serial.println("Starting to build header");
    gui_get_bitmap_header(header, sizeof(header));
    Serial.println("Finished building header");

    // Initialize the SD
        if (!sd.begin(SD_CONFIG)) {
            sd.initErrorHalt(&Serial);
        }
        // Open or create file - truncate existing file.
        if (!file.open(LOG_FILENAME, O_RDWR | O_CREAT | O_TRUNC)) {
            Serial.println("open failed\n");
            return;
        }
        // File must be pre-allocated to avoid huge
        // delays searching for free clusters.
        if (!file.preAllocate(LOG_FILE_SIZE)) {
            Serial.println("preAllocate failed\n");
            file.close();
            return;
        }
        Serial.println("Start write headers to SD");
        file.write(header, 122);
        Serial.println("Finish write headers to SD, and start write image to SD");
        for(uint32_t i=0; i<LOG_FILE_SIZE;i++){
              file.write(buff[i]);
        }
        Serial.println("Finished writing image data to SD");
        file.close();
        Serial.println("Snapshot saved to SD");
    }

Here is what the opened BMP looks like:
293774488_448940057107341_3818312794584837045_n

:frowning:

The idea is to craft the BMP header so that you can write the same VDB bitmap array as is (RGB565 left-to-right top-to-bottom) without conversion. This is the least taxing on the MCU. Using the information in the header any imaging application should be able to figure out how to display the image correctly. For me, all that was needed was a negative height to mirror it and the appropriate bit masks for each color channel.

Can you share the actual BMP file or the first 66 header bytes? I see you are writing 122 bytes to the header, but in my code the header only uses 66 bytes ( “BM” + sizeof(bmp_header_t)).

This is my screenshot solution based on this blog post:

File pFileOut;

/** Take Screenshot.
 *
 * Flush buffer into a binary file.
 *
 * @note: data pixel should be formated to uint16_t RGB. Set by Bitmap header.
 *
 * @param[in] pFileName   Output binary file name.
 *
 **/
void guiTakeScreenshot(const char* pFileName)
{
    uint8_t buffer[sizeof(bmp_header_t) + 2];            // "BM" + struct size
    gui_get_bitmap_header(buffer, sizeof(buffer));       // get header data

    pFileOut = HASP_FS.open(pFileName, "w");
    if(pFileOut) {

        size_t len = pFileOut.write(buffer, sizeof(buffer));
        if(len == sizeof(buffer)) {
            LOG_VERBOSE(TAG_GUI, F("Bitmap header written"));

            /* Refresh screen using a screenshot callback */
            lv_disp_t* disp = lv_disp_get_default();
            void (*flush_cb)(struct _disp_drv_t * disp_drv, const lv_area_t* area, lv_color_t* color_p);
            flush_cb              = disp->driver.flush_cb;     // store original callback first
            disp->driver.flush_cb = gui_screenshot_to_file;    // replace the callback function

            lv_obj_invalidate(lv_scr_act());       // invalidate whole screen area
            lv_refr_now(NULL);                     // Force a call our screenshot callback
            disp->driver.flush_cb = flush_cb;      // restore the original callback

            LOG_VERBOSE(TAG_GUI, F("Bitmap data flushed to %s"), pFileName);

        } else {
            LOG_ERROR(TAG_GUI, F("Data written does not match header size"));
        }
        pFileOut.close();

    } else {
        LOG_WARNING(TAG_GUI, F(D_FILE_SAVE_FAILED), pFileName);
    }
}

/* Flush VDB bytes to both a file and screen */
/* This function temporarily replaces the flush_cb while writing the screenshot */
static void gui_screenshot_to_file(lv_disp_drv_t* disp, const lv_area_t* area, lv_color_t* color_p)
{
    size_t len = (area->x2 - area->x1 + 1) * (area->y2 - area->y1 + 1); /* Number of pixels */
    len *= sizeof(lv_color_t);                                          /* Number of bytes */
    size_t res = pFileOut.write((uint8_t*)color_p, len);
    if(res != len) gui_flush_not_complete();

    // indirect callback to flush screenshot data to the screen too, so both are in sync
    Tft.flush_pixels(disp, area, color_p);
}

I don’t use lv_snapshot_take_to_buf but instead temporarily replace the flush_cb which uses the regular VDB to flush the bytes instead. It doesn’t need a separate buffer to snapshot the screen.

1 Like

Attached is the generated Bitmap file.
I believe there are two issues:

  1. the size of the file is too big. Image data is calculated at 480 * 320 * 3 where is should actually be
    480 * 320 * 2 as we don’t need the alpha byte (correct me if i am wrong)
  2. Only some of the pixel data is being pushed into the file, and not all of it.

snapshot.bmp.zip (2.3 KB)

The header is OK, except for bmp->bfOffBits must be 68 instead of 126 and bmp->biHeight should be -320.

The image data in the file is not the RGB565 pixel-array, so that’s why you see the stripes.
Technically that’s the image that is in the file, so the pixel array isn’t exported properly…
You don’t need to add an alpha byte, just plain RGB565 (as uint16 or byte array).

Opening the file in a binary like 010 Editor helps to verify the data. Now it is only a matter of writing the VDB buffer after the BMP header and it should work.

@fvanroie I got it working!
For some reason the snapshot function was not behaving well in v8.1
I upgraded to v8.3 and its working like a charm

Thank you for all the help!
PS here is a snapshot of one of the screens:
snapshot

1 Like

@reso Hi, I tried your approach, but I get snapshot failed due to snap == LV_RES_INV. I’m passing lv_scr_act() to takeSnapshot

@jojo how bug is your snapshot buffer?

It was a buffer size issue, I explicitly put the buffer size value on the last parameter and it worked. Now to the next problem, I get a black screen when I try to capture lv_scr_act(). Going to debug a bit and see if i can finally get this working.

Ok I got some data, but the bmp image is all screwed with weird pixels. Which gui_get_bitmap_header function did you use?

Ok I got it sorted out, now I have a proper image, however the colors are kinda messed up. @fvanroie do you know how to swap the two bytes colors from the BMP header? It seems that I have to swap the order from RGB to BGR.

You can try swapping the Red and Blue bitmask:

    // BI_BITFIELDS
    bmp->bdMask[0] = 0x001F; // Red bitmask  : 0000 0000 | 0001 1111
    bmp->bdMask[1] = 0x07E0; // Green bitmask: 0000 0111 | 1110 0000
    bmp->bdMask[2] = 0xF800; // Blue bitmask : 1111 1000 | 0000 0000

Thanks, I already tried that, didn’t change anything. I think that field is ignored if you are working with >8bit, in my project I use 16 bits.

I upgraded to 8.3 same issue with the snapshot function, there is a color noise around the edges of objects, for example around this circle you can see the noise: