From ad508473c85fa22a0a978aaccfc07d4c6f09645d Mon Sep 17 00:00:00 2001 From: Galina Date: Tue, 11 Apr 2023 22:57:51 +0300 Subject: [PATCH 01/10] Add --save-results-to for demo CLI and exportable --- otx/api/usecases/exportable_code/demo/demo.py | 13 ++++- .../demo_package/executors/synchronous.py | 5 +- .../demo/demo_package/utils.py | 4 +- .../exportable_code/streamer/streamer.py | 18 ++++--- .../exportable_code/visualizers/visualizer.py | 48 +++++++++++++++++++ otx/cli/tools/demo.py | 47 ++++++++++++++++-- otx/cli/tools/utils/demo/images_capture.py | 24 +++++----- 7 files changed, 134 insertions(+), 25 deletions(-) diff --git a/otx/api/usecases/exportable_code/demo/demo.py b/otx/api/usecases/exportable_code/demo/demo.py index a7bda9e8938..b613f1d720a 100644 --- a/otx/api/usecases/exportable_code/demo/demo.py +++ b/otx/api/usecases/exportable_code/demo/demo.py @@ -74,6 +74,13 @@ def build_argparser(): default="CPU", type=str, ) + args.add_argument( + "--save-results-to", + default=None, + type=str, + help="Output path to save input data with predictions.", + ) + return parser @@ -96,6 +103,10 @@ def get_inferencer_class(type_inference, models): def main(): """Main function that is used to run demo.""" args = build_argparser().parse_args() + + if args.loop and args.save_results_to: + raise ValueError('--loop and --save-results-to cannot be both specified') + # create models models = [] for model_dir in args.models: @@ -105,7 +116,7 @@ def main(): inferencer = get_inferencer_class(args.inference_type, models) # create visualizer - visualizer = create_visualizer(models[-1].task_type, no_show=args.no_show) + visualizer = create_visualizer(models[-1].task_type, no_show=args.no_show, save_results_to=args.save_results_to) if len(models) == 1: models = models[0] diff --git a/otx/api/usecases/exportable_code/demo/demo_package/executors/synchronous.py b/otx/api/usecases/exportable_code/demo/demo_package/executors/synchronous.py index 95184aff48f..43765a27489 100644 --- a/otx/api/usecases/exportable_code/demo/demo_package/executors/synchronous.py +++ b/otx/api/usecases/exportable_code/demo/demo_package/executors/synchronous.py @@ -32,11 +32,14 @@ def run(self, input_stream: Union[int, str], loop: bool = False) -> None: """Run demo using input stream (image, video stream, camera).""" streamer = get_streamer(input_stream, loop) - for frame in streamer: + for (frame, input_path) in streamer: # getting result include preprocessing, infer, postprocessing for sync infer predictions, frame_meta = self.model(frame) annotation_scene = self.converter.convert_to_annotation(predictions, frame_meta) output = self.visualizer.draw(frame, annotation_scene, frame_meta) self.visualizer.show(output) + self.visualizer.save_frame(output, input_path, str(streamer.get_type())) if self.visualizer.is_quit(): break + + self.visualizer.dump_frames(streamer) \ No newline at end of file diff --git a/otx/api/usecases/exportable_code/demo/demo_package/utils.py b/otx/api/usecases/exportable_code/demo/demo_package/utils.py index 663aafd6f19..5a136dfd8fe 100644 --- a/otx/api/usecases/exportable_code/demo/demo_package/utils.py +++ b/otx/api/usecases/exportable_code/demo/demo_package/utils.py @@ -47,9 +47,9 @@ def create_output_converter(task_type: TaskType, labels: LabelSchemaEntity): return create_converter(converter_type, labels) -def create_visualizer(_task_type: TaskType, no_show: bool = False): +def create_visualizer(_task_type: TaskType, no_show: bool = False, save_results_to: str = None): """Create visualizer according to kind of task.""" # TODO: use anomaly-specific visualizer for anomaly tasks - return Visualizer(window_name="Result", no_show=no_show) + return Visualizer(window_name="Result", no_show=no_show, save_results_to=save_results_to) diff --git a/otx/api/usecases/exportable_code/streamer/streamer.py b/otx/api/usecases/exportable_code/streamer/streamer.py index a31b1652877..f4d40e40e8c 100644 --- a/otx/api/usecases/exportable_code/streamer/streamer.py +++ b/otx/api/usecases/exportable_code/streamer/streamer.py @@ -143,6 +143,7 @@ class VideoStreamer(BaseStreamer): def __init__(self, input_path: str, loop: bool = False) -> None: self.media_type = MediaType.VIDEO + self.input_path = input_path self.loop = loop self.cap = cv2.VideoCapture() status = self.cap.open(input_path) @@ -157,13 +158,17 @@ def __iter__(self) -> Iterator[np.ndarray]: while True: status, image = self.cap.read() if status: - yield cv2.cvtColor(image, cv2.COLOR_BGR2RGB) + yield cv2.cvtColor(image, cv2.COLOR_BGR2RGB), self.input_path else: if self.loop: - self.cap.set(cv2.CAP_PROP_POS_FRAMES, 0) + self.cap.set(cv2.CAP_PROP_POS_FRAMES, 0), None else: break + def fps(self): + """Returns a frequency of getting images from source.""" + return self.cap.get(cv2.CAP_PROP_FPS) + def get_type(self) -> MediaType: """Returns the type of media.""" return MediaType.VIDEO @@ -204,7 +209,7 @@ def __iter__(self) -> Iterator[np.ndarray]: if not frame_available: break frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) - yield frame + yield frame, None self.stream.release() @@ -230,6 +235,7 @@ class ImageStreamer(BaseStreamer): def __init__(self, input_path: str, loop: bool = False) -> None: self.loop = loop self.media_type = MediaType.IMAGE + self.input_path = input_path if not os.path.isfile(input_path): raise InvalidInput(f"Can't find the image by {input_path}") self.image = cv2.imread(input_path, cv2.IMREAD_COLOR) @@ -240,10 +246,10 @@ def __init__(self, input_path: str, loop: bool = False) -> None: def __iter__(self) -> Iterator[np.ndarray]: """If loop is True, yield the image again and again.""" if not self.loop: - yield self.image + yield self.image, self.input_path else: while True: - yield self.image + yield self.image, None def get_type(self) -> MediaType: """Returns the type of the streamer.""" @@ -293,7 +299,7 @@ def __iter__(self) -> Iterator[np.ndarray]: else: self.file_id = self.file_id + 1 if not self.loop else 0 if image is not None: - yield cv2.cvtColor(image, cv2.COLOR_BGR2RGB) + yield cv2.cvtColor(image, cv2.COLOR_BGR2RGB), filename def get_type(self) -> MediaType: """Returns the type of the streamer.""" diff --git a/otx/api/usecases/exportable_code/visualizers/visualizer.py b/otx/api/usecases/exportable_code/visualizers/visualizer.py index 390a0d0d48a..773790a0b82 100644 --- a/otx/api/usecases/exportable_code/visualizers/visualizer.py +++ b/otx/api/usecases/exportable_code/visualizers/visualizer.py @@ -5,10 +5,12 @@ # import abc +from collections import defaultdict from typing import Optional import cv2 import numpy as np +from pathlib import Path from otx.api.entities.annotation import AnnotationSceneEntity from otx.api.utils.shape_drawer import ShapeDrawer @@ -66,6 +68,7 @@ def __init__( is_one_label: bool = False, no_show: bool = False, delay: Optional[int] = None, + save_results_to: Optional[str] = None ) -> None: self.window_name = "Window" if window_name is None else window_name self.shape_drawer = ShapeDrawer(show_count, is_one_label) @@ -74,6 +77,9 @@ def __init__( self.no_show = no_show if delay is None: self.delay = 1 + self.save_results_to = save_results_to + self.saved_frames = defaultdict(list) + def draw( self, @@ -111,3 +117,45 @@ def is_quit(self) -> bool: return False return ord("q") == cv2.waitKey(self.delay) + + def save_frame(self, image, input_path, streamer_type) -> None: + """Save result image into dict. + + Args: + image (np.ndarray): Image to be saved. + input_path (str): Filename of the image + streamer_type (str) : The type of the input + """ + + if self.save_results_to and input_path: + filename = Path(input_path).name + if "VIDEO" in streamer_type: + self.saved_frames[filename].append(image) + else: + self.saved_frames[filename] = image + + def dump_frames(self, streamer) -> None: + """Save frames to file system. + + Args: + streamer (str): The streamer with images to be saved + """ + if len(self.saved_frames) > 0: + if not Path(self.save_results_to).exists(): + Path(self.save_results_to).mkdir(parents=True) + + if "VIDEO" in str(streamer.get_type()): + filename, frames = list(self.saved_frames.items())[0] + w, h, _ = frames[0].shape + video_path = str(Path(self.save_results_to) / filename) + codec = cv2.VideoWriter_fourcc(*'mp4v') + out = cv2.VideoWriter(video_path, codec, streamer.fps(), (h, w)) + for frame in frames: + out.write(frame) + out.release() + print(f"Video was saved to {video_path}") + else: + for filename, frame in self.saved_frames.items(): + image_path = str(Path(self.save_results_to, filename)) + cv2.imwrite(image_path, frame) + print(f"Image was saved to {image_path}") diff --git a/otx/cli/tools/demo.py b/otx/cli/tools/demo.py index ea6547322e8..a00ab07da68 100644 --- a/otx/cli/tools/demo.py +++ b/otx/cli/tools/demo.py @@ -15,10 +15,11 @@ # and limitations under the License. import time -from collections import deque +from collections import deque, defaultdict import cv2 import numpy as np +from pathlib import Path from otx.api.entities.annotation import AnnotationSceneEntity, AnnotationSceneKind from otx.api.entities.datasets import DatasetEntity, DatasetItemEntity @@ -71,6 +72,12 @@ def get_args(): "These metrics take into account not only model inference time, but also " "frame reading, pre-processing and post-processing.", ) + parser.add_argument( + "--save-results-to", + default=None, + type=str, + help="Output path to save input data with predictions.", + ) add_hyper_parameters_sub_parser(parser, hyper_parameters, modes=("INFERENCE",)) override_param = [f"params.{param[2:].split('=')[0]}" for param in params if param.startswith("--")] @@ -106,6 +113,9 @@ def main(): # Dynamically create an argument parser based on override parameters. args, override_param = get_args() + if args.loop and args.save_results_to: + raise ValueError('--loop and --save-results-to cannot be both specified') + config_manager = ConfigManager(args, mode="demo") # Auto-Configuration for model template config_manager.configure_template() @@ -137,8 +147,9 @@ def main(): elapsed_times = deque(maxlen=10) frame_index = 0 + saved_frames = defaultdict(list) while True: - frame = capture.read() + frame, path = capture.read() if frame is None: break @@ -155,15 +166,43 @@ def main(): color=(255, 255, 255), ) - if args.delay >= 0: + if args.delay > 0: cv2.imshow("frame", frame) if cv2.waitKey(args.delay) == ESC_BUTTON: break else: print(f"{frame_index=}, {elapsed_time=}, {len(predictions)=}") + # path to input is returned during the first pass through input only + if args.save_results_to and path: + filename = Path(path).name + if capture.get_type() == "VIDEO": + saved_frames[filename].append(frame) + else: + saved_frames[filename] = frame + + if len(saved_frames) > 0: + if not Path(args.save_results_to).exists(): + Path(args.save_results_to).mkdir(parents=True) + + if capture.get_type() == "VIDEO": + filename, frames = list(saved_frames.items())[0] + w, h, _ = frames[0].shape + video_path = str(Path(args.save_results_to) / filename) + codec = cv2.VideoWriter_fourcc(*'mp4v') + out = cv2.VideoWriter(video_path, codec, capture.fps(), (h, w)) + for frame in frames: + out.write(frame) + out.release() + print(f"Video was saved to {video_path}") + else: + for filename, frame in saved_frames.items(): + image_path = str(Path(args.save_results_to, filename)) + cv2.imwrite(image_path, frame) + print(f"Image was saved to {image_path}") + return dict(retcode=0, template=template.name) if __name__ == "__main__": - main() + main() \ No newline at end of file diff --git a/otx/cli/tools/utils/demo/images_capture.py b/otx/cli/tools/utils/demo/images_capture.py index 23002e85762..8b486515af3 100644 --- a/otx/cli/tools/utils/demo/images_capture.py +++ b/otx/cli/tools/utils/demo/images_capture.py @@ -63,6 +63,7 @@ def __init__(self, source, loop): self.loop = loop if not os.path.isfile(source): raise InvalidInput(f"Can't find the image by {source}") + self.source = source self.image = cv2.imread(source, cv2.IMREAD_COLOR) if self.image is None: raise OpenError(f"Can't open the image from {source}") @@ -70,12 +71,12 @@ def __init__(self, source, loop): def read(self): """Returns captured image.""" - if self.loop: - return copy.deepcopy(self.image) if self.can_read: self.can_read = False - return copy.deepcopy(self.image) - return None + return copy.deepcopy(self.image), self.source + if self.loop: + return copy.deepcopy(self.image), None + return None, None def fps(self): """Returns a frequency of getting images from source.""" @@ -112,7 +113,7 @@ def read(self): image = cv2.imread(filename, cv2.IMREAD_COLOR) self.file_id += 1 if image is not None: - return image + return image, filename if self.loop: self.file_id = 0 while self.file_id < len(self.names): @@ -120,8 +121,8 @@ def read(self): image = cv2.imread(filename, cv2.IMREAD_COLOR) self.file_id += 1 if image is not None: - return image - return None + return image, None + return None, None def fps(self): """Returns a frequency of getting images from source.""" @@ -138,6 +139,7 @@ class VideoCapWrapper(ImagesCapture): def __init__(self, source, loop): self.loop = loop self.cap = cv2.VideoCapture() + self.source = source status = self.cap.open(source) if not status: raise InvalidInput(f"Can't open the video from {source}") @@ -147,12 +149,12 @@ def read(self): status, image = self.cap.read() if not status: if not self.loop: - return None + return None, None self.cap.set(cv2.CAP_PROP_POS_FRAMES, 0) status, image = self.cap.read() if not status: - return None - return image + return None, None + return image, self.source def fps(self): """Returns a frequency of getting images from source.""" @@ -186,7 +188,7 @@ def read(self): status, image = self.cap.read() if not status: return None - return image + return image, None def fps(self): """Returns a frequency of getting images from source.""" From d43861b9d8ef718a8a0a5bb35792a7a55b8a72fe Mon Sep 17 00:00:00 2001 From: Galina Date: Wed, 12 Apr 2023 16:31:00 +0300 Subject: [PATCH 02/10] Fix linter issues --- otx/api/usecases/exportable_code/demo/demo.py | 7 +-- .../demo_package/executors/asynchronous.py | 5 +- .../demo_package/executors/sync_pipeline.py | 5 +- .../demo_package/executors/synchronous.py | 2 +- .../demo/demo_package/utils.py | 2 +- .../exportable_code/visualizers/visualizer.py | 31 +++++----- otx/cli/tools/demo.py | 56 ++++++++++--------- 7 files changed, 62 insertions(+), 46 deletions(-) diff --git a/otx/api/usecases/exportable_code/demo/demo.py b/otx/api/usecases/exportable_code/demo/demo.py index b613f1d720a..1c6d00ae4d4 100644 --- a/otx/api/usecases/exportable_code/demo/demo.py +++ b/otx/api/usecases/exportable_code/demo/demo.py @@ -77,11 +77,10 @@ def build_argparser(): args.add_argument( "--save-results-to", default=None, - type=str, - help="Output path to save input data with predictions.", + type=Path, + help="Optional. Output path to save input data with predictions.", ) - return parser @@ -105,7 +104,7 @@ def main(): args = build_argparser().parse_args() if args.loop and args.save_results_to: - raise ValueError('--loop and --save-results-to cannot be both specified') + raise ValueError("--loop and --save-results-to cannot be both specified") # create models models = [] diff --git a/otx/api/usecases/exportable_code/demo/demo_package/executors/asynchronous.py b/otx/api/usecases/exportable_code/demo/demo_package/executors/asynchronous.py index 0d935ef1815..c23258b14be 100644 --- a/otx/api/usecases/exportable_code/demo/demo_package/executors/asynchronous.py +++ b/otx/api/usecases/exportable_code/demo/demo_package/executors/asynchronous.py @@ -39,12 +39,13 @@ def run(self, input_stream: Union[int, str], loop: bool = False) -> None: next_frame_id_to_show = 0 stop_visualization = False - for frame in streamer: + for (frame, input_path) in streamer: results = self.async_pipeline.get_result(next_frame_id_to_show) while results: output = self.render_result(results) next_frame_id_to_show += 1 self.visualizer.show(output) + self.visualizer.save_frame(output, input_path, str(streamer.get_type())) if self.visualizer.is_quit(): stop_visualization = True results = self.async_pipeline.get_result(next_frame_id_to_show) @@ -57,6 +58,8 @@ def run(self, input_stream: Union[int, str], loop: bool = False) -> None: results = self.async_pipeline.get_result(next_frame_id_to_show) output = self.render_result(results) self.visualizer.show(output) + self.visualizer.save_frame(output, input_path, str(streamer.get_type())) + self.visualizer.dump_frames(streamer) def render_result(self, results: Tuple[Any, dict]) -> np.ndarray: """Render for results of inference.""" diff --git a/otx/api/usecases/exportable_code/demo/demo_package/executors/sync_pipeline.py b/otx/api/usecases/exportable_code/demo/demo_package/executors/sync_pipeline.py index eae537837ff..a6804327690 100644 --- a/otx/api/usecases/exportable_code/demo/demo_package/executors/sync_pipeline.py +++ b/otx/api/usecases/exportable_code/demo/demo_package/executors/sync_pipeline.py @@ -79,10 +79,13 @@ def run(self, input_stream: Union[int, str], loop: bool = False) -> None: """Run demo using input stream (image, video stream, camera).""" streamer = get_streamer(input_stream, loop) - for frame in streamer: + for (frame, input_path) in streamer: # getting result for single image annotation_scene = self.single_run(frame) output = self.visualizer.draw(frame, annotation_scene, {}) self.visualizer.show(output) + self.visualizer.save_frame(output, input_path, str(streamer.get_type())) if self.visualizer.is_quit(): break + + self.visualizer.dump_frames(streamer) \ No newline at end of file diff --git a/otx/api/usecases/exportable_code/demo/demo_package/executors/synchronous.py b/otx/api/usecases/exportable_code/demo/demo_package/executors/synchronous.py index 43765a27489..3b6b0b38767 100644 --- a/otx/api/usecases/exportable_code/demo/demo_package/executors/synchronous.py +++ b/otx/api/usecases/exportable_code/demo/demo_package/executors/synchronous.py @@ -42,4 +42,4 @@ def run(self, input_stream: Union[int, str], loop: bool = False) -> None: if self.visualizer.is_quit(): break - self.visualizer.dump_frames(streamer) \ No newline at end of file + self.visualizer.dump_frames(streamer) diff --git a/otx/api/usecases/exportable_code/demo/demo_package/utils.py b/otx/api/usecases/exportable_code/demo/demo_package/utils.py index 5a136dfd8fe..2f9695e70ff 100644 --- a/otx/api/usecases/exportable_code/demo/demo_package/utils.py +++ b/otx/api/usecases/exportable_code/demo/demo_package/utils.py @@ -47,7 +47,7 @@ def create_output_converter(task_type: TaskType, labels: LabelSchemaEntity): return create_converter(converter_type, labels) -def create_visualizer(_task_type: TaskType, no_show: bool = False, save_results_to: str = None): +def create_visualizer(_task_type: TaskType, no_show: bool = False, save_results_to: Path = None): """Create visualizer according to kind of task.""" # TODO: use anomaly-specific visualizer for anomaly tasks diff --git a/otx/api/usecases/exportable_code/visualizers/visualizer.py b/otx/api/usecases/exportable_code/visualizers/visualizer.py index 773790a0b82..e3fd3b2f7a7 100644 --- a/otx/api/usecases/exportable_code/visualizers/visualizer.py +++ b/otx/api/usecases/exportable_code/visualizers/visualizer.py @@ -5,12 +5,11 @@ # import abc -from collections import defaultdict -from typing import Optional +from pathlib import Path +from typing import Dict, Optional import cv2 import numpy as np -from pathlib import Path from otx.api.entities.annotation import AnnotationSceneEntity from otx.api.utils.shape_drawer import ShapeDrawer @@ -61,14 +60,15 @@ class Visualizer(IVisualizer): >>> visualizer.show(output) """ - def __init__( + def __init__( # pylint: disable=too-many-arguments self, window_name: Optional[str] = None, show_count: bool = False, is_one_label: bool = False, no_show: bool = False, delay: Optional[int] = None, - save_results_to: Optional[str] = None + save_results_to: str = None, + saved_frames: Dict[str, list] = None, ) -> None: self.window_name = "Window" if window_name is None else window_name self.shape_drawer = ShapeDrawer(show_count, is_one_label) @@ -78,8 +78,10 @@ def __init__( if delay is None: self.delay = 1 self.save_results_to = save_results_to - self.saved_frames = defaultdict(list) - + if not saved_frames: + self.saved_frames = {} + else: + self.saved_frames = saved_frames def draw( self, @@ -129,6 +131,8 @@ def save_frame(self, image, input_path, streamer_type) -> None: if self.save_results_to and input_path: filename = Path(input_path).name + if filename not in self.saved_frames: + self.saved_frames[filename] = [] if "VIDEO" in streamer_type: self.saved_frames[filename].append(image) else: @@ -140,15 +144,16 @@ def dump_frames(self, streamer) -> None: Args: streamer (str): The streamer with images to be saved """ - if len(self.saved_frames) > 0: - if not Path(self.save_results_to).exists(): - Path(self.save_results_to).mkdir(parents=True) + if len(self.saved_frames) > 0 and self.save_results_to: + output_path = Path(self.save_results_to) + if not output_path.exists(): + output_path.mkdir(parents=True) if "VIDEO" in str(streamer.get_type()): filename, frames = list(self.saved_frames.items())[0] w, h, _ = frames[0].shape - video_path = str(Path(self.save_results_to) / filename) - codec = cv2.VideoWriter_fourcc(*'mp4v') + video_path = str(output_path / filename) + codec = cv2.VideoWriter_fourcc(*"mp4v") out = cv2.VideoWriter(video_path, codec, streamer.fps(), (h, w)) for frame in frames: out.write(frame) @@ -156,6 +161,6 @@ def dump_frames(self, streamer) -> None: print(f"Video was saved to {video_path}") else: for filename, frame in self.saved_frames.items(): - image_path = str(Path(self.save_results_to, filename)) + image_path = str(output_path / filename) cv2.imwrite(image_path, frame) print(f"Image was saved to {image_path}") diff --git a/otx/cli/tools/demo.py b/otx/cli/tools/demo.py index a00ab07da68..cbde542c412 100644 --- a/otx/cli/tools/demo.py +++ b/otx/cli/tools/demo.py @@ -15,11 +15,11 @@ # and limitations under the License. import time -from collections import deque, defaultdict +from collections import defaultdict, deque +from pathlib import Path import cv2 import numpy as np -from pathlib import Path from otx.api.entities.annotation import AnnotationSceneEntity, AnnotationSceneKind from otx.api.entities.datasets import DatasetEntity, DatasetItemEntity @@ -107,6 +107,31 @@ def get_predictions(task, frame): return item.get_annotations(), elapsed_time +def dump_frames(saved_frames, output_path, capture): + """Saves images/videos with predictions from saved_frames to file system.""" + + if len(saved_frames) > 0: + output_path = Path(output_path) + if not output_path.exists(): + output_path.mkdir(parents=True) + + if capture.get_type() == "VIDEO": + filename, frames = list(saved_frames.items())[0] + w, h, _ = frames[0].shape + video_path = str(output_path / filename) + codec = cv2.VideoWriter_fourcc(*"mp4v") + out = cv2.VideoWriter(video_path, codec, capture.fps(), (h, w)) + for frame in frames: + out.write(frame) + out.release() + print(f"Video was saved to {video_path}") + else: + for filename, frame in saved_frames.items(): + image_path = str(output_path / filename) + cv2.imwrite(image_path, frame) + print(f"Image was saved to {image_path}") + + def main(): """Main function that is used for model demonstration.""" @@ -114,7 +139,7 @@ def main(): args, override_param = get_args() if args.loop and args.save_results_to: - raise ValueError('--loop and --save-results-to cannot be both specified') + raise ValueError("--loop and --save-results-to cannot be both specified") config_manager = ConfigManager(args, mode="demo") # Auto-Configuration for model template @@ -146,7 +171,6 @@ def main(): capture = open_images_capture(args.input, args.loop) elapsed_times = deque(maxlen=10) - frame_index = 0 saved_frames = defaultdict(list) while True: frame, path = capture.read() @@ -171,7 +195,7 @@ def main(): if cv2.waitKey(args.delay) == ESC_BUTTON: break else: - print(f"{frame_index=}, {elapsed_time=}, {len(predictions)=}") + print(f"Frame: {elapsed_time=}, {len(predictions)=}") # path to input is returned during the first pass through input only if args.save_results_to and path: @@ -181,28 +205,10 @@ def main(): else: saved_frames[filename] = frame - if len(saved_frames) > 0: - if not Path(args.save_results_to).exists(): - Path(args.save_results_to).mkdir(parents=True) - - if capture.get_type() == "VIDEO": - filename, frames = list(saved_frames.items())[0] - w, h, _ = frames[0].shape - video_path = str(Path(args.save_results_to) / filename) - codec = cv2.VideoWriter_fourcc(*'mp4v') - out = cv2.VideoWriter(video_path, codec, capture.fps(), (h, w)) - for frame in frames: - out.write(frame) - out.release() - print(f"Video was saved to {video_path}") - else: - for filename, frame in saved_frames.items(): - image_path = str(Path(args.save_results_to, filename)) - cv2.imwrite(image_path, frame) - print(f"Image was saved to {image_path}") + dump_frames(saved_frames, args.save_results_to, capture) return dict(retcode=0, template=template.name) if __name__ == "__main__": - main() \ No newline at end of file + main() From fea9160d1914715c4133e28e8c3d116eda2d0250 Mon Sep 17 00:00:00 2001 From: Galina Date: Wed, 12 Apr 2023 21:02:03 +0300 Subject: [PATCH 03/10] Update docs & CHANGELOG --- CHANGELOG.md | 1 + .../quick_start_guide/cli_commands.rst | 7 ++--- docs/source/guide/tutorials/base/demo.rst | 27 +++++++++++-------- docs/source/guide/tutorials/base/deploy.rst | 15 ++++++++--- 4 files changed, 33 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2dc2642083d..23d3b8aed77 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ All notable changes to this project will be documented in this file. - Add generating feature cli_report.log in output for otx training () - Support multiple python versions up to 3.10 () - Support export of onnx models () +- Add option to save images after inference in OTX CLI demo together with demo in exportable code () ### Enhancements diff --git a/docs/source/guide/get_started/quick_start_guide/cli_commands.rst b/docs/source/guide/get_started/quick_start_guide/cli_commands.rst index d2d9da0bd39..485e442307d 100644 --- a/docs/source/guide/get_started/quick_start_guide/cli_commands.rst +++ b/docs/source/guide/get_started/quick_start_guide/cli_commands.rst @@ -474,10 +474,10 @@ Demonstration .. code-block:: (otx) ...$ otx demo --help - usage: otx demo [-h] -i INPUT --load-weights LOAD_WEIGHTS [--fit-to-size FIT_TO_SIZE FIT_TO_SIZE] [--loop] [--delay DELAY] [--display-perf] [template] {params} ... + usage: otx demo [-h] -i INPUT --load-weights LOAD_WEIGHTS [--fit-to-size FIT_TO_SIZE FIT_TO_SIZE] [--loop] [--delay DELAY] [--display-perf] [--save-results-to SAVE_RESULTS_TO] [template] {params} ... positional arguments: - template Enter the path or ID or name of the template file. + template Enter the path or ID or name of the template file. This can be omitted if you have train-data-roots or run inside a workspace. {params} sub-command help params Hyper parameters defined in template file. @@ -493,7 +493,8 @@ Demonstration --loop Enable reading the input in a loop. --delay DELAY Frame visualization time in ms. --display-perf This option enables writing performance metrics on displayed frame. These metrics take into account not only model inference time, but also frame reading, pre-processing and post-processing. - + --save-results-to SAVE_RESULTS_TO + Output path to save input data with predictions. Command example of the demonstration: diff --git a/docs/source/guide/tutorials/base/demo.rst b/docs/source/guide/tutorials/base/demo.rst index 735cc664515..dec41e44d44 100644 --- a/docs/source/guide/tutorials/base/demo.rst +++ b/docs/source/guide/tutorials/base/demo.rst @@ -34,9 +34,14 @@ But if we'll provide a single image the demo processes and renders it quickly, t (demo) ...$ otx demo --input docs/utils/images/wgisd_dataset_sample.jpg \ --load-weights outputs/weights.pth --loop -In this case, you can stop the demo by killing the process in the terminal (``Ctrl+C`` for Linux). +In this case, you can stop the demo by pressing `Esc` button or killing the process in the terminal (``Ctrl+C`` for Linux). -3. In WGISD dataset we have high-resolution images, +3. If we want to pass an images folder, it's better to specify the delay parameter, that defines, how much millisecond pause will be held between showing the next image. +For example ``--delay 100`` will make this pause 0.1 ms. +If you want to skip showing the resulting image and instead see the number of predictions and time spent on each image inference, specify ``--delay 0``. + + +4. In WGISD dataset we have high-resolution images, so the ``--fit-to-size`` parameter would be quite useful. It resizes the resulting image to a specified: .. code-block:: @@ -44,11 +49,17 @@ so the ``--fit-to-size`` parameter would be quite useful. It resizes the resulti (demo) ...$ otx demo --input docs/utils/images/wgisd_dataset_sample.jpg \ --load-weights outputs/weights.pth --loop --fit-to-size 800 600 -4. If we want to pass an images folder, it's better to specify the delay parameter, that defines, how much millisecond pause will be held between showing the next image. -For example ``--delay 100`` will make this pause 0.1 ms. +5. To save inferenced results with predictions on it, we can specify the folder path, using ``--save-results-to``. +It works for images, videos and image folders. To prevent issues, do not specify it together with a ``--loop`` parameter. -5. If we want to show inference speed right on images, +.. code-block:: + + (demo) ...$ otx demo --input docs/utils/images/wgisd_dataset_sample.jpg \ + --load-weights outputs/weights.pth \ + --save-results-to resulted_images + +6. If we want to show inference speed right on images, we can run the following line: .. code-block:: @@ -57,12 +68,6 @@ we can run the following line: --load-weights outputs/weights.pth --loop \ --fit-to-size 800 600 --display-perf -.. The result will look like this: - -.. .. image:: ../../../../utils/images/wgisd_pr_sample.jpg -.. :width: 600 -.. :alt: this image shows the inference results with inference time on the WGISD dataset -.. image to be generated and added 6. To run a demo on a web camera, you need to know its ID. You can check a list of camera devices by running the command line below on Linux system: diff --git a/docs/source/guide/tutorials/base/deploy.rst b/docs/source/guide/tutorials/base/deploy.rst index dfba9bda762..66902e4081f 100644 --- a/docs/source/guide/tutorials/base/deploy.rst +++ b/docs/source/guide/tutorials/base/deploy.rst @@ -100,11 +100,20 @@ For example, the model inference on image from WGISD dataset, which we used for If you provide a single image as input, the demo processes and renders it quickly, then exits. To continuously visualize inference results on the screen, apply the ``loop`` option, which enforces processing a single image in a loop. - In this case, you can stop the demo by killing the process in the terminal (``Ctrl+C`` for Linux). + In this case, you can stop the demo by pressing `Esc` button or killing the process in the terminal (``Ctrl+C`` for Linux). To learn how to run the demo on Windows and MacOS, please refer to the ``outputs/deploy/python/README.md`` file in exportable code. -4. To run a demo on a web camera, we need to know its ID. +4. To save inferenced results with predictions on it, we can specify the folder path, using ``--save-results-to``. +It works for images, videos and image folders. To prevent issues, do not specify it together with a ``--loop`` parameter. + +.. code-block:: + + (demo) ...$ python outputs/deploy/python/demo.py --input docs/utils/images/wgisd_dataset_sample.jpg \ + --models outputs/deploy/model \ + --save-results-to resulted_images + +5. To run a demo on a web camera, we need to know its ID. We can check a list of camera devices by running this command line on Linux system: .. code-block:: @@ -121,7 +130,7 @@ The output will look like this: After that, we can use this ``/dev/video0`` as a camera ID for ``--input``. -5. We can also change ``config.json`` that specifies the confidence threshold and +6. We can also change ``config.json`` that specifies the confidence threshold and color for each class visualization, but any changes should be made with caution. For example, in our image of the winery we see, that a lot of objects weren't detected. From 8c11c3d42c726e6e0eda7364f00cf8ed4dbdc2e9 Mon Sep 17 00:00:00 2001 From: Galina Date: Wed, 12 Apr 2023 22:03:39 +0300 Subject: [PATCH 04/10] Fix linter --- otx/api/usecases/exportable_code/demo/demo.py | 2 +- .../demo/demo_package/executors/asynchronous.py | 1 - .../demo/demo_package/executors/sync_pipeline.py | 2 +- .../exportable_code/demo/demo_package/utils.py | 2 +- .../exportable_code/streamer/streamer.py | 2 +- otx/cli/tools/demo.py | 16 +++++++++++----- 6 files changed, 15 insertions(+), 10 deletions(-) diff --git a/otx/api/usecases/exportable_code/demo/demo.py b/otx/api/usecases/exportable_code/demo/demo.py index 1c6d00ae4d4..ec6e6d97513 100644 --- a/otx/api/usecases/exportable_code/demo/demo.py +++ b/otx/api/usecases/exportable_code/demo/demo.py @@ -77,7 +77,7 @@ def build_argparser(): args.add_argument( "--save-results-to", default=None, - type=Path, + type=str, help="Optional. Output path to save input data with predictions.", ) diff --git a/otx/api/usecases/exportable_code/demo/demo_package/executors/asynchronous.py b/otx/api/usecases/exportable_code/demo/demo_package/executors/asynchronous.py index c23258b14be..3f93a11adca 100644 --- a/otx/api/usecases/exportable_code/demo/demo_package/executors/asynchronous.py +++ b/otx/api/usecases/exportable_code/demo/demo_package/executors/asynchronous.py @@ -58,7 +58,6 @@ def run(self, input_stream: Union[int, str], loop: bool = False) -> None: results = self.async_pipeline.get_result(next_frame_id_to_show) output = self.render_result(results) self.visualizer.show(output) - self.visualizer.save_frame(output, input_path, str(streamer.get_type())) self.visualizer.dump_frames(streamer) def render_result(self, results: Tuple[Any, dict]) -> np.ndarray: diff --git a/otx/api/usecases/exportable_code/demo/demo_package/executors/sync_pipeline.py b/otx/api/usecases/exportable_code/demo/demo_package/executors/sync_pipeline.py index a6804327690..bf8f6527350 100644 --- a/otx/api/usecases/exportable_code/demo/demo_package/executors/sync_pipeline.py +++ b/otx/api/usecases/exportable_code/demo/demo_package/executors/sync_pipeline.py @@ -88,4 +88,4 @@ def run(self, input_stream: Union[int, str], loop: bool = False) -> None: if self.visualizer.is_quit(): break - self.visualizer.dump_frames(streamer) \ No newline at end of file + self.visualizer.dump_frames(streamer) diff --git a/otx/api/usecases/exportable_code/demo/demo_package/utils.py b/otx/api/usecases/exportable_code/demo/demo_package/utils.py index 2f9695e70ff..5a136dfd8fe 100644 --- a/otx/api/usecases/exportable_code/demo/demo_package/utils.py +++ b/otx/api/usecases/exportable_code/demo/demo_package/utils.py @@ -47,7 +47,7 @@ def create_output_converter(task_type: TaskType, labels: LabelSchemaEntity): return create_converter(converter_type, labels) -def create_visualizer(_task_type: TaskType, no_show: bool = False, save_results_to: Path = None): +def create_visualizer(_task_type: TaskType, no_show: bool = False, save_results_to: str = None): """Create visualizer according to kind of task.""" # TODO: use anomaly-specific visualizer for anomaly tasks diff --git a/otx/api/usecases/exportable_code/streamer/streamer.py b/otx/api/usecases/exportable_code/streamer/streamer.py index f4d40e40e8c..c4c4f12ea6b 100644 --- a/otx/api/usecases/exportable_code/streamer/streamer.py +++ b/otx/api/usecases/exportable_code/streamer/streamer.py @@ -161,7 +161,7 @@ def __iter__(self) -> Iterator[np.ndarray]: yield cv2.cvtColor(image, cv2.COLOR_BGR2RGB), self.input_path else: if self.loop: - self.cap.set(cv2.CAP_PROP_POS_FRAMES, 0), None + self.cap.set(cv2.CAP_PROP_POS_FRAMES, 0) else: break diff --git a/otx/cli/tools/demo.py b/otx/cli/tools/demo.py index cbde542c412..6162470a270 100644 --- a/otx/cli/tools/demo.py +++ b/otx/cli/tools/demo.py @@ -107,6 +107,16 @@ def get_predictions(task, frame): return item.get_annotations(), elapsed_time +def save_frame(frame, path, capture_type, saved_frames): + """Saves frames with predictions to saved_frames to to dump it in one time.""" + + filename = Path(path).name + if capture_type == "VIDEO": + saved_frames[filename].append(frame) + else: + saved_frames[filename] = frame + + def dump_frames(saved_frames, output_path, capture): """Saves images/videos with predictions from saved_frames to file system.""" @@ -199,11 +209,7 @@ def main(): # path to input is returned during the first pass through input only if args.save_results_to and path: - filename = Path(path).name - if capture.get_type() == "VIDEO": - saved_frames[filename].append(frame) - else: - saved_frames[filename] = frame + save_frame(frame, path, capture.get_type(), saved_frames) dump_frames(saved_frames, args.save_results_to, capture) From dc5316c8cc0608a134aa85e48c2a51278d11aadd Mon Sep 17 00:00:00 2001 From: Galina Date: Wed, 12 Apr 2023 22:39:50 +0300 Subject: [PATCH 05/10] Update exportable code requirements otx commit --- otx/api/usecases/exportable_code/demo/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/otx/api/usecases/exportable_code/demo/requirements.txt b/otx/api/usecases/exportable_code/demo/requirements.txt index f0bba0497d7..903eb7b6480 100644 --- a/otx/api/usecases/exportable_code/demo/requirements.txt +++ b/otx/api/usecases/exportable_code/demo/requirements.txt @@ -1,4 +1,4 @@ openvino==2022.3.0 openmodelzoo-modelapi==2022.3.0 -otx @ git+https://github.com/openvinotoolkit/training_extensions/@dd03235da2319815227f1b75bce298ee6e8b0f31#egg=otx +otx @ git+https://github.com/openvinotoolkit/training_extensions/@8c11c3d42c726e6e0eda7364f00cf8ed4dbdc2e9#egg=otx numpy>=1.21.0,<=1.23.5 # np.bool was removed in 1.24.0 which was used in openvino runtime From 7fabd677830b7ec821e714dbfd1220c6490a757a Mon Sep 17 00:00:00 2001 From: Galina Date: Thu, 13 Apr 2023 21:10:21 +0300 Subject: [PATCH 06/10] Revert stream modification --- .../demo_package/executors/synchronous.py | 9 ++- otx/cli/tools/demo.py | 76 ++++++++++--------- otx/cli/tools/utils/demo/images_capture.py | 24 +++--- 3 files changed, 57 insertions(+), 52 deletions(-) diff --git a/otx/api/usecases/exportable_code/demo/demo_package/executors/synchronous.py b/otx/api/usecases/exportable_code/demo/demo_package/executors/synchronous.py index 3b6b0b38767..a7a8864435b 100644 --- a/otx/api/usecases/exportable_code/demo/demo_package/executors/synchronous.py +++ b/otx/api/usecases/exportable_code/demo/demo_package/executors/synchronous.py @@ -14,6 +14,8 @@ from otx.api.usecases.exportable_code.streamer import get_streamer from otx.api.usecases.exportable_code.visualizers import Visualizer +from otx.cli.tools.demo import get_input_names_list, dump_frames + class SyncExecutor: """Synchronous executor for model inference. @@ -31,6 +33,7 @@ def __init__(self, model: ModelContainer, visualizer: Visualizer) -> None: def run(self, input_stream: Union[int, str], loop: bool = False) -> None: """Run demo using input stream (image, video stream, camera).""" streamer = get_streamer(input_stream, loop) + saved_frames = [] for (frame, input_path) in streamer: # getting result include preprocessing, infer, postprocessing for sync infer @@ -38,8 +41,10 @@ def run(self, input_stream: Union[int, str], loop: bool = False) -> None: annotation_scene = self.converter.convert_to_annotation(predictions, frame_meta) output = self.visualizer.draw(frame, annotation_scene, frame_meta) self.visualizer.show(output) - self.visualizer.save_frame(output, input_path, str(streamer.get_type())) + if self.visualizer.save_results_to: + saved_frames.append(frame) + # self.visualizer.save_frame(output, input_path, str(streamer.get_type())) if self.visualizer.is_quit(): break - self.visualizer.dump_frames(streamer) + dump_frames(saved_frames, self.visualizer.save_results_to, input_stream, streamer) \ No newline at end of file diff --git a/otx/cli/tools/demo.py b/otx/cli/tools/demo.py index 6162470a270..014e7124f1b 100644 --- a/otx/cli/tools/demo.py +++ b/otx/cli/tools/demo.py @@ -107,39 +107,42 @@ def get_predictions(task, frame): return item.get_annotations(), elapsed_time -def save_frame(frame, path, capture_type, saved_frames): - """Saves frames with predictions to saved_frames to to dump it in one time.""" +def get_input_names_list(input_path, capture): + """Lists the filenames of all inputs for demo.""" - filename = Path(path).name - if capture_type == "VIDEO": - saved_frames[filename].append(frame) + if "DIR" in capture.get_type(): + return [f.name for f in Path(input_path).iterdir() if f.is_file()] else: - saved_frames[filename] = frame - - -def dump_frames(saved_frames, output_path, capture): - """Saves images/videos with predictions from saved_frames to file system.""" - - if len(saved_frames) > 0: - output_path = Path(output_path) - if not output_path.exists(): - output_path.mkdir(parents=True) - - if capture.get_type() == "VIDEO": - filename, frames = list(saved_frames.items())[0] - w, h, _ = frames[0].shape - video_path = str(output_path / filename) - codec = cv2.VideoWriter_fourcc(*"mp4v") - out = cv2.VideoWriter(video_path, codec, capture.fps(), (h, w)) - for frame in frames: - out.write(frame) - out.release() - print(f"Video was saved to {video_path}") - else: - for filename, frame in saved_frames.items(): - image_path = str(output_path / filename) - cv2.imwrite(image_path, frame) - print(f"Image was saved to {image_path}") + return [Path(input_path).name] + + +def dump_frames(saved_frames, output_path, input_path, capture): + """Saves images/videos with predictions from saved_frames to output folder with proper names.""" + + if not saved_frames: + return + + output_path = Path(output_path) + if not output_path.exists(): + output_path.mkdir(parents=True) + + filenames = get_input_names_list(input_path, capture) + + if "VIDEO" in capture.get_type(): + filename = filenames[0] + w, h, _ = saved_frames[0].shape + video_path = str(output_path / filename) + codec = cv2.VideoWriter_fourcc(*"mp4v") + out = cv2.VideoWriter(video_path, codec, capture.fps(), (h, w)) + for frame in saved_frames: + out.write(frame) + out.release() + print(f"Video was saved to {video_path}") + else: + for filename, frame in zip(filenames, saved_frames): + image_path = str(output_path / filename) + cv2.imwrite(image_path, frame) + print(f"Image was saved to {image_path}") def main(): @@ -181,9 +184,9 @@ def main(): capture = open_images_capture(args.input, args.loop) elapsed_times = deque(maxlen=10) - saved_frames = defaultdict(list) + saved_frames = [] while True: - frame, path = capture.read() + frame = capture.read() if frame is None: break @@ -207,11 +210,10 @@ def main(): else: print(f"Frame: {elapsed_time=}, {len(predictions)=}") - # path to input is returned during the first pass through input only - if args.save_results_to and path: - save_frame(frame, path, capture.get_type(), saved_frames) + if args.save_results_to: + saved_frames.append(frame) - dump_frames(saved_frames, args.save_results_to, capture) + dump_frames(saved_frames, args.save_results_to, args.input, capture) return dict(retcode=0, template=template.name) diff --git a/otx/cli/tools/utils/demo/images_capture.py b/otx/cli/tools/utils/demo/images_capture.py index 8b486515af3..23002e85762 100644 --- a/otx/cli/tools/utils/demo/images_capture.py +++ b/otx/cli/tools/utils/demo/images_capture.py @@ -63,7 +63,6 @@ def __init__(self, source, loop): self.loop = loop if not os.path.isfile(source): raise InvalidInput(f"Can't find the image by {source}") - self.source = source self.image = cv2.imread(source, cv2.IMREAD_COLOR) if self.image is None: raise OpenError(f"Can't open the image from {source}") @@ -71,12 +70,12 @@ def __init__(self, source, loop): def read(self): """Returns captured image.""" + if self.loop: + return copy.deepcopy(self.image) if self.can_read: self.can_read = False - return copy.deepcopy(self.image), self.source - if self.loop: - return copy.deepcopy(self.image), None - return None, None + return copy.deepcopy(self.image) + return None def fps(self): """Returns a frequency of getting images from source.""" @@ -113,7 +112,7 @@ def read(self): image = cv2.imread(filename, cv2.IMREAD_COLOR) self.file_id += 1 if image is not None: - return image, filename + return image if self.loop: self.file_id = 0 while self.file_id < len(self.names): @@ -121,8 +120,8 @@ def read(self): image = cv2.imread(filename, cv2.IMREAD_COLOR) self.file_id += 1 if image is not None: - return image, None - return None, None + return image + return None def fps(self): """Returns a frequency of getting images from source.""" @@ -139,7 +138,6 @@ class VideoCapWrapper(ImagesCapture): def __init__(self, source, loop): self.loop = loop self.cap = cv2.VideoCapture() - self.source = source status = self.cap.open(source) if not status: raise InvalidInput(f"Can't open the video from {source}") @@ -149,12 +147,12 @@ def read(self): status, image = self.cap.read() if not status: if not self.loop: - return None, None + return None self.cap.set(cv2.CAP_PROP_POS_FRAMES, 0) status, image = self.cap.read() if not status: - return None, None - return image, self.source + return None + return image def fps(self): """Returns a frequency of getting images from source.""" @@ -188,7 +186,7 @@ def read(self): status, image = self.cap.read() if not status: return None - return image, None + return image def fps(self): """Returns a frequency of getting images from source.""" From a90655472e9aeeb78784acee1ca02006dc7c6908 Mon Sep 17 00:00:00 2001 From: Galina Date: Fri, 14 Apr 2023 00:56:39 +0300 Subject: [PATCH 07/10] Revoke stream changes + change parameter name --- .../quick_start_guide/cli_commands.rst | 6 +- docs/source/guide/tutorials/base/demo.rst | 4 +- docs/source/guide/tutorials/base/deploy.rst | 4 +- otx/api/usecases/exportable_code/demo/demo.py | 8 +-- .../demo_package/executors/asynchronous.py | 9 ++- .../demo_package/executors/sync_pipeline.py | 9 ++- .../demo_package/executors/synchronous.py | 10 ++-- .../demo/demo_package/utils.py | 4 +- .../exportable_code/streamer/streamer.py | 12 ++-- .../exportable_code/visualizers/visualizer.py | 56 +------------------ otx/cli/tools/demo.py | 53 +++--------------- otx/cli/tools/utils/demo/visualization.py | 49 +++++++++++++++- 12 files changed, 92 insertions(+), 132 deletions(-) diff --git a/docs/source/guide/get_started/quick_start_guide/cli_commands.rst b/docs/source/guide/get_started/quick_start_guide/cli_commands.rst index 485e442307d..4c2dedd348d 100644 --- a/docs/source/guide/get_started/quick_start_guide/cli_commands.rst +++ b/docs/source/guide/get_started/quick_start_guide/cli_commands.rst @@ -474,10 +474,10 @@ Demonstration .. code-block:: (otx) ...$ otx demo --help - usage: otx demo [-h] -i INPUT --load-weights LOAD_WEIGHTS [--fit-to-size FIT_TO_SIZE FIT_TO_SIZE] [--loop] [--delay DELAY] [--display-perf] [--save-results-to SAVE_RESULTS_TO] [template] {params} ... + usage: otx demo [-h] -i INPUT --load-weights LOAD_WEIGHTS [--fit-to-size FIT_TO_SIZE FIT_TO_SIZE] [--loop] [--delay DELAY] [--display-perf] [--output OUTPUT] [template] {params} ... positional arguments: - template Enter the path or ID or name of the template file. + template Enter the path or ID or name of the template file. This can be omitted if you have train-data-roots or run inside a workspace. {params} sub-command help params Hyper parameters defined in template file. @@ -493,7 +493,7 @@ Demonstration --loop Enable reading the input in a loop. --delay DELAY Frame visualization time in ms. --display-perf This option enables writing performance metrics on displayed frame. These metrics take into account not only model inference time, but also frame reading, pre-processing and post-processing. - --save-results-to SAVE_RESULTS_TO + --output OUTPUT Output path to save input data with predictions. Command example of the demonstration: diff --git a/docs/source/guide/tutorials/base/demo.rst b/docs/source/guide/tutorials/base/demo.rst index dec41e44d44..1527c1e50eb 100644 --- a/docs/source/guide/tutorials/base/demo.rst +++ b/docs/source/guide/tutorials/base/demo.rst @@ -50,14 +50,14 @@ so the ``--fit-to-size`` parameter would be quite useful. It resizes the resulti --load-weights outputs/weights.pth --loop --fit-to-size 800 600 -5. To save inferenced results with predictions on it, we can specify the folder path, using ``--save-results-to``. +5. To save inferenced results with predictions on it, we can specify the folder path, using ``--output``. It works for images, videos and image folders. To prevent issues, do not specify it together with a ``--loop`` parameter. .. code-block:: (demo) ...$ otx demo --input docs/utils/images/wgisd_dataset_sample.jpg \ --load-weights outputs/weights.pth \ - --save-results-to resulted_images + --output resulted_images 6. If we want to show inference speed right on images, we can run the following line: diff --git a/docs/source/guide/tutorials/base/deploy.rst b/docs/source/guide/tutorials/base/deploy.rst index 66902e4081f..2e029f025fe 100644 --- a/docs/source/guide/tutorials/base/deploy.rst +++ b/docs/source/guide/tutorials/base/deploy.rst @@ -104,14 +104,14 @@ For example, the model inference on image from WGISD dataset, which we used for To learn how to run the demo on Windows and MacOS, please refer to the ``outputs/deploy/python/README.md`` file in exportable code. -4. To save inferenced results with predictions on it, we can specify the folder path, using ``--save-results-to``. +4. To save inferenced results with predictions on it, we can specify the folder path, using ``--output``. It works for images, videos and image folders. To prevent issues, do not specify it together with a ``--loop`` parameter. .. code-block:: (demo) ...$ python outputs/deploy/python/demo.py --input docs/utils/images/wgisd_dataset_sample.jpg \ --models outputs/deploy/model \ - --save-results-to resulted_images + --output resulted_images 5. To run a demo on a web camera, we need to know its ID. We can check a list of camera devices by running this command line on Linux system: diff --git a/otx/api/usecases/exportable_code/demo/demo.py b/otx/api/usecases/exportable_code/demo/demo.py index ec6e6d97513..54fba44f634 100644 --- a/otx/api/usecases/exportable_code/demo/demo.py +++ b/otx/api/usecases/exportable_code/demo/demo.py @@ -75,7 +75,7 @@ def build_argparser(): type=str, ) args.add_argument( - "--save-results-to", + "--output", default=None, type=str, help="Optional. Output path to save input data with predictions.", @@ -103,8 +103,8 @@ def main(): """Main function that is used to run demo.""" args = build_argparser().parse_args() - if args.loop and args.save_results_to: - raise ValueError("--loop and --save-results-to cannot be both specified") + if args.loop and args.output: + raise ValueError("--loop and --output cannot be both specified") # create models models = [] @@ -115,7 +115,7 @@ def main(): inferencer = get_inferencer_class(args.inference_type, models) # create visualizer - visualizer = create_visualizer(models[-1].task_type, no_show=args.no_show, save_results_to=args.save_results_to) + visualizer = create_visualizer(models[-1].task_type, no_show=args.no_show, output=args.output) if len(models) == 1: models = models[0] diff --git a/otx/api/usecases/exportable_code/demo/demo_package/executors/asynchronous.py b/otx/api/usecases/exportable_code/demo/demo_package/executors/asynchronous.py index 3f93a11adca..852f7519b5f 100644 --- a/otx/api/usecases/exportable_code/demo/demo_package/executors/asynchronous.py +++ b/otx/api/usecases/exportable_code/demo/demo_package/executors/asynchronous.py @@ -16,6 +16,7 @@ ) from otx.api.usecases.exportable_code.streamer import get_streamer from otx.api.usecases.exportable_code.visualizers import Visualizer +from otx.cli.tools.utils.demo.visualization import dump_frames class AsyncExecutor: @@ -38,14 +39,16 @@ def run(self, input_stream: Union[int, str], loop: bool = False) -> None: next_frame_id = 0 next_frame_id_to_show = 0 stop_visualization = False + saved_frames = [] - for (frame, input_path) in streamer: + for frame in streamer: results = self.async_pipeline.get_result(next_frame_id_to_show) while results: output = self.render_result(results) next_frame_id_to_show += 1 self.visualizer.show(output) - self.visualizer.save_frame(output, input_path, str(streamer.get_type())) + if self.visualizer.output: + saved_frames.append(frame) if self.visualizer.is_quit(): stop_visualization = True results = self.async_pipeline.get_result(next_frame_id_to_show) @@ -58,7 +61,7 @@ def run(self, input_stream: Union[int, str], loop: bool = False) -> None: results = self.async_pipeline.get_result(next_frame_id_to_show) output = self.render_result(results) self.visualizer.show(output) - self.visualizer.dump_frames(streamer) + dump_frames(saved_frames, self.visualizer.output, input_stream, streamer) def render_result(self, results: Tuple[Any, dict]) -> np.ndarray: """Render for results of inference.""" diff --git a/otx/api/usecases/exportable_code/demo/demo_package/executors/sync_pipeline.py b/otx/api/usecases/exportable_code/demo/demo_package/executors/sync_pipeline.py index bf8f6527350..49f8cb8840b 100644 --- a/otx/api/usecases/exportable_code/demo/demo_package/executors/sync_pipeline.py +++ b/otx/api/usecases/exportable_code/demo/demo_package/executors/sync_pipeline.py @@ -22,6 +22,7 @@ from otx.api.usecases.exportable_code.streamer import get_streamer from otx.api.usecases.exportable_code.visualizers import Visualizer from otx.api.utils.shape_factory import ShapeFactory +from otx.cli.tools.utils.demo.visualization import dump_frames class ChainExecutor: @@ -78,14 +79,16 @@ def crop( def run(self, input_stream: Union[int, str], loop: bool = False) -> None: """Run demo using input stream (image, video stream, camera).""" streamer = get_streamer(input_stream, loop) + saved_frames = [] - for (frame, input_path) in streamer: + for frame in streamer: # getting result for single image annotation_scene = self.single_run(frame) output = self.visualizer.draw(frame, annotation_scene, {}) self.visualizer.show(output) - self.visualizer.save_frame(output, input_path, str(streamer.get_type())) + if self.visualizer.output: + saved_frames.append(frame) if self.visualizer.is_quit(): break - self.visualizer.dump_frames(streamer) + dump_frames(saved_frames, self.visualizer.output, input_stream, streamer) diff --git a/otx/api/usecases/exportable_code/demo/demo_package/executors/synchronous.py b/otx/api/usecases/exportable_code/demo/demo_package/executors/synchronous.py index a7a8864435b..74ab3387311 100644 --- a/otx/api/usecases/exportable_code/demo/demo_package/executors/synchronous.py +++ b/otx/api/usecases/exportable_code/demo/demo_package/executors/synchronous.py @@ -13,8 +13,7 @@ ) from otx.api.usecases.exportable_code.streamer import get_streamer from otx.api.usecases.exportable_code.visualizers import Visualizer - -from otx.cli.tools.demo import get_input_names_list, dump_frames +from otx.cli.tools.utils.demo.visualization import dump_frames class SyncExecutor: @@ -35,16 +34,15 @@ def run(self, input_stream: Union[int, str], loop: bool = False) -> None: streamer = get_streamer(input_stream, loop) saved_frames = [] - for (frame, input_path) in streamer: + for frame in streamer: # getting result include preprocessing, infer, postprocessing for sync infer predictions, frame_meta = self.model(frame) annotation_scene = self.converter.convert_to_annotation(predictions, frame_meta) output = self.visualizer.draw(frame, annotation_scene, frame_meta) self.visualizer.show(output) - if self.visualizer.save_results_to: + if self.visualizer.output: saved_frames.append(frame) - # self.visualizer.save_frame(output, input_path, str(streamer.get_type())) if self.visualizer.is_quit(): break - dump_frames(saved_frames, self.visualizer.save_results_to, input_stream, streamer) \ No newline at end of file + dump_frames(saved_frames, self.visualizer.output, input_stream, streamer) diff --git a/otx/api/usecases/exportable_code/demo/demo_package/utils.py b/otx/api/usecases/exportable_code/demo/demo_package/utils.py index 5a136dfd8fe..752f3552d9f 100644 --- a/otx/api/usecases/exportable_code/demo/demo_package/utils.py +++ b/otx/api/usecases/exportable_code/demo/demo_package/utils.py @@ -47,9 +47,9 @@ def create_output_converter(task_type: TaskType, labels: LabelSchemaEntity): return create_converter(converter_type, labels) -def create_visualizer(_task_type: TaskType, no_show: bool = False, save_results_to: str = None): +def create_visualizer(_task_type: TaskType, no_show: bool = False, output: Optional[str] = None): """Create visualizer according to kind of task.""" # TODO: use anomaly-specific visualizer for anomaly tasks - return Visualizer(window_name="Result", no_show=no_show, save_results_to=save_results_to) + return Visualizer(window_name="Result", no_show=no_show, output=output) diff --git a/otx/api/usecases/exportable_code/streamer/streamer.py b/otx/api/usecases/exportable_code/streamer/streamer.py index c4c4f12ea6b..641cec68c6e 100644 --- a/otx/api/usecases/exportable_code/streamer/streamer.py +++ b/otx/api/usecases/exportable_code/streamer/streamer.py @@ -143,7 +143,6 @@ class VideoStreamer(BaseStreamer): def __init__(self, input_path: str, loop: bool = False) -> None: self.media_type = MediaType.VIDEO - self.input_path = input_path self.loop = loop self.cap = cv2.VideoCapture() status = self.cap.open(input_path) @@ -158,7 +157,7 @@ def __iter__(self) -> Iterator[np.ndarray]: while True: status, image = self.cap.read() if status: - yield cv2.cvtColor(image, cv2.COLOR_BGR2RGB), self.input_path + yield cv2.cvtColor(image, cv2.COLOR_BGR2RGB) else: if self.loop: self.cap.set(cv2.CAP_PROP_POS_FRAMES, 0) @@ -209,7 +208,7 @@ def __iter__(self) -> Iterator[np.ndarray]: if not frame_available: break frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) - yield frame, None + yield frame self.stream.release() @@ -235,7 +234,6 @@ class ImageStreamer(BaseStreamer): def __init__(self, input_path: str, loop: bool = False) -> None: self.loop = loop self.media_type = MediaType.IMAGE - self.input_path = input_path if not os.path.isfile(input_path): raise InvalidInput(f"Can't find the image by {input_path}") self.image = cv2.imread(input_path, cv2.IMREAD_COLOR) @@ -246,10 +244,10 @@ def __init__(self, input_path: str, loop: bool = False) -> None: def __iter__(self) -> Iterator[np.ndarray]: """If loop is True, yield the image again and again.""" if not self.loop: - yield self.image, self.input_path + yield self.image else: while True: - yield self.image, None + yield self.image def get_type(self) -> MediaType: """Returns the type of the streamer.""" @@ -299,7 +297,7 @@ def __iter__(self) -> Iterator[np.ndarray]: else: self.file_id = self.file_id + 1 if not self.loop else 0 if image is not None: - yield cv2.cvtColor(image, cv2.COLOR_BGR2RGB), filename + yield cv2.cvtColor(image, cv2.COLOR_BGR2RGB) def get_type(self) -> MediaType: """Returns the type of the streamer.""" diff --git a/otx/api/usecases/exportable_code/visualizers/visualizer.py b/otx/api/usecases/exportable_code/visualizers/visualizer.py index e3fd3b2f7a7..218501fd03d 100644 --- a/otx/api/usecases/exportable_code/visualizers/visualizer.py +++ b/otx/api/usecases/exportable_code/visualizers/visualizer.py @@ -60,15 +60,14 @@ class Visualizer(IVisualizer): >>> visualizer.show(output) """ - def __init__( # pylint: disable=too-many-arguments + def __init__( self, window_name: Optional[str] = None, show_count: bool = False, is_one_label: bool = False, no_show: bool = False, delay: Optional[int] = None, - save_results_to: str = None, - saved_frames: Dict[str, list] = None, + output: Optional[str] = None, ) -> None: self.window_name = "Window" if window_name is None else window_name self.shape_drawer = ShapeDrawer(show_count, is_one_label) @@ -77,11 +76,7 @@ def __init__( # pylint: disable=too-many-arguments self.no_show = no_show if delay is None: self.delay = 1 - self.save_results_to = save_results_to - if not saved_frames: - self.saved_frames = {} - else: - self.saved_frames = saved_frames + self.output = output def draw( self, @@ -119,48 +114,3 @@ def is_quit(self) -> bool: return False return ord("q") == cv2.waitKey(self.delay) - - def save_frame(self, image, input_path, streamer_type) -> None: - """Save result image into dict. - - Args: - image (np.ndarray): Image to be saved. - input_path (str): Filename of the image - streamer_type (str) : The type of the input - """ - - if self.save_results_to and input_path: - filename = Path(input_path).name - if filename not in self.saved_frames: - self.saved_frames[filename] = [] - if "VIDEO" in streamer_type: - self.saved_frames[filename].append(image) - else: - self.saved_frames[filename] = image - - def dump_frames(self, streamer) -> None: - """Save frames to file system. - - Args: - streamer (str): The streamer with images to be saved - """ - if len(self.saved_frames) > 0 and self.save_results_to: - output_path = Path(self.save_results_to) - if not output_path.exists(): - output_path.mkdir(parents=True) - - if "VIDEO" in str(streamer.get_type()): - filename, frames = list(self.saved_frames.items())[0] - w, h, _ = frames[0].shape - video_path = str(output_path / filename) - codec = cv2.VideoWriter_fourcc(*"mp4v") - out = cv2.VideoWriter(video_path, codec, streamer.fps(), (h, w)) - for frame in frames: - out.write(frame) - out.release() - print(f"Video was saved to {video_path}") - else: - for filename, frame in self.saved_frames.items(): - image_path = str(output_path / filename) - cv2.imwrite(image_path, frame) - print(f"Image was saved to {image_path}") diff --git a/otx/cli/tools/demo.py b/otx/cli/tools/demo.py index 014e7124f1b..c1cdc59fb4d 100644 --- a/otx/cli/tools/demo.py +++ b/otx/cli/tools/demo.py @@ -15,8 +15,7 @@ # and limitations under the License. import time -from collections import defaultdict, deque -from pathlib import Path +from collections import deque import cv2 import numpy as np @@ -28,7 +27,7 @@ from otx.api.entities.task_environment import TaskEnvironment from otx.cli.manager import ConfigManager from otx.cli.tools.utils.demo.images_capture import open_images_capture -from otx.cli.tools.utils.demo.visualization import draw_predictions, put_text_on_rect_bg +from otx.cli.tools.utils.demo.visualization import draw_predictions, dump_frames, put_text_on_rect_bg from otx.cli.utils.importing import get_impl_class from otx.cli.utils.io import read_label_schema, read_model from otx.cli.utils.parser import ( @@ -73,7 +72,7 @@ def get_args(): "frame reading, pre-processing and post-processing.", ) parser.add_argument( - "--save-results-to", + "--output", default=None, type=str, help="Output path to save input data with predictions.", @@ -107,52 +106,14 @@ def get_predictions(task, frame): return item.get_annotations(), elapsed_time -def get_input_names_list(input_path, capture): - """Lists the filenames of all inputs for demo.""" - - if "DIR" in capture.get_type(): - return [f.name for f in Path(input_path).iterdir() if f.is_file()] - else: - return [Path(input_path).name] - - -def dump_frames(saved_frames, output_path, input_path, capture): - """Saves images/videos with predictions from saved_frames to output folder with proper names.""" - - if not saved_frames: - return - - output_path = Path(output_path) - if not output_path.exists(): - output_path.mkdir(parents=True) - - filenames = get_input_names_list(input_path, capture) - - if "VIDEO" in capture.get_type(): - filename = filenames[0] - w, h, _ = saved_frames[0].shape - video_path = str(output_path / filename) - codec = cv2.VideoWriter_fourcc(*"mp4v") - out = cv2.VideoWriter(video_path, codec, capture.fps(), (h, w)) - for frame in saved_frames: - out.write(frame) - out.release() - print(f"Video was saved to {video_path}") - else: - for filename, frame in zip(filenames, saved_frames): - image_path = str(output_path / filename) - cv2.imwrite(image_path, frame) - print(f"Image was saved to {image_path}") - - def main(): """Main function that is used for model demonstration.""" # Dynamically create an argument parser based on override parameters. args, override_param = get_args() - if args.loop and args.save_results_to: - raise ValueError("--loop and --save-results-to cannot be both specified") + if args.loop and args.output: + raise ValueError("--loop and --output cannot be both specified") config_manager = ConfigManager(args, mode="demo") # Auto-Configuration for model template @@ -210,10 +171,10 @@ def main(): else: print(f"Frame: {elapsed_time=}, {len(predictions)=}") - if args.save_results_to: + if args.output: saved_frames.append(frame) - dump_frames(saved_frames, args.save_results_to, args.input, capture) + dump_frames(saved_frames, args.output, args.input, capture) return dict(retcode=0, template=template.name) diff --git a/otx/cli/tools/utils/demo/visualization.py b/otx/cli/tools/utils/demo/visualization.py index 09edd4abfe2..4392fc7de04 100644 --- a/otx/cli/tools/utils/demo/visualization.py +++ b/otx/cli/tools/utils/demo/visualization.py @@ -15,18 +15,22 @@ # and limitations under the License. -from typing import List, Tuple +from pathlib import Path +from typing import List, Tuple, Union from warnings import warn import cv2 import numpy as np from cv2 import Mat +from otx.algorithms.common.utils.logger import get_logger from otx.api.entities.annotation import Annotation from otx.api.entities.model_template import TaskType from otx.api.entities.shapes.polygon import Polygon from otx.api.entities.shapes.rectangle import Rectangle +logger = get_logger() + def put_text_on_rect_bg(frame: Mat, message: str, position: Tuple[int, int], color=(255, 255, 0)): """Puts a text message on a black rectangular aread in specified position of a frame.""" @@ -161,3 +165,46 @@ def draw_predictions(task_type: TaskType, predictions: List[Annotation], frame: else: raise ValueError(f"Unknown task type: {task_type}") return frame + + +def get_input_names_list(input_path: Union[str, int], capture): + """Lists the filenames of all inputs for demo.""" + + # Web camera input + if isinstance(input_path, int): + return [] + if "DIR" in capture.get_type(): + return [f.name for f in Path(input_path).iterdir() if f.is_file()] + else: + return [Path(input_path).name] + + +def dump_frames(saved_frames: list, output: str, input_path: Union[str, int], capture): + """Saves images/videos with predictions from saved_frames to output folder with proper names.""" + + if not saved_frames: + return + + output_path = Path(output) + if not output_path.exists(): + output_path.mkdir(parents=True) + + filenames = get_input_names_list(input_path, capture) + + if "VIDEO" in capture.get_type(): + filename = filenames[0] + w, h, _ = saved_frames[0].shape + video_path = str(output_path / filename) + codec = cv2.VideoWriter_fourcc(*"mp4v") + out = cv2.VideoWriter(video_path, codec, capture.fps(), (h, w)) + for frame in saved_frames: + out.write(frame) + out.release() + logger.info(f"Video was saved to {video_path}") + else: + if len(filenames) < len(saved_frames): + filenames = [f"output_{i}" for i, _ in enumerate(saved_frames)] + for filename, frame in zip(filenames, saved_frames): + image_path = str(output_path / filename) + cv2.imwrite(image_path, frame) + logger.info(f"Image was saved to {image_path}") From 76bdc025fc525af9eb8889ebdb234688735bcd89 Mon Sep 17 00:00:00 2001 From: Galina Date: Fri, 14 Apr 2023 01:12:48 +0300 Subject: [PATCH 08/10] Remove unused imports --- otx/api/usecases/exportable_code/visualizers/visualizer.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/otx/api/usecases/exportable_code/visualizers/visualizer.py b/otx/api/usecases/exportable_code/visualizers/visualizer.py index 218501fd03d..ed79a4a0ca8 100644 --- a/otx/api/usecases/exportable_code/visualizers/visualizer.py +++ b/otx/api/usecases/exportable_code/visualizers/visualizer.py @@ -5,8 +5,7 @@ # import abc -from pathlib import Path -from typing import Dict, Optional +from typing import Optional import cv2 import numpy as np From b7149d2780469c4bae129c6f7228fdfbe616ddc7 Mon Sep 17 00:00:00 2001 From: Galina Date: Fri, 14 Apr 2023 01:52:39 +0300 Subject: [PATCH 09/10] Update exportable code demo --- docs/source/guide/tutorials/base/demo.rst | 4 +- docs/source/guide/tutorials/base/deploy.rst | 4 +- .../usecases/exportable_code/demo/README.md | 107 +++++++++++------- 3 files changed, 73 insertions(+), 42 deletions(-) diff --git a/docs/source/guide/tutorials/base/demo.rst b/docs/source/guide/tutorials/base/demo.rst index 1527c1e50eb..ad700d52fae 100644 --- a/docs/source/guide/tutorials/base/demo.rst +++ b/docs/source/guide/tutorials/base/demo.rst @@ -34,7 +34,7 @@ But if we'll provide a single image the demo processes and renders it quickly, t (demo) ...$ otx demo --input docs/utils/images/wgisd_dataset_sample.jpg \ --load-weights outputs/weights.pth --loop -In this case, you can stop the demo by pressing `Esc` button or killing the process in the terminal (``Ctrl+C`` for Linux). +In this case, you can stop the demo by pressing `Q` button or killing the process in the terminal (``Ctrl+C`` for Linux). 3. If we want to pass an images folder, it's better to specify the delay parameter, that defines, how much millisecond pause will be held between showing the next image. For example ``--delay 100`` will make this pause 0.1 ms. @@ -51,7 +51,7 @@ so the ``--fit-to-size`` parameter would be quite useful. It resizes the resulti 5. To save inferenced results with predictions on it, we can specify the folder path, using ``--output``. -It works for images, videos and image folders. To prevent issues, do not specify it together with a ``--loop`` parameter. +It works for images, videos, image folders and web cameras. To prevent issues, do not specify it together with a ``--loop`` parameter. .. code-block:: diff --git a/docs/source/guide/tutorials/base/deploy.rst b/docs/source/guide/tutorials/base/deploy.rst index 2e029f025fe..9a4d4ddc5ed 100644 --- a/docs/source/guide/tutorials/base/deploy.rst +++ b/docs/source/guide/tutorials/base/deploy.rst @@ -100,12 +100,12 @@ For example, the model inference on image from WGISD dataset, which we used for If you provide a single image as input, the demo processes and renders it quickly, then exits. To continuously visualize inference results on the screen, apply the ``loop`` option, which enforces processing a single image in a loop. - In this case, you can stop the demo by pressing `Esc` button or killing the process in the terminal (``Ctrl+C`` for Linux). + In this case, you can stop the demo by pressing `Q` button or killing the process in the terminal (``Ctrl+C`` for Linux). To learn how to run the demo on Windows and MacOS, please refer to the ``outputs/deploy/python/README.md`` file in exportable code. 4. To save inferenced results with predictions on it, we can specify the folder path, using ``--output``. -It works for images, videos and image folders. To prevent issues, do not specify it together with a ``--loop`` parameter. +It works for images, videos, image folders and web cameras. To prevent issues, do not specify it together with a ``--loop`` parameter. .. code-block:: diff --git a/otx/api/usecases/exportable_code/demo/README.md b/otx/api/usecases/exportable_code/demo/README.md index 7de5c35382e..eeb8af10826 100644 --- a/otx/api/usecases/exportable_code/demo/README.md +++ b/otx/api/usecases/exportable_code/demo/README.md @@ -85,44 +85,75 @@ Exportable code is a .zip archive that contains simple demo to get and visualize ## Usecase -Running the `demo.py` application with the `-h` option yields the following usage message: - -```bash -usage: demo.py [-h] -i INPUT -m MODELS [MODELS ...] [-it {sync,async}] [-l] -Options: - -h, --help Show this help message and exit. - -i INPUT, --input INPUT - Required. An input to process. The input must be a - single image, a folder of images, video file or camera - id. - -m MODELS [MODELS ...], --models MODELS [MODELS ...] - Required. Path to directory with trained model and - configuration file. If you provide several models you - will start the task chain pipeline with the provided - models in the order in which they were specified - -it {sync,async}, --inference_type {sync,async} - Optional. Type of inference for single model - -l, --loop Optional. Enable reading the input in a loop. - --no_show - Optional. If this flag is specified, the demo - won't show the inference results on UI. -``` - -As a model, you can use path to model directory from generated zip. So you can use the following command to do inference with a pre-trained model: - -```bash -python3 demo.py \ - -i /inputVideo.mp4 \ - -m -``` - -You can press `Q` to stop inference during demo running. - -> **NOTE**: If you provide a single image as an input, the demo processes and renders it quickly, then exits. To continuously -> visualize inference results on the screen, apply the `loop` option, which enforces processing a single image in a loop. -> -> **NOTE**: Default configuration contains info about pre- and post processing for inference and is guaranteed to be correct. -> Also you can change `config.json` that specifies needed parameters, but any changes should be made with caution. +1. Running the `demo.py` application with the `-h` option yields the following usage message: + + ```bash + usage: demo.py [-h] -i INPUT -m MODELS [MODELS ...] [-it {sync,async}] [-l] [--no_show] [-d {CPU,GPU}] [--output OUTPUT] + + Options: + -h, --help Show this help message and exit. + -i INPUT, --input INPUT + Required. An input to process. The input must be a single image, a folder of images, video file or camera id. + -m MODELS [MODELS ...], --models MODELS [MODELS ...] + Required. Path to directory with trained model and configuration file. If you provide several models you will start the task chain pipeline with the provided models in the order in + which they were specified. + -it {sync,async}, --inference_type {sync,async} + Optional. Type of inference for single model. + -l, --loop Optional. Enable reading the input in a loop. + --no_show Optional. Disables showing inference results on UI. + -d {CPU,GPU}, --device {CPU,GPU} + Optional. Device to infer the model. + --output OUTPUT Optional. Output path to save input data with predictions. + ``` + +2. As a `model`, you can use path to model directory from generated zip. You can pass as `input` a single image, a folder of images, a video file, or a web camera id. So you can use the following command to do inference with a pre-trained model: + + ```bash + python3 demo.py \ + -i /inputVideo.mp4 \ + -m + ``` + + You can press `Q` to stop inference during demo running. + + > **NOTE**: If you provide a single image as input, the demo processes and renders it quickly, then exits. To continuously + > visualize inference results on the screen, apply the `--loop` option, which enforces processing a single image in a loop. + > In this case, you can stop the demo by pressing `Q` button or killing the process in the terminal (`Ctrl+C` for Linux). + > + > **NOTE**: Default configuration contains info about pre- and post processing for inference and is guaranteed to be correct. + > Also you can change `config.json` that specifies the confidence threshold and color for each class visualization, but any + > changes should be made with caution. + + +3. To save inferenced results with predictions on it, you can specify the folder path, using `--output`. +It works for images, videos, image folders and web cameras. To prevent issues, do not specify it together with a `--loop` parameter. + + ```bash + python3 demo.py \ + --input /inputImage.jpg \ + --models ../model \ + --output resulted_images + ``` + +4. To run a demo on a web camera, you need to know its ID. +You can check a list of camera devices by running this command line on Linux system: + + ```bash + sudo apt-get install v4l-utils + v4l2-ctl --list-devices + ``` + + The output will look like this: + + ```bash + Integrated Camera (usb-0000:00:1a.0-1.6): + /dev/video0 + ``` + + After that, you can use this `/dev/video0` as a camera ID for `--input`. + + + ## Troubleshooting From 91f9ae63e6914d6e73b2b6296b2ae95f071c2506 Mon Sep 17 00:00:00 2001 From: Galina Date: Fri, 14 Apr 2023 06:47:39 +0300 Subject: [PATCH 10/10] Fix pylint --- otx/api/usecases/exportable_code/demo/README.md | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/otx/api/usecases/exportable_code/demo/README.md b/otx/api/usecases/exportable_code/demo/README.md index eeb8af10826..f4f716f3a7d 100644 --- a/otx/api/usecases/exportable_code/demo/README.md +++ b/otx/api/usecases/exportable_code/demo/README.md @@ -124,9 +124,8 @@ Exportable code is a .zip archive that contains simple demo to get and visualize > Also you can change `config.json` that specifies the confidence threshold and color for each class visualization, but any > changes should be made with caution. - -3. To save inferenced results with predictions on it, you can specify the folder path, using `--output`. -It works for images, videos, image folders and web cameras. To prevent issues, do not specify it together with a `--loop` parameter. +3. To save inferenced results with predictions on it, you can specify the folder path, using `--output`. + It works for images, videos, image folders and web cameras. To prevent issues, do not specify it together with a `--loop` parameter. ```bash python3 demo.py \ @@ -135,8 +134,8 @@ It works for images, videos, image folders and web cameras. To prevent issues, d --output resulted_images ``` -4. To run a demo on a web camera, you need to know its ID. -You can check a list of camera devices by running this command line on Linux system: +4. To run a demo on a web camera, you need to know its ID. + You can check a list of camera devices by running this command line on Linux system: ```bash sudo apt-get install v4l-utils @@ -152,9 +151,6 @@ You can check a list of camera devices by running this command line on Linux sys After that, you can use this `/dev/video0` as a camera ID for `--input`. - - - ## Troubleshooting 1. If you have access to the Internet through the proxy server only, please use pip with proxy call as demonstrated by command below: