Python + OpenCV로 야구 공을 추적

오랜만에 OpenCV의 사이트를 보면 새롭게 되었으므로, 조금 만져 보았습니다.

대부분 전에 자바에서 야구 공을 추적하고 있었기 때문에 이번에는 파이썬으로 시도했습니다.
추적은 배경 차이와 템플릿 매칭을 결합하여 수행됩니다.

환경 구축



환경은 다음과 같습니다.
  • Python : 3.8.0
  • OpentCV : 4.2.0
  • venv 에서 환경을 만들고 있습니다. venv 그렇지 않아도 문제 없습니다.
    $ python -m venv pythonOpevCV
    $ cd pythonOpenCV
    $ Script\activate
    $ python -m pip install --upgrade pip
    $ pip install opencv-python
    Successfully installed numpy-1.18.2 opencv-python-4.2.0.34
    
    numpy 도 설치됩니다.

    준비하는 것



    다음을 준비하십시오.
  • 야구 공이 비치는 동영상
  • 야구 공을 잘라낸 이미지 (템플릿 이미지)

  • 공을 잘라낸 이미지는 다음과 같이 괜찮습니다. 이하 견본입니다.



    절차



    다음 절차에 따라 추적을 수행합니다.
  • 비디오 프레임 당 이미지로 저장
  • 프레임 이미지와 템플릿 이미지를 그레이 스케일링
  • 프레임 이미지와 템플릿 이미지를 이진화
  • 앞뒤 프레임 이미지에서 배경 차이를 수행합니다
  • 배경 차이의 결과 이미지에서 템플릿 일치를 수행합니다.
  • 추적 결과 그리기

  • 갑자기 오리지날의 프레임 화상에 대해서 템플릿 매칭을 실시해도, 구름이나 배경의 건물을 볼과 잘못해 검출해 버려, 정밀도가 좋지 않기 때문에 그레이 스케일→2치화로 흑백의 화상으로 합니다. 그 흑백 화상 상태에서 전후의 프레임으로 배경 차분을 실시하면, 구름이나 배경은 0.1초 정도에서는 거의 움직이지 않기 때문에 이동하고 있는 볼을 확실히 검출할 수 있습니다.

    회색조 이미지는 다음과 같습니다.



    빨간색 테두리로 둘러싸인 범위에 있는 공을 감지합니다. 이 이미지를 이진화하여 흑백 이미지로 만듭니다.



    공을 흰색으로 감지할 수 있지만 배경과 공이 같은 흰색이기 때문에 공만을 감지할 수 없습니다. 여기에서 전후의 프레임 화상으로 배경 차분을 실시하면 이하와 같이 됩니다.



    배경의 흰색은 작동하지 않으므로 배경 차이로는 감지되지 않으며 공과 기타 노이즈만 흰색으로 감지됩니다. 이 상태에서 다음과 같은 템플릿 이미지를 2진화한 이미지와 템플릿 매칭을 실시합니다.



    이렇게 하면 광도나 배경의 영향을 받지 않고 볼을 검출할 수 있습니다.

    소스 코드



    소스 코드는 다음과 같습니다. VIDEOPATH 에 대상 동영상을, TEMPLATEPATH 에 템플릿 이미지를 배치하십시오.

    main.py
    import glob
    import re
    import cv2
    
    
    VIDEOPATH = "media/video/video.mp4"
    IMAGEPATH = "media/image/"
    TEMPLATEPATH = "template.jpeg"
    
    
    def save_frames(video_path, image_dir):
        """
        動画からフレームの画像を抽出
        """
        cap = cv2.VideoCapture(video_path)
        digit = len(str(int(cap.get(cv2.CAP_PROP_FRAME_COUNT))))
        n = 0
        while True:
            ret, frame = cap.read()
            if ret:
                cv2.imwrite("{}original/frame_{}.{}".format(IMAGEPATH, n, "jpeg"), frame)
                n += 1
            else:
                return
    
    
    def do_grayscale(image_path):
        """
        画像をグレースケール化
        """
        img = cv2.imread(image_path)
        gray = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY)
        save_image(image_path, "gray", gray)
    
    
    def do_binarization(image_path):
        """
        画像を2値化
        """
        img = cv2.imread(image_path)
        ret, img_thresh = cv2.threshold(img, 100, 255, cv2.THRESH_BINARY)
        save_image(image_path, "binary", img_thresh)
    
    
    def do_backgroundsub():
        """
        背景差分を行う
        """
        img_list = glob.glob(IMAGEPATH + "binary/frame*.jpeg")
        num = lambda val: int(re.sub("\D","",val))
        sorted(img_list,key=(num))
        source = img_list[0]
        for path in img_list:
            diff = cv2.absdiff(cv2.imread(source),cv2.imread(path))
            source = path
            save_image(path, "bgsub", diff)
    
    
    def do_template_matching():
        """
        テンプレート画像とフレーム画像でテンプレートマッチングを行う
        """
        template_img = cv2.imread(IMAGEPATH + "binary/" + TEMPLATEPATH)
        img_list = glob.glob(IMAGEPATH + "bgsub/frame*.jpeg")
        num = lambda val: int(re.sub("\D","",val))
        sorted(img_list,key=(num))
        location_list = []
        for path in img_list:
            result = cv2.matchTemplate(cv2.imread(path), template_img, cv2.TM_CCOEFF)
            minVal, maxVal, minLoc, maxLoc = cv2.minMaxLoc(result)
            location_list.append(maxLoc)
        return location_list
    
    def draw_rectangle(location_list):
        """
        マッチング結果を画像に描画する
        """
        source = cv2.imread(IMAGEPATH + "original/frame_0.jpeg")
        cv2.imwrite(IMAGEPATH + "result.jpeg",source)
        source = cv2.imread(IMAGEPATH + "result.jpeg")
        for loc in location_list:
            lx, ly, rx, ry = loc[0] - 10, loc[1] - 10, loc[0] + 10, loc[1] + 10
            img = cv2.rectangle(source, (lx, ly), (rx, ry), (0, 255, 0), 3)
            cv2.imwrite(IMAGEPATH + "result.jpeg",img)
    
    def save_image(img_path, dir, img):
        """
        画像を保存する
        img_path : 画像のパス
        dir : ディレクトリ名
        img : 画像データ
        """
        file_name = img_path.replace("\\","/").split(".")[0].split("/")[-1]
        cv2.imwrite("{}{}/{}.{}".format(IMAGEPATH, dir, file_name,"jpeg"), img)
    
    
    if __name__=="__main__":
        # ①動画をフレームごとに分割
        save_frames(VIDEOPATH,IMAGEPATH)
        # ②テンプレート画像とフレーム画像をグレースケール化
        do_grayscale(IMAGEPATH + TEMPLATEPATH)
        for path in glob.glob(IMAGEPATH + "original/*.jpeg"):
            do_grayscale(path)
        # ③テンプレート画像とフレーム画像の2値化
        for path in glob.glob(IMAGEPATH + "gray/*.jpeg"):
            do_binarization(path)
        # ④背景差分を行う
        do_backgroundsub()
        # ⑤テンプレートマッチングを行う
        location_list = do_template_matching()
        # ⑥マッチングした座標を投影
        draw_rectangle(location_list)
    
    

    결과



    투구 동영상으로 공을 검출해 보았습니다. 일반적으로 검출할 수 있습니다만, 볼의 궤도 이외의 부분도 검출하고 있습니다.



    노이즈나 이상치를 수정하는 방법이 있었는데 잊었기 때문에 다시 기억합니다.

    요약



    Python + OpenCV에서 배경 차이와 템플릿 매칭을 사용하여 야구 공을 추적했습니다.YOLO 를 사용하면 이미지 안에서 야구공을 검출할 수 있는 것 같아서 이 근처도 시도해보고 싶습니다.

    좋은 웹페이지 즐겨찾기