-
[Mask R-CNN] Segmentating DeepFashion2Deep-Learning/[Vision] 실습 2020. 2. 11. 15:34
Data-Set : https://github.com/switchablenorms/DeepFashion2
1. Intro
Semantic Segmentation을 Fashion과 접목시켜보고자 한다. Fashion 관련 데이터셋 중 공개가 되어 있으며, 옷에 대한 방대한 사진과 annotation이 있는 DeepFashion2 Dataset을 이용하여 연구를 약 1년간 진행할 예정이다.
DeepFashion2 data-set은 말 그대로 포괄적인 패션 관련 데이터셋이다. 쇼핑 상점과 소비자의 사진(후기 사진으로 추측됨)을 13개의 의류 카테고리로 나눠놓은 데이터로써, 학습 세트 (391K 이미지), 유효성 검사 세트 (34k 이미지) 및 테스트 세트 (67k 이미지)로 분할되어 있다.
또한, 이미지의 각 항목에는 크기, 폐색, 확대, 관점, 범주, 스타일, 경계 상자, 짙은 랜드 마크 및 픽셀별 마스크로 레이블이 지정되어 있다.
하단의 데이터셋 예시를 보면 이해가 빠를 것이다.
Examples of DeepFashion2 2. Data Organization
Data Organization은 000001.jpg와 000001.json을 같이 보며 확인해보자.
123456789import jsonwith open('000001.json', 'r') as f:content = json.load(f)print(content.keys())for i in content.keys():print("\n",content[i])먼저 위 소스코드를 실행해보면 key값은 ['item2', 'source', 'pair_id', 'item1'] 로 구성되어 있음을 알 수 있다.
여기서, source는 'shop'의 사진인지 아니면 'user'가 찍은 사진인지 를 나타내며, pair_id는 동일한 상점에서 가지고 온 것인지 를 의미하는 값이다.(데이터셋 공홈을 보면 이 두 개의 데이터는 건들지 않는 것이 좋을 것이라고 권장한다.)
나머지 item1, item2, ..., itemn은 아래의 image와 함께 보자.
000001.jpg 위 사진은 000001의 사진이다. 위 사진을 보면, 옷이 '바지'와 '티셔츠' 두 개를 착용하고 있음을 알 수 있다. 그렇기에 json의 label의 item이 2개가 잡힌 것이다. item2 혹은 item1이 어떤 상품을 가르키는지에 대한 것은 천천히 살펴보도록 하자.
1print(content['item1'].keys())이제 item에 담긴 data를 살펴보자. item1에는 ['segmentation', 'scale', 'viewpoint', 'zoom_in', 'landmarks', 'style', 'bounding_box', 'category_id', 'occlusion', 'category_name'] 가 담겨있다. 공홈의 정보를 빌려 순서대로 어떤 데이터가 담긴 것인지 살펴보도록 하겠다.
1) segmentation : 여기서 [x1, y1, xn, yn]은 다각형을 나타내며 단일 의류 아이템은 하나 이상의 다각형을 포함 할 수 있다. 즉, 세그멘테이션 좌표값임을 알 수 있겠다.
2) scale : 1은 소규모를 나타내고 2는 보통 규모를, 3은 대규모를 나타낸다. 즉, 해당 item이 얼마나 큰가를 나타내주는 값이다.
3) viewpoint : 1은 마모가 없음을 나타내고 2는 정면 시점을 나타내고 3은 측면 또는 후면 시점을 나타낸다.
4) zoom_in : 여기서 1은 확대하지 않음, 2는 중간 확대, 3은 큰 확대를 나타낸다.
5) landmarks : 세그멘테이션 테두리의 좌표값을 나타낸다. [x1, y1, v1, ..., xn, yn, vn]으로 구성되어 있으며, 여기서 v는 가시성을 나타낸다.(v = 2 visible, v = 1 폐색, v = 0 레이블이 없음)
6) style : 동일한 페어 ID를 가진 이미지와 의류 아이템을 구별하기위한 숫자이다. (shop에 나온 상품과 동일하면 0에 가깝다는데 무엇을 의미하는지는 정확히 모르겠다....)
7) bounding_box : bounding_box의 좌표값을 나타낸다.
8) category_id : 1은 짧은 소매상의, 2는 긴 소매상의, 3은 짧은 소매 의류, 4는 긴 소매 의류, 5는 조끼, 6은 슬링, 7은 반바지, 8은 바지, 9는 치마, 10은 짧은 소매 드레스, 11은 긴 소매 드레스, 12는 조끼 드레스, 13은 슬링 드레스
9) occlusion : 폐색의 정도를 나타내며 1은 약간의 폐색, 2는 중간 폐색, 3은 완전 폐색을 나타낸다.
10) category_name : str으로 표현한 카테고리 이름이다.
landmarks를 아래의 사진과 함께 조금 더 자세히 살펴보자.
그림의 숫자는 annotation 파일에서 각 카테고리의 랜드 landmark 순서를 나타낸다. 13개의 카테고리를 포함하는 총 294 개의 랜드 마크가 정의되어 있다.
Definitions of landmarks and skeletons 또한, 데이터셋의 구성은 이미지는 'user'의 이미지와 'shop'의 이미지를 포함하여 연속적인 'pair_id'로 구성되어 있다. 예를 들어, 000001.jpg는 'user'의 이미지이며, 000002.jpg는 'shop'의 이미지라는 것이다.
그렇기에 pair로 묶여있는 이미지에 대해서는 style이 같을 수 밖에 없기에 bounding_box의 color도 하단의 사진처럼 동일할 수 밖에 없다.
3. Mask R CNN
DeepFashion2 Data-Set을 Segmentation 하기 위해 Mask R-CNN의 sample 코드를 활용해보고자 한다. Balloon.py 코드를 활용하였으며, Balloon.py에 대한 내용은 하단의 링크를 참고하기 바란다.
* balloon.py 트레이닝 과정 및 학습 결과 : https://kuklife.tistory.com/124?category=875154
[Mask R-CNN] Balloon.py 트레이닝(Window10)
Python/Tensorflow/Keras를 이용한 Mask RCNN : https://github.com/matterport/Mask_RCNN 0. 개요 Mask R-CNN에 대해 알아보고 Sample Code인 Balloon.py를 트레이닝 시킨다. 트레이닝 후 얻은 가중치를 이용해..
kuklife.tistory.com
3-1. deepfashion2 to COCO format
DeepFashion2는 논문을 읽어보면 COCO annotation format을 사용하였다. 하지만, DeepFashion2에서 제공하는 annotation format은 coco 형태가 아니다. 따라서, DeepFashion2의 annotation을 COCO annotation format으로 수정해야한다. 다행히도 github에 annotation을 coco format으로 변환시켜주는 코드를 함께 공개하였기에 이를 사용하여 format을 바꾸어보자.
* 변환 코드 : https://github.com/switchablenorms/DeepFashion2/blob/master/evaluation/deepfashion2_to_coco.py
코드를 사용하는 방법은 다음과 같다. 먼저 소스코드를 다운 받은 후, 107번째 줄의 num_images 변수 값을 바꿀 annotation의 개수로 바꾼다.
예를 들면, train의 json 파일은 191,961개이고 validation의 json 파일은 32,153개이다. train의 annotation을 COCO format으로 바꾸기 위해선 소스코드를 다음과 같이 수정하면 된다.
106107108109sub_index = 0 # the index of ground truth instancefor num in range(1,191961+1):json_name = '/.../val_annos/' + str(num).zfill(6)+'.json'image_name = '/.../val/' + str(num).zfill(6)+'.jpg'그 후, 234번째 줄의 절대경로를 바꿀 annotation의 루트로 바꾸어 주면 된다. 그럼 해당 루트에 deepfashion2.json 파일이 생성되는데, 이름이 헷갈리지 않도록 "validation_json"으로 바꿀 것을 권장한다.
3-2. Import & ROOT_DIR 설정
위와 같이 셋팅이 완료되었으면 필요한 라이브러리들을 올리고 ROOT_DIR 설정을 하면 된다.
12345678910111213141516171819202122232425import osimport sysimport jsonimport datetimeimport numpy as npimport skimage.draw# Root directory of the projectROOT_DIR = os.path.abspath("C:/Users/whtjd/Mask_RCNN")# Import Mask RCNNfrom cocoapi.PythonAPI.pycocotools.coco import COCOfrom cocoapi.PythonAPI.pycocotools import mask as maskUtilsfrom mrcnn.config import Configfrom mrcnn import utilsfrom mrcnn.model import MaskRCNN# Path to trained weights fileCOCO_WEIGHTS_PATH = os.path.join(ROOT_DIR, "mask_rcnn_coco.h5")# Directory to save logs and model checkpoints, if not provided# through the command line argument --logsDEFAULT_LOGS_DIR = os.path.join(ROOT_DIR, "logs")3-3. Configurations 설정
3-1. 에서 바꿔주었던 coco format 형태의 annotation과 image가 있는 절대경로를 사전에 변수로 저장해둔 후 config를 만들면 나중에 편하기에 사전에 아래와 같이 설정해두는 것이 좋다.
12345678910111213141516171819202122232425262728############################################################# Configurations############################################################class DeepFashion2Config(Config):"""Configuration for training on DeepFashion2.Derives from the base Config class and overrides values specificto the DeepFashion2 dataset."""# Give the configuration a recognizable nameNAME = "deepfashion2"# We use a GPU with 12GB memory, which can fit two images.# Adjust down if you use a smaller GPU.IMAGES_PER_GPU = 2# Uncomment to train on 8 GPUs (default is 1)GPU_COUNT = 1# Number of classes (including background)NUM_CLASSES = 1 + 13 # Background + categoryUSE_MINI_MASK = Truetrain_img_dir = "C:/Users/whtjd/Mask_RCNN/DeepFashion2/train/image"train_json_path = "C:/Users/whtjd/Mask_RCNN/DeepFashion2/train/train_json.json"valid_img_dir = "C:/Users/whtjd/Mask_RCNN/DeepFashion2/validation/image"valid_json_path = "C:/Users/whtjd/Mask_RCNN/DeepFashion2/validation/validation_json.json"3-4. coco, keypoint, mask, image 등 Load Data-set 관련 Class
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150############################################################# Dataset############################################################class DeepFashion2Dataset(utils.Dataset):def load_coco(self, image_dir, json_path, class_ids=None,class_map=None, return_coco=False):"""Load the DeepFashion2 dataset."""coco = COCO(json_path)# Load all classes or a subset?if not class_ids:# All classesclass_ids = sorted(coco.getCatIds())# All images or a subset?if class_ids:image_ids = []for id in class_ids:# Remove duplicatesimage_ids = list(set(image_ids))else:# All imagesimage_ids = list(coco.imgs.keys())# Add classesfor i in class_ids:self.add_class("deepfashion2", i, coco.loadCats(i)[0]["name"])# Add imagesfor i in image_ids:self.add_image("deepfashion2", image_id=i,path=os.path.join(image_dir, coco.imgs[i]['file_name']),width=coco.imgs[i]["width"],height=coco.imgs[i]["height"],annotations=coco.loadAnns(coco.getAnnIds(imgIds=[i], catIds=class_ids, iscrowd=None)))if return_coco:return cocodef load_keypoint(self, image_id):""""""image_info = self.image_info[image_id]if image_info["source"] != "deepfashion2":return super(DeepFashion2Dataset, self).load_mask(image_id)instance_keypoints = []class_ids = []annotations = self.image_info[image_id]["annotations"]for annotation in annotations:class_id = self.map_source_class_id("deepfashion2.{}".format(annotation['category_id']))if class_id:keypoint = annotation['keypoints']keypoints = np.stack(instance_keypoints, axis=1)class_ids = np.array(class_ids, dtype=np.int32)return keypoints, class_idsdef load_mask(self, image_id):"""Load instance masks for the given image.Different datasets use different ways to store masks. Thisfunction converts the different mask format to one formatin the form of a bitmap [height, width, instances].Returns:masks: A bool array of shape [height, width, instance count] withone mask per instance.class_ids: a 1D array of class IDs of the instance masks."""# If not a COCO image, delegate to parent class.image_info = self.image_info[image_id]if image_info["source"] != "deepfashion2":return super(DeepFashion2Dataset, self).load_mask(image_id)instance_masks = []class_ids = []annotations = self.image_info[image_id]["annotations"]# Build mask of shape [height, width, instance_count] and list# of class IDs that correspond to each channel of the mask.for annotation in annotations:class_id = self.map_source_class_id("deepfashion2.{}".format(annotation['category_id']))if class_id:m = self.annToMask(annotation, image_info["height"],image_info["width"])# Some objects are so small that they're less than 1 pixel area# and end up rounded out. Skip those objects.if m.max() < 1:continue# Is it a crowd? If so, use a negative class ID.if annotation['iscrowd']:# Use negative class ID for crowdsclass_id *= -1# For crowd masks, annToMask() sometimes returns a mask# smaller than the given dimensions. If so, resize it.if m.shape[0] != image_info["height"] or m.shape[1] != image_info["width"]:m = np.ones([image_info["height"], image_info["width"]], dtype=bool)# Pack instance masks into an arrayif class_ids:mask = np.stack(instance_masks, axis=2).astype(np.bool)class_ids = np.array(class_ids, dtype=np.int32)return mask, class_idselse:# Call super class to return an empty maskreturn super(DeepFashion2Dataset, self).load_mask(image_id)def image_reference(self, image_id):"""Return a link to the image in the COCO Website."""super(DeepFashion2Dataset, self).image_reference(image_id)# The following two functions are from pycocotools with a few changes.def annToRLE(self, ann, height, width):"""Convert annotation which can be polygons, uncompressed RLE to RLE.:return: binary mask (numpy 2D array)"""segm = ann['segmentation']if isinstance(segm, list):# polygon -- a single object might consist of multiple parts# we merge all parts into one mask rle coderles = maskUtils.frPyObjects(segm, height, width)rle = maskUtils.merge(rles)elif isinstance(segm['counts'], list):# uncompressed RLErle = maskUtils.frPyObjects(segm, height, width)else:# rlerle = ann['segmentation']return rledef annToMask(self, ann, height, width):"""Convert annotation which can be polygons, uncompressed RLE, or RLE to binary mask.:return: binary mask (numpy 2D array)"""rle = self.annToRLE(ann, height, width)return m3-5. Train
123456789101112131415def train(model, config):""""""dataset_train = DeepFashion2Dataset()dataset_train.load_coco(config.train_img_dir, config.train_json_path)dataset_train.prepare()dataset_valid = DeepFashion2Dataset()dataset_valid.load_coco(config.valid_img_dir, config.valid_json_path)dataset_valid.prepare()learning_rate=config.LEARNING_RATE,epochs=30,layers='3+')3-6. Splash
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374############################################################# Splash############################################################def color_splash(image, mask):"""Apply color splash effect.image: RGB image [height, width, 3]mask: instance segmentation mask [height, width, instance count]Returns result image."""# Make a grayscale copy of the image. The grayscale copy still# has 3 RGB channels, though.# Copy color pixels from the original color image where mask is setif mask.shape[-1] > 0:# We're treating all instances as one, so collapse the mask into one layermask = (np.sum(mask, -1, keepdims=True) >= 1)else:splash = gray.astype(np.uint8)return splashdef detect_and_color_splash(model, image_path=None, video_path=None):assert image_path or video_path# Image or video?if image_path:# Run model detection and generate the color splash effectprint("Running on {}".format(args.image))# Read imageimage = skimage.io.imread(args.image)# Detect objectsr = model.detect([image], verbose=1)[0]# Color splashsplash = color_splash(image, r['masks'])# Save outputfile_name = "splash_{:%Y%m%dT%H%M%S}.png".format(datetime.datetime.now())elif video_path:import cv2# Video capturevcapture = cv2.VideoCapture(video_path)width = int(vcapture.get(cv2.CAP_PROP_FRAME_WIDTH))height = int(vcapture.get(cv2.CAP_PROP_FRAME_HEIGHT))# Define codec and create video writerfile_name = "splash_{:%Y%m%dT%H%M%S}.avi".format(datetime.datetime.now())vwriter = cv2.VideoWriter(file_name,cv2.VideoWriter_fourcc(*'MJPG'),fps, (width, height))count = 0success = Truewhile success:print("frame: ", count)# Read next imagesuccess, image = vcapture.read()if success:# OpenCV returns images as BGR, convert to RGBimage = image[..., ::-1]# Detect objectsr = model.detect([image], verbose=0)[0]# Color splashsplash = color_splash(image, r['masks'])# RGB -> BGR to save image to videosplash = splash[..., ::-1]# Add image to video writercount += 1vwriter.release()print("Saved to ", file_name)3-7. main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100############################################################# main############################################################if __name__ == "__main__":ROOT_DIR = os.path.abspath("./")DEFAULT_LOGS_DIR = os.path.join(ROOT_DIR, "logs")COCO_WEIGHTS_PATH = os.path.join(ROOT_DIR, "mask_rcnn_coco.h5")import argparse# Parse command line argumentsparser = argparse.ArgumentParser(description='Train Match R-CNN for DeepFashion.')parser.add_argument("command",metavar="<command>",help="'train' or 'splash'")parser.add_argument('--weights', required=True,metavar="/path/to/weights.h5",help="Path to weights .h5 file or 'coco'")parser.add_argument('--logs', required=False,default=DEFAULT_LOGS_DIR,metavar="/path/to/logs/",help='Logs and checkpoints directory (default=logs/)')parser.add_argument('--image', required=False,metavar="path or URL to image",help='Image to apply the color splash effect on')parser.add_argument('--video', required=False,metavar="path or URL to video",help='Video to apply the color splash effect on')args = parser.parse_args()"""# Validate argumentsif args.command == "train":assert args.dataset, "Argument --dataset is required for training"elif args.command == "splash":assert args.image or args.video,\"Provide --image or --video to apply color splash""""print("Weights: ", args.weights)print("Logs: ", args.logs)# Configurationsif args.command == "train":config = DeepFashion2Config()else:class InferenceConfig(DeepFashion2Config):# Set batch size to 1 since we'll be running inference on# one image at a time. Batch size = GPU_COUNT * IMAGES_PER_GPUGPU_COUNT = 1IMAGES_PER_GPU = 1config = InferenceConfig()config.display()# Create modelif args.command == "train":model = MaskRCNN(mode="training", config=config,model_dir=args.logs)else:model = MaskRCNN(mode="inference", config=config,model_dir=args.logs)# Select weights file to loadif args.weights.lower() == "coco":weights_path = COCO_WEIGHTS_PATH# Download weights fileutils.download_trained_weights(weights_path)elif args.weights.lower() == "last":# Find last trained weightsweights_path = model.find_last()elif args.weights.lower() == "imagenet":# Start from ImageNet trained weightsweights_path = model.get_imagenet_weights()else:weights_path = args.weights# Load weightsprint("Loading weights ", weights_path)if args.weights.lower() == "coco":# Exclude the last layers because they require a matching# number of classesmodel.load_weights(weights_path, by_name=True, exclude=["mrcnn_class_logits", "mrcnn_bbox_fc","mrcnn_bbox", "mrcnn_mask"])else:model.load_weights(weights_path, by_name=True)# Train or evaluateif args.command == "train":train(model, config)elif args.command == "splash":detect_and_color_splash(model, image_path=args.image,video_path=args.video)else:print("'{}' is not recognized. ""Use 'train' or 'splash'".format(args.command))4. 트레이닝
0) 정확도를 향상시키는 방법(소스코드 내에서는 적용시켜놓은 상태)
Train 함수 부분에 layers='3+'라고 적힌 부분이 있는데, Mask R-CNN은 친절하게도 head만 학습할건지, 네트워크 전체를 학습할건지 에 대한 옵션 선택이 가능하다. 해당 옵션을 조정해가며 정확도를 향상시킬 수도 있다. 초기에는 head로 잡아 두었다가 늦게 알게되어 3+로 변경하여 학습하였다.
1) Training
위와 같이 코드를 작업하였으면, balloon.py와 비슷한 방식으로 아래와 같이 실행해주면 된다. 훈련된 가중치는 루트 디렉토리 내의 logs 폴더에 저장되며, 1000 step에 30 epochs이기에 시간이 굉장히 오래걸린다.(1080Ti 기준으로 6~8시간 가량 걸렸던걸로 기억한다.)
python Mask_RCNN/Mask_RCNN_DeepFashion2.py train --weights=coco 2) 학습 완료된 가중치 확인 및 학습시킨 모델 적용
test 폴더에 있는 image를 통해 splash를 시도해보자. 여러가지 결과를 확인해보기 위해 나는 000003, 000004, 000006, 000017, 000035 총 5개를 확인해보았다. 아래의 코드는 예시이다.
python Mask_RCNN/Mask_RCNN_DeepFashion2.py splash --weights=Mask_RCNN/mask_rcnn_deepfashion2_0030.h5 --image=Mask_RCNN/DeepFashion2/test/test/image/000004.jpg 000003, 000004, 000006의 Mask R-CNN 결과 000017, 000035의 Mask R-CNN 결과 5. 평가
Segmentation의 평가지표로 잘 알려져 있는 MIoU 값을 활용하여 위의 결과를 평가해보고자 한다.(MIoU에 대한 설명은 여기 클릭)
To be continue.......
6. 결론
결과를 보면 옷이 전체적으로 보이지 않는 경우는 segmentation을 못하는 모습이 많이 보였으며, 3번째 결과 같은 경우 segmentation을 하면 안되는 책을 mask 씌웠다. 또한, 자세히 보면 옷의 겉부분은 segmentation을 잘하지 못하는 경향을 보였다.
즉, 전체적인 결과를 볼 필요도 없이 결과 자체는 좋지 못하며 완벽하게 segmentation 되지는 않지만 추후 데이터셋을 늘리거나 레이어 조정 등을 시도하면 더 정확한 segmentation을 할 것으로 보인다.
다음에는 segmentation에서 가장 좋은 모델이라고 불리는 deeplab v3+와 Panoptic DeepLab으로 실험을 해볼 예정이다. (논문을 쓸 아이디어가 나올 때까지....)
7. github link
To be continue.......
오류가 발생되어 실험이 불가능하다면 댓글 혹은 방명록에 작성해주시기 바랍니다. 또한, 더 좋은 방향으로 학습시키는 방법이 있다면 말씀해주시면 감사하겠습니다.
'Deep-Learning > [Vision] 실습' 카테고리의 다른 글
[Mask R-CNN] Balloon.py 트레이닝(Window10) (1) 2020.02.24 [Deep Learning] Neural Network for predicting XOR operations in Python (0) 2019.04.09