diff options
Diffstat (limited to 'src')
166 files changed, 8722 insertions, 2003 deletions
diff --git a/src/apps/cam/camera_session.cpp b/src/apps/cam/camera_session.cpp index 6e9890cc..9e934827 100644 --- a/src/apps/cam/camera_session.cpp +++ b/src/apps/cam/camera_session.cpp @@ -159,12 +159,18 @@ CameraSession::~CameraSession() void CameraSession::listControls() const { for (const auto &[id, info] : camera_->controls()) { + std::stringstream io; + io << "[" + << (id->isInput() ? "in" : " ") + << (id->isOutput() ? "out" : " ") + << "] "; + if (info.values().empty()) { - std::cout << "Control: " + std::cout << "Control: " << io.str() << id->vendor() << "::" << id->name() << ": " << info.toString() << std::endl; } else { - std::cout << "Control: " + std::cout << "Control: " << io.str() << id->vendor() << "::" << id->name() << ":" << std::endl; for (const auto &value : info.values()) { diff --git a/src/apps/common/event_loop.cpp b/src/apps/common/event_loop.cpp index f7f9afa0..b6230f4b 100644 --- a/src/apps/common/event_loop.cpp +++ b/src/apps/common/event_loop.cpp @@ -21,12 +21,35 @@ EventLoop::EventLoop() evthread_use_pthreads(); base_ = event_base_new(); instance_ = this; + + callsTrigger_ = event_new(base_, -1, EV_PERSIST, [](evutil_socket_t, short, void *closure) { + auto *self = static_cast<EventLoop *>(closure); + + for (;;) { + std::function<void()> call; + + { + std::lock_guard locker(self->lock_); + if (self->calls_.empty()) + break; + + call = std::move(self->calls_.front()); + self->calls_.pop_front(); + } + + call(); + } + }, this); + assert(callsTrigger_); + event_add(callsTrigger_, nullptr); } EventLoop::~EventLoop() { instance_ = nullptr; + event_free(callsTrigger_); + events_.clear(); event_base_free(base_); libevent_global_shutdown(); @@ -50,20 +73,20 @@ void EventLoop::exit(int code) event_base_loopbreak(base_); } -void EventLoop::callLater(const std::function<void()> &func) +void EventLoop::callLater(std::function<void()> &&func) { { std::unique_lock<std::mutex> locker(lock_); - calls_.push_back(func); + calls_.push_back(std::move(func)); } - event_base_once(base_, -1, EV_TIMEOUT, dispatchCallback, this, nullptr); + event_active(callsTrigger_, 0, 0); } void EventLoop::addFdEvent(int fd, EventType type, - const std::function<void()> &callback) + std::function<void()> &&callback) { - std::unique_ptr<Event> event = std::make_unique<Event>(callback); + std::unique_ptr<Event> event = std::make_unique<Event>(std::move(callback)); short events = (type & Read ? EV_READ : 0) | (type & Write ? EV_WRITE : 0) | EV_PERSIST; @@ -85,9 +108,9 @@ void EventLoop::addFdEvent(int fd, EventType type, } void EventLoop::addTimerEvent(const std::chrono::microseconds period, - const std::function<void()> &callback) + std::function<void()> &&callback) { - std::unique_ptr<Event> event = std::make_unique<Event>(callback); + std::unique_ptr<Event> event = std::make_unique<Event>(std::move(callback)); event->event_ = event_new(base_, -1, EV_PERSIST, &EventLoop::Event::dispatch, event.get()); if (!event->event_) { @@ -108,31 +131,8 @@ void EventLoop::addTimerEvent(const std::chrono::microseconds period, events_.push_back(std::move(event)); } -void EventLoop::dispatchCallback([[maybe_unused]] evutil_socket_t fd, - [[maybe_unused]] short flags, void *param) -{ - EventLoop *loop = static_cast<EventLoop *>(param); - loop->dispatchCall(); -} - -void EventLoop::dispatchCall() -{ - std::function<void()> call; - - { - std::unique_lock<std::mutex> locker(lock_); - if (calls_.empty()) - return; - - call = calls_.front(); - calls_.pop_front(); - } - - call(); -} - -EventLoop::Event::Event(const std::function<void()> &callback) - : callback_(callback), event_(nullptr) +EventLoop::Event::Event(std::function<void()> &&callback) + : callback_(std::move(callback)), event_(nullptr) { } diff --git a/src/apps/common/event_loop.h b/src/apps/common/event_loop.h index ef129b9a..d8b6df2f 100644 --- a/src/apps/common/event_loop.h +++ b/src/apps/common/event_loop.h @@ -8,11 +8,14 @@ #pragma once #include <chrono> +#include <deque> #include <functional> #include <list> #include <memory> #include <mutex> +#include <libcamera/base/class.h> + #include <event2/util.h> struct event_base; @@ -33,18 +36,20 @@ public: int exec(); void exit(int code = 0); - void callLater(const std::function<void()> &func); + void callLater(std::function<void()> &&func); void addFdEvent(int fd, EventType type, - const std::function<void()> &handler); + std::function<void()> &&handler); - using duration = std::chrono::steady_clock::duration; void addTimerEvent(const std::chrono::microseconds period, - const std::function<void()> &handler); + std::function<void()> &&handler); private: + LIBCAMERA_DISABLE_COPY_AND_MOVE(EventLoop) + struct Event { - Event(const std::function<void()> &callback); + Event(std::function<void()> &&callback); + LIBCAMERA_DISABLE_COPY_AND_MOVE(Event) ~Event(); static void dispatch(int fd, short events, void *arg); @@ -58,11 +63,9 @@ private: struct event_base *base_; int exitCode_; - std::list<std::function<void()>> calls_; + std::deque<std::function<void()>> calls_; + struct event *callsTrigger_ = nullptr; + std::list<std::unique_ptr<Event>> events_; std::mutex lock_; - - static void dispatchCallback(evutil_socket_t fd, short flags, - void *param); - void dispatchCall(); }; diff --git a/src/apps/common/options.cpp b/src/apps/common/options.cpp index ece268d0..cae193cc 100644 --- a/src/apps/common/options.cpp +++ b/src/apps/common/options.cpp @@ -1040,7 +1040,7 @@ void OptionsParser::usageOptions(const std::list<Option> &options, std::cerr << std::setw(indent) << argument; - for (const char *help = option.help, *end = help; end; ) { + for (const char *help = option.help, *end = help; end;) { end = strchr(help, '\n'); if (end) { std::cerr << std::string(help, end - help + 1); diff --git a/src/apps/common/ppm_writer.cpp b/src/apps/common/ppm_writer.cpp index d6c8641d..368de8bf 100644 --- a/src/apps/common/ppm_writer.cpp +++ b/src/apps/common/ppm_writer.cpp @@ -7,6 +7,7 @@ #include "ppm_writer.h" +#include <errno.h> #include <fstream> #include <iostream> @@ -28,7 +29,7 @@ int PPMWriter::write(const char *filename, std::ofstream output(filename, std::ios::binary); if (!output) { std::cerr << "Failed to open ppm file: " << filename << std::endl; - return -EINVAL; + return -EIO; } output << "P6" << std::endl @@ -36,7 +37,7 @@ int PPMWriter::write(const char *filename, << "255" << std::endl; if (!output) { std::cerr << "Failed to write the file header" << std::endl; - return -EINVAL; + return -EIO; } const unsigned int rowLength = config.size.width * 3; @@ -45,7 +46,7 @@ int PPMWriter::write(const char *filename, output.write(row, rowLength); if (!output) { std::cerr << "Failed to write image data at row " << y << std::endl; - return -EINVAL; + return -EIO; } } diff --git a/src/apps/lc-compliance/environment.h b/src/apps/lc-compliance/environment.h index 543e5372..834c722e 100644 --- a/src/apps/lc-compliance/environment.h +++ b/src/apps/lc-compliance/environment.h @@ -23,5 +23,5 @@ private: Environment() = default; std::string cameraId_; - libcamera::CameraManager *cm_; + libcamera::CameraManager *cm_ = nullptr; }; diff --git a/src/apps/lc-compliance/helpers/capture.cpp b/src/apps/lc-compliance/helpers/capture.cpp index 90c1530b..f2c6d58c 100644 --- a/src/apps/lc-compliance/helpers/capture.cpp +++ b/src/apps/lc-compliance/helpers/capture.cpp @@ -7,13 +7,14 @@ #include "capture.h" +#include <assert.h> + #include <gtest/gtest.h> using namespace libcamera; Capture::Capture(std::shared_ptr<Camera> camera) - : loop_(nullptr), camera_(camera), - allocator_(std::make_unique<FrameBufferAllocator>(camera)) + : camera_(std::move(camera)), allocator_(camera_) { } @@ -26,10 +27,8 @@ void Capture::configure(StreamRole role) { config_ = camera_->generateConfiguration({ role }); - if (!config_) { - std::cout << "Role not supported by camera" << std::endl; - GTEST_SKIP(); - } + if (!config_) + GTEST_SKIP() << "Role not supported by camera"; if (config_->validate() != CameraConfiguration::Valid) { config_.reset(); @@ -42,155 +41,106 @@ void Capture::configure(StreamRole role) } } -void Capture::start() +void Capture::run(unsigned int captureLimit, std::optional<unsigned int> queueLimit) { - Stream *stream = config_->at(0).stream(); - int count = allocator_->allocate(stream); - - ASSERT_GE(count, 0) << "Failed to allocate buffers"; - EXPECT_EQ(count, config_->at(0).bufferCount) << "Allocated less buffers than expected"; - - camera_->requestCompleted.connect(this, &Capture::requestComplete); - - ASSERT_EQ(camera_->start(), 0) << "Failed to start camera"; -} + assert(!queueLimit || captureLimit <= *queueLimit); -void Capture::stop() -{ - if (!config_ || !allocator_->allocated()) - return; + captureLimit_ = captureLimit; + queueLimit_ = queueLimit; - camera_->stop(); + captureCount_ = queueCount_ = 0; - camera_->requestCompleted.disconnect(this); + EventLoop loop; + loop_ = &loop; - Stream *stream = config_->at(0).stream(); - requests_.clear(); - allocator_->free(stream); -} - -/* CaptureBalanced */ - -CaptureBalanced::CaptureBalanced(std::shared_ptr<Camera> camera) - : Capture(camera) -{ -} - -void CaptureBalanced::capture(unsigned int numRequests) -{ start(); - Stream *stream = config_->at(0).stream(); - const std::vector<std::unique_ptr<FrameBuffer>> &buffers = allocator_->buffers(stream); - - /* No point in testing less requests then the camera depth. */ - if (buffers.size() > numRequests) { - std::cout << "Camera needs " + std::to_string(buffers.size()) - + " requests, can't test only " - + std::to_string(numRequests) << std::endl; - GTEST_SKIP(); - } + for (const auto &request : requests_) + queueRequest(request.get()); - queueCount_ = 0; - captureCount_ = 0; - captureLimit_ = numRequests; + EXPECT_EQ(loop_->exec(), 0); - /* Queue the recommended number of requests. */ - for (const std::unique_ptr<FrameBuffer> &buffer : buffers) { - std::unique_ptr<Request> request = camera_->createRequest(); - ASSERT_TRUE(request) << "Can't create request"; - - ASSERT_EQ(request->addBuffer(stream, buffer.get()), 0) << "Can't set buffer for request"; - - ASSERT_EQ(queueRequest(request.get()), 0) << "Failed to queue request"; - - requests_.push_back(std::move(request)); - } - - /* Run capture session. */ - loop_ = new EventLoop(); - loop_->exec(); stop(); - delete loop_; - ASSERT_EQ(captureCount_, captureLimit_); + EXPECT_LE(captureLimit_, captureCount_); + EXPECT_LE(captureCount_, queueCount_); + EXPECT_TRUE(!queueLimit_ || queueCount_ <= *queueLimit_); } -int CaptureBalanced::queueRequest(Request *request) +int Capture::queueRequest(libcamera::Request *request) { - queueCount_++; - if (queueCount_ > captureLimit_) + if (queueLimit_ && queueCount_ >= *queueLimit_) return 0; - return camera_->queueRequest(request); + int ret = camera_->queueRequest(request); + if (ret < 0) + return ret; + + queueCount_ += 1; + return 0; } -void CaptureBalanced::requestComplete(Request *request) +void Capture::requestComplete(Request *request) { - EXPECT_EQ(request->status(), Request::Status::RequestComplete) - << "Request didn't complete successfully"; - captureCount_++; if (captureCount_ >= captureLimit_) { loop_->exit(0); return; } + EXPECT_EQ(request->status(), Request::Status::RequestComplete) + << "Request didn't complete successfully"; + request->reuse(Request::ReuseBuffers); if (queueRequest(request)) loop_->exit(-EINVAL); } -/* CaptureUnbalanced */ - -CaptureUnbalanced::CaptureUnbalanced(std::shared_ptr<Camera> camera) - : Capture(camera) -{ -} - -void CaptureUnbalanced::capture(unsigned int numRequests) +void Capture::start() { - start(); + assert(config_); + assert(!config_->empty()); + assert(!allocator_.allocated()); + assert(requests_.empty()); Stream *stream = config_->at(0).stream(); - const std::vector<std::unique_ptr<FrameBuffer>> &buffers = allocator_->buffers(stream); + int count = allocator_.allocate(stream); - captureCount_ = 0; - captureLimit_ = numRequests; + ASSERT_GE(count, 0) << "Failed to allocate buffers"; + EXPECT_EQ(count, config_->at(0).bufferCount) << "Allocated less buffers than expected"; + + const std::vector<std::unique_ptr<FrameBuffer>> &buffers = allocator_.buffers(stream); + + /* No point in testing less requests then the camera depth. */ + if (queueLimit_ && *queueLimit_ < buffers.size()) { + GTEST_SKIP() << "Camera needs " << buffers.size() + << " requests, can't test only " << *queueLimit_; + } - /* Queue the recommended number of requests. */ for (const std::unique_ptr<FrameBuffer> &buffer : buffers) { std::unique_ptr<Request> request = camera_->createRequest(); ASSERT_TRUE(request) << "Can't create request"; ASSERT_EQ(request->addBuffer(stream, buffer.get()), 0) << "Can't set buffer for request"; - ASSERT_EQ(camera_->queueRequest(request.get()), 0) << "Failed to queue request"; - requests_.push_back(std::move(request)); } - /* Run capture session. */ - loop_ = new EventLoop(); - int status = loop_->exec(); - stop(); - delete loop_; + camera_->requestCompleted.connect(this, &Capture::requestComplete); - ASSERT_EQ(status, 0); + ASSERT_EQ(camera_->start(), 0) << "Failed to start camera"; } -void CaptureUnbalanced::requestComplete(Request *request) +void Capture::stop() { - captureCount_++; - if (captureCount_ >= captureLimit_) { - loop_->exit(0); + if (!config_ || !allocator_.allocated()) return; - } - EXPECT_EQ(request->status(), Request::Status::RequestComplete) - << "Request didn't complete successfully"; + camera_->stop(); - request->reuse(Request::ReuseBuffers); - if (camera_->queueRequest(request)) - loop_->exit(-EINVAL); + camera_->requestCompleted.disconnect(this); + + Stream *stream = config_->at(0).stream(); + requests_.clear(); + allocator_.free(stream); } diff --git a/src/apps/lc-compliance/helpers/capture.h b/src/apps/lc-compliance/helpers/capture.h index 19b6927c..0e7b848f 100644 --- a/src/apps/lc-compliance/helpers/capture.h +++ b/src/apps/lc-compliance/helpers/capture.h @@ -8,6 +8,7 @@ #pragma once #include <memory> +#include <optional> #include <libcamera/libcamera.h> @@ -16,51 +17,29 @@ class Capture { public: + Capture(std::shared_ptr<libcamera::Camera> camera); + ~Capture(); + void configure(libcamera::StreamRole role); + void run(unsigned int captureLimit, std::optional<unsigned int> queueLimit = {}); -protected: - Capture(std::shared_ptr<libcamera::Camera> camera); - virtual ~Capture(); +private: + LIBCAMERA_DISABLE_COPY_AND_MOVE(Capture) void start(); void stop(); - virtual void requestComplete(libcamera::Request *request) = 0; - - EventLoop *loop_; + int queueRequest(libcamera::Request *request); + void requestComplete(libcamera::Request *request); std::shared_ptr<libcamera::Camera> camera_; - std::unique_ptr<libcamera::FrameBufferAllocator> allocator_; + libcamera::FrameBufferAllocator allocator_; std::unique_ptr<libcamera::CameraConfiguration> config_; std::vector<std::unique_ptr<libcamera::Request>> requests_; -}; - -class CaptureBalanced : public Capture -{ -public: - CaptureBalanced(std::shared_ptr<libcamera::Camera> camera); - - void capture(unsigned int numRequests); - -private: - int queueRequest(libcamera::Request *request); - void requestComplete(libcamera::Request *request) override; - - unsigned int queueCount_; - unsigned int captureCount_; - unsigned int captureLimit_; -}; - -class CaptureUnbalanced : public Capture -{ -public: - CaptureUnbalanced(std::shared_ptr<libcamera::Camera> camera); - - void capture(unsigned int numRequests); - -private: - void requestComplete(libcamera::Request *request) override; - unsigned int captureCount_; - unsigned int captureLimit_; + EventLoop *loop_ = nullptr; + unsigned int captureLimit_ = 0; + std::optional<unsigned int> queueLimit_; + unsigned int captureCount_ = 0; + unsigned int queueCount_ = 0; }; diff --git a/src/apps/lc-compliance/main.cpp b/src/apps/lc-compliance/main.cpp index 3f1d2a61..e9f0ffbb 100644 --- a/src/apps/lc-compliance/main.cpp +++ b/src/apps/lc-compliance/main.cpp @@ -45,13 +45,11 @@ class ThrowListener : public testing::EmptyTestEventListener static void listCameras(CameraManager *cm) { for (const std::shared_ptr<Camera> &cam : cm->cameras()) - std::cout << "- " << cam.get()->id() << std::endl; + std::cout << "- " << cam->id() << std::endl; } static int initCamera(CameraManager *cm, OptionsParser::Options options) { - std::shared_ptr<Camera> camera; - int ret = cm->start(); if (ret) { std::cout << "Failed to start camera manager: " @@ -66,7 +64,7 @@ static int initCamera(CameraManager *cm, OptionsParser::Options options) } const std::string &cameraId = options[OptCamera]; - camera = cm->get(cameraId); + std::shared_ptr<Camera> camera = cm->get(cameraId); if (!camera) { std::cout << "Camera " << cameraId << " not found, available cameras:" << std::endl; listCameras(cm); @@ -82,45 +80,27 @@ static int initCamera(CameraManager *cm, OptionsParser::Options options) static int initGtestParameters(char *arg0, OptionsParser::Options options) { - const std::map<std::string, std::string> gtestFlags = { { "list", "--gtest_list_tests" }, - { "filter", "--gtest_filter" } }; - - int argc = 0; + std::vector<const char *> argv; std::string filterParam; - /* - * +2 to have space for both the 0th argument that is needed but not - * used and the null at the end. - */ - char **argv = new char *[(gtestFlags.size() + 2)]; - if (!argv) - return -ENOMEM; - - argv[0] = arg0; - argc++; + argv.push_back(arg0); - if (options.isSet(OptList)) { - argv[argc] = const_cast<char *>(gtestFlags.at("list").c_str()); - argc++; - } + if (options.isSet(OptList)) + argv.push_back("--gtest_list_tests"); if (options.isSet(OptFilter)) { /* * The filter flag needs to be passed as a single parameter, in * the format --gtest_filter=filterStr */ - filterParam = gtestFlags.at("filter") + "=" + - static_cast<const std::string &>(options[OptFilter]); - - argv[argc] = const_cast<char *>(filterParam.c_str()); - argc++; + filterParam = "--gtest_filter=" + options[OptFilter].toString(); + argv.push_back(filterParam.c_str()); } - argv[argc] = nullptr; - - ::testing::InitGoogleTest(&argc, argv); + argv.push_back(nullptr); - delete[] argv; + int argc = argv.size(); + ::testing::InitGoogleTest(&argc, const_cast<char **>(argv.data())); return 0; } diff --git a/src/apps/lc-compliance/tests/capture_test.cpp b/src/apps/lc-compliance/tests/capture_test.cpp index ad3a1da2..93bed48f 100644 --- a/src/apps/lc-compliance/tests/capture_test.cpp +++ b/src/apps/lc-compliance/tests/capture_test.cpp @@ -14,10 +14,13 @@ #include "environment.h" +namespace { + using namespace libcamera; -const std::vector<int> NUMREQUESTS = { 1, 2, 3, 5, 8, 13, 21, 34, 55, 89 }; -const std::vector<StreamRole> ROLES = { +const int NUMREQUESTS[] = { 1, 2, 3, 5, 8, 13, 21, 34, 55, 89 }; + +const StreamRole ROLES[] = { StreamRole::Raw, StreamRole::StillCapture, StreamRole::VideoRecording, @@ -84,11 +87,11 @@ TEST_P(SingleStream, Capture) { auto [role, numRequests] = GetParam(); - CaptureBalanced capture(camera_); + Capture capture(camera_); capture.configure(role); - capture.capture(numRequests); + capture.run(numRequests, numRequests); } /* @@ -103,12 +106,12 @@ TEST_P(SingleStream, CaptureStartStop) auto [role, numRequests] = GetParam(); unsigned int numRepeats = 3; - CaptureBalanced capture(camera_); + Capture capture(camera_); capture.configure(role); for (unsigned int starts = 0; starts < numRepeats; starts++) - capture.capture(numRequests); + capture.run(numRequests, numRequests); } /* @@ -122,11 +125,11 @@ TEST_P(SingleStream, UnbalancedStop) { auto [role, numRequests] = GetParam(); - CaptureUnbalanced capture(camera_); + Capture capture(camera_); capture.configure(role); - capture.capture(numRequests); + capture.run(numRequests); } INSTANTIATE_TEST_SUITE_P(CaptureTests, @@ -134,3 +137,5 @@ INSTANTIATE_TEST_SUITE_P(CaptureTests, testing::Combine(testing::ValuesIn(ROLES), testing::ValuesIn(NUMREQUESTS)), SingleStream::nameParameters); + +} /* namespace */ diff --git a/src/apps/qcam/format_converter.cpp b/src/apps/qcam/format_converter.cpp index 32123493..b025a3c7 100644 --- a/src/apps/qcam/format_converter.cpp +++ b/src/apps/qcam/format_converter.cpp @@ -249,7 +249,7 @@ void FormatConverter::convertYUVPacked(const Image *srcImage, unsigned char *dst dst_stride = width_ * 4; for (src_y = 0, dst_y = 0; dst_y < height_; src_y++, dst_y++) { - for (src_x = 0, dst_x = 0; dst_x < width_; ) { + for (src_x = 0, dst_x = 0; dst_x < width_;) { cb = src[src_y * src_stride + src_x * 4 + cb_pos_]; cr = src[src_y * src_stride + src_x * 4 + cr_pos]; diff --git a/src/apps/qcam/main_window.cpp b/src/apps/qcam/main_window.cpp index de487672..3880a846 100644 --- a/src/apps/qcam/main_window.cpp +++ b/src/apps/qcam/main_window.cpp @@ -251,16 +251,14 @@ void MainWindow::updateTitle() void MainWindow::switchCamera() { /* Get and acquire the new camera. */ - std::string newCameraId = chooseCamera(); + std::shared_ptr<Camera> cam = chooseCamera(); - if (newCameraId.empty()) + if (!cam) return; - if (camera_ && newCameraId == camera_->id()) + if (camera_ && cam == camera_) return; - const std::shared_ptr<Camera> &cam = cm_->get(newCameraId); - if (cam->acquire()) { qInfo() << "Failed to acquire camera" << cam->id().c_str(); return; @@ -282,46 +280,41 @@ void MainWindow::switchCamera() startStopAction_->setChecked(true); /* Display the current cameraId in the toolbar .*/ - cameraSelectButton_->setText(QString::fromStdString(newCameraId)); + cameraSelectButton_->setText(QString::fromStdString(cam->id())); } -std::string MainWindow::chooseCamera() +std::shared_ptr<Camera> MainWindow::chooseCamera() { if (cameraSelectorDialog_->exec() != QDialog::Accepted) - return std::string(); + return {}; - return cameraSelectorDialog_->getCameraId(); + std::string id = cameraSelectorDialog_->getCameraId(); + return cm_->get(id); } int MainWindow::openCamera() { - std::string cameraName; - /* * If a camera is specified on the command line, get it. Otherwise, if * only one camera is available, pick it automatically, else, display * the selector dialog box. */ if (options_.isSet(OptCamera)) { - cameraName = static_cast<std::string>(options_[OptCamera]); + std::string cameraName = static_cast<std::string>(options_[OptCamera]); + camera_ = cm_->get(cameraName); + if (!camera_) + qInfo() << "Camera" << cameraName.c_str() << "not found"; } else { std::vector<std::shared_ptr<Camera>> cameras = cm_->cameras(); - if (cameras.size() == 1) - cameraName = cameras[0]->id(); - else - cameraName = chooseCamera(); + camera_ = (cameras.size() == 1) ? cameras[0] : chooseCamera(); + if (!camera_) + qInfo() << "No camera detected"; } - if (cameraName == "") - return -EINVAL; - - /* Get and acquire the camera. */ - camera_ = cm_->get(cameraName); - if (!camera_) { - qInfo() << "Camera" << cameraName.c_str() << "not found"; + if (!camera_) return -ENODEV; - } + /* Acquire the camera. */ if (camera_->acquire()) { qInfo() << "Failed to acquire camera"; camera_.reset(); @@ -329,7 +322,7 @@ int MainWindow::openCamera() } /* Set the camera switch button with the currently selected Camera id. */ - cameraSelectButton_->setText(QString::fromStdString(cameraName)); + cameraSelectButton_->setText(QString::fromStdString(camera_->id())); return 0; } diff --git a/src/apps/qcam/main_window.h b/src/apps/qcam/main_window.h index 4cead734..81fcf915 100644 --- a/src/apps/qcam/main_window.h +++ b/src/apps/qcam/main_window.h @@ -73,7 +73,7 @@ private Q_SLOTS: private: int createToolbars(); - std::string chooseCamera(); + std::shared_ptr<libcamera::Camera> chooseCamera(); int openCamera(); int startCapture(); diff --git a/src/gstreamer/gstlibcamera-controls.cpp.in b/src/gstreamer/gstlibcamera-controls.cpp.in index ace36b71..d937b19e 100644 --- a/src/gstreamer/gstlibcamera-controls.cpp.in +++ b/src/gstreamer/gstlibcamera-controls.cpp.in @@ -39,7 +39,7 @@ static void value_set_rectangle(GValue *value, const Rectangle &rect) GValue height = G_VALUE_INIT; g_value_init(&height, G_TYPE_INT); - g_value_set_int(&x, size.height); + g_value_set_int(&height, size.height); gst_value_array_append_and_take_value(value, &height); } diff --git a/src/gstreamer/gstlibcamera-utils.cpp b/src/gstreamer/gstlibcamera-utils.cpp index 732987ef..a466b305 100644 --- a/src/gstreamer/gstlibcamera-utils.cpp +++ b/src/gstreamer/gstlibcamera-utils.cpp @@ -85,7 +85,7 @@ static struct { }; static GstVideoColorimetry -colorimetry_from_colorspace(const ColorSpace &colorSpace) +colorimetry_from_colorspace(const ColorSpace &colorSpace, GstVideoTransferFunction transfer) { GstVideoColorimetry colorimetry; @@ -113,6 +113,8 @@ colorimetry_from_colorspace(const ColorSpace &colorSpace) break; case ColorSpace::TransferFunction::Rec709: colorimetry.transfer = GST_VIDEO_TRANSFER_BT709; + if (transfer != GST_VIDEO_TRANSFER_UNKNOWN) + colorimetry.transfer = transfer; break; } @@ -144,7 +146,8 @@ colorimetry_from_colorspace(const ColorSpace &colorSpace) } static std::optional<ColorSpace> -colorspace_from_colorimetry(const GstVideoColorimetry &colorimetry) +colorspace_from_colorimetry(const GstVideoColorimetry &colorimetry, + GstVideoTransferFunction *transfer) { std::optional<ColorSpace> colorspace = ColorSpace::Raw; @@ -188,6 +191,7 @@ colorspace_from_colorimetry(const GstVideoColorimetry &colorimetry) case GST_VIDEO_TRANSFER_BT2020_12: case GST_VIDEO_TRANSFER_BT709: colorspace->transferFunction = ColorSpace::TransferFunction::Rec709; + *transfer = colorimetry.transfer; break; default: GST_WARNING("Colorimetry transfer function %d not mapped in gstlibcamera", @@ -379,7 +383,8 @@ gst_libcamera_stream_formats_to_caps(const StreamFormats &formats) } GstCaps * -gst_libcamera_stream_configuration_to_caps(const StreamConfiguration &stream_cfg) +gst_libcamera_stream_configuration_to_caps(const StreamConfiguration &stream_cfg, + GstVideoTransferFunction transfer) { GstCaps *caps = gst_caps_new_empty(); GstStructure *s = bare_structure_from_format(stream_cfg.pixelFormat); @@ -390,7 +395,7 @@ gst_libcamera_stream_configuration_to_caps(const StreamConfiguration &stream_cfg nullptr); if (stream_cfg.colorSpace) { - GstVideoColorimetry colorimetry = colorimetry_from_colorspace(stream_cfg.colorSpace.value()); + GstVideoColorimetry colorimetry = colorimetry_from_colorspace(stream_cfg.colorSpace.value(), transfer); g_autofree gchar *colorimetry_str = gst_video_colorimetry_to_string(&colorimetry); if (colorimetry_str) @@ -405,9 +410,8 @@ gst_libcamera_stream_configuration_to_caps(const StreamConfiguration &stream_cfg return caps; } -void -gst_libcamera_configure_stream_from_caps(StreamConfiguration &stream_cfg, - GstCaps *caps) +void gst_libcamera_configure_stream_from_caps(StreamConfiguration &stream_cfg, + GstCaps *caps, GstVideoTransferFunction *transfer) { GstVideoFormat gst_format = pixel_format_to_gst_format(stream_cfg.pixelFormat); guint i; @@ -495,7 +499,7 @@ gst_libcamera_configure_stream_from_caps(StreamConfiguration &stream_cfg, if (!gst_video_colorimetry_from_string(&colorimetry, colorimetry_str)) g_critical("Invalid colorimetry %s", colorimetry_str); - stream_cfg.colorSpace = colorspace_from_colorimetry(colorimetry); + stream_cfg.colorSpace = colorspace_from_colorimetry(colorimetry, transfer); } } diff --git a/src/gstreamer/gstlibcamera-utils.h b/src/gstreamer/gstlibcamera-utils.h index cab1c814..4978987c 100644 --- a/src/gstreamer/gstlibcamera-utils.h +++ b/src/gstreamer/gstlibcamera-utils.h @@ -16,9 +16,10 @@ #include <gst/video/video.h> GstCaps *gst_libcamera_stream_formats_to_caps(const libcamera::StreamFormats &formats); -GstCaps *gst_libcamera_stream_configuration_to_caps(const libcamera::StreamConfiguration &stream_cfg); +GstCaps *gst_libcamera_stream_configuration_to_caps(const libcamera::StreamConfiguration &stream_cfg, + GstVideoTransferFunction transfer); void gst_libcamera_configure_stream_from_caps(libcamera::StreamConfiguration &stream_cfg, - GstCaps *caps); + GstCaps *caps, GstVideoTransferFunction *transfer); void gst_libcamera_get_framerate_from_caps(GstCaps *caps, GstStructure *element_caps); void gst_libcamera_clamp_and_set_frameduration(libcamera::ControlList &controls, const libcamera::ControlInfoMap &camera_controls, diff --git a/src/gstreamer/gstlibcameraallocator.cpp b/src/gstreamer/gstlibcameraallocator.cpp index 7e4c904d..d4492d99 100644 --- a/src/gstreamer/gstlibcameraallocator.cpp +++ b/src/gstreamer/gstlibcameraallocator.cpp @@ -8,6 +8,8 @@ #include "gstlibcameraallocator.h" +#include <utility> + #include <libcamera/camera.h> #include <libcamera/framebuffer_allocator.h> #include <libcamera/stream.h> @@ -199,22 +201,20 @@ GstLibcameraAllocator * gst_libcamera_allocator_new(std::shared_ptr<Camera> camera, CameraConfiguration *config_) { - auto *self = GST_LIBCAMERA_ALLOCATOR(g_object_new(GST_TYPE_LIBCAMERA_ALLOCATOR, - nullptr)); + g_autoptr(GstLibcameraAllocator) self = GST_LIBCAMERA_ALLOCATOR(g_object_new(GST_TYPE_LIBCAMERA_ALLOCATOR, + nullptr)); gint ret; self->cm_ptr = new std::shared_ptr<CameraManager>(gst_libcamera_get_camera_manager(ret)); - if (ret) { - g_object_unref(self); + if (ret) return nullptr; - } self->fb_allocator = new FrameBufferAllocator(camera); for (StreamConfiguration &streamCfg : *config_) { Stream *stream = streamCfg.stream(); ret = self->fb_allocator->allocate(stream); - if (ret == 0) + if (ret <= 0) return nullptr; GQueue *pool = g_queue_new(); @@ -228,7 +228,7 @@ gst_libcamera_allocator_new(std::shared_ptr<Camera> camera, g_hash_table_insert(self->pools, stream, pool); } - return self; + return std::exchange(self, nullptr); } bool diff --git a/src/gstreamer/gstlibcamerasrc.cpp b/src/gstreamer/gstlibcamerasrc.cpp index 8efa25f4..5e9e843d 100644 --- a/src/gstreamer/gstlibcamerasrc.cpp +++ b/src/gstreamer/gstlibcamerasrc.cpp @@ -433,6 +433,8 @@ static bool gst_libcamera_src_negotiate(GstLibcameraSrc *self) { GstLibcameraSrcState *state = self->state; + std::vector<GstVideoTransferFunction> transfer(state->srcpads_.size(), + GST_VIDEO_TRANSFER_UNKNOWN); g_autoptr(GstStructure) element_caps = gst_structure_new_empty("caps"); @@ -448,7 +450,7 @@ gst_libcamera_src_negotiate(GstLibcameraSrc *self) /* Fixate caps and configure the stream. */ caps = gst_caps_make_writable(caps); - gst_libcamera_configure_stream_from_caps(stream_cfg, caps); + gst_libcamera_configure_stream_from_caps(stream_cfg, caps, &transfer[i]); gst_libcamera_get_framerate_from_caps(caps, element_caps); } @@ -476,7 +478,7 @@ gst_libcamera_src_negotiate(GstLibcameraSrc *self) GstPad *srcpad = state->srcpads_[i]; const StreamConfiguration &stream_cfg = state->config_->at(i); - g_autoptr(GstCaps) caps = gst_libcamera_stream_configuration_to_caps(stream_cfg); + g_autoptr(GstCaps) caps = gst_libcamera_stream_configuration_to_caps(stream_cfg, transfer[i]); gst_libcamera_framerate_to_caps(caps, element_caps); if (!gst_pad_push_event(srcpad, gst_event_new_caps(caps))) diff --git a/src/ipa/ipu3/algorithms/awb.h b/src/ipa/ipu3/algorithms/awb.h index 1916990a..dbf69c90 100644 --- a/src/ipa/ipu3/algorithms/awb.h +++ b/src/ipa/ipu3/algorithms/awb.h @@ -13,7 +13,7 @@ #include <libcamera/geometry.h> -#include "libipa/vector.h" +#include "libcamera/internal/vector.h" #include "algorithm.h" diff --git a/src/ipa/ipu3/ipa_context.cpp b/src/ipa/ipu3/ipa_context.cpp index fe455e07..3b22f791 100644 --- a/src/ipa/ipu3/ipa_context.cpp +++ b/src/ipa/ipu3/ipa_context.cpp @@ -39,6 +39,10 @@ namespace libcamera::ipa::ipu3 { * \struct IPAContext * \brief Global IPA context data shared between all algorithms * + * \fn IPAContext::IPAContext + * \brief Initialize the instance with the given number of frame contexts + * \param[in] frameContextSize Size of the frame context ring buffer + * * \var IPAContext::configuration * \brief The IPA session configuration, immutable during the session * diff --git a/src/ipa/ipu3/ipa_context.h b/src/ipa/ipu3/ipa_context.h index 29f38b36..97fcf06c 100644 --- a/src/ipa/ipu3/ipa_context.h +++ b/src/ipa/ipu3/ipa_context.h @@ -84,6 +84,11 @@ struct IPAFrameContext : public FrameContext { }; struct IPAContext { + IPAContext(unsigned int frameContextSize) + : frameContexts(frameContextSize) + { + } + IPASessionConfiguration configuration; IPAActiveState activeState; diff --git a/src/ipa/ipu3/ipu3.cpp b/src/ipa/ipu3/ipu3.cpp index 44c98cbf..1cae08bf 100644 --- a/src/ipa/ipu3/ipu3.cpp +++ b/src/ipa/ipu3/ipu3.cpp @@ -187,7 +187,7 @@ private: }; IPAIPU3::IPAIPU3() - : context_({ {}, {}, { kMaxFrameContexts }, {} }) + : context_(kMaxFrameContexts) { } diff --git a/src/ipa/libipa/agc_mean_luminance.cpp b/src/ipa/libipa/agc_mean_luminance.cpp index cd175708..02555a44 100644 --- a/src/ipa/libipa/agc_mean_luminance.cpp +++ b/src/ipa/libipa/agc_mean_luminance.cpp @@ -248,7 +248,7 @@ int AgcMeanLuminance::parseExposureModes(const YamlObject &tuningData) } std::vector<uint32_t> exposureTimes = - modeValues["exposure-time"].getList<uint32_t>().value_or(std::vector<uint32_t>{}); + modeValues["exposureTime"].getList<uint32_t>().value_or(std::vector<uint32_t>{}); std::vector<double> gains = modeValues["gain"].getList<double>().value_or(std::vector<double>{}); @@ -338,7 +338,7 @@ int AgcMeanLuminance::parseExposureModes(const YamlObject &tuningData) * For the AeExposureMode control the data should contain a dictionary called * AeExposureMode containing per-mode setting dictionaries with the key being a * value from \ref controls::AeExposureModeNameValueMap. Each mode dict should - * contain an array of exposure times with the key "exposure-time" and an array + * contain an array of exposure times with the key "exposureTime" and an array * of gain values with the key "gain", in this format: * * \code{.unparsed} @@ -346,10 +346,10 @@ int AgcMeanLuminance::parseExposureModes(const YamlObject &tuningData) * - Agc: * AeExposureMode: * ExposureNormal: - * exposure-time: [ 100, 10000, 30000, 60000, 120000 ] + * exposureTime: [ 100, 10000, 30000, 60000, 120000 ] * gain: [ 2.0, 4.0, 6.0, 8.0, 10.0 ] * ExposureShort: - * exposure-time: [ 100, 10000, 30000, 60000, 120000 ] + * exposureTime: [ 100, 10000, 30000, 60000, 120000 ] * gain: [ 2.0, 4.0, 6.0, 8.0, 10.0 ] * * \endcode diff --git a/src/ipa/libipa/awb.cpp b/src/ipa/libipa/awb.cpp new file mode 100644 index 00000000..925fac23 --- /dev/null +++ b/src/ipa/libipa/awb.cpp @@ -0,0 +1,265 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +/* + * Copyright (C) 2024 Ideas on Board Oy + * + * Generic AWB algorithms + */ + +#include "awb.h" + +#include <libcamera/base/log.h> + +#include <libcamera/control_ids.h> + +/** + * \file awb.h + * \brief Base classes for AWB algorithms + */ + +namespace libcamera { + +LOG_DEFINE_CATEGORY(Awb) + +namespace ipa { + +/** + * \class AwbResult + * \brief The result of an AWB calculation + * + * This class holds the result of an auto white balance calculation. + */ + +/** + * \var AwbResult::gains + * \brief The calculated white balance gains + */ + +/** + * \var AwbResult::colourTemperature + * \brief The calculated colour temperature in Kelvin + */ + +/** + * \class AwbStats + * \brief An abstraction class wrapping hardware-specific AWB statistics + * + * IPA modules using an AWB algorithm based on the AwbAlgorithm class need to + * implement this class to give the algorithm access to the hardware-specific + * statistics data. + */ + +/** + * \fn AwbStats::computeColourError() + * \brief Compute an error value for when the given gains would be applied + * \param[in] gains The gains to apply + * + * Compute an error value (non-greyness) assuming the given \a gains would be + * applied. To keep the actual implementations computationally inexpensive, + * the squared colour error shall be returned. + * + * If the AWB statistics provide multiple zones, the average of the individual + * squared errors shall be returned. Averaging/normalizing is necessary so that + * the numeric dimensions are the same on all hardware platforms. + * + * \return The computed error value + */ + +/** + * \fn AwbStats::rgbMeans() + * \brief Get RGB means of the statistics + * + * Fetch the RGB means from the statistics. The values of each channel are + * dimensionless and only the ratios are used for further calculations. This is + * used by the simple grey world model to calculate the gains to apply. + * + * \return The RGB means + */ + +/** + * \class AwbAlgorithm + * \brief A base class for auto white balance algorithms + * + * This class is a base class for auto white balance algorithms. It defines an + * interface for the algorithms to implement, and is used by the IPAs to + * interact with the concrete implementation. + */ + +/** + * \fn AwbAlgorithm::init() + * \brief Initialize the algorithm with the given tuning data + * \param[in] tuningData The tuning data to use for the algorithm + * + * \return 0 on success, a negative error code otherwise + */ + +/** + * \fn AwbAlgorithm::calculateAwb() + * \brief Calculate AWB data from the given statistics + * \param[in] stats The statistics to use for the calculation + * \param[in] lux The lux value of the scene + * + * Calculate an AwbResult object from the given statistics and lux value. A \a + * lux value of 0 means it is unknown or invalid and the algorithm shall ignore + * it. + * + * \return The AWB result + */ + +/** + * \fn AwbAlgorithm::gainsFromColourTemperature() + * \brief Compute white balance gains from a colour temperature + * \param[in] colourTemperature The colour temperature in Kelvin + * + * Compute the white balance gains from a \a colourTemperature. This function + * does not take any statistics into account. It is used to compute the colour + * gains when the user manually specifies a colour temperature. + * + * \return The colour gains + */ + +/** + * \fn AwbAlgorithm::controls() + * \brief Get the controls info map for this algorithm + * + * \return The controls info map + */ + +/** + * \fn AwbAlgorithm::handleControls() + * \param[in] controls The controls to handle + * \brief Handle the controls supplied in a request + */ + +/** + * \brief Parse the mode configurations from the tuning data + * \param[in] tuningData the YamlObject representing the tuning data + * \param[in] def The default value for the AwbMode control + * + * Utility function to parse the tuning data for an AwbMode entry and read all + * provided modes. It adds controls::AwbMode to AwbAlgorithm::controls_ and + * populates AwbAlgorithm::modes_. For a list of possible modes see \ref + * controls::AwbModeEnum. + * + * Each mode entry must contain a "lo" and "hi" key to specify the lower and + * upper colour temperature of that mode. For example: + * + * \code{.unparsed} + * algorithms: + * - Awb: + * AwbMode: + * AwbAuto: + * lo: 2500 + * hi: 8000 + * AwbIncandescent: + * lo: 2500 + * hi: 3000 + * ... + * \endcode + * + * If \a def is supplied but not contained in the the \a tuningData, -EINVAL is + * returned. + * + * \sa controls::AwbModeEnum + * \return Zero on success, negative error code otherwise + */ +int AwbAlgorithm::parseModeConfigs(const YamlObject &tuningData, + const ControlValue &def) +{ + std::vector<ControlValue> availableModes; + + const YamlObject &yamlModes = tuningData[controls::AwbMode.name()]; + if (!yamlModes.isDictionary()) { + LOG(Awb, Error) + << "AwbModes must be a dictionary."; + return -EINVAL; + } + + for (const auto &[modeName, modeDict] : yamlModes.asDict()) { + if (controls::AwbModeNameValueMap.find(modeName) == + controls::AwbModeNameValueMap.end()) { + LOG(Awb, Warning) + << "Skipping unknown AWB mode '" + << modeName << "'"; + continue; + } + + if (!modeDict.isDictionary()) { + LOG(Awb, Error) + << "Invalid AWB mode '" << modeName << "'"; + return -EINVAL; + } + + const auto &modeValue = static_cast<controls::AwbModeEnum>( + controls::AwbModeNameValueMap.at(modeName)); + + ModeConfig &config = modes_[modeValue]; + + auto hi = modeDict["hi"].get<double>(); + if (!hi) { + LOG(Awb, Error) << "Failed to read hi param of mode " + << modeName; + return -EINVAL; + } + config.ctHi = *hi; + + auto lo = modeDict["lo"].get<double>(); + if (!lo) { + LOG(Awb, Error) << "Failed to read low param of mode " + << modeName; + return -EINVAL; + } + config.ctLo = *lo; + + availableModes.push_back(modeValue); + } + + if (modes_.empty()) { + LOG(Awb, Error) << "No AWB modes configured"; + return -EINVAL; + } + + if (!def.isNone() && + modes_.find(def.get<controls::AwbModeEnum>()) == modes_.end()) { + const auto &names = controls::AwbMode.enumerators(); + LOG(Awb, Error) << names.at(def.get<controls::AwbModeEnum>()) + << " mode is missing in the configuration."; + return -EINVAL; + } + + controls_[&controls::AwbMode] = ControlInfo(availableModes, def); + + return 0; +} + +/** + * \class AwbAlgorithm::ModeConfig + * \brief Holds the configuration of a single AWB mode + * + * AWB modes limit the regulation of the AWB algorithm to a specific range of + * colour temperatures. + */ + +/** + * \var AwbAlgorithm::ModeConfig::ctLo + * \brief The lowest valid colour temperature of that mode + */ + +/** + * \var AwbAlgorithm::ModeConfig::ctHi + * \brief The highest valid colour temperature of that mode + */ + +/** + * \var AwbAlgorithm::controls_ + * \brief Controls info map for the controls provided by the algorithm + */ + +/** + * \var AwbAlgorithm::modes_ + * \brief Map of all configured modes + * \sa AwbAlgorithm::parseModeConfigs + */ + +} /* namespace ipa */ + +} /* namespace libcamera */ diff --git a/src/ipa/libipa/awb.h b/src/ipa/libipa/awb.h new file mode 100644 index 00000000..4bab7a45 --- /dev/null +++ b/src/ipa/libipa/awb.h @@ -0,0 +1,66 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +/* + * Copyright (C) 2024 Ideas on Board Oy + * + * Generic AWB algorithms + */ + +#pragma once + +#include <map> + +#include <libcamera/control_ids.h> +#include <libcamera/controls.h> + +#include "libcamera/internal/vector.h" +#include "libcamera/internal/yaml_parser.h" + +namespace libcamera { + +namespace ipa { + +struct AwbResult { + RGB<double> gains; + double colourTemperature; +}; + +struct AwbStats { + virtual double computeColourError(const RGB<double> &gains) const = 0; + virtual RGB<double> rgbMeans() const = 0; + +protected: + ~AwbStats() = default; +}; + +class AwbAlgorithm +{ +public: + virtual ~AwbAlgorithm() = default; + + virtual int init(const YamlObject &tuningData) = 0; + virtual AwbResult calculateAwb(const AwbStats &stats, unsigned int lux) = 0; + virtual RGB<double> gainsFromColourTemperature(double colourTemperature) = 0; + + const ControlInfoMap::Map &controls() const + { + return controls_; + } + + virtual void handleControls([[maybe_unused]] const ControlList &controls) {} + +protected: + int parseModeConfigs(const YamlObject &tuningData, + const ControlValue &def = {}); + + struct ModeConfig { + double ctHi; + double ctLo; + }; + + ControlInfoMap::Map controls_; + std::map<controls::AwbModeEnum, AwbAlgorithm::ModeConfig> modes_; +}; + +} /* namespace ipa */ + +} /* namespace libcamera */ diff --git a/src/ipa/libipa/awb_bayes.cpp b/src/ipa/libipa/awb_bayes.cpp new file mode 100644 index 00000000..d1d0eaf0 --- /dev/null +++ b/src/ipa/libipa/awb_bayes.cpp @@ -0,0 +1,505 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +/* + * Copyright (C) 2019, Raspberry Pi Ltd + * Copyright (C) 2024 Ideas on Board Oy + * + * Implementation of a bayesian AWB algorithm + */ + +#include "awb_bayes.h" + +#include <algorithm> +#include <cmath> +#include <limits> +#include <map> +#include <ostream> +#include <vector> + +#include <libcamera/base/log.h> +#include <libcamera/control_ids.h> + +#include "colours.h" + +/** + * \file awb_bayes.h + * \brief Implementation of bayesian auto white balance algorithm + * + * This implementation is based on the initial implementation done by + * RaspberryPi. + * + * \todo Documentation + * + * \todo Not all the features implemented by RaspberryPi were ported over to + * this algorithm because they either rely on hardware features not generally + * available or were considered not important enough at the moment. + * + * The following parts are not implemented: + * + * - min_pixels: minimum proportion of pixels counted within AWB region for it + * to be "useful" + * - min_g: minimum G value of those pixels, to be regarded a "useful" + * - min_regions: number of AWB regions that must be "useful" in order to do the + * AWB calculation + * - deltaLimit: clamp on colour error term (so as not to penalize non-grey + * excessively) + * - bias_proportion: The biasProportion parameter adds a small proportion of + * the counted pixels to a region biased to the biasCT colour temperature. + * A typical value for biasProportion would be between 0.05 to 0.1. + * - bias_ct: CT target for the search bias + * - sensitivityR: red sensitivity ratio (set to canonical sensor's R/G divided + * by this sensor's R/G) + * - sensitivityB: blue sensitivity ratio (set to canonical sensor's B/G divided + * by this sensor's B/G) + */ + +namespace libcamera { + +LOG_DECLARE_CATEGORY(Awb) + +namespace { + +template<typename T> +class LimitsRecorder +{ +public: + LimitsRecorder() + : min_(std::numeric_limits<T>::max()), + max_(std::numeric_limits<T>::min()) + { + } + + void record(const T &value) + { + min_ = std::min(min_, value); + max_ = std::max(max_, value); + } + + const T &min() const { return min_; } + const T &max() const { return max_; } + +private: + T min_; + T max_; +}; + +#ifndef __DOXYGEN__ +template<typename T> +std::ostream &operator<<(std::ostream &out, const LimitsRecorder<T> &v) +{ + out << "[ " << v.min() << ", " << v.max() << " ]"; + return out; +} +#endif + +} /* namespace */ + +namespace ipa { + +/** + * \brief Step size control for CT search + */ +constexpr double kSearchStep = 0.2; + +/** + * \copydoc libcamera::ipa::Interpolator::interpolate() + */ +template<> +void Interpolator<Pwl>::interpolate(const Pwl &a, const Pwl &b, Pwl &dest, double lambda) +{ + dest = Pwl::combine(a, b, + [=](double /*x*/, double y0, double y1) -> double { + return y0 * (1.0 - lambda) + y1 * lambda; + }); +} + +/** + * \class AwbBayes + * \brief Implementation of a bayesian auto white balance algorithm + * + * In a bayesian AWB algorithm the auto white balance estimation is improved by + * taking the likelihood of a given lightsource based on the estimated lux level + * into account. E.g. If it is very bright we can assume that we are outside and + * that colour temperatures around 6500 are preferred. + * + * The second part of this algorithm is the search for the most likely colour + * temperature. It is implemented in AwbBayes::coarseSearch() and in + * AwbBayes::fineSearch(). The search works very well without prior likelihoods + * and therefore the algorithm itself provides very good results even without + * prior likelihoods. + */ + +/** + * \var AwbBayes::transversePos_ + * \brief How far to wander off CT curve towards "more purple" + */ + +/** + * \var AwbBayes::transverseNeg_ + * \brief How far to wander off CT curve towards "more green" + */ + +/** + * \var AwbBayes::currentMode_ + * \brief The currently selected mode + */ + +int AwbBayes::init(const YamlObject &tuningData) +{ + int ret = colourGainCurve_.readYaml(tuningData["colourGains"], "ct", "gains"); + if (ret) { + LOG(Awb, Error) + << "Failed to parse 'colourGains' " + << "parameter from tuning file"; + return ret; + } + + ctR_.clear(); + ctB_.clear(); + for (const auto &[ct, g] : colourGainCurve_.data()) { + ctR_.append(ct, 1.0 / g[0]); + ctB_.append(ct, 1.0 / g[1]); + } + + /* We will want the inverse functions of these too. */ + ctRInverse_ = ctR_.inverse().first; + ctBInverse_ = ctB_.inverse().first; + + ret = readPriors(tuningData); + if (ret) { + LOG(Awb, Error) << "Failed to read priors"; + return ret; + } + + ret = parseModeConfigs(tuningData, controls::AwbAuto); + if (ret) { + LOG(Awb, Error) + << "Failed to parse mode parameter from tuning file"; + return ret; + } + currentMode_ = &modes_[controls::AwbAuto]; + + transversePos_ = tuningData["transversePos"].get<double>(0.01); + transverseNeg_ = tuningData["transverseNeg"].get<double>(0.01); + if (transversePos_ <= 0 || transverseNeg_ <= 0) { + LOG(Awb, Error) << "AwbConfig: transversePos/Neg must be > 0"; + return -EINVAL; + } + + return 0; +} + +int AwbBayes::readPriors(const YamlObject &tuningData) +{ + const auto &priorsList = tuningData["priors"]; + std::map<uint32_t, Pwl> priors; + + if (!priorsList) { + LOG(Awb, Error) << "No priors specified"; + return -EINVAL; + } + + for (const auto &p : priorsList.asList()) { + if (!p.contains("lux")) { + LOG(Awb, Error) << "Missing lux value"; + return -EINVAL; + } + + uint32_t lux = p["lux"].get<uint32_t>(0); + if (priors.count(lux)) { + LOG(Awb, Error) << "Duplicate prior for lux value " << lux; + return -EINVAL; + } + + std::vector<uint32_t> temperatures = + p["ct"].getList<uint32_t>().value_or(std::vector<uint32_t>{}); + std::vector<double> probabilities = + p["probability"].getList<double>().value_or(std::vector<double>{}); + + if (temperatures.size() != probabilities.size()) { + LOG(Awb, Error) + << "Ct and probability array sizes are unequal"; + return -EINVAL; + } + + if (temperatures.empty()) { + LOG(Awb, Error) + << "Ct and probability arrays are empty"; + return -EINVAL; + } + + std::map<int, double> ctToProbability; + for (unsigned int i = 0; i < temperatures.size(); i++) { + int t = temperatures[i]; + if (ctToProbability.count(t)) { + LOG(Awb, Error) << "Duplicate ct value"; + return -EINVAL; + } + + ctToProbability[t] = probabilities[i]; + } + + auto &pwl = priors[lux]; + for (const auto &[ct, prob] : ctToProbability) { + if (prob < 1e-6) { + LOG(Awb, Error) << "Prior probability must be larger than 1e-6"; + return -EINVAL; + } + pwl.append(ct, prob); + } + } + + if (priors.empty()) { + LOG(Awb, Error) << "No priors specified"; + return -EINVAL; + } + + priors_.setData(std::move(priors)); + + return 0; +} + +void AwbBayes::handleControls(const ControlList &controls) +{ + auto mode = controls.get(controls::AwbMode); + if (mode) { + auto it = modes_.find(static_cast<controls::AwbModeEnum>(*mode)); + if (it != modes_.end()) + currentMode_ = &it->second; + else + LOG(Awb, Error) << "Unsupported AWB mode " << *mode; + } +} + +RGB<double> AwbBayes::gainsFromColourTemperature(double colourTemperature) +{ + /* + * \todo In the RaspberryPi code, the ct curve was interpolated in + * the white point space (1/x) not in gains space. This feels counter + * intuitive, as the gains are in linear space. But I can't prove it. + */ + const auto &gains = colourGainCurve_.getInterpolated(colourTemperature); + return { { gains[0], 1.0, gains[1] } }; +} + +AwbResult AwbBayes::calculateAwb(const AwbStats &stats, unsigned int lux) +{ + ipa::Pwl prior; + if (lux > 0) { + prior = priors_.getInterpolated(lux); + prior.map([](double x, double y) { + LOG(Awb, Debug) << "(" << x << "," << y << ")"; + }); + } else { + prior.append(0, 1.0); + } + + double t = coarseSearch(prior, stats); + double r = ctR_.eval(t); + double b = ctB_.eval(t); + LOG(Awb, Debug) + << "After coarse search: r " << r << " b " << b << " (gains r " + << 1 / r << " b " << 1 / b << ")"; + + /* + * Original comment from RaspberryPi: + * Not entirely sure how to handle the fine search yet. Mostly the + * estimated CT is already good enough, but the fine search allows us to + * wander transversely off the CT curve. Under some illuminants, where + * there may be more or less green light, this may prove beneficial, + * though I probably need more real datasets before deciding exactly how + * this should be controlled and tuned. + */ + fineSearch(t, r, b, prior, stats); + LOG(Awb, Debug) + << "After fine search: r " << r << " b " << b << " (gains r " + << 1 / r << " b " << 1 / b << ")"; + + return { { { 1.0 / r, 1.0, 1.0 / b } }, t }; +} + +double AwbBayes::coarseSearch(const ipa::Pwl &prior, const AwbStats &stats) const +{ + std::vector<Pwl::Point> points; + size_t bestPoint = 0; + double t = currentMode_->ctLo; + int spanR = -1; + int spanB = -1; + LimitsRecorder<double> errorLimits; + LimitsRecorder<double> priorLogLikelihoodLimits; + + /* Step down the CT curve evaluating log likelihood. */ + while (true) { + double r = ctR_.eval(t, &spanR); + double b = ctB_.eval(t, &spanB); + RGB<double> gains({ 1 / r, 1.0, 1 / b }); + double delta2Sum = stats.computeColourError(gains); + double priorLogLikelihood = log(prior.eval(prior.domain().clamp(t))); + double finalLogLikelihood = delta2Sum - priorLogLikelihood; + + errorLimits.record(delta2Sum); + priorLogLikelihoodLimits.record(priorLogLikelihood); + + points.push_back({ { t, finalLogLikelihood } }); + if (points.back().y() < points[bestPoint].y()) + bestPoint = points.size() - 1; + + if (t == currentMode_->ctHi) + break; + + /* + * Ensure even steps along the r/b curve by scaling them by the + * current t. + */ + t = std::min(t + t / 10 * kSearchStep, currentMode_->ctHi); + } + + t = points[bestPoint].x(); + LOG(Awb, Debug) << "Coarse search found CT " << t + << " error limits:" << errorLimits + << " prior log likelihood limits:" << priorLogLikelihoodLimits; + + /* + * We have the best point of the search, but refine it with a quadratic + * interpolation around its neighbors. + */ + if (points.size() > 2) { + bestPoint = std::clamp(bestPoint, std::size_t{ 1 }, points.size() - 2); + t = interpolateQuadratic(points[bestPoint - 1], + points[bestPoint], + points[bestPoint + 1]); + LOG(Awb, Debug) + << "After quadratic refinement, coarse search has CT " + << t; + } + + return t; +} + +void AwbBayes::fineSearch(double &t, double &r, double &b, ipa::Pwl const &prior, const AwbStats &stats) const +{ + int spanR = -1; + int spanB = -1; + double step = t / 10 * kSearchStep * 0.1; + int nsteps = 5; + ctR_.eval(t, &spanR); + ctB_.eval(t, &spanB); + double rDiff = ctR_.eval(t + nsteps * step, &spanR) - + ctR_.eval(t - nsteps * step, &spanR); + double bDiff = ctB_.eval(t + nsteps * step, &spanB) - + ctB_.eval(t - nsteps * step, &spanB); + Pwl::Point transverse({ bDiff, -rDiff }); + if (transverse.length2() < 1e-6) + return; + /* + * transverse is a unit vector orthogonal to the b vs. r function + * (pointing outwards with r and b increasing) + */ + transverse = transverse / transverse.length(); + double bestLogLikelihood = 0; + double bestT = 0; + Pwl::Point bestRB(0); + double transverseRange = transverseNeg_ + transversePos_; + const int maxNumDeltas = 12; + LimitsRecorder<double> errorLimits; + LimitsRecorder<double> priorLogLikelihoodLimits; + + + /* a transverse step approximately every 0.01 r/b units */ + int numDeltas = floor(transverseRange * 100 + 0.5) + 1; + numDeltas = std::clamp(numDeltas, 3, maxNumDeltas); + + /* + * Step down CT curve. March a bit further if the transverse range is + * large. + */ + nsteps += numDeltas; + for (int i = -nsteps; i <= nsteps; i++) { + double tTest = t + i * step; + double priorLogLikelihood = + log(prior.eval(prior.domain().clamp(tTest))); + priorLogLikelihoodLimits.record(priorLogLikelihood); + Pwl::Point rbStart{ { ctR_.eval(tTest, &spanR), + ctB_.eval(tTest, &spanB) } }; + Pwl::Point samples[maxNumDeltas]; + int bestPoint = 0; + + /* + * Sample numDeltas points transversely *off* the CT curve + * in the range [-transverseNeg, transversePos]. + * The x values of a sample contains the distance and the y + * value contains the corresponding log likelihood. + */ + double transverseStep = transverseRange / (numDeltas - 1); + for (int j = 0; j < numDeltas; j++) { + auto &p = samples[j]; + p.x() = -transverseNeg_ + transverseStep * j; + Pwl::Point rbTest = rbStart + transverse * p.x(); + RGB<double> gains({ 1 / rbTest[0], 1.0, 1 / rbTest[1] }); + double delta2Sum = stats.computeColourError(gains); + errorLimits.record(delta2Sum); + p.y() = delta2Sum - priorLogLikelihood; + + if (p.y() < samples[bestPoint].y()) + bestPoint = j; + } + + /* + * We have all samples transversely across the CT curve, + * now let's do a quadratic interpolation for the best result. + */ + bestPoint = std::clamp(bestPoint, 1, numDeltas - 2); + double bestOffset = interpolateQuadratic(samples[bestPoint - 1], + samples[bestPoint], + samples[bestPoint + 1]); + Pwl::Point rbTest = rbStart + transverse * bestOffset; + RGB<double> gains({ 1 / rbTest[0], 1.0, 1 / rbTest[1] }); + double delta2Sum = stats.computeColourError(gains); + errorLimits.record(delta2Sum); + double finalLogLikelihood = delta2Sum - priorLogLikelihood; + + if (bestT == 0 || finalLogLikelihood < bestLogLikelihood) { + bestLogLikelihood = finalLogLikelihood; + bestT = tTest; + bestRB = rbTest; + } + } + + t = bestT; + r = bestRB[0]; + b = bestRB[1]; + LOG(Awb, Debug) + << "Fine search found t " << t << " r " << r << " b " << b + << " error limits: " << errorLimits + << " prior log likelihood limits: " << priorLogLikelihoodLimits; +} + +/** + * \brief Find extremum of function + * \param[in] a First point + * \param[in] b Second point + * \param[in] c Third point + * + * Given 3 points on a curve, find the extremum of the function in that interval + * by fitting a quadratic. + * + * \return The x value of the extremum clamped to the interval [a.x(), c.x()] + */ +double AwbBayes::interpolateQuadratic(Pwl::Point const &a, Pwl::Point const &b, + Pwl::Point const &c) const +{ + const double eps = 1e-3; + Pwl::Point ca = c - a; + Pwl::Point ba = b - a; + double denominator = 2 * (ba.y() * ca.x() - ca.y() * ba.x()); + if (std::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)); + } + /* has degenerated to straight line segment */ + return a.y() < c.y() - eps ? a.x() : (c.y() < a.y() - eps ? c.x() : b.x()); +} + +} /* namespace ipa */ + +} /* namespace libcamera */ diff --git a/src/ipa/libipa/awb_bayes.h b/src/ipa/libipa/awb_bayes.h new file mode 100644 index 00000000..bb933038 --- /dev/null +++ b/src/ipa/libipa/awb_bayes.h @@ -0,0 +1,59 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +/* + * Copyright (C) 2024 Ideas on Board Oy + * + * Base class for bayes AWB algorithms + */ + +#pragma once + +#include <libcamera/controls.h> + +#include "libcamera/internal/vector.h" +#include "libcamera/internal/yaml_parser.h" + +#include "awb.h" +#include "interpolator.h" +#include "pwl.h" + +namespace libcamera { + +namespace ipa { + +class AwbBayes : public AwbAlgorithm +{ +public: + AwbBayes() = default; + + int init(const YamlObject &tuningData) override; + AwbResult calculateAwb(const AwbStats &stats, unsigned int lux) override; + RGB<double> gainsFromColourTemperature(double temperatureK) override; + void handleControls(const ControlList &controls) override; + +private: + int readPriors(const YamlObject &tuningData); + + void fineSearch(double &t, double &r, double &b, ipa::Pwl const &prior, + const AwbStats &stats) const; + double coarseSearch(const ipa::Pwl &prior, const AwbStats &stats) const; + double interpolateQuadratic(ipa::Pwl::Point const &a, + ipa::Pwl::Point const &b, + ipa::Pwl::Point const &c) const; + + Interpolator<Pwl> priors_; + Interpolator<Vector<double, 2>> colourGainCurve_; + + ipa::Pwl ctR_; + ipa::Pwl ctB_; + ipa::Pwl ctRInverse_; + ipa::Pwl ctBInverse_; + + double transversePos_; + double transverseNeg_; + + ModeConfig *currentMode_ = nullptr; +}; + +} /* namespace ipa */ + +} /* namespace libcamera */ diff --git a/src/ipa/libipa/awb_grey.cpp b/src/ipa/libipa/awb_grey.cpp new file mode 100644 index 00000000..d3d2132b --- /dev/null +++ b/src/ipa/libipa/awb_grey.cpp @@ -0,0 +1,114 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +/* + * Copyright (C) 2024 Ideas on Board Oy + * + * Implementation of grey world AWB algorithm + */ + +#include "awb_grey.h" + +#include <algorithm> + +#include <libcamera/base/log.h> +#include <libcamera/control_ids.h> + +#include "colours.h" + +using namespace libcamera::controls; + +/** + * \file awb_grey.h + * \brief Implementation of a grey world AWB algorithm + */ + +namespace libcamera { + +LOG_DECLARE_CATEGORY(Awb) +namespace ipa { + +/** + * \class AwbGrey + * \brief A Grey world auto white balance algorithm + */ + +/** + * \brief Initialize the algorithm with the given tuning data + * \param[in] tuningData The tuning data for the algorithm + * + * Load the colour temperature curve from the tuning data. If there is no tuning + * data available, continue with a warning. Manual colour temperature will not + * work in that case. + * + * \return 0 on success, a negative error code otherwise + */ +int AwbGrey::init(const YamlObject &tuningData) +{ + Interpolator<Vector<double, 2>> gains; + int ret = gains.readYaml(tuningData["colourGains"], "ct", "gains"); + if (ret < 0) + LOG(Awb, Warning) + << "Failed to parse 'colourGains' " + << "parameter from tuning file; " + << "manual colour temperature will not work properly"; + else + colourGainCurve_ = gains; + + return 0; +} + +/** + * \brief Calculate AWB data from the given statistics + * \param[in] stats The statistics to use for the calculation + * \param[in] lux The lux value of the scene + * + * The colour temperature is estimated based on the colours::estimateCCT() + * function. The gains are calculated purely based on the RGB means provided by + * the \a stats. The colour temperature is not taken into account when + * calculating the gains. + * + * The \a lux parameter is not used in this algorithm. + * + * \return The AWB result + */ +AwbResult AwbGrey::calculateAwb(const AwbStats &stats, [[maybe_unused]] unsigned int lux) +{ + AwbResult result; + auto means = stats.rgbMeans(); + result.colourTemperature = estimateCCT(means); + + /* + * Estimate the red and blue gains to apply in a grey world. The green + * gain is hardcoded to 1.0. Avoid divisions by zero by clamping the + * divisor to a minimum value of 1.0. + */ + result.gains.r() = means.g() / std::max(means.r(), 1.0); + result.gains.g() = 1.0; + result.gains.b() = means.g() / std::max(means.b(), 1.0); + return result; +} + +/** + * \brief Compute white balance gains from a colour temperature + * \param[in] colourTemperature The colour temperature in Kelvin + * + * Compute the white balance gains from a \a colourTemperature. This function + * does not take any statistics into account. It simply interpolates the colour + * gains configured in the colour temperature curve. + * + * \return The colour gains if a colour temperature curve is available, + * [1, 1, 1] otherwise. + */ +RGB<double> AwbGrey::gainsFromColourTemperature(double colourTemperature) +{ + if (!colourGainCurve_) { + LOG(Awb, Error) << "No gains defined"; + return RGB<double>({ 1.0, 1.0, 1.0 }); + } + + auto gains = colourGainCurve_->getInterpolated(colourTemperature); + return { { gains[0], 1.0, gains[1] } }; +} + +} /* namespace ipa */ + +} /* namespace libcamera */ diff --git a/src/ipa/libipa/awb_grey.h b/src/ipa/libipa/awb_grey.h new file mode 100644 index 00000000..7ec7bfa5 --- /dev/null +++ b/src/ipa/libipa/awb_grey.h @@ -0,0 +1,36 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +/* + * Copyright (C) 2024 Ideas on Board Oy + * + * AWB grey world algorithm + */ + +#pragma once + +#include <optional> + +#include "libcamera/internal/vector.h" + +#include "awb.h" +#include "interpolator.h" + +namespace libcamera { + +namespace ipa { + +class AwbGrey : public AwbAlgorithm +{ +public: + AwbGrey() = default; + + int init(const YamlObject &tuningData) override; + AwbResult calculateAwb(const AwbStats &stats, unsigned int lux) override; + RGB<double> gainsFromColourTemperature(double colourTemperature) override; + +private: + std::optional<Interpolator<Vector<double, 2>>> colourGainCurve_; +}; + +} /* namespace ipa */ + +} /* namespace libcamera */ diff --git a/src/ipa/libipa/camera_sensor_helper.cpp b/src/ipa/libipa/camera_sensor_helper.cpp index a76e5e75..7c66cd57 100644 --- a/src/ipa/libipa/camera_sensor_helper.cpp +++ b/src/ipa/libipa/camera_sensor_helper.cpp @@ -87,21 +87,16 @@ namespace ipa { */ uint32_t CameraSensorHelper::gainCode(double gain) const { - const AnalogueGainConstants &k = gainConstants_; + if (auto *l = std::get_if<AnalogueGainLinear>(&gain_)) { + ASSERT(l->m0 == 0 || l->m1 == 0); - switch (gainType_) { - case AnalogueGainLinear: - ASSERT(k.linear.m0 == 0 || k.linear.m1 == 0); + return (l->c0 - l->c1 * gain) / + (l->m1 * gain - l->m0); + } else if (auto *e = std::get_if<AnalogueGainExp>(&gain_)) { + ASSERT(e->a != 0 && e->m != 0); - return (k.linear.c0 - k.linear.c1 * gain) / - (k.linear.m1 * gain - k.linear.m0); - - case AnalogueGainExponential: - ASSERT(k.exp.a != 0 && k.exp.m != 0); - - return std::log2(gain / k.exp.a) / k.exp.m; - - default: + return std::log2(gain / e->a) / e->m; + } else { ASSERT(false); return 0; } @@ -119,38 +114,26 @@ uint32_t CameraSensorHelper::gainCode(double gain) const */ double CameraSensorHelper::gain(uint32_t gainCode) const { - const AnalogueGainConstants &k = gainConstants_; double gain = static_cast<double>(gainCode); - switch (gainType_) { - case AnalogueGainLinear: - ASSERT(k.linear.m0 == 0 || k.linear.m1 == 0); - - return (k.linear.m0 * gain + k.linear.c0) / - (k.linear.m1 * gain + k.linear.c1); + if (auto *l = std::get_if<AnalogueGainLinear>(&gain_)) { + ASSERT(l->m0 == 0 || l->m1 == 0); - case AnalogueGainExponential: - ASSERT(k.exp.a != 0 && k.exp.m != 0); + return (l->m0 * gain + l->c0) / + (l->m1 * gain + l->c1); + } else if (auto *e = std::get_if<AnalogueGainExp>(&gain_)) { + ASSERT(e->a != 0 && e->m != 0); - return k.exp.a * std::exp2(k.exp.m * gain); - - default: + return e->a * std::exp2(e->m * gain); + } else { ASSERT(false); return 0.0; } } /** - * \enum CameraSensorHelper::AnalogueGainType - * \brief The gain calculation modes as defined by the MIPI CCS - * - * Describes the image sensor analogue gain capabilities. - * Two modes are possible, depending on the sensor: Linear and Exponential. - */ - -/** - * \var CameraSensorHelper::AnalogueGainLinear - * \brief Gain is computed using linear gain estimation + * \struct CameraSensorHelper::AnalogueGainLinear + * \brief Analogue gain constants for the linear gain model * * The relationship between the integer gain parameter and the resulting gain * multiplier is given by the following equation: @@ -165,11 +148,27 @@ double CameraSensorHelper::gain(uint32_t gainCode) const * The full Gain equation therefore reduces to either: * * \f$gain=\frac{c0}{m1x+c1}\f$ or \f$\frac{m0x+c0}{c1}\f$ + * + * \var CameraSensorHelper::AnalogueGainLinear::m0 + * \brief Constant used in the linear gain coding/decoding + * + * \note Either m0 or m1 shall be zero. + * + * \var CameraSensorHelper::AnalogueGainLinear::c0 + * \brief Constant used in the linear gain coding/decoding + * + * \var CameraSensorHelper::AnalogueGainLinear::m1 + * \brief Constant used in the linear gain coding/decoding + * + * \note Either m0 or m1 shall be zero. + * + * \var CameraSensorHelper::AnalogueGainLinear::c1 + * \brief Constant used in the linear gain coding/decoding */ /** - * \var CameraSensorHelper::AnalogueGainExponential - * \brief Gain is expressed using an exponential model + * \struct CameraSensorHelper::AnalogueGainExp + * \brief Analogue gain constants for the exponential gain model * * The relationship between the integer gain parameter and the resulting gain * multiplier is given by the following equation: @@ -185,67 +184,22 @@ double CameraSensorHelper::gain(uint32_t gainCode) const * * When the gain is expressed in dB, 'a' is equal to 1 and 'm' to * \f$log_{2}{10^{\frac{1}{20}}}\f$. - */ - -/** - * \struct CameraSensorHelper::AnalogueGainLinearConstants - * \brief Analogue gain constants for the linear gain model * - * \var CameraSensorHelper::AnalogueGainLinearConstants::m0 - * \brief Constant used in the linear gain coding/decoding - * - * \note Either m0 or m1 shall be zero. - * - * \var CameraSensorHelper::AnalogueGainLinearConstants::c0 - * \brief Constant used in the linear gain coding/decoding - * - * \var CameraSensorHelper::AnalogueGainLinearConstants::m1 - * \brief Constant used in the linear gain coding/decoding - * - * \note Either m0 or m1 shall be zero. - * - * \var CameraSensorHelper::AnalogueGainLinearConstants::c1 - * \brief Constant used in the linear gain coding/decoding - */ - -/** - * \struct CameraSensorHelper::AnalogueGainExpConstants - * \brief Analogue gain constants for the exponential gain model - * - * \var CameraSensorHelper::AnalogueGainExpConstants::a + * \var CameraSensorHelper::AnalogueGainExp::a * \brief Constant used in the exponential gain coding/decoding * - * \var CameraSensorHelper::AnalogueGainExpConstants::m + * \var CameraSensorHelper::AnalogueGainExp::m * \brief Constant used in the exponential gain coding/decoding */ /** - * \struct CameraSensorHelper::AnalogueGainConstants - * \brief Analogue gain model constants - * - * This union stores the constants used to calculate the analogue gain. The - * CameraSensorHelper::gainType_ variable selects which union member is valid. - * - * \var CameraSensorHelper::AnalogueGainConstants::linear - * \brief Constants for the linear gain model - * - * \var CameraSensorHelper::AnalogueGainConstants::exp - * \brief Constants for the exponential gain model - */ - -/** * \var CameraSensorHelper::blackLevel_ * \brief The black level of the sensor * \sa CameraSensorHelper::blackLevel() */ /** - * \var CameraSensorHelper::gainType_ - * \brief The analogue gain model type - */ - -/** - * \var CameraSensorHelper::gainConstants_ + * \var CameraSensorHelper::gain_ * \brief The analogue gain parameters used for calculation * * The analogue gain is calculated through a formula, and its parameters are @@ -526,8 +480,7 @@ public: { /* From datasheet: 64 at 10bits. */ blackLevel_ = 4096; - gainType_ = AnalogueGainLinear; - gainConstants_.linear = { 100, 0, 0, 1024 }; + gain_ = AnalogueGainLinear{ 100, 0, 0, 1024 }; } }; REGISTER_CAMERA_SENSOR_HELPER("gc05a2", CameraSensorHelperGc05a2) @@ -539,8 +492,7 @@ public: { /* From datasheet: 64 at 10bits. */ blackLevel_ = 4096; - gainType_ = AnalogueGainLinear; - gainConstants_.linear = { 100, 0, 0, 1024 }; + gain_ = AnalogueGainLinear{ 100, 0, 0, 1024 }; } }; REGISTER_CAMERA_SENSOR_HELPER("gc08a3", CameraSensorHelperGc08a3) @@ -552,8 +504,7 @@ public: { /* From datasheet: 64 at 10bits. */ blackLevel_ = 4096; - gainType_ = AnalogueGainLinear; - gainConstants_.linear = { 0, 512, -1, 512 }; + gain_ = AnalogueGainLinear{ 0, 512, -1, 512 }; } }; REGISTER_CAMERA_SENSOR_HELPER("imx214", CameraSensorHelperImx214) @@ -565,8 +516,7 @@ public: { /* From datasheet: 64 at 10bits. */ blackLevel_ = 4096; - gainType_ = AnalogueGainLinear; - gainConstants_.linear = { 0, 256, -1, 256 }; + gain_ = AnalogueGainLinear{ 0, 256, -1, 256 }; } }; REGISTER_CAMERA_SENSOR_HELPER("imx219", CameraSensorHelperImx219) @@ -578,8 +528,7 @@ public: { /* From datasheet: 0x40 at 10bits. */ blackLevel_ = 4096; - gainType_ = AnalogueGainLinear; - gainConstants_.linear = { 0, 512, -1, 512 }; + gain_ = AnalogueGainLinear{ 0, 512, -1, 512 }; } }; REGISTER_CAMERA_SENSOR_HELPER("imx258", CameraSensorHelperImx258) @@ -591,8 +540,7 @@ public: { /* From datasheet: 0x32 at 10bits. */ blackLevel_ = 3200; - gainType_ = AnalogueGainLinear; - gainConstants_.linear = { 0, 2048, -1, 2048 }; + gain_ = AnalogueGainLinear{ 0, 2048, -1, 2048 }; } }; REGISTER_CAMERA_SENSOR_HELPER("imx283", CameraSensorHelperImx283) @@ -604,8 +552,7 @@ public: { /* From datasheet: 0xf0 at 12bits. */ blackLevel_ = 3840; - gainType_ = AnalogueGainExponential; - gainConstants_.exp = { 1.0, expGainDb(0.3) }; + gain_ = AnalogueGainExp{ 1.0, expGainDb(0.3) }; } }; REGISTER_CAMERA_SENSOR_HELPER("imx290", CameraSensorHelperImx290) @@ -615,8 +562,7 @@ class CameraSensorHelperImx296 : public CameraSensorHelper public: CameraSensorHelperImx296() { - gainType_ = AnalogueGainExponential; - gainConstants_.exp = { 1.0, expGainDb(0.1) }; + gain_ = AnalogueGainExp{ 1.0, expGainDb(0.1) }; } }; REGISTER_CAMERA_SENSOR_HELPER("imx296", CameraSensorHelperImx296) @@ -633,8 +579,7 @@ public: { /* From datasheet: 0x32 at 10bits. */ blackLevel_ = 3200; - gainType_ = AnalogueGainExponential; - gainConstants_.exp = { 1.0, expGainDb(0.3) }; + gain_ = AnalogueGainExp{ 1.0, expGainDb(0.3) }; } }; REGISTER_CAMERA_SENSOR_HELPER("imx335", CameraSensorHelperImx335) @@ -644,8 +589,7 @@ class CameraSensorHelperImx415 : public CameraSensorHelper public: CameraSensorHelperImx415() { - gainType_ = AnalogueGainExponential; - gainConstants_.exp = { 1.0, expGainDb(0.3) }; + gain_ = AnalogueGainExp{ 1.0, expGainDb(0.3) }; } }; REGISTER_CAMERA_SENSOR_HELPER("imx415", CameraSensorHelperImx415) @@ -660,8 +604,7 @@ class CameraSensorHelperImx477 : public CameraSensorHelper public: CameraSensorHelperImx477() { - gainType_ = AnalogueGainLinear; - gainConstants_.linear = { 0, 1024, -1, 1024 }; + gain_ = AnalogueGainLinear{ 0, 1024, -1, 1024 }; } }; REGISTER_CAMERA_SENSOR_HELPER("imx477", CameraSensorHelperImx477) @@ -675,8 +618,7 @@ public: * The Sensor Manual doesn't appear to document the gain model. * This has been validated with some empirical testing only. */ - gainType_ = AnalogueGainLinear; - gainConstants_.linear = { 1, 0, 0, 128 }; + gain_ = AnalogueGainLinear{ 1, 0, 0, 128 }; } }; REGISTER_CAMERA_SENSOR_HELPER("ov2685", CameraSensorHelperOv2685) @@ -686,8 +628,7 @@ class CameraSensorHelperOv2740 : public CameraSensorHelper public: CameraSensorHelperOv2740() { - gainType_ = AnalogueGainLinear; - gainConstants_.linear = { 1, 0, 0, 128 }; + gain_ = AnalogueGainLinear{ 1, 0, 0, 128 }; } }; REGISTER_CAMERA_SENSOR_HELPER("ov2740", CameraSensorHelperOv2740) @@ -699,8 +640,7 @@ public: { /* From datasheet: 0x40 at 12bits. */ blackLevel_ = 1024; - gainType_ = AnalogueGainLinear; - gainConstants_.linear = { 1, 0, 0, 128 }; + gain_ = AnalogueGainLinear{ 1, 0, 0, 128 }; } }; REGISTER_CAMERA_SENSOR_HELPER("ov4689", CameraSensorHelperOv4689) @@ -712,8 +652,7 @@ public: { /* From datasheet: 0x10 at 10bits. */ blackLevel_ = 1024; - gainType_ = AnalogueGainLinear; - gainConstants_.linear = { 1, 0, 0, 16 }; + gain_ = AnalogueGainLinear{ 1, 0, 0, 16 }; } }; REGISTER_CAMERA_SENSOR_HELPER("ov5640", CameraSensorHelperOv5640) @@ -723,8 +662,7 @@ class CameraSensorHelperOv5647 : public CameraSensorHelper public: CameraSensorHelperOv5647() { - gainType_ = AnalogueGainLinear; - gainConstants_.linear = { 1, 0, 0, 16 }; + gain_ = AnalogueGainLinear{ 1, 0, 0, 16 }; } }; REGISTER_CAMERA_SENSOR_HELPER("ov5647", CameraSensorHelperOv5647) @@ -734,8 +672,7 @@ class CameraSensorHelperOv5670 : public CameraSensorHelper public: CameraSensorHelperOv5670() { - gainType_ = AnalogueGainLinear; - gainConstants_.linear = { 1, 0, 0, 128 }; + gain_ = AnalogueGainLinear{ 1, 0, 0, 128 }; } }; REGISTER_CAMERA_SENSOR_HELPER("ov5670", CameraSensorHelperOv5670) @@ -747,8 +684,7 @@ public: { /* From Linux kernel driver: 0x40 at 10bits. */ blackLevel_ = 4096; - gainType_ = AnalogueGainLinear; - gainConstants_.linear = { 1, 0, 0, 128 }; + gain_ = AnalogueGainLinear{ 1, 0, 0, 128 }; } }; REGISTER_CAMERA_SENSOR_HELPER("ov5675", CameraSensorHelperOv5675) @@ -758,8 +694,7 @@ class CameraSensorHelperOv5693 : public CameraSensorHelper public: CameraSensorHelperOv5693() { - gainType_ = AnalogueGainLinear; - gainConstants_.linear = { 1, 0, 0, 16 }; + gain_ = AnalogueGainLinear{ 1, 0, 0, 16 }; } }; REGISTER_CAMERA_SENSOR_HELPER("ov5693", CameraSensorHelperOv5693) @@ -769,8 +704,7 @@ class CameraSensorHelperOv64a40 : public CameraSensorHelper public: CameraSensorHelperOv64a40() { - gainType_ = AnalogueGainLinear; - gainConstants_.linear = { 1, 0, 0, 128 }; + gain_ = AnalogueGainLinear{ 1, 0, 0, 128 }; } }; REGISTER_CAMERA_SENSOR_HELPER("ov64a40", CameraSensorHelperOv64a40) @@ -780,15 +714,13 @@ class CameraSensorHelperOv8858 : public CameraSensorHelper public: CameraSensorHelperOv8858() { - gainType_ = AnalogueGainLinear; - /* * \todo Validate the selected 1/128 step value as it differs * from what the sensor manual describes. * * See: https://patchwork.linuxtv.org/project/linux-media/patch/20221106171129.166892-2-nicholas@rothemail.net/#142267 */ - gainConstants_.linear = { 1, 0, 0, 128 }; + gain_ = AnalogueGainLinear{ 1, 0, 0, 128 }; } }; REGISTER_CAMERA_SENSOR_HELPER("ov8858", CameraSensorHelperOv8858) @@ -798,8 +730,7 @@ class CameraSensorHelperOv8865 : public CameraSensorHelper public: CameraSensorHelperOv8865() { - gainType_ = AnalogueGainLinear; - gainConstants_.linear = { 1, 0, 0, 128 }; + gain_ = AnalogueGainLinear{ 1, 0, 0, 128 }; } }; REGISTER_CAMERA_SENSOR_HELPER("ov8865", CameraSensorHelperOv8865) @@ -809,8 +740,7 @@ class CameraSensorHelperOv13858 : public CameraSensorHelper public: CameraSensorHelperOv13858() { - gainType_ = AnalogueGainLinear; - gainConstants_.linear = { 1, 0, 0, 128 }; + gain_ = AnalogueGainLinear{ 1, 0, 0, 128 }; } }; REGISTER_CAMERA_SENSOR_HELPER("ov13858", CameraSensorHelperOv13858) diff --git a/src/ipa/libipa/camera_sensor_helper.h b/src/ipa/libipa/camera_sensor_helper.h index 75868205..a9300a64 100644 --- a/src/ipa/libipa/camera_sensor_helper.h +++ b/src/ipa/libipa/camera_sensor_helper.h @@ -11,6 +11,7 @@ #include <optional> #include <stdint.h> #include <string> +#include <variant> #include <vector> #include <libcamera/base/class.h> @@ -30,31 +31,20 @@ public: virtual double gain(uint32_t gainCode) const; protected: - enum AnalogueGainType { - AnalogueGainLinear, - AnalogueGainExponential, - }; - - struct AnalogueGainLinearConstants { + struct AnalogueGainLinear { int16_t m0; int16_t c0; int16_t m1; int16_t c1; }; - struct AnalogueGainExpConstants { + struct AnalogueGainExp { double a; double m; }; - union AnalogueGainConstants { - AnalogueGainLinearConstants linear; - AnalogueGainExpConstants exp; - }; - std::optional<int16_t> blackLevel_; - AnalogueGainType gainType_; - AnalogueGainConstants gainConstants_; + std::variant<std::monostate, AnalogueGainLinear, AnalogueGainExp> gain_; private: LIBCAMERA_DISABLE_COPY_AND_MOVE(CameraSensorHelper) diff --git a/src/ipa/libipa/colours.h b/src/ipa/libipa/colours.h index fa6a8b57..d39b2ca8 100644 --- a/src/ipa/libipa/colours.h +++ b/src/ipa/libipa/colours.h @@ -9,7 +9,7 @@ #include <stdint.h> -#include "vector.h" +#include "libcamera/internal/vector.h" namespace libcamera { diff --git a/src/ipa/libipa/exposure_mode_helper.cpp b/src/ipa/libipa/exposure_mode_helper.cpp index f235316d..0c1e99e3 100644 --- a/src/ipa/libipa/exposure_mode_helper.cpp +++ b/src/ipa/libipa/exposure_mode_helper.cpp @@ -182,6 +182,7 @@ ExposureModeHelper::splitExposure(utils::Duration exposure) const * the stage limits are initialised. */ + /* Clamp the gain to lastStageGain and regulate exposureTime. */ if (stageExposureTime * lastStageGain >= exposure) { exposureTime = clampExposureTime(exposure / clampGain(lastStageGain)); gain = clampGain(exposure / exposureTime); @@ -189,8 +190,9 @@ ExposureModeHelper::splitExposure(utils::Duration exposure) const return { exposureTime, gain, exposure / (exposureTime * gain) }; } + /* Clamp the exposureTime to stageExposureTime and regulate gain. */ if (stageExposureTime * stageGain >= exposure) { - exposureTime = clampExposureTime(exposure / clampGain(stageGain)); + exposureTime = clampExposureTime(stageExposureTime); gain = clampGain(exposure / exposureTime); return { exposureTime, gain, exposure / (exposureTime * gain) }; diff --git a/src/ipa/rkisp1/utils.cpp b/src/ipa/libipa/fixedpoint.cpp index 960ec64e..6b698fc5 100644 --- a/src/ipa/rkisp1/utils.cpp +++ b/src/ipa/libipa/fixedpoint.cpp @@ -2,18 +2,18 @@ /* * Copyright (C) 2024, Paul Elder <paul.elder@ideasonboard.com> * - * Miscellaneous utility functions specific to rkisp1 + * Fixed / floating point conversions */ -#include "utils.h" +#include "fixedpoint.h" /** - * \file utils.h + * \file fixedpoint.h */ namespace libcamera { -namespace ipa::rkisp1::utils { +namespace ipa { /** * \fn R floatingToFixedPoint(T number) @@ -37,6 +37,6 @@ namespace ipa::rkisp1::utils { * \return The converted value */ -} /* namespace ipa::rkisp1::utils */ +} /* namespace ipa */ } /* namespace libcamera */ diff --git a/src/ipa/rkisp1/utils.h b/src/ipa/libipa/fixedpoint.h index 5f38b50b..709cf50f 100644 --- a/src/ipa/rkisp1/utils.h +++ b/src/ipa/libipa/fixedpoint.h @@ -2,7 +2,7 @@ /* * Copyright (C) 2024, Paul Elder <paul.elder@ideasonboard.com> * - * Miscellaneous utility functions specific to rkisp1 + * Fixed / floating point conversions */ #pragma once @@ -12,7 +12,7 @@ namespace libcamera { -namespace ipa::rkisp1::utils { +namespace ipa { #ifndef __DOXYGEN__ template<unsigned int I, unsigned int F, typename R, typename T, @@ -60,6 +60,6 @@ constexpr R fixedToFloatingPoint(T number) return static_cast<R>(t) / static_cast<R>(1 << F); } -} /* namespace ipa::rkisp1::utils */ +} /* namespace ipa */ } /* namespace libcamera */ diff --git a/src/ipa/libipa/interpolator.cpp b/src/ipa/libipa/interpolator.cpp index 73e8d3b7..f901a86e 100644 --- a/src/ipa/libipa/interpolator.cpp +++ b/src/ipa/libipa/interpolator.cpp @@ -126,6 +126,13 @@ namespace ipa { */ /** + * \fn std::map<unsigned int, T> &Interpolator<T>::data() const + * \brief Access the internal map + * + * \return The internal map + */ + +/** * \fn const T& Interpolator<T>::getInterpolated() * \brief Retrieve an interpolated value for the given key * \param[in] key The unsigned integer key of the object to retrieve @@ -136,8 +143,7 @@ namespace ipa { */ /** - * \fn void Interpolator<T>::interpolate(const T &a, const T &b, T &dest, double - * lambda) + * \fn void Interpolator<T>::interpolate(const T &a, const T &b, T &dest, double lambda) * \brief Interpolate between two instances of T * \param a The first value to interpolate * \param b The second value to interpolate diff --git a/src/ipa/libipa/interpolator.h b/src/ipa/libipa/interpolator.h index fffce214..7880db69 100644 --- a/src/ipa/libipa/interpolator.h +++ b/src/ipa/libipa/interpolator.h @@ -81,6 +81,11 @@ public: lastInterpolatedKey_.reset(); } + const std::map<unsigned int, T> &data() const + { + return data_; + } + const T &getInterpolated(unsigned int key, unsigned int *quantizedKey = nullptr) { ASSERT(data_.size() > 0); diff --git a/src/ipa/libipa/lux.cpp b/src/ipa/libipa/lux.cpp new file mode 100644 index 00000000..899e8824 --- /dev/null +++ b/src/ipa/libipa/lux.cpp @@ -0,0 +1,173 @@ +/* SPDX-License-Identifier: BSD-2-Clause */ +/* + * Copyright (C) 2019, Raspberry Pi Ltd + * Copyright (C) 2024, Paul Elder <paul.elder@ideasonboard.com> + * + * Helper class that implements lux estimation + */ +#include "lux.h" + +#include <algorithm> +#include <chrono> + +#include <libcamera/base/log.h> + +#include "libcamera/internal/yaml_parser.h" + +#include "histogram.h" + +/** + * \file lux.h + * \brief Helper class that implements lux estimation + * + * Estimating the lux level of an image is a common operation that can for + * instance be used to adjust the target Y value in AGC or for Bayesian AWB + * estimation. + */ + +namespace libcamera { + +using namespace std::literals::chrono_literals; + +LOG_DEFINE_CATEGORY(Lux) + +namespace ipa { + +/** + * \class Lux + * \brief Class that implements lux estimation + * + * IPAs that wish to use lux estimation should create a Lux algorithm module + * that lightly wraps this module by providing the platform-specific luminance + * histogram. The Lux entry in the tuning file must then precede the algorithms + * that depend on the estimated lux value. + */ + +/** + * \var Lux::referenceExposureTime_ + * \brief The exposure time of the reference image, in microseconds + */ + +/** + * \var Lux::referenceAnalogueGain_ + * \brief The analogue gain of the reference image + */ + +/** + * \var Lux::referenceDigitalGain_ + * \brief The analogue gain of the reference image + */ + +/** + * \var Lux::referenceY_ + * \brief The measured luminance of the reference image, normalized to 1 + * + */ + +/** + * \var Lux::referenceLux_ + * \brief The estimated lux level of the reference image + */ + +/** + * \brief Construct the Lux helper module + */ +Lux::Lux() +{ +} + +/** + * \brief Parse tuning data + * \param[in] tuningData The YamlObject representing the tuning data + * + * This function parses yaml tuning data for the common Lux module. It requires + * reference exposure time, analogue gain, digital gain, and lux values. + * + * \code{.unparsed} + * algorithms: + * - Lux: + * referenceExposureTime: 10000 + * referenceAnalogueGain: 4.0 + * referenceDigitalGain: 1.0 + * referenceY: 0.1831 + * referenceLux: 1000 + * \endcode + * + * \return 0 on success or a negative error code + */ +int Lux::parseTuningData(const YamlObject &tuningData) +{ + auto value = tuningData["referenceExposureTime"].get<double>(); + if (!value) { + LOG(Lux, Error) << "Missing tuning parameter: " + << "'referenceExposureTime'"; + return -EINVAL; + } + referenceExposureTime_ = *value * 1.0us; + + value = tuningData["referenceAnalogueGain"].get<double>(); + if (!value) { + LOG(Lux, Error) << "Missing tuning parameter: " + << "'referenceAnalogueGain'"; + return -EINVAL; + } + referenceAnalogueGain_ = *value; + + value = tuningData["referenceDigitalGain"].get<double>(); + if (!value) { + LOG(Lux, Error) << "Missing tuning parameter: " + << "'referenceDigitalGain'"; + return -EINVAL; + } + referenceDigitalGain_ = *value; + + value = tuningData["referenceY"].get<double>(); + if (!value) { + LOG(Lux, Error) << "Missing tuning parameter: " + << "'referenceY'"; + return -EINVAL; + } + referenceY_ = *value; + + value = tuningData["referenceLux"].get<double>(); + if (!value) { + LOG(Lux, Error) << "Missing tuning parameter: " + << "'referenceLux'"; + return -EINVAL; + } + referenceLux_ = *value; + + return 0; +} + +/** + * \brief Estimate lux given runtime values + * \param[in] exposureTime Exposure time applied to the frame + * \param[in] aGain Analogue gain applied to the frame + * \param[in] dGain Digital gain applied to the frame + * \param[in] yHist Histogram from the ISP statistics + * + * Estimate the lux given the exposure time, gain, and histogram. + * + * \return Estimated lux value + */ +double Lux::estimateLux(utils::Duration exposureTime, + double aGain, double dGain, + const Histogram &yHist) const +{ + double currentY = yHist.interQuantileMean(0, 1); + double exposureTimeRatio = referenceExposureTime_ / exposureTime; + double aGainRatio = referenceAnalogueGain_ / aGain; + double dGainRatio = referenceDigitalGain_ / dGain; + double yRatio = (currentY / yHist.bins()) / referenceY_; + + double estimatedLux = exposureTimeRatio * aGainRatio * dGainRatio * + yRatio * referenceLux_; + + LOG(Lux, Debug) << "Estimated lux " << estimatedLux; + return estimatedLux; +} + +} /* namespace ipa */ + +} /* namespace libcamera */ diff --git a/src/ipa/libipa/lux.h b/src/ipa/libipa/lux.h new file mode 100644 index 00000000..d95bcdaf --- /dev/null +++ b/src/ipa/libipa/lux.h @@ -0,0 +1,41 @@ +/* SPDX-License-Identifier: BSD-2-Clause */ +/* + * Copyright (C) 2019, Raspberry Pi Ltd + * Copyright (C) 2024, Paul Elder <paul.elder@ideasonboard.com> + * + * Helper class that implements lux estimation + */ + +#pragma once + +#include <libcamera/base/utils.h> + +namespace libcamera { + +class YamlObject; + +namespace ipa { + +class Histogram; + +class Lux +{ +public: + Lux(); + + int parseTuningData(const YamlObject &tuningData); + double estimateLux(utils::Duration exposureTime, + double aGain, double dGain, + const Histogram &yHist) const; + +private: + utils::Duration referenceExposureTime_; + double referenceAnalogueGain_; + double referenceDigitalGain_; + double referenceY_; + double referenceLux_; +}; + +} /* namespace ipa */ + +} /* namespace libcamera */ diff --git a/src/ipa/libipa/matrix.h b/src/ipa/libipa/matrix.h deleted file mode 100644 index 5471e697..00000000 --- a/src/ipa/libipa/matrix.h +++ /dev/null @@ -1,203 +0,0 @@ -/* 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 <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/meson.build b/src/ipa/libipa/meson.build index 788d037a..660be940 100644 --- a/src/ipa/libipa/meson.build +++ b/src/ipa/libipa/meson.build @@ -3,33 +3,39 @@ libipa_headers = files([ 'agc_mean_luminance.h', 'algorithm.h', + 'awb_bayes.h', + 'awb_grey.h', + 'awb.h', 'camera_sensor_helper.h', 'colours.h', 'exposure_mode_helper.h', 'fc_queue.h', + 'fixedpoint.h', 'histogram.h', 'interpolator.h', 'lsc_polynomial.h', - 'matrix.h', + 'lux.h', 'module.h', 'pwl.h', - 'vector.h', ]) libipa_sources = files([ 'agc_mean_luminance.cpp', 'algorithm.cpp', + 'awb_bayes.cpp', + 'awb_grey.cpp', + 'awb.cpp', 'camera_sensor_helper.cpp', 'colours.cpp', 'exposure_mode_helper.cpp', 'fc_queue.cpp', + 'fixedpoint.cpp', 'histogram.cpp', 'interpolator.cpp', 'lsc_polynomial.cpp', - 'matrix.cpp', + 'lux.cpp', 'module.cpp', 'pwl.cpp', - 'vector.cpp', ]) libipa_includes = include_directories('..') diff --git a/src/ipa/libipa/pwl.cpp b/src/ipa/libipa/pwl.cpp index 88fe2022..3fa005ba 100644 --- a/src/ipa/libipa/pwl.cpp +++ b/src/ipa/libipa/pwl.cpp @@ -160,6 +160,11 @@ void Pwl::prepend(double x, double y, const double eps) */ /** + * \fn Pwl::clear() + * \brief Clear the piecewise linear function + */ + +/** * \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 diff --git a/src/ipa/libipa/pwl.h b/src/ipa/libipa/pwl.h index d4ec9f4f..c1496c30 100644 --- a/src/ipa/libipa/pwl.h +++ b/src/ipa/libipa/pwl.h @@ -12,7 +12,7 @@ #include <utility> #include <vector> -#include "vector.h" +#include "libcamera/internal/vector.h" namespace libcamera { @@ -49,6 +49,7 @@ public: void append(double x, double y, double eps = 1e-6); bool empty() const { return points_.empty(); } + void clear() { points_.clear(); } size_t size() const { return points_.size(); } Interval domain() const; diff --git a/src/ipa/libipa/vector.h b/src/ipa/libipa/vector.h deleted file mode 100644 index 9bdd54b6..00000000 --- a/src/ipa/libipa/vector.h +++ /dev/null @@ -1,371 +0,0 @@ -/* 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 <functional> -#include <numeric> -#include <optional> -#include <ostream> - -#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 explicit Vector(T scalar) - { - data_.fill(scalar); - } - - 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]; - } - - 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 operator+(const Vector &other) const - { - return apply(*this, other, std::plus<>{}); - } - - constexpr Vector operator+(T scalar) const - { - return apply(*this, scalar, std::plus<>{}); - } - - constexpr Vector operator-(const Vector &other) const - { - return apply(*this, other, std::minus<>{}); - } - - constexpr Vector operator-(T scalar) const - { - return apply(*this, scalar, std::minus<>{}); - } - - constexpr Vector operator*(const Vector &other) const - { - return apply(*this, other, std::multiplies<>{}); - } - - constexpr Vector operator*(T scalar) const - { - return apply(*this, scalar, std::multiplies<>{}); - } - - constexpr Vector operator/(const Vector &other) const - { - return apply(*this, other, std::divides<>{}); - } - - constexpr Vector operator/(T scalar) const - { - return apply(*this, scalar, std::divides<>{}); - } - - Vector &operator+=(const Vector &other) - { - return apply(other, [](T a, T b) { return a + b; }); - } - - Vector &operator+=(T scalar) - { - return apply(scalar, [](T a, T b) { return a + b; }); - } - - Vector &operator-=(const Vector &other) - { - return apply(other, [](T a, T b) { return a - b; }); - } - - Vector &operator-=(T scalar) - { - return apply(scalar, [](T a, T b) { return a - b; }); - } - - Vector &operator*=(const Vector &other) - { - return apply(other, [](T a, T b) { return a * b; }); - } - - Vector &operator*=(T scalar) - { - return apply(scalar, [](T a, T b) { return a * b; }); - } - - Vector &operator/=(const Vector &other) - { - return apply(other, [](T a, T b) { return a / b; }); - } - - Vector &operator/=(T scalar) - { - return apply(scalar, [](T a, T b) { return a / b; }); - } - - constexpr Vector min(const Vector &other) const - { - return apply(*this, other, [](T a, T b) { return std::min(a, b); }); - } - - constexpr Vector min(T scalar) const - { - return apply(*this, scalar, [](T a, T b) { return std::min(a, b); }); - } - - constexpr Vector max(const Vector &other) const - { - return apply(*this, other, [](T a, T b) { return std::max(a, b); }); - } - - constexpr Vector max(T scalar) const - { - return apply(*this, scalar, [](T a, T b) -> T { return std::max(a, b); }); - } - - constexpr T dot(const Vector<T, Rows> &other) const - { - T ret = 0; - for (unsigned int i = 0; i < Rows; i++) - ret += data_[i] * other[i]; - return ret; - } - -#ifndef __DOXYGEN__ - template<bool Dependent = false, typename = std::enable_if_t<Dependent || Rows >= 1>> -#endif /* __DOXYGEN__ */ - constexpr const T &x() const { return data_[0]; } -#ifndef __DOXYGEN__ - template<bool Dependent = false, typename = std::enable_if_t<Dependent || Rows >= 2>> -#endif /* __DOXYGEN__ */ - constexpr const T &y() const { return data_[1]; } -#ifndef __DOXYGEN__ - template<bool Dependent = false, typename = std::enable_if_t<Dependent || Rows >= 3>> -#endif /* __DOXYGEN__ */ - constexpr const T &z() const { return data_[2]; } -#ifndef __DOXYGEN__ - template<bool Dependent = false, typename = std::enable_if_t<Dependent || Rows >= 1>> -#endif /* __DOXYGEN__ */ - constexpr T &x() { return data_[0]; } -#ifndef __DOXYGEN__ - template<bool Dependent = false, typename = std::enable_if_t<Dependent || Rows >= 2>> -#endif /* __DOXYGEN__ */ - constexpr T &y() { return data_[1]; } -#ifndef __DOXYGEN__ - template<bool Dependent = false, typename = std::enable_if_t<Dependent || Rows >= 3>> -#endif /* __DOXYGEN__ */ - constexpr T &z() { return data_[2]; } - -#ifndef __DOXYGEN__ - template<bool Dependent = false, typename = std::enable_if_t<Dependent || Rows >= 1>> -#endif /* __DOXYGEN__ */ - constexpr const T &r() const { return data_[0]; } -#ifndef __DOXYGEN__ - template<bool Dependent = false, typename = std::enable_if_t<Dependent || Rows >= 2>> -#endif /* __DOXYGEN__ */ - constexpr const T &g() const { return data_[1]; } -#ifndef __DOXYGEN__ - template<bool Dependent = false, typename = std::enable_if_t<Dependent || Rows >= 3>> -#endif /* __DOXYGEN__ */ - constexpr const T &b() const { return data_[2]; } -#ifndef __DOXYGEN__ - template<bool Dependent = false, typename = std::enable_if_t<Dependent || Rows >= 1>> -#endif /* __DOXYGEN__ */ - constexpr T &r() { return data_[0]; } -#ifndef __DOXYGEN__ - template<bool Dependent = false, typename = std::enable_if_t<Dependent || Rows >= 2>> -#endif /* __DOXYGEN__ */ - constexpr T &g() { return data_[1]; } -#ifndef __DOXYGEN__ - template<bool Dependent = false, typename = std::enable_if_t<Dependent || Rows >= 3>> -#endif /* __DOXYGEN__ */ - constexpr T &b() { return data_[2]; } - - 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()); - } - - template<typename R = T> - constexpr R sum() const - { - return std::accumulate(data_.begin(), data_.end(), R{}); - } - -private: - template<class BinaryOp> - static constexpr Vector apply(const Vector &lhs, const Vector &rhs, BinaryOp op) - { - Vector result; - std::transform(lhs.data_.begin(), lhs.data_.end(), - rhs.data_.begin(), result.data_.begin(), - op); - - return result; - } - - template<class BinaryOp> - static constexpr Vector apply(const Vector &lhs, T rhs, BinaryOp op) - { - Vector result; - std::transform(lhs.data_.begin(), lhs.data_.end(), - result.data_.begin(), - [&op, rhs](T v) { return op(v, rhs); }); - - return result; - } - - template<class BinaryOp> - Vector &apply(const Vector &other, BinaryOp op) - { - auto itOther = other.data_.begin(); - std::for_each(data_.begin(), data_.end(), - [&op, &itOther](T &v) { v = op(v, *itOther++); }); - - return *this; - } - - template<class BinaryOp> - Vector &apply(T scalar, BinaryOp op) - { - std::for_each(data_.begin(), data_.end(), - [&op, scalar](T &v) { v = op(v, scalar); }); - - return *this; - } - - std::array<T, Rows> data_; -}; - -template<typename T> -using RGB = Vector<T, 3>; - -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/mali-c55/algorithms/agc.cpp b/src/ipa/mali-c55/algorithms/agc.cpp new file mode 100644 index 00000000..70667db3 --- /dev/null +++ b/src/ipa/mali-c55/algorithms/agc.cpp @@ -0,0 +1,410 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +/* + * Copyright (C) 2024, Ideas On Board Oy + * + * agc.cpp - AGC/AEC mean-based control algorithm + */ + +#include "agc.h" + +#include <cmath> + +#include <libcamera/base/log.h> +#include <libcamera/base/utils.h> + +#include <libcamera/control_ids.h> +#include <libcamera/property_ids.h> + +#include "libipa/colours.h" +#include "libipa/fixedpoint.h" + +namespace libcamera { + +using namespace std::literals::chrono_literals; + +namespace ipa::mali_c55::algorithms { + +LOG_DEFINE_CATEGORY(MaliC55Agc) + +/* + * Number of histogram bins. This is only true for the specific configuration we + * set to the ISP; 4 separate histograms of 256 bins each. If that configuration + * ever changes then this constant will need updating. + */ +static constexpr unsigned int kNumHistogramBins = 256; + +/* + * The Mali-C55 ISP has a digital gain block which allows setting gain in Q5.8 + * format, a range of 0.0 to (very nearly) 32.0. We clamp from 1.0 to the actual + * max value which is 8191 * 2^-8. + */ +static constexpr double kMinDigitalGain = 1.0; +static constexpr double kMaxDigitalGain = 31.99609375; + +uint32_t AgcStatistics::decodeBinValue(uint16_t binVal) +{ + int exponent = (binVal & 0xf000) >> 12; + int mantissa = binVal & 0xfff; + + if (!exponent) + return mantissa * 2; + else + return (mantissa + 4096) * std::pow(2, exponent); +} + +/* + * We configure the ISP to give us 4 histograms of 256 bins each, with + * a single histogram per colour channel (R/Gr/Gb/B). The memory space + * containing the data is a single block containing all 4 histograms + * with the position of each colour's histogram within it dependent on + * the bayer pattern of the data input to the ISP. + * + * NOTE: The validity of this function depends on the parameters we have + * configured. With different skip/offset x, y values not all of the + * colour channels would be populated, and they may not be in the same + * planes as calculated here. + */ +int AgcStatistics::setBayerOrderIndices(BayerFormat::Order bayerOrder) +{ + switch (bayerOrder) { + case BayerFormat::Order::RGGB: + rIndex_ = 0; + grIndex_ = 1; + gbIndex_ = 2; + bIndex_ = 3; + break; + case BayerFormat::Order::GRBG: + grIndex_ = 0; + rIndex_ = 1; + bIndex_ = 2; + gbIndex_ = 3; + break; + case BayerFormat::Order::GBRG: + gbIndex_ = 0; + bIndex_ = 1; + rIndex_ = 2; + grIndex_ = 3; + break; + case BayerFormat::Order::BGGR: + bIndex_ = 0; + gbIndex_ = 1; + grIndex_ = 2; + rIndex_ = 3; + break; + default: + LOG(MaliC55Agc, Error) + << "Invalid bayer format " << bayerOrder; + return -EINVAL; + } + + return 0; +} + +void AgcStatistics::parseStatistics(const mali_c55_stats_buffer *stats) +{ + uint32_t r[256], g[256], b[256], y[256]; + + /* + * We need to decode the bin values for each histogram from their 16-bit + * compressed values to a 32-bit value. We also take the average of the + * Gr/Gb values into a single green histogram. + */ + for (unsigned int i = 0; i < 256; i++) { + r[i] = decodeBinValue(stats->ae_1024bin_hist.bins[i + (256 * rIndex_)]); + g[i] = (decodeBinValue(stats->ae_1024bin_hist.bins[i + (256 * grIndex_)]) + + decodeBinValue(stats->ae_1024bin_hist.bins[i + (256 * gbIndex_)])) / 2; + b[i] = decodeBinValue(stats->ae_1024bin_hist.bins[i + (256 * bIndex_)]); + + y[i] = rec601LuminanceFromRGB({ { static_cast<double>(r[i]), + static_cast<double>(g[i]), + static_cast<double>(b[i]) } }); + } + + rHist = Histogram(Span<uint32_t>(r, kNumHistogramBins)); + gHist = Histogram(Span<uint32_t>(g, kNumHistogramBins)); + bHist = Histogram(Span<uint32_t>(b, kNumHistogramBins)); + yHist = Histogram(Span<uint32_t>(y, kNumHistogramBins)); +} + +Agc::Agc() + : AgcMeanLuminance() +{ +} + +int Agc::init(IPAContext &context, const YamlObject &tuningData) +{ + int ret = parseTuningData(tuningData); + if (ret) + return ret; + + context.ctrlMap[&controls::AeEnable] = ControlInfo(false, true); + context.ctrlMap[&controls::DigitalGain] = ControlInfo( + static_cast<float>(kMinDigitalGain), + static_cast<float>(kMaxDigitalGain), + static_cast<float>(kMinDigitalGain) + ); + context.ctrlMap.merge(controls()); + + return 0; +} + +int Agc::configure(IPAContext &context, + [[maybe_unused]] const IPACameraSensorInfo &configInfo) +{ + int ret = statistics_.setBayerOrderIndices(context.configuration.sensor.bayerOrder); + if (ret) + return ret; + + /* + * Defaults; we use whatever the sensor's default exposure is and the + * minimum analogue gain. AEGC is _active_ by default. + */ + context.activeState.agc.autoEnabled = true; + context.activeState.agc.automatic.sensorGain = context.configuration.agc.minAnalogueGain; + context.activeState.agc.automatic.exposure = context.configuration.agc.defaultExposure; + context.activeState.agc.automatic.ispGain = kMinDigitalGain; + context.activeState.agc.manual.sensorGain = context.configuration.agc.minAnalogueGain; + context.activeState.agc.manual.exposure = context.configuration.agc.defaultExposure; + context.activeState.agc.manual.ispGain = kMinDigitalGain; + context.activeState.agc.constraintMode = constraintModes().begin()->first; + context.activeState.agc.exposureMode = exposureModeHelpers().begin()->first; + + /* \todo Run this again when FrameDurationLimits is passed in */ + setLimits(context.configuration.agc.minShutterSpeed, + context.configuration.agc.maxShutterSpeed, + context.configuration.agc.minAnalogueGain, + context.configuration.agc.maxAnalogueGain); + + resetFrameCount(); + + return 0; +} + +void Agc::queueRequest(IPAContext &context, const uint32_t frame, + [[maybe_unused]] IPAFrameContext &frameContext, + const ControlList &controls) +{ + auto &agc = context.activeState.agc; + + const auto &constraintMode = controls.get(controls::AeConstraintMode); + agc.constraintMode = constraintMode.value_or(agc.constraintMode); + + const auto &exposureMode = controls.get(controls::AeExposureMode); + agc.exposureMode = exposureMode.value_or(agc.exposureMode); + + const auto &agcEnable = controls.get(controls::AeEnable); + if (agcEnable && *agcEnable != agc.autoEnabled) { + agc.autoEnabled = *agcEnable; + + LOG(MaliC55Agc, Info) + << (agc.autoEnabled ? "Enabling" : "Disabling") + << " AGC"; + } + + /* + * If the automatic exposure and gain is enabled we have no further work + * to do here... + */ + if (agc.autoEnabled) + return; + + /* + * ...otherwise we need to look for exposure and gain controls and use + * those to set the activeState. + */ + const auto &exposure = controls.get(controls::ExposureTime); + if (exposure) { + agc.manual.exposure = *exposure * 1.0us / context.configuration.sensor.lineDuration; + + LOG(MaliC55Agc, Debug) + << "Exposure set to " << agc.manual.exposure + << " on request sequence " << frame; + } + + const auto &analogueGain = controls.get(controls::AnalogueGain); + if (analogueGain) { + agc.manual.sensorGain = *analogueGain; + + LOG(MaliC55Agc, Debug) + << "Analogue gain set to " << agc.manual.sensorGain + << " on request sequence " << frame; + } + + const auto &digitalGain = controls.get(controls::DigitalGain); + if (digitalGain) { + agc.manual.ispGain = *digitalGain; + + LOG(MaliC55Agc, Debug) + << "Digital gain set to " << agc.manual.ispGain + << " on request sequence " << frame; + } +} + +size_t Agc::fillGainParamBlock(IPAContext &context, IPAFrameContext &frameContext, + mali_c55_params_block block) +{ + IPAActiveState &activeState = context.activeState; + double gain; + + if (activeState.agc.autoEnabled) + gain = activeState.agc.automatic.ispGain; + else + gain = activeState.agc.manual.ispGain; + + block.header->type = MALI_C55_PARAM_BLOCK_DIGITAL_GAIN; + block.header->flags = MALI_C55_PARAM_BLOCK_FL_NONE; + block.header->size = sizeof(struct mali_c55_params_digital_gain); + + block.digital_gain->gain = floatingToFixedPoint<5, 8, uint16_t, double>(gain); + frameContext.agc.ispGain = gain; + + return block.header->size; +} + +size_t Agc::fillParamsBuffer(mali_c55_params_block block, + enum mali_c55_param_block_type type) +{ + block.header->type = type; + block.header->flags = MALI_C55_PARAM_BLOCK_FL_NONE; + block.header->size = sizeof(struct mali_c55_params_aexp_hist); + + /* Collect every 3rd pixel horizontally */ + block.aexp_hist->skip_x = 1; + /* Start from first column */ + block.aexp_hist->offset_x = 0; + /* Collect every pixel vertically */ + block.aexp_hist->skip_y = 0; + /* Start from the first row */ + block.aexp_hist->offset_y = 0; + /* 1x scaling (i.e. none) */ + block.aexp_hist->scale_bottom = 0; + block.aexp_hist->scale_top = 0; + /* Collect all Bayer planes into 4 separate histograms */ + block.aexp_hist->plane_mode = 1; + /* Tap the data immediately after the digital gain block */ + block.aexp_hist->tap_point = MALI_C55_AEXP_HIST_TAP_FS; + + return block.header->size; +} + +size_t Agc::fillWeightsArrayBuffer(mali_c55_params_block block, + enum mali_c55_param_block_type type) +{ + block.header->type = type; + block.header->flags = MALI_C55_PARAM_BLOCK_FL_NONE; + block.header->size = sizeof(struct mali_c55_params_aexp_weights); + + /* We use every zone - a 15x15 grid */ + block.aexp_weights->nodes_used_horiz = 15; + block.aexp_weights->nodes_used_vert = 15; + + /* + * We uniformly weight the zones to 1 - this results in the collected + * histograms containing a true pixel count, which we can then use to + * approximate colour channel averages for the image. + */ + Span<uint8_t> weights{ + block.aexp_weights->zone_weights, + MALI_C55_MAX_ZONES + }; + std::fill(weights.begin(), weights.end(), 1); + + return block.header->size; +} + +void Agc::prepare(IPAContext &context, const uint32_t frame, + IPAFrameContext &frameContext, mali_c55_params_buffer *params) +{ + mali_c55_params_block block; + + block.data = ¶ms->data[params->total_size]; + params->total_size += fillGainParamBlock(context, frameContext, block); + + if (frame > 0) + return; + + block.data = ¶ms->data[params->total_size]; + params->total_size += fillParamsBuffer(block, + MALI_C55_PARAM_BLOCK_AEXP_HIST); + + block.data = ¶ms->data[params->total_size]; + params->total_size += fillWeightsArrayBuffer(block, + MALI_C55_PARAM_BLOCK_AEXP_HIST_WEIGHTS); + + block.data = ¶ms->data[params->total_size]; + params->total_size += fillParamsBuffer(block, + MALI_C55_PARAM_BLOCK_AEXP_IHIST); + + block.data = ¶ms->data[params->total_size]; + params->total_size += fillWeightsArrayBuffer(block, + MALI_C55_PARAM_BLOCK_AEXP_IHIST_WEIGHTS); +} + +double Agc::estimateLuminance(const double gain) const +{ + double rAvg = statistics_.rHist.interQuantileMean(0, 1) * gain; + double gAvg = statistics_.gHist.interQuantileMean(0, 1) * gain; + double bAvg = statistics_.bHist.interQuantileMean(0, 1) * gain; + double yAvg = rec601LuminanceFromRGB({ { rAvg, gAvg, bAvg } }); + + return yAvg / kNumHistogramBins; +} + +void Agc::process(IPAContext &context, + [[maybe_unused]] const uint32_t frame, + IPAFrameContext &frameContext, + const mali_c55_stats_buffer *stats, + [[maybe_unused]] ControlList &metadata) +{ + IPASessionConfiguration &configuration = context.configuration; + IPAActiveState &activeState = context.activeState; + + if (!stats) { + LOG(MaliC55Agc, Error) << "No statistics buffer passed to Agc"; + return; + } + + statistics_.parseStatistics(stats); + context.activeState.agc.temperatureK = estimateCCT({ { statistics_.rHist.interQuantileMean(0, 1), + statistics_.gHist.interQuantileMean(0, 1), + statistics_.bHist.interQuantileMean(0, 1) } }); + + /* + * The Agc algorithm needs to know the effective exposure value that was + * applied to the sensor when the statistics were collected. + */ + uint32_t exposure = frameContext.agc.exposure; + double analogueGain = frameContext.agc.sensorGain; + double digitalGain = frameContext.agc.ispGain; + double totalGain = analogueGain * digitalGain; + utils::Duration currentShutter = exposure * configuration.sensor.lineDuration; + utils::Duration effectiveExposureValue = currentShutter * totalGain; + + utils::Duration shutterTime; + double aGain, dGain; + std::tie(shutterTime, aGain, dGain) = + calculateNewEv(activeState.agc.constraintMode, + activeState.agc.exposureMode, statistics_.yHist, + effectiveExposureValue); + + dGain = std::clamp(dGain, kMinDigitalGain, kMaxDigitalGain); + + LOG(MaliC55Agc, Debug) + << "Divided up shutter, analogue gain and digital gain are " + << shutterTime << ", " << aGain << " and " << dGain; + + activeState.agc.automatic.exposure = shutterTime / configuration.sensor.lineDuration; + activeState.agc.automatic.sensorGain = aGain; + activeState.agc.automatic.ispGain = dGain; + + metadata.set(controls::ExposureTime, currentShutter.get<std::micro>()); + metadata.set(controls::AnalogueGain, frameContext.agc.sensorGain); + metadata.set(controls::DigitalGain, frameContext.agc.ispGain); + metadata.set(controls::ColourTemperature, context.activeState.agc.temperatureK); +} + +REGISTER_IPA_ALGORITHM(Agc, "Agc") + +} /* namespace ipa::mali_c55::algorithms */ + +} /* namespace libcamera */ diff --git a/src/ipa/mali-c55/algorithms/agc.h b/src/ipa/mali-c55/algorithms/agc.h new file mode 100644 index 00000000..c5c574e5 --- /dev/null +++ b/src/ipa/mali-c55/algorithms/agc.h @@ -0,0 +1,81 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +/* + * Copyright (C) 2023, Ideas on Board Oy + * + * agc.h - Mali C55 AGC/AEC mean-based control algorithm + */ + +#pragma once + +#include <libcamera/base/utils.h> + +#include "libcamera/internal/bayer_format.h" + +#include "libipa/agc_mean_luminance.h" +#include "libipa/histogram.h" + +#include "algorithm.h" +#include "ipa_context.h" + +namespace libcamera { + +namespace ipa::mali_c55::algorithms { + +class AgcStatistics +{ +public: + AgcStatistics() + { + } + + int setBayerOrderIndices(BayerFormat::Order bayerOrder); + uint32_t decodeBinValue(uint16_t binVal); + void parseStatistics(const mali_c55_stats_buffer *stats); + + Histogram rHist; + Histogram gHist; + Histogram bHist; + Histogram yHist; +private: + unsigned int rIndex_; + unsigned int grIndex_; + unsigned int gbIndex_; + unsigned int bIndex_; +}; + +class Agc : public Algorithm, public AgcMeanLuminance +{ +public: + Agc(); + ~Agc() = 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, + mali_c55_params_buffer *params) override; + void process(IPAContext &context, const uint32_t frame, + IPAFrameContext &frameContext, + const mali_c55_stats_buffer *stats, + ControlList &metadata) override; + +private: + double estimateLuminance(const double gain) const override; + size_t fillGainParamBlock(IPAContext &context, + IPAFrameContext &frameContext, + mali_c55_params_block block); + size_t fillParamsBuffer(mali_c55_params_block block, + enum mali_c55_param_block_type type); + size_t fillWeightsArrayBuffer(mali_c55_params_block block, + enum mali_c55_param_block_type type); + + AgcStatistics statistics_; +}; + +} /* namespace ipa::mali_c55::algorithms */ + +} /* namespace libcamera */ diff --git a/src/ipa/mali-c55/algorithms/algorithm.h b/src/ipa/mali-c55/algorithms/algorithm.h new file mode 100644 index 00000000..36a3bff0 --- /dev/null +++ b/src/ipa/mali-c55/algorithms/algorithm.h @@ -0,0 +1,39 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +/* + * Copyright (C) 2024, Ideas On Board + * + * algorithm.h - Mali-C55 control algorithm interface + */ + +#pragma once + +#include <linux/mali-c55-config.h> + +#include <libipa/algorithm.h> + +#include "module.h" + +namespace libcamera { + +namespace ipa::mali_c55 { + +class Algorithm : public libcamera::ipa::Algorithm<Module> +{ +}; + +union mali_c55_params_block { + struct mali_c55_params_block_header *header; + struct mali_c55_params_sensor_off_preshading *sensor_offs; + struct mali_c55_params_aexp_hist *aexp_hist; + struct mali_c55_params_aexp_weights *aexp_weights; + struct mali_c55_params_digital_gain *digital_gain; + struct mali_c55_params_awb_gains *awb_gains; + struct mali_c55_params_awb_config *awb_config; + struct mali_c55_params_mesh_shading_config *shading_config; + struct mali_c55_params_mesh_shading_selection *shading_selection; + __u8 *data; +}; + +} /* namespace ipa::mali_c55 */ + +} /* namespace libcamera */ diff --git a/src/ipa/mali-c55/algorithms/awb.cpp b/src/ipa/mali-c55/algorithms/awb.cpp new file mode 100644 index 00000000..050b191b --- /dev/null +++ b/src/ipa/mali-c55/algorithms/awb.cpp @@ -0,0 +1,230 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +/* + * Copyright (C) 2024, Ideas On Board Oy + * + * awb.cpp - Mali C55 grey world auto white balance algorithm + */ + +#include "awb.h" + +#include <cmath> + +#include <libcamera/base/log.h> +#include <libcamera/base/utils.h> + +#include <libcamera/control_ids.h> + +#include "libipa/fixedpoint.h" + +namespace libcamera { + +namespace ipa::mali_c55::algorithms { + +LOG_DEFINE_CATEGORY(MaliC55Awb) + +/* Number of frames at which we should run AWB at full speed */ +static constexpr uint32_t kNumStartupFrames = 4; + +Awb::Awb() +{ +} + +int Awb::configure([[maybe_unused]] IPAContext &context, + [[maybe_unused]] const IPACameraSensorInfo &configInfo) +{ + /* + * Initially we have no idea what the colour balance will be like, so + * for the first frame we will make no assumptions and leave the R/B + * channels unmodified. + */ + context.activeState.awb.rGain = 1.0; + context.activeState.awb.bGain = 1.0; + + return 0; +} + +size_t Awb::fillGainsParamBlock(mali_c55_params_block block, IPAContext &context, + IPAFrameContext &frameContext) +{ + block.header->type = MALI_C55_PARAM_BLOCK_AWB_GAINS; + block.header->flags = MALI_C55_PARAM_BLOCK_FL_NONE; + block.header->size = sizeof(struct mali_c55_params_awb_gains); + + double rGain = context.activeState.awb.rGain; + double bGain = context.activeState.awb.bGain; + + /* + * The gains here map as follows: + * gain00 = R + * gain01 = Gr + * gain10 = Gb + * gain11 = B + * + * This holds true regardless of the bayer order of the input data, as + * the mapping is done internally in the ISP. + */ + block.awb_gains->gain00 = floatingToFixedPoint<4, 8, uint16_t, double>(rGain); + block.awb_gains->gain01 = floatingToFixedPoint<4, 8, uint16_t, double>(1.0); + block.awb_gains->gain10 = floatingToFixedPoint<4, 8, uint16_t, double>(1.0); + block.awb_gains->gain11 = floatingToFixedPoint<4, 8, uint16_t, double>(bGain); + + frameContext.awb.rGain = rGain; + frameContext.awb.bGain = bGain; + + return sizeof(struct mali_c55_params_awb_gains); +} + +size_t Awb::fillConfigParamBlock(mali_c55_params_block block) +{ + block.header->type = MALI_C55_PARAM_BLOCK_AWB_CONFIG; + block.header->flags = MALI_C55_PARAM_BLOCK_FL_NONE; + block.header->size = sizeof(struct mali_c55_params_awb_config); + + /* Tap the stats after the purple fringe block */ + block.awb_config->tap_point = MALI_C55_AWB_STATS_TAP_PF; + + /* Get R/G and B/G ratios as statistics */ + block.awb_config->stats_mode = MALI_C55_AWB_MODE_RGBG; + + /* Default white level */ + block.awb_config->white_level = 1023; + + /* Default black level */ + block.awb_config->black_level = 0; + + /* + * By default pixels are included who's colour ratios are bounded in a + * region (on a cr ratio x cb ratio graph) defined by four points: + * (0.25, 0.25) + * (0.25, 1.99609375) + * (1.99609375, 1.99609375) + * (1.99609375, 0.25) + * + * The ratios themselves are stored in Q4.8 format. + * + * \todo should these perhaps be tunable? + */ + block.awb_config->cr_max = 511; + block.awb_config->cr_min = 64; + block.awb_config->cb_max = 511; + block.awb_config->cb_min = 64; + + /* We use the full 15x15 zoning scheme */ + block.awb_config->nodes_used_horiz = 15; + block.awb_config->nodes_used_vert = 15; + + /* + * We set the trimming boundaries equivalent to the main boundaries. In + * other words; no trimming. + */ + block.awb_config->cr_high = 511; + block.awb_config->cr_low = 64; + block.awb_config->cb_high = 511; + block.awb_config->cb_low = 64; + + return sizeof(struct mali_c55_params_awb_config); +} + +void Awb::prepare(IPAContext &context, const uint32_t frame, + IPAFrameContext &frameContext, mali_c55_params_buffer *params) +{ + mali_c55_params_block block; + block.data = ¶ms->data[params->total_size]; + + params->total_size += fillGainsParamBlock(block, context, frameContext); + + if (frame > 0) + return; + + block.data = ¶ms->data[params->total_size]; + params->total_size += fillConfigParamBlock(block); +} + +void Awb::process(IPAContext &context, const uint32_t frame, + IPAFrameContext &frameContext, const mali_c55_stats_buffer *stats, + [[maybe_unused]] ControlList &metadata) +{ + const struct mali_c55_awb_average_ratios *awb_ratios = stats->awb_ratios; + + /* + * The ISP produces average R:G and B:G ratios for zones. We take the + * average of all the zones with data and simply invert them to provide + * gain figures that we can apply to approximate a grey world. + */ + unsigned int counted_zones = 0; + double rgSum = 0, bgSum = 0; + + for (unsigned int i = 0; i < 225; i++) { + if (!awb_ratios[i].num_pixels) + continue; + + /* + * The statistics are in Q4.8 format, so we convert to double + * here. + */ + rgSum += fixedToFloatingPoint<4, 8, double, uint16_t>(awb_ratios[i].avg_rg_gr); + bgSum += fixedToFloatingPoint<4, 8, double, uint16_t>(awb_ratios[i].avg_bg_br); + counted_zones++; + } + + /* + * Sometimes the first frame's statistics have no valid pixels, in which + * case we'll just assume a grey world until they say otherwise. + */ + double rgAvg, bgAvg; + if (!counted_zones) { + rgAvg = 1.0; + bgAvg = 1.0; + } else { + rgAvg = rgSum / counted_zones; + bgAvg = bgSum / counted_zones; + } + + /* + * The statistics are generated _after_ white balancing is performed in + * the ISP. To get the true ratio we therefore have to adjust the stats + * figure by the gains that were applied when the statistics for this + * frame were generated. + */ + double rRatio = rgAvg / frameContext.awb.rGain; + double bRatio = bgAvg / frameContext.awb.bGain; + + /* + * And then we can simply invert the ratio to find the gain we should + * apply. + */ + double rGain = 1 / rRatio; + double bGain = 1 / bRatio; + + /* + * Running at full speed, this algorithm results in oscillations in the + * colour balance. To remove those we dampen the speed at which it makes + * changes in gain, unless we're in the startup phase in which case we + * want to fix the miscolouring as quickly as possible. + */ + double speed = frame < kNumStartupFrames ? 1.0 : 0.2; + rGain = speed * rGain + context.activeState.awb.rGain * (1.0 - speed); + bGain = speed * bGain + context.activeState.awb.bGain * (1.0 - speed); + + context.activeState.awb.rGain = rGain; + context.activeState.awb.bGain = bGain; + + metadata.set(controls::ColourGains, { + static_cast<float>(frameContext.awb.rGain), + static_cast<float>(frameContext.awb.bGain), + }); + + LOG(MaliC55Awb, Debug) << "For frame number " << frame << ": " + << "Average R/G Ratio: " << rgAvg + << ", Average B/G Ratio: " << bgAvg + << "\nrGain applied to this frame: " << frameContext.awb.rGain + << ", bGain applied to this frame: " << frameContext.awb.bGain + << "\nrGain to apply: " << context.activeState.awb.rGain + << ", bGain to apply: " << context.activeState.awb.bGain; +} + +REGISTER_IPA_ALGORITHM(Awb, "Awb") + +} /* namespace ipa::mali_c55::algorithms */ + +} /* namespace libcamera */ diff --git a/src/ipa/mali-c55/algorithms/awb.h b/src/ipa/mali-c55/algorithms/awb.h new file mode 100644 index 00000000..800c2e83 --- /dev/null +++ b/src/ipa/mali-c55/algorithms/awb.h @@ -0,0 +1,40 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +/* + * Copyright (C) 2024, Ideas on Board Oy + * + * awb.h - Mali C55 grey world auto white balance algorithm + */ + +#include "algorithm.h" +#include "ipa_context.h" + +namespace libcamera { + +namespace ipa::mali_c55::algorithms { + +class Awb : public Algorithm +{ +public: + Awb(); + ~Awb() = default; + + int configure(IPAContext &context, + const IPACameraSensorInfo &configInfo) override; + void prepare(IPAContext &context, const uint32_t frame, + IPAFrameContext &frameContext, + mali_c55_params_buffer *params) override; + void process(IPAContext &context, const uint32_t frame, + IPAFrameContext &frameContext, + const mali_c55_stats_buffer *stats, + ControlList &metadata) override; + +private: + size_t fillGainsParamBlock(mali_c55_params_block block, + IPAContext &context, + IPAFrameContext &frameContext); + size_t fillConfigParamBlock(mali_c55_params_block block); +}; + +} /* namespace ipa::mali_c55::algorithms */ + +} /* namespace libcamera */ diff --git a/src/ipa/mali-c55/algorithms/blc.cpp b/src/ipa/mali-c55/algorithms/blc.cpp new file mode 100644 index 00000000..2a54c86a --- /dev/null +++ b/src/ipa/mali-c55/algorithms/blc.cpp @@ -0,0 +1,140 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +/* + * Copyright (C) 2024, Ideas On Board + * + * Mali-C55 sensor offset (black level) correction + */ + +#include "blc.h" + +#include <libcamera/base/log.h> +#include <libcamera/control_ids.h> + +#include "libcamera/internal/yaml_parser.h" + +/** + * \file blc.h + */ + +namespace libcamera { + +namespace ipa::mali_c55::algorithms { + +/** + * \class BlackLevelCorrection + * \brief MaliC55 Black Level Correction control + */ + +LOG_DEFINE_CATEGORY(MaliC55Blc) + +BlackLevelCorrection::BlackLevelCorrection() + : tuningParameters_(false) +{ +} + +/** + * \copydoc libcamera::ipa::Algorithm::init + */ +int BlackLevelCorrection::init([[maybe_unused]] IPAContext &context, + const YamlObject &tuningData) +{ + offset00 = tuningData["offset00"].get<uint32_t>(0); + offset01 = tuningData["offset01"].get<uint32_t>(0); + offset10 = tuningData["offset10"].get<uint32_t>(0); + offset11 = tuningData["offset11"].get<uint32_t>(0); + + if (offset00 > kMaxOffset || offset01 > kMaxOffset || + offset10 > kMaxOffset || offset11 > kMaxOffset) { + LOG(MaliC55Blc, Error) << "Invalid black level offsets"; + return -EINVAL; + } + + tuningParameters_ = true; + + LOG(MaliC55Blc, Debug) + << "Black levels: 00 " << offset00 << ", 01 " << offset01 + << ", 10 " << offset10 << ", 11 " << offset11; + + return 0; +} + +/** + * \copydoc libcamera::ipa::Algorithm::configure + */ +int BlackLevelCorrection::configure(IPAContext &context, + [[maybe_unused]] const IPACameraSensorInfo &configInfo) +{ + /* + * If no Black Levels were passed in through tuning data then we could + * use the value from the CameraSensorHelper if one is available. + */ + if (context.configuration.sensor.blackLevel && + !(offset00 + offset01 + offset10 + offset11)) { + offset00 = context.configuration.sensor.blackLevel; + offset01 = context.configuration.sensor.blackLevel; + offset10 = context.configuration.sensor.blackLevel; + offset11 = context.configuration.sensor.blackLevel; + } + + return 0; +} + +/** + * \copydoc libcamera::ipa::Algorithm::prepare + */ +void BlackLevelCorrection::prepare([[maybe_unused]] IPAContext &context, + const uint32_t frame, + [[maybe_unused]] IPAFrameContext &frameContext, + mali_c55_params_buffer *params) +{ + mali_c55_params_block block; + block.data = ¶ms->data[params->total_size]; + + if (frame > 0) + return; + + if (!tuningParameters_) + return; + + block.header->type = MALI_C55_PARAM_BLOCK_SENSOR_OFFS; + block.header->flags = MALI_C55_PARAM_BLOCK_FL_NONE; + block.header->size = sizeof(mali_c55_params_sensor_off_preshading); + + block.sensor_offs->chan00 = offset00; + block.sensor_offs->chan01 = offset01; + block.sensor_offs->chan10 = offset10; + block.sensor_offs->chan11 = offset11; + + params->total_size += block.header->size; +} + +void BlackLevelCorrection::process([[maybe_unused]] IPAContext &context, + [[maybe_unused]] const uint32_t frame, + [[maybe_unused]] IPAFrameContext &frameContext, + [[maybe_unused]] const mali_c55_stats_buffer *stats, + ControlList &metadata) +{ + /* + * Black Level Offsets in tuning data need to be 20-bit, whereas the + * metadata expects values from a 16-bit range. Right-shift to remove + * the 4 least significant bits. + * + * The black levels should be reported in the order R, Gr, Gb, B. We + * ignore that here given we're using matching values so far, but it + * would be safer to check the sensor's bayer order. + * + * \todo Account for bayer order. + */ + metadata.set(controls::SensorBlackLevels, { + static_cast<int32_t>(offset00 >> 4), + static_cast<int32_t>(offset01 >> 4), + static_cast<int32_t>(offset10 >> 4), + static_cast<int32_t>(offset11 >> 4), + }); +} + +REGISTER_IPA_ALGORITHM(BlackLevelCorrection, "BlackLevelCorrection") + +} /* namespace ipa::mali_c55::algorithms */ + +} /* namespace libcamera */ diff --git a/src/ipa/mali-c55/algorithms/blc.h b/src/ipa/mali-c55/algorithms/blc.h new file mode 100644 index 00000000..9696e8e9 --- /dev/null +++ b/src/ipa/mali-c55/algorithms/blc.h @@ -0,0 +1,42 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +/* + * Copyright (C) 2024, Ideas On Board + * + * Mali-C55 sensor offset (black level) correction + */ + +#include "algorithm.h" + +namespace libcamera { + +namespace ipa::mali_c55::algorithms { + +class BlackLevelCorrection : public Algorithm +{ +public: + BlackLevelCorrection(); + ~BlackLevelCorrection() = default; + + int init(IPAContext &context, const YamlObject &tuningData) override; + int configure(IPAContext &context, + const IPACameraSensorInfo &configInfo) override; + void prepare(IPAContext &context, const uint32_t frame, + IPAFrameContext &frameContext, + mali_c55_params_buffer *params) override; + void process(IPAContext &context, const uint32_t frame, + IPAFrameContext &frameContext, + const mali_c55_stats_buffer *stats, + ControlList &metadata) override; + +private: + static constexpr uint32_t kMaxOffset = 0xfffff; + + bool tuningParameters_; + uint32_t offset00; + uint32_t offset01; + uint32_t offset10; + uint32_t offset11; +}; + +} /* namespace ipa::mali_c55::algorithms */ +} /* namespace libcamera */ diff --git a/src/ipa/mali-c55/algorithms/lsc.cpp b/src/ipa/mali-c55/algorithms/lsc.cpp new file mode 100644 index 00000000..c5afc04d --- /dev/null +++ b/src/ipa/mali-c55/algorithms/lsc.cpp @@ -0,0 +1,216 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +/* + * Copyright (C) 2024, Ideas On Board Oy + * + * lsc.cpp - Mali-C55 Lens shading correction algorithm + */ + +#include "lsc.h" + +#include "libcamera/internal/yaml_parser.h" + +namespace libcamera { + +namespace ipa::mali_c55::algorithms { + +LOG_DEFINE_CATEGORY(MaliC55Lsc) + +int Lsc::init([[maybe_unused]] IPAContext &context, const YamlObject &tuningData) +{ + if (!tuningData.contains("meshScale")) { + LOG(MaliC55Lsc, Error) << "meshScale missing from tuningData"; + return -EINVAL; + } + + meshScale_ = tuningData["meshScale"].get<uint32_t>(0); + + const YamlObject &yamlSets = tuningData["sets"]; + if (!yamlSets.isList()) { + LOG(MaliC55Lsc, Error) << "LSC tables missing or invalid"; + return -EINVAL; + } + + size_t tableSize = 0; + const auto &sets = yamlSets.asList(); + for (const auto &yamlSet : sets) { + uint32_t ct = yamlSet["ct"].get<uint32_t>(0); + + if (!ct) { + LOG(MaliC55Lsc, Error) << "Invalid colour temperature"; + return -EINVAL; + } + + if (std::count(colourTemperatures_.begin(), + colourTemperatures_.end(), ct)) { + LOG(MaliC55Lsc, Error) + << "Multiple sets found for colour temperature"; + return -EINVAL; + } + + std::vector<uint8_t> rTable = + yamlSet["r"].getList<uint8_t>().value_or(std::vector<uint8_t>{}); + std::vector<uint8_t> gTable = + yamlSet["g"].getList<uint8_t>().value_or(std::vector<uint8_t>{}); + std::vector<uint8_t> bTable = + yamlSet["b"].getList<uint8_t>().value_or(std::vector<uint8_t>{}); + + /* + * Some validation to do; only 16x16 and 32x32 tables of + * coefficients are acceptable, and all tables across all of the + * sets must be the same size. The first time we encounter a + * table we check that it is an acceptable size and if so make + * sure all other tables are of equal size. + */ + if (!tableSize) { + if (rTable.size() != 256 && rTable.size() != 1024) { + LOG(MaliC55Lsc, Error) + << "Invalid table size for colour temperature " << ct; + return -EINVAL; + } + tableSize = rTable.size(); + } + + if (rTable.size() != tableSize || + gTable.size() != tableSize || + bTable.size() != tableSize) { + LOG(MaliC55Lsc, Error) + << "Invalid or mismatched table size for colour temperature " << ct; + return -EINVAL; + } + + if (colourTemperatures_.size() >= 3) { + LOG(MaliC55Lsc, Error) + << "A maximum of 3 colour temperatures are supported"; + return -EINVAL; + } + + for (unsigned int i = 0; i < tableSize; i++) { + mesh_[kRedOffset + i] |= + (rTable[i] << (colourTemperatures_.size() * 8)); + mesh_[kGreenOffset + i] |= + (gTable[i] << (colourTemperatures_.size() * 8)); + mesh_[kBlueOffset + i] |= + (bTable[i] << (colourTemperatures_.size() * 8)); + } + + colourTemperatures_.push_back(ct); + } + + /* + * The mesh has either 16x16 or 32x32 nodes, we tell the driver which it + * is based on the number of values in the tuning data's table. + */ + if (tableSize == 256) + meshSize_ = 15; + else + meshSize_ = 31; + + return 0; +} + +size_t Lsc::fillConfigParamsBlock(mali_c55_params_block block) const +{ + block.header->type = MALI_C55_PARAM_MESH_SHADING_CONFIG; + block.header->flags = MALI_C55_PARAM_BLOCK_FL_NONE; + block.header->size = sizeof(struct mali_c55_params_mesh_shading_config); + + block.shading_config->mesh_show = false; + block.shading_config->mesh_scale = meshScale_; + block.shading_config->mesh_page_r = 0; + block.shading_config->mesh_page_g = 1; + block.shading_config->mesh_page_b = 2; + block.shading_config->mesh_width = meshSize_; + block.shading_config->mesh_height = meshSize_; + + std::copy(mesh_.begin(), mesh_.end(), block.shading_config->mesh); + + return block.header->size; +} + +size_t Lsc::fillSelectionParamsBlock(mali_c55_params_block block, uint8_t bank, + uint8_t alpha) const +{ + block.header->type = MALI_C55_PARAM_MESH_SHADING_SELECTION; + block.header->flags = MALI_C55_PARAM_BLOCK_FL_NONE; + block.header->size = sizeof(struct mali_c55_params_mesh_shading_selection); + + block.shading_selection->mesh_alpha_bank_r = bank; + block.shading_selection->mesh_alpha_bank_g = bank; + block.shading_selection->mesh_alpha_bank_b = bank; + block.shading_selection->mesh_alpha_r = alpha; + block.shading_selection->mesh_alpha_g = alpha; + block.shading_selection->mesh_alpha_b = alpha; + block.shading_selection->mesh_strength = 0x1000; /* Otherwise known as 1.0 */ + + return block.header->size; +} + +std::tuple<uint8_t, uint8_t> Lsc::findBankAndAlpha(uint32_t ct) const +{ + unsigned int i; + + ct = std::clamp<uint32_t>(ct, colourTemperatures_.front(), + colourTemperatures_.back()); + + for (i = 0; i < colourTemperatures_.size() - 1; i++) { + if (ct >= colourTemperatures_[i] && + ct <= colourTemperatures_[i + 1]) + break; + } + + /* + * With the clamping, we're guaranteed an index into colourTemperatures_ + * that's <= colourTemperatures_.size() - 1. + */ + uint8_t alpha = (255 * (ct - colourTemperatures_[i])) / + (colourTemperatures_[i + 1] - colourTemperatures_[i]); + + return { i, alpha }; +} + +void Lsc::prepare(IPAContext &context, [[maybe_unused]] const uint32_t frame, + [[maybe_unused]] IPAFrameContext &frameContext, + mali_c55_params_buffer *params) +{ + /* + * For each frame we assess the colour temperature of the **last** frame + * and then select an appropriately blended table of coefficients based + * on that ct. As a bit of a shortcut, if we've only a single table the + * handling is somewhat simpler; if it's the first frame we just select + * that table and if we're past the first frame then we can just do + * nothing - the config will never change. + */ + uint32_t temperatureK = context.activeState.agc.temperatureK; + uint8_t bank, alpha; + + if (colourTemperatures_.size() == 1) { + if (frame > 0) + return; + + bank = 0; + alpha = 0; + } else { + std::tie(bank, alpha) = findBankAndAlpha(temperatureK); + } + + mali_c55_params_block block; + block.data = ¶ms->data[params->total_size]; + + params->total_size += fillSelectionParamsBlock(block, bank, alpha); + + if (frame > 0) + return; + + /* + * If this is the first frame, we need to load the parsed coefficient + * tables from tuning data to the ISP. + */ + block.data = ¶ms->data[params->total_size]; + params->total_size += fillConfigParamsBlock(block); +} + +REGISTER_IPA_ALGORITHM(Lsc, "Lsc") + +} /* namespace ipa::mali_c55::algorithms */ + +} /* namespace libcamera */ diff --git a/src/ipa/mali-c55/algorithms/lsc.h b/src/ipa/mali-c55/algorithms/lsc.h new file mode 100644 index 00000000..e613277a --- /dev/null +++ b/src/ipa/mali-c55/algorithms/lsc.h @@ -0,0 +1,45 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +/* + * Copyright (C) 2024, Ideas On Board Oy + * + * lsc.h - Mali-C55 Lens shading correction algorithm + */ + +#include <map> +#include <tuple> + +#include "algorithm.h" + +namespace libcamera { + +namespace ipa::mali_c55::algorithms { + +class Lsc : public Algorithm +{ +public: + Lsc() = default; + ~Lsc() = default; + + int init(IPAContext &context, const YamlObject &tuningData) override; + void prepare(IPAContext &context, const uint32_t frame, + IPAFrameContext &frameContext, + mali_c55_params_buffer *params) override; +private: + static constexpr unsigned int kRedOffset = 0; + static constexpr unsigned int kGreenOffset = 1024; + static constexpr unsigned int kBlueOffset = 2048; + + size_t fillConfigParamsBlock(mali_c55_params_block block) const; + size_t fillSelectionParamsBlock(mali_c55_params_block block, + uint8_t bank, uint8_t alpha) const; + std::tuple<uint8_t, uint8_t> findBankAndAlpha(uint32_t ct) const; + + std::vector<uint32_t> mesh_ = std::vector<uint32_t>(3072); + std::vector<uint32_t> colourTemperatures_; + uint32_t meshScale_; + uint32_t meshSize_; +}; + +} /* namespace ipa::mali_c55::algorithms */ + +} /* namespace libcamera */ diff --git a/src/ipa/mali-c55/algorithms/meson.build b/src/ipa/mali-c55/algorithms/meson.build new file mode 100644 index 00000000..1665da07 --- /dev/null +++ b/src/ipa/mali-c55/algorithms/meson.build @@ -0,0 +1,8 @@ +# SPDX-License-Identifier: CC0-1.0 + +mali_c55_ipa_algorithms = files([ + 'agc.cpp', + 'awb.cpp', + 'blc.cpp', + 'lsc.cpp', +]) diff --git a/src/ipa/mali-c55/data/imx415.yaml b/src/ipa/mali-c55/data/imx415.yaml new file mode 100644 index 00000000..126b427a --- /dev/null +++ b/src/ipa/mali-c55/data/imx415.yaml @@ -0,0 +1,325 @@ +# SPDX-License-Identifier: CC0-1.0 +%YAML 1.1 +--- +version: 1 +algorithms: + - Agc: + - Awb: + - BlackLevelCorrection: + offset00: 51200 + offset01: 51200 + offset10: 51200 + offset11: 51200 + - Lsc: + meshScale: 4 # 1.0 - 2.0 Gain + sets: + - ct: 2500 + r: [ + 21, 20, 19, 17, 15, 14, 12, 11, 9, 9, 9, 9, 8, 8, 9, 9, 9, 9, 9, 9, 9, 9, 13, 13, 10, 10, 13, 16, 17, 18, 21, 22, + 21, 20, 18, 16, 14, 13, 12, 11, 10, 9, 9, 8, 8, 8, 8, 8, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 12, 15, 17, 18, 21, 21, + 20, 19, 17, 16, 14, 13, 12, 11, 10, 9, 8, 8, 8, 7, 7, 7, 8, 8, 8, 8, 8, 9, 9, 8, 8, 8, 11, 15, 17, 18, 21, 21, + 19, 19, 17, 15, 14, 13, 12, 11, 10, 8, 8, 7, 7, 7, 6, 6, 7, 7, 8, 8, 8, 8, 8, 7, 7, 8, 10, 14, 17, 18, 20, 22, + 19, 18, 17, 15, 14, 13, 11, 11, 9, 8, 8, 7, 7, 6, 5, 5, 5, 5, 6, 7, 8, 7, 7, 6, 7, 7, 10, 12, 16, 18, 20, 22, + 18, 18, 16, 15, 14, 12, 11, 10, 9, 8, 6, 6, 5, 5, 4, 4, 5, 5, 5, 5, 6, 6, 6, 6, 6, 7, 8, 12, 16, 18, 19, 20, + 18, 18, 16, 14, 13, 12, 11, 9, 9, 7, 6, 5, 5, 5, 4, 4, 4, 5, 5, 4, 4, 5, 5, 5, 5, 6, 8, 11, 15, 18, 18, 19, + 18, 17, 15, 14, 13, 12, 11, 9, 8, 7, 6, 5, 5, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 5, 5, 6, 7, 9, 14, 17, 18, 18, + 18, 17, 15, 14, 13, 12, 11, 9, 8, 7, 5, 5, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 5, 5, 6, 8, 12, 17, 18, 18, + 18, 16, 15, 13, 12, 11, 10, 9, 8, 7, 5, 4, 4, 4, 4, 3, 3, 4, 4, 4, 3, 3, 4, 4, 4, 5, 6, 8, 12, 16, 19, 19, + 17, 16, 15, 13, 12, 11, 10, 8, 7, 6, 4, 4, 3, 3, 3, 3, 3, 3, 4, 4, 3, 3, 3, 4, 4, 5, 6, 9, 12, 16, 19, 20, + 17, 15, 15, 13, 12, 11, 10, 8, 6, 6, 5, 4, 3, 3, 3, 2, 3, 3, 4, 4, 3, 3, 2, 3, 4, 5, 6, 9, 11, 16, 19, 20, + 17, 15, 15, 14, 11, 11, 10, 8, 6, 5, 5, 4, 3, 3, 2, 2, 2, 3, 3, 3, 3, 3, 2, 3, 4, 5, 6, 8, 11, 16, 18, 19, + 16, 16, 15, 13, 11, 11, 10, 7, 6, 5, 4, 4, 3, 2, 2, 2, 2, 3, 3, 3, 3, 2, 2, 2, 3, 4, 6, 8, 11, 14, 17, 18, + 16, 16, 14, 13, 11, 10, 9, 7, 6, 5, 4, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 2, 2, 2, 3, 4, 6, 8, 10, 14, 17, 18, + 16, 15, 14, 13, 13, 10, 9, 7, 6, 4, 4, 2, 2, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 3, 4, 4, 6, 8, 11, 17, 18, 19, + 16, 15, 14, 14, 13, 12, 9, 8, 7, 5, 4, 2, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 3, 4, 5, 6, 8, 12, 17, 19, 20, + 17, 15, 15, 14, 13, 12, 9, 8, 7, 5, 3, 2, 1, 0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 4, 5, 6, 8, 13, 16, 19, 21, + 17, 16, 15, 13, 13, 12, 9, 8, 7, 5, 3, 2, 0, 0, 0, 0, 1, 1, 1, 2, 2, 3, 3, 4, 5, 5, 7, 8, 13, 16, 19, 20, + 17, 16, 15, 14, 13, 12, 9, 8, 7, 5, 3, 2, 0, 0, 0, 0, 1, 1, 1, 2, 3, 3, 3, 4, 5, 6, 7, 9, 13, 17, 19, 20, + 18, 16, 15, 14, 13, 12, 9, 8, 7, 5, 4, 2, 1, 0, 0, 0, 0, 1, 2, 2, 3, 3, 3, 4, 5, 6, 8, 9, 13, 17, 20, 20, + 18, 16, 16, 15, 14, 12, 10, 9, 7, 6, 5, 3, 2, 1, 0, 0, 0, 1, 2, 3, 3, 3, 4, 5, 6, 7, 9, 10, 14, 18, 20, 20, + 18, 17, 16, 15, 14, 12, 10, 9, 8, 7, 6, 5, 3, 3, 1, 0, 0, 1, 2, 3, 4, 4, 5, 6, 6, 7, 9, 12, 15, 19, 20, 20, + 18, 18, 17, 16, 14, 13, 11, 10, 9, 8, 7, 6, 5, 5, 3, 1, 1, 1, 2, 3, 5, 5, 5, 6, 7, 9, 12, 15, 17, 19, 20, 20, + 18, 18, 17, 16, 15, 13, 12, 10, 10, 9, 8, 7, 6, 5, 4, 2, 1, 2, 3, 4, 5, 5, 6, 6, 8, 10, 13, 16, 18, 20, 20, 21, + 19, 18, 17, 16, 15, 14, 13, 11, 10, 10, 9, 8, 7, 6, 5, 4, 3, 2, 3, 3, 5, 5, 6, 7, 10, 11, 14, 17, 19, 20, 21, 22, + 20, 19, 18, 17, 16, 15, 13, 12, 11, 10, 10, 9, 8, 7, 6, 5, 4, 3, 3, 3, 5, 6, 6, 7, 10, 12, 14, 18, 20, 21, 22, 23, + 21, 20, 19, 18, 17, 16, 14, 13, 12, 11, 10, 10, 9, 7, 7, 5, 5, 4, 4, 5, 6, 6, 7, 8, 11, 13, 16, 19, 21, 22, 22, 22, + 22, 21, 20, 19, 18, 17, 16, 14, 13, 12, 12, 10, 9, 8, 7, 6, 6, 5, 5, 6, 7, 7, 8, 9, 12, 14, 18, 20, 21, 22, 22, 22, + 23, 22, 21, 20, 19, 17, 16, 15, 14, 14, 13, 12, 10, 9, 8, 7, 6, 5, 5, 6, 7, 8, 8, 10, 12, 15, 18, 20, 21, 22, 22, 22, + 24, 23, 22, 21, 20, 18, 17, 16, 15, 15, 14, 14, 13, 11, 9, 8, 6, 6, 6, 6, 7, 8, 9, 11, 14, 17, 19, 20, 21, 21, 21, 21, + 24, 24, 23, 21, 20, 19, 17, 16, 15, 15, 15, 14, 14, 14, 11, 9, 6, 5, 5, 6, 8, 8, 10, 12, 15, 17, 20, 20, 21, 21, 21, 21, + ] + g: [ + 19, 18, 17, 15, 13, 12, 10, 9, 8, 8, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 12, 12, 9, 9, 11, 15, 15, 16, 19, 20, + 19, 18, 16, 15, 12, 12, 10, 10, 8, 7, 7, 7, 7, 6, 7, 7, 7, 7, 7, 7, 7, 7, 8, 8, 7, 7, 11, 14, 15, 16, 19, 19, + 18, 17, 16, 14, 12, 12, 10, 10, 8, 7, 7, 6, 6, 5, 6, 6, 7, 7, 7, 7, 7, 7, 7, 7, 6, 6, 10, 14, 15, 17, 19, 19, + 17, 17, 16, 14, 12, 12, 10, 10, 8, 7, 6, 6, 6, 5, 5, 4, 5, 5, 6, 6, 6, 7, 7, 5, 6, 6, 9, 13, 15, 17, 18, 20, + 17, 17, 15, 14, 12, 11, 10, 9, 8, 7, 6, 5, 5, 4, 4, 4, 4, 4, 5, 6, 6, 6, 5, 5, 5, 6, 8, 11, 15, 17, 18, 20, + 17, 17, 15, 13, 12, 11, 9, 9, 8, 7, 5, 5, 4, 4, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4, 4, 5, 7, 11, 15, 17, 18, 18, + 17, 16, 15, 13, 12, 11, 9, 8, 8, 6, 5, 4, 4, 4, 3, 3, 4, 4, 3, 3, 3, 4, 4, 4, 4, 5, 6, 9, 14, 17, 17, 18, + 17, 16, 14, 13, 12, 11, 9, 8, 7, 6, 5, 4, 4, 4, 4, 3, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 6, 8, 13, 16, 17, 17, + 17, 15, 14, 13, 12, 11, 9, 8, 7, 6, 5, 4, 4, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 4, 4, 5, 7, 12, 16, 17, 17, + 17, 15, 14, 12, 11, 10, 9, 8, 7, 6, 4, 4, 3, 3, 3, 2, 2, 3, 3, 3, 2, 2, 3, 3, 3, 4, 5, 7, 11, 15, 18, 18, + 16, 14, 13, 12, 11, 10, 9, 7, 7, 5, 4, 3, 3, 3, 2, 2, 2, 3, 3, 3, 2, 2, 2, 3, 3, 4, 5, 8, 11, 15, 18, 19, + 16, 14, 13, 12, 11, 10, 9, 7, 5, 5, 4, 3, 3, 2, 2, 2, 2, 3, 3, 3, 2, 2, 2, 2, 3, 4, 5, 8, 10, 15, 18, 19, + 16, 14, 14, 13, 11, 10, 9, 7, 5, 5, 4, 3, 2, 2, 2, 2, 2, 2, 3, 3, 2, 2, 2, 2, 3, 3, 5, 7, 10, 15, 17, 18, + 16, 15, 14, 12, 11, 10, 9, 7, 5, 5, 4, 3, 2, 2, 1, 1, 2, 2, 2, 2, 2, 2, 1, 2, 2, 3, 5, 6, 10, 14, 17, 17, + 15, 15, 13, 12, 11, 10, 9, 7, 5, 4, 3, 2, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 1, 2, 2, 3, 5, 7, 10, 14, 17, 18, + 15, 14, 13, 12, 12, 10, 9, 7, 6, 4, 3, 2, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 3, 4, 5, 7, 10, 17, 18, 18, + 15, 14, 14, 13, 12, 11, 9, 7, 6, 4, 3, 2, 1, 0, 1, 1, 1, 2, 2, 2, 2, 2, 2, 3, 3, 4, 5, 7, 12, 17, 19, 19, + 16, 14, 14, 13, 12, 12, 9, 7, 6, 4, 3, 2, 0, 0, 0, 1, 1, 1, 2, 2, 2, 2, 2, 3, 4, 4, 5, 8, 12, 17, 19, 20, + 16, 15, 14, 13, 12, 12, 9, 7, 7, 4, 3, 1, 0, 0, 0, 0, 1, 1, 1, 2, 2, 2, 3, 3, 4, 4, 6, 8, 12, 17, 19, 20, + 17, 15, 14, 13, 12, 12, 9, 7, 7, 5, 3, 1, 0, 0, 0, 0, 1, 1, 1, 2, 2, 3, 3, 4, 4, 5, 6, 8, 12, 17, 19, 20, + 18, 15, 15, 14, 13, 12, 9, 8, 7, 5, 4, 2, 1, 0, 0, 0, 0, 1, 2, 2, 3, 3, 3, 4, 5, 6, 7, 9, 13, 17, 19, 20, + 18, 16, 15, 14, 13, 12, 9, 9, 7, 6, 5, 3, 2, 1, 0, 0, 0, 1, 2, 3, 3, 3, 4, 5, 6, 7, 9, 10, 14, 18, 20, 20, + 18, 16, 16, 15, 13, 12, 10, 9, 8, 7, 6, 5, 3, 3, 1, 0, 0, 1, 2, 3, 4, 4, 5, 5, 6, 7, 9, 12, 15, 19, 20, 20, + 18, 18, 16, 16, 14, 13, 10, 10, 9, 8, 7, 6, 5, 5, 3, 1, 1, 1, 2, 3, 5, 5, 5, 6, 6, 8, 11, 15, 17, 19, 20, 20, + 18, 18, 16, 16, 14, 13, 12, 10, 9, 9, 8, 7, 6, 5, 4, 3, 1, 2, 3, 5, 5, 5, 5, 6, 7, 10, 12, 15, 18, 20, 20, 20, + 18, 18, 17, 16, 15, 14, 12, 11, 10, 10, 9, 8, 7, 6, 5, 4, 3, 2, 3, 3, 5, 5, 5, 6, 9, 11, 14, 16, 19, 20, 20, 21, + 19, 19, 18, 17, 16, 15, 13, 12, 11, 10, 10, 9, 8, 7, 5, 4, 4, 3, 3, 3, 5, 5, 6, 7, 10, 12, 14, 18, 20, 20, 21, 22, + 21, 20, 18, 18, 17, 16, 14, 12, 11, 11, 10, 10, 8, 7, 7, 5, 4, 4, 4, 5, 6, 6, 6, 7, 11, 13, 16, 19, 20, 21, 21, 22, + 22, 21, 20, 19, 18, 16, 15, 14, 12, 12, 12, 10, 9, 8, 7, 6, 5, 4, 5, 6, 6, 7, 7, 8, 12, 13, 17, 19, 21, 21, 21, 21, + 23, 22, 21, 20, 18, 17, 16, 16, 14, 14, 13, 12, 10, 10, 8, 7, 6, 5, 5, 6, 7, 7, 8, 9, 12, 15, 18, 19, 21, 21, 21, 20, + 23, 22, 22, 21, 20, 18, 17, 16, 15, 15, 15, 14, 13, 11, 9, 8, 6, 6, 6, 6, 7, 8, 9, 10, 13, 16, 19, 20, 20, 21, 21, 20, + 24, 23, 22, 21, 20, 19, 17, 17, 16, 16, 15, 15, 14, 14, 11, 9, 6, 6, 6, 6, 8, 8, 10, 12, 15, 17, 19, 20, 20, 21, 21, 20, + ] + b: [ + 11, 9, 9, 7, 6, 5, 4, 4, 3, 3, 3, 3, 3, 4, 5, 5, 5, 4, 5, 4, 4, 4, 7, 7, 3, 3, 5, 8, 8, 9, 11, 11, + 11, 10, 8, 7, 5, 5, 5, 4, 3, 3, 3, 3, 3, 3, 4, 4, 5, 4, 5, 4, 4, 4, 4, 4, 2, 2, 5, 8, 8, 9, 11, 11, + 10, 10, 7, 7, 5, 5, 5, 4, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4, 3, 1, 1, 5, 7, 9, 9, 11, 11, + 10, 9, 8, 7, 6, 5, 5, 5, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 3, 2, 1, 1, 4, 7, 9, 10, 12, 13, + 9, 9, 8, 7, 6, 5, 5, 5, 4, 3, 3, 3, 3, 2, 2, 3, 3, 3, 3, 3, 4, 3, 2, 1, 1, 1, 4, 6, 9, 10, 12, 13, + 9, 9, 9, 7, 6, 6, 5, 4, 4, 3, 3, 3, 2, 2, 2, 2, 3, 3, 2, 1, 2, 2, 2, 1, 1, 1, 2, 6, 9, 10, 11, 12, + 8, 9, 9, 7, 7, 6, 5, 4, 4, 3, 3, 2, 2, 2, 2, 2, 3, 3, 2, 1, 1, 1, 1, 0, 0, 1, 2, 5, 9, 11, 11, 11, + 8, 9, 9, 7, 7, 6, 5, 4, 4, 3, 3, 2, 2, 2, 2, 2, 3, 3, 2, 1, 1, 1, 1, 0, 0, 0, 1, 3, 8, 11, 11, 11, + 9, 9, 8, 7, 7, 7, 6, 4, 4, 3, 3, 2, 2, 2, 2, 2, 3, 3, 2, 1, 1, 1, 1, 0, 0, 0, 1, 3, 7, 11, 11, 11, + 9, 9, 8, 7, 7, 7, 6, 5, 4, 3, 2, 2, 2, 2, 2, 2, 2, 3, 2, 1, 1, 0, 0, 0, 0, 0, 1, 3, 7, 10, 12, 13, + 9, 9, 8, 8, 7, 7, 6, 5, 4, 3, 2, 1, 2, 1, 2, 2, 2, 3, 2, 1, 0, 0, 0, 0, 0, 0, 1, 4, 7, 10, 13, 13, + 9, 8, 8, 8, 7, 7, 6, 5, 3, 3, 2, 1, 1, 1, 1, 1, 2, 3, 3, 2, 1, 0, 0, 0, 0, 1, 2, 4, 7, 11, 13, 13, + 9, 8, 8, 7, 7, 6, 6, 5, 3, 3, 2, 2, 2, 1, 1, 1, 2, 3, 3, 2, 1, 0, 0, 0, 0, 1, 2, 4, 6, 11, 13, 13, + 9, 8, 8, 7, 7, 6, 6, 4, 3, 3, 2, 2, 1, 1, 1, 1, 2, 3, 3, 3, 1, 1, 0, 0, 1, 1, 2, 3, 6, 10, 12, 13, + 9, 8, 8, 7, 7, 6, 6, 4, 3, 3, 2, 1, 1, 1, 1, 1, 1, 3, 3, 3, 1, 1, 1, 1, 1, 2, 2, 4, 6, 10, 13, 13, + 9, 9, 8, 8, 8, 6, 6, 4, 4, 3, 2, 1, 1, 1, 1, 1, 2, 2, 3, 3, 1, 1, 1, 1, 2, 2, 3, 4, 6, 13, 14, 14, + 9, 9, 8, 8, 8, 8, 6, 5, 4, 3, 2, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 1, 1, 2, 2, 3, 3, 5, 9, 13, 15, 15, + 10, 9, 9, 9, 10, 10, 6, 6, 5, 3, 2, 1, 0, 0, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 4, 5, 9, 13, 15, 16, + 10, 10, 9, 9, 10, 10, 7, 6, 5, 3, 2, 1, 0, 0, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 3, 3, 4, 5, 9, 13, 15, 16, + 11, 10, 9, 9, 10, 10, 7, 6, 6, 3, 2, 1, 0, 0, 1, 1, 1, 2, 2, 2, 2, 2, 2, 3, 3, 4, 5, 6, 9, 13, 15, 16, + 12, 10, 10, 10, 10, 10, 7, 6, 6, 4, 3, 1, 1, 1, 1, 1, 1, 2, 2, 3, 3, 3, 3, 3, 5, 5, 6, 7, 10, 14, 15, 16, + 12, 11, 10, 10, 10, 10, 8, 7, 6, 6, 4, 3, 2, 1, 1, 1, 1, 2, 2, 3, 4, 4, 4, 4, 5, 5, 6, 8, 11, 15, 16, 16, + 12, 12, 11, 11, 10, 10, 9, 8, 7, 6, 6, 4, 4, 3, 2, 2, 2, 2, 3, 4, 4, 4, 5, 5, 5, 6, 7, 10, 13, 15, 16, 15, + 12, 12, 12, 12, 11, 10, 10, 8, 8, 7, 7, 6, 5, 5, 3, 2, 2, 2, 3, 4, 5, 5, 5, 5, 6, 7, 10, 13, 14, 16, 16, 16, + 12, 12, 13, 12, 12, 11, 11, 9, 8, 8, 8, 7, 6, 6, 5, 4, 3, 3, 3, 5, 6, 6, 6, 6, 7, 9, 11, 14, 15, 17, 17, 16, + 13, 13, 13, 13, 12, 12, 11, 11, 10, 10, 9, 8, 7, 7, 6, 5, 5, 4, 4, 4, 5, 6, 6, 6, 9, 10, 12, 14, 16, 17, 17, 18, + 13, 13, 14, 13, 13, 13, 12, 12, 11, 10, 10, 9, 8, 7, 6, 6, 6, 5, 4, 4, 6, 6, 6, 7, 9, 11, 12, 16, 17, 17, 17, 18, + 15, 15, 15, 15, 14, 13, 13, 13, 12, 11, 11, 10, 9, 9, 8, 7, 6, 5, 5, 5, 6, 7, 7, 8, 10, 12, 14, 17, 17, 18, 17, 17, + 16, 16, 16, 16, 15, 14, 14, 14, 13, 13, 12, 11, 11, 9, 8, 7, 7, 6, 6, 6, 7, 7, 8, 8, 10, 12, 16, 17, 18, 18, 17, 17, + 18, 17, 17, 16, 16, 15, 14, 14, 14, 14, 14, 13, 11, 11, 9, 8, 7, 7, 6, 6, 7, 7, 8, 9, 10, 13, 16, 17, 17, 18, 17, 16, + 18, 17, 17, 17, 16, 15, 15, 15, 15, 15, 15, 14, 14, 12, 10, 9, 7, 7, 6, 6, 7, 7, 8, 9, 11, 14, 16, 16, 17, 17, 16, 15, + 18, 18, 17, 17, 17, 16, 15, 15, 15, 16, 15, 15, 14, 14, 12, 9, 7, 6, 6, 6, 7, 7, 9, 10, 12, 14, 15, 16, 16, 16, 15, 15, + ] + - ct: 5500 + r: [ + 19, 18, 17, 16, 15, 13, 11, 10, 9, 9, 9, 8, 8, 8, 8, 8, 8, 9, 10, 10, 8, 8, 9, 9, 9, 11, 14, 15, 16, 16, 18, 18, + 18, 18, 17, 15, 14, 13, 11, 11, 9, 9, 8, 8, 7, 7, 7, 7, 7, 8, 9, 8, 8, 8, 9, 9, 8, 8, 11, 14, 16, 17, 17, 18, + 18, 17, 17, 15, 14, 13, 12, 11, 9, 9, 8, 7, 7, 6, 6, 6, 6, 8, 8, 8, 8, 8, 8, 8, 7, 7, 10, 13, 16, 17, 18, 18, + 17, 17, 16, 15, 14, 12, 11, 11, 9, 8, 7, 7, 6, 5, 5, 5, 5, 6, 8, 7, 8, 8, 8, 6, 6, 6, 9, 12, 16, 16, 18, 19, + 17, 17, 16, 14, 13, 12, 11, 10, 10, 8, 7, 7, 5, 5, 5, 5, 5, 5, 6, 7, 7, 7, 6, 6, 6, 6, 8, 11, 15, 16, 17, 19, + 18, 17, 16, 14, 13, 12, 11, 10, 10, 8, 7, 6, 5, 5, 4, 4, 5, 5, 5, 5, 5, 5, 5, 5, 5, 6, 7, 10, 14, 16, 17, 18, + 18, 17, 16, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 4, 4, 4, 4, 4, 3, 4, 5, 5, 5, 5, 5, 6, 10, 13, 16, 16, 17, + 18, 16, 15, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 4, 4, 4, 4, 4, 3, 3, 3, 4, 4, 4, 4, 5, 6, 8, 12, 16, 16, 16, + 17, 16, 15, 13, 12, 11, 10, 9, 8, 7, 5, 4, 4, 4, 4, 4, 4, 4, 3, 3, 3, 3, 3, 3, 4, 4, 5, 7, 11, 16, 16, 16, + 17, 16, 15, 12, 12, 12, 10, 8, 7, 7, 4, 4, 4, 4, 4, 4, 4, 4, 3, 3, 3, 3, 3, 3, 3, 4, 5, 7, 10, 16, 16, 16, + 16, 16, 14, 12, 12, 11, 10, 8, 7, 6, 4, 4, 3, 3, 3, 3, 3, 4, 3, 3, 2, 2, 2, 3, 3, 4, 4, 7, 10, 14, 16, 16, + 16, 15, 14, 13, 12, 11, 10, 8, 7, 6, 5, 4, 3, 3, 3, 3, 3, 4, 3, 3, 3, 2, 2, 3, 3, 3, 5, 7, 10, 14, 15, 16, + 16, 15, 15, 13, 11, 11, 11, 9, 7, 5, 5, 4, 3, 2, 2, 2, 3, 3, 3, 3, 2, 2, 2, 2, 3, 3, 5, 6, 9, 14, 15, 16, + 16, 15, 15, 13, 11, 11, 11, 10, 7, 5, 4, 4, 2, 2, 2, 2, 2, 3, 3, 3, 2, 2, 2, 2, 3, 3, 5, 6, 10, 13, 15, 17, + 15, 15, 14, 12, 11, 11, 11, 10, 7, 4, 4, 3, 2, 2, 2, 2, 2, 2, 3, 3, 2, 2, 2, 3, 3, 3, 5, 6, 9, 13, 16, 17, + 15, 15, 14, 12, 11, 11, 10, 9, 7, 4, 4, 3, 1, 1, 1, 1, 2, 2, 3, 3, 2, 2, 3, 3, 3, 3, 5, 7, 10, 15, 17, 17, + 15, 15, 14, 12, 11, 11, 10, 9, 7, 4, 4, 3, 1, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 5, 7, 11, 16, 17, 18, + 16, 15, 15, 12, 12, 11, 10, 9, 6, 4, 4, 3, 1, 0, 0, 1, 1, 2, 2, 2, 2, 3, 3, 3, 4, 4, 5, 7, 12, 15, 17, 18, + 16, 16, 15, 12, 12, 11, 10, 8, 6, 5, 4, 3, 1, 0, 0, 1, 1, 1, 2, 2, 2, 3, 3, 3, 4, 5, 6, 8, 12, 15, 17, 18, + 15, 15, 15, 13, 12, 12, 10, 8, 7, 5, 4, 3, 2, 0, 0, 0, 1, 1, 2, 2, 3, 3, 3, 3, 4, 5, 7, 9, 12, 16, 17, 18, + 15, 15, 15, 13, 13, 12, 10, 8, 7, 5, 5, 3, 2, 1, 0, 0, 1, 1, 1, 2, 3, 3, 3, 4, 5, 5, 7, 9, 13, 16, 18, 18, + 15, 15, 15, 14, 13, 12, 10, 8, 7, 6, 5, 5, 3, 2, 1, 0, 1, 1, 1, 2, 3, 3, 3, 5, 5, 6, 8, 10, 14, 17, 18, 19, + 16, 16, 16, 15, 13, 12, 10, 9, 8, 7, 6, 6, 5, 4, 2, 1, 1, 1, 1, 2, 3, 3, 5, 5, 6, 7, 9, 12, 15, 18, 18, 19, + 17, 16, 16, 16, 13, 12, 11, 10, 9, 8, 7, 7, 6, 5, 4, 2, 1, 1, 2, 3, 4, 4, 5, 5, 6, 8, 11, 14, 16, 18, 19, 19, + 18, 18, 17, 16, 14, 13, 12, 10, 9, 8, 8, 7, 7, 5, 5, 3, 2, 2, 4, 4, 5, 5, 5, 5, 7, 10, 12, 16, 17, 19, 19, 20, + 18, 18, 17, 16, 15, 13, 12, 10, 10, 9, 9, 9, 7, 6, 5, 4, 3, 3, 4, 4, 5, 5, 5, 6, 7, 11, 15, 17, 18, 19, 19, 20, + 19, 18, 18, 17, 16, 14, 13, 11, 10, 10, 9, 9, 8, 6, 5, 5, 4, 4, 4, 4, 6, 6, 6, 7, 9, 11, 15, 17, 19, 19, 20, 21, + 20, 19, 19, 19, 17, 16, 13, 12, 11, 11, 10, 9, 9, 7, 6, 5, 5, 4, 4, 5, 6, 7, 7, 8, 9, 12, 15, 18, 19, 19, 20, 20, + 21, 20, 20, 19, 19, 16, 16, 13, 12, 12, 11, 11, 9, 8, 7, 6, 6, 5, 5, 6, 7, 7, 8, 8, 10, 13, 17, 19, 19, 20, 20, 20, + 22, 21, 20, 20, 19, 17, 16, 14, 13, 13, 14, 12, 11, 9, 8, 7, 6, 5, 5, 6, 7, 7, 8, 9, 11, 15, 17, 19, 19, 20, 20, 20, + 22, 22, 21, 20, 19, 18, 16, 15, 14, 14, 15, 15, 13, 11, 9, 8, 7, 5, 5, 6, 7, 8, 9, 10, 13, 16, 18, 18, 19, 20, 19, 19, + 22, 22, 21, 20, 19, 19, 16, 16, 15, 15, 15, 15, 15, 13, 10, 9, 7, 5, 5, 6, 8, 8, 10, 11, 14, 16, 18, 18, 19, 19, 19, 19, + ] + g: [ + 16, 16, 15, 14, 13, 11, 10, 9, 8, 7, 7, 7, 6, 6, 6, 6, 6, 7, 8, 8, 6, 6, 7, 7, 7, 9, 12, 13, 13, 14, 14, 14, + 16, 16, 15, 14, 13, 11, 10, 9, 8, 7, 7, 6, 6, 6, 6, 6, 6, 6, 7, 7, 6, 6, 7, 7, 6, 6, 10, 12, 13, 14, 14, 14, + 16, 15, 15, 13, 13, 11, 10, 9, 8, 7, 7, 6, 5, 5, 5, 5, 5, 6, 6, 6, 6, 6, 6, 6, 5, 5, 8, 12, 14, 15, 15, 16, + 15, 15, 14, 13, 12, 11, 10, 10, 8, 7, 6, 6, 5, 4, 4, 4, 4, 5, 6, 6, 6, 6, 6, 5, 5, 5, 7, 11, 14, 15, 15, 17, + 16, 15, 14, 13, 12, 11, 10, 10, 9, 7, 6, 6, 4, 4, 4, 3, 3, 4, 4, 6, 6, 5, 5, 4, 4, 5, 6, 10, 14, 14, 16, 16, + 16, 15, 15, 13, 12, 11, 10, 10, 9, 7, 6, 6, 4, 4, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4, 4, 5, 9, 13, 14, 15, 16, + 16, 15, 15, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 4, 3, 3, 3, 3, 3, 3, 3, 4, 3, 3, 3, 4, 5, 8, 13, 14, 15, 15, + 16, 15, 14, 12, 11, 10, 9, 8, 7, 6, 6, 5, 4, 3, 3, 3, 3, 3, 2, 2, 2, 3, 3, 3, 3, 4, 5, 7, 11, 14, 15, 15, + 16, 15, 14, 12, 11, 10, 9, 8, 7, 6, 5, 3, 3, 3, 3, 3, 3, 3, 3, 2, 2, 2, 2, 2, 3, 3, 4, 6, 10, 14, 15, 14, + 16, 15, 14, 12, 11, 11, 9, 8, 7, 6, 4, 3, 3, 3, 3, 3, 3, 3, 3, 2, 2, 2, 2, 2, 2, 3, 4, 6, 10, 15, 15, 15, + 15, 15, 13, 12, 11, 11, 10, 8, 7, 5, 4, 3, 3, 3, 3, 3, 3, 3, 3, 2, 2, 2, 2, 2, 2, 3, 3, 6, 9, 13, 15, 15, + 15, 14, 13, 12, 11, 11, 10, 8, 7, 5, 5, 3, 2, 2, 2, 2, 3, 3, 3, 3, 2, 2, 2, 2, 2, 2, 4, 6, 9, 13, 14, 15, + 15, 14, 14, 13, 11, 11, 10, 8, 7, 5, 5, 3, 2, 2, 2, 2, 2, 3, 3, 3, 2, 2, 1, 2, 2, 2, 4, 5, 8, 13, 14, 15, + 15, 14, 14, 13, 11, 11, 11, 10, 7, 5, 4, 3, 2, 2, 2, 2, 2, 2, 3, 3, 2, 2, 1, 2, 2, 2, 4, 5, 8, 13, 14, 15, + 15, 14, 13, 12, 11, 11, 10, 9, 7, 4, 4, 3, 2, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 4, 5, 8, 13, 15, 16, + 15, 15, 13, 12, 11, 11, 10, 9, 7, 4, 4, 2, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 3, 4, 6, 8, 15, 16, 16, + 15, 15, 14, 12, 11, 11, 10, 9, 7, 4, 4, 2, 1, 0, 1, 1, 1, 2, 2, 2, 2, 2, 2, 3, 3, 3, 4, 6, 11, 15, 16, 17, + 15, 15, 15, 12, 12, 11, 10, 10, 7, 4, 4, 2, 1, 0, 0, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 4, 4, 6, 11, 15, 17, 17, + 15, 15, 15, 12, 12, 12, 10, 9, 7, 5, 4, 2, 1, 0, 0, 1, 1, 1, 2, 2, 2, 3, 3, 3, 3, 4, 5, 7, 11, 15, 17, 17, + 15, 15, 15, 13, 12, 12, 10, 9, 7, 5, 4, 3, 2, 0, 0, 0, 1, 1, 1, 2, 3, 3, 3, 3, 4, 4, 6, 8, 12, 15, 17, 17, + 15, 15, 15, 13, 13, 12, 10, 9, 7, 6, 5, 3, 2, 1, 0, 0, 1, 1, 1, 2, 3, 3, 3, 3, 4, 5, 6, 9, 13, 16, 17, 18, + 15, 15, 15, 14, 13, 13, 10, 9, 8, 6, 6, 5, 3, 2, 1, 1, 1, 1, 2, 2, 3, 3, 4, 4, 5, 6, 7, 10, 14, 17, 18, 18, + 15, 16, 16, 15, 13, 13, 11, 9, 8, 7, 6, 6, 5, 4, 2, 1, 1, 1, 2, 2, 3, 4, 5, 5, 6, 7, 8, 12, 15, 17, 18, 18, + 16, 16, 16, 16, 13, 13, 11, 10, 9, 8, 7, 7, 6, 6, 4, 2, 2, 2, 2, 3, 5, 5, 5, 5, 6, 8, 11, 14, 16, 18, 18, 18, + 17, 17, 17, 16, 14, 13, 13, 11, 10, 9, 8, 8, 7, 6, 5, 4, 2, 2, 4, 5, 5, 5, 5, 5, 7, 9, 12, 15, 17, 18, 18, 18, + 18, 18, 17, 16, 15, 14, 13, 11, 10, 10, 9, 9, 7, 6, 5, 5, 4, 3, 4, 4, 5, 5, 5, 6, 7, 10, 15, 16, 18, 18, 18, 19, + 19, 18, 18, 17, 16, 14, 13, 12, 11, 10, 10, 9, 9, 7, 6, 5, 5, 4, 4, 4, 6, 6, 6, 6, 8, 10, 15, 16, 18, 18, 18, 19, + 20, 19, 19, 19, 17, 16, 14, 13, 12, 11, 11, 10, 9, 7, 7, 6, 5, 4, 4, 5, 6, 6, 6, 7, 9, 11, 15, 17, 18, 18, 18, 18, + 22, 20, 20, 20, 19, 17, 16, 14, 13, 13, 12, 11, 10, 9, 7, 6, 6, 5, 5, 6, 7, 7, 7, 8, 9, 12, 16, 18, 18, 19, 18, 18, + 22, 22, 21, 20, 19, 18, 17, 16, 14, 14, 15, 13, 11, 10, 9, 8, 7, 6, 5, 6, 7, 8, 8, 9, 10, 14, 17, 18, 18, 19, 18, 17, + 22, 22, 22, 21, 20, 19, 17, 17, 16, 16, 16, 16, 14, 12, 10, 8, 7, 6, 6, 7, 8, 8, 8, 10, 13, 16, 18, 18, 18, 18, 18, 17, + 22, 22, 22, 21, 20, 20, 18, 17, 16, 16, 17, 17, 16, 14, 11, 10, 8, 6, 6, 7, 8, 8, 10, 11, 14, 17, 18, 18, 18, 18, 18, 17, + ] + b: [ + 13, 12, 12, 12, 11, 9, 8, 7, 7, 7, 7, 6, 6, 6, 6, 5, 5, 6, 7, 7, 6, 6, 5, 5, 5, 6, 9, 9, 9, 10, 9, 8, + 13, 13, 12, 11, 11, 9, 9, 8, 7, 7, 7, 6, 5, 5, 5, 5, 5, 6, 6, 6, 6, 6, 5, 5, 4, 4, 7, 9, 9, 10, 10, 9, + 13, 13, 12, 11, 11, 9, 9, 8, 7, 7, 6, 6, 5, 4, 4, 4, 4, 5, 6, 6, 6, 5, 5, 5, 3, 3, 6, 9, 10, 11, 10, 11, + 13, 13, 12, 11, 11, 10, 9, 9, 7, 7, 6, 5, 4, 4, 4, 4, 4, 4, 5, 5, 5, 5, 5, 4, 3, 3, 5, 8, 10, 11, 11, 12, + 13, 13, 12, 11, 11, 10, 9, 9, 8, 7, 6, 6, 4, 4, 3, 3, 3, 4, 4, 5, 5, 5, 4, 3, 2, 3, 4, 8, 11, 11, 11, 12, + 13, 13, 13, 11, 11, 10, 9, 9, 8, 7, 6, 6, 4, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 2, 2, 2, 3, 7, 11, 11, 11, 12, + 14, 14, 13, 11, 11, 10, 9, 8, 8, 7, 6, 6, 4, 3, 3, 3, 3, 3, 2, 2, 2, 3, 2, 2, 2, 2, 3, 6, 11, 11, 11, 11, + 14, 14, 13, 11, 11, 10, 9, 8, 7, 7, 6, 5, 3, 3, 3, 3, 3, 3, 2, 2, 2, 2, 2, 2, 2, 2, 2, 4, 9, 11, 11, 11, + 14, 14, 13, 12, 11, 11, 9, 8, 7, 6, 5, 4, 3, 3, 3, 3, 3, 3, 2, 2, 2, 2, 2, 2, 2, 2, 2, 4, 8, 12, 12, 11, + 14, 13, 13, 12, 12, 11, 10, 8, 7, 6, 4, 3, 3, 3, 3, 3, 3, 3, 2, 2, 2, 2, 2, 2, 2, 2, 2, 4, 8, 12, 12, 13, + 13, 13, 13, 12, 12, 12, 11, 9, 7, 6, 4, 3, 2, 2, 2, 2, 3, 3, 3, 2, 2, 2, 2, 2, 2, 2, 2, 4, 8, 12, 12, 13, + 14, 13, 13, 12, 12, 11, 11, 9, 7, 6, 5, 3, 2, 2, 2, 2, 2, 3, 3, 3, 2, 2, 2, 2, 2, 2, 3, 4, 8, 12, 13, 13, + 14, 13, 13, 13, 12, 11, 11, 9, 7, 6, 5, 4, 2, 1, 1, 1, 2, 3, 3, 3, 2, 2, 2, 2, 2, 2, 3, 4, 7, 12, 13, 13, + 14, 13, 13, 13, 12, 12, 12, 11, 7, 5, 5, 4, 2, 1, 1, 1, 1, 3, 3, 3, 2, 2, 2, 2, 2, 3, 3, 4, 7, 12, 13, 13, + 14, 14, 13, 13, 12, 12, 11, 11, 7, 5, 4, 3, 1, 1, 1, 1, 1, 2, 3, 3, 2, 2, 2, 3, 3, 3, 4, 4, 7, 12, 13, 14, + 14, 14, 13, 13, 12, 12, 11, 11, 8, 5, 4, 3, 1, 1, 0, 0, 1, 1, 3, 3, 3, 3, 3, 3, 3, 4, 4, 5, 8, 13, 15, 15, + 14, 15, 14, 13, 13, 12, 11, 11, 8, 5, 4, 3, 1, 0, 0, 0, 0, 1, 2, 2, 3, 3, 3, 4, 4, 4, 5, 6, 10, 15, 15, 16, + 15, 15, 15, 14, 13, 13, 12, 11, 8, 5, 4, 3, 1, 0, 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 4, 4, 5, 6, 11, 15, 16, 16, + 15, 15, 15, 14, 14, 14, 12, 11, 8, 6, 5, 3, 1, 0, 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 4, 5, 6, 7, 11, 15, 16, 16, + 15, 15, 15, 15, 14, 14, 12, 11, 9, 7, 5, 3, 2, 1, 0, 1, 1, 1, 2, 3, 3, 4, 4, 4, 4, 5, 6, 8, 12, 15, 16, 16, + 15, 16, 15, 15, 15, 14, 13, 11, 9, 7, 6, 5, 3, 2, 1, 1, 1, 1, 2, 3, 3, 4, 4, 4, 5, 6, 7, 9, 12, 16, 16, 16, + 15, 16, 16, 15, 15, 15, 13, 11, 10, 8, 7, 6, 4, 3, 2, 1, 1, 2, 2, 3, 4, 4, 5, 5, 6, 7, 8, 10, 14, 16, 16, 16, + 16, 16, 17, 16, 15, 15, 14, 12, 11, 9, 8, 8, 6, 5, 3, 2, 2, 2, 3, 3, 5, 5, 6, 6, 7, 8, 9, 12, 15, 16, 16, 16, + 16, 17, 18, 17, 16, 15, 14, 13, 11, 10, 9, 9, 8, 6, 5, 3, 3, 3, 3, 4, 6, 6, 6, 6, 7, 8, 11, 14, 16, 16, 16, 16, + 17, 18, 18, 18, 17, 16, 16, 14, 12, 11, 10, 9, 8, 7, 6, 5, 3, 3, 5, 6, 6, 6, 6, 6, 8, 10, 12, 16, 17, 17, 17, 16, + 18, 18, 18, 18, 18, 17, 16, 14, 13, 12, 11, 11, 8, 8, 6, 6, 5, 4, 5, 5, 6, 6, 6, 7, 8, 11, 15, 16, 17, 17, 16, 16, + 18, 19, 19, 19, 19, 17, 17, 15, 14, 13, 12, 11, 11, 8, 7, 6, 6, 5, 5, 5, 7, 7, 7, 8, 9, 11, 15, 17, 17, 17, 16, 16, + 20, 20, 20, 20, 19, 19, 17, 17, 15, 14, 14, 12, 11, 9, 8, 7, 7, 6, 6, 6, 8, 8, 8, 8, 9, 12, 15, 18, 18, 16, 16, 16, + 22, 22, 22, 22, 21, 20, 19, 18, 17, 16, 15, 14, 12, 11, 10, 8, 8, 7, 7, 7, 8, 8, 8, 9, 10, 13, 17, 18, 18, 16, 16, 15, + 23, 22, 22, 22, 22, 21, 20, 20, 18, 18, 19, 16, 14, 13, 11, 10, 9, 8, 7, 7, 8, 9, 9, 10, 11, 15, 17, 18, 17, 17, 16, 14, + 23, 23, 23, 23, 23, 22, 21, 21, 20, 20, 20, 19, 18, 15, 12, 11, 10, 8, 8, 8, 9, 9, 10, 11, 13, 17, 17, 17, 17, 16, 15, 13, + 23, 23, 24, 24, 23, 23, 22, 21, 21, 21, 20, 20, 19, 17, 14, 12, 11, 9, 8, 9, 9, 10, 10, 12, 15, 17, 17, 17, 16, 16, 15, 13, + ] + - ct: 8500 + r: [ + 18, 17, 16, 15, 13, 12, 10, 9, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 12, 12, 8, 8, 10, 13, 14, 15, 17, 18, + 17, 17, 16, 14, 12, 11, 10, 10, 8, 8, 8, 7, 7, 7, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 7, 7, 10, 13, 14, 15, 17, 17, + 17, 16, 15, 13, 12, 11, 10, 10, 8, 8, 7, 7, 7, 6, 7, 7, 8, 8, 8, 7, 7, 7, 7, 7, 6, 6, 9, 12, 14, 15, 17, 17, + 16, 16, 15, 13, 12, 11, 10, 10, 9, 7, 7, 7, 6, 6, 5, 5, 6, 6, 7, 7, 7, 7, 7, 5, 5, 5, 8, 11, 14, 15, 17, 19, + 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 7, 6, 6, 5, 5, 5, 5, 5, 5, 6, 6, 6, 5, 5, 5, 5, 7, 10, 13, 15, 17, 19, + 15, 15, 14, 13, 12, 11, 9, 9, 8, 7, 6, 5, 5, 5, 4, 4, 5, 5, 5, 4, 4, 4, 4, 4, 4, 5, 6, 9, 13, 15, 16, 17, + 15, 15, 13, 12, 11, 11, 9, 8, 8, 6, 5, 5, 4, 4, 4, 4, 4, 4, 4, 3, 3, 4, 4, 4, 4, 4, 5, 8, 13, 15, 15, 16, + 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 4, 4, 4, 4, 4, 4, 4, 3, 3, 3, 3, 3, 3, 4, 5, 7, 11, 14, 15, 15, + 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 4, 4, 4, 3, 3, 4, 3, 3, 3, 2, 3, 3, 3, 3, 4, 6, 10, 14, 15, 15, + 15, 13, 13, 11, 11, 10, 9, 8, 7, 6, 4, 4, 4, 3, 3, 3, 3, 3, 3, 3, 2, 2, 2, 2, 2, 3, 4, 6, 10, 13, 16, 16, + 14, 13, 12, 11, 10, 10, 9, 7, 7, 5, 4, 3, 3, 3, 3, 2, 2, 3, 3, 3, 2, 2, 2, 2, 2, 3, 4, 6, 9, 13, 16, 17, + 14, 13, 12, 11, 10, 10, 9, 7, 6, 5, 4, 3, 3, 3, 2, 2, 2, 3, 3, 3, 2, 2, 1, 2, 2, 3, 4, 6, 9, 13, 16, 17, + 14, 13, 12, 12, 10, 10, 9, 7, 5, 5, 4, 3, 3, 2, 2, 2, 2, 3, 3, 3, 2, 2, 1, 1, 2, 3, 4, 6, 9, 13, 15, 17, + 14, 13, 12, 11, 10, 9, 9, 7, 5, 5, 4, 3, 2, 2, 2, 2, 2, 3, 3, 3, 2, 2, 1, 1, 2, 3, 4, 6, 9, 12, 15, 16, + 13, 13, 12, 11, 10, 9, 8, 7, 5, 4, 4, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 2, 2, 3, 4, 6, 8, 12, 15, 16, + 13, 13, 12, 11, 11, 9, 8, 7, 6, 4, 3, 2, 1, 1, 1, 1, 2, 2, 2, 2, 2, 1, 1, 2, 3, 3, 4, 6, 9, 15, 16, 16, + 13, 13, 12, 12, 11, 10, 9, 7, 6, 4, 3, 2, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 3, 4, 4, 6, 10, 15, 17, 18, + 14, 13, 13, 12, 12, 11, 9, 7, 6, 4, 3, 2, 1, 0, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 3, 4, 5, 7, 11, 15, 17, 18, + 14, 13, 13, 12, 12, 11, 9, 7, 7, 4, 3, 1, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 4, 4, 5, 7, 11, 15, 17, 18, + 15, 13, 13, 12, 12, 11, 8, 7, 6, 4, 3, 1, 0, 0, 0, 0, 1, 1, 1, 2, 2, 2, 2, 3, 4, 5, 6, 7, 11, 15, 17, 18, + 15, 14, 13, 13, 12, 11, 8, 8, 7, 5, 4, 2, 1, 0, 0, 0, 0, 1, 2, 2, 2, 2, 3, 3, 5, 5, 7, 8, 11, 15, 17, 18, + 15, 14, 14, 13, 12, 11, 9, 8, 7, 6, 5, 3, 2, 1, 0, 0, 0, 1, 2, 2, 3, 3, 3, 5, 5, 6, 8, 9, 13, 16, 18, 18, + 15, 14, 14, 13, 12, 11, 9, 9, 8, 7, 6, 5, 3, 3, 1, 0, 0, 1, 2, 2, 3, 4, 4, 5, 5, 6, 8, 11, 14, 17, 18, 18, + 16, 16, 15, 14, 13, 12, 10, 9, 8, 8, 7, 6, 5, 5, 3, 1, 1, 1, 2, 3, 4, 4, 5, 5, 6, 8, 10, 13, 15, 17, 18, 18, + 16, 16, 15, 14, 14, 12, 11, 10, 9, 9, 8, 7, 6, 5, 4, 3, 1, 1, 2, 4, 4, 5, 5, 5, 7, 9, 11, 14, 16, 18, 18, 19, + 16, 16, 15, 15, 14, 13, 12, 11, 10, 10, 9, 8, 7, 6, 5, 4, 3, 2, 3, 3, 4, 5, 5, 6, 9, 10, 13, 15, 18, 18, 19, 19, + 17, 17, 16, 15, 15, 14, 12, 11, 11, 10, 10, 9, 8, 7, 6, 5, 4, 3, 3, 3, 5, 5, 6, 6, 9, 11, 13, 17, 18, 19, 19, 20, + 19, 18, 17, 17, 15, 15, 13, 12, 11, 11, 10, 10, 9, 8, 7, 5, 5, 4, 4, 5, 6, 6, 6, 7, 10, 12, 15, 18, 19, 19, 19, 20, + 19, 19, 18, 17, 17, 16, 14, 13, 12, 12, 11, 10, 9, 8, 7, 6, 6, 5, 5, 6, 6, 6, 7, 8, 11, 12, 16, 18, 19, 19, 19, 19, + 20, 19, 19, 18, 17, 16, 15, 14, 13, 13, 12, 12, 10, 9, 8, 7, 6, 5, 5, 6, 6, 7, 8, 9, 11, 14, 17, 18, 19, 19, 19, 19, + 20, 20, 19, 18, 18, 17, 15, 15, 14, 14, 14, 13, 12, 11, 9, 7, 6, 5, 5, 6, 7, 7, 8, 9, 12, 15, 17, 18, 18, 19, 18, 18, + 21, 20, 20, 19, 18, 18, 15, 15, 14, 14, 14, 14, 13, 13, 11, 9, 6, 5, 5, 6, 7, 7, 9, 10, 13, 15, 18, 18, 18, 18, 18, 18, + ] + g: [ + 16, 16, 15, 13, 12, 10, 9, 8, 7, 7, 7, 6, 6, 6, 7, 7, 7, 6, 6, 6, 6, 6, 11, 11, 6, 6, 9, 12, 12, 13, 15, 15, + 16, 15, 14, 13, 11, 10, 9, 9, 8, 7, 7, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 5, 5, 9, 12, 12, 13, 15, 15, + 15, 15, 14, 12, 11, 11, 9, 9, 8, 7, 6, 6, 6, 5, 5, 5, 6, 6, 6, 6, 6, 6, 6, 5, 4, 5, 8, 11, 13, 14, 15, 15, + 15, 15, 14, 12, 11, 10, 9, 9, 8, 6, 6, 6, 5, 5, 4, 4, 4, 4, 6, 6, 6, 6, 6, 4, 4, 4, 7, 11, 13, 14, 15, 17, + 15, 15, 13, 12, 11, 10, 9, 9, 8, 6, 6, 5, 5, 4, 4, 3, 3, 4, 4, 5, 5, 5, 4, 4, 3, 4, 6, 9, 13, 14, 15, 17, + 15, 14, 13, 12, 11, 10, 9, 8, 8, 6, 5, 5, 4, 4, 3, 3, 3, 3, 3, 3, 3, 4, 3, 3, 3, 4, 5, 8, 13, 14, 14, 15, + 14, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 4, 3, 3, 3, 3, 3, 3, 3, 2, 3, 3, 3, 3, 3, 4, 7, 12, 14, 14, 14, + 14, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 3, 3, 3, 3, 3, 3, 2, 2, 2, 2, 2, 3, 3, 4, 6, 11, 14, 14, 14, + 14, 13, 12, 11, 11, 10, 9, 8, 7, 6, 4, 4, 3, 3, 3, 2, 3, 3, 3, 3, 2, 2, 2, 2, 2, 3, 3, 5, 10, 14, 14, 14, + 15, 13, 12, 11, 11, 10, 9, 8, 7, 6, 4, 4, 3, 3, 2, 2, 2, 3, 3, 3, 2, 2, 2, 2, 2, 2, 3, 5, 10, 13, 15, 16, + 14, 13, 12, 11, 11, 10, 9, 7, 7, 5, 4, 3, 2, 2, 2, 2, 2, 2, 3, 2, 2, 1, 2, 2, 2, 2, 3, 6, 9, 13, 16, 16, + 14, 13, 12, 11, 10, 10, 9, 7, 5, 5, 4, 3, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 2, 2, 2, 4, 6, 8, 13, 16, 16, + 14, 13, 13, 12, 10, 10, 9, 7, 5, 5, 4, 3, 2, 2, 1, 1, 1, 2, 2, 2, 2, 1, 1, 1, 2, 2, 4, 5, 8, 13, 15, 16, + 14, 13, 13, 12, 10, 10, 9, 7, 5, 5, 4, 3, 2, 1, 1, 1, 1, 2, 2, 2, 2, 1, 1, 1, 1, 2, 3, 5, 8, 12, 14, 15, + 14, 13, 12, 11, 10, 10, 9, 7, 6, 4, 3, 2, 1, 1, 1, 1, 1, 2, 2, 2, 2, 1, 1, 1, 2, 3, 3, 5, 8, 12, 15, 15, + 14, 13, 12, 11, 11, 10, 9, 7, 6, 4, 3, 2, 1, 1, 1, 1, 1, 1, 2, 2, 1, 1, 1, 2, 2, 3, 4, 5, 8, 14, 16, 16, + 14, 13, 13, 12, 12, 10, 9, 7, 6, 4, 3, 2, 1, 0, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 3, 3, 4, 5, 10, 15, 16, 17, + 14, 13, 13, 13, 12, 12, 9, 7, 7, 4, 3, 2, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 2, 3, 3, 4, 6, 11, 15, 17, 17, + 14, 14, 13, 12, 12, 12, 9, 8, 7, 5, 3, 2, 0, 0, 0, 0, 1, 1, 1, 2, 2, 2, 2, 3, 3, 4, 5, 7, 11, 15, 17, 17, + 15, 14, 13, 13, 12, 12, 9, 8, 7, 5, 3, 2, 0, 0, 0, 0, 1, 1, 1, 2, 2, 2, 3, 3, 4, 5, 5, 7, 11, 15, 17, 17, + 16, 14, 14, 13, 13, 12, 9, 8, 7, 5, 4, 2, 1, 0, 0, 0, 0, 1, 2, 2, 3, 3, 3, 3, 5, 5, 6, 8, 12, 16, 17, 17, + 16, 15, 14, 14, 13, 12, 10, 9, 7, 6, 5, 4, 2, 1, 0, 0, 0, 1, 2, 3, 3, 3, 3, 5, 5, 6, 8, 9, 13, 17, 17, 17, + 16, 15, 15, 14, 13, 12, 10, 10, 8, 7, 6, 5, 4, 3, 2, 0, 1, 1, 2, 3, 3, 4, 5, 5, 5, 6, 8, 11, 14, 17, 17, 17, + 16, 16, 15, 15, 14, 13, 11, 10, 9, 8, 7, 6, 5, 5, 3, 2, 1, 1, 2, 3, 5, 5, 5, 5, 6, 7, 10, 13, 16, 17, 17, 17, + 17, 17, 15, 15, 14, 13, 12, 11, 10, 9, 9, 7, 6, 6, 5, 3, 2, 2, 3, 4, 5, 5, 5, 6, 6, 9, 11, 14, 16, 18, 18, 18, + 17, 17, 16, 15, 15, 14, 13, 12, 11, 10, 10, 9, 7, 7, 5, 4, 3, 2, 3, 4, 5, 5, 5, 6, 9, 10, 13, 15, 18, 18, 18, 18, + 18, 17, 17, 16, 15, 15, 14, 12, 11, 11, 11, 9, 8, 7, 6, 5, 4, 3, 3, 4, 5, 5, 6, 6, 9, 11, 13, 17, 18, 18, 18, 18, + 20, 19, 18, 18, 16, 16, 14, 13, 12, 11, 11, 10, 9, 8, 7, 5, 5, 4, 4, 5, 6, 6, 6, 7, 10, 11, 15, 18, 18, 18, 18, 18, + 20, 20, 19, 18, 18, 17, 15, 14, 13, 13, 12, 11, 10, 9, 8, 6, 6, 5, 5, 6, 6, 7, 7, 8, 11, 12, 16, 18, 18, 18, 18, 18, + 22, 21, 20, 19, 18, 17, 17, 17, 15, 15, 14, 13, 11, 10, 8, 7, 6, 6, 6, 6, 7, 7, 8, 8, 11, 14, 17, 18, 18, 18, 18, 17, + 22, 22, 21, 20, 19, 18, 17, 17, 17, 16, 16, 15, 14, 12, 10, 8, 7, 6, 6, 7, 7, 8, 8, 10, 13, 16, 18, 18, 18, 18, 18, 16, + 22, 22, 22, 21, 20, 19, 18, 17, 17, 17, 16, 16, 15, 15, 12, 10, 7, 6, 6, 7, 8, 8, 9, 11, 14, 16, 18, 18, 18, 18, 17, 16, + ] + b: [ + 13, 13, 13, 11, 10, 9, 9, 8, 7, 7, 7, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 9, 9, 4, 4, 7, 9, 9, 9, 10, 10, + 13, 13, 12, 11, 10, 9, 9, 8, 7, 7, 7, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 3, 3, 7, 9, 9, 10, 10, 10, + 13, 13, 12, 11, 10, 10, 9, 9, 7, 7, 6, 6, 6, 5, 5, 5, 5, 6, 6, 6, 5, 5, 5, 4, 3, 3, 6, 9, 10, 11, 11, 11, + 13, 13, 12, 11, 10, 10, 9, 9, 8, 7, 6, 6, 5, 5, 4, 4, 4, 4, 5, 5, 5, 5, 5, 3, 2, 3, 5, 9, 10, 11, 11, 13, + 13, 13, 12, 11, 11, 10, 9, 9, 8, 7, 6, 5, 5, 4, 4, 3, 3, 4, 4, 5, 5, 5, 3, 2, 2, 3, 5, 8, 11, 11, 12, 13, + 13, 12, 12, 11, 11, 10, 9, 8, 8, 7, 5, 5, 4, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 2, 2, 2, 3, 7, 11, 11, 11, 11, + 13, 13, 12, 11, 11, 10, 9, 8, 8, 7, 5, 4, 4, 3, 3, 3, 3, 3, 3, 2, 2, 3, 2, 2, 2, 2, 3, 6, 11, 11, 11, 11, + 13, 13, 12, 11, 11, 11, 9, 8, 7, 6, 5, 4, 3, 3, 3, 3, 3, 3, 3, 2, 2, 2, 2, 2, 2, 2, 2, 5, 10, 12, 12, 11, + 13, 13, 13, 11, 11, 11, 10, 8, 7, 6, 5, 4, 3, 3, 3, 2, 2, 3, 3, 2, 2, 2, 2, 2, 2, 2, 2, 4, 9, 12, 12, 12, + 14, 13, 13, 12, 11, 11, 10, 8, 7, 6, 4, 4, 3, 2, 2, 2, 2, 3, 3, 2, 2, 2, 2, 2, 2, 2, 2, 4, 9, 12, 13, 14, + 13, 13, 12, 12, 11, 11, 10, 8, 7, 6, 4, 3, 2, 2, 2, 1, 2, 3, 3, 2, 2, 1, 2, 2, 2, 2, 2, 4, 8, 12, 14, 14, + 13, 13, 12, 12, 11, 11, 11, 8, 7, 6, 5, 3, 2, 2, 1, 1, 2, 3, 3, 2, 2, 2, 2, 2, 2, 2, 3, 4, 8, 12, 14, 14, + 13, 13, 12, 12, 12, 11, 11, 8, 7, 6, 5, 3, 2, 1, 1, 1, 1, 2, 3, 3, 2, 2, 2, 2, 2, 3, 3, 4, 8, 12, 14, 14, + 13, 13, 13, 12, 12, 11, 11, 8, 6, 6, 5, 3, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 4, 7, 12, 13, 14, + 13, 13, 13, 12, 12, 11, 10, 8, 6, 5, 4, 2, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 3, 3, 4, 5, 7, 12, 14, 14, + 14, 14, 13, 13, 13, 11, 10, 8, 7, 5, 4, 2, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 3, 3, 4, 4, 5, 8, 14, 16, 16, + 14, 14, 13, 13, 13, 12, 11, 9, 7, 5, 4, 2, 1, 0, 0, 0, 1, 1, 2, 2, 2, 2, 3, 3, 4, 4, 4, 6, 10, 15, 16, 16, + 15, 14, 14, 14, 15, 15, 11, 9, 8, 5, 4, 2, 1, 0, 0, 0, 1, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 7, 11, 15, 16, 17, + 15, 15, 14, 14, 15, 15, 12, 10, 9, 6, 4, 2, 1, 0, 0, 0, 1, 1, 1, 2, 3, 3, 3, 4, 5, 5, 6, 7, 11, 15, 16, 16, + 15, 15, 15, 15, 15, 15, 12, 10, 9, 6, 5, 2, 1, 0, 0, 0, 1, 1, 1, 3, 3, 3, 4, 4, 5, 6, 6, 7, 11, 15, 16, 17, + 16, 16, 15, 16, 15, 15, 12, 11, 10, 7, 5, 3, 2, 1, 0, 0, 1, 1, 2, 3, 4, 4, 4, 5, 6, 6, 7, 9, 12, 16, 16, 16, + 16, 16, 16, 16, 16, 15, 13, 12, 10, 9, 6, 5, 3, 2, 1, 1, 1, 2, 3, 4, 4, 4, 5, 6, 6, 7, 8, 9, 14, 16, 17, 16, + 16, 16, 16, 16, 16, 15, 14, 12, 11, 10, 8, 6, 5, 4, 2, 1, 1, 2, 3, 4, 5, 5, 6, 6, 6, 7, 9, 12, 15, 16, 16, 16, + 17, 17, 18, 17, 16, 16, 14, 13, 12, 11, 9, 8, 6, 5, 4, 2, 2, 2, 3, 4, 6, 6, 6, 6, 7, 8, 11, 14, 16, 17, 16, 16, + 17, 17, 18, 18, 17, 16, 16, 14, 13, 12, 11, 9, 8, 7, 5, 4, 2, 3, 4, 6, 6, 6, 6, 7, 8, 10, 12, 15, 17, 17, 17, 16, + 18, 18, 18, 18, 18, 17, 16, 15, 14, 13, 12, 11, 9, 8, 6, 5, 4, 4, 4, 5, 6, 6, 7, 7, 10, 11, 14, 16, 17, 17, 17, 17, + 18, 18, 19, 19, 19, 18, 17, 16, 15, 14, 14, 11, 10, 9, 7, 6, 6, 5, 5, 5, 6, 7, 7, 7, 10, 12, 14, 17, 17, 17, 17, 17, + 20, 20, 20, 20, 20, 19, 18, 17, 16, 15, 14, 13, 11, 10, 9, 7, 6, 6, 6, 6, 7, 7, 7, 8, 11, 12, 15, 18, 18, 17, 17, 16, + 22, 21, 21, 21, 21, 21, 20, 19, 17, 16, 16, 14, 13, 11, 10, 8, 7, 7, 7, 7, 8, 8, 8, 9, 11, 13, 17, 18, 18, 17, 16, 15, + 23, 22, 22, 22, 22, 21, 21, 20, 19, 19, 18, 16, 14, 13, 11, 9, 8, 8, 8, 8, 8, 8, 9, 10, 12, 15, 18, 18, 18, 17, 16, 14, + 23, 24, 24, 23, 23, 22, 22, 21, 21, 20, 20, 19, 18, 15, 13, 11, 9, 8, 8, 8, 9, 9, 10, 11, 13, 16, 18, 18, 17, 17, 15, 14, + 24, 24, 24, 24, 24, 23, 22, 22, 22, 22, 20, 20, 19, 18, 15, 13, 10, 9, 9, 9, 9, 10, 11, 12, 15, 17, 18, 18, 17, 16, 15, 13, + ] +... diff --git a/src/ipa/mali-c55/data/meson.build b/src/ipa/mali-c55/data/meson.build new file mode 100644 index 00000000..8a5fdd36 --- /dev/null +++ b/src/ipa/mali-c55/data/meson.build @@ -0,0 +1,9 @@ +# SPDX-License-Identifier: CC0-1.0 + +conf_files = files([ + 'imx415.yaml', + 'uncalibrated.yaml' +]) + +install_data(conf_files, + install_dir : ipa_data_dir / 'mali-c55') diff --git a/src/ipa/mali-c55/data/uncalibrated.yaml b/src/ipa/mali-c55/data/uncalibrated.yaml new file mode 100644 index 00000000..6dcc0295 --- /dev/null +++ b/src/ipa/mali-c55/data/uncalibrated.yaml @@ -0,0 +1,7 @@ +# SPDX-License-Identifier: CC0-1.0 +%YAML 1.1 +--- +version: 1 +algorithms: + - Agc: +... diff --git a/src/ipa/mali-c55/ipa_context.cpp b/src/ipa/mali-c55/ipa_context.cpp new file mode 100644 index 00000000..99f76ecd --- /dev/null +++ b/src/ipa/mali-c55/ipa_context.cpp @@ -0,0 +1,101 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +/* + * Copyright (C) 2024, Ideas On Board + * + * ipa_context.cpp - MaliC55 IPA Context + */ + +#include "ipa_context.h" + +/** + * \file ipa_context.h + * \brief Context and state information shared between the algorithms + */ + +namespace libcamera::ipa::mali_c55 { + +/** + * \struct IPASessionConfiguration + * \brief Session configuration for the IPA module + * + * The session configuration contains all IPA configuration parameters that + * remain constant during the capture session, from IPA module start to stop. + * It is typically set during the configure() operation of the IPA module, but + * may also be updated in the start() operation. + */ + +/** + * \struct IPAActiveState + * \brief Active state for algorithms + * + * The active state contains all algorithm-specific data that needs to be + * maintained by algorithms across frames. Unlike the session configuration, + * the active state is mutable and constantly updated by algorithms. The active + * state is accessible through the IPAContext structure. + * + * The active state stores two distinct categories of information: + * + * - The consolidated value of all algorithm controls. Requests passed to + * the queueRequest() function store values for controls that the + * application wants to modify for that particular frame, and the + * queueRequest() function updates the active state with those values. + * The active state thus contains a consolidated view of the value of all + * controls handled by the algorithm. + * + * - The value of parameters computed by the algorithm when running in auto + * mode. Algorithms running in auto mode compute new parameters every + * time statistics buffers are received (either synchronously, or + * possibly in a background thread). The latest computed value of those + * parameters is stored in the active state in the process() function. + * + * Each of the members in the active state belongs to a specific algorithm. A + * member may be read by any algorithm, but shall only be written by its owner. + */ + +/** + * \struct IPAFrameContext + * \brief Per-frame context for algorithms + * + * The frame context stores two distinct categories of information: + * + * - The value of the controls to be applied to the frame. These values are + * typically set in the queueRequest() function, from the consolidated + * control values stored in the active state. The frame context thus stores + * values for all controls related to the algorithm, not limited to the + * controls specified in the corresponding request, but consolidated from all + * requests that have been queued so far. + * + * For controls that can be set manually or computed by an algorithm + * (depending on the algorithm operation mode), such as for instance the + * colour gains for the AWB algorithm, the control value will be stored in + * the frame context in the queueRequest() function only when operating in + * manual mode. When operating in auto mode, the values are computed by the + * algorithm in process(), stored in the active state, and copied to the + * frame context in prepare(), just before being stored in the ISP parameters + * buffer. + * + * The queueRequest() function can also store ancillary data in the frame + * context, such as flags to indicate if (and what) control values have + * changed compared to the previous request. + * + * - Status information computed by the algorithm for a frame. For instance, + * the colour temperature estimated by the AWB algorithm from ISP statistics + * calculated on a frame is stored in the frame context for that frame in + * the process() function. + */ + +/** + * \struct IPAContext + * \brief Global IPA context data shared between all algorithms + * + * \var IPAContext::configuration + * \brief The IPA session configuration, immutable during the session + * + * \var IPAContext::activeState + * \brief The IPA active state, storing the latest state for all algorithms + * + * \var IPAContext::frameContexts + * \brief Ring buffer of per-frame contexts + */ + +} /* namespace libcamera::ipa::mali_c55 */ diff --git a/src/ipa/mali-c55/ipa_context.h b/src/ipa/mali-c55/ipa_context.h new file mode 100644 index 00000000..5e3e2fbd --- /dev/null +++ b/src/ipa/mali-c55/ipa_context.h @@ -0,0 +1,90 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +/* + * Copyright (C) 2024, Ideas On Board + * + * ipa_context.h - Mali-C55 IPA Context + */ + +#pragma once + +#include <libcamera/base/utils.h> +#include <libcamera/controls.h> + +#include "libcamera/internal/bayer_format.h" + +#include <libipa/fc_queue.h> + +namespace libcamera { + +namespace ipa::mali_c55 { + +struct IPASessionConfiguration { + struct { + utils::Duration minShutterSpeed; + utils::Duration maxShutterSpeed; + uint32_t defaultExposure; + double minAnalogueGain; + double maxAnalogueGain; + } agc; + + struct { + BayerFormat::Order bayerOrder; + utils::Duration lineDuration; + uint32_t blackLevel; + } sensor; +}; + +struct IPAActiveState { + struct { + struct { + uint32_t exposure; + double sensorGain; + double ispGain; + } automatic; + struct { + uint32_t exposure; + double sensorGain; + double ispGain; + } manual; + bool autoEnabled; + uint32_t constraintMode; + uint32_t exposureMode; + uint32_t temperatureK; + } agc; + + struct { + double rGain; + double bGain; + } awb; +}; + +struct IPAFrameContext : public FrameContext { + struct { + uint32_t exposure; + double sensorGain; + double ispGain; + } agc; + + struct { + double rGain; + double bGain; + } awb; +}; + +struct IPAContext { + IPAContext(unsigned int frameContextSize) + : frameContexts(frameContextSize) + { + } + + IPASessionConfiguration configuration; + IPAActiveState activeState; + + FCQueue<IPAFrameContext> frameContexts; + + ControlInfoMap::Map ctrlMap; +}; + +} /* namespace ipa::mali_c55 */ + +} /* namespace libcamera*/ diff --git a/src/ipa/mali-c55/mali-c55.cpp b/src/ipa/mali-c55/mali-c55.cpp new file mode 100644 index 00000000..c6941a95 --- /dev/null +++ b/src/ipa/mali-c55/mali-c55.cpp @@ -0,0 +1,399 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +/* + * Copyright (C) 2023, Ideas on Board Oy + * + * mali-c55.cpp - Mali-C55 ISP image processing algorithms + */ + +#include <map> +#include <string.h> +#include <vector> + +#include <linux/mali-c55-config.h> +#include <linux/v4l2-controls.h> + +#include <libcamera/base/file.h> +#include <libcamera/base/log.h> + +#include <libcamera/control_ids.h> +#include <libcamera/ipa/ipa_interface.h> +#include <libcamera/ipa/ipa_module_info.h> +#include <libcamera/ipa/mali-c55_ipa_interface.h> + +#include "libcamera/internal/bayer_format.h" +#include "libcamera/internal/mapped_framebuffer.h" +#include "libcamera/internal/yaml_parser.h" + +#include "algorithms/algorithm.h" +#include "libipa/camera_sensor_helper.h" + +#include "ipa_context.h" + +namespace libcamera { + +LOG_DEFINE_CATEGORY(IPAMaliC55) + +using namespace std::literals::chrono_literals; + +namespace ipa::mali_c55 { + +/* Maximum number of frame contexts to be held */ +static constexpr uint32_t kMaxFrameContexts = 16; + +class IPAMaliC55 : public IPAMaliC55Interface, public Module +{ +public: + IPAMaliC55(); + + int init(const IPASettings &settings, const IPAConfigInfo &ipaConfig, + ControlInfoMap *ipaControls) override; + int start() override; + void stop() override; + int configure(const IPAConfigInfo &ipaConfig, uint8_t bayerOrder, + ControlInfoMap *ipaControls) override; + void mapBuffers(const std::vector<IPABuffer> &buffers, bool readOnly) override; + void unmapBuffers(const std::vector<IPABuffer> &buffers) override; + void queueRequest(const uint32_t request, const ControlList &controls) override; + void fillParams(unsigned int request, uint32_t bufferId) override; + void processStats(unsigned int request, unsigned int bufferId, + const ControlList &sensorControls) override; + +protected: + std::string logPrefix() const override; + +private: + void updateSessionConfiguration(const IPACameraSensorInfo &info, + const ControlInfoMap &sensorControls, + BayerFormat::Order bayerOrder); + void updateControls(const IPACameraSensorInfo &sensorInfo, + const ControlInfoMap &sensorControls, + ControlInfoMap *ipaControls); + void setControls(); + + std::map<unsigned int, MappedFrameBuffer> buffers_; + + ControlInfoMap sensorControls_; + + /* Interface to the Camera Helper */ + std::unique_ptr<CameraSensorHelper> camHelper_; + + /* Local parameter storage */ + struct IPAContext context_; +}; + +namespace { + +} /* namespace */ + +IPAMaliC55::IPAMaliC55() + : context_(kMaxFrameContexts) +{ +} + +std::string IPAMaliC55::logPrefix() const +{ + return "mali-c55"; +} + +int IPAMaliC55::init(const IPASettings &settings, const IPAConfigInfo &ipaConfig, + ControlInfoMap *ipaControls) +{ + camHelper_ = CameraSensorHelperFactoryBase::create(settings.sensorModel); + if (!camHelper_) { + LOG(IPAMaliC55, Error) + << "Failed to create camera sensor helper for " + << settings.sensorModel; + return -ENODEV; + } + + File file(settings.configurationFile); + if (!file.open(File::OpenModeFlag::ReadOnly)) { + int ret = file.error(); + LOG(IPAMaliC55, Error) + << "Failed to open configuration file " + << settings.configurationFile << ": " << strerror(-ret); + return ret; + } + + std::unique_ptr<libcamera::YamlObject> data = YamlParser::parse(file); + if (!data) + return -EINVAL; + + if (!data->contains("algorithms")) { + LOG(IPAMaliC55, Error) + << "Tuning file doesn't contain any algorithm"; + return -EINVAL; + } + + int ret = createAlgorithms(context_, (*data)["algorithms"]); + if (ret) + return ret; + + updateControls(ipaConfig.sensorInfo, ipaConfig.sensorControls, ipaControls); + + return 0; +} + +void IPAMaliC55::setControls() +{ + IPAActiveState &activeState = context_.activeState; + uint32_t exposure; + uint32_t gain; + + if (activeState.agc.autoEnabled) { + exposure = activeState.agc.automatic.exposure; + gain = camHelper_->gainCode(activeState.agc.automatic.sensorGain); + } else { + exposure = activeState.agc.manual.exposure; + gain = camHelper_->gainCode(activeState.agc.manual.sensorGain); + } + + ControlList ctrls(sensorControls_); + ctrls.set(V4L2_CID_EXPOSURE, static_cast<int32_t>(exposure)); + ctrls.set(V4L2_CID_ANALOGUE_GAIN, static_cast<int32_t>(gain)); + + setSensorControls.emit(ctrls); +} + +int IPAMaliC55::start() +{ + return 0; +} + +void IPAMaliC55::stop() +{ + context_.frameContexts.clear(); +} + +void IPAMaliC55::updateSessionConfiguration(const IPACameraSensorInfo &info, + const ControlInfoMap &sensorControls, + BayerFormat::Order bayerOrder) +{ + context_.configuration.sensor.bayerOrder = bayerOrder; + + const ControlInfo &v4l2Exposure = sensorControls.find(V4L2_CID_EXPOSURE)->second; + int32_t minExposure = v4l2Exposure.min().get<int32_t>(); + int32_t maxExposure = v4l2Exposure.max().get<int32_t>(); + int32_t defExposure = v4l2Exposure.def().get<int32_t>(); + + const ControlInfo &v4l2Gain = sensorControls.find(V4L2_CID_ANALOGUE_GAIN)->second; + int32_t minGain = v4l2Gain.min().get<int32_t>(); + int32_t maxGain = v4l2Gain.max().get<int32_t>(); + + /* + * When the AGC computes the new exposure values for a frame, it needs + * to know the limits for shutter speed and analogue gain. + * As it depends on the sensor, update it with the controls. + * + * \todo take VBLANK into account for maximum shutter speed + */ + context_.configuration.sensor.lineDuration = info.minLineLength * 1.0s / info.pixelRate; + context_.configuration.agc.minShutterSpeed = minExposure * context_.configuration.sensor.lineDuration; + context_.configuration.agc.maxShutterSpeed = maxExposure * context_.configuration.sensor.lineDuration; + context_.configuration.agc.defaultExposure = defExposure; + context_.configuration.agc.minAnalogueGain = camHelper_->gain(minGain); + context_.configuration.agc.maxAnalogueGain = camHelper_->gain(maxGain); + + if (camHelper_->blackLevel().has_value()) { + /* + * The black level from CameraSensorHelper is a 16-bit value. + * The Mali-C55 ISP expects 20-bit settings, so we shift it to + * the appropriate width + */ + context_.configuration.sensor.blackLevel = + camHelper_->blackLevel().value() << 4; + } +} + +void IPAMaliC55::updateControls(const IPACameraSensorInfo &sensorInfo, + const ControlInfoMap &sensorControls, + ControlInfoMap *ipaControls) +{ + ControlInfoMap::Map ctrlMap; + + /* + * Compute the frame duration limits. + * + * The frame length is computed assuming a fixed line length combined + * with the vertical frame sizes. + */ + const ControlInfo &v4l2HBlank = sensorControls.find(V4L2_CID_HBLANK)->second; + uint32_t hblank = v4l2HBlank.def().get<int32_t>(); + uint32_t lineLength = sensorInfo.outputSize.width + hblank; + + const ControlInfo &v4l2VBlank = sensorControls.find(V4L2_CID_VBLANK)->second; + std::array<uint32_t, 3> frameHeights{ + v4l2VBlank.min().get<int32_t>() + sensorInfo.outputSize.height, + v4l2VBlank.max().get<int32_t>() + sensorInfo.outputSize.height, + v4l2VBlank.def().get<int32_t>() + sensorInfo.outputSize.height, + }; + + std::array<int64_t, 3> frameDurations; + for (unsigned int i = 0; i < frameHeights.size(); ++i) { + uint64_t frameSize = lineLength * frameHeights[i]; + frameDurations[i] = frameSize / (sensorInfo.pixelRate / 1000000U); + } + + ctrlMap[&controls::FrameDurationLimits] = ControlInfo(frameDurations[0], + frameDurations[1], + frameDurations[2]); + + /* + * Compute exposure time limits from the V4L2_CID_EXPOSURE control + * limits and the line duration. + */ + double lineDuration = sensorInfo.minLineLength / sensorInfo.pixelRate; + + const ControlInfo &v4l2Exposure = sensorControls.find(V4L2_CID_EXPOSURE)->second; + int32_t minExposure = v4l2Exposure.min().get<int32_t>() * lineDuration; + int32_t maxExposure = v4l2Exposure.max().get<int32_t>() * lineDuration; + int32_t defExposure = v4l2Exposure.def().get<int32_t>() * lineDuration; + ctrlMap[&controls::ExposureTime] = ControlInfo(minExposure, maxExposure, defExposure); + + /* Compute the analogue gain limits. */ + const ControlInfo &v4l2Gain = sensorControls.find(V4L2_CID_ANALOGUE_GAIN)->second; + float minGain = camHelper_->gain(v4l2Gain.min().get<int32_t>()); + float maxGain = camHelper_->gain(v4l2Gain.max().get<int32_t>()); + float defGain = camHelper_->gain(v4l2Gain.def().get<int32_t>()); + ctrlMap[&controls::AnalogueGain] = ControlInfo(minGain, maxGain, defGain); + + /* + * Merge in any controls that we support either statically or from the + * algorithms. + */ + ctrlMap.merge(context_.ctrlMap); + + *ipaControls = ControlInfoMap(std::move(ctrlMap), controls::controls); +} + +int IPAMaliC55::configure(const IPAConfigInfo &ipaConfig, uint8_t bayerOrder, + ControlInfoMap *ipaControls) +{ + sensorControls_ = ipaConfig.sensorControls; + + /* Clear the IPA context before the streaming session. */ + context_.configuration = {}; + context_.activeState = {}; + context_.frameContexts.clear(); + + const IPACameraSensorInfo &info = ipaConfig.sensorInfo; + + updateSessionConfiguration(info, ipaConfig.sensorControls, + static_cast<BayerFormat::Order>(bayerOrder)); + updateControls(info, ipaConfig.sensorControls, ipaControls); + + for (auto const &a : algorithms()) { + Algorithm *algo = static_cast<Algorithm *>(a.get()); + + int ret = algo->configure(context_, info); + if (ret) + return ret; + } + + return 0; +} + +void IPAMaliC55::mapBuffers(const std::vector<IPABuffer> &buffers, bool readOnly) +{ + for (const IPABuffer &buffer : buffers) { + const FrameBuffer fb(buffer.planes); + buffers_.emplace( + buffer.id, + MappedFrameBuffer( + &fb, + readOnly ? MappedFrameBuffer::MapFlag::Read + : MappedFrameBuffer::MapFlag::ReadWrite)); + } +} + +void IPAMaliC55::unmapBuffers(const std::vector<IPABuffer> &buffers) +{ + for (const IPABuffer &buffer : buffers) { + auto it = buffers_.find(buffer.id); + if (it == buffers_.end()) + continue; + + buffers_.erase(buffer.id); + } +} + +void IPAMaliC55::queueRequest(const uint32_t request, const ControlList &controls) +{ + IPAFrameContext &frameContext = context_.frameContexts.alloc(request); + + for (auto const &a : algorithms()) { + Algorithm *algo = static_cast<Algorithm *>(a.get()); + + algo->queueRequest(context_, request, frameContext, controls); + } +} + +void IPAMaliC55::fillParams(unsigned int request, + [[maybe_unused]] uint32_t bufferId) +{ + struct mali_c55_params_buffer *params; + IPAFrameContext &frameContext = context_.frameContexts.get(request); + + params = reinterpret_cast<mali_c55_params_buffer *>( + buffers_.at(bufferId).planes()[0].data()); + memset(params, 0, sizeof(mali_c55_params_buffer)); + + params->version = MALI_C55_PARAM_BUFFER_V1; + + for (auto const &algo : algorithms()) { + algo->prepare(context_, request, frameContext, params); + + ASSERT(params->total_size <= MALI_C55_PARAMS_MAX_SIZE); + } + + paramsComputed.emit(request); +} + +void IPAMaliC55::processStats(unsigned int request, unsigned int bufferId, + const ControlList &sensorControls) +{ + IPAFrameContext &frameContext = context_.frameContexts.get(request); + const mali_c55_stats_buffer *stats = nullptr; + + stats = reinterpret_cast<mali_c55_stats_buffer *>( + buffers_.at(bufferId).planes()[0].data()); + + frameContext.agc.exposure = + sensorControls.get(V4L2_CID_EXPOSURE).get<int32_t>(); + frameContext.agc.sensorGain = + camHelper_->gain(sensorControls.get(V4L2_CID_ANALOGUE_GAIN).get<int32_t>()); + + ControlList metadata(controls::controls); + + for (auto const &a : algorithms()) { + Algorithm *algo = static_cast<Algorithm *>(a.get()); + + algo->process(context_, request, frameContext, stats, metadata); + } + + setControls(); + + statsProcessed.emit(request, metadata); +} + +} /* namespace ipa::mali_c55 */ + +/* + * External IPA module interface + */ +extern "C" { +const struct IPAModuleInfo ipaModuleInfo = { + IPA_MODULE_API_VERSION, + 1, + "mali-c55", + "mali-c55", +}; + +IPAInterface *ipaCreate() +{ + return new ipa::mali_c55::IPAMaliC55(); +} + +} /* extern "C" */ + +} /* namespace libcamera */ diff --git a/src/ipa/mali-c55/meson.build b/src/ipa/mali-c55/meson.build new file mode 100644 index 00000000..864d90ec --- /dev/null +++ b/src/ipa/mali-c55/meson.build @@ -0,0 +1,33 @@ +# SPDX-License-Identifier: CC0-1.0 + +subdir('algorithms') +subdir('data') + +ipa_name = 'ipa_mali_c55' + +mali_c55_ipa_sources = files([ + 'ipa_context.cpp', + 'mali-c55.cpp' +]) + +mali_c55_ipa_sources += mali_c55_ipa_algorithms + +mod = shared_module(ipa_name, + mali_c55_ipa_sources, + name_prefix : '', + include_directories : [ipa_includes, libipa_includes], + dependencies : libcamera_private, + link_with : libipa, + install : true, + install_dir : ipa_install_dir) + +if ipa_sign_module + custom_target(ipa_name + '.so.sign', + input : mod, + output : ipa_name + '.so.sign', + command : [ipa_sign, ipa_priv_key, '@INPUT@', '@OUTPUT@'], + install : false, + build_by_default : true) +endif + +ipa_names += ipa_name diff --git a/src/ipa/mali-c55/module.h b/src/ipa/mali-c55/module.h new file mode 100644 index 00000000..1d85ec1f --- /dev/null +++ b/src/ipa/mali-c55/module.h @@ -0,0 +1,27 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +/* + * Copyright (C) 2024, Ideas On Board + * + * module.h - Mali-C55 IPA Module + */ + +#pragma once + +#include <linux/mali-c55-config.h> + +#include <libcamera/ipa/mali-c55_ipa_interface.h> + +#include <libipa/module.h> + +#include "ipa_context.h" + +namespace libcamera { + +namespace ipa::mali_c55 { + +using Module = ipa::Module<IPAContext, IPAFrameContext, IPACameraSensorInfo, + mali_c55_params_buffer, mali_c55_stats_buffer>; + +} /* namespace ipa::mali_c55 */ + +} /* namespace libcamera*/ diff --git a/src/ipa/rkisp1/algorithms/agc.cpp b/src/ipa/rkisp1/algorithms/agc.cpp index 40e5a8f4..5a3ba013 100644 --- a/src/ipa/rkisp1/algorithms/agc.cpp +++ b/src/ipa/rkisp1/algorithms/agc.cpp @@ -148,7 +148,16 @@ int Agc::init(IPAContext &context, const YamlObject &tuningData) if (ret) return ret; - context.ctrlMap[&controls::AeEnable] = ControlInfo(false, true); + context.ctrlMap[&controls::ExposureTimeMode] = + ControlInfo({ { ControlValue(controls::ExposureTimeModeAuto), + ControlValue(controls::ExposureTimeModeManual) } }, + ControlValue(controls::ExposureTimeModeAuto)); + context.ctrlMap[&controls::AnalogueGainMode] = + ControlInfo({ { ControlValue(controls::AnalogueGainModeAuto), + ControlValue(controls::AnalogueGainModeManual) } }, + ControlValue(controls::AnalogueGainModeAuto)); + /* \todo Move this to the Camera class */ + context.ctrlMap[&controls::AeEnable] = ControlInfo(false, true, true); context.ctrlMap.merge(controls()); return 0; @@ -169,7 +178,8 @@ int Agc::configure(IPAContext &context, const IPACameraSensorInfo &configInfo) 10ms / context.configuration.sensor.lineDuration; context.activeState.agc.manual.gain = context.activeState.agc.automatic.gain; context.activeState.agc.manual.exposure = context.activeState.agc.automatic.exposure; - context.activeState.agc.autoEnabled = !context.configuration.raw; + context.activeState.agc.autoExposureEnabled = !context.configuration.raw; + context.activeState.agc.autoGainEnabled = !context.configuration.raw; context.activeState.agc.constraintMode = static_cast<controls::AeConstraintModeEnum>(constraintModes().begin()->first); @@ -178,12 +188,10 @@ int Agc::configure(IPAContext &context, const IPACameraSensorInfo &configInfo) 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.maxExposureTime; + /* Limit the frame duration to match current initialisation */ + ControlInfo &frameDurationLimits = context.ctrlMap[&controls::FrameDurationLimits]; + context.activeState.agc.minFrameDuration = std::chrono::microseconds(frameDurationLimits.min().get<int64_t>()); + context.activeState.agc.maxFrameDuration = std::chrono::microseconds(frameDurationLimits.max().get<int64_t>()); /* * Define the measurement window for AGC as a centered rectangle @@ -215,18 +223,47 @@ void Agc::queueRequest(IPAContext &context, auto &agc = context.activeState.agc; if (!context.configuration.raw) { - const auto &agcEnable = controls.get(controls::AeEnable); - if (agcEnable && *agcEnable != agc.autoEnabled) { - agc.autoEnabled = *agcEnable; + const auto &aeEnable = controls.get(controls::ExposureTimeMode); + if (aeEnable && + (*aeEnable == controls::ExposureTimeModeAuto) != agc.autoExposureEnabled) { + agc.autoExposureEnabled = (*aeEnable == controls::ExposureTimeModeAuto); + + LOG(RkISP1Agc, Debug) + << (agc.autoExposureEnabled ? "Enabling" : "Disabling") + << " AGC (exposure)"; + + /* + * If we go from auto -> manual with no manual control + * set, use the last computed value, which we don't + * know until prepare() so save this information. + * + * \todo Check the previous frame at prepare() time + * instead of saving a flag here + */ + if (!agc.autoExposureEnabled && !controls.get(controls::ExposureTime)) + frameContext.agc.autoExposureModeChange = true; + } + + const auto &agEnable = controls.get(controls::AnalogueGainMode); + if (agEnable && + (*agEnable == controls::AnalogueGainModeAuto) != agc.autoGainEnabled) { + agc.autoGainEnabled = (*agEnable == controls::AnalogueGainModeAuto); LOG(RkISP1Agc, Debug) - << (agc.autoEnabled ? "Enabling" : "Disabling") - << " AGC"; + << (agc.autoGainEnabled ? "Enabling" : "Disabling") + << " AGC (gain)"; + /* + * If we go from auto -> manual with no manual control + * set, use the last computed value, which we don't + * know until prepare() so save this information. + */ + if (!agc.autoGainEnabled && !controls.get(controls::AnalogueGain)) + frameContext.agc.autoGainModeChange = true; } } const auto &exposure = controls.get(controls::ExposureTime); - if (exposure && !agc.autoEnabled) { + if (exposure && !agc.autoExposureEnabled) { agc.manual.exposure = *exposure * 1.0us / context.configuration.sensor.lineDuration; @@ -235,18 +272,19 @@ void Agc::queueRequest(IPAContext &context, } const auto &gain = controls.get(controls::AnalogueGain); - if (gain && !agc.autoEnabled) { + if (gain && !agc.autoGainEnabled) { agc.manual.gain = *gain; LOG(RkISP1Agc, Debug) << "Set gain to " << agc.manual.gain; } - frameContext.agc.autoEnabled = agc.autoEnabled; + frameContext.agc.autoExposureEnabled = agc.autoExposureEnabled; + frameContext.agc.autoGainEnabled = agc.autoGainEnabled; - if (!frameContext.agc.autoEnabled) { + if (!frameContext.agc.autoExposureEnabled) frameContext.agc.exposure = agc.manual.exposure; + if (!frameContext.agc.autoGainEnabled) frameContext.agc.gain = agc.manual.gain; - } const auto &meteringMode = controls.get(controls::AeMeteringMode); if (meteringMode) { @@ -270,10 +308,21 @@ void Agc::queueRequest(IPAContext &context, const auto &frameDurationLimits = controls.get(controls::FrameDurationLimits); if (frameDurationLimits) { - utils::Duration maxFrameDuration = - std::chrono::milliseconds((*frameDurationLimits).back()); - agc.maxFrameDuration = maxFrameDuration; + /* Limit the control value to the limits in ControlInfo */ + ControlInfo &limits = context.ctrlMap[&controls::FrameDurationLimits]; + int64_t minFrameDuration = + std::clamp((*frameDurationLimits).front(), + limits.min().get<int64_t>(), + limits.max().get<int64_t>()); + int64_t maxFrameDuration = + std::clamp((*frameDurationLimits).back(), + limits.min().get<int64_t>(), + limits.max().get<int64_t>()); + + agc.minFrameDuration = std::chrono::microseconds(minFrameDuration); + agc.maxFrameDuration = std::chrono::microseconds(maxFrameDuration); } + frameContext.agc.minFrameDuration = agc.minFrameDuration; frameContext.agc.maxFrameDuration = agc.maxFrameDuration; } @@ -283,9 +332,26 @@ void Agc::queueRequest(IPAContext &context, void Agc::prepare(IPAContext &context, const uint32_t frame, IPAFrameContext &frameContext, RkISP1Params *params) { - if (frameContext.agc.autoEnabled) { - frameContext.agc.exposure = context.activeState.agc.automatic.exposure; - frameContext.agc.gain = context.activeState.agc.automatic.gain; + uint32_t activeAutoExposure = context.activeState.agc.automatic.exposure; + double activeAutoGain = context.activeState.agc.automatic.gain; + + /* Populate exposure and gain in auto mode */ + if (frameContext.agc.autoExposureEnabled) + frameContext.agc.exposure = activeAutoExposure; + if (frameContext.agc.autoGainEnabled) + frameContext.agc.gain = activeAutoGain; + + /* + * Populate manual exposure and gain from the active auto values when + * transitioning from auto to manual + */ + if (!frameContext.agc.autoExposureEnabled && frameContext.agc.autoExposureModeChange) { + context.activeState.agc.manual.exposure = activeAutoExposure; + frameContext.agc.exposure = activeAutoExposure; + } + if (!frameContext.agc.autoGainEnabled && frameContext.agc.autoGainModeChange) { + context.activeState.agc.manual.gain = activeAutoGain; + frameContext.agc.gain = activeAutoGain; } if (frame > 0 && !frameContext.agc.updateMetering) @@ -333,14 +399,15 @@ 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 - + context.configuration.sensor.defVBlank; - utils::Duration frameDuration = context.configuration.sensor.lineDuration - * vTotal; - metadata.set(controls::FrameDuration, frameDuration.get<std::micro>()); + metadata.set(controls::FrameDuration, frameContext.agc.frameDuration.get<std::micro>()); + metadata.set(controls::ExposureTimeMode, + frameContext.agc.autoExposureEnabled + ? controls::ExposureTimeModeAuto + : controls::ExposureTimeModeManual); + metadata.set(controls::AnalogueGainMode, + frameContext.agc.autoGainEnabled + ? controls::AnalogueGainModeAuto + : controls::AnalogueGainModeManual); metadata.set(controls::AeMeteringMode, frameContext.agc.meteringMode); metadata.set(controls::AeExposureMode, frameContext.agc.exposureMode); @@ -384,6 +451,27 @@ double Agc::estimateLuminance(double gain) const } /** + * \brief Process frame duration and compute vblank + * \param[in] context The shared IPA context + * \param[in] frameContext The current frame context + * \param[in] frameDuration The target frame duration + * + * Compute and populate vblank from the target frame duration. + */ +void Agc::processFrameDuration(IPAContext &context, + IPAFrameContext &frameContext, + utils::Duration frameDuration) +{ + IPACameraSensorInfo &sensorInfo = context.sensorInfo; + utils::Duration lineDuration = context.configuration.sensor.lineDuration; + + frameContext.agc.vblank = (frameDuration / lineDuration) - sensorInfo.outputSize.height; + + /* Update frame duration accounting for line length quantization. */ + frameContext.agc.frameDuration = (sensorInfo.outputSize.height + frameContext.agc.vblank) * lineDuration; +} + +/** * \brief Process RkISP1 statistics, and run AGC operations * \param[in] context The shared IPA context * \param[in] frame The frame context sequence number @@ -399,16 +487,20 @@ void Agc::process(IPAContext &context, [[maybe_unused]] const uint32_t frame, ControlList &metadata) { if (!stats) { + processFrameDuration(context, frameContext, + frameContext.agc.minFrameDuration); fillMetadata(context, frameContext, metadata); return; } - + if (!(stats->meas_type & RKISP1_CIF_ISP_STAT_AUTOEXP)) { fillMetadata(context, frameContext, metadata); LOG(RkISP1Agc, Error) << "AUTOEXP data is missing in statistics"; return; } + const utils::Duration &lineDuration = context.configuration.sensor.lineDuration; + /* * \todo Verify that the exposure and gain applied by the sensor for * this frame match what has been requested. This isn't a hard @@ -424,21 +516,41 @@ 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 maxExposureTime = - std::clamp(frameContext.agc.maxFrameDuration, - context.configuration.sensor.minExposureTime, - context.configuration.sensor.maxExposureTime); - setLimits(context.configuration.sensor.minExposureTime, - maxExposureTime, - context.configuration.sensor.minAnalogueGain, - context.configuration.sensor.maxAnalogueGain); + /* + * Set the AGC limits using the fixed exposure time and/or gain in + * manual mode, or the sensor limits in auto mode. + */ + utils::Duration minExposureTime; + utils::Duration maxExposureTime; + double minAnalogueGain; + double maxAnalogueGain; + + if (frameContext.agc.autoExposureEnabled) { + minExposureTime = context.configuration.sensor.minExposureTime; + maxExposureTime = std::clamp(frameContext.agc.maxFrameDuration, + context.configuration.sensor.minExposureTime, + context.configuration.sensor.maxExposureTime); + } else { + minExposureTime = context.configuration.sensor.lineDuration + * frameContext.agc.exposure; + maxExposureTime = minExposureTime; + } + + if (frameContext.agc.autoGainEnabled) { + minAnalogueGain = context.configuration.sensor.minAnalogueGain; + maxAnalogueGain = context.configuration.sensor.maxAnalogueGain; + } else { + minAnalogueGain = frameContext.agc.gain; + maxAnalogueGain = frameContext.agc.gain; + } + + setLimits(minExposureTime, maxExposureTime, minAnalogueGain, maxAnalogueGain); /* * The Agc algorithm needs to know the effective exposure value that was * applied to the sensor when the statistics were collected. */ - utils::Duration exposureTime = context.configuration.sensor.lineDuration - * frameContext.sensor.exposure; + utils::Duration exposureTime = lineDuration * frameContext.sensor.exposure; double analogueGain = frameContext.sensor.gain; utils::Duration effectiveExposureValue = exposureTime * analogueGain; @@ -455,10 +567,16 @@ void Agc::process(IPAContext &context, [[maybe_unused]] const uint32_t frame, IPAActiveState &activeState = context.activeState; /* Update the estimated exposure and gain. */ - activeState.agc.automatic.exposure = newExposureTime - / context.configuration.sensor.lineDuration; + activeState.agc.automatic.exposure = newExposureTime / lineDuration; activeState.agc.automatic.gain = aGain; + /* + * Expand the target frame duration so that we do not run faster than + * the minimum frame duration when we have short exposures. + */ + processFrameDuration(context, frameContext, + std::max(frameContext.agc.minFrameDuration, newExposureTime)); + fillMetadata(context, frameContext, metadata); expMeans_ = {}; } diff --git a/src/ipa/rkisp1/algorithms/agc.h b/src/ipa/rkisp1/algorithms/agc.h index aa86f2c5..62bcde99 100644 --- a/src/ipa/rkisp1/algorithms/agc.h +++ b/src/ipa/rkisp1/algorithms/agc.h @@ -50,6 +50,9 @@ private: void fillMetadata(IPAContext &context, IPAFrameContext &frameContext, ControlList &metadata); double estimateLuminance(double gain) const override; + void processFrameDuration(IPAContext &context, + IPAFrameContext &frameContext, + utils::Duration frameDuration); Span<const uint8_t> expMeans_; diff --git a/src/ipa/rkisp1/algorithms/awb.cpp b/src/ipa/rkisp1/algorithms/awb.cpp index 4bb4f5b8..eafe9308 100644 --- a/src/ipa/rkisp1/algorithms/awb.cpp +++ b/src/ipa/rkisp1/algorithms/awb.cpp @@ -16,6 +16,8 @@ #include <libcamera/ipa/core_ipa_interface.h> +#include "libipa/awb_bayes.h" +#include "libipa/awb_grey.h" #include "libipa/colours.h" /** @@ -33,23 +35,100 @@ namespace ipa::rkisp1::algorithms { LOG_DEFINE_CATEGORY(RkISP1Awb) +constexpr int32_t kMinColourTemperature = 2500; +constexpr int32_t kMaxColourTemperature = 10000; +constexpr int32_t kDefaultColourTemperature = 5000; + /* Minimum mean value below which AWB can't operate. */ constexpr double kMeanMinThreshold = 2.0; +class RkISP1AwbStats final : public AwbStats +{ +public: + RkISP1AwbStats(const RGB<double> &rgbMeans) + : rgbMeans_(rgbMeans) + { + rg_ = rgbMeans_.r() / rgbMeans_.g(); + bg_ = rgbMeans_.b() / rgbMeans_.g(); + } + + double computeColourError(const RGB<double> &gains) const override + { + /* + * Compute the sum of the squared colour error (non-greyness) as + * it appears in the log likelihood equation. + */ + double deltaR = gains.r() * rg_ - 1.0; + double deltaB = gains.b() * bg_ - 1.0; + double delta2 = deltaR * deltaR + deltaB * deltaB; + + return delta2; + } + + RGB<double> rgbMeans() const override + { + return rgbMeans_; + } + +private: + RGB<double> rgbMeans_; + double rg_; + double bg_; +}; + Awb::Awb() : rgbMode_(false) { } /** + * \copydoc libcamera::ipa::Algorithm::init + */ +int Awb::init(IPAContext &context, const YamlObject &tuningData) +{ + auto &cmap = context.ctrlMap; + cmap[&controls::ColourTemperature] = ControlInfo(kMinColourTemperature, + kMaxColourTemperature, + kDefaultColourTemperature); + + if (!tuningData.contains("algorithm")) + LOG(RkISP1Awb, Info) << "No AWB algorithm specified." + << " Default to grey world"; + + auto mode = tuningData["algorithm"].get<std::string>("grey"); + if (mode == "grey") { + awbAlgo_ = std::make_unique<AwbGrey>(); + } else if (mode == "bayes") { + awbAlgo_ = std::make_unique<AwbBayes>(); + } else { + LOG(RkISP1Awb, Error) << "Unknown AWB algorithm: " << mode; + return -EINVAL; + } + LOG(RkISP1Awb, Debug) << "Using AWB algorithm: " << mode; + + int ret = awbAlgo_->init(tuningData); + if (ret) { + LOG(RkISP1Awb, Error) << "Failed to init AWB algorithm"; + return ret; + } + + const auto &src = awbAlgo_->controls(); + cmap.insert(src.begin(), src.end()); + + return 0; +} + +/** * \copydoc libcamera::ipa::Algorithm::configure */ int Awb::configure(IPAContext &context, const IPACameraSensorInfo &configInfo) { context.activeState.awb.gains.manual = RGB<double>{ 1.0 }; - context.activeState.awb.gains.automatic = RGB<double>{ 1.0 }; + context.activeState.awb.gains.automatic = + awbAlgo_->gainsFromColourTemperature(kDefaultColourTemperature); context.activeState.awb.autoEnabled = true; + context.activeState.awb.temperatureK = kDefaultColourTemperature; /* * Define the measurement window for AWB as a centered rectangle @@ -83,19 +162,39 @@ void Awb::queueRequest(IPAContext &context, << (*awbEnable ? "Enabling" : "Disabling") << " AWB"; } + awbAlgo_->handleControls(controls); + + frameContext.awb.autoEnabled = awb.autoEnabled; + + if (awb.autoEnabled) + return; + const auto &colourGains = controls.get(controls::ColourGains); - if (colourGains && !awb.autoEnabled) { + const auto &colourTemperature = controls.get(controls::ColourTemperature); + bool update = false; + if (colourGains) { awb.gains.manual.r() = (*colourGains)[0]; awb.gains.manual.b() = (*colourGains)[1]; + /* + * \todo Colour temperature reported in metadata is now + * incorrect, as we can't deduce the temperature from the gains. + * This will be fixed with the bayes AWB algorithm. + */ + update = true; + } else if (colourTemperature) { + const auto &gains = awbAlgo_->gainsFromColourTemperature(*colourTemperature); + awb.gains.manual.r() = gains.r(); + awb.gains.manual.b() = gains.b(); + awb.temperatureK = *colourTemperature; + update = true; + } + if (update) LOG(RkISP1Awb, Debug) << "Set colour gains to " << awb.gains.manual; - } - - frameContext.awb.autoEnabled = awb.autoEnabled; - if (!awb.autoEnabled) - frameContext.awb.gains = awb.gains.manual; + frameContext.awb.gains = awb.gains.manual; + frameContext.awb.temperatureK = awb.temperatureK; } /** @@ -108,8 +207,10 @@ void Awb::prepare(IPAContext &context, const uint32_t frame, * This is the latest time we can read the active state. This is the * most up-to-date automatic values we can read. */ - if (frameContext.awb.autoEnabled) + if (frameContext.awb.autoEnabled) { frameContext.awb.gains = context.activeState.awb.gains.automatic; + frameContext.awb.temperatureK = context.activeState.awb.temperatureK; + } auto gainConfig = params->block<BlockType::AwbGain>(); gainConfig.setEnabled(true); @@ -178,27 +279,71 @@ void Awb::process(IPAContext &context, const rkisp1_stat_buffer *stats, ControlList &metadata) { - const rkisp1_cif_isp_stat *params = &stats->params; - const rkisp1_cif_isp_awb_stat *awb = ¶ms->awb; IPAActiveState &activeState = context.activeState; - RGB<double> rgbMeans; metadata.set(controls::AwbEnable, frameContext.awb.autoEnabled); metadata.set(controls::ColourGains, { static_cast<float>(frameContext.awb.gains.r()), static_cast<float>(frameContext.awb.gains.b()) }); - metadata.set(controls::ColourTemperature, activeState.awb.temperatureK); + metadata.set(controls::ColourTemperature, frameContext.awb.temperatureK); if (!stats || !(stats->meas_type & RKISP1_CIF_ISP_STAT_AWB)) { LOG(RkISP1Awb, Error) << "AWB data is missing in statistics"; return; } + const rkisp1_cif_isp_stat *params = &stats->params; + const rkisp1_cif_isp_awb_stat *awb = ¶ms->awb; + + RGB<double> rgbMeans = calculateRgbMeans(frameContext, awb); + + /* + * If the means are too small we don't have enough information to + * meaningfully calculate gains. Freeze the algorithm in that case. + */ + if (rgbMeans.r() < kMeanMinThreshold && rgbMeans.g() < kMeanMinThreshold && + rgbMeans.b() < kMeanMinThreshold) + return; + + RkISP1AwbStats awbStats{ rgbMeans }; + AwbResult awbResult = awbAlgo_->calculateAwb(awbStats, frameContext.lux.lux); + + activeState.awb.temperatureK = awbResult.colourTemperature; + + /* Metadata shall contain the up to date measurement */ + metadata.set(controls::ColourTemperature, activeState.awb.temperatureK); + + /* + * Clamp the gain values to the hardware, which expresses gains as Q2.8 + * unsigned integer values. Set the minimum just above zero to avoid + * divisions by zero when computing the raw means in subsequent + * iterations. + */ + awbResult.gains = awbResult.gains.max(1.0 / 256).min(1023.0 / 256); + + /* Filter the values to avoid oscillations. */ + double speed = 0.2; + awbResult.gains = awbResult.gains * speed + + activeState.awb.gains.automatic * (1 - speed); + + activeState.awb.gains.automatic = awbResult.gains; + + LOG(RkISP1Awb, Debug) + << std::showpoint + << "Means " << rgbMeans << ", gains " + << activeState.awb.gains.automatic << ", temp " + << activeState.awb.temperatureK << "K"; +} + +RGB<double> Awb::calculateRgbMeans(const IPAFrameContext &frameContext, const rkisp1_cif_isp_awb_stat *awb) const +{ + Vector<double, 3> rgbMeans; + if (rgbMode_) { rgbMeans = {{ - static_cast<double>(awb->awb_mean[0].mean_y_or_g), static_cast<double>(awb->awb_mean[0].mean_cr_or_r), + static_cast<double>(awb->awb_mean[0].mean_y_or_g), static_cast<double>(awb->awb_mean[0].mean_cb_or_b) }}; } else { @@ -253,49 +398,7 @@ void Awb::process(IPAContext &context, */ rgbMeans /= frameContext.awb.gains; - /* - * If the means are too small we don't have enough information to - * meaningfully calculate gains. Freeze the algorithm in that case. - */ - if (rgbMeans.r() < kMeanMinThreshold && rgbMeans.g() < kMeanMinThreshold && - rgbMeans.b() < kMeanMinThreshold) - return; - - activeState.awb.temperatureK = estimateCCT(rgbMeans); - - /* Metadata shall contain the up to date measurement */ - metadata.set(controls::ColourTemperature, activeState.awb.temperatureK); - - /* - * Estimate the red and blue gains to apply in a grey world. The green - * gain is hardcoded to 1.0. Avoid divisions by zero by clamping the - * divisor to a minimum value of 1.0. - */ - RGB<double> gains({ - rgbMeans.g() / std::max(rgbMeans.r(), 1.0), - 1.0, - rgbMeans.g() / std::max(rgbMeans.b(), 1.0) - }); - - /* - * Clamp the gain values to the hardware, which expresses gains as Q2.8 - * unsigned integer values. Set the minimum just above zero to avoid - * divisions by zero when computing the raw means in subsequent - * iterations. - */ - gains = gains.max(1.0 / 256).min(1023.0 / 256); - - /* Filter the values to avoid oscillations. */ - double speed = 0.2; - gains = gains * speed + activeState.awb.gains.automatic * (1 - speed); - - activeState.awb.gains.automatic = gains; - - LOG(RkISP1Awb, Debug) - << std::showpoint - << "Means " << rgbMeans << ", gains " - << activeState.awb.gains.automatic << ", temp " - << activeState.awb.temperatureK << "K"; + return rgbMeans; } REGISTER_IPA_ALGORITHM(Awb, "Awb") diff --git a/src/ipa/rkisp1/algorithms/awb.h b/src/ipa/rkisp1/algorithms/awb.h index 6ac3a5c3..7e6c3862 100644 --- a/src/ipa/rkisp1/algorithms/awb.h +++ b/src/ipa/rkisp1/algorithms/awb.h @@ -7,6 +7,13 @@ #pragma once +#include <optional> + +#include "libcamera/internal/vector.h" + +#include "libipa/awb.h" +#include "libipa/interpolator.h" + #include "algorithm.h" namespace libcamera { @@ -19,6 +26,7 @@ public: Awb(); ~Awb() = 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, @@ -32,6 +40,11 @@ public: ControlList &metadata) override; private: + RGB<double> calculateRgbMeans(const IPAFrameContext &frameContext, + const rkisp1_cif_isp_awb_stat *awb) const; + + std::unique_ptr<AwbAlgorithm> awbAlgo_; + bool rgbMode_; }; diff --git a/src/ipa/rkisp1/algorithms/ccm.cpp b/src/ipa/rkisp1/algorithms/ccm.cpp index 6b7d2e2c..eb8ca39e 100644 --- a/src/ipa/rkisp1/algorithms/ccm.cpp +++ b/src/ipa/rkisp1/algorithms/ccm.cpp @@ -18,7 +18,7 @@ #include "libcamera/internal/yaml_parser.h" -#include "../utils.h" +#include "libipa/fixedpoint.h" #include "libipa/interpolator.h" /** @@ -72,7 +72,7 @@ void Ccm::setParameters(struct rkisp1_cif_isp_ctk_config &config, 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]); + floatingToFixedPoint<4, 7, uint16_t, double>(matrix[i][j]); } for (unsigned int i = 0; i < 3; i++) @@ -120,12 +120,7 @@ void Ccm::process([[maybe_unused]] IPAContext &context, [[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 * 3 + j] = frameContext.ccm.ccm[i][j]; - } - metadata.set(controls::ColourCorrectionMatrix, m); + metadata.set(controls::ColourCorrectionMatrix, frameContext.ccm.ccm.data()); } REGISTER_IPA_ALGORITHM(Ccm, "Ccm") diff --git a/src/ipa/rkisp1/algorithms/ccm.h b/src/ipa/rkisp1/algorithms/ccm.h index 46a1416e..a5d9a9a4 100644 --- a/src/ipa/rkisp1/algorithms/ccm.h +++ b/src/ipa/rkisp1/algorithms/ccm.h @@ -9,8 +9,9 @@ #include <linux/rkisp1-config.h> +#include "libcamera/internal/matrix.h" + #include "libipa/interpolator.h" -#include "libipa/matrix.h" #include "algorithm.h" diff --git a/src/ipa/rkisp1/algorithms/lux.cpp b/src/ipa/rkisp1/algorithms/lux.cpp new file mode 100644 index 00000000..a467767e --- /dev/null +++ b/src/ipa/rkisp1/algorithms/lux.cpp @@ -0,0 +1,76 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +/* + * Copyright (C) 2024, Ideas On Board + * + * lux.cpp - RkISP1 Lux control + */ + +#include "lux.h" + +#include <libcamera/base/log.h> + +#include <libcamera/control_ids.h> + +#include "libipa/histogram.h" +#include "libipa/lux.h" + +/** + * \file lux.h + */ + +namespace libcamera { + +namespace ipa::rkisp1::algorithms { + +/** + * \class Lux + * \brief RkISP1 Lux control + * + * The Lux algorithm is responsible for estimating the lux level of the image. + * It doesn't take or generate any controls, but it provides a lux level for + * other algorithms (such as AGC) to use. + */ + +/** + * \brief Construct an rkisp1 Lux algo module + */ +Lux::Lux() +{ +} + +/** + * \copydoc libcamera::ipa::Algorithm::init + */ +int Lux::init([[maybe_unused]] IPAContext &context, const YamlObject &tuningData) +{ + return lux_.parseTuningData(tuningData); +} + +/** + * \copydoc libcamera::ipa::Algorithm::process + */ +void Lux::process(IPAContext &context, + [[maybe_unused]] const uint32_t frame, + IPAFrameContext &frameContext, + const rkisp1_stat_buffer *stats, + ControlList &metadata) +{ + utils::Duration exposureTime = context.configuration.sensor.lineDuration + * frameContext.sensor.exposure; + double gain = frameContext.sensor.gain; + + /* \todo Deduplicate the histogram calculation from AGC */ + const rkisp1_cif_isp_stat *params = &stats->params; + Histogram yHist({ params->hist.hist_bins, context.hw->numHistogramBins }, + [](uint32_t x) { return x >> 4; }); + + double lux = lux_.estimateLux(exposureTime, gain, 1.0, yHist); + frameContext.lux.lux = lux; + metadata.set(controls::Lux, lux); +} + +REGISTER_IPA_ALGORITHM(Lux, "Lux") + +} /* namespace ipa::rkisp1::algorithms */ + +} /* namespace libcamera */ diff --git a/src/ipa/rkisp1/algorithms/lux.h b/src/ipa/rkisp1/algorithms/lux.h new file mode 100644 index 00000000..8a90de55 --- /dev/null +++ b/src/ipa/rkisp1/algorithms/lux.h @@ -0,0 +1,36 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +/* + * Copyright (C) 2024, Ideas On Board + * + * lux.h - RkISP1 Lux control + */ + +#pragma once + +#include <sys/types.h> + +#include "libipa/lux.h" + +#include "algorithm.h" + +namespace libcamera { + +namespace ipa::rkisp1::algorithms { + +class Lux : public Algorithm +{ +public: + Lux(); + + int init(IPAContext &context, const YamlObject &tuningData) override; + void process(IPAContext &context, const uint32_t frame, + IPAFrameContext &frameContext, + const rkisp1_stat_buffer *stats, + ControlList &metadata) override; + +private: + ipa::Lux lux_; +}; + +} /* namespace ipa::rkisp1::algorithms */ +} /* namespace libcamera */ diff --git a/src/ipa/rkisp1/algorithms/meson.build b/src/ipa/rkisp1/algorithms/meson.build index 1734a667..c66b0b70 100644 --- a/src/ipa/rkisp1/algorithms/meson.build +++ b/src/ipa/rkisp1/algorithms/meson.build @@ -12,4 +12,5 @@ rkisp1_ipa_algorithms = files([ 'goc.cpp', 'gsl.cpp', 'lsc.cpp', + 'lux.cpp', ]) diff --git a/src/ipa/rkisp1/ipa_context.cpp b/src/ipa/rkisp1/ipa_context.cpp index 80b99df8..99611bd5 100644 --- a/src/ipa/rkisp1/ipa_context.cpp +++ b/src/ipa/rkisp1/ipa_context.cpp @@ -165,8 +165,11 @@ namespace libcamera::ipa::rkisp1 { * \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.autoExposureEnabled + * \brief Manual/automatic AGC state (exposure) as set by the ExposureTimeMode control + * + * \var IPAActiveState::agc.autoGainEnabled + * \brief Manual/automatic AGC state (gain) as set by the AnalogueGainMode control * * \var IPAActiveState::agc.constraintMode * \brief Constraint mode as set by the AeConstraintMode control @@ -177,6 +180,9 @@ namespace libcamera::ipa::rkisp1 { * \var IPAActiveState::agc.meteringMode * \brief Metering mode as set by the AeMeteringMode control * + * \var IPAActiveState::agc.minFrameDuration + * \brief Minimum frame duration as set by the FrameDurationLimits control + * * \var IPAActiveState::agc.maxFrameDuration * \brief Maximum frame duration as set by the FrameDurationLimits control */ @@ -279,7 +285,9 @@ namespace libcamera::ipa::rkisp1 { * \brief Automatic Gain Control parameters for this frame * * The exposure and gain are provided by the AGC algorithm, and are to be - * applied to the sensor in order to take effect for this frame. + * applied to the sensor in order to take effect for this frame. Additionally + * the vertical blanking period is determined to maintain a consistent frame + * rate matched to the FrameDurationLimits as set by the user. * * \var IPAFrameContext::agc.exposure * \brief Exposure time expressed as a number of lines computed by the algorithm @@ -289,8 +297,14 @@ namespace libcamera::ipa::rkisp1 { * * 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.vblank + * \brief Vertical blanking parameter computed by the algorithm + * + * \var IPAFrameContext::agc.autoExposureEnabled + * \brief Manual/automatic AGC state (exposure) as set by the ExposureTimeMode control + * + * \var IPAFrameContext::agc.autoGainEnabled + * \brief Manual/automatic AGC state (gain) as set by the AnalogueGainMode control * * \var IPAFrameContext::agc.constraintMode * \brief Constraint mode as set by the AeConstraintMode control @@ -301,11 +315,27 @@ namespace libcamera::ipa::rkisp1 { * \var IPAFrameContext::agc.meteringMode * \brief Metering mode as set by the AeMeteringMode control * + * \var IPAFrameContext::agc.minFrameDuration + * \brief Minimum frame duration as set by the FrameDurationLimits control + * * \var IPAFrameContext::agc.maxFrameDuration * \brief Maximum frame duration as set by the FrameDurationLimits control * + * \var IPAFrameContext::agc.frameDuration + * \brief The actual FrameDuration used by the algorithm for the frame + * * \var IPAFrameContext::agc.updateMetering * \brief Indicate if new ISP AGC metering parameters need to be applied + * + * \var IPAFrameContext::agc.autoExposureModeChange + * \brief Indicate if autoExposureEnabled has changed from true in the previous + * frame to false in the current frame, and no manual exposure value has been + * supplied in the current frame. + * + * \var IPAFrameContext::agc.autoGainModeChange + * \brief Indicate if autoGainEnabled has changed from true in the previous + * frame to false in the current frame, and no manual gain value has been + * supplied in the current frame. */ /** diff --git a/src/ipa/rkisp1/ipa_context.h b/src/ipa/rkisp1/ipa_context.h index 0f8ab713..474f7036 100644 --- a/src/ipa/rkisp1/ipa_context.h +++ b/src/ipa/rkisp1/ipa_context.h @@ -21,11 +21,11 @@ #include <libcamera/ipa/core_ipa_interface.h> #include "libcamera/internal/debug_controls.h" +#include "libcamera/internal/matrix.h" +#include "libcamera/internal/vector.h" #include <libipa/camera_sensor_helper.h> #include <libipa/fc_queue.h> -#include <libipa/matrix.h> -#include <libipa/vector.h> namespace libcamera { @@ -79,10 +79,12 @@ struct IPAActiveState { double gain; } automatic; - bool autoEnabled; + bool autoExposureEnabled; + bool autoGainEnabled; controls::AeConstraintModeEnum constraintMode; controls::AeExposureModeEnum exposureMode; controls::AeMeteringModeEnum meteringMode; + utils::Duration minFrameDuration; utils::Duration maxFrameDuration; } agc; @@ -124,17 +126,24 @@ struct IPAFrameContext : public FrameContext { struct { uint32_t exposure; double gain; - bool autoEnabled; + uint32_t vblank; + bool autoExposureEnabled; + bool autoGainEnabled; controls::AeConstraintModeEnum constraintMode; controls::AeExposureModeEnum exposureMode; controls::AeMeteringModeEnum meteringMode; + utils::Duration minFrameDuration; utils::Duration maxFrameDuration; + utils::Duration frameDuration; bool updateMetering; + bool autoExposureModeChange; + bool autoGainModeChange; } agc; struct { RGB<double> gains; bool autoEnabled; + unsigned int temperatureK; } awb; struct { @@ -168,6 +177,10 @@ struct IPAFrameContext : public FrameContext { struct { Matrix<float, 3, 3> ccm; } ccm; + + struct { + double lux; + } lux; }; struct IPAContext { diff --git a/src/ipa/rkisp1/meson.build b/src/ipa/rkisp1/meson.build index 34844f14..26a9fa40 100644 --- a/src/ipa/rkisp1/meson.build +++ b/src/ipa/rkisp1/meson.build @@ -9,7 +9,6 @@ rkisp1_ipa_sources = files([ 'ipa_context.cpp', 'params.cpp', 'rkisp1.cpp', - 'utils.cpp', ]) rkisp1_ipa_sources += rkisp1_ipa_algorithms diff --git a/src/ipa/rkisp1/rkisp1.cpp b/src/ipa/rkisp1/rkisp1.cpp index 2ffdd99b..7547d2f2 100644 --- a/src/ipa/rkisp1/rkisp1.cpp +++ b/src/ipa/rkisp1/rkisp1.cpp @@ -435,9 +435,9 @@ void IPARkISP1::updateControls(const IPACameraSensorInfo &sensorInfo, frameDurations[i] = frameSize / (sensorInfo.pixelRate / 1000000U); } - ctrlMap[&controls::FrameDurationLimits] = ControlInfo(frameDurations[0], - frameDurations[1], - frameDurations[2]); + /* \todo Move this (and other agc-related controls) to agc */ + context_.ctrlMap[&controls::FrameDurationLimits] = + ControlInfo(frameDurations[0], frameDurations[1], frameDurations[2]); ctrlMap.insert(context_.ctrlMap.begin(), context_.ctrlMap.end()); *ipaControls = ControlInfoMap(std::move(ctrlMap), controls::controls); @@ -453,10 +453,12 @@ void IPARkISP1::setControls(unsigned int frame) IPAFrameContext &frameContext = context_.frameContexts.get(frame); uint32_t exposure = frameContext.agc.exposure; uint32_t gain = context_.camHelper->gainCode(frameContext.agc.gain); + uint32_t vblank = frameContext.agc.vblank; ControlList ctrls(sensorControls_); ctrls.set(V4L2_CID_EXPOSURE, static_cast<int32_t>(exposure)); ctrls.set(V4L2_CID_ANALOGUE_GAIN, static_cast<int32_t>(gain)); + ctrls.set(V4L2_CID_VBLANK, static_cast<int32_t>(vblank)); setSensorControls.emit(frame, ctrls); } diff --git a/src/ipa/rpi/cam_helper/cam_helper.cpp b/src/ipa/rpi/cam_helper/cam_helper.cpp index 6493e882..a78db9c1 100644 --- a/src/ipa/rpi/cam_helper/cam_helper.cpp +++ b/src/ipa/rpi/cam_helper/cam_helper.cpp @@ -156,17 +156,9 @@ void CamHelper::setCameraMode(const CameraMode &mode) } } -void CamHelper::getDelays(int &exposureDelay, int &gainDelay, - int &vblankDelay, int &hblankDelay) const +void CamHelper::setHwConfig(const Controller::HardwareConfig &hwConfig) { - /* - * These values are correct for many sensors. Other sensors will - * need to over-ride this function. - */ - exposureDelay = 2; - gainDelay = 1; - vblankDelay = 2; - hblankDelay = 2; + hwConfig_ = hwConfig; } bool CamHelper::sensorEmbeddedDataPresent() const diff --git a/src/ipa/rpi/cam_helper/cam_helper.h b/src/ipa/rpi/cam_helper/cam_helper.h index 4a4ab5e6..4a826690 100644 --- a/src/ipa/rpi/cam_helper/cam_helper.h +++ b/src/ipa/rpi/cam_helper/cam_helper.h @@ -36,11 +36,6 @@ namespace RPiController { * exposure time, and to convert between the sensor's gain codes and actual * gains. * - * A function to return the number of frames of delay between updating exposure, - * analogue gain and vblanking, and for the changes to take effect. For many - * sensors these take the values 2, 1 and 2 respectively, but sensors that are - * different will need to over-ride the default function provided. - * * A function to query if the sensor outputs embedded data that can be parsed. * * A function to return the sensitivity of a given camera mode. @@ -76,6 +71,7 @@ public: CamHelper(std::unique_ptr<MdParser> parser, unsigned int frameIntegrationDiff); virtual ~CamHelper(); void setCameraMode(const CameraMode &mode); + void setHwConfig(const Controller::HardwareConfig &hwConfig); virtual void prepare(libcamera::Span<const uint8_t> buffer, Metadata &metadata); virtual void process(StatisticsPtr &stats, Metadata &metadata); @@ -91,8 +87,6 @@ public: libcamera::utils::Duration lineLengthPckToDuration(uint32_t lineLengthPck) const; virtual uint32_t gainCode(double gain) const = 0; virtual double gain(uint32_t gainCode) const = 0; - virtual void getDelays(int &exposureDelay, int &gainDelay, - int &vblankDelay, int &hblankDelay) const; virtual bool sensorEmbeddedDataPresent() const; virtual double getModeSensitivity(const CameraMode &mode) const; virtual unsigned int hideFramesStartup() const; @@ -108,6 +102,7 @@ protected: std::unique_ptr<MdParser> parser_; CameraMode mode_; + Controller::HardwareConfig hwConfig_; private: /* diff --git a/src/ipa/rpi/cam_helper/cam_helper_imx283.cpp b/src/ipa/rpi/cam_helper/cam_helper_imx283.cpp index cb0be72a..efc03193 100644 --- a/src/ipa/rpi/cam_helper/cam_helper_imx283.cpp +++ b/src/ipa/rpi/cam_helper/cam_helper_imx283.cpp @@ -17,8 +17,6 @@ public: CamHelperImx283(); uint32_t gainCode(double gain) const override; double gain(uint32_t gainCode) const override; - void getDelays(int &exposureDelay, int &gainDelay, - int &vblankDelay, int &hblankDelay) const override; unsigned int hideFramesModeSwitch() const override; private: @@ -49,16 +47,6 @@ double CamHelperImx283::gain(uint32_t gainCode) const return static_cast<double>(2048.0 / (2048 - gainCode)); } -void CamHelperImx283::getDelays(int &exposureDelay, int &gainDelay, - int &vblankDelay, int &hblankDelay) const -{ - /* The driver appears to behave as follows: */ - exposureDelay = 2; - gainDelay = 2; - vblankDelay = 2; - hblankDelay = 2; -} - unsigned int CamHelperImx283::hideFramesModeSwitch() const { /* After a mode switch, we seem to get 1 bad frame. */ diff --git a/src/ipa/rpi/cam_helper/cam_helper_imx290.cpp b/src/ipa/rpi/cam_helper/cam_helper_imx290.cpp index 3b87751e..c1aa8528 100644 --- a/src/ipa/rpi/cam_helper/cam_helper_imx290.cpp +++ b/src/ipa/rpi/cam_helper/cam_helper_imx290.cpp @@ -17,8 +17,6 @@ public: CamHelperImx290(); uint32_t gainCode(double gain) const override; double gain(uint32_t gainCode) const override; - void getDelays(int &exposureDelay, int &gainDelay, - int &vblankDelay, int &hblankDelay) const override; unsigned int hideFramesStartup() const override; unsigned int hideFramesModeSwitch() const override; @@ -46,15 +44,6 @@ double CamHelperImx290::gain(uint32_t gainCode) const return std::pow(10, 0.015 * gainCode); } -void CamHelperImx290::getDelays(int &exposureDelay, int &gainDelay, - int &vblankDelay, int &hblankDelay) const -{ - exposureDelay = 2; - gainDelay = 2; - vblankDelay = 2; - hblankDelay = 2; -} - unsigned int CamHelperImx290::hideFramesStartup() const { /* On startup, we seem to get 1 bad frame. */ diff --git a/src/ipa/rpi/cam_helper/cam_helper_imx296.cpp b/src/ipa/rpi/cam_helper/cam_helper_imx296.cpp index d4a4fa79..ac7ee2ea 100644 --- a/src/ipa/rpi/cam_helper/cam_helper_imx296.cpp +++ b/src/ipa/rpi/cam_helper/cam_helper_imx296.cpp @@ -23,8 +23,6 @@ public: double gain(uint32_t gainCode) const override; uint32_t exposureLines(const Duration exposure, const Duration lineLength) const override; Duration exposure(uint32_t exposureLines, const Duration lineLength) const override; - void getDelays(int &exposureDelay, int &gainDelay, - int &vblankDelay, int &hblankDelay) const override; private: static constexpr uint32_t minExposureLines = 1; @@ -66,15 +64,6 @@ Duration CamHelperImx296::exposure(uint32_t exposureLines, return std::max<uint32_t>(minExposureLines, exposureLines) * timePerLine + 14.26us; } -void CamHelperImx296::getDelays(int &exposureDelay, int &gainDelay, - int &vblankDelay, int &hblankDelay) const -{ - exposureDelay = 2; - gainDelay = 2; - vblankDelay = 2; - hblankDelay = 2; -} - static CamHelper *create() { return new CamHelperImx296(); diff --git a/src/ipa/rpi/cam_helper/cam_helper_imx415.cpp b/src/ipa/rpi/cam_helper/cam_helper_imx415.cpp new file mode 100644 index 00000000..c0a09eee --- /dev/null +++ b/src/ipa/rpi/cam_helper/cam_helper_imx415.cpp @@ -0,0 +1,64 @@ +/* SPDX-License-Identifier: BSD-2-Clause */ +/* + * Copyright (C) 2025, Raspberry Pi Ltd + * + * camera helper for imx415 sensor + */ + +#include <cmath> + +#include "cam_helper.h" + +using namespace RPiController; + +class CamHelperImx415 : public CamHelper +{ +public: + CamHelperImx415(); + uint32_t gainCode(double gain) const override; + double gain(uint32_t gainCode) const override; + unsigned int hideFramesStartup() const override; + unsigned int hideFramesModeSwitch() const override; + +private: + /* + * Smallest difference between the frame length and integration time, + * in units of lines. + */ + static constexpr int frameIntegrationDiff = 8; +}; + +CamHelperImx415::CamHelperImx415() + : CamHelper({}, frameIntegrationDiff) +{ +} + +uint32_t CamHelperImx415::gainCode(double gain) const +{ + int code = 66.6667 * std::log10(gain); + return std::max(0, std::min(code, 0xf0)); +} + +double CamHelperImx415::gain(uint32_t gainCode) const +{ + return std::pow(10, 0.015 * gainCode); +} + +unsigned int CamHelperImx415::hideFramesStartup() const +{ + /* On startup, we seem to get 1 bad frame. */ + return 1; +} + +unsigned int CamHelperImx415::hideFramesModeSwitch() const +{ + /* After a mode switch, we seem to get 1 bad frame. */ + return 1; +} + +static CamHelper *create() +{ + return new CamHelperImx415(); +} + +static RegisterCamHelper reg("imx415", &create); diff --git a/src/ipa/rpi/cam_helper/cam_helper_imx477.cpp b/src/ipa/rpi/cam_helper/cam_helper_imx477.cpp index a53c40cd..a72ac67d 100644 --- a/src/ipa/rpi/cam_helper/cam_helper_imx477.cpp +++ b/src/ipa/rpi/cam_helper/cam_helper_imx477.cpp @@ -51,8 +51,6 @@ public: void prepare(libcamera::Span<const uint8_t> buffer, Metadata &metadata) override; std::pair<uint32_t, uint32_t> getBlanking(Duration &exposure, Duration minFrameDuration, Duration maxFrameDuration) const override; - void getDelays(int &exposureDelay, int &gainDelay, - int &vblankDelay, int &hblankDelay) const override; bool sensorEmbeddedDataPresent() const override; private: @@ -159,15 +157,6 @@ std::pair<uint32_t, uint32_t> CamHelperImx477::getBlanking(Duration &exposure, return { frameLength - mode_.height, hblank }; } -void CamHelperImx477::getDelays(int &exposureDelay, int &gainDelay, - int &vblankDelay, int &hblankDelay) const -{ - exposureDelay = 2; - gainDelay = 2; - vblankDelay = 3; - hblankDelay = 3; -} - bool CamHelperImx477::sensorEmbeddedDataPresent() const { return true; diff --git a/src/ipa/rpi/cam_helper/cam_helper_imx519.cpp b/src/ipa/rpi/cam_helper/cam_helper_imx519.cpp index 2ff08653..10cbea48 100644 --- a/src/ipa/rpi/cam_helper/cam_helper_imx519.cpp +++ b/src/ipa/rpi/cam_helper/cam_helper_imx519.cpp @@ -51,8 +51,6 @@ public: void prepare(libcamera::Span<const uint8_t> buffer, Metadata &metadata) override; std::pair<uint32_t, uint32_t> getBlanking(Duration &exposure, Duration minFrameDuration, Duration maxFrameDuration) const override; - void getDelays(int &exposureDelay, int &gainDelay, - int &vblankDelay, int &hblankDelay) const override; bool sensorEmbeddedDataPresent() const override; private: @@ -159,15 +157,6 @@ std::pair<uint32_t, uint32_t> CamHelperImx519::getBlanking(Duration &exposure, return { frameLength - mode_.height, hblank }; } -void CamHelperImx519::getDelays(int &exposureDelay, int &gainDelay, - int &vblankDelay, int &hblankDelay) const -{ - exposureDelay = 2; - gainDelay = 2; - vblankDelay = 3; - hblankDelay = 3; -} - bool CamHelperImx519::sensorEmbeddedDataPresent() const { return true; diff --git a/src/ipa/rpi/cam_helper/cam_helper_imx708.cpp b/src/ipa/rpi/cam_helper/cam_helper_imx708.cpp index ec83d9fd..6150909c 100644 --- a/src/ipa/rpi/cam_helper/cam_helper_imx708.cpp +++ b/src/ipa/rpi/cam_helper/cam_helper_imx708.cpp @@ -54,8 +54,6 @@ public: void process(StatisticsPtr &stats, Metadata &metadata) override; std::pair<uint32_t, uint32_t> getBlanking(Duration &exposure, Duration minFrameDuration, Duration maxFrameDuration) const override; - void getDelays(int &exposureDelay, int &gainDelay, - int &vblankDelay, int &hblankDelay) const override; bool sensorEmbeddedDataPresent() const override; double getModeSensitivity(const CameraMode &mode) const override; unsigned int hideFramesModeSwitch() const override; @@ -66,7 +64,7 @@ private: * Smallest difference between the frame length and integration time, * in units of lines. */ - static constexpr int frameIntegrationDiff = 22; + static constexpr int frameIntegrationDiff = 48; /* Maximum frame length allowable for long exposure calculations. */ static constexpr int frameLengthMax = 0xffdc; /* Largest long exposure scale factor given as a left shift on the frame length. */ @@ -208,15 +206,6 @@ std::pair<uint32_t, uint32_t> CamHelperImx708::getBlanking(Duration &exposure, return { frameLength - mode_.height, hblank }; } -void CamHelperImx708::getDelays(int &exposureDelay, int &gainDelay, - int &vblankDelay, int &hblankDelay) const -{ - exposureDelay = 2; - gainDelay = 2; - vblankDelay = 3; - hblankDelay = 3; -} - bool CamHelperImx708::sensorEmbeddedDataPresent() const { return true; diff --git a/src/ipa/rpi/cam_helper/cam_helper_ov5647.cpp b/src/ipa/rpi/cam_helper/cam_helper_ov5647.cpp index c30b017c..40d6b6d7 100644 --- a/src/ipa/rpi/cam_helper/cam_helper_ov5647.cpp +++ b/src/ipa/rpi/cam_helper/cam_helper_ov5647.cpp @@ -17,8 +17,6 @@ public: CamHelperOv5647(); uint32_t gainCode(double gain) const override; double gain(uint32_t gainCode) const override; - void getDelays(int &exposureDelay, int &gainDelay, - int &vblankDelay, int &hblankDelay) const override; unsigned int hideFramesStartup() const override; unsigned int hideFramesModeSwitch() const override; unsigned int mistrustFramesStartup() const override; @@ -52,19 +50,6 @@ double CamHelperOv5647::gain(uint32_t gainCode) const return static_cast<double>(gainCode) / 16.0; } -void CamHelperOv5647::getDelays(int &exposureDelay, int &gainDelay, - int &vblankDelay, int &hblankDelay) const -{ - /* - * We run this sensor in a mode where the gain delay is bumped up to - * 2. It seems to be the only way to make the delays "predictable". - */ - exposureDelay = 2; - gainDelay = 2; - vblankDelay = 2; - hblankDelay = 2; -} - unsigned int CamHelperOv5647::hideFramesStartup() const { /* diff --git a/src/ipa/rpi/cam_helper/cam_helper_ov64a40.cpp b/src/ipa/rpi/cam_helper/cam_helper_ov64a40.cpp index a8efd389..980495a8 100644 --- a/src/ipa/rpi/cam_helper/cam_helper_ov64a40.cpp +++ b/src/ipa/rpi/cam_helper/cam_helper_ov64a40.cpp @@ -18,8 +18,6 @@ public: CamHelperOv64a40(); uint32_t gainCode(double gain) const override; double gain(uint32_t gainCode) const override; - void getDelays(int &exposureDelay, int &gainDelay, - int &vblankDelay, int &hblankDelay) const override; double getModeSensitivity(const CameraMode &mode) const override; private: @@ -45,16 +43,6 @@ double CamHelperOv64a40::gain(uint32_t gainCode) const return static_cast<double>(gainCode) / 128.0; } -void CamHelperOv64a40::getDelays(int &exposureDelay, int &gainDelay, - int &vblankDelay, int &hblankDelay) const -{ - /* The driver appears to behave as follows: */ - exposureDelay = 2; - gainDelay = 2; - vblankDelay = 2; - hblankDelay = 2; -} - double CamHelperOv64a40::getModeSensitivity(const CameraMode &mode) const { if (mode.binX >= 2 && mode.scaleX >= 4) { diff --git a/src/ipa/rpi/cam_helper/cam_helper_ov7251.cpp b/src/ipa/rpi/cam_helper/cam_helper_ov7251.cpp index 7b12c445..fc7b999f 100644 --- a/src/ipa/rpi/cam_helper/cam_helper_ov7251.cpp +++ b/src/ipa/rpi/cam_helper/cam_helper_ov7251.cpp @@ -17,8 +17,6 @@ public: CamHelperOv7251(); uint32_t gainCode(double gain) const override; double gain(uint32_t gainCode) const override; - void getDelays(int &exposureDelay, int &gainDelay, - int &vblankDelay, int &hblankDelay) const override; private: /* @@ -48,16 +46,6 @@ double CamHelperOv7251::gain(uint32_t gainCode) const return static_cast<double>(gainCode) / 16.0; } -void CamHelperOv7251::getDelays(int &exposureDelay, int &gainDelay, - int &vblankDelay, int &hblankDelay) const -{ - /* The driver appears to behave as follows: */ - exposureDelay = 2; - gainDelay = 2; - vblankDelay = 2; - hblankDelay = 2; -} - static CamHelper *create() { return new CamHelperOv7251(); diff --git a/src/ipa/rpi/cam_helper/cam_helper_ov9281.cpp b/src/ipa/rpi/cam_helper/cam_helper_ov9281.cpp index a65c8ac0..e93a4691 100644 --- a/src/ipa/rpi/cam_helper/cam_helper_ov9281.cpp +++ b/src/ipa/rpi/cam_helper/cam_helper_ov9281.cpp @@ -17,15 +17,13 @@ public: CamHelperOv9281(); uint32_t gainCode(double gain) const override; double gain(uint32_t gainCode) const override; - void getDelays(int &exposureDelay, int &gainDelay, - int &vblankDelay, int &hblankDelay) const override; private: /* * Smallest difference between the frame length and integration time, * in units of lines. */ - static constexpr int frameIntegrationDiff = 4; + static constexpr int frameIntegrationDiff = 25; }; /* @@ -48,16 +46,6 @@ double CamHelperOv9281::gain(uint32_t gainCode) const return static_cast<double>(gainCode) / 16.0; } -void CamHelperOv9281::getDelays(int &exposureDelay, int &gainDelay, - int &vblankDelay, int &hblankDelay) const -{ - /* The driver appears to behave as follows: */ - exposureDelay = 2; - gainDelay = 2; - vblankDelay = 2; - hblankDelay = 2; -} - static CamHelper *create() { return new CamHelperOv9281(); diff --git a/src/ipa/rpi/cam_helper/meson.build b/src/ipa/rpi/cam_helper/meson.build index 03e88fe0..abf02147 100644 --- a/src/ipa/rpi/cam_helper/meson.build +++ b/src/ipa/rpi/cam_helper/meson.build @@ -7,6 +7,7 @@ rpi_ipa_cam_helper_sources = files([ 'cam_helper_imx283.cpp', 'cam_helper_imx290.cpp', 'cam_helper_imx296.cpp', + 'cam_helper_imx415.cpp', 'cam_helper_imx477.cpp', 'cam_helper_imx519.cpp', 'cam_helper_imx708.cpp', diff --git a/src/ipa/rpi/common/ipa_base.cpp b/src/ipa/rpi/common/ipa_base.cpp index 5fce17e6..6734c32e 100644 --- a/src/ipa/rpi/common/ipa_base.cpp +++ b/src/ipa/rpi/common/ipa_base.cpp @@ -55,9 +55,19 @@ constexpr Duration controllerMinFrameDuration = 1.0s / 30.0; /* List of controls handled by the Raspberry Pi IPA */ const ControlInfoMap::Map ipaControls{ - { &controls::AeEnable, ControlInfo(false, true) }, - { &controls::ExposureTime, ControlInfo(0, 66666) }, - { &controls::AnalogueGain, ControlInfo(1.0f, 16.0f) }, + /* \todo Move this to the Camera class */ + { &controls::AeEnable, ControlInfo(false, true, true) }, + { &controls::ExposureTimeMode, + ControlInfo(static_cast<int32_t>(controls::ExposureTimeModeAuto), + static_cast<int32_t>(controls::ExposureTimeModeManual), + static_cast<int32_t>(controls::ExposureTimeModeAuto)) }, + { &controls::ExposureTime, + ControlInfo(1, 66666, static_cast<int32_t>(defaultExposureTime.get<std::micro>())) }, + { &controls::AnalogueGainMode, + ControlInfo(static_cast<int32_t>(controls::AnalogueGainModeAuto), + static_cast<int32_t>(controls::AnalogueGainModeManual), + static_cast<int32_t>(controls::AnalogueGainModeAuto)) }, + { &controls::AnalogueGain, ControlInfo(1.0f, 16.0f, 1.0f) }, { &controls::AeMeteringMode, ControlInfo(controls::AeMeteringModeValues) }, { &controls::AeConstraintMode, ControlInfo(controls::AeConstraintModeValues) }, { &controls::AeExposureMode, ControlInfo(controls::AeExposureModeValues) }, @@ -71,7 +81,9 @@ const ControlInfoMap::Map ipaControls{ { &controls::HdrMode, ControlInfo(controls::HdrModeValues) }, { &controls::Sharpness, ControlInfo(0.0f, 16.0f, 1.0f) }, { &controls::ScalerCrop, ControlInfo(Rectangle{}, Rectangle(65535, 65535, 65535, 65535), Rectangle{}) }, - { &controls::FrameDurationLimits, ControlInfo(INT64_C(33333), INT64_C(120000)) }, + { &controls::FrameDurationLimits, + ControlInfo(INT64_C(33333), INT64_C(120000), + static_cast<int64_t>(defaultMinFrameDuration.get<std::micro>())) }, { &controls::draft::NoiseReductionMode, ControlInfo(controls::draft::NoiseReductionModeValues) }, { &controls::rpi::StatsOutputEnable, ControlInfo(false, true, false) }, }; @@ -81,6 +93,7 @@ const ControlInfoMap::Map ipaColourControls{ { &controls::AwbEnable, ControlInfo(false, true) }, { &controls::AwbMode, ControlInfo(controls::AwbModeValues) }, { &controls::ColourGains, ControlInfo(0.0f, 32.0f) }, + { &controls::ColourTemperature, ControlInfo(100, 100000) }, { &controls::Saturation, ControlInfo(0.0f, 32.0f, 1.0f) }, }; @@ -134,18 +147,8 @@ int32_t IpaBase::init(const IPASettings &settings, const InitParams ¶ms, Ini return -EINVAL; } - /* - * Pass out the sensor config to the pipeline handler in order - * to setup the staggered writer class. - */ - int gainDelay, exposureDelay, vblankDelay, hblankDelay, sensorMetadata; - helper_->getDelays(exposureDelay, gainDelay, vblankDelay, hblankDelay); - sensorMetadata = helper_->sensorEmbeddedDataPresent(); - - result->sensorConfig.gainDelay = gainDelay; - result->sensorConfig.exposureDelay = exposureDelay; - result->sensorConfig.vblankDelay = vblankDelay; - result->sensorConfig.hblankDelay = hblankDelay; + /* Pass out the sensor metadata to the pipeline handler */ + int sensorMetadata = helper_->sensorEmbeddedDataPresent(); result->sensorConfig.sensorMetadata = sensorMetadata; /* Load the tuning file for this sensor. */ @@ -160,6 +163,7 @@ int32_t IpaBase::init(const IPASettings &settings, const InitParams ¶ms, Ini lensPresent_ = params.lensPresent; controller_.initialise(); + helper_->setHwConfig(controller_.getHardwareConfig()); /* Return the controls handled by the IPA */ ControlInfoMap::Map ctrlMap = ipaControls; @@ -258,15 +262,18 @@ int32_t IpaBase::configure(const IPACameraSensorInfo &sensorInfo, const ConfigPa ControlInfoMap::Map ctrlMap = ipaControls; ctrlMap[&controls::FrameDurationLimits] = ControlInfo(static_cast<int64_t>(mode_.minFrameDuration.get<std::micro>()), - static_cast<int64_t>(mode_.maxFrameDuration.get<std::micro>())); + static_cast<int64_t>(mode_.maxFrameDuration.get<std::micro>()), + static_cast<int64_t>(defaultMinFrameDuration.get<std::micro>())); ctrlMap[&controls::AnalogueGain] = ControlInfo(static_cast<float>(mode_.minAnalogueGain), - static_cast<float>(mode_.maxAnalogueGain)); + static_cast<float>(mode_.maxAnalogueGain), + static_cast<float>(defaultAnalogueGain)); ctrlMap[&controls::ExposureTime] = ControlInfo(static_cast<int32_t>(mode_.minExposureTime.get<std::micro>()), - static_cast<int32_t>(mode_.maxExposureTime.get<std::micro>())); + static_cast<int32_t>(mode_.maxExposureTime.get<std::micro>()), + static_cast<int32_t>(defaultExposureTime.get<std::micro>())); /* Declare colour processing related controls for non-mono sensors. */ if (!monoSensor_) @@ -757,6 +764,42 @@ void IpaBase::applyControls(const ControlList &controls) af->setMode(mode->second); } + /* + * Because some AE controls are mode-specific, handle the AE-related + * mode changes first. + */ + const auto analogueGainMode = controls.get(controls::AnalogueGainMode); + const auto exposureTimeMode = controls.get(controls::ExposureTimeMode); + + if (analogueGainMode || exposureTimeMode) { + RPiController::AgcAlgorithm *agc = dynamic_cast<RPiController::AgcAlgorithm *>( + controller_.getAlgorithm("agc")); + if (agc) { + if (analogueGainMode) { + if (*analogueGainMode == controls::AnalogueGainModeManual) + agc->disableAutoGain(); + else + agc->enableAutoGain(); + + libcameraMetadata_.set(controls::AnalogueGainMode, + *analogueGainMode); + } + + if (exposureTimeMode) { + if (*exposureTimeMode == controls::ExposureTimeModeManual) + agc->disableAutoExposure(); + else + agc->enableAutoExposure(); + + libcameraMetadata_.set(controls::ExposureTimeMode, + *exposureTimeMode); + } + } else { + LOG(IPARPI, Warning) + << "Could not set AnalogueGainMode or ExposureTimeMode - no AGC algorithm"; + } + } + /* Iterate over controls */ for (auto const &ctrl : controls) { LOG(IPARPI, Debug) << "Request ctrl: " @@ -764,23 +807,8 @@ void IpaBase::applyControls(const ControlList &controls) << " = " << ctrl.second.toString(); switch (ctrl.first) { - case controls::AE_ENABLE: { - RPiController::AgcAlgorithm *agc = dynamic_cast<RPiController::AgcAlgorithm *>( - controller_.getAlgorithm("agc")); - if (!agc) { - LOG(IPARPI, Warning) - << "Could not set AE_ENABLE - no AGC algorithm"; - break; - } - - if (ctrl.second.get<bool>() == false) - agc->disableAuto(); - else - agc->enableAuto(); - - libcameraMetadata_.set(controls::AeEnable, ctrl.second.get<bool>()); - break; - } + case controls::EXPOSURE_TIME_MODE: + break; /* We already handled this one above */ case controls::EXPOSURE_TIME: { RPiController::AgcAlgorithm *agc = dynamic_cast<RPiController::AgcAlgorithm *>( @@ -791,6 +819,13 @@ void IpaBase::applyControls(const ControlList &controls) break; } + /* + * Ignore manual exposure time when the auto exposure + * algorithm is running. + */ + if (agc->autoExposureEnabled()) + break; + /* The control provides units of microseconds. */ agc->setFixedExposureTime(0, ctrl.second.get<int32_t>() * 1.0us); @@ -798,6 +833,9 @@ void IpaBase::applyControls(const ControlList &controls) break; } + case controls::ANALOGUE_GAIN_MODE: + break; /* We already handled this one above */ + case controls::ANALOGUE_GAIN: { RPiController::AgcAlgorithm *agc = dynamic_cast<RPiController::AgcAlgorithm *>( controller_.getAlgorithm("agc")); @@ -807,6 +845,13 @@ void IpaBase::applyControls(const ControlList &controls) break; } + /* + * Ignore manual analogue gain value when the auto gain + * algorithm is running. + */ + if (agc->autoGainEnabled()) + break; + agc->setFixedAnalogueGain(0, ctrl.second.get<float>()); libcameraMetadata_.set(controls::AnalogueGain, @@ -863,6 +908,13 @@ void IpaBase::applyControls(const ControlList &controls) break; } + /* + * Ignore AE_EXPOSURE_MODE if the shutter or the gain + * are in auto mode. + */ + if (agc->autoExposureEnabled() || agc->autoGainEnabled()) + break; + int32_t idx = ctrl.second.get<int32_t>(); if (ExposureModeTable.count(idx)) { agc->setExposureMode(ExposureModeTable.at(idx)); @@ -1021,6 +1073,25 @@ void IpaBase::applyControls(const ControlList &controls) break; } + case controls::COLOUR_TEMPERATURE: { + /* Silently ignore this control for a mono sensor. */ + if (monoSensor_) + break; + + auto temperatureK = ctrl.second.get<int32_t>(); + RPiController::AwbAlgorithm *awb = dynamic_cast<RPiController::AwbAlgorithm *>( + controller_.getAlgorithm("awb")); + if (!awb) { + LOG(IPARPI, Warning) + << "Could not set COLOUR_TEMPERATURE - no AWB algorithm"; + break; + } + + awb->setColourTemperature(temperatureK); + /* This metadata will get reported back automatically. */ + break; + } + case controls::BRIGHTNESS: { RPiController::ContrastAlgorithm *contrast = dynamic_cast<RPiController::ContrastAlgorithm *>( controller_.getAlgorithm("contrast")); @@ -1323,9 +1394,19 @@ void IpaBase::reportMetadata(unsigned int ipaContext) } AgcPrepareStatus *agcPrepareStatus = rpiMetadata.getLocked<AgcPrepareStatus>("agc.prepare_status"); - if (agcPrepareStatus) { - libcameraMetadata_.set(controls::AeLocked, agcPrepareStatus->locked); + if (agcPrepareStatus) libcameraMetadata_.set(controls::DigitalGain, agcPrepareStatus->digitalGain); + + RPiController::AgcAlgorithm *agc = dynamic_cast<RPiController::AgcAlgorithm *>( + controller_.getAlgorithm("agc")); + if (agc) { + if (!agc->autoExposureEnabled() && !agc->autoGainEnabled()) + libcameraMetadata_.set(controls::AeState, controls::AeStateIdle); + else if (agcPrepareStatus) + libcameraMetadata_.set(controls::AeState, + agcPrepareStatus->locked + ? controls::AeStateConverged + : controls::AeStateSearching); } LuxStatus *luxStatus = rpiMetadata.getLocked<LuxStatus>("lux.status"); diff --git a/src/ipa/rpi/controller/agc_algorithm.h b/src/ipa/rpi/controller/agc_algorithm.h index c9782857..fdaa10e6 100644 --- a/src/ipa/rpi/controller/agc_algorithm.h +++ b/src/ipa/rpi/controller/agc_algorithm.h @@ -30,8 +30,12 @@ public: virtual void setMeteringMode(std::string const &meteringModeName) = 0; virtual void setExposureMode(std::string const &exposureModeName) = 0; virtual void setConstraintMode(std::string const &contraintModeName) = 0; - virtual void enableAuto() = 0; - virtual void disableAuto() = 0; + virtual void enableAutoExposure() = 0; + virtual void disableAutoExposure() = 0; + virtual bool autoExposureEnabled() const = 0; + virtual void enableAutoGain() = 0; + virtual void disableAutoGain() = 0; + virtual bool autoGainEnabled() const = 0; virtual void setActiveChannels(const std::vector<unsigned int> &activeChannels) = 0; }; diff --git a/src/ipa/rpi/controller/awb_algorithm.h b/src/ipa/rpi/controller/awb_algorithm.h index 1779b050..d941ed4e 100644 --- a/src/ipa/rpi/controller/awb_algorithm.h +++ b/src/ipa/rpi/controller/awb_algorithm.h @@ -19,6 +19,7 @@ public: virtual void initialValues(double &gainR, double &gainB) = 0; virtual void setMode(std::string const &modeName) = 0; virtual void setManualGains(double manualR, double manualB) = 0; + virtual void setColourTemperature(double temperatureK) = 0; virtual void enableAuto() = 0; virtual void disableAuto() = 0; }; diff --git a/src/ipa/rpi/controller/controller.cpp b/src/ipa/rpi/controller/controller.cpp index e0131018..651fff63 100644 --- a/src/ipa/rpi/controller/controller.cpp +++ b/src/ipa/rpi/controller/controller.cpp @@ -39,6 +39,7 @@ static const std::map<std::string, Controller::HardwareConfig> HardwareConfigMap .pipelineWidth = 13, .statsInline = false, .minPixelProcessingTime = 0s, + .dataBufferStrided = true, } }, { @@ -71,6 +72,7 @@ static const std::map<std::string, Controller::HardwareConfig> HardwareConfigMap * frames wider than ~16,000 pixels. */ .minPixelProcessingTime = 1.0us / 380, + .dataBufferStrided = false, } }, }; diff --git a/src/ipa/rpi/controller/controller.h b/src/ipa/rpi/controller/controller.h index eff520bd..fdb46557 100644 --- a/src/ipa/rpi/controller/controller.h +++ b/src/ipa/rpi/controller/controller.h @@ -49,6 +49,7 @@ public: unsigned int pipelineWidth; bool statsInline; libcamera::utils::Duration minPixelProcessingTime; + bool dataBufferStrided; }; Controller(); diff --git a/src/ipa/rpi/controller/metadata.h b/src/ipa/rpi/controller/metadata.h index b4650d25..77d3b074 100644 --- a/src/ipa/rpi/controller/metadata.h +++ b/src/ipa/rpi/controller/metadata.h @@ -12,6 +12,7 @@ #include <map> #include <mutex> #include <string> +#include <utility> #include <libcamera/base/thread_annotations.h> @@ -36,10 +37,10 @@ public: } template<typename T> - void set(std::string const &tag, T const &value) + void set(std::string const &tag, T &&value) { std::scoped_lock lock(mutex_); - data_[tag] = value; + data_[tag] = std::forward<T>(value); } template<typename T> @@ -90,6 +91,12 @@ public: data_.insert(other.data_.begin(), other.data_.end()); } + void erase(std::string const &tag) + { + std::scoped_lock lock(mutex_); + eraseLocked(tag); + } + template<typename T> T *getLocked(std::string const &tag) { @@ -104,10 +111,18 @@ public: } template<typename T> - void setLocked(std::string const &tag, T const &value) + void setLocked(std::string const &tag, T &&value) { /* Use this only if you're holding the lock yourself. */ - data_[tag] = value; + data_[tag] = std::forward<T>(value); + } + + void eraseLocked(std::string const &tag) + { + auto it = data_.find(tag); + if (it == data_.end()) + return; + data_.erase(it); } /* diff --git a/src/ipa/rpi/controller/rpi/agc.cpp b/src/ipa/rpi/controller/rpi/agc.cpp index c48fdf15..02bfdb4a 100644 --- a/src/ipa/rpi/controller/rpi/agc.cpp +++ b/src/ipa/rpi/controller/rpi/agc.cpp @@ -74,22 +74,62 @@ int Agc::checkChannel(unsigned int channelIndex) const return 0; } -void Agc::disableAuto() +void Agc::disableAutoExposure() { - LOG(RPiAgc, Debug) << "disableAuto"; + LOG(RPiAgc, Debug) << "disableAutoExposure"; /* All channels are enabled/disabled together. */ for (auto &data : channelData_) - data.channel.disableAuto(); + data.channel.disableAutoExposure(); } -void Agc::enableAuto() +void Agc::enableAutoExposure() { - LOG(RPiAgc, Debug) << "enableAuto"; + LOG(RPiAgc, Debug) << "enableAutoExposure"; /* All channels are enabled/disabled together. */ for (auto &data : channelData_) - data.channel.enableAuto(); + data.channel.enableAutoExposure(); +} + +bool Agc::autoExposureEnabled() const +{ + LOG(RPiAgc, Debug) << "autoExposureEnabled"; + + /* + * We always have at least one channel, and since all channels are + * enabled and disabled together we can simply check the first one. + */ + return channelData_[0].channel.autoExposureEnabled(); +} + +void Agc::disableAutoGain() +{ + LOG(RPiAgc, Debug) << "disableAutoGain"; + + /* All channels are enabled/disabled together. */ + for (auto &data : channelData_) + data.channel.disableAutoGain(); +} + +void Agc::enableAutoGain() +{ + LOG(RPiAgc, Debug) << "enableAutoGain"; + + /* All channels are enabled/disabled together. */ + for (auto &data : channelData_) + data.channel.enableAutoGain(); +} + +bool Agc::autoGainEnabled() const +{ + LOG(RPiAgc, Debug) << "autoGainEnabled"; + + /* + * We always have at least one channel, and since all channels are + * enabled and disabled together we can simply check the first one. + */ + return channelData_[0].channel.autoGainEnabled(); } unsigned int Agc::getConvergenceFrames() const diff --git a/src/ipa/rpi/controller/rpi/agc.h b/src/ipa/rpi/controller/rpi/agc.h index 3aca000b..c3a940bf 100644 --- a/src/ipa/rpi/controller/rpi/agc.h +++ b/src/ipa/rpi/controller/rpi/agc.h @@ -40,8 +40,12 @@ public: void setMeteringMode(std::string const &meteringModeName) override; void setExposureMode(std::string const &exposureModeName) override; void setConstraintMode(std::string const &contraintModeName) override; - void enableAuto() override; - void disableAuto() override; + void enableAutoExposure() override; + void disableAutoExposure() override; + bool autoExposureEnabled() const override; + void enableAutoGain() override; + void disableAutoGain() override; + bool autoGainEnabled() const override; void switchMode(CameraMode const &cameraMode, Metadata *metadata) override; void prepare(Metadata *imageMetadata) override; void process(StatisticsPtr &stats, Metadata *imageMetadata) override; diff --git a/src/ipa/rpi/controller/rpi/agc_channel.cpp b/src/ipa/rpi/controller/rpi/agc_channel.cpp index 79c45973..a5562760 100644 --- a/src/ipa/rpi/controller/rpi/agc_channel.cpp +++ b/src/ipa/rpi/controller/rpi/agc_channel.cpp @@ -12,8 +12,9 @@ #include <libcamera/base/log.h> +#include "libcamera/internal/vector.h" + #include "libipa/colours.h" -#include "libipa/vector.h" #include "../awb_status.h" #include "../device_status.h" @@ -319,18 +320,36 @@ int AgcChannel::read(const libcamera::YamlObject ¶ms, return 0; } -void AgcChannel::disableAuto() +void AgcChannel::disableAutoExposure() { fixedExposureTime_ = status_.exposureTime; - fixedAnalogueGain_ = status_.analogueGain; } -void AgcChannel::enableAuto() +void AgcChannel::enableAutoExposure() { fixedExposureTime_ = 0s; +} + +bool AgcChannel::autoExposureEnabled() const +{ + return fixedExposureTime_ == 0s; +} + +void AgcChannel::disableAutoGain() +{ + fixedAnalogueGain_ = status_.analogueGain; +} + +void AgcChannel::enableAutoGain() +{ fixedAnalogueGain_ = 0; } +bool AgcChannel::autoGainEnabled() const +{ + return fixedAnalogueGain_ == 0; +} + unsigned int AgcChannel::getConvergenceFrames() const { /* @@ -682,7 +701,7 @@ static double computeInitialY(StatisticsPtr &stats, AwbStatus const &awb, * Note that the weights are applied by the IPA to the statistics directly, * before they are given to us here. */ - ipa::RGB<double> sum{ 0.0 }; + RGB<double> sum{ 0.0 }; double pixelSum = 0; for (unsigned int i = 0; i < stats->agcRegions.numRegions(); i++) { auto ®ion = stats->agcRegions.get(i); @@ -698,7 +717,7 @@ static double computeInitialY(StatisticsPtr &stats, AwbStatus const &awb, /* Factor in the AWB correction if needed. */ if (stats->agcStatsPos == Statistics::AgcStatsPos::PreWb) - sum *= ipa::RGB<double>{{ awb.gainR, awb.gainR, awb.gainB }}; + sum *= RGB<double>{ { awb.gainR, awb.gainR, awb.gainB } }; double ySum = ipa::rec601LuminanceFromRGB(sum); diff --git a/src/ipa/rpi/controller/rpi/agc_channel.h b/src/ipa/rpi/controller/rpi/agc_channel.h index 734e5efd..fa697e6f 100644 --- a/src/ipa/rpi/controller/rpi/agc_channel.h +++ b/src/ipa/rpi/controller/rpi/agc_channel.h @@ -96,8 +96,12 @@ public: void setMeteringMode(std::string const &meteringModeName); void setExposureMode(std::string const &exposureModeName); void setConstraintMode(std::string const &contraintModeName); - void enableAuto(); - void disableAuto(); + void enableAutoExposure(); + void disableAutoExposure(); + bool autoExposureEnabled() const; + void enableAutoGain(); + void disableAutoGain(); + bool autoGainEnabled() const; void switchMode(CameraMode const &cameraMode, Metadata *metadata); void prepare(Metadata *imageMetadata); void process(StatisticsPtr &stats, DeviceStatus const &deviceStatus, Metadata *imageMetadata, diff --git a/src/ipa/rpi/controller/rpi/awb.cpp b/src/ipa/rpi/controller/rpi/awb.cpp index c277a176..8479ae40 100644 --- a/src/ipa/rpi/controller/rpi/awb.cpp +++ b/src/ipa/rpi/controller/rpi/awb.cpp @@ -293,6 +293,24 @@ void Awb::setManualGains(double manualR, double manualB) } } +void Awb::setColourTemperature(double temperatureK) +{ + if (!config_.bayes) { + LOG(RPiAwb, Warning) << "AWB uncalibrated - cannot set colour temperature"; + return; + } + + temperatureK = config_.ctR.domain().clamp(temperatureK); + manualR_ = 1 / config_.ctR.eval(temperatureK); + manualB_ = 1 / config_.ctB.eval(temperatureK); + + syncResults_.temperatureK = temperatureK; + syncResults_.gainR = manualR_; + syncResults_.gainG = 1.0; + syncResults_.gainB = manualB_; + prevSyncResults_ = syncResults_; +} + void Awb::switchMode([[maybe_unused]] CameraMode const &cameraMode, Metadata *metadata) { diff --git a/src/ipa/rpi/controller/rpi/awb.h b/src/ipa/rpi/controller/rpi/awb.h index 5d628b47..86640f8f 100644 --- a/src/ipa/rpi/controller/rpi/awb.h +++ b/src/ipa/rpi/controller/rpi/awb.h @@ -105,6 +105,7 @@ public: void initialValues(double &gainR, double &gainB) override; void setMode(std::string const &name) override; void setManualGains(double manualR, double manualB) override; + void setColourTemperature(double temperatureK) override; void enableAuto() override; void disableAuto() override; void switchMode(CameraMode const &cameraMode, Metadata *metadata) override; diff --git a/src/ipa/rpi/controller/rpi/ccm.cpp b/src/ipa/rpi/controller/rpi/ccm.cpp index aefa580c..8607f152 100644 --- a/src/ipa/rpi/controller/rpi/ccm.cpp +++ b/src/ipa/rpi/controller/rpi/ccm.cpp @@ -29,34 +29,7 @@ LOG_DEFINE_CATEGORY(RPiCcm) #define NAME "rpi.ccm" -Matrix::Matrix() -{ - memset(m, 0, sizeof(m)); -} -Matrix::Matrix(double m0, double m1, double m2, double m3, double m4, double m5, - double m6, double m7, double m8) -{ - m[0][0] = m0, m[0][1] = m1, m[0][2] = m2, m[1][0] = m3, m[1][1] = m4, - m[1][2] = m5, m[2][0] = m6, m[2][1] = m7, m[2][2] = m8; -} -int Matrix::read(const libcamera::YamlObject ¶ms) -{ - double *ptr = (double *)m; - - if (params.size() != 9) { - LOG(RPiCcm, Error) << "Wrong number of values in CCM"; - return -EINVAL; - } - - for (const auto ¶m : params.asList()) { - auto value = param.get<double>(); - if (!value) - return -EINVAL; - *ptr++ = *value; - } - - return 0; -} +using Matrix3x3 = Matrix<double, 3, 3>; Ccm::Ccm(Controller *controller) : CcmAlgorithm(controller), saturation_(1.0) {} @@ -68,8 +41,6 @@ char const *Ccm::name() const int Ccm::read(const libcamera::YamlObject ¶ms) { - int ret; - if (params.contains("saturation")) { config_.saturation = params["saturation"].get<ipa::Pwl>(ipa::Pwl{}); if (config_.saturation.empty()) @@ -83,9 +54,12 @@ int Ccm::read(const libcamera::YamlObject ¶ms) CtCcm ctCcm; ctCcm.ct = *value; - ret = ctCcm.ccm.read(p["ccm"]); - if (ret) - return ret; + + auto ccm = p["ccm"].get<Matrix3x3>(); + if (!ccm) + return -EINVAL; + + ctCcm.ccm = *ccm; if (!config_.ccms.empty() && ctCcm.ct <= config_.ccms.back().ct) { LOG(RPiCcm, Error) @@ -125,7 +99,7 @@ bool getLocked(Metadata *metadata, std::string const &tag, T &value) return true; } -Matrix calculateCcm(std::vector<CtCcm> const &ccms, double ct) +Matrix3x3 calculateCcm(std::vector<CtCcm> const &ccms, double ct) { if (ct <= ccms.front().ct) return ccms.front().ccm; @@ -141,13 +115,20 @@ Matrix calculateCcm(std::vector<CtCcm> const &ccms, double ct) } } -Matrix applySaturation(Matrix const &ccm, double saturation) +Matrix3x3 applySaturation(Matrix3x3 const &ccm, double saturation) { - Matrix RGB2Y(0.299, 0.587, 0.114, -0.169, -0.331, 0.500, 0.500, -0.419, - -0.081); - Matrix Y2RGB(1.000, 0.000, 1.402, 1.000, -0.345, -0.714, 1.000, 1.771, - 0.000); - Matrix S(1, 0, 0, 0, saturation, 0, 0, 0, saturation); + static const Matrix3x3 RGB2Y({ 0.299, 0.587, 0.114, + -0.169, -0.331, 0.500, + 0.500, -0.419, -0.081 }); + + static const Matrix3x3 Y2RGB({ 1.000, 0.000, 1.402, + 1.000, -0.345, -0.714, + 1.000, 1.771, 0.000 }); + + Matrix3x3 S({ 1, 0, 0, + 0, saturation, 0, + 0, 0, saturation }); + return Y2RGB * S * RGB2Y * ccm; } @@ -170,7 +151,7 @@ void Ccm::prepare(Metadata *imageMetadata) LOG(RPiCcm, Warning) << "no colour temperature found"; if (!luxOk) LOG(RPiCcm, Warning) << "no lux value found"; - Matrix ccm = calculateCcm(config_.ccms, awb.temperatureK); + Matrix3x3 ccm = calculateCcm(config_.ccms, awb.temperatureK); double saturation = saturation_; struct CcmStatus ccmStatus; ccmStatus.saturation = saturation; @@ -181,7 +162,7 @@ void Ccm::prepare(Metadata *imageMetadata) for (int j = 0; j < 3; j++) for (int i = 0; i < 3; i++) ccmStatus.matrix[j * 3 + i] = - std::max(-8.0, std::min(7.9999, ccm.m[j][i])); + std::max(-8.0, std::min(7.9999, ccm[j][i])); LOG(RPiCcm, Debug) << "colour temperature " << awb.temperatureK << "K"; LOG(RPiCcm, Debug) diff --git a/src/ipa/rpi/controller/rpi/ccm.h b/src/ipa/rpi/controller/rpi/ccm.h index 4e5b33fe..c05dbb17 100644 --- a/src/ipa/rpi/controller/rpi/ccm.h +++ b/src/ipa/rpi/controller/rpi/ccm.h @@ -8,6 +8,7 @@ #include <vector> +#include "libcamera/internal/matrix.h" #include <libipa/pwl.h> #include "../ccm_algorithm.h" @@ -16,41 +17,9 @@ namespace RPiController { /* Algorithm to calculate colour matrix. Should be placed after AWB. */ -struct Matrix { - Matrix(double m0, double m1, double m2, double m3, double m4, double m5, - double m6, double m7, double m8); - Matrix(); - double m[3][3]; - int read(const libcamera::YamlObject ¶ms); -}; -static inline Matrix operator*(double d, Matrix const &m) -{ - return Matrix(m.m[0][0] * d, m.m[0][1] * d, m.m[0][2] * d, - m.m[1][0] * d, m.m[1][1] * d, m.m[1][2] * d, - m.m[2][0] * d, m.m[2][1] * d, m.m[2][2] * d); -} -static inline Matrix operator*(Matrix const &m1, Matrix const &m2) -{ - Matrix m; - for (int i = 0; i < 3; i++) - for (int j = 0; j < 3; j++) - m.m[i][j] = m1.m[i][0] * m2.m[0][j] + - m1.m[i][1] * m2.m[1][j] + - m1.m[i][2] * m2.m[2][j]; - return m; -} -static inline Matrix operator+(Matrix const &m1, Matrix const &m2) -{ - Matrix m; - for (int i = 0; i < 3; i++) - for (int j = 0; j < 3; j++) - m.m[i][j] = m1.m[i][j] + m2.m[i][j]; - return m; -} - struct CtCcm { double ct; - Matrix ccm; + libcamera::Matrix<double, 3, 3> ccm; }; struct CcmConfig { diff --git a/src/ipa/rpi/vc4/data/imx415.json b/src/ipa/rpi/vc4/data/imx415.json new file mode 100755 index 00000000..6ed16b17 --- /dev/null +++ b/src/ipa/rpi/vc4/data/imx415.json @@ -0,0 +1,413 @@ +{ + "version": 2.0, + "target": "bcm2835", + "algorithms": [ + { + "rpi.black_level": + { + "black_level": 3840 + } + }, + { + "rpi.dpc": { } + }, + { + "rpi.lux": + { + "reference_shutter_speed": 19230, + "reference_gain": 1.0, + "reference_aperture": 1.0, + "reference_lux": 1198, + "reference_Y": 14876 + } + }, + { + "rpi.noise": + { + "reference_constant": 17, + "reference_slope": 3.439 + } + }, + { + "rpi.geq": + { + "offset": 193, + "slope": 0.00902 + } + }, + { + "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, + "ct_curve": + [ + 2698.0, 0.7681, 0.2026, + 2930.0, 0.7515, 0.2116, + 3643.0, 0.6355, 0.2858, + 4605.0, 0.4992, 0.4041, + 5658.0, 0.4498, 0.4574 + ], + "sensitivity_r": 1.0, + "sensitivity_b": 1.0, + "transverse_pos": 0.0112, + "transverse_neg": 0.01424 + } + }, + { + "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.8, + "calibrations_Cr": [ + { + "ct": 3000, + "table": + [ + 1.025, 1.016, 1.013, 1.011, 1.008, 1.005, 1.003, 1.001, 1.003, 1.005, 1.008, 1.011, 1.014, 1.019, 1.027, 1.035, + 1.025, 1.017, 1.013, 1.011, 1.008, 1.005, 1.003, 1.003, 1.004, 1.005, 1.009, 1.012, 1.017, 1.023, 1.029, 1.035, + 1.022, 1.017, 1.013, 1.009, 1.007, 1.005, 1.003, 1.003, 1.004, 1.006, 1.009, 1.012, 1.017, 1.023, 1.029, 1.035, + 1.019, 1.015, 1.011, 1.007, 1.005, 1.003, 1.001, 1.001, 1.003, 1.004, 1.007, 1.009, 1.015, 1.022, 1.028, 1.035, + 1.018, 1.014, 1.009, 1.006, 1.004, 1.002, 1.001, 1.001, 1.001, 1.003, 1.006, 1.009, 1.015, 1.021, 1.028, 1.035, + 1.018, 1.013, 1.011, 1.006, 1.003, 1.002, 1.001, 1.001, 1.001, 1.003, 1.006, 1.009, 1.015, 1.022, 1.028, 1.036, + 1.018, 1.014, 1.011, 1.007, 1.004, 1.002, 1.001, 1.001, 1.001, 1.004, 1.007, 1.009, 1.015, 1.023, 1.029, 1.036, + 1.019, 1.014, 1.012, 1.008, 1.005, 1.003, 1.002, 1.001, 1.003, 1.005, 1.008, 1.012, 1.016, 1.024, 1.031, 1.037, + 1.021, 1.016, 1.013, 1.009, 1.008, 1.005, 1.003, 1.003, 1.005, 1.008, 1.011, 1.014, 1.019, 1.026, 1.033, 1.039, + 1.025, 1.021, 1.016, 1.013, 1.009, 1.008, 1.006, 1.006, 1.008, 1.011, 1.014, 1.019, 1.024, 1.031, 1.038, 1.046, + 1.029, 1.025, 1.021, 1.018, 1.014, 1.013, 1.011, 1.011, 1.012, 1.015, 1.019, 1.023, 1.028, 1.035, 1.046, 1.051, + 1.032, 1.029, 1.023, 1.021, 1.018, 1.015, 1.014, 1.014, 1.015, 1.018, 1.022, 1.027, 1.033, 1.041, 1.051, 1.054 + ] + }, + { + "ct": 5000, + "table": + [ + 1.025, 1.011, 1.009, 1.005, 1.004, 1.003, 1.001, 1.001, 1.002, 1.006, 1.009, 1.012, 1.016, 1.021, 1.031, 1.041, + 1.025, 1.014, 1.009, 1.007, 1.005, 1.004, 1.003, 1.003, 1.004, 1.007, 1.009, 1.013, 1.021, 1.028, 1.037, 1.041, + 1.023, 1.014, 1.009, 1.007, 1.005, 1.004, 1.003, 1.003, 1.005, 1.007, 1.011, 1.014, 1.021, 1.028, 1.037, 1.048, + 1.022, 1.012, 1.007, 1.005, 1.002, 1.001, 1.001, 1.001, 1.003, 1.005, 1.009, 1.014, 1.019, 1.028, 1.039, 1.048, + 1.022, 1.011, 1.006, 1.003, 1.001, 1.001, 1.001, 1.001, 1.002, 1.005, 1.009, 1.014, 1.021, 1.029, 1.039, 1.051, + 1.022, 1.012, 1.007, 1.003, 1.002, 1.001, 1.001, 1.001, 1.002, 1.005, 1.009, 1.015, 1.021, 1.031, 1.041, 1.053, + 1.023, 1.013, 1.009, 1.005, 1.003, 1.003, 1.001, 1.002, 1.004, 1.006, 1.011, 1.015, 1.022, 1.031, 1.042, 1.056, + 1.024, 1.015, 1.012, 1.008, 1.005, 1.004, 1.004, 1.004, 1.006, 1.009, 1.013, 1.018, 1.024, 1.034, 1.045, 1.057, + 1.027, 1.017, 1.015, 1.012, 1.009, 1.007, 1.007, 1.008, 1.009, 1.013, 1.018, 1.023, 1.029, 1.038, 1.051, 1.061, + 1.029, 1.023, 1.017, 1.015, 1.014, 1.012, 1.011, 1.011, 1.014, 1.018, 1.024, 1.029, 1.036, 1.044, 1.056, 1.066, + 1.034, 1.028, 1.023, 1.022, 1.019, 1.019, 1.018, 1.018, 1.021, 1.025, 1.031, 1.035, 1.042, 1.053, 1.066, 1.074, + 1.041, 1.034, 1.027, 1.025, 1.025, 1.023, 1.023, 1.023, 1.025, 1.031, 1.035, 1.041, 1.049, 1.059, 1.074, 1.079 + ] + } + ], + "calibrations_Cb": [ + { + "ct": 3000, + "table": + [ + 1.001, 1.001, 1.007, 1.015, 1.027, 1.034, 1.038, 1.041, 1.042, 1.043, 1.043, 1.043, 1.041, 1.039, 1.049, 1.054, + 1.011, 1.011, 1.013, 1.023, 1.032, 1.039, 1.044, 1.047, 1.052, 1.056, 1.059, 1.059, 1.055, 1.051, 1.054, 1.056, + 1.015, 1.015, 1.019, 1.032, 1.039, 1.044, 1.047, 1.052, 1.055, 1.059, 1.061, 1.066, 1.063, 1.058, 1.061, 1.064, + 1.016, 1.017, 1.023, 1.032, 1.041, 1.045, 1.048, 1.053, 1.056, 1.061, 1.066, 1.069, 1.067, 1.064, 1.065, 1.068, + 1.018, 1.019, 1.025, 1.033, 1.042, 1.045, 1.049, 1.054, 1.058, 1.063, 1.071, 1.072, 1.071, 1.068, 1.069, 1.071, + 1.023, 1.024, 1.029, 1.035, 1.043, 1.048, 1.052, 1.057, 1.061, 1.065, 1.074, 1.075, 1.075, 1.072, 1.072, 1.075, + 1.027, 1.028, 1.031, 1.038, 1.045, 1.051, 1.054, 1.059, 1.064, 1.068, 1.075, 1.079, 1.078, 1.075, 1.076, 1.081, + 1.029, 1.031, 1.033, 1.044, 1.048, 1.054, 1.059, 1.064, 1.067, 1.073, 1.079, 1.082, 1.082, 1.079, 1.081, 1.085, + 1.033, 1.033, 1.035, 1.047, 1.053, 1.058, 1.064, 1.067, 1.073, 1.079, 1.084, 1.086, 1.086, 1.084, 1.089, 1.091, + 1.037, 1.037, 1.038, 1.049, 1.057, 1.062, 1.068, 1.073, 1.079, 1.084, 1.089, 1.092, 1.092, 1.092, 1.096, 1.104, + 1.041, 1.041, 1.043, 1.051, 1.061, 1.068, 1.073, 1.079, 1.083, 1.089, 1.092, 1.094, 1.097, 1.099, 1.105, 1.115, + 1.048, 1.044, 1.044, 1.051, 1.063, 1.071, 1.076, 1.082, 1.088, 1.091, 1.094, 1.097, 1.099, 1.104, 1.115, 1.126 + ] + }, + { + "ct": 5000, + "table": + [ + 1.001, 1.001, 1.005, 1.011, 1.014, 1.018, 1.019, 1.019, 1.019, 1.021, 1.021, 1.021, 1.019, 1.017, 1.014, 1.014, + 1.009, 1.009, 1.011, 1.014, 1.019, 1.024, 1.026, 1.029, 1.031, 1.032, 1.032, 1.031, 1.027, 1.023, 1.022, 1.022, + 1.011, 1.012, 1.015, 1.018, 1.024, 1.026, 1.029, 1.032, 1.035, 1.036, 1.036, 1.034, 1.031, 1.027, 1.025, 1.025, + 1.012, 1.013, 1.015, 1.019, 1.025, 1.029, 1.032, 1.035, 1.036, 1.038, 1.038, 1.036, 1.034, 1.029, 1.026, 1.026, + 1.013, 1.014, 1.016, 1.019, 1.027, 1.031, 1.034, 1.037, 1.039, 1.039, 1.041, 1.039, 1.036, 1.031, 1.028, 1.027, + 1.014, 1.014, 1.017, 1.021, 1.027, 1.033, 1.037, 1.039, 1.041, 1.041, 1.042, 1.042, 1.039, 1.033, 1.029, 1.028, + 1.015, 1.015, 1.018, 1.021, 1.027, 1.033, 1.037, 1.041, 1.041, 1.042, 1.042, 1.042, 1.039, 1.034, 1.029, 1.029, + 1.015, 1.016, 1.018, 1.022, 1.027, 1.033, 1.037, 1.041, 1.041, 1.042, 1.043, 1.043, 1.041, 1.035, 1.031, 1.031, + 1.015, 1.016, 1.018, 1.022, 1.027, 1.032, 1.037, 1.041, 1.042, 1.042, 1.044, 1.043, 1.041, 1.036, 1.034, 1.033, + 1.016, 1.017, 1.017, 1.022, 1.027, 1.032, 1.036, 1.039, 1.042, 1.042, 1.043, 1.043, 1.041, 1.039, 1.036, 1.034, + 1.017, 1.017, 1.018, 1.022, 1.027, 1.031, 1.035, 1.039, 1.041, 1.042, 1.042, 1.042, 1.042, 1.039, 1.039, 1.039, + 1.018, 1.017, 1.017, 1.021, 1.027, 1.031, 1.033, 1.038, 1.041, 1.041, 1.042, 1.042, 1.041, 1.041, 1.041, 1.041 + ] + } + ], + "luminance_lut": + [ + 2.102, 1.903, 1.658, 1.483, 1.358, 1.267, 1.202, 1.202, 1.202, 1.242, 1.323, 1.431, 1.585, 1.797, 2.096, 2.351, + 1.996, 1.776, 1.549, 1.385, 1.273, 1.204, 1.138, 1.133, 1.133, 1.185, 1.252, 1.343, 1.484, 1.679, 1.954, 2.228, + 1.923, 1.689, 1.474, 1.318, 1.204, 1.138, 1.079, 1.071, 1.071, 1.133, 1.185, 1.284, 1.415, 1.597, 1.854, 2.146, + 1.881, 1.631, 1.423, 1.272, 1.159, 1.079, 1.051, 1.026, 1.046, 1.071, 1.144, 1.245, 1.369, 1.543, 1.801, 2.095, + 1.867, 1.595, 1.391, 1.242, 1.131, 1.051, 1.013, 1.002, 1.013, 1.046, 1.121, 1.219, 1.343, 1.511, 1.752, 2.079, + 1.867, 1.589, 1.385, 1.236, 1.125, 1.048, 1.001, 1.001, 1.003, 1.045, 1.118, 1.217, 1.342, 1.511, 1.746, 2.079, + 1.867, 1.589, 1.385, 1.236, 1.125, 1.048, 1.011, 1.003, 1.011, 1.046, 1.118, 1.217, 1.343, 1.511, 1.746, 2.079, + 1.884, 1.621, 1.411, 1.261, 1.149, 1.071, 1.048, 1.024, 1.046, 1.069, 1.141, 1.239, 1.369, 1.541, 1.781, 2.093, + 1.913, 1.675, 1.459, 1.304, 1.191, 1.125, 1.071, 1.065, 1.069, 1.124, 1.181, 1.278, 1.413, 1.592, 1.842, 2.133, + 1.981, 1.755, 1.529, 1.368, 1.251, 1.191, 1.125, 1.124, 1.124, 1.181, 1.242, 1.337, 1.479, 1.669, 1.935, 2.207, + 2.078, 1.867, 1.625, 1.453, 1.344, 1.251, 1.202, 1.201, 1.201, 1.242, 1.333, 1.418, 1.571, 1.776, 2.063, 2.321, + 2.217, 2.011, 1.747, 1.562, 1.431, 1.331, 1.278, 1.278, 1.278, 1.313, 1.407, 1.523, 1.686, 1.911, 2.226, 2.484 + ], + "sigma": 0.00135, + "sigma_Cb": 0.00279 + } + }, + { + "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": + { + "ccms": [ + { + "ct": 2698, + "ccm": + [ + 1.57227, -0.32596, -0.24631, + -0.61264, 1.70791, -0.09526, + -0.43254, 0.48489, 0.94765 + ] + }, + { + "ct": 2930, + "ccm": + [ + 1.69455, -0.52724, -0.16731, + -0.67131, 1.78468, -0.11338, + -0.41609, 0.54693, 0.86916 + ] + }, + { + "ct": 3643, + "ccm": + [ + 1.74041, -0.77553, 0.03512, + -0.44073, 1.34131, 0.09943, + -0.11035, -0.93919, 2.04954 + ] + }, + { + "ct": 4605, + "ccm": + [ + 1.49865, -0.41638, -0.08227, + -0.39445, 1.70114, -0.30669, + 0.01319, -0.88009, 1.86689 + ] + }, + { + "ct": 5658, + "ccm": + [ + 1.38601, -0.23128, -0.15472, + -0.37641, 1.70444, -0.32803, + -0.01575, -0.71466, 1.73041 + ] + } + ] + } + }, + { + "rpi.sharpen": { } + } + ] +} diff --git a/src/ipa/rpi/vc4/data/meson.build b/src/ipa/rpi/vc4/data/meson.build index 94c0ee6e..7a8001ee 100644 --- a/src/ipa/rpi/vc4/data/meson.build +++ b/src/ipa/rpi/vc4/data/meson.build @@ -9,6 +9,7 @@ conf_files = files([ 'imx296_mono.json', 'imx327.json', 'imx378.json', + 'imx415.json', 'imx462.json', 'imx477.json', 'imx477_noir.json', diff --git a/src/ipa/simple/algorithms/blc.cpp b/src/ipa/simple/algorithms/blc.cpp index b4e32fe1..1d7d370b 100644 --- a/src/ipa/simple/algorithms/blc.cpp +++ b/src/ipa/simple/algorithms/blc.cpp @@ -21,7 +21,8 @@ BlackLevel::BlackLevel() { } -int BlackLevel::init(IPAContext &context, const YamlObject &tuningData) +int BlackLevel::init([[maybe_unused]] IPAContext &context, + const YamlObject &tuningData) { auto blackLevel = tuningData["blackLevel"].get<int16_t>(); if (blackLevel.has_value()) { @@ -29,7 +30,7 @@ int BlackLevel::init(IPAContext &context, const YamlObject &tuningData) * Convert 16 bit values from the tuning file to 8 bit black * level for the SoftISP. */ - context.configuration.black.level = blackLevel.value() >> 8; + definedLevel_ = blackLevel.value() >> 8; } return 0; } @@ -37,6 +38,8 @@ int BlackLevel::init(IPAContext &context, const YamlObject &tuningData) int BlackLevel::configure(IPAContext &context, [[maybe_unused]] const IPAConfigInfo &configInfo) { + if (definedLevel_.has_value()) + context.configuration.black.level = definedLevel_; context.activeState.blc.level = context.configuration.black.level.value_or(255); return 0; diff --git a/src/ipa/simple/algorithms/blc.h b/src/ipa/simple/algorithms/blc.h index 67c688ae..52d59cab 100644 --- a/src/ipa/simple/algorithms/blc.h +++ b/src/ipa/simple/algorithms/blc.h @@ -7,6 +7,9 @@ #pragma once +#include <optional> +#include <stdint.h> + #include "algorithm.h" namespace libcamera { @@ -29,6 +32,7 @@ public: private: int32_t exposure_; double gain_; + std::optional<uint8_t> definedLevel_; }; } /* namespace ipa::soft::algorithms */ diff --git a/src/ipa/simple/algorithms/lut.cpp b/src/ipa/simple/algorithms/lut.cpp index 9744e773..0ba2391f 100644 --- a/src/ipa/simple/algorithms/lut.cpp +++ b/src/ipa/simple/algorithms/lut.cpp @@ -9,39 +9,75 @@ #include <algorithm> #include <cmath> +#include <optional> #include <stdint.h> #include <libcamera/base/log.h> #include "simple/ipa_context.h" +#include "control_ids.h" + namespace libcamera { +LOG_DEFINE_CATEGORY(IPASoftLut) + namespace ipa::soft::algorithms { +int Lut::init(IPAContext &context, + [[maybe_unused]] const YamlObject &tuningData) +{ + context.ctrlMap[&controls::Contrast] = ControlInfo(0.0f, 2.0f, 1.0f); + return 0; +} + int Lut::configure(IPAContext &context, [[maybe_unused]] const IPAConfigInfo &configInfo) { /* Gamma value is fixed */ context.configuration.gamma = 0.5; + context.activeState.knobs.contrast = std::optional<double>(); updateGammaTable(context); return 0; } +void Lut::queueRequest(typename Module::Context &context, + [[maybe_unused]] const uint32_t frame, + [[maybe_unused]] typename Module::FrameContext &frameContext, + const ControlList &controls) +{ + const auto &contrast = controls.get(controls::Contrast); + if (contrast.has_value()) { + context.activeState.knobs.contrast = contrast; + LOG(IPASoftLut, Debug) << "Setting contrast to " << contrast.value(); + } +} + void Lut::updateGammaTable(IPAContext &context) { auto &gammaTable = context.activeState.gamma.gammaTable; - auto blackLevel = context.activeState.blc.level; + const auto blackLevel = context.activeState.blc.level; const unsigned int blackIndex = blackLevel * gammaTable.size() / 256; + const auto contrast = context.activeState.knobs.contrast.value_or(1.0); std::fill(gammaTable.begin(), gammaTable.begin() + blackIndex, 0); const float divisor = gammaTable.size() - blackIndex - 1.0; - for (unsigned int i = blackIndex; i < gammaTable.size(); i++) - gammaTable[i] = UINT8_MAX * std::pow((i - blackIndex) / divisor, - context.configuration.gamma); + for (unsigned int i = blackIndex; i < gammaTable.size(); i++) { + double normalized = (i - blackIndex) / divisor; + /* Convert 0..2 to 0..infinity; avoid actual inifinity at tan(pi/2) */ + double contrastExp = tan(std::clamp(contrast * M_PI_4, 0.0, M_PI_2 - 0.00001)); + /* Apply simple S-curve */ + if (normalized < 0.5) + normalized = 0.5 * std::pow(normalized / 0.5, contrastExp); + else + normalized = 1.0 - 0.5 * std::pow((1.0 - normalized) / 0.5, contrastExp); + gammaTable[i] = UINT8_MAX * + std::pow(normalized, context.configuration.gamma); + } context.activeState.gamma.blackLevel = blackLevel; + context.activeState.gamma.contrast = contrast; } void Lut::prepare(IPAContext &context, @@ -55,7 +91,8 @@ void Lut::prepare(IPAContext &context, * observed, it's not permanently prone to minor fluctuations or * rounding errors. */ - if (context.activeState.gamma.blackLevel != context.activeState.blc.level) + if (context.activeState.gamma.blackLevel != context.activeState.blc.level || + context.activeState.gamma.contrast != context.activeState.knobs.contrast) updateGammaTable(context); auto &gains = context.activeState.gains; diff --git a/src/ipa/simple/algorithms/lut.h b/src/ipa/simple/algorithms/lut.h index b635987d..889f864b 100644 --- a/src/ipa/simple/algorithms/lut.h +++ b/src/ipa/simple/algorithms/lut.h @@ -19,7 +19,13 @@ public: Lut() = default; ~Lut() = default; + int init(IPAContext &context, const YamlObject &tuningData) override; int configure(IPAContext &context, const IPAConfigInfo &configInfo) override; + void queueRequest(typename Module::Context &context, + const uint32_t frame, + typename Module::FrameContext &frameContext, + const ControlList &controls) + override; void prepare(IPAContext &context, const uint32_t frame, IPAFrameContext &frameContext, diff --git a/src/ipa/simple/ipa_context.h b/src/ipa/simple/ipa_context.h index 2885a851..4af51306 100644 --- a/src/ipa/simple/ipa_context.h +++ b/src/ipa/simple/ipa_context.h @@ -11,6 +11,8 @@ #include <optional> #include <stdint.h> +#include <libcamera/controls.h> + #include <libipa/fc_queue.h> namespace libcamera { @@ -43,7 +45,12 @@ struct IPAActiveState { struct { std::array<double, kGammaLookupSize> gammaTable; uint8_t blackLevel; + double contrast; } gamma; + struct { + /* 0..2 range, 1.0 = normal */ + std::optional<double> contrast; + } knobs; }; struct IPAFrameContext : public FrameContext { @@ -54,9 +61,15 @@ struct IPAFrameContext : public FrameContext { }; struct IPAContext { + IPAContext(unsigned int frameContextSize) + : frameContexts(frameContextSize) + { + } + IPASessionConfiguration configuration; IPAActiveState activeState; FCQueue<IPAFrameContext> frameContexts; + ControlInfoMap::Map ctrlMap; }; } /* namespace ipa::soft */ diff --git a/src/ipa/simple/soft_simple.cpp b/src/ipa/simple/soft_simple.cpp index ba3d5265..b26e4e15 100644 --- a/src/ipa/simple/soft_simple.cpp +++ b/src/ipa/simple/soft_simple.cpp @@ -41,7 +41,7 @@ class IPASoftSimple : public ipa::soft::IPASoftInterface, public Module { public: IPASoftSimple() - : context_({ {}, {}, { kMaxFrameContexts } }) + : context_(kMaxFrameContexts) { } @@ -50,7 +50,8 @@ public: int init(const IPASettings &settings, const SharedFD &fdStats, const SharedFD &fdParams, - const ControlInfoMap &sensorInfoMap) override; + const ControlInfoMap &sensorInfoMap, + ControlInfoMap *ipaControls) override; int configure(const IPAConfigInfo &configInfo) override; int start() override; @@ -87,7 +88,8 @@ IPASoftSimple::~IPASoftSimple() int IPASoftSimple::init(const IPASettings &settings, const SharedFD &fdStats, const SharedFD &fdParams, - const ControlInfoMap &sensorInfoMap) + const ControlInfoMap &sensorInfoMap, + ControlInfoMap *ipaControls) { camHelper_ = CameraSensorHelperFactoryBase::create(settings.sensorModel); if (!camHelper_) { @@ -158,6 +160,9 @@ int IPASoftSimple::init(const IPASettings &settings, stats_ = static_cast<SwIspStats *>(mem); } + ControlInfoMap::Map ctrlMap = context_.ctrlMap; + *ipaControls = ControlInfoMap(std::move(ctrlMap), controls::controls); + /* * Check if the sensor driver supports the controls required by the * Soft IPA. @@ -206,8 +211,7 @@ int IPASoftSimple::configure(const IPAConfigInfo &configInfo) (context_.configuration.agc.againMax - context_.configuration.agc.againMin) / 100.0; - if (!context_.configuration.black.level.has_value() && - camHelper_->blackLevel().has_value()) { + if (camHelper_->blackLevel().has_value()) { /* * The black level from camHelper_ is a 16 bit value, software ISP * works with 8 bit pixel values, both regardless of the actual diff --git a/src/libcamera/base/log.cpp b/src/libcamera/base/log.cpp index 3a656b8f..8bf3e1da 100644 --- a/src/libcamera/base/log.cpp +++ b/src/libcamera/base/log.cpp @@ -8,12 +8,15 @@ #include <libcamera/base/log.h> #include <array> +#include <charconv> +#include <fnmatch.h> #include <fstream> #include <iostream> #include <list> #include <stdio.h> #include <stdlib.h> #include <string.h> +#include <string_view> #include <syslog.h> #include <time.h> #include <unordered_set> @@ -38,8 +41,8 @@ * The levels are configurable through the LIBCAMERA_LOG_LEVELS environment * variable that contains a comma-separated list of 'category:level' pairs. * - * The category names are strings and can include a wildcard ('*') character at - * the end to match multiple categories. + * The category names are strings and can include a wildcard ('*') character to + * match multiple categories. * * The level are either numeric values, or strings containing the log level * name. The available log levels are DEBUG, INFO, WARN, ERROR and FATAL. Log @@ -311,15 +314,15 @@ private: void parseLogFile(); void parseLogLevels(); - static LogSeverity parseLogLevel(const std::string &level); + static LogSeverity parseLogLevel(std::string_view level); friend LogCategory; - void registerCategory(LogCategory *category); - LogCategory *findCategory(const char *name) const; + LogCategory *findOrCreateCategory(std::string_view name); static bool destroyed_; - std::vector<LogCategory *> categories_; + Mutex mutex_; + std::vector<std::unique_ptr<LogCategory>> categories_ LIBCAMERA_TSA_GUARDED_BY(mutex_); std::list<std::pair<std::string, LogSeverity>> levels_; std::shared_ptr<LogOutput> output_; @@ -436,9 +439,6 @@ void logSetLevel(const char *category, const char *level) Logger::~Logger() { destroyed_ = true; - - for (LogCategory *category : categories_) - delete category; } /** @@ -569,7 +569,9 @@ void Logger::logSetLevel(const char *category, const char *level) if (severity == LogInvalid) return; - for (LogCategory *c : categories_) { + MutexLocker locker(mutex_); + + for (const auto &c : categories_) { if (c->name() == category) { c->setSeverity(severity); break; @@ -640,17 +642,17 @@ void Logger::parseLogLevels() if (!len) continue; - std::string category; - std::string level; + std::string_view category; + std::string_view level; const char *colon = static_cast<const char *>(memchr(pair, ':', len)); if (!colon) { /* 'x' is a shortcut for '*:x'. */ category = "*"; - level = std::string(pair, len); + level = std::string_view(pair, len); } else { - category = std::string(pair, colon - pair); - level = std::string(colon + 1, comma - colon - 1); + category = std::string_view(pair, colon - pair); + level = std::string_view(colon + 1, comma - colon - 1); } /* Both the category and the level must be specified. */ @@ -661,7 +663,7 @@ void Logger::parseLogLevels() if (severity == LogInvalid) continue; - levels_.push_back({ category, severity }); + levels_.emplace_back(category, severity); } } @@ -675,7 +677,7 @@ void Logger::parseLogLevels() * * \return The log severity, or LogInvalid if the string is invalid */ -LogSeverity Logger::parseLogLevel(const std::string &level) +LogSeverity Logger::parseLogLevel(std::string_view level) { static const char *const names[] = { "DEBUG", @@ -685,15 +687,13 @@ LogSeverity Logger::parseLogLevel(const std::string &level) "FATAL", }; - int severity; + unsigned int severity = LogInvalid; if (std::isdigit(level[0])) { - char *endptr; - severity = strtoul(level.c_str(), &endptr, 10); - if (*endptr != '\0' || severity > LogFatal) + auto [end, ec] = std::from_chars(level.data(), level.data() + level.size(), severity); + if (ec != std::errc() || *end != '\0' || severity > LogFatal) severity = LogInvalid; } else { - severity = LogInvalid; for (unsigned int i = 0; i < std::size(names); ++i) { if (names[i] == level) { severity = i; @@ -706,52 +706,28 @@ LogSeverity Logger::parseLogLevel(const std::string &level) } /** - * \brief Register a log category with the logger - * \param[in] category The log category - * - * Log categories must have unique names. It is invalid to call this function - * if a log category with the same name already exists. + * \brief Find an existing log category with the given name or create one + * \param[in] name Name of the log category + * \return The pointer to the log category found or created */ -void Logger::registerCategory(LogCategory *category) +LogCategory *Logger::findOrCreateCategory(std::string_view name) { - categories_.push_back(category); - - const std::string &name = category->name(); - for (const std::pair<std::string, LogSeverity> &level : levels_) { - bool match = true; - - for (unsigned int i = 0; i < level.first.size(); ++i) { - if (level.first[i] == '*') - break; - - if (i >= name.size() || - name[i] != level.first[i]) { - match = false; - break; - } - } + MutexLocker locker(mutex_); - if (match) { - category->setSeverity(level.second); - break; - } + for (const auto &category : categories_) { + if (category->name() == name) + return category.get(); } -} -/** - * \brief Find an existing log category with the given name - * \param[in] name Name of the log category - * \return The pointer to the found log category or nullptr if not found - */ -LogCategory *Logger::findCategory(const char *name) const -{ - if (auto it = std::find_if(categories_.begin(), categories_.end(), - [name](auto c) { return c->name() == name; }); - it != categories_.end()) { - return *it; + LogCategory *category = categories_.emplace_back(std::unique_ptr<LogCategory>(new LogCategory(name))).get(); + const char *categoryName = category->name().c_str(); + + for (const auto &[pattern, severity] : levels_) { + if (fnmatch(pattern.c_str(), categoryName, FNM_NOESCAPE) == 0) + category->setSeverity(severity); } - return nullptr; + return category; } /** @@ -787,25 +763,16 @@ LogCategory *Logger::findCategory(const char *name) const * * \return The pointer to the LogCategory */ -LogCategory *LogCategory::create(const char *name) +LogCategory *LogCategory::create(std::string_view name) { - static Mutex mutex_; - MutexLocker locker(mutex_); - LogCategory *category = Logger::instance()->findCategory(name); - - if (!category) { - category = new LogCategory(name); - Logger::instance()->registerCategory(category); - } - - return category; + return Logger::instance()->findOrCreateCategory(name); } /** * \brief Construct a log category * \param[in] name The category name */ -LogCategory::LogCategory(const char *name) +LogCategory::LogCategory(std::string_view name) : name_(name), severity_(LogSeverity::LogInfo) { } @@ -824,15 +791,12 @@ LogCategory::LogCategory(const char *name) */ /** + * \fn LogCategory::setSeverity(LogSeverity severity) * \brief Set the severity of the log category * * Messages of severity higher than or equal to the severity of the log category * are printed, other messages are discarded. */ -void LogCategory::setSeverity(LogSeverity severity) -{ - severity_ = severity; -} /** * \brief Retrieve the default log category @@ -874,39 +838,12 @@ const LogCategory &LogCategory::defaultCategory() */ LogMessage::LogMessage(const char *fileName, unsigned int line, const LogCategory &category, LogSeverity severity, - const std::string &prefix) - : category_(category), severity_(severity), prefix_(prefix) + std::string prefix) + : category_(category), severity_(severity), + timestamp_(utils::clock::now()), + fileInfo_(static_cast<std::ostringstream &&>(std::ostringstream() << utils::basename(fileName) << ":" << line).str()), + prefix_(std::move(prefix)) { - init(fileName, line); -} - -/** - * \brief Move-construct a log message - * \param[in] other The other message - * - * The move constructor is meant to support the _log() functions. Thanks to copy - * elision it will likely never be called, but C++11 only permits copy elision, - * it doesn't enforce it unlike C++17. To avoid potential link errors depending - * on the compiler type and version, and optimization level, the move - * constructor is defined even if it will likely never be called, and ensures - * that the destructor of the \a other message will not output anything to the - * log by setting the severity to LogInvalid. - */ -LogMessage::LogMessage(LogMessage &&other) - : msgStream_(std::move(other.msgStream_)), category_(other.category_), - severity_(other.severity_) -{ - other.severity_ = LogInvalid; -} - -void LogMessage::init(const char *fileName, unsigned int line) -{ - /* Log the timestamp, severity and file information. */ - timestamp_ = utils::clock::now(); - - std::ostringstream ossFileInfo; - ossFileInfo << utils::basename(fileName) << ":" << line; - fileInfo_ = ossFileInfo.str(); } LogMessage::~LogMessage() diff --git a/src/libcamera/base/object.cpp b/src/libcamera/base/object.cpp index 745d2565..37d133cc 100644 --- a/src/libcamera/base/object.cpp +++ b/src/libcamera/base/object.cpp @@ -350,7 +350,7 @@ void Object::connect(SignalBase *signal) void Object::disconnect(SignalBase *signal) { - for (auto iter = signals_.begin(); iter != signals_.end(); ) { + for (auto iter = signals_.begin(); iter != signals_.end();) { if (*iter == signal) iter = signals_.erase(iter); else diff --git a/src/libcamera/base/signal.cpp b/src/libcamera/base/signal.cpp index b782e050..7876a21d 100644 --- a/src/libcamera/base/signal.cpp +++ b/src/libcamera/base/signal.cpp @@ -49,7 +49,7 @@ void SignalBase::disconnect(std::function<bool(SlotList::iterator &)> match) { MutexLocker locker(signalsLock); - for (auto iter = slots_.begin(); iter != slots_.end(); ) { + for (auto iter = slots_.begin(); iter != slots_.end();) { if (match(iter)) { Object *object = (*iter)->object(); if (object) diff --git a/src/libcamera/base/thread.cpp b/src/libcamera/base/thread.cpp index f6322fe3..d8fe0d69 100644 --- a/src/libcamera/base/thread.cpp +++ b/src/libcamera/base/thread.cpp @@ -257,6 +257,8 @@ void Thread::start() data_->exit_.store(false, std::memory_order_relaxed); thread_ = std::thread(&Thread::startThread, this); + + setThreadAffinityInternal(); } void Thread::startThread() @@ -284,8 +286,6 @@ void Thread::startThread() data_->tid_ = syscall(SYS_gettid); currentThreadData = data_; - setThreadAffinityInternal(); - run(); } @@ -604,10 +604,12 @@ void Thread::removeMessages(Object *receiver) /** * \brief Dispatch posted messages for this thread * \param[in] type The message type + * \param[in] receiver The receiver whose messages to dispatch * - * This function immediately dispatches all the messages previously posted for - * this thread with postMessage() that match the message \a type. If the \a type - * is Message::Type::None, all messages are dispatched. + * This function immediately dispatches all the messages of the given \a type + * previously posted to this thread for the \a receiver with postMessage(). If + * the \a type is Message::Type::None, all messages types are dispatched. If the + * \a receiver is null, messages to all receivers are dispatched. * * Messages shall only be dispatched from the current thread, typically within * the thread from the run() function. Calling this function outside of the @@ -617,7 +619,7 @@ void Thread::removeMessages(Object *receiver) * same thread from an object's message handler. It guarantees delivery of * messages in the order they have been posted in all cases. */ -void Thread::dispatchMessages(Message::Type type) +void Thread::dispatchMessages(Message::Type type, Object *receiver) { ASSERT(data_ == ThreadData::current()); @@ -634,6 +636,9 @@ void Thread::dispatchMessages(Message::Type type) if (type != Message::Type::None && msg->type() != type) continue; + if (receiver && receiver != msg->receiver_) + continue; + /* * Move the message, setting the entry in the list to null. It * will cause recursive calls to ignore the entry, and the erase @@ -641,12 +646,12 @@ void Thread::dispatchMessages(Message::Type type) */ std::unique_ptr<Message> message = std::move(msg); - Object *receiver = message->receiver_; - ASSERT(data_ == receiver->thread()->data_); - receiver->pendingMessages_--; + Object *messageReceiver = message->receiver_; + ASSERT(data_ == messageReceiver->thread()->data_); + messageReceiver->pendingMessages_--; locker.unlock(); - receiver->message(message.get()); + messageReceiver->message(message.get()); message.reset(); locker.lock(); } @@ -657,7 +662,7 @@ void Thread::dispatchMessages(Message::Type type) * the outer calls. */ if (!--data_->messages_.recursion_) { - for (auto iter = messages.begin(); iter != messages.end(); ) { + for (auto iter = messages.begin(); iter != messages.end();) { if (!*iter) iter = messages.erase(iter); else diff --git a/src/libcamera/base/utils.cpp b/src/libcamera/base/utils.cpp index 67e5a896..bcfc1941 100644 --- a/src/libcamera/base/utils.cpp +++ b/src/libcamera/base/utils.cpp @@ -276,21 +276,6 @@ std::string details::StringSplitter::iterator::operator*() const return ss_->str_.substr(pos_, count); } -bool details::StringSplitter::iterator::operator!=(const details::StringSplitter::iterator &other) const -{ - return pos_ != other.pos_; -} - -details::StringSplitter::iterator details::StringSplitter::begin() const -{ - return iterator(this, 0); -} - -details::StringSplitter::iterator details::StringSplitter::end() const -{ - return iterator(this, std::string::npos); -} - /** * \fn template<typename Container, typename UnaryOp> \ * std::string utils::join(const Container &items, const std::string &sep, UnaryOp op) diff --git a/src/libcamera/bayer_format.cpp b/src/libcamera/bayer_format.cpp index c2120d1c..3dab91fc 100644 --- a/src/libcamera/bayer_format.cpp +++ b/src/libcamera/bayer_format.cpp @@ -225,6 +225,10 @@ const std::unordered_map<unsigned int, BayerFormat> mbusCodeToBayer{ { MEDIA_BUS_FMT_SGBRG16_1X16, { BayerFormat::GBRG, 16, BayerFormat::Packing::None } }, { MEDIA_BUS_FMT_SGRBG16_1X16, { BayerFormat::GRBG, 16, BayerFormat::Packing::None } }, { MEDIA_BUS_FMT_SRGGB16_1X16, { BayerFormat::RGGB, 16, BayerFormat::Packing::None } }, + { MEDIA_BUS_FMT_SBGGR20_1X20, { BayerFormat::BGGR, 20, BayerFormat::Packing::None } }, + { MEDIA_BUS_FMT_SGBRG20_1X20, { BayerFormat::GBRG, 20, BayerFormat::Packing::None } }, + { MEDIA_BUS_FMT_SGRBG20_1X20, { BayerFormat::GRBG, 20, BayerFormat::Packing::None } }, + { MEDIA_BUS_FMT_SRGGB20_1X20, { BayerFormat::RGGB, 20, BayerFormat::Packing::None } }, { MEDIA_BUS_FMT_Y8_1X8, { BayerFormat::MONO, 8, BayerFormat::Packing::None } }, { MEDIA_BUS_FMT_Y10_1X10, { BayerFormat::MONO, 10, BayerFormat::Packing::None } }, { MEDIA_BUS_FMT_Y12_1X12, { BayerFormat::MONO, 12, BayerFormat::Packing::None } }, diff --git a/src/libcamera/camera.cpp b/src/libcamera/camera.cpp index 4c865a46..56c58519 100644 --- a/src/libcamera/camera.cpp +++ b/src/libcamera/camera.cpp @@ -19,6 +19,7 @@ #include <libcamera/base/thread.h> #include <libcamera/color_space.h> +#include <libcamera/control_ids.h> #include <libcamera/framebuffer_allocator.h> #include <libcamera/request.h> #include <libcamera/stream.h> @@ -604,6 +605,11 @@ Camera::Private::~Private() */ /** + * \fn Camera::Private::pipe() const + * \copydoc Camera::Private::pipe() + */ + +/** * \fn Camera::Private::validator() * \brief Retrieve the control validator related to this camera * \return The control validator associated with this camera @@ -1320,6 +1326,25 @@ int Camera::queueRequest(Request *request) } } + /* Pre-process AeEnable. */ + ControlList &controls = request->controls(); + const auto &aeEnable = controls.get(controls::AeEnable); + if (aeEnable) { + if (_d()->controlInfo_.count(controls::AnalogueGainMode.id()) && + !controls.contains(controls::AnalogueGainMode.id())) { + controls.set(controls::AnalogueGainMode, + *aeEnable ? controls::AnalogueGainModeAuto + : controls::AnalogueGainModeManual); + } + + if (_d()->controlInfo_.count(controls::ExposureTimeMode.id()) && + !controls.contains(controls::ExposureTimeMode.id())) { + controls.set(controls::ExposureTimeMode, + *aeEnable ? controls::ExposureTimeModeAuto + : controls::ExposureTimeModeManual); + } + } + d->pipe_->invokeMethod(&PipelineHandler::queueRequest, ConnectionTypeQueued, request); diff --git a/src/libcamera/control_ids.cpp.in b/src/libcamera/control_ids.cpp.in index afe9e2c9..65668d48 100644 --- a/src/libcamera/control_ids.cpp.in +++ b/src/libcamera/control_ids.cpp.in @@ -89,9 +89,9 @@ extern const std::map<std::string, {{ctrl.type}}> {{ctrl.name}}NameValueMap = { { "{{enum.name}}", {{enum.name}} }, {%- endfor %} }; -extern const Control<{{ctrl.type}}> {{ctrl.name}}({{ctrl.name|snake_case|upper}}, "{{ctrl.name}}", "{{vendor}}", {{ctrl.name}}NameValueMap); +extern const Control<{{ctrl.type}}> {{ctrl.name}}({{ctrl.name|snake_case|upper}}, "{{ctrl.name}}", "{{vendor}}", {{ctrl.direction}}, {{ctrl.name}}NameValueMap); {% else -%} -extern const Control<{{ctrl.type}}> {{ctrl.name}}({{ctrl.name|snake_case|upper}}, "{{ctrl.name}}", "{{vendor}}"); +extern const Control<{{ctrl.type}}> {{ctrl.name}}({{ctrl.name|snake_case|upper}}, "{{ctrl.name}}", "{{vendor}}", {{ctrl.direction}}); {% endif -%} {%- endfor %} diff --git a/src/libcamera/control_ids_core.yaml b/src/libcamera/control_ids_core.yaml index d45cf8e5..aa744864 100644 --- a/src/libcamera/control_ids_core.yaml +++ b/src/libcamera/control_ids_core.yaml @@ -10,27 +10,89 @@ vendor: libcamera controls: - AeEnable: type: bool + direction: in description: | - Enable or disable the AE. + Enable or disable the AEGC algorithm. When this control is set to true, + both ExposureTimeMode and AnalogueGainMode are set to auto, and if this + control is set to false then both are set to manual. - \sa ExposureTime AnalogueGain + If ExposureTimeMode or AnalogueGainMode are also set in the same + request as AeEnable, then the modes supplied by ExposureTimeMode or + AnalogueGainMode will take precedence. - - AeLocked: - type: bool + \sa ExposureTimeMode AnalogueGainMode + + - AeState: + type: int32_t + direction: out description: | - Report the lock status of a running AE algorithm. + Report the AEGC algorithm state. - If the AE algorithm is locked the value shall be set to true, if it's - converging it shall be set to false. If the AE algorithm is not - running the control shall not be present in the metadata control list. + The AEGC algorithm computes the exposure time and the analogue gain + to be applied to the image sensor. + + The AEGC algorithm behaviour is controlled by the ExposureTimeMode and + AnalogueGainMode controls, which allow applications to decide how + the exposure time and gain are computed, in Auto or Manual mode, + independently from one another. + + The AeState control reports the AEGC algorithm state through a single + value and describes it as a single computation block which computes + both the exposure time and the analogue gain values. + + When both the exposure time and analogue gain values are configured to + be in Manual mode, the AEGC algorithm is quiescent and does not actively + compute any value and the AeState control will report AeStateIdle. + + When at least the exposure time or analogue gain are configured to be + computed by the AEGC algorithm, the AeState control will report if the + algorithm has converged to stable values for all of the controls set + to be computed in Auto mode. + + \sa AnalogueGainMode + \sa ExposureTimeMode + + enum: + - name: AeStateIdle + value: 0 + description: | + The AEGC algorithm is inactive. + + This state is returned when both AnalogueGainMode and + ExposureTimeMode are set to Manual and the algorithm is not + actively computing any value. + - name: AeStateSearching + value: 1 + description: | + The AEGC algorithm is actively computing new values, for either the + exposure time or the analogue gain, but has not converged to a + stable result yet. + + This state is returned if at least one of AnalogueGainMode or + ExposureTimeMode is auto and the algorithm hasn't converged yet. + + The AEGC algorithm converges once stable values are computed for + all of the controls set to be computed in Auto mode. Once the + algorithm converges the state is moved to AeStateConverged. + - name: AeStateConverged + value: 2 + description: | + The AEGC algorithm has converged. + + This state is returned if at least one of AnalogueGainMode or + ExposureTimeMode is Auto, and the AEGC algorithm has converged to a + stable value. - \sa AeEnable + If the measurements move too far away from the convergence point + then the AEGC algorithm might start adjusting again, in which case + the state is moved to AeStateSearching. # AeMeteringMode needs further attention: # - Auto-generate max enum value. # - Better handling of custom types. - AeMeteringMode: type: int32_t + direction: inout description: | Specify a metering mode for the AE algorithm to use. @@ -56,6 +118,7 @@ controls: # - Better handling of custom types. - AeConstraintMode: type: int32_t + direction: inout description: | Specify a constraint mode for the AE algorithm to use. @@ -98,12 +161,20 @@ controls: # - Better handling of custom types. - AeExposureMode: type: int32_t + direction: inout description: | Specify an exposure mode for the AE algorithm to use. The exposure modes specify how the desired total exposure is divided between the exposure time and the sensor's analogue gain. They are platform specific, and not all exposure modes may be supported. + + When one of AnalogueGainMode or ExposureTimeMode is set to Manual, + the fixed values will override any choices made by AeExposureMode. + + \sa AnalogueGainMode + \sa ExposureTimeMode + enum: - name: ExposureNormal value: 0 @@ -120,59 +191,216 @@ controls: - ExposureValue: type: float + direction: inout description: | Specify an Exposure Value (EV) parameter. The EV parameter will only be applied if the AE algorithm is currently - enabled. + enabled, that is, at least one of AnalogueGainMode and ExposureTimeMode + are in Auto mode. By convention EV adjusts the exposure as log2. For example EV = [-2, -1, -0.5, 0, 0.5, 1, 2] results in an exposure adjustment of [1/4x, 1/2x, 1/sqrt(2)x, 1x, sqrt(2)x, 2x, 4x]. - \sa AeEnable + \sa AnalogueGainMode + \sa ExposureTimeMode - ExposureTime: type: int32_t + direction: inout description: | Exposure time for the frame applied in the sensor device. This value is specified in micro-seconds. - Setting this value means that it is now fixed and the AE algorithm may - not change it. Setting it back to zero returns it to the control of the - AE algorithm. + This control will only take effect if ExposureTimeMode is Manual. If + this control is set when ExposureTimeMode is Auto, the value will be + ignored and will not be retained. + + When reported in metadata, this control indicates what exposure time + was used for the current frame, regardless of ExposureTimeMode. + ExposureTimeMode will indicate the source of the exposure time value, + whether it came from the AE algorithm or not. + + \sa AnalogueGain + \sa ExposureTimeMode + + - ExposureTimeMode: + type: int32_t + direction: inout + description: | + Controls the source of the exposure time that is applied to the image + sensor. + + When set to Auto, the AE algorithm computes the exposure time and + configures the image sensor accordingly. When set to Manual, the value + of the ExposureTime control is used. + + When transitioning from Auto to Manual mode and no ExposureTime control + is provided by the application, the last value computed by the AE + algorithm when the mode was Auto will be used. If the ExposureTimeMode + was never set to Auto (either because the camera started in Manual mode, + or Auto is not supported by the camera), the camera should use a + best-effort default value. + + If ExposureTimeModeManual is supported, the ExposureTime control must + also be supported. + + Cameras that support manual control of the sensor shall support manual + mode for both ExposureTimeMode and AnalogueGainMode, and shall expose + the ExposureTime and AnalogueGain controls. If the camera also has an + AEGC implementation, both ExposureTimeMode and AnalogueGainMode shall + support both manual and auto mode. If auto mode is available, it shall + be the default mode. These rules do not apply to black box cameras + such as UVC cameras, where the available gain and exposure modes are + completely dependent on what the device exposes. + + \par Flickerless exposure mode transitions + + Applications that wish to transition from ExposureTimeModeAuto to direct + control of the exposure time without causing extra flicker can do so by + selecting an ExposureTime value as close as possible to the last value + computed by the auto exposure algorithm in order to avoid any visible + flickering. + + To select the correct value to use as ExposureTime value, applications + should accommodate the natural delay in applying controls caused by the + capture pipeline frame depth. - \sa AnalogueGain AeEnable + When switching to manual exposure mode, applications should not + immediately specify an ExposureTime value in the same request where + ExposureTimeMode is set to Manual. They should instead wait for the + first Request where ExposureTimeMode is reported as + ExposureTimeModeManual in the Request metadata, and use the reported + ExposureTime to populate the control value in the next Request to be + queued to the Camera. - \todo Document the interactions between AeEnable and setting a fixed - value for this control. Consider interactions with other AE features, - such as aperture and aperture/shutter priority mode, and decide if - control of which features should be automatically adjusted shouldn't - better be handled through a separate AE mode control. + The implementation of the auto-exposure algorithm should equally try to + minimize flickering and when transitioning from manual exposure mode to + auto exposure use the last value provided by the application as starting + point. + + 1. Start with ExposureTimeMode set to Auto + + 2. Set ExposureTimeMode to Manual + + 3. Wait for the first completed request that has ExposureTimeMode + set to Manual + + 4. Copy the value reported in ExposureTime into a new request, and + submit it + + 5. Proceed to run manual exposure time as desired + + \sa ExposureTime + enum: + - name: ExposureTimeModeAuto + value: 0 + description: | + The exposure time will be calculated automatically and set by the + AE algorithm. + + If ExposureTime is set while this mode is active, it will be + ignored, and its value will not be retained. + + When transitioning from Manual to Auto mode, the AEGC should start + its adjustments based on the last set manual ExposureTime value. + - name: ExposureTimeModeManual + value: 1 + description: | + The exposure time will not be updated by the AE algorithm. + + When transitioning from Auto to Manual mode, the last computed + exposure value is used until a new value is specified through the + ExposureTime control. If an ExposureTime value is specified in the + same request where the ExposureTimeMode is changed from Auto to + Manual, the provided ExposureTime is applied immediately. - AnalogueGain: type: float + direction: inout description: | Analogue gain value applied in the sensor device. The value of the control specifies the gain multiplier applied to all colour channels. This value cannot be lower than 1.0. - Setting this value means that it is now fixed and the AE algorithm may - not change it. Setting it back to zero returns it to the control of the - AE algorithm. + This control will only take effect if AnalogueGainMode is Manual. If + this control is set when AnalogueGainMode is Auto, the value will be + ignored and will not be retained. + + When reported in metadata, this control indicates what analogue gain + was used for the current request, regardless of AnalogueGainMode. + AnalogueGainMode will indicate the source of the analogue gain value, + whether it came from the AEGC algorithm or not. + + \sa ExposureTime + \sa AnalogueGainMode + + - AnalogueGainMode: + type: int32_t + direction: inout + description: | + Controls the source of the analogue gain that is applied to the image + sensor. + + When set to Auto, the AEGC algorithm computes the analogue gain and + configures the image sensor accordingly. When set to Manual, the value + of the AnalogueGain control is used. + + When transitioning from Auto to Manual mode and no AnalogueGain control + is provided by the application, the last value computed by the AEGC + algorithm when the mode was Auto will be used. If the AnalogueGainMode + was never set to Auto (either because the camera started in Manual mode, + or Auto is not supported by the camera), the camera should use a + best-effort default value. + + If AnalogueGainModeManual is supported, the AnalogueGain control must + also be supported. + + For cameras where we have control over the ISP, both ExposureTimeMode + and AnalogueGainMode are expected to support manual mode, and both + controls (as well as ExposureTimeMode and AnalogueGain) are expected to + be present. If the camera also has an AEGC implementation, both + ExposureTimeMode and AnalogueGainMode shall support both manual and + auto mode. If auto mode is available, it shall be the default mode. + These rules do not apply to black box cameras such as UVC cameras, + where the available gain and exposure modes are completely dependent on + what the hardware exposes. + + The same procedure described for performing flickerless transitions in + the ExposureTimeMode control documentation can be applied to analogue + gain. + + \sa ExposureTimeMode + \sa AnalogueGain + enum: + - name: AnalogueGainModeAuto + value: 0 + description: | + The analogue gain will be calculated automatically and set by the + AEGC algorithm. + + If AnalogueGain is set while this mode is active, it will be + ignored, and it will also not be retained. - \sa ExposureTime AeEnable + When transitioning from Manual to Auto mode, the AEGC should start + its adjustments based on the last set manual AnalogueGain value. + - name: AnalogueGainModeManual + value: 1 + description: | + The analogue gain will not be updated by the AEGC algorithm. - \todo Document the interactions between AeEnable and setting a fixed - value for this control. Consider interactions with other AE features, - such as aperture and aperture/shutter priority mode, and decide if - control of which features should be automatically adjusted shouldn't - better be handled through a separate AE mode control. + When transitioning from Auto to Manual mode, the last computed + gain value is used until a new value is specified through the + AnalogueGain control. If an AnalogueGain value is specified in the + same request where the AnalogueGainMode is changed from Auto to + Manual, the provided AnalogueGain is applied immediately. - AeFlickerMode: type: int32_t + direction: inout description: | Set the flicker avoidance mode for AGC/AEC. @@ -215,6 +443,7 @@ controls: - AeFlickerPeriod: type: int32_t + direction: inout description: | Manual flicker period in microseconds. @@ -235,6 +464,7 @@ controls: - AeFlickerDetected: type: int32_t + direction: out description: | Flicker period detected in microseconds. @@ -257,6 +487,7 @@ controls: - Brightness: type: float + direction: inout description: | Specify a fixed brightness parameter. @@ -265,6 +496,7 @@ controls: - Contrast: type: float + direction: inout description: | Specify a fixed contrast parameter. @@ -273,6 +505,7 @@ controls: - Lux: type: float + direction: out description: | Report an estimate of the current illuminance level in lux. @@ -280,16 +513,30 @@ controls: - AwbEnable: type: bool + direction: inout description: | Enable or disable the AWB. + When AWB is enabled, the algorithm estimates the colour temperature of + the scene and computes colour gains and the colour correction matrix + automatically. The computed colour temperature, gains and correction + matrix are reported in metadata. The corresponding controls are ignored + if set in a request. + + When AWB is disabled, the colour temperature, gains and correction + matrix are not updated automatically and can be set manually in + requests. + + \sa ColourCorrectionMatrix \sa ColourGains + \sa ColourTemperature # AwbMode needs further attention: # - Auto-generate max enum value. # - Better handling of custom types. - AwbMode: type: int32_t + direction: inout description: | Specify the range of illuminants to use for the AWB algorithm. @@ -323,6 +570,7 @@ controls: - AwbLocked: type: bool + direction: out description: | Report the lock status of a running AWB algorithm. @@ -334,24 +582,44 @@ controls: - ColourGains: type: float + direction: inout description: | Pair of gain values for the Red and Blue colour channels, in that order. ColourGains can only be applied in a Request when the AWB is disabled. + If ColourGains is set in a request but ColourTemperature is not, the + implementation shall calculate and set the ColourTemperature based on + the ColourGains. \sa AwbEnable + \sa ColourTemperature size: [2] - ColourTemperature: type: int32_t + direction: out description: | - Report the estimate of the colour temperature for the frame, in kelvin. + ColourTemperature of the frame, in kelvin. + + ColourTemperature can only be applied in a Request when the AWB is + disabled. - The ColourTemperature control can only be returned in metadata. + If ColourTemperature is set in a request but ColourGains is not, the + implementation shall calculate and set the ColourGains based on the + given ColourTemperature. If ColourTemperature is set (either directly, + or indirectly by setting ColourGains) but ColourCorrectionMatrix is not, + the ColourCorrectionMatrix is updated based on the ColourTemperature. + + The ColourTemperature used to process the frame is reported in metadata. + + \sa AwbEnable + \sa ColourCorrectionMatrix + \sa ColourGains - Saturation: type: float + direction: inout description: | Specify a fixed saturation parameter. @@ -360,6 +628,7 @@ controls: - SensorBlackLevels: type: int32_t + direction: out description: | Reports the sensor black levels used for processing a frame. @@ -370,6 +639,7 @@ controls: - Sharpness: type: float + direction: inout description: | Intensity of the sharpening applied to the image. @@ -384,6 +654,7 @@ controls: - FocusFoM: type: int32_t + direction: out description: | Reports a Figure of Merit (FoM) to indicate how in-focus the frame is. @@ -396,6 +667,7 @@ controls: - ColourCorrectionMatrix: type: float + direction: inout description: | The 3x3 matrix that converts camera RGB to sRGB within the imaging pipeline. @@ -405,10 +677,16 @@ controls: stored in conventional reading order in an array of 9 floating point values. + ColourCorrectionMatrix can only be applied in a Request when the AWB is + disabled. + + \sa AwbEnable + \sa ColourTemperature size: [3,3] - ScalerCrop: type: Rectangle + direction: inout description: | Sets the image portion that will be scaled to form the whole of the final output image. @@ -424,6 +702,7 @@ controls: - DigitalGain: type: float + direction: inout description: | Digital gain value applied during the processing steps applied to the image as captured from the sensor. @@ -441,6 +720,7 @@ controls: - FrameDuration: type: int64_t + direction: out description: | The instantaneous frame duration from start of frame exposure to start of next exposure, expressed in microseconds. @@ -449,6 +729,7 @@ controls: - FrameDurationLimits: type: int64_t + direction: inout description: | The minimum and maximum (in that order) frame duration, expressed in microseconds. @@ -485,6 +766,7 @@ controls: - SensorTemperature: type: float + direction: out description: | Temperature measure from the camera sensor in Celsius. @@ -497,6 +779,7 @@ controls: - SensorTimestamp: type: int64_t + direction: out description: | The time when the first row of the image sensor active array is exposed. @@ -511,6 +794,7 @@ controls: - AfMode: type: int32_t + direction: inout description: | The mode of the AF (autofocus) algorithm. @@ -575,6 +859,7 @@ controls: - AfRange: type: int32_t + direction: inout description: | The range of focus distances that is scanned. @@ -602,6 +887,7 @@ controls: - AfSpeed: type: int32_t + direction: inout description: | Determine whether the AF is to move the lens as quickly as possible or more steadily. @@ -620,6 +906,7 @@ controls: - AfMetering: type: int32_t + direction: inout description: | The parts of the image used by the AF algorithm to measure focus. enum: @@ -636,6 +923,7 @@ controls: - AfWindows: type: Rectangle + direction: inout description: | The focus windows used by the AF algorithm when AfMetering is set to AfMeteringWindows. @@ -665,6 +953,7 @@ controls: - AfTrigger: type: int32_t + direction: in description: | Start an autofocus scan. @@ -690,6 +979,7 @@ controls: - AfPause: type: int32_t + direction: in description: | Pause lens movements when in continuous autofocus mode. @@ -734,6 +1024,7 @@ controls: - LensPosition: type: float + direction: inout description: | Set and report the focus lens position. @@ -768,6 +1059,7 @@ controls: - AfState: type: int32_t + direction: out description: | The current state of the AF algorithm. @@ -825,6 +1117,7 @@ controls: - AfPauseState: type: int32_t + direction: out description: | Report whether the autofocus is currently running, paused or pausing. @@ -860,6 +1153,7 @@ controls: - HdrMode: type: int32_t + direction: inout description: | Set the mode to be used for High Dynamic Range (HDR) imaging. @@ -926,6 +1220,7 @@ controls: - HdrChannel: type: int32_t + direction: out description: | The HDR channel used to capture the frame. @@ -960,6 +1255,7 @@ controls: - Gamma: type: float + direction: inout description: | Specify a fixed gamma value. @@ -968,6 +1264,7 @@ controls: - DebugMetadataEnable: type: bool + direction: inout description: | Enable or disable the debug metadata. diff --git a/src/libcamera/control_ids_draft.yaml b/src/libcamera/control_ids_draft.yaml index 1b284257..03309eea 100644 --- a/src/libcamera/control_ids_draft.yaml +++ b/src/libcamera/control_ids_draft.yaml @@ -10,6 +10,7 @@ vendor: draft controls: - AePrecaptureTrigger: type: int32_t + direction: inout description: | Control for AE metering trigger. Currently identical to ANDROID_CONTROL_AE_PRECAPTURE_TRIGGER. @@ -31,6 +32,7 @@ controls: - NoiseReductionMode: type: int32_t + direction: inout description: | Control to select the noise reduction algorithm mode. Currently identical to ANDROID_NOISE_REDUCTION_MODE. @@ -59,6 +61,7 @@ controls: - ColorCorrectionAberrationMode: type: int32_t + direction: inout description: | Control to select the color correction aberration mode. Currently identical to ANDROID_COLOR_CORRECTION_ABERRATION_MODE. @@ -77,37 +80,9 @@ controls: High quality aberration correction which might reduce the frame rate. - - AeState: - type: int32_t - description: | - Control to report the current AE algorithm state. Currently identical to - ANDROID_CONTROL_AE_STATE. - - Current state of the AE algorithm. - enum: - - name: AeStateInactive - value: 0 - description: The AE algorithm is inactive. - - name: AeStateSearching - value: 1 - description: The AE algorithm has not converged yet. - - name: AeStateConverged - value: 2 - description: The AE algorithm has converged. - - name: AeStateLocked - value: 3 - description: The AE algorithm is locked. - - name: AeStateFlashRequired - value: 4 - description: The AE algorithm would need a flash for good results - - name: AeStatePrecapture - value: 5 - description: | - The AE algorithm has started a pre-capture metering session. - \sa AePrecaptureTrigger - - AwbState: type: int32_t + direction: out description: | Control to report the current AWB algorithm state. Currently identical to ANDROID_CONTROL_AWB_STATE. @@ -129,6 +104,7 @@ controls: - SensorRollingShutterSkew: type: int64_t + direction: out description: | Control to report the time between the start of exposure of the first row and the start of exposure of the last row. Currently identical to @@ -136,6 +112,7 @@ controls: - LensShadingMapMode: type: int32_t + direction: inout description: | Control to report if the lens shading map is available. Currently identical to ANDROID_STATISTICS_LENS_SHADING_MAP_MODE. @@ -149,6 +126,7 @@ controls: - PipelineDepth: type: int32_t + direction: out description: | Specifies the number of pipeline stages the frame went through from when it was exposed to when the final completed result was available to the @@ -163,6 +141,7 @@ controls: - MaxLatency: type: int32_t + direction: out description: | The maximum number of frames that can occur after a request (different than the previous) has been submitted, and before the result's state @@ -172,6 +151,7 @@ controls: - TestPatternMode: type: int32_t + direction: inout description: | Control to select the test pattern mode. Currently identical to ANDROID_SENSOR_TEST_PATTERN_MODE. @@ -229,6 +209,7 @@ controls: - FaceDetectMode: type: int32_t + direction: inout description: | Control to select the face detection mode used by the pipeline. @@ -262,6 +243,7 @@ controls: - FaceDetectFaceRectangles: type: Rectangle + direction: out description: | Boundary rectangles of the detected faces. The number of values is the number of detected faces. @@ -273,6 +255,7 @@ controls: - FaceDetectFaceScores: type: uint8_t + direction: out description: | Confidence score of each of the detected faces. The range of score is [0, 100]. The number of values should be the number of faces reported @@ -285,6 +268,7 @@ controls: - FaceDetectFaceLandmarks: type: Point + direction: out description: | Array of human face landmark coordinates in format [..., left_eye_i, right_eye_i, mouth_i, left_eye_i+1, ...], with i = index of face. The @@ -298,6 +282,7 @@ controls: - FaceDetectFaceIds: type: int32_t + direction: out description: | Each detected face is given a unique ID that is valid for as long as the face is visible to the camera device. A face that leaves the field of diff --git a/src/libcamera/control_ids_rpi.yaml b/src/libcamera/control_ids_rpi.yaml index 34bbdfc8..7524c5d2 100644 --- a/src/libcamera/control_ids_rpi.yaml +++ b/src/libcamera/control_ids_rpi.yaml @@ -9,6 +9,7 @@ vendor: rpi controls: - StatsOutputEnable: type: bool + direction: inout description: | Toggles the Raspberry Pi IPA to output the hardware generated statistics. @@ -21,6 +22,7 @@ controls: - Bcm2835StatsOutput: type: uint8_t size: [n] + direction: out description: | Span of the BCM2835 ISP generated statistics for the current frame. @@ -33,6 +35,7 @@ controls: - ScalerCrops: type: Rectangle size: [n] + direction: out description: | An array of rectangles, where each singular value has identical functionality to the ScalerCrop control. This control allows the diff --git a/src/libcamera/control_serializer.cpp b/src/libcamera/control_serializer.cpp index 0a5e8220..17834648 100644 --- a/src/libcamera/control_serializer.cpp +++ b/src/libcamera/control_serializer.cpp @@ -281,6 +281,7 @@ int ControlSerializer::serialize(const ControlInfoMap &infoMap, entry.id = id->id(); entry.type = id->type(); entry.offset = values.offset(); + entry.direction = static_cast<ControlId::DirectionFlags::Type>(id->direction()); entries.write(&entry); store(info, values); @@ -493,12 +494,17 @@ ControlInfoMap ControlSerializer::deserialize<ControlInfoMap>(ByteStreamBuffer & /* If we're using a local id map, populate it. */ if (localIdMap) { + ControlId::DirectionFlags flags{ + static_cast<ControlId::Direction>(entry->direction) + }; + /** * \todo Find a way to preserve the control name for * debugging purpose. */ controlIds_.emplace_back(std::make_unique<ControlId>(entry->id, - "", "local", type)); + "", "local", type, + flags)); (*localIdMap)[entry->id] = controlIds_.back().get(); } diff --git a/src/libcamera/controls.cpp b/src/libcamera/controls.cpp index 65eeef2d..70f6f609 100644 --- a/src/libcamera/controls.cpp +++ b/src/libcamera/controls.cpp @@ -412,15 +412,16 @@ void ControlValue::reserve(ControlType type, bool isArray, std::size_t numElemen * \param[in] name The control name * \param[in] vendor The vendor name * \param[in] type The control data type + * \param[in] direction The direction of the control, if it can be used in Controls or Metadata * \param[in] size The size of the array control, or 0 if scalar control * \param[in] enumStrMap The map from enum names to values (optional) */ ControlId::ControlId(unsigned int id, const std::string &name, const std::string &vendor, ControlType type, - std::size_t size, + DirectionFlags direction, std::size_t size, const std::map<std::string, int32_t> &enumStrMap) - : id_(id), name_(name), vendor_(vendor), type_(type), size_(size), - enumStrMap_(enumStrMap) + : id_(id), name_(name), vendor_(vendor), type_(type), + direction_(direction), size_(size), enumStrMap_(enumStrMap) { for (const auto &pair : enumStrMap_) reverseMap_[pair.second] = pair.first; @@ -451,6 +452,37 @@ ControlId::ControlId(unsigned int id, const std::string &name, */ /** + * \fn DirectionFlags ControlId::direction() const + * \brief Return the direction that the control can be used in + * + * This is similar to \sa isInput() and \sa isOutput(), but returns the flags + * direction instead of booleans for each direction. + * + * \return The direction flags corresponding to if the control can be used as + * an input control or as output metadata + */ + +/** + * \fn bool ControlId::isInput() const + * \brief Determine if the control is available to be used as an input control + * + * Controls can be used either as input in controls, or as output in metadata. + * This function checks if the control is allowed to be used as the former. + * + * \return True if the control can be used as an input control, false otherwise + */ + +/** + * \fn bool ControlId::isOutput() const + * \brief Determine if the control is available to be used in output metadata + * + * Controls can be used either as input in controls, or as output in metadata. + * This function checks if the control is allowed to be used as the latter. + * + * \return True if the control can be returned in output metadata, false otherwise + */ + +/** * \fn bool ControlId::isArray() const * \brief Determine if the control is an array control * \return True if the control is an array control, false otherwise @@ -488,6 +520,22 @@ ControlId::ControlId(unsigned int id, const std::string &name, */ /** + * \enum ControlId::Direction + * \brief The direction the control is capable of being passed from/to + * + * \var ControlId::Direction::In + * \brief The control can be passed as input in controls + * + * \var ControlId::Direction::Out + * \brief The control can be returned as output in metadata + */ + +/** + * \typedef ControlId::DirectionFlags + * \brief A wrapper for ControlId::Direction so that it can be used as flags + */ + +/** * \class Control * \brief Describe a control and its intrinsic properties * @@ -520,6 +568,8 @@ ControlId::ControlId(unsigned int id, const std::string &name, * \param[in] id The control numerical ID * \param[in] name The control name * \param[in] vendor The vendor name + * \param[in] direction The direction of the control, if it can be used in + * Controls or Metadata * \param[in] enumStrMap The map from enum names to values (optional) * * The control data type is automatically deduced from the template type T. diff --git a/src/libcamera/converter.cpp b/src/libcamera/converter.cpp index 3a3f8434..d551b908 100644 --- a/src/libcamera/converter.cpp +++ b/src/libcamera/converter.cpp @@ -51,6 +51,16 @@ LOG_DEFINE_CATEGORY(Converter) */ /** + * \enum Converter::Alignment + * \brief The alignment mode specified when adjusting the converter input or + * output sizes + * \var Converter::Alignment::Down + * \brief Adjust the Converter sizes to a smaller valid size + * \var Converter::Alignment::Up + * \brief Adjust the Converter sizes to a larger valid size + */ + +/** * \brief Construct a Converter instance * \param[in] media The media device implementing the converter * \param[in] features Features flags representing supported features @@ -111,6 +121,26 @@ Converter::~Converter() */ /** + * \fn Converter::adjustInputSize() + * \brief Adjust the converter input \a size to a valid value + * \param[in] pixFmt The pixel format of the converter input stream + * \param[in] size The converter input size to adjust to a valid value + * \param[in] align The desired alignment + * \return The adjusted converter input size or a null Size if \a size cannot + * be adjusted + */ + +/** + * \fn Converter::adjustOutputSize() + * \brief Adjust the converter output \a size to a valid value + * \param[in] pixFmt The pixel format of the converter output stream + * \param[in] size The converter output size to adjust to a valid value + * \param[in] align The desired alignment + * \return The adjusted converter output size or a null Size if \a size cannot + * be adjusted + */ + +/** * \fn Converter::strideAndFrameSize() * \brief Retrieve the output stride and frame size for an input configutation * \param[in] pixelFormat Input stream pixel format @@ -119,6 +149,16 @@ Converter::~Converter() */ /** + * \fn Converter::validateOutput() + * \brief Validate and possibily adjust \a cfg to a valid converter output + * \param[inout] cfg The StreamConfiguration to validate and adjust + * \param[out] adjusted Set to true if \a cfg has been adjusted + * \param[in] align The desired alignment + * \return 0 if \a cfg is valid or has been adjusted, a negative error code + * otherwise if \a cfg cannot be adjusted + */ + +/** * \fn Converter::configure() * \brief Configure a set of output stream conversion from an input stream * \param[in] inputCfg Input stream configuration @@ -127,6 +167,13 @@ Converter::~Converter() */ /** + * \fn Converter::isConfigured() + * \brief Check if a given stream is configured + * \param[in] stream The output stream + * \return True if the \a stream is configured or false otherwise + */ + +/** * \fn Converter::exportBuffers() * \brief Export buffers from the converter device * \param[in] stream Output stream pointer exporting the buffers @@ -185,6 +232,16 @@ Converter::~Converter() /** * \fn Converter::inputCropBounds() + * \brief Retrieve the crop bounds of the converter + * + * Retrieve the minimum and maximum crop bounds of the converter. This can be + * used to query the crop bounds before configuring a stream. + * + * \return A pair containing the minimum and maximum crop bound in that order + */ + +/** + * \fn Converter::inputCropBounds(const Stream *stream) * \brief Retrieve the crop bounds for \a stream * \param[in] stream The output stream * @@ -195,6 +252,9 @@ Converter::~Converter() * this function should be called after the \a stream has been configured using * configure(). * + * When called with an unconfigured \a stream, this function returns a pair of + * null rectangles. + * * \return A pair containing the minimum and maximum crop bound in that order */ diff --git a/src/libcamera/converter/converter_v4l2_m2m.cpp b/src/libcamera/converter/converter_v4l2_m2m.cpp index d63ef2f8..ee05b798 100644 --- a/src/libcamera/converter/converter_v4l2_m2m.cpp +++ b/src/libcamera/converter/converter_v4l2_m2m.cpp @@ -8,6 +8,7 @@ #include "libcamera/internal/converter/converter_v4l2_m2m.h" +#include <algorithm> #include <limits.h> #include <libcamera/base/log.h> @@ -30,6 +31,52 @@ namespace libcamera { LOG_DECLARE_CATEGORY(Converter) +namespace { + +int getCropBounds(V4L2VideoDevice *device, Rectangle &minCrop, + Rectangle &maxCrop) +{ + Rectangle minC; + Rectangle maxC; + + /* Find crop bounds */ + minC.width = 1; + minC.height = 1; + maxC.width = UINT_MAX; + maxC.height = UINT_MAX; + + int ret = device->setSelection(V4L2_SEL_TGT_CROP, &minC); + if (ret) { + LOG(Converter, Error) + << "Could not query minimum selection crop: " + << strerror(-ret); + return ret; + } + + ret = device->getSelection(V4L2_SEL_TGT_CROP_BOUNDS, &maxC); + if (ret) { + LOG(Converter, Error) + << "Could not query maximum selection crop: " + << strerror(-ret); + return ret; + } + + /* Reset the crop to its maximum */ + ret = device->setSelection(V4L2_SEL_TGT_CROP, &maxC); + if (ret) { + LOG(Converter, Error) + << "Could not reset selection crop: " + << strerror(-ret); + return ret; + } + + minCrop = minC; + maxCrop = maxC; + return 0; +} + +} /* namespace */ + /* ----------------------------------------------------------------------------- * V4L2M2MConverter::V4L2M2MStream */ @@ -98,41 +145,10 @@ int V4L2M2MConverter::V4L2M2MStream::configure(const StreamConfiguration &inputC outputBufferCount_ = outputCfg.bufferCount; if (converter_->features() & Feature::InputCrop) { - Rectangle minCrop; - Rectangle maxCrop; - - /* Find crop bounds */ - minCrop.width = 1; - minCrop.height = 1; - maxCrop.width = UINT_MAX; - maxCrop.height = UINT_MAX; - - ret = setInputSelection(V4L2_SEL_TGT_CROP, &minCrop); - if (ret) { - LOG(Converter, Error) - << "Could not query minimum selection crop: " - << strerror(-ret); - return ret; - } - - ret = getInputSelection(V4L2_SEL_TGT_CROP_BOUNDS, &maxCrop); - if (ret) { - LOG(Converter, Error) - << "Could not query maximum selection crop: " - << strerror(-ret); + ret = getCropBounds(m2m_->output(), inputCropBounds_.first, + inputCropBounds_.second); + if (ret) return ret; - } - - /* Reset the crop to its maximum */ - ret = setInputSelection(V4L2_SEL_TGT_CROP, &maxCrop); - if (ret) { - LOG(Converter, Error) - << "Could not reset selection crop: " - << strerror(-ret); - return ret; - } - - inputCropBounds_ = { minCrop, maxCrop }; } return 0; @@ -258,27 +274,9 @@ V4L2M2MConverter::V4L2M2MConverter(MediaDevice *media) return; } - /* Discover Feature::InputCrop */ - Rectangle maxCrop; - maxCrop.width = UINT_MAX; - maxCrop.height = UINT_MAX; - - ret = m2m_->output()->setSelection(V4L2_SEL_TGT_CROP, &maxCrop); - if (ret) - return; - - /* - * Rectangles for cropping targets are defined even if the device - * does not support cropping. Their sizes and positions will be - * fixed in such cases. - * - * Set and inspect a crop equivalent to half of the maximum crop - * returned earlier. Use this to determine whether the crop on - * input is really supported. - */ - Rectangle halfCrop(maxCrop.size() / 2); - ret = m2m_->output()->setSelection(V4L2_SEL_TGT_CROP, &halfCrop); - if (!ret && halfCrop != maxCrop) { + ret = getCropBounds(m2m_->output(), inputCropBounds_.first, + inputCropBounds_.second); + if (!ret && inputCropBounds_.first != inputCropBounds_.second) { features_ |= Feature::InputCrop; LOG(Converter, Info) @@ -404,6 +402,127 @@ V4L2M2MConverter::strideAndFrameSize(const PixelFormat &pixelFormat, } /** + * \copydoc libcamera::Converter::adjustInputSize + */ +Size V4L2M2MConverter::adjustInputSize(const PixelFormat &pixFmt, + const Size &size, Alignment align) +{ + auto formats = m2m_->output()->formats(); + V4L2PixelFormat v4l2PixFmt = m2m_->output()->toV4L2PixelFormat(pixFmt); + + auto it = formats.find(v4l2PixFmt); + if (it == formats.end()) { + LOG(Converter, Info) + << "Unsupported pixel format " << pixFmt; + return {}; + } + + return adjustSizes(size, it->second, align); +} + +/** + * \copydoc libcamera::Converter::adjustOutputSize + */ +Size V4L2M2MConverter::adjustOutputSize(const PixelFormat &pixFmt, + const Size &size, Alignment align) +{ + auto formats = m2m_->capture()->formats(); + V4L2PixelFormat v4l2PixFmt = m2m_->capture()->toV4L2PixelFormat(pixFmt); + + auto it = formats.find(v4l2PixFmt); + if (it == formats.end()) { + LOG(Converter, Info) + << "Unsupported pixel format " << pixFmt; + return {}; + } + + return adjustSizes(size, it->second, align); +} + +Size V4L2M2MConverter::adjustSizes(const Size &cfgSize, + const std::vector<SizeRange> &ranges, + Alignment align) +{ + Size size = cfgSize; + + if (ranges.size() == 1) { + /* + * The device supports either V4L2_FRMSIZE_TYPE_CONTINUOUS or + * V4L2_FRMSIZE_TYPE_STEPWISE. + */ + const SizeRange &range = *ranges.begin(); + + size.width = std::clamp(size.width, range.min.width, + range.max.width); + size.height = std::clamp(size.height, range.min.height, + range.max.height); + + /* + * Check if any alignment is needed. If the sizes are already + * aligned, or the device supports V4L2_FRMSIZE_TYPE_CONTINUOUS + * with hStep and vStep equal to 1, we're done here. + */ + int widthR = size.width % range.hStep; + int heightR = size.height % range.vStep; + + /* Align up or down according to the caller request. */ + + if (widthR != 0) + size.width = size.width - widthR + + ((align == Alignment::Up) ? range.hStep : 0); + + if (heightR != 0) + size.height = size.height - heightR + + ((align == Alignment::Up) ? range.vStep : 0); + } else { + /* + * The device supports V4L2_FRMSIZE_TYPE_DISCRETE, find the + * size closer to the requested output configuration. + * + * The size ranges vector is not ordered, so we sort it first. + * If we align up, start from the larger element. + */ + std::vector<Size> sizes(ranges.size()); + std::transform(ranges.begin(), ranges.end(), std::back_inserter(sizes), + [](const SizeRange &range) { return range.max; }); + std::sort(sizes.begin(), sizes.end()); + + if (align == Alignment::Up) + std::reverse(sizes.begin(), sizes.end()); + + /* + * Return true if s2 is valid according to the desired + * alignment: smaller than s1 if we align down, larger than s1 + * if we align up. + */ + auto nextSizeValid = [](const Size &s1, const Size &s2, Alignment a) { + return a == Alignment::Down + ? (s1.width > s2.width && s1.height > s2.height) + : (s1.width < s2.width && s1.height < s2.height); + }; + + Size newSize; + for (const Size &sz : sizes) { + if (!nextSizeValid(size, sz, align)) + break; + + newSize = sz; + } + + if (newSize.isNull()) { + LOG(Converter, Error) + << "Cannot adjust " << cfgSize + << " to a supported converter size"; + return {}; + } + + size = newSize; + } + + return size; +} + +/** * \copydoc libcamera::Converter::configure */ int V4L2M2MConverter::configure(const StreamConfiguration &inputCfg, @@ -441,6 +560,14 @@ int V4L2M2MConverter::configure(const StreamConfiguration &inputCfg, } /** + * \copydoc libcamera::Converter::isConfigured + */ +bool V4L2M2MConverter::isConfigured(const Stream *stream) const +{ + return streams_.find(stream) != streams_.end(); +} + +/** * \copydoc libcamera::Converter::exportBuffers */ int V4L2M2MConverter::exportBuffers(const Stream *stream, unsigned int count, @@ -471,14 +598,21 @@ int V4L2M2MConverter::setInputCrop(const Stream *stream, Rectangle *rect) } /** - * \copydoc libcamera::Converter::inputCropBounds + * \fn libcamera::V4L2M2MConverter::inputCropBounds() + * \copydoc libcamera::Converter::inputCropBounds() + */ + +/** + * \copydoc libcamera::Converter::inputCropBounds(const Stream *stream) */ std::pair<Rectangle, Rectangle> V4L2M2MConverter::inputCropBounds(const Stream *stream) { auto iter = streams_.find(stream); - if (iter == streams_.end()) + if (iter == streams_.end()) { + LOG(Converter, Error) << "Invalid output stream"; return {}; + } return iter->second->inputCropBounds(); } @@ -511,6 +645,53 @@ void V4L2M2MConverter::stop() } /** + * \copydoc libcamera::Converter::validateOutput + */ +int V4L2M2MConverter::validateOutput(StreamConfiguration *cfg, bool *adjusted, + Alignment align) +{ + V4L2VideoDevice *capture = m2m_->capture(); + V4L2VideoDevice::Formats fmts = capture->formats(); + + if (adjusted) + *adjusted = false; + + PixelFormat fmt = cfg->pixelFormat; + V4L2PixelFormat v4l2PixFmt = capture->toV4L2PixelFormat(fmt); + + auto it = fmts.find(v4l2PixFmt); + if (it == fmts.end()) { + it = fmts.begin(); + v4l2PixFmt = it->first; + cfg->pixelFormat = v4l2PixFmt.toPixelFormat(); + + if (adjusted) + *adjusted = true; + + LOG(Converter, Info) + << "Converter output pixel format adjusted to " + << cfg->pixelFormat; + } + + const Size cfgSize = cfg->size; + cfg->size = adjustSizes(cfgSize, it->second, align); + + if (cfg->size.isNull()) + return -EINVAL; + + if (cfg->size.width != cfgSize.width || + cfg->size.height != cfgSize.height) { + LOG(Converter, Info) + << "Converter size adjusted to " + << cfg->size; + if (adjusted) + *adjusted = true; + } + + return 0; +} + +/** * \copydoc libcamera::Converter::queueBuffers */ int V4L2M2MConverter::queueBuffers(FrameBuffer *input, @@ -557,7 +738,7 @@ int V4L2M2MConverter::queueBuffers(FrameBuffer *input, } /* - * \todo: This should be extended to include Feature::Flag to denote + * \todo This should be extended to include Feature::Flag to denote * what each converter supports feature-wise. */ static std::initializer_list<std::string> compatibles = { diff --git a/src/libcamera/dma_buf_allocator.cpp b/src/libcamera/dma_buf_allocator.cpp index 3cc52f96..d8c62dd6 100644 --- a/src/libcamera/dma_buf_allocator.cpp +++ b/src/libcamera/dma_buf_allocator.cpp @@ -311,9 +311,26 @@ DmaSyncer::DmaSyncer(SharedFD fd, SyncType type) sync(DMA_BUF_SYNC_START); } +/** + * \fn DmaSyncer::DmaSyncer(DmaSyncer &&other); + * \param[in] other The other instance + * \brief Enable move on class DmaSyncer + */ + +/** + * \fn DmaSyncer::operator=(DmaSyncer &&other); + * \param[in] other The other instance + * \brief Enable move on class DmaSyncer + */ + DmaSyncer::~DmaSyncer() { - sync(DMA_BUF_SYNC_END); + /* + * DmaSyncer might be moved and left with an empty SharedFD. + * Avoid syncing with an invalid file descriptor in this case. + */ + if (fd_.isValid()) + sync(DMA_BUF_SYNC_END); } void DmaSyncer::sync(uint64_t step) diff --git a/src/libcamera/geometry.cpp b/src/libcamera/geometry.cpp index 90ccf8c1..81cc8cd5 100644 --- a/src/libcamera/geometry.cpp +++ b/src/libcamera/geometry.cpp @@ -838,6 +838,55 @@ Rectangle Rectangle::translatedBy(const Point &point) const } /** + * \brief Transform a Rectangle from one reference rectangle to another + * \param[in] source The \a source reference rectangle + * \param[in] destination The \a destination reference rectangle + * + * The \a source and \a destination parameters describe two rectangles defined + * in different reference systems. The Rectangle is translated from the source + * reference system into the destination reference system. + * + * The typical use case for this function is to translate a selection rectangle + * specified in a reference system, in example the sensor's pixel array, into + * the same rectangle re-scaled and translated into a different reference + * system, in example the output frame on which the selection rectangle is + * applied to. + * + * For example, consider a sensor with a resolution of 4040x2360 pixels and a + * assume a rectangle of (100, 100)/3840x2160 (sensorFrame) in sensor + * coordinates is mapped to a rectangle (0,0)/(1920,1080) (displayFrame) in + * display coordinates. This function can be used to transform an arbitrary + * rectangle from display coordinates to sensor coordinates or vice versa: + * + * \code{.cpp} + * Rectangle sensorReference(100, 100, 3840, 2160); + * Rectangle displayReference(0, 0, 1920, 1080); + * + * // Bottom right quarter in sensor coordinates + * Rectangle sensorRect(2020, 100, 1920, 1080); + * displayRect = sensorRect.transformedBetween(sensorReference, displayReference); + * // displayRect is now (960, 540)/960x540 + * + * // Transformation back to sensor coordinates + * sensorRect = displayRect.transformedBetween(displayReference, sensorReference); + * \endcode + */ +Rectangle Rectangle::transformedBetween(const Rectangle &source, + const Rectangle &destination) const +{ + Rectangle r; + double sx = static_cast<double>(destination.width) / source.width; + double sy = static_cast<double>(destination.height) / source.height; + + r.x = static_cast<int>((x - source.x) * sx) + destination.x; + r.y = static_cast<int>((y - source.y) * sy) + destination.y; + r.width = static_cast<int>(width * sx); + r.height = static_cast<int>(height * sy); + + return r; +} + +/** * \brief Compare rectangles for equality * \return True if the two rectangles are equal, false otherwise */ diff --git a/src/libcamera/ipa_controls.cpp b/src/libcamera/ipa_controls.cpp index 9420c889..12d92ebe 100644 --- a/src/libcamera/ipa_controls.cpp +++ b/src/libcamera/ipa_controls.cpp @@ -220,6 +220,10 @@ static_assert(sizeof(ipa_control_value_entry) == 16, * \var ipa_control_info_entry::offset * The offset in bytes from the beginning of the data section to the control * info data (shall be a multiple of 8 bytes) + * \var ipa_control_info_entry::direction + * The directions in which the control is allowed to be sent. This is a flags + * value, where 0x1 signifies input (as controls), and 0x2 signifies output (as + * metadata). \sa ControlId::Direction * \var ipa_control_info_entry::padding * Padding bytes (shall be set to 0) */ diff --git a/src/libcamera/ipa_proxy.cpp b/src/libcamera/ipa_proxy.cpp index 85004737..9907b961 100644 --- a/src/libcamera/ipa_proxy.cpp +++ b/src/libcamera/ipa_proxy.cpp @@ -98,16 +98,33 @@ IPAProxy::~IPAProxy() std::string IPAProxy::configurationFile(const std::string &name, const std::string &fallbackName) const { - struct stat statbuf; - int ret; - /* * The IPA module name can be used as-is to build directory names as it * has been validated when loading the module. */ - std::string ipaName = ipam_->info().name; + const std::string ipaName = ipam_->info().name; + + /* + * Start with any user override through the module-specific environment + * variable. Use the name of the IPA module up to the first '/' to + * construct the variable name. + */ + std::string ipaEnvName = ipaName.substr(0, ipaName.find('/')); + std::transform(ipaEnvName.begin(), ipaEnvName.end(), ipaEnvName.begin(), + [](unsigned char c) { return std::toupper(c); }); + ipaEnvName = "LIBCAMERA_" + ipaEnvName + "_TUNING_FILE"; - /* Check the environment variable first. */ + char const *configFromEnv = utils::secure_getenv(ipaEnvName.c_str()); + if (configFromEnv && *configFromEnv != '\0') + return { configFromEnv }; + + struct stat statbuf; + int ret; + + /* + * Check the directory pointed to by the IPA config path environment + * variable next. + */ const char *confPaths = utils::secure_getenv("LIBCAMERA_IPA_CONFIG_PATH"); if (confPaths) { for (const auto &dir : utils::split(confPaths, ":")) { diff --git a/src/ipa/libipa/matrix.cpp b/src/libcamera/matrix.cpp index 8346f0d3..e7e02722 100644 --- a/src/ipa/libipa/matrix.cpp +++ b/src/libcamera/matrix.cpp @@ -5,7 +5,7 @@ * Matrix and related operations */ -#include "matrix.h" +#include "libcamera/internal/matrix.h" #include <libcamera/base/log.h> @@ -18,8 +18,6 @@ namespace libcamera { LOG_DEFINE_CATEGORY(Matrix) -namespace ipa { - /** * \class Matrix * \brief Matrix class @@ -34,7 +32,7 @@ namespace ipa { */ /** - * \fn Matrix::Matrix(const std::vector<T> &data) + * \fn Matrix::Matrix(const std::array<T, Rows * Cols> &data) * \brief Construct a matrix from supplied data * \param[in] data Data from which to construct a matrix * @@ -55,6 +53,17 @@ namespace ipa { */ /** + * \fn Matrix::data() + * \brief Access the matrix data as a linear array + * + * Access the contents of the matrix as a one-dimensional linear array of + * values in row-major order. The size of the array is equal to the product of + * the number of rows and columns of the matrix (Rows x Cols). + * + * \return A span referencing the matrix data as a linear array + */ + +/** * \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 @@ -144,6 +153,4 @@ bool matrixValidateYaml(const YamlObject &obj, unsigned int size) } #endif /* __DOXYGEN__ */ -} /* namespace ipa */ - } /* namespace libcamera */ diff --git a/src/libcamera/meson.build b/src/libcamera/meson.build index 21cae117..de22b8e6 100644 --- a/src/libcamera/meson.build +++ b/src/libcamera/meson.build @@ -40,6 +40,7 @@ libcamera_internal_sources = files([ 'ipc_pipe_unixsocket.cpp', 'ipc_unixsocket.cpp', 'mapped_framebuffer.cpp', + 'matrix.cpp', 'media_device.cpp', 'media_object.cpp', 'pipeline_handler.cpp', @@ -52,6 +53,7 @@ libcamera_internal_sources = files([ 'v4l2_pixelformat.cpp', 'v4l2_subdevice.cpp', 'v4l2_videodevice.cpp', + 'vector.cpp', 'yaml_parser.cpp', ]) diff --git a/src/libcamera/pipeline/mali-c55/mali-c55.cpp b/src/libcamera/pipeline/mali-c55/mali-c55.cpp index e40025b4..a05e11fc 100644 --- a/src/libcamera/pipeline/mali-c55/mali-c55.cpp +++ b/src/libcamera/pipeline/mali-c55/mali-c55.cpp @@ -12,6 +12,7 @@ #include <set> #include <string> +#include <linux/mali-c55-config.h> #include <linux/media-bus-format.h> #include <linux/media.h> @@ -20,14 +21,24 @@ #include <libcamera/camera.h> #include <libcamera/formats.h> #include <libcamera/geometry.h> +#include <libcamera/property_ids.h> #include <libcamera/stream.h> +#include <libcamera/ipa/core_ipa_interface.h> +#include <libcamera/ipa/mali-c55_ipa_interface.h> +#include <libcamera/ipa/mali-c55_ipa_proxy.h> + #include "libcamera/internal/bayer_format.h" #include "libcamera/internal/camera.h" #include "libcamera/internal/camera_sensor.h" +#include "libcamera/internal/camera_sensor_properties.h" +#include "libcamera/internal/delayed_controls.h" #include "libcamera/internal/device_enumerator.h" +#include "libcamera/internal/framebuffer.h" +#include "libcamera/internal/ipa_manager.h" #include "libcamera/internal/media_device.h" #include "libcamera/internal/pipeline_handler.h" +#include "libcamera/internal/request.h" #include "libcamera/internal/v4l2_subdevice.h" #include "libcamera/internal/v4l2_videodevice.h" @@ -57,32 +68,27 @@ const std::map<libcamera::PixelFormat, unsigned int> maliC55FmtToCode = { { formats::NV21, MEDIA_BUS_FMT_YUV10_1X30 }, /* RAW formats, FR pipe only. */ - { formats::SGBRG8, MEDIA_BUS_FMT_SGBRG8_1X8 }, - { formats::SRGGB8, MEDIA_BUS_FMT_SRGGB8_1X8 }, - { formats::SBGGR8, MEDIA_BUS_FMT_SBGGR8_1X8 }, - { formats::SGRBG8, MEDIA_BUS_FMT_SGRBG8_1X8 }, - { formats::SGBRG10, MEDIA_BUS_FMT_SGBRG10_1X10 }, - { formats::SRGGB10, MEDIA_BUS_FMT_SRGGB10_1X10 }, - { formats::SBGGR10, MEDIA_BUS_FMT_SBGGR10_1X10 }, - { formats::SGRBG10, MEDIA_BUS_FMT_SGRBG10_1X10 }, - { formats::SGBRG12, MEDIA_BUS_FMT_SGBRG12_1X12 }, - { formats::SRGGB12, MEDIA_BUS_FMT_SRGGB12_1X12 }, - { formats::SBGGR12, MEDIA_BUS_FMT_SBGGR12_1X12 }, - { formats::SGRBG12, MEDIA_BUS_FMT_SGRBG12_1X12 }, - { formats::SGBRG14, MEDIA_BUS_FMT_SGBRG14_1X14 }, - { formats::SRGGB14, MEDIA_BUS_FMT_SRGGB14_1X14 }, - { formats::SBGGR14, MEDIA_BUS_FMT_SBGGR14_1X14 }, - { formats::SGRBG14, MEDIA_BUS_FMT_SGRBG14_1X14 }, { formats::SGBRG16, MEDIA_BUS_FMT_SGBRG16_1X16 }, { formats::SRGGB16, MEDIA_BUS_FMT_SRGGB16_1X16 }, { formats::SBGGR16, MEDIA_BUS_FMT_SBGGR16_1X16 }, { formats::SGRBG16, MEDIA_BUS_FMT_SGRBG16_1X16 }, }; +constexpr Size kMaliC55MinInputSize = { 640, 480 }; constexpr Size kMaliC55MinSize = { 128, 128 }; constexpr Size kMaliC55MaxSize = { 8192, 8192 }; constexpr unsigned int kMaliC55ISPInternalFormat = MEDIA_BUS_FMT_RGB121212_1X36; +struct MaliC55FrameInfo { + Request *request; + + FrameBuffer *paramBuffer; + FrameBuffer *statBuffer; + + bool paramsDone; + bool statsDone; +}; + class MaliC55CameraData : public Camera::Private { public: @@ -92,13 +98,16 @@ public: } int init(); + int loadIPA(); /* Deflect these functionalities to either TPG or CameraSensor. */ - const std::vector<unsigned int> mbusCodes() const; const std::vector<Size> sizes(unsigned int mbusCode) const; const Size resolution() const; - PixelFormat bestRawFormat() const; + int pixfmtToMbusCode(const PixelFormat &pixFmt) const; + const PixelFormat &bestRawFormat() const; + + void updateControls(const ControlInfoMap &ipaControls); PixelFormat adjustRawFormat(const PixelFormat &pixFmt) const; Size adjustRawSizes(const PixelFormat &pixFmt, const Size &rawSize) const; @@ -111,8 +120,15 @@ public: Stream frStream_; Stream dsStream_; + std::unique_ptr<ipa::mali_c55::IPAProxyMaliC55> ipa_; + std::vector<IPABuffer> ipaStatBuffers_; + std::vector<IPABuffer> ipaParamBuffers_; + + std::unique_ptr<DelayedControls> delayedCtrls_; + private: void initTPGData(); + void setSensorControls(const ControlList &sensorControls); std::string id_; std::vector<unsigned int> tpgCodes_; @@ -176,12 +192,9 @@ void MaliC55CameraData::initTPGData() tpgResolution_ = tpgSizes_.back(); } -const std::vector<unsigned int> MaliC55CameraData::mbusCodes() const +void MaliC55CameraData::setSensorControls(const ControlList &sensorControls) { - if (sensor_) - return sensor_->mbusCodes(); - - return tpgCodes_; + delayedCtrls_->push(sensorControls); } const std::vector<Size> MaliC55CameraData::sizes(unsigned int mbusCode) const @@ -215,33 +228,102 @@ const Size MaliC55CameraData::resolution() const return tpgResolution_; } -PixelFormat MaliC55CameraData::bestRawFormat() const +/* + * The Mali C55 ISP can only produce 16-bit RAW output in bypass modes, but the + * sensors connected to it might produce 8/10/12/16 bits. We simply search the + * sensor's supported formats for the one with a matching bayer order and the + * greatest bitdepth. + */ +int MaliC55CameraData::pixfmtToMbusCode(const PixelFormat &pixFmt) const { + auto it = maliC55FmtToCode.find(pixFmt); + if (it == maliC55FmtToCode.end()) + return -EINVAL; + + BayerFormat bayerFormat = BayerFormat::fromMbusCode(it->second); + if (!bayerFormat.isValid()) + return -EINVAL; + + V4L2Subdevice::Formats formats = sd_->formats(0); + unsigned int sensorMbusCode = 0; unsigned int bitDepth = 0; - PixelFormat rawFormat; - /* - * Iterate over all the supported PixelFormat and find the one - * supported by the camera with the largest bitdepth. - */ - for (const auto &maliFormat : maliC55FmtToCode) { - PixelFormat pixFmt = maliFormat.first; - if (!isFormatRaw(pixFmt)) + for (const auto &[code, sizes] : formats) { + BayerFormat sdBayerFormat = BayerFormat::fromMbusCode(code); + if (!sdBayerFormat.isValid()) + continue; + + if (sdBayerFormat.order != bayerFormat.order) continue; - unsigned int rawCode = maliFormat.second; - const auto rawSizes = sizes(rawCode); - if (rawSizes.empty()) + if (sdBayerFormat.bitDepth > bitDepth) { + bitDepth = sdBayerFormat.bitDepth; + sensorMbusCode = code; + } + } + + if (!sensorMbusCode) + return -EINVAL; + + return sensorMbusCode; +} + +/* + * Find a RAW PixelFormat supported by both the ISP and the sensor. + * + * The situation is mildly complicated by the fact that we expect the sensor to + * output something like RAW8/10/12/16, but the ISP can only accept as input + * RAW20 and can only produce as output RAW16. The one constant in that is the + * bayer order of the data, so we'll simply check that the sensor produces a + * format with a bayer order that matches that of one of the formats we support, + * and select that. + */ +const PixelFormat &MaliC55CameraData::bestRawFormat() const +{ + static const PixelFormat invalidPixFmt = {}; + + for (const auto &fmt : sd_->formats(0)) { + BayerFormat sensorBayer = BayerFormat::fromMbusCode(fmt.first); + + if (!sensorBayer.isValid()) continue; - BayerFormat bayer = BayerFormat::fromMbusCode(rawCode); - if (bayer.bitDepth > bitDepth) { - bitDepth = bayer.bitDepth; - rawFormat = pixFmt; + for (const auto &[pixFmt, rawCode] : maliC55FmtToCode) { + if (!isFormatRaw(pixFmt)) + continue; + + BayerFormat bayer = BayerFormat::fromMbusCode(rawCode); + if (bayer.order == sensorBayer.order) + return pixFmt; } } - return rawFormat; + LOG(MaliC55, Error) << "Sensor doesn't provide a compatible format"; + return invalidPixFmt; +} + +void MaliC55CameraData::updateControls(const ControlInfoMap &ipaControls) +{ + if (!sensor_) + return; + + IPACameraSensorInfo sensorInfo; + int ret = sensor_->sensorInfo(&sensorInfo); + if (ret) { + LOG(MaliC55, Error) << "Failed to retrieve sensor info"; + return; + } + + ControlInfoMap::Map controls; + Rectangle ispMinCrop{ 0, 0, 640, 480 }; + controls[&controls::ScalerCrop] = + ControlInfo(ispMinCrop, sensorInfo.analogCrop, + sensorInfo.analogCrop); + + for (auto const &c : ipaControls) + controls.emplace(c.first, c.second); + + controlInfo_ = ControlInfoMap(std::move(controls), controls::controls); } /* @@ -250,13 +332,11 @@ PixelFormat MaliC55CameraData::bestRawFormat() const */ PixelFormat MaliC55CameraData::adjustRawFormat(const PixelFormat &rawFmt) const { - /* Make sure the provided raw format is supported by the pipeline. */ - auto it = maliC55FmtToCode.find(rawFmt); - if (it == maliC55FmtToCode.end()) + /* Make sure the RAW mbus code is supported by the image source. */ + int rawCode = pixfmtToMbusCode(rawFmt); + if (rawCode < 0) return bestRawFormat(); - /* Now make sure the RAW mbus code is supported by the image source. */ - unsigned int rawCode = it->second; const auto rawSizes = sizes(rawCode); if (rawSizes.empty()) return bestRawFormat(); @@ -264,15 +344,16 @@ PixelFormat MaliC55CameraData::adjustRawFormat(const PixelFormat &rawFmt) const return rawFmt; } -Size MaliC55CameraData::adjustRawSizes(const PixelFormat &rawFmt, const Size &rawSize) const +Size MaliC55CameraData::adjustRawSizes(const PixelFormat &rawFmt, const Size &size) const { - /* Just make sure the format is supported. */ - auto it = maliC55FmtToCode.find(rawFmt); - if (it == maliC55FmtToCode.end()) - return {}; + /* Expand the RAW size to the minimum ISP input size. */ + Size rawSize = size.expandedTo(kMaliC55MinInputSize); /* Check if the size is natively supported. */ - unsigned int rawCode = it->second; + int rawCode = pixfmtToMbusCode(rawFmt); + if (rawCode < 0) + return {}; + const auto rawSizes = sizes(rawCode); auto sizeIt = std::find(rawSizes.begin(), rawSizes.end(), rawSize); if (sizeIt != rawSizes.end()) @@ -281,20 +362,59 @@ Size MaliC55CameraData::adjustRawSizes(const PixelFormat &rawFmt, const Size &ra /* Or adjust it to the closest supported size. */ uint16_t distance = std::numeric_limits<uint16_t>::max(); Size bestSize; - for (const Size &size : rawSizes) { + for (const Size &sz : rawSizes) { uint16_t dist = std::abs(static_cast<int>(rawSize.width) - - static_cast<int>(size.width)) + + static_cast<int>(sz.width)) + std::abs(static_cast<int>(rawSize.height) - - static_cast<int>(size.height)); + static_cast<int>(sz.height)); if (dist < distance) { dist = distance; - bestSize = size; + bestSize = sz; } } return bestSize; } +int MaliC55CameraData::loadIPA() +{ + int ret; + + /* Do not initialize IPA for TPG. */ + if (!sensor_) + return 0; + + ipa_ = IPAManager::createIPA<ipa::mali_c55::IPAProxyMaliC55>(pipe(), 1, 1); + if (!ipa_) + return -ENOENT; + + ipa_->setSensorControls.connect(this, &MaliC55CameraData::setSensorControls); + + std::string ipaTuningFile = ipa_->configurationFile(sensor_->model() + ".yaml", + "uncalibrated.yaml"); + + /* We need to inform the IPA of the sensor configuration */ + ipa::mali_c55::IPAConfigInfo ipaConfig{}; + + ret = sensor_->sensorInfo(&ipaConfig.sensorInfo); + if (ret) + return ret; + + ipaConfig.sensorControls = sensor_->controls(); + + ControlInfoMap ipaControls; + ret = ipa_->init({ ipaTuningFile, sensor_->model() }, ipaConfig, + &ipaControls); + if (ret) { + LOG(MaliC55, Error) << "Failed to initialise the Mali-C55 IPA"; + return ret; + } + + updateControls(ipaControls); + + return 0; +} + class MaliC55CameraConfiguration : public CameraConfiguration { public: @@ -304,6 +424,7 @@ public: } Status validate() override; + const Transform &combinedTransform() { return combinedTransform_; } V4L2SubdeviceFormat sensorFormat_; @@ -311,6 +432,7 @@ private: static constexpr unsigned int kMaxStreams = 2; const MaliC55CameraData *data_; + Transform combinedTransform_; }; CameraConfiguration::Status MaliC55CameraConfiguration::validate() @@ -320,6 +442,19 @@ CameraConfiguration::Status MaliC55CameraConfiguration::validate() if (config_.empty()) return Invalid; + /* + * The TPG doesn't support flips, so we only need to calculate a + * transform if we have a sensor. + */ + if (data_->sensor_) { + Orientation requestedOrientation = orientation; + combinedTransform_ = data_->sensor_->computeTransform(&orientation); + if (orientation != requestedOrientation) + status = Adjusted; + } else { + combinedTransform_ = Transform::Rot0; + } + /* Only 2 streams available. */ if (config_.size() > kMaxStreams) { config_.resize(kMaxStreams); @@ -341,7 +476,11 @@ CameraConfiguration::Status MaliC55CameraConfiguration::validate() rawConfig = &config; } - Size maxSize = kMaliC55MaxSize; + /* + * The C55 can not upscale. Limit the configuration to the ISP + * capabilities and the sensor resolution. + */ + Size maxSize = kMaliC55MaxSize.boundedTo(data_->resolution()); if (rawConfig) { /* * \todo Take into account the Bayer components ordering once @@ -349,6 +488,10 @@ CameraConfiguration::Status MaliC55CameraConfiguration::validate() */ PixelFormat rawFormat = data_->adjustRawFormat(rawConfig->pixelFormat); + + if (!rawFormat.isValid()) + return Invalid; + if (rawFormat != rawConfig->pixelFormat) { LOG(MaliC55, Debug) << "RAW format adjusted to " << rawFormat; @@ -367,12 +510,21 @@ CameraConfiguration::Status MaliC55CameraConfiguration::validate() maxSize = rawSize; + const PixelFormatInfo &info = PixelFormatInfo::info(rawConfig->pixelFormat); + rawConfig->stride = info.stride(rawConfig->size.width, 0, 4); + rawConfig->frameSize = info.frameSize(rawConfig->size, 4); + rawConfig->setStream(const_cast<Stream *>(&data_->frStream_)); frPipeAvailable = false; } - /* Adjust processed streams. */ - Size maxYuvSize; + /* + * Adjust processed streams. + * + * Compute the minimum sensor size to be later used to select the + * sensor configuration. + */ + Size minSensorSize = kMaliC55MinInputSize; for (StreamConfiguration &config : config_) { if (isFormatRaw(config.pixelFormat)) continue; @@ -394,8 +546,8 @@ CameraConfiguration::Status MaliC55CameraConfiguration::validate() status = Adjusted; } - if (maxYuvSize < size) - maxYuvSize = size; + if (minSensorSize < size) + minSensorSize = size; if (frPipeAvailable) { config.setStream(const_cast<Stream *>(&data_->frStream_)); @@ -409,30 +561,30 @@ CameraConfiguration::Status MaliC55CameraConfiguration::validate() /* If there's a RAW config, sensor configuration follows it. */ if (rawConfig) { - const auto it = maliC55FmtToCode.find(rawConfig->pixelFormat); - sensorFormat_.code = it->second; - sensorFormat_.size = rawConfig->size; + sensorFormat_.code = data_->pixfmtToMbusCode(rawConfig->pixelFormat); + sensorFormat_.size = rawConfig->size.expandedTo(minSensorSize); return status; } /* If there's no RAW config, compute the sensor configuration here. */ PixelFormat rawFormat = data_->bestRawFormat(); - const auto it = maliC55FmtToCode.find(rawFormat); - sensorFormat_.code = it->second; + if (!rawFormat.isValid()) + return Invalid; + + sensorFormat_.code = data_->pixfmtToMbusCode(rawFormat); uint16_t distance = std::numeric_limits<uint16_t>::max(); - const auto sizes = data_->sizes(it->second); + const auto sizes = data_->sizes(sensorFormat_.code); Size bestSize; for (const auto &size : sizes) { - /* Skip sensor sizes that are smaller than the max YUV size. */ - if (maxYuvSize.width > size.width || - maxYuvSize.height > size.height) + if (minSensorSize.width > size.width || + minSensorSize.height > size.height) continue; - uint16_t dist = std::abs(static_cast<int>(maxYuvSize.width) - + uint16_t dist = std::abs(static_cast<int>(minSensorSize.width) - static_cast<int>(size.width)) + - std::abs(static_cast<int>(maxYuvSize.height) - + std::abs(static_cast<int>(minSensorSize.height) - static_cast<int>(size.height)); if (dist < distance) { dist = distance; @@ -457,6 +609,8 @@ public: int exportFrameBuffers(Camera *camera, Stream *stream, std::vector<std::unique_ptr<FrameBuffer>> *buffers) override; + int allocateBuffers(Camera *camera); + void freeBuffers(Camera *camera); int start(Camera *camera, const ControlList *controls) override; void stopDevice(Camera *camera) override; @@ -464,6 +618,10 @@ public: int queueRequestDevice(Camera *camera, Request *request) override; void imageBufferReady(FrameBuffer *buffer); + void paramsBufferReady(FrameBuffer *buffer); + void statsBufferReady(FrameBuffer *buffer); + void paramsComputed(unsigned int requestId); + void statsProcessed(unsigned int requestId, const ControlList &metadata); bool match(DeviceEnumerator *enumerator) override; @@ -471,6 +629,7 @@ private: struct MaliC55Pipe { std::unique_ptr<V4L2Subdevice> resizer; std::unique_ptr<V4L2VideoDevice> cap; + MediaLink *link; Stream *stream; }; @@ -507,6 +666,10 @@ private: pipe.stream = nullptr; } + MaliC55FrameInfo *findFrameInfo(FrameBuffer *buffer); + MaliC55FrameInfo *findFrameInfo(Request *request); + void tryComplete(MaliC55FrameInfo *info); + int configureRawStream(MaliC55CameraData *data, const StreamConfiguration &config, V4L2SubdeviceFormat &subdevFormat); @@ -514,13 +677,25 @@ private: const StreamConfiguration &config, V4L2SubdeviceFormat &subdevFormat); - void registerMaliCamera(std::unique_ptr<MaliC55CameraData> data, + void applyScalerCrop(Camera *camera, const ControlList &controls); + + bool registerMaliCamera(std::unique_ptr<MaliC55CameraData> data, const std::string &name); bool registerTPGCamera(MediaLink *link); bool registerSensorCamera(MediaLink *link); MediaDevice *media_; std::unique_ptr<V4L2Subdevice> isp_; + std::unique_ptr<V4L2VideoDevice> stats_; + std::unique_ptr<V4L2VideoDevice> params_; + + std::vector<std::unique_ptr<FrameBuffer>> statsBuffers_; + std::queue<FrameBuffer *> availableStatsBuffers_; + + std::vector<std::unique_ptr<FrameBuffer>> paramsBuffers_; + std::queue<FrameBuffer *> availableParamsBuffers_; + + std::map<unsigned int, MaliC55FrameInfo> frameInfoMap_; std::array<MaliC55Pipe, MaliC55NumPipes> pipes_; @@ -606,7 +781,10 @@ PipelineHandlerMaliC55::generateConfiguration(Camera *camera, if (isRaw) { /* Make sure the mbus code is supported. */ - unsigned int rawCode = maliFormat.second; + int rawCode = data->pixfmtToMbusCode(pixFmt); + if (rawCode < 0) + continue; + const auto sizes = data->sizes(rawCode); if (sizes.empty()) continue; @@ -714,16 +892,28 @@ int PipelineHandlerMaliC55::configureProcessedStream(MaliC55CameraData *data, if (ret) return ret; - /* \todo Configure the resizer crop/compose rectangles. */ - Rectangle ispCrop = { 0, 0, config.size }; + /* + * Compute the scaler-in to scaler-out ratio: first center-crop to align + * the FOV to the desired resolution, then scale to the desired size. + */ + Size scalerIn = subdevFormat.size.boundedToAspectRatio(config.size); + int xCrop = (subdevFormat.size.width - scalerIn.width) / 2; + int yCrop = (subdevFormat.size.height - scalerIn.height) / 2; + Rectangle ispCrop = { xCrop, yCrop, scalerIn }; ret = pipe->resizer->setSelection(0, V4L2_SEL_TGT_CROP, &ispCrop); if (ret) return ret; - ret = pipe->resizer->setSelection(0, V4L2_SEL_TGT_COMPOSE, &ispCrop); + Rectangle ispCompose = { 0, 0, config.size }; + ret = pipe->resizer->setSelection(0, V4L2_SEL_TGT_COMPOSE, &ispCompose); if (ret) return ret; + /* + * The source pad format size comes directly from the sink + * compose rectangle. + */ + subdevFormat.size = ispCompose.size(); subdevFormat.code = maliC55FmtToCode.find(config.pixelFormat)->second; return pipe->resizer->setFormat(1, &subdevFormat); } @@ -755,16 +945,33 @@ int PipelineHandlerMaliC55::configure(Camera *camera, if (ret) return ret; + if (data->sensor_) { + ret = data->sensor_->setFormat(&subdevFormat, + maliConfig->combinedTransform()); + if (ret) + return ret; + } + if (data->csi_) { ret = data->csi_->setFormat(0, &subdevFormat); if (ret) return ret; - ret = data->csi_->setFormat(1, &subdevFormat); + ret = data->csi_->getFormat(1, &subdevFormat); if (ret) return ret; } + V4L2DeviceFormat statsFormat; + ret = stats_->getFormat(&statsFormat); + if (ret) + return ret; + + if (statsFormat.planes[0].size != sizeof(struct mali_c55_stats_buffer)) { + LOG(MaliC55, Error) << "3a stats buffer size invalid"; + return -EINVAL; + } + /* * Propagate the format to the ISP sink pad and configure the input * crop rectangle (no crop at the moment). @@ -794,6 +1001,17 @@ int PipelineHandlerMaliC55::configure(Camera *camera, Stream *stream = streamConfig.stream(); MaliC55Pipe *pipe = pipeFromStream(data, stream); + /* + * Enable the media link between the pipe's resizer and the + * capture video device + */ + + ret = pipe->link->setEnabled(true); + if (ret) { + LOG(MaliC55, Error) << "Couldn't enable resizer's link"; + return ret; + } + if (isFormatRaw(streamConfig.pixelFormat)) ret = configureRawStream(data, streamConfig, subdevFormat); else @@ -815,6 +1033,56 @@ int PipelineHandlerMaliC55::configure(Camera *camera, pipe->stream = stream; } + if (!data->ipa_) + return 0; + + /* + * Enable the media link between the ISP subdevice and the statistics + * video device. + */ + const MediaEntity *ispEntity = isp_->entity(); + ret = ispEntity->getPadByIndex(3)->links()[0]->setEnabled(true); + if (ret) { + LOG(MaliC55, Error) << "Couldn't enable statistics link"; + return ret; + } + + /* + * Enable the media link between the ISP subdevice and the parameters + * video device. + */ + ret = ispEntity->getPadByIndex(4)->links()[0]->setEnabled(true); + if (ret) { + LOG(MaliC55, Error) << "Couldn't enable parameters link"; + return ret; + } + + /* We need to inform the IPA of the sensor configuration */ + ipa::mali_c55::IPAConfigInfo ipaConfig{}; + + ret = data->sensor_->sensorInfo(&ipaConfig.sensorInfo); + if (ret) + return ret; + + ipaConfig.sensorControls = data->sensor_->controls(); + + /* + * And we also need to tell the IPA the bayerOrder of the data (as + * affected by any flips that we've configured) + */ + const Transform &combinedTransform = maliConfig->combinedTransform(); + BayerFormat::Order bayerOrder = data->sensor_->bayerOrder(combinedTransform); + + ControlInfoMap ipaControls; + ret = data->ipa_->configure(ipaConfig, utils::to_underlying(bayerOrder), + &ipaControls); + if (ret) { + LOG(MaliC55, Error) << "Failed to configure IPA"; + return ret; + } + + data->updateControls(ipaControls); + return 0; } @@ -827,32 +1095,166 @@ int PipelineHandlerMaliC55::exportFrameBuffers(Camera *camera, Stream *stream, return pipe->cap->exportBuffers(count, buffers); } -int PipelineHandlerMaliC55::start([[maybe_unused]] Camera *camera, [[maybe_unused]] const ControlList *controls) +void PipelineHandlerMaliC55::freeBuffers(Camera *camera) +{ + MaliC55CameraData *data = cameraData(camera); + + while (!availableStatsBuffers_.empty()) + availableStatsBuffers_.pop(); + while (!availableParamsBuffers_.empty()) + availableParamsBuffers_.pop(); + + statsBuffers_.clear(); + paramsBuffers_.clear(); + + if (data->ipa_) { + data->ipa_->unmapBuffers(data->ipaStatBuffers_); + data->ipa_->unmapBuffers(data->ipaParamBuffers_); + } + data->ipaStatBuffers_.clear(); + data->ipaParamBuffers_.clear(); + + if (stats_->releaseBuffers()) + LOG(MaliC55, Error) << "Failed to release stats buffers"; + + if (params_->releaseBuffers()) + LOG(MaliC55, Error) << "Failed to release params buffers"; + + return; +} + +int PipelineHandlerMaliC55::allocateBuffers(Camera *camera) +{ + MaliC55CameraData *data = cameraData(camera); + unsigned int ipaBufferId = 1; + unsigned int bufferCount; + int ret; + + bufferCount = std::max({ + data->frStream_.configuration().bufferCount, + data->dsStream_.configuration().bufferCount, + }); + + ret = stats_->allocateBuffers(bufferCount, &statsBuffers_); + if (ret < 0) + return ret; + + for (std::unique_ptr<FrameBuffer> &buffer : statsBuffers_) { + buffer->setCookie(ipaBufferId++); + data->ipaStatBuffers_.emplace_back(buffer->cookie(), + buffer->planes()); + availableStatsBuffers_.push(buffer.get()); + } + + ret = params_->allocateBuffers(bufferCount, ¶msBuffers_); + if (ret < 0) + return ret; + + for (std::unique_ptr<FrameBuffer> &buffer : paramsBuffers_) { + buffer->setCookie(ipaBufferId++); + data->ipaParamBuffers_.emplace_back(buffer->cookie(), + buffer->planes()); + availableParamsBuffers_.push(buffer.get()); + } + + if (data->ipa_) { + data->ipa_->mapBuffers(data->ipaStatBuffers_, true); + data->ipa_->mapBuffers(data->ipaParamBuffers_, false); + } + + return 0; +} + +int PipelineHandlerMaliC55::start(Camera *camera, [[maybe_unused]] const ControlList *controls) { + MaliC55CameraData *data = cameraData(camera); + int ret; + + ret = allocateBuffers(camera); + if (ret) + return ret; + + if (data->ipa_) { + ret = data->ipa_->start(); + if (ret) { + LOG(MaliC55, Error) + << "Failed to start IPA" << camera->id(); + freeBuffers(camera); + return ret; + } + } + for (MaliC55Pipe &pipe : pipes_) { if (!pipe.stream) continue; Stream *stream = pipe.stream; - int ret = pipe.cap->importBuffers(stream->configuration().bufferCount); + ret = pipe.cap->importBuffers(stream->configuration().bufferCount); if (ret) { LOG(MaliC55, Error) << "Failed to import buffers"; + if (data->ipa_) + data->ipa_->stop(); + freeBuffers(camera); return ret; } ret = pipe.cap->streamOn(); if (ret) { LOG(MaliC55, Error) << "Failed to start stream"; + if (data->ipa_) + data->ipa_->stop(); + freeBuffers(camera); return ret; } } + ret = stats_->streamOn(); + if (ret) { + LOG(MaliC55, Error) << "Failed to start stats stream"; + + if (data->ipa_) + data->ipa_->stop(); + + for (MaliC55Pipe &pipe : pipes_) { + if (pipe.stream) + pipe.cap->streamOff(); + } + + freeBuffers(camera); + return ret; + } + + ret = params_->streamOn(); + if (ret) { + LOG(MaliC55, Error) << "Failed to start params stream"; + + stats_->streamOff(); + if (data->ipa_) + data->ipa_->stop(); + + for (MaliC55Pipe &pipe : pipes_) { + if (pipe.stream) + pipe.cap->streamOff(); + } + + freeBuffers(camera); + return ret; + } + + ret = isp_->setFrameStartEnabled(true); + if (ret) + LOG(MaliC55, Error) << "Failed to enable frame start events"; + return 0; } -void PipelineHandlerMaliC55::stopDevice([[maybe_unused]] Camera *camera) +void PipelineHandlerMaliC55::stopDevice(Camera *camera) { + MaliC55CameraData *data = cameraData(camera); + + isp_->setFrameStartEnabled(false); + for (MaliC55Pipe &pipe : pipes_) { if (!pipe.stream) continue; @@ -860,38 +1262,285 @@ void PipelineHandlerMaliC55::stopDevice([[maybe_unused]] Camera *camera) pipe.cap->streamOff(); pipe.cap->releaseBuffers(); } + + stats_->streamOff(); + params_->streamOff(); + if (data->ipa_) + data->ipa_->stop(); + freeBuffers(camera); +} + +void PipelineHandlerMaliC55::applyScalerCrop(Camera *camera, + const ControlList &controls) +{ + MaliC55CameraData *data = cameraData(camera); + + const auto &scalerCrop = controls.get<Rectangle>(controls::ScalerCrop); + if (!scalerCrop) + return; + + if (!data->sensor_) { + LOG(MaliC55, Error) << "ScalerCrop not supported for TPG"; + return; + } + + Rectangle nativeCrop = *scalerCrop; + + IPACameraSensorInfo sensorInfo; + int ret = data->sensor_->sensorInfo(&sensorInfo); + if (ret) { + LOG(MaliC55, Error) << "Failed to retrieve sensor info"; + return; + } + + /* + * The ScalerCrop rectangle re-scaling in the ISP crop rectangle + * comes straight from the RPi pipeline handler. + * + * Create a version of the crop rectangle aligned to the analogue crop + * rectangle top-left coordinates and scaled in the [analogue crop to + * output frame] ratio to take into account binning/skipping on the + * sensor. + */ + Rectangle ispCrop = nativeCrop.translatedBy(-sensorInfo.analogCrop + .topLeft()); + ispCrop.scaleBy(sensorInfo.outputSize, sensorInfo.analogCrop.size()); + + /* + * The crop rectangle should be: + * 1. At least as big as ispMinCropSize_, once that's been + * enlarged to the same aspect ratio. + * 2. With the same mid-point, if possible. + * 3. But it can't go outside the sensor area. + */ + Rectangle ispMinCrop{ 0, 0, 640, 480 }; + Size minSize = ispMinCrop.size().expandedToAspectRatio(nativeCrop.size()); + Size size = ispCrop.size().expandedTo(minSize); + ispCrop = size.centeredTo(ispCrop.center()) + .enclosedIn(Rectangle(sensorInfo.outputSize)); + + /* + * As the resizer can't upscale, the crop rectangle has to be larger + * than the larger stream output size. + */ + Size maxYuvSize; + for (MaliC55Pipe &pipe : pipes_) { + if (!pipe.stream) + continue; + + const StreamConfiguration &config = pipe.stream->configuration(); + if (isFormatRaw(config.pixelFormat)) { + LOG(MaliC55, Debug) << "Cannot crop with a RAW stream"; + return; + } + + Size streamSize = config.size; + if (streamSize.width > maxYuvSize.width) + maxYuvSize.width = streamSize.width; + if (streamSize.height > maxYuvSize.height) + maxYuvSize.height = streamSize.height; + } + + ispCrop.size().expandTo(maxYuvSize); + + /* + * Now apply the scaler crop to each enabled output. This overrides the + * crop configuration performed at configure() time and can cause + * square pixels if the crop rectangle and scaler output FOV ratio are + * different. + */ + for (MaliC55Pipe &pipe : pipes_) { + if (!pipe.stream) + continue; + + /* Create a copy to avoid setSelection() to modify ispCrop. */ + Rectangle pipeCrop = ispCrop; + ret = pipe.resizer->setSelection(0, V4L2_SEL_TGT_CROP, &pipeCrop); + if (ret) { + LOG(MaliC55, Error) + << "Failed to apply crop to " + << (pipe.stream == &data->frStream_ ? + "FR" : "DS") << " pipe"; + return; + } + } } int PipelineHandlerMaliC55::queueRequestDevice(Camera *camera, Request *request) { - int ret; + MaliC55CameraData *data = cameraData(camera); - for (auto &[stream, buffer] : request->buffers()) { - MaliC55Pipe *pipe = pipeFromStream(cameraData(camera), stream); + /* Do not run the IPA if the TPG is in use. */ + if (!data->ipa_) { + MaliC55FrameInfo frameInfo; + frameInfo.request = request; + frameInfo.statBuffer = nullptr; + frameInfo.paramBuffer = nullptr; + frameInfo.paramsDone = true; + frameInfo.statsDone = true; - ret = pipe->cap->queueBuffer(buffer); - if (ret) - return ret; + frameInfoMap_[request->sequence()] = frameInfo; + + for (auto &[stream, buffer] : request->buffers()) { + MaliC55Pipe *pipe = pipeFromStream(data, stream); + + pipe->cap->queueBuffer(buffer); + } + + return 0; } + if (availableStatsBuffers_.empty()) { + LOG(MaliC55, Error) << "Stats buffer underrun"; + return -ENOENT; + } + + if (availableParamsBuffers_.empty()) { + LOG(MaliC55, Error) << "Params buffer underrun"; + return -ENOENT; + } + + MaliC55FrameInfo frameInfo; + frameInfo.request = request; + + frameInfo.statBuffer = availableStatsBuffers_.front(); + availableStatsBuffers_.pop(); + frameInfo.paramBuffer = availableParamsBuffers_.front(); + availableParamsBuffers_.pop(); + + frameInfo.paramsDone = false; + frameInfo.statsDone = false; + + frameInfoMap_[request->sequence()] = frameInfo; + + data->ipa_->queueRequest(request->sequence(), request->controls()); + data->ipa_->fillParams(request->sequence(), + frameInfo.paramBuffer->cookie()); + return 0; } -void PipelineHandlerMaliC55::imageBufferReady(FrameBuffer *buffer) +MaliC55FrameInfo *PipelineHandlerMaliC55::findFrameInfo(Request *request) { - Request *request = buffer->request(); + for (auto &[sequence, info] : frameInfoMap_) { + if (info.request == request) + return &info; + } + + return nullptr; +} + +MaliC55FrameInfo *PipelineHandlerMaliC55::findFrameInfo(FrameBuffer *buffer) +{ + for (auto &[sequence, info] : frameInfoMap_) { + if (info.paramBuffer == buffer || + info.statBuffer == buffer) + return &info; + } + + return nullptr; +} - completeBuffer(request, buffer); +void PipelineHandlerMaliC55::tryComplete(MaliC55FrameInfo *info) +{ + if (!info->paramsDone) + return; + if (!info->statsDone) + return; + Request *request = info->request; if (request->hasPendingBuffers()) return; + if (info->statBuffer) + availableStatsBuffers_.push(info->statBuffer); + if (info->paramBuffer) + availableParamsBuffers_.push(info->paramBuffer); + + frameInfoMap_.erase(request->sequence()); + completeRequest(request); } -void PipelineHandlerMaliC55::registerMaliCamera(std::unique_ptr<MaliC55CameraData> data, +void PipelineHandlerMaliC55::imageBufferReady(FrameBuffer *buffer) +{ + Request *request = buffer->request(); + MaliC55FrameInfo *info = findFrameInfo(request); + ASSERT(info); + + if (completeBuffer(request, buffer)) + tryComplete(info); +} + +void PipelineHandlerMaliC55::paramsBufferReady(FrameBuffer *buffer) +{ + MaliC55FrameInfo *info = findFrameInfo(buffer); + ASSERT(info); + + info->paramsDone = true; + + tryComplete(info); +} + +void PipelineHandlerMaliC55::statsBufferReady(FrameBuffer *buffer) +{ + MaliC55FrameInfo *info = findFrameInfo(buffer); + ASSERT(info); + + Request *request = info->request; + MaliC55CameraData *data = cameraData(request->_d()->camera()); + + ControlList sensorControls = data->delayedCtrls_->get(buffer->metadata().sequence); + + data->ipa_->processStats(request->sequence(), buffer->cookie(), + sensorControls); +} + +void PipelineHandlerMaliC55::paramsComputed(unsigned int requestId) +{ + MaliC55FrameInfo &frameInfo = frameInfoMap_[requestId]; + Request *request = frameInfo.request; + MaliC55CameraData *data = cameraData(request->_d()->camera()); + + /* + * Queue buffers for stats and params, then queue buffers to the capture + * video devices. + */ + + frameInfo.paramBuffer->_d()->metadata().planes()[0].bytesused = + sizeof(struct mali_c55_params_buffer); + params_->queueBuffer(frameInfo.paramBuffer); + stats_->queueBuffer(frameInfo.statBuffer); + + for (auto &[stream, buffer] : request->buffers()) { + MaliC55Pipe *pipe = pipeFromStream(data, stream); + + pipe->cap->queueBuffer(buffer); + } +} + +void PipelineHandlerMaliC55::statsProcessed(unsigned int requestId, + const ControlList &metadata) +{ + MaliC55FrameInfo &frameInfo = frameInfoMap_[requestId]; + + frameInfo.statsDone = true; + frameInfo.request->metadata().merge(metadata); + + tryComplete(&frameInfo); +} + +bool PipelineHandlerMaliC55::registerMaliCamera(std::unique_ptr<MaliC55CameraData> data, const std::string &name) { + if (data->loadIPA()) + return false; + + if (data->ipa_) { + data->ipa_->statsProcessed.connect(this, &PipelineHandlerMaliC55::statsProcessed); + data->ipa_->paramsComputed.connect(this, &PipelineHandlerMaliC55::paramsComputed); + } + std::set<Stream *> streams{ &data->frStream_ }; if (dsFitted_) streams.insert(&data->dsStream_); @@ -899,6 +1548,8 @@ void PipelineHandlerMaliC55::registerMaliCamera(std::unique_ptr<MaliC55CameraDat std::shared_ptr<Camera> camera = Camera::create(std::move(data), name, streams); registerCamera(std::move(camera)); + + return true; } /* @@ -924,9 +1575,7 @@ bool PipelineHandlerMaliC55::registerTPGCamera(MediaLink *link) if (data->init()) return false; - registerMaliCamera(std::move(data), name); - - return true; + return registerMaliCamera(std::move(data), name); } /* @@ -952,9 +1601,24 @@ bool PipelineHandlerMaliC55::registerSensorCamera(MediaLink *ispLink) if (data->init()) return false; - /* \todo: Init properties and controls. */ + data->properties_ = data->sensor_->properties(); - registerMaliCamera(std::move(data), sensor->name()); + const CameraSensorProperties::SensorDelays &delays = data->sensor_->sensorDelays(); + std::unordered_map<uint32_t, DelayedControls::ControlParams> params = { + { V4L2_CID_ANALOGUE_GAIN, { delays.gainDelay, false } }, + { V4L2_CID_EXPOSURE, { delays.exposureDelay, false } }, + }; + + data->delayedCtrls_ = + std::make_unique<DelayedControls>(data->sensor_->device(), + params); + isp_->frameStart.connect(data->delayedCtrls_.get(), + &DelayedControls::applyControls); + + /* \todo Init properties. */ + + if (!registerMaliCamera(std::move(data), sensor->name())) + return false; } return true; @@ -965,7 +1629,7 @@ bool PipelineHandlerMaliC55::match(DeviceEnumerator *enumerator) const MediaPad *ispSink; /* - * We search for just the ISP subdevice and the full resolution pipe. + * We search for just the always-available elements of the media graph. * The TPG and the downscale pipe are both optional blocks and may not * be fitted. */ @@ -973,6 +1637,8 @@ bool PipelineHandlerMaliC55::match(DeviceEnumerator *enumerator) dm.add("mali-c55 isp"); dm.add("mali-c55 resizer fr"); dm.add("mali-c55 fr"); + dm.add("mali-c55 3a stats"); + dm.add("mali-c55 3a params"); media_ = acquireMediaDevice(enumerator, dm); if (!media_) @@ -982,6 +1648,14 @@ bool PipelineHandlerMaliC55::match(DeviceEnumerator *enumerator) if (isp_->open() < 0) return false; + stats_ = V4L2VideoDevice::fromEntityName(media_, "mali-c55 3a stats"); + if (stats_->open() < 0) + return false; + + params_ = V4L2VideoDevice::fromEntityName(media_, "mali-c55 3a params"); + if (params_->open() < 0) + return false; + MaliC55Pipe *frPipe = &pipes_[MaliC55FR]; frPipe->resizer = V4L2Subdevice::fromEntityName(media_, "mali-c55 resizer fr"); if (frPipe->resizer->open() < 0) @@ -991,6 +1665,12 @@ bool PipelineHandlerMaliC55::match(DeviceEnumerator *enumerator) if (frPipe->cap->open() < 0) return false; + frPipe->link = media_->link("mali-c55 resizer fr", 1, "mali-c55 fr", 0); + if (!frPipe->link) { + LOG(MaliC55, Error) << "No link between fr resizer and video node"; + return false; + } + frPipe->cap->bufferReady.connect(this, &PipelineHandlerMaliC55::imageBufferReady); dsFitted_ = !!media_->getEntityByName("mali-c55 ds"); @@ -1007,9 +1687,19 @@ bool PipelineHandlerMaliC55::match(DeviceEnumerator *enumerator) if (dsPipe->cap->open() < 0) return false; + dsPipe->link = media_->link("mali-c55 resizer ds", 1, + "mali-c55 ds", 0); + if (!dsPipe->link) { + LOG(MaliC55, Error) << "No link between ds resizer and video node"; + return false; + } + dsPipe->cap->bufferReady.connect(this, &PipelineHandlerMaliC55::imageBufferReady); } + stats_->bufferReady.connect(this, &PipelineHandlerMaliC55::statsBufferReady); + params_->bufferReady.connect(this, &PipelineHandlerMaliC55::paramsBufferReady); + ispSink = isp_->entity()->getPadByIndex(0); if (!ispSink || ispSink->links().empty()) { LOG(MaliC55, Error) << "ISP sink pad error"; diff --git a/src/libcamera/pipeline/rkisp1/rkisp1.cpp b/src/libcamera/pipeline/rkisp1/rkisp1.cpp index 908724e2..52633fe3 100644 --- a/src/libcamera/pipeline/rkisp1/rkisp1.cpp +++ b/src/libcamera/pipeline/rkisp1/rkisp1.cpp @@ -24,6 +24,7 @@ #include <libcamera/control_ids.h> #include <libcamera/formats.h> #include <libcamera/framebuffer.h> +#include <libcamera/property_ids.h> #include <libcamera/request.h> #include <libcamera/stream.h> #include <libcamera/transform.h> @@ -97,6 +98,7 @@ public: } PipelineHandlerRkISP1 *pipe(); + const PipelineHandlerRkISP1 *pipe() const; int loadIPA(unsigned int hwRevision); Stream mainPathStream_; @@ -175,6 +177,7 @@ private: } friend RkISP1CameraData; + friend RkISP1CameraConfiguration; friend RkISP1Frames; int initLinks(Camera *camera, const CameraSensor *sensor, @@ -205,6 +208,7 @@ private: RkISP1SelfPath selfPath_; std::unique_ptr<V4L2M2MConverter> dewarper_; + Rectangle scalerMaxCrop_; bool useDewarper_; std::optional<Rectangle> activeCrop_; @@ -361,6 +365,11 @@ PipelineHandlerRkISP1 *RkISP1CameraData::pipe() return static_cast<PipelineHandlerRkISP1 *>(Camera::Private::pipe()); } +const PipelineHandlerRkISP1 *RkISP1CameraData::pipe() const +{ + return static_cast<const PipelineHandlerRkISP1 *>(Camera::Private::pipe()); +} + int RkISP1CameraData::loadIPA(unsigned int hwRevision) { ipa_ = IPAManager::createIPA<ipa::rkisp1::IPAProxyRkISP1>(pipe(), 1, 1); @@ -371,18 +380,9 @@ int RkISP1CameraData::loadIPA(unsigned int hwRevision) ipa_->paramsComputed.connect(this, &RkISP1CameraData::paramsComputed); ipa_->metadataReady.connect(this, &RkISP1CameraData::metadataReady); - /* - * The API tuning file is made from the sensor name unless the - * environment variable overrides it. - */ - std::string ipaTuningFile; - char const *configFromEnv = utils::secure_getenv("LIBCAMERA_RKISP1_TUNING_FILE"); - if (!configFromEnv || *configFromEnv == '\0') { - ipaTuningFile = - ipa_->configurationFile(sensor_->model() + ".yaml", "uncalibrated.yaml"); - } else { - ipaTuningFile = std::string(configFromEnv); - } + /* The IPA tuning file is made from the sensor name. */ + std::string ipaTuningFile = + ipa_->configurationFile(sensor_->model() + ".yaml", "uncalibrated.yaml"); IPACameraSensorInfo sensorInfo{}; int ret = sensor_->sensorInfo(&sensorInfo); @@ -488,6 +488,7 @@ bool RkISP1CameraConfiguration::fitsAllPaths(const StreamConfiguration &cfg) CameraConfiguration::Status RkISP1CameraConfiguration::validate() { + const PipelineHandlerRkISP1 *pipe = data_->pipe(); const CameraSensor *sensor = data_->sensor_.get(); unsigned int pathCount = data_->selfPath_ ? 2 : 1; Status status; @@ -544,6 +545,18 @@ CameraConfiguration::Status RkISP1CameraConfiguration::validate() } } + bool useDewarper = false; + if (pipe->dewarper_) { + /* + * Platforms with dewarper support, such as i.MX8MP, support + * only a single stream. We can inspect config_[0] only here. + */ + bool isRaw = PixelFormatInfo::info(config_[0].pixelFormat).colourEncoding == + PixelFormatInfo::ColourEncodingRAW; + if (!isRaw) + useDewarper = true; + } + /* * If there are more than one stream in the configuration figure out the * order to evaluate the streams. The first stream has the highest @@ -556,50 +569,72 @@ CameraConfiguration::Status RkISP1CameraConfiguration::validate() if (config_.size() == 2 && fitsAllPaths(config_[0])) std::reverse(order.begin(), order.end()); + /* + * Validate the configuration against the desired path and, if the + * platform supports it, the dewarper. + */ + auto validateConfig = [&](StreamConfiguration &cfg, RkISP1Path *path, + Stream *stream, Status expectedStatus) { + StreamConfiguration tryCfg = cfg; + + Status ret = path->validate(sensor, sensorConfig, &tryCfg); + if (ret == Invalid) + return false; + + if (!useDewarper && + (expectedStatus == Valid && ret == Adjusted)) + return false; + + if (useDewarper) { + bool adjusted; + + pipe->dewarper_->validateOutput(&tryCfg, &adjusted, + Converter::Alignment::Down); + if (expectedStatus == Valid && adjusted) + return false; + } + + cfg = tryCfg; + cfg.setStream(stream); + return true; + }; + bool mainPathAvailable = true; bool selfPathAvailable = data_->selfPath_; + RkISP1Path *mainPath = data_->mainPath_; + RkISP1Path *selfPath = data_->selfPath_; + Stream *mainPathStream = const_cast<Stream *>(&data_->mainPathStream_); + Stream *selfPathStream = const_cast<Stream *>(&data_->selfPathStream_); for (unsigned int index : order) { StreamConfiguration &cfg = config_[index]; /* Try to match stream without adjusting configuration. */ if (mainPathAvailable) { - StreamConfiguration tryCfg = cfg; - if (data_->mainPath_->validate(sensor, sensorConfig, &tryCfg) == Valid) { + if (validateConfig(cfg, mainPath, mainPathStream, Valid)) { mainPathAvailable = false; - cfg = tryCfg; - cfg.setStream(const_cast<Stream *>(&data_->mainPathStream_)); continue; } } if (selfPathAvailable) { - StreamConfiguration tryCfg = cfg; - if (data_->selfPath_->validate(sensor, sensorConfig, &tryCfg) == Valid) { + if (validateConfig(cfg, selfPath, selfPathStream, Valid)) { selfPathAvailable = false; - cfg = tryCfg; - cfg.setStream(const_cast<Stream *>(&data_->selfPathStream_)); continue; } } /* Try to match stream allowing adjusting configuration. */ if (mainPathAvailable) { - StreamConfiguration tryCfg = cfg; - if (data_->mainPath_->validate(sensor, sensorConfig, &tryCfg) == Adjusted) { + if (validateConfig(cfg, mainPath, mainPathStream, Adjusted)) { mainPathAvailable = false; - cfg = tryCfg; - cfg.setStream(const_cast<Stream *>(&data_->mainPathStream_)); status = Adjusted; continue; } } if (selfPathAvailable) { - StreamConfiguration tryCfg = cfg; - if (data_->selfPath_->validate(sensor, sensorConfig, &tryCfg) == Adjusted) { + if (validateConfig(cfg, selfPath, selfPathStream, Adjusted)) { selfPathAvailable = false; - cfg = tryCfg; - cfg.setStream(const_cast<Stream *>(&data_->selfPathStream_)); status = Adjusted; continue; } @@ -633,7 +668,8 @@ CameraConfiguration::Status RkISP1CameraConfiguration::validate() [](const auto &value) { return value.second; }); } - sensorFormat_ = sensor->getFormat(mbusCodes, maxSize); + sensorFormat_ = sensor->getFormat(mbusCodes, maxSize, + mainPath->maxResolution()); if (sensorFormat_.size.isNull()) sensorFormat_.size = sensor->resolution(); @@ -795,28 +831,48 @@ int PipelineHandlerRkISP1::configure(Camera *camera, CameraConfiguration *c) if (ret < 0) return ret; - Rectangle rect(0, 0, format.size); - ret = isp_->setSelection(0, V4L2_SEL_TGT_CROP, &rect); + Rectangle inputCrop(0, 0, format.size); + ret = isp_->setSelection(0, V4L2_SEL_TGT_CROP, &inputCrop); if (ret < 0) return ret; LOG(RkISP1, Debug) << "ISP input pad configured with " << format - << " crop " << rect; + << " crop " << inputCrop; + Rectangle outputCrop = inputCrop; const PixelFormat &streamFormat = config->at(0).pixelFormat; const PixelFormatInfo &info = PixelFormatInfo::info(streamFormat); isRaw_ = info.colourEncoding == PixelFormatInfo::ColourEncodingRAW; + useDewarper_ = dewarper_ && !isRaw_; /* YUYV8_2X8 is required on the ISP source path pad for YUV output. */ if (!isRaw_) format.code = MEDIA_BUS_FMT_YUYV8_2X8; + /* + * On devices without DUAL_CROP (like the imx8mp) cropping needs to be + * done on the ISP/IS output. + */ + if (media_->hwRevision() == RKISP1_V_IMX8MP) { + /* imx8mp has only a single path. */ + const auto &cfg = config->at(0); + Size ispCrop = format.size.boundedToAspectRatio(cfg.size); + if (useDewarper_) + ispCrop = dewarper_->adjustInputSize(cfg.pixelFormat, + ispCrop); + else + ispCrop.alignUpTo(2, 2); + + outputCrop = ispCrop.centeredTo(Rectangle(format.size).center()); + format.size = ispCrop; + } + LOG(RkISP1, Debug) << "Configuring ISP output pad with " << format - << " crop " << rect; + << " crop " << outputCrop; - ret = isp_->setSelection(2, V4L2_SEL_TGT_CROP, &rect); + ret = isp_->setSelection(2, V4L2_SEL_TGT_CROP, &outputCrop); if (ret < 0) return ret; @@ -827,7 +883,12 @@ int PipelineHandlerRkISP1::configure(Camera *camera, CameraConfiguration *c) LOG(RkISP1, Debug) << "ISP output pad configured with " << format - << " crop " << rect; + << " crop " << outputCrop; + + IPACameraSensorInfo sensorInfo; + ret = data->sensor_->sensorInfo(&sensorInfo); + if (ret) + return ret; std::map<unsigned int, IPAStream> streamConfig; std::vector<std::reference_wrapper<StreamConfiguration>> outputCfgs; @@ -841,7 +902,17 @@ int PipelineHandlerRkISP1::configure(Camera *camera, CameraConfiguration *c) if (dewarper_ && !isRaw_) { outputCfgs.push_back(const_cast<StreamConfiguration &>(cfg)); ret = dewarper_->configure(cfg, outputCfgs); - useDewarper_ = ret ? false : true; + if (ret) + return ret; + + /* + * Calculate the crop rectangle of the data + * flowing into the dewarper in sensor + * coordinates. + */ + scalerMaxCrop_ = + outputCrop.transformedBetween(inputCrop, + sensorInfo.analogCrop); } } else if (hasSelfPath_) { ret = selfPath_.configure(cfg, format); @@ -868,14 +939,9 @@ int PipelineHandlerRkISP1::configure(Camera *camera, CameraConfiguration *c) return ret; /* Inform IPA of stream configuration and sensor controls. */ - ipa::rkisp1::IPAConfigInfo ipaConfig{}; - - ret = data->sensor_->sensorInfo(&ipaConfig.sensorInfo); - if (ret) - return ret; - - ipaConfig.sensorControls = data->sensor_->controls(); - ipaConfig.paramFormat = paramFormat.fourcc; + ipa::rkisp1::IPAConfigInfo ipaConfig{ sensorInfo, + data->sensor_->controls(), + paramFormat.fourcc }; ret = data->ipa_->configure(ipaConfig, streamConfig, &data->ipaControls_); if (ret) { @@ -1044,8 +1110,8 @@ int PipelineHandlerRkISP1::start(Camera *camera, [[maybe_unused]] const ControlL LOG(RkISP1, Error) << "Failed to start dewarper"; return ret; } + actions += [&]() { dewarper_->stop(); }; } - actions += [&]() { dewarper_->stop(); }; } if (data->mainPath_->isEnabled()) { @@ -1206,13 +1272,26 @@ int PipelineHandlerRkISP1::updateControls(RkISP1CameraData *data) ControlInfoMap::Map controls; if (dewarper_) { - std::pair<Rectangle, Rectangle> cropLimits = - dewarper_->inputCropBounds(&data->mainPathStream_); + std::pair<Rectangle, Rectangle> cropLimits; + if (dewarper_->isConfigured(&data->mainPathStream_)) + cropLimits = dewarper_->inputCropBounds(&data->mainPathStream_); + else + cropLimits = dewarper_->inputCropBounds(); - controls[&controls::ScalerCrop] = ControlInfo(cropLimits.first, - cropLimits.second, - cropLimits.second); - activeCrop_ = cropLimits.second; + /* + * ScalerCrop is specified to be in Sensor coordinates. + * So we need to transform the limits to sensor coordinates. + * We can safely assume that the maximum crop limit contains the + * full fov of the dewarper. + */ + Rectangle min = cropLimits.first.transformedBetween(cropLimits.second, + scalerMaxCrop_); + + controls[&controls::ScalerCrop] = ControlInfo(min, + scalerMaxCrop_, + scalerMaxCrop_); + data->properties_.set(properties::ScalerCropMaximum, scalerMaxCrop_); + activeCrop_ = scalerMaxCrop_; } /* Add the IPA registered controls to list of camera controls. */ @@ -1240,10 +1319,13 @@ int PipelineHandlerRkISP1::createCamera(MediaEntity *sensor) /* Initialize the camera properties. */ data->properties_ = data->sensor_->properties(); + scalerMaxCrop_ = Rectangle(data->sensor_->resolution()); + const CameraSensorProperties::SensorDelays &delays = data->sensor_->sensorDelays(); std::unordered_map<uint32_t, DelayedControls::ControlParams> params = { { V4L2_CID_ANALOGUE_GAIN, { delays.gainDelay, false } }, { V4L2_CID_EXPOSURE, { delays.exposureDelay, false } }, + { V4L2_CID_VBLANK, { 1, false } }, }; data->delayedCtrls_ = @@ -1459,22 +1541,33 @@ void PipelineHandlerRkISP1::imageBufferReady(FrameBuffer *buffer) /* Handle scaler crop control. */ const auto &crop = request->controls().get(controls::ScalerCrop); if (crop) { - Rectangle appliedRect = crop.value(); + Rectangle rect = crop.value(); + + /* + * ScalerCrop is specified to be in Sensor coordinates. + * So we need to transform it into dewarper coordinates. + * We can safely assume that the maximum crop limit contains the + * full fov of the dewarper. + */ + std::pair<Rectangle, Rectangle> cropLimits = + dewarper_->inputCropBounds(&data->mainPathStream_); + rect = rect.transformedBetween(scalerMaxCrop_, cropLimits.second); int ret = dewarper_->setInputCrop(&data->mainPathStream_, - &appliedRect); - if (!ret && appliedRect != crop.value()) { + &rect); + rect = rect.transformedBetween(cropLimits.second, scalerMaxCrop_); + if (!ret && rect != crop.value()) { /* * If the rectangle is changed by setInputCrop on the * dewarper, log a debug message and cache the actual * applied rectangle for metadata reporting. */ LOG(RkISP1, Debug) - << "Applied rectangle " << appliedRect.toString() + << "Applied rectangle " << rect.toString() << " differs from requested " << crop.value().toString(); } - activeCrop_ = appliedRect; + activeCrop_ = rect; } /* diff --git a/src/libcamera/pipeline/rkisp1/rkisp1_path.cpp b/src/libcamera/pipeline/rkisp1/rkisp1_path.cpp index 236d05af..eee5b09e 100644 --- a/src/libcamera/pipeline/rkisp1/rkisp1_path.cpp +++ b/src/libcamera/pipeline/rkisp1/rkisp1_path.cpp @@ -417,11 +417,14 @@ int RkISP1Path::configure(const StreamConfiguration &config, /* * Crop on the resizer input to maintain FOV before downscaling. * - * \todo The alignment to a multiple of 2 pixels is required but may - * change the aspect ratio very slightly. A more advanced algorithm to - * compute the resizer input crop rectangle is needed, and it should - * also take into account the need to crop away the edge pixels affected - * by the ISP processing blocks. + * Note that this does not apply to devices without DUAL_CROP support + * (like imx8mp) , where the cropping needs to be done on the + * ImageStabilizer block on the ISP source pad and therefore is + * configured before this stage. For simplicity we still set the crop. + * This gets ignored by the kernel driver because the hardware is + * missing the capability. + * + * Alignment to a multiple of 2 pixels is required by the resizer. */ Size ispCrop = inputFormat.size.boundedToAspectRatio(config.size) .alignedUpTo(2, 2); diff --git a/src/libcamera/pipeline/rkisp1/rkisp1_path.h b/src/libcamera/pipeline/rkisp1/rkisp1_path.h index 45be8476..2a1ef0ab 100644 --- a/src/libcamera/pipeline/rkisp1/rkisp1_path.h +++ b/src/libcamera/pipeline/rkisp1/rkisp1_path.h @@ -62,6 +62,7 @@ public: int queueBuffer(FrameBuffer *buffer) { return video_->queueBuffer(buffer); } Signal<FrameBuffer *> &bufferReady() { return video_->bufferReady; } + const Size &maxResolution() const { return maxResolution_; } private: void populateFormats(); diff --git a/src/libcamera/pipeline/rpi/common/pipeline_base.cpp b/src/libcamera/pipeline/rpi/common/pipeline_base.cpp index 6f278b29..1f13e523 100644 --- a/src/libcamera/pipeline/rpi/common/pipeline_base.cpp +++ b/src/libcamera/pipeline/rpi/common/pipeline_base.cpp @@ -816,11 +816,12 @@ int PipelineHandlerBase::registerCamera(std::unique_ptr<RPi::CameraData> &camera * Setup our delayed control writer with the sensor default * gain and exposure delays. Mark VBLANK for priority write. */ + const CameraSensorProperties::SensorDelays &delays = data->sensor_->sensorDelays(); std::unordered_map<uint32_t, RPi::DelayedControls::ControlParams> params = { - { V4L2_CID_ANALOGUE_GAIN, { result.sensorConfig.gainDelay, false } }, - { V4L2_CID_EXPOSURE, { result.sensorConfig.exposureDelay, false } }, - { V4L2_CID_HBLANK, { result.sensorConfig.hblankDelay, false } }, - { V4L2_CID_VBLANK, { result.sensorConfig.vblankDelay, true } } + { V4L2_CID_ANALOGUE_GAIN, { delays.gainDelay, false } }, + { V4L2_CID_EXPOSURE, { delays.exposureDelay, false } }, + { V4L2_CID_HBLANK, { delays.hblankDelay, false } }, + { V4L2_CID_VBLANK, { delays.vblankDelay, true } } }; data->delayedCtrls_ = std::make_unique<RPi::DelayedControls>(data->sensor_->device(), params); data->sensorMetadata_ = result.sensorConfig.sensorMetadata; @@ -1155,20 +1156,11 @@ int CameraData::loadIPA(ipa::RPi::InitResult *result) if (!ipa_) return -ENOENT; - /* - * The configuration (tuning file) is made from the sensor name unless - * the environment variable overrides it. - */ - std::string configurationFile; - char const *configFromEnv = utils::secure_getenv("LIBCAMERA_RPI_TUNING_FILE"); - if (!configFromEnv || *configFromEnv == '\0') { - std::string model = sensor_->model(); - if (isMonoSensor(sensor_)) - model += "_mono"; - configurationFile = ipa_->configurationFile(model + ".json"); - } else { - configurationFile = std::string(configFromEnv); - } + /* The configuration (tuning file) is made from the sensor name. */ + std::string model = sensor_->model(); + if (isMonoSensor(sensor_)) + model += "_mono"; + std::string configurationFile = ipa_->configurationFile(model + ".json"); IPASettings settings(configurationFile, sensor_->model()); ipa::RPi::InitParams params; diff --git a/src/libcamera/pipeline/simple/simple.cpp b/src/libcamera/pipeline/simple/simple.cpp index dae9f31f..6e039bf3 100644 --- a/src/libcamera/pipeline/simple/simple.cpp +++ b/src/libcamera/pipeline/simple/simple.cpp @@ -531,27 +531,13 @@ int SimpleCameraData::init() * Instantiate Soft ISP if this is enabled for the given driver and no converter is used. */ if (!converter_ && pipe->swIspEnabled()) { - swIsp_ = std::make_unique<SoftwareIsp>(pipe, sensor_.get()); + swIsp_ = std::make_unique<SoftwareIsp>(pipe, sensor_.get(), &controlInfo_); if (!swIsp_->isValid()) { LOG(SimplePipeline, Warning) << "Failed to create software ISP, disabling software debayering"; swIsp_.reset(); } else { - /* - * The inputBufferReady signal is emitted from the soft ISP thread, - * and needs to be handled in the pipeline handler thread. Signals - * implement queued delivery, but this works transparently only if - * the receiver is bound to the target thread. As the - * SimpleCameraData class doesn't inherit from the Object class, it - * is not bound to any thread, and the signal would be delivered - * synchronously. Instead, connect the signal to a lambda function - * bound explicitly to the pipe, which is bound to the pipeline - * handler thread. The function then simply forwards the call to - * conversionInputDone(). - */ - swIsp_->inputBufferReady.connect(pipe, [this](FrameBuffer *buffer) { - this->conversionInputDone(buffer); - }); + swIsp_->inputBufferReady.connect(this, &SimpleCameraData::conversionInputDone); swIsp_->outputBufferReady.connect(this, &SimpleCameraData::conversionOutputDone); swIsp_->ispStatsReady.connect(this, &SimpleCameraData::ispStatsReady); swIsp_->setSensorControls.connect(this, &SimpleCameraData::setSensorControls); diff --git a/src/libcamera/pipeline/uvcvideo/uvcvideo.cpp b/src/libcamera/pipeline/uvcvideo/uvcvideo.cpp index 8c2c6baf..7470b562 100644 --- a/src/libcamera/pipeline/uvcvideo/uvcvideo.cpp +++ b/src/libcamera/pipeline/uvcvideo/uvcvideo.cpp @@ -298,7 +298,7 @@ int PipelineHandlerUVC::processControl(ControlList *controls, unsigned int id, cid = V4L2_CID_CONTRAST; else if (id == controls::Saturation) cid = V4L2_CID_SATURATION; - else if (id == controls::AeEnable) + else if (id == controls::ExposureTimeMode) cid = V4L2_CID_EXPOSURE_AUTO; else if (id == controls::ExposureTime) cid = V4L2_CID_EXPOSURE_ABSOLUTE; @@ -647,7 +647,7 @@ void UVCCameraData::addControl(uint32_t cid, const ControlInfo &v4l2Info, id = &controls::Saturation; break; case V4L2_CID_EXPOSURE_AUTO: - id = &controls::AeEnable; + id = &controls::ExposureTimeMode; break; case V4L2_CID_EXPOSURE_ABSOLUTE: id = &controls::ExposureTime; @@ -660,6 +660,7 @@ void UVCCameraData::addControl(uint32_t cid, const ControlInfo &v4l2Info, } /* Map the control info. */ + const std::vector<ControlValue> &v4l2Values = v4l2Info.values(); int32_t min = v4l2Info.min().get<int32_t>(); int32_t max = v4l2Info.max().get<int32_t>(); int32_t def = v4l2Info.def().get<int32_t>(); @@ -697,10 +698,52 @@ void UVCCameraData::addControl(uint32_t cid, const ControlInfo &v4l2Info, }; break; - case V4L2_CID_EXPOSURE_AUTO: - info = ControlInfo{ false, true, true }; + case V4L2_CID_EXPOSURE_AUTO: { + /* + * From the V4L2_CID_EXPOSURE_AUTO documentation: + * + * ------------------------------------------------------------ + * V4L2_EXPOSURE_AUTO: + * Automatic exposure time, automatic iris aperture. + * + * V4L2_EXPOSURE_MANUAL: + * Manual exposure time, manual iris. + * + * V4L2_EXPOSURE_SHUTTER_PRIORITY: + * Manual exposure time, auto iris. + * + * V4L2_EXPOSURE_APERTURE_PRIORITY: + * Auto exposure time, manual iris. + *------------------------------------------------------------- + * + * ExposureTimeModeAuto = { V4L2_EXPOSURE_AUTO, + * V4L2_EXPOSURE_APERTURE_PRIORITY } + * + * + * ExposureTimeModeManual = { V4L2_EXPOSURE_MANUAL, + * V4L2_EXPOSURE_SHUTTER_PRIORITY } + */ + std::array<int32_t, 2> values{}; + + auto it = std::find_if(v4l2Values.begin(), v4l2Values.end(), + [&](const ControlValue &val) { + return (val.get<int32_t>() == V4L2_EXPOSURE_APERTURE_PRIORITY || + val.get<int32_t>() == V4L2_EXPOSURE_AUTO) ? true : false; + }); + if (it != v4l2Values.end()) + values.back() = static_cast<int32_t>(controls::ExposureTimeModeAuto); + + it = std::find_if(v4l2Values.begin(), v4l2Values.end(), + [&](const ControlValue &val) { + return (val.get<int32_t>() == V4L2_EXPOSURE_SHUTTER_PRIORITY || + val.get<int32_t>() == V4L2_EXPOSURE_MANUAL) ? true : false; + }); + if (it != v4l2Values.end()) + values.back() = static_cast<int32_t>(controls::ExposureTimeModeManual); + + info = ControlInfo{Span<int32_t>{values}, values[0]}; break; - + } case V4L2_CID_EXPOSURE_ABSOLUTE: /* * ExposureTime is in units of 1 µs, and UVC expects diff --git a/src/libcamera/pipeline/virtual/config_parser.cpp b/src/libcamera/pipeline/virtual/config_parser.cpp index 0cbfe39b..1e009946 100644 --- a/src/libcamera/pipeline/virtual/config_parser.cpp +++ b/src/libcamera/pipeline/virtual/config_parser.cpp @@ -54,7 +54,7 @@ ConfigParser::parseConfigFile(File &file, PipelineHandler *pipe) data->config_.id = cameraId; ControlInfoMap::Map controls; - /* todo: Check which resolution's frame rate to be reported */ + /* \todo Check which resolution's frame rate to be reported */ controls[&controls::FrameDurationLimits] = ControlInfo(1000000 / data->config_.resolutions[0].frameRates[1], 1000000 / data->config_.resolutions[0].frameRates[0]); diff --git a/src/libcamera/pipeline/virtual/data/meson.build b/src/libcamera/pipeline/virtual/data/meson.build new file mode 100644 index 00000000..ce63f9a2 --- /dev/null +++ b/src/libcamera/pipeline/virtual/data/meson.build @@ -0,0 +1,4 @@ +install_data('virtual.yaml', + install_dir : pipeline_data_dir / 'virtual', + install_tag : 'runtime', + rename: 'virtual.yaml.example') diff --git a/src/libcamera/pipeline/virtual/image_frame_generator.cpp b/src/libcamera/pipeline/virtual/image_frame_generator.cpp index 277efbb0..d1545b5d 100644 --- a/src/libcamera/pipeline/virtual/image_frame_generator.cpp +++ b/src/libcamera/pipeline/virtual/image_frame_generator.cpp @@ -129,7 +129,7 @@ int ImageFrameGenerator::generateFrame(const Size &size, const FrameBuffer *buff MappedFrameBuffer mappedFrameBuffer(buffer, MappedFrameBuffer::MapFlag::Write); - auto planes = mappedFrameBuffer.planes(); + const auto &planes = mappedFrameBuffer.planes(); /* Loop only around the number of images available */ frameIndex_ %= imageFrameDatas_.size(); diff --git a/src/libcamera/pipeline/virtual/meson.build b/src/libcamera/pipeline/virtual/meson.build index 4786fe2e..c8434593 100644 --- a/src/libcamera/pipeline/virtual/meson.build +++ b/src/libcamera/pipeline/virtual/meson.build @@ -11,3 +11,5 @@ libjpeg = dependency('libjpeg', required : true) libcamera_deps += [libyuv_dep] libcamera_deps += [libjpeg] + +subdir('data') diff --git a/src/libcamera/pipeline/virtual/test_pattern_generator.cpp b/src/libcamera/pipeline/virtual/test_pattern_generator.cpp index 7bc2b338..745be83b 100644 --- a/src/libcamera/pipeline/virtual/test_pattern_generator.cpp +++ b/src/libcamera/pipeline/virtual/test_pattern_generator.cpp @@ -7,12 +7,34 @@ #include "test_pattern_generator.h" +#include <string.h> + #include <libcamera/base/log.h> #include "libcamera/internal/mapped_framebuffer.h" #include <libyuv/convert_from_argb.h> +namespace { + +template<size_t SampleSize> +void rotateLeft1Column(const libcamera::Size &size, uint8_t *image) +{ + if (size.width < 2) + return; + + const size_t stride = size.width * SampleSize; + uint8_t first[SampleSize]; + + for (size_t i = 0; i < size.height; i++, image += stride) { + memcpy(first, &image[0], SampleSize); + memmove(&image[0], &image[SampleSize], stride - SampleSize); + memcpy(&image[stride - SampleSize], first, SampleSize); + } +} + +} /* namespace */ + namespace libcamera { LOG_DECLARE_CATEGORY(Virtual) @@ -25,9 +47,9 @@ int TestPatternGenerator::generateFrame(const Size &size, MappedFrameBuffer mappedFrameBuffer(buffer, MappedFrameBuffer::MapFlag::Write); - auto planes = mappedFrameBuffer.planes(); + const auto &planes = mappedFrameBuffer.planes(); - shiftLeft(size); + rotateLeft1Column<kARGBSize>(size, template_.get()); /* Convert the template_ to the frame buffer */ int ret = libyuv::ARGBToNV12(template_.get(), size.width * kARGBSize, @@ -40,39 +62,6 @@ int TestPatternGenerator::generateFrame(const Size &size, return ret; } -void TestPatternGenerator::shiftLeft(const Size &size) -{ - /* Store the first column temporarily */ - auto firstColumn = std::make_unique<uint8_t[]>(size.height * kARGBSize); - for (size_t h = 0; h < size.height; h++) { - unsigned int index = h * size.width * kARGBSize; - unsigned int index1 = h * kARGBSize; - firstColumn[index1] = template_[index]; - firstColumn[index1 + 1] = template_[index + 1]; - firstColumn[index1 + 2] = template_[index + 2]; - firstColumn[index1 + 3] = 0x00; - } - - /* Overwrite template_ */ - uint8_t *buf = template_.get(); - for (size_t h = 0; h < size.height; h++) { - for (size_t w = 0; w < size.width - 1; w++) { - /* Overwrite with the pixel on the right */ - unsigned int index = (h * size.width + w + 1) * kARGBSize; - *buf++ = template_[index]; /* B */ - *buf++ = template_[index + 1]; /* G */ - *buf++ = template_[index + 2]; /* R */ - *buf++ = 0x00; /* A */ - } - /* Overwrite the new last column with the original first column */ - unsigned int index1 = h * kARGBSize; - *buf++ = firstColumn[index1]; /* B */ - *buf++ = firstColumn[index1 + 1]; /* G */ - *buf++ = firstColumn[index1 + 2]; /* R */ - *buf++ = 0x00; /* A */ - } -} - void ColorBarsGenerator::configure(const Size &size) { constexpr uint8_t kColorBar[8][3] = { diff --git a/src/libcamera/pipeline/virtual/test_pattern_generator.h b/src/libcamera/pipeline/virtual/test_pattern_generator.h index 05f4ab7a..2a51bd31 100644 --- a/src/libcamera/pipeline/virtual/test_pattern_generator.h +++ b/src/libcamera/pipeline/virtual/test_pattern_generator.h @@ -29,10 +29,6 @@ public: protected: /* Buffer of test pattern template */ std::unique_ptr<uint8_t[]> template_; - -private: - /* Shift the buffer by 1 pixel left each frame */ - void shiftLeft(const Size &size); }; class ColorBarsGenerator : public TestPatternGenerator diff --git a/src/libcamera/pipeline/virtual/virtual.cpp b/src/libcamera/pipeline/virtual/virtual.cpp index cec8a85b..049ebcba 100644 --- a/src/libcamera/pipeline/virtual/virtual.cpp +++ b/src/libcamera/pipeline/virtual/virtual.cpp @@ -232,8 +232,7 @@ PipelineHandlerVirtual::generateConfiguration(Camera *camera, default: LOG(Virtual, Error) << "Requested stream role not supported: " << role; - config.reset(); - return config; + return {}; } std::map<PixelFormat, std::vector<SizeRange>> streamFormats; @@ -275,11 +274,10 @@ int PipelineHandlerVirtual::exportFrameBuffers([[maybe_unused]] Camera *camera, return -ENOBUFS; const StreamConfiguration &config = stream->configuration(); - - auto info = PixelFormatInfo::info(config.pixelFormat); + const PixelFormatInfo &info = PixelFormatInfo::info(config.pixelFormat); std::vector<unsigned int> planeSizes; - for (size_t i = 0; i < info.planes.size(); ++i) + for (size_t i = 0; i < info.numPlanes(); ++i) planeSizes.push_back(info.planeSize(config.size, i)); return dmaBufAllocator_.exportBuffers(config.bufferCount, planeSizes, buffers); @@ -288,6 +286,11 @@ int PipelineHandlerVirtual::exportFrameBuffers([[maybe_unused]] Camera *camera, int PipelineHandlerVirtual::start([[maybe_unused]] Camera *camera, [[maybe_unused]] const ControlList *controls) { + VirtualCameraData *data = cameraData(camera); + + for (auto &s : data->streamConfigs_) + s.seq = 0; + return 0; } @@ -299,16 +302,27 @@ int PipelineHandlerVirtual::queueRequestDevice([[maybe_unused]] Camera *camera, Request *request) { VirtualCameraData *data = cameraData(camera); + const auto timestamp = currentTimestamp(); for (auto const &[stream, buffer] : request->buffers()) { bool found = false; /* map buffer and fill test patterns */ for (auto &streamConfig : data->streamConfigs_) { if (stream == &streamConfig.stream) { + FrameMetadata &fmd = buffer->_d()->metadata(); + + fmd.status = FrameMetadata::Status::FrameSuccess; + fmd.sequence = streamConfig.seq++; + fmd.timestamp = timestamp; + + for (const auto [i, p] : utils::enumerate(buffer->planes())) + fmd.planes()[i].bytesused = p.length; + found = true; + if (streamConfig.frameGenerator->generateFrame( stream->configuration().size, buffer)) - buffer->_d()->cancel(); + fmd.status = FrameMetadata::Status::FrameError; completeBuffer(request, buffer); break; @@ -317,7 +331,7 @@ int PipelineHandlerVirtual::queueRequestDevice([[maybe_unused]] Camera *camera, ASSERT(found); } - request->metadata().set(controls::SensorTimestamp, currentTimestamp()); + request->metadata().set(controls::SensorTimestamp, timestamp); completeRequest(request); return 0; @@ -330,10 +344,17 @@ bool PipelineHandlerVirtual::match([[maybe_unused]] DeviceEnumerator *enumerator created_ = true; - File file(configurationFile("virtual", "virtual.yaml")); - bool isOpen = file.open(File::OpenModeFlag::ReadOnly); - if (!isOpen) { - LOG(Virtual, Error) << "Failed to open config file: " << file.fileName(); + std::string configFile = configurationFile("virtual", "virtual.yaml", true); + if (configFile.empty()) { + LOG(Virtual, Debug) + << "Configuration file not found, skipping virtual cameras"; + return false; + } + + File file(configFile); + if (!file.open(File::OpenModeFlag::ReadOnly)) { + LOG(Virtual, Error) + << "Failed to open config file `" << file.fileName() << "`"; return false; } diff --git a/src/libcamera/pipeline/virtual/virtual.h b/src/libcamera/pipeline/virtual/virtual.h index 92ad7d4a..683cb82b 100644 --- a/src/libcamera/pipeline/virtual/virtual.h +++ b/src/libcamera/pipeline/virtual/virtual.h @@ -37,6 +37,7 @@ public: struct StreamConfig { Stream stream; std::unique_ptr<FrameGenerator> frameGenerator; + unsigned int seq = 0; }; /* The config file is parsed to the Configuration struct */ struct Configuration { diff --git a/src/libcamera/pipeline_handler.cpp b/src/libcamera/pipeline_handler.cpp index caa5c20e..d84dff3c 100644 --- a/src/libcamera/pipeline_handler.cpp +++ b/src/libcamera/pipeline_handler.cpp @@ -581,6 +581,7 @@ void PipelineHandler::cancelRequest(Request *request) * \brief Retrieve the absolute path to a platform configuration file * \param[in] subdir The pipeline handler specific subdirectory name * \param[in] name The configuration file name + * \param[in] silent Disable error messages * * This function locates a named platform configuration file and returns * its absolute path to the pipeline handler. It searches the following @@ -596,7 +597,8 @@ void PipelineHandler::cancelRequest(Request *request) * string if no configuration file can be found */ std::string PipelineHandler::configurationFile(const std::string &subdir, - const std::string &name) const + const std::string &name, + bool silent) const { std::string confPath; struct stat statbuf; @@ -626,9 +628,11 @@ std::string PipelineHandler::configurationFile(const std::string &subdir, if (ret == 0 && (statbuf.st_mode & S_IFMT) == S_IFREG) return confPath; - LOG(Pipeline, Error) - << "Configuration file '" << confPath - << "' not found for pipeline handler '" << PipelineHandler::name() << "'"; + if (!silent) + LOG(Pipeline, Error) + << "Configuration file '" << confPath + << "' not found for pipeline handler '" + << PipelineHandler::name() << "'"; return std::string(); } diff --git a/src/libcamera/process.cpp b/src/libcamera/process.cpp index bc9833f4..68fad327 100644 --- a/src/libcamera/process.cpp +++ b/src/libcamera/process.cpp @@ -75,7 +75,7 @@ void ProcessManager::sighandler() return; } - for (auto it = processes_.begin(); it != processes_.end(); ) { + for (auto it = processes_.begin(); it != processes_.end();) { Process *process = *it; int wstatus; diff --git a/src/libcamera/request.cpp b/src/libcamera/request.cpp index 8c56ed30..b206ac13 100644 --- a/src/libcamera/request.cpp +++ b/src/libcamera/request.cpp @@ -475,6 +475,15 @@ int Request::addBuffer(const Stream *stream, FrameBuffer *buffer, return -EINVAL; } + /* + * Make sure the fence has been extracted from the buffer + * to avoid waiting on a stale fence. + */ + if (buffer->_d()->fence()) { + LOG(Request, Error) << "Can't add buffer that still references a fence"; + return -EEXIST; + } + auto it = bufferMap_.find(stream); if (it != bufferMap_.end()) { LOG(Request, Error) << "FrameBuffer already set for stream"; @@ -485,15 +494,6 @@ int Request::addBuffer(const Stream *stream, FrameBuffer *buffer, _d()->pending_.insert(buffer); bufferMap_[stream] = buffer; - /* - * Make sure the fence has been extracted from the buffer - * to avoid waiting on a stale fence. - */ - if (buffer->_d()->fence()) { - LOG(Request, Error) << "Can't add buffer that still references a fence"; - return -EEXIST; - } - if (fence && fence->isValid()) buffer->_d()->setFence(std::move(fence)); diff --git a/src/libcamera/sensor/camera_sensor.cpp b/src/libcamera/sensor/camera_sensor.cpp index 208a1603..d19b5e2e 100644 --- a/src/libcamera/sensor/camera_sensor.cpp +++ b/src/libcamera/sensor/camera_sensor.cpp @@ -116,6 +116,7 @@ CameraSensor::~CameraSensor() = default; * \brief Retrieve the best sensor format for a desired output * \param[in] mbusCodes The list of acceptable media bus codes * \param[in] size The desired size + * \param[in] maxSize The maximum size * * Media bus codes are selected from \a mbusCodes, which lists all acceptable * codes in decreasing order of preference. Media bus codes supported by the @@ -134,6 +135,8 @@ CameraSensor::~CameraSensor() = default; * bandwidth. * - The desired \a size shall be supported by one of the media bus code listed * in \a mbusCodes. + * - The desired \a size shall fit into the maximum size \a maxSize if it is not + * null. * * When multiple media bus codes can produce the same size, the code at the * lowest position in \a mbusCodes is selected. @@ -197,6 +200,73 @@ CameraSensor::~CameraSensor() = default; */ /** + * \brief Retrieve the image source stream + * + * Sensors that produce multiple streams do not guarantee that the image stream + * is always assigned number 0. This function allows callers to retrieve the + * image stream on the sensor's source pad, in order to configure the receiving + * side accordingly. + * + * \return The image source stream + */ +V4L2Subdevice::Stream CameraSensor::imageStream() const +{ + return { 0, 0 }; +} + +/** + * \brief Retrieve the embedded data source stream + * + * Some sensors produce embedded data in a stream separate from the image + * stream. This function indicates if the sensor supports this feature by + * returning the embedded data stream on the sensor's source pad if available, + * or an std::optional<> without a value otheriwse. + * + * \return The embedded data source stream + */ +std::optional<V4L2Subdevice::Stream> CameraSensor::embeddedDataStream() const +{ + return {}; +} + +/** + * \brief Retrieve the format on the embedded data stream + * + * When an embedded data stream is available, this function returns the + * corresponding format on the sensor's source pad. The format may vary with + * the image stream format, and should therefore be retrieved after configuring + * the image stream. + * + * If the sensor doesn't support embedded data, this function returns a + * default-constructed format. + * + * \return The format on the embedded data stream + */ +V4L2SubdeviceFormat CameraSensor::embeddedDataFormat() const +{ + return {}; +} + +/** + * \brief Enable or disable the embedded data stream + * \param[in] enable True to enable the embedded data stream, false to disable it + * + * For sensors that support embedded data, this function enables or disables + * generation of embedded data. Some of such sensors always produce embedded + * data, in which case this function return -EISCONN if the caller attempts to + * disable embedded data. + * + * If the sensor doesn't support embedded data, this function returns 0 when \a + * enable is false, and -ENOSTR otherwise. + * + * \return 0 on success, or a negative error code otherwise + */ +int CameraSensor::setEmbeddedDataEnabled(bool enable) +{ + return enable ? -ENOSTR : 0; +} + +/** * \fn CameraSensor::properties() * \brief Retrieve the camera sensor properties * \return The list of camera sensor properties diff --git a/src/libcamera/sensor/camera_sensor_legacy.cpp b/src/libcamera/sensor/camera_sensor_legacy.cpp index 17d6fa68..32989c19 100644 --- a/src/libcamera/sensor/camera_sensor_legacy.cpp +++ b/src/libcamera/sensor/camera_sensor_legacy.cpp @@ -74,7 +74,8 @@ public: Size resolution() const override; V4L2SubdeviceFormat getFormat(const std::vector<unsigned int> &mbusCodes, - const Size &size) const override; + const Size &size, + const Size maxSize) const override; int setFormat(V4L2SubdeviceFormat *format, Transform transform = Transform::Identity) override; int tryFormat(V4L2SubdeviceFormat *format) const override; @@ -699,7 +700,7 @@ Size CameraSensorLegacy::resolution() const V4L2SubdeviceFormat CameraSensorLegacy::getFormat(const std::vector<unsigned int> &mbusCodes, - const Size &size) const + const Size &size, Size maxSize) const { unsigned int desiredArea = size.width * size.height; unsigned int bestArea = UINT_MAX; @@ -716,6 +717,10 @@ CameraSensorLegacy::getFormat(const std::vector<unsigned int> &mbusCodes, for (const SizeRange &range : formats->second) { const Size &sz = range.max; + if (!maxSize.isNull() && + (sz.width > maxSize.width || sz.height > maxSize.height)) + continue; + if (sz.width < size.width || sz.height < size.height) continue; diff --git a/src/libcamera/sensor/camera_sensor_properties.cpp b/src/libcamera/sensor/camera_sensor_properties.cpp index 2b06c5a1..e2f518f9 100644 --- a/src/libcamera/sensor/camera_sensor_properties.cpp +++ b/src/libcamera/sensor/camera_sensor_properties.cpp @@ -232,7 +232,12 @@ const CameraSensorProperties *CameraSensorProperties::get(const std::string &sen { "imx415", { .unitCellSize = { 1450, 1450 }, .testPatternModes = {}, - .sensorDelays = { }, + .sensorDelays = { + .exposureDelay = 2, + .gainDelay = 2, + .vblankDelay = 2, + .hblankDelay = 2 + }, } }, { "imx462", { .unitCellSize = { 2900, 2900 }, @@ -331,6 +336,14 @@ const CameraSensorProperties *CameraSensorProperties::get(const std::string &sen { "ov5647", { .unitCellSize = { 1400, 1400 }, .testPatternModes = {}, + /* + * We run this sensor in a mode where the gain delay is + * bumped up to 2. It seems to be the only way to make + * the delays "predictable". + * + * \todo Verify these delays properly, as the upstream + * driver appears to configure _no_ delay. + */ .sensorDelays = { .exposureDelay = 2, .gainDelay = 2, @@ -386,6 +399,16 @@ const CameraSensorProperties *CameraSensorProperties::get(const std::string &sen .hblankDelay = 2 }, } }, + { "ov7251", { + .unitCellSize = { 3000, 3000 }, + .testPatternModes = { }, + .sensorDelays = { + .exposureDelay = 2, + .gainDelay = 2, + .vblankDelay = 2, + .hblankDelay = 2 + }, + } }, { "ov8858", { .unitCellSize = { 1120, 1120 }, .testPatternModes = { @@ -415,6 +438,16 @@ const CameraSensorProperties *CameraSensorProperties::get(const std::string &sen }, .sensorDelays = { }, } }, + { "ov9281", { + .unitCellSize = { 3000, 3000 }, + .testPatternModes = { }, + .sensorDelays = { + .exposureDelay = 2, + .gainDelay = 2, + .vblankDelay = 2, + .hblankDelay = 2 + }, + } }, { "ov13858", { .unitCellSize = { 1120, 1120 }, .testPatternModes = { diff --git a/src/libcamera/sensor/camera_sensor_raw.cpp b/src/libcamera/sensor/camera_sensor_raw.cpp new file mode 100644 index 00000000..ab75b1f8 --- /dev/null +++ b/src/libcamera/sensor/camera_sensor_raw.cpp @@ -0,0 +1,1157 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +/* + * Copyright (C) 2024, Ideas on Board Oy. + * + * camera_sensor_raw.cpp - A raw camera sensor using the V4L2 streams API + */ + +#include <algorithm> +#include <cmath> +#include <float.h> +#include <iomanip> +#include <limits.h> +#include <map> +#include <memory> +#include <optional> +#include <string.h> +#include <string> +#include <vector> + +#include <libcamera/base/class.h> +#include <libcamera/base/log.h> +#include <libcamera/base/utils.h> + +#include <libcamera/camera.h> +#include <libcamera/control_ids.h> +#include <libcamera/controls.h> +#include <libcamera/geometry.h> +#include <libcamera/orientation.h> +#include <libcamera/property_ids.h> +#include <libcamera/transform.h> + +#include <libcamera/ipa/core_ipa_interface.h> + +#include "libcamera/internal/bayer_format.h" +#include "libcamera/internal/camera_lens.h" +#include "libcamera/internal/camera_sensor.h" +#include "libcamera/internal/camera_sensor_properties.h" +#include "libcamera/internal/formats.h" +#include "libcamera/internal/media_device.h" +#include "libcamera/internal/sysfs.h" +#include "libcamera/internal/v4l2_subdevice.h" + +namespace libcamera { + +class BayerFormat; +class CameraLens; +class MediaEntity; +class SensorConfiguration; + +struct CameraSensorProperties; + +enum class Orientation; + +LOG_DECLARE_CATEGORY(CameraSensor) + +class CameraSensorRaw : public CameraSensor, protected Loggable +{ +public: + CameraSensorRaw(const MediaEntity *entity); + ~CameraSensorRaw(); + + static std::variant<std::unique_ptr<CameraSensor>, int> + match(MediaEntity *entity); + + const std::string &model() const override { return model_; } + const std::string &id() const override { return id_; } + + const MediaEntity *entity() const override { return entity_; } + V4L2Subdevice *device() override { return subdev_.get(); } + + CameraLens *focusLens() override { return focusLens_.get(); } + + const std::vector<unsigned int> &mbusCodes() const override { return mbusCodes_; } + std::vector<Size> sizes(unsigned int mbusCode) const override; + Size resolution() const override; + + V4L2SubdeviceFormat getFormat(const std::vector<unsigned int> &mbusCodes, + const Size &size, + const Size maxSize) const override; + int setFormat(V4L2SubdeviceFormat *format, + Transform transform = Transform::Identity) override; + int tryFormat(V4L2SubdeviceFormat *format) const override; + + int applyConfiguration(const SensorConfiguration &config, + Transform transform = Transform::Identity, + V4L2SubdeviceFormat *sensorFormat = nullptr) override; + + V4L2Subdevice::Stream imageStream() const override; + std::optional<V4L2Subdevice::Stream> embeddedDataStream() const override; + V4L2SubdeviceFormat embeddedDataFormat() const override; + int setEmbeddedDataEnabled(bool enable) override; + + const ControlList &properties() const override { return properties_; } + int sensorInfo(IPACameraSensorInfo *info) const override; + Transform computeTransform(Orientation *orientation) const override; + BayerFormat::Order bayerOrder(Transform t) const override; + + const ControlInfoMap &controls() const override; + ControlList getControls(const std::vector<uint32_t> &ids) override; + int setControls(ControlList *ctrls) override; + + const std::vector<controls::draft::TestPatternModeEnum> & + testPatternModes() const override { return testPatternModes_; } + int setTestPatternMode(controls::draft::TestPatternModeEnum mode) override; + const CameraSensorProperties::SensorDelays &sensorDelays() override; + +protected: + std::string logPrefix() const override; + +private: + LIBCAMERA_DISABLE_COPY(CameraSensorRaw) + + std::optional<int> init(); + int initProperties(); + void initStaticProperties(); + void initTestPatternModes(); + int applyTestPatternMode(controls::draft::TestPatternModeEnum mode); + + const MediaEntity *entity_; + std::unique_ptr<V4L2Subdevice> subdev_; + + struct Streams { + V4L2Subdevice::Stream sink; + V4L2Subdevice::Stream source; + }; + + struct { + Streams image; + std::optional<Streams> edata; + } streams_; + + const CameraSensorProperties *staticProps_; + + std::string model_; + std::string id_; + + V4L2Subdevice::Formats formats_; + std::vector<unsigned int> mbusCodes_; + std::vector<Size> sizes_; + std::vector<controls::draft::TestPatternModeEnum> testPatternModes_; + controls::draft::TestPatternModeEnum testPatternMode_; + + Size pixelArraySize_; + Rectangle activeArea_; + BayerFormat::Order cfaPattern_; + bool supportFlips_; + bool flipsAlterBayerOrder_; + Orientation mountingOrientation_; + + ControlList properties_; + + std::unique_ptr<CameraLens> focusLens_; +}; + +/** + * \class CameraSensorRaw + * \brief A camera sensor based on V4L2 subdevices + * + * This class supports single-subdev sensors with a single source pad and one + * or two internal sink pads (for the image and embedded data streams). + */ + +CameraSensorRaw::CameraSensorRaw(const MediaEntity *entity) + : entity_(entity), staticProps_(nullptr), supportFlips_(false), + flipsAlterBayerOrder_(false), properties_(properties::properties) +{ +} + +CameraSensorRaw::~CameraSensorRaw() = default; + +std::variant<std::unique_ptr<CameraSensor>, int> +CameraSensorRaw::match(MediaEntity *entity) +{ + /* Check the entity type. */ + if (entity->type() != MediaEntity::Type::V4L2Subdevice || + entity->function() != MEDIA_ENT_F_CAM_SENSOR) { + libcamera::LOG(CameraSensor, Debug) + << entity->name() << ": unsupported entity type (" + << utils::to_underlying(entity->type()) + << ") or function (" << utils::hex(entity->function()) << ")"; + return { 0 }; + } + + /* Count and check the number of pads. */ + static constexpr uint32_t kPadFlagsMask = MEDIA_PAD_FL_SINK + | MEDIA_PAD_FL_SOURCE + | MEDIA_PAD_FL_INTERNAL; + unsigned int numSinks = 0; + unsigned int numSources = 0; + + for (const MediaPad *pad : entity->pads()) { + switch (pad->flags() & kPadFlagsMask) { + case MEDIA_PAD_FL_SINK | MEDIA_PAD_FL_INTERNAL: + numSinks++; + break; + + case MEDIA_PAD_FL_SOURCE: + numSources++; + break; + + default: + libcamera::LOG(CameraSensor, Debug) + << entity->name() << ": unsupported pad " << pad->index() + << " type " << utils::hex(pad->flags()); + return { 0 }; + } + } + + if (numSinks < 1 || numSinks > 2 || numSources != 1) { + libcamera::LOG(CameraSensor, Debug) + << entity->name() << ": unsupported number of sinks (" + << numSinks << ") or sources (" << numSources << ")"; + return { 0 }; + } + + /* + * The entity matches. Create the camera sensor and initialize it. The + * init() function will perform further match checks. + */ + std::unique_ptr<CameraSensorRaw> sensor = + std::make_unique<CameraSensorRaw>(entity); + + std::optional<int> err = sensor->init(); + if (err) + return { *err }; + + return { std::move(sensor) }; +} + +std::optional<int> CameraSensorRaw::init() +{ + /* Create and open the subdev. */ + subdev_ = std::make_unique<V4L2Subdevice>(entity_); + int ret = subdev_->open(); + if (ret) + return { ret }; + + /* + * 1. Identify the pads. + */ + + /* + * First locate the source pad. The match() function guarantees there + * is one and only one source pad. + */ + unsigned int sourcePad = UINT_MAX; + + for (const MediaPad *pad : entity_->pads()) { + if (pad->flags() & MEDIA_PAD_FL_SOURCE) { + sourcePad = pad->index(); + break; + } + } + + /* + * Iterate over the routes to identify the streams on the source pad, + * and the internal sink pads. + */ + V4L2Subdevice::Routing routing = {}; + ret = subdev_->getRouting(&routing, V4L2Subdevice::TryFormat); + if (ret) + return { ret }; + + bool imageStreamFound = false; + + for (const V4L2Subdevice::Route &route : routing) { + if (route.source.pad != sourcePad) { + LOG(CameraSensor, Error) << "Invalid route " << route; + return { -EINVAL }; + } + + /* Identify the stream type based on the supported formats. */ + V4L2Subdevice::Formats formats = subdev_->formats(route.source); + + std::optional<MediaBusFormatInfo::Type> type; + + for (const auto &[code, sizes] : formats) { + const MediaBusFormatInfo &info = + MediaBusFormatInfo::info(code); + if (info.isValid()) { + type = info.type; + break; + } + } + + if (!type) { + LOG(CameraSensor, Warning) + << "No known format on pad " << route.source; + continue; + } + + switch (*type) { + case MediaBusFormatInfo::Type::Image: + if (imageStreamFound) { + LOG(CameraSensor, Error) + << "Multiple internal image streams (" + << streams_.image.sink << " and " + << route.sink << ")"; + return { -EINVAL }; + } + + imageStreamFound = true; + streams_.image.sink = route.sink; + streams_.image.source = route.source; + break; + + case MediaBusFormatInfo::Type::Metadata: + /* + * Skip metadata streams that are not sensor embedded + * data. The source stream reports a generic metadata + * format, check the sink stream for the exact format. + */ + formats = subdev_->formats(route.sink); + if (formats.size() != 1) + continue; + + if (MediaBusFormatInfo::info(formats.cbegin()->first).type != + MediaBusFormatInfo::Type::EmbeddedData) + continue; + + if (streams_.edata) { + LOG(CameraSensor, Error) + << "Multiple internal embedded data streams (" + << streams_.edata->sink << " and " + << route.sink << ")"; + return { -EINVAL }; + } + + streams_.edata = { route.sink, route.source }; + break; + + default: + break; + } + } + + if (!imageStreamFound) { + LOG(CameraSensor, Error) << "No image stream found"; + return { -EINVAL }; + } + + LOG(CameraSensor, Debug) + << "Found image stream " << streams_.image.sink + << " -> " << streams_.image.source; + + if (streams_.edata) + LOG(CameraSensor, Debug) + << "Found embedded data stream " << streams_.edata->sink + << " -> " << streams_.edata->source; + + /* + * 2. Enumerate and cache the media bus codes, sizes and colour filter + * array order for the image stream. + */ + + /* + * Get the native sensor CFA pattern. It is simpler to retrieve it from + * the internal image sink pad as it is guaranteed to expose a single + * format, and is not affected by flips. + */ + V4L2Subdevice::Formats formats = subdev_->formats(streams_.image.sink); + if (formats.size() != 1) { + LOG(CameraSensor, Error) + << "Image pad has " << formats.size() + << " formats, expected 1"; + return { -EINVAL }; + } + + uint32_t nativeFormat = formats.cbegin()->first; + const BayerFormat &bayerFormat = BayerFormat::fromMbusCode(nativeFormat); + if (!bayerFormat.isValid()) { + LOG(CameraSensor, Error) + << "Invalid native format " << nativeFormat; + return { 0 }; + } + + cfaPattern_ = bayerFormat.order; + + /* + * Retrieve and cache the media bus codes and sizes on the source image + * stream. + */ + formats_ = subdev_->formats(streams_.image.source); + if (formats_.empty()) { + LOG(CameraSensor, Error) << "No image format found"; + return { -EINVAL }; + } + + /* Populate and sort the media bus codes and the sizes. */ + for (const auto &[code, ranges] : formats_) { + /* Drop non-raw formats (in case we have a hybrid sensor). */ + const MediaBusFormatInfo &info = MediaBusFormatInfo::info(code); + if (info.colourEncoding != PixelFormatInfo::ColourEncodingRAW) + continue; + + mbusCodes_.push_back(code); + std::transform(ranges.begin(), ranges.end(), std::back_inserter(sizes_), + [](const SizeRange &range) { return range.max; }); + } + + if (mbusCodes_.empty()) { + LOG(CameraSensor, Debug) << "No raw image formats found"; + return { 0 }; + } + + std::sort(mbusCodes_.begin(), mbusCodes_.end()); + std::sort(sizes_.begin(), sizes_.end()); + + /* + * Remove duplicate sizes. There are no duplicate media bus codes as + * they are the keys in the formats map. + */ + auto last = std::unique(sizes_.begin(), sizes_.end()); + sizes_.erase(last, sizes_.end()); + + /* + * 3. Query selection rectangles. Retrieve properties, and verify that + * all the expected selection rectangles are supported. + */ + + Rectangle rect; + ret = subdev_->getSelection(streams_.image.sink, V4L2_SEL_TGT_CROP_BOUNDS, + &rect); + if (ret) { + LOG(CameraSensor, Error) << "No pixel array crop bounds"; + return { ret }; + } + + pixelArraySize_ = rect.size(); + + ret = subdev_->getSelection(streams_.image.sink, V4L2_SEL_TGT_CROP_DEFAULT, + &activeArea_); + if (ret) { + LOG(CameraSensor, Error) << "No pixel array crop default"; + return { ret }; + } + + ret = subdev_->getSelection(streams_.image.sink, V4L2_SEL_TGT_CROP, + &rect); + if (ret) { + LOG(CameraSensor, Error) << "No pixel array crop rectangle"; + return { ret }; + } + + /* + * 4. Verify that all required controls are present. + */ + + const ControlIdMap &controls = subdev_->controls().idmap(); + + static constexpr uint32_t mandatoryControls[] = { + V4L2_CID_ANALOGUE_GAIN, + V4L2_CID_CAMERA_ORIENTATION, + V4L2_CID_EXPOSURE, + V4L2_CID_HBLANK, + V4L2_CID_PIXEL_RATE, + V4L2_CID_VBLANK, + }; + + ret = 0; + + for (uint32_t ctrl : mandatoryControls) { + if (!controls.count(ctrl)) { + LOG(CameraSensor, Error) + << "Mandatory V4L2 control " << utils::hex(ctrl) + << " not available"; + ret = -EINVAL; + } + } + + if (ret) { + LOG(CameraSensor, Error) + << "The sensor kernel driver needs to be fixed"; + LOG(CameraSensor, Error) + << "See Documentation/sensor_driver_requirements.rst in the libcamera sources for more information"; + return { ret }; + } + + /* + * Verify if sensor supports horizontal/vertical flips + * + * \todo Handle horizontal and vertical flips independently. + */ + const struct v4l2_query_ext_ctrl *hflipInfo = subdev_->controlInfo(V4L2_CID_HFLIP); + const struct v4l2_query_ext_ctrl *vflipInfo = subdev_->controlInfo(V4L2_CID_VFLIP); + if (hflipInfo && !(hflipInfo->flags & V4L2_CTRL_FLAG_READ_ONLY) && + vflipInfo && !(vflipInfo->flags & V4L2_CTRL_FLAG_READ_ONLY)) { + supportFlips_ = true; + + if (hflipInfo->flags & V4L2_CTRL_FLAG_MODIFY_LAYOUT || + vflipInfo->flags & V4L2_CTRL_FLAG_MODIFY_LAYOUT) + flipsAlterBayerOrder_ = true; + } + + if (!supportFlips_) + LOG(CameraSensor, Debug) + << "Camera sensor does not support horizontal/vertical flip"; + + /* + * 5. Discover ancillary devices. + * + * \todo This code may be shared by different V4L2 sensor classes. + */ + for (MediaEntity *ancillary : entity_->ancillaryEntities()) { + switch (ancillary->function()) { + case MEDIA_ENT_F_LENS: + focusLens_ = std::make_unique<CameraLens>(ancillary); + ret = focusLens_->init(); + if (ret) { + LOG(CameraSensor, Error) + << "Lens initialisation failed, lens disabled"; + focusLens_.reset(); + } + break; + + default: + LOG(CameraSensor, Warning) + << "Unsupported ancillary entity function " + << ancillary->function(); + break; + } + } + + /* + * 6. Initialize properties. + */ + + ret = initProperties(); + if (ret) + return { ret }; + + /* + * 7. Initialize controls. + */ + + /* + * Set HBLANK to the minimum to start with a well-defined line length, + * allowing IPA modules that do not modify HBLANK to use the sensor + * minimum line length in their calculations. + */ + const struct v4l2_query_ext_ctrl *hblankInfo = subdev_->controlInfo(V4L2_CID_HBLANK); + if (hblankInfo && !(hblankInfo->flags & V4L2_CTRL_FLAG_READ_ONLY)) { + ControlList ctrl(subdev_->controls()); + + ctrl.set(V4L2_CID_HBLANK, static_cast<int32_t>(hblankInfo->minimum)); + ret = subdev_->setControls(&ctrl); + if (ret) + return ret; + } + + ret = applyTestPatternMode(controls::draft::TestPatternModeEnum::TestPatternModeOff); + if (ret) + return { ret }; + + return {}; +} + +int CameraSensorRaw::initProperties() +{ + model_ = subdev_->model(); + properties_.set(properties::Model, utils::toAscii(model_)); + + /* Generate a unique ID for the sensor. */ + id_ = sysfs::firmwareNodePath(subdev_->devicePath()); + if (id_.empty()) { + LOG(CameraSensor, Error) << "Can't generate sensor ID"; + return -EINVAL; + } + + /* Initialize the static properties from the sensor database. */ + initStaticProperties(); + + /* Retrieve and register properties from the kernel interface. */ + const ControlInfoMap &controls = subdev_->controls(); + + const auto &orientation = controls.find(V4L2_CID_CAMERA_ORIENTATION); + if (orientation != controls.end()) { + int32_t v4l2Orientation = orientation->second.def().get<int32_t>(); + int32_t propertyValue; + + switch (v4l2Orientation) { + default: + LOG(CameraSensor, Warning) + << "Unsupported camera location " + << v4l2Orientation << ", setting to External"; + [[fallthrough]]; + case V4L2_CAMERA_ORIENTATION_EXTERNAL: + propertyValue = properties::CameraLocationExternal; + break; + case V4L2_CAMERA_ORIENTATION_FRONT: + propertyValue = properties::CameraLocationFront; + break; + case V4L2_CAMERA_ORIENTATION_BACK: + propertyValue = properties::CameraLocationBack; + break; + } + properties_.set(properties::Location, propertyValue); + } else { + LOG(CameraSensor, Warning) << "Failed to retrieve the camera location"; + } + + const auto &rotationControl = controls.find(V4L2_CID_CAMERA_SENSOR_ROTATION); + if (rotationControl != controls.end()) { + int32_t propertyValue = rotationControl->second.def().get<int32_t>(); + + /* + * Cache the Transform associated with the camera mounting + * rotation for later use in computeTransform(). + */ + bool success; + mountingOrientation_ = orientationFromRotation(propertyValue, &success); + if (!success) { + LOG(CameraSensor, Warning) + << "Invalid rotation of " << propertyValue + << " degrees - ignoring"; + mountingOrientation_ = Orientation::Rotate0; + } + + properties_.set(properties::Rotation, propertyValue); + } else { + LOG(CameraSensor, Warning) + << "Rotation control not available, default to 0 degrees"; + properties_.set(properties::Rotation, 0); + mountingOrientation_ = Orientation::Rotate0; + } + + properties_.set(properties::PixelArraySize, pixelArraySize_); + properties_.set(properties::PixelArrayActiveAreas, { activeArea_ }); + + /* Color filter array pattern. */ + uint32_t cfa; + + switch (cfaPattern_) { + case BayerFormat::BGGR: + cfa = properties::draft::BGGR; + break; + case BayerFormat::GBRG: + cfa = properties::draft::GBRG; + break; + case BayerFormat::GRBG: + cfa = properties::draft::GRBG; + break; + case BayerFormat::RGGB: + cfa = properties::draft::RGGB; + break; + case BayerFormat::MONO: + default: + cfa = properties::draft::MONO; + break; + } + + properties_.set(properties::draft::ColorFilterArrangement, cfa); + + return 0; +} + +void CameraSensorRaw::initStaticProperties() +{ + staticProps_ = CameraSensorProperties::get(model_); + if (!staticProps_) + return; + + /* Register the properties retrieved from the sensor database. */ + properties_.set(properties::UnitCellSize, staticProps_->unitCellSize); + + initTestPatternModes(); +} + +const CameraSensorProperties::SensorDelays &CameraSensorRaw::sensorDelays() +{ + static constexpr CameraSensorProperties::SensorDelays defaultSensorDelays = { + .exposureDelay = 2, + .gainDelay = 1, + .vblankDelay = 2, + .hblankDelay = 2, + }; + + if (!staticProps_ || + (!staticProps_->sensorDelays.exposureDelay && + !staticProps_->sensorDelays.gainDelay && + !staticProps_->sensorDelays.vblankDelay && + !staticProps_->sensorDelays.hblankDelay)) { + LOG(CameraSensor, Warning) + << "No sensor delays found in static properties. " + "Assuming unverified defaults."; + + return defaultSensorDelays; + } + + return staticProps_->sensorDelays; +} + +void CameraSensorRaw::initTestPatternModes() +{ + const auto &v4l2TestPattern = controls().find(V4L2_CID_TEST_PATTERN); + if (v4l2TestPattern == controls().end()) { + LOG(CameraSensor, Debug) << "V4L2_CID_TEST_PATTERN is not supported"; + return; + } + + const auto &testPatternModes = staticProps_->testPatternModes; + if (testPatternModes.empty()) { + /* + * The camera sensor supports test patterns but we don't know + * how to map them so this should be fixed. + */ + LOG(CameraSensor, Debug) << "No static test pattern map for \'" + << model() << "\'"; + return; + } + + /* + * Create a map that associates the V4L2 control index to the test + * pattern mode by reversing the testPatternModes map provided by the + * camera sensor properties. This makes it easier to verify if the + * control index is supported in the below for loop that creates the + * list of supported test patterns. + */ + std::map<int32_t, controls::draft::TestPatternModeEnum> indexToTestPatternMode; + for (const auto &it : testPatternModes) + indexToTestPatternMode[it.second] = it.first; + + for (const ControlValue &value : v4l2TestPattern->second.values()) { + const int32_t index = value.get<int32_t>(); + + const auto it = indexToTestPatternMode.find(index); + if (it == indexToTestPatternMode.end()) { + LOG(CameraSensor, Debug) + << "Test pattern mode " << index << " ignored"; + continue; + } + + testPatternModes_.push_back(it->second); + } +} + +std::vector<Size> CameraSensorRaw::sizes(unsigned int mbusCode) const +{ + std::vector<Size> sizes; + + const auto &format = formats_.find(mbusCode); + if (format == formats_.end()) + return sizes; + + const std::vector<SizeRange> &ranges = format->second; + std::transform(ranges.begin(), ranges.end(), std::back_inserter(sizes), + [](const SizeRange &range) { return range.max; }); + + std::sort(sizes.begin(), sizes.end()); + + return sizes; +} + +Size CameraSensorRaw::resolution() const +{ + return std::min(sizes_.back(), activeArea_.size()); +} + +V4L2SubdeviceFormat +CameraSensorRaw::getFormat(const std::vector<unsigned int> &mbusCodes, + const Size &size, Size maxSize) const +{ + unsigned int desiredArea = size.width * size.height; + unsigned int bestArea = UINT_MAX; + float desiredRatio = static_cast<float>(size.width) / size.height; + float bestRatio = FLT_MAX; + const Size *bestSize = nullptr; + uint32_t bestCode = 0; + + for (unsigned int code : mbusCodes) { + const auto formats = formats_.find(code); + if (formats == formats_.end()) + continue; + + for (const SizeRange &range : formats->second) { + const Size &sz = range.max; + + if (!maxSize.isNull() && + (sz.width > maxSize.width || sz.height > maxSize.height)) + continue; + + if (sz.width < size.width || sz.height < size.height) + continue; + + float ratio = static_cast<float>(sz.width) / sz.height; + float ratioDiff = std::abs(ratio - desiredRatio); + unsigned int area = sz.width * sz.height; + unsigned int areaDiff = area - desiredArea; + + if (ratioDiff > bestRatio) + continue; + + if (ratioDiff < bestRatio || areaDiff < bestArea) { + bestRatio = ratioDiff; + bestArea = areaDiff; + bestSize = &sz; + bestCode = code; + } + } + } + + if (!bestSize) { + LOG(CameraSensor, Debug) << "No supported format or size found"; + return {}; + } + + V4L2SubdeviceFormat format{ + .code = bestCode, + .size = *bestSize, + .colorSpace = ColorSpace::Raw, + }; + + return format; +} + +int CameraSensorRaw::setFormat(V4L2SubdeviceFormat *format, Transform transform) +{ + /* Configure flips if the sensor supports that. */ + if (supportFlips_) { + ControlList flipCtrls(subdev_->controls()); + + flipCtrls.set(V4L2_CID_HFLIP, + static_cast<int32_t>(!!(transform & Transform::HFlip))); + flipCtrls.set(V4L2_CID_VFLIP, + static_cast<int32_t>(!!(transform & Transform::VFlip))); + + int ret = subdev_->setControls(&flipCtrls); + if (ret) + return ret; + } + + /* Apply format on the subdev. */ + int ret = subdev_->setFormat(streams_.image.source, format); + if (ret) + return ret; + + subdev_->updateControlInfo(); + return 0; +} + +int CameraSensorRaw::tryFormat(V4L2SubdeviceFormat *format) const +{ + return subdev_->setFormat(streams_.image.source, format, + V4L2Subdevice::Whence::TryFormat); +} + +int CameraSensorRaw::applyConfiguration(const SensorConfiguration &config, + Transform transform, + V4L2SubdeviceFormat *sensorFormat) +{ + if (!config.isValid()) { + LOG(CameraSensor, Error) << "Invalid sensor configuration"; + return -EINVAL; + } + + std::vector<unsigned int> filteredCodes; + std::copy_if(mbusCodes_.begin(), mbusCodes_.end(), + std::back_inserter(filteredCodes), + [&config](unsigned int mbusCode) { + BayerFormat bayer = BayerFormat::fromMbusCode(mbusCode); + if (bayer.bitDepth == config.bitDepth) + return true; + return false; + }); + if (filteredCodes.empty()) { + LOG(CameraSensor, Error) + << "Cannot find any format with bit depth " + << config.bitDepth; + return -EINVAL; + } + + /* + * Compute the sensor's data frame size by applying the cropping + * rectangle, subsampling and output crop to the sensor's pixel array + * size. + * + * \todo The actual size computation is for now ignored and only the + * output size is considered. This implies that resolutions obtained + * with two different cropping/subsampling will look identical and + * only the first found one will be considered. + */ + V4L2SubdeviceFormat subdevFormat = {}; + for (unsigned int code : filteredCodes) { + for (const Size &size : sizes(code)) { + if (size.width != config.outputSize.width || + size.height != config.outputSize.height) + continue; + + subdevFormat.code = code; + subdevFormat.size = size; + break; + } + } + if (!subdevFormat.code) { + LOG(CameraSensor, Error) << "Invalid output size in sensor configuration"; + return -EINVAL; + } + + int ret = setFormat(&subdevFormat, transform); + if (ret) + return ret; + + /* + * Return to the caller the format actually applied to the sensor. + * This is relevant if transform has changed the bayer pattern order. + */ + if (sensorFormat) + *sensorFormat = subdevFormat; + + /* \todo Handle AnalogCrop. Most sensors do not support set_selection */ + /* \todo Handle scaling in the digital domain. */ + + return 0; +} + +V4L2Subdevice::Stream CameraSensorRaw::imageStream() const +{ + return streams_.image.source; +} + +std::optional<V4L2Subdevice::Stream> CameraSensorRaw::embeddedDataStream() const +{ + if (!streams_.edata) + return {}; + + return { streams_.edata->source }; +} + +V4L2SubdeviceFormat CameraSensorRaw::embeddedDataFormat() const +{ + if (!streams_.edata) + return {}; + + V4L2SubdeviceFormat format; + int ret = subdev_->getFormat(streams_.edata->source, &format); + if (ret) + return {}; + + return format; +} + +int CameraSensorRaw::setEmbeddedDataEnabled(bool enable) +{ + if (!streams_.edata) + return enable ? -ENOSTR : 0; + + V4L2Subdevice::Routing routing{ 2 }; + + routing[0].sink = streams_.image.sink; + routing[0].source = streams_.image.source; + routing[0].flags = V4L2_SUBDEV_ROUTE_FL_ACTIVE; + + routing[1].sink = streams_.edata->sink; + routing[1].source = streams_.edata->source; + routing[1].flags = enable ? V4L2_SUBDEV_ROUTE_FL_ACTIVE : 0; + + int ret = subdev_->setRouting(&routing); + if (ret) + return ret; + + /* + * Check if the embedded data stream has been enabled or disabled + * correctly. Assume at least one route will match the embedded data + * source stream, as there would be something seriously wrong + * otherwise. + */ + bool enabled = false; + + for (const V4L2Subdevice::Route &route : routing) { + if (route.source != streams_.edata->source) + continue; + + enabled = route.flags & V4L2_SUBDEV_ROUTE_FL_ACTIVE; + break; + } + + if (enabled != enable) + return enabled ? -EISCONN : -ENOSTR; + + return 0; +} + +int CameraSensorRaw::sensorInfo(IPACameraSensorInfo *info) const +{ + info->model = model(); + + /* + * The active area size is a static property, while the crop + * rectangle needs to be re-read as it depends on the sensor + * configuration. + */ + info->activeAreaSize = { activeArea_.width, activeArea_.height }; + + int ret = subdev_->getSelection(streams_.image.sink, V4L2_SEL_TGT_CROP, + &info->analogCrop); + if (ret) + return ret; + + /* + * IPACameraSensorInfo::analogCrop::x and IPACameraSensorInfo::analogCrop::y + * are defined relatively to the active pixel area, while V4L2's + * TGT_CROP target is defined in respect to the full pixel array. + * + * Compensate it by subtracting the active area offset. + */ + info->analogCrop.x -= activeArea_.x; + info->analogCrop.y -= activeArea_.y; + + /* The bit depth and image size depend on the currently applied format. */ + V4L2SubdeviceFormat format{}; + ret = subdev_->getFormat(streams_.image.source, &format); + if (ret) + return ret; + info->bitsPerPixel = MediaBusFormatInfo::info(format.code).bitsPerPixel; + info->outputSize = format.size; + + std::optional<int32_t> cfa = properties_.get(properties::draft::ColorFilterArrangement); + info->cfaPattern = cfa ? *cfa : properties::draft::RGB; + + /* + * Retrieve the pixel rate, line length and minimum/maximum frame + * duration through V4L2 controls. Support for the V4L2_CID_PIXEL_RATE, + * V4L2_CID_HBLANK and V4L2_CID_VBLANK controls is mandatory. + */ + ControlList ctrls = subdev_->getControls({ V4L2_CID_PIXEL_RATE, + V4L2_CID_HBLANK, + V4L2_CID_VBLANK }); + if (ctrls.empty()) { + LOG(CameraSensor, Error) + << "Failed to retrieve camera info controls"; + return -EINVAL; + } + + info->pixelRate = ctrls.get(V4L2_CID_PIXEL_RATE).get<int64_t>(); + + const ControlInfo hblank = ctrls.infoMap()->at(V4L2_CID_HBLANK); + info->minLineLength = info->outputSize.width + hblank.min().get<int32_t>(); + info->maxLineLength = info->outputSize.width + hblank.max().get<int32_t>(); + + const ControlInfo vblank = ctrls.infoMap()->at(V4L2_CID_VBLANK); + info->minFrameLength = info->outputSize.height + vblank.min().get<int32_t>(); + info->maxFrameLength = info->outputSize.height + vblank.max().get<int32_t>(); + + return 0; +} + +Transform CameraSensorRaw::computeTransform(Orientation *orientation) const +{ + /* + * If we cannot do any flips we cannot change the native camera mounting + * orientation. + */ + if (!supportFlips_) { + *orientation = mountingOrientation_; + return Transform::Identity; + } + + /* + * Now compute the required transform to obtain 'orientation' starting + * from the mounting rotation. + * + * As a note: + * orientation / mountingOrientation_ = transform + * mountingOrientation_ * transform = orientation + */ + Transform transform = *orientation / mountingOrientation_; + + /* + * If transform contains any Transpose we cannot do it, so adjust + * 'orientation' to report the image native orientation and return Identity. + */ + if (!!(transform & Transform::Transpose)) { + *orientation = mountingOrientation_; + return Transform::Identity; + } + + return transform; +} + +BayerFormat::Order CameraSensorRaw::bayerOrder(Transform t) const +{ + if (!flipsAlterBayerOrder_) + return cfaPattern_; + + /* + * Apply the transform to the native (i.e. untransformed) Bayer order, + * using the rest of the Bayer format supplied by the caller. + */ + BayerFormat format{ cfaPattern_, 8, BayerFormat::Packing::None }; + return format.transform(t).order; +} + +const ControlInfoMap &CameraSensorRaw::controls() const +{ + return subdev_->controls(); +} + +ControlList CameraSensorRaw::getControls(const std::vector<uint32_t> &ids) +{ + return subdev_->getControls(ids); +} + +int CameraSensorRaw::setControls(ControlList *ctrls) +{ + return subdev_->setControls(ctrls); +} + +int CameraSensorRaw::setTestPatternMode(controls::draft::TestPatternModeEnum mode) +{ + if (testPatternMode_ == mode) + return 0; + + if (testPatternModes_.empty()) { + LOG(CameraSensor, Error) + << "Camera sensor does not support test pattern modes."; + return -EINVAL; + } + + return applyTestPatternMode(mode); +} + +int CameraSensorRaw::applyTestPatternMode(controls::draft::TestPatternModeEnum mode) +{ + if (testPatternModes_.empty()) + return 0; + + auto it = std::find(testPatternModes_.begin(), testPatternModes_.end(), + mode); + if (it == testPatternModes_.end()) { + LOG(CameraSensor, Error) << "Unsupported test pattern mode " + << mode; + return -EINVAL; + } + + LOG(CameraSensor, Debug) << "Apply test pattern mode " << mode; + + int32_t index = staticProps_->testPatternModes.at(mode); + ControlList ctrls{ controls() }; + ctrls.set(V4L2_CID_TEST_PATTERN, index); + + int ret = setControls(&ctrls); + if (ret) + return ret; + + testPatternMode_ = mode; + + return 0; +} + +std::string CameraSensorRaw::logPrefix() const +{ + return "'" + entity_->name() + "'"; +} + +REGISTER_CAMERA_SENSOR(CameraSensorRaw, 0) + +} /* namespace libcamera */ diff --git a/src/libcamera/sensor/meson.build b/src/libcamera/sensor/meson.build index f0d58897..dce74ed6 100644 --- a/src/libcamera/sensor/meson.build +++ b/src/libcamera/sensor/meson.build @@ -4,4 +4,5 @@ libcamera_internal_sources += files([ 'camera_sensor.cpp', 'camera_sensor_legacy.cpp', 'camera_sensor_properties.cpp', + 'camera_sensor_raw.cpp', ]) diff --git a/src/libcamera/software_isp/software_isp.cpp b/src/libcamera/software_isp/software_isp.cpp index 2ccbeacc..a64e6b2c 100644 --- a/src/libcamera/software_isp/software_isp.cpp +++ b/src/libcamera/software_isp/software_isp.cpp @@ -13,9 +13,14 @@ #include <sys/types.h> #include <unistd.h> +#include <libcamera/base/log.h> +#include <libcamera/base/thread.h> + +#include <libcamera/controls.h> #include <libcamera/formats.h> #include <libcamera/stream.h> +#include "libcamera/internal/framebuffer.h" #include "libcamera/internal/ipa_manager.h" #include "libcamera/internal/software_isp/debayer_params.h" @@ -60,9 +65,11 @@ LOG_DEFINE_CATEGORY(SoftwareIsp) * \brief Constructs SoftwareIsp object * \param[in] pipe The pipeline handler in use * \param[in] sensor Pointer to the CameraSensor instance owned by the pipeline + * \param[out] ipaControls The IPA controls to update * handler */ -SoftwareIsp::SoftwareIsp(PipelineHandler *pipe, const CameraSensor *sensor) +SoftwareIsp::SoftwareIsp(PipelineHandler *pipe, const CameraSensor *sensor, + ControlInfoMap *ipaControls) : dmaHeap_(DmaBufAllocator::DmaBufAllocatorFlag::CmaHeap | DmaBufAllocator::DmaBufAllocatorFlag::SystemHeap | DmaBufAllocator::DmaBufAllocatorFlag::UDmaBuf) @@ -124,7 +131,8 @@ SoftwareIsp::SoftwareIsp(PipelineHandler *pipe, const CameraSensor *sensor) int ret = ipa_->init(IPASettings{ ipaTuningFile, sensor->model() }, debayer_->getStatsFD(), sharedParams_.fd(), - sensor->controls()); + sensor->controls(), + ipaControls); if (ret) { LOG(SoftwareIsp, Error) << "IPA init failed"; debayer_.reset(); @@ -287,15 +295,22 @@ int SoftwareIsp::queueBuffers(uint32_t frame, FrameBuffer *input, if (outputs.empty()) return -EINVAL; + /* We only support a single stream for now. */ + if (outputs.size() != 1) + return -EINVAL; + for (auto [stream, buffer] : outputs) { if (!buffer) return -EINVAL; - if (outputs.size() != 1) /* only single stream atm */ - return -EINVAL; } - for (auto iter = outputs.begin(); iter != outputs.end(); iter++) - process(frame, input, iter->second); + queuedInputBuffers_.push_back(input); + + for (auto iter = outputs.begin(); iter != outputs.end(); iter++) { + FrameBuffer *const buffer = iter->second; + queuedOutputBuffers_.push_back(buffer); + process(frame, input, buffer); + } return 0; } @@ -316,13 +331,32 @@ int SoftwareIsp::start() /** * \brief Stops the Software ISP streaming operation + * + * All pending buffers are returned back as canceled before this function + * returns. */ void SoftwareIsp::stop() { ispWorkerThread_.exit(); ispWorkerThread_.wait(); + Thread::current()->dispatchMessages(Message::Type::InvokeMessage, this); + ipa_->stop(); + + for (auto buffer : queuedOutputBuffers_) { + FrameMetadata &metadata = buffer->_d()->metadata(); + metadata.status = FrameMetadata::FrameCancelled; + outputBufferReady.emit(buffer); + } + queuedOutputBuffers_.clear(); + + for (auto buffer : queuedInputBuffers_) { + FrameMetadata &metadata = buffer->_d()->metadata(); + metadata.status = FrameMetadata::FrameCancelled; + inputBufferReady.emit(buffer); + } + queuedInputBuffers_.clear(); } /** @@ -355,11 +389,15 @@ void SoftwareIsp::statsReady(uint32_t frame, uint32_t bufferId) void SoftwareIsp::inputReady(FrameBuffer *input) { + ASSERT(queuedInputBuffers_.front() == input); + queuedInputBuffers_.pop_front(); inputBufferReady.emit(input); } void SoftwareIsp::outputReady(FrameBuffer *output) { + ASSERT(queuedOutputBuffers_.front() == output); + queuedOutputBuffers_.pop_front(); outputBufferReady.emit(output); } diff --git a/src/libcamera/stream.cpp b/src/libcamera/stream.cpp index 1f75dbbc..978d7275 100644 --- a/src/libcamera/stream.cpp +++ b/src/libcamera/stream.cpp @@ -392,7 +392,23 @@ StreamConfiguration::StreamConfiguration(const StreamFormats &formats) */ std::string StreamConfiguration::toString() const { - return size.toString() + "-" + pixelFormat.toString(); + std::stringstream ss; + ss << *this; + + return ss.str(); +} + +/** + * \brief Insert a text representation of a StreamConfiguration into an output + * stream + * \param[in] out The output stream + * \param[in] cfg The StreamConfiguration + * \return The output stream \a out + */ +std::ostream &operator<<(std::ostream &out, const StreamConfiguration &cfg) +{ + out << cfg.size << "-" << cfg.pixelFormat; + return out; } /** diff --git a/src/libcamera/v4l2_device.cpp b/src/libcamera/v4l2_device.cpp index 664b74af..2f65a43a 100644 --- a/src/libcamera/v4l2_device.cpp +++ b/src/libcamera/v4l2_device.cpp @@ -565,7 +565,15 @@ std::unique_ptr<ControlId> V4L2Device::v4l2ControlId(const v4l2_query_ext_ctrl & const std::string name(static_cast<const char *>(ctrl.name), len); const ControlType type = v4l2CtrlType(ctrl.type); - return std::make_unique<ControlId>(ctrl.id, name, "v4l2", type); + ControlId::DirectionFlags flags; + if (ctrl.flags & V4L2_CTRL_FLAG_READ_ONLY) + flags = ControlId::Direction::Out; + else if (ctrl.flags & V4L2_CTRL_FLAG_WRITE_ONLY) + flags = ControlId::Direction::In; + else + flags = ControlId::Direction::In | ControlId::Direction::Out; + + return std::make_unique<ControlId>(ctrl.id, name, "v4l2", type, flags); } /** diff --git a/src/libcamera/v4l2_pixelformat.cpp b/src/libcamera/v4l2_pixelformat.cpp index eb9ac222..e8b3eb9c 100644 --- a/src/libcamera/v4l2_pixelformat.cpp +++ b/src/libcamera/v4l2_pixelformat.cpp @@ -373,6 +373,40 @@ V4L2PixelFormat::fromPixelFormat(const PixelFormat &pixelFormat) } /** + * \brief Test if a V4L2PixelFormat is one of the line based generic metadata + * formats + * + * A limited number of metadata formats, the ones that represents generic + * line-based metadata buffers, need to have their width, height and + * bytesperline set by userspace. + * + * This function tests if the current V4L2PixelFormat is one of those. + * + * Note: It would have been nicer to store this information in a + * V4L2PixelFormat::Info instance, but as metadata format are not exposed to + * applications, there are no PixelFormat and DRM fourcc codes associated to + * them. + * + * \return True if the V4L2PixelFormat() is a generic line based format, false + * otherwise + */ +bool V4L2PixelFormat::isGenericLineBasedMetadata() const +{ + switch (fourcc_) { + case V4L2_META_FMT_GENERIC_8: + case V4L2_META_FMT_GENERIC_CSI2_10: + case V4L2_META_FMT_GENERIC_CSI2_12: + case V4L2_META_FMT_GENERIC_CSI2_14: + case V4L2_META_FMT_GENERIC_CSI2_16: + case V4L2_META_FMT_GENERIC_CSI2_20: + case V4L2_META_FMT_GENERIC_CSI2_24: + return true; + default: + return false; + } +} + +/** * \brief Insert a text representation of a V4L2PixelFormat into an output * stream * \param[in] out The output stream diff --git a/src/libcamera/v4l2_subdevice.cpp b/src/libcamera/v4l2_subdevice.cpp index 9f2ec479..33279654 100644 --- a/src/libcamera/v4l2_subdevice.cpp +++ b/src/libcamera/v4l2_subdevice.cpp @@ -8,12 +8,18 @@ #include "libcamera/internal/v4l2_subdevice.h" #include <fcntl.h> -#include <regex> #include <sstream> #include <string.h> #include <sys/ioctl.h> #include <unistd.h> +#pragma GCC diagnostic push +#if defined __SANITIZE_ADDRESS__ && defined __OPTIMIZE__ +#pragma GCC diagnostic ignored "-Wmaybe-uninitialized" +#endif +#include <regex> +#pragma GCC diagnostic pop + #include <linux/media-bus-format.h> #include <linux/v4l2-subdev.h> @@ -188,6 +194,20 @@ const std::map<uint32_t, MediaBusFormatInfo> mediaBusFormatInfo{ .bitsPerPixel = 24, .colourEncoding = PixelFormatInfo::ColourEncodingRGB, } }, + { MEDIA_BUS_FMT_RGB121212_1X36, { + .name = "RGB121212_1X36", + .code = MEDIA_BUS_FMT_RGB121212_1X36, + .type = MediaBusFormatInfo::Type::Image, + .bitsPerPixel = 36, + .colourEncoding = PixelFormatInfo::ColourEncodingRGB, + } }, + { MEDIA_BUS_FMT_RGB202020_1X60, { + .name = "RGB202020_1X60", + .code = MEDIA_BUS_FMT_RGB202020_1X60, + .type = MediaBusFormatInfo::Type::Image, + .bitsPerPixel = 60, + .colourEncoding = PixelFormatInfo::ColourEncodingRGB, + } }, { MEDIA_BUS_FMT_ARGB8888_1X32, { .name = "ARGB8888_1X32", .code = MEDIA_BUS_FMT_ARGB8888_1X32, @@ -678,6 +698,34 @@ const std::map<uint32_t, MediaBusFormatInfo> mediaBusFormatInfo{ .bitsPerPixel = 16, .colourEncoding = PixelFormatInfo::ColourEncodingRAW } }, + { MEDIA_BUS_FMT_SBGGR20_1X20, { + .name = "SBGGR20_1X20", + .code = MEDIA_BUS_FMT_SBGGR20_1X20, + .type = MediaBusFormatInfo::Type::Image, + .bitsPerPixel = 20, + .colourEncoding = PixelFormatInfo::ColourEncodingRAW + } }, + { MEDIA_BUS_FMT_SGBRG20_1X20, { + .name = "SGBRG20_1X20", + .code = MEDIA_BUS_FMT_SGBRG20_1X20, + .type = MediaBusFormatInfo::Type::Image, + .bitsPerPixel = 20, + .colourEncoding = PixelFormatInfo::ColourEncodingRAW + } }, + { MEDIA_BUS_FMT_SGRBG20_1X20, { + .name = "SGRBG20_1X20", + .code = MEDIA_BUS_FMT_SGRBG20_1X20, + .type = MediaBusFormatInfo::Type::Image, + .bitsPerPixel = 20, + .colourEncoding = PixelFormatInfo::ColourEncodingRAW + } }, + { MEDIA_BUS_FMT_SRGGB20_1X20, { + .name = "SRGGB20_1X20", + .code = MEDIA_BUS_FMT_SRGGB20_1X20, + .type = MediaBusFormatInfo::Type::Image, + .bitsPerPixel = 20, + .colourEncoding = PixelFormatInfo::ColourEncodingRAW + } }, /* \todo Clarify colour encoding for HSV formats */ { MEDIA_BUS_FMT_AHSV8888_1X32, { .name = "AHSV8888_1X32", @@ -700,6 +748,69 @@ const std::map<uint32_t, MediaBusFormatInfo> mediaBusFormatInfo{ .bitsPerPixel = 0, .colourEncoding = PixelFormatInfo::ColourEncodingRAW, } }, + { MEDIA_BUS_FMT_META_8, { + .name = "META_8", + .code = MEDIA_BUS_FMT_META_8, + .type = MediaBusFormatInfo::Type::Metadata, + .bitsPerPixel = 8, + .colourEncoding = PixelFormatInfo::ColourEncodingRAW, + } }, + { MEDIA_BUS_FMT_META_10, { + .name = "META_10", + .code = MEDIA_BUS_FMT_META_10, + .type = MediaBusFormatInfo::Type::Metadata, + .bitsPerPixel = 10, + .colourEncoding = PixelFormatInfo::ColourEncodingRAW, + } }, + { MEDIA_BUS_FMT_META_12, { + .name = "META_12", + .code = MEDIA_BUS_FMT_META_12, + .type = MediaBusFormatInfo::Type::Metadata, + .bitsPerPixel = 12, + .colourEncoding = PixelFormatInfo::ColourEncodingRAW, + } }, + { MEDIA_BUS_FMT_META_14, { + .name = "META_14", + .code = MEDIA_BUS_FMT_META_14, + .type = MediaBusFormatInfo::Type::Metadata, + .bitsPerPixel = 14, + .colourEncoding = PixelFormatInfo::ColourEncodingRAW, + } }, + { MEDIA_BUS_FMT_META_16, { + .name = "META_16", + .code = MEDIA_BUS_FMT_META_16, + .type = MediaBusFormatInfo::Type::Metadata, + .bitsPerPixel = 16, + .colourEncoding = PixelFormatInfo::ColourEncodingRAW, + } }, + { MEDIA_BUS_FMT_META_20, { + .name = "META_20", + .code = MEDIA_BUS_FMT_META_20, + .type = MediaBusFormatInfo::Type::Metadata, + .bitsPerPixel = 20, + .colourEncoding = PixelFormatInfo::ColourEncodingRAW, + } }, + { MEDIA_BUS_FMT_META_24, { + .name = "META_24", + .code = MEDIA_BUS_FMT_META_24, + .type = MediaBusFormatInfo::Type::Metadata, + .bitsPerPixel = 24, + .colourEncoding = PixelFormatInfo::ColourEncodingRAW, + } }, + { MEDIA_BUS_FMT_CCS_EMBEDDED, { + .name = "CCS_EMBEDDED", + .code = MEDIA_BUS_FMT_CCS_EMBEDDED, + .type = MediaBusFormatInfo::Type::EmbeddedData, + .bitsPerPixel = 0, + .colourEncoding = PixelFormatInfo::ColourEncodingRAW, + } }, + { MEDIA_BUS_FMT_OV2740_EMBEDDED, { + .name = "OV2740_EMBEDDED", + .code = MEDIA_BUS_FMT_CCS_EMBEDDED, + .type = MediaBusFormatInfo::Type::EmbeddedData, + .bitsPerPixel = 0, + .colourEncoding = PixelFormatInfo::ColourEncodingRAW, + } }, }; } /* namespace */ diff --git a/src/libcamera/v4l2_videodevice.cpp b/src/libcamera/v4l2_videodevice.cpp index a5cf6784..e241eb47 100644 --- a/src/libcamera/v4l2_videodevice.cpp +++ b/src/libcamera/v4l2_videodevice.cpp @@ -888,7 +888,7 @@ int V4L2VideoDevice::setFormat(V4L2DeviceFormat *format) int V4L2VideoDevice::getFormatMeta(V4L2DeviceFormat *format) { struct v4l2_format v4l2Format = {}; - struct v4l2_meta_format *pix = &v4l2Format.fmt.meta; + struct v4l2_meta_format *meta = &v4l2Format.fmt.meta; int ret; v4l2Format.type = bufferType_; @@ -898,25 +898,42 @@ int V4L2VideoDevice::getFormatMeta(V4L2DeviceFormat *format) return ret; } - format->size.width = 0; - format->size.height = 0; - format->fourcc = V4L2PixelFormat(pix->dataformat); + format->fourcc = V4L2PixelFormat(meta->dataformat); + format->planes[0].size = meta->buffersize; format->planesCount = 1; - format->planes[0].bpl = pix->buffersize; - format->planes[0].size = pix->buffersize; + + bool genericLineBased = caps_.isMetaCapture() && + format->fourcc.isGenericLineBasedMetadata(); + + if (genericLineBased) { + format->size.width = meta->width; + format->size.height = meta->height; + format->planes[0].bpl = meta->bytesperline; + } else { + format->size.width = 0; + format->size.height = 0; + format->planes[0].bpl = meta->buffersize; + } return 0; } int V4L2VideoDevice::trySetFormatMeta(V4L2DeviceFormat *format, bool set) { + bool genericLineBased = caps_.isMetaCapture() && + format->fourcc.isGenericLineBasedMetadata(); struct v4l2_format v4l2Format = {}; - struct v4l2_meta_format *pix = &v4l2Format.fmt.meta; + struct v4l2_meta_format *meta = &v4l2Format.fmt.meta; int ret; v4l2Format.type = bufferType_; - pix->dataformat = format->fourcc; - pix->buffersize = format->planes[0].size; + meta->dataformat = format->fourcc; + meta->buffersize = format->planes[0].size; + if (genericLineBased) { + meta->width = format->size.width; + meta->height = format->size.height; + meta->bytesperline = format->planes[0].bpl; + } ret = ioctl(set ? VIDIOC_S_FMT : VIDIOC_TRY_FMT, &v4l2Format); if (ret) { LOG(V4L2, Error) @@ -929,12 +946,18 @@ int V4L2VideoDevice::trySetFormatMeta(V4L2DeviceFormat *format, bool set) * Return to caller the format actually applied on the video device, * which might differ from the requested one. */ - format->size.width = 0; - format->size.height = 0; - format->fourcc = V4L2PixelFormat(pix->dataformat); + format->fourcc = V4L2PixelFormat(meta->dataformat); format->planesCount = 1; - format->planes[0].bpl = pix->buffersize; - format->planes[0].size = pix->buffersize; + format->planes[0].size = meta->buffersize; + if (genericLineBased) { + format->size.width = meta->width; + format->size.height = meta->height; + format->planes[0].bpl = meta->bytesperline; + } else { + format->size.width = 0; + format->size.height = 0; + format->planes[0].bpl = meta->buffersize; + } return 0; } diff --git a/src/ipa/libipa/vector.cpp b/src/libcamera/vector.cpp index 8019f8cf..85ca2208 100644 --- a/src/ipa/libipa/vector.cpp +++ b/src/libcamera/vector.cpp @@ -5,7 +5,7 @@ * Vector and related operations */ -#include "vector.h" +#include "libcamera/internal/vector.h" #include <libcamera/base/log.h> @@ -18,8 +18,6 @@ namespace libcamera { LOG_DEFINE_CATEGORY(Vector) -namespace ipa { - /** * \class Vector * \brief Vector class @@ -346,6 +344,4 @@ bool vectorValidateYaml(const YamlObject &obj, unsigned int size) } #endif /* __DOXYGEN__ */ -} /* namespace ipa */ - } /* namespace libcamera */ diff --git a/src/libcamera/yaml_parser.cpp b/src/libcamera/yaml_parser.cpp index db256ec5..a5e42461 100644 --- a/src/libcamera/yaml_parser.cpp +++ b/src/libcamera/yaml_parser.cpp @@ -8,10 +8,10 @@ #include "libcamera/internal/yaml_parser.h" #include <charconv> -#include <cstdlib> #include <errno.h> #include <functional> #include <limits> +#include <stdlib.h> #include <libcamera/base/file.h> #include <libcamera/base/log.h> @@ -194,7 +194,7 @@ YamlObject::Getter<double>::get(const YamlObject &obj) const if (obj.type_ != Type::Value) return std::nullopt; - if (obj.value_ == "") + if (obj.value_.empty()) return std::nullopt; char *end; @@ -509,8 +509,17 @@ YamlParserContext::EventPtr YamlParserContext::nextEvent() EventPtr event(new yaml_event_t); /* yaml_parser_parse returns 1 when it succeeds */ - if (!yaml_parser_parse(&parser_, event.get())) + if (!yaml_parser_parse(&parser_, event.get())) { + File *file = static_cast<File *>(parser_.read_handler_data); + + LOG(YamlParser, Error) << file->fileName() << ":" + << parser_.problem_mark.line << ":" + << parser_.problem_mark.column << " " + << parser_.problem << " " + << parser_.context; + return nullptr; + } return event; } diff --git a/src/meson.build b/src/meson.build index 76198e95..8eb8f05b 100644 --- a/src/meson.build +++ b/src/meson.build @@ -29,8 +29,18 @@ endif # libyuv, used by the Android adaptation layer and the virtual pipeline handler. # Fallback to a subproject if libyuv isn't found, as it's typically not provided -# by distributions. -libyuv_dep = dependency('libyuv', required : false) +# by distributions. Where libyuv is provided by a distribution, it may not +# always supply a pkg-config implementation, requiring cxx.find_library() to +# search for it. +if not get_option('force_fallback_for').contains('libyuv') + libyuv_dep = dependency('libyuv', required : false) + if not libyuv_dep.found() + libyuv_dep = cxx.find_library('yuv', has_headers : 'libyuv.h', + required : false) + endif +else + libyuv_dep = dependency('', required : false) +endif if (pipelines.contains('virtual') or get_option('android').allowed()) and \ not libyuv_dep.found() diff --git a/src/py/libcamera/gen-py-controls.py b/src/py/libcamera/gen-py-controls.py index cf09c146..d43a7c1c 100755 --- a/src/py/libcamera/gen-py-controls.py +++ b/src/py/libcamera/gen-py-controls.py @@ -83,7 +83,7 @@ def main(argv): vendors.append(vendor) for ctrl in data['controls']: - ctrl = Control(*ctrl.popitem(), vendor) + ctrl = Control(*ctrl.popitem(), vendor, args.mode) controls.append(extend_control(ctrl, args.mode)) data = { diff --git a/src/v4l2/meson.build b/src/v4l2/meson.build index 58f53bf3..2c040414 100644 --- a/src/v4l2/meson.build +++ b/src/v4l2/meson.build @@ -1,12 +1,11 @@ # SPDX-License-Identifier: CC0-1.0 -if not get_option('v4l2') - v4l2_enabled = false +v4l2_enabled = get_option('v4l2').allowed() + +if not v4l2_enabled subdir_done() endif -v4l2_enabled = true - v4l2_compat_sources = files([ 'v4l2_camera.cpp', 'v4l2_camera_file.cpp', |