1# Copyright 2014 The Android Open Source Project 2# 3# Licensed under the Apache License, Version 2.0 (the "License"); 4# you may not use this file except in compliance with the License. 5# You may obtain a copy of the License at 6# 7# http://www.apache.org/licenses/LICENSE-2.0 8# 9# Unless required by applicable law or agreed to in writing, software 10# distributed under the License is distributed on an "AS IS" BASIS, 11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12# See the License for the specific language governing permissions and 13# limitations under the License. 14"""CameraITS script to generate noise models.""" 15 16import logging 17import math 18import os.path 19import pathlib 20import pickle 21import tempfile 22import textwrap 23 24import capture_read_noise_utils 25import its_base_test 26import its_session_utils 27from matplotlib import pylab 28import matplotlib.pyplot as plt 29import matplotlib.ticker 30from mobly import test_runner 31import noise_model_constants 32import noise_model_utils 33import numpy as np 34 35_IS_QUAD_BAYER = False # A manual flag to choose standard or quad Bayer noise 36 # model generation. 37if _IS_QUAD_BAYER: 38 _COLOR_CHANNEL_NAMES = noise_model_constants.QUAD_BAYER_COLORS 39 _PLOT_COLORS = noise_model_constants.QUAD_BAYER_PLOT_COLORS 40 _TILE_SIZE = 64 # Tile size to compute mean/variance. Large tiles may have 41 # their variance corrupted by low freq image changes. 42 _STATS_FORMAT = 'raw10QuadBayerStats' # rawQuadBayerStats|raw10QuadBayerStats 43 _READ_NOISE_RAW_FORMAT = 'raw10QuadBayer' # rawQuadBayer|raw10QuadBayer 44else: 45 _COLOR_CHANNEL_NAMES = noise_model_constants.BAYER_COLORS 46 _PLOT_COLORS = noise_model_constants.BAYER_PLOT_COLORS 47 _TILE_SIZE = 32 # Tile size to compute mean/variance. Large tiles may have 48 # their variance corrupted by low freq image changes. 49 _STATS_FORMAT = 'rawStats' # rawStats|raw10Stats 50 _READ_NOISE_RAW_FORMAT = 'raw' # raw|raw10 51 52_STATS_CONFIG = { 53 'format': _STATS_FORMAT, 54 'gridWidth': _TILE_SIZE, 55 'gridHeight': _TILE_SIZE, 56} 57_BRACKET_MAX = 8 # Exposure bracketing range in stops 58_BRACKET_FACTOR = math.pow(2, _BRACKET_MAX) 59_ISO_MAX_VALUE = None # ISO range max value, uses sensor max if None 60_ISO_MIN_VALUE = None # ISO range min value, uses sensor min if None 61_MAX_SCALE_FUDGE = 1.1 62_MAX_SIGNAL_VALUE = 0.25 # Maximum value to allow mean of the tiles to go. 63_NAME = os.path.basename(__file__).split('.')[0] 64_NAME_READ_NOISE = os.path.join(tempfile.gettempdir(), 'CameraITS/ReadNoise') 65_NAME_READ_NOISE_FILE = 'read_noise_results.pkl' 66_STATS_FILE_NAME = 'stats.pkl' 67_OUTLIER_MEDIAN_ABS_DEVS = 10 # Defines the number of Median Absolute 68 # Deviations that constitutes acceptable data 69_READ_NOISE_STEPS_PER_STOP = 12 # Sensitivities per stop to sample for read 70 # noise 71_REMOVE_VAR_OUTLIERS = False # When True, filters the variance to remove 72 # outliers 73_STEPS_PER_STOP = 3 # How many sensitivities per stop to sample. 74_ISO_MULTIPLIER = math.pow(2, 1.0 / _STEPS_PER_STOP) 75_TILE_CROP_N = 0 # Number of tiles to crop from edge of image. Usually 0. 76_TWO_STAGE_MODEL = False # Require read noise data prior to running noise model 77_ZOOM_RATIO = 1 # Zoom target to be used while running the model 78_FIG_DPI = 100 # DPI for plotting noise model figures. 79_BAYER_COLORS_FOR_NOISE_PROFILE = tuple( 80 map(str.lower, noise_model_constants.BAYER_COLORS) 81) 82_QUAD_BAYER_COLORS_FOR_NOISE_PROFILE = tuple( 83 map(str.lower, noise_model_constants.QUAD_BAYER_COLORS) 84) 85 86 87class DngNoiseModel(its_base_test.ItsBaseTest): 88 """Create DNG noise model. 89 90 Captures RAW images with increasing analog gains to create the model. 91 """ 92 93 def _create_noise_model_code(self, noise_model, sens_min, sens_max, 94 sens_max_analog, file_path): 95 """Creates the C file for the noise model. 96 97 Args: 98 noise_model: Noise model parameters. 99 sens_min: The minimum sensitivity value. 100 sens_max: The maximum sensitivity value. 101 sens_max_analog: The maximum analog sensitivity value. 102 file_path: The path to the noise model file. 103 """ 104 # Generate individual noise model components. 105 scale_a, scale_b, offset_a, offset_b = zip(*noise_model) 106 digital_gain_cdef = ( 107 f'(sens / {sens_max_analog:.1f}) < 1.0 ? ' 108 f'1.0 : (sens / {sens_max_analog:.1f})' 109 ) 110 111 with open(file_path, 'w') as text_file: 112 scale_a_str = ','.join([str(i) for i in scale_a]) 113 scale_b_str = ','.join([str(i) for i in scale_b]) 114 offset_a_str = ','.join([str(i) for i in offset_a]) 115 offset_b_str = ','.join([str(i) for i in offset_b]) 116 # pylint: disable=line-too-long 117 code = textwrap.dedent(f"""\ 118 /* Generated test code to dump a table of data for external validation 119 * of the noise model parameters. 120 */ 121 #include <stdio.h> 122 #include <assert.h> 123 double compute_noise_model_entry_S(int plane, int sens); 124 double compute_noise_model_entry_O(int plane, int sens); 125 int main(void) {{ 126 for (int plane = 0; plane < {len(scale_a)}; plane++) {{ 127 for (int sens = {sens_min}; sens <= {sens_max}; sens += 100) {{ 128 double o = compute_noise_model_entry_O(plane, sens); 129 double s = compute_noise_model_entry_S(plane, sens); 130 printf("%d,%d,%lf,%lf\\n", plane, sens, o, s); 131 }} 132 }} 133 return 0; 134 }} 135 136 /* Generated functions to map a given sensitivity to the O and S noise 137 * model parameters in the DNG noise model. The planes are in 138 * R, Gr, Gb, B order. 139 */ 140 double compute_noise_model_entry_S(int plane, int sens) {{ 141 static double noise_model_A[] = {{ {scale_a_str:s} }}; 142 static double noise_model_B[] = {{ {scale_b_str:s} }}; 143 double A = noise_model_A[plane]; 144 double B = noise_model_B[plane]; 145 double s = A * sens + B; 146 return s < 0.0 ? 0.0 : s; 147 }} 148 149 double compute_noise_model_entry_O(int plane, int sens) {{ 150 static double noise_model_C[] = {{ {offset_a_str:s} }}; 151 static double noise_model_D[] = {{ {offset_b_str:s} }}; 152 double digital_gain = {digital_gain_cdef:s}; 153 double C = noise_model_C[plane]; 154 double D = noise_model_D[plane]; 155 double o = C * sens * sens + D * digital_gain * digital_gain; 156 return o < 0.0 ? 0.0 : o; 157 }} 158 """) 159 160 text_file.write(code) 161 162 def _create_noise_profile_code(self, noise_model, color_channels, file_path): 163 """Creates the noise profile C++ file. 164 165 Args: 166 noise_model: Noise model parameters. 167 color_channels: Color channels in canonical order. 168 file_path: The path to the noise profile C++ file. 169 """ 170 # Generate individual noise model components. 171 scale_a, scale_b, offset_a, offset_b = zip(*noise_model) 172 num_channels = noise_model.shape[0] 173 params = [] 174 for ch, color in enumerate(color_channels): 175 prefix = f'.noise_coefficients_{color} = {{' 176 spaces = ' ' * len(prefix) 177 suffix = '},' if ch != num_channels - 1 else '}' 178 params.append(textwrap.dedent(f""" 179 {prefix}.gradient_slope = {scale_a[ch]}, 180 {spaces}.offset_slope = {scale_b[ch]}, 181 {spaces}.gradient_intercept = {offset_a[ch]}, 182 {spaces}.offset_intercept = {offset_b[ch]}{suffix}""")) 183 184 with open(file_path, 'w') as text_file: 185 # pylint: disable=line-too-long 186 code_comment = textwrap.dedent("""\ 187 /* noise_profile.cc 188 Note: gradient_slope --> gradient of API s_measured parameter 189 offset_slope --> o_model of API s_measured parameter 190 gradient_intercept--> gradient of API o_measured parameter 191 offset_intercept --> o_model of API o_measured parameter 192 Note: SENSOR_NOISE_PROFILE in Android Developers doc uses 193 N(x) = sqrt(Sx + O), where 'S' is 's_measured' & 'O' is 'o_measured' 194 */ 195 """) 196 params_str = textwrap.indent(''.join(params), ' ' * 4) 197 code_params = '.profile = {' + params_str + '},' 198 code = code_comment + code_params 199 text_file.write(code) 200 201 def _create_noise_model_and_profile_code(self, noise_model, sens_min, 202 sens_max, sens_max_analog, log_path): 203 """Creates the code file with noise model parameters. 204 205 Args: 206 noise_model: Noise model parameters. 207 sens_min: The minimum sensitivity value. 208 sens_max: The maximum sensitivity value. 209 sens_max_analog: The maximum analog sensitivity value. 210 log_path: The path to the log file. 211 """ 212 noise_model_utils.check_noise_model_shape(noise_model) 213 # Create noise model code with noise model parameters. 214 self._create_noise_model_code( 215 noise_model, 216 sens_min, 217 sens_max, 218 sens_max_analog, 219 os.path.join(log_path, 'noise_model.c'), 220 ) 221 222 num_channels = noise_model.shape[0] 223 is_quad_bayer = ( 224 num_channels == noise_model_constants.NUM_QUAD_BAYER_CHANNELS 225 ) 226 if is_quad_bayer: 227 # Average noise model parameters of every four channels. 228 avg_noise_model = noise_model.reshape(-1, 4, noise_model.shape[1]).mean( 229 axis=1 230 ) 231 # Create noise model code with average noise model parameters. 232 self._create_noise_model_code( 233 avg_noise_model, 234 sens_min, 235 sens_max, 236 sens_max_analog, 237 os.path.join(log_path, 'noise_model_avg.c'), 238 ) 239 # Create noise profile code with average noise model parameters. 240 self._create_noise_profile_code( 241 avg_noise_model, 242 _BAYER_COLORS_FOR_NOISE_PROFILE, 243 os.path.join(log_path, 'noise_profile_avg.cc'), 244 ) 245 # Create noise profile code with noise model parameters. 246 self._create_noise_profile_code( 247 noise_model, 248 _QUAD_BAYER_COLORS_FOR_NOISE_PROFILE, 249 os.path.join(log_path, 'noise_profile.cc'), 250 ) 251 252 else: 253 # Create noise profile code with noise model parameters. 254 self._create_noise_profile_code( 255 noise_model, 256 _BAYER_COLORS_FOR_NOISE_PROFILE, 257 os.path.join(log_path, 'noise_profile.cc'), 258 ) 259 260 def _plot_stats_and_noise_model_fittings( 261 self, iso_to_stats_dict, measured_models, noise_model, sens_max_analog, 262 folder_path_prefix): 263 """Plots the stats (means, vars_) and noise models fittings. 264 265 Args: 266 iso_to_stats_dict: A dictionary mapping ISO to a list of tuples of 267 exposure time in milliseconds, mean values, and variance values. 268 measured_models: A list of measured noise models for each ISO value. 269 noise_model: A numpy array of global noise model parameters for all ISO 270 values. 271 sens_max_analog: The maximum analog sensitivity value. 272 folder_path_prefix: The prefix of path to save figures. 273 274 Raises: 275 ValueError: If the noise model shape is invalid. 276 """ 277 noise_model_utils.check_noise_model_shape(noise_model) 278 # Separate individual noise model components. 279 scale_a, scale_b, offset_a, offset_b = zip(*noise_model) 280 281 iso_pidx_to_measured_model_dict = {} 282 num_channels = noise_model.shape[0] 283 for pidx in range(num_channels): 284 for iso, s_measured, o_measured in measured_models[pidx]: 285 iso_pidx_to_measured_model_dict[(iso, pidx)] = (s_measured, o_measured) 286 287 isos = np.asarray(sorted(iso_to_stats_dict.keys())) 288 digital_gains = noise_model_utils.compute_digital_gains( 289 isos, sens_max_analog 290 ) 291 292 x_range = [0, _MAX_SIGNAL_VALUE] 293 for iso, digital_gain in zip(isos, digital_gains): 294 logging.info('Plotting stats and noise model for ISO %d.', iso) 295 fig, subplots = noise_model_utils.create_stats_figure( 296 iso, _COLOR_CHANNEL_NAMES 297 ) 298 299 xmax = 0 300 stats_per_plane = [[] for _ in range(num_channels)] 301 for exposure_ms, means, vars_ in iso_to_stats_dict[iso]: 302 exposure_norm = noise_model_constants.COLOR_NORM(np.log2(exposure_ms)) 303 exposure_color = noise_model_constants.RAINBOW_CMAP(exposure_norm) 304 for pidx in range(num_channels): 305 means_p = means[pidx] 306 vars_p = vars_[pidx] 307 308 if means_p.size > 0 and vars_p.size > 0: 309 subplots[pidx].plot( 310 means_p, 311 vars_p, 312 color=exposure_color, 313 marker='.', 314 markeredgecolor=exposure_color, 315 markersize=1, 316 linestyle='None', 317 alpha=0.5, 318 ) 319 320 stats_per_plane[pidx].extend(list(zip(means_p, vars_p))) 321 xmax = max(xmax, max(means_p)) 322 323 iso_sq = iso ** 2 324 digital_gain_sq = digital_gain ** 2 325 for pidx in range(num_channels): 326 # Add the final noise model to subplots. 327 s_model = scale_a[pidx] * iso * digital_gain + scale_b[pidx] 328 o_model = (offset_a[pidx] * iso_sq + offset_b[pidx]) * digital_gain_sq 329 330 plot_color = _PLOT_COLORS[pidx] 331 subplots[pidx].plot( 332 x_range, 333 [o_model, s_model * _MAX_SIGNAL_VALUE + o_model], 334 color=plot_color, 335 linestyle='-', 336 label='Model', 337 alpha=0.5, 338 ) 339 340 # Add the noise model measured by captures with current iso to subplots. 341 if (iso, pidx) not in iso_pidx_to_measured_model_dict: 342 continue 343 344 s_measured, o_measured = iso_pidx_to_measured_model_dict[(iso, pidx)] 345 346 subplots[pidx].plot( 347 x_range, 348 [o_measured, s_measured * _MAX_SIGNAL_VALUE + o_measured], 349 color=plot_color, 350 linestyle='--', 351 label='Linear fit', 352 ) 353 354 ymax = (o_measured + s_measured * xmax) * _MAX_SCALE_FUDGE 355 subplots[pidx].set_xlim(xmin=0, xmax=xmax) 356 subplots[pidx].set_ylim(ymin=0, ymax=ymax) 357 subplots[pidx].legend() 358 359 fig.savefig( 360 f'{folder_path_prefix}_samples_iso{iso:04d}.png', dpi=_FIG_DPI 361 ) 362 363 def _plot_noise_model_single_plane( 364 self, pidx, plot, sens, measured_params, modeled_params): 365 """Plots the noise model for one color plane specified by pidx. 366 367 Args: 368 pidx: The index of the color plane in Bayer pattern. 369 plot: The ax to plot on. 370 sens: The sensitivity of the sensor. 371 measured_params: The measured parameters. 372 modeled_params: The modeled parameters. 373 """ 374 color_channel = _COLOR_CHANNEL_NAMES[pidx] 375 measured_label = f'{color_channel}-Measured' 376 model_label = f'{color_channel}-Model' 377 378 plot_color = _PLOT_COLORS[pidx] 379 # Plot the measured parameters. 380 plot.loglog( 381 sens, 382 measured_params, 383 color=plot_color, 384 marker='+', 385 markeredgecolor=plot_color, 386 linestyle='None', 387 base=10, 388 label=measured_label, 389 ) 390 # Plot the modeled parameters. 391 plot.loglog( 392 sens, 393 modeled_params, 394 color=plot_color, 395 marker='o', 396 markeredgecolor=plot_color, 397 linestyle='None', 398 base=10, 399 label=model_label, 400 alpha=0.3, 401 ) 402 403 def _plot_noise_model(self, isos, measured_models, noise_model, 404 sens_max_analog, name_with_log_path): 405 """Plot the noise model for a given set of ISO values. 406 407 The read noise model is defined by the following equation: 408 f(x) = s_model * x + o_model 409 where we have: 410 s_model = scale_a * analog_gain * digital_gain + scale_b is the 411 multiplicative factor, 412 o_model = (offset_a * analog_gain^2 + offset_b) * digital_gain^2 413 is the offset term. 414 415 Args: 416 isos: A list of ISO values. 417 measured_models: A list of measured models, each of which is a tuple of 418 (sens, s_measured, o_measured). 419 noise_model: Noise model parameters of each plane, each of which is a 420 tuple of (scale_a, scale_b, offset_a, offset_b). 421 sens_max_analog: The maximum analog gain. 422 name_with_log_path: The name of the file to save the logs to. 423 """ 424 noise_model_utils.check_noise_model_shape(noise_model) 425 426 # Plot noise model parameters. 427 fig, axes = plt.subplots(4, 2, figsize=(22, 17)) 428 s_plots, o_plots = axes[:, 0], axes[:, 1] 429 num_channels = noise_model.shape[0] 430 is_quad_bayer = ( 431 num_channels == noise_model_constants.NUM_QUAD_BAYER_CHANNELS 432 ) 433 for pidx, measured_model in enumerate(measured_models): 434 # Grab the sensitivities and line parameters of each sensitivity. 435 sens, s_measured, o_measured = zip(*measured_model) 436 sens = np.asarray(sens) 437 sens_sq = np.square(sens) 438 scale_a, scale_b, offset_a, offset_b = noise_model[pidx] 439 # Plot noise model components with the values predicted by the model. 440 digital_gains = noise_model_utils.compute_digital_gains( 441 sens, sens_max_analog 442 ) 443 444 # s_model = scale_a * analog_gain * digital_gain + scale_b, 445 # o_model = (offset_a * analog_gain^2 + offset_b) * digital_gain^2. 446 s_model = scale_a * sens * digital_gains + scale_b 447 o_model = (offset_a * sens_sq + offset_b) * np.square(digital_gains) 448 if is_quad_bayer: 449 s_plot, o_plot = s_plots[pidx // 4], o_plots[pidx // 4] 450 else: 451 s_plot, o_plot = s_plots[pidx], o_plots[pidx] 452 453 self._plot_noise_model_single_plane( 454 pidx, s_plot, sens, s_measured, s_model) 455 self._plot_noise_model_single_plane( 456 pidx, o_plot, sens, o_measured, o_model) 457 458 # Set figure attributes after plotting noise model parameters. 459 for s_plot, o_plot in zip(s_plots, o_plots): 460 s_plot.set_xlabel('ISO') 461 s_plot.set_ylabel('S') 462 463 o_plot.set_xlabel('ISO') 464 o_plot.set_ylabel('O') 465 466 for sub_plot in (s_plot, o_plot): 467 sub_plot.set_xticks(isos) 468 # No minor ticks. 469 sub_plot.xaxis.set_minor_locator(matplotlib.ticker.NullLocator()) 470 sub_plot.xaxis.set_major_formatter(matplotlib.ticker.ScalarFormatter()) 471 sub_plot.legend() 472 473 fig.suptitle('Noise model: N(x) = sqrt(Sx + O)', x=0.54, y=0.99) 474 pylab.tight_layout() 475 fig.savefig(f'{name_with_log_path}.png', dpi=_FIG_DPI) 476 477 def test_dng_noise_model_generation(self): 478 """Calibrates standard Bayer or quad Bayer noise model. 479 480 def requires 'test' in name to actually run. 481 This function: 482 * Calibrates read noise (optional). 483 * Captures stats images of different ISO values and exposure times. 484 * Measures linear fittings for each ISO value. 485 * Computes and validates overall noise model parameters. 486 * Plots noise model parameters figures. 487 * Plots stats samples, linear fittings and model fittings. 488 * Saves the read noise plot and csv data (optional). 489 * Generates noise model and noise profile code. 490 """ 491 read_noise_file_path = capture_read_noise_utils.calibrate_read_noise( 492 self.dut.serial, 493 self.camera_id, 494 self.hidden_physical_id, 495 _NAME_READ_NOISE, 496 _NAME_READ_NOISE_FILE, 497 _READ_NOISE_STEPS_PER_STOP, 498 raw_format=_READ_NOISE_RAW_FORMAT, 499 is_two_stage_model=_TWO_STAGE_MODEL, 500 ) 501 502 # Begin DNG Noise Model Calibration 503 with its_session_utils.ItsSession( 504 device_id=self.dut.serial, 505 camera_id=self.camera_id, 506 hidden_physical_id=self.hidden_physical_id) as cam: 507 props = cam.get_camera_properties() 508 props = cam.override_with_hidden_physical_camera_props(props) 509 log_path = self.log_path 510 name_with_log_path = os.path.join(log_path, _NAME) 511 logging.info('Starting %s for camera %s', _NAME, cam.get_camera_name()) 512 513 # Get basic properties we need. 514 sens_min, sens_max = props['android.sensor.info.sensitivityRange'] 515 sens_max_analog = props['android.sensor.maxAnalogSensitivity'] 516 # Maximum sensitivity for measuring noise model. 517 sens_max_meas = sens_max_analog 518 519 # Change the ISO min and/or max values if specified 520 if _ISO_MIN_VALUE is not None: 521 sens_min = _ISO_MIN_VALUE 522 if _ISO_MAX_VALUE is not None: 523 sens_max_meas = _ISO_MAX_VALUE 524 525 logging.info('Sensitivity range: [%d, %d]', sens_min, sens_max) 526 logging.info('Max analog sensitivity: %d', sens_max_analog) 527 logging.info( 528 'Sensitivity range for measurement: [%d, %d]', 529 sens_min, sens_max_meas, 530 ) 531 532 offset_a, offset_b = None, None 533 read_noise_data = None 534 if _TWO_STAGE_MODEL: 535 # Check if read noise results exist for this device and camera 536 if not os.path.exists(read_noise_file_path): 537 raise AssertionError( 538 'Read noise results file does not exist for this device. Run' 539 ' capture_read_noise_file_path script to gather read noise data' 540 ' for current sensor' 541 ) 542 543 with open(read_noise_file_path, 'rb') as f: 544 read_noise_data = pickle.load(f) 545 546 offset_a, offset_b = ( 547 capture_read_noise_utils.get_read_noise_coefficients( 548 read_noise_data, 549 sens_min, 550 sens_max_meas, 551 ) 552 ) 553 554 iso_to_stats_dict = noise_model_utils.capture_stats_images( 555 cam, 556 props, 557 _STATS_CONFIG, 558 sens_min, 559 sens_max_meas, 560 _ZOOM_RATIO, 561 _TILE_CROP_N, 562 _MAX_SIGNAL_VALUE, 563 _ISO_MULTIPLIER, 564 _BRACKET_MAX, 565 _BRACKET_FACTOR, 566 self.log_path, 567 stats_file_name=_STATS_FILE_NAME, 568 is_remove_var_outliers=_REMOVE_VAR_OUTLIERS, 569 outlier_median_abs_deviations=_OUTLIER_MEDIAN_ABS_DEVS, 570 is_debug_mode=self.debug_mode, 571 ) 572 573 measured_models, samples = noise_model_utils.measure_linear_noise_models( 574 iso_to_stats_dict, 575 _COLOR_CHANNEL_NAMES, 576 ) 577 578 noise_model = noise_model_utils.compute_noise_model( 579 samples, 580 sens_max_analog, 581 offset_a, 582 offset_b, 583 _TWO_STAGE_MODEL, 584 ) 585 586 noise_model_utils.validate_noise_model( 587 noise_model, 588 _COLOR_CHANNEL_NAMES, 589 sens_min, 590 ) 591 592 self._plot_noise_model( 593 sorted(iso_to_stats_dict.keys()), 594 measured_models, 595 noise_model, 596 sens_max_analog, 597 name_with_log_path, 598 ) 599 600 self._plot_stats_and_noise_model_fittings( 601 iso_to_stats_dict, 602 measured_models, 603 noise_model, 604 sens_max_analog, 605 name_with_log_path, 606 ) 607 608 # If 2-Stage model is enabled, save the read noise graph and csv data 609 if _TWO_STAGE_MODEL: 610 # Save the linear plot of the read noise data 611 filename = f'{pathlib.Path(_NAME_READ_NOISE_FILE).stem}.png' 612 file_path = os.path.join(log_path, filename) 613 capture_read_noise_utils.plot_read_noise_data( 614 read_noise_data, 615 sens_min, 616 sens_max_meas, 617 file_path, 618 _COLOR_CHANNEL_NAMES, 619 _PLOT_COLORS, 620 ) 621 622 # Save the data as a csv file 623 filename = f'{pathlib.Path(_NAME_READ_NOISE_FILE).stem}.csv' 624 file_path = os.path.join(log_path, filename) 625 capture_read_noise_utils.save_read_noise_data_as_csv( 626 read_noise_data, 627 sens_min, 628 sens_max_meas, 629 file_path, 630 _COLOR_CHANNEL_NAMES, 631 ) 632 633 # Generate the noise model file. 634 self._create_noise_model_and_profile_code( 635 noise_model, 636 sens_min, 637 sens_max, 638 sens_max_analog, 639 log_path, 640 ) 641 642 643if __name__ == '__main__': 644 test_runner.main() 645