/* SPDX-License-Identifier: GPL-2.0-or-later */ /* * Copyright (C) 2019, Google Inc. * * camera_session.cpp - Camera capture session */ #include #include #include #include #include #include #include "../common/event_loop.h" #include "../common/stream_options.h" #include "camera_session.h" #include "capture_script.h" #include "file_sink.h" #ifdef HAVE_KMS #include "kms_sink.h" #endif #include "main.h" #ifdef HAVE_SDL #include "sdl_sink.h" #endif using namespace libcamera; CameraSession::CameraSession(CameraManager *cm, const std::string &cameraId, unsigned int cameraIndex, const OptionsParser::Options &options) : options_(options), cameraIndex_(cameraIndex), last_(0), queueCount_(0), captureCount_(0), captureLimit_(0), printMetadata_(false) { char *endptr; unsigned long index = strtoul(cameraId.c_str(), &endptr, 10); if (*endptr == '\0' && index > 0 && index <= cm->cameras().size()) camera_ = cm->cameras()[index - 1]; else camera_ = cm->get(cameraId); if (!camera_) { std::cerr << "Camera " << cameraId << " not found" << std::endl; return; } if (camera_->acquire()) { std::cerr << "Failed to acquire camera " << cameraId << std::endl; return; } std::vector roles = StreamKeyValueParser::roles(options_[OptStream]); std::unique_ptr config = camera_->generateConfiguration(roles); if (!config || config->size() != roles.size()) { std::cerr << "Failed to get default stream configuration" << std::endl; return; } if (options_.isSet(OptOrientation)) { std::string orientOpt = options_[OptOrientation].toString(); static const std::map orientations{ { "rot0", libcamera::Orientation::Rotate0 }, { "rot180", libcamera::Orientation::Rotate180 }, { "mirror", libcamera::Orientation::Rotate0Mirror }, { "flip", libcamera::Orientation::Rotate180Mirror }, }; auto orientation = orientations.find(orientOpt); if (orientation == orientations.end()) { std::cerr << "Invalid orientation " << orientOpt << std::endl; return; } config->orientation = orientation->second; } /* Apply configuration if explicitly requested. */ if (StreamKeyValueParser::updateConfiguration(config.get(), options_[OptStream])) { std::cerr << "Failed to update configuration" << std::endl; return; } bool strictFormats = options_.isSet(OptStrictFormats); #ifdef HAVE_KMS if (options_.isSet(OptDisplay)) { if (options_.isSet(OptFile)) { std::cerr << "--display and --file options are mutually exclusive" << std::endl; return; } if (roles.size() != 1) { std::cerr << "Display doesn't support multiple streams" << std::endl; return; } if (roles[0] != StreamRole::Viewfinder) { std::cerr << "Display requires a viewfinder stream" << std::endl; return; } } #endif if (options_.isSet(OptCaptureScript)) { std::string scriptName = options_[OptCaptureScript].toString(); script_ = std::make_unique(camera_, scriptName); if (!script_->valid()) { std::cerr << "Invalid capture script '" << scriptName << "'" << std::endl; return; } } switch (config->validate()) { case CameraConfiguration::Valid: break; case CameraConfiguration::Adjusted: if (strictFormats) { std::cout << "Adjusting camera configuration disallowed by --strict-formats argument" << std::endl; return; } std::cout << "Camera configuration adjusted" << std::endl; break; case CameraConfiguration::Invalid: std::cout << "Camera configuration invalid" << std::endl; return; } config_ = std::move(config); } CameraSession::~CameraSession() { if (camera_) camera_->release(); } void CameraSession::listControls() const { for (const auto &[id, info] : camera_->controls()) { std::cout << "Control: " << id->name() << ": " << info.toString() << std::endl; } } void CameraSession::listProperties() const { for (const auto &[key, value] : camera_->properties()) { const ControlId *id = properties::properties.at(key); std::cout << "Property: " << id->name() << " = " << value.toString() << std::endl; } } void CameraSession::infoConfiguration() const { unsigned int index = 0; for (const StreamConfiguration &cfg : *config_) { std::cout << index << ": " << cfg.toString() << std::endl; const StreamFormats &formats = cfg.formats(); for (PixelFormat pixelformat : formats.pixelformats()) { std::cout << " * Pixelformat: " << pixelformat << " " << formats.range(pixelformat).toString() << std::endl; for (const Size &size : formats.sizes(pixelformat)) std::cout << " - " << size << std::endl; } index++; } } int CameraSession::start() { int ret; queueCount_ = 0; captureCount_ = 0; captureLimit_ = options_[OptCapture].toInteger(); printMetadata_ = options_.isSet(OptMetadata); ret = camera_->configure(config_.get()); if (ret < 0) { std::cout << "Failed to configure camera" << std::endl; return ret; } streamNames_.clear(); for (unsigned int index = 0; index < config_->size(); ++index) { StreamConfiguration &cfg = config_->at(index); streamNames_[cfg.stream()] = "cam" + std::to_string(cameraIndex_) + "-stream" + std::to_string(index); } camera_->requestCompleted.connect(this, &CameraSession::requestComplete); #ifdef HAVE_KMS if (options_.isSet(OptDisplay)) sink_ = std::make_unique(options_[OptDisplay].toString()); #endif #ifdef HAVE_SDL if (options_.isSet(OptSDL)) sink_ = std::make_unique(); #endif if (options_.isSet(OptFile)) { if (!options_[OptFile].toString().empty()) sink_ = std::make_unique(camera_.get(), streamNames_, options_[OptFile]); else sink_ = std::make_unique(camera_.get(), streamNames_); } if (sink_) { ret = sink_->configure(*config_); if (ret < 0) { std::cout << "Failed to configure frame sink" << std::endl; return ret; } sink_->requestProcessed.connect(this, &CameraSession::sinkRelease); } allocator_ = std::make_unique(camera_); return startCapture(); } void CameraSession::stop() { int ret = camera_->stop(); if (ret) std::cout << "Failed to stop capture" << std::endl; if (sink_) { ret = sink_->stop(); if (ret) std::cout << "Failed to stop frame sink" << std::endl; } sink_.reset(); requests_.clear(); allocator_.reset(); } int CameraSession::startCapture() { int ret; /* Identify the stream with the least number of buffers. */ unsigned int nbuffers = UINT_MAX; for (StreamConfiguration &cfg : *config_) { ret = allocator_->allocate(cfg.stream()); if (ret < 0) { std::cerr << "Can't allocate buffers" << std::endl; return -ENOMEM; } unsigned int allocated = allocator_->buffers(cfg.stream()).size(); nbuffers = std::min(nbuffers, allocated); } /* * TODO: make cam tool smarter to support still capture by for * example pushing a button. For now run all streams all the time. */ for (unsigned int i = 0; i < nbuffers; i++) { std::unique_ptr request = camera_->createRequest(); if (!request) { std::cerr << "Can't create request" << std::endl; return -ENOMEM; } for (StreamConfiguration &cfg : *config_) { Stream *stream = cfg.stream(); const std::vector> &buffers = allocator_->buffers(stream); const std::unique_ptr &buffer = buffers[i]; ret = request->addBuffer(stream, buffer.get()); if (ret < 0) { std::cerr << "Can't set buffer for request" << std::endl; return ret; } if (sink_) sink_->mapBuffer(buffer.get()); } requests_.push_back(std::move(request)); } if (sink_) { ret = sink_->start(); if (ret) { std::cout << "Failed to start frame sink" << std::endl; return ret; } } ret = camera_->start(); if (ret) { std::cout << "Failed to start capture" << std::endl; if (sink_) sink_->stop(); return ret; } for (std::unique_ptr &request : requests_) { ret = queueRequest(request.get()); if (ret < 0) { std::cerr << "Can't queue request" << std::endl; camera_->stop(); if (sink_) sink_->stop(); return ret; } } if (captureLimit_) std::cout << "cam" << cameraIndex_ << ": Capture " << captureLimit_ << " frames" << std::endl; else std::cout << "cam" << cameraIndex_ << ": Capture until user interrupts by SIGINT" << std::endl; return 0; } int CameraSession::queueRequest(Request *request) { if (captureLimit_ && queueCount_ >= captureLimit_) return 0; if (script_) request->controls() = script_->frameControls(queueCount_); queueCount_++; return camera_->queueRequest(request); } void CameraSession::requestComplete(Request *request) { if (request->status() == Request::RequestCancelled) return; /* * Defer processing of the completed request to the event loop, to avoid * blocking the camera manager thread. */ EventLoop::instance()->callLater([=]() { processRequest(request); }); } void CameraSession::processRequest(Request *request) { /* * If we've reached the capture limit, we're done. This doesn't * duplicate the check below that emits the captureDone signal, as this * function will be called for each request still in flight after the * capture limit is reached and we don't want to emit the signal every * single time. */ if (captureLimit_ && captureCount_ >= captureLimit_) return; const Request::BufferMap &buffers = request->buffers(); /* * Compute the frame rate. The timestamp is arbitrarily retrieved from * the first buffer, as all buffers should have matching timestamps. */ uint64_t ts = buffers.begin()->second->metadata().timestamp; double fps = ts - last_; fps = last_ != 0 && fps ? 1000000000.0 / fps : 0.0; last_ = ts; bool requeue = true; std::stringstream info; info << ts / 1000000000 << "." << std::setw(6) << std::setfill('0') << ts / 1000 % 1000000 << " (" << std::fixed << std::setprecision(# SPDX-License-Identifier: GPL-2.0-or-later # # Copyright (C) 2022, Paul Elder <paul.elder@ideasonboard.com> # # libtuning.py - An infrastructure for camera tuning tools import argparse import libtuning as lt import libtuning.utils as utils from libtuning.utils import eprint from enum import Enum, IntEnum class Color(IntEnum): R = 0 GR = 1 GB = 2 B = 3 class Debug(Enum): Plot = 1 # @brief What to do with the leftover pixels after dividing them into ALSC # sectors, when the division gradient is uniform # @var Float Force floating point division so all sectors divide equally # @var DistributeFront Divide the remainder equally (until running out, # obviously) into the existing sectors, starting from the front # @var DistributeBack Same as DistributeFront but starting from the back class Remainder(Enum): Float = 0 DistributeFront = 1 DistributeBack = 2 # @brief A helper class to contain a default value for a module configuration # parameter class Param(object): # @var Required The value contained in this instance is irrelevant, and the # value must be provided by the tuning configuration file. # @var Optional If the value is not provided by the tuning configuration # file, then the value contained in this instance will be used instead. # @var Hardcode The value contained in this instance will be used class Mode(Enum): Required = 0 Optional = 1 Hardcode = 2 # @param name Name of the parameter. Shall match the name used in the # configuration file for the parameter # @param required Whether or not a value is required in the config # parameter of get_value() # @param val Default value (only relevant if mode is Optional) def __init__(self, name: str, required: Mode, val=None): self.name = name self.__required = required self.val = val def get_value(self, config: dict): if self.__required is self.Mode.Hardcode: return self.val if self.__required is self.Mode.Required and self.name not in config: raise ValueError(f'Parameter {self.name} is required but not provided in the configuration') return config[self.name] if self.required else self.val @property def required(self): return self.__required is self.Mode.Required # @brief Used by libtuning to auto-generate help information for the tuning # script on the available parameters for the configuration file # \todo Implement this @property def info(self): raise NotImplementedError class Tuner(object): # External functions def __init__(self, platform_name): self.name = platform_name self.modules = [] self.parser = None self.generator = None self.output_order = [] self.config = {} self.output = {} def add(self, module): self.modules.append(module) def set_input_parser(self, parser): self.parser = parser def set_output_formatter(self, output): self.generator = output def set_output_order(self, modules): self.output_order = modules # @brief Convert classes in self.output_order to the instances in self.modules def _prepare_output_order(self): output_order = self.output_order self.output_order = [] for module_type in output_order: modules = [module for module in self.modules if module.type == module_type.type] if len(modules) > 1: eprint(f'Multiple modules found for module type "{module_type.type}"') return False if len(modules) < 1: eprint(f'No module found for module type "{module_type.type}"') return False self.output_order.append(modules[0]) return True # \todo Validate parser and generator at Tuner construction time? def _validate_settings(self): if self.parser is None: eprint('Missing parser') return False if self.generator is None: eprint('Missing generator') return False if len(self.modules) == 0: eprint('No modules added') return False if len(self.output_order) != len(self.modules): eprint('Number of outputs does not match number of modules') return False return True def _process_args(self, argv, platform_name): parser = argparse.ArgumentParser(description=f'Camera Tuning for {platform_name}') parser.add_argument('-i', '--input', type=str, required=True, help='''Directory containing calibration images (required). Images for ALSC must be named "alsc_{Color Temperature}k_1[u].dng", and all other images must be named "{Color Temperature}k_{Lux Level}l.dng"''') parser.add_argument('-o', '--output', type=str, required=True, help='Output file (required)') # It is not our duty to scan all modules to figure out their default # options, so simply return an empty configuration if none is provided. parser.add_argument('-c', '--config', type=str, default='', help='Config file (optional)') # \todo Check if we really need this or if stderr is good enough, or if # we want a better logging infrastructure with log levels parser.add_argument('-l', '--log', type=str, default=None, help='Output log file (optional)') return parser.parse_args(argv[1:]) def run(self, argv): args = self._process_args(argv, self.name) if args is None: return -1 if not self._validate_settings(): return -1 if not self._prepare_output_order(): return -1 if len(args.config) > 0: self.config, disable = self.parser.parse(args.config, self.modules) else: self.config = {'general': {}} disable = [] # Remove disabled modules for module in disable: if module in self.modules: self.modules.remove(module) for module in self.modules: if not module.validate_config(self.config): eprint(f'Config is invalid for module {module.type}') return -1 has_lsc = any(isinstance(m, lt.modules.lsc.LSC) for m in self.modules) # Only one LSC module allowed has_only_lsc = has_lsc and len(self.modules) == 1 images = utils.load_images(args.input, self.config, not has_only_lsc, has_lsc) if images is None or len(images) == 0: eprint(f'No images were found, or able to load') return -1 # Do the tuning for module in self.modules: out = module.process(self.config, images, self.output) if out is None: eprint(f'Module {module.name} failed to process, aborting') break self.output[module] = out self.generator.write(args.output, self.output, self.output_order) return 0