diff --git a/README.md b/README.md index 7d44e30..4f424e6 100644 --- a/README.md +++ b/README.md @@ -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: diff --git a/examples/camera_diag.py b/examples/camera_diag.py new file mode 100644 index 0000000..fcd46f9 --- /dev/null +++ b/examples/camera_diag.py @@ -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() diff --git a/examples/camera_formats.py b/examples/camera_formats.py new file mode 100644 index 0000000..4fe67ec --- /dev/null +++ b/examples/camera_formats.py @@ -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() diff --git a/examples/camera_frame.py b/examples/camera_frame.py new file mode 100644 index 0000000..1d034cf --- /dev/null +++ b/examples/camera_frame.py @@ -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() diff --git a/examples/camera_transform.py b/examples/camera_transform.py new file mode 100644 index 0000000..1ba4116 --- /dev/null +++ b/examples/camera_transform.py @@ -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()