From patchwork Thu May 5 10:40:52 2022 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Tomi Valkeinen X-Patchwork-Id: 15784 Return-Path: X-Original-To: parsemail@patchwork.libcamera.org Delivered-To: parsemail@patchwork.libcamera.org Received: from lancelot.ideasonboard.com (lancelot.ideasonboard.com [92.243.16.209]) by patchwork.libcamera.org (Postfix) with ESMTPS id CE957C0F2A for ; Thu, 5 May 2022 10:41:30 +0000 (UTC) Received: from lancelot.ideasonboard.com (localhost [IPv6:::1]) by lancelot.ideasonboard.com (Postfix) with ESMTP id BA0F36564F; Thu, 5 May 2022 12:41:28 +0200 (CEST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=libcamera.org; s=mail; t=1651747288; bh=bB6rZ6S6R9ASu5qcUfCRjPXOiG5EUvfsuAzBK+VC0m4=; h=To:Date:In-Reply-To:References:Subject:List-Id:List-Unsubscribe: List-Archive:List-Post:List-Help:List-Subscribe:From:Reply-To: From; b=mcyLIZv5jvtv+SwmTkEehRUSK46cKoEF2ut3q7gtAtDv4etdf0IGwzxoqpVkLhm1d 8H1ENjU3bK17rvpAjckBktpHCXP4+lqosE/xtHFBKRKmmyiGAsOm39vIkz4pCaK/7f ooQDFBBWfV4vu+sx6NJFjoN+h2JYIEDP4hV1qzeuZFuXXxPI3/ZI+6sfb2h8IDxQjs sbhAIBm2qPvhUvqVOwkZTnemEG1ZouvdckP0xPHZyx8VLMFPrnPOSFOdaj+a8vMymn lPW2g3klIHAzRjqWBAAq1MwqD/4x3XfSQHkEuQ/Q01cRLJENC4wtrMmPDgz19iISUl 9UxS4eu2/BRaA== Received: from perceval.ideasonboard.com (perceval.ideasonboard.com [213.167.242.64]) by lancelot.ideasonboard.com (Postfix) with ESMTPS id 1F117603AB for ; Thu, 5 May 2022 12:41:26 +0200 (CEST) Authentication-Results: lancelot.ideasonboard.com; dkim=pass (1024-bit key; unprotected) header.d=ideasonboard.com header.i=@ideasonboard.com header.b="egsVuFWG"; dkim-atps=neutral Received: from deskari.lan (91-156-85-209.elisa-laajakaista.fi [91.156.85.209]) by perceval.ideasonboard.com (Postfix) with ESMTPSA id 3DE444A8; Thu, 5 May 2022 12:41:25 +0200 (CEST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=ideasonboard.com; s=mail; t=1651747285; bh=bB6rZ6S6R9ASu5qcUfCRjPXOiG5EUvfsuAzBK+VC0m4=; h=From:To:Cc:Subject:Date:In-Reply-To:References:From; b=egsVuFWGY2lta7zukLRlYpYvO2QD6sgjz+UXZdGGQazcrbhsyH1zYtfB0YHOKsX9Z AiG4uXrUa1Gi5ANK+TcJB9zakQ/dBS72Z3hDN4PK/fTD2fefXUj319KL/tyCR7TbPo hE5Cksd81PfFLBn+iX/dQvLtgKRmdfJscZgCsNh0= To: libcamera-devel@lists.libcamera.org, David Plowman , Kieran Bingham , Laurent Pinchart , Jacopo Mondi Date: Thu, 5 May 2022 13:40:52 +0300 Message-Id: <20220505104104.70841-2-tomi.valkeinen@ideasonboard.com> X-Mailer: git-send-email 2.34.1 In-Reply-To: <20220505104104.70841-1-tomi.valkeinen@ideasonboard.com> References: <20220505104104.70841-1-tomi.valkeinen@ideasonboard.com> MIME-Version: 1.0 Subject: [libcamera-devel] [PATCH v7 01/13] meson: require meson 0.56+ X-BeenThere: libcamera-devel@lists.libcamera.org X-Mailman-Version: 2.1.29 Precedence: list List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , X-Patchwork-Original-From: Tomi Valkeinen via libcamera-devel From: Tomi Valkeinen Reply-To: Tomi Valkeinen Errors-To: libcamera-devel-bounces@lists.libcamera.org Sender: "libcamera-devel" Ubuntu 22.04 LTS has been released with meson 0.61.2, and it is easy to install a recent version of meson with python-pip, so let's update the required meson version to get rid of the Android compilation limitation. Additionally, going to meson 0.55 gives the ability to have patch files for git-wraps which is useful for Python bindings. 0.56 brings meson.project_source_root() and meson.project_build_root(), allowing us to get rid of the deprecated meson.source_root() and meson.build_root(). So, let's update the required meson version to 0.56. Signed-off-by: Tomi Valkeinen Reviewed-by: Kieran Bingham Reviewed-by: Laurent Pinchart --- README.rst | 4 +--- meson.build | 6 +----- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/README.rst b/README.rst index aae6b79f..ae5ede17 100644 --- a/README.rst +++ b/README.rst @@ -47,9 +47,7 @@ A C++ toolchain: [required] Either {g++, clang} Meson Build system: [required] - meson (>= 0.53) ninja-build pkg-config - - meson (>= 0.55) is required for building Android (-Dandroid=enabled) + meson (>= 0.56) ninja-build pkg-config If your distribution doesn't provide a recent enough version of meson, you can install or upgrade it using pip3. diff --git a/meson.build b/meson.build index 29d8542d..b892ba84 100644 --- a/meson.build +++ b/meson.build @@ -1,11 +1,7 @@ # SPDX-License-Identifier: CC0-1.0 project('libcamera', 'c', 'cpp', - # Use of the Android component requires meson 0.55, but Ubuntu 20.04 LTS - # ships meson 0.53. Improve the Ubuntu experience at the expense of - # Android as the former is a much more common use case than the latter at - # this point. This should be fixed after Ubuntu releases 22.04 LTS. - meson_version : '>= 0.53', + meson_version : '>= 0.56', version : '0.0.0', default_options : [ 'werror=true', From patchwork Thu May 5 10:40:53 2022 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Tomi Valkeinen X-Patchwork-Id: 15785 Return-Path: X-Original-To: parsemail@patchwork.libcamera.org Delivered-To: parsemail@patchwork.libcamera.org Received: from lancelot.ideasonboard.com (lancelot.ideasonboard.com [92.243.16.209]) by patchwork.libcamera.org (Postfix) with ESMTPS id 4AACEC0F2A for ; Thu, 5 May 2022 10:41:32 +0000 (UTC) Received: from lancelot.ideasonboard.com (localhost [IPv6:::1]) by lancelot.ideasonboard.com (Postfix) with ESMTP id 2F82E65655; Thu, 5 May 2022 12:41:29 +0200 (CEST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=libcamera.org; s=mail; t=1651747289; bh=DArP7Vx06OZE0ST1EUrPmhhvKGvmluGFhh49JBJ7RO4=; h=To:Date:In-Reply-To:References:Subject:List-Id:List-Unsubscribe: List-Archive:List-Post:List-Help:List-Subscribe:From:Reply-To: From; b=PZdEwL7VeBOafFWwy3RsF1Y42EMVOPp3qJEUwV6gsb6FdX8oGQWtlQbD/f2v3GZGU ANP6INBiorrvF9wjKRMIxC595X94Pfwp6XCa8E7EMjBhkeYaKzhCM9VFtljgnUGCEA Nva/ta+rtIMKCGyEt9xMIypgIlkFYECk5RfEvGw6CQLHWDuCsAaFdTFHIRhkLn+cs9 PbJbou3WbJ+oILkHpfD55GMMgfRHXxtUGRyftX7He8eYV9nH+8D64+KMbtxWokPkUz OZSAxbi34MTmd+1PHAvbFnf7rIFPw6WUDtQanUE4FWs9LfqT0Mst6VNvlBRoohExWQ EsemUNLffO16w== Received: from perceval.ideasonboard.com (perceval.ideasonboard.com [213.167.242.64]) by lancelot.ideasonboard.com (Postfix) with ESMTPS id 4F9006563F for ; Thu, 5 May 2022 12:41:26 +0200 (CEST) Authentication-Results: lancelot.ideasonboard.com; dkim=pass (1024-bit key; unprotected) header.d=ideasonboard.com header.i=@ideasonboard.com header.b="Sj7O317B"; dkim-atps=neutral Received: from deskari.lan (91-156-85-209.elisa-laajakaista.fi [91.156.85.209]) by perceval.ideasonboard.com (Postfix) with ESMTPSA id BA587A43; Thu, 5 May 2022 12:41:25 +0200 (CEST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=ideasonboard.com; s=mail; t=1651747286; bh=DArP7Vx06OZE0ST1EUrPmhhvKGvmluGFhh49JBJ7RO4=; h=From:To:Cc:Subject:Date:In-Reply-To:References:From; b=Sj7O317BUg1/oUfjK1u3jg0lKqwRZYdsY+4K+HjwqCUK5hr6qwvJlPFlYbDaNPpm8 7jwPpxBr+ot7DYzP3M25a7E8q+0nyKv1nNHyFurZmPtsEpO7BXZ3AusHolIDv29xFn CHEVi8sV8Tym1D6ak6fJYCo3QUEbJubDvreljjCc= To: libcamera-devel@lists.libcamera.org, David Plowman , Kieran Bingham , Laurent Pinchart , Jacopo Mondi Date: Thu, 5 May 2022 13:40:53 +0300 Message-Id: <20220505104104.70841-3-tomi.valkeinen@ideasonboard.com> X-Mailer: git-send-email 2.34.1 In-Reply-To: <20220505104104.70841-1-tomi.valkeinen@ideasonboard.com> References: <20220505104104.70841-1-tomi.valkeinen@ideasonboard.com> MIME-Version: 1.0 Subject: [libcamera-devel] [PATCH v7 02/13] meson: use new project_*_root() functions X-BeenThere: libcamera-devel@lists.libcamera.org X-Mailman-Version: 2.1.29 Precedence: list List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , X-Patchwork-Original-From: Tomi Valkeinen via libcamera-devel From: Tomi Valkeinen Reply-To: Tomi Valkeinen Errors-To: libcamera-devel-bounces@lists.libcamera.org Sender: "libcamera-devel" meson.source_root() and meson.build_root() are deprecated. Use meson.project_source_root() and meson.project_build_root() instead. Signed-off-by: Tomi Valkeinen Reviewed-by: Kieran Bingham Reviewed-by: Laurent Pinchart --- Documentation/meson.build | 4 ++-- include/libcamera/ipa/meson.build | 8 ++++---- meson.build | 6 +++--- src/libcamera/meson.build | 4 ++-- .../include/libcamera/ipa/meson.build | 4 ++-- 5 files changed, 13 insertions(+), 13 deletions(-) diff --git a/Documentation/meson.build b/Documentation/meson.build index 33af82aa..8e2eacc6 100644 --- a/Documentation/meson.build +++ b/Documentation/meson.build @@ -12,8 +12,8 @@ dot = find_program('dot', required : get_option('documentation')) if doxygen.found() and dot.found() cdata = configuration_data() cdata.set('VERSION', 'v@0@'.format(libcamera_git_version)) - cdata.set('TOP_SRCDIR', meson.source_root()) - cdata.set('TOP_BUILDDIR', meson.build_root()) + cdata.set('TOP_SRCDIR', meson.project_source_root()) + cdata.set('TOP_BUILDDIR', meson.project_build_root()) doxyfile = configure_file(input : 'Doxyfile.in', output : 'Doxyfile', diff --git a/include/libcamera/ipa/meson.build b/include/libcamera/ipa/meson.build index 6ea94fb5..442ca3dd 100644 --- a/include/libcamera/ipa/meson.build +++ b/include/libcamera/ipa/meson.build @@ -25,8 +25,8 @@ ipa_mojom_core = custom_target(core_mojom_file.split('.')[0] + '_mojom_module', output : core_mojom_file + '-module', command : [ mojom_parser, - '--output-root', meson.build_root(), - '--input-root', meson.source_root(), + '--output-root', meson.project_build_root(), + '--input-root', meson.project_source_root(), '--mojoms', '@INPUT@' ]) @@ -89,8 +89,8 @@ foreach file : ipa_mojom_files depends : ipa_mojom_core, command : [ mojom_parser, - '--output-root', meson.build_root(), - '--input-root', meson.source_root(), + '--output-root', meson.project_build_root(), + '--input-root', meson.project_source_root(), '--mojoms', '@INPUT@' ]) diff --git a/meson.build b/meson.build index b892ba84..10ad8c5c 100644 --- a/meson.build +++ b/meson.build @@ -17,8 +17,8 @@ project('libcamera', 'c', 'cpp', # git version tag, the build metadata (e.g. +211-c94a24f4) is omitted from # libcamera_git_version. libcamera_git_version = run_command('utils/gen-version.sh', - meson.build_root(), - meson.source_root()).stdout().strip() + meson.project_build_root(), + meson.project_source_root()).stdout().strip() if libcamera_git_version == '' libcamera_git_version = meson.project_version() endif @@ -160,7 +160,7 @@ endif # Create a symlink from the build root to the source root. This is used when # running libcamera from the build directory to locate resources in the source # directory (such as IPA configuration files). -run_command('ln', '-fsT', meson.source_root(), meson.build_root() / 'source') +run_command('ln', '-fsT', meson.project_source_root(), meson.project_build_root() / 'source') configure_file(output : 'config.h', configuration : config_h) diff --git a/src/libcamera/meson.build b/src/libcamera/meson.build index 26912ca1..d6a78760 100644 --- a/src/libcamera/meson.build +++ b/src/libcamera/meson.build @@ -98,11 +98,11 @@ endforeach libcamera_sources += control_sources -gen_version = meson.source_root() / 'utils' / 'gen-version.sh' +gen_version = meson.project_source_root() / 'utils' / 'gen-version.sh' # Use vcs_tag() and not configure_file() or run_command(), to ensure that the # version gets updated with every ninja build and not just at meson setup time. -version_cpp = vcs_tag(command : [gen_version, meson.build_root(), meson.source_root()], +version_cpp = vcs_tag(command : [gen_version, meson.project_build_root(), meson.project_source_root()], input : 'version.cpp.in', output : 'version.cpp', fallback : meson.project_version()) diff --git a/test/serialization/generated_serializer/include/libcamera/ipa/meson.build b/test/serialization/generated_serializer/include/libcamera/ipa/meson.build index ba198f7a..6f8794c1 100644 --- a/test/serialization/generated_serializer/include/libcamera/ipa/meson.build +++ b/test/serialization/generated_serializer/include/libcamera/ipa/meson.build @@ -6,8 +6,8 @@ mojom = custom_target('test_mojom_module', output : 'test.mojom-module', command : [ mojom_parser, - '--output-root', meson.build_root(), - '--input-root', meson.source_root(), + '--output-root', meson.project_build_root(), + '--input-root', meson.project_source_root(), '--mojoms', '@INPUT@' ]) From patchwork Thu May 5 10:40:54 2022 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Tomi Valkeinen X-Patchwork-Id: 15786 Return-Path: X-Original-To: parsemail@patchwork.libcamera.org Delivered-To: parsemail@patchwork.libcamera.org Received: from lancelot.ideasonboard.com (lancelot.ideasonboard.com [92.243.16.209]) by patchwork.libcamera.org (Postfix) with ESMTPS id 34335C326C for ; Thu, 5 May 2022 10:41:33 +0000 (UTC) Received: from lancelot.ideasonboard.com (localhost [IPv6:::1]) by lancelot.ideasonboard.com (Postfix) with ESMTP id BCF6F65649; Thu, 5 May 2022 12:41:29 +0200 (CEST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=libcamera.org; s=mail; t=1651747289; bh=XdDdQDQXDl9lQ0P90pPKtd1P2JY7LUYPJegdEbDP2cI=; h=To:Date:In-Reply-To:References:Subject:List-Id:List-Unsubscribe: List-Archive:List-Post:List-Help:List-Subscribe:From:Reply-To: From; b=eV7cKraRrqZ1Al8/xR47yRh/s0FpkqushYQuGMFCfNXvNfCUVFRUOTB2cOvn7MLWz wqT9A/yvPmf4lOBdFU3N0/4mIm5ml1DOERfZLQW6L3hnj2rLKBxDFUEpZcNXs7OXty 8mdVcfKEOvFvMUlNsiH7X8fEDCexRBio0DTVH8Iy9cOf3drRtMG16vb4jYX92vLKng VmXKrig6uz1ilqT12WVicf7hl53M6kr/onAENWoZxyGD6gLhbX+2c3ewmTFwCZrrga iQSxC64TW/glIGwo3N4whCwh0Tk2su4ypDelp+3v3+hOq6FYjWJi5Z2+Rn93gQgmm9 xnc4t5q2hvPdg== Received: from perceval.ideasonboard.com (perceval.ideasonboard.com [213.167.242.64]) by lancelot.ideasonboard.com (Postfix) with ESMTPS id CC2F36563F for ; Thu, 5 May 2022 12:41:26 +0200 (CEST) Authentication-Results: lancelot.ideasonboard.com; dkim=pass (1024-bit key; unprotected) header.d=ideasonboard.com header.i=@ideasonboard.com header.b="QpUwNYRi"; dkim-atps=neutral Received: from deskari.lan (91-156-85-209.elisa-laajakaista.fi [91.156.85.209]) by perceval.ideasonboard.com (Postfix) with ESMTPSA id 4121AA46; Thu, 5 May 2022 12:41:26 +0200 (CEST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=ideasonboard.com; s=mail; t=1651747286; bh=XdDdQDQXDl9lQ0P90pPKtd1P2JY7LUYPJegdEbDP2cI=; h=From:To:Cc:Subject:Date:In-Reply-To:References:From; b=QpUwNYRigGBjDqafVbzncJ42YS0kFXKDqEho9RO7rV80qBsaN7gESvlMIp6cWPLQ/ 0OQks8KfAuWWMXNCNbq16jIWDa/PbVytygJToZX4E4A5c3shMWzTbZqILw/9n5ucMd 5/PYLJZjcA0+EWc0omlxx0DAXzvaDg4s5kY6Q498= To: libcamera-devel@lists.libcamera.org, David Plowman , Kieran Bingham , Laurent Pinchart , Jacopo Mondi Date: Thu, 5 May 2022 13:40:54 +0300 Message-Id: <20220505104104.70841-4-tomi.valkeinen@ideasonboard.com> X-Mailer: git-send-email 2.34.1 In-Reply-To: <20220505104104.70841-1-tomi.valkeinen@ideasonboard.com> References: <20220505104104.70841-1-tomi.valkeinen@ideasonboard.com> MIME-Version: 1.0 Subject: [libcamera-devel] [PATCH v7 03/13] meson: add 'check: true' for run_command() calls X-BeenThere: libcamera-devel@lists.libcamera.org X-Mailman-Version: 2.1.29 Precedence: list List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , X-Patchwork-Original-From: Tomi Valkeinen via libcamera-devel From: Tomi Valkeinen Reply-To: Tomi Valkeinen Errors-To: libcamera-devel-bounces@lists.libcamera.org Sender: "libcamera-devel" Add 'check: true' to all run_command() calls as suggested in https://github.com/mesonbuild/meson/issues/9300 to get rid of meson warning "You should add the boolean check kwarg to the run_command call." This makes meson fail if the executed command fails, which makes sense. Signed-off-by: Tomi Valkeinen Reviewed-by: Kieran Bingham Reviewed-by: Laurent Pinchart --- meson.build | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/meson.build b/meson.build index 10ad8c5c..0124e7d3 100644 --- a/meson.build +++ b/meson.build @@ -18,7 +18,8 @@ project('libcamera', 'c', 'cpp', # libcamera_git_version. libcamera_git_version = run_command('utils/gen-version.sh', meson.project_build_root(), - meson.project_source_root()).stdout().strip() + meson.project_source_root(), + check: true).stdout().strip() if libcamera_git_version == '' libcamera_git_version = meson.project_version() endif @@ -148,7 +149,7 @@ subdir('test') if not meson.is_cross_build() kernel_version_req = '>= 5.0.0' - kernel_version = run_command('uname', '-r').stdout().strip() + kernel_version = run_command('uname', '-r', check: true).stdout().strip() if not kernel_version.version_compare(kernel_version_req) warning('The current running kernel version @0@ is too old to run libcamera.' .format(kernel_version)) @@ -160,7 +161,8 @@ endif # Create a symlink from the build root to the source root. This is used when # running libcamera from the build directory to locate resources in the source # directory (such as IPA configuration files). -run_command('ln', '-fsT', meson.project_source_root(), meson.project_build_root() / 'source') +run_command('ln', '-fsT', meson.project_source_root(), meson.project_build_root() / 'source', + check: true) configure_file(output : 'config.h', configuration : config_h) From patchwork Thu May 5 10:40:55 2022 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Tomi Valkeinen X-Patchwork-Id: 15787 Return-Path: X-Original-To: parsemail@patchwork.libcamera.org Delivered-To: parsemail@patchwork.libcamera.org Received: from lancelot.ideasonboard.com (lancelot.ideasonboard.com [92.243.16.209]) by patchwork.libcamera.org (Postfix) with ESMTPS id 109D9C0F2A for ; Thu, 5 May 2022 10:41:34 +0000 (UTC) Received: from lancelot.ideasonboard.com (localhost [IPv6:::1]) by lancelot.ideasonboard.com (Postfix) with ESMTP id 81ED96565D; Thu, 5 May 2022 12:41:30 +0200 (CEST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=libcamera.org; s=mail; t=1651747290; bh=yEHy2G907recooxGYC6CB2JBjvpEGkYmPe1aZLsmaWo=; h=To:Date:In-Reply-To:References:Subject:List-Id:List-Unsubscribe: List-Archive:List-Post:List-Help:List-Subscribe:From:Reply-To: From; b=D9qOnj9VgjPmmX5Y4Db4E4hBmISfDv7uf+ysvb9DdNzvhe+o2PSw78ImsznZ20X33 +cbE8v5L1rWc52b/tKnnOlwSEp/ZkiAwyFbSVF0OSEUscnBbmyNsVFRFOpRr8DF723 7HMmQ7VYcI1r9y0uH8Gnm4dYVF5hVTgJ2CgfkqHW2D2jxdK8xe7+8dm9V3DcF/8GnH +sKd0HSTfh/HCBVrw+fvbK4XhDg+2MdZM1fgWR/Si5YiKs39C3fNNG2hPVE4DhnhZG rXdBYmAOWyzQgL9mXbQ5Fytsywnt+g1X2+/NljiLrDM08/4kg8qg+isNcbrAssbVbU QOlBbpM5Bp9vQ== Received: from perceval.ideasonboard.com (perceval.ideasonboard.com [IPv6:2001:4b98:dc2:55:216:3eff:fef7:d647]) by lancelot.ideasonboard.com (Postfix) with ESMTPS id 5BFA360424 for ; Thu, 5 May 2022 12:41:27 +0200 (CEST) Authentication-Results: lancelot.ideasonboard.com; dkim=pass (1024-bit key; unprotected) header.d=ideasonboard.com header.i=@ideasonboard.com header.b="Es33la3c"; dkim-atps=neutral Received: from deskari.lan (91-156-85-209.elisa-laajakaista.fi [91.156.85.209]) by perceval.ideasonboard.com (Postfix) with ESMTPSA id C1131A50; Thu, 5 May 2022 12:41:26 +0200 (CEST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=ideasonboard.com; s=mail; t=1651747287; bh=yEHy2G907recooxGYC6CB2JBjvpEGkYmPe1aZLsmaWo=; h=From:To:Cc:Subject:Date:In-Reply-To:References:From; b=Es33la3cWIKobgDMmighOc3nWp+yUdoSv4gHxvLM7JLznMNt8IAnalL1UfzEuEHcX BfJ8yioVpriRJTiqiI8vHtW/TjZPqjBQN5kvmiN0H6QUSArs4vgxcDvc5ZoG0LE6IY DYVKXip9n6qDIFGmJbP3G4DfLGGI6zMRipDTaY04= To: libcamera-devel@lists.libcamera.org, David Plowman , Kieran Bingham , Laurent Pinchart , Jacopo Mondi Date: Thu, 5 May 2022 13:40:55 +0300 Message-Id: <20220505104104.70841-5-tomi.valkeinen@ideasonboard.com> X-Mailer: git-send-email 2.34.1 In-Reply-To: <20220505104104.70841-1-tomi.valkeinen@ideasonboard.com> References: <20220505104104.70841-1-tomi.valkeinen@ideasonboard.com> MIME-Version: 1.0 Subject: [libcamera-devel] [PATCH v7 04/13] Add Python bindings X-BeenThere: libcamera-devel@lists.libcamera.org X-Mailman-Version: 2.1.29 Precedence: list List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , X-Patchwork-Original-From: Tomi Valkeinen via libcamera-devel From: Tomi Valkeinen Reply-To: Tomi Valkeinen Errors-To: libcamera-devel-bounces@lists.libcamera.org Sender: "libcamera-devel" 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: https://github.com/pybind/pybind11/pull/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 Reviewed-by: Kieran Bingham Reviewed-by: Laurent Pinchart --- meson.build | 1 + meson_options.txt | 5 + src/meson.build | 1 + src/py/libcamera/__init__.py | 12 + src/py/libcamera/meson.build | 44 ++ src/py/libcamera/pyenums.cpp | 53 ++ src/py/libcamera/pymain.cpp | 452 ++++++++++++++++++ src/py/meson.build | 1 + subprojects/.gitignore | 3 +- subprojects/packagefiles/pybind11/meson.build | 8 + subprojects/pybind11.wrap | 8 + 11 files changed, 587 insertions(+), 1 deletion(-) create mode 100644 src/py/libcamera/__init__.py create mode 100644 src/py/libcamera/meson.build create mode 100644 src/py/libcamera/pyenums.cpp create mode 100644 src/py/libcamera/pymain.cpp create mode 100644 src/py/meson.build create mode 100644 subprojects/packagefiles/pybind11/meson.build create mode 100644 subprojects/pybind11.wrap diff --git a/meson.build b/meson.build index 0124e7d3..60a911e0 100644 --- a/meson.build +++ b/meson.build @@ -177,6 +177,7 @@ summary({ 'Tracing support': tracing_enabled, 'Android support': android_enabled, 'GStreamer support': gst_enabled, + 'Python bindings': pycamera_enabled, 'V4L2 emulation support': v4l2_enabled, 'cam application': cam_enabled, 'qcam application': qcam_enabled, diff --git a/meson_options.txt b/meson_options.txt index 2c80ad8b..ca00c78e 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -58,3 +58,8 @@ option('v4l2', type : 'boolean', value : false, description : 'Compile the V4L2 compatibility layer') + +option('pycamera', + type : 'feature', + value : 'auto', + description : 'Enable libcamera Python bindings (experimental)') diff --git a/src/meson.build b/src/meson.build index e0ea9c35..34663a6f 100644 --- a/src/meson.build +++ b/src/meson.build @@ -37,4 +37,5 @@ subdir('cam') subdir('qcam') subdir('gstreamer') +subdir('py') subdir('v4l2') diff --git a/src/py/libcamera/__init__.py b/src/py/libcamera/__init__.py new file mode 100644 index 00000000..cd7512a2 --- /dev/null +++ b/src/py/libcamera/__init__.py @@ -0,0 +1,12 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +# Copyright (C) 2021, Tomi Valkeinen + +from ._libcamera import * +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 diff --git a/src/py/libcamera/meson.build b/src/py/libcamera/meson.build new file mode 100644 index 00000000..e1f5cf21 --- /dev/null +++ b/src/py/libcamera/meson.build @@ -0,0 +1,44 @@ +# SPDX-License-Identifier: CC0-1.0 + +py3_dep = dependency('python3', required : get_option('pycamera')) + +if not py3_dep.found() + pycamera_enabled = false + subdir_done() +endif + +pycamera_enabled = true + +pybind11_proj = subproject('pybind11') +pybind11_dep = pybind11_proj.get_variable('pybind11_dep') + +pycamera_sources = files([ + 'pymain.cpp', + 'pyenums.cpp', +]) + +pycamera_deps = [ + libcamera_public, + py3_dep, + pybind11_dep, +] + +pycamera_args = ['-fvisibility=hidden'] +pycamera_args += ['-Wno-shadow'] +pycamera_args += ['-DPYBIND11_USE_SMART_HOLDER_AS_DEFAULT'] + +destdir = get_option('libdir') + '/python' + py3_dep.version() + '/site-packages/libcamera' + +pycamera = shared_module('_libcamera', + pycamera_sources, + install : true, + install_dir : destdir, + name_prefix : '', + dependencies : pycamera_deps, + cpp_args : pycamera_args) + +run_command('ln', '-fsT', '../../../../src/py/libcamera/__init__.py', + meson.current_build_dir() / '__init__.py', + check: true) + +install_data(['__init__.py'], install_dir : destdir) diff --git a/src/py/libcamera/pyenums.cpp b/src/py/libcamera/pyenums.cpp new file mode 100644 index 00000000..39886656 --- /dev/null +++ b/src/py/libcamera/pyenums.cpp @@ -0,0 +1,53 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +/* + * Copyright (C) 2021, Tomi Valkeinen + * + * Python bindings + */ + +#include + +#include + +namespace py = pybind11; + +using namespace libcamera; + +void init_pyenums(py::module &m) +{ + py::enum_(m, "ConfigurationStatus") + .value("Valid", CameraConfiguration::Valid) + .value("Adjusted", CameraConfiguration::Adjusted) + .value("Invalid", CameraConfiguration::Invalid); + + py::enum_(m, "StreamRole") + .value("StillCapture", StreamRole::StillCapture) + .value("Raw", StreamRole::Raw) + .value("VideoRecording", StreamRole::VideoRecording) + .value("Viewfinder", StreamRole::Viewfinder); + + py::enum_(m, "RequestStatus") + .value("Pending", Request::RequestPending) + .value("Complete", Request::RequestComplete) + .value("Cancelled", Request::RequestCancelled); + + py::enum_(m, "FrameMetadataStatus") + .value("Success", FrameMetadata::FrameSuccess) + .value("Error", FrameMetadata::FrameError) + .value("Cancelled", FrameMetadata::FrameCancelled); + + py::enum_(m, "ReuseFlag") + .value("Default", Request::ReuseFlag::Default) + .value("ReuseBuffers", Request::ReuseFlag::ReuseBuffers); + + py::enum_(m, "ControlType") + .value("None", ControlType::ControlTypeNone) + .value("Bool", ControlType::ControlTypeBool) + .value("Byte", ControlType::ControlTypeByte) + .value("Integer32", ControlType::ControlTypeInteger32) + .value("Integer64", ControlType::ControlTypeInteger64) + .value("Float", ControlType::ControlTypeFloat) + .value("String", ControlType::ControlTypeString) + .value("Rectangle", ControlType::ControlTypeRectangle) + .value("Size", ControlType::ControlTypeSize); +} diff --git a/src/py/libcamera/pymain.cpp b/src/py/libcamera/pymain.cpp new file mode 100644 index 00000000..54674caf --- /dev/null +++ b/src/py/libcamera/pymain.cpp @@ -0,0 +1,452 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +/* + * Copyright (C) 2021, Tomi Valkeinen + * + * Python bindings + */ + +/* + * To generate pylibcamera stubs: + * PYTHONPATH=build/src/py pybind11-stubgen --no-setup-py -o build/src/py libcamera + */ + +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include + +namespace py = pybind11; + +using namespace std; +using namespace libcamera; + +template +static py::object ValueOrTuple(const ControlValue &cv) +{ + if (cv.isArray()) { + const T *v = reinterpret_cast(cv.data().data()); + auto t = py::tuple(cv.numElements()); + + for (size_t i = 0; i < cv.numElements(); ++i) + t[i] = v[i]; + + return t; + } + + return py::cast(cv.get()); +} + +static py::object ControlValueToPy(const ControlValue &cv) +{ + switch (cv.type()) { + case ControlTypeBool: + return ValueOrTuple(cv); + case ControlTypeByte: + return ValueOrTuple(cv); + case ControlTypeInteger32: + return ValueOrTuple(cv); + case ControlTypeInteger64: + return ValueOrTuple(cv); + case ControlTypeFloat: + return ValueOrTuple(cv); + case ControlTypeString: + return py::cast(cv.get()); + case ControlTypeRectangle: { + const Rectangle *v = reinterpret_cast(cv.data().data()); + return py::make_tuple(v->x, v->y, v->width, v->height); + } + case ControlTypeSize: { + const Size *v = reinterpret_cast(cv.data().data()); + return py::make_tuple(v->width, v->height); + } + case ControlTypeNone: + default: + throw runtime_error("Unsupported ControlValue type"); + } +} + +static ControlValue PyToControlValue(const py::object &ob, ControlType type) +{ + switch (type) { + case ControlTypeBool: + return ControlValue(ob.cast()); + case ControlTypeByte: + return ControlValue(ob.cast()); + case ControlTypeInteger32: + return ControlValue(ob.cast()); + case ControlTypeInteger64: + return ControlValue(ob.cast()); + case ControlTypeFloat: + return ControlValue(ob.cast()); + case ControlTypeString: + return ControlValue(ob.cast()); + case ControlTypeRectangle: + case ControlTypeSize: + case ControlTypeNone: + default: + throw runtime_error("Control type not implemented"); + } +} + +static weak_ptr g_camera_manager; +static int g_eventfd; +static mutex g_reqlist_mutex; +static vector g_reqlist; + +static void handleRequestCompleted(Request *req) +{ + { + lock_guard guard(g_reqlist_mutex); + g_reqlist.push_back(req); + } + + uint64_t v = 1; + write(g_eventfd, &v, 8); +} + +void init_pyenums(py::module &m); + +PYBIND11_MODULE(_libcamera, m) +{ + init_pyenums(m); + + /* Forward declarations */ + + /* + * We need to declare all the classes here so that Python docstrings + * can be generated correctly. + * https://pybind11.readthedocs.io/en/latest/advanced/misc.html#avoiding-c-types-in-docstrings + */ + + auto pyCameraManager = py::class_(m, "CameraManager"); + auto pyCamera = py::class_(m, "Camera"); + auto pyCameraConfiguration = py::class_(m, "CameraConfiguration"); + auto pyStreamConfiguration = py::class_(m, "StreamConfiguration"); + auto pyStreamFormats = py::class_(m, "StreamFormats"); + auto pyFrameBufferAllocator = py::class_(m, "FrameBufferAllocator"); + auto pyFrameBuffer = py::class_(m, "FrameBuffer"); + auto pyStream = py::class_(m, "Stream"); + auto pyControlId = py::class_(m, "ControlId"); + auto pyRequest = py::class_(m, "Request"); + auto pyFrameMetadata = py::class_(m, "FrameMetadata"); + + /* Global functions */ + m.def("logSetLevel", &logSetLevel); + + /* Classes */ + pyCameraManager + .def_static("singleton", []() { + shared_ptr cm = g_camera_manager.lock(); + if (cm) + return cm; + + int fd = eventfd(0, 0); + if (fd == -1) + throw std::system_error(errno, std::generic_category(), "Failed to create eventfd"); + + cm = shared_ptr(new CameraManager, [](auto p) { + close(g_eventfd); + g_eventfd = -1; + delete p; + }); + + g_eventfd = fd; + g_camera_manager = cm; + + int ret = cm->start(); + if (ret) + throw std::system_error(-ret, std::generic_category(), "Failed to start CameraManager"); + + return cm; + }) + + .def_property_readonly("version", &CameraManager::version) + + .def_property_readonly("efd", [](CameraManager &) { + return g_eventfd; + }) + + .def("getReadyRequests", [](CameraManager &) { + vector v; + + { + lock_guard guard(g_reqlist_mutex); + swap(v, g_reqlist); + } + + vector ret; + + for (Request *req : v) { + py::object o = py::cast(req); + /* decrease the ref increased in Camera::queueRequest() */ + o.dec_ref(); + ret.push_back(o); + } + + return ret; + }) + + .def("get", py::overload_cast(&CameraManager::get), py::keep_alive<0, 1>()) + + .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(); + }, py::keep_alive<0, 1>()) + + /* 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; + }); + + pyCamera + .def_property_readonly("id", &Camera::id) + .def("acquire", &Camera::acquire) + .def("release", &Camera::release) + .def("start", [](Camera &self) { + self.requestCompleted.connect(handleRequestCompleted); + + int ret = self.start(); + if (ret) + self.requestCompleted.disconnect(handleRequestCompleted); + + return ret; + }) + + .def("stop", [](Camera &self) { + int ret = self.stop(); + if (!ret) + self.requestCompleted.disconnect(handleRequestCompleted); + + return ret; + }) + + .def("__repr__", [](Camera &self) { + return ""; + }) + + /* Keep the camera alive, as StreamConfiguration contains a Stream* */ + .def("generateConfiguration", &Camera::generateConfiguration, py::keep_alive<0, 1>()) + .def("configure", &Camera::configure) + + .def("createRequest", &Camera::createRequest, py::arg("cookie") = 0) + + .def("queueRequest", [](Camera &self, Request *req) { + py::object py_req = py::cast(req); + + py_req.inc_ref(); + + int ret = self.queueRequest(req); + if (ret) + py_req.dec_ref(); + + return ret; + }) + + .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("find_control", [](Camera &self, const string &name) { + const auto &controls = self.controls(); + + auto it = find_if(controls.begin(), controls.end(), + [&name](const auto &kvp) { return kvp.first->name() == name; }); + + if (it == controls.end()) + throw runtime_error("Control not found"); + + return it->first; + }, py::return_value_policy::reference_internal) + + .def_property_readonly("controls", [](Camera &self) { + py::dict ret; + + for (const auto &[id, ci] : self.controls()) { + ret[id->name().c_str()] = make_tuple(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; + }); + + pyCameraConfiguration + .def("__iter__", [](CameraConfiguration &self) { + return py::make_iterator(self); + }, py::keep_alive<0, 1>()) + .def("__len__", [](CameraConfiguration &self) { + return self.size(); + }) + .def("validate", &CameraConfiguration::validate) + .def("at", py::overload_cast(&CameraConfiguration::at), py::return_value_policy::reference_internal) + .def_property_readonly("size", &CameraConfiguration::size) + .def_property_readonly("empty", &CameraConfiguration::empty); + + pyStreamConfiguration + .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 size) { self.size.width = get<0>(size); self.size.height = get<1>(size); }) + .def_property( + "pixelFormat", + [](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); + + pyStreamFormats + .def_property_readonly("pixelFormats", [](StreamFormats &self) { + vector 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> 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)); + }); + + pyFrameBufferAllocator + .def(py::init>(), py::keep_alive<1, 2>()) + .def("allocate", &FrameBufferAllocator::allocate) + .def_property_readonly("allocated", &FrameBufferAllocator::allocated) + /* Create a list of FrameBuffers, where each FrameBuffer has a keep-alive to FrameBufferAllocator */ + .def("buffers", [](FrameBufferAllocator &self, Stream *stream) { + py::object py_self = py::cast(self); + py::list l; + for (auto &ub : self.buffers(stream)) { + py::object py_buf = py::cast(ub.get(), py::return_value_policy::reference_internal, py_self); + l.append(py_buf); + } + return l; + }); + + pyFrameBuffer + /* TODO: implement FrameBuffer::Plane properly */ + .def(py::init([](vector> planes, unsigned int cookie) { + vector v; + for (const auto &t : planes) + v.push_back({ SharedFD(get<0>(t)), FrameBuffer::Plane::kInvalidOffset, 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.get(); + }) + .def_property("cookie", &FrameBuffer::cookie, &FrameBuffer::setCookie); + + pyStream + .def_property_readonly("configuration", &Stream::configuration); + + pyControlId + .def_property_readonly("id", &ControlId::id) + .def_property_readonly("name", &ControlId::name) + .def_property_readonly("type", &ControlId::type); + + pyRequest + /* Fence is not supported, so we cannot expose addBuffer() directly */ + .def("addBuffer", [](Request &self, const Stream *stream, FrameBuffer *buffer) { + return self.addBuffer(stream, buffer); + }, py::keep_alive<1, 3>()) /* Request keeps Framebuffer alive */ + .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, ControlId &id, py::object value) { + self.controls().set(id.id(), PyToControlValue(value, id.type())); + }) + .def_property_readonly("metadata", [](Request &self) { + py::dict ret; + + for (const auto &[key, cv] : self.metadata()) { + const ControlId *id = controls::controls.at(key); + py::object ob = ControlValueToPy(cv); + + ret[id->name().c_str()] = ob; + } + + return ret; + }) + /* As we add a keep_alive to the fb in addBuffers(), we can only allow reuse with ReuseBuffers. */ + .def("reuse", [](Request &self) { self.reuse(Request::ReuseFlag::ReuseBuffers); }); + + pyFrameMetadata + .def_readonly("status", &FrameMetadata::status) + .def_readonly("sequence", &FrameMetadata::sequence) + .def_readonly("timestamp", &FrameMetadata::timestamp) + /* temporary helper, to be removed */ + .def_property_readonly("bytesused", [](FrameMetadata &self) { + vector v; + v.resize(self.planes().size()); + transform(self.planes().begin(), self.planes().end(), v.begin(), [](const auto &p) { return p.bytesused; }); + return v; + }); +} diff --git a/src/py/meson.build b/src/py/meson.build new file mode 100644 index 00000000..4ce9668c --- /dev/null +++ b/src/py/meson.build @@ -0,0 +1 @@ +subdir('libcamera') diff --git a/subprojects/.gitignore b/subprojects/.gitignore index 391fde2c..0e194289 100644 --- a/subprojects/.gitignore +++ b/subprojects/.gitignore @@ -1,3 +1,4 @@ /googletest-release* /libyuv -/packagecache \ No newline at end of file +/packagecache +/pybind11 diff --git a/subprojects/packagefiles/pybind11/meson.build b/subprojects/packagefiles/pybind11/meson.build new file mode 100644 index 00000000..67e89aec --- /dev/null +++ b/subprojects/packagefiles/pybind11/meson.build @@ -0,0 +1,8 @@ +project('pybind11', 'cpp', + version : '2.9.1', + license : 'BSD-3-Clause') + +pybind11_incdir = include_directories('include') + +pybind11_dep = declare_dependency( + include_directories : pybind11_incdir) diff --git a/subprojects/pybind11.wrap b/subprojects/pybind11.wrap new file mode 100644 index 00000000..2413e9ca --- /dev/null +++ b/subprojects/pybind11.wrap @@ -0,0 +1,8 @@ +[wrap-git] +url = https://github.com/pybind/pybind11.git +revision = 82734801f23314b4c34d70a79509e060a2648e04 +depth = 1 +patch_directory = pybind11 + +[provide] +pybind11 = pybind11_dep From patchwork Thu May 5 10:40:56 2022 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Tomi Valkeinen X-Patchwork-Id: 15788 Return-Path: X-Original-To: parsemail@patchwork.libcamera.org Delivered-To: parsemail@patchwork.libcamera.org Received: from lancelot.ideasonboard.com (lancelot.ideasonboard.com [92.243.16.209]) by patchwork.libcamera.org (Postfix) with ESMTPS id 37B87C326C for ; Thu, 5 May 2022 10:41:35 +0000 (UTC) Received: from lancelot.ideasonboard.com (localhost [IPv6:::1]) by lancelot.ideasonboard.com (Postfix) with ESMTP id 757946564D; Thu, 5 May 2022 12:41:31 +0200 (CEST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=libcamera.org; s=mail; t=1651747291; bh=drreAiTzVPj/8papvYf8Ka9q5BVWu4HwHS0fgUN0E98=; h=To:Date:In-Reply-To:References:Subject:List-Id:List-Unsubscribe: List-Archive:List-Post:List-Help:List-Subscribe:From:Reply-To: From; b=gT7b5PZGK0NsjEHODW0R6lUDh9U5FD5HItULVaY5IiXFvGDVyl8a9F2MnjtSy8W3+ lQIFEHuBfGp2qge6w5p05YfErpN6BC1oTEkupEcAVpls9CKjp0UoRuxOIlAwH9Je0D pFzGYrGOEGl72BbzFh+dMnJmPbncLvz/2m/eHAm1ISyWzDQLhHekMuM4kpUQ160oLn LpXhYThsqE1EG7pqQTFSa+0AHm3q9tRxrNLoIIBs38LnQVT85W0g+JZic343YaY0Cs Rs1JB92w6EEclZv/hsXCqwo2DEOrzdxWA5MNFJNK/wMKbjwt/UyaFFWhz77aqw4ylp 4RkCrN2YA+0mQ== Received: from perceval.ideasonboard.com (perceval.ideasonboard.com [213.167.242.64]) by lancelot.ideasonboard.com (Postfix) with ESMTPS id D1B5C65649 for ; Thu, 5 May 2022 12:41:27 +0200 (CEST) Authentication-Results: lancelot.ideasonboard.com; dkim=pass (1024-bit key; unprotected) header.d=ideasonboard.com header.i=@ideasonboard.com header.b="GqT4NQpM"; dkim-atps=neutral Received: from deskari.lan (91-156-85-209.elisa-laajakaista.fi [91.156.85.209]) by perceval.ideasonboard.com (Postfix) with ESMTPSA id 4D658492; Thu, 5 May 2022 12:41:27 +0200 (CEST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=ideasonboard.com; s=mail; t=1651747287; bh=drreAiTzVPj/8papvYf8Ka9q5BVWu4HwHS0fgUN0E98=; h=From:To:Cc:Subject:Date:In-Reply-To:References:From; b=GqT4NQpMjtvaN8AY8KJbWqeXFJjc1R4UBNKPxcX3FOYgDAk4Phgb9WBvJzVstUeJe qsKQBWfH3IOwHjopFv780TrwQuv5PCYrn1Z94YHN7wJeQaorpyvEwzOI2RGKywHm7n EjXt6V1SnE7R8ZCpZNiB3T0DfkClxmQIb+irnkD8= To: libcamera-devel@lists.libcamera.org, David Plowman , Kieran Bingham , Laurent Pinchart , Jacopo Mondi Date: Thu, 5 May 2022 13:40:56 +0300 Message-Id: <20220505104104.70841-6-tomi.valkeinen@ideasonboard.com> X-Mailer: git-send-email 2.34.1 In-Reply-To: <20220505104104.70841-1-tomi.valkeinen@ideasonboard.com> References: <20220505104104.70841-1-tomi.valkeinen@ideasonboard.com> MIME-Version: 1.0 Subject: [libcamera-devel] [PATCH v7 05/13] py: generate control enums from yaml X-BeenThere: libcamera-devel@lists.libcamera.org X-Mailman-Version: 2.1.29 Precedence: list List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , X-Patchwork-Original-From: Tomi Valkeinen via libcamera-devel From: Tomi Valkeinen Reply-To: Tomi Valkeinen Errors-To: libcamera-devel-bounces@lists.libcamera.org Sender: "libcamera-devel" Generate enums for controls from control_ids.yaml. The generator script has some heuristics to generate nicer enum names. E.g. instead of having "LensShadingMapMode.LensShadingMapModeOff" we get "LensShadingMapMode.Off". This heuristics may need to be updated when the yaml file is changed or new controls are added. Signed-off-by: Tomi Valkeinen Reviewed-by: Kieran Bingham Reviewed-by: Laurent Pinchart --- src/py/libcamera/gen-py-control-enums.py | 95 +++++++++++++++++++++++ src/py/libcamera/meson.build | 9 +++ src/py/libcamera/pyenums_generated.cpp.in | 21 +++++ src/py/libcamera/pymain.cpp | 2 + 4 files changed, 127 insertions(+) create mode 100755 src/py/libcamera/gen-py-control-enums.py create mode 100644 src/py/libcamera/pyenums_generated.cpp.in diff --git a/src/py/libcamera/gen-py-control-enums.py b/src/py/libcamera/gen-py-control-enums.py new file mode 100755 index 00000000..f1b18389 --- /dev/null +++ b/src/py/libcamera/gen-py-control-enums.py @@ -0,0 +1,95 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0-or-later +# +# Generate Python bindings enums for controls from YAML + +import argparse +import string +import sys +import yaml + + +def find_common_prefix(strings): + prefix = strings[0] + + for string in strings[1:]: + while string[:len(prefix)] != prefix and prefix: + prefix = prefix[:len(prefix) - 1] + if not prefix: + break + + return prefix + + +def generate_py(controls): + out = "" + + for ctrl in controls: + name, ctrl = ctrl.popitem() + + enum = ctrl.get('enum') + if not enum: + continue + + if ctrl.get('draft'): + ns = "libcamera::controls::draft::" + else: + ns = "libcamera::controls::" + + cpp_enum = name + "Enum" + + out += "\tpy::enum_<{}{}>(m, \"{}\")\n".format(ns, cpp_enum, name) + + if name == "LensShadingMapMode": + prefix = "LensShadingMapMode" + else: + prefix = find_common_prefix([e["name"] for e in enum]) + + for entry in enum: + cpp_enum = entry["name"] + py_enum = entry["name"][len(prefix):] + + out += "\t\t.value(\"{}\", {}{})\n".format(py_enum, ns, cpp_enum) + + out += "\t;\n" + + return {"enums": out} + + +def fill_template(template, data): + template = open(template, 'rb').read() + template = template.decode('utf-8') + template = string.Template(template) + return template.substitute(data) + + +def main(argv): + # Parse command line arguments + parser = argparse.ArgumentParser() + parser.add_argument('-o', dest='output', metavar='file', type=str, + help='Output file name. Defaults to standard output if not specified.') + parser.add_argument('input', type=str, + help='Input file name.') + parser.add_argument('template', type=str, + help='Template file name.') + args = parser.parse_args(argv[1:]) + + data = open(args.input, 'rb').read() + controls = yaml.safe_load(data)['controls'] + + data = generate_py(controls) + + data = fill_template(args.template, data) + + if args.output: + output = open(args.output, 'wb') + output.write(data.encode('utf-8')) + output.close() + else: + sys.stdout.write(data) + + return 0 + + +if __name__ == '__main__': + sys.exit(main(sys.argv)) diff --git a/src/py/libcamera/meson.build b/src/py/libcamera/meson.build index e1f5cf21..42988e55 100644 --- a/src/py/libcamera/meson.build +++ b/src/py/libcamera/meson.build @@ -17,6 +17,15 @@ pycamera_sources = files([ 'pyenums.cpp', ]) +gen_input_files = [meson.project_source_root() / 'src/libcamera/control_ids.yaml', 'pyenums_generated.cpp.in'] + +generated_sources = custom_target('py_gen_controls', + input : gen_input_files, + output : ['pyenums_generated.cpp'], + command : ['gen-py-control-enums.py', '-o', '@OUTPUT@', '@INPUT@']) + +pycamera_sources += generated_sources + pycamera_deps = [ libcamera_public, py3_dep, diff --git a/src/py/libcamera/pyenums_generated.cpp.in b/src/py/libcamera/pyenums_generated.cpp.in new file mode 100644 index 00000000..96daf257 --- /dev/null +++ b/src/py/libcamera/pyenums_generated.cpp.in @@ -0,0 +1,21 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +/* + * Copyright (C) 2021, Tomi Valkeinen + * + * Python bindings + * + * This file is auto-generated. Do not edit. + */ + +#include + +#include + +namespace py = pybind11; + +using namespace libcamera; + +void init_pyenums_generated(py::module& m) +{ +${enums} +} diff --git a/src/py/libcamera/pymain.cpp b/src/py/libcamera/pymain.cpp index 54674caf..81d48a20 100644 --- a/src/py/libcamera/pymain.cpp +++ b/src/py/libcamera/pymain.cpp @@ -115,10 +115,12 @@ static void handleRequestCompleted(Request *req) } void init_pyenums(py::module &m); +void init_pyenums_generated(py::module &m); PYBIND11_MODULE(_libcamera, m) { init_pyenums(m); + init_pyenums_generated(m); /* Forward declarations */ From patchwork Thu May 5 10:40:57 2022 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Tomi Valkeinen X-Patchwork-Id: 15789 Return-Path: X-Original-To: parsemail@patchwork.libcamera.org Delivered-To: parsemail@patchwork.libcamera.org Received: from lancelot.ideasonboard.com (lancelot.ideasonboard.com [92.243.16.209]) by patchwork.libcamera.org (Postfix) with ESMTPS id E4D1AC326D for ; Thu, 5 May 2022 10:41:35 +0000 (UTC) Received: from lancelot.ideasonboard.com (localhost [IPv6:::1]) by lancelot.ideasonboard.com (Postfix) with ESMTP id D8AD365650; Thu, 5 May 2022 12:41:32 +0200 (CEST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=libcamera.org; s=mail; t=1651747292; bh=BlGrgw8+v8zkqQ3ayRhd6rzwdtmWYvCqgWsyJUVwng4=; h=To:Date:In-Reply-To:References:Subject:List-Id:List-Unsubscribe: List-Archive:List-Post:List-Help:List-Subscribe:From:Reply-To: From; b=B4+x7s1CTJypCoYUuoepxJrsagCD+qfmNYmk/QUJiWhYTzeXye58mJN6yv55Dxi5u XV+B178NxnDqRp4hEFVa8tLJHp5Gpo2IAaT3oSa0OUTL5lGM/qFQXeLtPV597UzDU2 ZrQ2BEQwO7MsYfQwkzAZ1gIOjdDDGZgh2IHnVA4Lf4HK3QPcMyCmupHZfwe0pOuWf6 0DrhTKuDdrK0ec4yoEePuZ8W1IzujPZLmowunDe5pSzZhf6Q9HQWzxmVwRr5X6QrtF MPQW41XORvJ6PvCHhlfexmrnjE/uU/5A/PQOinTKyS+U6eHvbX/BGoZZemoI/ukqsh Uhgx7YODvHvfw== Received: from perceval.ideasonboard.com (perceval.ideasonboard.com [IPv6:2001:4b98:dc2:55:216:3eff:fef7:d647]) by lancelot.ideasonboard.com (Postfix) with ESMTPS id 92F5365645 for ; Thu, 5 May 2022 12:41:28 +0200 (CEST) Authentication-Results: lancelot.ideasonboard.com; dkim=pass (1024-bit key; unprotected) header.d=ideasonboard.com header.i=@ideasonboard.com header.b="wmxUa2ai"; dkim-atps=neutral Received: from deskari.lan (91-156-85-209.elisa-laajakaista.fi [91.156.85.209]) by perceval.ideasonboard.com (Postfix) with ESMTPSA id D0B8C4A8; Thu, 5 May 2022 12:41:27 +0200 (CEST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=ideasonboard.com; s=mail; t=1651747288; bh=BlGrgw8+v8zkqQ3ayRhd6rzwdtmWYvCqgWsyJUVwng4=; h=From:To:Cc:Subject:Date:In-Reply-To:References:From; b=wmxUa2aiU79aw8TaUXrXyHgB3U/W0sVJ70Ku07kHOZ3ibL8ARNDhd2/MOh8pGeW4C Ua3qS1r9NlcYn9MduMk6QoOpaqSC6FJQwjDoEkyRV1eFnxyV6Px2zG03aq9mpbt/T+ V69Iq7RaiD6W+7RcxeitgbTS8ulY3/HC8Lo6aB6A= To: libcamera-devel@lists.libcamera.org, David Plowman , Kieran Bingham , Laurent Pinchart , Jacopo Mondi Date: Thu, 5 May 2022 13:40:57 +0300 Message-Id: <20220505104104.70841-7-tomi.valkeinen@ideasonboard.com> X-Mailer: git-send-email 2.34.1 In-Reply-To: <20220505104104.70841-1-tomi.valkeinen@ideasonboard.com> References: <20220505104104.70841-1-tomi.valkeinen@ideasonboard.com> MIME-Version: 1.0 Subject: [libcamera-devel] [PATCH v7 06/13] py: add unittests.py X-BeenThere: libcamera-devel@lists.libcamera.org X-Mailman-Version: 2.1.29 Precedence: list List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , X-Patchwork-Original-From: Tomi Valkeinen via libcamera-devel From: Tomi Valkeinen Reply-To: Tomi Valkeinen Errors-To: libcamera-devel-bounces@lists.libcamera.org Sender: "libcamera-devel" Add a simple unittests.py as a base for python unittests. Signed-off-by: Tomi Valkeinen Reviewed-by: Laurent Pinchart --- test/meson.build | 1 + test/py/meson.build | 17 ++ test/py/unittests.py | 368 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 386 insertions(+) create mode 100644 test/py/meson.build create mode 100755 test/py/unittests.py diff --git a/test/meson.build b/test/meson.build index fd4c5ca0..623f3baa 100644 --- a/test/meson.build +++ b/test/meson.build @@ -18,6 +18,7 @@ subdir('log') subdir('media_device') subdir('pipeline') subdir('process') +subdir('py') subdir('serialization') subdir('stream') subdir('v4l2_compat') diff --git a/test/py/meson.build b/test/py/meson.build new file mode 100644 index 00000000..f6b42bd0 --- /dev/null +++ b/test/py/meson.build @@ -0,0 +1,17 @@ +# SPDX-License-Identifier: CC0-1.0 + +if not pycamera_enabled + subdir_done() +endif + +pymod = import('python') +py3 = pymod.find_installation('python3') + +pypathdir = meson.project_build_root() / 'src/py' + +test('pyunittests', + py3, + args : files('unittests.py'), + env : ['PYTHONPATH=' + pypathdir], + suite : 'pybindings', + is_parallel : false) diff --git a/test/py/unittests.py b/test/py/unittests.py new file mode 100755 index 00000000..15d5b4a7 --- /dev/null +++ b/test/py/unittests.py @@ -0,0 +1,368 @@ +#!/usr/bin/env python3 + +# SPDX-License-Identifier: GPL-2.0-or-later +# Copyright (C) 2021, Tomi Valkeinen + +from collections import defaultdict +import errno +import gc +import libcamera as libcam +import os +import selectors +import time +import unittest +import weakref + + +class MyTestCase(unittest.TestCase): + def assertZero(self, a, msg=None): + self.assertEqual(a, 0, msg) + + +class SimpleTestMethods(MyTestCase): + def test_find_ref(self): + cm = libcam.CameraManager.singleton() + wr_cm = weakref.ref(cm) + + cam = cm.find("platform/vimc") + self.assertIsNotNone(cam) + wr_cam = weakref.ref(cam) + + cm = None + gc.collect() + self.assertIsNotNone(wr_cm()) + + cam = None + gc.collect() + self.assertIsNone(wr_cm()) + self.assertIsNone(wr_cam()) + + def test_get_ref(self): + cm = libcam.CameraManager.singleton() + wr_cm = weakref.ref(cm) + + cam = cm.get("platform/vimc.0 Sensor B") + self.assertTrue(cam is not None) + wr_cam = weakref.ref(cam) + + cm = None + gc.collect() + self.assertIsNotNone(wr_cm()) + + cam = None + gc.collect() + self.assertIsNone(wr_cm()) + self.assertIsNone(wr_cam()) + + def test_acquire_release(self): + cm = libcam.CameraManager.singleton() + cam = cm.get("platform/vimc.0 Sensor B") + self.assertTrue(cam is not None) + + ret = cam.acquire() + self.assertZero(ret) + + ret = cam.release() + self.assertZero(ret) + + def test_double_acquire(self): + cm = libcam.CameraManager.singleton() + cam = cm.get("platform/vimc.0 Sensor B") + self.assertTrue(cam is not None) + + ret = cam.acquire() + self.assertZero(ret) + + libcam.logSetLevel("Camera", "FATAL") + ret = cam.acquire() + self.assertEqual(ret, -errno.EBUSY) + libcam.logSetLevel("Camera", "ERROR") + + ret = cam.release() + self.assertZero(ret) + + ret = cam.release() + # I expected EBUSY, but looks like double release works fine + self.assertZero(ret) + + +class CameraTesterBase(MyTestCase): + def setUp(self): + self.cm = libcam.CameraManager.singleton() + self.cam = self.cm.find("platform/vimc") + if self.cam is None: + self.cm = None + raise Exception("No vimc found") + + ret = self.cam.acquire() + if ret != 0: + self.cam = None + self.cm = None + raise Exception("Failed to acquire camera") + + def tearDown(self): + # If a test fails, the camera may be in running state. So always stop. + self.cam.stop() + + ret = self.cam.release() + if ret != 0: + raise Exception("Failed to release camera") + + self.cam = None + self.cm = None + + +class AllocatorTestMethods(CameraTesterBase): + def test_allocator(self): + cam = self.cam + + camconfig = cam.generateConfiguration([libcam.StreamRole.StillCapture]) + self.assertTrue(camconfig.size == 1) + wr_camconfig = weakref.ref(camconfig) + + streamconfig = camconfig.at(0) + wr_streamconfig = weakref.ref(streamconfig) + + ret = cam.configure(camconfig) + self.assertZero(ret) + + stream = streamconfig.stream + wr_stream = weakref.ref(stream) + + # stream should keep streamconfig and camconfig alive + streamconfig = None + camconfig = None + gc.collect() + self.assertIsNotNone(wr_camconfig()) + self.assertIsNotNone(wr_streamconfig()) + + allocator = libcam.FrameBufferAllocator(cam) + ret = allocator.allocate(stream) + self.assertTrue(ret > 0) + wr_allocator = weakref.ref(allocator) + + buffers = allocator.buffers(stream) + buffers = None + + buffer = allocator.buffers(stream)[0] + self.assertIsNotNone(buffer) + wr_buffer = weakref.ref(buffer) + + allocator = None + gc.collect() + self.assertIsNotNone(wr_buffer()) + self.assertIsNotNone(wr_allocator()) + self.assertIsNotNone(wr_stream()) + + buffer = None + gc.collect() + self.assertIsNone(wr_buffer()) + self.assertIsNone(wr_allocator()) + self.assertIsNotNone(wr_stream()) + + stream = None + gc.collect() + self.assertIsNone(wr_stream()) + self.assertIsNone(wr_camconfig()) + self.assertIsNone(wr_streamconfig()) + + +class SimpleCaptureMethods(CameraTesterBase): + def test_sleep(self): + cm = self.cm + cam = self.cam + + camconfig = cam.generateConfiguration([libcam.StreamRole.StillCapture]) + self.assertTrue(camconfig.size == 1) + + streamconfig = camconfig.at(0) + fmts = streamconfig.formats + + ret = cam.configure(camconfig) + self.assertZero(ret) + + stream = streamconfig.stream + + allocator = libcam.FrameBufferAllocator(cam) + ret = allocator.allocate(stream) + self.assertTrue(ret > 0) + + num_bufs = len(allocator.buffers(stream)) + + reqs = [] + for i in range(num_bufs): + req = cam.createRequest(i) + self.assertIsNotNone(req) + + buffer = allocator.buffers(stream)[i] + ret = req.addBuffer(stream, buffer) + self.assertZero(ret) + + reqs.append(req) + + buffer = None + + ret = cam.start() + self.assertZero(ret) + + for req in reqs: + ret = cam.queueRequest(req) + self.assertZero(ret) + + reqs = None + gc.collect() + + time.sleep(0.5) + + reqs = cm.getReadyRequests() + + self.assertTrue(len(reqs) == num_bufs) + + for i, req in enumerate(reqs): + self.assertTrue(i == req.cookie) + + reqs = None + gc.collect() + + ret = cam.stop() + self.assertZero(ret) + + def test_select(self): + cm = self.cm + cam = self.cam + + camconfig = cam.generateConfiguration([libcam.StreamRole.StillCapture]) + self.assertTrue(camconfig.size == 1) + + streamconfig = camconfig.at(0) + fmts = streamconfig.formats + + ret = cam.configure(camconfig) + self.assertZero(ret) + + stream = streamconfig.stream + + allocator = libcam.FrameBufferAllocator(cam) + ret = allocator.allocate(stream) + self.assertTrue(ret > 0) + + num_bufs = len(allocator.buffers(stream)) + + reqs = [] + for i in range(num_bufs): + req = cam.createRequest(i) + self.assertIsNotNone(req) + + buffer = allocator.buffers(stream)[i] + ret = req.addBuffer(stream, buffer) + self.assertZero(ret) + + reqs.append(req) + + buffer = None + + ret = cam.start() + self.assertZero(ret) + + for req in reqs: + ret = cam.queueRequest(req) + self.assertZero(ret) + + reqs = None + gc.collect() + + sel = selectors.DefaultSelector() + sel.register(cm.efd, selectors.EVENT_READ, 123) + + reqs = [] + + running = True + while running: + events = sel.select() + for key, mask in events: + os.read(key.fileobj, 8) + + ready_reqs = cm.getReadyRequests() + + self.assertTrue(len(ready_reqs) > 0) + + reqs += ready_reqs + + if len(reqs) == num_bufs: + running = False + + self.assertTrue(len(reqs) == num_bufs) + + for i, req in enumerate(reqs): + self.assertTrue(i == req.cookie) + + reqs = None + gc.collect() + + ret = cam.stop() + self.assertZero(ret) + + +# Recursively expand slist's objects into olist, using seen to track already +# processed objects. +def _getr(slist, olist, seen): + for e in slist: + if id(e) in seen: + continue + seen.add(id(e)) + olist.append(e) + tl = gc.get_referents(e) + if tl: + _getr(tl, olist, seen) + + +def get_all_objects(ignored=[]): + gcl = gc.get_objects() + olist = [] + seen = set() + + seen.add(id(gcl)) + seen.add(id(olist)) + seen.add(id(seen)) + seen.update(set([id(o) for o in ignored])) + + _getr(gcl, olist, seen) + + return olist + + +def create_type_count_map(olist): + map = defaultdict(int) + for o in olist: + map[type(o)] += 1 + return map + + +def diff_type_count_maps(before, after): + return [(k, after[k] - before[k]) for k in after if after[k] != before[k]] + + +if __name__ == '__main__': + # doesn't work very well, as things always leak a bit + test_leaks = False + + if test_leaks: + gc.unfreeze() + gc.collect() + + obs_before = get_all_objects() + + unittest.main(exit=False) + + if test_leaks: + gc.unfreeze() + gc.collect() + + obs_after = get_all_objects([obs_before]) + + before = create_type_count_map(obs_before) + after = create_type_count_map(obs_after) + + leaks = diff_type_count_maps(before, after) + if len(leaks) > 0: + print(leaks) From patchwork Thu May 5 10:40:58 2022 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Tomi Valkeinen X-Patchwork-Id: 15790 Return-Path: X-Original-To: parsemail@patchwork.libcamera.org Delivered-To: parsemail@patchwork.libcamera.org Received: from lancelot.ideasonboard.com (lancelot.ideasonboard.com [92.243.16.209]) by patchwork.libcamera.org (Postfix) with ESMTPS id 6BA59C326E for ; Thu, 5 May 2022 10:41:36 +0000 (UTC) Received: from lancelot.ideasonboard.com (localhost [IPv6:::1]) by lancelot.ideasonboard.com (Postfix) with ESMTP id 497B365669; Thu, 5 May 2022 12:41:34 +0200 (CEST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=libcamera.org; s=mail; t=1651747294; bh=u0GWr0XFBD34MHNnGfk2T5yscNS0W1+lRomnDScD0js=; h=To:Date:In-Reply-To:References:Subject:List-Id:List-Unsubscribe: List-Archive:List-Post:List-Help:List-Subscribe:From:Reply-To: From; b=OUSZI+0QTHV2AjWw4Ep73LypYHyQ+BkIWEJNclMQXK5TPdDQW0pWEWtCZ9PyMP+a2 iasLpVBArECPHt2TIn+b/Pg/YVpBh4pmrZD0X8jmNPnVpEqSbTLes9a6WaXG9e7qrA kStmCEkGoai7MjYNciIcM/67RKVkmRGuEEmWe2xvKSV/TxKWaxAkb3rlKYO5A43AIE D8PX788JW77suAjnppfCPmFtka5+5s45F71BMemrm7Vn5ZDkOmDiwcxZp5kcUx6kec 0ejjlmiY72xCKBAr0s0/xcbNPvR4SnOaBx0LH6eTDF90Ho6hR+DYY/MH4wScWyEZdD XHeABEXgrAKcw== Received: from perceval.ideasonboard.com (perceval.ideasonboard.com [IPv6:2001:4b98:dc2:55:216:3eff:fef7:d647]) by lancelot.ideasonboard.com (Postfix) with ESMTPS id 6E84665650 for ; Thu, 5 May 2022 12:41:29 +0200 (CEST) Authentication-Results: lancelot.ideasonboard.com; dkim=pass (1024-bit key; unprotected) header.d=ideasonboard.com header.i=@ideasonboard.com header.b="Wnme43OR"; dkim-atps=neutral Received: from deskari.lan (91-156-85-209.elisa-laajakaista.fi [91.156.85.209]) by perceval.ideasonboard.com (Postfix) with ESMTPSA id 59915492; Thu, 5 May 2022 12:41:28 +0200 (CEST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=ideasonboard.com; s=mail; t=1651747289; bh=u0GWr0XFBD34MHNnGfk2T5yscNS0W1+lRomnDScD0js=; h=From:To:Cc:Subject:Date:In-Reply-To:References:From; b=Wnme43OReqtcbbiWaS9x9AKHPJqAGV8gdD/T/92rqAAfL5wH2axlhxxJhHJuzbFYn 3rxtApvwxdsO5uZ0D6sfY9iKc7SfYbB6foszGdmmq7GT4Z4e5ALjTYSTUz0XqltrRf KMtddYlJ7X1l3WiQirmHIk1mnQMHN/6n2bf0coME= To: libcamera-devel@lists.libcamera.org, David Plowman , Kieran Bingham , Laurent Pinchart , Jacopo Mondi Date: Thu, 5 May 2022 13:40:58 +0300 Message-Id: <20220505104104.70841-8-tomi.valkeinen@ideasonboard.com> X-Mailer: git-send-email 2.34.1 In-Reply-To: <20220505104104.70841-1-tomi.valkeinen@ideasonboard.com> References: <20220505104104.70841-1-tomi.valkeinen@ideasonboard.com> MIME-Version: 1.0 Subject: [libcamera-devel] [PATCH v7 07/13] py: Add cam.py X-BeenThere: libcamera-devel@lists.libcamera.org X-Mailman-Version: 2.1.29 Precedence: list List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , X-Patchwork-Original-From: Tomi Valkeinen via libcamera-devel From: Tomi Valkeinen Reply-To: Tomi Valkeinen Errors-To: libcamera-devel-bounces@lists.libcamera.org Sender: "libcamera-devel" Add cam.py, which mimics the 'cam' tool. Four rendering backends are added: * null - Do nothing * kms - Use KMS with dmabufs * qt - SW render on a Qt window * qtgl - OpenGL render on a Qt window All the renderers handle only a few pixel formats, and especially the GL renderer is just a prototype. Signed-off-by: Tomi Valkeinen --- src/py/cam/cam.py | 483 +++++++++++++++++++++++++++++++++++++++ src/py/cam/cam_kms.py | 183 +++++++++++++++ src/py/cam/cam_null.py | 47 ++++ src/py/cam/cam_qt.py | 354 ++++++++++++++++++++++++++++ src/py/cam/cam_qtgl.py | 385 +++++++++++++++++++++++++++++++ src/py/cam/gl_helpers.py | 74 ++++++ 6 files changed, 1526 insertions(+) create mode 100755 src/py/cam/cam.py create mode 100644 src/py/cam/cam_kms.py create mode 100644 src/py/cam/cam_null.py create mode 100644 src/py/cam/cam_qt.py create mode 100644 src/py/cam/cam_qtgl.py create mode 100644 src/py/cam/gl_helpers.py diff --git a/src/py/cam/cam.py b/src/py/cam/cam.py new file mode 100755 index 00000000..4efa6459 --- /dev/null +++ b/src/py/cam/cam.py @@ -0,0 +1,483 @@ +#!/usr/bin/env python3 + +# SPDX-License-Identifier: GPL-2.0-or-later +# Copyright (C) 2021, Tomi Valkeinen + +import argparse +import binascii +import libcamera as libcam +import os +import sys + + +class CustomCameraAction(argparse.Action): + def __call__(self, parser, namespace, values, option_string=None): + print(self.dest, values) + + if "camera" not in namespace or namespace.camera is None: + setattr(namespace, "camera", []) + + previous = namespace.camera + previous.append((self.dest, values)) + setattr(namespace, "camera", previous) + + +class CustomAction(argparse.Action): + def __init__(self, option_strings, dest, **kwargs): + super().__init__(option_strings, dest, default={}, **kwargs) + + def __call__(self, parser, namespace, values, option_string=None): + if len(namespace.camera) == 0: + print(f"Option {option_string} requires a --camera context") + sys.exit(-1) + + if self.type == bool: + values = True + + current = namespace.camera[-1] + + data = getattr(namespace, self.dest) + + if self.nargs == "+": + if current not in data: + data[current] = [] + + data[current] += values + else: + data[current] = values + + +def do_cmd_list(cm): + print("Available cameras:") + + for idx, c in enumerate(cm.cameras): + print(f"{idx + 1}: {c.id}") + + +def do_cmd_list_props(ctx): + camera = ctx["camera"] + + print("Properties for", ctx["id"]) + + for name, prop in camera.properties.items(): + print("\t{}: {}".format(name, prop)) + + +def do_cmd_list_controls(ctx): + camera = ctx["camera"] + + print("Controls for", ctx["id"]) + + for name, prop in camera.controls.items(): + print("\t{}: {}".format(name, prop)) + + +def do_cmd_info(ctx): + camera = ctx["camera"] + + print("Stream info for", ctx["id"]) + + roles = [libcam.StreamRole.Viewfinder] + + camconfig = camera.generateConfiguration(roles) + if camconfig is None: + raise Exception("Generating config failed") + + for i, stream_config in enumerate(camconfig): + print("\t{}: {}".format(i, stream_config.toString())) + + formats = stream_config.formats + for fmt in formats.pixelFormats: + print("\t * Pixelformat:", fmt, formats.range(fmt)) + + for size in formats.sizes(fmt): + print("\t -", size) + + +def acquire(ctx): + camera = ctx["camera"] + + camera.acquire() + + +def release(ctx): + camera = ctx["camera"] + + camera.release() + + +def parse_streams(ctx): + streams = [] + + for stream_desc in ctx["opt-stream"]: + stream_opts = {"role": libcam.StreamRole.Viewfinder} + + for stream_opt in stream_desc.split(","): + if stream_opt == 0: + continue + + arr = stream_opt.split("=") + if len(arr) != 2: + print("Bad stream option", stream_opt) + sys.exit(-1) + + key = arr[0] + value = arr[1] + + if key in ["width", "height"]: + value = int(value) + elif key == "role": + rolemap = { + "still": libcam.StreamRole.StillCapture, + "raw": libcam.StreamRole.Raw, + "video": libcam.StreamRole.VideoRecording, + "viewfinder": libcam.StreamRole.Viewfinder, + } + + role = rolemap.get(value.lower(), None) + + if role is None: + print("Bad stream role", value) + sys.exit(-1) + + value = role + elif key == "pixelformat": + pass + else: + print("Bad stream option key", key) + sys.exit(-1) + + stream_opts[key] = value + + streams.append(stream_opts) + + return streams + + +def configure(ctx): + camera = ctx["camera"] + + streams = parse_streams(ctx) + + roles = [opts["role"] for opts in streams] + + camconfig = camera.generateConfiguration(roles) + if camconfig is None: + raise Exception("Generating config failed") + + for idx, stream_opts in enumerate(streams): + stream_config = camconfig.at(idx) + + if "width" in stream_opts and "height" in stream_opts: + stream_config.size = (stream_opts["width"], stream_opts["height"]) + + if "pixelformat" in stream_opts: + stream_config.pixelFormat = stream_opts["pixelformat"] + + stat = camconfig.validate() + + if stat == libcam.ConfigurationStatus.Invalid: + print("Camera configuration invalid") + exit(-1) + elif stat == libcam.ConfigurationStatus.Adjusted: + if ctx["opt-strict-formats"]: + print("Adjusting camera configuration disallowed by --strict-formats argument") + exit(-1) + + print("Camera configuration adjusted") + + r = camera.configure(camconfig) + if r != 0: + raise Exception("Configure failed") + + ctx["stream-names"] = {} + ctx["streams"] = [] + + for idx, stream_config in enumerate(camconfig): + stream = stream_config.stream + ctx["streams"].append(stream) + ctx["stream-names"][stream] = "stream" + str(idx) + print("{}-{}: stream config {}".format(ctx["id"], ctx["stream-names"][stream], stream.configuration.toString())) + + +def alloc_buffers(ctx): + camera = ctx["camera"] + + allocator = libcam.FrameBufferAllocator(camera) + + for idx, stream in enumerate(ctx["streams"]): + ret = allocator.allocate(stream) + if ret < 0: + print("Can't allocate buffers") + exit(-1) + + allocated = len(allocator.buffers(stream)) + + print("{}-{}: Allocated {} buffers".format(ctx["id"], ctx["stream-names"][stream], allocated)) + + ctx["allocator"] = allocator + + +def create_requests(ctx): + camera = ctx["camera"] + + ctx["requests"] = [] + + # Identify the stream with the least number of buffers + num_bufs = min([len(ctx["allocator"].buffers(stream)) for stream in ctx["streams"]]) + + requests = [] + + for buf_num in range(num_bufs): + request = camera.createRequest(ctx["idx"]) + + if request is None: + print("Can't create request") + exit(-1) + + for stream in ctx["streams"]: + buffers = ctx["allocator"].buffers(stream) + buffer = buffers[buf_num] + + ret = request.addBuffer(stream, buffer) + if ret < 0: + print("Can't set buffer for request") + exit(-1) + + requests.append(request) + + ctx["requests"] = requests + + +def start(ctx): + camera = ctx["camera"] + + camera.start() + + +def stop(ctx): + camera = ctx["camera"] + + camera.stop() + + +def queue_requests(ctx): + camera = ctx["camera"] + + for request in ctx["requests"]: + camera.queueRequest(request) + ctx["reqs-queued"] += 1 + + del ctx["requests"] + + +def capture_init(contexts): + for ctx in contexts: + acquire(ctx) + + for ctx in contexts: + configure(ctx) + + for ctx in contexts: + alloc_buffers(ctx) + + for ctx in contexts: + create_requests(ctx) + + +def capture_start(contexts): + for ctx in contexts: + start(ctx) + + for ctx in contexts: + queue_requests(ctx) + + +# Called from renderer when there is a libcamera event +def event_handler(state): + cm = state["cm"] + contexts = state["contexts"] + + os.read(cm.efd, 8) + + reqs = cm.getReadyRequests() + + for req in reqs: + ctx = next(ctx for ctx in contexts if ctx["idx"] == req.cookie) + request_handler(state, ctx, req) + + running = any(ctx["reqs-completed"] < ctx["opt-capture"] for ctx in contexts) + return running + + +def request_handler(state, ctx, req): + if req.status != libcam.RequestStatus.Complete: + raise Exception("{}: Request failed: {}".format(ctx["id"], req.status)) + + buffers = req.buffers + + # Compute the frame rate. The timestamp is arbitrarily retrieved from + # the first buffer, as all buffers should have matching timestamps. + ts = buffers[next(iter(buffers))].metadata.timestamp + last = ctx.get("last", 0) + fps = 1000000000.0 / (ts - last) if (last != 0 and (ts - last) != 0) else 0 + ctx["last"] = ts + ctx["fps"] = fps + + for stream, fb in buffers.items(): + stream_name = ctx["stream-names"][stream] + + crcs = [] + if ctx["opt-crc"]: + with fb.mmap(0) as b: + crc = binascii.crc32(b) + crcs.append(crc) + + meta = fb.metadata + + print("{:.6f} ({:.2f} fps) {}-{}: seq {}, bytes {}, CRCs {}" + .format(ts / 1000000000, fps, + ctx["id"], stream_name, + meta.sequence, meta.bytesused, + crcs)) + + if ctx["opt-metadata"]: + reqmeta = req.metadata + for ctrl, val in reqmeta.items(): + print(f"\t{ctrl} = {val}") + + if ctx["opt-save-frames"]: + with fb.mmap(0) as b: + filename = "frame-{}-{}-{}.data".format(ctx["id"], stream_name, ctx["reqs-completed"]) + with open(filename, "wb") as f: + f.write(b) + + state["renderer"].request_handler(ctx, req) + + ctx["reqs-completed"] += 1 + + +# Called from renderer when it has finished with a request +def request_prcessed(ctx, req): + camera = ctx["camera"] + + if ctx["reqs-queued"] < ctx["opt-capture"]: + req.reuse() + camera.queueRequest(req) + ctx["reqs-queued"] += 1 + + +def capture_deinit(contexts): + for ctx in contexts: + stop(ctx) + + for ctx in contexts: + release(ctx) + + +def do_cmd_capture(state): + capture_init(state["contexts"]) + + renderer = state["renderer"] + + renderer.setup() + + capture_start(state["contexts"]) + + renderer.run() + + capture_deinit(state["contexts"]) + + +def main(): + parser = argparse.ArgumentParser() + # global options + parser.add_argument("-l", "--list", action="store_true", help="List all cameras") + parser.add_argument("-c", "--camera", type=int, action="extend", nargs=1, default=[], help="Specify which camera to operate on, by index") + parser.add_argument("-p", "--list-properties", action="store_true", help="List cameras properties") + parser.add_argument("--list-controls", action="store_true", help="List cameras controls") + parser.add_argument("-I", "--info", action="store_true", help="Display information about stream(s)") + parser.add_argument("-R", "--renderer", default="null", help="Renderer (null, kms, qt, qtgl)") + + # per camera options + parser.add_argument("-C", "--capture", nargs="?", type=int, const=1000000, action=CustomAction, help="Capture until interrupted by user or until CAPTURE frames captured") + parser.add_argument("--crc", nargs=0, type=bool, action=CustomAction, help="Print CRC32 for captured frames") + parser.add_argument("--save-frames", nargs=0, type=bool, action=CustomAction, help="Save captured frames to files") + parser.add_argument("--metadata", nargs=0, type=bool, action=CustomAction, help="Print the metadata for completed requests") + parser.add_argument("--strict-formats", type=bool, nargs=0, action=CustomAction, help="Do not allow requested stream format(s) to be adjusted") + parser.add_argument("-s", "--stream", nargs="+", action=CustomAction) + args = parser.parse_args() + + cm = libcam.CameraManager.singleton() + + if args.list: + do_cmd_list(cm) + + contexts = [] + + for cam_idx in args.camera: + camera = next((c for i, c in enumerate(cm.cameras) if i + 1 == cam_idx), None) + + if camera is None: + print("Unable to find camera", cam_idx) + return -1 + + contexts.append({ + "camera": camera, + "idx": cam_idx, + "id": "cam" + str(cam_idx), + "reqs-queued": 0, + "reqs-completed": 0, + "opt-capture": args.capture.get(cam_idx, False), + "opt-crc": args.crc.get(cam_idx, False), + "opt-save-frames": args.save_frames.get(cam_idx, False), + "opt-metadata": args.metadata.get(cam_idx, False), + "opt-strict-formats": args.strict_formats.get(cam_idx, False), + "opt-stream": args.stream.get(cam_idx, ["role=viewfinder"]), + }) + + for ctx in contexts: + print("Using camera {} as {}".format(ctx["camera"].id, ctx["id"])) + + for ctx in contexts: + if args.list_properties: + do_cmd_list_props(ctx) + if args.list_controls: + do_cmd_list_controls(ctx) + if args.info: + do_cmd_info(ctx) + + if args.capture: + + state = { + "cm": cm, + "contexts": contexts, + "event_handler": event_handler, + "request_prcessed": request_prcessed, + } + + if args.renderer == "null": + import cam_null + renderer = cam_null.NullRenderer(state) + elif args.renderer == "kms": + import cam_kms + renderer = cam_kms.KMSRenderer(state) + elif args.renderer == "qt": + import cam_qt + renderer = cam_qt.QtRenderer(state) + elif args.renderer == "qtgl": + import cam_qtgl + renderer = cam_qtgl.QtRenderer(state) + else: + print("Bad renderer", args.renderer) + return -1 + + state["renderer"] = renderer + + do_cmd_capture(state) + + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/src/py/cam/cam_kms.py b/src/py/cam/cam_kms.py new file mode 100644 index 00000000..ee9fe6c7 --- /dev/null +++ b/src/py/cam/cam_kms.py @@ -0,0 +1,183 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +# Copyright (C) 2021, Tomi Valkeinen + +import pykms +import selectors +import sys + +FMT_MAP = { + "RGB888": pykms.PixelFormat.RGB888, + "YUYV": pykms.PixelFormat.YUYV, + "ARGB8888": pykms.PixelFormat.ARGB8888, + "XRGB8888": pykms.PixelFormat.XRGB8888, +} + + +class KMSRenderer: + def __init__(self, state): + self.state = state + + self.cm = state["cm"] + self.contexts = state["contexts"] + self.running = False + + card = pykms.Card() + + res = pykms.ResourceManager(card) + conn = res.reserve_connector() + crtc = res.reserve_crtc(conn) + mode = conn.get_default_mode() + modeb = mode.to_blob(card) + + req = pykms.AtomicReq(card) + req.add_connector(conn, crtc) + req.add_crtc(crtc, modeb) + r = req.commit_sync(allow_modeset=True) + assert(r == 0) + + self.card = card + self.resman = res + self.crtc = crtc + self.mode = mode + + self.bufqueue = [] + self.current = None + self.next = None + self.cam_2_drm = {} + + # KMS + + def close(self): + req = pykms.AtomicReq(self.card) + for s in self.streams: + req.add_plane(s["plane"], None, None, dst=(0, 0, 0, 0)) + req.commit() + + def add_plane(self, req, stream, fb): + s = next(s for s in self.streams if s["stream"] == stream) + idx = s["idx"] + plane = s["plane"] + + if idx % 2 == 0: + x = 0 + else: + x = self.mode.hdisplay - fb.width + + if idx // 2 == 0: + y = 0 + else: + y = self.mode.vdisplay - fb.height + + req.add_plane(plane, fb, self.crtc, dst=(x, y, fb.width, fb.height)) + + def apply_request(self, drmreq): + + buffers = drmreq["camreq"].buffers + + for stream, fb in buffers.items(): + drmfb = self.cam_2_drm.get(fb, None) + + req = pykms.AtomicReq(self.card) + self.add_plane(req, stream, drmfb) + req.commit() + + 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: + drmreq = self.next + + self.apply_request(drmreq) + + if old: + req = old["camreq"] + ctx = old["camctx"] + self.state["request_prcessed"](ctx, req) + + def queue(self, drmreq): + if not self.next: + self.next = drmreq + self.apply_request(drmreq) + else: + self.bufqueue.append(drmreq) + + # libcamera + + def setup(self): + self.streams = [] + + idx = 0 + for ctx in self.contexts: + for stream in ctx["streams"]: + + cfg = stream.configuration + fmt = cfg.pixelFormat + fmt = FMT_MAP[fmt] + + plane = self.resman.reserve_generic_plane(self.crtc, fmt) + assert(plane is not None) + + self.streams.append({ + "idx": idx, + "stream": stream, + "plane": plane, + "fmt": fmt, + "size": cfg.size, + }) + + for fb in ctx["allocator"].buffers(stream): + w, h = cfg.size + stride = cfg.stride + fd = fb.fd(0) + drmfb = pykms.DmabufFramebuffer(self.card, w, h, fmt, + [fd], [stride], [0]) + self.cam_2_drm[fb] = drmfb + + idx += 1 + + def readdrm(self, fileobj): + for ev in self.card.read_events(): + if ev.type == pykms.DrmEventType.FLIP_COMPLETE: + self.handle_page_flip(ev.seq, ev.time) + + def readcam(self, fd): + self.running = self.state["event_handler"](self.state) + + def readkey(self, fileobj): + sys.stdin.readline() + self.running = False + + def run(self): + print("Capturing...") + + self.running = True + + sel = selectors.DefaultSelector() + sel.register(self.card.fd, selectors.EVENT_READ, self.readdrm) + sel.register(self.cm.efd, selectors.EVENT_READ, self.readcam) + sel.register(sys.stdin, selectors.EVENT_READ, self.readkey) + + print("Press enter to exit") + + while self.running: + events = sel.select() + for key, mask in events: + callback = key.data + callback(key.fileobj) + + print("Exiting...") + + def request_handler(self, ctx, req): + + drmreq = { + "camctx": ctx, + "camreq": req, + } + + self.queue(drmreq) diff --git a/src/py/cam/cam_null.py b/src/py/cam/cam_null.py new file mode 100644 index 00000000..f6e30835 --- /dev/null +++ b/src/py/cam/cam_null.py @@ -0,0 +1,47 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +# Copyright (C) 2021, Tomi Valkeinen + +import selectors +import sys + + +class NullRenderer: + def __init__(self, state): + self.state = state + + self.cm = state["cm"] + self.contexts = state["contexts"] + + self.running = False + + def setup(self): + pass + + def run(self): + print("Capturing...") + + self.running = True + + sel = selectors.DefaultSelector() + sel.register(self.cm.efd, selectors.EVENT_READ, self.readcam) + sel.register(sys.stdin, selectors.EVENT_READ, self.readkey) + + print("Press enter to exit") + + while self.running: + events = sel.select() + for key, mask in events: + callback = key.data + callback(key.fileobj) + + print("Exiting...") + + def readcam(self, fd): + self.running = self.state["event_handler"](self.state) + + def readkey(self, fileobj): + sys.stdin.readline() + self.running = False + + def request_handler(self, ctx, req): + self.state["request_prcessed"](ctx, req) diff --git a/src/py/cam/cam_qt.py b/src/py/cam/cam_qt.py new file mode 100644 index 00000000..30fb7a1d --- /dev/null +++ b/src/py/cam/cam_qt.py @@ -0,0 +1,354 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +# Copyright (C) 2021, Tomi Valkeinen +# +# Debayering code from PiCamera documentation + +from io import BytesIO +from numpy.lib.stride_tricks import as_strided +from PIL import Image +from PIL.ImageQt import ImageQt +from PyQt5 import QtCore, QtGui, QtWidgets +import numpy as np +import sys + + +def rgb_to_pix(rgb): + img = Image.frombuffer("RGB", (rgb.shape[1], rgb.shape[0]), rgb) + qim = ImageQt(img).copy() + pix = QtGui.QPixmap.fromImage(qim) + return pix + + +def separate_components(data, r0, g0, g1, b0): + # Now to split the data up into its red, green, and blue components. The + # Bayer pattern of the OV5647 sensor is BGGR. In other words the first + # row contains alternating green/blue elements, the second row contains + # alternating red/green elements, and so on as illustrated below: + # + # GBGBGBGBGBGBGB + # RGRGRGRGRGRGRG + # GBGBGBGBGBGBGB + # RGRGRGRGRGRGRG + # + # Please note that if you use vflip or hflip to change the orientation + # of the capture, you must flip the Bayer pattern accordingly + + rgb = np.zeros(data.shape + (3,), dtype=data.dtype) + rgb[r0[1]::2, r0[0]::2, 0] = data[r0[1]::2, r0[0]::2] # Red + rgb[g0[1]::2, g0[0]::2, 1] = data[g0[1]::2, g0[0]::2] # Green + rgb[g1[1]::2, g1[0]::2, 1] = data[g1[1]::2, g1[0]::2] # Green + rgb[b0[1]::2, b0[0]::2, 2] = data[b0[1]::2, b0[0]::2] # Blue + + return rgb + + +def demosaic(rgb, r0, g0, g1, b0): + # At this point we now have the raw Bayer data with the correct values + # and colors but the data still requires de-mosaicing and + # post-processing. If you wish to do this yourself, end the script here! + # + # Below we present a fairly naive de-mosaic method that simply + # calculates the weighted average of a pixel based on the pixels + # surrounding it. The weighting is provided b0[1] a b0[1]te representation of + # the Bayer filter which we construct first: + + bayer = np.zeros(rgb.shape, dtype=np.uint8) + bayer[r0[1]::2, r0[0]::2, 0] = 1 # Red + bayer[g0[1]::2, g0[0]::2, 1] = 1 # Green + bayer[g1[1]::2, g1[0]::2, 1] = 1 # Green + bayer[b0[1]::2, b0[0]::2, 2] = 1 # Blue + + # Allocate an array to hold our output with the same shape as the input + # data. After this we define the size of window that will be used to + # calculate each weighted average (3x3). Then we pad out the rgb and + # bayer arrays, adding blank pixels at their edges to compensate for the + # size of the window when calculating averages for edge pixels. + + output = np.empty(rgb.shape, dtype=rgb.dtype) + window = (3, 3) + borders = (window[0] - 1, window[1] - 1) + border = (borders[0] // 2, borders[1] // 2) + + # rgb_pad = np.zeros(( + # rgb.shape[0] + borders[0], + # rgb.shape[1] + borders[1], + # rgb.shape[2]), dtype=rgb.dtype) + # rgb_pad[ + # border[0]:rgb_pad.shape[0] - border[0], + # border[1]:rgb_pad.shape[1] - border[1], + # :] = rgb + # rgb = rgb_pad + # + # bayer_pad = np.zeros(( + # bayer.shape[0] + borders[0], + # bayer.shape[1] + borders[1], + # bayer.shape[2]), dtype=bayer.dtype) + # bayer_pad[ + # border[0]:bayer_pad.shape[0] - border[0], + # border[1]:bayer_pad.shape[1] - border[1], + # :] = bayer + # bayer = bayer_pad + + # In numpy >=1.7.0 just use np.pad (version in Raspbian is 1.6.2 at the + # time of writing...) + # + rgb = np.pad(rgb, [ + (border[0], border[0]), + (border[1], border[1]), + (0, 0), + ], 'constant') + bayer = np.pad(bayer, [ + (border[0], border[0]), + (border[1], border[1]), + (0, 0), + ], 'constant') + + # For each plane in the RGB data, we use a nifty numpy trick + # (as_strided) to construct a view over the plane of 3x3 matrices. We do + # the same for the bayer array, then use Einstein summation on each + # (np.sum is simpler, but copies the data so it's slower), and divide + # the results to get our weighted average: + + for plane in range(3): + p = rgb[..., plane] + b = bayer[..., plane] + pview = as_strided(p, shape=( + p.shape[0] - borders[0], + p.shape[1] - borders[1]) + window, strides=p.strides * 2) + bview = as_strided(b, shape=( + b.shape[0] - borders[0], + b.shape[1] - borders[1]) + window, strides=b.strides * 2) + psum = np.einsum('ijkl->ij', pview) + bsum = np.einsum('ijkl->ij', bview) + output[..., plane] = psum // bsum + + return output + + +def to_rgb(fmt, size, data): + w = size[0] + h = size[1] + + if fmt == "YUYV": + # YUV422 + yuyv = data.reshape((h, w // 2 * 4)) + + # YUV444 + yuv = np.empty((h, w, 3), dtype=np.uint8) + yuv[:, :, 0] = yuyv[:, 0::2] # Y + yuv[:, :, 1] = yuyv[:, 1::4].repeat(2, axis=1) # U + yuv[:, :, 2] = yuyv[:, 3::4].repeat(2, axis=1) # V + + m = np.array([ + [ 1.0, 1.0, 1.0], + [-0.000007154783816076815, -0.3441331386566162, 1.7720025777816772], + [ 1.4019975662231445, -0.7141380310058594 , 0.00001542569043522235] + ]) + + rgb = np.dot(yuv, m) + rgb[:, :, 0] -= 179.45477266423404 + rgb[:, :, 1] += 135.45870971679688 + rgb[:, :, 2] -= 226.8183044444304 + rgb = rgb.astype(np.uint8) + + elif fmt == "RGB888": + rgb = data.reshape((h, w, 3)) + rgb[:, :, [0, 1, 2]] = rgb[:, :, [2, 1, 0]] + + elif fmt == "BGR888": + rgb = data.reshape((h, w, 3)) + + elif fmt in ["ARGB8888", "XRGB8888"]: + rgb = data.reshape((h, w, 4)) + rgb = np.flip(rgb, axis=2) + # drop alpha component + rgb = np.delete(rgb, np.s_[0::4], axis=2) + + elif fmt.startswith("S"): + bayer_pattern = fmt[1:5] + bitspp = int(fmt[5:]) + + # TODO: shifting leaves the lowest bits 0 + if bitspp == 8: + data = data.reshape((h, w)) + data = data.astype(np.uint16) << 8 + elif bitspp in [10, 12]: + data = data.view(np.uint16) + data = data.reshape((h, w)) + data = data << (16 - bitspp) + else: + raise Exception("Bad bitspp:" + str(bitspp)) + + idx = bayer_pattern.find("R") + assert(idx != -1) + r0 = (idx % 2, idx // 2) + + idx = bayer_pattern.find("G") + assert(idx != -1) + g0 = (idx % 2, idx // 2) + + idx = bayer_pattern.find("G", idx + 1) + assert(idx != -1) + g1 = (idx % 2, idx // 2) + + idx = bayer_pattern.find("B") + assert(idx != -1) + b0 = (idx % 2, idx // 2) + + rgb = separate_components(data, r0, g0, g1, b0) + rgb = demosaic(rgb, r0, g0, g1, b0) + rgb = (rgb >> 8).astype(np.uint8) + + else: + rgb = None + + return rgb + + +class QtRenderer: + def __init__(self, state): + self.state = state + + self.cm = state["cm"] + self.contexts = state["contexts"] + + def setup(self): + self.app = QtWidgets.QApplication([]) + + windows = [] + + for ctx in self.contexts: + camera = ctx["camera"] + + for stream in ctx["streams"]: + fmt = stream.configuration.pixelFormat + size = stream.configuration.size + + window = MainWindow(ctx, stream) + window.setAttribute(QtCore.Qt.WA_ShowWithoutActivating) + window.show() + windows.append(window) + + self.windows = windows + + def run(self): + camnotif = QtCore.QSocketNotifier(self.cm.efd, QtCore.QSocketNotifier.Read) + camnotif.activated.connect(lambda x: self.readcam()) + + keynotif = QtCore.QSocketNotifier(sys.stdin.fileno(), QtCore.QSocketNotifier.Read) + keynotif.activated.connect(lambda x: self.readkey()) + + print("Capturing...") + + self.app.exec() + + print("Exiting...") + + def readcam(self): + running = self.state["event_handler"](self.state) + + if not running: + self.app.quit() + + def readkey(self): + sys.stdin.readline() + self.app.quit() + + def request_handler(self, ctx, req): + buffers = req.buffers + + for stream, fb in buffers.items(): + wnd = next(wnd for wnd in self.windows if wnd.stream == stream) + + wnd.handle_request(stream, fb) + + self.state["request_prcessed"](ctx, req) + + def cleanup(self): + for w in self.windows: + w.close() + + +class MainWindow(QtWidgets.QWidget): + def __init__(self, ctx, stream): + super().__init__() + + self.ctx = ctx + self.stream = stream + + 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(ctx["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) + + camera = ctx["camera"] + + 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() + + def buf_to_qpixmap(self, stream, fb): + with fb.mmap(0) as b: + cfg = stream.configuration + w, h = cfg.size + pitch = cfg.stride + + if cfg.pixelFormat == "MJPEG": + img = Image.open(BytesIO(b)) + qim = ImageQt(img).copy() + pix = QtGui.QPixmap.fromImage(qim) + else: + data = np.array(b, dtype=np.uint8) + rgb = to_rgb(cfg.pixelFormat, cfg.size, data) + + if rgb is None: + raise Exception("Format not supported: " + cfg.pixelFormat) + + pix = rgb_to_pix(rgb) + + return pix + + def handle_request(self, stream, fb): + ctx = self.ctx + + pix = self.buf_to_qpixmap(stream, fb) + self.label.setPixmap(pix) + + self.frameLabel.setText("Queued: {}\nDone: {}\nFps: {:.2f}" + .format(ctx["reqs-queued"], ctx["reqs-completed"], ctx["fps"])) diff --git a/src/py/cam/cam_qtgl.py b/src/py/cam/cam_qtgl.py new file mode 100644 index 00000000..8f9ab457 --- /dev/null +++ b/src/py/cam/cam_qtgl.py @@ -0,0 +1,385 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +# Copyright (C) 2021, Tomi Valkeinen + +from PyQt5 import QtCore, QtWidgets +from PyQt5.QtCore import Qt + +import math +import numpy as np +import os +import sys + +os.environ["PYOPENGL_PLATFORM"] = "egl" + +import OpenGL +# OpenGL.FULL_LOGGING = True + +from OpenGL import GL as gl +from OpenGL.EGL.EXT.image_dma_buf_import import * +from OpenGL.EGL.KHR.image import * +from OpenGL.EGL.VERSION.EGL_1_0 import * +from OpenGL.EGL.VERSION.EGL_1_2 import * +from OpenGL.EGL.VERSION.EGL_1_3 import * + +from OpenGL.GLES2.OES.EGL_image import * +from OpenGL.GLES2.OES.EGL_image_external import * +from OpenGL.GLES2.VERSION.GLES2_2_0 import * +from OpenGL.GLES3.VERSION.GLES3_3_0 import * + +from OpenGL.GL import shaders + +from gl_helpers import * + +# libcamera format string -> DRM fourcc +FMT_MAP = { + "RGB888": "RG24", + "XRGB8888": "XR24", + "ARGB8888": "AR24", + "YUYV": "YUYV", +} + + +class EglState: + def __init__(self): + self.create_display() + self.choose_config() + self.create_context() + self.check_extensions() + + def create_display(self): + xdpy = getEGLNativeDisplay() + dpy = eglGetDisplay(xdpy) + self.display = dpy + + def choose_config(self): + dpy = self.display + + major, minor = EGLint(), EGLint() + + b = eglInitialize(dpy, major, minor) + assert(b) + + print("EGL {} {}".format( + eglQueryString(dpy, EGL_VENDOR).decode(), + eglQueryString(dpy, EGL_VERSION).decode())) + + check_egl_extensions(dpy, ["EGL_EXT_image_dma_buf_import"]) + + b = eglBindAPI(EGL_OPENGL_ES_API) + assert(b) + + def print_config(dpy, cfg): + + def _getconf(dpy, cfg, a): + value = ctypes.c_long() + eglGetConfigAttrib(dpy, cfg, a, value) + return value.value + + getconf = lambda a: _getconf(dpy, cfg, a) + + print("EGL Config {}: color buf {}/{}/{}/{} = {}, depth {}, stencil {}, native visualid {}, native visualtype {}".format( + getconf(EGL_CONFIG_ID), + getconf(EGL_ALPHA_SIZE), + getconf(EGL_RED_SIZE), + getconf(EGL_GREEN_SIZE), + getconf(EGL_BLUE_SIZE), + getconf(EGL_BUFFER_SIZE), + getconf(EGL_DEPTH_SIZE), + getconf(EGL_STENCIL_SIZE), + getconf(EGL_NATIVE_VISUAL_ID), + getconf(EGL_NATIVE_VISUAL_TYPE))) + + if False: + num_configs = ctypes.c_long() + eglGetConfigs(dpy, None, 0, num_configs) + print("{} configs".format(num_configs.value)) + + configs = (EGLConfig * num_configs.value)() + eglGetConfigs(dpy, configs, num_configs.value, num_configs) + for config_id in configs: + print_config(dpy, config_id) + + config_attribs = [ + EGL_SURFACE_TYPE, EGL_WINDOW_BIT, + EGL_RED_SIZE, 8, + EGL_GREEN_SIZE, 8, + EGL_BLUE_SIZE, 8, + EGL_ALPHA_SIZE, 0, + EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT, + EGL_NONE, + ] + + n = EGLint() + configs = (EGLConfig * 1)() + b = eglChooseConfig(dpy, config_attribs, configs, 1, n) + assert(b and n.value == 1) + config = configs[0] + + print("Chosen Config:") + print_config(dpy, config) + + self.config = config + + def create_context(self): + dpy = self.display + + context_attribs = [ + EGL_CONTEXT_CLIENT_VERSION, 2, + EGL_NONE, + ] + + context = eglCreateContext(dpy, self.config, EGL_NO_CONTEXT, context_attribs) + assert(context) + + b = eglMakeCurrent(dpy, EGL_NO_SURFACE, EGL_NO_SURFACE, context) + assert(b) + + self.context = context + + def check_extensions(self): + check_gl_extensions(["GL_OES_EGL_image"]) + + assert(eglCreateImageKHR) + assert(eglDestroyImageKHR) + assert(glEGLImageTargetTexture2DOES) + + +class QtRenderer: + def __init__(self, state): + self.state = state + + def setup(self): + self.app = QtWidgets.QApplication([]) + + window = MainWindow(self.state) + window.setAttribute(QtCore.Qt.WA_ShowWithoutActivating) + window.show() + + self.window = window + + def run(self): + camnotif = QtCore.QSocketNotifier(self.state["cm"].efd, QtCore.QSocketNotifier.Read) + camnotif.activated.connect(lambda x: self.readcam()) + + keynotif = QtCore.QSocketNotifier(sys.stdin.fileno(), QtCore.QSocketNotifier.Read) + keynotif.activated.connect(lambda x: self.readkey()) + + print("Capturing...") + + self.app.exec() + + print("Exiting...") + + def readcam(self): + running = self.state["event_handler"](self.state) + + if not running: + self.app.quit() + + def readkey(self): + sys.stdin.readline() + self.app.quit() + + def request_handler(self, ctx, req): + self.window.handle_request(ctx, req) + + def cleanup(self): + self.window.close() + + +class MainWindow(QtWidgets.QWidget): + def __init__(self, state): + super().__init__() + + self.setAttribute(Qt.WA_PaintOnScreen) + self.setAttribute(Qt.WA_NativeWindow) + + self.state = state + + self.textures = {} + self.reqqueue = {} + self.current = {} + + for ctx in self.state["contexts"]: + + self.reqqueue[ctx["idx"]] = [] + self.current[ctx["idx"]] = [] + + for stream in ctx["streams"]: + fmt = stream.configuration.pixelFormat + size = stream.configuration.size + + if fmt not in FMT_MAP: + raise Exception("Unsupported pixel format: " + str(fmt)) + + self.textures[stream] = None + + num_tiles = len(self.textures) + self.num_columns = math.ceil(math.sqrt(num_tiles)) + self.num_rows = math.ceil(num_tiles / self.num_columns) + + self.egl = EglState() + + self.surface = None + + def paintEngine(self): + return None + + def create_surface(self): + native_surface = c_void_p(self.winId().__int__()) + surface = eglCreateWindowSurface(self.egl.display, self.egl.config, + native_surface, None) + + b = eglMakeCurrent(self.egl.display, self.surface, self.surface, self.egl.context) + assert(b) + + self.surface = surface + + def init_gl(self): + self.create_surface() + + vertShaderSrc = """ + attribute vec2 aPosition; + varying vec2 texcoord; + + void main() + { + gl_Position = vec4(aPosition * 2.0 - 1.0, 0.0, 1.0); + texcoord.x = aPosition.x; + texcoord.y = 1.0 - aPosition.y; + } + """ + fragShaderSrc = """ + #extension GL_OES_EGL_image_external : enable + precision mediump float; + varying vec2 texcoord; + uniform samplerExternalOES texture; + + void main() + { + gl_FragColor = texture2D(texture, texcoord); + } + """ + + program = shaders.compileProgram( + shaders.compileShader(vertShaderSrc, GL_VERTEX_SHADER), + shaders.compileShader(fragShaderSrc, GL_FRAGMENT_SHADER) + ) + + glUseProgram(program) + + glClearColor(0.5, 0.8, 0.7, 1.0) + + vertPositions = [ + 0.0, 0.0, + 1.0, 0.0, + 1.0, 1.0, + 0.0, 1.0 + ] + + inputAttrib = glGetAttribLocation(program, "aPosition") + glVertexAttribPointer(inputAttrib, 2, GL_FLOAT, GL_FALSE, 0, vertPositions) + glEnableVertexAttribArray(inputAttrib) + + def create_texture(self, stream, fb): + cfg = stream.configuration + fmt = cfg.pixelFormat + fmt = str_to_fourcc(FMT_MAP[fmt]) + w, h = cfg.size + + attribs = [ + EGL_WIDTH, w, + EGL_HEIGHT, h, + EGL_LINUX_DRM_FOURCC_EXT, fmt, + EGL_DMA_BUF_PLANE0_FD_EXT, fb.fd(0), + EGL_DMA_BUF_PLANE0_OFFSET_EXT, 0, + EGL_DMA_BUF_PLANE0_PITCH_EXT, cfg.stride, + EGL_NONE, + ] + + image = eglCreateImageKHR(self.egl.display, + EGL_NO_CONTEXT, + EGL_LINUX_DMA_BUF_EXT, + None, + attribs) + assert(image) + + textures = glGenTextures(1) + glBindTexture(GL_TEXTURE_EXTERNAL_OES, textures) + glTexParameteri(GL_TEXTURE_EXTERNAL_OES, GL_TEXTURE_MAG_FILTER, GL_LINEAR) + glTexParameteri(GL_TEXTURE_EXTERNAL_OES, GL_TEXTURE_MIN_FILTER, GL_LINEAR) + glTexParameteri(GL_TEXTURE_EXTERNAL_OES, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE) + glTexParameteri(GL_TEXTURE_EXTERNAL_OES, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE) + glEGLImageTargetTexture2DOES(GL_TEXTURE_EXTERNAL_OES, image) + + return textures + + def resizeEvent(self, event): + size = event.size() + + print("Resize", size) + + super().resizeEvent(event) + + if self.surface is None: + return + + glViewport(0, 0, size.width() // 2, size.height()) + + def paintEvent(self, event): + if self.surface is None: + self.init_gl() + + for ctx_idx, queue in self.reqqueue.items(): + if len(queue) == 0: + continue + + ctx = next(ctx for ctx in self.state["contexts"] if ctx["idx"] == ctx_idx) + + if self.current[ctx_idx]: + old = self.current[ctx_idx] + self.current[ctx_idx] = None + self.state["request_prcessed"](ctx, old) + + next_req = queue.pop(0) + self.current[ctx_idx] = next_req + + stream, fb = next(iter(next_req.buffers.items())) + + self.textures[stream] = self.create_texture(stream, fb) + + self.paint_gl() + + def paint_gl(self): + b = eglMakeCurrent(self.egl.display, self.surface, self.surface, self.egl.context) + assert(b) + + glClear(GL_COLOR_BUFFER_BIT) + + size = self.size() + + for idx, ctx in enumerate(self.state["contexts"]): + for stream in ctx["streams"]: + if self.textures[stream] is None: + continue + + w = size.width() // self.num_columns + h = size.height() // self.num_rows + + x = idx % self.num_columns + y = idx // self.num_columns + + x *= w + y *= h + + glViewport(x, y, w, h) + + glBindTexture(GL_TEXTURE_EXTERNAL_OES, self.textures[stream]) + glDrawArrays(GL_TRIANGLE_FAN, 0, 4) + + b = eglSwapBuffers(self.egl.display, self.surface) + assert(b) + + def handle_request(self, ctx, req): + self.reqqueue[ctx["idx"]].append(req) + self.update() diff --git a/src/py/cam/gl_helpers.py b/src/py/cam/gl_helpers.py new file mode 100644 index 00000000..925901dd --- /dev/null +++ b/src/py/cam/gl_helpers.py @@ -0,0 +1,74 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +# Copyright (C) 2021, Tomi Valkeinen + +from OpenGL.EGL.VERSION.EGL_1_0 import EGLNativeDisplayType, eglGetProcAddress, eglQueryString, EGL_EXTENSIONS + +from OpenGL.raw.GLES2 import _types as _cs +from OpenGL.GLES2.VERSION.GLES2_2_0 import * +from OpenGL.GLES3.VERSION.GLES3_3_0 import * +from OpenGL import GL as gl + +from ctypes import c_int, c_char_p, c_void_p, cdll, POINTER, util, \ + pointer, CFUNCTYPE, c_bool + + +def getEGLNativeDisplay(): + _x11lib = cdll.LoadLibrary(util.find_library("X11")) + XOpenDisplay = _x11lib.XOpenDisplay + XOpenDisplay.argtypes = [c_char_p] + XOpenDisplay.restype = POINTER(EGLNativeDisplayType) + + xdpy = XOpenDisplay(None) + + +# Hack. PyOpenGL doesn't seem to manage to find glEGLImageTargetTexture2DOES. +def getglEGLImageTargetTexture2DOES(): + funcptr = eglGetProcAddress("glEGLImageTargetTexture2DOES") + prototype = CFUNCTYPE(None, _cs.GLenum, _cs.GLeglImageOES) + return prototype(funcptr) + + +glEGLImageTargetTexture2DOES = getglEGLImageTargetTexture2DOES() + + +def str_to_fourcc(str): + assert(len(str) == 4) + fourcc = 0 + for i, v in enumerate([ord(c) for c in str]): + fourcc |= v << (i * 8) + return fourcc + + +def get_gl_extensions(): + n = GLint() + glGetIntegerv(GL_NUM_EXTENSIONS, n) + gl_extensions = [] + for i in range(n.value): + gl_extensions.append(gl.glGetStringi(GL_EXTENSIONS, i).decode()) + return gl_extensions + + +def check_gl_extensions(required_extensions): + extensions = get_gl_extensions() + + if False: + print("GL EXTENSIONS: ", " ".join(extensions)) + + for ext in required_extensions: + if ext not in extensions: + raise Exception(ext + " missing") + + +def get_egl_extensions(egl_display): + return eglQueryString(egl_display, EGL_EXTENSIONS).decode().split(" ") + + +def check_egl_extensions(egl_display, required_extensions): + extensions = get_egl_extensions(egl_display) + + if False: + print("EGL EXTENSIONS: ", " ".join(extensions)) + + for ext in required_extensions: + if ext not in extensions: + raise Exception(ext + " missing") From patchwork Thu May 5 10:40:59 2022 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Tomi Valkeinen X-Patchwork-Id: 15791 Return-Path: X-Original-To: parsemail@patchwork.libcamera.org Delivered-To: parsemail@patchwork.libcamera.org Received: from lancelot.ideasonboard.com (lancelot.ideasonboard.com [92.243.16.209]) by patchwork.libcamera.org (Postfix) with ESMTPS id F04DDC0F2A for ; Thu, 5 May 2022 10:41:36 +0000 (UTC) Received: from lancelot.ideasonboard.com (localhost [IPv6:::1]) by lancelot.ideasonboard.com (Postfix) with ESMTP id 2FA4465658; Thu, 5 May 2022 12:41:35 +0200 (CEST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=libcamera.org; s=mail; t=1651747295; bh=PVAyCaitM3siiYv1k27wm6GStBa71P4ZGo3rLLegKjY=; h=To:Date:In-Reply-To:References:Subject:List-Id:List-Unsubscribe: List-Archive:List-Post:List-Help:List-Subscribe:From:Reply-To: From; b=iGDRsb5Y5O3D6kTTnHgBT9yvP1nFLUDrlvsja+nTywMY+c9gLRTbyNK1CNh+Ilr51 EZkls9U2unVGHiZbT+ZCAZ4Cm3nEe/3c8KrJVX2AoPc2VU+I011VXLxK1kOKx7cGe/ Xgbulhlj8ICsND5sPGZvOi4cFAVNfmHbtkLIlorR0hNpBVd3J2OIyaFp/pXBEpRpMm tw6rD87dACKUopyO5KPc7Z/ISL+FbhIeGZ5sjX/UMN9ycKtBVh6jq+CluInfjTrFbn PA536hn7LVQIGvi/JHHCFRYRLHIMxoF+3+q7QXjAonKLi9CEkBlfwVPt3b2W4JQHHG 4kHdSvk4s+tvg== Received: from perceval.ideasonboard.com (perceval.ideasonboard.com [213.167.242.64]) by lancelot.ideasonboard.com (Postfix) with ESMTPS id EBCA165658 for ; Thu, 5 May 2022 12:41:29 +0200 (CEST) Authentication-Results: lancelot.ideasonboard.com; dkim=pass (1024-bit key; unprotected) header.d=ideasonboard.com header.i=@ideasonboard.com header.b="hrJEj6PI"; dkim-atps=neutral Received: from deskari.lan (91-156-85-209.elisa-laajakaista.fi [91.156.85.209]) by perceval.ideasonboard.com (Postfix) with ESMTPSA id 5E5414A8; Thu, 5 May 2022 12:41:29 +0200 (CEST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=ideasonboard.com; s=mail; t=1651747289; bh=PVAyCaitM3siiYv1k27wm6GStBa71P4ZGo3rLLegKjY=; h=From:To:Cc:Subject:Date:In-Reply-To:References:From; b=hrJEj6PIlgG6oVEO5I3Lq1HZGCQ/ygE5SDfH9dyX4Xhkwm3X+tzqDs6+zkRS+JZAC 8nv/by9B5zVgzyYaGLtNzSqM3Mvm6/cMEAJW/Xkkbv4ElV8RSeSBasamDKMv6BhD/I cfhy338eRCxwFrIuGsJeqCKIRK9cXqFFOsbaS94Y= To: libcamera-devel@lists.libcamera.org, David Plowman , Kieran Bingham , Laurent Pinchart , Jacopo Mondi Date: Thu, 5 May 2022 13:40:59 +0300 Message-Id: <20220505104104.70841-9-tomi.valkeinen@ideasonboard.com> X-Mailer: git-send-email 2.34.1 In-Reply-To: <20220505104104.70841-1-tomi.valkeinen@ideasonboard.com> References: <20220505104104.70841-1-tomi.valkeinen@ideasonboard.com> MIME-Version: 1.0 Subject: [libcamera-devel] [PATCH v7 08/13] py: support setting array-controls X-BeenThere: libcamera-devel@lists.libcamera.org X-Mailman-Version: 2.1.29 Precedence: list List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , X-Patchwork-Original-From: Tomi Valkeinen via libcamera-devel From: Tomi Valkeinen Reply-To: Tomi Valkeinen Errors-To: libcamera-devel-bounces@lists.libcamera.org Sender: "libcamera-devel" Improve PyToControlValue() to convert python lists and tuples to a ControlValue with a Span<> value. Original version from David Plowman . Signed-off-by: Tomi Valkeinen Reviewed-by: Laurent Pinchart --- src/py/libcamera/pymain.cpp | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/src/py/libcamera/pymain.cpp b/src/py/libcamera/pymain.cpp index 81d48a20..db9d90ab 100644 --- a/src/py/libcamera/pymain.cpp +++ b/src/py/libcamera/pymain.cpp @@ -75,19 +75,30 @@ static py::object ControlValueToPy(const ControlValue &cv) } } +template +static ControlValue ControlValueMaybeArray(const py::object &ob) +{ + if (py::isinstance(ob) || py::isinstance(ob)) { + std::vector vec = ob.cast>(); + return ControlValue(Span(vec)); + } + + return ControlValue(ob.cast()); +} + static ControlValue PyToControlValue(const py::object &ob, ControlType type) { switch (type) { case ControlTypeBool: return ControlValue(ob.cast()); case ControlTypeByte: - return ControlValue(ob.cast()); + return ControlValueMaybeArray(ob); case ControlTypeInteger32: - return ControlValue(ob.cast()); + return ControlValueMaybeArray(ob); case ControlTypeInteger64: - return ControlValue(ob.cast()); + return ControlValueMaybeArray(ob); case ControlTypeFloat: - return ControlValue(ob.cast()); + return ControlValueMaybeArray(ob); case ControlTypeString: return ControlValue(ob.cast()); case ControlTypeRectangle: From patchwork Thu May 5 10:41:00 2022 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Tomi Valkeinen X-Patchwork-Id: 15792 Return-Path: X-Original-To: parsemail@patchwork.libcamera.org Delivered-To: parsemail@patchwork.libcamera.org Received: from lancelot.ideasonboard.com (lancelot.ideasonboard.com [92.243.16.209]) by patchwork.libcamera.org (Postfix) with ESMTPS id 60A4DC326F for ; Thu, 5 May 2022 10:41:37 +0000 (UTC) Received: from lancelot.ideasonboard.com (localhost [IPv6:::1]) by lancelot.ideasonboard.com (Postfix) with ESMTP id A94866566B; Thu, 5 May 2022 12:41:35 +0200 (CEST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=libcamera.org; s=mail; t=1651747295; bh=kKxfsicKBd8oRUdODrs8kZ8BAB5F+y6fI6wvRUvLZxY=; h=To:Date:In-Reply-To:References:Subject:List-Id:List-Unsubscribe: List-Archive:List-Post:List-Help:List-Subscribe:From:Reply-To: From; b=ncYFJtR6VWSK9s6aj2R1sJtsVpuy92aQ220g9u68X/115FBvU2VZ6V1xcJrypiuRS KYl7t28WYfQl5E2XEIW1qaNERvVg19jEkM79XwGN+E8gaXoaYqZNYXCpVvlXTOUMiW wkCrTIb1Guu9Ut+8gbyLkoMhN7XqmcrhUiudH59T+YOlzKYTkHoJbTKtN+blrSLYR3 ScIPbiKpSyuNTG9wTLWgz6D9aAexFcqsxB0lcj4+0OdXYNyMl94oMgNfawj4JVnxZn f1Dii4zVFEoBx4pkZ7jHADMzk5gWwSv1/ii1wifR28oQrJkAwpJ2QS6772+0lhF2st i/t8FGa9U/QnQ== Received: from perceval.ideasonboard.com (perceval.ideasonboard.com [213.167.242.64]) by lancelot.ideasonboard.com (Postfix) with ESMTPS id 761AA65657 for ; Thu, 5 May 2022 12:41:30 +0200 (CEST) Authentication-Results: lancelot.ideasonboard.com; dkim=pass (1024-bit key; unprotected) header.d=ideasonboard.com header.i=@ideasonboard.com header.b="g0xaLdyH"; dkim-atps=neutral Received: from deskari.lan (91-156-85-209.elisa-laajakaista.fi [91.156.85.209]) by perceval.ideasonboard.com (Postfix) with ESMTPSA id DD83AA46; Thu, 5 May 2022 12:41:29 +0200 (CEST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=ideasonboard.com; s=mail; t=1651747290; bh=kKxfsicKBd8oRUdODrs8kZ8BAB5F+y6fI6wvRUvLZxY=; h=From:To:Cc:Subject:Date:In-Reply-To:References:From; b=g0xaLdyH75SS15qLVvlVqsR7ZWJjhqm1cq+18TrJrweJqbb9KGqIqn1SrGN4Qkt10 tiS3NeSDGVrul3JZFbFZKXaNHE1PM/Kw/v82t7NMYxf71l/oDMQrxo+152Quc6veY8 isMcL4Ox84KHrAlVwVP0uIF5uhD5eeA+GQGRhnng= To: libcamera-devel@lists.libcamera.org, David Plowman , Kieran Bingham , Laurent Pinchart , Jacopo Mondi Date: Thu, 5 May 2022 13:41:00 +0300 Message-Id: <20220505104104.70841-10-tomi.valkeinen@ideasonboard.com> X-Mailer: git-send-email 2.34.1 In-Reply-To: <20220505104104.70841-1-tomi.valkeinen@ideasonboard.com> References: <20220505104104.70841-1-tomi.valkeinen@ideasonboard.com> MIME-Version: 1.0 Subject: [libcamera-devel] [PATCH v7 09/13] py: add Transform X-BeenThere: libcamera-devel@lists.libcamera.org X-Mailman-Version: 2.1.29 Precedence: list List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , X-Patchwork-Original-From: Tomi Valkeinen via libcamera-devel From: Tomi Valkeinen Reply-To: Tomi Valkeinen Errors-To: libcamera-devel-bounces@lists.libcamera.org Sender: "libcamera-devel" Add binding for Transform. C++'s Transform is an enum class, but we expose it as a Python class so that it can be easily manipulated. Original version from David Plowman . Signed-off-by: Tomi Valkeinen --- src/py/libcamera/pymain.cpp | 63 ++++++++++++++++++++++++++++++++++++- 1 file changed, 62 insertions(+), 1 deletion(-) diff --git a/src/py/libcamera/pymain.cpp b/src/py/libcamera/pymain.cpp index db9d90ab..3d2393ab 100644 --- a/src/py/libcamera/pymain.cpp +++ b/src/py/libcamera/pymain.cpp @@ -152,6 +152,7 @@ PYBIND11_MODULE(_libcamera, m) auto pyControlId = py::class_(m, "ControlId"); auto pyRequest = py::class_(m, "Request"); auto pyFrameMetadata = py::class_(m, "FrameMetadata"); + auto pyTransform = py::class_(m, "Transform"); /* Global functions */ m.def("logSetLevel", &logSetLevel); @@ -342,7 +343,8 @@ PYBIND11_MODULE(_libcamera, m) .def("validate", &CameraConfiguration::validate) .def("at", py::overload_cast(&CameraConfiguration::at), py::return_value_policy::reference_internal) .def_property_readonly("size", &CameraConfiguration::size) - .def_property_readonly("empty", &CameraConfiguration::empty); + .def_property_readonly("empty", &CameraConfiguration::empty) + .def_readwrite("transform", &CameraConfiguration::transform); pyStreamConfiguration .def("toString", &StreamConfiguration::toString) @@ -462,4 +464,63 @@ PYBIND11_MODULE(_libcamera, m) transform(self.planes().begin(), self.planes().end(), v.begin(), [](const auto &p) { return p.bytesused; }); return v; }); + + pyTransform + .def(py::init([](int rotation, bool hflip, bool vflip, bool transpose) { + bool ok; + + Transform t = transformFromRotation(rotation, &ok); + if (!ok) + throw std::runtime_error("Unable to create the transform"); + + if (hflip) + t ^= Transform::HFlip; + if (vflip) + t ^= Transform::VFlip; + if (transpose) + t ^= Transform::Transpose; + return t; + }), py::arg("rotation") = 0, py::arg("hflip") = false, + py::arg("vflip") = false, py::arg("transpose") = false) + .def(py::init([](Transform &other) { return other; })) + .def("__repr__", [](Transform &self) { + return ""; + }) + .def_property("hflip", + [](Transform &self) { + return !!(self & Transform::HFlip); + }, + [](Transform &self, bool hflip) { + if (hflip) + self |= Transform::HFlip; + else + self &= ~Transform::HFlip; + }) + .def_property("vflip", + [](Transform &self) { + return !!(self & Transform::VFlip); + }, + [](Transform &self, bool vflip) { + if (vflip) + self |= Transform::VFlip; + else + self &= ~Transform::VFlip; + }) + .def_property("transpose", + [](Transform &self) { + return !!(self & Transform::Transpose); + }, + [](Transform &self, bool transpose) { + if (transpose) + self |= Transform::Transpose; + else + self &= ~Transform::Transpose; + }) + .def("inverse", [](Transform &self) { return -self; }) + .def("invert", [](Transform &self) { + self = -self; + }) + .def("compose", [](Transform &self, Transform &other) { + self = self * other; + }); } From patchwork Thu May 5 10:41:01 2022 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Tomi Valkeinen X-Patchwork-Id: 15793 Return-Path: X-Original-To: parsemail@patchwork.libcamera.org Delivered-To: parsemail@patchwork.libcamera.org Received: from lancelot.ideasonboard.com (lancelot.ideasonboard.com [92.243.16.209]) by patchwork.libcamera.org (Postfix) with ESMTPS id 9DED9C3270 for ; Thu, 5 May 2022 10:41:37 +0000 (UTC) Received: from lancelot.ideasonboard.com (localhost [IPv6:::1]) by lancelot.ideasonboard.com (Postfix) with ESMTP id 46B3E6566E; Thu, 5 May 2022 12:41:36 +0200 (CEST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=libcamera.org; s=mail; t=1651747296; bh=h080XrRzIOw87QZOXhK9AAjRy/YihUNOM7v9sFqPMtk=; h=To:Date:In-Reply-To:References:Subject:List-Id:List-Unsubscribe: List-Archive:List-Post:List-Help:List-Subscribe:From:Reply-To: From; b=yHfOmOXpfuVpbNDZ+6Orxwgp4eW4VsO7pH1Zs9jDSXVYH03o0uULM8wW4RNXrbetu F+5fam5FcOf4yBiAbCNhw2SGt4RLBJdcMARxkcYsxVkVfwgEBZMHriZQ7XOHPtiX2k cx87Y238X8OttYM8QYWmzX5ghfbxAYPKTqpCecKpn3XKTD1cYmQ03+gUBez3048sgu y8z8SXogZvBdL26CWK9UtBFP/T6RnVgpF/5/BMf2ERhfbGqjGqCzR6xdtMd32zNmVf IHCjlec205TVBgh19cP4d0jey6a9/KN5JVjb0am63B8nYn6JGHKFqD++1j4D5d3PG9 rsdhSpvrMHWTA== Received: from perceval.ideasonboard.com (perceval.ideasonboard.com [IPv6:2001:4b98:dc2:55:216:3eff:fef7:d647]) by lancelot.ideasonboard.com (Postfix) with ESMTPS id F250D65660 for ; Thu, 5 May 2022 12:41:30 +0200 (CEST) Authentication-Results: lancelot.ideasonboard.com; dkim=pass (1024-bit key; unprotected) header.d=ideasonboard.com header.i=@ideasonboard.com header.b="XOrYxQL3"; dkim-atps=neutral Received: from deskari.lan (91-156-85-209.elisa-laajakaista.fi [91.156.85.209]) by perceval.ideasonboard.com (Postfix) with ESMTPSA id 6B2B5A58; Thu, 5 May 2022 12:41:30 +0200 (CEST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=ideasonboard.com; s=mail; t=1651747290; bh=h080XrRzIOw87QZOXhK9AAjRy/YihUNOM7v9sFqPMtk=; h=From:To:Cc:Subject:Date:In-Reply-To:References:From; b=XOrYxQL3KpJBrFDvOqVjmOP8k7fhGNCRGjVHcTKCDNsLnqbY+G6O715A+DOcQ0R9b KySe43fpbVXNIwKVBHC6hmbn6akn3b9wHhJQw/dh1tZ2o/0P1VB3OUHXMzdiffih6v 756B8Bv2fQPl3MLqtdIby11U/pCcOcnbK40j7w7M= To: libcamera-devel@lists.libcamera.org, David Plowman , Kieran Bingham , Laurent Pinchart , Jacopo Mondi Date: Thu, 5 May 2022 13:41:01 +0300 Message-Id: <20220505104104.70841-11-tomi.valkeinen@ideasonboard.com> X-Mailer: git-send-email 2.34.1 In-Reply-To: <20220505104104.70841-1-tomi.valkeinen@ideasonboard.com> References: <20220505104104.70841-1-tomi.valkeinen@ideasonboard.com> MIME-Version: 1.0 Subject: [libcamera-devel] [PATCH v7 10/13] py: add support for the ColorSpace X-BeenThere: libcamera-devel@lists.libcamera.org X-Mailman-Version: 2.1.29 Precedence: list List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , X-Patchwork-Original-From: Tomi Valkeinen via libcamera-devel From: Tomi Valkeinen Reply-To: Tomi Valkeinen Errors-To: libcamera-devel-bounces@lists.libcamera.org Sender: "libcamera-devel" From: David Plowman Add binding for ColorSpace. Original version by David Plowman . Signed-off-by: Tomi Valkeinen Reviewed-by: Laurent Pinchart --- src/py/libcamera/pymain.cpp | 52 ++++++++++++++++++++++++++++++++++++- 1 file changed, 51 insertions(+), 1 deletion(-) diff --git a/src/py/libcamera/pymain.cpp b/src/py/libcamera/pymain.cpp index 3d2393ab..c442ad50 100644 --- a/src/py/libcamera/pymain.cpp +++ b/src/py/libcamera/pymain.cpp @@ -153,6 +153,11 @@ PYBIND11_MODULE(_libcamera, m) auto pyRequest = py::class_(m, "Request"); auto pyFrameMetadata = py::class_(m, "FrameMetadata"); auto pyTransform = py::class_(m, "Transform"); + auto pyColorSpace = py::class_(m, "ColorSpace"); + auto pyColorSpacePrimaries = py::enum_(pyColorSpace, "Primaries"); + auto pyColorSpaceTransferFunction = py::enum_(pyColorSpace, "TransferFunction"); + auto pyColorSpaceYcbcrEncoding = py::enum_(pyColorSpace, "YcbcrEncoding"); + auto pyColorSpaceRange = py::enum_(pyColorSpace, "Range"); /* Global functions */ m.def("logSetLevel", &logSetLevel); @@ -360,7 +365,8 @@ PYBIND11_MODULE(_libcamera, m) .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); + .def_property_readonly("formats", &StreamConfiguration::formats, py::return_value_policy::reference_internal) + .def_readwrite("colorSpace", &StreamConfiguration::colorSpace); pyStreamFormats .def_property_readonly("pixelFormats", [](StreamFormats &self) { @@ -523,4 +529,48 @@ PYBIND11_MODULE(_libcamera, m) .def("compose", [](Transform &self, Transform &other) { self = self * other; }); + + pyColorSpace + .def(py::init([](ColorSpace::Primaries primaries, + ColorSpace::TransferFunction transferFunction, + ColorSpace::YcbcrEncoding ycbcrEncoding, + ColorSpace::Range range) { + return ColorSpace(primaries, transferFunction, ycbcrEncoding, range); + }), py::arg("primaries"), py::arg("transferFunction"), + py::arg("ycbcrEncoding"), py::arg("range")) + .def(py::init([](ColorSpace &other) { return other; })) + .def("__repr__", [](ColorSpace &self) { + return ""; + }) + .def_readwrite("primaries", &ColorSpace::primaries) + .def_readwrite("transferFunction", &ColorSpace::transferFunction) + .def_readwrite("ycbcrEncoding", &ColorSpace::ycbcrEncoding) + .def_readwrite("range", &ColorSpace::range) + .def_static("Raw", []() { return ColorSpace::Raw; }) + .def_static("Jpeg", []() { return ColorSpace::Jpeg; }) + .def_static("Srgb", []() { return ColorSpace::Srgb; }) + .def_static("Smpte170m", []() { return ColorSpace::Smpte170m; }) + .def_static("Rec709", []() { return ColorSpace::Rec709; }) + .def_static("Rec2020", []() { return ColorSpace::Rec2020; }); + + pyColorSpacePrimaries + .value("Raw", ColorSpace::Primaries::Raw) + .value("Smpte170m", ColorSpace::Primaries::Smpte170m) + .value("Rec709", ColorSpace::Primaries::Rec709) + .value("Rec2020", ColorSpace::Primaries::Rec2020); + + pyColorSpaceTransferFunction + .value("Linear", ColorSpace::TransferFunction::Linear) + .value("Srgb", ColorSpace::TransferFunction::Srgb) + .value("Rec709", ColorSpace::TransferFunction::Rec709); + + pyColorSpaceYcbcrEncoding + .value("Null", ColorSpace::YcbcrEncoding::None) + .value("Rec601", ColorSpace::YcbcrEncoding::Rec601) + .value("Rec709", ColorSpace::YcbcrEncoding::Rec709) + .value("Rec2020", ColorSpace::YcbcrEncoding::Rec2020); + + pyColorSpaceRange + .value("Full", ColorSpace::Range::Full) + .value("Limited", ColorSpace::Range::Limited); } From patchwork Thu May 5 10:41:02 2022 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Tomi Valkeinen X-Patchwork-Id: 15794 Return-Path: X-Original-To: parsemail@patchwork.libcamera.org Delivered-To: parsemail@patchwork.libcamera.org Received: from lancelot.ideasonboard.com (lancelot.ideasonboard.com [92.243.16.209]) by patchwork.libcamera.org (Postfix) with ESMTPS id 17081C3271 for ; Thu, 5 May 2022 10:41:38 +0000 (UTC) Received: from lancelot.ideasonboard.com (localhost [IPv6:::1]) by lancelot.ideasonboard.com (Postfix) with ESMTP id 65A636566F; Thu, 5 May 2022 12:41:37 +0200 (CEST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=libcamera.org; s=mail; t=1651747297; bh=na9LqJT9Kll4y+N+bUCkl2veymqhxZf7LnOIYxMG3tE=; h=To:Date:In-Reply-To:References:Subject:List-Id:List-Unsubscribe: List-Archive:List-Post:List-Help:List-Subscribe:From:Reply-To: From; b=oDluzx7eH51RY7+IgMbjRBcKd9vV0T7Po5EUdL6ECjxTVrAlpbIL8B8RLu9ZuBCxC UIvAZ4MgFIdsccrxHguaDUJn/5/CliYtwPBrEWfVSdYN7BY1YxKt3dGqtROqCrzAtb Z06ruNuXlGU1O0hvbfF5gs5xWMwmAY+UKCdJKF6bzFWYE40CIUuoJCEqF3yeIGGCDb ywWdv6iIM9pfskLnM17reF7A2+xS6huA0rZMq0Gj78CGkHXgDFAUOyiRhddSOUQmTe EXNaTWiuWQHVdF2db1UWAw+EcjURKA0nEDOqoz0sEJY4rkmBl2XidjutID0tgfDnH4 mNHXKEn2vAEhQ== Received: from perceval.ideasonboard.com (perceval.ideasonboard.com [IPv6:2001:4b98:dc2:55:216:3eff:fef7:d647]) by lancelot.ideasonboard.com (Postfix) with ESMTPS id 8C22560424 for ; Thu, 5 May 2022 12:41:31 +0200 (CEST) Authentication-Results: lancelot.ideasonboard.com; dkim=pass (1024-bit key; unprotected) header.d=ideasonboard.com header.i=@ideasonboard.com header.b="tq+jH4ho"; dkim-atps=neutral Received: from deskari.lan (91-156-85-209.elisa-laajakaista.fi [91.156.85.209]) by perceval.ideasonboard.com (Postfix) with ESMTPSA id ED9B4492; Thu, 5 May 2022 12:41:30 +0200 (CEST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=ideasonboard.com; s=mail; t=1651747291; bh=na9LqJT9Kll4y+N+bUCkl2veymqhxZf7LnOIYxMG3tE=; h=From:To:Cc:Subject:Date:In-Reply-To:References:From; b=tq+jH4hoHbGD8fOfI0iqpkIN7en9Yj/rJ4Dfoj2x0ld3j0oVY8v02Di7h764l2XFZ u8OURpVZcuY5s8Aa7d1fjrfZKMo23kBgyJ5XCy7jF0U/VmTDW6/GRveKkfCg1I2kjZ pVzN+a0Hf5wugKLcQSc+NUyh0j/jSAb61+ZMl8+U= To: libcamera-devel@lists.libcamera.org, David Plowman , Kieran Bingham , Laurent Pinchart , Jacopo Mondi Date: Thu, 5 May 2022 13:41:02 +0300 Message-Id: <20220505104104.70841-12-tomi.valkeinen@ideasonboard.com> X-Mailer: git-send-email 2.34.1 In-Reply-To: <20220505104104.70841-1-tomi.valkeinen@ideasonboard.com> References: <20220505104104.70841-1-tomi.valkeinen@ideasonboard.com> MIME-Version: 1.0 Subject: [libcamera-devel] [PATCH v7 11/13] py: support controls in the start method X-BeenThere: libcamera-devel@lists.libcamera.org X-Mailman-Version: 2.1.29 Precedence: list List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , X-Patchwork-Original-From: Tomi Valkeinen via libcamera-devel From: Tomi Valkeinen Reply-To: Tomi Valkeinen Errors-To: libcamera-devel-bounces@lists.libcamera.org Sender: "libcamera-devel" From: David Plowman Libcamera has for a while allowed controls to be set in the camera's start method. The Python bindings need to permit this too. Signed-off-by: David Plowman Signed-off-by: Tomi Valkeinen Reviewed-by: Laurent Pinchart --- src/py/libcamera/pymain.cpp | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/src/py/libcamera/pymain.cpp b/src/py/libcamera/pymain.cpp index c442ad50..6375e326 100644 --- a/src/py/libcamera/pymain.cpp +++ b/src/py/libcamera/pymain.cpp @@ -250,15 +250,34 @@ PYBIND11_MODULE(_libcamera, m) .def_property_readonly("id", &Camera::id) .def("acquire", &Camera::acquire) .def("release", &Camera::release) - .def("start", [](Camera &self) { + .def("start", [](Camera &self, py::dict controls) { self.requestCompleted.connect(handleRequestCompleted); - int ret = self.start(); + const ControlInfoMap &controlMap = self.controls(); + ControlList controlList(controlMap); + for (const auto& [hkey, hval]: controls) { + auto key = hkey.cast(); + + auto it = find_if(controlMap.begin(), controlMap.end(), + [&key](const auto &kvp) { + return kvp.first->name() == key; }); + + if (it == controlMap.end()) + throw runtime_error("Control " + key + " not found"); + + const auto &id = it->first; + auto obj = py::cast(hval); + + controlList.set(id->id(), PyToControlValue(obj, id->type())); + } + + int ret = self.start(&controlList); if (ret) self.requestCompleted.disconnect(handleRequestCompleted); return ret; - }) + }, + py::arg("controls") = py::dict()) .def("stop", [](Camera &self) { int ret = self.stop(); From patchwork Thu May 5 10:41:03 2022 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Tomi Valkeinen X-Patchwork-Id: 15795 Return-Path: X-Original-To: parsemail@patchwork.libcamera.org Delivered-To: parsemail@patchwork.libcamera.org Received: from lancelot.ideasonboard.com (lancelot.ideasonboard.com [92.243.16.209]) by patchwork.libcamera.org (Postfix) with ESMTPS id 500DBC326C for ; Thu, 5 May 2022 10:41:39 +0000 (UTC) Received: from lancelot.ideasonboard.com (localhost [IPv6:::1]) by lancelot.ideasonboard.com (Postfix) with ESMTP id 041066563F; Thu, 5 May 2022 12:41:39 +0200 (CEST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=libcamera.org; s=mail; t=1651747299; bh=7ewMQMZf89XHV6ZaBIDyoZUBi1MJitfvIQ3YDvgenug=; h=To:Date:In-Reply-To:References:Subject:List-Id:List-Unsubscribe: List-Archive:List-Post:List-Help:List-Subscribe:From:Reply-To: From; b=I3W911YCnBSHtIokLsXBiVNRQAC0GVk0ogtaCCqAI/9M7RpwTHp4klXKLGJ+/WCrU Sj/FtENsHEBm9RCk71wAmUd/sx2DUgm698R/rAKt1eycsd9wQePS2Zymgx0j3u4yul RjJxVJMXWPSWzT1PA9Gk2ePkN6KBcoNYSKqrtcUSAGolssnldeFEyC4oK9V7mRfvKv 2VPZIJF5KEIoR2VoMEAqtfQPKOC/hd4A8YWekixF32yMPrt1MpM5ZpQ8/+hLEov8eY rhuZ9R4qfvgIn+fm/WdsbTtDo1fCYyjOb/EZmMJMmBLCrTvtKYyIPHWqVZ1xfShQXi 2S5sZcIeKEUYQ== Received: from perceval.ideasonboard.com (perceval.ideasonboard.com [IPv6:2001:4b98:dc2:55:216:3eff:fef7:d647]) by lancelot.ideasonboard.com (Postfix) with ESMTPS id 1344865645 for ; Thu, 5 May 2022 12:41:32 +0200 (CEST) Authentication-Results: lancelot.ideasonboard.com; dkim=pass (1024-bit key; unprotected) header.d=ideasonboard.com header.i=@ideasonboard.com header.b="qvqHBVQM"; dkim-atps=neutral Received: from deskari.lan (91-156-85-209.elisa-laajakaista.fi [91.156.85.209]) by perceval.ideasonboard.com (Postfix) with ESMTPSA id 80B49D9D; Thu, 5 May 2022 12:41:31 +0200 (CEST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=ideasonboard.com; s=mail; t=1651747291; bh=7ewMQMZf89XHV6ZaBIDyoZUBi1MJitfvIQ3YDvgenug=; h=From:To:Cc:Subject:Date:In-Reply-To:References:From; b=qvqHBVQMiVC+VrfVFRCdyiLGGdh6a9Q5+B5X7b1fMGhs44W+G9B9i8GPNDPWnTVJ4 YmPlM5IPBFsbPGR7ooT2LVCZjmMQOZYuz/tn8u2P2IW/0sK+/12+h3boFyTiWN8A6C ZneTvPjKn/LUxH/iWUUMgqt9zvPyxuBqzluJOYXE= To: libcamera-devel@lists.libcamera.org, David Plowman , Kieran Bingham , Laurent Pinchart , Jacopo Mondi Date: Thu, 5 May 2022 13:41:03 +0300 Message-Id: <20220505104104.70841-13-tomi.valkeinen@ideasonboard.com> X-Mailer: git-send-email 2.34.1 In-Reply-To: <20220505104104.70841-1-tomi.valkeinen@ideasonboard.com> References: <20220505104104.70841-1-tomi.valkeinen@ideasonboard.com> MIME-Version: 1.0 Subject: [libcamera-devel] [PATCH v7 12/13] py: add support for setting Rectangle and Size controls X-BeenThere: libcamera-devel@lists.libcamera.org X-Mailman-Version: 2.1.29 Precedence: list List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , X-Patchwork-Original-From: Tomi Valkeinen via libcamera-devel From: Tomi Valkeinen Reply-To: Tomi Valkeinen Errors-To: libcamera-devel-bounces@lists.libcamera.org Sender: "libcamera-devel" From: David Plowman Allows, for example, ScalerCrop to be controlled for digital zoom. Signed-off-by: David Plowman Signed-off-by: Tomi Valkeinen Reviewed-by: Laurent Pinchart --- src/py/libcamera/pymain.cpp | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/py/libcamera/pymain.cpp b/src/py/libcamera/pymain.cpp index 6375e326..b9b52f6b 100644 --- a/src/py/libcamera/pymain.cpp +++ b/src/py/libcamera/pymain.cpp @@ -101,8 +101,14 @@ static ControlValue PyToControlValue(const py::object &ob, ControlType type) return ControlValueMaybeArray(ob); case ControlTypeString: return ControlValue(ob.cast()); - case ControlTypeRectangle: - case ControlTypeSize: + case ControlTypeRectangle: { + auto array = ob.cast>(); + return ControlValue(Rectangle(array[0], array[1], array[2], array[3])); + } + case ControlTypeSize: { + auto array = ob.cast>(); + return ControlValue(Size(array[0], array[1])); + } case ControlTypeNone: default: throw runtime_error("Control type not implemented"); From patchwork Thu May 5 10:41:04 2022 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Tomi Valkeinen X-Patchwork-Id: 15796 Return-Path: X-Original-To: parsemail@patchwork.libcamera.org Delivered-To: parsemail@patchwork.libcamera.org Received: from lancelot.ideasonboard.com (lancelot.ideasonboard.com [92.243.16.209]) by patchwork.libcamera.org (Postfix) with ESMTPS id D089CC3272 for ; Thu, 5 May 2022 10:41:39 +0000 (UTC) Received: from lancelot.ideasonboard.com (localhost [IPv6:::1]) by lancelot.ideasonboard.com (Postfix) with ESMTP id 73A4E6564A; Thu, 5 May 2022 12:41:39 +0200 (CEST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=libcamera.org; s=mail; t=1651747299; bh=pmogevaeNcSjdHQ7Hgy5H641piE912B+uxhkbOw+v34=; h=To:Date:In-Reply-To:References:Subject:List-Id:List-Unsubscribe: List-Archive:List-Post:List-Help:List-Subscribe:From:Reply-To: From; b=YnTP/T/aXaDhBGNbZWPFBUAgwsr0z5x23AsQEkRYXYnhGPdGNHTz169d43PDcl2vj 1gaCrMZChuwBJMYY7d0AX/S13aTqRlIMChACoG8G1pxWciHxoga+g/Nspbydl+88ec X94UiuFgeSQl9L5LGXFkTMICGLagmIENDTRRnKjzMa7uVc2jcJjMENd8nPzVaZpYOr +WGxGd56cuRkP7hjBTTuEsUFf7F/a6794v1s2y5k0/XTIqIldArzec7UJw+g955zes ckzteEx0WVI67iEM4Q5wFWEEHAvW2xabybG0jDMOXkPmc6TD0w1q9rhmpKfZiLXAw4 5b+FMNuNsmo+A== Received: from perceval.ideasonboard.com (perceval.ideasonboard.com [IPv6:2001:4b98:dc2:55:216:3eff:fef7:d647]) by lancelot.ideasonboard.com (Postfix) with ESMTPS id 9A2F865664 for ; Thu, 5 May 2022 12:41:32 +0200 (CEST) Authentication-Results: lancelot.ideasonboard.com; dkim=pass (1024-bit key; unprotected) header.d=ideasonboard.com header.i=@ideasonboard.com header.b="R47LJtFq"; dkim-atps=neutral Received: from deskari.lan (91-156-85-209.elisa-laajakaista.fi [91.156.85.209]) by perceval.ideasonboard.com (Postfix) with ESMTPSA id 0A80D4A8; Thu, 5 May 2022 12:41:31 +0200 (CEST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=ideasonboard.com; s=mail; t=1651747292; bh=pmogevaeNcSjdHQ7Hgy5H641piE912B+uxhkbOw+v34=; h=From:To:Cc:Subject:Date:In-Reply-To:References:From; b=R47LJtFqKkaxe4CnB8vC67c8c6HTABikZ2J9EKEP+cqF/YAeBb1FrjHl740TgH4Sj gdmxtRtNxDMaMnE7Be6I62Qx4UpRWy2v5KGLPCs5dCv56ZLYMDl+DOqGZqjrLGBE8/ 1NQxJRm4a4lAJNwRXOqWfgKSUsBtMVoCP9xa0BTE= To: libcamera-devel@lists.libcamera.org, David Plowman , Kieran Bingham , Laurent Pinchart , Jacopo Mondi Date: Thu, 5 May 2022 13:41:04 +0300 Message-Id: <20220505104104.70841-14-tomi.valkeinen@ideasonboard.com> X-Mailer: git-send-email 2.34.1 In-Reply-To: <20220505104104.70841-1-tomi.valkeinen@ideasonboard.com> References: <20220505104104.70841-1-tomi.valkeinen@ideasonboard.com> MIME-Version: 1.0 Subject: [libcamera-devel] [PATCH v7 13/13] py: implement MappedFrameBuffer X-BeenThere: libcamera-devel@lists.libcamera.org X-Mailman-Version: 2.1.29 Precedence: list List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , X-Patchwork-Original-From: Tomi Valkeinen via libcamera-devel From: Tomi Valkeinen Reply-To: Tomi Valkeinen Errors-To: libcamera-devel-bounces@lists.libcamera.org Sender: "libcamera-devel" Instead of just exposing plain mmap via fb.mmap(planenum), implement a MappedFrameBuffer class, similar to C++'s MappedFrameBuffer. MappedFrameBuffer mmaps the underlying filedescriptors and provides Python memoryviews for each plane. As an example, to save a Framebuffer to a file: with fb.mmap() as mfb: with open(filename, "wb") as f: for p in mfb.planes: f.write(p) The objects in mfb.planes are memoryviews that cover only the plane in question. Signed-off-by: Tomi Valkeinen Reviewed-by: Laurent Pinchart --- src/py/cam/cam.py | 11 +++--- src/py/cam/cam_qt.py | 6 +-- src/py/libcamera/__init__.py | 72 +++++++++++++++++++++++++++++++++++- src/py/libcamera/pymain.cpp | 7 ++++ 4 files changed, 86 insertions(+), 10 deletions(-) diff --git a/src/py/cam/cam.py b/src/py/cam/cam.py index 4efa6459..c0ebb186 100755 --- a/src/py/cam/cam.py +++ b/src/py/cam/cam.py @@ -329,9 +329,9 @@ def request_handler(state, ctx, req): crcs = [] if ctx["opt-crc"]: - with fb.mmap(0) as b: - crc = binascii.crc32(b) - crcs.append(crc) + with fb.mmap() as mfb: + plane_crcs = [binascii.crc32(p) for p in mfb.planes] + crcs.append(plane_crcs) meta = fb.metadata @@ -347,10 +347,11 @@ def request_handler(state, ctx, req): print(f"\t{ctrl} = {val}") if ctx["opt-save-frames"]: - with fb.mmap(0) as b: + with fb.mmap() as mfb: filename = "frame-{}-{}-{}.data".format(ctx["id"], stream_name, ctx["reqs-completed"]) with open(filename, "wb") as f: - f.write(b) + for p in mfb.planes: + f.write(p) state["renderer"].request_handler(ctx, req) diff --git a/src/py/cam/cam_qt.py b/src/py/cam/cam_qt.py index 30fb7a1d..d394987b 100644 --- a/src/py/cam/cam_qt.py +++ b/src/py/cam/cam_qt.py @@ -324,17 +324,17 @@ class MainWindow(QtWidgets.QWidget): controlsLayout.addStretch() def buf_to_qpixmap(self, stream, fb): - with fb.mmap(0) as b: + with fb.mmap() as mfb: cfg = stream.configuration w, h = cfg.size pitch = cfg.stride if cfg.pixelFormat == "MJPEG": - img = Image.open(BytesIO(b)) + img = Image.open(BytesIO(mfb.planes[0])) qim = ImageQt(img).copy() pix = QtGui.QPixmap.fromImage(qim) else: - data = np.array(b, dtype=np.uint8) + data = np.array(mfb.planes[0], dtype=np.uint8) rgb = to_rgb(cfg.pixelFormat, cfg.size, data) if rgb is None: diff --git a/src/py/libcamera/__init__.py b/src/py/libcamera/__init__.py index cd7512a2..caf06af7 100644 --- a/src/py/libcamera/__init__.py +++ b/src/py/libcamera/__init__.py @@ -1,12 +1,80 @@ # SPDX-License-Identifier: LGPL-2.1-or-later # Copyright (C) 2021, Tomi Valkeinen +from os import lseek, SEEK_END from ._libcamera import * import mmap -def __FrameBuffer__mmap(self, plane): - return mmap.mmap(self.fd(plane), self.length(plane), mmap.MAP_SHARED, mmap.PROT_READ) +def __FrameBuffer__mmap(self): + return MappedFrameBuffer(self) FrameBuffer.mmap = __FrameBuffer__mmap + + +class MappedFrameBuffer: + def __init__(self, fb): + self.fb = fb + + # Collect information about the buffers + + bufinfos = {} + + for i in range(fb.num_planes): + fd = fb.fd(i) + + if fd not in bufinfos: + buflen = lseek(fd, 0, SEEK_END) + bufinfos[fd] = {"maplen": 0, "buflen": buflen} + else: + buflen = bufinfos[fd]["buflen"] + + if fb.offset(i) > buflen or fb.offset(i) + fb.length(i) > buflen: + raise RuntimeError(f"plane is out of buffer: buffer length={buflen}, " + f"plane offset={fb.offset(i)}, plane length={fb.length(i)}") + + bufinfos[fd]["maplen"] = max(bufinfos[fd]["maplen"], fb.offset(i) + fb.length(i)) + + # mmap the buffers + + maps = [] + + for fd, info in bufinfos.items(): + map = mmap.mmap(fd, info["maplen"], mmap.MAP_SHARED, mmap.PROT_READ | mmap.PROT_WRITE) + info["map"] = map + maps.append(map) + + self.maps = tuple(maps) + + # Create memoryviews for the planes + + planes = [] + + for i in range(fb.num_planes): + fd = fb.fd(i) + info = bufinfos[fd] + + mv = memoryview(info["map"]) + + start = fb.offset(i) + end = fb.offset(i) + fb.length(i) + + mv = mv[start:end] + + planes.append(mv) + + self.planes = tuple(planes) + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, exc_traceback): + for p in self.planes: + p.release() + + for mm in self.maps: + mm.close() + + def planes(self): + return self.planes diff --git a/src/py/libcamera/pymain.cpp b/src/py/libcamera/pymain.cpp index b9b52f6b..73d29479 100644 --- a/src/py/libcamera/pymain.cpp +++ b/src/py/libcamera/pymain.cpp @@ -439,6 +439,9 @@ PYBIND11_MODULE(_libcamera, m) return new FrameBuffer(v, cookie); })) .def_property_readonly("metadata", &FrameBuffer::metadata, py::return_value_policy::reference_internal) + .def_property_readonly("num_planes", [](const FrameBuffer &self) { + return self.planes().size(); + }) .def("length", [](FrameBuffer &self, uint32_t idx) { const FrameBuffer::Plane &plane = self.planes()[idx]; return plane.length; @@ -447,6 +450,10 @@ PYBIND11_MODULE(_libcamera, m) const FrameBuffer::Plane &plane = self.planes()[idx]; return plane.fd.get(); }) + .def("offset", [](FrameBuffer &self, uint32_t idx) { + const FrameBuffer::Plane &plane = self.planes()[idx]; + return plane.offset; + }) .def_property("cookie", &FrameBuffer::cookie, &FrameBuffer::setCookie); pyStream