1# Copyright 2016 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"""Verifies post RAW sensitivity boost."""
15
16
17import logging
18import math
19import os.path
20import matplotlib
21from matplotlib import pylab
22from mobly import test_runner
23
24import its_base_test
25import camera_properties_utils
26import capture_request_utils
27import error_util
28import image_processing_utils
29import its_session_utils
30import target_exposure_utils
31
32_COLORS = ('R', 'G', 'B')
33_MAX_YUV_SIZE = (1920, 1080)
34_NAME = os.path.splitext(os.path.basename(__file__))[0]
35_PATCH_H = 0.1  # center 10%
36_PATCH_W = 0.1
37_PATCH_X = 0.5 - _PATCH_W/2
38_PATCH_Y = 0.5 - _PATCH_H/2
39_RATIO_RTOL = 0.1  # +/-10% TOL on images vs expected values
40_RAW_PIXEL_THRESH = 0.03  # Waive check if RAW [0, 1] value below this thresh
41
42
43def create_requests(cam, props, log_path):
44  """Create the requests and settings lists."""
45  w, h = capture_request_utils.get_available_output_sizes(
46      'yuv', props, _MAX_YUV_SIZE)[0]
47
48  if camera_properties_utils.raw16(props):
49    raw_format = 'raw'
50  elif camera_properties_utils.raw10(props):
51    raw_format = 'raw10'
52  elif camera_properties_utils.raw12(props):
53    raw_format = 'raw12'
54  else:  # should not reach here
55    raise error_util.Error('Cannot find available RAW output format')
56
57  out_surfaces = [{'format': raw_format},
58                  {'format': 'yuv', 'width': w, 'height': h}]
59  sens_min, sens_max = props['android.sensor.info.sensitivityRange']
60  sens_boost_min, sens_boost_max = props[
61      'android.control.postRawSensitivityBoostRange']
62  exp_target, sens_target = target_exposure_utils.get_target_exposure_combos(
63      log_path, cam)['midSensitivity']
64
65  reqs = []
66  settings = []
67  sens_boost = sens_boost_min
68  while sens_boost <= sens_boost_max:
69    sens_raw = int(round(sens_target * 100.0 / sens_boost))
70    if sens_raw < sens_min or sens_raw > sens_max:
71      break
72    req = capture_request_utils.manual_capture_request(sens_raw, exp_target)
73    req['android.control.postRawSensitivityBoost'] = sens_boost
74    reqs.append(req)
75    settings.append((sens_raw, sens_boost))
76    if sens_boost == sens_boost_max:
77      break
78    sens_boost *= 2
79    # Always try to test maximum sensitivity boost value
80    if sens_boost > sens_boost_max:
81      sens_boost = sens_boost_max
82
83  return settings, reqs, out_surfaces
84
85
86def compute_patch_means(cap, props, file_name):
87  """Compute the RGB means for center patch of capture."""
88
89  rgb_img = image_processing_utils.convert_capture_to_rgb_image(
90      cap, props=props)
91  patch = image_processing_utils.get_image_patch(
92      rgb_img, _PATCH_X, _PATCH_Y, _PATCH_W, _PATCH_H)
93  image_processing_utils.write_image(patch, file_name)
94  return image_processing_utils.compute_image_means(patch)
95
96
97def create_plots(idx, raw_means, yuv_means, log_path):
98  """Create plots from data.
99
100  Args:
101    idx: capture request indices for x-axis.
102    raw_means: array of RAW capture RGB converted means.
103    yuv_means: array of YUV capture RGB converted means.
104    log_path: path to save files.
105  """
106
107  pylab.clf()
108  for i, _ in enumerate(_COLORS):
109    pylab.plot(idx, [ch[i] for ch in yuv_means], '-'+'rgb'[i]+'s', label='YUV',
110               alpha=0.7)
111    pylab.plot(idx, [ch[i] for ch in raw_means], '-'+'rgb'[i]+'o', label='RAW',
112               alpha=0.7)
113  pylab.ylim([0, 1])
114  pylab.title('%s' % _NAME)
115  pylab.xlabel('requests')
116  pylab.ylabel('RGB means')
117  pylab.legend(loc='lower right', numpoints=1, fancybox=True)
118  matplotlib.pyplot.savefig('%s_plot_means.png' % os.path.join(log_path, _NAME))
119
120
121class PostRawSensitivityBoost(its_base_test.ItsBaseTest):
122  """Check post RAW sensitivity boost.
123
124  Captures a set of RAW/YUV images with different sensitivity/post RAW
125  sensitivity boost combination and checks if output means match req settings
126
127  RAW images should get brighter. YUV images should stay about the same.
128    asserts RAW is ~2x brighter per step
129    asserts YUV is about the same per step
130  """
131
132  def test_post_raw_sensitivity_boost(self):
133    logging.debug('Starting %s', _NAME)
134    with its_session_utils.ItsSession(
135        device_id=self.dut.serial,
136        camera_id=self.camera_id,
137        hidden_physical_id=self.hidden_physical_id) as cam:
138      props = cam.get_camera_properties()
139      props = cam.override_with_hidden_physical_camera_props(props)
140      camera_properties_utils.skip_unless(
141          camera_properties_utils.raw_output(props) and
142          camera_properties_utils.post_raw_sensitivity_boost(props) and
143          camera_properties_utils.compute_target_exposure(props) and
144          camera_properties_utils.per_frame_control(props) and
145          not camera_properties_utils.mono_camera(props))
146      log_path = self.log_path
147
148      # Load chart for scene
149      its_session_utils.load_scene(
150          cam, props, self.scene, self.tablet, self.chart_distance)
151
152      # Create reqs & do caps
153      settings, reqs, out_surfaces = create_requests(cam, props, log_path)
154      raw_caps, yuv_caps = cam.do_capture(reqs, out_surfaces)
155      if not isinstance(raw_caps, list):
156        raw_caps = [raw_caps]
157      if not isinstance(yuv_caps, list):
158        yuv_caps = [yuv_caps]
159
160      # Extract data
161      raw_means = []
162      yuv_means = []
163      for i in range(len(reqs)):
164        sens, sens_boost = settings[i]
165        raw_file_name = '%s_raw_s=%04d_boost=%04d.jpg' % (
166            os.path.join(log_path, _NAME), sens, sens_boost)
167        raw_means.append(compute_patch_means(raw_caps[i], props, raw_file_name))
168
169        yuv_file_name = '%s_yuv_s=%04d_boost=%04d.jpg' % (
170            os.path.join(log_path, _NAME), sens, sens_boost)
171        yuv_means.append(compute_patch_means(yuv_caps[i], props, yuv_file_name))
172
173        logging.debug('s=%d, s_boost=%d: raw_means %s, yuv_means %s',
174                      sens, sens_boost, str(raw_means[-1]), str(yuv_means[-1]))
175      cap_idxs = range(len(reqs))
176
177      # Create plots
178      create_plots(cap_idxs, raw_means, yuv_means, log_path)
179
180      # RAW asserts
181      for step in range(1, len(reqs)):
182        sens_prev, _ = settings[step - 1]
183        sens, sens_boost = settings[step]
184        expected_ratio = sens_prev / sens
185        for ch, _ in enumerate(_COLORS):
186          ratio_per_step = raw_means[step-1][ch] / raw_means[step][ch]
187          logging.debug('Step: (%d, %d) %s channel: (%f, %f), ratio: %f,',
188                        step - 1, step, _COLORS[ch], raw_means[step - 1][ch],
189                        raw_means[step][ch], ratio_per_step)
190          if raw_means[step][ch] <= _RAW_PIXEL_THRESH:
191            continue
192          if not math.isclose(ratio_per_step, expected_ratio,
193                              rel_tol=_RATIO_RTOL):
194            raise AssertionError(
195                f'step: {step}, ratio: {ratio_per_step}, expected ratio: '
196                f'{expected_ratio:.3f}, RTOL: {_RATIO_RTOL}')
197
198      # YUV asserts
199      for ch, _ in enumerate(_COLORS):
200        vals = [val[ch] for val in yuv_means]
201        for idx in cap_idxs:
202          if raw_means[idx][ch] <= _RAW_PIXEL_THRESH:
203            vals = vals[:idx]
204        mean = sum(vals) / len(vals)
205        logging.debug('%s channel vals %s mean %f', _COLORS[ch], vals, mean)
206        for step in range(len(vals)):
207          ratio_mean = vals[step] / mean
208          if not math.isclose(1.0, ratio_mean, rel_tol=_RATIO_RTOL):
209            raise AssertionError(
210                f'Capture vs mean ratio: {ratio_mean}, RTOL: +/- {_RATIO_RTOL}')
211
212if __name__ == '__main__':
213  test_runner.main()
214