Skip to content

Commit 6bc83d5

Browse files
Treehugger RobotGerrit Code Review
authored andcommitted
Merge "Camera: ITS: Allow spurious faces detected by opencv" into android14-tests-dev
2 parents c7bc4f3 + 0bed311 commit 6bc83d5

3 files changed

Lines changed: 148 additions & 116 deletions

File tree

apps/CameraITS/tests/scene2_a/test_num_faces.py

Lines changed: 4 additions & 110 deletions
Original file line numberDiff line numberDiff line change
@@ -32,95 +32,14 @@
3232
_CV2_FACE_SCALE_FACTOR = 1.05 # 5% step for resizing image to find face
3333
_CV2_FACE_MIN_NEIGHBORS = 4 # recommended 3-6: higher for less faces
3434
_CV2_GREEN = (0, 1, 0)
35-
_CV2_RED = (1, 0, 0)
36-
_FACE_CENTER_MATCH_TOL_X = 10 # 10 pixels or ~1.5% in 640x480 image
37-
_FACE_CENTER_MATCH_TOL_Y = 20 # 20 pixels or ~4% in 640x480 image
38-
_FACE_CENTER_MIN_LOGGING_DIST = 50
3935
_FD_MODE_OFF, _FD_MODE_SIMPLE, _FD_MODE_FULL = 0, 1, 2
40-
_MIN_NUM_FACES_ALIGNED = 2
41-
_MIN_CENTER_DELTA = 15
4236
_NAME = os.path.splitext(os.path.basename(__file__))[0]
4337
_NUM_FACES = 3
4438
_NUM_TEST_FRAMES = 20
4539
_TEST_REQUIRED_MPC = 34
4640
_W, _H = 640, 480
4741

4842

49-
def eliminate_duplicate_centers(coordinates_list):
50-
"""Checks center coordinates of OpenCV's face rectangles
51-
52-
Method makes sure that the list of face rectangles' centers do not
53-
contain duplicates from the same face.
54-
55-
Args:
56-
coordinates_list: list; coordinates of face rectangles' centers
57-
Returns:
58-
non_duplicate_list: list; coordinates of face rectangles' centers
59-
without duplicates on the same face
60-
"""
61-
output = set()
62-
63-
for i, xy1 in enumerate(coordinates_list):
64-
for j, xy2 in enumerate(coordinates_list):
65-
if distance.euclidean(xy1, xy2) < _MIN_CENTER_DELTA:
66-
continue
67-
if xy1 not in output:
68-
output.add(xy1)
69-
else:
70-
output.add(xy2)
71-
return list(output)
72-
73-
74-
def match_face_locations(faces_cropped, faces_opencv, mode, img, img_name):
75-
"""Assert face locations between two methods.
76-
77-
Method determines if center of opencv face boxes is within face detection
78-
face boxes. Using math.hypot to measure the distance between the centers,
79-
as math.dist is not available for python versions before 3.8.
80-
81-
Args:
82-
faces_cropped: list of lists with (l, r, t, b) for each face.
83-
faces_opencv: list of lists with (x, y, w, h) for each face.
84-
mode: int indicating face detection mode
85-
img: np image array
86-
img_name: text string with path to image file
87-
"""
88-
# turn faces_opencv into list of center locations
89-
faces_opencv_center = [(x+w//2, y+h//2) for (x, y, w, h) in faces_opencv]
90-
cropped_faces_centers = [
91-
((l+r)//2, (t+b)//2) for (l, r, t, b) in faces_cropped]
92-
faces_opencv_center.sort(key=lambda t: [t[1], t[0]])
93-
cropped_faces_centers.sort(key=lambda t: [t[1], t[0]])
94-
logging.debug('cropped face centers: %s', str(cropped_faces_centers))
95-
logging.debug('opencv face center: %s', str(faces_opencv_center))
96-
faces_opencv_centers = []
97-
num_centers_aligned = 0
98-
99-
# eliminate duplicate openCV face rectangles' centers the same face
100-
faces_opencv_centers = eliminate_duplicate_centers(faces_opencv_center)
101-
logging.debug('opencv face centers: %s', str(faces_opencv_centers))
102-
103-
for (x, y) in faces_opencv_centers:
104-
for (x1, y1) in cropped_faces_centers:
105-
centers_dist = math.hypot(x-x1, y-y1)
106-
if centers_dist < _FACE_CENTER_MIN_LOGGING_DIST:
107-
logging.debug('centers_dist: %.3f', centers_dist)
108-
if (abs(x-x1) < _FACE_CENTER_MATCH_TOL_X and
109-
abs(y-y1) < _FACE_CENTER_MATCH_TOL_Y):
110-
num_centers_aligned += 1
111-
112-
# If test failed, save image with green AND OpenCV red rectangles
113-
image_processing_utils.write_image(img, img_name)
114-
if num_centers_aligned < _MIN_NUM_FACES_ALIGNED:
115-
for (x, y, w, h) in faces_opencv:
116-
cv2.rectangle(img, (x, y), (x+w, y+h), _CV2_RED, 2)
117-
image_processing_utils.write_image(img, img_name)
118-
logging.debug('centered: %s', str(num_centers_aligned))
119-
raise AssertionError(f'Mode {mode} face rectangles in wrong location(s)!. '
120-
f'Found {num_centers_aligned} rectangles near cropped '
121-
f'face centers, expected {_MIN_NUM_FACES_ALIGNED}')
122-
123-
12443
def check_face_bounding_box(rect, aw, ah, index):
12544
"""Checks face bounding box is within the active array area.
12645
@@ -187,32 +106,6 @@ def check_face_landmarks(face, fd_mode, index):
187106
raise AssertionError(f'Unknown face detection mode: {fd_mode}.')
188107

189108

190-
def correct_faces_for_crop(faces, img, crop):
191-
"""Correct face rectangles for sensor crop.
192-
193-
Args:
194-
faces: list of dicts with face information
195-
img: np image array
196-
crop: dict of crop region size with 'top, right, left, bottom' as keys
197-
Returns:
198-
list of face locations (left, right, top, bottom) corrected
199-
"""
200-
faces_corrected = []
201-
cw, ch = crop['right'] - crop['left'], crop['bottom'] - crop['top']
202-
logging.debug('crop region: %s', str(crop))
203-
w = img.shape[1]
204-
h = img.shape[0]
205-
for rect in [face['bounds'] for face in faces]:
206-
logging.debug('rect: %s', str(rect))
207-
left = int(round((rect['left'] - crop['left']) * w / cw))
208-
right = int(round((rect['right'] - crop['left']) * w / cw))
209-
top = int(round((rect['top'] - crop['top']) * h / ch))
210-
bottom = int(round((rect['bottom'] - crop['top']) * h / ch))
211-
faces_corrected.append([left, right, top, bottom])
212-
logging.debug('faces_corrected: %s', str(faces_corrected))
213-
return faces_corrected
214-
215-
216109
class NumFacesTest(its_base_test.ItsBaseTest):
217110
"""Test face detection with different skin tones.
218111
"""
@@ -281,7 +174,8 @@ def test_num_faces(self):
281174

282175
# draw boxes around faces in green
283176
crop_region = cap['metadata']['android.scaler.cropRegion']
284-
faces_cropped = correct_faces_for_crop(faces, img, crop_region)
177+
faces_cropped = opencv_processing_utils.correct_faces_for_crop(
178+
faces, img, crop_region)
285179
for (l, r, t, b) in faces_cropped:
286180
cv2.rectangle(img, (l, t), (r, b), _CV2_GREEN, 2)
287181

@@ -315,8 +209,8 @@ def test_num_faces(self):
315209
faces_opencv = opencv_processing_utils.find_opencv_faces(
316210
img, _CV2_FACE_SCALE_FACTOR, _CV2_FACE_MIN_NEIGHBORS)
317211
if fd_mode: # non-zero value for ON
318-
match_face_locations(faces_cropped, faces_opencv,
319-
fd_mode, img, img_name)
212+
opencv_processing_utils.match_face_locations(
213+
faces_cropped, faces_opencv, img, img_name)
320214

321215
if not faces:
322216
continue

apps/CameraITS/tests/scene2_d/test_autoframing.py

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
_AUTOFRAMING_CONVERGED = 2
3131
_CV2_FACE_SCALE_FACTOR = 1.05 # 5% step for resizing image to find face
3232
_CV2_FACE_MIN_NEIGHBORS = 4 # recommended 3-6: higher for less faces
33+
_NAME = os.path.splitext(os.path.basename(__file__))[0]
3334
_NUM_TEST_FRAMES = 150
3435
_NUM_FACES = 3
3536
_W, _H = 640, 480
@@ -84,21 +85,32 @@ def test_autoframing(self):
8485
# Face detection and autoframing could take several frames to warm up,
8586
# but should detect the correct number of faces before the last frame
8687
if autoframing_state == _AUTOFRAMING_CONVERGED:
88+
# Save image when autoframing state converges
89+
control_zoom_ratio = cap['metadata']['android.control.zoomRatio']
90+
logging.debug('Control zoom ratio: %d', control_zoom_ratio)
91+
img = image_processing_utils.convert_capture_to_rgb_image(
92+
cap, props=props)
93+
file_name_stem = os.path.join(self.log_path, _NAME)
94+
img_name = f'{file_name_stem}.jpg'
95+
96+
# Save images with green boxes around faces
97+
crop_region = cap['metadata']['android.scaler.cropRegion']
98+
faces_cropped = opencv_processing_utils.correct_faces_for_crop(
99+
faces, img, crop_region)
100+
opencv_processing_utils.draw_green_boxes_around_faces(
101+
img, faces_cropped, img_name)
102+
87103
num_faces_found = len(faces)
88104
if num_faces_found != _NUM_FACES:
89105
raise AssertionError('Wrong num of faces found! Found: '
90106
f'{num_faces_found}, expected: {_NUM_FACES}')
91107

92108
# Also check the faces with open cv to make sure the scene is not
93109
# distorted or anything.
94-
img = image_processing_utils.convert_capture_to_rgb_image(
95-
cap, props=props)
96110
opencv_faces = opencv_processing_utils.find_opencv_faces(
97111
img, _CV2_FACE_SCALE_FACTOR, _CV2_FACE_MIN_NEIGHBORS)
98-
num_opencv_faces = len(opencv_faces)
99-
if num_opencv_faces != _NUM_FACES:
100-
raise AssertionError('Wrong num of faces found with OpenCV! Found: '
101-
f'{num_opencv_faces}, expected: {_NUM_FACES}')
112+
opencv_processing_utils.match_face_locations(
113+
faces_cropped, opencv_faces, img, img_name)
102114
break
103115

104116
# Autoframing didn't converge till the last frame

apps/CameraITS/utils/opencv_processing_utils.py

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121
import cv2
2222
import numpy
2323

24+
from scipy.spatial import distance
25+
2426
import capture_request_utils
2527
import error_util
2628
import image_processing_utils
@@ -50,13 +52,20 @@
5052

5153
CV2_LINE_THICKNESS = 3 # line thickness for drawing on images
5254
CV2_RED = (255, 0, 0) # color in cv2 to draw lines
55+
CV2_GREEN = (0, 1, 0)
5356
CV2_THRESHOLD_BLOCK_SIZE = 11
5457
CV2_THRESHOLD_CONSTANT = 2
5558

5659
CV2_HOME_DIRECTORY = os.path.dirname(cv2.__file__)
5760
CV2_ALTERNATE_DIRECTORY = pathlib.Path(CV2_HOME_DIRECTORY).parents[3]
5861
HAARCASCADE_FILE_NAME = 'haarcascade_frontalface_default.xml'
5962

63+
FACES_ALIGNED_MIN_NUM = 2
64+
FACE_CENTER_MATCH_TOL_X = 10 # 10 pixels or ~1.5% in 640x480 image
65+
FACE_CENTER_MATCH_TOL_Y = 20 # 20 pixels or ~4% in 640x480 image
66+
FACE_CENTER_MIN_LOGGING_DIST = 50
67+
FACE_MIN_CENTER_DELTA = 15
68+
6069
FOV_THRESH_TELE25 = 25
6170
FOV_THRESH_TELE40 = 40
6271
FOV_THRESH_TELE = 60
@@ -907,3 +916,120 @@ def get_angle(input_img):
907916
return None
908917

909918
return numpy.median(filtered_angles)
919+
920+
921+
def correct_faces_for_crop(faces, img, crop):
922+
"""Correct face rectangles for sensor crop.
923+
924+
Args:
925+
faces: list of dicts with face information
926+
img: np image array
927+
crop: dict of crop region size with 'top, right, left, bottom' as keys
928+
Returns:
929+
list of face locations (left, right, top, bottom) corrected
930+
"""
931+
faces_corrected = []
932+
cw, ch = crop['right'] - crop['left'], crop['bottom'] - crop['top']
933+
logging.debug('crop region: %s', str(crop))
934+
w = img.shape[1]
935+
h = img.shape[0]
936+
for rect in [face['bounds'] for face in faces]:
937+
logging.debug('rect: %s', str(rect))
938+
left = int(round((rect['left'] - crop['left']) * w / cw))
939+
right = int(round((rect['right'] - crop['left']) * w / cw))
940+
top = int(round((rect['top'] - crop['top']) * h / ch))
941+
bottom = int(round((rect['bottom'] - crop['top']) * h / ch))
942+
faces_corrected.append([left, right, top, bottom])
943+
logging.debug('faces_corrected: %s', str(faces_corrected))
944+
return faces_corrected
945+
946+
947+
def eliminate_duplicate_centers(coordinates_list):
948+
"""Checks center coordinates of OpenCV's face rectangles
949+
950+
Method makes sure that the list of face rectangles' centers do not
951+
contain duplicates from the same face
952+
953+
Args:
954+
coordinates_list: list; coordinates of face rectangles' centers
955+
Returns:
956+
non_duplicate_list: list; coordinates of face rectangles' centers
957+
without duplicates on the same face
958+
"""
959+
output = set()
960+
961+
for i, xy1 in enumerate(coordinates_list):
962+
for j, xy2 in enumerate(coordinates_list):
963+
if distance.euclidean(xy1, xy2) < FACE_MIN_CENTER_DELTA:
964+
continue
965+
if xy1 not in output:
966+
output.add(xy1)
967+
else:
968+
output.add(xy2)
969+
return list(output)
970+
971+
972+
def match_face_locations(faces_cropped, faces_opencv, img, img_name):
973+
"""Assert face locations between two methods.
974+
975+
Method determines if center of opencv face boxes is within face detection
976+
face boxes. Using math.hypot to measure the distance between the centers,
977+
as math.dist is not available for python versions before 3.8.
978+
979+
Args:
980+
faces_cropped: list of lists with (l, r, t, b) for each face.
981+
faces_opencv: list of lists with (x, y, w, h) for each face.
982+
img: numpy [0, 1] image array
983+
img_name: text string with path to image file
984+
"""
985+
# turn faces_opencv into list of center locations
986+
faces_opencv_center = [(x+w//2, y+h//2) for (x, y, w, h) in faces_opencv]
987+
cropped_faces_centers = [
988+
((l+r)//2, (t+b)//2) for (l, r, t, b) in faces_cropped]
989+
faces_opencv_center.sort(key=lambda t: [t[1], t[0]])
990+
cropped_faces_centers.sort(key=lambda t: [t[1], t[0]])
991+
logging.debug('cropped face centers: %s', str(cropped_faces_centers))
992+
logging.debug('opencv face center: %s', str(faces_opencv_center))
993+
faces_opencv_centers = []
994+
num_centers_aligned = 0
995+
996+
# eliminate duplicate openCV face rectangles' centers the same face
997+
faces_opencv_centers = eliminate_duplicate_centers(faces_opencv_center)
998+
logging.debug('opencv face centers: %s', str(faces_opencv_centers))
999+
1000+
for (x, y) in faces_opencv_centers:
1001+
for (x1, y1) in cropped_faces_centers:
1002+
centers_dist = math.hypot(x-x1, y-y1)
1003+
if centers_dist < FACE_CENTER_MIN_LOGGING_DIST:
1004+
logging.debug('centers_dist: %.3f', centers_dist)
1005+
if (abs(x-x1) < FACE_CENTER_MATCH_TOL_X and
1006+
abs(y-y1) < FACE_CENTER_MATCH_TOL_Y):
1007+
num_centers_aligned += 1
1008+
1009+
# If test failed, save image with green AND OpenCV red rectangles
1010+
image_processing_utils.write_image(img, img_name)
1011+
if num_centers_aligned < FACES_ALIGNED_MIN_NUM:
1012+
for (x, y, w, h) in faces_opencv:
1013+
cv2.rectangle(img, (x, y), (x+w, y+h), tuple(numpy.array(CV2_RED)/255), 2)
1014+
image_processing_utils.write_image(img, img_name)
1015+
logging.debug('centered: %s', str(num_centers_aligned))
1016+
raise AssertionError(f'Face rectangles in wrong location(s)!. '
1017+
f'Found {num_centers_aligned} rectangles near cropped '
1018+
f'face centers, expected {FACES_ALIGNED_MIN_NUM}')
1019+
1020+
1021+
def draw_green_boxes_around_faces(img, faces_cropped, img_name):
1022+
"""Correct face rectangles for sensor crop.
1023+
1024+
Args:
1025+
img: numpy [0, 1] image array
1026+
faces_cropped: list of lists with (l, r, t, b) for each face
1027+
img_name: text string with path to image file
1028+
Returns:
1029+
image with green rectangles
1030+
"""
1031+
# draw boxes around faces in green and save image
1032+
for (l, r, t, b) in faces_cropped:
1033+
cv2.rectangle(img, (l, t), (r, b), CV2_GREEN, 2)
1034+
image_processing_utils.write_image(img, img_name)
1035+

0 commit comments

Comments
 (0)