Skip to content

Commit 8aa0227

Browse files
tombakbingham
authored andcommitted
Add Python bindings
Add libcamera Python bindings. pybind11 is used to generate the C++ <-> Python layer. We use pybind11 'smart_holder' version to avoid issues with private destructors and shared_ptr. There is also an alternative solution here: pybind/pybind11#2067 Only a subset of libcamera classes are exposed. Implementing and testing the wrapper classes is challenging, and as such only classes that I have needed have been added so far. Signed-off-by: Tomi Valkeinen <tomi.valkeinen@ideasonboard.com> Reviewed-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com> Signed-off-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com> Signed-off-by: Kieran Bingham <kieran.bingham@ideasonboard.com>
1 parent 5cb17e0 commit 8aa0227

11 files changed

Lines changed: 845 additions & 1 deletion

File tree

meson.build

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,7 @@ summary({
177177
'Tracing support': tracing_enabled,
178178
'Android support': android_enabled,
179179
'GStreamer support': gst_enabled,
180+
'Python bindings': pycamera_enabled,
180181
'V4L2 emulation support': v4l2_enabled,
181182
'cam application': cam_enabled,
182183
'qcam application': qcam_enabled,

meson_options.txt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,3 +58,8 @@ option('v4l2',
5858
type : 'boolean',
5959
value : false,
6060
description : 'Compile the V4L2 compatibility layer')
61+
62+
option('pycamera',
63+
type : 'feature',
64+
value : 'auto',
65+
description : 'Enable libcamera Python bindings (experimental)')

src/meson.build

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,4 +37,5 @@ subdir('cam')
3737
subdir('qcam')
3838

3939
subdir('gstreamer')
40+
subdir('py')
4041
subdir('v4l2')

src/py/libcamera/__init__.py

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
# SPDX-License-Identifier: LGPL-2.1-or-later
2+
# Copyright (C) 2022, Tomi Valkeinen <tomi.valkeinen@ideasonboard.com>
3+
4+
from ._libcamera import *
5+
6+
7+
class MappedFrameBuffer:
8+
def __init__(self, fb):
9+
self.__fb = fb
10+
11+
def __enter__(self):
12+
import os
13+
import mmap
14+
15+
fb = self.__fb
16+
17+
# Collect information about the buffers
18+
19+
bufinfos = {}
20+
21+
for i in range(fb.num_planes):
22+
fd = fb.fd(i)
23+
24+
if fd not in bufinfos:
25+
buflen = os.lseek(fd, 0, os.SEEK_END)
26+
bufinfos[fd] = {'maplen': 0, 'buflen': buflen}
27+
else:
28+
buflen = bufinfos[fd]['buflen']
29+
30+
if fb.offset(i) > buflen or fb.offset(i) + fb.length(i) > buflen:
31+
raise RuntimeError(f'plane is out of buffer: buffer length={buflen}, ' +
32+
f'plane offset={fb.offset(i)}, plane length={fb.length(i)}')
33+
34+
bufinfos[fd]['maplen'] = max(bufinfos[fd]['maplen'], fb.offset(i) + fb.length(i))
35+
36+
# mmap the buffers
37+
38+
maps = []
39+
40+
for fd, info in bufinfos.items():
41+
map = mmap.mmap(fd, info['maplen'], mmap.MAP_SHARED, mmap.PROT_READ | mmap.PROT_WRITE)
42+
info['map'] = map
43+
maps.append(map)
44+
45+
self.__maps = tuple(maps)
46+
47+
# Create memoryviews for the planes
48+
49+
planes = []
50+
51+
for i in range(fb.num_planes):
52+
fd = fb.fd(i)
53+
info = bufinfos[fd]
54+
55+
mv = memoryview(info['map'])
56+
57+
start = fb.offset(i)
58+
end = fb.offset(i) + fb.length(i)
59+
60+
mv = mv[start:end]
61+
62+
planes.append(mv)
63+
64+
self.__planes = tuple(planes)
65+
66+
return self
67+
68+
def __exit__(self, exc_type, exc_value, exc_traceback):
69+
for p in self.__planes:
70+
p.release()
71+
72+
for mm in self.__maps:
73+
mm.close()
74+
75+
@property
76+
def planes(self):
77+
return self.__planes
78+
79+
80+
def __FrameBuffer__mmap(self):
81+
return MappedFrameBuffer(self)
82+
83+
84+
FrameBuffer.mmap = __FrameBuffer__mmap

src/py/libcamera/meson.build

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
# SPDX-License-Identifier: CC0-1.0
2+
3+
py3_dep = dependency('python3', required : get_option('pycamera'))
4+
5+
if not py3_dep.found()
6+
pycamera_enabled = false
7+
subdir_done()
8+
endif
9+
10+
pycamera_enabled = true
11+
12+
pybind11_proj = subproject('pybind11')
13+
pybind11_dep = pybind11_proj.get_variable('pybind11_dep')
14+
15+
pycamera_sources = files([
16+
'pyenums.cpp',
17+
'pymain.cpp',
18+
])
19+
20+
pycamera_deps = [
21+
libcamera_public,
22+
py3_dep,
23+
pybind11_dep,
24+
]
25+
26+
pycamera_args = [
27+
'-fvisibility=hidden',
28+
'-Wno-shadow',
29+
'-DPYBIND11_USE_SMART_HOLDER_AS_DEFAULT',
30+
'-DLIBCAMERA_BASE_PRIVATE',
31+
]
32+
33+
destdir = get_option('libdir') / ('python' + py3_dep.version()) / 'site-packages' / 'libcamera'
34+
35+
pycamera = shared_module('_libcamera',
36+
pycamera_sources,
37+
install : true,
38+
install_dir : destdir,
39+
name_prefix : '',
40+
dependencies : pycamera_deps,
41+
cpp_args : pycamera_args)
42+
43+
run_command('ln', '-fsT', '../../../../src/py/libcamera/__init__.py',
44+
meson.current_build_dir() / '__init__.py',
45+
check: true)
46+
47+
install_data(['__init__.py'], install_dir : destdir)
48+
49+
# \todo Generate stubs when building. See https://peps.python.org/pep-0484/#stub-files
50+
# Note: Depends on pybind11-stubgen. To generate pylibcamera stubs:
51+
# $ PYTHONPATH=build/src/py pybind11-stubgen --no-setup-py -o build/src/py libcamera
52+
# $ mv build/src/py/libcamera-stubs/* build/src/py/libcamera/

src/py/libcamera/pyenums.cpp

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/* SPDX-License-Identifier: LGPL-2.1-or-later */
2+
/*
3+
* Copyright (C) 2022, Tomi Valkeinen <tomi.valkeinen@ideasonboard.com>
4+
*
5+
* Python bindings - Enumerations
6+
*/
7+
8+
#include <libcamera/libcamera.h>
9+
10+
#include <pybind11/smart_holder.h>
11+
12+
namespace py = pybind11;
13+
14+
using namespace libcamera;
15+
16+
void init_pyenums(py::module &m)
17+
{
18+
py::enum_<StreamRole>(m, "StreamRole")
19+
.value("StillCapture", StreamRole::StillCapture)
20+
.value("Raw", StreamRole::Raw)
21+
.value("VideoRecording", StreamRole::VideoRecording)
22+
.value("Viewfinder", StreamRole::Viewfinder);
23+
24+
py::enum_<ControlType>(m, "ControlType")
25+
.value("None", ControlType::ControlTypeNone)
26+
.value("Bool", ControlType::ControlTypeBool)
27+
.value("Byte", ControlType::ControlTypeByte)
28+
.value("Integer32", ControlType::ControlTypeInteger32)
29+
.value("Integer64", ControlType::ControlTypeInteger64)
30+
.value("Float", ControlType::ControlTypeFloat)
31+
.value("String", ControlType::ControlTypeString)
32+
.value("Rectangle", ControlType::ControlTypeRectangle)
33+
.value("Size", ControlType::ControlTypeSize);
34+
}

0 commit comments

Comments
 (0)