Skip to content

Instantly share code, notes, and snippets.

@SpicySyntax
Created April 22, 2019 19:35
Show Gist options
  • Save SpicySyntax/c8fd433c52e13bc28f68cd7ea300378e to your computer and use it in GitHub Desktop.
Save SpicySyntax/c8fd433c52e13bc28f68cd7ea300378e to your computer and use it in GitHub Desktop.
CrackSegmentation
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