Как преобразовать поток байтов 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()
{
}
@Bhushan Mahajan, 👍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("=====================================");
}
- esp32, platformio A fatal error occurred: Packet content transfer stopped (received 8 bytes) *** [upload] Error 2
- Как выбрать альтернативные контакты I2C на ESP32?
- Драйверы для чипа последовательного порта CH9102X
- Как преобразовать форматированный оператор print в строковую переменную?
- ESP32 - "Детектор Браунаута был активирован" при запуске Wi-Fi
- Питание esp32cam от аккумулятора
- Контакты RX и TX на esp32
- ESP32: отключить детектор отключения питания