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