Skip to content

Viame Manual Annotation Tools

construct_image_filename_from_video_frame(video_filename, time, outfile_format, outfile_dir)

construct a filename from a given video file and frame time

Parameters:

Name Type Description Default
video_filename str

the file name of the video. This will be formatted into the outfile_format as video_filename. See that arg for more details.

required
time time

the time that locates the desired frame in the video

required
outfile_format str | None

if None, defaults to '{video_filename}.%H.%M.%S.%f.jpg' video_filename is the argument of this name to this function The remainder is passed through a strftime from the time arg, see the strftime docs the extension .jpg will determine the output file format if this filename is used to write an image file.

required
outfile_dir str | None

if not None, this is simply path joined to the filename output

required

Returns:

Name Type Description
frame_filename str

a filename appropriate for the specified frame in the video

Source code in viame2coco/viame_manual_annotations.py
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
def construct_image_filename_from_video_frame(
        video_filename: str, 
        time: datetime.time, 
        outfile_format: str | None, 
        outfile_dir: str | None) -> str:
    '''
    construct a filename from a given video file and frame time

    Parameters
    ----------
    video_filename: str
        the file name of the video.  This will be formatted into the
        outfile_format as `video_filename`.  See that arg for more details.
    time: datetime.time
        the time that locates the desired frame in the video
    outfile_format: str | None
        if None, defaults to '{video_filename}.%H.%M.%S.%f.jpg'
        `video_filename` is the argument of this name to this function
        The remainder is passed through a `strftime` from the time arg,
        see the [`strftime` docs](https://docs.python.org/3/library/datetime.html#format-codes)
        the extension `.jpg` will determine the output file format if this
        filename is used to write an image file.
    outfile_dir: str | None
        if not None, this is simply path joined to the filename output

    Returns
    -------
    frame_filename: str
        a filename appropriate for the specified frame in the video
    '''
    if outfile_format is None:
        outfile_format = '{video_filename}.%H.%M.%S.%f.jpg'
    frame_filename = time.strftime(outfile_format).format(video_filename = video_filename)
    if outfile_dir is not None:
        frame_filename = os.path.join(outfile_dir, frame_filename)
    return frame_filename

extract_frame_microseconds(cv2_video_cap, microseconds, outfile=None)

extract a frame from the provided cv2 video at the given number of microseconds. Optionally write the frame to outfile.

Parameters:

Name Type Description Default
cv2_video_cap VideoCapture

the video from which to capture the frame

required
microseconds float

the location in microseconds into the video at which to extract the desired frame

required
outfile str | None

the optional filename to which the desired frame should be writ

None

Returns:

Name Type Description
image ndarray | None

the video frame at the given number of microseconds, or None if the frame read was unsuccessful. Additionally, the frame may be written to a file as a side-effect if outfile was passed as an argument.

Source code in viame2coco/viame_manual_annotations.py
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
def extract_frame_microseconds(
        cv2_video_cap: cv2.VideoCapture, 
        microseconds: float, 
        outfile: str | None = None) -> np.ndarray | None:
    '''
    extract a frame from the provided cv2 video at the given number 
    of microseconds.  Optionally write the frame to outfile.

    Parameters
    ----------
    cv2_video_cap: cv2.VideoCapture
        the video from which to capture the frame
    microseconds: float
        the location in microseconds into the video
        at which to extract the desired frame
    outfile: str | None:
        the optional filename to which the desired frame should 
        be writ
    Returns
    -------
    image: numpy.ndarray | None
        the video frame at the given number of microseconds, or None
        if the frame read was unsuccessful.  Additionally, the frame
        may be written to a file as a side-effect if `outfile` was
        passed as an argument.
    '''
    logger.debug(f"extracting frame at {microseconds:.3f} microseconds")
    cv2_video_cap.set(cv2.CAP_PROP_POS_MSEC, microseconds // 1000)
    success, image = cv2_video_cap.read()
    if outfile is not None:
        try:
            cv2.imwrite(outfile, image)
        except cv2.error as e:
            # sometimes times very close to the end are "too far", scale it back to the end
            ALLOWED_FUDGE_MICROS = 10000 # ten milliseconds
            PROBLEMATIC_DATA_MICROS = 1000000 # one second
            logger.info(f"issue reading video at {microseconds:.3f} microseconds")
            last_valid_timestamp = find_last_valid_timestamp(cv2_video_cap, 0, microseconds / 1000) # in milliseconds
            logger.info(f"last valid timestamp found at {last_valid_timestamp:.3f} milliseconds")
            fudge = microseconds/1000 - last_valid_timestamp # milliseconds
            logger.info("fudging {} milliseconds".format(fudge))
            if fudge > PROBLEMATIC_DATA_MICROS:
                raise Exception(f"Something is wrong with this data, annotation is {fudge:.3f} ms away from end of video at {last_valid_timestamp:.3f}")
            elif fudge < ALLOWED_FUDGE_MICROS:
                # this is fine, just use the last frame
                logger.info(f"fudging annotation at {fudge:.3f} ms from computed end of video")
                cv2_video_cap.set(cv2.CAP_PROP_POS_MSEC, last_valid_timestamp)
                success, image = cv2_video_cap.read()
                if outfile is not None:
                    # if this still fails, let the error bubble up
                    cv2.imwrite(outfile, image)
            else:
                # timestamp is outside of allowed fudge factor (frame will be too far from annotation),
                # but not so far as to indicate problematic data.  Just ditch this datum and move on.
                logger.info(f"discarding annotation at {fudge:.3f} ms from computed end of video")
                return None
    return image

extract_viame_video_annotations(viame_csv, video_file, outfile_format=None, outfile_dir=None)

extract the manual annotations and frames from a VIAME-style annotaiton csv

Writes the frames to files.

Parameters:

Name Type Description Default
viame_csv Iterable[Sequence]

the data rows from a VIAME-style annotation csv should not include the headers

required
video_file str

the file name of the video. This will be formatted into the outfile_format as video_filename. See that arg for more details.

required
outfile_format str | None

see construct_image_filename_from_video_frame signature

None
outfile_dir str | None

see construct_image_filename_from_video_frame signature

None

Returns:

Name Type Description
viame_csv Iterable[Sequence]

the data rows in the input only when the annotations are manual, skipping any automated annotations

Source code in viame2coco/viame_manual_annotations.py
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
def extract_viame_video_annotations(
        viame_csv: Iterable[Sequence], 
        video_file: str, 
        outfile_format: str | None = None, 
        outfile_dir: str | None = None) -> Iterable[Sequence]:
    '''
    extract the manual annotations and frames from a VIAME-style
    annotaiton csv

    Writes the frames to files.

    Parameters
    ----------
    viame_csv: Iterable[Sequence]
        the data rows from a VIAME-style annotation csv
        should not include the headers
    video_file: str
        the file name of the video.  This will be formatted into the
        outfile_format as `video_filename`.  See that arg for more details.
    outfile_format: str | None
        see `construct_image_filename_from_video_frame` signature
    outfile_dir: str | None
        see `construct_image_filename_from_video_frame` signature

    Returns
    -------
    viame_csv: Iterable[Sequence]
        the data rows in the input only when the annotations
        are manual, skipping any automated annotations
    '''
    logger.info(f"extracting images from video {video_file}")
    cap = cv2.VideoCapture(video_file)
    logger.info("Video opened: %s", cap.isOpened())
    logger.info("Frame count: %s", cap.get(cv2.CAP_PROP_FRAME_COUNT))
    logger.info("FPS: %s", cap.get(cv2.CAP_PROP_FPS))
    logger.info("Duration (ms, naive): %s", cap.get(cv2.CAP_PROP_FRAME_COUNT)/cap.get(cv2.CAP_PROP_FPS)*1000 if cap.get(cv2.CAP_PROP_FPS) else "unknown")
    video_filename_leaf = os.path.split(video_file)[1]
    if outfile_dir is not None:
        os.makedirs(outfile_dir, exist_ok=True)
    for row in filter_viame_manual_annotations(viame_csv):
        frame_time = datetime.time.fromisoformat(row[VIAME_VIDEO_TIME_COL])
        microseconds = time2micros(frame_time)
        frame_filename = construct_image_filename_from_video_frame(video_filename_leaf, frame_time, outfile_format, outfile_dir)
        image = extract_frame_microseconds(cap, microseconds, frame_filename)
        if image is not None:
            # if we fail to extract the image, just move along
            row[VIAME_VIDEO_TIME_COL] = frame_filename
            yield row
        else:
            logger.info(f"image extraction failed {frame_filename}")

filter_viame_manual_annotations(viame_csv)

filters an iterable of data rows read from a VIAME-style annotation csv to only rows that contain manual annotations

Parameters:

Name Type Description Default
viame_csv Iterable[Sequence]

the data rows from a VIAME-style annotation csv should not include the headers

required

Returns:

Name Type Description
viame_csv Iterable[Sequence]

the data rows in the input only when the annotations are manual, skipping any automated annotations

Source code in viame2coco/viame_manual_annotations.py
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
def filter_viame_manual_annotations(
        viame_csv: Iterable[Sequence]) -> Iterable[Sequence]:
    '''
    filters an iterable of data rows read from a VIAME-style annotation csv
    to only rows that contain manual annotations

    Parameters
    ----------
    viame_csv: Iterable[Sequence]
        the data rows from a VIAME-style annotation csv
        should not include the headers

    Returns
    -------
    viame_csv: Iterable[Sequence]
        the data rows in the input only when the annotations
        are manual, skipping any automated annotations
    '''
    yield from filter(viame_is_manual_annotation, viame_csv)

time2micros(time)

convert a datetime.time into total microseconds

>>> time2micros(datetime.time(1,1,1)) # 1 hour, 1 min, 1 sec
3661000000

Parameters:

Name Type Description Default
time time

the time to convert into microseconds

required

Returns:

Name Type Description
microseconds float | int

the total number of microseconds in the time argument

Source code in viame2coco/viame_manual_annotations.py
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
def time2micros(time: datetime.time) -> float:
    '''
    convert a datetime.time into total microseconds

    ```
    >>> time2micros(datetime.time(1,1,1)) # 1 hour, 1 min, 1 sec
    3661000000

    ```

    Parameters
    ----------
    time: datetime.time
        the time to convert into microseconds

    Returns
    -------
    microseconds: float | int
        the total number of microseconds in the time argument        
    '''
    return time.hour * MS2H + time.minute * MS2M + time.second * MS2S + time.microsecond

viame_is_manual_annotation(viame_csv_row)

returns whether a given row in a VIAME-style annotation output csv represents a manual annotation or an automated annotation.

basically, just checks if the annotation confidence is 1

Parameters:

Name Type Description Default
viame_csv_row Sequence

a row read from a VIAME-style annotation csv

required

Returns:

Name Type Description
is_manual_annotation bool

a boolean representing whether this row is manual or not

Source code in viame2coco/viame_manual_annotations.py
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
def viame_is_manual_annotation(viame_csv_row: Sequence) -> bool:
    '''
    returns whether a given row in a VIAME-style annotation output csv
    represents a manual annotation or an automated annotation.

    basically, just checks if the annotation confidence is 1

    Parameters
    ----------
    viame_csv_row: Sequence
        a row read from a VIAME-style annotation csv

    Returns
    -------
    is_manual_annotation: bool
        a boolean representing whether this row is manual or not 
    '''
    is_manual_annotation = (
        (len(viame_csv_row) > VIAME_CONFIDENCE_COL) 
            and 
        (float(viame_csv_row[VIAME_CONFIDENCE_COL]) == 1)
    )
    return is_manual_annotation