From a8a4862075cc57dc891ef40e6d46f9fbac29a267 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 15 Apr 2024 21:37:08 +0000 Subject: [PATCH 01/10] Bump scikit-learn from 1.4.1.post1 to 1.4.2 Bumps [scikit-learn](https://github.com/scikit-learn/scikit-learn) from 1.4.1.post1 to 1.4.2. - [Release notes](https://github.com/scikit-learn/scikit-learn/releases) - [Commits](https://github.com/scikit-learn/scikit-learn/compare/1.4.1.post1...1.4.2) --- updated-dependencies: - dependency-name: scikit-learn dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 64938665..a0cdd4a1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,7 +30,7 @@ dependencies = [ "nrtk", "numpy", "Pillow", - "scikit-learn==1.4.1.post1", + "scikit-learn==1.4.2", "smqtk-classifier==0.19.0", "accelerate", "smqtk-core==0.19.0", From 87a292df21262e9f1f723a5a9be82d3fa3f89acc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 25 Mar 2024 21:17:22 +0000 Subject: [PATCH 02/10] Bump ubelt from 1.3.4 to 1.3.5 Bumps [ubelt](https://github.com/Erotemic/ubelt) from 1.3.4 to 1.3.5. - [Release notes](https://github.com/Erotemic/ubelt/releases) - [Changelog](https://github.com/Erotemic/ubelt/blob/main/CHANGELOG.md) - [Commits](https://github.com/Erotemic/ubelt/compare/v1.3.4...v1.3.5) --- updated-dependencies: - dependency-name: ubelt dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index a0cdd4a1..d02882c4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -47,7 +47,7 @@ dependencies = [ "trame-client>=2.15.0", "trame-quasar", "trame-server>=2.15.0", - "ubelt==1.3.4", + "ubelt==1.3.5", "umap-learn", "tabulate", ] From 0131172a2480a3e75bdb353741561e31d1625bcd Mon Sep 17 00:00:00 2001 From: Vicente Adolfo Bolea Sanchez Date: Sun, 21 Apr 2024 19:46:39 -0400 Subject: [PATCH 03/10] fix(nrtk-explorer): multi platform paths --- tests/test_embeddings.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/test_embeddings.py b/tests/test_embeddings.py index fe193dc5..1abd57c4 100644 --- a/tests/test_embeddings.py +++ b/tests/test_embeddings.py @@ -5,6 +5,7 @@ from tabulate import tabulate from itertools import product +from pathlib import Path import json import os @@ -12,17 +13,17 @@ import timeit CURRENT_DIR_NAME = os.path.dirname(nrtk_explorer.test_data.__file__) -DATASET = f"{CURRENT_DIR_NAME}/coco-od-2017/test_val2017.json" +inc_ds_path = Path(f"{CURRENT_DIR_NAME}/coco-od-2017/test_val2017.json") def image_paths_impl(): - with open(DATASET) as f: + with open(inc_ds_path) as f: dataset = json.load(f) images = dataset["images"] paths = list() for image_metadata in images: - paths.append(os.path.join(os.path.dirname(DATASET), image_metadata["file_name"])) + paths.append(os.path.join(os.path.dirname(inc_ds_path), image_metadata["file_name"])) return paths From 34b2fdbeff7b724c542ac053d4d19fca6114939c Mon Sep 17 00:00:00 2001 From: Vicente Adolfo Bolea Sanchez Date: Sun, 21 Apr 2024 19:31:08 -0400 Subject: [PATCH 04/10] fix(benchmarks): allow using a external COCO ds --- pyproject.toml | 2 +- tests/conftest.py | 21 ++++++++++------- tests/test_embeddings.py | 51 ++++++++++++++++++++++------------------ 3 files changed, 41 insertions(+), 33 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index d02882c4..7f7040a7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -128,7 +128,7 @@ build_command = """ python -m venv .venv source .venv/bin/activate pip install -U pip - python -m pip install "build<0.10.0" "python-semantic-release" "setuptools" "wheel" + python -m pip install build<0.10.0 python-semantic-release setuptools wheel python -m build . """ diff --git a/tests/conftest.py b/tests/conftest.py index d660338c..5d6b6975 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -4,21 +4,24 @@ def pytest_addoption(parser): - parser.addoption("--runslow", action="store_true", default=False, help="run slow tests") + parser.addoption( + "--benchmark-dataset-file", default=None, help="COCO JSON path for benchmarks" + ) def pytest_configure(config): - config.addinivalue_line("markers", "slow: mark test as slow to run") + config.addinivalue_line("markers", "benchmark: mark test as benchmarks") def pytest_collection_modifyitems(config, items): - if config.getoption("--runslow"): + json_file = config.getoption("--benchmark-dataset-file") + if json_file is not None: # list test durations config.option.verbose = 1 config.option.durations = 0 - # --runslow given in cli: do not skip slow tests - return - skip_slow = pytest.mark.skip(reason="need --runslow option to run") - for item in items: - if "slow" in item.keywords: - item.add_marker(skip_slow) + + else: + do_skip = pytest.mark.skip(reason="need --benchmark-dataset-file opt set to run") + for item in items: + if "benchmark" in item.keywords: + item.add_marker(do_skip) diff --git a/tests/test_embeddings.py b/tests/test_embeddings.py index 1abd57c4..8e1d2d01 100644 --- a/tests/test_embeddings.py +++ b/tests/test_embeddings.py @@ -16,20 +16,26 @@ inc_ds_path = Path(f"{CURRENT_DIR_NAME}/coco-od-2017/test_val2017.json") -def image_paths_impl(): - with open(inc_ds_path) as f: +def image_paths_impl(file_name): + with open(file_name) as f: dataset = json.load(f) images = dataset["images"] paths = list() for image_metadata in images: - paths.append(os.path.join(os.path.dirname(inc_ds_path), image_metadata["file_name"])) + paths.append(os.path.join(os.path.dirname(file_name), image_metadata["file_name"])) + return paths @pytest.fixture -def image_paths(): - return image_paths_impl() +def image_paths(request): + return image_paths_impl(inc_ds_path) + + +@pytest.fixture +def image_paths_external(request): + return image_paths_impl(request.config.getoption("--benchmark-dataset-file")) def test_features_small(image_paths): @@ -46,11 +52,11 @@ def test_features_zero(image_paths): print(features) -@pytest.mark.slow -def test_features_all(image_paths): +@pytest.mark.benchmark +def test_features_all(image_paths_external): extractor = embeddings_extractor.EmbeddingsExtractor() - features = extractor.extract(image_paths) - assert len(features) == len(image_paths) + features = extractor.extract(image_paths_external) + assert len(features) == len(image_paths_external) print(f"Number of features: {len(features)}") @@ -106,7 +112,6 @@ def test_reducer_manager(image_paths): assert len(old_points) > 0 assert len(old_points[0]) == 3 - # breakpoint() new_points = mgr.reduce(fit_features=features, features=features, name="PCA", dims=3) assert id(old_points) == id(new_points) @@ -114,8 +119,8 @@ def test_reducer_manager(image_paths): assert id(old_points) != id(new_points_2d) -@pytest.mark.slow -def test_features_extractor_benchmark(image_paths): +@pytest.mark.benchmark +def test_features_extractor_benchmark(image_paths_external): repetitions = 3 sampling = [10, 100] batch_size = [1, 8, 16, 32] @@ -126,13 +131,13 @@ def test_features_extractor_benchmark(image_paths): # Pre-load images manager = images_manager.ImagesManager() - for path in image_paths[: max(sampling)]: + for path in image_paths_external[: max(sampling)]: manager.load_image_for_model(path) for n, batch_size in setups: extractor = embeddings_extractor.EmbeddingsExtractor(manager=manager) output = timeit.repeat( - stmt=lambda: extractor.extract(image_paths[:n], batch_size=batch_size), + stmt=lambda: extractor.extract(image_paths_external[:n], batch_size=batch_size), number=repetitions, repeat=5, ) @@ -141,8 +146,8 @@ def test_features_extractor_benchmark(image_paths): print(tabulate(table, headers=["#Samples", "batch_size", "ExecTime(sec)"], tablefmt="github")) -@pytest.mark.slow -def test_reducer_manager_benchmark(image_paths): +@pytest.mark.benchmark +def test_reducer_manager_benchmark(image_paths_external): setups = [ ("PCA", 10, True, 100), ("PCA", 10, False, 100), @@ -156,7 +161,7 @@ def test_reducer_manager_benchmark(image_paths): mgr = dimension_reducers.DimReducerManager() extractor = embeddings_extractor.EmbeddingsExtractor() - features = extractor.extract(image_paths) + features = extractor.extract(image_paths_external) # Short benchmarks cached for name, n, cache, iterations in setups: @@ -172,10 +177,10 @@ def test_reducer_manager_benchmark(image_paths): ) -@pytest.mark.slow -def test_pca_3d_large(image_paths): +@pytest.mark.benchmark +def test_pca_3d_large(image_paths_external): extractor = embeddings_extractor.EmbeddingsExtractor() - features = extractor.extract(image_paths) + features = extractor.extract(image_paths_external) model = dimension_reducers.PCAReducer(3) points = model.reduce(features) assert len(points) > 0 @@ -184,10 +189,10 @@ def test_pca_3d_large(image_paths): print(points) -@pytest.mark.slow -def test_umap_3d_large(image_paths): +@pytest.mark.benchmark +def test_umap_3d_large(image_paths_external): extractor = embeddings_extractor.EmbeddingsExtractor() - features = extractor.extract(image_paths) + features = extractor.extract(image_paths_external) model = dimension_reducers.UMAPReducer(3) points = model.reduce(features) assert len(points) > 0 From fcc8f0a278acf9cd8837185b8f046c75c315246a Mon Sep 17 00:00:00 2001 From: Paul Elliott Date: Mon, 29 Apr 2024 20:24:27 -0400 Subject: [PATCH 05/10] fix(object_detector): maintain input paths order of output predictions Caused annotations to be overlaid on the wrong images in image list view. --- src/nrtk_explorer/library/object_detector.py | 22 +++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/src/nrtk_explorer/library/object_detector.py b/src/nrtk_explorer/library/object_detector.py index 97a807ae..203bf07e 100644 --- a/src/nrtk_explorer/library/object_detector.py +++ b/src/nrtk_explorer/library/object_detector.py @@ -1,9 +1,7 @@ import logging -import operator import torch import transformers -from functools import reduce from typing import Optional from nrtk_explorer.library import images_manager @@ -62,17 +60,25 @@ def eval( images: dict = {} - # Group images by size (shape) + # Some models require all the images in a batch to be the same size, + # otherwise crash or UB. for path in paths: img = None if content and path in content: img = content[path] else: img = self.manager.load_image(path) + images.setdefault(img.size, {})[path] = img - images.setdefault(img.size, []).append(img) + def run_group(group): + imgs = list(group.values()) + predictions = self.pipeline(imgs, batch_size=batch_size) + # { path -> prediction } + paths = group.keys() + return {path: pred for path, pred in zip(paths, predictions)} - # Call by each group - predictions = [self.pipeline(group, batch_size=batch_size) for group in images.values()] - # Flatten the list of predictions - return reduce(operator.iadd, predictions) + predictions = [run_group(group) for group in images.values()] + + # match order of input paths arg + pathsToPrediction = {path: pred for group in predictions for path, pred in group.items()} + return [pathsToPrediction[path] for path in paths] From f12cd7b2432b2b7fa957602e8dc34196bb183643 Mon Sep 17 00:00:00 2001 From: Paul Elliott Date: Mon, 29 Apr 2024 20:35:44 -0400 Subject: [PATCH 06/10] fix(ImageDetection): fallback to Unknown name when uncatagorized Horse drawn trolly thing has category ID of 0. --- vue-components/src/components/ImageDetection.vue | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/vue-components/src/components/ImageDetection.vue b/vue-components/src/components/ImageDetection.vue index d609e9f6..ea8e565b 100644 --- a/vue-components/src/components/ImageDetection.vue +++ b/vue-components/src/components/ImageDetection.vue @@ -230,8 +230,8 @@ function mouseMove(e: MouseEvent) { const item = document.createElement('li') if (hit.data != undefined) { const annotation = props.annotations[hit.data] - const category = props.categories[annotation.category_id] - item.textContent = `(${annotation.id}): ${category.name}` + const { name } = props.categories[annotation.category_id] ?? { name: 'Unknown' } + item.textContent = `(${annotation.id}): ${name}` const color = CATEGORY_COLORS[annotation.category_id % CATEGORY_COLORS.length] item.style.textShadow = `rgba(${color.join(',')},0.6) 1px 1px 3px` list.appendChild(item) From 633e5f46b22eaf594f3eab657b124f75386fff45 Mon Sep 17 00:00:00 2001 From: Vicente Adolfo Bolea Sanchez Date: Tue, 30 Apr 2024 16:46:07 -0400 Subject: [PATCH 07/10] fix: change object_detector output --- src/nrtk_explorer/app/transforms.py | 4 +-- src/nrtk_explorer/library/object_detector.py | 30 ++++++++++---------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/src/nrtk_explorer/app/transforms.py b/src/nrtk_explorer/app/transforms.py index 9b3f711d..14e1347b 100644 --- a/src/nrtk_explorer/app/transforms.py +++ b/src/nrtk_explorer/app/transforms.py @@ -157,9 +157,9 @@ def compute_annotations(self, ids): for id_ in ids: self.context["annotations"][id_] = [] - prediction = self.detector.eval(paths=ids, content=self.context.image_objects) + predictions = self.detector.eval(paths=ids, content=self.context.image_objects) - for id_, annotations in zip(ids, prediction): + for id_, annotations in predictions: image_annotations = self.context["annotations"].setdefault(id_, []) for prediction in annotations: category_id = 0 diff --git a/src/nrtk_explorer/library/object_detector.py b/src/nrtk_explorer/library/object_detector.py index 203bf07e..07060ab5 100644 --- a/src/nrtk_explorer/library/object_detector.py +++ b/src/nrtk_explorer/library/object_detector.py @@ -1,12 +1,14 @@ import logging +import operator import torch import transformers +from functools import reduce from typing import Optional from nrtk_explorer.library import images_manager -Annotations = list[list[dict]] +Annotations = list[tuple[str, dict]] class ObjectDetector: @@ -54,7 +56,7 @@ def eval( content: Optional[dict] = None, batch_size: int = 32, ) -> Annotations: - """Compute object recognition, return it in a dictionary of COCO format""" + """Compute object recognition, return it in a list of tuples in the form of [(path, annotations dict in COCO Format)]""" if len(paths) == 0: return [] @@ -68,17 +70,15 @@ def eval( img = content[path] else: img = self.manager.load_image(path) - images.setdefault(img.size, {})[path] = img - def run_group(group): - imgs = list(group.values()) - predictions = self.pipeline(imgs, batch_size=batch_size) - # { path -> prediction } - paths = group.keys() - return {path: pred for path, pred in zip(paths, predictions)} - - predictions = [run_group(group) for group in images.values()] - - # match order of input paths arg - pathsToPrediction = {path: pred for group in predictions for path, pred in group.items()} - return [pathsToPrediction[path] for path in paths] + images.setdefault(img.size, [[], []]) + images[img.size][0].append(path) + images[img.size][1].append(img) + + # Call by each group + predictions = [ + list(zip(group[0], self.pipeline(group[1], batch_size=batch_size))) + for group in images.values() + ] + # Flatten the list of predictions + return reduce(operator.iadd, predictions) From 42ae90d33325b2251a8d7585776ee6ebc129c6db Mon Sep 17 00:00:00 2001 From: Paul Elliott Date: Wed, 8 May 2024 15:39:44 -0400 Subject: [PATCH 08/10] feat: support hot-reload for ui If starting app with `nrtk_explorer --hot-reload`, then a button appears in the upper right to reload the ui modules and re-run Engine.ui method. --- src/nrtk_explorer/app/core.py | 170 ++----------------- src/nrtk_explorer/app/transforms.py | 6 +- src/nrtk_explorer/app/ui/__init__.py | 20 +++ src/nrtk_explorer/app/ui/collapsible_card.py | 2 +- src/nrtk_explorer/app/ui/layout.py | 155 +++++++++++++++++ 5 files changed, 194 insertions(+), 159 deletions(-) create mode 100644 src/nrtk_explorer/app/ui/layout.py diff --git a/src/nrtk_explorer/app/core.py b/src/nrtk_explorer/app/core.py index 2082f916..d90e5347 100644 --- a/src/nrtk_explorer/app/core.py +++ b/src/nrtk_explorer/app/core.py @@ -1,12 +1,6 @@ -r""" -Define your classes and create the instances that you need to expose -""" - import logging from typing import Iterable -from trame.ui.quasar import QLayout -from trame.widgets import quasar from trame.widgets import html from trame_server.utils.namespace import Translator from nrtk_explorer.library import images_manager @@ -16,7 +10,7 @@ from nrtk_explorer.app.transforms import TransformsApp from nrtk_explorer.app.filtering import FilteringApp from nrtk_explorer.app.applet import Applet -from nrtk_explorer.app.ui.collapsible_card import collapsible_card +from nrtk_explorer.app import ui import nrtk_explorer.test_data from pathlib import Path @@ -49,10 +43,6 @@ def image_id_to_result(image_id): return f"{image_id}_result" -def parse_dataset_dirs(datasets): - return [{"label": Path(ds).name, "value": ds} for ds in datasets] - - # --------------------------------------------------------- # Engine class # --------------------------------------------------------- @@ -213,147 +203,17 @@ def reload_images(self): self.state.annotation_categories = categories self.state.images_ids = [img["id"] for img in selected_images] - def ui(self, *args, **kwargs): - if self._ui is None: - with QLayout( - self.server, view="lhh LpR lff", classes="shadow-2 rounded-borders bg-grey-2" - ) as layout: - # # Toolbar - with quasar.QHeader(): - with quasar.QToolbar(classes="shadow-4"): - quasar.QToolbarTitle("NRTK_EXPLORER") - - # # Main content - with quasar.QPageContainer(): - with quasar.QPage(): - with quasar.QSplitter( - model_value=("horizontal_split",), - classes="inherit-height", - before_class="inherit-height zero-height scroll", - after_class="inherit-height zero-height", - ): - with html.Template(v_slot_before=True): - with html.Div(classes="q-pa-md q-gutter-md"): - ( - dataset_title_slot, - dataset_content_slot, - dataset_actions_slot, - ) = collapsible_card("collapse_dataset") - - with dataset_title_slot: - html.Span("Dataset Selection", classes="text-h6") - - with dataset_content_slot: - quasar.QSelect( - label="Dataset", - v_model=("current_dataset",), - options=(parse_dataset_dirs(self.input_paths),), - filled=True, - emit_value=True, - map_options=True, - dense=True, - ) - quasar.QSlider( - v_model=("num_images", 15), - min=(0,), - max=("num_images_max", 25), - disable=("num_images_disabled", True), - step=(1,), - ) - html.P( - "{{num_images}}/{{num_images_max}} images", - classes="text-caption text-center", - ) - - quasar.QToggle( - v_model=("random_sampling", False), - dense=False, - label="Random selection", - ) - - ( - embeddings_title_slot, - embeddings_content_slot, - embeddings_actions_slot, - ) = collapsible_card("collapse_embeddings") - - with embeddings_title_slot: - html.Span("Embeddings", classes="text-h6") - - with embeddings_content_slot: - self._embeddings_app.settings_widget() - - with embeddings_actions_slot: - self._embeddings_app.compute_ui() - - filter_title_slot, filter_content_slot, filter_actions_slot = ( - collapsible_card("collapse_filter") - ) - - with filter_title_slot: - html.Span("Category Filter", classes="text-h6") - - with filter_content_slot: - self._filtering_app.filter_operator_ui() - self._filtering_app.filter_options_ui() - - with filter_actions_slot: - self._filtering_app.filter_apply_ui() - - ( - transforms_title_slot, - transforms_content_slot, - transforms_actions_slot, - ) = collapsible_card("collapse_transforms") - - with transforms_title_slot: - html.Span("Transform Settings", classes="text-h6") - - with transforms_content_slot: - self._transforms_app.settings_widget() - - with transforms_actions_slot: - self._transforms_app.apply_ui() - - with html.Template(v_slot_after=True): - with quasar.QSplitter( - v_model=("vertical_split",), - limits=("[0,100]",), - horizontal=True, - classes="inherit-height zero-height", - before_class="q-pa-md", - after_class="q-pa-md", - ): - with html.Template(v_slot_before=True): - self._embeddings_app.visualization_widget() - - with html.Template(v_slot_after=True): - with html.Div(classes="row q-col-gutter-md"): - with html.Div(classes="col-6"): - with quasar.QCard(flat=True, bordered=True): - with quasar.QCardSection(): - html.Span( - "Original Dataset", classes="text-h5" - ) - - with quasar.QCardSection(): - self._transforms_app.original_dataset_widget() - - with html.Div(classes="col-6"): - with quasar.QCard( - flat=True, - bordered=True, - style="background-color: #ffcdd2;", - ): - with quasar.QCardSection(): - html.Span( - "Transformed Dataset", - classes="text-h5", - ) - - with quasar.QCardSection(): - self._transforms_app.transformed_dataset_widget() - - self._ui = layout - - return self._ui + def ui(self): + extra_args = {} + if self.server.hot_reload: + ui.reload(ui) + extra_args["reload"] = self.ui + + return ui.build_layout( + self.server, + self.input_paths, + self._embeddings_app, + self._filtering_app, + self._transforms_app, + **extra_args, + ) diff --git a/src/nrtk_explorer/app/transforms.py b/src/nrtk_explorer/app/transforms.py index 14e1347b..46e261d3 100644 --- a/src/nrtk_explorer/app/transforms.py +++ b/src/nrtk_explorer/app/transforms.py @@ -13,7 +13,7 @@ import nrtk_explorer.library.transforms as trans import nrtk_explorer.library.nrtk_transforms as nrtk_trans from nrtk_explorer.library import images_manager, object_detector -from nrtk_explorer.app.ui.image_list import image_list_component +from nrtk_explorer.app import ui from nrtk_explorer.app.applet import Applet from nrtk_explorer.app.parameters import ParametersApp from nrtk_explorer.library.ml_models import ( @@ -350,11 +350,11 @@ def apply_ui(self): def original_dataset_widget(self): with html.Div(trame_server=self.server): - image_list_component("source_image_ids", self.on_hover) + ui.image_list_component("source_image_ids", self.on_hover) def transformed_dataset_widget(self): with html.Div(trame_server=self.server): - image_list_component("transformed_image_ids", self.on_hover, is_transformation=True) + ui.image_list_component("transformed_image_ids", self.on_hover, is_transformation=True) # This is only used within when this module (file) is executed as an Standalone app. @property diff --git a/src/nrtk_explorer/app/ui/__init__.py b/src/nrtk_explorer/app/ui/__init__.py index e69de29b..560d4721 100644 --- a/src/nrtk_explorer/app/ui/__init__.py +++ b/src/nrtk_explorer/app/ui/__init__.py @@ -0,0 +1,20 @@ +from .layout import build_layout +from .image_list import image_list_component +from .collapsible_card import card + + +def reload(m=None): + from . import collapsible_card, image_list, layout + + collapsible_card.__loader__.exec_module(collapsible_card) + image_list.__loader__.exec_module(image_list) + layout.__loader__.exec_module(layout) + if m: + m.__loader__.exec_module(m) + + +__all__ = [ + "build_layout", + "image_list_component", + "card", +] diff --git a/src/nrtk_explorer/app/ui/collapsible_card.py b/src/nrtk_explorer/app/ui/collapsible_card.py index f3c00930..6203a8cc 100644 --- a/src/nrtk_explorer/app/ui/collapsible_card.py +++ b/src/nrtk_explorer/app/ui/collapsible_card.py @@ -2,7 +2,7 @@ from trame.widgets import html -def collapsible_card(collapse_key): +def card(collapse_key): with quasar.QCard(): with quasar.QCardSection(): with html.Div(classes="row items-center no-wrap"): diff --git a/src/nrtk_explorer/app/ui/layout.py b/src/nrtk_explorer/app/ui/layout.py new file mode 100644 index 00000000..ae331974 --- /dev/null +++ b/src/nrtk_explorer/app/ui/layout.py @@ -0,0 +1,155 @@ +from pathlib import Path +from trame.ui.quasar import QLayout +from trame.widgets import quasar +from trame.widgets import html +from nrtk_explorer.app import ui + + +def parse_dataset_dirs(datasets): + return [{"label": Path(ds).name, "value": ds} for ds in datasets] + + +def build_layout( + server, dataset_paths, embeddings_app, filtering_app, transforms_app, reload=None +): + with QLayout(server, view="lhh LpR lff", classes="shadow-2 rounded-borders bg-grey-2"): + # # Toolbar + with quasar.QHeader(): + with quasar.QToolbar(classes="shadow-4"): + quasar.QToolbarTitle("NRTK_EXPLORER") + + if reload: + quasar.QBtn( + "Reload", + click=(reload,), + flat=True, + ) + + # # Main content + with quasar.QPageContainer(): + with quasar.QPage(): + with quasar.QSplitter( + model_value=("horizontal_split",), + classes="inherit-height", + before_class="inherit-height zero-height scroll", + after_class="inherit-height zero-height", + ): + with html.Template(v_slot_before=True): + with html.Div(classes="q-pa-md q-gutter-md"): + ( + dataset_title_slot, + dataset_content_slot, + dataset_actions_slot, + ) = ui.card("collapse_dataset") + + with dataset_title_slot: + html.Span("Dataset Selection", classes="text-h6") + + with dataset_content_slot: + quasar.QSelect( + label="Dataset", + v_model=("current_dataset",), + options=(parse_dataset_dirs(dataset_paths),), + filled=True, + emit_value=True, + map_options=True, + dense=True, + ) + quasar.QSlider( + v_model=("num_images", 15), + min=(0,), + max=("num_images_max", 25), + disable=("num_images_disabled", True), + step=(1,), + ) + html.P( + "{{num_images}}/{{num_images_max}} images", + classes="text-caption text-center", + ) + + quasar.QToggle( + v_model=("random_sampling", False), + dense=False, + label="Random selection", + ) + + ( + embeddings_title_slot, + embeddings_content_slot, + embeddings_actions_slot, + ) = ui.card("collapse_embeddings") + + with embeddings_title_slot: + html.Span("Embeddings", classes="text-h6") + + with embeddings_content_slot: + embeddings_app.settings_widget() + + with embeddings_actions_slot: + embeddings_app.compute_ui() + + filter_title_slot, filter_content_slot, filter_actions_slot = ui.card( + "collapse_filter" + ) + + with filter_title_slot: + html.Span("Category Filter", classes="text-h6") + + with filter_content_slot: + filtering_app.filter_operator_ui() + filtering_app.filter_options_ui() + + with filter_actions_slot: + filtering_app.filter_apply_ui() + + ( + transforms_title_slot, + transforms_content_slot, + transforms_actions_slot, + ) = ui.card("collapse_transforms") + + with transforms_title_slot: + html.Span("Transform Settings", classes="text-h6") + + with transforms_content_slot: + transforms_app.settings_widget() + + with transforms_actions_slot: + transforms_app.apply_ui() + + with html.Template(v_slot_after=True): + with quasar.QSplitter( + v_model=("vertical_split",), + limits=("[0,100]",), + horizontal=True, + classes="inherit-height zero-height", + before_class="q-pa-md", + after_class="q-pa-md", + ): + with html.Template(v_slot_before=True): + embeddings_app.visualization_widget() + + with html.Template(v_slot_after=True): + with html.Div(classes="row q-col-gutter-md"): + with html.Div(classes="col-6"): + with quasar.QCard(flat=True, bordered=True): + with quasar.QCardSection(): + html.Span("Original Dataset", classes="text-h5") + + with quasar.QCardSection(): + transforms_app.original_dataset_widget() + + with html.Div(classes="col-6"): + with quasar.QCard( + flat=True, + bordered=False, + style="background-color: #ffcdd2;", + ): + with quasar.QCardSection(): + html.Span( + "Transformed Dataset", + classes="text-h5", + ) + + with quasar.QCardSection(): + transforms_app.transformed_dataset_widget() From efaf36a602b3383ab7a7374ec698b49a6e80d5b9 Mon Sep 17 00:00:00 2001 From: Paul Elliott Date: Wed, 8 May 2024 16:22:14 -0400 Subject: [PATCH 09/10] fix(nrtk_transforms): support new nrtk PybsmPerturber api --- src/nrtk_explorer/library/nrtk_transforms.py | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/src/nrtk_explorer/library/nrtk_transforms.py b/src/nrtk_explorer/library/nrtk_transforms.py index f372d228..58cf8d96 100644 --- a/src/nrtk_explorer/library/nrtk_transforms.py +++ b/src/nrtk_explorer/library/nrtk_transforms.py @@ -1,15 +1,13 @@ from typing import Any, Optional, Dict, Tuple +import numpy as np +from PIL import Image as ImageModule from PIL.Image import Image - -from nrtk_explorer.library.transforms import ImageTransform, ParameterDescription - +from pybsm.otf import darkCurrentFromDensity from nrtk.impls.perturb_image.generic.cv2.blur import GaussianBlurPerturber from nrtk.impls.perturb_image.pybsm.perturber import PybsmPerturber, PybsmSensor, PybsmScenario -import pybsm -import numpy as np -from PIL import Image as ImageModule +from nrtk_explorer.library.transforms import ImageTransform, ParameterDescription class NrtkGaussianBlurTransform(ImageTransform): @@ -48,7 +46,8 @@ def execute(self, input: Image, *input_args: Any) -> Image: # Taken from the nrtk package tests -def createSampleSensorandScenario() -> Tuple[PybsmSensor, PybsmScenario]: +# https://github.com/Kitware/nrtk/blob/main/tests/impls/perturb_image/pybsm/test_pybsm_pertuber.py#L21 +def createSampleSensorAndScenario() -> Tuple[PybsmSensor, PybsmScenario]: name = "L32511x" @@ -76,7 +75,7 @@ def createSampleSensorandScenario() -> Tuple[PybsmSensor, PybsmScenario]: intTime = 30.0e-3 # dark current density of 1 nA/cm2 guess, guess mid range for a silicon camera - darkCurrent = pybsm.darkCurrentFromDensity(1e-5, wx, wy) + darkCurrent = darkCurrentFromDensity(1e-5, wx, wy) # rms read noise (rms electrons) readNoise = 25.0 @@ -144,7 +143,7 @@ def createSampleSensorandScenario() -> Tuple[PybsmSensor, PybsmScenario]: class NrtkPybsmTransform(ImageTransform): def __init__(self, perturber: Optional[PybsmPerturber] = None): if perturber is None: - sensor, scenario = createSampleSensorandScenario() + sensor, scenario = createSampleSensorAndScenario() perturber = PybsmPerturber(sensor=sensor, scenario=scenario) self._perturber: PybsmPerturber = perturber @@ -158,7 +157,6 @@ def get_parameters(self) -> dict[str, Any]: def set_parameters(self, params: Dict[str, Any]): self._perturber.sensor.D = params["D"] self._perturber.sensor.f = params["f"] - self._perturber.metrics = pybsm.niirs(self._perturber.sensor, self._perturber.scenario) def get_parameters_description(self) -> Dict[str, ParameterDescription]: aperture_description: ParameterDescription = { From dfa9a857675766fae6fdb66d3b4d68346607dea9 Mon Sep 17 00:00:00 2001 From: Paul Elliott Date: Thu, 9 May 2024 16:14:24 -0400 Subject: [PATCH 10/10] refactor(layout): factor out sections to functions --- src/nrtk_explorer/app/core.py | 26 ++- src/nrtk_explorer/app/ui/layout.py | 306 ++++++++++++++++------------- 2 files changed, 180 insertions(+), 152 deletions(-) diff --git a/src/nrtk_explorer/app/core.py b/src/nrtk_explorer/app/core.py index d90e5347..6a0190f6 100644 --- a/src/nrtk_explorer/app/core.py +++ b/src/nrtk_explorer/app/core.py @@ -67,8 +67,6 @@ def __init__(self, server=None): self.context["images_manager"] = images_manager.ImagesManager() self.context["annotations"] = {} - self._ui = None - self.state.collapse_dataset = False self.state.collapse_embeddings = False self.state.collapse_filter = False @@ -115,7 +113,7 @@ def __init__(self, server=None): self.state.trame__title = "nrtk_explorer" # Bind instance methods to controller - self.ctrl.on_server_reload = self.ui + self.ctrl.on_server_reload = self._build_ui self.ctrl.add("on_server_ready")(self.on_server_ready) self.state.num_images_max = 0 @@ -124,8 +122,8 @@ def __init__(self, server=None): self.state.random_sampling_disabled = True self.state.images_id = [] - # Generate UI - self.ui() + self._build_ui() + self.context.images_manager = images_manager.ImagesManager() def on_server_ready(self, *args, **kwargs): @@ -203,17 +201,17 @@ def reload_images(self): self.state.annotation_categories = categories self.state.images_ids = [img["id"] for img in selected_images] - def ui(self): + def _build_ui(self): extra_args = {} if self.server.hot_reload: ui.reload(ui) - extra_args["reload"] = self.ui - - return ui.build_layout( - self.server, - self.input_paths, - self._embeddings_app, - self._filtering_app, - self._transforms_app, + extra_args["reload"] = self._build_ui + + self.ui = ui.build_layout( + server=self.server, + dataset_paths=self.input_paths, + embeddings_app=self._embeddings_app, + filtering_app=self._filtering_app, + transforms_app=self._transforms_app, **extra_args, ) diff --git a/src/nrtk_explorer/app/ui/layout.py b/src/nrtk_explorer/app/ui/layout.py index ae331974..e8f53736 100644 --- a/src/nrtk_explorer/app/ui/layout.py +++ b/src/nrtk_explorer/app/ui/layout.py @@ -5,151 +5,181 @@ from nrtk_explorer.app import ui +def toolbar(reload=None): + with quasar.QHeader(): + with quasar.QToolbar(classes="shadow-4"): + quasar.QToolbarTitle("NRTK_EXPLORER") + if reload: + quasar.QBtn( + "Reload", + click=(reload,), + flat=True, + ) + + def parse_dataset_dirs(datasets): return [{"label": Path(ds).name, "value": ds} for ds in datasets] -def build_layout( - server, dataset_paths, embeddings_app, filtering_app, transforms_app, reload=None +def parameters(dataset_paths=[], embeddings_app=None, filtering_app=None, transforms_app=None): + with html.Div(classes="q-pa-md q-gutter-md"): + ( + dataset_title_slot, + dataset_content_slot, + _, + ) = ui.card("collapse_dataset") + + with dataset_title_slot: + html.Span("Dataset Selection", classes="text-h6") + + with dataset_content_slot: + quasar.QSelect( + label="Dataset", + v_model=("current_dataset",), + options=(parse_dataset_dirs(dataset_paths),), + filled=True, + emit_value=True, + map_options=True, + dense=True, + ) + quasar.QSlider( + v_model=("num_images", 15), + min=(0,), + max=("num_images_max", 25), + disable=("num_images_disabled", True), + step=(1,), + ) + html.P( + "{{num_images}}/{{num_images_max}} images", + classes="text-caption text-center", + ) + + quasar.QToggle( + v_model=("random_sampling", False), + dense=False, + label="Random selection", + ) + + ( + embeddings_title_slot, + embeddings_content_slot, + embeddings_actions_slot, + ) = ui.card("collapse_embeddings") + + with embeddings_title_slot: + html.Span("Embeddings", classes="text-h6") + + with embeddings_content_slot: + embeddings_app.settings_widget() + + with embeddings_actions_slot: + embeddings_app.compute_ui() + + filter_title_slot, filter_content_slot, filter_actions_slot = ui.card("collapse_filter") + + with filter_title_slot: + html.Span("Category Filter", classes="text-h6") + + with filter_content_slot: + filtering_app.filter_operator_ui() + filtering_app.filter_options_ui() + + with filter_actions_slot: + filtering_app.filter_apply_ui() + + ( + transforms_title_slot, + transforms_content_slot, + transforms_actions_slot, + ) = ui.card("collapse_transforms") + + with transforms_title_slot: + html.Span("Transform Settings", classes="text-h6") + + with transforms_content_slot: + transforms_app.settings_widget() + + with transforms_actions_slot: + transforms_app.apply_ui() + + +def dataset_view( + embeddings_app=None, + transforms_app=None, ): - with QLayout(server, view="lhh LpR lff", classes="shadow-2 rounded-borders bg-grey-2"): - # # Toolbar - with quasar.QHeader(): - with quasar.QToolbar(classes="shadow-4"): - quasar.QToolbarTitle("NRTK_EXPLORER") - - if reload: - quasar.QBtn( - "Reload", - click=(reload,), + with quasar.QSplitter( + v_model=("vertical_split",), + limits=("[0,100]",), + horizontal=True, + classes="inherit-height zero-height", + before_class="q-pa-md", + after_class="q-pa-md", + ): + with html.Template(v_slot_before=True): + embeddings_app.visualization_widget() + + with html.Template(v_slot_after=True): + with html.Div(classes="row q-col-gutter-md"): + with html.Div(classes="col-6"): + with quasar.QCard(flat=True, bordered=True): + with quasar.QCardSection(): + html.Span("Original Dataset", classes="text-h5") + + with quasar.QCardSection(): + transforms_app.original_dataset_widget() + + with html.Div(classes="col-6"): + with quasar.QCard( flat=True, - ) + bordered=False, + style="background-color: #ffcdd2;", + ): + with quasar.QCardSection(): + html.Span( + "Transformed Dataset", + classes="text-h5", + ) + + with quasar.QCardSection(): + transforms_app.transformed_dataset_widget() + + +def explorer( + dataset_paths=[], + embeddings_app=None, + filtering_app=None, + transforms_app=None, +): + with quasar.QSplitter( + model_value=("horizontal_split",), + classes="inherit-height", + before_class="inherit-height zero-height scroll", + after_class="inherit-height zero-height", + ): + with html.Template(v_slot_before=True): + parameters( + dataset_paths=dataset_paths, + embeddings_app=embeddings_app, + filtering_app=filtering_app, + transforms_app=transforms_app, + ) + + with html.Template(v_slot_after=True): + dataset_view(embeddings_app=embeddings_app, transforms_app=transforms_app) + + +def build_layout( + server=None, + reload=None, + **kwargs, +): + with QLayout( + server, view="lhh LpR lff", classes="shadow-2 rounded-borders bg-grey-2" + ) as layout: + toolbar(reload=reload) - # # Main content with quasar.QPageContainer(): with quasar.QPage(): - with quasar.QSplitter( - model_value=("horizontal_split",), - classes="inherit-height", - before_class="inherit-height zero-height scroll", - after_class="inherit-height zero-height", - ): - with html.Template(v_slot_before=True): - with html.Div(classes="q-pa-md q-gutter-md"): - ( - dataset_title_slot, - dataset_content_slot, - dataset_actions_slot, - ) = ui.card("collapse_dataset") - - with dataset_title_slot: - html.Span("Dataset Selection", classes="text-h6") - - with dataset_content_slot: - quasar.QSelect( - label="Dataset", - v_model=("current_dataset",), - options=(parse_dataset_dirs(dataset_paths),), - filled=True, - emit_value=True, - map_options=True, - dense=True, - ) - quasar.QSlider( - v_model=("num_images", 15), - min=(0,), - max=("num_images_max", 25), - disable=("num_images_disabled", True), - step=(1,), - ) - html.P( - "{{num_images}}/{{num_images_max}} images", - classes="text-caption text-center", - ) - - quasar.QToggle( - v_model=("random_sampling", False), - dense=False, - label="Random selection", - ) - - ( - embeddings_title_slot, - embeddings_content_slot, - embeddings_actions_slot, - ) = ui.card("collapse_embeddings") - - with embeddings_title_slot: - html.Span("Embeddings", classes="text-h6") - - with embeddings_content_slot: - embeddings_app.settings_widget() - - with embeddings_actions_slot: - embeddings_app.compute_ui() - - filter_title_slot, filter_content_slot, filter_actions_slot = ui.card( - "collapse_filter" - ) + explorer(**kwargs) - with filter_title_slot: - html.Span("Category Filter", classes="text-h6") - - with filter_content_slot: - filtering_app.filter_operator_ui() - filtering_app.filter_options_ui() - - with filter_actions_slot: - filtering_app.filter_apply_ui() - - ( - transforms_title_slot, - transforms_content_slot, - transforms_actions_slot, - ) = ui.card("collapse_transforms") - - with transforms_title_slot: - html.Span("Transform Settings", classes="text-h6") - - with transforms_content_slot: - transforms_app.settings_widget() - - with transforms_actions_slot: - transforms_app.apply_ui() - - with html.Template(v_slot_after=True): - with quasar.QSplitter( - v_model=("vertical_split",), - limits=("[0,100]",), - horizontal=True, - classes="inherit-height zero-height", - before_class="q-pa-md", - after_class="q-pa-md", - ): - with html.Template(v_slot_before=True): - embeddings_app.visualization_widget() - - with html.Template(v_slot_after=True): - with html.Div(classes="row q-col-gutter-md"): - with html.Div(classes="col-6"): - with quasar.QCard(flat=True, bordered=True): - with quasar.QCardSection(): - html.Span("Original Dataset", classes="text-h5") - - with quasar.QCardSection(): - transforms_app.original_dataset_widget() - - with html.Div(classes="col-6"): - with quasar.QCard( - flat=True, - bordered=False, - style="background-color: #ffcdd2;", - ): - with quasar.QCardSection(): - html.Span( - "Transformed Dataset", - classes="text-h5", - ) - - with quasar.QCardSection(): - transforms_app.transformed_dataset_widget() + return layout