[{"id":26458,"web_url":"https://patchwork.libcamera.org/comment/26458/","msgid":"<Y+yzwqK00l+sFOvk@pendragon.ideasonboard.com>","date":"2023-02-15T10:28:18","subject":"Re: [libcamera-devel] [RFC] CPU-only auto-exposure,\n\tand where to put it","submitter":{"id":2,"url":"https://patchwork.libcamera.org/api/people/2/","name":"Laurent Pinchart","email":"laurent.pinchart@ideasonboard.com"},"content":"Hi Pavel,\n\nThank you for the patch.\n\nOn Fri, Feb 10, 2023 at 08:37:16PM +0100, Pavel Machek via libcamera-devel wrote:\n> Hi!\n> \n> So I have this, which kind-of works on PinePhone and Librem 5. I\n> started with autoexposure.\n> \n> AgcExposureMode and divideUpExposure are from RPi code, I'm not sure\n> how to reuse the code.\n\nCommon logic show be stored in libipa. As that requires building\nabstractions, it's not always worth it, so some code duplication is\nacceptable. It's a case-by-case decision.\n\n> I guess I should convert statistics to histograms.\n\nThat would be nice.\n\n> I have placed my hooks in SimpleCameraData::bufferReady. Is that\n> reasonable or is there better place?\n> \n> Where should the code go? It is now in\n> src/libcamera/pipeline/simple/simple.cpp, would something like\n> src/libcamera/pipeline/simple/ae.cpp be suitable?\n\nThis should be abstracted in a software ISP class, usable by different\npipeline handlers. The way I imagine it, it would have the exact same\nAPI as a GPU ISP class, so the two could be used interchangeably.\n\nFollowing libcamera's architecture, the control algorithms for the\nsoftware (CPU or GPU) ISP would go to an IPA module, but that may not be\nworth it here. I thus don't object in principle against bundling the\nalgorithms with the ISP implementation, but they should at least be\ndesigned in the same spirit as IPA modules, with control algorithm code\nseparate from the statistics gathering and image processing code.\n\n> Don't look at the code too much, it clearly needs... more work.\n\nI like when I'm told not to look at code, that's less work :-)\n\n> diff --git a/src/libcamera/pipeline/simple/simple.cpp b/src/libcamera/pipeline/simple/simple.cpp\n> index 24ded4db..92d2e8a5 100644\n> --- a/src/libcamera/pipeline/simple/simple.cpp\n> +++ b/src/libcamera/pipeline/simple/simple.cpp\n> @@ -27,6 +28,7 @@\n>  #include <libcamera/control_ids.h>\n>  #include <libcamera/request.h>\n>  #include <libcamera/stream.h>\n> +#include <libcamera/formats.h>\n>  \n>  #include \"libcamera/internal/camera.h\"\n>  #include \"libcamera/internal/camera_sensor.h\"\n> @@ -36,7 +38,9 @@\n>  #include \"libcamera/internal/pipeline_handler.h\"\n>  #include \"libcamera/internal/v4l2_subdevice.h\"\n>  #include \"libcamera/internal/v4l2_videodevice.h\"\n> +#include \"libcamera/internal/mapped_framebuffer.h\"\n>  \n> +using libcamera::utils::Duration;\n>  \n>  namespace libcamera {\n>  \n> @@ -213,6 +217,7 @@ public:\n>  \tint setupFormats(V4L2SubdeviceFormat *format,\n>  \t\t\t V4L2Subdevice::Whence whence);\n>  \tvoid bufferReady(FrameBuffer *buffer);\n> +\tvoid autoProcessing(Request *request);\n>  \n>  \tunsigned int streamIndex(const Stream *stream) const\n>  \t{\n> @@ -724,6 +729,372 @@ int SimpleCameraData::setupFormats(V4L2SubdeviceFormat *format,\n>  \treturn 0;\n>  }\n>  \n> +class MappedPixels {\n> +public:\n> +\tunsigned char *data;\n> +\tPixelFormat format;\n> +\tSize size;\n> +\tint bpp;\n> +\tint maxval;\n> +  \n> +\tMappedPixels(unsigned char *_data, const struct StreamConfiguration &_config) {\n> +\t\tdata = _data;\n> +\t\tformat = _config.pixelFormat;\n> +\t\tsize = _config.size;\n> +\n> +\t\tswitch (format) {\n> +\t\tcase formats::SGRBG8:\n> +\t\t\tbpp = 1;\n> +\t\t\tmaxval = 255;\n> +\t\t\tbreak;\n> +\t\tcase formats::SBGGR8:\n> +\t\t\tbpp = 1;\n> +\t\t\tmaxval = 255;\n> +\t\t\tbreak;\n> +\t\tcase formats::SGRBG10:\n> +\t\t\tbpp = 2;\n> +\t\t\tmaxval = 1023;\n> +\t\t\tbreak;\n> +\t\tdefault:\n> +\t\t\tLOG(SimplePipeline, Error) << \"Mapped pixels \" << format << \" is unknown format.\";\n> +\t\t}\n> +\t}\n> +\n> +\tint getValue(unsigned int x, unsigned int y) {\n> +\t\tunsigned int v;\n> +\t\tif (x >= size.width)\n> +\t\t\tx = size.width - 1;\n> +\t\tif (y >= size.height)\n> +\t\t\ty = size.height - 1;\n> +\t\tint i = (x + size.width * y) * bpp;\n> +\t\tv = data[i];\n> +\t\tif (bpp > 1)\n> +\t\t\tv |= data[i+1] << 8;\n> +\t\treturn v;\n> +\t}\n> +\n> +\tint getMaxValue(unsigned int x, unsigned int y) {\n> +\t\tint v, v2;\n> +\n> +\t\tv = getValue(x, y);\n> +\t\tv2 = getValue(x+1, y);\n> +\t\tif (v2 > v)\n> +\t\t\tv = v2;\n> +\t\tv2 = getValue(x, y+1);\n> +\t\tif (v2 > v)\n> +\t\t\tv = v2;\n> +\t\tv2 = getValue(x+1, y+1);\n> +\t\tif (v2 > v)\n> +\t\t\tv = v2;\n> +\t\treturn v;\n> +\t}\n> +\n> +\tfloat getMaxValueR(float x, float y) {\n> +\t\tfloat v = getMaxValue(x * size.width, y * size.height);\n> +\t\treturn v/maxval;\n> +\t}\n> +\n> +\tvoid debugPaint(void) {\n> +\t\tchar map[] = \"  .,:;-+=*#\";\n> +\t\tfor (float y = 0; y < 1; y += 1/25.) {\n> +\t\t\tfor (float x = 0; x < 1; x += 1/80.) {\n> +\t\t\t\tfloat v = getMaxValueR(x, y);\n> +\t\t\t\tprintf(\"%c\", map[ int (v * (sizeof(map) - 2)) ]);\n> +\t\t\t}\n> +\t\t\tprintf(\"\\n\");\n> +\t\t}\n> +\t}\t\n> +};\n> +\n> +LOG_DEFINE_CATEGORY(SimpleAgc)\n> +LOG_DEFINE_CATEGORY(RPiAgc)\n> +\n> +// FIXME: from src/ipa/raspberrypi/controller/rpi/agc.h\n> +struct AgcExposureMode {\n> +        std::vector<libcamera::utils::Duration> shutter;\n> +        std::vector<double> gain;\n> +\n> +\tAgcExposureMode(void) {\n> +\t\tlibcamera::utils::Duration v1(1.0);\n> +\t\tlibcamera::utils::Duration v2(1000.0);\n> +\t\tlibcamera::utils::Duration v3(1000000.0);\n> +\t\tshutter = { v1, v2, v2, v3 };\n> +\t\tgain = { 1.0, 1.0, 16.0, 16.0 };\n> +\t}\n> +};\n> +\n> +\n> +struct AgcStatus {\n> +\tlibcamera::utils::Duration totalExposureValue; /* value for all exposure and gain for this image */\n> +\tlibcamera::utils::Duration targetExposureValue; /* (unfiltered) target total exposure AGC is aiming for */\n> +\tlibcamera::utils::Duration shutterTime;\n> +\tdouble analogueGain;\n> +\tchar exposureMode[32];\n> +\tchar constraintMode[32];\n> +\tchar meteringMode[32];\n> +\tdouble ev;\n> +\tlibcamera::utils::Duration flickerPeriod;\n> +\tint floatingRegionEnable;\n> +\tlibcamera::utils::Duration fixedShutter;\n> +\tdouble fixedAnalogueGain;\n> +\tdouble digitalGain;\n> +\tint locked;\n> +};\n> +\n> +class Agc {\n> +public:\n> +\tControlList ctrls;\n> +\n> +\tint exposure_min, exposure_max;\n> +\tint again_min, again_max;\n> +\tint dgain_min, dgain_max;\n> +\n> +        AgcStatus status_;\n> +  \tAgcExposureMode *exposureMode_;\n> +\n> +  \tlibcamera::utils::Duration shutter_conv;\n> +\n> +        struct ExposureValues {\n> +                ExposureValues();\n> +  \n> +                libcamera::utils::Duration shutter;\n> +                double analogueGain;\n> +                libcamera::utils::Duration totalExposure;\n> +                libcamera::utils::Duration totalExposureNoDG; /* without digital gain */\n> +        };\n> +\n> +\tstruct ExposureValues current_, filtered_;\n> +        int have_ad_gain;\n> +\tunsigned long cid_gain;\n> +\n> +\tAgc(std::unique_ptr<CameraSensor> & sensor_) {\n> +\t  /*\n> +\t    sudo yavta -w '0x009a0901 1' /dev/v4l-subdev0 # gc2145\n> +\t    sudo yavta -w '0x009a0901 1' /dev/v4l-subdev1 # ae, ov\n> +\t    sudo yavta -w '0x00980912 0' /dev/v4l-subdev1 # ag, ov\n> +\t    sudo yavta -l /dev/v4l-subdev1\n> +\t  */\n> +\t\thave_ad_gain = 0;\n> +\t\tif (have_ad_gain) {\n> +\t\t\tctrls = sensor_->getControls({ V4L2_CID_EXPOSURE, V4L2_CID_ANALOGUE_GAIN, V4L2_CID_DIGITAL_GAIN });\n> +\t\t\tcid_gain = V4L2_CID_ANALOGUE_GAIN;\n> +\t\t} else {\n> +\t\t\tctrls = sensor_->getControls({ V4L2_CID_EXPOSURE, V4L2_CID_GAIN });\n> +\t\t\tcid_gain = V4L2_CID_GAIN;\n> +\t\t}\n> +\n> +\t\tconst ControlInfoMap &infoMap = *ctrls.infoMap();\n> +\n> +\t\tconst ControlInfo &exposure_info = infoMap.find(V4L2_CID_EXPOSURE)->second;\n> +\t\tconst ControlInfo &gain_info = infoMap.find(cid_gain)->second;\n> +\t\tconst ControlInfo &dgain_info = infoMap.find(V4L2_CID_DIGITAL_GAIN)->second;\n> +\n> +\t        memset(&status_, 0, sizeof(status_));\n> +        \tstatus_.ev = 1.0;\n> +\n> +\t\texposureMode_ = new AgcExposureMode();\n> +\t\tlibcamera::utils::Duration msec(1);\n> +\t\tshutter_conv = msec;\n> +\n> +\t\texposure_min = exposure_info.min().get<int>();\n> +\t\tif (!exposure_min) {\n> +\t\t\tLOG(SimplePipeline, Error) << \"Minimum exposure is zero, that can't be linear\";\n> +\t\t\texposure_min = 1;\n> +\t\t}\n> +\t\texposure_max = exposure_info.max().get<int>();\n> +\t\tagain_min = gain_info.min().get<int>();\n> +\t\tif (!again_min) {\n> +\t\t\tLOG(SimplePipeline, Error) << \"Minimum gain is zero, that can't be linear\";\n> +\t\t\tagain_min = 100;\n> +\t\t}\n> +\t\t\n> +\t\tagain_max = gain_info.max().get<int>();\n> +\t\tif (have_ad_gain) {\n> +\t\t\tdgain_min = dgain_info.min().get<int>();\n> +\t\t\tdgain_max = dgain_info.max().get<int>();\n> +\t\t} else {\n> +\t\t\tdgain_min = 1;\n> +\t\t\tdgain_max = 1;\n> +\t\t}\n> +\n> +\t\tprintf(\"Exposure %d %d, gain %d %d, dgain %d %d\\n\", \n> +\t\t\t\texposure_min, exposure_max,\n> +\t\t\t\tagain_min, again_max,\n> +\t\t\t\tdgain_min, dgain_max);\n> +\t}\n> +\n> +\tvoid get_exposure() {\n> +\t\tint exposure = ctrls.get(V4L2_CID_EXPOSURE).get<int>();\n> +\t\tint gain = ctrls.get(cid_gain).get<int>();\n> +\t\tint dgain;\n> +\t\tif (have_ad_gain)\n> +\t\t\tdgain = ctrls.get(V4L2_CID_DIGITAL_GAIN).get<int>();\n> +\t\telse\n> +\t\t\tdgain = 1;\n> +\n> +\t\tprintf(\"Old exp %d, gain %d, dgain %d\\n\", exposure, gain, dgain);\n> +\n> +\t\tcurrent_.shutter = (double) exposure * shutter_conv;\n> +\t\tcurrent_.analogueGain = (double) gain / again_min;\n> +\t}\n> +\n> +\tvoid set_exposure(std::unique_ptr<CameraSensor> & sensor_) {\n> +\t\tint exposure = (int)(filtered_.shutter / shutter_conv);\n> +\t\tint gain = (int)(filtered_.analogueGain * again_min);\n> +\t\tprintf(\" new exp %d, gain %d, dgain %d \", exposure, gain, 0);\n> +\t\tctrls.set(V4L2_CID_EXPOSURE, exposure);\n> +\t\tctrls.set(cid_gain, (int)(filtered_.analogueGain * again_min));\n> +\t\tif (have_ad_gain)\n> +\t\t\tctrls.set(V4L2_CID_DIGITAL_GAIN, 768);\n> +\t\tsensor_->setControls(&ctrls);\n> +\t}\n> +\n> +\tvoid process(std::unique_ptr<CameraSensor> & sensor_, Request *request) {\n> +        for (auto [stream, buffer] : request->buffers()) {\n> +\t\tMappedFrameBuffer mappedBuffer(buffer, MappedFrameBuffer::MapFlag::Read);\n> +\t\tconst std::vector<Span<uint8_t>> &planes = mappedBuffer.planes();\n> +\t\tunsigned char *img = planes[0].data();\n> +\t\tconst struct StreamConfiguration &config = stream->configuration();\n> +\t\tMappedPixels pixels(img, config);\n> +\n> + \t\t//LOG(SimplePipeline, Error) << config.pixelFormat << \" \" << config.size;\n> +\n> +\t\tpixels.debugPaint();\n> +\n> +\t\tint bright = 0, too_bright = 0, total = 0;\n> +\n> +\t\tfor (float y = 0; y < 1; y += 1/30.) {\n> +\t\t\tfor (float x = 0; x < 1; x += 1/40.) {\n> +\t\t\t\tfloat v = pixels.getMaxValueR(x, y);\n> +\n> +\t\t\t\ttotal++;\n> +\t\t\t\tif (v > 240./255)\n> +\t\t\t\t\ttoo_bright++;\n> +\t\t\t\tif (v > 200./255)\n> +\t\t\t\t\tbright++;\n> +\t\t\t}\n> +\t\t}\n> +\n> +\t\tget_exposure();\n> +\t\tLOG(RPiAgc, Error) << \"Current values are \" << current_.shutter << \" and \" << current_.analogueGain;\n> +\t\tfiltered_ = current_;\n> +\t\tfiltered_.totalExposureNoDG = filtered_.analogueGain * filtered_.shutter;\n> +\t\tif ((bright / (float) total) < 0.01) {\n> +\t\t\tfiltered_.totalExposureNoDG  *= 1.1;\n> +\t\t\tprintf(\"ADJ+\");\n> +\t\t}\n> +\t\tif ((too_bright / (float) total) > 0.08) {\n> +\t\t\tfiltered_.totalExposureNoDG  *= 0.9;\n> +\t\t\tprintf(\"ADJ-\");\n> +\t\t}\n> +\n> +\t\tdivideUpExposure();\n> +\t\tset_exposure(sensor_);\n> +\t\t//LOG(SimpleAgc, Error) << \"Hello world\";\n> +\t}\n> +#if 0\n> +\tconst ControlInfoMap &infoMap = controls();\n> +\n> +\tif (infoMap.find(V4L2_CID_BRIGHTNESS) != infoMap.end()) {\n> +\t\t//const ControlInfo &brightness = infoMap.find(V4L2_CID_BRIGHTNESS)->second;\n> +\t}\n> +#endif\n> +\t}\n> +\n> +\tvoid divideUpExposure();\n> +\tDuration clipShutter(Duration shutter);\n> +};\n> +\n> +/* from ...agc.cpp */\n> +Agc::ExposureValues::ExposureValues()\n> +\t: shutter(0), analogueGain(0),\n> +\t  totalExposure(0), totalExposureNoDG(0)\n> +{\n> +}\n> +\n> +Duration Agc::clipShutter(Duration shutter)\n> +{\n> +  //if (maxShutter_)\n> +  //\t\tshutter = std::min(shutter, maxShutter_);\n> +\treturn shutter;\n> +}\n> +\n> +void Agc::divideUpExposure()\n> +{\n> +\t/*\n> +\t * Sending the fixed shutter/gain cases through the same code may seem\n> +\t * unnecessary, but it will make more sense when extend this to cover\n> +\t * variable aperture.\n> +\t */\n> +\tDuration exposureValue = filtered_.totalExposureNoDG;\n> +\tDuration shutterTime;\n> +\tdouble analogueGain;\n> +\tshutterTime = status_.fixedShutter ? status_.fixedShutter\n> +\t\t\t\t\t   : exposureMode_->shutter[0];\n> +\tshutterTime = clipShutter(shutterTime);\n> +\tanalogueGain = status_.fixedAnalogueGain != 0.0 ? status_.fixedAnalogueGain\n> +\t\t\t\t\t\t\t: exposureMode_->gain[0];\n> +\tif (shutterTime * analogueGain < exposureValue) {\n> +\t\tfor (unsigned int stage = 1;\n> +\t\t     stage < exposureMode_->gain.size(); stage++) {\n> +\t\t\tprintf(\"Stage %d\\n\", stage);\n> +\t\t\tLOG(RPiAgc, Error) << \"Stage \" << stage << \" s/g is \" << shutterTime << \" and \"\n> +\t\t\t   << analogueGain;\n> +\n> +\t\t\tif (!status_.fixedShutter) {\n> +\t\t\t\tDuration stageShutter =\n> +\t\t\t\t\tclipShutter(exposureMode_->shutter[stage]);\n> +\t\t\t\tif (stageShutter * analogueGain >= exposureValue) {\n> +\t\t\t\t\tshutterTime = exposureValue / analogueGain;\n> +\t\t\t\t\tbreak;\n> +\t\t\t\t}\n> +\t\t\t\tshutterTime = stageShutter;\n> +\t\t\t}\n> +\t\t\tif (status_.fixedAnalogueGain == 0.0) {\n> +\t\t\t\tif (exposureMode_->gain[stage] * shutterTime >= exposureValue) {\n> +\t\t\t\t\tanalogueGain = exposureValue / shutterTime;\n> +\t\t\t\t\tbreak;\n> +\t\t\t\t}\n> +\t\t\t\tanalogueGain = exposureMode_->gain[stage];\n> +\t\t\t}\n> +\t\t}\n> +\t}\n> +\tLOG(RPiAgc, Error) << \"Divided up shutter and gain are \" << shutterTime << \" and \"\n> +\t\t\t   << analogueGain;\n> +\t/*\n> +\t * Finally adjust shutter time for flicker avoidance (require both\n> +\t * shutter and gain not to be fixed).\n> +\t */\n> +\tif (!status_.fixedShutter && !status_.fixedAnalogueGain &&\n> +\t    status_.flickerPeriod) {\n> +\t\tint flickerPeriods = shutterTime / status_.flickerPeriod;\n> +\t\tif (flickerPeriods) {\n> +\t\t\tDuration newShutterTime = flickerPeriods * status_.flickerPeriod;\n> +\t\t\tanalogueGain *= shutterTime / newShutterTime;\n> +\t\t\t/*\n> +\t\t\t * We should still not allow the ag to go over the\n> +\t\t\t * largest value in the exposure mode. Note that this\n> +\t\t\t * may force more of the total exposure into the digital\n> +\t\t\t * gain as a side-effect.\n> +\t\t\t */\n> +\t\t\tanalogueGain = std::min(analogueGain, exposureMode_->gain.back());\n> +\t\t\tshutterTime = newShutterTime;\n> +\t\t}\n> +\t\tLOG(RPiAgc, Error) << \"After flicker avoidance, shutter \"\n> +\t\t\t\t   << shutterTime << \" gain \" << analogueGain;\n> +\t}\n> +\tfiltered_.shutter = shutterTime;\n> +\tfiltered_.analogueGain = analogueGain;\n> +}\n> +\n> +\n> +void SimpleCameraData::autoProcessing(Request *request)\n> +{\n> +\tAgc agc = Agc(sensor_);\n> +\n> +\tagc.process(sensor_, request);\n> +}\n> +\n>  void SimpleCameraData::bufferReady(FrameBuffer *buffer)\n>  {\n>  \tSimplePipelineHandler *pipe = SimpleCameraData::pipe();\n> @@ -823,8 +1197,10 @@ void SimpleCameraData::converterOutputDone(FrameBuffer *buffer)\n>  \n>  \t/* Complete the buffer and the request. */\n>  \tRequest *request = buffer->request();\n> -\tif (pipe->completeBuffer(request, buffer))\n> +\tif (pipe->completeBuffer(request, buffer)) {\n> +\t\tautoProcessing(request);\n>  \t\tpipe->completeRequest(request);\n> +\t}\n>  }\n>  \n>  /* Retrieve all source pads connected to a sink pad through active routes. */","headers":{"Return-Path":"<libcamera-devel-bounces@lists.libcamera.org>","X-Original-To":"parsemail@patchwork.libcamera.org","Delivered-To":"parsemail@patchwork.libcamera.org","Received":["from lancelot.ideasonboard.com (lancelot.ideasonboard.com\n\t[92.243.16.209])\n\tby patchwork.libcamera.org (Postfix) with ESMTPS id 20A86BE080\n\tfor <parsemail@patchwork.libcamera.org>;\n\tWed, 15 Feb 2023 10:28:22 +0000 (UTC)","from lancelot.ideasonboard.com (localhost [IPv6:::1])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTP id 7E53D625E3;\n\tWed, 15 Feb 2023 11:28:21 +0100 (CET)","from perceval.ideasonboard.com (perceval.ideasonboard.com\n\t[213.167.242.64])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTPS id 394C161EEB\n\tfor <libcamera-devel@lists.libcamera.org>;\n\tWed, 15 Feb 2023 11:28:20 +0100 (CET)","from pendragon.ideasonboard.com (213-243-189-158.bb.dnainternet.fi\n\t[213.243.189.158])\n\tby perceval.ideasonboard.com (Postfix) with ESMTPSA id 4A25727C;\n\tWed, 15 Feb 2023 11:28:19 +0100 (CET)"],"DKIM-Signature":["v=1; a=rsa-sha256; c=relaxed/simple; d=libcamera.org;\n\ts=mail; t=1676456901;\n\tbh=Rwy9eMEeJegNlFmYvBImq92B1/UXH/ndANLTmP7mSjo=;\n\th=Date:To:References:In-Reply-To:Subject:List-Id:List-Unsubscribe:\n\tList-Archive:List-Post:List-Help:List-Subscribe:From:Reply-To:Cc:\n\tFrom;\n\tb=y7D2bhMXlgLk2RknpLDEAjMFsKZIWc/bl3sEsh8oTB39iWwdVV5dw75weSL0nhZ5o\n\tW1bmZvqXI6gHvphICVT8J7f7wKrPe/HtabyUiutpMFbABgC8FzEuB7paRZM9CO+UCk\n\tPu26iX4Wq/zamabkd6PigX5XHCz8CSBFmmdJLxTu0lHPUxHVTpGS+OC6C9B4erm3TV\n\tMrlNwnZ58vdZLpvSe8NgcFN1R4P/tGTH1MLX+kOybA2yaQ7vEJxK3xJWYybndyI6Pj\n\tEjZKRFaRtndKGTnmBkSgxOuoRXtiYFl0WYQxteKWGYepWFIm1j/SnRTfiZk+Wb9QSL\n\tLAKlTlgIEPUpw==","v=1; a=rsa-sha256; c=relaxed/simple; d=ideasonboard.com;\n\ts=mail; t=1676456899;\n\tbh=Rwy9eMEeJegNlFmYvBImq92B1/UXH/ndANLTmP7mSjo=;\n\th=Date:From:To:Cc:Subject:References:In-Reply-To:From;\n\tb=IMolY4r+rx2Xv++CusqsCJM6WJDYSm0FFLfmfjryoZyhCGz6MQ/a8Tq0TypdE4M5B\n\tJn7EPRv7gYwddNyswKHqpLuHVxkYOgKcWQxI9fAC0jnv4gE/i5WJZmCVx9VhQSra2z\n\twqbQCGqqhufkWal7IqUsIWyL/sAmfP1GD7959yvU="],"Authentication-Results":"lancelot.ideasonboard.com; dkim=pass (1024-bit key; \n\tunprotected) header.d=ideasonboard.com\n\theader.i=@ideasonboard.com\n\theader.b=\"IMolY4r+\"; dkim-atps=neutral","Date":"Wed, 15 Feb 2023 12:28:18 +0200","To":"Pavel Machek <pavel@ucw.cz>","Message-ID":"<Y+yzwqK00l+sFOvk@pendragon.ideasonboard.com>","References":"<Y+ac7Eg8UsbX14j5@duo.ucw.cz>","MIME-Version":"1.0","Content-Type":"text/plain; charset=utf-8","Content-Disposition":"inline","In-Reply-To":"<Y+ac7Eg8UsbX14j5@duo.ucw.cz>","Subject":"Re: [libcamera-devel] [RFC] CPU-only auto-exposure,\n\tand where to put it","X-BeenThere":"libcamera-devel@lists.libcamera.org","X-Mailman-Version":"2.1.29","Precedence":"list","List-Id":"<libcamera-devel.lists.libcamera.org>","List-Unsubscribe":"<https://lists.libcamera.org/options/libcamera-devel>,\n\t<mailto:libcamera-devel-request@lists.libcamera.org?subject=unsubscribe>","List-Archive":"<https://lists.libcamera.org/pipermail/libcamera-devel/>","List-Post":"<mailto:libcamera-devel@lists.libcamera.org>","List-Help":"<mailto:libcamera-devel-request@lists.libcamera.org?subject=help>","List-Subscribe":"<https://lists.libcamera.org/listinfo/libcamera-devel>,\n\t<mailto:libcamera-devel-request@lists.libcamera.org?subject=subscribe>","From":"Laurent Pinchart via libcamera-devel\n\t<libcamera-devel@lists.libcamera.org>","Reply-To":"Laurent Pinchart <laurent.pinchart@ideasonboard.com>","Cc":"libcamera-devel@lists.libcamera.org","Errors-To":"libcamera-devel-bounces@lists.libcamera.org","Sender":"\"libcamera-devel\" <libcamera-devel-bounces@lists.libcamera.org>"}}]