@@ -5,3 +5,5 @@ build/
patches/
*.patch
*.pyc
+subprojects/packagecache/
+subprojects/pybind11-2.3.0/
@@ -37,3 +37,5 @@ option('v4l2',
type : 'boolean',
value : false,
description : 'Compile the V4L2 compatibility layer')
+
+option('pycamera', type : 'feature', value : 'auto')
@@ -23,3 +23,4 @@ if get_option('v4l2')
endif
subdir('gstreamer')
+subdir('py')
new file mode 100644
@@ -0,0 +1 @@
+subdir('pycamera')
new file mode 100644
@@ -0,0 +1,11 @@
+from .pycamera import *
+from enum import Enum
+import os
+import struct
+import mmap
+
+
+def __FrameBuffer__mmap(self, plane):
+ return mmap.mmap(self.fd(plane), self.length(plane), mmap.MAP_SHARED, mmap.PROT_READ)
+
+FrameBuffer.mmap = __FrameBuffer__mmap
new file mode 100644
@@ -0,0 +1,38 @@
+# SPDX-License-Identifier: CC0-1.0
+
+py3_dep = dependency('python3', required : get_option('pycamera'))
+
+if py3_dep.found() == false
+ subdir_done()
+endif
+
+pybind11_proj = subproject('pybind11')
+pybind11_dep = pybind11_proj.get_variable('pybind11_dep')
+
+pycamera_sources = files([
+ 'pymain.cpp',
+])
+
+pycamera_deps = [
+ libcamera_dep,
+ py3_dep,
+ pybind11_dep,
+]
+
+pycamera_args = [ '-fvisibility=hidden' ]
+pycamera_args += [ '-Wno-shadow' ]
+
+destdir = get_option('libdir') + '/python' + py3_dep.version() + '/site-packages/pycamera'
+
+pycamera = shared_module('pycamera',
+ pycamera_sources,
+ install : true,
+ install_dir : destdir,
+ name_prefix : '',
+ dependencies : pycamera_deps,
+ cpp_args : pycamera_args)
+
+# Copy __init__.py to build dir so that we can run without installing
+configure_file(input: '__init__.py', output: '__init__.py', copy: true)
+
+install_data(['__init__.py'], install_dir: destdir)
new file mode 100644
@@ -0,0 +1,382 @@
+#include <chrono>
+#include <thread>
+#include <fcntl.h>
+#include <unistd.h>
+#include <sys/mman.h>
+#include <sys/eventfd.h>
+#include <mutex>
+
+#include <pybind11/pybind11.h>
+#include <pybind11/stl.h>
+#include <pybind11/stl_bind.h>
+#include <pybind11/functional.h>
+
+#include <libcamera/libcamera.h>
+
+namespace py = pybind11;
+
+using namespace std;
+using namespace libcamera;
+
+static py::object ControlValueToPy(const ControlValue &cv)
+{
+ //assert(!cv.isArray());
+ //assert(cv.numElements() == 1);
+
+ switch (cv.type()) {
+ case ControlTypeBool:
+ return py::cast(cv.get<bool>());
+ case ControlTypeByte:
+ return py::cast(cv.get<uint8_t>());
+ case ControlTypeInteger32:
+ return py::cast(cv.get<int32_t>());
+ case ControlTypeInteger64:
+ return py::cast(cv.get<int64_t>());
+ case ControlTypeFloat:
+ return py::cast(cv.get<float>());
+ case ControlTypeString:
+ return py::cast(cv.get<string>());
+ case ControlTypeRectangle:
+ case ControlTypeSize:
+ case ControlTypeNone:
+ default:
+ throw runtime_error("Unsupported ControlValue type");
+ }
+}
+
+static ControlValue PyToControlValue(py::object &ob, ControlType type)
+{
+ switch (type) {
+ case ControlTypeBool:
+ return ControlValue(ob.cast<bool>());
+ case ControlTypeByte:
+ return ControlValue(ob.cast<uint8_t>());
+ case ControlTypeInteger32:
+ return ControlValue(ob.cast<int32_t>());
+ case ControlTypeInteger64:
+ return ControlValue(ob.cast<int64_t>());
+ case ControlTypeFloat:
+ return ControlValue(ob.cast<float>());
+ case ControlTypeString:
+ return ControlValue(ob.cast<string>());
+ case ControlTypeRectangle:
+ case ControlTypeSize:
+ case ControlTypeNone:
+ default:
+ throw runtime_error("Control type not implemented");
+ }
+}
+
+struct CameraEvent
+{
+ shared_ptr<Camera> camera;
+ Request::Status status;
+ map<const Stream *, FrameBuffer *> bufmap;
+ ControlList metadata;
+ uint64_t cookie;
+};
+
+static int g_eventfd;
+static mutex g_buflist_mutex;
+static vector<CameraEvent> g_buflist;
+
+static void handle_request_completed(Request *req)
+{
+ CameraEvent ev;
+ ev.camera = req->camera();
+ ev.status = req->status();
+ ev.bufmap = req->buffers();
+ ev.metadata = req->metadata();
+ ev.cookie = req->cookie();
+
+ {
+ lock_guard guard(g_buflist_mutex);
+ g_buflist.push_back(ev);
+ }
+
+ uint64_t v = 1;
+ write(g_eventfd, &v, 8);
+}
+
+PYBIND11_MODULE(pycamera, m)
+{
+ py::class_<CameraEvent>(m, "CameraEvent")
+ .def_readonly("camera", &CameraEvent::camera)
+ .def_readonly("status", &CameraEvent::status)
+ .def_readonly("buffers", &CameraEvent::bufmap)
+ .def_property_readonly("metadata", [](const CameraEvent& self) {
+ py::dict ret;
+
+ for (const auto &[key, cv] : self.metadata) {
+ const ControlId *id = properties::properties.at(key);
+ py::object ob = ControlValueToPy(cv);
+
+ ret[id->name().c_str()] = ob;
+ }
+
+ return ret;
+ })
+ .def_readonly("cookie", &CameraEvent::cookie)
+ ;
+
+ py::class_<CameraManager>(m, "CameraManager")
+ /*
+ * CameraManager::stop() cannot be called, as CameraManager expects all Camera
+ * instances to be released before calling stop and we can't have such requirement
+ * in python, especially as we have a keep-alive from Camera to CameraManager.
+ * So we rely on GC and the keep-alives, and call CameraManager::start() from
+ * the constructor.
+ */
+
+ .def(py::init([]() {
+ g_eventfd = eventfd(0, 0);
+
+ auto cm = make_unique<CameraManager>();
+ cm->start();
+ return cm;
+ }))
+
+ .def_property_readonly("efd", [](CameraManager &) {
+ return g_eventfd;
+ })
+
+ .def("get_ready_requests", [](CameraManager &) {
+ vector<CameraEvent> v;
+
+ {
+ lock_guard guard(g_buflist_mutex);
+ swap(v, g_buflist);
+ }
+
+ return v;
+ })
+
+ .def("get", py::overload_cast<const string &>(&CameraManager::get))
+ .def("find", [](CameraManager &self, string str) {
+ std::transform(str.begin(), str.end(), str.begin(), ::tolower);
+
+ for (auto c : self.cameras()) {
+ string id = c->id();
+
+ std::transform(id.begin(), id.end(), id.begin(), ::tolower);
+
+ if (id.find(str) != string::npos)
+ return c;
+ }
+
+ return shared_ptr<Camera>();
+ })
+ .def_property_readonly("version", &CameraManager::version)
+
+ // Create a list of Cameras, where each camera has a keep-alive to CameraManager
+ .def_property_readonly("cameras", [](CameraManager &self) {
+ py::list l;
+ for (auto &c : self.cameras()) {
+ py::object py_cm = py::cast(self);
+ py::object py_cam = py::cast(c);
+ py::detail::keep_alive_impl(py_cam, py_cm);
+ l.append(py_cam);
+ }
+ return l;
+ });
+
+ py::class_<Camera, shared_ptr<Camera>>(m, "Camera", py::dynamic_attr())
+ .def_property_readonly("id", &Camera::id)
+ .def("acquire", &Camera::acquire)
+ .def("release", &Camera::release)
+ .def("start", [](shared_ptr<Camera> &self) {
+ self->requestCompleted.connect(handle_request_completed);
+
+ self->start();
+ })
+
+ .def("stop", [](shared_ptr<Camera> &self) {
+ self->stop();
+
+ self->requestCompleted.disconnect(handle_request_completed);
+ })
+
+ .def("__repr__", [](shared_ptr<Camera> &self) {
+ return "<pycamera.Camera '" + self->id() + "'>";
+ })
+
+ // Keep the camera alive, as StreamConfiguration contains a Stream*
+ .def("generateConfiguration", &Camera::generateConfiguration, py::keep_alive<0, 1>())
+ .def("configure", &Camera::configure)
+
+ // XXX created requests MUST be queued to be freed, python will not free them
+ .def("createRequest", &Camera::createRequest, py::arg("cookie") = 0, py::return_value_policy::reference_internal)
+ .def("queueRequest", &Camera::queueRequest)
+
+ .def_property_readonly("streams", [](Camera &self) {
+ py::set set;
+ for (auto &s : self.streams()) {
+ py::object py_self = py::cast(self);
+ py::object py_s = py::cast(s);
+ py::detail::keep_alive_impl(py_s, py_self);
+ set.add(py_s);
+ }
+ return set;
+ })
+
+ .def_property_readonly("controls", [](Camera &self) {
+ py::dict ret;
+
+ for (const auto &[id, ci] : self.controls()) {
+ ret[id->name().c_str()] = make_tuple<py::object>(ControlValueToPy(ci.min()),
+ ControlValueToPy(ci.max()),
+ ControlValueToPy(ci.def()));
+ }
+
+ return ret;
+ })
+
+ .def_property_readonly("properties", [](Camera &self) {
+ py::dict ret;
+
+ for (const auto &[key, cv] : self.properties()) {
+ const ControlId *id = properties::properties.at(key);
+ py::object ob = ControlValueToPy(cv);
+
+ ret[id->name().c_str()] = ob;
+ }
+
+ return ret;
+ });
+
+ py::class_<CameraConfiguration>(m, "CameraConfiguration")
+ .def("at", py::overload_cast<unsigned int>(&CameraConfiguration::at), py::return_value_policy::reference_internal)
+ .def("validate", &CameraConfiguration::validate)
+ .def_property_readonly("size", &CameraConfiguration::size)
+ .def_property_readonly("empty", &CameraConfiguration::empty);
+
+ py::class_<StreamConfiguration>(m, "StreamConfiguration")
+ .def("toString", &StreamConfiguration::toString)
+ .def_property_readonly("stream", &StreamConfiguration::stream, py::return_value_policy::reference_internal)
+ .def_property(
+ "size",
+ [](StreamConfiguration &self) { return make_tuple(self.size.width, self.size.height); },
+ [](StreamConfiguration &self, tuple<uint32_t, uint32_t> size) { self.size.width = get<0>(size); self.size.height = get<1>(size); })
+ .def_property(
+ "fmt",
+ [](StreamConfiguration &self) { return self.pixelFormat.toString(); },
+ [](StreamConfiguration &self, string fmt) { self.pixelFormat = PixelFormat::fromString(fmt); })
+ .def_readwrite("stride", &StreamConfiguration::stride)
+ .def_readwrite("frameSize", &StreamConfiguration::frameSize)
+ .def_readwrite("bufferCount", &StreamConfiguration::bufferCount)
+ .def_property_readonly("formats", &StreamConfiguration::formats, py::return_value_policy::reference_internal);
+ ;
+
+ py::class_<StreamFormats>(m, "StreamFormats")
+ .def_property_readonly("pixelFormats", [](StreamFormats &self) {
+ vector<string> fmts;
+ for (auto &fmt : self.pixelformats())
+ fmts.push_back(fmt.toString());
+ return fmts;
+ })
+ .def("sizes", [](StreamFormats &self, const string &pixelFormat) {
+ auto fmt = PixelFormat::fromString(pixelFormat);
+ vector<tuple<uint32_t, uint32_t>> fmts;
+ for (const auto &s : self.sizes(fmt))
+ fmts.push_back(make_tuple(s.width, s.height));
+ return fmts;
+ })
+ .def("range", [](StreamFormats &self, const string &pixelFormat) {
+ auto fmt = PixelFormat::fromString(pixelFormat);
+ const auto &range = self.range(fmt);
+ return make_tuple(make_tuple(range.hStep, range.vStep),
+ make_tuple(range.min.width, range.min.height),
+ make_tuple(range.max.width, range.max.height));
+ });
+
+ py::enum_<StreamRole>(m, "StreamRole")
+ .value("StillCapture", StreamRole::StillCapture)
+ .value("StillCaptureRaw", StreamRole::Raw)
+ .value("VideoRecording", StreamRole::VideoRecording)
+ .value("Viewfinder", StreamRole::Viewfinder);
+
+ py::class_<FrameBufferAllocator>(m, "FrameBufferAllocator")
+ .def(py::init<shared_ptr<Camera>>(), py::keep_alive<1, 2>())
+ .def("allocate", &FrameBufferAllocator::allocate)
+ .def("free", &FrameBufferAllocator::free)
+ .def_property_readonly("allocated", &FrameBufferAllocator::allocated)
+ // Create a list of FrameBuffer, where each FrameBuffer has a keep-alive to FrameBufferAllocator
+ .def("buffers", [](FrameBufferAllocator &self, Stream *stream) {
+ py::list l;
+ for (auto &ub : self.buffers(stream)) {
+ py::object py_fa = py::cast(self);
+ py::object py_buf = py::cast(ub.get());
+ py::detail::keep_alive_impl(py_buf, py_fa);
+ l.append(py_buf);
+ }
+ return l;
+ });
+
+ py::class_<FrameBuffer, unique_ptr<FrameBuffer, py::nodelete>>(m, "FrameBuffer")
+ // XXX who frees this
+ .def(py::init([](vector<tuple<int, unsigned int>> planes, unsigned int cookie) {
+ vector<FrameBuffer::Plane> v;
+ for (const auto& t : planes)
+ v.push_back({FileDescriptor(get<0>(t)), get<1>(t)});
+ return new FrameBuffer(v, cookie);
+ }))
+ .def_property_readonly("metadata", &FrameBuffer::metadata, py::return_value_policy::reference_internal)
+ .def("length", [](FrameBuffer &self, uint32_t idx) {
+ const FrameBuffer::Plane &plane = self.planes()[idx];
+ return plane.length;
+ })
+ .def("fd", [](FrameBuffer &self, uint32_t idx) {
+ const FrameBuffer::Plane &plane = self.planes()[idx];
+ return plane.fd.fd();
+ })
+ .def_property("cookie", &FrameBuffer::cookie, &FrameBuffer::setCookie);
+
+ py::class_<Stream, unique_ptr<Stream, py::nodelete>>(m, "Stream")
+ .def_property_readonly("configuration", &Stream::configuration);
+
+ py::class_<Request, unique_ptr<Request, py::nodelete>>(m, "Request")
+ .def("addBuffer", &Request::addBuffer)
+ .def_property_readonly("status", &Request::status)
+ .def_property_readonly("buffers", &Request::buffers)
+ .def_property_readonly("cookie", &Request::cookie)
+ .def_property_readonly("hasPendingBuffers", &Request::hasPendingBuffers)
+ .def("set_control", [](Request &self, string &control, py::object value) {
+ const auto &controls = self.camera()->controls();
+
+ auto it = find_if(controls.begin(), controls.end(),
+ [&control](const auto &kvp) { return kvp.first->name() == control; });
+
+ if (it == controls.end())
+ throw runtime_error("Control not found");
+
+ const auto &id = it->first;
+
+ self.controls().set(id->id(), PyToControlValue(value, id->type()));
+ });
+
+ py::enum_<Request::Status>(m, "RequestStatus")
+ .value("Pending", Request::RequestPending)
+ .value("Complete", Request::RequestComplete)
+ .value("Cancelled", Request::RequestCancelled);
+
+ py::enum_<FrameMetadata::Status>(m, "FrameMetadataStatus")
+ .value("Success", FrameMetadata::FrameSuccess)
+ .value("Error", FrameMetadata::FrameError)
+ .value("Cancelled", FrameMetadata::FrameCancelled);
+
+ py::class_<FrameMetadata>(m, "FrameMetadata")
+ .def_readonly("status", &FrameMetadata::status)
+ .def_readonly("sequence", &FrameMetadata::sequence)
+ .def_readonly("timestamp", &FrameMetadata::timestamp)
+ .def_property_readonly("bytesused", [](FrameMetadata &self) {
+ vector<unsigned int> v;
+ v.resize(self.planes.size());
+ transform(self.planes.begin(), self.planes.end(), v.begin(), [](const auto &p) { return p.bytesused; });
+ return v;
+ });
+
+ py::enum_<CameraConfiguration::Status>(m, "ConfigurationStatus")
+ .value("Valid", CameraConfiguration::Valid)
+ .value("Adjusted", CameraConfiguration::Adjusted)
+ .value("Invalid", CameraConfiguration::Invalid);
+}
new file mode 100755
@@ -0,0 +1,129 @@
+#!/usr/bin/python3
+
+from simplecamera import SimpleCameraManager, SimpleCamera
+import pykms
+import pycamera as pycam
+import time
+import argparse
+import selectors
+import sys
+
+card = pykms.Card()
+
+res = pykms.ResourceManager(card)
+conn = res.reserve_connector()
+crtc = res.reserve_crtc(conn)
+plane = res.reserve_generic_plane(crtc)
+mode = conn.get_default_mode()
+modeb = mode.to_blob(card)
+
+req = pykms.AtomicReq(card)
+req.add_connector(conn, crtc)
+req.add_crtc(crtc, modeb)
+req.commit_sync(allow_modeset = True)
+
+class ScreenHandler:
+ def __init__(self, card, crtc, plane):
+ self.card = card
+ self.crtc = crtc
+ self.plane = plane
+ self.bufqueue = []
+ self.current = None
+ self.next = None
+
+ def handle_page_flip(self, frame, time):
+ old = self.current
+ self.current = self.next
+
+ if len(self.bufqueue) > 0:
+ self.next = self.bufqueue.pop(0)
+ else:
+ self.next = None
+
+ if self.next:
+ req = pykms.AtomicReq(self.card)
+ req.add_plane(self.plane, fb, self.crtc, dst=(0, 0, fb.width, fb.height))
+ req.commit()
+
+ return old
+
+ def queue(self, fb):
+ if not self.next:
+ self.next = fb
+
+ req = pykms.AtomicReq(self.card)
+ req.add_plane(self.plane, fb, self.crtc, dst=(0, 0, fb.width, fb.height))
+ req.commit()
+ else:
+ self.bufqueue.append(fb)
+
+
+
+
+screen = ScreenHandler(card, crtc, plane)
+
+
+
+def handle_camera_frame(camera, stream, fb):
+ screen.queue(cam_2_drm_map[fb])
+
+cm = SimpleCameraManager()
+cam = cm.find("imx219")
+cam.open()
+
+cam.format = "ARGB8888"
+cam.resolution = (1920, 1080)
+
+cam.callback = lambda stream, fb, camera=cam: handle_camera_frame(camera, stream, fb)
+
+cam_2_drm_map = {}
+drm_2_cam_map = {}
+
+cam.xxx_config()
+
+drmbuffers = []
+stream_cfg = cam.stream_config
+for fb in cam.buffers:
+ w, h = stream_cfg.size
+ stride = stream_cfg.stride
+ drmfb = pykms.DmabufFramebuffer(card, w, h, pykms.PixelFormat.ARGB8888,
+ [fb.fd(0)], [stride], [0])
+ drmbuffers.append(drmfb)
+
+ cam_2_drm_map[fb] = drmfb
+ drm_2_cam_map[drmfb] = fb
+
+
+cam.start()
+
+def readdrm(fileobj, mask):
+ for ev in card.read_events():
+ if ev.type == pykms.DrmEventType.FLIP_COMPLETE:
+ old = screen.handle_page_flip(ev.seq, ev.time)
+
+ if old:
+ fb = drm_2_cam_map[old]
+ cam.queue_fb(fb)
+
+running = True
+
+def readkey(fileobj, mask):
+ global running
+ sys.stdin.readline()
+ running = False
+
+sel = selectors.DefaultSelector()
+sel.register(card.fd, selectors.EVENT_READ, readdrm)
+sel.register(sys.stdin, selectors.EVENT_READ, readkey)
+
+print("Press enter to exit")
+
+while running:
+ events = sel.select()
+ for key, mask in events:
+ callback = key.data
+ callback(key.fileobj, mask)
+
+cam.stop()
+
+print("Done")
new file mode 100755
@@ -0,0 +1,154 @@
+#!/usr/bin/python3 -i
+
+from simplecamera import SimpleCameraManager, SimpleCamera
+from PyQt5 import QtCore, QtGui, QtWidgets
+import pycamera as pycam
+import argparse
+
+parser = argparse.ArgumentParser()
+parser.add_argument("-c", "--cameras", type=str, default=None)
+args = parser.parse_args()
+
+format_map = {
+ "YUYV": QtGui.QImage.Format.Format_RGB16,
+ "BGR888": QtGui.QImage.Format.Format_RGB888,
+ "MJPEG": QtGui.QImage.Format.Format_RGB888,
+}
+
+
+class MainWindow(QtWidgets.QWidget):
+ requestDone = QtCore.pyqtSignal(pycam.Stream, pycam.FrameBuffer)
+
+ def __init__(self, camera):
+ super().__init__()
+
+ # Use signal to handle request, so that the execution is transferred to the main thread
+ self.requestDone.connect(self.handle_request)
+ camera.callback = lambda stream, fb: self.requestDone.emit(stream, fb)
+
+ camera.xxx_config()
+
+ self.camera = camera
+
+ self.label = QtWidgets.QLabel()
+
+ windowLayout = QtWidgets.QHBoxLayout()
+ self.setLayout(windowLayout)
+
+ windowLayout.addWidget(self.label)
+
+ controlsLayout = QtWidgets.QVBoxLayout()
+ windowLayout.addLayout(controlsLayout)
+
+ windowLayout.addStretch()
+
+ group = QtWidgets.QGroupBox("Info")
+ groupLayout = QtWidgets.QVBoxLayout()
+ group.setLayout(groupLayout)
+ controlsLayout.addWidget(group)
+
+ lab = QtWidgets.QLabel(camera.id)
+ groupLayout.addWidget(lab)
+
+ self.frameLabel = QtWidgets.QLabel()
+ groupLayout.addWidget(self.frameLabel)
+
+
+ group = QtWidgets.QGroupBox("Properties")
+ groupLayout = QtWidgets.QVBoxLayout()
+ group.setLayout(groupLayout)
+ controlsLayout.addWidget(group)
+
+ for k, v in camera.properties.items():
+ lab = QtWidgets.QLabel()
+ lab.setText(k + " = " + str(v))
+ groupLayout.addWidget(lab)
+
+ group = QtWidgets.QGroupBox("Controls")
+ groupLayout = QtWidgets.QVBoxLayout()
+ group.setLayout(groupLayout)
+ controlsLayout.addWidget(group)
+
+ for k, (min, max, default) in camera.controls.items():
+ lab = QtWidgets.QLabel()
+ lab.setText("{} = {}/{}/{}".format(k, min, max, default))
+ groupLayout.addWidget(lab)
+
+ controlsLayout.addStretch()
+
+ self.camera.start()
+
+ def closeEvent(self, event):
+ self.camera.stop()
+ super().closeEvent(event)
+
+ def handle_request(self, stream, fb):
+ global format_map
+
+ #meta = fb.metadata
+ #print("Buf seq {}, bytes {}".format(meta.sequence, meta.bytesused))
+
+ with fb.mmap(0) as b:
+ cfg = stream.configuration
+ qfmt = format_map[cfg.fmt]
+ w, h = cfg.size
+ pitch = cfg.stride
+ img = QtGui.QImage(b, w, h, pitch, qfmt)
+ self.label.setPixmap(QtGui.QPixmap.fromImage(img))
+
+ self.frameLabel.setText("Queued: {}\nDone: {}".format(camera.reqs_queued, camera.reqs_completed))
+
+ self.camera.queue_fb(fb)
+
+
+app = QtWidgets.QApplication([])
+cm = SimpleCameraManager()
+
+notif = QtCore.QSocketNotifier(cm.cm.efd, QtCore.QSocketNotifier.Read)
+notif.activated.connect(lambda x: cm.read_events())
+
+if not args.cameras:
+ cameras = cm.cameras
+else:
+ cameras = []
+ for name in args.cameras.split(","):
+ c = cm.find(name)
+ if not c:
+ print("Camera not found: ", name)
+ exit(-1)
+ cameras.append(c)
+
+windows = []
+
+i = 0
+for camera in cameras:
+ globals()["cam" + str(i)] = camera
+ i += 1
+
+ camera.open()
+
+ fmts = camera.formats
+ if "BGR888" in fmts:
+ camera.format = "BGR888"
+ elif "YUYV" in fmts:
+ camera.format = "YUYV"
+ else:
+ raise Exception("Unsupported pixel format")
+
+ camera.resolution = (640, 480)
+
+ window = MainWindow(camera)
+ window.setAttribute(QtCore.Qt.WA_ShowWithoutActivating)
+ window.show()
+ windows.append(window)
+
+def cleanup():
+ for w in windows:
+ w.close()
+
+ for camera in cameras:
+ camera.close()
+ print("Done")
+
+import atexit
+atexit.register(cleanup)
new file mode 100755
@@ -0,0 +1,6 @@
+#!/bin/bash
+
+export PYTHONMALLOC=malloc
+export PYTHONPATH=../../../build/debug/src/py
+
+valgrind --suppressions=valgrind-pycamera.supp --leak-check=full --show-leak-kinds=definite --gen-suppressions=yes python3 test.py $*
new file mode 100755
@@ -0,0 +1,3 @@
+#!/bin/bash
+
+PYTHONPATH=../../../build/debug/src/py python3 test.py $*
new file mode 100644
@@ -0,0 +1,198 @@
+import pycamera as pycam
+import os
+
+class SimpleCameraManager:
+ def __init__(self):
+ self.cm = pycam.CameraManager()
+
+ self.cameras = []
+ for c in self.cm.cameras:
+ self.cameras.append(SimpleCamera(c))
+
+ def find(self, name):
+ for c in self.cameras:
+ if name.lower() in c.id.lower():
+ return c
+
+ return None
+
+ def read_events(self):
+ data = os.read(self.cm.efd, 8)
+
+ reqs = self.cm.get_ready_requests()
+
+ for req in reqs:
+ for c in self.cameras:
+ if c.pycam == req.camera:
+ c.req_complete_cb(req)
+
+class SimpleCamera:
+ def __init__(self, camera):
+ self.pycam = camera
+
+ self.callback = None
+
+ self.control_values = {}
+ #for k, (min, max, default) in self.pycam.controls.items():
+ # self.control_values[k] = default
+
+ self.running = False
+
+ def __repr__(self):
+ return "<SimpleCamera '" + self.pycam.id + "'>"
+
+ @property
+ def id(self):
+ return self.pycam.id
+
+ @property
+ def formats(self):
+ return self.stream_config.formats.pixelFormats
+
+ def open(self):
+ self.pycam.acquire()
+
+ self.camera_config = self.pycam.generateConfiguration([pycam.StreamRole.Viewfinder])
+ self.stream_config = self.camera_config.at(0)
+
+ def close(self):
+ self.pycam.release()
+
+ @property
+ def properties(self):
+ return self.pycam.properties
+
+ @property
+ def controls(self):
+ return self.pycam.controls
+
+ def xxx_config(self):
+ self.configure_camera()
+ self.alloc_buffers()
+
+ def start(self):
+ self.reqs_queued = 0
+ self.reqs_completed = 0
+
+ self.running = True
+ self.pycam.start()
+
+ self.queue_initial_fbs()
+
+ def stop(self):
+ self.running = False
+
+ self.pycam.stop()
+
+ self.buffers = None
+
+ @property
+ def resolution(self):
+ return self.stream_config.size
+
+ @resolution.setter
+ def resolution(self, val):
+ running = self.running
+ if running:
+ self.stop()
+
+ self.stream_config.size = val
+ self.camera_config.validate()
+
+ if running:
+ self.start()
+
+ @property
+ def format(self):
+ return self.stream_config.fmt
+
+ @format.setter
+ def format(self, val):
+ running = self.running
+ if running:
+ self.stop()
+
+ self.stream_config.fmt = val
+ self.camera_config.validate()
+
+ if running:
+ self.start()
+
+ def configure_camera(self):
+ camera = self.pycam
+
+ status = self.camera_config.validate()
+
+ if status == pycam.ConfigurationStatus.Invalid:
+ raise Exception("Invalid configuration")
+
+ print("Cam: config {}".format(self.stream_config.toString()))
+
+ camera.configure(self.camera_config);
+
+ def alloc_buffers(self):
+ camera = self.pycam
+ stream = self.stream_config.stream
+
+ allocator = pycam.FrameBufferAllocator(camera);
+ ret = allocator.allocate(stream)
+ if ret < 0:
+ raise Exception("Can't allocate buffers")
+
+ self.buffers = allocator.buffers(stream)
+
+ print("Cam: Allocated {} buffers for stream".format(len(self.buffers)))
+
+ def queue_initial_fbs(self):
+ buffers = self.buffers
+
+ for fb in buffers:
+ self.queue_fb(fb)
+
+ def queue_fb(self, fb):
+ camera = self.pycam
+ stream = self.stream_config.stream
+
+ request = camera.createRequest()
+
+ if request == None:
+ raise Exception("Can't create request")
+
+ ret = request.addBuffer(stream, fb)
+ if ret < 0:
+ raise Exception("Can't set buffer for request")
+
+ # XXX: ExposureTime cannot be set if AeEnable == True
+ skip_exp_time = "AeEnable" in self.control_values and self.control_values["AeEnable"] == True
+
+ for k, v in self.control_values.items():
+ if k == "ExposureTime" and skip_exp_time:
+ continue
+ request.set_control(k, v)
+
+ control_values = {}
+
+ camera.queueRequest(request)
+
+ self.reqs_queued += 1
+
+
+ def req_complete_cb(self, req):
+ camera = self.pycam
+
+ assert(len(req.buffers) == 1)
+
+ stream, fb = next(iter(req.buffers.items()))
+
+ self.reqs_completed += 1
+
+ if self.running and self.callback:
+ self.callback(stream, fb)
+
+ def set_control(self, control, value):
+ if not control in self.pycam.controls:
+ for k in self.pycam.controls:
+ if control.lower() == k.lower():
+ control = k
+
+ self.control_values[control] = value
new file mode 100755
@@ -0,0 +1,210 @@
+#!/usr/bin/python3
+
+import pycamera as pycam
+import time
+import binascii
+import argparse
+import selectors
+import os
+
+parser = argparse.ArgumentParser()
+parser.add_argument("-n", "--num-frames", type=int, default=10)
+parser.add_argument("-c", "--print-crc", action="store_true")
+parser.add_argument("-s", "--save-frames", action="store_true")
+parser.add_argument("-m", "--max-cameras", type=int, default=1)
+args = parser.parse_args()
+
+cm = pycam.CameraManager()
+
+cameras = cm.cameras
+
+if len(cameras) == 0:
+ print("No cameras")
+ exit(0)
+
+print("Cameras:")
+for c in cameras:
+ print(" {}".format(c.id))
+ print(" Properties:", c.properties)
+ print(" Controls:", c.controls)
+
+contexts = []
+
+for i in range(len(cameras)):
+ contexts.append({ "camera": cameras[i], "id": i })
+ if args.max_cameras and args.max_cameras - 1 == i:
+ break
+
+for ctx in contexts:
+ ctx["camera"].acquire()
+
+def configure_camera(ctx):
+ camera = ctx["camera"]
+
+ # Configure
+
+ config = camera.generateConfiguration([pycam.StreamRole.Viewfinder])
+ stream_config = config.at(0)
+
+ #stream_config.size = (1920, 480)
+ #stream_config.fmt = "BGR888"
+
+ print("Cam {}: stream config {}".format(ctx["id"], stream_config.toString()))
+
+ camera.configure(config);
+
+ ctx["config"] = config
+
+def alloc_buffers(ctx):
+ camera = ctx["camera"]
+ stream = ctx["config"].at(0).stream
+
+ allocator = pycam.FrameBufferAllocator(camera);
+ ret = allocator.allocate(stream)
+ if ret < 0:
+ print("Can't allocate buffers")
+ exit(-1)
+
+ allocated = len(allocator.buffers(stream))
+ print("Cam {}: Allocated {} buffers for stream".format(ctx["id"], allocated))
+
+ ctx["allocator"] = allocator
+
+def create_requests(ctx):
+ camera = ctx["camera"]
+ stream = ctx["config"].at(0).stream
+ buffers = ctx["allocator"].buffers(stream)
+
+ requests = []
+
+ b = -1
+
+ for buffer in buffers:
+ request = camera.createRequest()
+ if request == None:
+ print("Can't create request")
+ exit(-1)
+
+ ret = request.addBuffer(stream, buffer)
+ if ret < 0:
+ print("Can't set buffer for request")
+ exit(-1)
+
+ #request.set_control("Brightness", b)
+ b += 0.25
+
+ requests.append(request)
+
+ ctx["requests"] = requests
+
+
+def req_complete_cb(ctx, req):
+ camera = ctx["camera"]
+
+ print("Cam {}: Req {} Complete: {}".format(ctx["id"], ctx["reqs_completed"], req.status))
+
+ bufs = req.buffers
+ for stream, fb in bufs.items():
+ meta = fb.metadata
+ print("Cam {}: Buf seq {}, bytes {}".format(ctx["id"], meta.sequence, meta.bytesused))
+
+ with fb.mmap(0) as b:
+ if args.print_crc:
+ crc = binascii.crc32(b)
+ print("Cam {}: CRC {:#x}".format(ctx["id"], crc))
+
+ if args.save_frames:
+ id = ctx["id"]
+ num = ctx["reqs_completed"]
+ filename = "frame-{}-{}.data".format(id, num)
+ with open(filename, "wb") as f:
+ f.write(b)
+ print("Cam {}: Saved {}".format(ctx["id"], filename))
+
+ ctx["reqs_completed"] += 1
+
+ if ctx["reqs_queued"] < args.num_frames:
+ request = camera.createRequest()
+ if request == None:
+ print("Can't create request")
+ exit(-1)
+
+ for stream, fb in bufs.items():
+ ret = request.addBuffer(stream, fb)
+ if ret < 0:
+ print("Can't set buffer for request")
+ exit(-1)
+
+ camera.queueRequest(request)
+ ctx["reqs_queued"] += 1
+
+
+def setup_callbacks(ctx):
+ camera = ctx["camera"]
+
+ ctx["reqs_queued"] = 0
+ ctx["reqs_completed"] = 0
+
+def queue_requests(ctx):
+ camera = ctx["camera"]
+ requests = ctx["requests"]
+
+ camera.start()
+
+ for request in requests:
+ camera.queueRequest(request)
+ ctx["reqs_queued"] += 1
+
+
+
+for ctx in contexts:
+ configure_camera(ctx)
+ alloc_buffers(ctx)
+ create_requests(ctx)
+ setup_callbacks(ctx)
+
+for ctx in contexts:
+ queue_requests(ctx)
+
+
+print("Processing...")
+
+# Need to release GIL here, so that callbacks can be called
+#while any(ctx["reqs_completed"] < args.num_frames for ctx in contexts):
+# pycam.sleep(0.1)
+
+running = True
+
+def readcam(fileobj, mask):
+ global running
+ data = os.read(fileobj, 8)
+
+ reqs = cm.get_ready_requests()
+
+ ctx = contexts[0]
+ for req in reqs:
+ ctx = next(ctx for ctx in contexts if ctx["camera"] == req.camera)
+ req_complete_cb(ctx, req)
+
+ running = any(ctx["reqs_completed"] < args.num_frames for ctx in contexts)
+
+
+sel = selectors.DefaultSelector()
+sel.register(cm.efd, selectors.EVENT_READ, readcam)
+
+print("Press enter to exit")
+
+while running:
+ events = sel.select()
+ for key, mask in events:
+ callback = key.data
+ callback(key.fileobj, mask)
+
+print("Exiting...")
+
+for ctx in contexts:
+ camera = ctx["camera"]
+ camera.stop()
+ camera.release()
+
+print("Done")
new file mode 100644
@@ -0,0 +1,17 @@
+{
+ <insert_a_suppression_name_here>
+ Memcheck:Leak
+ match-leak-kinds: definite
+ fun:_Znwm
+ fun:_ZN8pybind116moduleC1EPKcS2_
+ fun:PyInit_pycamera
+ fun:_PyImport_LoadDynamicModuleWithSpec
+ obj:/usr/bin/python3.8
+ obj:/usr/bin/python3.8
+ fun:PyVectorcall_Call
+ fun:_PyEval_EvalFrameDefault
+ fun:_PyEval_EvalCodeWithName
+ fun:_PyFunction_Vectorcall
+ fun:_PyEval_EvalFrameDefault
+ fun:_PyFunction_Vectorcall
+}
new file mode 100644
@@ -0,0 +1,10 @@
+[wrap-file]
+directory = pybind11-2.3.0
+
+source_url = https://github.com/pybind/pybind11/archive/v2.3.0.zip
+source_filename = pybind11-2.3.0.zip
+source_hash = 1f844c071d9d98f5bb08458f128baa0aa08a9e5939a6b42276adb1bacd8b483e
+
+patch_url = https://wrapdb.mesonbuild.com/v1/projects/pybind11/2.3.0/2/get_zip
+patch_filename = pybind11-2.3.0-2-wrap.zip
+patch_hash = f3bed4bfc8961b3b985ff1e63fc6e57c674f35b353f0d42adbc037f9416381fb
Main issues: - Memory management in general. Who owns the object, how to pass ownership, etc. - Specifically, Request is currently broken. We can't, afaik, pass ownership around. So currently Python never frees a Request, and if the Request is not given to Camera::queueRequest, it will leak. - Need public Camera destructor. It is not clear to me why C++ allows it to be private, but pybind11 doesn't. Signed-off-by: Tomi Valkeinen <tomi.valkeinen@iki.fi> --- .gitignore | 2 + meson_options.txt | 2 + src/meson.build | 1 + src/py/meson.build | 1 + src/py/pycamera/__init__.py | 11 + src/py/pycamera/meson.build | 38 +++ src/py/pycamera/pymain.cpp | 382 +++++++++++++++++++++++++++++ src/py/test/drmtest.py | 129 ++++++++++ src/py/test/icam.py | 154 ++++++++++++ src/py/test/run-valgrind.sh | 6 + src/py/test/run.sh | 3 + src/py/test/simplecamera.py | 198 +++++++++++++++ src/py/test/test.py | 210 ++++++++++++++++ src/py/test/valgrind-pycamera.supp | 17 ++ subprojects/pybind11.wrap | 10 + 15 files changed, 1164 insertions(+) create mode 100644 src/py/meson.build create mode 100644 src/py/pycamera/__init__.py create mode 100644 src/py/pycamera/meson.build create mode 100644 src/py/pycamera/pymain.cpp create mode 100755 src/py/test/drmtest.py create mode 100755 src/py/test/icam.py create mode 100755 src/py/test/run-valgrind.sh create mode 100755 src/py/test/run.sh create mode 100644 src/py/test/simplecamera.py create mode 100755 src/py/test/test.py create mode 100644 src/py/test/valgrind-pycamera.supp create mode 100644 subprojects/pybind11.wrap