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_ATOL = 0.001 # 1 DN in [0:1] (1/(1023-64) 39_RGB_VAR_ATOL = 0.0039 # 1/255 40_RGB_MEAN_ATOL = 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_ATOL): 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_ATOL) 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): 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 """ 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_with_log_path}_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 raw_patch = image_processing_utils.get_image_patch(img_raw, x_norm, _Y_NORM, 163 w_norm, _H_NORM) 164 yuv_patch = image_processing_utils.get_image_patch(img_yuv, x_norm, _Y_NORM, 165 w_norm, _H_NORM) 166 raw_means = np.array(image_processing_utils.compute_image_means(raw_patch)) 167 raw_vars = np.array( 168 image_processing_utils.compute_image_variances(raw_patch)) 169 yuv_means = np.array(image_processing_utils.compute_image_means(yuv_patch)) 170 yuv_means /= _TONEMAP_MAX # Normalize to tonemap max 171 yuv_vars = np.array( 172 image_processing_utils.compute_image_variances(yuv_patch)) 173 if not np.allclose(raw_means, yuv_means, atol=_RGB_MEAN_ATOL): 174 color_match_errs.append( 175 f'means RAW: {raw_means}, RGB(norm): {np.round(yuv_means, 3)}, ' 176 f'ATOL: {_RGB_MEAN_ATOL}') 177 image_processing_utils.write_image( 178 raw_patch, f'{name_with_log_path}_match_error_raw_{n}.jpg', 179 apply_gamma=True) 180 image_processing_utils.write_image( 181 yuv_patch, f'{name_with_log_path}_match_error_yuv_{n}.jpg', 182 apply_gamma=True) 183 if not np.allclose(raw_vars, yuv_vars, atol=_RGB_VAR_ATOL): 184 color_variance_errs.append( 185 f'variances RAW: {raw_vars}, RGB: {yuv_vars}, ' 186 f'ATOL: {_RGB_VAR_ATOL}') 187 image_processing_utils.write_image( 188 raw_patch, f'{name_with_log_path}_variance_error_raw_{n}.jpg', 189 apply_gamma=True) 190 image_processing_utils.write_image( 191 yuv_patch, f'{name_with_log_path}_variance_error_yuv_{n}.jpg', 192 apply_gamma=True) 193 194 # Print all errors before assertion 195 if color_match_errs: 196 for err in color_match_errs: 197 logging.debug(err) 198 for err in color_variance_errs: 199 logging.error(err) 200 raise AssertionError('Color match errors. See test_log.DEBUG') 201 if color_variance_errs: 202 for err in color_variance_errs: 203 logging.error(err) 204 raise AssertionError('Color variance errors. See test_log.DEBUG') 205 206 207def test_tonemap_curve_impl(name_with_log_path, cam, props): 208 """Test tonemap curve with sensor test pattern. 209 210 Args: 211 name_with_log_path: Path to save the captured image. 212 cam: An open device session. 213 props: Properties of cam. 214 """ 215 216 avail_patterns = props['android.sensor.availableTestPatternModes'] 217 logging.debug('Available Patterns: %s', avail_patterns) 218 sens_min, _ = props['android.sensor.info.sensitivityRange'] 219 min_exposure = min(props['android.sensor.info.exposureTimeRange']) 220 221 # RAW image 222 req_raw = capture_request_utils.manual_capture_request( 223 int(sens_min), min_exposure) 224 req_raw['android.sensor.testPatternMode'] = _COLOR_BAR_PATTERN 225 fmt_raw = {'format': 'raw'} 226 cap_raw = cam.do_capture(req_raw, fmt_raw) 227 img_raw = image_processing_utils.convert_capture_to_rgb_image( 228 cap_raw, props=props) 229 230 # Save RAW pattern 231 image_processing_utils.write_image( 232 img_raw, f'{name_with_log_path}_raw_COLOR_BARS.jpg', True) 233 234 # Check pattern for correctness 235 check_raw_pattern(img_raw) 236 237 # YUV image 238 req_yuv = capture_request_utils.manual_capture_request( 239 int(sens_min), min_exposure) 240 req_yuv['android.sensor.testPatternMode'] = _COLOR_BAR_PATTERN 241 req_yuv['android.distortionCorrection.mode'] = 0 242 req_yuv['android.tonemap.mode'] = 0 243 req_yuv['android.tonemap.curve'] = { 244 'red': _LINEAR_TONEMAP, 245 'green': _LINEAR_TONEMAP, 246 'blue': _LINEAR_TONEMAP 247 } 248 fmt_yuv = {'format': 'yuv', 'width': _YUV_W, 'height': _YUV_H} 249 cap_yuv = cam.do_capture(req_yuv, fmt_yuv) 250 img_yuv = image_processing_utils.convert_capture_to_rgb_image(cap_yuv, True) 251 252 # Save YUV pattern 253 image_processing_utils.write_image( 254 img_yuv, f'{name_with_log_path}_yuv_COLOR_BARS.jpg', True) 255 256 # Check pattern for correctness 257 check_yuv_vs_raw(img_raw, img_yuv, name_with_log_path) 258 259 260class TonemapCurveTest(its_base_test.ItsBaseTest): 261 """Test conversion of test pattern from RAW to YUV with linear tonemap. 262 263 Test makes use of android.sensor.testPatternMode 2 (_COLOR_BARS). 264 """ 265 266 def test_tonemap_curve(self): 267 name_with_log_path = os.path.join(self.log_path, _NAME) 268 with its_session_utils.ItsSession( 269 device_id=self.dut.serial, 270 camera_id=self.camera_id, 271 hidden_physical_id=self.hidden_physical_id) as cam: 272 props = cam.get_camera_properties() 273 camera_properties_utils.skip_unless( 274 camera_properties_utils.raw16(props) and 275 camera_properties_utils.manual_sensor(props) and 276 camera_properties_utils.per_frame_control(props) and 277 camera_properties_utils.manual_post_proc(props) and 278 camera_properties_utils.color_bars_test_pattern(props)) 279 280 test_tonemap_curve_impl(name_with_log_path, cam, props) 281 282 283if __name__ == '__main__': 284 test_runner.main() 285