본문 바로가기

Data-science/deep learning

[detection] 이미지 labeling이 이상하다 싶지만 정상일 때 해결방법. PIL Image rotated 회전시키는 녀석. (PIL image 쓸데 없이 고성능)

반응형

문제점.  AP가 0.9점 대로 굉장히 높음에도 불구하고 테스트 셋에 대한 성능이 극악했다. 이유가 뭘까 고심해보았고, 학습 세트 레이블링이 잘못된 건 아닐까 하고 뜯어보았다. 

1. PIL image open을 이용하여 이미지와 레이블을 읽어 보니 레이블링이 엉망이었다.

PASCAL VOC 레이블링을 읽고 다음과 같이 DataFrame 형태로 저장해둔다.

df = pd.DataFrame()
for i, file in tqdm(enumerate(selcet_files)):
    tree = elemTree.parse(file)
    bbox = tree.find('object').find('bndbox')
    filename = tree.find('filename').text
    xmin = bbox.find('xmin').text
    ymin = bbox.find('ymin').text
    xmax = bbox.find('xmax').text
    ymax = bbox.find('ymax').text
    objectname = tree.find('object').find('name').text
    df.loc[i, 'image'] = filename
    df.loc[i, 'name'] = objectname
    df.loc[i, 'xmin'] = xmin
    df.loc[i, 'ymin'] = ymin
    df.loc[i, 'xmax'] = xmax
    df.loc[i, 'ymax'] = ymax

그러면 열어보면 df는 아래와 같은 형태로 나타난다. 이제 이를 하나씩 읽고 bounding box를 이미지 내에 표현해보자.

import matplotlib.pyplot as plt
from matplotlib import patches

def get_rectangle_edges_from_pascal_bbox(bbox):
    xmin_top_left, ymin_top_left, xmax_bottom_right, ymax_bottom_right = bbox

    bottom_left = (xmin_top_left, ymax_bottom_right)
    width = xmax_bottom_right - xmin_top_left
    height = ymin_top_left - ymax_bottom_right

    return bottom_left, width, height

def draw_pascal_voc_bboxes(
    plot_ax,
    bboxes,
    get_rectangle_corners_fn=get_rectangle_edges_from_pascal_bbox,
):
    for bbox in bboxes:
        bottom_left, width, height = get_rectangle_corners_fn(bbox)

        rect_1 = patches.Rectangle(
            bottom_left,
            width,
            height,
            linewidth=4,
            edgecolor="black",
            fill=False,
        )
        rect_2 = patches.Rectangle(
            bottom_left,
            width,
            height,
            linewidth=2,
            edgecolor="white",
            fill=False,
        )

        # Add the patch to the Axes
        plot_ax.add_patch(rect_1)
        plot_ax.add_patch(rect_2)

def show_image(
    image, bboxes=None, draw_bboxes_fn=draw_pascal_voc_bboxes, figsize=(10, 10)
):
    fig, ax = plt.subplots(1, figsize=figsize)
    ax.imshow(image)

    if bboxes is not None:
        draw_bboxes_fn(ax, bboxes)

    plt.show()

 

from pathlib import Path

import PIL

import numpy as np

class CarsDatasetAdaptor:
    def __init__(self, images_dir_path, annotations_dataframe):
        self.images_dir_path = Path(images_dir_path)
        self.annotations_df = annotations_dataframe
        self.images = self.annotations_df.image.unique().tolist()

    def __len__(self) -> int:
        return len(self.images)

    def get_image_and_labels_by_idx(self, index):
        image_name = self.images[index]
        image = PIL.Image.open(self.images_dir_path / image_name)
#         image = ImageOps.exif_transpose(image)
#         image = image.transpose(PIL.Image.FLIP_TOP_BOTTOM)
#         image = image.transpose(PIL.Image.FLIP_LEFT_RIGHT)
        pascal_bboxes = self.annotations_df[self.annotations_df.image == image_name][
            ["xmin", "ymin", "xmax", "ymax"]
        ].values
        class_labels = np.ones(len(pascal_bboxes))

        return image, pascal_bboxes, class_labels, index
    
    def show_image(self, index):
        image, bboxes, class_labels, image_id = self.get_image_and_labels_by_idx(index)
        print(f"image_id: {image_id}")
        show_image(image, bboxes.tolist())
        print(class_labels)
cars_train_ds = CarsDatasetAdaptor(train_data_path/'../JPEGImages/', df)

이미지 레이블링이 뭔가 규칙적으로 개떡같다... 

처음에는 레이블링이 잘못된줄 알았다. 그랬지만....

2. 레이블링 툴을 이용하여 학습 데이터 셋을 읽을 경우 레이블링이 잘 되어있음을 확인하였다. (레이블링 문제는 아님)

내 코드에 뭐가 잘못된 걸까? 확인에 확인을 거듭해봤는데 이상한 점은 없었다.

3. 학습 모델이 학습 세트는 제대로 디텍팅하는지 확인해봤다.

 

그러다가 가로 세로가 뒤 바뀐게 아닌가?하는 생각이 들었다.

PIL image rotated로 검색을 해보니... 비슷한 현상을 경험한 누군가가 있었다.

https://stackoverflow.com/questions/4228530/pil-thumbnail-is-rotating-my-image

 

PIL thumbnail is rotating my image?

I'm attempting to take large (huge) images (from a digital camera), and convert them into something that I can display on the web. This seems straightforward, and probably should be. However, when I

stackoverflow.com

When a picture is taller than it is wide, it means the camera was rotated. Some cameras can detect this and write that info in the picture's EXIF metadata. Some viewers take note of this metadata and display the image appropriately.

PIL can read the picture's metadata, but it does not write/copy metadata when you save an Image. Consequently, your smart image viewer will not rotate the image as it did before.

이미지가 길쭉하면 카메라가 회전되어 있다는 걸 의미한단다. 어떤 카메라는 이런 메타정보까지 읽어서 저장한다. PIL은 이 사진의 메타정보를 읽어서 길쭉한걸 회전시켜서 읽어버린다...

그렇다 문제의 원인은 PIL.Image.open()이 쓸 데없이 고성능인 데 있다.

해결 방안은 코드 한 줄이다.

from PIL import Image, ImageOps

original_image = Image.open(filename)

fixed_image = ImageOps.exif_transpose(original_image)

그렇다면 학습도 이상하게 된 것일까? 학습 시 데이터 제너레이터에 위 코드를 적용시켜보고 ImageOps.exif_transpose전후를 비교해보았다. 전에는 넓찍하게 나온다. 원래 길쭉한 이미지인데... 저리하면 bounding box값도 이상하게 되지... 변환하니 길쭉하게 제대로 나오네

원인 파악 및 해결 완료

 

완전 삽질했네. 누군가가 삽질하지 않길 바라며 이 글을 쓴다.

학습이 얼마나 잘 되었는지는 결과를 공유하겠다.

반응형