안경잡이개발자

728x90
반응형

  FAIR (Facebook AI Research) 팀에서 만든 fvcore를 이용하여 NumPy 형식의 이미지변형(transformation)할 수 있다. 다음과 같이 fvcore를 설치할 수 있으며, 사용 방법 또한 매우 간단하다.

 

!pip install fvcore

 

  실습을 위해 간단히 한 장의 고양이 이미지를 준비해 보자. 필자는 상업적으로 사용이 가능한 무료 이미지를 준비해 보았다.

 

!curl https://visualhunt.com/photos/16/cat.jpg -o cat.jpg

 

 

import cv2

np_image = cv2.imread('cat.jpg')
print(np_image.shape)

 

  실행 결과를 확인해 보면 (696, 1024, 3)이라는 값이 이미지 해상도로 출력된다. 이후에 필요한 라이브러리를 불러와 보자. 기본적으로 랜덤 변형(Random Transformation)은 다음과 같이 fvcore의 다양한 Transform 함수를 활용한다. 그래서 일단 필요한 라이브러리를 모두 불러올 수 있도록 하자.

 

import numpy as np
import matplotlib.pyplot as plt

from fvcore.transforms.transform import (
    BlendTransform,
    CropTransform,
    HFlipTransform,
    NoOpTransform,
    PadTransform,
    Transform,
    TransformList,
    VFlipTransform,
)

 

1. Random Contrast

 

  랜덤으로 대비(contrast)를 변경할 수 있다. 파라미터로 intensity_min과 intensity_max를 넣을 수 있는데, 값이 1일 때는 변화를 주지 않겠다는 것이다. 필자의 경우 [0.8, 1.2] 정도의 옵션으로 많이 사용했다.

 

class RandomContrast():
    """
    Randomly transforms image contrast.
    Contrast intensity is uniformly sampled in (intensity_min, intensity_max).
    - intensity < 1 will reduce contrast
    - intensity = 1 will preserve the input image
    - intensity > 1 will increase contrast
    See: https://pillow.readthedocs.io/en/3.0.x/reference/ImageEnhance.html
    """

    def __init__(self, intensity_min, intensity_max):
        """
        Args:
            intensity_min (float): Minimum augmentation
            intensity_max (float): Maximum augmentation
        """
        self.intensity_min = intensity_min
        self.intensity_max = intensity_max

    def get_transform(self, image):
        w = np.random.uniform(self.intensity_min, self.intensity_max)
        return BlendTransform(src_image=image.mean(), src_weight=1 - w, dst_weight=w)

 

  RandomContrast는 다음과 같이 사용하면 된다.

 

random_contrast = RandomContrast(intensity_min=0.8, intensity_max=1.2)
transform = random_contrast.get_transform(np_image)
print(transform)
plt.imshow(transform.apply_image(np_image)[...,[2,1,0]])

 

  대비 값이 1.2일 때(intensity_min=1.2, intensity_max=1.2)는 다음과 같다.

 

 

  대비 값이 0.8일 때intensity_min=0.8, intensity_max=0.8)는 다음과 같다.

 

 

2. Random Brightness

 

  랜덤으로 명도(brightness)를 변경할 수 있다. 파라미터로 intensity_min과 intensity_max를 넣을 수 있는데, 값이 1일 때는 변화를 주지 않겠다는 것이다. 필자의 경우 [0.8, 1.2] 정도의 옵션으로 많이 사용했다.

 

class RandomBrightness():
    """
    Randomly transforms image brightness.
    Brightness intensity is uniformly sampled in (intensity_min, intensity_max).
    - intensity < 1 will reduce brightness
    - intensity = 1 will preserve the input image
    - intensity > 1 will increase brightness
    See: https://pillow.readthedocs.io/en/3.0.x/reference/ImageEnhance.html
    """

    def __init__(self, intensity_min, intensity_max):
        """
        Args:
            intensity_min (float): Minimum augmentation
            intensity_max (float): Maximum augmentation
        """
        self.intensity_min = intensity_min
        self.intensity_max = intensity_max

    def get_transform(self, image):
        w = np.random.uniform(self.intensity_min, self.intensity_max)
        return BlendTransform(src_image=0, src_weight=1 - w, dst_weight=w)

 

  RandomBrightness는 다음과 같이 사용하면 된다.

 

random_brightness = RandomBrightness(intensity_min=0.8, intensity_max=1.2)
transform = random_brightness.get_transform(np_image)
print(transform)
plt.imshow(transform.apply_image(np_image)[...,[2,1,0]])

 

  명도 값이 1.2일 때(intensity_min=1.2, intensity_max=1.2)는 다음과 같다.

 

 

  명도 값이 0.8일 때(intensity_min=0.8, intensity_max=0.8)는 다음과 같다.

 

 

3. Random Flip

 

  랜덤으로 뒤집기(flip)를 수행할 수 있다. 아래 코드의 경우 기본 설정(default setting)상 horizontal flip만을 사용한다. 일반적으로 computer vision에서 사물을 분류(classification)하는 작업(task)의 경우 vertical flip으로 인해 오히려 성능이 떨어지는 경우도 있는 반면에, horizontal flip은 대체로 성능상의 향상을 이끌어 낸다는 특징이 있다. 또한 prob는 flip을 수행할 확률에 해당한다.

 

class RandomFlip():
    """
    Flip the image horizontally or vertically with the given probability.
    """

    def __init__(self, prob=0.5, *, horizontal=True, vertical=False):
        """
        Args:
            prob (float): probability of flip.
            horizontal (boolean): whether to apply horizontal flipping
            vertical (boolean): whether to apply vertical flipping
        """
        if horizontal and vertical:
            raise ValueError("Cannot do both horiz and vert. Please use two Flip instead.")
        if not horizontal and not vertical:
            raise ValueError("At least one of horiz or vert has to be True!")

        self.prob = prob
        self.horizontal = horizontal
        self.vertical = vertical

    def _rand_range(self, low=1.0, high=None, size=None):
        """
        Uniform float random number between low and high.
        """
        if high is None:
            low, high = 0, low
        if size is None:
            size = []
        return np.random.uniform(low, high, size)

    def get_transform(self, image):
        h, w = image.shape[:2]
        do = self._rand_range() < self.prob
        if do:
            if self.horizontal:
                return HFlipTransform(w)
            elif self.vertical:
                return VFlipTransform(h)
        else:
            return NoOpTransform()

 

  다음과 같이 사용하면 된다.

 

random_flip = RandomFlip(prob=0.5, horizontal=True, vertical=False)
transform = random_flip.get_transform(np_image)
print(transform)
plt.imshow(transform.apply_image(np_image)[...,[2,1,0]])

 

  좌우 반전(horizontal flip) 결과는 다음과 같다. (prob=1.0, horizontal=True, vertical=False)

 

 

  상하 반전(vertical flip) 결과는 다음과 같다. (prob=1.0, horizontal=False, vertical=True)

 

 

4. Random Saturation

 

  랜덤으로 채도(saturation)를 변경할 수 있다. 파라미터로 intensity_min과 intensity_max를 넣을 수 있는데, 값이 1일 때는 변화를 주지 않겠다는 것이다. 필자의 경우 [0.8, 1.2] 정도의 옵션으로 많이 사용했다.

 

class RandomSaturation():
    """
    Randomly transforms saturation of an RGB image.
    Input images are assumed to have 'RGB' channel order.
    Saturation intensity is uniformly sampled in (intensity_min, intensity_max).
    - intensity < 1 will reduce saturation (make the image more grayscale)
    - intensity = 1 will preserve the input image
    - intensity > 1 will increase saturation
    See: https://pillow.readthedocs.io/en/3.0.x/reference/ImageEnhance.html
    """

    def __init__(self, intensity_min, intensity_max):
        """
        Args:
            intensity_min (float): Minimum augmentation (1 preserves input).
            intensity_max (float): Maximum augmentation (1 preserves input).
        """
        self.intensity_min = intensity_min
        self.intensity_max = intensity_max

    def get_transform(self, image):
        assert image.shape[-1] == 3, "RandomSaturation only works on RGB images"
        w = np.random.uniform(self.intensity_min, self.intensity_max)
        grayscale = image.dot([0.299, 0.587, 0.114])[:, :, np.newaxis]
        return BlendTransform(src_image=grayscale, src_weight=1 - w, dst_weight=w)

 

  다음과 같이 사용하면 된다.

 

random_saturation = RandomSaturation(intensity_min=0.8, intensity_max=1.2)
transform = random_saturation.get_transform(np_image)
print(transform)
plt.imshow(transform.apply_image(np_image)[...,[2,1,0]])

 

  채도 값이 0.8일 때(intensity_min=0.8, intensity_max=0.8)는 다음과 같다.

 

 

  채도 값이 1.2일 때(intensity_min=1.2, intensity_max=1.2)는 다음과 같다.

 

 

5. Random Rotation

 

  랜덤으로 회전(rotation)을 수행할 수 있다. angle 변수에 (min, max) 값을 넣어 회전을 수행할 수 있다. 필자의 경우 (-10, 10) 정도의 옵션으로 많이 사용했다.

 

class RotationTransform(Transform):
    """
    This method returns a copy of this image, rotated the given
    number of degrees counter clockwise around its center.
    """

    def __init__(self, h, w, angle, expand=True, center=None, interp=None):
        """
        Args:
            h, w (int): original image size
            angle (float): degrees for rotation
            expand (bool): choose if the image should be resized to fit the whole
                rotated image (default), or simply cropped
            center (tuple (width, height)): coordinates of the rotation center
                if left to None, the center will be fit to the center of each image
                center has no effect if expand=True because it only affects shifting
            interp: cv2 interpolation method, default cv2.INTER_LINEAR
        """
        super().__init__()
        image_center = np.array((w / 2, h / 2))
        if center is None:
            center = image_center
        if interp is None:
            interp = cv2.INTER_LINEAR
        abs_cos, abs_sin = (abs(np.cos(np.deg2rad(angle))), abs(np.sin(np.deg2rad(angle))))
        if expand:
            # find the new width and height bounds
            bound_w, bound_h = np.rint(
                [h * abs_sin + w * abs_cos, h * abs_cos + w * abs_sin]
            ).astype(int)
        else:
            bound_w, bound_h = w, h

        self._set_attributes(locals())
        self.rm_coords = self.create_rotation_matrix()
        # Needed because of this problem https://github.com/opencv/opencv/issues/11784
        self.rm_image = self.create_rotation_matrix(offset=-0.5)

    def apply_image(self, img, interp=None):
        """
        img should be a numpy array, formatted as Height * Width * Nchannels
        """
        if len(img) == 0 or self.angle % 360 == 0:
            return img
        assert img.shape[:2] == (self.h, self.w)
        interp = interp if interp is not None else self.interp
        return cv2.warpAffine(img, self.rm_image, (self.bound_w, self.bound_h), flags=interp)

    def apply_coords(self, coords):
        """
        coords should be a N * 2 array-like, containing N couples of (x, y) points
        """
        coords = np.asarray(coords, dtype=float)
        if len(coords) == 0 or self.angle % 360 == 0:
            return coords
        return cv2.transform(coords[:, np.newaxis, :], self.rm_coords)[:, 0, :]

    def apply_segmentation(self, segmentation):
        segmentation = self.apply_image(segmentation, interp=cv2.INTER_NEAREST)
        return segmentation

    def create_rotation_matrix(self, offset=0):
        center = (self.center[0] + offset, self.center[1] + offset)
        rm = cv2.getRotationMatrix2D(tuple(center), self.angle, 1)
        if self.expand:
            # Find the coordinates of the center of rotation in the new image
            # The only point for which we know the future coordinates is the center of the image
            rot_im_center = cv2.transform(self.image_center[None, None, :] + offset, rm)[0, 0, :]
            new_center = np.array([self.bound_w / 2, self.bound_h / 2]) + offset - rot_im_center
            # shift the rotation center to the new coordinates
            rm[:, 2] += new_center
        return rm

    def inverse(self):
        """
        The inverse is to rotate it back with expand, and crop to get the original shape.
        """
        if not self.expand:  # Not possible to inverse if a part of the image is lost
            raise NotImplementedError()
        rotation = RotationTransform(
            self.bound_h, self.bound_w, -self.angle, True, None, self.interp
        )
        crop = CropTransform(
            (rotation.bound_w - self.w) // 2, (rotation.bound_h - self.h) // 2, self.w, self.h
        )
        return TransformList([rotation, crop])


class RandomRotation():
    """
    This method returns a copy of this image, rotated the given
    number of degrees counter clockwise around the given center.
    """

    def __init__(self, angle, expand=True, center=None, sample_style="range", interp=None):
        """
        Args:
            angle (list[float]): If ``sample_style=="range"``,
                a [min, max] interval from which to sample the angle (in degrees).
                If ``sample_style=="choice"``, a list of angles to sample from
            expand (bool): choose if the image should be resized to fit the whole
                rotated image (default), or simply cropped
            center (list[[float, float]]):  If ``sample_style=="range"``,
                a [[minx, miny], [maxx, maxy]] relative interval from which to sample the center,
                [0, 0] being the top left of the image and [1, 1] the bottom right.
                If ``sample_style=="choice"``, a list of centers to sample from
                Default: None, which means that the center of rotation is the center of the image
                center has no effect if expand=True because it only affects shifting
        """
        super().__init__()
        assert sample_style in ["range", "choice"], sample_style
        self.is_range = sample_style == "range"
        if isinstance(angle, (float, int)):
            angle = (angle, angle)
        if center is not None and isinstance(center[0], (float, int)):
            center = (center, center)
        self.angle = angle
        self.expand = expand
        self.center = center
        self.sample_style = sample_style
        self.interp = interp

    def get_transform(self, image):
        h, w = image.shape[:2]
        center = None
        if self.is_range:
            angle = np.random.uniform(self.angle[0], self.angle[1])
            if self.center is not None:
                center = (
                    np.random.uniform(self.center[0][0], self.center[1][0]),
                    np.random.uniform(self.center[0][1], self.center[1][1]),
                )
        else:
            angle = np.random.choice(self.angle)
            if self.center is not None:
                center = np.random.choice(self.center)

        if center is not None:
            center = (w * center[0], h * center[1])  # Convert to absolute coordinates

        if angle % 360 == 0:
            return NoOpTransform()

        return RotationTransform(h, w, angle, expand=self.expand, center=center, interp=self.interp)

 

  다음과 같이 사용하면 된다. 만약에 expand를 사용하고 싶지 않다면 expand의 값을 False로 넣으면 된다.

 

random_rotation = RandomRotation(angle=(10, 10))
transform = random_rotation.get_transform(np_image)
print(transform)
plt.imshow(transform.apply_image(np_image)[...,[2,1,0]])

 

  angle 값이 10일 때(angle=[10, 10])는 다음과 같다.

 

 

  angle 값이 10일 때(angle=[10, 10], expand=False)의 또 다른 예시는 다음과 같다. 이 경우 회전만 시키고, 원래의 이미지 사이즈에 맞게 이미지를 resize하지 않기 때문에, 사진의 일부 영역이 가려져 보이지 않게 된다.

 

 

6. Resize Shortest Edge

 

  참고로 랜덤(random) 변형은 아니지만, computer vision task에서 많이 사용되는 augmentation 기법 중 하나로 resize shortest edge가 있다. 이것은 이미지의 가로 혹은 세로 중에서 짧은 변의 길이가 특정한 값이 되도록 이미지를 resize하는 기법이다.

 

import sys
from PIL import Image


class ResizeTransform(Transform):
    """
    Resize the image to a target size.
    """

    def __init__(self, h, w, new_h, new_w, interp=None):
        """
        Args:
            h, w (int): original image size
            new_h, new_w (int): new image size
            interp: PIL interpolation methods, defaults to bilinear.
        """
        # TODO decide on PIL vs opencv
        super().__init__()
        if interp is None:
            interp = Image.BILINEAR
        self._set_attributes(locals())

    def apply_image(self, img, interp=None):
        assert img.shape[:2] == (self.h, self.w)
        assert len(img.shape) <= 4
        interp_method = interp if interp is not None else self.interp

        if img.dtype == np.uint8:
            if len(img.shape) > 2 and img.shape[2] == 1:
                pil_image = Image.fromarray(img[:, :, 0], mode="L")
            else:
                pil_image = Image.fromarray(img)
            pil_image = pil_image.resize((self.new_w, self.new_h), interp_method)
            ret = np.asarray(pil_image)
            if len(img.shape) > 2 and img.shape[2] == 1:
                ret = np.expand_dims(ret, -1)
        else:
            # PIL only supports uint8
            if any(x < 0 for x in img.strides):
                img = np.ascontiguousarray(img)
            img = torch.from_numpy(img)
            shape = list(img.shape)
            shape_4d = shape[:2] + [1] * (4 - len(shape)) + shape[2:]
            img = img.view(shape_4d).permute(2, 3, 0, 1)  # hw(c) -> nchw
            _PIL_RESIZE_TO_INTERPOLATE_MODE = {
                Image.NEAREST: "nearest",
                Image.BILINEAR: "bilinear",
                Image.BICUBIC: "bicubic",
            }
            mode = _PIL_RESIZE_TO_INTERPOLATE_MODE[interp_method]
            align_corners = None if mode == "nearest" else False
            img = F.interpolate(
                img, (self.new_h, self.new_w), mode=mode, align_corners=align_corners
            )
            shape[:2] = (self.new_h, self.new_w)
            ret = img.permute(2, 3, 0, 1).view(shape).numpy()  # nchw -> hw(c)

        return ret

    def apply_coords(self, coords):
        coords[:, 0] = coords[:, 0] * (self.new_w * 1.0 / self.w)
        coords[:, 1] = coords[:, 1] * (self.new_h * 1.0 / self.h)
        return coords

    def apply_segmentation(self, segmentation):
        segmentation = self.apply_image(segmentation, interp=Image.NEAREST)
        return segmentation

    def inverse(self):
        return ResizeTransform(self.new_h, self.new_w, self.h, self.w, self.interp)


class ResizeShortestEdge():
    """
    Scale the shorter edge to the given size, with a limit of `max_size` on the longer edge.
    If `max_size` is reached, then downscale so that the longer edge does not exceed max_size.
    """

    def __init__(
        self, short_edge_length, max_size=sys.maxsize, sample_style="range", interp=Image.BILINEAR
    ):
        """
        Args:
            short_edge_length (list[int]): If ``sample_style=="range"``,
                a [min, max] interval from which to sample the shortest edge length.
                If ``sample_style=="choice"``, a list of shortest edge lengths to sample from.
            max_size (int): maximum allowed longest edge length.
            sample_style (str): either "range" or "choice".
        """
        super().__init__()
        assert sample_style in ["range", "choice"], sample_style

        self.is_range = sample_style == "range"
        if isinstance(short_edge_length, int):
            short_edge_length = (short_edge_length, short_edge_length)
        if self.is_range:
            assert len(short_edge_length) == 2, (
                "short_edge_length must be two values using 'range' sample style."
                f" Got {short_edge_length}!"
            )
        self.short_edge_length = short_edge_length
        self.max_size = max_size
        self.sample_style = sample_style
        self.interp = interp

    def get_transform(self, image):
        h, w = image.shape[:2]
        if self.is_range:
            size = np.random.randint(self.short_edge_length[0], self.short_edge_length[1] + 1)
        else:
            size = np.random.choice(self.short_edge_length)
        if size == 0:
            return NoOpTransform()

        scale = size * 1.0 / min(h, w)
        if h < w:
            newh, neww = size, scale * w
        else:
            newh, neww = scale * h, size
        if max(newh, neww) > self.max_size:
            scale = self.max_size * 1.0 / max(newh, neww)
            newh = newh * scale
            neww = neww * scale
        neww = int(neww + 0.5)
        newh = int(newh + 0.5)
        return ResizeTransform(h, w, newh, neww, self.interp)

 

  min_size = 400, max_size = 4,000일 때의 예시는 다음과 같다. 짧은 변(세로)의 길이가 400이 된다.

 

resize_shortest_edge = ResizeShortestEdge(400, 4000)
transform = resize_shortest_edge.get_transform(np_image)
print(transform)
plt.imshow(transform.apply_image(np_image)[...,[2,1,0]])

 

 

  min_size = 700, max_size = 4,000일 때의 예시는 다음과 같다. 짧은 변(세로)의 길이가 700이 된다.

 

 

  min_size = 3600, max_size = 4,000일 때의 예시는 다음과 같다. 긴 변(가로)의 길이가 최대 4,000이어야 하기 때문에, 짧은 변(세로)의 길이가 2719까지만 증가하고 멈추게 된다.

 

728x90
반응형