1# Copyright 2024 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"""Verifies JPEG still capture images are correct in the complex scene.""" 15 16 17import logging 18import os.path 19 20from mobly import test_runner 21import numpy as np 22import PIL 23 24import its_base_test 25import camera_properties_utils 26import capture_request_utils 27import image_processing_utils 28import its_session_utils 29 30 31_BUSY_SCENE_VARIANCE_ATOL = 0.01 # busy scenes variances > this for [0, 1] img 32_JPEG_EXTENSION = '.jpg' 33_JPEG_QUALITY_SETTING = 100 # set value to max 34_NAME = os.path.splitext(os.path.basename(__file__))[0] 35_NUM_STEPS = 8 36_ZOOM_RATIO_MAX = 4 # too high zoom ratios will eventualy reduce entropy 37_ZOOM_RATIO_MIN = 1 # low zoom ratios don't fill up FoV 38_ZOOM_RATIO_THRESH = 2 # some zoom ratio needed to fill up FoV 39 40 41def _read_files_back_from_disk(log_path): 42 """Read the JPEG files written as part of test back from disk. 43 44 Args: 45 log_path: string; location to read files. 46 47 Returns: 48 list of uint8 images read with Image.read(). 49 jpeg_size_max: int; max size of jpeg files. 50 """ 51 jpeg_files = [] 52 for file in sorted(os.listdir(log_path)): 53 if _JPEG_EXTENSION in file: 54 jpeg_files.append(file) 55 if jpeg_files: 56 logging.debug('JPEG files from directory: %s', jpeg_files) 57 else: 58 raise AssertionError(f'No JPEG files in {log_path}') 59 for jpeg_file in jpeg_files: 60 jpeg_file_with_log_path = os.path.join(log_path, jpeg_file) 61 jpeg_file_size = os.stat(jpeg_file_with_log_path).st_size 62 logging.debug('Opening file %s', jpeg_file) 63 logging.debug('File size %d (bytes)', jpeg_file_size) 64 try: 65 image_processing_utils.convert_image_to_numpy_array( 66 jpeg_file_with_log_path) 67 except PIL.UnidentifiedImageError as e: 68 raise AssertionError(f'Cannot read {jpeg_file_with_log_path}') from e 69 logging.debug('Successfully read %s.', jpeg_file) 70 71 72class JpegHighEntropyTest(its_base_test.ItsBaseTest): 73 """Tests JPEG still capture with a complex scene. 74 75 Steps zoom ratio to ensure the complex scene fills the camera FoV. 76 """ 77 78 def test_jpeg_high_entropy(self): 79 with its_session_utils.ItsSession( 80 device_id=self.dut.serial, 81 camera_id=self.camera_id, 82 hidden_physical_id=self.hidden_physical_id) as cam: 83 props = cam.get_camera_properties() 84 props = cam.override_with_hidden_physical_camera_props(props) 85 log_path = self.log_path 86 test_name_with_log_path = os.path.join(log_path, _NAME) 87 88 # Load chart for scene 89 its_session_utils.load_scene( 90 cam, props, self.scene, self.tablet, 91 its_session_utils.CHART_DISTANCE_NO_SCALING) 92 93 # Determine test zoom range 94 zoom_range = props['android.control.zoomRatioRange'] 95 zoom_min, zoom_max = float(zoom_range[0]), float(zoom_range[1]) 96 logging.debug('Zoom max value: %.2f', zoom_max) 97 if zoom_min == zoom_max: 98 zoom_ratios = [zoom_min] 99 else: 100 zoom_max = min(zoom_max, _ZOOM_RATIO_MAX) 101 zoom_ratios = np.arange( 102 _ZOOM_RATIO_MIN, zoom_max, 103 (zoom_max - _ZOOM_RATIO_MIN) / (_NUM_STEPS - 1) 104 ) 105 zoom_ratios = np.append(zoom_ratios, zoom_max) 106 logging.debug('Testing zoom range: %s', zoom_ratios) 107 108 # Do captures over zoom range 109 req = capture_request_utils.auto_capture_request() 110 req['android.jpeg.quality'] = _JPEG_QUALITY_SETTING 111 out_surface = capture_request_utils.get_largest_jpeg_format(props) 112 logging.debug('req W: %d, H: %d', 113 out_surface['width'], out_surface['height']) 114 115 for zoom_ratio in zoom_ratios: 116 req['android.control.zoomRatio'] = zoom_ratio 117 logging.debug('zoom ratio: %.3f', zoom_ratio) 118 cam.do_3a(zoom_ratio=zoom_ratio) 119 cap = cam.do_capture(req, out_surface) 120 121 # Save JPEG image 122 try: 123 img = image_processing_utils.convert_capture_to_rgb_image( 124 cap, props=props) 125 except PIL.UnidentifiedImageError as e: 126 raise AssertionError( 127 f'Cannot convert cap to JPEG for zoom: {zoom_ratio:.2f}') from e 128 logging.debug('cap size (pixels): %d', img.shape[1]*img.shape[0]) 129 image_processing_utils.write_image( 130 img, f'{test_name_with_log_path}_{zoom_ratio:.2f}{_JPEG_EXTENSION}') 131 132 r_var, b_var, g_var = image_processing_utils.compute_image_variances( 133 img 134 ) 135 logging.debug('img vars: %.4f, %.4f, %.4f', r_var, g_var, b_var) 136 if max(r_var, g_var, b_var) < _BUSY_SCENE_VARIANCE_ATOL: 137 raise AssertionError( 138 'Scene is not busy enough! Measured RGB variances: ' 139 f'{r_var:.4f}, {g_var:.4f}, {b_var:.4f}, ' 140 f'ATOL: {_BUSY_SCENE_VARIANCE_ATOL}' 141 ) 142 143 # Read JPEG files back to ensure readable encoding 144 _read_files_back_from_disk(log_path) 145 146if __name__ == '__main__': 147 test_runner.main() 148