descriptron-core 2.0.3__tar.gz → 2.1.0__tar.gz
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.
- {descriptron_core-2.0.3 → descriptron_core-2.1.0}/PKG-INFO +1 -1
- descriptron_core-2.1.0/pyproject.toml +82 -0
- descriptron_core-2.1.0/pyproject.toml.bak_pre_204_20260924_165335 +82 -0
- descriptron_core-2.1.0/pyproject.toml.bak_pre_210_20260925_103754 +82 -0
- descriptron_core-2.1.0/src/descriptron_core/cli.py +99 -0
- {descriptron_core-2.0.3 → descriptron_core-2.1.0}/src/descriptron_core/tools/biorag_llm_backend.py +6 -0
- descriptron_core-2.1.0/src/descriptron_core/tools/coco_combiner_V13.py +306 -0
- descriptron_core-2.1.0/src/descriptron_core/tools/coco_converter_v24.py +447 -0
- descriptron_core-2.1.0/src/descriptron_core/tools/descriptron_centerline.py +377 -0
- descriptron_core-2.1.0/src/descriptron_core/tools/descriptron_coco_tools.py +326 -0
- descriptron_core-2.1.0/src/descriptron_core/tools/descriptron_convert.py +734 -0
- descriptron_core-2.1.0/src/descriptron_core/tools/descriptron_credentials.py +178 -0
- descriptron_core-2.1.0/src/descriptron_core/tools/descriptron_joints.py +204 -0
- descriptron_core-2.1.0/src/descriptron_core/tools/descriptron_metadata.py +230 -0
- descriptron_core-2.1.0/src/descriptron_core/tools/descriptron_shape_stats.py +716 -0
- descriptron_core-2.1.0/src/descriptron_core/tools/landmark_gpa_V2.py +756 -0
- {descriptron_core-2.0.3 → descriptron_core-2.1.0}/src/descriptron_core/tools/run_full_pipeline_v2.py +27 -3
- descriptron_core-2.1.0/src/descriptron_core/tools/semi_landmark_and_kpts_procrustesV42_GPA.py +2388 -0
- descriptron_core-2.1.0/src/descriptron_core/tools/validate_pipeline_gm_vs_geomorph.py +220 -0
- descriptron_core-2.1.0/src/descriptron_core/tools/validate_semilandmarks_vs_geomorph.py +164 -0
- descriptron_core-2.1.0/src/descriptron_core/tools/validate_shape_stats_vs_geomorph.py +387 -0
- descriptron_core-2.1.0/src/descriptron_core/tools/validate_shape_stats_vs_rrpp.py +244 -0
- {descriptron_core-2.0.3 → descriptron_core-2.1.0}/.gitignore +0 -0
- {descriptron_core-2.0.3 → descriptron_core-2.1.0}/LICENSE +0 -0
- {descriptron_core-2.0.3 → descriptron_core-2.1.0}/NOTICE +0 -0
- {descriptron_core-2.0.3 → descriptron_core-2.1.0}/README.md +0 -0
- {descriptron_core-2.0.3 → descriptron_core-2.1.0}/pyproject.toml.bak_pre_203_20260924_145320 +0 -0
- /descriptron_core-2.0.3/pyproject.toml → /descriptron_core-2.1.0/pyproject.toml.bak_pre_keys_20260924_165335 +0 -0
- {descriptron_core-2.0.3 → descriptron_core-2.1.0}/src/descriptron_core/__init__.py +0 -0
- /descriptron_core-2.0.3/src/descriptron_core/cli.py → /descriptron_core-2.1.0/src/descriptron_core/cli.py.bak_pre_keys_20260924_165335 +0 -0
- {descriptron_core-2.0.3 → descriptron_core-2.1.0}/src/descriptron_core/data/biorag_prompts/biorag_system_prompts_v1.txt +0 -0
- {descriptron_core-2.0.3 → descriptron_core-2.1.0}/src/descriptron_core/data/biorag_prompts/biorag_system_prompts_v2.txt +0 -0
- {descriptron_core-2.0.3 → descriptron_core-2.1.0}/src/descriptron_core/data/biorag_prompts/diaphorina_user_prompts.txt +0 -0
- {descriptron_core-2.0.3 → descriptron_core-2.1.0}/src/descriptron_core/data/biorag_prompts/ontologies/aism.obo +0 -0
- {descriptron_core-2.0.3 → descriptron_core-2.1.0}/src/descriptron_core/data/biorag_prompts/ontologies/pato.obo +0 -0
- {descriptron_core-2.0.3 → descriptron_core-2.1.0}/src/descriptron_core/data/biorag_prompts/schemas/sdd_1.1/SDD.xsd +0 -0
- {descriptron_core-2.0.3 → descriptron_core-2.1.0}/src/descriptron_core/data/biorag_prompts/schemas/sdd_1.1/SDD_EnumLib.xsd +0 -0
- {descriptron_core-2.0.3 → descriptron_core-2.1.0}/src/descriptron_core/data/biorag_prompts/schemas/sdd_1.1/SDD_TypeLib.xsd +0 -0
- {descriptron_core-2.0.3 → descriptron_core-2.1.0}/src/descriptron_core/data/biorag_prompts/schemas/sdd_1.1/UBIF_BasePattern.xsd +0 -0
- {descriptron_core-2.0.3 → descriptron_core-2.1.0}/src/descriptron_core/data/biorag_prompts/schemas/sdd_1.1/UBIF_CoreExtensions.xsd +0 -0
- {descriptron_core-2.0.3 → descriptron_core-2.1.0}/src/descriptron_core/data/biorag_prompts/schemas/sdd_1.1/UBIF_CoreOntology.xsd +0 -0
- {descriptron_core-2.0.3 → descriptron_core-2.1.0}/src/descriptron_core/data/biorag_prompts/schemas/sdd_1.1/UBIF_EnumLib.xsd +0 -0
- {descriptron_core-2.0.3 → descriptron_core-2.1.0}/src/descriptron_core/data/biorag_prompts/schemas/sdd_1.1/UBIF_LinneanCore.xsd +0 -0
- {descriptron_core-2.0.3 → descriptron_core-2.1.0}/src/descriptron_core/data/biorag_prompts/schemas/sdd_1.1/UBIF_TypeLib.xsd +0 -0
- {descriptron_core-2.0.3 → descriptron_core-2.1.0}/src/descriptron_core/data/biorag_prompts/schemas/sdd_1.1/w3c-schemata/xml.xsd +0 -0
- {descriptron_core-2.0.3 → descriptron_core-2.1.0}/src/descriptron_core/data/biorag_prompts/schemas/taxpub/sample_treatment_deposit_example.xml +0 -0
- {descriptron_core-2.0.3 → descriptron_core-2.1.0}/src/descriptron_core/data/biorag_prompts/schemas/taxpub/sample_zookeys_26056_tp.xml +0 -0
- {descriptron_core-2.0.3 → descriptron_core-2.1.0}/src/descriptron_core/data/biorag_prompts/schemas/taxpub/tax-treatment-NS0-v1_flat.dtd +0 -0
- {descriptron_core-2.0.3 → descriptron_core-2.1.0}/src/descriptron_core/data/biorag_prompts/taxon_profiles/TEMPLATE_taxon_profile.yaml +0 -0
- {descriptron_core-2.0.3 → descriptron_core-2.1.0}/src/descriptron_core/data/biorag_prompts/taxon_profiles/diaphorina_taxon_profile.yaml +0 -0
- {descriptron_core-2.0.3 → descriptron_core-2.1.0}/src/descriptron_core/tools/FHS_and_CLAHE_V21.py +0 -0
- {descriptron_core-2.0.3 → descriptron_core-2.1.0}/src/descriptron_core/tools/biorag_add_discrete_characters_v1.py +0 -0
- {descriptron_core-2.0.3 → descriptron_core-2.1.0}/src/descriptron_core/tools/biorag_annotation_reorient_v1.py +0 -0
- {descriptron_core-2.0.3 → descriptron_core-2.1.0}/src/descriptron_core/tools/biorag_annotation_screen_v1.py +0 -0
- {descriptron_core-2.0.3 → descriptron_core-2.1.0}/src/descriptron_core/tools/biorag_autapomorphy_v1.py +0 -0
- {descriptron_core-2.0.3 → descriptron_core-2.1.0}/src/descriptron_core/tools/biorag_calibrate_v1.py +0 -0
- {descriptron_core-2.0.3 → descriptron_core-2.1.0}/src/descriptron_core/tools/biorag_calibration_figures_v1.py +0 -0
- {descriptron_core-2.0.3 → descriptron_core-2.1.0}/src/descriptron_core/tools/biorag_character_gate_v1.py +0 -0
- {descriptron_core-2.0.3 → descriptron_core-2.1.0}/src/descriptron_core/tools/biorag_character_reliability_v1.py +0 -0
- {descriptron_core-2.0.3 → descriptron_core-2.1.0}/src/descriptron_core/tools/biorag_character_robustness_v1.py +0 -0
- {descriptron_core-2.0.3 → descriptron_core-2.1.0}/src/descriptron_core/tools/biorag_collaborator_forms_v1.py +0 -0
- {descriptron_core-2.0.3 → descriptron_core-2.1.0}/src/descriptron_core/tools/biorag_computed_sets_v1.py +0 -0
- {descriptron_core-2.0.3 → descriptron_core-2.1.0}/src/descriptron_core/tools/biorag_confabulation_checker_v2.py +0 -0
- {descriptron_core-2.0.3 → descriptron_core-2.1.0}/src/descriptron_core/tools/biorag_congruence_compare_v1.py +0 -0
- {descriptron_core-2.0.3 → descriptron_core-2.1.0}/src/descriptron_core/tools/biorag_description_refiner_v1.py +0 -0
- {descriptron_core-2.0.3 → descriptron_core-2.1.0}/src/descriptron_core/tools/biorag_descriptive_scoring_v1.py +0 -0
- {descriptron_core-2.0.3 → descriptron_core-2.1.0}/src/descriptron_core/tools/biorag_feature_policy.py +0 -0
- {descriptron_core-2.0.3 → descriptron_core-2.1.0}/src/descriptron_core/tools/biorag_frame_orientation_calibrate_v1.py +0 -0
- {descriptron_core-2.0.3 → descriptron_core-2.1.0}/src/descriptron_core/tools/biorag_frame_orientation_v1.py +0 -0
- {descriptron_core-2.0.3 → descriptron_core-2.1.0}/src/descriptron_core/tools/biorag_gate_compare_v1.py +0 -0
- {descriptron_core-2.0.3 → descriptron_core-2.1.0}/src/descriptron_core/tools/biorag_graph_identify_v1.py +0 -0
- {descriptron_core-2.0.3 → descriptron_core-2.1.0}/src/descriptron_core/tools/biorag_graph_support_figure_v1.py +0 -0
- {descriptron_core-2.0.3 → descriptron_core-2.1.0}/src/descriptron_core/tools/biorag_homology_frame_v1.py +0 -0
- {descriptron_core-2.0.3 → descriptron_core-2.1.0}/src/descriptron_core/tools/biorag_instrument_compare_v3.py +0 -0
- {descriptron_core-2.0.3 → descriptron_core-2.1.0}/src/descriptron_core/tools/biorag_key_builder_v1.py +0 -0
- {descriptron_core-2.0.3 → descriptron_core-2.1.0}/src/descriptron_core/tools/biorag_key_feature_filter_v2.py +0 -0
- {descriptron_core-2.0.3 → descriptron_core-2.1.0}/src/descriptron_core/tools/biorag_key_fuzzy_figures_v1.py +0 -0
- {descriptron_core-2.0.3 → descriptron_core-2.1.0}/src/descriptron_core/tools/biorag_key_fuzzy_v1.py +0 -0
- {descriptron_core-2.0.3 → descriptron_core-2.1.0}/src/descriptron_core/tools/biorag_key_holdout_v1.py +0 -0
- {descriptron_core-2.0.3 → descriptron_core-2.1.0}/src/descriptron_core/tools/biorag_key_jackknife_support_v1.py +0 -0
- {descriptron_core-2.0.3 → descriptron_core-2.1.0}/src/descriptron_core/tools/biorag_label_conflict_check_v1.py +0 -0
- {descriptron_core-2.0.3 → descriptron_core-2.1.0}/src/descriptron_core/tools/biorag_make_taxon_profile_v1.py +0 -0
- {descriptron_core-2.0.3 → descriptron_core-2.1.0}/src/descriptron_core/tools/biorag_methods_summary_figure_v1.py +0 -0
- {descriptron_core-2.0.3 → descriptron_core-2.1.0}/src/descriptron_core/tools/biorag_novelty_score_v1.py +0 -0
- {descriptron_core-2.0.3 → descriptron_core-2.1.0}/src/descriptron_core/tools/biorag_ontology_annotator_v2.py +0 -0
- {descriptron_core-2.0.3 → descriptron_core-2.1.0}/src/descriptron_core/tools/biorag_outgroup_batch_check_v1.py +0 -0
- {descriptron_core-2.0.3 → descriptron_core-2.1.0}/src/descriptron_core/tools/biorag_outline_shape_v1.py +0 -0
- {descriptron_core-2.0.3 → descriptron_core-2.1.0}/src/descriptron_core/tools/biorag_prompts/biorag_system_prompts_v1.txt +0 -0
- {descriptron_core-2.0.3 → descriptron_core-2.1.0}/src/descriptron_core/tools/biorag_prompts/biorag_system_prompts_v2.txt +0 -0
- {descriptron_core-2.0.3 → descriptron_core-2.1.0}/src/descriptron_core/tools/biorag_prompts/diaphorina_user_prompts.txt +0 -0
- {descriptron_core-2.0.3 → descriptron_core-2.1.0}/src/descriptron_core/tools/biorag_prompts/ontologies/aism.obo +0 -0
- {descriptron_core-2.0.3 → descriptron_core-2.1.0}/src/descriptron_core/tools/biorag_prompts/ontologies/pato.obo +0 -0
- {descriptron_core-2.0.3 → descriptron_core-2.1.0}/src/descriptron_core/tools/biorag_prompts/schemas/sdd_1.1/SDD.xsd +0 -0
- {descriptron_core-2.0.3 → descriptron_core-2.1.0}/src/descriptron_core/tools/biorag_prompts/schemas/sdd_1.1/SDD_EnumLib.xsd +0 -0
- {descriptron_core-2.0.3 → descriptron_core-2.1.0}/src/descriptron_core/tools/biorag_prompts/schemas/sdd_1.1/SDD_TypeLib.xsd +0 -0
- {descriptron_core-2.0.3 → descriptron_core-2.1.0}/src/descriptron_core/tools/biorag_prompts/schemas/sdd_1.1/UBIF_BasePattern.xsd +0 -0
- {descriptron_core-2.0.3 → descriptron_core-2.1.0}/src/descriptron_core/tools/biorag_prompts/schemas/sdd_1.1/UBIF_CoreExtensions.xsd +0 -0
- {descriptron_core-2.0.3 → descriptron_core-2.1.0}/src/descriptron_core/tools/biorag_prompts/schemas/sdd_1.1/UBIF_CoreOntology.xsd +0 -0
- {descriptron_core-2.0.3 → descriptron_core-2.1.0}/src/descriptron_core/tools/biorag_prompts/schemas/sdd_1.1/UBIF_EnumLib.xsd +0 -0
- {descriptron_core-2.0.3 → descriptron_core-2.1.0}/src/descriptron_core/tools/biorag_prompts/schemas/sdd_1.1/UBIF_LinneanCore.xsd +0 -0
- {descriptron_core-2.0.3 → descriptron_core-2.1.0}/src/descriptron_core/tools/biorag_prompts/schemas/sdd_1.1/UBIF_TypeLib.xsd +0 -0
- {descriptron_core-2.0.3 → descriptron_core-2.1.0}/src/descriptron_core/tools/biorag_prompts/schemas/sdd_1.1/w3c-schemata/xml.xsd +0 -0
- {descriptron_core-2.0.3 → descriptron_core-2.1.0}/src/descriptron_core/tools/biorag_prompts/schemas/taxpub/sample_treatment_deposit_example.xml +0 -0
- {descriptron_core-2.0.3 → descriptron_core-2.1.0}/src/descriptron_core/tools/biorag_prompts/schemas/taxpub/sample_zookeys_26056_tp.xml +0 -0
- {descriptron_core-2.0.3 → descriptron_core-2.1.0}/src/descriptron_core/tools/biorag_prompts/schemas/taxpub/tax-treatment-NS0-v1_flat.dtd +0 -0
- {descriptron_core-2.0.3 → descriptron_core-2.1.0}/src/descriptron_core/tools/biorag_prompts/taxon_profiles/TEMPLATE_taxon_profile.yaml +0 -0
- {descriptron_core-2.0.3 → descriptron_core-2.1.0}/src/descriptron_core/tools/biorag_prompts/taxon_profiles/diaphorina_taxon_profile.yaml +0 -0
- {descriptron_core-2.0.3 → descriptron_core-2.1.0}/src/descriptron_core/tools/biorag_rebuild_downstream_v1.sh +0 -0
- {descriptron_core-2.0.3 → descriptron_core-2.1.0}/src/descriptron_core/tools/biorag_reliability_figures_v1.py +0 -0
- {descriptron_core-2.0.3 → descriptron_core-2.1.0}/src/descriptron_core/tools/biorag_rename_species_v1.py +0 -0
- {descriptron_core-2.0.3 → descriptron_core-2.1.0}/src/descriptron_core/tools/biorag_rerun_descriptive_tables_v1.sh +0 -0
- {descriptron_core-2.0.3 → descriptron_core-2.1.0}/src/descriptron_core/tools/biorag_retest_compare_v1.py +0 -0
- {descriptron_core-2.0.3 → descriptron_core-2.1.0}/src/descriptron_core/tools/biorag_specimen_id.py +0 -0
- {descriptron_core-2.0.3 → descriptron_core-2.1.0}/src/descriptron_core/tools/biorag_subjective_checks_v1.py +0 -0
- {descriptron_core-2.0.3 → descriptron_core-2.1.0}/src/descriptron_core/tools/biorag_type_material_v1.py +0 -0
- {descriptron_core-2.0.3 → descriptron_core-2.1.0}/src/descriptron_core/tools/biorag_vlm_characters_v1.py +0 -0
- {descriptron_core-2.0.3 → descriptron_core-2.1.0}/src/descriptron_core/tools/biorag_vlm_heatmap_figures_v1.py +0 -0
- {descriptron_core-2.0.3 → descriptron_core-2.1.0}/src/descriptron_core/tools/biorag_vlm_reliability_figure_v1.py +0 -0
- {descriptron_core-2.0.3 → descriptron_core-2.1.0}/src/descriptron_core/tools/biorag_vlm_roi_figures_v1.py +0 -0
- {descriptron_core-2.0.3 → descriptron_core-2.1.0}/src/descriptron_core/tools/biosyslit_rag_retrieval.py +0 -0
- {descriptron_core-2.0.3 → descriptron_core-2.1.0}/src/descriptron_core/tools/biosyslit_rag_retrieval_v2.py +0 -0
- {descriptron_core-2.0.3 → descriptron_core-2.1.0}/src/descriptron_core/tools/build_monograph_exports_v1.py +0 -0
- {descriptron_core-2.0.3 → descriptron_core-2.1.0}/src/descriptron_core/tools/build_species_treatment_docx_v2.py +0 -0
- {descriptron_core-2.0.3 → descriptron_core-2.1.0}/src/descriptron_core/tools/color_extraction_with_names-v8.py +0 -0
- {descriptron_core-2.0.3 → descriptron_core-2.1.0}/src/descriptron_core/tools/color_phenomics_homology_v2_1.py +0 -0
- {descriptron_core-2.0.3 → descriptron_core-2.1.0}/src/descriptron_core/tools/compile_specimen_data.py +0 -0
- {descriptron_core-2.0.3 → descriptron_core-2.1.0}/src/descriptron_core/tools/descriptron_rosetta.py +0 -0
- {descriptron_core-2.0.3 → descriptron_core-2.1.0}/src/descriptron_core/tools/landmark_gpa_V1.py +0 -0
- {descriptron_core-2.0.3 → descriptron_core-2.1.0}/src/descriptron_core/tools/make_reproducibility_bundle.py +0 -0
- {descriptron_core-2.0.3 → descriptron_core-2.1.0}/src/descriptron_core/tools/semi_landmark_and_kpts_procrustesV34_GPA.py +0 -0
- {descriptron_core-2.0.3 → descriptron_core-2.1.0}/src/descriptron_core/tools/texture_phenomics_from_coco.py +0 -0
- {descriptron_core-2.0.3 → descriptron_core-2.1.0}/src/descriptron_core/tools/texture_phenomics_homology.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: descriptron-core
|
|
3
|
-
Version: 2.0
|
|
3
|
+
Version: 2.1.0
|
|
4
4
|
Summary: Evidence-tiered species descriptions, keys and audits from phenomic matrices
|
|
5
5
|
Project-URL: Homepage, https://github.com/alexrvandam/Descriptron
|
|
6
6
|
Project-URL: Repository, https://github.com/alexrvandam/Descriptron
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "descriptron-core"
|
|
7
|
+
version = "2.1.0"
|
|
8
|
+
description = "Evidence-tiered species descriptions, keys and audits from phenomic matrices"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = { text = "Apache-2.0" }
|
|
12
|
+
authors = [{ name = "Alex R. Van Dam" }]
|
|
13
|
+
keywords = ["taxonomy", "morphometrics", "phenomics", "species description", "COCO"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"Topic :: Scientific/Engineering :: Bio-Informatics",
|
|
17
|
+
"Topic :: Scientific/Engineering :: Image Processing",
|
|
18
|
+
"Intended Audience :: Science/Research",
|
|
19
|
+
"License :: OSI Approved :: Apache Software License",
|
|
20
|
+
"Operating System :: OS Independent",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
# Derived from the imports of the 62 programs in this package, not from memory:
|
|
24
|
+
# `python -m descriptron_core.cli --list` names them. No torch, no CUDA, no
|
|
25
|
+
# compiler — this installs in about a minute on Linux, macOS and Windows, and
|
|
26
|
+
# with the deposited COCO files it reproduces every analysis in the methods paper.
|
|
27
|
+
dependencies = [
|
|
28
|
+
"numpy>=1.23",
|
|
29
|
+
"pandas>=1.5",
|
|
30
|
+
"scipy>=1.9",
|
|
31
|
+
"matplotlib>=3.6",
|
|
32
|
+
"scikit-learn>=1.2",
|
|
33
|
+
"scikit-image>=0.20",
|
|
34
|
+
"opencv-python-headless>=4.7", # headless: the analysis half never opens a window
|
|
35
|
+
"pillow>=9",
|
|
36
|
+
"pycocotools>=2.0.6",
|
|
37
|
+
"seaborn>=0.12",
|
|
38
|
+
"statsmodels>=0.14",
|
|
39
|
+
"scikit-posthocs>=0.7",
|
|
40
|
+
"umap-learn>=0.5",
|
|
41
|
+
"networkx>=3",
|
|
42
|
+
"pyefd>=1.6",
|
|
43
|
+
"biopython>=1.81",
|
|
44
|
+
"PyYAML>=6",
|
|
45
|
+
"lxml>=4.9",
|
|
46
|
+
"openpyxl>=3.1",
|
|
47
|
+
"python-docx>=1.0", # treatment and monograph .docx export
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
[project.optional-dependencies]
|
|
51
|
+
# Only needed to *write* treatments. The audit, key, matrix, novelty scoring and
|
|
52
|
+
# delimitation are all deterministic and need no model at all.
|
|
53
|
+
llm = ["anthropic>=0.40"]
|
|
54
|
+
# literature retrieval (the RAG in BioRAG): BioSysLit treatments from Zenodo and your
|
|
55
|
+
# own PDFs. Keyword retrieval needs only the first two; sentence-embedding retrieval
|
|
56
|
+
# (`index --embeddings`) also needs sentence-transformers + faiss-cpu, which pull in torch.
|
|
57
|
+
rag = ["requests>=2.28", "pdfplumber>=0.10"]
|
|
58
|
+
rag-embeddings = ["requests>=2.28", "pdfplumber>=0.10", "sentence-transformers>=2.2", "faiss-cpu>=1.7"]
|
|
59
|
+
dev = ["pytest>=7", "build>=1", "twine>=5"]
|
|
60
|
+
|
|
61
|
+
[project.scripts]
|
|
62
|
+
descriptron = "descriptron_core.cli:main"
|
|
63
|
+
biorag-pipeline = "descriptron_core.cli:run_pipeline"
|
|
64
|
+
biorag-key = "descriptron_core.cli:run_key_builder"
|
|
65
|
+
biorag-audit = "descriptron_core.cli:run_confabulation_checker"
|
|
66
|
+
biorag-novelty = "descriptron_core.cli:run_novelty_score"
|
|
67
|
+
biorag-calibrate = "descriptron_core.cli:run_calibrate"
|
|
68
|
+
descriptron-keys = "descriptron_core.cli:run_keys"
|
|
69
|
+
|
|
70
|
+
[project.urls]
|
|
71
|
+
Homepage = "https://github.com/alexrvandam/Descriptron"
|
|
72
|
+
Repository = "https://github.com/alexrvandam/Descriptron"
|
|
73
|
+
Issues = "https://github.com/alexrvandam/Descriptron/issues"
|
|
74
|
+
|
|
75
|
+
[tool.hatch.build.targets.wheel]
|
|
76
|
+
packages = ["src/descriptron_core"]
|
|
77
|
+
|
|
78
|
+
# tools/ and data/ sit inside the package directory, so `packages` already ships
|
|
79
|
+
# them; a force-include here would add every file a second time and the build
|
|
80
|
+
# fails with "a second file is being added to the wheel archive at the same path".
|
|
81
|
+
[tool.hatch.build]
|
|
82
|
+
artifacts = ["src/descriptron_core/tools/**", "src/descriptron_core/data/**"]
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "descriptron-core"
|
|
7
|
+
version = "2.0.3"
|
|
8
|
+
description = "Evidence-tiered species descriptions, keys and audits from phenomic matrices"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = { text = "Apache-2.0" }
|
|
12
|
+
authors = [{ name = "Alex R. Van Dam" }]
|
|
13
|
+
keywords = ["taxonomy", "morphometrics", "phenomics", "species description", "COCO"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"Topic :: Scientific/Engineering :: Bio-Informatics",
|
|
17
|
+
"Topic :: Scientific/Engineering :: Image Processing",
|
|
18
|
+
"Intended Audience :: Science/Research",
|
|
19
|
+
"License :: OSI Approved :: Apache Software License",
|
|
20
|
+
"Operating System :: OS Independent",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
# Derived from the imports of the 62 programs in this package, not from memory:
|
|
24
|
+
# `python -m descriptron_core.cli --list` names them. No torch, no CUDA, no
|
|
25
|
+
# compiler — this installs in about a minute on Linux, macOS and Windows, and
|
|
26
|
+
# with the deposited COCO files it reproduces every analysis in the methods paper.
|
|
27
|
+
dependencies = [
|
|
28
|
+
"numpy>=1.23",
|
|
29
|
+
"pandas>=1.5",
|
|
30
|
+
"scipy>=1.9",
|
|
31
|
+
"matplotlib>=3.6",
|
|
32
|
+
"scikit-learn>=1.2",
|
|
33
|
+
"scikit-image>=0.20",
|
|
34
|
+
"opencv-python-headless>=4.7", # headless: the analysis half never opens a window
|
|
35
|
+
"pillow>=9",
|
|
36
|
+
"pycocotools>=2.0.6",
|
|
37
|
+
"seaborn>=0.12",
|
|
38
|
+
"statsmodels>=0.14",
|
|
39
|
+
"scikit-posthocs>=0.7",
|
|
40
|
+
"umap-learn>=0.5",
|
|
41
|
+
"networkx>=3",
|
|
42
|
+
"pyefd>=1.6",
|
|
43
|
+
"biopython>=1.81",
|
|
44
|
+
"PyYAML>=6",
|
|
45
|
+
"lxml>=4.9",
|
|
46
|
+
"openpyxl>=3.1",
|
|
47
|
+
"python-docx>=1.0", # treatment and monograph .docx export
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
[project.optional-dependencies]
|
|
51
|
+
# Only needed to *write* treatments. The audit, key, matrix, novelty scoring and
|
|
52
|
+
# delimitation are all deterministic and need no model at all.
|
|
53
|
+
llm = ["anthropic>=0.40"]
|
|
54
|
+
# literature retrieval (the RAG in BioRAG): BioSysLit treatments from Zenodo and your
|
|
55
|
+
# own PDFs. Keyword retrieval needs only the first two; sentence-embedding retrieval
|
|
56
|
+
# (`index --embeddings`) also needs sentence-transformers + faiss-cpu, which pull in torch.
|
|
57
|
+
rag = ["requests>=2.28", "pdfplumber>=0.10"]
|
|
58
|
+
rag-embeddings = ["requests>=2.28", "pdfplumber>=0.10", "sentence-transformers>=2.2", "faiss-cpu>=1.7"]
|
|
59
|
+
dev = ["pytest>=7", "build>=1", "twine>=5"]
|
|
60
|
+
|
|
61
|
+
[project.scripts]
|
|
62
|
+
descriptron = "descriptron_core.cli:main"
|
|
63
|
+
biorag-pipeline = "descriptron_core.cli:run_pipeline"
|
|
64
|
+
biorag-key = "descriptron_core.cli:run_key_builder"
|
|
65
|
+
biorag-audit = "descriptron_core.cli:run_confabulation_checker"
|
|
66
|
+
biorag-novelty = "descriptron_core.cli:run_novelty_score"
|
|
67
|
+
biorag-calibrate = "descriptron_core.cli:run_calibrate"
|
|
68
|
+
descriptron-keys = "descriptron_core.cli:run_keys"
|
|
69
|
+
|
|
70
|
+
[project.urls]
|
|
71
|
+
Homepage = "https://github.com/alexrvandam/Descriptron"
|
|
72
|
+
Repository = "https://github.com/alexrvandam/Descriptron"
|
|
73
|
+
Issues = "https://github.com/alexrvandam/Descriptron/issues"
|
|
74
|
+
|
|
75
|
+
[tool.hatch.build.targets.wheel]
|
|
76
|
+
packages = ["src/descriptron_core"]
|
|
77
|
+
|
|
78
|
+
# tools/ and data/ sit inside the package directory, so `packages` already ships
|
|
79
|
+
# them; a force-include here would add every file a second time and the build
|
|
80
|
+
# fails with "a second file is being added to the wheel archive at the same path".
|
|
81
|
+
[tool.hatch.build]
|
|
82
|
+
artifacts = ["src/descriptron_core/tools/**", "src/descriptron_core/data/**"]
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "descriptron-core"
|
|
7
|
+
version = "2.0.4"
|
|
8
|
+
description = "Evidence-tiered species descriptions, keys and audits from phenomic matrices"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = { text = "Apache-2.0" }
|
|
12
|
+
authors = [{ name = "Alex R. Van Dam" }]
|
|
13
|
+
keywords = ["taxonomy", "morphometrics", "phenomics", "species description", "COCO"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"Topic :: Scientific/Engineering :: Bio-Informatics",
|
|
17
|
+
"Topic :: Scientific/Engineering :: Image Processing",
|
|
18
|
+
"Intended Audience :: Science/Research",
|
|
19
|
+
"License :: OSI Approved :: Apache Software License",
|
|
20
|
+
"Operating System :: OS Independent",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
# Derived from the imports of the 62 programs in this package, not from memory:
|
|
24
|
+
# `python -m descriptron_core.cli --list` names them. No torch, no CUDA, no
|
|
25
|
+
# compiler — this installs in about a minute on Linux, macOS and Windows, and
|
|
26
|
+
# with the deposited COCO files it reproduces every analysis in the methods paper.
|
|
27
|
+
dependencies = [
|
|
28
|
+
"numpy>=1.23",
|
|
29
|
+
"pandas>=1.5",
|
|
30
|
+
"scipy>=1.9",
|
|
31
|
+
"matplotlib>=3.6",
|
|
32
|
+
"scikit-learn>=1.2",
|
|
33
|
+
"scikit-image>=0.20",
|
|
34
|
+
"opencv-python-headless>=4.7", # headless: the analysis half never opens a window
|
|
35
|
+
"pillow>=9",
|
|
36
|
+
"pycocotools>=2.0.6",
|
|
37
|
+
"seaborn>=0.12",
|
|
38
|
+
"statsmodels>=0.14",
|
|
39
|
+
"scikit-posthocs>=0.7",
|
|
40
|
+
"umap-learn>=0.5",
|
|
41
|
+
"networkx>=3",
|
|
42
|
+
"pyefd>=1.6",
|
|
43
|
+
"biopython>=1.81",
|
|
44
|
+
"PyYAML>=6",
|
|
45
|
+
"lxml>=4.9",
|
|
46
|
+
"openpyxl>=3.1",
|
|
47
|
+
"python-docx>=1.0", # treatment and monograph .docx export
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
[project.optional-dependencies]
|
|
51
|
+
# Only needed to *write* treatments. The audit, key, matrix, novelty scoring and
|
|
52
|
+
# delimitation are all deterministic and need no model at all.
|
|
53
|
+
llm = ["anthropic>=0.40"]
|
|
54
|
+
# literature retrieval (the RAG in BioRAG): BioSysLit treatments from Zenodo and your
|
|
55
|
+
# own PDFs. Keyword retrieval needs only the first two; sentence-embedding retrieval
|
|
56
|
+
# (`index --embeddings`) also needs sentence-transformers + faiss-cpu, which pull in torch.
|
|
57
|
+
rag = ["requests>=2.28", "pdfplumber>=0.10"]
|
|
58
|
+
rag-embeddings = ["requests>=2.28", "pdfplumber>=0.10", "sentence-transformers>=2.2", "faiss-cpu>=1.7"]
|
|
59
|
+
dev = ["pytest>=7", "build>=1", "twine>=5"]
|
|
60
|
+
|
|
61
|
+
[project.scripts]
|
|
62
|
+
descriptron = "descriptron_core.cli:main"
|
|
63
|
+
biorag-pipeline = "descriptron_core.cli:run_pipeline"
|
|
64
|
+
biorag-key = "descriptron_core.cli:run_key_builder"
|
|
65
|
+
biorag-audit = "descriptron_core.cli:run_confabulation_checker"
|
|
66
|
+
biorag-novelty = "descriptron_core.cli:run_novelty_score"
|
|
67
|
+
biorag-calibrate = "descriptron_core.cli:run_calibrate"
|
|
68
|
+
descriptron-keys = "descriptron_core.cli:run_keys"
|
|
69
|
+
|
|
70
|
+
[project.urls]
|
|
71
|
+
Homepage = "https://github.com/alexrvandam/Descriptron"
|
|
72
|
+
Repository = "https://github.com/alexrvandam/Descriptron"
|
|
73
|
+
Issues = "https://github.com/alexrvandam/Descriptron/issues"
|
|
74
|
+
|
|
75
|
+
[tool.hatch.build.targets.wheel]
|
|
76
|
+
packages = ["src/descriptron_core"]
|
|
77
|
+
|
|
78
|
+
# tools/ and data/ sit inside the package directory, so `packages` already ships
|
|
79
|
+
# them; a force-include here would add every file a second time and the build
|
|
80
|
+
# fails with "a second file is being added to the wheel archive at the same path".
|
|
81
|
+
[tool.hatch.build]
|
|
82
|
+
artifacts = ["src/descriptron_core/tools/**", "src/descriptron_core/data/**"]
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""
|
|
2
|
+
descriptron_core.cli — one entry point for the analysis programs
|
|
3
|
+
================================================================
|
|
4
|
+
|
|
5
|
+
The 62 programs in this package are run with `runpy` exactly as they are run
|
|
6
|
+
from a shell, rather than being imported as modules. That is deliberate: they
|
|
7
|
+
were written as scripts, they use `if __name__ == "__main__"`, and several
|
|
8
|
+
import their siblings by plain name. Executing them as `__main__` with their own
|
|
9
|
+
directory on `sys.path` preserves that behaviour exactly, so packaging cannot
|
|
10
|
+
quietly change what a program does — which matters when the output is a
|
|
11
|
+
published species description.
|
|
12
|
+
|
|
13
|
+
descriptron --list
|
|
14
|
+
descriptron biorag_key_builder_v1 --help
|
|
15
|
+
biorag-audit --help
|
|
16
|
+
"""
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import os
|
|
20
|
+
import runpy
|
|
21
|
+
import sys
|
|
22
|
+
from importlib.resources import files
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
|
|
25
|
+
__all__ = ["main", "tools_dir", "data_path", "available"]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def tools_dir() -> Path:
|
|
29
|
+
return Path(str(files("descriptron_core") / "tools"))
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def data_path(*parts: str) -> Path:
|
|
33
|
+
"""Prompts, taxon profiles, ontology releases and schemas that ship with the package."""
|
|
34
|
+
return Path(str(files("descriptron_core") / "data")).joinpath(*parts)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def available() -> list[str]:
|
|
38
|
+
d = tools_dir()
|
|
39
|
+
if not d.exists():
|
|
40
|
+
return []
|
|
41
|
+
return sorted(p.stem for p in d.iterdir()
|
|
42
|
+
if p.suffix in (".py", ".sh") and not p.name.startswith("_"))
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _resolve(name: str) -> Path:
|
|
46
|
+
d = tools_dir()
|
|
47
|
+
for candidate in (d / name, d / f"{name}.py", d / f"{name}.sh"):
|
|
48
|
+
if candidate.exists():
|
|
49
|
+
return candidate
|
|
50
|
+
matches = [t for t in available() if t.startswith(name)]
|
|
51
|
+
if len(matches) == 1:
|
|
52
|
+
return _resolve(matches[0])
|
|
53
|
+
if matches:
|
|
54
|
+
raise SystemExit(f"'{name}' is ambiguous: {', '.join(matches)}")
|
|
55
|
+
raise SystemExit(f"unknown program '{name}'. `descriptron --list` shows them all.")
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def run(name: str, argv: list[str] | None = None) -> None:
|
|
59
|
+
path = _resolve(name)
|
|
60
|
+
argv = list(sys.argv[1:] if argv is None else argv)
|
|
61
|
+
# the programs' own directory goes on sys.path so a script that imports a
|
|
62
|
+
# sibling by plain name keeps working, and the packaged prompts and ontology
|
|
63
|
+
# releases are findable without anyone passing a path
|
|
64
|
+
sys.path.insert(0, str(path.parent))
|
|
65
|
+
os.environ.setdefault("DESCRIPTRON_DATA", str(data_path()))
|
|
66
|
+
if path.suffix == ".sh":
|
|
67
|
+
os.execvp("bash", ["bash", str(path), *argv])
|
|
68
|
+
sys.argv = [str(path), *argv]
|
|
69
|
+
runpy.run_path(str(path), run_name="__main__")
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def main(argv: list[str] | None = None) -> None:
|
|
73
|
+
args = list(sys.argv[1:] if argv is None else argv)
|
|
74
|
+
if not args or args[0] in ("-h", "--help", "help"):
|
|
75
|
+
print(__doc__.strip())
|
|
76
|
+
print(f"\n{len(available())} programs available; --list shows them.")
|
|
77
|
+
return
|
|
78
|
+
if args[0] in ("--list", "list"):
|
|
79
|
+
for name in available():
|
|
80
|
+
print(name)
|
|
81
|
+
return
|
|
82
|
+
if args[0] in ("--where", "where"):
|
|
83
|
+
print(f"tools: {tools_dir()}\ndata: {data_path()}")
|
|
84
|
+
return
|
|
85
|
+
run(args[0], args[1:])
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _fixed(script: str):
|
|
89
|
+
def entry() -> None:
|
|
90
|
+
run(script, sys.argv[1:])
|
|
91
|
+
return entry
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
run_pipeline = _fixed("run_full_pipeline_v2")
|
|
95
|
+
run_key_builder = _fixed("biorag_key_builder_v1")
|
|
96
|
+
run_confabulation_checker = _fixed("biorag_confabulation_checker_v2")
|
|
97
|
+
run_novelty_score = _fixed("biorag_novelty_score_v1")
|
|
98
|
+
run_calibrate = _fixed("biorag_calibrate_v1")
|
|
99
|
+
run_keys = _fixed("descriptron_credentials")
|
{descriptron_core-2.0.3 → descriptron_core-2.1.0}/src/descriptron_core/tools/biorag_llm_backend.py
RENAMED
|
@@ -288,6 +288,12 @@ def client_from_args(args, log_path: Optional[str] = None):
|
|
|
288
288
|
--api-base-url, or no model at all."""
|
|
289
289
|
import os
|
|
290
290
|
key_env = getattr(args, "api_key_env", None) or "ANTHROPIC_API_KEY"
|
|
291
|
+
if (getattr(args, "llm_backend", "") or "").lower() == "api" and not os.environ.get(key_env):
|
|
292
|
+
try: # v2.0.4: credentials file / first-use prompt
|
|
293
|
+
import descriptron_credentials as _creds
|
|
294
|
+
_creds.ensure_key(key_env)
|
|
295
|
+
except ImportError:
|
|
296
|
+
pass
|
|
291
297
|
return make_llm_client(getattr(args, "llm_backend", "claude-code"),
|
|
292
298
|
api_key=os.environ.get(key_env),
|
|
293
299
|
claude_bin=getattr(args, "claude_bin", None),
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
import json
|
|
4
|
+
import argparse
|
|
5
|
+
import logging
|
|
6
|
+
import glob
|
|
7
|
+
import cv2 # Use OpenCV for reading dimensions
|
|
8
|
+
|
|
9
|
+
def main():
|
|
10
|
+
# Set up logging
|
|
11
|
+
logging.basicConfig(
|
|
12
|
+
level=logging.INFO,
|
|
13
|
+
format='%(asctime)s [%(levelname)s] %(message)s',
|
|
14
|
+
handlers=[
|
|
15
|
+
logging.StreamHandler(sys.stdout)
|
|
16
|
+
]
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
# Argument parsing
|
|
20
|
+
parser = argparse.ArgumentParser(description='Combine multiple COCO JSON files into one, ensuring width/height are included. Tries to append .jpg/.png/.tif if missing.')
|
|
21
|
+
parser.add_argument('--input-jsons', nargs='+', required=True,
|
|
22
|
+
help='List of input COCO JSON files to combine. Can be space-separated or a CSV string.')
|
|
23
|
+
parser.add_argument('--output-json', required=True, help='Output path for the combined COCO JSON file.')
|
|
24
|
+
parser.add_argument('--skip-categories', nargs='*', default=[], help='List of category names to skip.')
|
|
25
|
+
parser.add_argument('--images-folders', nargs='*', default=[],
|
|
26
|
+
help='One or more folders where input images are located. Used to look up file extensions if missing and for reading dimensions.')
|
|
27
|
+
|
|
28
|
+
args = parser.parse_args()
|
|
29
|
+
|
|
30
|
+
# Handle input JSON files (space-separated or CSV)
|
|
31
|
+
input_json_files = []
|
|
32
|
+
for item in args.input_jsons:
|
|
33
|
+
input_json_files.extend(item.split(',')) # Split CSV strings
|
|
34
|
+
input_json_files = [f.strip() for f in input_json_files if f.strip()]
|
|
35
|
+
|
|
36
|
+
output_json_path = args.output_json
|
|
37
|
+
skip_categories = args.skip_categories
|
|
38
|
+
images_folders = args.images_folders
|
|
39
|
+
|
|
40
|
+
# Check if the output filename has a '.json' extension
|
|
41
|
+
if not output_json_path.lower().endswith('.json'):
|
|
42
|
+
# Append '_combined.json' to the output filename (without extension)
|
|
43
|
+
output_json_path = os.path.splitext(output_json_path)[0] + '_combined.json'
|
|
44
|
+
logging.info(f"Output filename does not have a '.json' extension. Using '{output_json_path}' as the output file.")
|
|
45
|
+
|
|
46
|
+
# Now proceed with combining the files
|
|
47
|
+
combine_coco_json_files(input_json_files, output_json_path, skip_categories, images_folders)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def combine_coco_json_files(input_json_files, output_json_path, skip_categories=[], images_folders=[]):
|
|
51
|
+
combined_output = {
|
|
52
|
+
"images": [],
|
|
53
|
+
"annotations": [],
|
|
54
|
+
"categories": []
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
# IDs for new images and annotations
|
|
58
|
+
new_image_id = 1
|
|
59
|
+
new_annotation_id = 1
|
|
60
|
+
|
|
61
|
+
# Mapping for categories: maps category name to new id
|
|
62
|
+
category_name_to_new_id = {}
|
|
63
|
+
new_category_id = 1 # Start category IDs from 1
|
|
64
|
+
|
|
65
|
+
# Process each input file
|
|
66
|
+
for json_file in input_json_files:
|
|
67
|
+
if not os.path.exists(json_file):
|
|
68
|
+
logging.error(f"File not found: {json_file}")
|
|
69
|
+
continue
|
|
70
|
+
|
|
71
|
+
logging.info(f"Processing file: {json_file}")
|
|
72
|
+
with open(json_file, 'r') as file:
|
|
73
|
+
coco_data = json.load(file)
|
|
74
|
+
|
|
75
|
+
# Handle categories: map old category IDs to new ones
|
|
76
|
+
old_category_id_to_new_id = {}
|
|
77
|
+
|
|
78
|
+
# Check if categories exist, if not create a default one for annotations with category_id = -1
|
|
79
|
+
if not coco_data.get('categories'):
|
|
80
|
+
logging.warning(f"No categories found in {json_file}. Creating default category for keypoints.")
|
|
81
|
+
# Check if any annotations have keypoints to determine if we need a keypoint category
|
|
82
|
+
has_keypoints = any('keypoints' in ann for ann in coco_data.get('annotations', []))
|
|
83
|
+
if has_keypoints:
|
|
84
|
+
# Create a default category with keypoints support
|
|
85
|
+
default_category = {
|
|
86
|
+
'id': -1,
|
|
87
|
+
'name': 'keypoints',
|
|
88
|
+
'supercategory': 'none',
|
|
89
|
+
'keypoints': ['point_' + str(i) for i in range(1, 9)], # 8 keypoints as seen in data
|
|
90
|
+
'skeleton': [] # Empty skeleton, can be filled later if needed
|
|
91
|
+
}
|
|
92
|
+
coco_data['categories'] = [default_category]
|
|
93
|
+
logging.info(f"Created default keypoints category")
|
|
94
|
+
|
|
95
|
+
for category in coco_data.get('categories', []):
|
|
96
|
+
cat_name = category['name'].strip()
|
|
97
|
+
if cat_name in skip_categories:
|
|
98
|
+
logging.info(f"Skipping category '{cat_name}' as per skip list.")
|
|
99
|
+
continue # Skip this category
|
|
100
|
+
|
|
101
|
+
if cat_name not in category_name_to_new_id:
|
|
102
|
+
new_category = {
|
|
103
|
+
"id": new_category_id,
|
|
104
|
+
"name": cat_name,
|
|
105
|
+
"supercategory": category.get('supercategory', 'none')
|
|
106
|
+
}
|
|
107
|
+
# Explicitly copy keypoints and skeleton if present
|
|
108
|
+
if 'keypoints' in category:
|
|
109
|
+
new_category['keypoints'] = category['keypoints']
|
|
110
|
+
if 'skeleton' in category:
|
|
111
|
+
new_category['skeleton'] = category['skeleton']
|
|
112
|
+
# === new ADD THIS TO PRESERVE ROSETTA ONTOLOGY ===
|
|
113
|
+
if 'ontology' in category:
|
|
114
|
+
new_category['ontology'] = category['ontology']
|
|
115
|
+
# =============================================
|
|
116
|
+
# === ADD THIS TO PRESERVE LINES ===
|
|
117
|
+
if 'lines' in category:
|
|
118
|
+
new_category['lines'] = category['lines']
|
|
119
|
+
# ==================================
|
|
120
|
+
# end new #
|
|
121
|
+
category_name_to_new_id[cat_name] = new_category_id
|
|
122
|
+
combined_output['categories'].append(new_category)
|
|
123
|
+
logging.info(f"Assigned new category ID {new_category_id} to category '{cat_name}'.")
|
|
124
|
+
new_category_id += 1
|
|
125
|
+
|
|
126
|
+
# === V13 FIX: First-wins for old_category_id_to_new_id ===
|
|
127
|
+
# When COCO JSONs have duplicate category entries from multiple
|
|
128
|
+
# annotation rounds (e.g. id=2 → "left_elytron" then later
|
|
129
|
+
# id=2 → "pronotum"), the last write silently corrupts the mapping.
|
|
130
|
+
# First-wins preserves the original annotation intent.
|
|
131
|
+
old_id = category['id']
|
|
132
|
+
new_id = category_name_to_new_id[cat_name]
|
|
133
|
+
if old_id not in old_category_id_to_new_id:
|
|
134
|
+
old_category_id_to_new_id[old_id] = new_id
|
|
135
|
+
elif old_category_id_to_new_id[old_id] != new_id:
|
|
136
|
+
logging.warning(
|
|
137
|
+
f"Duplicate category id={old_id} in {json_file}: "
|
|
138
|
+
f"keeping first mapping (→new_id={old_category_id_to_new_id[old_id]}), "
|
|
139
|
+
f"ignoring \"{cat_name}\" (→new_id={new_id})"
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
# FIXED: Local mapping for images - handle both STRING and INTEGER IDs
|
|
143
|
+
old_image_id_to_new_id = {}
|
|
144
|
+
if coco_data.get('images'):
|
|
145
|
+
for image in coco_data.get('images', []):
|
|
146
|
+
old_img_id = image['id'] # Can be string or int
|
|
147
|
+
local_new_id = new_image_id
|
|
148
|
+
new_image = image.copy()
|
|
149
|
+
# Ensure file_name has an extension if possible
|
|
150
|
+
if 'file_name' in new_image:
|
|
151
|
+
new_image['file_name'] = get_image_file_name(new_image['file_name'], images_folders)
|
|
152
|
+
# Make sure we have width/height via OpenCV
|
|
153
|
+
add_width_and_height_if_missing_cv2(new_image, images_folders)
|
|
154
|
+
new_image['id'] = new_image_id
|
|
155
|
+
combined_output['images'].append(new_image)
|
|
156
|
+
new_image_id += 1
|
|
157
|
+
|
|
158
|
+
old_image_id_to_new_id[old_img_id] = local_new_id
|
|
159
|
+
logging.info(f"Mapped old image ID '{old_img_id}' (type: {type(old_img_id).__name__}) to new ID {local_new_id}")
|
|
160
|
+
|
|
161
|
+
# Handle annotations
|
|
162
|
+
for annotation in coco_data.get('annotations', []):
|
|
163
|
+
old_cat_id = annotation['category_id']
|
|
164
|
+
new_cat_id = old_category_id_to_new_id.get(old_cat_id)
|
|
165
|
+
if new_cat_id is None:
|
|
166
|
+
logging.warning(f"Annotation ID {annotation.get('id', 'unknown')} has category ID {old_cat_id} which is not mapped. Skipping.")
|
|
167
|
+
continue # Skip annotations with categories that were skipped
|
|
168
|
+
|
|
169
|
+
old_img_id = annotation['image_id'] # Can be string or int
|
|
170
|
+
# If the image was not already added, create a new image entry.
|
|
171
|
+
if old_img_id not in old_image_id_to_new_id:
|
|
172
|
+
local_new_id = new_image_id
|
|
173
|
+
new_image_id += 1
|
|
174
|
+
|
|
175
|
+
# Handle string IDs for file_name
|
|
176
|
+
if isinstance(old_img_id, str):
|
|
177
|
+
file_name = old_img_id
|
|
178
|
+
else:
|
|
179
|
+
file_name = str(old_img_id)
|
|
180
|
+
file_name = get_image_file_name(file_name, images_folders)
|
|
181
|
+
|
|
182
|
+
new_image = {
|
|
183
|
+
"id": local_new_id,
|
|
184
|
+
"file_name": file_name
|
|
185
|
+
}
|
|
186
|
+
add_width_and_height_if_missing_cv2(new_image, images_folders)
|
|
187
|
+
combined_output['images'].append(new_image)
|
|
188
|
+
|
|
189
|
+
old_image_id_to_new_id[old_img_id] = local_new_id
|
|
190
|
+
logging.info(f"Created new image entry for old image ID '{old_img_id}' -> new ID {local_new_id}")
|
|
191
|
+
|
|
192
|
+
# Create the new annotation entry explicitly preserving keypoints
|
|
193
|
+
new_annotation = annotation.copy()
|
|
194
|
+
new_annotation['id'] = new_annotation_id
|
|
195
|
+
new_annotation_id += 1
|
|
196
|
+
|
|
197
|
+
# Update image_id and category_id to the new ones
|
|
198
|
+
new_annotation['image_id'] = old_image_id_to_new_id[old_img_id]
|
|
199
|
+
new_annotation['category_id'] = new_cat_id
|
|
200
|
+
|
|
201
|
+
# Explicitly preserve keypoints fields if present
|
|
202
|
+
if 'keypoints' in annotation:
|
|
203
|
+
new_annotation['keypoints'] = annotation['keypoints']
|
|
204
|
+
logging.info(f"Preserved {len(annotation['keypoints'])} keypoint values for annotation {new_annotation_id-1}")
|
|
205
|
+
if 'num_keypoints' in annotation:
|
|
206
|
+
new_annotation['num_keypoints'] = annotation['num_keypoints']
|
|
207
|
+
|
|
208
|
+
combined_output['annotations'].append(new_annotation)
|
|
209
|
+
|
|
210
|
+
# Debug prints
|
|
211
|
+
logging.info(f"Total Combined Categories: {len(combined_output['categories'])}")
|
|
212
|
+
logging.info(f"Total Combined Images: {len(combined_output['images'])}")
|
|
213
|
+
logging.info(f"Total Combined Annotations: {len(combined_output['annotations'])}")
|
|
214
|
+
|
|
215
|
+
# Check keypoints in output
|
|
216
|
+
annotations_with_keypoints = sum(1 for ann in combined_output['annotations'] if 'keypoints' in ann)
|
|
217
|
+
logging.info(f"Annotations with keypoints: {annotations_with_keypoints}")
|
|
218
|
+
|
|
219
|
+
# Save the combined output
|
|
220
|
+
with open(output_json_path, 'w') as out_file:
|
|
221
|
+
json.dump(combined_output, out_file, indent=4)
|
|
222
|
+
logging.info(f"Combined COCO JSON saved to {output_json_path}")
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def get_image_file_name(file_name, images_folders):
|
|
226
|
+
"""
|
|
227
|
+
If file_name has an extension, return as-is.
|
|
228
|
+
Otherwise:
|
|
229
|
+
1) Try a wildcard search: file_name + '.*' in images_folders.
|
|
230
|
+
2) If that fails, systematically try .jpg, .jpeg, .png, .tif, .tiff.
|
|
231
|
+
3) Return the discovered file name or the input as a fallback.
|
|
232
|
+
"""
|
|
233
|
+
base, ext = os.path.splitext(file_name)
|
|
234
|
+
if ext:
|
|
235
|
+
# Already has extension
|
|
236
|
+
return file_name
|
|
237
|
+
|
|
238
|
+
# 1) Try wildcard search: file_name + '.*'
|
|
239
|
+
for folder in images_folders:
|
|
240
|
+
pattern = os.path.join(folder, base + ".*")
|
|
241
|
+
matches = glob.glob(pattern)
|
|
242
|
+
if matches:
|
|
243
|
+
found_file = os.path.basename(matches[0])
|
|
244
|
+
logging.info(f"Found extension for '{file_name}' in folder '{folder}': using '{found_file}'.")
|
|
245
|
+
return found_file
|
|
246
|
+
# 2) If still not found, systematically try known extensions
|
|
247
|
+
possible_exts = [".jpg", ".jpeg", ".png", ".tif", ".tiff"]
|
|
248
|
+
for folder in images_folders:
|
|
249
|
+
for e in possible_exts:
|
|
250
|
+
candidate = os.path.join(folder, base + e)
|
|
251
|
+
if os.path.exists(candidate):
|
|
252
|
+
found_file = os.path.basename(candidate)
|
|
253
|
+
logging.info(f"Found matching file for '{file_name}' in folder '{folder}': using '{found_file}'.")
|
|
254
|
+
return found_file
|
|
255
|
+
|
|
256
|
+
# Also try in current directory if images_folders fails
|
|
257
|
+
for e in possible_exts:
|
|
258
|
+
if os.path.exists(base + e):
|
|
259
|
+
found_file = base + e
|
|
260
|
+
logging.info(f"Found matching file in current dir for '{file_name}': using '{found_file}'.")
|
|
261
|
+
return found_file
|
|
262
|
+
|
|
263
|
+
# If we got here, no match
|
|
264
|
+
logging.info(f"Could NOT find any file for '{file_name}' in images_folders or current dir.")
|
|
265
|
+
return file_name
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def add_width_and_height_if_missing_cv2(image_dict, images_folders):
|
|
269
|
+
"""
|
|
270
|
+
Checks if 'width' or 'height' is missing from the image_dict.
|
|
271
|
+
If so, tries reading with OpenCV (looping over folders + direct).
|
|
272
|
+
Inserts the discovered w/h, logs success/failure.
|
|
273
|
+
"""
|
|
274
|
+
if 'width' not in image_dict or 'height' not in image_dict:
|
|
275
|
+
w, h = get_image_dimensions_cv2(image_dict['file_name'], images_folders)
|
|
276
|
+
if w is not None and h is not None:
|
|
277
|
+
image_dict['width'] = w
|
|
278
|
+
image_dict['height'] = h
|
|
279
|
+
logging.info(f"Width={w}, Height={h} for image '{image_dict['file_name']}' (ID {image_dict.get('id','?')})")
|
|
280
|
+
else:
|
|
281
|
+
logging.info(f"Could NOT determine width/height for image '{image_dict['file_name']}'")
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def get_image_dimensions_cv2(file_name, images_folders):
|
|
285
|
+
"""
|
|
286
|
+
Attempts to load the image using OpenCV from the provided folders or the current path,
|
|
287
|
+
returning (width, height) if successful, else (None, None).
|
|
288
|
+
"""
|
|
289
|
+
# (1) Check each images_folder
|
|
290
|
+
for folder in images_folders:
|
|
291
|
+
candidate = os.path.join(folder, file_name)
|
|
292
|
+
if os.path.exists(candidate):
|
|
293
|
+
img = cv2.imread(candidate)
|
|
294
|
+
if img is not None:
|
|
295
|
+
h, w = img.shape[:2]
|
|
296
|
+
return w, h
|
|
297
|
+
# (2) If not found in the folders, try directly
|
|
298
|
+
if os.path.exists(file_name):
|
|
299
|
+
img = cv2.imread(file_name)
|
|
300
|
+
if img is not None:
|
|
301
|
+
h, w = img.shape[:2]
|
|
302
|
+
return w, h
|
|
303
|
+
return None, None
|
|
304
|
+
|
|
305
|
+
if __name__ == '__main__':
|
|
306
|
+
main()
|