From patchwork Mon May 30 14:27:07 2022 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Tomi Valkeinen X-Patchwork-Id: 16108 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 08FDFC3256 for ; Mon, 30 May 2022 14:27:42 +0000 (UTC) Received: from lancelot.ideasonboard.com (localhost [IPv6:::1]) by lancelot.ideasonboard.com (Postfix) with ESMTP id 7E60A6563F; Mon, 30 May 2022 16:27:41 +0200 (CEST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=libcamera.org; s=mail; t=1653920861; bh=BZb0p4A1aVTXfJycBSMBlwFjjsXco7PjNcFq4WRlQTo=; 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=4XthDISsYQzlQ4Dy6aoxBAzT3usIOkCKvChEU2+41LGKUzCDEwoEuq/oVAN7H94X7 uXOcC7mQzA8DeeVCjpip4X1zMwSPU/KmGeJmpH+vEx2st7xGwvi5+kZr5zBioLyg6L 3EQqSTUjd+7EhSSGDuETK3YFURpAOR4hBcOK4ImZsDjKXu8wxxKsH9P9s9LcpV33LM ZnRLfsRhamQsLzWNImytcK7LUHUII+3mojP/Ps3gJ4n4tVcWjyRjT/WIDDwxdii6Yb Aq2ry5+RB5fhxJleVddd4oaIq5gWBq813R3VxR1TvqJlYKmhisENuct+aVJ0LlCwWo JoNjNVW8aR/sQ== 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 B941F60411 for ; Mon, 30 May 2022 16:27:39 +0200 (CEST) Authentication-Results: lancelot.ideasonboard.com; dkim=pass (1024-bit key; unprotected) header.d=ideasonboard.com header.i=@ideasonboard.com header.b="FvRn04+n"; 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 310D3B90; Mon, 30 May 2022 16:27:39 +0200 (CEST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=ideasonboard.com; s=mail; t=1653920859; bh=BZb0p4A1aVTXfJycBSMBlwFjjsXco7PjNcFq4WRlQTo=; h=From:To:Cc:Subject:Date:In-Reply-To:References:From; b=FvRn04+n5o8xsJnJWoGDaBpPwDnfOfl9+jSqxQ9l2yNb4Ntp+M+6D0OAGgUA9YZo2 vchHFvXDYnStZhoLMvSVUW6tjceEDcmtC75vSh1QD4BIxO1XTnzyJHpG1PHFO55x7Z Ql+LuaxgPpKk5OjBgr+8W936ZcaW5a48yeC0lFHo= To: libcamera-devel@lists.libcamera.org, David Plowman , Kieran Bingham , Laurent Pinchart , Jacopo Mondi Date: Mon, 30 May 2022 17:27:07 +0300 Message-Id: <20220530142722.57618-2-tomi.valkeinen@ideasonboard.com> X-Mailer: git-send-email 2.34.1 In-Reply-To: <20220530142722.57618-1-tomi.valkeinen@ideasonboard.com> References: <20220530142722.57618-1-tomi.valkeinen@ideasonboard.com> MIME-Version: 1.0 Subject: [libcamera-devel] [PATCH v4 01/16] py: unittests: Fix test_sleep() 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" Waiting for 0.5 secs and expecting that the requests have been completed is... bad. Fix the test case by using cam.read_event() as a blocking wait, and wait until we have received all the requests that we queued. Signed-off-by: Tomi Valkeinen Reviewed-by: Laurent Pinchart --- test/py/unittests.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/test/py/unittests.py b/test/py/unittests.py index 426efb06..45b35223 100755 --- a/test/py/unittests.py +++ b/test/py/unittests.py @@ -161,7 +161,7 @@ class AllocatorTestMethods(CameraTesterBase): class SimpleCaptureMethods(CameraTesterBase): - def test_sleep(self): + def test_blocking(self): cm = self.cm cam = self.cam @@ -207,11 +207,17 @@ class SimpleCaptureMethods(CameraTesterBase): reqs = None gc.collect() - time.sleep(0.5) + reqs = [] - reqs = cm.get_ready_requests() + while True: + cm.read_event() - self.assertTrue(len(reqs) == num_bufs) + ready_reqs = cm.get_ready_requests() + + reqs += ready_reqs + + if len(reqs) == num_bufs: + break for i, req in enumerate(reqs): self.assertTrue(i == req.cookie) From patchwork Mon May 30 14:27:08 2022 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Tomi Valkeinen X-Patchwork-Id: 16109 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 D792DC326D for ; Mon, 30 May 2022 14:27:44 +0000 (UTC) Received: from lancelot.ideasonboard.com (localhost [IPv6:::1]) by lancelot.ideasonboard.com (Postfix) with ESMTP id 2FB3B65644; Mon, 30 May 2022 16:27:44 +0200 (CEST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=libcamera.org; s=mail; t=1653920864; bh=WH3FanN0B7wtHyKVOVazV8b7jBn/0LRb+mJc3K8kM9A=; 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=hzyZOpwHoYYYylz/WBBam5S9DdLRlgp7+laoKs99oUnV8tYZ4WV++cyhTyDWc7g4G Tfs4pnNDFdAmP1r0I2TjHpmhXYROwegJ9qAODMe8cC4j59FyF8IBv0nWHh3FcL+Sd4 IrHPFEcnFa2c5NsfaxA5d43qHPgPSLnUwqczaVTHga5ecp4VUwUsCXL9+KnNjlS9uh MGrkitctNLr0CxAiyYadTVvsYJlos4dMv+uEjMJHMK2j4spOpuLmj1zORQjqO0WJ4G +xoOemxX3hUA4dKVyQui6n4yc7qJ11LwXNPilbSv3Hg7Dd17afiTR9oJOBilwmH9ec 9VsR24AJt60KA== Received: from perceval.ideasonboard.com (perceval.ideasonboard.com [213.167.242.64]) by lancelot.ideasonboard.com (Postfix) with ESMTPS id 4718360411 for ; Mon, 30 May 2022 16:27:40 +0200 (CEST) Authentication-Results: lancelot.ideasonboard.com; dkim=pass (1024-bit key; unprotected) header.d=ideasonboard.com header.i=@ideasonboard.com header.b="nfky05ho"; 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 AC154E57; Mon, 30 May 2022 16:27:39 +0200 (CEST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=ideasonboard.com; s=mail; t=1653920860; bh=WH3FanN0B7wtHyKVOVazV8b7jBn/0LRb+mJc3K8kM9A=; h=From:To:Cc:Subject:Date:In-Reply-To:References:From; b=nfky05hofx59QsDJIDBnXisd7kSrYb+rWxSXAtjkDhmBNG6jy0CZk5Xy6C2Nf8EGl kRwf2wk98PeGG/0m/DDTRY/DyAJNEi5SMyW9iexitrpWRd0KAEgRSyf88zIOvL4mdb BjVnIKABLFh3APqhCJ8XbxjNKvqnOLzFWO3gTHwU= To: libcamera-devel@lists.libcamera.org, David Plowman , Kieran Bingham , Laurent Pinchart , Jacopo Mondi Date: Mon, 30 May 2022 17:27:08 +0300 Message-Id: <20220530142722.57618-3-tomi.valkeinen@ideasonboard.com> X-Mailer: git-send-email 2.34.1 In-Reply-To: <20220530142722.57618-1-tomi.valkeinen@ideasonboard.com> References: <20220530142722.57618-1-tomi.valkeinen@ideasonboard.com> MIME-Version: 1.0 Subject: [libcamera-devel] [PATCH v4 02/16] py: unittests: Fix test_select() 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" The test_select() currently uses self.assertTrue(len(ready_reqs) > 0) to see that cm.get_ready_requests() returns something. This is not always the case, as there may be two eventfd events queued, and the first call to cm.get_ready_requests() returns all the requests, and thus the second call returns none. Remove the self.assertTrue(len(ready_reqs) > 0) assert. Signed-off-by: Tomi Valkeinen Reviewed-by: Laurent Pinchart --- test/py/unittests.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/test/py/unittests.py b/test/py/unittests.py index 45b35223..33b35a0a 100755 --- a/test/py/unittests.py +++ b/test/py/unittests.py @@ -287,8 +287,6 @@ class SimpleCaptureMethods(CameraTesterBase): ready_reqs = cm.get_ready_requests() - self.assertTrue(len(ready_reqs) > 0) - reqs += ready_reqs if len(reqs) == num_bufs: From patchwork Mon May 30 14:27:09 2022 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Tomi Valkeinen X-Patchwork-Id: 16110 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 EA1D5C326E for ; Mon, 30 May 2022 14:27:45 +0000 (UTC) Received: from lancelot.ideasonboard.com (localhost [IPv6:::1]) by lancelot.ideasonboard.com (Postfix) with ESMTP id 5205665634; Mon, 30 May 2022 16:27:45 +0200 (CEST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=libcamera.org; s=mail; t=1653920865; bh=3nBroEXQeB9WF5QXGcL42axygbVgcecjyW2a/47DYVw=; 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=ZccRwFhu6UUBqRCGWjKibgEm3jRjbgvlNDmSZOC3dFovyiMen2XtT2OKwA0xIzjPb atykQQlagTjF4XRh24u1ogFeJdjr7zBOVkon1ZLfKt0jxcFm4kKclDKKRvtXASbqxO 0jeCk1NxfNK75IgX67eNpwoKyF8l8PDHs07G38XKxxWS4FsOuwZmr4WDnJhsbzVFl8 3OiYoTodpbmOKk6BKx0bh2bc/MYVhuohZ+TbLRYbpT+5IVd2CuIsJ+Vk1o/213XBxi K3qefjt/Osdj7h1w8gDPefIsnL2aWmoCjI7/Djk/8GGKNu77hmZ4/UgUIofIpouuxZ Bi2X41COkXEfw== 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 C147A60411 for ; Mon, 30 May 2022 16:27:40 +0200 (CEST) Authentication-Results: lancelot.ideasonboard.com; dkim=pass (1024-bit key; unprotected) header.d=ideasonboard.com header.i=@ideasonboard.com header.b="pv+wVpSS"; 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 33A13EF1; Mon, 30 May 2022 16:27:40 +0200 (CEST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=ideasonboard.com; s=mail; t=1653920860; bh=3nBroEXQeB9WF5QXGcL42axygbVgcecjyW2a/47DYVw=; h=From:To:Cc:Subject:Date:In-Reply-To:References:From; b=pv+wVpSSljPeDpfRparm19tAkrUS0c6nNp0QF9tnrs3vUAwyzTE6uXBSk7dIzTcwv OPgdV2atxxmyXdjCAWayorgu5z4KM449xQNXiAMz+qmSoFCjR65oMBqHMzXWBUFdDh YiVBbVg/L0s06iRRanwBqdVyZFq00oa9z0nYGfWI= To: libcamera-devel@lists.libcamera.org, David Plowman , Kieran Bingham , Laurent Pinchart , Jacopo Mondi Date: Mon, 30 May 2022 17:27:09 +0300 Message-Id: <20220530142722.57618-4-tomi.valkeinen@ideasonboard.com> X-Mailer: git-send-email 2.34.1 In-Reply-To: <20220530142722.57618-1-tomi.valkeinen@ideasonboard.com> References: <20220530142722.57618-1-tomi.valkeinen@ideasonboard.com> MIME-Version: 1.0 Subject: [libcamera-devel] [PATCH v4 03/16] py: cam: Move conversion funcs to helpers.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" Move conversion functions from cam_qt.py to helpers.py to clean up the code and so that they can be used from other cam renderers. Signed-off-by: Tomi Valkeinen Reviewed-by: Laurent Pinchart --- src/py/cam/cam_qt.py | 156 +-------------------------- src/py/cam/{cam_qt.py => helpers.py} | 154 +------------------------- 2 files changed, 2 insertions(+), 308 deletions(-) copy src/py/cam/{cam_qt.py => helpers.py} (55%) diff --git a/src/py/cam/cam_qt.py b/src/py/cam/cam_qt.py index c294c999..61a77f45 100644 --- a/src/py/cam/cam_qt.py +++ b/src/py/cam/cam_qt.py @@ -1,16 +1,13 @@ # SPDX-License-Identifier: GPL-2.0-or-later # Copyright (C) 2022, Tomi Valkeinen -# -# Debayering code from PiCamera documentation +from helpers import mfb_to_rgb 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 libcamera as libcam import libcamera.utils -import numpy as np import sys @@ -21,157 +18,6 @@ def rgb_to_pix(rgb): return pix -def demosaic(data, r0, g0, g1, b0): - # Separate the components from the Bayer data to RGB planes - - 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 - - # 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 by a byte 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 = 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.width - h = size.height - - if fmt == libcam.formats.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 == libcam.formats.RGB888: - rgb = data.reshape((h, w, 3)) - rgb[:, :, [0, 1, 2]] = rgb[:, :, [2, 1, 0]] - - elif fmt == libcam.formats.BGR888: - rgb = data.reshape((h, w, 3)) - - elif fmt in [libcam.formats.ARGB8888, libcam.formats.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 str(fmt).startswith('S'): - fmt = str(fmt) - 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 = demosaic(data, r0, g0, g1, b0) - rgb = (rgb >> 8).astype(np.uint8) - - else: - rgb = None - - return rgb - - -# A naive format conversion to 24-bit RGB -def mfb_to_rgb(mfb, cfg): - data = np.array(mfb.planes[0], dtype=np.uint8) - rgb = to_rgb(cfg.pixel_format, cfg.size, data) - return rgb - - class QtRenderer: def __init__(self, state): self.state = state diff --git a/src/py/cam/cam_qt.py b/src/py/cam/helpers.py similarity index 55% copy from src/py/cam/cam_qt.py copy to src/py/cam/helpers.py index c294c999..6b32a134 100644 --- a/src/py/cam/cam_qt.py +++ b/src/py/cam/helpers.py @@ -3,22 +3,10 @@ # # 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 libcamera as libcam import libcamera.utils 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 demosaic(data, r0, g0, g1, b0): @@ -166,147 +154,7 @@ def to_rgb(fmt, size, data): # A naive format conversion to 24-bit RGB -def mfb_to_rgb(mfb, cfg): +def mfb_to_rgb(mfb: libcamera.utils.MappedFrameBuffer, cfg: libcam.StreamConfiguration): data = np.array(mfb.planes[0], dtype=np.uint8) rgb = to_rgb(cfg.pixel_format, cfg.size, data) 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: - for stream in ctx.streams: - window = MainWindow(ctx, stream) - window.show() - windows.append(window) - - self.windows = windows - - def run(self): - camnotif = QtCore.QSocketNotifier(self.cm.event_fd, QtCore.QSocketNotifier.Read) - camnotif.activated.connect(lambda _: self.readcam()) - - keynotif = QtCore.QSocketNotifier(sys.stdin.fileno(), QtCore.QSocketNotifier.Read) - keynotif.activated.connect(lambda _: self.readkey()) - - print('Capturing...') - - self.app.exec() - - print('Exiting...') - - def readcam(self): - running = self.state.event_handler() - - 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_processed(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 cid, cv in camera.properties.items(): - lab = QtWidgets.QLabel() - lab.setText('{} = {}'.format(cid, cv)) - groupLayout.addWidget(lab) - - group = QtWidgets.QGroupBox('Controls') - groupLayout = QtWidgets.QVBoxLayout() - group.setLayout(groupLayout) - controlsLayout.addWidget(group) - - for cid, cinfo in camera.controls.items(): - lab = QtWidgets.QLabel() - lab.setText('{} = {}/{}/{}' - .format(cid, cinfo.min, cinfo.max, cinfo.default)) - groupLayout.addWidget(lab) - - controlsLayout.addStretch() - - def buf_to_qpixmap(self, stream, fb): - with libcamera.utils.MappedFrameBuffer(fb) as mfb: - cfg = stream.configuration - - if cfg.pixel_format == libcam.formats.MJPEG: - img = Image.open(BytesIO(mfb.planes[0])) - qim = ImageQt(img).copy() - pix = QtGui.QPixmap.fromImage(qim) - else: - rgb = mfb_to_rgb(mfb, cfg) - if rgb is None: - raise Exception('Format not supported: ' + cfg.pixel_format) - - 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)) From patchwork Mon May 30 14:27:10 2022 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Tomi Valkeinen X-Patchwork-Id: 16111 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 090CBC326F for ; Mon, 30 May 2022 14:27:47 +0000 (UTC) Received: from lancelot.ideasonboard.com (localhost [IPv6:::1]) by lancelot.ideasonboard.com (Postfix) with ESMTP id 308ED6564F; Mon, 30 May 2022 16:27:47 +0200 (CEST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=libcamera.org; s=mail; t=1653920867; bh=1AFq90Ktt2OqgK4q7nRfejvXrZdM2ryLizgojc4jWLY=; 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=xt/kD7iqCQW6ylsdh/jKZwl/z9X+h+n+xll1SB6rnpts+kFyyBHEj2DPc1MABp9xp q1bhSTwXxF2XZCXtAuxr+09QtnVrJzPe+nHarM4cOLaBuSZSLSJ24guIZPZtVycVmV nQzmnlZ21YWDmnZ0yhQVzo/cn5YkhGV4ASDJ4KFva/D1UUKf1+TvVoPkLkMQ/SFnmW x1kzX2uYLW+oh8k6X3wuHR7LYlKSJb7jjWHT/sq16gzhpreKiyE0wkZnDkOiYerDGw ST5y4vOwN0D449Exkk4rySvnOYpVXkJKAT4wIsVX5q+pp3PmX4uGIYFmUM+r9rBd0D LTqrllwKe3oTw== 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 48C066563B for ; Mon, 30 May 2022 16:27:41 +0200 (CEST) Authentication-Results: lancelot.ideasonboard.com; dkim=pass (1024-bit key; unprotected) header.d=ideasonboard.com header.i=@ideasonboard.com header.b="ju5CRlAW"; 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 B5343F95; Mon, 30 May 2022 16:27:40 +0200 (CEST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=ideasonboard.com; s=mail; t=1653920861; bh=1AFq90Ktt2OqgK4q7nRfejvXrZdM2ryLizgojc4jWLY=; h=From:To:Cc:Subject:Date:In-Reply-To:References:From; b=ju5CRlAWRbzfO6WCN47nynQ5U3IOR9NMlvxUMpqgak9wu2Vtrkxlyj0t0q7AWtiO4 fQ4IIGLc4pKqFME5LTfGKeyTG1ca7WQv8CF7YVok4xbEMcrJDBjx8zdTTHZABBO9lg CqaKHWr4+vVdPlwra1KRIlFEg8XK/nN7rOZZhgfQ= To: libcamera-devel@lists.libcamera.org, David Plowman , Kieran Bingham , Laurent Pinchart , Jacopo Mondi Date: Mon, 30 May 2022 17:27:10 +0300 Message-Id: <20220530142722.57618-5-tomi.valkeinen@ideasonboard.com> X-Mailer: git-send-email 2.34.1 In-Reply-To: <20220530142722.57618-1-tomi.valkeinen@ideasonboard.com> References: <20220530142722.57618-1-tomi.valkeinen@ideasonboard.com> MIME-Version: 1.0 Subject: [libcamera-devel] [PATCH v4 04/16] py: cam: Drop PIL dependency 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" We can use Qt directly to accomplish the same as we do with PIL. A minor downside is that loading MJPEG frame with Qt produces a "Corrupt JPEG data" warning. The resulting picture looks fine, though. So add a message handler to ignore that warning. Signed-off-by: Tomi Valkeinen Reviewed-by: Laurent Pinchart --- src/py/cam/cam_qt.py | 29 +++++++++++++++++++++-------- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/src/py/cam/cam_qt.py b/src/py/cam/cam_qt.py index 61a77f45..b6412bdf 100644 --- a/src/py/cam/cam_qt.py +++ b/src/py/cam/cam_qt.py @@ -2,18 +2,32 @@ # Copyright (C) 2022, Tomi Valkeinen from helpers import mfb_to_rgb -from io import BytesIO -from PIL import Image -from PIL.ImageQt import ImageQt from PyQt5 import QtCore, QtGui, QtWidgets import libcamera as libcam import libcamera.utils import sys +# Loading MJPEG to a QPixmap produces corrupt JPEG data warnings. Ignore these. +def qt_message_handler(msg_type, msg_log_context, msg_string): + if msg_string.startswith("Corrupt JPEG data"): + return + + # For some reason qInstallMessageHandler returns None, so we won't + # call the old handler + if old_msg_handler is not None: + old_msg_handler(msg_type, msg_log_context, msg_string) + else: + print(msg_string) + + +old_msg_handler = QtCore.qInstallMessageHandler(qt_message_handler) + + def rgb_to_pix(rgb): - img = Image.frombuffer('RGB', (rgb.shape[1], rgb.shape[0]), rgb) - qim = ImageQt(img).copy() + w = rgb.shape[1] + h = rgb.shape[0] + qim = QtGui.QImage(rgb, w, h, QtGui.QImage.Format.Format_RGB888) pix = QtGui.QPixmap.fromImage(qim) return pix @@ -136,9 +150,8 @@ class MainWindow(QtWidgets.QWidget): cfg = stream.configuration if cfg.pixel_format == libcam.formats.MJPEG: - img = Image.open(BytesIO(mfb.planes[0])) - qim = ImageQt(img).copy() - pix = QtGui.QPixmap.fromImage(qim) + pix = QtGui.QPixmap(cfg.size.width, cfg.size.height) + pix.loadFromData(mfb.planes[0]) else: rgb = mfb_to_rgb(mfb, cfg) if rgb is None: From patchwork Mon May 30 14:27:11 2022 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Tomi Valkeinen X-Patchwork-Id: 16112 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 02D8FC3270 for ; Mon, 30 May 2022 14:27:48 +0000 (UTC) Received: from lancelot.ideasonboard.com (localhost [IPv6:::1]) by lancelot.ideasonboard.com (Postfix) with ESMTP id 4AA066563D; Mon, 30 May 2022 16:27:48 +0200 (CEST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=libcamera.org; s=mail; t=1653920868; bh=61t+dhJe27bzpHCJUf8idA6AgToLpGZf2TvyO61VH1A=; 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=IkSON5ETut2PvZm0gRiU87rhvIBYpwrOQ6DAE1tID5Bwz7tDEzBfJaVLc5JWRO0cA s6a4aBV5oN1k5Co3/Ug9YHWPOyzid8QK1mpIv1JgB8DukH464W69gOunQw/cIC5TeW D6O4NZm7ILBDwOu59ayAX3o70XtixBYuWl5lFPQAJtf6n8IeQW5rnJHZN15N1s4umB inHo4PJtnnU9A2tQyfX5nTvLwvOezBDZPpODqM3o5ekjfGPAfntGD/bm42v5/0YVrE EMVNtnkUzO/2h3cyCbveIFkp1wIADCrTnkMN9V5DPAW0xcWsEExPTZqVamFZrVHHuM 8DxlIItS8U25g== 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 C59E565641 for ; Mon, 30 May 2022 16:27:41 +0200 (CEST) Authentication-Results: lancelot.ideasonboard.com; dkim=pass (1024-bit key; unprotected) header.d=ideasonboard.com header.i=@ideasonboard.com header.b="K9hgSIdu"; 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 3C2D4101E; Mon, 30 May 2022 16:27:41 +0200 (CEST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=ideasonboard.com; s=mail; t=1653920861; bh=61t+dhJe27bzpHCJUf8idA6AgToLpGZf2TvyO61VH1A=; h=From:To:Cc:Subject:Date:In-Reply-To:References:From; b=K9hgSIduAJ7G0kdjzHV1+wfBeNSoWb0F3LWbMVsQGNY7kcyaNcCocmh/12Pmyq4zf Wad/EQUTY4HLdwUQV0997b8/Jbm6jj0ERZspYaH20++1NXx6f15BrQFFFqwwprdHfD 0TCqjaOUFEGEHLS1R3Qbis4Wo58Ae0nsnNcyUeLY= To: libcamera-devel@lists.libcamera.org, David Plowman , Kieran Bingham , Laurent Pinchart , Jacopo Mondi Date: Mon, 30 May 2022 17:27:11 +0300 Message-Id: <20220530142722.57618-6-tomi.valkeinen@ideasonboard.com> X-Mailer: git-send-email 2.34.1 In-Reply-To: <20220530142722.57618-1-tomi.valkeinen@ideasonboard.com> References: <20220530142722.57618-1-tomi.valkeinen@ideasonboard.com> MIME-Version: 1.0 Subject: [libcamera-devel] [PATCH v4 05/16] py: Add Request.__str__() 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 Request.__str__() which maps directly to Request::toString(). Signed-off-by: Tomi Valkeinen Reviewed-by: Laurent Pinchart --- src/py/libcamera/py_main.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/py/libcamera/py_main.cpp b/src/py/libcamera/py_main.cpp index 3220a9e6..9e827845 100644 --- a/src/py/libcamera/py_main.cpp +++ b/src/py/libcamera/py_main.cpp @@ -496,7 +496,8 @@ PYBIND11_MODULE(_libcamera, m) * \todo 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); }); + .def("reuse", [](Request &self) { self.reuse(Request::ReuseFlag::ReuseBuffers); }) + .def("__str__", &Request::toString); pyRequestStatus .value("Pending", Request::RequestPending) From patchwork Mon May 30 14:27:12 2022 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Tomi Valkeinen X-Patchwork-Id: 16113 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 347E0C3271 for ; Mon, 30 May 2022 14:27:50 +0000 (UTC) Received: from lancelot.ideasonboard.com (localhost [IPv6:::1]) by lancelot.ideasonboard.com (Postfix) with ESMTP id 67DEF65647; Mon, 30 May 2022 16:27:49 +0200 (CEST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=libcamera.org; s=mail; t=1653920869; bh=e+fvoGpCQfNdN138CUPcW/BL1DzCs9VA5alM5oMPYTs=; 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=vFwCcTQjmiltU398NL3TCAeMANlhYtSFiQDaY0cQjOhzSe+VIp9oQhhXK03JkIXFa qD80TZJ95y9dEdmYX5ZQ1rbEjxILncv/4za8bfnE99C3larmeadDH97RjPdYba3YLn PrBGCgdP3cd1e354mMAC0PAYE5Aydu3MfCzv3ZzaMGtspfE3Gwk2y+6aKx/XrqSXK/ ho4VtkXmhoS0yxdQjFmBav1PndhnDyqHQJFGmRHfRzgbP65598G7VmHvwXLQaQqHhu L9Dw5gjpIGFHzCkoqJ0zC64T6E2onEQ3BT2Ci/trpJQ7UyB9SU0WokIK3Dx/8Ng24r Hd4QOTt2VoCNQ== Received: from perceval.ideasonboard.com (perceval.ideasonboard.com [213.167.242.64]) by lancelot.ideasonboard.com (Postfix) with ESMTPS id 4640465634 for ; Mon, 30 May 2022 16:27:42 +0200 (CEST) Authentication-Results: lancelot.ideasonboard.com; dkim=pass (1024-bit key; unprotected) header.d=ideasonboard.com header.i=@ideasonboard.com header.b="NlbhS5OT"; 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 B534C11C1; Mon, 30 May 2022 16:27:41 +0200 (CEST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=ideasonboard.com; s=mail; t=1653920862; bh=e+fvoGpCQfNdN138CUPcW/BL1DzCs9VA5alM5oMPYTs=; h=From:To:Cc:Subject:Date:In-Reply-To:References:From; b=NlbhS5OT7FvlbvvuZH6Gvk+7Yhgd9MNsZwpEjCpYTeWB+zplMMuhiHxzOAIXhOKYg kSDcATp0Ux6lYptspTp4sQNp/3oejVPDpAyw+uHBmRcbMMOF/lAVFAAjQXfz0weW6U 5se5y6wjHftnbrxDztMXdpDcswFzogkkmFzBpxEI= To: libcamera-devel@lists.libcamera.org, David Plowman , Kieran Bingham , Laurent Pinchart , Jacopo Mondi Date: Mon, 30 May 2022 17:27:12 +0300 Message-Id: <20220530142722.57618-7-tomi.valkeinen@ideasonboard.com> X-Mailer: git-send-email 2.34.1 In-Reply-To: <20220530142722.57618-1-tomi.valkeinen@ideasonboard.com> References: <20220530142722.57618-1-tomi.valkeinen@ideasonboard.com> MIME-Version: 1.0 Subject: [libcamera-devel] [PATCH v4 06/16] py: Add FrameMetadataPlane 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 FrameMetadataPlane class and adjust the methods and uses accordingly. Signed-off-by: Tomi Valkeinen Reviewed-by: Laurent Pinchart --- src/py/cam/cam.py | 3 ++- src/py/libcamera/py_main.cpp | 13 ++++++++----- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/py/cam/cam.py b/src/py/cam/cam.py index 677dd680..bf8529d9 100755 --- a/src/py/cam/cam.py +++ b/src/py/cam/cam.py @@ -285,7 +285,8 @@ class CaptureState: print('{:.6f} ({:.2f} fps) {}-{}: seq {}, bytes {}, CRCs {}' .format(ts / 1000000000, fps, ctx.id, stream_name, - meta.sequence, meta.bytesused, + meta.sequence, + '/'.join([str(p.bytes_used) for p in meta.planes]), crcs)) if ctx.opt_metadata: diff --git a/src/py/libcamera/py_main.cpp b/src/py/libcamera/py_main.cpp index 9e827845..52f70811 100644 --- a/src/py/libcamera/py_main.cpp +++ b/src/py/libcamera/py_main.cpp @@ -163,6 +163,7 @@ PYBIND11_MODULE(_libcamera, m) auto pyRequestReuse = py::enum_(pyRequest, "Reuse"); auto pyFrameMetadata = py::class_(m, "FrameMetadata"); auto pyFrameMetadataStatus = py::enum_(pyFrameMetadata, "Status"); + auto pyFrameMetadataPlane = py::class_(pyFrameMetadata, "Plane"); auto pyTransform = py::class_(m, "Transform"); auto pyColorSpace = py::class_(m, "ColorSpace"); auto pyColorSpacePrimaries = py::enum_(pyColorSpace, "Primaries"); @@ -512,11 +513,10 @@ PYBIND11_MODULE(_libcamera, m) .def_readonly("status", &FrameMetadata::status) .def_readonly("sequence", &FrameMetadata::sequence) .def_readonly("timestamp", &FrameMetadata::timestamp) - /* \todo Implement FrameMetadata::Plane properly */ - .def_property_readonly("bytesused", [](FrameMetadata &self) { - std::vector v; - v.resize(self.planes().size()); - transform(self.planes().begin(), self.planes().end(), v.begin(), [](const auto &p) { return p.bytesused; }); + .def_property_readonly("planes", [](const FrameMetadata &self) { + /* Convert from Span<> to std::vector<> */ + /* Note: this creates a copy */ + std::vector v(self.planes().begin(), self.planes().end()); return v; }); @@ -525,6 +525,9 @@ PYBIND11_MODULE(_libcamera, m) .value("Error", FrameMetadata::FrameError) .value("Cancelled", FrameMetadata::FrameCancelled); + pyFrameMetadataPlane + .def_readwrite("bytes_used", &FrameMetadata::Plane::bytesused); + pyTransform .def(py::init([](int rotation, bool hflip, bool vflip, bool transpose) { bool ok; From patchwork Mon May 30 14:27:13 2022 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Tomi Valkeinen X-Patchwork-Id: 16114 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 120B0BD161 for ; Mon, 30 May 2022 14:27:51 +0000 (UTC) Received: from lancelot.ideasonboard.com (localhost [IPv6:::1]) by lancelot.ideasonboard.com (Postfix) with ESMTP id 71BD565646; Mon, 30 May 2022 16:27:50 +0200 (CEST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=libcamera.org; s=mail; t=1653920870; bh=5EQrQnLIMh7Q1vLf6YDk88bvwFYgl0J3102xsGZzwhQ=; 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=c0Joi3Kuv7MR94qFFF0rZsMXbhkUcjrIDJ58VvE+/BTD4tCUPJcjath7eMje0Sv7m SMEOMZpnWepWK8reDEqKjue4ZbPzOeyBOKRJXa6rXD5T2V8ubRlskJOGHEnxnOdS2R f0CqcSZrn6EKtQCVZWfbzMrkF7M2TN+N3wCBFjxA3h5Mf/DOBA24jFRB5SV8g0ma8U lyzN876+kn7s/K0CJ3QTHnXnpPYxUmhGiZtdnFjuQF1PHCiw6W1FF3l0Qay5BtYpzv L06sBVfqn7eTBz+P/DUbSC85fcWqm0EVmMUU+TNtsjiuisdgUtpXGpmUrjJ1vdNcp0 YemUMW74GOrgg== Received: from perceval.ideasonboard.com (perceval.ideasonboard.com [213.167.242.64]) by lancelot.ideasonboard.com (Postfix) with ESMTPS id D1E3265642 for ; Mon, 30 May 2022 16:27:42 +0200 (CEST) Authentication-Results: lancelot.ideasonboard.com; dkim=pass (1024-bit key; unprotected) header.d=ideasonboard.com header.i=@ideasonboard.com header.b="E96PVq2W"; 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 3FCE01217; Mon, 30 May 2022 16:27:42 +0200 (CEST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=ideasonboard.com; s=mail; t=1653920862; bh=5EQrQnLIMh7Q1vLf6YDk88bvwFYgl0J3102xsGZzwhQ=; h=From:To:Cc:Subject:Date:In-Reply-To:References:From; b=E96PVq2Wm6AwDo3NyyuadyhPkZW2pqIRlSPEjszHMmiPmvyd/O80m7gzwCqyL6Mn/ xlBS9gmjshTeqyAyInVLsIZnuFEE9rmc3O3AiTRMaXXhBVfyfNc9Pta+Cf/jdqABXn gLktR5BbCOWDfAfF6RYdUilaMZ2yZ4n56rQnUyos= To: libcamera-devel@lists.libcamera.org, David Plowman , Kieran Bingham , Laurent Pinchart , Jacopo Mondi Date: Mon, 30 May 2022 17:27:13 +0300 Message-Id: <20220530142722.57618-8-tomi.valkeinen@ideasonboard.com> X-Mailer: git-send-email 2.34.1 In-Reply-To: <20220530142722.57618-1-tomi.valkeinen@ideasonboard.com> References: <20220530142722.57618-1-tomi.valkeinen@ideasonboard.com> MIME-Version: 1.0 Subject: [libcamera-devel] [PATCH v4 07/16] py: Implement FrameBufferPlane 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" Implement FrameBufferPlane class and adjust the methods and uses accordingly. Note that we don't expose the fd as a SharedFD, but as an int. Signed-off-by: Tomi Valkeinen Reviewed-by: Laurent Pinchart --- src/py/cam/cam_kms.py | 6 +-- src/py/cam/cam_qtgl.py | 2 +- src/py/libcamera/py_main.cpp | 45 +++++++++++---------- src/py/libcamera/utils/MappedFrameBuffer.py | 18 ++++----- 4 files changed, 36 insertions(+), 35 deletions(-) diff --git a/src/py/cam/cam_kms.py b/src/py/cam/cam_kms.py index 49b00211..38fc382d 100644 --- a/src/py/cam/cam_kms.py +++ b/src/py/cam/cam_kms.py @@ -131,10 +131,10 @@ class KMSRenderer: fds = [] strides = [] offsets = [] - for i in range(fb.num_planes): - fds.append(fb.fd(i)) + for plane in fb.planes: + fds.append(plane.fd) strides.append(cfg.stride) - offsets.append(fb.offset(i)) + offsets.append(plane.offset) drmfb = pykms.DmabufFramebuffer(self.card, w, h, fmt, fds, strides, offsets) diff --git a/src/py/cam/cam_qtgl.py b/src/py/cam/cam_qtgl.py index 4b43f51d..6cfbd347 100644 --- a/src/py/cam/cam_qtgl.py +++ b/src/py/cam/cam_qtgl.py @@ -269,7 +269,7 @@ class MainWindow(QtWidgets.QWidget): 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_FD_EXT, fb.planes[0].fd, EGL_DMA_BUF_PLANE0_OFFSET_EXT, 0, EGL_DMA_BUF_PLANE0_PITCH_EXT, cfg.stride, EGL_NONE, diff --git a/src/py/libcamera/py_main.cpp b/src/py/libcamera/py_main.cpp index 52f70811..fcf009f0 100644 --- a/src/py/libcamera/py_main.cpp +++ b/src/py/libcamera/py_main.cpp @@ -155,6 +155,7 @@ PYBIND11_MODULE(_libcamera, m) auto pyStreamFormats = py::class_(m, "StreamFormats"); auto pyFrameBufferAllocator = py::class_(m, "FrameBufferAllocator"); auto pyFrameBuffer = py::class_(m, "FrameBuffer"); + auto pyFrameBufferPlane = py::class_(pyFrameBuffer, "Plane"); auto pyStream = py::class_(m, "Stream"); auto pyControlId = py::class_(m, "ControlId"); auto pyControlInfo = py::class_(m, "ControlInfo"); @@ -408,31 +409,31 @@ PYBIND11_MODULE(_libcamera, m) }); pyFrameBuffer - /* \todo implement FrameBuffer::Plane properly */ - .def(py::init([](std::vector> planes, unsigned int cookie) { - std::vector v; - for (const auto &t : planes) - v.push_back({ SharedFD(std::get<0>(t)), FrameBuffer::Plane::kInvalidOffset, std::get<1>(t) }); - return new FrameBuffer(v, cookie); - })) + .def(py::init, unsigned int>(), + py::arg("planes"), py::arg("cookie") = 0) .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; - }) - .def("fd", [](FrameBuffer &self, uint32_t idx) { - 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_readonly("planes", &FrameBuffer::planes) .def_property("cookie", &FrameBuffer::cookie, &FrameBuffer::setCookie); + pyFrameBufferPlane + .def(py::init()) + .def(py::init([](int fd, unsigned int offset, unsigned int length) { + auto p = FrameBuffer::Plane(); + p.fd = SharedFD(fd); + p.offset = offset; + p.length = length; + return p; + }), py::arg("fd"), py::arg("offset"), py::arg("length")) + .def_property("fd", + [](const FrameBuffer::Plane &self) { + return self.fd.get(); + }, + [](FrameBuffer::Plane &self, int fd) { + self.fd = SharedFD(fd); + }) + .def_readwrite("offset", &FrameBuffer::Plane::offset) + .def_readwrite("length", &FrameBuffer::Plane::length); + pyStream .def_property_readonly("configuration", &Stream::configuration); diff --git a/src/py/libcamera/utils/MappedFrameBuffer.py b/src/py/libcamera/utils/MappedFrameBuffer.py index fc2726b6..a8502d51 100644 --- a/src/py/libcamera/utils/MappedFrameBuffer.py +++ b/src/py/libcamera/utils/MappedFrameBuffer.py @@ -21,8 +21,8 @@ class MappedFrameBuffer: bufinfos = {} - for i in range(fb.num_planes): - fd = fb.fd(i) + for plane in fb.planes: + fd = plane.fd if fd not in bufinfos: buflen = os.lseek(fd, 0, os.SEEK_END) @@ -30,11 +30,11 @@ class MappedFrameBuffer: else: buflen = bufinfos[fd]['buflen'] - if fb.offset(i) > buflen or fb.offset(i) + fb.length(i) > buflen: + if plane.offset > buflen or plane.offset + plane.length > buflen: raise RuntimeError(f'plane is out of buffer: buffer length={buflen}, ' + - f'plane offset={fb.offset(i)}, plane length={fb.length(i)}') + f'plane offset={plane.offset}, plane length={plane.length}') - bufinfos[fd]['maplen'] = max(bufinfos[fd]['maplen'], fb.offset(i) + fb.length(i)) + bufinfos[fd]['maplen'] = max(bufinfos[fd]['maplen'], plane.offset + plane.length) # mmap the buffers @@ -51,14 +51,14 @@ class MappedFrameBuffer: planes = [] - for i in range(fb.num_planes): - fd = fb.fd(i) + for plane in fb.planes: + fd = plane.fd info = bufinfos[fd] mv = memoryview(info['map']) - start = fb.offset(i) - end = fb.offset(i) + fb.length(i) + start = plane.offset + end = plane.offset + plane.length mv = mv[start:end] From patchwork Mon May 30 14:27:14 2022 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Tomi Valkeinen X-Patchwork-Id: 16115 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 E84B6C3272 for ; Mon, 30 May 2022 14:27:51 +0000 (UTC) Received: from lancelot.ideasonboard.com (localhost [IPv6:::1]) by lancelot.ideasonboard.com (Postfix) with ESMTP id 864526564D; Mon, 30 May 2022 16:27:51 +0200 (CEST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=libcamera.org; s=mail; t=1653920871; bh=UkzTO1W+wg3gwxN5XqnlVlliRzafRc+gxBtG4GGaOsU=; 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=K0bAJSy/9T83aftfJgfvp2F1JBZ8XcY1Rou3f6Q3clcSTdzTOqulQ/Zrj5/E4IDxm YVoeB35x+tuEHOszjVWyRSX6kwkOoyUYHc1+VXkFATrv+sSenr65h6fBqR+YJbtsWm Of4KpTvgB3xerEyQiirIOImeS2dcAua6wZyzooMOsSM0mBRiwHrUIpE5Zw92B4hC+t DotcW13ihk6twEwlZ4yeATNUpkVkB3gletzjkLAZlBSHrUuCOxI/mgsje/XupKrTxm bBnen4kZQPrcQilHL/7L+2rsb6sLK+outgdkGxu7m9hEsuWBHlYHKHA6Two8oTvFAg tcvsoL14E6m7Q== 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 5E22560411 for ; Mon, 30 May 2022 16:27:43 +0200 (CEST) Authentication-Results: lancelot.ideasonboard.com; dkim=pass (1024-bit key; unprotected) header.d=ideasonboard.com header.i=@ideasonboard.com header.b="OlVcfBir"; 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 BE0821287; Mon, 30 May 2022 16:27:42 +0200 (CEST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=ideasonboard.com; s=mail; t=1653920863; bh=UkzTO1W+wg3gwxN5XqnlVlliRzafRc+gxBtG4GGaOsU=; h=From:To:Cc:Subject:Date:In-Reply-To:References:From; b=OlVcfBir3IORTor3L/JhhasXzh+BrpL3dUrLcD/RF2ZwaL+kx03V5vp5le8Gf2+uj tro5IQPnNJjxz4EFW8vC/xE9H1dNxQgGsRUUO3kh1p6uPmlMpEkN8+tvvNxyFXFKf1 +sAMZIoQrVpoFv3G622jD0Yzr1/+jOa+pMp7K3Q0= To: libcamera-devel@lists.libcamera.org, David Plowman , Kieran Bingham , Laurent Pinchart , Jacopo Mondi Date: Mon, 30 May 2022 17:27:14 +0300 Message-Id: <20220530142722.57618-9-tomi.valkeinen@ideasonboard.com> X-Mailer: git-send-email 2.34.1 In-Reply-To: <20220530142722.57618-1-tomi.valkeinen@ideasonboard.com> References: <20220530142722.57618-1-tomi.valkeinen@ideasonboard.com> MIME-Version: 1.0 Subject: [libcamera-devel] [PATCH v4 08/16] py: MappedFrameBuffer: Support non-contextmanager use 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" Implement non-contextmanager use to MappedFrameBuffer so that we can either: with MappedFrameBuffer(fb) as mfb: ... or mfb = MappedFrameBuffer(fb) mfb.mmap() ... mfb.munmap() While at it, improve the error handling a bit. Note that the mmap() returns self. In other words, one can do this: mfb = MappedFrameBuffer(fb).mmap() ... mfb.munmap() Signed-off-by: Tomi Valkeinen Reviewed-by: Laurent Pinchart --- src/py/libcamera/utils/MappedFrameBuffer.py | 22 ++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/src/py/libcamera/utils/MappedFrameBuffer.py b/src/py/libcamera/utils/MappedFrameBuffer.py index a8502d51..c300a6d7 100644 --- a/src/py/libcamera/utils/MappedFrameBuffer.py +++ b/src/py/libcamera/utils/MappedFrameBuffer.py @@ -10,8 +10,19 @@ class MappedFrameBuffer: """ def __init__(self, fb: libcamera.FrameBuffer): self.__fb = fb + self.__planes = () + self.__maps = () def __enter__(self): + return self.mmap() + + def __exit__(self, exc_type, exc_value, exc_traceback): + self.munmap() + + def mmap(self): + if self.__planes: + raise RuntimeError('MappedFrameBuffer already mmapped') + import os import mmap @@ -68,14 +79,23 @@ class MappedFrameBuffer: return self - def __exit__(self, exc_type, exc_value, exc_traceback): + def munmap(self): + if not self.__planes: + raise RuntimeError('MappedFrameBuffer not mmapped') + for p in self.__planes: p.release() for mm in self.__maps: mm.close() + self.__planes = () + self.__maps = () + @property def planes(self) -> Tuple[memoryview, ...]: """memoryviews for the planes""" + if not self.__planes: + raise RuntimeError('MappedFrameBuffer not mmapped') + return self.__planes From patchwork Mon May 30 14:27:15 2022 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Tomi Valkeinen X-Patchwork-Id: 16116 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 AEEC1C3273 for ; Mon, 30 May 2022 14:27:52 +0000 (UTC) Received: from lancelot.ideasonboard.com (localhost [IPv6:::1]) by lancelot.ideasonboard.com (Postfix) with ESMTP id 269066564B; Mon, 30 May 2022 16:27:52 +0200 (CEST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=libcamera.org; s=mail; t=1653920872; bh=qGmTmx2JrrUbwgjByhpegUxVXapfx4GmMBhaDImje0Q=; 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=FlmEcWg0F/HSu3awjNc2wfUmV/KMlWpQVnzJeqMK/ShsIMSck71lo5G91tGeSJtkl KDk1uXp/ypiZ1kI1rV1elDJr7NM4CLds5BSrTg7VyIJvAoI3gG6N3eyT85FaSEKbuW KC8wC9SrHsrLvowazbIPG0NWAy7YJq2G/igypKHIzec1Y149PCxEoewsyVBzqT+TPd pZgzRF3bnDXCtasGqitbbWmf2vbTY6AMZIAM4tMsY5jImXjV18PiJijsm3j4hAhNHC OL0RpcY6gYZqVXG4gApbKCcm3HAnbs4915xqjiQkztWr4CDIZSsKavJReebqvTly/D 09sGNoiA7JIfQ== Received: from perceval.ideasonboard.com (perceval.ideasonboard.com [213.167.242.64]) by lancelot.ideasonboard.com (Postfix) with ESMTPS id D7CC061FB7 for ; Mon, 30 May 2022 16:27:43 +0200 (CEST) Authentication-Results: lancelot.ideasonboard.com; dkim=pass (1024-bit key; unprotected) header.d=ideasonboard.com header.i=@ideasonboard.com header.b="sCVraaB+"; 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 4768E128D; Mon, 30 May 2022 16:27:43 +0200 (CEST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=ideasonboard.com; s=mail; t=1653920863; bh=qGmTmx2JrrUbwgjByhpegUxVXapfx4GmMBhaDImje0Q=; h=From:To:Cc:Subject:Date:In-Reply-To:References:From; b=sCVraaB++KT/wJHxxBiWVuA5x3Xp+npB1rkBNk1ATl9lggd7WYdJc+UPEbp0Mi9N6 p1X0lj+cadRb9Lhw0fbgfi0vZd4KjMk8racaRhqrdAPKLGzSAIf1W+N+NlaPG5Scjk Mb+pMeOJxJRUkDRJQmxDdRi/afqop/yyaQsp9qH0= To: libcamera-devel@lists.libcamera.org, David Plowman , Kieran Bingham , Laurent Pinchart , Jacopo Mondi Date: Mon, 30 May 2022 17:27:15 +0300 Message-Id: <20220530142722.57618-10-tomi.valkeinen@ideasonboard.com> X-Mailer: git-send-email 2.34.1 In-Reply-To: <20220530142722.57618-1-tomi.valkeinen@ideasonboard.com> References: <20220530142722.57618-1-tomi.valkeinen@ideasonboard.com> MIME-Version: 1.0 Subject: [libcamera-devel] [PATCH v4 09/16] py: MappedFrameBuffer: Add 'fb' property 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 'fb' property to expose the underlying FrameBuffer. Signed-off-by: Tomi Valkeinen Reviewed-by: Laurent Pinchart --- src/py/libcamera/utils/MappedFrameBuffer.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/py/libcamera/utils/MappedFrameBuffer.py b/src/py/libcamera/utils/MappedFrameBuffer.py index c300a6d7..329e51fa 100644 --- a/src/py/libcamera/utils/MappedFrameBuffer.py +++ b/src/py/libcamera/utils/MappedFrameBuffer.py @@ -99,3 +99,7 @@ class MappedFrameBuffer: raise RuntimeError('MappedFrameBuffer not mmapped') return self.__planes + + @property + def fb(self): + return self.__fb From patchwork Mon May 30 14:27:16 2022 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Tomi Valkeinen X-Patchwork-Id: 16117 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 87ED7C3274 for ; Mon, 30 May 2022 14:27:53 +0000 (UTC) Received: from lancelot.ideasonboard.com (localhost [IPv6:::1]) by lancelot.ideasonboard.com (Postfix) with ESMTP id E82E165645; Mon, 30 May 2022 16:27:52 +0200 (CEST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=libcamera.org; s=mail; t=1653920872; bh=n+W+jX0Fs4ev88/WDptxjT/u3x5ep0kwU/KdgsI3Z4w=; 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=B47wvHVEUC25FmY6oiuctty3PDy2IsxW/XtZ1Yvvz2oe0V5NVXu9P5QNh7aljnuU4 2pSTmVJl18NbXoDah5Qa1UTeCFW+vpGlCpwh969MKsNEErVkDfeKBt9cpMTFPIGuaM 07gPhiuUQ+pLMCl1dWndHGpu90iAWkITHHdB+X27MVJMjiImDT1+eYYtdb1TK0ZJa/ +Ediv6374K+4kIts8m88ohAQaWOwiOr7EViSui04ebX1EKkITLkhsmVODfSfTYzn26 jfMX5hLlEQj7y/JUW9wEQrGS5vfBUtFFAcMKv7x3+LSF4LEkth40+PGDo7+3b7HBYm CdQOaobFQ/nlQ== 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 6841165647 for ; Mon, 30 May 2022 16:27:44 +0200 (CEST) Authentication-Results: lancelot.ideasonboard.com; dkim=pass (1024-bit key; unprotected) header.d=ideasonboard.com header.i=@ideasonboard.com header.b="WlFi6YWq"; 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 CC021110E; Mon, 30 May 2022 16:27:43 +0200 (CEST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=ideasonboard.com; s=mail; t=1653920864; bh=n+W+jX0Fs4ev88/WDptxjT/u3x5ep0kwU/KdgsI3Z4w=; h=From:To:Cc:Subject:Date:In-Reply-To:References:From; b=WlFi6YWq8VkpYZMmY5s4KIftdrNBW+1c5n+c1LO6igBRMwy1DeYjsTKRnrRFCs+el XkNz0172pLjMWcMDo5AJtC/J1Z9YZ6NAy0MV224pN0w4N1ertWIUSAInVBT+DPoc3f lCs7yDe6viHlHRf0RtUXQi6ghQm38l/ZTdrvXexQ= To: libcamera-devel@lists.libcamera.org, David Plowman , Kieran Bingham , Laurent Pinchart , Jacopo Mondi Date: Mon, 30 May 2022 17:27:16 +0300 Message-Id: <20220530142722.57618-11-tomi.valkeinen@ideasonboard.com> X-Mailer: git-send-email 2.34.1 In-Reply-To: <20220530142722.57618-1-tomi.valkeinen@ideasonboard.com> References: <20220530142722.57618-1-tomi.valkeinen@ideasonboard.com> MIME-Version: 1.0 Subject: [libcamera-devel] [PATCH v4 10/16] py: cam: cam_qt: mmap the fbs only once 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 doing an mmap and munmap every time a Request is complete, mmap all the buffers once at the start of the program. Signed-off-by: Tomi Valkeinen Reviewed-by: Laurent Pinchart --- src/py/cam/cam_qt.py | 39 +++++++++++++++++++++++++-------------- 1 file changed, 25 insertions(+), 14 deletions(-) diff --git a/src/py/cam/cam_qt.py b/src/py/cam/cam_qt.py index b6412bdf..c1723b44 100644 --- a/src/py/cam/cam_qt.py +++ b/src/py/cam/cam_qt.py @@ -52,6 +52,16 @@ class QtRenderer: self.windows = windows + buf_mmap_map = {} + + for ctx in self.contexts: + for stream in ctx.streams: + for buf in ctx.allocator.buffers(stream): + mfb = libcamera.utils.MappedFrameBuffer(buf).mmap() + buf_mmap_map[buf] = mfb + + self.buf_mmap_map = buf_mmap_map + def run(self): camnotif = QtCore.QSocketNotifier(self.cm.event_fd, QtCore.QSocketNotifier.Read) camnotif.activated.connect(lambda _: self.readcam()) @@ -81,7 +91,9 @@ class QtRenderer: for stream, fb in buffers.items(): wnd = next(wnd for wnd in self.windows if wnd.stream == stream) - wnd.handle_request(stream, fb) + mfb = self.buf_mmap_map[fb] + + wnd.handle_request(stream, mfb) self.state.request_processed(ctx, req) @@ -145,26 +157,25 @@ class MainWindow(QtWidgets.QWidget): controlsLayout.addStretch() - def buf_to_qpixmap(self, stream, fb): - with libcamera.utils.MappedFrameBuffer(fb) as mfb: - cfg = stream.configuration + def buf_to_qpixmap(self, stream, mfb): + cfg = stream.configuration - if cfg.pixel_format == libcam.formats.MJPEG: - pix = QtGui.QPixmap(cfg.size.width, cfg.size.height) - pix.loadFromData(mfb.planes[0]) - else: - rgb = mfb_to_rgb(mfb, cfg) - if rgb is None: - raise Exception('Format not supported: ' + cfg.pixel_format) + if cfg.pixel_format == libcam.formats.MJPEG: + pix = QtGui.QPixmap(cfg.size.width, cfg.size.height) + pix.loadFromData(mfb.planes[0]) + else: + rgb = mfb_to_rgb(mfb, cfg) + if rgb is None: + raise Exception('Format not supported: ' + cfg.pixel_format) - pix = rgb_to_pix(rgb) + pix = rgb_to_pix(rgb) return pix - def handle_request(self, stream, fb): + def handle_request(self, stream, mfb): ctx = self.ctx - pix = self.buf_to_qpixmap(stream, fb) + pix = self.buf_to_qpixmap(stream, mfb) self.label.setPixmap(pix) self.frameLabel.setText('Queued: {}\nDone: {}\nFps: {:.2f}' From patchwork Mon May 30 14:27:17 2022 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Tomi Valkeinen X-Patchwork-Id: 16118 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 F1E4DC3275 for ; Mon, 30 May 2022 14:27:53 +0000 (UTC) Received: from lancelot.ideasonboard.com (localhost [IPv6:::1]) by lancelot.ideasonboard.com (Postfix) with ESMTP id 951AB6564C; Mon, 30 May 2022 16:27:53 +0200 (CEST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=libcamera.org; s=mail; t=1653920873; bh=nImSIZ5uytByW9XxCoN6/Shcks/45bpyiqqgaYQ2cEE=; 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=sRRmP3Vhi4a5274RMh8fr3NfvNB2tDI3nfhaj8tEOtuIfWir825BOzQpNuVoBizt4 fkqqBR93XWg5TbfMx22OPZx/mV752y7qHyPBJItk7KRfKknmZbBKvYqWA5unAklWcA x69HXc5ITf/EAyUkYjrJ+2XU7AUaRF7Z4KhtiYYEI+YUqhTA18VJMCSwMO/P+LtaJQ N1knvVRUT6HV4odfn0Q19skw2gJlrOE1zF37AqeYnDlx9QK5sUcmkXEyoj38jUjMk7 tr6vGnfAGEj4pkmLsSFgE0+1jhzXn9bSeD9G63NhfEl+Mnpp+MAA3VhkVAStzRCDn2 PR04vwQwxMg5A== 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 D93F465641 for ; Mon, 30 May 2022 16:27:44 +0200 (CEST) Authentication-Results: lancelot.ideasonboard.com; dkim=pass (1024-bit key; unprotected) header.d=ideasonboard.com header.i=@ideasonboard.com header.b="gNfQ1dDu"; 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 511E7145B; Mon, 30 May 2022 16:27:44 +0200 (CEST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=ideasonboard.com; s=mail; t=1653920864; bh=nImSIZ5uytByW9XxCoN6/Shcks/45bpyiqqgaYQ2cEE=; h=From:To:Cc:Subject:Date:In-Reply-To:References:From; b=gNfQ1dDumnu8RcJZQYZ16P2wPLcPxFW8FFC8iXM61kpDaLHz3Jk5olIo1ceF3RCoD rAYaDo/bYNmVleWZI2wDb21+fQfjzHcWEMLWW2O1LVx2OnnGErxQ3Y1XGH2ZRyg370 5HEEgk3+DekvjwNBH2lsAboI3Xfd+k+8bZkbuj+I= To: libcamera-devel@lists.libcamera.org, David Plowman , Kieran Bingham , Laurent Pinchart , Jacopo Mondi Date: Mon, 30 May 2022 17:27:17 +0300 Message-Id: <20220530142722.57618-12-tomi.valkeinen@ideasonboard.com> X-Mailer: git-send-email 2.34.1 In-Reply-To: <20220530142722.57618-1-tomi.valkeinen@ideasonboard.com> References: <20220530142722.57618-1-tomi.valkeinen@ideasonboard.com> MIME-Version: 1.0 Subject: [libcamera-devel] [PATCH v4 11/16] py: merge read_event() and get_ready_requests() 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" We always call CameraManager.read_event() and CameraManager.get_ready_requests(), so to simplify the use merge the read_event() into the get_ready_requests(). This has the side effect that get_ready_requests() will now block if there is no event ready. If we ever need to call get_ready_requests() in a polling manner we will need a new function which behaves differently. However, afaics the only sensible way to manage the event loop is to use select/poll on the eventfd and then call get_ready_requests() once, which is the use case what the current merged function supports. Signed-off-by: Tomi Valkeinen Reviewed-by: Laurent Pinchart Reviewed-by: Laurent Pinchart --- src/py/cam/cam.py | 2 -- src/py/libcamera/py_main.cpp | 7 ++----- test/py/unittests.py | 4 ---- 3 files changed, 2 insertions(+), 11 deletions(-) diff --git a/src/py/cam/cam.py b/src/py/cam/cam.py index bf8529d9..2ae89fa8 100755 --- a/src/py/cam/cam.py +++ b/src/py/cam/cam.py @@ -243,8 +243,6 @@ class CaptureState: # Called from renderer when there is a libcamera event def event_handler(self): try: - self.cm.read_event() - reqs = self.cm.get_ready_requests() for req in reqs: diff --git a/src/py/libcamera/py_main.cpp b/src/py/libcamera/py_main.cpp index fcf009f0..505cc3dc 100644 --- a/src/py/libcamera/py_main.cpp +++ b/src/py/libcamera/py_main.cpp @@ -213,15 +213,12 @@ PYBIND11_MODULE(_libcamera, m) return gEventfd; }) - .def("read_event", [](CameraManager &) { + .def("get_ready_requests", [](CameraManager &) { uint8_t buf[8]; - int ret = read(gEventfd, buf, 8); - if (ret != 8) + if (read(gEventfd, buf, 8) != 8) throw std::system_error(errno, std::generic_category()); - }) - .def("get_ready_requests", [](CameraManager &) { std::vector v; { diff --git a/test/py/unittests.py b/test/py/unittests.py index 33b35a0a..9adc4337 100755 --- a/test/py/unittests.py +++ b/test/py/unittests.py @@ -210,8 +210,6 @@ class SimpleCaptureMethods(CameraTesterBase): reqs = [] while True: - cm.read_event() - ready_reqs = cm.get_ready_requests() reqs += ready_reqs @@ -283,8 +281,6 @@ class SimpleCaptureMethods(CameraTesterBase): while running: events = sel.select() for key, _ in events: - cm.read_event() - ready_reqs = cm.get_ready_requests() reqs += ready_reqs From patchwork Mon May 30 14:27:18 2022 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Tomi Valkeinen X-Patchwork-Id: 16119 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 B9C12C3276 for ; Mon, 30 May 2022 14:27:54 +0000 (UTC) Received: from lancelot.ideasonboard.com (localhost [IPv6:::1]) by lancelot.ideasonboard.com (Postfix) with ESMTP id 4096C65653; Mon, 30 May 2022 16:27:54 +0200 (CEST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=libcamera.org; s=mail; t=1653920874; bh=O4WPfISxap1qo2Aw7PecJQiEt5HvyOlR1XhKIk0NQgk=; 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=PeSLrdIJ2SafyY0G2v5A7+1cAdYSkA2qzZw5eHZpNUMLdlygj1/89ilcnn05D7BXk xwiDLUYwCKPlvu+LPDhs8x3cuiYhqSwSYJpYsg1QWo+HECoE4GzpcZMrw1nuRgfYUL ndjf5Idqr94krknjdoZh5xnSLi1kGzOzcWTB3ktI4Lxac8GW2p9jmoDzYRr8c/65IV t7kUDngCV1FmkqYERnN5OemQ9gYODlNjrh4mYqlQU5Fx1FoSWE4yFdF4ycBPu8o285 fOvByihps7OABQyRQ2tcEVaod4onA0DyFA/a177QdarnnI0ceCi3e/HqhCNytOMnKo zgA8KuaGLKYNg== 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 68E3965635 for ; Mon, 30 May 2022 16:27:45 +0200 (CEST) Authentication-Results: lancelot.ideasonboard.com; dkim=pass (1024-bit key; unprotected) header.d=ideasonboard.com header.i=@ideasonboard.com header.b="FoZ1ud/v"; 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 D20A718ED; Mon, 30 May 2022 16:27:44 +0200 (CEST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=ideasonboard.com; s=mail; t=1653920865; bh=O4WPfISxap1qo2Aw7PecJQiEt5HvyOlR1XhKIk0NQgk=; h=From:To:Cc:Subject:Date:In-Reply-To:References:From; b=FoZ1ud/vMZlI/ZIBPTCpHAtqVDtcLK/QdGpamOq3SVMtber7Ual+LKB7a/o1o+hBU tGbNIwg0QdkdUS+daN8WMVM4mWNpX8+UEBnYXtdOKwugt/ndUgR8PM24mnriQma76a 3l60mep0OaYfhNCywLTOukIyShQwrz9GnJK6a0xI= To: libcamera-devel@lists.libcamera.org, David Plowman , Kieran Bingham , Laurent Pinchart , Jacopo Mondi Date: Mon, 30 May 2022 17:27:18 +0300 Message-Id: <20220530142722.57618-13-tomi.valkeinen@ideasonboard.com> X-Mailer: git-send-email 2.34.1 In-Reply-To: <20220530142722.57618-1-tomi.valkeinen@ideasonboard.com> References: <20220530142722.57618-1-tomi.valkeinen@ideasonboard.com> MIME-Version: 1.0 Subject: [libcamera-devel] [PATCH v4 12/16] Documentation: Add python-bindings.rst 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 basic doc for the Python bindings. While not really proper documentation yet, the file and the examples should give enough guidance for users who are somewhat familiar with libcamera. Signed-off-by: Tomi Valkeinen Reviewed-by: Laurent Pinchart --- Documentation/index.rst | 1 + Documentation/meson.build | 1 + Documentation/python-bindings.rst | 70 +++++++++++++++++++++++++++++++ 3 files changed, 72 insertions(+) create mode 100644 Documentation/python-bindings.rst diff --git a/Documentation/index.rst b/Documentation/index.rst index 0ee10044..43d8b017 100644 --- a/Documentation/index.rst +++ b/Documentation/index.rst @@ -22,3 +22,4 @@ Environment variables Sensor driver requirements Lens driver requirements + Python Bindings diff --git a/Documentation/meson.build b/Documentation/meson.build index 8e2eacc6..7695bcb1 100644 --- a/Documentation/meson.build +++ b/Documentation/meson.build @@ -67,6 +67,7 @@ if sphinx.found() 'guides/tracing.rst', 'index.rst', 'lens_driver_requirements.rst', + 'python-bindings.rst', 'sensor_driver_requirements.rst', '../README.rst', ] diff --git a/Documentation/python-bindings.rst b/Documentation/python-bindings.rst new file mode 100644 index 00000000..cfa9ec10 --- /dev/null +++ b/Documentation/python-bindings.rst @@ -0,0 +1,70 @@ +.. SPDX-License-Identifier: CC-BY-SA-4.0 + +.. _python-bindings: + +Python Bindings for libcamera +============================= + +*WARNING* The bindings are under work, and the API will change. + +Differences to the C++ API +-------------------------- + +As a rule of thumb the bindings try to follow the C++ API when possible. This +chapter lists the differences. + +Mostly these differences fall under two categories: + +1. Differences caused by the inherent differences between C++ and Python. +These differences are usually caused by the use of threads or differences in +C++ vs Python memory management. + +2. Differences caused by the code being work-in-progress. It's not always +trivial to create a binding in a satisfying way, and the current bindings +contain simplified versions of the C++ API just to get forward. These +differences are expected to eventually go away. + +Coding Style +------------ + +The C++ code for the bindings follows the libcamera coding style as much as +possible. Note that the indentation does not quite follow the clang-format +style, as clang-format makes a mess of the style used. + +The API visible to the Python side follows the Python style as much as possible. + +This means that e.g. ``Camera::generateConfiguration`` maps to +``Camera.generate_configuration``. + +CameraManager +------------- + +The Python API provides a singleton CameraManager via ``CameraManager.singleton()``. +There is no need to start or stop the CameraManager. + +Handling Completed Requests +--------------------------- + +The Python bindings do not expose the ``Camera::requestCompleted`` signal +directly as the signal is invoked from another thread and it has real-time +constraints. Instead the bindings will internally queue the completed +requests and use an eventfd to inform the user that there are completed +requests. + +The user can wait on the eventfd, and upon getting an event, use +``CameraManager.get_ready_requests()`` to clear the eventfd event and to get +the completed requests. + +Controls & Properties +--------------------- + +The classes related to controls and properties are rather complex to implement +directly in the Python bindings. There are some simplifications on the Python +bindings: + +- There is no ControlValue class. Python objects are automatically converted + to ControlValues and vice versa. +- There is no ControlList class. A python dict with ControlId keys and python + object values is used instead. +- There is no ControlInfoMap class. A python dict with ControlId keys and + ControlInfo values is used instead. From patchwork Mon May 30 14:27:19 2022 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Tomi Valkeinen X-Patchwork-Id: 16120 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 49A24C3277 for ; Mon, 30 May 2022 14:27:55 +0000 (UTC) Received: from lancelot.ideasonboard.com (localhost [IPv6:::1]) by lancelot.ideasonboard.com (Postfix) with ESMTP id AAB3A65696; Mon, 30 May 2022 16:27:54 +0200 (CEST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=libcamera.org; s=mail; t=1653920874; bh=VzVRfN3S0oyzifH7QvzuO2LvslqT4zU0t3q4tuchhwY=; 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=LltNGoTlilEP+Or2y5dfkD+B/gOqO7MRyBDM1eMxhq+Dh3zESnAG1ryI9GGXSYmNa bW0oKQb8flfYb1iA96PypU7mkqOb/RBxRm3BRAKsmFS9zLOW+WVTix5gocpiZXljgT 3hXs+Rh1PTgymgKKLONfOm4brUQQ1/uyhwlVfefPhDtpKEoSUR3Xdcew67ezGyAs79 nCLM1/NYxOk6nTXByoHxTpOLISOvf5Z/AlBAXTfn3VXGR0TR39KWTpwtmD2Yn2Kgtn Jx7PZ4Uj1krqfWAJ7WLsijoEcZKskSEGFDcabwiHGf5Y/nFXaNqvLHdOF0EzlhgJLZ hsgwyYd+dKw+w== Received: from perceval.ideasonboard.com (perceval.ideasonboard.com [213.167.242.64]) by lancelot.ideasonboard.com (Postfix) with ESMTPS id E5F1165632 for ; Mon, 30 May 2022 16:27:45 +0200 (CEST) Authentication-Results: lancelot.ideasonboard.com; dkim=pass (1024-bit key; unprotected) header.d=ideasonboard.com header.i=@ideasonboard.com header.b="I5j8esfG"; 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 587AC18F6; Mon, 30 May 2022 16:27:45 +0200 (CEST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=ideasonboard.com; s=mail; t=1653920865; bh=VzVRfN3S0oyzifH7QvzuO2LvslqT4zU0t3q4tuchhwY=; h=From:To:Cc:Subject:Date:In-Reply-To:References:From; b=I5j8esfGd7L+tzmgGKQBmR2tiAonI6WWBHwZLQzwhNfgkEd7059wSfRvCSQmZ9O0d suQuWog7JpNk3p0Vc08oWxzO7mLYRGAEquceTB8QTGU/4w/kdQQ+mE1+nsyCcn9NII GQptkTDimMhAxKW3Pi+ZOBqvOW/GPAE9jyMfpevU= To: libcamera-devel@lists.libcamera.org, David Plowman , Kieran Bingham , Laurent Pinchart , Jacopo Mondi Date: Mon, 30 May 2022 17:27:19 +0300 Message-Id: <20220530142722.57618-14-tomi.valkeinen@ideasonboard.com> X-Mailer: git-send-email 2.34.1 In-Reply-To: <20220530142722.57618-1-tomi.valkeinen@ideasonboard.com> References: <20220530142722.57618-1-tomi.valkeinen@ideasonboard.com> MIME-Version: 1.0 Subject: [libcamera-devel] [PATCH v4 13/16] py: examples: Add simple-capture.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 an example to showcase the more-or-less minimal capture case. Signed-off-by: Tomi Valkeinen Reviewed-by: Jacopo Mondi --- src/py/examples/simple-capture.py | 162 ++++++++++++++++++++++++++++++ 1 file changed, 162 insertions(+) create mode 100755 src/py/examples/simple-capture.py diff --git a/src/py/examples/simple-capture.py b/src/py/examples/simple-capture.py new file mode 100755 index 00000000..ff8cef4f --- /dev/null +++ b/src/py/examples/simple-capture.py @@ -0,0 +1,162 @@ +#!/usr/bin/env python3 + +# SPDX-License-Identifier: BSD-3-Clause +# Copyright (C) 2022, Tomi Valkeinen + +# A simple capture example showing: +# - How to setup the camera +# - Capture certain number of frames in a blocking manner +# - How to stop the camera +# +# This simple example is, in many ways, too simple. The purpose of the example +# is to introduce the concepts. A more realistic example is given in +# simple-continuous-capture.py. + +import argparse +import libcamera as libcam +import sys + +# Number of frames to capture +TOTAL_FRAMES = 30 + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument('-c', '--camera', type=str, default='1', + help='Camera index number (starting from 1) or part of the name') + parser.add_argument('-f', '--format', type=str, help='Pixel format') + parser.add_argument('-s', '--size', type=str, help='Size ("WxH")') + args = parser.parse_args() + + cm = libcam.CameraManager.singleton() + + try: + if args.camera.isnumeric(): + cam_idx = int(args.camera) + cam = next((cam for i, cam in enumerate(cm.cameras) if i + 1 == cam_idx)) + else: + cam = next((cam for cam in cm.cameras if args.camera in cam.id)) + except Exception: + print(f'Failed to find camera "{args.camera}"') + return -1 + + # Acquire the camera for our use + + ret = cam.acquire() + assert ret == 0 + + # Configure the camera + + cam_config = cam.generate_configuration([libcam.StreamRole.Viewfinder]) + + stream_config = cam_config.at(0) + + print(f'Capturing {TOTAL_FRAMES} frames with {stream_config}') + + if args.format: + fmt = libcam.PixelFormat(args.format) + stream_config.pixel_format = fmt + + if args.size: + w, h = [int(v) for v in args.size.split('x')] + stream_config.size = libcam.Size(w, h) + + ret = cam.configure(cam_config) + assert ret == 0 + + stream = stream_config.stream + + # Allocate the buffers for capture + + allocator = libcam.FrameBufferAllocator(cam) + ret = allocator.allocate(stream) + assert ret > 0 + + num_bufs = len(allocator.buffers(stream)) + + # Create the requests and assign a buffer for each request + + reqs = [] + for i in range(num_bufs): + # Use the buffer index as the cookie + req = cam.create_request(i) + + buffer = allocator.buffers(stream)[i] + ret = req.add_buffer(stream, buffer) + assert ret == 0 + + reqs.append(req) + + # Start the camera + + ret = cam.start() + assert ret == 0 + + # frames_queued and frames_done track the number of frames queued and done + + frames_queued = 0 + frames_done = 0 + + # Queue the requests to the camera + + for req in reqs: + ret = cam.queue_request(req) + assert ret == 0 + frames_queued += 1 + + # The main loop. Wait for the queued Requests to complete, process them, + # and re-queue them again. + + while frames_done < TOTAL_FRAMES: + # cm.get_ready_requests() blocks until there is an event and returns + # all the ready requests. Here we should almost always get a single + # Request, but in some cases there could be multiple or none. + + reqs = cm.get_ready_requests() + + for req in reqs: + frames_done += 1 + + buffers = req.buffers + + # A ready Request could contain multiple buffers if multiple streams + # were being used. Here we know we only have a single stream, + # and we use next(iter()) to get the first and only buffer. + + assert len(buffers) == 1 + + stream, fb = next(iter(buffers.items())) + + # Here we could process the received buffer. In this example we only + # print a few details below. + + meta = fb.metadata + + print("seq {:3}, bytes {}, frames queued/done {:3}/{:<3}" + .format(meta.sequence, + '/'.join([str(p.bytes_used) for p in meta.planes]), + frames_queued, frames_done)) + + # If we want to capture more frames we need to queue more Requests. + # We could create a totally new Request, but it is more efficient + # to reuse the existing one that we just received. + if frames_queued < TOTAL_FRAMES: + req.reuse() + cam.queue_request(req) + frames_queued += 1 + + # Stop the camera + + ret = cam.stop() + assert ret == 0 + + # Release the camera + + ret = cam.release() + assert ret == 0 + + return 0 + + +if __name__ == '__main__': + sys.exit(main()) From patchwork Mon May 30 14:27:20 2022 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Tomi Valkeinen X-Patchwork-Id: 16121 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 A16B4C3278 for ; Mon, 30 May 2022 14:27:55 +0000 (UTC) Received: from lancelot.ideasonboard.com (localhost [IPv6:::1]) by lancelot.ideasonboard.com (Postfix) with ESMTP id 1C5FF65658; Mon, 30 May 2022 16:27:55 +0200 (CEST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=libcamera.org; s=mail; t=1653920875; bh=aKA4jstH6cyqJmG0C/jiNqrb9//1jSJbxG3xC5syilw=; 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=CJF1SYuh7DtJ1C7ppMd4TSVACJsJBkH1UvKW9wi35HHXCJGKD3vTSk5wbMR0cUAxH G0da889vCgmIKWRTlFJV5zaFONwBmRrQpf+ppc0C1fMf7Jek6OXwq4wSVEXHQ3gWPE vPEJbP+2CCUBovZpx1jvARrMWvsJN/LxT4bRFK4VpqGuR7WGbMAnk4HiQcqG9aCdJW G0YzQhZV6Nnb+r8K4J3hVy9S7FpndFnA95rcJjc2Bpb+xml/qjBg7lN1UBPGWLJ6Xk /cIJp52Qo9+Ra0HXK3asUTyL89iuqtIzlCwqGdJb34hw/c6byy5zSo4QeburWFu+1F IyOTkyAqN/+sA== Received: from perceval.ideasonboard.com (perceval.ideasonboard.com [213.167.242.64]) by lancelot.ideasonboard.com (Postfix) with ESMTPS id 6A3DD6564E for ; Mon, 30 May 2022 16:27:46 +0200 (CEST) Authentication-Results: lancelot.ideasonboard.com; dkim=pass (1024-bit key; unprotected) header.d=ideasonboard.com header.i=@ideasonboard.com header.b="jNxXjMiM"; 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 D7EDD6BD; Mon, 30 May 2022 16:27:45 +0200 (CEST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=ideasonboard.com; s=mail; t=1653920866; bh=aKA4jstH6cyqJmG0C/jiNqrb9//1jSJbxG3xC5syilw=; h=From:To:Cc:Subject:Date:In-Reply-To:References:From; b=jNxXjMiMc0yzKjLOA+SvBspo25a6kX4OvcCAvVqaQA2orJUI+E3oxVrUGi5oftRq7 2AOFf1ZM6vlPtMozP0UdTydnf8lDYnRYZYo1FAH47ICV1Y8SbVSwQ00R/x+kX4k6Ek k3KkpyXwzw7LPRyGIogk09xHa2mr+K/vN8UBPnes= To: libcamera-devel@lists.libcamera.org, David Plowman , Kieran Bingham , Laurent Pinchart , Jacopo Mondi Date: Mon, 30 May 2022 17:27:20 +0300 Message-Id: <20220530142722.57618-15-tomi.valkeinen@ideasonboard.com> X-Mailer: git-send-email 2.34.1 In-Reply-To: <20220530142722.57618-1-tomi.valkeinen@ideasonboard.com> References: <20220530142722.57618-1-tomi.valkeinen@ideasonboard.com> MIME-Version: 1.0 Subject: [libcamera-devel] [PATCH v4 14/16] py: examples: Add simple-continuous-capture.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 slightly more complex, and I think a more realistic, example, where the script reacts to events and re-queues the buffers. Signed-off-by: Tomi Valkeinen Reviewed-by: Jacopo Mondi --- src/py/examples/simple-continuous-capture.py | 189 +++++++++++++++++++ 1 file changed, 189 insertions(+) create mode 100755 src/py/examples/simple-continuous-capture.py diff --git a/src/py/examples/simple-continuous-capture.py b/src/py/examples/simple-continuous-capture.py new file mode 100755 index 00000000..d0f8a7e9 --- /dev/null +++ b/src/py/examples/simple-continuous-capture.py @@ -0,0 +1,189 @@ +#!/usr/bin/env python3 + +# SPDX-License-Identifier: BSD-3-Clause +# Copyright (C) 2022, Tomi Valkeinen + +# A simple capture example extending the simple-capture.py example: +# - Capture frames using events from multiple cameras +# - Listening events from stdin to exit the application +# - Memory mapping the frames and calculating CRC + +import binascii +import libcamera as libcam +import libcamera.utils +import selectors +import sys + + +# A container class for our state per camera +class CameraCaptureContext: + idx: int + cam: libcam.Camera + reqs: list[libcam.Request] + mfbs: dict[libcam.FrameBuffer, libcamera.utils.MappedFrameBuffer] + + def __init__(self, cam, idx): + # Acquire the camera for our use + + ret = cam.acquire() + assert ret == 0 + + # Configure the camera + + cam_config = cam.generate_configuration([libcam.StreamRole.Viewfinder]) + + stream_config = cam_config.at(0) + + ret = cam.configure(cam_config) + assert ret == 0 + + stream = stream_config.stream + + # Allocate the buffers for capture + + allocator = libcam.FrameBufferAllocator(cam) + ret = allocator.allocate(stream) + assert ret > 0 + + num_bufs = len(allocator.buffers(stream)) + + print(f'cam{idx} ({cam.id}): capturing {num_bufs} buffers with {stream_config}') + + # Create the requests and assign a buffer for each request + + reqs = [] + for i in range(num_bufs): + # Use the buffer index as the "cookie" + req = cam.create_request(idx) + + buffer = allocator.buffers(stream)[i] + ret = req.add_buffer(stream, buffer) + assert ret == 0 + + reqs.append(req) + + self.idx = idx + self.cam = cam + self.reqs = reqs + self.mfbs = dict([(fb, libcamera.utils.MappedFrameBuffer(fb).mmap()) for fb in allocator.buffers(stream)]) + + def uninit_camera(self): + # Stop the camera + + ret = self.cam.stop() + assert ret == 0 + + # Release the camera + + ret = self.cam.release() + assert ret == 0 + + +# A container class for our state +class CaptureContext: + cm: libcam.CameraManager + camera_contexts: list[CameraCaptureContext] = [] + + def handle_camera_event(self): + # cm.get_ready_requests() will not block here, as we know there is an event + # to read. + + reqs = self.cm.get_ready_requests() + + # Process the captured frames + + for req in reqs: + self.handle_request(req) + + return True + + def handle_request(self, req: libcam.Request): + cam_ctx = self.camera_contexts[req.cookie] + + buffers = req.buffers + + assert len(buffers) == 1 + + # A ready Request could contain multiple buffers if multiple streams + # were being used. Here we know we only have a single stream, + # and we use next(iter()) to get the first and only buffer. + + stream, fb = next(iter(buffers.items())) + + # Use the MappedFrameBuffer to access the pixel data with CPU. We calculate + # the crc for each plane. + + mfb = cam_ctx.mfbs[fb] + crcs = [binascii.crc32(p) for p in mfb.planes] + + meta = fb.metadata + + print('cam{:<6} seq {:<6} bytes {:10} CRCs {}' + .format(cam_ctx.idx, + meta.sequence, + '/'.join([str(p.bytes_used) for p in meta.planes]), + crcs)) + + # We want to re-queue the buffer we just handled. Instead of creating + # a new Request, we re-use the old one. We need to call req.reuse() + # to re-initialize the Request before queuing. + + req.reuse() + cam_ctx.cam.queue_request(req) + + def capture(self): + # Queue the requests to the camera + + for cam_ctx in self.camera_contexts: + for req in cam_ctx.reqs: + ret = cam_ctx.cam.queue_request(req) + assert ret == 0 + + # Use Selector to wait for events from the camera and from the keyboard + + sel = selectors.DefaultSelector() + sel.register(sys.stdin, selectors.EVENT_READ, handle_key_event) + sel.register(self.cm.event_fd, selectors.EVENT_READ, lambda: self.handle_camera_event()) + + running = True + + while running: + events = sel.select() + for key, mask in events: + # If the handler return False, we should exit + if not key.data(): + running = False + + +def handle_key_event(): + sys.stdin.readline() + print('Exiting...') + return False + + +def main(): + cm = libcam.CameraManager.singleton() + + ctx = CaptureContext() + ctx.cm = cm + + for idx, cam in enumerate(cm.cameras): + cam_ctx = CameraCaptureContext(cam, idx) + ctx.camera_contexts.append(cam_ctx) + + # Start the cameras + + for cam_ctx in ctx.camera_contexts: + ret = cam_ctx.cam.start() + assert ret == 0 + + ctx.capture() + + for cam_ctx in ctx.camera_contexts: + cam_ctx.uninit_camera() + + return 0 + + +if __name__ == '__main__': + sys.exit(main()) From patchwork Mon May 30 14:27:21 2022 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Tomi Valkeinen X-Patchwork-Id: 16122 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 28816C3279 for ; Mon, 30 May 2022 14:27:56 +0000 (UTC) Received: from lancelot.ideasonboard.com (localhost [IPv6:::1]) by lancelot.ideasonboard.com (Postfix) with ESMTP id 885B165699; Mon, 30 May 2022 16:27:55 +0200 (CEST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=libcamera.org; s=mail; t=1653920875; bh=Dqbd2jWSxJoWb7CMOMAO8z/GpiNtZdD+FqW7WPAxi3c=; 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=z6wK3r1yOvuxvFG+OD61SWygsGLvERHnTa57flEUnlRLzlRA5O6bx/wGCof29i2jH maYBRMpTftLnD0zS0K5g7gvLDMePPLv9cmLdepaPSdSa47juFTcWL6t7cZ83tEwMnd DGXLGX402+nW1NBZawUsQ8YunrRLRzjbtJIbAM+W9A9RdzKkkC7atcFH3tLoJLIleI ZGwRQnFO5gZe0JEZ/Sau/KZyseA+FYyHKpSnQ5oAtNCDh4hmA/e2gB/HQYPYE/yxPf hOYmK4hJVMjXxkD92snqhjNN0U4Tek5EXQs5gjSYhN6pyKqKy92kEaoz0T/GiTPa4d SNBE/Ia33M7vA== 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 EEC6A6563B for ; Mon, 30 May 2022 16:27:46 +0200 (CEST) Authentication-Results: lancelot.ideasonboard.com; dkim=pass (1024-bit key; unprotected) header.d=ideasonboard.com header.i=@ideasonboard.com header.b="hV2abj3x"; 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 604581918; Mon, 30 May 2022 16:27:46 +0200 (CEST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=ideasonboard.com; s=mail; t=1653920866; bh=Dqbd2jWSxJoWb7CMOMAO8z/GpiNtZdD+FqW7WPAxi3c=; h=From:To:Cc:Subject:Date:In-Reply-To:References:From; b=hV2abj3xlK2uHgBShu00YOf06XVwNySpnMh63NQg7Ya73HPm+i75QsNy75s8S0UHY umyM9C3BvEZpQDkCAi/zpwqzx+EHpKUuMsVKFXZeKZ4cZWaEetllAjIaSF5r6th4+R bxudVSPvaR20DP02aNDx1oA6sjhsDn7QYg/6nbSo= To: libcamera-devel@lists.libcamera.org, David Plowman , Kieran Bingham , Laurent Pinchart , Jacopo Mondi Date: Mon, 30 May 2022 17:27:21 +0300 Message-Id: <20220530142722.57618-16-tomi.valkeinen@ideasonboard.com> X-Mailer: git-send-email 2.34.1 In-Reply-To: <20220530142722.57618-1-tomi.valkeinen@ideasonboard.com> References: <20220530142722.57618-1-tomi.valkeinen@ideasonboard.com> MIME-Version: 1.0 Subject: [libcamera-devel] [PATCH v4 15/16] py: examples: Add itest.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 small tool which gives the user an IPython based interactive shell with libcamera capturing in the background. Can be used to study and test small things with libcamera. Signed-off-by: Tomi Valkeinen --- src/py/examples/itest.py | 195 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 195 insertions(+) create mode 100755 src/py/examples/itest.py diff --git a/src/py/examples/itest.py b/src/py/examples/itest.py new file mode 100755 index 00000000..9c97649d --- /dev/null +++ b/src/py/examples/itest.py @@ -0,0 +1,195 @@ +#!/usr/bin/env python3 + +# SPDX-License-Identifier: BSD-3-Clause +# Copyright (C) 2022, Tomi Valkeinen + +# This sets up a single camera, starts a capture loop in a background thread, +# and starts an IPython shell which can be used to study and test libcamera. +# +# Note: This is not an example as such, but more of a developer tool to try +# out things. + +import argparse +import IPython +import libcamera as libcam +import selectors +import sys +import threading + + +def handle_camera_event(ctx): + cm = ctx['cm'] + cam = ctx['cam'] + + # cm.get_ready_requests() will not block here, as we know there is an event + # to read. + + reqs = cm.get_ready_requests() + + assert len(reqs) > 0 + + # Process the captured frames + + for req in reqs: + buffers = req.buffers + + assert len(buffers) == 1 + + # We want to re-queue the buffer we just handled. Instead of creating + # a new Request, we re-use the old one. We need to call req.reuse() + # to re-initialize the Request before queuing. + + req.reuse() + cam.queue_request(req) + + scope = ctx['scope'] + + if 'num_frames' not in scope: + scope['num_frames'] = 0 + + scope['num_frames'] += 1 + + +def capture(ctx): + cm = ctx['cm'] + cam = ctx['cam'] + reqs = ctx['reqs'] + + # Queue the requests to the camera + + for req in reqs: + ret = cam.queue_request(req) + assert ret == 0 + + # Use Selector to wait for events from the camera and from the keyboard + + sel = selectors.DefaultSelector() + sel.register(cm.event_fd, selectors.EVENT_READ, lambda fd: handle_camera_event(ctx)) + + reqs = [] + + while ctx['running']: + events = sel.select() + for key, mask in events: + key.data(key.fileobj) + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument('-c', '--camera', type=str, default='1', + help='Camera index number (starting from 1) or part of the name') + parser.add_argument('-f', '--format', type=str, help='Pixel format') + parser.add_argument('-s', '--size', type=str, help='Size ("WxH")') + args = parser.parse_args() + + cm = libcam.CameraManager.singleton() + + try: + if args.camera.isnumeric(): + cam_idx = int(args.camera) + cam = next((cam for i, cam in enumerate(cm.cameras) if i + 1 == cam_idx)) + else: + cam = next((cam for cam in cm.cameras if args.camera in cam.id)) + except Exception: + print(f'Failed to find camera "{args.camera}"') + return -1 + + # Acquire the camera for our use + + ret = cam.acquire() + assert ret == 0 + + # Configure the camera + + cam_config = cam.generate_configuration([libcam.StreamRole.Viewfinder]) + + stream_config = cam_config.at(0) + + if args.format: + fmt = libcam.PixelFormat(args.format) + stream_config.pixel_format = fmt + + if args.size: + w, h = [int(v) for v in args.size.split('x')] + stream_config.size = libcam.Size(w, h) + + ret = cam.configure(cam_config) + assert ret == 0 + + stream = stream_config.stream + + # Allocate the buffers for capture + + allocator = libcam.FrameBufferAllocator(cam) + ret = allocator.allocate(stream) + assert ret > 0 + + num_bufs = len(allocator.buffers(stream)) + + print(f'Capturing {stream_config} with {num_bufs} buffers from {cam.id}') + + # Create the requests and assign a buffer for each request + + reqs = [] + for i in range(num_bufs): + # Use the buffer index as the "cookie" + req = cam.create_request(i) + + buffer = allocator.buffers(stream)[i] + ret = req.add_buffer(stream, buffer) + assert ret == 0 + + reqs.append(req) + + # Start the camera + + ret = cam.start() + assert ret == 0 + + # Create a scope for the IPython shell + scope = { + 'cm': cm, + 'cam': cam, + 'libcam': libcam, + } + + # Create a simple context shared between the background thread and the main + # thread. + ctx = { + 'running': True, + 'cm': cm, + 'cam': cam, + 'reqs': reqs, + 'scope': scope, + } + + # Note that "In CPython, due to the Global Interpreter Lock, only one thread + # can execute Python code at once". We rely on that here, which is not very + # nice. I am sure an fd-based polling loop could be integrated with IPython, + # somehow, which would allow us to drop the threading. + + t = threading.Thread(target=capture, args=(ctx,)) + t.start() + + IPython.embed(banner1='', exit_msg='', confirm_exit=False, user_ns=scope) + + print('Exiting...') + + ctx['running'] = False + t.join() + + # Stop the camera + + ret = cam.stop() + assert ret == 0 + + # Release the camera + + ret = cam.release() + assert ret == 0 + + return 0 + + +if __name__ == '__main__': + sys.exit(main()) From patchwork Mon May 30 14:27:22 2022 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Tomi Valkeinen X-Patchwork-Id: 16123 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 5EA28C327A for ; Mon, 30 May 2022 14:27:56 +0000 (UTC) Received: from lancelot.ideasonboard.com (localhost [IPv6:::1]) by lancelot.ideasonboard.com (Postfix) with ESMTP id F02426565A; Mon, 30 May 2022 16:27:55 +0200 (CEST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=libcamera.org; s=mail; t=1653920876; bh=masI9k2SFEkID/Qz6UXrafngk6RBE7sHx6zZZMSllPg=; 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=0K/PDAFXxmCEC0t/uXaT0MqYwdd6zDMaP3G24lZELSmrsHKkUcJTB04HEF3wmEYBI Af5mMbbD70xMzyF/ftBW9m6rsj2eYPpsaPdDpt7n6Btq2ruqOMxq8SRqKpl3uTQvTv SeA8K7HzKxfPOmw3wprPWKEYY0Z/lp2HcGgQLNNhw1mfbmN45DFNKibd4tRDs8D8b0 F+OH+ALchFdKtDacQtL0o5qzKvefwLQks9YM90doOzd9XJJiOxSdUefJQ2iKK0R/FJ /yWyMdvtqLB3nNLYNdO8vJWsr4I9Ws58kXAkUDXrQrR6uQFZfgBJ479np7qGOoNyCf zPcXn3MWur0sQ== Received: from perceval.ideasonboard.com (perceval.ideasonboard.com [213.167.242.64]) by lancelot.ideasonboard.com (Postfix) with ESMTPS id 71B1E65652 for ; Mon, 30 May 2022 16:27:47 +0200 (CEST) Authentication-Results: lancelot.ideasonboard.com; dkim=pass (1024-bit key; unprotected) header.d=ideasonboard.com header.i=@ideasonboard.com header.b="qvJrLZcl"; 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 E02F1B90; Mon, 30 May 2022 16:27:46 +0200 (CEST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=ideasonboard.com; s=mail; t=1653920867; bh=masI9k2SFEkID/Qz6UXrafngk6RBE7sHx6zZZMSllPg=; h=From:To:Cc:Subject:Date:In-Reply-To:References:From; b=qvJrLZcl9+7uOw4YaAr0hCu/YyE5WehvVYxIGDM/BR0B/TTMTL9vLX0iWuss1cAag luAJ5/yT+AW7nSqzWpM2IUvk+9kqVehz4VJjcvXv+IdaLIYNX/Jky4W+zaYpbIHJ1n YgAWnG97RetBKWiTnoUqCwl2hW6eE8Evwzp8HAgA= To: libcamera-devel@lists.libcamera.org, David Plowman , Kieran Bingham , Laurent Pinchart , Jacopo Mondi Date: Mon, 30 May 2022 17:27:22 +0300 Message-Id: <20220530142722.57618-17-tomi.valkeinen@ideasonboard.com> X-Mailer: git-send-email 2.34.1 In-Reply-To: <20220530142722.57618-1-tomi.valkeinen@ideasonboard.com> References: <20220530142722.57618-1-tomi.valkeinen@ideasonboard.com> MIME-Version: 1.0 Subject: [libcamera-devel] [PATCH v4 16/16] py: examples: Add simple-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 a Python version of simple-cam from: https://git.libcamera.org/libcamera/simple-cam.git Let's keep this in the libcamera repository until the Python API has stabilized a bit more, and then we could move this to the simple-cam repo. Signed-off-by: Tomi Valkeinen Reviewed-by: Jacopo Mondi Reviewed-by: Laurent Pinchart --- src/py/examples/simple-cam.py | 350 ++++++++++++++++++++++++++++++++++ 1 file changed, 350 insertions(+) create mode 100755 src/py/examples/simple-cam.py diff --git a/src/py/examples/simple-cam.py b/src/py/examples/simple-cam.py new file mode 100755 index 00000000..2b81bb65 --- /dev/null +++ b/src/py/examples/simple-cam.py @@ -0,0 +1,350 @@ +#!/usr/bin/env python3 + +# SPDX-License-Identifier: BSD-3-Clause +# Copyright (C) 2022, Tomi Valkeinen + +# A simple libcamera capture example +# +# This is a python version of simple-cam from: +# https://git.libcamera.org/libcamera/simple-cam.git +# +# \todo Move to simple-cam repository when the Python API has stabilized more + +import libcamera as libcam +import selectors +import sys +import time + +TIMEOUT_SEC = 3 + + +def handle_camera_event(cm): + # cm.get_ready_requests() will not block here, as we know there is an event + # to read. + + reqs = cm.get_ready_requests() + + # Process the captured frames + + for req in reqs: + process_request(req) + + +def process_request(request): + global camera + + print() + + print(f'Request completed: {request}') + + # When a request has completed, it is populated with a metadata control + # list that allows an application to determine various properties of + # the completed request. This can include the timestamp of the Sensor + # capture, or its gain and exposure values, or properties from the IPA + # such as the state of the 3A algorithms. + # + # To examine each request, print all the metadata for inspection. A custom + # application can parse each of these items and process them according to + # its needs. + + requestMetadata = request.metadata + for id, value in requestMetadata.items(): + print(f'\t{id.name} = {value}') + + # Each buffer has its own FrameMetadata to describe its state, or the + # usage of each buffer. While in our simple capture we only provide one + # buffer per request, a request can have a buffer for each stream that + # is established when configuring the camera. + # + # This allows a viewfinder and a still image to be processed at the + # same time, or to allow obtaining the RAW capture buffer from the + # sensor along with the image as processed by the ISP. + + buffers = request.buffers + for _, buffer in buffers.items(): + metadata = buffer.metadata + + # Print some information about the buffer which has completed. + print(f' seq: {metadata.sequence:06} timestamp: {metadata.timestamp} bytesused: ' + + '/'.join([str(p.bytes_used) for p in metadata.planes])) + + # Image data can be accessed here, but the FrameBuffer + # must be mapped by the application + + # Re-queue the Request to the camera. + request.reuse() + camera.queue_request(request) + + +# ---------------------------------------------------------------------------- +# Camera Naming. +# +# Applications are responsible for deciding how to name cameras, and present +# that information to the users. Every camera has a unique identifier, though +# this string is not designed to be friendly for a human reader. +# +# To support human consumable names, libcamera provides camera properties +# that allow an application to determine a naming scheme based on its needs. +# +# In this example, we focus on the location property, but also detail the +# model string for external cameras, as this is more likely to be visible +# information to the user of an externally connected device. +# +# The unique camera ID is appended for informative purposes. +# +def camera_name(camera): + props = camera.properties + location = props.get(libcam.properties.Location, None) + + if location == libcam.properties.LocationEnum.Front: + name = 'Internal front camera' + elif location == libcam.properties.LocationEnum.Back: + name = 'Internal back camera' + elif location == libcam.properties.LocationEnum.External: + name = 'External camera' + if libcam.properties.Model in props: + name += f' "{props[libcam.properties.Model]}"' + else: + name = 'Undefined location' + + name += f' ({camera.id})' + + return name + + +def main(): + global camera + + # -------------------------------------------------------------------- + # Get the Camera Manager. + # + # The Camera Manager is responsible for enumerating all the Camera + # in the system, by associating Pipeline Handlers with media entities + # registered in the system. + # + # The CameraManager provides a list of available Cameras that + # applications can operate on. + # + # There can only be a single CameraManager within any process space. + + cm = libcam.CameraManager.singleton() + + # Just as a test, generate names of the Cameras registered in the + # system, and list them. + + for camera in cm.cameras: + print(f' - {camera_name(camera)}') + + # -------------------------------------------------------------------- + # Camera + # + # Camera are entities created by pipeline handlers, inspecting the + # entities registered in the system and reported to applications + # by the CameraManager. + # + # In general terms, a Camera corresponds to a single image source + # available in the system, such as an image sensor. + # + # Application lock usage of Camera by 'acquiring' them. + # Once done with it, application shall similarly 'release' the Camera. + # + # As an example, use the first available camera in the system after + # making sure that at least one camera is available. + # + # Cameras can be obtained by their ID or their index, to demonstrate + # this, the following code gets the ID of the first camera; then gets + # the camera associated with that ID (which is of course the same as + # cm.cameras[0]). + + if not cm.cameras: + print('No cameras were identified on the system.') + return -1 + + camera_id = cm.cameras[0].id + camera = cm.get(camera_id) + camera.acquire() + + # -------------------------------------------------------------------- + # Stream + # + # Each Camera supports a variable number of Stream. A Stream is + # produced by processing data produced by an image source, usually + # by an ISP. + # + # +-------------------------------------------------------+ + # | Camera | + # | +-----------+ | + # | +--------+ | |------> [ Main output ] | + # | | Image | | | | + # | | |---->| ISP |------> [ Viewfinder ] | + # | | Source | | | | + # | +--------+ | |------> [ Still Capture ] | + # | +-----------+ | + # +-------------------------------------------------------+ + # + # The number and capabilities of the Stream in a Camera are + # a platform dependent property, and it's the pipeline handler + # implementation that has the responsibility of correctly + # report them. + + # -------------------------------------------------------------------- + # Camera Configuration. + # + # Camera configuration is tricky! It boils down to assign resources + # of the system (such as DMA engines, scalers, format converters) to + # the different image streams an application has requested. + # + # Depending on the system characteristics, some combinations of + # sizes, formats and stream usages might or might not be possible. + # + # A Camera produces a CameraConfigration based on a set of intended + # roles for each Stream the application requires. + + config = camera.generate_configuration([libcam.StreamRole.Viewfinder]) + + # The CameraConfiguration contains a StreamConfiguration instance + # for each StreamRole requested by the application, provided + # the Camera can support all of them. + # + # Each StreamConfiguration has default size and format, assigned + # by the Camera depending on the Role the application has requested. + + stream_config = config.at(0) + print(f'Default viewfinder configuration is: {stream_config}') + + # Each StreamConfiguration parameter which is part of a + # CameraConfiguration can be independently modified by the + # application. + # + # In order to validate the modified parameter, the CameraConfiguration + # should be validated -before- the CameraConfiguration gets applied + # to the Camera. + # + # The CameraConfiguration validation process adjusts each + # StreamConfiguration to a valid value. + + # Validating a CameraConfiguration -before- applying it will adjust it + # to a valid configuration which is as close as possible to the one + # requested. + + config.validate() + print(f'Validated viewfinder configuration is: {stream_config}') + + # Once we have a validated configuration, we can apply it to the + # Camera. + + camera.configure(config) + + # -------------------------------------------------------------------- + # Buffer Allocation + # + # Now that a camera has been configured, it knows all about its + # Streams sizes and formats. The captured images need to be stored in + # framebuffers which can either be provided by the application to the + # library, or allocated in the Camera and exposed to the application + # by libcamera. + # + # An application may decide to allocate framebuffers from elsewhere, + # for example in memory allocated by the display driver that will + # render the captured frames. The application will provide them to + # libcamera by constructing FrameBuffer instances to capture images + # directly into. + # + # Alternatively libcamera can help the application by exporting + # buffers allocated in the Camera using a FrameBufferAllocator + # instance and referencing a configured Camera to determine the + # appropriate buffer size and types to create. + + allocator = libcam.FrameBufferAllocator(camera) + + for cfg in config: + ret = allocator.allocate(cfg.stream) + if ret < 0: + print('Can\'t allocate buffers') + return -1 + + allocated = len(allocator.buffers(cfg.stream)) + print(f'Allocated {allocated} buffers for stream') + + # -------------------------------------------------------------------- + # Frame Capture + # + # libcamera frames capture model is based on the 'Request' concept. + # For each frame a Request has to be queued to the Camera. + # + # A Request refers to (at least one) Stream for which a Buffer that + # will be filled with image data shall be added to the Request. + # + # A Request is associated with a list of Controls, which are tunable + # parameters (similar to v4l2_controls) that have to be applied to + # the image. + # + # Once a request completes, all its buffers will contain image data + # that applications can access and for each of them a list of metadata + # properties that reports the capture parameters applied to the image. + + stream = stream_config.stream + buffers = allocator.buffers(stream) + requests = [] + for i in range(len(buffers)): + request = camera.create_request() + if not request: + print('Can\'t create request') + return -1 + + buffer = buffers[i] + ret = request.add_buffer(stream, buffer) + if ret < 0: + print('Can\'t set buffer for request') + return -1 + + # Controls can be added to a request on a per frame basis. + request.set_control(libcam.controls.Brightness, 0.5) + + requests.append(request) + + # -------------------------------------------------------------------- + # Start Capture + # + # In order to capture frames the Camera has to be started and + # Request queued to it. Enough Request to fill the Camera pipeline + # depth have to be queued before the Camera start delivering frames. + # + # When a Request has been completed, it will be added to a list in the + # CameraManager and an event will be raised using eventfd. + # + # The list of completed Requests can be retrieved with + # CameraManager.get_ready_requests(), which will also clear the list in the + # CameraManager. + # + # The eventfd can be retrieved from CameraManager.event_fd, and the fd can + # be waited upon using e.g. Python's selectors. + + camera.start() + for request in requests: + camera.queue_request(request) + + sel = selectors.DefaultSelector() + sel.register(cm.event_fd, selectors.EVENT_READ, lambda fd: handle_camera_event(cm)) + + start_time = time.time() + + while time.time() - start_time < TIMEOUT_SEC: + events = sel.select() + for key, mask in events: + key.data(key.fileobj) + + # -------------------------------------------------------------------- + # Clean Up + # + # Stop the Camera, release resources and stop the CameraManager. + # libcamera has now released all resources it owned. + + camera.stop() + camera.release() + + return 0 + + +if __name__ == '__main__': + sys.exit(main())