• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Copyright 2013 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 correct exposure control."""
15
16
17import logging
18import os.path
19
20from matplotlib import pyplot as plt
21from mobly import test_runner
22import numpy as np
23
24import its_base_test
25import camera_properties_utils
26import capture_request_utils
27import image_processing_utils
28import its_session_utils
29import target_exposure_utils
30
31_EXP_CORRECTION_FACTOR = 2  # mult or div factor to correct brightness
32_NAME = os.path.splitext(os.path.basename(__file__))[0]
33_NUM_PTS_2X_GAIN = 3  # 3 points every 2x increase in gain
34_PATCH_H = 0.1  # center 10% patch params
35_PATCH_W = 0.1
36_PATCH_X = 0.5 - _PATCH_W/2
37_PATCH_Y = 0.5 - _PATCH_H/2
38_RAW_STATS_GRID = 9  # define 9x9 (11.11%) spacing grid for rawStats processing
39_RAW_STATS_XY = _RAW_STATS_GRID//2  # define X, Y location for center rawStats
40_THRESH_MIN_LEVEL = 0.1
41_THRESH_MAX_LEVEL = 0.9
42_THRESH_MAX_LEVEL_DIFF = 0.045
43_THRESH_MAX_LEVEL_DIFF_WIDE_RANGE = 0.06
44_THRESH_MAX_OUTLIER_DIFF = 0.1
45_THRESH_ROUND_DOWN_ISO = 0.04
46_THRESH_ROUND_DOWN_EXP = 0.03
47_THRESH_ROUND_DOWN_EXP0 = 1.00  # RTOL @0ms exp; theoretical limit @ 4-line exp
48_THRESH_EXP_KNEE = 6E6  # exposures less than knee have relaxed tol
49_WIDE_EXP_RANGE_THRESH = 64.0  # threshold for 'wide' range sensor
50
51
52def adjust_exp_for_brightness(
53    cam, props, fmt, exp, iso, sync_latency, test_name_with_path):
54  """Take an image and adjust exposure and sensitivity.
55
56  Args:
57    cam: camera object
58    props: camera properties dict
59    fmt: capture format
60    exp: exposure time (ns)
61    iso: sensitivity
62    sync_latency: number for sync latency
63    test_name_with_path: path for saved files
64
65  Returns:
66    adjusted exposure
67  """
68  req = capture_request_utils.manual_capture_request(
69      iso, exp, 0.0, True, props)
70  cap = its_session_utils.do_capture_with_latency(
71      cam, req, sync_latency, fmt)
72  img = image_processing_utils.convert_capture_to_rgb_image(cap)
73  image_processing_utils.write_image(
74      img, f'{test_name_with_path}.jpg')
75  patch = image_processing_utils.get_image_patch(
76      img, _PATCH_X, _PATCH_Y, _PATCH_W, _PATCH_H)
77  r, g, b = image_processing_utils.compute_image_means(patch)
78  logging.debug('Sample RGB values: %.3f, %.3f, %.3f', r, g, b)
79  if g < _THRESH_MIN_LEVEL:
80    exp *= _EXP_CORRECTION_FACTOR
81    logging.debug('exp increased by %dx: %d', _EXP_CORRECTION_FACTOR, exp)
82  elif g > _THRESH_MAX_LEVEL:
83    exp //= _EXP_CORRECTION_FACTOR
84    logging.debug('exp decreased to 1/%dx: %d', _EXP_CORRECTION_FACTOR, exp)
85  return exp
86
87
88def plot_rgb_means(title, x, r, g, b, test_name_with_path):
89  """Plot the RGB mean data.
90
91  Args:
92    title: string for figure title
93    x: x values for plot, gain multiplier
94    r: r plane means
95    g: g plane means
96    b: b plane menas
97    test_name_with_path: path for saved files
98  """
99  plt.figure(title)
100  plt.semilogx(x, r, 'ro-')
101  plt.semilogx(x, g, 'go-')
102  plt.semilogx(x, b, 'bo-')
103  plt.title(f'{_NAME} {title}')
104  plt.xlabel('Gain Multiplier')
105  plt.ylabel('Normalized RGB Plane Avg')
106  plt.minorticks_off()
107  plt.xticks(x[0::_NUM_PTS_2X_GAIN], x[0::_NUM_PTS_2X_GAIN])
108  plt.ylim([0, 1])
109  plt.savefig(f'{test_name_with_path}_plot_rgb_means.png')
110
111
112def plot_raw_means(title, x, r, gr, gb, b, test_name_with_path):
113  """Plot the RAW mean data.
114
115  Args:
116    title: string for figure title
117    x: x values for plot, gain multiplier
118    r: R plane means
119    gr: Gr plane means
120    gb: Gb plane means
121    b: B plane menas
122    test_name_with_path: path for saved files
123  """
124  plt.figure(title)
125  plt.semilogx(x, r, 'ro-', label='R')
126  plt.semilogx(x, gr, 'go-', label='Gr')
127  plt.semilogx(x, gb, 'ko-', label='Gb')
128  plt.semilogx(x, b, 'bo-', label='B')
129  plt.title(f'{_NAME} {title}')
130  plt.xlabel('Gain Multiplier')
131  plt.ylabel('Normalized RAW Plane Avg')
132  plt.minorticks_off()
133  plt.xticks(x[0::_NUM_PTS_2X_GAIN], x[0::_NUM_PTS_2X_GAIN])
134  plt.ylim([0, 1])
135  plt.legend(numpoints=1)
136  plot_name = f'{test_name_with_path}_plot_raw_means.png'
137  plt.savefig(plot_name)
138
139
140def check_line_fit(color, mults, values, thresh_max_level_diff):
141  """Find line fit and check values.
142
143  Check for linearity. Verify sample pixel mean values are close to each
144  other. Also ensure that the images aren't clamped to 0 or 1
145  (which would also make them look like flat lines).
146
147  Args:
148    color: string to define RGB or RAW channel
149    mults: list of multiplication values for gain*m, exp/m
150    values: mean values for chan
151    thresh_max_level_diff: threshold for max difference
152  """
153
154  m, b = np.polyfit(mults, values, 1).tolist()
155  min_val = min(values)
156  max_val = max(values)
157  max_diff = max_val - min_val
158  logging.debug('Channel %s line fit (y = mx+b): m = %f, b = %f', color, m, b)
159  logging.debug('Channel min %f max %f diff %f', min_val, max_val, max_diff)
160  if max_diff >= thresh_max_level_diff:
161    raise AssertionError(f'max_diff: {max_diff:.4f}, '
162                         f'THRESH: {thresh_max_level_diff:.3f}')
163  if not _THRESH_MAX_LEVEL > b > _THRESH_MIN_LEVEL:
164    raise AssertionError(f'b: {b:.2f}, THRESH_MIN: {_THRESH_MIN_LEVEL}, '
165                         f'THRESH_MAX: {_THRESH_MAX_LEVEL}')
166  for v in values:
167    if not _THRESH_MAX_LEVEL > v > _THRESH_MIN_LEVEL:
168      raise AssertionError(f'v: {v:.2f}, THRESH_MIN: {_THRESH_MIN_LEVEL}, '
169                           f'THRESH_MAX: {_THRESH_MAX_LEVEL}')
170
171    if abs(v - b) >= _THRESH_MAX_OUTLIER_DIFF:
172      raise AssertionError(f'v: {v:.2f}, b: {b:.2f}, '
173                           f'THRESH_DIFF: {_THRESH_MAX_OUTLIER_DIFF}')
174
175
176def get_raw_active_array_size(props):
177  """Return the active array w, h from props."""
178  aaw = (props['android.sensor.info.preCorrectionActiveArraySize']['right'] -
179         props['android.sensor.info.preCorrectionActiveArraySize']['left'])
180  aah = (props['android.sensor.info.preCorrectionActiveArraySize']['bottom'] -
181         props['android.sensor.info.preCorrectionActiveArraySize']['top'])
182  return aaw, aah
183
184
185class ExposureXIsoTest(its_base_test.ItsBaseTest):
186  """Test that a constant brightness is seen as ISO and exposure time vary.
187
188  Take a series of shots that have ISO and exposure time chosen to balance
189  each other; result should be the same brightness, but over the sequence
190  the images should get noisier.
191  """
192
193  def test_exposure_x_iso(self):
194    mults = []
195    r_means = []
196    g_means = []
197    b_means = []
198    raw_r_means = []
199    raw_gr_means = []
200    raw_gb_means = []
201    raw_b_means = []
202    thresh_max_level_diff = _THRESH_MAX_LEVEL_DIFF
203
204    with its_session_utils.ItsSession(
205        device_id=self.dut.serial,
206        camera_id=self.camera_id,
207        hidden_physical_id=self.hidden_physical_id) as cam:
208      props = cam.get_camera_properties()
209      props = cam.override_with_hidden_physical_camera_props(props)
210      test_name_with_path = os.path.join(self.log_path, _NAME)
211
212      # Check SKIP conditions
213      camera_properties_utils.skip_unless(
214          camera_properties_utils.compute_target_exposure(props))
215
216      # Load chart for scene
217      its_session_utils.load_scene(
218          cam, props, self.scene, self.tablet,
219          its_session_utils.CHART_DISTANCE_NO_SCALING)
220
221      # Initialize params for requests
222      debug = self.debug_mode
223      raw_avlb = (camera_properties_utils.raw16(props) and
224                  camera_properties_utils.manual_sensor(props))
225      sync_latency = camera_properties_utils.sync_latency(props)
226      logging.debug('sync latency: %d frames', sync_latency)
227      largest_yuv = capture_request_utils.get_largest_format('yuv', props)
228      match_ar = (largest_yuv['width'], largest_yuv['height'])
229      fmt = capture_request_utils.get_near_vga_yuv_format(
230          props, match_ar=match_ar)
231      e, s = target_exposure_utils.get_target_exposure_combos(
232          self.log_path, cam)['minSensitivity']
233
234      # Take a shot and adjust parameters for brightness
235      logging.debug('Target exposure combo values. exp: %d, iso: %d',
236                    e, s)
237      e = adjust_exp_for_brightness(
238          cam, props, fmt, e, s, sync_latency, test_name_with_path)
239
240      # Initialize values to define test range
241      s_e_product = s * e
242      expt_range = props['android.sensor.info.exposureTimeRange']
243      sens_range = props['android.sensor.info.sensitivityRange']
244      m = 1.0
245
246      # Do captures with a range of exposures, but constant s*e
247      while s*m < sens_range[1] and e/m > expt_range[0]:
248        mults.append(m)
249        s_req = round(s * m)
250        e_req = s_e_product // s_req
251        logging.debug('Testing s: %d, e: %dns', s_req, e_req)
252        req = capture_request_utils.manual_capture_request(
253            s_req, e_req, 0.0, True, props)
254        cap = its_session_utils.do_capture_with_latency(
255            cam, req, sync_latency, fmt)
256        s_res = cap['metadata']['android.sensor.sensitivity']
257        e_res = cap['metadata']['android.sensor.exposureTime']
258        # determine exposure tolerance based on exposure time
259        if e_req >= _THRESH_EXP_KNEE:
260          thresh_round_down_exp = _THRESH_ROUND_DOWN_EXP
261        else:
262          thresh_round_down_exp = (
263              _THRESH_ROUND_DOWN_EXP +
264              (_THRESH_ROUND_DOWN_EXP0 - _THRESH_ROUND_DOWN_EXP) *
265              (_THRESH_EXP_KNEE - e_req) / _THRESH_EXP_KNEE)
266        if not 0 <= s_req - s_res < s_req * _THRESH_ROUND_DOWN_ISO:
267          raise AssertionError(f's_req: {s_req}, s_res: {s_res}, '
268                               f'RTOL=-{_THRESH_ROUND_DOWN_ISO*100}%')
269        if not 0 <= e_req - e_res < e_req * thresh_round_down_exp:
270          raise AssertionError(f'e_req: {e_req}ns, e_res: {e_res}ns, '
271                               f'RTOL=-{thresh_round_down_exp*100}%')
272        s_e_product_res = s_res * e_res
273        req_res_ratio = s_e_product / s_e_product_res
274        logging.debug('Capture result s: %d, e: %dns', s_res, e_res)
275        img = image_processing_utils.convert_capture_to_rgb_image(cap)
276        image_processing_utils.write_image(
277            img, f'{test_name_with_path}_mult={m:.2f}.jpg')
278        patch = image_processing_utils.get_image_patch(
279            img, _PATCH_X, _PATCH_Y, _PATCH_W, _PATCH_H)
280        rgb_means = image_processing_utils.compute_image_means(patch)
281
282        # Adjust for the difference between request and result
283        r_means.append(rgb_means[0] * req_res_ratio)
284        g_means.append(rgb_means[1] * req_res_ratio)
285        b_means.append(rgb_means[2] * req_res_ratio)
286
287        # Do with RAW_STATS space if debug
288        if raw_avlb and debug:
289          aaw, aah = get_raw_active_array_size(props)
290          fmt_raw = {'format': 'rawStats',
291                     'gridWidth': aaw//_RAW_STATS_GRID,
292                     'gridHeight': aah//_RAW_STATS_GRID}
293          raw_cap = its_session_utils.do_capture_with_latency(
294              cam, req, sync_latency, fmt_raw)
295          r, gr, gb, b = image_processing_utils.convert_capture_to_planes(
296              raw_cap, props)
297          raw_r_means.append(r[_RAW_STATS_XY, _RAW_STATS_XY] * req_res_ratio)
298          raw_gr_means.append(gr[_RAW_STATS_XY, _RAW_STATS_XY] * req_res_ratio)
299          raw_gb_means.append(gb[_RAW_STATS_XY, _RAW_STATS_XY] * req_res_ratio)
300          raw_b_means.append(b[_RAW_STATS_XY, _RAW_STATS_XY] * req_res_ratio)
301
302          # Test number of points per 2x gain
303        m *= pow(2, 1.0/_NUM_PTS_2X_GAIN)
304
305      # Loosen threshold for devices with wider exposure range
306      if m >= _WIDE_EXP_RANGE_THRESH:
307        thresh_max_level_diff = _THRESH_MAX_LEVEL_DIFF_WIDE_RANGE
308
309    # Draw plots and check data
310    if raw_avlb and debug:
311      plot_raw_means('RAW data', mults, raw_r_means, raw_gr_means, raw_gb_means,
312                     raw_b_means, test_name_with_path)
313      for ch, color in enumerate(['R', 'Gr', 'Gb', 'B']):
314        values = [raw_r_means, raw_gr_means, raw_gb_means, raw_b_means][ch]
315        check_line_fit(color, mults, values, thresh_max_level_diff)
316
317    plot_rgb_means(f'RGB (1x: iso={s}, exp={e})', mults,
318                   r_means, g_means, b_means, test_name_with_path)
319    for ch, color in enumerate(['R', 'G', 'B']):
320      values = [r_means, g_means, b_means][ch]
321      check_line_fit(color, mults, values, thresh_max_level_diff)
322
323if __name__ == '__main__':
324  test_runner.main()
325