|
|
|
|
@ -1,21 +1,24 @@
|
|
|
|
|
# Copyright (C) 2019-2022 Intel Corporation
|
|
|
|
|
# Copyright (C) 2023 CVAT.ai Corporation
|
|
|
|
|
#
|
|
|
|
|
# SPDX-License-Identifier: MIT
|
|
|
|
|
|
|
|
|
|
from copy import copy, deepcopy
|
|
|
|
|
|
|
|
|
|
import math
|
|
|
|
|
import numpy as np
|
|
|
|
|
from itertools import chain
|
|
|
|
|
from scipy.optimize import linear_sum_assignment
|
|
|
|
|
from shapely import geometry
|
|
|
|
|
|
|
|
|
|
from cvat.apps.engine.models import ShapeType
|
|
|
|
|
from cvat.apps.engine.models import ShapeType, DimensionType
|
|
|
|
|
from cvat.apps.engine.serializers import LabeledDataSerializer
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class AnnotationIR:
|
|
|
|
|
def __init__(self, data=None):
|
|
|
|
|
def __init__(self, dimension, data=None):
|
|
|
|
|
self.reset()
|
|
|
|
|
self.dimension = dimension
|
|
|
|
|
if data:
|
|
|
|
|
self.tags = getattr(data, 'tags', []) or data['tags']
|
|
|
|
|
self.shapes = getattr(data, 'shapes', []) or data['shapes']
|
|
|
|
|
@ -80,7 +83,7 @@ class AnnotationIR:
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
def _slice_track(cls, track_, start, stop):
|
|
|
|
|
def _slice_track(cls, track_, start, stop, dimension):
|
|
|
|
|
def filter_track_shapes(shapes):
|
|
|
|
|
shapes = [s for s in shapes if cls._is_shape_inside(s, start, stop)]
|
|
|
|
|
drop_count = 0
|
|
|
|
|
@ -97,9 +100,9 @@ class AnnotationIR:
|
|
|
|
|
|
|
|
|
|
if len(segment_shapes) < len(track['shapes']):
|
|
|
|
|
for element in track.get('elements', []):
|
|
|
|
|
element = cls._slice_track(element, start, stop)
|
|
|
|
|
element = cls._slice_track(element, start, stop, dimension)
|
|
|
|
|
interpolated_shapes = TrackManager.get_interpolated_shapes(
|
|
|
|
|
track, start, stop)
|
|
|
|
|
track, start, stop, dimension)
|
|
|
|
|
scoped_shapes = filter_track_shapes(interpolated_shapes)
|
|
|
|
|
|
|
|
|
|
if scoped_shapes:
|
|
|
|
|
@ -121,8 +124,8 @@ class AnnotationIR:
|
|
|
|
|
return track
|
|
|
|
|
|
|
|
|
|
def slice(self, start, stop):
|
|
|
|
|
#makes a data copy from specified frame interval
|
|
|
|
|
splitted_data = AnnotationIR()
|
|
|
|
|
# makes a data copy from specified frame interval
|
|
|
|
|
splitted_data = AnnotationIR(self.dimension)
|
|
|
|
|
splitted_data.tags = [deepcopy(t)
|
|
|
|
|
for t in self.tags if self._is_shape_inside(t, start, stop)]
|
|
|
|
|
splitted_data.shapes = [deepcopy(s)
|
|
|
|
|
@ -130,7 +133,7 @@ class AnnotationIR:
|
|
|
|
|
splitted_tracks = []
|
|
|
|
|
for t in self.tracks:
|
|
|
|
|
if self._is_track_inside(t, start, stop):
|
|
|
|
|
track = self._slice_track(t, start, stop)
|
|
|
|
|
track = self._slice_track(t, start, stop, self.dimension)
|
|
|
|
|
if 0 < len(track['shapes']):
|
|
|
|
|
splitted_tracks.append(track)
|
|
|
|
|
splitted_data.tracks = splitted_tracks
|
|
|
|
|
@ -147,19 +150,19 @@ class AnnotationManager:
|
|
|
|
|
def __init__(self, data):
|
|
|
|
|
self.data = data
|
|
|
|
|
|
|
|
|
|
def merge(self, data, start_frame, overlap):
|
|
|
|
|
def merge(self, data, start_frame, overlap, dimension):
|
|
|
|
|
tags = TagManager(self.data.tags)
|
|
|
|
|
tags.merge(data.tags, start_frame, overlap)
|
|
|
|
|
tags.merge(data.tags, start_frame, overlap, dimension)
|
|
|
|
|
|
|
|
|
|
shapes = ShapeManager(self.data.shapes)
|
|
|
|
|
shapes.merge(data.shapes, start_frame, overlap)
|
|
|
|
|
shapes.merge(data.shapes, start_frame, overlap, dimension)
|
|
|
|
|
|
|
|
|
|
tracks = TrackManager(self.data.tracks)
|
|
|
|
|
tracks.merge(data.tracks, start_frame, overlap)
|
|
|
|
|
tracks = TrackManager(self.data.tracks, dimension)
|
|
|
|
|
tracks.merge(data.tracks, start_frame, overlap, dimension)
|
|
|
|
|
|
|
|
|
|
def to_shapes(self, end_frame):
|
|
|
|
|
def to_shapes(self, end_frame, dimension):
|
|
|
|
|
shapes = self.data.shapes
|
|
|
|
|
tracks = TrackManager(self.data.tracks)
|
|
|
|
|
tracks = TrackManager(self.data.tracks, dimension)
|
|
|
|
|
|
|
|
|
|
return shapes + tracks.to_shapes(end_frame)
|
|
|
|
|
|
|
|
|
|
@ -190,7 +193,7 @@ class ObjectManager:
|
|
|
|
|
raise NotImplementedError()
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
def _calc_objects_similarity(obj0, obj1, start_frame, overlap):
|
|
|
|
|
def _calc_objects_similarity(obj0, obj1, start_frame, overlap, dimension):
|
|
|
|
|
raise NotImplementedError()
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
@ -200,7 +203,7 @@ class ObjectManager:
|
|
|
|
|
def _modify_unmached_object(self, obj, end_frame):
|
|
|
|
|
raise NotImplementedError()
|
|
|
|
|
|
|
|
|
|
def merge(self, objects, start_frame, overlap):
|
|
|
|
|
def merge(self, objects, start_frame, overlap, dimension):
|
|
|
|
|
# 1. Split objects on two parts: new and which can be intersected
|
|
|
|
|
# with existing objects.
|
|
|
|
|
new_objects = [obj for obj in objects
|
|
|
|
|
@ -239,7 +242,7 @@ class ObjectManager:
|
|
|
|
|
for i, int_obj in enumerate(int_objects):
|
|
|
|
|
for j, old_obj in enumerate(old_objects):
|
|
|
|
|
cost_matrix[i][j] = 1 - self._calc_objects_similarity(
|
|
|
|
|
int_obj, old_obj, start_frame, overlap)
|
|
|
|
|
int_obj, old_obj, start_frame, overlap, dimension)
|
|
|
|
|
|
|
|
|
|
# 6. Find optimal solution using Hungarian algorithm.
|
|
|
|
|
row_ind, col_ind = linear_sum_assignment(cost_matrix)
|
|
|
|
|
@ -274,7 +277,7 @@ class TagManager(ObjectManager):
|
|
|
|
|
return 0.25
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
def _calc_objects_similarity(obj0, obj1, start_frame, overlap):
|
|
|
|
|
def _calc_objects_similarity(obj0, obj1, start_frame, overlap, dimension):
|
|
|
|
|
# TODO: improve the trivial implementation, compare attributes
|
|
|
|
|
return 1 if obj0["label_id"] == obj1["label_id"] else 0
|
|
|
|
|
|
|
|
|
|
@ -320,7 +323,7 @@ class ShapeManager(ObjectManager):
|
|
|
|
|
return 0.25
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
def _calc_objects_similarity(obj0, obj1, start_frame, overlap):
|
|
|
|
|
def _calc_objects_similarity(obj0, obj1, start_frame, overlap, dimension):
|
|
|
|
|
def _calc_polygons_similarity(p0, p1):
|
|
|
|
|
if p0.is_valid and p1.is_valid: # check validity of polygons
|
|
|
|
|
overlap_area = p0.intersection(p1).area
|
|
|
|
|
@ -335,17 +338,61 @@ class ShapeManager(ObjectManager):
|
|
|
|
|
has_same_label = obj0.get("label_id") == obj1.get("label_id")
|
|
|
|
|
if has_same_type and has_same_label:
|
|
|
|
|
if obj0["type"] == ShapeType.RECTANGLE:
|
|
|
|
|
# FIXME: need to consider rotated boxes
|
|
|
|
|
p0 = geometry.box(*obj0["points"])
|
|
|
|
|
p1 = geometry.box(*obj1["points"])
|
|
|
|
|
|
|
|
|
|
return _calc_polygons_similarity(p0, p1)
|
|
|
|
|
elif obj0["type"] == ShapeType.CUBOID and dimension == DimensionType.DIM_3D:
|
|
|
|
|
[x_c0, y_c0, z_c0] = obj0["points"][0:3]
|
|
|
|
|
[x_c1, y_c1, z_c1] = obj1["points"][0:3]
|
|
|
|
|
|
|
|
|
|
[x_len0, y_len0, z_len0] = obj0["points"][6:9]
|
|
|
|
|
[x_len1, y_len1, z_len1] = obj1["points"][6:9]
|
|
|
|
|
|
|
|
|
|
top_view_0 = [
|
|
|
|
|
x_c0 - x_len0 / 2,
|
|
|
|
|
y_c0 - y_len0 / 2,
|
|
|
|
|
x_c0 + x_len0 / 2,
|
|
|
|
|
y_c0 + y_len0 / 2
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
top_view_1 = [
|
|
|
|
|
x_c1 - x_len1 / 2,
|
|
|
|
|
y_c1 - y_len1 / 2,
|
|
|
|
|
x_c1 + x_len1 / 2,
|
|
|
|
|
y_c1 + y_len1 / 2
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
p_top0 = geometry.box(*top_view_0)
|
|
|
|
|
p_top1 = geometry.box(*top_view_1)
|
|
|
|
|
top_similarity =_calc_polygons_similarity(p_top0, p_top1)
|
|
|
|
|
|
|
|
|
|
side_view_0 = [
|
|
|
|
|
x_c0 - x_len0 / 2,
|
|
|
|
|
z_c0 - z_len0 / 2,
|
|
|
|
|
x_c0 + x_len0 / 2,
|
|
|
|
|
z_c0 + z_len0 / 2
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
side_view_1 = [
|
|
|
|
|
x_c1 - x_len1 / 2,
|
|
|
|
|
z_c1 - z_len1 / 2,
|
|
|
|
|
x_c1 + x_len1 / 2,
|
|
|
|
|
z_c1 + z_len1 / 2
|
|
|
|
|
]
|
|
|
|
|
p_side0 = geometry.box(*side_view_0)
|
|
|
|
|
p_side1 = geometry.box(*side_view_1)
|
|
|
|
|
side_similarity =_calc_polygons_similarity(p_side0, p_side1)
|
|
|
|
|
|
|
|
|
|
return top_similarity * side_similarity
|
|
|
|
|
elif obj0["type"] == ShapeType.POLYGON:
|
|
|
|
|
p0 = geometry.Polygon(pairwise(obj0["points"]))
|
|
|
|
|
p1 = geometry.Polygon(pairwise(obj1["points"]))
|
|
|
|
|
|
|
|
|
|
return _calc_polygons_similarity(p0, p1)
|
|
|
|
|
else:
|
|
|
|
|
return 0 # FIXME: need some similarity for points and polylines
|
|
|
|
|
return 0 # FIXME: need some similarity for points, polylines, ellipses and 2D cuboids
|
|
|
|
|
return 0
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
@ -357,11 +404,15 @@ class ShapeManager(ObjectManager):
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
class TrackManager(ObjectManager):
|
|
|
|
|
def __init__(self, objects, dimension):
|
|
|
|
|
self._dimension = dimension
|
|
|
|
|
super().__init__(objects)
|
|
|
|
|
|
|
|
|
|
def to_shapes(self, end_frame, end_skeleton_frame=None):
|
|
|
|
|
shapes = []
|
|
|
|
|
for idx, track in enumerate(self.objects):
|
|
|
|
|
track_shapes = []
|
|
|
|
|
for shape in TrackManager.get_interpolated_shapes(track, 0, end_frame):
|
|
|
|
|
for shape in TrackManager.get_interpolated_shapes(track, 0, end_frame, self._dimension):
|
|
|
|
|
shape["label_id"] = track["label_id"]
|
|
|
|
|
shape["group"] = track["group"]
|
|
|
|
|
shape["track_id"] = idx
|
|
|
|
|
@ -375,7 +426,7 @@ class TrackManager(ObjectManager):
|
|
|
|
|
track_shapes.append(shape)
|
|
|
|
|
|
|
|
|
|
if len(track.get("elements", [])):
|
|
|
|
|
element_tracks = TrackManager(track["elements"])
|
|
|
|
|
element_tracks = TrackManager(track["elements"], self._dimension)
|
|
|
|
|
element_shapes = element_tracks.to_shapes(end_frame, end_skeleton_frame=track_shapes[-1]["frame"])
|
|
|
|
|
|
|
|
|
|
for i in range(len(element_shapes) // len(track_shapes)):
|
|
|
|
|
@ -404,14 +455,14 @@ class TrackManager(ObjectManager):
|
|
|
|
|
return 0.5
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
def _calc_objects_similarity(obj0, obj1, start_frame, overlap):
|
|
|
|
|
def _calc_objects_similarity(obj0, obj1, start_frame, overlap, dimension):
|
|
|
|
|
if obj0["label_id"] == obj1["label_id"]:
|
|
|
|
|
# Here start_frame is the start frame of next segment
|
|
|
|
|
# and stop_frame is the stop frame of current segment
|
|
|
|
|
# end_frame == stop_frame + 1
|
|
|
|
|
end_frame = start_frame + overlap
|
|
|
|
|
obj0_shapes = TrackManager.get_interpolated_shapes(obj0, start_frame, end_frame)
|
|
|
|
|
obj1_shapes = TrackManager.get_interpolated_shapes(obj1, start_frame, end_frame)
|
|
|
|
|
obj0_shapes = TrackManager.get_interpolated_shapes(obj0, start_frame, end_frame, dimension)
|
|
|
|
|
obj1_shapes = TrackManager.get_interpolated_shapes(obj1, start_frame, end_frame, dimension)
|
|
|
|
|
obj0_shapes_by_frame = {shape["frame"]:shape for shape in obj0_shapes}
|
|
|
|
|
obj1_shapes_by_frame = {shape["frame"]:shape for shape in obj1_shapes}
|
|
|
|
|
assert obj0_shapes_by_frame and obj1_shapes_by_frame
|
|
|
|
|
@ -424,7 +475,7 @@ class TrackManager(ObjectManager):
|
|
|
|
|
if shape0["outside"] != shape1["outside"]:
|
|
|
|
|
error += 1
|
|
|
|
|
else:
|
|
|
|
|
error += 1 - ShapeManager._calc_objects_similarity(shape0, shape1, start_frame, overlap)
|
|
|
|
|
error += 1 - ShapeManager._calc_objects_similarity(shape0, shape1, start_frame, overlap, dimension)
|
|
|
|
|
count += 1
|
|
|
|
|
elif shape0 or shape1:
|
|
|
|
|
error += 1
|
|
|
|
|
@ -446,7 +497,7 @@ class TrackManager(ObjectManager):
|
|
|
|
|
self._modify_unmached_object(element, end_frame)
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
def get_interpolated_shapes(track, start_frame, end_frame):
|
|
|
|
|
def get_interpolated_shapes(track, start_frame, end_frame, dimension):
|
|
|
|
|
def copy_shape(source, frame, points=None, rotation=None):
|
|
|
|
|
copied = deepcopy(source)
|
|
|
|
|
copied["keyframe"] = False
|
|
|
|
|
@ -483,6 +534,23 @@ class TrackManager(ObjectManager):
|
|
|
|
|
|
|
|
|
|
return shapes
|
|
|
|
|
|
|
|
|
|
def simple_3d_interpolation(shape0, shape1):
|
|
|
|
|
result = simple_interpolation(shape0, shape1)
|
|
|
|
|
angles = (shape0["points"][3:6] + shape1["points"][3:6])
|
|
|
|
|
distance = shape1["frame"] - shape0["frame"]
|
|
|
|
|
|
|
|
|
|
for shape in result:
|
|
|
|
|
offset = (shape["frame"] - shape0["frame"]) / distance
|
|
|
|
|
for i, angle0 in enumerate(angles):
|
|
|
|
|
if i < 3:
|
|
|
|
|
angle1 = angles[i + 3]
|
|
|
|
|
angle0 = (angle0 if angle0 >= 0 else angle0 + math.pi * 2) * 180 / math.pi
|
|
|
|
|
angle1 = (angle1 if angle1 >= 0 else angle1 + math.pi * 2) * 180 / math.pi
|
|
|
|
|
angle = angle0 + find_angle_diff(angle1, angle0) * offset * math.pi / 180
|
|
|
|
|
shape["points"][i + 3] = angle if angle <= math.pi else angle - math.pi * 2
|
|
|
|
|
|
|
|
|
|
return result
|
|
|
|
|
|
|
|
|
|
def points_interpolation(shape0, shape1):
|
|
|
|
|
if len(shape0["points"]) == 2 and len(shape1["points"]) == 2:
|
|
|
|
|
return simple_interpolation(shape0, shape1)
|
|
|
|
|
@ -725,6 +793,8 @@ class TrackManager(ObjectManager):
|
|
|
|
|
raise NotImplementedError()
|
|
|
|
|
|
|
|
|
|
shapes = []
|
|
|
|
|
if dimension == DimensionType.DIM_3D:
|
|
|
|
|
shapes = simple_3d_interpolation(shape0, shape1)
|
|
|
|
|
if is_rectangle or is_cuboid or is_ellipse or is_skeleton:
|
|
|
|
|
shapes = simple_interpolation(shape0, shape1)
|
|
|
|
|
elif is_points:
|
|
|
|
|
@ -741,11 +811,15 @@ class TrackManager(ObjectManager):
|
|
|
|
|
for shape in sorted(track["shapes"], key=lambda shape: shape["frame"]):
|
|
|
|
|
curr_frame = shape["frame"]
|
|
|
|
|
if end_frame <= curr_frame:
|
|
|
|
|
if not prev_shape:
|
|
|
|
|
shape["keyframe"] = True
|
|
|
|
|
shapes.append(shape)
|
|
|
|
|
prev_shape = shape
|
|
|
|
|
break
|
|
|
|
|
# if we exceed endframe, we still need to interpolate using the next keyframe
|
|
|
|
|
# but we keep the results only up to end_frame
|
|
|
|
|
interpolated = interpolate(prev_shape, deepcopy(shape))
|
|
|
|
|
for shape in sorted(interpolated, key=lambda shape: shape["frame"]):
|
|
|
|
|
if shape["frame"] < end_frame:
|
|
|
|
|
shapes.append(shape)
|
|
|
|
|
else:
|
|
|
|
|
break
|
|
|
|
|
return shapes
|
|
|
|
|
|
|
|
|
|
if prev_shape:
|
|
|
|
|
assert shape["frame"] > prev_shape["frame"]
|
|
|
|
|
@ -760,6 +834,7 @@ class TrackManager(ObjectManager):
|
|
|
|
|
prev_shape = shape
|
|
|
|
|
|
|
|
|
|
if not prev_shape["outside"]:
|
|
|
|
|
# valid when the latest keyframe of a track less than end_frame and it is not outside, so, need to propagate
|
|
|
|
|
shape = deepcopy(prev_shape)
|
|
|
|
|
shape["frame"] = end_frame
|
|
|
|
|
shapes.extend(interpolate(prev_shape, shape))
|
|
|
|
|
|