blob: 81cfd841550c2d6b5148381075cdf83ca0d33f8a [file]
# Copyright 2024 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.
"""Verifies 3 faces with different skin tones are detected in preview."""
import logging
import os.path
import cv2
from mobly import test_runner
import its_base_test
import camera_properties_utils
import image_processing_utils
import its_session_utils
import opencv_processing_utils
import preview_processing_utils
import video_processing_utils
_CV2_GREEN = (0, 255, 0)
_CV2_LINE_THICKNESS = 3
_CV2_RADIUS = 10
_FACE_DETECT_PROFILE_SCENE = 'scene2_g'
_FD_MODE_OFF, _FD_MODE_SIMPLE, _FD_MODE_FULL = 0, 1, 2
_FRAME_INDEX = -1 # last frame
_NAME = os.path.splitext(os.path.basename(__file__))[0]
_PREVIEW_FACES_MIN_NUM = 3
_PREVIEW_RECORDING_DURATION_SECONDS = 3
_RGB_FULL_CHANNEL = 255
_VALID_FD_MODES = {_FD_MODE_OFF, _FD_MODE_SIMPLE, _FD_MODE_FULL}
def _check_face_landmarks(
crop, face, fd_mode, index, preview_img, preview_size):
"""Checks face landmarks fall within face bounding box.
Face ID should be -1 for SIMPLE and unique for FULL
Args:
crop: dict; crop region size with 'top', 'right', 'left', 'bottom'
as keys to desired region of the sensor to read out.
face: dict; from face detection algorithm.
fd_mode: int; of face detection mode.
index: int; to designate face number.
preview_img: str; Numpy image array.
preview_size: str; preview size used for recording. Ex: 1920x1080.
"""
logging.debug('Checking landmarks in face %d: %s', index, str(face))
if fd_mode == _FD_MODE_SIMPLE:
if 'leftEye' in face or 'rightEye' in face:
raise AssertionError('Eyes not supported in FD_MODE_SIMPLE.')
if 'mouth' in face:
raise AssertionError('Mouth not supported in FD_MODE_SIMPLE.')
if face['id'] != -1:
raise AssertionError('face_id should be -1 in FD_MODE_SIMPLE.')
elif fd_mode == _FD_MODE_FULL:
l, r = face['bounds']['left'], face['bounds']['right']
t, b = face['bounds']['top'], face['bounds']['bottom']
l_eye_x, l_eye_y = face['leftEye']['x'], face['leftEye']['y']
r_eye_x, r_eye_y = face['rightEye']['x'], face['rightEye']['y']
mouth_x, mouth_y = face['mouth']['x'], face['mouth']['y']
_draw_facial_features(crop, l_eye_x, l_eye_y, mouth_x, mouth_y,
preview_img, preview_size, r_eye_x, r_eye_y)
if not l <= l_eye_x <= r:
raise AssertionError(f'Face l: {l}, r: {r}, left eye x: {l_eye_x}')
if not t <= l_eye_y <= b:
raise AssertionError(f'Face t: {t}, b: {b}, left eye y: {l_eye_y}')
if not l <= r_eye_x <= r:
raise AssertionError(f'Face l: {l}, r: {r}, right eye x: {r_eye_x}')
if not t <= r_eye_y <= b:
raise AssertionError(f'Face t: {t}, b: {b}, right eye y: {r_eye_y}')
if not l <= mouth_x <= r:
raise AssertionError(f'Face l: {l}, r: {r}, mouth x: {mouth_x}')
if not t <= mouth_y <= b:
raise AssertionError(f'Face t: {t}, b: {b}, mouth y: {mouth_y}')
else:
raise AssertionError(f'Unknown face detection mode: {fd_mode}.')
def _do_preview_recording_and_retrieve_result(
dut, cam, preview_size, fd_mode, log_path):
"""Issue a preview request and read back the preview recording object.
Args:
dut: obj; Android controller device object.
cam: obj; Camera obj.
preview_size: str; Preview resolution at which to record. Ex. "1920x1080".
fd_mode: int; STATISTICS_FACE_DETECT_MODE. Set if not None.
log_path: str; Log path to save preview recording.
Returns:
result: obj; Recording object.
"""
# Record preview video with face detection.
result = cam.do_preview_recording(
preview_size, _PREVIEW_RECORDING_DURATION_SECONDS, stabilize=False,
zoom_ratio=None, face_detect_mode=fd_mode)
its_session_utils.pull_file_from_dut(dut, result, log_path)
logging.debug('Preview recording with face detection is completed.')
return result
def _draw_facial_features(crop, l_eye_x, l_eye_y, mouth_x, mouth_y,
preview_img, preview_size, r_eye_x, r_eye_y):
"""Mark facial features with green circles.
Args:
crop: dict; crop region size with 'top', 'right', 'left', 'bottom'
as keys to desired region of the sensor to read out.
l_eye_x: int; x-coordinate of the center of the left eye.
l_eye_y: int; y-coordinate of the center of the left eye.
mouth_x: int; x-coordinate of the center of the mouth.
mouth_y: int; y-coordinate of the center of the mouth.
preview_img: str; Numpy image array.
preview_size: str; preview size used for recording. ex: 1920x1080.
r_eye_x: int; x-coordinate of the center of the right eye.
r_eye_y: int; y-coordinate of the center of the right eye.
"""
# Find out the size of active arrays and image.
aa_width = crop['right'] - crop['left']
aa_height = crop['bottom'] - crop['top']
img_width = int(preview_size.split('x')[0])
img_height = int(preview_size.split('x')[1])
# Convert sensor coordinates to image coordinates.
l_eye = image_processing_utils.convert_sensor_coords_to_image_coords(
aa_width, aa_height, (l_eye_x, l_eye_y), img_width, img_height)
r_eye = image_processing_utils.convert_sensor_coords_to_image_coords(
aa_width, aa_height, (r_eye_x, r_eye_y), img_width, img_height)
mouth = image_processing_utils.convert_sensor_coords_to_image_coords(
aa_width, aa_height, (mouth_x, mouth_y), img_width, img_height)
# Draw circles at the center of facial features.
cv2.circle(
preview_img, (int(l_eye[0]), int(l_eye[1])), _CV2_RADIUS,
_CV2_GREEN, _CV2_LINE_THICKNESS)
cv2.circle(
preview_img, (int(r_eye[0]), int(r_eye[1])), _CV2_RADIUS,
_CV2_GREEN, _CV2_LINE_THICKNESS)
cv2.circle(
preview_img, (int(mouth[0]), int(mouth[1])), _CV2_RADIUS,
_CV2_GREEN, _CV2_LINE_THICKNESS)
def _draw_face_rectangles(result, faces, preview_img):
"""Draw boxes around faces in green and save image.
Args:
result: obj; Recorded object returned from ItsService.
faces: list; List of dicts with face information.
preview_img: str; Numpy image array.
"""
# draw boxes around faces in green
crop_region = result['captureMetadata'][_FRAME_INDEX][
'android.scaler.cropRegion']
faces_cropped = (
opencv_processing_utils.correct_faces_for_crop(
faces, preview_img, crop_region)
)
for (l, r, t, b) in faces_cropped:
cv2.rectangle(
preview_img, (l, t), (r, b), _CV2_GREEN, _CV2_LINE_THICKNESS)
class PreviewNumFacesTest(its_base_test.ItsBaseTest):
"""Test face detection with different skin tones in preview."""
def test_preview_num_faces(self):
"""Test face detection."""
log_path = self.log_path
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()
props = cam.override_with_hidden_physical_camera_props(props)
# Load chart for scene.
its_session_utils.load_scene(
cam, props, self.scene, self.tablet, self.chart_distance,
log_path=log_path)
# Check skip conditions.
should_run = (camera_properties_utils.face_detect(props) and
(its_session_utils.get_first_api_level(self.dut.serial) >=
its_session_utils.ANDROID16_API_LEVEL))
camera_properties_utils.skip_unless(should_run)
mono_camera = camera_properties_utils.mono_camera(props)
fd_modes = props['android.statistics.info.availableFaceDetectModes']
cam.do_3a(mono_camera=mono_camera)
for fd_mode in fd_modes:
logging.debug('Face detection mode: %d', fd_mode)
if fd_mode not in _VALID_FD_MODES:
raise AssertionError(f'FD mode {fd_mode} not in MODES! '
f'MODES: {_VALID_FD_MODES}')
# Find largest preview size string and set as recording size.
preview_size = preview_processing_utils.get_max_preview_test_size(
cam, self.camera_id)
logging.debug('Preview size used for recording: %s', preview_size)
# Issue a preview request and read back the preview recording object.
result = _do_preview_recording_and_retrieve_result(
self.dut, cam, preview_size, fd_mode, log_path)
preview_file_name = (
result['recordedOutputPath'].split('/')[-1])
logging.debug('Recorded preview name: %s', preview_file_name)
# Get last key frames from the preview recording.
preview_img = (
video_processing_utils.extract_last_key_frame_from_recording(
log_path, preview_file_name))
# Check face detect mode is correctly set.
fd_mode_cap = (
result['captureMetadata'][_FRAME_INDEX][
'android.statistics.faceDetectMode'])
if fd_mode_cap != fd_mode:
raise AssertionError(f'metadata {fd_mode_cap} != req {fd_mode}')
# 0 faces should be returned for OFF mode.
# Skip remaining checks for _FD_MODE_OFF if no faces were detected.
faces = result['captureMetadata'][_FRAME_INDEX][
'android.statistics.faces']
if fd_mode == _FD_MODE_OFF:
if faces:
raise AssertionError(f'Error: faces detected in OFF: {faces}')
continue
# If front camera, flip preview image to match camera capture.
file_name_stem = os.path.join(log_path, _NAME)
if (props['android.lens.facing'] ==
camera_properties_utils.LENS_FACING['FRONT']):
preview_img = (
image_processing_utils.mirror_preview_image_by_sensor_orientation(
props['android.sensor.orientation'], preview_img))
else:
file_name_stem = os.path.join(log_path, 'rear_preview')
# Draw boxes around faces in green and save image.
_draw_face_rectangles(result, faces, preview_img)
# Face landmarks (if provided) are within face bounding box.
crop = result['captureMetadata'][_FRAME_INDEX][
'android.scaler.cropRegion']
for i, face in enumerate(faces):
_check_face_landmarks(
crop, face, fd_mode, i, preview_img, preview_size)
# Save image with green rectangles.
img_name = f'{file_name_stem}_fd_mode_{fd_mode}.jpg'
image_processing_utils.write_image(
preview_img / _RGB_FULL_CHANNEL, img_name)
# Check if the expected number of faces were detected.
num_faces = len(faces)
logging.debug('Face detection mode %d found %d faces',
fd_mode, num_faces)
if num_faces != _PREVIEW_FACES_MIN_NUM:
e_msg = (f'Face detection in preview found {num_faces} '
f'faces, but expected {_PREVIEW_FACES_MIN_NUM}')
# FAIL* if scene2_g and if faceDetectMode does not support FULL
if self.scene == _FACE_DETECT_PROFILE_SCENE:
if not camera_properties_utils.face_detect_full(props):
e_msg = f'{its_session_utils.NOT_YET_MANDATED_MESSAGE}\n\n{e_msg}'
raise AssertionError(e_msg)
if __name__ == '__main__':
test_runner.main()