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"""Ensure the captures from the default camera app and JCA are consistent.""" 15 16import logging 17import math 18import os 19import pathlib 20import types 21 22import camera_properties_utils 23import gen2_rig_controller_utils 24import ip_chart_extraction_utils as ce 25import ip_chart_pattern_detector as pd 26import ip_metrics_utils 27import its_base_test 28import its_device_utils 29import its_session_utils 30from mobly import test_runner 31import sensor_fusion_utils 32from snippet_uiautomator import uiautomator 33import ui_interaction_utils 34 35 36_CAMERA_HARDWARE_LEVEL_MAPPING = types.MappingProxyType({ 37 0: 'LIMITED', 38 1: 'FULL', 39 2: 'LEGACY', 40 3: 'LEVEL_3', 41 4: 'EXTERNAL', 42}) 43_JETPACK_CAMERA_APP_PACKAGE_NAME = 'com.google.jetpackcamera' 44_AWB_DIFF_THRESHOLD = 4 45_BRIGHTNESS_DIFF_THRESHOLD = 10 46_NAME = os.path.splitext(os.path.basename(__file__))[0] 47 48 49class DefaultJcaImageParityClassTest(its_base_test.ItsBaseTest): 50 """Test for default camera and JCA image parity.""" 51 52 def _setup_gen2rig(self): 53 logging.debug('Setting up gen2 rig') 54 # Configure and setup gen2 rig 55 motor_channel = int(self.rotator_ch) 56 lights_channel = int(self.lighting_ch) 57 lights_port = gen2_rig_controller_utils.find_serial_port(self.lighting_cntl) 58 sensor_fusion_utils.establish_serial_comm(lights_port) 59 gen2_rig_controller_utils.set_lighting_state( 60 lights_port, lights_channel, 'ON') 61 62 motor_port = gen2_rig_controller_utils.find_serial_port( 63 self.rotator_cntl) 64 gen2_rig_controller_utils.configure_rotator(motor_port, motor_channel) 65 gen2_rig_controller_utils.rotate(motor_port, motor_channel) 66 67 def setup_class(self): 68 super().setup_class() 69 self.dut.services.register( 70 uiautomator.ANDROID_SERVICE_NAME, uiautomator.UiAutomatorService 71 ) 72 73 def teardown_test(self): 74 ui_interaction_utils.force_stop_app( 75 self.dut, _JETPACK_CAMERA_APP_PACKAGE_NAME 76 ) 77 78 if self.rotator_cntl == 'gen2_rotator': 79 # Release the serial ports properly after the test 80 motor_port = gen2_rig_controller_utils.find_serial_port(self.rotator_cntl) 81 motor_port.close() 82 if self.lighting_cntl == 'gen2_lights': 83 # Lights will go back to default state after the test 84 lights_port = gen2_rig_controller_utils.find_serial_port( 85 self.lighting_cntl 86 ) 87 lights_port.close() 88 89 def on_fail(self, record): 90 super().on_fail(record) 91 self.dut.take_screenshot(self.log_path, prefix='on_test_fail') 92 93 def test_default_jca_capture_ip(self): 94 """Check default camera and JCA app image consistency.""" 95 96 with its_session_utils.ItsSession( 97 device_id=self.dut.serial, 98 camera_id=self.camera_id, 99 hidden_physical_id=self.hidden_physical_id) as cam: 100 props = cam.get_camera_properties() 101 props = cam.override_with_hidden_physical_camera_props(props) 102 if (props['android.lens.facing'] 103 == camera_properties_utils.LENS_FACING['FRONT']): 104 camera_facing = 'front' 105 else: 106 camera_facing = 'rear' 107 logging.debug('Camera facing: %s', camera_facing) 108 camera_hardware_level = _CAMERA_HARDWARE_LEVEL_MAPPING[ 109 props.get('android.info.supportedHardwareLevel') 110 ] 111 logging.debug('Camera hardware level: %s', camera_hardware_level) 112 # logging for data collection 113 print(f'{_NAME}_camera_hardware_level: {camera_hardware_level}') 114 first_api_level = its_session_utils.get_first_api_level(self.dut.serial) 115 is_dut_tablet_or_desktop = its_device_utils.is_dut_tablet_or_desktop( 116 self.dut.serial) 117 118 # Skip the test if camera is not primary or if it is a tablet 119 is_primary_camera = self.hidden_physical_id is None 120 camera_properties_utils.skip_unless( 121 not is_dut_tablet_or_desktop and 122 is_primary_camera and 123 first_api_level >= its_session_utils.ANDROID16_API_LEVEL 124 ) 125 # close camera after props have been retrieved 126 cam.close_camera() 127 device_id = self.dut.serial 128 129 # Set up gen2 rig controllers 130 if self.rotator_cntl == 'None' or self.lighting_cntl == 'None': 131 logging.debug('Gen2 rig is not available.') 132 else: 133 self._setup_gen2rig() 134 135 # Get default camera app pkg name 136 pkg_name = cam.get_default_camera_pkg() 137 logging.debug('Default camera pkg name: %s', pkg_name) 138 ui_interaction_utils.default_camera_app_dut_setup(device_id, pkg_name) 139 140 # Launch ItsTestActivity 141 its_device_utils.start_its_test_activity(device_id) 142 if self.dut.ui(text='OK').wait.exists( 143 timeout=ui_interaction_utils.WAIT_INTERVAL_FIVE_SECONDS 144 ): 145 self.dut.ui(text='OK').click.wait() 146 147 # Take capture with default camera app 148 device_img_path = ui_interaction_utils.launch_and_take_capture( 149 dut=self.dut, 150 pkg_name=pkg_name, 151 camera_facing=camera_facing, 152 log_path=self.log_path, 153 ) 154 ui_interaction_utils.pull_img_files( 155 device_id, device_img_path, self.log_path 156 ) 157 default_img_name = pathlib.Path(device_img_path).name 158 default_path = os.path.join(self.log_path, default_img_name) 159 logging.debug('Default capture img name: %s', default_img_name) 160 default_capture_path = pathlib.Path(default_path) 161 default_capture_path = default_capture_path.with_name( 162 f'{default_capture_path.stem}_default{default_capture_path.suffix}' 163 ) 164 os.rename(default_path, default_capture_path) 165 # Get the zoomRatio value used by default camera app 166 default_watch_dump_file = os.path.join( 167 self.log_path, 168 ui_interaction_utils.DEFAULT_CAMERA_WATCH_DUMP_FILE 169 ) 170 zoom_ratio = ui_interaction_utils.get_default_camera_zoom_ratio( 171 default_watch_dump_file) 172 logging.debug('Default camera captures zoomRatio value: %s', zoom_ratio) 173 jca_zoom_ratio = None 174 if zoom_ratio != 1: 175 jca_zoom_ratio = zoom_ratio 176 video_stabilization = None 177 video_stabilization_mode = ( 178 ui_interaction_utils.get_default_camera_video_stabilization( 179 default_watch_dump_file) 180 ) 181 if video_stabilization_mode == 'OFF': 182 # Check if device has OIS enabled 183 ois_enabled = ( 184 ui_interaction_utils.get_default_camera_ois_mode( 185 default_watch_dump_file) 186 ) 187 if ois_enabled == 'ON': 188 video_stabilization = ( 189 ui_interaction_utils.JCA_VIDEO_STABILIZATION_MODE_OPTICAL 190 ) 191 else: 192 video_stabilization = ( 193 ui_interaction_utils.JCA_VIDEO_STABILIZATION_MODE_OFF 194 ) 195 else: 196 video_stabilization = ( 197 ui_interaction_utils.JCA_VIDEO_STABILIZATION_MODE_ON 198 ) 199 # Take JCA capture with UI 200 jca_capture_path = ui_interaction_utils.launch_jca_and_capture( 201 self.dut, 202 self.log_path, 203 camera_facing=props['android.lens.facing'], 204 zoom_ratio=jca_zoom_ratio, 205 video_stabilization=video_stabilization 206 ) 207 ui_interaction_utils.pull_img_files( 208 device_id, jca_capture_path, self.log_path 209 ) 210 img_name = pathlib.Path(jca_capture_path).name 211 jca_path = os.path.join(self.log_path, img_name) 212 logging.debug('JCA capture img name: %s', img_name) 213 jca_capture_path = pathlib.Path(jca_path) 214 jca_capture_path = jca_capture_path.with_name( 215 f'{jca_capture_path.stem}_jca{jca_capture_path.suffix}' 216 ) 217 os.rename(jca_path, jca_capture_path) 218 219 # Extract FULL_CHART from the captured image. 220 _, _ = ( 221 ce.get_feature_from_image( 222 default_capture_path, 223 'default_full_chart', 224 self.log_path, 225 pd.TestChartFeature.FULL_CHART, 226 ) 227 ) 228 229 _, _ = ce.get_feature_from_image( 230 jca_capture_path, 231 'jca_full_chart', 232 self.log_path, 233 pd.TestChartFeature.FULL_CHART, 234 ) 235 236 default_qr_code, _ = ce.get_feature_from_image( 237 default_capture_path, 238 'default_qr_code', 239 self.log_path, 240 pd.TestChartFeature.CENTER_QR_CODE, 241 ) 242 243 jca_qr_code, _ = ce.get_feature_from_image( 244 jca_capture_path, 245 'jca_qr_code', 246 self.log_path, 247 pd.TestChartFeature.CENTER_QR_CODE, 248 ) 249 250 logging.debug('Checking if FoV match between default and jca captures') 251 default_fov = ip_metrics_utils.get_fov_in_degrees( 252 default_capture_path, default_qr_code, self.chart_distance) 253 logging.debug('Default camera FoV: %.2f', default_fov) 254 255 jca_fov = ip_metrics_utils.get_fov_in_degrees( 256 jca_capture_path, jca_qr_code, self.chart_distance) 257 logging.debug('JCA camera FoV: %.2f', jca_fov) 258 fov_match = True 259 if not math.isclose( 260 default_fov, jca_fov, rel_tol=ip_metrics_utils.FOV_REL_TOL): 261 fov_match = False 262 263 logging.debug( 264 'Default and JCA FOV difference within tolerance: %s.\n ' 265 'Expected: %s, Actual: %s', fov_match, 266 ip_metrics_utils.FOV_REL_TOL, 267 abs(default_fov - jca_fov) / max(abs(default_fov), abs(jca_fov)) 268 ) 269 # logging for data collection 270 print(f'{_NAME}_fov_match: {fov_match}') 271 272 # Get cropped dynamic range patch cells 273 default_dynamic_range_patch_cells = ( 274 ce.get_cropped_dynamic_range_patch_cells( 275 default_capture_path, self.log_path, 'default') 276 ) 277 jca_dynamic_range_patch_cells = ce.get_cropped_dynamic_range_patch_cells( 278 jca_capture_path, self.log_path, 'jca' 279 ) 280 e_msg = [] 281 282 # Get brightness diff between default and jca captures 283 mean_brightness_diff = ip_metrics_utils.do_brightness_check( 284 default_dynamic_range_patch_cells, jca_dynamic_range_patch_cells 285 ) 286 # logging for data collection 287 print(f'{_NAME}_mean_brightness_diff: {mean_brightness_diff}') 288 logging.debug('mean_brightness_diff: %f', mean_brightness_diff) 289 if abs(mean_brightness_diff) > _BRIGHTNESS_DIFF_THRESHOLD: 290 e_msg.append('Device fails the brightness difference criteria.') 291 292 # Get white balance diff between default and jca captures 293 mean_white_balance_diff = ip_metrics_utils.do_white_balance_check( 294 default_dynamic_range_patch_cells, jca_dynamic_range_patch_cells 295 ) 296 # logging for data collection 297 print(f'{_NAME}_mean_white_balance_diff: {mean_white_balance_diff}') 298 logging.debug('mean_white_balance_diff: %f', mean_white_balance_diff) 299 if abs(mean_white_balance_diff) > _AWB_DIFF_THRESHOLD: 300 e_msg.append('Device fails the white balance difference criteria.') 301 if not fov_match: 302 e_msg.append('Device fails the FOV match check.') 303 if e_msg: 304 raise AssertionError( 305 f'{its_session_utils.NOT_YET_MANDATED_MESSAGE}\n\n{e_msg}') 306 307 308if __name__ == '__main__': 309 test_runner.main() 310