def track_faces(
    self,
    clip_dir: str,
    out_base_dir: str,
    draw_on_dir: str = None,
    detect_only: bool = False,
):
    """
    This is NOT recognition
    Tracking should be based on smooth object motion, not face recognition

    Steps Every frame:

    detect faces
    for old-face in tracked-faces:
        for new-face in detected-faces:
            if new-face in old-face-region and old-face in new-face-region:
                match new-face and old-face
                break
        if old-face not in matches and tracker.update(img) > thresh:
            match to tracked location

    for new-face not in matches:
        create new tracked-face
    """
    # Setup
    # load image paths
    frames: List[os.DirEntry] = load_and_sort_dir(clip_dir)
    draw_on_frames: List[os.DirEntry] = load_and_sort_dir(draw_on_dir)
    assert len(draw_on_frames) in (0, len(frames))

    # create output directory
    out_dir: str = create_output_dir(out_base_dir)

    # initialize variables required for object tracking
    new_face_id: Iterator[int] = count(start=1)
    tracked_faces: Dict[int, TrackedFace] = {}

    prev_img = None

    # Iterate Through Video Frames
    for frame, draw_on_frame in zip_longest(frames, draw_on_frames):
        # Read Images
        # read image to process
        img: np.ndarray = cv.imread(frame.path)
        # read image to draw on (if different)
        out_img = img.copy() if draw_on_frame is None else cv.imread(
            draw_on_frame.path)
        # ensure out_img is at least as large as img
        assert len(img.shape) == len(out_img.shape) and all(
            out_dim >= in_dim
            for in_dim, out_dim in zip(img.shape, out_img.shape))

        detected_face_boxes: List[Box] = self.detect_faces(img)

        # If tracking is disabled, draw the boxes and move to next frame
        if detect_only:
            write_boxes(
                out_path=os.path.join(out_dir, frame.name),
                out_img=out_img,
                boxes=detected_face_boxes,
            )
            continue

        current_ids_to_detection_idx: Dict[int, Optional[int]] = {}
        lost_tracked_face_ids: List[int] = []

        # Iterate over the known (tracked) faces
        for tracked_face in tracked_faces.values():
            # Update the tracker with the new image
            # Tracker generates new predicted_rect from previous predicted_rect
            # Tracker returns its confidence that the face is inside new predicted_rect
            predicted_rect_confidence: float = tracked_face.tracker.update(img)
            if predicted_rect_confidence < self.tracking_threshold:
                # We've lost te object. Maybe due to a cut. Can't simply look for closest faces.
                # We assume the face is no longer present in img and stop tracking it
                print(
                    f"Too low: id={tracked_face.id_}, conf={predicted_rect_confidence}, frame={frame.name}"
                )
                lost_tracked_face_ids.append(tracked_face.id_)
                # TODO: In this case, maybe matchTemplate with found faces to see if one is above thresh
                continue
            predicted_rect: dlib.rectangle = tracked_face.tracker.get_position(
            )
            tracked_last_rect: dlib.rectangle = tracked_face.box.to_dlib_rect()

            # Iterate over newly detected faces
            for detected_i, detected_face_box in enumerate(
                    detected_face_boxes):

                # TODO Maybe just do distance based
                #  add confidence here?
                #  I think track motion and distance
                detected_rect = detected_face_box.to_dlib_rect()

                if (
                        # TODO: verify these are good checks. Maybe check that the l2 dist is minimal instead
                        #  need to make sure not modifying tracked faces as we go if we start computing minimums
                        #  THEY ARENT
                        # sanity check: face hasn't moved too much
                        tracked_last_rect.contains(detected_rect.center())
                        and detected_rect.contains(tracked_last_rect.center())
                        # sanity check: tracker prediction isn't too far from detection
                        and detected_rect.contains(predicted_rect.center()) and
                        predicted_rect.contains(detected_rect.center())):

                    # detected_face_box and tracked_face are the same face
                    # tracker was already update to this location
                    if tracked_face.id_ in current_ids_to_detection_idx:
                        print(
                            f'[ERROR]  {tracked_face.id_} found multiple times. Keeping first match'
                        )
                    else:
                        tracked_face.box = detected_face_box
                        current_ids_to_detection_idx[
                            tracked_face.id_] = detected_i
                        new_tracker = dlib.correlation_tracker()
                        new_tracker.start_track(image=img,
                                                bounding_box=detected_rect)
                        tracked_face.tracker = new_tracker

            if tracked_face.id_ not in current_ids_to_detection_idx:
                assert predicted_rect_confidence >= self.tracking_threshold
                # Didn't detect this face, but tracker is confident it is at the predicted location.
                # We assume detector gave false negative
                tracked_face.box = Box.from_dlib_rect(predicted_rect)
                # tracker was updated to predicted_rect in update() call in condition
                current_ids_to_detection_idx[tracked_face.id_] = None

        # Remove lost face ids
        for lost_tracked_face_id in lost_tracked_face_ids:
            del tracked_faces[lost_tracked_face_id]

        tracked_detection_idxs = current_ids_to_detection_idx.values()

        # Track new faces
        for detected_i, detected_face_box in enumerate(detected_face_boxes):
            if detected_i not in tracked_detection_idxs:
                # Assume new face has entered frame and start tracking it
                id_ = next(new_face_id)
                tracker: dlib.correlation_tracker = dlib.correlation_tracker()
                tracker.start_track(
                    image=img, bounding_box=detected_face_box.to_dlib_rect())
                tracked_faces[id_] = TrackedFace(id_=id_,
                                                 box=detected_face_box,
                                                 tracker=tracker)
                current_ids_to_detection_idx[id_] = detected_i

        tracked_detection_idxs = current_ids_to_detection_idx.values()
        assert all(i in tracked_detection_idxs
                   for i in range(len(detected_face_boxes)))
        assert len(current_ids_to_detection_idx) == len(tracked_faces)

        write_boxes(
            out_path=os.path.join(out_dir, frame.name),
            out_img=out_img,
            boxes=[face.box for face in tracked_faces.values()],
            labelss=[[(f'Person {face.id_}', Point(1, -9))]
                     for face in tracked_faces.values()],
        )
示例#2
0
    def track_faces(
        self,
        clip_dir: str,
        out_base_dir: str,
        draw_on_dir: str = None,
        detect_only: bool = False,
    ):
        # Setup
        # load image paths
        frames: List[os.DirEntry] = load_and_sort_dir(clip_dir)
        draw_on_frames: List[os.DirEntry] = load_and_sort_dir(draw_on_dir)
        assert len(draw_on_frames) in (0, len(frames))

        # create output directory
        out_dir: str = create_output_dir(out_base_dir)

        # initialize variables required for object tracking
        new_face_id: Iterator[int] = count(start=1)
        tracked_faces: Dict[int, TrackedFace] = {}

        # Iterate Through Video Frames
        for frame, draw_on_frame in zip_longest(frames, draw_on_frames):
            # load new frame
            img = cv.imread(frame.path)

            # load out_img
            out_img: np.ndarray = (img.copy() if draw_on_frame is None else
                                   cv.imread(draw_on_frame.path))

            # ensure out_img is at least as large as img
            assert len(img.shape) == len(out_img.shape) and all(
                out_dim >= in_dim
                for in_dim, out_dim in zip(img.shape, out_img.shape))

            detected_face_boxes: List[Box] = self.detect_face_boxes(img)

            # If tracking is disabled, draw the boxes and move to next frame
            if detect_only:
                write_boxes(
                    out_path=os.path.join(out_dir, frame.name),
                    out_img=out_img,
                    boxes=detected_face_boxes,
                )
                continue

            detected_faces: List[GenderedFace] = gender_faces(
                img=img,
                faces=[
                    self.recognize_face(img, detected_face_box)
                    for detected_face_box in detected_face_boxes
                ],
            )

            current_face_ids: Set[int] = set()
            lost_face_ids: Set[int] = set()

            # Iterate over the known (tracked) faces
            for tracked_face in tracked_faces.values():
                matched_detected_faces: List[GenderedFace] = [
                    detected_face for detected_face in detected_faces
                    if self.faces_match(tracked_face, detected_face)
                ]

                if not matched_detected_faces:
                    # Tracked face was not matched to and detected face
                    # Increment staleness since we didn't detect this face
                    tracked_face.staleness += 1
                    # Update tracker with img and get confidence
                    tracked_confidence: float = tracked_face.tracker.update(
                        img)
                    if (tracked_face.staleness < self.tracking_expiry
                            and tracked_confidence >= self.tracking_threshold):
                        # Assume face is still in frame but we failed to detect
                        # Update box with predicted location box
                        predicted_box: Box = Box.from_dlib_rect(
                            tracked_face.tracker.get_position())
                        tracked_face.box = predicted_box
                        current_face_ids.add(tracked_face.id_)
                    else:
                        # Assume face has left frame because either it is too stale or confidence is too low
                        if self.remember_identities:
                            # Set effectively infinite staleness to force tracker reset if face is found again later
                            tracked_face.staleness = sys.maxsize
                        else:
                            lost_face_ids.add(tracked_face.id_)
                    continue

                # Tracked face was matched to one or more detected faces
                # Multiple matches should rarely happen if faces in frame are distinct. We take closest to prev location
                # TODO: Handle same person multiple times in frame
                matched_detected_face = min(
                    matched_detected_faces,
                    key=lambda face: tracked_face.box.distance_to(face.box),
                )
                # Update tracked_face
                tracked_face.descriptor = matched_detected_face.descriptor
                tracked_face.shape = matched_detected_face.descriptor
                tracked_face.box = matched_detected_face.box
                if tracked_face.staleness >= self.tracking_expiry:
                    # Face was not present in last frame so reset tracker
                    tracked_face.tracker = dlib.correlation_tracker()
                    tracked_face.tracker.start_track(
                        image=img,
                        bounding_box=tracked_face.box.to_dlib_rect())
                else:
                    # Face was present in last frame so just update guess
                    tracked_face.tracker.update(
                        image=img, guess=tracked_face.box.to_dlib_rect())
                tracked_face.staleness = 0
                tracked_face.gender = matched_detected_face.gender
                tracked_face.gender_confidence = matched_detected_face.gender_confidence
                # Add tracked_face to current_ids to reflect that it is in the frame
                current_face_ids.add(tracked_face.id_)
                # remove matched_detected_face from detected_faces
                detected_faces.remove(matched_detected_face)

            # Delete all faces that were being tracked but are now lost
            # lost_face_ids will always be empty if self.remember_identities is True
            for id_ in lost_face_ids:
                del tracked_faces[id_]

            for new_face in detected_faces:
                # This is a new face (previously unseen)
                id_ = next(new_face_id)
                tracker: dlib.correlation_tracker = dlib.correlation_tracker()
                tracker.start_track(image=img,
                                    bounding_box=new_face.box.to_dlib_rect())
                tracked_faces[id_] = TrackedFace(
                    box=new_face.box,
                    descriptor=new_face.descriptor,
                    shape=new_face.shape,
                    id_=id_,
                    tracker=tracker,
                    gender=new_face.gender,
                    gender_confidence=new_face.gender_confidence,
                )
                current_face_ids.add(id_)

            write_boxes(
                out_path=os.path.join(out_dir, frame.name),
                out_img=out_img,
                boxes=[tracked_faces[id_].box for id_ in current_face_ids],
                labelss=[[
                    (
                        f'Person {id_}',
                        Point(3, 14),
                    ),
                    (
                        f'{tracked_faces[id_].gender.name[0].upper()}: {round(100 * tracked_faces[id_].gender_confidence, 1)}%',
                        Point(3, 30),
                    ),
                ] for id_ in current_face_ids],
                color=Color.yellow(),
            )

            print(
                f"Processed {frame.name}.  Currently tracking {len(tracked_faces)} faces"
            )
        return out_dir