From 285145be592f85aaccf1576156781a961d60295f Mon Sep 17 00:00:00 2001
From: danielgural <28dang28@gmail.com>
Date: Thu, 24 Oct 2024 15:20:18 -0400
Subject: [PATCH 001/104] Fixed Hugging Face Transformers not using GPU
---
fiftyone/utils/transformers.py | 39 +++++++++++++++++-----------------
1 file changed, 19 insertions(+), 20 deletions(-)
diff --git a/fiftyone/utils/transformers.py b/fiftyone/utils/transformers.py
index 95bc3b23e0..ee781a05bb 100644
--- a/fiftyone/utils/transformers.py
+++ b/fiftyone/utils/transformers.py
@@ -6,16 +6,16 @@
| `voxel51.com `_
|
"""
-import logging
-import numpy as np
+import logging
import eta.core.utils as etau
+import numpy as np
-from fiftyone.core.config import Config
import fiftyone.core.labels as fol
-from fiftyone.core.models import Model, EmbeddingsMixin, PromptMixin
import fiftyone.core.utils as fou
+from fiftyone.core.config import Config
+from fiftyone.core.models import EmbeddingsMixin, Model, PromptMixin
from fiftyone.zoo.models import HasZooModel
torch = fou.lazy_import("torch")
@@ -451,9 +451,7 @@ class FiftyOneTransformer(TransformerEmbeddingsMixin, Model):
def __init__(self, config):
self.config = config
self.model = self._load_model(config)
- self.device = (
- "cuda" if next(self.model.parameters()).is_cuda else "cpu"
- )
+ self.device = "cuda" if torch.cuda.is_available() else "cpu"
self.image_processor = self._load_image_processor()
@property
@@ -498,9 +496,7 @@ def __init__(self, config):
self.config = config
self.classes = config.classes
self.model = self._load_model(config)
- self.device = (
- "cuda" if next(self.model.parameters()).is_cuda else "cpu"
- )
+ self.device = "cuda" if torch.cuda.is_available() else "cpu"
self.processor = self._load_processor()
self._text_prompts = None
@@ -749,9 +745,7 @@ def __init__(self, config):
self.classes = config.classes
self.processor = self._load_processor(config)
self.model = self._load_model(config)
- self.device = (
- "cuda" if next(self.model.parameters()).is_cuda else "cpu"
- )
+ self.device = "cuda" if torch.cuda.is_available() else "cpu"
self._text_prompts = None
def _load_processor(self, config):
@@ -760,7 +754,9 @@ def _load_processor(self, config):
if config.model is not None:
name_or_path = config.model.name_or_path
- return transformers.AutoProcessor.from_pretrained(name_or_path)
+ return transformers.AutoProcessor.from_pretrained(name_or_path).to(
+ self.device
+ )
def _load_model(self, config):
name_or_path = config.name_or_path
@@ -825,7 +821,7 @@ def _load_model(self, config):
return transformers.AutoModelForObjectDetection.from_pretrained(
config.name_or_path
- )
+ ).to(self.device)
def _predict(self, inputs, target_sizes):
with torch.no_grad():
@@ -876,10 +872,11 @@ def _load_model(self, config):
if config.model is not None:
model = config.model
else:
+ device = "cuda" if torch.cuda.is_available() else "cpu"
model = (
transformers.AutoModelForSemanticSegmentation.from_pretrained(
config.name_or_path
- )
+ ).to(device)
)
self.mask_targets = model.config.id2label
@@ -929,10 +926,10 @@ class FiftyOneTransformerForDepthEstimation(FiftyOneTransformer):
def _load_model(self, config):
if config.model is not None:
return config.model
-
+ device = "cuda" if torch.cuda.is_available() else "cpu"
return transformers.AutoModelForDepthEstimation.from_pretrained(
config.name_or_path
- )
+ ).to(device)
def _predict(self, inputs, target_sizes):
with torch.no_grad():
@@ -1034,7 +1031,8 @@ def _get_model_for_image_text_retrieval(base_model, model_name_or_path):
__import__(module_name, fromlist=[itr_class_name]),
itr_class_name,
)
- return itr_class.from_pretrained(model_name_or_path)
+
+ return itr_class.from_pretrained(model_name_or_path).to(base_model.device)
def _get_image_processor_fallback(model):
@@ -1083,4 +1081,5 @@ def _get_detector_from_processor(processor, model_name_or_path):
__import__(module_name, fromlist=[detector_class_name]),
detector_class_name,
)
- return detector_class.from_pretrained(model_name_or_path)
+ device = "cuda" if torch.cuda.is_available() else "cpu"
+ return detector_class.from_pretrained(model_name_or_path).to(device)
From 3d6084fa08a941c308732b2cbf7d3ac6b00e0e67 Mon Sep 17 00:00:00 2001
From: danielgural <28dang28@gmail.com>
Date: Thu, 24 Oct 2024 15:34:34 -0400
Subject: [PATCH 002/104] Fixed some classes not having device on them
---
fiftyone/utils/transformers.py | 13 ++++++++-----
1 file changed, 8 insertions(+), 5 deletions(-)
diff --git a/fiftyone/utils/transformers.py b/fiftyone/utils/transformers.py
index ee781a05bb..0b54029586 100644
--- a/fiftyone/utils/transformers.py
+++ b/fiftyone/utils/transformers.py
@@ -581,7 +581,10 @@ def _load_model(self, config):
if config.model is not None:
return config.model
- model = transformers.AutoModel.from_pretrained(config.name_or_path)
+ device = "cuda" if torch.cuda.is_available() else "cpu"
+ model = transformers.AutoModel.from_pretrained(config.name_or_path).to(
+ device
+ )
if _has_image_text_retrieval(model):
model = _get_model_for_image_text_retrieval(
model, config.name_or_path
@@ -690,10 +693,10 @@ class FiftyOneTransformerForImageClassification(FiftyOneTransformer):
def _load_model(self, config):
if config.model is not None:
return config.model
-
+ device = "cuda" if torch.cuda.is_available() else "cpu"
return transformers.AutoModelForImageClassification.from_pretrained(
config.name_or_path
- )
+ ).to(device)
def _predict(self, inputs):
with torch.no_grad():
@@ -818,10 +821,10 @@ class FiftyOneTransformerForObjectDetection(FiftyOneTransformer):
def _load_model(self, config):
if config.model is not None:
return config.model
-
+ device = "cuda" if torch.cuda.is_available() else "cpu"
return transformers.AutoModelForObjectDetection.from_pretrained(
config.name_or_path
- ).to(self.device)
+ ).to(device)
def _predict(self, inputs, target_sizes):
with torch.no_grad():
From 81baea439eaff177d89ea1634fcbf717d25ea12f Mon Sep 17 00:00:00 2001
From: afoley587 <54959686+afoley587@users.noreply.github.com>
Date: Wed, 4 Dec 2024 14:30:08 -0500
Subject: [PATCH 003/104] chore: Bump brain version (#5212)
---
setup.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/setup.py b/setup.py
index 1feb01edee..c4715aaeea 100644
--- a/setup.py
+++ b/setup.py
@@ -74,7 +74,7 @@ def get_version():
"universal-analytics-python3>=1.0.1,<2",
"pydash",
# internal packages
- "fiftyone-brain>=0.17.0,<0.18",
+ "fiftyone-brain>=0.18.0,<0.19",
"fiftyone-db>=0.4,<2.0",
"voxel51-eta>=0.13.0,<0.14",
]
From 1d7c7e77d11756b3f4298466471bb9e2d4f3f571 Mon Sep 17 00:00:00 2001
From: brimoor
Date: Wed, 4 Dec 2024 23:00:14 -0500
Subject: [PATCH 004/104] bump release date
---
docs/source/release-notes.rst | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/docs/source/release-notes.rst b/docs/source/release-notes.rst
index 17aa4f7a62..d677d83fdd 100644
--- a/docs/source/release-notes.rst
+++ b/docs/source/release-notes.rst
@@ -5,7 +5,7 @@ FiftyOne Release Notes
FiftyOne Teams 2.2.0
--------------------
-*Released December 4, 2024*
+*Released December 5, 2024*
Includes all updates from :ref:`FiftyOne 1.1.0 `, plus:
@@ -30,7 +30,7 @@ Includes all updates from :ref:`FiftyOne 1.1.0 `, plus:
FiftyOne 1.1.0
--------------
-*Released December 4, 2024*
+*Released December 5, 2024*
What's New
From 68344b99e352a6ec8c01f89f68308fc7529fc82a Mon Sep 17 00:00:00 2001
From: Ritchie Martori
Date: Thu, 5 Dec 2024 08:11:00 -0700
Subject: [PATCH 005/104] fix issue with disabled schedule exec option
---
.../operators/src/ExecutionOptionItem.tsx | 22 +++++++++
app/packages/operators/src/SplitButton.tsx | 26 ++--------
.../OperatorExecutionMenu/index.tsx | 48 +++++++++++++------
app/packages/operators/src/state.ts | 2 +
4 files changed, 60 insertions(+), 38 deletions(-)
create mode 100644 app/packages/operators/src/ExecutionOptionItem.tsx
diff --git a/app/packages/operators/src/ExecutionOptionItem.tsx b/app/packages/operators/src/ExecutionOptionItem.tsx
new file mode 100644
index 0000000000..a87a791dab
--- /dev/null
+++ b/app/packages/operators/src/ExecutionOptionItem.tsx
@@ -0,0 +1,22 @@
+import { useTheme } from "@fiftyone/components";
+
+export default function ExecutionOptionItem({ label, tag, disabled }) {
+ const theme = useTheme();
+ const tagEl = tag ? (
+
+ {tag}
+
+ ) : null;
+ return (
+
+ {label}
+ {tagEl}
+
+ );
+}
diff --git a/app/packages/operators/src/SplitButton.tsx b/app/packages/operators/src/SplitButton.tsx
index fbb4b1266d..d75c54430a 100644
--- a/app/packages/operators/src/SplitButton.tsx
+++ b/app/packages/operators/src/SplitButton.tsx
@@ -15,7 +15,7 @@ import {
} from "@mui/material";
import ArrowDropDownIcon from "@mui/icons-material/ArrowDropDown";
import { onEnter } from "./utils";
-import { useTheme } from "@fiftyone/components";
+import ExecutionOptionItem from "./ExecutionOptionItem";
const ButtonStylesOverrides: ButtonProps["sx"] = {
color: (theme) => theme.palette.text.secondary,
@@ -63,6 +63,7 @@ export default function SplitButton({
};
const handleSelect = (option) => {
+ if (!option.onSelect) return;
option.onSelect();
setOpen(false);
};
@@ -142,7 +143,7 @@ export default function SplitButton({
: theme.palette.text.disabled,
}}
primary={
-
);
}
-
-function PrimaryWithTag({ label, tag, disabled }) {
- const theme = useTheme();
- const tagEl = tag ? (
-
- {tag}
-
- ) : null;
- return (
-
- {label}
- {tagEl}
-
- );
-}
diff --git a/app/packages/operators/src/components/OperatorExecutionMenu/index.tsx b/app/packages/operators/src/components/OperatorExecutionMenu/index.tsx
index a047776493..f0b2fd747b 100644
--- a/app/packages/operators/src/components/OperatorExecutionMenu/index.tsx
+++ b/app/packages/operators/src/components/OperatorExecutionMenu/index.tsx
@@ -1,6 +1,6 @@
import { Menu, MenuItem, Stack, Typography } from "@mui/material";
-import React from "react";
import { OperatorExecutionOption } from "../../state";
+import ExecutionOptionItem from "../../ExecutionOptionItem";
/**
* Component which provides a context menu for executing an operator using a
@@ -28,24 +28,42 @@ export const OperatorExecutionMenu = ({
return (
);
};
export default OperatorExecutionMenu;
+
+function Item({ target, disabled, onClose, onOptionClick }) {
+ return (
+
+ );
+}
diff --git a/app/packages/operators/src/state.ts b/app/packages/operators/src/state.ts
index 6d4a87e92c..331ba2a18f 100644
--- a/app/packages/operators/src/state.ts
+++ b/app/packages/operators/src/state.ts
@@ -246,6 +246,7 @@ export type OperatorExecutionOption = {
default?: boolean;
selected?: boolean;
onSelect?: () => void;
+ isDisabledSchedule?: boolean;
};
const useOperatorPromptSubmitOptions = (
@@ -346,6 +347,7 @@ const useOperatorPromptSubmitOptions = (
id: "disabled-schedule",
description: markdownDesc,
isDelegated: true,
+ isDisabledSchedule: true,
});
}
From be1681de6e40cfcb3c419f41efb9f660d7d8e07b Mon Sep 17 00:00:00 2001
From: Rodrigo Agundez
Date: Thu, 5 Dec 2024 17:36:12 +0800
Subject: [PATCH 006/104] Allow to provide an email for login when pushing an
annotation to an annoration backend
---
fiftyone/utils/cvat.py | 28 ++++++++++++++++++++++------
1 file changed, 22 insertions(+), 6 deletions(-)
diff --git a/fiftyone/utils/cvat.py b/fiftyone/utils/cvat.py
index c30b6811dd..046a66f304 100644
--- a/fiftyone/utils/cvat.py
+++ b/fiftyone/utils/cvat.py
@@ -3045,6 +3045,7 @@ class CVATBackendConfig(foua.AnnotationBackendConfig):
media files on disk to upload
url (None): the url of the CVAT server
username (None): the CVAT username
+ email (None): the CVAT email
password (None): the CVAT password
headers (None): an optional dict of headers to add to all CVAT API
requests
@@ -3125,6 +3126,7 @@ def __init__(
media_field="filepath",
url=None,
username=None,
+ email=None,
password=None,
headers=None,
task_size=None,
@@ -3172,6 +3174,7 @@ def __init__(
# store privately so these aren't serialized
self._username = username
+ self._email = email
self._password = password
self._headers = headers
@@ -3183,6 +3186,14 @@ def username(self):
def username(self, value):
self._username = value
+ @property
+ def email(self):
+ return self._email
+
+ @email.setter
+ def email(self, value):
+ self._email = value
+
@property
def password(self):
return self._password
@@ -3200,10 +3211,10 @@ def headers(self, value):
self._headers = value
def load_credentials(
- self, url=None, username=None, password=None, headers=None
+ self, url=None, username=None, password=None, email=None, headers=None
):
self._load_parameters(
- url=url, username=username, password=password, headers=headers
+ url=url, username=username, password=password, email=email, headers=headers
)
@@ -3303,6 +3314,7 @@ def _connect_to_api(self):
self.config.name,
self.config.url,
username=self.config.username,
+ email=self.config.email,
password=self.config.password,
headers=self.config.headers,
organization=self.config.organization,
@@ -3550,6 +3562,7 @@ class CVATAnnotationAPI(foua.AnnotationAPI):
name: the name of the backend
url: url of the CVAT server
username (None): the CVAT username
+ email (None): the CVAT email
password (None): the CVAT password
headers (None): an optional dict of headers to add to all requests
organization (None): the name of the organization to use when sending
@@ -3561,6 +3574,7 @@ def __init__(
name,
url,
username=None,
+ email=None,
password=None,
headers=None,
organization=None,
@@ -3568,6 +3582,7 @@ def __init__(
self._name = name
self._url = url.rstrip("/")
self._username = username
+ self._email = email
self._password = password
self._headers = headers
self._organization = organization
@@ -3722,6 +3737,7 @@ def _setup(self):
username = self._username
password = self._password
+ email = self._email
if username is None or password is None:
username, password = self._prompt_username_password(
@@ -3739,13 +3755,13 @@ def _setup(self):
self._server_version = Version("2")
try:
- self._login(username, password)
+ self._login(username, password, email)
except requests.exceptions.HTTPError as e:
if e.response.status_code != 404:
raise e
self._server_version = Version("1")
- self._login(username, password)
+ self._login(username, password, email)
self._add_referer()
self._add_organization()
@@ -3782,12 +3798,12 @@ def _add_organization(self):
def close(self):
self._session.close()
- def _login(self, username, password):
+ def _login(self, username, password, email):
response = self._make_request(
self._session.post,
self.login_url,
print_error_info=False,
- json={"username": username, "password": password},
+ json={"username": username, "password": password, "email": email},
)
if "csrftoken" in response.cookies:
From f1369010d59baf03578ffeb229378c85b2f2c5df Mon Sep 17 00:00:00 2001
From: Rodrigo Agundez
Date: Thu, 5 Dec 2024 18:08:37 +0800
Subject: [PATCH 007/104] Add email for payload only if provided
---
fiftyone/utils/cvat.py | 8 +++++++-
1 file changed, 7 insertions(+), 1 deletion(-)
diff --git a/fiftyone/utils/cvat.py b/fiftyone/utils/cvat.py
index 046a66f304..9e7de7dd98 100644
--- a/fiftyone/utils/cvat.py
+++ b/fiftyone/utils/cvat.py
@@ -3799,11 +3799,17 @@ def close(self):
self._session.close()
def _login(self, username, password, email):
+ payload = {
+ "username": username,
+ "password": password,
+ }
+ if email is not None:
+ payload["email"] = email
response = self._make_request(
self._session.post,
self.login_url,
print_error_info=False,
- json={"username": username, "password": password, "email": email},
+ json=payload,
)
if "csrftoken" in response.cookies:
From f42e4fe5278156b5c212987c21fb4f47e69938ef Mon Sep 17 00:00:00 2001
From: brimoor
Date: Thu, 5 Dec 2024 12:19:24 -0500
Subject: [PATCH 008/104] run pre-commit hooks
---
fiftyone/utils/cvat.py | 15 ++++++++++-----
1 file changed, 10 insertions(+), 5 deletions(-)
diff --git a/fiftyone/utils/cvat.py b/fiftyone/utils/cvat.py
index 9e7de7dd98..1966f2b390 100644
--- a/fiftyone/utils/cvat.py
+++ b/fiftyone/utils/cvat.py
@@ -3214,7 +3214,11 @@ def load_credentials(
self, url=None, username=None, password=None, email=None, headers=None
):
self._load_parameters(
- url=url, username=username, password=password, email=email, headers=headers
+ url=url,
+ username=username,
+ password=password,
+ email=email,
+ headers=headers,
)
@@ -3755,13 +3759,13 @@ def _setup(self):
self._server_version = Version("2")
try:
- self._login(username, password, email)
+ self._login(username, password, email=email)
except requests.exceptions.HTTPError as e:
if e.response.status_code != 404:
raise e
self._server_version = Version("1")
- self._login(username, password, email)
+ self._login(username, password, email=email)
self._add_referer()
self._add_organization()
@@ -3798,13 +3802,14 @@ def _add_organization(self):
def close(self):
self._session.close()
- def _login(self, username, password, email):
+ def _login(self, username, password, email=None):
payload = {
"username": username,
- "password": password,
+ "password": password,
}
if email is not None:
payload["email"] = email
+
response = self._make_request(
self._session.post,
self.login_url,
From a7cb7e566ee56e876d1d9f53605e75e5d986ab43 Mon Sep 17 00:00:00 2001
From: brimoor
Date: Thu, 5 Dec 2024 12:23:33 -0500
Subject: [PATCH 009/104] added release note for 5218
---
docs/source/release-notes.rst | 3 +++
1 file changed, 3 insertions(+)
diff --git a/docs/source/release-notes.rst b/docs/source/release-notes.rst
index d677d83fdd..1605878b8d 100644
--- a/docs/source/release-notes.rst
+++ b/docs/source/release-notes.rst
@@ -76,6 +76,9 @@ SDK
- Fixed a bug that prevented users with `pydantic` installed from loading the
:ref:`quickstart-3d dataset ` from the zoo
`#4994 `_
+- Added optional `email` parameter to the
+ :ref:`CVAT integration `
+ `#5218 `_
Brain
From b2bf811ada48096147bdb32e04523f1e8c95eed1 Mon Sep 17 00:00:00 2001
From: Daniel Bogdoll
Date: Thu, 5 Dec 2024 12:45:16 -0500
Subject: [PATCH 010/104] Fixed example for
zero-shot-detection-transformer-torch
---
docs/scripts/make_model_zoo_docs.py | 11 +++++++++++
1 file changed, 11 insertions(+)
diff --git a/docs/scripts/make_model_zoo_docs.py b/docs/scripts/make_model_zoo_docs.py
index 472e0b31db..67769c5e90 100644
--- a/docs/scripts/make_model_zoo_docs.py
+++ b/docs/scripts/make_model_zoo_docs.py
@@ -173,6 +173,17 @@
model = foz.load_zoo_model("{{ name }}")
embeddings = dataset.compute_embeddings(model)
+
+{% elif 'zero-shot-detection-transformer-torch' in name %}
+ model = foz.load_zoo_model(
+ "{{ name }}",
+ classes=["person", "dog", "cat", "bird", "car", "tree", "chair"]
+ )
+
+ dataset.apply_model(model, label_field="predictions")
+
+ session = fo.launch_app(dataset)
+
{% else %}
model = foz.load_zoo_model("{{ name }}")
From 8e85dd478be3f18874f08670f246cab901cb3cea Mon Sep 17 00:00:00 2001
From: brimoor
Date: Thu, 5 Dec 2024 13:03:19 -0500
Subject: [PATCH 011/104] lint
---
docs/scripts/make_model_zoo_docs.py | 6 ++----
1 file changed, 2 insertions(+), 4 deletions(-)
diff --git a/docs/scripts/make_model_zoo_docs.py b/docs/scripts/make_model_zoo_docs.py
index 67769c5e90..9775b9ccfb 100644
--- a/docs/scripts/make_model_zoo_docs.py
+++ b/docs/scripts/make_model_zoo_docs.py
@@ -173,17 +173,15 @@
model = foz.load_zoo_model("{{ name }}")
embeddings = dataset.compute_embeddings(model)
-
{% elif 'zero-shot-detection-transformer-torch' in name %}
model = foz.load_zoo_model(
- "{{ name }}",
- classes=["person", "dog", "cat", "bird", "car", "tree", "chair"]
+ "{{ name }}",
+ classes=["person", "dog", "cat", "bird", "car", "tree", "chair"],
)
dataset.apply_model(model, label_field="predictions")
session = fo.launch_app(dataset)
-
{% else %}
model = foz.load_zoo_model("{{ name }}")
From ec234cd8863eb2256e57b7255483155dd377b9bb Mon Sep 17 00:00:00 2001
From: brimoor
Date: Thu, 5 Dec 2024 13:18:14 -0500
Subject: [PATCH 012/104] also applies to zero shot classification
---
docs/scripts/make_model_zoo_docs.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docs/scripts/make_model_zoo_docs.py b/docs/scripts/make_model_zoo_docs.py
index 9775b9ccfb..3f6dd02f79 100644
--- a/docs/scripts/make_model_zoo_docs.py
+++ b/docs/scripts/make_model_zoo_docs.py
@@ -173,7 +173,7 @@
model = foz.load_zoo_model("{{ name }}")
embeddings = dataset.compute_embeddings(model)
-{% elif 'zero-shot-detection-transformer-torch' in name %}
+{% elif 'zero-shot' in name and 'transformer' in name %}
model = foz.load_zoo_model(
"{{ name }}",
classes=["person", "dog", "cat", "bird", "car", "tree", "chair"],
From c2e2d0fa97f1aab1820b2fa57ea034aa232a509b Mon Sep 17 00:00:00 2001
From: manivoxel51 <109545780+manivoxel51@users.noreply.github.com>
Date: Thu, 5 Dec 2024 11:01:41 -0800
Subject: [PATCH 013/104] Fixes treeSelection filtering of samples in group
(#5227)
---
.../core/src/plugins/SchemaIO/components/TreeSelectionView.tsx | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/app/packages/core/src/plugins/SchemaIO/components/TreeSelectionView.tsx b/app/packages/core/src/plugins/SchemaIO/components/TreeSelectionView.tsx
index e8c20164a1..d4765371a7 100644
--- a/app/packages/core/src/plugins/SchemaIO/components/TreeSelectionView.tsx
+++ b/app/packages/core/src/plugins/SchemaIO/components/TreeSelectionView.tsx
@@ -177,7 +177,8 @@ export default function TreeSelectionView(props: ViewPropsType) {
const selectedSampleIds = Object.keys(updatedState).filter((key) => {
const isSample =
!structure.some(([parentId]) => parentId === key) &&
- key !== "selectAll";
+ key !== "selectAll" &&
+ !Object.keys(updatedState[key])?.includes("indeterminate");
return isSample && updatedState[key].checked;
});
From c9baa5493c5dec0c36842c41d1eda9587d9107fa Mon Sep 17 00:00:00 2001
From: Sashank Aryal
Date: Thu, 5 Dec 2024 14:12:54 -0600
Subject: [PATCH 014/104] use custom getsize and center
---
.../looker-3d/src/fo3d/MediaTypeFo3d.tsx | 20 ++++++++++++-------
1 file changed, 13 insertions(+), 7 deletions(-)
diff --git a/app/packages/looker-3d/src/fo3d/MediaTypeFo3d.tsx b/app/packages/looker-3d/src/fo3d/MediaTypeFo3d.tsx
index c1da5f5a47..e829429c1d 100644
--- a/app/packages/looker-3d/src/fo3d/MediaTypeFo3d.tsx
+++ b/app/packages/looker-3d/src/fo3d/MediaTypeFo3d.tsx
@@ -388,7 +388,7 @@ export const MediaTypeFo3dComponent = () => {
return;
}
- const labelBoundingBoxes = [];
+ const labelBoundingBoxes: THREE.Box3[] = [];
for (const selectedLabel of currentSelectedLabels) {
const field = selectedLabel.field;
@@ -441,17 +441,23 @@ export const MediaTypeFo3dComponent = () => {
labelBoundingBoxes.push(thisLabelBoundingBox);
}
- const unionBoundingBox = labelBoundingBoxes[0].clone();
+ const unionBoundingBox: THREE.Box3 = labelBoundingBoxes[0];
for (let i = 1; i < labelBoundingBoxes.length; i++) {
unionBoundingBox.union(labelBoundingBoxes[i]);
}
- const unionBoundingBoxCenter = unionBoundingBox.getCenter(
- new THREE.Vector3()
- );
- const unionBoundingBoxSize = unionBoundingBox.getSize(
- new THREE.Vector3()
+ // center = (min + max) / 2
+ let unionBoundingBoxCenter = new Vector3();
+ unionBoundingBoxCenter = unionBoundingBoxCenter
+ .addVectors(unionBoundingBox.min, unionBoundingBox.max)
+ .multiplyScalar(0.5);
+
+ // size = max - min
+ let unionBoundingBoxSize = new Vector3();
+ unionBoundingBoxSize = unionBoundingBoxSize.subVectors(
+ unionBoundingBox.max,
+ unionBoundingBox.min
);
const maxSize = Math.max(
From 18892a53528a3e3f25b58d1a32e82891ca3553da Mon Sep 17 00:00:00 2001
From: Sashank Aryal
Date: Thu, 5 Dec 2024 14:20:51 -0600
Subject: [PATCH 015/104] use clone
---
app/packages/looker-3d/src/fo3d/MediaTypeFo3d.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/app/packages/looker-3d/src/fo3d/MediaTypeFo3d.tsx b/app/packages/looker-3d/src/fo3d/MediaTypeFo3d.tsx
index e829429c1d..c02c7ec2aa 100644
--- a/app/packages/looker-3d/src/fo3d/MediaTypeFo3d.tsx
+++ b/app/packages/looker-3d/src/fo3d/MediaTypeFo3d.tsx
@@ -441,7 +441,7 @@ export const MediaTypeFo3dComponent = () => {
labelBoundingBoxes.push(thisLabelBoundingBox);
}
- const unionBoundingBox: THREE.Box3 = labelBoundingBoxes[0];
+ const unionBoundingBox: THREE.Box3 = labelBoundingBoxes[0].clone();
for (let i = 1; i < labelBoundingBoxes.length; i++) {
unionBoundingBox.union(labelBoundingBoxes[i]);
From e2e92926e76beaba2f4ea31cf60c83ce47a78373 Mon Sep 17 00:00:00 2001
From: brimoor
Date: Thu, 5 Dec 2024 17:38:00 -0500
Subject: [PATCH 016/104] bump release date
---
docs/source/release-notes.rst | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/docs/source/release-notes.rst b/docs/source/release-notes.rst
index 1605878b8d..047e39e395 100644
--- a/docs/source/release-notes.rst
+++ b/docs/source/release-notes.rst
@@ -5,7 +5,7 @@ FiftyOne Release Notes
FiftyOne Teams 2.2.0
--------------------
-*Released December 5, 2024*
+*Released December 6, 2024*
Includes all updates from :ref:`FiftyOne 1.1.0 `, plus:
@@ -30,7 +30,7 @@ Includes all updates from :ref:`FiftyOne 1.1.0 `, plus:
FiftyOne 1.1.0
--------------
-*Released December 5, 2024*
+*Released December 6, 2024*
What's New
From e44f554a190d00e44fb75cf3e3fcd8a97f9332fb Mon Sep 17 00:00:00 2001
From: Brian Moore
Date: Fri, 6 Dec 2024 22:27:54 -0500
Subject: [PATCH 017/104] fix broken docs refs (#5238)
---
docs/source/release-notes.rst | 6 +++---
docs/source/teams/query_performance.rst | 2 +-
docs/source/user_guide/app.rst | 24 ++++++++++++------------
3 files changed, 16 insertions(+), 16 deletions(-)
diff --git a/docs/source/release-notes.rst b/docs/source/release-notes.rst
index 047e39e395..5fa058f8cf 100644
--- a/docs/source/release-notes.rst
+++ b/docs/source/release-notes.rst
@@ -36,7 +36,7 @@ What's New
- Added a :ref:`Model Evaluation panel ` for
visually and interactively evaluating models in the FiftyOne App
-- Introduced :ref:`Query Performance ` in the
+- Introduced :ref:`Query Performance ` in the
App, which automatically nudges you to create the necessary indexes to
greatly optimize queries on large datasets
- Added a :ref:`leaky splits method ` for automatically
@@ -169,7 +169,7 @@ Core
App
-- Added a new :ref:`TimelineView ` for
+- Added a new :class:`TimelineView ` for
building custom animations
`#4965 `_
- Fixed overlay z-index and overflow for panels
@@ -480,7 +480,7 @@ What's New
that allows users to build custom no-code dashboards that display statistics
of interest about the current dataset (and beyond)
- Added `Segment Anything 2 `_ to the
- :ref:`model zoo `!
+ :ref:`model zoo `!
`#4671 `_
- Added an :ref:`Elasticsearch integration ` for
native text and image searches on FiftyOne datasets!
diff --git a/docs/source/teams/query_performance.rst b/docs/source/teams/query_performance.rst
index 462ff71f7b..d16f92620f 100644
--- a/docs/source/teams/query_performance.rst
+++ b/docs/source/teams/query_performance.rst
@@ -106,7 +106,7 @@ field's filter widget and performing queries on it will be noticably faster.
before creating multiple indexes simultaneously.
You can also create and manage custom indexes
-:ref:`via the SDK `.
+:ref:`via the SDK `.
.. _query-performance-summary:
diff --git a/docs/source/user_guide/app.rst b/docs/source/user_guide/app.rst
index f1c89dc842..3db044fbf6 100644
--- a/docs/source/user_guide/app.rst
+++ b/docs/source/user_guide/app.rst
@@ -395,9 +395,9 @@ only those samples and/or labels that match the filter.
:alt: app-filters
:align: center
-.. _app-optimize-query-performance:
+.. _app-optimizing-query-performance:
-Optimizing query performance
+Optimizing Query Performance
----------------------------
The App's sidebar is optimized to leverage database indexes whenever possible.
@@ -491,8 +491,8 @@ perform initial filters on:
:ref:`App config `.
For :ref:`grouped datasets `, you should create two indexes for each
-field you wish to filter by in query performance mode: the field itself and a
-compound index that includes the group slice name:
+field you wish to filter by: the field itself and a compound index that
+includes the group slice name:
.. code-block:: python
:linenos:
@@ -542,27 +542,27 @@ field:
Numeric field filters are not supported by wildcard indexes.
-.. _app-disasbling-query-performance:
+.. _app-disabling-query-performance:
-Disabling query performance
+Disabling Query Performance
---------------------------
-Query performance is enabled by default for all datasets. This is generally the
-recommended setting for all large datasets to ensure that queries are
-performant.
+:ref:`Query Performance ` is enabled by
+default for all datasets. This is generally the recommended setting for all
+large datasets to ensure that queries are performant.
-However, in certain circumstances you may prefer to disable query performance,
+However, in certain circumstances you may prefer to disable Query Performance,
which enables the App's sidebar to show additional information such as
label/value counts that are useful but more expensive to compute.
-You can disable query performance for a particular dataset for its lifetime
+You can disable Query Performance for a particular dataset for its lifetime
(in your current browser) via the gear icon in the Samples panel's actions row:
.. image:: /images/app/app-query-performance-disabled.gif
:alt: app-query-performance-disabled
:align: center
-You can also disable query performance by default for all datasets by setting
+You can also disable Query Performance by default for all datasets by setting
`default_query_performance=False` in your
:ref:`App config `.
From 882055568b0fd6d83c4bcd01c5cc28eaceb16230 Mon Sep 17 00:00:00 2001
From: Brian Moore
Date: Sat, 7 Dec 2024 09:49:05 -0500
Subject: [PATCH 018/104] fix broken docs refs (#5238) (#5239)
---
docs/source/release-notes.rst | 6 +++---
docs/source/teams/query_performance.rst | 2 +-
docs/source/user_guide/app.rst | 24 ++++++++++++------------
3 files changed, 16 insertions(+), 16 deletions(-)
diff --git a/docs/source/release-notes.rst b/docs/source/release-notes.rst
index 047e39e395..5fa058f8cf 100644
--- a/docs/source/release-notes.rst
+++ b/docs/source/release-notes.rst
@@ -36,7 +36,7 @@ What's New
- Added a :ref:`Model Evaluation panel ` for
visually and interactively evaluating models in the FiftyOne App
-- Introduced :ref:`Query Performance ` in the
+- Introduced :ref:`Query Performance ` in the
App, which automatically nudges you to create the necessary indexes to
greatly optimize queries on large datasets
- Added a :ref:`leaky splits method ` for automatically
@@ -169,7 +169,7 @@ Core
App
-- Added a new :ref:`TimelineView ` for
+- Added a new :class:`TimelineView ` for
building custom animations
`#4965 `_
- Fixed overlay z-index and overflow for panels
@@ -480,7 +480,7 @@ What's New
that allows users to build custom no-code dashboards that display statistics
of interest about the current dataset (and beyond)
- Added `Segment Anything 2 `_ to the
- :ref:`model zoo `!
+ :ref:`model zoo `!
`#4671 `_
- Added an :ref:`Elasticsearch integration ` for
native text and image searches on FiftyOne datasets!
diff --git a/docs/source/teams/query_performance.rst b/docs/source/teams/query_performance.rst
index 462ff71f7b..d16f92620f 100644
--- a/docs/source/teams/query_performance.rst
+++ b/docs/source/teams/query_performance.rst
@@ -106,7 +106,7 @@ field's filter widget and performing queries on it will be noticably faster.
before creating multiple indexes simultaneously.
You can also create and manage custom indexes
-:ref:`via the SDK `.
+:ref:`via the SDK `.
.. _query-performance-summary:
diff --git a/docs/source/user_guide/app.rst b/docs/source/user_guide/app.rst
index f1c89dc842..3db044fbf6 100644
--- a/docs/source/user_guide/app.rst
+++ b/docs/source/user_guide/app.rst
@@ -395,9 +395,9 @@ only those samples and/or labels that match the filter.
:alt: app-filters
:align: center
-.. _app-optimize-query-performance:
+.. _app-optimizing-query-performance:
-Optimizing query performance
+Optimizing Query Performance
----------------------------
The App's sidebar is optimized to leverage database indexes whenever possible.
@@ -491,8 +491,8 @@ perform initial filters on:
:ref:`App config `.
For :ref:`grouped datasets `, you should create two indexes for each
-field you wish to filter by in query performance mode: the field itself and a
-compound index that includes the group slice name:
+field you wish to filter by: the field itself and a compound index that
+includes the group slice name:
.. code-block:: python
:linenos:
@@ -542,27 +542,27 @@ field:
Numeric field filters are not supported by wildcard indexes.
-.. _app-disasbling-query-performance:
+.. _app-disabling-query-performance:
-Disabling query performance
+Disabling Query Performance
---------------------------
-Query performance is enabled by default for all datasets. This is generally the
-recommended setting for all large datasets to ensure that queries are
-performant.
+:ref:`Query Performance ` is enabled by
+default for all datasets. This is generally the recommended setting for all
+large datasets to ensure that queries are performant.
-However, in certain circumstances you may prefer to disable query performance,
+However, in certain circumstances you may prefer to disable Query Performance,
which enables the App's sidebar to show additional information such as
label/value counts that are useful but more expensive to compute.
-You can disable query performance for a particular dataset for its lifetime
+You can disable Query Performance for a particular dataset for its lifetime
(in your current browser) via the gear icon in the Samples panel's actions row:
.. image:: /images/app/app-query-performance-disabled.gif
:alt: app-query-performance-disabled
:align: center
-You can also disable query performance by default for all datasets by setting
+You can also disable Query Performance by default for all datasets by setting
`default_query_performance=False` in your
:ref:`App config `.
From ca5c5577f5cede9108ec2d14ee5075af24122e2d Mon Sep 17 00:00:00 2001
From: Brian Moore
Date: Mon, 9 Dec 2024 09:30:55 -0500
Subject: [PATCH 019/104] include more brain modules in docs builds (#5242)
---
docs/generate_docs.bash | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docs/generate_docs.bash b/docs/generate_docs.bash
index 5c11b80dc4..076665e026 100755
--- a/docs/generate_docs.bash
+++ b/docs/generate_docs.bash
@@ -102,7 +102,7 @@ echo "Generating API docs"
sphinx-apidoc --force --no-toc --separate --follow-links \
--templatedir=docs/templates/apidoc \
-o docs/source/api fiftyone \
- fiftyone/brain/internal \
+ fiftyone/brain/internal/models \
fiftyone/server \
fiftyone/service \
fiftyone/management \
From f8d5d8bd64ca61b293ab025132ee8cbb8702f0a9 Mon Sep 17 00:00:00 2001
From: Jacob Marks
Date: Mon, 9 Dec 2024 10:04:55 -0500
Subject: [PATCH 020/104] fix initialization
---
fiftyone/utils/ultralytics.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/fiftyone/utils/ultralytics.py b/fiftyone/utils/ultralytics.py
index 6eafa7eab2..6ee7ea222b 100644
--- a/fiftyone/utils/ultralytics.py
+++ b/fiftyone/utils/ultralytics.py
@@ -467,7 +467,7 @@ class FiftyOneRTDETRModelConfig(FiftyOneYOLOModelConfig):
"""
def __init__(self, d):
- pass
+ super().__init__(d)
class FiftyOneRTDETRModel(Model):
From 517c21d60957525390d1c537d130eff7d51d11ed Mon Sep 17 00:00:00 2001
From: Rodrigo Agundez
Date: Fri, 6 Dec 2024 12:14:05 +0800
Subject: [PATCH 021/104] Update docs with addition of FIFTYONE_CVAT_EMAIL
---
docs/source/integrations/cvat.rst | 3 ++-
docs/source/plugins/developing_plugins.rst | 5 +++++
docs/source/plugins/using_plugins.rst | 4 ++++
docs/source/teams/secrets.rst | 2 ++
docs/source/tutorials/cvat_annotation.ipynb | 3 ++-
docs/source/user_guide/annotation.rst | 3 ++-
6 files changed, 17 insertions(+), 3 deletions(-)
diff --git a/docs/source/integrations/cvat.rst b/docs/source/integrations/cvat.rst
index ceab66776a..95337c7ab5 100644
--- a/docs/source/integrations/cvat.rst
+++ b/docs/source/integrations/cvat.rst
@@ -211,12 +211,13 @@ which can be done in a variety of ways.
The recommended way to configure your CVAT login credentials is to store them
in the `FIFTYONE_CVAT_USERNAME` and `FIFTYONE_CVAT_PASSWORD` environment
-variables. These are automatically accessed by FiftyOne whenever a connection
+variables. Optionally, you can also set the `FIFTYONE_CVAT_EMAIL` environment variable. These are automatically accessed by FiftyOne whenever a connection
to CVAT is made.
.. code-block:: shell
export FIFTYONE_CVAT_USERNAME=...
+ export FIFTYONE_CVAT_EMAIL=...
export FIFTYONE_CVAT_PASSWORD=...
**FiftyOne annotation config**
diff --git a/docs/source/plugins/developing_plugins.rst b/docs/source/plugins/developing_plugins.rst
index 7a44d352bc..472375388d 100644
--- a/docs/source/plugins/developing_plugins.rst
+++ b/docs/source/plugins/developing_plugins.rst
@@ -251,6 +251,7 @@ plugin's `fiftyone.yml` looks like this:
secrets:
- FIFTYONE_CVAT_URL
- FIFTYONE_CVAT_USERNAME
+ - FIFTYONE_CVAT_EMAIL
- FIFTYONE_CVAT_PASSWORD
- FIFTYONE_LABELBOX_URL
- FIFTYONE_LABELBOX_API_KEY
@@ -1527,6 +1528,7 @@ plugin declares the following secrets:
secrets:
- FIFTYONE_CVAT_URL
- FIFTYONE_CVAT_USERNAME
+ - FIFTYONE_CVAT_EMAIL
- FIFTYONE_CVAT_PASSWORD
- FIFTYONE_LABELBOX_URL
- FIFTYONE_LABELBOX_API_KEY
@@ -1543,6 +1545,7 @@ plugin, you would set:
FIFTYONE_CVAT_URL=...
FIFTYONE_CVAT_USERNAME=...
+ FIFTYONE_CVAT_EMAIL=...
FIFTYONE_CVAT_PASSWORD=...
At runtime, the plugin's :ref:`execution context `
@@ -1555,6 +1558,7 @@ plugin. Operators can access these secrets via the `ctx.secrets` dict:
def execute(self, ctx):
url = ctx.secrets["FIFTYONE_CVAT_URL"]
username = ctx.secrets["FIFTYONE_CVAT_USERNAME"]
+ email = ctx.secrets["FIFTYONE_CVAT_EMAIL"]
password = ctx.secrets["FIFTYONE_CVAT_PASSWORD"]
.. _operator-outputs:
@@ -2500,6 +2504,7 @@ plugin. Panels can access these secrets via the `ctx.secrets` dict:
def on_load(self, ctx):
url = ctx.secrets["FIFTYONE_CVAT_URL"]
username = ctx.secrets["FIFTYONE_CVAT_USERNAME"]
+ email = ctx.secrets["FIFTYONE_CVAT_EMAIL"]
password = ctx.secrets["FIFTYONE_CVAT_PASSWORD"]
.. _panel-common-patterns:
diff --git a/docs/source/plugins/using_plugins.rst b/docs/source/plugins/using_plugins.rst
index fc4adba6c8..3bb963b0aa 100644
--- a/docs/source/plugins/using_plugins.rst
+++ b/docs/source/plugins/using_plugins.rst
@@ -295,6 +295,7 @@ available metadata about a plugin:
server_path /plugins/fiftyone-plugins/plugins/annotation
secrets FIFTYONE_CVAT_URL
FIFTYONE_CVAT_USERNAME
+ FIFTYONE_CVAT_EMAIL
FIFTYONE_CVAT_PASSWORD
FIFTYONE_LABELBOX_URL
FIFTYONE_LABELBOX_API_KEY
@@ -468,6 +469,7 @@ plugin declares the following secrets:
secrets:
- FIFTYONE_CVAT_URL
- FIFTYONE_CVAT_USERNAME
+ - FIFTYONE_CVAT_EMAIL
- FIFTYONE_CVAT_PASSWORD
- FIFTYONE_LABELBOX_URL
- FIFTYONE_LABELBOX_API_KEY
@@ -490,6 +492,7 @@ plugin, you would set:
FIFTYONE_CVAT_URL=...
FIFTYONE_CVAT_USERNAME=...
+ FIFTYONE_CVAT_EMAIL=...
FIFTYONE_CVAT_PASSWORD=...
At runtime, the plugin's execution context will automatically be hydrated with
@@ -502,6 +505,7 @@ secrets via the `ctx.secrets` dict:
def execute(self, ctx):
url = ctx.secrets["FIFTYONE_CVAT_URL"]
username = ctx.secrets["FIFTYONE_CVAT_USERNAME"]
+ email = ctx.secrets["FIFTYONE_CVAT_EMAIL"]
password = ctx.secrets["FIFTYONE_CVAT_PASSWORD"]
.. _using-panels:
diff --git a/docs/source/teams/secrets.rst b/docs/source/teams/secrets.rst
index 5122320845..8574ebb2ca 100644
--- a/docs/source/teams/secrets.rst
+++ b/docs/source/teams/secrets.rst
@@ -66,6 +66,7 @@ plugin declares the following secrets:
secrets:
- FIFTYONE_CVAT_URL
- FIFTYONE_CVAT_USERNAME
+ - FIFTYONE_CVAT_EMAIL
- FIFTYONE_CVAT_PASSWORD
- FIFTYONE_LABELBOX_URL
- FIFTYONE_LABELBOX_API_KEY
@@ -82,6 +83,7 @@ secrets via the ``ctx.secrets`` dict:
def execute(self, ctx):
url = ctx.secrets["FIFTYONE_CVAT_URL"]
username = ctx.secrets["FIFTYONE_CVAT_USERNAME"]
+ email = ctx.secrets["FIFTYONE_CVAT_EMAIL"]
password = ctx.secrets["FIFTYONE_CVAT_PASSWORD"]
The ``ctx.secrets`` dict will also be automatically populated with the
diff --git a/docs/source/tutorials/cvat_annotation.ipynb b/docs/source/tutorials/cvat_annotation.ipynb
index 4246f7c3fb..c2d610c84a 100644
--- a/docs/source/tutorials/cvat_annotation.ipynb
+++ b/docs/source/tutorials/cvat_annotation.ipynb
@@ -70,6 +70,7 @@
"outputs": [],
"source": [
"!export FIFTYONE_CVAT_USERNAME=\n",
+ "!export FIFTYONE_CVAT_EMAIL= # optional\n",
"!export FIFTYONE_CVAT_PASSWORD="
]
},
@@ -421,7 +422,7 @@
},
{
"data": {
- "image/png": "iVBORw0KGgoAAAANSUhEUgAAA8YAAAHCCAYAAAAtn9D+AAAgAElEQVR4XuydB5QkVdn+n66OE3pmd2cnbd4lCahgQBAUQZEkKyCIKyCgqIsECSsgOeOKgGRYkqyIiAjCBwqfZBD1AyWI/pHgLmycvDPTPT2dqvp/bs1O6AnbVd0VbnU/dc4ePcyt9773earDr++97/XlcrkceFEBKkAFqAAVoAJUgApQASpABagAFahQBXwE4wp1nsOmAlSAClABKkAFqAAVoAJUgApQAV0BgjEfBCpABagAFaACVIAKUAEqQAWoABWoaAUIxhVtPwdPBagAFaACVIAKUAEqQAWoABWgAgRjPgNUgApQASpABagAFaACVIAKUAEqUNEKEIwr2n4OngpQASpABagAFaACVIAKUAEqQAUIxnwGqAAVoAJUgApQASpABagAFaACVKCiFSAYV7T9HDwVoAJUgApQASpABagAFaACVIAKEIz5DFABKkAFqAAVoAJUgApQASpABahARStAMK5o+zl4KkAFqAAVoAJUgApQASpABagAFSAY8xmgAlSAClABKkAFqAAVoAJUgApQgYpWgGBc0fZz8FSAClABKkAFqAAVoAJUgApQASpAMOYzQAWoABWgAlSAClABKkAFqAAVoAIVrQDBuKLt5+CpABWgAlSAClABKkAFqAAVoAJUgGDMZ4AKUAEqQAWoABWgAlSAClABKkAFKloBgnFF28/BUwEqQAWoABWgAlSAClABKkAFqADBmM8AFaACVIAKUAEqQAWoABWgAlSAClS0AgTjirafg6cCVIAKUAEqQAWoABWgAlSAClABgjGfASpABagAFaACVIAKUAEqQAWoABWoaAUIxhVtPwdPBagAFaACVIAKUAEqQAWoABWgAgRjPgNUgApQASpABagAFaACVIAKUAEqUNEKEIwr2n4OngpQASpABagAFaACVIAKUAEqQAUIxnwGqAAVoAJUgApQASpABagAFaACVKCiFSAYV7T9HDwVoAJUgApQASpABagAFaACVIAKEIz5DFABKkAFqAAVoAJUgApQASpABahARStAMK5o+zl4KkAFqAAVoAJUgApQASpABagAFSAY8xmgAlSAClABKkAFqAAVoAJUgApQgYpWgGBc0fZz8FSAClABKkAFqAAVoAJUgApQASpAMOYzQAWoABWgAlSAClAB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\n",
+ "image/png": "iVBORw0KGgoAAAANSUhEUgAAA8YAAAHCCAYAAAAtn9D+AAAgAElEQVR4XuydB5QkVdn+n66OE3pmd2cnbd4lCahgQBAUQZEkKyCIKyCgqIsECSsgOeOKgGRYkqyIiAjCBwqfZBD1AyWI/pHgLmycvDPTPT2dqvp/bs1O6AnbVd0VbnU/dc4ePcyt9773earDr++97/XlcrkceFEBKkAFqAAVoAJUgApQASpABagAFahQBXwE4wp1nsOmAlSAClABKkAFqAAVoAJUgApQAV0BgjEfBCpABagAFaACVIAKUAEqQAWoABWoaAUIxhVtPwdPBagAFaACVIAKUAEqQAWoABWgAgRjPgNUgApQASpABagAFaACVIAKUAEqUNEKEIwr2n4OngpQASpABagAFaACVIAKUAEqQAUIxnwGqAAVoAJUgApQASpABagAFaACVKCiFSAYV7T9HDwVoAJUgApQASpABagAFaACVIAKEIz5DFABKkAFqAAVoAJUgApQASpABahARStAMK5o+zl4KkAFqAAVoAJUgApQASpABagAFSAY8xmgAlSAClABKkAFqAAVoAJUgApQgYpWgGBc0fZz8FSAClABKkAFqAAVoAJUgApQASpAMOYzQAWoABWgAlSAClABKkAFqAAVoAIVrQDBuKLt5+CpABWgAlSAClABKkAFqAAVoAJUgGDMZ4AKUAEqQAWoABWgAlSAClABKkAFKloBgnFF28/BUwEqQAWoABWgAlSAClABKkAFqADBmM8AFaACVIAKUAEqQAWoABWgAlSAClS0AgTjirafg6cCVIAKUAEqQAWoABWgAlSAClABgjGfASpABagAFaACVIAKUAEqQAWoABWoaAUIxhVtPwdPBagAFaACVIAKUAEqQAWoABWgAgRjPgNUgApQASpABagAFaACVIAKUAEqUNEKEIwr2n4OngpQASpABagAFaACVIAKUAEqQAUIxnwGqAAVoAJUgApQASpABagAFaACVKCiFSAYV7T9HDwVoAJUgApQASpABagAFaACVIAKEIz5DFABKkAFqAAVoAJUgApQASpABahARStAMK5o+zl4KkAFqAAVoAJUgApQASpABagAFSAY8xmgAlSAClABKkAFqAAVoAJUgApQgYpWgGBc0fZz8FSAClABKkAFqAAVoAJUgApQASpAMOYzQAWoABWgAlSAClABKkAFqAAVoAIVrQDBuKLt5+CpABWgAlSAClABKkAFqAAVoAJUQDow7unpyXMlFApB/IvH43TLZQUURUFdXR16e3tdzoTdCwWmT5+ue5HL5SiIywrU1tYinU7r/3i5q0AkEoF4r0okEu4mwt4RCARQXV2N/v5+qiGBAjNmzMD471gSpFWRKYjvUuI9KpvNVuT4ZRq0eI/SNA3JZNLRtOrr6+H3+x3tk50VVoBgXFgjttisAMFYrkeBYCyPHwRjebwgGMvjBcFYHi9EJgRjefwgGMvjBcFYHi9kyIRgLIMLHsmBYCyXUQRjefwgGMvjBcFYHi8IxvJ4QTCWywuCsTx+EIzl8UKGTAjGMrjgkRwIxnIZRTCWxw+CsTxeEIzl8YJgLI8XBGO5vCAYy+MHwVgeL2TIhGAsgwseyYFgLJdRBGN5/CAYy+MFwVgeLwjG8nhBMJbLC4KxPH4QjOXxQoZMCMYyuOCRHAjGchlFMJbHD4KxPF4QjOXxgmAsjxcEY7m8IBjL4wfBWB4vZMiEYCyDCx7JgWAsl1EEY3n8IBjL4wXBWB4vCMbyeEEwlssLgrE8fhCM5fFChkwIxjK44JEcCMZyGUUwlscPgrE8XhCM5fGCYCyPFwRjubwgGMvjB8FYHi9kyIRgLIMLHsmBYCyXUQRjefwgGMvjBcFYHi8IxvJ4QTCWywuCsTx+VDoYxxIq3l03iH+8G8OshhC2mVON7eZWyWOQw5kQjB0W3MvdEYzlco9gLI8fBGN5vCAYy+MFwVgeLwjGcnlBMJbHj0oG4/uf6cCKxzYgPqjmGfKpbaO46LgFOihX2kUwrjTHSxgvwbgE8Wy4lWBsg6hFhiQYFymcDbcRjG0QtciQBOMihbPpthkzZqCnp8em6AxrRgGCsRm17G1bqWB88T0f4PG/dk8pbm2VHyuWbWfr7PEBBxyAa665BjvssIOex/vvv4/vfve7eP755+01fQvRCcauSe+9jgnGcnlGMJbHD4KxPF4QjOXxgmAsjxciE4KxPH4QjOXxohLB+Pk3evGjW/9b0IRt51Th1xcMQasdF8HYgKrjf80MhUIQ/+LxuIG72cROBQjGdqprPjbB2Lxmdt1BMLZLWfNxCcbmNbPrDoKxXcoWF5dgXJxudtxFMLZD1eJiViIYLz73LWzsThsS7KJjF2Dx7g2G2k7W6C9/+QtuuOEGbNiwQZ8ZPuecczB//nxccskleOyxxyA+J3w+H5YuXYrPfe5zOP7443HyySfjvvvuQ39/P4455hgcd9xxeuh0Oo2bb74ZTz31FDKZDPbff3+ceuqpeowHHngAL730EhYtWoQ//vGP2H333XHppZeazpszxqYlq9wbCMZyeU8wlscPgrE8XhCM5fGCYCyPFyITgrE8fhCM5fGi0sBYFNva+/Q3DBuw5ItN+NE35hpuP7bhunXrcPTRR+Oqq67Cxz/+cTz00EN48MEH9X/BYBCTzRgvWbIE4t/BBx+M9vZ2LFu2DI888ghaW1tx9dVX64AtoDqXy+l/22+//XD44YfrYHzdddfpYP35z38eNTU1mDNnjum8CcamJavcGwjGcnlPMJbHD4KxPF4QjOXxgmAsjxcEY7m8IBjL40elgfHf34nhhGvfNWzAJ7epxe0/2s5w+7EN77rrLqxevRqXX375yH9evHgxLrzwQuyyyy6TgvH4PcZf+9rXcNZZZ2HXXXfVgVdAtYBkcYmZ4yeeeALXXnutDsavvPKKvme5lItgXIp6FXYvwVguwwnG8vhBMJbHC4KxPF4QjOXxgmAslxcEY3n8qDQwdnLG+Cc/+Qmi0ai+NHr4EuB7yCGH4KCDDjIExmLGWSyz3nHHHbHvvvuipaVlJJaqqvqy7Ntuu41gLM9LqnIyIRjL5TXBWB4/CMbyeEEwlscLgrE8XhCM5fKCYCyPH5UGxkL5g855C2099u8xvvPOO/HBBx9MmDG+4IIL8JnPfEaH45/+9Kc69IprsqrUw2C8xx57QPx7/PHH0dAwcc8zZ4zleU1VTCYEY7msJhjL4wfBWB4vCMbyeEEwlscLgrFcXhCM5fGjEsHYaFXqbeZU4f4SqlKvXbtW32Ms4HfnnXfW9xj/9re/xe9+9zt9j7HYDyyKZIk9xaKwVnd394TjmobBWCyjFkuyRRuxt1jAsVimvWbNGr0IF8FYntdUxWRCMJbLaoKxPH4QjOXxgmAsjxcEY3m8IBjL5QXBWB4/KhGMhfpOnWP85z//Wa9KvXHjRmy//fZ6VeqFCxfqD8Crr76qF9Iahl0Bz+P3GI8F42QyqS+bfvrpp7Fp0ybMnTtXB28x80wwluc1VTGZEIzlsppgLI8fBGN5vCAYy+MFwVgeLwjGcnlBMJbHj0oFY+HAr5/pwO2PbUB8UM0zRBTcuvjbCzGrISSPUQ5lwuJbDgldDt0QjOVykWAsjx8EYwu9yGag9XYBkRooNVHA5zMVnGBsSi5bGxOMbZXXdHAe12RaMttuIBjbJq3pwJUMxkIsUYzrnbUJ/OPdGGY1hLHt3GpsN7fKtI7lcgPBuFycdGAcBGMHRDbRBcHYhFg2NyUYWyNw6onfIPnLq6HF+vSAgUXbo/qiO+BvaDbcAcHYsFS2NyQY2y6xqQ4IxqbksrUxwdhWeU0Fr3QwNiVWBTQmGFeAyVYNkWBslZLWxCEYW6OjFVEqEYy1ng7Ezjka2oYP4PMpqD7/VgQ/sQd8wSKXXmUz6F/6Zajt6/MsqTnnRgR32Ru+UNiQVU6AsZaII9fdBtROh79+OqAohnKrtEYEY7kcJxjL4wfBWB4vCMbyeCFDJgRjGVzwSA4EY7mMIhjL40fFgXEuh77v7g1tHMTW3/IElLlbmV7+LJxUuzYituyIIeAcc4UPPAqRb50OJVpvyHC7wXjwrp8g9eQDyA0O6Pn4F2yHmkvuMjWrbWggZdCIYCyXiQRjefwgGMvjBcFYHi9kyIRgLIMLHsmBYCyXUQRjefyoNDDOdaxH/5lLoI2D2OrvnI3g/kugVNeaNkfMwsZP/grUjg35M8YnXIjAl74GparGUEw7wXiqHKMX3wn/Tp8tfrbc0Mi814hgLJdnBGN5/CAYy+MFwVgeL2TIJA+MxXlQouz1k08+iWg0OiG/VCqFU045Rf/vt99++5T533jjjVi5cmXe33fbbTfcdNNNBcfc09OT1yYUCkH8i8fjBe9lA3sVIBjbq6/Z6ARjs4rZ155gPKRtKWAs7h+4/AfI/ONF5DLpoYA+H+rvfAZK81zD5tkJxura/yJ+/rHQutvz8qlacjLCXzseviJ+EDA8MA82JBjLZRrBWB4/CMbyeEEwlscLGTIZAeMTTzwR7733nn4u1HPPPTcBjFVVxdlnn43Ozk6Ew+GCYCwA94wzzhgZo/iArKoqXOWMYCzDYzF5DgRjubwhGMvjR6WBMWxYSj3sZua9t5D9+wtQmmYhtPu+8FWZm322E4xz8X70n7IYWue4We0f34DgZ75oeB+0PE+uvZkQjO3V12x0grFZxexrTzC2T1uzkQnGZhUr7/Z5M8axWAx77733pGB8+eWX67C8aNEiPPbYYwXBWMQ699xzTatHMDYtmWM3EIwdk9pQRwRjQzI50qjiwBiA5cW3LHLKTjAWKQ5cdgIyr700Mqvtq6lD3U2PQ2lstWgE5ROGYCyXlwRjefwgGMvjRaWDsTgFIv3+vzD42ssItM5FeOuPIrTtx+QxyOFMDIGxWALd1dWFiy66CI8//rghMH7ggQcgviw2NDTggAMO0JdoG7kIxkZUcqcNwdgd3afqlWAsjx+VCMbyqJ+fid1gLHpT17wP9d+vwtc6H4EdPiXtTHEipWIwnUPQ70NtRIGimDsTulSPCcalKmjt/QRja/UsJRrBuBT1rL23ksG474Hb0HPXT6HF+/NErfrEHmg6/yYEWudZK7YHohUEYwHCTz/9NK6++mqIDzkxW1xoxnjt2rUQS6/Fkut33nkHV1xxBZYuXYrDDz98RBKxdDuXy+VJdOuttyKTyeT9NwFjPp9Pj8fLXQWED36/H9ls1t1E2LuuQDAY1L0Y/zqiPM4rIF4XwgdN05zvnD3yM2OSZ+DD9iQGU6OfsT4fsKg1gmDAOTjmZ4ZcL07xmTH+O5ZcGVZONuL7tPhey89v9z136/Nb9CsYx62r4/KTEfvj/VN2r9TWYfZN/+Pq7PFnP/tZXHfdddh1110dk6kgGN9www349a9/rcOpuMQXP/FiFm+wzz77rKF9w6JQ15tvvombb755ZGB///vfJ7wh7LLLLhBLsMde4s1D9DU4OOiYKOxocgXEM1BTU8NCaJI8IGJrgyhKxw9W9w0R9RPEF07+aOS+F6JYo3ivEsUiK/XKqjl82J6G+N+xV8uMAOqqA6KmmSOX+OInfiBPJBKT9pf6v2cQu/YsaH09CO30WUTPvg7+GU2O5FaJnYjPjPHfsSpRBxnGLGYpxXsUJ33cd0O8R4nvUen05qKPDqUkngHxHunGNfDiH9D242MKdh3a5qOYu/KFgu3saiBqX82ePRtCK6eugmA8PhEjM8bj7xFw3dbWhiuvvLLguLiUuqBErjXgUmrXpJ+0Yy6llscPLqWWxwsnllLLM9rJM0lnc1jflUEmmw/GM6IKZtQF4dSK6i0tpc7++1Uklp8KdVPnyCCU5tmIXvUbKA0tskvsyfy4lFoe27iUWh4vKnEp9Ydf2xnZtrWGTGg670ZEv3KkobbjG7311ls466yz8MQTT4z86bDDDsM555yDT3/60/rfxJZbwYivvfYaFixYgJ/85CeYNWuW3n7fffeFOOlou+22QzKZ1FcvP/PMM/oPCtOmTcNHPvIRiBpYhfoRP3qIydmnnnpKn8TYf//9ceqpp+orocdfJYOxmEE+6aSTcOyxx0IcySSu5cuXY5999sHChQvx9ttv44ILLtD3J++1114FhSUYF5TItQYEY9ekJxjLJf2EbAjG7hikaTnEBjV9ZjRa7Uco4APBWKzsyuGD9syEGeM5M4OoCvtGVoDZ7dqWwDh22iHIrnpbJJuXRv1dz0FpnmN3ahUZn2Asj+0EY3m8qDQwFsW2Vu+3yLAB9UcsxczTCk9sThawELAKMH733Xf1o4C32WYbXHvttRDvUxdeeOEEMP7Zz34Gcazw+eefrwOtqH8lGNQIGAug3rBhAy655BJ9dcCyZcuw33775W3xHc5/BIyPOuoobNy4Ef39/RAv2Hnz5uGee+6ZMM7xM8Zi6aAAYgG+ixcv1tuLBF544QV0d3ejublZL7wlfiEwchGMjajkThuCsTu6T9UrZ4zl8YNg7LwXGTWHtR358NdQH0DrzBoE/MqUy3edz9SdHnv6s+iJqdA2TxpXRxS0TA8g4HdoHTWgf3kRXzrF94rxV/9JB+pFzMTRX2Ovujufhb/F+LnV7qjrzV4JxvL4RjCWx4tKA+PB1/6MDScfbNiAyM67Y/YtjxluP7ahETDeeeedceSRQzPSTz75JETx5l/84hd5YLztttviC1/4Au6++25svfXW+t8Eo77//vsFwfhTn/oUPv/5z+PBBx9Ea+vQ6RFi5ljMYgsQH3/lzRgXNWqLbyIYWyyoheEIxhaKaUEogrEFIloUgmBskZAmwog9tKlMPlSJ27dfUIdwyF/xYCy0EL+Mi9l0v+JzvCK16H9LYJx++Ukkbr4Quf5NI64rVTWI3vxH/QxrXtYrQDC2XtNiIxKMi1XO+vsqDYxlmzEeC8YvvfQSVqxYgV/96ld5YNzY2Kgvq/7zn/+srwozA8bimGFxb0vL6BYdsbd//vz5uO222wjG1r+kKiciwVgurwnG8vhBMHbei9Vt6Ql7aEUWH5lfh0i4MBjnUoNIrLgUmdf+jOBu+6D6qNPgi9Y7P5Ay7rHQcU3J361A8rcrkEvEoExvRO1V90NpmefYUu8yln7SoRGM5XGcYCyPF5UGxkL5D7+2E7Jt6wyZUMoe4//85z8QpxCJYs3D1/g9xkbAeKuttoKoUP3QQw/pK5rHg/GW+vnkJz+JPfbYQz9uWOxnLnRxxriQQvz7iAIEY7keBoKxPH4QjJ33oq0ni9igOO4kv28jM8a5bAaxpV+G2r5+9P2tbjqi1z8KpXFoqRWv0hUoBMal98AIZhQgGJtRy962BGN79TUTvRLB2HBV6q13xNxfvmhGzry2ogq+mK0V223FcmixfPnee+/V9wcPF98yAsai+NYPf/hDfWvOD37wA4hjgUVhZxFT7DEu1I9oI7b3ir3FAo7FXuU1a9boRbjGXwTjou2uvBsJxnJ5TjCWxw+CsTteiAJTmaw2AseiuNT0+iq9YuVURwSJTNMvPo7B2y6FNmYZr/jvdTc+BmX+tvC5eLakO0ra0yvB2B5di41KMC5WOevvIxhbr2mxESsRjIVWHZedhNgTv5lSNqUmitk3P1byOcYPP/wwbrnlFv3o3UMPPRR/+MMf9KLMZsFYVK4WRbnE7PDHPvYxvYaVqDYtoFdcW+pHVLQWy6affvppbNq0CXPnztXrXx100EEE42JfOLwP+kHk4s28t7eXckigAMFYAhM2p0Awds8LVRvaSyv20YrzeY1UpR58+A6kHrgVuYFYXuLRnz2AwLY7AS6dLemeivb0TDC2R9dioxKMi1XO+vsIxtZrWmzESgVjoVffA7ei586fQhv3WSgKbjVfcDMCrUPLlmW8xhbfsjI/zhhbqWaZxyIYy2UwwVgePwjG8nhhBIzVtf9F/PxjoXW35yVed8fT8LfOl2cwHs+EYCyXgQRjefwgGMvjRSWDsXBBFONKvfcWBl97GcHWuQhv87GSZ4mdcJdgHI87oTP72IICBGO5Hg+CsTx+EIzl8cIIGItskyuvRvLxXyE3OKAnX33qlQjteRB84Sp5BuPxTAjGchlIMJbHD4KxPF5UOhjL44S5TAjGBGNzT4wNrQnGNohaQkiCcQniWXwrwdhiQUsIZxSMRRc5VQXSySEY9sC+YrVtLQbOPxZq2zr4gkHUXHIXgjt+GvAHSlDMvlsJxvZpW0xkgnExqtlzD8HYHl2LiUowLka18r2HS6nL11vLR0YwtlzSkgISjEuSz9KbCcaWyllSMDNgXFJHTt+czaB/XCVtkUL9Hc9AkXQfGMHY6Ydky/0RjOXxg2AsjxcEY3m8kCETgrEMLngkB4KxXEYRjOXxg2AsjxflCsaZN/+GxNWnQ9vUlSd27YW3I/CJPeALhuQxYXMmBGO5LCEYy+MHwVgeLwjG8nghQyYEYxlc8EgOBGO5jCIYy+MHwVgeLwjG8nhBMJbHC5EJwVgePwjG8nhBMJbHCxkyIRjL4IJHciAYy2UUwVgePwjG8nhRrmAMLqWW5yHzaCYEY3mMIxjL4wXBWB4vZMiEYCyDCx7JgWAsl1EEY3n8IBjL40XZgjEAFt+S5znzYiYEY3lcIxjL4wXBWB4vZMiEYCyDCx7JgWAsl1EEY3n8IBjL40U5g/EWVdY0DNx4HjIv/gEIhlFzzg0I7Php+AJB18zhUmrXpJ+0YyvBWNVyUHw++HxyjdEr2RCM5XGKYCyPFzJkQjCWwQWP5EAwlssogrE8fhCM5fGiUsE4dvJXkP3wPSCXGzEjev2jCCz8iGtHURGM5XldiEysAONEUkV7r4pMdug5Cwd9aG0IIhQgIZtxm2BsRi172xKM7dXXa9EJxl5zzMV8CcYuij9J1wRjefwgGMvjRSWCsTY4gNhJB0Lr2JBnRPjAo1B1zBnw1da5YhDB2BXZp+zUCjBe05FBMq3l9dFQp2BabQB+hXBs1HGCsVGl7G9HMLZfYy/1QDD2klsu50owdtmAcd0TjOXxg2AsjxeVCMYYiKHv5IOgdeaDcWjvg1H9/fPhi05zxSCCsSuy2wbGYvn0us4MUpnRVQmis1DQh1mcNTZlNsHYlFy2NiYY2yqv54ITjD1nmXsJE4zd036yngnG8vhBMJbHi4oEYwB939kLWsf6PCNqLr4DoZ33AFzaZ0wwlud1ITIpdcZYrNJf2zlxxri2yo+maX4E/JwxNuo4wdioUva3Ixjbr7GXeiAYe8ktl3MlGLtsAGeM5TJgTDYEY3msKQWMc7kc1nRmkdq8VLQ6rKBlRsATX/jV7jbElh2BXHebvs84fPC3UXXkKfDVRF0zh2DsmvSTdlwqGIugbZuyiA9q0LTRWeM5jQFUh/1yDVbybAjG8hhEMJbHCxkyIRjL4IJHciAYy2UUZ4zl8YNgLI8XpYDxhx2ZESgeHtG0Wj8a6vze2T85XHxLgnLBBGN5XhdWzBgPj2YgqWFTPAu/z4eGej9CAUWugXogG4KxPCYRjOXxQoZMCMYyuOCRHAjGchlFMJbHD4KxfV6IeSkzCzRLAePVbRlksvmFhcTIFraEEGTVXdMmE4xNS2brDVbMGNuaYAUFJxjLYzbBWB4vZMiEYCyDCx7JgWAsl1EEY3n8IBhb64VYpilmb7NqTj99KBxUMHumsSXNpYFxeuQYmuERiYnXBc0E42IcJhgXo5p99xCM7dPWbGSCsVnF7GtPMLZPWy9GJhh70TWXciYYuyT8FN0SjOXxg2BsrRcCitMZbeyRvKir8aOxXhwJs+W+SgHj9k1Z9CfUvH6bpgVQV61A4VE0pk0mGJuWzNYbCMa2ymsqOMHYlFy2NiYY2yqv54ITjD1nmXsJE4zd036yngnG8vhBMLbWi1KWNJcCxmIUPf1Z9MRUiCXcAsQJxcV7SzAuXjs77iQY26FqcTEJxsXpZsddBGM7VPVuTIKxd71zPHOCseOSb7FDgrE8fhCMrfViddvEJc2KAsxvCiJYoNBPqWBs7UgqOxrBWC7/Ccby+EEwlscLgrE8XsiQCcFYBhc8kgPBWC6jCMby+EEwttaLrr4segfyj4RpnRGAOC+1ULFlr4Jxb3xozGKpePO0IEJBMyXHrNXfqmgEY6uUtCYOwdgaHa2IQjC2QkVrYhCMrdGxXKIQjMvFSQfGQTB2QGQTXRCMTYhlc1OCsfUC9w2oQ0uac0DjND9qI4WhWGThRTBe15lGIjV6LqwYx/ymAMIhb58NSzC2/nVRSkSCcSnqWXsvwdhaPUuJRjAuRb3yu5dgXH6e2jYigrFt0hYVmGBclGy23EQw3rKs6acfxuCvrgVUFVUnXorgp/aELxS2xQuvgbEA/w/aJx4TNSPqx/Soh85PnsRNgrEtj3jRQQnGRUtn+Y0EY8slLTogwbho6cryRoJxWdpqz6AIxvboWmxUgnGxyll/H8F4ak0H77gS6T/9FtrgwEijmrN+juCu+8AXjlhuhtfAWBVHU7UPHU019opW+yEqYheqwm25gBYGJBhbKKYFoQjGxYuYSGpo782OHOfWMiOI2oiv6Gr1BOPivbD6ToKx1Yp6Ox7B2Nv+OZo9wdhRuQt2RjAuKJFjDQjGk0itacipKvqX7gOtY0NeAyVShegtT0JpmmW5R14DYyHAZMXGWhsChpePWy6iRQEJxhYJaVEYr4JxJqtBgKnfr6A6osDpk9PEj1frOjNIZfJ/vJrbFERVqMD5cVN4RzC26KG2IAzB2AIRyygEwbiMzLR7KARjuxU2F59gbE4vO1sTjPPVHVh+KjKvPINcOjWl7PV3Pw+labbltngRjAfTKjZ2qyOzxtVhH8SMVMDv7QJcBGPLH++SAnoRjDv7MugfyEHA6fA1tzGIqnBxQFqMgPFBFZ29KjLjVnXMagiiJqIULAg4WZ8E42KcsOcegrE9uno1KsHYq865kDfB2AXRt9AlwVgePwjGo16kn/k9Bu+8Elqsd0qDwnt9FZHvnw+lbhgAbO0AACAASURBVLrlJnoRjIdF0LQcfIoP3sbhUUsJxpY/3iUF9CIYr+nIIJnW8sY9o86P6bXObTNIJHObl1Hn5zF7ZgDVYQHG5l+xBOOSHmVLbyYYWyqn54MRjD1voXMDIBg7p7WRngjGRlRypg3BeFTn2OmHQV31b30Z9WSXf/42qL30F1Aamm0xx8tgbIsgLgYlGLso/iRdew2MVQ2blzDnA2lNlYKmen/BM82tUj+Xy2FtZ3YCoM9rCiLCpdRWyexaHIKxa9JL2THBWEpb5EyKYCyXLwTjyf0YX8woGPBBLL2zc1kqwXjUi6mWUUdXPIXArHmAz94lkARjed6nCMbyeCEy8RoYi5wnmzGeWedHfa2zFdtFcTxRfEvsdQ4FfGieHigaisW4OGMsz2uDYCyPFzJkQjCWwQWP5EAwlssogvHkfgwffSOOwRm+aiMKmqYHbINjgvGo1lpfD2KnHgKta+PIf/Rv83HUXHAr/DOabH8REYxtl9hwBwRjw1I50tCLYBxLaOjqy47s7/X7fZgzM4hw0PzyZUdENtgJwdigUA40Ixg7ILKHuiAYe8gst1MlGLvtQH7/BOPJ/VjdNvFMWNFyYUsIYvbYjotgnK+qgOPEzRdCe++fCO23BKGvHgOlutYO6SfEJBg7IrOhTgjGhmRyrJEXwViII37kFIWvRDVqO1f+OGYEZ4ydlLpgXwTjghJVVAOCcUXZXdpgCcal6Wf13QTjqcA4PXLW5HALcRas2A8WDNizjJdgbPXTXXw8gnHx2ll9J8HYakVLi+dVMC5t1KN3i202Ykm0+BgQRz+5eXHG2E318/smGMvjhQyZEIxlcMEjORCM5TKKYDy5H939WWyKq9DG1GuZ1RBATcRf1LEaRlwnGBtRyZk2BGNndDbSC8HYiErOtalkMN7Yk0F8UNNnn8XldAGv8S4TjJ177gv1RDAupFBl/Z1gXFl+lzRagnFJ8ll+M8F4akkHUjl092Uhzr2ZGVX0My9Hj9QQ34ysXVJNMLb88S46YKWBsZaII3nvz6G+8ybCByxB8PMHwhepLlo/K28kGFupZumxKhWM0xkNG3qySGfGFJ4AMEcctxTxly5sEREIxkWIZtMtBGObhPVo2JLA+PHHH8d9992H+++/f9Lhp1IpnHLKKfrfbr/9dkMS9fT05LULhUIQ/+LxuKH72cg+BQjG9mlbTGSCsTnVBq77MbIv/RFaahC+2npEr30ISuu8os6gHN8zwdicF3a2riQwziVi6D/pK9A6RwudBT6xB2qWXQNlWoOdMhuKTTA2JJNjjSoVjGMJFV196kgBr2HBZ80Mokb/0dQxC0Y6Ihg7r/lUPRKM5fFChkyKAuPOzk5873vfQ29vL1pbWycFY1VVcfbZZ0O0DYfDBGMZ3C4xB4JxiQJafDvB2LigyQdvQ/LB2yFAYvgScFx342NQGluNB5qipRNgLJaGr+9KYzCdg9ge19oQRHXY3X1yJQtnQ4BKAuPENWci/ZcnkUsl85Ssu/NZ+Fvm2qCuuZAEY3N62d26UsE4lclhoz5jnH8e8pxG995DCcZ2P+3G4xOMjWtVCS2LAuNhYZ5//nmsWLFiUjC+/PLLEY1GsWjRIjz22GME4zJ4mgjGcplIMDbuR9/xe0PrWD9U3nTMVX/3c1Ca5hgP5CIYr26bWFRsfrM4toRwPNYWAcZIJzHw4fvw1dQPzZwq5alR7PSvIfvffyNvQ72oeLviKfhnLyj5uS41AMG4VAWtvb9SwViouLYzjcHU6Pu/OO5J/LgoziR24yIYu6H65H0SjOXxQoZMbAHjm266CV1dXbjooosgllsTjGWwuvQcCMala2hlBIKxcTX7l34Z6voPJtzgFTDOZHNY15mZsBRwZr0f02r9UNxYC2hcfkdbqr+/E/Hf3AptoF/v19c4C3VX/xZKQ7OjeTjRWfLxXyH5q58jFx8a6/BVf9dzUJpL/8Gn1DG4AcZiVYjW3Q6lvgGorYevTH8UKcabSgZjoZeYOU6mNYT8QCRsXzFGI94QjI2o5EwbgrEzOnulF8vBWIDw008/jauvvhriQ1FA8WRgfMABByA3bvbmySef9IpuzJMKUAEPKZB45Xl0XLIU6qaukayrPvk5NF92J/zTG6UfiVgCuKY9NQGMG6eHMCMa0M/35AXk0imsWfIZZNvW5cnRfPEK1HzhIPjCkbKTacMPD8Xg638B1Kw+tpYrfoHqPfaDLxQuu7EWGlDHZSch/uwjI0vLI5/YHS2X3+2J13ihsfHvVIAKlJcCYsup3+9O8bfyUtLa0VgOxjfccAN+/etfjxS00TQNwvxgMIhnn30WVVVV+gjEjPL4a+bMmWDxLWsNtjIaZ4ytVLP0WJwxNqdh5vWXkbj+x/psUnj/JYgcswxKtN5ckClaO7HHeHVbBpls/h45LqXON0Tr2oj4j74BtWu0GJVoET7wSES+dYZlflvy0FgYJJdOAmKfcXUtfP6AhZFLC+XkjHGuuw39Z3wdWndbXtLRn/8ega12KNvl9GYcqvQZ40Ja9cZV9MTUobOO/aKOQwBVYnrZhoszxjaIWmRIzhgXKVyZ3mY5GI/XaaoZ46n0JBjL+6QRjOXyhmAsjx9OgLGYNV7flR2ZNW6ZHkC02t3lgPI4MJSJlkwgftKBUNvX56VWfdJlCO79VSiSHGMkm2525eMkGGf+72kkbjgPWl/+yRa159yAwC5frMgZ9PG+EoynftIHkhraN2V1KB6+/IoPs2cGEAlZX6OAYGzXu475uARj85qV8x0E43J21+KxEYwtFrTEcATjEgW08HYnwNjCdMs6VPKGc5F68XFoyUF9nL5gGHW3/S+U5tllPW4ZB+ckGKvrVyN+7rf0FSFjr+jVv0Vgm48Ddi9ZzOUw+NAdyDz1OyizFiDy3XMRmDUfrpwFNMXDQDCe+lWytiONZDqH/PKMwNymIKoIxjK+vViWE8HYMinLIlBRYNzW1oajjjoKmUwGyWRSrz69ePFinHbaaRNE4YxxWTwn+iAIxnJ5STCWxw+CsTxeiKrU2sYPEfubqDg+C4Edd4EvMrSFh5d1CiRSqn50WNDvQ21EgTLJRncnwViMLHb2kVDfeR257NB+a9/MVtRd86Ajhddiyw5H9t1/5lW+j173ewS2/qh1opcYyatg3DegYlNc1Q8VaJoW0I+ps7re4Piq1cNSE4xLfOg8cDvB2AMmOZhiUWBsZ35cSm2nuqXFJhiXpp/VdxOMrVa0+HgE4+K1s/rOSjrH2GrtjMYbDxECUhY0hxAcd/SN02As8tdWvY30268hOH9b+LfbCQiGjA6r6Hbapk7EzzsW6pr38mJUf/98hPY5DL7q2qJjW3mjF8G4ozeL/oSadyLZrJlB1FgMx4MpFW2bVIgTAIYv8TzPagjYciQel1Jb+WSXFotgXJp+5XY3wbjcHLVxPARjG8UtIjTBuAjRbLqFYGyTsEWEJRgXIZqJW8QezDUdmby9mOL2lhkBRKvy97y7AcYmhmJZU30Z92UnQFu3Ki9m5OvfR/iQ46HUz7Csr1ICeRGMxbMmjlgae4nCWLNnWn+Gu9hnLEBcwHFVWIGo4zD+x55S9B97L8HYKiVLj0MwLl3DcopAMC4nN20eC8HYZoFNhicYmxTMxuYEYxvFNRmaYGxSMJPN09kc1neJCun5uzFnRBXMqAvmHR1WKWCcy6QRO+MwqKv/k6dm7cV3Irjz7kAgaFJle5qXCxiLo6nnzAzaUhTLHuUnRiUYO6V04X4IxoU1qqQWBONKcrvEsRKMSxTQ4tsJxhYLajacqiKXiAGRakSnz0A6ndb/8XJXAYKxvfprWg4ftE+cMRagUhX2jRzVKLKoFDAWY8288iwGb7sEascG3YDQlw9H5OjT4G9ottcQE9HLBYyn1foxI+pHwO/dA9wJxiYeXJubEoxtFthj4QnGHjPMzXQJxm6qP7HvSgDjnlhWL7qi+HxomubfXHTF/S9DqScfQHLlz6DF+nRjqvb8CmpPuRzZKjn2Esr1pDqbDcHYfr17+rP6ea/a5knj6sjQstPxoFJJYKyrnsshlxqET+xrlug86eEnohQw1nq7ocU2QWlogVJV41i1bVXLYV1nBqnM0MMWCfr0ZfuhoPVHKNn/yhntgWDspNpb7otgLI8XMmRCMJbBBY/kQDCWy6hyB+MN3RnEB/P3ls1pFEdn5M9KOe5KOoW+E/aFtnlmaORL5zUPILe1OBYm4HhK7HBUAYKxM09DLpfT9xmLs14nq0gtsqg4MHZG+qJ7KQqMczn0Lzsc6ntvjVTcrjr6dIS/chR80fqicynmRlGV2upq1MXkYcU9BGMrVLQmBsHYGh3LJQrBuFycdGAcBGMHRDbRRbmD8X83pCFmC8ZeohDK3Magq0vo1HWrED/vmAnnpdYcfSqCXz1Omgq0Jh6lsmpKMJbHToKxPF6ITIoB4+RDdyD18J3Q+nryBlN3w6PwL9pBrgF6KBuCsTxmEYzl8UKGTAjGMrjgkRwIxnIZVYlgLIquzG8KIhhwbxmd1teN2KmHQuvamP9F8cxr4Nvty1DCPDPXzVcKwdhN9fP7JhjL40WxYBw/91vI/PtVQFXzBhO99iEEtvlY+UzhOmwVwdhhwbfQHcFYHi9kyIRgLIMLHsmBYCyXUV4D496BLLr6VLENDw11fkyvDWxxWdxkM8bTo0NFV8TyTTev2GmHILvqbYwcrunzoeGeF6E1tLiZFvsWexAjEYj3qkQiQT1cVoBg7LIB47ovasZ45dVIPnE/cvH+fDC+/hEEttpRrgF6KBuCsTxmEYzl8UKGTAjGMrjgkRwIxnIZ5SUw7t5crEdA8fA1IxrQAXmqPWNiGfXqjemRAj9ib/GshiD8klQiFQW4Uk8/BP82H0Xjd3+sF95iVWr3XyMEY/c9GM7A62CcUXMQVbjFFg5RANDrVzFgnMtmEDv9a3lHUclYcdtr3hCM5XGMYCyPFzJkQjCWwQWP5EAwlssoL4HxZLO/Qs2tZoUMzf7KXnSF5xjL89ogGMvjhZfBeHVbGpmsKP43BMTN0wOoq576hzx5VJ86k2LAWI+macj++1WI+gqB7T8J/5xF0pzN7LTu6aymH0kmdvOI/y32IhgXq5z19xGMrdfUyxEJxl52z+HcCcYOC16gu0oCY7mUn5gNwVgehwjG1nkhqk6Lq9jzYr0Kxh29WfQNDG37GHstbAnps8devYoGY68O2MK8E0kV7b0qMtnhhyKHuY0hVIWLq3dBMLbQnBJDEYxLFLDMbicYl5mhdg6HYGynuuZjewmMN3RnER/ML95SHVYwqyEw5VEv5hVx7w6CsXvaj+/ZE2A8TFwlzDjZqbjYxvBhe0Y/jmn4mt8cRNjk2bFeBeMP2jP6bDHB2M6nzFux13RkkEznHx9YFfahZbooBmn+xxKCsTz+E4zl8UKGTAjGMrjgkRwIxnIZ5SUwFsqt6UgjmR76oh0OKfqxSy7X0LLMUIKxZVKWHEhmMFbXf4D4uUePHPVVdfRpCH31WCjVtSWP28oAH3ZkkBoHAWJvv6gIb2b22KtgvLFn6Ax1grGVT5W3Y00GxuJ3LfE5FgmZnzUmGMvzPBCM5fFChkwIxjK44JEcCMZyGeU1MB5WT/b9wsW4TDAuRjV77pEWjHM59H13b2jt6/MGHr3qN/BvuxN8gYA9ghQRdWh/7bh1xADMLiX2KhiLglti1njsjPmMOlERP+DpH/O4lLqIF8PmWyYD40jIh5YZAYSKOD6QYFy8F1bfSTC2WlFvxyMYe9s/R7MnGDsqd8HOvArGBQfmwQYEY3lMkxWMtZ4OxM44fML515GDj0P4m6dAqa2TRsRKB+NhI+JJTT++tyqMouBHGkM3J0IwLt6R2KCGzt5s3o8lc2YGUB3xFxWUYFyUbLbcRDC2RVbPBiUYe9Y65xMnGDuv+ZZ6JBjL4wfBWB4vpAXjWC9iPzwYWueGPLEi3zwZkUOPh0+i5dSDKRUbe9Q8CGidEUBtlbmqzF6dMZbnabY2E4JxaXpmtRwGkzm9ULk4PtDMtoLxPROMS/PCyrsJxlaq6f1YBGPve+jYCAjGjkltqCOCsSGZHGlEMHZEZkOdyArGIvm+730J2sa1AEaXKdfd8gT8c7cSZ78YGp9TjUQBLjFLpmlAtFpBsIjzwwnGTrllrB+C8USdxPMtjmDyK0Og69TLkGBs7Jl1ohXB2AmVvdMHwdg7XrmeKcHYdQvyEvA6GKf+5x4MrrwGuVQSvtp61N38BygNzXKJbDAbgrFBoRxoJjMY5zIZDP78TKT/9jR8NXWoPf8W+Lf+GOAvbjmmA3KW1AXBeKJ8sYSKzr6h2fiaiIKmaYGiqhoXYwzBOF+13riKntjoyghF8UEsjy6mmJZZPwjGZhWzrz3B2D5tvRiZYOxF11zKmWDskvBTdOtlMM6+8TIGrjgJ2uDAyOh8ih/1978KX01ULqENZEMwNiCSQ01kBmOHJJCmG4JxvhUDgxrax+1TFRA2VMDJ/hUDBON8PyYrqNVQp2BabUCfQbbzIhjbqa652ARjc3qVe2uCcbk7bOH4CMYWimlBKC+Dcf8pX4X24TvIiXVsY65p970CX/10C9RxNgTB2Fm9t9QbwVgeLwjG40EsjVQmN+EYqLmNAVSF7V81QDAe9UPVgHWdGaQy+Z9BNVUKmur9CBZRadrMK49gbEYte9sSjO3V12vRCcZec8zFfAnGLoo/SddeBuPYj76O7Lv/hL6BcSwY//pV+OqmySW0gWxKAWOtrwfxy0+E9t6bUOZujZpzb4a/Za50e063JMNAModkWtML0lSFFcf26U2WE8HYwAPrUBOC8Xgwzuivk/GXOAtXvG7svgjGhf3gjLHdT6F88QnG8nniZkYEYzfV91jfBGO5DPMyGKur/4P+s5YAY5dS19aj/u7nparOa9TxosE4m0H/0i9DHXe2bf1dz0FpnmO0e1fbjT/aRywNndUQKKliaykDIhiXop619xKM8/XsG1DR3Z9f7TvgB2bPDCIcJBhb+/QVjsY9xoU1qoQWBONKcNn4GAnGxrWq+JYEY7keAS+DsVBS32d85SnQEnEEt/8kai6+XS9I5MWrWDDO/N8zSNxwLsSs8dir7qrfQNl2J/gCAanlEDPF7Zsyecf6iITnNwUQDtm/NHQycQjG8jwyBOOJXnT1ZdE3oEFU/Q4FFLQ2BBAO2rufdTgLzhhP9EMUQRPL28WWYuGDKMDlxMWl1E6obKwPgrExnSqlFcG4Upy2YJwEYwtEtDCE18HYQilcD1UsGKee/x8MrrgMuVhv3hiiV6yEf8dd4AsEXR/blhLYFBMzYFloo6cP6c2d2jNJMJb68QDBWC5/CMb2+zEwqKKjT0Umm4NYDdDaEERVaOJqAIKx/V4Y7YFgbFSpymhHMK4Mny0ZJcHYEhktC0IwtkzKkgMVC8ZarBexHx4MrXNDXg71dzwDpXVeyXnZHUDsl9zQnZ0wY7ygJeRIlV2Csd0OlxafYFyaflbfTTC2WtH8eAKGxfvh+IJe85qCE46AchuM2zdl9XPKxQR58/QAqsPiDGdnZsvtdcF8dIKxec3K+Q6CcTm7a/HYCMYWC1piOIJxiQJaeHuxYCxSGL+cuua8mxH81BfgC4UtzNC+UBu6MxhIaiOVdmdE/ZhRF9C/cLlxcSm1G6pP3ifBWB4vRCYEY3v96OhV0Z9QoY1bQjNnZhDVkfxZYzfBeE3HxOro4vzm6og721/sdaVwdIJxYY0qqQXBuJLcLnGsBOMSBbT4doKxxYKWEK4UMB7pNpfzVCXqsXKJL4Jiz2QgoMAlHh5JR0YwFseS6bMxFTYjQzAu4U3FhlsrAYzFrg633oO8AMZTHVM1PerH9Fq/a0UTbXjcDYckGBuWqiIaEowrwmZrBkkwtkZHq6IQjK1SsvQ4loBx6WkwAgCZwDiXTiF22qFQ174PMaWuNM9G7fJfw984qyK8IhjLZXM5g/HYomZC9aZpAdTV+B1dueKFpdRZFVjfNfH85mm1fjTU+eF3a6mPiy8VgrGL4kvYNcFYQlNkTYlgLJczBGN5/CAYy+OFTGAcW/Z1qO+/hZyqjggU3OmzqD7zWijTZsojmk2ZEIxtErbIsOUKxml9b28G6Ux+FcC5TZMXvipSPkO3eaH41pqOiedpz2kM6efQV9iiFt1TgrGhR7tiGhGMK8bq0gdKMC5dQysjEIyLUzOXy0F8kVJ8PgQD1iy6qyQwzqWTUNe8r++BVlrnwxcMFWeETXfJBMZ939kLWsf6CSMV53UrTbNtUkCesARjebwQmcgIxqI+gSgEJY5NCvp9aJ0ZQMTkmc7jzyMeVn12YxDVOuxZ8z5vpZtu7jEeP7M9o04sow7Ab/9R2lZKaFksgrFlUpZFIIJxWdjozCAIxs7obLQXgrFRpUbbxcWXsJ4MxD4rcSmKOHM3iGCgtG8ElQLGmb8+hcRN5+eduxy99X/hn7NQmi+f0oOxz4f6u54jGJt/+fKOEhWQDYyTmRzaerJIZza/IW9+TxbFqiKTHHE01fBjgyo6e9UJ1fHnCDAOl/beXqLkU97uJhjbNSavxiUYe9U5e/ImGNuja1lGJRjLZSvB2Lwfq9vS+vmSY6/6Wj9mlri3qlLAeLIZ0MCOn0bNj2+AMr3RvCE23CETGCd/twLJ365ALhEbGWn1CRcitM9h8EWqbRi9XCE5YyyXH7KB8YauDAZSoxXth9Wa7HijQkqOXx7s9wMCsMMmZ58L9WPV3wnGVilZehyCcekallMEgnE5uWnzWAjGNgtsMrzdYCyqDH/QloY4eUIUTBbFORrrA57dgyQqJ3/QnpkwqyC+QM1vCpVUjbMSwFjsk+3//pegtY9bGhwIou72p+CXZGmwTGAsXtLpl59E8u7lyCUHUfW98xDcfV/4QhGTr3ZvNicYy+VbOYOx+Izq6lcRT6r6LPGMqIKgxGuDnQRjsUxd/CAcCigQn3e88hUgGPOJGKsAwZjPg2EFCMaGpXKkod1g/N8Naf0InrFX8/QA6qoVaZbNmhV6dVsGmezosj1xf21EQdP0AMHYgJicMTYgEpuMKEAwluthkA2MrVpKLZfKxrJxCozXdg4V2hI/HIhLVOoW1afFfm5eQwoQjPkkEIz5DBSlAMG4KNlsu8lOMBYfoqs2TgRjUaxqfnPI0SMwrBSwJ6aipz+rz4IPXwtaQgiVWISrEmaMhV7cY2zl0yhXLK1/E7SNH+rVspWZrbBiaolgLJfHsoGxUMeK4ltyqWwsGyfAuG9ARXf/xL3XblTrNqaKO60Ixu7oLmuvnDGW1RkJ8yIYy2WKvWCcw6qNokhV/oxxJKzo+7a8fNShWFYmvoyJ8xqrwz4oFgymUsBYvAK0RAzZf/8dvnAVAtvuBF+kSqoXhmxLqaUSZ4pkBq49C5mXn0AuldRb+Ga2ou6aB6E0NJeUPsG4JPksv1lGMLZ8kB4J6AQYiyOsxGfd8GzxsDQyFyVzwz6CsRuqy9snwVheb6TLjGAslyVFgbGmAWIFla9wpc4P29NIjTsXspiiKHKpZk82lQTG9ihoXVSCsTkttb5uxE49FFrXxrwba8+7BcFPfwEo4TgugrE5L+xuTTC2W2Hj8Z0AY84YG/ODYGxMp0ppRTCuFKctGCfB2AIRLQxRCIwz77yBzCvPwT9nEYIf2xWxk78CbSCmV9IKfPQzqL3odviqaraYUUevCvHhqvhyaJnuR01VwMIRlE8ogrE8XhKMzXmR/X//wMBPToG2qTPvxsg3TkLksO/CV11rLuCY1gTjoqWz5UaCsS2yFhXUCTAWiXGPcWF7CMaFNaqkFnlgvHr1ahx99NF48sknEY1GR3S499578eijj2Ljxo2oqqrCnnvuiTPPPFP//5NdN954I1auXJn3p9122w033XRTQW17enry2oRCIYh/8Xi84L1sYK8CBGN79TUbfUtgHD/vGGT/9QpEJeGprsgRP0BkyUnwhcJmu2b7cQoQjOV5JAjG5rzgjLE5vbzcmmAsj3tOgbEYcTqbQzojqlL7EAqy6Nb4p4BgLM/rQoZMRsD4xBNPxHvvvYdNmzbhueeeywPjRx55BIsWLcK8efPQ3d2NZcuW4YgjjsCRRx45JRgLwD3jjDNG/i5+OZ4KpMcGIRjL8FhMngPBWC5vpgJjMSsc+84XhmaHt3D5whHU//Iv8NWM/ggm1wi9kw3BWB6vCMbmveAeY/OaefEOgrE8rjkJxvKMWs5MCMZy+uJWVnkzxrFYDHvvvfcEMB6bXFdXF5YuXarPGItZ4MkuMWMsYp177rmmx0UwNi2ZYzcQjB2T2lBHU4Gx2rEe/ScdBAxueZWF0tCCulufKGmppKFEK6ARwVgekwnGxXmR625D9t03oTTPhTJvG/gCweICjbmLS6lLltDSAARjS+UsKRjBuCT5LL2ZYGypnJ4PZhiMVVXFfvvth0QigbPPPhsHH3zwlIMXYPzAAw9AfFlsaGjAAQccoC/RHnuJOOMv8XASjOV9pgjGcnkz5VLqbAa9R+2KXIEZ49rlv0Zwh08BSuFCXHKNXL5sCMbyeEIwlscLgrE8XohMCMaT+yHOXnB6gTHBWJ7XBsFYHi9kyMQwGItkOzs7sWrVKlx88cW44IILsPvuu086hrVr10KAdDgcxjvvvIMrrrhCn2U+/PDDR9rvtdde0ESF3DHXiy++KIMmzIEKeF6BvgdvR/ctl4wcvxLebifUfG4/9P52BZRIDZouuxNVH92FUOx5pzkAKkAFqAAVMKtALpfDmvYUBlMaBBgHAz7MbQojHPTWD8Xi+EFxrGLQb83Rg2Z1ZPviFRCc5Pf7iw/AO21RwBQYD2dw/fXXQyypvuyyywwldfvtt+PNN9/EzTffXLA9Z4wLSuRaA84Yuyb9pB0XqkqNbAZqTwd8NXVQuI/YVvM4Y2yrvKaCc8bYlFy2NuaMsa3ymg7OGeNRydZ1oAJNcAAAIABJREFUppFICSQevarCClqmB3RItvuyYsZ4bWcag2PGMD0awPRaBQG//fnbrY+T8Tlj7KTa8vdVFBgvX74cqVQKF110kaER3nDDDWhra8OVV15ZsD3BuKBErjUgGLsmfXFgLFe6ZZ0NwVgeewnG8ngxFozV1f/BwE9Phbp+NYKf+SKqT7kCyrQGeZKtgEwIxqMmr+nIIJnOX7Uo/jqvKYhIyP5Z41LBOJbQ0NWXRUbNh/u5TUFUOZB/Ob1cCMbl5GbpYykIxmKqXxTRWrJkiV6V+o033tCB+KqrrtKXUovl0CeddBKOPfbYkWJcApz32WcfLFy4EG+//ba+7FrcI5ZPF7oIxoUUcu/vBGP3tJ+s54IzxnKlW9bZEIzlsZdgLI8Xw2C86T9vIXbOUch1t48kp8xoRvTa30GZ2SJPwmWeCcHY+2Csvv0aBldejXRPD2JfPRXpHT6LXGj06NQ5jUFUh+0H+3J6qRCMy8nN0scyAsZHHXWUfk5xf38/xC9ZAoLvueceiH0YAmpff/11/aimlpYWfPvb38bixYv13rPZrA7Eos3wf7v66qvxwgsv6O2bm5v1wluHHXaYoWwJxoZkcqURwdgV2afslGAsjx8EY3m8IBjL48UwGG844xvIvPEyxPaOsVf0tj8hMGehPAmXeSYE41GDe+MqemIqxB7d4WtmfQD1NQr8iv1LkYuZMU4//RAGf3kttJ6OkZzjS87D4G6LgUit/t84Y2z+RUwwNq9ZOd+RN2Msw0AJxjK4MHkOBGO5vCEYy+PHEBinkE7nf/GXJ0NrM8nlgDUdKaQ2D7dpWgB11QoUB75QFhoJwbiQQs79fRiM1y89ENl33wBUNa/zulufhH/OIsBnP4g4N2p5eyIY53szkBTLkVVktRxm1vkRrfbDqbewYsC4/4dfhbrq7bxB+BQ/us95AOrcj6CmSkFTvR/BAGeMzbwKCcZm1Cr/tgTj8vfYshESjC2T0pJABGNLZCw5yMB1P0b2pT9CSw3CV1uP6LUPQWmdB18Zf9lf3ZZGJpu/t21eUwiRkPuAQzAu+ZG2LMAwGHc/+xgS150Nrbc7L3b93c9DaZptWX8MtGUFCMbyPCFWgbEYkW/57xHZdge9onYZf+zYZh7B2DZpPRmYYOxJ29xJmmDsju5T9eo2GItZw0r/EE4+eBuSD96OXCI2YpOA47obH4PS2CrXA2NRNuKUvQ/a03lLEEXo+tqAPuvid3mygmBskdEWhBlbfCt5/41IPnw3coNx+AJB1F7zIAILt+eRcRbobDSEpWAsPgDEQUc+l1/wRgcvWbtiwDh+zlHI/r9/IDdm5UVg64+i+qyfwz9rgWQj9E46BGPveOVEpgRjJ1Qukz4IxnIZ6RYYb+jOID44Ws1zYWtIP0OxEq++4/eG1rEe0L8kjl71dz8HpWlOWUpCMC5LW20Z1KTHNfEXNVu0NhLUKjAe+NnpyPztGeRSg0AgiOhP7kNgu50q+keOZCaHzk0ZqBowo86P2siWt5YUA8ZaMoGBs5Ygu+o/+o8Svuooapffh8Ci7Y3YzzZTKEAw5qMxVgGCMZ8HwwoQjA1L5UhDN8C4J5ZFd786ngOx1ayQIwVLHBHWRCf9S78Mdf0HE+7wHBibnP3hUmoTD0kFN+U5xnKZbwUYJx+4BalH74HWv2lkcL5wBNFrfgf/gu3kGrBD2Yi9yu2bsnmraBrqFEyrDUz5uVgMGA8PJ5dK6su1fMEQl21Z4DHB2AIRyygEwbiMzLR7KARjuxU2F98NMBZAJKp4jpsgxaJZIQScqlpiTiZbW2defxmJa5bl7Z0Mfnw3fWmbMm2mrX1bFTx+6feRff1l5DJp/YtW9PpH4Z+71Ra/cHmx+JZYfph57SXkOjfAv/Pu8DfPhc/vt0pGxplEAYKxXI+FFWA8WQEoMcro9Y8gsNWOcg3YoWyKORO5FDB2aFgV0w3BuGKsNjRQgrEhmdhIKEAwlus5cAOMP2xPI5XJXzYsVFnUGkKgQpdTCzhO3ngusp0bEd5/CSLHLIMSrZfrYZkim8TKa5B6/F5gcGCkhRKpQvSWJ6E0zfLEGMYnOdkeYy0RR/zkr0Dt2DDSPPLNkxE59Hj4qoeOOXHyig1q6Ni87FJRgLmNQb1wTrldBGO5HLUCjGM/+jrUd/+JnNhTMeaqEz+obbWDXAN2KBuCsUNC29QNwdgmYT0almDsUePcSJtg7IbqU/fpBhinMhrWdmYw9jtRddiHWQ1BKY7qccshr55j3PedvYb2SI+76u56Vp9R9eI1GRjHf3IKsq8+h1w6lTek+rueg9Ls7F5w8cOS2Kc/vqr3wpYQgoHy2qtvBxinnnwAqacfgn+bj6L6m6fAVzfdi4+pKzlbAcbZ995C4menQ93w4cgYgp/eG9UnXuzZH9NKNWNDVwYDKS1vJZWouzFrZmDKH7w4Y1yq6tbdTzC2TstyiEQwLgcXHRoDwdghoQ124wYYi9QEHLdtUpHJaphW40dDXaDiq1N7FYz7T9wf6tpVE4uHuQCMBh/7gs0mA+PYCfsiu07sBc9f7VB3xzPwt84rGNPKBht7horXjd+OML+5/GaNrQbj2GmHICvOcR3+Zc7ng+5hizd/xLHyuTISywowFv0IDwZvuRDqmvcQ/srRCB/yHSj1M4ykULZths91F69rsQpk9swgqkJTrwIhGMvzKBCM5fFChkwIxjK44JEcCMZyGeUWGMulghzZeBWM1XfeQPzyE6Ft6hyd/dnty6g+5XLPftGdDIwTN52P9HP/M1RFd8zlxoxx26YsYomJBezmNwUR3sIXaTmedHNZFAZj8UOFsVlyra8bsVMPhda1MS+JmjOuQmCP/aGEq8wl58HW6pr3kf6/Z/Tz0kO772v6NWoVGHtQOkdSHv6xy8gxhgRjRywx1AnB2JBMFdOIYFwxVpc+UIJx6RpaGYFgbKWapcXyKhjrsz/v/wuJ638MrW0tIl/7HkIHHwfFhX23pTkwevdkYJzLZtF/wpehtY8erVVz+nIEP3cgfA4DVTqrYV1nfgVb8UV6QXNlLKUWy9ljPzoC6gf/0Wd+jRarU9etQvy8Y6B1t+c9Km7uFbfqmTUSJ7HiMqSf+X3emenRa8WS8o/BZ4TEABCMjSjtTBuCsTM6G+mFYGxEpcppQzCuHK9LHinBuGQJLQ1AMLZUzpKCeRmMSxq4hDdPBsbDaardbcgl4lAaZ0MUGXPrGkyraOtR9QrvoYCCWQ2BsttfLLSdbMa4/9SDoa4eguLhK3zANwsXrUun0HfCvtDGFFAT90eX/wqB7T8F+ANu2Wl7v6JifPyMw5AVuo25Qnt+BVXH/xhKQ4uhHAjGhmRypBHB2BGZDXVCMDYkU8U0IhhXjNWlD5RgXLqGVkYgGFupZmmxCMal6Wfl3VsCYyv7YazCCkwGxn3H7zU0cz/28vmgL2tvmr3FoKLwVnLlz6DF+vR2wd33Q83Jl5V9AS4t3of42UdC/fDdPH0Csxei6vxbERDHqxm4CMYGRHKoCcHYIaENdEMwNiBSBTUhGFeQ2aUOlWBcqoLW3k8wtlbPUqIRjEtRz9p7CcbW6llKNKNgrJ+ffduf4G/eMhjruajq0HLiSLV+7nZFXJqGkcJjYwYcOeIERA75juEfBgjG8jwtBGN5vCAYy+OFDJkQjGVwwSM5EIzlMopgLI8fBGN5vCAYy+PFZGA8cN2PkX3pj9DGFEKrPvlyhPb+quP7veVRqnAm6b89jcEVl0LrHCo+pjS2ovbSu+Gfu3Xhmze3IBgblsr2hgRj2yU23AHB2LBUFdGQYFwRNlszSIKxNTpaFYVgbJWSpcchGJeuoVURCMZWKVl6nKmqUicfvA3J390BpJOoOuEihPZaTCg2ILcoIqf1dsEXjkCpqRs6F8jERTA2IZbNTQnGNgtsIjzB2IRYFdCUYFwBJls1RIKxVUpaE4dgbI2OVkQhGFuhojUxktkAuvs1DCQGEQ6Wb2Era9SyN0rh45rs7Z/R8xUgGMvzRBCM5fGCYCyPFzJkQjCWwQWP5EAwlssogrE8fhCM5fBiIKVhU1yBlvMhmUzqSQUCPsxrDCLgN3ZerhwjKY8sCMZy+UgwlscPI2CcymjIZIFw0FeWVetlcYNgLIsTcuRBMJbDB09kQTCWyyaCsTx+EIzl8OLD9jR8SgjwjYKxyGxBSwihAMHYaZcIxk4rvuX+CMby+FEIjNd0ZJBMjx5pNj3qx/RaP3/gs8FCgrENono4JMHYw+Y5nTrB2GnFt9wfwVgePwjGcnhBMJbDh+EsCMZy+eEWGCf/ZyVSD94GbVMXlGkNqLnkLgQW7aD/gFWp15bAuKs/i764BlXL5ckztzGIqrC5feWVqq+ZcROMzahV/m0JxuXvsWUjJBhbJqUlgQjGlshoSRCCsSUylhzEE0upc+LLbg7wle8XXG1wAPEzl0Bb8y5ymobI4d9H5IgfwFddW7LHDFC8Am6A8fhq2iJ7JVKN2qt/C/+C7YofjMfv3BIYr+1MYzCVD8ViuHMbA6gK+z0+cvnSJxjL54mbGRGM3VTfY30TjOUyrFzBePD+m/TZhVw6hfDB30bVMWfoVVhlvgjG1riTU1X4/AIYi59Jkrn41sDVy5D565+QSyXhUxTUXvvQ0MyZyerC1qg9GkWw+vruDBJJTU9lVkMQVSEffEXO6PV/70tQN64d+gFg81Vz+k8R+NwBUMJVVqfPeAYVcAOMY2d+A9l33gC00WXBIt3a6x5BcOsdJ2QunpjiX/0GhZCgGWeMJTBhcwoEY3m8kCETgrEMLngkh3IAY7E0SfGJL3zGRM/lckV/OTTWQ/GtyhGMkyuvRvKRXyCXSY8IEz7wm4h86wwo0WnFi2XznQTj0gTOvv8vDFy6FFpPhx4ofNC3UHX0afDV1pkOLOtxTanH78Xgr65DLt4/MiYBx3V3PgulabbpcVp5w+q2NDLZ/Bmq+c1Bvaq32UtAf/8P9oPWsSHvVt/MVtRd8yCUhmazIdneIgXcAOP4ed9C5l+vAqqaN4ro9Y8isNUOI/+ts08sH1YxvHq4eXoA0SoFimLww9oijZwKwz3GTilduB+CcWGNKqkFwbiS3C5xrF4G44Gkho09WWibP3UjIR/mNIYw1Wdud38WPTEV+qpHALNnBlETMf8lsUTJt3h7OYJx7zc+idxAbMK46+9+3nV42JIZBOPin3Sx1Lb/e1+E1r4+L0jNFSsR+uhnAH/AVHBZwbjvhH2RW/8BxI9tY696AcYtc02N0crGWTUHUehH/O/Ya4Yo9hP1w28STMRKj/4T9p0Axv7ZC1B75b1QGlqsTJ+xTCjgBhir77wBsVJC3bhmJFOlsRW1l94N/9yt9f82kFTR0atO+HGmnJcOFwJjoQurUpt4uEtoSjAuQbwyvJVgXIam2jUkL4PxfzekJxSyaJ0RQLR64n6ddEbTvyiOq3uBRa0hqSpCEoztetLNx50MjAUAZd96Bera9xD66C5Q5mxlGvLMZ+K9O9SedsTP+Dq0ro15yYcXfwtVR5mfNZYVjGM/+gay7705Yeas/q5noTS7B8aZrIa1ndlJwDiA6VHFNBgLE2M//CrUD97R9xcPX7UX3o7AJ/aALxjy3kM6JmPxul7TmUVqc8Xg6rCClhkBqT4bphLYDTAWuWRefwmDN18ItX09Ajt8ClWnLkdg1vyRNDf2ZBAf1EZ+iB7+w5zGIIS+5XgZAeNyHLeMYyIYy+iKezkRjN3T3nM9exWMxfLp1RvTE0A3Wq2geVpgwlKt7v4MemITP6Tnt4QQlujIl3IE44GfnYH0n58A1OzI6yP46S+g+rTlUKbNlPY1MwGMxSzo97+kfxEc/rYX3H0/1Jx8GXx106UdhxuJ5Qb60X/yYmid+Utvq791OoKLj4FismCTrGCsrnkfsfOPRW7zcnEdGBbtjP4TrsPcbVqLWrZslV+TLaWe2xRAVai4Qj9ir3jiZ2cg88ozQDaL6lOvLJv9xR92ZEageFj/abV+iBl22c/KdguMCz2nXWIZ9UBlVWEmGBd6Kpz7O8HYOa290BPB2AsuSZKjV8FYrFxctXHijHFD1I8ZdYEJ+437ExraN2Um/Hq9sDWEoF+e/U7lCMbiUY9f/gNk//48ctksAh/9DGouXGEajpx+yYwH4+TvViD52xXIJfKXhUdXPIXA7AVOpyd9f/0n7g917aqRHxFEwtFb/xf+OQtN7/GXFYzFmAQcb7rqR1A2rsLgnkuQOOD7yFVHEQkpmNXg3qyj2F+8rmtoObV4v2yc5kd9tb/k/Z3leFzT6rYMxCz7+GteU1D3UeZLVjAWW5zWdmaQyowu5xfnjrc2BFz9wchOLwnGdqprLjbB2Jxe5d6aYFzuDls4Pq+CsZBAfOkTFVfHXgtbgwjqFXDzr8lAWlRqXdgSKmpZoYUW5IUqVzAeGaQwwmiVNLtENhh3PBjHLzoe2Tf/ilw2kxchet0jCCza3vUqxAaH5VgzseR28M6fIP3s76HMbEbNmT8f2n9YRLVmmcFYCDoVWIn3l6BEK1KsMF9WMBZgK/ZOF1PYabLZdfE2Jc6YJRgX/9SIVfed/VkkUhpqIwqm18o/A1/8aAGCcSnqWXsvwdhaPb0ejWDsdQcdzN/LYCxkig1q6IllEAn6MbN+y0VlxPLrDd1ZJNPiQ9qHpmkB+CeBaAfln9BV2YOxm+Ka7Hs8GKeefADJlT+DFuvLi1R/xzNQWueZjM7mZhSQH4wnVoAOBX2YMzMo/VJcMz6ItrKBsah63NWvjtSbKGamvn1TFv2J0cKMYpzi80FszTFbqMysnqW2l3XGuNRxefF+grE8rhGM5fFChkwIxjK44JEcvA7GHpHZcJoEY8NS2d5wsuJbsdMOQXbV2yPnd0a+fgIiX18Kn8k9s7YnX2YdyA7GokK+gKuxVaC9sAy3mMdENjCebLZenNksThwwszilZ/jUAgCN9QHUVXvjWCGCcTFPsT33yAzG+rnmXWkkUkNL2/VnvEb+H36KdYpgXKxy5Xkfwbg8fbVlVARjW2QtOmixYJx+9XkkfnoqcskElBlNiF73MJQZPFu0aCMATHVck9bXjVz/JvgaWqBU1XhmaXgpWrh9r+xgLPQRK1L0L505oCoMBGxejSIqKaeygA85vU5CMUuIi/FVJjAWy6fXdWaRGXcslSic1VBn/liqYvRw+x6CsdsOjPYvMxiLUznEarmxlzjesirkM/UDkjxqbzkTgrFXnHImT4KxMzqXRS8EY7lsLAaMs2v/i/iyw5FLxEcH4/Nh2m/+AV9NVK4BeigbnmMsj1leAGMn1UqkVLT1qHkz1PObAggXWXHaTO4ygbEo8PRB+8TzmsVsWH2NN2Z8zWg/WVuCcakKWne/rGAs9nqv7UznFUIToxZHWzbWl+e+b4Kxdc91OUQiGJeDiw6NgWDskNAGuykGjOOXfh+Zf7w48SzVX7wA38xW0xWADaZa9s0IxvJYTDDO92KyYlFO7WmWCYyFKhu6MxBL2cVS0eGrHIueTfVqJBjL8z5FMJbHC4KxPF7IkAnBWAYXPJIDwVguowjG8vhBMJbHC4LxeDCe/HghJ4BQNjAWyoglogKOxZLy2qrKmCkefiIIxvK8T8kKxkIhLqV25jmpr6+H31/cWfHOZFiZvRCMK9P3okZNMC5KNttuKgaMuZTaHjusAmOxF7RvQEUyk0NNWEFtlb8s93TZ48JQVIIxZ4ztfL68HJtgLI97MoMxi28585wQjJ3R2WwvBGOzilVwe4KxXOYXA8ZiBDIU3xKFcHw+X9kcT2MFGAso/qA9C6HN8FXMcTJyPaXOZ0Mwztece4ydfwZl7ZFgLI8zMoOxPCo5kwmXUjujs1d6IRh7xSkJ8iQYS2DCmBSKBWM3R5HJ5vBhexra2D1+rSF9WaOXLyvAuKsvi94BDaJI0NhrQXMQoaDiZXkczZ1gPFFu8Uwl0hoUnw+RoFg+7IwlMi6ldmbkcvZCMHbel4FBFR19KsRnX8APtDYEURVSQDB23oupeiQYy+OFDJkQjGVwwSM5EIzlMsqLYLxqY1o/qmZs8ZtwUMHcpiAUD7OxFWA8dG4kIGaOx17zm4MQGvEypgDB2JhOTrQiGDuhsvE+CMbGtbKipYDhDd1ZpDL5Rx+Jc8ubZk5DIpFANpu1oivGKEEBgnEJ4pXhrSWB8eOPP4777rsP999/f5409957Lx599FFs3LgRVVVV2HPPPXHmmWfq/7/Q1dPTk9ckFApB/IvHxxwvUygI/26LAgRjW2QtOqgXwfi/G4bAePy11awQ/B4mYyvAOJ7U0N6TgZr/HQoLmkMQVYR5GVOAYGxMJydaEYydUNl4HwRj41pZ0bKjV0V/Qp2wCmjOzCBamgjGVmhsRQyCsRUqlk+MosC4s7MT3/ve99Db24vW1tYJYPzII49g0aJFmDdvHrq7u7Fs2TIcccQROPLIIwsqRzAuKJFrDQjGrkk/acflAsZiWaeokGsLGKdTyKUG4auuBfwB2wy0AoxFcuOPk2meHkBdNQtwmTGOYGxGLXvbEozt1ddsdIKxWcVKa08wLk0/p+4mGDultDf6KQqMh4f2/PPPY8WKFRPAeOzQu7q6sHTpUn3GeLfddiuoCsG4oESuNSAYuyZ92YCxmBVt68nm/YIullGLPVdWX/ErTkL2Hy8gl07poWtOvRLBPQ+CL1x45YrZXKwCY9GvWEktllOL4mQ+ThSbtYJVqU0rZs8N6WwO3bEcoIThRxINUcWeH7/sSb8soxKMnbWVS6md1bvY3gjGxSpXnvfZBsaqqmK//fbT91CcffbZOPjggw0pSDA2JJMljVKZHOKDKsQxamJWShRm2dIlwDhz93LEHl2JXCaNwKwFqLnmQSjRaZbkwyDmFPDijLEYYUbVEE+IfcY51NcG4LeeiZH+y58wePMF0Pryt2bU3/UslOa55oQ20NpKMDbQHZtsQQGnZoxFQauMKgrq+Ah84/wQQLC2M4McFP2HioGBAX07gFhCKvTi5Y4CBGNrdBcnB2TVHEIBBf4CzzOLb1mjuZ1RCMZ2quu92LaBsZBCLLletWoVLr74YlxwwQXYfffdRxT697//PUGtHXfcEbFYLO+/i6VYwWAQg4OD3lNX4ozbN2XQN6DlFfpZNCu8xerAid/dgYFfXoNcKjkyMn/jLExf8b9QaqISj3ZiauKsWKGBmJ0TS3kXtGx57DIOLhqN6nvvxxdrkjFXp3PadN5xyLzxMpDN5HU94+Y/ILBo+yHTLbxE/YRMJsNCKhZqWmwoUZNCzLanUkMrBey4OnqH3j+HK4iHBfQ1hgh9m8UWheQGkhoUxa9/fieTQ58ZC1vDCAUIxnY8k0Ziis+M8d+xjNzHNqMKiFMVkunRQhDTo37MiAZMv/YFjIn3KDGJxMtdBcLhsP49Kp1OO5qIeAb8YmaKl1QK2ArGwyO9/vrrIZZUX3bZZSODP+644yZ8oV+5cqX+5XLsJWYpxZccvnlY+9y8ty45oQhSTcSH2TPDUKYogrRu8UegxfqG1nqOueb84R0otfXWJmhjNPGhtqYjBW1ckaNt5kQ8NfMjvnCKipYE44kPS++KyxF/5B5oifyifa2//DOC87aC1WuUxYeb8EEb/1DZ+Bwz9OQK2P2ZIYrHrd6Y0meM8t4HG0OoiXA/uNBk9cZBpDZ/lIvXxvDn9yIBxqyw7tpLV3xmjP+O5VoyHuy4sy+D3pg64bvT/OYwqsLmfmwVkz7idcHPb/cfBLc+v0W/4vOKl1wKOALGy5cv138Zu+iiiwqOnkupC0pUcgMxyyGOzRlfHFj8cLWwJTzlsTl9R+0KrW+T2AWZl8O0B/4BX01dyXk5FWB9dwYDg+OoWJ/N8NZ5urYvpRY/gHh1k2s6hb4T9oXWsWHksQpstSNqLrodyowmyx81LqW2XNKiA9q9lDqRUtHWo04A4xlRBTPqvH3sWNGij7txuMI6fP6RpdSiiSiyF+SMsVUym47DpdSmJcu7YW1nGoOpiacqzG0MoCpsbuaP5xiX5oWVd3MptZVqej+W5WAsfgE799xzsWTJEr0q9RtvvKED8VVXXZW3lHoq6QjGzjxUkx2bI5YEzawLTMlCmZf+iMT150BLJkZhY9udUHP5PVBE1V+PXBt6sognJi5f8tqXNrvAWCxBTlxxErTBAd3RmnNuRHC3feCzsaqzLY9OOoXB+65H5p//h/CXDkXoS4fCV1VjS1cEY1tkLSqo3WAsZorXdmT0/cVjr1kNQdRExAqnotIuu5s6+7KIJ30IhcL6HuO5jUHTs2plJ4rLAyIYQ5/tFWcLD6aGfhxvnuZHVNRYMXBcYFd/Fn1xbcKMcTHPNsHY5RfDmO4JxvJ4IUMmRYFxW1sbjjrqKH1Jjtg7JPatLF68GKeddpq+LESA8Ouvv64f1dTS0oJvf/vb+t+NXARjIyqV3kbs/9o4pjpwodli0aNY8qH8+Q/ouuZs/Qic4B77o+q05fDbBBulj3LyCOmshjXtmbwZc/FldlGrTUcG2TQQO8BYwHD/cZ9HbiB/r3/9vX+FMn2mTSPxfliCsTwe2g3GYqTjj9USBXjmN7Gw1PinQCwXraqqRizWL88DUsGZEIyBNR2ZvD3C4nGY0xhEtcGl0OPvFxMK02v9pvcYE4zleSESjOXxQoZMigJjOxP3JhiLmQNj0wTZDR8g88Rv9CWqoQOPhL95jqvLVcWsh/ih1MgZslYf15R+8Q9I3HQBcskEIocvRWTJifCFwnY+XiOxxXLIjT2qXjwnHFQwe2bQlurIdg7GDjDOvPIcBq46Tfdk7FV7+UoEPrYrfCwUMcFStasNuWcfhhrvg3/vQ+GfuzV8AfvOTLbzmSqH2E6AsdApnclBvI+IyrRifyFniic+PTzHWK5XVKWDsVjtsb4ri1QmfyvVtFpRQMs43Ir7M1lAFN0rdmsAwVie1wbBWB4vZMiEYFxznp6LAAAgAElEQVSCCwPLT0Xmr39CTs3CF52GulufhDKtYcqIqWcexuAtF+uzrcNX7SV3IbjzHtDPTJL8shKM008/hMStl+RpEfriIag+6VJbzpmVXNqi0rMDjLP/egXxS76P3OZl1MOJRa/6DQIf+YTl1ZyLGrhEN2VefxmJa5ZB6+0eyar65MsR2vurfI5d8skpMHZpeJ7qlmAsl10EY2vA2ApXCcZWqGhNDIKxNTqWSxSCcZFOir2LqYfuQC49eiSIgOP6u56Db4r9tr3f+OSEJaq+YAj1v/obfB447shKMJ5MC2HFtAde84QWRT42lt5mBxiLUt293/z0hOd02m/+Dp+HKo9bKvQWgvV9Zy9oHesntKi/+3koTbOdSoP9jFGAYCzP40AwlscLkUmlg7HQoNSl1FY5SjC2SsnS4xCMS9ewnCIQjIt0s3fJp4bgYdzRRVuq0GwEBkVhCMXnk3JZHsG4yIfFpttsAWMAWrwfAxcdj+x7/4R/1kLUXPYLKDNb9GPTeOUrQDCW74kgGMvjCcFYHi8IxkNelFJ8y0o3CcZWqllaLIJxafqV290E4yId7Tt6t7zlk8NhtgjGx+yOXE9nXo/Kgo+g7qr7kVCq84phRUI+zGkMTXl0UpFpl3SblWCcuOE8pJ/9PXLZ0XOrA7MWoOa6h6FUR0vKs1JutguMK0U/K8YZW/Z1qO+/hZw6WuVcmd6I6M8f1n9M4OW8AmbBWIv1IvvC4/qZ18E9vwK/mOnn2ZKWGEcwtkTGSYOIIwc7+rL6sWH1Ncb2yHLG2D4/zEYuazDWNGT//SrUdasQ2P6T8M9ZBASCZiVyrD3B2DGpPdERwbhIm9KvPo/ET0/NK1IU/MTnUH3uTVCmqNKs9W9C/3e/iFwirvcqllHX3f2CXu13suOTWmcE9GMEZLmsBGMxpviF30Hmzb8AqgplZiuiNz0Ghct1DdtNMDYslW0NRZGy2EkHQhXnJW8+97n+5j9CmbuVq0X1bBuwBwKbAePsf97AwBUnQts0+oNlzVnXIbDrl6CEIx4YrdwpEozt8SeWUNHZ9//ZexMwOap6/f+tXmefSSaZSUIISQCR6wUxoJcrLgS5uAEim0pAZJPNhU32RWULEEAStiAgIAgoqFejBDfgCvrzL7iiiCyBrJNMZu+Z6bXq/5yazNKzdXX3qepvdb/1PDzemznne77nfau66lPn1DnZe2nXVQfR0jT9AlIEY3f8KCRquYKxGuzoO/dIZNb9a0SWyP8cjarjz0GwubUQqVyvQzB2XWJfNUAwLsIuG45vvgBWfy+ih56A6i98DYaTh6n+XlgwRr6lVVN71m1JZm0fpNKqrwmgtSnkaH+9IrrhuKpuMB5t2Pmq3o6TrYCCBGM5JtcYJpKJBFKhKAyONpbUmHzAuOeUpUPfiI/9JMYw7LUi+I148TYSjIvXcLIIk30nq8otaAmjKhKYslGCsTt+FBK1XME4/uAKxJ96FFYse4u2+m/9CKHd/rMQqVyvQzB2XWJfNUAwFmCXeiZ7c0tywqbxzfVBzGwIifne2D0wFmCCD1MgGMsxjfsYy/EiLzDm4mmuGkcwdkdegrE7unoZtVzBOHbpCUj944/2TMCxR/1NjyO0xz4iP1MhGHt55stvi2AsxKON21MYiGfvrbdobhjh4NRvf71OnWDsteLTt0cwluMHwViOF/mAce9XDof51quwzNHfXqO2AQ23r0Fg9lw5nfJpJgRjd4zb3pNGT7+Z9TJd7ac9tzmIaJgjxu6orjdquYJx/MlvI/HDe2H2dGaD8W0/RmjXd+kVUVM0grEmIcskDMFYs5Fq4/e2rgxSaRNNtUE05zHi2zdoorMvhapwELMagwgGClsFeHjVxXjSRF2VgZamEIIaAJtgrPlkKTIcwbhIAQEkUhbau1PImIY9O0NdL4Usvk0wLt4LXRHyAWMr1oPeLx8Gs33LSPP1t/0vQoveKXJkQ5dGXsUhGLun9NbuDNS3xqZpIRQE5jVPP41aZcKp1O75kW/kcgVj9VlK7/lHI/Pa30c+Uak+/lxEP7kMRn1jvjJ5Up5g7InMvmmEYKzRKgXFG9pTaivYkaMmatg3rECBkJtvepNNy1afPC6aE3EE2gqmY4MmImEDdVWBrLwJxvm64W55gnFx+g4kTLR1Dq3qOnzMbAhiRl3+L6UIxsV5obN2PmA83K7ZvR3qh9tomAkjFNKZTkXHIhi7b79lWY630iMYu++H0xbcBuP+uIl0BqiKApGgeuFb2ECL0/6ML2du3QirYyuM+Ytg1DWJXnuDYFyoy+VZj2Cs0de3tybtEajxx+K5EYSC3vwo9Q6Y2NqVGr+9MhbNjSCcI4ctnWn7DfTYY2zuBGONJ4uGUATj4kR8a2vKntkxbity+yVSOJTf9UowLs4LnbULAWOd7TPWqAIEY1lnA8FYjh9ugbGaQaAGaMY+i6rZUE21Ac+eQ+Wo7CwTgrEznSqlFMFYo9Pr2pL26NP4B+3F8yIIeTRi3NGbQmffxIf9XeZEEJ3mYV+9dX5zi5pSmg32sxtDaKoL2tNLCcYaTxYNoQjGxYmortdUeuKLLIJxcbqWujbBWI8D6gFbHcXMdiIY6/FCVxSCsS4li4/jFhhv7VYDHKY9xX7skWvF8uJ75N8IBGP/eudG5gRjjap29qXR0ZuZAMa7znM2jbnQVNQPoJrGPfxNslqxctxvInKNWmdMYN2WxLRbRhGMC3XInXoE4+J03dKRQiye/RIpGDSwS0s47zfrHDEuzotia3fHMvZvr3qxF41GsXBOFaxMvNiwFVv/7a0pJFMmhh+td54dRnU0/4UgCcayTiGCsRw/3ALj9duGZi6OH6AhGE/tPcFYznUhIROCsWYXNquH7cHRj4ydTGEuJoXuWBrtPdkwPqMuhO7+9MgP406zwqityv1Q88bmiVtGzZkRsvdTVt+nEIyLcUp/XYJx8Zq+vS2FRHLoelWzIhQUR6ZZ1XWqFgnGxXtRaITBpIktHaPfiiswVr9Xc5vMvKfEF5pDOdXb2pVG78DEF7yFzKQgGMs6MwjGcvxwC4zb1YrlscyEQQ6CMcFYztkvOxOCsUv+qLd1Xqx1MBnMNtQG0dI4NP05nwUX1KrYbZ2j3ydHQgYWtEYwPAucYOzSyVJgWIJxgcKNqzb8Zr2Y65VgrMeLQqKol5FqoZlhH4fBuLUxM+3WNYW0VQl1pvrEYGFrxF6UMZ+DYJyPWu6XJRi7r7HTFtwCY9W+GjVWC6kCQ9er2plEPRd69EWfUwnElOOIsRgrRCRCMBZhQ2FJqCnUb25JTngzGAyqVaijBf8IpjIWgvYIcXZeBOPCfHKrFsHYLWXzj0swzl8zXTXGj3COgHFDGtFIUFczFRNn7CyKsZ2u5BFjNUV/IGFBzS2vjgIhDdsfluKEIhiXQvXJ23QTjFWLaRPIZNSsmUDBz4Jy1HI3E4Kxu/r6LTrB2G+Ojct3qhHj1qaQ9hFrgrGsk4VgLMcPgrH3XiT+9zsY/N4qWP19yCx+N7q+eCusphb7G2MFLi0NafuhkEd+CiRTFjZtT0G9IB0+ZtQHMbM+/23MymHEWM1GUC9fxm7r5tdpqQTj/K4FN0u7DcZu5l5usQnG5eZocf0hGBenX8lrT/aNca6FtgpNmmBcqHLu1CMYu6NrIVEJxoWoVnidxK9/hPi918Hs6x4JYrYuROc596Fpl4WYNyuCVJKLbxWqsILA7T1pJNMW1FYvtVG1zkT+0coBjCebWq6mlM+flf8iffkrqLcGwVivnsVEIxgXo57eugRjvXr6PRrB2O8OAvZKrANxE6GgmuY1ceqg2dWO2IWfQ6ZtPQL1M1B346PYHNkJg8mhztdVB6AW2cq1LQfBWNbJQjCW4wfB2Fsvek77CMwtG2DPbR1zNN7/DGoW7GYvFDgwMOBtUmxtggLlAcZD+52PPwqZWl7qU4RgXGoHRtsnGMvxgmAsxwsJmRCMJbjgZg6ZDLqPe6893XDs0XHz8zBrm0b+qVEt2JVj+jXB2E2j8o9NMM5fM7dqEIzdUnbyuARjb/UutLXyAOOJ+51XRQKY1xzKe1u3QnXUVY9grEvJ4uMQjIvXUFcEgrEuJcsjDsG4PHycshfpv/4e/decCXOwP6tM9wUPIbV4H4xdYSvXfssEY1knC8FYjh8EY2+9mGwqdXCnRai99iHUzl/IEWNv7ZiytXIAY7W67+Yx24Gpzu7SGvbliucE48lP1fS//ozUC0/DqGtA5OCjEJjZ4vq2IgRjIT9SAAjGcryQkAnBWIILLuaQevE5DNzw1YlgfO79SO62Lwy1hPWOg2DsohEuhCYYuyBqgSEJxgUKV0S1sYtvhfbYBzWX3o5gcyuqqqoIxkXoqrNqOYDxsB5qUTLDsHy9oBvBeOLZ3X/LhUj97mlY8dFPL+pveRKh3fdyFY4Jxjp/aYqLRTAuTr9yq00wLjdHx/XHSsbRc8L7J06lXvE8zLrRqdTNDWrV0elXsuaI8cSTxerrgRUKIVBd6/mZRDD2XPIpGyQYy/GCYCzHi3ICYzmqFp4JwThbOyuZQOz8o5Fe96+sP1QdcRKix5yOQGNz4WLnqEkwdk3avAMTjPOWrKwrEIzL2t6hzmXe+Af6Ll4Ga8d06rprH0LXzkvQMwhYFuAEilUcgvHoyZLe8Dpi5x8DayBm/2Ngxmw03P00jNp6z84ogrFnUudsiGCcUyLPChCMPZM6Z0ME45wSeVqAYDwOjPt7EbvoOKTfejXrD+H/PgQ1X7wcgdlzXfOnnMG4O5ZBZ1/G3uKsKmxgzswQImG5W+cRjF07zX0ZmGDsS9tKkzTBeFT37s8smTAKHz3kGFSffgWMaLUnBg2DcfLlPyL29VOB+ACM2gbU3/ZjBFt2cnUamCcd9FEjBGM5ZhGM5XhBMJbjhcqEYDzOD8tC31c/hfSbr2T9ofrMbyCy9HAEaupcM9AvYNzZl0bvgIlIyMCsxiAiOfaG7xvMoL17CIqHDwXFarE6FUPiQTCW6ErpciIYl05737VMMB6yzEol0XP8/hPA2IhE0fjd33s2aqzAuHP9OvScchCsgexVx5see8leSISHNwoQjL3R2UkrBGMnKnlTptLAOP2XFzD48G2wBgdQc/JFCO71Pqj7gpSDYDzRifTLf8TAty5Cpk1t/waEFr0TNRevhFrMz83DD2C8oT2JeNKyZxYOHzvPDk26Lejw39dvSyKRyq6j/rbz7DCqozJHjQnGbp7p/otNMPafZyXLmGC8Q3rLQvdn950AxoH5i1F/yxMI1HgznVqBcdt3bsHgd2+BlYhnnRdqWrd9YzdkvqEt2UnsUsMEY5eELSAswbgA0VyqUklgHH/y20j88F6YPZ0jatZecDPC//0/ns0iymUjwXgKhRT5DfTBDIQQqKr25L4pHYzTGWDT9hQSqew9vGc3htBQG0AwMPmzBcE411U4+vfGxkYExyyA67wmS7qpAMHYTXXLLDbBeNTQwfuWI7Hmu/bo8fDReNdaKDj2CkYJxnIuMIKxHC8IxnK8qCQw7v3K4ciMm5JrVNei7qbHEVq4hwhTCMYibLCTkA7GyfTQNmVqNfaxx8yGEJpqA1Pu4c2p1M7PMYKxc628LEkw9lJtn7dFMM42MPWXFxB/9HYYDTNRe/rlMJrneAbFKhNOpZZzQRGM5XhBMJbjRcWA8RTfqtoAtPInCC7eU4QpBGMRNvgCjC3Lwob2NNQ+3mOP+WpKdMSAMc1sNC6+5ew8Ixg708nrUgRjrxX3cXsEY1nmcfEtOX4QjOV4QTCW40XFgDGAvnOPRPq1l9UqFCMGhPf9MKrP+gaCrTuJMIVgLMIGX4CxSrI/bmJrV3pkIa2G2qC9i0k4WF6faPEbYznXhYRMCMYSXPBJDm6DsXpDmUxbCBgGwkJXL5RkFbdrkuMGwViOFwRjOV5UEhibsW5765/M26/ZBgRmtqDu6gcQ3GV3MYYQjMVYIX4q9VilTNOyR4jLdckSgrGc60JCJgRjCS74JAc3wbhv0ERbZ2pk9cNAAFg0JzLlAg8+kczVNAnGrsqbV3CCcV5yuVqYYOyqvHkFryQwHhbGSsYBIwAjFPbs05rhVYNzgYskMFaLOrV1ZZBMmVDjj3ObQ6iJBqadopvXySe8sPRvjIXLpzU9grFWOX0fjGDsewu968AwGHd0dqE/btk3s5oqIKgotsjjjc1JZMzsRR6a6oJQKyDmutkX2bRvqxOM3bEuFjeRyQDVUeTcs3E4A4KxO14UEpVgXIhq7tSpRDB2R8nJo6qRvI32ysFD2+MEgwZ2ag6hKjL5PVkKGJsmoLYCUnmPPSRv6aPbV4KxbkULj0cwLly7cqxJMC5HV13qkwJjI1SLV9/qgLohDx8L50SK2rhdxXpzSxLjuBhqFftFc6KYYlcAl3rpn7AEY71eqfPwra2pke+pVPSZDUHMrA/lPAcJxnq9KCYawbgY9fTWJRjr1XN8NAWXg4lsuKwKG5gzM4RIeCIcSwHj2KCJbd2j364O92unWeEdo8bu6iYhOsFYggtDORCM5XghIROCsQQXfJKDAuO2njC6e3qzMo6EDSxoieSEh+m6OdmIcX11AK0zQgiQjCeVjmCs98LZ0pmCemAbnpY4HF1N6c/1zTvBWK8XxUQjGBejnt66BGO9eo6Ptn5basKqwarMgpbwpKPGBGN3/cgnOsE4H7XcLUswdldfv0UnGPvNsRLmOxUYq5R2nVfc98Dbe9Po6stkQcnieRGECMVTOk4w1nsxqNHiVJpgrFdV76MRjL3XfKoWCcbuejEZGKtPj9SU5MmmU0sBY06llr+PsbtnrqzoBGNZfpQ6G4JxqR3wUftTgXFVxMD82cWNGCsZ0hnLHrELBQ3UVk2/T56PZHMtVYKxXmnV1L6e/uyXM6oFjhjr1dntaARjtxV2Hp9g7FyrQkr2DmTQ0ZtBKj06nbp1Rhj1NYFJZ3BJAWPVVy6+1YCBgQGk0+lCrGcdjQoQjDWKWQahCMZlYKJXXVBgHI7W4R9vbM/6xnjR3EjZ7WvnlabFtEMwLka9yeuua0vao8awl5aDPZW/oSaYcwE4TqXW70WhEQnGhSqnvx7BWL+m4yMOJkxs783Y92S1x2xt1dS/V5LAeLgf5taNiH3jNGQ2vAE1ZazmrG8icuBhMGrq3BevhC1wKnUJxR/XNMFYjhcSMskC43Xr1uH444/H2rVrUV9fP5Lf3XffjV/96lfYsmULmpub7TLHHnvslPmvWrUKDz74YNbf999/f9x+++05+9zZ2ZlVJhKJQP0Xi8Vy1mUBdxUYu11TMqX2tbMQDhW/IrW7WZdvdIKxO96mMpb9kKm+K1Z7ajs5CMZOVPKmDMHYG52dtDIdGJsdbRi440qYb72K6OFfQPiQYxAocxhyopmbZcSBsWmi75wjkH7zlaxu19/wKILvfA8MtQJnmR4EYznGEozleCEhkxEwPuuss/Daa6+hq6sLzzzzTBYY33bbbTjggAOwePFivPLKK7jwwguh4HfJkiWT9kH9TQHueeedN/J3dYOsrq7O2edKBmPLNDF4x1VI/d9PEZi7ALWX3YlAy06e7YWYyxw39zHO1Tb/PlEBgrGcs4JgLMcLgrEcL6YCY3PbJvR97bNQcDx8hN97IGq+uhyBpua8O2BlMhi45kyk/vICrFQSVZ/7Eqo+fUrZjzrmK5Q0MDbbN6PvypNhqtHiMYft3+EnwqhvyreLvilPMJZjFcFYjhcSMskaMe7r68PSpUsngPH4RE899VQcfPDB+OxnPzslGKtYl156ad59rGQw7jnxAJgd27I0a3jodwjOnJ23jm5UIBi7oWrhMQnGhWunuybBWLeihccjGBeune6aU4Fx7OLjkf7XS7DGfV/ZeP8zCLTMzzuNvnOPRPqNfwBqVacdR81Z30DkoCNgVNXkHa9cK0gDY6tjK/ou/wIyG17PkrzmhPMQ+cRxMOoby9UKEIzlWEswluOFhEzyBuN4PI5PfvKTuOGGG7DffvtNCcaPP/441MOimnr98Y9/3J5+7eSoVDC2Yj3oOXkprIG+7BvEKRcjcugJMMIRJ/K5WoZg7Kq8eQcnGOctmWsVCMauSZt3YIJx3pK5VmEqMO494xBkNq6b0G7jvb9BYM7OeeWjZlr1nnYQzK2bsutV16Hxzp8jMHtuXvHKubA0MFZa937lcGTGT6W+5QmEdt97ZLZcMm3CMAyoL7fU/5bDQTCW4yLBWI4XEjLJG4y/+c1vor293Z5KPdWxYcMGZDIZRKNRvPrqq7j22mtx+umn4+ijjx6p8tRTT02orgBajTSPPdSNNRwOY3BwUIJeruWQ2b4FXV/8KMz+7D2Ca475ImpPOA9GtMq1tp0GVjek2tpafu/tVDCXy6l1ANS399b4jXddbpfhJyqgPhNJpVJcYVTAyaHWpFC/VYlEQkA2lZ1CMBi0nwPU6rtjj8Hv34X+x+6acL9rfuh5BFvzGzFWYNx50oeQaduY1YZRU4+Zq59GsGVeZZswpvfqnjH+GavU4qjBgN6rz0Lyb/8PRiSK+ktWIvKeD9iDAQPxDNq60mNW3bawoCWK6qj/1zZRMKZ+o9SzMo/SKqB+o9RzVDKZ9DQRdQ6o30geshTIC4xvvfVWvPTSS1CLcakREqfHPffcg7/+9a+44447RqpceeWVEx7or776avvhcuyhRinVQ065/3iom/umw94JM5YNxq33rEVUvTkNlP5GoHxQFzG3F3B65rtbTr0wUl4QjN3V2Ul0dV0oH8wxUzmd1GMZ/QpUyj1Dv3L6I053z2j/2ucQ/9PzsNJD9/zW23+C6H/sCxTwoNj2xY8i+drfs6ZSzzzvBtR+9BhOpR5jq7pnjH/G0u+6vojrtsSRSI1uRaUiV0cNzGuO2osj+vlQgz7quZb379K7WKr7t2pX3a94yFLAERirh+/rr78e69evh4LjfKBYdXflypVoa2vDddddl7P3lTqVWgmT/vdfEbvsRFiD/bZOVUeeiqplXxUxWqzy4VTqnKevqwVSr7yE9LNrYMyYhcgnl6F5wSJ0d3fzxuqq6s6Ccyq1M528KMWp1F6o7KyNnNs1pVP2d8b2jKgipshy8S1nfkicSj1d5uu3pRBPjn43rsqq02Tn2WFURfwNFJxK7eyc9aIUp1J7obJ/2sgJxgqKzznnHHvLJDXKq/53GJLUA4gaITn77LNx4oknQm3JpI7ly5fbi3MtWrTIXsX6iiuuwFVXXYUDDzwwpzKVDMbD4qibvBFUP/qy3ogSjHOevq4V6F9xPpK//TmQSY+0sfOTf0Ys6nzmhmvJMbD9slBNw/J6Khaln6gAwVjOWZETjOWkWhGZlAMYV0UMzJkZQsTnW0USjOVccgRjOV5IyGQEjJctW2bvU9zb22uvlrdgwQI88MAD9vcoaqXq8ccuu+yCJ5980p7KqYBYge9hhx1mF1uxYgWee+45dHR0oLW11V5466ijjnLUX4KxI5lKUohgXBLZ7RGVnmXvg9Wf/f191ZIPoPrSOwCuuloaY8a0SjAuuQUjCRCM5XhBMJbjhcrEb2DcN2iivTuNdGZ0OvX8WSHUVPn/u0yCsZxrg2AsxwsJmWSNGEtIiGAswYXJcyAYl8YbNbW+5wsfnADGwRmzUL/6l9yrszS2ZLVKMBZgwo4UCMZyvCAYy/HCj2Csck6bFgbjlj2BrjpiIBSUNZOuUIcJxoUqp78ewVi/pn6OSDD2s3se504w9ljwMc11f2bJBDCe8YULgE+dBAjYyqt0yshomWAswweVBcFYjhcEYzle+BWMZSmoLxuCsT4ti41EMC5WwfKqTzAuLz9d7Q3B2FV5pw2efvUviF3+hZGF2UI7LcL8B36D3iRXtSydK6MtE4wluDCUA8FYjhcEYzleEIxleUEwntyP9MY3MXj7FUi/8TKiBx+FqmPPRGDGbFfNIxi7Kq/vghOMfWdZ6RKeDIzNbZthdrQhMHchAo0zilpZtHQ980fL9rYOsR4gEoURrcaMGTO4KrUQ6wjGQowgGMsxAgDBWJQdvvvGWJZ6xWWT2b4VVkcbjOY5UJ9BNcyYYe/vze0vR3VVUDxwzZnIbHxz5B+D79gbtRfcguC8XYozYJraBGPXpPVlYIKxZ7apxSP8/W3MeDDuOWUprG2bRrYLin74UFR9+VoEuBiUJ2cVwdgTmR01QjB2JJMnhThi7InMjhohGDuSybNCflt8yzNhXG6o75JlyPzzJagdR9QRed9BmH3BTUg2zKx4MFbv+9VLf7Xnef9VpyD9t9+P7G0+bEv9bT9GaNd3ueYSwdg1aX0ZmGDsom3qR7Dvy4cjs+E1deUj9I53o+bq7yBYW+9iq+6FHgvG6T+/gP7rzoa5Y8/l4VYbHnwBweYW95Jg5BEFCMZyTgaCsRwvCMZyvCAYy/FCZUIw9t6P9D9eRP+tF8Js25DV+Jy7n0Jmlz2Q3gHL3mdW+hbbutKIDZowzaFVx1tuXgbr9b/Zz8tjj/pv/Rih3QjGpXesMjIgGLvoc9+5RyL9xj8Ac3SD+vABH0PNuTf4clR1LBgPfm8lEk/cAyuZyFKw8fY1MBbsDiOg9mHm4aYCBGM31c0vtp/AOP7T7yLx6EpYA/2oPulCRA45BkZ1bX4dFlyaYCzHHIKxHC8IxqXxIv6TB5H4wd0wu7ZnJdB64/eQ2XNfZIzKfFbqj5vY2pW9FVfV80+i4We3w+pqH9FKPUsqMA4u3tM1Azli7Jq0vgxMMHbRtslWElbNNT3+Jxg+HDXmiLGLJ0sBoQnGBYjmUhW/gHH8wRWIr3l4ZBE3JUf1589D9NATymbbL4KxSyd5AWEJxgWI5mIVjhi7KO4UoTliPLkwmzuS6I9b4weHMeeHV8P87Rr7HqWguPaaBxF+13uBoHt7VxOMvb8uJLdIMHbRnXIGYyUbvzF28eRxEJpg7EAkj4r4BYx7Tj4Q5rZNE1RpvIs6bHoAACAASURBVP9ZBFp28kgtd5shGLurbz7RCcb5qOV+WYKx+xpP1gK/MZ6oyvaeNHr6TWR2TKMeLrHz7LC9XzWg9q72ZjSdYFya60JqqwRjF50ZWH01kmsfg5VKjrRSc9qliHxiGQwf7j072arUmbdeRWbD6wi94z0ItMzlqtQunk/jQxOMPRQ7R1Neg3H/ivOR+v0vYCXiCO25BDWXrEJwZu5v+wnGcs6ZSsiEYCzLZYJx6fzIbHgT5sbXYczfDcG5O6NxZnNFr0qtFtza0J5GPDn6qWHNlldRf/9FsDa+CWOnhai7eCWCu7zD9edKgnHprguJLROMXXYl/sP7hr7nS6VQc8aViBx8FBAKu9yqO+G5j7E7uhYalWBcqHL663kJxv2rLkPq2Z/CSgyOdCS0z/tRe8HNCDTNmrZzscs+j9TLfwQy6ZFy4f/6CGq+ch0CjTP1C1OCiBwxLoHoUzRJMJbjhcqEYCzHD+5jPLQadUdvBgNJE7X92xC+4TR7oGX0xhZGw60/RHDRO101jmDsqry+C04w9p1lUyecylj26n6RcMCVjaEIxrJOFoKxHD+8BONiRn3VSvmxi45D+rW/ApkMQrv9J2quuBvB5lY5YhaZCcG4SAE1VicYaxRTQyiCsQYRNYUgGGcLOXjnVUg8+xNYA7GsP9Td8CjCey4BXFzQlWCs6aQukzAEY41GKjBNpy2EQwZCwew9iy3TtPdpg/rPheONzYkd32oMxZ87M4T6Gr2LFRCMXTCuiJAE4yLE01zVL2Cc3W3/760+mY0EY80ndxHhCMZFiOdCVYKxC6IWGJJgnC1c/60XIf27pydsAVq//HsI7rkEBhffKvBMY7V8FSAY56vYFOXXb0tlfytRZWDezDCMdBK9px0Ms6PNrhmYszPUZuWB2gZNLQNqL7je/qGN48ceu86LIBjQB+IEY22WaQlEMNYio5YgXoJx/NFViP/o/qw365H/OQrVJ1+MQH2Tlv74OQjBWI57BGM5XqhMCMZy/CAYZ3uRXv86Bq87G+mNb2b9oWHlT1zdqkk1xhFjOdeFhEwIxhpcUIsHbNyeHtmkfDjkorkRDH7pE8hsfCNrw/Lwkg/ai+UENO0d+uaWpD1aPG5PdBCMNZgrOATBWI45XoKx6vXgE/cg8f27YQ30IfqJZfaWS0advpdtcpTNPxOCcf6auVWDYOyWsoXFJRgXppsbtQjGE1VN/u5pDN5zDcztbTCiVaj9+n0I/8e+rm7VRDB24+z2d0yCsQb/umNptPdkJoDpzi1hJE7YD1Z/34RWdO5lvKkjhf7B0ZX9hhsjGGswV3AIgrEcc7wGYzk9l5cJwViOJwRjOV6oTAjGcvwgGAPJlInBhIlQKIDqaAAaJzjmZTRHjPOSq+wLE4w1WDzdiHH/cftOBGPDQNNjL8GordfQOuyRajVqPHY7uFmNIcyoC2r9pJlTqbXYpS0IwViblEUHIhgXLaG2AARjbVIWHYhgXLSEWgMQjLXKWVSwSgfjbT1DnwCaO8Z01PI7ag/jqog3exePNY9gXNSpXHaVCcaaLJ3qG+PEY6uQeOIeWMnESEu151yP8IGfgqF526a+QdP+kampAsJB/T8uBGNNJ4umMARjTUJqCEMw1iCiphAEY01CaghDMNYgosYQBGONYhYZqpLBWA3mbGhPIZFSC0COHrMag2isDWpdG8eJTQRjJypVThmCsUavEykTyTRQFTbslamHD7UEfeK+66FWpq7+0tUIv+8gGMGQxpa9CUUw9kZnp600NTSgp7cX2beWHLWHP0R3aXV0p7mXWzmCsRxHCcZyvCAYy/FCZUIwluNHJYNxOmNh0/Y01DPz2KOxLojm+uCEXV3cdo1g7LbC/opPMPaXXyXNlmBcUvlHGk/99ucYuO0SmPEB+99qvnQ1ogcfBUwzA8HcvgU9Z30SGIzZC8FFjz4d1cd9GUYkKqNTPs+CYCzHQIKxHC8IxnK8cBuMBx9cgcRTj9r7o9d86RqE9/8IjGi1LAEEZVPJYGxZasQ4nbWTi7KmdcbQNqNef2tMMBZ0YQhIhWAswAS/pEAwLr1TahXini98KGurHpVV40MvIDCzZcoEuz+zZMK37rVX3YPIkg+5vuJj6VVzPwOCsfsaO22BYOxUKffLEYzd1zifFtwaMY5ddSrSf/0drHRqJJ3ay+5AeN8P8+XrFAZVMhgrSbr60uiKmVCjx+pQsyznNYcQDev/DDDXNUIwzqVQZf2dYFxZfhfVW4JxUfJpqZz8f7/CwIrzYe0YLR4OWnfNAwjvtf/kkBvrRffJB9pb+4w9QrvvhdprH0Kgpk5LbpUchGAsx32CsRwvCMZyvFCZuAHGCob7zj0SmXX/yupscP5i1F5+F9T/8pioQKWDsVJEfWucNmGPEIeCo58fen2+EIy9Vlx2ewRj2f6Iyo5gXHo7Uq+8hP4rT4E12J+VTP3NP0Bo972BwMS3reZADL0nfWjCiHF43w+h5uKV2vbTLr06pcuAYFw67ce3TDCW4wXBWI4XroFxMo6+845G5q1XszprzJiN3nPvQ7xlCIxnNQShviENej1PVpYFI9kQjIs3JvncT5H43kqY/X2oOvVSRPY/GEZVTd6BCcZ5S1bWFQjGZW2v3s4RjPXqWVA0y0L3ZyduAWZv/1XXMGVIBcaZ9i3ZMH3XWoTU23yhC3Gpt8lvbU0iY9qfRaOuykDLjHBJ3yxPJTDBuKCz2ZVKBGNXZC0oKMG4INlcq+TGiLFKtvcrhyPz5itZeSeOvQB9+x8Fq2Z0W8r5s8OoiXo/VdY1QYsITDAuQjwAgw/dguRT34PZ1zMSqObL1yL8wU/kPQuOYFycF+VWm2Bcbo662B+CsYvi5hHaTMQxcNN5SP35twi9Yx/UXHwbgo0zp49gmoipOr97GkZNPeq+/m2oqdSTjTDnkYqrRdXe3BnTsqF4+JjZEMTM+pDni3Pk6ijBOJdC3v2dYOyd1rlaIhjnUsjbv7sFxmasG7FLP4/Mm2o6tYXwhw5D5+HnIl6Xve6F+v2eUReCC7tJeiukhtYIxsWJONnLGCNahfqbn0Bw4R55BScY5yVX2RcmGJe9xfo6SDDWp6WOSOW+j/Ebm4fAeOyhZorv0hJGOCRr1IFgrOOM1hODYKxHRx1RCMY6VNQXwy0wHslwx1tM9d3oZNvxcDr1qJcVA8aZNFJ/fgGZ7VsQ3vu/EWydX/SCn2pV676vfmrCLAWlbsPKnyC4eM+8LhqCcV5ylX1hgnHZW6yvgwRjfVrqiFSJYKwW6FjQIm86NcFYxxmtJwbBWI+OOqIQjHWoqC+G62A8JtX121ITtuOZPzuEmmhQX4d8HKkSwNjq60bfxcuQefvfI05Vff58VH3iuGk//XJia98FxyDz77/BMkf3Qg4f8DHUnHYZArPmOAkxUoZgnJdcZV+YYFz2FuvrIMFYn5Y6IpU7GG9oT2IwkT1iPK85jNqqgLjPognGOs5oPTEIxnp01BGFYKxDRX0xvARjNXi8tTuN2KBpT51unREkFI+xshLAOPaN05H+y/OwUsmsk7iQUd3xV4EZH0D/JcuQef2fsCwTwZ0Xo/aywlZBJxjr+40ph0gE43Jw0aM+EIw9EtphM+UOxkqG9p40umMZG4Rbm0Koqw6KgeLB796KxE8fsveUrvvc2ag+9kykw1GH7rGYWwoQjN1SNv+4BOP8NXOzhpdg7GY/yiF2JYDxZN8BK+/qb/sxQru+S4+N6TQsAzCCoYLjEYwLlq4sKxKMy9JWdzpFMHZH10KjVgIYF6qN2/UG7rwKqd/8GOqt9fBRfewZiB59OgzuC+22/NPGJxiXVP6sxgnGcrxQmRCM5fhRCWDcv+J8pH7/S1iJwSzhdYwY63SSYKxTTf/HIhj730PPekAw9kxqRw0RjB3J5EqhnpMPhLlt04TYjfc/i0DLTq60yaDOFCAYO9PJi1IEYy9Udt4Gwdi5Vm6XrAQwthJx9J2fvcd1zXk3IvL+jxa037BbnhCM3VLWn3EJxv70rSRZE4xLIvuUjRKMS+cHwbh02udqmWCcSyHv/k4wdq51R+/QZyNqVYWWRvXZSACBgOE8gIOSBGMHInlUpBLAeFjKzPY2ID6AwKy5MKqqPVLYeTMEY+daVUJJgnEluKypjwRjTUJqCkMw1iRkAWH6rz4DqT/9NmtRkeqPfBrRUy+FUd9UQERW0aUAwViXksXHIRg703BLZ8pepGrsnu2tM0Ko1wzHBGNnfnhRqpLA2As9i2mDYFyMeuVXl2Bcfp661iOCsWvSFhSYYFyQbFoqqX0UB645cwSOq963FPUX3Ix0Tb2W+AxSuAIE48K1012TYOxM0cm2NlJb0+00K4RoWN+e7QRjZ354UYpg7IXKztogGDvTqVJKEYwrxWkN/SQYaxBRYwiCsUYxiwllWairr0cymbT/41FaBQjGpdV/bOsEY2deTAbGgQAwf1YYVRGCsTMV/VWKYDy9X6mMha1dai9s2J8VNDcEEQ7q/bRgOAOCsb+uHbezJRi7rXAZxScYyzKTYCzHD+5jLMcLgrEcLwjGzrxYvy2JRMrKmko9syGEptoA1MixroMjxrqULD4OwXhqDdMZYNP2FBIpc6RQJBzAvOYQIiF91wPBuPjzuBwjEIzL0VWX+kQwdknYAsMSjAsUzoVqboBxMj30xjyRAhpqg2iuDyCoeTEeF6QoeUiCccktGEmAYOzMC9O0sNEGgSE4rq0y0NIUQjikb7RYZUIwduaHF6UIxlOr3NaVtr+5V9fF2GPnljCqNc6gIBh7cab7rw2Csf88K1nGBOOSST9pwwRjOX7oBuNU2sKG9hTSmdEHg0jYsKdW6hxBkqOgvkwIxvq0LDYSwbhYBfXWJxjr1bOYaKUEY3PHQKyari/x2NiexEAiG4pVngta9H5aQDCW6H7pcyIYl94D32RAMJZlFcFYjh+6wXhzRwr98exValVvF86JuDKVTI6SxWdCMC5eQ10RCMa6lNQTh2CsR0cdUUoBxmo2gnrhGk+OTlGePzuMmqgsQlb3vq1d6awXwwRjHWcdYzhRoCgwXrNmDR555BE8+uijWW3dfffd+NWvfoUtW7agubkZxx9/PI499lgn+aCzszOrXCQSgfovFos5qs9C7ilAMHZP20IilxKMt3Wn0dOvwM2yV01VN9egrHtrIZIWXEc3GL+9NWFPoR5/LJoTQdiFb6wK7rjAigRjOaYQjOV4oTIhGMvxoxRgPNlIbCgI7DQrrHX1cx0qb+9NoydmIrNjOrXKsSZqwDD4jbEOfRljagUKAuP29nacdtpp6O7uxty5cyeA8W233YYDDjgAixcvxiuvvIILL7wQq1atwpIlS3J6QTDOKVHJChCMSyb9pA2XCow7+9Lo6M1kLRSjYG2X1ggq9RNY3WAcU2/MO1PIjL7Yt88BgnHua5BgnFujkRJqTqV90ep/2FRtEIzz8MKDogRjD0R22EQpwHiy1c9Vum5NUXYoxbTF1Ci3Cyyc1SZXpdbhVPnEKAiMh7v/7LPPYvXq1RPAeLw8p556Kg4++GB89rOfzakcwTinRCUrQDAumfSiwPiNzcmRt7hjE1s8N1Kx37/qBmOla3vP0Kj88AIkO88Oo1rYlDdZV8RQNgTj3K6YHW3oPe8YWB1tduHwew9EzVeXI9DUnOMh1UL67/8fMhteQ+Q/34vA/F2BYGjKOgTj3F54WYJg7KXa07dFMJbjBcFYjhcSMnEdjOPxOD75yU/ihhtuwH777ZezzwTjnBKVrADBuGTSiwLjN7cMgbF6k5sNxmphqMqcT+0GGA9r68Ubc1lndnHZEIxz69dz8oEwt23KKlhz8kUIf+yzCNTUTR7ANNH7xY8gs3UThi/+8Ps/itovXQ2jYcakdQjGub3wsgTB2Eu15YFx/2AG23oyUIs7Dh+zG0NoqK3sHQ8IxnKuCwmZuA7G3/zmN6GmXqup1GMP9R3y+OOMM87A4OBg1j8Hg0Go/5LJpAS9KjoH9W1HNBqFetnBo/QKKAAohRe9/Rls6UxmgXFNVRDzZ1fuVGq1DkImk7H/41FaBRSMqd+qVGqSj7RLm5qI1q3EILaddOAQ4I45gjNbMOvOnyE4e+6kecYeuwOx790Bs7836++zH/wtwvMXTVpHvUwNh8NIJBIi+l7pSVRXV094xqp0TUrVf/UspX6jzOEloj1KREHx9p6hha2aG0L2LCS3pyp71LWCm1G/UWq9lHQ6XXCMQiqq5wbFNzxkKeAqGN9666146aWXoCBYjaiMPe69994JSqgp1wRjWSfI2GwIxrK8KRUYKxW6Y2l7j101mtlQE8ScmWEEKvUDY8BeIJBgLOP6IBjn8CGdwrYTP4h028asguHd3oUZ1z6I0Kw5kwbovOQEJP70PKx09guHWXc9hchu7wIm2fuFYCzjmhjOgmAsx49SgbEcBeRkQjCW44WETFwBY/XW5frrr8f69euh4Hg8FE/XcU6llnBaTJ4Dp1LL8qZUi2/JUkFGNm5OpZbRQ/9kwanUub2KXXky0n/7f1mQW3/jYwjtsQ8wxQhGYu3jiD94E8y+nqwGGr/9awTmLpi0UU6lzu2FlyU4ldpLtadvqxTfGMvpvaxMOJValh+lzkY7GCsoPuecc+wRlCuvvNL+X3UoqFIPLLkOgnEuhUr3d4Jx6bSfrGWCsRw/CMZyvCAYO/Mi8cRqxH/8HSBSjZoLViC0x7thTLOQlorad84RSL/5CrBj+mfVMWeg6pjTYUzxXTLB2JkXXpUiGHuldO52CMa5NfKqBMHYK6X90U5BYNzW1oZly5bZ30eobxzr6+tx2GGH2UDc19eHpUuXTuj9LrvsgieffDKnKgTjnBKVrADBuGTST9owwViOHwRjOV4QjN31wtq2CeltmxDceTcE1KJb03ygSDB214t8oxOM81XMvfIEY/e0zTcywThfxcq7fEFg7KYkBGM31S0uNsG4OP101yYY61a08HgE48K1012TYKxb0cLjEYwL186NmgRjN1QtLCbBuDDd3KhFMHZDVf/GJBj71zvPMycYey75tA0SjOX4QTCW4wXBWI4XBGM5XqhMCMZy/PACjNXK0+pQuyiqxVN5TK4AwZhnxlgFCMY8HxwrQDB2LJUnBd0EY7XatNq+QN1Lvb6hZja/jdjXjoXZ0wUjHEadWhRo8Z6AEbAXDEpvXofwXvsjOG/hlAsFeWLAmEYIxl4rPnV7BGM5XhCM5XhBMJblhZtgrO7fG9pTiCdNu9OBri2Ydf/5sN54GYHGZtRedgeCu+8Ng1sF2foQjGVdG6XOhmBcagd81D7BWJZZboHx+q0JxMfsyLLTrDBqqwLedD6dQvey/4LV35fVXuNda9F31ckwt20e+ffoJ5ah+vPnwahr8Ca3aVohGJfcgpEECMZyvPA7GFumOfRisExG2zhiLOfacBOMt3SkEYtn7O0UkU5g5vLjENz4albnG277XwR3/Q85gpQwE4JxCcUX2DTBWKApUlMiGMtyxg0w7oqlsb1nxw11THd3nRdB0IN9ilN/eh4D138J5mB/ltih/3wvMq/9HVYing3M9/0GgdadS24MwbjkFhCM5VgwkolfwdhKJtB3zqeR2fC6mjqDQOtOqFv+PQRnzxOosvOUCMbOtXK7pJtgvH7b6Ghx5O/Poe6xaxDs2JLVpbqr7kFonwNghId2jqnkg2Bcye5P7DvBmOeDYwUIxo6l8qSgG2D81tYkkqmh75LGHovmhBEOuT9qPBUYGzX1QLwfagRn7FF/9y8Qmr/IE72na4RgXHILCgbjchsVlOME4Fcw7jv/GGRe/zusTGZEzvC7/xs1X7sFgaZZkiTOKxeCcV5yuVrYTTDe0J5EPGnZI8aRP/8S9d9fjkDX1mwwvuIuhN7zQRiRqKv99ENwgrEfXPIuR4Kxd1r7viWCsSwL3QDjzZ1pxAZGHwaHe7x4XgQhD0aMMcVU6shHPo3U738JayCWZULj/c8g0DK/5MYQjEtuQd5gXK6jgnKc8BaMrfgA+i49AeYb/7CBtuZL1yCy9HAY0eq8Jek5+UCY2zZNqNd4/7MItOyUdzwpFQjGUpwA3ATjZNrE5o700EvueAwzbjoBoU2vZb9U/taPENrtP+UIUsJMCMYlFF9g0wRjgaZITYlgLMsZN8BYDci+uSUBc8ygcW2Vgbkzwwh4AcYAJlt8K7hwD/R++VBYm96yFwVTR80plyDysc/AqK4tuTEE45JbkDcYl+uooBwnvAXjnlOWDsHsjt8HpUP9jY8h+I53wwiF8pJlUjA2DDTep17EEYzzEpOFJ1XATTBWDap7eF9/BhkTiG74GzK3fc2+txqBAGovvR2hJR/iaPEOZwjGvEjHKkAw5vngWAGCsWOpPCnoBhjbN1TTQntP2p6KNaM+iPrqgOcrU08loNnVDivWA2P2PASqajzR2UkjBGMnKnlTxuniW+U6KuiNys5a8WoqtTXQh96zPwmzPfs7yughR6PqpIsQqG9ylvCOUvEnViP+/dVQcYePmjOuROTgo2AI+t3Jq1PcrilfuVwt7zYYu5p8mQUnGJeZoUV2h2BcpICVVJ1gLMttt8BYVi/9kQ3BWI5PRYFxGYwKynHCuxFj3WCsNEy+sBbx+5fDig+i+rTLEH7/ITAiVZLkzTsXTqXOWzLXKhCMXZM278AE47wlK+sKBOOytldv5wjGevUsNhrBuFgF9dUnGOvTsthITsG4XEcFi9VPZ32vRoxVzjqnUuvUQFIsgrEcNwjGcrwgGMvxQkImBGMJLvgkB4KxLKMIxnL8IBjL8cIpGJfrqKAcJ7wbMVZ91rn4liQNdeZCMNapZnGxCMbF6aezNsFYp5r+j0Uw9r+HnvWAYOyZ1I4aIhg7ksmTQgRjT2R21Eg+YOwoIAsVrICXI8YFJ1lBFQnGcswmGMvxgmAsxwsJmRCMJbjgkxwIxrKMIhjL8YNgLMcLgrEcLwjGcrxQmRCM5fhBMJbjBcFYjhcSMiEYS3DBJzkQjGUZRTCW4wfBWI4XBGM5XhCM5XhBMJblxTAYJ/75J6Tf+CeCi/dEcNE7uYVSCWwiGJdAdMFNEowFmyMtNYKxLEcIxnL8IBjL8YJgLMcLgrEcLwjGsrxQYLz51EOQeu3vgGXayYXetS9qzrkRwbkLZCVb5tkQjMvc4Dy7RzDOU7BKLk4wluU+wViOHwRjOV74DYwtywIMA4YcCbVlQjDWJqWWQJxKrUVGLUEir/8N25efg0zbhqx49Su+j+A73g0jENDSDoPkVoBgnFujSipBMK4kt4vsK8G4SAGnq64ejtVhOH88Jhi76EeeoQnGeQrmYnG/gHF/PIOtXRmkM0PXfn1NEC1NIQTL6HmYYOziiV5AaIJxAaK5VeUXj6Pnu9+C2bU9G4y/fi+C7/5vGOGIWy0z7jgFCMY8JcYqQDDm+eBYAYKxY6kcFzR7O9F3xsdg9narDUdQ9YnjED3lYgSi1TljEIxzSuRZAYKxZ1LnbMgvYLyuLYVUemgK5fAxf3YE1REjn/djOfUoZQGCcSnVn9g2wViOH1Wb3sC2b5yOzOa3s8H4licR2n2vvF6Sy+mVPzMhGPvTN7eyJhi7pWwZxiUY6ze1+zNLYPX3ZQWuPfcGhD98GIxQeNoGCcb6/Sg0IsG4UOX01/MDGKcyFjZsS42MFg+r0FQXQHODGjV2PnNEv4L6IhKM9WmpIxLBWIeKemKob4y3XHAcUn/+LaxU0g4aOejTqPr8eQjOmqOnEUZxpADB2JFMFVOIYFwxVhffUYJx8RqOjWDGB9B74gcmgHFg1lw03PlzGDV1BGO9krsWjWDsmrR5B/YDGGdMC29vnQjGzY1BzKgLoUy4GATjvE9fVysQjF2VN6/gw6tSp7o7YHa1w2iaBaO2gd8W56WinsIEYz06lksUgnG5OOlBPwjGekVWb4l7jt9/AhgHd9kD9Tc9RjDWK7er0QjGrsqbV3A/gLHq0Nvbkkgkd6wtsKOHC+dEEAmVx2ix6hLBOK9T1/XCBGPXJXbcAPcxdiyV6wUJxq5L7KsGCMa+squ0yRKM9evfe/ahyKz/NzC8+JZahOeWJxDabS8gx6qUnEqt349CIxKMC1VOf71iwdjs68bgzRcg/Y8XEdrnAFSd9XUEZ8x2lKgaCQ6oFaYdsm1nXxo9AyYiQQOzm0JlBcUEY0enjKeFCMaeyj1tYwRjOV4QjOV4ISETgrEEF3ySA8HYHaMGVl6G5G9+BESrUHfZnQi+670wgsGcjRGMc0rkWQGCsWdST2ioO5aBAkzDMNDSFMTMxhoEg0EMDAzknZQZH0TfWR+DuW3zSN1AVTXq71yLQMu8KeONX2G6KhLAvOYQQkGHhJx3pv6owBFjWT4RjOX4QTCW4wXBWI4XEjIhGEtwwSc5EIxlGUUwluMHwbg0XrR1ptA3aI6dcIFd59ehrjqEwcHBvJOKf/9OxJ+4F9ZA9oJ4DSt/guDCPaacxTHZCtOtM0JoqAk6Hj3OO1kfVCAYyzLJd2CcScMciMGI1sCIlNf2RQRjOdcGwViOFxIyIRhLcMEnORCMZRlFMJbjB8G4NF5MBqS1NVEsmluNZCJ/MB644wqknvkJ1MJ4Y4/6Fd9HaPe9gUlmcky1kFZddcDel7iSR40JxqW5LqZq1U9gPPjwt5BY8zCsWI/dneinvoCqY85AoKlZlqgFZkMwLlA4F6oRjF0Q1cchCcY+Ns/r1AnGXis+fXsEYzl+EIxL48VkYFxdHcXiuVVIJeN5J5V569+IXXkSzM5tWXUb7v01gnMWTBpPLQ/w1taJexLPrA9iRn2wbLZeyltMLr5ViGSu1vELGFvbt6DvypORWf969guqm59A6B17l8UevwRjV0/1vIITjPOSq+wLE4zL3mJ9HSQY69NSRySCsQ4V9cQgGOvRMd8o69qSSKWzV3aeM6sGs5siSMTzHzFW7cd/dB/ij90x9O2jdAAAIABJREFUslp83aW3I7TfgTAi0SnT29yRQn88e0r3Lq1hRMOBfLtUVuU5YizLTr+AcfLZn2DgOzfC6tiaJWDdJauGrsVolSxhC8iGYFyAaC5VIRi7JKxPwxKMfWpcKdImGJdC9anb9BSMTROWaQ4tCuZ0yV1ZcrmaDcHYVXmnDD5+GnN1xMDineoRDhe2+NbYhiwzAyOQexG84TqxuInO3hQioSDUfsThCl94S+lCMC7NdTFVq34B4/Q/X8LArRcis2V9Nhhf8yBCe70PRjAkS9gCsiEYFyCaS1UIxi4J69OwBGOfGleKtAnGpVC99GAc+/ppSP35t0AmYydTf9PjCO2xT87tpGSp5W42BGN39XUSXU1pVu9sit2uyUlbLONMAYKxM528KuUXMLYsC7FzjkD6zVdGtjIM1DWibvn3EFz4Dq/kcrUdgrGr8uYVnGCcl1xlX5hgXPYW6+sgwVifljoieTFinHjqMcTvvQ7muIWMmh57CUZdg45ulEUMgrEcGwnGcrwgGMvxQmXiFzC2VTNNJNZ8F8nnn0LoXe9F9PATEZgxS5agRWRDMC5CPM1VCcaaBfV5OIKxzw30Mn2CsZdq527LCzDuPf2jyGxeN/LWfjirxkf+gEDjzNxJVkgJgrEcownGcrwgGMvxwndgLEs67dkQjLVLWnBAgnHB0pVlRYJxWdrqTqcIxu7oWmhUL8B4/DTq4VybHn0RRn1joamXXT2CsRxLCcZyvCAYy/GCYCzLC4KxHD8IxnK8kJAJwViCCz7JgWAsyygvwNjs7kDvF/8H1kDfSOdD71yC2m/eh0BNnSxBSpgNwbiE4o9rmmAsxwuCsRwvCMayvCAYy/GDYCzHCwmZEIwluOCTHAjGsozyAoxVjxUc999yIay3X0Xk8C8gevjnYYQjssQocTYE4xIbMKZ5qWA8mMhga3fG3l6qviaI2Y0hBH28m5PZ04nYNWfBfO2vCOy8G2ovvQPBOTtnrVpPMJZzXRCMZXlBMJbjB8FYjhcSMiEYS3DBJzkQjPUaZXZsRezas2Ctfw3hDx2K6i9eDqOqxnEjXoGx44QquCDBWI75EsF4MJnBlo4M0pnRPZfVHsc7zQoh5MdtndIp9J7+P8hs3ZRlfON9zyDQOn/k3wjGcq4LgrEsLwjGcvwgGMvxQkImBGMJLvgkB4KxPqOsvm70nLIU1kBsJGhgxmw0rP4FDIdTlAnG+vwoNhLBuFgF9dWXCMZvb00ikRqF4uHeLpwTQSRk6Ou8R5FSf/g1BlZeCjVqPPZouPExBN7xbhihoX1mCcYeGeKwGV+tSu2wT34tRjCW4xzBWI4XEjIhGEtwwSc5EIz1GRW7+WtI//ZnsNKprKCNDz6PQHOro4YIxo5k8qQQwdgTmR01IhKMt6WQTJlQey2PPfwKxolnf4LB1VdDveAbe9Rf+yCC73ovjFCYYOzobPW2EMHYW72na41gLMcLgrEcLyRkQjCW4IJPciAY6zMqdukJSP/jj7AymaygDfc+g0DLTkibFkLBAIxpBpMIxvr8KDYSwbhYBfXVlwjGPf0ZbO/JIGOOkrG6the2RhD24Yix2deNvq98Cmb75uwXe9/+NQJzF4z8G0eM9Z3XOiIRjHWoqCcGwViPjjqiEIx1qFg+MbLAeN26dTj++OOxdu1a1NfXZ/UynU5j+fLlUA8dF1xwwbQKrFq1Cg8++GBWmf333x+33357TuU6O7OnZkUiEaj/YrHRKac5g7CAKwoQjPXJmvrLCxi49myYg/1ZQZOr/4Be1MHaMbRUXxNAa1MIgcBEQiYY6/Oj2EgE42IV1FdfIhir3m3vSaO734RpWggEgAWzw4iE/bv61vjp1LWX3YHwvh+GEYmKBGP1m2pM96ZR3ykoNhLBWI41BGM5XhCM5XghIZMRMD7rrLPw2muvoaurC88880wWGD/33HO46aab0N3djSOOOMIRGCvAPe+887JukNXV1Tn7TDDOKVHJChCM9Uo/eN9yJNZ8F1YqaQeuvuFxbGzYE6aR/bC8aG4E4UkW6CEY6/WjmGgE42LU01tXKhjr7aWgaOol3hTAWeoR4/GLnjXVBdHcEERwkheNghR1LRWCsWvS5h2YYJy3ZK5VIBi7Jq0vA2eNGPf19WHp0qUTwHi4ZytWrLD/TycjxirWpZdemrcoBOO8JfOsAsHYHaktMwMjEERs0EJbV8oeURp7zJ8VRk3VxJEl0WA8zcOyOyqWNirBuLT6j22dYCzHi1KD8bq2FFJpc9zvaQjVUfWZiv8WPSvWWYJxsQrqq08w1qdlsZEIxsUqWF71XQPjxx9/HOphsbm5GR//+MftKdpODoKxE5VKU4Zg7K7uiaSJDdsVGGe3s0trGGprl/GHRDBOPP19xO+5BmZi0E63/uYfILT73rDnjpbxQTCWYy7BWI4XpQTjVMbChm2prC2ylDJNdQE0N6g9pAnGcs6UysuEYCzHc4KxHC8kZOIKGG/YsAGZTAbRaBSvvvoqrr32Wpx++uk4+uijR/p88cUXT+i/+oY5lcpepVfBmHqzq+LxKK0CyodgMAj1vTkPdxR4Y3McqfToiHEwaGDXeVWY7BkuHA7bXgx/j+xORs6jpto2ou0ktQVVX1al+Wv+hUB9k/NAPiyprgvlgzn+rYYP++L3lHnPkONgKe8ZaqGzdVsSE8B4dlMYM+pDk/6mylHOnUzUPWP8M5Y7LTFqLgXUSyP1XCvl/p0r33L+e6nu36pddb/iIUsBV8B4fBfvuece/PWvf8Udd9wx8qdf//rXE34QDj74YKgp2GMP9eOhfswHB4dGoHiUTgH1kFNbW8uF0PKwQK063XPRcUj980V7Beraky9E7ZGnAuHIlFFigxnEUxaqwgbqqoNTllML5KlF6aTcWAcevxP9D98GK5nIynnmPb9EaMFuU36HmIecYouq9RPUAydfGpXeIrVYo/qtSiSyz8PSZ5ZfBmrWRfypx5HZ8Dqi+38E4Xe/P2thq/yilaa0evBTL8gHBgZKksBbW5MTtslSW2RNNgOnJAl63Ki6Z4x/xvI4BTa3QwE1Sql+ozjoU/pTQv1GqeeoZHJovRevDnUOqN9IHrIU8ASMV65ciba2Nlx33XU5e8+p1DklKlkBTqXOX/re0w9BZvNbGLuBae15NyL8oUNH9vrMP+pQDWlTqRM//x4G77seViKe1aWGb/8awTk7lzUYcyp1oWex/nrlMJVazbroPfuTMNu3jAgUes8BqD3/ZgSamvWL5lLEUk6lHu5Se3cKvYMWQgFgzsxQxUKx0oPfGLt0ohcQllOpCxDNpSqcSu2SsD4NWzQYq6mDZ599Nk488USoLZnUoaZEq9HfRYsW4ZVXXsEVV1yBq666CgceeGBOmQjGOSUqWQGCcX7SW6aJns/tB6s/exYEaurR9MD/waipyy/guNLSwFhtPdX7hQ9m9zcQRNOjf4RRm739W1EdF1iZYCzHlHIA44Gbv4bk79ZOfMl072+GXjL55JAAxj6RypM0CcaeyOyoEYKxI5k8KUQw9kRm3zQyAsbLli3Dli1b0NvbC3XBLliwAA888IDdkaeffho33njjyHRmNW3w8ssvt1ewVlMHFRAr8D3ssMPs8mr1arXFU0dHB1pbW+2Ft4466ihHohCMHclUkkIE4/xkrzQwVuqY7ZsRu+IkZDatQ2jPJai74m4YZf59seo3wTi/a8PN0uUAxn3nHon0G//A+JX4Glb/EsGdFropn9bYBGOtchYdjGBctITaAhCMtUlZdCCCcdESllWArBFjCT0jGEtwYfIcCMb5e1NJU6nzV6d8ahCM5XhZDmAcX/Mw4g/fCivWmyVs433PINA6X47YOTIhGMuyimAsxw+CsRwvCMZyvJCQCcFYggs+yYFgnL9RasGt/ss+j/QrL9mLb1WdeD6qjzh52sW3nLYibSq107zLsRzBWI6r5QDGSs0+9bvx8h+BzNAuAHWXrELovUt9tQAXwVjOdaEyIRjL8YNgLMcLgrEcLyRkQjCW4IJPciAYyzKKYCzHD4KxHC/KBYyVolYyDqjF7GrqYARDckR2mAnB2KFQHhUjGHsktINmCMYORPKoCMHYI6F90gzB2CdGSUiTYCzBhdEcCMZy/CAYy/GinMC4UFU7etPoimXsT5QNA1jQEi7JaszlBsZqh3mjUFME1CMYCzBhRwoEYzleEIzleCEhE4KxBBd8kgPBWJZRBGM5fhCM5XhR6WDcn7CwtTOFdEZh3OixaE4E4ZC3WFcOYJxKW9jQPqpnJGxg/qwwQkFvtdRxhRGMdaioJwbBWI+OOqIQjHWoWD4xCMbl46XrPSEYuy5xXg0QjPOSy9XCBGNX5c0reKWD8fptScST2VCsBFzYGkYkHMhLy2ILlwMYr2tLQsHx2KOxLohZDUEEA/6CY4JxsWe0vvoEY31aFhuJYFysguVVn2BcXn662huCcRHy2nMa1Tw8fQ+mBOMi/NBclWCsWdAiwlU6GG/uSKE/bsGysmGOYFzYSbWuLYVU2syqHAgY2KUl7PkIfGE9GK1FMC5WQX31Ccb6tCw2EsG4WAXLqz7BuLz8dLU3BOP85TUHYug77SMwe7rUUjoILnon6m94FEZNXf7BxtUgGBctobYABGNtUhYdqNLBOJkysWl7GqkxU6nVFOqdZ3s//bc8RowngrFfp1MTjIv+edEWgGCsTcqiAxGMi5awrAIQjMvKTnc7QzDOX9+eEw+A2bEtq2Lk4KNQc+ZVMKLV+QccU4NgXJR8WisTjLXKWVSwSgdjJV48aWJrVxrJlIXaqgBaZoRK8k1sOYBxe08aPf0mTHN0BH7+7DBqovpm/xR1wudRmWCch1guFyUYuyxwHuEJxnmIVQFFCcYVYLKuLhKM81NSTWXs+ey+sPr7JlRsevxPMGrr8ws4rvRYMM5sfBOxq06FuW0jwks+iNqLvgWjprj4RSVXYZUJxnIMJxjL8aIcwFipGYub6OhJ2yt8z24ModqHUKz6QTCWc20QjOV4QTCW44WETAjGElzwSQ4E4/yN6v7MkglgHKiuQcMDz2sD43THVvSd8TFYA6MArqZqN37n/4puI/8eV2YNgrEc3wnGcrwoFzCWo2hxmRCMi9NPZ22CsU41i4tFMC5Ov3KrTTAuN0dd7A/BOH9xB2+/EolfPQErnRqpXHflaoSWfAhGKJR/wDE1hkeMYzecg+QLa4FMOite4wO/RWDWnKLaYGVnChCMnenkRSmCsRcqO2uDYOxMJ69KEYy9Ujp3OwTj3Bp5VYJg7JXS/miHYOwPn0RkSTAuzIbEU48i/vCtQDCE2vNuQmiv/YFgsLBgk4Bx72WfR+bvf4CVyWSD8X3PItAyD/b8Px6uKkAwdlXevIITjPOSy9XCBGNX5c07OME4b8lcq0Awdk3avAMTjPOWrKwrEIzL2l69nSMY69Wz2GjDI8bJf76I/itPgTXYnxWy6bGXYNQ1FNsM6ztQgGDsQCSPihCMPRLaQTMEYwcieViEYOyh2DmaIhjL8YJgLMcLCZkQjCW44JMcCMayjBq7+NbgE/cg+egqmIm4PULccPsaBBfsztFijywjGHsktINmCMYORPKoCMHYI6EdNkMwdiiUB8UIxh6I7LAJgrFDoSqkGMG4QozW0U2CsQ4V9cWYdLsmy/IEhs2ONpjbNiHQugCBGbM8aVOfcvojEYz1a1poRIJxocrpr0cw1q9pMREJxsWop7cuwVivnsVEIxgXo1751SUYl5+nrvWIYOyatAUFLtU+xr1fPhzm26/CMk0779A+70ftZXciUF1bUD/KoRLBWI6LBGM5XhQDxhnTwkDCAiygOgqEgv7bN1iOE0OZEIzlOEIwluMFwViOFxIyIRhLcMEnORCMZRlVCjA2X38ZvZccP+F75sbvPAdj1lwYFbrQF8FYzrVBMJbjRaFg3B83sbUrjXTGGunMgpYwqiKE42LcJRgXo57eugRjvXoWE41gXIx65VeXYFx+nrrWI4Kxa9IWFLgUYBz/2SOI378clvqWecxRf+NjCL3zPUCgMh9cCcYFncKuVCIYuyJrQUELBeN1bUmk0qNQrBqPhA3MnxVGKMhV9gsygyPGhcrmSj2CsSuyFhSUYFyQbGVbiWBcttbq7xjBWL+mxUQsBRhzxHhyxwjGxZzJeusSjPXqWUy0wsE4hVR66FONsceiORGEQwTjQj3hiHGhyumvRzDWr2mhEQnGhSpXnvUIxuXpqyu9Ihi7ImvBQUsBxipZfmM80TKCccGnsfaKBGPtkhYcsHAwnjhirKZRz2sOccS4YDf4jXER0mmvSjDWLmnBAQnGBUtXlhUJxmVpqzudIhi7o2uhUceCsan2ME6l7H2LDQ+mM6c3vA7zrX8htOteMObs7EmbherkRT2CsRcqO2uDYOxMJy9KFQrG8aSJzR3Z3xjv0hpGNFyZn2ro8oojxrqULD4Owbh4DXVFIBjrUrI84hCMy8NHT3pBMPZEZseNDINx9xcPgbl5HaC2agJQv+L7CL3j3RX7va9jATUWJBhrFLPIUG6CcXzNw0j+5EEEmltRdfY3EZy3sOJfCk1nV6FgPBwzmbJgGBbCIQJxkZeFXZ1grENFPTEIxnp01BGFYKxDxfKJQTAuHy9d7wnB2HWJ82pAgfHmFRchsea7sFLJrLpNj/8JRm19XvFYuHAFCMaFa6e7pltg3HfZ55F++Y9AJj2ScsNdaxGcv7ji9/GeysNiwdjpuWFZFpIvPI3M336PwMJ3IvLhQxHg798E+QjGTs8o98sRjN3X2GkLBGOnSlVGOYJxZfispZcEYy0yaguiwPitQ98Js6sD9mafY47G7/0RgYYmbW0x0PQKEIzlnCFugLGVjKP3jI/C3LY5q6PVR56K6GfO4kuoKez3Cox7zzgEmY3rRrMIR9C4+pcItMyTc2IKyIRgLMCEHSkQjOV4QTCW44WETAjGElzwSQ4EY1lGKTB++5SDkfn33wAzewXXpsdfglHbICvhMs6GYCzHXDfA2Iz1oO/Lh8NszwbjyIcORfUZVyLQMEOOAIIy8QKMM+v+hdhVp8Ds3JbV8/rljyC45xIYwZAgRUqbCsG4tPqPbZ1gLMcLgrEcLyRkQjCW4IJPciAYyzJKgXHnG6+i54yPwRroG0kuesgxqD79ChjRalkJl3E2BGM55roBxmqqbu+pS2Fu3ZTV0ZqLvoXIfx0MIxItqQBqsaqtXWmob3JrqwJomSFj9WYvwDj5h19jcOWlMHs6szyou2QVQu9dWnJvSnpijGucYCzHDYKxHC8IxnK8kJAJwViCCz7JgWAsy6jhxbfM3i4M3HMtMhvfQNUxZyD8XwdxlMRjqwjGHgs+TXNugLFqLv3a39F/9RkjI5Oh9xyA2vNvRqCpuaSdT6ZMbNqeRioz+jmF2ut359nhkm9t5AUYmz0d6Pvqp2Fu35LlQ8PtaxBYsDsXRxujCsG4pJdq9vnZ0ICBgQGk06NrFsjJrrIyIRhXlt+5ekswzqUQ/z6iAMFY1slQqn2MZakgIxuCsQwfVBZugfFwD634ABCKwAjJmKK7uSOF/rgFNao99ljYGkakxNsbeQHGqs+JJ1Zj8PurR2bOVH3iOERPOA+B+kY5J6aATAjGAkzYkQJHjOV4QTCW44WETAjGElzwSQ4EY1lGEYzl+EEwluOF22Asp6dDmazflkQ8mQ3F6t8rCYxVf610egiMq2oRiESk2SQiH4KxCBvsJAjGcrwgGMvxQkImBGMJLvgkB4KxLKMIxnL8IBjL8aLSwLg/YWFrZwrpMVOplRuL5kSgplSX8vBqxLiUffRT2wRjOW4RjOV4QTCW44WETAjGElzwSQ4EY1lGEYzl+EEwluNFpYGxUr6jN42uWMZenN4wgAUtYURLPI1a5UUwlnNdqEwIxnL8IBjL8YJgLMcLCZkQjCW44JMcCMayjCIYy/GDYCzHi0oEYznqZ2dCMJblDMFYjh8EYzleEIzleCEhE4KxBBd8kgPBWJZRBGM5fhCM5XhBMJbhRcYEIuEgamtr0dvbKyOpCs+CYCznBCAYy/GCYCzHCwmZEIwluOCTHAjGsowiGMvxg2AsxwuCsXte9CdMqD2Tq8IB1EQD9rTt8UcsbtrfPCswDgaDaG6qRV0kXvKto9xTxT+RCcZyvCIYy/GCYCzHCwmZEIwluOCTHAjGsowiGMvxg2AsxwuCsTtevLU1hVTaxPCuUJGwgfmzJu6VvK5tqJw6FBgrPxqrkqitmhyk3cmWUSdTgGAs57wgGMvxgmAsxwsJmRCMJbjgkxwIxrKMIhjL8YNgLMcLgrF+LwYSGbR1ZiasfK0W+aqKBEYaTGUsbNg2ukL2MBiHjTiaG4IIBkq7SrZ+ZfwVkWAsxy+CsRwvCMZyvJCQCcFYggs+yYFgLMsogrEcPwjGcrwgGOv3oieWxvZeExkze7/k+bPDqI4YMHbMqVarYr+1NTkC0MNgXB2Ko6kuiMBkc6/1p8uIUyhAMJZzahCM5XhBMJbjhYRMCMYSXPBJDgRjWUYRjOX4QTCW4wXBWL8XyZSFTdtTUCPCY49dWkKIRoJZ/7ZhWxKDyaFyw2A8uy4FNfWaR2kVIBiXVv+xrROM5XhBMJbjhYRMigLjNWvW4JFHHsGjjz46oS/pdBrLly+3vy+64IILHPe1s7Mzq2wkEoH6LxaLOY7Bgu4oQDB2R9dCoxKMC1VOfz2CsX5NC41IMC5Uuenrbe1Oo7c/M/KNsRoBnmp6dO9ABt2xDGqrw9h5TgMG+vvcSYpR81KAYJyXXK4WJhi7Km9ewQnGeclV9oULAuP29nacdtpp6O7uxty5cyeA8XPPPYebbrrJ/vsRRxxBMC6T04hgLMtIgrEcPwjGcrwgGLvnhWla9nTqUHB0+vR0rXEfY/e8KCQywbgQ1dypQzB2R9dCohKMC1GtfOsUBMbDcjz77LNYvXr1pCPGqsyKFSvsohwxLo8TiGAsy0eCsRw/CMZyvCAYy/GCYCzHC5UJwViOHwRjOV4QjOV4ISETgrEEF3ySA8FYllEEYzl+EIzleEEwluMFwViOFwRjWV4QjOX4QTCW44WETEoGxscddxxMtYTlmOOxxx5DKpXK+jcFY2rFy0wmI0Gvis5B+aAWU1Hfj/MovQLhcNj2whreWLT0KVVsBuq6UD6M/02rWEFK2HHeM0oo/rimec+Q44XKRN0zxj9jycqwcrJRL43Ucy3v36X3vFT3b9Wuul/xkKVAycD49ddfn6DEbrvthr6+7EU61I+H+jEfHByUpVwFZqMecmpra7kQmhDv6+vrbS94Yy29IdXV1fYDJ18ald4LtVij+q1KJBKlT6bCM1APftFoFAMDAxWuhIzuq3vG+GcsGZlVXhZqlFL9RnHQp/Teq98o9RyVTCY9TUadA+o3kocsBUoGxlPJwFWpZZ0gY7PhVGpZ3nAqtRw/OJVajhecSi3HC06lluOFyoTfGMvxg1Op5XjBqdRyvJCQCcFYggs+yYFgLMsogrEcPwjGcrwgGMvxgmAsxwuCsSwvCMZy/CAYy/FCQiYFgXFbWxuWLVtmTx2Mx+NQ03MOO+wwnHPOOXafnn76adx4440j05/VNMPLL78cS5cuzdlnjhjnlKhkBQjGJZN+0oYJxnL8IBjL8YJgrM8LKxFH/w1fRfrPzwNNs1B/xd0ILtwDcPhdHMFYnxc6InHEWIeKemIQjPXoqCMKwViHiuUToyAwdrP7BGM31S0uNsG4OP101yYY61a08HgE48K1012TYKxP0Z6TD4S5bVNWwIbVv0Rwp4WOGiEYO5LJs0IEY8+kztkQwTinRJ4VIBh7JrUvGiIY+8ImGUkSjGX4MJwFwViOHwRjOV4QjDV4YVlIPPu/GLznWlh93VkBa06/AuGDj0KgujZnQwTjnBJ5WoBg7Knc0zZGMJbjBcFYjhcSMiEYS3DBJzkQjGUZRTCW4wfBWI4XBOMivbAs9Jx2EMytm4BJtoKr/vx5iBx6AgI1dTkbIhjnlMjTAgRjT+UmGMuRe9pMCMY+McqjNAnGHgldDs0QjGW5SDCW4wfBWI4XBOPivEj+4dcYXHkpzJ7OSQPVf+vHCC3e09F3xgTj4rzQXZtgrFvRwuNxxLhw7XTXJBjrVtTf8QjG/vbP0+wJxp7KnbMxgnFOiTwrQDD2TOqcDRGMc0o0bYHBH6xG4gerYQ30TSiXzzRqVZlgXJwXumsTjHUrWng8gnHh2umuSTDWrai/4xGM/e2fp9kTjD2VO2djBOOcEnlWgGDsmdQ5GyIY55Ro2gKp1/6OgavPgNm5LatcPotuDVckGBfnhe7aBGPdihYej2BcuHa6axKMdSvq73gEY3/752n2BGNP5c7ZGME4p0SeFSAYeyZ1zoYIxjklylmg/5ozkXrp/2ClknbZ6NGno/rYM2A4+K54bHCCcU6pPS1AMPZU7mkbIxjL8YJgLMcLCZkQjCW44JMcCMayjCIYy/GDYCzHC4KxHi/MgRjQ3wujYSaMaFVBQQnGBcnmWiWCsWvS5h2YYJy3ZK5VIBi7Jq0vAxOMfWlbaZImGJdG96laJRjL8YNgLMcLgrEcLwjGcrxQmRCM5fhBMJbjBcFYjhcSMiEYS3DBJzkQjGUZRTCW4wfBWI4XBGM5XhCM5XhBMJblBcFYjh8EYzleSMiEYCzBBZ/kQDCWZRTBWI4fBGM5XhCM5XhBMJbjBcFYlhcEYzl+EIzleCEhE4KxBBd8kgPBWJZRBGM5fhCM5XhBMJbjBcFYjhcEY1leEIzl+EEwluOFhEwIxhJc8EkOBGNZRhGM5fhBMJbjBcFYjhcEYzleEIxleUEwluMHwViOFxIyIRhLcMEnORCMZRlFMJbjB8FYjhcEYzleEIzleEEwluUFwViOHwRjOV5IyIRgLMEFn+RAMJZlFMFYjh8EYzleEIzleEEwluMFwViWFwRjOX4QjOV4ISETgrEEF3zlthXuAAAML0lEQVSSA8FYllEEYzl+EIzleEEwluMFwViOFwRjWV4QjOX4QTCW44WETAjGElzwSQ4EY1lGEYzl+EEwluMFwViOFwRjOV4QjGV5QTCW4wfBWI4XEjIhGEtwwSc5EIxlGUUwluMHwViOFwRjOV4QjOV4QTCW5QXBWI4fBGM5XkjIhGAswQWf5EAwlmUUwViOHwRjOV4QjOV4QTCW4wXBWJYXBGM5fhCM5XghIROCsQQXfJIDwViWUQRjOX4QjOV4QTCW4wXBWI4XBGNZXhCM5fhBMJbjhYRMCMYSXPBJDgRjWUYRjOX4QTCW4wXBWI4XBGM5XhCMZXlBMJbjB8FYjhcSMiEYS3DBJzkQjGUZRTCW4wfBWI4XBGM5XhCM5XhBMJblBcFYjh8EYzleSMiEYCzBBZ/kQDCWZRTBWI4fBGM5XhCM5XhBMJbjBcFYlhcEYzl+EIzleCEhE4KxBBd8kgPBWJZRBGM5fhCM5XhBMJbjBcFYjhcEY1leEIzl+EEwluOFhEwIxhJc8EkOBGNZRhGM5fhBMJbjBcFYjhcEYzleEIxleUEwluMHwViOFxIyIRhLcMEnORCMZRlFMJbjB8FYjhcEYzleEIzleEEwluUFwViOHwRjOV5IyIRgLMEFn+RAMJZlFMFYjh8EYzleEIzleEEwluMFwViWFwRjOX4QjOV4ISETgrEEF3ySA8FYllEEYzl+EIzleEEwluMFwViOFwRjWV4QjOX4QTCW44WETMSBsQRRmAMVoAJUgApQASpABagAFaACVIAKVI4C4sF47dq1eP7553HNNddUjitCe7plyxacdtppWLNmjdAMKyutj33sY3j44Ycxa9asyuq4wN5+7Wtfw8c//nEcdNBBArOrrJTUNbF9+3acc845ldVxgb39y1/+glWrVuG+++4TmF3lpbTffvvhxRdfrLyOC+zxSSedhHPPPRd77723wOwqK6VbbrkFLS0tOP744yur4+ztpAoQjHliOFaAYOxYKk8KEow9kdlRIwRjRzJ5Uohg7InMjhohGDuSybNCBGPPpM7ZEME4p0SeFSAYeya1LxoiGPvCJhlJEoxl+DCcBcFYjh8EYzleEIzleEEwluOFyoRgLMcPgrEcLwjGcryQkAnBWIILPsmBYCzLKIKxHD8IxnK8IBjL8YJgLMcLgrEsLwjGcvwgGMvxQkImBGMJLvgkB4KxLKMIxnL8IBjL8YJgLMcLgrEcLwjGsrwgGMvxg2AsxwsJmYgHYwkiMQcqQAWoABWgAlSAClABKkAFqAAVKF8FCMbl6y17RgWoABWgAlSAClABKkAFqAAVoAIOFCAYOxCJRagAFaACVIAKUAEqQAWoABWgAlSgfBUgGJevt+wZFaACVIAKUAEqQAWoABWgAlSACjhQgGDsQCQWoQJUgApQASpABagAFaACVIAKUIHyVUAsGB911FF4++23Jyj/s5/9DK2treXriNCePfDAA/jxj3+MRCKBlpYWfPWrX8WSJUuEZlveaf3hD3/AypUr7etj9913x3nnnYe99tqrvDstrHdr1qzBI488gkcffTQrs3/+85+49tpr8cYbb2DevHk4//zzccABBwjLvrzSWbduHY4//nisXbsW9fX1WZ1Lp9NYvnw5qqqqcMEFF5RXxwX2pre3F2eeeSZOO+00HHjggSMZ/upXv8JDDz2Et956C9FoFAcddJB9bUQiEYG9KI+Upjr3N23ahOuvvx6vvvoqBgYGsHDhQtuzD3zgA+XRcYG9cPI7pHw5+eSTceSRR+L0008X2IvySWmq+/eqVavw4IMPZnV0//33x+23314+nWdPciogFoz7+/thmuZIBzZu3Gj/eCswrq2tzdkxFtCnwC9/+UvceeeduP/++zFjxgyo///qq6/GL37xC/uBk4d3CqgHyxNPPNF+sNlnn33w05/+FHfccQd++MMfYtasWd4lUqEttbe32w/93d3dmDt3bhYYp1IpfOpTn8IxxxyDT3/603j++edxww032B41NTVVqGLudvuss87Ca6+9hq6uLjzzzDNZYPzcc8/hpptusr064ogjCMbuWmHfI9S53tnZaZ/3Y8H4Bz/4gX3v2HvvvdHT04NLLrkEH/3oR+1riYd+BaY799U95OWXX7bvH3V1dfa9Q720UNePYRj6k6nwiE5+h9Q1o37L1LF06VKCsUvnzHT3b9WkAmPlhRpsGD5CoRCqq6tdyohhJSogFozHi3XZZZfZIzBnn322RB3LOqd77rkHL730ElavXm33MxaL2Q89P//5z+3RYx7eKXDvvffiX//6F1asWDHSqBotO/zww3Hsscd6l0iFt/Tss8/a18PYEeMXX3wRaj/j3/zmNyMPmMcddxzUf4ceemiFK+Ze9/v6+uyHyfFgPNzi8LXCEWP3PBgb+XOf+5z9YD8WjMe3fNddd+HNN9+0X1zw+P/bO5tQer4wjp+ULUXKwkteSqJkYcnKgoWdokSKZCHZkbwvLBS2SrZiYcFGFlbCliyUrFiwYE1Kfn1OXf3/mnvvzOTcmWu+p+5u5syZzzN3znyf8zzPcUcg27PP4sPu7q797+zs7LgbiHr+nrN/vodYBGLRZ2RkxNoBh6tWjN0+MF7zd0oYM5/Mzs66HYB6jzWBvBDGd3d39kVxeHhoioqKYg30Lw6O1fqxsTHT1NRkBgcHzcXFhcHzNj8//xdvN9b3tL6+bl5eXuyKcaotLCyYkpISMzU1Feux/6XBeU2sBwcH5ujo6H+hWDMzM6ayslIOPYfGlzB2CDdE136EMak4jY2NZnx8PMQVdIpfApmEMelROFrr6upsak5xcbHfbnVcCAJetvj4+DCTk5Omu7vbRhstLS1JGIdgG/SUTMJ4f3/fRlKUlpZau7DwoJYsAnkhjPngb25uNqOjo8myTkzulpf3ysqKHQ15rfwQZsqdzL2BcEogtjY2NkxLS4vBabG8vGzD4iSMc2cPr4mVnOOzszOztbX1PRA+dJhktVrpzjYSxu7Yhuk5mzAmHYqcPaItlGIQhrD/czIJY/KLcbKyYnx/f2+2t7cVSu0fbeAjvWzB3F1dXW2Gh4dtfxLGgbGGOiGdMH58fDSfn5+2DgI5+NQLYVGut7c31HV0Un4SiL0wvrq6sh+VrBYrtziahwxvMkVV5ubm7ADInaRwCkKgvr4+mkEl+Kp4NPH2YxNWXcivJIy6r68vwVRye+vpVoyPj4/tKkyq4cSoqKgwExMTuR1ggq4mYRwvY2cSxqenpzb/mLoIFA5Uc0sgWyg1V397ezPt7e22uCbvKjU3BLxsgSBGgKUaRbrI82YhiJouam4IpBPGP69GGuH19bV9X6klh0DshTGrxB0dHWZoaCg5VonZnaYqVg4MDHyPjGI2hMF1dXXFbLTJGg753tiCSbSqqipZNx/h3abLMUYIU5wuVcSG/OL+/n6bA67mhoCEsRuuYXtNJ4wRXjiNcLTW1taG7V7nBSDgRxhTnK6zs9NQqbe8vDxA7zo0CAE/ttCKcRCi4Y/1K4x5Vz0/P5vV1dXwF9OZeUcg1sL4/PzchvCyWqzqx9E9W3zMsBUK4W9MnGwXND09bfb29jSRRmAWXtT8Hx4eHuxHZkNDgy36pJY7Al4TKykHCGCEAdvNEVZNKBZ5x+SAq7khIGHshmvYXr2EMWG6RFNQI+G/2y1S7VWVkMOSzn6elxijQjiNbWhgv7m5aV5fX20Ukpo7AhLG7tgG7TmdMGZrP5xENTU15vb21tbRWVxczFhIMOi1dXz8CcRWGH99fdmk956eHrviohYdAcJ7yJtEHLMlDdXBKRjR2toa3aASfGXC2HEaEfbGajGrkgUFBQkmkrtbxylB5AT/g/f3d7s9EO+oVH73zc2N9S6zty7VRbGV9gd1Zx9s8fT0ZNMKKMxI1ETqA//k5MSsra3ZUFEaIox0ECpYq/0+AVjDHEcFjrvCwkKDCMMphFhmW62fjTlF28z9vi0yPfuXl5e2oj7bNiGM29rarGO1rKzs9weiHu1/wu97SCvGbh+YbPM3zgu218JRhAMPDYKTWy1ZBGIrjJNlBt2tCIiACIiACIiACIiACIiACIhAVAQkjKMir+uKgAiIgAiIgAiIgAiIgAiIgAjEgoCEcSzMoEGIgAiIgAiIgAiIgAiIgAiIgAhEReAfwEgv4Vke4HwAAAAASUVORK5CYII=",
"text/plain": [
""
]
diff --git a/docs/source/user_guide/annotation.rst b/docs/source/user_guide/annotation.rst
index 98805ca350..bea898b7c9 100644
--- a/docs/source/user_guide/annotation.rst
+++ b/docs/source/user_guide/annotation.rst
@@ -357,12 +357,13 @@ settings that you declare in this way will be passed as keyword arguments to
methods like
:meth:`annotate() `
whenever the corresponding backend is in use. For example, you can configure
-the URL, username, and password of your CVAT server as follows:
+the URL, username, email, and password of your CVAT server as follows:
.. code-block:: shell
export FIFTYONE_CVAT_URL=http://localhost:8080
export FIFTYONE_CVAT_USERNAME=...
+ export FIFTYONE_CVAT_EMAIL=...
export FIFTYONE_CVAT_PASSWORD=...
The `FIFTYONE_ANNOTATION_BACKENDS` environment variable can be set to a
From c0590ff811925d3d356c493fbd5f900b89dc1098 Mon Sep 17 00:00:00 2001
From: brimoor
Date: Mon, 9 Dec 2024 22:59:12 -0500
Subject: [PATCH 022/104] clarifications
---
docs/source/integrations/cvat.rst | 7 ++++---
docs/source/plugins/developing_plugins.rst | 10 +++++-----
docs/source/plugins/using_plugins.rst | 8 ++++----
docs/source/release-notes.rst | 4 ++++
docs/source/teams/secrets.rst | 4 ++--
docs/source/tutorials/cvat_annotation.ipynb | 4 ++--
docs/source/user_guide/annotation.rst | 5 +++--
7 files changed, 24 insertions(+), 18 deletions(-)
diff --git a/docs/source/integrations/cvat.rst b/docs/source/integrations/cvat.rst
index 95337c7ab5..c64fe7fedf 100644
--- a/docs/source/integrations/cvat.rst
+++ b/docs/source/integrations/cvat.rst
@@ -211,14 +211,14 @@ which can be done in a variety of ways.
The recommended way to configure your CVAT login credentials is to store them
in the `FIFTYONE_CVAT_USERNAME` and `FIFTYONE_CVAT_PASSWORD` environment
-variables. Optionally, you can also set the `FIFTYONE_CVAT_EMAIL` environment variable. These are automatically accessed by FiftyOne whenever a connection
+variables. These are automatically accessed by FiftyOne whenever a connection
to CVAT is made.
.. code-block:: shell
export FIFTYONE_CVAT_USERNAME=...
- export FIFTYONE_CVAT_EMAIL=...
export FIFTYONE_CVAT_PASSWORD=...
+ export FIFTYONE_CVAT_EMAIL=... # if applicable
**FiftyOne annotation config**
@@ -233,7 +233,8 @@ You can also store your credentials in your
"cvat": {
...
"username": ...,
- "password": ...
+ "password": ...,
+ "email": ... # if applicable
}
}
}
diff --git a/docs/source/plugins/developing_plugins.rst b/docs/source/plugins/developing_plugins.rst
index 472375388d..fe0ffb5576 100644
--- a/docs/source/plugins/developing_plugins.rst
+++ b/docs/source/plugins/developing_plugins.rst
@@ -251,8 +251,8 @@ plugin's `fiftyone.yml` looks like this:
secrets:
- FIFTYONE_CVAT_URL
- FIFTYONE_CVAT_USERNAME
- - FIFTYONE_CVAT_EMAIL
- FIFTYONE_CVAT_PASSWORD
+ - FIFTYONE_CVAT_EMAIL
- FIFTYONE_LABELBOX_URL
- FIFTYONE_LABELBOX_API_KEY
- FIFTYONE_LABELSTUDIO_URL
@@ -1528,8 +1528,8 @@ plugin declares the following secrets:
secrets:
- FIFTYONE_CVAT_URL
- FIFTYONE_CVAT_USERNAME
- - FIFTYONE_CVAT_EMAIL
- FIFTYONE_CVAT_PASSWORD
+ - FIFTYONE_CVAT_EMAIL
- FIFTYONE_LABELBOX_URL
- FIFTYONE_LABELBOX_API_KEY
- FIFTYONE_LABELSTUDIO_URL
@@ -1545,8 +1545,8 @@ plugin, you would set:
FIFTYONE_CVAT_URL=...
FIFTYONE_CVAT_USERNAME=...
- FIFTYONE_CVAT_EMAIL=...
FIFTYONE_CVAT_PASSWORD=...
+ FIFTYONE_CVAT_EMAIL=...
At runtime, the plugin's :ref:`execution context `
is automatically hydrated with any available secrets that are declared by the
@@ -1558,8 +1558,8 @@ plugin. Operators can access these secrets via the `ctx.secrets` dict:
def execute(self, ctx):
url = ctx.secrets["FIFTYONE_CVAT_URL"]
username = ctx.secrets["FIFTYONE_CVAT_USERNAME"]
- email = ctx.secrets["FIFTYONE_CVAT_EMAIL"]
password = ctx.secrets["FIFTYONE_CVAT_PASSWORD"]
+ email = ctx.secrets["FIFTYONE_CVAT_EMAIL"]
.. _operator-outputs:
@@ -2504,8 +2504,8 @@ plugin. Panels can access these secrets via the `ctx.secrets` dict:
def on_load(self, ctx):
url = ctx.secrets["FIFTYONE_CVAT_URL"]
username = ctx.secrets["FIFTYONE_CVAT_USERNAME"]
- email = ctx.secrets["FIFTYONE_CVAT_EMAIL"]
password = ctx.secrets["FIFTYONE_CVAT_PASSWORD"]
+ email = ctx.secrets["FIFTYONE_CVAT_EMAIL"]
.. _panel-common-patterns:
diff --git a/docs/source/plugins/using_plugins.rst b/docs/source/plugins/using_plugins.rst
index 3bb963b0aa..a35727baa8 100644
--- a/docs/source/plugins/using_plugins.rst
+++ b/docs/source/plugins/using_plugins.rst
@@ -295,8 +295,8 @@ available metadata about a plugin:
server_path /plugins/fiftyone-plugins/plugins/annotation
secrets FIFTYONE_CVAT_URL
FIFTYONE_CVAT_USERNAME
- FIFTYONE_CVAT_EMAIL
FIFTYONE_CVAT_PASSWORD
+ FIFTYONE_CVAT_EMAIL
FIFTYONE_LABELBOX_URL
FIFTYONE_LABELBOX_API_KEY
FIFTYONE_LABELSTUDIO_URL
@@ -469,8 +469,8 @@ plugin declares the following secrets:
secrets:
- FIFTYONE_CVAT_URL
- FIFTYONE_CVAT_USERNAME
- - FIFTYONE_CVAT_EMAIL
- FIFTYONE_CVAT_PASSWORD
+ - FIFTYONE_CVAT_EMAIL
- FIFTYONE_LABELBOX_URL
- FIFTYONE_LABELBOX_API_KEY
- FIFTYONE_LABELSTUDIO_URL
@@ -492,8 +492,8 @@ plugin, you would set:
FIFTYONE_CVAT_URL=...
FIFTYONE_CVAT_USERNAME=...
- FIFTYONE_CVAT_EMAIL=...
FIFTYONE_CVAT_PASSWORD=...
+ FIFTYONE_CVAT_EMAIL=...
At runtime, the plugin's execution context will automatically be hydrated with
any available secrets that are declared by the plugin. Operators access these
@@ -505,8 +505,8 @@ secrets via the `ctx.secrets` dict:
def execute(self, ctx):
url = ctx.secrets["FIFTYONE_CVAT_URL"]
username = ctx.secrets["FIFTYONE_CVAT_USERNAME"]
- email = ctx.secrets["FIFTYONE_CVAT_EMAIL"]
password = ctx.secrets["FIFTYONE_CVAT_PASSWORD"]
+ email = ctx.secrets["FIFTYONE_CVAT_EMAIL"]
.. _using-panels:
diff --git a/docs/source/release-notes.rst b/docs/source/release-notes.rst
index 5fa058f8cf..6344e829c0 100644
--- a/docs/source/release-notes.rst
+++ b/docs/source/release-notes.rst
@@ -51,6 +51,8 @@ App
`#4931 `_
- Gracefully handle deleted + recreated datasets of the same name
`#5183 `_
+- Added a `referrerPolicy` so the App can run behind reverse proxies
+ `#4944 `_
- Fixed a bug that prevented video playback from working for videos with
unknown frame rate
`#5155 `_
@@ -61,6 +63,8 @@ SDK
:meth:`max() ` and
aggregations
`#5029 `_
+- Optimized object detection evaluation with r-trees
+ `#4758 `_
- Improved support for creating summary fields and indexes
`#5091 `_
- Added support for creating compound indexes when using the builtin
diff --git a/docs/source/teams/secrets.rst b/docs/source/teams/secrets.rst
index 8574ebb2ca..783cbb88eb 100644
--- a/docs/source/teams/secrets.rst
+++ b/docs/source/teams/secrets.rst
@@ -66,8 +66,8 @@ plugin declares the following secrets:
secrets:
- FIFTYONE_CVAT_URL
- FIFTYONE_CVAT_USERNAME
- - FIFTYONE_CVAT_EMAIL
- FIFTYONE_CVAT_PASSWORD
+ - FIFTYONE_CVAT_EMAIL
- FIFTYONE_LABELBOX_URL
- FIFTYONE_LABELBOX_API_KEY
- FIFTYONE_LABELSTUDIO_URL
@@ -83,8 +83,8 @@ secrets via the ``ctx.secrets`` dict:
def execute(self, ctx):
url = ctx.secrets["FIFTYONE_CVAT_URL"]
username = ctx.secrets["FIFTYONE_CVAT_USERNAME"]
- email = ctx.secrets["FIFTYONE_CVAT_EMAIL"]
password = ctx.secrets["FIFTYONE_CVAT_PASSWORD"]
+ email = ctx.secrets["FIFTYONE_CVAT_EMAIL"]
The ``ctx.secrets`` dict will also be automatically populated with the
values of any environment variables whose name matches a secret key declared
diff --git a/docs/source/tutorials/cvat_annotation.ipynb b/docs/source/tutorials/cvat_annotation.ipynb
index c2d610c84a..b8a736a457 100644
--- a/docs/source/tutorials/cvat_annotation.ipynb
+++ b/docs/source/tutorials/cvat_annotation.ipynb
@@ -70,8 +70,8 @@
"outputs": [],
"source": [
"!export FIFTYONE_CVAT_USERNAME=\n",
- "!export FIFTYONE_CVAT_EMAIL= # optional\n",
- "!export FIFTYONE_CVAT_PASSWORD="
+ "!export FIFTYONE_CVAT_PASSWORD=\n",
+ "!export FIFTYONE_CVAT_EMAIL= # if applicable"
]
},
{
diff --git a/docs/source/user_guide/annotation.rst b/docs/source/user_guide/annotation.rst
index bea898b7c9..b355f5b238 100644
--- a/docs/source/user_guide/annotation.rst
+++ b/docs/source/user_guide/annotation.rst
@@ -357,14 +357,15 @@ settings that you declare in this way will be passed as keyword arguments to
methods like
:meth:`annotate() `
whenever the corresponding backend is in use. For example, you can configure
-the URL, username, email, and password of your CVAT server as follows:
+the URL, username, password, and email (if applicable) of your CVAT server as
+follows:
.. code-block:: shell
export FIFTYONE_CVAT_URL=http://localhost:8080
export FIFTYONE_CVAT_USERNAME=...
- export FIFTYONE_CVAT_EMAIL=...
export FIFTYONE_CVAT_PASSWORD=...
+ export FIFTYONE_CVAT_EMAIL=... # if applicable
The `FIFTYONE_ANNOTATION_BACKENDS` environment variable can be set to a
`list,of,backends` that you want to expose in your session, which may exclude
From fe56fb367a275d4642c06cba0741d30e1fe6bb27 Mon Sep 17 00:00:00 2001
From: brimoor
Date: Tue, 10 Dec 2024 10:18:05 -0500
Subject: [PATCH 023/104] add_path_to_sidebar_group util
---
fiftyone/core/dataset.py | 38 +++++++-----------------------------
fiftyone/core/odm/dataset.py | 38 ++++++++++++++++++++++++++++++++++++
2 files changed, 45 insertions(+), 31 deletions(-)
diff --git a/fiftyone/core/dataset.py b/fiftyone/core/dataset.py
index 9db20ee4c4..65e75cd910 100644
--- a/fiftyone/core/dataset.py
+++ b/fiftyone/core/dataset.py
@@ -45,8 +45,7 @@
import fiftyone.core.labels as fol
import fiftyone.core.media as fom
import fiftyone.core.metadata as fome
-from fiftyone.core.odm.dataset import SampleFieldDocument
-from fiftyone.core.odm.dataset import DatasetAppConfig, SidebarGroupDocument
+from fiftyone.core.odm.dataset import DatasetAppConfig
import fiftyone.migrations as fomi
import fiftyone.core.odm as foo
import fiftyone.core.sample as fos
@@ -1866,35 +1865,12 @@ def create_summary_field(
if sidebar_group is None:
sidebar_group = "summaries"
- if self.app_config.sidebar_groups is None:
- sidebar_groups = DatasetAppConfig.default_sidebar_groups(self)
- self.app_config.sidebar_groups = sidebar_groups
- else:
- sidebar_groups = self.app_config.sidebar_groups
-
- index_group = None
- for group in sidebar_groups:
- if group.name == sidebar_group:
- index_group = group
- else:
- if field_name in group.paths:
- group.paths.remove(field_name)
-
- if index_group is None:
- index_group = SidebarGroupDocument(name=sidebar_group)
-
- insert_after = None
- for i, group in enumerate(sidebar_groups):
- if group.name == "labels":
- insert_after = i
-
- if insert_after is None:
- sidebar_groups.append(index_group)
- else:
- sidebar_groups.insert(insert_after + 1, index_group)
-
- if field_name not in index_group.paths:
- index_group.paths.append(field_name)
+ self.app_config._add_path_to_sidebar_group(
+ field_name,
+ sidebar_group,
+ after_group="labels",
+ dataset=self,
+ )
if create_index:
for _field_name in index_fields:
diff --git a/fiftyone/core/odm/dataset.py b/fiftyone/core/odm/dataset.py
index bb25a907c0..559306e5c6 100644
--- a/fiftyone/core/odm/dataset.py
+++ b/fiftyone/core/odm/dataset.py
@@ -624,6 +624,44 @@ def _rename_paths(self, paths, new_paths):
for path, new_path in zip(paths, new_paths):
self._rename_path(path, new_path)
+ def _add_path_to_sidebar_group(
+ self,
+ path,
+ sidebar_group,
+ after_group=None,
+ dataset=None,
+ ):
+ if self.sidebar_groups is None:
+ if dataset is None:
+ return
+
+ self.sidebar_groups = self.default_sidebar_groups(dataset)
+
+ index_group = None
+ for group in self.sidebar_groups:
+ if group.name == sidebar_group:
+ index_group = group
+ else:
+ if path in group.paths:
+ group.paths.remove(path)
+
+ if index_group is None:
+ index_group = SidebarGroupDocument(name=sidebar_group)
+
+ insert_after = None
+ if after_group is not None:
+ for i, group in enumerate(self.sidebar_groups):
+ if group.name == after_group:
+ insert_after = i
+
+ if insert_after is None:
+ self.sidebar_groups.append(index_group)
+ else:
+ self.sidebar_groups.insert(insert_after + 1, index_group)
+
+ if path not in index_group.paths:
+ index_group.paths.append(path)
+
def _make_default_sidebar_groups(sample_collection):
# Possible sidebar groups
From d367c15f2776e84abd7af16889d829bb43f86e21 Mon Sep 17 00:00:00 2001
From: brimoor
Date: Tue, 10 Dec 2024 11:42:11 -0500
Subject: [PATCH 024/104] bump compatibility version
---
fiftyone/constants.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/fiftyone/constants.py b/fiftyone/constants.py
index 893204ef93..18ca73f47a 100644
--- a/fiftyone/constants.py
+++ b/fiftyone/constants.py
@@ -42,7 +42,7 @@
# This setting may be ``None`` if this client has no compatibility with other
# versions
#
-COMPATIBLE_VERSIONS = ">=0.19,<1.2"
+COMPATIBLE_VERSIONS = ">=0.19,<1.3"
# Package metadata
_META = metadata("fiftyone")
From 0948c7c868d7ac5d75fb1fec3f4f9de39fd3f9bd Mon Sep 17 00:00:00 2001
From: imanjra
Date: Fri, 6 Dec 2024 10:40:12 -0500
Subject: [PATCH 025/104] fix an issue where backtick can't be when editing
evaluation note
---
.../NativeModelEvaluationView/Evaluation.tsx | 11 +++++++++--
app/packages/operators/src/state.ts | 6 ++++--
app/packages/state/src/recoil/atoms.ts | 5 +++++
3 files changed, 18 insertions(+), 4 deletions(-)
diff --git a/app/packages/core/src/plugins/SchemaIO/components/NativeModelEvaluationView/Evaluation.tsx b/app/packages/core/src/plugins/SchemaIO/components/NativeModelEvaluationView/Evaluation.tsx
index c7f9c24a4c..d7f932b23f 100644
--- a/app/packages/core/src/plugins/SchemaIO/components/NativeModelEvaluationView/Evaluation.tsx
+++ b/app/packages/core/src/plugins/SchemaIO/components/NativeModelEvaluationView/Evaluation.tsx
@@ -1,5 +1,5 @@
import { Dialog } from "@fiftyone/components";
-import { view } from "@fiftyone/state";
+import { editingFieldAtom, view } from "@fiftyone/state";
import {
ArrowBack,
ArrowDropDown,
@@ -41,7 +41,7 @@ import {
useTheme,
} from "@mui/material";
import React, { useEffect, useMemo, useState } from "react";
-import { useRecoilState } from "recoil";
+import { useRecoilState, useSetRecoilState } from "recoil";
import EvaluationNotes from "./EvaluationNotes";
import EvaluationPlot from "./EvaluationPlot";
import Status from "./Status";
@@ -133,6 +133,7 @@ export default function Evaluation(props: EvaluationProps) {
const triggerEvent = useTriggerEvent();
const activeFilter = useActiveFilter(evaluation, compareEvaluation);
+ const setEditingField = useSetRecoilState(editingFieldAtom);
const closeNoteDialog = () => {
setEditNoteState((note) => ({ ...note, open: false }));
@@ -1295,6 +1296,12 @@ export default function Evaluation(props: EvaluationProps) {
{
+ setEditingField(true);
+ }}
+ onBlur={() => {
+ setEditingField(false);
+ }}
multiline
rows={10}
defaultValue={evaluationNotes}
diff --git a/app/packages/operators/src/state.ts b/app/packages/operators/src/state.ts
index 331ba2a18f..876dd3d6c8 100644
--- a/app/packages/operators/src/state.ts
+++ b/app/packages/operators/src/state.ts
@@ -851,6 +851,7 @@ export function useOperatorBrowser() {
const choices = useRecoilValue(operatorBrowserChoices);
const promptForInput = usePromptOperatorInput();
const isOperatorPaletteOpened = useRecoilValue(operatorPaletteOpened);
+ const editingField = useRecoilValue(fos.editingFieldAtom);
const selectedValue = useMemo(() => {
return selected ?? defaultSelected;
@@ -913,7 +914,8 @@ export function useOperatorBrowser() {
(e) => {
if (e.key !== "`" && !isVisible) return;
if (e.key === "`" && isOperatorPaletteOpened) return;
- if (BROWSER_CONTROL_KEYS.includes(e.key)) e.preventDefault();
+ if (BROWSER_CONTROL_KEYS.includes(e.key) && !editingField)
+ e.preventDefault();
switch (e.key) {
case "ArrowDown":
selectNext();
@@ -922,7 +924,7 @@ export function useOperatorBrowser() {
selectPrevious();
break;
case "`":
- if (isOperatorPaletteOpened) break;
+ if (isOperatorPaletteOpened || editingField) break;
if (isVisible) {
close();
} else {
diff --git a/app/packages/state/src/recoil/atoms.ts b/app/packages/state/src/recoil/atoms.ts
index 2cdea1588a..fca20b2db9 100644
--- a/app/packages/state/src/recoil/atoms.ts
+++ b/app/packages/state/src/recoil/atoms.ts
@@ -385,3 +385,8 @@ export const escapeKeyHandlerIdsAtom = atom>({
key: "escapeKeyHandlerIdsAtom",
default: new Set(),
});
+
+export const editingFieldAtom = atom({
+ key: "editingFieldAtom",
+ default: false,
+});
From 1594990bde82aab739177c9a7bfd0202f648dc86 Mon Sep 17 00:00:00 2001
From: brimoor
Date: Wed, 11 Dec 2024 00:28:45 -0500
Subject: [PATCH 026/104] support on-disk instance segmentations in SDK
---
docs/source/user_guide/using_datasets.rst | 8 +-
fiftyone/core/collections.py | 3 +
fiftyone/core/labels.py | 7 +-
fiftyone/utils/data/exporters.py | 102 +++++++++++--------
fiftyone/utils/data/importers.py | 52 ++++++----
fiftyone/utils/labels.py | 113 +++++++++++++++++-----
tests/unittests/import_export_tests.py | 109 +++++++++++++++++++++
7 files changed, 303 insertions(+), 91 deletions(-)
diff --git a/docs/source/user_guide/using_datasets.rst b/docs/source/user_guide/using_datasets.rst
index 729434e07e..8bab5b83a6 100644
--- a/docs/source/user_guide/using_datasets.rst
+++ b/docs/source/user_guide/using_datasets.rst
@@ -2542,7 +2542,7 @@ Object detections stored in |Detections| may also have instance segmentation
masks.
These masks can be stored in one of two ways: either directly in the database
-via the :attr:`mask` attribute, or on
+via the :attr:`mask ` attribute, or on
disk referenced by the
:attr:`mask_path ` attribute.
@@ -2605,8 +2605,10 @@ object's bounding box when visualizing in the App.
,
}>
-Like all |Label| types, you can also add custom attributes to your detections
-by dynamically adding new fields to each |Detection| instance:
+Like all |Label| types, you can also add custom attributes to your instance
+segmentations by dynamically adding new fields to each |Detection| instance:
.. code-block:: python
:linenos:
diff --git a/fiftyone/core/collections.py b/fiftyone/core/collections.py
index d53a25fce1..8200255fb4 100644
--- a/fiftyone/core/collections.py
+++ b/fiftyone/core/collections.py
@@ -10681,6 +10681,9 @@ def _get_media_fields(
app_media_fields.discard("filepath")
for field_name, field in schema.items():
+ while isinstance(field, fof.ListField):
+ field = field.field
+
if field_name in app_media_fields:
media_fields[field_name] = None
elif isinstance(field, fof.EmbeddedDocumentField) and issubclass(
diff --git a/fiftyone/core/labels.py b/fiftyone/core/labels.py
index e8b9bd9390..e6b09d8267 100644
--- a/fiftyone/core/labels.py
+++ b/fiftyone/core/labels.py
@@ -409,7 +409,8 @@ class Detection(_HasAttributesDict, _HasID, _HasMedia, Label):
its bounding box, which should be a 2D binary or 0/1 integer numpy
array
mask_path (None): the absolute path to the instance segmentation image
- on disk
+ on disk, which should be a single-channel PNG image where any
+ non-zero values represent the instance's extent
confidence (None): a confidence in ``[0, 1]`` for the detection
index (None): an index for the object
attributes ({}): a dict mapping attribute names to :class:`Attribute`
@@ -532,8 +533,8 @@ def to_segmentation(self, mask=None, frame_size=None, target=255):
"""
if not self.has_mask:
raise ValueError(
- "Only detections with their `mask` attributes populated can "
- "be converted to segmentations"
+ "Only detections with their `mask` or `mask_path` attribute "
+ "populated can be converted to segmentations"
)
mask, target = _parse_segmentation_target(mask, frame_size, target)
diff --git a/fiftyone/utils/data/exporters.py b/fiftyone/utils/data/exporters.py
index e2a0780380..7a9b7da68e 100644
--- a/fiftyone/utils/data/exporters.py
+++ b/fiftyone/utils/data/exporters.py
@@ -12,11 +12,13 @@
import warnings
from collections import defaultdict
+from bson import json_util
+import pydash
+
import eta.core.datasets as etad
import eta.core.frameutils as etaf
import eta.core.serial as etas
import eta.core.utils as etau
-from bson import json_util
import fiftyone as fo
import fiftyone.core.collections as foc
@@ -2029,34 +2031,38 @@ def _export_frame_labels(self, sample, uuid):
def _export_media_fields(self, sd):
for field_name, key in self._media_fields.items():
- value = sd.get(field_name, None)
- if value is None:
- continue
-
- if key is not None:
- self._export_media_field(value, field_name, key=key)
- else:
- self._export_media_field(sd, field_name)
+ self._export_media_field(sd, field_name, key=key)
def _export_media_field(self, d, field_name, key=None):
- if key is not None:
- value = d.get(key, None)
- else:
- key = field_name
- value = d.get(field_name, None)
-
+ value = pydash.get(d, field_name, None)
if value is None:
return
media_exporter = self._get_media_field_exporter(field_name)
- outpath, _ = media_exporter.export(value)
- if self.abs_paths:
- d[key] = outpath
- else:
- d[key] = fou.safe_relpath(
- outpath, self.export_dir, default=outpath
- )
+ if not isinstance(value, (list, tuple)):
+ value = [value]
+
+ for _d in value:
+ if key is not None:
+ _value = _d.get(key, None)
+ else:
+ _value = _d
+
+ if _value is None:
+ continue
+
+ outpath, _ = media_exporter.export(_value)
+
+ if not self.abs_paths:
+ outpath = fou.safe_relpath(
+ outpath, self.export_dir, default=outpath
+ )
+
+ if key is not None:
+ _d[key] = outpath
+ else:
+ pydash.set_(d, field_name, outpath)
def _get_media_field_exporter(self, field_name):
media_exporter = self._media_field_exporters.get(field_name, None)
@@ -2333,33 +2339,43 @@ def _prep_sample(sd):
def _export_media_fields(self, sd):
for field_name, key in self._media_fields.items():
- value = sd.get(field_name, None)
- if value is None:
- continue
+ self._export_media_field(sd, field_name, key=key)
+
+ def _export_media_field(self, d, field_name, key=None):
+ value = pydash.get(d, field_name, None)
+ if value is None:
+ return
+ media_exporter = self._get_media_field_exporter(field_name)
+
+ if not isinstance(value, (list, tuple)):
+ value = [value]
+
+ for _d in value:
if key is not None:
- self._export_media_field(value, field_name, key=key)
+ _value = _d.get(key, None)
else:
- self._export_media_field(sd, field_name)
+ _value = _d
- def _export_media_field(self, d, field_name, key=None):
- if key is not None:
- value = d.get(key, None)
- else:
- key = field_name
- value = d.get(field_name, None)
+ if _value is None:
+ continue
- if value is None:
- return
+ if self.export_media is not False:
+ # Store relative path
+ _, uuid = media_exporter.export(_value)
+ outpath = os.path.join("fields", field_name, uuid)
+ elif self.rel_dir is not None:
+ # Remove `rel_dir` prefix from path
+ outpath = fou.safe_relpath(
+ _value, self.rel_dir, default=_value
+ )
+ else:
+ continue
- if self.export_media is not False:
- # Store relative path
- media_exporter = self._get_media_field_exporter(field_name)
- _, uuid = media_exporter.export(value)
- d[key] = os.path.join("fields", field_name, uuid)
- elif self.rel_dir is not None:
- # Remove `rel_dir` prefix from path
- d[key] = fou.safe_relpath(value, self.rel_dir, default=value)
+ if key is not None:
+ _d[key] = outpath
+ else:
+ pydash.set_(d, field_name, outpath)
def _get_media_field_exporter(self, field_name):
media_exporter = self._media_field_exporters.get(field_name, None)
diff --git a/fiftyone/utils/data/importers.py b/fiftyone/utils/data/importers.py
index 11c50f45a5..299827f3c0 100644
--- a/fiftyone/utils/data/importers.py
+++ b/fiftyone/utils/data/importers.py
@@ -14,6 +14,7 @@
from bson import json_util
from mongoengine.base import get_document
+import pydash
import eta.core.datasets as etad
import eta.core.image as etai
@@ -2151,32 +2152,43 @@ def _import_runs(dataset, runs, results_dir, run_cls):
def _parse_media_fields(sd, media_fields, rel_dir):
for field_name, key in media_fields.items():
- value = sd.get(field_name, None)
+ value = pydash.get(sd, field_name, None)
if value is None:
continue
if isinstance(value, dict):
- if key is False:
- try:
- _cls = value.get("_cls", None)
- key = get_document(_cls)._MEDIA_FIELD
- except Exception as e:
- logger.warning(
- "Failed to infer media field for '%s'. Reason: %s",
- field_name,
- e,
- )
- key = None
-
- media_fields[field_name] = key
-
- if key is not None:
- path = value.get(key, None)
- if path is not None and not os.path.isabs(path):
- value[key] = os.path.join(rel_dir, path)
+ _parse_nested_media_field(
+ value, media_fields, rel_dir, field_name, key
+ )
+ elif isinstance(value, list):
+ for d in value:
+ _parse_nested_media_field(
+ d, media_fields, rel_dir, field_name, key
+ )
elif etau.is_str(value):
if not os.path.isabs(value):
- sd[field_name] = os.path.join(rel_dir, value)
+ pydash.set_(sd, field_name, os.path.join(rel_dir, value))
+
+
+def _parse_nested_media_field(d, media_fields, rel_dir, field_name, key):
+ if key is False:
+ try:
+ _cls = d.get("_cls", None)
+ key = get_document(_cls)._MEDIA_FIELD
+ except Exception as e:
+ logger.warning(
+ "Failed to infer media field for '%s'. Reason: %s",
+ field_name,
+ e,
+ )
+ key = None
+
+ media_fields[field_name] = key
+
+ if key is not None:
+ path = d.get(key, None)
+ if path is not None and not os.path.isabs(path):
+ d[key] = os.path.join(rel_dir, path)
class ImageDirectoryImporter(UnlabeledImageDatasetImporter):
diff --git a/fiftyone/utils/labels.py b/fiftyone/utils/labels.py
index 7071d1f001..f28bfac205 100644
--- a/fiftyone/utils/labels.py
+++ b/fiftyone/utils/labels.py
@@ -155,8 +155,8 @@ def export_segmentations(
overwrite=False,
progress=None,
):
- """Exports the segmentations (or heatmaps) stored as in-database arrays in
- the specified field to images on disk.
+ """Exports the semantic segmentations, instance segmentations, or heatmaps
+ stored as in-database arrays in the specified field to images on disk.
Any labels without in-memory arrays are skipped.
@@ -164,7 +164,9 @@ def export_segmentations(
sample_collection: a
:class:`fiftyone.core.collections.SampleCollection`
in_field: the name of the
- :class:`fiftyone.core.labels.Segmentation` or
+ :class:`fiftyone.core.labels.Segmentation`,
+ :class:`fiftyone.core.labels.Detection`,
+ :class:`fiftyone.core.labels.Detections`, or
:class:`fiftyone.core.labels.Heatmap` field
output_dir: the directory in which to write the images
rel_dir (None): an optional relative directory to strip from each input
@@ -183,7 +185,9 @@ def export_segmentations(
"""
fov.validate_non_grouped_collection(sample_collection)
fov.validate_collection_label_fields(
- sample_collection, in_field, (fol.Segmentation, fol.Heatmap)
+ sample_collection,
+ in_field,
+ (fol.Segmentation, fol.Detection, fol.Detections, fol.Heatmap),
)
samples = sample_collection.select_fields(in_field)
@@ -207,16 +211,31 @@ def export_segmentations(
if label is None:
continue
- outpath = filename_maker.get_output_path(
- image.filepath, output_ext=".png"
- )
-
- if isinstance(label, fol.Heatmap):
- if label.map is not None:
- label.export_map(outpath, update=update)
- else:
+ if isinstance(label, fol.Segmentation):
+ if label.mask is not None:
+ outpath = filename_maker.get_output_path(
+ image.filepath, output_ext=".png"
+ )
+ label.export_mask(outpath, update=update)
+ elif isinstance(label, fol.Detection):
if label.mask is not None:
+ outpath = filename_maker.get_output_path(
+ image.filepath, output_ext=".png"
+ )
label.export_mask(outpath, update=update)
+ elif isinstance(label, fol.Detections):
+ for detection in label.detections:
+ if detection.mask is not None:
+ outpath = filename_maker.get_output_path(
+ image.filepath, output_ext=".png"
+ )
+ detection.export_mask(outpath, update=update)
+ elif isinstance(label, fol.Heatmap):
+ if label.map is not None:
+ outpath = filename_maker.get_output_path(
+ image.filepath, output_ext=".png"
+ )
+ label.export_map(outpath, update=update)
def import_segmentations(
@@ -226,8 +245,8 @@ def import_segmentations(
delete_images=False,
progress=None,
):
- """Imports the segmentations (or heatmaps) stored on disk in the specified
- field to in-database arrays.
+ """Imports the semantic segmentations, instance segmentations, or heatmaps
+ stored on disk in the specified field to in-database arrays.
Any labels without images on disk are skipped.
@@ -235,7 +254,9 @@ def import_segmentations(
sample_collection: a
:class:`fiftyone.core.collections.SampleCollection`
in_field: the name of the
- :class:`fiftyone.core.labels.Segmentation` or
+ :class:`fiftyone.core.labels.Segmentation`,
+ :class:`fiftyone.core.labels.Detection`,
+ :class:`fiftyone.core.labels.Detections`, or
:class:`fiftyone.core.labels.Heatmap` field
update (True): whether to delete the image paths from the labels
delete_images (False): whether to delete any imported images from disk
@@ -245,7 +266,9 @@ def import_segmentations(
"""
fov.validate_non_grouped_collection(sample_collection)
fov.validate_collection_label_fields(
- sample_collection, in_field, (fol.Segmentation, fol.Heatmap)
+ sample_collection,
+ in_field,
+ (fol.Segmentation, fol.Detection, fol.Detections, fol.Heatmap),
)
samples = sample_collection.select_fields(in_field)
@@ -262,18 +285,33 @@ def import_segmentations(
if label is None:
continue
- if isinstance(label, fol.Heatmap):
- if label.map_path is not None:
- del_path = label.map_path if delete_images else None
- label.import_map(update=update)
+ if isinstance(label, fol.Segmentation):
+ if label.mask_path is not None:
+ del_path = label.mask_path if delete_images else None
+ label.import_mask(update=update)
if del_path:
etau.delete_file(del_path)
- else:
+ elif isinstance(label, fol.Detection):
if label.mask_path is not None:
del_path = label.mask_path if delete_images else None
label.import_mask(update=update)
if del_path:
etau.delete_file(del_path)
+ elif isinstance(label, fol.Detections):
+ for detection in label.detections:
+ if detection.mask_path is not None:
+ del_path = (
+ detection.mask_path if delete_images else None
+ )
+ detection.import_mask(update=update)
+ if del_path:
+ etau.delete_file(del_path)
+ elif isinstance(label, fol.Heatmap):
+ if label.map_path is not None:
+ del_path = label.map_path if delete_images else None
+ label.import_map(update=update)
+ if del_path:
+ etau.delete_file(del_path)
def transform_segmentations(
@@ -389,6 +427,9 @@ def segmentations_to_detections(
out_field,
mask_targets=None,
mask_types="stuff",
+ output_dir=None,
+ rel_dir=None,
+ overwrite=False,
progress=None,
):
"""Converts the semantic segmentations masks in the specified field of the
@@ -423,6 +464,18 @@ def segmentations_to_detections(
- ``"thing"`` if all classes are thing classes
- a dict mapping pixel values (2D masks) or RGB hex strings (3D
masks) to ``"stuff"`` or ``"thing"`` for each class
+ output_dir (None): an optional output directory in which to write
+ instance segmentation images. If none is provided, the instance
+ segmentations are stored in the database
+ rel_dir (None): an optional relative directory to strip from each input
+ filepath to generate a unique identifier that is joined with
+ ``output_dir`` to generate an output path for each instance
+ segmentation image. This argument allows for populating nested
+ subdirectories in ``output_dir`` that match the shape of the input
+ paths. The path is converted to an absolute path (if necessary) via
+ :func:`fiftyone.core.storage.normalize_path`
+ overwrite (False): whether to delete ``output_dir`` prior to exporting
+ if it exists
progress (None): whether to render a progress bar (True/False), use the
default value ``fiftyone.config.show_progress_bars`` (None), or a
progress callback function to invoke instead
@@ -438,6 +491,14 @@ def segmentations_to_detections(
in_field, processing_frames = samples._handle_frame_field(in_field)
out_field, _ = samples._handle_frame_field(out_field)
+ if overwrite and output_dir is not None:
+ etau.delete_dir(output_dir)
+
+ if output_dir is not None:
+ filename_maker = fou.UniqueFilenameMaker(
+ output_dir=output_dir, rel_dir=rel_dir, idempotent=False
+ )
+
for sample in samples.iter_samples(autosave=True, progress=progress):
if processing_frames:
images = sample.frames.values()
@@ -449,9 +510,17 @@ def segmentations_to_detections(
if label is None:
continue
- image[out_field] = label.to_detections(
+ detections = label.to_detections(
mask_targets=mask_targets, mask_types=mask_types
)
+ if output_dir is not None:
+ for detection in detections.detections:
+ mask_path = filename_maker.get_output_path(
+ image.filepath, output_ext=".png"
+ )
+ detection.export_mask(mask_path, update=True)
+
+ image[out_field] = detections
def instances_to_polylines(
diff --git a/tests/unittests/import_export_tests.py b/tests/unittests/import_export_tests.py
index 896429d8a7..d7f601a9e4 100644
--- a/tests/unittests/import_export_tests.py
+++ b/tests/unittests/import_export_tests.py
@@ -2218,6 +2218,115 @@ def _test_image_segmentation_fiftyone_dataset(self, dataset_type):
dataset2.values("segmentations.mask_path"),
)
+ @drop_datasets
+ def test_instance_segmentation_fiftyone_dataset(self):
+ self._test_instance_segmentation_fiftyone_dataset(
+ fo.types.FiftyOneDataset
+ )
+
+ @drop_datasets
+ def test_instance_segmentation_legacy_fiftyone_dataset(self):
+ self._test_instance_segmentation_fiftyone_dataset(
+ fo.types.LegacyFiftyOneDataset
+ )
+
+ def _test_instance_segmentation_fiftyone_dataset(self, dataset_type):
+ dataset = self._make_dataset()
+
+ # In-database instance segmentations
+
+ export_dir = self._new_dir()
+
+ dataset.export(
+ export_dir=export_dir,
+ dataset_type=dataset_type,
+ )
+
+ dataset2 = fo.Dataset.from_dir(
+ dataset_dir=export_dir,
+ dataset_type=dataset_type,
+ )
+
+ self.assertEqual(len(dataset), len(dataset2))
+ self.assertEqual(dataset.count("detections.detections.mask_path"), 0)
+ self.assertEqual(dataset2.count("detections.detections.mask_path"), 0)
+ self.assertEqual(
+ dataset.count("detections.detections.mask"),
+ dataset2.count("detections.detections.mask"),
+ )
+
+ # Convert to on-disk instance segmentations
+
+ segmentations_dir = self._new_dir()
+
+ foul.export_segmentations(dataset, "detections", segmentations_dir)
+
+ self.assertEqual(dataset.count("detections.detections.mask"), 0)
+ for mask_path in dataset.values("detections.detections[].mask_path"):
+ if mask_path is not None:
+ self.assertTrue(mask_path.startswith(segmentations_dir))
+
+ # On-disk instance segmentations
+
+ export_dir = self._new_dir()
+ field_dir = os.path.join(export_dir, "fields", "detections.detections")
+
+ dataset.export(
+ export_dir=export_dir,
+ dataset_type=dataset_type,
+ )
+
+ dataset2 = fo.Dataset.from_dir(
+ dataset_dir=export_dir,
+ dataset_type=dataset_type,
+ )
+
+ self.assertEqual(len(dataset), len(dataset2))
+ self.assertEqual(dataset2.count("detections.detections.mask"), 0)
+ self.assertEqual(
+ dataset.count("detections.detections.mask_path"),
+ dataset2.count("detections.detections.mask_path"),
+ )
+
+ for mask_path in dataset2.values("detections.detections[].mask_path"):
+ if mask_path is not None:
+ self.assertTrue(mask_path.startswith(field_dir))
+
+ # On-disk instance segmentations (don't export media)
+
+ export_dir = self._new_dir()
+
+ dataset.export(
+ export_dir=export_dir,
+ dataset_type=dataset_type,
+ export_media=False,
+ )
+
+ dataset2 = fo.Dataset.from_dir(
+ dataset_dir=export_dir,
+ dataset_type=dataset_type,
+ )
+
+ self.assertEqual(len(dataset), len(dataset2))
+ self.assertListEqual(
+ dataset.values("filepath"),
+ dataset2.values("filepath"),
+ )
+ self.assertListEqual(
+ dataset.values("detections.detections[].mask_path"),
+ dataset2.values("detections.detections[].mask_path"),
+ )
+
+ # Convert to in-database instance segmentations
+
+ foul.import_segmentations(dataset2, "detections")
+
+ self.assertEqual(dataset2.count("detections.detections.mask_path"), 0)
+ self.assertEqual(
+ dataset2.count("detections.detections.mask"),
+ dataset.count("detections.detections.mask_path"),
+ )
+
class DICOMDatasetTests(ImageDatasetTests):
def _get_dcm_path(self):
From 0a86bcf25d3c366a290802b3e491ab1679accb4f Mon Sep 17 00:00:00 2001
From: Minh-Tue Vo
Date: Tue, 10 Dec 2024 23:05:36 -0800
Subject: [PATCH 027/104] Add uniqueness validation for TableView (#5170)
* Added uniqueness validation to TableView
* Added tests
---------
Co-authored-by: minhtuevo
---
fiftyone/operators/types.py | 39 +++++++++++++----
tests/unittests/operators/tableview_tests.py | 46 ++++++++++++++++++++
2 files changed, 77 insertions(+), 8 deletions(-)
create mode 100644 tests/unittests/operators/tableview_tests.py
diff --git a/fiftyone/operators/types.py b/fiftyone/operators/types.py
index 2caf7ce47d..d2cda5219f 100644
--- a/fiftyone/operators/types.py
+++ b/fiftyone/operators/types.py
@@ -1827,16 +1827,18 @@ class Action(View):
on_click: the operator to execute when the action is clicked
"""
- def __init__(self, **kwargs):
+ def __init__(self, name, **kwargs):
super().__init__(**kwargs)
+ self.name = name
def clone(self):
- clone = Action(**self._kwargs)
+ clone = Action(self.name, **self._kwargs)
return clone
def to_json(self):
- return {**super().to_json()}
-
+ return {**super().to_json(), "name": self.name}
+
+
class Tooltip(View):
"""A tooltip (currently supported only in a :class:`TableView`).
@@ -1846,15 +1848,17 @@ class Tooltip(View):
column: the column of the tooltip
"""
- def __init__(self, **kwargs):
+ def __init__(self, row, column, **kwargs):
super().__init__(**kwargs)
+ self.row = row
+ self.column = column
def clone(self):
- clone = Tooltip(**self._kwargs)
+ clone = Tooltip(self.row, self.column, **self._kwargs)
return clone
def to_json(self):
- return {**super().to_json()}
+ return {**super().to_json(), "row": self.row, "column": self.column}
class TableView(View):
@@ -1870,11 +1874,16 @@ def __init__(self, **kwargs):
self.columns = kwargs.get("columns", [])
self.row_actions = kwargs.get("row_actions", [])
self.tooltips = kwargs.get("tooltips", [])
+ self._tooltip_map = {}
def keys(self):
return [column.key for column in self.columns]
def add_column(self, key, **kwargs):
+ for column in self.columns:
+ if column.key == key:
+ raise ValueError(f"Column with key '{key}' already exists")
+
column = Column(key, **kwargs)
self.columns.append(column)
return column
@@ -1882,6 +1891,10 @@ def add_column(self, key, **kwargs):
def add_row_action(
self, name, on_click, label=None, icon=None, tooltip=None, **kwargs
):
+ for action in self.row_actions:
+ if action.name == name:
+ raise ValueError(f"Action with name '{name}' already exists")
+
row_action = Action(
name=name,
on_click=on_click,
@@ -1892,10 +1905,16 @@ def add_row_action(
)
self.row_actions.append(row_action)
return row_action
-
+
def add_tooltip(self, row, column, value, **kwargs):
+ if (row, column) in self._tooltip_map:
+ raise ValueError(
+ f"Tooltip for row '{row}' and column '{column}' already exists"
+ )
+
tooltip = Tooltip(row=row, column=column, value=value, **kwargs)
self.tooltips.append(tooltip)
+ self._tooltip_map[(row, column)] = tooltip
return tooltip
def clone(self):
@@ -1903,6 +1922,10 @@ def clone(self):
clone.columns = [column.clone() for column in self.columns]
clone.row_actions = [action.clone() for action in self.row_actions]
clone.tooltips = [tooltip.clone() for tooltip in self.tooltips]
+ clone._tooltip_map = {
+ (tooltip.row, tooltip.column): tooltip
+ for tooltip in clone.tooltips
+ }
return clone
def to_json(self):
diff --git a/tests/unittests/operators/tableview_tests.py b/tests/unittests/operators/tableview_tests.py
new file mode 100644
index 0000000000..88945ec92c
--- /dev/null
+++ b/tests/unittests/operators/tableview_tests.py
@@ -0,0 +1,46 @@
+import unittest
+
+from fiftyone.operators.types import TableView
+
+
+class TableViewTests(unittest.TestCase):
+ def test_table_view_basic(self):
+ table = TableView()
+ table.add_column("column1", label="Column 1")
+ table.add_column("column2", label="Column 2")
+ assert table.keys() == ["column1", "column2"]
+
+ with self.assertRaises(ValueError):
+ table.add_column("column1", label="Column 3")
+
+ mock_on_click = lambda: None
+
+ table.add_row_action(
+ "action1",
+ on_click=mock_on_click,
+ icon="icon1",
+ color="primary",
+ tooltip="Action 1",
+ )
+ table.add_row_action(
+ "action2",
+ on_click=mock_on_click,
+ icon="icon2",
+ color="secondary",
+ tooltip="Action 2",
+ )
+
+ with self.assertRaises(ValueError):
+ table.add_row_action(
+ "action1",
+ on_click=mock_on_click,
+ icon="icon3",
+ color="primary",
+ tooltip="Action 3",
+ )
+
+ table.add_tooltip(1, 1, "Tooltip 1")
+ table.add_tooltip(1, 2, "Tooltip 2")
+
+ with self.assertRaises(ValueError):
+ table.add_tooltip(1, 1, "Tooltip 3")
From 867fc3bc82468711aeef32d452aa7da5846f585b Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Wed, 11 Dec 2024 10:40:13 -0500
Subject: [PATCH 028/104] Bump nanoid from 3.3.7 to 3.3.8 in /app (#5249)
Bumps [nanoid](https://github.com/ai/nanoid) from 3.3.7 to 3.3.8.
- [Release notes](https://github.com/ai/nanoid/releases)
- [Changelog](https://github.com/ai/nanoid/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ai/nanoid/compare/3.3.7...3.3.8)
---
updated-dependencies:
- dependency-name: nanoid
dependency-type: indirect
...
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
app/yarn.lock | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/app/yarn.lock b/app/yarn.lock
index 93f3b05f4c..bb7e8ab020 100644
--- a/app/yarn.lock
+++ b/app/yarn.lock
@@ -13228,11 +13228,11 @@ __metadata:
linkType: hard
"nanoid@npm:^3.3.6, nanoid@npm:^3.3.7":
- version: 3.3.7
- resolution: "nanoid@npm:3.3.7"
+ version: 3.3.8
+ resolution: "nanoid@npm:3.3.8"
bin:
nanoid: bin/nanoid.cjs
- checksum: d36c427e530713e4ac6567d488b489a36582ef89da1d6d4e3b87eded11eb10d7042a877958c6f104929809b2ab0bafa17652b076cdf84324aa75b30b722204f2
+ checksum: dfe0adbc0c77e9655b550c333075f51bb28cfc7568afbf3237249904f9c86c9aaaed1f113f0fddddba75673ee31c758c30c43d4414f014a52a7a626efc5958c9
languageName: node
linkType: hard
From d626e60c637bca53829dd7b82959e6f3f845f684 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Wed, 11 Dec 2024 16:30:43 +0000
Subject: [PATCH 029/104] Bump nanoid from 3.3.7 to 3.3.8 in /e2e-pw (#5257)
Bumps [nanoid](https://github.com/ai/nanoid) from 3.3.7 to 3.3.8.
- [Release notes](https://github.com/ai/nanoid/releases)
- [Changelog](https://github.com/ai/nanoid/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ai/nanoid/compare/3.3.7...3.3.8)
---
updated-dependencies:
- dependency-name: nanoid
dependency-type: indirect
...
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
e2e-pw/yarn.lock | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/e2e-pw/yarn.lock b/e2e-pw/yarn.lock
index d9ad021c5f..1fb4f76cbd 100644
--- a/e2e-pw/yarn.lock
+++ b/e2e-pw/yarn.lock
@@ -2595,11 +2595,11 @@ __metadata:
linkType: hard
"nanoid@npm:^3.3.7":
- version: 3.3.7
- resolution: "nanoid@npm:3.3.7"
+ version: 3.3.8
+ resolution: "nanoid@npm:3.3.8"
bin:
nanoid: bin/nanoid.cjs
- checksum: 10/ac1eb60f615b272bccb0e2b9cd933720dad30bf9708424f691b8113826bb91aca7e9d14ef5d9415a6ba15c266b37817256f58d8ce980c82b0ba3185352565679
+ checksum: 10/2d1766606cf0d6f47b6f0fdab91761bb81609b2e3d367027aff45e6ee7006f660fb7e7781f4a34799fe6734f1268eeed2e37a5fdee809ade0c2d4eb11b0f9c40
languageName: node
linkType: hard
From 6766a55c5097f4cc3a333559a9561d44d1ec9488 Mon Sep 17 00:00:00 2001
From: afoley587 <54959686+afoley587@users.noreply.github.com>
Date: Wed, 11 Dec 2024 12:31:43 -0500
Subject: [PATCH 030/104] chore(ci): Wrapping FFMPEG and updating actions
(#5258)
---
.github/workflows/e2e.yml | 9 +++++++--
.github/workflows/pr.yml | 1 +
2 files changed, 8 insertions(+), 2 deletions(-)
diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml
index 23120eb6c6..2fae78b497 100644
--- a/.github/workflows/e2e.yml
+++ b/.github/workflows/e2e.yml
@@ -68,8 +68,13 @@ jobs:
python tests/utils/setup_config.py
python tests/utils/github_actions_flags.py
- - name: FFmpeg
- uses: FedericoCarboni/setup-ffmpeg@v3
+ # - name: Setup FFmpeg (with retries)
+ # uses: FedericoCarboni/setup-ffmpeg@v3
+
+ # Use this until https://github.com/federicocarboni/setup-ffmpeg/pull/23
+ # is merged or the maintainer addresses the root issue.
+ - name: Setup FFmpeg (with retries)
+ uses: afoley587/setup-ffmpeg@main
- name: Cache E2E Node Modules
id: e2e-node-cache
diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml
index 773b009678..00a9d7922c 100644
--- a/.github/workflows/pr.yml
+++ b/.github/workflows/pr.yml
@@ -30,6 +30,7 @@ jobs:
- 'app/**'
e2e-pw:
- 'e2e-pw/**'
+ - '.github/workflows/e2e.yml'
fiftyone:
- 'fiftyone/**'
- 'package/**'
From 9fae650b6119f640d146c8d039402da62d047f0b Mon Sep 17 00:00:00 2001
From: imanjra
Date: Fri, 6 Dec 2024 14:06:47 -0500
Subject: [PATCH 031/104] fix hover background for panel in add panel popover
---
app/packages/spaces/src/components/AddPanelButton.tsx | 2 +-
app/packages/spaces/src/components/AddPanelItem.tsx | 1 +
2 files changed, 2 insertions(+), 1 deletion(-)
diff --git a/app/packages/spaces/src/components/AddPanelButton.tsx b/app/packages/spaces/src/components/AddPanelButton.tsx
index 1deff2d616..3c0b09c1a8 100644
--- a/app/packages/spaces/src/components/AddPanelButton.tsx
+++ b/app/packages/spaces/src/components/AddPanelButton.tsx
@@ -121,7 +121,7 @@ function PanelCategories({ children }) {
function PanelCategory({ label, children }) {
const theme = useTheme();
return (
-
+
From 68eb682a998bebae98ff2a05f6cdffa3a87e98aa Mon Sep 17 00:00:00 2001
From: brimoor
Date: Thu, 12 Dec 2024 14:55:35 -0500
Subject: [PATCH 032/104] handle nested roots
---
fiftyone/core/collections.py | 31 +++++++++++++++++++++----------
fiftyone/utils/data/exporters.py | 4 ++--
2 files changed, 23 insertions(+), 12 deletions(-)
diff --git a/fiftyone/core/collections.py b/fiftyone/core/collections.py
index 8200255fb4..65b6a78e39 100644
--- a/fiftyone/core/collections.py
+++ b/fiftyone/core/collections.py
@@ -10662,9 +10662,7 @@ def _handle_db_fields(self, paths, frames=False):
db_fields_map = self._get_db_fields_map(frames=frames)
return [db_fields_map.get(p, p) for p in paths]
- def _get_media_fields(
- self, include_filepath=True, whitelist=None, frames=False
- ):
+ def _get_media_fields(self, whitelist=None, blacklist=None, frames=False):
media_fields = {}
if frames:
@@ -10674,11 +10672,8 @@ def _get_media_fields(
schema = self.get_field_schema(flat=True)
app_media_fields = set(self._dataset.app_config.media_fields)
- if include_filepath:
- # 'filepath' should already be in set, but add it just in case
- app_media_fields.add("filepath")
- else:
- app_media_fields.discard("filepath")
+ # 'filepath' should already be in set, but add it just in case
+ app_media_fields.add("filepath")
for field_name, field in schema.items():
while isinstance(field, fof.ListField):
@@ -10698,7 +10693,21 @@ def _get_media_fields(
whitelist = {whitelist}
media_fields = {
- k: v for k, v in media_fields.items() if k in whitelist
+ k: v
+ for k, v in media_fields.items()
+ if any(w == k or k.startswith(w + ".") for w in whitelist)
+ }
+
+ if blacklist is not None:
+ if etau.is_container(blacklist):
+ blacklist = set(blacklist)
+ else:
+ blacklist = {blacklist}
+
+ media_fields = {
+ k: v
+ for k, v in media_fields.items()
+ if not any(w == k or k.startswith(w + ".") for w in blacklist)
}
return media_fields
@@ -10714,7 +10723,9 @@ def _resolve_media_field(self, media_field):
if leaf is not None:
leaf = root + "." + leaf
- if _media_field in (root, leaf):
+ if _media_field in (root, leaf) or root.startswith(
+ _media_field + "."
+ ):
_resolved_field = leaf if leaf is not None else root
if is_frame_field:
_resolved_field = self._FRAMES_PREFIX + _resolved_field
diff --git a/fiftyone/utils/data/exporters.py b/fiftyone/utils/data/exporters.py
index 7a9b7da68e..475e4286b3 100644
--- a/fiftyone/utils/data/exporters.py
+++ b/fiftyone/utils/data/exporters.py
@@ -1894,7 +1894,7 @@ def log_collection(self, sample_collection):
self._metadata["frame_fields"] = schema
self._media_fields = sample_collection._get_media_fields(
- include_filepath=False
+ blacklist="filepath",
)
info = dict(sample_collection.info)
@@ -2202,7 +2202,7 @@ def export_samples(self, sample_collection, progress=None):
_sample_collection = sample_collection
self._media_fields = sample_collection._get_media_fields(
- include_filepath=False
+ blacklist="filepath"
)
logger.info("Exporting samples...")
From 833166132b9c2feb494e8f5e58f0ac1276d19735 Mon Sep 17 00:00:00 2001
From: brimoor
Date: Thu, 12 Dec 2024 17:09:40 -0500
Subject: [PATCH 033/104] handle list fields
---
fiftyone/core/collections.py | 12 +++++++++---
1 file changed, 9 insertions(+), 3 deletions(-)
diff --git a/fiftyone/core/collections.py b/fiftyone/core/collections.py
index 65b6a78e39..2aafc32ee2 100644
--- a/fiftyone/core/collections.py
+++ b/fiftyone/core/collections.py
@@ -10712,9 +10712,9 @@ def _get_media_fields(self, whitelist=None, blacklist=None, frames=False):
return media_fields
- def _resolve_media_field(self, media_field):
+ def _parse_media_field(self, media_field):
if media_field in self._dataset.app_config.media_fields:
- return media_field
+ return media_field, None
_media_field, is_frame_field = self._handle_frame_field(media_field)
@@ -10730,7 +10730,13 @@ def _resolve_media_field(self, media_field):
if is_frame_field:
_resolved_field = self._FRAMES_PREFIX + _resolved_field
- return _resolved_field
+ _list_fields = self._parse_field_name(
+ _resolved_field, auto_unwind=False
+ )[-2]
+ if _list_fields:
+ return _resolved_field, _list_fields[0]
+
+ return _resolved_field, None
raise ValueError("'%s' is not a valid media field" % media_field)
From 038138e6b697709cdd1ad810ae79b3e014790792 Mon Sep 17 00:00:00 2001
From: Sashank Aryal
Date: Mon, 9 Dec 2024 14:27:32 -0600
Subject: [PATCH 034/104] optimize main thread to worker transfer when
recoloring
---
app/packages/looker/src/lookers/abstract.ts | 55 ++++++++++++++-----
app/packages/looker/src/overlays/base.ts | 4 +-
app/packages/looker/src/overlays/detection.ts | 6 ++
app/packages/looker/src/overlays/heatmap.ts | 9 ++-
.../looker/src/overlays/segmentation.ts | 9 ++-
.../looker/src/worker/disk-overlay-decoder.ts | 6 +-
6 files changed, 68 insertions(+), 21 deletions(-)
diff --git a/app/packages/looker/src/lookers/abstract.ts b/app/packages/looker/src/lookers/abstract.ts
index 3eca774de8..81b9c9ab1e 100644
--- a/app/packages/looker/src/lookers/abstract.ts
+++ b/app/packages/looker/src/lookers/abstract.ts
@@ -23,7 +23,7 @@ import {
import { Events } from "../elements/base";
import { COMMON_SHORTCUTS, LookerElement } from "../elements/common";
import { ClassificationsOverlay, loadOverlays } from "../overlays";
-import { CONTAINS, Overlay } from "../overlays/base";
+import { CONTAINS, LabelMask, Overlay } from "../overlays/base";
import processOverlays from "../processOverlays";
import {
BaseState,
@@ -515,7 +515,29 @@ export abstract class AbstractLooker<
abstract updateOptions(options: Partial): void;
updateSample(sample: Sample) {
- this.loadSample(sample);
+ // collect any mask targets array buffer that overalys might have
+ // we'll transfer that to the worker instead of copying it
+ const arrayBuffers: ArrayBuffer[] = [];
+
+ for (const overlay of this.pluckedOverlays ?? []) {
+ // we paint overlays again, so cleanup the old ones
+ // this helps prevent memory leaks from, for instance, dangling ImageBitmaps
+ overlay.cleanup();
+
+ let overlayData: LabelMask = null;
+
+ if ("mask" in overlay.label) {
+ overlayData = overlay.label.mask as LabelMask;
+ } else if ("map" in overlay.label) {
+ overlayData = overlay.label.map as LabelMask;
+ }
+
+ if (overlayData?.data?.buffer) {
+ arrayBuffers.push(overlayData.data.buffer);
+ }
+ }
+
+ this.loadSample(sample, arrayBuffers);
}
getSample(): Promise {
@@ -698,7 +720,7 @@ export abstract class AbstractLooker<
);
}
- private loadSample(sample: Sample) {
+ private loadSample(sample: Sample, transfer: Transferable[] = []) {
const messageUUID = uuid();
const labelsWorker = getLabelsWorker();
@@ -719,18 +741,21 @@ export abstract class AbstractLooker<
labelsWorker.addEventListener("message", listener);
- labelsWorker.postMessage({
- sample: sample as ProcessSample["sample"],
- method: "processSample",
- coloring: this.state.options.coloring,
- customizeColorSetting: this.state.options.customizeColorSetting,
- colorscale: this.state.options.colorscale,
- labelTagColors: this.state.options.labelTagColors,
- selectedLabelTags: this.state.options.selectedLabelTags,
- sources: this.state.config.sources,
- schema: this.state.config.fieldSchema,
- uuid: messageUUID,
- } as ProcessSample);
+ labelsWorker.postMessage(
+ {
+ sample: sample as ProcessSample["sample"],
+ method: "processSample",
+ coloring: this.state.options.coloring,
+ customizeColorSetting: this.state.options.customizeColorSetting,
+ colorscale: this.state.options.colorscale,
+ labelTagColors: this.state.options.labelTagColors,
+ selectedLabelTags: this.state.options.selectedLabelTags,
+ sources: this.state.config.sources,
+ schema: this.state.config.fieldSchema,
+ uuid: messageUUID,
+ } as ProcessSample,
+ transfer
+ );
}
}
diff --git a/app/packages/looker/src/overlays/base.ts b/app/packages/looker/src/overlays/base.ts
index a3ec867766..9b433e9400 100644
--- a/app/packages/looker/src/overlays/base.ts
+++ b/app/packages/looker/src/overlays/base.ts
@@ -42,6 +42,7 @@ export interface SelectData {
export type LabelMask = {
bitmap?: ImageBitmap;
+ closedBitmapDims?: { width: number; height: number };
data?: OverlayMask;
};
@@ -67,6 +68,7 @@ export interface Overlay> {
draw(ctx: CanvasRenderingContext2D, state: State): void;
isShown(state: Readonly): boolean;
field?: string;
+ label?: BaseLabel;
containsPoint(state: Readonly): CONTAINS;
getMouseDistance(state: Readonly): number;
getPointInfo(state: Readonly): any;
@@ -82,7 +84,7 @@ export abstract class CoordinateOverlay<
> implements Overlay
{
readonly field: string;
- protected label: Label;
+ readonly label: Label;
constructor(field: string, label: Label) {
this.field = field;
diff --git a/app/packages/looker/src/overlays/detection.ts b/app/packages/looker/src/overlays/detection.ts
index 137beae7b4..d5b80d9873 100644
--- a/app/packages/looker/src/overlays/detection.ts
+++ b/app/packages/looker/src/overlays/detection.ts
@@ -263,8 +263,14 @@ export default class DetectionOverlay<
public cleanup(): void {
if (this.label.mask?.bitmap) {
+ // store height and width in bitmap object since it might be used again
+ const height = this.label.mask.bitmap.height;
+ const width = this.label.mask.bitmap.width;
+
this.label.mask?.bitmap.close();
this.label.mask.bitmap = null;
+
+ this.label.mask.closedBitmapDims = { width, height };
}
}
}
diff --git a/app/packages/looker/src/overlays/heatmap.ts b/app/packages/looker/src/overlays/heatmap.ts
index e8e8817643..c1be209ed5 100644
--- a/app/packages/looker/src/overlays/heatmap.ts
+++ b/app/packages/looker/src/overlays/heatmap.ts
@@ -39,7 +39,7 @@ export default class HeatmapOverlay
implements Overlay
{
readonly field: string;
- private label: HeatmapLabel;
+ readonly label: HeatmapLabel;
private targets?: TypedArray;
private readonly range: [number, number];
@@ -208,7 +208,14 @@ export default class HeatmapOverlay
public cleanup(): void {
if (this.label.map?.bitmap) {
+ // store height and width in bitmap object since it might be used again
+ const height = this.label.map.bitmap.height;
+ const width = this.label.map.bitmap.width;
+
this.label.map?.bitmap.close();
+ this.label.map.bitmap = null;
+
+ this.label.map.closedBitmapDims = { width, height };
}
}
}
diff --git a/app/packages/looker/src/overlays/segmentation.ts b/app/packages/looker/src/overlays/segmentation.ts
index a4cb098254..3218db80a6 100644
--- a/app/packages/looker/src/overlays/segmentation.ts
+++ b/app/packages/looker/src/overlays/segmentation.ts
@@ -30,7 +30,7 @@ export default class SegmentationOverlay
implements Overlay
{
readonly field: string;
- private label: SegmentationLabel;
+ readonly label: SegmentationLabel;
private targets?: TypedArray;
private isRgbMaskTargets = false;
@@ -263,7 +263,14 @@ export default class SegmentationOverlay
public cleanup(): void {
if (this.label.mask?.bitmap) {
+ // store height and width in bitmap object since it might be used again
+ const height = this.label.mask.bitmap.height;
+ const width = this.label.mask.bitmap.width;
+
this.label.mask?.bitmap.close();
+ this.label.mask.bitmap = null;
+
+ this.label.mask.closedBitmapDims = { width, height };
}
}
}
diff --git a/app/packages/looker/src/worker/disk-overlay-decoder.ts b/app/packages/looker/src/worker/disk-overlay-decoder.ts
index 8730f74bf0..e2cbf6081b 100644
--- a/app/packages/looker/src/worker/disk-overlay-decoder.ts
+++ b/app/packages/looker/src/worker/disk-overlay-decoder.ts
@@ -56,11 +56,11 @@ export const decodeOverlayOnDisk = async (
// it's possible we're just re-coloring, in which case re-init mask image and set bitmap to null
if (
label[overlayField] &&
- label[overlayField].bitmap &&
+ label[overlayField].closedBitmapDims &&
!label[overlayField].image
) {
- const height = label[overlayField].bitmap.height;
- const width = label[overlayField].bitmap.width;
+ const height = label[overlayField].closedBitmapDims.height;
+ const width = label[overlayField].closedBitmapDims.width;
label[overlayField].image = new ArrayBuffer(height * width * 4);
label[overlayField].bitmap.close();
label[overlayField].bitmap = null;
From ece501a06c7b7e4cb3d9554a1f3c683cd61b9ac2 Mon Sep 17 00:00:00 2001
From: Sashank Aryal
Date: Mon, 9 Dec 2024 14:34:57 -0600
Subject: [PATCH 035/104] fix typo
---
app/packages/looker/src/lookers/abstract.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/app/packages/looker/src/lookers/abstract.ts b/app/packages/looker/src/lookers/abstract.ts
index 81b9c9ab1e..423656e49a 100644
--- a/app/packages/looker/src/lookers/abstract.ts
+++ b/app/packages/looker/src/lookers/abstract.ts
@@ -515,7 +515,7 @@ export abstract class AbstractLooker<
abstract updateOptions(options: Partial): void;
updateSample(sample: Sample) {
- // collect any mask targets array buffer that overalys might have
+ // collect any mask targets array buffer that overlays might have
// we'll transfer that to the worker instead of copying it
const arrayBuffers: ArrayBuffer[] = [];
From 89fea41f0829bc58db41bddfd19668dc62b78aea Mon Sep 17 00:00:00 2001
From: Sashank Aryal
Date: Mon, 9 Dec 2024 18:53:49 -0600
Subject: [PATCH 036/104] check if array buffer detached
---
app/packages/looker/src/lookers/abstract.ts | 64 +++++++++++++++------
app/packages/looker/src/worker/index.ts | 4 ++
2 files changed, 50 insertions(+), 18 deletions(-)
diff --git a/app/packages/looker/src/lookers/abstract.ts b/app/packages/looker/src/lookers/abstract.ts
index 423656e49a..6234ae6082 100644
--- a/app/packages/looker/src/lookers/abstract.ts
+++ b/app/packages/looker/src/lookers/abstract.ts
@@ -532,12 +532,30 @@ export abstract class AbstractLooker<
overlayData = overlay.label.map as LabelMask;
}
- if (overlayData?.data?.buffer) {
- arrayBuffers.push(overlayData.data.buffer);
+ const buffer = overlayData?.data?.buffer;
+
+ if (!buffer) {
+ continue;
+ }
+
+ // check for detached buffer (happens if user is switching colors too fast)
+ // note: ArrayBuffer.prototype.detached is a new browser API
+ if (typeof buffer.detached !== "undefined") {
+ if (buffer.detached) {
+ // most likely sample is already being processed, skip update
+ return;
+ } else {
+ arrayBuffers.push(buffer);
+ }
+ } else {
+ // hope we don't run into this edge case (old browser)
+ // if we do, we'll just copy the buffer
+ // might get a DataCloneError if user is switching colors too fast
+ arrayBuffers.push(buffer);
}
}
- this.loadSample(sample, arrayBuffers);
+ this.loadSample(sample, arrayBuffers.flat());
}
getSample(): Promise {
@@ -741,21 +759,31 @@ export abstract class AbstractLooker<
labelsWorker.addEventListener("message", listener);
- labelsWorker.postMessage(
- {
- sample: sample as ProcessSample["sample"],
- method: "processSample",
- coloring: this.state.options.coloring,
- customizeColorSetting: this.state.options.customizeColorSetting,
- colorscale: this.state.options.colorscale,
- labelTagColors: this.state.options.labelTagColors,
- selectedLabelTags: this.state.options.selectedLabelTags,
- sources: this.state.config.sources,
- schema: this.state.config.fieldSchema,
- uuid: messageUUID,
- } as ProcessSample,
- transfer
- );
+ const workerArgs = {
+ sample: sample as ProcessSample["sample"],
+ method: "processSample",
+ coloring: this.state.options.coloring,
+ customizeColorSetting: this.state.options.customizeColorSetting,
+ colorscale: this.state.options.colorscale,
+ labelTagColors: this.state.options.labelTagColors,
+ selectedLabelTags: this.state.options.selectedLabelTags,
+ sources: this.state.config.sources,
+ schema: this.state.config.fieldSchema,
+ uuid: messageUUID,
+ } as ProcessSample;
+
+ try {
+ labelsWorker.postMessage(workerArgs, transfer);
+ } catch (error) {
+ // rarely we'll get a DataCloneError
+ // if one of the buffers is detached and we didn't catch it
+ // try again without transferring the buffers (copying them)
+ if (error.name === "DataCloneError") {
+ labelsWorker.postMessage(workerArgs);
+ } else {
+ throw error;
+ }
+ }
}
}
diff --git a/app/packages/looker/src/worker/index.ts b/app/packages/looker/src/worker/index.ts
index dcf0b2e79b..3f1e6cefeb 100644
--- a/app/packages/looker/src/worker/index.ts
+++ b/app/packages/looker/src/worker/index.ts
@@ -327,6 +327,10 @@ const processSample = async ({
labelTagColors,
schema,
}: ProcessSample) => {
+ if (!sample) {
+ return;
+ }
+
mapId(sample);
const imageBitmapPromises: Promise[] = [];
From d3bf17472338ec0838f03800dee5f22cac12736b Mon Sep 17 00:00:00 2001
From: Sashank Aryal
Date: Tue, 10 Dec 2024 10:48:39 -0600
Subject: [PATCH 037/104] add clarifying comments
---
app/packages/looker/src/lookers/abstract.ts | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/app/packages/looker/src/lookers/abstract.ts b/app/packages/looker/src/lookers/abstract.ts
index 6234ae6082..499e2634a5 100644
--- a/app/packages/looker/src/lookers/abstract.ts
+++ b/app/packages/looker/src/lookers/abstract.ts
@@ -547,9 +547,10 @@ export abstract class AbstractLooker<
} else {
arrayBuffers.push(buffer);
}
- } else {
+ } else if (buffer.byteLength) {
// hope we don't run into this edge case (old browser)
- // if we do, we'll just copy the buffer
+ // sometimes detached buffers have bytelength > 0
+ // if we run into this case, we'll just attempt to transfer the buffer
// might get a DataCloneError if user is switching colors too fast
arrayBuffers.push(buffer);
}
From 4f76488631edecb8dc8b210ff4a59646bfdb5c06 Mon Sep 17 00:00:00 2001
From: Sashank Aryal
Date: Thu, 12 Dec 2024 15:55:43 -0600
Subject: [PATCH 038/104] cleanup overlays in the worker listener callback
instead
---
app/packages/looker/src/lookers/abstract.ts | 13 +++++++++----
app/packages/looker/src/overlays/base.ts | 1 -
app/packages/looker/src/overlays/detection.ts | 12 ++----------
app/packages/looker/src/overlays/heatmap.ts | 12 ++----------
app/packages/looker/src/overlays/segmentation.ts | 12 ++----------
.../looker/src/worker/disk-overlay-decoder.ts | 11 ++++++++---
6 files changed, 23 insertions(+), 38 deletions(-)
diff --git a/app/packages/looker/src/lookers/abstract.ts b/app/packages/looker/src/lookers/abstract.ts
index 499e2634a5..1043c4d7b8 100644
--- a/app/packages/looker/src/lookers/abstract.ts
+++ b/app/packages/looker/src/lookers/abstract.ts
@@ -520,10 +520,6 @@ export abstract class AbstractLooker<
const arrayBuffers: ArrayBuffer[] = [];
for (const overlay of this.pluckedOverlays ?? []) {
- // we paint overlays again, so cleanup the old ones
- // this helps prevent memory leaks from, for instance, dangling ImageBitmaps
- overlay.cleanup();
-
let overlayData: LabelMask = null;
if ("mask" in overlay.label) {
@@ -739,6 +735,12 @@ export abstract class AbstractLooker<
);
}
+ protected cleanOverlays() {
+ for (const overlay of this.sampleOverlays ?? []) {
+ overlay.cleanup();
+ }
+ }
+
private loadSample(sample: Sample, transfer: Transferable[] = []) {
const messageUUID = uuid();
@@ -746,6 +748,9 @@ export abstract class AbstractLooker<
const listener = ({ data: { sample, coloring, uuid } }) => {
if (uuid === messageUUID) {
+ // we paint overlays again, so cleanup the old ones
+ // this helps prevent memory leaks from, for instance, dangling ImageBitmaps
+ this.cleanOverlays();
this.sample = sample;
this.state.options.coloring = coloring;
this.loadOverlays(sample);
diff --git a/app/packages/looker/src/overlays/base.ts b/app/packages/looker/src/overlays/base.ts
index 9b433e9400..faf6f284b5 100644
--- a/app/packages/looker/src/overlays/base.ts
+++ b/app/packages/looker/src/overlays/base.ts
@@ -42,7 +42,6 @@ export interface SelectData {
export type LabelMask = {
bitmap?: ImageBitmap;
- closedBitmapDims?: { width: number; height: number };
data?: OverlayMask;
};
diff --git a/app/packages/looker/src/overlays/detection.ts b/app/packages/looker/src/overlays/detection.ts
index d5b80d9873..1298cf2fbc 100644
--- a/app/packages/looker/src/overlays/detection.ts
+++ b/app/packages/looker/src/overlays/detection.ts
@@ -262,16 +262,8 @@ export default class DetectionOverlay<
}
public cleanup(): void {
- if (this.label.mask?.bitmap) {
- // store height and width in bitmap object since it might be used again
- const height = this.label.mask.bitmap.height;
- const width = this.label.mask.bitmap.width;
-
- this.label.mask?.bitmap.close();
- this.label.mask.bitmap = null;
-
- this.label.mask.closedBitmapDims = { width, height };
- }
+ this.label.mask?.bitmap?.close();
+ this.label.mask.bitmap = null;
}
}
diff --git a/app/packages/looker/src/overlays/heatmap.ts b/app/packages/looker/src/overlays/heatmap.ts
index c1be209ed5..b9e41e1a9c 100644
--- a/app/packages/looker/src/overlays/heatmap.ts
+++ b/app/packages/looker/src/overlays/heatmap.ts
@@ -207,16 +207,8 @@ export default class HeatmapOverlay
}
public cleanup(): void {
- if (this.label.map?.bitmap) {
- // store height and width in bitmap object since it might be used again
- const height = this.label.map.bitmap.height;
- const width = this.label.map.bitmap.width;
-
- this.label.map?.bitmap.close();
- this.label.map.bitmap = null;
-
- this.label.map.closedBitmapDims = { width, height };
- }
+ this.label.map?.bitmap?.close();
+ this.label.map.bitmap = null;
}
}
diff --git a/app/packages/looker/src/overlays/segmentation.ts b/app/packages/looker/src/overlays/segmentation.ts
index 3218db80a6..566a7153b0 100644
--- a/app/packages/looker/src/overlays/segmentation.ts
+++ b/app/packages/looker/src/overlays/segmentation.ts
@@ -262,16 +262,8 @@ export default class SegmentationOverlay
}
public cleanup(): void {
- if (this.label.mask?.bitmap) {
- // store height and width in bitmap object since it might be used again
- const height = this.label.mask.bitmap.height;
- const width = this.label.mask.bitmap.width;
-
- this.label.mask?.bitmap.close();
- this.label.mask.bitmap = null;
-
- this.label.mask.closedBitmapDims = { width, height };
- }
+ this.label.mask?.bitmap?.close();
+ this.label.mask.bitmap = null;
}
}
diff --git a/app/packages/looker/src/worker/disk-overlay-decoder.ts b/app/packages/looker/src/worker/disk-overlay-decoder.ts
index e2cbf6081b..c5ac65acd1 100644
--- a/app/packages/looker/src/worker/disk-overlay-decoder.ts
+++ b/app/packages/looker/src/worker/disk-overlay-decoder.ts
@@ -56,11 +56,16 @@ export const decodeOverlayOnDisk = async (
// it's possible we're just re-coloring, in which case re-init mask image and set bitmap to null
if (
label[overlayField] &&
- label[overlayField].closedBitmapDims &&
+ label[overlayField].bitmap &&
!label[overlayField].image
) {
- const height = label[overlayField].closedBitmapDims.height;
- const width = label[overlayField].closedBitmapDims.width;
+ const height = label[overlayField].bitmap.height;
+ const width = label[overlayField].bitmap.width;
+
+ // close the copied bitmap
+ label[overlayField].bitmap.close();
+ label[overlayField].bitmap = null;
+
label[overlayField].image = new ArrayBuffer(height * width * 4);
label[overlayField].bitmap.close();
label[overlayField].bitmap = null;
From 1510b47fccab0aac3512026982559fc5ecca63d8 Mon Sep 17 00:00:00 2001
From: Sashank Aryal
Date: Thu, 12 Dec 2024 16:43:42 -0600
Subject: [PATCH 039/104] remove bitmap = null (obj closed for modification)
---
app/packages/looker/src/overlays/detection.ts | 1 -
app/packages/looker/src/overlays/heatmap.ts | 1 -
app/packages/looker/src/overlays/segmentation.ts | 1 -
3 files changed, 3 deletions(-)
diff --git a/app/packages/looker/src/overlays/detection.ts b/app/packages/looker/src/overlays/detection.ts
index 1298cf2fbc..e8616f71b1 100644
--- a/app/packages/looker/src/overlays/detection.ts
+++ b/app/packages/looker/src/overlays/detection.ts
@@ -263,7 +263,6 @@ export default class DetectionOverlay<
public cleanup(): void {
this.label.mask?.bitmap?.close();
- this.label.mask.bitmap = null;
}
}
diff --git a/app/packages/looker/src/overlays/heatmap.ts b/app/packages/looker/src/overlays/heatmap.ts
index b9e41e1a9c..d8fb8909d5 100644
--- a/app/packages/looker/src/overlays/heatmap.ts
+++ b/app/packages/looker/src/overlays/heatmap.ts
@@ -208,7 +208,6 @@ export default class HeatmapOverlay
public cleanup(): void {
this.label.map?.bitmap?.close();
- this.label.map.bitmap = null;
}
}
diff --git a/app/packages/looker/src/overlays/segmentation.ts b/app/packages/looker/src/overlays/segmentation.ts
index 566a7153b0..04c2fc693b 100644
--- a/app/packages/looker/src/overlays/segmentation.ts
+++ b/app/packages/looker/src/overlays/segmentation.ts
@@ -263,7 +263,6 @@ export default class SegmentationOverlay
public cleanup(): void {
this.label.mask?.bitmap?.close();
- this.label.mask.bitmap = null;
}
}
From e22b56ec942eee32064a49ac1ac74fa734fe36f5 Mon Sep 17 00:00:00 2001
From: Sashank Aryal
Date: Thu, 12 Dec 2024 16:50:55 -0600
Subject: [PATCH 040/104] remove unnecessary sample null check guard
---
app/packages/looker/src/worker/index.ts | 4 ----
1 file changed, 4 deletions(-)
diff --git a/app/packages/looker/src/worker/index.ts b/app/packages/looker/src/worker/index.ts
index 3f1e6cefeb..dcf0b2e79b 100644
--- a/app/packages/looker/src/worker/index.ts
+++ b/app/packages/looker/src/worker/index.ts
@@ -327,10 +327,6 @@ const processSample = async ({
labelTagColors,
schema,
}: ProcessSample) => {
- if (!sample) {
- return;
- }
-
mapId(sample);
const imageBitmapPromises: Promise[] = [];
From 73da3ae62932b9b09189aee2f1e59d827662dd7b Mon Sep 17 00:00:00 2001
From: Benjamin Kane
Date: Thu, 12 Dec 2024 18:14:41 -0500
Subject: [PATCH 041/104] use buffers for hasFrame (#5264)
---
app/packages/looker/src/lookers/utils.test.ts | 19 +++++++++++++++++++
app/packages/looker/src/lookers/utils.ts | 7 +++++++
app/packages/looker/src/lookers/video.ts | 9 ++-------
3 files changed, 28 insertions(+), 7 deletions(-)
create mode 100644 app/packages/looker/src/lookers/utils.test.ts
create mode 100644 app/packages/looker/src/lookers/utils.ts
diff --git a/app/packages/looker/src/lookers/utils.test.ts b/app/packages/looker/src/lookers/utils.test.ts
new file mode 100644
index 0000000000..6c0d307e6e
--- /dev/null
+++ b/app/packages/looker/src/lookers/utils.test.ts
@@ -0,0 +1,19 @@
+import { describe, expect, it } from "vitest";
+import type { Buffers } from "../state";
+import { hasFrame } from "./utils";
+
+describe("looker utilities", () => {
+ it("determines frame availability given a buffer list", () => {
+ const BUFFERS: Buffers = [
+ [1, 3],
+ [5, 25],
+ ];
+ for (const frameNumber of [1, 10, 25]) {
+ expect(hasFrame(BUFFERS, frameNumber)).toBe(true);
+ }
+
+ for (const frameNumber of [0, 4, 26]) {
+ expect(hasFrame(BUFFERS, frameNumber)).toBe(false);
+ }
+ });
+});
diff --git a/app/packages/looker/src/lookers/utils.ts b/app/packages/looker/src/lookers/utils.ts
new file mode 100644
index 0000000000..ea645401f0
--- /dev/null
+++ b/app/packages/looker/src/lookers/utils.ts
@@ -0,0 +1,7 @@
+import type { Buffers } from "../state";
+
+export const hasFrame = (buffers: Buffers, frameNumber: number) => {
+ return buffers.some(
+ ([start, end]) => start <= frameNumber && frameNumber <= end
+ );
+};
diff --git a/app/packages/looker/src/lookers/video.ts b/app/packages/looker/src/lookers/video.ts
index 24ab04feb0..2fe24f7fab 100644
--- a/app/packages/looker/src/lookers/video.ts
+++ b/app/packages/looker/src/lookers/video.ts
@@ -19,6 +19,7 @@ import { addToBuffers, removeFromBuffers } from "../util";
import { AbstractLooker } from "./abstract";
import { type Frame, acquireReader, clearReader } from "./frame-reader";
import { LookerUtils, withFrames } from "./shared";
+import { hasFrame } from "./utils";
let LOOKER_WITH_READER: VideoLooker | null = null;
@@ -394,13 +395,7 @@ export class VideoLooker extends AbstractLooker {
}
private hasFrame(frameNumber: number) {
- if (frameNumber === this.firstFrameNumber) {
- return this.firstFrame;
- }
- return (
- this.frames.has(frameNumber) &&
- this.frames.get(frameNumber)?.deref() !== undefined
- );
+ return hasFrame(this.state.buffers, frameNumber);
}
private getFrame(frameNumber: number) {
From b0388a2f463f9bcdabc9c048c4c42b51c98d5c69 Mon Sep 17 00:00:00 2001
From: Sashank Aryal
Date: Thu, 12 Dec 2024 17:54:42 -0600
Subject: [PATCH 042/104] use heuristic for detecting grayscale images
---
.../looker/src/worker/canvas-decoder.test.ts | 36 +++++++++++++
.../looker/src/worker/canvas-decoder.ts | 50 +++++++++++++------
2 files changed, 71 insertions(+), 15 deletions(-)
create mode 100644 app/packages/looker/src/worker/canvas-decoder.test.ts
diff --git a/app/packages/looker/src/worker/canvas-decoder.test.ts b/app/packages/looker/src/worker/canvas-decoder.test.ts
new file mode 100644
index 0000000000..57ce3de37b
--- /dev/null
+++ b/app/packages/looker/src/worker/canvas-decoder.test.ts
@@ -0,0 +1,36 @@
+import { describe, expect, it } from "vitest";
+import { isGrayscale } from "./canvas-decoder";
+
+const createData = (
+ pixels: Array<[number, number, number, number]>
+): Uint8ClampedArray => {
+ return new Uint8ClampedArray(pixels.flat());
+};
+
+describe("isGrayscale", () => {
+ it("should return true for a perfectly grayscale image", () => {
+ // all pixels are (100, 100, 100, 255)
+ const data = createData(Array(100).fill([100, 100, 100, 255]));
+ expect(isGrayscale(data)).toBe(true);
+ });
+
+ it("should return false if alpha is not 255", () => {
+ // one pixel with alpha < 255
+ const data = createData([
+ [100, 100, 100, 255],
+ [100, 100, 100, 254],
+ ...Array(98).fill([100, 100, 100, 255]),
+ ]);
+ expect(isGrayscale(data)).toBe(false);
+ });
+
+ it("should return false if any pixel is not grayscale", () => {
+ // one pixel differs in g channel
+ const data = createData([
+ [100, 100, 100, 255],
+ [100, 101, 100, 255],
+ ...Array(98).fill([100, 100, 100, 255]),
+ ]);
+ expect(isGrayscale(data)).toBe(false);
+ });
+});
diff --git a/app/packages/looker/src/worker/canvas-decoder.ts b/app/packages/looker/src/worker/canvas-decoder.ts
index a394554b74..c69da17500 100644
--- a/app/packages/looker/src/worker/canvas-decoder.ts
+++ b/app/packages/looker/src/worker/canvas-decoder.ts
@@ -1,5 +1,26 @@
import { OverlayMask } from "../numpy";
+/**
+ * Checks if the given pixel data is grayscale by sampling a subset of pixels.
+ * If the image is grayscale, the R, G, and B channels will be equal for CHECKS iteration,
+ * and the alpha channel will always be 255.
+ *
+ * Note: this is a very useful heuristic but still doesn't guarantee accuracy.
+ */
+export const isGrayscale = (data: Uint8ClampedArray, checks = 500): boolean => {
+ const totalPixels = data.length / 4;
+ const step = Math.max(1, Math.floor(totalPixels / checks));
+
+ for (let p = 0; p < totalPixels; p += step) {
+ const i = p * 4;
+ const [r, g, b, a] = [data[i], data[i + 1], data[i + 2], data[i + 3]];
+ if (a !== 255 || r !== g || g !== b) {
+ return false;
+ }
+ }
+ return true;
+};
+
/**
* Decodes a given image source into an OverlayMask using an OffscreenCanvas
*/
@@ -12,25 +33,24 @@ export const decodeWithCanvas = async (blob: ImageBitmapSource) => {
const ctx = canvas.getContext("2d");
ctx.drawImage(imageBitmap, 0, 0);
+ imageBitmap.close();
const imageData = ctx.getImageData(0, 0, width, height);
+ const channels = isGrayscale(imageData.data) ? 1 : 4;
- const numChannels = imageData.data.length / (width * height);
-
- const overlayData = {
- width,
- height,
- data: imageData.data,
- channels: numChannels,
- };
-
- // dispose
- imageBitmap.close();
+ if (channels === 1) {
+ // get rid of the G, B, and A channels, new buffer will be 1/4 the size
+ const data = new Uint8ClampedArray(width * height);
+ for (let i = 0; i < data.length; i++) {
+ data[i] = imageData.data[i * 4];
+ }
+ imageData.data.set(data);
+ }
return {
- buffer: overlayData.data.buffer,
- channels: numChannels,
- arrayType: overlayData.data.constructor.name as OverlayMask["arrayType"],
- shape: [overlayData.height, overlayData.width],
+ buffer: imageData.data.buffer,
+ channels,
+ arrayType: "Uint8ClampedArray",
+ shape: [height, width],
} as OverlayMask;
};
From e7f3eddb408c142b59233571c3a7319c6d1c0fcb Mon Sep 17 00:00:00 2001
From: topher
Date: Fri, 13 Dec 2024 00:01:45 +0000
Subject: [PATCH 043/104] bump version after release branch creation
---
fiftyone/constants.py | 2 +-
setup.py | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/fiftyone/constants.py b/fiftyone/constants.py
index 18ca73f47a..fa116a2fc0 100644
--- a/fiftyone/constants.py
+++ b/fiftyone/constants.py
@@ -42,7 +42,7 @@
# This setting may be ``None`` if this client has no compatibility with other
# versions
#
-COMPATIBLE_VERSIONS = ">=0.19,<1.3"
+COMPATIBLE_VERSIONS = ">=0.19,<1.4"
# Package metadata
_META = metadata("fiftyone")
diff --git a/setup.py b/setup.py
index 1009d750c3..544099b830 100644
--- a/setup.py
+++ b/setup.py
@@ -13,7 +13,7 @@
from setuptools import setup, find_packages
-VERSION = "1.2.0"
+VERSION = "1.3.0"
def get_version():
From d570937910e8136f4f965b53b27900b7dd244ac9 Mon Sep 17 00:00:00 2001
From: Sashank Aryal
Date: Thu, 12 Dec 2024 18:11:45 -0600
Subject: [PATCH 044/104] add 1% min
---
.../looker/src/worker/canvas-decoder.test.ts | 13 ++++++++++---
app/packages/looker/src/worker/canvas-decoder.ts | 8 ++++----
2 files changed, 14 insertions(+), 7 deletions(-)
diff --git a/app/packages/looker/src/worker/canvas-decoder.test.ts b/app/packages/looker/src/worker/canvas-decoder.test.ts
index 57ce3de37b..427b3c6131 100644
--- a/app/packages/looker/src/worker/canvas-decoder.test.ts
+++ b/app/packages/looker/src/worker/canvas-decoder.test.ts
@@ -9,13 +9,11 @@ const createData = (
describe("isGrayscale", () => {
it("should return true for a perfectly grayscale image", () => {
- // all pixels are (100, 100, 100, 255)
const data = createData(Array(100).fill([100, 100, 100, 255]));
expect(isGrayscale(data)).toBe(true);
});
it("should return false if alpha is not 255", () => {
- // one pixel with alpha < 255
const data = createData([
[100, 100, 100, 255],
[100, 100, 100, 254],
@@ -25,7 +23,6 @@ describe("isGrayscale", () => {
});
it("should return false if any pixel is not grayscale", () => {
- // one pixel differs in g channel
const data = createData([
[100, 100, 100, 255],
[100, 101, 100, 255],
@@ -33,4 +30,14 @@ describe("isGrayscale", () => {
]);
expect(isGrayscale(data)).toBe(false);
});
+
+ it("should detect a non-grayscale pixel placed deep enough to ensure at least 1% of pixels are checked", () => {
+ // large image: 100,000 pixels. 1% of 100,000 is 1,000.
+ // the function will check at least 1,000 pixels.
+ // place a non-grayscale pixel after 800 pixels.
+ const pixels = Array(100000).fill([50, 50, 50, 255]);
+ pixels[800] = [50, 51, 50, 255]; // this is within the first 1% of pixels
+ const data = createData(pixels);
+ expect(isGrayscale(data)).toBe(false);
+ });
});
diff --git a/app/packages/looker/src/worker/canvas-decoder.ts b/app/packages/looker/src/worker/canvas-decoder.ts
index c69da17500..52d01b5d7b 100644
--- a/app/packages/looker/src/worker/canvas-decoder.ts
+++ b/app/packages/looker/src/worker/canvas-decoder.ts
@@ -2,13 +2,13 @@ import { OverlayMask } from "../numpy";
/**
* Checks if the given pixel data is grayscale by sampling a subset of pixels.
- * If the image is grayscale, the R, G, and B channels will be equal for CHECKS iteration,
+ * The function will check at least 500 pixels or 1% of all pixels, whichever is larger.
+ * If the image is grayscale, the R, G, and B channels will be equal for all sampled pixels,
* and the alpha channel will always be 255.
- *
- * Note: this is a very useful heuristic but still doesn't guarantee accuracy.
*/
-export const isGrayscale = (data: Uint8ClampedArray, checks = 500): boolean => {
+export const isGrayscale = (data: Uint8ClampedArray): boolean => {
const totalPixels = data.length / 4;
+ const checks = Math.max(500, Math.floor(totalPixels * 0.01));
const step = Math.max(1, Math.floor(totalPixels / checks));
for (let p = 0; p < totalPixels; p += step) {
From e48511db7b6ca0b4b18827e862c863445ca38957 Mon Sep 17 00:00:00 2001
From: Sashank Aryal
Date: Thu, 12 Dec 2024 18:13:13 -0600
Subject: [PATCH 045/104] add clarifying comments
---
app/packages/looker/src/worker/canvas-decoder.ts | 2 ++
1 file changed, 2 insertions(+)
diff --git a/app/packages/looker/src/worker/canvas-decoder.ts b/app/packages/looker/src/worker/canvas-decoder.ts
index 52d01b5d7b..390ace2a04 100644
--- a/app/packages/looker/src/worker/canvas-decoder.ts
+++ b/app/packages/looker/src/worker/canvas-decoder.ts
@@ -36,6 +36,8 @@ export const decodeWithCanvas = async (blob: ImageBitmapSource) => {
imageBitmap.close();
const imageData = ctx.getImageData(0, 0, width, height);
+
+ // for nongrayscale images, channel is guaranteed to be 4 (RGBA)
const channels = isGrayscale(imageData.data) ? 1 : 4;
if (channels === 1) {
From d83c00ab0d4590c2a0541a85558a104a35cdcd35 Mon Sep 17 00:00:00 2001
From: Sashank Aryal
Date: Thu, 12 Dec 2024 19:41:45 -0600
Subject: [PATCH 046/104] fix rgb mask recoloring bug
---
app/packages/looker/src/worker/painter.ts | 9 ++++++++-
1 file changed, 8 insertions(+), 1 deletion(-)
diff --git a/app/packages/looker/src/worker/painter.ts b/app/packages/looker/src/worker/painter.ts
index 2e9f5a3ea3..6730d90cac 100644
--- a/app/packages/looker/src/worker/painter.ts
+++ b/app/packages/looker/src/worker/painter.ts
@@ -278,7 +278,14 @@ export const PainterFactory = (requestColor) => ({
const isRgbMaskTargets_ = isRgbMaskTargets(maskTargets);
- if (maskData.channels > 2) {
+ // we have an additional guard for targets length = new image buffer byte length
+ // because we reduce the RGBA mask into a grayscale mask in first load for
+ // performance reasons
+ // For subsequent mask updates, the maskData.buffer is already a single channel
+ if (
+ maskData.channels === 4 &&
+ targets.length === label.mask.image.byteLength
+ ) {
for (let i = 0; i < overlay.length; i++) {
const [r, g, b] = getRgbFromMaskData(targets, maskData.channels, i);
From 686be45e78255804c3a2ad7c42c24b13f82b9ece Mon Sep 17 00:00:00 2001
From: brimoor
Date: Fri, 13 Dec 2024 01:15:18 -0500
Subject: [PATCH 047/104] fix #5254
---
.../panels/model_evaluation/__init__.py | 58 ++++++++++---------
1 file changed, 31 insertions(+), 27 deletions(-)
diff --git a/fiftyone/operators/builtins/panels/model_evaluation/__init__.py b/fiftyone/operators/builtins/panels/model_evaluation/__init__.py
index cb33082f9d..b91efbe01c 100644
--- a/fiftyone/operators/builtins/panels/model_evaluation/__init__.py
+++ b/fiftyone/operators/builtins/panels/model_evaluation/__init__.py
@@ -5,16 +5,17 @@
| `voxel51.com `_
|
"""
-
+from collections import defaultdict, Counter
import os
import traceback
-import fiftyone.operators.types as types
-from collections import defaultdict, Counter
+import numpy as np
+
from fiftyone import ViewField as F
from fiftyone.operators.categories import Categories
from fiftyone.operators.panel import Panel, PanelConfig
from fiftyone.core.plots.plotly import _to_log_colorscale
+import fiftyone.operators.types as types
STORE_NAME = "model_evaluation_panel_builtin"
@@ -104,29 +105,32 @@ def get_avg_confidence(self, per_class_metrics):
total += metrics["confidence"]
return total / count if count > 0 else None
- def get_tp_fp_fn(self, ctx):
- view_state = ctx.panel.get_state("view") or {}
- key = view_state.get("key")
- dataset = ctx.dataset
- tp_key = f"{key}_tp"
- fp_key = f"{key}_fp"
- fn_key = f"{key}_fn"
- tp_total = (
- sum(ctx.dataset.values(tp_key))
- if dataset.has_field(tp_key)
- else None
- )
- fp_total = (
- sum(ctx.dataset.values(fp_key))
- if dataset.has_field(fp_key)
- else None
- )
- fn_total = (
- sum(ctx.dataset.values(fn_key))
- if dataset.has_field(fn_key)
- else None
- )
- return tp_total, fp_total, fn_total
+ def get_tp_fp_fn(self, info, results):
+ # Binary classification
+ if (
+ info.config.type == "classification"
+ and info.config.method == "binary"
+ ):
+ neg_label, pos_label = results.classes
+ tp_count = np.count_nonzero(
+ (results.ytrue == pos_label) & (results.ypred == pos_label)
+ )
+ fp_count = np.count_nonzero(
+ (results.ytrue != pos_label) & (results.ypred == pos_label)
+ )
+ fn_count = np.count_nonzero(
+ (results.ytrue == pos_label) & (results.ypred != pos_label)
+ )
+ return tp_count, fp_count, fn_count
+
+ # Object detection
+ if info.config.type == "detection":
+ tp_count = np.count_nonzero(results.ytrue == results.ypred)
+ fp_count = np.count_nonzero(results.ytrue == results.missing)
+ fn_count = np.count_nonzero(results.ypred == results.missing)
+ return tp_count, fp_count, fn_count
+
+ return None, None, None
def get_map(self, results):
try:
@@ -298,7 +302,7 @@ def load_evaluation(self, ctx):
per_class_metrics
)
metrics["tp"], metrics["fp"], metrics["fn"] = self.get_tp_fp_fn(
- ctx
+ info, results
)
metrics["mAP"] = self.get_map(results)
evaluation_data = {
From 305377967ab16fde74febb01562cc437320d5ff7 Mon Sep 17 00:00:00 2001
From: Justin Newberry
Date: Fri, 13 Dec 2024 10:32:29 -0500
Subject: [PATCH 048/104] Sort Shuffle Stage in FfityOne App (#5270)
* sort
* also here
---------
Co-authored-by: Justin Newberry
---
fiftyone/__public__.py | 2 +-
fiftyone/core/stages.py | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/fiftyone/__public__.py b/fiftyone/__public__.py
index f343bdf718..1ccd9d15a5 100644
--- a/fiftyone/__public__.py
+++ b/fiftyone/__public__.py
@@ -215,7 +215,6 @@
MatchLabels,
MatchTags,
Mongo,
- Shuffle,
Select,
SelectBy,
SelectFields,
@@ -224,6 +223,7 @@
SelectGroupSlices,
SelectLabels,
SetField,
+ Shuffle,
Skip,
SortBy,
SortBySimilarity,
diff --git a/fiftyone/core/stages.py b/fiftyone/core/stages.py
index eb29d5a942..a71272bf15 100644
--- a/fiftyone/core/stages.py
+++ b/fiftyone/core/stages.py
@@ -8628,7 +8628,6 @@ def repr_ViewExpression(self, expr, level):
MatchLabels,
MatchTags,
Mongo,
- Shuffle,
Select,
SelectBy,
SelectFields,
@@ -8637,6 +8636,7 @@ def repr_ViewExpression(self, expr, level):
SelectGroupSlices,
SelectLabels,
SetField,
+ Shuffle,
Skip,
SortBy,
SortBySimilarity,
From 79b83950344bad5bf43455be50ba31c006db9a9e Mon Sep 17 00:00:00 2001
From: afoley587 <54959686+afoley587@users.noreply.github.com>
Date: Fri, 13 Dec 2024 11:12:25 -0500
Subject: [PATCH 049/104] fix(ci): AS-359 Update Ubuntu24 Binaries For MongoDB
(#5269)
---
.github/workflows/test.yml | 2 +-
package/db/setup.py | 6 +++---
2 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index e21ef73509..c85454979b 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -4,7 +4,7 @@ on: workflow_call
jobs:
test-app:
- runs-on: ubuntu-20.04
+ runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
diff --git a/package/db/setup.py b/package/db/setup.py
index 63c5ac5dfe..ccf07ba9ef 100644
--- a/package/db/setup.py
+++ b/package/db/setup.py
@@ -124,8 +124,8 @@
"x86_64": "https://fastdl.mongodb.org/linux/mongodb-linux-x86_64-ubuntu2204-7.0.4.tgz",
},
"24": {
- "aarch64": "https://fastdl.mongodb.org/linux/mongodb-linux-aarch64-ubuntu2204-7.0.4.tgz",
- "x86_64": "https://fastdl.mongodb.org/linux/mongodb-linux-x86_64-ubuntu2204-7.0.4.tgz",
+ "aarch64": "https://fastdl.mongodb.org/linux/mongodb-linux-aarch64-ubuntu2404-8.0.4.tgz",
+ "x86_64": "https://fastdl.mongodb.org/linux/mongodb-linux-x86_64-ubuntu2404-8.0.4.tgz",
},
},
}
@@ -175,7 +175,7 @@ def _get_download():
MONGODB_BINARIES = ["mongod"]
-VERSION = "1.1.7"
+VERSION = "1.2.0"
def get_version():
From 8da1243d81384d0e74adda97e2a64d25055c18fe Mon Sep 17 00:00:00 2001
From: topher
Date: Fri, 13 Dec 2024 11:49:47 -0500
Subject: [PATCH 050/104] Sort Shuffle Stage in FfityOne App (#5270) (#5272)
* sort
* also here
---------
Co-authored-by: Justin Newberry
Co-authored-by: Justin Newberry
---
fiftyone/__public__.py | 2 +-
fiftyone/core/stages.py | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/fiftyone/__public__.py b/fiftyone/__public__.py
index f343bdf718..1ccd9d15a5 100644
--- a/fiftyone/__public__.py
+++ b/fiftyone/__public__.py
@@ -215,7 +215,6 @@
MatchLabels,
MatchTags,
Mongo,
- Shuffle,
Select,
SelectBy,
SelectFields,
@@ -224,6 +223,7 @@
SelectGroupSlices,
SelectLabels,
SetField,
+ Shuffle,
Skip,
SortBy,
SortBySimilarity,
diff --git a/fiftyone/core/stages.py b/fiftyone/core/stages.py
index eb29d5a942..a71272bf15 100644
--- a/fiftyone/core/stages.py
+++ b/fiftyone/core/stages.py
@@ -8628,7 +8628,6 @@ def repr_ViewExpression(self, expr, level):
MatchLabels,
MatchTags,
Mongo,
- Shuffle,
Select,
SelectBy,
SelectFields,
@@ -8637,6 +8636,7 @@ def repr_ViewExpression(self, expr, level):
SelectGroupSlices,
SelectLabels,
SetField,
+ Shuffle,
Skip,
SortBy,
SortBySimilarity,
From d41fffce5d5ba8a34361bf3de2f3b5ad238eca8a Mon Sep 17 00:00:00 2001
From: imanjra
Date: Fri, 13 Dec 2024 14:51:13 -0500
Subject: [PATCH 051/104] TP/FP/NP support for binary classification model
evaluation
---
.../NativeModelEvaluationView/Evaluation.tsx | 10 +++++---
.../panels/model_evaluation/__init__.py | 25 +++++++++++++------
2 files changed, 24 insertions(+), 11 deletions(-)
diff --git a/app/packages/core/src/plugins/SchemaIO/components/NativeModelEvaluationView/Evaluation.tsx b/app/packages/core/src/plugins/SchemaIO/components/NativeModelEvaluationView/Evaluation.tsx
index d7f932b23f..c3ee377dab 100644
--- a/app/packages/core/src/plugins/SchemaIO/components/NativeModelEvaluationView/Evaluation.tsx
+++ b/app/packages/core/src/plugins/SchemaIO/components/NativeModelEvaluationView/Evaluation.tsx
@@ -166,6 +166,7 @@ export default function Evaluation(props: EvaluationProps) {
const evaluationConfig = evaluationInfo.config;
const evaluationMetrics = evaluation.metrics;
const evaluationType = evaluationConfig.type;
+ const evaluationMethod = evaluationConfig.method;
const compareEvaluationInfo = compareEvaluation?.info || {};
const compareEvaluationKey = compareEvaluationInfo?.key;
const compareEvaluationTimestamp = compareEvaluationInfo?.timestamp;
@@ -174,6 +175,9 @@ export default function Evaluation(props: EvaluationProps) {
const compareEvaluationType = compareEvaluationConfig.type;
const isObjectDetection = evaluationType === "detection";
const isSegmentation = evaluationType === "segmentation";
+ const isBinaryClassification =
+ evaluationType === "classification" && evaluationMethod === "binary";
+ const showTpFpFn = isObjectDetection || isBinaryClassification;
const infoRows = [
{
id: "evaluation_key",
@@ -385,7 +389,7 @@ export default function Evaluation(props: EvaluationProps) {
? "compare"
: "selected"
: false,
- hide: !isObjectDetection,
+ hide: !showTpFpFn,
},
{
id: "fp",
@@ -400,7 +404,7 @@ export default function Evaluation(props: EvaluationProps) {
? "compare"
: "selected"
: false,
- hide: !isObjectDetection,
+ hide: !showTpFpFn,
},
{
id: "fn",
@@ -415,7 +419,7 @@ export default function Evaluation(props: EvaluationProps) {
? "compare"
: "selected"
: false,
- hide: !isObjectDetection,
+ hide: !showTpFpFn,
},
];
diff --git a/fiftyone/operators/builtins/panels/model_evaluation/__init__.py b/fiftyone/operators/builtins/panels/model_evaluation/__init__.py
index b91efbe01c..e8a1aff301 100644
--- a/fiftyone/operators/builtins/panels/model_evaluation/__init__.py
+++ b/fiftyone/operators/builtins/panels/model_evaluation/__init__.py
@@ -5,6 +5,7 @@
| `voxel51.com `_
|
"""
+
from collections import defaultdict, Counter
import os
import traceback
@@ -96,6 +97,12 @@ def on_load(self, ctx):
ctx.panel.set_data("permissions", permissions)
self.load_pending_evaluations(ctx)
+ def is_binary_classification(self, info):
+ return (
+ info.config.type == "classification"
+ and info.config.method == "binary"
+ )
+
def get_avg_confidence(self, per_class_metrics):
count = 0
total = 0
@@ -107,10 +114,7 @@ def get_avg_confidence(self, per_class_metrics):
def get_tp_fp_fn(self, info, results):
# Binary classification
- if (
- info.config.type == "classification"
- and info.config.method == "binary"
- ):
+ if self.is_binary_classification(info):
neg_label, pos_label = results.classes
tp_count = np.count_nonzero(
(results.ytrue == pos_label) & (results.ypred == pos_label)
@@ -422,10 +426,15 @@ def load_view(self, ctx):
gt_field, F("label") == y
).filter_labels(pred_field, F("label") == x)
elif view_type == "field":
- view = ctx.dataset.filter_labels(
- pred_field, F(computed_eval_key) == field
- )
-
+ if self.is_binary_classification(info):
+ uppercase_field = field.upper()
+ view = ctx.dataset.match(
+ {computed_eval_key: {"$eq": uppercase_field}}
+ )
+ else:
+ view = ctx.dataset.filter_labels(
+ pred_field, F(computed_eval_key) == field
+ )
if view is not None:
ctx.ops.set_view(view)
From b2734ab695e94bf42bf226ae7dfa4c9a7ebe1378 Mon Sep 17 00:00:00 2001
From: prerna <163362853+prernadh@users.noreply.github.com>
Date: Sat, 14 Dec 2024 02:27:22 +0530
Subject: [PATCH 052/104] Capturing error in loading a specific config class
better and bumping log level (#5213)
* Capturing the error better and bumping log level
* Updating error message
---
fiftyone/core/runs.py | 7 ++++---
1 file changed, 4 insertions(+), 3 deletions(-)
diff --git a/fiftyone/core/runs.py b/fiftyone/core/runs.py
index 442588798c..f9ecff22a1 100644
--- a/fiftyone/core/runs.py
+++ b/fiftyone/core/runs.py
@@ -135,9 +135,10 @@ def from_dict(cls, d):
try:
config_cls = etau.get_class(config_cls)
- except:
- logger.debug(
- "Unable to load '%s'; falling back to base class", config_cls
+ except Exception as e:
+ logger.warning(
+ f"Unable to load {config_cls}; falling back to base class",
+ exc_info=True,
)
config_cls = cls.base_config_cls(type)
From 981c42df845e46492e960a0a5b9ab91d196f5688 Mon Sep 17 00:00:00 2001
From: brimoor
Date: Fri, 13 Dec 2024 01:17:03 -0500
Subject: [PATCH 053/104] include all labels in views
---
.../panels/model_evaluation/__init__.py | 97 +++++++++++++++----
1 file changed, 76 insertions(+), 21 deletions(-)
diff --git a/fiftyone/operators/builtins/panels/model_evaluation/__init__.py b/fiftyone/operators/builtins/panels/model_evaluation/__init__.py
index e8a1aff301..807c446cd5 100644
--- a/fiftyone/operators/builtins/panels/model_evaluation/__init__.py
+++ b/fiftyone/operators/builtins/panels/model_evaluation/__init__.py
@@ -97,12 +97,6 @@ def on_load(self, ctx):
ctx.panel.set_data("permissions", permissions)
self.load_pending_evaluations(ctx)
- def is_binary_classification(self, info):
- return (
- info.config.type == "classification"
- and info.config.method == "binary"
- )
-
def get_avg_confidence(self, per_class_metrics):
count = 0
total = 0
@@ -114,7 +108,10 @@ def get_avg_confidence(self, per_class_metrics):
def get_tp_fp_fn(self, info, results):
# Binary classification
- if self.is_binary_classification(info):
+ if (
+ info.config.type == "classification"
+ and info.config.method == "binary"
+ ):
neg_label, pos_label = results.classes
tp_count = np.count_nonzero(
(results.ytrue == pos_label) & (results.ypred == pos_label)
@@ -418,23 +415,81 @@ def load_view(self, ctx):
y = view_options.get("y", None)
field = view_options.get("field", None)
computed_eval_key = view_options.get("key", eval_key)
+ eval_view = ctx.dataset.load_evaluation_view(eval_key)
+
view = None
- if view_type == "class":
- view = ctx.dataset.filter_labels(pred_field, F("label") == x)
- elif view_type == "matrix":
- view = ctx.dataset.filter_labels(
- gt_field, F("label") == y
- ).filter_labels(pred_field, F("label") == x)
- elif view_type == "field":
- if self.is_binary_classification(info):
- uppercase_field = field.upper()
- view = ctx.dataset.match(
- {computed_eval_key: {"$eq": uppercase_field}}
+ if info.config.type == "classification":
+ if view_type == "class":
+ view = eval_view.match(
+ (F(f"{gt_field}.label") == x)
+ | (F(f"{pred_field}.label") == x)
)
- else:
- view = ctx.dataset.filter_labels(
- pred_field, F(computed_eval_key) == field
+ elif view_type == "matrix":
+ view = eval_view.match(
+ (F(f"{gt_field}.label") == y)
+ & (F(f"{pred_field}.label") == x)
+ )
+ elif view_type == "field":
+ if field == "fn":
+ view = eval_view.match(
+ F(f"{gt_field}.{computed_eval_key}") == field
+ )
+ else:
+ view = eval_view.match(
+ F(f"{pred_field}.{computed_eval_key}") == field
+ )
+ elif info.config.type == "detection":
+ _, pred_root = ctx.dataset._get_label_field_path(pred_field)
+ _, gt_root = ctx.dataset._get_label_field_path(gt_field)
+
+ if view_type == "class":
+ view = (
+ eval_view.filter_labels(
+ pred_field, F("label") == x, only_matches=False
+ )
+ .filter_labels(
+ gt_field, F("label") == x, only_matches=False
+ )
+ .match(
+ (F(pred_root).length() > 0) | (F(gt_root).length() > 0)
+ )
)
+ elif view_type == "matrix":
+ view = (
+ eval_view.filter_labels(
+ gt_field, F("label") == y, only_matches=False
+ )
+ .filter_labels(
+ pred_field, F("label") == x, only_matches=False
+ )
+ .match(
+ (F(pred_root).length() > 0) & (F(gt_root).length() > 0)
+ )
+ )
+ elif view_type == "field":
+ if field == "tp":
+ view = eval_view.filter_labels(
+ gt_field,
+ F(computed_eval_key) == field,
+ only_matches=False,
+ ).filter_labels(
+ pred_field,
+ F(computed_eval_key) == field,
+ only_matches=True,
+ )
+ elif field == "fn":
+ view = eval_view.filter_labels(
+ gt_field,
+ F(computed_eval_key) == field,
+ only_matches=True,
+ )
+ else:
+ view = eval_view.filter_labels(
+ pred_field,
+ F(computed_eval_key) == field,
+ only_matches=True,
+ )
+
if view is not None:
ctx.ops.set_view(view)
From a9ea1c3f1a31031d567cd4520e8ae27e60285bec Mon Sep 17 00:00:00 2001
From: brimoor
Date: Sat, 14 Dec 2024 22:45:14 -0500
Subject: [PATCH 054/104] filtering comparison field as well
---
.../panels/model_evaluation/__init__.py | 135 +++++++++++-------
1 file changed, 87 insertions(+), 48 deletions(-)
diff --git a/fiftyone/operators/builtins/panels/model_evaluation/__init__.py b/fiftyone/operators/builtins/panels/model_evaluation/__init__.py
index 807c446cd5..96684ce080 100644
--- a/fiftyone/operators/builtins/panels/model_evaluation/__init__.py
+++ b/fiftyone/operators/builtins/panels/model_evaluation/__init__.py
@@ -312,6 +312,8 @@ def load_evaluation(self, ctx):
"confusion_matrices": self.get_confusion_matrices(results),
"per_class_metrics": per_class_metrics,
}
+ ctx.panel.set_state("missing", results.missing)
+
if ENABLE_CACHING:
# Cache the evaluation data
try:
@@ -406,88 +408,125 @@ def load_view(self, ctx):
return
view_state = ctx.panel.get_state("view") or {}
+ view_options = ctx.params.get("options", {})
+
eval_key = view_state.get("key")
+ eval_key = view_options.get("key", eval_key)
+ eval_view = ctx.dataset.load_evaluation_view(eval_key)
info = ctx.dataset.get_evaluation_info(eval_key)
pred_field = info.config.pred_field
gt_field = info.config.gt_field
- view_options = ctx.params.get("options", {})
+
+ eval_key2 = view_state.get("compareKey", None)
+ pred_field2 = None
+ gt_field2 = None
+ if eval_key2 is not None:
+ info2 = ctx.dataset.get_evaluation_info(eval_key2)
+ pred_field2 = info2.config.pred_field
+ if info2.config.gt_field != gt_field:
+ gt_field2 = info2.config.gt_field
+
x = view_options.get("x", None)
y = view_options.get("y", None)
field = view_options.get("field", None)
- computed_eval_key = view_options.get("key", eval_key)
- eval_view = ctx.dataset.load_evaluation_view(eval_key)
+ missing = ctx.panel.get_state("missing", "(none)")
view = None
if info.config.type == "classification":
if view_type == "class":
- view = eval_view.match(
- (F(f"{gt_field}.label") == x)
- | (F(f"{pred_field}.label") == x)
- )
+ # All GT/predictions of class `x`
+ expr = F(f"{gt_field}.label") == x
+ expr |= F(f"{pred_field}.label") == x
+ if gt_field2 is not None:
+ expr |= F(f"{gt_field2}.label") == x
+ if pred_field2 is not None:
+ expr |= F(f"{pred_field2}.label") == x
+ view = eval_view.match(expr)
elif view_type == "matrix":
- view = eval_view.match(
- (F(f"{gt_field}.label") == y)
- & (F(f"{pred_field}.label") == x)
- )
+ # Specific confusion matrix cell (including FP/FN)
+ expr = F(f"{gt_field}.label") == y
+ expr &= F(f"{pred_field}.label") == x
+ view = eval_view.match(expr)
elif view_type == "field":
- if field == "fn":
- view = eval_view.match(
- F(f"{gt_field}.{computed_eval_key}") == field
- )
+ if info.config.method == "binary":
+ # All TP/FP/FN
+ expr = F(f"{eval_key}") == field.upper()
+ view = eval_view.match(expr)
else:
- view = eval_view.match(
- F(f"{pred_field}.{computed_eval_key}") == field
- )
+ # Correct/incorrect
+ expr = F(f"{eval_key}") == field
+ view = eval_view.match(expr)
elif info.config.type == "detection":
- _, pred_root = ctx.dataset._get_label_field_path(pred_field)
_, gt_root = ctx.dataset._get_label_field_path(gt_field)
+ _, pred_root = ctx.dataset._get_label_field_path(pred_field)
+ if gt_field2 is not None:
+ _, gt_root2 = ctx.dataset._get_label_field_path(gt_field2)
+ if pred_field2 is not None:
+ _, pred_root2 = ctx.dataset._get_label_field_path(pred_field2)
if view_type == "class":
- view = (
- eval_view.filter_labels(
- pred_field, F("label") == x, only_matches=False
- )
- .filter_labels(
- gt_field, F("label") == x, only_matches=False
+ # All GT/predictions of class `x`
+ view = eval_view.filter_labels(
+ gt_field, F("label") == x, only_matches=False
+ )
+ expr = F(gt_root).length() > 0
+ view = view.filter_labels(
+ pred_field, F("label") == x, only_matches=False
+ )
+ expr |= F(pred_root).length() > 0
+ if gt_field2 is not None:
+ view = view.filter_labels(
+ gt_field2, F("label") == x, only_matches=False
)
- .match(
- (F(pred_root).length() > 0) | (F(gt_root).length() > 0)
+ expr |= F(gt_root2).length() > 0
+ if pred_field2 is not None:
+ view = view.filter_labels(
+ pred_field2, F("label") == x, only_matches=False
)
- )
+ expr |= F(pred_root2).length() > 0
+ view = view.match(expr)
elif view_type == "matrix":
- view = (
- eval_view.filter_labels(
+ if y == missing:
+ # False positives of class `x`
+ expr = (F("label") == x) & (F(eval_key) == "fp")
+ view = eval_view.filter_labels(
+ pred_field, expr, only_matches=True
+ )
+ elif x == missing:
+ # False negatives of class `y`
+ expr = (F("label") == y) & (F(eval_key) == "fn")
+ view = eval_view.filter_labels(
+ gt_field, expr, only_matches=True
+ )
+ else:
+ # All class `y` GT and class `x` predictions in same sample
+ view = eval_view.filter_labels(
gt_field, F("label") == y, only_matches=False
)
- .filter_labels(
+ expr = F(gt_root).length() > 0
+ view = view.filter_labels(
pred_field, F("label") == x, only_matches=False
)
- .match(
- (F(pred_root).length() > 0) & (F(gt_root).length() > 0)
- )
- )
+ expr &= F(pred_root).length() > 0
+ view = view.match(expr)
elif view_type == "field":
if field == "tp":
+ # All true positives
view = eval_view.filter_labels(
- gt_field,
- F(computed_eval_key) == field,
- only_matches=False,
- ).filter_labels(
- pred_field,
- F(computed_eval_key) == field,
- only_matches=True,
+ gt_field, F(eval_key) == field, only_matches=False
+ )
+ view = view.filter_labels(
+ pred_field, F(eval_key) == field, only_matches=True
)
elif field == "fn":
+ # All false negatives
view = eval_view.filter_labels(
- gt_field,
- F(computed_eval_key) == field,
- only_matches=True,
+ gt_field, F(eval_key) == field, only_matches=True
)
else:
+ # All false positives
view = eval_view.filter_labels(
- pred_field,
- F(computed_eval_key) == field,
- only_matches=True,
+ pred_field, F(eval_key) == field, only_matches=True
)
if view is not None:
From fdf58fcadce758fa1af7298d26981e93ce0625f8 Mon Sep 17 00:00:00 2001
From: Ritchie Martori
Date: Mon, 16 Dec 2024 14:54:16 -0700
Subject: [PATCH 055/104] initial executor test
---
tests/unittests/operators/executor_tests.py | 49 +++++++++++++++++++++
1 file changed, 49 insertions(+)
create mode 100644 tests/unittests/operators/executor_tests.py
diff --git a/tests/unittests/operators/executor_tests.py b/tests/unittests/operators/executor_tests.py
new file mode 100644
index 0000000000..1958997e29
--- /dev/null
+++ b/tests/unittests/operators/executor_tests.py
@@ -0,0 +1,49 @@
+import pytest
+from unittest.mock import MagicMock, patch
+from starlette.exceptions import HTTPException
+
+from fiftyone.operators.operator import Operator
+from fiftyone.operators.executor import (
+ execute_or_delegate_operator,
+ ExecutionResult,
+ ExecutionContext,
+)
+from fiftyone.operators import OperatorConfig
+
+
+class EchoOperator(Operator):
+ @property
+ def config(self):
+ return OperatorConfig(name="echo")
+
+ @property
+ def uri(self):
+ return "@testing/plugin/echo"
+
+ def execute(self, ctx):
+ return {"message": ctx.params.get("message", None)}
+
+
+@pytest.mark.asyncio
+@patch("fiftyone.operators.executor.OperatorRegistry")
+async def test_execute_or_delegate_operator_with_global_mock(
+ mock_registry_cls,
+):
+ test_op = EchoOperator()
+ mock_registry = MagicMock()
+ mock_registry.can_execute.return_value = True
+ mock_registry.operator_exists.return_value = True
+ mock_registry.get_operator.return_value = test_op
+ mock_registry_cls.return_value = mock_registry
+
+ request_params = {
+ "dataset_name": "test_dataset",
+ "operator_uri": test_op.uri,
+ "params": {"message": "Hello, World!"},
+ }
+
+ result = await execute_or_delegate_operator(test_op.uri, request_params)
+
+ assert isinstance(result, ExecutionResult)
+ json_result = result.to_json()
+ assert json_result["result"]["message"] == "Hello, World!"
From 3fb035b401f2b462d6100bf398148e52267b0540 Mon Sep 17 00:00:00 2001
From: Ritchie Martori
Date: Tue, 17 Dec 2024 09:39:47 -0700
Subject: [PATCH 056/104] remove executor test mocking
---
tests/unittests/operators/executor_tests.py | 32 ++++++++++-----------
1 file changed, 16 insertions(+), 16 deletions(-)
diff --git a/tests/unittests/operators/executor_tests.py b/tests/unittests/operators/executor_tests.py
index 1958997e29..ef2321a8dc 100644
--- a/tests/unittests/operators/executor_tests.py
+++ b/tests/unittests/operators/executor_tests.py
@@ -2,6 +2,7 @@
from unittest.mock import MagicMock, patch
from starlette.exceptions import HTTPException
+import fiftyone.operators.types as types
from fiftyone.operators.operator import Operator
from fiftyone.operators.executor import (
execute_or_delegate_operator,
@@ -9,6 +10,10 @@
ExecutionContext,
)
from fiftyone.operators import OperatorConfig
+import fiftyone.operators.builtin as builtin
+
+
+ECHO_URI = "@voxel51/operators/echo"
class EchoOperator(Operator):
@@ -16,33 +21,28 @@ class EchoOperator(Operator):
def config(self):
return OperatorConfig(name="echo")
- @property
- def uri(self):
- return "@testing/plugin/echo"
+ def resolve_input(self, ctx):
+ inputs = types.Object()
+ inputs.str("message")
+ return types.Property(inputs)
def execute(self, ctx):
return {"message": ctx.params.get("message", None)}
-@pytest.mark.asyncio
-@patch("fiftyone.operators.executor.OperatorRegistry")
-async def test_execute_or_delegate_operator_with_global_mock(
- mock_registry_cls,
-):
- test_op = EchoOperator()
- mock_registry = MagicMock()
- mock_registry.can_execute.return_value = True
- mock_registry.operator_exists.return_value = True
- mock_registry.get_operator.return_value = test_op
- mock_registry_cls.return_value = mock_registry
+# Force registration of the operator for testing
+builtin.BUILTIN_OPERATORS.append(EchoOperator(_builtin=True))
+
+@pytest.mark.asyncio
+async def test_execute_or_delegate_operator():
request_params = {
"dataset_name": "test_dataset",
- "operator_uri": test_op.uri,
+ "operator_uri": ECHO_URI,
"params": {"message": "Hello, World!"},
}
- result = await execute_or_delegate_operator(test_op.uri, request_params)
+ result = await execute_or_delegate_operator(ECHO_URI, request_params)
assert isinstance(result, ExecutionResult)
json_result = result.to_json()
From 8997a39c724d8bbf462e22986390c1d4a7c1a04f Mon Sep 17 00:00:00 2001
From: imanjra
Date: Mon, 16 Dec 2024 12:25:45 -0500
Subject: [PATCH 057/104] consistent table heading row text style
---
.../NativeModelEvaluationView/Evaluation.tsx | 38 +++++++++++++------
1 file changed, 26 insertions(+), 12 deletions(-)
diff --git a/app/packages/core/src/plugins/SchemaIO/components/NativeModelEvaluationView/Evaluation.tsx b/app/packages/core/src/plugins/SchemaIO/components/NativeModelEvaluationView/Evaluation.tsx
index c3ee377dab..d6697699ce 100644
--- a/app/packages/core/src/plugins/SchemaIO/components/NativeModelEvaluationView/Evaluation.tsx
+++ b/app/packages/core/src/plugins/SchemaIO/components/NativeModelEvaluationView/Evaluation.tsx
@@ -600,14 +600,16 @@ export default function Evaluation(props: EvaluationProps) {
theme.palette.text.secondary,
fontSize: "1rem",
fontWeight: 600,
},
}}
>
- Metric
+
+ Metric
+
{compareKey}
- Difference
+
+ Difference
+
>
)}
@@ -869,14 +873,16 @@ export default function Evaluation(props: EvaluationProps) {
theme.palette.text.secondary,
fontSize: "1rem",
fontWeight: 600,
},
}}
>
- Metric
+
+ Metric
+
{compareKey}
- {" "}
- Difference
+
+
+ Difference
+
>
)}
@@ -1048,14 +1056,16 @@ export default function Evaluation(props: EvaluationProps) {
theme.palette.text.secondary,
fontSize: "1rem",
fontWeight: 600,
},
}}
>
- Metric
+
+ Metric
+
{compareKey}
{" "}
- Difference
+
+ Difference
+
>
)}
@@ -1236,14 +1248,16 @@ export default function Evaluation(props: EvaluationProps) {
theme.palette.text.secondary,
fontSize: "1rem",
fontWeight: 600,
},
}}
>
- Property
+
+ Property
+
Date: Mon, 16 Dec 2024 12:50:09 -0500
Subject: [PATCH 058/104] fix evaluation timestamp
---
.../SchemaIO/components/NativeModelEvaluationView/utils.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/app/packages/core/src/plugins/SchemaIO/components/NativeModelEvaluationView/utils.ts b/app/packages/core/src/plugins/SchemaIO/components/NativeModelEvaluationView/utils.ts
index 44e7ffec33..4e6ed9d4dd 100644
--- a/app/packages/core/src/plugins/SchemaIO/components/NativeModelEvaluationView/utils.ts
+++ b/app/packages/core/src/plugins/SchemaIO/components/NativeModelEvaluationView/utils.ts
@@ -35,7 +35,7 @@ export function getNumericDifference(
export function formatValue(value: string | number, fractionDigits = 3) {
const numericValue =
typeof value === "number" ? value : parseFloat(value as string);
- if (!isNaN(numericValue)) {
+ if (!isNaN(numericValue) && numericValue == value) {
return parseFloat(numericValue.toFixed(fractionDigits));
}
return value;
From 4cdeff05735b55045e089e4bd422f92a072dd87d Mon Sep 17 00:00:00 2001
From: imanjra
Date: Mon, 16 Dec 2024 13:00:56 -0500
Subject: [PATCH 059/104] hide unsupported metrics in model eval panel
---
.../NativeModelEvaluationView/Evaluation.tsx | 40 +++++++++++--------
1 file changed, 24 insertions(+), 16 deletions(-)
diff --git a/app/packages/core/src/plugins/SchemaIO/components/NativeModelEvaluationView/Evaluation.tsx b/app/packages/core/src/plugins/SchemaIO/components/NativeModelEvaluationView/Evaluation.tsx
index d6697699ce..69aa0a4b39 100644
--- a/app/packages/core/src/plugins/SchemaIO/components/NativeModelEvaluationView/Evaluation.tsx
+++ b/app/packages/core/src/plugins/SchemaIO/components/NativeModelEvaluationView/Evaluation.tsx
@@ -174,6 +174,7 @@ export default function Evaluation(props: EvaluationProps) {
const compareEvaluationMetrics = compareEvaluation?.metrics || {};
const compareEvaluationType = compareEvaluationConfig.type;
const isObjectDetection = evaluationType === "detection";
+ const isClassification = evaluationType === "classification";
const isSegmentation = evaluationType === "segmentation";
const isBinaryClassification =
evaluationType === "classification" && evaluationMethod === "binary";
@@ -226,6 +227,7 @@ export default function Evaluation(props: EvaluationProps) {
property: "IoU Threshold",
value: evaluationConfig.iou,
compareValue: compareEvaluationConfig.iou,
+ hide: !isObjectDetection,
},
{
id: "classwise",
@@ -266,6 +268,7 @@ export default function Evaluation(props: EvaluationProps) {
compareValue: Array.isArray(compareEvaluationConfig.iou_threshs)
? compareEvaluationConfig.iou_threshs.join(", ")
: "",
+ hide: !isObjectDetection,
},
{
id: "max_preds",
@@ -299,12 +302,14 @@ export default function Evaluation(props: EvaluationProps) {
property: "Average Confidence",
value: evaluationMetrics.average_confidence,
compareValue: compareEvaluationMetrics.average_confidence,
+ hide: isSegmentation,
},
{
id: "iou",
property: "IoU Threshold",
value: evaluationConfig.iou,
compareValue: compareEvaluationConfig.iou,
+ hide: !isObjectDetection,
},
{
id: "precision",
@@ -325,6 +330,7 @@ export default function Evaluation(props: EvaluationProps) {
compareValue: compareEvaluationMetrics.fscore,
},
];
+ const computedMetricPerformance = metricPerformance.filter((m) => !m.hide);
const summaryRows = [
{
id: "average_confidence",
@@ -847,8 +853,8 @@ export default function Evaluation(props: EvaluationProps) {
data={[
{
histfunc: "sum",
- y: metricPerformance.map((m) => m.value),
- x: metricPerformance.map((m) => m.property),
+ y: computedMetricPerformance.map((m) => m.value),
+ x: computedMetricPerformance.map((m) => m.property),
type: "histogram",
name: name,
marker: {
@@ -857,8 +863,8 @@ export default function Evaluation(props: EvaluationProps) {
},
{
histfunc: "sum",
- y: metricPerformance.map((m) => m.compareValue),
- x: metricPerformance.map((m) => m.property),
+ y: computedMetricPerformance.map((m) => m.compareValue),
+ x: computedMetricPerformance.map((m) => m.property),
type: "histogram",
name: compareKey,
marker: {
@@ -913,7 +919,7 @@ export default function Evaluation(props: EvaluationProps) {
- {metricPerformance.map((row) => (
+ {computedMetricPerformance.map((row) => (
{row.property}
@@ -1283,17 +1289,19 @@ export default function Evaluation(props: EvaluationProps) {
- {infoRows.map((row) => (
-
-
- {row.property}
-
- {formatValue(row.value)}
- {compareKey && (
- {formatValue(row.compareValue)}
- )}
-
- ))}
+ {infoRows.map((row) =>
+ row.hide ? null : (
+
+
+ {row.property}
+
+ {formatValue(row.value)}
+ {compareKey && (
+ {formatValue(row.compareValue)}
+ )}
+
+ )
+ )}
From 09bb793cba9eadcd908740766e5d087f27c1e08f Mon Sep 17 00:00:00 2001
From: imanjra
Date: Mon, 16 Dec 2024 14:03:40 -0500
Subject: [PATCH 060/104] gracefully handle unsupported model evaluation
---
.../NativeModelEvaluationView/Error.tsx | 40 +++++++++++++++++++
.../NativeModelEvaluationView/ErrorIcon.tsx | 33 +++++++++++++++
.../NativeModelEvaluationView/Evaluation.tsx | 22 +++++++++-
.../panels/model_evaluation/__init__.py | 11 ++++-
4 files changed, 104 insertions(+), 2 deletions(-)
create mode 100644 app/packages/core/src/plugins/SchemaIO/components/NativeModelEvaluationView/Error.tsx
create mode 100644 app/packages/core/src/plugins/SchemaIO/components/NativeModelEvaluationView/ErrorIcon.tsx
diff --git a/app/packages/core/src/plugins/SchemaIO/components/NativeModelEvaluationView/Error.tsx b/app/packages/core/src/plugins/SchemaIO/components/NativeModelEvaluationView/Error.tsx
new file mode 100644
index 0000000000..b04547134e
--- /dev/null
+++ b/app/packages/core/src/plugins/SchemaIO/components/NativeModelEvaluationView/Error.tsx
@@ -0,0 +1,40 @@
+import { West } from "@mui/icons-material";
+import { Box, Button, Card, Stack, Typography } from "@mui/material";
+import React from "react";
+import ErrorIcon from "./ErrorIcon";
+
+export default function Error(props: ErrorProps) {
+ const { onBack } = props;
+ return (
+
+
+ } color="secondary">
+ Back to Model Evaluation
+
+
+
+
+
+
+ Analyze and improve models collaboratively with your team
+
+
+ The Model Evaluation panel currently supports only classification,
+ detection, and segmentation evaluations
+
+
+
+
+ );
+}
+
+type ErrorProps = {
+ onBack: () => void;
+};
diff --git a/app/packages/core/src/plugins/SchemaIO/components/NativeModelEvaluationView/ErrorIcon.tsx b/app/packages/core/src/plugins/SchemaIO/components/NativeModelEvaluationView/ErrorIcon.tsx
new file mode 100644
index 0000000000..8f595c416d
--- /dev/null
+++ b/app/packages/core/src/plugins/SchemaIO/components/NativeModelEvaluationView/ErrorIcon.tsx
@@ -0,0 +1,33 @@
+import { SvgIcon, SvgIconProps } from "@mui/material";
+import React from "react";
+
+export default function ErrorIcon(props: SvgIconProps) {
+ return (
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/app/packages/core/src/plugins/SchemaIO/components/NativeModelEvaluationView/Evaluation.tsx b/app/packages/core/src/plugins/SchemaIO/components/NativeModelEvaluationView/Evaluation.tsx
index 69aa0a4b39..819fdbd5b5 100644
--- a/app/packages/core/src/plugins/SchemaIO/components/NativeModelEvaluationView/Evaluation.tsx
+++ b/app/packages/core/src/plugins/SchemaIO/components/NativeModelEvaluationView/Evaluation.tsx
@@ -42,6 +42,7 @@ import {
} from "@mui/material";
import React, { useEffect, useMemo, useState } from "react";
import { useRecoilState, useSetRecoilState } from "recoil";
+import Error from "./Error";
import EvaluationNotes from "./EvaluationNotes";
import EvaluationPlot from "./EvaluationPlot";
import Status from "./Status";
@@ -90,6 +91,14 @@ export default function Evaluation(props: EvaluationProps) {
const evaluation = data?.[`evaluation_${compareKey}`];
return evaluation;
}, [data]);
+ const evaluationError = useMemo(() => {
+ const evaluation = data?.[`evaluation_${name}_error`];
+ return evaluation;
+ }, [data]);
+ const compareEvaluationError = useMemo(() => {
+ const evaluation = data?.[`evaluation_${compareKey}_error`];
+ return evaluation;
+ }, [data]);
const confusionMatrix = useMemo(() => {
return getMatrix(evaluation?.confusion_matrices, confusionMatrixConfig);
}, [evaluation, confusionMatrixConfig]);
@@ -145,6 +154,10 @@ export default function Evaluation(props: EvaluationProps) {
setConfusionMatrixDialogConfig((state) => ({ ...state, open: false }));
};
+ if (evaluationError) {
+ return ;
+ }
+
if (!evaluation) {
return (
- Compare against
+
+ Compare against
+ {compareEvaluationError && (
+
+ Unsupported model evaluation type
+
+ )}
+
{compareKeys.length === 0 ? (
Date: Fri, 6 Dec 2024 10:40:12 -0500
Subject: [PATCH 061/104] use mask targets in model evaluation panel
---
.../NativeModelEvaluationView/Evaluation.tsx | 50 ++++++++++++++++---
.../panels/model_evaluation/__init__.py | 19 ++++++-
2 files changed, 60 insertions(+), 9 deletions(-)
diff --git a/app/packages/core/src/plugins/SchemaIO/components/NativeModelEvaluationView/Evaluation.tsx b/app/packages/core/src/plugins/SchemaIO/components/NativeModelEvaluationView/Evaluation.tsx
index 819fdbd5b5..cb22817d63 100644
--- a/app/packages/core/src/plugins/SchemaIO/components/NativeModelEvaluationView/Evaluation.tsx
+++ b/app/packages/core/src/plugins/SchemaIO/components/NativeModelEvaluationView/Evaluation.tsx
@@ -99,15 +99,32 @@ export default function Evaluation(props: EvaluationProps) {
const evaluation = data?.[`evaluation_${compareKey}_error`];
return evaluation;
}, [data]);
+ const evaluationMaskTargets = useMemo(() => {
+ return evaluation?.mask_targets || {};
+ }, [evaluation]);
+ const compareEvaluationMaskTargets = useMemo(() => {
+ return compareEvaluation?.mask_targets || {};
+ }, [compareEvaluation]);
const confusionMatrix = useMemo(() => {
- return getMatrix(evaluation?.confusion_matrices, confusionMatrixConfig);
- }, [evaluation, confusionMatrixConfig]);
+ return getMatrix(
+ evaluation?.confusion_matrices,
+ confusionMatrixConfig,
+ evaluationMaskTargets
+ );
+ }, [evaluation, confusionMatrixConfig, evaluationMaskTargets]);
const compareConfusionMatrix = useMemo(() => {
return getMatrix(
compareEvaluation?.confusion_matrices,
- confusionMatrixConfig
+ confusionMatrixConfig,
+ evaluationMaskTargets,
+ compareEvaluationMaskTargets
);
- }, [compareEvaluation, confusionMatrixConfig]);
+ }, [
+ compareEvaluation,
+ confusionMatrixConfig,
+ evaluationMaskTargets,
+ compareEvaluationMaskTargets,
+ ]);
const compareKeys = useMemo(() => {
const keys: string[] = [];
const evaluations = data?.evaluations || [];
@@ -452,9 +469,12 @@ export default function Evaluation(props: EvaluationProps) {
if (!perClassPerformance[metric]) {
perClassPerformance[metric] = [];
}
+ const maskTarget = evaluationMaskTargets?.[key];
+ const compareMaskTarget = compareEvaluationMaskTargets?.[key];
perClassPerformance[metric].push({
id: key,
- property: key,
+ property: maskTarget || key,
+ compareProperty: compareMaskTarget || maskTarget || key,
value: metrics[metric],
compareValue: compareMetrics[metric],
});
@@ -1059,7 +1079,10 @@ export default function Evaluation(props: EvaluationProps) {
y: classPerformance.map(
(metrics) => metrics.compareValue
),
- x: classPerformance.map((metrics) => metrics.property),
+ x: classPerformance.map(
+ (metrics) =>
+ metrics.compareProperty || metrics.property
+ ),
type: "histogram",
name: `${CLASS_LABELS[performanceClass]} per class`,
marker: {
@@ -1218,6 +1241,10 @@ export default function Evaluation(props: EvaluationProps) {
layout={{
yaxis: {
autorange: "reversed",
+ type: "category",
+ },
+ xaxis: {
+ type: "category",
},
}}
/>
@@ -1258,6 +1285,10 @@ export default function Evaluation(props: EvaluationProps) {
layout={{
yaxis: {
autorange: "reversed",
+ type: "category",
+ },
+ xaxis: {
+ type: "category",
},
}}
/>
@@ -1613,14 +1644,17 @@ function formatPerClassPerformance(perClassPerformance, barConfig) {
return computedPerClassPerformance;
}
-function getMatrix(matrices, config) {
+function getMatrix(matrices, config, maskTargets, compareMaskTargets?) {
if (!matrices) return;
const { sortBy = "az", limit } = config;
const parsedLimit = typeof limit === "number" ? limit : undefined;
const classes = matrices[`${sortBy}_classes`].slice(0, parsedLimit);
const matrix = matrices[`${sortBy}_matrix`].slice(0, parsedLimit);
const colorscale = matrices[`${sortBy}_colorscale`];
- return { labels: classes, matrix, colorscale };
+ const labels = classes.map((c) => {
+ return compareMaskTargets?.[c] || maskTargets?.[c] || c;
+ });
+ return { labels, matrix, colorscale };
}
function getConfigLabel({ config, type, dashed }) {
diff --git a/fiftyone/operators/builtins/panels/model_evaluation/__init__.py b/fiftyone/operators/builtins/panels/model_evaluation/__init__.py
index ef8d9b1c47..7b27aa45a8 100644
--- a/fiftyone/operators/builtins/panels/model_evaluation/__init__.py
+++ b/fiftyone/operators/builtins/panels/model_evaluation/__init__.py
@@ -288,6 +288,16 @@ def get_confusion_matrices(self, results):
"lc_colorscale": lc_colorscale,
}
+ def get_mask_targets(self, dataset, gt_field):
+ mask_targets = dataset.mask_targets.get(gt_field, None)
+ if mask_targets:
+ return mask_targets
+
+ if dataset.default_mask_targets:
+ return dataset.default_mask_targets
+
+ return None
+
def load_evaluation(self, ctx):
view_state = ctx.panel.get_state("view") or {}
eval_key = view_state.get("key")
@@ -300,7 +310,6 @@ def load_evaluation(self, ctx):
)
if evaluation_data is None:
info = ctx.dataset.get_evaluation_info(computed_eval_key)
- serialized_info = info.serialize()
evaluation_type = info.config.type
if evaluation_type not in SUPPORTED_EVALUATION_TYPES:
ctx.panel.set_data(
@@ -308,6 +317,13 @@ def load_evaluation(self, ctx):
{"error": "unsupported", "info": serialized_info},
)
return
+ serialized_info = info.serialize()
+ gt_field = info.config.gt_field
+ mask_targets = (
+ self.get_mask_targets(ctx.dataset, gt_field)
+ if evaluation_type == "segmentation"
+ else None
+ )
results = ctx.dataset.load_evaluation_results(computed_eval_key)
metrics = results.metrics()
per_class_metrics = self.get_per_class_metrics(info, results)
@@ -323,6 +339,7 @@ def load_evaluation(self, ctx):
"info": serialized_info,
"confusion_matrices": self.get_confusion_matrices(results),
"per_class_metrics": per_class_metrics,
+ "mask_targets": mask_targets,
}
if ENABLE_CACHING:
# Cache the evaluation data
From 1ddd2bfe601eb8d83f0c8b29360682884b95ce4b Mon Sep 17 00:00:00 2001
From: Benjamin Kane
Date: Tue, 17 Dec 2024 14:57:19 -0500
Subject: [PATCH 062/104] do not add valid list field filters to collapsed
paths (#5280)
---
app/packages/state/src/recoil/sidebar.test.ts | 24 +++++++++++++++++--
app/packages/state/src/recoil/sidebar.ts | 12 +++++++---
2 files changed, 31 insertions(+), 5 deletions(-)
diff --git a/app/packages/state/src/recoil/sidebar.test.ts b/app/packages/state/src/recoil/sidebar.test.ts
index a69542c689..2aa826fc9b 100644
--- a/app/packages/state/src/recoil/sidebar.test.ts
+++ b/app/packages/state/src/recoil/sidebar.test.ts
@@ -2,8 +2,8 @@ import { describe, expect, it, vi } from "vitest";
vi.mock("recoil");
vi.mock("recoil-relay");
-import { Field } from "@fiftyone/utilities";
-import { setMockAtoms, TestSelector } from "../../../../__mocks__/recoil";
+import { DICT_FIELD, Field, STRING_FIELD } from "@fiftyone/utilities";
+import { TestSelector, setMockAtoms } from "../../../../__mocks__/recoil";
import * as sidebar from "./sidebar";
const mockFields = {
@@ -566,3 +566,23 @@ describe("hiddenNoneGroups selector", () => {
expect(testHiddenNoneGroups()).toStrictEqual(present);
});
});
+
+describe("collapsedPaths resolution", () => {
+ it("does not add valid list fields (i.e. with a primitive subfield)", () => {
+ setMockAtoms({
+ field: (path) =>
+ path === "dict_list"
+ ? { subfield: DICT_FIELD }
+ : { subfield: STRING_FIELD },
+ fieldPaths: ({ ftype }) =>
+ ftype === DICT_FIELD ? [] : ["dict_list", "string_list"],
+ fields: () => [],
+ });
+
+ const collapsed = >(
+ (sidebar.collapsedPaths)
+ );
+
+ expect(collapsed()).toStrictEqual(new Set(["dict_list"]));
+ });
+});
diff --git a/app/packages/state/src/recoil/sidebar.ts b/app/packages/state/src/recoil/sidebar.ts
index d68c42865e..d92783dbdd 100644
--- a/app/packages/state/src/recoil/sidebar.ts
+++ b/app/packages/state/src/recoil/sidebar.ts
@@ -20,6 +20,7 @@ import {
LABELS_PATH,
LABEL_DOC_TYPES,
LIST_FIELD,
+ UNSUPPORTED_FILTER_TYPES,
VALID_LABEL_TYPES,
VALID_PRIMITIVE_TYPES,
withPath,
@@ -292,7 +293,6 @@ export const resolveGroups = (
"other",
fieldsMatcher(frameFields, () => true, present, "frames.")
);
-
return groups;
};
@@ -761,11 +761,16 @@ export const isDisabledFilterPath = selectorFamily({
get(disabledFilterPaths).has(path),
});
-const collapsedPaths = selector>({
+export const collapsedPaths = selector>({
key: "collapsedPaths",
get: ({ get }) => {
let paths = [...get(fieldPaths({ ftype: DICT_FIELD }))];
- paths = [...paths, ...get(fieldPaths({ ftype: LIST_FIELD }))];
+ paths = [
+ ...paths,
+ ...get(fieldPaths({ ftype: LIST_FIELD })).filter((path) =>
+ UNSUPPORTED_FILTER_TYPES.includes(get(field(path)).subfield)
+ ),
+ ];
for (const { fields: fieldsData, name: prefix } of get(
fields({ ftype: EMBEDDED_DOCUMENT_FIELD, space: State.SPACE.SAMPLE })
@@ -877,6 +882,7 @@ export const groupShown = selectorFamily<
if (["tags"].includes(group)) {
return null;
}
+
return (
!data.paths.length ||
!data.paths.every((path) => get(collapsedPaths).has(path))
From f7040793c640af3bc52decfd1447518d10173fe4 Mon Sep 17 00:00:00 2001
From: Benjamin Kane
Date: Tue, 17 Dec 2024 14:59:09 -0500
Subject: [PATCH 063/104] add extended selection (#5286)
---
app/packages/state/src/recoil/filters.ts | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/app/packages/state/src/recoil/filters.ts b/app/packages/state/src/recoil/filters.ts
index a22036bfda..d8d91d67ff 100644
--- a/app/packages/state/src/recoil/filters.ts
+++ b/app/packages/state/src/recoil/filters.ts
@@ -6,6 +6,7 @@ import {
import { VALID_PRIMITIVE_TYPES } from "@fiftyone/utilities";
import { DefaultValue, selectorFamily } from "recoil";
import { getSessionRef, sessionAtom } from "../session";
+import { extendedSelection } from "./atoms";
import { pathHasIndexes, queryPerformance } from "./queryPerformance";
import { expandPath, fields } from "./schema";
import { hiddenLabelIds, isFrameField } from "./selectors";
@@ -107,8 +108,10 @@ export const hasFilters = selectorFamily({
({ get }) => {
const f = Object.keys(get(modal ? modalFilters : filters)).length > 0;
const hidden = Boolean(modal && get(hiddenLabelIds).size);
+ const selection =
+ !modal && Boolean(get(extendedSelection)?.selection?.length);
- return f || hidden;
+ return f || hidden || selection;
},
});
From 9c3cb74832267105f280cab1131a8fce9516fd65 Mon Sep 17 00:00:00 2001
From: Sashank Aryal
Date: Tue, 17 Dec 2024 14:38:45 -0600
Subject: [PATCH 064/104] add detections fields to additional media fields
---
fiftyone/server/metadata.py | 2 ++
1 file changed, 2 insertions(+)
diff --git a/fiftyone/server/metadata.py b/fiftyone/server/metadata.py
index 6992447e4a..b5303731b9 100644
--- a/fiftyone/server/metadata.py
+++ b/fiftyone/server/metadata.py
@@ -31,6 +31,8 @@
logger = logging.getLogger(__name__)
_ADDITIONAL_MEDIA_FIELDS = {
+ fol.Detection: "mask_path",
+ fol.Detections: "mask_path",
fol.Heatmap: "map_path",
fol.Segmentation: "mask_path",
OrthographicProjectionMetadata: "filepath",
From bd7d2de1c942c640ddaf961892aeac8d81ecc8cc Mon Sep 17 00:00:00 2001
From: Sashank Aryal
Date: Tue, 17 Dec 2024 14:40:36 -0600
Subject: [PATCH 065/104] use pydash.get instead of _deep_get
---
fiftyone/server/metadata.py | 15 ++-------------
1 file changed, 2 insertions(+), 13 deletions(-)
diff --git a/fiftyone/server/metadata.py b/fiftyone/server/metadata.py
index b5303731b9..bfab94e403 100644
--- a/fiftyone/server/metadata.py
+++ b/fiftyone/server/metadata.py
@@ -12,6 +12,7 @@
import typing as t
from functools import reduce
+from pydash import get
import asyncio
import aiofiles
@@ -415,7 +416,7 @@ def _create_media_urls(
media_urls = []
for field in media_fields:
- path = _deep_get(sample, field)
+ path = get(sample, field)
if path not in cache:
cache[path] = path
@@ -452,15 +453,3 @@ def _get_additional_media_fields(
additional.append(f"{field_name}.{subfield_name}")
return opm_field, additional
-
-
-def _deep_get(sample, keys, default=None):
- """
- Get a value from a nested dictionary by specifying keys delimited by '.',
- similar to lodash's ``_.get()``.
- """
- return reduce(
- lambda d, key: d.get(key, default) if isinstance(d, dict) else default,
- keys.split("."),
- sample,
- )
From d0462b9f7026dcc8574ca514ba0fbbc731e30722 Mon Sep 17 00:00:00 2001
From: Sashank Aryal
Date: Tue, 17 Dec 2024 14:44:45 -0600
Subject: [PATCH 066/104] use detections fields in media urls creation, too
---
fiftyone/server/metadata.py | 38 ++++++++++++++++++++++++++++++++++---
1 file changed, 35 insertions(+), 3 deletions(-)
diff --git a/fiftyone/server/metadata.py b/fiftyone/server/metadata.py
index bfab94e403..47d3a41bf8 100644
--- a/fiftyone/server/metadata.py
+++ b/fiftyone/server/metadata.py
@@ -71,7 +71,11 @@ async def get_metadata(
filepath = sample["filepath"]
metadata = sample.get("metadata", None)
- opm_field, additional_fields = _get_additional_media_fields(collection)
+ (
+ opm_field,
+ detections_fields,
+ additional_fields,
+ ) = _get_additional_media_fields(collection)
filepath_result, filepath_source, urls = _create_media_urls(
collection,
@@ -80,6 +84,7 @@ async def get_metadata(
url_cache,
additional_fields=additional_fields,
opm_field=opm_field,
+ detections_fields=detections_fields,
)
if filepath_result is not None:
filepath = filepath_result
@@ -392,6 +397,7 @@ def _create_media_urls(
cache: t.Dict,
additional_fields: t.Optional[t.List[str]] = None,
opm_field: t.Optional[str] = None,
+ detections_fields: t.Optional[t.List[str]] = None,
) -> t.Dict[str, str]:
filepath_source = None
media_fields = collection.app_config.media_fields.copy()
@@ -399,6 +405,23 @@ def _create_media_urls(
if additional_fields is not None:
media_fields.extend(additional_fields)
+ if detections_fields is not None:
+ for field in detections_fields:
+ detections = get(sample, field)
+
+ if not detections:
+ continue
+
+ detections_list = get(detections, "detections")
+
+ if not detections_list or len(detections_list) == 0:
+ continue
+
+ len_detections = len(detections_list)
+
+ for i in range(len_detections):
+ media_fields.append(f"{field}.detections[{i}].mask_path")
+
if (
sample_media_type == fom.POINT_CLOUD
or sample_media_type == fom.THREE_D
@@ -438,6 +461,8 @@ def _get_additional_media_fields(
) -> t.List[str]:
additional = []
opm_field = None
+ detections_fields = None
+
for cls, subfield_name in _ADDITIONAL_MEDIA_FIELDS.items():
for field_name, field in collection.get_field_schema(
flat=True
@@ -450,6 +475,13 @@ def _get_additional_media_fields(
if cls == OrthographicProjectionMetadata:
opm_field = field_name
- additional.append(f"{field_name}.{subfield_name}")
+ if cls == fol.Detections:
+ if detections_fields is None:
+ detections_fields = [field_name]
+ else:
+ detections_fields.append(field_name)
+
+ else:
+ additional.append(f"{field_name}.{subfield_name}")
- return opm_field, additional
+ return opm_field, detections_fields, additional
From 89d18538603cd08bd62bf6a8ab7eb45e89a1e7ab Mon Sep 17 00:00:00 2001
From: Sashank Aryal
Date: Tue, 17 Dec 2024 14:45:28 -0600
Subject: [PATCH 067/104] add support for collection overlay types in disk
decoder
---
.../looker/src/worker/disk-overlay-decoder.ts | 27 +++++++++++++++----
1 file changed, 22 insertions(+), 5 deletions(-)
diff --git a/app/packages/looker/src/worker/disk-overlay-decoder.ts b/app/packages/looker/src/worker/disk-overlay-decoder.ts
index c5ac65acd1..a7a793b4a9 100644
--- a/app/packages/looker/src/worker/disk-overlay-decoder.ts
+++ b/app/packages/looker/src/worker/disk-overlay-decoder.ts
@@ -25,12 +25,17 @@ export const decodeOverlayOnDisk = async (
sources: { [path: string]: string },
cls: string,
maskPathDecodingPromises: Promise[] = [],
- maskTargetsBuffers: ArrayBuffer[] = []
+ maskTargetsBuffers: ArrayBuffer[] = [],
+ overlayCollectionProcessingParams:
+ | { idx: number; cls: string }
+ | undefined = undefined
) => {
// handle all list types here
- if (cls === DETECTIONS) {
+ if (cls === DETECTIONS && label.detections) {
const promises: Promise[] = [];
- for (const detection of label.detections) {
+
+ for (let i = 0; i < label.detections.length; i++) {
+ const detection = label.detections[i];
promises.push(
decodeOverlayOnDisk(
field,
@@ -38,10 +43,11 @@ export const decodeOverlayOnDisk = async (
coloring,
customizeColorSetting,
colorscale,
- {},
+ sources,
DETECTION,
maskPathDecodingPromises,
- maskTargetsBuffers
+ maskTargetsBuffers,
+ { idx: i, cls: DETECTIONS }
)
);
}
@@ -74,6 +80,17 @@ export const decodeOverlayOnDisk = async (
return;
}
+ // if we have an explicit source defined from sample.urls, use that
+ // otherwise, use the path field from the label
+ let source = sources[`${field}.${overlayPathField}`];
+
+ if (typeof overlayCollectionProcessingParams !== "undefined") {
+ source =
+ sources[
+ `${field}.${overlayCollectionProcessingParams.cls}[${overlayCollectionProcessingParams.idx}].${overlayPathField}`
+ ];
+ }
+
// convert absolute file path to a URL that we can "fetch" from
const overlayImageUrl = getSampleSrc(
sources[`${field}.${overlayPathField}`] || label[overlayPathField]
From d2038b0f549c0593b7dafd07d5fa293b1121cf96 Mon Sep 17 00:00:00 2001
From: Sashank Aryal
Date: Tue, 17 Dec 2024 14:55:09 -0600
Subject: [PATCH 068/104] return if no path
---
fiftyone/server/metadata.py | 3 +++
1 file changed, 3 insertions(+)
diff --git a/fiftyone/server/metadata.py b/fiftyone/server/metadata.py
index 47d3a41bf8..060b95a241 100644
--- a/fiftyone/server/metadata.py
+++ b/fiftyone/server/metadata.py
@@ -441,6 +441,9 @@ def _create_media_urls(
for field in media_fields:
path = get(sample, field)
+ if not path:
+ continue
+
if path not in cache:
cache[path] = path
From cd8eaee5f2d8bfc480d6fa91380b61a81d6d2fff Mon Sep 17 00:00:00 2001
From: Sashank Aryal
Date: Tue, 17 Dec 2024 15:00:35 -0600
Subject: [PATCH 069/104] fix src bug
---
app/packages/looker/src/worker/disk-overlay-decoder.ts | 4 +---
1 file changed, 1 insertion(+), 3 deletions(-)
diff --git a/app/packages/looker/src/worker/disk-overlay-decoder.ts b/app/packages/looker/src/worker/disk-overlay-decoder.ts
index a7a793b4a9..c3aaea0074 100644
--- a/app/packages/looker/src/worker/disk-overlay-decoder.ts
+++ b/app/packages/looker/src/worker/disk-overlay-decoder.ts
@@ -92,9 +92,7 @@ export const decodeOverlayOnDisk = async (
}
// convert absolute file path to a URL that we can "fetch" from
- const overlayImageUrl = getSampleSrc(
- sources[`${field}.${overlayPathField}`] || label[overlayPathField]
- );
+ const overlayImageUrl = getSampleSrc(source || label[overlayPathField]);
const urlTokens = overlayImageUrl.split("?");
let baseUrl = overlayImageUrl;
From 5d6f683968065de6ac920554be308b40cb91592b Mon Sep 17 00:00:00 2001
From: Sashank Aryal
Date: Tue, 17 Dec 2024 15:31:02 -0600
Subject: [PATCH 070/104] add clarification comment for sources
---
app/packages/looker/src/worker/disk-overlay-decoder.ts | 6 +++++-
1 file changed, 5 insertions(+), 1 deletion(-)
diff --git a/app/packages/looker/src/worker/disk-overlay-decoder.ts b/app/packages/looker/src/worker/disk-overlay-decoder.ts
index c3aaea0074..73b5d02b6c 100644
--- a/app/packages/looker/src/worker/disk-overlay-decoder.ts
+++ b/app/packages/looker/src/worker/disk-overlay-decoder.ts
@@ -85,9 +85,13 @@ export const decodeOverlayOnDisk = async (
let source = sources[`${field}.${overlayPathField}`];
if (typeof overlayCollectionProcessingParams !== "undefined") {
+ // example: for detections, we need to access the source from the parent label
+ // like: if field is "prediction_masks", we're trying to get "predictiion_masks.detections[INDEX].mask"
source =
sources[
- `${field}.${overlayCollectionProcessingParams.cls}[${overlayCollectionProcessingParams.idx}].${overlayPathField}`
+ `${field}.${overlayCollectionProcessingParams.cls.toLocaleLowerCase()}[${
+ overlayCollectionProcessingParams.idx
+ }].${overlayPathField}`
];
}
From bc44e3ea5221c1f898ce1d3ae41accbd3c39acc9 Mon Sep 17 00:00:00 2001
From: Sashank Aryal
Date: Tue, 17 Dec 2024 15:31:35 -0600
Subject: [PATCH 071/104] don't get rid of query params if source is defined
---
app/packages/looker/src/worker/disk-overlay-decoder.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/app/packages/looker/src/worker/disk-overlay-decoder.ts b/app/packages/looker/src/worker/disk-overlay-decoder.ts
index 73b5d02b6c..2948831061 100644
--- a/app/packages/looker/src/worker/disk-overlay-decoder.ts
+++ b/app/packages/looker/src/worker/disk-overlay-decoder.ts
@@ -102,7 +102,7 @@ export const decodeOverlayOnDisk = async (
let baseUrl = overlayImageUrl;
// remove query params if not local URL
- if (!urlTokens.at(1)?.startsWith("filepath=")) {
+ if (!urlTokens.at(1)?.startsWith("filepath=") && !source) {
baseUrl = overlayImageUrl.split("?")[0];
}
From d9e5f386b3d03a29716282379716b6b3058d1df4 Mon Sep 17 00:00:00 2001
From: brimoor
Date: Tue, 17 Dec 2024 18:47:19 -0500
Subject: [PATCH 072/104] bump brain version
---
setup.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/setup.py b/setup.py
index 1009d750c3..0b50552ffd 100644
--- a/setup.py
+++ b/setup.py
@@ -74,7 +74,7 @@ def get_version():
"universal-analytics-python3>=1.0.1,<2",
"pydash",
# internal packages
- "fiftyone-brain>=0.18.0,<0.19",
+ "fiftyone-brain>=0.18.2,<0.19",
"fiftyone-db>=0.4,<2.0",
"voxel51-eta>=0.13.0,<0.14",
]
From d872a3bc4b0c2c006d17ab9caa46f57b4e2f879e Mon Sep 17 00:00:00 2001
From: Eric Hofesmann
Date: Fri, 13 Dec 2024 12:46:23 -0500
Subject: [PATCH 073/104] similarity race condition patch
---
fiftyone/utils/torch.py | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/fiftyone/utils/torch.py b/fiftyone/utils/torch.py
index c8390dfc5b..112a9d5fac 100644
--- a/fiftyone/utils/torch.py
+++ b/fiftyone/utils/torch.py
@@ -572,9 +572,9 @@ def __exit__(self, *args):
if self.config.cudnn_benchmark is not None:
torch.backends.cudnn.benchmark = self._benchmark_orig
self._benchmark_orig = None
-
- self._no_grad.__exit__(*args)
- self._no_grad = None
+ if self._no_grad is not None:
+ self._no_grad.__exit__(*args)
+ self._no_grad = None
@property
def media_type(self):
From 4fdcc9caf00853a04cf11e6ff809d2b9007a20ab Mon Sep 17 00:00:00 2001
From: brimoor
Date: Tue, 17 Dec 2024 19:32:39 -0500
Subject: [PATCH 074/104] lint
---
fiftyone/utils/torch.py | 1 +
1 file changed, 1 insertion(+)
diff --git a/fiftyone/utils/torch.py b/fiftyone/utils/torch.py
index 112a9d5fac..88ae1f6f20 100644
--- a/fiftyone/utils/torch.py
+++ b/fiftyone/utils/torch.py
@@ -572,6 +572,7 @@ def __exit__(self, *args):
if self.config.cudnn_benchmark is not None:
torch.backends.cudnn.benchmark = self._benchmark_orig
self._benchmark_orig = None
+
if self._no_grad is not None:
self._no_grad.__exit__(*args)
self._no_grad = None
From 5603258575ef6374beb6199c22d30c7474c896d0 Mon Sep 17 00:00:00 2001
From: imanjra
Date: Tue, 17 Dec 2024 22:42:19 -0500
Subject: [PATCH 075/104] model evaluation load_view bug fixes
---
.../NativeModelEvaluationView/Evaluation.tsx | 10 +++++++++-
.../builtins/panels/model_evaluation/__init__.py | 2 +-
2 files changed, 10 insertions(+), 2 deletions(-)
diff --git a/app/packages/core/src/plugins/SchemaIO/components/NativeModelEvaluationView/Evaluation.tsx b/app/packages/core/src/plugins/SchemaIO/components/NativeModelEvaluationView/Evaluation.tsx
index c3ee377dab..cefbc3f6c6 100644
--- a/app/packages/core/src/plugins/SchemaIO/components/NativeModelEvaluationView/Evaluation.tsx
+++ b/app/packages/core/src/plugins/SchemaIO/components/NativeModelEvaluationView/Evaluation.tsx
@@ -1217,6 +1217,14 @@ export default function Evaluation(props: EvaluationProps) {
].join("
") + "",
},
]}
+ onClick={({ points }) => {
+ const firstPoint = points[0];
+ loadView("matrix", {
+ x: firstPoint.x,
+ y: firstPoint.y,
+ key: compareKey,
+ });
+ }}
layout={{
yaxis: {
autorange: "reversed",
@@ -1598,7 +1606,7 @@ function useActiveFilter(evaluation, compareEvaluation) {
const evalKey = evaluation?.info?.key;
const compareKey = compareEvaluation?.info?.key;
const [stages] = useRecoilState(view);
- if (stages?.length === 1) {
+ if (stages?.length >= 1) {
const stage = stages[0];
const { _cls, kwargs } = stage;
if (_cls.endsWith("FilterLabels")) {
diff --git a/fiftyone/operators/builtins/panels/model_evaluation/__init__.py b/fiftyone/operators/builtins/panels/model_evaluation/__init__.py
index 96684ce080..f9b1b147e9 100644
--- a/fiftyone/operators/builtins/panels/model_evaluation/__init__.py
+++ b/fiftyone/operators/builtins/panels/model_evaluation/__init__.py
@@ -420,7 +420,7 @@ def load_view(self, ctx):
eval_key2 = view_state.get("compareKey", None)
pred_field2 = None
gt_field2 = None
- if eval_key2 is not None:
+ if eval_key2:
info2 = ctx.dataset.get_evaluation_info(eval_key2)
pred_field2 = info2.config.pred_field
if info2.config.gt_field != gt_field:
From 01807ac0cf64c63a1785e9df731362d03a1aea54 Mon Sep 17 00:00:00 2001
From: Sashank Aryal
Date: Wed, 18 Dec 2024 12:23:56 -0600
Subject: [PATCH 076/104] decode png header to know num channels
---
.../looker/src/worker/canvas-decoder.ts | 72 ++++++++++++-------
app/packages/looker/src/worker/painter.ts | 19 ++---
2 files changed, 56 insertions(+), 35 deletions(-)
diff --git a/app/packages/looker/src/worker/canvas-decoder.ts b/app/packages/looker/src/worker/canvas-decoder.ts
index 390ace2a04..fdde3b91cc 100644
--- a/app/packages/looker/src/worker/canvas-decoder.ts
+++ b/app/packages/looker/src/worker/canvas-decoder.ts
@@ -1,45 +1,65 @@
import { OverlayMask } from "../numpy";
/**
- * Checks if the given pixel data is grayscale by sampling a subset of pixels.
- * The function will check at least 500 pixels or 1% of all pixels, whichever is larger.
- * If the image is grayscale, the R, G, and B channels will be equal for all sampled pixels,
- * and the alpha channel will always be 255.
+ * Reads the PNG's image header chunk to determine the color type.
+ * Returns the color type if PNG, otherwise undefined.
*/
-export const isGrayscale = (data: Uint8ClampedArray): boolean => {
- const totalPixels = data.length / 4;
- const checks = Math.max(500, Math.floor(totalPixels * 0.01));
- const step = Math.max(1, Math.floor(totalPixels / checks));
-
- for (let p = 0; p < totalPixels; p += step) {
- const i = p * 4;
- const [r, g, b, a] = [data[i], data[i + 1], data[i + 2], data[i + 3]];
- if (a !== 255 || r !== g || g !== b) {
- return false;
+const getPngcolorType = async (blob: Blob): Promise => {
+ // https://www.w3.org/TR/2003/REC-PNG-20031110/#11IHDR
+
+ // PNG signature is 8 bytes
+ // IHDR (image header): length(4 bytes), chunk type(4 bytes), then data(13 bytes)
+ // data layout of IHDR: width(4), height(4), bit depth(1), color type(1), ...
+ // color type is at offset: 8(signature) + 4(length) + 4(chunk type) + 8(width+height) + 1(bit depth)
+ // = 8 + 4 + 4 + 8 + 1 = 25 (0-based index)
+
+ const header = new Uint8Array(await blob.slice(0, 26).arrayBuffer());
+ const pngSignature = [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a];
+
+ // check PNG signature
+ for (let i = 0; i < pngSignature.length; i++) {
+ if (header[i] !== pngSignature[i]) {
+ // not a PNG
+ return undefined;
}
}
- return true;
+
+ // color type at byte 25 (0-based)
+ const colorType = header[25];
+ return colorType;
};
-/**
- * Decodes a given image source into an OverlayMask using an OffscreenCanvas
- */
-export const decodeWithCanvas = async (blob: ImageBitmapSource) => {
+export const decodeWithCanvas = async (blob: Blob) => {
+ let channels: number = 4;
+
+ if (blob.type === "image/png") {
+ const colorType = await getPngcolorType(blob);
+ if (colorType !== undefined) {
+ // according to PNG specs:
+ // 0: Grayscale => 1 channel
+ // 2: Truecolor (RGB) => (would be 3 channels, but we can safely use 4)
+ // 3: Indexed-color => (palette-based, treat as non-grayscale => 4)
+ // 4: Grayscale+Alpha => Grayscale image (so treat as grayscale => 1)
+ // 6: RGBA => non-grayscale => 4
+ if (colorType === 0 || colorType === 4) {
+ channels = 1;
+ } else {
+ channels = 4;
+ }
+ }
+ }
+ // if not PNG, use 4 channels
+
const imageBitmap = await createImageBitmap(blob);
- const width = imageBitmap.width;
- const height = imageBitmap.height;
+ const { width, height } = imageBitmap;
const canvas = new OffscreenCanvas(width, height);
- const ctx = canvas.getContext("2d");
-
+ const ctx = canvas.getContext("2d")!;
ctx.drawImage(imageBitmap, 0, 0);
imageBitmap.close();
const imageData = ctx.getImageData(0, 0, width, height);
- // for nongrayscale images, channel is guaranteed to be 4 (RGBA)
- const channels = isGrayscale(imageData.data) ? 1 : 4;
-
if (channels === 1) {
// get rid of the G, B, and A channels, new buffer will be 1/4 the size
const data = new Uint8ClampedArray(width * height);
diff --git a/app/packages/looker/src/worker/painter.ts b/app/packages/looker/src/worker/painter.ts
index 6730d90cac..1e8675aa5a 100644
--- a/app/packages/looker/src/worker/painter.ts
+++ b/app/packages/looker/src/worker/painter.ts
@@ -113,6 +113,7 @@ export const PainterFactory = (requestColor) => ({
}
}
+ const numChannels = label.mask.data.channels;
const overlay = new Uint32Array(label.mask.image);
const targets = new ARRAY_TYPES[label.mask.data.arrayType](
label.mask.data.buffer
@@ -120,16 +121,16 @@ export const PainterFactory = (requestColor) => ({
const bitColor = get32BitColor(color);
if (label.mask_path) {
- // putImageData results in an UInt8ClampedArray (for both grayscale or RGB masks),
- // where each pixel is represented by 4 bytes (RGBA)
- // it's packed like: [R, G, B, A, R, G, B, A, ...]
- // use first channel info to determine if the pixel is in the mask
- // skip second (G), third (B) and fourth (A) channels
- for (let i = 0; i < targets.length; i += 4) {
+ // putImageData results in an UInt8ClampedArray,
+ // if image is grayscale, it'll be packed as:
+ // [I, I, I, I, I...], where I is the intensity value
+ // or else it'll be packed as:
+ // [R, G, B, A, R, G, B, A...]
+ // for non-grayscale masks, we can check every nth byte,
+ // where n = numChannels, to check for whether or not the pixel is part of the mask
+ for (let i = 0; i < targets.length; i += numChannels) {
if (targets[i]) {
- // overlay image is a Uint32Array, where each pixel is represented by 4 bytes (RGBA)
- // so we need to divide by 4 to get the correct index to assign 32 bit color
- const overlayIndex = i / 4;
+ const overlayIndex = i / numChannels;
overlay[overlayIndex] = bitColor;
}
}
From b86906ac09eb1370d1ea37ae40aa24beec33ddf6 Mon Sep 17 00:00:00 2001
From: Sashank Aryal
Date: Wed, 18 Dec 2024 12:27:52 -0600
Subject: [PATCH 077/104] use const png signature
---
app/packages/looker/src/worker/canvas-decoder.ts | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/app/packages/looker/src/worker/canvas-decoder.ts b/app/packages/looker/src/worker/canvas-decoder.ts
index fdde3b91cc..56749a6d2e 100644
--- a/app/packages/looker/src/worker/canvas-decoder.ts
+++ b/app/packages/looker/src/worker/canvas-decoder.ts
@@ -1,5 +1,6 @@
import { OverlayMask } from "../numpy";
+const PNG_SIGNATURE = [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a];
/**
* Reads the PNG's image header chunk to determine the color type.
* Returns the color type if PNG, otherwise undefined.
@@ -14,11 +15,10 @@ const getPngcolorType = async (blob: Blob): Promise => {
// = 8 + 4 + 4 + 8 + 1 = 25 (0-based index)
const header = new Uint8Array(await blob.slice(0, 26).arrayBuffer());
- const pngSignature = [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a];
// check PNG signature
- for (let i = 0; i < pngSignature.length; i++) {
- if (header[i] !== pngSignature[i]) {
+ for (let i = 0; i < PNG_SIGNATURE.length; i++) {
+ if (header[i] !== PNG_SIGNATURE[i]) {
// not a PNG
return undefined;
}
From b4acf0fb52fc5385b653e8d5e1c109ff0841564d Mon Sep 17 00:00:00 2001
From: Sashank Aryal
Date: Wed, 18 Dec 2024 13:09:35 -0600
Subject: [PATCH 078/104] remove invalid tests
---
.../looker/src/worker/canvas-decoder.test.ts | 43 -------------------
1 file changed, 43 deletions(-)
delete mode 100644 app/packages/looker/src/worker/canvas-decoder.test.ts
diff --git a/app/packages/looker/src/worker/canvas-decoder.test.ts b/app/packages/looker/src/worker/canvas-decoder.test.ts
deleted file mode 100644
index 427b3c6131..0000000000
--- a/app/packages/looker/src/worker/canvas-decoder.test.ts
+++ /dev/null
@@ -1,43 +0,0 @@
-import { describe, expect, it } from "vitest";
-import { isGrayscale } from "./canvas-decoder";
-
-const createData = (
- pixels: Array<[number, number, number, number]>
-): Uint8ClampedArray => {
- return new Uint8ClampedArray(pixels.flat());
-};
-
-describe("isGrayscale", () => {
- it("should return true for a perfectly grayscale image", () => {
- const data = createData(Array(100).fill([100, 100, 100, 255]));
- expect(isGrayscale(data)).toBe(true);
- });
-
- it("should return false if alpha is not 255", () => {
- const data = createData([
- [100, 100, 100, 255],
- [100, 100, 100, 254],
- ...Array(98).fill([100, 100, 100, 255]),
- ]);
- expect(isGrayscale(data)).toBe(false);
- });
-
- it("should return false if any pixel is not grayscale", () => {
- const data = createData([
- [100, 100, 100, 255],
- [100, 101, 100, 255],
- ...Array(98).fill([100, 100, 100, 255]),
- ]);
- expect(isGrayscale(data)).toBe(false);
- });
-
- it("should detect a non-grayscale pixel placed deep enough to ensure at least 1% of pixels are checked", () => {
- // large image: 100,000 pixels. 1% of 100,000 is 1,000.
- // the function will check at least 1,000 pixels.
- // place a non-grayscale pixel after 800 pixels.
- const pixels = Array(100000).fill([50, 50, 50, 255]);
- pixels[800] = [50, 51, 50, 255]; // this is within the first 1% of pixels
- const data = createData(pixels);
- expect(isGrayscale(data)).toBe(false);
- });
-});
From 94c3fe998a19ffb53496144eb05f2d56823b0118 Mon Sep 17 00:00:00 2001
From: Benjamin Kane
Date: Wed, 18 Dec 2024 15:06:25 -0500
Subject: [PATCH 079/104] Reset to initial buffers in video looker (#5293)
* reset buffers to initial state, handle single frame
* assert grid tagging with locators
---
app/packages/looker/src/elements/video.ts | 11 ++++++-----
app/packages/looker/src/lookers/video.ts | 2 ++
e2e-pw/src/oss/specs/grid-tagging.spec.ts | 17 +++++++++++------
3 files changed, 19 insertions(+), 11 deletions(-)
diff --git a/app/packages/looker/src/elements/video.ts b/app/packages/looker/src/elements/video.ts
index 0954e47e01..8f5bfcfcd1 100644
--- a/app/packages/looker/src/elements/video.ts
+++ b/app/packages/looker/src/elements/video.ts
@@ -65,17 +65,18 @@ export class LoaderBar extends BaseElement {
}: Readonly) {
const shown =
!error && hovering && (waitingForVideo || buffering || waitingToStream);
+
+ if (shown === this.shown) {
+ return this.element;
+ }
+
const start = lockedToSupport ? support[0] : 1;
const end = lockedToSupport
? support[1]
: getFrameNumber(duration, duration, frameRate);
- if (shown === this.shown || start === end) {
- return this.element;
- }
this.shown = shown;
-
- if (this.shown) {
+ if (this.shown && start !== end) {
this.element.style.display = "block";
} else {
this.element.style.display = "none";
diff --git a/app/packages/looker/src/lookers/video.ts b/app/packages/looker/src/lookers/video.ts
index 2fe24f7fab..af4405e3ba 100644
--- a/app/packages/looker/src/lookers/video.ts
+++ b/app/packages/looker/src/lookers/video.ts
@@ -52,6 +52,7 @@ export class VideoLooker extends AbstractLooker {
if (LOOKER_WITH_READER === this) {
clearReader();
LOOKER_WITH_READER = null;
+ this.state.buffers = this.initialBuffers(this.state.config);
}
super.detach();
}
@@ -361,6 +362,7 @@ export class VideoLooker extends AbstractLooker {
if (LOOKER_WITH_READER === this) {
if (this.state.config.thumbnail && !this.state.hovering) {
clearReader();
+ this.state.buffers = this.initialBuffers(this.state.config);
LOOKER_WITH_READER = null;
}
}
diff --git a/e2e-pw/src/oss/specs/grid-tagging.spec.ts b/e2e-pw/src/oss/specs/grid-tagging.spec.ts
index 33abd664f8..2fd65c70fa 100644
--- a/e2e-pw/src/oss/specs/grid-tagging.spec.ts
+++ b/e2e-pw/src/oss/specs/grid-tagging.spec.ts
@@ -44,9 +44,10 @@ test("grid tagging", async ({ fiftyoneLoader, grid, page, sidebar }) => {
await sidebar.clickFieldCheckbox("filepath");
await sidebar.clickFieldCheckbox("tags");
await grid.scrollBottom();
- await expect(await grid.locator).toHaveScreenshot("grid-untagged.png", {
- animations: "allow",
- });
+ for (let i = 31; i <= 54; i++) {
+ const locator = grid.locator.getByText(`/tmp/${i}.png`);
+ await expect(locator).toBeVisible();
+ }
await grid.run(async () => {
await grid.actionsRow.toggleTagSamplesOrLabels();
@@ -54,7 +55,11 @@ test("grid tagging", async ({ fiftyoneLoader, grid, page, sidebar }) => {
await grid.tagger.addNewTag("sample", "grid-test");
});
- await expect(await grid.locator).toHaveScreenshot("grid-tagged.png", {
- animations: "allow",
- });
+ for (let i = 31; i <= 54; i++) {
+ const locator = grid.locator.getByText(`/tmp/${i}.png`);
+ await expect(locator).toBeVisible();
+ await expect(
+ locator.locator("..").getByTestId("tag-tags-grid-test")
+ ).toBeVisible();
+ }
});
From cf3d4a2ec4d62679ed8ee347167705f9e2645b8c Mon Sep 17 00:00:00 2001
From: manivoxel51
Date: Wed, 18 Dec 2024 13:05:12 -0800
Subject: [PATCH 080/104] Fixes an issue where spliting the panel space wrongly
triggers on_unload
---
app/packages/operators/src/CustomPanel.tsx | 3 +++
app/packages/operators/src/useCustomPanelHooks.ts | 8 --------
2 files changed, 3 insertions(+), 8 deletions(-)
diff --git a/app/packages/operators/src/CustomPanel.tsx b/app/packages/operators/src/CustomPanel.tsx
index 14d3548ca0..3674374027 100644
--- a/app/packages/operators/src/CustomPanel.tsx
+++ b/app/packages/operators/src/CustomPanel.tsx
@@ -14,12 +14,14 @@ import { useActivePanelEventsCount } from "./hooks";
import { Property } from "./types";
import { CustomPanelProps, useCustomPanelHooks } from "./useCustomPanelHooks";
import { useTrackEvent } from "@fiftyone/analytics";
+import usePanelEvent from "./usePanelEvent";
export function CustomPanel(props: CustomPanelProps) {
const { panelId, dimensions, panelName, panelLabel, isModalPanel } = props;
const { height, width } = dimensions?.bounds || {};
const { count } = useActivePanelEventsCount(panelId);
const [_, setLoading] = usePanelLoading(panelId);
+ const triggerPanelEvent = usePanelEvent();
const {
handlePanelStateChange,
@@ -36,6 +38,7 @@ export function CustomPanel(props: CustomPanelProps) {
setPanelCloseEffect(() => {
clearUseKeyStores(panelId);
trackEvent("close_panel", { panel: panelName });
+ triggerPanelEvent(panelId, { operator: props.onUnLoad });
});
}, []);
diff --git a/app/packages/operators/src/useCustomPanelHooks.ts b/app/packages/operators/src/useCustomPanelHooks.ts
index 7363234138..60c685255e 100644
--- a/app/packages/operators/src/useCustomPanelHooks.ts
+++ b/app/packages/operators/src/useCustomPanelHooks.ts
@@ -165,14 +165,6 @@ export function useCustomPanelHooks(props: CustomPanelProps): CustomPanelHooks {
triggerPanelEvent,
]);
- useEffect(() => {
- return () => {
- if (props.onUnLoad) {
- triggerPanelEvent(panelId, { operator: props.onUnLoad });
- }
- };
- }, []); // eslint-disable-line react-hooks/exhaustive-deps
-
const handlePanelStateChangeOpDebounced = useMemo(() => {
return debounce(
(state, onChange, panelId) => {
From c82bb8106953fd42bb37e31ec716cee5dfe5f8ff Mon Sep 17 00:00:00 2001
From: Benjamin Kane
Date: Wed, 18 Dec 2024 16:46:14 -0500
Subject: [PATCH 081/104] Fix cached looker font sizes (#5287)
* update font size when pulling from looker cache
* add fontSize to attach
---
app/packages/core/src/components/Grid/Grid.tsx | 5 ++++-
.../core/src/components/ImageContainerHeader.tsx | 2 +-
app/packages/looker/src/lookers/abstract.ts | 10 ++++++++--
3 files changed, 13 insertions(+), 4 deletions(-)
diff --git a/app/packages/core/src/components/Grid/Grid.tsx b/app/packages/core/src/components/Grid/Grid.tsx
index 303146c09a..8948d8b1ff 100644
--- a/app/packages/core/src/components/Grid/Grid.tsx
+++ b/app/packages/core/src/components/Grid/Grid.tsx
@@ -72,7 +72,10 @@ function Grid() {
get: (next) => page(next),
render: (id, element, dimensions, zooming) => {
if (lookerStore.has(id.description)) {
- lookerStore.get(id.description)?.attach(element, dimensions);
+ lookerStore
+ .get(id.description)
+ ?.attach(element, dimensions, getFontSize());
+
return;
}
diff --git a/app/packages/core/src/components/ImageContainerHeader.tsx b/app/packages/core/src/components/ImageContainerHeader.tsx
index ed6e73b339..1c7c804523 100644
--- a/app/packages/core/src/components/ImageContainerHeader.tsx
+++ b/app/packages/core/src/components/ImageContainerHeader.tsx
@@ -3,7 +3,7 @@ import * as fos from "@fiftyone/state";
import { isGroup as isGroupAtom } from "@fiftyone/state";
import { Apps, ImageAspectRatio } from "@mui/icons-material";
import Color from "color";
-import { Suspense, useMemo } from "react";
+import React, { Suspense, useMemo } from "react";
import {
constSelector,
useRecoilCallback,
diff --git a/app/packages/looker/src/lookers/abstract.ts b/app/packages/looker/src/lookers/abstract.ts
index 1043c4d7b8..08991adba9 100644
--- a/app/packages/looker/src/lookers/abstract.ts
+++ b/app/packages/looker/src/lookers/abstract.ts
@@ -469,13 +469,18 @@ export abstract class AbstractLooker<
/**
* Attaches the instance to the provided HTMLElement and adds event listeners
*/
- attach(element: HTMLElement | string, dimensions?: Dimensions): void {
+ attach(
+ element: HTMLElement | string,
+ dimensions?: Dimensions,
+ fontSize?: number
+ ): void {
if (typeof element === "string") {
element = document.getElementById(element);
}
if (element === this.lookerElement.element.parentElement) {
- this.state.disabled && this.updater({ disabled: false });
+ this.state.disabled &&
+ this.updater({ disabled: false, options: { fontSize } });
return;
}
@@ -491,6 +496,7 @@ export abstract class AbstractLooker<
this.updater({
windowBBox: dimensions ? [0, 0, ...dimensions] : getElementBBox(element),
disabled: false,
+ options: { fontSize },
});
element.appendChild(this.lookerElement.element);
!dimensions && this.resizeObserver.observe(element);
From 75d31dc983367a93502a435f808112f81835f991 Mon Sep 17 00:00:00 2001
From: Sashank Aryal
Date: Wed, 18 Dec 2024 21:15:47 -0600
Subject: [PATCH 082/104] remove erroneous base url extraction
---
app/packages/looker/src/worker/disk-overlay-decoder.ts | 10 +---------
1 file changed, 1 insertion(+), 9 deletions(-)
diff --git a/app/packages/looker/src/worker/disk-overlay-decoder.ts b/app/packages/looker/src/worker/disk-overlay-decoder.ts
index 2948831061..32177e079c 100644
--- a/app/packages/looker/src/worker/disk-overlay-decoder.ts
+++ b/app/packages/looker/src/worker/disk-overlay-decoder.ts
@@ -97,19 +97,11 @@ export const decodeOverlayOnDisk = async (
// convert absolute file path to a URL that we can "fetch" from
const overlayImageUrl = getSampleSrc(source || label[overlayPathField]);
- const urlTokens = overlayImageUrl.split("?");
-
- let baseUrl = overlayImageUrl;
-
- // remove query params if not local URL
- if (!urlTokens.at(1)?.startsWith("filepath=") && !source) {
- baseUrl = overlayImageUrl.split("?")[0];
- }
let overlayImageBlob: Blob;
try {
const overlayImageFetchResponse = await enqueueFetch({
- url: baseUrl,
+ url: overlayImageUrl,
options: { priority: "low" },
});
overlayImageBlob = await overlayImageFetchResponse.blob();
From b92e7472670d57ff147fa26e649340020ca4f512 Mon Sep 17 00:00:00 2001
From: imanjra
Date: Thu, 19 Dec 2024 12:49:31 -0500
Subject: [PATCH 083/104] fix load_evaluation bug with serialized_info
---
fiftyone/operators/builtins/panels/model_evaluation/__init__.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/fiftyone/operators/builtins/panels/model_evaluation/__init__.py b/fiftyone/operators/builtins/panels/model_evaluation/__init__.py
index c83e8972cf..e75052ed4b 100644
--- a/fiftyone/operators/builtins/panels/model_evaluation/__init__.py
+++ b/fiftyone/operators/builtins/panels/model_evaluation/__init__.py
@@ -308,13 +308,13 @@ def load_evaluation(self, ctx):
if evaluation_data is None:
info = ctx.dataset.get_evaluation_info(computed_eval_key)
evaluation_type = info.config.type
+ serialized_info = info.serialize()
if evaluation_type not in SUPPORTED_EVALUATION_TYPES:
ctx.panel.set_data(
f"evaluation_{computed_eval_key}_error",
{"error": "unsupported", "info": serialized_info},
)
return
- serialized_info = info.serialize()
gt_field = info.config.gt_field
mask_targets = (
self.get_mask_targets(ctx.dataset, gt_field)
From 79349afe49ffe8495bcc797e1e1f64d000ecb4d8 Mon Sep 17 00:00:00 2001
From: imanjra
Date: Tue, 10 Dec 2024 12:00:21 -0500
Subject: [PATCH 084/104] model evaluation panel permission ux tweaks
---
.../NativeModelEvaluationView/Evaluate.tsx | 20 ++++++++---
.../NativeModelEvaluationView/Evaluation.tsx | 34 +++++++++++--------
.../NativeModelEvaluationView/Overview.tsx | 2 +-
.../NativeModelEvaluationView/Status.tsx | 13 +++++--
4 files changed, 47 insertions(+), 22 deletions(-)
diff --git a/app/packages/core/src/plugins/SchemaIO/components/NativeModelEvaluationView/Evaluate.tsx b/app/packages/core/src/plugins/SchemaIO/components/NativeModelEvaluationView/Evaluate.tsx
index c9aed0c979..69022c3b5d 100644
--- a/app/packages/core/src/plugins/SchemaIO/components/NativeModelEvaluationView/Evaluate.tsx
+++ b/app/packages/core/src/plugins/SchemaIO/components/NativeModelEvaluationView/Evaluate.tsx
@@ -1,13 +1,25 @@
import { MuiButton } from "@fiftyone/components";
import { Add } from "@mui/icons-material";
+import { Box } from "@mui/material";
import React from "react";
export default function Evaluate(props: EvaluateProps) {
- const { onEvaluate } = props;
+ const { onEvaluate, permissions } = props;
+ const canEvaluate = permissions.can_evaluate;
return (
- } variant="contained">
- Evaluate Model
-
+
+ }
+ variant="contained"
+ disabled={!canEvaluate}
+ >
+ Evaluate Model
+
+
);
}
diff --git a/app/packages/core/src/plugins/SchemaIO/components/NativeModelEvaluationView/Evaluation.tsx b/app/packages/core/src/plugins/SchemaIO/components/NativeModelEvaluationView/Evaluation.tsx
index eb5b401efc..37340cc928 100644
--- a/app/packages/core/src/plugins/SchemaIO/components/NativeModelEvaluationView/Evaluation.tsx
+++ b/app/packages/core/src/plugins/SchemaIO/components/NativeModelEvaluationView/Evaluation.tsx
@@ -611,20 +611,26 @@ export default function Evaluation(props: EvaluationProps) {
Evaluation notes
- {can_edit_note && (
-
- {
- setEditNoteState((note) => ({ ...note, open: true }));
- }}
- >
-
-
-
- )}
+
+ {
+ setEditNoteState((note) => ({ ...note, open: true }));
+ }}
+ disabled={!can_edit_note}
+ >
+
+
+
diff --git a/app/packages/core/src/plugins/SchemaIO/components/NativeModelEvaluationView/Overview.tsx b/app/packages/core/src/plugins/SchemaIO/components/NativeModelEvaluationView/Overview.tsx
index b0aa8f99d8..643ec14b7d 100644
--- a/app/packages/core/src/plugins/SchemaIO/components/NativeModelEvaluationView/Overview.tsx
+++ b/app/packages/core/src/plugins/SchemaIO/components/NativeModelEvaluationView/Overview.tsx
@@ -97,7 +97,7 @@ function EvaluationCard(props: EvaluationCardProps) {
}
/>
)}
- {status && }
+ {status && }
{note && }
diff --git a/app/packages/core/src/plugins/SchemaIO/components/NativeModelEvaluationView/Status.tsx b/app/packages/core/src/plugins/SchemaIO/components/NativeModelEvaluationView/Status.tsx
index 346cbb3245..bc3eb52635 100644
--- a/app/packages/core/src/plugins/SchemaIO/components/NativeModelEvaluationView/Status.tsx
+++ b/app/packages/core/src/plugins/SchemaIO/components/NativeModelEvaluationView/Status.tsx
@@ -4,10 +4,10 @@ import React from "react";
import { useTriggerEvent } from "./utils";
export default function Status(props: StatusProps) {
- const { status, canEdit, setStatusEvent } = props;
+ const { status, canEdit, readOnly, setStatusEvent } = props;
const triggerEvent = useTriggerEvent();
- if (canEdit) {
+ if (!readOnly) {
return (