descriptron-vision 2.0.2__tar.gz → 2.0.4__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 (25) hide show
  1. {descriptron_vision-2.0.2 → descriptron_vision-2.0.4}/PKG-INFO +2 -2
  2. descriptron_vision-2.0.4/pyproject.toml +64 -0
  3. descriptron_vision-2.0.4/pyproject.toml.bak_pre_204_20260924_165335 +64 -0
  4. descriptron_vision-2.0.4/src/descriptron_vision/cli.py +66 -0
  5. {descriptron_vision-2.0.2 → descriptron_vision-2.0.4}/src/descriptron_vision/tools/dinov3_landmark_transfer_v51.py +40 -5
  6. descriptron_vision-2.0.4/src/descriptron_vision/tools/dinov3_landmark_transfer_v52.py +1019 -0
  7. {descriptron_vision-2.0.2 → descriptron_vision-2.0.4}/src/descriptron_vision/tools/sam2_pal_batch_v21.py +167 -4
  8. {descriptron_vision-2.0.2 → descriptron_vision-2.0.4}/.gitignore +0 -0
  9. {descriptron_vision-2.0.2 → descriptron_vision-2.0.4}/LICENSE +0 -0
  10. {descriptron_vision-2.0.2 → descriptron_vision-2.0.4}/NOTICE +0 -0
  11. {descriptron_vision-2.0.2 → descriptron_vision-2.0.4}/README.md +0 -0
  12. /descriptron_vision-2.0.2/pyproject.toml → /descriptron_vision-2.0.4/pyproject.toml.bak_pre_203_20260924_145320 +0 -0
  13. {descriptron_vision-2.0.2 → descriptron_vision-2.0.4}/src/descriptron_vision/__init__.py +0 -0
  14. /descriptron_vision-2.0.2/src/descriptron_vision/cli.py → /descriptron_vision-2.0.4/src/descriptron_vision/cli.py.bak_pre_v52_20260924_142454 +0 -0
  15. {descriptron_vision-2.0.2 → descriptron_vision-2.0.4}/src/descriptron_vision/tools/biosyslit_rag_retrieval.py +0 -0
  16. {descriptron_vision-2.0.2 → descriptron_vision-2.0.4}/src/descriptron_vision/tools/measurement_script_to_try_after_kpts_prediction_measure_kpts_V35.py +0 -0
  17. {descriptron_vision-2.0.2 → descriptron_vision-2.0.4}/src/descriptron_vision/tools/torchvision_det/README.md +0 -0
  18. {descriptron_vision-2.0.2 → descriptron_vision-2.0.4}/src/descriptron_vision/tools/torchvision_det/d2_predict_fold_v1.py +0 -0
  19. {descriptron_vision-2.0.2 → descriptron_vision-2.0.4}/src/descriptron_vision/tools/torchvision_det/fix_coco_image_sizes_v1.py +0 -0
  20. {descriptron_vision-2.0.2 → descriptron_vision-2.0.4}/src/descriptron_vision/tools/torchvision_det/render_detections_v1.py +0 -0
  21. {descriptron_vision-2.0.2 → descriptron_vision-2.0.4}/src/descriptron_vision/tools/torchvision_det/run_20k_comparison.sh +0 -0
  22. {descriptron_vision-2.0.2 → descriptron_vision-2.0.4}/src/descriptron_vision/tools/torchvision_det/tv_coco_dataset.py +0 -0
  23. {descriptron_vision-2.0.2 → descriptron_vision-2.0.4}/src/descriptron_vision/tools/torchvision_det/tv_predict_v1.py +0 -0
  24. {descriptron_vision-2.0.2 → descriptron_vision-2.0.4}/src/descriptron_vision/tools/torchvision_det/tv_sweep_v1.py +0 -0
  25. {descriptron_vision-2.0.2 → descriptron_vision-2.0.4}/src/descriptron_vision/tools/torchvision_det/tv_train_v1.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: descriptron-vision
3
- Version: 2.0.2
3
+ Version: 2.0.4
4
4
  Summary: Mask and landmark prediction for Descriptron: torchvision detectors, SAM2-PAL propagation, DINOv3 landmark transfer
5
5
  Project-URL: Homepage, https://github.com/alexrvandam/Descriptron
6
6
  Author: Alex R. Van Dam
@@ -13,7 +13,7 @@ Classifier: Operating System :: OS Independent
13
13
  Classifier: Programming Language :: Python :: 3
14
14
  Classifier: Topic :: Scientific/Engineering :: Image Recognition
15
15
  Requires-Python: >=3.9
16
- Requires-Dist: descriptron-core==2.0.2
16
+ Requires-Dist: descriptron-core==2.0.4
17
17
  Requires-Dist: einops>=0.7
18
18
  Requires-Dist: timm>=0.9
19
19
  Requires-Dist: torch>=2.3
@@ -0,0 +1,64 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "descriptron-vision"
7
+ version = "2.0.4"
8
+ description = "Mask and landmark prediction for Descriptron: torchvision detectors, SAM2-PAL propagation, DINOv3 landmark transfer"
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = { text = "Apache-2.0" }
12
+ authors = [{ name = "Alex R. Van Dam" }]
13
+ classifiers = [
14
+ "Programming Language :: Python :: 3",
15
+ "Topic :: Scientific/Engineering :: Image Recognition",
16
+ "Intended Audience :: Science/Research",
17
+ "License :: OSI Approved :: Apache Software License",
18
+ "Operating System :: OS Independent",
19
+ ]
20
+
21
+ # torch and torchvision are ordinary wheels on Linux, macOS and Windows, so this
22
+ # installs without a compiler. On a Mac the torchvision detectors can use the
23
+ # Apple GPU through PyTorch's MPS device; Detectron2 cannot, which is why the
24
+ # torchvision backend exists.
25
+ #
26
+ # For CUDA, install torch from PyTorch's own index FIRST, then this package:
27
+ # pip install torch torchvision --index-url https://download.pytorch.org/whl/cu121
28
+ dependencies = [
29
+ "descriptron-core==2.0.4",
30
+ # Capped deliberately. Unbounded, pip installs torch 2.14 with ~3 GB of CUDA 13
31
+ # wheels — not the torch 2.4.0+cu121 this system was validated on, and a version
32
+ # in which the AMP calls below are already deprecated. Raise the cap only after
33
+ # testing, not before.
34
+ "torch>=2.3", # torch.amp.GradScaler("cuda") in tv_train_v1 needs 2.3
35
+ "torchvision>=0.18",
36
+ "transformers>=4.38", # Florence-2 scale-bar reading
37
+ "timm>=0.9",
38
+ "einops>=0.7",
39
+ "tqdm>=4.65",
40
+ ]
41
+
42
+ [project.optional-dependencies]
43
+ # SAM2 is not on PyPI and PyPI rejects a git dependency in package metadata, so
44
+ # it cannot be declared here. It is imported lazily and the error message gives
45
+ # the install command:
46
+ # pip install git+https://github.com/facebookresearch/segment-anything-2
47
+ # Detectron2 is the same story, and additionally needs a compiler; see the
48
+ # Docker image if you would rather not build it.
49
+ ocr = ["easyocr>=1.7"]
50
+ dev = ["pytest>=7", "build>=1", "twine>=5"]
51
+
52
+ [project.scripts]
53
+ descriptron-train = "descriptron_vision.cli:train"
54
+ descriptron-predict = "descriptron_vision.cli:predict"
55
+ descriptron-sweep = "descriptron_vision.cli:sweep"
56
+ descriptron-sam2pal = "descriptron_vision.cli:sam2_pal"
57
+ descriptron-dinoland = "descriptron_vision.cli:dinoland"
58
+ descriptron-measure = "descriptron_vision.cli:measure"
59
+
60
+ [project.urls]
61
+ Homepage = "https://github.com/alexrvandam/Descriptron"
62
+
63
+ [tool.hatch.build.targets.wheel]
64
+ packages = ["src/descriptron_vision"]
@@ -0,0 +1,64 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "descriptron-vision"
7
+ version = "2.0.3"
8
+ description = "Mask and landmark prediction for Descriptron: torchvision detectors, SAM2-PAL propagation, DINOv3 landmark transfer"
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = { text = "Apache-2.0" }
12
+ authors = [{ name = "Alex R. Van Dam" }]
13
+ classifiers = [
14
+ "Programming Language :: Python :: 3",
15
+ "Topic :: Scientific/Engineering :: Image Recognition",
16
+ "Intended Audience :: Science/Research",
17
+ "License :: OSI Approved :: Apache Software License",
18
+ "Operating System :: OS Independent",
19
+ ]
20
+
21
+ # torch and torchvision are ordinary wheels on Linux, macOS and Windows, so this
22
+ # installs without a compiler. On a Mac the torchvision detectors can use the
23
+ # Apple GPU through PyTorch's MPS device; Detectron2 cannot, which is why the
24
+ # torchvision backend exists.
25
+ #
26
+ # For CUDA, install torch from PyTorch's own index FIRST, then this package:
27
+ # pip install torch torchvision --index-url https://download.pytorch.org/whl/cu121
28
+ dependencies = [
29
+ "descriptron-core==2.0.3",
30
+ # Capped deliberately. Unbounded, pip installs torch 2.14 with ~3 GB of CUDA 13
31
+ # wheels — not the torch 2.4.0+cu121 this system was validated on, and a version
32
+ # in which the AMP calls below are already deprecated. Raise the cap only after
33
+ # testing, not before.
34
+ "torch>=2.3", # torch.amp.GradScaler("cuda") in tv_train_v1 needs 2.3
35
+ "torchvision>=0.18",
36
+ "transformers>=4.38", # Florence-2 scale-bar reading
37
+ "timm>=0.9",
38
+ "einops>=0.7",
39
+ "tqdm>=4.65",
40
+ ]
41
+
42
+ [project.optional-dependencies]
43
+ # SAM2 is not on PyPI and PyPI rejects a git dependency in package metadata, so
44
+ # it cannot be declared here. It is imported lazily and the error message gives
45
+ # the install command:
46
+ # pip install git+https://github.com/facebookresearch/segment-anything-2
47
+ # Detectron2 is the same story, and additionally needs a compiler; see the
48
+ # Docker image if you would rather not build it.
49
+ ocr = ["easyocr>=1.7"]
50
+ dev = ["pytest>=7", "build>=1", "twine>=5"]
51
+
52
+ [project.scripts]
53
+ descriptron-train = "descriptron_vision.cli:train"
54
+ descriptron-predict = "descriptron_vision.cli:predict"
55
+ descriptron-sweep = "descriptron_vision.cli:sweep"
56
+ descriptron-sam2pal = "descriptron_vision.cli:sam2_pal"
57
+ descriptron-dinoland = "descriptron_vision.cli:dinoland"
58
+ descriptron-measure = "descriptron_vision.cli:measure"
59
+
60
+ [project.urls]
61
+ Homepage = "https://github.com/alexrvandam/Descriptron"
62
+
63
+ [tool.hatch.build.targets.wheel]
64
+ packages = ["src/descriptron_vision"]
@@ -0,0 +1,66 @@
1
+ """
2
+ descriptron_vision.cli — detectors, propagation and landmark transfer
3
+ =====================================================================
4
+
5
+ Same dispatch as `descriptron_core.cli`. The two dependencies that are not on
6
+ PyPI — SAM2 and Detectron2 — are not declared anywhere: PyPI rejects a git
7
+ dependency in package metadata. They are imported lazily by the programs that
8
+ need them, and `require()` below turns the resulting ImportError into the exact
9
+ command to run.
10
+ """
11
+ from __future__ import annotations
12
+
13
+ import sys
14
+
15
+ from descriptron_core.cli import run as _run_core # noqa: F401 (shared behaviour)
16
+ from descriptron_core.cli import data_path # noqa: F401
17
+
18
+ import os
19
+ import runpy
20
+ from importlib.resources import files
21
+ from pathlib import Path
22
+
23
+ INSTALL_HINTS = {
24
+ "sam2": ("SAM2 is not on PyPI, so it cannot be a declared dependency.\n"
25
+ " pip install git+https://github.com/facebookresearch/segment-anything-2"),
26
+ "detectron2": ("Detectron2 is not on PyPI and must be compiled:\n"
27
+ " pip install git+https://github.com/facebookresearch/detectron2\n"
28
+ "It needs a C++/CUDA toolchain and has no macOS wheels. If you would\n"
29
+ "rather not build it, the Docker image carries it ready to run, and the\n"
30
+ "torchvision backend needs none of this."),
31
+ }
32
+
33
+
34
+ def require(module: str):
35
+ """Import an optional heavyweight dependency, or explain how to get it."""
36
+ try:
37
+ return __import__(module)
38
+ except ImportError as exc:
39
+ hint = INSTALL_HINTS.get(module, f" pip install {module}")
40
+ raise SystemExit(f"{module} is required for this step but is not installed.\n"
41
+ f"{hint}\n\n(original error: {exc})") from exc
42
+
43
+
44
+ def tools_dir() -> Path:
45
+ return Path(str(files("descriptron_vision") / "tools"))
46
+
47
+
48
+ def _run(script: str, argv: list[str], subdir: str | None = None) -> None:
49
+ base = tools_dir() / subdir if subdir else tools_dir()
50
+ path = base / script
51
+ if not path.exists():
52
+ raise SystemExit(f"{path} is missing — was sync_sources.py run before building?")
53
+ sys.path.insert(0, str(path.parent))
54
+ sys.path.insert(0, str(tools_dir()))
55
+ os.environ.setdefault("DESCRIPTRON_DATA", str(data_path()))
56
+ sys.argv = [str(path), *argv]
57
+ runpy.run_path(str(path), run_name="__main__")
58
+
59
+
60
+ def train(): _run("tv_train_v1.py", sys.argv[1:], "torchvision_det")
61
+ def predict(): _run("tv_predict_v1.py", sys.argv[1:], "torchvision_det")
62
+ def sweep(): _run("tv_sweep_v1.py", sys.argv[1:], "torchvision_det")
63
+ def sam2_pal(): _run("sam2_pal_batch_v21.py", sys.argv[1:])
64
+ def dinoland(): _run("dinov3_landmark_transfer_v52.py", sys.argv[1:]) # v52 = v51 + --orientation_search
65
+ def measure():
66
+ _run("measurement_script_to_try_after_kpts_prediction_measure_kpts_V35.py", sys.argv[1:])
@@ -639,6 +639,12 @@ def main():
639
639
  "padding) or on the 512x512 letterboxed tensor the model saw")
640
640
  ap.add_argument("--fig_per_page", type=int, default=12,
641
641
  help="panels per preview page; 170 targets in one figure is unusable")
642
+ ap.add_argument("--mirror_refs", action="store_true",
643
+ help="mirror-aware transfer: add a left-right flipped copy of every reference "
644
+ "(image flipped, landmark x mirrored, landmark numbers kept) and, per target, "
645
+ "keep only the handedness group whose references match best (share of "
646
+ "landmarks accepted). Needed when some specimens are photographed as mirror "
647
+ "images; the two handednesses are never mixed in one vote.")
642
648
  ap.add_argument("--emit_refs", action="store_true",
643
649
  help="also write one COCO file per target under <outdir>/per_image/ and "
644
650
  "list the fully-confirmed ones in refs_confirmed.txt, so a checked "
@@ -688,6 +694,26 @@ def main():
688
694
  print(f"reference {fname}: {len(present)}/{len(ids)} landmarks present")
689
695
  print(f"{len(refs)} reference(s); landmark ids {slot_ids}")
690
696
 
697
+ # ---- mirror-aware mode: handedness of each reference, plus flipped copies ----
698
+ # Handedness = sign of the signed area (shoelace) of the landmarks in their fixed
699
+ # numbering order: a mirror image reverses it. A flipped reference keeps its landmark
700
+ # numbers, so its votes stay in the same slots.
701
+ def _handedness(pts):
702
+ x, y = pts[:, 0], pts[:, 1]
703
+ return 1 if 0.5 * np.sum(x * np.roll(y, -1) - np.roll(x, -1) * y) >= 0 else -1
704
+ ref_hand = [_handedness(r[3]) for r in refs]
705
+ if a.mirror_refs:
706
+ for (rname, _fa, mk, pts, present, im), hnd in list(zip(refs, ref_hand)):
707
+ im_f = im.transpose(Image.FLIP_LEFT_RIGHT)
708
+ pts_f = pts.copy()
709
+ pts_f[:, 0] = bb.img_dim - pts_f[:, 0]
710
+ refs.append((rname + " [flipped]", bb.embed(im_f, LAYER, "none", a.facet),
711
+ mk[:, ::-1].copy(), pts_f, present, im_f))
712
+ ref_hand.append(-hnd)
713
+ print(f"mirror-aware: {len(refs)} references "
714
+ f"({ref_hand.count(1)} one handedness, {ref_hand.count(-1)} the other)")
715
+ two_hands = a.mirror_refs and len(set(ref_hand)) == 2
716
+
691
717
  targets = sorted(_glob.glob(a.batch_glob))
692
718
  ref_names = {os.path.basename(r[0]).replace('_keypoints.json', '') for r in refs}
693
719
  targets = [t for t in targets if os.path.abspath(t) != os.path.abspath(a.imgA)]
@@ -701,16 +727,24 @@ def main():
701
727
  bb, rawB, LAYER, tp, a.mask_dir)
702
728
  fb = bb.embed(imgB, LAYER, "none", a.facet)
703
729
 
704
- votes = {k: [] for k in range(len(slot_ids))} # slot -> [(xy, accepted)]
705
- for rname, fa, maskA_r, pts, present, _im in refs:
730
+ group_votes, group_score = {}, {}
731
+ for gi, ((rname, fa, maskA_r, pts, present, _im), hnd) in enumerate(zip(refs, ref_hand)):
732
+ g = hnd if two_hands else 0
733
+ votes = group_votes.setdefault(g, {k: [] for k in range(len(slot_ids))})
706
734
  A = None
707
735
  if a.align == "feature":
708
736
  A = feature_affine(fa, fb, maskA_r, maskB, bb.patch, bb.grid)
709
737
  if A is None:
710
738
  A = pca_affine(maskA_r, maskB)
711
739
  r = transfer(fa, fb, pts, A, bb.patch, bb.grid, radius_patches=a.radius)
740
+ group_score.setdefault(g, []).append(float(np.mean(r.accepted)))
712
741
  for j, slot in enumerate(present):
713
742
  votes[slot].append((r.pred[j], bool(r.accepted[j])))
743
+ # RANSAC fits rotation/scale/shift only, never a reflection, so references of the
744
+ # wrong handedness get most landmarks rejected: the higher mean accepted share wins
745
+ chosen = max(group_votes, key=lambda g: np.mean(group_score[g]))
746
+ votes = group_votes[chosen]
747
+ hand_used = chosen if two_hands else None
714
748
 
715
749
  # ---- consensus: median of the votes, spread = agreement ------------
716
750
  cons = {}
@@ -726,7 +760,7 @@ def main():
726
760
 
727
761
  results.append((os.path.basename(tp).rsplit('.', 1)[0], imgB, maskB,
728
762
  cons, (W0, H0), os.path.basename(tp),
729
- (tscale, tox, toy, tcx, tcy), tp))
763
+ (tscale, tox, toy, tcx, tcy), tp, hand_used))
730
764
  agree = sum(1 for k in cons if cons[k][0] is not None and cons[k][1] > 0)
731
765
  print(f"{os.path.basename(tp)[:44]:<46} confirmed {agree}/{len(slot_ids)}")
732
766
 
@@ -745,7 +779,7 @@ def main():
745
779
  os.makedirs(per_image_dir, exist_ok=True)
746
780
  confirmed_refs = []
747
781
  anns_by_name = {} # name -> annotation, for the preview
748
- for nm, imgB, mkB, cons, (W0, H0), fname, (tscale, tox, toy, tcx, tcy), tp in results:
782
+ for nm, imgB, mkB, cons, (W0, H0), fname, (tscale, tox, toy, tcx, tcy), tp, hand_used in results:
749
783
  kp, nvis, detail = [], 0, []
750
784
  xs, ys = [], []
751
785
  for k, sid in enumerate(slot_ids):
@@ -770,6 +804,7 @@ def main():
770
804
  "bbox": bbox, "iscrowd": 0,
771
805
  "point_order": slot_ids,
772
806
  "n_references": len(refs),
807
+ "handedness_used": hand_used,
773
808
  "landmark_detail": detail}
774
809
  coco["images"].append(img_entry)
775
810
  coco["annotations"].append(ann)
@@ -807,7 +842,7 @@ def main():
807
842
  n = len(page); cols = min(3, n); rows = int(np.ceil(n / cols))
808
843
  fig, axes = plt.subplots(rows, cols, figsize=(4.0 * cols, 4.2 * rows), dpi=150)
809
844
  axes = np.atleast_1d(axes).ravel()
810
- for ax_, (nm, imgB, mkB, cons, (W0, H0), _fn, _lb, tp) in zip(axes, page):
845
+ for ax_, (nm, imgB, mkB, cons, (W0, H0), _fn, _lb, tp, _hand) in zip(axes, page):
811
846
  if a.preview == "original":
812
847
  ax_.imshow(Image.open(tp).convert("RGB"))
813
848
  kpx = anns_by_name[nm]["keypoints"]