Spaces:
Runtime error
Runtime error
Download lib/MiVOS_STCN/davisinteractive/robot/interactive_robot.py from Lim-Kevin/VideoInpainterHF: direct link, hf CLI and curl.
- Browser
- Download file 12.6 kB
-
https://huggingface.co/spaces/Lim-Kevin/VideoInpainterHF/resolve/main/lib/MiVOS_STCN/davisinteractive/robot/interactive_robot.py
- Command line
-
hf download hf://spaces/Lim-Kevin/VideoInpainterHF/lib/MiVOS_STCN/davisinteractive/robot/interactive_robot.py
-
curl -L -o interactive_robot.py https://huggingface.co/spaces/Lim-Kevin/VideoInpainterHF/resolve/main/lib/MiVOS_STCN/davisinteractive/robot/interactive_robot.py
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 | |