Skip to content

Commit ce6c317

Browse files
authored
Merge pull request #9 from clamsproject/sdk-1.2.x-update
releasing 1.2.4
2 parents 6be9441 + fc3d4be commit ce6c317

File tree

5 files changed

+161
-18
lines changed

5 files changed

+161
-18
lines changed

Containerfile

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# Use the same base image version as the clams-python python library version
2-
FROM ghcr.io/clamsproject/clams-python-opencv4:1.0.9
2+
FROM ghcr.io/clamsproject/clams-python-opencv4:1.2.4
33
# See https://github.com/orgs/clamsproject/packages?tab=packages&q=clams-python for more base images
44
# IF you want to automatically publish this image to the clamsproject organization,
55
# 1. you should have generated this template without --no-github-actions flag
@@ -19,6 +19,14 @@ ENV CLAMS_APP_VERSION ${CLAMS_APP_VERSION}
1919
# install more system packages as needed using the apt manager
2020
################################################################################
2121

22+
# https://github.com/openai/whisper/blob/ba3f3cd54b0e5b8ce1ab3de13e32122d0d5f98ab/whisper/__init__.py#L130
23+
ENV XDG_CACHE_HOME='/cache'
24+
# https://huggingface.co/docs/huggingface_hub/main/en/package_reference/environment_variables#hfhome
25+
ENV HF_HOME="/cache/huggingface"
26+
# https://pytorch.org/docs/stable/hub.html#where-are-my-downloaded-models-saved
27+
ENV TORCH_HOME="/cache/torch"
28+
29+
# RUN mkdir /cache && rm -rf /root/.cache && ln -s /cache /root/.cache
2230
################################################################################
2331
# main app installation
2432
COPY ./ /app

app.py

Lines changed: 56 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import logging
33
from typing import Union, Sequence
44

5+
import copy
56
import cv2
67
import itertools
78
import numpy as np
@@ -22,24 +23,49 @@ def _appmetadata(self):
2223
pass
2324

2425
def _annotate(self, mmif: Union[str, dict, Mmif], **parameters) -> Mmif:
26+
"""Internal Annotate Wrapper Method
27+
28+
Generates a new set of annotations for `mmif`
29+
via EAST Text Detection on Videos and Images.
30+
31+
### params
32+
+ mmif => a mmif object
33+
+ **parameters => runtime parameters (see `metadata.py`)
34+
35+
### returns
36+
+ mmif object, with new app annotations.
37+
"""
38+
39+
# Run app on contained VideoDocument(s) in MMIF
2540
for videodocument in mmif.get_documents_by_type(DocumentTypes.VideoDocument):
2641
# one view per video document
2742
new_view = mmif.new_view()
2843
self.sign_view(new_view, parameters)
29-
config = self.get_configuration(**parameters)
30-
new_view.new_contain(AnnotationTypes.BoundingBox, document=videodocument.id, timeUnit=config["timeUnit"])
44+
new_view.new_contain(AnnotationTypes.BoundingBox, document=videodocument.id, timeUnit=parameters["timeUnit"])
3145
self.logger.debug(f"Running on video {videodocument.location_path()}")
32-
mmif = self.run_on_video(mmif, videodocument, new_view, **config)
46+
mmif = self.run_on_video(mmif, videodocument, new_view, **parameters)
47+
48+
# Run app on contained ImageDocument(s) in MMIF
3349
if mmif.get_documents_by_type(DocumentTypes.ImageDocument):
3450
# one view for all image documents
3551
new_view = mmif.new_view()
3652
self.sign_view(new_view, parameters)
3753
new_view.new_contain(AnnotationTypes.BoundingBox)
3854
self.logger.debug(f"Running on all images")
3955
mmif = self.run_on_images(mmif, new_view)
56+
4057
return mmif
4158

4259
def run_on_images(self, mmif: Mmif, new_view: View) -> Mmif:
60+
"""Run EAST on ImageDocuments
61+
62+
### params
63+
+ mmif => Mmif Object
64+
+ new_view => a single mmif View (representing all ImageDocuments)
65+
66+
### returns
67+
+ mmif, annotated with boundingboxes
68+
"""
4369
for imgdocument in mmif.get_documents_by_type(DocumentTypes.ImageDocument):
4470
image = cv2.imread(imgdocument.location)
4571
box_list = image_to_east_boxes(image)
@@ -54,6 +80,16 @@ def run_on_images(self, mmif: Mmif, new_view: View) -> Mmif:
5480
return mmif
5581

5682
def run_on_video(self, mmif: Mmif, videodocument: Document, new_view: View, **config) -> Mmif:
83+
"""Run EAST on a VideoDocument
84+
85+
### params
86+
+ mmif => Mmif Object
87+
+ videodocument => VideoDocument file
88+
+ new_view => a single mmif View
89+
90+
### returns
91+
+ mmif, annotated with boundingboxes
92+
"""
5793
cap = vdh.capture(videodocument)
5894
views_with_tframe = [v for v in mmif.get_views_for_document(videodocument.id)
5995
if v.metadata.contains[AnnotationTypes.TimeFrame]]
@@ -66,20 +102,20 @@ def run_on_video(self, mmif: Mmif, videodocument: Document, new_view: View, **co
66102
for v in views_with_tframe for a in v.get_annotations(AnnotationTypes.TimeFrame)
67103
if not frame_type or a.get_property("frameType") in frame_type])
68104
target_frames = list(map(int, target_frames))
69-
self.logger.debug(f"Processing frames {target_frames} from TimeFrame annotations of {frame_type} types")
70105
else:
71106
target_frames = vdh.sample_frames(
72-
sample_ratio=config['sampleRatio'], start_frame=0,
73-
end_frame=min(int(config['stopAt']), videodocument.get_property("frameCount"))
107+
start_frame=0,
108+
end_frame=min(int(config['stopAt']), videodocument.get_property("frameCount")),
109+
sample_rate=config['sampleRate']
74110
)
111+
75112
target_frames.sort()
76-
self.logger.debug(f"Running on frames {target_frames}")
77-
for fn, fi in zip(target_frames, vdh.extract_frames_as_images(videodocument, target_frames)):
78-
self.logger.debug(f"Processing frame {fn}")
113+
114+
for fn, fi in zip(target_frames, vdh.extract_frames_as_images(videodocument, copy.deepcopy(target_frames))):
79115
result_list = image_to_east_boxes(fi)
80116
for box in result_list:
81117
bb_annotation = new_view.new_annotation(AnnotationTypes.BoundingBox)
82-
tp = vdh.convert(time=fn, in_unit='frame', out_unit=config['timeUnit'], fps=videodocument.get_property("fps"))
118+
tp = vdh.convert(t=fn, in_unit='frame', out_unit=config['timeUnit'], fps=videodocument.get_property("fps"))
83119
self.logger.debug(f"Adding a timepoint at frame: {fn} >> {tp}")
84120

85121
tp_annotation = new_view.new_annotation(AnnotationTypes.TimePoint)
@@ -97,9 +133,16 @@ def run_on_video(self, mmif: Mmif, videodocument: Document, new_view: View, **co
97133

98134
return mmif
99135

100-
136+
def get_app():
137+
"""
138+
This function effectively creates an instance of the app class, without any arguments passed in, meaning, any
139+
external information such as initial app configuration should be set without using function arguments. The easiest
140+
way to do this is to set global variables before calling this.
141+
"""
142+
return EastTextDetection()
101143

102144
if __name__ == "__main__":
145+
103146
parser = argparse.ArgumentParser()
104147
parser.add_argument("--port", action="store", default="5000", help="set port to listen" )
105148
parser.add_argument("--production", action="store_true", help="run gunicorn server")
@@ -109,8 +152,8 @@ def run_on_video(self, mmif: Mmif, videodocument: Document, new_view: View, **co
109152
parsed_args = parser.parse_args()
110153

111154
# create the app instance
112-
app = EastTextDetection()
113-
155+
app = get_app()
156+
114157
http_app = Restifier(app, port=int(parsed_args.port))
115158
# for running the application in production mode
116159
if parsed_args.production:

cli.py

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
#!/usr/bin/env python3
2+
"""
3+
The purpose of this file is to define a thin CLI interface for your app
4+
5+
DO NOT CHANGE the name of the file
6+
"""
7+
8+
import argparse
9+
import sys
10+
from contextlib import redirect_stdout
11+
12+
import app
13+
14+
import clams.app
15+
from clams import AppMetadata
16+
17+
18+
def metadata_to_argparser(app_metadata: AppMetadata) -> argparse.ArgumentParser:
19+
"""
20+
Automatically generate an argparse.ArgumentParser from parameters specified in the app metadata (metadata.py).
21+
"""
22+
23+
parser = argparse.ArgumentParser(
24+
description=f"{app_metadata.name}: {app_metadata.description} (visit {app_metadata.url} for more info)",
25+
formatter_class=argparse.RawDescriptionHelpFormatter)
26+
27+
# parse cli args from app parameters
28+
for parameter in app_metadata.parameters:
29+
if parameter.multivalued:
30+
a = parser.add_argument(
31+
f"--{parameter.name}",
32+
help=parameter.description,
33+
nargs='+',
34+
action='extend',
35+
type=str
36+
)
37+
else:
38+
a = parser.add_argument(
39+
f"--{parameter.name}",
40+
help=parameter.description,
41+
nargs=1,
42+
action="store",
43+
type=str)
44+
if parameter.choices is not None:
45+
a.choices = parameter.choices
46+
if parameter.default is not None:
47+
a.help += f" (default: {parameter.default}"
48+
if parameter.type == "boolean":
49+
a.help += (f", any value except for {[v for v in clams.app.falsy_values if isinstance(v, str)]} "
50+
f"will be interpreted as True")
51+
a.help += ')'
52+
# then we don't have to add default values to the arg_parser
53+
# since that's handled by the app._refined_params() method.
54+
parser.add_argument('IN_MMIF_FILE', nargs='?', type=argparse.FileType('r'),
55+
help='input MMIF file path, or STDIN if `-` or not provided. NOTE: When running this cli.py in '
56+
'a containerized environment, make sure the container is run with `-i` flag to keep stdin '
57+
'open.',
58+
# will check if stdin is a keyboard, and return None if it is
59+
default=None if sys.stdin.isatty() else sys.stdin)
60+
parser.add_argument('OUT_MMIF_FILE', nargs='?', type=argparse.FileType('w'),
61+
help='output MMIF file path, or STDOUT if `-` or not provided. NOTE: When this is set to '
62+
'STDOUT, any print statements in the app code will be redirected to stderr.',
63+
default=sys.stdout)
64+
return parser
65+
66+
67+
if __name__ == "__main__":
68+
clamsapp = app.get_app()
69+
arg_parser = metadata_to_argparser(app_metadata=clamsapp.metadata)
70+
args = arg_parser.parse_args()
71+
if args.IN_MMIF_FILE:
72+
in_data = args.IN_MMIF_FILE.read()
73+
# since flask webapp interface will pass parameters as "unflattened" dict to handle multivalued parameters
74+
# (https://werkzeug.palletsprojects.com/en/latest/datastructures/#werkzeug.datastructures.MultiDict.to_dict)
75+
# we need to convert arg_parsers results into a similar structure, which is the dict values are wrapped in lists
76+
params = {}
77+
for pname, pvalue in vars(args).items():
78+
if pvalue is None or pname in ['IN_MMIF_FILE', 'OUT_MMIF_FILE']:
79+
continue
80+
elif isinstance(pvalue, list):
81+
params[pname] = pvalue
82+
else:
83+
params[pname] = [pvalue]
84+
if args.OUT_MMIF_FILE.name == '<stdout>':
85+
with redirect_stdout(sys.stderr):
86+
out_mmif = clamsapp.annotate(in_data, **params)
87+
else:
88+
out_mmif = clamsapp.annotate(in_data, **params)
89+
args.OUT_MMIF_FILE.write(out_mmif)
90+
else:
91+
arg_parser.print_help()
92+
sys.exit(1)

metadata.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,15 +39,15 @@ def appmetadata() -> AppMetadata:
3939
description="Segments of video to run on. Only works with VideoDocument input and TimeFrame input. Empty value means run on the every frame types.",
4040
)
4141
metadata.add_parameter(
42-
name="sampleRatio",
42+
name="sampleRate",
4343
type="integer",
44-
default="30",
44+
default=30,
4545
description="Frequency to sample frames. Only works with VideoDocument input, and without TimeFrame input. (when `TimeFrame` annotation is found, this parameter is ignored.)",
4646
)
4747
metadata.add_parameter(
4848
name="stopAt",
4949
type="integer",
50-
default=108000, # ~2 hours of video at 30fps 1 * 60 * 60 * 30
50+
default=2 * 60 * 60 * 30, # ~2 hours of video at 30fps 1 * 60 * 60 * 30
5151
description="Frame number to stop running. Only works with VideoDocument input. The default is roughly 2 hours of video at 30fps.",
5252
)
5353
metadata.add_parameter(

requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
clams-python==1.0.9
1+
clams-python==1.2.4
22

33
opencv-python-rolling==4.7.* # standard 4.7.x version (no rolling release) has bug with old CPUs
44
imutils

0 commit comments

Comments
 (0)