scitex 2.3.0__py3-none-any.whl → 2.4.1__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.
- scitex/ai/classification/reporters/reporter_utils/_Plotter.py +1 -1
- scitex/ai/plt/__init__.py +2 -2
- scitex/ai/plt/{_plot_conf_mat.py → _stx_conf_mat.py} +3 -3
- scitex/config/PriorityConfig.py +195 -0
- scitex/config/__init__.py +24 -0
- scitex/io/_save.py +125 -34
- scitex/io/_save_modules/_image.py +37 -20
- scitex/plt/__init__.py +470 -17
- scitex/plt/_subplots/_AxisWrapper.py +98 -50
- scitex/plt/_subplots/_AxisWrapperMixins/_MatplotlibPlotMixin.py +559 -124
- scitex/plt/_subplots/_AxisWrapperMixins/_SeabornMixin.py +49 -8
- scitex/plt/_subplots/_SubplotsWrapper.py +76 -91
- scitex/plt/_subplots/_export_as_csv.py +127 -58
- scitex/plt/_subplots/_export_as_csv_formatters/__init__.py +25 -16
- scitex/plt/_subplots/_export_as_csv_formatters/_format_contourf.py +54 -0
- scitex/plt/_subplots/_export_as_csv_formatters/_format_hexbin.py +41 -0
- scitex/plt/_subplots/_export_as_csv_formatters/_format_hist2d.py +41 -0
- scitex/plt/_subplots/_export_as_csv_formatters/_format_imshow.py +59 -47
- scitex/plt/_subplots/_export_as_csv_formatters/_format_matshow.py +42 -0
- scitex/plt/_subplots/_export_as_csv_formatters/_format_pie.py +42 -0
- scitex/plt/_subplots/_export_as_csv_formatters/_format_plot.py +72 -35
- scitex/plt/_subplots/_export_as_csv_formatters/_format_plot_box.py +1 -1
- scitex/plt/_subplots/_export_as_csv_formatters/_format_plot_kde.py +2 -2
- scitex/plt/_subplots/_export_as_csv_formatters/_format_quiver.py +53 -0
- scitex/plt/_subplots/_export_as_csv_formatters/_format_stem.py +42 -0
- scitex/plt/_subplots/_export_as_csv_formatters/_format_step.py +42 -0
- scitex/plt/_subplots/_export_as_csv_formatters/_format_streamplot.py +48 -0
- scitex/plt/_subplots/_export_as_csv_formatters/{_format_plot_conf_mat.py → _format_stx_conf_mat.py} +2 -2
- scitex/plt/_subplots/_export_as_csv_formatters/{_format_plot_ecdf.py → _format_stx_ecdf.py} +2 -2
- scitex/plt/_subplots/_export_as_csv_formatters/{_format_plot_fillv.py → _format_stx_fillv.py} +2 -2
- scitex/plt/_subplots/_export_as_csv_formatters/{_format_plot_heatmap.py → _format_stx_heatmap.py} +2 -2
- scitex/plt/_subplots/_export_as_csv_formatters/{_format_plot_image.py → _format_stx_image.py} +2 -2
- scitex/plt/_subplots/_export_as_csv_formatters/{_format_plot_joyplot.py → _format_stx_joyplot.py} +2 -2
- scitex/plt/_subplots/_export_as_csv_formatters/{_format_plot_line.py → _format_stx_line.py} +3 -3
- scitex/plt/_subplots/_export_as_csv_formatters/{_format_plot_mean_ci.py → _format_stx_mean_ci.py} +2 -2
- scitex/plt/_subplots/_export_as_csv_formatters/{_format_plot_mean_std.py → _format_stx_mean_std.py} +2 -2
- scitex/plt/_subplots/_export_as_csv_formatters/{_format_plot_median_iqr.py → _format_stx_median_iqr.py} +2 -2
- scitex/plt/_subplots/_export_as_csv_formatters/{_format_plot_raster.py → _format_stx_raster.py} +2 -2
- scitex/plt/_subplots/_export_as_csv_formatters/{_format_plot_rectangle.py → _format_stx_rectangle.py} +1 -1
- scitex/plt/_subplots/_export_as_csv_formatters/{_format_plot_scatter_hist.py → _format_stx_scatter_hist.py} +2 -2
- scitex/plt/_subplots/_export_as_csv_formatters/{_format_plot_shaded_line.py → _format_stx_shaded_line.py} +2 -2
- scitex/plt/_subplots/_export_as_csv_formatters/{_format_plot_violin.py → _format_stx_violin.py} +2 -2
- scitex/plt/_subplots/_export_as_csv_formatters/verify_formatters.py +23 -23
- scitex/plt/ax/__init__.py +16 -15
- scitex/plt/ax/_plot/__init__.py +30 -30
- scitex/plt/ax/_plot/_add_fitted_line.py +65 -11
- scitex/plt/ax/_plot/_plot_statistical_shaded_line.py +104 -76
- scitex/plt/ax/_plot/{_plot_conf_mat.py → _stx_conf_mat.py} +10 -10
- scitex/plt/ax/_plot/_stx_ecdf.py +109 -0
- scitex/plt/ax/_plot/{_plot_fillv.py → _stx_fillv.py} +7 -7
- scitex/plt/ax/_plot/_stx_heatmap.py +366 -0
- scitex/plt/ax/_plot/{_plot_image.py → _stx_image.py} +1 -1
- scitex/plt/ax/_plot/_stx_joyplot.py +113 -0
- scitex/plt/ax/_plot/{_plot_raster.py → _stx_raster.py} +37 -25
- scitex/plt/ax/_plot/{_plot_rectangle.py → _stx_rectangle.py} +10 -9
- scitex/plt/ax/_plot/{_plot_scatter_hist.py → _stx_scatter_hist.py} +1 -1
- scitex/plt/ax/_plot/_stx_shaded_line.py +215 -0
- scitex/plt/ax/_plot/{_plot_violin.py → _stx_violin.py} +13 -6
- scitex/plt/ax/_style/__init__.py +3 -0
- scitex/plt/ax/_style/_style_barplot.py +13 -2
- scitex/plt/ax/_style/_style_boxplot.py +78 -32
- scitex/plt/ax/_style/_style_errorbar.py +17 -3
- scitex/plt/ax/_style/_style_scatter.py +17 -3
- scitex/plt/ax/_style/_style_violinplot.py +109 -0
- scitex/plt/color/_vizualize_colors.py +3 -3
- scitex/plt/styles/SCITEX_STYLE.yaml +104 -0
- scitex/plt/styles/__init__.py +57 -0
- scitex/plt/styles/_plot_defaults.py +209 -0
- scitex/plt/styles/_plot_postprocess.py +518 -0
- scitex/plt/styles/_style_loader.py +268 -0
- scitex/plt/styles/presets.py +208 -0
- scitex/plt/utils/_collect_figure_metadata.py +160 -18
- scitex/plt/utils/_colorbar.py +72 -10
- scitex/plt/utils/_configure_mpl.py +108 -52
- scitex/plt/utils/_crop.py +21 -7
- scitex/plt/utils/_figure_mm.py +21 -7
- scitex/stats/__init__.py +13 -1
- scitex/stats/_schema.py +578 -0
- scitex/stats/tests/__init__.py +13 -0
- scitex/stats/tests/correlation/__init__.py +13 -0
- scitex/stats/tests/correlation/_test_pearson.py +262 -0
- scitex/vis/__init__.py +6 -0
- scitex/vis/editor/__init__.py +23 -0
- scitex/vis/editor/_defaults.py +205 -0
- scitex/vis/editor/_edit.py +342 -0
- scitex/vis/editor/_mpl_editor.py +231 -0
- scitex/vis/editor/_tkinter_editor.py +466 -0
- scitex/vis/editor/_web_editor.py +1440 -0
- scitex/vis/model/plot_types.py +15 -15
- {scitex-2.3.0.dist-info → scitex-2.4.1.dist-info}/METADATA +2 -1
- {scitex-2.3.0.dist-info → scitex-2.4.1.dist-info}/RECORD +94 -67
- {scitex-2.3.0.dist-info → scitex-2.4.1.dist-info}/WHEEL +1 -1
- scitex/plt/ax/_plot/_plot_ecdf.py +0 -84
- scitex/plt/ax/_plot/_plot_heatmap.py +0 -277
- scitex/plt/ax/_plot/_plot_joyplot.py +0 -77
- scitex/plt/ax/_plot/_plot_shaded_line.py +0 -142
- scitex/plt/presets.py +0 -224
- {scitex-2.3.0.dist-info → scitex-2.4.1.dist-info}/entry_points.txt +0 -0
- {scitex-2.3.0.dist-info → scitex-2.4.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -44,7 +44,7 @@ try:
|
|
|
44
44
|
|
|
45
45
|
# Import scitex plotting functions
|
|
46
46
|
import scitex as stx
|
|
47
|
-
from scitex.ai.plt.
|
|
47
|
+
from scitex.ai.plt.stx_conf_mat import stx_conf_mat as conf_mat
|
|
48
48
|
from scitex.ai.plt.plot_roc_curve import plot_roc_curve as roc_auc
|
|
49
49
|
from scitex.ai.plt.plot_pre_rec_curve import plot_pre_rec_curve as pre_rec_auc
|
|
50
50
|
|
scitex/ai/plt/__init__.py
CHANGED
|
@@ -6,7 +6,7 @@ but re-exported here for backward compatibility. New code should import directly
|
|
|
6
6
|
from scitex.ai.metrics instead.
|
|
7
7
|
"""
|
|
8
8
|
|
|
9
|
-
from .
|
|
9
|
+
from ._stx_conf_mat import calc_bACC_from_conf_mat, calc_bacc_from_conf_mat, stx_conf_mat, conf_mat
|
|
10
10
|
from ._plot_learning_curve import (
|
|
11
11
|
plot_learning_curve,
|
|
12
12
|
_prepare_metrics_df,
|
|
@@ -37,7 +37,7 @@ vline_at_epochs = _add_epoch_vlines
|
|
|
37
37
|
|
|
38
38
|
__all__ = [
|
|
39
39
|
# Plotting functions
|
|
40
|
-
"
|
|
40
|
+
"stx_conf_mat",
|
|
41
41
|
"conf_mat", # backward compat
|
|
42
42
|
"plot_learning_curve",
|
|
43
43
|
"learning_curve", # backward compat
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
# -*- coding: utf-8 -*-
|
|
3
3
|
# Timestamp: "2025-10-02 07:15:10 (ywatanabe)"
|
|
4
|
-
# File: /ssh:sp:/home/ywatanabe/proj/scitex_repo/src/scitex/ml/plt/
|
|
4
|
+
# File: /ssh:sp:/home/ywatanabe/proj/scitex_repo/src/scitex/ml/plt/stx_conf_mat.py
|
|
5
5
|
# ----------------------------------------
|
|
6
6
|
from __future__ import annotations
|
|
7
7
|
import os
|
|
@@ -28,7 +28,7 @@ from scitex.ai.metrics import calc_bacc_from_conf_mat
|
|
|
28
28
|
calc_bACC_from_conf_mat = calc_bacc_from_conf_mat
|
|
29
29
|
|
|
30
30
|
|
|
31
|
-
def
|
|
31
|
+
def stx_conf_mat(
|
|
32
32
|
cm=None,
|
|
33
33
|
y_true=None,
|
|
34
34
|
y_pred=None,
|
|
@@ -270,7 +270,7 @@ def plot_conf_mat(
|
|
|
270
270
|
|
|
271
271
|
|
|
272
272
|
# Backward compatibility alias
|
|
273
|
-
conf_mat =
|
|
273
|
+
conf_mat = stx_conf_mat
|
|
274
274
|
|
|
275
275
|
|
|
276
276
|
# Metric calculation functions have been moved to scitex.ai.metrics
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
# Timestamp: "2025-12-01 18:53:13 (ywatanabe)"
|
|
4
|
+
# File: /home/ywatanabe/proj/scitex-code/src/scitex/config/PriorityConfig.py
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
"""
|
|
8
|
+
Priority-based configuration resolver.
|
|
9
|
+
|
|
10
|
+
Provides clean precedence hierarchy: direct → env → config → default
|
|
11
|
+
|
|
12
|
+
Based on priority-config by ywatanabe (https://github.com/ywatanabe1989/priority-config)
|
|
13
|
+
Incorporated into scitex for self-contained configuration management.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import os
|
|
17
|
+
from typing import Dict
|
|
18
|
+
from typing import List
|
|
19
|
+
from typing import Optional, Type, Any
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class PriorityConfig:
|
|
23
|
+
"""Universal config resolver with precedence: direct → env → config → default
|
|
24
|
+
|
|
25
|
+
Examples
|
|
26
|
+
--------
|
|
27
|
+
>>> from scitex.config import PriorityConfig
|
|
28
|
+
>>> config = PriorityConfig(config_dict={"port": 3000}, env_prefix="SCITEX_")
|
|
29
|
+
>>> port = config.resolve("port", None, default=8000, type=int)
|
|
30
|
+
3000 # from config_dict (env not set)
|
|
31
|
+
>>> # With env: PORT=5000 python script.py
|
|
32
|
+
>>> port = config.resolve("port", None, default=8000, type=int)
|
|
33
|
+
5000 # env takes precedence over config
|
|
34
|
+
>>> port = config.resolve("port", 9000, default=8000, type=int)
|
|
35
|
+
9000 # direct value takes highest precedence
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
SENSITIVE_EXPRESSIONS = [
|
|
39
|
+
"API",
|
|
40
|
+
"PASSWORD",
|
|
41
|
+
"SECRET",
|
|
42
|
+
"TOKEN",
|
|
43
|
+
"KEY",
|
|
44
|
+
"PASS",
|
|
45
|
+
"AUTH",
|
|
46
|
+
"CREDENTIAL",
|
|
47
|
+
"PRIVATE",
|
|
48
|
+
"CERT",
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
def __init__(
|
|
52
|
+
self,
|
|
53
|
+
config_dict: Optional[Dict[str, Any]] = None,
|
|
54
|
+
env_prefix: str = "",
|
|
55
|
+
auto_uppercase: bool = True,
|
|
56
|
+
):
|
|
57
|
+
"""Initialize PriorityConfig.
|
|
58
|
+
|
|
59
|
+
Parameters
|
|
60
|
+
----------
|
|
61
|
+
config_dict : dict, optional
|
|
62
|
+
Dictionary with configuration values
|
|
63
|
+
env_prefix : str
|
|
64
|
+
Prefix for environment variables (e.g., "SCITEX_")
|
|
65
|
+
auto_uppercase : bool
|
|
66
|
+
Whether to uppercase keys for env lookup
|
|
67
|
+
"""
|
|
68
|
+
self.config_dict = config_dict or {}
|
|
69
|
+
self.env_prefix = env_prefix
|
|
70
|
+
self.auto_uppercase = auto_uppercase
|
|
71
|
+
self.resolution_log: List[Dict[str, Any]] = []
|
|
72
|
+
|
|
73
|
+
def __repr__(self) -> str:
|
|
74
|
+
return f"PriorityConfig(prefix='{self.env_prefix}', configs={len(self.config_dict)})"
|
|
75
|
+
|
|
76
|
+
def get(self, key: str) -> Any:
|
|
77
|
+
"""Get value from config dict only."""
|
|
78
|
+
return self.config_dict.get(key)
|
|
79
|
+
|
|
80
|
+
def resolve(
|
|
81
|
+
self,
|
|
82
|
+
key: str,
|
|
83
|
+
direct_val: Any = None,
|
|
84
|
+
default: Any = None,
|
|
85
|
+
type: Type = str,
|
|
86
|
+
mask: Optional[bool] = None,
|
|
87
|
+
) -> Any:
|
|
88
|
+
"""Get value with precedence hierarchy.
|
|
89
|
+
|
|
90
|
+
Precedence: direct → env → config → default
|
|
91
|
+
|
|
92
|
+
Parameters
|
|
93
|
+
----------
|
|
94
|
+
key : str
|
|
95
|
+
Configuration key to resolve
|
|
96
|
+
direct_val : Any, optional
|
|
97
|
+
Direct value (highest precedence)
|
|
98
|
+
default : Any, optional
|
|
99
|
+
Default value if not found elsewhere
|
|
100
|
+
type : Type
|
|
101
|
+
Type conversion (str, int, float, bool, list)
|
|
102
|
+
mask : bool, optional
|
|
103
|
+
Override automatic masking of sensitive values
|
|
104
|
+
|
|
105
|
+
Returns
|
|
106
|
+
-------
|
|
107
|
+
Any
|
|
108
|
+
Resolved configuration value
|
|
109
|
+
"""
|
|
110
|
+
source = None
|
|
111
|
+
final_value = None
|
|
112
|
+
|
|
113
|
+
# Replace dots with underscores for env key (e.g., axes.width_mm -> AXES_WIDTH_MM)
|
|
114
|
+
normalized_key = key.replace(".", "_")
|
|
115
|
+
env_key = f"{self.env_prefix}{normalized_key.upper() if self.auto_uppercase else normalized_key}"
|
|
116
|
+
env_val = os.getenv(env_key)
|
|
117
|
+
|
|
118
|
+
# Priority: direct → env → config → default
|
|
119
|
+
if direct_val is not None:
|
|
120
|
+
source = "direct"
|
|
121
|
+
final_value = direct_val
|
|
122
|
+
elif env_val:
|
|
123
|
+
source = f"env:{env_key}"
|
|
124
|
+
final_value = self._convert_type(env_val, type)
|
|
125
|
+
elif key in self.config_dict:
|
|
126
|
+
source = "config"
|
|
127
|
+
final_value = self.config_dict[key]
|
|
128
|
+
else:
|
|
129
|
+
source = "default"
|
|
130
|
+
final_value = default
|
|
131
|
+
|
|
132
|
+
if mask is False:
|
|
133
|
+
should_mask = False
|
|
134
|
+
else:
|
|
135
|
+
should_mask = self._is_sensitive(key)
|
|
136
|
+
|
|
137
|
+
display_value = (
|
|
138
|
+
self._mask_value(final_value) if should_mask else final_value
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
self.resolution_log.append(
|
|
142
|
+
{
|
|
143
|
+
"key": key,
|
|
144
|
+
"source": source,
|
|
145
|
+
"value": display_value,
|
|
146
|
+
"type": type.__name__,
|
|
147
|
+
}
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
return final_value
|
|
151
|
+
|
|
152
|
+
def print_resolutions(self) -> None:
|
|
153
|
+
"""Print how each config was resolved."""
|
|
154
|
+
if not self.resolution_log:
|
|
155
|
+
print("No configurations resolved yet")
|
|
156
|
+
return
|
|
157
|
+
|
|
158
|
+
print("Configuration Resolution Log:")
|
|
159
|
+
print("-" * 50)
|
|
160
|
+
for entry in self.resolution_log:
|
|
161
|
+
print(
|
|
162
|
+
f"{entry['key']:<20} = {entry['value']:<20} ({entry['source']})"
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
def clear_log(self) -> None:
|
|
166
|
+
"""Clear resolution log."""
|
|
167
|
+
self.resolution_log = []
|
|
168
|
+
|
|
169
|
+
def _convert_type(self, value: str, type: Type) -> Any:
|
|
170
|
+
"""Convert string value to specified type."""
|
|
171
|
+
if type == int:
|
|
172
|
+
return int(value)
|
|
173
|
+
elif type == float:
|
|
174
|
+
return float(value)
|
|
175
|
+
elif type == bool:
|
|
176
|
+
return value.lower() in ("true", "1", "yes")
|
|
177
|
+
elif type == list:
|
|
178
|
+
return value.split(",")
|
|
179
|
+
return value
|
|
180
|
+
|
|
181
|
+
def _is_sensitive(self, key: str) -> bool:
|
|
182
|
+
"""Check if key contains sensitive expressions."""
|
|
183
|
+
key_upper = key.upper()
|
|
184
|
+
return any(expr in key_upper for expr in self.SENSITIVE_EXPRESSIONS)
|
|
185
|
+
|
|
186
|
+
def _mask_value(self, value: Any) -> str:
|
|
187
|
+
"""Mask sensitive values for display."""
|
|
188
|
+
if value is None:
|
|
189
|
+
return None
|
|
190
|
+
value_str = str(value)
|
|
191
|
+
if len(value_str) <= 4:
|
|
192
|
+
return "****"
|
|
193
|
+
return value_str[:2] + "*" * (len(value_str) - 4) + value_str[-2:]
|
|
194
|
+
|
|
195
|
+
# EOF
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
# Timestamp: "2025-12-01 21:00:00 (ywatanabe)"
|
|
4
|
+
# File: ./src/scitex/config/__init__.py
|
|
5
|
+
|
|
6
|
+
"""
|
|
7
|
+
SciTeX configuration module.
|
|
8
|
+
|
|
9
|
+
Provides priority-based configuration resolution with clean precedence:
|
|
10
|
+
direct → config → env → default
|
|
11
|
+
|
|
12
|
+
Usage:
|
|
13
|
+
from scitex.config import PriorityConfig
|
|
14
|
+
|
|
15
|
+
config = PriorityConfig(config_dict={"port": 3000}, env_prefix="SCITEX_")
|
|
16
|
+
port = config.resolve("port", None, default=8000, type=int)
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from .PriorityConfig import PriorityConfig
|
|
20
|
+
|
|
21
|
+
__all__ = ["PriorityConfig"]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# EOF
|
scitex/io/_save.py
CHANGED
|
@@ -157,7 +157,7 @@ def save(
|
|
|
157
157
|
dry_run: bool = False,
|
|
158
158
|
no_csv: bool = False,
|
|
159
159
|
use_caller_path: bool = False,
|
|
160
|
-
auto_crop: bool =
|
|
160
|
+
auto_crop: bool = True,
|
|
161
161
|
crop_margin_mm: float = 1.0,
|
|
162
162
|
metadata_extra: dict = None,
|
|
163
163
|
**kwargs,
|
|
@@ -183,7 +183,7 @@ def save(
|
|
|
183
183
|
If True, simulate the saving process without actually writing files. Default is False.
|
|
184
184
|
auto_crop : bool, optional
|
|
185
185
|
If True, automatically crop the saved image to content area with margin (for PNG/JPEG/TIFF).
|
|
186
|
-
Vector formats (PDF/SVG) are not cropped. Default is
|
|
186
|
+
Vector formats (PDF/SVG) are not cropped. Default is True.
|
|
187
187
|
crop_margin_mm : float, optional
|
|
188
188
|
Margin in millimeters to add around content when auto_crop=True.
|
|
189
189
|
At 300 DPI: 1mm = ~12 pixels. Default is 1.0mm (Nature Reviews style).
|
|
@@ -764,6 +764,13 @@ def _handle_image_with_csv(
|
|
|
764
764
|
ext = _os.path.splitext(spath)[1].lower()
|
|
765
765
|
ext_wo_dot = ext.replace(".", "")
|
|
766
766
|
|
|
767
|
+
# Check if the path contains an image extension directory (e.g., ./png/, ./jpg/)
|
|
768
|
+
# If so, save CSV in a parallel ./csv/ directory
|
|
769
|
+
image_extensions = ['png', 'jpg', 'jpeg', 'gif', 'tiff', 'tif', 'svg', 'pdf']
|
|
770
|
+
parent_dir = _os.path.dirname(spath)
|
|
771
|
+
parent_name = _os.path.basename(parent_dir)
|
|
772
|
+
filename_without_ext = _os.path.splitext(_os.path.basename(spath))[0]
|
|
773
|
+
|
|
767
774
|
try:
|
|
768
775
|
# Get the figure object that may contain plot data
|
|
769
776
|
fig_obj = _get_figure_with_data(obj)
|
|
@@ -773,10 +780,18 @@ def _handle_image_with_csv(
|
|
|
773
780
|
if hasattr(fig_obj, "export_as_csv"):
|
|
774
781
|
csv_data = fig_obj.export_as_csv()
|
|
775
782
|
if csv_data is not None and not csv_data.empty:
|
|
776
|
-
#
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
783
|
+
# Determine CSV path based on parent directory name
|
|
784
|
+
if parent_name.lower() in image_extensions:
|
|
785
|
+
# Parent directory is named after an image extension (e.g., png/)
|
|
786
|
+
# Create parallel csv/ directory
|
|
787
|
+
grandparent_dir = _os.path.dirname(parent_dir)
|
|
788
|
+
csv_dir = _os.path.join(grandparent_dir, "csv")
|
|
789
|
+
csv_path = _os.path.join(csv_dir, filename_without_ext + ".csv")
|
|
790
|
+
else:
|
|
791
|
+
# Save CSV in same directory as image
|
|
792
|
+
csv_path = _os.path.splitext(spath)[0] + ".csv"
|
|
793
|
+
|
|
794
|
+
# Ensure parent directory exists
|
|
780
795
|
_os.makedirs(_os.path.dirname(csv_path), exist_ok=True)
|
|
781
796
|
# Save directly using _save to avoid path doubling
|
|
782
797
|
# Don't pass image-specific kwargs to CSV save
|
|
@@ -791,9 +806,19 @@ def _handle_image_with_csv(
|
|
|
791
806
|
|
|
792
807
|
# Create symlink_to for CSV if it was specified for the image
|
|
793
808
|
if symlink_to:
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
)
|
|
809
|
+
# Apply same directory transformation for symlink
|
|
810
|
+
symlink_parent_dir = _os.path.dirname(symlink_to)
|
|
811
|
+
symlink_parent_name = _os.path.basename(symlink_parent_dir)
|
|
812
|
+
symlink_filename_without_ext = _os.path.splitext(_os.path.basename(symlink_to))[0]
|
|
813
|
+
|
|
814
|
+
if symlink_parent_name.lower() in image_extensions:
|
|
815
|
+
symlink_grandparent_dir = _os.path.dirname(symlink_parent_dir)
|
|
816
|
+
csv_symlink_to = _os.path.join(
|
|
817
|
+
symlink_grandparent_dir, "csv", symlink_filename_without_ext + ".csv"
|
|
818
|
+
)
|
|
819
|
+
else:
|
|
820
|
+
csv_symlink_to = _os.path.splitext(symlink_to)[0] + ".csv"
|
|
821
|
+
|
|
797
822
|
_symlink_to(csv_path, csv_symlink_to, True)
|
|
798
823
|
|
|
799
824
|
# Create symlink for CSV manually if needed
|
|
@@ -810,13 +835,22 @@ def _handle_image_with_csv(
|
|
|
810
835
|
"specified_path"
|
|
811
836
|
]
|
|
812
837
|
if isinstance(original_path, str):
|
|
813
|
-
#
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
838
|
+
# Apply same directory transformation for symlink
|
|
839
|
+
orig_parent_dir = _os.path.dirname(original_path)
|
|
840
|
+
orig_parent_name = _os.path.basename(orig_parent_dir)
|
|
841
|
+
orig_filename_without_ext = _os.path.splitext(_os.path.basename(original_path))[0]
|
|
842
|
+
|
|
843
|
+
if orig_parent_name.lower() in image_extensions:
|
|
844
|
+
orig_grandparent_dir = _os.path.dirname(orig_parent_dir)
|
|
845
|
+
csv_relative = _os.path.join(
|
|
846
|
+
orig_grandparent_dir, "csv", orig_filename_without_ext + ".csv"
|
|
847
|
+
)
|
|
848
|
+
else:
|
|
849
|
+
csv_relative = original_path.replace(
|
|
850
|
+
_os.path.splitext(original_path)[1],
|
|
851
|
+
".csv",
|
|
852
|
+
)
|
|
853
|
+
|
|
820
854
|
csv_cwd = _os.path.join(
|
|
821
855
|
_os.getcwd(), csv_relative
|
|
822
856
|
)
|
|
@@ -835,10 +869,18 @@ def _handle_image_with_csv(
|
|
|
835
869
|
if hasattr(fig_obj, "export_as_csv_for_sigmaplot"):
|
|
836
870
|
sigmaplot_data = fig_obj.export_as_csv_for_sigmaplot()
|
|
837
871
|
if sigmaplot_data is not None and not sigmaplot_data.empty:
|
|
838
|
-
#
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
872
|
+
# Determine SigmaPlot CSV path based on parent directory name
|
|
873
|
+
if parent_name.lower() in image_extensions:
|
|
874
|
+
grandparent_dir = _os.path.dirname(parent_dir)
|
|
875
|
+
csv_dir = _os.path.join(grandparent_dir, "csv")
|
|
876
|
+
csv_sigmaplot_path = _os.path.join(csv_dir, filename_without_ext + "_for_sigmaplot.csv")
|
|
877
|
+
else:
|
|
878
|
+
csv_sigmaplot_path = spath.replace(
|
|
879
|
+
ext_wo_dot, "csv"
|
|
880
|
+
).replace(".csv", "_for_sigmaplot.csv")
|
|
881
|
+
|
|
882
|
+
# Ensure parent directory exists
|
|
883
|
+
_os.makedirs(_os.path.dirname(csv_sigmaplot_path), exist_ok=True)
|
|
842
884
|
# Save directly using _save to avoid path doubling
|
|
843
885
|
# Don't pass image-specific kwargs to CSV save
|
|
844
886
|
_save(
|
|
@@ -852,10 +894,21 @@ def _handle_image_with_csv(
|
|
|
852
894
|
|
|
853
895
|
# Create symlink_to for SigmaPlot CSV if it was specified for the image
|
|
854
896
|
if symlink_to:
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
897
|
+
symlink_parent_dir = _os.path.dirname(symlink_to)
|
|
898
|
+
symlink_parent_name = _os.path.basename(symlink_parent_dir)
|
|
899
|
+
symlink_filename_without_ext = _os.path.splitext(_os.path.basename(symlink_to))[0]
|
|
900
|
+
|
|
901
|
+
if symlink_parent_name.lower() in image_extensions:
|
|
902
|
+
symlink_grandparent_dir = _os.path.dirname(symlink_parent_dir)
|
|
903
|
+
csv_sigmaplot_symlink_to = _os.path.join(
|
|
904
|
+
symlink_grandparent_dir, "csv", symlink_filename_without_ext + "_for_sigmaplot.csv"
|
|
905
|
+
)
|
|
906
|
+
else:
|
|
907
|
+
csv_sigmaplot_symlink_to = (
|
|
908
|
+
_os.path.splitext(symlink_to)[0]
|
|
909
|
+
+ "_for_sigmaplot.csv"
|
|
910
|
+
)
|
|
911
|
+
|
|
859
912
|
_symlink_to(
|
|
860
913
|
csv_sigmaplot_path,
|
|
861
914
|
csv_sigmaplot_symlink_to,
|
|
@@ -881,11 +934,26 @@ def _handle_image_with_csv(
|
|
|
881
934
|
# Save metadata as JSON if collected
|
|
882
935
|
if collected_metadata is not None and not dry_run:
|
|
883
936
|
try:
|
|
884
|
-
#
|
|
885
|
-
#
|
|
886
|
-
|
|
937
|
+
# Check if the path contains an image extension directory (e.g., ./png/, ./jpg/)
|
|
938
|
+
# If so, save JSON in a parallel ./json/ directory
|
|
939
|
+
# Example: ./path/to/output/png/fig.png -> ./path/to/output/json/fig.json
|
|
940
|
+
# Example: ./path/to/output/fig.png -> ./path/to/output/fig.json (same dir)
|
|
941
|
+
image_extensions = ['png', 'jpg', 'jpeg', 'gif', 'tiff', 'tif', 'svg', 'pdf']
|
|
942
|
+
parent_dir = _os.path.dirname(spath)
|
|
943
|
+
parent_name = _os.path.basename(parent_dir)
|
|
944
|
+
filename_without_ext = _os.path.splitext(_os.path.basename(spath))[0]
|
|
945
|
+
|
|
946
|
+
if parent_name.lower() in image_extensions:
|
|
947
|
+
# Parent directory is named after an image extension (e.g., png/)
|
|
948
|
+
# Create parallel json/ directory
|
|
949
|
+
grandparent_dir = _os.path.dirname(parent_dir)
|
|
950
|
+
json_dir = _os.path.join(grandparent_dir, "json")
|
|
951
|
+
json_path = _os.path.join(json_dir, filename_without_ext + ".json")
|
|
952
|
+
else:
|
|
953
|
+
# Save JSON in same directory as image
|
|
954
|
+
json_path = _os.path.splitext(spath)[0] + ".json"
|
|
887
955
|
|
|
888
|
-
# Ensure parent directory exists
|
|
956
|
+
# Ensure parent directory exists
|
|
889
957
|
_os.makedirs(_os.path.dirname(json_path), exist_ok=True)
|
|
890
958
|
|
|
891
959
|
# Save metadata as JSON
|
|
@@ -900,7 +968,19 @@ def _handle_image_with_csv(
|
|
|
900
968
|
|
|
901
969
|
# Create symlink_to for JSON if it was specified for the image
|
|
902
970
|
if symlink_to:
|
|
903
|
-
|
|
971
|
+
# Apply same directory transformation for symlink
|
|
972
|
+
symlink_parent_dir = _os.path.dirname(symlink_to)
|
|
973
|
+
symlink_parent_name = _os.path.basename(symlink_parent_dir)
|
|
974
|
+
symlink_filename_without_ext = _os.path.splitext(_os.path.basename(symlink_to))[0]
|
|
975
|
+
|
|
976
|
+
if symlink_parent_name.lower() in image_extensions:
|
|
977
|
+
symlink_grandparent_dir = _os.path.dirname(symlink_parent_dir)
|
|
978
|
+
json_symlink_to = _os.path.join(
|
|
979
|
+
symlink_grandparent_dir, "json", symlink_filename_without_ext + ".json"
|
|
980
|
+
)
|
|
981
|
+
else:
|
|
982
|
+
json_symlink_to = _os.path.splitext(symlink_to)[0] + ".json"
|
|
983
|
+
|
|
904
984
|
_symlink_to(json_path, json_symlink_to, True)
|
|
905
985
|
|
|
906
986
|
# Create symlink for JSON manually if needed
|
|
@@ -915,11 +995,22 @@ def _handle_image_with_csv(
|
|
|
915
995
|
if "specified_path" in frame.frame.f_locals:
|
|
916
996
|
original_path = frame.frame.f_locals["specified_path"]
|
|
917
997
|
if isinstance(original_path, str):
|
|
918
|
-
#
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
998
|
+
# Apply same directory transformation for symlink
|
|
999
|
+
orig_parent_dir = _os.path.dirname(original_path)
|
|
1000
|
+
orig_parent_name = _os.path.basename(orig_parent_dir)
|
|
1001
|
+
orig_filename_without_ext = _os.path.splitext(_os.path.basename(original_path))[0]
|
|
1002
|
+
|
|
1003
|
+
if orig_parent_name.lower() in image_extensions:
|
|
1004
|
+
orig_grandparent_dir = _os.path.dirname(orig_parent_dir)
|
|
1005
|
+
json_relative = _os.path.join(
|
|
1006
|
+
orig_grandparent_dir, "json", orig_filename_without_ext + ".json"
|
|
1007
|
+
)
|
|
1008
|
+
else:
|
|
1009
|
+
json_relative = original_path.replace(
|
|
1010
|
+
_os.path.splitext(original_path)[1],
|
|
1011
|
+
".json",
|
|
1012
|
+
)
|
|
1013
|
+
|
|
923
1014
|
json_cwd = _os.path.join(_os.getcwd(), json_relative)
|
|
924
1015
|
_symlink(json_path, json_cwd, True, True)
|
|
925
1016
|
break
|
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
# -*- coding: utf-8 -*-
|
|
3
|
-
# Timestamp: "2025-
|
|
4
|
-
# File: /
|
|
5
|
-
|
|
3
|
+
# Timestamp: "2025-12-01 12:23:32 (ywatanabe)"
|
|
4
|
+
# File: /home/ywatanabe/proj/scitex-code/src/scitex/io/_save_modules/_image.py
|
|
5
|
+
|
|
6
6
|
import os
|
|
7
|
+
|
|
7
8
|
__FILE__ = __file__
|
|
8
|
-
__DIR__ = os.path.dirname(__FILE__)
|
|
9
|
-
# ----------------------------------------
|
|
10
9
|
|
|
11
10
|
import io as _io
|
|
12
11
|
import logging
|
|
@@ -17,15 +16,23 @@ from PIL import Image
|
|
|
17
16
|
logger = logging.getLogger(__name__)
|
|
18
17
|
|
|
19
18
|
|
|
20
|
-
def save_image(
|
|
19
|
+
def save_image(
|
|
20
|
+
obj,
|
|
21
|
+
spath,
|
|
22
|
+
metadata=None,
|
|
23
|
+
add_qr=False,
|
|
24
|
+
qr_position="bottom-right",
|
|
25
|
+
verbose=False,
|
|
26
|
+
**kwargs,
|
|
27
|
+
):
|
|
21
28
|
# Add URL to metadata if not present
|
|
22
29
|
if metadata is not None:
|
|
23
30
|
if verbose:
|
|
24
31
|
logger.info(f"📝 Saving figure with metadata to: {spath}")
|
|
25
32
|
|
|
26
|
-
if
|
|
33
|
+
if "url" not in metadata:
|
|
27
34
|
metadata = dict(metadata)
|
|
28
|
-
metadata[
|
|
35
|
+
metadata["url"] = "https://scitex.ai"
|
|
29
36
|
if verbose:
|
|
30
37
|
logger.info(" • Auto-added URL: https://scitex.ai")
|
|
31
38
|
|
|
@@ -35,12 +42,16 @@ def save_image(obj, spath, metadata=None, add_qr=False, qr_position='bottom-righ
|
|
|
35
42
|
logger.info(f" • Adding QR code at position: {qr_position}")
|
|
36
43
|
try:
|
|
37
44
|
from .._qr_utils import add_qr_to_figure
|
|
45
|
+
|
|
38
46
|
# Only add QR for matplotlib figures
|
|
39
|
-
if hasattr(obj,
|
|
40
|
-
|
|
47
|
+
if hasattr(obj, "savefig") or (
|
|
48
|
+
hasattr(obj, "figure") and hasattr(obj.figure, "savefig")
|
|
49
|
+
):
|
|
50
|
+
fig = obj if hasattr(obj, "savefig") else obj.figure
|
|
41
51
|
obj = add_qr_to_figure(fig, metadata, position=qr_position)
|
|
42
52
|
except Exception as e:
|
|
43
53
|
import warnings
|
|
54
|
+
|
|
44
55
|
warnings.warn(f"Failed to add QR code: {e}")
|
|
45
56
|
|
|
46
57
|
# png
|
|
@@ -67,7 +78,7 @@ def save_image(obj, spath, metadata=None, add_qr=False, qr_position='bottom-righ
|
|
|
67
78
|
# matplotlib
|
|
68
79
|
else:
|
|
69
80
|
# Use kwargs dpi if provided, otherwise default to 300
|
|
70
|
-
save_kwargs = {
|
|
81
|
+
save_kwargs = {"format": "tiff", "dpi": kwargs.get("dpi", 300)}
|
|
71
82
|
save_kwargs.update(kwargs)
|
|
72
83
|
try:
|
|
73
84
|
obj.savefig(spath, **save_kwargs)
|
|
@@ -85,7 +96,9 @@ def save_image(obj, spath, metadata=None, add_qr=False, qr_position='bottom-righ
|
|
|
85
96
|
obj.write_image(buf, format="png")
|
|
86
97
|
buf.seek(0)
|
|
87
98
|
img = Image.open(buf)
|
|
88
|
-
img.convert("RGB").save(
|
|
99
|
+
img.convert("RGB").save(
|
|
100
|
+
spath, "JPEG", quality=100, subsampling=0, optimize=False
|
|
101
|
+
)
|
|
89
102
|
buf.close()
|
|
90
103
|
|
|
91
104
|
# PIL image
|
|
@@ -95,7 +108,7 @@ def save_image(obj, spath, metadata=None, add_qr=False, qr_position='bottom-righ
|
|
|
95
108
|
|
|
96
109
|
# matplotlib
|
|
97
110
|
else:
|
|
98
|
-
save_kwargs = {
|
|
111
|
+
save_kwargs = {"format": "png"}
|
|
99
112
|
save_kwargs.update(kwargs)
|
|
100
113
|
try:
|
|
101
114
|
obj.savefig(buf, **save_kwargs)
|
|
@@ -105,7 +118,9 @@ def save_image(obj, spath, metadata=None, add_qr=False, qr_position='bottom-righ
|
|
|
105
118
|
buf.seek(0)
|
|
106
119
|
img = Image.open(buf)
|
|
107
120
|
# Save JPEG with very high quality settings for daily use (quality=98 is near-lossless)
|
|
108
|
-
img.convert("RGB").save(
|
|
121
|
+
img.convert("RGB").save(
|
|
122
|
+
spath, "JPEG", quality=100, subsampling=0, optimize=False
|
|
123
|
+
)
|
|
109
124
|
buf.close()
|
|
110
125
|
del obj
|
|
111
126
|
|
|
@@ -125,7 +140,7 @@ def save_image(obj, spath, metadata=None, add_qr=False, qr_position='bottom-righ
|
|
|
125
140
|
# matplotlib
|
|
126
141
|
else:
|
|
127
142
|
buf = _io.BytesIO()
|
|
128
|
-
save_kwargs = {
|
|
143
|
+
save_kwargs = {"format": "png"}
|
|
129
144
|
save_kwargs.update(kwargs)
|
|
130
145
|
try:
|
|
131
146
|
obj.savefig(buf, **save_kwargs)
|
|
@@ -144,7 +159,7 @@ def save_image(obj, spath, metadata=None, add_qr=False, qr_position='bottom-righ
|
|
|
144
159
|
obj.write_image(file=spath, format="svg")
|
|
145
160
|
# Matplotlib
|
|
146
161
|
else:
|
|
147
|
-
save_kwargs = {
|
|
162
|
+
save_kwargs = {"format": "svg"}
|
|
148
163
|
save_kwargs.update(kwargs)
|
|
149
164
|
try:
|
|
150
165
|
obj.savefig(spath, **save_kwargs)
|
|
@@ -160,15 +175,15 @@ def save_image(obj, spath, metadata=None, add_qr=False, qr_position='bottom-righ
|
|
|
160
175
|
# PIL Image - convert to PDF
|
|
161
176
|
elif isinstance(obj, Image.Image):
|
|
162
177
|
# Convert RGBA to RGB if needed
|
|
163
|
-
if obj.mode ==
|
|
164
|
-
rgb_img = Image.new(
|
|
178
|
+
if obj.mode == "RGBA":
|
|
179
|
+
rgb_img = Image.new("RGB", obj.size, (255, 255, 255))
|
|
165
180
|
rgb_img.paste(obj, mask=obj.split()[3])
|
|
166
181
|
rgb_img.save(spath, "PDF")
|
|
167
182
|
else:
|
|
168
183
|
obj.save(spath, "PDF")
|
|
169
184
|
# Matplotlib
|
|
170
185
|
else:
|
|
171
|
-
save_kwargs = {
|
|
186
|
+
save_kwargs = {"format": "pdf"}
|
|
172
187
|
save_kwargs.update(kwargs)
|
|
173
188
|
try:
|
|
174
189
|
obj.savefig(spath, **save_kwargs)
|
|
@@ -179,12 +194,14 @@ def save_image(obj, spath, metadata=None, add_qr=False, qr_position='bottom-righ
|
|
|
179
194
|
# Embed metadata if provided
|
|
180
195
|
if metadata is not None:
|
|
181
196
|
from .._metadata import embed_metadata
|
|
197
|
+
|
|
182
198
|
try:
|
|
183
199
|
embed_metadata(spath, metadata)
|
|
184
200
|
if verbose:
|
|
185
|
-
logger.
|
|
201
|
+
logger.debug(f" • Embedded metadata: {metadata}")
|
|
186
202
|
except Exception as e:
|
|
187
203
|
import warnings
|
|
204
|
+
|
|
188
205
|
warnings.warn(f"Failed to embed metadata: {e}")
|
|
189
206
|
|
|
190
207
|
# EOF
|