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