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
29NAME = os.path.basename(__file__).split('.')[0]
30COLOR_BAR_PATTERN = 2  # Note scene0/test_test_patterns must PASS
31COLOR_BARS = ['WHITE', 'YELLOW', 'CYAN', 'GREEN', 'MAGENTA', 'RED',
32              'BLUE', 'BLACK']
33N_BARS = len(COLOR_BARS)
34COLOR_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]}
37DELTA = 0.0005  # crop on edge of color bars
38RAW_TOL = 0.001  # 1 DN in [0:1] (1/(1023-64)
39RGB_VAR_TOL = 0.0039  # 1/255
40RGB_MEAN_TOL = 0.1
41TONEMAP_MAX = 0.5
42YUV_H = 480
43YUV_W = 640
44# Normalized co-ordinates for the color bar patch.
45Y_NORM = 0.0
46W_NORM = 1.0 / N_BARS - 2 * DELTA
47H_NORM = 1.0
48
49# Linear tonemap with maximum of 0.5
50LINEAR_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:  # adject 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('RAW COLOR_BARS test pattern does not have all colors')
123
124
125def check_yuv_vs_raw(img_raw, img_yuv, name, debug):
126  """Checks for YUV vs RAW match in 8 patches.
127
128  Check for correct values and color consistency
129
130  Args:
131    img_raw: RAW image
132    img_yuv: YUV image
133    name: string for test name with path
134    debug: boolean to log additional information
135  """
136  logging.debug('Checking YUV/RAW match')
137  raw_w = img_raw.shape[1]
138  raw_h = img_raw.shape[0]
139  raw_aspect_ratio = raw_w/raw_h
140  yuv_aspect_ratio = YUV_W/YUV_H
141  logging.debug('raw_img: W, H, AR: %d, %d, %.3f',
142                raw_w, raw_h, raw_aspect_ratio)
143
144  # Crop RAW to match YUV 4:3 format
145  raw_w_cropped = raw_w
146  if raw_aspect_ratio > yuv_aspect_ratio:  # vertical crop sensor
147    logging.debug('Cropping RAW to match YUV aspect ratio.')
148    w_norm_raw = yuv_aspect_ratio / raw_aspect_ratio
149    x_norm_raw = (1 - w_norm_raw) / 2
150    img_raw = image_processing_utils.get_image_patch(
151        img_raw, x_norm_raw, 0, w_norm_raw, 1)
152    raw_w_cropped = img_raw.shape[1]
153    logging.debug('New RAW W, H: %d, %d', raw_w_cropped, img_raw.shape[0])
154    image_processing_utils.write_image(
155        img_raw, f'{name}_raw_cropped_COLOR_BARS.jpg', True)
156
157  # Compare YUV and RAW color patches
158  color_match_errs = []
159  color_variance_errs = []
160  for n in range(N_BARS):
161    x_norm, w_norm = get_yuv_patch_coordinates(n, raw_w, raw_w_cropped)
162    logging.debug('x_norm: %.3f', x_norm)
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}_raw_patch_{n}.jpg', True)
170      image_processing_utils.write_image(
171          yuv_patch, f'{name}_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          'means RAW: %s, RGB(norm): %s, ATOL: %.2f' %
182          (str(raw_means), str(np.round(yuv_means, 3)), RGB_MEAN_TOL))
183    if not np.allclose(raw_vars, yuv_vars, atol=RGB_VAR_TOL):
184      color_variance_errs.append('variances RAW: %s, RGB: %s, ATOL: %.4f' %
185                                 (str(raw_vars), str(yuv_vars), RGB_VAR_TOL))
186
187  # Print all errors before assertion
188  if color_match_errs:
189    for err in color_match_errs:
190      logging.debug(err)
191    for err in color_variance_errs:
192      logging.error(err)
193    raise AssertionError('Color match errors. See test_log.DEBUG')
194  if color_variance_errs:
195    for err in color_variance_errs:
196      logging.error(err)
197    raise AssertionError('Color variance errors. See test_log.DEBUG')
198
199
200def test_tonemap_curve_impl(name, cam, props, debug):
201  """Test tonemap curve with sensor test pattern.
202
203  Args:
204   name: Path to save the captured image.
205   cam: An open device session.
206   props: Properties of cam.
207   debug: boolean for debug mode
208  """
209
210  avail_patterns = props['android.sensor.availableTestPatternModes']
211  logging.debug('Available Patterns: %s', avail_patterns)
212  sens_min, _ = props['android.sensor.info.sensitivityRange']
213  min_exposure = min(props['android.sensor.info.exposureTimeRange'])
214
215  # RAW image
216  req_raw = capture_request_utils.manual_capture_request(
217      int(sens_min), min_exposure)
218  req_raw['android.sensor.testPatternMode'] = COLOR_BAR_PATTERN
219  fmt_raw = {'format': 'raw'}
220  cap_raw = cam.do_capture(req_raw, fmt_raw)
221  img_raw = image_processing_utils.convert_capture_to_rgb_image(
222      cap_raw, props=props)
223
224  # Save RAW pattern
225  image_processing_utils.write_image(
226      img_raw, f'{name}_raw_COLOR_BARS.jpg', True)
227
228  # Check pattern for correctness
229  check_raw_pattern(img_raw)
230
231  # YUV image
232  req_yuv = capture_request_utils.manual_capture_request(
233      int(sens_min), min_exposure)
234  req_yuv['android.sensor.testPatternMode'] = COLOR_BAR_PATTERN
235  req_yuv['android.distortionCorrection.mode'] = 0
236  req_yuv['android.tonemap.mode'] = 0
237  req_yuv['android.tonemap.curve'] = {
238      'red': LINEAR_TONEMAP,
239      'green': LINEAR_TONEMAP,
240      'blue': LINEAR_TONEMAP
241  }
242  fmt_yuv = {'format': 'yuv', 'width': YUV_W, 'height': YUV_H}
243  cap_yuv = cam.do_capture(req_yuv, fmt_yuv)
244  img_yuv = image_processing_utils.convert_capture_to_rgb_image(cap_yuv, True)
245
246  # Save YUV pattern
247  image_processing_utils.write_image(
248      img_yuv, f'{name}_yuv_COLOR_BARS.jpg', True)
249
250  # Check pattern for correctness
251  check_yuv_vs_raw(img_raw, img_yuv, name, debug)
252
253
254class TonemapCurveTest(its_base_test.ItsBaseTest):
255  """Test conversion of test pattern from RAW to YUV with linear tonemap.
256
257  Test makes use of android.sensor.testPatternMode 2 (COLOR_BARS).
258  """
259
260  def test_tonemap_curve(self):
261    logging.debug('Starting %s', NAME)
262    name = os.path.join(self.log_path, NAME)
263    with its_session_utils.ItsSession(
264        device_id=self.dut.serial,
265        camera_id=self.camera_id,
266        hidden_physical_id=self.hidden_physical_id) as cam:
267      props = cam.get_camera_properties()
268      camera_properties_utils.skip_unless(
269          camera_properties_utils.raw16(props) and
270          camera_properties_utils.manual_sensor(props) and
271          camera_properties_utils.per_frame_control(props) and
272          camera_properties_utils.manual_post_proc(props) and
273          camera_properties_utils.color_bars_test_pattern(props))
274
275      test_tonemap_curve_impl(name, cam, props, self.debug_mode)
276
277
278if __name__ == '__main__':
279  test_runner.main()
280