···11+// Copyright 2022, Collabora, Ltd.
22+// SPDX-License-Identifier: BSL-1.0
33+/*!
44+ * @file
55+ * @brief Automatically compute exposure and gain values from an image stream
66+ * @author Mateo de Mayo <mateo.demayo@collabora.com>
77+ * @ingroup aux_util
88+ */
99+1010+#include "util/u_autoexpgain.h"
1111+#include "util/u_format.h"
1212+#include "util/u_misc.h"
1313+#include "util/u_var.h"
1414+1515+#include <assert.h>
1616+#include <math.h>
1717+#include <stdint.h>
1818+1919+#define MIN(a, b) ((a) < (b) ? (a) : (b))
2020+#define MAX(a, b) ((a) > (b) ? (a) : (b))
2121+#define CLAMP(X, A, B) (MIN(MAX((X), (A)), (B)))
2222+2323+#define LEVELS 256 //!< Possible pixel intensity values, only 8-bit supported
2424+#define INITIAL_BRIGHTNESS 0.5
2525+#define INITIAL_MAX_BRIGHTNES_STEP 0.05 //!< 0.1 is faster but introduces oscilations more often
2626+#define INITIAL_THRESHOLD 0.1
2727+#define GRID_COLS 40 //!< Amount of columns for the histogram sample grid
2828+2929+//! Auto exposure and gain (AEG) adjustment algorithm state.
3030+struct u_autoexpgain
3131+{
3232+ bool enable; //!< Whether to enable auto exposure and gain adjustment
3333+3434+ //! Algorithm strategy that affects how score and brightness are computed
3535+ enum u_aeg_strategy strategy;
3636+ struct u_var_combo strategy_combo; //!< UI combo box for selecting `strategy`
3737+3838+ float histogram[LEVELS]; //!< Pixel intensity histogram
3939+ struct u_var_histogram_f32 histogram_ui; //!< UI for `histogram`
4040+4141+ //! This is a made up scalar that lives in the [0, 1] range. 0 maps to minimum
4242+ //! exp/gain values while 1 to their maximums. An autoexposure strategy limits
4343+ //! itself to modify this value. The mapping between the scalar and the
4444+ //! respective exp/gain values is provided by `brightness_to_expgain`.
4545+ struct u_var_draggable_f32 brightness;
4646+ float last_brightness; //!< Triggers a exp/gain update when it differs
4747+ float max_brightness_step; //!< Max `brightness` step for each update
4848+4949+ //! The AEG score lives in the [-1, +1] range and represents how dark or
5050+ //! bright this image is. Values close to zero (by `threshold`) represent
5151+ //! images with a good enough `brightness` value.
5252+ float current_score;
5353+5454+ //! Scores further than `threshold` from zero will trigger a `brightness` update.
5555+ float threshold;
5656+5757+ uint32_t frame_counter; //!< Number of frames received
5858+5959+ //! Every how many frames should we update `brightness`. Some cameras take a
6060+ //! couple of frames until the new exposure/gain sets in and a new score can
6161+ //! be recomputed properly.
6262+ uint8_t update_every;
6363+6464+ float exposure; //!< Currently computed exposure value to use
6565+ float gain; //!< Currently computed gain value to use
6666+};
6767+6868+//! Maps a `brightness` in [0, 1] to a pair of exposure and gain values based on
6969+//! a piecewise function.
7070+static void
7171+brightness_to_expgain(struct u_autoexpgain *aeg, float brightness, float *out_exposure, float *out_gain)
7272+{
7373+7474+ //! These are steps for constructing a piecewise linear function that maps
7575+ //! brightness into (exposure, gain) pairs.
7676+ struct step
7777+ {
7878+ float b; //!< Brightness
7979+ float e; //!< Exposure
8080+ float g; //!< Gain
8181+ };
8282+8383+ // These tables were tuned over WMR cameras such that increasing
8484+ // brightness increases the histogram range more or less linearly.
8585+ struct step steps_t[] = {{0, 120, 16}, {0.15, 4500, 16}, {0.5, 4500, 127},
8686+ {0.55, 6000, 127}, {0.9, 6000, 255}, {1, 9000, 255}};
8787+ struct step steps_dr[] = {{0, 120, 16}, {0.3, 9000, 16}, {1.0, 9000, 255}};
8888+8989+ // Select the steps table to use based on our strategy/objective
9090+ struct step *steps = NULL;
9191+ int steps_count = 0;
9292+ if (aeg->strategy == U_AEG_STRATEGY_TRACKING) {
9393+ steps = steps_t;
9494+ steps_count = sizeof(steps_t) / sizeof(struct step);
9595+ } else if (aeg->strategy == U_AEG_STRATEGY_DYNAMIC_RANGE) {
9696+ steps = steps_dr;
9797+ steps_count = sizeof(steps_dr) / sizeof(struct step);
9898+ } else {
9999+ assert(false && "unexpected branch taken");
100100+ }
101101+102102+ // Other simpler tables that might work for WMR are:
103103+ // {{0, 120, 16}, {0.2, 6000, 16}, {1.0, 6000, 255}};
104104+ // {{0, 120, 16}, {0.2, 6000, 16}, {0.9, 6000, 255}, {1.0, 9000, 255}};
105105+106106+ // Assertions
107107+ assert(steps_count >= 2 && "Exoected at least two steps");
108108+ assert(steps[0].b == 0 && "First step should be at b=0");
109109+ assert(steps[steps_count - 1].b == 1 && "Last step should be at b=1");
110110+ assert(brightness >= 0 && brightness <= 1);
111111+112112+ // Compute the piecewise function result from `steps`
113113+ float exposure = 0;
114114+ float gain = 0;
115115+ for (int i = 1; i < steps_count; i++) {
116116+ struct step s0 = steps[i - 1];
117117+ struct step s1 = steps[i];
118118+119119+ float lower_b = s0.b;
120120+ float higher_b = s1.b;
121121+122122+ if (brightness >= lower_b && brightness <= higher_b) {
123123+ exposure = s0.e + ((brightness - lower_b) / (higher_b - lower_b)) * (s1.e - s0.e);
124124+ gain = s0.g + ((brightness - lower_b) / (higher_b - lower_b)) * (s1.g - s0.g);
125125+ break;
126126+ }
127127+ }
128128+ *out_exposure = exposure;
129129+ *out_gain = gain;
130130+}
131131+132132+//! Update `exposure` and `gain` based on current `brightness` value.
133133+static void
134134+update_expgain(struct u_autoexpgain *aeg)
135135+{
136136+ float brightness = aeg->brightness.val;
137137+ if (aeg->last_brightness == brightness) {
138138+ return;
139139+ }
140140+ aeg->last_brightness = brightness;
141141+142142+ float exposure = -1;
143143+ float gain = -1;
144144+ brightness_to_expgain(aeg, brightness, &exposure, &gain);
145145+ aeg->exposure = (uint16_t)exposure;
146146+ aeg->gain = (uint8_t)gain;
147147+}
148148+149149+//! Returns a value in the range [-1, 1] describing how dark-bright the image
150150+//! is, 0 means it's alright.
151151+static float
152152+get_score(struct u_autoexpgain *aeg, struct xrt_frame *xf)
153153+{
154154+ uint32_t w = xf->width;
155155+ uint32_t h = xf->height;
156156+ uint32_t s = w / GRID_COLS; // Grid cell size
157157+158158+ // Compute histogram (PDF)
159159+ int histogram[LEVELS] = {0};
160160+ int samples_count = 0;
161161+ size_t pixel_size = u_format_block_size(xf->format);
162162+ for (uint32_t y = 0; y < h; y += s) {
163163+ for (uint32_t x = 0; x < w; x += s) {
164164+ // Note that for multichannel images only the first channel is in use.
165165+ uint8_t intensity = xf->data[y * xf->stride + x * pixel_size];
166166+ histogram[intensity]++;
167167+ samples_count++;
168168+ }
169169+ }
170170+171171+ // Draw histogram
172172+ for (int i = 0; i < LEVELS; i++) {
173173+ aeg->histogram[i] = histogram[i];
174174+ }
175175+176176+ // Compute mean
177177+ float mean = 0;
178178+ for (int i = 0; i < LEVELS; i++) {
179179+ mean += (float)i * histogram[i];
180180+ }
181181+ mean /= samples_count;
182182+183183+ float score = 0;
184184+185185+ // Score that tries to make the mean reach TARGET_MEAN.
186186+187187+ float target_mean = -1;
188188+ if (aeg->strategy == U_AEG_STRATEGY_TRACKING) {
189189+ // We are not that much interested in using the full dynamic range for tracking
190190+ // so we prefer a darkish image because that reduces exposure and gain.
191191+ target_mean = LEVELS / 4;
192192+ } else if (aeg->strategy == U_AEG_STRATEGY_DYNAMIC_RANGE) {
193193+ target_mean = LEVELS / 2;
194194+ }
195195+196196+ const float range_size = mean < target_mean ? target_mean : (LEVELS - target_mean);
197197+ score = (mean - target_mean) / range_size;
198198+ score = CLAMP(score, -1, 1);
199199+200200+ return score;
201201+}
202202+203203+204204+static void
205205+update_brightness(struct u_autoexpgain *aeg, struct xrt_frame *xf)
206206+{
207207+ float score = get_score(aeg, xf);
208208+ aeg->current_score = score;
209209+210210+ if (!aeg->enable) {
211211+ return;
212212+ }
213213+214214+ aeg->frame_counter++;
215215+ if (aeg->frame_counter % aeg->update_every != 0) {
216216+ return;
217217+ }
218218+219219+ bool score_is_high = fabsf(score) > aeg->threshold;
220220+ if (!score_is_high) {
221221+ return;
222222+ }
223223+224224+ float max_step = aeg->max_brightness_step;
225225+ float step = CLAMP(max_step * score, -max_step, max_step);
226226+ aeg->brightness.val -= step;
227227+ aeg->brightness.val = CLAMP(aeg->brightness.val, 0, 1);
228228+}
229229+230230+/*
231231+ *
232232+ * Exported functions
233233+ *
234234+ */
235235+236236+struct u_autoexpgain *
237237+u_autoexpgain_create(enum u_aeg_strategy strategy, bool enabled_from_start, uint8_t update_every)
238238+{
239239+ struct u_autoexpgain *aeg = U_TYPED_CALLOC(struct u_autoexpgain);
240240+241241+ aeg->enable = enabled_from_start;
242242+ aeg->strategy = strategy;
243243+ aeg->strategy_combo.count = U_AEG_STRATEGY_COUNT;
244244+ aeg->strategy_combo.options = "Tracking\0Dynamic Range\0\0";
245245+ aeg->strategy_combo.value = (int *)&aeg->strategy;
246246+247247+ aeg->histogram_ui.values = aeg->histogram;
248248+ aeg->histogram_ui.count = LEVELS;
249249+250250+ aeg->brightness.max = 1;
251251+ aeg->brightness.min = 0;
252252+ aeg->brightness.step = 0.002;
253253+ aeg->brightness.val = INITIAL_BRIGHTNESS;
254254+ aeg->last_brightness = INITIAL_BRIGHTNESS;
255255+ aeg->max_brightness_step = INITIAL_MAX_BRIGHTNES_STEP;
256256+257257+ aeg->threshold = INITIAL_THRESHOLD;
258258+ aeg->frame_counter = 0;
259259+ aeg->update_every = update_every;
260260+261261+ brightness_to_expgain(aeg, INITIAL_BRIGHTNESS, &aeg->exposure, &aeg->gain);
262262+263263+ return aeg;
264264+}
265265+266266+void
267267+u_autoexpgain_add_vars(struct u_autoexpgain *aeg, void *root)
268268+{
269269+ u_var_add_bool(root, &aeg->enable, "Enable AEG");
270270+ u_var_add_u8(root, &aeg->update_every, "Update every X frames");
271271+ u_var_add_combo(root, &aeg->strategy_combo, "Strategy");
272272+ u_var_add_draggable_f32(root, &aeg->brightness, "Brightness");
273273+ u_var_add_f32(root, &aeg->threshold, "Score threshold");
274274+ u_var_add_f32(root, &aeg->max_brightness_step, "Max brightness step");
275275+ u_var_add_ro_f32(root, &aeg->current_score, "Image score");
276276+ u_var_add_histogram_f32(root, &aeg->histogram_ui, "Intensity histogram");
277277+}
278278+279279+void
280280+u_autoexpgain_update(struct u_autoexpgain *aeg, struct xrt_frame *xf)
281281+{
282282+ update_brightness(aeg, xf);
283283+ update_expgain(aeg);
284284+}
285285+286286+float
287287+u_autoexpgain_get_exposure(struct u_autoexpgain *aeg)
288288+{
289289+ return aeg->exposure;
290290+}
291291+292292+float
293293+u_autoexpgain_get_gain(struct u_autoexpgain *aeg)
294294+{
295295+ return aeg->gain;
296296+}
297297+298298+void
299299+u_autoexpgain_destroy(struct u_autoexpgain **aeg)
300300+{
301301+ free(*aeg);
302302+ *aeg = NULL;
303303+}
+62
src/xrt/auxiliary/util/u_autoexpgain.h
···11+// Copyright 2022, Collabora, Ltd.
22+// SPDX-License-Identifier: BSL-1.0
33+/*!
44+ * @file
55+ * @brief Automatically compute exposure and gain values from an image stream
66+ * @author Mateo de Mayo <mateo.demayo@collabora.com>
77+ * @ingroup aux_util
88+ */
99+1010+#pragma once
1111+1212+#include "xrt/xrt_frame.h"
1313+#include <stdint.h>
1414+1515+#ifdef __cplusplus
1616+extern "C" {
1717+#endif
1818+1919+//! An auto exposure/gain strategy tunes the algorithm for specific objectives.
2020+enum u_aeg_strategy
2121+{
2222+ U_AEG_STRATEGY_TRACKING = 0, //!< Lower exposure and gain at the cost of darker images.
2323+ U_AEG_STRATEGY_DYNAMIC_RANGE, //!< Tries to maximize the image information
2424+ U_AEG_STRATEGY_COUNT
2525+};
2626+2727+struct u_autoexpgain;
2828+2929+/*!
3030+ * Create auto exposure and gain (AEG) algorithm object.
3131+ *
3232+ * @param strategy What objective is preferred for the algorithm.
3333+ * @param enabled_from_start Update exposure/gain from the start.
3434+ * @param update_every Every how many frames should we update exposure/gain
3535+ * @return struct u_autoexpgain* Created object
3636+ */
3737+struct u_autoexpgain *
3838+u_autoexpgain_create(enum u_aeg_strategy strategy, bool enabled_from_start, uint8_t update_every);
3939+4040+//! Setup UI for the AEG algorithm
4141+void
4242+u_autoexpgain_add_vars(struct u_autoexpgain *aeg, void *root);
4343+4444+//! Update the AEG with a frame
4545+void
4646+u_autoexpgain_update(struct u_autoexpgain *aeg, struct xrt_frame *xf);
4747+4848+//! Get currently computed exposure value in usecs.
4949+float
5050+u_autoexpgain_get_exposure(struct u_autoexpgain *aeg);
5151+5252+//! Get currently computed gain value in the [0, 255] range.
5353+float
5454+u_autoexpgain_get_gain(struct u_autoexpgain *aeg);
5555+5656+//! Destroy AEG object
5757+void
5858+u_autoexpgain_destroy(struct u_autoexpgain **aeg);
5959+6060+#ifdef __cplusplus
6161+}
6262+#endif