xref: /aosp_15_r20/cts/apps/CameraITS/utils/noise_model_utils.py (revision b7c941bb3fa97aba169d73cee0bed2de8ac964bf)
1*b7c941bbSAndroid Build Coastguard Worker# Copyright 2014 The Android Open Source Project.
2*b7c941bbSAndroid Build Coastguard Worker
3*b7c941bbSAndroid Build Coastguard Worker# Licensed under the Apache License, Version 2.0 (the "License");
4*b7c941bbSAndroid Build Coastguard Worker# you may not use this file except in compliance with the License.
5*b7c941bbSAndroid Build Coastguard Worker# You may obtain a copy of the License at
6*b7c941bbSAndroid Build Coastguard Worker#
7*b7c941bbSAndroid Build Coastguard Worker#      http://www.apache.org/licenses/LICENSE-2.0
8*b7c941bbSAndroid Build Coastguard Worker#
9*b7c941bbSAndroid Build Coastguard Worker# Unless required by applicable law or agreed to in writing, software
10*b7c941bbSAndroid Build Coastguard Worker# distributed under the License is distributed on an "AS IS" BASIS,
11*b7c941bbSAndroid Build Coastguard Worker# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12*b7c941bbSAndroid Build Coastguard Worker# See the License for the specific language governing permissions and
13*b7c941bbSAndroid Build Coastguard Worker# limitations under the License.
14*b7c941bbSAndroid Build Coastguard Worker"""Noise model utility functions."""
15*b7c941bbSAndroid Build Coastguard Worker
16*b7c941bbSAndroid Build Coastguard Workerimport collections
17*b7c941bbSAndroid Build Coastguard Workerimport logging
18*b7c941bbSAndroid Build Coastguard Workerimport math
19*b7c941bbSAndroid Build Coastguard Workerimport os.path
20*b7c941bbSAndroid Build Coastguard Workerimport pickle
21*b7c941bbSAndroid Build Coastguard Workerfrom typing import Any, Dict, List, Tuple
22*b7c941bbSAndroid Build Coastguard Workerimport warnings
23*b7c941bbSAndroid Build Coastguard Workerimport capture_request_utils
24*b7c941bbSAndroid Build Coastguard Workerimport image_processing_utils
25*b7c941bbSAndroid Build Coastguard Workerfrom matplotlib import pyplot as plt
26*b7c941bbSAndroid Build Coastguard Workerimport noise_model_constants
27*b7c941bbSAndroid Build Coastguard Workerimport numpy as np
28*b7c941bbSAndroid Build Coastguard Workerimport scipy.stats
29*b7c941bbSAndroid Build Coastguard Worker
30*b7c941bbSAndroid Build Coastguard Worker
31*b7c941bbSAndroid Build Coastguard Worker_OUTLIER_MEDIAN_ABS_DEVS_DEFAULT = (
32*b7c941bbSAndroid Build Coastguard Worker    noise_model_constants.OUTLIER_MEDIAN_ABS_DEVS_DEFAULT
33*b7c941bbSAndroid Build Coastguard Worker)
34*b7c941bbSAndroid Build Coastguard Worker
35*b7c941bbSAndroid Build Coastguard Worker
36*b7c941bbSAndroid Build Coastguard Workerdef _check_auto_exposure_targets(
37*b7c941bbSAndroid Build Coastguard Worker    auto_exposure_ns: float,
38*b7c941bbSAndroid Build Coastguard Worker    sens_min: int,
39*b7c941bbSAndroid Build Coastguard Worker    sens_max: int,
40*b7c941bbSAndroid Build Coastguard Worker    bracket_factor: int,
41*b7c941bbSAndroid Build Coastguard Worker    min_exposure_ns: int,
42*b7c941bbSAndroid Build Coastguard Worker    max_exposure_ns: int,
43*b7c941bbSAndroid Build Coastguard Worker) -> None:
44*b7c941bbSAndroid Build Coastguard Worker  """Checks if AE too bright for highest gain & too dark for lowest gain.
45*b7c941bbSAndroid Build Coastguard Worker
46*b7c941bbSAndroid Build Coastguard Worker  Args:
47*b7c941bbSAndroid Build Coastguard Worker    auto_exposure_ns: The auto exposure value in nanoseconds.
48*b7c941bbSAndroid Build Coastguard Worker    sens_min: The minimum sensitivity value.
49*b7c941bbSAndroid Build Coastguard Worker    sens_max: The maximum sensitivity value.
50*b7c941bbSAndroid Build Coastguard Worker    bracket_factor: Exposure bracket factor.
51*b7c941bbSAndroid Build Coastguard Worker    min_exposure_ns: The minimum exposure time in nanoseconds.
52*b7c941bbSAndroid Build Coastguard Worker    max_exposure_ns: The maximum exposure time in nanoseconds.
53*b7c941bbSAndroid Build Coastguard Worker  """
54*b7c941bbSAndroid Build Coastguard Worker
55*b7c941bbSAndroid Build Coastguard Worker  if auto_exposure_ns < min_exposure_ns * sens_max:
56*b7c941bbSAndroid Build Coastguard Worker    raise AssertionError(
57*b7c941bbSAndroid Build Coastguard Worker        'Scene is too bright to properly expose at highest '
58*b7c941bbSAndroid Build Coastguard Worker        f'sensitivity: {sens_max}'
59*b7c941bbSAndroid Build Coastguard Worker    )
60*b7c941bbSAndroid Build Coastguard Worker  if auto_exposure_ns * bracket_factor > max_exposure_ns * sens_min:
61*b7c941bbSAndroid Build Coastguard Worker    raise AssertionError(
62*b7c941bbSAndroid Build Coastguard Worker        'Scene is too dark to properly expose at lowest '
63*b7c941bbSAndroid Build Coastguard Worker        f'sensitivity: {sens_min}'
64*b7c941bbSAndroid Build Coastguard Worker    )
65*b7c941bbSAndroid Build Coastguard Worker
66*b7c941bbSAndroid Build Coastguard Worker
67*b7c941bbSAndroid Build Coastguard Workerdef check_noise_model_shape(noise_model: np.ndarray) -> None:
68*b7c941bbSAndroid Build Coastguard Worker  """Checks if the shape of noise model is valid.
69*b7c941bbSAndroid Build Coastguard Worker
70*b7c941bbSAndroid Build Coastguard Worker  Args:
71*b7c941bbSAndroid Build Coastguard Worker    noise_model: A numpy array of shape (num_channels, num_parameters).
72*b7c941bbSAndroid Build Coastguard Worker  """
73*b7c941bbSAndroid Build Coastguard Worker  num_channels, num_parameters = noise_model.shape
74*b7c941bbSAndroid Build Coastguard Worker  if num_channels not in noise_model_constants.VALID_NUM_CHANNELS:
75*b7c941bbSAndroid Build Coastguard Worker    raise AssertionError(
76*b7c941bbSAndroid Build Coastguard Worker        f'The number of channels {num_channels} is not in'
77*b7c941bbSAndroid Build Coastguard Worker        f' {noise_model_constants.VALID_NUM_CHANNELS}.'
78*b7c941bbSAndroid Build Coastguard Worker    )
79*b7c941bbSAndroid Build Coastguard Worker  if num_parameters != 4:
80*b7c941bbSAndroid Build Coastguard Worker    raise AssertionError(
81*b7c941bbSAndroid Build Coastguard Worker        f'The number of parameters of each channel {num_parameters} != 4.'
82*b7c941bbSAndroid Build Coastguard Worker    )
83*b7c941bbSAndroid Build Coastguard Worker
84*b7c941bbSAndroid Build Coastguard Worker
85*b7c941bbSAndroid Build Coastguard Workerdef validate_noise_model(
86*b7c941bbSAndroid Build Coastguard Worker    noise_model: np.ndarray,
87*b7c941bbSAndroid Build Coastguard Worker    color_channels: List[str],
88*b7c941bbSAndroid Build Coastguard Worker    sens_min: int,
89*b7c941bbSAndroid Build Coastguard Worker) -> None:
90*b7c941bbSAndroid Build Coastguard Worker  """Performs validation checks on the noise model.
91*b7c941bbSAndroid Build Coastguard Worker
92*b7c941bbSAndroid Build Coastguard Worker  This function checks if read noise and intercept gradient are positive for
93*b7c941bbSAndroid Build Coastguard Worker  each color channel.
94*b7c941bbSAndroid Build Coastguard Worker
95*b7c941bbSAndroid Build Coastguard Worker  Args:
96*b7c941bbSAndroid Build Coastguard Worker      noise_model: Noise model parameters each channel, including scale_a,
97*b7c941bbSAndroid Build Coastguard Worker        scale_b, offset_a, offset_b.
98*b7c941bbSAndroid Build Coastguard Worker      color_channels: Array of color channels.
99*b7c941bbSAndroid Build Coastguard Worker      sens_min: Minimum sensitivity value.
100*b7c941bbSAndroid Build Coastguard Worker  """
101*b7c941bbSAndroid Build Coastguard Worker  check_noise_model_shape(noise_model)
102*b7c941bbSAndroid Build Coastguard Worker  num_channels = noise_model.shape[0]
103*b7c941bbSAndroid Build Coastguard Worker  if len(color_channels) != num_channels:
104*b7c941bbSAndroid Build Coastguard Worker    raise AssertionError(
105*b7c941bbSAndroid Build Coastguard Worker        f'Number of color channels {num_channels} != number of noise model '
106*b7c941bbSAndroid Build Coastguard Worker        f'channels {len(color_channels)}.'
107*b7c941bbSAndroid Build Coastguard Worker    )
108*b7c941bbSAndroid Build Coastguard Worker
109*b7c941bbSAndroid Build Coastguard Worker  scale_a, _, offset_a, offset_b = zip(*noise_model)
110*b7c941bbSAndroid Build Coastguard Worker  for i, color_channel in enumerate(color_channels):
111*b7c941bbSAndroid Build Coastguard Worker    if scale_a[i] < 0:
112*b7c941bbSAndroid Build Coastguard Worker      raise AssertionError(
113*b7c941bbSAndroid Build Coastguard Worker          f'{color_channel} model API scale gradient < 0: {scale_a[i]:.4e}'
114*b7c941bbSAndroid Build Coastguard Worker      )
115*b7c941bbSAndroid Build Coastguard Worker
116*b7c941bbSAndroid Build Coastguard Worker    if offset_a[i] <= 0:
117*b7c941bbSAndroid Build Coastguard Worker      raise AssertionError(
118*b7c941bbSAndroid Build Coastguard Worker          f'{color_channel} model API intercept gradient < 0: {offset_a[i]:.4e}'
119*b7c941bbSAndroid Build Coastguard Worker      )
120*b7c941bbSAndroid Build Coastguard Worker
121*b7c941bbSAndroid Build Coastguard Worker    read_noise = offset_a[i] * sens_min * sens_min + offset_b[i]
122*b7c941bbSAndroid Build Coastguard Worker    if read_noise <= 0:
123*b7c941bbSAndroid Build Coastguard Worker      raise AssertionError(
124*b7c941bbSAndroid Build Coastguard Worker          f'{color_channel} model min ISO noise < 0! '
125*b7c941bbSAndroid Build Coastguard Worker          f'API intercept gradient: {offset_a[i]:.4e}, '
126*b7c941bbSAndroid Build Coastguard Worker          f'API intercept offset: {offset_b[i]:.4e}, '
127*b7c941bbSAndroid Build Coastguard Worker          f'read_noise: {read_noise:.4e}'
128*b7c941bbSAndroid Build Coastguard Worker      )
129*b7c941bbSAndroid Build Coastguard Worker
130*b7c941bbSAndroid Build Coastguard Worker
131*b7c941bbSAndroid Build Coastguard Workerdef compute_digital_gains(
132*b7c941bbSAndroid Build Coastguard Worker    gains: np.ndarray,
133*b7c941bbSAndroid Build Coastguard Worker    sens_max_analog: np.ndarray,
134*b7c941bbSAndroid Build Coastguard Worker) -> np.ndarray:
135*b7c941bbSAndroid Build Coastguard Worker  """Computes the digital gains for the given gains and maximum analog gain.
136*b7c941bbSAndroid Build Coastguard Worker
137*b7c941bbSAndroid Build Coastguard Worker  Define digital gain as the gain divide the max analog gain sensitivity.
138*b7c941bbSAndroid Build Coastguard Worker  This function ensures that the digital gains are always equal to 1. If any
139*b7c941bbSAndroid Build Coastguard Worker  of the digital gains is not equal to 1, an AssertionError is raised.
140*b7c941bbSAndroid Build Coastguard Worker
141*b7c941bbSAndroid Build Coastguard Worker  Args:
142*b7c941bbSAndroid Build Coastguard Worker    gains: An array of gains.
143*b7c941bbSAndroid Build Coastguard Worker    sens_max_analog: The maximum analog gain sensitivity.
144*b7c941bbSAndroid Build Coastguard Worker
145*b7c941bbSAndroid Build Coastguard Worker  Returns:
146*b7c941bbSAndroid Build Coastguard Worker    An numpy array of digital gains.
147*b7c941bbSAndroid Build Coastguard Worker  """
148*b7c941bbSAndroid Build Coastguard Worker  digital_gains = np.maximum(gains / sens_max_analog, 1)
149*b7c941bbSAndroid Build Coastguard Worker  if not np.all(digital_gains == 1):
150*b7c941bbSAndroid Build Coastguard Worker    raise AssertionError(
151*b7c941bbSAndroid Build Coastguard Worker        f'Digital gains are not all 1! gains: {gains}, '
152*b7c941bbSAndroid Build Coastguard Worker        f'Max analog gain sensitivity: {sens_max_analog}.'
153*b7c941bbSAndroid Build Coastguard Worker    )
154*b7c941bbSAndroid Build Coastguard Worker  return digital_gains
155*b7c941bbSAndroid Build Coastguard Worker
156*b7c941bbSAndroid Build Coastguard Worker
157*b7c941bbSAndroid Build Coastguard Workerdef crop_and_save_capture(
158*b7c941bbSAndroid Build Coastguard Worker    cap,
159*b7c941bbSAndroid Build Coastguard Worker    props,
160*b7c941bbSAndroid Build Coastguard Worker    capture_path: str,
161*b7c941bbSAndroid Build Coastguard Worker    num_tiles_crop: int,
162*b7c941bbSAndroid Build Coastguard Worker) -> None:
163*b7c941bbSAndroid Build Coastguard Worker  """Crops and saves a capture image.
164*b7c941bbSAndroid Build Coastguard Worker
165*b7c941bbSAndroid Build Coastguard Worker  Args:
166*b7c941bbSAndroid Build Coastguard Worker    cap: The capture to be cropped and saved.
167*b7c941bbSAndroid Build Coastguard Worker    props: The properties to be used to convert the capture to an RGB image.
168*b7c941bbSAndroid Build Coastguard Worker    capture_path: The path to which the capture image should be saved.
169*b7c941bbSAndroid Build Coastguard Worker    num_tiles_crop: The number of tiles to crop.
170*b7c941bbSAndroid Build Coastguard Worker  """
171*b7c941bbSAndroid Build Coastguard Worker  img = image_processing_utils.convert_capture_to_rgb_image(cap, props=props)
172*b7c941bbSAndroid Build Coastguard Worker  height, width, _ = img.shape
173*b7c941bbSAndroid Build Coastguard Worker  num_tiles_crop_max = min(height, width) // 2
174*b7c941bbSAndroid Build Coastguard Worker  if num_tiles_crop >= num_tiles_crop_max:
175*b7c941bbSAndroid Build Coastguard Worker    raise AssertionError(
176*b7c941bbSAndroid Build Coastguard Worker        f'Number of tiles to corp {num_tiles_crop} >= {num_tiles_crop_max}.'
177*b7c941bbSAndroid Build Coastguard Worker    )
178*b7c941bbSAndroid Build Coastguard Worker  img = img[
179*b7c941bbSAndroid Build Coastguard Worker      num_tiles_crop: height - num_tiles_crop,
180*b7c941bbSAndroid Build Coastguard Worker      num_tiles_crop: width - num_tiles_crop,
181*b7c941bbSAndroid Build Coastguard Worker      :,
182*b7c941bbSAndroid Build Coastguard Worker  ]
183*b7c941bbSAndroid Build Coastguard Worker
184*b7c941bbSAndroid Build Coastguard Worker  image_processing_utils.write_image(img, capture_path, True)
185*b7c941bbSAndroid Build Coastguard Worker
186*b7c941bbSAndroid Build Coastguard Worker
187*b7c941bbSAndroid Build Coastguard Workerdef crop_and_reorder_stats_images(
188*b7c941bbSAndroid Build Coastguard Worker    mean_img: np.ndarray,
189*b7c941bbSAndroid Build Coastguard Worker    var_img: np.ndarray,
190*b7c941bbSAndroid Build Coastguard Worker    num_tiles_crop: int,
191*b7c941bbSAndroid Build Coastguard Worker    channel_indices: List[int],
192*b7c941bbSAndroid Build Coastguard Worker) -> Tuple[np.ndarray, np.ndarray]:
193*b7c941bbSAndroid Build Coastguard Worker  """Crops the stats images and sorts stats images channels in canonical order.
194*b7c941bbSAndroid Build Coastguard Worker
195*b7c941bbSAndroid Build Coastguard Worker  Args:
196*b7c941bbSAndroid Build Coastguard Worker      mean_img: The mean image.
197*b7c941bbSAndroid Build Coastguard Worker      var_img: The variance image.
198*b7c941bbSAndroid Build Coastguard Worker      num_tiles_crop: The number of tiles to crop from each side of the image.
199*b7c941bbSAndroid Build Coastguard Worker      channel_indices: The channel indices to sort stats image channels in
200*b7c941bbSAndroid Build Coastguard Worker        canonical order.
201*b7c941bbSAndroid Build Coastguard Worker
202*b7c941bbSAndroid Build Coastguard Worker  Returns:
203*b7c941bbSAndroid Build Coastguard Worker      The cropped and reordered mean image and variance image.
204*b7c941bbSAndroid Build Coastguard Worker  """
205*b7c941bbSAndroid Build Coastguard Worker  if mean_img.shape != var_img.shape:
206*b7c941bbSAndroid Build Coastguard Worker    raise AssertionError(
207*b7c941bbSAndroid Build Coastguard Worker        'Unmatched shapes of mean and variance image: '
208*b7c941bbSAndroid Build Coastguard Worker        f'shape of mean image is {mean_img.shape}, '
209*b7c941bbSAndroid Build Coastguard Worker        f'shape of variance image is {var_img.shape}.'
210*b7c941bbSAndroid Build Coastguard Worker    )
211*b7c941bbSAndroid Build Coastguard Worker  height, width, _ = mean_img.shape
212*b7c941bbSAndroid Build Coastguard Worker  if 2 * num_tiles_crop > min(height, width):
213*b7c941bbSAndroid Build Coastguard Worker    raise AssertionError(
214*b7c941bbSAndroid Build Coastguard Worker        f'The number of tiles to crop ({num_tiles_crop}) is so large that'
215*b7c941bbSAndroid Build Coastguard Worker        ' images cannot be cropped.'
216*b7c941bbSAndroid Build Coastguard Worker    )
217*b7c941bbSAndroid Build Coastguard Worker
218*b7c941bbSAndroid Build Coastguard Worker  means = []
219*b7c941bbSAndroid Build Coastguard Worker  vars_ = []
220*b7c941bbSAndroid Build Coastguard Worker  for i in channel_indices:
221*b7c941bbSAndroid Build Coastguard Worker    means_i = mean_img[
222*b7c941bbSAndroid Build Coastguard Worker        num_tiles_crop: height - num_tiles_crop,
223*b7c941bbSAndroid Build Coastguard Worker        num_tiles_crop: width - num_tiles_crop,
224*b7c941bbSAndroid Build Coastguard Worker        i,
225*b7c941bbSAndroid Build Coastguard Worker    ]
226*b7c941bbSAndroid Build Coastguard Worker    vars_i = var_img[
227*b7c941bbSAndroid Build Coastguard Worker        num_tiles_crop: height - num_tiles_crop,
228*b7c941bbSAndroid Build Coastguard Worker        num_tiles_crop: width - num_tiles_crop,
229*b7c941bbSAndroid Build Coastguard Worker        i,
230*b7c941bbSAndroid Build Coastguard Worker    ]
231*b7c941bbSAndroid Build Coastguard Worker    means.append(means_i)
232*b7c941bbSAndroid Build Coastguard Worker    vars_.append(vars_i)
233*b7c941bbSAndroid Build Coastguard Worker  means, vars_ = np.asarray(means), np.asarray(vars_)
234*b7c941bbSAndroid Build Coastguard Worker  return means, vars_
235*b7c941bbSAndroid Build Coastguard Worker
236*b7c941bbSAndroid Build Coastguard Worker
237*b7c941bbSAndroid Build Coastguard Workerdef filter_stats(
238*b7c941bbSAndroid Build Coastguard Worker    means: np.ndarray,
239*b7c941bbSAndroid Build Coastguard Worker    vars_: np.ndarray,
240*b7c941bbSAndroid Build Coastguard Worker    black_levels: List[float],
241*b7c941bbSAndroid Build Coastguard Worker    white_level: float,
242*b7c941bbSAndroid Build Coastguard Worker    max_signal_value: float = 0.25,
243*b7c941bbSAndroid Build Coastguard Worker    is_remove_var_outliers: bool = False,
244*b7c941bbSAndroid Build Coastguard Worker    deviations: int = _OUTLIER_MEDIAN_ABS_DEVS_DEFAULT,
245*b7c941bbSAndroid Build Coastguard Worker) -> Tuple[np.ndarray, np.ndarray]:
246*b7c941bbSAndroid Build Coastguard Worker  """Filters means outliers and variance outliers.
247*b7c941bbSAndroid Build Coastguard Worker
248*b7c941bbSAndroid Build Coastguard Worker  Args:
249*b7c941bbSAndroid Build Coastguard Worker      means: A numpy ndarray of pixel mean values.
250*b7c941bbSAndroid Build Coastguard Worker      vars_: A numpy ndarray of pixel variance values.
251*b7c941bbSAndroid Build Coastguard Worker      black_levels: A list of black levels for each pixel.
252*b7c941bbSAndroid Build Coastguard Worker      white_level: A scalar white level.
253*b7c941bbSAndroid Build Coastguard Worker      max_signal_value: The maximum signal (mean) value.
254*b7c941bbSAndroid Build Coastguard Worker      is_remove_var_outliers: A boolean value indicating whether to remove
255*b7c941bbSAndroid Build Coastguard Worker        variance outliers.
256*b7c941bbSAndroid Build Coastguard Worker      deviations: A scalar value specifying the number of standard deviations to
257*b7c941bbSAndroid Build Coastguard Worker        use when removing variance outliers.
258*b7c941bbSAndroid Build Coastguard Worker
259*b7c941bbSAndroid Build Coastguard Worker  Returns:
260*b7c941bbSAndroid Build Coastguard Worker      A tuple of (means_filtered, vars_filtered) where means_filtered and
261*b7c941bbSAndroid Build Coastguard Worker      vars_filtered are numpy ndarrays of filtered pixel mean and variance
262*b7c941bbSAndroid Build Coastguard Worker      values, respectively.
263*b7c941bbSAndroid Build Coastguard Worker  """
264*b7c941bbSAndroid Build Coastguard Worker  if means.shape != vars_.shape:
265*b7c941bbSAndroid Build Coastguard Worker    raise AssertionError(
266*b7c941bbSAndroid Build Coastguard Worker        f'Unmatched shapes of means and vars: means.shape={means.shape},'
267*b7c941bbSAndroid Build Coastguard Worker        f' vars.shape={vars_.shape}.'
268*b7c941bbSAndroid Build Coastguard Worker    )
269*b7c941bbSAndroid Build Coastguard Worker  num_planes = len(means)
270*b7c941bbSAndroid Build Coastguard Worker  means_filtered = []
271*b7c941bbSAndroid Build Coastguard Worker  vars_filtered = []
272*b7c941bbSAndroid Build Coastguard Worker
273*b7c941bbSAndroid Build Coastguard Worker  for pidx in range(num_planes):
274*b7c941bbSAndroid Build Coastguard Worker    black_level = black_levels[pidx]
275*b7c941bbSAndroid Build Coastguard Worker    means_i = means[pidx]
276*b7c941bbSAndroid Build Coastguard Worker    vars_i = vars_[pidx]
277*b7c941bbSAndroid Build Coastguard Worker
278*b7c941bbSAndroid Build Coastguard Worker    # Basic constraints:
279*b7c941bbSAndroid Build Coastguard Worker    # (1) means are within the range [0, 1],
280*b7c941bbSAndroid Build Coastguard Worker    # (2) vars are non-negative values.
281*b7c941bbSAndroid Build Coastguard Worker    constraints = [
282*b7c941bbSAndroid Build Coastguard Worker        means_i >= black_level,
283*b7c941bbSAndroid Build Coastguard Worker        means_i <= white_level,
284*b7c941bbSAndroid Build Coastguard Worker        vars_i >= 0,
285*b7c941bbSAndroid Build Coastguard Worker    ]
286*b7c941bbSAndroid Build Coastguard Worker    if is_remove_var_outliers:
287*b7c941bbSAndroid Build Coastguard Worker      # Filter out variances that differ too much from the median of variances.
288*b7c941bbSAndroid Build Coastguard Worker      std_dev = scipy.stats.median_abs_deviation(vars_i, axis=None, scale=1)
289*b7c941bbSAndroid Build Coastguard Worker      med = np.median(vars_i)
290*b7c941bbSAndroid Build Coastguard Worker      constraints.extend([
291*b7c941bbSAndroid Build Coastguard Worker          vars_i > med - deviations * std_dev,
292*b7c941bbSAndroid Build Coastguard Worker          vars_i < med + deviations * std_dev,
293*b7c941bbSAndroid Build Coastguard Worker      ])
294*b7c941bbSAndroid Build Coastguard Worker
295*b7c941bbSAndroid Build Coastguard Worker    keep_indices = np.where(np.logical_and.reduce(constraints))
296*b7c941bbSAndroid Build Coastguard Worker    if not np.any(keep_indices):
297*b7c941bbSAndroid Build Coastguard Worker      logging.info('After filter channel %d, stats array is empty.', pidx)
298*b7c941bbSAndroid Build Coastguard Worker
299*b7c941bbSAndroid Build Coastguard Worker    # Normalizes the range to [0, 1].
300*b7c941bbSAndroid Build Coastguard Worker    means_i = (means_i[keep_indices] - black_level) / (
301*b7c941bbSAndroid Build Coastguard Worker        white_level - black_level
302*b7c941bbSAndroid Build Coastguard Worker    )
303*b7c941bbSAndroid Build Coastguard Worker    vars_i = vars_i[keep_indices] / ((white_level - black_level) ** 2)
304*b7c941bbSAndroid Build Coastguard Worker    # Filter out the tiles if they have samples that might be clipped.
305*b7c941bbSAndroid Build Coastguard Worker    mean_var_pairs = list(
306*b7c941bbSAndroid Build Coastguard Worker        filter(
307*b7c941bbSAndroid Build Coastguard Worker            lambda x: x[0] + 2 * math.sqrt(x[1]) < max_signal_value,
308*b7c941bbSAndroid Build Coastguard Worker            zip(means_i, vars_i),
309*b7c941bbSAndroid Build Coastguard Worker        )
310*b7c941bbSAndroid Build Coastguard Worker    )
311*b7c941bbSAndroid Build Coastguard Worker    if mean_var_pairs:
312*b7c941bbSAndroid Build Coastguard Worker      means_i, vars_i = zip(*mean_var_pairs)
313*b7c941bbSAndroid Build Coastguard Worker    else:
314*b7c941bbSAndroid Build Coastguard Worker      means_i, vars_i = [], []
315*b7c941bbSAndroid Build Coastguard Worker    means_i = np.asarray(means_i)
316*b7c941bbSAndroid Build Coastguard Worker    vars_i = np.asarray(vars_i)
317*b7c941bbSAndroid Build Coastguard Worker    means_filtered.append(means_i)
318*b7c941bbSAndroid Build Coastguard Worker    vars_filtered.append(vars_i)
319*b7c941bbSAndroid Build Coastguard Worker
320*b7c941bbSAndroid Build Coastguard Worker  # After filtering, means_filtered and vars_filtered may have different shapes
321*b7c941bbSAndroid Build Coastguard Worker  # in each color planes.
322*b7c941bbSAndroid Build Coastguard Worker  means_filtered = np.asarray(means_filtered, dtype=object)
323*b7c941bbSAndroid Build Coastguard Worker  vars_filtered = np.asarray(vars_filtered, dtype=object)
324*b7c941bbSAndroid Build Coastguard Worker  return means_filtered, vars_filtered
325*b7c941bbSAndroid Build Coastguard Worker
326*b7c941bbSAndroid Build Coastguard Worker
327*b7c941bbSAndroid Build Coastguard Workerdef get_next_iso(
328*b7c941bbSAndroid Build Coastguard Worker    iso: float,
329*b7c941bbSAndroid Build Coastguard Worker    max_iso: int,
330*b7c941bbSAndroid Build Coastguard Worker    iso_multiplier: float,
331*b7c941bbSAndroid Build Coastguard Worker) -> float:
332*b7c941bbSAndroid Build Coastguard Worker  """Moves to the next sensitivity.
333*b7c941bbSAndroid Build Coastguard Worker
334*b7c941bbSAndroid Build Coastguard Worker  Args:
335*b7c941bbSAndroid Build Coastguard Worker    iso: The current ISO sensitivity.
336*b7c941bbSAndroid Build Coastguard Worker    max_iso: The maximum ISO sensitivity.
337*b7c941bbSAndroid Build Coastguard Worker    iso_multiplier: The ISO multiplier to use.
338*b7c941bbSAndroid Build Coastguard Worker
339*b7c941bbSAndroid Build Coastguard Worker  Returns:
340*b7c941bbSAndroid Build Coastguard Worker    The next ISO sensitivity.
341*b7c941bbSAndroid Build Coastguard Worker  """
342*b7c941bbSAndroid Build Coastguard Worker  if iso_multiplier <= 1:
343*b7c941bbSAndroid Build Coastguard Worker    raise AssertionError(
344*b7c941bbSAndroid Build Coastguard Worker        f'ISO multiplier is {iso_multiplier}, which should be greater than 1.'
345*b7c941bbSAndroid Build Coastguard Worker    )
346*b7c941bbSAndroid Build Coastguard Worker
347*b7c941bbSAndroid Build Coastguard Worker  if round(iso) < max_iso < round(iso * iso_multiplier):
348*b7c941bbSAndroid Build Coastguard Worker    return max_iso
349*b7c941bbSAndroid Build Coastguard Worker  else:
350*b7c941bbSAndroid Build Coastguard Worker    return iso * iso_multiplier
351*b7c941bbSAndroid Build Coastguard Worker
352*b7c941bbSAndroid Build Coastguard Worker
353*b7c941bbSAndroid Build Coastguard Workerdef capture_stats_images(
354*b7c941bbSAndroid Build Coastguard Worker    cam,
355*b7c941bbSAndroid Build Coastguard Worker    props,
356*b7c941bbSAndroid Build Coastguard Worker    stats_config: Dict[str, Any],
357*b7c941bbSAndroid Build Coastguard Worker    sens_min: int,
358*b7c941bbSAndroid Build Coastguard Worker    sens_max_meas: int,
359*b7c941bbSAndroid Build Coastguard Worker    zoom_ratio: float,
360*b7c941bbSAndroid Build Coastguard Worker    num_tiles_crop: int,
361*b7c941bbSAndroid Build Coastguard Worker    max_signal_value: float,
362*b7c941bbSAndroid Build Coastguard Worker    iso_multiplier: float,
363*b7c941bbSAndroid Build Coastguard Worker    max_bracket: int,
364*b7c941bbSAndroid Build Coastguard Worker    bracket_factor: int,
365*b7c941bbSAndroid Build Coastguard Worker    capture_path_prefix: str,
366*b7c941bbSAndroid Build Coastguard Worker    stats_file_name: str = '',
367*b7c941bbSAndroid Build Coastguard Worker    is_remove_var_outliers: bool = False,
368*b7c941bbSAndroid Build Coastguard Worker    outlier_median_abs_deviations: int = _OUTLIER_MEDIAN_ABS_DEVS_DEFAULT,
369*b7c941bbSAndroid Build Coastguard Worker    is_debug_mode: bool = False,
370*b7c941bbSAndroid Build Coastguard Worker) -> Dict[int, List[Tuple[float, np.ndarray, np.ndarray]]]:
371*b7c941bbSAndroid Build Coastguard Worker  """Capture stats images and saves the stats in a dictionary.
372*b7c941bbSAndroid Build Coastguard Worker
373*b7c941bbSAndroid Build Coastguard Worker  This function captures stats images at different ISO values and exposure
374*b7c941bbSAndroid Build Coastguard Worker  times, and stores the stats data in a file with the specified name.
375*b7c941bbSAndroid Build Coastguard Worker  The stats data includes the mean and variance of each plane, as well as
376*b7c941bbSAndroid Build Coastguard Worker  exposure times.
377*b7c941bbSAndroid Build Coastguard Worker
378*b7c941bbSAndroid Build Coastguard Worker  Args:
379*b7c941bbSAndroid Build Coastguard Worker    cam: The camera session (its_session_utils.ItsSession) for capturing stats
380*b7c941bbSAndroid Build Coastguard Worker      images.
381*b7c941bbSAndroid Build Coastguard Worker    props: Camera property object.
382*b7c941bbSAndroid Build Coastguard Worker    stats_config: The stats format config, a dictionary that specifies the raw
383*b7c941bbSAndroid Build Coastguard Worker      stats image format and tile size.
384*b7c941bbSAndroid Build Coastguard Worker    sens_min: The minimum sensitivity.
385*b7c941bbSAndroid Build Coastguard Worker    sens_max_meas: The maximum sensitivity to measure.
386*b7c941bbSAndroid Build Coastguard Worker    zoom_ratio: The zoom ratio to use.
387*b7c941bbSAndroid Build Coastguard Worker    num_tiles_crop: The number of tiles to crop the images into.
388*b7c941bbSAndroid Build Coastguard Worker    max_signal_value: The maximum signal value to allow.
389*b7c941bbSAndroid Build Coastguard Worker    iso_multiplier: The ISO multiplier to use.
390*b7c941bbSAndroid Build Coastguard Worker    max_bracket: The maximum number of bracketed exposures to capture.
391*b7c941bbSAndroid Build Coastguard Worker    bracket_factor: The bracket factor with default value 2^max_bracket.
392*b7c941bbSAndroid Build Coastguard Worker    capture_path_prefix: The path prefix to use for captured images.
393*b7c941bbSAndroid Build Coastguard Worker    stats_file_name: The name of the file to save the stats images to.
394*b7c941bbSAndroid Build Coastguard Worker    is_remove_var_outliers: Whether to remove variance outliers.
395*b7c941bbSAndroid Build Coastguard Worker    outlier_median_abs_deviations: The number of median absolute deviations to
396*b7c941bbSAndroid Build Coastguard Worker      use for detecting outliers.
397*b7c941bbSAndroid Build Coastguard Worker    is_debug_mode: Whether to enable debug mode.
398*b7c941bbSAndroid Build Coastguard Worker
399*b7c941bbSAndroid Build Coastguard Worker  Returns:
400*b7c941bbSAndroid Build Coastguard Worker    A dictionary mapping ISO values to mean and variance image of each plane.
401*b7c941bbSAndroid Build Coastguard Worker  """
402*b7c941bbSAndroid Build Coastguard Worker  if is_debug_mode:
403*b7c941bbSAndroid Build Coastguard Worker    logging.info('Capturing stats images with stats config: %s.', stats_config)
404*b7c941bbSAndroid Build Coastguard Worker    capture_folder = os.path.join(capture_path_prefix, 'captures')
405*b7c941bbSAndroid Build Coastguard Worker    if not os.path.exists(capture_folder):
406*b7c941bbSAndroid Build Coastguard Worker      os.makedirs(capture_folder)
407*b7c941bbSAndroid Build Coastguard Worker    logging.info('Capture folder: %s', capture_folder)
408*b7c941bbSAndroid Build Coastguard Worker
409*b7c941bbSAndroid Build Coastguard Worker  white_level = props['android.sensor.info.whiteLevel']
410*b7c941bbSAndroid Build Coastguard Worker  min_exposure_ns, max_exposure_ns = props[
411*b7c941bbSAndroid Build Coastguard Worker      'android.sensor.info.exposureTimeRange'
412*b7c941bbSAndroid Build Coastguard Worker  ]
413*b7c941bbSAndroid Build Coastguard Worker  # Focus at zero to intentionally blur the scene as much as possible.
414*b7c941bbSAndroid Build Coastguard Worker  f_dist = 0.0
415*b7c941bbSAndroid Build Coastguard Worker  # Whether the stats images are quad Bayer or standard Bayer.
416*b7c941bbSAndroid Build Coastguard Worker  is_quad_bayer = 'QuadBayer' in stats_config['format']
417*b7c941bbSAndroid Build Coastguard Worker  if is_quad_bayer:
418*b7c941bbSAndroid Build Coastguard Worker    num_channels = noise_model_constants.NUM_QUAD_BAYER_CHANNELS
419*b7c941bbSAndroid Build Coastguard Worker  else:
420*b7c941bbSAndroid Build Coastguard Worker    num_channels = noise_model_constants.NUM_BAYER_CHANNELS
421*b7c941bbSAndroid Build Coastguard Worker  # A dict maps iso to stats images of different exposure times.
422*b7c941bbSAndroid Build Coastguard Worker  iso_to_stats_dict = collections.defaultdict(list)
423*b7c941bbSAndroid Build Coastguard Worker  # Start the sensitivity at the minimum.
424*b7c941bbSAndroid Build Coastguard Worker  iso = sens_min
425*b7c941bbSAndroid Build Coastguard Worker  # Previous iso cap.
426*b7c941bbSAndroid Build Coastguard Worker  pre_iso_cap = None
427*b7c941bbSAndroid Build Coastguard Worker  if stats_file_name:
428*b7c941bbSAndroid Build Coastguard Worker    stats_file_path = os.path.join(capture_path_prefix, stats_file_name)
429*b7c941bbSAndroid Build Coastguard Worker    if os.path.isfile(stats_file_path):
430*b7c941bbSAndroid Build Coastguard Worker      try:
431*b7c941bbSAndroid Build Coastguard Worker        with open(stats_file_path, 'rb') as f:
432*b7c941bbSAndroid Build Coastguard Worker          saved_iso_to_stats_dict = pickle.load(f)
433*b7c941bbSAndroid Build Coastguard Worker          # Filter saved stats data.
434*b7c941bbSAndroid Build Coastguard Worker          if saved_iso_to_stats_dict:
435*b7c941bbSAndroid Build Coastguard Worker            for iso, stats in saved_iso_to_stats_dict.items():
436*b7c941bbSAndroid Build Coastguard Worker              if sens_min <= iso <= sens_max_meas:
437*b7c941bbSAndroid Build Coastguard Worker                iso_to_stats_dict[iso] = stats
438*b7c941bbSAndroid Build Coastguard Worker
439*b7c941bbSAndroid Build Coastguard Worker        # Set the starting iso to the last iso in saved stats file.
440*b7c941bbSAndroid Build Coastguard Worker        if iso_to_stats_dict.keys():
441*b7c941bbSAndroid Build Coastguard Worker          pre_iso_cap = max(iso_to_stats_dict.keys())
442*b7c941bbSAndroid Build Coastguard Worker          iso = get_next_iso(pre_iso_cap, sens_max_meas, iso_multiplier)
443*b7c941bbSAndroid Build Coastguard Worker      except OSError as e:
444*b7c941bbSAndroid Build Coastguard Worker        logging.exception(
445*b7c941bbSAndroid Build Coastguard Worker            'Failed to load stats file stored at %s. Error message: %s',
446*b7c941bbSAndroid Build Coastguard Worker            stats_file_path,
447*b7c941bbSAndroid Build Coastguard Worker            e,
448*b7c941bbSAndroid Build Coastguard Worker        )
449*b7c941bbSAndroid Build Coastguard Worker
450*b7c941bbSAndroid Build Coastguard Worker  if round(iso) <= sens_max_meas:
451*b7c941bbSAndroid Build Coastguard Worker    # Wait until camera is repositioned for noise model calibration.
452*b7c941bbSAndroid Build Coastguard Worker    input(
453*b7c941bbSAndroid Build Coastguard Worker        f'\nPress <ENTER> after covering camera lense {cam.get_camera_name()} '
454*b7c941bbSAndroid Build Coastguard Worker        'with frosted glass diffuser, and facing lense at evenly illuminated'
455*b7c941bbSAndroid Build Coastguard Worker        ' surface.\n'
456*b7c941bbSAndroid Build Coastguard Worker    )
457*b7c941bbSAndroid Build Coastguard Worker    # Do AE to get a rough idea of where we are.
458*b7c941bbSAndroid Build Coastguard Worker    iso_ae, exp_ae, _, _, _ = cam.do_3a(
459*b7c941bbSAndroid Build Coastguard Worker        get_results=True, do_awb=False, do_af=False
460*b7c941bbSAndroid Build Coastguard Worker    )
461*b7c941bbSAndroid Build Coastguard Worker
462*b7c941bbSAndroid Build Coastguard Worker    # Underexpose to get more data for low signal levels.
463*b7c941bbSAndroid Build Coastguard Worker    auto_exposure_ns = iso_ae * exp_ae / bracket_factor
464*b7c941bbSAndroid Build Coastguard Worker    _check_auto_exposure_targets(
465*b7c941bbSAndroid Build Coastguard Worker        auto_exposure_ns,
466*b7c941bbSAndroid Build Coastguard Worker        sens_min,
467*b7c941bbSAndroid Build Coastguard Worker        sens_max_meas,
468*b7c941bbSAndroid Build Coastguard Worker        bracket_factor,
469*b7c941bbSAndroid Build Coastguard Worker        min_exposure_ns,
470*b7c941bbSAndroid Build Coastguard Worker        max_exposure_ns,
471*b7c941bbSAndroid Build Coastguard Worker    )
472*b7c941bbSAndroid Build Coastguard Worker
473*b7c941bbSAndroid Build Coastguard Worker  while round(iso) <= sens_max_meas:
474*b7c941bbSAndroid Build Coastguard Worker    req = capture_request_utils.manual_capture_request(
475*b7c941bbSAndroid Build Coastguard Worker        round(iso), min_exposure_ns, f_dist
476*b7c941bbSAndroid Build Coastguard Worker    )
477*b7c941bbSAndroid Build Coastguard Worker    cap = cam.do_capture(req, stats_config)
478*b7c941bbSAndroid Build Coastguard Worker    # Instead of raising an error when the sensitivity readback != requested
479*b7c941bbSAndroid Build Coastguard Worker    # use the readback value for calculations instead.
480*b7c941bbSAndroid Build Coastguard Worker    iso_cap = cap['metadata']['android.sensor.sensitivity']
481*b7c941bbSAndroid Build Coastguard Worker
482*b7c941bbSAndroid Build Coastguard Worker    # Different iso values may result in captures with the same iso_cap
483*b7c941bbSAndroid Build Coastguard Worker    # value, so skip this capture if it's redundant.
484*b7c941bbSAndroid Build Coastguard Worker    if iso_cap == pre_iso_cap:
485*b7c941bbSAndroid Build Coastguard Worker      logging.info(
486*b7c941bbSAndroid Build Coastguard Worker          'Skip current capture because of the same iso %d with the previous'
487*b7c941bbSAndroid Build Coastguard Worker          ' capture.',
488*b7c941bbSAndroid Build Coastguard Worker          iso_cap,
489*b7c941bbSAndroid Build Coastguard Worker      )
490*b7c941bbSAndroid Build Coastguard Worker      iso = get_next_iso(iso, sens_max_meas, iso_multiplier)
491*b7c941bbSAndroid Build Coastguard Worker      continue
492*b7c941bbSAndroid Build Coastguard Worker    pre_iso_cap = iso_cap
493*b7c941bbSAndroid Build Coastguard Worker
494*b7c941bbSAndroid Build Coastguard Worker    logging.info('Request ISO: %d, Capture ISO: %d.', iso, iso_cap)
495*b7c941bbSAndroid Build Coastguard Worker
496*b7c941bbSAndroid Build Coastguard Worker    for bracket in range(max_bracket):
497*b7c941bbSAndroid Build Coastguard Worker      # Get the exposure for this sensitivity and exposure time.
498*b7c941bbSAndroid Build Coastguard Worker      exposure_ns = round(math.pow(2, bracket) * auto_exposure_ns / iso)
499*b7c941bbSAndroid Build Coastguard Worker      exposure_ms = round(exposure_ns * 1.0e-6, 3)
500*b7c941bbSAndroid Build Coastguard Worker      logging.info('ISO: %d, exposure time: %.3f ms.', iso_cap, exposure_ms)
501*b7c941bbSAndroid Build Coastguard Worker      req = capture_request_utils.manual_capture_request(
502*b7c941bbSAndroid Build Coastguard Worker          iso_cap,
503*b7c941bbSAndroid Build Coastguard Worker          exposure_ns,
504*b7c941bbSAndroid Build Coastguard Worker          f_dist,
505*b7c941bbSAndroid Build Coastguard Worker      )
506*b7c941bbSAndroid Build Coastguard Worker      req['android.control.zoomRatio'] = zoom_ratio
507*b7c941bbSAndroid Build Coastguard Worker      cap = cam.do_capture(req, stats_config)
508*b7c941bbSAndroid Build Coastguard Worker
509*b7c941bbSAndroid Build Coastguard Worker      if is_debug_mode:
510*b7c941bbSAndroid Build Coastguard Worker        capture_path = os.path.join(
511*b7c941bbSAndroid Build Coastguard Worker            capture_folder, f'iso{iso_cap}_exposure{exposure_ns}ns.jpg'
512*b7c941bbSAndroid Build Coastguard Worker        )
513*b7c941bbSAndroid Build Coastguard Worker        crop_and_save_capture(cap, props, capture_path, num_tiles_crop)
514*b7c941bbSAndroid Build Coastguard Worker
515*b7c941bbSAndroid Build Coastguard Worker      mean_img, var_img = image_processing_utils.unpack_rawstats_capture(
516*b7c941bbSAndroid Build Coastguard Worker          cap, num_channels=num_channels
517*b7c941bbSAndroid Build Coastguard Worker      )
518*b7c941bbSAndroid Build Coastguard Worker      cfa_order = image_processing_utils.get_canonical_cfa_order(
519*b7c941bbSAndroid Build Coastguard Worker          props, is_quad_bayer
520*b7c941bbSAndroid Build Coastguard Worker      )
521*b7c941bbSAndroid Build Coastguard Worker
522*b7c941bbSAndroid Build Coastguard Worker      means, vars_ = crop_and_reorder_stats_images(
523*b7c941bbSAndroid Build Coastguard Worker          mean_img,
524*b7c941bbSAndroid Build Coastguard Worker          var_img,
525*b7c941bbSAndroid Build Coastguard Worker          num_tiles_crop,
526*b7c941bbSAndroid Build Coastguard Worker          cfa_order,
527*b7c941bbSAndroid Build Coastguard Worker      )
528*b7c941bbSAndroid Build Coastguard Worker      if is_debug_mode:
529*b7c941bbSAndroid Build Coastguard Worker        logging.info('Raw stats image size: %s', mean_img.shape)
530*b7c941bbSAndroid Build Coastguard Worker        logging.info('R plane means image size: %s', means[0].shape)
531*b7c941bbSAndroid Build Coastguard Worker        logging.info(
532*b7c941bbSAndroid Build Coastguard Worker            'means min: %.3f, median: %.3f, max: %.3f',
533*b7c941bbSAndroid Build Coastguard Worker            np.min(means), np.median(means), np.max(means),
534*b7c941bbSAndroid Build Coastguard Worker        )
535*b7c941bbSAndroid Build Coastguard Worker        logging.info(
536*b7c941bbSAndroid Build Coastguard Worker            'vars_ min: %.4f, median: %.4f, max: %.4f',
537*b7c941bbSAndroid Build Coastguard Worker            np.min(vars_), np.median(vars_), np.max(vars_),
538*b7c941bbSAndroid Build Coastguard Worker        )
539*b7c941bbSAndroid Build Coastguard Worker
540*b7c941bbSAndroid Build Coastguard Worker      black_levels = image_processing_utils.get_black_levels(
541*b7c941bbSAndroid Build Coastguard Worker          props,
542*b7c941bbSAndroid Build Coastguard Worker          cap['metadata'],
543*b7c941bbSAndroid Build Coastguard Worker          is_quad_bayer,
544*b7c941bbSAndroid Build Coastguard Worker      )
545*b7c941bbSAndroid Build Coastguard Worker
546*b7c941bbSAndroid Build Coastguard Worker      means, vars_ = filter_stats(
547*b7c941bbSAndroid Build Coastguard Worker          means,
548*b7c941bbSAndroid Build Coastguard Worker          vars_,
549*b7c941bbSAndroid Build Coastguard Worker          black_levels,
550*b7c941bbSAndroid Build Coastguard Worker          white_level,
551*b7c941bbSAndroid Build Coastguard Worker          max_signal_value,
552*b7c941bbSAndroid Build Coastguard Worker          is_remove_var_outliers,
553*b7c941bbSAndroid Build Coastguard Worker          outlier_median_abs_deviations,
554*b7c941bbSAndroid Build Coastguard Worker      )
555*b7c941bbSAndroid Build Coastguard Worker
556*b7c941bbSAndroid Build Coastguard Worker      iso_to_stats_dict[iso_cap].append((exposure_ms, means, vars_))
557*b7c941bbSAndroid Build Coastguard Worker
558*b7c941bbSAndroid Build Coastguard Worker    if stats_file_name:
559*b7c941bbSAndroid Build Coastguard Worker      with open(stats_file_path, 'wb+') as f:
560*b7c941bbSAndroid Build Coastguard Worker        pickle.dump(iso_to_stats_dict, f)
561*b7c941bbSAndroid Build Coastguard Worker    iso = get_next_iso(iso, sens_max_meas, iso_multiplier)
562*b7c941bbSAndroid Build Coastguard Worker
563*b7c941bbSAndroid Build Coastguard Worker  return iso_to_stats_dict
564*b7c941bbSAndroid Build Coastguard Worker
565*b7c941bbSAndroid Build Coastguard Worker
566*b7c941bbSAndroid Build Coastguard Workerdef measure_linear_noise_models(
567*b7c941bbSAndroid Build Coastguard Worker    iso_to_stats_dict: Dict[int, List[Tuple[float, np.ndarray, np.ndarray]]],
568*b7c941bbSAndroid Build Coastguard Worker    color_planes: List[str],
569*b7c941bbSAndroid Build Coastguard Worker):
570*b7c941bbSAndroid Build Coastguard Worker  """Measures linear noise models.
571*b7c941bbSAndroid Build Coastguard Worker
572*b7c941bbSAndroid Build Coastguard Worker  This function measures linear noise models from means and variances for each
573*b7c941bbSAndroid Build Coastguard Worker  color plane and ISO setting.
574*b7c941bbSAndroid Build Coastguard Worker
575*b7c941bbSAndroid Build Coastguard Worker  Args:
576*b7c941bbSAndroid Build Coastguard Worker      iso_to_stats_dict: A dictionary mapping ISO settings to a list of stats
577*b7c941bbSAndroid Build Coastguard Worker        data.
578*b7c941bbSAndroid Build Coastguard Worker      color_planes: A list of color planes.
579*b7c941bbSAndroid Build Coastguard Worker
580*b7c941bbSAndroid Build Coastguard Worker  Returns:
581*b7c941bbSAndroid Build Coastguard Worker      A tuple containing:
582*b7c941bbSAndroid Build Coastguard Worker          measured_models: A list of linear models, one for each color plane.
583*b7c941bbSAndroid Build Coastguard Worker          samples: A list of samples, one for each color plane. Each sample is a
584*b7c941bbSAndroid Build Coastguard Worker              tuple of (iso, mean, var).
585*b7c941bbSAndroid Build Coastguard Worker  """
586*b7c941bbSAndroid Build Coastguard Worker  num_planes = len(color_planes)
587*b7c941bbSAndroid Build Coastguard Worker  # Model parameters for each color plane.
588*b7c941bbSAndroid Build Coastguard Worker  measured_models = [[] for _ in range(num_planes)]
589*b7c941bbSAndroid Build Coastguard Worker  # Samples (ISO, mean and var) of each quad Bayer color channels.
590*b7c941bbSAndroid Build Coastguard Worker  samples = [[] for _ in range(num_planes)]
591*b7c941bbSAndroid Build Coastguard Worker
592*b7c941bbSAndroid Build Coastguard Worker  for iso in sorted(iso_to_stats_dict.keys()):
593*b7c941bbSAndroid Build Coastguard Worker    logging.info('Calculating measured models for ISO %d.', iso)
594*b7c941bbSAndroid Build Coastguard Worker    stats_per_plane = [[] for _ in range(num_planes)]
595*b7c941bbSAndroid Build Coastguard Worker    for _, means, vars_ in iso_to_stats_dict[iso]:
596*b7c941bbSAndroid Build Coastguard Worker      for pidx in range(num_planes):
597*b7c941bbSAndroid Build Coastguard Worker        means_p = means[pidx]
598*b7c941bbSAndroid Build Coastguard Worker        vars_p = vars_[pidx]
599*b7c941bbSAndroid Build Coastguard Worker        if means_p.size > 0 and vars_p.size > 0:
600*b7c941bbSAndroid Build Coastguard Worker          stats_per_plane[pidx].extend(list(zip(means_p, vars_p)))
601*b7c941bbSAndroid Build Coastguard Worker
602*b7c941bbSAndroid Build Coastguard Worker    for pidx, mean_var_pairs in enumerate(stats_per_plane):
603*b7c941bbSAndroid Build Coastguard Worker      if not mean_var_pairs:
604*b7c941bbSAndroid Build Coastguard Worker        raise ValueError(
605*b7c941bbSAndroid Build Coastguard Worker            f'For ISO {iso}, samples are empty in color plane'
606*b7c941bbSAndroid Build Coastguard Worker            f' {color_planes[pidx]}.'
607*b7c941bbSAndroid Build Coastguard Worker        )
608*b7c941bbSAndroid Build Coastguard Worker      slope, intercept, rvalue, _, _ = scipy.stats.linregress(mean_var_pairs)
609*b7c941bbSAndroid Build Coastguard Worker
610*b7c941bbSAndroid Build Coastguard Worker      measured_models[pidx].append((iso, slope, intercept))
611*b7c941bbSAndroid Build Coastguard Worker      logging.info(
612*b7c941bbSAndroid Build Coastguard Worker          (
613*b7c941bbSAndroid Build Coastguard Worker              'Measured model for ISO %d and color plane %s: '
614*b7c941bbSAndroid Build Coastguard Worker              'y = %e * x + %e (R=%.6f).'
615*b7c941bbSAndroid Build Coastguard Worker          ),
616*b7c941bbSAndroid Build Coastguard Worker          iso, color_planes[pidx], slope, intercept, rvalue,
617*b7c941bbSAndroid Build Coastguard Worker      )
618*b7c941bbSAndroid Build Coastguard Worker
619*b7c941bbSAndroid Build Coastguard Worker      # Add the samples for this sensitivity to the global samples list.
620*b7c941bbSAndroid Build Coastguard Worker      samples[pidx].extend([(iso, mean, var) for (mean, var) in mean_var_pairs])
621*b7c941bbSAndroid Build Coastguard Worker
622*b7c941bbSAndroid Build Coastguard Worker  return measured_models, samples
623*b7c941bbSAndroid Build Coastguard Worker
624*b7c941bbSAndroid Build Coastguard Worker
625*b7c941bbSAndroid Build Coastguard Workerdef compute_noise_model(
626*b7c941bbSAndroid Build Coastguard Worker    samples: List[List[Tuple[float, np.ndarray, np.ndarray]]],
627*b7c941bbSAndroid Build Coastguard Worker    sens_max_analog: int,
628*b7c941bbSAndroid Build Coastguard Worker    offset_a: np.ndarray,
629*b7c941bbSAndroid Build Coastguard Worker    offset_b: np.ndarray,
630*b7c941bbSAndroid Build Coastguard Worker    is_two_stage_model: bool = False,
631*b7c941bbSAndroid Build Coastguard Worker) -> np.ndarray:
632*b7c941bbSAndroid Build Coastguard Worker  """Computes noise model parameters from samples.
633*b7c941bbSAndroid Build Coastguard Worker
634*b7c941bbSAndroid Build Coastguard Worker  The noise model is defined by the following equation:
635*b7c941bbSAndroid Build Coastguard Worker    f(x) = scale * x + offset
636*b7c941bbSAndroid Build Coastguard Worker
637*b7c941bbSAndroid Build Coastguard Worker  where we have:
638*b7c941bbSAndroid Build Coastguard Worker    scale = scale_a * analog_gain * digital_gain + scale_b,
639*b7c941bbSAndroid Build Coastguard Worker    offset = (offset_a * analog_gain^2 + offset_b) * digital_gain^2.
640*b7c941bbSAndroid Build Coastguard Worker    scale is the multiplicative factor and offset is the offset term.
641*b7c941bbSAndroid Build Coastguard Worker
642*b7c941bbSAndroid Build Coastguard Worker  Assume digital_gain is 1.0 and scale_a, scale_b, offset_a, offset_b are
643*b7c941bbSAndroid Build Coastguard Worker  sa, sb, oa, ob respectively, so we have noise model function:
644*b7c941bbSAndroid Build Coastguard Worker  f(x) = (sa * analog_gain + sb) * x + (oa * analog_gain^2 + ob).
645*b7c941bbSAndroid Build Coastguard Worker
646*b7c941bbSAndroid Build Coastguard Worker  The noise model is fit to the mesuared data using the scipy.optimize
647*b7c941bbSAndroid Build Coastguard Worker  function, which uses an iterative Levenberg-Marquardt algorithm to
648*b7c941bbSAndroid Build Coastguard Worker  find the model parameters that minimize the mean squared error.
649*b7c941bbSAndroid Build Coastguard Worker
650*b7c941bbSAndroid Build Coastguard Worker  Args:
651*b7c941bbSAndroid Build Coastguard Worker    samples: A list of samples, each of which is a list of tuples of `(gains,
652*b7c941bbSAndroid Build Coastguard Worker      means, vars_)`.
653*b7c941bbSAndroid Build Coastguard Worker    sens_max_analog: The maximum analog gain.
654*b7c941bbSAndroid Build Coastguard Worker    offset_a: The gradient coefficients from the read noise calibration.
655*b7c941bbSAndroid Build Coastguard Worker    offset_b: The intercept coefficients from the read noise calibration.
656*b7c941bbSAndroid Build Coastguard Worker    is_two_stage_model: A boolean flag indicating if the noise model is
657*b7c941bbSAndroid Build Coastguard Worker      calibrated in the two-stage mode.
658*b7c941bbSAndroid Build Coastguard Worker
659*b7c941bbSAndroid Build Coastguard Worker  Returns:
660*b7c941bbSAndroid Build Coastguard Worker    A numpy array containing noise model parameters (scale_a, scale_b,
661*b7c941bbSAndroid Build Coastguard Worker    offset_a, offset_b) of each channel.
662*b7c941bbSAndroid Build Coastguard Worker  """
663*b7c941bbSAndroid Build Coastguard Worker  noise_model = []
664*b7c941bbSAndroid Build Coastguard Worker  for pidx, samples_p in enumerate(samples):
665*b7c941bbSAndroid Build Coastguard Worker    gains, means, vars_ = zip(*samples_p)
666*b7c941bbSAndroid Build Coastguard Worker    gains = np.asarray(gains).flatten()
667*b7c941bbSAndroid Build Coastguard Worker    means = np.asarray(means).flatten()
668*b7c941bbSAndroid Build Coastguard Worker    vars_ = np.asarray(vars_).flatten()
669*b7c941bbSAndroid Build Coastguard Worker
670*b7c941bbSAndroid Build Coastguard Worker    compute_digital_gains(gains, sens_max_analog)
671*b7c941bbSAndroid Build Coastguard Worker
672*b7c941bbSAndroid Build Coastguard Worker    # Use a global linear optimization to fit the noise model.
673*b7c941bbSAndroid Build Coastguard Worker    # Noise model function:
674*b7c941bbSAndroid Build Coastguard Worker    # f(x) = scale * x + offset
675*b7c941bbSAndroid Build Coastguard Worker    # Where:
676*b7c941bbSAndroid Build Coastguard Worker    # scale = scale_a * analog_gain * digital_gain + scale_b.
677*b7c941bbSAndroid Build Coastguard Worker    # offset = (offset_a * analog_gain^2 + offset_b) * digital_gain^2.
678*b7c941bbSAndroid Build Coastguard Worker    # Function f will be used to train the scale and offset coefficients
679*b7c941bbSAndroid Build Coastguard Worker    # scale_a, scale_b, offset_a, offset_b.
680*b7c941bbSAndroid Build Coastguard Worker    if is_two_stage_model:
681*b7c941bbSAndroid Build Coastguard Worker      # For the two-stage model, we want to use the line fit coefficients
682*b7c941bbSAndroid Build Coastguard Worker      # found from capturing read noise data (offset_a and offset_b) to
683*b7c941bbSAndroid Build Coastguard Worker      # train the scale coefficients.
684*b7c941bbSAndroid Build Coastguard Worker      oa, ob = offset_a[pidx], offset_b[pidx]
685*b7c941bbSAndroid Build Coastguard Worker
686*b7c941bbSAndroid Build Coastguard Worker      # Cannot pass oa and ob as the parameters of f since we only want
687*b7c941bbSAndroid Build Coastguard Worker      # curve_fit return 2 parameters.
688*b7c941bbSAndroid Build Coastguard Worker      def f(x, sa, sb):
689*b7c941bbSAndroid Build Coastguard Worker        scale = sa * x[0] + sb
690*b7c941bbSAndroid Build Coastguard Worker        # pylint: disable=cell-var-from-loop
691*b7c941bbSAndroid Build Coastguard Worker        offset = oa * x[0] ** 2 + ob
692*b7c941bbSAndroid Build Coastguard Worker        return (scale * x[1] + offset) / x[0]
693*b7c941bbSAndroid Build Coastguard Worker
694*b7c941bbSAndroid Build Coastguard Worker    else:
695*b7c941bbSAndroid Build Coastguard Worker      def f(x, sa, sb, oa, ob):
696*b7c941bbSAndroid Build Coastguard Worker        scale = sa * x[0] + sb
697*b7c941bbSAndroid Build Coastguard Worker        offset = oa * x[0] ** 2 + ob
698*b7c941bbSAndroid Build Coastguard Worker        return (scale * x[1] + offset) / x[0]
699*b7c941bbSAndroid Build Coastguard Worker
700*b7c941bbSAndroid Build Coastguard Worker    # Divide the whole system by gains*means.
701*b7c941bbSAndroid Build Coastguard Worker    coeffs, _ = scipy.optimize.curve_fit(f, (gains, means), vars_ / (gains))
702*b7c941bbSAndroid Build Coastguard Worker
703*b7c941bbSAndroid Build Coastguard Worker    # If using two-stage model, two of the coefficients calculated above are
704*b7c941bbSAndroid Build Coastguard Worker    # constant, so we need to append them to the coeffs ndarray.
705*b7c941bbSAndroid Build Coastguard Worker    if is_two_stage_model:
706*b7c941bbSAndroid Build Coastguard Worker      coeffs = np.append(coeffs, offset_a[pidx])
707*b7c941bbSAndroid Build Coastguard Worker      coeffs = np.append(coeffs, offset_b[pidx])
708*b7c941bbSAndroid Build Coastguard Worker
709*b7c941bbSAndroid Build Coastguard Worker    # coeffs[0:4] = (scale_a, scale_b, offset_a, offset_b).
710*b7c941bbSAndroid Build Coastguard Worker    noise_model.append(coeffs[0:4])
711*b7c941bbSAndroid Build Coastguard Worker
712*b7c941bbSAndroid Build Coastguard Worker  noise_model = np.asarray(noise_model)
713*b7c941bbSAndroid Build Coastguard Worker  check_noise_model_shape(noise_model)
714*b7c941bbSAndroid Build Coastguard Worker  return noise_model
715*b7c941bbSAndroid Build Coastguard Worker
716*b7c941bbSAndroid Build Coastguard Worker
717*b7c941bbSAndroid Build Coastguard Workerdef create_stats_figure(
718*b7c941bbSAndroid Build Coastguard Worker    iso: int,
719*b7c941bbSAndroid Build Coastguard Worker    color_channel_names: List[str],
720*b7c941bbSAndroid Build Coastguard Worker):
721*b7c941bbSAndroid Build Coastguard Worker  """Creates a figure with subplots showing the mean and variance samples.
722*b7c941bbSAndroid Build Coastguard Worker
723*b7c941bbSAndroid Build Coastguard Worker  Args:
724*b7c941bbSAndroid Build Coastguard Worker    iso: The ISO setting for the images.
725*b7c941bbSAndroid Build Coastguard Worker    color_channel_names: A list of strings containing the names of the color
726*b7c941bbSAndroid Build Coastguard Worker      channels.
727*b7c941bbSAndroid Build Coastguard Worker
728*b7c941bbSAndroid Build Coastguard Worker  Returns:
729*b7c941bbSAndroid Build Coastguard Worker    A tuple of the figure and a list of the subplots.
730*b7c941bbSAndroid Build Coastguard Worker  """
731*b7c941bbSAndroid Build Coastguard Worker  if len(color_channel_names) not in noise_model_constants.VALID_NUM_CHANNELS:
732*b7c941bbSAndroid Build Coastguard Worker    raise AssertionError(
733*b7c941bbSAndroid Build Coastguard Worker        'The number of channels should be in'
734*b7c941bbSAndroid Build Coastguard Worker        f' {noise_model_constants.VALID_NUM_CHANNELS}, but found'
735*b7c941bbSAndroid Build Coastguard Worker        f' {len(color_channel_names)}. '
736*b7c941bbSAndroid Build Coastguard Worker    )
737*b7c941bbSAndroid Build Coastguard Worker
738*b7c941bbSAndroid Build Coastguard Worker  is_quad_bayer = (
739*b7c941bbSAndroid Build Coastguard Worker      len(color_channel_names) == noise_model_constants.NUM_QUAD_BAYER_CHANNELS
740*b7c941bbSAndroid Build Coastguard Worker  )
741*b7c941bbSAndroid Build Coastguard Worker  if is_quad_bayer:
742*b7c941bbSAndroid Build Coastguard Worker    # Adds a plot of the mean and variance samples for each color plane.
743*b7c941bbSAndroid Build Coastguard Worker    fig, axes = plt.subplots(4, 4, figsize=(22, 22))
744*b7c941bbSAndroid Build Coastguard Worker    fig.gca()
745*b7c941bbSAndroid Build Coastguard Worker    fig.suptitle('ISO %d' % iso, x=0.52, y=0.99)
746*b7c941bbSAndroid Build Coastguard Worker
747*b7c941bbSAndroid Build Coastguard Worker    cax = fig.add_axes([0.65, 0.995, 0.33, 0.003])
748*b7c941bbSAndroid Build Coastguard Worker    cax.set_title('log(exposure_ms):', x=-0.13, y=-2.0)
749*b7c941bbSAndroid Build Coastguard Worker    fig.colorbar(
750*b7c941bbSAndroid Build Coastguard Worker        noise_model_constants.COLOR_BAR, cax=cax, orientation='horizontal'
751*b7c941bbSAndroid Build Coastguard Worker    )
752*b7c941bbSAndroid Build Coastguard Worker
753*b7c941bbSAndroid Build Coastguard Worker    # Add a big axis, hide frame.
754*b7c941bbSAndroid Build Coastguard Worker    fig.add_subplot(111, frameon=False)
755*b7c941bbSAndroid Build Coastguard Worker
756*b7c941bbSAndroid Build Coastguard Worker    # Add a common x-axis and y-axis.
757*b7c941bbSAndroid Build Coastguard Worker    plt.tick_params(
758*b7c941bbSAndroid Build Coastguard Worker        labelcolor='none',
759*b7c941bbSAndroid Build Coastguard Worker        which='both',
760*b7c941bbSAndroid Build Coastguard Worker        top=False,
761*b7c941bbSAndroid Build Coastguard Worker        bottom=False,
762*b7c941bbSAndroid Build Coastguard Worker        left=False,
763*b7c941bbSAndroid Build Coastguard Worker        right=False,
764*b7c941bbSAndroid Build Coastguard Worker    )
765*b7c941bbSAndroid Build Coastguard Worker    plt.xlabel('Mean signal level', ha='center')
766*b7c941bbSAndroid Build Coastguard Worker    plt.ylabel('Variance', va='center', rotation='vertical')
767*b7c941bbSAndroid Build Coastguard Worker
768*b7c941bbSAndroid Build Coastguard Worker    subplots = []
769*b7c941bbSAndroid Build Coastguard Worker    for pidx in range(noise_model_constants.NUM_QUAD_BAYER_CHANNELS):
770*b7c941bbSAndroid Build Coastguard Worker      subplot = axes[pidx // 4, pidx % 4]
771*b7c941bbSAndroid Build Coastguard Worker      subplot.set_title(color_channel_names[pidx])
772*b7c941bbSAndroid Build Coastguard Worker      # Set 'y' axis to scientific notation for all numbers by setting
773*b7c941bbSAndroid Build Coastguard Worker      # scilimits to (0, 0).
774*b7c941bbSAndroid Build Coastguard Worker      subplot.ticklabel_format(axis='y', style='sci', scilimits=(0, 0))
775*b7c941bbSAndroid Build Coastguard Worker      subplots.append(subplot)
776*b7c941bbSAndroid Build Coastguard Worker
777*b7c941bbSAndroid Build Coastguard Worker  else:
778*b7c941bbSAndroid Build Coastguard Worker    # Adds a plot of the mean and variance samples for each color plane.
779*b7c941bbSAndroid Build Coastguard Worker    fig, [[plt_r, plt_gr], [plt_gb, plt_b]] = plt.subplots(
780*b7c941bbSAndroid Build Coastguard Worker        2, 2, figsize=(11, 11)
781*b7c941bbSAndroid Build Coastguard Worker    )
782*b7c941bbSAndroid Build Coastguard Worker    fig.gca()
783*b7c941bbSAndroid Build Coastguard Worker    # Add color bar to show exposure times.
784*b7c941bbSAndroid Build Coastguard Worker    cax = fig.add_axes([0.73, 0.99, 0.25, 0.01])
785*b7c941bbSAndroid Build Coastguard Worker    cax.set_title('log(exposure_ms):', x=-0.3, y=-1.0)
786*b7c941bbSAndroid Build Coastguard Worker    fig.colorbar(
787*b7c941bbSAndroid Build Coastguard Worker        noise_model_constants.COLOR_BAR, cax=cax, orientation='horizontal'
788*b7c941bbSAndroid Build Coastguard Worker    )
789*b7c941bbSAndroid Build Coastguard Worker
790*b7c941bbSAndroid Build Coastguard Worker    subplots = [plt_r, plt_gr, plt_gb, plt_b]
791*b7c941bbSAndroid Build Coastguard Worker    fig.suptitle('ISO %d' % iso, x=0.54, y=0.99)
792*b7c941bbSAndroid Build Coastguard Worker    for pidx, subplot in enumerate(subplots):
793*b7c941bbSAndroid Build Coastguard Worker      subplot.set_title(color_channel_names[pidx])
794*b7c941bbSAndroid Build Coastguard Worker      subplot.set_xlabel('Mean signal level')
795*b7c941bbSAndroid Build Coastguard Worker      subplot.set_ylabel('Variance')
796*b7c941bbSAndroid Build Coastguard Worker      subplot.ticklabel_format(axis='y', style='sci', scilimits=(0, 0))
797*b7c941bbSAndroid Build Coastguard Worker
798*b7c941bbSAndroid Build Coastguard Worker  with warnings.catch_warnings():
799*b7c941bbSAndroid Build Coastguard Worker    warnings.simplefilter('ignore', UserWarning)
800*b7c941bbSAndroid Build Coastguard Worker    plt.tight_layout()
801*b7c941bbSAndroid Build Coastguard Worker
802*b7c941bbSAndroid Build Coastguard Worker  return fig, subplots
803