Skip to content

h264enc pipelining; espdl releases the GIL and gains HumanFaceDetect - #325

Merged
bdbarnett merged 5 commits into
mainfrom
p4x-eye-encode-vision
Nov 9, 2026
Merged

bdbarnett merged 5 commits into
mainfrom
p4x-eye-encode-vision

Conversation

@bdbarnett

Copy link
Copy Markdown
Contributor

Three changes for camera apps on the ESP32-P4.

h264enc.Encoder(..., pipeline=True) overlaps the PPA's colour conversion of one frame with the H.264 encode of the frame before. A plain encoder runs them back to back, so a 1920x1072 camera frame costs about 40 ms to convert plus 38 ms to encode, 12 fps; pipelined, a frame should cost about the longer of the two. encode() then returns the previous frame's access unit (b"" first) and flush() or close() the last one. It is off by default, because the one-frame latency changes what encode() returns. stats() gains wall_us. The C API that castif uses is unchanged. The README notes that the PPA does one operation at a time, so a preview's own ppa.srm() each frame adds to the conversion time.

espdl now releases the GIL while a model runs, so detection on a _thread no longer stops the main loop (and LVGL) for 70 to 80 ms per frame. espdl.HumanFaceDetect(msr_path, mnp_path) adds ESP-DL's two-stage face detector, returning (x, y, w, h, score, keypoints) with five landmark points; its model files are MIT, from ESP-DL's human_face_detect 0.5.0, and are copied to the board like the other espdl models. esp-vision's py_espdl.cpp stays as pinned: both changes are patch files in modules/vision/esp-vision-patches, applied to a copy in the build tree.

Checked: the firmware builds for ESP32_GENERIC_P4 P4X_EYE with h264enc,ppa,cameraif,vision,pydevices, the patched copy carries the six GIL releases and the new class, and the code was read against esp_h264 1.4.1 and esp-dl 3.3.6. Not verified yet: nothing here has run on a board. The pipelined frame rate, the byte-for-byte match the hardware test checks, the main loop's pause in espdl_thread.py, and the face detector's output all wait on a board run, and the two new examples have no Tested on: line until then. This branch sits on the P4X_EYE variant branch, so the diff against main includes those commits.

A plain Encoder runs the PPA's RGB565-to-YUV420 conversion and the H.264
encode one after the other, so a 1920x1072 frame costs about 40 + 38 ms
(12 fps). The two are separate hardware engines. With pipeline=True,
encode() converts frame N on the caller's thread while a worker task
encodes frame N-1, and returns frame N-1's access unit (b"" for the
first); flush() and close() return the last one. A frame then costs about
the longer of the two. stats() gains wall_us, how long encode() kept its
caller. The default stays the plain encoder, since the one-frame latency
changes what encode() returns.

The C API (castif's cast task) is unchanged. Built for ESP32_GENERIC_P4
P4X_EYE with h264enc, ppa, cameraif, vision; the hardware test now also
checks the pipelined stream against the plain one byte for byte. Not yet
run on a board.
espdl's detect(), classify() and predict() held the GIL for the whole
model run (70 to 80 ms for a 240x240 frame on the ESP32-P4), stopping
every other thread. The model run now happens without it, so detection
on a _thread leaves the main loop running; examples/espdl_thread.py
measures the main loop's longest pause during detection.

espdl.HumanFaceDetect(msr_path, mnp_path) runs ESP-DL's two-stage
human_face_detect models (MIT; release 0.5.0, the one for the pinned
esp-dl 3.3.6) and returns (x, y, w, h, score, keypoints) with ten
keypoint coordinates; the whole detection runs without the GIL.
examples/espdl_face_landmarks.py marks faces in a photo.

esp-vision's py_espdl.cpp stays as pinned: the two changes are patch
files in modules/vision/esp-vision-patches, applied to a copy in the
build tree. Built for ESP32_GENERIC_P4 P4X_EYE; not yet run on a board.
@bdbarnett

Copy link
Copy Markdown
Contributor Author

Run on the ESP32-P4X-EYE (OV2710 at 1920x1080, frames borrowed with cam.frame(), out_size=1 MB, 50 frames each):

Size Plain Pipelined Plain + preview ppa.srm Pipelined + preview
1920x1072 12.3 fps (ppa 41 ms, enc 37 ms) 13.2 fps (ppa 67 ms overlapped, wall 72 ms) 10.4 fps 11.1 fps
1280x720 25.5 fps (17 + 17 ms) 25.5 fps (wall 18 ms) 19.4 fps 20.7 fps

At 720p the pipeline halves the caller's wait (34 to 18 ms) and the sensor's 25 fps is the ceiling. At 1080p the PPA conversion is the limit and gets slower when it overlaps the encode (41 to 67 ms), so the pipeline buys one frame a second there. The README's "PPA is the limit" note is right; a YUV420 frame straight from the ISP is the next step for 1080p.

Internal RAM: with the board's display (DMA spibus, 128 KB ring) and camera up, the plain 1080p encoder leaves 15 KB internal and the pipelined one fails with "not enough internal RAM in one piece". 720p pipelined fits (104 KB).

espdl_thread-style check: ten detections of 73 ms on a _thread, the main loop's longest pause 20 ms. HumanFaceDetect opens with the factory's two model files and runs in 17 ms on an empty frame; its output on a face and the keypoint order are still to be checked. image.Image's own buffer is not 64-byte aligned, so capture_scaled needs a separate aligned buffer copied into it.

@bdbarnett
bdbarnett marked this pull request as ready for review November 9, 2026 03:15
@bdbarnett
bdbarnett merged commit 93c9e04 into main Nov 9, 2026
26 checks passed
@bdbarnett
bdbarnett deleted the p4x-eye-encode-vision branch November 9, 2026 03:17
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