• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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