summaryrefslogtreecommitdiff
path: root/apps/CameraITS/tests/scene4/test_video_aspect_ratio_and_crop.py
blob: bb713672e00486f160b360859735f4d4571648b8 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
# Copyright 2022 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.
"""Validate video aspect ratio, crop and FoV vs format."""

import logging
import os.path
from mobly import test_runner

import its_base_test
import camera_properties_utils
import its_session_utils
import video_processing_utils
import capture_request_utils
import image_processing_utils
import opencv_processing_utils
import image_fov_utils

_NAME = os.path.splitext(os.path.basename(__file__))[0]
_VIDEO_RECORDING_DURATION_SECONDS = 3
_FOV_PERCENT_RTOL = 0.15  # Relative tolerance on circle FoV % to expected.
_AR_CHECKED_PRE_API_30 = ('4:3', '16:9', '18:9')
_AR_DIFF_ATOL = 0.01
_MAX_8BIT_IMGS = 255
_MAX_10BIT_IMGS = 1023


def _print_failed_test_results(failed_ar, failed_fov, failed_crop,
                               quality):
  """Print failed test results."""
  if failed_ar:
    logging.error('Aspect ratio test summary for %s', quality)
    logging.error('Images failed in the aspect ratio test:')
    logging.error('Aspect ratio value: width / height')
    for fa in failed_ar:
      logging.error('%s', fa)

  if failed_fov:
    logging.error('FoV test summary for %s', quality)
    logging.error('Images failed in the FoV test:')
    for fov in failed_fov:
      logging.error('%s', str(fov))

  if failed_crop:
    logging.error('Crop test summary for %s', quality)
    logging.error('Images failed in the crop test:')
    logging.error('Circle center (H x V) relative to the image center.')
    for fc in failed_crop:
      logging.error('%s', fc)


class VideoAspectRatioAndCropTest(its_base_test.ItsBaseTest):
  """Test aspect ratio/field of view/cropping for each tested fmt.

    This test checks for:
    1. Aspect ratio: images are not stretched
    2. Crop: center of images is not shifted
    3. FOV: images cropped to keep maximum possible FOV with only 1 dimension
       (horizontal or veritical) cropped.

  Video recording will be done using the SDR profile as well as HLG10
  if available.

  The test video is a black circle on a white background.

  When RAW capture is available, set the height vs. width ratio of the circle in
  the full-frame RAW as ground truth. In an ideal setup such ratio should be
  very close to 1.0, but here we just use the value derived from full resolution
  RAW as ground truth to account for the possibility that the chart is not well
  positioned to be precisely parallel to image sensor plane.
  The test then compares the ground truth ratio with the same ratio measured
  on videos captued using different formats.

  If RAW capture is unavailable, a full resolution JPEG image is used to setup
  ground truth. In this case, the ground truth aspect ratio is defined as 1.0
  and it is the tester's responsibility to make sure the test chart is
  properly positioned so the detected circles indeed have aspect ratio close
  to 1.0 assuming no bugs causing image stretched.

  The aspect ratio test checks the aspect ratio of the detected circle and
  it will fail if the aspect ratio differs too much from the ground truth
  aspect ratio mentioned above.

  The FOV test examines the ratio between the detected circle area and the
  image size. When the aspect ratio of the test image is the same as the
  ground truth image, the ratio should be very close to the ground truth
  value. When the aspect ratio is different, the difference is factored in
  per the expectation of the Camera2 API specification, which mandates the
  FOV reduction from full sensor area must only occur in one dimension:
  horizontally or vertically, and never both. For example, let's say a sensor
  has a 16:10 full sensor FOV. For all 16:10 output images there should be no
  FOV reduction on them. For 16:9 output images the FOV should be vertically
  cropped by 9/10. For 4:3 output images the FOV should be cropped
  horizontally instead and the ratio (r) can be calculated as follows:
      (16 * r) / 10 = 4 / 3 => r = 40 / 48 = 0.8333
  Say the circle is covering x percent of the 16:10 sensor on the full 16:10
  FOV, and assume the circle in the center will never be cut in any output
  sizes (this can be achieved by picking the right size and position of the
  test circle), the from above cropping expectation we can derive on a 16:9
  output image the circle will cover (x / 0.9) percent of the 16:9 image; on
  a 4:3 output image the circle will cover (x / 0.8333) percent of the 4:3
  image.

  The crop test checks that the center of any output image remains aligned
  with center of sensor's active area, no matter what kind of cropping or
  scaling is applied. The test verifies that by checking the relative vector
  from the image center to the center of detected circle remains unchanged.
  The relative part is normalized by the detected circle size to account for
  scaling effect.
  """

  def test_video_aspect_ratio_and_crop(self):
    logging.debug('Starting %s', _NAME)
    failed_ar = []  # Streams failed the aspect ratio test.
    failed_crop = []  # Streams failed the crop test.
    failed_fov = []  # Streams that fail FoV test.

    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()
      fls_logical = props['android.lens.info.availableFocalLengths']
      logging.debug('logical available focal lengths: %s', str(fls_logical))
      props = cam.override_with_hidden_physical_camera_props(props)
      fls_physical = props['android.lens.info.availableFocalLengths']
      logging.debug('physical available focal lengths: %s', str(fls_physical))

      # Check SKIP conditions.
      vendor_api_level = its_session_utils.get_vendor_api_level(self.dut.serial)
      camera_properties_utils.skip_unless(
          vendor_api_level >= its_session_utils.ANDROID13_API_LEVEL)

      # Load scene.
      its_session_utils.load_scene(cam, props, self.scene,
                                   self.tablet, self.chart_distance)

      # Determine camera capabilities.
      supported_video_qualities = cam.get_supported_video_qualities(
          self.camera_id)
      logging.debug('Supported video qualities: %s', supported_video_qualities)
      full_or_better = camera_properties_utils.full_or_better(props)
      raw_avlb = camera_properties_utils.raw16(props)

      req = capture_request_utils.auto_capture_request()
      ref_img_name_stem = f'{os.path.join(self.log_path, _NAME)}'

      if raw_avlb and (fls_physical == fls_logical):
        logging.debug('RAW')
        raw_bool = True
      else:
        logging.debug('JPEG')
        raw_bool = False
      ref_fov, cc_ct_gt, aspect_ratio_gt = image_fov_utils.find_fov_reference(
          cam, req, props, raw_bool, ref_img_name_stem)

      run_crop_test = full_or_better and raw_avlb

      for quality_profile_id_pair in supported_video_qualities:
        quality = quality_profile_id_pair.split(':')[0]
        profile_id = quality_profile_id_pair.split(':')[-1]
        # Check if we support testing this quality.
        if quality in video_processing_utils.ITS_SUPPORTED_QUALITIES:
          logging.debug('Testing video recording for quality: %s', quality)
          hlg10_params = [False]
          hlg10_supported = cam.is_hlg10_recording_supported(profile_id)
          logging.debug('HLG10 supported: %s', hlg10_supported)
          if hlg10_supported:
            hlg10_params.append(hlg10_supported)

          for hlg10_param in hlg10_params:
            video_recording_obj = cam.do_basic_recording(
                profile_id, quality, _VIDEO_RECORDING_DURATION_SECONDS, 0,
                hlg10_param)
            logging.debug('video_recording_obj: %s', video_recording_obj)
            # TODO(ruchamk): Modify video recording object to send videoFrame
            # width and height instead of videoSize to avoid string operation
            # here.
            video_size = video_recording_obj['videoSize']
            width = int(video_size.split('x')[0])
            height = int(video_size.split('x')[-1])

            # Pull the video recording file from the device.
            self.dut.adb.pull([video_recording_obj['recordedOutputPath'],
                               self.log_path])
            logging.debug('Recorded video is available at: %s',
                          self.log_path)
            video_file_name = video_recording_obj[
                'recordedOutputPath'].split('/')[-1]
            logging.debug('video_file_name: %s', video_file_name)

            key_frame_files = []
            key_frame_files = video_processing_utils.extract_key_frames_from_video(
                self.log_path, video_file_name)
            logging.debug('key_frame_files:%s', key_frame_files)

            # Get the key frame file to process.
            last_key_frame_file = video_processing_utils.get_key_frame_to_process(
                key_frame_files)
            logging.debug('last_key_frame: %s', last_key_frame_file)
            last_key_frame_path = os.path.join(
                self.log_path, last_key_frame_file)

            # Convert lastKeyFrame to numpy array
            np_image = image_processing_utils.convert_image_to_numpy_array(
                last_key_frame_path)
            logging.debug('numpy image shape: %s', np_image.shape)

            # Check fov
            circle = opencv_processing_utils.find_circle(
                np_image, ref_img_name_stem, image_fov_utils.CIRCLE_MIN_AREA,
                image_fov_utils.CIRCLE_COLOR)

            max_img_value = _MAX_8BIT_IMGS
            if hlg10_param:
              max_img_value = _MAX_10BIT_IMGS

            # Check pass/fail for fov coverage for all fmts in AR_CHECKED
            fov_chk_msg = image_fov_utils.check_fov(
                circle, ref_fov, width, height)
            if fov_chk_msg:
              img_name = '%s_%s_w%d_h%d_fov.png' % (
                  os.path.join(self.log_path, _NAME), quality, width, height)
              fov_chk_quality_msg = f'Quality: {quality} {fov_chk_msg}'
              failed_fov.append(fov_chk_quality_msg)
              image_processing_utils.write_image(
                  np_image/max_img_value, img_name, True)

            # Check pass/fail for aspect ratio.
            ar_chk_msg = image_fov_utils.check_ar(
                circle, aspect_ratio_gt, width, height,
                f'{quality}')
            if ar_chk_msg:
              img_name = '%s_%s_w%d_h%d_ar.png' % (
                  os.path.join(self.log_path, _NAME), quality, width, height)
              failed_ar.append(ar_chk_msg)
              image_processing_utils.write_image(
                  np_image/max_img_value, img_name, True)

            # Check pass/fail for crop.
            if run_crop_test:
              # Normalize the circle size to 1/4 of the image size, so that
              # circle size won't affect the crop test result
              crop_thresh_factor = ((min(ref_fov['w'], ref_fov['h']) / 4.0) /
                                    max(ref_fov['circle_w'],
                                        ref_fov['circle_h']))
              crop_chk_msg = image_fov_utils.check_crop(
                  circle, cc_ct_gt, width, height,
                  f'{quality}', crop_thresh_factor)
              if crop_chk_msg:
                crop_img_name = '%s_%s_w%d_h%d_crop.png' % (
                    os.path.join(self.log_path, _NAME), quality, width, height)
                opencv_processing_utils.append_circle_center_to_img(
                    circle, np_image*max_img_value, crop_img_name)
                failed_crop.append(crop_chk_msg)
                image_processing_utils.write_image(np_image/max_img_value,
                                                   crop_img_name, True)
            else:
              logging.debug('Crop test skipped')

      # Print any failed test results.
      _print_failed_test_results(failed_ar, failed_fov, failed_crop, quality)
      e_msg = ''
      if failed_ar:
        e_msg = 'Aspect ratio '
      if failed_fov:
        e_msg += 'FoV '
      if failed_crop:
        e_msg += 'Crop '
      if e_msg:
        raise AssertionError(f'{e_msg}check failed.')

if __name__ == '__main__':
  test_runner.main()