summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.clang-format3
-rw-r--r--Documentation/guides/application-developer.rst12
-rw-r--r--include/libcamera/internal/dma_buf_allocator.h42
-rw-r--r--include/libcamera/internal/dma_heaps.h38
-rw-r--r--include/libcamera/internal/meson.build2
-rw-r--r--include/libcamera/internal/software_isp/debayer_params.h19
-rw-r--r--include/libcamera/internal/software_isp/software_isp.h4
-rw-r--r--include/libcamera/internal/v4l2_subdevice.h3
-rw-r--r--include/libcamera/internal/yaml_parser.h34
-rw-r--r--include/linux/README2
-rw-r--r--include/linux/drm_fourcc.h10
-rw-r--r--include/linux/intel-ipu3.h3
-rw-r--r--include/linux/media-bus-format.h9
-rw-r--r--include/linux/rkisp1-config.h56
-rw-r--r--include/linux/udmabuf.h33
-rw-r--r--include/linux/v4l2-controls.h6
-rw-r--r--include/linux/v4l2-mediabus.h18
-rw-r--r--include/linux/v4l2-subdev.h29
-rw-r--r--include/linux/videodev2.h72
-rw-r--r--meson.build6
-rw-r--r--src/android/camera_capabilities.cpp2
-rw-r--r--src/apps/cam/camera_session.cpp11
-rw-r--r--src/gstreamer/gstlibcamerasrc.cpp5
-rw-r--r--src/ipa/ipu3/meson.build5
-rw-r--r--src/ipa/libipa/agc_mean_luminance.cpp10
-rw-r--r--src/ipa/libipa/agc_mean_luminance.h6
-rw-r--r--src/ipa/libipa/camera_sensor_helper.cpp36
-rw-r--r--src/ipa/libipa/matrix.cpp149
-rw-r--r--src/ipa/libipa/matrix.h204
-rw-r--r--src/ipa/libipa/matrix_interpolator.cpp110
-rw-r--r--src/ipa/libipa/matrix_interpolator.h122
-rw-r--r--src/ipa/libipa/meson.build12
-rw-r--r--src/ipa/libipa/pwl.cpp459
-rw-r--r--src/ipa/libipa/pwl.h88
-rw-r--r--src/ipa/libipa/vector.cpp168
-rw-r--r--src/ipa/libipa/vector.h219
-rw-r--r--src/ipa/rkisp1/algorithms/agc.cpp166
-rw-r--r--src/ipa/rkisp1/algorithms/agc.h6
-rw-r--r--src/ipa/rkisp1/algorithms/ccm.cpp147
-rw-r--r--src/ipa/rkisp1/algorithms/ccm.h49
-rw-r--r--src/ipa/rkisp1/algorithms/cproc.cpp59
-rw-r--r--src/ipa/rkisp1/algorithms/cproc.h3
-rw-r--r--src/ipa/rkisp1/algorithms/goc.cpp148
-rw-r--r--src/ipa/rkisp1/algorithms/goc.h42
-rw-r--r--src/ipa/rkisp1/algorithms/meson.build2
-rw-r--r--src/ipa/rkisp1/ipa_context.cpp82
-rw-r--r--src/ipa/rkisp1/ipa_context.h26
-rw-r--r--src/ipa/rkisp1/meson.build6
-rw-r--r--src/ipa/rkisp1/rkisp1.cpp6
-rw-r--r--src/ipa/rkisp1/utils.cpp42
-rw-r--r--src/ipa/rkisp1/utils.h66
-rw-r--r--src/ipa/rpi/common/ipa_base.cpp7
-rw-r--r--src/ipa/rpi/common/ipa_base.h5
-rw-r--r--src/ipa/rpi/controller/cac_status.h2
-rw-r--r--src/ipa/rpi/controller/contrast_status.h4
-rw-r--r--src/ipa/rpi/controller/meson.build2
-rw-r--r--src/ipa/rpi/controller/pwl.cpp269
-rw-r--r--src/ipa/rpi/controller/pwl.h127
-rw-r--r--src/ipa/rpi/controller/rpi/af.cpp4
-rw-r--r--src/ipa/rpi/controller/rpi/af.h5
-rw-r--r--src/ipa/rpi/controller/rpi/agc_channel.cpp13
-rw-r--r--src/ipa/rpi/controller/rpi/agc_channel.h7
-rw-r--r--src/ipa/rpi/controller/rpi/awb.cpp85
-rw-r--r--src/ipa/rpi/controller/rpi/awb.h23
-rw-r--r--src/ipa/rpi/controller/rpi/ccm.cpp8
-rw-r--r--src/ipa/rpi/controller/rpi/ccm.h5
-rw-r--r--src/ipa/rpi/controller/rpi/contrast.cpp16
-rw-r--r--src/ipa/rpi/controller/rpi/contrast.h5
-rw-r--r--src/ipa/rpi/controller/rpi/geq.cpp9
-rw-r--r--src/ipa/rpi/controller/rpi/geq.h4
-rw-r--r--src/ipa/rpi/controller/rpi/hdr.cpp8
-rw-r--r--src/ipa/rpi/controller/rpi/hdr.h9
-rw-r--r--src/ipa/rpi/controller/rpi/tonemap.cpp2
-rw-r--r--src/ipa/rpi/controller/rpi/tonemap.h5
-rw-r--r--src/ipa/rpi/controller/tonemap_status.h4
-rw-r--r--src/ipa/rpi/vc4/meson.build4
-rw-r--r--src/ipa/simple/black_level.cpp2
-rw-r--r--src/ipa/simple/black_level.h5
-rw-r--r--src/ipa/simple/meson.build5
-rw-r--r--src/ipa/simple/soft_simple.cpp75
-rw-r--r--src/ipa/vimc/meson.build5
-rw-r--r--src/libcamera/control_ids_core.yaml7
-rw-r--r--src/libcamera/dma_buf_allocator.cpp246
-rw-r--r--src/libcamera/dma_heaps.cpp165
-rw-r--r--src/libcamera/meson.build2
-rw-r--r--src/libcamera/pipeline/rpi/common/pipeline_base.cpp2
-rw-r--r--src/libcamera/pipeline/rpi/vc4/vc4.cpp4
-rw-r--r--src/libcamera/pipeline/simple/simple.cpp3
-rw-r--r--src/libcamera/pipeline/vimc/vimc.cpp47
-rw-r--r--src/libcamera/software_isp/TODO13
-rw-r--r--src/libcamera/software_isp/debayer.cpp28
-rw-r--r--src/libcamera/software_isp/debayer_cpu.cpp118
-rw-r--r--src/libcamera/software_isp/debayer_cpu.h21
-rw-r--r--src/libcamera/software_isp/software_isp.cpp28
-rw-r--r--src/libcamera/v4l2_subdevice.cpp122
-rw-r--r--src/libcamera/yaml_parser.cpp94
-rw-r--r--src/v4l2/v4l2_compat.cpp30
-rw-r--r--test/camera/buffer_import.cpp21
-rw-r--r--test/camera/capture.cpp22
-rw-r--r--test/fence.cpp75
-rw-r--r--test/ipa/meson.build8
-rw-r--r--test/ipa/rkisp1/meson.build15
-rw-r--r--test/ipa/rkisp1/rkisp1-utils.cpp108
-rw-r--r--test/v4l2_videodevice/capture_async.cpp11
-rwxr-xr-xutils/checkstyle.py85
-rwxr-xr-xutils/raspberrypi/ctt/alsc_only.py18
-rw-r--r--utils/raspberrypi/ctt/cac_only.py142
-rwxr-xr-xutils/raspberrypi/ctt/convert_tuning.py98
-rwxr-xr-xutils/raspberrypi/ctt/ctt.py253
-rw-r--r--utils/raspberrypi/ctt/ctt_alsc.py81
-rw-r--r--utils/raspberrypi/ctt/ctt_awb.py11
-rw-r--r--utils/raspberrypi/ctt/ctt_cac.py228
-rw-r--r--utils/raspberrypi/ctt/ctt_ccm.py6
-rw-r--r--utils/raspberrypi/ctt/ctt_config_example.json5
-rw-r--r--utils/raspberrypi/ctt/ctt_dots_locator.py118
-rw-r--r--utils/raspberrypi/ctt/ctt_image_load.py1
-rwxr-xr-xutils/raspberrypi/ctt/ctt_pisp.py805
-rwxr-xr-xutils/raspberrypi/ctt/ctt_pretty_print_json.py22
-rw-r--r--utils/raspberrypi/ctt/ctt_tools.py3
-rwxr-xr-xutils/raspberrypi/ctt/ctt_vc4.py126
-rw-r--r--utils/tuning/libtuning/modules/agc/__init__.py6
-rw-r--r--utils/tuning/libtuning/modules/agc/agc.py21
-rw-r--r--utils/tuning/libtuning/modules/agc/rkisp1.py79
-rwxr-xr-xutils/tuning/rkisp1.py4
-rwxr-xr-xutils/update-kernel-headers.sh1
125 files changed, 5535 insertions, 1349 deletions
diff --git a/.clang-format b/.clang-format
index cac7029f..f2b44e87 100644
--- a/.clang-format
+++ b/.clang-format
@@ -1,6 +1,6 @@
# SPDX-License-Identifier: GPL-2.0-only
#
-# clang-format configuration file. Intended for clang-format >= 7.
+# clang-format configuration file. Intended for clang-format >= 12.
#
# For more information, see:
#
@@ -75,6 +75,7 @@ IncludeCategories:
Priority: 9
# Qt includes (match before C++ standard library)
- Regex: '<Q([A-Za-z0-9\-_])+>'
+ CaseSensitive: true
Priority: 9
# Headers in <> with an extension. (+system libraries)
- Regex: '<([A-Za-z0-9\-_])+\.h>'
diff --git a/Documentation/guides/application-developer.rst b/Documentation/guides/application-developer.rst
index 9a9905b1..92e2a373 100644
--- a/Documentation/guides/application-developer.rst
+++ b/Documentation/guides/application-developer.rst
@@ -116,19 +116,21 @@ available.
.. code:: cpp
- if (cm->cameras().empty()) {
+ auto cameras = cm->cameras();
+ if (cameras.empty()) {
std::cout << "No cameras were identified on the system."
<< std::endl;
cm->stop();
return EXIT_FAILURE;
}
- std::string cameraId = cm->cameras()[0]->id();
- camera = cm->get(cameraId);
+ std::string cameraId = cameras[0]->id();
+ auto camera = cm->get(cameraId);
/*
- * Note that is equivalent to:
- * camera = cm->cameras()[0];
+ * Note that `camera` may not compare equal to `cameras[0]`.
+ * In fact, it might simply be a `nullptr`, as the particular
+ * device might have disappeared (and reappeared) in the meantime.
*/
Once a camera has been selected an application needs to acquire an exclusive
diff --git a/include/libcamera/internal/dma_buf_allocator.h b/include/libcamera/internal/dma_buf_allocator.h
new file mode 100644
index 00000000..36ec1696
--- /dev/null
+++ b/include/libcamera/internal/dma_buf_allocator.h
@@ -0,0 +1,42 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2020, Raspberry Pi Ltd
+ *
+ * Helper class for dma-buf allocations.
+ */
+
+#pragma once
+
+#include <stddef.h>
+
+#include <libcamera/base/flags.h>
+#include <libcamera/base/unique_fd.h>
+
+namespace libcamera {
+
+class DmaBufAllocator
+{
+public:
+ enum class DmaBufAllocatorFlag {
+ CmaHeap = 1 << 0,
+ SystemHeap = 1 << 1,
+ UDmaBuf = 1 << 2,
+ };
+
+ using DmaBufAllocatorFlags = Flags<DmaBufAllocatorFlag>;
+
+ DmaBufAllocator(DmaBufAllocatorFlags flags = DmaBufAllocatorFlag::CmaHeap);
+ ~DmaBufAllocator();
+ bool isValid() const { return providerHandle_.isValid(); }
+ UniqueFD alloc(const char *name, std::size_t size);
+
+private:
+ UniqueFD allocFromHeap(const char *name, std::size_t size);
+ UniqueFD allocFromUDmaBuf(const char *name, std::size_t size);
+ UniqueFD providerHandle_;
+ DmaBufAllocatorFlag type_;
+};
+
+LIBCAMERA_FLAGS_ENABLE_OPERATORS(DmaBufAllocator::DmaBufAllocatorFlag)
+
+} /* namespace libcamera */
diff --git a/include/libcamera/internal/dma_heaps.h b/include/libcamera/internal/dma_heaps.h
deleted file mode 100644
index f0a8aa5d..00000000
--- a/include/libcamera/internal/dma_heaps.h
+++ /dev/null
@@ -1,38 +0,0 @@
-/* SPDX-License-Identifier: LGPL-2.1-or-later */
-/*
- * Copyright (C) 2020, Raspberry Pi Ltd
- *
- * Helper class for dma-heap allocations.
- */
-
-#pragma once
-
-#include <stddef.h>
-
-#include <libcamera/base/flags.h>
-#include <libcamera/base/unique_fd.h>
-
-namespace libcamera {
-
-class DmaHeap
-{
-public:
- enum class DmaHeapFlag {
- Cma = 1 << 0,
- System = 1 << 1,
- };
-
- using DmaHeapFlags = Flags<DmaHeapFlag>;
-
- DmaHeap(DmaHeapFlags flags = DmaHeapFlag::Cma);
- ~DmaHeap();
- bool isValid() const { return dmaHeapHandle_.isValid(); }
- UniqueFD alloc(const char *name, std::size_t size);
-
-private:
- UniqueFD dmaHeapHandle_;
-};
-
-LIBCAMERA_FLAGS_ENABLE_OPERATORS(DmaHeap::DmaHeapFlag)
-
-} /* namespace libcamera */
diff --git a/include/libcamera/internal/meson.build b/include/libcamera/internal/meson.build
index 160fdc37..9713ea1c 100644
--- a/include/libcamera/internal/meson.build
+++ b/include/libcamera/internal/meson.build
@@ -25,7 +25,7 @@ libcamera_internal_headers = files([
'device_enumerator.h',
'device_enumerator_sysfs.h',
'device_enumerator_udev.h',
- 'dma_heaps.h',
+ 'dma_buf_allocator.h',
'formats.h',
'framebuffer.h',
'ipa_manager.h',
diff --git a/include/libcamera/internal/software_isp/debayer_params.h b/include/libcamera/internal/software_isp/debayer_params.h
index ce1b5945..7d8fdd48 100644
--- a/include/libcamera/internal/software_isp/debayer_params.h
+++ b/include/libcamera/internal/software_isp/debayer_params.h
@@ -1,6 +1,6 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
/*
- * Copyright (C) 2023, Red Hat Inc.
+ * Copyright (C) 2023, 2024 Red Hat Inc.
*
* Authors:
* Hans de Goede <hdegoede@redhat.com>
@@ -10,20 +10,19 @@
#pragma once
+#include <array>
+#include <stdint.h>
+
namespace libcamera {
struct DebayerParams {
- static constexpr unsigned int kGain10 = 256;
+ static constexpr unsigned int kRGBLookupSize = 256;
- unsigned int gainR;
- unsigned int gainG;
- unsigned int gainB;
+ using ColorLookupTable = std::array<uint8_t, kRGBLookupSize>;
- float gamma;
- /**
- * \brief Level of the black point, 0..255, 0 is no correction.
- */
- unsigned int blackLevel;
+ ColorLookupTable red;
+ ColorLookupTable green;
+ ColorLookupTable blue;
};
} /* namespace libcamera */
diff --git a/include/libcamera/internal/software_isp/software_isp.h b/include/libcamera/internal/software_isp/software_isp.h
index 7e9fae6a..c5338c05 100644
--- a/include/libcamera/internal/software_isp/software_isp.h
+++ b/include/libcamera/internal/software_isp/software_isp.h
@@ -27,7 +27,7 @@
#include <libcamera/ipa/soft_ipa_proxy.h>
#include "libcamera/internal/camera_sensor.h"
-#include "libcamera/internal/dma_heaps.h"
+#include "libcamera/internal/dma_buf_allocator.h"
#include "libcamera/internal/pipeline_handler.h"
#include "libcamera/internal/shared_mem_object.h"
#include "libcamera/internal/software_isp/debayer_params.h"
@@ -91,7 +91,7 @@ private:
Thread ispWorkerThread_;
SharedMemObject<DebayerParams> sharedParams_;
DebayerParams debayerParams_;
- DmaHeap dmaHeap_;
+ DmaBufAllocator dmaHeap_;
std::unique_ptr<ipa::soft::IPAProxySoft> ipa_;
};
diff --git a/include/libcamera/internal/v4l2_subdevice.h b/include/libcamera/internal/v4l2_subdevice.h
index a1de0fb0..194382f8 100644
--- a/include/libcamera/internal/v4l2_subdevice.h
+++ b/include/libcamera/internal/v4l2_subdevice.h
@@ -176,6 +176,9 @@ private:
std::vector<SizeRange> enumPadSizes(const Stream &stream,
unsigned int code);
+ int getRoutingLegacy(Routing *routing, Whence whence);
+ int setRoutingLegacy(Routing *routing, Whence whence);
+
const MediaEntity *entity_;
std::string model_;
diff --git a/include/libcamera/internal/yaml_parser.h b/include/libcamera/internal/yaml_parser.h
index b6979d73..e38a2df9 100644
--- a/include/libcamera/internal/yaml_parser.h
+++ b/include/libcamera/internal/yaml_parser.h
@@ -161,34 +161,23 @@ public:
std::size_t size() const;
-#ifndef __DOXYGEN__
- template<typename T,
- std::enable_if_t<
- std::is_same_v<bool, T> ||
- std::is_same_v<double, T> ||
- std::is_same_v<int8_t, T> ||
- std::is_same_v<uint8_t, T> ||
- std::is_same_v<int16_t, T> ||
- std::is_same_v<uint16_t, T> ||
- std::is_same_v<int32_t, T> ||
- std::is_same_v<uint32_t, T> ||
- std::is_same_v<std::string, T> ||
- std::is_same_v<Size, T>> * = nullptr>
-#else
template<typename T>
-#endif
- std::optional<T> get() const;
+ std::optional<T> get() const
+ {
+ return Getter<T>{}.get(*this);
+ }
- template<typename T>
- T get(const T &defaultValue) const
+ template<typename T, typename U>
+ T get(U &&defaultValue) const
{
- return get<T>().value_or(defaultValue);
+ return get<T>().value_or(std::forward<U>(defaultValue));
}
#ifndef __DOXYGEN__
template<typename T,
std::enable_if_t<
std::is_same_v<bool, T> ||
+ std::is_same_v<float, T> ||
std::is_same_v<double, T> ||
std::is_same_v<int8_t, T> ||
std::is_same_v<uint8_t, T> ||
@@ -214,6 +203,8 @@ public:
private:
LIBCAMERA_DISABLE_COPY_AND_MOVE(YamlObject)
+ template<typename T>
+ friend struct Getter;
friend class YamlParserContext;
enum class Type {
@@ -222,6 +213,11 @@ private:
Value,
};
+ template<typename T>
+ struct Getter {
+ std::optional<T> get(const YamlObject &obj) const;
+ };
+
Type type_;
std::string value_;
diff --git a/include/linux/README b/include/linux/README
index 101e4997..b7795309 100644
--- a/include/linux/README
+++ b/include/linux/README
@@ -1,4 +1,4 @@
# SPDX-License-Identifier: CC0-1.0
-Files in this directory are imported from v6.7 of the Linux kernel. Do not
+Files in this directory are imported from v6.10-rc1 of the Linux kernel. Do not
modify them manually.
diff --git a/include/linux/drm_fourcc.h b/include/linux/drm_fourcc.h
index b4e1a092..d8e99940 100644
--- a/include/linux/drm_fourcc.h
+++ b/include/linux/drm_fourcc.h
@@ -54,7 +54,7 @@ extern "C" {
* Format modifiers may change any property of the buffer, including the number
* of planes and/or the required allocation size. Format modifiers are
* vendor-namespaced, and as such the relationship between a fourcc code and a
- * modifier is specific to the modifer being used. For example, some modifiers
+ * modifier is specific to the modifier being used. For example, some modifiers
* may preserve meaning - such as number of planes - from the fourcc code,
* whereas others may not.
*
@@ -79,7 +79,7 @@ extern "C" {
* format.
* - Higher-level programs interfacing with KMS/GBM/EGL/Vulkan/etc: these users
* see modifiers as opaque tokens they can check for equality and intersect.
- * These users musn't need to know to reason about the modifier value
+ * These users mustn't need to know to reason about the modifier value
* (i.e. they are not expected to extract information out of the modifier).
*
* Vendors should document their modifier usage in as much detail as
@@ -610,7 +610,7 @@ extern "C" {
* This is a tiled layout using 4Kb tiles in row-major layout.
* Within the tile pixels are laid out in 16 256 byte units / sub-tiles which
* are arranged in four groups (two wide, two high) with column-major layout.
- * Each group therefore consits out of four 256 byte units, which are also laid
+ * Each group therefore consists out of four 256 byte units, which are also laid
* out as 2x2 column-major.
* 256 byte units are made out of four 64 byte blocks of pixels, producing
* either a square block or a 2:1 unit.
@@ -1183,7 +1183,7 @@ drm_fourcc_canonicalize_nvidia_format_mod(__u64 modifier)
*/
/*
- * The top 4 bits (out of the 56 bits alloted for specifying vendor specific
+ * The top 4 bits (out of the 56 bits allotted for specifying vendor specific
* modifiers) denote the category for modifiers. Currently we have three
* categories of modifiers ie AFBC, MISC and AFRC. We can have a maximum of
* sixteen different categories.
@@ -1499,7 +1499,7 @@ drm_fourcc_canonicalize_nvidia_format_mod(__u64 modifier)
* Amlogic FBC Memory Saving mode
*
* Indicates the storage is packed when pixel size is multiple of word
- * boudaries, i.e. 8bit should be stored in this mode to save allocation
+ * boundaries, i.e. 8bit should be stored in this mode to save allocation
* memory.
*
* This mode reduces body layout to 3072 bytes per 64x32 superblock with
diff --git a/include/linux/intel-ipu3.h b/include/linux/intel-ipu3.h
index bd771f1b..8c192f35 100644
--- a/include/linux/intel-ipu3.h
+++ b/include/linux/intel-ipu3.h
@@ -2485,11 +2485,9 @@ struct ipu3_uapi_anr_config {
* &ipu3_uapi_yuvp1_y_ee_nr_config
* @yds: y down scaler config. See &ipu3_uapi_yuvp1_yds_config
* @chnr: chroma noise reduction config. See &ipu3_uapi_yuvp1_chnr_config
- * @reserved1: reserved
* @yds2: y channel down scaler config. See &ipu3_uapi_yuvp1_yds_config
* @tcc: total color correction config as defined in struct
* &ipu3_uapi_yuvp2_tcc_static_config
- * @reserved2: reserved
* @anr: advanced noise reduction config.See &ipu3_uapi_anr_config
* @awb_fr: AWB filter response config. See ipu3_uapi_awb_fr_config
* @ae: auto exposure config As specified by &ipu3_uapi_ae_config
@@ -2724,7 +2722,6 @@ struct ipu3_uapi_obgrid_param {
* @acc_ae: 0 = no update, 1 = update.
* @acc_af: 0 = no update, 1 = update.
* @acc_awb: 0 = no update, 1 = update.
- * @__acc_osys: 0 = no update, 1 = update.
* @reserved3: Not used.
* @lin_vmem_params: 0 = no update, 1 = update.
* @tnr3_vmem_params: 0 = no update, 1 = update.
diff --git a/include/linux/media-bus-format.h b/include/linux/media-bus-format.h
index f05f747e..d4c1d991 100644
--- a/include/linux/media-bus-format.h
+++ b/include/linux/media-bus-format.h
@@ -174,4 +174,13 @@
*/
#define MEDIA_BUS_FMT_METADATA_FIXED 0x7001
+/* Generic line based metadata formats for serial buses. Next is 0x8008. */
+#define MEDIA_BUS_FMT_META_8 0x8001
+#define MEDIA_BUS_FMT_META_10 0x8002
+#define MEDIA_BUS_FMT_META_12 0x8003
+#define MEDIA_BUS_FMT_META_14 0x8004
+#define MEDIA_BUS_FMT_META_16 0x8005
+#define MEDIA_BUS_FMT_META_20 0x8006
+#define MEDIA_BUS_FMT_META_24 0x8007
+
#endif /* __LINUX_MEDIA_BUS_FORMAT_H */
diff --git a/include/linux/rkisp1-config.h b/include/linux/rkisp1-config.h
index 2d1c448a..f87c6bd4 100644
--- a/include/linux/rkisp1-config.h
+++ b/include/linux/rkisp1-config.h
@@ -4,8 +4,8 @@
* Copyright (C) 2017 Rockchip Electronics Co., Ltd.
*/
-#ifndef _UAPI_RKISP1_CONFIG_H
-#define _UAPI_RKISP1_CONFIG_H
+#ifndef _RKISP1_CONFIG_H
+#define _RKISP1_CONFIG_H
#include <linux/types.h>
@@ -175,11 +175,14 @@
/**
* enum rkisp1_cif_isp_version - ISP variants
*
- * @RKISP1_V10: used at least in rk3288 and rk3399
- * @RKISP1_V11: declared in the original vendor code, but not used
- * @RKISP1_V12: used at least in rk3326 and px30
- * @RKISP1_V13: used at least in rk1808
- * @RKISP1_V_IMX8MP: used in at least imx8mp
+ * @RKISP1_V10: Used at least in RK3288 and RK3399.
+ * @RKISP1_V11: Declared in the original vendor code, but not used. Same number
+ * of entries in grids and histogram as v10.
+ * @RKISP1_V12: Used at least in RK3326 and PX30.
+ * @RKISP1_V13: Used at least in RK1808. Same number of entries in grids and
+ * histogram as v12.
+ * @RKISP1_V_IMX8MP: Used in at least i.MX8MP. Same number of entries in grids
+ * and histogram as v10.
*/
enum rkisp1_cif_isp_version {
RKISP1_V10 = 10,
@@ -586,10 +589,9 @@ enum rkisp1_cif_isp_goc_mode {
* as is reported by the hw_revision field of the struct media_device_info
* that is returned by ioctl MEDIA_IOC_DEVICE_INFO.
*
- * Versions <= V11 have RKISP1_CIF_ISP_GAMMA_OUT_MAX_SAMPLES_V10
- * entries, versions >= V12 have RKISP1_CIF_ISP_GAMMA_OUT_MAX_SAMPLES_V12
- * entries. RKISP1_CIF_ISP_GAMMA_OUT_MAX_SAMPLES is equal to the maximum
- * of the two.
+ * V10 has RKISP1_CIF_ISP_GAMMA_OUT_MAX_SAMPLES_V10 entries, V12 has
+ * RKISP1_CIF_ISP_GAMMA_OUT_MAX_SAMPLES_V12 entries.
+ * RKISP1_CIF_ISP_GAMMA_OUT_MAX_SAMPLES is equal to the maximum of the two.
*/
struct rkisp1_cif_isp_goc_config {
__u32 mode;
@@ -609,10 +611,10 @@ struct rkisp1_cif_isp_goc_config {
* as is reported by the hw_revision field of the struct media_device_info
* that is returned by ioctl MEDIA_IOC_DEVICE_INFO.
*
- * Versions <= V11 have RKISP1_CIF_ISP_HISTOGRAM_WEIGHT_GRIDS_SIZE_V10
- * entries, versions >= V12 have RKISP1_CIF_ISP_HISTOGRAM_WEIGHT_GRIDS_SIZE_V12
- * entries. RKISP1_CIF_ISP_HISTOGRAM_WEIGHT_GRIDS_SIZE is equal to the maximum
- * of the two.
+ * V10 has RKISP1_CIF_ISP_HISTOGRAM_WEIGHT_GRIDS_SIZE_V10 entries, V12 has
+ * RKISP1_CIF_ISP_HISTOGRAM_WEIGHT_GRIDS_SIZE_V12 entries.
+ * RKISP1_CIF_ISP_HISTOGRAM_WEIGHT_GRIDS_SIZE is equal to the maximum of the
+ * two.
*/
struct rkisp1_cif_isp_hst_config {
__u32 mode;
@@ -904,9 +906,9 @@ struct rkisp1_cif_isp_bls_meas_val {
* as is reported by the hw_revision field of the struct media_device_info
* that is returned by ioctl MEDIA_IOC_DEVICE_INFO.
*
- * Versions <= V11 have RKISP1_CIF_ISP_AE_MEAN_MAX_V10 entries,
- * versions >= V12 have RKISP1_CIF_ISP_AE_MEAN_MAX_V12 entries.
- * RKISP1_CIF_ISP_AE_MEAN_MAX is equal to the maximum of the two.
+ * V10 has RKISP1_CIF_ISP_AE_MEAN_MAX_V10 entries, V12 has
+ * RKISP1_CIF_ISP_AE_MEAN_MAX_V12 entries. RKISP1_CIF_ISP_AE_MEAN_MAX is equal
+ * to the maximum of the two.
*
* Image is divided into 5x5 blocks on V10 and 9x9 blocks on V12.
*/
@@ -946,21 +948,21 @@ struct rkisp1_cif_isp_af_stat {
* integer part.
*
* The window of the measurements area is divided to 5x5 sub-windows for
- * V10/V11 and to 9x9 sub-windows for V12. The histogram is then computed for
- * each sub-window independently and the final result is a weighted average of
- * the histogram measurements on all sub-windows. The window of the
- * measurements area and the weight of each sub-window are configurable using
+ * V10 and to 9x9 sub-windows for V12. The histogram is then computed for each
+ * sub-window independently and the final result is a weighted average of the
+ * histogram measurements on all sub-windows. The window of the measurements
+ * area and the weight of each sub-window are configurable using
* struct @rkisp1_cif_isp_hst_config.
*
- * The histogram contains 16 bins in V10/V11 and 32 bins in V12/V13.
+ * The histogram contains 16 bins in V10 and 32 bins in V12.
*
* The number of entries of @hist_bins depends on the hardware revision
* as is reported by the hw_revision field of the struct media_device_info
* that is returned by ioctl MEDIA_IOC_DEVICE_INFO.
*
- * Versions <= V11 have RKISP1_CIF_ISP_HIST_BIN_N_MAX_V10 entries,
- * versions >= V12 have RKISP1_CIF_ISP_HIST_BIN_N_MAX_V12 entries.
- * RKISP1_CIF_ISP_HIST_BIN_N_MAX is equal to the maximum of the two.
+ * V10 has RKISP1_CIF_ISP_HIST_BIN_N_MAX_V10 entries, V12 has
+ * RKISP1_CIF_ISP_HIST_BIN_N_MAX_V12 entries. RKISP1_CIF_ISP_HIST_BIN_N_MAX is
+ * equal to the maximum of the two.
*/
struct rkisp1_cif_isp_hist_stat {
__u32 hist_bins[RKISP1_CIF_ISP_HIST_BIN_N_MAX];
@@ -994,4 +996,4 @@ struct rkisp1_stat_buffer {
struct rkisp1_cif_isp_stat params;
};
-#endif /* _UAPI_RKISP1_CONFIG_H */
+#endif /* _RKISP1_CONFIG_H */
diff --git a/include/linux/udmabuf.h b/include/linux/udmabuf.h
new file mode 100644
index 00000000..76cc7de9
--- /dev/null
+++ b/include/linux/udmabuf.h
@@ -0,0 +1,33 @@
+/* SPDX-License-Identifier: GPL-2.0 WITH Linux-syscall-note */
+#ifndef _LINUX_UDMABUF_H
+#define _LINUX_UDMABUF_H
+
+#include <linux/types.h>
+#include <linux/ioctl.h>
+
+#define UDMABUF_FLAGS_CLOEXEC 0x01
+
+struct udmabuf_create {
+ __u32 memfd;
+ __u32 flags;
+ __u64 offset;
+ __u64 size;
+};
+
+struct udmabuf_create_item {
+ __u32 memfd;
+ __u32 __pad;
+ __u64 offset;
+ __u64 size;
+};
+
+struct udmabuf_create_list {
+ __u32 flags;
+ __u32 count;
+ struct udmabuf_create_item list[];
+};
+
+#define UDMABUF_CREATE _IOW('u', 0x42, struct udmabuf_create)
+#define UDMABUF_CREATE_LIST _IOW('u', 0x43, struct udmabuf_create_list)
+
+#endif /* _LINUX_UDMABUF_H */
diff --git a/include/linux/v4l2-controls.h b/include/linux/v4l2-controls.h
index b9f64810..1e6e816b 100644
--- a/include/linux/v4l2-controls.h
+++ b/include/linux/v4l2-controls.h
@@ -211,6 +211,12 @@ enum v4l2_colorfx {
*/
#define V4L2_CID_USER_NPCM_BASE (V4L2_CID_USER_BASE + 0x11b0)
+/*
+ * The base for THine THP7312 driver controls.
+ * We reserve 32 controls for this driver.
+ */
+#define V4L2_CID_USER_THP7312_BASE (V4L2_CID_USER_BASE + 0x11c0)
+
/* MPEG-class control IDs */
/* The MPEG controls are applicable to all codec controls
* and the 'MPEG' part of the define is historical */
diff --git a/include/linux/v4l2-mediabus.h b/include/linux/v4l2-mediabus.h
index 2c318de1..097ef739 100644
--- a/include/linux/v4l2-mediabus.h
+++ b/include/linux/v4l2-mediabus.h
@@ -19,12 +19,18 @@
* @width: image width
* @height: image height
* @code: data format code (from enum v4l2_mbus_pixelcode)
- * @field: used interlacing type (from enum v4l2_field)
- * @colorspace: colorspace of the data (from enum v4l2_colorspace)
- * @ycbcr_enc: YCbCr encoding of the data (from enum v4l2_ycbcr_encoding)
- * @hsv_enc: HSV encoding of the data (from enum v4l2_hsv_encoding)
- * @quantization: quantization of the data (from enum v4l2_quantization)
- * @xfer_func: transfer function of the data (from enum v4l2_xfer_func)
+ * @field: used interlacing type (from enum v4l2_field), zero for metadata
+ * mbus codes
+ * @colorspace: colorspace of the data (from enum v4l2_colorspace), zero on
+ * metadata mbus codes
+ * @ycbcr_enc: YCbCr encoding of the data (from enum v4l2_ycbcr_encoding), zero
+ * for metadata mbus codes
+ * @hsv_enc: HSV encoding of the data (from enum v4l2_hsv_encoding), zero for
+ * metadata mbus codes
+ * @quantization: quantization of the data (from enum v4l2_quantization), zero
+ * for metadata mbus codes
+ * @xfer_func: transfer function of the data (from enum v4l2_xfer_func), zero
+ * for metadata mbus codes
* @flags: flags (V4L2_MBUS_FRAMEFMT_*)
* @reserved: reserved bytes that can be later used
*/
diff --git a/include/linux/v4l2-subdev.h b/include/linux/v4l2-subdev.h
index b383c2fe..2347e266 100644
--- a/include/linux/v4l2-subdev.h
+++ b/include/linux/v4l2-subdev.h
@@ -50,6 +50,10 @@ struct v4l2_subdev_format {
* @rect: pad crop rectangle boundaries
* @stream: stream number, defined in subdev routing
* @reserved: drivers and applications must zero this array
+ *
+ * The subdev crop API is an obsolete interface and may be removed in the
+ * future. It is superseded by the selection API. No new extensions to this
+ * structure will be accepted.
*/
struct v4l2_subdev_crop {
__u32 which;
@@ -116,13 +120,15 @@ struct v4l2_subdev_frame_size_enum {
* @pad: pad number, as reported by the media API
* @interval: frame interval in seconds
* @stream: stream number, defined in subdev routing
+ * @which: interval type (from enum v4l2_subdev_format_whence)
* @reserved: drivers and applications must zero this array
*/
struct v4l2_subdev_frame_interval {
__u32 pad;
struct v4l2_fract interval;
__u32 stream;
- __u32 reserved[8];
+ __u32 which;
+ __u32 reserved[7];
};
/**
@@ -133,7 +139,7 @@ struct v4l2_subdev_frame_interval {
* @width: frame width in pixels
* @height: frame height in pixels
* @interval: frame interval in seconds
- * @which: format type (from enum v4l2_subdev_format_whence)
+ * @which: interval type (from enum v4l2_subdev_format_whence)
* @stream: stream number, defined in subdev routing
* @reserved: drivers and applications must zero this array
*/
@@ -222,15 +228,19 @@ struct v4l2_subdev_route {
* struct v4l2_subdev_routing - Subdev routing information
*
* @which: configuration type (from enum v4l2_subdev_format_whence)
- * @num_routes: the total number of routes in the routes array
+ * @len_routes: the length of the routes array, in routes; set by the user, not
+ * modified by the kernel
* @routes: pointer to the routes array
+ * @num_routes: the total number of routes, possibly more than fits in the
+ * routes array
* @reserved: drivers and applications must zero this array
*/
struct v4l2_subdev_routing {
__u32 which;
- __u32 num_routes;
+ __u32 len_routes;
__u64 routes;
- __u32 reserved[6];
+ __u32 num_routes;
+ __u32 reserved[11];
};
/*
@@ -239,7 +249,14 @@ struct v4l2_subdev_routing {
* set (which is the default), the 'stream' fields will be forced to 0 by the
* kernel.
*/
- #define V4L2_SUBDEV_CLIENT_CAP_STREAMS (1ULL << 0)
+#define V4L2_SUBDEV_CLIENT_CAP_STREAMS (1ULL << 0)
+
+/*
+ * The client is aware of the struct v4l2_subdev_frame_interval which field. If
+ * this is not set (which is the default), the which field is forced to
+ * V4L2_SUBDEV_FORMAT_ACTIVE by the kernel.
+ */
+#define V4L2_SUBDEV_CLIENT_CAP_INTERVAL_USES_WHICH (1ULL << 1)
/**
* struct v4l2_subdev_client_capability - Capabilities of the client accessing
diff --git a/include/linux/videodev2.h b/include/linux/videodev2.h
index 0b5482a0..7fe522e8 100644
--- a/include/linux/videodev2.h
+++ b/include/linux/videodev2.h
@@ -574,6 +574,8 @@ struct v4l2_pix_format {
#define V4L2_PIX_FMT_Y10BPACK v4l2_fourcc('Y', '1', '0', 'B') /* 10 Greyscale bit-packed */
#define V4L2_PIX_FMT_Y10P v4l2_fourcc('Y', '1', '0', 'P') /* 10 Greyscale, MIPI RAW10 packed */
#define V4L2_PIX_FMT_IPU3_Y10 v4l2_fourcc('i', 'p', '3', 'y') /* IPU3 packed 10-bit greyscale */
+#define V4L2_PIX_FMT_Y12P v4l2_fourcc('Y', '1', '2', 'P') /* 12 Greyscale, MIPI RAW12 packed */
+#define V4L2_PIX_FMT_Y14P v4l2_fourcc('Y', '1', '4', 'P') /* 14 Greyscale, MIPI RAW14 packed */
/* Palette formats */
#define V4L2_PIX_FMT_PAL8 v4l2_fourcc('P', 'A', 'L', '8') /* 8 8-bit palette */
@@ -867,6 +869,7 @@ struct v4l2_fmtdesc {
#define V4L2_FMT_FLAG_CSC_YCBCR_ENC 0x0080
#define V4L2_FMT_FLAG_CSC_HSV_ENC V4L2_FMT_FLAG_CSC_YCBCR_ENC
#define V4L2_FMT_FLAG_CSC_QUANTIZATION 0x0100
+#define V4L2_FMT_FLAG_META_LINE_BASED 0x0200
/* Frame Size and frame rate enumeration */
/*
@@ -1016,18 +1019,20 @@ struct v4l2_requestbuffers {
#define V4L2_BUF_CAP_SUPPORTS_ORPHANED_BUFS (1 << 4)
#define V4L2_BUF_CAP_SUPPORTS_M2M_HOLD_CAPTURE_BUF (1 << 5)
#define V4L2_BUF_CAP_SUPPORTS_MMAP_CACHE_HINTS (1 << 6)
+#define V4L2_BUF_CAP_SUPPORTS_MAX_NUM_BUFFERS (1 << 7)
+#define V4L2_BUF_CAP_SUPPORTS_REMOVE_BUFS (1 << 8)
/**
* struct v4l2_plane - plane info for multi-planar buffers
* @bytesused: number of bytes occupied by data in the plane (payload)
* @length: size of this plane (NOT the payload) in bytes
- * @mem_offset: when memory in the associated struct v4l2_buffer is
+ * @m.mem_offset: when memory in the associated struct v4l2_buffer is
* V4L2_MEMORY_MMAP, equals the offset from the start of
* the device memory for this plane (or is a "cookie" that
* should be passed to mmap() called on the video node)
- * @userptr: when memory is V4L2_MEMORY_USERPTR, a userspace pointer
+ * @m.userptr: when memory is V4L2_MEMORY_USERPTR, a userspace pointer
* pointing to this plane
- * @fd: when memory is V4L2_MEMORY_DMABUF, a userspace file
+ * @m.fd: when memory is V4L2_MEMORY_DMABUF, a userspace file
* descriptor associated with this plane
* @m: union of @mem_offset, @userptr and @fd
* @data_offset: offset in the plane to the start of data; usually 0,
@@ -1065,14 +1070,14 @@ struct v4l2_plane {
* @sequence: sequence count of this frame
* @memory: enum v4l2_memory; the method, in which the actual video data is
* passed
- * @offset: for non-multiplanar buffers with memory == V4L2_MEMORY_MMAP;
+ * @m.offset: for non-multiplanar buffers with memory == V4L2_MEMORY_MMAP;
* offset from the start of the device memory for this plane,
* (or a "cookie" that should be passed to mmap() as offset)
- * @userptr: for non-multiplanar buffers with memory == V4L2_MEMORY_USERPTR;
+ * @m.userptr: for non-multiplanar buffers with memory == V4L2_MEMORY_USERPTR;
* a userspace pointer pointing to this buffer
- * @fd: for non-multiplanar buffers with memory == V4L2_MEMORY_DMABUF;
+ * @m.fd: for non-multiplanar buffers with memory == V4L2_MEMORY_DMABUF;
* a userspace file descriptor associated with this buffer
- * @planes: for multiplanar buffers; userspace pointer to the array of plane
+ * @m.planes: for multiplanar buffers; userspace pointer to the array of plane
* info structs for this buffer
* @m: union of @offset, @userptr, @planes and @fd
* @length: size in bytes of the buffer (NOT its payload) for single-plane
@@ -1810,8 +1815,10 @@ struct v4l2_ext_control {
struct v4l2_ctrl_av1_tile_group_entry *p_av1_tile_group_entry;
struct v4l2_ctrl_av1_frame *p_av1_frame;
struct v4l2_ctrl_av1_film_grain *p_av1_film_grain;
+ struct v4l2_ctrl_hdr10_cll_info *p_hdr10_cll_info;
+ struct v4l2_ctrl_hdr10_mastering_display *p_hdr10_mastering_display;
void *ptr;
- };
+ } __attribute__ ((packed));
} __attribute__ ((packed));
struct v4l2_ext_controls {
@@ -2381,23 +2388,32 @@ struct v4l2_sdr_format {
* struct v4l2_meta_format - metadata format definition
* @dataformat: little endian four character code (fourcc)
* @buffersize: maximum size in bytes required for data
+ * @width: number of data units of data per line (valid for line
+ * based formats only, see format documentation)
+ * @height: number of lines of data per buffer (valid for line based
+ * formats only)
+ * @bytesperline: offset between the beginnings of two adjacent lines in
+ * bytes (valid for line based formats only)
*/
struct v4l2_meta_format {
__u32 dataformat;
__u32 buffersize;
+ __u32 width;
+ __u32 height;
+ __u32 bytesperline;
} __attribute__ ((packed));
/**
* struct v4l2_format - stream data format
- * @type: enum v4l2_buf_type; type of the data stream
- * @pix: definition of an image format
- * @pix_mp: definition of a multiplanar image format
- * @win: definition of an overlaid image
- * @vbi: raw VBI capture or output parameters
- * @sliced: sliced VBI capture or output parameters
- * @raw_data: placeholder for future extensions and custom formats
- * @fmt: union of @pix, @pix_mp, @win, @vbi, @sliced, @sdr, @meta
- * and @raw_data
+ * @type: enum v4l2_buf_type; type of the data stream
+ * @fmt.pix: definition of an image format
+ * @fmt.pix_mp: definition of a multiplanar image format
+ * @fmt.win: definition of an overlaid image
+ * @fmt.vbi: raw VBI capture or output parameters
+ * @fmt.sliced: sliced VBI capture or output parameters
+ * @fmt.raw_data: placeholder for future extensions and custom formats
+ * @fmt: union of @pix, @pix_mp, @win, @vbi, @sliced, @sdr,
+ * @meta and @raw_data
*/
struct v4l2_format {
__u32 type;
@@ -2570,6 +2586,9 @@ struct v4l2_dbg_chip_info {
* @flags: additional buffer management attributes (ignored unless the
* queue has V4L2_BUF_CAP_SUPPORTS_MMAP_CACHE_HINTS capability
* and configured for MMAP streaming I/O).
+ * @max_num_buffers: if V4L2_BUF_CAP_SUPPORTS_MAX_NUM_BUFFERS capability flag is set
+ * this field indicate the maximum possible number of buffers
+ * for this queue.
* @reserved: future extensions
*/
struct v4l2_create_buffers {
@@ -2579,7 +2598,22 @@ struct v4l2_create_buffers {
struct v4l2_format format;
__u32 capabilities;
__u32 flags;
- __u32 reserved[6];
+ __u32 max_num_buffers;
+ __u32 reserved[5];
+};
+
+/**
+ * struct v4l2_remove_buffers - VIDIOC_REMOVE_BUFS argument
+ * @index: the first buffer to be removed
+ * @count: number of buffers to removed
+ * @type: enum v4l2_buf_type
+ * @reserved: future extensions
+ */
+struct v4l2_remove_buffers {
+ __u32 index;
+ __u32 count;
+ __u32 type;
+ __u32 reserved[13];
};
/*
@@ -2681,6 +2715,8 @@ struct v4l2_create_buffers {
#define VIDIOC_DBG_G_CHIP_INFO _IOWR('V', 102, struct v4l2_dbg_chip_info)
#define VIDIOC_QUERY_EXT_CTRL _IOWR('V', 103, struct v4l2_query_ext_ctrl)
+#define VIDIOC_REMOVE_BUFS _IOWR('V', 104, struct v4l2_remove_buffers)
+
/* Reminder: when adding new ioctls please add support for them to
drivers/media/v4l2-core/v4l2-compat-ioctl32.c as well! */
diff --git a/meson.build b/meson.build
index 39e4947f..0ef4cdaa 100644
--- a/meson.build
+++ b/meson.build
@@ -2,7 +2,7 @@
project('libcamera', 'c', 'cpp',
meson_version : '>= 0.60',
- version : '0.2.0',
+ version : '0.3.0',
default_options : [
'werror=true',
'warning_level=2',
@@ -74,6 +74,10 @@ cc = meson.get_compiler('c')
cxx = meson.get_compiler('cpp')
config_h = configuration_data()
+if cc.has_header_symbol('fcntl.h', 'F_ADD_SEALS', prefix : '#define _GNU_SOURCE')
+ config_h.set('HAVE_FILE_SEALS', 1)
+endif
+
if cc.has_header_symbol('unistd.h', 'issetugid')
config_h.set('HAVE_ISSETUGID', 1)
endif
diff --git a/src/android/camera_capabilities.cpp b/src/android/camera_capabilities.cpp
index 6f4d48de..71043e12 100644
--- a/src/android/camera_capabilities.cpp
+++ b/src/android/camera_capabilities.cpp
@@ -1081,7 +1081,7 @@ int CameraCapabilities::initializeStaticMetadata()
}
{
- const Span<const Rectangle> &rects =
+ const Span<const Rectangle> rects =
properties.get(properties::PixelArrayActiveAreas).value_or(Span<const Rectangle>{});
std::vector<int32_t> data{
static_cast<int32_t>(rects[0].x),
diff --git a/src/apps/cam/camera_session.cpp b/src/apps/cam/camera_session.cpp
index f13355ba..097dc479 100644
--- a/src/apps/cam/camera_session.cpp
+++ b/src/apps/cam/camera_session.cpp
@@ -39,9 +39,14 @@ CameraSession::CameraSession(CameraManager *cm,
{
char *endptr;
unsigned long index = strtoul(cameraId.c_str(), &endptr, 10);
- if (*endptr == '\0' && index > 0 && index <= cm->cameras().size())
- camera_ = cm->cameras()[index - 1];
- else
+
+ if (*endptr == '\0' && index > 0) {
+ auto cameras = cm->cameras();
+ if (index <= cameras.size())
+ camera_ = cameras[index - 1];
+ }
+
+ if (!camera_)
camera_ = cm->get(cameraId);
if (!camera_) {
diff --git a/src/gstreamer/gstlibcamerasrc.cpp b/src/gstreamer/gstlibcamerasrc.cpp
index 6a95b6af..9680d809 100644
--- a/src/gstreamer/gstlibcamerasrc.cpp
+++ b/src/gstreamer/gstlibcamerasrc.cpp
@@ -385,13 +385,14 @@ gst_libcamera_src_open(GstLibcameraSrc *self)
return false;
}
} else {
- if (cm->cameras().empty()) {
+ auto cameras = cm->cameras();
+ if (cameras.empty()) {
GST_ELEMENT_ERROR(self, RESOURCE, NOT_FOUND,
("Could not find any supported camera on this system."),
("libcamera::CameraMananger::cameras() is empty"));
return false;
}
- cam = cm->cameras()[0];
+ cam = cameras[0];
}
GST_INFO_OBJECT(self, "Using camera '%s'", cam->id().c_str());
diff --git a/src/ipa/ipu3/meson.build b/src/ipa/ipu3/meson.build
index 66c39843..e76f97c0 100644
--- a/src/ipa/ipu3/meson.build
+++ b/src/ipa/ipu3/meson.build
@@ -15,9 +15,8 @@ ipu3_ipa_sources += ipu3_ipa_algorithms
mod = shared_module(ipa_name,
[ipu3_ipa_sources, libcamera_generated_ipa_headers],
name_prefix : '',
- include_directories : [ipa_includes, libipa_includes],
- dependencies : libcamera_private,
- link_with : libipa,
+ include_directories : [ipa_includes],
+ dependencies : [libcamera_private, libipa_dep],
install : true,
install_dir : ipa_install_dir)
diff --git a/src/ipa/libipa/agc_mean_luminance.cpp b/src/ipa/libipa/agc_mean_luminance.cpp
index 271b5ae4..f97ef117 100644
--- a/src/ipa/libipa/agc_mean_luminance.cpp
+++ b/src/ipa/libipa/agc_mean_luminance.cpp
@@ -59,9 +59,9 @@ static constexpr double kDefaultRelativeLuminanceTarget = 0.16;
/**
* \enum AgcMeanLuminance::AgcConstraint::Bound
* \brief Specify whether the constraint defines a lower or upper bound
- * \var AgcMeanLuminance::AgcConstraint::lower
+ * \var AgcMeanLuminance::AgcConstraint::Lower
* \brief The constraint defines a lower bound
- * \var AgcMeanLuminance::AgcConstraint::upper
+ * \var AgcMeanLuminance::AgcConstraint::Upper
* \brief The constraint defines an upper bound
*/
@@ -209,7 +209,7 @@ int AgcMeanLuminance::parseConstraintModes(const YamlObject &tuningData)
*/
if (constraintModes_.empty()) {
AgcConstraint constraint = {
- AgcConstraint::Bound::lower,
+ AgcConstraint::Bound::Lower,
0.98,
1.0,
0.5
@@ -467,11 +467,11 @@ double AgcMeanLuminance::constraintClampGain(uint32_t constraintModeIndex,
double newGain = constraint.yTarget * hist.bins() /
hist.interQuantileMean(constraint.qLo, constraint.qHi);
- if (constraint.bound == AgcConstraint::Bound::lower &&
+ if (constraint.bound == AgcConstraint::Bound::Lower &&
newGain > gain)
gain = newGain;
- if (constraint.bound == AgcConstraint::Bound::upper &&
+ if (constraint.bound == AgcConstraint::Bound::Upper &&
newGain < gain)
gain = newGain;
}
diff --git a/src/ipa/libipa/agc_mean_luminance.h b/src/ipa/libipa/agc_mean_luminance.h
index 0a81c6d2..576d28be 100644
--- a/src/ipa/libipa/agc_mean_luminance.h
+++ b/src/ipa/libipa/agc_mean_luminance.h
@@ -12,6 +12,8 @@
#include <tuple>
#include <vector>
+#include <libcamera/base/utils.h>
+
#include <libcamera/controls.h>
#include "libcamera/internal/yaml_parser.h"
@@ -31,8 +33,8 @@ public:
struct AgcConstraint {
enum class Bound {
- lower = 0,
- upper = 1
+ Lower = 0,
+ Upper = 1
};
Bound bound;
double qLo;
diff --git a/src/ipa/libipa/camera_sensor_helper.cpp b/src/ipa/libipa/camera_sensor_helper.cpp
index 2cd61fcc..782ff990 100644
--- a/src/ipa/libipa/camera_sensor_helper.cpp
+++ b/src/ipa/libipa/camera_sensor_helper.cpp
@@ -369,30 +369,26 @@ static constexpr double expGainDb(double step)
class CameraSensorHelperAr0521 : public CameraSensorHelper
{
public:
- uint32_t gainCode(double gain) const override;
- double gain(uint32_t gainCode) const override;
-
-private:
- static constexpr double kStep_ = 16;
-};
-
-uint32_t CameraSensorHelperAr0521::gainCode(double gain) const
-{
- gain = std::clamp(gain, 1.0, 15.5);
- unsigned int coarse = std::log2(gain);
- unsigned int fine = (gain / (1 << coarse) - 1) * kStep_;
+ uint32_t gainCode(double gain) const override
+ {
+ gain = std::clamp(gain, 1.0, 15.5);
+ unsigned int coarse = std::log2(gain);
+ unsigned int fine = (gain / (1 << coarse) - 1) * kStep_;
- return (coarse << 4) | (fine & 0xf);
-}
+ return (coarse << 4) | (fine & 0xf);
+ }
-double CameraSensorHelperAr0521::gain(uint32_t gainCode) const
-{
- unsigned int coarse = gainCode >> 4;
- unsigned int fine = gainCode & 0xf;
+ double gain(uint32_t gainCode) const override
+ {
+ unsigned int coarse = gainCode >> 4;
+ unsigned int fine = gainCode & 0xf;
- return (1 << coarse) * (1 + fine / kStep_);
-}
+ return (1 << coarse) * (1 + fine / kStep_);
+ }
+private:
+ static constexpr double kStep_ = 16;
+};
REGISTER_CAMERA_SENSOR_HELPER("ar0521", CameraSensorHelperAr0521)
class CameraSensorHelperImx219 : public CameraSensorHelper
diff --git a/src/ipa/libipa/matrix.cpp b/src/ipa/libipa/matrix.cpp
new file mode 100644
index 00000000..8346f0d3
--- /dev/null
+++ b/src/ipa/libipa/matrix.cpp
@@ -0,0 +1,149 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2024, Paul Elder <paul.elder@ideasonboard.com>
+ *
+ * Matrix and related operations
+ */
+
+#include "matrix.h"
+
+#include <libcamera/base/log.h>
+
+/**
+ * \file matrix.h
+ * \brief Matrix class
+ */
+
+namespace libcamera {
+
+LOG_DEFINE_CATEGORY(Matrix)
+
+namespace ipa {
+
+/**
+ * \class Matrix
+ * \brief Matrix class
+ * \tparam T Type of numerical values to be stored in the matrix
+ * \tparam Rows Number of rows in the matrix
+ * \tparam Cols Number of columns in the matrix
+ */
+
+/**
+ * \fn Matrix::Matrix()
+ * \brief Construct a zero matrix
+ */
+
+/**
+ * \fn Matrix::Matrix(const std::vector<T> &data)
+ * \brief Construct a matrix from supplied data
+ * \param[in] data Data from which to construct a matrix
+ *
+ * \a data is a one-dimensional vector and will be turned into a matrix in
+ * row-major order. The size of \a data must be equal to the product of the
+ * number of rows and columns of the matrix (Rows x Cols).
+ */
+
+/**
+ * \fn Matrix::identity()
+ * \brief Construct an identity matrix
+ */
+
+/**
+ * \fn Matrix::toString()
+ * \brief Assemble and return a string describing the matrix
+ * \return A string describing the matrix
+ */
+
+/**
+ * \fn Span<const T, Cols> Matrix::operator[](size_t i) const
+ * \brief Index to a row in the matrix
+ * \param[in] i Index of row to retrieve
+ *
+ * This operator[] returns a Span, which can then be indexed into again with
+ * another operator[], allowing a convenient m[i][j] to access elements of the
+ * matrix. Note that the lifetime of the Span returned by this first-level
+ * operator[] is bound to that of the Matrix itself, so it is not recommended
+ * to save the Span that is the result of this operator[].
+ *
+ * \return Row \a i from the matrix, as a Span
+ */
+
+/**
+ * \fn Matrix::operator[](size_t i)
+ * \copydoc Matrix::operator[](size_t i) const
+ */
+
+/**
+ * \fn Matrix<T, Rows, Cols> &Matrix::operator*=(U d)
+ * \brief Multiply the matrix by a scalar in-place
+ * \tparam U Type of the numerical scalar value
+ * \param d The scalar multiplier
+ * \return Product of this matrix and scalar \a d
+ */
+
+/**
+ * \fn Matrix::Matrix<U, Rows, Cols> operator*(T d, const Matrix<U, Rows, Cols> &m)
+ * \brief Multiply the matrix by a scalar
+ * \tparam T Type of the numerical scalar value
+ * \tparam U Type of numerical values in the matrix
+ * \tparam Rows Number of rows in the matrix
+ * \tparam Cols Number of columns in the matrix
+ * \param d The scalar multiplier
+ * \param m The matrix
+ * \return Product of scalar \a d and matrix \a m
+ */
+
+/**
+ * \fn Matrix::Matrix<U, Rows, Cols> operator*(const Matrix<U, Rows, Cols> &m, T d)
+ * \copydoc operator*(T d, const Matrix<U, Rows, Cols> &m)
+ */
+
+/**
+ * \fn Matrix<T, R1, C2> operator*(const Matrix<T, R1, C1> &m1, const Matrix<T, R2, C2> &m2)
+ * \brief Matrix multiplication
+ * \tparam T Type of numerical values in the matrices
+ * \tparam R1 Number of rows in the first matrix
+ * \tparam C1 Number of columns in the first matrix
+ * \tparam R2 Number of rows in the second matrix
+ * \tparam C2 Number of columns in the second matrix
+ * \param m1 Multiplicand matrix
+ * \param m2 Multiplier matrix
+ * \return Matrix product of matrices \a m1 and \a m2
+ */
+
+/**
+ * \fn Matrix<T, Rows, Cols> operator+(const Matrix<T, Rows, Cols> &m1, const Matrix<T, Rows, Cols> &m2)
+ * \brief Matrix addition
+ * \tparam T Type of numerical values in the matrices
+ * \tparam Rows Number of rows in the matrices
+ * \tparam Cols Number of columns in the matrices
+ * \param m1 Summand matrix
+ * \param m2 Summand matrix
+ * \return Matrix sum of matrices \a m1 and \a m2
+ */
+
+#ifndef __DOXYGEN__
+/*
+ * The YAML data shall be a list of numerical values. Its size shall be equal
+ * to the product of the number of rows and columns of the matrix (Rows x
+ * Cols). The values shall be stored in row-major order.
+ */
+bool matrixValidateYaml(const YamlObject &obj, unsigned int size)
+{
+ if (!obj.isList())
+ return false;
+
+ if (obj.size() != size) {
+ LOG(Matrix, Error)
+ << "Wrong number of values in matrix: expected "
+ << size << ", got " << obj.size();
+ return false;
+ }
+
+ return true;
+}
+#endif /* __DOXYGEN__ */
+
+} /* namespace ipa */
+
+} /* namespace libcamera */
diff --git a/src/ipa/libipa/matrix.h b/src/ipa/libipa/matrix.h
new file mode 100644
index 00000000..8aa8f343
--- /dev/null
+++ b/src/ipa/libipa/matrix.h
@@ -0,0 +1,204 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2024, Paul Elder <paul.elder@ideasonboard.com>
+ *
+ * Matrix and related operations
+ */
+#pragma once
+
+#include <algorithm>
+#include <cmath>
+#include <sstream>
+#include <vector>
+
+#include <libcamera/base/log.h>
+#include <libcamera/base/span.h>
+
+#include "libcamera/internal/yaml_parser.h"
+
+namespace libcamera {
+
+LOG_DECLARE_CATEGORY(Matrix)
+
+namespace ipa {
+
+#ifndef __DOXYGEN__
+template<typename T, unsigned int Rows, unsigned int Cols,
+ std::enable_if_t<std::is_arithmetic_v<T>> * = nullptr>
+#else
+template<typename T, unsigned int Rows, unsigned int Cols>
+#endif /* __DOXYGEN__ */
+class Matrix
+{
+public:
+ Matrix()
+ {
+ data_.fill(static_cast<T>(0));
+ }
+
+ Matrix(const std::vector<T> &data)
+ {
+ std::copy(data.begin(), data.end(), data_.begin());
+ }
+
+ static Matrix identity()
+ {
+ Matrix ret;
+ for (size_t i = 0; i < std::min(Rows, Cols); i++)
+ ret[i][i] = static_cast<T>(1);
+ return ret;
+ }
+
+ ~Matrix() = default;
+
+ const std::string toString() const
+ {
+ std::stringstream out;
+
+ out << "Matrix { ";
+ for (unsigned int i = 0; i < Rows; i++) {
+ out << "[ ";
+ for (unsigned int j = 0; j < Cols; j++) {
+ out << (*this)[i][j];
+ out << ((j + 1 < Cols) ? ", " : " ");
+ }
+ out << ((i + 1 < Rows) ? "], " : "]");
+ }
+ out << " }";
+
+ return out.str();
+ }
+
+ Span<const T, Cols> operator[](size_t i) const
+ {
+ return Span<const T, Cols>{ &data_.data()[i * Cols], Cols };
+ }
+
+ Span<T, Cols> operator[](size_t i)
+ {
+ return Span<T, Cols>{ &data_.data()[i * Cols], Cols };
+ }
+
+#ifndef __DOXYGEN__
+ template<typename U, std::enable_if_t<std::is_arithmetic_v<U>>>
+#else
+ template<typename U>
+#endif /* __DOXYGEN__ */
+ Matrix<T, Rows, Cols> &operator*=(U d)
+ {
+ for (unsigned int i = 0; i < Rows * Cols; i++)
+ data_[i] *= d;
+ return *this;
+ }
+
+private:
+ std::array<T, Rows * Cols> data_;
+};
+
+#ifndef __DOXYGEN__
+template<typename T, typename U, unsigned int Rows, unsigned int Cols,
+ std::enable_if_t<std::is_arithmetic_v<T>> * = nullptr>
+#else
+template<typename T, typename U, unsigned int Rows, unsigned int Cols>
+#endif /* __DOXYGEN__ */
+Matrix<U, Rows, Cols> operator*(T d, const Matrix<U, Rows, Cols> &m)
+{
+ Matrix<U, Rows, Cols> result;
+
+ for (unsigned int i = 0; i < Rows; i++) {
+ for (unsigned int j = 0; j < Cols; j++)
+ result[i][j] = d * m[i][j];
+ }
+
+ return result;
+}
+
+#ifndef __DOXYGEN__
+template<typename T, typename U, unsigned int Rows, unsigned int Cols,
+ std::enable_if_t<std::is_arithmetic_v<T>> * = nullptr>
+#else
+template<typename T, typename U, unsigned int Rows, unsigned int Cols>
+#endif /* __DOXYGEN__ */
+Matrix<U, Rows, Cols> operator*(const Matrix<U, Rows, Cols> &m, T d)
+{
+ return d * m;
+}
+
+#ifndef __DOXYGEN__
+template<typename T,
+ unsigned int R1, unsigned int C1,
+ unsigned int R2, unsigned int C2,
+ std::enable_if_t<C1 == R2> * = nullptr>
+#else
+template<typename T, unsigned int R1, unsigned int C1, unsigned int R2, unsigned in C2>
+#endif /* __DOXYGEN__ */
+Matrix<T, R1, C2> operator*(const Matrix<T, R1, C1> &m1, const Matrix<T, R2, C2> &m2)
+{
+ Matrix<T, R1, C2> result;
+
+ for (unsigned int i = 0; i < R1; i++) {
+ for (unsigned int j = 0; j < C2; j++) {
+ T sum = 0;
+
+ for (unsigned int k = 0; k < C1; k++)
+ sum += m1[i][k] * m2[k][j];
+
+ result[i][j] = sum;
+ }
+ }
+
+ return result;
+}
+
+template<typename T, unsigned int Rows, unsigned int Cols>
+Matrix<T, Rows, Cols> operator+(const Matrix<T, Rows, Cols> &m1, const Matrix<T, Rows, Cols> &m2)
+{
+ Matrix<T, Rows, Cols> result;
+
+ for (unsigned int i = 0; i < Rows; i++) {
+ for (unsigned int j = 0; j < Cols; j++)
+ result[i][j] = m1[i][j] + m2[i][j];
+ }
+
+ return result;
+}
+
+#ifndef __DOXYGEN__
+bool matrixValidateYaml(const YamlObject &obj, unsigned int size);
+#endif /* __DOXYGEN__ */
+
+} /* namespace ipa */
+
+#ifndef __DOXYGEN__
+template<typename T, unsigned int Rows, unsigned int Cols>
+std::ostream &operator<<(std::ostream &out, const ipa::Matrix<T, Rows, Cols> &m)
+{
+ out << m.toString();
+ return out;
+}
+
+template<typename T, unsigned int Rows, unsigned int Cols>
+struct YamlObject::Getter<ipa::Matrix<T, Rows, Cols>> {
+ std::optional<ipa::Matrix<T, Rows, Cols>> get(const YamlObject &obj) const
+ {
+ if (!ipa::matrixValidateYaml(obj, Rows * Cols))
+ return std::nullopt;
+
+ ipa::Matrix<T, Rows, Cols> matrix;
+ T *data = &matrix[0][0];
+
+ unsigned int i = 0;
+ for (const YamlObject &entry : obj.asList()) {
+ const auto value = entry.get<T>();
+ if (!value)
+ return std::nullopt;
+
+ data[i++] = *value;
+ }
+
+ return matrix;
+ }
+};
+#endif /* __DOXYGEN__ */
+
+} /* namespace libcamera */
diff --git a/src/ipa/libipa/matrix_interpolator.cpp b/src/ipa/libipa/matrix_interpolator.cpp
new file mode 100644
index 00000000..04ca177f
--- /dev/null
+++ b/src/ipa/libipa/matrix_interpolator.cpp
@@ -0,0 +1,110 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2024, Paul Elder <paul.elder@ideasonboard.com>
+ *
+ * Helper class for interpolating maps of matrices
+ */
+#include "matrix_interpolator.h"
+
+#include <algorithm>
+#include <string>
+
+#include <libcamera/base/log.h>
+
+#include "libcamera/internal/yaml_parser.h"
+
+#include "matrix.h"
+
+/**
+ * \file matrix_interpolator.h
+ * \brief Helper class for interpolating maps of matrices
+ */
+
+namespace libcamera {
+
+LOG_DEFINE_CATEGORY(MatrixInterpolator)
+
+namespace ipa {
+
+/**
+ * \class MatrixInterpolator
+ * \brief Class for storing, retrieving, and interpolating matrices
+ * \tparam T Type of numerical values to be stored in the matrices
+ * \tparam R Number of rows in the matrices
+ * \tparam C Number of columns in the matrices
+ *
+ * The main use case is to pass a map from color temperatures to corresponding
+ * matrices (eg. color correction), and then requesting a matrix for a specific
+ * color temperature. This class will abstract away the interpolation portion.
+ */
+
+/**
+ * \fn MatrixInterpolator::MatrixInterpolator(const std::map<unsigned int, Matrix<T, R, C>> &matrices)
+ * \brief Construct a matrix interpolator from a map of matrices
+ * \param matrices Map from which to construct the matrix interpolator
+ */
+
+/**
+ * \fn MatrixInterpolator::reset()
+ * \brief Reset the matrix interpolator content to a single identity matrix
+ */
+
+/**
+ * \fn int MatrixInterpolator<T, R, C>::readYaml()
+ * \brief Initialize an MatrixInterpolator instance from yaml
+ * \tparam T Type of data stored in the matrices
+ * \tparam R Number of rows of the matrices
+ * \tparam C Number of columns of the matrices
+ * \param[in] yaml The yaml object that contains the map of unsigned integers to matrices
+ * \param[in] key_name The name of the key in the yaml object
+ * \param[in] matrix_name The name of the matrix in the yaml object
+ *
+ * The yaml object is expected to be a list of maps. Each map has two or more
+ * pairs: one of \a key_name to the key value (usually color temperature), and
+ * one or more of \a matrix_name to the matrix. This is a bit difficult to
+ * explain, so here is an example (in python, as it is easier to parse than
+ * yaml):
+ * [
+ * {
+ * 'ct': 2860,
+ * 'ccm': [ 2.12089, -0.52461, -0.59629,
+ * -0.85342, 2.80445, -0.95103,
+ * -0.26897, -1.14788, 2.41685 ],
+ * 'offsets': [ 0, 0, 0 ]
+ * },
+ *
+ * {
+ * 'ct': 2960,
+ * 'ccm': [ 2.26962, -0.54174, -0.72789,
+ * -0.77008, 2.60271, -0.83262,
+ * -0.26036, -1.51254, 2.77289 ],
+ * 'offsets': [ 0, 0, 0 ]
+ * },
+ *
+ * {
+ * 'ct': 3603,
+ * 'ccm': [ 2.18644, -0.66148, -0.52496,
+ * -0.77828, 2.69474, -0.91645,
+ * -0.25239, -0.83059, 2.08298 ],
+ * 'offsets': [ 0, 0, 0 ]
+ * },
+ * ]
+ *
+ * In this case, \a key_name would be 'ct', and \a matrix_name can be either
+ * 'ccm' or 'offsets'. This way multiple matrix interpolators can be defined in
+ * one set of color temperature ranges in the tuning file, and they can be
+ * retrieved separately with the \a matrix_name parameter.
+ *
+ * \return Zero on success, negative error code otherwise
+ */
+
+/**
+ * \fn Matrix<T, R, C> MatrixInterpolator<T, R, C>::get(unsigned int key)
+ * \brief Retrieve a matrix from the list of matrices, interpolating if necessary
+ * \param[in] key The unsigned integer key of the matrix to retrieve
+ * \return The matrix corresponding to the color temperature
+ */
+
+} /* namespace ipa */
+
+} /* namespace libcamera */
diff --git a/src/ipa/libipa/matrix_interpolator.h b/src/ipa/libipa/matrix_interpolator.h
new file mode 100644
index 00000000..087c4fd1
--- /dev/null
+++ b/src/ipa/libipa/matrix_interpolator.h
@@ -0,0 +1,122 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2024, Paul Elder <paul.elder@ideasonboard.com>
+ *
+ * Helper class for interpolating maps of matrices
+ */
+
+#pragma once
+
+#include <algorithm>
+#include <map>
+#include <string>
+#include <tuple>
+
+#include <libcamera/base/log.h>
+
+#include "libcamera/internal/yaml_parser.h"
+
+#include "matrix.h"
+
+namespace libcamera {
+
+LOG_DECLARE_CATEGORY(MatrixInterpolator)
+
+namespace ipa {
+
+#ifndef __DOXYGEN__
+template<typename T, unsigned int R, unsigned int C,
+ std::enable_if_t<std::is_arithmetic_v<T>> * = nullptr>
+#else
+template<typename T, unsigned int R, unsigned int C>
+#endif /* __DOXYGEN__ */
+class MatrixInterpolator
+{
+public:
+ MatrixInterpolator()
+ {
+ reset();
+ }
+
+ MatrixInterpolator(const std::map<unsigned int, Matrix<T, R, C>> &matrices)
+ {
+ for (const auto &pair : matrices)
+ matrices_[pair.first] = pair.second;
+ }
+
+ ~MatrixInterpolator() {}
+
+ void reset()
+ {
+ matrices_.clear();
+ matrices_[0] = Matrix<T, R, C>::identity();
+ }
+
+ int readYaml(const libcamera::YamlObject &yaml,
+ const std::string &key_name,
+ const std::string &matrix_name)
+ {
+ matrices_.clear();
+
+ if (!yaml.isList()) {
+ LOG(MatrixInterpolator, Error) << "yaml object must be a list";
+ return -EINVAL;
+ }
+
+ for (const auto &value : yaml.asList()) {
+ unsigned int ct = std::stoul(value[key_name].get<std::string>(""));
+ std::optional<Matrix<T, R, C>> matrix =
+ value[matrix_name].get<Matrix<T, R, C>>();
+ if (!matrix) {
+ LOG(MatrixInterpolator, Error) << "Failed to read matrix";
+ return -EINVAL;
+ }
+
+ matrices_[ct] = *matrix;
+
+ LOG(MatrixInterpolator, Debug)
+ << "Read matrix '" << matrix_name << "' for key '"
+ << key_name << "' " << ct << ": "
+ << matrices_[ct].toString();
+ }
+
+ if (matrices_.size() < 1) {
+ LOG(MatrixInterpolator, Error) << "Need at least one matrix";
+ return -EINVAL;
+ }
+
+ return 0;
+ }
+
+ Matrix<T, R, C> get(unsigned int ct)
+ {
+ ASSERT(matrices_.size() > 0);
+
+ if (matrices_.size() == 1 ||
+ ct <= matrices_.begin()->first)
+ return matrices_.begin()->second;
+
+ if (ct >= matrices_.rbegin()->first)
+ return matrices_.rbegin()->second;
+
+ if (matrices_.find(ct) != matrices_.end())
+ return matrices_[ct];
+
+ /* The above four guarantee that this will succeed */
+ auto iter = matrices_.upper_bound(ct);
+ unsigned int ctUpper = iter->first;
+ unsigned int ctLower = (--iter)->first;
+
+ double lambda = (ct - ctLower) / static_cast<double>(ctUpper - ctLower);
+ Matrix<T, R, C> ret =
+ lambda * matrices_[ctUpper] + (1.0 - lambda) * matrices_[ctLower];
+ return ret;
+ }
+
+private:
+ std::map<unsigned int, Matrix<T, R, C>> matrices_;
+};
+
+} /* namespace ipa */
+
+} /* namespace libcamera */
diff --git a/src/ipa/libipa/meson.build b/src/ipa/libipa/meson.build
index 7ce885da..eff8ce26 100644
--- a/src/ipa/libipa/meson.build
+++ b/src/ipa/libipa/meson.build
@@ -7,7 +7,11 @@ libipa_headers = files([
'exposure_mode_helper.h',
'fc_queue.h',
'histogram.h',
+ 'matrix.h',
+ 'matrix_interpolator.h',
'module.h',
+ 'pwl.h',
+ 'vector.h',
])
libipa_sources = files([
@@ -17,7 +21,11 @@ libipa_sources = files([
'exposure_mode_helper.cpp',
'fc_queue.cpp',
'histogram.cpp',
+ 'matrix.cpp',
+ 'matrix_interpolator.cpp',
'module.cpp',
+ 'pwl.cpp',
+ 'vector.cpp',
])
libipa_includes = include_directories('..')
@@ -25,3 +33,7 @@ libipa_includes = include_directories('..')
libipa = static_library('ipa', [libipa_sources, libipa_headers],
include_directories : ipa_includes,
dependencies : libcamera_private)
+
+libipa_dep = declare_dependency(sources : libipa_headers,
+ include_directories : libipa_includes,
+ link_with : libipa)
diff --git a/src/ipa/libipa/pwl.cpp b/src/ipa/libipa/pwl.cpp
new file mode 100644
index 00000000..9b213754
--- /dev/null
+++ b/src/ipa/libipa/pwl.cpp
@@ -0,0 +1,459 @@
+/* SPDX-License-Identifier: BSD-2-Clause */
+/*
+ * Copyright (C) 2019, Raspberry Pi Ltd
+ * Copyright (C) 2024, Ideas on Board Oy
+ *
+ * Piecewise linear functions
+ */
+
+#include "pwl.h"
+
+#include <assert.h>
+#include <cmath>
+#include <sstream>
+#include <stdexcept>
+
+/**
+ * \file pwl.h
+ * \brief Piecewise linear functions
+ */
+
+namespace libcamera {
+
+namespace ipa {
+
+/**
+ * \class Pwl
+ * \brief Describe a univariate piecewise linear function in two-dimensional
+ * real space
+ *
+ * A piecewise linear function is a univariate function that maps reals to
+ * reals, and it is composed of multiple straight-line segments.
+ *
+ * While a mathematical piecewise linear function would usually be defined by
+ * a list of linear functions and for which values of the domain they apply,
+ * this Pwl class is instead defined by a list of points at which these line
+ * segments intersect. These intersecting points are known as knots.
+ *
+ * https://en.wikipedia.org/wiki/Piecewise_linear_function
+ *
+ * A consequence of the Pwl class being defined by knots instead of linear
+ * functions is that the values of the piecewise linear function past the ends
+ * of the function are constants as opposed to linear functions. In a
+ * mathematical piecewise linear function that is defined by multiple linear
+ * functions, the ends of the function are also linear functions and hence grow
+ * to infinity (or negative infinity). However, since this Pwl class is defined
+ * by knots, the y-value of the leftmost and rightmost knots will hold for all
+ * x values to negative infinity and positive infinity, respectively.
+ */
+
+/**
+ * \typedef Pwl::Point
+ * \brief Describe a point in two-dimensional real space
+ */
+
+/**
+ * \class Pwl::Interval
+ * \brief Describe an interval in one-dimensional real space
+ */
+
+/**
+ * \fn Pwl::Interval::Interval(double _start, double _end)
+ * \brief Construct an interval
+ * \param[in] _start Start of the interval
+ * \param[in] _end End of the interval
+ */
+
+/**
+ * \fn Pwl::Interval::contains
+ * \brief Check if a given value falls within the interval
+ * \param[in] value Value to check
+ * \return True if the value falls within the interval, including its bounds,
+ * or false otherwise
+ */
+
+/**
+ * \fn Pwl::Interval::clamp
+ * \brief Clamp a value such that it is within the interval
+ * \param[in] value Value to clamp
+ * \return The clamped value
+ */
+
+/**
+ * \fn Pwl::Interval::length
+ * \brief Compute the length of the interval
+ * \return The length of the interval
+ */
+
+/**
+ * \var Pwl::Interval::start
+ * \brief Start of the interval
+ */
+
+/**
+ * \var Pwl::Interval::end
+ * \brief End of the interval
+ */
+
+/**
+ * \brief Construct an empty piecewise linear function
+ */
+Pwl::Pwl()
+{
+}
+
+/**
+ * \brief Construct a piecewise linear function from a list of 2D points
+ * \param[in] points Vector of points from which to construct the piecewise
+ * linear function
+ *
+ * \a points must be in ascending order of x-value.
+ */
+Pwl::Pwl(const std::vector<Point> &points)
+ : points_(points)
+{
+}
+
+/**
+ * \copydoc Pwl::Pwl(const std::vector<Point> &points)
+ *
+ * The contents of the \a points vector is moved to the newly constructed Pwl
+ * instance.
+ */
+Pwl::Pwl(std::vector<Point> &&points)
+ : points_(std::move(points))
+{
+}
+
+/**
+ * \brief Append a point to the end of the piecewise linear function
+ * \param[in] x x-coordinate of the point to add to the piecewise linear function
+ * \param[in] y y-coordinate of the point to add to the piecewise linear function
+ * \param[in] eps Epsilon for the minimum x distance between points (optional)
+ *
+ * The point's x-coordinate must be greater than the x-coordinate of the last
+ * (= greatest) point already in the piecewise linear function.
+ */
+void Pwl::append(double x, double y, const double eps)
+{
+ if (points_.empty() || points_.back().x() + eps < x)
+ points_.push_back(Point({ x, y }));
+}
+
+/**
+ * \brief Prepend a point to the beginning of the piecewise linear function
+ * \param[in] x x-coordinate of the point to add to the piecewise linear function
+ * \param[in] y y-coordinate of the point to add to the piecewise linear function
+ * \param[in] eps Epsilon for the minimum x distance between points (optional)
+ *
+ * The point's x-coordinate must be less than the x-coordinate of the first
+ * (= smallest) point already in the piecewise linear function.
+ */
+void Pwl::prepend(double x, double y, const double eps)
+{
+ if (points_.empty() || points_.front().x() - eps > x)
+ points_.insert(points_.begin(), Point({ x, y }));
+}
+
+/**
+ * \fn Pwl::empty() const
+ * \brief Check if the piecewise linear function is empty
+ * \return True if there are no points in the function, false otherwise
+ */
+
+/**
+ * \fn Pwl::size() const
+ * \brief Retrieve the number of points in the piecewise linear function
+ * \return The number of points in the piecewise linear function
+ */
+
+/**
+ * \brief Get the domain of the piecewise linear function
+ * \return An interval representing the domain
+ */
+Pwl::Interval Pwl::domain() const
+{
+ return Interval(points_[0].x(), points_[points_.size() - 1].x());
+}
+
+/**
+ * \brief Get the range of the piecewise linear function
+ * \return An interval representing the range
+ */
+Pwl::Interval Pwl::range() const
+{
+ double lo = points_[0].y(), hi = lo;
+ for (auto &p : points_)
+ lo = std::min(lo, p.y()), hi = std::max(hi, p.y());
+ return Interval(lo, hi);
+}
+
+/**
+ * \brief Evaluate the piecewise linear function
+ * \param[in] x The x value to input into the function
+ * \param[inout] span Initial guess for span
+ * \param[in] updateSpan Set to true to update span
+ *
+ * Evaluate Pwl, optionally supplying an initial guess for the
+ * "span". The "span" may be optionally be updated. If you want to know
+ * the "span" value but don't have an initial guess you can set it to
+ * -1.
+ *
+ * \return The result of evaluating the piecewise linear function at position \a x
+ */
+double Pwl::eval(double x, int *span, bool updateSpan) const
+{
+ int index = findSpan(x, span && *span != -1
+ ? *span
+ : points_.size() / 2 - 1);
+ if (span && updateSpan)
+ *span = index;
+ return points_[index].y() +
+ (x - points_[index].x()) * (points_[index + 1].y() - points_[index].y()) /
+ (points_[index + 1].x() - points_[index].x());
+}
+
+int Pwl::findSpan(double x, int span) const
+{
+ /*
+ * Pwls are generally small, so linear search may well be faster than
+ * binary, though could review this if large Pwls start turning up.
+ */
+ int lastSpan = points_.size() - 2;
+ /*
+ * some algorithms may call us with span pointing directly at the last
+ * control point
+ */
+ span = std::max(0, std::min(lastSpan, span));
+ while (span < lastSpan && x >= points_[span + 1].x())
+ span++;
+ while (span && x < points_[span].x())
+ span--;
+ return span;
+}
+
+/**
+ * \brief Compute the inverse function
+ * \param[in] eps Epsilon for the minimum x distance between points (optional)
+ *
+ * The output includes whether the resulting inverse function is a proper
+ * (true) inverse, or only a best effort (e.g. input was non-monotonic).
+ *
+ * \return A pair of the inverse piecewise linear function, and whether or not
+ * the result is a proper/true inverse
+ */
+std::pair<Pwl, bool> Pwl::inverse(const double eps) const
+{
+ bool appended = false, prepended = false, neither = false;
+ Pwl inverse;
+
+ for (Point const &p : points_) {
+ if (inverse.empty()) {
+ inverse.append(p.y(), p.x(), eps);
+ } else if (std::abs(inverse.points_.back().x() - p.y()) <= eps ||
+ std::abs(inverse.points_.front().x() - p.y()) <= eps) {
+ /* do nothing */;
+ } else if (p.y() > inverse.points_.back().x()) {
+ inverse.append(p.y(), p.x(), eps);
+ appended = true;
+ } else if (p.y() < inverse.points_.front().x()) {
+ inverse.prepend(p.y(), p.x(), eps);
+ prepended = true;
+ } else {
+ neither = true;
+ }
+ }
+
+ /*
+ * This is not a proper inverse if we found ourselves putting points
+ * onto both ends of the inverse, or if there were points that couldn't
+ * go on either.
+ */
+ bool trueInverse = !(neither || (appended && prepended));
+
+ return { inverse, trueInverse };
+}
+
+/**
+ * \brief Compose two piecewise linear functions together
+ * \param[in] other The "other" piecewise linear function
+ * \param[in] eps Epsilon for the minimum x distance between points (optional)
+ *
+ * The "this" function is done first, and "other" after.
+ *
+ * \return The composed piecewise linear function
+ */
+Pwl Pwl::compose(Pwl const &other, const double eps) const
+{
+ double thisX = points_[0].x(), thisY = points_[0].y();
+ int thisSpan = 0, otherSpan = other.findSpan(thisY, 0);
+ Pwl result({ Point({ thisX, other.eval(thisY, &otherSpan, false) }) });
+
+ while (thisSpan != (int)points_.size() - 1) {
+ double dx = points_[thisSpan + 1].x() - points_[thisSpan].x(),
+ dy = points_[thisSpan + 1].y() - points_[thisSpan].y();
+ if (std::abs(dy) > eps &&
+ otherSpan + 1 < (int)other.points_.size() &&
+ points_[thisSpan + 1].y() >= other.points_[otherSpan + 1].x() + eps) {
+ /*
+ * next control point in result will be where this
+ * function's y reaches the next span in other
+ */
+ thisX = points_[thisSpan].x() +
+ (other.points_[otherSpan + 1].x() -
+ points_[thisSpan].y()) *
+ dx / dy;
+ thisY = other.points_[++otherSpan].x();
+ } else if (std::abs(dy) > eps && otherSpan > 0 &&
+ points_[thisSpan + 1].y() <=
+ other.points_[otherSpan - 1].x() - eps) {
+ /*
+ * next control point in result will be where this
+ * function's y reaches the previous span in other
+ */
+ thisX = points_[thisSpan].x() +
+ (other.points_[otherSpan + 1].x() -
+ points_[thisSpan].y()) *
+ dx / dy;
+ thisY = other.points_[--otherSpan].x();
+ } else {
+ /* we stay in the same span in other */
+ thisSpan++;
+ thisX = points_[thisSpan].x(),
+ thisY = points_[thisSpan].y();
+ }
+ result.append(thisX, other.eval(thisY, &otherSpan, false),
+ eps);
+ }
+ return result;
+}
+
+/**
+ * \brief Apply function to (x, y) values at every control point
+ * \param[in] f Function to be applied
+ */
+void Pwl::map(std::function<void(double x, double y)> f) const
+{
+ for (auto &pt : points_)
+ f(pt.x(), pt.y());
+}
+
+/**
+ * \brief Apply function to (x, y0, y1) values wherever either Pwl has a
+ * control point.
+ * \param[in] pwl0 First piecewise linear function
+ * \param[in] pwl1 Second piecewise linear function
+ * \param[in] f Function to be applied
+ *
+ * This applies the function \a f to every parameter (x, y0, y1), where x is
+ * the combined list of x-values from \a pwl0 and \a pwl1, y0 is the y-value
+ * for the given x in \a pwl0, and y1 is the y-value for the same x in \a pwl1.
+ */
+void Pwl::map2(Pwl const &pwl0, Pwl const &pwl1,
+ std::function<void(double x, double y0, double y1)> f)
+{
+ int span0 = 0, span1 = 0;
+ double x = std::min(pwl0.points_[0].x(), pwl1.points_[0].x());
+ f(x, pwl0.eval(x, &span0, false), pwl1.eval(x, &span1, false));
+
+ while (span0 < (int)pwl0.points_.size() - 1 ||
+ span1 < (int)pwl1.points_.size() - 1) {
+ if (span0 == (int)pwl0.points_.size() - 1)
+ x = pwl1.points_[++span1].x();
+ else if (span1 == (int)pwl1.points_.size() - 1)
+ x = pwl0.points_[++span0].x();
+ else if (pwl0.points_[span0 + 1].x() > pwl1.points_[span1 + 1].x())
+ x = pwl1.points_[++span1].x();
+ else
+ x = pwl0.points_[++span0].x();
+ f(x, pwl0.eval(x, &span0, false), pwl1.eval(x, &span1, false));
+ }
+}
+
+/**
+ * \brief Combine two Pwls
+ * \param[in] pwl0 First piecewise linear function
+ * \param[in] pwl1 Second piecewise linear function
+ * \param[in] f Function to be applied
+ * \param[in] eps Epsilon for the minimum x distance between points (optional)
+ *
+ * Create a new Pwl where the y values are given by running \a f wherever
+ * either pwl has a knot.
+ *
+ * \return The combined pwl
+ */
+Pwl Pwl::combine(Pwl const &pwl0, Pwl const &pwl1,
+ std::function<double(double x, double y0, double y1)> f,
+ const double eps)
+{
+ Pwl result;
+ map2(pwl0, pwl1, [&](double x, double y0, double y1) {
+ result.append(x, f(x, y0, y1), eps);
+ });
+ return result;
+}
+
+/**
+ * \brief Multiply the piecewise linear function
+ * \param[in] d Scalar multiplier to multiply the function by
+ * \return This function, after it has been multiplied by \a d
+ */
+Pwl &Pwl::operator*=(double d)
+{
+ for (auto &pt : points_)
+ pt[1] *= d;
+ return *this;
+}
+
+/**
+ * \brief Assemble and return a string describing the piecewise linear function
+ * \return A string describing the piecewise linear function
+ */
+std::string Pwl::toString() const
+{
+ std::stringstream ss;
+ ss << "Pwl { ";
+ for (auto &p : points_)
+ ss << "(" << p.x() << ", " << p.y() << ") ";
+ ss << "}";
+ return ss.str();
+}
+
+} /* namespace ipa */
+
+#ifndef __DOXYGEN__
+/*
+ * The YAML data shall be a list of numerical values with an even number of
+ * elements. They are parsed in pairs into x and y points in the piecewise
+ * linear function, and added in order. x must be monotonically increasing.
+ */
+template<>
+std::optional<ipa::Pwl>
+YamlObject::Getter<ipa::Pwl>::get(const YamlObject &obj) const
+{
+ if (!obj.size() || obj.size() % 2)
+ return std::nullopt;
+
+ ipa::Pwl pwl;
+
+ const auto &list = obj.asList();
+
+ for (auto it = list.begin(); it != list.end(); it++) {
+ auto x = it->get<double>();
+ if (!x)
+ return std::nullopt;
+ auto y = (++it)->get<double>();
+ if (!y)
+ return std::nullopt;
+
+ pwl.append(*x, *y);
+ }
+
+ if (pwl.size() != obj.size() / 2)
+ return std::nullopt;
+
+ return pwl;
+}
+#endif /* __DOXYGEN__ */
+
+} /* namespace libcamera */
diff --git a/src/ipa/libipa/pwl.h b/src/ipa/libipa/pwl.h
new file mode 100644
index 00000000..b6f93494
--- /dev/null
+++ b/src/ipa/libipa/pwl.h
@@ -0,0 +1,88 @@
+/* SPDX-License-Identifier: BSD-2-Clause */
+/*
+ * Copyright (C) 2019, Raspberry Pi Ltd
+ *
+ * Piecewise linear functions interface
+ */
+#pragma once
+
+#include <algorithm>
+#include <cmath>
+#include <functional>
+#include <string>
+#include <utility>
+#include <vector>
+
+#include "libcamera/internal/yaml_parser.h"
+
+#include "vector.h"
+
+namespace libcamera {
+
+namespace ipa {
+
+class Pwl
+{
+public:
+ using Point = Vector<double, 2>;
+
+ struct Interval {
+ Interval(double _start, double _end)
+ : start(_start), end(_end) {}
+
+ bool contains(double value)
+ {
+ return value >= start && value <= end;
+ }
+
+ double clamp(double value)
+ {
+ return std::clamp(value, start, end);
+ }
+
+ double length() const { return end - start; }
+
+ double start, end;
+ };
+
+ Pwl();
+ Pwl(const std::vector<Point> &points);
+ Pwl(std::vector<Point> &&points);
+
+ void append(double x, double y, double eps = 1e-6);
+
+ bool empty() const { return points_.empty(); }
+ size_t size() const { return points_.size(); }
+
+ Interval domain() const;
+ Interval range() const;
+
+ double eval(double x, int *span = nullptr,
+ bool updateSpan = true) const;
+
+ std::pair<Pwl, bool> inverse(double eps = 1e-6) const;
+ Pwl compose(const Pwl &other, double eps = 1e-6) const;
+
+ void map(std::function<void(double x, double y)> f) const;
+
+ static Pwl
+ combine(const Pwl &pwl0, const Pwl &pwl1,
+ std::function<double(double x, double y0, double y1)> f,
+ double eps = 1e-6);
+
+ Pwl &operator*=(double d);
+
+ std::string toString() const;
+
+private:
+ static void map2(const Pwl &pwl0, const Pwl &pwl1,
+ std::function<void(double x, double y0, double y1)> f);
+ void prepend(double x, double y, double eps = 1e-6);
+ int findSpan(double x, int span) const;
+
+ std::vector<Point> points_;
+};
+
+} /* namespace ipa */
+
+} /* namespace libcamera */
diff --git a/src/ipa/libipa/vector.cpp b/src/ipa/libipa/vector.cpp
new file mode 100644
index 00000000..bd00b019
--- /dev/null
+++ b/src/ipa/libipa/vector.cpp
@@ -0,0 +1,168 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2024, Paul Elder <paul.elder@ideasonboard.com>
+ *
+ * Vector and related operations
+ */
+
+#include "vector.h"
+
+#include <libcamera/base/log.h>
+
+/**
+ * \file vector.h
+ * \brief Vector class
+ */
+
+namespace libcamera {
+
+LOG_DEFINE_CATEGORY(Vector)
+
+namespace ipa {
+
+/**
+ * \class Vector
+ * \brief Vector class
+ * \tparam T Type of numerical values to be stored in the vector
+ * \tparam Rows Number of dimension of the vector (= number of elements)
+ */
+
+/**
+ * \fn Vector::Vector()
+ * \brief Construct a zero vector
+ */
+
+/**
+ * \fn Vector::Vector(const std::array<T, Rows> &data)
+ * \brief Construct vector from supplied data
+ * \param data Data from which to construct a vector
+ *
+ * The size of \a data must be equal to the dimension size Rows of the vector.
+ */
+
+/**
+ * \fn T Vector::operator[](size_t i) const
+ * \brief Index to an element in the vector
+ * \param i Index of element to retrieve
+ * \return Element at index \a i from the vector
+ */
+
+/**
+ * \fn T &Vector::operator[](size_t i)
+ * \copydoc Vector::operator[](size_t i) const
+ */
+
+/**
+ * \fn Vector::x()
+ * \brief Convenience function to access the first element of the vector
+ * \return The first element of the vector
+ */
+
+/**
+ * \fn Vector::y()
+ * \brief Convenience function to access the second element of the vector
+ * \return The second element of the vector
+ */
+
+/**
+ * \fn Vector::z()
+ * \brief Convenience function to access the third element of the vector
+ * \return The third element of the vector
+ */
+
+/**
+ * \fn Vector::operator-() const
+ * \brief Negate a Vector by negating both all of its coordinates
+ * \return The negated vector
+ */
+
+/**
+ * \fn Vector::operator-(Vector const &other) const
+ * \brief Subtract one vector from another
+ * \param[in] other The other vector
+ * \return The difference of \a other from this vector
+ */
+
+/**
+ * \fn Vector::operator+()
+ * \brief Add two vectors together
+ * \param[in] other The other vector
+ * \return The sum of the two vectors
+ */
+
+/**
+ * \fn Vector::operator*(const Vector<T, Rows> &other) const
+ * \brief Compute the dot product
+ * \param[in] other The other vector
+ * \return The dot product of the two vectors
+ */
+
+/**
+ * \fn Vector::operator*(T factor) const
+ * \brief Multiply the vector by a scalar
+ * \param[in] factor The factor
+ * \return The vector multiplied by \a factor
+ */
+
+/**
+ * \fn Vector::operator/()
+ * \brief Divide the vector by a scalar
+ * \param[in] factor The factor
+ * \return The vector divided by \a factor
+ */
+
+/**
+ * \fn Vector::length2()
+ * \brief Get the squared length of the vector
+ * \return The squared length of the vector
+ */
+
+/**
+ * \fn Vector::length()
+ * \brief Get the length of the vector
+ * \return The length of the vector
+ */
+
+/**
+ * \fn Vector<T, Rows> operator*(const Matrix<T, Rows, Cols> &m, const Vector<T, Cols> &v)
+ * \brief Multiply a matrix by a vector
+ * \tparam T Numerical type of the contents of the matrix and vector
+ * \tparam Rows The number of rows in the matrix
+ * \tparam Cols The number of columns in the matrix (= rows in the vector)
+ * \param m The matrix
+ * \param v The vector
+ * \return Product of matrix \a m and vector \a v
+ */
+
+/**
+ * \fn bool operator==(const Vector<T, Rows> &lhs, const Vector<T, Rows> &rhs)
+ * \brief Compare vectors for equality
+ * \return True if the two vectors are equal, false otherwise
+ */
+
+/**
+ * \fn bool operator!=(const Vector<T, Rows> &lhs, const Vector<T, Rows> &rhs)
+ * \brief Compare vectors for inequality
+ * \return True if the two vectors are not equal, false otherwise
+ */
+
+#ifndef __DOXYGEN__
+bool vectorValidateYaml(const YamlObject &obj, unsigned int size)
+{
+ if (!obj.isList())
+ return false;
+
+ if (obj.size() != size) {
+ LOG(Vector, Error)
+ << "Wrong number of values in YAML vector: expected "
+ << size << ", got " << obj.size();
+ return false;
+ }
+
+ return true;
+}
+#endif /* __DOXYGEN__ */
+
+} /* namespace ipa */
+
+} /* namespace libcamera */
diff --git a/src/ipa/libipa/vector.h b/src/ipa/libipa/vector.h
new file mode 100644
index 00000000..556e0967
--- /dev/null
+++ b/src/ipa/libipa/vector.h
@@ -0,0 +1,219 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2024, Paul Elder <paul.elder@ideasonboard.com>
+ *
+ * Vector and related operations
+ */
+#pragma once
+
+#include <algorithm>
+#include <array>
+#include <cmath>
+#include <sstream>
+
+#include <libcamera/base/log.h>
+#include <libcamera/base/span.h>
+
+#include "libcamera/internal/yaml_parser.h"
+
+#include "matrix.h"
+
+namespace libcamera {
+
+LOG_DECLARE_CATEGORY(Vector)
+
+namespace ipa {
+
+#ifndef __DOXYGEN__
+template<typename T, unsigned int Rows,
+ std::enable_if_t<std::is_arithmetic_v<T>> * = nullptr>
+#else
+template<typename T, unsigned int Rows>
+#endif /* __DOXYGEN__ */
+class Vector
+{
+public:
+ constexpr Vector() = default;
+
+ constexpr Vector(const std::array<T, Rows> &data)
+ {
+ for (unsigned int i = 0; i < Rows; i++)
+ data_[i] = data[i];
+ }
+
+ const T &operator[](size_t i) const
+ {
+ ASSERT(i < data_.size());
+ return data_[i];
+ }
+
+ T &operator[](size_t i)
+ {
+ ASSERT(i < data_.size());
+ return data_[i];
+ }
+
+#ifndef __DOXYGEN__
+ template<bool Dependent = false, typename = std::enable_if_t<Dependent || Rows >= 1>>
+#endif /* __DOXYGEN__ */
+ constexpr T x() const
+ {
+ return data_[0];
+ }
+
+#ifndef __DOXYGEN__
+ template<bool Dependent = false, typename = std::enable_if_t<Dependent || Rows >= 2>>
+#endif /* __DOXYGEN__ */
+ constexpr T y() const
+ {
+ return data_[1];
+ }
+
+#ifndef __DOXYGEN__
+ template<bool Dependent = false, typename = std::enable_if_t<Dependent || Rows >= 3>>
+#endif /* __DOXYGEN__ */
+ constexpr T z() const
+ {
+ return data_[2];
+ }
+
+ constexpr Vector<T, Rows> operator-() const
+ {
+ Vector<T, Rows> ret;
+ for (unsigned int i = 0; i < Rows; i++)
+ ret[i] = -data_[i];
+ return ret;
+ }
+
+ constexpr Vector<T, Rows> operator-(const Vector<T, Rows> &other) const
+ {
+ Vector<T, Rows> ret;
+ for (unsigned int i = 0; i < Rows; i++)
+ ret[i] = data_[i] - other[i];
+ return ret;
+ }
+
+ constexpr Vector<T, Rows> operator+(const Vector<T, Rows> &other) const
+ {
+ Vector<T, Rows> ret;
+ for (unsigned int i = 0; i < Rows; i++)
+ ret[i] = data_[i] + other[i];
+ return ret;
+ }
+
+ constexpr T operator*(const Vector<T, Rows> &other) const
+ {
+ T ret = 0;
+ for (unsigned int i = 0; i < Rows; i++)
+ ret += data_[i] * other[i];
+ return ret;
+ }
+
+ constexpr Vector<T, Rows> operator*(T factor) const
+ {
+ Vector<T, Rows> ret;
+ for (unsigned int i = 0; i < Rows; i++)
+ ret[i] = data_[i] * factor;
+ return ret;
+ }
+
+ constexpr Vector<T, Rows> operator/(T factor) const
+ {
+ Vector<T, Rows> ret;
+ for (unsigned int i = 0; i < Rows; i++)
+ ret[i] = data_[i] / factor;
+ return ret;
+ }
+
+ constexpr double length2() const
+ {
+ double ret = 0;
+ for (unsigned int i = 0; i < Rows; i++)
+ ret += data_[i] * data_[i];
+ return ret;
+ }
+
+ constexpr double length() const
+ {
+ return std::sqrt(length2());
+ }
+
+private:
+ std::array<T, Rows> data_;
+};
+
+template<typename T, unsigned int Rows, unsigned int Cols>
+Vector<T, Rows> operator*(const Matrix<T, Rows, Cols> &m, const Vector<T, Cols> &v)
+{
+ Vector<T, Rows> result;
+
+ for (unsigned int i = 0; i < Rows; i++) {
+ T sum = 0;
+ for (unsigned int j = 0; j < Cols; j++)
+ sum += m[i][j] * v[j];
+ result[i] = sum;
+ }
+
+ return result;
+}
+
+template<typename T, unsigned int Rows>
+bool operator==(const Vector<T, Rows> &lhs, const Vector<T, Rows> &rhs)
+{
+ for (unsigned int i = 0; i < Rows; i++) {
+ if (lhs[i] != rhs[i])
+ return false;
+ }
+
+ return true;
+}
+
+template<typename T, unsigned int Rows>
+bool operator!=(const Vector<T, Rows> &lhs, const Vector<T, Rows> &rhs)
+{
+ return !(lhs == rhs);
+}
+
+#ifndef __DOXYGEN__
+bool vectorValidateYaml(const YamlObject &obj, unsigned int size);
+#endif /* __DOXYGEN__ */
+
+} /* namespace ipa */
+
+#ifndef __DOXYGEN__
+template<typename T, unsigned int Rows>
+std::ostream &operator<<(std::ostream &out, const ipa::Vector<T, Rows> &v)
+{
+ out << "Vector { ";
+ for (unsigned int i = 0; i < Rows; i++) {
+ out << v[i];
+ out << ((i + 1 < Rows) ? ", " : " ");
+ }
+ out << " }";
+
+ return out;
+}
+
+template<typename T, unsigned int Rows>
+struct YamlObject::Getter<ipa::Vector<T, Rows>> {
+ std::optional<ipa::Vector<T, Rows>> get(const YamlObject &obj) const
+ {
+ if (!ipa::vectorValidateYaml(obj, Rows))
+ return std::nullopt;
+
+ ipa::Vector<T, Rows> vector;
+
+ unsigned int i = 0;
+ for (const YamlObject &entry : obj.asList()) {
+ const auto value = entry.get<T>();
+ if (!value)
+ return std::nullopt;
+ vector[i++] = *value;
+ }
+
+ return vector;
+ }
+};
+#endif /* __DOXYGEN__ */
+
+} /* namespace libcamera */
diff --git a/src/ipa/rkisp1/algorithms/agc.cpp b/src/ipa/rkisp1/algorithms/agc.cpp
index 50e0690f..f12f8b60 100644
--- a/src/ipa/rkisp1/algorithms/agc.cpp
+++ b/src/ipa/rkisp1/algorithms/agc.cpp
@@ -10,6 +10,8 @@
#include <algorithm>
#include <chrono>
#include <cmath>
+#include <tuple>
+#include <vector>
#include <libcamera/base/log.h>
#include <libcamera/base/utils.h>
@@ -17,6 +19,8 @@
#include <libcamera/control_ids.h>
#include <libcamera/ipa/core_ipa_interface.h>
+#include "libcamera/internal/yaml_parser.h"
+
#include "libipa/histogram.h"
/**
@@ -36,6 +40,86 @@ namespace ipa::rkisp1::algorithms {
LOG_DEFINE_CATEGORY(RkISP1Agc)
+int Agc::parseMeteringModes(IPAContext &context, const YamlObject &tuningData)
+{
+ if (!tuningData.isDictionary())
+ LOG(RkISP1Agc, Warning)
+ << "'AeMeteringMode' parameter not found in tuning file";
+
+ for (const auto &[key, value] : tuningData.asDict()) {
+ if (controls::AeMeteringModeNameValueMap.find(key) ==
+ controls::AeMeteringModeNameValueMap.end()) {
+ LOG(RkISP1Agc, Warning)
+ << "Skipping unknown metering mode '" << key << "'";
+ continue;
+ }
+
+ std::vector<uint8_t> weights =
+ value.getList<uint8_t>().value_or(std::vector<uint8_t>{});
+ if (weights.size() != context.hw->numHistogramWeights) {
+ LOG(RkISP1Agc, Warning)
+ << "Failed to read metering mode'" << key << "'";
+ continue;
+ }
+
+ meteringModes_[controls::AeMeteringModeNameValueMap.at(key)] = weights;
+ }
+
+ if (meteringModes_.empty()) {
+ LOG(RkISP1Agc, Warning)
+ << "No metering modes read from tuning file; defaulting to matrix";
+ int32_t meteringModeId = controls::AeMeteringModeNameValueMap.at("MeteringMatrix");
+ std::vector<uint8_t> weights(context.hw->numHistogramWeights, 1);
+
+ meteringModes_[meteringModeId] = weights;
+ }
+
+ std::vector<ControlValue> meteringModes;
+ std::vector<int> meteringModeKeys = utils::map_keys(meteringModes_);
+ std::transform(meteringModeKeys.begin(), meteringModeKeys.end(),
+ std::back_inserter(meteringModes),
+ [](int x) { return ControlValue(x); });
+ context.ctrlMap[&controls::AeMeteringMode] = ControlInfo(meteringModes);
+
+ return 0;
+}
+
+uint8_t Agc::computeHistogramPredivider(const Size &size,
+ enum rkisp1_cif_isp_histogram_mode mode)
+{
+ /*
+ * The maximum number of pixels that could potentially be in one bin is
+ * if all the pixels of the image are in it, multiplied by 3 for the
+ * three color channels. The counter for each bin is 16 bits wide, so
+ * `factor` thus contains the number of times we'd wrap around. This is
+ * obviously the number of pixels that we need to skip to make sure
+ * that we don't wrap around, but we compute the square root of it
+ * instead, as the skip that we need to program is for both the x and y
+ * directions.
+ *
+ * Even though it looks like dividing into a counter of 65536 would
+ * overflow by 1, this is apparently fine according to the hardware
+ * documentation, and this successfully gets the expected documented
+ * predivider size for cases where:
+ * (width / predivider) * (height / predivider) * 3 == 65536.
+ *
+ * There's a bit of extra rounding math to make sure the rounding goes
+ * the correct direction so that the square of the step is big enough
+ * to encompass the `factor` number of pixels that we need to skip.
+ *
+ * \todo Take into account weights. That is, if the weights are low
+ * enough we can potentially reduce the predivider to increase
+ * precision. This needs some investigation however, as this hardware
+ * behavior is undocumented and is only an educated guess.
+ */
+ int count = mode == RKISP1_CIF_ISP_HISTOGRAM_MODE_RGB_COMBINED ? 3 : 1;
+ double factor = size.width * size.height * count / 65536.0;
+ double root = std::sqrt(factor);
+ uint8_t predivider = static_cast<uint8_t>(std::ceil(root));
+
+ return std::clamp<uint8_t>(predivider, 3, 127);
+}
+
Agc::Agc()
{
supportsRaw_ = true;
@@ -59,6 +143,12 @@ int Agc::init(IPAContext &context, const YamlObject &tuningData)
if (ret)
return ret;
+ const YamlObject &yamlMeteringModes = tuningData["AeMeteringMode"];
+ ret = parseMeteringModes(context, yamlMeteringModes);
+ if (ret)
+ return ret;
+
+ context.ctrlMap[&controls::AeEnable] = ControlInfo(false, true);
context.ctrlMap.merge(controls());
return 0;
@@ -81,8 +171,19 @@ int Agc::configure(IPAContext &context, const IPACameraSensorInfo &configInfo)
context.activeState.agc.manual.exposure = context.activeState.agc.automatic.exposure;
context.activeState.agc.autoEnabled = !context.configuration.raw;
- context.activeState.agc.constraintMode = constraintModes().begin()->first;
- context.activeState.agc.exposureMode = exposureModeHelpers().begin()->first;
+ context.activeState.agc.constraintMode =
+ static_cast<controls::AeConstraintModeEnum>(constraintModes().begin()->first);
+ context.activeState.agc.exposureMode =
+ static_cast<controls::AeExposureModeEnum>(exposureModeHelpers().begin()->first);
+ context.activeState.agc.meteringMode =
+ static_cast<controls::AeMeteringModeEnum>(meteringModes_.begin()->first);
+
+ /*
+ * \todo This should probably come from FrameDurationLimits instead,
+ * except it's computed in the IPA and not here so we'd have to
+ * recompute it.
+ */
+ context.activeState.agc.maxFrameDuration = context.configuration.sensor.maxShutterSpeed;
/*
* Define the measurement window for AGC as a centered rectangle
@@ -93,7 +194,6 @@ int Agc::configure(IPAContext &context, const IPACameraSensorInfo &configInfo)
context.configuration.agc.measureWindow.h_size = 3 * configInfo.outputSize.width / 4;
context.configuration.agc.measureWindow.v_size = 3 * configInfo.outputSize.height / 4;
- /* \todo Run this again when FrameDurationLimits is passed in */
setLimits(context.configuration.sensor.minShutterSpeed,
context.configuration.sensor.maxShutterSpeed,
context.configuration.sensor.minAnalogueGain,
@@ -147,6 +247,34 @@ void Agc::queueRequest(IPAContext &context,
frameContext.agc.exposure = agc.manual.exposure;
frameContext.agc.gain = agc.manual.gain;
}
+
+ const auto &meteringMode = controls.get(controls::AeMeteringMode);
+ if (meteringMode) {
+ frameContext.agc.updateMetering = agc.meteringMode != *meteringMode;
+ agc.meteringMode =
+ static_cast<controls::AeMeteringModeEnum>(*meteringMode);
+ }
+ frameContext.agc.meteringMode = agc.meteringMode;
+
+ const auto &exposureMode = controls.get(controls::AeExposureMode);
+ if (exposureMode)
+ agc.exposureMode =
+ static_cast<controls::AeExposureModeEnum>(*exposureMode);
+ frameContext.agc.exposureMode = agc.exposureMode;
+
+ const auto &constraintMode = controls.get(controls::AeConstraintMode);
+ if (constraintMode)
+ agc.constraintMode =
+ static_cast<controls::AeConstraintModeEnum>(*constraintMode);
+ frameContext.agc.constraintMode = agc.constraintMode;
+
+ const auto &frameDurationLimits = controls.get(controls::FrameDurationLimits);
+ if (frameDurationLimits) {
+ utils::Duration maxFrameDuration =
+ std::chrono::milliseconds((*frameDurationLimits).back());
+ agc.maxFrameDuration = maxFrameDuration;
+ }
+ frameContext.agc.maxFrameDuration = agc.maxFrameDuration;
}
/**
@@ -160,7 +288,7 @@ void Agc::prepare(IPAContext &context, const uint32_t frame,
frameContext.agc.gain = context.activeState.agc.automatic.gain;
}
- if (frame > 0)
+ if (frame > 0 && !frameContext.agc.updateMetering)
return;
/* Configure the measurement window. */
@@ -178,14 +306,20 @@ void Agc::prepare(IPAContext &context, const uint32_t frame,
params->meas.hst_config.meas_window = context.configuration.agc.measureWindow;
/* Produce the luminance histogram. */
params->meas.hst_config.mode = RKISP1_CIF_ISP_HISTOGRAM_MODE_Y_HISTOGRAM;
+
/* Set an average weighted histogram. */
Span<uint8_t> weights{
params->meas.hst_config.hist_weight,
context.hw->numHistogramWeights
};
- std::fill(weights.begin(), weights.end(), 1);
- /* Step size can't be less than 3. */
- params->meas.hst_config.histogram_predivider = 4;
+ std::vector<uint8_t> &modeWeights = meteringModes_.at(frameContext.agc.meteringMode);
+ std::copy(modeWeights.begin(), modeWeights.end(), weights.begin());
+
+ struct rkisp1_cif_isp_window window = params->meas.hst_config.meas_window;
+ Size windowSize = { window.h_size, window.v_size };
+ params->meas.hst_config.histogram_predivider =
+ computeHistogramPredivider(windowSize,
+ static_cast<rkisp1_cif_isp_histogram_mode>(params->meas.hst_config.mode));
/* Update the configuration for histogram. */
params->module_cfg_update |= RKISP1_CIF_ISP_MODULE_HST;
@@ -201,6 +335,7 @@ void Agc::fillMetadata(IPAContext &context, IPAFrameContext &frameContext,
* frameContext.sensor.exposure;
metadata.set(controls::AnalogueGain, frameContext.sensor.gain);
metadata.set(controls::ExposureTime, exposureTime.get<std::micro>());
+ metadata.set(controls::AeEnable, frameContext.agc.autoEnabled);
/* \todo Use VBlank value calculated from each frame exposure. */
uint32_t vTotal = context.configuration.sensor.size.height
@@ -208,6 +343,10 @@ void Agc::fillMetadata(IPAContext &context, IPAFrameContext &frameContext,
utils::Duration frameDuration = context.configuration.sensor.lineDuration
* vTotal;
metadata.set(controls::FrameDuration, frameDuration.get<std::micro>());
+
+ metadata.set(controls::AeMeteringMode, frameContext.agc.meteringMode);
+ metadata.set(controls::AeExposureMode, frameContext.agc.exposureMode);
+ metadata.set(controls::AeConstraintMode, frameContext.agc.constraintMode);
}
/**
@@ -282,6 +421,15 @@ void Agc::process(IPAContext &context, [[maybe_unused]] const uint32_t frame,
[](uint32_t x) { return x >> 4; });
expMeans_ = { params->ae.exp_mean, context.hw->numAeCells };
+ utils::Duration maxShutterSpeed =
+ std::clamp(frameContext.agc.maxFrameDuration,
+ context.configuration.sensor.minShutterSpeed,
+ context.configuration.sensor.maxShutterSpeed);
+ setLimits(context.configuration.sensor.minShutterSpeed,
+ maxShutterSpeed,
+ context.configuration.sensor.minAnalogueGain,
+ context.configuration.sensor.maxAnalogueGain);
+
/*
* The Agc algorithm needs to know the effective exposure value that was
* applied to the sensor when the statistics were collected.
@@ -294,8 +442,8 @@ void Agc::process(IPAContext &context, [[maybe_unused]] const uint32_t frame,
utils::Duration shutterTime;
double aGain, dGain;
std::tie(shutterTime, aGain, dGain) =
- calculateNewEv(context.activeState.agc.constraintMode,
- context.activeState.agc.exposureMode,
+ calculateNewEv(frameContext.agc.constraintMode,
+ frameContext.agc.exposureMode,
hist, effectiveExposureValue);
LOG(RkISP1Agc, Debug)
diff --git a/src/ipa/rkisp1/algorithms/agc.h b/src/ipa/rkisp1/algorithms/agc.h
index 04b3247e..9ceaa82b 100644
--- a/src/ipa/rkisp1/algorithms/agc.h
+++ b/src/ipa/rkisp1/algorithms/agc.h
@@ -44,11 +44,17 @@ public:
ControlList &metadata) override;
private:
+ int parseMeteringModes(IPAContext &context, const YamlObject &tuningData);
+ uint8_t computeHistogramPredivider(const Size &size,
+ enum rkisp1_cif_isp_histogram_mode mode);
+
void fillMetadata(IPAContext &context, IPAFrameContext &frameContext,
ControlList &metadata);
double estimateLuminance(double gain) const override;
Span<const uint8_t> expMeans_;
+
+ std::map<int32_t, std::vector<uint8_t>> meteringModes_;
};
} /* namespace ipa::rkisp1::algorithms */
diff --git a/src/ipa/rkisp1/algorithms/ccm.cpp b/src/ipa/rkisp1/algorithms/ccm.cpp
new file mode 100644
index 00000000..c1f5403a
--- /dev/null
+++ b/src/ipa/rkisp1/algorithms/ccm.cpp
@@ -0,0 +1,147 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2024, Ideas On Board
+ *
+ * RkISP1 Color Correction Matrix control algorithm
+ */
+
+#include "ccm.h"
+
+#include <algorithm>
+#include <chrono>
+#include <cmath>
+#include <tuple>
+#include <vector>
+
+#include <libcamera/base/log.h>
+#include <libcamera/base/utils.h>
+
+#include <libcamera/control_ids.h>
+
+#include <libcamera/ipa/core_ipa_interface.h>
+
+#include "libcamera/internal/yaml_parser.h"
+
+#include "../utils.h"
+#include "libipa/matrix_interpolator.h"
+
+/**
+ * \file ccm.h
+ */
+
+namespace libcamera {
+
+namespace ipa::rkisp1::algorithms {
+
+/**
+ * \class Ccm
+ * \brief A color correction matrix algorithm
+ */
+
+LOG_DEFINE_CATEGORY(RkISP1Ccm)
+
+/**
+ * \copydoc libcamera::ipa::Algorithm::init
+ */
+int Ccm::init([[maybe_unused]] IPAContext &context, const YamlObject &tuningData)
+{
+ int ret = ccm_.readYaml(tuningData["ccms"], "ct", "ccm");
+ if (ret < 0) {
+ LOG(RkISP1Ccm, Warning)
+ << "Failed to parse 'ccm' "
+ << "parameter from tuning file; falling back to unit matrix";
+ ccm_.reset();
+ }
+
+ ret = offsets_.readYaml(tuningData["ccms"], "ct", "offsets");
+ if (ret < 0) {
+ LOG(RkISP1Ccm, Warning)
+ << "Failed to parse 'offsets' "
+ << "parameter from tuning file; falling back to zero offsets";
+ /*
+ * MatrixInterpolator::reset() resets to identity matrices
+ * while here we need zero matrices so we need to construct it
+ * ourselves.
+ */
+ Matrix<int16_t, 3, 1> m({ 0, 0, 0 });
+ std::map<unsigned int, Matrix<int16_t, 3, 1>> matrices = { { 0, m } };
+ offsets_ = MatrixInterpolator<int16_t, 3, 1>(matrices);
+ }
+
+ return 0;
+}
+
+void Ccm::setParameters(rkisp1_params_cfg *params,
+ const Matrix<float, 3, 3> &matrix,
+ const Matrix<int16_t, 3, 1> &offsets)
+{
+ struct rkisp1_cif_isp_ctk_config &config = params->others.ctk_config;
+
+ /*
+ * 4 bit integer and 7 bit fractional, ranging from -8 (0x400) to
+ * +7.992 (0x3ff)
+ */
+ for (unsigned int i = 0; i < 3; i++) {
+ for (unsigned int j = 0; j < 3; j++)
+ config.coeff[i][j] =
+ utils::floatingToFixedPoint<4, 7, uint16_t, double>(matrix[i][j]);
+ }
+
+ for (unsigned int i = 0; i < 3; i++)
+ config.ct_offset[i] = offsets[i][0] & 0xfff;
+
+ LOG(RkISP1Ccm, Debug) << "Setting matrix " << matrix;
+ LOG(RkISP1Ccm, Debug) << "Setting offsets " << offsets;
+
+ params->module_en_update |= RKISP1_CIF_ISP_MODULE_CTK;
+ params->module_ens |= RKISP1_CIF_ISP_MODULE_CTK;
+ params->module_cfg_update |= RKISP1_CIF_ISP_MODULE_CTK;
+}
+
+/**
+ * \copydoc libcamera::ipa::Algorithm::prepare
+ */
+void Ccm::prepare(IPAContext &context, const uint32_t frame,
+ IPAFrameContext &frameContext,
+ rkisp1_params_cfg *params)
+{
+ uint32_t ct = context.activeState.awb.temperatureK;
+
+ /*
+ * \todo The colour temperature will likely be noisy, add filtering to
+ * avoid updating the CCM matrix all the time.
+ */
+ if (frame > 0 && ct == ct_)
+ return;
+
+ ct_ = ct;
+ Matrix<float, 3, 3> ccm = ccm_.get(ct);
+ Matrix<int16_t, 3, 1> offsets = offsets_.get(ct);
+
+ frameContext.ccm.ccm = ccm;
+
+ setParameters(params, ccm, offsets);
+}
+
+/**
+ * \copydoc libcamera::ipa::Algorithm::process
+ */
+void Ccm::process([[maybe_unused]] IPAContext &context,
+ [[maybe_unused]] const uint32_t frame,
+ IPAFrameContext &frameContext,
+ [[maybe_unused]] const rkisp1_stat_buffer *stats,
+ ControlList &metadata)
+{
+ float m[9];
+ for (unsigned int i = 0; i < 3; i++) {
+ for (unsigned int j = 0; j < 3; j++)
+ m[i] = frameContext.ccm.ccm[i][j];
+ }
+ metadata.set(controls::ColourCorrectionMatrix, m);
+}
+
+REGISTER_IPA_ALGORITHM(Ccm, "Ccm")
+
+} /* namespace ipa::rkisp1::algorithms */
+
+} /* namespace libcamera */
diff --git a/src/ipa/rkisp1/algorithms/ccm.h b/src/ipa/rkisp1/algorithms/ccm.h
new file mode 100644
index 00000000..30cb8821
--- /dev/null
+++ b/src/ipa/rkisp1/algorithms/ccm.h
@@ -0,0 +1,49 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2024, Ideas On Board
+ *
+ * RkISP1 Color Correction Matrix control algorithm
+ */
+
+#pragma once
+
+#include <linux/rkisp1-config.h>
+
+#include "libipa/matrix.h"
+#include "libipa/matrix_interpolator.h"
+
+#include "algorithm.h"
+
+namespace libcamera {
+
+namespace ipa::rkisp1::algorithms {
+
+class Ccm : public Algorithm
+{
+public:
+ Ccm() {}
+ ~Ccm() = default;
+
+ int init(IPAContext &context, const YamlObject &tuningData) override;
+ void prepare(IPAContext &context, const uint32_t frame,
+ IPAFrameContext &frameContext,
+ rkisp1_params_cfg *params) override;
+ void process(IPAContext &context, const uint32_t frame,
+ IPAFrameContext &frameContext,
+ const rkisp1_stat_buffer *stats,
+ ControlList &metadata) override;
+
+private:
+ void parseYaml(const YamlObject &tuningData);
+ void setParameters(rkisp1_params_cfg *params,
+ const Matrix<float, 3, 3> &matrix,
+ const Matrix<int16_t, 3, 1> &offsets);
+
+ unsigned int ct_;
+ MatrixInterpolator<float, 3, 3> ccm_;
+ MatrixInterpolator<int16_t, 3, 1> offsets_;
+};
+
+} /* namespace ipa::rkisp1::algorithms */
+
+} /* namespace libcamera */
diff --git a/src/ipa/rkisp1/algorithms/cproc.cpp b/src/ipa/rkisp1/algorithms/cproc.cpp
index 68bb8180..ef0931b2 100644
--- a/src/ipa/rkisp1/algorithms/cproc.cpp
+++ b/src/ipa/rkisp1/algorithms/cproc.cpp
@@ -33,20 +33,71 @@ namespace ipa::rkisp1::algorithms {
LOG_DEFINE_CATEGORY(RkISP1CProc)
+namespace {
+
+constexpr float kDefaultBrightness = 0.0f;
+constexpr float kDefaultContrast = 1.0f;
+constexpr float kDefaultSaturation = 1.0f;
+
+int convertBrightness(const float v)
+{
+ return std::clamp<int>(std::lround(v * 128), -128, 127);
+}
+
+int convertContrastOrSaturation(const float v)
+{
+ return std::clamp<int>(std::lround(v * 128), 0, 255);
+}
+
+} /* namespace */
+
+/**
+ * \copydoc libcamera::ipa::Algorithm::init
+ */
+int ColorProcessing::init(IPAContext &context,
+ [[maybe_unused]] const YamlObject &tuningData)
+{
+ auto &cmap = context.ctrlMap;
+
+ cmap[&controls::Brightness] = ControlInfo(-1.0f, 0.993f, kDefaultBrightness);
+ cmap[&controls::Contrast] = ControlInfo(0.0f, 1.993f, kDefaultContrast);
+ cmap[&controls::Saturation] = ControlInfo(0.0f, 1.993f, kDefaultSaturation);
+
+ return 0;
+}
+
+/**
+ * \copydoc libcamera::ipa::Algorithm::configure
+ */
+int ColorProcessing::configure(IPAContext &context,
+ [[maybe_unused]] const IPACameraSensorInfo &configInfo)
+{
+ auto &cproc = context.activeState.cproc;
+
+ cproc.brightness = convertBrightness(kDefaultBrightness);
+ cproc.contrast = convertContrastOrSaturation(kDefaultContrast);
+ cproc.saturation = convertContrastOrSaturation(kDefaultSaturation);
+
+ return 0;
+}
+
/**
* \copydoc libcamera::ipa::Algorithm::queueRequest
*/
void ColorProcessing::queueRequest(IPAContext &context,
- [[maybe_unused]] const uint32_t frame,
+ const uint32_t frame,
IPAFrameContext &frameContext,
const ControlList &controls)
{
auto &cproc = context.activeState.cproc;
bool update = false;
+ if (frame == 0)
+ update = true;
+
const auto &brightness = controls.get(controls::Brightness);
if (brightness) {
- int value = std::clamp<int>(std::lround(*brightness * 128), -128, 127);
+ int value = convertBrightness(*brightness);
if (cproc.brightness != value) {
cproc.brightness = value;
update = true;
@@ -57,7 +108,7 @@ void ColorProcessing::queueRequest(IPAContext &context,
const auto &contrast = controls.get(controls::Contrast);
if (contrast) {
- int value = std::clamp<int>(std::lround(*contrast * 128), 0, 255);
+ int value = convertContrastOrSaturation(*contrast);
if (cproc.contrast != value) {
cproc.contrast = value;
update = true;
@@ -68,7 +119,7 @@ void ColorProcessing::queueRequest(IPAContext &context,
const auto saturation = controls.get(controls::Saturation);
if (saturation) {
- int value = std::clamp<int>(std::lround(*saturation * 128), 0, 255);
+ int value = convertContrastOrSaturation(*saturation);
if (cproc.saturation != value) {
cproc.saturation = value;
update = true;
diff --git a/src/ipa/rkisp1/algorithms/cproc.h b/src/ipa/rkisp1/algorithms/cproc.h
index bafba5cc..e50e7200 100644
--- a/src/ipa/rkisp1/algorithms/cproc.h
+++ b/src/ipa/rkisp1/algorithms/cproc.h
@@ -21,6 +21,9 @@ public:
ColorProcessing() = default;
~ColorProcessing() = default;
+ int init(IPAContext &context, const YamlObject &tuningData) override;
+ int configure(IPAContext &context,
+ const IPACameraSensorInfo &configInfo) override;
void queueRequest(IPAContext &context, const uint32_t frame,
IPAFrameContext &frameContext,
const ControlList &controls) override;
diff --git a/src/ipa/rkisp1/algorithms/goc.cpp b/src/ipa/rkisp1/algorithms/goc.cpp
new file mode 100644
index 00000000..a82cee3b
--- /dev/null
+++ b/src/ipa/rkisp1/algorithms/goc.cpp
@@ -0,0 +1,148 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2024, Ideas On Board
+ *
+ * RkISP1 Gamma out control
+ */
+#include "goc.h"
+
+#include <cmath>
+
+#include <libcamera/base/log.h>
+#include <libcamera/base/utils.h>
+
+#include <libcamera/control_ids.h>
+
+#include "libcamera/internal/yaml_parser.h"
+
+#include "linux/rkisp1-config.h"
+
+/**
+ * \file goc.h
+ */
+
+namespace libcamera {
+
+namespace ipa::rkisp1::algorithms {
+
+/**
+ * \class GammaOutCorrection
+ * \brief RkISP1 Gamma out correction
+ *
+ * This algorithm implements the gamma out curve for the RkISP1. It defaults to
+ * a gamma value of 2.2.
+ *
+ * As gamma is internally represented as a piecewise linear function with only
+ * 17 knots, the difference between gamma=2.2 and sRGB gamma is minimal.
+ * Therefore sRGB gamma was not implemented as special case.
+ *
+ * Useful links:
+ * - https://www.cambridgeincolour.com/tutorials/gamma-correction.htm
+ * - https://en.wikipedia.org/wiki/SRGB
+ */
+
+LOG_DEFINE_CATEGORY(RkISP1Gamma)
+
+const float kDefaultGamma = 2.2f;
+
+/**
+ * \copydoc libcamera::ipa::Algorithm::init
+ */
+int GammaOutCorrection::init(IPAContext &context, const YamlObject &tuningData)
+{
+ if (context.hw->numGammaOutSamples !=
+ RKISP1_CIF_ISP_GAMMA_OUT_MAX_SAMPLES_V10) {
+ LOG(RkISP1Gamma, Error)
+ << "Gamma is not implemented for RkISP1 V12";
+ return -EINVAL;
+ }
+
+ defaultGamma_ = tuningData["gamma"].get<double>(kDefaultGamma);
+ context.ctrlMap[&controls::Gamma] = ControlInfo(0.1f, 10.0f, defaultGamma_);
+
+ return 0;
+}
+
+/**
+ * \copydoc libcamera::ipa::Algorithm::configure
+ */
+int GammaOutCorrection::configure(IPAContext &context,
+ [[maybe_unused]] const IPACameraSensorInfo &configInfo)
+{
+ context.activeState.goc.gamma = defaultGamma_;
+ return 0;
+}
+
+/**
+ * \copydoc libcamera::ipa::Algorithm::queueRequest
+ */
+void GammaOutCorrection::queueRequest(IPAContext &context, const uint32_t frame,
+ IPAFrameContext &frameContext,
+ const ControlList &controls)
+{
+ if (frame == 0)
+ frameContext.goc.update = true;
+
+ const auto &gamma = controls.get(controls::Gamma);
+ if (gamma) {
+ context.activeState.goc.gamma = *gamma;
+ frameContext.goc.update = true;
+ LOG(RkISP1Gamma, Debug) << "Set gamma to " << *gamma;
+ }
+
+ frameContext.goc.gamma = context.activeState.goc.gamma;
+}
+
+/**
+ * \copydoc libcamera::ipa::Algorithm::prepare
+ */
+void GammaOutCorrection::prepare(IPAContext &context,
+ [[maybe_unused]] const uint32_t frame,
+ IPAFrameContext &frameContext,
+ rkisp1_params_cfg *params)
+{
+ ASSERT(context.hw->numGammaOutSamples ==
+ RKISP1_CIF_ISP_GAMMA_OUT_MAX_SAMPLES_V10);
+
+ /*
+ * The logarithmic segments as specified in the reference.
+ * Plus an additional 0 to make the loop easier
+ */
+ static constexpr std::array<unsigned int, RKISP1_CIF_ISP_GAMMA_OUT_MAX_SAMPLES_V10> segments = {
+ 64, 64, 64, 64, 128, 128, 128, 128, 256,
+ 256, 256, 512, 512, 512, 512, 512, 0
+ };
+ __u16 *gamma_y = params->others.goc_config.gamma_y;
+
+ if (!frameContext.goc.update)
+ return;
+
+ unsigned x = 0;
+ for (const auto [i, size] : utils::enumerate(segments)) {
+ gamma_y[i] = std::pow(x / 4096.0, 1.0 / frameContext.goc.gamma) * 1023.0;
+ x += size;
+ }
+
+ params->others.goc_config.mode = RKISP1_CIF_ISP_GOC_MODE_LOGARITHMIC;
+ params->module_cfg_update |= RKISP1_CIF_ISP_MODULE_GOC;
+ params->module_en_update |= RKISP1_CIF_ISP_MODULE_GOC;
+ params->module_ens |= RKISP1_CIF_ISP_MODULE_GOC;
+}
+
+/**
+ * \copydoc libcamera::ipa::Algorithm::process
+ */
+void GammaOutCorrection::process([[maybe_unused]] IPAContext &context,
+ [[maybe_unused]] const uint32_t frame,
+ IPAFrameContext &frameContext,
+ [[maybe_unused]] const rkisp1_stat_buffer *stats,
+ ControlList &metadata)
+{
+ metadata.set(controls::Gamma, frameContext.goc.gamma);
+}
+
+REGISTER_IPA_ALGORITHM(GammaOutCorrection, "GammaOutCorrection")
+
+} /* namespace ipa::rkisp1::algorithms */
+
+} /* namespace libcamera */
diff --git a/src/ipa/rkisp1/algorithms/goc.h b/src/ipa/rkisp1/algorithms/goc.h
new file mode 100644
index 00000000..0e05d7ce
--- /dev/null
+++ b/src/ipa/rkisp1/algorithms/goc.h
@@ -0,0 +1,42 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2024, Ideas On Board
+ *
+ * RkISP1 Gamma out control
+ */
+
+#pragma once
+
+#include "algorithm.h"
+
+namespace libcamera {
+
+namespace ipa::rkisp1::algorithms {
+
+class GammaOutCorrection : public Algorithm
+{
+public:
+ GammaOutCorrection() = default;
+ ~GammaOutCorrection() = default;
+
+ int init(IPAContext &context, const YamlObject &tuningData) override;
+ int configure(IPAContext &context,
+ const IPACameraSensorInfo &configInfo) override;
+ void queueRequest(IPAContext &context,
+ const uint32_t frame,
+ IPAFrameContext &frameContext,
+ const ControlList &controls) override;
+ void prepare(IPAContext &context, const uint32_t frame,
+ IPAFrameContext &frameContext,
+ rkisp1_params_cfg *params) override;
+ void process(IPAContext &context, const uint32_t frame,
+ IPAFrameContext &frameContext,
+ const rkisp1_stat_buffer *stats,
+ ControlList &metadata) override;
+
+private:
+ float defaultGamma_;
+};
+
+} /* namespace ipa::rkisp1::algorithms */
+} /* namespace libcamera */
diff --git a/src/ipa/rkisp1/algorithms/meson.build b/src/ipa/rkisp1/algorithms/meson.build
index 93a48329..1734a667 100644
--- a/src/ipa/rkisp1/algorithms/meson.build
+++ b/src/ipa/rkisp1/algorithms/meson.build
@@ -4,10 +4,12 @@ rkisp1_ipa_algorithms = files([
'agc.cpp',
'awb.cpp',
'blc.cpp',
+ 'ccm.cpp',
'cproc.cpp',
'dpcc.cpp',
'dpf.cpp',
'filter.cpp',
+ 'goc.cpp',
'gsl.cpp',
'lsc.cpp',
])
diff --git a/src/ipa/rkisp1/ipa_context.cpp b/src/ipa/rkisp1/ipa_context.cpp
index 283bc131..9f3f576a 100644
--- a/src/ipa/rkisp1/ipa_context.cpp
+++ b/src/ipa/rkisp1/ipa_context.cpp
@@ -137,13 +137,43 @@ namespace libcamera::ipa::rkisp1 {
* \var IPAActiveState::agc
* \brief State for the Automatic Gain Control algorithm
*
- * The exposure and gain are the latest values computed by the AGC algorithm.
+ * The \a automatic variables track the latest values computed by algorithm
+ * based on the latest processed statistics. All other variables track the
+ * consolidated controls requested in queued requests.
*
- * \var IPAActiveState::agc.exposure
- * \brief Exposure time expressed as a number of lines
+ * \struct IPAActiveState::agc.manual
+ * \brief Manual exposure time and analog gain (set through requests)
*
- * \var IPAActiveState::agc.gain
- * \brief Analogue gain multiplier
+ * \var IPAActiveState::agc.manual.exposure
+ * \brief Manual exposure time expressed as a number of lines as set by the
+ * ExposureTime control
+ *
+ * \var IPAActiveState::agc.manual.gain
+ * \brief Manual analogue gain as set by the AnalogueGain control
+ *
+ * \struct IPAActiveState::agc.automatic
+ * \brief Automatic exposure time and analog gain (computed by the algorithm)
+ *
+ * \var IPAActiveState::agc.automatic.exposure
+ * \brief Automatic exposure time expressed as a number of lines
+ *
+ * \var IPAActiveState::agc.automatic.gain
+ * \brief Automatic analogue gain multiplier
+ *
+ * \var IPAActiveState::agc.autoEnabled
+ * \brief Manual/automatic AGC state as set by the AeEnable control
+ *
+ * \var IPAActiveState::agc.constraintMode
+ * \brief Constraint mode as set by the AeConstraintMode control
+ *
+ * \var IPAActiveState::agc.exposureMode
+ * \brief Exposure mode as set by the AeExposureMode control
+ *
+ * \var IPAActiveState::agc.meteringMode
+ * \brief Metering mode as set by the AeMeteringMode control
+ *
+ * \var IPAActiveState::agc.maxFrameDuration
+ * \brief Maximum frame duration as set by the FrameDurationLimits control
*/
/**
@@ -218,6 +248,14 @@ namespace libcamera::ipa::rkisp1 {
*/
/**
+ * \var IPAActiveState::goc
+ * \brief State for the goc algorithm
+ *
+ * \var IPAActiveState::goc.gamma
+ * \brief Gamma value applied as 1.0/gamma
+ */
+
+/**
* \struct IPAFrameContext
* \brief Per-frame context for algorithms
*
@@ -257,12 +295,30 @@ namespace libcamera::ipa::rkisp1 {
* applied to the sensor in order to take effect for this frame.
*
* \var IPAFrameContext::agc.exposure
- * \brief Exposure time expressed as a number of lines
+ * \brief Exposure time expressed as a number of lines computed by the algorithm
*
* \var IPAFrameContext::agc.gain
- * \brief Analogue gain multiplier
+ * \brief Analogue gain multiplier computed by the algorithm
*
* The gain should be adapted to the sensor specific gain code before applying.
+ *
+ * \var IPAFrameContext::agc.autoEnabled
+ * \brief Manual/automatic AGC state as set by the AeEnable control
+ *
+ * \var IPAFrameContext::agc.constraintMode
+ * \brief Constraint mode as set by the AeConstraintMode control
+ *
+ * \var IPAFrameContext::agc.exposureMode
+ * \brief Exposure mode as set by the AeExposureMode control
+ *
+ * \var IPAFrameContext::agc.meteringMode
+ * \brief Metering mode as set by the AeMeteringMode control
+ *
+ * \var IPAFrameContext::agc.maxFrameDuration
+ * \brief Maximum frame duration as set by the FrameDurationLimits control
+ *
+ * \var IPAFrameContext::agc.updateMetering
+ * \brief Indicate if new ISP AGC metering parameters need to be applied
*/
/**
@@ -334,6 +390,18 @@ namespace libcamera::ipa::rkisp1 {
*/
/**
+ * \var IPAFrameContext::goc
+ * \brief Gamma out correction parameters for this frame
+ *
+ * \var IPAFrameContext::goc.gamma
+ * \brief Gamma value applied as 1.0/gamma
+ *
+ * \var IPAFrameContext::goc.update
+ * \brief Indicates if the goc parameters have been updated compared to the
+ * previous frame
+ */
+
+/**
* \var IPAFrameContext::sensor
* \brief Sensor configuration that used been used for this frame
*
diff --git a/src/ipa/rkisp1/ipa_context.h b/src/ipa/rkisp1/ipa_context.h
index bd02a7a2..8602b408 100644
--- a/src/ipa/rkisp1/ipa_context.h
+++ b/src/ipa/rkisp1/ipa_context.h
@@ -12,10 +12,12 @@
#include <libcamera/base/utils.h>
+#include <libcamera/control_ids.h>
#include <libcamera/controls.h>
#include <libcamera/geometry.h>
#include <libipa/fc_queue.h>
+#include <libipa/matrix.h>
namespace libcamera {
@@ -68,8 +70,10 @@ struct IPAActiveState {
} automatic;
bool autoEnabled;
- uint32_t constraintMode;
- uint32_t exposureMode;
+ controls::AeConstraintModeEnum constraintMode;
+ controls::AeExposureModeEnum exposureMode;
+ controls::AeMeteringModeEnum meteringMode;
+ utils::Duration maxFrameDuration;
} agc;
struct {
@@ -104,6 +108,10 @@ struct IPAActiveState {
uint8_t denoise;
uint8_t sharpness;
} filter;
+
+ struct {
+ double gamma;
+ } goc;
};
struct IPAFrameContext : public FrameContext {
@@ -111,6 +119,11 @@ struct IPAFrameContext : public FrameContext {
uint32_t exposure;
double gain;
bool autoEnabled;
+ controls::AeConstraintModeEnum constraintMode;
+ controls::AeExposureModeEnum exposureMode;
+ controls::AeMeteringModeEnum meteringMode;
+ utils::Duration maxFrameDuration;
+ bool updateMetering;
} agc;
struct {
@@ -143,9 +156,18 @@ struct IPAFrameContext : public FrameContext {
} filter;
struct {
+ double gamma;
+ bool update;
+ } goc;
+
+ struct {
uint32_t exposure;
double gain;
} sensor;
+
+ struct {
+ Matrix<float, 3, 3> ccm;
+ } ccm;
};
struct IPAContext {
diff --git a/src/ipa/rkisp1/meson.build b/src/ipa/rkisp1/meson.build
index e813da53..e8b266f1 100644
--- a/src/ipa/rkisp1/meson.build
+++ b/src/ipa/rkisp1/meson.build
@@ -8,6 +8,7 @@ ipa_name = 'ipa_rkisp1'
rkisp1_ipa_sources = files([
'ipa_context.cpp',
'rkisp1.cpp',
+ 'utils.cpp',
])
rkisp1_ipa_sources += rkisp1_ipa_algorithms
@@ -15,9 +16,8 @@ rkisp1_ipa_sources += rkisp1_ipa_algorithms
mod = shared_module(ipa_name,
[rkisp1_ipa_sources, libcamera_generated_ipa_headers],
name_prefix : '',
- include_directories : [ipa_includes, libipa_includes],
- dependencies : libcamera_private,
- link_with : libipa,
+ include_directories : [ipa_includes],
+ dependencies : [libcamera_private, libipa_dep],
install : true,
install_dir : ipa_install_dir)
diff --git a/src/ipa/rkisp1/rkisp1.cpp b/src/ipa/rkisp1/rkisp1.cpp
index 6687c91e..d31cdbab 100644
--- a/src/ipa/rkisp1/rkisp1.cpp
+++ b/src/ipa/rkisp1/rkisp1.cpp
@@ -106,12 +106,8 @@ const IPAHwSettings ipaHwSettingsV12{
/* List of controls handled by the RkISP1 IPA */
const ControlInfoMap::Map rkisp1Controls{
- { &controls::AeEnable, ControlInfo(false, true) },
{ &controls::AwbEnable, ControlInfo(false, true) },
{ &controls::ColourGains, ControlInfo(0.0f, 3.996f, 1.0f) },
- { &controls::Brightness, ControlInfo(-1.0f, 0.993f, 0.0f) },
- { &controls::Contrast, ControlInfo(0.0f, 1.993f, 1.0f) },
- { &controls::Saturation, ControlInfo(0.0f, 1.993f, 1.0f) },
{ &controls::Sharpness, ControlInfo(0.0f, 10.0f, 1.0f) },
{ &controls::draft::NoiseReductionMode, ControlInfo(controls::draft::NoiseReductionModeValues) },
};
@@ -427,7 +423,7 @@ void IPARkISP1::updateControls(const IPACameraSensorInfo &sensorInfo,
frameDurations[1],
frameDurations[2]);
- ctrlMap.merge(context_.ctrlMap);
+ ctrlMap.insert(context_.ctrlMap.begin(), context_.ctrlMap.end());
*ipaControls = ControlInfoMap(std::move(ctrlMap), controls::controls);
}
diff --git a/src/ipa/rkisp1/utils.cpp b/src/ipa/rkisp1/utils.cpp
new file mode 100644
index 00000000..960ec64e
--- /dev/null
+++ b/src/ipa/rkisp1/utils.cpp
@@ -0,0 +1,42 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2024, Paul Elder <paul.elder@ideasonboard.com>
+ *
+ * Miscellaneous utility functions specific to rkisp1
+ */
+
+#include "utils.h"
+
+/**
+ * \file utils.h
+ */
+
+namespace libcamera {
+
+namespace ipa::rkisp1::utils {
+
+/**
+ * \fn R floatingToFixedPoint(T number)
+ * \brief Convert a floating point number to a fixed-point representation
+ * \tparam I Bit width of the integer part of the fixed-point
+ * \tparam F Bit width of the fractional part of the fixed-point
+ * \tparam R Return type of the fixed-point representation
+ * \tparam T Input type of the floating point representation
+ * \param number The floating point number to convert to fixed point
+ * \return The converted value
+ */
+
+/**
+ * \fn R fixedToFloatingPoint(T number)
+ * \brief Convert a fixed-point number to a floating point representation
+ * \tparam I Bit width of the integer part of the fixed-point
+ * \tparam F Bit width of the fractional part of the fixed-point
+ * \tparam R Return type of the floating point representation
+ * \tparam T Input type of the fixed-point representation
+ * \param number The fixed point number to convert to floating point
+ * \return The converted value
+ */
+
+} /* namespace ipa::rkisp1::utils */
+
+} /* namespace libcamera */
diff --git a/src/ipa/rkisp1/utils.h b/src/ipa/rkisp1/utils.h
new file mode 100644
index 00000000..450f2244
--- /dev/null
+++ b/src/ipa/rkisp1/utils.h
@@ -0,0 +1,66 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2024, Paul Elder <paul.elder@ideasonboard.com>
+ *
+ * Miscellaneous utility functions specific to rkisp1
+ */
+
+#pragma once
+
+#include <cmath>
+#include <limits>
+#include <type_traits>
+
+namespace libcamera {
+
+namespace ipa::rkisp1::utils {
+
+#ifndef __DOXYGEN__
+template<unsigned int I, unsigned int F, typename R, typename T,
+ std::enable_if_t<std::is_integral_v<R> &&
+ std::is_floating_point_v<T>> * = nullptr>
+#else
+template<unsigned int I, unsigned int F, typename R, typename T>
+#endif
+constexpr R floatingToFixedPoint(T number)
+{
+ static_assert(sizeof(int) >= sizeof(R));
+ static_assert(I + F <= sizeof(R) * 8);
+
+ /*
+ * The intermediate cast to int is needed on arm platforms to properly
+ * cast negative values. See
+ * https://embeddeduse.com/2013/08/25/casting-a-negative-float-to-an-unsigned-int/
+ */
+ R mask = (1 << (F + I)) - 1;
+ R frac = static_cast<R>(static_cast<int>(std::round(number * (1 << F)))) & mask;
+
+ return frac;
+}
+
+#ifndef __DOXYGEN__
+template<unsigned int I, unsigned int F, typename R, typename T,
+ std::enable_if_t<std::is_floating_point_v<R> &&
+ std::is_integral_v<T>> * = nullptr>
+#else
+template<unsigned int I, unsigned int F, typename R, typename T>
+#endif
+constexpr R fixedToFloatingPoint(T number)
+{
+ static_assert(sizeof(int) >= sizeof(T));
+ static_assert(I + F <= sizeof(T) * 8);
+
+ /*
+ * Recreate the upper bits in case of a negative number by shifting the sign
+ * bit from the fixed point to the first bit of the unsigned and then right shifting
+ * by the same amount which keeps the sign bit in place.
+ * This can be optimized by the compiler quite well.
+ */
+ int remaining_bits = sizeof(int) * 8 - (I + F);
+ int t = static_cast<int>(static_cast<unsigned>(number) << remaining_bits) >> remaining_bits;
+ return static_cast<R>(t) / static_cast<R>(1 << F);
+}
+
+} /* namespace ipa::rkisp1::utils */
+
+} /* namespace libcamera */
diff --git a/src/ipa/rpi/common/ipa_base.cpp b/src/ipa/rpi/common/ipa_base.cpp
index 61490bd6..ee3848b5 100644
--- a/src/ipa/rpi/common/ipa_base.cpp
+++ b/src/ipa/rpi/common/ipa_base.cpp
@@ -73,7 +73,7 @@ const ControlInfoMap::Map ipaControls{
{ &controls::ScalerCrop, ControlInfo(Rectangle{}, Rectangle(65535, 65535, 65535, 65535), Rectangle{}) },
{ &controls::FrameDurationLimits, ControlInfo(INT64_C(33333), INT64_C(120000)) },
{ &controls::draft::NoiseReductionMode, ControlInfo(controls::draft::NoiseReductionModeValues) },
- { &controls::rpi::StatsOutputEnable, ControlInfo(false, true) },
+ { &controls::rpi::StatsOutputEnable, ControlInfo(false, true, false) },
};
/* IPA controls handled conditionally, if the sensor is not mono */
@@ -103,8 +103,9 @@ LOG_DEFINE_CATEGORY(IPARPI)
namespace ipa::RPi {
IpaBase::IpaBase()
- : controller_(), frameLengths_(FrameLengthsQueueSize, 0s), stitchSwapBuffers_(false), frameCount_(0),
- mistrustCount_(0), lastRunTimestamp_(0), firstStart_(true), flickerState_({ 0, 0s })
+ : controller_(), frameLengths_(FrameLengthsQueueSize, 0s), statsMetadataOutput_(false),
+ stitchSwapBuffers_(false), frameCount_(0), mistrustCount_(0), lastRunTimestamp_(0),
+ firstStart_(true), flickerState_({ 0, 0s })
{
}
diff --git a/src/ipa/rpi/common/ipa_base.h b/src/ipa/rpi/common/ipa_base.h
index 074b304e..1a811beb 100644
--- a/src/ipa/rpi/common/ipa_base.h
+++ b/src/ipa/rpi/common/ipa_base.h
@@ -49,6 +49,11 @@ public:
void processStats(const ProcessParams &params) override;
protected:
+ bool monoSensor() const
+ {
+ return monoSensor_;
+ }
+
/* Raspberry Pi controller specific defines. */
std::unique_ptr<RPiController::CamHelper> helper_;
RPiController::Controller controller_;
diff --git a/src/ipa/rpi/controller/cac_status.h b/src/ipa/rpi/controller/cac_status.h
index 475d4c5c..adffce41 100644
--- a/src/ipa/rpi/controller/cac_status.h
+++ b/src/ipa/rpi/controller/cac_status.h
@@ -6,8 +6,6 @@
*/
#pragma once
-#include "pwl.h"
-
struct CacStatus {
std::vector<double> lutRx;
std::vector<double> lutRy;
diff --git a/src/ipa/rpi/controller/contrast_status.h b/src/ipa/rpi/controller/contrast_status.h
index 7c67f054..1f175872 100644
--- a/src/ipa/rpi/controller/contrast_status.h
+++ b/src/ipa/rpi/controller/contrast_status.h
@@ -6,7 +6,7 @@
*/
#pragma once
-#include "pwl.h"
+#include "libipa/pwl.h"
/*
* The "contrast" algorithm creates a gamma curve, optionally doing a little bit
@@ -14,7 +14,7 @@
*/
struct ContrastStatus {
- RPiController::Pwl gammaCurve;
+ libcamera::ipa::Pwl gammaCurve;
double brightness;
double contrast;
};
diff --git a/src/ipa/rpi/controller/meson.build b/src/ipa/rpi/controller/meson.build
index 32a4d31c..74b74888 100644
--- a/src/ipa/rpi/controller/meson.build
+++ b/src/ipa/rpi/controller/meson.build
@@ -5,7 +5,6 @@ rpi_ipa_controller_sources = files([
'controller.cpp',
'device_status.cpp',
'histogram.cpp',
- 'pwl.cpp',
'rpi/af.cpp',
'rpi/agc.cpp',
'rpi/agc_channel.cpp',
@@ -32,4 +31,5 @@ rpi_ipa_controller_deps = [
]
rpi_ipa_controller_lib = static_library('rpi_ipa_controller', rpi_ipa_controller_sources,
+ include_directories : libipa_includes,
dependencies : rpi_ipa_controller_deps)
diff --git a/src/ipa/rpi/controller/pwl.cpp b/src/ipa/rpi/controller/pwl.cpp
deleted file mode 100644
index e3912376..00000000
--- a/src/ipa/rpi/controller/pwl.cpp
+++ /dev/null
@@ -1,269 +0,0 @@
-/* SPDX-License-Identifier: BSD-2-Clause */
-/*
- * Copyright (C) 2019, Raspberry Pi Ltd
- *
- * piecewise linear functions
- */
-
-#include <cassert>
-#include <cmath>
-#include <stdexcept>
-
-#include "pwl.h"
-
-using namespace RPiController;
-
-int Pwl::read(const libcamera::YamlObject &params)
-{
- if (!params.size() || params.size() % 2)
- return -EINVAL;
-
- const auto &list = params.asList();
-
- for (auto it = list.begin(); it != list.end(); it++) {
- auto x = it->get<double>();
- if (!x)
- return -EINVAL;
- if (it != list.begin() && *x <= points_.back().x)
- return -EINVAL;
-
- auto y = (++it)->get<double>();
- if (!y)
- return -EINVAL;
-
- points_.push_back(Point(*x, *y));
- }
-
- return 0;
-}
-
-void Pwl::append(double x, double y, const double eps)
-{
- if (points_.empty() || points_.back().x + eps < x)
- points_.push_back(Point(x, y));
-}
-
-void Pwl::prepend(double x, double y, const double eps)
-{
- if (points_.empty() || points_.front().x - eps > x)
- points_.insert(points_.begin(), Point(x, y));
-}
-
-Pwl::Interval Pwl::domain() const
-{
- return Interval(points_[0].x, points_[points_.size() - 1].x);
-}
-
-Pwl::Interval Pwl::range() const
-{
- double lo = points_[0].y, hi = lo;
- for (auto &p : points_)
- lo = std::min(lo, p.y), hi = std::max(hi, p.y);
- return Interval(lo, hi);
-}
-
-bool Pwl::empty() const
-{
- return points_.empty();
-}
-
-double Pwl::eval(double x, int *spanPtr, bool updateSpan) const
-{
- int span = findSpan(x, spanPtr && *spanPtr != -1 ? *spanPtr : points_.size() / 2 - 1);
- if (spanPtr && updateSpan)
- *spanPtr = span;
- return points_[span].y +
- (x - points_[span].x) * (points_[span + 1].y - points_[span].y) /
- (points_[span + 1].x - points_[span].x);
-}
-
-int Pwl::findSpan(double x, int span) const
-{
- /*
- * Pwls are generally small, so linear search may well be faster than
- * binary, though could review this if large PWls start turning up.
- */
- int lastSpan = points_.size() - 2;
- /*
- * some algorithms may call us with span pointing directly at the last
- * control point
- */
- span = std::max(0, std::min(lastSpan, span));
- while (span < lastSpan && x >= points_[span + 1].x)
- span++;
- while (span && x < points_[span].x)
- span--;
- return span;
-}
-
-Pwl::PerpType Pwl::invert(Point const &xy, Point &perp, int &span,
- const double eps) const
-{
- assert(span >= -1);
- bool prevOffEnd = false;
- for (span = span + 1; span < (int)points_.size() - 1; span++) {
- Point spanVec = points_[span + 1] - points_[span];
- double t = ((xy - points_[span]) % spanVec) / spanVec.len2();
- if (t < -eps) /* off the start of this span */
- {
- if (span == 0) {
- perp = points_[span];
- return PerpType::Start;
- } else if (prevOffEnd) {
- perp = points_[span];
- return PerpType::Vertex;
- }
- } else if (t > 1 + eps) /* off the end of this span */
- {
- if (span == (int)points_.size() - 2) {
- perp = points_[span + 1];
- return PerpType::End;
- }
- prevOffEnd = true;
- } else /* a true perpendicular */
- {
- perp = points_[span] + spanVec * t;
- return PerpType::Perpendicular;
- }
- }
- return PerpType::None;
-}
-
-Pwl Pwl::inverse(bool *trueInverse, const double eps) const
-{
- bool appended = false, prepended = false, neither = false;
- Pwl inverse;
-
- for (Point const &p : points_) {
- if (inverse.empty())
- inverse.append(p.y, p.x, eps);
- else if (std::abs(inverse.points_.back().x - p.y) <= eps ||
- std::abs(inverse.points_.front().x - p.y) <= eps)
- /* do nothing */;
- else if (p.y > inverse.points_.back().x) {
- inverse.append(p.y, p.x, eps);
- appended = true;
- } else if (p.y < inverse.points_.front().x) {
- inverse.prepend(p.y, p.x, eps);
- prepended = true;
- } else
- neither = true;
- }
-
- /*
- * This is not a proper inverse if we found ourselves putting points
- * onto both ends of the inverse, or if there were points that couldn't
- * go on either.
- */
- if (trueInverse)
- *trueInverse = !(neither || (appended && prepended));
-
- return inverse;
-}
-
-Pwl Pwl::compose(Pwl const &other, const double eps) const
-{
- double thisX = points_[0].x, thisY = points_[0].y;
- int thisSpan = 0, otherSpan = other.findSpan(thisY, 0);
- Pwl result({ { thisX, other.eval(thisY, &otherSpan, false) } });
- while (thisSpan != (int)points_.size() - 1) {
- double dx = points_[thisSpan + 1].x - points_[thisSpan].x,
- dy = points_[thisSpan + 1].y - points_[thisSpan].y;
- if (std::abs(dy) > eps &&
- otherSpan + 1 < (int)other.points_.size() &&
- points_[thisSpan + 1].y >=
- other.points_[otherSpan + 1].x + eps) {
- /*
- * next control point in result will be where this
- * function's y reaches the next span in other
- */
- thisX = points_[thisSpan].x +
- (other.points_[otherSpan + 1].x -
- points_[thisSpan].y) *
- dx / dy;
- thisY = other.points_[++otherSpan].x;
- } else if (std::abs(dy) > eps && otherSpan > 0 &&
- points_[thisSpan + 1].y <=
- other.points_[otherSpan - 1].x - eps) {
- /*
- * next control point in result will be where this
- * function's y reaches the previous span in other
- */
- thisX = points_[thisSpan].x +
- (other.points_[otherSpan + 1].x -
- points_[thisSpan].y) *
- dx / dy;
- thisY = other.points_[--otherSpan].x;
- } else {
- /* we stay in the same span in other */
- thisSpan++;
- thisX = points_[thisSpan].x,
- thisY = points_[thisSpan].y;
- }
- result.append(thisX, other.eval(thisY, &otherSpan, false),
- eps);
- }
- return result;
-}
-
-void Pwl::map(std::function<void(double x, double y)> f) const
-{
- for (auto &pt : points_)
- f(pt.x, pt.y);
-}
-
-void Pwl::map2(Pwl const &pwl0, Pwl const &pwl1,
- std::function<void(double x, double y0, double y1)> f)
-{
- int span0 = 0, span1 = 0;
- double x = std::min(pwl0.points_[0].x, pwl1.points_[0].x);
- f(x, pwl0.eval(x, &span0, false), pwl1.eval(x, &span1, false));
- while (span0 < (int)pwl0.points_.size() - 1 ||
- span1 < (int)pwl1.points_.size() - 1) {
- if (span0 == (int)pwl0.points_.size() - 1)
- x = pwl1.points_[++span1].x;
- else if (span1 == (int)pwl1.points_.size() - 1)
- x = pwl0.points_[++span0].x;
- else if (pwl0.points_[span0 + 1].x > pwl1.points_[span1 + 1].x)
- x = pwl1.points_[++span1].x;
- else
- x = pwl0.points_[++span0].x;
- f(x, pwl0.eval(x, &span0, false), pwl1.eval(x, &span1, false));
- }
-}
-
-Pwl Pwl::combine(Pwl const &pwl0, Pwl const &pwl1,
- std::function<double(double x, double y0, double y1)> f,
- const double eps)
-{
- Pwl result;
- map2(pwl0, pwl1, [&](double x, double y0, double y1) {
- result.append(x, f(x, y0, y1), eps);
- });
- return result;
-}
-
-void Pwl::matchDomain(Interval const &domain, bool clip, const double eps)
-{
- int span = 0;
- prepend(domain.start, eval(clip ? points_[0].x : domain.start, &span),
- eps);
- span = points_.size() - 2;
- append(domain.end, eval(clip ? points_.back().x : domain.end, &span),
- eps);
-}
-
-Pwl &Pwl::operator*=(double d)
-{
- for (auto &pt : points_)
- pt.y *= d;
- return *this;
-}
-
-void Pwl::debug(FILE *fp) const
-{
- fprintf(fp, "Pwl {\n");
- for (auto &p : points_)
- fprintf(fp, "\t(%g, %g)\n", p.x, p.y);
- fprintf(fp, "}\n");
-}
diff --git a/src/ipa/rpi/controller/pwl.h b/src/ipa/rpi/controller/pwl.h
deleted file mode 100644
index 7d5e7e4d..00000000
--- a/src/ipa/rpi/controller/pwl.h
+++ /dev/null
@@ -1,127 +0,0 @@
-/* SPDX-License-Identifier: BSD-2-Clause */
-/*
- * Copyright (C) 2019, Raspberry Pi Ltd
- *
- * piecewise linear functions interface
- */
-#pragma once
-
-#include <functional>
-#include <math.h>
-#include <vector>
-
-#include "libcamera/internal/yaml_parser.h"
-
-namespace RPiController {
-
-class Pwl
-{
-public:
- struct Interval {
- Interval(double _start, double _end)
- : start(_start), end(_end)
- {
- }
- double start, end;
- bool contains(double value)
- {
- return value >= start && value <= end;
- }
- double clip(double value)
- {
- return value < start ? start
- : (value > end ? end : value);
- }
- double len() const { return end - start; }
- };
- struct Point {
- Point() : x(0), y(0) {}
- Point(double _x, double _y)
- : x(_x), y(_y) {}
- double x, y;
- Point operator-(Point const &p) const
- {
- return Point(x - p.x, y - p.y);
- }
- Point operator+(Point const &p) const
- {
- return Point(x + p.x, y + p.y);
- }
- double operator%(Point const &p) const
- {
- return x * p.x + y * p.y;
- }
- Point operator*(double f) const { return Point(x * f, y * f); }
- Point operator/(double f) const { return Point(x / f, y / f); }
- double len2() const { return x * x + y * y; }
- double len() const { return sqrt(len2()); }
- };
- Pwl() {}
- Pwl(std::vector<Point> const &points) : points_(points) {}
- int read(const libcamera::YamlObject &params);
- void append(double x, double y, const double eps = 1e-6);
- void prepend(double x, double y, const double eps = 1e-6);
- Interval domain() const;
- Interval range() const;
- bool empty() const;
- /*
- * Evaluate Pwl, optionally supplying an initial guess for the
- * "span". The "span" may be optionally be updated. If you want to know
- * the "span" value but don't have an initial guess you can set it to
- * -1.
- */
- double eval(double x, int *spanPtr = nullptr,
- bool updateSpan = true) const;
- /*
- * Find perpendicular closest to xy, starting from span+1 so you can
- * call it repeatedly to check for multiple closest points (set span to
- * -1 on the first call). Also returns "pseudo" perpendiculars; see
- * PerpType enum.
- */
- enum class PerpType {
- None, /* no perpendicular found */
- Start, /* start of Pwl is closest point */
- End, /* end of Pwl is closest point */
- Vertex, /* vertex of Pwl is closest point */
- Perpendicular /* true perpendicular found */
- };
- PerpType invert(Point const &xy, Point &perp, int &span,
- const double eps = 1e-6) const;
- /*
- * Compute the inverse function. Indicate if it is a proper (true)
- * inverse, or only a best effort (e.g. input was non-monotonic).
- */
- Pwl inverse(bool *trueInverse = nullptr, const double eps = 1e-6) const;
- /* Compose two Pwls together, doing "this" first and "other" after. */
- Pwl compose(Pwl const &other, const double eps = 1e-6) const;
- /* Apply function to (x,y) values at every control point. */
- void map(std::function<void(double x, double y)> f) const;
- /*
- * Apply function to (x, y0, y1) values wherever either Pwl has a
- * control point.
- */
- static void map2(Pwl const &pwl0, Pwl const &pwl1,
- std::function<void(double x, double y0, double y1)> f);
- /*
- * Combine two Pwls, meaning we create a new Pwl where the y values are
- * given by running f wherever either has a knot.
- */
- static Pwl
- combine(Pwl const &pwl0, Pwl const &pwl1,
- std::function<double(double x, double y0, double y1)> f,
- const double eps = 1e-6);
- /*
- * Make "this" match (at least) the given domain. Any extension my be
- * clipped or linear.
- */
- void matchDomain(Interval const &domain, bool clip = true,
- const double eps = 1e-6);
- Pwl &operator*=(double d);
- void debug(FILE *fp = stdout) const;
-
-private:
- int findSpan(double x, int span) const;
- std::vector<Point> points_;
-};
-
-} /* namespace RPiController */
diff --git a/src/ipa/rpi/controller/rpi/af.cpp b/src/ipa/rpi/controller/rpi/af.cpp
index c5fd8482..5ca76dd9 100644
--- a/src/ipa/rpi/controller/rpi/af.cpp
+++ b/src/ipa/rpi/controller/rpi/af.cpp
@@ -139,7 +139,7 @@ int Af::CfgParams::read(const libcamera::YamlObject &params)
readNumber<uint32_t>(skipFrames, params, "skip_frames");
if (params.contains("map"))
- map.read(params["map"]);
+ map = params["map"].get<ipa::Pwl>(ipa::Pwl{});
else
LOG(RPiAf, Warning) << "No map defined";
@@ -721,7 +721,7 @@ bool Af::setLensPosition(double dioptres, int *hwpos)
if (mode_ == AfModeManual) {
LOG(RPiAf, Debug) << "setLensPosition: " << dioptres;
- ftarget_ = cfg_.map.domain().clip(dioptres);
+ ftarget_ = cfg_.map.domain().clamp(dioptres);
changed = !(initted_ && fsmooth_ == ftarget_);
updateLensPosition();
}
diff --git a/src/ipa/rpi/controller/rpi/af.h b/src/ipa/rpi/controller/rpi/af.h
index 2617e2ac..317a51f3 100644
--- a/src/ipa/rpi/controller/rpi/af.h
+++ b/src/ipa/rpi/controller/rpi/af.h
@@ -9,7 +9,8 @@
#include "../af_algorithm.h"
#include "../af_status.h"
#include "../pdaf_data.h"
-#include "../pwl.h"
+
+#include "libipa/pwl.h"
/*
* This algorithm implements a hybrid of CDAF and PDAF, favouring PDAF.
@@ -100,7 +101,7 @@ private:
uint32_t confThresh; /* PDAF confidence cell min (sensor-specific) */
uint32_t confClip; /* PDAF confidence cell max (sensor-specific) */
uint32_t skipFrames; /* frames to skip at start or modeswitch */
- Pwl map; /* converts dioptres -> lens driver position */
+ libcamera::ipa::Pwl map; /* converts dioptres -> lens driver position */
CfgParams();
int read(const libcamera::YamlObject &params);
diff --git a/src/ipa/rpi/controller/rpi/agc_channel.cpp b/src/ipa/rpi/controller/rpi/agc_channel.cpp
index a77ccec3..cf2565a8 100644
--- a/src/ipa/rpi/controller/rpi/agc_channel.cpp
+++ b/src/ipa/rpi/controller/rpi/agc_channel.cpp
@@ -130,7 +130,8 @@ int AgcConstraint::read(const libcamera::YamlObject &params)
return -EINVAL;
qHi = *value;
- return yTarget.read(params["y_target"]);
+ yTarget = params["y_target"].get<ipa::Pwl>(ipa::Pwl{});
+ return yTarget.empty() ? -EINVAL : 0;
}
static std::tuple<int, AgcConstraintMode>
@@ -237,9 +238,9 @@ int AgcConfig::read(const libcamera::YamlObject &params)
return ret;
}
- ret = yTarget.read(params["y_target"]);
- if (ret)
- return ret;
+ yTarget = params["y_target"].get<ipa::Pwl>(ipa::Pwl{});
+ if (yTarget.empty())
+ return -EINVAL;
speed = params["speed"].get<double>(0.2);
startupFrames = params["startup_frames"].get<uint16_t>(10);
@@ -715,7 +716,7 @@ static constexpr double EvGainYTargetLimit = 0.9;
static double constraintComputeGain(AgcConstraint &c, const Histogram &h, double lux,
double evGain, double &targetY)
{
- targetY = c.yTarget.eval(c.yTarget.domain().clip(lux));
+ targetY = c.yTarget.eval(c.yTarget.domain().clamp(lux));
targetY = std::min(EvGainYTargetLimit, targetY * evGain);
double iqm = h.interQuantileMean(c.qLo, c.qHi);
return (targetY * h.bins()) / iqm;
@@ -734,7 +735,7 @@ void AgcChannel::computeGain(StatisticsPtr &statistics, Metadata *imageMetadata,
* The initial gain and target_Y come from some of the regions. After
* that we consider the histogram constraints.
*/
- targetY = config_.yTarget.eval(config_.yTarget.domain().clip(lux.lux));
+ targetY = config_.yTarget.eval(config_.yTarget.domain().clamp(lux.lux));
targetY = std::min(EvGainYTargetLimit, targetY * evGain);
/*
diff --git a/src/ipa/rpi/controller/rpi/agc_channel.h b/src/ipa/rpi/controller/rpi/agc_channel.h
index 99033e23..58368889 100644
--- a/src/ipa/rpi/controller/rpi/agc_channel.h
+++ b/src/ipa/rpi/controller/rpi/agc_channel.h
@@ -12,10 +12,11 @@
#include <libcamera/base/utils.h>
+#include <libipa/pwl.h>
+
#include "../agc_status.h"
#include "../awb_status.h"
#include "../controller.h"
-#include "../pwl.h"
/* This is our implementation of AGC. */
@@ -40,7 +41,7 @@ struct AgcConstraint {
Bound bound;
double qLo;
double qHi;
- Pwl yTarget;
+ libcamera::ipa::Pwl yTarget;
int read(const libcamera::YamlObject &params);
};
@@ -61,7 +62,7 @@ struct AgcConfig {
std::map<std::string, AgcExposureMode> exposureModes;
std::map<std::string, AgcConstraintMode> constraintModes;
std::vector<AgcChannelConstraint> channelConstraints;
- Pwl yTarget;
+ libcamera::ipa::Pwl yTarget;
double speed;
uint16_t startupFrames;
unsigned int convergenceFrames;
diff --git a/src/ipa/rpi/controller/rpi/awb.cpp b/src/ipa/rpi/controller/rpi/awb.cpp
index abe5906e..003c8fa1 100644
--- a/src/ipa/rpi/controller/rpi/awb.cpp
+++ b/src/ipa/rpi/controller/rpi/awb.cpp
@@ -49,10 +49,11 @@ int AwbPrior::read(const libcamera::YamlObject &params)
return -EINVAL;
lux = *value;
- return prior.read(params["prior"]);
+ prior = params["prior"].get<ipa::Pwl>(ipa::Pwl{});
+ return prior.empty() ? -EINVAL : 0;
}
-static int readCtCurve(Pwl &ctR, Pwl &ctB, const libcamera::YamlObject &params)
+static int readCtCurve(ipa::Pwl &ctR, ipa::Pwl &ctB, const libcamera::YamlObject &params)
{
if (params.size() % 3) {
LOG(RPiAwb, Error) << "AwbConfig: incomplete CT curve entry";
@@ -103,8 +104,8 @@ int AwbConfig::read(const libcamera::YamlObject &params)
if (ret)
return ret;
/* We will want the inverse functions of these too. */
- ctRInverse = ctR.inverse();
- ctBInverse = ctB.inverse();
+ ctRInverse = ctR.inverse().first;
+ ctBInverse = ctB.inverse().first;
}
if (params.contains("priors")) {
@@ -207,7 +208,7 @@ void Awb::initialise()
* them.
*/
if (!config_.ctR.empty() && !config_.ctB.empty()) {
- syncResults_.temperatureK = config_.ctR.domain().clip(4000);
+ syncResults_.temperatureK = config_.ctR.domain().clamp(4000);
syncResults_.gainR = 1.0 / config_.ctR.eval(syncResults_.temperatureK);
syncResults_.gainG = 1.0;
syncResults_.gainB = 1.0 / config_.ctB.eval(syncResults_.temperatureK);
@@ -273,8 +274,8 @@ void Awb::setManualGains(double manualR, double manualB)
syncResults_.gainB = prevSyncResults_.gainB = manualB_;
if (config_.bayes) {
/* Also estimate the best corresponding colour temperature from the curves. */
- double ctR = config_.ctRInverse.eval(config_.ctRInverse.domain().clip(1 / manualR_));
- double ctB = config_.ctBInverse.eval(config_.ctBInverse.domain().clip(1 / manualB_));
+ double ctR = config_.ctRInverse.eval(config_.ctRInverse.domain().clamp(1 / manualR_));
+ double ctB = config_.ctBInverse.eval(config_.ctBInverse.domain().clamp(1 / manualB_));
prevSyncResults_.temperatureK = (ctR + ctB) / 2;
syncResults_.temperatureK = prevSyncResults_.temperatureK;
}
@@ -468,7 +469,7 @@ double Awb::computeDelta2Sum(double gainR, double gainB)
return delta2Sum;
}
-Pwl Awb::interpolatePrior()
+ipa::Pwl Awb::interpolatePrior()
{
/*
* Interpolate the prior log likelihood function for our current lux
@@ -485,7 +486,7 @@ Pwl Awb::interpolatePrior()
idx++;
double lux0 = config_.priors[idx].lux,
lux1 = config_.priors[idx + 1].lux;
- return Pwl::combine(config_.priors[idx].prior,
+ return ipa::Pwl::combine(config_.priors[idx].prior,
config_.priors[idx + 1].prior,
[&](double /*x*/, double y0, double y1) {
return y0 + (y1 - y0) *
@@ -494,26 +495,26 @@ Pwl Awb::interpolatePrior()
}
}
-static double interpolateQuadatric(Pwl::Point const &a, Pwl::Point const &b,
- Pwl::Point const &c)
+static double interpolateQuadatric(ipa::Pwl::Point const &a, ipa::Pwl::Point const &b,
+ ipa::Pwl::Point const &c)
{
/*
* Given 3 points on a curve, find the extremum of the function in that
* interval by fitting a quadratic.
*/
const double eps = 1e-3;
- Pwl::Point ca = c - a, ba = b - a;
- double denominator = 2 * (ba.y * ca.x - ca.y * ba.x);
+ ipa::Pwl::Point ca = c - a, ba = b - a;
+ double denominator = 2 * (ba.y() * ca.x() - ca.y() * ba.x());
if (abs(denominator) > eps) {
- double numerator = ba.y * ca.x * ca.x - ca.y * ba.x * ba.x;
- double result = numerator / denominator + a.x;
- return std::max(a.x, std::min(c.x, result));
+ double numerator = ba.y() * ca.x() * ca.x() - ca.y() * ba.x() * ba.x();
+ double result = numerator / denominator + a.x();
+ return std::max(a.x(), std::min(c.x(), result));
}
/* has degenerated to straight line segment */
- return a.y < c.y - eps ? a.x : (c.y < a.y - eps ? c.x : b.x);
+ return a.y() < c.y() - eps ? a.x() : (c.y() < a.y() - eps ? c.x() : b.x());
}
-double Awb::coarseSearch(Pwl const &prior)
+double Awb::coarseSearch(ipa::Pwl const &prior)
{
points_.clear(); /* assume doesn't deallocate memory */
size_t bestPoint = 0;
@@ -525,22 +526,22 @@ double Awb::coarseSearch(Pwl const &prior)
double b = config_.ctB.eval(t, &spanB);
double gainR = 1 / r, gainB = 1 / b;
double delta2Sum = computeDelta2Sum(gainR, gainB);
- double priorLogLikelihood = prior.eval(prior.domain().clip(t));
+ double priorLogLikelihood = prior.eval(prior.domain().clamp(t));
double finalLogLikelihood = delta2Sum - priorLogLikelihood;
LOG(RPiAwb, Debug)
<< "t: " << t << " gain R " << gainR << " gain B "
<< gainB << " delta2_sum " << delta2Sum
<< " prior " << priorLogLikelihood << " final "
<< finalLogLikelihood;
- points_.push_back(Pwl::Point(t, finalLogLikelihood));
- if (points_.back().y < points_[bestPoint].y)
+ points_.push_back(ipa::Pwl::Point({ t, finalLogLikelihood }));
+ if (points_.back().y() < points_[bestPoint].y())
bestPoint = points_.size() - 1;
if (t == mode_->ctHi)
break;
/* for even steps along the r/b curve scale them by the current t */
t = std::min(t + t / 10 * config_.coarseStep, mode_->ctHi);
}
- t = points_[bestPoint].x;
+ t = points_[bestPoint].x();
LOG(RPiAwb, Debug) << "Coarse search found CT " << t;
/*
* We have the best point of the search, but refine it with a quadratic
@@ -559,7 +560,7 @@ double Awb::coarseSearch(Pwl const &prior)
return t;
}
-void Awb::fineSearch(double &t, double &r, double &b, Pwl const &prior)
+void Awb::fineSearch(double &t, double &r, double &b, ipa::Pwl const &prior)
{
int spanR = -1, spanB = -1;
config_.ctR.eval(t, &spanR);
@@ -570,14 +571,14 @@ void Awb::fineSearch(double &t, double &r, double &b, Pwl const &prior)
config_.ctR.eval(t - nsteps * step, &spanR);
double bDiff = config_.ctB.eval(t + nsteps * step, &spanB) -
config_.ctB.eval(t - nsteps * step, &spanB);
- Pwl::Point transverse(bDiff, -rDiff);
- if (transverse.len2() < 1e-6)
+ ipa::Pwl::Point transverse({ bDiff, -rDiff });
+ if (transverse.length2() < 1e-6)
return;
/*
* unit vector orthogonal to the b vs. r function (pointing outwards
* with r and b increasing)
*/
- transverse = transverse / transverse.len();
+ transverse = transverse / transverse.length();
double bestLogLikelihood = 0, bestT = 0, bestR = 0, bestB = 0;
double transverseRange = config_.transverseNeg + config_.transversePos;
const int maxNumDeltas = 12;
@@ -592,26 +593,26 @@ void Awb::fineSearch(double &t, double &r, double &b, Pwl const &prior)
for (int i = -nsteps; i <= nsteps; i++) {
double tTest = t + i * step;
double priorLogLikelihood =
- prior.eval(prior.domain().clip(tTest));
+ prior.eval(prior.domain().clamp(tTest));
double rCurve = config_.ctR.eval(tTest, &spanR);
double bCurve = config_.ctB.eval(tTest, &spanB);
/* x will be distance off the curve, y the log likelihood there */
- Pwl::Point points[maxNumDeltas];
+ ipa::Pwl::Point points[maxNumDeltas];
int bestPoint = 0;
/* Take some measurements transversely *off* the CT curve. */
for (int j = 0; j < numDeltas; j++) {
- points[j].x = -config_.transverseNeg +
- (transverseRange * j) / (numDeltas - 1);
- Pwl::Point rbTest = Pwl::Point(rCurve, bCurve) +
- transverse * points[j].x;
- double rTest = rbTest.x, bTest = rbTest.y;
+ points[j][0] = -config_.transverseNeg +
+ (transverseRange * j) / (numDeltas - 1);
+ ipa::Pwl::Point rbTest = ipa::Pwl::Point({ rCurve, bCurve }) +
+ transverse * points[j].x();
+ double rTest = rbTest.x(), bTest = rbTest.y();
double gainR = 1 / rTest, gainB = 1 / bTest;
double delta2Sum = computeDelta2Sum(gainR, gainB);
- points[j].y = delta2Sum - priorLogLikelihood;
+ points[j][1] = delta2Sum - priorLogLikelihood;
LOG(RPiAwb, Debug)
<< "At t " << tTest << " r " << rTest << " b "
- << bTest << ": " << points[j].y;
- if (points[j].y < points[bestPoint].y)
+ << bTest << ": " << points[j].y();
+ if (points[j].y() < points[bestPoint].y())
bestPoint = j;
}
/*
@@ -619,11 +620,11 @@ void Awb::fineSearch(double &t, double &r, double &b, Pwl const &prior)
* now let's do a quadratic interpolation for the best result.
*/
bestPoint = std::max(1, std::min(bestPoint, numDeltas - 2));
- Pwl::Point rbTest = Pwl::Point(rCurve, bCurve) +
- transverse * interpolateQuadatric(points[bestPoint - 1],
- points[bestPoint],
- points[bestPoint + 1]);
- double rTest = rbTest.x, bTest = rbTest.y;
+ ipa::Pwl::Point rbTest = ipa::Pwl::Point({ rCurve, bCurve }) +
+ transverse * interpolateQuadatric(points[bestPoint - 1],
+ points[bestPoint],
+ points[bestPoint + 1]);
+ double rTest = rbTest.x(), bTest = rbTest.y();
double gainR = 1 / rTest, gainB = 1 / bTest;
double delta2Sum = computeDelta2Sum(gainR, gainB);
double finalLogLikelihood = delta2Sum - priorLogLikelihood;
@@ -653,7 +654,7 @@ void Awb::awbBayes()
* Get the current prior, and scale according to how many zones are
* valid... not entirely sure about this.
*/
- Pwl prior = interpolatePrior();
+ ipa::Pwl prior = interpolatePrior();
prior *= zones_.size() / (double)(statistics_->awbRegions.numRegions());
prior.map([](double x, double y) {
LOG(RPiAwb, Debug) << "(" << x << "," << y << ")";
diff --git a/src/ipa/rpi/controller/rpi/awb.h b/src/ipa/rpi/controller/rpi/awb.h
index 499b4519..ab30f4fa 100644
--- a/src/ipa/rpi/controller/rpi/awb.h
+++ b/src/ipa/rpi/controller/rpi/awb.h
@@ -10,11 +10,14 @@
#include <condition_variable>
#include <thread>
+#include <libcamera/geometry.h>
+
#include "../awb_algorithm.h"
-#include "../pwl.h"
#include "../awb_status.h"
#include "../statistics.h"
+#include "libipa/pwl.h"
+
namespace RPiController {
/* Control algorithm to perform AWB calculations. */
@@ -28,7 +31,7 @@ struct AwbMode {
struct AwbPrior {
int read(const libcamera::YamlObject &params);
double lux; /* lux level */
- Pwl prior; /* maps CT to prior log likelihood for this lux level */
+ libcamera::ipa::Pwl prior; /* maps CT to prior log likelihood for this lux level */
};
struct AwbConfig {
@@ -41,10 +44,10 @@ struct AwbConfig {
unsigned int convergenceFrames; /* approx number of frames to converge */
double speed; /* IIR filter speed applied to algorithm results */
bool fast; /* "fast" mode uses a 16x16 rather than 32x32 grid */
- Pwl ctR; /* function maps CT to r (= R/G) */
- Pwl ctB; /* function maps CT to b (= B/G) */
- Pwl ctRInverse; /* inverse of ctR */
- Pwl ctBInverse; /* inverse of ctB */
+ libcamera::ipa::Pwl ctR; /* function maps CT to r (= R/G) */
+ libcamera::ipa::Pwl ctB; /* function maps CT to b (= B/G) */
+ libcamera::ipa::Pwl ctRInverse; /* inverse of ctR */
+ libcamera::ipa::Pwl ctBInverse; /* inverse of ctB */
/* table of illuminant priors at different lux levels */
std::vector<AwbPrior> priors;
/* AWB "modes" (determines the search range) */
@@ -161,11 +164,11 @@ private:
void awbGrey();
void prepareStats();
double computeDelta2Sum(double gainR, double gainB);
- Pwl interpolatePrior();
- double coarseSearch(Pwl const &prior);
- void fineSearch(double &t, double &r, double &b, Pwl const &prior);
+ libcamera::ipa::Pwl interpolatePrior();
+ double coarseSearch(libcamera::ipa::Pwl const &prior);
+ void fineSearch(double &t, double &r, double &b, libcamera::ipa::Pwl const &prior);
std::vector<RGB> zones_;
- std::vector<Pwl::Point> points_;
+ std::vector<libcamera::ipa::Pwl::Point> points_;
/* manual r setting */
double manualR_;
/* manual b setting */
diff --git a/src/ipa/rpi/controller/rpi/ccm.cpp b/src/ipa/rpi/controller/rpi/ccm.cpp
index c5588029..e673964c 100644
--- a/src/ipa/rpi/controller/rpi/ccm.cpp
+++ b/src/ipa/rpi/controller/rpi/ccm.cpp
@@ -71,9 +71,9 @@ int Ccm::read(const libcamera::YamlObject &params)
int ret;
if (params.contains("saturation")) {
- ret = config_.saturation.read(params["saturation"]);
- if (ret)
- return ret;
+ config_.saturation = params["saturation"].get<ipa::Pwl>(ipa::Pwl{});
+ if (config_.saturation.empty())
+ return -EINVAL;
}
for (auto &p : params["ccms"].asList()) {
@@ -172,7 +172,7 @@ void Ccm::prepare(Metadata *imageMetadata)
ccmStatus.saturation = saturation;
if (!config_.saturation.empty())
saturation *= config_.saturation.eval(
- config_.saturation.domain().clip(lux.lux));
+ config_.saturation.domain().clamp(lux.lux));
ccm = applySaturation(ccm, saturation);
for (int j = 0; j < 3; j++)
for (int i = 0; i < 3; i++)
diff --git a/src/ipa/rpi/controller/rpi/ccm.h b/src/ipa/rpi/controller/rpi/ccm.h
index b3abeddf..4e5b33fe 100644
--- a/src/ipa/rpi/controller/rpi/ccm.h
+++ b/src/ipa/rpi/controller/rpi/ccm.h
@@ -8,8 +8,9 @@
#include <vector>
+#include <libipa/pwl.h>
+
#include "../ccm_algorithm.h"
-#include "../pwl.h"
namespace RPiController {
@@ -54,7 +55,7 @@ struct CtCcm {
struct CcmConfig {
std::vector<CtCcm> ccms;
- Pwl saturation;
+ libcamera::ipa::Pwl saturation;
};
class Ccm : public CcmAlgorithm
diff --git a/src/ipa/rpi/controller/rpi/contrast.cpp b/src/ipa/rpi/controller/rpi/contrast.cpp
index 9eef792d..9b37943a 100644
--- a/src/ipa/rpi/controller/rpi/contrast.cpp
+++ b/src/ipa/rpi/controller/rpi/contrast.cpp
@@ -53,7 +53,9 @@ int Contrast::read(const libcamera::YamlObject &params)
config_.hiHistogram = params["hi_histogram"].get<double>(0.95);
config_.hiLevel = params["hi_level"].get<double>(0.95);
config_.hiMax = params["hi_max"].get<double>(2000);
- return config_.gammaCurve.read(params["gamma_curve"]);
+
+ config_.gammaCurve = params["gamma_curve"].get<ipa::Pwl>(ipa::Pwl{});
+ return config_.gammaCurve.empty() ? -EINVAL : 0;
}
void Contrast::setBrightness(double brightness)
@@ -92,10 +94,10 @@ void Contrast::prepare(Metadata *imageMetadata)
imageMetadata->set("contrast.status", status_);
}
-Pwl computeStretchCurve(Histogram const &histogram,
+ipa::Pwl computeStretchCurve(Histogram const &histogram,
ContrastConfig const &config)
{
- Pwl enhance;
+ ipa::Pwl enhance;
enhance.append(0, 0);
/*
* If the start of the histogram is rather empty, try to pull it down a
@@ -136,10 +138,10 @@ Pwl computeStretchCurve(Histogram const &histogram,
return enhance;
}
-Pwl applyManualContrast(Pwl const &gammaCurve, double brightness,
- double contrast)
+ipa::Pwl applyManualContrast(ipa::Pwl const &gammaCurve, double brightness,
+ double contrast)
{
- Pwl newGammaCurve;
+ ipa::Pwl newGammaCurve;
LOG(RPiContrast, Debug)
<< "Manual brightness " << brightness << " contrast " << contrast;
gammaCurve.map([&](double x, double y) {
@@ -160,7 +162,7 @@ void Contrast::process(StatisticsPtr &stats,
* ways: 1. Adjust the gamma curve so as to pull the start of the
* histogram down, and possibly push the end up.
*/
- Pwl gammaCurve = config_.gammaCurve;
+ ipa::Pwl gammaCurve = config_.gammaCurve;
if (ceEnable_) {
if (config_.loMax != 0 || config_.hiMax != 0)
gammaCurve = computeStretchCurve(histogram, config_).compose(gammaCurve);
diff --git a/src/ipa/rpi/controller/rpi/contrast.h b/src/ipa/rpi/controller/rpi/contrast.h
index a9d9bbc9..c0f7db98 100644
--- a/src/ipa/rpi/controller/rpi/contrast.h
+++ b/src/ipa/rpi/controller/rpi/contrast.h
@@ -8,8 +8,9 @@
#include <mutex>
+#include <libipa/pwl.h>
+
#include "../contrast_algorithm.h"
-#include "../pwl.h"
namespace RPiController {
@@ -26,7 +27,7 @@ struct ContrastConfig {
double hiHistogram;
double hiLevel;
double hiMax;
- Pwl gammaCurve;
+ libcamera::ipa::Pwl gammaCurve;
};
class Contrast : public ContrastAlgorithm
diff --git a/src/ipa/rpi/controller/rpi/geq.cpp b/src/ipa/rpi/controller/rpi/geq.cpp
index fb539d1f..40e7191b 100644
--- a/src/ipa/rpi/controller/rpi/geq.cpp
+++ b/src/ipa/rpi/controller/rpi/geq.cpp
@@ -9,7 +9,6 @@
#include "../device_status.h"
#include "../lux_status.h"
-#include "../pwl.h"
#include "geq.h"
@@ -45,9 +44,9 @@ int Geq::read(const libcamera::YamlObject &params)
}
if (params.contains("strength")) {
- int ret = config_.strength.read(params["strength"]);
- if (ret)
- return ret;
+ config_.strength = params["strength"].get<ipa::Pwl>(ipa::Pwl{});
+ if (config_.strength.empty())
+ return -EINVAL;
}
return 0;
@@ -67,7 +66,7 @@ void Geq::prepare(Metadata *imageMetadata)
GeqStatus geqStatus = {};
double strength = config_.strength.empty()
? 1.0
- : config_.strength.eval(config_.strength.domain().clip(luxStatus.lux));
+ : config_.strength.eval(config_.strength.domain().clamp(luxStatus.lux));
strength *= deviceStatus.analogueGain;
double offset = config_.offset * strength;
double slope = config_.slope * strength;
diff --git a/src/ipa/rpi/controller/rpi/geq.h b/src/ipa/rpi/controller/rpi/geq.h
index 2c8400c2..e8b9f427 100644
--- a/src/ipa/rpi/controller/rpi/geq.h
+++ b/src/ipa/rpi/controller/rpi/geq.h
@@ -6,6 +6,8 @@
*/
#pragma once
+#include <libipa/pwl.h>
+
#include "../algorithm.h"
#include "../geq_status.h"
@@ -16,7 +18,7 @@ namespace RPiController {
struct GeqConfig {
uint16_t offset;
double slope;
- Pwl strength; /* lux to strength factor */
+ libcamera::ipa::Pwl strength; /* lux to strength factor */
};
class Geq : public Algorithm
diff --git a/src/ipa/rpi/controller/rpi/hdr.cpp b/src/ipa/rpi/controller/rpi/hdr.cpp
index 34cf360e..f3da8291 100644
--- a/src/ipa/rpi/controller/rpi/hdr.cpp
+++ b/src/ipa/rpi/controller/rpi/hdr.cpp
@@ -42,7 +42,7 @@ void HdrConfig::read(const libcamera::YamlObject &params, const std::string &mod
/* Lens shading related parameters. */
if (params.contains("spatial_gain_curve")) {
- spatialGainCurve.read(params["spatial_gain_curve"]);
+ spatialGainCurve = params["spatial_gain_curve"].get<ipa::Pwl>(ipa::Pwl{});
} else if (params.contains("spatial_gain")) {
double spatialGain = params["spatial_gain"].get<double>(2.0);
spatialGainCurve.append(0.0, spatialGain);
@@ -66,7 +66,7 @@ void HdrConfig::read(const libcamera::YamlObject &params, const std::string &mod
iirStrength = params["iir_strength"].get<double>(8.0);
strength = params["strength"].get<double>(1.5);
if (tonemapEnable)
- tonemap.read(params["tonemap"]);
+ tonemap = params["tonemap"].get<ipa::Pwl>(ipa::Pwl{});
speed = params["speed"].get<double>(1.0);
if (params.contains("hi_quantile_targets")) {
hiQuantileTargets = params["hi_quantile_targets"].getList<double>().value();
@@ -212,7 +212,7 @@ bool Hdr::updateTonemap([[maybe_unused]] StatisticsPtr &stats, HdrConfig &config
/* When there's a change of HDR mode we start over with a new tonemap curve. */
if (delayedStatus_.mode != previousMode_) {
previousMode_ = delayedStatus_.mode;
- tonemap_ = Pwl();
+ tonemap_ = ipa::Pwl();
}
/* No tonemapping. No need to output a tonemap.status. */
@@ -275,7 +275,7 @@ bool Hdr::updateTonemap([[maybe_unused]] StatisticsPtr &stats, HdrConfig &config
double power = std::clamp(min_power, config.powerMin, config.powerMax);
/* Generate the tonemap, including the contrast adjustment factors. */
- Pwl tonemap;
+ libcamera::ipa::Pwl tonemap;
tonemap.append(0, 0);
for (unsigned int i = 0; i <= 6; i++) {
double x = 1 << (i + 9); /* x loops from 512 to 32768 inclusive */
diff --git a/src/ipa/rpi/controller/rpi/hdr.h b/src/ipa/rpi/controller/rpi/hdr.h
index 9b7327f8..5c2f3988 100644
--- a/src/ipa/rpi/controller/rpi/hdr.h
+++ b/src/ipa/rpi/controller/rpi/hdr.h
@@ -12,9 +12,10 @@
#include <libcamera/geometry.h>
+#include <libipa/pwl.h>
+
#include "../hdr_algorithm.h"
#include "../hdr_status.h"
-#include "../pwl.h"
/* This is our implementation of an HDR algorithm. */
@@ -26,7 +27,7 @@ struct HdrConfig {
std::map<unsigned int, std::string> channelMap;
/* Lens shading related parameters. */
- Pwl spatialGainCurve; /* Brightness to gain curve for different image regions. */
+ libcamera::ipa::Pwl spatialGainCurve; /* Brightness to gain curve for different image regions. */
unsigned int diffusion; /* How much to diffuse the gain spatially. */
/* Tonemap related parameters. */
@@ -35,7 +36,7 @@ struct HdrConfig {
double detailSlope;
double iirStrength;
double strength;
- Pwl tonemap;
+ libcamera::ipa::Pwl tonemap;
/* These relate to adaptive tonemap calculation. */
double speed;
std::vector<double> hiQuantileTargets; /* quantiles to check for unsaturated images */
@@ -75,7 +76,7 @@ private:
HdrStatus status_; /* track the current HDR mode and channel */
HdrStatus delayedStatus_; /* track the delayed HDR mode and channel */
std::string previousMode_;
- Pwl tonemap_;
+ libcamera::ipa::Pwl tonemap_;
libcamera::Size regions_; /* stats regions */
unsigned int numRegions_; /* total number of stats regions */
std::vector<double> gains_[2];
diff --git a/src/ipa/rpi/controller/rpi/tonemap.cpp b/src/ipa/rpi/controller/rpi/tonemap.cpp
index 0426e972..3422adfe 100644
--- a/src/ipa/rpi/controller/rpi/tonemap.cpp
+++ b/src/ipa/rpi/controller/rpi/tonemap.cpp
@@ -33,7 +33,7 @@ int Tonemap::read(const libcamera::YamlObject &params)
config_.detailSlope = params["detail_slope"].get<double>(0.1);
config_.iirStrength = params["iir_strength"].get<double>(1.0);
config_.strength = params["strength"].get<double>(1.0);
- config_.tonemap.read(params["tone_curve"]);
+ config_.tonemap = params["tone_curve"].get<ipa::Pwl>(ipa::Pwl{});
return 0;
}
diff --git a/src/ipa/rpi/controller/rpi/tonemap.h b/src/ipa/rpi/controller/rpi/tonemap.h
index f25aa47f..ba0cf5c4 100644
--- a/src/ipa/rpi/controller/rpi/tonemap.h
+++ b/src/ipa/rpi/controller/rpi/tonemap.h
@@ -6,8 +6,9 @@
*/
#pragma once
+#include <libipa/pwl.h>
+
#include "algorithm.h"
-#include "pwl.h"
namespace RPiController {
@@ -16,7 +17,7 @@ struct TonemapConfig {
double detailSlope;
double iirStrength;
double strength;
- Pwl tonemap;
+ libcamera::ipa::Pwl tonemap;
};
class Tonemap : public Algorithm
diff --git a/src/ipa/rpi/controller/tonemap_status.h b/src/ipa/rpi/controller/tonemap_status.h
index 41a7bf2f..0364ff66 100644
--- a/src/ipa/rpi/controller/tonemap_status.h
+++ b/src/ipa/rpi/controller/tonemap_status.h
@@ -6,12 +6,12 @@
*/
#pragma once
-#include "pwl.h"
+#include <libipa/pwl.h>
struct TonemapStatus {
uint16_t detailConstant;
double detailSlope;
double iirStrength;
double strength;
- RPiController::Pwl tonemap;
+ libcamera::ipa::Pwl tonemap;
};
diff --git a/src/ipa/rpi/vc4/meson.build b/src/ipa/rpi/vc4/meson.build
index 590e9197..63fc5925 100644
--- a/src/ipa/rpi/vc4/meson.build
+++ b/src/ipa/rpi/vc4/meson.build
@@ -15,7 +15,6 @@ vc4_ipa_libs = [
vc4_ipa_includes = [
ipa_includes,
- libipa_includes,
]
vc4_ipa_sources = files([
@@ -28,8 +27,7 @@ mod = shared_module(ipa_name,
[vc4_ipa_sources, libcamera_generated_ipa_headers],
name_prefix : '',
include_directories : vc4_ipa_includes,
- dependencies : vc4_ipa_deps,
- link_with : libipa,
+ dependencies : [vc4_ipa_deps, libipa_dep],
link_whole : vc4_ipa_libs,
install : true,
install_dir : ipa_install_dir)
diff --git a/src/ipa/simple/black_level.cpp b/src/ipa/simple/black_level.cpp
index 53b4c040..cc490eb5 100644
--- a/src/ipa/simple/black_level.cpp
+++ b/src/ipa/simple/black_level.cpp
@@ -43,7 +43,7 @@ BlackLevel::BlackLevel()
* \return The black level, in the range from 0 (minimum) to 255 (maximum).
* If the black level couldn't be determined yet, return 0.
*/
-unsigned int BlackLevel::get() const
+uint8_t BlackLevel::get() const
{
return blackLevelSet_ ? blackLevel_ : 0;
}
diff --git a/src/ipa/simple/black_level.h b/src/ipa/simple/black_level.h
index 25094217..5e032f9f 100644
--- a/src/ipa/simple/black_level.h
+++ b/src/ipa/simple/black_level.h
@@ -8,6 +8,7 @@
#pragma once
#include <array>
+#include <stdint.h>
#include "libcamera/internal/software_isp/swisp_stats.h"
@@ -17,11 +18,11 @@ class BlackLevel
{
public:
BlackLevel();
- unsigned int get() const;
+ uint8_t get() const;
void update(SwIspStats::Histogram &yHistogram);
private:
- unsigned int blackLevel_;
+ uint8_t blackLevel_;
bool blackLevelSet_;
};
diff --git a/src/ipa/simple/meson.build b/src/ipa/simple/meson.build
index 44b5f1d7..33d1c96a 100644
--- a/src/ipa/simple/meson.build
+++ b/src/ipa/simple/meson.build
@@ -10,9 +10,8 @@ soft_simple_sources = files([
mod = shared_module(ipa_name,
[soft_simple_sources, libcamera_generated_ipa_headers],
name_prefix : '',
- include_directories : [ipa_includes, libipa_includes],
- dependencies : libcamera_private,
- link_with : libipa,
+ include_directories : [ipa_includes],
+ dependencies : [libcamera_private, libipa_dep],
install : true,
install_dir : ipa_install_dir)
diff --git a/src/ipa/simple/soft_simple.cpp b/src/ipa/simple/soft_simple.cpp
index a5bb2bbf..b7746ce0 100644
--- a/src/ipa/simple/soft_simple.cpp
+++ b/src/ipa/simple/soft_simple.cpp
@@ -5,6 +5,9 @@
* Simple Software Image Processing Algorithm module
*/
+#include <cmath>
+#include <numeric>
+#include <stdint.h>
#include <sys/mman.h>
#include <linux/v4l2-controls.h>
@@ -82,6 +85,10 @@ private:
ControlInfoMap sensorInfoMap_;
BlackLevel blackLevel_;
+ static constexpr unsigned int kGammaLookupSize = 1024;
+ std::array<uint8_t, kGammaLookupSize> gammaTable_;
+ int lastBlackLevel_ = -1;
+
int32_t exposureMin_, exposureMax_;
int32_t exposure_;
double againMin_, againMax_, againMinStep_;
@@ -240,27 +247,61 @@ void IPASoftSimple::stop()
void IPASoftSimple::processStats(const ControlList &sensorControls)
{
+ SwIspStats::Histogram histogram = stats_->yHistogram;
+ if (ignoreUpdates_ > 0)
+ blackLevel_.update(histogram);
+ const uint8_t blackLevel = blackLevel_.get();
+
+ /*
+ * Black level must be subtracted to get the correct AWB ratios, they
+ * would be off if they were computed from the whole brightness range
+ * rather than from the sensor range.
+ */
+ const uint64_t nPixels = std::accumulate(
+ histogram.begin(), histogram.end(), 0);
+ const uint64_t offset = blackLevel * nPixels;
+ const uint64_t sumR = stats_->sumR_ - offset / 4;
+ const uint64_t sumG = stats_->sumG_ - offset / 2;
+ const uint64_t sumB = stats_->sumB_ - offset / 4;
+
/*
* Calculate red and blue gains for AWB.
* Clamp max gain at 4.0, this also avoids 0 division.
+ * Gain: 128 = 0.5, 256 = 1.0, 512 = 2.0, etc.
*/
- if (stats_->sumR_ <= stats_->sumG_ / 4)
- params_->gainR = 1024;
- else
- params_->gainR = 256 * stats_->sumG_ / stats_->sumR_;
+ const unsigned int gainR = sumR <= sumG / 4 ? 1024 : 256 * sumG / sumR;
+ const unsigned int gainB = sumB <= sumG / 4 ? 1024 : 256 * sumG / sumB;
+ /* Green gain and gamma values are fixed */
+ constexpr unsigned int gainG = 256;
+
+ /* Update the gamma table if needed */
+ if (blackLevel != lastBlackLevel_) {
+ constexpr float gamma = 0.5;
+ const unsigned int blackIndex = blackLevel * kGammaLookupSize / 256;
+ std::fill(gammaTable_.begin(), gammaTable_.begin() + blackIndex, 0);
+ const float divisor = kGammaLookupSize - blackIndex - 1.0;
+ for (unsigned int i = blackIndex; i < kGammaLookupSize; i++)
+ gammaTable_[i] = UINT8_MAX *
+ std::pow((i - blackIndex) / divisor, gamma);
+
+ lastBlackLevel_ = blackLevel;
+ }
- if (stats_->sumB_ <= stats_->sumG_ / 4)
- params_->gainB = 1024;
- else
- params_->gainB = 256 * stats_->sumG_ / stats_->sumB_;
+ for (unsigned int i = 0; i < DebayerParams::kRGBLookupSize; i++) {
+ constexpr unsigned int div =
+ DebayerParams::kRGBLookupSize * 256 / kGammaLookupSize;
+ unsigned int idx;
- /* Green gain and gamma values are fixed */
- params_->gainG = 256;
- params_->gamma = 0.5;
+ /* Apply gamma after gain! */
+ idx = std::min({ i * gainR / div, (kGammaLookupSize - 1) });
+ params_->red[i] = gammaTable_[idx];
- if (ignoreUpdates_ > 0)
- blackLevel_.update(stats_->yHistogram);
- params_->blackLevel = blackLevel_.get();
+ idx = std::min({ i * gainG / div, (kGammaLookupSize - 1) });
+ params_->green[i] = gammaTable_[idx];
+
+ idx = std::min({ i * gainB / div, (kGammaLookupSize - 1) });
+ params_->blue[i] = gammaTable_[idx];
+ }
setIspParams.emit();
@@ -281,7 +322,7 @@ void IPASoftSimple::processStats(const ControlList &sensorControls)
* https://www.araa.asn.au/acra/acra2007/papers/paper84final.pdf
*/
const unsigned int blackLevelHistIdx =
- params_->blackLevel / (256 / SwIspStats::kYHistogramSize);
+ blackLevel / (256 / SwIspStats::kYHistogramSize);
const unsigned int histogramSize =
SwIspStats::kYHistogramSize - blackLevelHistIdx;
const unsigned int yHistValsPerBin = histogramSize / kExposureBinsCount;
@@ -329,8 +370,8 @@ void IPASoftSimple::processStats(const ControlList &sensorControls)
LOG(IPASoft, Debug) << "exposureMSV " << exposureMSV
<< " exp " << exposure_ << " again " << again_
- << " gain R/B " << params_->gainR << "/" << params_->gainB
- << " black level " << params_->blackLevel;
+ << " gain R/B " << gainR << "/" << gainB
+ << " black level " << static_cast<unsigned int>(blackLevel);
}
void IPASoftSimple::updateExposure(double exposureMSV)
diff --git a/src/ipa/vimc/meson.build b/src/ipa/vimc/meson.build
index 264a2d9a..d0b63edd 100644
--- a/src/ipa/vimc/meson.build
+++ b/src/ipa/vimc/meson.build
@@ -5,9 +5,8 @@ ipa_name = 'ipa_vimc'
mod = shared_module(ipa_name,
['vimc.cpp', libcamera_generated_ipa_headers],
name_prefix : '',
- include_directories : [ipa_includes, libipa_includes],
- dependencies : libcamera_private,
- link_with : libipa,
+ include_directories : [ipa_includes],
+ dependencies : [libcamera_private, libipa_dep],
install : true,
install_dir : ipa_install_dir)
diff --git a/src/libcamera/control_ids_core.yaml b/src/libcamera/control_ids_core.yaml
index bf1f1a83..9d413a94 100644
--- a/src/libcamera/control_ids_core.yaml
+++ b/src/libcamera/control_ids_core.yaml
@@ -865,4 +865,11 @@ controls:
description: |
This is a long exposure image.
+ - Gamma:
+ type: float
+ description: |
+ Specify a fixed gamma value. Default must be 2.2 which closely mimics
+ sRGB gamma. Note that this is camera gamma, so it is applied as
+ 1.0/gamma.
+
...
diff --git a/src/libcamera/dma_buf_allocator.cpp b/src/libcamera/dma_buf_allocator.cpp
new file mode 100644
index 00000000..c06eca7d
--- /dev/null
+++ b/src/libcamera/dma_buf_allocator.cpp
@@ -0,0 +1,246 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2024, Red Hat Inc.
+ * Copyright (C) 2020, Raspberry Pi Ltd
+ *
+ * Helper class for dma-buf allocations.
+ */
+
+#include "libcamera/internal/dma_buf_allocator.h"
+
+#include <array>
+#include <fcntl.h>
+#include <sys/ioctl.h>
+#include <sys/mman.h>
+#include <sys/stat.h>
+#include <sys/syscall.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+#include <linux/dma-buf.h>
+#include <linux/dma-heap.h>
+#include <linux/udmabuf.h>
+
+#include <libcamera/base/log.h>
+
+/**
+ * \file dma_buf_allocator.cpp
+ * \brief dma-buf allocator
+ */
+
+namespace libcamera {
+
+#ifndef __DOXYGEN__
+struct DmaBufAllocatorInfo {
+ DmaBufAllocator::DmaBufAllocatorFlag type;
+ const char *deviceNodeName;
+};
+#endif
+
+static constexpr std::array<DmaBufAllocatorInfo, 4> providerInfos = { {
+ /*
+ * /dev/dma_heap/linux,cma is the CMA dma-heap. When the cma heap size is
+ * specified on the kernel command line, this gets renamed to "reserved".
+ */
+ { DmaBufAllocator::DmaBufAllocatorFlag::CmaHeap, "/dev/dma_heap/linux,cma" },
+ { DmaBufAllocator::DmaBufAllocatorFlag::CmaHeap, "/dev/dma_heap/reserved" },
+ { DmaBufAllocator::DmaBufAllocatorFlag::SystemHeap, "/dev/dma_heap/system" },
+ { DmaBufAllocator::DmaBufAllocatorFlag::UDmaBuf, "/dev/udmabuf" },
+} };
+
+LOG_DEFINE_CATEGORY(DmaBufAllocator)
+
+/**
+ * \class DmaBufAllocator
+ * \brief Helper class for dma-buf allocations
+ *
+ * This class wraps a userspace dma-buf provider selected at construction time,
+ * and exposes functions to allocate dma-buffers from this provider.
+ *
+ * Different providers may provide dma-buffers with different properties for
+ * the underlying memory. Which providers are acceptable is specified through
+ * the type argument passed to the DmaBufAllocator() constructor.
+ */
+
+/**
+ * \enum DmaBufAllocator::DmaBufAllocatorFlag
+ * \brief Type of the dma-buf provider
+ * \var DmaBufAllocator::CmaHeap
+ * \brief Allocate from a CMA dma-heap, providing physically-contiguous memory
+ * \var DmaBufAllocator::SystemHeap
+ * \brief Allocate from the system dma-heap, using the page allocator
+ * \var DmaBufAllocator::UDmaBuf
+ * \brief Allocate using a memfd + /dev/udmabuf
+ */
+
+/**
+ * \typedef DmaBufAllocator::DmaBufAllocatorFlags
+ * \brief A bitwise combination of DmaBufAllocator::DmaBufAllocatorFlag values
+ */
+
+/**
+ * \brief Construct a DmaBufAllocator of a given type
+ * \param[in] type The type(s) of the dma-buf providers to allocate from
+ *
+ * The dma-buf provider type is selected with the \a type parameter, which
+ * defaults to the CMA heap. If no provider of the given type can be accessed,
+ * the constructed DmaBufAllocator instance is invalid as indicated by
+ * the isValid() function.
+ *
+ * Multiple types can be selected by combining type flags, in which case
+ * the constructed DmaBufAllocator will match one of the types. If multiple
+ * requested types can work on the system, which provider is used is undefined.
+ */
+DmaBufAllocator::DmaBufAllocator(DmaBufAllocatorFlags type)
+{
+ for (const auto &info : providerInfos) {
+ if (!(type & info.type))
+ continue;
+
+ int ret = ::open(info.deviceNodeName, O_RDWR | O_CLOEXEC, 0);
+ if (ret < 0) {
+ ret = errno;
+ LOG(DmaBufAllocator, Debug)
+ << "Failed to open " << info.deviceNodeName << ": "
+ << strerror(ret);
+ continue;
+ }
+
+ LOG(DmaBufAllocator, Debug) << "Using " << info.deviceNodeName;
+ providerHandle_ = UniqueFD(ret);
+ type_ = info.type;
+ break;
+ }
+
+ if (!providerHandle_.isValid())
+ LOG(DmaBufAllocator, Error) << "Could not open any dma-buf provider";
+}
+
+/**
+ * \brief Destroy the DmaBufAllocator instance
+ */
+DmaBufAllocator::~DmaBufAllocator() = default;
+
+/**
+ * \fn DmaBufAllocator::isValid()
+ * \brief Check if the DmaBufAllocator instance is valid
+ * \return True if the DmaBufAllocator is valid, false otherwise
+ */
+
+/* uClibc doesn't provide the file sealing API. */
+#ifndef __DOXYGEN__
+#if not HAVE_FILE_SEALS
+#define F_ADD_SEALS 1033
+#define F_SEAL_SHRINK 0x0002
+#endif
+#endif
+
+UniqueFD DmaBufAllocator::allocFromUDmaBuf(const char *name, std::size_t size)
+{
+ /* Size must be a multiple of the page size. Round it up. */
+ std::size_t pageMask = sysconf(_SC_PAGESIZE) - 1;
+ size = (size + pageMask) & ~pageMask;
+
+#if HAVE_MEMFD_CREATE
+ int ret = memfd_create(name, MFD_ALLOW_SEALING | MFD_CLOEXEC);
+#else
+ int ret = syscall(SYS_memfd_create, name, MFD_ALLOW_SEALING | MFD_CLOEXEC);
+#endif
+ if (ret < 0) {
+ ret = errno;
+ LOG(DmaBufAllocator, Error)
+ << "Failed to allocate memfd storage for " << name
+ << ": " << strerror(ret);
+ return {};
+ }
+
+ UniqueFD memfd(ret);
+
+ ret = ftruncate(memfd.get(), size);
+ if (ret < 0) {
+ ret = errno;
+ LOG(DmaBufAllocator, Error)
+ << "Failed to set memfd size for " << name
+ << ": " << strerror(ret);
+ return {};
+ }
+
+ /* udmabuf dma-buffers *must* have the F_SEAL_SHRINK seal. */
+ ret = fcntl(memfd.get(), F_ADD_SEALS, F_SEAL_SHRINK);
+ if (ret < 0) {
+ ret = errno;
+ LOG(DmaBufAllocator, Error)
+ << "Failed to seal the memfd for " << name
+ << ": " << strerror(ret);
+ return {};
+ }
+
+ struct udmabuf_create create;
+
+ create.memfd = memfd.get();
+ create.flags = UDMABUF_FLAGS_CLOEXEC;
+ create.offset = 0;
+ create.size = size;
+
+ ret = ::ioctl(providerHandle_.get(), UDMABUF_CREATE, &create);
+ if (ret < 0) {
+ ret = errno;
+ LOG(DmaBufAllocator, Error)
+ << "Failed to create dma buf for " << name
+ << ": " << strerror(ret);
+ return {};
+ }
+
+ /* The underlying memfd is kept as as a reference in the kernel. */
+ return UniqueFD(ret);
+}
+
+UniqueFD DmaBufAllocator::allocFromHeap(const char *name, std::size_t size)
+{
+ struct dma_heap_allocation_data alloc = {};
+ int ret;
+
+ alloc.len = size;
+ alloc.fd_flags = O_CLOEXEC | O_RDWR;
+
+ ret = ::ioctl(providerHandle_.get(), DMA_HEAP_IOCTL_ALLOC, &alloc);
+ if (ret < 0) {
+ LOG(DmaBufAllocator, Error)
+ << "dma-heap allocation failure for " << name;
+ return {};
+ }
+
+ UniqueFD allocFd(alloc.fd);
+ ret = ::ioctl(allocFd.get(), DMA_BUF_SET_NAME, name);
+ if (ret < 0) {
+ LOG(DmaBufAllocator, Error)
+ << "dma-heap naming failure for " << name;
+ return {};
+ }
+
+ return allocFd;
+}
+
+/**
+ * \brief Allocate a dma-buf from the DmaBufAllocator
+ * \param [in] name The name to set for the allocated buffer
+ * \param [in] size The size of the buffer to allocate
+ *
+ * Allocates a dma-buf with read/write access.
+ *
+ * If the allocation fails, return an invalid UniqueFD.
+ *
+ * \return The UniqueFD of the allocated buffer
+ */
+UniqueFD DmaBufAllocator::alloc(const char *name, std::size_t size)
+{
+ if (!name)
+ return {};
+
+ if (type_ == DmaBufAllocator::DmaBufAllocatorFlag::UDmaBuf)
+ return allocFromUDmaBuf(name, size);
+ else
+ return allocFromHeap(name, size);
+}
+
+} /* namespace libcamera */
diff --git a/src/libcamera/dma_heaps.cpp b/src/libcamera/dma_heaps.cpp
deleted file mode 100644
index d4cb880b..00000000
--- a/src/libcamera/dma_heaps.cpp
+++ /dev/null
@@ -1,165 +0,0 @@
-/* SPDX-License-Identifier: LGPL-2.1-or-later */
-/*
- * Copyright (C) 2020, Raspberry Pi Ltd
- *
- * Helper class for dma-heap allocations.
- */
-
-#include "libcamera/internal/dma_heaps.h"
-
-#include <array>
-#include <fcntl.h>
-#include <sys/ioctl.h>
-#include <unistd.h>
-
-#include <linux/dma-buf.h>
-#include <linux/dma-heap.h>
-
-#include <libcamera/base/log.h>
-
-/**
- * \file dma_heaps.cpp
- * \brief dma-heap allocator
- */
-
-namespace libcamera {
-
-/*
- * /dev/dma_heap/linux,cma is the dma-heap allocator, which allows dmaheap-cma
- * to only have to worry about importing.
- *
- * Annoyingly, should the cma heap size be specified on the kernel command line
- * instead of DT, the heap gets named "reserved" instead.
- */
-
-#ifndef __DOXYGEN__
-struct DmaHeapInfo {
- DmaHeap::DmaHeapFlag type;
- const char *deviceNodeName;
-};
-#endif
-
-static constexpr std::array<DmaHeapInfo, 3> heapInfos = { {
- { DmaHeap::DmaHeapFlag::Cma, "/dev/dma_heap/linux,cma" },
- { DmaHeap::DmaHeapFlag::Cma, "/dev/dma_heap/reserved" },
- { DmaHeap::DmaHeapFlag::System, "/dev/dma_heap/system" },
-} };
-
-LOG_DEFINE_CATEGORY(DmaHeap)
-
-/**
- * \class DmaHeap
- * \brief Helper class for dma-heap allocations
- *
- * DMA heaps are kernel devices that provide an API to allocate memory from
- * different pools called "heaps", wrap each allocated piece of memory in a
- * dmabuf object, and return the dmabuf file descriptor to userspace. Multiple
- * heaps can be provided by the system, with different properties for the
- * underlying memory.
- *
- * This class wraps a DMA heap selected at construction time, and exposes
- * functions to manage memory allocation.
- */
-
-/**
- * \enum DmaHeap::DmaHeapFlag
- * \brief Type of the dma-heap
- * \var DmaHeap::Cma
- * \brief Allocate from a CMA dma-heap, providing physically-contiguous memory
- * \var DmaHeap::System
- * \brief Allocate from the system dma-heap, using the page allocator
- */
-
-/**
- * \typedef DmaHeap::DmaHeapFlags
- * \brief A bitwise combination of DmaHeap::DmaHeapFlag values
- */
-
-/**
- * \brief Construct a DmaHeap of a given type
- * \param[in] type The type(s) of the dma-heap(s) to allocate from
- *
- * The DMA heap type is selected with the \a type parameter, which defaults to
- * the CMA heap. If no heap of the given type can be accessed, the constructed
- * DmaHeap instance is invalid as indicated by the isValid() function.
- *
- * Multiple types can be selected by combining type flags, in which case the
- * constructed DmaHeap will match one of the types. If the system provides
- * multiple heaps that match the requested types, which heap is used is
- * undefined.
- */
-DmaHeap::DmaHeap(DmaHeapFlags type)
-{
- for (const auto &info : heapInfos) {
- if (!(type & info.type))
- continue;
-
- int ret = ::open(info.deviceNodeName, O_RDWR | O_CLOEXEC, 0);
- if (ret < 0) {
- ret = errno;
- LOG(DmaHeap, Debug)
- << "Failed to open " << info.deviceNodeName << ": "
- << strerror(ret);
- continue;
- }
-
- LOG(DmaHeap, Debug) << "Using " << info.deviceNodeName;
- dmaHeapHandle_ = UniqueFD(ret);
- break;
- }
-
- if (!dmaHeapHandle_.isValid())
- LOG(DmaHeap, Error) << "Could not open any dmaHeap device";
-}
-
-/**
- * \brief Destroy the DmaHeap instance
- */
-DmaHeap::~DmaHeap() = default;
-
-/**
- * \fn DmaHeap::isValid()
- * \brief Check if the DmaHeap instance is valid
- * \return True if the DmaHeap is valid, false otherwise
- */
-
-/**
- * \brief Allocate a dma-buf from the DmaHeap
- * \param [in] name The name to set for the allocated buffer
- * \param [in] size The size of the buffer to allocate
- *
- * Allocates a dma-buf with read/write access.
- *
- * If the allocation fails, return an invalid UniqueFD.
- *
- * \return The UniqueFD of the allocated buffer
- */
-UniqueFD DmaHeap::alloc(const char *name, std::size_t size)
-{
- int ret;
-
- if (!name)
- return {};
-
- struct dma_heap_allocation_data alloc = {};
-
- alloc.len = size;
- alloc.fd_flags = O_CLOEXEC | O_RDWR;
-
- ret = ::ioctl(dmaHeapHandle_.get(), DMA_HEAP_IOCTL_ALLOC, &alloc);
- if (ret < 0) {
- LOG(DmaHeap, Error) << "dmaHeap allocation failure for " << name;
- return {};
- }
-
- UniqueFD allocFd(alloc.fd);
- ret = ::ioctl(allocFd.get(), DMA_BUF_SET_NAME, name);
- if (ret < 0) {
- LOG(DmaHeap, Error) << "dmaHeap naming failure for " << name;
- return {};
- }
-
- return allocFd;
-}
-
-} /* namespace libcamera */
diff --git a/src/libcamera/meson.build b/src/libcamera/meson.build
index a3b12bc1..89504cee 100644
--- a/src/libcamera/meson.build
+++ b/src/libcamera/meson.build
@@ -15,7 +15,7 @@ libcamera_sources = files([
'delayed_controls.cpp',
'device_enumerator.cpp',
'device_enumerator_sysfs.cpp',
- 'dma_heaps.cpp',
+ 'dma_buf_allocator.cpp',
'fence.cpp',
'formats.cpp',
'framebuffer.cpp',
diff --git a/src/libcamera/pipeline/rpi/common/pipeline_base.cpp b/src/libcamera/pipeline/rpi/common/pipeline_base.cpp
index 289af516..3041fd1e 100644
--- a/src/libcamera/pipeline/rpi/common/pipeline_base.cpp
+++ b/src/libcamera/pipeline/rpi/common/pipeline_base.cpp
@@ -496,8 +496,6 @@ PipelineHandlerBase::generateConfiguration(Camera *camera, Span<const StreamRole
config->addConfiguration(cfg);
}
- config->validate();
-
return config;
}
diff --git a/src/libcamera/pipeline/rpi/vc4/vc4.cpp b/src/libcamera/pipeline/rpi/vc4/vc4.cpp
index 37fb310f..4a89e35f 100644
--- a/src/libcamera/pipeline/rpi/vc4/vc4.cpp
+++ b/src/libcamera/pipeline/rpi/vc4/vc4.cpp
@@ -12,7 +12,7 @@
#include <libcamera/formats.h>
#include "libcamera/internal/device_enumerator.h"
-#include "libcamera/internal/dma_heaps.h"
+#include "libcamera/internal/dma_buf_allocator.h"
#include "../common/pipeline_base.h"
#include "../common/rpi_stream.h"
@@ -86,7 +86,7 @@ public:
RPi::Device<Isp, 4> isp_;
/* DMAHEAP allocation helper. */
- DmaHeap dmaHeap_;
+ DmaBufAllocator dmaHeap_;
SharedFD lsTable_;
struct Config {
diff --git a/src/libcamera/pipeline/simple/simple.cpp b/src/libcamera/pipeline/simple/simple.cpp
index db3575c3..eb36578e 100644
--- a/src/libcamera/pipeline/simple/simple.cpp
+++ b/src/libcamera/pipeline/simple/simple.cpp
@@ -13,8 +13,8 @@
#include <memory>
#include <queue>
#include <set>
-#include <string>
#include <string.h>
+#include <string>
#include <unordered_map>
#include <utility>
#include <vector>
@@ -38,7 +38,6 @@
#include "libcamera/internal/v4l2_subdevice.h"
#include "libcamera/internal/v4l2_videodevice.h"
-
namespace libcamera {
LOG_DEFINE_CATEGORY(SimplePipeline)
diff --git a/src/libcamera/pipeline/vimc/vimc.cpp b/src/libcamera/pipeline/vimc/vimc.cpp
index c7650432..0ec9928e 100644
--- a/src/libcamera/pipeline/vimc/vimc.cpp
+++ b/src/libcamera/pipeline/vimc/vimc.cpp
@@ -114,6 +114,9 @@ static const std::map<PixelFormat, uint32_t> pixelformats{
{ formats::BGR888, MEDIA_BUS_FMT_RGB888_1X24 },
};
+static constexpr Size kMinSize{ 16, 16 };
+static constexpr Size kMaxSize{ 4096, 2160 };
+
} /* namespace */
VimcCameraConfiguration::VimcCameraConfiguration(VimcCameraData *data)
@@ -153,14 +156,20 @@ CameraConfiguration::Status VimcCameraConfiguration::validate()
const Size size = cfg.size;
/*
- * The scaler hardcodes a x3 scale-up ratio, and the sensor output size
- * is aligned to two pixels in both directions. The output width and
- * height thus have to be multiples of 6.
+ * The sensor output size is aligned to two pixels in both directions.
+ * Additionally, prior to v5.16, the scaler hardcodes a x3 scale-up
+ * ratio, requiring the output width and height to be multiples of 6.
*/
- cfg.size.width = std::max(48U, std::min(4096U, cfg.size.width));
- cfg.size.height = std::max(48U, std::min(2160U, cfg.size.height));
- cfg.size.width -= cfg.size.width % 6;
- cfg.size.height -= cfg.size.height % 6;
+ Size minSize{ kMinSize };
+ unsigned int alignment = 2;
+
+ if (data_->media_->version() < KERNEL_VERSION(5, 16, 0)) {
+ minSize *= 3;
+ alignment *= 3;
+ }
+
+ cfg.size.expandTo(minSize).boundTo(kMaxSize)
+ .alignDownTo(alignment, alignment);
if (cfg.size != size) {
LOG(VIMC, Debug)
@@ -216,10 +225,12 @@ PipelineHandlerVimc::generateConfiguration(Camera *camera,
}
}
- /* The scaler hardcodes a x3 scale-up ratio. */
- std::vector<SizeRange> sizes{
- SizeRange{ { 48, 48 }, { 4096, 2160 } }
- };
+ /* Prior to v5.16, the scaler hardcodes a x3 scale-up ratio. */
+ Size minSize{ kMinSize };
+ if (data->media_->version() < KERNEL_VERSION(5, 16, 0))
+ minSize *= 3;
+
+ std::vector<SizeRange> sizes{ { minSize, kMaxSize } };
formats[pixelformat.first] = sizes;
}
@@ -242,10 +253,18 @@ int PipelineHandlerVimc::configure(Camera *camera, CameraConfiguration *config)
StreamConfiguration &cfg = config->at(0);
int ret;
- /* The scaler hardcodes a x3 scale-up ratio. */
+ /*
+ * Prior to v5.16, the scaler hardcodes a x3 scale-up ratio. For newer
+ * kernels, use a sensor resolution of 1920x1080 and let the scaler
+ * produce the requested stream size.
+ */
+ Size sensorSize{ 1920, 1080 };
+ if (data->media_->version() < KERNEL_VERSION(5, 16, 0))
+ sensorSize = { cfg.size.width / 3, cfg.size.height / 3 };
+
V4L2SubdeviceFormat subformat = {};
subformat.code = MEDIA_BUS_FMT_SGRBG8_1X8;
- subformat.size = { cfg.size.width / 3, cfg.size.height / 3 };
+ subformat.size = sensorSize;
ret = data->sensor_->setFormat(&subformat);
if (ret)
@@ -293,7 +312,7 @@ int PipelineHandlerVimc::configure(Camera *camera, CameraConfiguration *config)
* vimc driver will fail pipeline validation.
*/
format.fourcc = V4L2PixelFormat(V4L2_PIX_FMT_SGRBG8);
- format.size = { cfg.size.width / 3, cfg.size.height / 3 };
+ format.size = sensorSize;
ret = data->raw_->setFormat(&format);
if (ret)
diff --git a/src/libcamera/software_isp/TODO b/src/libcamera/software_isp/TODO
index 4fcee39b..6bdc5905 100644
--- a/src/libcamera/software_isp/TODO
+++ b/src/libcamera/software_isp/TODO
@@ -72,19 +72,6 @@ stats in hardware, such as the i.MX7), but please keep it on your radar.
---
-4. Hide internal representation of gains from callers
-
-> struct DebayerParams {
-> static constexpr unsigned int kGain10 = 256;
-
-Forcing the caller to deal with the internal representation of gains
-isn't nice, especially given that it precludes implementing gains of
-different precisions in different backend. Wouldn't it be better to pass
-the values as floating point numbers, and convert them to the internal
-representation in the implementation of process() before using them ?
-
----
-
5. Store ISP parameters in per-frame buffers
> /**
diff --git a/src/libcamera/software_isp/debayer.cpp b/src/libcamera/software_isp/debayer.cpp
index efe75ea8..f4a299d5 100644
--- a/src/libcamera/software_isp/debayer.cpp
+++ b/src/libcamera/software_isp/debayer.cpp
@@ -1,7 +1,7 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
/*
* Copyright (C) 2023, Linaro Ltd
- * Copyright (C) 2023, Red Hat Inc.
+ * Copyright (C) 2023, 2024 Red Hat Inc.
*
* Authors:
* Hans de Goede <hdegoede@redhat.com>
@@ -19,34 +19,28 @@ namespace libcamera {
*/
/**
- * \var DebayerParams::kGain10
- * \brief const value for 1.0 gain
+ * \var DebayerParams::kRGBLookupSize
+ * \brief Size of a color lookup table
*/
/**
- * \var DebayerParams::gainR
- * \brief Red gain
- *
- * 128 = 0.5, 256 = 1.0, 512 = 2.0, etc.
+ * \typedef DebayerParams::ColorLookupTable
+ * \brief Type of the lookup tables for red, green, blue values
*/
/**
- * \var DebayerParams::gainG
- * \brief Green gain
- *
- * 128 = 0.5, 256 = 1.0, 512 = 2.0, etc.
+ * \var DebayerParams::red
+ * \brief Lookup table for red color, mapping input values to output values
*/
/**
- * \var DebayerParams::gainB
- * \brief Blue gain
- *
- * 128 = 0.5, 256 = 1.0, 512 = 2.0, etc.
+ * \var DebayerParams::green
+ * \brief Lookup table for green color, mapping input values to output values
*/
/**
- * \var DebayerParams::gamma
- * \brief Gamma correction, 1.0 is no correction
+ * \var DebayerParams::blue
+ * \brief Lookup table for blue color, mapping input values to output values
*/
/**
diff --git a/src/libcamera/software_isp/debayer_cpu.cpp b/src/libcamera/software_isp/debayer_cpu.cpp
index 8254bbe9..f8d2677d 100644
--- a/src/libcamera/software_isp/debayer_cpu.cpp
+++ b/src/libcamera/software_isp/debayer_cpu.cpp
@@ -11,7 +11,6 @@
#include "debayer_cpu.h"
-#include <math.h>
#include <stdlib.h>
#include <time.h>
@@ -35,7 +34,7 @@ namespace libcamera {
* \param[in] stats Pointer to the stats object to use
*/
DebayerCpu::DebayerCpu(std::unique_ptr<SwStatsCpu> stats)
- : stats_(std::move(stats)), gammaCorrection_(1.0), blackLevel_(0)
+ : stats_(std::move(stats))
{
/*
* Reading from uncached buffers may be very slow.
@@ -47,9 +46,9 @@ DebayerCpu::DebayerCpu(std::unique_ptr<SwStatsCpu> stats)
*/
enableInputMemcpy_ = true;
- /* Initialize gamma to 1.0 curve */
- for (unsigned int i = 0; i < kGammaLookupSize; i++)
- gamma_[i] = i / (kGammaLookupSize / kRGBLookupSize);
+ /* Initialize color lookup tables */
+ for (unsigned int i = 0; i < DebayerParams::kRGBLookupSize; i++)
+ red_[i] = green_[i] = blue_[i] = i;
for (unsigned int i = 0; i < kMaxLineBuffers; i++)
lineBuffers_[i] = nullptr;
@@ -75,6 +74,8 @@ DebayerCpu::~DebayerCpu()
*dst++ = blue_[curr[x] / (div)]; \
*dst++ = green_[(prev[x] + curr[x - p] + curr[x + n] + next[x]) / (4 * (div))]; \
*dst++ = red_[(prev[x - p] + prev[x + n] + next[x - p] + next[x + n]) / (4 * (div))]; \
+ if constexpr (addAlphaByte) \
+ *dst++ = 255; \
x++;
/*
@@ -86,6 +87,8 @@ DebayerCpu::~DebayerCpu()
*dst++ = blue_[(prev[x] + next[x]) / (2 * (div))]; \
*dst++ = green_[curr[x] / (div)]; \
*dst++ = red_[(curr[x - p] + curr[x + n]) / (2 * (div))]; \
+ if constexpr (addAlphaByte) \
+ *dst++ = 255; \
x++;
/*
@@ -97,6 +100,8 @@ DebayerCpu::~DebayerCpu()
*dst++ = blue_[(curr[x - p] + curr[x + n]) / (2 * (div))]; \
*dst++ = green_[curr[x] / (div)]; \
*dst++ = red_[(prev[x] + next[x]) / (2 * (div))]; \
+ if constexpr (addAlphaByte) \
+ *dst++ = 255; \
x++;
/*
@@ -108,8 +113,11 @@ DebayerCpu::~DebayerCpu()
*dst++ = blue_[(prev[x - p] + prev[x + n] + next[x - p] + next[x + n]) / (4 * (div))]; \
*dst++ = green_[(prev[x] + curr[x - p] + curr[x + n] + next[x]) / (4 * (div))]; \
*dst++ = red_[curr[x] / (div)]; \
+ if constexpr (addAlphaByte) \
+ *dst++ = 255; \
x++;
+template<bool addAlphaByte>
void DebayerCpu::debayer8_BGBG_BGR888(uint8_t *dst, const uint8_t *src[])
{
DECLARE_SRC_POINTERS(uint8_t)
@@ -120,6 +128,7 @@ void DebayerCpu::debayer8_BGBG_BGR888(uint8_t *dst, const uint8_t *src[])
}
}
+template<bool addAlphaByte>
void DebayerCpu::debayer8_GRGR_BGR888(uint8_t *dst, const uint8_t *src[])
{
DECLARE_SRC_POINTERS(uint8_t)
@@ -130,6 +139,7 @@ void DebayerCpu::debayer8_GRGR_BGR888(uint8_t *dst, const uint8_t *src[])
}
}
+template<bool addAlphaByte>
void DebayerCpu::debayer10_BGBG_BGR888(uint8_t *dst, const uint8_t *src[])
{
DECLARE_SRC_POINTERS(uint16_t)
@@ -141,6 +151,7 @@ void DebayerCpu::debayer10_BGBG_BGR888(uint8_t *dst, const uint8_t *src[])
}
}
+template<bool addAlphaByte>
void DebayerCpu::debayer10_GRGR_BGR888(uint8_t *dst, const uint8_t *src[])
{
DECLARE_SRC_POINTERS(uint16_t)
@@ -152,6 +163,7 @@ void DebayerCpu::debayer10_GRGR_BGR888(uint8_t *dst, const uint8_t *src[])
}
}
+template<bool addAlphaByte>
void DebayerCpu::debayer12_BGBG_BGR888(uint8_t *dst, const uint8_t *src[])
{
DECLARE_SRC_POINTERS(uint16_t)
@@ -163,6 +175,7 @@ void DebayerCpu::debayer12_BGBG_BGR888(uint8_t *dst, const uint8_t *src[])
}
}
+template<bool addAlphaByte>
void DebayerCpu::debayer12_GRGR_BGR888(uint8_t *dst, const uint8_t *src[])
{
DECLARE_SRC_POINTERS(uint16_t)
@@ -174,6 +187,7 @@ void DebayerCpu::debayer12_GRGR_BGR888(uint8_t *dst, const uint8_t *src[])
}
}
+template<bool addAlphaByte>
void DebayerCpu::debayer10P_BGBG_BGR888(uint8_t *dst, const uint8_t *src[])
{
const int widthInBytes = window_.width * 5 / 4;
@@ -199,6 +213,7 @@ void DebayerCpu::debayer10P_BGBG_BGR888(uint8_t *dst, const uint8_t *src[])
}
}
+template<bool addAlphaByte>
void DebayerCpu::debayer10P_GRGR_BGR888(uint8_t *dst, const uint8_t *src[])
{
const int widthInBytes = window_.width * 5 / 4;
@@ -219,6 +234,7 @@ void DebayerCpu::debayer10P_GRGR_BGR888(uint8_t *dst, const uint8_t *src[])
}
}
+template<bool addAlphaByte>
void DebayerCpu::debayer10P_GBGB_BGR888(uint8_t *dst, const uint8_t *src[])
{
const int widthInBytes = window_.width * 5 / 4;
@@ -239,6 +255,7 @@ void DebayerCpu::debayer10P_GBGB_BGR888(uint8_t *dst, const uint8_t *src[])
}
}
+template<bool addAlphaByte>
void DebayerCpu::debayer10P_RGRG_BGR888(uint8_t *dst, const uint8_t *src[])
{
const int widthInBytes = window_.width * 5 / 4;
@@ -281,7 +298,12 @@ int DebayerCpu::getInputConfig(PixelFormat inputFormat, DebayerInputConfig &conf
config.bpp = (bayerFormat.bitDepth + 7) & ~7;
config.patternSize.width = 2;
config.patternSize.height = 2;
- config.outputFormats = std::vector<PixelFormat>({ formats::RGB888, formats::BGR888 });
+ config.outputFormats = std::vector<PixelFormat>({ formats::RGB888,
+ formats::XRGB8888,
+ formats::ARGB8888,
+ formats::BGR888,
+ formats::XBGR8888,
+ formats::ABGR8888 });
return 0;
}
@@ -291,7 +313,12 @@ int DebayerCpu::getInputConfig(PixelFormat inputFormat, DebayerInputConfig &conf
config.bpp = 10;
config.patternSize.width = 4; /* 5 bytes per *4* pixels */
config.patternSize.height = 2;
- config.outputFormats = std::vector<PixelFormat>({ formats::RGB888, formats::BGR888 });
+ config.outputFormats = std::vector<PixelFormat>({ formats::RGB888,
+ formats::XRGB8888,
+ formats::ARGB8888,
+ formats::BGR888,
+ formats::XBGR8888,
+ formats::ABGR8888 });
return 0;
}
@@ -307,6 +334,12 @@ int DebayerCpu::getOutputConfig(PixelFormat outputFormat, DebayerOutputConfig &c
return 0;
}
+ if (outputFormat == formats::XRGB8888 || outputFormat == formats::ARGB8888 ||
+ outputFormat == formats::XBGR8888 || outputFormat == formats::ABGR8888) {
+ config.bpp = 32;
+ return 0;
+ }
+
LOG(Debayer, Info)
<< "Unsupported output format " << outputFormat.toString();
return -EINVAL;
@@ -342,6 +375,7 @@ int DebayerCpu::setDebayerFunctions(PixelFormat inputFormat, PixelFormat outputF
{
BayerFormat bayerFormat =
BayerFormat::fromPixelFormat(inputFormat);
+ bool addAlphaByte = false;
xShift_ = 0;
swapRedBlueGains_ = false;
@@ -352,8 +386,16 @@ int DebayerCpu::setDebayerFunctions(PixelFormat inputFormat, PixelFormat outputF
};
switch (outputFormat) {
+ case formats::XRGB8888:
+ case formats::ARGB8888:
+ addAlphaByte = true;
+ [[fallthrough]];
case formats::RGB888:
break;
+ case formats::XBGR8888:
+ case formats::ABGR8888:
+ addAlphaByte = true;
+ [[fallthrough]];
case formats::BGR888:
/* Swap R and B in bayer order to generate BGR888 instead of RGB888 */
swapRedBlueGains_ = true;
@@ -384,16 +426,16 @@ int DebayerCpu::setDebayerFunctions(PixelFormat inputFormat, PixelFormat outputF
isStandardBayerOrder(bayerFormat.order)) {
switch (bayerFormat.bitDepth) {
case 8:
- debayer0_ = &DebayerCpu::debayer8_BGBG_BGR888;
- debayer1_ = &DebayerCpu::debayer8_GRGR_BGR888;
+ debayer0_ = addAlphaByte ? &DebayerCpu::debayer8_BGBG_BGR888<true> : &DebayerCpu::debayer8_BGBG_BGR888<false>;
+ debayer1_ = addAlphaByte ? &DebayerCpu::debayer8_GRGR_BGR888<true> : &DebayerCpu::debayer8_GRGR_BGR888<false>;
break;
case 10:
- debayer0_ = &DebayerCpu::debayer10_BGBG_BGR888;
- debayer1_ = &DebayerCpu::debayer10_GRGR_BGR888;
+ debayer0_ = addAlphaByte ? &DebayerCpu::debayer10_BGBG_BGR888<true> : &DebayerCpu::debayer10_BGBG_BGR888<false>;
+ debayer1_ = addAlphaByte ? &DebayerCpu::debayer10_GRGR_BGR888<true> : &DebayerCpu::debayer10_GRGR_BGR888<false>;
break;
case 12:
- debayer0_ = &DebayerCpu::debayer12_BGBG_BGR888;
- debayer1_ = &DebayerCpu::debayer12_GRGR_BGR888;
+ debayer0_ = addAlphaByte ? &DebayerCpu::debayer12_BGBG_BGR888<true> : &DebayerCpu::debayer12_BGBG_BGR888<false>;
+ debayer1_ = addAlphaByte ? &DebayerCpu::debayer12_GRGR_BGR888<true> : &DebayerCpu::debayer12_GRGR_BGR888<false>;
break;
}
setupStandardBayerOrder(bayerFormat.order);
@@ -404,20 +446,20 @@ int DebayerCpu::setDebayerFunctions(PixelFormat inputFormat, PixelFormat outputF
bayerFormat.packing == BayerFormat::Packing::CSI2) {
switch (bayerFormat.order) {
case BayerFormat::BGGR:
- debayer0_ = &DebayerCpu::debayer10P_BGBG_BGR888;
- debayer1_ = &DebayerCpu::debayer10P_GRGR_BGR888;
+ debayer0_ = addAlphaByte ? &DebayerCpu::debayer10P_BGBG_BGR888<true> : &DebayerCpu::debayer10P_BGBG_BGR888<false>;
+ debayer1_ = addAlphaByte ? &DebayerCpu::debayer10P_GRGR_BGR888<true> : &DebayerCpu::debayer10P_GRGR_BGR888<false>;
return 0;
case BayerFormat::GBRG:
- debayer0_ = &DebayerCpu::debayer10P_GBGB_BGR888;
- debayer1_ = &DebayerCpu::debayer10P_RGRG_BGR888;
+ debayer0_ = addAlphaByte ? &DebayerCpu::debayer10P_GBGB_BGR888<true> : &DebayerCpu::debayer10P_GBGB_BGR888<false>;
+ debayer1_ = addAlphaByte ? &DebayerCpu::debayer10P_RGRG_BGR888<true> : &DebayerCpu::debayer10P_RGRG_BGR888<false>;
return 0;
case BayerFormat::GRBG:
- debayer0_ = &DebayerCpu::debayer10P_GRGR_BGR888;
- debayer1_ = &DebayerCpu::debayer10P_BGBG_BGR888;
+ debayer0_ = addAlphaByte ? &DebayerCpu::debayer10P_GRGR_BGR888<true> : &DebayerCpu::debayer10P_GRGR_BGR888<false>;
+ debayer1_ = addAlphaByte ? &DebayerCpu::debayer10P_BGBG_BGR888<true> : &DebayerCpu::debayer10P_BGBG_BGR888<false>;
return 0;
case BayerFormat::RGGB:
- debayer0_ = &DebayerCpu::debayer10P_RGRG_BGR888;
- debayer1_ = &DebayerCpu::debayer10P_GBGB_BGR888;
+ debayer0_ = addAlphaByte ? &DebayerCpu::debayer10P_RGRG_BGR888<true> : &DebayerCpu::debayer10P_RGRG_BGR888<false>;
+ debayer1_ = addAlphaByte ? &DebayerCpu::debayer10P_GBGB_BGR888<true> : &DebayerCpu::debayer10P_GBGB_BGR888<false>;
return 0;
default:
break;
@@ -698,37 +740,9 @@ void DebayerCpu::process(FrameBuffer *input, FrameBuffer *output, DebayerParams
clock_gettime(CLOCK_MONOTONIC_RAW, &frameStartTime);
}
- /* Apply DebayerParams */
- if (params.gamma != gammaCorrection_ || params.blackLevel != blackLevel_) {
- const unsigned int blackIndex =
- params.blackLevel * kGammaLookupSize / 256;
- std::fill(gamma_.begin(), gamma_.begin() + blackIndex, 0);
- const float divisor = kGammaLookupSize - blackIndex - 1.0;
- for (unsigned int i = blackIndex; i < kGammaLookupSize; i++)
- gamma_[i] = UINT8_MAX * powf((i - blackIndex) / divisor, params.gamma);
-
- gammaCorrection_ = params.gamma;
- blackLevel_ = params.blackLevel;
- }
-
- if (swapRedBlueGains_)
- std::swap(params.gainR, params.gainB);
-
- for (unsigned int i = 0; i < kRGBLookupSize; i++) {
- constexpr unsigned int div =
- kRGBLookupSize * DebayerParams::kGain10 / kGammaLookupSize;
- unsigned int idx;
-
- /* Apply gamma after gain! */
- idx = std::min({ i * params.gainR / div, (kGammaLookupSize - 1) });
- red_[i] = gamma_[idx];
-
- idx = std::min({ i * params.gainG / div, (kGammaLookupSize - 1) });
- green_[i] = gamma_[idx];
-
- idx = std::min({ i * params.gainB / div, (kGammaLookupSize - 1) });
- blue_[i] = gamma_[idx];
- }
+ green_ = params.green;
+ red_ = swapRedBlueGains_ ? params.blue : params.red;
+ blue_ = swapRedBlueGains_ ? params.red : params.blue;
/* Copy metadata from the input buffer */
FrameMetadata &metadata = output->_d()->metadata();
diff --git a/src/libcamera/software_isp/debayer_cpu.h b/src/libcamera/software_isp/debayer_cpu.h
index de216fe3..1dac6435 100644
--- a/src/libcamera/software_isp/debayer_cpu.h
+++ b/src/libcamera/software_isp/debayer_cpu.h
@@ -85,18 +85,28 @@ private:
using debayerFn = void (DebayerCpu::*)(uint8_t *dst, const uint8_t *src[]);
/* 8-bit raw bayer format */
+ template<bool addAlphaByte>
void debayer8_BGBG_BGR888(uint8_t *dst, const uint8_t *src[]);
+ template<bool addAlphaByte>
void debayer8_GRGR_BGR888(uint8_t *dst, const uint8_t *src[]);
/* unpacked 10-bit raw bayer format */
+ template<bool addAlphaByte>
void debayer10_BGBG_BGR888(uint8_t *dst, const uint8_t *src[]);
+ template<bool addAlphaByte>
void debayer10_GRGR_BGR888(uint8_t *dst, const uint8_t *src[]);
/* unpacked 12-bit raw bayer format */
+ template<bool addAlphaByte>
void debayer12_BGBG_BGR888(uint8_t *dst, const uint8_t *src[]);
+ template<bool addAlphaByte>
void debayer12_GRGR_BGR888(uint8_t *dst, const uint8_t *src[]);
/* CSI-2 packed 10-bit raw bayer format (all the 4 orders) */
+ template<bool addAlphaByte>
void debayer10P_BGBG_BGR888(uint8_t *dst, const uint8_t *src[]);
+ template<bool addAlphaByte>
void debayer10P_GRGR_BGR888(uint8_t *dst, const uint8_t *src[]);
+ template<bool addAlphaByte>
void debayer10P_GBGB_BGR888(uint8_t *dst, const uint8_t *src[]);
+ template<bool addAlphaByte>
void debayer10P_RGRG_BGR888(uint8_t *dst, const uint8_t *src[]);
struct DebayerInputConfig {
@@ -122,15 +132,12 @@ private:
void process2(const uint8_t *src, uint8_t *dst);
void process4(const uint8_t *src, uint8_t *dst);
- static constexpr unsigned int kGammaLookupSize = 1024;
- static constexpr unsigned int kRGBLookupSize = 256;
/* Max. supported Bayer pattern height is 4, debayering this requires 5 lines */
static constexpr unsigned int kMaxLineBuffers = 5;
- std::array<uint8_t, kGammaLookupSize> gamma_;
- std::array<uint8_t, kRGBLookupSize> red_;
- std::array<uint8_t, kRGBLookupSize> green_;
- std::array<uint8_t, kRGBLookupSize> blue_;
+ DebayerParams::ColorLookupTable red_;
+ DebayerParams::ColorLookupTable green_;
+ DebayerParams::ColorLookupTable blue_;
debayerFn debayer0_;
debayerFn debayer1_;
debayerFn debayer2_;
@@ -146,8 +153,6 @@ private:
unsigned int xShift_; /* Offset of 0/1 applied to window_.x */
bool enableInputMemcpy_;
bool swapRedBlueGains_;
- float gammaCorrection_;
- unsigned int blackLevel_;
unsigned int measuredFrames_;
int64_t frameProcessTime_;
/* Skip 30 frames for things to stabilize then measure 30 frames */
diff --git a/src/libcamera/software_isp/software_isp.cpp b/src/libcamera/software_isp/software_isp.cpp
index c9b6be56..20fb6f48 100644
--- a/src/libcamera/software_isp/software_isp.cpp
+++ b/src/libcamera/software_isp/software_isp.cpp
@@ -7,6 +7,8 @@
#include "libcamera/internal/software_isp/software_isp.h"
+#include <cmath>
+#include <stdint.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <unistd.h>
@@ -18,6 +20,7 @@
#include "libcamera/internal/framebuffer.h"
#include "libcamera/internal/ipa_manager.h"
#include "libcamera/internal/mapped_framebuffer.h"
+#include "libcamera/internal/software_isp/debayer_params.h"
#include "debayer_cpu.h"
@@ -63,12 +66,29 @@ LOG_DEFINE_CATEGORY(SoftwareIsp)
* handler
*/
SoftwareIsp::SoftwareIsp(PipelineHandler *pipe, const CameraSensor *sensor)
- : debayerParams_{ DebayerParams::kGain10, DebayerParams::kGain10,
- DebayerParams::kGain10, 0.5f, 0 },
- dmaHeap_(DmaHeap::DmaHeapFlag::Cma | DmaHeap::DmaHeapFlag::System)
+ : dmaHeap_(DmaBufAllocator::DmaBufAllocatorFlag::CmaHeap |
+ DmaBufAllocator::DmaBufAllocatorFlag::SystemHeap |
+ DmaBufAllocator::DmaBufAllocatorFlag::UDmaBuf)
{
+ /*
+ * debayerParams_ must be initialized because the initial value is used for
+ * the first two frames, i.e. until stats processing starts providing its
+ * own parameters.
+ *
+ * \todo This should be handled in the same place as the related
+ * operations, in the IPA module.
+ */
+ std::array<uint8_t, 256> gammaTable;
+ for (unsigned int i = 0; i < 256; i++)
+ gammaTable[i] = UINT8_MAX * std::pow(i / 256.0, 0.5);
+ for (unsigned int i = 0; i < DebayerParams::kRGBLookupSize; i++) {
+ debayerParams_.red[i] = gammaTable[i];
+ debayerParams_.green[i] = gammaTable[i];
+ debayerParams_.blue[i] = gammaTable[i];
+ }
+
if (!dmaHeap_.isValid()) {
- LOG(SoftwareIsp, Error) << "Failed to create DmaHeap object";
+ LOG(SoftwareIsp, Error) << "Failed to create DmaBufAllocator object";
return;
}
diff --git a/src/libcamera/v4l2_subdevice.cpp b/src/libcamera/v4l2_subdevice.cpp
index 6da77775..82824433 100644
--- a/src/libcamera/v4l2_subdevice.cpp
+++ b/src/libcamera/v4l2_subdevice.cpp
@@ -1366,8 +1366,62 @@ void routeToKernel(const V4L2Subdevice::Route &route,
kroute.flags = route.flags;
}
+/*
+ * Legacy routing support for pre-v6.10-rc1 kernels. Drop when v6.12-rc1 gets
+ * released.
+ */
+struct v4l2_subdev_routing_legacy {
+ __u32 which;
+ __u32 num_routes;
+ __u64 routes;
+ __u32 reserved[6];
+};
+
+#define VIDIOC_SUBDEV_G_ROUTING_LEGACY _IOWR('V', 38, struct v4l2_subdev_routing_legacy)
+#define VIDIOC_SUBDEV_S_ROUTING_LEGACY _IOWR('V', 39, struct v4l2_subdev_routing_legacy)
+
} /* namespace */
+int V4L2Subdevice::getRoutingLegacy(Routing *routing, Whence whence)
+{
+ struct v4l2_subdev_routing_legacy rt = {};
+
+ rt.which = whence;
+
+ int ret = ioctl(VIDIOC_SUBDEV_G_ROUTING_LEGACY, &rt);
+ if (ret == 0 || ret == -ENOTTY)
+ return ret;
+
+ if (ret != -ENOSPC) {
+ LOG(V4L2, Error)
+ << "Failed to retrieve number of routes: "
+ << strerror(-ret);
+ return ret;
+ }
+
+ std::vector<struct v4l2_subdev_route> routes{ rt.num_routes };
+ rt.routes = reinterpret_cast<uintptr_t>(routes.data());
+
+ ret = ioctl(VIDIOC_SUBDEV_G_ROUTING_LEGACY, &rt);
+ if (ret) {
+ LOG(V4L2, Error)
+ << "Failed to retrieve routes: " << strerror(-ret);
+ return ret;
+ }
+
+ if (rt.num_routes != routes.size()) {
+ LOG(V4L2, Error) << "Invalid number of routes";
+ return -EINVAL;
+ }
+
+ routing->resize(rt.num_routes);
+
+ for (const auto &[i, route] : utils::enumerate(routes))
+ routeFromKernel((*routing)[i], route);
+
+ return 0;
+}
+
/**
* \brief Retrieve the subdevice's internal routing table
* \param[out] routing The routing table
@@ -1388,19 +1442,25 @@ int V4L2Subdevice::getRouting(Routing *routing, Whence whence)
rt.which = whence;
int ret = ioctl(VIDIOC_SUBDEV_G_ROUTING, &rt);
- if (ret == 0 || ret == -ENOTTY)
- return ret;
+ if (ret == -ENOTTY)
+ return V4L2Subdevice::getRoutingLegacy(routing, whence);
- if (ret != -ENOSPC) {
+ if (ret) {
LOG(V4L2, Error)
<< "Failed to retrieve number of routes: "
<< strerror(-ret);
return ret;
}
+ if (!rt.num_routes)
+ return 0;
+
std::vector<struct v4l2_subdev_route> routes{ rt.num_routes };
rt.routes = reinterpret_cast<uintptr_t>(routes.data());
+ rt.len_routes = rt.num_routes;
+ rt.num_routes = 0;
+
ret = ioctl(VIDIOC_SUBDEV_G_ROUTING, &rt);
if (ret) {
LOG(V4L2, Error)
@@ -1421,6 +1481,33 @@ int V4L2Subdevice::getRouting(Routing *routing, Whence whence)
return 0;
}
+int V4L2Subdevice::setRoutingLegacy(Routing *routing, Whence whence)
+{
+ std::vector<struct v4l2_subdev_route> routes{ routing->size() };
+
+ for (const auto &[i, route] : utils::enumerate(*routing))
+ routeToKernel(route, routes[i]);
+
+ struct v4l2_subdev_routing_legacy rt = {};
+ rt.which = whence;
+ rt.num_routes = routes.size();
+ rt.routes = reinterpret_cast<uintptr_t>(routes.data());
+
+ int ret = ioctl(VIDIOC_SUBDEV_S_ROUTING_LEGACY, &rt);
+ if (ret) {
+ LOG(V4L2, Error) << "Failed to set routes: " << strerror(-ret);
+ return ret;
+ }
+
+ routes.resize(rt.num_routes);
+ routing->resize(rt.num_routes);
+
+ for (const auto &[i, route] : utils::enumerate(routes))
+ routeFromKernel((*routing)[i], route);
+
+ return 0;
+}
+
/**
* \brief Set a routing table on the V4L2 subdevice
* \param[inout] routing The routing table
@@ -1447,16 +1534,43 @@ int V4L2Subdevice::setRouting(Routing *routing, Whence whence)
struct v4l2_subdev_routing rt = {};
rt.which = whence;
+ rt.len_routes = routes.size();
rt.num_routes = routes.size();
rt.routes = reinterpret_cast<uintptr_t>(routes.data());
int ret = ioctl(VIDIOC_SUBDEV_S_ROUTING, &rt);
+ if (ret == -ENOTTY)
+ return setRoutingLegacy(routing, whence);
+
if (ret) {
LOG(V4L2, Error) << "Failed to set routes: " << strerror(-ret);
return ret;
}
- routes.resize(rt.num_routes);
+ /*
+ * The kernel may want to return more routes than we have space for. In
+ * that event, we must issue a VIDIOC_SUBDEV_G_ROUTING call to retrieve
+ * the additional routes.
+ */
+ if (rt.num_routes > routes.size()) {
+ routes.resize(rt.num_routes);
+
+ rt.len_routes = rt.num_routes;
+ rt.num_routes = 0;
+
+ ret = ioctl(VIDIOC_SUBDEV_G_ROUTING, &rt);
+ if (ret) {
+ LOG(V4L2, Error)
+ << "Failed to retrieve routes: " << strerror(-ret);
+ return ret;
+ }
+ }
+
+ if (rt.num_routes != routes.size()) {
+ LOG(V4L2, Error) << "Invalid number of routes";
+ return -EINVAL;
+ }
+
routing->resize(rt.num_routes);
for (const auto &[i, route] : utils::enumerate(routes))
diff --git a/src/libcamera/yaml_parser.cpp b/src/libcamera/yaml_parser.cpp
index 55f81916..025006bc 100644
--- a/src/libcamera/yaml_parser.cpp
+++ b/src/libcamera/yaml_parser.cpp
@@ -104,7 +104,7 @@ std::size_t YamlObject::size() const
*/
/**
- * \fn template<typename T> YamlObject::get<T>(const T &defaultValue) const
+ * \fn template<typename T, typename U> YamlObject::get<T>(U &&defaultValue) const
* \brief Parse the YamlObject as a \a T value
* \param[in] defaultValue The default value when failing to parse
*
@@ -118,14 +118,15 @@ std::size_t YamlObject::size() const
#ifndef __DOXYGEN__
template<>
-std::optional<bool> YamlObject::get() const
+std::optional<bool>
+YamlObject::Getter<bool>::get(const YamlObject &obj) const
{
- if (type_ != Type::Value)
+ if (obj.type_ != Type::Value)
return std::nullopt;
- if (value_ == "true")
+ if (obj.value_ == "true")
return true;
- else if (value_ == "false")
+ else if (obj.value_ == "false")
return false;
return std::nullopt;
@@ -182,14 +183,15 @@ bool parseUnsignedInteger(const std::string &str, unsigned long max,
} /* namespace */
template<>
-std::optional<int8_t> YamlObject::get() const
+std::optional<int8_t>
+YamlObject::Getter<int8_t>::get(const YamlObject &obj) const
{
- if (type_ != Type::Value)
+ if (obj.type_ != Type::Value)
return std::nullopt;
long value;
- if (!parseSignedInteger(value_, std::numeric_limits<int8_t>::min(),
+ if (!parseSignedInteger(obj.value_, std::numeric_limits<int8_t>::min(),
std::numeric_limits<int8_t>::max(), &value))
return std::nullopt;
@@ -197,14 +199,15 @@ std::optional<int8_t> YamlObject::get() const
}
template<>
-std::optional<uint8_t> YamlObject::get() const
+std::optional<uint8_t>
+YamlObject::Getter<uint8_t>::get(const YamlObject &obj) const
{
- if (type_ != Type::Value)
+ if (obj.type_ != Type::Value)
return std::nullopt;
unsigned long value;
- if (!parseUnsignedInteger(value_, std::numeric_limits<uint8_t>::max(),
+ if (!parseUnsignedInteger(obj.value_, std::numeric_limits<uint8_t>::max(),
&value))
return std::nullopt;
@@ -212,14 +215,15 @@ std::optional<uint8_t> YamlObject::get() const
}
template<>
-std::optional<int16_t> YamlObject::get() const
+std::optional<int16_t>
+YamlObject::Getter<int16_t>::get(const YamlObject &obj) const
{
- if (type_ != Type::Value)
+ if (obj.type_ != Type::Value)
return std::nullopt;
long value;
- if (!parseSignedInteger(value_, std::numeric_limits<int16_t>::min(),
+ if (!parseSignedInteger(obj.value_, std::numeric_limits<int16_t>::min(),
std::numeric_limits<int16_t>::max(), &value))
return std::nullopt;
@@ -227,14 +231,15 @@ std::optional<int16_t> YamlObject::get() const
}
template<>
-std::optional<uint16_t> YamlObject::get() const
+std::optional<uint16_t>
+YamlObject::Getter<uint16_t>::get(const YamlObject &obj) const
{
- if (type_ != Type::Value)
+ if (obj.type_ != Type::Value)
return std::nullopt;
unsigned long value;
- if (!parseUnsignedInteger(value_, std::numeric_limits<uint16_t>::max(),
+ if (!parseUnsignedInteger(obj.value_, std::numeric_limits<uint16_t>::max(),
&value))
return std::nullopt;
@@ -242,14 +247,15 @@ std::optional<uint16_t> YamlObject::get() const
}
template<>
-std::optional<int32_t> YamlObject::get() const
+std::optional<int32_t>
+YamlObject::Getter<int32_t>::get(const YamlObject &obj) const
{
- if (type_ != Type::Value)
+ if (obj.type_ != Type::Value)
return std::nullopt;
long value;
- if (!parseSignedInteger(value_, std::numeric_limits<int32_t>::min(),
+ if (!parseSignedInteger(obj.value_, std::numeric_limits<int32_t>::min(),
std::numeric_limits<int32_t>::max(), &value))
return std::nullopt;
@@ -257,14 +263,15 @@ std::optional<int32_t> YamlObject::get() const
}
template<>
-std::optional<uint32_t> YamlObject::get() const
+std::optional<uint32_t>
+YamlObject::Getter<uint32_t>::get(const YamlObject &obj) const
{
- if (type_ != Type::Value)
+ if (obj.type_ != Type::Value)
return std::nullopt;
unsigned long value;
- if (!parseUnsignedInteger(value_, std::numeric_limits<uint32_t>::max(),
+ if (!parseUnsignedInteger(obj.value_, std::numeric_limits<uint32_t>::max(),
&value))
return std::nullopt;
@@ -272,18 +279,26 @@ std::optional<uint32_t> YamlObject::get() const
}
template<>
-std::optional<double> YamlObject::get() const
+std::optional<float>
+YamlObject::Getter<float>::get(const YamlObject &obj) const
{
- if (type_ != Type::Value)
+ return obj.get<double>();
+}
+
+template<>
+std::optional<double>
+YamlObject::Getter<double>::get(const YamlObject &obj) const
+{
+ if (obj.type_ != Type::Value)
return std::nullopt;
- if (value_ == "")
+ if (obj.value_ == "")
return std::nullopt;
char *end;
errno = 0;
- double value = utils::strtod(value_.c_str(), &end);
+ double value = utils::strtod(obj.value_.c_str(), &end);
if ('\0' != *end || errno == ERANGE)
return std::nullopt;
@@ -292,28 +307,30 @@ std::optional<double> YamlObject::get() const
}
template<>
-std::optional<std::string> YamlObject::get() const
+std::optional<std::string>
+YamlObject::Getter<std::string>::get(const YamlObject &obj) const
{
- if (type_ != Type::Value)
+ if (obj.type_ != Type::Value)
return std::nullopt;
- return value_;
+ return obj.value_;
}
template<>
-std::optional<Size> YamlObject::get() const
+std::optional<Size>
+YamlObject::Getter<Size>::get(const YamlObject &obj) const
{
- if (type_ != Type::List)
+ if (obj.type_ != Type::List)
return std::nullopt;
- if (list_.size() != 2)
+ if (obj.list_.size() != 2)
return std::nullopt;
- auto width = list_[0].value->get<uint32_t>();
+ auto width = obj.list_[0].value->get<uint32_t>();
if (!width)
return std::nullopt;
- auto height = list_[1].value->get<uint32_t>();
+ auto height = obj.list_[1].value->get<uint32_t>();
if (!height)
return std::nullopt;
@@ -339,6 +356,7 @@ std::optional<Size> YamlObject::get() const
template<typename T,
std::enable_if_t<
std::is_same_v<bool, T> ||
+ std::is_same_v<float, T> ||
std::is_same_v<double, T> ||
std::is_same_v<int8_t, T> ||
std::is_same_v<uint8_t, T> ||
@@ -367,6 +385,7 @@ std::optional<std::vector<T>> YamlObject::getList() const
}
template std::optional<std::vector<bool>> YamlObject::getList<bool>() const;
+template std::optional<std::vector<float>> YamlObject::getList<float>() const;
template std::optional<std::vector<double>> YamlObject::getList<double>() const;
template std::optional<std::vector<int8_t>> YamlObject::getList<int8_t>() const;
template std::optional<std::vector<uint8_t>> YamlObject::getList<uint8_t>() const;
@@ -468,10 +487,13 @@ bool YamlObject::contains(const std::string &key) const
*/
const YamlObject &YamlObject::operator[](const std::string &key) const
{
- if (type_ != Type::Dictionary || !contains(key))
+ if (type_ != Type::Dictionary)
return empty;
auto iter = dictionary_.find(key);
+ if (iter == dictionary_.end())
+ return empty;
+
return *iter->second;
}
diff --git a/src/v4l2/v4l2_compat.cpp b/src/v4l2/v4l2_compat.cpp
index 8e2b7e92..bd016cbd 100644
--- a/src/v4l2/v4l2_compat.cpp
+++ b/src/v4l2/v4l2_compat.cpp
@@ -7,6 +7,7 @@
#include "v4l2_compat_manager.h"
+#include <assert.h>
#include <errno.h>
#include <fcntl.h>
#include <stdarg.h>
@@ -28,12 +29,25 @@ using namespace libcamera;
va_end(ap); \
}
+namespace {
+
+/*
+ * Determine if the flags require a further mode arguments that needs to be
+ * parsed from va_args.
+ */
+bool needs_mode(int flags)
+{
+ return (flags & O_CREAT) || ((flags & O_TMPFILE) == O_TMPFILE);
+}
+
+} /* namespace */
+
extern "C" {
LIBCAMERA_PUBLIC int open(const char *path, int oflag, ...)
{
mode_t mode = 0;
- if (oflag & O_CREAT || oflag & O_TMPFILE)
+ if (needs_mode(oflag))
extract_va_arg(mode_t, mode, oflag);
return V4L2CompatManager::instance()->openat(AT_FDCWD, path,
@@ -43,6 +57,7 @@ LIBCAMERA_PUBLIC int open(const char *path, int oflag, ...)
/* _FORTIFY_SOURCE redirects open to __open_2 */
LIBCAMERA_PUBLIC int __open_2(const char *path, int oflag)
{
+ assert(!needs_mode(oflag));
return open(path, oflag);
}
@@ -50,7 +65,7 @@ LIBCAMERA_PUBLIC int __open_2(const char *path, int oflag)
LIBCAMERA_PUBLIC int open64(const char *path, int oflag, ...)
{
mode_t mode = 0;
- if (oflag & O_CREAT || oflag & O_TMPFILE)
+ if (needs_mode(oflag))
extract_va_arg(mode_t, mode, oflag);
return V4L2CompatManager::instance()->openat(AT_FDCWD, path,
@@ -59,14 +74,15 @@ LIBCAMERA_PUBLIC int open64(const char *path, int oflag, ...)
LIBCAMERA_PUBLIC int __open64_2(const char *path, int oflag)
{
- return open(path, oflag);
+ assert(!needs_mode(oflag));
+ return open64(path, oflag);
}
#endif
LIBCAMERA_PUBLIC int openat(int dirfd, const char *path, int oflag, ...)
{
mode_t mode = 0;
- if (oflag & O_CREAT || oflag & O_TMPFILE)
+ if (needs_mode(oflag))
extract_va_arg(mode_t, mode, oflag);
return V4L2CompatManager::instance()->openat(dirfd, path, oflag, mode);
@@ -74,6 +90,7 @@ LIBCAMERA_PUBLIC int openat(int dirfd, const char *path, int oflag, ...)
LIBCAMERA_PUBLIC int __openat_2(int dirfd, const char *path, int oflag)
{
+ assert(!needs_mode(oflag));
return openat(dirfd, path, oflag);
}
@@ -81,7 +98,7 @@ LIBCAMERA_PUBLIC int __openat_2(int dirfd, const char *path, int oflag)
LIBCAMERA_PUBLIC int openat64(int dirfd, const char *path, int oflag, ...)
{
mode_t mode = 0;
- if (oflag & O_CREAT || oflag & O_TMPFILE)
+ if (needs_mode(oflag))
extract_va_arg(mode_t, mode, oflag);
return V4L2CompatManager::instance()->openat(dirfd, path,
@@ -90,7 +107,8 @@ LIBCAMERA_PUBLIC int openat64(int dirfd, const char *path, int oflag, ...)
LIBCAMERA_PUBLIC int __openat64_2(int dirfd, const char *path, int oflag)
{
- return openat(dirfd, path, oflag);
+ assert(!needs_mode(oflag));
+ return openat64(dirfd, path, oflag);
}
#endif
diff --git a/test/camera/buffer_import.cpp b/test/camera/buffer_import.cpp
index 92884004..815d1cae 100644
--- a/test/camera/buffer_import.cpp
+++ b/test/camera/buffer_import.cpp
@@ -63,6 +63,8 @@ protected:
request->reuse();
request->addBuffer(stream, buffer);
camera_->queueRequest(request);
+
+ dispatcher_->interrupt();
}
int init() override
@@ -76,6 +78,8 @@ protected:
return TestFail;
}
+ dispatcher_ = Thread::current()->eventDispatcher();
+
return TestPass;
}
@@ -133,17 +137,20 @@ protected:
}
}
- EventDispatcher *dispatcher = Thread::current()->eventDispatcher();
+ const unsigned int nFrames = cfg.bufferCount * 2;
Timer timer;
- timer.start(1000ms);
- while (timer.isRunning())
- dispatcher->processEvents();
+ timer.start(500ms * nFrames);
+ while (timer.isRunning()) {
+ dispatcher_->processEvents();
+ if (completeRequestsCount_ > nFrames)
+ break;
+ }
- if (completeRequestsCount_ < cfg.bufferCount * 2) {
+ if (completeRequestsCount_ < nFrames) {
std::cout << "Failed to capture enough frames (got "
<< completeRequestsCount_ << " expected at least "
- << cfg.bufferCount * 2 << ")" << std::endl;
+ << nFrames << ")" << std::endl;
return TestFail;
}
@@ -161,6 +168,8 @@ protected:
}
private:
+ EventDispatcher *dispatcher_;
+
std::vector<std::unique_ptr<Request>> requests_;
unsigned int completeBuffersCount_;
diff --git a/test/camera/capture.cpp b/test/camera/capture.cpp
index de824083..8766fb19 100644
--- a/test/camera/capture.cpp
+++ b/test/camera/capture.cpp
@@ -59,6 +59,8 @@ protected:
request->reuse();
request->addBuffer(stream, buffer);
camera_->queueRequest(request);
+
+ dispatcher_->interrupt();
}
int init() override
@@ -73,6 +75,7 @@ protected:
}
allocator_ = new FrameBufferAllocator(camera_);
+ dispatcher_ = Thread::current()->eventDispatcher();
return TestPass;
}
@@ -135,19 +138,20 @@ protected:
}
}
- EventDispatcher *dispatcher = Thread::current()->eventDispatcher();
+ unsigned int nFrames = allocator_->buffers(stream).size() * 2;
Timer timer;
- timer.start(1000ms);
- while (timer.isRunning())
- dispatcher->processEvents();
-
- unsigned int nbuffers = allocator_->buffers(stream).size();
+ timer.start(500ms * nFrames);
+ while (timer.isRunning()) {
+ dispatcher_->processEvents();
+ if (completeRequestsCount_ > nFrames)
+ break;
+ }
- if (completeRequestsCount_ < nbuffers * 2) {
+ if (completeRequestsCount_ < nFrames) {
cout << "Failed to capture enough frames (got "
<< completeRequestsCount_ << " expected at least "
- << nbuffers * 2 << ")" << endl;
+ << nFrames * 2 << ")" << endl;
return TestFail;
}
@@ -164,6 +168,8 @@ protected:
return TestPass;
}
+ EventDispatcher *dispatcher_;
+
std::vector<std::unique_ptr<Request>> requests_;
std::unique_ptr<CameraConfiguration> config_;
diff --git a/test/fence.cpp b/test/fence.cpp
index ada650ff..8095b228 100644
--- a/test/fence.cpp
+++ b/test/fence.cpp
@@ -43,7 +43,6 @@ private:
void signalFence();
- std::unique_ptr<Fence> fence_;
EventDispatcher *dispatcher_;
UniqueFD eventFd_;
UniqueFD eventFd2_;
@@ -58,8 +57,11 @@ private:
bool expectedCompletionResult_ = true;
bool setFence_ = true;
- unsigned int completedRequest_;
-
+ /*
+ * Request IDs track the number of requests that have completed. They
+ * are one-based, and don't wrap.
+ */
+ unsigned int completedRequestId_;
unsigned int signalledRequestId_;
unsigned int expiredRequestId_;
unsigned int nbuffers_;
@@ -127,8 +129,19 @@ int FenceTest::init()
return TestFail;
}
- signalledRequestId_ = nbuffers_ - 2;
- expiredRequestId_ = nbuffers_ - 1;
+ completedRequestId_ = 0;
+
+ /*
+ * All but two requests are queued without a fence. Request
+ * expiredRequestId_ will be queued with a fence that we won't signal
+ * (which is then expected to expire), and request signalledRequestId_
+ * will be queued with a fence that gets signalled. Select nbuffers_
+ * and nbuffers_ * 2 for those two requests, to space them by a few
+ * frames while still not requiring a long time for the test to
+ * complete.
+ */
+ expiredRequestId_ = nbuffers_;
+ signalledRequestId_ = nbuffers_ * 2;
return TestPass;
}
@@ -190,16 +203,16 @@ void FenceTest::requestRequeue(Request *request)
const Request::BufferMap &buffers = request->buffers();
const Stream *stream = buffers.begin()->first;
FrameBuffer *buffer = buffers.begin()->second;
- uint64_t cookie = request->cookie();
request->reuse();
- if (cookie == signalledRequestId_ && setFence_) {
+ if (completedRequestId_ == signalledRequestId_ - nbuffers_ && setFence_) {
/*
- * The second time this request is queued add a fence to it.
- *
- * The main loop signals it by using a timer to write to the
- * efd2_ file descriptor before the fence expires.
+ * This is the request that will be used to test fence
+ * signalling when it completes next time. Add a fence to it,
+ * using efd2_. The main loop will signal the fence by using a
+ * timer to write to the efd2_ file descriptor before the fence
+ * expires.
*/
std::unique_ptr<Fence> fence =
std::make_unique<Fence>(std::move(eventFd2_));
@@ -214,16 +227,15 @@ void FenceTest::requestRequeue(Request *request)
void FenceTest::requestComplete(Request *request)
{
- uint64_t cookie = request->cookie();
- completedRequest_ = cookie;
+ completedRequestId_++;
/*
- * The last request is expected to fail as its fence has not been
- * signaled.
+ * Request expiredRequestId_ is expected to fail as its fence has not
+ * been signalled.
*
* Validate the fence status but do not re-queue it.
*/
- if (cookie == expiredRequestId_) {
+ if (completedRequestId_ == expiredRequestId_) {
if (validateExpiredRequest(request) != TestPass)
expectedCompletionResult_ = false;
@@ -231,7 +243,7 @@ void FenceTest::requestComplete(Request *request)
return;
}
- /* Validate all requests but the last. */
+ /* Validate all other requests. */
if (validateRequest(request) != TestPass) {
expectedCompletionResult_ = false;
@@ -272,15 +284,16 @@ int FenceTest::run()
}
int ret;
- if (i == expiredRequestId_) {
+ if (i == expiredRequestId_ - 1) {
/* This request will have a fence, and it will expire. */
- fence_ = std::make_unique<Fence>(std::move(eventFd_));
- if (!fence_->isValid()) {
+ std::unique_ptr<Fence> fence =
+ std::make_unique<Fence>(std::move(eventFd_));
+ if (!fence->isValid()) {
cerr << "Fence should be valid" << endl;
return TestFail;
}
- ret = request->addBuffer(stream_, buffer.get(), std::move(fence_));
+ ret = request->addBuffer(stream_, buffer.get(), std::move(fence));
} else {
/* All other requests will have no Fence. */
ret = request->addBuffer(stream_, buffer.get());
@@ -314,15 +327,21 @@ int FenceTest::run()
Timer fenceTimer;
fenceTimer.timeout.connect(this, &FenceTest::signalFence);
- /* Loop for one second. */
+ /*
+ * Loop long enough for all requests to complete, allowing 500ms per
+ * request.
+ */
Timer timer;
- timer.start(1000ms);
- while (timer.isRunning() && expectedCompletionResult_) {
- if (completedRequest_ == signalledRequestId_ && setFence_)
+ timer.start(500ms * (signalledRequestId_ + 1));
+ while (timer.isRunning() && expectedCompletionResult_ &&
+ completedRequestId_ <= signalledRequestId_ + 1) {
+ if (completedRequestId_ == signalledRequestId_ - 1 && setFence_)
/*
- * signalledRequestId_ has just completed and it has
- * been re-queued with a fence. Start the timer to
- * signal the fence in 10 msec.
+ * The request just before signalledRequestId_ has just
+ * completed. Request signalledRequestId_ has been
+ * queued with a fence, and libcamera is likely already
+ * waiting on the fence, or will soon. Start the timer
+ * to signal the fence in 10 msec.
*/
fenceTimer.start(10ms);
diff --git a/test/ipa/meson.build b/test/ipa/meson.build
index 180b0da0..fe21ca58 100644
--- a/test/ipa/meson.build
+++ b/test/ipa/meson.build
@@ -1,5 +1,7 @@
# SPDX-License-Identifier: CC0-1.0
+subdir('rkisp1')
+
ipa_test = [
{'name': 'ipa_module_test', 'sources': ['ipa_module_test.cpp']},
{'name': 'ipa_interface_test', 'sources': ['ipa_interface_test.cpp']},
@@ -7,9 +9,9 @@ ipa_test = [
foreach test : ipa_test
exe = executable(test['name'], test['sources'], libcamera_generated_ipa_headers,
- dependencies : libcamera_private,
- link_with : [libipa, test_libraries],
- include_directories : [libipa_includes, test_includes_internal])
+ dependencies : [libcamera_private, libipa_dep],
+ link_with : [test_libraries],
+ include_directories : [test_includes_internal])
test(test['name'], exe, suite : 'ipa')
endforeach
diff --git a/test/ipa/rkisp1/meson.build b/test/ipa/rkisp1/meson.build
new file mode 100644
index 00000000..5b08e293
--- /dev/null
+++ b/test/ipa/rkisp1/meson.build
@@ -0,0 +1,15 @@
+# SPDX-License-Identifier: CC0-1.0
+
+rkisp1_ipa_test = [
+ {'name': 'rkisp1-utils', 'sources': ['rkisp1-utils.cpp']},
+]
+
+foreach test : rkisp1_ipa_test
+ exe = executable(test['name'], test['sources'], libcamera_generated_ipa_headers,
+ dependencies : [libcamera_private, libipa_dep],
+ link_with : [test_libraries],
+ include_directories : [test_includes_internal,
+ '../../../src/ipa/rkisp1/'])
+
+ test(test['name'], exe, suite : 'ipa')
+endforeach
diff --git a/test/ipa/rkisp1/rkisp1-utils.cpp b/test/ipa/rkisp1/rkisp1-utils.cpp
new file mode 100644
index 00000000..b1863894
--- /dev/null
+++ b/test/ipa/rkisp1/rkisp1-utils.cpp
@@ -0,0 +1,108 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2024, Paul Elder <paul.elder@ideasonboard.com>
+ *
+ * Miscellaneous utility tests
+ */
+
+#include <cmath>
+#include <iostream>
+#include <map>
+#include <stdint.h>
+
+#include "../src/ipa/rkisp1/utils.h"
+
+#include "test.h"
+
+using namespace std;
+using namespace libcamera;
+using namespace ipa::rkisp1;
+
+class RkISP1UtilsTest : public Test
+{
+protected:
+ /* R for real, I for integer */
+ template<unsigned int IntPrec, unsigned int FracPrec, typename I, typename R>
+ int testFixedToFloat(I input, R expected)
+ {
+ R out = utils::fixedToFloatingPoint<IntPrec, FracPrec, R>(input);
+ R prec = 1.0 / (1 << FracPrec);
+ if (std::abs(out - expected) > prec) {
+ cerr << "Reverse conversion expected " << input
+ << " to convert to " << expected
+ << ", got " << out << std::endl;
+ return TestFail;
+ }
+
+ return TestPass;
+ }
+
+ template<unsigned int IntPrec, unsigned int FracPrec, typename T>
+ int testSingleFixedPoint(double input, T expected)
+ {
+ T ret = utils::floatingToFixedPoint<IntPrec, FracPrec, T>(input);
+ if (ret != expected) {
+ cerr << "Expected " << input << " to convert to "
+ << expected << ", got " << ret << std::endl;
+ return TestFail;
+ }
+
+ /*
+ * The precision check is fairly arbitrary but is based on what
+ * the rkisp1 is capable of in the crosstalk module.
+ */
+ double f = utils::fixedToFloatingPoint<IntPrec, FracPrec, double>(ret);
+ if (std::abs(f - input) > 0.005) {
+ cerr << "Reverse conversion expected " << ret
+ << " to convert to " << input
+ << ", got " << f << std::endl;
+ return TestFail;
+ }
+
+ return TestPass;
+ }
+
+ int testFixedPoint()
+ {
+ /*
+ * The second 7.992 test is to test that unused bits don't
+ * affect the result.
+ */
+ std::map<double, uint16_t> testCases = {
+ { 7.992, 0x3ff },
+ { 0.2, 0x01a },
+ { -0.2, 0x7e6 },
+ { -0.8, 0x79a },
+ { -0.4, 0x7cd },
+ { -1.4, 0x74d },
+ { -8, 0x400 },
+ { 0, 0 },
+ };
+
+ int ret;
+ for (const auto &testCase : testCases) {
+ ret = testSingleFixedPoint<4, 7, uint16_t>(testCase.first,
+ testCase.second);
+ if (ret != TestPass)
+ return ret;
+ }
+
+ /* Special case with a superfluous one in the unused bits */
+ ret = testFixedToFloat<4, 7, uint16_t, double>(0xbff, 7.992);
+ if (ret != TestPass)
+ return ret;
+
+ return TestPass;
+ }
+
+ int run()
+ {
+ /* fixed point conversion test */
+ if (testFixedPoint() != TestPass)
+ return TestFail;
+
+ return TestPass;
+ }
+};
+
+TEST_REGISTER(RkISP1UtilsTest)
diff --git a/test/v4l2_videodevice/capture_async.cpp b/test/v4l2_videodevice/capture_async.cpp
index 42e1e671..67366461 100644
--- a/test/v4l2_videodevice/capture_async.cpp
+++ b/test/v4l2_videodevice/capture_async.cpp
@@ -61,10 +61,12 @@ protected:
if (ret)
return TestFail;
- timeout.start(10000ms);
+ const unsigned int nFrames = 30;
+
+ timeout.start(500ms * nFrames);
while (timeout.isRunning()) {
dispatcher->processEvents();
- if (frames > 30)
+ if (frames > nFrames)
break;
}
@@ -73,8 +75,9 @@ protected:
return TestFail;
}
- if (frames < 30) {
- std::cout << "Failed to capture 30 frames within timeout." << std::endl;
+ if (frames < nFrames) {
+ std::cout << "Failed to capture " << nFrames
+ << " frames within timeout." << std::endl;
return TestFail;
}
diff --git a/utils/checkstyle.py b/utils/checkstyle.py
index 4e287b2e..4185c39a 100755
--- a/utils/checkstyle.py
+++ b/utils/checkstyle.py
@@ -556,20 +556,49 @@ class StyleChecker(metaclass=ClassRegistry):
class StyleIssue(object):
- def __init__(self, line_number, line, msg):
+ def __init__(self, line_number, position, line, msg):
self.line_number = line_number
+ self.position = position
self.line = line
self.msg = msg
+class HexValueChecker(StyleChecker):
+ patterns = ('*.c', '*.cpp', '*.h')
+
+ regex = re.compile(r'\b0[xX][0-9a-fA-F]+\b')
+
+ def __init__(self, content):
+ super().__init__()
+ self.__content = content
+
+ def check(self, line_numbers):
+ issues = []
+
+ for line_number in line_numbers:
+ line = self.__content[line_number - 1]
+ match = HexValueChecker.regex.search(line)
+ if not match:
+ continue
+
+ value = match.group(0)
+ if value == value.lower():
+ continue
+
+ issues.append(StyleIssue(line_number, match.span(0), line,
+ f'Use lowercase hex constant {value.lower()}'))
+
+ return issues
+
+
class IncludeChecker(StyleChecker):
patterns = ('*.cpp', '*.h')
- headers = ('assert', 'ctype', 'errno', 'fenv', 'float', 'inttypes',
- 'limits', 'locale', 'setjmp', 'signal', 'stdarg', 'stddef',
- 'stdint', 'stdio', 'stdlib', 'string', 'time', 'uchar', 'wchar',
- 'wctype')
- include_regex = re.compile(r'^#include <c([a-z]*)>')
+ headers = ('cassert', 'cctype', 'cerrno', 'cfenv', 'cfloat', 'cinttypes',
+ 'climits', 'clocale', 'csetjmp', 'csignal', 'cstdarg', 'cstddef',
+ 'cstdint', 'cstdio', 'cstdlib', 'cstring', 'ctime', 'cuchar',
+ 'cwchar', 'cwctype', 'math.h')
+ include_regex = re.compile(r'^#include <([a-z.]*)>')
def __init__(self, content):
super().__init__()
@@ -588,8 +617,15 @@ class IncludeChecker(StyleChecker):
if header not in IncludeChecker.headers:
continue
- issues.append(StyleIssue(line_number, line,
- 'C compatibility header <%s.h> is preferred' % header))
+ if header.endswith('.h'):
+ header_type = 'C++'
+ header = 'c' + header[:-2]
+ else:
+ header_type = 'C compatibility'
+ header = header[1:] + '.h'
+
+ issues.append(StyleIssue(line_number, match.span(1), line,
+ f'{header_type} header <{header}> is preferred'))
return issues
@@ -606,10 +642,12 @@ class LogCategoryChecker(StyleChecker):
issues = []
for line_number in line_numbers:
line = self.__content[line_number-1]
- if not LogCategoryChecker.log_regex.search(line):
+ match = LogCategoryChecker.log_regex.search(line)
+ if not match:
continue
- issues.append(StyleIssue(line_number, line, 'LOG() should use categories'))
+ issues.append(StyleIssue(line_number, match.span(1), line,
+ 'LOG() should use categories'))
return issues
@@ -625,8 +663,10 @@ class MesonChecker(StyleChecker):
issues = []
for line_number in line_numbers:
line = self.__content[line_number-1]
- if line.find('\t') != -1:
- issues.append(StyleIssue(line_number, line, 'meson.build should use spaces for indentation'))
+ pos = line.find('\t')
+ if pos != -1:
+ issues.append(StyleIssue(line_number, [pos, pos], line,
+ 'meson.build should use spaces for indentation'))
return issues
@@ -646,7 +686,7 @@ class Pep8Checker(StyleChecker):
ret = subprocess.run(['pycodestyle', '--ignore=E501', '-'],
input=data, stdout=subprocess.PIPE)
except FileNotFoundError:
- issues.append(StyleIssue(0, None, 'Please install pycodestyle to validate python additions'))
+ issues.append(StyleIssue(0, None, None, 'Please install pycodestyle to validate python additions'))
return issues
results = ret.stdout.decode('utf-8').splitlines()
@@ -658,7 +698,7 @@ class Pep8Checker(StyleChecker):
if line_number in line_numbers:
line = self.__content[line_number - 1]
- issues.append(StyleIssue(line_number, line, msg))
+ issues.append(StyleIssue(line_number, None, line, msg))
return issues
@@ -679,7 +719,7 @@ class ShellChecker(StyleChecker):
ret = subprocess.run(['shellcheck', '-Cnever', '-'],
input=data, stdout=subprocess.PIPE)
except FileNotFoundError:
- issues.append(StyleIssue(0, None, 'Please install shellcheck to validate shell script additions'))
+ issues.append(StyleIssue(0, None, None, 'Please install shellcheck to validate shell script additions'))
return issues
results = ret.stdout.decode('utf-8').splitlines()
@@ -692,11 +732,8 @@ class ShellChecker(StyleChecker):
line = results[nr + 1]
msg = results[nr + 2]
- # Determined, but not yet used
- position = msg.find('^') + 1
-
if line_number in line_numbers:
- issues.append(StyleIssue(line_number, line, msg))
+ issues.append(StyleIssue(line_number, None, line, msg))
return issues
@@ -938,6 +975,16 @@ def check_file(top_level, commit, filename, checkers):
print('%s+%s%s' % (Colours.fg(Colours.Yellow), issue.line.rstrip(),
Colours.reset()))
+ if issue.position is not None:
+ # Align the position marker by using the original line with
+ # all characters except for tabs replaced with spaces. This
+ # ensures proper alignment regardless of how the code is
+ # indented.
+ start = issue.position[0]
+ prefix = ''.join([c if c == '\t' else ' ' for c in issue.line[:start]])
+ length = issue.position[1] - start - 1
+ print(' ' + prefix + '^' + '~' * length)
+
return len(formatted_diff) + len(issues)
diff --git a/utils/raspberrypi/ctt/alsc_only.py b/utils/raspberrypi/ctt/alsc_only.py
index 092aa40e..a521c4ad 100755
--- a/utils/raspberrypi/ctt/alsc_only.py
+++ b/utils/raspberrypi/ctt/alsc_only.py
@@ -2,12 +2,14 @@
#
# SPDX-License-Identifier: BSD-2-Clause
#
-# Copyright (C) 2022, Raspberry Pi (Trading) Limited
+# Copyright (C) 2022, Raspberry Pi Ltd
#
# alsc tuning tool
-from ctt import *
+import sys
+from ctt import *
+from ctt_tools import parse_input
if __name__ == '__main__':
"""
@@ -15,13 +17,14 @@ if __name__ == '__main__':
"""
if len(sys.argv) == 1:
print("""
- Pisp Camera Tuning Tool version 1.0
+ PiSP Lens Shading Camera Tuning Tool version 1.0
Required Arguments:
'-i' : Calibration image directory.
'-o' : Name of output json file.
Optional Arguments:
+ '-t' : Target platform - 'pisp' or 'vc4'. Default 'vc4'
'-c' : Config file for the CTT. If not passed, default parameters used.
'-l' : Name of output log file. If not passed, 'ctt_log.txt' used.
""")
@@ -30,5 +33,10 @@ if __name__ == '__main__':
"""
parse input arguments
"""
- json_output, directory, config, log_output = parse_input()
- run_ctt(json_output, directory, config, log_output, alsc_only=True)
+ json_output, directory, config, log_output, target = parse_input()
+ if target == 'pisp':
+ from ctt_pisp import json_template, grid_size
+ elif target == 'vc4':
+ from ctt_vc4 import json_template, grid_size
+
+ run_ctt(json_output, directory, config, log_output, json_template, grid_size, target, alsc_only=True)
diff --git a/utils/raspberrypi/ctt/cac_only.py b/utils/raspberrypi/ctt/cac_only.py
new file mode 100644
index 00000000..1c0a8193
--- /dev/null
+++ b/utils/raspberrypi/ctt/cac_only.py
@@ -0,0 +1,142 @@
+#!/usr/bin/env python3
+#
+# SPDX-License-Identifier: BSD-2-Clause
+#
+# Copyright (C) 2023, Raspberry Pi (Trading) Ltd.
+#
+# cac_only.py - cac tuning tool
+
+
+# This file allows you to tune only the chromatic aberration correction
+# Specify any number of files in the command line args, and it shall iterate through
+# and generate an averaged cac table from all the input images, which you can then
+# input into your tuning file.
+
+# Takes .dng files produced by the camera modules of the dots grid and calculates the chromatic abberation of each dot.
+# Then takes each dot, and works out where it was in the image, and uses that to output a tables of the shifts
+# across the whole image.
+
+from PIL import Image
+import numpy as np
+import rawpy
+import sys
+import getopt
+
+from ctt_cac import *
+
+
+def cac(filelist, output_filepath, plot_results=False):
+ np.set_printoptions(precision=3)
+ np.set_printoptions(suppress=True)
+
+ # Create arrays to hold all the dots data and their colour offsets
+ red_shift = [] # Format is: [[Dot Center X, Dot Center Y, x shift, y shift]]
+ blue_shift = []
+ # Iterate through the files
+ # Multiple files is reccomended to average out the lens aberration through rotations
+ for file in filelist:
+ print("\n Processing file " + str(file))
+ # Read the raw RGB values from the .dng file
+ with rawpy.imread(file) as raw:
+ rgb = raw.postprocess()
+ sizes = (raw.sizes)
+
+ image_size = [sizes[2], sizes[3]] # Image size, X, Y
+ # Create a colour copy of the RGB values to use later in the calibration
+ imout = Image.new(mode="RGB", size=image_size)
+ rgb_image = np.array(imout)
+ # The rgb values need reshaping from a 1d array to a 3d array to be worked with easily
+ rgb.reshape((image_size[0], image_size[1], 3))
+ rgb_image = rgb
+
+ # Pass the RGB image through to the dots locating program
+ # Returns an array of the dots (colour rectangles around the dots), and an array of their locations
+ print("Finding dots")
+ dots, dots_locations = find_dots_locations(rgb_image)
+
+ # Now, analyse each dot. Work out the centroid of each colour channel, and use that to work out
+ # by how far the chromatic aberration has shifted each channel
+ print('Dots found: ' + str(len(dots)))
+
+ for dot, dot_location in zip(dots, dots_locations):
+ if len(dot) > 0:
+ if (dot_location[0] > 0) and (dot_location[1] > 0):
+ ret = analyse_dot(dot, dot_location)
+ red_shift.append(ret[0])
+ blue_shift.append(ret[1])
+
+ # Take our arrays of red shifts and locations, push them through to be interpolated into a 9x9 matrix
+ # for the CAC block to handle and then store these as a .json file to be added to the camera
+ # tuning file
+ print("\nCreating output grid")
+ rx, ry, bx, by = shifts_to_yaml(red_shift, blue_shift, image_size)
+
+ print("CAC correction complete!")
+
+ # The json format that we then paste into the tuning file (manually)
+ sample = '''
+ {
+ "rpi.cac" :
+ {
+ "strength": 1.0,
+ "lut_rx" : [
+ rx_vals
+ ],
+ "lut_ry" : [
+ ry_vals
+ ],
+ "lut_bx" : [
+ bx_vals
+ ],
+ "lut_by" : [
+ by_vals
+ ]
+ }
+ }
+ '''
+
+ # Below, may look incorrect, however, the PiSP (standard) dimensions are flipped in comparison to
+ # PIL image coordinate directions, hence why xr -> yr. Also, the shifts calculated are colour shifts,
+ # and the PiSP block asks for the values it should shift (hence the * -1, to convert from colour shift to a pixel shift)
+ sample = sample.replace("rx_vals", pprint_array(ry * -1))
+ sample = sample.replace("ry_vals", pprint_array(rx * -1))
+ sample = sample.replace("bx_vals", pprint_array(by * -1))
+ sample = sample.replace("by_vals", pprint_array(bx * -1))
+ print("Successfully converted to JSON")
+ f = open(str(output_filepath), "w+")
+ f.write(sample)
+ f.close()
+ print("Successfully written to json file")
+ '''
+ If you wish to see a plot of the colour channel shifts, add the -p or --plots option
+ Can be a quick way of validating if the data/dots you've got are good, or if you need to
+ change some parameters/take some better images
+ '''
+ if plot_results:
+ plot_shifts(red_shift, blue_shift)
+
+
+if __name__ == "__main__":
+ argv = sys.argv
+ # Detect the input and output file paths
+ arg_output = "output.json"
+ arg_help = "{0} -i <input> -o <output> -p <plot results>".format(argv[0])
+ opts, args = getopt.getopt(argv[1:], "hi:o:p", ["help", "input=", "output=", "plot"])
+
+ output_location = 0
+ input_location = 0
+ filelist = []
+ plot_results = False
+ for i in range(len(argv)):
+ if ("-h") in argv[i]:
+ print(arg_help) # print the help message
+ sys.exit(2)
+ if "-o" in argv[i]:
+ output_location = i
+ if ".dng" in argv[i]:
+ filelist.append(argv[i])
+ if "-p" in argv[i]:
+ plot_results = True
+
+ arg_output = argv[output_location + 1]
+ cac(filelist, arg_output, plot_results)
diff --git a/utils/raspberrypi/ctt/convert_tuning.py b/utils/raspberrypi/ctt/convert_tuning.py
index f4504d45..83cf69d4 100755
--- a/utils/raspberrypi/ctt/convert_tuning.py
+++ b/utils/raspberrypi/ctt/convert_tuning.py
@@ -8,30 +8,104 @@
import argparse
import json
+import numpy as np
import sys
from ctt_pretty_print_json import pretty_print
+from ctt_pisp import grid_size as grid_size_pisp
+from ctt_pisp import json_template as json_template_pisp
+from ctt_vc4 import grid_size as grid_size_vc4
+from ctt_vc4 import json_template as json_template_vc4
-def convert_v2(in_json: dict) -> str:
+def interp_2d(in_ls, src_w, src_h, dst_w, dst_h):
- if 'version' in in_json.keys() and in_json['version'] != 1.0:
- print(f'The JSON config reports version {in_json["version"]} that is incompatible with this tool.')
- sys.exit(-1)
+ out_ls = np.zeros((dst_h, dst_w))
+ for i in range(src_h):
+ out_ls[i] = np.interp(np.linspace(0, dst_w - 1, dst_w),
+ np.linspace(0, dst_w - 1, src_w),
+ in_ls[i])
+ for i in range(dst_w):
+ out_ls[:,i] = np.interp(np.linspace(0, dst_h - 1, dst_h),
+ np.linspace(0, dst_h - 1, src_h),
+ out_ls[:src_h, i])
+ return out_ls
- converted = {
- 'version': 2.0,
- 'target': 'bcm2835',
- 'algorithms': [{algo: config} for algo, config in in_json.items()]
- }
- return pretty_print(converted)
+def convert_target(in_json: dict, target: str):
+
+ src_w, src_h = grid_size_pisp if target == 'vc4' else grid_size_vc4
+ dst_w, dst_h = grid_size_vc4 if target == 'vc4' else grid_size_pisp
+ json_template = json_template_vc4 if target == 'vc4' else json_template_pisp
+
+ # ALSC grid sizes
+ alsc = next(algo for algo in in_json['algorithms'] if 'rpi.alsc' in algo)['rpi.alsc']
+ for colour in ['calibrations_Cr', 'calibrations_Cb']:
+ if colour not in alsc:
+ continue
+ for temperature in alsc[colour]:
+ in_ls = np.reshape(temperature['table'], (src_h, src_w))
+ out_ls = interp_2d(in_ls, src_w, src_h, dst_w, dst_h)
+ temperature['table'] = np.round(out_ls.flatten(), 3).tolist()
+
+ if 'luminance_lut' in alsc:
+ in_ls = np.reshape(alsc['luminance_lut'], (src_h, src_w))
+ out_ls = interp_2d(in_ls, src_w, src_h, dst_w, dst_h)
+ alsc['luminance_lut'] = np.round(out_ls.flatten(), 3).tolist()
+
+ # Denoise blocks
+ for i, algo in enumerate(in_json['algorithms']):
+ if list(algo.keys())[0] == 'rpi.sdn':
+ in_json['algorithms'][i] = {'rpi.denoise': json_template['rpi.sdn'] if target == 'vc4' else json_template['rpi.denoise']}
+ break
+
+ # AGC mode weights
+ agc = next(algo for algo in in_json['algorithms'] if 'rpi.agc' in algo)['rpi.agc']
+ if 'channels' in agc:
+ for i, channel in enumerate(agc['channels']):
+ target_agc_metering = json_template['rpi.agc']['channels'][i]['metering_modes']
+ for mode, v in channel['metering_modes'].items():
+ v['weights'] = target_agc_metering[mode]['weights']
+ else:
+ for mode, v in agc["metering_modes"].items():
+ target_agc_metering = json_template['rpi.agc']['channels'][0]['metering_modes']
+ v['weights'] = target_agc_metering[mode]['weights']
+
+ # HDR
+ if target == 'pisp':
+ for i, algo in enumerate(in_json['algorithms']):
+ if list(algo.keys())[0] == 'rpi.hdr':
+ in_json['algorithms'][i] = {'rpi.hdr': json_template['rpi.hdr']}
+
+ return in_json
+
+
+def convert_v2(in_json: dict, target: str) -> str:
+
+ if 'version' in in_json.keys() and in_json['version'] == 1.0:
+ converted = {
+ 'version': 2.0,
+ 'target': target,
+ 'algorithms': [{algo: config} for algo, config in in_json.items()]
+ }
+ else:
+ converted = in_json
+
+ # Convert between vc4 <-> pisp targets. This is a best effort thing.
+ if converted['target'] != target:
+ converted = convert_target(converted, target)
+ converted['target'] = target
+
+ grid_size = grid_size_vc4[0] if target == 'vc4' else grid_size_pisp[0]
+ return pretty_print(converted, custom_elems={'table': grid_size, 'luminance_lut': grid_size})
if __name__ == "__main__":
parser = argparse.ArgumentParser(formatter_class=argparse.RawTextHelpFormatter, description=
- 'Convert the format of the Raspberry Pi camera tuning file from v1.0 to v2.0.\n')
+ 'Convert the format of the Raspberry Pi camera tuning file from v1.0 to v2.0 and/or the vc4 <-> pisp targets.\n')
parser.add_argument('input', type=str, help='Input tuning file.')
+ parser.add_argument('-t', '--target', type=str, help='Target platform.',
+ choices=['pisp', 'vc4'], default='vc4')
parser.add_argument('output', type=str, nargs='?',
help='Output converted tuning file. If not provided, the input file will be updated in-place.',
default=None)
@@ -40,7 +114,7 @@ if __name__ == "__main__":
with open(args.input, 'r') as f:
in_json = json.load(f)
- out_json = convert_v2(in_json)
+ out_json = convert_v2(in_json, args.target)
with open(args.output if args.output is not None else args.input, 'w') as f:
f.write(out_json)
diff --git a/utils/raspberrypi/ctt/ctt.py b/utils/raspberrypi/ctt/ctt.py
index bbe960b0..96f1b5e6 100755
--- a/utils/raspberrypi/ctt/ctt.py
+++ b/utils/raspberrypi/ctt/ctt.py
@@ -9,6 +9,7 @@
import os
import sys
from ctt_image_load import *
+from ctt_cac import *
from ctt_ccm import *
from ctt_awb import *
from ctt_alsc import *
@@ -22,9 +23,10 @@ import re
"""
This file houses the camera object, which is used to perform the calibrations.
-The camera object houses all the calibration images as attributes in two lists:
+The camera object houses all the calibration images as attributes in three lists:
- imgs (macbeth charts)
- imgs_alsc (alsc correction images)
+ - imgs_cac (cac correction images)
Various calibrations are methods of the camera object, and the output is stored
in a dictionary called self.json.
Once all the caibration has been completed, the Camera.json is written into a
@@ -67,139 +69,26 @@ Camera object that is the backbone of the tuning tool.
Input is the desired path of the output json.
"""
class Camera:
- def __init__(self, jfile):
+ def __init__(self, jfile, json):
self.path = os.path.dirname(os.path.expanduser(__file__)) + '/'
if self.path == '/':
self.path = ''
self.imgs = []
self.imgs_alsc = []
+ self.imgs_cac = []
self.log = 'Log created : ' + time.asctime(time.localtime(time.time()))
self.log_separator = '\n'+'-'*70+'\n'
self.jf = jfile
"""
initial json dict populated by uncalibrated values
"""
- self.json = {
- "rpi.black_level": {
- "black_level": 4096
- },
- "rpi.dpc": {
- },
- "rpi.lux": {
- "reference_shutter_speed": 10000,
- "reference_gain": 1,
- "reference_aperture": 1.0
- },
- "rpi.noise": {
- },
- "rpi.geq": {
- },
- "rpi.sdn": {
- },
- "rpi.awb": {
- "priors": [
- {"lux": 0, "prior": [2000, 1.0, 3000, 0.0, 13000, 0.0]},
- {"lux": 800, "prior": [2000, 0.0, 6000, 2.0, 13000, 2.0]},
- {"lux": 1500, "prior": [2000, 0.0, 4000, 1.0, 6000, 6.0, 6500, 7.0, 7000, 1.0, 13000, 1.0]}
- ],
- "modes": {
- "auto": {"lo": 2500, "hi": 8000},
- "incandescent": {"lo": 2500, "hi": 3000},
- "tungsten": {"lo": 3000, "hi": 3500},
- "fluorescent": {"lo": 4000, "hi": 4700},
- "indoor": {"lo": 3000, "hi": 5000},
- "daylight": {"lo": 5500, "hi": 6500},
- "cloudy": {"lo": 7000, "hi": 8600}
- },
- "bayes": 1
- },
- "rpi.agc": {
- "metering_modes": {
- "centre-weighted": {
- "weights": [3, 3, 3, 2, 2, 2, 2, 1, 1, 1, 1, 0, 0, 0, 0]
- },
- "spot": {
- "weights": [2, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
- },
- "matrix": {
- "weights": [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
- }
- },
- "exposure_modes": {
- "normal": {
- "shutter": [100, 10000, 30000, 60000, 120000],
- "gain": [1.0, 2.0, 4.0, 6.0, 6.0]
- },
- "short": {
- "shutter": [100, 5000, 10000, 20000, 120000],
- "gain": [1.0, 2.0, 4.0, 6.0, 6.0]
- }
- },
- "constraint_modes": {
- "normal": [
- {"bound": "LOWER", "q_lo": 0.98, "q_hi": 1.0, "y_target": [0, 0.5, 1000, 0.5]}
- ],
- "highlight": [
- {"bound": "LOWER", "q_lo": 0.98, "q_hi": 1.0, "y_target": [0, 0.5, 1000, 0.5]},
- {"bound": "UPPER", "q_lo": 0.98, "q_hi": 1.0, "y_target": [0, 0.8, 1000, 0.8]}
- ]
- },
- "y_target": [0, 0.16, 1000, 0.165, 10000, 0.17]
- },
- "rpi.alsc": {
- 'omega': 1.3,
- 'n_iter': 100,
- 'luminance_strength': 0.7,
- },
- "rpi.contrast": {
- "ce_enable": 1,
- "gamma_curve": [
- 0, 0,
- 1024, 5040,
- 2048, 9338,
- 3072, 12356,
- 4096, 15312,
- 5120, 18051,
- 6144, 20790,
- 7168, 23193,
- 8192, 25744,
- 9216, 27942,
- 10240, 30035,
- 11264, 32005,
- 12288, 33975,
- 13312, 35815,
- 14336, 37600,
- 15360, 39168,
- 16384, 40642,
- 18432, 43379,
- 20480, 45749,
- 22528, 47753,
- 24576, 49621,
- 26624, 51253,
- 28672, 52698,
- 30720, 53796,
- 32768, 54876,
- 36864, 57012,
- 40960, 58656,
- 45056, 59954,
- 49152, 61183,
- 53248, 62355,
- 57344, 63419,
- 61440, 64476,
- 65535, 65535
- ]
- },
- "rpi.ccm": {
- },
- "rpi.sharpen": {
- }
- }
+ self.json = json
"""
Perform colour correction calibrations by comparing macbeth patch colours
to standard macbeth chart colours.
"""
- def ccm_cal(self, do_alsc_colour):
+ def ccm_cal(self, do_alsc_colour, grid_size):
if 'rpi.ccm' in self.disable:
return 1
print('\nStarting CCM calibration')
@@ -245,7 +134,7 @@ class Camera:
Do CCM calibration
"""
try:
- ccms = ccm(self, cal_cr_list, cal_cb_list)
+ ccms = ccm(self, cal_cr_list, cal_cb_list, grid_size)
except ArithmeticError:
print('ERROR: Matrix is singular!\nTake new pictures and try again...')
self.log += '\nERROR: Singular matrix encountered during fit!'
@@ -259,11 +148,67 @@ class Camera:
print('Finished CCM calibration')
"""
+ Perform chromatic abberation correction using multiple dots images.
+ """
+ def cac_cal(self, do_alsc_colour):
+ if 'rpi.cac' in self.disable:
+ return 1
+ print('\nStarting CAC calibration')
+ self.log_new_sec('CAC')
+ """
+ check if cac images have been taken
+ """
+ if len(self.imgs_cac) == 0:
+ print('\nError:\nNo cac calibration images found')
+ self.log += '\nERROR: No CAC calibration images found!'
+ self.log += '\nCAC calibration aborted!'
+ return 1
+ """
+ if image is greyscale then CAC makes no sense
+ """
+ if self.grey:
+ print('\nERROR: Can\'t do CAC on greyscale image!')
+ self.log += '\nERROR: Cannot perform CAC calibration '
+ self.log += 'on greyscale image!\nCAC aborted!'
+ del self.json['rpi.cac']
+ return 0
+ a = time.time()
+ """
+ Check if camera is greyscale or color. If not greyscale, then perform cac
+ """
+ if do_alsc_colour:
+ """
+ Here we have a color sensor. Perform cac
+ """
+ try:
+ cacs = cac(self)
+ except ArithmeticError:
+ print('ERROR: Matrix is singular!\nTake new pictures and try again...')
+ self.log += '\nERROR: Singular matrix encountered during fit!'
+ self.log += '\nCAC aborted!'
+ return 1
+ else:
+ """
+ case where config options suggest greyscale camera. No point in doing CAC
+ """
+ cal_cr_list, cal_cb_list = None, None
+ self.log += '\nWARNING: No ALSC tables found.\nCAC calibration '
+ self.log += 'performed without ALSC correction...'
+
+ """
+ Write output to json
+ """
+ self.json['rpi.cac']['cac'] = cacs
+ self.log += '\nCAC calibration written to json file'
+ print('Finished CAC calibration')
+
+
+ """
Auto white balance calibration produces a colour curve for
various colour temperatures, as well as providing a maximum 'wiggle room'
distance from this curve (transverse_neg/pos).
"""
- def awb_cal(self, greyworld, do_alsc_colour):
+ def awb_cal(self, greyworld, do_alsc_colour, grid_size):
if 'rpi.awb' in self.disable:
return 1
print('\nStarting AWB calibration')
@@ -306,7 +251,7 @@ class Camera:
call calibration function
"""
plot = "rpi.awb" in self.plot
- awb_out = awb(self, cal_cr_list, cal_cb_list, plot)
+ awb_out = awb(self, cal_cr_list, cal_cb_list, plot, grid_size)
ct_curve, transverse_neg, transverse_pos = awb_out
"""
write output to json
@@ -324,7 +269,7 @@ class Camera:
colour channel seperately, and then partially corrects for vignetting.
The extent of the correction depends on the 'luminance_strength' parameter.
"""
- def alsc_cal(self, luminance_strength, do_alsc_colour):
+ def alsc_cal(self, luminance_strength, do_alsc_colour, grid_size, max_gain=8.0):
if 'rpi.alsc' in self.disable:
return 1
print('\nStarting ALSC calibration')
@@ -347,7 +292,7 @@ class Camera:
call calibration function
"""
plot = "rpi.alsc" in self.plot
- alsc_out = alsc_all(self, do_alsc_colour, plot)
+ alsc_out = alsc_all(self, do_alsc_colour, plot, grid_size, max_gain=max_gain)
cal_cr_list, cal_cb_list, luminance_lut, av_corn = alsc_out
"""
write output to json and finish if not do_alsc_colour
@@ -393,7 +338,7 @@ class Camera:
"""
obtain worst-case scenario residual sigmas
"""
- sigma_r, sigma_b = get_sigma(self, cal_cr_list, cal_cb_list)
+ sigma_r, sigma_b = get_sigma(self, cal_cr_list, cal_cb_list, grid_size)
"""
write output to json
"""
@@ -509,19 +454,20 @@ class Camera:
"""
writes the json dictionary to the raw json file then make pretty
"""
- def write_json(self):
+ def write_json(self, version=2.0, target='bcm2835', grid_size=(16, 12)):
"""
Write json dictionary to file using our version 2 format
"""
out_json = {
- "version": 2.0,
- 'target': 'bcm2835',
+ "version": version,
+ 'target': target if target != 'vc4' else 'bcm2835',
"algorithms": [{name: data} for name, data in self.json.items()],
}
with open(self.jf, 'w') as f:
- f.write(pretty_print(out_json))
+ f.write(pretty_print(out_json,
+ custom_elems={'table': grid_size[0], 'luminance_lut': grid_size[0]}))
"""
add a new section to the log file
@@ -627,6 +573,16 @@ class Camera:
self.log += '\nWARNING: Error reading colour temperature'
self.log += '\nImage discarded!'
print('DISCARDED')
+ elif 'cac' in filename:
+ Img = load_image(self, address, mac=False)
+ self.log += '\nIdentified as an CAC image'
+ Img.name = filename
+ self.log += '\nColour temperature: {} K'.format(col)
+ self.imgs_cac.append(Img)
+ if blacklevel != -1:
+ Img.blacklevel_16 = blacklevel
+ print(img_suc_msg)
+ continue
else:
self.log += '\nIdentified as macbeth chart image'
"""
@@ -672,6 +628,7 @@ class Camera:
self.log += '\n\nImages found:'
self.log += '\nMacbeth : {}'.format(len(self.imgs))
self.log += '\nALSC : {} '.format(len(self.imgs_alsc))
+ self.log += '\nCAC: {} '.format(len(self.imgs_cac))
self.log += '\n\nCamera metadata'
"""
check usable images found
@@ -680,22 +637,21 @@ class Camera:
print('\nERROR: No usable macbeth chart images found')
self.log += '\nERROR: No usable macbeth chart images found'
return 0
- elif len(self.imgs) == 0 and len(self.imgs_alsc) == 0:
+ elif len(self.imgs) == 0 and len(self.imgs_alsc) == 0 and len(self.imgs_cac) == 0:
print('\nERROR: No usable images found')
self.log += '\nERROR: No usable images found'
return 0
"""
Double check that every image has come from the same camera...
"""
- all_imgs = self.imgs + self.imgs_alsc
+ all_imgs = self.imgs + self.imgs_alsc + self.imgs_cac
camNames = list(set([Img.camName for Img in all_imgs]))
patterns = list(set([Img.pattern for Img in all_imgs]))
sigbitss = list(set([Img.sigbits for Img in all_imgs]))
blacklevels = list(set([Img.blacklevel_16 for Img in all_imgs]))
sizes = list(set([(Img.w, Img.h) for Img in all_imgs]))
- if len(camNames) == 1 and len(patterns) == 1 and len(sigbitss) == 1 and \
- len(blacklevels) == 1 and len(sizes) == 1:
+ if 1:
self.grey = (patterns[0] == 128)
self.blacklevel_16 = blacklevels[0]
self.log += '\nName: {}'.format(camNames[0])
@@ -712,7 +668,7 @@ class Camera:
return 0
-def run_ctt(json_output, directory, config, log_output, alsc_only=False):
+def run_ctt(json_output, directory, config, log_output, json_template, grid_size, target, alsc_only=False):
"""
check input files are jsons
"""
@@ -748,12 +704,14 @@ def run_ctt(json_output, directory, config, log_output, alsc_only=False):
greyworld = get_config(awb_d, "greyworld", 0, 'bool')
alsc_d = get_config(configs, "alsc", {}, 'dict')
do_alsc_colour = get_config(alsc_d, "do_alsc_colour", 1, 'bool')
- luminance_strength = get_config(alsc_d, "luminance_strength", 0.5, 'num')
+ luminance_strength = get_config(alsc_d, "luminance_strength", 0.8, 'num')
+ lsc_max_gain = get_config(alsc_d, "max_gain", 8.0, 'num')
blacklevel = get_config(configs, "blacklevel", -1, 'num')
macbeth_d = get_config(configs, "macbeth", {}, 'dict')
mac_small = get_config(macbeth_d, "small", 0, 'bool')
mac_show = get_config(macbeth_d, "show", 0, 'bool')
mac_config = (mac_small, mac_show)
+ print("Read lsc_max_gain", lsc_max_gain)
if blacklevel < -1 or blacklevel >= 2**16:
print('\nInvalid blacklevel, defaulted to 64')
@@ -772,7 +730,7 @@ def run_ctt(json_output, directory, config, log_output, alsc_only=False):
initialise tuning tool and load images
"""
try:
- Cam = Camera(json_output)
+ Cam = Camera(json_output, json=json_template)
Cam.log_user_input(json_output, directory, config, log_output)
if alsc_only:
disable = set(Cam.json.keys()).symmetric_difference({"rpi.alsc"})
@@ -794,14 +752,17 @@ def run_ctt(json_output, directory, config, log_output, alsc_only=False):
Cam.json['rpi.black_level']['black_level'] = Cam.blacklevel_16
Cam.json_remove(disable)
print('\nSTARTING CALIBRATIONS')
- Cam.alsc_cal(luminance_strength, do_alsc_colour)
+ Cam.alsc_cal(luminance_strength, do_alsc_colour, grid_size, max_gain=lsc_max_gain)
Cam.geq_cal()
Cam.lux_cal()
Cam.noise_cal()
- Cam.awb_cal(greyworld, do_alsc_colour)
- Cam.ccm_cal(do_alsc_colour)
+ if "rpi.cac" in json_template:
+ Cam.cac_cal(do_alsc_colour)
+ Cam.awb_cal(greyworld, do_alsc_colour, grid_size)
+ Cam.ccm_cal(do_alsc_colour, grid_size)
+
print('\nFINISHED CALIBRATIONS')
- Cam.write_json()
+ Cam.write_json(target=target, grid_size=grid_size)
Cam.write_log(log_output)
print('\nCalibrations written to: '+json_output)
if log_output is None:
@@ -811,20 +772,19 @@ def run_ctt(json_output, directory, config, log_output, alsc_only=False):
else:
Cam.write_log(log_output)
-
if __name__ == '__main__':
"""
initialise calibration
"""
if len(sys.argv) == 1:
print("""
- Pisp Camera Tuning Tool version 1.0
-
+ PiSP Tuning Tool version 1.0
Required Arguments:
'-i' : Calibration image directory.
'-o' : Name of output json file.
Optional Arguments:
+ '-t' : Target platform - 'pisp' or 'vc4'. Default 'vc4'
'-c' : Config file for the CTT. If not passed, default parameters used.
'-l' : Name of output log file. If not passed, 'ctt_log.txt' used.
""")
@@ -833,5 +793,10 @@ if __name__ == '__main__':
"""
parse input arguments
"""
- json_output, directory, config, log_output = parse_input()
- run_ctt(json_output, directory, config, log_output)
+ json_output, directory, config, log_output, target = parse_input()
+ if target == 'pisp':
+ from ctt_pisp import json_template, grid_size
+ elif target == 'vc4':
+ from ctt_vc4 import json_template, grid_size
+
+ run_ctt(json_output, directory, config, log_output, json_template, grid_size, target)
diff --git a/utils/raspberrypi/ctt/ctt_alsc.py b/utils/raspberrypi/ctt/ctt_alsc.py
index b0201ac4..1d94dfa5 100644
--- a/utils/raspberrypi/ctt/ctt_alsc.py
+++ b/utils/raspberrypi/ctt/ctt_alsc.py
@@ -13,8 +13,9 @@ from mpl_toolkits.mplot3d import Axes3D
"""
preform alsc calibration on a set of images
"""
-def alsc_all(Cam, do_alsc_colour, plot):
+def alsc_all(Cam, do_alsc_colour, plot, grid_size=(16, 12), max_gain=8.0):
imgs_alsc = Cam.imgs_alsc
+ grid_w, grid_h = grid_size
"""
create list of colour temperatures and associated calibration tables
"""
@@ -23,7 +24,7 @@ def alsc_all(Cam, do_alsc_colour, plot):
list_cb = []
list_cg = []
for Img in imgs_alsc:
- col, cr, cb, cg, size = alsc(Cam, Img, do_alsc_colour, plot)
+ col, cr, cb, cg, size = alsc(Cam, Img, do_alsc_colour, plot, grid_size=grid_size, max_gain=max_gain)
list_col.append(col)
list_cr.append(cr)
list_cb.append(cb)
@@ -68,11 +69,12 @@ def alsc_all(Cam, do_alsc_colour, plot):
t_b = np.where((100*t_b) % 1 >= 0.95, t_b-0.001, t_b)
t_r = np.round(t_r, 3)
t_b = np.round(t_b, 3)
- r_corners = (t_r[0], t_r[15], t_r[-1], t_r[-16])
- b_corners = (t_b[0], t_b[15], t_b[-1], t_b[-16])
- r_cen = t_r[5*16+7]+t_r[5*16+8]+t_r[6*16+7]+t_r[6*16+8]
+ r_corners = (t_r[0], t_r[grid_w - 1], t_r[-1], t_r[-grid_w])
+ b_corners = (t_b[0], t_b[grid_w - 1], t_b[-1], t_b[-grid_w])
+ middle_pos = (grid_h // 2 - 1) * grid_w + grid_w - 1
+ r_cen = t_r[middle_pos]+t_r[middle_pos + 1]+t_r[middle_pos + grid_w]+t_r[middle_pos + grid_w + 1]
r_cen = round(r_cen/4, 3)
- b_cen = t_b[5*16+7]+t_b[5*16+8]+t_b[6*16+7]+t_b[6*16+8]
+ b_cen = t_b[middle_pos]+t_b[middle_pos + 1]+t_b[middle_pos + grid_w]+t_b[middle_pos + grid_w + 1]
b_cen = round(b_cen/4, 3)
Cam.log += '\nRed table corners: {}'.format(r_corners)
Cam.log += '\nRed table centre: {}'.format(r_cen)
@@ -116,8 +118,9 @@ def alsc_all(Cam, do_alsc_colour, plot):
"""
calculate g/r and g/b for 32x32 points arranged in a grid for a single image
"""
-def alsc(Cam, Img, do_alsc_colour, plot=False):
+def alsc(Cam, Img, do_alsc_colour, plot=False, grid_size=(16, 12), max_gain=8.0):
Cam.log += '\nProcessing image: ' + Img.name
+ grid_w, grid_h = grid_size
"""
get channel in correct order
"""
@@ -128,31 +131,34 @@ def alsc(Cam, Img, do_alsc_colour, plot=False):
where w is a multiple of 32.
"""
w, h = Img.w/2, Img.h/2
- dx, dy = int(-(-(w-1)//16)), int(-(-(h-1)//12))
+ dx, dy = int(-(-(w-1)//grid_w)), int(-(-(h-1)//grid_h))
"""
average the green channels into one
"""
av_ch_g = np.mean((channels[1:3]), axis=0)
if do_alsc_colour:
"""
- obtain 16x12 grid of intensities for each channel and subtract black level
+ obtain grid_w x grid_h grid of intensities for each channel and subtract black level
"""
- g = get_16x12_grid(av_ch_g, dx, dy) - Img.blacklevel_16
- r = get_16x12_grid(channels[0], dx, dy) - Img.blacklevel_16
- b = get_16x12_grid(channels[3], dx, dy) - Img.blacklevel_16
+ g = get_grid(av_ch_g, dx, dy, grid_size) - Img.blacklevel_16
+ r = get_grid(channels[0], dx, dy, grid_size) - Img.blacklevel_16
+ b = get_grid(channels[3], dx, dy, grid_size) - Img.blacklevel_16
"""
calculate ratios as 32 bit in order to be supported by medianBlur function
"""
- cr = np.reshape(g/r, (12, 16)).astype('float32')
- cb = np.reshape(g/b, (12, 16)).astype('float32')
- cg = np.reshape(1/g, (12, 16)).astype('float32')
+ cr = np.reshape(g/r, (grid_h, grid_w)).astype('float32')
+ cb = np.reshape(g/b, (grid_h, grid_w)).astype('float32')
+ cg = np.reshape(1/g, (grid_h, grid_w)).astype('float32')
"""
median blur to remove peaks and save as float 64
"""
cr = cv2.medianBlur(cr, 3).astype('float64')
+ cr = cr/np.min(cr) # gain tables are easier for humans to read if the minimum is 1.0
cb = cv2.medianBlur(cb, 3).astype('float64')
+ cb = cb/np.min(cb)
cg = cv2.medianBlur(cg, 3).astype('float64')
cg = cg/np.min(cg)
+ cg = [min(v, max_gain) for v in cg.flatten()] # never exceed the max luminance gain
"""
debugging code showing 2D surface plot of vignetting. Quite useful for
@@ -164,7 +170,7 @@ def alsc(Cam, Img, do_alsc_colour, plot=False):
"""
note Y is plotted as -Y so plot has same axes as image
"""
- X, Y = np.meshgrid(range(16), range(12))
+ X, Y = np.meshgrid(range(grid_w), range(grid_h))
ha.plot_surface(X, -Y, cr, cmap=cm.coolwarm, linewidth=0)
ha.set_title('ALSC Plot\nImg: {}\n\ncr'.format(Img.str))
hb = hf.add_subplot(312, projection='3d')
@@ -176,21 +182,22 @@ def alsc(Cam, Img, do_alsc_colour, plot=False):
# print(Img.str)
plt.show()
- return Img.col, cr.flatten(), cb.flatten(), cg.flatten(), (w, h, dx, dy)
+ return Img.col, cr.flatten(), cb.flatten(), cg, (w, h, dx, dy)
else:
"""
only perform calculations for luminance shading
"""
- g = get_16x12_grid(av_ch_g, dx, dy) - Img.blacklevel_16
- cg = np.reshape(1/g, (12, 16)).astype('float32')
+ g = get_grid(av_ch_g, dx, dy, grid_size) - Img.blacklevel_16
+ cg = np.reshape(1/g, (grid_h, grid_w)).astype('float32')
cg = cv2.medianBlur(cg, 3).astype('float64')
cg = cg/np.min(cg)
+ cg = [min(v, max_gain) for v in cg.flatten()] # never exceed the max luminance gain
if plot:
hf = plt.figure(figssize=(8, 8))
ha = hf.add_subplot(1, 1, 1, projection='3d')
- X, Y = np.meashgrid(range(16), range(12))
+ X, Y = np.meashgrid(range(grid_w), range(grid_h))
ha.plot_surface(X, -Y, cg, cmap=cm.coolwarm, linewidth=0)
ha.set_title('ALSC Plot (Luminance only!)\nImg: {}\n\ncg').format(Img.str)
plt.show()
@@ -199,21 +206,22 @@ def alsc(Cam, Img, do_alsc_colour, plot=False):
"""
-Compresses channel down to a 16x12 grid
+Compresses channel down to a grid of the requested size
"""
-def get_16x12_grid(chan, dx, dy):
+def get_grid(chan, dx, dy, grid_size):
+ grid_w, grid_h = grid_size
grid = []
"""
since left and bottom border will not necessarily have rectangles of
dimension dx x dy, the 32nd iteration has to be handled separately.
"""
- for i in range(11):
- for j in range(15):
+ for i in range(grid_h - 1):
+ for j in range(grid_w - 1):
grid.append(np.mean(chan[dy*i:dy*(1+i), dx*j:dx*(1+j)]))
- grid.append(np.mean(chan[dy*i:dy*(1+i), 15*dx:]))
- for j in range(15):
- grid.append(np.mean(chan[11*dy:, dx*j:dx*(1+j)]))
- grid.append(np.mean(chan[11*dy:, 15*dx:]))
+ grid.append(np.mean(chan[dy*i:dy*(1+i), (grid_w - 1)*dx:]))
+ for j in range(grid_w - 1):
+ grid.append(np.mean(chan[(grid_h - 1)*dy:, dx*j:dx*(1+j)]))
+ grid.append(np.mean(chan[(grid_h - 1)*dy:, (grid_w - 1)*dx:]))
"""
return as np.array, ready for further manipulation
"""
@@ -223,7 +231,7 @@ def get_16x12_grid(chan, dx, dy):
"""
obtains sigmas for red and blue, effectively a measure of the 'error'
"""
-def get_sigma(Cam, cal_cr_list, cal_cb_list):
+def get_sigma(Cam, cal_cr_list, cal_cb_list, grid_size):
Cam.log += '\nCalculating sigmas'
"""
provided colour alsc tables were generated for two different colour
@@ -241,8 +249,8 @@ def get_sigma(Cam, cal_cr_list, cal_cb_list):
sigma_rs = []
sigma_bs = []
for i in range(len(cts)-1):
- sigma_rs.append(calc_sigma(cal_cr_list[i]['table'], cal_cr_list[i+1]['table']))
- sigma_bs.append(calc_sigma(cal_cb_list[i]['table'], cal_cb_list[i+1]['table']))
+ sigma_rs.append(calc_sigma(cal_cr_list[i]['table'], cal_cr_list[i+1]['table'], grid_size))
+ sigma_bs.append(calc_sigma(cal_cb_list[i]['table'], cal_cb_list[i+1]['table'], grid_size))
Cam.log += '\nColour temperature interval {} - {} K'.format(cts[i], cts[i+1])
Cam.log += '\nSigma red: {}'.format(sigma_rs[-1])
Cam.log += '\nSigma blue: {}'.format(sigma_bs[-1])
@@ -263,12 +271,13 @@ def get_sigma(Cam, cal_cr_list, cal_cb_list):
"""
calculate sigma from two adjacent gain tables
"""
-def calc_sigma(g1, g2):
+def calc_sigma(g1, g2, grid_size):
+ grid_w, grid_h = grid_size
"""
reshape into 16x12 matrix
"""
- g1 = np.reshape(g1, (12, 16))
- g2 = np.reshape(g2, (12, 16))
+ g1 = np.reshape(g1, (grid_h, grid_w))
+ g2 = np.reshape(g2, (grid_h, grid_w))
"""
apply gains to gain table
"""
@@ -280,8 +289,8 @@ def calc_sigma(g1, g2):
neighbours, then append to list
"""
diffs = []
- for i in range(10):
- for j in range(14):
+ for i in range(grid_h - 2):
+ for j in range(grid_w - 2):
"""
note indexing is incremented by 1 since all patches on borders are
not counted
diff --git a/utils/raspberrypi/ctt/ctt_awb.py b/utils/raspberrypi/ctt/ctt_awb.py
index 5ba6f978..4af1fe41 100644
--- a/utils/raspberrypi/ctt/ctt_awb.py
+++ b/utils/raspberrypi/ctt/ctt_awb.py
@@ -13,7 +13,7 @@ from scipy.optimize import fmin
"""
obtain piecewise linear approximation for colour curve
"""
-def awb(Cam, cal_cr_list, cal_cb_list, plot):
+def awb(Cam, cal_cr_list, cal_cb_list, plot, grid_size):
imgs = Cam.imgs
"""
condense alsc calibration tables into one dictionary
@@ -43,7 +43,7 @@ def awb(Cam, cal_cr_list, cal_cb_list, plot):
Note: if alsc is disabled then colour_cals will be set to None and the
function will just return the greyscale patches
"""
- r_patchs, b_patchs, g_patchs = get_alsc_patches(Img, colour_cals)
+ r_patchs, b_patchs, g_patchs = get_alsc_patches(Img, colour_cals, grid_size=grid_size)
"""
calculate ratio of r, b to g
"""
@@ -293,12 +293,13 @@ def awb(Cam, cal_cr_list, cal_cb_list, plot):
"""
obtain greyscale patches and perform alsc colour correction
"""
-def get_alsc_patches(Img, colour_cals, grey=True):
+def get_alsc_patches(Img, colour_cals, grey=True, grid_size=(16, 12)):
"""
get patch centre coordinates, image colour and the actual
patches for each channel, remembering to subtract blacklevel
If grey then only greyscale patches considered
"""
+ grid_w, grid_h = grid_size
if grey:
cen_coords = Img.cen_coords[3::4]
col = Img.col
@@ -345,12 +346,12 @@ def get_alsc_patches(Img, colour_cals, grey=True):
bef_tabs = np.array(colour_cals[bef])
aft_tabs = np.array(colour_cals[aft])
col_tabs = (bef_tabs*db + aft_tabs*da)/(da+db)
- col_tabs = np.reshape(col_tabs, (2, 12, 16))
+ col_tabs = np.reshape(col_tabs, (2, grid_h, grid_w))
"""
calculate dx, dy used to calculate alsc table
"""
w, h = Img.w/2, Img.h/2
- dx, dy = int(-(-(w-1)//16)), int(-(-(h-1)//12))
+ dx, dy = int(-(-(w-1)//grid_w)), int(-(-(h-1)//grid_h))
"""
make list of pairs of gains for each patch by selecting the correct value
in alsc colour calibration table
diff --git a/utils/raspberrypi/ctt/ctt_cac.py b/utils/raspberrypi/ctt/ctt_cac.py
new file mode 100644
index 00000000..5a4c5101
--- /dev/null
+++ b/utils/raspberrypi/ctt/ctt_cac.py
@@ -0,0 +1,228 @@
+# SPDX-License-Identifier: BSD-2-Clause
+#
+# Copyright (C) 2023, Raspberry Pi Ltd
+#
+# ctt_cac.py - CAC (Chromatic Aberration Correction) tuning tool
+
+from PIL import Image
+import numpy as np
+import matplotlib.pyplot as plt
+from matplotlib import cm
+
+from ctt_dots_locator import find_dots_locations
+
+
+# This is the wrapper file that creates a JSON entry for you to append
+# to your camera tuning file.
+# It calculates the chromatic aberration at different points throughout
+# the image and uses that to produce a martix that can then be used
+# in the camera tuning files to correct this aberration.
+
+
+def pprint_array(array):
+ # Function to print the array in a tidier format
+ array = array
+ output = ""
+ for i in range(len(array)):
+ for j in range(len(array[0])):
+ output += str(round(array[i, j], 2)) + ", "
+ # Add the necessary indentation to the array
+ output += "\n "
+ # Cut off the end of the array (nicely formats it)
+ return output[:-22]
+
+
+def plot_shifts(red_shifts, blue_shifts):
+ # If users want, they can pass a command line option to show the shifts on a graph
+ # Can be useful to check that the functions are all working, and that the sample
+ # images are doing the right thing
+ Xs = np.array(red_shifts)[:, 0]
+ Ys = np.array(red_shifts)[:, 1]
+ Zs = np.array(red_shifts)[:, 2]
+ Zs2 = np.array(red_shifts)[:, 3]
+ Zs3 = np.array(blue_shifts)[:, 2]
+ Zs4 = np.array(blue_shifts)[:, 3]
+
+ fig, axs = plt.subplots(2, 2)
+ ax = fig.add_subplot(2, 2, 1, projection='3d')
+ ax.scatter(Xs, Ys, Zs, cmap=cm.jet, linewidth=0)
+ ax.set_title('Red X Shift')
+ ax = fig.add_subplot(2, 2, 2, projection='3d')
+ ax.scatter(Xs, Ys, Zs2, cmap=cm.jet, linewidth=0)
+ ax.set_title('Red Y Shift')
+ ax = fig.add_subplot(2, 2, 3, projection='3d')
+ ax.scatter(Xs, Ys, Zs3, cmap=cm.jet, linewidth=0)
+ ax.set_title('Blue X Shift')
+ ax = fig.add_subplot(2, 2, 4, projection='3d')
+ ax.scatter(Xs, Ys, Zs4, cmap=cm.jet, linewidth=0)
+ ax.set_title('Blue Y Shift')
+ fig.tight_layout()
+ plt.show()
+
+
+def shifts_to_yaml(red_shift, blue_shift, image_dimensions, output_grid_size=9):
+ # Convert the shifts to a numpy array for easier handling and initialise other variables
+ red_shifts = np.array(red_shift)
+ blue_shifts = np.array(blue_shift)
+ # create a grid that's smaller than the output grid, which we then interpolate from to get the output values
+ xrgrid = np.zeros((output_grid_size - 1, output_grid_size - 1))
+ xbgrid = np.zeros((output_grid_size - 1, output_grid_size - 1))
+ yrgrid = np.zeros((output_grid_size - 1, output_grid_size - 1))
+ ybgrid = np.zeros((output_grid_size - 1, output_grid_size - 1))
+
+ xrsgrid = []
+ xbsgrid = []
+ yrsgrid = []
+ ybsgrid = []
+ xg = np.zeros((output_grid_size - 1, output_grid_size - 1))
+ yg = np.zeros((output_grid_size - 1, output_grid_size - 1))
+
+ # Format the grids - numpy doesn't work for this, it wants a
+ # nice uniformly spaced grid, which we don't know if we have yet, hence the rather mundane setup
+ for x in range(output_grid_size - 1):
+ xrsgrid.append([])
+ yrsgrid.append([])
+ xbsgrid.append([])
+ ybsgrid.append([])
+ for y in range(output_grid_size - 1):
+ xrsgrid[x].append([])
+ yrsgrid[x].append([])
+ xbsgrid[x].append([])
+ ybsgrid[x].append([])
+
+ image_size = (image_dimensions[0], image_dimensions[1])
+ gridxsize = image_size[0] / (output_grid_size - 1)
+ gridysize = image_size[1] / (output_grid_size - 1)
+
+ # Iterate through each dot, and it's shift values and put these into the correct grid location
+ for red_shift in red_shifts:
+ xgridloc = int(red_shift[0] / gridxsize)
+ ygridloc = int(red_shift[1] / gridysize)
+ xrsgrid[xgridloc][ygridloc].append(red_shift[2])
+ yrsgrid[xgridloc][ygridloc].append(red_shift[3])
+
+ for blue_shift in blue_shifts:
+ xgridloc = int(blue_shift[0] / gridxsize)
+ ygridloc = int(blue_shift[1] / gridysize)
+ xbsgrid[xgridloc][ygridloc].append(blue_shift[2])
+ ybsgrid[xgridloc][ygridloc].append(blue_shift[3])
+
+ # Now calculate the average pixel shift for each square in the grid
+ for x in range(output_grid_size - 1):
+ for y in range(output_grid_size - 1):
+ xrgrid[x, y] = np.mean(xrsgrid[x][y])
+ yrgrid[x, y] = np.mean(yrsgrid[x][y])
+ xbgrid[x, y] = np.mean(xbsgrid[x][y])
+ ybgrid[x, y] = np.mean(ybsgrid[x][y])
+
+ # Next, we start to interpolate the central points of the grid that gets passed to the tuning file
+ input_grids = np.array([xrgrid, yrgrid, xbgrid, ybgrid])
+ output_grids = np.zeros((4, output_grid_size, output_grid_size))
+
+ # Interpolate the centre of the grid
+ output_grids[:, 1:-1, 1:-1] = (input_grids[:, 1:, :-1] + input_grids[:, 1:, 1:] + input_grids[:, :-1, 1:] + input_grids[:, :-1, :-1]) / 4
+
+ # Edge cases:
+ output_grids[:, 1:-1, 0] = ((input_grids[:, :-1, 0] + input_grids[:, 1:, 0]) / 2 - output_grids[:, 1:-1, 1]) * 2 + output_grids[:, 1:-1, 1]
+ output_grids[:, 1:-1, -1] = ((input_grids[:, :-1, 7] + input_grids[:, 1:, 7]) / 2 - output_grids[:, 1:-1, -2]) * 2 + output_grids[:, 1:-1, -2]
+ output_grids[:, 0, 1:-1] = ((input_grids[:, 0, :-1] + input_grids[:, 0, 1:]) / 2 - output_grids[:, 1, 1:-1]) * 2 + output_grids[:, 1, 1:-1]
+ output_grids[:, -1, 1:-1] = ((input_grids[:, 7, :-1] + input_grids[:, 7, 1:]) / 2 - output_grids[:, -2, 1:-1]) * 2 + output_grids[:, -2, 1:-1]
+
+ # Corner Cases:
+ output_grids[:, 0, 0] = (output_grids[:, 0, 1] - output_grids[:, 1, 1]) + (output_grids[:, 1, 0] - output_grids[:, 1, 1]) + output_grids[:, 1, 1]
+ output_grids[:, 0, -1] = (output_grids[:, 0, -2] - output_grids[:, 1, -2]) + (output_grids[:, 1, -1] - output_grids[:, 1, -2]) + output_grids[:, 1, -2]
+ output_grids[:, -1, 0] = (output_grids[:, -1, 1] - output_grids[:, -2, 1]) + (output_grids[:, -2, 0] - output_grids[:, -2, 1]) + output_grids[:, -2, 1]
+ output_grids[:, -1, -1] = (output_grids[:, -2, -1] - output_grids[:, -2, -2]) + (output_grids[:, -1, -2] - output_grids[:, -2, -2]) + output_grids[:, -2, -2]
+
+ # Below, we swap the x and the y coordinates, and also multiply by a factor of -1
+ # This is due to the PiSP (standard) dimensions being flipped in comparison to
+ # PIL image coordinate directions, hence why xr -> yr. Also, the shifts calculated are colour shifts,
+ # and the PiSP block asks for the values it should shift by (hence the * -1, to convert from colour shift to a pixel shift)
+
+ output_grid_yr, output_grid_xr, output_grid_yb, output_grid_xb = output_grids * -1
+ return output_grid_xr, output_grid_yr, output_grid_xb, output_grid_yb
+
+
+def analyse_dot(dot, dot_location=[0, 0]):
+ # Scan through the dot, calculate the centroid of each colour channel by doing:
+ # pixel channel brightness * distance from top left corner
+ # Sum these, and divide by the sum of each channel's brightnesses to get a centroid for each channel
+ red_channel = np.array(dot)[:, :, 0]
+ y_num_pixels = len(red_channel[0])
+ x_num_pixels = len(red_channel)
+ yred_weight = np.sum(np.dot(red_channel, np.arange(y_num_pixels)))
+ xred_weight = np.sum(np.dot(np.arange(x_num_pixels), red_channel))
+ red_sum = np.sum(red_channel)
+
+ green_channel = np.array(dot)[:, :, 1]
+ ygreen_weight = np.sum(np.dot(green_channel, np.arange(y_num_pixels)))
+ xgreen_weight = np.sum(np.dot(np.arange(x_num_pixels), green_channel))
+ green_sum = np.sum(green_channel)
+
+ blue_channel = np.array(dot)[:, :, 2]
+ yblue_weight = np.sum(np.dot(blue_channel, np.arange(y_num_pixels)))
+ xblue_weight = np.sum(np.dot(np.arange(x_num_pixels), blue_channel))
+ blue_sum = np.sum(blue_channel)
+
+ # We return this structure. It contains 2 arrays that contain:
+ # the locations of the dot center, along with the channel shifts in the x and y direction:
+ # [ [red_center_x, red_center_y, red_x_shift, red_y_shift], [blue_center_x, blue_center_y, blue_x_shift, blue_y_shift] ]
+
+ return [[int(dot_location[0]) + int(len(dot) / 2), int(dot_location[1]) + int(len(dot[0]) / 2), xred_weight / red_sum - xgreen_weight / green_sum, yred_weight / red_sum - ygreen_weight / green_sum], [dot_location[0] + int(len(dot) / 2), dot_location[1] + int(len(dot[0]) / 2), xblue_weight / blue_sum - xgreen_weight / green_sum, yblue_weight / blue_sum - ygreen_weight / green_sum]]
+
+
+def cac(Cam):
+ filelist = Cam.imgs_cac
+
+ Cam.log += '\nCAC analysing files: {}'.format(str(filelist))
+ np.set_printoptions(precision=3)
+ np.set_printoptions(suppress=True)
+
+ # Create arrays to hold all the dots data and their colour offsets
+ red_shift = [] # Format is: [[Dot Center X, Dot Center Y, x shift, y shift]]
+ blue_shift = []
+ # Iterate through the files
+ # Multiple files is reccomended to average out the lens aberration through rotations
+ for file in filelist:
+ Cam.log += '\nCAC processing file'
+ print("\n Processing file")
+ # Read the raw RGB values
+ rgb = file.rgb
+ image_size = [file.h, file.w] # Image size, X, Y
+ # Create a colour copy of the RGB values to use later in the calibration
+ imout = Image.new(mode="RGB", size=image_size)
+ rgb_image = np.array(imout)
+ # The rgb values need reshaping from a 1d array to a 3d array to be worked with easily
+ rgb.reshape((image_size[0], image_size[1], 3))
+ rgb_image = rgb
+
+ # Pass the RGB image through to the dots locating program
+ # Returns an array of the dots (colour rectangles around the dots), and an array of their locations
+ print("Finding dots")
+ Cam.log += '\nFinding dots'
+ dots, dots_locations = find_dots_locations(rgb_image)
+
+ # Now, analyse each dot. Work out the centroid of each colour channel, and use that to work out
+ # by how far the chromatic aberration has shifted each channel
+ Cam.log += '\nDots found: {}'.format(str(len(dots)))
+ print('Dots found: ' + str(len(dots)))
+
+ for dot, dot_location in zip(dots, dots_locations):
+ if len(dot) > 0:
+ if (dot_location[0] > 0) and (dot_location[1] > 0):
+ ret = analyse_dot(dot, dot_location)
+ red_shift.append(ret[0])
+ blue_shift.append(ret[1])
+
+ # Take our arrays of red shifts and locations, push them through to be interpolated into a 9x9 matrix
+ # for the CAC block to handle and then store these as a .json file to be added to the camera
+ # tuning file
+ print("\nCreating output grid")
+ Cam.log += '\nCreating output grid'
+ rx, ry, bx, by = shifts_to_yaml(red_shift, blue_shift, image_size)
+
+ print("CAC correction complete!")
+ Cam.log += '\nCAC correction complete!'
+
+ # Give the JSON dict back to the main ctt program
+ return {"strength": 1.0, "lut_rx": list(rx.round(2).reshape(81)), "lut_ry": list(ry.round(2).reshape(81)), "lut_bx": list(bx.round(2).reshape(81)), "lut_by": list(by.round(2).reshape(81))}
diff --git a/utils/raspberrypi/ctt/ctt_ccm.py b/utils/raspberrypi/ctt/ctt_ccm.py
index 59753e33..07c943a8 100644
--- a/utils/raspberrypi/ctt/ctt_ccm.py
+++ b/utils/raspberrypi/ctt/ctt_ccm.py
@@ -56,7 +56,7 @@ FInds colour correction matrices for list of images
"""
-def ccm(Cam, cal_cr_list, cal_cb_list):
+def ccm(Cam, cal_cr_list, cal_cb_list, grid_size):
global matrix_selection_types, typenum
imgs = Cam.imgs
"""
@@ -133,9 +133,7 @@ def ccm(Cam, cal_cr_list, cal_cb_list):
Note: if alsc is disabled then colour_cals will be set to None and no
the function will simply return the macbeth patches
"""
- r, b, g = get_alsc_patches(Img, colour_cals, grey=False)
- # 256 values for each patch of sRGB values
-
+ r, b, g = get_alsc_patches(Img, colour_cals, grey=False, grid_size=grid_size)
"""
do awb
Note: awb is done by measuring the macbeth chart in the image, rather
diff --git a/utils/raspberrypi/ctt/ctt_config_example.json b/utils/raspberrypi/ctt/ctt_config_example.json
index c7f90761..1105862c 100644
--- a/utils/raspberrypi/ctt/ctt_config_example.json
+++ b/utils/raspberrypi/ctt/ctt_config_example.json
@@ -3,7 +3,8 @@
"plot": [],
"alsc": {
"do_alsc_colour": 1,
- "luminance_strength": 0.5
+ "luminance_strength": 0.8,
+ "max_gain": 8.0
},
"awb": {
"greyworld": 0
@@ -13,4 +14,4 @@
"small": 0,
"show": 0
}
-} \ No newline at end of file
+}
diff --git a/utils/raspberrypi/ctt/ctt_dots_locator.py b/utils/raspberrypi/ctt/ctt_dots_locator.py
new file mode 100644
index 00000000..4945c04b
--- /dev/null
+++ b/utils/raspberrypi/ctt/ctt_dots_locator.py
@@ -0,0 +1,118 @@
+# SPDX-License-Identifier: BSD-2-Clause
+#
+# Copyright (C) 2023, Raspberry Pi Ltd
+#
+# find_dots.py - Used by CAC algorithm to convert image to set of dots
+
+'''
+This file takes the black and white version of the image, along with
+the color version. It then located the black dots on the image by
+thresholding dark pixels.
+In a rather fun way, the algorithm bounces around the thresholded area in a random path
+We then use the maximum and minimum of these paths to determine the dot shape and size
+This info is then used to return colored dots and locations back to the main file
+'''
+
+import numpy as np
+import random
+from PIL import Image, ImageEnhance, ImageFilter
+
+
+def find_dots_locations(rgb_image, color_threshold=100, dots_edge_avoid=75, image_edge_avoid=10, search_path_length=500, grid_scan_step_size=10, logfile=open("log.txt", "a+")):
+ # Initialise some starting variables
+ pixels = Image.fromarray(rgb_image)
+ pixels = pixels.convert("L")
+ enhancer = ImageEnhance.Contrast(pixels)
+ im_output = enhancer.enhance(1.4)
+ # We smooth it slightly to make it easier for the dot recognition program to locate the dots
+ im_output = im_output.filter(ImageFilter.GaussianBlur(radius=2))
+ bw_image = np.array(im_output)
+
+ location = [0, 0]
+ dots = []
+ dots_location = []
+ # the program takes away the edges - we don't want a dot that is half a circle, the
+ # centroids would all be wrong
+ for x in range(dots_edge_avoid, len(bw_image) - dots_edge_avoid, grid_scan_step_size):
+ for y in range(dots_edge_avoid, len(bw_image[0]) - dots_edge_avoid, grid_scan_step_size):
+ location = [x, y]
+ scrap_dot = False # A variable used to make sure that this is a valid dot
+ if (bw_image[location[0], location[1]] < color_threshold) and not (scrap_dot):
+ heading = "south" # Define a starting direction to move in
+ coords = []
+ for i in range(search_path_length): # Creates a path of length `search_path_length`. This turns out to always be enough to work out the rough shape of the dot.
+ # Now make sure that the thresholded area doesn't come within 10 pixels of the edge of the image, ensures we capture all the CA
+ if ((image_edge_avoid < location[0] < len(bw_image) - image_edge_avoid) and (image_edge_avoid < location[1] < len(bw_image[0]) - image_edge_avoid)) and not (scrap_dot):
+ if heading == "south":
+ if bw_image[location[0] + 1, location[1]] < color_threshold:
+ # Here, notice it does not go south, but actually goes southeast
+ # This is crucial in ensuring that we make our way around the majority of the dot
+ location[0] = location[0] + 1
+ location[1] = location[1] + 1
+ heading = "south"
+ else:
+ # This happens when we reach a thresholded edge. We now randomly change direction and keep searching
+ dir = random.randint(1, 2)
+ if dir == 1:
+ heading = "west"
+ if dir == 2:
+ heading = "east"
+
+ if heading == "east":
+ if bw_image[location[0], location[1] + 1] < color_threshold:
+ location[1] = location[1] + 1
+ heading = "east"
+ else:
+ dir = random.randint(1, 2)
+ if dir == 1:
+ heading = "north"
+ if dir == 2:
+ heading = "south"
+
+ if heading == "west":
+ if bw_image[location[0], location[1] - 1] < color_threshold:
+ location[1] = location[1] - 1
+ heading = "west"
+ else:
+ dir = random.randint(1, 2)
+ if dir == 1:
+ heading = "north"
+ if dir == 2:
+ heading = "south"
+
+ if heading == "north":
+ if bw_image[location[0] - 1, location[1]] < color_threshold:
+ location[0] = location[0] - 1
+ heading = "north"
+ else:
+ dir = random.randint(1, 2)
+ if dir == 1:
+ heading = "west"
+ if dir == 2:
+ heading = "east"
+ # Log where our particle travels across the dot
+ coords.append([location[0], location[1]])
+ else:
+ scrap_dot = True # We just don't have enough space around the dot, discard this one, and move on
+ if not scrap_dot:
+ # get the size of the dot surrounding the dot
+ x_coords = np.array(coords)[:, 0]
+ y_coords = np.array(coords)[:, 1]
+ hsquaresize = max(list(x_coords)) - min(list(x_coords))
+ vsquaresize = max(list(y_coords)) - min(list(y_coords))
+ # Create the bounding coordinates of the rectangle surrounding the dot
+ # Program uses the dotsize + half of the dotsize to ensure we get all that color fringing
+ extra_space_factor = 0.45
+ top_left_x = (min(list(x_coords)) - int(hsquaresize * extra_space_factor))
+ btm_right_x = max(list(x_coords)) + int(hsquaresize * extra_space_factor)
+ top_left_y = (min(list(y_coords)) - int(vsquaresize * extra_space_factor))
+ btm_right_y = max(list(y_coords)) + int(vsquaresize * extra_space_factor)
+ # Overwrite the area of the dot to ensure we don't use it again
+ bw_image[top_left_x:btm_right_x, top_left_y:btm_right_y] = 255
+ # Add the color version of the dot to the list to send off, along with some coordinates.
+ dots.append(rgb_image[top_left_x:btm_right_x, top_left_y:btm_right_y])
+ dots_location.append([top_left_x, top_left_y])
+ else:
+ # Dot was too close to the image border to be useable
+ pass
+ return dots, dots_location
diff --git a/utils/raspberrypi/ctt/ctt_image_load.py b/utils/raspberrypi/ctt/ctt_image_load.py
index d76ece73..531de328 100644
--- a/utils/raspberrypi/ctt/ctt_image_load.py
+++ b/utils/raspberrypi/ctt/ctt_image_load.py
@@ -350,6 +350,7 @@ def dng_load_image(Cam, im_str):
c2 = np.left_shift(raw_data[1::2, 0::2].astype(np.int64), shift)
c3 = np.left_shift(raw_data[1::2, 1::2].astype(np.int64), shift)
Img.channels = [c0, c1, c2, c3]
+ Img.rgb = raw_im.postprocess()
except Exception:
print("\nERROR: failed to load DNG file", im_str)
diff --git a/utils/raspberrypi/ctt/ctt_pisp.py b/utils/raspberrypi/ctt/ctt_pisp.py
new file mode 100755
index 00000000..a59b053c
--- /dev/null
+++ b/utils/raspberrypi/ctt/ctt_pisp.py
@@ -0,0 +1,805 @@
+#!/usr/bin/env python3
+#
+# SPDX-License-Identifier: BSD-2-Clause
+#
+# Copyright (C) 2019, Raspberry Pi Ltd
+#
+# ctt_pisp.py - camera tuning tool data for PiSP platforms
+
+
+json_template = {
+ "rpi.black_level": {
+ "black_level": 4096
+ },
+ "rpi.lux": {
+ "reference_shutter_speed": 10000,
+ "reference_gain": 1,
+ "reference_aperture": 1.0
+ },
+ "rpi.dpc": {
+ "strength": 1
+ },
+ "rpi.noise": {
+ },
+ "rpi.geq": {
+ },
+ "rpi.denoise":
+ {
+ "normal":
+ {
+ "sdn":
+ {
+ "deviation": 1.6,
+ "strength": 0.5,
+ "deviation2": 3.2,
+ "deviation_no_tdn": 3.2,
+ "strength_no_tdn": 0.75
+ },
+ "cdn":
+ {
+ "deviation": 200,
+ "strength": 0.3
+ },
+ "tdn":
+ {
+ "deviation": 0.8,
+ "threshold": 0.05
+ }
+ },
+ "hdr":
+ {
+ "sdn":
+ {
+ "deviation": 1.6,
+ "strength": 0.5,
+ "deviation2": 3.2,
+ "deviation_no_tdn": 3.2,
+ "strength_no_tdn": 0.75
+ },
+ "cdn":
+ {
+ "deviation": 200,
+ "strength": 0.3
+ },
+ "tdn":
+ {
+ "deviation": 1.3,
+ "threshold": 0.1
+ }
+ },
+ "night":
+ {
+ "sdn":
+ {
+ "deviation": 1.6,
+ "strength": 0.5,
+ "deviation2": 3.2,
+ "deviation_no_tdn": 3.2,
+ "strength_no_tdn": 0.75
+ },
+ "cdn":
+ {
+ "deviation": 200,
+ "strength": 0.3
+ },
+ "tdn":
+ {
+ "deviation": 1.3,
+ "threshold": 0.1
+ }
+ }
+ },
+ "rpi.awb": {
+ "priors": [
+ {"lux": 0, "prior": [2000, 1.0, 3000, 0.0, 13000, 0.0]},
+ {"lux": 800, "prior": [2000, 0.0, 6000, 2.0, 13000, 2.0]},
+ {"lux": 1500, "prior": [2000, 0.0, 4000, 1.0, 6000, 6.0, 6500, 7.0, 7000, 1.0, 13000, 1.0]}
+ ],
+ "modes": {
+ "auto": {"lo": 2500, "hi": 7700},
+ "incandescent": {"lo": 2500, "hi": 3000},
+ "tungsten": {"lo": 3000, "hi": 3500},
+ "fluorescent": {"lo": 4000, "hi": 4700},
+ "indoor": {"lo": 3000, "hi": 5000},
+ "daylight": {"lo": 5500, "hi": 6500},
+ "cloudy": {"lo": 7000, "hi": 8000}
+ },
+ "bayes": 1
+ },
+ "rpi.agc":
+ {
+ "channels":
+ [
+ {
+ "comment": "Channel 0 is normal AGC",
+ "metering_modes":
+ {
+ "centre-weighted":
+ {
+ "weights":
+ [
+ 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0,
+ 0, 1, 1, 1, 1, 1, 2, 2, 2, 1, 1, 1, 1, 1, 0,
+ 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1,
+ 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1,
+ 1, 1, 2, 2, 2, 2, 3, 3, 3, 2, 2, 2, 2, 1, 1,
+ 1, 1, 2, 2, 2, 3, 3, 3, 3, 3, 2, 2, 2, 1, 1,
+ 1, 1, 2, 2, 3, 3, 3, 4, 3, 3, 3, 2, 2, 1, 1,
+ 1, 1, 2, 2, 3, 3, 4, 4, 4, 3, 3, 2, 2, 1, 1,
+ 1, 1, 2, 2, 3, 3, 3, 4, 3, 3, 3, 2, 2, 1, 1,
+ 1, 1, 2, 2, 2, 3, 3, 3, 3, 3, 2, 2, 2, 1, 1,
+ 1, 1, 2, 2, 2, 2, 3, 3, 3, 2, 2, 2, 2, 1, 1,
+ 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1,
+ 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1,
+ 0, 1, 1, 1, 1, 1, 2, 2, 2, 1, 1, 1, 1, 1, 0,
+ 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0
+ ]
+ },
+ "spot":
+ {
+ "weights":
+ [
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 1, 2, 1, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 1, 2, 3, 2, 1, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 1, 2, 1, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
+ ]
+ },
+ "matrix":
+ {
+ "weights":
+ [
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1
+ ]
+ }
+ },
+ "exposure_modes":
+ {
+ "normal":
+ {
+ "shutter": [ 100, 10000, 30000, 60000, 66666 ],
+ "gain": [ 1.0, 1.5, 2.0, 4.0, 8.0 ]
+ },
+ "short":
+ {
+ "shutter": [ 100, 5000, 10000, 20000, 60000 ],
+ "gain": [ 1.0, 1.5, 2.0, 4.0, 8.0 ]
+ },
+ "long":
+ {
+ "shutter": [ 100, 10000, 30000, 60000, 90000, 120000 ],
+ "gain": [ 1.0, 1.5, 2.0, 4.0, 8.0, 12.0 ]
+ }
+ },
+ "constraint_modes":
+ {
+ "normal": [
+ {
+ "bound": "LOWER",
+ "q_lo": 0.98,
+ "q_hi": 1.0,
+ "y_target":
+ [
+ 0, 0.5,
+ 1000, 0.5
+ ]
+ }
+ ],
+ "highlight": [
+ {
+ "bound": "LOWER",
+ "q_lo": 0.98,
+ "q_hi": 1.0,
+ "y_target":
+ [
+ 0, 0.5,
+ 1000, 0.5
+ ]
+ },
+ {
+ "bound": "UPPER",
+ "q_lo": 0.98,
+ "q_hi": 1.0,
+ "y_target":
+ [
+ 0, 0.8,
+ 1000, 0.8
+ ]
+ },
+ ],
+ "shadows": [
+ {
+ "bound": "LOWER",
+ "q_lo": 0.0,
+ "q_hi": 0.5,
+ "y_target":
+ [
+ 0, 0.17,
+ 1000, 0.17
+ ]
+ }
+ ]
+ },
+ "y_target":
+ [
+ 0, 0.16,
+ 1000, 0.165,
+ 10000, 0.17
+ ]
+ },
+ {
+ "comment": "Channel 1 is the HDR short channel",
+ "desaturate": 0,
+ "metering_modes":
+ {
+ "centre-weighted":
+ {
+ "weights":
+ [
+ 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0,
+ 0, 1, 1, 1, 1, 1, 2, 2, 2, 1, 1, 1, 1, 1, 0,
+ 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1,
+ 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1,
+ 1, 1, 2, 2, 2, 2, 3, 3, 3, 2, 2, 2, 2, 1, 1,
+ 1, 1, 2, 2, 2, 3, 3, 3, 3, 3, 2, 2, 2, 1, 1,
+ 1, 1, 2, 2, 3, 3, 3, 4, 3, 3, 3, 2, 2, 1, 1,
+ 1, 1, 2, 2, 3, 3, 4, 4, 4, 3, 3, 2, 2, 1, 1,
+ 1, 1, 2, 2, 3, 3, 3, 4, 3, 3, 3, 2, 2, 1, 1,
+ 1, 1, 2, 2, 2, 3, 3, 3, 3, 3, 2, 2, 2, 1, 1,
+ 1, 1, 2, 2, 2, 2, 3, 3, 3, 2, 2, 2, 2, 1, 1,
+ 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1,
+ 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1,
+ 0, 1, 1, 1, 1, 1, 2, 2, 2, 1, 1, 1, 1, 1, 0,
+ 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0
+ ]
+ },
+ "spot":
+ {
+ "weights":
+ [
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 1, 2, 1, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 1, 2, 3, 2, 1, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 1, 2, 1, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
+ ]
+ },
+ "matrix":
+ {
+ "weights":
+ [
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1
+ ]
+ }
+ },
+ "exposure_modes":
+ {
+ "normal":
+ {
+ "shutter": [ 100, 20000, 60000 ],
+ "gain": [ 1.0, 1.0, 1.0 ]
+ },
+ "short":
+ {
+ "shutter": [ 100, 20000, 60000 ],
+ "gain": [ 1.0, 1.0, 1.0 ]
+ },
+ "long":
+ {
+ "shutter": [ 100, 20000, 60000 ],
+ "gain": [ 1.0, 1.0, 1.0 ]
+ }
+ },
+ "constraint_modes":
+ {
+ "normal": [
+ {
+ "bound": "LOWER",
+ "q_lo": 0.95,
+ "q_hi": 1.0,
+ "y_target":
+ [
+ 0, 0.5,
+ 1000, 0.5
+ ]
+ },
+ {
+ "bound": "UPPER",
+ "q_lo": 0.95,
+ "q_hi": 1.0,
+ "y_target":
+ [
+ 0, 0.7,
+ 1000, 0.7
+ ]
+ },
+ {
+ "bound": "LOWER",
+ "q_lo": 0.0,
+ "q_hi": 0.2,
+ "y_target":
+ [
+ 0, 0.002,
+ 1000, 0.002
+ ]
+ }
+ ],
+ "highlight": [
+ {
+ "bound": "LOWER",
+ "q_lo": 0.95,
+ "q_hi": 1.0,
+ "y_target":
+ [
+ 0, 0.5,
+ 1000, 0.5
+ ]
+ },
+ {
+ "bound": "UPPER",
+ "q_lo": 0.95,
+ "q_hi": 1.0,
+ "y_target":
+ [
+ 0, 0.7,
+ 1000, 0.7
+ ]
+ },
+ {
+ "bound": "LOWER",
+ "q_lo": 0.0,
+ "q_hi": 0.2,
+ "y_target":
+ [
+ 0, 0.002,
+ 1000, 0.002
+ ]
+ }
+ ],
+ "shadows": [
+ {
+ "bound": "LOWER",
+ "q_lo": 0.95,
+ "q_hi": 1.0,
+ "y_target":
+ [
+ 0, 0.5,
+ 1000, 0.5
+ ]
+ },
+ {
+ "bound": "UPPER",
+ "q_lo": 0.95,
+ "q_hi": 1.0,
+ "y_target":
+ [
+ 0, 0.7,
+ 1000, 0.7
+ ]
+ },
+ {
+ "bound": "LOWER",
+ "q_lo": 0.0,
+ "q_hi": 0.2,
+ "y_target":
+ [
+ 0, 0.002,
+ 1000, 0.002
+ ]
+ }
+ ]
+ },
+ "y_target":
+ [
+ 0, 0.16,
+ 1000, 0.165,
+ 10000, 0.17
+ ]
+ },
+ {
+ "comment": "Channel 2 is the HDR long channel",
+ "desaturate": 0,
+ "metering_modes":
+ {
+ "centre-weighted":
+ {
+ "weights":
+ [
+ 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0,
+ 0, 1, 1, 1, 1, 1, 2, 2, 2, 1, 1, 1, 1, 1, 0,
+ 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1,
+ 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1,
+ 1, 1, 2, 2, 2, 2, 3, 3, 3, 2, 2, 2, 2, 1, 1,
+ 1, 1, 2, 2, 2, 3, 3, 3, 3, 3, 2, 2, 2, 1, 1,
+ 1, 1, 2, 2, 3, 3, 3, 4, 3, 3, 3, 2, 2, 1, 1,
+ 1, 1, 2, 2, 3, 3, 4, 4, 4, 3, 3, 2, 2, 1, 1,
+ 1, 1, 2, 2, 3, 3, 3, 4, 3, 3, 3, 2, 2, 1, 1,
+ 1, 1, 2, 2, 2, 3, 3, 3, 3, 3, 2, 2, 2, 1, 1,
+ 1, 1, 2, 2, 2, 2, 3, 3, 3, 2, 2, 2, 2, 1, 1,
+ 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1,
+ 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1,
+ 0, 1, 1, 1, 1, 1, 2, 2, 2, 1, 1, 1, 1, 1, 0,
+ 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0
+ ]
+ },
+ "spot":
+ {
+ "weights":
+ [
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 1, 2, 1, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 1, 2, 3, 2, 1, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 1, 2, 1, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
+ ]
+ },
+ "matrix":
+ {
+ "weights":
+ [
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1
+ ]
+ }
+ },
+ "exposure_modes":
+ {
+ "normal":
+ {
+ "shutter": [ 100, 20000, 30000, 60000 ],
+ "gain": [ 1.0, 2.0, 4.0, 8.0 ]
+ },
+ "short":
+ {
+ "shutter": [ 100, 20000, 30000, 60000 ],
+ "gain": [ 1.0, 2.0, 4.0, 8.0 ]
+ },
+ "long":
+ {
+ "shutter": [ 100, 20000, 30000, 60000 ],
+ "gain": [ 1.0, 2.0, 4.0, 8.0 ]
+ }
+ },
+ "constraint_modes":
+ {
+ "normal": [
+ ],
+ "highlight": [
+ ],
+ "shadows": [
+ ]
+ },
+ "channel_constraints":
+ [
+ {
+ "bound": "UPPER",
+ "channel": 4,
+ "factor": 8
+ },
+ {
+ "bound": "LOWER",
+ "channel": 4,
+ "factor": 2
+ }
+ ],
+ "y_target":
+ [
+ 0, 0.16,
+ 1000, 0.165,
+ 10000, 0.17
+ ]
+ },
+ {
+ "comment": "Channel 3 is the night mode channel",
+ "base_ev": 0.33,
+ "metering_modes":
+ {
+ "centre-weighted":
+ {
+ "weights":
+ [
+ 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0,
+ 0, 1, 1, 1, 1, 1, 2, 2, 2, 1, 1, 1, 1, 1, 0,
+ 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1,
+ 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1,
+ 1, 1, 2, 2, 2, 2, 3, 3, 3, 2, 2, 2, 2, 1, 1,
+ 1, 1, 2, 2, 2, 3, 3, 3, 3, 3, 2, 2, 2, 1, 1,
+ 1, 1, 2, 2, 3, 3, 3, 4, 3, 3, 3, 2, 2, 1, 1,
+ 1, 1, 2, 2, 3, 3, 4, 4, 4, 3, 3, 2, 2, 1, 1,
+ 1, 1, 2, 2, 3, 3, 3, 4, 3, 3, 3, 2, 2, 1, 1,
+ 1, 1, 2, 2, 2, 3, 3, 3, 3, 3, 2, 2, 2, 1, 1,
+ 1, 1, 2, 2, 2, 2, 3, 3, 3, 2, 2, 2, 2, 1, 1,
+ 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1,
+ 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1,
+ 0, 1, 1, 1, 1, 1, 2, 2, 2, 1, 1, 1, 1, 1, 0,
+ 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0
+ ]
+ },
+ "spot":
+ {
+ "weights":
+ [
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 1, 2, 1, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 1, 2, 3, 2, 1, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 1, 2, 1, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
+ ]
+ },
+ "matrix":
+ {
+ "weights":
+ [
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1
+ ]
+ }
+ },
+ "exposure_modes":
+ {
+ "normal":
+ {
+ "shutter": [ 100, 20000, 66666 ],
+ "gain": [ 1.0, 2.0, 4.0 ]
+ },
+ "short":
+ {
+ "shutter": [ 100, 20000, 33333 ],
+ "gain": [ 1.0, 2.0, 4.0 ]
+ },
+ "long":
+ {
+ "shutter": [ 100, 20000, 66666, 120000 ],
+ "gain": [ 1.0, 2.0, 4.0, 4.0 ]
+ }
+ },
+ "constraint_modes":
+ {
+ "normal": [
+ {
+ "bound": "LOWER",
+ "q_lo": 0.98,
+ "q_hi": 1.0,
+ "y_target":
+ [
+ 0, 0.5,
+ 1000, 0.5
+ ]
+ }
+ ],
+ "highlight": [
+ {
+ "bound": "LOWER",
+ "q_lo": 0.98,
+ "q_hi": 1.0,
+ "y_target":
+ [
+ 0, 0.5,
+ 1000, 0.5
+ ]
+ },
+ {
+ "bound": "UPPER",
+ "q_lo": 0.98,
+ "q_hi": 1.0,
+ "y_target":
+ [
+ 0, 0.8,
+ 1000, 0.8
+ ]
+ }
+ ],
+ "shadows": [
+ {
+ "bound": "LOWER",
+ "q_lo": 0.98,
+ "q_hi": 1.0,
+ "y_target":
+ [
+ 0, 0.5,
+ 1000, 0.5
+ ]
+ }
+ ]
+ },
+ "y_target":
+ [
+ 0, 0.16,
+ 1000, 0.16,
+ 10000, 0.17
+ ]
+ }
+ ]
+ },
+ "rpi.alsc": {
+ 'omega': 1.3,
+ 'n_iter': 100,
+ 'luminance_strength': 0.8,
+ },
+ "rpi.contrast": {
+ "ce_enable": 1,
+ "gamma_curve": [
+ 0, 0,
+ 1024, 5040,
+ 2048, 9338,
+ 3072, 12356,
+ 4096, 15312,
+ 5120, 18051,
+ 6144, 20790,
+ 7168, 23193,
+ 8192, 25744,
+ 9216, 27942,
+ 10240, 30035,
+ 11264, 32005,
+ 12288, 33975,
+ 13312, 35815,
+ 14336, 37600,
+ 15360, 39168,
+ 16384, 40642,
+ 18432, 43379,
+ 20480, 45749,
+ 22528, 47753,
+ 24576, 49621,
+ 26624, 51253,
+ 28672, 52698,
+ 30720, 53796,
+ 32768, 54876,
+ 36864, 57012,
+ 40960, 58656,
+ 45056, 59954,
+ 49152, 61183,
+ 53248, 62355,
+ 57344, 63419,
+ 61440, 64476,
+ 65535, 65535
+ ]
+ },
+ "rpi.ccm": {
+ },
+ "rpi.cac": {
+ },
+ "rpi.sharpen": {
+ "threshold": 0.25,
+ "limit": 1.0,
+ "strength": 1.0
+ },
+ "rpi.hdr":
+ {
+ "Off":
+ {
+ "cadence": [ 0 ]
+ },
+ "MultiExposureUnmerged":
+ {
+ "cadence": [ 1, 2 ],
+ "channel_map": { "short": 1, "long": 2 }
+ },
+ "SingleExposure":
+ {
+ "cadence": [1],
+ "channel_map": { "short": 1 },
+ "spatial_gain": 2.0,
+ "tonemap_enable": 1
+ },
+ "MultiExposure":
+ {
+ "cadence": [1, 2],
+ "channel_map": { "short": 1, "long": 2 },
+ "stitch_enable": 1,
+ "spatial_gain": 2.0,
+ "tonemap_enable": 1
+ },
+ "Night":
+ {
+ "cadence": [ 3 ],
+ "channel_map": { "night": 3 },
+ "tonemap_enable": 1,
+ "tonemap":
+ [
+ 0, 0,
+ 5000, 20000,
+ 10000, 30000,
+ 20000, 47000,
+ 30000, 55000,
+ 65535, 65535
+ ]
+ }
+ }
+}
+
+grid_size = (32, 32)
diff --git a/utils/raspberrypi/ctt/ctt_pretty_print_json.py b/utils/raspberrypi/ctt/ctt_pretty_print_json.py
index 3e3b8475..a4cae62d 100755
--- a/utils/raspberrypi/ctt/ctt_pretty_print_json.py
+++ b/utils/raspberrypi/ctt/ctt_pretty_print_json.py
@@ -19,13 +19,19 @@ class Encoder(json.JSONEncoder):
self.indentation_level = 0
self.hard_break = 120
self.custom_elems = {
+ 'weights': 15,
'table': 16,
'luminance_lut': 16,
'ct_curve': 3,
'ccm': 3,
+ 'lut_rx': 9,
+ 'lut_bx': 9,
+ 'lut_by': 9,
+ 'lut_ry': 9,
'gamma_curve': 2,
'y_target': 2,
- 'prior': 2
+ 'prior': 2,
+ 'tonemap': 2
}
def encode(self, o, node_key=None):
@@ -87,7 +93,7 @@ class Encoder(json.JSONEncoder):
return self.encode(o)
-def pretty_print(in_json: dict) -> str:
+def pretty_print(in_json: dict, custom_elems={}) -> str:
if 'version' not in in_json or \
'target' not in in_json or \
@@ -95,12 +101,15 @@ def pretty_print(in_json: dict) -> str:
in_json['version'] < 2.0:
raise RuntimeError('Incompatible JSON dictionary has been provided')
- return json.dumps(in_json, cls=Encoder, indent=4, sort_keys=False)
+ encoder = Encoder(indent=4, sort_keys=False)
+ encoder.custom_elems |= custom_elems
+ return encoder.encode(in_json) #json.dumps(in_json, cls=Encoder, indent=4, sort_keys=False)
if __name__ == "__main__":
parser = argparse.ArgumentParser(formatter_class=argparse.RawTextHelpFormatter, description=
'Prettify a version 2.0 camera tuning config JSON file.')
+ parser.add_argument('-t', '--target', type=str, help='Target platform', choices=['pisp', 'vc4'], default='vc4')
parser.add_argument('input', type=str, help='Input tuning file.')
parser.add_argument('output', type=str, nargs='?',
help='Output converted tuning file. If not provided, the input file will be updated in-place.',
@@ -110,7 +119,12 @@ if __name__ == "__main__":
with open(args.input, 'r') as f:
in_json = json.load(f)
- out_json = pretty_print(in_json)
+ if args.target == 'pisp':
+ from ctt_pisp import grid_size
+ elif args.target == 'vc4':
+ from ctt_vc4 import grid_size
+
+ out_json = pretty_print(in_json, custom_elems={'table': grid_size[0], 'luminance_lut': grid_size[0]})
with open(args.output if args.output is not None else args.input, 'w') as f:
f.write(out_json)
diff --git a/utils/raspberrypi/ctt/ctt_tools.py b/utils/raspberrypi/ctt/ctt_tools.py
index 27c52193..50b01ecf 100644
--- a/utils/raspberrypi/ctt/ctt_tools.py
+++ b/utils/raspberrypi/ctt/ctt_tools.py
@@ -65,11 +65,12 @@ def parse_input():
directory = get_config(args_dict, '-i', None, 'string')
config = get_config(args_dict, '-c', None, 'string')
log_path = get_config(args_dict, '-l', None, 'string')
+ target = get_config(args_dict, '-t', "vc4", 'string')
if directory is None:
raise ArgError('\n\nERROR! No input directory given.')
if json_output is None:
raise ArgError('\n\nERROR! No output json given.')
- return json_output, directory, config, log_path
+ return json_output, directory, config, log_path, target
"""
diff --git a/utils/raspberrypi/ctt/ctt_vc4.py b/utils/raspberrypi/ctt/ctt_vc4.py
new file mode 100755
index 00000000..7154e110
--- /dev/null
+++ b/utils/raspberrypi/ctt/ctt_vc4.py
@@ -0,0 +1,126 @@
+#!/usr/bin/env python3
+#
+# SPDX-License-Identifier: BSD-2-Clause
+#
+# Copyright (C) 2019, Raspberry Pi Ltd
+#
+# ctt_vc4.py - camera tuning tool data for VC4 platforms
+
+
+json_template = {
+ "rpi.black_level": {
+ "black_level": 4096
+ },
+ "rpi.dpc": {
+ },
+ "rpi.lux": {
+ "reference_shutter_speed": 10000,
+ "reference_gain": 1,
+ "reference_aperture": 1.0
+ },
+ "rpi.noise": {
+ },
+ "rpi.geq": {
+ },
+ "rpi.sdn": {
+ },
+ "rpi.awb": {
+ "priors": [
+ {"lux": 0, "prior": [2000, 1.0, 3000, 0.0, 13000, 0.0]},
+ {"lux": 800, "prior": [2000, 0.0, 6000, 2.0, 13000, 2.0]},
+ {"lux": 1500, "prior": [2000, 0.0, 4000, 1.0, 6000, 6.0, 6500, 7.0, 7000, 1.0, 13000, 1.0]}
+ ],
+ "modes": {
+ "auto": {"lo": 2500, "hi": 8000},
+ "incandescent": {"lo": 2500, "hi": 3000},
+ "tungsten": {"lo": 3000, "hi": 3500},
+ "fluorescent": {"lo": 4000, "hi": 4700},
+ "indoor": {"lo": 3000, "hi": 5000},
+ "daylight": {"lo": 5500, "hi": 6500},
+ "cloudy": {"lo": 7000, "hi": 8600}
+ },
+ "bayes": 1
+ },
+ "rpi.agc": {
+ "metering_modes": {
+ "centre-weighted": {
+ "weights": [3, 3, 3, 2, 2, 2, 2, 1, 1, 1, 1, 0, 0, 0, 0]
+ },
+ "spot": {
+ "weights": [2, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
+ },
+ "matrix": {
+ "weights": [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
+ }
+ },
+ "exposure_modes": {
+ "normal": {
+ "shutter": [100, 10000, 30000, 60000, 120000],
+ "gain": [1.0, 2.0, 4.0, 6.0, 6.0]
+ },
+ "short": {
+ "shutter": [100, 5000, 10000, 20000, 120000],
+ "gain": [1.0, 2.0, 4.0, 6.0, 6.0]
+ }
+ },
+ "constraint_modes": {
+ "normal": [
+ {"bound": "LOWER", "q_lo": 0.98, "q_hi": 1.0, "y_target": [0, 0.5, 1000, 0.5]}
+ ],
+ "highlight": [
+ {"bound": "LOWER", "q_lo": 0.98, "q_hi": 1.0, "y_target": [0, 0.5, 1000, 0.5]},
+ {"bound": "UPPER", "q_lo": 0.98, "q_hi": 1.0, "y_target": [0, 0.8, 1000, 0.8]}
+ ]
+ },
+ "y_target": [0, 0.16, 1000, 0.165, 10000, 0.17]
+ },
+ "rpi.alsc": {
+ 'omega': 1.3,
+ 'n_iter': 100,
+ 'luminance_strength': 0.7,
+ },
+ "rpi.contrast": {
+ "ce_enable": 1,
+ "gamma_curve": [
+ 0, 0,
+ 1024, 5040,
+ 2048, 9338,
+ 3072, 12356,
+ 4096, 15312,
+ 5120, 18051,
+ 6144, 20790,
+ 7168, 23193,
+ 8192, 25744,
+ 9216, 27942,
+ 10240, 30035,
+ 11264, 32005,
+ 12288, 33975,
+ 13312, 35815,
+ 14336, 37600,
+ 15360, 39168,
+ 16384, 40642,
+ 18432, 43379,
+ 20480, 45749,
+ 22528, 47753,
+ 24576, 49621,
+ 26624, 51253,
+ 28672, 52698,
+ 30720, 53796,
+ 32768, 54876,
+ 36864, 57012,
+ 40960, 58656,
+ 45056, 59954,
+ 49152, 61183,
+ 53248, 62355,
+ 57344, 63419,
+ 61440, 64476,
+ 65535, 65535
+ ]
+ },
+ "rpi.ccm": {
+ },
+ "rpi.sharpen": {
+ }
+}
+
+grid_size = (16, 12)
diff --git a/utils/tuning/libtuning/modules/agc/__init__.py b/utils/tuning/libtuning/modules/agc/__init__.py
new file mode 100644
index 00000000..4db9ca37
--- /dev/null
+++ b/utils/tuning/libtuning/modules/agc/__init__.py
@@ -0,0 +1,6 @@
+# SPDX-License-Identifier: GPL-2.0-or-later
+#
+# Copyright (C) 2024, Paul Elder <paul.elder@ideasonboard.com>
+
+from libtuning.modules.agc.agc import AGC
+from libtuning.modules.agc.rkisp1 import AGCRkISP1
diff --git a/utils/tuning/libtuning/modules/agc/agc.py b/utils/tuning/libtuning/modules/agc/agc.py
new file mode 100644
index 00000000..9c8899ba
--- /dev/null
+++ b/utils/tuning/libtuning/modules/agc/agc.py
@@ -0,0 +1,21 @@
+# SPDX-License-Identifier: BSD-2-Clause
+#
+# Copyright (C) 2019, Raspberry Pi Ltd
+# Copyright (C) 2024, Paul Elder <paul.elder@ideasonboard.com>
+
+from ..module import Module
+
+import libtuning as lt
+
+
+class AGC(Module):
+ type = 'agc'
+ hr_name = 'AGC (Base)'
+ out_name = 'GenericAGC'
+
+ # \todo Add sector shapes and stuff just like lsc
+ def __init__(self, *,
+ debug: list):
+ super().__init__()
+
+ self.debug = debug
diff --git a/utils/tuning/libtuning/modules/agc/rkisp1.py b/utils/tuning/libtuning/modules/agc/rkisp1.py
new file mode 100644
index 00000000..19a5555b
--- /dev/null
+++ b/utils/tuning/libtuning/modules/agc/rkisp1.py
@@ -0,0 +1,79 @@
+# SPDX-License-Identifier: BSD-2-Clause
+#
+# Copyright (C) 2019, Raspberry Pi Ltd
+# Copyright (C) 2024, Paul Elder <paul.elder@ideasonboard.com>
+#
+# rkisp1.py - AGC module for tuning rkisp1
+
+from .agc import AGC
+
+import libtuning as lt
+
+
+class AGCRkISP1(AGC):
+ hr_name = 'AGC (RkISP1)'
+ out_name = 'Agc'
+
+ def __init__(self, **kwargs):
+ super().__init__(**kwargs)
+
+ # We don't actually need anything from the config file
+ def validate_config(self, config: dict) -> bool:
+ return True
+
+ def _generate_metering_modes(self) -> dict:
+ centre_weighted = [
+ 0, 0, 0, 0, 0,
+ 0, 6, 8, 6, 0,
+ 0, 8, 16, 8, 0,
+ 0, 6, 8, 6, 0,
+ 0, 0, 0, 0, 0
+ ]
+
+ spot = [
+ 0, 0, 0, 0, 0,
+ 0, 2, 4, 2, 0,
+ 0, 4, 16, 4, 0,
+ 0, 2, 4, 2, 0,
+ 0, 0, 0, 0, 0
+ ]
+
+ matrix = [1 for i in range(0, 25)]
+
+ return {
+ 'MeteringCentreWeighted': centre_weighted,
+ 'MeteringSpot': spot,
+ 'MeteringMatrix': matrix
+ }
+
+ def _generate_exposure_modes(self) -> dict:
+ normal = {'shutter': [100, 10000, 30000, 60000, 120000],
+ 'gain': [2.0, 4.0, 6.0, 6.0, 6.0]}
+ short = {'shutter': [100, 5000, 10000, 20000, 120000],
+ 'gain': [2.0, 4.0, 6.0, 6.0, 6.0]}
+
+ return {'ExposureNormal': normal, 'ExposureShort': short}
+
+ def _generate_constraint_modes(self) -> dict:
+ normal = {'lower': {'qLo': 0.98, 'qHi': 1.0, 'yTarget': 0.5}}
+ highlight = {
+ 'lower': {'qLo': 0.98, 'qHi': 1.0, 'yTarget': 0.5},
+ 'upper': {'qLo': 0.98, 'qHi': 1.0, 'yTarget': 0.8}
+ }
+
+ return {'ConstraintNormal': normal, 'ConstraintHighlight': highlight}
+
+ def _generate_y_target(self) -> list:
+ return 0.16
+
+ def process(self, config: dict, images: list, outputs: dict) -> dict:
+ output = {}
+
+ output['AeMeteringMode'] = self._generate_metering_modes()
+ output['AeExposureMode'] = self._generate_exposure_modes()
+ output['AeConstraintMode'] = self._generate_constraint_modes()
+ output['relativeLuminanceTarget'] = self._generate_y_target()
+
+ # \todo Debug functionality
+
+ return output
diff --git a/utils/tuning/rkisp1.py b/utils/tuning/rkisp1.py
index 517c791e..d0ce15d5 100755
--- a/utils/tuning/rkisp1.py
+++ b/utils/tuning/rkisp1.py
@@ -11,6 +11,7 @@ import libtuning as lt
from libtuning.parsers import YamlParser
from libtuning.generators import YamlOutput
from libtuning.modules.lsc import LSCRkISP1
+from libtuning.modules.agc import AGCRkISP1
tuner = lt.Tuner('RkISP1')
tuner.add(LSCRkISP1(
@@ -32,9 +33,10 @@ tuner.add(LSCRkISP1(
# values. This can also be a custom function.
smoothing_function=lt.smoothing.MedianBlur(3),
))
+tuner.add(AGCRkISP1(debug=[lt.Debug.Plot]))
tuner.set_input_parser(YamlParser())
tuner.set_output_formatter(YamlOutput())
-tuner.set_output_order([LSCRkISP1])
+tuner.set_output_order([AGCRkISP1, LSCRkISP1])
if __name__ == '__main__':
sys.exit(tuner.run(sys.argv))
diff --git a/utils/update-kernel-headers.sh b/utils/update-kernel-headers.sh
index 590986d2..8405be0a 100755
--- a/utils/update-kernel-headers.sh
+++ b/utils/update-kernel-headers.sh
@@ -52,6 +52,7 @@ headers="
linux/media-bus-format.h
linux/media.h
linux/rkisp1-config.h
+ linux/udmabuf.h
linux/v4l2-common.h
linux/v4l2-controls.h
linux/v4l2-mediabus.h