OpenVINO の Person-reidentification(人再認識)モデルを使って人を追跡する


はじめに

OpenVINO の Person-reidentification(人再認識)モデルを使用して、人を追跡するプログラムを作りました。進む方向によって人の出入りをカウントすることもできます。なかなか苦労したので、実装した内容をまとめておきます。

画像はYouTubeのリンクになっています

テストに使用した動画は Videezy からダウンロードしました。4K 動画なので こちらのサイトで 1920 x 1080 に縮小しました。

コードは GitHub に上げました。

Intel の Pedestrian Tracker Demo

そもそもの元ネタである、Intel の Pedestrian Tracker Demo は、 エスカレーターで移動する人を追跡する動画になっていて YouTube(注:音が出ます) で 見られます。

Pedestrian Tracker Demo

Intelのデモ1と同じ動画 (1920 x 1080) でテストした様子です。本家より追跡の精度が悪い(私の実装の問題)ですが、まあまあ再現できてると思います。

画像はYouTubeのリンクになっています。オリジナルの動画は、Pexels Videos からダウンロード2できます。

環境

  • Python 3.7.6
  • Windows 10 CPU Intel(R) Core(TM) i5-7200
  • OpenVINO Toolkit 2020.4.117

難しかったところ

人間は(連続する時間において)無意識に、簡単に「同じ人」を認識できますが、コンピュータにそれをさせるのは簡単ではありません。検知した「人」を連続するフレーム間で「同じ人」として認識させる仕組みが必要です。

「同じ人」として認識するために、Intel の person-reidentification モデルを使います。基本的な使用方法は、顔の再認識 face-reidentificationと同じ3です。

このモデルは 検出した人の特徴ベクトルを出力するので 複数のベクトル間のコサイン類似度から同一性を判断できるわけですが、動きが速い、激しい、人が重なる など、再認識においてタフな状況をクリアする必要があります。

単純にコサイン類似度を閾値として判定した結果がこちらです。人が重なると「追跡」がロストして 意図せず別人として登録されてしまいます。閾値を下げて同一人物と判定する条件を低くすれば解決しそうに思えますが、今度は別の人を同一人物と扱ってしまうため「追跡」がうまく機能しなくなります。

実装のポイント

実装のポイントと思った点を以下に記載します。これらは Intel のデモプログラムを参考にしました。4

1. コサイン類似度の閾値
 ・コサイン類似度が高い(同一人物である可能性が高い)場合は、「同一」と扱う。0.6 程度がちょうど良さそう。
2. 除外条件は大切
 ・質の高い特徴ベクトルを保持するため、人が重なっている場合は特徴ベクトルのアップデートや登録を行わない。
3. 複合的な条件を組み合わせる
 ・コサイン類似度が低い場合でも、複合的な条件を組み合わせて同一性を保持する(「同一と見做す人」の特徴ベクトルや現在位置をアップデートする)。
 ・複合的な条件として「同一と見做す人」の 現在と直近フレームの中心点間の距離(ユークリッド距離)、Bounding BOX の重なり度合(IoU : Intersection over Union) を利用する。
4. 余計な情報は保持しない
 ・
特徴ベクトルの比較精度を高めるため 予め定義したTracking 範囲から外れた人のTracking 情報はクリアする。
5. 新しい人として登録する人の数を抑制する
 ・上記の条件以外であれば、新たな人として登録する。

※一番最初に掲載したGIF画像では、1. のコサイン類似度の閾値は 0.6、2. のIoU 閾値は 0.2(2割重なったら処理をスキップ) 、3.のやや低めのコサイン類似度の閾値は 0.3 としました。

この部分の抜粋です。上記の実装をしても 誤検知(FN・FP)は多いので、まだ追跡を維持する工夫が必要です。

tracker.py
    def person_reidentification(self, frame, persons, person_frames, boxes):

        if not person_frames:
            frame = self.draw_params(frame)
            frame = self.draw_couter_stats(frame)
            return frame

        feature_vecs = self.get_feature_vecs(person_frames[:reid_limit])

        # at the first loop
        if self.person_vecs is None:
            self.fisrt_detection(feature_vecs, boxes)

        similarities = cos_similarity(feature_vecs, self.person_vecs)
        similarities[np.isnan(similarities)] = 0
        person_ids = np.nanargmax(similarities, axis=1)

        for det_id, person_id in enumerate(person_ids):
            center = np.array(self.get_center_of_person(boxes[det_id]))

            # get the closest location of the person from track points
            track_points = np.array(
                [track_point[-1] for track_point in self.track_points]
            )
            closest_id, closest_distance = self.closest_distance(center, track_points)

            # get cosine similarity and check if the person frames are overlapped
            similarity = similarities[det_id][person_id]
            is_overlap = self.is_overlap(det_id, boxes)

            # 1. most likely the same person
            if similarity > sim_thld:
                self.update_tracking(
                    person_id, feature_vecs[det_id].reshape(1, 256), boxes[det_id]
                )
                frame = self.person_is_matched(
                    frame, person_id, boxes[det_id], similarity
                )
            # 2. nothing to do when person is out of counter area
            elif not is_overlap:
                # 3. apply minumum similarity threshold when a person is in the closest distance
                #      (Euclidean distance) with their lastet saved track points
                if similarity >= min_sim_thld and closest_id == person_id:
                    self.update_tracking(
                        person_id, feature_vecs[det_id].reshape(1, 256), boxes[det_id]
                    )
                # 4. nothing to do when person is out of counter area
                elif self.is_out_of_couter_area(center):
                    continue
                # 5. finally register a new person
                else:
                    self.register_person_vec(
                        feature_vecs[det_id], self.prev_feature_vecs, boxes[det_id]
                    )

        self.prev_feature_vecs = feature_vecs
        frame = self.draw_couter_stats(frame)
        frame = self.draw_params(frame)
        return frame

まとめ

実装してみての感想となりますが、自分なりに気づいたことが1つあります。

思った結果が安定して出ない時は、パラメータをちょこちょこと変えるレベルではダメで、実装を見直した方が良いということです。

パラメータの組み合わせを変え始めたら、ある程度のところで辞めないと時間がいくらあっても足りません。うまくいかない時には類似度の閾値を 0.05 単位でいじって、やみくもに何度もリトライしました。もはや思考停止状態です。仮にちょっとうまくいったように見えても他の動画で試すと全くダメだったりします。

今回はお手本プログラムがあったので助かりましたが、随分と無駄な時間を費やしてしまいました。でもこれもきっと必要な時間なのだと考えることにします。


  1. OpenVINOをインストールしている方は[OpenVINO Install Path]\openvino\deployment_tools\open_model_zoo\demos\pedestrian_tracker_demo にプログラムがあります。デモプログラムはC++で書かれています。 Python で再現しようと思いましたが、私には無理と判断したので何となく仕組みを真似をしたものが本記事の内容です。ですので、本家のデモプログラムとは実装は異なります。 

  2. escalator で検索すると見つけられます。 

  3. 詳細はOpenVINO で Face re-identification (顔再識別)を参考にしていただければと思います。 

  4. デモプログラムでは、Bounding Box の「形」、「動き」、「時間」による同一性の判断や、「忘れる」ための仕組みも実装しているようでもっと緻密な判定をしています。