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