| # Copyright 2018 The Android Open Source Project |
| # |
| # Licensed under the Apache License, Version 2.0 (the "License"); |
| # you may not use this file except in compliance with the License. |
| # You may obtain a copy of the License at |
| # |
| # http://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, software |
| # distributed under the License is distributed on an "AS IS" BASIS, |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| # See the License for the specific language governing permissions and |
| # limitations under the License. |
| """CameraITS test for tonemap curve with sensor test pattern.""" |
| |
| import logging |
| import os |
| |
| from mobly import test_runner |
| import numpy as np |
| |
| import its_base_test |
| import camera_properties_utils |
| import capture_request_utils |
| import image_processing_utils |
| import its_session_utils |
| |
| |
| _NAME = os.path.basename(__file__).split('.')[0] |
| _COLOR_BAR_PATTERN = 2 # Note scene0/test_test_patterns must PASS |
| _COLOR_BARS = ['WHITE', 'YELLOW', 'CYAN', 'GREEN', 'MAGENTA', 'RED', |
| 'BLUE', 'BLACK'] |
| _N_BARS = len(_COLOR_BARS) |
| _COLOR_CHECKER = {'BLACK': [0, 0, 0], 'RED': [1, 0, 0], 'GREEN': [0, 1, 0], |
| 'BLUE': [0, 0, 1], 'MAGENTA': [1, 0, 1], 'CYAN': [0, 1, 1], |
| 'YELLOW': [1, 1, 0], 'WHITE': [1, 1, 1]} |
| _DELTA = 0.005 # crop on each edge of color bars |
| _RAW_ATOL = 0.001 # 1 DN in [0:1] (1/(1023-64) |
| _RGB_VAR_ATOL = 0.0039 # 1/255 |
| _RGB_MEAN_ATOL = 0.1 |
| _TONEMAP_MAX = 0.5 |
| _YUV_H = 480 |
| _YUV_W = 640 |
| # Normalized co-ordinates for the color bar patch. |
| _Y_NORM = 0.0 |
| _W_NORM = 1.0 / _N_BARS - 2 * _DELTA |
| _H_NORM = 1.0 |
| |
| # Linear tonemap with maximum of 0.5 |
| _LINEAR_TONEMAP = sum([[i/63.0, i/126.0] for i in range(64)], []) |
| |
| |
| def get_yuv_patch_coordinates(num, w_orig, w_crop): |
| """Returns the normalized x co-ordinate for the title. |
| |
| Args: |
| num: int; position on color in the color bar. |
| w_orig: float; original RAW image W |
| w_crop: float; cropped RAW image W |
| |
| Returns: |
| normalized x, w values for color patch. |
| """ |
| if w_crop == w_orig: # uncropped image |
| x_norm = num / _N_BARS + _DELTA |
| w_norm = 1 / _N_BARS - 2 * _DELTA |
| logging.debug('x_norm: %.5f, w_norm: %.5f', x_norm, w_norm) |
| elif w_crop < w_orig: # adjust patch width to match vertical RAW crop |
| w_delta_edge = (w_orig - w_crop) / 2 |
| w_bar_orig = w_orig / _N_BARS |
| if num == 0: # left-most bar |
| x_norm = _DELTA |
| w_norm = (w_bar_orig - w_delta_edge) / w_crop - 2 * _DELTA |
| elif num == _N_BARS: # right-most bar |
| x_norm = (w_bar_orig * num - w_delta_edge) / w_crop + _DELTA |
| w_norm = (w_bar_orig - w_delta_edge) / w_crop - 2 * _DELTA |
| else: # middle bars |
| x_norm = (w_bar_orig * num - w_delta_edge) / w_crop + _DELTA |
| w_norm = w_bar_orig / w_crop - 2 * _DELTA |
| logging.debug('x_norm: %.5f, w_norm: %.5f (crop-corrected)', x_norm, w_norm) |
| else: |
| raise AssertionError('Cropped image is larger than original!') |
| return x_norm, w_norm |
| |
| |
| def get_x_norm(num): |
| """Returns the normalized x co-ordinate for the title. |
| |
| Args: |
| num: int; position on color in the color bar. |
| |
| Returns: |
| normalized x co-ordinate. |
| """ |
| return float(num) / _N_BARS + _DELTA |
| |
| |
| def check_raw_pattern(img_raw): |
| """Checks for RAW capture matches color bar pattern. |
| |
| Args: |
| img_raw: RAW image |
| """ |
| logging.debug('Checking RAW/PATTERN match') |
| color_match = [] |
| for n in range(_N_BARS): |
| x_norm = get_x_norm(n) |
| raw_patch = image_processing_utils.get_image_patch(img_raw, x_norm, _Y_NORM, |
| _W_NORM, _H_NORM) |
| raw_means = image_processing_utils.compute_image_means(raw_patch) |
| logging.debug('patch: %d, x_norm: %.3f, RAW means: %s', |
| n, x_norm, str(raw_means)) |
| for color in _COLOR_BARS: |
| if np.allclose(_COLOR_CHECKER[color], raw_means, atol=_RAW_ATOL): |
| color_match.append(color) |
| logging.debug('%s match', color) |
| break |
| else: |
| logging.debug('No match w/ %s: %s, ATOL: %.3f', |
| color, str(_COLOR_CHECKER[color]), _RAW_ATOL) |
| if set(color_match) != set(_COLOR_BARS): |
| raise AssertionError( |
| 'RAW _COLOR_BARS test pattern does not have all colors') |
| |
| |
| def check_yuv_vs_raw(img_raw, img_yuv, name_with_log_path): |
| """Checks for YUV vs RAW match in 8 patches. |
| |
| Check for correct values and color consistency |
| |
| Args: |
| img_raw: RAW image |
| img_yuv: YUV image |
| name_with_log_path: string for test name with path |
| """ |
| logging.debug('Checking YUV/RAW match') |
| raw_w = img_raw.shape[1] |
| raw_h = img_raw.shape[0] |
| raw_aspect_ratio = raw_w/raw_h |
| yuv_aspect_ratio = _YUV_W/_YUV_H |
| logging.debug('raw_img: W, H, AR: %d, %d, %.3f', |
| raw_w, raw_h, raw_aspect_ratio) |
| |
| # Crop RAW to match YUV 4:3 format |
| raw_w_cropped = raw_w |
| if raw_aspect_ratio > yuv_aspect_ratio: # vertical crop sensor |
| logging.debug('Cropping RAW to match YUV aspect ratio.') |
| w_norm_raw = yuv_aspect_ratio / raw_aspect_ratio |
| x_norm_raw = (1 - w_norm_raw) / 2 |
| img_raw = image_processing_utils.get_image_patch( |
| img_raw, x_norm_raw, 0, w_norm_raw, 1) |
| raw_w_cropped = img_raw.shape[1] |
| logging.debug('New RAW W, H: %d, %d', raw_w_cropped, img_raw.shape[0]) |
| image_processing_utils.write_image( |
| img_raw, f'{name_with_log_path}_raw_cropped_COLOR_BARS.jpg', True) |
| |
| # Compare YUV and RAW color patches |
| color_match_errs = [] |
| color_variance_errs = [] |
| for n in range(_N_BARS): |
| x_norm, w_norm = get_yuv_patch_coordinates(n, raw_w, raw_w_cropped) |
| raw_patch = image_processing_utils.get_image_patch(img_raw, x_norm, _Y_NORM, |
| w_norm, _H_NORM) |
| yuv_patch = image_processing_utils.get_image_patch(img_yuv, x_norm, _Y_NORM, |
| w_norm, _H_NORM) |
| raw_means = np.array(image_processing_utils.compute_image_means(raw_patch)) |
| raw_vars = np.array( |
| image_processing_utils.compute_image_variances(raw_patch)) |
| yuv_means = np.array(image_processing_utils.compute_image_means(yuv_patch)) |
| yuv_means /= _TONEMAP_MAX # Normalize to tonemap max |
| yuv_vars = np.array( |
| image_processing_utils.compute_image_variances(yuv_patch)) |
| if not np.allclose(raw_means, yuv_means, atol=_RGB_MEAN_ATOL): |
| color_match_errs.append( |
| f'means RAW: {raw_means}, RGB(norm): {np.round(yuv_means, 3)}, ' |
| f'ATOL: {_RGB_MEAN_ATOL}') |
| image_processing_utils.write_image( |
| raw_patch, f'{name_with_log_path}_match_error_raw_{n}.jpg', |
| apply_gamma=True) |
| image_processing_utils.write_image( |
| yuv_patch, f'{name_with_log_path}_match_error_yuv_{n}.jpg', |
| apply_gamma=True) |
| if not np.allclose(raw_vars, yuv_vars, atol=_RGB_VAR_ATOL): |
| color_variance_errs.append( |
| f'variances RAW: {raw_vars}, RGB: {yuv_vars}, ' |
| f'ATOL: {_RGB_VAR_ATOL}') |
| image_processing_utils.write_image( |
| raw_patch, f'{name_with_log_path}_variance_error_raw_{n}.jpg', |
| apply_gamma=True) |
| image_processing_utils.write_image( |
| yuv_patch, f'{name_with_log_path}_variance_error_yuv_{n}.jpg', |
| apply_gamma=True) |
| |
| # Print all errors before assertion |
| if color_match_errs: |
| for err in color_match_errs: |
| logging.debug(err) |
| for err in color_variance_errs: |
| logging.error(err) |
| raise AssertionError('Color match errors. See test_log.DEBUG') |
| if color_variance_errs: |
| for err in color_variance_errs: |
| logging.error(err) |
| raise AssertionError('Color variance errors. See test_log.DEBUG') |
| |
| |
| def test_tonemap_curve_impl(name_with_log_path, cam, props): |
| """Test tonemap curve with sensor test pattern. |
| |
| Args: |
| name_with_log_path: Path to save the captured image. |
| cam: An open device session. |
| props: Properties of cam. |
| """ |
| |
| avail_patterns = props['android.sensor.availableTestPatternModes'] |
| logging.debug('Available Patterns: %s', avail_patterns) |
| sens_min, _ = props['android.sensor.info.sensitivityRange'] |
| min_exposure = min(props['android.sensor.info.exposureTimeRange']) |
| |
| # RAW image |
| req_raw = capture_request_utils.manual_capture_request( |
| int(sens_min), min_exposure) |
| req_raw['android.sensor.testPatternMode'] = _COLOR_BAR_PATTERN |
| fmt_raw = {'format': 'raw'} |
| cap_raw = cam.do_capture(req_raw, fmt_raw) |
| img_raw = image_processing_utils.convert_capture_to_rgb_image( |
| cap_raw, props=props) |
| |
| # Save RAW pattern |
| image_processing_utils.write_image( |
| img_raw, f'{name_with_log_path}_raw_COLOR_BARS.jpg', True) |
| |
| # Check pattern for correctness |
| check_raw_pattern(img_raw) |
| |
| # YUV image |
| req_yuv = capture_request_utils.manual_capture_request( |
| int(sens_min), min_exposure) |
| req_yuv['android.sensor.testPatternMode'] = _COLOR_BAR_PATTERN |
| req_yuv['android.distortionCorrection.mode'] = 0 |
| req_yuv['android.tonemap.mode'] = 0 |
| req_yuv['android.tonemap.curve'] = { |
| 'red': _LINEAR_TONEMAP, |
| 'green': _LINEAR_TONEMAP, |
| 'blue': _LINEAR_TONEMAP |
| } |
| fmt_yuv = {'format': 'yuv', 'width': _YUV_W, 'height': _YUV_H} |
| cap_yuv = cam.do_capture(req_yuv, fmt_yuv) |
| img_yuv = image_processing_utils.convert_capture_to_rgb_image(cap_yuv, True) |
| |
| # Save YUV pattern |
| image_processing_utils.write_image( |
| img_yuv, f'{name_with_log_path}_yuv_COLOR_BARS.jpg', True) |
| |
| # Check pattern for correctness |
| check_yuv_vs_raw(img_raw, img_yuv, name_with_log_path) |
| |
| |
| class TonemapCurveTest(its_base_test.ItsBaseTest): |
| """Test conversion of test pattern from RAW to YUV with linear tonemap. |
| |
| Test makes use of android.sensor.testPatternMode 2 (_COLOR_BARS). |
| """ |
| |
| def test_tonemap_curve(self): |
| name_with_log_path = os.path.join(self.log_path, _NAME) |
| with its_session_utils.ItsSession( |
| device_id=self.dut.serial, |
| camera_id=self.camera_id, |
| hidden_physical_id=self.hidden_physical_id) as cam: |
| props = cam.get_camera_properties() |
| camera_properties_utils.skip_unless( |
| camera_properties_utils.raw16(props) and |
| camera_properties_utils.manual_sensor(props) and |
| camera_properties_utils.per_frame_control(props) and |
| camera_properties_utils.manual_post_proc(props) and |
| camera_properties_utils.color_bars_test_pattern(props)) |
| |
| test_tonemap_curve_impl(name_with_log_path, cam, props) |
| |
| |
| if __name__ == '__main__': |
| test_runner.main() |