A.I
CAM, Grad-CAM 본문
Class Activation Map 만들기¶
- mkdir -p ~/aiffel/class_activation_map
- ln -s ~/data ~/aiffel/class_activation_map(클라우드 사용시)
CAM, Grad-CAM용 모델 준비하기 (1) 데이터셋 준비하기¶
CAM¶
- 일반적인 딥러닝 모델은 모델 안에서 어떤 일이 일어나는지 알수가 없다.
- 일반적인 모델은 Conv층을 거듭하면서 이미지에 대한 feature를 추출하고, classification을 수행할 때는 Fully Connected Layer(Dense Layer)를 지난 후 얻은 Logits을 softmax층을 통과시켜 각 클래스에 대한 확률값을 얻게 된다.
- 여기서 CAM(Class Avtivation Map)의 경우에는 Feature Extraction하는 부분을 똑같이 수행하지만, 이후에 Classification을 수행할때 기존의 Fully Connected Layer를 사용하는 것이 아닌 Global Average Pooling을 사용하게 된다.
- 이러한 GAP(Globak Average Pooling)을 사용하면 기존에 Dense로 층을 구현하는 거에 비해서 입력값의 위치정보가 유지되기 때문에 이후에 나오는 특성맵의 정보를 활용해서 이미지 검출(detection)이나 Segmentation 등의 문제를 푸는데도 활용이 가능해진다.
- 전체적인 흐름을 보자면 GAP를 통과하고 객 채널 별 정보를 요약하고 난 후에, Softmax 층을 지나고 각 클래스에 대한 개별 채널의 중요도를 걸졍하게 된다. - 그러고 난 후 각 채널의 가중합을 구하면 각 클래스가 활성화맵에 어떤 부분을 주로 활성화 시키는지 확인할 수 있다.
- 이후에 보간을 통해서 입력이미지의 사이즈와 같게 만들고, 원본 이미지와 겹쳐서 표현을 하면 된다.
In [38]:
# TensorFlow and tf.keras
import tensorflow as tf
from tensorflow import keras
# Helper libraries
import numpy as np
import matplotlib.pyplot as plt
import tensorflow_datasets as tfds
import copy
import cv2
from PIL import Image
In [ ]:
tf.config.list_physical_devices('GPU')
In [ ]:
# 최초 수행시에는 다운로드가 진행됩니다. 오래 걸릴 수 있으니 유의해 주세요.
import urllib3
urllib3.disable_warnings()
(ds_train, ds_test), ds_info = tfds.load(
'cars196',
split=['train', 'test'],
shuffle_files=True,
with_info=True,
)
In [ ]:
tfds.show_examples(ds_train, ds_info)
In [ ]:
tfds.show_examples(ds_test, ds_info)
CAM, Grad-CAM용 모델 준비하기 (2) 물체의 위치정보¶
In [ ]:
ds_info.features
CAM, Grad-CAM용 모델 준비하기 (3) CAM을 위한 모델 만들기¶
In [ ]:
num_classes = ds_info.features["label"].num_classes
base_model = keras.applications.resnet.ResNet50(
include_top=False, # Imagenet 분류기 fully connected layer 제거
weights='imagenet',
input_shape=(224, 224, 3),
pooling='avg', # 마지막 fully connected layer 대신 GAP을 사용 # None, avg, max 사용
)
x = base_model.output
preds = keras.layers.Dense(num_classes, activation = 'softmax')(x)
cam_model=keras.Model(inputs=base_model.input, outputs=preds)
In [ ]:
cam_model.summary()
CAM, Grad-CAM용 모델 준비하기 (4) CAM 모델 학습하기¶
- input에 이전과 달리 bbox 정보가 포함되어있지만, 지금 수행해야 할 CAM 모델의 학습에는 필요가 없으므로 normalize_and_resize_img과정에서 제외
- CAM 모델은 object detection이나 segmentation에도 활용될 수 있지만, bounding box같은 직접적인 라벨을 사용하지 않고 weakly supervised learning을 통해 물체 영역을 간접적으로 학습시키는 방식이기 때문입니다.
In [ ]:
def normalize_and_resize_img(input):
"""Normalizes images: `uint8` -> `float32`."""
image = tf.image.resize(input['image'], [224, 224])
input['image'] = tf.cast(image, tf.float32) / 255.
return input['image'], input['label']
def apply_normalize_on_dataset(ds, is_test=False, batch_size=16):
ds = ds.map(
normalize_and_resize_img,
num_parallel_calls=2
)
ds = ds.batch(batch_size)
if not is_test:
ds = ds.repeat()
ds = ds.shuffle(200)
ds = ds.prefetch(tf.data.experimental.AUTOTUNE)
return ds
In [ ]:
# 데이터셋에 전처리와 배치처리를 적용합니다.
ds_train_norm = apply_normalize_on_dataset(ds_train)
ds_test_norm = apply_normalize_on_dataset(ds_test)
# 구성된 배치의 모양을 확인해 봅니다.
for input in ds_train_norm.take(1):
image, label = input
print(image.shape)
print(label.shape)
In [ ]:
tf.random.set_seed(2021)
cam_model.compile(
loss='sparse_categorical_crossentropy',
optimizer=tf.keras.optimizers.SGD(lr=0.01),
metrics=['accuracy'],
)
In [ ]:
history_cam_model = cam_model.fit(
ds_train_norm,
steps_per_epoch=int(ds_info.splits['train'].num_examples/16),
validation_steps=int(ds_info.splits['test'].num_examples/16),
epochs=15,
validation_data=ds_test_norm,
verbose=1,
use_multiprocessing=True,
)
In [ ]:
import os
cam_model_path = os.getenv('HOME')+'/aiffel/class_activation_map/cam_model.h5'
cam_model.save(cam_model_path)
print("저장 완료!")
CAM¶
In [1]:
import tensorflow as tf
from tensorflow import keras
import numpy as np
import matplotlib.pyplot as plt
import tensorflow_datasets as tfds
import copy
import cv2
from PIL import Image
import urllib3
urllib3.disable_warnings()
(ds_train, ds_test), ds_info = tfds.load(
'cars196',
split=['train', 'test'],
shuffle_files=True,
with_info=True,
)
def normalize_and_resize_img(input):
"""Normalizes images: `uint8` -> `float32`."""
image = tf.image.resize(input['image'], [224, 224])
input['image'] = tf.cast(image, tf.float32) / 255.
return input['image'], input['label']
def apply_normalize_on_dataset(ds, is_test=False, batch_size=16):
ds = ds.map(
normalize_and_resize_img,
num_parallel_calls=2
)
ds = ds.batch(batch_size)
if not is_test:
ds = ds.repeat()
ds = ds.shuffle(200)
ds = ds.prefetch(tf.data.experimental.AUTOTUNE)
return ds
In [2]:
# CAM 생성 작업은 데이터셋 배치 단위가 아니라 개별 이미지 데이터 단위로 이루어지기 때문에,
# get_one() 함수로 데이터셋에서 한 장씩 뽑는다.
def get_one(ds):
ds = ds.take(1)
sample_data = list(ds.as_numpy_iterator())
bbox = sample_data[0]['bbox']
image = sample_data[0]['image']
label = sample_data[0]['label']
return sample_data[0]
In [3]:
item = get_one(ds_test)
print(item['label'])
plt.imshow(item['image'])
120
Out[3]:
<matplotlib.image.AxesImage at 0x7fb6900b03d0>
In [4]:
import os
cam_model_path = os.getenv('HOME')+'/aiffel/class_activation_map/cam_model.h5'
cam_model = tf.keras.models.load_model(cam_model_path)
CAM 생생 조건¶
- 특성 맵
- 클래스 별 확률을 얻기 위한 소프트맥스 레이어의 가중치
- 원하는 클래스의 출력값
- 이미지에서 모델이 어떤 부분을 보는지 직관적으로 확인하려면 네트워크에서 나온 CAM을 입력 이미지 사이즈와 같게 만들어 함께 시각화 이를 고려해서 model과 item을 받았을 때 입력 이미지와 동일한 크기의 CAM을 반환하는 함수를 만들어야 합니다.
In [5]:
def generate_cam(model, item):
item = copy.deepcopy(item)
width = item['image'].shape[1]
height = item['image'].shape[0]
img_tensor, class_idx = normalize_and_resize_img(item)
# 학습한 모델에서 원하는 Layer의 output을 얻기 위해서 모델의 input과 output을 새롭게 정의해줍니다.
# model.layers[-3].output에서는 우리가 필요로 하는 GAP 이전 Convolution layer의 output을 얻을 수 있습니다.
cam_model = tf.keras.models.Model([model.inputs], [model.layers[-3].output, model.output])
conv_outputs, predictions = cam_model(tf.expand_dims(img_tensor, 0))
conv_outputs = conv_outputs[0, :, :, :] # 필요없는 맨 앞의 차원을 버립니다.
class_weights = model.layers[-1].get_weights()[0] #마지막 모델의 weight activation을 가져옵니다.
cam_image = np.zeros(dtype=np.float32, shape=conv_outputs.shape[0:2])
for i, w in enumerate(class_weights[:, class_idx]):
# W * f 를 통해 class별 activation map을 계산합니다.
cam_image += w * conv_outputs[:, :, i]
cam_image /= np.max(cam_image) # activation score를 normalize합니다.
cam_image = cam_image.numpy()
cam_image = cv2.resize(cam_image, (width, height)) # 원래 이미지의 크기로 resize합니다.
return cam_image
In [6]:
cam_image = generate_cam(cam_model, item)
plt.imshow(cam_image)
Out[6]:
<matplotlib.image.AxesImage at 0x7fb63c34ff50>
In [7]:
def visualize_cam_on_image(src1, src2, alpha=0.5):
beta = (1.0 - alpha)
merged_image = cv2.addWeighted(src1, alpha, src2, beta, 0.0)
return merged_image
In [8]:
origin_image = item['image'].astype(np.uint8)
cam_image_3channel = np.stack([cam_image*255]*3, axis=-1).astype(np.uint8)
blended_image = visualize_cam_on_image(cam_image_3channel, origin_image)
plt.imshow(blended_image)
Out[8]:
<matplotlib.image.AxesImage at 0x7fb6f1c15590>
Grad-CAM¶
grad_cam은 관찰을 원하는 레이어와 정답 클래스에 대한 예측값 사이의 그래디언트를 구하고, 여기에 GAP 연산을 적용함으로써 관찰 대상이 되는 레이어의 채널별 가중치를 구합니다. 최종 CAM 이미지를 구하기 위해서는 레이어의 채널별 가중치(weights)와 레이어에서 나온 채널별 특성 맵을 가중합 해주어 cam_image를 얻게 됩니다.
CAM 함수와 달리, Grad-CAM은 이번에는 어떤 레이어든 CAM 이미지를 뽑아낼 수 있으므로, 그래디언트 계산을 원하는 관찰 대상 레이어 activation_layer를 뽑아서 쓸 수 있도록 activation_layer의 이름을 받고 이를 활용해야 합니다.
아래 generate_grad_cam()에서는 원하는 레이어의 output과 특정 클 래스의 prediction 사이의 그래디언트 grad_val을 얻고 이를 weights로 활용합니다.
In [9]:
item = get_one(ds_test)
print(item['label'])
plt.imshow(item['image'])
130
Out[9]:
<matplotlib.image.AxesImage at 0x7fb6f1c02d90>
In [10]:
def generate_grad_cam(model, activation_layer, item):
item = copy.deepcopy(item)
width = item['image'].shape[1]
height = item['image'].shape[0]
img_tensor, class_idx = normalize_and_resize_img(item)
# Grad cam에서도 cam과 같이 특정 레이어의 output을 필요로 하므로 모델의 input과 output을 새롭게 정의합니다.
# 이때 원하는 레이어가 다를 수 있으니 해당 레이어의 이름으로 찾은 후 output으로 추가합니다.
grad_model = tf.keras.models.Model([model.inputs], [model.get_layer(activation_layer).output, model.output])
# Gradient를 얻기 위해 tape를 사용합니다.
with tf.GradientTape() as tape:
conv_output, pred = grad_model(tf.expand_dims(img_tensor, 0))
loss = pred[:, class_idx] # 원하는 class(여기서는 정답으로 활용) 예측값을 얻습니다.
output = conv_output[0] # 원하는 layer의 output을 얻습니다.
grad_val = tape.gradient(loss, conv_output)[0] # 예측값에 따른 Layer의 gradient를 얻습니다.
weights = np.mean(grad_val, axis=(0, 1)) # gradient의 GAP으로 class별 weight를 구합니다.
grad_cam_image = np.zeros(dtype=np.float32, shape=conv_output.shape[0:2])
for k, w in enumerate(weights):
# 각 class별 weight와 해당 layer의 output을 곱해 class activation map을 얻습니다.
grad_cam_image += w * output[:, :, k]
grad_cam_image /= np.max(grad_cam_image)
grad_cam_image = grad_cam_image.numpy()
grad_cam_image = cv2.resize(grad_cam_image, (width, height))
return grad_cam_image
In [11]:
grad_cam_image = generate_grad_cam(cam_model, 'conv5_block3_out', item)
plt.imshow(grad_cam_image)
Out[11]:
<matplotlib.image.AxesImage at 0x7fb63c353150>
In [12]:
grad_cam_image = generate_grad_cam(cam_model, 'conv4_block3_out', item)
plt.imshow(grad_cam_image)
Out[12]:
<matplotlib.image.AxesImage at 0x7fb6f1adf110>
In [13]:
grad_cam_image = generate_grad_cam(cam_model, 'conv3_block3_out', item)
plt.imshow(grad_cam_image)
Out[13]:
<matplotlib.image.AxesImage at 0x7fb6f1a5d590>
Detection with CAM¶
In [14]:
item = get_one(ds_test)
print(item['label'])
plt.imshow(item['image'])
130
Out[14]:
<matplotlib.image.AxesImage at 0x7fb6f19d5ed0>
In [15]:
cam_image = generate_cam(cam_model, item)
plt.imshow(cam_image)
Out[15]:
<matplotlib.image.AxesImage at 0x7fb6f19a7fd0>
In [16]:
def get_bbox(cam_image, score_thresh=0.05):
low_indicies = cam_image <= score_thresh
cam_image[low_indicies] = 0
cam_image = (cam_image*255).astype(np.uint8)
contours,_ = cv2.findContours(cam_image, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
cnt = contours[0]
rotated_rect = cv2.minAreaRect(cnt)
rect = cv2.boxPoints(rotated_rect)
rect = np.int0(rect)
return rect
- get_bbox() 함수는 바운딩 박스를 만들기 위해서 score_thresh를 받아 역치값 이하의 바운딩 박스는 없앤다.
- OpenCV의 findContours()와 minAreaRect()로 사각형을 찾는다.
- 이때 rotated_rect 라는 회전된 바운딩 박스를 얻고, boxPoints()로 이를 꼭지점으로 바꾸어 줍니다.
- 마지막에는 int 자료형으로 변환해 줍니다.
바운딩 박스를 표시하는 방법¶
- 'xywh' 는 바운딩박스 중심점을 x, y로 표기하고, 사각형의 너비 w와 높이 h를 표기하는 방법입니다. (예) (x_center, y_center, width, height) x, y가 중심점이 아니라 좌측 상단의 점을 가리킬 수도 있습니다.
- 'minmax'는 바운딩박스를 이루는 좌표의 최소값과 최대값을 통해 표기하는 방법입니다. (예) (x_min, x_max, y_min, y_max) 좌표의 절대값이 아니라, 전체 이미지의 너비와 높이를 기준으로 normalize한 상대적인 값을 표기하는 것이 일반적입니다.
- 위 두가지 뿐만 아니라 이미지의 상하좌우 끝단으로부터 거리로 표현하는 방법, 좌우측의 x값과 상하측의 y값 네 개로 표시하는 방법(LRTB), 네 점의 x, y 좌표 값을 모두 표시하는 방법(QUAD) 등 여러 가지 방법이 있습니다.
In [17]:
image = copy.deepcopy(item['image'])
rect = get_bbox(cam_image)
rect
Out[17]:
array([[ 62, 169], [ 66, 44], [262, 51], [257, 176]])
In [18]:
image = cv2.drawContours(image,[rect],0,(0,0,255),2)
plt.imshow(image)
Out[18]:
<matplotlib.image.AxesImage at 0x7fb6f19426d0>
IoU¶
두 개 영역의 합집합인 "union" 영역으로 교집합 영역인 "intersection" 영역의 넓이를 나누어준 값으로 찾고자 하는 물건의 절대적인 면적과 상관없이, 영역을 정확하게 잘 찾아내었는지 상대적인 비율을 구할 수 있으므로 모델이 영역을 잘 찾았는지 비교하는 좋은 지표가 됩니다.
In [19]:
# rect의 좌표는 (x, y) 형태로, bbox는 (y_min, x_min, y_max, x_max)의 normalized 형태로 주어집니다.
def rect_to_minmax(rect, image):
bbox = [
rect[:,1].min()/float(image.shape[0]), #bounding box의 y_min
rect[:,0].min()/float(image.shape[1]), #bounding box의 x_min
rect[:,1].max()/float(image.shape[0]), #bounding box의 y_max
rect[:,0].max()/float(image.shape[1]) #bounding box의 x_max
]
return bbox
In [20]:
# rect를 minmax bbox 형태로 치환
pred_bbox = rect_to_minmax(rect, item['image'])
pred_bbox
Out[20]:
[0.2268041237113402, 0.23938223938223938, 0.9072164948453608, 1.0115830115830116]
In [21]:
# 데이터의 ground truth bbox를 확인
item['bbox']
Out[21]:
array([0.08762886, 0.11969112, 0.8556701 , 0.98841697], dtype=float32)
In [22]:
def get_iou(boxA, boxB):
y_min = max(boxA[0], boxB[0])
x_min= max(boxA[1], boxB[1])
y_max = min(boxA[2], boxB[2])
x_max = min(boxA[3], boxB[3])
interArea = max(0, x_max - x_min) * max(0, y_max - y_min)
boxAArea = (boxA[2] - boxA[0]) * (boxA[3] - boxA[1])
boxBArea = (boxB[2] - boxB[0]) * (boxB[3] - boxB[1])
iou = interArea / float(boxAArea + boxBArea - interArea)
return iou
In [23]:
get_iou(pred_bbox, item['bbox'])
Out[23]:
0.6527842811502831
프로젝트: CAM을 만들고 평가¶
In [104]:
item = get_one(ds_test)
print(item['label'])
plt.imshow(item['image'])
130
Out[104]:
<matplotlib.image.AxesImage at 0x7fb6d8c06ed0>
In [105]:
def generate_cam(model, item):
item = copy.deepcopy(item)
width = item['image'].shape[1]
height = item['image'].shape[0]
img_tensor, class_idx = normalize_and_resize_img(item)
cam_model = tf.keras.models.Model([model.inputs], [model.layers[-3].output, model.output])
conv_outputs, predictions = cam_model(tf.expand_dims(img_tensor, 0))
conv_outputs = conv_outputs[0, :, :, :] # 필요없는 맨 앞의 차원을 버립니다.
class_weights = model.layers[-1].get_weights()[0] #마지막 모델의 weight activation을 가져옵니다.
cam_image = np.zeros(dtype=np.float32, shape=conv_outputs.shape[0:2])
for i, w in enumerate(class_weights[:, class_idx]):
# W * f 를 통해 class별 activation map을 계산합니다.
cam_image += w * conv_outputs[:, :, i]
cam_image /= np.max(cam_image) # activation score를 normalize합니다.
cam_image = cam_image.numpy()
cam_image = cv2.resize(cam_image, (width, height)) # 원래 이미지의 크기로 resize합니다.
return cam_image
In [106]:
cam_image = generate_cam(cam_model, item)
plt.imshow(cam_image)
Out[106]:
<matplotlib.image.AxesImage at 0x7fb6d8beb690>
In [107]:
def visualize_cam_on_image(src1, src2, alpha=0.5):
beta = (1.0 - alpha)
merged_image = cv2.addWeighted(src1, alpha, src2, beta, 0.0)
return merged_image
In [108]:
origin_image = item['image'].astype(np.uint8)
cam_image_3channel = np.stack([cam_image*255]*3, axis=-1).astype(np.uint8)
blended_image = visualize_cam_on_image(cam_image_3channel, origin_image)
plt.imshow(blended_image)
Out[108]:
<matplotlib.image.AxesImage at 0x7fb6d8b57490>
In [117]:
item2 = get_one(ds_test)
print(item['label'])
plt.imshow(item2['image'])
130
Out[117]:
<matplotlib.image.AxesImage at 0x7fb6d890b810>
In [118]:
def generate_grad_cam(model, activation_layer, item):
item = copy.deepcopy(item)
width = item['image'].shape[1]
height = item['image'].shape[0]
img_tensor, class_idx = normalize_and_resize_img(item)
# Grad cam에서도 cam과 같이 특정 레이어의 output을 필요로 하므로 모델의 input과 output을 새롭게 정의합니다.
# 이때 원하는 레이어가 다를 수 있으니 해당 레이어의 이름으로 찾은 후 output으로 추가합니다.
grad_model = tf.keras.models.Model([model.inputs], [model.get_layer(activation_layer).output, model.output])
# Gradient를 얻기 위해 tape를 사용합니다.
with tf.GradientTape() as tape:
conv_output, pred = grad_model(tf.expand_dims(img_tensor, 0))
loss = pred[:, class_idx] # 원하는 class(여기서는 정답으로 활용) 예측값을 얻습니다.
output = conv_output[0] # 원하는 layer의 output을 얻습니다.
grad_val = tape.gradient(loss, conv_output)[0] # 예측값에 따른 Layer의 gradient를 얻습니다.
weights = np.mean(grad_val, axis=(0, 1)) # gradient의 GAP으로 class별 weight를 구합니다.
grad_cam_image = np.zeros(dtype=np.float32, shape=conv_output.shape[0:2])
for k, w in enumerate(weights):
# 각 class별 weight와 해당 layer의 output을 곱해 class activation map을 얻습니다.
grad_cam_image += w * output[:, :, k]
grad_cam_image /= np.max(grad_cam_image)
grad_cam_image = grad_cam_image.numpy()
grad_cam_image = cv2.resize(grad_cam_image, (width, height))
return grad_cam_image
In [119]:
grad_cam_image = generate_grad_cam(cam_model, 'conv5_block3_out', item)
plt.imshow(grad_cam_image)
Out[119]:
<matplotlib.image.AxesImage at 0x7fb6d88ac690>
In [120]:
def get_bbox(grad_cam_image, score_thresh=0.05):
low_indicies = cam_image <= score_thresh
grad_cam_image[low_indicies] = 0
grad_cam_image = (grad_cam_image*255).astype(np.uint8)
contours,_ = cv2.findContours(grad_cam_image, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
cnt = contours[0]
rotated_rect = cv2.minAreaRect(cnt)
rect = cv2.boxPoints(rotated_rect)
rect = np.int0(rect)
return rect
In [121]:
image = copy.deepcopy(item2['image'])
rect = get_bbox(grad_cam_image)
rect
Out[121]:
array([[ 62, 169], [ 66, 44], [262, 51], [257, 176]])
In [122]:
image = cv2.drawContours(image,[rect],0,(0,0,255),2)
plt.imshow(image)
Out[122]:
<matplotlib.image.AxesImage at 0x7fb6d8805650>
In [131]:
def rect_to_minmax(rect, image):
bbox = [
rect[:,1].min()/float(image.shape[0]), #bounding box의 y_min
rect[:,0].min()/float(image.shape[1]), #bounding box의 x_min
rect[:,1].max()/float(image.shape[0]), #bounding box의 y_max
rect[:,0].max()/float(image.shape[1]) #bounding box의 x_max
]
return bbox
In [132]:
# rect를 minmax bbox 형태로 치환
pred_bbox = rect_to_minmax(rect, item['image'])
pred_bbox
Out[132]:
[0.2268041237113402, 0.23938223938223938, 0.9072164948453608, 1.0115830115830116]
In [133]:
item['bbox']
Out[133]:
array([0.08762886, 0.11969112, 0.8556701 , 0.98841697], dtype=float32)
In [134]:
def get_iou(boxA, boxB):
y_min = max(boxA[0], boxB[0])
x_min= max(boxA[1], boxB[1])
y_max = min(boxA[2], boxB[2])
x_max = min(boxA[3], boxB[3])
interArea = max(0, x_max - x_min) * max(0, y_max - y_min)
boxAArea = (boxA[2] - boxA[0]) * (boxA[3] - boxA[1])
boxBArea = (boxB[2] - boxB[0]) * (boxB[3] - boxB[1])
iou = interArea / float(boxAArea + boxBArea - interArea)
return iou
In [135]:
get_iou(pred_bbox, item['bbox'])
Out[135]:
0.6527842811502831
'Going Deeper' 카테고리의 다른 글
Camera Sticker 붙여보기 (0) | 2021.04.26 |
---|---|
OCR의 개요 (0) | 2021.04.24 |
U-Net으로 시맨틱 세그멘테이션을 이용해 도로찾기 (2) | 2021.04.16 |
Segmentation (0) | 2021.04.15 |
RetinaNet으로 자율주행 시스템 만들기 (0) | 2021.04.13 |