Как преобразовать поток байтов JPEG в изображение JPEG и отобразить его на 1,8-дюймовом TFT-экране

У меня есть ESP32 с камерой, которая передает сжатый поток JPEG.

  • TTGO T-Journal ESP32
  • Совет по разработке камеры
  • OV2640
  • SMA Wi-Fi
  • Антенна 3dbi
  • Плата OLED-камеры 0,91

У меня есть ESP32 с 1,8-дюймовым TFT-экраном и слотом для SD-карты, который выполняет функцию приемника.

  • ESP32 TS V1.2
  • MPU9250
  • 1,8-дюймовый TFT-дисплей
  • Bluetooth
  • Wi-Fi
  • Слот для карты MicroSD
  • Модуль динамиков

В браузере Chrome изображения JPEG отображаются как непрерывное изображение за изображением (видео).

Я вижу входящий сжатый поток JPEG и печатаю его на последовательном мониторе. Но мне нужно декодировать/распаковать поток JPEG на приемной стороне и нарисовать пиксель TFT-экрана.

Какие шаги я пропустил? Может ли кто-нибудь предложить код рендеринга изображений JPEG на TFT-экране?

Скетч передатчика:

#include "OV2640.h"
#include <WiFi.h>
#include <WebServer.h>
#include <WiFiClient.h>

#define ENABLE_OLED //если хотите использовать oled, включите этот макрос

#ifdef ENABLE_OLED
#include "SSD1306.h"
#define OLED_ADDRESS 0x3c
#define I2C_SDA 14
#define I2C_SCL 13
SSD1306Wire display(OLED_ADDRESS, I2C_SDA, I2C_SCL, GEOMETRY_128_32);
#endif

OV2640 cam;
WebServer server(80);

IPAddress apIP = IPAddress(192, 168, 1, 1);

void handle_jpg_stream(void)
{
    WiFiClient client = server.client();
    String response = "HTTP/1.1 200 OK\r\n";
response += "Content-Type: multipart/x-mixed-replace; boundary=frame\r\n\r\n";
server.sendContent(response);

while (1)
{
    cam.run();
    if (!client.connected())
        break;
    response = "--frame\r\n";
    response += "Content-Type: image/jpeg\r\n\r\n";
    server.sendContent(response);

    client.write((char *)cam.getfb(), cam.getSize());
    server.sendContent("\r\n");
    if (!client.connected())
        break;
    }
}

void handle_jpg(void)
{
    WiFiClient client = server.client();

    cam.run();
    if (!client.connected())
    {
        return;
    }
     String response = "HTTP/1.1 200 OK\r\n";
     response += "Content-disposition: inline; filename=capture.jpg\r\n";
    response += "Content-type: image/jpeg\r\n\r\n";
    server.sendContent(response);
    client.write((char *)cam.getfb(), cam.getSize());
}

void handleNotFound()
{
    String message = "Server is running!\n\n";
    message += "URI: ";
    message += server.uri();
    message += "\nMethod: ";
    message += (server.method() == HTTP_GET) ? "GET" : "POST";
   message += "\nArguments: ";
   message += server.args();
   message += "\n";
   server.send(200, "text/plain", message);
}

void setup()
{
    Serial.begin(115200);
    while (!Serial)
    {
        ;
     }
camera_config_t camera_config;
camera_config.ledc_channel = LEDC_CHANNEL_0;
camera_config.ledc_timer = LEDC_TIMER_0;
camera_config.pin_d0 = 17;
camera_config.pin_d1 = 35;
camera_config.pin_d2 = 34;
camera_config.pin_d3 = 5;
camera_config.pin_d4 = 39;
camera_config.pin_d5 = 18;
camera_config.pin_d6 = 36;
camera_config.pin_d7 = 19;
camera_config.pin_xclk = 27;
camera_config.pin_pclk = 21;
camera_config.pin_vsync = 22;
camera_config.pin_href = 26;
camera_config.pin_sscb_sda = 25;
camera_config.pin_sscb_scl = 23;
camera_config.pin_reset = 15;
camera_config.xclk_freq_hz = 20000000;
camera_config.pixel_format = CAMERA_PF_JPEG;
camera_config.frame_size = CAMERA_FS_SVGA;

cam.init(camera_config);

WiFi.mode(WIFI_AP);
WiFi.softAPConfig(apIP, apIP, IPAddress(255, 255, 255, 0));
bool result = WiFi.softAP("TTGO-CAMERA", "12345678", 1, 0);
if (!result)
{
    Serial.println("AP Config failed.");
    return;
}
else
{
    Serial.println("AP Config Success.");
    Serial.print("AP MAC: ");
    Serial.println(WiFi.softAPmacAddress());
}

#ifdef ENABLE_OLED
display.init();
display.flipScreenVertically();
display.setFont(ArialMT_Plain_16);
display.setTextAlignment(TEXT_ALIGN_CENTER);
display.drawString(128 / 2, 32 / 2, WiFi.softAPIP().toString());
display.display();
#endif

server.on("/", HTTP_GET, handle_jpg_stream);
server.on("/jpg", HTTP_GET, handle_jpg);
server.onNotFound(handleNotFound);
server.begin();
}

void loop()
{
    server.handleClient();
}

Скетч приемника:

#include <WiFi.h>
#include "FS.h"
#include "SD.h"
#include "SPI.h"
#include <Adafruit_GFX.h>    // Базовая графическая библиотека
#include <Adafruit_ST7735.h> // Аппаратная библиотека
#include <Adafruit_ImageReader.h> // Функции чтения изображений
//#include <Adafruit_TFTLCD.h> // Аппаратная библиотека
#include <SPI.h>
#include "WiFi.h"
#include <JPEGDecoder.h>
//#include <FileIO.h>




// Для пробоя можно использовать любые 2 или 3 контакта
// Эти контакты также подойдут для экрана TFT 1,8 дюйма.
#define TFT_CS 16
#define TFT_RST 9  // вы также можете подключить это к сбросу Arduino
                  // в этом случае установите для этого вывода #define значение -1!
#define TFT_DC 17

// Вариант 2: используйте любые пины, но чуть медленнее!
#define TFT_SCLK 5   // установите любые контакты, которые вам нравятся!
#define TFT_MOSI 23   // установите любые контакты, которые вам нравятся!
#define PIN_NUM_MISO 2
#define PIN_NUM_MOSI 15
#define PIN_NUM_CLK  14
#define PIN_NUM_CS   13



Adafruit_ST7735 tft = Adafruit_ST7735(TFT_CS, TFT_DC, TFT_MOSI, TFT_SCLK,     TFT_RST);

String text="";


const char* ssid     = "TTGO-CAMERA";
const char* password = "12345678";
const char* host     = "192.168.1.1";
const char* url      = "/";

IPAddress local_IP(192, 168, 1, 3);
IPAddress gateway(192, 168, 1, 1);
IPAddress subnet(255, 255, 255, 0);
IPAddress primaryDNS(8, 8, 8, 8); //необязательный
IPAddress secondaryDNS(8, 8, 4, 4); //необязательный

// Логотип PImage;


#define BUFFPIXEL 20
  int      bmpWidth, bmpHeight;   // Ш+В в пикселях
  uint8_t  bmpDepth;              // Разрядность (на данный момент должна быть 24)
  uint32_t bmpImageoffset;        // Начало данных изображения в файле
  uint32_t rowSize;               // Не всегда = bmpWidth; может иметь прокладку
  uint8_t  sdbuffer[3*BUFFPIXEL]; // буфер пикселей (R+G+B на пиксель)
  uint8_t  buffidx = sizeof(sdbuffer); // Текущая позиция в буфере памяти
  boolean  goodBmp = false;       // Устанавливаем значение true при правильном анализе заголовка
  boolean  flip    = true;        // BMP хранится снизу вверх
  int      w, h, row, col;
  uint8_t  r, g, b;               // цвет пикселя для tft
  File     bmpFile; File     bmpFile1;
  uint32_t pos = 0, startTime ;
  uint8_t xx=0; uint16_t yy=0; int ww=0; int hh=0;
  int bmpfilepos;

//Как использовать -
//tft.fillScreen(ST7735_BLACK);
//testdrawtext(2,3,ST7735_GREEN,text);



void testdrawtext( int leftoffset , int topoffset , uint16_t color ,  String text ) {
  tft.setCursor(leftoffset, topoffset);  // (смещение от левого поля, смещение от верхнего поля.)
  tft.setTextColor(color);
  tft.setTextWrap(true);
  tft.setTextSize(1);
  tft.print(text  ); 
    }




void jpegbytesdisplay()
{
 // Создаем буфер для пакета
    char dataBuff[240];

    delay(5000);

  Serial.print("connecting to ");
  Serial.println(host);

  // Используем класс WiFiClient для создания TCP-соединений
  WiFiClient client;
  const int httpPort = 80;
  int cnt=0;

  if (!client.connect(host, httpPort)) {
    Serial.println("connection failed");
    return;
  }

  Serial.print("Requesting URL: ");
  Serial.println(url);

  // Это отправит запрос на веб-камеру
  client.print(String("GET ") + url + " HTTP/1.1\r\n" +
           "Host: " + host + "\r\n" +
           "Connection: close\r\n\r\n");
  unsigned long timeout = millis();



  while (client.available() == 0) 
  {
    if (millis() - timeout > 5000) 
    {
      Serial.println(">>> Client Timeout !");
      client.stop();
      return;
    }
  }


   int max_y=200;
   int ypos =200;
   int max_x=0 ;
   int xpos =0;

  // Читаем все строки ответа с сервера и печатаем их в Serial

  ImageReturnCode stat;
  while (client.available()) 
  {
    //===============================
   // Строковая строка = client.readStringUntil('\r');
   // Serial.print(строка);


    //===============================
    //байты помещаются в буфер данных char[len].
    size_t len=client.available();
    unsigned char buf[len];
    client.readBytes(buf, len);

    Serial.println("------------------------");
    Serial.println(len);
    //Serial.write(buf);

     //===============================
     // буфер для рендеринга пикселей TFT-экрана

      int col=0;     
  int buffidx =0;
  cnt++;

    tft.fillScreen(ST7735_BLACK);
    tft.startWrite();
    tft.setAddrWindow(0, 0, 500, 500);


String fname="/image2" + String(cnt) + ".jpg";
File myFile = SD.open(fname, FILE_WRITE);
myFile.write(buf,len);
myFile.close();  

     for (col=0; col<len; col++) 
       { 
          Serial.print(buf[col]);
       }
    Serial.println();
    Serial.println("------------------------");


      // Для каждого пикселя...
     for (col=0; col<len; col++) 
     { 
         if (buffidx >= sizeof(buf)) 
              {  
          buffidx = 0; // Устанавливаем индекс в начало
                break;
        }


        b = buf[buffidx++]; // [Б]
        g = buf[buffidx++]; // [Г]
        r = buf[buffidx++]; // [Р]



        tft.pushColor(tft.color565(r,g,b));   
                             // Отображение пикселей на TFT-экране один за другим.



   } // конец цикла пикселей.

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

   delay(1000);
    tft.endWrite();    
  }

    }







 void setupNormal()
{


   Serial.begin(115200);


   // Используйте этот инициализатор, если вы используете 1,8-дюймовый TFT-дисплей
  tft.initR(INITR_BLACKTAB);   // инициализируем чип ST7735S, черная вкладка

  Serial.println("Initialized");


  if (!WiFi.config(local_IP, gateway, subnet, primaryDNS, secondaryDNS)) {
    Serial.println("STA Failed to configure");
  }

  Serial.print("Connecting to ");
  Serial.println(ssid);

  WiFi.begin(ssid, password);

  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }


  tft.fillScreen(ST7735_BLACK);

}


void setup()
{

    setupNormal();
    jpegbytesdisplay();


      }



void loop()
{

}

, 👍1


1 ответ


1

Вот что я нашел для отображения файлов изображений JPEG на любом TFT -

Я использовал библиотеку JPEGDecoder и вместо tft.pushColor я использовал метод tft.drawPixel.

  for (int jj  =mcu_y;jj<mcu_y + win_h;jj++)
     {
        for (int ii=mcu_x;ii<mcu_x + win_w ;ii++)
        {
            // TFTscreen.drawPixel(ii,jj,tft.color565(255,0,0));
             TFTscreen.drawPixel(ii,jj,*pImg++ ); 
        }
     } 

Для BMP нам нужны значения RGB для рендеринга каждого пикселя на TFT-экране.

Но в случае JPG сначала нам нужно декодировать и создать плитки MCU на TFT. Затем мы раскрашиваем эти тайлы MCU по цвету, который получаем от декодера потока JPEG. Здесь *pImg — это указатель, содержащий информацию о цвете. По мере увеличения массива *pImg++ каждый раз мы получаем новый цветовой код типа uInt8_t. Внутри тайлов MCU эти цвета должны быть введены. Каждая плитка MCU раскрашивается и рисуется в заданной последовательности с учетом того, где находится граница TFT-экрана.

Он автоматически создает изображение JPEG.

///////////////////////////////////////////// //////////////////
#include <Adafruit_GFX.h>    // Базовая графическая библиотека
#include <Adafruit_ST7735.h> // Аппаратная библиотека
#include <Adafruit_ImageReader.h> // Функции чтения изображений

// Для пробоя можно использовать любые 2 или 3 контакта
// Эти контакты также подойдут для экрана TFT 1,8 дюйма.
#define TFT_CS 16
#define TFT_RST 9  // вы также можете подключить это к сбросу Arduino
                      // установите это любые контакты, которые вам нравятся!
// в этом случае установите для этого вывода #define значение -1!
#define TFT_DC 17

// Вариант 2: используйте любые пины, но чуть медленнее!
#define TFT_SCLK 5   // установите любые контакты, которые вам нравятся!
#define TFT_MOSI 23   

Adafruit_ST7735 TFTscreen = Adafruit_ST7735(TFT_CS, TFT_DC, TFT_MOSI, TFT_SCLK, TFT_RST);

/////////////////////////////////////////////////// ////////////

#include <JPEGDecoder.h>  // библиотека декодера JPEG

/////////////////////////////////////////////////// ////////////
// подключаем необходимые библиотеки
#include <SPI.h>
#include <SD.h>
/////////////////////////////////////////////////// ////////////


// определение контакта для Mega
#define sd_cs  53
#define lcd_cs 49
#define dc     48
#define rst    47

#define TFT_WHITE 0xFFFF
#define TFT_BLACK 0x0000
#define TFT_RED   0xF800


 /////////////////////////////////////////////////// ////////////

 #define PIN_NUM_MISO 2
#define PIN_NUM_MOSI 15
#define PIN_NUM_CLK  14
#define PIN_NUM_CS   13



  Adafruit_ImageReader reader;
    /////////////////////////////////////////////////// /////





String text="";

// эта функция определяет минимум из двух чисел
#define minimum(a,b)     (((a) < (b)) ? (a) : (b))



  //============================================== ===================================
    // настраивать
    //============================================== ===================================

void setup() {
  // инициализируем GLCD и показываем сообщение
  // просим пользователя открыть последовательную линию
  /////////////////////////////////////////////////// ////////////////
   // Используйте этот инициализатор, если вы используете 1,8-дюймовый TFT-дисплей
  TFTscreen.initR(INITR_BLACKTAB);   // инициализируем чип ST7735S, черная вкладка
/////////////////////////////////////////////////// ////////////////

  TFTscreen.fillScreen(TFT_WHITE); // Альтернатива:

TFTscreen.background(255, 255, 255);



TFTscreen.setTextColor(TFT_RED); // Альтернатива: TFTscreen.stroke(0, 0, 255);
  TFTscreen.println();
  TFTscreen.println(F("Arduino TFT Jpeg Example"));

  TFTscreen.setTextColor(TFT_BLACK); // Альтернатива: TFTscreen.stroke(0, 0, 0);
  TFTscreen.println(F("Open serial monitor"));
  TFTscreen.println(F("to run the sketch"));

  // инициализируем последовательный порт: он будет использоваться для
  // распечатываем некоторую диагностическую информацию
  Serial.begin(115200);
  while (!Serial) {
    // ждем подключения последовательного порта. Требуется только для собственного порта USB
  }

  // очищаем экран GLCD перед запуском
// TFTscreen.background(255, 255, 255);
  TFTscreen.fillScreen(TFT_WHITE);

  // попытаемся получить доступ к SD-карте. Если это не помогло (например,
  // карта отсутствует), процесс установки остановится.
  Serial.print(F("Initializing SD card..."));

/////////////////////////////////////////////////// ////////////////


  SPI.begin(PIN_NUM_CLK, PIN_NUM_MISO, PIN_NUM_MOSI, PIN_NUM_CS);

  if (!SD.begin(PIN_NUM_CS)) {
    Serial.println(F("failed!"));
    while (1); // Инициализация SD не удалась, поэтому подождите здесь
  }
  Serial.println(F("OK!"));
/////////////////////////////////////////////////// ////////////////


  // инициализируем и очищаем экран GLCD
 // TFTscreen.begin();
  TFTscreen.fillScreen(TFT_WHITE); // Альтернатива: TFTscreen.background(255, 255, 255);

  // теперь, когда SD-карта доступна, проверяем
  // файл изображения существует.
  if (SD.exists("/EagleEye.jpg")) {
    Serial.println("EagleEye.jpg found on SD card.");
  } else {
    Serial.println("EagleEye.jpg not found on SD card.");
    while (1); // Файл изображения отсутствует, поэтому оставайтесь здесь
  }

}

//============================================== ===================================
// Основной цикл
//============================================== ===================================
void loop() {

  // открываем файл изображения
  File jpgFile = SD.open( "/EagleEye.jpg", FILE_READ);

  // инициализируем декодер, чтобы предоставить доступ к информации об изображении
  JpegDec.decodeSdFile(jpgFile);

  //выводим информацию об изображении в последовательный порт
  jpegInfo();

  // отображаем изображение на экране по координате 0,0
  renderJPEG(0, 0);

  // подождем немного, прежде чем очистить экран до случайного цвета и снова нарисовать
  delay(4000);

  // очисти экран
  TFTscreen.fillScreen(random(0xFFFF));  // Альтернатива: TFTscreen.background(255, 255, 255);
}

//============================================== ===================================
// Выводим информацию об изображении
//============================================== ===================================

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("==============="));
}

//============================================== ===================================
// Декодируем и рисуем на TFT-экране
//============================================== ===================================
void renderJPEG(int xpos, int ypos) {

  // получаем информацию об изображении
  uint16_t *pImg;
  uint16_t mcu_w = JpegDec.MCUWidth;
  uint16_t mcu_h = JpegDec.MCUHeight;
  uint32_t max_x = JpegDec.width;
  uint32_t max_y = JpegDec.height;

  // Изображения Jpeg рисуются как набор блоков изображений (тайлов), называемых минимальными единицами кодирования (MCU).
  // Обычно эти MCU представляют собой блоки размером 16x16 пикселей.
  // Определяем ширину и высоту правого и нижнего краевых блоков изображения
  uint32_t min_w = minimum(mcu_w, max_x % mcu_w);
  uint32_t min_h = minimum(mcu_h, max_y % mcu_h);

  // сохраняем текущий размер блока изображения
  uint32_t win_w = mcu_w;
  uint32_t win_h = mcu_h;

  // записываем текущее время, чтобы мы могли измерить, сколько времени потребуется для рисования изображения
  uint32_t drawTime = millis();

  // сохраняем координаты правого и нижнего краев, чтобы облегчить обрезку изображения
  // к размеру экрана
  max_x += xpos;
  max_y += ypos;

   char str[100];
  // читаем каждый блок MCU, пока их не останется
  while ( JpegDec.read()) {

    // сохраняем указатель на блок изображения
    pImg = JpegDec.pImage;

    // вычисляем, где на экране должен быть нарисован блок изображения
    int mcu_x = JpegDec.MCUx * mcu_w + xpos;
    int mcu_y = JpegDec.MCUy * mcu_h + ypos;

    // проверяем, нужно ли менять размер блока изображения для правого и нижнего края
    if (mcu_x + mcu_w <= max_x) win_w = mcu_w;
    else win_w = min_w;
    if (mcu_y + mcu_h <= max_y) win_h = mcu_h;
    else win_h = min_h;

    // вычисляем, сколько пикселей необходимо отрисовать
    uint32_t mcu_pixels = win_w * win_h;

    // рисуем блок изображения, если он помещается на экране
    if ( ( mcu_x + win_w) <= TFTscreen.width() && ( mcu_y + win_h) <= TFTscreen.height()) {
      // открываем окно на экран, чтобы раскрасить пиксели
      //TFTscreen.setAddrWindow(mcu_x, mcu_y, mcu_x + win_w - 1, mcu_y + win_h - 1);
      //TFTscreen.setAddrWindow(mcu_x, mcu_y, mcu_x + win_w - 1, mcu_y + win_h - 1);
      // выводим все пиксели блока изображения на экран


 /////////////////////////////////////////////////// //////

 // пока (mcu_pixels--)
 // {
 // TFTscreen.pushColor(*pImg++);
 // } // Отправка в TFT по 16 бит за раз



 /////////////////////////////////////////////////// //////



 // Записываем все пиксели MCU в окно TFT





  for (int jj=mcu_y;jj<mcu_y + win_h;jj++)
     {
        for (int ii=mcu_x;ii<mcu_x + win_w ;ii++)
        {
            // TFTscreen.drawPixel(ii,jj,tft.color565(255,0,0));
             TFTscreen.drawPixel(ii,jj,*pImg++ ); 
        }
     } 


 /////////////////////////////////////////////////// //////

    }

    // прекращаем рисовать блоки, если достигнут нижний край экрана
    // функция прерывания закроет файл
    else if ( ( mcu_y + win_h) >= TFTscreen.height()) JpegDec.abort();

  }

  // вычисляем, сколько времени ушло на отрисовку изображения
  drawTime = millis() - drawTime; // Вычисляем время, которое потребовалось



 // распечатываем результаты в последовательный порт
  Serial.print  ("Total render time was    : "); Serial.print(drawTime); Serial.println(" ms");
  Serial.println("=====================================");

}
,