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