descriptron-core 2.0.4__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.
Files changed (132) hide show
  1. {descriptron_core-2.0.4 → descriptron_core-2.1.0}/PKG-INFO +1 -1
  2. descriptron_core-2.1.0/pyproject.toml +82 -0
  3. descriptron_core-2.1.0/src/descriptron_core/tools/coco_combiner_V13.py +306 -0
  4. descriptron_core-2.1.0/src/descriptron_core/tools/coco_converter_v24.py +447 -0
  5. descriptron_core-2.1.0/src/descriptron_core/tools/descriptron_centerline.py +377 -0
  6. descriptron_core-2.1.0/src/descriptron_core/tools/descriptron_coco_tools.py +326 -0
  7. descriptron_core-2.1.0/src/descriptron_core/tools/descriptron_convert.py +734 -0
  8. descriptron_core-2.1.0/src/descriptron_core/tools/descriptron_joints.py +204 -0
  9. descriptron_core-2.1.0/src/descriptron_core/tools/descriptron_metadata.py +230 -0
  10. descriptron_core-2.1.0/src/descriptron_core/tools/descriptron_shape_stats.py +716 -0
  11. descriptron_core-2.1.0/src/descriptron_core/tools/landmark_gpa_V2.py +756 -0
  12. {descriptron_core-2.0.4 → descriptron_core-2.1.0}/src/descriptron_core/tools/run_full_pipeline_v2.py +14 -3
  13. descriptron_core-2.1.0/src/descriptron_core/tools/semi_landmark_and_kpts_procrustesV42_GPA.py +2388 -0
  14. descriptron_core-2.1.0/src/descriptron_core/tools/validate_pipeline_gm_vs_geomorph.py +220 -0
  15. descriptron_core-2.1.0/src/descriptron_core/tools/validate_semilandmarks_vs_geomorph.py +164 -0
  16. descriptron_core-2.1.0/src/descriptron_core/tools/validate_shape_stats_vs_geomorph.py +387 -0
  17. descriptron_core-2.1.0/src/descriptron_core/tools/validate_shape_stats_vs_rrpp.py +244 -0
  18. {descriptron_core-2.0.4 → descriptron_core-2.1.0}/.gitignore +0 -0
  19. {descriptron_core-2.0.4 → descriptron_core-2.1.0}/LICENSE +0 -0
  20. {descriptron_core-2.0.4 → descriptron_core-2.1.0}/NOTICE +0 -0
  21. {descriptron_core-2.0.4 → descriptron_core-2.1.0}/README.md +0 -0
  22. {descriptron_core-2.0.4 → descriptron_core-2.1.0}/pyproject.toml.bak_pre_203_20260924_145320 +0 -0
  23. {descriptron_core-2.0.4 → descriptron_core-2.1.0}/pyproject.toml.bak_pre_204_20260924_165335 +0 -0
  24. /descriptron_core-2.0.4/pyproject.toml → /descriptron_core-2.1.0/pyproject.toml.bak_pre_210_20260925_103754 +0 -0
  25. {descriptron_core-2.0.4 → descriptron_core-2.1.0}/pyproject.toml.bak_pre_keys_20260924_165335 +0 -0
  26. {descriptron_core-2.0.4 → descriptron_core-2.1.0}/src/descriptron_core/__init__.py +0 -0
  27. {descriptron_core-2.0.4 → descriptron_core-2.1.0}/src/descriptron_core/cli.py +0 -0
  28. {descriptron_core-2.0.4 → descriptron_core-2.1.0}/src/descriptron_core/cli.py.bak_pre_keys_20260924_165335 +0 -0
  29. {descriptron_core-2.0.4 → descriptron_core-2.1.0}/src/descriptron_core/data/biorag_prompts/biorag_system_prompts_v1.txt +0 -0
  30. {descriptron_core-2.0.4 → descriptron_core-2.1.0}/src/descriptron_core/data/biorag_prompts/biorag_system_prompts_v2.txt +0 -0
  31. {descriptron_core-2.0.4 → descriptron_core-2.1.0}/src/descriptron_core/data/biorag_prompts/diaphorina_user_prompts.txt +0 -0
  32. {descriptron_core-2.0.4 → descriptron_core-2.1.0}/src/descriptron_core/data/biorag_prompts/ontologies/aism.obo +0 -0
  33. {descriptron_core-2.0.4 → descriptron_core-2.1.0}/src/descriptron_core/data/biorag_prompts/ontologies/pato.obo +0 -0
  34. {descriptron_core-2.0.4 → descriptron_core-2.1.0}/src/descriptron_core/data/biorag_prompts/schemas/sdd_1.1/SDD.xsd +0 -0
  35. {descriptron_core-2.0.4 → descriptron_core-2.1.0}/src/descriptron_core/data/biorag_prompts/schemas/sdd_1.1/SDD_EnumLib.xsd +0 -0
  36. {descriptron_core-2.0.4 → descriptron_core-2.1.0}/src/descriptron_core/data/biorag_prompts/schemas/sdd_1.1/SDD_TypeLib.xsd +0 -0
  37. {descriptron_core-2.0.4 → descriptron_core-2.1.0}/src/descriptron_core/data/biorag_prompts/schemas/sdd_1.1/UBIF_BasePattern.xsd +0 -0
  38. {descriptron_core-2.0.4 → descriptron_core-2.1.0}/src/descriptron_core/data/biorag_prompts/schemas/sdd_1.1/UBIF_CoreExtensions.xsd +0 -0
  39. {descriptron_core-2.0.4 → descriptron_core-2.1.0}/src/descriptron_core/data/biorag_prompts/schemas/sdd_1.1/UBIF_CoreOntology.xsd +0 -0
  40. {descriptron_core-2.0.4 → descriptron_core-2.1.0}/src/descriptron_core/data/biorag_prompts/schemas/sdd_1.1/UBIF_EnumLib.xsd +0 -0
  41. {descriptron_core-2.0.4 → descriptron_core-2.1.0}/src/descriptron_core/data/biorag_prompts/schemas/sdd_1.1/UBIF_LinneanCore.xsd +0 -0
  42. {descriptron_core-2.0.4 → descriptron_core-2.1.0}/src/descriptron_core/data/biorag_prompts/schemas/sdd_1.1/UBIF_TypeLib.xsd +0 -0
  43. {descriptron_core-2.0.4 → descriptron_core-2.1.0}/src/descriptron_core/data/biorag_prompts/schemas/sdd_1.1/w3c-schemata/xml.xsd +0 -0
  44. {descriptron_core-2.0.4 → descriptron_core-2.1.0}/src/descriptron_core/data/biorag_prompts/schemas/taxpub/sample_treatment_deposit_example.xml +0 -0
  45. {descriptron_core-2.0.4 → descriptron_core-2.1.0}/src/descriptron_core/data/biorag_prompts/schemas/taxpub/sample_zookeys_26056_tp.xml +0 -0
  46. {descriptron_core-2.0.4 → descriptron_core-2.1.0}/src/descriptron_core/data/biorag_prompts/schemas/taxpub/tax-treatment-NS0-v1_flat.dtd +0 -0
  47. {descriptron_core-2.0.4 → descriptron_core-2.1.0}/src/descriptron_core/data/biorag_prompts/taxon_profiles/TEMPLATE_taxon_profile.yaml +0 -0
  48. {descriptron_core-2.0.4 → descriptron_core-2.1.0}/src/descriptron_core/data/biorag_prompts/taxon_profiles/diaphorina_taxon_profile.yaml +0 -0
  49. {descriptron_core-2.0.4 → descriptron_core-2.1.0}/src/descriptron_core/tools/FHS_and_CLAHE_V21.py +0 -0
  50. {descriptron_core-2.0.4 → descriptron_core-2.1.0}/src/descriptron_core/tools/biorag_add_discrete_characters_v1.py +0 -0
  51. {descriptron_core-2.0.4 → descriptron_core-2.1.0}/src/descriptron_core/tools/biorag_annotation_reorient_v1.py +0 -0
  52. {descriptron_core-2.0.4 → descriptron_core-2.1.0}/src/descriptron_core/tools/biorag_annotation_screen_v1.py +0 -0
  53. {descriptron_core-2.0.4 → descriptron_core-2.1.0}/src/descriptron_core/tools/biorag_autapomorphy_v1.py +0 -0
  54. {descriptron_core-2.0.4 → descriptron_core-2.1.0}/src/descriptron_core/tools/biorag_calibrate_v1.py +0 -0
  55. {descriptron_core-2.0.4 → descriptron_core-2.1.0}/src/descriptron_core/tools/biorag_calibration_figures_v1.py +0 -0
  56. {descriptron_core-2.0.4 → descriptron_core-2.1.0}/src/descriptron_core/tools/biorag_character_gate_v1.py +0 -0
  57. {descriptron_core-2.0.4 → descriptron_core-2.1.0}/src/descriptron_core/tools/biorag_character_reliability_v1.py +0 -0
  58. {descriptron_core-2.0.4 → descriptron_core-2.1.0}/src/descriptron_core/tools/biorag_character_robustness_v1.py +0 -0
  59. {descriptron_core-2.0.4 → descriptron_core-2.1.0}/src/descriptron_core/tools/biorag_collaborator_forms_v1.py +0 -0
  60. {descriptron_core-2.0.4 → descriptron_core-2.1.0}/src/descriptron_core/tools/biorag_computed_sets_v1.py +0 -0
  61. {descriptron_core-2.0.4 → descriptron_core-2.1.0}/src/descriptron_core/tools/biorag_confabulation_checker_v2.py +0 -0
  62. {descriptron_core-2.0.4 → descriptron_core-2.1.0}/src/descriptron_core/tools/biorag_congruence_compare_v1.py +0 -0
  63. {descriptron_core-2.0.4 → descriptron_core-2.1.0}/src/descriptron_core/tools/biorag_description_refiner_v1.py +0 -0
  64. {descriptron_core-2.0.4 → descriptron_core-2.1.0}/src/descriptron_core/tools/biorag_descriptive_scoring_v1.py +0 -0
  65. {descriptron_core-2.0.4 → descriptron_core-2.1.0}/src/descriptron_core/tools/biorag_feature_policy.py +0 -0
  66. {descriptron_core-2.0.4 → descriptron_core-2.1.0}/src/descriptron_core/tools/biorag_frame_orientation_calibrate_v1.py +0 -0
  67. {descriptron_core-2.0.4 → descriptron_core-2.1.0}/src/descriptron_core/tools/biorag_frame_orientation_v1.py +0 -0
  68. {descriptron_core-2.0.4 → descriptron_core-2.1.0}/src/descriptron_core/tools/biorag_gate_compare_v1.py +0 -0
  69. {descriptron_core-2.0.4 → descriptron_core-2.1.0}/src/descriptron_core/tools/biorag_graph_identify_v1.py +0 -0
  70. {descriptron_core-2.0.4 → descriptron_core-2.1.0}/src/descriptron_core/tools/biorag_graph_support_figure_v1.py +0 -0
  71. {descriptron_core-2.0.4 → descriptron_core-2.1.0}/src/descriptron_core/tools/biorag_homology_frame_v1.py +0 -0
  72. {descriptron_core-2.0.4 → descriptron_core-2.1.0}/src/descriptron_core/tools/biorag_instrument_compare_v3.py +0 -0
  73. {descriptron_core-2.0.4 → descriptron_core-2.1.0}/src/descriptron_core/tools/biorag_key_builder_v1.py +0 -0
  74. {descriptron_core-2.0.4 → descriptron_core-2.1.0}/src/descriptron_core/tools/biorag_key_feature_filter_v2.py +0 -0
  75. {descriptron_core-2.0.4 → descriptron_core-2.1.0}/src/descriptron_core/tools/biorag_key_fuzzy_figures_v1.py +0 -0
  76. {descriptron_core-2.0.4 → descriptron_core-2.1.0}/src/descriptron_core/tools/biorag_key_fuzzy_v1.py +0 -0
  77. {descriptron_core-2.0.4 → descriptron_core-2.1.0}/src/descriptron_core/tools/biorag_key_holdout_v1.py +0 -0
  78. {descriptron_core-2.0.4 → descriptron_core-2.1.0}/src/descriptron_core/tools/biorag_key_jackknife_support_v1.py +0 -0
  79. {descriptron_core-2.0.4 → descriptron_core-2.1.0}/src/descriptron_core/tools/biorag_label_conflict_check_v1.py +0 -0
  80. {descriptron_core-2.0.4 → descriptron_core-2.1.0}/src/descriptron_core/tools/biorag_llm_backend.py +0 -0
  81. {descriptron_core-2.0.4 → descriptron_core-2.1.0}/src/descriptron_core/tools/biorag_make_taxon_profile_v1.py +0 -0
  82. {descriptron_core-2.0.4 → descriptron_core-2.1.0}/src/descriptron_core/tools/biorag_methods_summary_figure_v1.py +0 -0
  83. {descriptron_core-2.0.4 → descriptron_core-2.1.0}/src/descriptron_core/tools/biorag_novelty_score_v1.py +0 -0
  84. {descriptron_core-2.0.4 → descriptron_core-2.1.0}/src/descriptron_core/tools/biorag_ontology_annotator_v2.py +0 -0
  85. {descriptron_core-2.0.4 → descriptron_core-2.1.0}/src/descriptron_core/tools/biorag_outgroup_batch_check_v1.py +0 -0
  86. {descriptron_core-2.0.4 → descriptron_core-2.1.0}/src/descriptron_core/tools/biorag_outline_shape_v1.py +0 -0
  87. {descriptron_core-2.0.4 → descriptron_core-2.1.0}/src/descriptron_core/tools/biorag_prompts/biorag_system_prompts_v1.txt +0 -0
  88. {descriptron_core-2.0.4 → descriptron_core-2.1.0}/src/descriptron_core/tools/biorag_prompts/biorag_system_prompts_v2.txt +0 -0
  89. {descriptron_core-2.0.4 → descriptron_core-2.1.0}/src/descriptron_core/tools/biorag_prompts/diaphorina_user_prompts.txt +0 -0
  90. {descriptron_core-2.0.4 → descriptron_core-2.1.0}/src/descriptron_core/tools/biorag_prompts/ontologies/aism.obo +0 -0
  91. {descriptron_core-2.0.4 → descriptron_core-2.1.0}/src/descriptron_core/tools/biorag_prompts/ontologies/pato.obo +0 -0
  92. {descriptron_core-2.0.4 → descriptron_core-2.1.0}/src/descriptron_core/tools/biorag_prompts/schemas/sdd_1.1/SDD.xsd +0 -0
  93. {descriptron_core-2.0.4 → descriptron_core-2.1.0}/src/descriptron_core/tools/biorag_prompts/schemas/sdd_1.1/SDD_EnumLib.xsd +0 -0
  94. {descriptron_core-2.0.4 → descriptron_core-2.1.0}/src/descriptron_core/tools/biorag_prompts/schemas/sdd_1.1/SDD_TypeLib.xsd +0 -0
  95. {descriptron_core-2.0.4 → descriptron_core-2.1.0}/src/descriptron_core/tools/biorag_prompts/schemas/sdd_1.1/UBIF_BasePattern.xsd +0 -0
  96. {descriptron_core-2.0.4 → descriptron_core-2.1.0}/src/descriptron_core/tools/biorag_prompts/schemas/sdd_1.1/UBIF_CoreExtensions.xsd +0 -0
  97. {descriptron_core-2.0.4 → descriptron_core-2.1.0}/src/descriptron_core/tools/biorag_prompts/schemas/sdd_1.1/UBIF_CoreOntology.xsd +0 -0
  98. {descriptron_core-2.0.4 → descriptron_core-2.1.0}/src/descriptron_core/tools/biorag_prompts/schemas/sdd_1.1/UBIF_EnumLib.xsd +0 -0
  99. {descriptron_core-2.0.4 → descriptron_core-2.1.0}/src/descriptron_core/tools/biorag_prompts/schemas/sdd_1.1/UBIF_LinneanCore.xsd +0 -0
  100. {descriptron_core-2.0.4 → descriptron_core-2.1.0}/src/descriptron_core/tools/biorag_prompts/schemas/sdd_1.1/UBIF_TypeLib.xsd +0 -0
  101. {descriptron_core-2.0.4 → descriptron_core-2.1.0}/src/descriptron_core/tools/biorag_prompts/schemas/sdd_1.1/w3c-schemata/xml.xsd +0 -0
  102. {descriptron_core-2.0.4 → descriptron_core-2.1.0}/src/descriptron_core/tools/biorag_prompts/schemas/taxpub/sample_treatment_deposit_example.xml +0 -0
  103. {descriptron_core-2.0.4 → descriptron_core-2.1.0}/src/descriptron_core/tools/biorag_prompts/schemas/taxpub/sample_zookeys_26056_tp.xml +0 -0
  104. {descriptron_core-2.0.4 → descriptron_core-2.1.0}/src/descriptron_core/tools/biorag_prompts/schemas/taxpub/tax-treatment-NS0-v1_flat.dtd +0 -0
  105. {descriptron_core-2.0.4 → descriptron_core-2.1.0}/src/descriptron_core/tools/biorag_prompts/taxon_profiles/TEMPLATE_taxon_profile.yaml +0 -0
  106. {descriptron_core-2.0.4 → descriptron_core-2.1.0}/src/descriptron_core/tools/biorag_prompts/taxon_profiles/diaphorina_taxon_profile.yaml +0 -0
  107. {descriptron_core-2.0.4 → descriptron_core-2.1.0}/src/descriptron_core/tools/biorag_rebuild_downstream_v1.sh +0 -0
  108. {descriptron_core-2.0.4 → descriptron_core-2.1.0}/src/descriptron_core/tools/biorag_reliability_figures_v1.py +0 -0
  109. {descriptron_core-2.0.4 → descriptron_core-2.1.0}/src/descriptron_core/tools/biorag_rename_species_v1.py +0 -0
  110. {descriptron_core-2.0.4 → descriptron_core-2.1.0}/src/descriptron_core/tools/biorag_rerun_descriptive_tables_v1.sh +0 -0
  111. {descriptron_core-2.0.4 → descriptron_core-2.1.0}/src/descriptron_core/tools/biorag_retest_compare_v1.py +0 -0
  112. {descriptron_core-2.0.4 → descriptron_core-2.1.0}/src/descriptron_core/tools/biorag_specimen_id.py +0 -0
  113. {descriptron_core-2.0.4 → descriptron_core-2.1.0}/src/descriptron_core/tools/biorag_subjective_checks_v1.py +0 -0
  114. {descriptron_core-2.0.4 → descriptron_core-2.1.0}/src/descriptron_core/tools/biorag_type_material_v1.py +0 -0
  115. {descriptron_core-2.0.4 → descriptron_core-2.1.0}/src/descriptron_core/tools/biorag_vlm_characters_v1.py +0 -0
  116. {descriptron_core-2.0.4 → descriptron_core-2.1.0}/src/descriptron_core/tools/biorag_vlm_heatmap_figures_v1.py +0 -0
  117. {descriptron_core-2.0.4 → descriptron_core-2.1.0}/src/descriptron_core/tools/biorag_vlm_reliability_figure_v1.py +0 -0
  118. {descriptron_core-2.0.4 → descriptron_core-2.1.0}/src/descriptron_core/tools/biorag_vlm_roi_figures_v1.py +0 -0
  119. {descriptron_core-2.0.4 → descriptron_core-2.1.0}/src/descriptron_core/tools/biosyslit_rag_retrieval.py +0 -0
  120. {descriptron_core-2.0.4 → descriptron_core-2.1.0}/src/descriptron_core/tools/biosyslit_rag_retrieval_v2.py +0 -0
  121. {descriptron_core-2.0.4 → descriptron_core-2.1.0}/src/descriptron_core/tools/build_monograph_exports_v1.py +0 -0
  122. {descriptron_core-2.0.4 → descriptron_core-2.1.0}/src/descriptron_core/tools/build_species_treatment_docx_v2.py +0 -0
  123. {descriptron_core-2.0.4 → descriptron_core-2.1.0}/src/descriptron_core/tools/color_extraction_with_names-v8.py +0 -0
  124. {descriptron_core-2.0.4 → descriptron_core-2.1.0}/src/descriptron_core/tools/color_phenomics_homology_v2_1.py +0 -0
  125. {descriptron_core-2.0.4 → descriptron_core-2.1.0}/src/descriptron_core/tools/compile_specimen_data.py +0 -0
  126. {descriptron_core-2.0.4 → descriptron_core-2.1.0}/src/descriptron_core/tools/descriptron_credentials.py +0 -0
  127. {descriptron_core-2.0.4 → descriptron_core-2.1.0}/src/descriptron_core/tools/descriptron_rosetta.py +0 -0
  128. {descriptron_core-2.0.4 → descriptron_core-2.1.0}/src/descriptron_core/tools/landmark_gpa_V1.py +0 -0
  129. {descriptron_core-2.0.4 → descriptron_core-2.1.0}/src/descriptron_core/tools/make_reproducibility_bundle.py +0 -0
  130. {descriptron_core-2.0.4 → descriptron_core-2.1.0}/src/descriptron_core/tools/semi_landmark_and_kpts_procrustesV34_GPA.py +0 -0
  131. {descriptron_core-2.0.4 → descriptron_core-2.1.0}/src/descriptron_core/tools/texture_phenomics_from_coco.py +0 -0
  132. {descriptron_core-2.0.4 → 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.4
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,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()