Module vipy.activity

Expand source code Browse git
import numpy as np
from vipy.geometry import BoundingBox
from vipy.util import isstring, tolist
import uuid
import copy
from vipy.object import Track

try:
    import ujson as json  # faster
except ImportError:
    import json

ACTIVITY_GUID = int(uuid.uuid4().hex[0:8], 16)  

class Activity(object):
    """vipy.object.Activity class
    
    An activity is a grouping of one or more tracks involved in an activity within a given startframe and endframe.
    The activity occurs at a given (startframe, endframe), where these frame indexes are extracted at the provided framerate.
    All objects are passed by reference with a globally unique track ID, for the tracks involved with the activity.  This 
    is done since tracks can exist after an activity completes, and that tracks should update the spatial transformation of boxes.
    The shortlabel defines the string shown on the visualization video.

    Valid constructors

    ```python 
    t = vipy.object.Track(category='Person').add(...))
    a = vipy.object.Activity(startframe=0, endframe=10, category='Walking', tracks={t.id():t})
    ```

    """
    def __init__(self, startframe, endframe, framerate=None, label=None, shortlabel=None, category=None, tracks=None, attributes=None, actorid=None, confidence=None, id=None):
        assert not (label is not None and category is not None), "Activity() Constructor requires either label or category kwargs, not both"
        assert startframe <= endframe, "Start frame must be less than or equal to end frame"
        if tracks:
            tracks = [v for (k,v) in tracks.items()] if isinstance(tracks, dict) else tracks  # backwards compatible, track dictionary input
            assert (all([isstring(t) for t in tracks]) or all([isinstance(t, Track) for t in tracks])), "Invalid track input"
            if all([isinstance(t, Track) for t in tracks]):
                assert any([any([t.during(f) for f in range(startframe, endframe)]) for t in tracks]), "At least one track must be be present in at least one frame when this activity occurs"
                tracks = [t.id() for t in tracks]  
        trackid = set(tracks) if tracks is not None else (set([actorid]) if actorid is not None else set([]))  # only store IDs, not tracks
        
        if tracks is not None and actorid is not None and actorid not in trackid:
            trackid.add(actorid)

        if id is None:
            global ACTIVITY_GUID; self._id = hex(int(ACTIVITY_GUID))[2:];  ACTIVITY_GUID = ACTIVITY_GUID + 1;  # faster, increment package level UUID4 initialized GUID
        else:
            self._id = id  # use provided
        self._startframe = int(startframe)
        self._endframe = int(endframe)
        self._framerate = float(framerate) if framerate is not None else framerate
        self._label = category if category is not None else label        
        self._shortlabel = self._label if shortlabel is None else shortlabel
        self._trackid = trackid
        self._actorid = actorid

        self.attributes = attributes.copy() if attributes is not None else {}  # shallow copy
        if confidence is not None:
            self.attributes['confidence'] = float(confidence)

    def hasattribute(self, k):
        return k in self.attributes

    def confidence(self, c=None):
        if c is not None:
            self.attributes['confidence'] = float(c)
            return self
        else:
            return self.attributes['confidence'] if 'confidence' in self.attributes else None
    
    @classmethod
    def from_json(obj, s):
        d = json.loads(s) if not isinstance(s, dict) else s
        d = {k.lstrip('_'):v for (k,v) in d.items()}  # prettyjson (remove "_" prefix to attributes)                                        
        return obj(startframe=int(d['startframe']),
                   endframe=int(d['endframe']),
                   framerate=d['framerate'],
                   category=d['label'],
                   shortlabel=d['shortlabel'] if 'shortlabel' in d else None,
                   tracks=d['trackid'],
                   attributes=d['attributes'] if 'attributes' in d else None,
                   actorid=d['actorid'],
                   id=d['id'] if 'id' in d else None)
                
    def __len__(self):
        """Return activity length in frames, or zero if degenerate"""
        return max(0, self.endframe() - self.startframe())

    def duration(self, s=None, centered=False):
        """The length of the activity in seconds.

        Args:
            s: [float] The number of seconds for this activity, starting at the startframe
            centered: [bool] If true, then set the duration centered on the middle frame

        Returns:
            The duration in seconds of this activity object (if s=None)
            This activity object with the requested duration (if s!=None)
        """
        assert self.framerate() is not None, "Framerate must be set in constructor"
        if s is None:
            return len(self) / float(self.framerate())
        else:
            return (self.endframe(self.startframe() + int(round(s*self.framerate()))) if not centered else 
                    self.truncate(startframe=self.middleframe()-int(np.ceil(s*self.framerate())//2), endframe=self.middleframe()+int(np.ceil(s*self.framerate())//2)))
        
    def __repr__(self):
        return str('<vipy.activity: category="%s", frames=(%d,%d), tracks=%s%s>' % (self.category(), self.startframe(), self.endframe(), len(self.trackids()), '' if self.confidence() is None else ', confidence=%1.2f' % self.confidence()))

    def dict(self):
        """Return a python dictionary containing the relevant serialized attributes suitable for JSON encoding"""
        return self.json(s=None, encode=False)

    def __json__(self):
        """Serialization method for json package"""
        return self.json(encode=True)
    
    def json(self, encode=True):
        d = {k:v for (k,v) in self.__dict__.items() if not ((k == '_shortlabel' and v is None) or
                                                            (k == 'attributes' and (v is None or isinstance(v, dict) and len(v)==0)) or
                                                            (k == '_id' and v is None))}  # don't bother to store None values
        d = {k:v if k != '_trackid' else tuple(v) for (k,v) in d.items()}  # sets are non-serializable
        d = {k.lstrip('_'):v for (k,v) in d.items()}  # prettyjson (remove "_" prefix to attributes)                                                
        return json.dumps(d) if encode else d
    
    def actorid(self, actorid=None):
        if actorid is None:
            return self._actorid
        else:
            self._actorid = actorid
            if actorid not in self._trackid:
                self._trackid.add(actorid) if isinstance(self._trackid, set) else self._trackid.append(actorid)                    
            return self

    def startframe(self, f=None):
        if f is None:
            return self._startframe
        else:
            self._startframe = f
            return self
    
    def endframe(self, f=None):
        if f is None:
            return self._endframe
        else:
            self._endframe = int(f)
            return self

    def middleframe(self):
        """Return the middle frame number of the activity"""
        return int(np.round((self._endframe - self._startframe) / 2.0)) + self._startframe

    def _set_framerate(self, fps):
        """Override framerate conversion and just set the framerate attribute.  This should really be set only in constructor.  Use with caution!"""
        self._framerate = float(fps)
        return self

    def framerate(self, fps=None, speed=None):
        """Resample (startframe, endframe) from known original framerate set by constructor to be new framerate fps"""        
        if fps is None and speed is None:
            return self._framerate
        else:
            assert fps is not None or speed is not None, "Invalid input"
            assert not (fps is not None and speed is not None), "Invalid input"
            assert speed is None or speed > 0, "Invalid speed, must specify speed multiplier s=1, s=2 for 2x faster, s=0.5 for half slower"                    
            assert fps is None or fps > 0, "Invalid framerate"
            assert self._framerate is not None, "Framerate conversion requires that the framerate is known for current activities.  This must be provided to the vipy.object.Activity() constructor."

            fps = float(fps) if fps is not None else (1.0/speed)*self._framerate            
            (self._startframe, self._endframe) = [int(np.round(f*(fps/float(self._framerate)))) for f in (self._startframe, self._endframe)]
            self._framerate = float(fps)
            return self
    
    def category(self, label=None, shortlabel=None):
        """Change the label (and shortlabel) to the new label (and shortlabel)"""
        if label is not None:
            self._label = label
            return self.shortlabel(shortlabel) if shortlabel is not None else self
        else:
            return self._label

    def categoryif(self, ifcategory, tocategory=None):
        """If the current category is equal to ifcategory, then change it to newcategory.

        Args:
            
            ifcategory [dict, str]: May be a dictionary {ifcategory:tocategory}, or just an ifcategory
            tocategory [str]:  the target category 

        Returns:
        
            this object with the category changed.

        .. note:: This is useful for converting synonyms such as self.categoryif('person_sits', 'person_sitting')
        """
        assert (isinstance(ifcategory, dict) and tocategory is None) or tocategory is not None

        if isinstance(ifcategory, dict):
            for (k,v) in ifcategory.items():
                self.categoryif(k, v)
        elif self.category() == ifcategory:
            self.category(tocategory, shortlabel=None)
        return self

    def label(self, label=None, shortlabel=None):
        """Alias for category"""
        return self.category(label, shortlabel=shortlabel)

    def shortlabel(self, label=None):
        """A optional shorter label string to show in the visualizations"""                
        if label is not None:
            self._shortlabel = label
            return self
        else:
            return self._shortlabel

    def add(self, track):
        """Add the track id for the track to this activity, so that if the track is changed externally it is reflected here"""
        assert isinstance(track, Track), "Invalid input - must be vipy.object.Track"
        assert self.during_interval(track.startframe(), track.endframe()), "The track must be present during the activity"
        if track.id() not in self._trackid:
            self._trackid = list(self._trackid)
            self._trackid.append(track.id())
        return self

    def addid(self, trackid):
        """Add the track id for the track to this activity, so that if the track is changed externally it is reflected here"""
        if trackid not in self._trackid:
            self._trackid = list(self._trackid)
            self._trackid.append(trackid)
        return self
    
    def tracks(self):
        """alias for trackids"""
        return self.trackids()

    def cleartracks(self):
        """Remove all track IDs from this activity"""
        self._trackid = []
        return self

    def trackids(self):
        """Return a set of track IDs associated with this activity"""
        return set(self._trackid)

    def hasoverlap(self, other, threshold=0):
        """Return true if the temporal_iou is greater than the provided threshold between self and other Track or other Activity"""
        assert isinstance(other, Activity) or isinstance(other, Track), "Invalid input"
        assert threshold >= 0 and threshold <= 1, "Invalid temporal IOU threshold"
        return (((min(self._endframe, other.endframe()) - max(self._startframe, other.startframe())) > 0) if threshold == 0 else
                self.temporal_iou(other) > threshold)
        
    def isneighbor(self, other, framegate=10):
        return self.temporal_iou(other.clone().temporalpad(framegate)) > 0 

    def hastrack(self, track):
        """Is the track part of the activity?"""
        assert isstring(track) or isinstance(track, Track), "Invalid input - Must be a vipy.object.Track().id() or vipy.object.Track()"
        trackid = track.id() if isinstance(track, Track) else track
        return trackid in self._trackid

    def hastrackoverlap(self, track):
        """is the activity occurring during the interval when the track is occurring and is this track assigned to the activity?"""
        assert isinstance(track, Track)
        return self.hastrack(track) and self.temporal_iou(track) > 0

    def append(self, newtrack):
        """Append newtrack to this activity and set as actorid()"""
        assert isinstance(newtrack, Track), "Invalid input - must be vipy.object.Track"
        self._trackid.add(newtrack.id()) if isinstance(self._trackid, set) else self._trackid.append(newtrack.id())
        self.actorid(newtrack.id())
        return self

    def trackfilter(self, f):
        """Remove all tracks such that the lambda function f(trackid) resolves to False"""
        assert callable(f)
        self._trackid = [tid for tid in self._trackid if f(tid)]
        if self.actorid() not in self._trackid:
            self._actorid = None
        return self

    def replace(self, oldtrack, newtrack):
        """Replace oldtrack with newtrack if present in self._tracks.  Pass in a trackdict to share reference to track, so that track owner can modify the track and this object observes the change"""
        assert isinstance(oldtrack, Track) and isinstance(newtrack, Track), "Invalid input - must be vipy.object.Track"
        if self.hastrack(oldtrack):
            self._trackid.discard(oldtrack.id())
            self._trackid.add(newtrack.id())
            if self.actorid() == oldtrack.id():
                self.actorid(newtrack.id())
        return self    

    def replaceid(self, oldtrackid, newtrackid):
        """Replace oldtrack with newtrack if present in self._tracks.  Pass in a trackdict to share reference to track, so that track owner can modify the track and this object observes the change"""
        if self.hastrack(oldtrackid):
            ti = set(self._trackid)
            ti.discard(oldtrackid)
            ti.add(newtrackid)
            if self.actorid() == oldtrackid:
                self.actorid(newtrackid)
            self._trackid = list(ti)
        return self    
    
    def during(self, frame):
        """Is frame during the time interval (startframe, endframe) inclusive?"""
        return int(frame) >= self._startframe and int(frame) <= self._endframe

    def during_interval(self, startframe, endframe, inclusive=False):
        """Is the activity occurring for any frames within the interval [startframe, endframe) (non-inclusive of endframe)?"""
        ef = endframe+(0 if not inclusive else 1)
        return (startframe >= self._startframe and startframe <= self._endframe) or (ef >= self._startframe and ef <= self._endframe) or (startframe <= self._startframe and ef >= self._endframe)

    
    def union(self, other, confweight=0.5, maxconf=False):
        """Compute the union of the new activity other to this activity by updating the start and end times and computing the mean confidence.
        
           -Note: other must have the same category and track IDs as self
           -confweight [0,1]:  the convex combinatiopn weight applied to the new activity 
        """
        assert isinstance(other, Activity), "Invalid input"
        assert self._actorid == other._actorid, "Actor ID must be the same"
        assert self._label == other._label, "Assigned activity is a different category"
        assert self._framerate == other._framerate, "Invalid input"
        assert confweight >= 0 and confweight <= 1, "Confidence weight must be [0,1]"

        self.startframe(min(other._startframe, self._startframe))
        self.endframe(max(other._endframe, self._endframe))
        if other.confidence() is not None and self.confidence() is not None:
            self.confidence(float((1.0-confweight)*self.confidence() + confweight*other.confidence()) if not maxconf else float(max(self.confidence(), other.confidence())))  # running mean confidence or max
        return self
        
    def temporal_iou(self, other):
        """Return the temporal intersection over union of two activities or this activity and a track"""
        assert isinstance(other, Activity) or isinstance(other, Track), "Invalid input - must be vipy.activity.Activity or vipy.object.Track"
        assert self._framerate == other._framerate, "invalid input - framerate must match"
        (sf, ef) = (other._startframe, other._endframe) if isinstance(other, Activity) else (other.startframe(), other.endframe())  # attribute access is faster than methods
        
        t_start = min(self._startframe, sf)
        t_end = max(self._endframe, ef)
        t_union = float(t_end - t_start)
        
        t_start = max(self._startframe, sf)
        t_end = min(self._endframe, ef)
        t_intersection = float(t_end - t_start)
        
        return (t_intersection / t_union) if t_intersection > 0 else 0
    
    def offset(self, dt):
        self._startframe = int(self._startframe + dt)
        self._endframe = int(self._endframe + dt)
        return self

    def truncate(self, startframe=None, endframe=None):
        """Truncate the activity so that it is between startframe and endframe"""
        self._startframe = self._startframe if startframe is None else max(self._startframe, startframe)
        self._endframe = self._endframe if endframe is None else min(self._endframe, endframe)
        self._endframe = self._endframe if self._endframe > self._startframe else self._startframe  # degenerate truncation
        return self
            
    def id(self, newid=None):
        if newid is None:
            return self._id
        else:
            self._id = newid
            return self

    def clone(self, rekey=False):
        #a = copy.deepcopy(self)
        a = Activity.from_json(self.json(encode=False))
        if rekey:
            global ACTIVITY_GUID; a.id(newid=hex(int(ACTIVITY_GUID))[2:]);  ACTIVITY_GUID = ACTIVITY_GUID + 1;  # faster, increment package level UUID4 initialized GUID
        return a
    
    def temporalpad(self, df):
        """Add a temporal pad of df=(before frames, after frames) or df=pad frames to the start and end of the activity.  The padded start frame may be negative."""
        df = (df, df) if not isinstance(df, tuple) else df
        self._startframe -= int(df[0])
        self._endframe += int(df[1])
        return self  

    def padto(self, t):
        """Add a symmetric temporal pad so that the activity is at least t seconds long"""
        return self.temporalpad(int(np.ceil(self.framerate()*((t - self.duration())/2.0)))) if t > self.duration() else self

    def disjoint(self, other, strict=False):
        """Enforce disjoint activities with other by shifting the endframe or startframe of self to not overlap if they share the same tracks.
           Other may be an Activity() or list of Activity()
           if strict=True, then throw an exception if other or self is fully contained with the other, resulting in degenerate activity after disjoint
        """
        for o in tolist(other):
            assert isinstance(o, Activity), "Invalid input - must be vipy.activity.Activity() or list of activities"       
            if strict:
                assert not (o.during(self.startframe()) and o.during(self.endframe())), "Self cannot fully overlap other"
                assert not (self.during(o.startframe()) and self.during(o.endframe())), "Other cannot fully overlap self"
            if o.trackids() == self.trackids() and o.during(self.endframe()):
                self.endframe(o.startframe()-1)
            if o.trackids() == self.trackids() and o.during(self.startframe()):
                self.startframe(o.endframe()+1)
        return self  # may be zero length now

    def temporal_distance(self, other):
        """Return the temporal distance in frames between self and other which is the minimum frame difference between the end of one to the start of the other, or zero if they overlap"""
        assert isinstance(other, Activity), "Invalid input - must be vipy.activity.Activity()"
        return (max(self.startframe(), other.startframe()) - min(self.endframe(), other.endframe())) if self.temporal_iou(other) == 0 else 0

    def within(self, startframe, endframe):
        """Is the activity within the frame rate (startframe, endframe)?"""
        return self.startframe() >= startframe and self.endframe() <= endframe
    

Classes

class Activity (startframe, endframe, framerate=None, label=None, shortlabel=None, category=None, tracks=None, attributes=None, actorid=None, confidence=None, id=None)

vipy.object.Activity class

An activity is a grouping of one or more tracks involved in an activity within a given startframe and endframe. The activity occurs at a given (startframe, endframe), where these frame indexes are extracted at the provided framerate. All objects are passed by reference with a globally unique track ID, for the tracks involved with the activity. This is done since tracks can exist after an activity completes, and that tracks should update the spatial transformation of boxes. The shortlabel defines the string shown on the visualization video.

Valid constructors

t = vipy.object.Track(category='Person').add(...))
a = vipy.object.Activity(startframe=0, endframe=10, category='Walking', tracks={t.id():t})
Expand source code Browse git
class Activity(object):
    """vipy.object.Activity class
    
    An activity is a grouping of one or more tracks involved in an activity within a given startframe and endframe.
    The activity occurs at a given (startframe, endframe), where these frame indexes are extracted at the provided framerate.
    All objects are passed by reference with a globally unique track ID, for the tracks involved with the activity.  This 
    is done since tracks can exist after an activity completes, and that tracks should update the spatial transformation of boxes.
    The shortlabel defines the string shown on the visualization video.

    Valid constructors

    ```python 
    t = vipy.object.Track(category='Person').add(...))
    a = vipy.object.Activity(startframe=0, endframe=10, category='Walking', tracks={t.id():t})
    ```

    """
    def __init__(self, startframe, endframe, framerate=None, label=None, shortlabel=None, category=None, tracks=None, attributes=None, actorid=None, confidence=None, id=None):
        assert not (label is not None and category is not None), "Activity() Constructor requires either label or category kwargs, not both"
        assert startframe <= endframe, "Start frame must be less than or equal to end frame"
        if tracks:
            tracks = [v for (k,v) in tracks.items()] if isinstance(tracks, dict) else tracks  # backwards compatible, track dictionary input
            assert (all([isstring(t) for t in tracks]) or all([isinstance(t, Track) for t in tracks])), "Invalid track input"
            if all([isinstance(t, Track) for t in tracks]):
                assert any([any([t.during(f) for f in range(startframe, endframe)]) for t in tracks]), "At least one track must be be present in at least one frame when this activity occurs"
                tracks = [t.id() for t in tracks]  
        trackid = set(tracks) if tracks is not None else (set([actorid]) if actorid is not None else set([]))  # only store IDs, not tracks
        
        if tracks is not None and actorid is not None and actorid not in trackid:
            trackid.add(actorid)

        if id is None:
            global ACTIVITY_GUID; self._id = hex(int(ACTIVITY_GUID))[2:];  ACTIVITY_GUID = ACTIVITY_GUID + 1;  # faster, increment package level UUID4 initialized GUID
        else:
            self._id = id  # use provided
        self._startframe = int(startframe)
        self._endframe = int(endframe)
        self._framerate = float(framerate) if framerate is not None else framerate
        self._label = category if category is not None else label        
        self._shortlabel = self._label if shortlabel is None else shortlabel
        self._trackid = trackid
        self._actorid = actorid

        self.attributes = attributes.copy() if attributes is not None else {}  # shallow copy
        if confidence is not None:
            self.attributes['confidence'] = float(confidence)

    def hasattribute(self, k):
        return k in self.attributes

    def confidence(self, c=None):
        if c is not None:
            self.attributes['confidence'] = float(c)
            return self
        else:
            return self.attributes['confidence'] if 'confidence' in self.attributes else None
    
    @classmethod
    def from_json(obj, s):
        d = json.loads(s) if not isinstance(s, dict) else s
        d = {k.lstrip('_'):v for (k,v) in d.items()}  # prettyjson (remove "_" prefix to attributes)                                        
        return obj(startframe=int(d['startframe']),
                   endframe=int(d['endframe']),
                   framerate=d['framerate'],
                   category=d['label'],
                   shortlabel=d['shortlabel'] if 'shortlabel' in d else None,
                   tracks=d['trackid'],
                   attributes=d['attributes'] if 'attributes' in d else None,
                   actorid=d['actorid'],
                   id=d['id'] if 'id' in d else None)
                
    def __len__(self):
        """Return activity length in frames, or zero if degenerate"""
        return max(0, self.endframe() - self.startframe())

    def duration(self, s=None, centered=False):
        """The length of the activity in seconds.

        Args:
            s: [float] The number of seconds for this activity, starting at the startframe
            centered: [bool] If true, then set the duration centered on the middle frame

        Returns:
            The duration in seconds of this activity object (if s=None)
            This activity object with the requested duration (if s!=None)
        """
        assert self.framerate() is not None, "Framerate must be set in constructor"
        if s is None:
            return len(self) / float(self.framerate())
        else:
            return (self.endframe(self.startframe() + int(round(s*self.framerate()))) if not centered else 
                    self.truncate(startframe=self.middleframe()-int(np.ceil(s*self.framerate())//2), endframe=self.middleframe()+int(np.ceil(s*self.framerate())//2)))
        
    def __repr__(self):
        return str('<vipy.activity: category="%s", frames=(%d,%d), tracks=%s%s>' % (self.category(), self.startframe(), self.endframe(), len(self.trackids()), '' if self.confidence() is None else ', confidence=%1.2f' % self.confidence()))

    def dict(self):
        """Return a python dictionary containing the relevant serialized attributes suitable for JSON encoding"""
        return self.json(s=None, encode=False)

    def __json__(self):
        """Serialization method for json package"""
        return self.json(encode=True)
    
    def json(self, encode=True):
        d = {k:v for (k,v) in self.__dict__.items() if not ((k == '_shortlabel' and v is None) or
                                                            (k == 'attributes' and (v is None or isinstance(v, dict) and len(v)==0)) or
                                                            (k == '_id' and v is None))}  # don't bother to store None values
        d = {k:v if k != '_trackid' else tuple(v) for (k,v) in d.items()}  # sets are non-serializable
        d = {k.lstrip('_'):v for (k,v) in d.items()}  # prettyjson (remove "_" prefix to attributes)                                                
        return json.dumps(d) if encode else d
    
    def actorid(self, actorid=None):
        if actorid is None:
            return self._actorid
        else:
            self._actorid = actorid
            if actorid not in self._trackid:
                self._trackid.add(actorid) if isinstance(self._trackid, set) else self._trackid.append(actorid)                    
            return self

    def startframe(self, f=None):
        if f is None:
            return self._startframe
        else:
            self._startframe = f
            return self
    
    def endframe(self, f=None):
        if f is None:
            return self._endframe
        else:
            self._endframe = int(f)
            return self

    def middleframe(self):
        """Return the middle frame number of the activity"""
        return int(np.round((self._endframe - self._startframe) / 2.0)) + self._startframe

    def _set_framerate(self, fps):
        """Override framerate conversion and just set the framerate attribute.  This should really be set only in constructor.  Use with caution!"""
        self._framerate = float(fps)
        return self

    def framerate(self, fps=None, speed=None):
        """Resample (startframe, endframe) from known original framerate set by constructor to be new framerate fps"""        
        if fps is None and speed is None:
            return self._framerate
        else:
            assert fps is not None or speed is not None, "Invalid input"
            assert not (fps is not None and speed is not None), "Invalid input"
            assert speed is None or speed > 0, "Invalid speed, must specify speed multiplier s=1, s=2 for 2x faster, s=0.5 for half slower"                    
            assert fps is None or fps > 0, "Invalid framerate"
            assert self._framerate is not None, "Framerate conversion requires that the framerate is known for current activities.  This must be provided to the vipy.object.Activity() constructor."

            fps = float(fps) if fps is not None else (1.0/speed)*self._framerate            
            (self._startframe, self._endframe) = [int(np.round(f*(fps/float(self._framerate)))) for f in (self._startframe, self._endframe)]
            self._framerate = float(fps)
            return self
    
    def category(self, label=None, shortlabel=None):
        """Change the label (and shortlabel) to the new label (and shortlabel)"""
        if label is not None:
            self._label = label
            return self.shortlabel(shortlabel) if shortlabel is not None else self
        else:
            return self._label

    def categoryif(self, ifcategory, tocategory=None):
        """If the current category is equal to ifcategory, then change it to newcategory.

        Args:
            
            ifcategory [dict, str]: May be a dictionary {ifcategory:tocategory}, or just an ifcategory
            tocategory [str]:  the target category 

        Returns:
        
            this object with the category changed.

        .. note:: This is useful for converting synonyms such as self.categoryif('person_sits', 'person_sitting')
        """
        assert (isinstance(ifcategory, dict) and tocategory is None) or tocategory is not None

        if isinstance(ifcategory, dict):
            for (k,v) in ifcategory.items():
                self.categoryif(k, v)
        elif self.category() == ifcategory:
            self.category(tocategory, shortlabel=None)
        return self

    def label(self, label=None, shortlabel=None):
        """Alias for category"""
        return self.category(label, shortlabel=shortlabel)

    def shortlabel(self, label=None):
        """A optional shorter label string to show in the visualizations"""                
        if label is not None:
            self._shortlabel = label
            return self
        else:
            return self._shortlabel

    def add(self, track):
        """Add the track id for the track to this activity, so that if the track is changed externally it is reflected here"""
        assert isinstance(track, Track), "Invalid input - must be vipy.object.Track"
        assert self.during_interval(track.startframe(), track.endframe()), "The track must be present during the activity"
        if track.id() not in self._trackid:
            self._trackid = list(self._trackid)
            self._trackid.append(track.id())
        return self

    def addid(self, trackid):
        """Add the track id for the track to this activity, so that if the track is changed externally it is reflected here"""
        if trackid not in self._trackid:
            self._trackid = list(self._trackid)
            self._trackid.append(trackid)
        return self
    
    def tracks(self):
        """alias for trackids"""
        return self.trackids()

    def cleartracks(self):
        """Remove all track IDs from this activity"""
        self._trackid = []
        return self

    def trackids(self):
        """Return a set of track IDs associated with this activity"""
        return set(self._trackid)

    def hasoverlap(self, other, threshold=0):
        """Return true if the temporal_iou is greater than the provided threshold between self and other Track or other Activity"""
        assert isinstance(other, Activity) or isinstance(other, Track), "Invalid input"
        assert threshold >= 0 and threshold <= 1, "Invalid temporal IOU threshold"
        return (((min(self._endframe, other.endframe()) - max(self._startframe, other.startframe())) > 0) if threshold == 0 else
                self.temporal_iou(other) > threshold)
        
    def isneighbor(self, other, framegate=10):
        return self.temporal_iou(other.clone().temporalpad(framegate)) > 0 

    def hastrack(self, track):
        """Is the track part of the activity?"""
        assert isstring(track) or isinstance(track, Track), "Invalid input - Must be a vipy.object.Track().id() or vipy.object.Track()"
        trackid = track.id() if isinstance(track, Track) else track
        return trackid in self._trackid

    def hastrackoverlap(self, track):
        """is the activity occurring during the interval when the track is occurring and is this track assigned to the activity?"""
        assert isinstance(track, Track)
        return self.hastrack(track) and self.temporal_iou(track) > 0

    def append(self, newtrack):
        """Append newtrack to this activity and set as actorid()"""
        assert isinstance(newtrack, Track), "Invalid input - must be vipy.object.Track"
        self._trackid.add(newtrack.id()) if isinstance(self._trackid, set) else self._trackid.append(newtrack.id())
        self.actorid(newtrack.id())
        return self

    def trackfilter(self, f):
        """Remove all tracks such that the lambda function f(trackid) resolves to False"""
        assert callable(f)
        self._trackid = [tid for tid in self._trackid if f(tid)]
        if self.actorid() not in self._trackid:
            self._actorid = None
        return self

    def replace(self, oldtrack, newtrack):
        """Replace oldtrack with newtrack if present in self._tracks.  Pass in a trackdict to share reference to track, so that track owner can modify the track and this object observes the change"""
        assert isinstance(oldtrack, Track) and isinstance(newtrack, Track), "Invalid input - must be vipy.object.Track"
        if self.hastrack(oldtrack):
            self._trackid.discard(oldtrack.id())
            self._trackid.add(newtrack.id())
            if self.actorid() == oldtrack.id():
                self.actorid(newtrack.id())
        return self    

    def replaceid(self, oldtrackid, newtrackid):
        """Replace oldtrack with newtrack if present in self._tracks.  Pass in a trackdict to share reference to track, so that track owner can modify the track and this object observes the change"""
        if self.hastrack(oldtrackid):
            ti = set(self._trackid)
            ti.discard(oldtrackid)
            ti.add(newtrackid)
            if self.actorid() == oldtrackid:
                self.actorid(newtrackid)
            self._trackid = list(ti)
        return self    
    
    def during(self, frame):
        """Is frame during the time interval (startframe, endframe) inclusive?"""
        return int(frame) >= self._startframe and int(frame) <= self._endframe

    def during_interval(self, startframe, endframe, inclusive=False):
        """Is the activity occurring for any frames within the interval [startframe, endframe) (non-inclusive of endframe)?"""
        ef = endframe+(0 if not inclusive else 1)
        return (startframe >= self._startframe and startframe <= self._endframe) or (ef >= self._startframe and ef <= self._endframe) or (startframe <= self._startframe and ef >= self._endframe)

    
    def union(self, other, confweight=0.5, maxconf=False):
        """Compute the union of the new activity other to this activity by updating the start and end times and computing the mean confidence.
        
           -Note: other must have the same category and track IDs as self
           -confweight [0,1]:  the convex combinatiopn weight applied to the new activity 
        """
        assert isinstance(other, Activity), "Invalid input"
        assert self._actorid == other._actorid, "Actor ID must be the same"
        assert self._label == other._label, "Assigned activity is a different category"
        assert self._framerate == other._framerate, "Invalid input"
        assert confweight >= 0 and confweight <= 1, "Confidence weight must be [0,1]"

        self.startframe(min(other._startframe, self._startframe))
        self.endframe(max(other._endframe, self._endframe))
        if other.confidence() is not None and self.confidence() is not None:
            self.confidence(float((1.0-confweight)*self.confidence() + confweight*other.confidence()) if not maxconf else float(max(self.confidence(), other.confidence())))  # running mean confidence or max
        return self
        
    def temporal_iou(self, other):
        """Return the temporal intersection over union of two activities or this activity and a track"""
        assert isinstance(other, Activity) or isinstance(other, Track), "Invalid input - must be vipy.activity.Activity or vipy.object.Track"
        assert self._framerate == other._framerate, "invalid input - framerate must match"
        (sf, ef) = (other._startframe, other._endframe) if isinstance(other, Activity) else (other.startframe(), other.endframe())  # attribute access is faster than methods
        
        t_start = min(self._startframe, sf)
        t_end = max(self._endframe, ef)
        t_union = float(t_end - t_start)
        
        t_start = max(self._startframe, sf)
        t_end = min(self._endframe, ef)
        t_intersection = float(t_end - t_start)
        
        return (t_intersection / t_union) if t_intersection > 0 else 0
    
    def offset(self, dt):
        self._startframe = int(self._startframe + dt)
        self._endframe = int(self._endframe + dt)
        return self

    def truncate(self, startframe=None, endframe=None):
        """Truncate the activity so that it is between startframe and endframe"""
        self._startframe = self._startframe if startframe is None else max(self._startframe, startframe)
        self._endframe = self._endframe if endframe is None else min(self._endframe, endframe)
        self._endframe = self._endframe if self._endframe > self._startframe else self._startframe  # degenerate truncation
        return self
            
    def id(self, newid=None):
        if newid is None:
            return self._id
        else:
            self._id = newid
            return self

    def clone(self, rekey=False):
        #a = copy.deepcopy(self)
        a = Activity.from_json(self.json(encode=False))
        if rekey:
            global ACTIVITY_GUID; a.id(newid=hex(int(ACTIVITY_GUID))[2:]);  ACTIVITY_GUID = ACTIVITY_GUID + 1;  # faster, increment package level UUID4 initialized GUID
        return a
    
    def temporalpad(self, df):
        """Add a temporal pad of df=(before frames, after frames) or df=pad frames to the start and end of the activity.  The padded start frame may be negative."""
        df = (df, df) if not isinstance(df, tuple) else df
        self._startframe -= int(df[0])
        self._endframe += int(df[1])
        return self  

    def padto(self, t):
        """Add a symmetric temporal pad so that the activity is at least t seconds long"""
        return self.temporalpad(int(np.ceil(self.framerate()*((t - self.duration())/2.0)))) if t > self.duration() else self

    def disjoint(self, other, strict=False):
        """Enforce disjoint activities with other by shifting the endframe or startframe of self to not overlap if they share the same tracks.
           Other may be an Activity() or list of Activity()
           if strict=True, then throw an exception if other or self is fully contained with the other, resulting in degenerate activity after disjoint
        """
        for o in tolist(other):
            assert isinstance(o, Activity), "Invalid input - must be vipy.activity.Activity() or list of activities"       
            if strict:
                assert not (o.during(self.startframe()) and o.during(self.endframe())), "Self cannot fully overlap other"
                assert not (self.during(o.startframe()) and self.during(o.endframe())), "Other cannot fully overlap self"
            if o.trackids() == self.trackids() and o.during(self.endframe()):
                self.endframe(o.startframe()-1)
            if o.trackids() == self.trackids() and o.during(self.startframe()):
                self.startframe(o.endframe()+1)
        return self  # may be zero length now

    def temporal_distance(self, other):
        """Return the temporal distance in frames between self and other which is the minimum frame difference between the end of one to the start of the other, or zero if they overlap"""
        assert isinstance(other, Activity), "Invalid input - must be vipy.activity.Activity()"
        return (max(self.startframe(), other.startframe()) - min(self.endframe(), other.endframe())) if self.temporal_iou(other) == 0 else 0

    def within(self, startframe, endframe):
        """Is the activity within the frame rate (startframe, endframe)?"""
        return self.startframe() >= startframe and self.endframe() <= endframe

Static methods

def from_json(s)
Expand source code Browse git
@classmethod
def from_json(obj, s):
    d = json.loads(s) if not isinstance(s, dict) else s
    d = {k.lstrip('_'):v for (k,v) in d.items()}  # prettyjson (remove "_" prefix to attributes)                                        
    return obj(startframe=int(d['startframe']),
               endframe=int(d['endframe']),
               framerate=d['framerate'],
               category=d['label'],
               shortlabel=d['shortlabel'] if 'shortlabel' in d else None,
               tracks=d['trackid'],
               attributes=d['attributes'] if 'attributes' in d else None,
               actorid=d['actorid'],
               id=d['id'] if 'id' in d else None)

Methods

def actorid(self, actorid=None)
Expand source code Browse git
def actorid(self, actorid=None):
    if actorid is None:
        return self._actorid
    else:
        self._actorid = actorid
        if actorid not in self._trackid:
            self._trackid.add(actorid) if isinstance(self._trackid, set) else self._trackid.append(actorid)                    
        return self
def add(self, track)

Add the track id for the track to this activity, so that if the track is changed externally it is reflected here

Expand source code Browse git
def add(self, track):
    """Add the track id for the track to this activity, so that if the track is changed externally it is reflected here"""
    assert isinstance(track, Track), "Invalid input - must be vipy.object.Track"
    assert self.during_interval(track.startframe(), track.endframe()), "The track must be present during the activity"
    if track.id() not in self._trackid:
        self._trackid = list(self._trackid)
        self._trackid.append(track.id())
    return self
def addid(self, trackid)

Add the track id for the track to this activity, so that if the track is changed externally it is reflected here

Expand source code Browse git
def addid(self, trackid):
    """Add the track id for the track to this activity, so that if the track is changed externally it is reflected here"""
    if trackid not in self._trackid:
        self._trackid = list(self._trackid)
        self._trackid.append(trackid)
    return self
def append(self, newtrack)

Append newtrack to this activity and set as actorid()

Expand source code Browse git
def append(self, newtrack):
    """Append newtrack to this activity and set as actorid()"""
    assert isinstance(newtrack, Track), "Invalid input - must be vipy.object.Track"
    self._trackid.add(newtrack.id()) if isinstance(self._trackid, set) else self._trackid.append(newtrack.id())
    self.actorid(newtrack.id())
    return self
def category(self, label=None, shortlabel=None)

Change the label (and shortlabel) to the new label (and shortlabel)

Expand source code Browse git
def category(self, label=None, shortlabel=None):
    """Change the label (and shortlabel) to the new label (and shortlabel)"""
    if label is not None:
        self._label = label
        return self.shortlabel(shortlabel) if shortlabel is not None else self
    else:
        return self._label
def categoryif(self, ifcategory, tocategory=None)

If the current category is equal to ifcategory, then change it to newcategory.

Args

ifcategory [dict, str]: May be a dictionary {ifcategory:tocategory}, or just an ifcategory tocategory [str]: the target category

Returns

this object with the category changed.

Note: This is useful for converting synonyms such as self.categoryif('person_sits', 'person_sitting')

Expand source code Browse git
def categoryif(self, ifcategory, tocategory=None):
    """If the current category is equal to ifcategory, then change it to newcategory.

    Args:
        
        ifcategory [dict, str]: May be a dictionary {ifcategory:tocategory}, or just an ifcategory
        tocategory [str]:  the target category 

    Returns:
    
        this object with the category changed.

    .. note:: This is useful for converting synonyms such as self.categoryif('person_sits', 'person_sitting')
    """
    assert (isinstance(ifcategory, dict) and tocategory is None) or tocategory is not None

    if isinstance(ifcategory, dict):
        for (k,v) in ifcategory.items():
            self.categoryif(k, v)
    elif self.category() == ifcategory:
        self.category(tocategory, shortlabel=None)
    return self
def cleartracks(self)

Remove all track IDs from this activity

Expand source code Browse git
def cleartracks(self):
    """Remove all track IDs from this activity"""
    self._trackid = []
    return self
def clone(self, rekey=False)
Expand source code Browse git
def clone(self, rekey=False):
    #a = copy.deepcopy(self)
    a = Activity.from_json(self.json(encode=False))
    if rekey:
        global ACTIVITY_GUID; a.id(newid=hex(int(ACTIVITY_GUID))[2:]);  ACTIVITY_GUID = ACTIVITY_GUID + 1;  # faster, increment package level UUID4 initialized GUID
    return a
def confidence(self, c=None)
Expand source code Browse git
def confidence(self, c=None):
    if c is not None:
        self.attributes['confidence'] = float(c)
        return self
    else:
        return self.attributes['confidence'] if 'confidence' in self.attributes else None
def dict(self)

Return a python dictionary containing the relevant serialized attributes suitable for JSON encoding

Expand source code Browse git
def dict(self):
    """Return a python dictionary containing the relevant serialized attributes suitable for JSON encoding"""
    return self.json(s=None, encode=False)
def disjoint(self, other, strict=False)

Enforce disjoint activities with other by shifting the endframe or startframe of self to not overlap if they share the same tracks. Other may be an Activity() or list of Activity() if strict=True, then throw an exception if other or self is fully contained with the other, resulting in degenerate activity after disjoint

Expand source code Browse git
def disjoint(self, other, strict=False):
    """Enforce disjoint activities with other by shifting the endframe or startframe of self to not overlap if they share the same tracks.
       Other may be an Activity() or list of Activity()
       if strict=True, then throw an exception if other or self is fully contained with the other, resulting in degenerate activity after disjoint
    """
    for o in tolist(other):
        assert isinstance(o, Activity), "Invalid input - must be vipy.activity.Activity() or list of activities"       
        if strict:
            assert not (o.during(self.startframe()) and o.during(self.endframe())), "Self cannot fully overlap other"
            assert not (self.during(o.startframe()) and self.during(o.endframe())), "Other cannot fully overlap self"
        if o.trackids() == self.trackids() and o.during(self.endframe()):
            self.endframe(o.startframe()-1)
        if o.trackids() == self.trackids() and o.during(self.startframe()):
            self.startframe(o.endframe()+1)
    return self  # may be zero length now
def duration(self, s=None, centered=False)

The length of the activity in seconds.

Args

s
[float] The number of seconds for this activity, starting at the startframe
centered
[bool] If true, then set the duration centered on the middle frame

Returns

The duration in seconds of this activity object (if s=None) This activity object with the requested duration (if s!=None)

Expand source code Browse git
def duration(self, s=None, centered=False):
    """The length of the activity in seconds.

    Args:
        s: [float] The number of seconds for this activity, starting at the startframe
        centered: [bool] If true, then set the duration centered on the middle frame

    Returns:
        The duration in seconds of this activity object (if s=None)
        This activity object with the requested duration (if s!=None)
    """
    assert self.framerate() is not None, "Framerate must be set in constructor"
    if s is None:
        return len(self) / float(self.framerate())
    else:
        return (self.endframe(self.startframe() + int(round(s*self.framerate()))) if not centered else 
                self.truncate(startframe=self.middleframe()-int(np.ceil(s*self.framerate())//2), endframe=self.middleframe()+int(np.ceil(s*self.framerate())//2)))
def during(self, frame)

Is frame during the time interval (startframe, endframe) inclusive?

Expand source code Browse git
def during(self, frame):
    """Is frame during the time interval (startframe, endframe) inclusive?"""
    return int(frame) >= self._startframe and int(frame) <= self._endframe
def during_interval(self, startframe, endframe, inclusive=False)

Is the activity occurring for any frames within the interval [startframe, endframe) (non-inclusive of endframe)?

Expand source code Browse git
def during_interval(self, startframe, endframe, inclusive=False):
    """Is the activity occurring for any frames within the interval [startframe, endframe) (non-inclusive of endframe)?"""
    ef = endframe+(0 if not inclusive else 1)
    return (startframe >= self._startframe and startframe <= self._endframe) or (ef >= self._startframe and ef <= self._endframe) or (startframe <= self._startframe and ef >= self._endframe)
def endframe(self, f=None)
Expand source code Browse git
def endframe(self, f=None):
    if f is None:
        return self._endframe
    else:
        self._endframe = int(f)
        return self
def framerate(self, fps=None, speed=None)

Resample (startframe, endframe) from known original framerate set by constructor to be new framerate fps

Expand source code Browse git
def framerate(self, fps=None, speed=None):
    """Resample (startframe, endframe) from known original framerate set by constructor to be new framerate fps"""        
    if fps is None and speed is None:
        return self._framerate
    else:
        assert fps is not None or speed is not None, "Invalid input"
        assert not (fps is not None and speed is not None), "Invalid input"
        assert speed is None or speed > 0, "Invalid speed, must specify speed multiplier s=1, s=2 for 2x faster, s=0.5 for half slower"                    
        assert fps is None or fps > 0, "Invalid framerate"
        assert self._framerate is not None, "Framerate conversion requires that the framerate is known for current activities.  This must be provided to the vipy.object.Activity() constructor."

        fps = float(fps) if fps is not None else (1.0/speed)*self._framerate            
        (self._startframe, self._endframe) = [int(np.round(f*(fps/float(self._framerate)))) for f in (self._startframe, self._endframe)]
        self._framerate = float(fps)
        return self
def hasattribute(self, k)
Expand source code Browse git
def hasattribute(self, k):
    return k in self.attributes
def hasoverlap(self, other, threshold=0)

Return true if the temporal_iou is greater than the provided threshold between self and other Track or other Activity

Expand source code Browse git
def hasoverlap(self, other, threshold=0):
    """Return true if the temporal_iou is greater than the provided threshold between self and other Track or other Activity"""
    assert isinstance(other, Activity) or isinstance(other, Track), "Invalid input"
    assert threshold >= 0 and threshold <= 1, "Invalid temporal IOU threshold"
    return (((min(self._endframe, other.endframe()) - max(self._startframe, other.startframe())) > 0) if threshold == 0 else
            self.temporal_iou(other) > threshold)
def hastrack(self, track)

Is the track part of the activity?

Expand source code Browse git
def hastrack(self, track):
    """Is the track part of the activity?"""
    assert isstring(track) or isinstance(track, Track), "Invalid input - Must be a vipy.object.Track().id() or vipy.object.Track()"
    trackid = track.id() if isinstance(track, Track) else track
    return trackid in self._trackid
def hastrackoverlap(self, track)

is the activity occurring during the interval when the track is occurring and is this track assigned to the activity?

Expand source code Browse git
def hastrackoverlap(self, track):
    """is the activity occurring during the interval when the track is occurring and is this track assigned to the activity?"""
    assert isinstance(track, Track)
    return self.hastrack(track) and self.temporal_iou(track) > 0
def id(self, newid=None)
Expand source code Browse git
def id(self, newid=None):
    if newid is None:
        return self._id
    else:
        self._id = newid
        return self
def isneighbor(self, other, framegate=10)
Expand source code Browse git
def isneighbor(self, other, framegate=10):
    return self.temporal_iou(other.clone().temporalpad(framegate)) > 0 
def json(self, encode=True)
Expand source code Browse git
def json(self, encode=True):
    d = {k:v for (k,v) in self.__dict__.items() if not ((k == '_shortlabel' and v is None) or
                                                        (k == 'attributes' and (v is None or isinstance(v, dict) and len(v)==0)) or
                                                        (k == '_id' and v is None))}  # don't bother to store None values
    d = {k:v if k != '_trackid' else tuple(v) for (k,v) in d.items()}  # sets are non-serializable
    d = {k.lstrip('_'):v for (k,v) in d.items()}  # prettyjson (remove "_" prefix to attributes)                                                
    return json.dumps(d) if encode else d
def label(self, label=None, shortlabel=None)

Alias for category

Expand source code Browse git
def label(self, label=None, shortlabel=None):
    """Alias for category"""
    return self.category(label, shortlabel=shortlabel)
def middleframe(self)

Return the middle frame number of the activity

Expand source code Browse git
def middleframe(self):
    """Return the middle frame number of the activity"""
    return int(np.round((self._endframe - self._startframe) / 2.0)) + self._startframe
def offset(self, dt)
Expand source code Browse git
def offset(self, dt):
    self._startframe = int(self._startframe + dt)
    self._endframe = int(self._endframe + dt)
    return self
def padto(self, t)

Add a symmetric temporal pad so that the activity is at least t seconds long

Expand source code Browse git
def padto(self, t):
    """Add a symmetric temporal pad so that the activity is at least t seconds long"""
    return self.temporalpad(int(np.ceil(self.framerate()*((t - self.duration())/2.0)))) if t > self.duration() else self
def replace(self, oldtrack, newtrack)

Replace oldtrack with newtrack if present in self._tracks. Pass in a trackdict to share reference to track, so that track owner can modify the track and this object observes the change

Expand source code Browse git
def replace(self, oldtrack, newtrack):
    """Replace oldtrack with newtrack if present in self._tracks.  Pass in a trackdict to share reference to track, so that track owner can modify the track and this object observes the change"""
    assert isinstance(oldtrack, Track) and isinstance(newtrack, Track), "Invalid input - must be vipy.object.Track"
    if self.hastrack(oldtrack):
        self._trackid.discard(oldtrack.id())
        self._trackid.add(newtrack.id())
        if self.actorid() == oldtrack.id():
            self.actorid(newtrack.id())
    return self    
def replaceid(self, oldtrackid, newtrackid)

Replace oldtrack with newtrack if present in self._tracks. Pass in a trackdict to share reference to track, so that track owner can modify the track and this object observes the change

Expand source code Browse git
def replaceid(self, oldtrackid, newtrackid):
    """Replace oldtrack with newtrack if present in self._tracks.  Pass in a trackdict to share reference to track, so that track owner can modify the track and this object observes the change"""
    if self.hastrack(oldtrackid):
        ti = set(self._trackid)
        ti.discard(oldtrackid)
        ti.add(newtrackid)
        if self.actorid() == oldtrackid:
            self.actorid(newtrackid)
        self._trackid = list(ti)
    return self    
def shortlabel(self, label=None)

A optional shorter label string to show in the visualizations

Expand source code Browse git
def shortlabel(self, label=None):
    """A optional shorter label string to show in the visualizations"""                
    if label is not None:
        self._shortlabel = label
        return self
    else:
        return self._shortlabel
def startframe(self, f=None)
Expand source code Browse git
def startframe(self, f=None):
    if f is None:
        return self._startframe
    else:
        self._startframe = f
        return self
def temporal_distance(self, other)

Return the temporal distance in frames between self and other which is the minimum frame difference between the end of one to the start of the other, or zero if they overlap

Expand source code Browse git
def temporal_distance(self, other):
    """Return the temporal distance in frames between self and other which is the minimum frame difference between the end of one to the start of the other, or zero if they overlap"""
    assert isinstance(other, Activity), "Invalid input - must be vipy.activity.Activity()"
    return (max(self.startframe(), other.startframe()) - min(self.endframe(), other.endframe())) if self.temporal_iou(other) == 0 else 0
def temporal_iou(self, other)

Return the temporal intersection over union of two activities or this activity and a track

Expand source code Browse git
def temporal_iou(self, other):
    """Return the temporal intersection over union of two activities or this activity and a track"""
    assert isinstance(other, Activity) or isinstance(other, Track), "Invalid input - must be vipy.activity.Activity or vipy.object.Track"
    assert self._framerate == other._framerate, "invalid input - framerate must match"
    (sf, ef) = (other._startframe, other._endframe) if isinstance(other, Activity) else (other.startframe(), other.endframe())  # attribute access is faster than methods
    
    t_start = min(self._startframe, sf)
    t_end = max(self._endframe, ef)
    t_union = float(t_end - t_start)
    
    t_start = max(self._startframe, sf)
    t_end = min(self._endframe, ef)
    t_intersection = float(t_end - t_start)
    
    return (t_intersection / t_union) if t_intersection > 0 else 0
def temporalpad(self, df)

Add a temporal pad of df=(before frames, after frames) or df=pad frames to the start and end of the activity. The padded start frame may be negative.

Expand source code Browse git
def temporalpad(self, df):
    """Add a temporal pad of df=(before frames, after frames) or df=pad frames to the start and end of the activity.  The padded start frame may be negative."""
    df = (df, df) if not isinstance(df, tuple) else df
    self._startframe -= int(df[0])
    self._endframe += int(df[1])
    return self  
def trackfilter(self, f)

Remove all tracks such that the lambda function f(trackid) resolves to False

Expand source code Browse git
def trackfilter(self, f):
    """Remove all tracks such that the lambda function f(trackid) resolves to False"""
    assert callable(f)
    self._trackid = [tid for tid in self._trackid if f(tid)]
    if self.actorid() not in self._trackid:
        self._actorid = None
    return self
def trackids(self)

Return a set of track IDs associated with this activity

Expand source code Browse git
def trackids(self):
    """Return a set of track IDs associated with this activity"""
    return set(self._trackid)
def tracks(self)

alias for trackids

Expand source code Browse git
def tracks(self):
    """alias for trackids"""
    return self.trackids()
def truncate(self, startframe=None, endframe=None)

Truncate the activity so that it is between startframe and endframe

Expand source code Browse git
def truncate(self, startframe=None, endframe=None):
    """Truncate the activity so that it is between startframe and endframe"""
    self._startframe = self._startframe if startframe is None else max(self._startframe, startframe)
    self._endframe = self._endframe if endframe is None else min(self._endframe, endframe)
    self._endframe = self._endframe if self._endframe > self._startframe else self._startframe  # degenerate truncation
    return self
def union(self, other, confweight=0.5, maxconf=False)

Compute the union of the new activity other to this activity by updating the start and end times and computing the mean confidence.

-Note: other must have the same category and track IDs as self -confweight [0,1]: the convex combinatiopn weight applied to the new activity

Expand source code Browse git
def union(self, other, confweight=0.5, maxconf=False):
    """Compute the union of the new activity other to this activity by updating the start and end times and computing the mean confidence.
    
       -Note: other must have the same category and track IDs as self
       -confweight [0,1]:  the convex combinatiopn weight applied to the new activity 
    """
    assert isinstance(other, Activity), "Invalid input"
    assert self._actorid == other._actorid, "Actor ID must be the same"
    assert self._label == other._label, "Assigned activity is a different category"
    assert self._framerate == other._framerate, "Invalid input"
    assert confweight >= 0 and confweight <= 1, "Confidence weight must be [0,1]"

    self.startframe(min(other._startframe, self._startframe))
    self.endframe(max(other._endframe, self._endframe))
    if other.confidence() is not None and self.confidence() is not None:
        self.confidence(float((1.0-confweight)*self.confidence() + confweight*other.confidence()) if not maxconf else float(max(self.confidence(), other.confidence())))  # running mean confidence or max
    return self
def within(self, startframe, endframe)

Is the activity within the frame rate (startframe, endframe)?

Expand source code Browse git
def within(self, startframe, endframe):
    """Is the activity within the frame rate (startframe, endframe)?"""
    return self.startframe() >= startframe and self.endframe() <= endframe