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