Created
April 22, 2019 19:35
-
-
Save SpicySyntax/c8fd433c52e13bc28f68cd7ea300378e to your computer and use it in GitHub Desktop.
CrackSegmentation
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import os | |
import sys | |
import itertools | |
import math | |
import logging | |
import json | |
import re | |
import random | |
from collections import OrderedDict | |
import numpy as np | |
import matplotlib | |
import matplotlib.pyplot as plt | |
import matplotlib.patches as patches | |
import matplotlib.lines as lines | |
from matplotlib.patches import Polygon | |
# Root directory of the project | |
ROOT_DIR = os.path.abspath("../../") | |
# Import Mask RCNN | |
sys.path.append(ROOT_DIR) # To find local version of the library | |
from mrcnn import utils | |
from mrcnn import visualize | |
from mrcnn.visualize import display_images | |
import mrcnn.model as modellib | |
from mrcnn.model import log | |
%matplotlib inline | |
from keras.backend.tensorflow_backend import set_session | |
from keras.backend.tensorflow_backend import clear_session | |
from keras.backend.tensorflow_backend import get_session | |
import tensorflow as tf | |
import gc | |
# Reset Keras Session to prevent GPU resource exaustion from remaining TF session graphs in memory | |
def reset_keras(model=None): | |
sess = get_session() | |
clear_session() | |
sess.close() | |
sess = get_session() | |
try: | |
del model # this is from global space - change this as you need | |
except: | |
pass | |
print(gc.collect()) # if it's done something you should see a number being outputted | |
# use the same config as you used to create the session | |
config = tf.ConfigProto() | |
config.gpu_options.per_process_gpu_memory_fraction = 1 | |
config.gpu_options.visible_device_list = "0" | |
set_session(tf.Session(config=config)) | |
reset_keras() | |
import os | |
import sys | |
import json | |
import datetime | |
import numpy as np | |
import skimage.io | |
from imgaug import augmenters as iaa | |
# Root directory of the project | |
ROOT_DIR = os.path.abspath("./") | |
print(ROOT_DIR) | |
# Directory to save logs and model checkpoints, if not provided | |
# through the command line argument --logs | |
DEFAULT_LOGS_DIR = os.path.join(ROOT_DIR, "logs") | |
# Results directory | |
# Save submission files here | |
RESULTS_DIR = os.path.join(ROOT_DIR, "results/cracks/") | |
# Import Mask RCNN | |
sys.path.append(ROOT_DIR) # To find local version of the library | |
from mrcnn.config import Config | |
from mrcnn import utils | |
from mrcnn import model as modellib | |
from mrcnn import visualize | |
# Path to trained weights file | |
COCO_WEIGHTS_PATH = os.path.join(ROOT_DIR, "mask_rcnn_coco.h5") | |
############################################################ | |
# Configurations | |
############################################################ | |
# The dataset doesn't have a standard train/val split, so I picked | |
# a variety of images to surve as a validation set. | |
VAL_IMAGE_IDS = [ | |
"image-1", | |
"image-50", | |
"image-100", | |
"image-150", | |
"image-200", | |
"image-250", | |
"image-300", | |
"image-350", | |
"image-400", | |
"image-450", | |
"image-500", | |
'''"image-550", | |
"image-600", | |
"image-650", | |
"image-700", | |
"image-700", | |
"image-800", | |
"image-850", | |
"image-900", | |
"image-950", | |
"image-1000", | |
"image-1050", | |
"image-1100", | |
"image-1150", | |
"image-1200", | |
"image-1250", | |
"image-1300", | |
"image-1350", | |
"image-1400",''' | |
] | |
class CrackConfig(Config): | |
"""Configuration for training on the Crack segmentation dataset.""" | |
# Give the configuration a recognizable name | |
NAME = "crack" | |
# Adjust depending on your GPU memory | |
IMAGES_PER_GPU = 1 | |
# Number of classes (including background) | |
NUM_CLASSES = 1 + 1 # Background + crack | |
# Number of training and validation steps per epoch | |
STEPS_PER_EPOCH = (450 - len(VAL_IMAGE_IDS)) // IMAGES_PER_GPU | |
VALIDATION_STEPS = max(1, len(VAL_IMAGE_IDS) // IMAGES_PER_GPU) | |
# Don't exclude based on confidence. Since we have two classes | |
# then 0.5 is the minimum anyway as it picks between crack and BG | |
DETECTION_MIN_CONFIDENCE = 0 | |
# Backbone network architecture | |
# Supported values are: resnet50, resnet101 | |
BACKBONE = "resnet50" | |
# Input image resizing | |
# Random crops of size 512x512 | |
IMAGE_RESIZE_MODE = "crop" | |
IMAGE_MIN_DIM = 512 | |
IMAGE_MAX_DIM = 512 | |
IMAGE_MIN_SCALE = 2.0 | |
# Length of square anchor side in pixels | |
RPN_ANCHOR_SCALES = (8, 16, 32, 64, 128) | |
# ROIs kept after non-maximum supression (training and inference) | |
POST_NMS_ROIS_TRAINING = 1000 | |
POST_NMS_ROIS_INFERENCE = 2000 | |
# Non-max suppression threshold to filter RPN proposals. | |
# You can increase this during training to generate more propsals. | |
RPN_NMS_THRESHOLD = 0.9 | |
# How many anchors per image to use for RPN training | |
RPN_TRAIN_ANCHORS_PER_IMAGE = 64 | |
# Image mean (RGB) | |
MEAN_PIXEL = np.array([43.53, 39.56, 48.22]) | |
# If enabled, resizes instance masks to a smaller size to reduce | |
# memory load. Recommended when using high-resolution images. | |
USE_MINI_MASK = True | |
MINI_MASK_SHAPE = (56, 56) # (height, width) of the mini-mask | |
# Number of ROIs per image to feed to classifier/mask heads | |
# The Mask RCNN paper uses 512 but often the RPN doesn't generate | |
# enough positive proposals to fill this and keep a positive:negative | |
# ratio of 1:3. You can increase the number of proposals by adjusting | |
# the RPN NMS threshold. | |
TRAIN_ROIS_PER_IMAGE = 128 | |
# Maximum number of ground truth instances to use in one image | |
MAX_GT_INSTANCES = 200 | |
# Max number of final detections per image | |
DETECTION_MAX_INSTANCES = 400 | |
class CrackInferenceConfig(CrackConfig): | |
# Set batch size to 1 to run one image at a time | |
GPU_COUNT = 1 | |
IMAGES_PER_GPU = 1 | |
# Don't resize imager for inferencing | |
IMAGE_RESIZE_MODE = "pad64" | |
# Non-max suppression threshold to filter RPN proposals. | |
# You can increase this during training to generate more propsals. | |
RPN_NMS_THRESHOLD = 0.7 | |
############################################################ | |
# Dataset | |
############################################################ | |
class CrackDataset(utils.Dataset): | |
def load_crack(self, dataset_dir, subset): | |
"""Load a subset of the crack dataset. | |
dataset_dir: Root directory of the dataset | |
subset: Subset to load. Either the name of the sub-directory, | |
such as stage1_train, stage1_test, ...etc. or, one of: | |
* train: stage1_train excluding validation images | |
* val: validation images from VAL_IMAGE_IDS | |
""" | |
# Add classes. We have one class. | |
# Naming the dataset crack, and the class crack | |
self.add_class("crack", 1, "crack") | |
# Which subset? | |
# "val": use hard-coded list above | |
# "train": use data from stage1_train minus the hard-coded list above | |
# else: use the data from the specified sub-directory | |
assert subset in ["train", "val", "test"] | |
subset_dir = "train" if subset in ["train", "val"] else subset | |
dataset_dir = os.path.join(dataset_dir, subset_dir) | |
if subset == "val": | |
image_ids = VAL_IMAGE_IDS | |
else: | |
# Get image ids from directory names | |
image_ids = next(os.walk(dataset_dir))[1] | |
if subset == "train": | |
image_ids = list(set(image_ids) - set(VAL_IMAGE_IDS)) | |
# Add images | |
for image_id in image_ids: | |
self.add_image( | |
"crack", | |
image_id=image_id, | |
path=os.path.join(dataset_dir, image_id, "images/{}.png".format(image_id))) | |
def load_mask(self, image_id): | |
"""Generate instance masks for an image. | |
Returns: | |
masks: A bool array of shape [height, width, instance count] with | |
one mask per instance. | |
class_ids: a 1D array of class IDs of the instance masks. | |
""" | |
info = self.image_info[image_id] | |
# Get mask directory from image path | |
mask_dir = os.path.join(os.path.dirname(os.path.dirname(info['path'])), "masks") | |
# Read mask files from .png image | |
mask = [] | |
for f in next(os.walk(mask_dir))[2]: | |
if f.endswith(".png"): | |
m = skimage.io.imread(os.path.join(mask_dir, f)).astype(np.bool) | |
mask.append(m) | |
mask = np.stack(mask, axis=-1) | |
# print("MASK SIZE:{}".format(mask.shape[2])) | |
if (mask.shape[2] != 1): | |
mask = np.squeeze(mask) | |
# print("MASK SIZE updated:{}".format(mask.shape)) | |
# Return mask, and array of class IDs of each instance. Since we have | |
# one class ID, we return an array of ones | |
return mask, np.ones([mask.shape[-1]], dtype=np.int32) | |
def image_reference(self, image_id): | |
"""Return the path of the image.""" | |
info = self.image_info[image_id] | |
if info["source"] == "crack": | |
return info["id"] | |
else: | |
super(self.__class__, self).image_reference(image_id) | |
############################################################ | |
# Training | |
############################################################ | |
def train(model, dataset_dir, subset): | |
"""Train the model.""" | |
# Training dataset. | |
dataset_train = CrackDataset() | |
print('Dataset dir: {}'.format(dataset_dir)) | |
dataset_train.load_crack(dataset_dir, subset) | |
dataset_train.prepare() | |
# Validation dataset | |
dataset_val = CrackDataset() | |
dataset_val.load_crack(dataset_dir, "val") | |
dataset_val.prepare() | |
# Image augmentation | |
# http://imgaug.readthedocs.io/en/latest/source/augmenters.html | |
augmentation = iaa.SomeOf((0, 2), [ | |
iaa.Fliplr(0.5), | |
iaa.Flipud(0.5), | |
iaa.OneOf([iaa.Affine(rotate=90), | |
iaa.Affine(rotate=180), | |
iaa.Affine(rotate=270)]), | |
iaa.Multiply((0.8, 1.5)), | |
iaa.GaussianBlur(sigma=(0.0, 5.0)) | |
]) | |
# *** This training schedule is an example. Update to your needs *** | |
# If starting from imagenet, train heads only for a bit | |
# since they have random weights | |
print("Train network heads") | |
model.train(dataset_train, dataset_val, | |
learning_rate=config.LEARNING_RATE, | |
epochs=20, | |
augmentation=augmentation, | |
layers='heads') | |
print("Train all layers") | |
model.train(dataset_train, dataset_val, | |
learning_rate=config.LEARNING_RATE, | |
epochs=40, | |
augmentation=augmentation, | |
layers='all') | |
############################################################ | |
# RLE Encoding | |
############################################################ | |
def rle_encode(mask): | |
"""Encodes a mask in Run Length Encoding (RLE). | |
Returns a string of space-separated values. | |
""" | |
assert mask.ndim == 2, "Mask must be of shape [Height, Width]" | |
# Flatten it column wise | |
m = mask.T.flatten() | |
# Compute gradient. Equals 1 or -1 at transition points | |
g = np.diff(np.concatenate([[0], m, [0]]), n=1) | |
# 1-based indicies of transition points (where gradient != 0) | |
rle = np.where(g != 0)[0].reshape([-1, 2]) + 1 | |
# Convert second index in each pair to lenth | |
rle[:, 1] = rle[:, 1] - rle[:, 0] | |
return " ".join(map(str, rle.flatten())) | |
def rle_decode(rle, shape): | |
"""Decodes an RLE encoded list of space separated | |
numbers and returns a binary mask.""" | |
rle = list(map(int, rle.split())) | |
rle = np.array(rle, dtype=np.int32).reshape([-1, 2]) | |
rle[:, 1] += rle[:, 0] | |
rle -= 1 | |
mask = np.zeros([shape[0] * shape[1]], np.bool) | |
for s, e in rle: | |
assert 0 <= s < mask.shape[0] | |
assert 1 <= e <= mask.shape[0], "shape: {} s {} e {}".format(shape, s, e) | |
mask[s:e] = 1 | |
# Reshape and transpose | |
mask = mask.reshape([shape[1], shape[0]]).T | |
return mask | |
def mask_to_rle(image_id, mask, scores): | |
"Encodes instance masks to submission format." | |
assert mask.ndim == 3, "Mask must be [H, W, count]" | |
# If mask is empty, return line with image ID only | |
if mask.shape[-1] == 0: | |
return "{},".format(image_id) | |
# Remove mask overlaps | |
# Multiply each instance mask by its score order | |
# then take the maximum across the last dimension | |
order = np.argsort(scores)[::-1] + 1 # 1-based descending | |
mask = np.max(mask * np.reshape(order, [1, 1, -1]), -1) | |
# Loop over instance masks | |
lines = [] | |
for o in order: | |
m = np.where(mask == o, 1, 0) | |
# Skip if empty | |
if m.sum() == 0.0: | |
continue | |
rle = rle_encode(m) | |
lines.append("{}, {}".format(image_id, rle)) | |
return "\n".join(lines) | |
############################################################ | |
# Detection | |
############################################################ | |
def detect(model, dataset_dir, subset): | |
"""Run detection on images in the given directory.""" | |
print("Running on {}".format(dataset_dir)) | |
# Create directory | |
if not os.path.exists(RESULTS_DIR): | |
os.makedirs(RESULTS_DIR) | |
submit_dir = "submit_{:%Y%m%dT%H%M%S}".format(datetime.datetime.now()) | |
submit_dir = os.path.join(RESULTS_DIR, submit_dir) | |
os.makedirs(submit_dir) | |
# Read dataset | |
dataset = CrackDataset() | |
dataset.load_crack(dataset_dir, subset) | |
dataset.prepare() | |
# Load over images | |
submission = [] | |
for image_id in dataset.image_ids: | |
# Load image and run detection | |
image = dataset.load_image(image_id) | |
# Detect objects | |
r = model.detect([image], verbose=0)[0] | |
# Encode image to RLE. Returns a string of multiple lines | |
source_id = dataset.image_info[image_id]["id"] | |
rle = mask_to_rle(source_id, r["masks"], r["scores"]) | |
submission.append(rle) | |
# Save image with masks | |
visualize.display_instances( | |
image, r['rois'], r['masks'], r['class_ids'], | |
dataset.class_names, r['scores'], | |
show_bbox=False, show_mask=False, | |
title="Predictions") | |
plt.savefig("{}/{}.png".format(submit_dir, dataset.image_info[image_id]["id"])) | |
# Save to csv file | |
submission = "ImageId,EncodedPixels\n" + "\n".join(submission) | |
file_path = os.path.join(submit_dir, "submit.csv") | |
with open(file_path, "w") as f: | |
f.write(submission) | |
print("Saved to ", submit_dir) | |
# Train the model | |
config = CrackConfig() | |
config.display() | |
model = modellib.MaskRCNN(mode="training", config=config, model_dir=DEFAULT_LOGS_DIR) | |
crack_dataset = CrackDataset() | |
weights_path = COCO_WEIGHTS_PATH | |
# Download weights file | |
if not os.path.exists(weights_path): | |
utils.download_trained_weights(weights_path) | |
# Exclude the last layers because they require a matching | |
# number of classes | |
model.load_weights(weights_path, by_name=True, exclude=[ | |
"mrcnn_class_logits", "mrcnn_bbox_fc", | |
"mrcnn_bbox", "mrcnn_mask"]) | |
train(model, "D:\git_projects\CrackSegmentation\\", "train") | |
config = CrackInferenceConfig() | |
config.display() | |
#model = modellib.MaskRCNN(mode="inference", config=config, model_dir=DEFAULT_LOGS_DIR) | |
model = modellib.MaskRCNN(mode="training", config=config, model_dir=DEFAULT_LOGS_DIR) | |
weights_path = model.find_last() | |
#weights_path = "D:\git_projects\CrackSegmentation\logs\crack20190410T1307\mask_rcnn_crack_0033.h5" | |
model.load_weights(weights_path, by_name=True) | |
detect(model, "D:\git_projects\CrackSegmentation\\", "test") |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment