Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -285,17 +285,26 @@ that no longer exists.
| [`camera_snapshot.py`](examples/camera_snapshot.py) | A still to the filesystem as hardware-encoded JPEG |
| [`camera_mjpeg.py`](examples/camera_mjpeg.py) | An MJPEG server any browser can open |
| [`camera_still.py`](examples/camera_still.py) | Live view, BOOT button takes the picture. The one that feels like a camera |

| [`camera_controls.py`](examples/camera_controls.py) | Discovering what a sensor supports, then sweeping it on screen |
| [`camera_frame.py`](examples/camera_frame.py) | Zero-copy `frame()` loop with `stats()` |
| [`camera_transform.py`](examples/camera_transform.py) | `capture_scaled` with rotate, mirror, and a cropped inset |
| [`camera_formats.py`](examples/camera_formats.py) | List `formats()`, then construct `Camera(...)` with a named format |
| [`camera_diag.py`](examples/camera_diag.py) | Bring-up: stats, test pattern, optional `reg()` |

The examples take the camera from `board_config`, so they run unchanged on
any board whose config provides one. On a board without one, construct a
`Camera` directly with your own pins.
`Camera` directly with your own pins -- see `camera_formats.py`.

`camera_still.py` is the one that uses `appdev` as the scheduler, which is
the house idiom for an application with input; the rest are plain scripts
because they have nothing to schedule.

USB webcam (the board presenting as UVC) lives in
[`usbif`](https://github.com/PyDevices/usbif)'s `examples/usbif_webcam.py`,
which sources frames from this module when a camera is present.

Known hole: [`available()` always returns True](https://github.com/PyDevices/cameraif/issues/1).

## Performance, measured

On an ESP32-P4 with an OV5647 at 800x800 RGB565:
Expand Down
69 changes: 69 additions & 0 deletions examples/camera_diag.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
"""Bring-up diagnostics when the picture is wrong or missing.

``stats()`` is the first thing to look at. If ``isr_done`` is not climbing,
nothing is arriving and the problem is upstream of this module -- power,
clock, lanes, or the sensor not streaming. If it is climbing and ``frames``
is not, frames are arriving and being rejected; ``last_received`` says how
big they were.

The sensor's test pattern is generated inside the sensor, after the pixel
array and before everything else. Pattern clean + picture broken means the
fault is in front of the sensor (optics, light, lens cap). Pattern broken
too means everything after the pixel array is suspect.

mpremote run camera_diag.py

``reg()`` is there for a deliberate poke when you already know which
register you mean -- not a tour of the sensor map.
"""

import time

import board_config
from board_config import display_drv as display


def main():
cam = board_config.camera
print("sensor %s id 0x%04x" % cam.sensor())
print("size", cam.size())
print("formats:")
for name, w, h in cam.formats():
print(" ", name, w, h)
print("controls", cam.controls())

fb = display.framebuffers()[0]

print("\n-- live scene, 2 s --")
cam.test_pattern(False)
end = time.ticks_add(time.ticks_ms(), 2000)
while time.ticks_diff(end, time.ticks_ms()) > 0:
cam.capture_scaled(fb, display.width, display.height, timeout=500)
display.show()
print("stats", cam.stats())

print("\n-- test pattern, 2 s --")
cam.test_pattern(True)
end = time.ticks_add(time.ticks_ms(), 2000)
while time.ticks_diff(end, time.ticks_ms()) > 0:
cam.capture_scaled(fb, display.width, display.height, timeout=500)
display.show()
print("stats", cam.stats())
print("test_pattern() reads back last ask:", cam.test_pattern())

# A single register read as a smoke check that SCCB still answers.
# 0x300A/0x300B are the OV5647 chip id registers; other sensors will
# return something else or raise -- that is still information.
try:
print("reg(0x300A)=", hex(cam.reg(0x300A)),
"reg(0x300B)=", hex(cam.reg(0x300B)))
except Exception as exc:
print("reg() not usable on this sensor/driver:", exc)

cam.test_pattern(False)
cam.deinit()
print("done")


if __name__ == "__main__":
main()
86 changes: 86 additions & 0 deletions examples/camera_formats.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
"""List the sensor's formats, then open one by name.

``board_config.camera`` picks the driver's default. This example shows the
board-agnostic constructor: pins from the board, format chosen from
``formats()``. Adding a different camera is a build-time ``CONFIG_CAMERA_*``,
not a code change here -- which is why the class is ``Camera``, not ``OV5647``.

mpremote run camera_formats.py

Closes the board_config camera first (only one ``Camera`` may exist -- the
P4 has one CSI controller), then constructs a fresh one with an explicit
format.
"""

import time

import board_config
import cameraif
from board_config import display_drv as display


def _pins_from_board():
"""Reuse whatever board_config used to open its camera."""
# board_config typically keeps constructor kwargs on the helper; fall
# back to the documented Waveshare P4-WIFI6-Touch-LCD-4B defaults if not.
cam_helper = getattr(board_config, "camera_pins", None)
if callable(cam_helper):
return cam_helper()
pins = getattr(board_config, "CAMERA_PINS", None)
if isinstance(pins, dict):
return pins
# Waveshare ESP32-P4-WIFI6-Touch-LCD-4B (the reference board).
return {"sda": 7, "scl": 8, "reset": 21, "i2c": 0}


def main():
# Peek at formats through the board's already-open camera, then release
# it so we can construct our own.
cam = board_config.camera
print("default sensor %s id 0x%04x" % cam.sensor())
offered = list(cam.formats())
print("formats this driver offers:")
for name, w, h in offered:
print(" %-48s %dx%d" % (name, w, h))
cam.deinit()

if not offered:
print("no formats; nothing to open")
return

# Prefer a format that fits the panel when one exists; else the first.
choice = offered[0]
for name, w, h in offered:
if w <= display.width and h <= display.height:
choice = (name, w, h)
break
name, w, h = choice
print("opening format %r (%dx%d)" % (name, w, h))

pins = _pins_from_board()
cam = cameraif.Camera(pins["sda"], pins["scl"],
format=name,
i2c=pins.get("i2c", 0),
reset=pins.get("reset", -1),
pwdn=pins.get("pwdn", -1),
xclk=pins.get("xclk", -1))
print("now", cam.sensor(), cam.size())

fb = display.framebuffers()[0]
frames = 0
t0 = time.ticks_ms()
try:
while frames < 60:
if cam.capture_scaled(fb, display.width, display.height,
timeout=500) is None:
continue
display.show()
frames += 1
dt = time.ticks_diff(time.ticks_ms(), t0)
print("%d frames in %d ms (%.1f fps)" % (frames, dt, frames * 1000 / dt))
finally:
cam.deinit()


if __name__ == "__main__":
main()
45 changes: 45 additions & 0 deletions examples/camera_frame.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
"""Zero-copy frames from the camera, with stats.

``frame()`` returns a memoryview of the buffer the DMA just filled -- no
copy. Valid until the next ``frame()`` or ``capture()`` on this camera.
Prints ``stats()`` so you can see whether frames are arriving (ISR climbing)
or being dropped.

mpremote run camera_frame.py

This is the "I have the pixels" use, not the panel use. For the camera on
the board's own screen see ``camera_preview.py``.
"""

import time

import board_config


def main(seconds=5):
cam = board_config.camera
print("sensor %s %dx%d" % ((cam.sensor()[0],) + cam.size()[:2]))

frames = 0
t0 = time.ticks_ms()
deadline = time.ticks_add(t0, seconds * 1000)
try:
while time.ticks_diff(deadline, time.ticks_ms()) > 0:
mv = cam.frame(500)
if mv is None:
continue
frames += 1
if frames % 30 == 0:
st = cam.stats()
dt = time.ticks_diff(time.ticks_ms(), t0)
print("%d frames, %.1f fps, %d bytes; stats %r"
% (frames, frames * 1000 / dt, len(mv), st))
except KeyboardInterrupt:
pass
finally:
print("final stats", cam.stats())
cam.deinit()


if __name__ == "__main__":
main()
65 changes: 65 additions & 0 deletions examples/camera_transform.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
"""Scale, rotate, mirror, and crop the camera onto the panel via the PPA.

``capture_scaled`` hands the frame to the Pixel Processing Accelerator:
rotate and mirror are free in the same pass as the scale. The destination is
described as a whole picture plus the rectangle to write inside it -- exactly
a panel framebuffer.

mpremote run camera_transform.py

Cycles through a few orientations so the effect is obvious on the glass.
"""

import time

import board_config
from board_config import display_drv as display


ORIENTATIONS = (
(0, False, "normal"),
(90, False, "rotate 90"),
(180, False, "rotate 180"),
(270, False, "rotate 270"),
(0, True, "mirrored"),
(180, True, "rotate 180 + mirror"),
)


def main(hold_s=2):
cam = board_config.camera
fb = display.framebuffers()[0]
print("sensor %s %dx%d -> panel %dx%d"
% ((cam.sensor()[0],) + cam.size()[:2] + (display.width, display.height)))

try:
for rotate, mirror, label in ORIENTATIONS:
print(label)
end = time.ticks_add(time.ticks_ms(), int(hold_s * 1000))
while time.ticks_diff(end, time.ticks_ms()) > 0:
# Full panel; crop example: pass x,y,w,h to write into a
# quadrant instead -- same call, smaller rectangle.
if cam.capture_scaled(fb, display.width, display.height,
rotate=rotate, mirror=mirror,
timeout=500) is None:
continue
display.show()
# And a cropped inset: centre quarter of the panel.
w, h = display.width // 2, display.height // 2
x, y = (display.width - w) // 2, (display.height - h) // 2
print(" + cropped inset %dx%d at (%d,%d)" % (w, h, x, y))
display.fill(0)
if cam.capture_scaled(fb, display.width, display.height,
x=x, y=y, w=w, h=h,
rotate=rotate, mirror=mirror,
timeout=500) is not None:
display.show()
time.sleep_ms(800)
except KeyboardInterrupt:
print("stopped")
finally:
cam.deinit()


if __name__ == "__main__":
main()
Loading