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, name_with_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    name_with_log_path: file name & 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(_NAME)
115  pylab.xlabel('requests')
116  pylab.ylabel('RGB means')
117  pylab.legend(loc='lower right', numpoints=1, fancybox=True)
118  matplotlib.pyplot.savefig(f'{name_with_log_path}_plot_means.png')
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      name_with_log_path = os.path.join(log_path, _NAME)
148
149      # Load chart for scene
150      its_session_utils.load_scene(
151          cam, props, self.scene, self.tablet,
152          its_session_utils.CHART_DISTANCE_NO_SCALING)
153
154      # Create reqs & do caps
155      settings, reqs, out_surfaces = create_requests(cam, props, log_path)
156      raw_caps, yuv_caps = cam.do_capture(reqs, out_surfaces)
157      if not isinstance(raw_caps, list):
158        raw_caps = [raw_caps]
159      if not isinstance(yuv_caps, list):
160        yuv_caps = [yuv_caps]
161
162      # Extract data
163      raw_means = []
164      yuv_means = []
165      for i in range(len(reqs)):
166        sens, sens_boost = settings[i]
167        sens_and_boost = f's={sens:04d}_boost={sens_boost:04d}'
168        raw_file_name = f'{name_with_log_path}_raw_{sens_and_boost}.jpg'
169        raw_means.append(compute_patch_means(raw_caps[i], props, raw_file_name))
170
171        yuv_file_name = f'{name_with_log_path}_yuv_{sens_and_boost}.jpg'
172        yuv_means.append(compute_patch_means(yuv_caps[i], props, yuv_file_name))
173
174        logging.debug('s=%d, s_boost=%d: raw_means %s, yuv_means %s',
175                      sens, sens_boost, str(raw_means[-1]), str(yuv_means[-1]))
176      cap_idxs = range(len(reqs))
177
178      # Create plots
179      create_plots(cap_idxs, raw_means, yuv_means, name_with_log_path)
180
181      # RAW asserts
182      for step in range(1, len(reqs)):
183        sens_prev, _ = settings[step - 1]
184        sens, sens_boost = settings[step]
185        expected_ratio = sens_prev / sens
186        for ch, _ in enumerate(_COLORS):
187          ratio_per_step = raw_means[step-1][ch] / raw_means[step][ch]
188          logging.debug('Step: (%d, %d) %s channel: (%f, %f), ratio: %f,',
189                        step - 1, step, _COLORS[ch], raw_means[step - 1][ch],
190                        raw_means[step][ch], ratio_per_step)
191          if raw_means[step][ch] <= _RAW_PIXEL_THRESH:
192            continue
193          if not math.isclose(ratio_per_step, expected_ratio,
194                              rel_tol=_RATIO_RTOL):
195            raise AssertionError(
196                f'step: {step}, ratio: {ratio_per_step}, expected ratio: '
197                f'{expected_ratio:.3f}, RTOL: {_RATIO_RTOL}')
198
199      # YUV asserts
200      for ch, _ in enumerate(_COLORS):
201        vals = [val[ch] for val in yuv_means]
202        for idx in cap_idxs:
203          if raw_means[idx][ch] <= _RAW_PIXEL_THRESH:
204            vals = vals[:idx]
205        mean = sum(vals) / len(vals)
206        logging.debug('%s channel vals %s mean %f', _COLORS[ch], vals, mean)
207        for step in range(len(vals)):
208          ratio_mean = vals[step] / mean
209          if not math.isclose(1.0, ratio_mean, rel_tol=_RATIO_RTOL):
210            raise AssertionError(
211                f'Capture vs mean ratio: {ratio_mean}, RTOL: +/- {_RATIO_RTOL}')
212
213if __name__ == '__main__':
214  test_runner.main()
215