truefoundry 0.3.3__py3-none-any.whl → 0.4.0.dev1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of truefoundry might be problematic. Click here for more details.
- truefoundry/cli/__main__.py +3 -17
- truefoundry/common/__init__.py +0 -0
- truefoundry/common/request_utils.py +56 -0
- truefoundry/deploy/cli/cli.py +1 -1
- truefoundry/deploy/cli/util.py +3 -1
- truefoundry/deploy/lib/auth/credential_provider.py +2 -12
- truefoundry/deploy/lib/clients/servicefoundry_client.py +0 -9
- truefoundry/deploy/lib/exceptions.py +1 -6
- truefoundry/deploy/lib/session.py +1 -16
- truefoundry/langchain/truefoundry_chat.py +1 -1
- truefoundry/langchain/truefoundry_embeddings.py +1 -1
- truefoundry/langchain/truefoundry_llm.py +1 -1
- truefoundry/langchain/utils.py +0 -41
- truefoundry/ml/__init__.py +46 -6
- truefoundry/ml/artifact/__init__.py +0 -0
- truefoundry/ml/artifact/truefoundry_artifact_repo.py +1120 -0
- truefoundry/ml/autogen/__init__.py +0 -0
- truefoundry/ml/autogen/client/__init__.py +373 -0
- truefoundry/ml/autogen/client/api/__init__.py +16 -0
- truefoundry/ml/autogen/client/api/auth_api.py +184 -0
- truefoundry/ml/autogen/client/api/deprecated_api.py +605 -0
- truefoundry/ml/autogen/client/api/experiments_api.py +2109 -0
- truefoundry/ml/autogen/client/api/health_api.py +299 -0
- truefoundry/ml/autogen/client/api/metrics_api.py +371 -0
- truefoundry/ml/autogen/client/api/mlfoundry_artifacts_api.py +7213 -0
- truefoundry/ml/autogen/client/api/python_deployment_config_api.py +201 -0
- truefoundry/ml/autogen/client/api/run_artifacts_api.py +231 -0
- truefoundry/ml/autogen/client/api/runs_api.py +2919 -0
- truefoundry/ml/autogen/client/api_client.py +822 -0
- truefoundry/ml/autogen/client/api_response.py +30 -0
- truefoundry/ml/autogen/client/configuration.py +489 -0
- truefoundry/ml/autogen/client/exceptions.py +161 -0
- truefoundry/ml/autogen/client/models/__init__.py +344 -0
- truefoundry/ml/autogen/client/models/add_custom_metrics_to_model_version_request_dto.py +69 -0
- truefoundry/ml/autogen/client/models/add_features_to_model_version_request_dto.py +83 -0
- truefoundry/ml/autogen/client/models/agent.py +125 -0
- truefoundry/ml/autogen/client/models/agent_app.py +118 -0
- truefoundry/ml/autogen/client/models/agent_open_api_tool.py +143 -0
- truefoundry/ml/autogen/client/models/agent_open_api_tool_with_fqn.py +144 -0
- truefoundry/ml/autogen/client/models/agent_with_fqn.py +127 -0
- truefoundry/ml/autogen/client/models/artifact_dto.py +115 -0
- truefoundry/ml/autogen/client/models/artifact_response_dto.py +75 -0
- truefoundry/ml/autogen/client/models/artifact_type.py +39 -0
- truefoundry/ml/autogen/client/models/artifact_version_dto.py +141 -0
- truefoundry/ml/autogen/client/models/artifact_version_response_dto.py +77 -0
- truefoundry/ml/autogen/client/models/artifact_version_status.py +35 -0
- truefoundry/ml/autogen/client/models/assistant_message.py +89 -0
- truefoundry/ml/autogen/client/models/authorize_user_for_model_request_dto.py +69 -0
- truefoundry/ml/autogen/client/models/authorize_user_for_model_version_request_dto.py +69 -0
- truefoundry/ml/autogen/client/models/backfill_default_storage_integration_id_request_dto.py +67 -0
- truefoundry/ml/autogen/client/models/blob_storage_reference.py +93 -0
- truefoundry/ml/autogen/client/models/body_get_search_runs_get.py +72 -0
- truefoundry/ml/autogen/client/models/chat_prompt.py +156 -0
- truefoundry/ml/autogen/client/models/chat_prompt_messages_inner.py +171 -0
- truefoundry/ml/autogen/client/models/columns_dto.py +73 -0
- truefoundry/ml/autogen/client/models/content.py +153 -0
- truefoundry/ml/autogen/client/models/content1.py +153 -0
- truefoundry/ml/autogen/client/models/content2.py +174 -0
- truefoundry/ml/autogen/client/models/content2_any_of_inner.py +150 -0
- truefoundry/ml/autogen/client/models/create_artifact_request_dto.py +74 -0
- truefoundry/ml/autogen/client/models/create_artifact_response_dto.py +66 -0
- truefoundry/ml/autogen/client/models/create_artifact_version_request_dto.py +74 -0
- truefoundry/ml/autogen/client/models/create_artifact_version_response_dto.py +66 -0
- truefoundry/ml/autogen/client/models/create_dataset_request_dto.py +76 -0
- truefoundry/ml/autogen/client/models/create_experiment_request_dto.py +94 -0
- truefoundry/ml/autogen/client/models/create_experiment_response_dto.py +67 -0
- truefoundry/ml/autogen/client/models/create_model_version_request_dto.py +95 -0
- truefoundry/ml/autogen/client/models/create_multi_part_upload_for_dataset_request_dto.py +73 -0
- truefoundry/ml/autogen/client/models/create_multi_part_upload_for_dataset_response_dto.py +79 -0
- truefoundry/ml/autogen/client/models/create_multi_part_upload_request_dto.py +73 -0
- truefoundry/ml/autogen/client/models/create_python_deployment_config_request_dto.py +72 -0
- truefoundry/ml/autogen/client/models/create_python_deployment_config_response_dto.py +68 -0
- truefoundry/ml/autogen/client/models/create_run_request_dto.py +97 -0
- truefoundry/ml/autogen/client/models/create_run_response_dto.py +76 -0
- truefoundry/ml/autogen/client/models/dataset_dto.py +112 -0
- truefoundry/ml/autogen/client/models/dataset_response_dto.py +75 -0
- truefoundry/ml/autogen/client/models/delete_artifact_versions_request_dto.py +65 -0
- truefoundry/ml/autogen/client/models/delete_dataset_request_dto.py +74 -0
- truefoundry/ml/autogen/client/models/delete_model_version_request_dto.py +65 -0
- truefoundry/ml/autogen/client/models/delete_run_request.py +65 -0
- truefoundry/ml/autogen/client/models/delete_tag_request_dto.py +68 -0
- truefoundry/ml/autogen/client/models/experiment_dto.py +127 -0
- truefoundry/ml/autogen/client/models/experiment_id_request_dto.py +67 -0
- truefoundry/ml/autogen/client/models/experiment_response_dto.py +75 -0
- truefoundry/ml/autogen/client/models/experiment_tag_dto.py +69 -0
- truefoundry/ml/autogen/client/models/feature_dto.py +68 -0
- truefoundry/ml/autogen/client/models/feature_value_type.py +35 -0
- truefoundry/ml/autogen/client/models/file_info_dto.py +76 -0
- truefoundry/ml/autogen/client/models/finalize_artifact_version_request_dto.py +101 -0
- truefoundry/ml/autogen/client/models/get_experiment_response_dto.py +88 -0
- truefoundry/ml/autogen/client/models/get_latest_run_log_response_dto.py +76 -0
- truefoundry/ml/autogen/client/models/get_metric_history_response.py +79 -0
- truefoundry/ml/autogen/client/models/get_signed_url_for_dataset_write_request_dto.py +68 -0
- truefoundry/ml/autogen/client/models/get_signed_urls_for_artifact_version_read_request_dto.py +68 -0
- truefoundry/ml/autogen/client/models/get_signed_urls_for_artifact_version_read_response_dto.py +81 -0
- truefoundry/ml/autogen/client/models/get_signed_urls_for_artifact_version_write_request_dto.py +69 -0
- truefoundry/ml/autogen/client/models/get_signed_urls_for_artifact_version_write_response_dto.py +83 -0
- truefoundry/ml/autogen/client/models/get_signed_urls_for_dataset_read_request_dto.py +68 -0
- truefoundry/ml/autogen/client/models/get_signed_urls_for_dataset_read_response_dto.py +81 -0
- truefoundry/ml/autogen/client/models/get_signed_urls_for_dataset_write_response_dto.py +81 -0
- truefoundry/ml/autogen/client/models/get_tenant_id_response_dto.py +74 -0
- truefoundry/ml/autogen/client/models/http_validation_error.py +82 -0
- truefoundry/ml/autogen/client/models/image_content_part.py +87 -0
- truefoundry/ml/autogen/client/models/image_url.py +75 -0
- truefoundry/ml/autogen/client/models/internal_metadata.py +180 -0
- truefoundry/ml/autogen/client/models/latest_run_log_dto.py +78 -0
- truefoundry/ml/autogen/client/models/list_artifact_versions_request_dto.py +107 -0
- truefoundry/ml/autogen/client/models/list_artifact_versions_response_dto.py +87 -0
- truefoundry/ml/autogen/client/models/list_artifacts_request_dto.py +96 -0
- truefoundry/ml/autogen/client/models/list_artifacts_response_dto.py +86 -0
- truefoundry/ml/autogen/client/models/list_colums_response_dto.py +75 -0
- truefoundry/ml/autogen/client/models/list_datasets_request_dto.py +78 -0
- truefoundry/ml/autogen/client/models/list_datasets_response_dto.py +86 -0
- truefoundry/ml/autogen/client/models/list_experiments_response_dto.py +86 -0
- truefoundry/ml/autogen/client/models/list_files_for_artifact_version_request_dto.py +76 -0
- truefoundry/ml/autogen/client/models/list_files_for_artifact_versions_response_dto.py +82 -0
- truefoundry/ml/autogen/client/models/list_files_for_dataset_request_dto.py +76 -0
- truefoundry/ml/autogen/client/models/list_files_for_dataset_response_dto.py +82 -0
- truefoundry/ml/autogen/client/models/list_latest_run_logs_response_dto.py +82 -0
- truefoundry/ml/autogen/client/models/list_metric_history_request_dto.py +69 -0
- truefoundry/ml/autogen/client/models/list_metric_history_response_dto.py +84 -0
- truefoundry/ml/autogen/client/models/list_model_version_response_dto.py +87 -0
- truefoundry/ml/autogen/client/models/list_model_versions_request_dto.py +93 -0
- truefoundry/ml/autogen/client/models/list_models_request_dto.py +89 -0
- truefoundry/ml/autogen/client/models/list_models_response_dto.py +84 -0
- truefoundry/ml/autogen/client/models/list_run_artifacts_response_dto.py +84 -0
- truefoundry/ml/autogen/client/models/list_run_logs_response_dto.py +82 -0
- truefoundry/ml/autogen/client/models/list_seed_experiments_response_dto.py +81 -0
- truefoundry/ml/autogen/client/models/log_batch_request_dto.py +106 -0
- truefoundry/ml/autogen/client/models/log_metric_request_dto.py +80 -0
- truefoundry/ml/autogen/client/models/log_param_request_dto.py +76 -0
- truefoundry/ml/autogen/client/models/method.py +37 -0
- truefoundry/ml/autogen/client/models/metric_collection_dto.py +82 -0
- truefoundry/ml/autogen/client/models/metric_dto.py +76 -0
- truefoundry/ml/autogen/client/models/mime_type.py +37 -0
- truefoundry/ml/autogen/client/models/model_configuration.py +103 -0
- truefoundry/ml/autogen/client/models/model_dto.py +122 -0
- truefoundry/ml/autogen/client/models/model_response_dto.py +75 -0
- truefoundry/ml/autogen/client/models/model_schema_dto.py +85 -0
- truefoundry/ml/autogen/client/models/model_version_dto.py +163 -0
- truefoundry/ml/autogen/client/models/model_version_response_dto.py +75 -0
- truefoundry/ml/autogen/client/models/multi_part_upload_dto.py +107 -0
- truefoundry/ml/autogen/client/models/multi_part_upload_response_dto.py +79 -0
- truefoundry/ml/autogen/client/models/multi_part_upload_storage_provider.py +34 -0
- truefoundry/ml/autogen/client/models/notify_artifact_version_failure_dto.py +65 -0
- truefoundry/ml/autogen/client/models/openapi_spec.py +152 -0
- truefoundry/ml/autogen/client/models/param_dto.py +66 -0
- truefoundry/ml/autogen/client/models/parameters.py +84 -0
- truefoundry/ml/autogen/client/models/prediction_type.py +34 -0
- truefoundry/ml/autogen/client/models/resolve_agent_app_response_dto.py +75 -0
- truefoundry/ml/autogen/client/models/restore_run_request_dto.py +65 -0
- truefoundry/ml/autogen/client/models/run_data_dto.py +104 -0
- truefoundry/ml/autogen/client/models/run_dto.py +84 -0
- truefoundry/ml/autogen/client/models/run_info_dto.py +105 -0
- truefoundry/ml/autogen/client/models/run_log_dto.py +90 -0
- truefoundry/ml/autogen/client/models/run_log_input_dto.py +80 -0
- truefoundry/ml/autogen/client/models/run_response_dto.py +75 -0
- truefoundry/ml/autogen/client/models/run_tag_dto.py +66 -0
- truefoundry/ml/autogen/client/models/search_runs_request_dto.py +94 -0
- truefoundry/ml/autogen/client/models/search_runs_response_dto.py +84 -0
- truefoundry/ml/autogen/client/models/set_experiment_tag_request_dto.py +73 -0
- truefoundry/ml/autogen/client/models/set_tag_request_dto.py +76 -0
- truefoundry/ml/autogen/client/models/signed_url_dto.py +69 -0
- truefoundry/ml/autogen/client/models/stop.py +152 -0
- truefoundry/ml/autogen/client/models/store_run_logs_request_dto.py +83 -0
- truefoundry/ml/autogen/client/models/system_message.py +89 -0
- truefoundry/ml/autogen/client/models/text.py +153 -0
- truefoundry/ml/autogen/client/models/text_content_part.py +84 -0
- truefoundry/ml/autogen/client/models/update_artifact_version_request_dto.py +74 -0
- truefoundry/ml/autogen/client/models/update_dataset_request_dto.py +74 -0
- truefoundry/ml/autogen/client/models/update_experiment_request_dto.py +74 -0
- truefoundry/ml/autogen/client/models/update_model_version_request_dto.py +93 -0
- truefoundry/ml/autogen/client/models/update_run_request_dto.py +78 -0
- truefoundry/ml/autogen/client/models/update_run_response_dto.py +76 -0
- truefoundry/ml/autogen/client/models/url.py +153 -0
- truefoundry/ml/autogen/client/models/user_message.py +89 -0
- truefoundry/ml/autogen/client/models/validation_error.py +87 -0
- truefoundry/ml/autogen/client/models/validation_error_loc_inner.py +154 -0
- truefoundry/ml/autogen/client/rest.py +426 -0
- truefoundry/ml/autogen/client_README.md +322 -0
- truefoundry/ml/cli/__init__.py +0 -0
- truefoundry/ml/cli/cli.py +18 -0
- truefoundry/ml/cli/commands/__init__.py +3 -0
- truefoundry/ml/cli/commands/download.py +87 -0
- truefoundry/ml/constants.py +84 -0
- truefoundry/ml/enums.py +70 -0
- truefoundry/ml/env_vars.py +13 -0
- truefoundry/ml/exceptions.py +8 -0
- truefoundry/ml/git_info.py +60 -0
- truefoundry/ml/internal_namespace.py +52 -0
- truefoundry/ml/log_types/__init__.py +4 -0
- truefoundry/ml/log_types/artifacts/artifact.py +427 -0
- truefoundry/ml/log_types/artifacts/constants.py +33 -0
- truefoundry/ml/log_types/artifacts/dataset.py +383 -0
- truefoundry/ml/log_types/artifacts/general_artifact.py +110 -0
- truefoundry/ml/log_types/artifacts/model.py +628 -0
- truefoundry/ml/log_types/artifacts/model_extras.py +48 -0
- truefoundry/ml/log_types/artifacts/utils.py +161 -0
- truefoundry/ml/log_types/image/__init__.py +3 -0
- truefoundry/ml/log_types/image/constants.py +8 -0
- truefoundry/ml/log_types/image/image.py +358 -0
- truefoundry/ml/log_types/image/image_normalizer.py +101 -0
- truefoundry/ml/log_types/image/types.py +68 -0
- truefoundry/ml/log_types/plot.py +281 -0
- truefoundry/ml/log_types/pydantic_base.py +10 -0
- truefoundry/ml/log_types/utils.py +12 -0
- truefoundry/ml/logger.py +17 -0
- truefoundry/ml/login.py +241 -0
- truefoundry/ml/mlfoundry_api.py +1620 -0
- truefoundry/ml/mlfoundry_run.py +1238 -0
- truefoundry/ml/run_utils.py +102 -0
- truefoundry/ml/services/__init__.py +0 -0
- truefoundry/ml/services/auth_service.py +109 -0
- truefoundry/ml/services/entities.py +108 -0
- truefoundry/ml/services/servicefoundry_service.py +35 -0
- truefoundry/ml/services/utils.py +122 -0
- truefoundry/ml/session.py +271 -0
- truefoundry/ml/validation_utils.py +346 -0
- truefoundry/pydantic_v1.py +5 -1
- {truefoundry-0.3.3.dist-info → truefoundry-0.4.0.dev1.dist-info}/METADATA +18 -11
- truefoundry-0.4.0.dev1.dist-info/RECORD +342 -0
- truefoundry-0.3.3.dist-info/RECORD +0 -136
- /truefoundry/{python_deploy_codegen.py → deploy/python_deploy_codegen.py} +0 -0
- {truefoundry-0.3.3.dist-info → truefoundry-0.4.0.dev1.dist-info}/WHEEL +0 -0
- {truefoundry-0.3.3.dist-info → truefoundry-0.4.0.dev1.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import logging
|
|
3
|
+
import os
|
|
4
|
+
import posixpath
|
|
5
|
+
import shutil
|
|
6
|
+
import tempfile
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any, Dict, Optional, Sequence, Tuple, Union
|
|
9
|
+
|
|
10
|
+
from truefoundry.ml.exceptions import MlFoundryException
|
|
11
|
+
from truefoundry.ml.log_types.artifacts.constants import DESCRIPTION_MAX_LENGTH
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger("truefoundry.ml")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _copy_tree(src_path, dest_path, symlinks=False, ignore_dangling_symlinks=False):
|
|
17
|
+
os.makedirs(dest_path, exist_ok=True)
|
|
18
|
+
for item in os.listdir(src_path):
|
|
19
|
+
src = os.path.join(src_path, item)
|
|
20
|
+
dest = os.path.join(dest_path, item)
|
|
21
|
+
if os.path.isdir(src):
|
|
22
|
+
_copy_tree(
|
|
23
|
+
src,
|
|
24
|
+
dest,
|
|
25
|
+
symlinks=symlinks,
|
|
26
|
+
ignore_dangling_symlinks=ignore_dangling_symlinks,
|
|
27
|
+
)
|
|
28
|
+
else:
|
|
29
|
+
shutil.copy2(src, dest, follow_symlinks=True)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def is_destination_path_dirlike(dest_path) -> bool:
|
|
33
|
+
if not dest_path:
|
|
34
|
+
return True
|
|
35
|
+
|
|
36
|
+
if dest_path.endswith(os.sep) or dest_path.endswith(posixpath.sep):
|
|
37
|
+
return True
|
|
38
|
+
|
|
39
|
+
if os.path.exists(dest_path) and os.path.isdir(dest_path):
|
|
40
|
+
return True
|
|
41
|
+
|
|
42
|
+
return False
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _copy_additional_files(
|
|
46
|
+
root_dir: str,
|
|
47
|
+
files_dir: str, # relative to root dir e.g. "files/"
|
|
48
|
+
model_dir: Optional[str], # relative to files_dir e.g "model/"
|
|
49
|
+
additional_files: Sequence[Tuple[Union[str, Path], Optional[str]]],
|
|
50
|
+
ignore_model_dir_dest_conflict: bool = False,
|
|
51
|
+
):
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
File copying examples:
|
|
55
|
+
# non ambiguous
|
|
56
|
+
# a.txt -> /tmp/ result /tmp/a.txt
|
|
57
|
+
# a.txt -> /tmp/a/ result /tmp/a/a.txt
|
|
58
|
+
# a.txt -> /tmp/a/b/c/d.txt result /tmp/a/b/c/d.txt
|
|
59
|
+
# .gitignore -> /tmp/.gitignore result /tmp/.gitignore
|
|
60
|
+
|
|
61
|
+
# ambiguous but destination directory exists
|
|
62
|
+
# a.txt -> /tmp result /tmp/a.txt
|
|
63
|
+
# a.txt -> /tmp/a (and /tmp/a/ exists) result /tmp/a/a.txt
|
|
64
|
+
|
|
65
|
+
# ambiguous - when the destination can't be reliably distinguished as a directory
|
|
66
|
+
# a -> /tmp/a result /tmp/a
|
|
67
|
+
# a -> /tmp/b result /tmp/b
|
|
68
|
+
# a -> /tmp/a.txt result /tmp/a.txt
|
|
69
|
+
# .gitignore -> /tmp/.gitinclude result /tmp/.gitinclude
|
|
70
|
+
# a.txt -> /tmp/a result /tmp/a
|
|
71
|
+
"""
|
|
72
|
+
for src_path, dest_path in additional_files:
|
|
73
|
+
src_path = str(src_path)
|
|
74
|
+
if not os.path.exists(src_path):
|
|
75
|
+
raise MlFoundryException(
|
|
76
|
+
f"Source path {src_path!r} in `additional_files` does not exist."
|
|
77
|
+
)
|
|
78
|
+
dest_path = dest_path or ""
|
|
79
|
+
normalized_path = os.path.normpath(dest_path)
|
|
80
|
+
if dest_path.endswith(os.sep) or dest_path.endswith(posixpath.sep):
|
|
81
|
+
normalized_path += os.sep
|
|
82
|
+
dest_path = normalized_path.lstrip(os.sep)
|
|
83
|
+
|
|
84
|
+
if (
|
|
85
|
+
model_dir
|
|
86
|
+
and dest_path.startswith(model_dir)
|
|
87
|
+
and not ignore_model_dir_dest_conflict
|
|
88
|
+
):
|
|
89
|
+
logger.warning(
|
|
90
|
+
f"Destination path {dest_path!r} in `additional_files` conflicts with "
|
|
91
|
+
f"reserved path {model_dir!r}/ which is being used to store the model. "
|
|
92
|
+
f"This might cause errors"
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
files_abs_dir = os.path.join(root_dir, files_dir)
|
|
96
|
+
dest_abs_path = os.path.join(files_abs_dir, dest_path)
|
|
97
|
+
|
|
98
|
+
if os.path.isfile(src_path):
|
|
99
|
+
_src = src_path
|
|
100
|
+
if is_destination_path_dirlike(dest_abs_path):
|
|
101
|
+
os.makedirs(dest_abs_path, exist_ok=True)
|
|
102
|
+
_dst = os.path.relpath(
|
|
103
|
+
os.path.join(dest_abs_path, os.path.basename(_src)), files_abs_dir
|
|
104
|
+
)
|
|
105
|
+
else:
|
|
106
|
+
os.makedirs(os.path.dirname(dest_abs_path), exist_ok=True)
|
|
107
|
+
_dst = os.path.relpath(dest_abs_path, files_abs_dir)
|
|
108
|
+
logger.info(f"Adding file {_src} as /{_dst}")
|
|
109
|
+
shutil.copy2(src_path, dest_abs_path, follow_symlinks=True)
|
|
110
|
+
elif os.path.isdir(src_path):
|
|
111
|
+
os.makedirs(dest_abs_path, exist_ok=True)
|
|
112
|
+
_src = src_path.rstrip("/")
|
|
113
|
+
_dst = os.path.relpath(dest_abs_path, files_abs_dir).rstrip("/")
|
|
114
|
+
logger.info(f"Adding contents of {_src}/ to /{_dst}/")
|
|
115
|
+
_copy_tree(
|
|
116
|
+
src_path=src_path,
|
|
117
|
+
dest_path=dest_abs_path,
|
|
118
|
+
symlinks=True,
|
|
119
|
+
ignore_dangling_symlinks=False,
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _validate_description(description: Optional[str]):
|
|
124
|
+
if description is not None:
|
|
125
|
+
if not isinstance(description, str):
|
|
126
|
+
raise MlFoundryException(
|
|
127
|
+
"`description` must be either `None` or type `str`"
|
|
128
|
+
)
|
|
129
|
+
if len(description) > DESCRIPTION_MAX_LENGTH:
|
|
130
|
+
raise MlFoundryException(
|
|
131
|
+
f"`description` cannot be longer than {DESCRIPTION_MAX_LENGTH} characters"
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _validate_artifact_metadata(metadata: Dict[str, Any]):
|
|
136
|
+
if not isinstance(metadata, dict):
|
|
137
|
+
raise MlFoundryException("`metadata` must be json serializable dict")
|
|
138
|
+
try:
|
|
139
|
+
json.dumps(metadata)
|
|
140
|
+
except ValueError as ve:
|
|
141
|
+
raise MlFoundryException("`metadata` must be json serializable dict") from ve
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def calculate_local_directory_size(
|
|
145
|
+
directory: tempfile.TemporaryDirectory, # type: ignore[type-arg]
|
|
146
|
+
):
|
|
147
|
+
"""
|
|
148
|
+
Tells about the size of the artifact
|
|
149
|
+
|
|
150
|
+
Args:
|
|
151
|
+
directory (str): directory path
|
|
152
|
+
|
|
153
|
+
Returns:
|
|
154
|
+
total size of the artifact
|
|
155
|
+
"""
|
|
156
|
+
total_size = 0
|
|
157
|
+
for path, _dirs, files in os.walk(directory.name):
|
|
158
|
+
for f in files:
|
|
159
|
+
file_path = os.path.join(path, f)
|
|
160
|
+
total_size += os.stat(file_path).st_size
|
|
161
|
+
return total_size
|
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
import posixpath
|
|
4
|
+
import shutil
|
|
5
|
+
import tempfile
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
|
|
11
|
+
from truefoundry.ml.autogen.client import ( # type: ignore[attr-defined]
|
|
12
|
+
ArtifactType,
|
|
13
|
+
)
|
|
14
|
+
from truefoundry.ml.exceptions import MlFoundryException
|
|
15
|
+
from truefoundry.ml.log_types.artifacts.artifact import (
|
|
16
|
+
ArtifactVersionInternalMetadata,
|
|
17
|
+
_log_artifact_version_helper,
|
|
18
|
+
)
|
|
19
|
+
from truefoundry.ml.log_types.artifacts.constants import (
|
|
20
|
+
FILES_DIR,
|
|
21
|
+
INTERNAL_METADATA_PATH,
|
|
22
|
+
)
|
|
23
|
+
from truefoundry.ml.log_types.image.constants import (
|
|
24
|
+
DEFAULT_IMAGE_FORMAT,
|
|
25
|
+
IMAGE_KEY_REGEX,
|
|
26
|
+
IMAGE_METADATA_FILE_NAME,
|
|
27
|
+
MISSING_PILLOW_PACKAGE_MESSAGE,
|
|
28
|
+
)
|
|
29
|
+
from truefoundry.ml.log_types.image.image_normalizer import normalize_image
|
|
30
|
+
from truefoundry.ml.log_types.image.types import BoundingBoxGroups, ClassGroups
|
|
31
|
+
from truefoundry.ml.log_types.pydantic_base import PydanticBase
|
|
32
|
+
from truefoundry.ml.run_utils import get_module
|
|
33
|
+
|
|
34
|
+
if TYPE_CHECKING:
|
|
35
|
+
# noinspection PyUnresolvedReferences
|
|
36
|
+
import PIL.Image
|
|
37
|
+
from numpy.typing import NDArray
|
|
38
|
+
|
|
39
|
+
from truefoundry.ml.mlfoundry_run import MlFoundryRun
|
|
40
|
+
|
|
41
|
+
ImageLikeNDArray = Union[
|
|
42
|
+
"NDArray[np.int8]",
|
|
43
|
+
"NDArray[np.uint8]",
|
|
44
|
+
"NDArray[np.float32]",
|
|
45
|
+
"NDArray[np.float64]",
|
|
46
|
+
"NDArray[np.bool_]",
|
|
47
|
+
]
|
|
48
|
+
DataOrPathType = Union[str, Path, ImageLikeNDArray, "PIL.Image.Image"]
|
|
49
|
+
ClassGroupsType = Dict[str, Union[str, List[str]]]
|
|
50
|
+
BBoxGrouptype = Dict[str, List[Dict[str, Any]]]
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def validate_key_name(key: str):
|
|
54
|
+
if not key or not IMAGE_KEY_REGEX.match(key):
|
|
55
|
+
raise MlFoundryException(
|
|
56
|
+
f"Invalid run image key: {key} should only contain alphanumeric, hyphen or underscore"
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class ImageVersionInternalMetadata(ArtifactVersionInternalMetadata):
|
|
61
|
+
image_file: str
|
|
62
|
+
image_metadata_file: str
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class Image:
|
|
66
|
+
def __init__(
|
|
67
|
+
self,
|
|
68
|
+
data_or_path: DataOrPathType,
|
|
69
|
+
caption: Optional[str] = None,
|
|
70
|
+
class_groups: Optional[ClassGroupsType] = None,
|
|
71
|
+
bbox_groups: Optional[BBoxGrouptype] = None,
|
|
72
|
+
):
|
|
73
|
+
"""Represent and log image using this class in `mlfoundry`.
|
|
74
|
+
|
|
75
|
+
You can initialize `truefoundry.ml.Image` by either by using a local path
|
|
76
|
+
or you can use a numpy array / PIL.Image object.
|
|
77
|
+
|
|
78
|
+
If you are using numpy array, we only support the following data types,
|
|
79
|
+
- bool
|
|
80
|
+
- integer [0 - 255]
|
|
81
|
+
- unsigned integer [0 - 255]
|
|
82
|
+
- float [0.0 - 1.0]
|
|
83
|
+
|
|
84
|
+
Any out of range value will be clipped.
|
|
85
|
+
|
|
86
|
+
As for array shape/dim, we follow the following structures,
|
|
87
|
+
- H x W (Grayscale)
|
|
88
|
+
- H x W x 1 (Grayscale)
|
|
89
|
+
- H x W x 3 (an RGB channel order is assumed)
|
|
90
|
+
- H x W x 4 (an RGBA channel order is assumed)
|
|
91
|
+
|
|
92
|
+
`PIL` package is required to log images. To install the `PIL` package,
|
|
93
|
+
run `pip install pillow`.
|
|
94
|
+
|
|
95
|
+
We can also log class names and bounding boxes associated with the image.
|
|
96
|
+
Class names and bounding boxes should be always grouped under `actuals` or
|
|
97
|
+
`predictions`. For example, if we have an image where the ground truth class
|
|
98
|
+
is "cat" and predicted class is "dog", we can represent it like,
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
from truefoundry.ml import Image
|
|
102
|
+
|
|
103
|
+
Image(
|
|
104
|
+
data_or_path=imarray,
|
|
105
|
+
class_groups={"actuals": "dog", "predictions": "cat"}
|
|
106
|
+
)
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
You can define a bounding box using the following dictionary structure,
|
|
110
|
+
```python
|
|
111
|
+
{
|
|
112
|
+
"position": {"min_x": 15, "min_y": 5, "max_x": 20, "max_y": 30}, # required, defines the position of the bounding box
|
|
113
|
+
# (min_x, min_y) defines the top left and
|
|
114
|
+
# (max_x, max_y) defines the bottom right corner of the box.
|
|
115
|
+
"class_name": "dog", # required, the class name of the bounding box
|
|
116
|
+
"caption": "dog", # optional, the caption of the bounding box.
|
|
117
|
+
# If not passed, the class name is set as caption.
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
data_or_path (Union[str, Path, "numpy.ndarray", "PIL.Image.Image"]):
|
|
123
|
+
Either the local path or the image object (Numpy array or PIL Image).
|
|
124
|
+
caption (Optional[str], optional): A string caption or label for the image.
|
|
125
|
+
class_groups (Optional[Dict[str, Union[str, List[str]]]], optional):
|
|
126
|
+
Class names associated with the image. Expects class name(s) grouped by
|
|
127
|
+
`predictions` or `actuals`.
|
|
128
|
+
bbox_groups (Optional[Dict[str, List[Dict]]], optional): Bounding boxes
|
|
129
|
+
associated with the image. Expects bounding boxes grouped by `predictions`
|
|
130
|
+
or `actuals`.
|
|
131
|
+
|
|
132
|
+
Examples:
|
|
133
|
+
### Logging images with caption and class names
|
|
134
|
+
|
|
135
|
+
```python
|
|
136
|
+
from truefoundry.ml import get_client, Image
|
|
137
|
+
import numpy as np
|
|
138
|
+
|
|
139
|
+
client = get_client()
|
|
140
|
+
run = client.create_run(
|
|
141
|
+
ml_repo="my-classification-project",
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
imarray = np.random.randint(low=0, high=256, size=(100, 100, 3))
|
|
145
|
+
|
|
146
|
+
images_to_log = {
|
|
147
|
+
"logged-image-array": Image(
|
|
148
|
+
data_or_path=imarray,
|
|
149
|
+
caption="testing image logging",
|
|
150
|
+
class_groups={"actuals": "dog", "predictions": "cat"},
|
|
151
|
+
),
|
|
152
|
+
}
|
|
153
|
+
run.log_images(images_to_log, step=1)
|
|
154
|
+
|
|
155
|
+
run.end()
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### Logging images for a multi-label classification problem
|
|
159
|
+
|
|
160
|
+
```python
|
|
161
|
+
from truefoundry.ml import Image
|
|
162
|
+
|
|
163
|
+
images_to_log = {
|
|
164
|
+
"logged-image-array": Image(
|
|
165
|
+
data_or_path=imarray,
|
|
166
|
+
caption="testing image logging",
|
|
167
|
+
class_groups={"actuals": ["dog", "human"], "predictions": ["cat", "human"]},
|
|
168
|
+
),
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
run.log_images(images_to_log, step=1)
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### Logging images with bounding boxes
|
|
175
|
+
|
|
176
|
+
```python
|
|
177
|
+
from truefoundry.ml import Image
|
|
178
|
+
|
|
179
|
+
images_to_log = {
|
|
180
|
+
"logged-image-array": Image(
|
|
181
|
+
data_or_path=imarray,
|
|
182
|
+
caption="testing image logging",
|
|
183
|
+
bbox_groups={
|
|
184
|
+
"predictions": [
|
|
185
|
+
{
|
|
186
|
+
"position": {"min_x": 5, "min_y": 5, "max_x": 20, "max_y": 30},
|
|
187
|
+
"class_name": "cat",
|
|
188
|
+
}
|
|
189
|
+
],
|
|
190
|
+
"actuals": [
|
|
191
|
+
{
|
|
192
|
+
"position": {"min_x": 15, "min_y": 5, "max_x": 20, "max_y": 30},
|
|
193
|
+
"class_name": "dog",
|
|
194
|
+
"caption": "dog",
|
|
195
|
+
}
|
|
196
|
+
],
|
|
197
|
+
},
|
|
198
|
+
),
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
run.log_images(images_to_log, step=1)
|
|
202
|
+
```
|
|
203
|
+
"""
|
|
204
|
+
|
|
205
|
+
self._caption: Optional[str] = None
|
|
206
|
+
self._class_groups: Optional[ClassGroups] = None
|
|
207
|
+
self._bbox_groups: Optional[BoundingBoxGroups] = None
|
|
208
|
+
|
|
209
|
+
self._image: Optional["PIL.Image.Image"] = None
|
|
210
|
+
self._image_artifact_path = None
|
|
211
|
+
self._local_image_path: Optional[str] = None
|
|
212
|
+
|
|
213
|
+
self._init_image(data_or_path)
|
|
214
|
+
self._init_metadata(
|
|
215
|
+
caption=caption, class_groups=class_groups, bbox_groups=bbox_groups
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
@property
|
|
219
|
+
def image(self) -> "PIL.Image.Image":
|
|
220
|
+
if self._image is None:
|
|
221
|
+
raise MlFoundryException("Image is not initialized")
|
|
222
|
+
return self._image
|
|
223
|
+
|
|
224
|
+
@property
|
|
225
|
+
def caption(self) -> Optional[str]:
|
|
226
|
+
return self._caption
|
|
227
|
+
|
|
228
|
+
@property
|
|
229
|
+
def class_groups(self) -> Optional[ClassGroups]:
|
|
230
|
+
return self._class_groups
|
|
231
|
+
|
|
232
|
+
@property
|
|
233
|
+
def bbox_groups(self) -> Optional[BoundingBoxGroups]:
|
|
234
|
+
return self._bbox_groups
|
|
235
|
+
|
|
236
|
+
def save_image_to_local_dir(self, local_dir) -> str:
|
|
237
|
+
if self._local_image_path is None:
|
|
238
|
+
image_file_name = self._serialize_and_save_image_as_artifact(local_dir)
|
|
239
|
+
else:
|
|
240
|
+
image_file_name = self._copy_local_image_as_artifact(local_dir)
|
|
241
|
+
|
|
242
|
+
return image_file_name
|
|
243
|
+
|
|
244
|
+
def _serialize_and_save_image_as_artifact(self, local_dir: str) -> str:
|
|
245
|
+
file_name = f"image.{DEFAULT_IMAGE_FORMAT}"
|
|
246
|
+
local_path = os.path.join(local_dir, file_name)
|
|
247
|
+
self.image.save(local_path)
|
|
248
|
+
|
|
249
|
+
return file_name
|
|
250
|
+
|
|
251
|
+
def _copy_local_image_as_artifact(self, local_dir: str) -> str:
|
|
252
|
+
assert self._local_image_path is not None
|
|
253
|
+
file_name = os.path.basename(self._local_image_path)
|
|
254
|
+
new_local_image_path = os.path.join(local_dir, file_name)
|
|
255
|
+
shutil.copy2(self._local_image_path, new_local_image_path)
|
|
256
|
+
return file_name
|
|
257
|
+
|
|
258
|
+
def _init_image(self, data_or_path: DataOrPathType):
|
|
259
|
+
pil_image_module = get_module(
|
|
260
|
+
module_name="PIL.Image",
|
|
261
|
+
required=True,
|
|
262
|
+
error_message=MISSING_PILLOW_PACKAGE_MESSAGE,
|
|
263
|
+
)
|
|
264
|
+
if isinstance(data_or_path, (str, Path)):
|
|
265
|
+
self._local_image_path = os.path.abspath(data_or_path)
|
|
266
|
+
with pil_image_module.open(data_or_path) as image:
|
|
267
|
+
image.load()
|
|
268
|
+
self._image = image
|
|
269
|
+
else:
|
|
270
|
+
self._image = normalize_image(data_or_path)
|
|
271
|
+
|
|
272
|
+
def _init_metadata(
|
|
273
|
+
self,
|
|
274
|
+
caption: Optional[str],
|
|
275
|
+
class_groups: Optional[ClassGroupsType],
|
|
276
|
+
bbox_groups: Optional[Dict[str, Any]],
|
|
277
|
+
):
|
|
278
|
+
if caption is not None:
|
|
279
|
+
self._caption = str(caption)
|
|
280
|
+
|
|
281
|
+
if class_groups:
|
|
282
|
+
self._class_groups = ClassGroups.parse_obj(class_groups)
|
|
283
|
+
|
|
284
|
+
if bbox_groups:
|
|
285
|
+
self._bbox_groups = BoundingBoxGroups.parse_obj(bbox_groups)
|
|
286
|
+
|
|
287
|
+
def _to_dict(self) -> Dict[str, Any]:
|
|
288
|
+
dict_ = {}
|
|
289
|
+
dict_["caption"] = self._caption
|
|
290
|
+
dict_["class_groups"] = (
|
|
291
|
+
self.class_groups.to_dict() if self.class_groups is not None else None
|
|
292
|
+
)
|
|
293
|
+
dict_["bbox_groups"] = (
|
|
294
|
+
self.bbox_groups.to_dict() if self.bbox_groups is not None else None
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
return dict_
|
|
298
|
+
|
|
299
|
+
def save(
|
|
300
|
+
self,
|
|
301
|
+
run: "MlFoundryRun",
|
|
302
|
+
key: str,
|
|
303
|
+
step: int = 0,
|
|
304
|
+
):
|
|
305
|
+
validate_key_name(key)
|
|
306
|
+
|
|
307
|
+
# creating temp dir, files directory, .truefoundry directory
|
|
308
|
+
temp_dir = tempfile.TemporaryDirectory(prefix="truefoundry-")
|
|
309
|
+
|
|
310
|
+
local_internal_metadata_path = os.path.join(
|
|
311
|
+
temp_dir.name, INTERNAL_METADATA_PATH
|
|
312
|
+
)
|
|
313
|
+
os.makedirs(os.path.dirname(local_internal_metadata_path), exist_ok=True)
|
|
314
|
+
|
|
315
|
+
local_files_dir = os.path.join(temp_dir.name, FILES_DIR)
|
|
316
|
+
os.makedirs(local_files_dir, exist_ok=True)
|
|
317
|
+
|
|
318
|
+
try:
|
|
319
|
+
# saving the image file
|
|
320
|
+
image_file_name = self.save_image_to_local_dir(local_files_dir)
|
|
321
|
+
|
|
322
|
+
# saving the image_metadata
|
|
323
|
+
local_image_metadata_file_path = os.path.join(
|
|
324
|
+
local_files_dir, IMAGE_METADATA_FILE_NAME
|
|
325
|
+
)
|
|
326
|
+
with open(local_image_metadata_file_path, "w") as fp:
|
|
327
|
+
fp.write(ImageRunLogType(value=self._to_dict()).json())
|
|
328
|
+
|
|
329
|
+
# creating and saving the internal_metadata file
|
|
330
|
+
internal_metadata = ImageVersionInternalMetadata(
|
|
331
|
+
files_dir=FILES_DIR,
|
|
332
|
+
# joining with posixpath for avoiding backslash in case logged from windows machine
|
|
333
|
+
image_file=posixpath.join(FILES_DIR, image_file_name),
|
|
334
|
+
image_metadata_file=posixpath.join(FILES_DIR, IMAGE_METADATA_FILE_NAME),
|
|
335
|
+
)
|
|
336
|
+
with open(local_internal_metadata_path, "w") as f:
|
|
337
|
+
json.dump(internal_metadata.dict(), f)
|
|
338
|
+
|
|
339
|
+
except Exception as e:
|
|
340
|
+
temp_dir.cleanup()
|
|
341
|
+
raise MlFoundryException("Failed to log Image") from e
|
|
342
|
+
|
|
343
|
+
_log_artifact_version_helper(
|
|
344
|
+
run=run,
|
|
345
|
+
name=key,
|
|
346
|
+
artifact_type=ArtifactType.IMAGE,
|
|
347
|
+
artifact_dir=temp_dir,
|
|
348
|
+
internal_metadata=internal_metadata,
|
|
349
|
+
step=step,
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
class ImageRunLogType(PydanticBase):
|
|
354
|
+
value: Dict[str, Any]
|
|
355
|
+
|
|
356
|
+
@staticmethod
|
|
357
|
+
def get_log_type() -> str:
|
|
358
|
+
return "image"
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# The logic is copied from
|
|
2
|
+
# https://github.com/truefoundry/mlfoundry-server/blob/c2334ace8f35322cd5e50c275b8df08327688c01/mlflow/tracking/client.py#L1188
|
|
3
|
+
from typing import TYPE_CHECKING, Union
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
|
|
7
|
+
from truefoundry.ml.exceptions import MlFoundryException
|
|
8
|
+
from truefoundry.ml.log_types.image.constants import MISSING_PILLOW_PACKAGE_MESSAGE
|
|
9
|
+
from truefoundry.ml.logger import logger
|
|
10
|
+
from truefoundry.ml.run_utils import get_module
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
import PIL.Image
|
|
14
|
+
from numpy.typing import NDArray
|
|
15
|
+
|
|
16
|
+
ImageLikeNDArray = Union[
|
|
17
|
+
"NDArray[np.int8]",
|
|
18
|
+
"NDArray[np.uint8]",
|
|
19
|
+
"NDArray[np.float32]",
|
|
20
|
+
"NDArray[np.float64]",
|
|
21
|
+
"NDArray[np.bool_]",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _normalize_to_uint8(x):
|
|
26
|
+
# Based on: https://github.com/matplotlib/matplotlib/blob/06567e021f21be046b6d6dcf00380c1cb9adaf3c/lib/matplotlib/image.py#L684
|
|
27
|
+
|
|
28
|
+
is_int = np.issubdtype(x.dtype, np.integer)
|
|
29
|
+
low = 0
|
|
30
|
+
high = 255 if is_int else 1
|
|
31
|
+
if x.min() < low or x.max() > high:
|
|
32
|
+
msg = (
|
|
33
|
+
"Out-of-range values are detected. "
|
|
34
|
+
"Clipping array (dtype: '{}') to [{}, {}]".format(x.dtype, low, high)
|
|
35
|
+
)
|
|
36
|
+
logger.warning(msg)
|
|
37
|
+
x = np.clip(x, low, high)
|
|
38
|
+
|
|
39
|
+
# float or bool
|
|
40
|
+
if not is_int:
|
|
41
|
+
x = x * 255
|
|
42
|
+
|
|
43
|
+
return x.astype(np.uint8)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _convert_numpy_to_pil_image(image: ImageLikeNDArray) -> "PIL.Image.Image":
|
|
47
|
+
pil_image_module = get_module(
|
|
48
|
+
module_name="PIL.Image",
|
|
49
|
+
required=True,
|
|
50
|
+
error_message=MISSING_PILLOW_PACKAGE_MESSAGE,
|
|
51
|
+
)
|
|
52
|
+
valid_data_types = {
|
|
53
|
+
"b": "bool",
|
|
54
|
+
"i": "signed integer",
|
|
55
|
+
"u": "unsigned integer",
|
|
56
|
+
"f": "floating",
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if image.dtype.kind not in valid_data_types.keys():
|
|
60
|
+
raise TypeError(
|
|
61
|
+
"Invalid array data type: '{}'. Must be one of {}".format(
|
|
62
|
+
image.dtype, list(valid_data_types.values())
|
|
63
|
+
)
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
if image.ndim not in [2, 3]:
|
|
67
|
+
raise ValueError(
|
|
68
|
+
"`image` must be a 2D or 3D array but got a {}D array".format(image.ndim)
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
if (image.ndim == 3) and (image.shape[2] not in [1, 3, 4]):
|
|
72
|
+
raise ValueError(
|
|
73
|
+
"Invalid channel length: {}. Must be one of [1, 3, 4]".format(
|
|
74
|
+
image.shape[2]
|
|
75
|
+
)
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
# squeeze a 3D grayscale image since `Image.fromarray` doesn't accept it.
|
|
79
|
+
if image.ndim == 3 and image.shape[2] == 1:
|
|
80
|
+
image = image[:, :, 0]
|
|
81
|
+
|
|
82
|
+
image = _normalize_to_uint8(image)
|
|
83
|
+
|
|
84
|
+
return pil_image_module.fromarray(image)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def normalize_image(
|
|
88
|
+
image: Union["PIL.Image.Image", ImageLikeNDArray],
|
|
89
|
+
) -> "PIL.Image.Image":
|
|
90
|
+
pil_image_module = get_module(
|
|
91
|
+
module_name="PIL.Image",
|
|
92
|
+
required=True,
|
|
93
|
+
error_message=MISSING_PILLOW_PACKAGE_MESSAGE,
|
|
94
|
+
)
|
|
95
|
+
if isinstance(image, pil_image_module.Image):
|
|
96
|
+
return image
|
|
97
|
+
if isinstance(image, np.ndarray):
|
|
98
|
+
return _convert_numpy_to_pil_image(image)
|
|
99
|
+
raise MlFoundryException(
|
|
100
|
+
f"image should be of type PIL Image/np.ndarray got type {type(image)}"
|
|
101
|
+
)
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import enum
|
|
2
|
+
from typing import Dict, List, Optional
|
|
3
|
+
|
|
4
|
+
from truefoundry.pydantic_v1 import BaseModel, NonEmptyStr, root_validator
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Group(enum.Enum):
|
|
8
|
+
ACTUALS = "actuals"
|
|
9
|
+
PREDICTIONS = "predictions"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Position(BaseModel):
|
|
13
|
+
min_x: float
|
|
14
|
+
min_y: float
|
|
15
|
+
max_x: float
|
|
16
|
+
max_y: float
|
|
17
|
+
|
|
18
|
+
class Config:
|
|
19
|
+
allow_mutation = False
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class BoundingBox(BaseModel):
|
|
23
|
+
position: Position
|
|
24
|
+
class_name: NonEmptyStr
|
|
25
|
+
caption: Optional[str] = None
|
|
26
|
+
|
|
27
|
+
class Config:
|
|
28
|
+
allow_mutation = False
|
|
29
|
+
|
|
30
|
+
@root_validator(pre=True)
|
|
31
|
+
def set_caption_if_not_passed(cls, values):
|
|
32
|
+
if not values.get("caption"):
|
|
33
|
+
values["caption"] = values.get("class_name")
|
|
34
|
+
|
|
35
|
+
return values
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class BoundingBoxGroups(BaseModel):
|
|
39
|
+
__root__: Dict[Group, List[BoundingBox]]
|
|
40
|
+
|
|
41
|
+
class Config:
|
|
42
|
+
allow_mutation = False
|
|
43
|
+
use_enum_values = True
|
|
44
|
+
|
|
45
|
+
def to_dict(self):
|
|
46
|
+
return self.dict()["__root__"]
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class ClassGroups(BaseModel):
|
|
50
|
+
__root__: Dict[Group, List[NonEmptyStr]]
|
|
51
|
+
|
|
52
|
+
class Config:
|
|
53
|
+
allow_mutation = False
|
|
54
|
+
use_enum_values = True
|
|
55
|
+
|
|
56
|
+
@root_validator(pre=True)
|
|
57
|
+
def transform_class_to_classes(cls, values):
|
|
58
|
+
values = values["__root__"]
|
|
59
|
+
processed_values = {}
|
|
60
|
+
for group, classes in values.items():
|
|
61
|
+
if isinstance(classes, str):
|
|
62
|
+
processed_values[group] = [classes]
|
|
63
|
+
else:
|
|
64
|
+
processed_values[group] = classes
|
|
65
|
+
return {"__root__": processed_values}
|
|
66
|
+
|
|
67
|
+
def to_dict(self):
|
|
68
|
+
return self.dict()["__root__"]
|