[esp32_camera] Add support for sensors without JPEG support (#9496)

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
Co-authored-by: J. Nick Koston <nick@koston.org>
This commit is contained in:
Marc Hörsken
2026-02-19 04:52:38 +01:00
committed by GitHub
parent eefad194d0
commit 4d05e4d576
5 changed files with 212 additions and 61 deletions
+49 -1
View File
@@ -22,8 +22,10 @@ from esphome.const import (
CONF_TRIGGER_ID,
CONF_VSYNC_PIN,
)
from esphome.core import CORE
from esphome.core.entity_helpers import setup_entity
import esphome.final_validate as fv
from esphome.types import ConfigType
_LOGGER = logging.getLogger(__name__)
@@ -84,6 +86,18 @@ FRAME_SIZES = {
"2560X1920": ESP32CameraFrameSize.ESP32_CAMERA_SIZE_2560X1920,
"QSXGA": ESP32CameraFrameSize.ESP32_CAMERA_SIZE_2560X1920,
}
ESP32CameraPixelFormat = esp32_camera_ns.enum("ESP32CameraPixelFormat")
PIXEL_FORMATS = {
"RGB565": ESP32CameraPixelFormat.ESP32_PIXEL_FORMAT_RGB565,
"YUV422": ESP32CameraPixelFormat.ESP32_PIXEL_FORMAT_YUV422,
"YUV420": ESP32CameraPixelFormat.ESP32_PIXEL_FORMAT_YUV420,
"GRAYSCALE": ESP32CameraPixelFormat.ESP32_PIXEL_FORMAT_GRAYSCALE,
"JPEG": ESP32CameraPixelFormat.ESP32_PIXEL_FORMAT_JPEG,
"RGB888": ESP32CameraPixelFormat.ESP32_PIXEL_FORMAT_RGB888,
"RAW": ESP32CameraPixelFormat.ESP32_PIXEL_FORMAT_RAW,
"RGB444": ESP32CameraPixelFormat.ESP32_PIXEL_FORMAT_RGB444,
"RGB555": ESP32CameraPixelFormat.ESP32_PIXEL_FORMAT_RGB555,
}
ESP32GainControlMode = esp32_camera_ns.enum("ESP32GainControlMode")
ENUM_GAIN_CONTROL_MODE = {
"MANUAL": ESP32GainControlMode.ESP32_GC_MODE_MANU,
@@ -131,6 +145,7 @@ CONF_EXTERNAL_CLOCK = "external_clock"
CONF_I2C_PINS = "i2c_pins"
CONF_POWER_DOWN_PIN = "power_down_pin"
# image
CONF_PIXEL_FORMAT = "pixel_format"
CONF_JPEG_QUALITY = "jpeg_quality"
CONF_VERTICAL_FLIP = "vertical_flip"
CONF_HORIZONTAL_MIRROR = "horizontal_mirror"
@@ -171,6 +186,21 @@ def validate_fb_location_(value):
return validator(value)
def validate_jpeg_quality(config: ConfigType) -> ConfigType:
quality = config.get(CONF_JPEG_QUALITY)
pixel_format = config.get(CONF_PIXEL_FORMAT, "JPEG")
if quality == 0:
# Set default JPEG quality if not specified for backwards compatibility
if pixel_format == "JPEG":
config[CONF_JPEG_QUALITY] = 10
# For pixel formats other than JPEG, the valid 0 means no conversion
elif quality < 6 or quality > 63:
raise cv.Invalid(f"jpeg_quality must be between 6 and 63, got {quality}")
return config
CONFIG_SCHEMA = cv.All(
cv.ENTITY_BASE_SCHEMA.extend(
{
@@ -206,7 +236,12 @@ CONFIG_SCHEMA = cv.All(
cv.Optional(CONF_RESOLUTION, default="640X480"): cv.enum(
FRAME_SIZES, upper=True
),
cv.Optional(CONF_JPEG_QUALITY, default=10): cv.int_range(min=6, max=63),
cv.Optional(CONF_PIXEL_FORMAT, default="JPEG"): cv.enum(
PIXEL_FORMATS, upper=True
),
cv.Optional(CONF_JPEG_QUALITY, default=0): cv.Any(
cv.one_of(0), cv.int_range(min=6, max=63)
),
cv.Optional(CONF_CONTRAST, default=0): camera_range_param,
cv.Optional(CONF_BRIGHTNESS, default=0): camera_range_param,
cv.Optional(CONF_SATURATION, default=0): camera_range_param,
@@ -270,11 +305,21 @@ CONFIG_SCHEMA = cv.All(
),
}
).extend(cv.COMPONENT_SCHEMA),
validate_jpeg_quality,
cv.has_exactly_one_key(CONF_I2C_PINS, CONF_I2C_ID),
)
def _final_validate(config):
# Check psram requirement for non-JPEG formats
if (
config.get(CONF_PIXEL_FORMAT, "JPEG") != "JPEG"
and psram_domain not in CORE.loaded_integrations
):
raise cv.Invalid(
f"Non-JPEG pixel formats require the '{psram_domain}' component for JPEG conversion"
)
if CONF_I2C_PINS not in config:
return
fconf = fv.full_config.get()
@@ -298,6 +343,7 @@ SETTERS = {
CONF_RESET_PIN: "set_reset_pin",
CONF_POWER_DOWN_PIN: "set_power_down_pin",
# image
CONF_PIXEL_FORMAT: "set_pixel_format",
CONF_JPEG_QUALITY: "set_jpeg_quality",
CONF_VERTICAL_FLIP: "set_vertical_flip",
CONF_HORIZONTAL_MIRROR: "set_horizontal_mirror",
@@ -351,6 +397,8 @@ async def to_code(config):
cg.add(var.set_frame_size(config[CONF_RESOLUTION]))
cg.add_define("USE_CAMERA")
if config[CONF_JPEG_QUALITY] != 0 and config[CONF_PIXEL_FORMAT] != "JPEG":
cg.add_define("USE_ESP32_CAMERA_JPEG_CONVERSION")
add_idf_component(name="espressif/esp32-camera", ref="2.1.1")
add_idf_sdkconfig_option("CONFIG_SCCB_HARDWARE_I2C_DRIVER_NEW", True)
+147 -60
View File
@@ -16,6 +16,74 @@ static constexpr size_t FRAMEBUFFER_TASK_STACK_SIZE = 1792;
static constexpr uint32_t FRAME_LOG_INTERVAL_MS = 60000;
#endif
static const char *frame_size_to_str(framesize_t size) {
switch (size) {
case FRAMESIZE_QQVGA:
return "160x120 (QQVGA)";
case FRAMESIZE_QCIF:
return "176x155 (QCIF)";
case FRAMESIZE_HQVGA:
return "240x176 (HQVGA)";
case FRAMESIZE_QVGA:
return "320x240 (QVGA)";
case FRAMESIZE_CIF:
return "400x296 (CIF)";
case FRAMESIZE_VGA:
return "640x480 (VGA)";
case FRAMESIZE_SVGA:
return "800x600 (SVGA)";
case FRAMESIZE_XGA:
return "1024x768 (XGA)";
case FRAMESIZE_SXGA:
return "1280x1024 (SXGA)";
case FRAMESIZE_UXGA:
return "1600x1200 (UXGA)";
case FRAMESIZE_FHD:
return "1920x1080 (FHD)";
case FRAMESIZE_P_HD:
return "720x1280 (P_HD)";
case FRAMESIZE_P_3MP:
return "864x1536 (P_3MP)";
case FRAMESIZE_QXGA:
return "2048x1536 (QXGA)";
case FRAMESIZE_QHD:
return "2560x1440 (QHD)";
case FRAMESIZE_WQXGA:
return "2560x1600 (WQXGA)";
case FRAMESIZE_P_FHD:
return "1080x1920 (P_FHD)";
case FRAMESIZE_QSXGA:
return "2560x1920 (QSXGA)";
default:
return "UNKNOWN";
}
}
static const char *pixel_format_to_str(pixformat_t format) {
switch (format) {
case PIXFORMAT_RGB565:
return "RGB565";
case PIXFORMAT_YUV422:
return "YUV422";
case PIXFORMAT_YUV420:
return "YUV420";
case PIXFORMAT_GRAYSCALE:
return "GRAYSCALE";
case PIXFORMAT_JPEG:
return "JPEG";
case PIXFORMAT_RGB888:
return "RGB888";
case PIXFORMAT_RAW:
return "RAW";
case PIXFORMAT_RGB444:
return "RGB444";
case PIXFORMAT_RGB555:
return "RGB555";
default:
return "UNKNOWN";
}
}
/* ---------------- public API (derivated) ---------------- */
void ESP32Camera::setup() {
#ifdef USE_I2C
@@ -68,64 +136,9 @@ void ESP32Camera::dump_config() {
this->name_.c_str(), YESNO(this->is_internal()), conf.pin_d0, conf.pin_d1, conf.pin_d2, conf.pin_d3,
conf.pin_d4, conf.pin_d5, conf.pin_d6, conf.pin_d7, conf.pin_vsync, conf.pin_href, conf.pin_pclk,
conf.pin_xclk, conf.xclk_freq_hz, conf.pin_sccb_sda, conf.pin_sccb_scl, conf.pin_reset);
switch (this->config_.frame_size) {
case FRAMESIZE_QQVGA:
ESP_LOGCONFIG(TAG, " Resolution: 160x120 (QQVGA)");
break;
case FRAMESIZE_QCIF:
ESP_LOGCONFIG(TAG, " Resolution: 176x155 (QCIF)");
break;
case FRAMESIZE_HQVGA:
ESP_LOGCONFIG(TAG, " Resolution: 240x176 (HQVGA)");
break;
case FRAMESIZE_QVGA:
ESP_LOGCONFIG(TAG, " Resolution: 320x240 (QVGA)");
break;
case FRAMESIZE_CIF:
ESP_LOGCONFIG(TAG, " Resolution: 400x296 (CIF)");
break;
case FRAMESIZE_VGA:
ESP_LOGCONFIG(TAG, " Resolution: 640x480 (VGA)");
break;
case FRAMESIZE_SVGA:
ESP_LOGCONFIG(TAG, " Resolution: 800x600 (SVGA)");
break;
case FRAMESIZE_XGA:
ESP_LOGCONFIG(TAG, " Resolution: 1024x768 (XGA)");
break;
case FRAMESIZE_SXGA:
ESP_LOGCONFIG(TAG, " Resolution: 1280x1024 (SXGA)");
break;
case FRAMESIZE_UXGA:
ESP_LOGCONFIG(TAG, " Resolution: 1600x1200 (UXGA)");
break;
case FRAMESIZE_FHD:
ESP_LOGCONFIG(TAG, " Resolution: 1920x1080 (FHD)");
break;
case FRAMESIZE_P_HD:
ESP_LOGCONFIG(TAG, " Resolution: 720x1280 (P_HD)");
break;
case FRAMESIZE_P_3MP:
ESP_LOGCONFIG(TAG, " Resolution: 864x1536 (P_3MP)");
break;
case FRAMESIZE_QXGA:
ESP_LOGCONFIG(TAG, " Resolution: 2048x1536 (QXGA)");
break;
case FRAMESIZE_QHD:
ESP_LOGCONFIG(TAG, " Resolution: 2560x1440 (QHD)");
break;
case FRAMESIZE_WQXGA:
ESP_LOGCONFIG(TAG, " Resolution: 2560x1600 (WQXGA)");
break;
case FRAMESIZE_P_FHD:
ESP_LOGCONFIG(TAG, " Resolution: 1080x1920 (P_FHD)");
break;
case FRAMESIZE_QSXGA:
ESP_LOGCONFIG(TAG, " Resolution: 2560x1920 (QSXGA)");
break;
default:
break;
}
ESP_LOGCONFIG(TAG, " Resolution: %s", frame_size_to_str(this->config_.frame_size));
ESP_LOGCONFIG(TAG, " Pixel Format: %s", pixel_format_to_str(this->config_.pixel_format));
if (this->is_failed()) {
ESP_LOGE(TAG, " Setup Failed: %s", esp_err_to_name(this->init_error_));
@@ -184,8 +197,19 @@ void ESP32Camera::loop() {
// check if we can return the image
if (this->can_return_image_()) {
// return image
auto *fb = this->current_image_->get_raw_buffer();
xQueueSend(this->framebuffer_return_queue_, &fb, portMAX_DELAY);
#ifdef USE_ESP32_CAMERA_JPEG_CONVERSION
if (this->config_.pixel_format != PIXFORMAT_JPEG && this->config_.jpeg_quality > 0) {
// for non-JPEG format, we need to free the data and raw buffer
auto *jpg_buf = this->current_image_->get_data_buffer();
free(jpg_buf); // NOLINT(cppcoreguidelines-no-malloc)
auto *fb = this->current_image_->get_raw_buffer();
this->fb_allocator_.deallocate(fb, 1);
} else
#endif
{
auto *fb = this->current_image_->get_raw_buffer();
xQueueSend(this->framebuffer_return_queue_, &fb, portMAX_DELAY);
}
this->current_image_.reset();
}
@@ -212,6 +236,38 @@ void ESP32Camera::loop() {
xQueueSend(this->framebuffer_return_queue_, &fb, portMAX_DELAY);
return;
}
#ifdef USE_ESP32_CAMERA_JPEG_CONVERSION
if (this->config_.pixel_format != PIXFORMAT_JPEG && this->config_.jpeg_quality > 0) {
// for non-JPEG format, we need to convert the frame to JPEG
uint8_t *jpg_buf;
size_t jpg_buf_len;
size_t width = fb->width;
size_t height = fb->height;
struct timeval timestamp = fb->timestamp;
bool ok = frame2jpg(fb, 100 - this->config_.jpeg_quality, &jpg_buf, &jpg_buf_len);
// return the original frame buffer to the queue
xQueueSend(this->framebuffer_return_queue_, &fb, portMAX_DELAY);
if (!ok) {
ESP_LOGE(TAG, "Failed to convert frame to JPEG!");
return;
}
// create a new camera_fb_t for the JPEG data
fb = this->fb_allocator_.allocate(1);
if (fb == nullptr) {
ESP_LOGE(TAG, "Failed to allocate memory for camera frame buffer!");
free(jpg_buf); // NOLINT(cppcoreguidelines-no-malloc)
return;
}
memset(fb, 0, sizeof(camera_fb_t));
fb->buf = jpg_buf;
fb->len = jpg_buf_len;
fb->width = width;
fb->height = height;
fb->format = PIXFORMAT_JPEG;
fb->timestamp = timestamp;
}
#endif
this->current_image_ = std::make_shared<ESP32CameraImage>(fb, this->single_requesters_ | this->stream_requesters_);
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
@@ -342,6 +398,37 @@ void ESP32Camera::set_frame_size(ESP32CameraFrameSize size) {
break;
}
}
void ESP32Camera::set_pixel_format(ESP32CameraPixelFormat format) {
switch (format) {
case ESP32_PIXEL_FORMAT_RGB565:
this->config_.pixel_format = PIXFORMAT_RGB565;
break;
case ESP32_PIXEL_FORMAT_YUV422:
this->config_.pixel_format = PIXFORMAT_YUV422;
break;
case ESP32_PIXEL_FORMAT_YUV420:
this->config_.pixel_format = PIXFORMAT_YUV420;
break;
case ESP32_PIXEL_FORMAT_GRAYSCALE:
this->config_.pixel_format = PIXFORMAT_GRAYSCALE;
break;
case ESP32_PIXEL_FORMAT_JPEG:
this->config_.pixel_format = PIXFORMAT_JPEG;
break;
case ESP32_PIXEL_FORMAT_RGB888:
this->config_.pixel_format = PIXFORMAT_RGB888;
break;
case ESP32_PIXEL_FORMAT_RAW:
this->config_.pixel_format = PIXFORMAT_RAW;
break;
case ESP32_PIXEL_FORMAT_RGB444:
this->config_.pixel_format = PIXFORMAT_RGB444;
break;
case ESP32_PIXEL_FORMAT_RGB555:
this->config_.pixel_format = PIXFORMAT_RGB555;
break;
}
}
void ESP32Camera::set_jpeg_quality(uint8_t quality) { this->config_.jpeg_quality = quality; }
void ESP32Camera::set_vertical_flip(bool vertical_flip) { this->vertical_flip_ = vertical_flip; }
void ESP32Camera::set_horizontal_mirror(bool horizontal_mirror) { this->horizontal_mirror_ = horizontal_mirror; }
@@ -41,6 +41,18 @@ enum ESP32CameraFrameSize {
ESP32_CAMERA_SIZE_2560X1920, // QSXGA
};
enum ESP32CameraPixelFormat {
ESP32_PIXEL_FORMAT_RGB565,
ESP32_PIXEL_FORMAT_YUV422,
ESP32_PIXEL_FORMAT_YUV420,
ESP32_PIXEL_FORMAT_GRAYSCALE,
ESP32_PIXEL_FORMAT_JPEG,
ESP32_PIXEL_FORMAT_RGB888,
ESP32_PIXEL_FORMAT_RAW,
ESP32_PIXEL_FORMAT_RGB444,
ESP32_PIXEL_FORMAT_RGB555,
};
enum ESP32AgcGainCeiling {
ESP32_GAINCEILING_2X = GAINCEILING_2X,
ESP32_GAINCEILING_4X = GAINCEILING_4X,
@@ -126,6 +138,7 @@ class ESP32Camera : public camera::Camera {
void set_reset_pin(uint8_t pin);
void set_power_down_pin(uint8_t pin);
/* -- image */
void set_pixel_format(ESP32CameraPixelFormat format);
void set_frame_size(ESP32CameraFrameSize size);
void set_jpeg_quality(uint8_t quality);
void set_vertical_flip(bool vertical_flip);
@@ -220,6 +233,7 @@ class ESP32Camera : public camera::Camera {
#ifdef USE_I2C
i2c::InternalI2CBus *i2c_bus_{nullptr};
#endif // USE_I2C
RAMAllocator<camera_fb_t> fb_allocator_{RAMAllocator<camera_fb_t>::ALLOC_INTERNAL};
};
class ESP32CameraImageTrigger : public Trigger<CameraImageData>, public camera::CameraListener {
+1
View File
@@ -43,6 +43,7 @@
#define USE_DEVICES
#define USE_DISPLAY
#define USE_ENTITY_ICON
#define USE_ESP32_CAMERA_JPEG_CONVERSION
#define USE_ESP32_HOSTED
#define USE_ESP32_IMPROV_STATE_CALLBACK
#define USE_EVENT
@@ -30,6 +30,7 @@ esp32_camera:
resolution: 640x480
jpeg_quality: 10
frame_buffer_location: PSRAM
pixel_format: JPEG
on_image:
then:
- lambda: |-