Skip to content

Commit 6b27de6

Browse files
authored
Merge pull request #77 from yoterel/master
better command line options for ui
2 parents 1a0973f + 07546a0 commit 6b27de6

6 files changed

Lines changed: 96 additions & 77 deletions

File tree

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,6 @@ src/icatcher.egg-info/
88
dist/
99
build/
1010
node_modules/
11+
tests/test_data/test_short/
12+
tests/test_data/test_short.npz
13+
tests/test_data/test_short.txt

README.md

Lines changed: 24 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -45,44 +45,44 @@ You can run iCatcher+ with the command:
4545

4646
`icatcher --help`
4747

48-
which will list all available options. The description below will help you get more familiar with some common command line arguments.
48+
Which will list all available options. Below we list some common options to help you get more familiar with iCatcher+. The pipeline is highly configurable, please see [the website](https://icatcherplus.github.io/) for more explanation about the flags.
4949

5050
### Annotating a Video
5151
To produce annotations for a video file (if a folder is provided, all videos will be used for prediction):
5252

5353
`icatcher /path/to/my/video.mp4`
5454

55-
>**NOTE:** For any videos you wish to visualize with the web app, you must use the `--ui_packaging_path` flag:
56-
>
57-
>`icatcher /path/to/my/video.mp4 --ui_packaging_path /path/to/desired/output/directory/`
55+
**NOTE:** For any videos you wish to visualize with the [Web App](#web-app), you must use the `--output_annotation` and the `--output_format ui` flags:
5856

59-
### Using the iCatcher Web App
60-
To launch the iCatcher+ web app, use:
57+
`icatcher /path/to/my/video.mp4 --output_annotation /path/to/desired/output/directory/ --output_format ui`
6158

62-
`icatcher --app`
59+
### Common Flags
6360

64-
The app should open automatically at [http://localhost:5001](http://localhost:5001). For more details, see [Web App](#web-app).
61+
- You can save a labeled video by adding:
6562

66-
### Common Annotation Flags
67-
A common option is to add:
63+
`--output_video_path /path/to/output_folder`
6864

69-
`icatcher /path/to/my/video.mp4 --use_fc_model`
65+
- If you want to output annotations to a file, use:
7066

71-
Which enables a child face detector for more robust results (however, sometimes this can result in too much loss of data).
67+
`--output_annotation /path/to/output_annotation_folder`
7268

73-
You can save a labeled video by adding:
69+
See [Output format](#output-format) below for more information on how the files are formatted.
7470

75-
`--output_video_path /path/to/output_folder`
71+
- To show the predictions online in a seperate window, add the option:
72+
73+
`--show_output`
7674

77-
If you want to output annotations to a file, use:
75+
- To launch the iCatcher+ [Web App](#web-app) (after annotating), use:
7876

79-
`--output_annotation /path/to/output_annotation_folder`
77+
`icatcher --app`
8078

81-
To show the predictions online in a seperate window, add the option:
79+
The app should open automatically at [http://localhost:5001](http://localhost:5001). For more details, see [Web App](#web-app).
8280

83-
`--show_output`
81+
- Originally a face classifier was used to distinguish between adult and infant faces (however this can result in too much loss of data). It can be turned on by using:
82+
83+
`icatcher /path/to/my/video.mp4 --use_fc_model`
8484

85-
You can also add parameters to crop the video a given percent before passing to iCatcher:
85+
- You can also add parameters to crop the video a given percent before passing to iCatcher:
8686

8787
`--crop_mode m` where `m` is any of [top, left, right], specifying which side of the video to crop from (if not provided, default is none; if crop_percent is provided but not crop_mode, default is top)
8888

@@ -94,22 +94,21 @@ Currently we supports 3 output formats, though further formats can be added upon
9494

9595
- **raw_output:** a file where each row will contain the frame number, the class prediction and the confidence of that prediction seperated by a comma
9696
- **compressed:** a npz file containing two numpy arrays, one encoding the predicted class (n x 1 int32) and another the confidence (n x 1 float32) where n is the number of frames. This file can be loaded into memory using the numpy.load function. For the map between class number and name see test.py ("predict_from_video" function).
97-
- **ui_output:** needed to open a video in the web app; produces a directory of the following structure
97+
- **ui:** needed for viewing results in the web app; produces a directory of the following structure
9898

9999
├── decorated_frames # dir containing annotated jpg files for each frame in the video
100-
├── video.mp4 # the original video
101-
├── labels.txt # file containing annotations in the `raw_output` form described above
100+
├── labels.txt # file containing annotations in the `raw_output` format described above
102101

103102
# Web App
104103
The iCatcher+ app is a tool that allows users to interact with output from the iCatcher+ ML pipeline in the browser. The tool is designed to operate entirely locally and will not upload any input files to remote servers.
105104

106105
### Using the UI
107106

108-
When you open the iCatcher+ UI, you will be met with a pop-up inviting you to upload your video directory. Please note, this requires you to upload *the whole output directory* which should include a `labels.txt` file and a sub-directory containing all of the frame images from the video.
107+
When you open the iCatcher+ UI, you will be met with a pop-up inviting you to upload a directory. Please note, this requires you to upload *the whole output directory* which should include a `labels.txt` file and a sub-directory named `decorated_frames` containing all of the frames of the video as image files.
109108

110-
Once you've submitted the video, you should see a pop-up asking if you want to upload the whole video. Rest assured, this will not upload those files through the internet or to any remote servers. This is only giving the local browser permission to access those files. The files will stay local to whatever computer is running the browser.
109+
Once you've uploaded a directory, you should see a pop-up asking whether you are sure want to upload all files. Rest assured, this will not upload the files to any remote servers. This is only giving the local browser permission to access those files. The files will stay local to whatever computer is running the browser.
111110

112-
At this point, you should see your video on the screen (you may need to give it a few second to load). Now you can start to review your annotations. Below the video you'll see heatmaps giving you a visual overview of the labels for each frame, as well as the confidence level for each frame.
111+
At this point, you should see the video on the screen (you may need to give it a few second to load). Now you can start to review the annotations. Below the video you'll see heatmaps giving you a visual overview of the labels for each frame, as well as the confidence level for each frame.
113112

114113
# Datasets access
115114

src/icatcher/cli.py

Lines changed: 38 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -269,15 +269,6 @@ def create_output_streams(video_path, framerate, resolution, opt):
269269
fourcc = cv2.VideoWriter_fourcc(
270270
*"MP4V"
271271
) # may need to be adjusted per available codecs & OS
272-
if opt.ui_packaging_path:
273-
video_creator = lambda path: cv2.VideoWriter(
274-
str(path), fourcc, framerate, resolution, True
275-
)
276-
ui_output_components = ui_packaging.prepare_ui_output_components(
277-
opt.ui_packaging_path,
278-
video_path,
279-
video_creator,
280-
)
281272
if opt.output_video_path:
282273
my_video_path = Path(opt.output_video_path, video_path.stem + "_output.mp4")
283274
video_output_file = cv2.VideoWriter(
@@ -286,7 +277,15 @@ def create_output_streams(video_path, framerate, resolution, opt):
286277
if opt.output_annotation:
287278
if opt.output_format == "compressed":
288279
prediction_output_file = Path(opt.output_annotation, video_path.stem)
289-
else:
280+
npz_extension = Path(str(prediction_output_file) + ".npz")
281+
if npz_extension.exists():
282+
if opt.overwrite:
283+
npz_extension.unlink()
284+
else:
285+
raise FileExistsError(
286+
"Annotation output file already exists. Use --overwrite flag to overwrite."
287+
)
288+
elif opt.output_format == "raw_output":
290289
prediction_output_file = Path(
291290
opt.output_annotation, video_path.stem + opt.output_file_suffix
292291
)
@@ -297,6 +296,16 @@ def create_output_streams(video_path, framerate, resolution, opt):
297296
raise FileExistsError(
298297
"Annotation output file already exists. Use --overwrite flag to overwrite."
299298
)
299+
elif opt.output_format == "ui":
300+
ui_output_components = ui_packaging.prepare_ui_output_components(
301+
opt.output_annotation,
302+
video_path,
303+
opt.overwrite,
304+
)
305+
else:
306+
raise NotImplementedError(
307+
"output format {} not implemented".format(opt.output_annotation)
308+
)
300309

301310
return video_output_file, prediction_output_file, ui_output_components, skip
302311

@@ -667,25 +676,25 @@ def handle_output(
667676
confidence,
668677
)
669678
)
670-
if opt.ui_packaging_path:
671-
if is_from_tracker and opt.track_face:
672-
rect_color = (0, 0, 255)
673-
else:
674-
rect_color = (0, 255, 0)
675-
output_for_ui = ui_packaging.prepare_frame_for_ui(
676-
cur_frame,
677-
cur_bbox,
678-
rect_color=rect_color,
679-
conf=confidence,
680-
class_text=class_text,
681-
frame_number=frame_count + cursor + 1,
682-
pic_in_pic=opt.pic_in_pic,
683-
)
684-
ui_packaging.save_ui_output(
685-
frame_idx=frame_count + cursor + 1,
686-
ui_output_components=ui_output_components,
687-
output_for_ui=output_for_ui,
688-
)
679+
elif opt.output_format == "ui":
680+
if is_from_tracker and opt.track_face:
681+
rect_color = (0, 0, 255)
682+
else:
683+
rect_color = (0, 255, 0)
684+
output_for_ui = ui_packaging.prepare_frame_for_ui(
685+
cur_frame,
686+
cur_bbox,
687+
rect_color=rect_color,
688+
conf=confidence,
689+
class_text=class_text,
690+
frame_number=frame_count + cursor + 1,
691+
pic_in_pic=opt.pic_in_pic,
692+
)
693+
ui_packaging.save_ui_output(
694+
frame_idx=frame_count + cursor + 1,
695+
ui_output_components=ui_output_components,
696+
output_for_ui=output_for_ui,
697+
)
689698
logging.info(
690699
"frame: {}, class: {}, confidence: {:.02f}, cur_fps: {:.02f}".format(
691700
str(frame_count + cursor + 1),

src/icatcher/options.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -109,16 +109,13 @@ def parse_arguments(my_string=None):
109109
"--output_format",
110110
type=str,
111111
default="raw_output",
112-
choices=["raw_output", "compressed"],
112+
choices=["raw_output", "compressed", "ui"],
113+
help="Selects output format.",
113114
)
114115
parser.add_argument(
115116
"--output_video_path",
116117
help="If present, annotated video will be saved to this folder.",
117118
)
118-
parser.add_argument(
119-
"--ui_packaging_path",
120-
help="If present, packages the output data into the UI format.",
121-
)
122119
parser.add_argument(
123120
"--pic_in_pic",
124121
action="store_true",

src/icatcher/ui_packaging.py

Lines changed: 13 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,33 @@
1-
import json
21
import cv2
32
import numpy as np
43
from pathlib import Path
54
from icatcher import draw
6-
7-
from typing import Callable, Dict, Union, Tuple
5+
from typing import Dict, Tuple
86

97

108
def prepare_ui_output_components(
11-
ui_packaging_path: str, video_path: str, video_creator: Callable
12-
) -> Dict[str, Union[cv2.VideoWriter, str]]:
9+
ui_packaging_path: str, video_path: str, overwrite: bool
10+
) -> Dict[str, str]:
1311
"""
1412
Given a path to a directory, prepares a dictionary of paths and videos necessary for the UI.
1513
1614
:param ui_packaging_path: path to folder in which the output will be saved
1715
:param video_path: the original video path
18-
:param video_creator: a function to create video files given a path
16+
:param overwrite: if true and label file already exists, overwrites it. else will throw an error.
1917
:return: a dictionary mapping each UI component to its path or video writer
2018
"""
2119

22-
decorated_video_path = Path(
23-
ui_packaging_path, video_path.stem, "decorated_video.mp4"
24-
)
20+
labels_path = Path(ui_packaging_path, video_path.stem, "labels.txt")
21+
if labels_path.exists():
22+
if overwrite:
23+
labels_path.unlink()
24+
else:
25+
raise FileExistsError(
26+
"Annotation output file already exists. Use --overwrite flag to overwrite."
27+
)
2528
decorated_frames_path = Path(ui_packaging_path, video_path.stem, "decorated_frames")
26-
2729
decorated_frames_path.mkdir(parents=True, exist_ok=True)
28-
29-
labels_path = Path(ui_packaging_path, video_path.stem, "labels.txt")
30-
3130
ui_output_components = {
32-
"decorated_video": video_creator(decorated_video_path),
3331
"decorated_frames_path": decorated_frames_path,
3432
"labels_path": labels_path,
3533
}
@@ -89,12 +87,10 @@ def save_ui_output(frame_idx: int, ui_output_components: Dict, output_for_ui: Tu
8987
decorated_frame, label_text = output_for_ui
9088

9189
# Save decorated frame
92-
ui_output_components["decorated_video"].write(decorated_frame)
9390
decorated_frame_path = Path(
9491
ui_output_components["decorated_frames_path"], f"frame_{frame_idx:05d}.jpg"
9592
)
9693
cv2.imwrite(str(decorated_frame_path), decorated_frame)
97-
98-
# Wrtie new annotation to labels file
94+
# Write new annotation to labels file
9995
with open(ui_output_components["labels_path"], "a", newline="") as f:
10096
f.write(f"{frame_idx}, {label_text}\n")

tests/test_basic.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,10 @@ def test_mask():
8787
"tests/test_data/test_short.mp4 --model icatcher+_lookit.pth --fd_model opencv_dnn --output_annotation tests/test_data --mirror_annotation --output_format compressed --overwrite",
8888
"tests/test_data/test_short_result.txt",
8989
),
90+
(
91+
"tests/test_data/test_short.mp4 --model icatcher+_lookit.pth --fd_model opencv_dnn --output_annotation tests/test_data --output_format ui --overwrite",
92+
"tests/test_data/test_short_result.txt",
93+
),
9094
],
9195
)
9296
def test_predict_from_video(args_string, result_file):
@@ -116,7 +120,7 @@ def test_predict_from_video(args_string, result_file):
116120
data = np.load(output_file)
117121
predicted_classes = data["arr_0"]
118122
confidences = data["arr_1"]
119-
else:
123+
elif args.output_format == "raw_output":
120124
output_file = Path("tests/test_data/{}.txt".format(Path(args.source).stem))
121125
with open(output_file, "r") as f:
122126
data = f.readlines()
@@ -125,6 +129,17 @@ def test_predict_from_video(args_string, result_file):
125129
[icatcher.classes[x] for x in predicted_classes]
126130
)
127131
confidences = np.array([float(x.split(",")[2].strip()) for x in data])
132+
elif args.output_format == "ui":
133+
output_file = Path(
134+
"tests/test_data/{}/labels.txt".format(Path(args.source).stem)
135+
)
136+
with open(output_file, "r") as f:
137+
data = f.readlines()
138+
predicted_classes = [x.split(",")[1].strip() for x in data]
139+
predicted_classes = np.array(
140+
[icatcher.classes[x] for x in predicted_classes]
141+
)
142+
confidences = np.array([float(x.split(",")[2].strip()) for x in data])
128143
assert len(predicted_classes) == len(confidences)
129144
assert len(predicted_classes) == len(gt_classes)
130145
if args.mirror_annotation:

0 commit comments

Comments
 (0)