traitly 0.1.0__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.
- traitly/__init__.py +39 -0
- traitly/cli.py +380 -0
- traitly/fruit_phenotyping/__init__.py +20 -0
- traitly/fruit_phenotyping/analysis_parameters.py +192 -0
- traitly/fruit_phenotyping/color_analysis.py +1412 -0
- traitly/fruit_phenotyping/color_plot.py +1136 -0
- traitly/fruit_phenotyping/external_analysis.py +1280 -0
- traitly/fruit_phenotyping/fruit_config.py +1313 -0
- traitly/fruit_phenotyping/geometry.py +419 -0
- traitly/fruit_phenotyping/internal_analysis.py +3173 -0
- traitly/fruit_phenotyping/mask.py +1654 -0
- traitly/fruit_phenotyping/processing.py +724 -0
- traitly/fruit_phenotyping/results_image.py +466 -0
- traitly/fruit_phenotyping/symmetry.py +223 -0
- traitly/package_data/config/setup-torch.py +30 -0
- traitly/package_data/models/label.pt +0 -0
- traitly/package_data/models/size_reference.pt +0 -0
- traitly/shiny_app/app.py +4094 -0
- traitly/shiny_app/www/parameters.json +100 -0
- traitly/utils/__init__.py +5 -0
- traitly/utils/basic_functions.py +359 -0
- traitly/utils/calibration.py +1085 -0
- traitly/utils/constants.py +20 -0
- traitly/utils/convert_pdf.py +305 -0
- traitly/utils/label.py +468 -0
- traitly-0.1.0.dist-info/METADATA +216 -0
- traitly-0.1.0.dist-info/RECORD +30 -0
- traitly-0.1.0.dist-info/WHEEL +4 -0
- traitly-0.1.0.dist-info/entry_points.txt +3 -0
- traitly-0.1.0.dist-info/licenses/LICENSE +661 -0
traitly/__init__.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# traitly/__init__.py
|
|
2
|
+
"""
|
|
3
|
+
Traitly: Phenotyping analysis of fruits in images using computer vision.
|
|
4
|
+
"""
|
|
5
|
+
import re
|
|
6
|
+
from importlib.metadata import version, metadata, PackageNotFoundError
|
|
7
|
+
|
|
8
|
+
try:
|
|
9
|
+
_meta = metadata("traitly")
|
|
10
|
+
__version__ = version("traitly")
|
|
11
|
+
|
|
12
|
+
_raw = _meta.get("Author-email") or ""
|
|
13
|
+
# Separar por coma fuera de los < >
|
|
14
|
+
_entries = re.split(r",\s*(?=[^<>]*(?:<|$))", _raw)
|
|
15
|
+
__authors__ = [
|
|
16
|
+
re.match(r"^(.*?)\s*<", a).group(1).strip()
|
|
17
|
+
for a in _entries if "<" in a
|
|
18
|
+
]
|
|
19
|
+
__author__ = ", ".join(__authors__)
|
|
20
|
+
|
|
21
|
+
except PackageNotFoundError:
|
|
22
|
+
__version__ = "unknown"
|
|
23
|
+
__authors__ = []
|
|
24
|
+
__author__ = "unknown"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# Import functions from utils
|
|
28
|
+
from .utils.basic_functions import (
|
|
29
|
+
load_img,
|
|
30
|
+
plot_img
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
__all__ = [
|
|
34
|
+
'__version__',
|
|
35
|
+
'__author__',
|
|
36
|
+
'__authors__',
|
|
37
|
+
'load_img',
|
|
38
|
+
'plot_img'
|
|
39
|
+
]
|
traitly/cli.py
ADDED
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
# traitly/cli.py
|
|
2
|
+
"""
|
|
3
|
+
Command-line interface for Traitly fruit phenotyping.
|
|
4
|
+
|
|
5
|
+
Provides the ``traitly`` entry point for running internal and external
|
|
6
|
+
fruit analysis pipelines directly from the terminal, on single images
|
|
7
|
+
or entire folders.
|
|
8
|
+
|
|
9
|
+
Usage
|
|
10
|
+
-----
|
|
11
|
+
.. code-block:: bash
|
|
12
|
+
|
|
13
|
+
traitly --fruit_internal -i PATH [-o PATH] [--json PATH] [--num_cores N]
|
|
14
|
+
traitly --fruit_external -i PATH [-o PATH] [--json PATH] [--num_cores N]
|
|
15
|
+
|
|
16
|
+
For folder inputs, delegates to
|
|
17
|
+
:meth:`~traitly.fruit_phenotyping.internal_analysis.FruitInternalAnalyzer.analyze_folder`
|
|
18
|
+
or
|
|
19
|
+
:meth:`~traitly.fruit_phenotyping.external_analysis.FruitExternalAnalyzer.analyze_folder`.
|
|
20
|
+
For single images, delegates to
|
|
21
|
+
:meth:`~traitly.fruit_phenotyping.internal_analysis.FruitInternalAnalyzer.process_single_file`.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
import argparse
|
|
25
|
+
import sys
|
|
26
|
+
import os
|
|
27
|
+
from pathlib import Path
|
|
28
|
+
|
|
29
|
+
try:
|
|
30
|
+
from tabulate import tabulate as _tabulate
|
|
31
|
+
def _fmt_examples(rows):
|
|
32
|
+
return _tabulate(rows, tablefmt='plain')
|
|
33
|
+
except ImportError:
|
|
34
|
+
def _fmt_examples(rows):
|
|
35
|
+
return "\n".join(f" {r[0]}" for r in rows)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
# ============================================================================
|
|
39
|
+
# Parser
|
|
40
|
+
# ============================================================================
|
|
41
|
+
|
|
42
|
+
def create_parser() -> argparse.ArgumentParser:
|
|
43
|
+
"""
|
|
44
|
+
Build and return the CLI argument parser for Traitly.
|
|
45
|
+
|
|
46
|
+
Defines a mutually exclusive mode group (``--fruit_internal`` /
|
|
47
|
+
``--fruit_external``), required and optional arguments, and a
|
|
48
|
+
formatted epilog with usage examples.
|
|
49
|
+
|
|
50
|
+
Returns
|
|
51
|
+
-------
|
|
52
|
+
argparse.ArgumentParser
|
|
53
|
+
Configured parser ready to call ``.parse_args()`` on.
|
|
54
|
+
"""
|
|
55
|
+
parser = argparse.ArgumentParser(
|
|
56
|
+
prog='traitly',
|
|
57
|
+
description='Traitly - Fruit Phenotyping Tool',
|
|
58
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
59
|
+
epilog="\n".join([
|
|
60
|
+
"",
|
|
61
|
+
"Examples:",
|
|
62
|
+
_fmt_examples([
|
|
63
|
+
(" # Internal structure analysis (single image or folder)",),
|
|
64
|
+
(" traitly --fruit_internal -i tests/sample_data/",),
|
|
65
|
+
(" traitly --fruit_internal -i tests/sample_data/ -o results/ --num_cores 4",),
|
|
66
|
+
(" traitly --fruit_internal -i tests/sample_data/ --json config.json",),
|
|
67
|
+
("",),
|
|
68
|
+
(" # External analysis (single image or folder)",),
|
|
69
|
+
(" traitly --fruit_external -i tests/sample_data/",),
|
|
70
|
+
(" traitly --fruit_external -i tests/sample_data/ -o results/ --json config.json --num_cores 4",),
|
|
71
|
+
]),
|
|
72
|
+
"",
|
|
73
|
+
"For more info: https://github.com/mariameraz/traitly",
|
|
74
|
+
"",
|
|
75
|
+
])
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
# Mode (mutually exclusive)
|
|
79
|
+
mode_group = parser.add_mutually_exclusive_group(required=True)
|
|
80
|
+
mode_group.add_argument(
|
|
81
|
+
'--fruit_internal',
|
|
82
|
+
action='store_true',
|
|
83
|
+
help='Analyze internal fruit structure (locules, pericarp, symmetry)'
|
|
84
|
+
)
|
|
85
|
+
mode_group.add_argument(
|
|
86
|
+
'--fruit_external',
|
|
87
|
+
action='store_true',
|
|
88
|
+
help='Analyze external fruit structure (morphology, color)'
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
# Required
|
|
92
|
+
parser.add_argument(
|
|
93
|
+
'-i', '--input',
|
|
94
|
+
type=str,
|
|
95
|
+
required=True,
|
|
96
|
+
metavar='PATH',
|
|
97
|
+
help='Path to image file or folder'
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
# Optional
|
|
101
|
+
parser.add_argument(
|
|
102
|
+
'-o', '--output',
|
|
103
|
+
type=str,
|
|
104
|
+
default=None,
|
|
105
|
+
metavar='PATH',
|
|
106
|
+
help='Output directory (default: <input>/Results/)'
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
parser.add_argument(
|
|
110
|
+
'--json',
|
|
111
|
+
type=str,
|
|
112
|
+
default=None,
|
|
113
|
+
metavar='PATH',
|
|
114
|
+
help='Path to JSON config file with analysis parameters'
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
parser.add_argument(
|
|
118
|
+
'--num_cores',
|
|
119
|
+
type=int,
|
|
120
|
+
default=1,
|
|
121
|
+
metavar='N',
|
|
122
|
+
help='Number of CPU cores for parallel processing (default: 1)'
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
parser.add_argument(
|
|
126
|
+
'--no_morphology',
|
|
127
|
+
action='store_true',
|
|
128
|
+
help='Skip morphology analysis'
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
parser.add_argument(
|
|
132
|
+
'--no_color',
|
|
133
|
+
action='store_true',
|
|
134
|
+
help='Skip color analysis'
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
parser.add_argument(
|
|
138
|
+
'--version',
|
|
139
|
+
action='version',
|
|
140
|
+
version='%(prog)s 0.1.0'
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
return parser
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
# ============================================================================
|
|
147
|
+
# Helpers
|
|
148
|
+
# ============================================================================
|
|
149
|
+
|
|
150
|
+
def _validate_input(path_str: str) -> Path:
|
|
151
|
+
"""
|
|
152
|
+
Validate that the input path exists and return it as a :class:`Path`.
|
|
153
|
+
|
|
154
|
+
Prints an error message and calls ``sys.exit(1)`` if the path does
|
|
155
|
+
not exist.
|
|
156
|
+
|
|
157
|
+
Parameters
|
|
158
|
+
----------
|
|
159
|
+
path_str : str
|
|
160
|
+
Raw input path string from the CLI argument.
|
|
161
|
+
|
|
162
|
+
Returns
|
|
163
|
+
-------
|
|
164
|
+
Path
|
|
165
|
+
Validated :class:`~pathlib.Path` object.
|
|
166
|
+
"""
|
|
167
|
+
path = Path(path_str)
|
|
168
|
+
if not path.exists():
|
|
169
|
+
print(f">> Error: Path does not exist: {path_str}")
|
|
170
|
+
sys.exit(1)
|
|
171
|
+
return path
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _validate_json(json_str: str) -> str:
|
|
175
|
+
"""
|
|
176
|
+
Validate that the JSON config file exists if provided.
|
|
177
|
+
|
|
178
|
+
Prints an error message and calls ``sys.exit(1)`` if ``json_str``
|
|
179
|
+
is not ``None`` and the file does not exist.
|
|
180
|
+
|
|
181
|
+
Parameters
|
|
182
|
+
----------
|
|
183
|
+
json_str : str or None
|
|
184
|
+
Path to the JSON config file, or ``None`` if not provided.
|
|
185
|
+
|
|
186
|
+
Returns
|
|
187
|
+
-------
|
|
188
|
+
str or None
|
|
189
|
+
The original ``json_str`` if valid, or ``None``.
|
|
190
|
+
"""
|
|
191
|
+
if json_str is not None and not Path(json_str).exists():
|
|
192
|
+
print(f">> Error: JSON config file does not exist: {json_str}")
|
|
193
|
+
sys.exit(1)
|
|
194
|
+
return json_str
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
# ============================================================================
|
|
198
|
+
# Internal analysis
|
|
199
|
+
# ============================================================================
|
|
200
|
+
|
|
201
|
+
def run_internal(args: argparse.Namespace) -> None:
|
|
202
|
+
"""
|
|
203
|
+
Run the internal fruit analysis pipeline from the CLI.
|
|
204
|
+
|
|
205
|
+
Instantiates :class:`~traitly.fruit_phenotyping.internal_analysis.FruitInternalAnalyzer`
|
|
206
|
+
and dispatches to:
|
|
207
|
+
|
|
208
|
+
- :meth:`~traitly.fruit_phenotyping.internal_analysis.FruitInternalAnalyzer.analyze_folder`
|
|
209
|
+
when ``args.input`` is a directory.
|
|
210
|
+
- :meth:`~traitly.fruit_phenotyping.internal_analysis.FruitInternalAnalyzer.process_single_file`
|
|
211
|
+
when ``args.input`` is a single image file.
|
|
212
|
+
|
|
213
|
+
Parameters
|
|
214
|
+
----------
|
|
215
|
+
args : argparse.Namespace
|
|
216
|
+
Parsed CLI arguments containing ``input``, ``output``, ``json``,
|
|
217
|
+
``num_cores``, ``no_morphology``, and ``no_color``.
|
|
218
|
+
"""
|
|
219
|
+
from traitly.fruit_phenotyping.internal_analysis import FruitInternalAnalyzer
|
|
220
|
+
|
|
221
|
+
path = _validate_input(args.input)
|
|
222
|
+
json_path = _validate_json(args.json)
|
|
223
|
+
|
|
224
|
+
try:
|
|
225
|
+
analyzer = FruitInternalAnalyzer(str(path))
|
|
226
|
+
except Exception as e:
|
|
227
|
+
print(f">> Error initializing FruitInternalAnalyzer: {e}")
|
|
228
|
+
sys.exit(1)
|
|
229
|
+
|
|
230
|
+
analyze_morphology = not args.no_morphology
|
|
231
|
+
analyze_color = not args.no_color
|
|
232
|
+
|
|
233
|
+
if path.is_dir():
|
|
234
|
+
try:
|
|
235
|
+
analyzer.analyze_folder(
|
|
236
|
+
analyze_morphology=analyze_morphology,
|
|
237
|
+
analyze_color=analyze_color,
|
|
238
|
+
json_path=json_path,
|
|
239
|
+
output_path=args.output,
|
|
240
|
+
num_cores=args.num_cores,
|
|
241
|
+
verbose=True,
|
|
242
|
+
)
|
|
243
|
+
except Exception as e:
|
|
244
|
+
print(f">> Error during internal folder analysis: {e}")
|
|
245
|
+
import traceback; traceback.print_exc()
|
|
246
|
+
sys.exit(1)
|
|
247
|
+
|
|
248
|
+
else:
|
|
249
|
+
# Single image — process_single_file
|
|
250
|
+
import json as _json
|
|
251
|
+
config = {}
|
|
252
|
+
if json_path:
|
|
253
|
+
with open(json_path, 'r', encoding='utf-8') as f:
|
|
254
|
+
config = _json.load(f) or {}
|
|
255
|
+
|
|
256
|
+
try:
|
|
257
|
+
analyzer.load_image(plot=False)
|
|
258
|
+
df_m, df_c, err, n_fruits, ann_img = analyzer.process_single_file(
|
|
259
|
+
config=config,
|
|
260
|
+
json_path=None,
|
|
261
|
+
analyze_morphology=analyze_morphology,
|
|
262
|
+
analyze_color=analyze_color,
|
|
263
|
+
save_image=True,
|
|
264
|
+
output_path=args.output,
|
|
265
|
+
)
|
|
266
|
+
if err:
|
|
267
|
+
print(f">> Error processing image: {err.get('status', 'Unknown error')}")
|
|
268
|
+
sys.exit(1)
|
|
269
|
+
print(f"Done. Fruits detected: {n_fruits}")
|
|
270
|
+
except Exception as e:
|
|
271
|
+
print(f">> Error during internal image analysis: {e}")
|
|
272
|
+
import traceback; traceback.print_exc()
|
|
273
|
+
sys.exit(1)
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
# ============================================================================
|
|
277
|
+
# External analysis
|
|
278
|
+
# ============================================================================
|
|
279
|
+
|
|
280
|
+
def run_external(args: argparse.Namespace) -> None:
|
|
281
|
+
"""
|
|
282
|
+
Run the external fruit analysis pipeline from the CLI.
|
|
283
|
+
|
|
284
|
+
Instantiates :class:`~traitly.fruit_phenotyping.external_analysis.FruitExternalAnalyzer`
|
|
285
|
+
and dispatches to:
|
|
286
|
+
|
|
287
|
+
- :meth:`~traitly.fruit_phenotyping.external_analysis.FruitExternalAnalyzer.analyze_folder`
|
|
288
|
+
when ``args.input`` is a directory.
|
|
289
|
+
- :meth:`~traitly.fruit_phenotyping.external_analysis.FruitExternalAnalyzer.process_single_file`
|
|
290
|
+
when ``args.input`` is a single image file.
|
|
291
|
+
|
|
292
|
+
Parameters
|
|
293
|
+
----------
|
|
294
|
+
args : argparse.Namespace
|
|
295
|
+
Parsed CLI arguments containing ``input``, ``output``, ``json``,
|
|
296
|
+
``num_cores``, ``no_morphology``, and ``no_color``.
|
|
297
|
+
"""
|
|
298
|
+
from traitly.fruit_phenotyping.external_analysis import FruitExternalAnalyzer
|
|
299
|
+
|
|
300
|
+
path = _validate_input(args.input)
|
|
301
|
+
json_path = _validate_json(args.json)
|
|
302
|
+
|
|
303
|
+
try:
|
|
304
|
+
analyzer = FruitExternalAnalyzer(str(path))
|
|
305
|
+
except Exception as e:
|
|
306
|
+
print(f">> Error initializing FruitExternalAnalyzer: {e}")
|
|
307
|
+
sys.exit(1)
|
|
308
|
+
|
|
309
|
+
analyze_morphology = not args.no_morphology
|
|
310
|
+
analyze_color = not args.no_color
|
|
311
|
+
|
|
312
|
+
if path.is_dir():
|
|
313
|
+
try:
|
|
314
|
+
analyzer.analyze_folder(
|
|
315
|
+
analyze_morphology=analyze_morphology,
|
|
316
|
+
analyze_color=analyze_color,
|
|
317
|
+
json_path=json_path,
|
|
318
|
+
output_path=args.output,
|
|
319
|
+
num_cores=args.num_cores,
|
|
320
|
+
verbose=True,
|
|
321
|
+
)
|
|
322
|
+
except Exception as e:
|
|
323
|
+
print(f">> Error during external folder analysis: {e}")
|
|
324
|
+
import traceback; traceback.print_exc()
|
|
325
|
+
sys.exit(1)
|
|
326
|
+
|
|
327
|
+
else:
|
|
328
|
+
# Single image
|
|
329
|
+
import json as _json
|
|
330
|
+
config = {}
|
|
331
|
+
if json_path:
|
|
332
|
+
with open(json_path, 'r', encoding='utf-8') as f:
|
|
333
|
+
config = _json.load(f) or {}
|
|
334
|
+
|
|
335
|
+
try:
|
|
336
|
+
analyzer.load_image(plot=False)
|
|
337
|
+
df_m, df_c, err, n_fruits, ann_img = analyzer.process_single_file(
|
|
338
|
+
config=config,
|
|
339
|
+
json_path=None,
|
|
340
|
+
analyze_morphology=analyze_morphology,
|
|
341
|
+
analyze_color=analyze_color,
|
|
342
|
+
save_image=True,
|
|
343
|
+
output_path=args.output,
|
|
344
|
+
)
|
|
345
|
+
if err:
|
|
346
|
+
print(f">> Error processing image: {err.get('status', 'Unknown error')}")
|
|
347
|
+
sys.exit(1)
|
|
348
|
+
print(f"Done. Fruits detected: {n_fruits}")
|
|
349
|
+
except Exception as e:
|
|
350
|
+
print(f">> Error during external image analysis: {e}")
|
|
351
|
+
import traceback; traceback.print_exc()
|
|
352
|
+
sys.exit(1)
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
# ============================================================================
|
|
356
|
+
# Entry point
|
|
357
|
+
# ============================================================================
|
|
358
|
+
|
|
359
|
+
def main() -> None:
|
|
360
|
+
"""
|
|
361
|
+
Entry point for the ``traitly`` CLI command.
|
|
362
|
+
|
|
363
|
+
Parses arguments via :func:`create_parser` and dispatches to
|
|
364
|
+
:func:`run_internal` or :func:`run_external` based on the selected
|
|
365
|
+
mode flag. Prints help and exits cleanly if neither flag is set.
|
|
366
|
+
"""
|
|
367
|
+
parser = create_parser()
|
|
368
|
+
args = parser.parse_args()
|
|
369
|
+
|
|
370
|
+
if args.fruit_internal:
|
|
371
|
+
run_internal(args)
|
|
372
|
+
elif args.fruit_external:
|
|
373
|
+
run_external(args)
|
|
374
|
+
else:
|
|
375
|
+
parser.print_help()
|
|
376
|
+
sys.exit(0)
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
if __name__ == '__main__':
|
|
380
|
+
main()
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from .internal_analysis import FruitInternalAnalyzer
|
|
2
|
+
from .external_analysis import FruitExternalAnalyzer
|
|
3
|
+
|
|
4
|
+
from .color_plot import (plot_color_scatter,
|
|
5
|
+
plot_color_histogram,
|
|
6
|
+
plot_color_correlation,
|
|
7
|
+
plot_dark_threshold,
|
|
8
|
+
plot_tissue_colors)
|
|
9
|
+
|
|
10
|
+
from .color_analysis import calculate_hue_index
|
|
11
|
+
|
|
12
|
+
__all__ = ['FruitInternalAnalyzer',
|
|
13
|
+
'FruitExternalAnalyzer',
|
|
14
|
+
'plot_color_scatter',
|
|
15
|
+
'plot_color_histogram',
|
|
16
|
+
'plot_color_correlation',
|
|
17
|
+
'calculate_hue_index',
|
|
18
|
+
'plot_dark_threshold',
|
|
19
|
+
'plot_tissue_colors']
|
|
20
|
+
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
# traitly/fruit_phenotyping/analysis_parameters.py
|
|
2
|
+
"""
|
|
3
|
+
Analysis metadata tracking for traitly `FruitInternalAnalyzer` and
|
|
4
|
+
`FruitExternalAnalyzer` pipelines.
|
|
5
|
+
|
|
6
|
+
Provides the :class:`AnalysisParameters` dataclass for capturing and
|
|
7
|
+
exporting the processing parameters used in each stage of the fruit
|
|
8
|
+
phenotyping pipeline, supporting reproducibility and traceability.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
# ============================================================================
|
|
12
|
+
# STANDARD LIBRARY
|
|
13
|
+
# ============================================================================
|
|
14
|
+
import importlib.metadata
|
|
15
|
+
import json
|
|
16
|
+
import sys
|
|
17
|
+
from dataclasses import asdict, dataclass, field
|
|
18
|
+
from datetime import datetime
|
|
19
|
+
from typing import Any, Dict
|
|
20
|
+
|
|
21
|
+
# ============================================================================
|
|
22
|
+
# INTERNAL IMPORTS
|
|
23
|
+
# ============================================================================
|
|
24
|
+
from traitly import __version__
|
|
25
|
+
|
|
26
|
+
# ============================================================================
|
|
27
|
+
# Save parameters used and session information as txt and json files
|
|
28
|
+
# ============================================================================
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass
|
|
32
|
+
class AnalysisParameters:
|
|
33
|
+
"""
|
|
34
|
+
Save and report processing parameters for each analysis step.
|
|
35
|
+
|
|
36
|
+
Captures the parameters used in each stage of the analysis pipeline
|
|
37
|
+
and provides methods to export them as formatted text or JSON for
|
|
38
|
+
reproducibility and traceability.
|
|
39
|
+
|
|
40
|
+
Attributes
|
|
41
|
+
----------
|
|
42
|
+
setup_measurements_params : Dict[str, Any]
|
|
43
|
+
Parameters used in the setup measurements step.
|
|
44
|
+
generate_fruit_mask_params : Dict[str, Any]
|
|
45
|
+
Parameters used in the fruit mask generation step.
|
|
46
|
+
enhance_locule_contrast_params : Dict[str, Any]
|
|
47
|
+
Parameters used in the locule contrast enhancement step.
|
|
48
|
+
generate_locule_mask_params : Dict[str, Any]
|
|
49
|
+
Parameters used in the locule mask generation step.
|
|
50
|
+
detect_fruits_params : Dict[str, Any]
|
|
51
|
+
Parameters used in the fruit detection step.
|
|
52
|
+
analyze_morphology_params : Dict[str, Any]
|
|
53
|
+
Parameters used in the morphology analysis step.
|
|
54
|
+
analyze_color_params : Dict[str, Any]
|
|
55
|
+
Parameters used in the color analysis step.
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
# Create a dictionary for each step
|
|
59
|
+
img_params: Dict[str, Any] = field(default_factory=dict)
|
|
60
|
+
setup_measurements_params: Dict[str, Any] = field(default_factory=dict)
|
|
61
|
+
generate_fruit_mask_params: Dict[str, Any] = field(default_factory=dict)
|
|
62
|
+
enhance_locule_contrast_params: Dict[str, Any] = field(default_factory=dict)
|
|
63
|
+
generate_locule_mask_params: Dict[str, Any] = field(default_factory=dict)
|
|
64
|
+
detect_fruits_params: Dict[str, Any] = field(default_factory=dict)
|
|
65
|
+
analyze_morphology_params: Dict[str, Any] = field(default_factory=dict)
|
|
66
|
+
analyze_color_params: Dict[str, Any] = field(default_factory=dict)
|
|
67
|
+
|
|
68
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
69
|
+
"""
|
|
70
|
+
Convert dataclass fields to a dictionary.
|
|
71
|
+
|
|
72
|
+
Returns
|
|
73
|
+
-------
|
|
74
|
+
Dict[str, Any]
|
|
75
|
+
Dictionary representation of all analysis parameter fields.
|
|
76
|
+
"""
|
|
77
|
+
return asdict(self)
|
|
78
|
+
|
|
79
|
+
def to_formatted_string(self) -> str:
|
|
80
|
+
"""
|
|
81
|
+
Return analysis parameters as a readable formatted string.
|
|
82
|
+
|
|
83
|
+
Includes version info, run date, per-step parameters, and
|
|
84
|
+
dependency versions.
|
|
85
|
+
|
|
86
|
+
Returns
|
|
87
|
+
-------
|
|
88
|
+
str
|
|
89
|
+
Formatted string with all processing parameters and metadata.
|
|
90
|
+
"""
|
|
91
|
+
|
|
92
|
+
date = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
93
|
+
|
|
94
|
+
lines = [
|
|
95
|
+
"=" * 70,
|
|
96
|
+
"ANALYSIS PROCESSING PARAMETERS",
|
|
97
|
+
"=" * 70,
|
|
98
|
+
f"traitly: v{__version__}",
|
|
99
|
+
f"run date: {date}",
|
|
100
|
+
f"image: {self.img_params.get('img_path', 'not set')}",
|
|
101
|
+
]
|
|
102
|
+
|
|
103
|
+
sections = {
|
|
104
|
+
"SETUP_MEASUREMENTS": self.setup_measurements_params,
|
|
105
|
+
"GENERATE_FRUIT_MASK": self.generate_fruit_mask_params,
|
|
106
|
+
"ENHANCE_LOCULE_CONTRAST": self.enhance_locule_contrast_params,
|
|
107
|
+
"GENERATE_LOCULE_MASK": self.generate_locule_mask_params,
|
|
108
|
+
"DETECT_FRUITS": self.detect_fruits_params,
|
|
109
|
+
"ANALYZE_MORPHOLOGY": self.analyze_morphology_params,
|
|
110
|
+
"ANALYZE_COLOR": self.analyze_color_params,
|
|
111
|
+
}
|
|
112
|
+
for title, params in sections.items():
|
|
113
|
+
if params:
|
|
114
|
+
lines.append(f"\n{title}:")
|
|
115
|
+
for key, value in params.items():
|
|
116
|
+
lines.append(f" - {key}: {value}")
|
|
117
|
+
|
|
118
|
+
lines.append("\n" + "=" * 70)
|
|
119
|
+
|
|
120
|
+
# Version summary
|
|
121
|
+
lines.append("\nDEPENDENCIES:")
|
|
122
|
+
for pkg, version in self.get_package_versions().items():
|
|
123
|
+
lines.append(f" - {pkg}: {version}")
|
|
124
|
+
|
|
125
|
+
return "\n".join(lines)
|
|
126
|
+
|
|
127
|
+
def save_to_file(self, filepath: str) -> None:
|
|
128
|
+
"""
|
|
129
|
+
Save formatted parameters to a plain text file.
|
|
130
|
+
|
|
131
|
+
Parameters
|
|
132
|
+
----------
|
|
133
|
+
filepath : str
|
|
134
|
+
Destination path for the output .txt file.
|
|
135
|
+
|
|
136
|
+
Raises
|
|
137
|
+
------
|
|
138
|
+
OSError
|
|
139
|
+
If the file cannot be created or written to ``filepath``.
|
|
140
|
+
"""
|
|
141
|
+
with open(filepath, "w", encoding="utf-8") as f:
|
|
142
|
+
f.write(self.to_formatted_string())
|
|
143
|
+
|
|
144
|
+
def save_to_json(self, filepath: str) -> None:
|
|
145
|
+
"""
|
|
146
|
+
Save parameters to a JSON file.
|
|
147
|
+
|
|
148
|
+
Parameters
|
|
149
|
+
----------
|
|
150
|
+
filepath : str
|
|
151
|
+
Destination path for the output .json file.
|
|
152
|
+
|
|
153
|
+
Raises
|
|
154
|
+
------
|
|
155
|
+
OSError
|
|
156
|
+
If the file cannot be created or written to ``filepath``.
|
|
157
|
+
"""
|
|
158
|
+
data = self.to_dict()
|
|
159
|
+
|
|
160
|
+
with open(filepath, "w", encoding="utf-8") as f:
|
|
161
|
+
json.dump(data, f, indent=2, ensure_ascii=False)
|
|
162
|
+
|
|
163
|
+
def get_package_versions(self) -> Dict[str, str]:
|
|
164
|
+
"""
|
|
165
|
+
Return installed versions of key dependencies used by traitly.
|
|
166
|
+
|
|
167
|
+
Returns
|
|
168
|
+
-------
|
|
169
|
+
Dict[str, str]
|
|
170
|
+
Dictionary mapping package names to their installed version
|
|
171
|
+
strings. Includes Python version under the key ``'python'``.
|
|
172
|
+
Packages not found are reported as ``'not installed'``.
|
|
173
|
+
"""
|
|
174
|
+
packages = [
|
|
175
|
+
"opencv-contrib-python",
|
|
176
|
+
"numpy",
|
|
177
|
+
"pandas",
|
|
178
|
+
"scipy",
|
|
179
|
+
"matplotlib",
|
|
180
|
+
"tqdm",
|
|
181
|
+
"psutil",
|
|
182
|
+
"easyocr",
|
|
183
|
+
"PyMuPDF",
|
|
184
|
+
"ultralytics",
|
|
185
|
+
]
|
|
186
|
+
versions = {"python": sys.version.split()[0]}
|
|
187
|
+
for pkg in packages:
|
|
188
|
+
try:
|
|
189
|
+
versions[pkg] = importlib.metadata.version(pkg)
|
|
190
|
+
except importlib.metadata.PackageNotFoundError:
|
|
191
|
+
versions[pkg] = "not installed"
|
|
192
|
+
return versions
|