Skip to content

jpegio encodes, and decodes on the P4's JPEG engine (media Phase 2) - #41

Merged
bdbarnett merged 3 commits into
mainfrom
media/2-jpegio-encode
Oct 6, 2026
Merged

bdbarnett merged 3 commits into
mainfrom
media/2-jpegio-encode

Conversation

@bdbarnett

Copy link
Copy Markdown
Contributor

Media modules roadmap, Phase 2.

  • jpegio.JpegEncoder(quality, subsampling, *, hardware, exact): RGB565 or GRAY buffers to baseline JPEG. Software everywhere (stb_image_write's writer, generated into src/jpegenc/), the ESP32-P4's JPEG engine there. The engine zero-fills RGB565 (a round trip caps near 33 dB), so by default jpegio widens to RGB888 first; exact=False lets it read the buffer directly (8-10 ms for 720x720).
  • JpegDecoder(hardware=True) decodes on the engine where it can and falls back to TJpgDec; .hardware says which ran. The driver's RGB output is studio-range BT.601 (crushed blacks, clipped whites), so jpegio asks for YUV and converts with JFIF's full-range matrix.
  • jpegio_encode() in src/jpegio.h, the C API cameraif's capture_jpeg() now calls (capture_jpeg() encodes through jpegio cameraif#10, pinned here).

Gate, all four items met on hardware: P4 and S3 encodes read back by Pillow as their source (30/36 dB thresholds fixed first, planted faults rejected); hardware decode 40-44 dB from TJpgDec with means within 0.11 of the corpus reference; two threads encoding at once give identical bytes; cameraif's snapshot and MJPEG examples run unchanged on the panel's camera at 35.7 fps capture. Details in the anchor's media-modules-roadmap.md.

…ere), hardware decode on the P4, and a C API for cameraif

JpegEncoder(quality, subsampling, *, hardware, exact).encode(buffer, w, h,
*, format=RGB565|GRAY, stride, swap) -> bytes. Software is stb_image_write's
JPEG writer generated into src/jpegenc/ by tools/make_jpegenc.py, reading
pixels through a fetch callback. On the ESP32-P4 the JPEG engine encodes:
by default from RGB888 widened as a display widens (the engine zero-fills
RGB565, which caps a round trip near 33 dB), or with exact=False straight
from the buffer (8-10 ms for 720x720).

JpegDecoder(hardware=True) decodes on the engine where it can (whole image,
buffer, scale 0) and falls back to TJpgDec otherwise; .hardware says which
ran. The engine is asked for YUV 4:4:4 and converted with JFIF's full-range
matrix here, because the driver's RGB path uses studio-range BT.601.

jpegio_encode() in src/jpegio.h is the C API cameraif's capture_jpeg() calls.
tests/test_jpegio_encode.py is the Phase 2 gate (thresholds fixed first);
tests/check_encode_pil.py has Pillow read every encoded file back.
… as the corpus test does

On the S3 the first gc.mem_free() after a collect wobbles by 1.4-3 KB (stale
stack words pin blocks); 100 and 300 encodes land on the same collected
number, so the check now looks for a trend, not before-vs-after.
@bdbarnett
bdbarnett merged commit 2098293 into main Oct 6, 2026
2 checks passed
@bdbarnett
bdbarnett deleted the media/2-jpegio-encode branch October 6, 2026 19:28
bdbarnett added a commit that referenced this pull request Oct 19, 2026
…sing a fifth of itself (usbif#37) (#41)

At high speed the audio data endpoint asked for a packet every 125 us.
TinyUSB re-arms that endpoint from the MicroPython task, and whenever the
interpreter took longer than a microframe the next packet was lost: 20 %
for long stretches, a third under load. The pump played what arrived back
to back, so music came out a major third or more sharp and rough at the
splices. bInterval 4 at high speed gives one 196-byte packet per
millisecond, the full-speed cadence, and eight times the room.

Adds uac_pump_clock() (USB packets, I2S DMA bytes consumed) and
uac_pump_tap() (the samples handed to I2S) and a board/host checker in
tests/hardware that measures delivery and pitch without ears.
bdbarnett added a commit that referenced this pull request Oct 19, 2026
…41) (#43)

test_jpegio_lvgl.py now fails when `import jpegio` after lv.init() registers
nothing on linux, darwin or win32, naming MICROPY_MODULE_BUILTIN_INIT. The
windows half needs #18, which turns the hook on
in the PyDevices windows variant; the README says so and tells other windows
builds to call register_lvgl_decoder().
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant