Kevin L
Initial commit
5fd3fc6
Raw History Blame Contribute Delete
12.6 kB
from __future__ import absolute_import, division
import time
import networkx as nx
import numpy as np
from scipy.ndimage import binary_dilation, binary_erosion
from scipy.special import comb
from skimage.filters import rank
from skimage.morphology import dilation, disk, erosion, medial_axis
from sklearn.neighbors import radius_neighbors_graph
from .. import logging
from ..metrics import batched_jaccard
from ..utils.operations import bezier_curve
__all__ = ['InteractiveScribblesRobot']
class InteractiveScribblesRobot(object):
""" Robot that generates realistic scribbles simulating human interaction.
# Attributes
kernel_size: Float. Fraction of the square root of the area used
to compute the dilation and erosion before computing the
skeleton of the error masks.
max_kernel_radius: Float. Maximum kernel radius when applying
dilation and erosion. Default 16 pixels.
min_nb_nodes: Integer. Number of nodes necessary to keep a connected
graph and convert it into a scribble.
nb_points: Integer. Number of points to sample the bezier curve
when converting the final paths into curves.
"""
def __init__(self,
kernel_size=.15,
max_kernel_radius=16,
min_nb_nodes=4,
nb_points=1000):
""" Robot constructor
"""
if kernel_size >= 1. or kernel_size < 0:
raise ValueError('kernel_size must be a value between [0, 1).')
self.kernel_size = kernel_size
self.max_kernel_radius = max_kernel_radius
self.min_nb_nodes = min_nb_nodes
self.nb_points = nb_points
def _generate_scribble_mask(self, mask):
""" Generate the skeleton from a mask
Given an error mask, the medial axis is computed to obtain the
skeleton of the objects. In order to obtain smoother skeleton and
remove small objects, an erosion and dilation operations are performed.
The kernel size used is proportional the squared of the area.
# Arguments
mask: Numpy Array. Error mask
Returns:
skel: Numpy Array. Skeleton mask
"""
mask = np.asarray(mask, dtype=np.uint8)
side = np.sqrt(np.sum(mask > 0))
mask_ = mask
# kernel_size = int(self.kernel_size * side)
kernel_radius = self.kernel_size * side * .5
kernel_radius = min(kernel_radius, self.max_kernel_radius)
logging.verbose(
'Erosion and dilation with kernel radius: {:.1f}'.format(
kernel_radius), 2)
compute = True
while kernel_radius > 1. and compute:
kernel = disk(kernel_radius)
mask_ = rank.minimum(mask.copy(), kernel)
mask_ = rank.maximum(mask_, kernel)
compute = False
if mask_.astype(np.bool).sum() == 0:
compute = True
prev_kernel_radius = kernel_radius
kernel_radius *= .9
logging.verbose('Reducing kernel radius from {:.1f} '.format(
prev_kernel_radius) +
'pixels to {:.1f}'.format(kernel_radius), 1)
mask_ = np.pad(
mask_, ((1, 1), (1, 1)), mode='constant', constant_values=False)
skel = medial_axis(mask_.astype(np.bool))
skel = skel[1:-1, 1:-1]
return skel
def _mask2graph(self, skeleton_mask):
""" Transforms a skeleton mask into a graph
Args:
skeleton_mask (ndarray): Skeleton mask
Returns:
tuple(nx.Graph, ndarray): Returns a tuple where the first element
is a Graph and the second element is an array of xy coordinates
indicating the coordinates for each Graph node.
If an empty mask is given, None is returned.
"""
mask = np.asarray(skeleton_mask, dtype=np.bool)
if np.sum(mask) == 0:
return None
h, w = mask.shape
x, y = np.arange(w), np.arange(h)
X, Y = np.meshgrid(x, y)
X, Y = X.ravel(), Y.ravel()
M = mask.ravel()
X, Y = X[M], Y[M]
points = np.c_[X, Y]
G = radius_neighbors_graph(points, np.sqrt(2), mode='distance')
T = nx.from_scipy_sparse_matrix(G)
return T, points
def _acyclics_subgraphs(self, G):
""" Divide a graph into connected components subgraphs
Divide a graph into connected components subgraphs and remove its
cycles removing the edge with higher weight inside the cycle. Also
prune the graphs by number of nodes in case the graph has not enought
nodes.
Args:
G (nx.Graph): Graph
Returns:
list(nx.Graph): Returns a list of graphs which are subgraphs of G
with cycles removed.
"""
if not isinstance(G, nx.Graph):
raise TypeError('G must be a nx.Graph instance')
S = [] # List of subgraphs of G
for c in nx.connected_components(G):
g = G.subgraph(c).copy()
# Remove all cycles that we may find
has_cycles = True
while has_cycles:
try:
cycle = nx.find_cycle(g)
weights = np.asarray([G[u][v]['weight'] for u, v in cycle])
idx = weights.argmax()
# Remove the edge with highest weight at cycle
g.remove_edge(*cycle[idx])
except nx.NetworkXNoCycle:
has_cycles = False
if len(g) < self.min_nb_nodes:
# Prune small subgraphs
logging.verbose('Remove a small line with {} nodes'.format(
len(g)), 1)
continue
S.append(g)
return S
def _longest_path_in_tree(self, G):
""" Given a tree graph, compute the longest path and return it
Given an undirected tree graph, compute the longest path and return it.
The approach use two shortest path transversals (shortest path in a
tree is the same as longest path). This could be improve but would
require implement it:
https://cs.stackexchange.com/questions/11263/longest-path-in-an-undirected-tree-with-only-one-traversal
Args:
G (nx.Graph): Graph which should be an undirected tree graph
Returns:
list(int): Returns a list of indexes of the nodes belonging to the
longest path.
"""
if not isinstance(G, nx.Graph):
raise TypeError('G must be a nx.Graph instance')
if not nx.is_tree(G):
raise ValueError('Graph G must be a tree (graph without cycles)')
# Compute the furthest node to the random node v
v = list(G.nodes())[0]
distance = nx.single_source_shortest_path_length(G, v)
vp = max(distance.items(), key=lambda x: x[1])[0]
# From this furthest point v' find again the longest path from it
distance = nx.single_source_shortest_path(G, vp)
longest_path = max(distance.values(), key=len)
# Return the longest path
return list(longest_path)
def interact(self,
sequence,
pred_masks,
gt_masks,
nb_objects=None,
frame=None):
""" Interaction of the Scribble robot given a prediction.
Given the sequence and a mask prediction, the robot will return a
scribble in the region that fails the most.
# Arguments
sequence: String. Name of the sequence to interact with.
pred_masks: Numpy Array. Array with the prediction masks. It must
be an integer array with shape (B x H x W), with B being the number
of frames of the sequence.
gt_masks: Numpy Array. Array with the ground truth of the sequence.
It must have the same data type and shape as `pred_masks`.
nb_objects: Integer. Number of objects in the ground truth mask. If
`None` the value will be infered from `y_true`. Setting this
value will speed up the computation.
frame: Integer. Frame to generate the scribble. If not given, the
worst frame given by the jaccard will be used.
# Returns
dict: Return a scribble (default representation).
"""
robot_start = time.time()
predictions = np.asarray(pred_masks, dtype=np.int)
annotations = np.asarray(gt_masks, dtype=np.int)
nb_frames = len(annotations)
if nb_objects is None:
obj_ids = np.unique(annotations)
obj_ids = obj_ids[(obj_ids > 0) & (obj_ids < 255)]
nb_objects = len(obj_ids)
obj_ids = [i for i in range(nb_objects + 1)]
# Infer height and width of the sequence
h, w = annotations.shape[1:3]
img_shape = np.asarray([w, h], dtype=np.float)
if frame is None:
jac = batched_jaccard(
annotations, predictions, nb_objects=nb_objects)
worst_frame = jac.argmin()
logging.verbose(
'For sequence {} the worst frames is #{} with Jaccard: {:.3f}'.
format(sequence, worst_frame, jac.min()), 2)
else:
worst_frame = frame
pred, gt = predictions[worst_frame], annotations[worst_frame]
scribbles = [[] for _ in range(nb_frames)]
for obj_id in obj_ids:
logging.verbose(
'Creating scribbles from error mask at object_id={}'.format(
obj_id), 2)
start_time = time.time()
error_mask = (gt == obj_id) & (pred != obj_id)
if error_mask.sum() == 0:
logging.info(
'Error mask of object ID {} is empty. Skip object ID.'.
format(obj_id))
continue
# Generate scribbles
skel_mask = self._generate_scribble_mask(error_mask)
skel_time = time.time() - start_time
logging.verbose(
'Time to compute the skeleton mask: {:.3f} ms'.format(
skel_time * 1000), 2)
if skel_mask.sum() == 0:
continue
G, P = self._mask2graph(skel_mask)
mask2graph_time = time.time() - start_time - skel_time
logging.verbose(
'Time to transform the skeleton mask into a graph: ' +
'{:.3f} ms'.format(mask2graph_time * 1000), 2)
t_start = time.time()
S = self._acyclics_subgraphs(G)
t = (time.time() - t_start) * 1000
logging.verbose(
'Time to split into connected components subgraphs ' +
'and remove the cycles: {:.3f} ms'.format(t), 2)
t_start = time.time()
longest_paths_idx = [self._longest_path_in_tree(s) for s in S]
longest_paths = [P[idx] for idx in longest_paths_idx]
t = (time.time() - t_start) * 1000
logging.verbose(
'Time to compute the longest path on the trees: {:.3f} ms'.
format(t), 2)
t_start = time.time()
scribbles_paths = [
bezier_curve(p, self.nb_points) for p in longest_paths
]
t = (time.time() - t_start) * 1000
logging.verbose(
'Time to compute the bezier curves: {:.3f} ms'.format(t), 2)
end_time = time.time()
logging.verbose(
'Generating the scribble for object id {} '.format(obj_id) +
'took {:.3f} ms'.format((end_time - start_time) * 1000), 2)
# Generate scribbles data file
for p in scribbles_paths:
p /= img_shape
path_data = {
'path': p.tolist(),
'object_id': int(obj_id),
'start_time': start_time,
'end_time': end_time
}
scribbles[worst_frame].append(path_data)
scribbles_data = {'scribbles': scribbles, 'sequence': sequence}
t = time.time() - robot_start
logging.info(('The robot took {:.3f} s to generate all the '
'scribbles for {} objects. Sequence {}.').format(
t, nb_objects, sequence))
return scribbles_data