• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Copyright 2018 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 test for tonemap curve with sensor test pattern."""
15
16import logging
17import os
18
19from mobly import test_runner
20import numpy as np
21
22import its_base_test
23import camera_properties_utils
24import capture_request_utils
25import image_processing_utils
26import its_session_utils
27
28
29_NAME = os.path.basename(__file__).split('.')[0]
30_COLOR_BAR_PATTERN = 2  # Note scene0/test_test_patterns must PASS
31_COLOR_BARS = ['WHITE', 'YELLOW', 'CYAN', 'GREEN', 'MAGENTA', 'RED',
32               'BLUE', 'BLACK']
33_N_BARS = len(_COLOR_BARS)
34_COLOR_CHECKER = {'BLACK': [0, 0, 0], 'RED': [1, 0, 0], 'GREEN': [0, 1, 0],
35                  'BLUE': [0, 0, 1], 'MAGENTA': [1, 0, 1], 'CYAN': [0, 1, 1],
36                  'YELLOW': [1, 1, 0], 'WHITE': [1, 1, 1]}
37_DELTA = 0.005  # crop on each edge of color bars
38_RAW_TOL = 0.001  # 1 DN in [0:1] (1/(1023-64)
39_RGB_VAR_TOL = 0.0039  # 1/255
40_RGB_MEAN_TOL = 0.1
41_TONEMAP_MAX = 0.5
42_YUV_H = 480
43_YUV_W = 640
44# Normalized co-ordinates for the color bar patch.
45_Y_NORM = 0.0
46_W_NORM = 1.0 / _N_BARS - 2 * _DELTA
47_H_NORM = 1.0
48
49# Linear tonemap with maximum of 0.5
50_LINEAR_TONEMAP = sum([[i/63.0, i/126.0] for i in range(64)], [])
51
52
53def get_yuv_patch_coordinates(num, w_orig, w_crop):
54  """Returns the normalized x co-ordinate for the title.
55
56  Args:
57   num: int; position on color in the color bar.
58   w_orig: float; original RAW image W
59   w_crop: float; cropped RAW image W
60
61  Returns:
62    normalized x, w values for color patch.
63  """
64  if w_crop == w_orig:  # uncropped image
65    x_norm = num / _N_BARS + _DELTA
66    w_norm = 1 / _N_BARS - 2 * _DELTA
67    logging.debug('x_norm: %.5f, w_norm: %.5f', x_norm, w_norm)
68  elif w_crop < w_orig:  # adjust patch width to match vertical RAW crop
69    w_delta_edge = (w_orig - w_crop) / 2
70    w_bar_orig = w_orig / _N_BARS
71    if num == 0:  # left-most bar
72      x_norm = _DELTA
73      w_norm = (w_bar_orig - w_delta_edge) / w_crop - 2 * _DELTA
74    elif num == _N_BARS:  # right-most bar
75      x_norm = (w_bar_orig * num - w_delta_edge) / w_crop + _DELTA
76      w_norm = (w_bar_orig - w_delta_edge) / w_crop - 2 * _DELTA
77    else:  # middle bars
78      x_norm = (w_bar_orig * num - w_delta_edge) / w_crop + _DELTA
79      w_norm = w_bar_orig / w_crop - 2 * _DELTA
80    logging.debug('x_norm: %.5f, w_norm: %.5f (crop-corrected)', x_norm, w_norm)
81  else:
82    raise AssertionError('Cropped image is larger than original!')
83  return x_norm, w_norm
84
85
86def get_x_norm(num):
87  """Returns the normalized x co-ordinate for the title.
88
89  Args:
90   num: int; position on color in the color bar.
91
92  Returns:
93    normalized x co-ordinate.
94  """
95  return float(num) / _N_BARS + _DELTA
96
97
98def check_raw_pattern(img_raw):
99  """Checks for RAW capture matches color bar pattern.
100
101  Args:
102    img_raw: RAW image
103  """
104  logging.debug('Checking RAW/PATTERN match')
105  color_match = []
106  for n in range(_N_BARS):
107    x_norm = get_x_norm(n)
108    raw_patch = image_processing_utils.get_image_patch(img_raw, x_norm, _Y_NORM,
109                                                       _W_NORM, _H_NORM)
110    raw_means = image_processing_utils.compute_image_means(raw_patch)
111    logging.debug('patch: %d, x_norm: %.3f, RAW means: %s',
112                  n, x_norm, str(raw_means))
113    for color in _COLOR_BARS:
114      if np.allclose(_COLOR_CHECKER[color], raw_means, atol=_RAW_TOL):
115        color_match.append(color)
116        logging.debug('%s match', color)
117        break
118      else:
119        logging.debug('No match w/ %s: %s, ATOL: %.3f',
120                      color, str(_COLOR_CHECKER[color]), _RAW_TOL)
121  if set(color_match) != set(_COLOR_BARS):
122    raise AssertionError(
123        'RAW _COLOR_BARS test pattern does not have all colors')
124
125
126def check_yuv_vs_raw(img_raw, img_yuv, name_with_log_path, debug):
127  """Checks for YUV vs RAW match in 8 patches.
128
129  Check for correct values and color consistency
130
131  Args:
132    img_raw: RAW image
133    img_yuv: YUV image
134    name_with_log_path: string for test name with path
135    debug: boolean to log additional information
136  """
137  logging.debug('Checking YUV/RAW match')
138  raw_w = img_raw.shape[1]
139  raw_h = img_raw.shape[0]
140  raw_aspect_ratio = raw_w/raw_h
141  yuv_aspect_ratio = _YUV_W/_YUV_H
142  logging.debug('raw_img: W, H, AR: %d, %d, %.3f',
143                raw_w, raw_h, raw_aspect_ratio)
144
145  # Crop RAW to match YUV 4:3 format
146  raw_w_cropped = raw_w
147  if raw_aspect_ratio > yuv_aspect_ratio:  # vertical crop sensor
148    logging.debug('Cropping RAW to match YUV aspect ratio.')
149    w_norm_raw = yuv_aspect_ratio / raw_aspect_ratio
150    x_norm_raw = (1 - w_norm_raw) / 2
151    img_raw = image_processing_utils.get_image_patch(
152        img_raw, x_norm_raw, 0, w_norm_raw, 1)
153    raw_w_cropped = img_raw.shape[1]
154    logging.debug('New RAW W, H: %d, %d', raw_w_cropped, img_raw.shape[0])
155    image_processing_utils.write_image(
156        img_raw, f'{name_with_log_path}_raw_cropped_COLOR_BARS.jpg', True)
157
158  # Compare YUV and RAW color patches
159  color_match_errs = []
160  color_variance_errs = []
161  for n in range(_N_BARS):
162    x_norm, w_norm = get_yuv_patch_coordinates(n, raw_w, raw_w_cropped)
163    raw_patch = image_processing_utils.get_image_patch(img_raw, x_norm, _Y_NORM,
164                                                       w_norm, _H_NORM)
165    yuv_patch = image_processing_utils.get_image_patch(img_yuv, x_norm, _Y_NORM,
166                                                       w_norm, _H_NORM)
167    if debug:
168      image_processing_utils.write_image(
169          raw_patch, f'{name_with_log_path}_raw_patch_{n}.jpg', True)
170      image_processing_utils.write_image(
171          yuv_patch, f'{name_with_log_path}_yuv_patch_{n}.jpg', True)
172    raw_means = np.array(image_processing_utils.compute_image_means(raw_patch))
173    raw_vars = np.array(
174        image_processing_utils.compute_image_variances(raw_patch))
175    yuv_means = np.array(image_processing_utils.compute_image_means(yuv_patch))
176    yuv_means /= _TONEMAP_MAX  # Normalize to tonemap max
177    yuv_vars = np.array(
178        image_processing_utils.compute_image_variances(yuv_patch))
179    if not np.allclose(raw_means, yuv_means, atol=_RGB_MEAN_TOL):
180      color_match_errs.append(
181          f'means RAW: {raw_means}, RGB(norm): {np.round(yuv_means, 3)}, '
182          f'ATOL: {_RGB_MEAN_TOL}')
183    if not np.allclose(raw_vars, yuv_vars, atol=_RGB_VAR_TOL):
184      color_variance_errs.append(
185          f'variances RAW: {raw_vars}, RGB: {yuv_vars}, '
186          f'ATOL: {_RGB_VAR_TOL}')
187
188  # Print all errors before assertion
189  if color_match_errs:
190    for err in color_match_errs:
191      logging.debug(err)
192    for err in color_variance_errs:
193      logging.error(err)
194    raise AssertionError('Color match errors. See test_log.DEBUG')
195  if color_variance_errs:
196    for err in color_variance_errs:
197      logging.error(err)
198    raise AssertionError('Color variance errors. See test_log.DEBUG')
199
200
201def test_tonemap_curve_impl(name_with_log_path, cam, props, debug):
202  """Test tonemap curve with sensor test pattern.
203
204  Args:
205   name_with_log_path: Path to save the captured image.
206   cam: An open device session.
207   props: Properties of cam.
208   debug: boolean for debug mode
209  """
210
211  avail_patterns = props['android.sensor.availableTestPatternModes']
212  logging.debug('Available Patterns: %s', avail_patterns)
213  sens_min, _ = props['android.sensor.info.sensitivityRange']
214  min_exposure = min(props['android.sensor.info.exposureTimeRange'])
215
216  # RAW image
217  req_raw = capture_request_utils.manual_capture_request(
218      int(sens_min), min_exposure)
219  req_raw['android.sensor.testPatternMode'] = _COLOR_BAR_PATTERN
220  fmt_raw = {'format': 'raw'}
221  cap_raw = cam.do_capture(req_raw, fmt_raw)
222  img_raw = image_processing_utils.convert_capture_to_rgb_image(
223      cap_raw, props=props)
224
225  # Save RAW pattern
226  image_processing_utils.write_image(
227      img_raw, f'{name_with_log_path}_raw_COLOR_BARS.jpg', True)
228
229  # Check pattern for correctness
230  check_raw_pattern(img_raw)
231
232  # YUV image
233  req_yuv = capture_request_utils.manual_capture_request(
234      int(sens_min), min_exposure)
235  req_yuv['android.sensor.testPatternMode'] = _COLOR_BAR_PATTERN
236  req_yuv['android.distortionCorrection.mode'] = 0
237  req_yuv['android.tonemap.mode'] = 0
238  req_yuv['android.tonemap.curve'] = {
239      'red': _LINEAR_TONEMAP,
240      'green': _LINEAR_TONEMAP,
241      'blue': _LINEAR_TONEMAP
242  }
243  fmt_yuv = {'format': 'yuv', 'width': _YUV_W, 'height': _YUV_H}
244  cap_yuv = cam.do_capture(req_yuv, fmt_yuv)
245  img_yuv = image_processing_utils.convert_capture_to_rgb_image(cap_yuv, True)
246
247  # Save YUV pattern
248  image_processing_utils.write_image(
249      img_yuv, f'{name_with_log_path}_yuv_COLOR_BARS.jpg', True)
250
251  # Check pattern for correctness
252  check_yuv_vs_raw(img_raw, img_yuv, name_with_log_path, debug)
253
254
255class TonemapCurveTest(its_base_test.ItsBaseTest):
256  """Test conversion of test pattern from RAW to YUV with linear tonemap.
257
258  Test makes use of android.sensor.testPatternMode 2 (_COLOR_BARS).
259  """
260
261  def test_tonemap_curve(self):
262    logging.debug('Starting %s', _NAME)
263    name_with_log_path = os.path.join(self.log_path, _NAME)
264    with its_session_utils.ItsSession(
265        device_id=self.dut.serial,
266        camera_id=self.camera_id,
267        hidden_physical_id=self.hidden_physical_id) as cam:
268      props = cam.get_camera_properties()
269      camera_properties_utils.skip_unless(
270          camera_properties_utils.raw16(props) and
271          camera_properties_utils.manual_sensor(props) and
272          camera_properties_utils.per_frame_control(props) and
273          camera_properties_utils.manual_post_proc(props) and
274          camera_properties_utils.color_bars_test_pattern(props))
275
276      test_tonemap_curve_impl(name_with_log_path, cam, props, self.debug_mode)
277
278
279if __name__ == '__main__':
280  test_runner.main()
281