diff --git a/README.md b/README.md
index f1f1db7d916392bfcdb42a0715bcff08dbcfe5da..54d6faf4b4c9d31553cbb03e52d88dfea2054224 100644
--- a/README.md
+++ b/README.md
@@ -44,6 +44,7 @@ The tool currently supports various popular image matching algorithms, namely:
| Algorithm | Supported | Conference/Journal | Year | GitHub Link |
|------------------|-----------|--------------------|------|-------------|
+| DaD | ✅ | ARXIV | 2025 | [Link](https://github.com/Parskatt/dad) |
| MINIMA | ✅ | ARXIV | 2024 | [Link](https://github.com/LSXI7/MINIMA) |
| XoFTR | ✅ | CVPR | 2024 | [Link](https://github.com/OnderT/XoFTR) |
| EfficientLoFTR | ✅ | CVPR | 2024 | [Link](https://github.com/zju3dv/EfficientLoFTR) |
diff --git a/config/config.yaml b/config/config.yaml
index bfe81370efae6bf4386579e0fc61a7aa0239f75c..2ae87542ef24edbd32be8ceece9a42acad3ff5b4 100644
--- a/config/config.yaml
+++ b/config/config.yaml
@@ -43,6 +43,17 @@ matcher_zoo:
# low, medium, high
efficiency: low
+ dad(RoMa):
+ matcher: dad_roma
+ skip_ci: true
+ dense: true
+ info:
+ name: Dad(RoMa) #dispaly name
+ source: "ARXIV 2025"
+ github: https://github.com/example/example
+ paper: https://arxiv.org/abs/2503.07347
+ display: true
+ efficiency: low # low, medium, high
minima(loftr):
matcher: minima_loftr
dense: true
@@ -50,6 +61,7 @@ matcher_zoo:
name: MINIMA(LoFTR) #dispaly name
source: "ARXIV 2024"
paper: https://arxiv.org/abs/2412.19412
+ github: https://github.com/LSXI7/MINIMA
display: true
minima(RoMa):
matcher: minima_roma
@@ -59,6 +71,7 @@ matcher_zoo:
name: MINIMA(RoMa) #dispaly name
source: "ARXIV 2024"
paper: https://arxiv.org/abs/2412.19412
+ github: https://github.com/LSXI7/MINIMA
display: false
efficiency: low # low, medium, high
omniglue:
@@ -164,6 +177,16 @@ matcher_zoo:
paper: https://arxiv.org/pdf/2404.09692
project: null
display: true
+ jamma:
+ matcher: jamma
+ dense: true
+ info:
+ name: Jamma #dispaly name
+ source: "CVPR 2024"
+ github: https://github.com/OnderT/XoFTR
+ paper: https://arxiv.org/pdf/2404.09692
+ project: null
+ display: false
cotr:
enable: false
skip_ci: true
diff --git a/imcui/hloc/match_dense.py b/imcui/hloc/match_dense.py
index 978db58b2438b337549a810196fdf017e0195e85..f8bdd0c1a7a3d5be739c3e2cc9895878ebe70345 100644
--- a/imcui/hloc/match_dense.py
+++ b/imcui/hloc/match_dense.py
@@ -102,6 +102,23 @@ confs = {
"max_error": 1, # max error for assigned keypoints (in px)
"cell_size": 1, # size of quantization patch (max 1 kp/patch)
},
+ "jamma": {
+ "output": "matches-jamma",
+ "model": {
+ "name": "jamma",
+ "weights": "jamma_weight.ckpt",
+ "max_keypoints": 2000,
+ "match_threshold": 0.3,
+ },
+ "preprocessing": {
+ "grayscale": True,
+ "resize_max": 1024,
+ "dfactor": 16,
+ "width": 832,
+ "height": 832,
+ "force_resize": True,
+ },
+ },
# "loftr_quadtree": {
# "output": "matches-loftr-quadtree",
# "model": {
@@ -295,7 +312,25 @@ confs = {
},
"preprocessing": {
"grayscale": False,
- "force_resize": True,
+ "force_resize": False,
+ "resize_max": 1024,
+ "width": 320,
+ "height": 240,
+ "dfactor": 8,
+ },
+ },
+ "dad_roma": {
+ "output": "matches-dad_roma",
+ "model": {
+ "name": "dad_roma",
+ "weights": "outdoor",
+ "model_name": "roma_outdoor.pth",
+ "max_keypoints": 2000,
+ "match_threshold": 0.2,
+ },
+ "preprocessing": {
+ "grayscale": False,
+ "force_resize": False,
"resize_max": 1024,
"width": 320,
"height": 240,
@@ -1010,9 +1045,17 @@ def match_images(model, image_0, image_1, conf, device="cpu"):
# Rescale keypoints and move to cpu
if "keypoints0" in pred.keys() and "keypoints1" in pred.keys():
kpts0, kpts1 = pred["keypoints0"], pred["keypoints1"]
+ mkpts0, mkpts1 = pred.get("mkeypoints0"), pred.get("mkeypoints1")
+ if mkpts0 is None or mkpts1 is None:
+ mkpts0 = kpts0
+ mkpts1 = kpts1
+
kpts0_origin = scale_keypoints(kpts0 + 0.5, s0) - 0.5
kpts1_origin = scale_keypoints(kpts1 + 0.5, s1) - 0.5
+ mkpts0_origin = scale_keypoints(mkpts0 + 0.5, s0) - 0.5
+ mkpts1_origin = scale_keypoints(mkpts1 + 0.5, s1) - 0.5
+
ret = {
"image0": image0.squeeze().cpu().numpy(),
"image1": image1.squeeze().cpu().numpy(),
@@ -1022,10 +1065,10 @@ def match_images(model, image_0, image_1, conf, device="cpu"):
"keypoints1": kpts1.cpu().numpy(),
"keypoints0_orig": kpts0_origin.cpu().numpy(),
"keypoints1_orig": kpts1_origin.cpu().numpy(),
- "mkeypoints0": kpts0.cpu().numpy(),
- "mkeypoints1": kpts1.cpu().numpy(),
- "mkeypoints0_orig": kpts0_origin.cpu().numpy(),
- "mkeypoints1_orig": kpts1_origin.cpu().numpy(),
+ "mkeypoints0": mkpts0.cpu().numpy(),
+ "mkeypoints1": mkpts1.cpu().numpy(),
+ "mkeypoints0_orig": mkpts0_origin.cpu().numpy(),
+ "mkeypoints1_orig": mkpts1_origin.cpu().numpy(),
"original_size0": np.array(image_0.shape[:2][::-1]),
"original_size1": np.array(image_1.shape[:2][::-1]),
"new_size0": np.array(image0.shape[-2:][::-1]),
diff --git a/imcui/hloc/matchers/dad_roma.py b/imcui/hloc/matchers/dad_roma.py
new file mode 100644
index 0000000000000000000000000000000000000000..46832d40937696cf4ff4d1b3d52da635f9a02f2c
--- /dev/null
+++ b/imcui/hloc/matchers/dad_roma.py
@@ -0,0 +1,121 @@
+import sys
+from pathlib import Path
+import tempfile
+import torch
+from PIL import Image
+
+from .. import MODEL_REPO_ID, logger
+from ..utils.base_model import BaseModel
+
+roma_path = Path(__file__).parent / "../../third_party/RoMa"
+sys.path.append(str(roma_path))
+from romatch.models.model_zoo import roma_model
+
+dad_path = Path(__file__).parent / "../../third_party/dad"
+sys.path.append(str(dad_path))
+import dad as dad_detector
+
+device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
+
+
+class Dad(BaseModel):
+ default_conf = {
+ "name": "two_view_pipeline",
+ "model_name": "roma_outdoor.pth",
+ "model_utils_name": "dinov2_vitl14_pretrain.pth",
+ "max_keypoints": 3000,
+ "coarse_res": (560, 560),
+ "upsample_res": (864, 1152),
+ }
+ required_inputs = [
+ "image0",
+ "image1",
+ ]
+
+ # Initialize the line matcher
+ def _init(self, conf):
+ model_path = self._download_model(
+ repo_id=MODEL_REPO_ID,
+ filename="{}/{}".format("roma", self.conf["model_name"]),
+ )
+
+ dinov2_weights = self._download_model(
+ repo_id=MODEL_REPO_ID,
+ filename="{}/{}".format("roma", self.conf["model_utils_name"]),
+ )
+
+ logger.info("Loading Dad + Roma model")
+ # load the model
+ weights = torch.load(model_path, map_location="cpu")
+ dinov2_weights = torch.load(dinov2_weights, map_location="cpu")
+
+ if str(device) == "cpu":
+ amp_dtype = torch.float32
+ else:
+ amp_dtype = torch.float16
+
+ self.matcher = roma_model(
+ resolution=self.conf["coarse_res"],
+ upsample_preds=True,
+ weights=weights,
+ dinov2_weights=dinov2_weights,
+ device=device,
+ amp_dtype=amp_dtype,
+ )
+ self.matcher.upsample_res = self.conf["upsample_res"]
+ self.matcher.symmetric = False
+
+ self.detector = dad_detector.load_DaD()
+ logger.info("Load Dad + Roma model done.")
+
+ def _forward(self, data):
+ img0 = data["image0"].cpu().numpy().squeeze() * 255
+ img1 = data["image1"].cpu().numpy().squeeze() * 255
+ img0 = img0.transpose(1, 2, 0)
+ img1 = img1.transpose(1, 2, 0)
+ img0 = Image.fromarray(img0.astype("uint8"))
+ img1 = Image.fromarray(img1.astype("uint8"))
+ W_A, H_A = img0.size
+ W_B, H_B = img1.size
+
+ # hack: bad way to save then match
+ with (
+ tempfile.NamedTemporaryFile(suffix=".png", delete=False) as temp_img0,
+ tempfile.NamedTemporaryFile(suffix=".png", delete=False) as temp_img1,
+ ):
+ img0_path = temp_img0.name
+ img1_path = temp_img1.name
+ img0.save(img0_path)
+ img1.save(img1_path)
+
+ # Match
+ warp, certainty = self.matcher.match(img0_path, img1_path, device=device)
+ # Detect
+ keypoints_A = self.detector.detect_from_path(
+ img0_path,
+ num_keypoints=self.conf["max_keypoints"],
+ )["keypoints"][0]
+ keypoints_B = self.detector.detect_from_path(
+ img1_path,
+ num_keypoints=self.conf["max_keypoints"],
+ )["keypoints"][0]
+ matches = self.matcher.match_keypoints(
+ keypoints_A,
+ keypoints_B,
+ warp,
+ certainty,
+ return_tuple=False,
+ )
+
+ # Sample matches for estimation
+ kpts1, kpts2 = self.matcher.to_pixel_coordinates(matches, H_A, W_A, H_B, W_B)
+ offset = self.detector.topleft - 0
+ kpts1, kpts2 = kpts1 - offset, kpts2 - offset
+ pred = {
+ "keypoints0": self.matcher._to_pixel_coordinates(keypoints_A, H_A, W_A),
+ "keypoints1": self.matcher._to_pixel_coordinates(keypoints_B, H_B, W_B),
+ "mkeypoints0": kpts1,
+ "mkeypoints1": kpts2,
+ "mconf": torch.ones_like(kpts1[:, 0]),
+ }
+ return pred
diff --git a/imcui/hloc/matchers/roma.py b/imcui/hloc/matchers/roma.py
index 0fb83e1c22a8d22d85fa39a989ed4a8897989354..2571b4817e0aa6f381a223977957031796846339 100644
--- a/imcui/hloc/matchers/roma.py
+++ b/imcui/hloc/matchers/roma.py
@@ -20,6 +20,8 @@ class Roma(BaseModel):
"model_name": "roma_outdoor.pth",
"model_utils_name": "dinov2_vitl14_pretrain.pth",
"max_keypoints": 3000,
+ "coarse_res": (560, 560),
+ "upsample_res": (864, 1152),
}
required_inputs = [
"image0",
@@ -43,15 +45,19 @@ class Roma(BaseModel):
weights = torch.load(model_path, map_location="cpu")
dinov2_weights = torch.load(dinov2_weights, map_location="cpu")
+ if str(device) == "cpu":
+ amp_dtype = torch.float32
+ else:
+ amp_dtype = torch.float16
self.net = roma_model(
- resolution=(14 * 8 * 6, 14 * 8 * 6),
- upsample_preds=False,
+ resolution=self.conf["coarse_res"],
+ upsample_preds=True,
weights=weights,
dinov2_weights=dinov2_weights,
device=device,
- # temp fix issue: https://github.com/Parskatt/RoMa/issues/26
- amp_dtype=torch.float32,
+ amp_dtype=amp_dtype,
)
+ self.matcher.upsample_res = self.conf["upsample_res"]
logger.info("Load Roma model done.")
def _forward(self, data):
diff --git a/imcui/hloc/matchers/xfeat_dense.py b/imcui/hloc/matchers/xfeat_dense.py
index 7b6956636c126d1d482c1dc3ff18e23978a9163b..c8b2f56f125f8b375c43e6e0c726c30f23106754 100644
--- a/imcui/hloc/matchers/xfeat_dense.py
+++ b/imcui/hloc/matchers/xfeat_dense.py
@@ -47,8 +47,10 @@ class XFeatDense(BaseModel):
# we use results from one batch
matches = matches[0]
pred = {
- "keypoints0": matches[:, :2],
- "keypoints1": matches[:, 2:],
+ "keypoints0": out0["keypoints"].squeeze(),
+ "keypoints1": out1["keypoints"].squeeze(),
+ "mkeypoints0": matches[:, :2],
+ "mkeypoints1": matches[:, 2:],
"mconf": torch.ones_like(matches[:, 0]),
}
return pred
diff --git a/imcui/hloc/matchers/xfeat_lightglue.py b/imcui/hloc/matchers/xfeat_lightglue.py
index 6041d29e1473cc2b0cebc3ca64f2b01df2753f06..6191ca74ae47c3f4226c9406140d91c0e4aea1e1 100644
--- a/imcui/hloc/matchers/xfeat_lightglue.py
+++ b/imcui/hloc/matchers/xfeat_lightglue.py
@@ -41,8 +41,10 @@ class XFeatLightGlue(BaseModel):
mkpts_0 = torch.from_numpy(mkpts_0) # n x 2
mkpts_1 = torch.from_numpy(mkpts_1) # n x 2
pred = {
- "keypoints0": mkpts_0,
- "keypoints1": mkpts_1,
+ "keypoints0": out0["keypoints"].squeeze(),
+ "keypoints1": out1["keypoints"].squeeze(),
+ "mkeypoints0": mkpts_0,
+ "mkeypoints1": mkpts_1,
"mconf": torch.ones_like(mkpts_0[:, 0]),
}
return pred
diff --git a/imcui/third_party/RoMa/.gitignore b/imcui/third_party/RoMa/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..7f8801c4b89dd12eeca1e5709e8161f017370a44
--- /dev/null
+++ b/imcui/third_party/RoMa/.gitignore
@@ -0,0 +1,11 @@
+*.egg-info*
+*.vscode*
+*__pycache__*
+vis*
+workspace*
+.venv
+.DS_Store
+jobs/*
+*ignore_me*
+*.pth
+wandb*
\ No newline at end of file
diff --git a/imcui/third_party/RoMa/LICENSE b/imcui/third_party/RoMa/LICENSE
new file mode 100644
index 0000000000000000000000000000000000000000..ca95157052a76debc473afb395bffae0c1329e63
--- /dev/null
+++ b/imcui/third_party/RoMa/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2023 Johan Edstedt
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/imcui/third_party/RoMa/README.md b/imcui/third_party/RoMa/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..8458762622603dc8d0b4a89ba5cb3354bee7deb5
--- /dev/null
+++ b/imcui/third_party/RoMa/README.md
@@ -0,0 +1,123 @@
+#
+
+
RoMa 🏛️:
Robust Dense Feature Matching
⭐CVPR 2024⭐
+
+ Johan Edstedt
+ ·
+ Qiyu Sun
+ ·
+ Georg Bökman
+ ·
+ Mårten Wadenbäck
+ ·
+ Michael Felsberg
+
+
+ Paper |
+ Project Page
+
+
+
+
+
+
+
+ RoMa is the robust dense feature matcher capable of estimating pixel-dense warps and reliable certainties for almost any image pair.
+
+
+## Setup/Install
+In your python environment (tested on Linux python 3.10), run:
+```bash
+pip install -e .
+```
+## Demo / How to Use
+We provide two demos in the [demos folder](demo).
+Here's the gist of it:
+```python
+from romatch import roma_outdoor
+roma_model = roma_outdoor(device=device)
+# Match
+warp, certainty = roma_model.match(imA_path, imB_path, device=device)
+# Sample matches for estimation
+matches, certainty = roma_model.sample(warp, certainty)
+# Convert to pixel coordinates (RoMa produces matches in [-1,1]x[-1,1])
+kptsA, kptsB = roma_model.to_pixel_coordinates(matches, H_A, W_A, H_B, W_B)
+# Find a fundamental matrix (or anything else of interest)
+F, mask = cv2.findFundamentalMat(
+ kptsA.cpu().numpy(), kptsB.cpu().numpy(), ransacReprojThreshold=0.2, method=cv2.USAC_MAGSAC, confidence=0.999999, maxIters=10000
+)
+```
+
+**New**: You can also match arbitrary keypoints with RoMa. See [match_keypoints](romatch/models/matcher.py) in RegressionMatcher.
+
+## Settings
+
+### Resolution
+By default RoMa uses an initial resolution of (560,560) which is then upsampled to (864,864).
+You can change this at construction (see roma_outdoor kwargs).
+You can also change this later, by changing the roma_model.w_resized, roma_model.h_resized, and roma_model.upsample_res.
+
+### Sampling
+roma_model.sample_thresh controls the thresholding used when sampling matches for estimation. In certain cases a lower or higher threshold may improve results.
+
+
+## Reproducing Results
+The experiments in the paper are provided in the [experiments folder](experiments).
+
+### Training
+1. First follow the instructions provided here: https://github.com/Parskatt/DKM for downloading and preprocessing datasets.
+2. Run the relevant experiment, e.g.,
+```bash
+torchrun --nproc_per_node=4 --nnodes=1 --rdzv_backend=c10d experiments/roma_outdoor.py
+```
+### Testing
+```bash
+python experiments/roma_outdoor.py --only_test --benchmark mega-1500
+```
+## License
+All our code except DINOv2 is MIT license.
+DINOv2 has an Apache 2 license [DINOv2](https://github.com/facebookresearch/dinov2/blob/main/LICENSE).
+
+## Acknowledgement
+Our codebase builds on the code in [DKM](https://github.com/Parskatt/DKM).
+
+## Tiny RoMa
+If you find that RoMa is too heavy, you might want to try Tiny RoMa which is built on top of XFeat.
+```python
+from romatch import tiny_roma_v1_outdoor
+tiny_roma_model = tiny_roma_v1_outdoor(device=device)
+```
+Mega1500:
+| | AUC@5 | AUC@10 | AUC@20 |
+|----------|----------|----------|----------|
+| XFeat | 46.4 | 58.9 | 69.2 |
+| XFeat* | 51.9 | 67.2 | 78.9 |
+| Tiny RoMa v1 | 56.4 | 69.5 | 79.5 |
+| RoMa | - | - | - |
+
+Mega-8-Scenes (See DKM):
+| | AUC@5 | AUC@10 | AUC@20 |
+|----------|----------|----------|----------|
+| XFeat | - | - | - |
+| XFeat* | 50.1 | 64.4 | 75.2 |
+| Tiny RoMa v1 | 57.7 | 70.5 | 79.6 |
+| RoMa | - | - | - |
+
+IMC22 :'):
+| | mAA@10 |
+|----------|----------|
+| XFeat | 42.1 |
+| XFeat* | - |
+| Tiny RoMa v1 | 42.2 |
+| RoMa | - |
+
+## BibTeX
+If you find our models useful, please consider citing our paper!
+```
+@article{edstedt2024roma,
+title={{RoMa: Robust Dense Feature Matching}},
+author={Edstedt, Johan and Sun, Qiyu and Bökman, Georg and Wadenbäck, Mårten and Felsberg, Michael},
+journal={IEEE Conference on Computer Vision and Pattern Recognition},
+year={2024}
+}
+```
diff --git a/imcui/third_party/RoMa/data/.gitignore b/imcui/third_party/RoMa/data/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..c96a04f008ee21e260b28f7701595ed59e2839e3
--- /dev/null
+++ b/imcui/third_party/RoMa/data/.gitignore
@@ -0,0 +1,2 @@
+*
+!.gitignore
\ No newline at end of file
diff --git a/imcui/third_party/RoMa/requirements.txt b/imcui/third_party/RoMa/requirements.txt
new file mode 100644
index 0000000000000000000000000000000000000000..81360b228245bc573dba770458e4dbeb76d0dd9f
--- /dev/null
+++ b/imcui/third_party/RoMa/requirements.txt
@@ -0,0 +1,14 @@
+torch
+einops
+torchvision
+opencv-python
+kornia
+albumentations
+loguru
+tqdm
+matplotlib
+h5py
+wandb
+timm
+poselib
+#xformers # Optional, used for memefficient attention
\ No newline at end of file
diff --git a/imcui/third_party/RoMa/romatch/models/matcher.py b/imcui/third_party/RoMa/romatch/models/matcher.py
index 8108a92393f4657e2c87d75c4072a46e7d61cdd6..6cc45b7866d7d519c1f75127e21c7530c2c76224 100644
--- a/imcui/third_party/RoMa/romatch/models/matcher.py
+++ b/imcui/third_party/RoMa/romatch/models/matcher.py
@@ -11,7 +11,7 @@ from PIL import Image
from romatch.utils import get_tuple_transform_ops
from romatch.utils.local_correlation import local_correlation
-from romatch.utils.utils import cls_to_flow_refine, get_autocast_params
+from romatch.utils.utils import check_rgb, cls_to_flow_refine, get_autocast_params, check_not_i16
from romatch.utils.kde import kde
class ConvRefiner(nn.Module):
@@ -573,12 +573,30 @@ class RegressionMatcher(nn.Module):
kpts_B = torch.stack((2/W_B * kpts_B[...,0] - 1, 2/H_B * kpts_B[...,1] - 1),axis=-1)
return kpts_A, kpts_B
- def match_keypoints(self, x_A, x_B, warp, certainty, return_tuple = True, return_inds = False):
- x_A_to_B = F.grid_sample(warp[...,-2:].permute(2,0,1)[None], x_A[None,None], align_corners = False, mode = "bilinear")[0,:,0].mT
- cert_A_to_B = F.grid_sample(certainty[None,None,...], x_A[None,None], align_corners = False, mode = "bilinear")[0,0,0]
+ def match_keypoints(
+ self, x_A, x_B, warp, certainty, return_tuple=True, return_inds=False, max_dist = 0.005, cert_th = 0,
+ ):
+ x_A_to_B = F.grid_sample(
+ warp[..., -2:].permute(2, 0, 1)[None],
+ x_A[None, None],
+ align_corners=False,
+ mode="bilinear",
+ )[0, :, 0].mT
+ cert_A_to_B = F.grid_sample(
+ certainty[None, None, ...],
+ x_A[None, None],
+ align_corners=False,
+ mode="bilinear",
+ )[0, 0, 0]
D = torch.cdist(x_A_to_B, x_B)
- inds_A, inds_B = torch.nonzero((D == D.min(dim=-1, keepdim = True).values) * (D == D.min(dim=-2, keepdim = True).values) * (cert_A_to_B[:,None] > self.sample_thresh), as_tuple = True)
-
+ inds_A, inds_B = torch.nonzero(
+ (D == D.min(dim=-1, keepdim=True).values)
+ * (D == D.min(dim=-2, keepdim=True).values)
+ * (cert_A_to_B[:, None] > cert_th)
+ * (D < max_dist),
+ as_tuple=True,
+ )
+
if return_tuple:
if return_inds:
return inds_A, inds_B
@@ -586,25 +604,38 @@ class RegressionMatcher(nn.Module):
return x_A[inds_A], x_B[inds_B]
else:
if return_inds:
- return torch.cat((inds_A, inds_B),dim=-1)
+ return torch.cat((inds_A, inds_B), dim=-1)
else:
- return torch.cat((x_A[inds_A], x_B[inds_B]),dim=-1)
+ return torch.cat((x_A[inds_A], x_B[inds_B]), dim=-1)
@torch.inference_mode()
def match(
self,
- im_A_path,
- im_B_path,
+ im_A_input,
+ im_B_input,
*args,
batched=False,
- device = None,
+ device=None,
):
if device is None:
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
- if isinstance(im_A_path, (str, os.PathLike)):
- im_A, im_B = Image.open(im_A_path).convert("RGB"), Image.open(im_B_path).convert("RGB")
+
+ # Check if inputs are file paths or already loaded images
+ if isinstance(im_A_input, (str, os.PathLike)):
+ im_A = Image.open(im_A_input)
+ check_not_i16(im_A)
+ im_A = im_A.convert("RGB")
+ else:
+ check_rgb(im_A_input)
+ im_A = im_A_input
+
+ if isinstance(im_B_input, (str, os.PathLike)):
+ im_B = Image.open(im_B_input)
+ check_not_i16(im_B)
+ im_B = im_B.convert("RGB")
else:
- im_A, im_B = im_A_path, im_B_path
+ check_rgb(im_B_input)
+ im_B = im_B_input
symmetric = self.symmetric
self.train(False)
@@ -616,9 +647,9 @@ class RegressionMatcher(nn.Module):
# Get images in good format
ws = self.w_resized
hs = self.h_resized
-
+
test_transform = get_tuple_transform_ops(
- resize=(hs, ws), normalize=True, clahe = False
+ resize=(hs, ws), normalize=True, clahe=False
)
im_A, im_B = test_transform((im_A, im_B))
batch = {"im_A": im_A[None].to(device), "im_B": im_B[None].to(device)}
@@ -633,20 +664,20 @@ class RegressionMatcher(nn.Module):
finest_scale = 1
# Run matcher
if symmetric:
- corresps = self.forward_symmetric(batch)
+ corresps = self.forward_symmetric(batch)
else:
- corresps = self.forward(batch, batched = True)
+ corresps = self.forward(batch, batched=True)
if self.upsample_preds:
hs, ws = self.upsample_res
-
+
if self.attenuate_cert:
low_res_certainty = F.interpolate(
- corresps[16]["certainty"], size=(hs, ws), align_corners=False, mode="bilinear"
+ corresps[16]["certainty"], size=(hs, ws), align_corners=False, mode="bilinear"
)
cert_clamp = 0
factor = 0.5
- low_res_certainty = factor*low_res_certainty*(low_res_certainty < cert_clamp)
+ low_res_certainty = factor * low_res_certainty * (low_res_certainty < cert_clamp)
if self.upsample_preds:
finest_corresps = corresps[finest_scale]
@@ -654,34 +685,39 @@ class RegressionMatcher(nn.Module):
test_transform = get_tuple_transform_ops(
resize=(hs, ws), normalize=True
)
- im_A, im_B = test_transform((Image.open(im_A_path).convert('RGB'), Image.open(im_B_path).convert('RGB')))
+ if isinstance(im_A_input, (str, os.PathLike)):
+ im_A, im_B = test_transform(
+ (Image.open(im_A_input).convert('RGB'), Image.open(im_B_input).convert('RGB')))
+ else:
+ im_A, im_B = test_transform((im_A_input, im_B_input))
+
im_A, im_B = im_A[None].to(device), im_B[None].to(device)
scale_factor = math.sqrt(self.upsample_res[0] * self.upsample_res[1] / (self.w_resized * self.h_resized))
batch = {"im_A": im_A, "im_B": im_B, "corresps": finest_corresps}
if symmetric:
- corresps = self.forward_symmetric(batch, upsample = True, batched=True, scale_factor = scale_factor)
+ corresps = self.forward_symmetric(batch, upsample=True, batched=True, scale_factor=scale_factor)
else:
- corresps = self.forward(batch, batched = True, upsample=True, scale_factor = scale_factor)
-
- im_A_to_im_B = corresps[finest_scale]["flow"]
+ corresps = self.forward(batch, batched=True, upsample=True, scale_factor=scale_factor)
+
+ im_A_to_im_B = corresps[finest_scale]["flow"]
certainty = corresps[finest_scale]["certainty"] - (low_res_certainty if self.attenuate_cert else 0)
if finest_scale != 1:
im_A_to_im_B = F.interpolate(
- im_A_to_im_B, size=(hs, ws), align_corners=False, mode="bilinear"
+ im_A_to_im_B, size=(hs, ws), align_corners=False, mode="bilinear"
)
certainty = F.interpolate(
- certainty, size=(hs, ws), align_corners=False, mode="bilinear"
+ certainty, size=(hs, ws), align_corners=False, mode="bilinear"
)
im_A_to_im_B = im_A_to_im_B.permute(
0, 2, 3, 1
- )
+ )
# Create im_A meshgrid
im_A_coords = torch.meshgrid(
(
torch.linspace(-1 + 1 / hs, 1 - 1 / hs, hs, device=device),
torch.linspace(-1 + 1 / ws, 1 - 1 / ws, ws, device=device),
),
- indexing = 'ij'
+ indexing='ij'
)
im_A_coords = torch.stack((im_A_coords[1], im_A_coords[0]))
im_A_coords = im_A_coords[None].expand(b, 2, hs, ws)
@@ -689,14 +725,14 @@ class RegressionMatcher(nn.Module):
im_A_coords = im_A_coords.permute(0, 2, 3, 1)
if (im_A_to_im_B.abs() > 1).any() and True:
wrong = (im_A_to_im_B.abs() > 1).sum(dim=-1) > 0
- certainty[wrong[:,None]] = 0
+ certainty[wrong[:, None]] = 0
im_A_to_im_B = torch.clamp(im_A_to_im_B, -1, 1)
if symmetric:
A_to_B, B_to_A = im_A_to_im_B.chunk(2)
q_warp = torch.cat((im_A_coords, A_to_B), dim=-1)
im_B_coords = im_A_coords
s_warp = torch.cat((B_to_A, im_B_coords), dim=-1)
- warp = torch.cat((q_warp, s_warp),dim=2)
+ warp = torch.cat((q_warp, s_warp), dim=2)
certainty = torch.cat(certainty.chunk(2), dim=3)
else:
warp = torch.cat((im_A_coords, im_A_to_im_B), dim=-1)
diff --git a/imcui/third_party/RoMa/romatch/models/transformer/layers/attention.py b/imcui/third_party/RoMa/romatch/models/transformer/layers/attention.py
index 1f9b0c94b40967dfdff4f261c127cbd21328c905..83725859ed74bf631be0b556f9eed3a17121b3f3 100644
--- a/imcui/third_party/RoMa/romatch/models/transformer/layers/attention.py
+++ b/imcui/third_party/RoMa/romatch/models/transformer/layers/attention.py
@@ -22,7 +22,7 @@ try:
XFORMERS_AVAILABLE = True
except ImportError:
- logger.warning("xFormers not available")
+ # logger.warning("xFormers not available")
XFORMERS_AVAILABLE = False
diff --git a/imcui/third_party/RoMa/romatch/models/transformer/layers/block.py b/imcui/third_party/RoMa/romatch/models/transformer/layers/block.py
index 25488f57cc0ad3c692f86b62555f6668e2a66db1..a711a1f2ee00c8a6b5e79504f41f13145450af79 100644
--- a/imcui/third_party/RoMa/romatch/models/transformer/layers/block.py
+++ b/imcui/third_party/RoMa/romatch/models/transformer/layers/block.py
@@ -29,7 +29,7 @@ try:
XFORMERS_AVAILABLE = True
except ImportError:
- logger.warning("xFormers not available")
+ # logger.warning("xFormers not available")
XFORMERS_AVAILABLE = False
diff --git a/imcui/third_party/RoMa/romatch/utils/utils.py b/imcui/third_party/RoMa/romatch/utils/utils.py
index 1c522b16b7020779a0ea58b28973f2f609145838..56dbde0e35bc07eae246bd236784a9c77c5dce5b 100644
--- a/imcui/third_party/RoMa/romatch/utils/utils.py
+++ b/imcui/third_party/RoMa/romatch/utils/utils.py
@@ -651,4 +651,12 @@ def get_autocast_params(device=None, enabled=False, dtype=None):
enabled = False
# mps is not supported
autocast_device = "cpu"
- return autocast_device, enabled, out_dtype
\ No newline at end of file
+ return autocast_device, enabled, out_dtype
+
+def check_not_i16(im):
+ if im.mode == "I;16":
+ raise NotImplementedError("Can't handle 16 bit images")
+
+def check_rgb(im):
+ if im.mode != "RGB":
+ raise NotImplementedError("Can't handle non-RGB images")
diff --git a/imcui/third_party/RoMa/setup.py b/imcui/third_party/RoMa/setup.py
index 7ec18f3bbb71b85d943fdfeed3ed5c47033aebbc..83da45b5b65619a4e890b4497ccff9365ebc2c08 100644
--- a/imcui/third_party/RoMa/setup.py
+++ b/imcui/third_party/RoMa/setup.py
@@ -3,7 +3,7 @@ from setuptools import setup, find_packages
setup(
name="romatch",
packages=find_packages(include=("romatch*",)),
- version="0.0.1",
+ version="0.0.2",
author="Johan Edstedt",
install_requires=open("requirements.txt", "r").read().split("\n"),
)
diff --git a/imcui/third_party/dad/.gitignore b/imcui/third_party/dad/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..27254a6037775259692c6e36e61492626f2ccffb
--- /dev/null
+++ b/imcui/third_party/dad/.gitignore
@@ -0,0 +1,170 @@
+# Byte-compiled / optimized / DLL files
+__pycache__/
+*.py[cod]
+*$py.class
+
+# C extensions
+*.so
+
+# Distribution / packaging
+.Python
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+wheels/
+share/python-wheels/
+*.egg-info/
+.installed.cfg
+*.egg
+MANIFEST
+
+# PyInstaller
+# Usually these files are written by a python script from a template
+# before PyInstaller builds the exe, so as to inject date/other infos into it.
+*.manifest
+*.spec
+
+# Installer logs
+pip-log.txt
+pip-delete-this-directory.txt
+
+# Unit test / coverage reports
+htmlcov/
+.tox/
+.nox/
+.coverage
+.coverage.*
+.cache
+nosetests.xml
+coverage.xml
+*.cover
+*.py,cover
+.hypothesis/
+.pytest_cache/
+cover/
+
+# Translations
+*.mo
+*.pot
+
+# Django stuff:
+*.log
+local_settings.py
+db.sqlite3
+db.sqlite3-journal
+
+# Flask stuff:
+instance/
+.webassets-cache
+
+# Scrapy stuff:
+.scrapy
+
+# Sphinx documentation
+docs/_build/
+
+# PyBuilder
+.pybuilder/
+target/
+
+# Jupyter Notebook
+.ipynb_checkpoints
+
+# IPython
+profile_default/
+ipython_config.py
+
+# pyenv
+# For a library or package, you might want to ignore these files since the code is
+# intended to run in multiple environments; otherwise, check them in:
+# .python-version
+
+# pipenv
+# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
+# However, in case of collaboration, if having platform-specific dependencies or dependencies
+# having no cross-platform support, pipenv may install dependencies that don't work, or not
+# install all needed dependencies.
+#Pipfile.lock
+
+# poetry
+# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
+# This is especially recommended for binary packages to ensure reproducibility, and is more
+# commonly ignored for libraries.
+# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
+#poetry.lock
+
+# pdm
+# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
+#pdm.lock
+# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
+# in version control.
+# https://pdm.fming.dev/#use-with-ide
+.pdm.toml
+
+# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
+__pypackages__/
+
+# Celery stuff
+celerybeat-schedule
+celerybeat.pid
+
+# SageMath parsed files
+*.sage.py
+
+# Environments
+.env
+.venv
+env/
+venv/
+ENV/
+env.bak/
+venv.bak/
+
+# Spyder project settings
+.spyderproject
+.spyproject
+
+# Rope project settings
+.ropeproject
+
+# mkdocs documentation
+/site
+
+# mypy
+.mypy_cache/
+.dmypy.json
+dmypy.json
+
+# Pyre type checker
+.pyre/
+
+# pytype static type analyzer
+.pytype/
+
+# Cython debug symbols
+cython_debug/
+
+# PyCharm
+# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
+# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
+# and can be added to the global gitignore or merged into this file. For a more nuclear
+# option (not recommended) you can uncomment the following to ignore the entire idea folder.
+#.idea/
+
+.vscode*
+*.pth
+wandb
+*.out
+vis/
+workspace/
+
+.DS_Store
+*.tar
\ No newline at end of file
diff --git a/imcui/third_party/dad/.python-version b/imcui/third_party/dad/.python-version
new file mode 100644
index 0000000000000000000000000000000000000000..7c7a975f4c47c3eb326eb8898503f12c10b5606e
--- /dev/null
+++ b/imcui/third_party/dad/.python-version
@@ -0,0 +1 @@
+3.10
\ No newline at end of file
diff --git a/imcui/third_party/dad/LICENSE b/imcui/third_party/dad/LICENSE
new file mode 100644
index 0000000000000000000000000000000000000000..e5e8310db5b6aca14e0aa9d5cd89b763c3b84e23
--- /dev/null
+++ b/imcui/third_party/dad/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2025 Johan Edstedt
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/imcui/third_party/dad/README.md b/imcui/third_party/dad/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..78734e1900f6186155aaedf9a1f7a191734ddd98
--- /dev/null
+++ b/imcui/third_party/dad/README.md
@@ -0,0 +1,130 @@
+
+
DaD: Distilled Reinforcement Learning for Diverse Keypoint Detection
+
+ Johan Edstedt
+ ·
+ Georg Bökman
+ ·
+ Mårten Wadenbäck
+ ·
+ Michael Felsberg
+
+
+ Paper
+
+
+
+
+ DaD's a pretty good keypoint detector, probably the best.
+
+
+
+
+
+## Run
+```python
+import dad
+from PIL import Image
+img_path = "assets/0015_A.jpg"
+W, H = Image.open(img_path).size# your image shape,
+detector = dad.load_DaD()
+detections = detector.detect_from_path(
+ img_path,
+ num_keypoints = 512,
+ return_dense_probs=True)
+detections["keypoints"] # 1 x 512 x 2, normalized coordinates of keypoints
+detector.to_pixel_coords(detections["keypoints"], H, W)
+detections["keypoint_probs"] # 1 x 512, probs of sampled keypoints
+detections["dense_probs"] # 1 x H x W, probability map
+```
+
+## Visualize
+```python
+import dad
+from dad.utils import visualize_keypoints
+detector = dad.load_DaD()
+img_path = "assets/0015_A.jpg"
+vis_path = "vis/0015_A_dad.jpg"
+visualize_keypoints(img_path, vis_path, detector, num_keypoints = 512)
+```
+
+## Install
+Get uv
+```bash
+curl -LsSf https://astral.sh/uv/install.sh | sh
+```
+### In an existing env
+Assuming you already have some env active:
+```bash
+uv pip install dad@git+https://github.com/Parskatt/dad.git
+```
+### As a project
+For dev, etc:
+```bash
+git clone git@github.com:Parskatt/dad.git
+uv sync
+source .venv/bin/activate
+```
+
+## Evaluation
+For to evaluate, e.g., DaD on ScanNet1500 with 512 keypoints, run
+```bash
+python experiments/benchmark.py --detector DaD --num_keypoints 512 --benchmark ScanNet1500
+```
+Note: leaving out num_keypoints will run the benchmark for all numbers of keypoints, i.e., [512, 1024, 2048, 4096, 8192].
+### Third party detectors
+We provide wrappers for a somewhat large set of previous detectors,
+```bash
+python experiments/benchmark.py --help
+```
+
+## Training
+To train our final model from the emergent light and dark detector, run
+```bash
+python experiments/repro_paper_results/distill.py
+```
+The emergent models come from running
+```bash
+python experiments/repro_paper_results/rl.py
+```
+Note however that the types of detectors that come from this type of training is stochastic, and you may need to do several runs to get a detector that matches our results.
+
+## How I run experiments
+(Note: You don't have to do this, it's just how I do it.)
+At the start of a new day I typically run
+```bash
+python new_day.py
+```
+This creates a new folder in experiments, e.g., `experiments/w11/monday`.
+I then typically just copy the contents of a previous experiment, e.g.,
+```bash
+cp experiments/repro_paper_results/rl.py experiments/w11/monday/new-cool-hparams.py
+```
+Change whatever you want to change in `experiments/w11/monday/new-cool-hparams.py`.
+
+Then run it with
+```bash
+python experiments/w11/monday/new-cool-hparams.py
+```
+This will be tracked in wandb as `w11-monday-new-cool-hparams` in the `DaD` project.
+
+You might not want to track stuff, and perhaps display some debugstuff, then you can run instead as, which also won't log to wandb
+```bash
+DEBUG=1 python experiments/w11/monday/new-cool-hparams.py
+```
+## Evaluation Results
+TODO
+
+## Licenses
+DaD is MIT licensed.
+
+Third party detectors in [dad/detectors/third_party](dad/detectors/third_party) have their own licenses. If you use them, please refer to their respective licenses in [here](licenses) (NOTE: There may be more licenses you need to care about than the ones listed. Before using any third pary code, make sure you're following their respective license).
+
+
+
+
+## BibTeX
+
+```txt
+TODO
+```
diff --git a/imcui/third_party/dad/dad/__init__.py b/imcui/third_party/dad/dad/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e191325ec3fe6cb6bef249e884caf9c2fcc925e4
--- /dev/null
+++ b/imcui/third_party/dad/dad/__init__.py
@@ -0,0 +1,17 @@
+from .logging import logger as logger
+from .logging import configure_logger as configure_logger
+import os
+from .detectors import load_DaD as load_DaD
+from .detectors import dedode_detector_S as dedode_detector_S
+from .detectors import dedode_detector_B as dedode_detector_B
+from .detectors import dedode_detector_L as dedode_detector_L
+from .detectors import load_DaDDark as load_DaDDark
+from .detectors import load_DaDLight as load_DaDLight
+from .types import Detector as Detector
+from .types import Matcher as Matcher
+from .types import Benchmark as Benchmark
+
+configure_logger()
+DEBUG_MODE = bool(os.environ.get("DEBUG", False))
+RANK = 0
+GLOBAL_STEP = 0
diff --git a/imcui/third_party/dad/dad/augs.py b/imcui/third_party/dad/dad/augs.py
new file mode 100644
index 0000000000000000000000000000000000000000..22e01ed6b86d8c9a481af330da47c3ee9ca7183f
--- /dev/null
+++ b/imcui/third_party/dad/dad/augs.py
@@ -0,0 +1,214 @@
+import random
+import warnings
+import numpy as np
+import torch
+from PIL import Image
+from torchvision import transforms
+from torchvision.transforms.functional import InterpolationMode
+import cv2
+
+
+# From Patch2Pix https://github.com/GrumpyZhou/patch2pix
+def get_depth_tuple_transform_ops(resize=None, normalize=True, unscale=False):
+ ops = []
+ if resize:
+ ops.append(
+ TupleResize(resize, mode=InterpolationMode.BILINEAR, antialias=False)
+ )
+ return TupleCompose(ops)
+
+
+def get_tuple_transform_ops(resize=None, normalize=True, unscale=False, clahe=False):
+ ops = []
+ if resize:
+ ops.append(TupleResize(resize, antialias=True))
+ if clahe:
+ ops.append(TupleClahe())
+ if normalize:
+ ops.append(TupleToTensorScaled())
+ ops.append(
+ TupleNormalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
+ ) # Imagenet mean/std
+ else:
+ if unscale:
+ ops.append(TupleToTensorUnscaled())
+ else:
+ ops.append(TupleToTensorScaled())
+ return TupleCompose(ops)
+
+
+class Clahe:
+ def __init__(self, cliplimit=2, blocksize=8) -> None:
+ self.clahe = cv2.createCLAHE(cliplimit, (blocksize, blocksize))
+
+ def __call__(self, im):
+ im_hsv = cv2.cvtColor(np.array(im), cv2.COLOR_RGB2HSV)
+ im_v = self.clahe.apply(im_hsv[:, :, 2])
+ im_hsv[..., 2] = im_v
+ im_clahe = cv2.cvtColor(im_hsv, cv2.COLOR_HSV2RGB)
+ return Image.fromarray(im_clahe)
+
+
+class TupleClahe:
+ def __init__(self, cliplimit=8, blocksize=8) -> None:
+ self.clahe = Clahe(cliplimit, blocksize)
+
+ def __call__(self, ims):
+ return [self.clahe(im) for im in ims]
+
+
+class ToTensorScaled(object):
+ """Convert a RGB PIL Image to a CHW ordered Tensor, scale the range to [0, 1]"""
+
+ def __call__(self, im):
+ if not isinstance(im, torch.Tensor):
+ im = np.array(im, dtype=np.float32).transpose((2, 0, 1))
+ im /= 255.0
+ return torch.from_numpy(im)
+ else:
+ return im
+
+ def __repr__(self):
+ return "ToTensorScaled(./255)"
+
+
+class TupleToTensorScaled(object):
+ def __init__(self):
+ self.to_tensor = ToTensorScaled()
+
+ def __call__(self, im_tuple):
+ return [self.to_tensor(im) for im in im_tuple]
+
+ def __repr__(self):
+ return "TupleToTensorScaled(./255)"
+
+
+class ToTensorUnscaled(object):
+ """Convert a RGB PIL Image to a CHW ordered Tensor"""
+
+ def __call__(self, im):
+ return torch.from_numpy(np.array(im, dtype=np.float32).transpose((2, 0, 1)))
+
+ def __repr__(self):
+ return "ToTensorUnscaled()"
+
+
+class TupleToTensorUnscaled(object):
+ """Convert a RGB PIL Image to a CHW ordered Tensor"""
+
+ def __init__(self):
+ self.to_tensor = ToTensorUnscaled()
+
+ def __call__(self, im_tuple):
+ return [self.to_tensor(im) for im in im_tuple]
+
+ def __repr__(self):
+ return "TupleToTensorUnscaled()"
+
+
+class TupleResize(object):
+ def __init__(self, size, mode=InterpolationMode.BICUBIC, antialias=None):
+ self.size = size
+ self.resize = transforms.Resize(size, mode, antialias=antialias)
+
+ def __call__(self, im_tuple):
+ return [self.resize(im) for im in im_tuple]
+
+ def __repr__(self):
+ return "TupleResize(size={})".format(self.size)
+
+
+class Normalize:
+ def __call__(self, im):
+ mean = im.mean(dim=(1, 2), keepdims=True)
+ std = im.std(dim=(1, 2), keepdims=True)
+ return (im - mean) / std
+
+
+class TupleNormalize(object):
+ def __init__(self, mean, std):
+ self.mean = mean
+ self.std = std
+ self.normalize = transforms.Normalize(mean=mean, std=std)
+
+ def __call__(self, im_tuple):
+ c, h, w = im_tuple[0].shape
+ if c > 3:
+ warnings.warn(f"Number of channels {c=} > 3, assuming first 3 are rgb")
+ return [self.normalize(im[:3]) for im in im_tuple]
+
+ def __repr__(self):
+ return "TupleNormalize(mean={}, std={})".format(self.mean, self.std)
+
+
+class TupleCompose(object):
+ def __init__(self, transforms):
+ self.transforms = transforms
+
+ def __call__(self, im_tuple):
+ for t in self.transforms:
+ im_tuple = t(im_tuple)
+ return im_tuple
+
+ def __repr__(self):
+ format_string = self.__class__.__name__ + "("
+ for t in self.transforms:
+ format_string += "\n"
+ format_string += " {0}".format(t)
+ format_string += "\n)"
+ return format_string
+
+
+def pad_kps(kps: torch.Tensor, pad_num_kps: int, value: int = -1):
+ assert len(kps.shape) == 2
+ N = len(kps)
+ padded_kps = value * torch.ones((pad_num_kps, 2)).to(kps)
+ padded_kps[:N] = kps
+ return padded_kps
+
+
+def crop(img: Image.Image, x: int, y: int, crop_size: int):
+ width, height = img.size
+ if width < crop_size or height < crop_size:
+ raise ValueError(f"Image dimensions must be at least {crop_size}x{crop_size}")
+ cropped_img = img.crop((x, y, x + crop_size, y + crop_size))
+ return cropped_img
+
+
+def random_crop(img: Image.Image, crop_size: int):
+ width, height = img.size
+
+ if width < crop_size or height < crop_size:
+ raise ValueError(f"Image dimensions must be at least {crop_size}x{crop_size}")
+
+ max_x = width - crop_size
+ max_y = height - crop_size
+
+ x = random.randint(0, max_x)
+ y = random.randint(0, max_y)
+
+ cropped_img = img.crop((x, y, x + crop_size, y + crop_size))
+ return cropped_img, (x, y)
+
+
+def luminance_negation(pil_img):
+ # Convert PIL RGB to numpy array
+ rgb_array = np.array(pil_img)
+
+ # Convert RGB to BGR (OpenCV format)
+ bgr = cv2.cvtColor(rgb_array, cv2.COLOR_RGB2BGR)
+
+ # Convert BGR to LAB
+ lab = cv2.cvtColor(bgr, cv2.COLOR_BGR2LAB)
+
+ # Negate L channel
+ lab[:, :, 0] = 255 - lab[:, :, 0]
+
+ # Convert back to BGR
+ bgr_result = cv2.cvtColor(lab, cv2.COLOR_LAB2BGR)
+
+ # Convert BGR back to RGB
+ rgb_result = cv2.cvtColor(bgr_result, cv2.COLOR_BGR2RGB)
+
+ # Convert numpy array back to PIL Image
+ return Image.fromarray(rgb_result)
diff --git a/imcui/third_party/dad/dad/benchmarks/__init__.py b/imcui/third_party/dad/dad/benchmarks/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..094805ec88c610c6994dc0abd88907b172bb22d4
--- /dev/null
+++ b/imcui/third_party/dad/dad/benchmarks/__init__.py
@@ -0,0 +1,21 @@
+# from .benchmark import Benchmark as Benchmark
+from .num_inliers import NumInliersBenchmark as NumInliersBenchmark
+from .megadepth import Mega1500 as Mega1500
+from .megadepth import Mega1500_F as Mega1500_F
+from .megadepth import MegaIMCPT as MegaIMCPT
+from .megadepth import MegaIMCPT_F as MegaIMCPT_F
+from .scannet import ScanNet1500 as ScanNet1500
+from .scannet import ScanNet1500_F as ScanNet1500_F
+from .hpatches import HPatchesViewpoint as HPatchesViewpoint
+from .hpatches import HPatchesIllum as HPatchesIllum
+
+all_benchmarks = [
+ Mega1500.__name__,
+ Mega1500_F.__name__,
+ MegaIMCPT.__name__,
+ MegaIMCPT_F.__name__,
+ ScanNet1500.__name__,
+ ScanNet1500_F.__name__,
+ HPatchesViewpoint.__name__,
+ HPatchesIllum.__name__,
+]
diff --git a/imcui/third_party/dad/dad/benchmarks/hpatches.py b/imcui/third_party/dad/dad/benchmarks/hpatches.py
new file mode 100644
index 0000000000000000000000000000000000000000..29b33d8a6b88f414bc5bf230b22a806187127095
--- /dev/null
+++ b/imcui/third_party/dad/dad/benchmarks/hpatches.py
@@ -0,0 +1,117 @@
+import os
+from typing import Optional
+
+import numpy as np
+import poselib
+from PIL import Image
+from tqdm import tqdm
+
+from dad.types import Detector, Matcher, Benchmark
+
+
+class HPatchesBenchmark(Benchmark):
+ def __init__(
+ self,
+ data_root="data/hpatches",
+ sample_every=1,
+ num_ransac_runs=5,
+ num_keypoints: Optional[list[int]] = None,
+ ) -> None:
+ super().__init__(
+ data_root=data_root,
+ num_keypoints=num_keypoints,
+ sample_every=sample_every,
+ num_ransac_runs=num_ransac_runs,
+ thresholds=[3, 5, 10],
+ )
+ seqs_dir = "hpatches-sequences-release"
+ self.seqs_path = os.path.join(self.data_root, seqs_dir)
+ self.seq_names = sorted(os.listdir(self.seqs_path))
+ self.topleft = 0.0
+ self._post_init()
+ self.skip_seqs: str
+ self.scene_names: list[str]
+
+ def _post_init(self):
+ # set self.skip_seqs and self.scene_names here
+ raise NotImplementedError()
+
+ def benchmark(self, detector: Detector, matcher: Matcher):
+ homog_dists = []
+ for seq_idx, seq_name in enumerate(tqdm(self.seq_names[:: self.sample_every])):
+ if self.skip_seqs in seq_name:
+ # skip illumination seqs
+ continue
+ im_A_path = os.path.join(self.seqs_path, seq_name, "1.ppm")
+ im_A = Image.open(im_A_path)
+ w1, h1 = im_A.size
+ for im_idx in list(range(2, 7)):
+ im_B_path = os.path.join(self.seqs_path, seq_name, f"{im_idx}.ppm")
+ H = np.loadtxt(
+ os.path.join(self.seqs_path, seq_name, "H_1_" + str(im_idx))
+ )
+ warp, certainty = matcher.match(im_A_path, im_B_path)
+ for num_kps in self.num_keypoints:
+ keypoints_A = detector.detect_from_path(
+ im_A_path,
+ num_keypoints=num_kps,
+ )["keypoints"][0]
+ keypoints_B = detector.detect_from_path(
+ im_B_path,
+ num_keypoints=num_kps,
+ )["keypoints"][0]
+ matches = matcher.match_keypoints(
+ keypoints_A,
+ keypoints_B,
+ warp,
+ certainty,
+ return_tuple=False,
+ )
+ im_A = Image.open(im_A_path)
+ w1, h1 = im_A.size
+ im_B = Image.open(im_B_path)
+ w2, h2 = im_B.size
+ kpts1, kpts2 = matcher.to_pixel_coordinates(matches, h1, w1, h2, w2)
+ offset = detector.topleft - self.topleft
+ kpts1, kpts2 = kpts1 - offset, kpts2 - offset
+ for _ in range(self.num_ransac_runs):
+ shuffling = np.random.permutation(np.arange(len(kpts1)))
+ kpts1 = kpts1[shuffling]
+ kpts2 = kpts2[shuffling]
+ threshold = 2.0
+ H_pred, res = poselib.estimate_homography(
+ kpts1.cpu().numpy(),
+ kpts2.cpu().numpy(),
+ ransac_opt={
+ "max_reproj_error": threshold,
+ },
+ )
+ corners = np.array(
+ [
+ [0, 0, 1],
+ [0, h1 - 1, 1],
+ [w1 - 1, 0, 1],
+ [w1 - 1, h1 - 1, 1],
+ ]
+ )
+ real_warped_corners = np.dot(corners, np.transpose(H))
+ real_warped_corners = (
+ real_warped_corners[:, :2] / real_warped_corners[:, 2:]
+ )
+ warped_corners = np.dot(corners, np.transpose(H_pred))
+ warped_corners = warped_corners[:, :2] / warped_corners[:, 2:]
+ mean_dist = np.mean(
+ np.linalg.norm(real_warped_corners - warped_corners, axis=1)
+ ) / (min(w2, h2) / 480.0)
+ homog_dists.append(mean_dist)
+ return self.compute_auc(np.array(homog_dists))
+
+
+class HPatchesViewpoint(HPatchesBenchmark):
+ def _post_init(self):
+ self.skip_seqs = "i_"
+
+
+class HPatchesIllum(HPatchesBenchmark):
+ def _post_init(self):
+ self.skip_seqs = "v_"
diff --git a/imcui/third_party/dad/dad/benchmarks/megadepth.py b/imcui/third_party/dad/dad/benchmarks/megadepth.py
new file mode 100644
index 0000000000000000000000000000000000000000..c6c3e6aa1b81e56268e9d41e63183796594c75cd
--- /dev/null
+++ b/imcui/third_party/dad/dad/benchmarks/megadepth.py
@@ -0,0 +1,219 @@
+from typing import Literal, Optional
+
+import numpy as np
+from PIL import Image
+from tqdm import tqdm
+
+from dad.types import Detector, Matcher, Benchmark
+from dad.utils import (
+ compute_pose_error,
+ compute_relative_pose,
+ estimate_pose_essential,
+ estimate_pose_fundamental,
+)
+
+
+class MegaDepthPoseEstimationBenchmark(Benchmark):
+ def __init__(
+ self,
+ data_root="data/megadepth",
+ sample_every=1,
+ num_ransac_runs=5,
+ num_keypoints: Optional[list[int]] = None,
+ ) -> None:
+ super().__init__(
+ data_root=data_root,
+ num_keypoints=num_keypoints,
+ sample_every=sample_every,
+ num_ransac_runs=num_ransac_runs,
+ thresholds=[5, 10, 20],
+ )
+ self.sample_every = sample_every
+ self.topleft = 0.5
+ self._post_init()
+ self.model: Literal["fundamental", "essential"]
+ self.scene_names: list[str]
+ self.benchmark_name: str
+
+ def _post_init(self):
+ raise NotImplementedError(
+ "Add scene names and benchmark name in derived class _post_init"
+ )
+
+ def benchmark(
+ self,
+ detector: Detector,
+ matcher: Matcher,
+ ):
+ self.scenes = [
+ np.load(f"{self.data_root}/{scene}", allow_pickle=True)
+ for scene in self.scene_names
+ ]
+
+ data_root = self.data_root
+ tot_e_pose = []
+ n_matches = []
+ for scene_ind in range(len(self.scenes)):
+ scene = self.scenes[scene_ind]
+ pairs = scene["pair_infos"]
+ intrinsics = scene["intrinsics"]
+ poses = scene["poses"]
+ im_paths = scene["image_paths"]
+ pair_inds = range(len(pairs))
+ for pairind in (
+ pbar := tqdm(
+ pair_inds[:: self.sample_every],
+ desc="Current AUC: ?",
+ mininterval=10,
+ )
+ ):
+ idx1, idx2 = pairs[pairind][0]
+ K1 = intrinsics[idx1].copy()
+ T1 = poses[idx1].copy()
+ R1, t1 = T1[:3, :3], T1[:3, 3]
+ K2 = intrinsics[idx2].copy()
+ T2 = poses[idx2].copy()
+ R2, t2 = T2[:3, :3], T2[:3, 3]
+ R, t = compute_relative_pose(R1, t1, R2, t2)
+ im_A_path = f"{data_root}/{im_paths[idx1]}"
+ im_B_path = f"{data_root}/{im_paths[idx2]}"
+
+ warp, certainty = matcher.match(im_A_path, im_B_path)
+ for num_kps in self.num_keypoints:
+ keypoints_A = detector.detect_from_path(
+ im_A_path,
+ num_keypoints=num_kps,
+ )["keypoints"][0]
+ keypoints_B = detector.detect_from_path(
+ im_B_path,
+ num_keypoints=num_kps,
+ )["keypoints"][0]
+ matches = matcher.match_keypoints(
+ keypoints_A,
+ keypoints_B,
+ warp,
+ certainty,
+ return_tuple=False,
+ )
+ n_matches.append(matches.shape[0])
+ im_A = Image.open(im_A_path)
+ w1, h1 = im_A.size
+ im_B = Image.open(im_B_path)
+ w2, h2 = im_B.size
+ kpts1, kpts2 = matcher.to_pixel_coordinates(matches, h1, w1, h2, w2)
+ offset = detector.topleft - self.topleft
+ kpts1, kpts2 = kpts1 - offset, kpts2 - offset
+
+ for _ in range(self.num_ransac_runs):
+ shuffling = np.random.permutation(np.arange(len(kpts1)))
+ kpts1 = kpts1[shuffling]
+ kpts2 = kpts2[shuffling]
+ threshold = 2.0
+ if self.model == "essential":
+ R_est, t_est = estimate_pose_essential(
+ kpts1.cpu().numpy(),
+ kpts2.cpu().numpy(),
+ w1,
+ h1,
+ K1,
+ w2,
+ h2,
+ K2,
+ threshold,
+ )
+ elif self.model == "fundamental":
+ R_est, t_est = estimate_pose_fundamental(
+ kpts1.cpu().numpy(),
+ kpts2.cpu().numpy(),
+ w1,
+ h1,
+ K1,
+ w2,
+ h2,
+ K2,
+ threshold,
+ )
+ T1_to_2_est = np.concatenate((R_est, t_est[:, None]), axis=-1)
+ e_t, e_R = compute_pose_error(T1_to_2_est, R, t)
+ e_pose = max(e_t, e_R)
+ tot_e_pose.append(e_pose)
+ pbar.set_description(
+ f"Current AUCS: {self.compute_auc(np.array(tot_e_pose))}"
+ )
+ n_matches = np.array(n_matches)
+ print(n_matches.mean(), np.median(n_matches), np.std(n_matches))
+ return self.compute_auc(np.array(tot_e_pose))
+
+
+class Mega1500(MegaDepthPoseEstimationBenchmark):
+ def _post_init(self):
+ self.scene_names = [
+ "0015_0.1_0.3.npz",
+ "0015_0.3_0.5.npz",
+ "0022_0.1_0.3.npz",
+ "0022_0.3_0.5.npz",
+ "0022_0.5_0.7.npz",
+ ]
+ self.benchmark_name = "Mega1500"
+ self.model = "essential"
+
+
+class Mega1500_F(MegaDepthPoseEstimationBenchmark):
+ def _post_init(self):
+ self.scene_names = [
+ "0015_0.1_0.3.npz",
+ "0015_0.3_0.5.npz",
+ "0022_0.1_0.3.npz",
+ "0022_0.3_0.5.npz",
+ "0022_0.5_0.7.npz",
+ ]
+ # self.benchmark_name = "Mega1500_F"
+ self.model = "fundamental"
+
+
+class MegaIMCPT(MegaDepthPoseEstimationBenchmark):
+ def _post_init(self):
+ self.scene_names = [
+ "mega_8_scenes_0008_0.1_0.3.npz",
+ "mega_8_scenes_0008_0.3_0.5.npz",
+ "mega_8_scenes_0019_0.1_0.3.npz",
+ "mega_8_scenes_0019_0.3_0.5.npz",
+ "mega_8_scenes_0021_0.1_0.3.npz",
+ "mega_8_scenes_0021_0.3_0.5.npz",
+ "mega_8_scenes_0024_0.1_0.3.npz",
+ "mega_8_scenes_0024_0.3_0.5.npz",
+ "mega_8_scenes_0025_0.1_0.3.npz",
+ "mega_8_scenes_0025_0.3_0.5.npz",
+ "mega_8_scenes_0032_0.1_0.3.npz",
+ "mega_8_scenes_0032_0.3_0.5.npz",
+ "mega_8_scenes_0063_0.1_0.3.npz",
+ "mega_8_scenes_0063_0.3_0.5.npz",
+ "mega_8_scenes_1589_0.1_0.3.npz",
+ "mega_8_scenes_1589_0.3_0.5.npz",
+ ]
+ # self.benchmark_name = "MegaIMCPT"
+ self.model = "essential"
+
+
+class MegaIMCPT_F(MegaDepthPoseEstimationBenchmark):
+ def _post_init(self):
+ self.scene_names = [
+ "mega_8_scenes_0008_0.1_0.3.npz",
+ "mega_8_scenes_0008_0.3_0.5.npz",
+ "mega_8_scenes_0019_0.1_0.3.npz",
+ "mega_8_scenes_0019_0.3_0.5.npz",
+ "mega_8_scenes_0021_0.1_0.3.npz",
+ "mega_8_scenes_0021_0.3_0.5.npz",
+ "mega_8_scenes_0024_0.1_0.3.npz",
+ "mega_8_scenes_0024_0.3_0.5.npz",
+ "mega_8_scenes_0025_0.1_0.3.npz",
+ "mega_8_scenes_0025_0.3_0.5.npz",
+ "mega_8_scenes_0032_0.1_0.3.npz",
+ "mega_8_scenes_0032_0.3_0.5.npz",
+ "mega_8_scenes_0063_0.1_0.3.npz",
+ "mega_8_scenes_0063_0.3_0.5.npz",
+ "mega_8_scenes_1589_0.1_0.3.npz",
+ "mega_8_scenes_1589_0.3_0.5.npz",
+ ]
+ # self.benchmark_name = "MegaIMCPT_F"
+ self.model = "fundamental"
diff --git a/imcui/third_party/dad/dad/benchmarks/num_inliers.py b/imcui/third_party/dad/dad/benchmarks/num_inliers.py
new file mode 100644
index 0000000000000000000000000000000000000000..ade9bac26d9752c0fe4087a727e01490b5159754
--- /dev/null
+++ b/imcui/third_party/dad/dad/benchmarks/num_inliers.py
@@ -0,0 +1,106 @@
+import torch
+import torch.nn.functional as F
+from tqdm import tqdm
+
+from dad.types import Detector
+from dad.utils import get_gt_warp, to_best_device
+
+
+class NumInliersBenchmark:
+ def __init__(
+ self,
+ dataset,
+ num_samples=1000,
+ batch_size=8,
+ num_keypoints=512,
+ **kwargs,
+ ) -> None:
+ sampler = torch.utils.data.WeightedRandomSampler(
+ torch.ones(len(dataset)), replacement=False, num_samples=num_samples
+ )
+ dataloader = torch.utils.data.DataLoader(
+ dataset, batch_size=batch_size, num_workers=batch_size, sampler=sampler
+ )
+ self.dataloader = dataloader
+ self.tracked_metrics = {}
+ self.batch_size = batch_size
+ self.N = len(dataloader)
+ self.num_keypoints = num_keypoints
+
+ def compute_batch_metrics(self, outputs, batch):
+ kpts_A, kpts_B = outputs["keypoints_A"], outputs["keypoints_B"]
+ B, K, H, W = batch["im_A"].shape
+ gt_warp_A_to_B, valid_mask_A_to_B = get_gt_warp(
+ batch["im_A_depth"],
+ batch["im_B_depth"],
+ batch["T_1to2"],
+ batch["K1"],
+ batch["K2"],
+ H=H,
+ W=W,
+ )
+ kpts_A_to_B = F.grid_sample(
+ gt_warp_A_to_B[..., 2:].float().permute(0, 3, 1, 2),
+ kpts_A[..., None, :],
+ align_corners=False,
+ mode="bilinear",
+ )[..., 0].mT
+ legit_A_to_B = F.grid_sample(
+ valid_mask_A_to_B.reshape(B, 1, H, W),
+ kpts_A[..., None, :],
+ align_corners=False,
+ mode="bilinear",
+ )[..., 0, :, 0]
+ dists = (
+ torch.cdist(kpts_A_to_B, kpts_B).min(dim=-1).values[legit_A_to_B > 0.0]
+ ).float()
+ if legit_A_to_B.sum() == 0:
+ return
+ percent_inliers_at_1 = (dists < 0.02).float().mean()
+ percent_inliers_at_05 = (dists < 0.01).float().mean()
+ percent_inliers_at_025 = (dists < 0.005).float().mean()
+ percent_inliers_at_01 = (dists < 0.002).float().mean()
+ percent_inliers_at_005 = (dists < 0.001).float().mean()
+
+ self.tracked_metrics["percent_inliers_at_1"] = (
+ self.tracked_metrics.get("percent_inliers_at_1", 0)
+ + 1 / self.N * percent_inliers_at_1
+ )
+ self.tracked_metrics["percent_inliers_at_05"] = (
+ self.tracked_metrics.get("percent_inliers_at_05", 0)
+ + 1 / self.N * percent_inliers_at_05
+ )
+ self.tracked_metrics["percent_inliers_at_025"] = (
+ self.tracked_metrics.get("percent_inliers_at_025", 0)
+ + 1 / self.N * percent_inliers_at_025
+ )
+ self.tracked_metrics["percent_inliers_at_01"] = (
+ self.tracked_metrics.get("percent_inliers_at_01", 0)
+ + 1 / self.N * percent_inliers_at_01
+ )
+ self.tracked_metrics["percent_inliers_at_005"] = (
+ self.tracked_metrics.get("percent_inliers_at_005", 0)
+ + 1 / self.N * percent_inliers_at_005
+ )
+
+ def benchmark(self, detector: Detector):
+ self.tracked_metrics = {}
+
+ print("Evaluating percent inliers...")
+ for idx, batch in enumerate(tqdm(self.dataloader, mininterval=10.0)):
+ batch = to_best_device(batch)
+ outputs = detector.detect(batch, num_keypoints=self.num_keypoints)
+ keypoints_A, keypoints_B = outputs["keypoints"].chunk(2)
+ if isinstance(outputs["keypoints"], (tuple, list)):
+ keypoints_A, keypoints_B = (
+ torch.stack(keypoints_A),
+ torch.stack(keypoints_B),
+ )
+ outputs = {"keypoints_A": keypoints_A, "keypoints_B": keypoints_B}
+ self.compute_batch_metrics(outputs, batch)
+ [
+ print(name, metric.item() * self.N / (idx + 1))
+ for name, metric in self.tracked_metrics.items()
+ if "percent" in name
+ ]
+ return self.tracked_metrics
diff --git a/imcui/third_party/dad/dad/benchmarks/scannet.py b/imcui/third_party/dad/dad/benchmarks/scannet.py
new file mode 100644
index 0000000000000000000000000000000000000000..7a992e774a7829a9d89a02964eb07590c805e408
--- /dev/null
+++ b/imcui/third_party/dad/dad/benchmarks/scannet.py
@@ -0,0 +1,163 @@
+import os.path as osp
+from typing import Literal, Optional
+
+import numpy as np
+import torch
+from PIL import Image
+from tqdm import tqdm
+
+from dad.types import Detector, Matcher, Benchmark
+from dad.utils import (
+ compute_pose_error,
+ estimate_pose_essential,
+ estimate_pose_fundamental,
+)
+
+
+class ScanNetBenchmark(Benchmark):
+ def __init__(
+ self,
+ sample_every: int = 1,
+ num_ransac_runs=5,
+ data_root: str = "data/scannet",
+ num_keypoints: Optional[list[int]] = None,
+ ) -> None:
+ super().__init__(
+ data_root=data_root,
+ num_keypoints=num_keypoints,
+ sample_every=sample_every,
+ num_ransac_runs=num_ransac_runs,
+ thresholds=[5, 10, 20],
+ )
+ self.sample_every = sample_every
+ self.topleft = 0.0
+ self._post_init()
+ self.model: Literal["fundamental", "essential"]
+ self.test_pairs: str
+ self.benchmark_name: str
+
+ def _post_init(self):
+ # set
+ raise NotImplementedError("")
+
+ @torch.no_grad()
+ def benchmark(self, matcher: Matcher, detector: Detector):
+ tmp = np.load(self.test_pairs)
+ pairs, rel_pose = tmp["name"], tmp["rel_pose"]
+ tot_e_pose = []
+ # pair_inds = np.random.choice(range(len(pairs)), size=len(pairs), replace=False)
+ for pairind in tqdm(
+ range(0, len(pairs), self.sample_every), smoothing=0.9, mininterval=10
+ ):
+ scene = pairs[pairind]
+ scene_name = f"scene0{scene[0]}_00"
+ im_A_path = osp.join(
+ self.data_root,
+ "scans_test",
+ scene_name,
+ "color",
+ f"{scene[2]}.jpg",
+ )
+ im_A = Image.open(im_A_path)
+ im_B_path = osp.join(
+ self.data_root,
+ "scans_test",
+ scene_name,
+ "color",
+ f"{scene[3]}.jpg",
+ )
+ im_B = Image.open(im_B_path)
+ T_gt = rel_pose[pairind].reshape(3, 4)
+ R, t = T_gt[:3, :3], T_gt[:3, 3]
+ K = np.stack(
+ [
+ np.array([float(i) for i in r.split()])
+ for r in open(
+ osp.join(
+ self.data_root,
+ "scans_test",
+ scene_name,
+ "intrinsic",
+ "intrinsic_color.txt",
+ ),
+ "r",
+ )
+ .read()
+ .split("\n")
+ if r
+ ]
+ )
+ w1, h1 = im_A.size
+ w2, h2 = im_B.size
+ K1 = K.copy()[:3, :3]
+ K2 = K.copy()[:3, :3]
+ warp, certainty = matcher.match(im_A_path, im_B_path)
+ for num_kps in self.num_keypoints:
+ keypoints_A = detector.detect_from_path(
+ im_A_path,
+ num_keypoints=num_kps,
+ )["keypoints"][0]
+ keypoints_B = detector.detect_from_path(
+ im_B_path,
+ num_keypoints=num_kps,
+ )["keypoints"][0]
+ matches = matcher.match_keypoints(
+ keypoints_A,
+ keypoints_B,
+ warp,
+ certainty,
+ return_tuple=False,
+ )
+ kpts1, kpts2 = matcher.to_pixel_coordinates(matches, h1, w1, h2, w2)
+
+ offset = detector.topleft - self.topleft
+ kpts1, kpts2 = kpts1 - offset, kpts2 - offset
+
+ for _ in range(self.num_ransac_runs):
+ shuffling = np.random.permutation(np.arange(len(kpts1)))
+ kpts1 = kpts1[shuffling]
+ kpts2 = kpts2[shuffling]
+ threshold = 2.0
+ if self.model == "essential":
+ R_est, t_est = estimate_pose_essential(
+ kpts1.cpu().numpy(),
+ kpts2.cpu().numpy(),
+ w1,
+ h1,
+ K1,
+ w2,
+ h2,
+ K2,
+ threshold,
+ )
+ elif self.model == "fundamental":
+ R_est, t_est = estimate_pose_fundamental(
+ kpts1.cpu().numpy(),
+ kpts2.cpu().numpy(),
+ w1,
+ h1,
+ K1,
+ w2,
+ h2,
+ K2,
+ threshold,
+ )
+ T1_to_2_est = np.concatenate((R_est, t_est[:, None]), axis=-1)
+ e_t, e_R = compute_pose_error(T1_to_2_est, R, t)
+ e_pose = max(e_t, e_R)
+ tot_e_pose.append(e_pose)
+ return self.compute_auc(np.array(tot_e_pose))
+
+
+class ScanNet1500(ScanNetBenchmark):
+ def _post_init(self):
+ self.test_pairs = osp.join(self.data_root, "test.npz")
+ self.benchmark_name = "ScanNet1500"
+ self.model = "essential"
+
+
+class ScanNet1500_F(ScanNetBenchmark):
+ def _post_init(self):
+ self.test_pairs = osp.join(self.data_root, "test.npz")
+ self.benchmark_name = "ScanNet1500_F"
+ self.model = "fundamental"
diff --git a/imcui/third_party/dad/dad/checkpoint.py b/imcui/third_party/dad/dad/checkpoint.py
new file mode 100644
index 0000000000000000000000000000000000000000..5ff059bab97b72ee4f037ffe74817429bc8ed5d3
--- /dev/null
+++ b/imcui/third_party/dad/dad/checkpoint.py
@@ -0,0 +1,61 @@
+import torch
+from torch.nn.parallel.data_parallel import DataParallel
+from torch.nn.parallel.distributed import DistributedDataParallel
+import gc
+from pathlib import Path
+import dad
+from dad.types import Detector
+
+class CheckPoint:
+ def __init__(self, dir):
+ self.dir = Path(dir)
+ self.dir.mkdir(parents=True, exist_ok=True)
+
+ def save(
+ self,
+ model: Detector,
+ optimizer,
+ lr_scheduler,
+ n,
+ ):
+ assert model is not None
+ if isinstance(model, (DataParallel, DistributedDataParallel)):
+ model = model.module
+ states = {
+ "model": model.state_dict(),
+ "n": n,
+ "optimizer": optimizer.state_dict(),
+ "lr_scheduler": lr_scheduler.state_dict(),
+ }
+ torch.save(states, self.dir / "model_latest.pth")
+ dad.logger.info(f"Saved states {list(states.keys())}, at step {n}")
+
+ def load(
+ self,
+ model: Detector,
+ optimizer,
+ lr_scheduler,
+ n,
+ ):
+ if not (self.dir / "model_latest.pth").exists():
+ return model, optimizer, lr_scheduler, n
+
+ states = torch.load(self.dir / "model_latest.pth")
+ if "model" in states:
+ model.load_state_dict(states["model"])
+ if "n" in states:
+ n = states["n"] if states["n"] else n
+ if "optimizer" in states:
+ try:
+ optimizer.load_state_dict(states["optimizer"])
+ except Exception as e:
+ dad.logger.warning(
+ f"Failed to load states for optimizer, with error {e}"
+ )
+ if "lr_scheduler" in states:
+ lr_scheduler.load_state_dict(states["lr_scheduler"])
+ dad.logger.info(f"Loaded states {list(states.keys())}, at step {n}")
+ del states
+ gc.collect()
+ torch.cuda.empty_cache()
+ return model, optimizer, lr_scheduler, n
diff --git a/imcui/third_party/dad/dad/datasets/__init__.py b/imcui/third_party/dad/dad/datasets/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/imcui/third_party/dad/dad/datasets/megadepth.py b/imcui/third_party/dad/dad/datasets/megadepth.py
new file mode 100644
index 0000000000000000000000000000000000000000..b32da09cfbc84e87e1312f2c23035e8febabe506
--- /dev/null
+++ b/imcui/third_party/dad/dad/datasets/megadepth.py
@@ -0,0 +1,312 @@
+import os
+from PIL import Image
+import h5py
+import math
+import numpy as np
+import torch
+import torchvision.transforms.functional as tvf
+from tqdm import tqdm
+
+import dad
+from dad.augs import (
+ get_tuple_transform_ops,
+ get_depth_tuple_transform_ops,
+)
+from torch.utils.data import ConcatDataset
+
+
+class MegadepthScene:
+ def __init__(
+ self,
+ data_root,
+ scene_info,
+ scene_name=None,
+ min_overlap=0.0,
+ max_overlap=1.0,
+ image_size=640,
+ normalize=True,
+ shake_t=32,
+ rot_360=False,
+ max_num_pairs=100_000,
+ ) -> None:
+ self.data_root = data_root
+ self.scene_name = (
+ os.path.splitext(scene_name)[0] + f"_{min_overlap}_{max_overlap}"
+ )
+ self.image_paths = scene_info["image_paths"]
+ self.depth_paths = scene_info["depth_paths"]
+ self.intrinsics = scene_info["intrinsics"]
+ self.poses = scene_info["poses"]
+ self.pairs = scene_info["pairs"]
+ self.overlaps = scene_info["overlaps"]
+ threshold = (self.overlaps > min_overlap) & (self.overlaps < max_overlap)
+ self.pairs = self.pairs[threshold]
+ self.overlaps = self.overlaps[threshold]
+ if len(self.pairs) > max_num_pairs:
+ pairinds = np.random.choice(
+ np.arange(0, len(self.pairs)), max_num_pairs, replace=False
+ )
+ self.pairs = self.pairs[pairinds]
+ self.overlaps = self.overlaps[pairinds]
+ self.im_transform_ops = get_tuple_transform_ops(
+ resize=(image_size, image_size),
+ normalize=normalize,
+ )
+ self.depth_transform_ops = get_depth_tuple_transform_ops(
+ resize=(image_size, image_size), normalize=False
+ )
+ self.image_size = image_size
+ self.shake_t = shake_t
+ self.rot_360 = rot_360
+
+ def load_im(self, im_B, crop=None):
+ im = Image.open(im_B)
+ return im
+
+ def rot_360_deg(self, im, depth, K, angle):
+ C, H, W = im.shape
+ im = tvf.rotate(im, angle, expand=True)
+ depth = tvf.rotate(depth, angle, expand=True)
+ radians = angle * math.pi / 180
+ rot_mat = torch.tensor(
+ [
+ [math.cos(radians), math.sin(radians), 0],
+ [-math.sin(radians), math.cos(radians), 0],
+ [0, 0, 1.0],
+ ]
+ ).to(K.device)
+ t_mat = torch.tensor([[1, 0, W / 2], [0, 1, H / 2], [0, 0, 1.0]]).to(K.device)
+ neg_t_mat = torch.tensor([[1, 0, -W / 2], [0, 1, -H / 2], [0, 0, 1.0]]).to(
+ K.device
+ )
+ transform = t_mat @ rot_mat @ neg_t_mat
+ K = transform @ K
+ return im, depth, K, transform
+
+ def load_depth(self, depth_ref, crop=None):
+ depth = np.array(h5py.File(depth_ref, "r")["depth"])
+ return torch.from_numpy(depth)
+
+ def __len__(self):
+ return len(self.pairs)
+
+ def scale_intrinsic(self, K, wi, hi):
+ sx, sy = self.image_size / wi, self.image_size / hi
+ sK = torch.tensor([[sx, 0, 0], [0, sy, 0], [0, 0, 1]])
+ return sK @ K
+
+ def rand_shake(self, *things):
+ t = np.random.choice(range(-self.shake_t, self.shake_t + 1), size=(2))
+ return [
+ tvf.affine(thing, angle=0.0, translate=list(t), scale=1.0, shear=[0.0, 0.0])
+ for thing in things
+ ], t
+
+ def __getitem__(self, pair_idx):
+ try:
+ # read intrinsics of original size
+ idx1, idx2 = self.pairs[pair_idx]
+ K1 = torch.tensor(self.intrinsics[idx1].copy(), dtype=torch.float).reshape(
+ 3, 3
+ )
+ K2 = torch.tensor(self.intrinsics[idx2].copy(), dtype=torch.float).reshape(
+ 3, 3
+ )
+
+ # read and compute relative poses
+ T1 = self.poses[idx1]
+ T2 = self.poses[idx2]
+ T_1to2 = torch.tensor(np.matmul(T2, np.linalg.inv(T1)), dtype=torch.float)[
+ :4, :4
+ ] # (4, 4)
+
+ # Load positive pair data
+ im_A, im_B = self.image_paths[idx1], self.image_paths[idx2]
+ depth1, depth2 = self.depth_paths[idx1], self.depth_paths[idx2]
+ im_A_ref = os.path.join(self.data_root, im_A)
+ im_B_ref = os.path.join(self.data_root, im_B)
+ depth_A_ref = os.path.join(self.data_root, depth1)
+ depth_B_ref = os.path.join(self.data_root, depth2)
+ im_A: Image.Image = self.load_im(im_A_ref)
+ im_B: Image.Image = self.load_im(im_B_ref)
+ depth_A = self.load_depth(depth_A_ref)
+ depth_B = self.load_depth(depth_B_ref)
+
+ # Recompute camera intrinsic matrix due to the resize
+ W_A, H_A = im_A.width, im_A.height
+ W_B, H_B = im_B.width, im_B.height
+
+ K1 = self.scale_intrinsic(K1, W_A, H_A)
+ K2 = self.scale_intrinsic(K2, W_B, H_B)
+
+ # Process images
+ im_A, im_B = self.im_transform_ops((im_A, im_B))
+ depth_A, depth_B = self.depth_transform_ops(
+ (depth_A[None, None], depth_B[None, None])
+ )
+ [im_A, depth_A], t_A = self.rand_shake(im_A, depth_A)
+ [im_B, depth_B], t_B = self.rand_shake(im_B, depth_B)
+
+ K1[:2, 2] += t_A
+ K2[:2, 2] += t_B
+
+ if self.rot_360:
+ angle_A = np.random.choice([-90, 0, 90, 180])
+ angle_B = np.random.choice([-90, 0, 90, 180])
+ angle_A, angle_B = int(angle_A), int(angle_B)
+ im_A, depth_A, K1, _ = self.rot_360_deg(
+ im_A, depth_A, K1, angle=angle_A
+ )
+ im_B, depth_B, K2, _ = self.rot_360_deg(
+ im_B, depth_B, K2, angle=angle_B
+ )
+ else:
+ angle_A = 0
+ angle_B = 0
+ data_dict = {
+ "im_A": im_A,
+ "im_A_identifier": self.image_paths[idx1]
+ .split("/")[-1]
+ .split(".jpg")[0],
+ "im_B": im_B,
+ "im_B_identifier": self.image_paths[idx2]
+ .split("/")[-1]
+ .split(".jpg")[0],
+ "im_A_depth": depth_A[0, 0],
+ "im_B_depth": depth_B[0, 0],
+ "pose_A": T1,
+ "pose_B": T2,
+ "K1": K1,
+ "K2": K2,
+ "T_1to2": T_1to2,
+ "im_A_path": im_A_ref,
+ "im_B_path": im_B_ref,
+ "angle_A": angle_A,
+ "angle_B": angle_B,
+ }
+ except Exception as e:
+ dad.logger.warning(e)
+ dad.logger.warning(f"Failed to load image pair {self.pairs[pair_idx]}")
+ dad.logger.warning("Loading a random pair in scene instead")
+ rand_ind = np.random.choice(range(len(self)))
+ return self[rand_ind]
+ return data_dict
+
+
+class MegadepthBuilder:
+ def __init__(self, data_root, loftr_ignore=True, imc21_ignore=True) -> None:
+ self.data_root = data_root
+ self.scene_info_root = os.path.join(data_root, "prep_scene_info")
+ self.all_scenes = os.listdir(self.scene_info_root)
+ self.test_scenes = ["0017.npy", "0004.npy", "0048.npy", "0013.npy"]
+ # LoFTR did the D2-net preprocessing differently than we did and got more ignore scenes, can optionially ignore those
+ self.loftr_ignore_scenes = set(
+ [
+ "0121.npy",
+ "0133.npy",
+ "0168.npy",
+ "0178.npy",
+ "0229.npy",
+ "0349.npy",
+ "0412.npy",
+ "0430.npy",
+ "0443.npy",
+ "1001.npy",
+ "5014.npy",
+ "5015.npy",
+ "5016.npy",
+ ]
+ )
+ self.imc21_scenes = set(
+ [
+ "0008.npy",
+ "0019.npy",
+ "0021.npy",
+ "0024.npy",
+ "0025.npy",
+ "0032.npy",
+ "0063.npy",
+ "1589.npy",
+ ]
+ )
+ self.test_scenes_loftr = ["0015.npy", "0022.npy"]
+ self.loftr_ignore = loftr_ignore
+ self.imc21_ignore = imc21_ignore
+
+ def build_scenes(self, split, **kwargs):
+ if split == "train":
+ scene_names = set(self.all_scenes) - set(self.test_scenes)
+ elif split == "train_loftr":
+ scene_names = set(self.all_scenes) - set(self.test_scenes_loftr)
+ elif split == "test":
+ scene_names = self.test_scenes
+ elif split == "test_loftr":
+ scene_names = self.test_scenes_loftr
+ elif split == "all_scenes":
+ scene_names = self.all_scenes
+ elif split == "custom":
+ scene_names = scene_names
+ else:
+ raise ValueError(f"Split {split} not available")
+ scenes = []
+ for scene_name in tqdm(scene_names):
+ if self.loftr_ignore and scene_name in self.loftr_ignore_scenes:
+ continue
+ if self.imc21_ignore and scene_name in self.imc21_scenes:
+ continue
+ if ".npy" not in scene_name:
+ continue
+ scene_info = np.load(
+ os.path.join(self.scene_info_root, scene_name), allow_pickle=True
+ ).item()
+
+ scenes.append(
+ MegadepthScene(
+ self.data_root,
+ scene_info,
+ scene_name=scene_name,
+ **kwargs,
+ )
+ )
+ return scenes
+
+ def weight_scenes(self, concat_dataset, alpha=0.5):
+ ns = []
+ for d in concat_dataset.datasets:
+ ns.append(len(d))
+ ws = torch.cat([torch.ones(n) / n**alpha for n in ns])
+ return ws
+
+ def dedode_train_split(self, **kwargs):
+ megadepth_train1 = self.build_scenes(
+ split="train_loftr", min_overlap=0.01, **kwargs
+ )
+ megadepth_train2 = self.build_scenes(
+ split="train_loftr", min_overlap=0.35, **kwargs
+ )
+
+ megadepth_train = ConcatDataset(megadepth_train1 + megadepth_train2)
+ return megadepth_train
+
+ def hard_train_split(self, **kwargs):
+ megadepth_train = self.build_scenes(
+ split="train_loftr", min_overlap=0.01, **kwargs
+ )
+ megadepth_train = ConcatDataset(megadepth_train)
+ return megadepth_train
+
+ def easy_train_split(self, **kwargs):
+ megadepth_train = self.build_scenes(
+ split="train_loftr", min_overlap=0.35, **kwargs
+ )
+ megadepth_train = ConcatDataset(megadepth_train)
+ return megadepth_train
+
+ def dedode_test_split(self, **kwargs):
+ megadepth_test = self.build_scenes(
+ split="test_loftr",
+ min_overlap=0.01,
+ **kwargs,
+ )
+ megadepth_test = ConcatDataset(megadepth_test)
+ return megadepth_test
diff --git a/imcui/third_party/dad/dad/detectors/__init__.py b/imcui/third_party/dad/dad/detectors/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..b8e6ed55c573d08ae79f5543c7c6fd6fda1b95dd
--- /dev/null
+++ b/imcui/third_party/dad/dad/detectors/__init__.py
@@ -0,0 +1,50 @@
+from .dedode_detector import load_DaD as load_DaD
+from .dedode_detector import load_DaDDark as load_DaDDark
+from .dedode_detector import load_DaDLight as load_DaDLight
+from .dedode_detector import dedode_detector_S as dedode_detector_S
+from .dedode_detector import dedode_detector_B as dedode_detector_B
+from .dedode_detector import dedode_detector_L as dedode_detector_L
+from .dedode_detector import load_dedode_v2 as load_dedode_v2
+
+
+lg_detectors = ["ALIKED", "ALIKEDROT", "SIFT", "DISK", "SuperPoint", "ReinforcedFP"]
+other_detectors = ["HesAff", "HarrisAff", "REKD"]
+dedode_detectors = [
+ "DeDoDe-v2",
+ "DaD",
+ "DaDLight",
+ "DaDDark",
+]
+all_detectors = lg_detectors + dedode_detectors + other_detectors
+
+
+def load_detector_by_name(detector_name, *, resize=1024, weights_path=None):
+ if detector_name == "DaD":
+ detector = load_DaD(resize=resize, weights_path=weights_path)
+ elif detector_name == "DaDLight":
+ detector = load_DaDLight(resize=resize, weights_path=weights_path)
+ elif detector_name == "DaDDark":
+ detector = load_DaDDark(resize=resize, weights_path=weights_path)
+ elif detector_name == "DeDoDe-v2":
+ detector = load_dedode_v2()
+ elif detector_name in lg_detectors:
+ from .third_party import lightglue, LightGlueDetector
+
+ detector = LightGlueDetector(
+ getattr(lightglue, detector_name), detection_threshold=0, resize=resize
+ )
+ elif detector_name == "HesAff":
+ from .third_party import HesAff
+
+ detector = HesAff()
+ elif detector_name == "HarrisAff":
+ from .third_party import HarrisAff
+
+ detector = HarrisAff()
+ elif detector_name == "REKD":
+ from .third_party import load_REKD
+
+ detector = load_REKD(resize=resize)
+ else:
+ raise ValueError(f"Couldn't find detector with detector name {detector_name}")
+ return detector
diff --git a/imcui/third_party/dad/dad/detectors/dedode_detector.py b/imcui/third_party/dad/dad/detectors/dedode_detector.py
new file mode 100644
index 0000000000000000000000000000000000000000..4edccf44abd5c8a467ee55ba6df8743ce2e0545e
--- /dev/null
+++ b/imcui/third_party/dad/dad/detectors/dedode_detector.py
@@ -0,0 +1,559 @@
+import numpy as np
+
+import torch
+import torch.nn as nn
+import torch.nn.functional as F
+import torchvision.models as tvm
+import torchvision.transforms as transforms
+from PIL import Image
+from dad.utils import get_best_device, sample_keypoints, check_not_i16
+
+from dad.types import Detector
+
+
+class DeDoDeDetector(Detector):
+ def __init__(
+ self,
+ *args,
+ encoder: nn.Module,
+ decoder: nn.Module,
+ resize: int,
+ nms_size: int,
+ subpixel: bool,
+ subpixel_temp: float,
+ keep_aspect_ratio: bool,
+ remove_borders: bool,
+ increase_coverage: bool,
+ coverage_pow: float,
+ coverage_size: int,
+ **kwargs,
+ ) -> None:
+ super().__init__(*args, **kwargs)
+ self.normalizer = transforms.Normalize(
+ mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]
+ )
+ self.encoder = encoder
+ self.decoder = decoder
+ self.remove_borders = remove_borders
+ self.resize = resize
+ self.increase_coverage = increase_coverage
+ self.coverage_pow = coverage_pow
+ self.coverage_size = coverage_size
+ self.nms_size = nms_size
+ self.keep_aspect_ratio = keep_aspect_ratio
+ self.subpixel = subpixel
+ self.subpixel_temp = subpixel_temp
+
+ @property
+ def topleft(self):
+ return 0.5
+
+ def forward_impl(
+ self,
+ images,
+ ):
+ features, sizes = self.encoder(images)
+ logits = 0
+ context = None
+ scales = ["8", "4", "2", "1"]
+ for idx, (feature_map, scale) in enumerate(zip(reversed(features), scales)):
+ delta_logits, context = self.decoder(
+ feature_map, context=context, scale=scale
+ )
+ logits = (
+ logits + delta_logits.float()
+ ) # ensure float (need bf16 doesnt have f.interpolate)
+ if idx < len(scales) - 1:
+ size = sizes[-(idx + 2)]
+ logits = F.interpolate(
+ logits, size=size, mode="bicubic", align_corners=False
+ )
+ context = F.interpolate(
+ context.float(), size=size, mode="bilinear", align_corners=False
+ )
+ return logits.float()
+
+ def forward(self, batch) -> dict[str, torch.Tensor]:
+ # wraps internal forward impl to handle
+ # different types of batches etc.
+ if "im_A" in batch:
+ images = torch.cat((batch["im_A"], batch["im_B"]))
+ else:
+ images = batch["image"]
+ scoremap = self.forward_impl(images)
+ return {"scoremap": scoremap}
+
+ @torch.inference_mode()
+ def detect(
+ self, batch, *, num_keypoints, return_dense_probs=False
+ ) -> dict[str, torch.Tensor]:
+ self.train(False)
+ scoremap = self.forward(batch)["scoremap"]
+ B, K, H, W = scoremap.shape
+ dense_probs = (
+ scoremap.reshape(B, K * H * W)
+ .softmax(dim=-1)
+ .reshape(B, K, H * W)
+ .sum(dim=1)
+ )
+ dense_probs = dense_probs.reshape(B, H, W)
+ keypoints, confidence = sample_keypoints(
+ dense_probs,
+ use_nms=True,
+ nms_size=self.nms_size,
+ sample_topk=True,
+ num_samples=num_keypoints,
+ return_probs=True,
+ increase_coverage=self.increase_coverage,
+ remove_borders=self.remove_borders,
+ coverage_pow=self.coverage_pow,
+ coverage_size=self.coverage_size,
+ subpixel=self.subpixel,
+ subpixel_temp=self.subpixel_temp,
+ scoremap=scoremap.reshape(B, H, W),
+ )
+ result = {"keypoints": keypoints, "keypoint_probs": confidence}
+ if return_dense_probs:
+ result["dense_probs"] = dense_probs
+ return result
+
+ def load_image(self, im_path, device=get_best_device()) -> dict[str, torch.Tensor]:
+ pil_im = Image.open(im_path)
+ check_not_i16(pil_im)
+ pil_im = pil_im.convert("RGB")
+ if self.keep_aspect_ratio:
+ W, H = pil_im.size
+ scale = self.resize / max(W, H)
+ W = int((scale * W) // 8 * 8)
+ H = int((scale * H) // 8 * 8)
+ else:
+ H, W = self.resize, self.resize
+ pil_im = pil_im.resize((W, H))
+ standard_im = np.array(pil_im) / 255.0
+ return {
+ "image": self.normalizer(torch.from_numpy(standard_im).permute(2, 0, 1))
+ .float()
+ .to(device)[None]
+ }
+
+
+class Decoder(nn.Module):
+ def __init__(
+ self, layers, *args, super_resolution=False, num_prototypes=1, **kwargs
+ ) -> None:
+ super().__init__(*args, **kwargs)
+ self.layers = layers
+ self.scales = self.layers.keys()
+ self.super_resolution = super_resolution
+ self.num_prototypes = num_prototypes
+
+ def forward(self, features, context=None, scale=None):
+ if context is not None:
+ features = torch.cat((features, context), dim=1)
+ stuff = self.layers[scale](features)
+ logits, context = (
+ stuff[:, : self.num_prototypes],
+ stuff[:, self.num_prototypes :],
+ )
+ return logits, context
+
+
+class ConvRefiner(nn.Module):
+ def __init__(
+ self,
+ in_dim=6,
+ hidden_dim=16,
+ out_dim=2,
+ dw=True,
+ kernel_size=5,
+ hidden_blocks=5,
+ amp=True,
+ residual=False,
+ amp_dtype=torch.float16,
+ ):
+ super().__init__()
+ self.block1 = self.create_block(
+ in_dim,
+ hidden_dim,
+ dw=False,
+ kernel_size=1,
+ )
+ self.hidden_blocks = nn.Sequential(
+ *[
+ self.create_block(
+ hidden_dim,
+ hidden_dim,
+ dw=dw,
+ kernel_size=kernel_size,
+ )
+ for hb in range(hidden_blocks)
+ ]
+ )
+ self.hidden_blocks = self.hidden_blocks
+ self.out_conv = nn.Conv2d(hidden_dim, out_dim, 1, 1, 0)
+ self.amp = amp
+ self.amp_dtype = amp_dtype
+ self.residual = residual
+
+ def create_block(
+ self,
+ in_dim,
+ out_dim,
+ dw=True,
+ kernel_size=5,
+ bias=True,
+ norm_type=nn.BatchNorm2d,
+ ):
+ num_groups = 1 if not dw else in_dim
+ if dw:
+ assert out_dim % in_dim == 0, (
+ "outdim must be divisible by indim for depthwise"
+ )
+ conv1 = nn.Conv2d(
+ in_dim,
+ out_dim,
+ kernel_size=kernel_size,
+ stride=1,
+ padding=kernel_size // 2,
+ groups=num_groups,
+ bias=bias,
+ )
+ norm = (
+ norm_type(out_dim)
+ if norm_type is nn.BatchNorm2d
+ else norm_type(num_channels=out_dim)
+ )
+ relu = nn.ReLU(inplace=True)
+ conv2 = nn.Conv2d(out_dim, out_dim, 1, 1, 0)
+ return nn.Sequential(conv1, norm, relu, conv2)
+
+ def forward(self, feats):
+ b, c, hs, ws = feats.shape
+ with torch.autocast(device_type=feats.device.type, enabled=self.amp, dtype=self.amp_dtype):
+ x0 = self.block1(feats)
+ x = self.hidden_blocks(x0)
+ if self.residual:
+ x = (x + x0) / 1.4
+ x = self.out_conv(x)
+ return x
+
+
+class VGG19(nn.Module):
+ def __init__(self, amp=False, amp_dtype=torch.float16) -> None:
+ super().__init__()
+ self.layers = nn.ModuleList(tvm.vgg19_bn().features[:40])
+ # Maxpool layers: 6, 13, 26, 39
+ self.amp = amp
+ self.amp_dtype = amp_dtype
+
+ def forward(self, x, **kwargs):
+ with torch.autocast(device_type=x.device.type, enabled=self.amp, dtype=self.amp_dtype):
+ feats = []
+ sizes = []
+ for layer in self.layers:
+ if isinstance(layer, nn.MaxPool2d):
+ feats.append(x)
+ sizes.append(x.shape[-2:])
+ x = layer(x)
+ return feats, sizes
+
+
+class VGG(nn.Module):
+ def __init__(self, size="19", amp=False, amp_dtype=torch.float16) -> None:
+ super().__init__()
+ if size == "11":
+ self.layers = nn.ModuleList(tvm.vgg11_bn().features[:22])
+ elif size == "13":
+ self.layers = nn.ModuleList(tvm.vgg13_bn().features[:28])
+ elif size == "19":
+ self.layers = nn.ModuleList(tvm.vgg19_bn().features[:40])
+ # Maxpool layers: 6, 13, 26, 39
+ self.amp = amp
+ self.amp_dtype = amp_dtype
+
+ def forward(self, x, **kwargs):
+ with torch.autocast(device_type=x.device.type, enabled=self.amp, dtype=self.amp_dtype):
+ feats = []
+ sizes = []
+ for layer in self.layers:
+ if isinstance(layer, nn.MaxPool2d):
+ feats.append(x)
+ sizes.append(x.shape[-2:])
+ x = layer(x)
+ return feats, sizes
+
+
+def dedode_detector_S():
+ residual = True
+ hidden_blocks = 3
+ amp_dtype = torch.float16
+ amp = True
+ NUM_PROTOTYPES = 1
+ conv_refiner = nn.ModuleDict(
+ {
+ "8": ConvRefiner(
+ 512,
+ 512,
+ 256 + NUM_PROTOTYPES,
+ hidden_blocks=hidden_blocks,
+ residual=residual,
+ amp=amp,
+ amp_dtype=amp_dtype,
+ ),
+ "4": ConvRefiner(
+ 256 + 256,
+ 256,
+ 128 + NUM_PROTOTYPES,
+ hidden_blocks=hidden_blocks,
+ residual=residual,
+ amp=amp,
+ amp_dtype=amp_dtype,
+ ),
+ "2": ConvRefiner(
+ 128 + 128,
+ 64,
+ 32 + NUM_PROTOTYPES,
+ hidden_blocks=hidden_blocks,
+ residual=residual,
+ amp=amp,
+ amp_dtype=amp_dtype,
+ ),
+ "1": ConvRefiner(
+ 64 + 32,
+ 32,
+ 1 + NUM_PROTOTYPES,
+ hidden_blocks=hidden_blocks,
+ residual=residual,
+ amp=amp,
+ amp_dtype=amp_dtype,
+ ),
+ }
+ )
+ encoder = VGG(size="11", amp=amp, amp_dtype=amp_dtype)
+ decoder = Decoder(conv_refiner)
+ return encoder, decoder
+
+
+def dedode_detector_B():
+ residual = True
+ hidden_blocks = 5
+ amp_dtype = torch.float16
+ amp = True
+ NUM_PROTOTYPES = 1
+ conv_refiner = nn.ModuleDict(
+ {
+ "8": ConvRefiner(
+ 512,
+ 512,
+ 256 + NUM_PROTOTYPES,
+ hidden_blocks=hidden_blocks,
+ residual=residual,
+ amp=amp,
+ amp_dtype=amp_dtype,
+ ),
+ "4": ConvRefiner(
+ 256 + 256,
+ 256,
+ 128 + NUM_PROTOTYPES,
+ hidden_blocks=hidden_blocks,
+ residual=residual,
+ amp=amp,
+ amp_dtype=amp_dtype,
+ ),
+ "2": ConvRefiner(
+ 128 + 128,
+ 64,
+ 32 + NUM_PROTOTYPES,
+ hidden_blocks=hidden_blocks,
+ residual=residual,
+ amp=amp,
+ amp_dtype=amp_dtype,
+ ),
+ "1": ConvRefiner(
+ 64 + 32,
+ 32,
+ 1 + NUM_PROTOTYPES,
+ hidden_blocks=hidden_blocks,
+ residual=residual,
+ amp=amp,
+ amp_dtype=amp_dtype,
+ ),
+ }
+ )
+ encoder = VGG19(amp=amp, amp_dtype=amp_dtype)
+ decoder = Decoder(conv_refiner)
+ return encoder, decoder
+
+
+def dedode_detector_L():
+ NUM_PROTOTYPES = 1
+ residual = True
+ hidden_blocks = 8
+ amp_dtype = (
+ torch.float16
+ ) # torch.bfloat16 if torch.cuda.is_bf16_supported() else torch.float16
+ amp = True
+ conv_refiner = nn.ModuleDict(
+ {
+ "8": ConvRefiner(
+ 512,
+ 512,
+ 256 + NUM_PROTOTYPES,
+ hidden_blocks=hidden_blocks,
+ residual=residual,
+ amp=amp,
+ amp_dtype=amp_dtype,
+ ),
+ "4": ConvRefiner(
+ 256 + 256,
+ 256,
+ 128 + NUM_PROTOTYPES,
+ hidden_blocks=hidden_blocks,
+ residual=residual,
+ amp=amp,
+ amp_dtype=amp_dtype,
+ ),
+ "2": ConvRefiner(
+ 128 + 128,
+ 128,
+ 64 + NUM_PROTOTYPES,
+ hidden_blocks=hidden_blocks,
+ residual=residual,
+ amp=amp,
+ amp_dtype=amp_dtype,
+ ),
+ "1": ConvRefiner(
+ 64 + 64,
+ 64,
+ 1 + NUM_PROTOTYPES,
+ hidden_blocks=hidden_blocks,
+ residual=residual,
+ amp=amp,
+ amp_dtype=amp_dtype,
+ ),
+ }
+ )
+ encoder = VGG19(amp=amp, amp_dtype=amp_dtype)
+ decoder = Decoder(conv_refiner)
+ return encoder, decoder
+
+
+class DaD(DeDoDeDetector):
+ def __init__(
+ self,
+ encoder: nn.Module,
+ decoder: nn.Module,
+ *args,
+ resize=1024,
+ nms_size=3,
+ remove_borders=False,
+ increase_coverage=False,
+ coverage_pow=None,
+ coverage_size=None,
+ subpixel=True,
+ subpixel_temp=0.5,
+ keep_aspect_ratio=True,
+ **kwargs,
+ ) -> None:
+ super().__init__(
+ *args,
+ encoder=encoder,
+ decoder=decoder,
+ resize=resize,
+ nms_size=nms_size,
+ remove_borders=remove_borders,
+ increase_coverage=increase_coverage,
+ coverage_pow=coverage_pow,
+ coverage_size=coverage_size,
+ subpixel=subpixel,
+ keep_aspect_ratio=keep_aspect_ratio,
+ subpixel_temp=subpixel_temp,
+ **kwargs,
+ )
+
+
+class DeDoDev2(DeDoDeDetector):
+ def __init__(
+ self,
+ encoder: nn.Module,
+ decoder: nn.Module,
+ *args,
+ resize=784,
+ nms_size=3,
+ remove_borders=False,
+ increase_coverage=True,
+ coverage_pow=0.5,
+ coverage_size=51,
+ subpixel=False,
+ subpixel_temp=None,
+ keep_aspect_ratio=False,
+ **kwargs,
+ ) -> None:
+ super().__init__(
+ *args,
+ encoder=encoder,
+ decoder=decoder,
+ resize=resize,
+ nms_size=nms_size,
+ remove_borders=remove_borders,
+ increase_coverage=increase_coverage,
+ coverage_pow=coverage_pow,
+ coverage_size=coverage_size,
+ subpixel=subpixel,
+ keep_aspect_ratio=keep_aspect_ratio,
+ subpixel_temp=subpixel_temp,
+ **kwargs,
+ )
+
+
+def load_DaD(resize=1024, pretrained=True, weights_path=None) -> DaD:
+ if weights_path is None:
+ weights_path = (
+ "https://github.com/Parskatt/dad/releases/download/v0.1.0/dad.pth"
+ )
+ device = get_best_device()
+ encoder, decoder = dedode_detector_S()
+ model = DaD(encoder, decoder, resize=resize).to(device)
+ if pretrained:
+ weights = torch.hub.load_state_dict_from_url(
+ weights_path, weights_only=False, map_location=device
+ )
+ model.load_state_dict(weights)
+ return model
+
+
+def load_DaDLight(resize=1024, weights_path=None) -> DaD:
+ if weights_path is None:
+ weights_path = (
+ "https://github.com/Parskatt/dad/releases/download/v0.1.0/dad_light.pth"
+ )
+ return load_DaD(
+ resize=resize,
+ pretrained=True,
+ weights_path=weights_path,
+ )
+
+
+def load_DaDDark(resize=1024, weights_path=None) -> DaD:
+ if weights_path is None:
+ weights_path = (
+ "https://github.com/Parskatt/dad/releases/download/v0.1.0/dad_dark.pth"
+ )
+ return load_DaD(
+ resize=resize,
+ pretrained=True,
+ weights_path=weights_path,
+ )
+
+
+def load_dedode_v2() -> DeDoDev2:
+ device = get_best_device()
+ weights = torch.hub.load_state_dict_from_url(
+ "https://github.com/Parskatt/DeDoDe/releases/download/v2/dedode_detector_L_v2.pth",
+ map_location=device,
+ )
+
+ encoder, decoder = dedode_detector_L()
+ model = DeDoDev2(encoder, decoder).to(device)
+ model.load_state_dict(weights)
+ return model
diff --git a/imcui/third_party/dad/dad/detectors/third_party/__init__.py b/imcui/third_party/dad/dad/detectors/third_party/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..46249aa7b8184fb87e9e5d42427f70b043dfc405
--- /dev/null
+++ b/imcui/third_party/dad/dad/detectors/third_party/__init__.py
@@ -0,0 +1,11 @@
+from .lightglue_detector import LightGlueDetector as LightGlueDetector
+from .lightglue import SuperPoint as SuperPoint
+from .lightglue import ReinforcedFP as ReinforcedFP
+from .lightglue import DISK as DISK
+from .lightglue import ALIKED as ALIKED
+from .lightglue import ALIKEDROT as ALIKEDROT
+from .lightglue import SIFT as SIFT
+from .lightglue import DoGHardNet as DoGHardNet
+from .hesaff import HesAff as HesAff
+from .harrisaff import HarrisAff as HarrisAff
+from .rekd.rekd import load_REKD as load_REKD
diff --git a/imcui/third_party/dad/dad/detectors/third_party/harrisaff.py b/imcui/third_party/dad/dad/detectors/third_party/harrisaff.py
new file mode 100644
index 0000000000000000000000000000000000000000..d0355cc165418d5020949ff7c4cfd3660aeb578c
--- /dev/null
+++ b/imcui/third_party/dad/dad/detectors/third_party/harrisaff.py
@@ -0,0 +1,35 @@
+import numpy as np
+import torch
+
+from dad.types import Detector
+import cv2
+
+from dad.utils import get_best_device
+
+
+class HarrisAff(Detector):
+ def __init__(self):
+ super().__init__()
+ self.detector = cv2.xfeatures2d.HarrisLaplaceFeatureDetector_create(
+ numOctaves=6, corn_thresh=0.0, DOG_thresh=0.0, maxCorners=8192, num_layers=4
+ )
+
+ @property
+ def topleft(self):
+ return 0.0
+
+ def load_image(self, im_path):
+ return {"image": cv2.imread(im_path, cv2.IMREAD_GRAYSCALE)}
+
+ @torch.inference_mode()
+ def detect(self, batch, *, num_keypoints, return_dense_probs=False) -> dict[str, torch.Tensor]:
+ img = batch["image"]
+ H, W = img.shape
+ # Detect keypoints
+ kps = self.detector.detect(img)
+ kps = np.array([kp.pt for kp in kps])[:num_keypoints]
+ kps_n = self.to_normalized_coords(torch.from_numpy(kps), H, W)[None]
+ detections = {"keypoints": kps_n.to(get_best_device()).float(), "keypoint_probs": None}
+ if return_dense_probs:
+ detections["dense_probs"] = None
+ return detections
diff --git a/imcui/third_party/dad/dad/detectors/third_party/hesaff.py b/imcui/third_party/dad/dad/detectors/third_party/hesaff.py
new file mode 100644
index 0000000000000000000000000000000000000000..b3083a6ab1cbe805292093e2c5095cbd91e79eb8
--- /dev/null
+++ b/imcui/third_party/dad/dad/detectors/third_party/hesaff.py
@@ -0,0 +1,40 @@
+from PIL import Image
+
+import torch
+
+from dad.utils import get_best_device
+from dad.types import Detector
+
+
+class HesAff(Detector):
+ def __init__(self):
+ raise NotImplementedError("Buggy implementation, don't use.")
+ super().__init__()
+ import pyhesaff
+
+ self.params = pyhesaff.get_hesaff_default_params()
+
+ @property
+ def topleft(self):
+ return 0.0
+
+ def load_image(self, im_path):
+ # pyhesaff doesn't seem to have a decoupled image loading and detection stage
+ # so load_image here is just identity
+ return {"image": im_path}
+
+ def detect(self, batch, *, num_keypoints, return_dense_probs=False):
+ import pyhesaff
+
+ im_path = batch["image"]
+ W, H = Image.open(im_path).size
+ detections = pyhesaff.detect_feats(im_path)[0][:num_keypoints]
+ kps = detections[..., :2]
+ kps_n = self.to_normalized_coords(torch.from_numpy(kps), H, W)[None]
+ result = {
+ "keypoints": kps_n.to(get_best_device()).float(),
+ "keypoint_probs": None,
+ }
+ if return_dense_probs is not None:
+ result["dense_probs"] = None
+ return result
diff --git a/imcui/third_party/dad/dad/detectors/third_party/lightglue/__init__.py b/imcui/third_party/dad/dad/detectors/third_party/lightglue/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..39ede02a289e34c60d271a2fcdb7a8d9fca2799b
--- /dev/null
+++ b/imcui/third_party/dad/dad/detectors/third_party/lightglue/__init__.py
@@ -0,0 +1,9 @@
+from .aliked import ALIKED # noqa
+from .aliked import ALIKEDROT as ALIKEDROT # noqa
+from .disk import DISK # noqa
+from .dog_hardnet import DoGHardNet # noqa
+from .lightglue import LightGlue # noqa
+from .sift import SIFT # noqa
+from .superpoint import SuperPoint # noqa
+from .superpoint import ReinforcedFP # noqa
+from .utils import match_pair # noqa
diff --git a/imcui/third_party/dad/dad/detectors/third_party/lightglue/aliked.py b/imcui/third_party/dad/dad/detectors/third_party/lightglue/aliked.py
new file mode 100644
index 0000000000000000000000000000000000000000..74870cb31b304931d89eca9ec47ed41a47aa2c61
--- /dev/null
+++ b/imcui/third_party/dad/dad/detectors/third_party/lightglue/aliked.py
@@ -0,0 +1,770 @@
+# BSD 3-Clause License
+
+# Copyright (c) 2022, Zhao Xiaoming
+# All rights reserved.
+
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+
+# 1. Redistributions of source code must retain the above copyright notice, this
+# list of conditions and the following disclaimer.
+
+# 2. Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+
+# 3. Neither the name of the copyright holder nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+# Authors:
+# Xiaoming Zhao, Xingming Wu, Weihai Chen, Peter C.Y. Chen, Qingsong Xu, and Zhengguo Li
+# Code from https://github.com/Shiaoming/ALIKED
+
+from typing import Callable, Optional
+
+import torch
+import torch.nn.functional as F
+import torchvision
+from kornia.color import grayscale_to_rgb
+from torch import nn
+from torch.nn.modules.utils import _pair
+from torchvision.models import resnet
+
+from .utils import Extractor
+
+
+def get_patches(
+ tensor: torch.Tensor, required_corners: torch.Tensor, ps: int
+) -> torch.Tensor:
+ c, h, w = tensor.shape
+ corner = (required_corners - ps / 2 + 1).long()
+ corner[:, 0] = corner[:, 0].clamp(min=0, max=w - 1 - ps)
+ corner[:, 1] = corner[:, 1].clamp(min=0, max=h - 1 - ps)
+ offset = torch.arange(0, ps)
+
+ kw = {"indexing": "ij"} if torch.__version__ >= "1.10" else {}
+ x, y = torch.meshgrid(offset, offset, **kw)
+ patches = torch.stack((x, y)).permute(2, 1, 0).unsqueeze(2)
+ patches = patches.to(corner) + corner[None, None]
+ pts = patches.reshape(-1, 2)
+ sampled = tensor.permute(1, 2, 0)[tuple(pts.T)[::-1]]
+ sampled = sampled.reshape(ps, ps, -1, c)
+ assert sampled.shape[:3] == patches.shape[:3]
+ return sampled.permute(2, 3, 0, 1)
+
+
+def simple_nms(scores: torch.Tensor, nms_radius: int):
+ """Fast Non-maximum suppression to remove nearby points"""
+
+ zeros = torch.zeros_like(scores)
+ max_mask = scores == torch.nn.functional.max_pool2d(
+ scores, kernel_size=nms_radius * 2 + 1, stride=1, padding=nms_radius
+ )
+
+ for _ in range(2):
+ supp_mask = (
+ torch.nn.functional.max_pool2d(
+ max_mask.float(),
+ kernel_size=nms_radius * 2 + 1,
+ stride=1,
+ padding=nms_radius,
+ )
+ > 0
+ )
+ supp_scores = torch.where(supp_mask, zeros, scores)
+ new_max_mask = supp_scores == torch.nn.functional.max_pool2d(
+ supp_scores, kernel_size=nms_radius * 2 + 1, stride=1, padding=nms_radius
+ )
+ max_mask = max_mask | (new_max_mask & (~supp_mask))
+ return torch.where(max_mask, scores, zeros)
+
+
+class DKD(nn.Module):
+ def __init__(
+ self,
+ radius: int = 2,
+ top_k: int = 0,
+ scores_th: float = 0.2,
+ n_limit: int = 20000,
+ ):
+ """
+ Args:
+ radius: soft detection radius, kernel size is (2 * radius + 1)
+ top_k: top_k > 0: return top k keypoints
+ scores_th: top_k <= 0 threshold mode:
+ scores_th > 0: return keypoints with scores>scores_th
+ else: return keypoints with scores > scores.mean()
+ n_limit: max number of keypoint in threshold mode
+ """
+ super().__init__()
+ self.radius = radius
+ self.top_k = top_k
+ self.scores_th = scores_th
+ self.n_limit = n_limit
+ self.kernel_size = 2 * self.radius + 1
+ self.temperature = 0.1 # tuned temperature
+ self.unfold = nn.Unfold(kernel_size=self.kernel_size, padding=self.radius)
+ # local xy grid
+ x = torch.linspace(-self.radius, self.radius, self.kernel_size)
+ # (kernel_size*kernel_size) x 2 : (w,h)
+ kw = {"indexing": "ij"} if torch.__version__ >= "1.10" else {}
+ self.hw_grid = (
+ torch.stack(torch.meshgrid([x, x], **kw)).view(2, -1).t()[:, [1, 0]]
+ )
+
+ def forward(
+ self,
+ scores_map: torch.Tensor,
+ sub_pixel: bool = True,
+ image_size: Optional[torch.Tensor] = None,
+ ):
+ """
+ :param scores_map: Bx1xHxW
+ :param descriptor_map: BxCxHxW
+ :param sub_pixel: whether to use sub-pixel keypoint detection
+ :return: kpts: list[Nx2,...]; kptscores: list[N,....] normalised position: -1~1
+ """
+ b, c, h, w = scores_map.shape
+ scores_nograd = scores_map.detach()
+ nms_scores = simple_nms(scores_nograd, self.radius)
+
+ # remove border
+ nms_scores[:, :, : self.radius, :] = 0
+ nms_scores[:, :, :, : self.radius] = 0
+ if image_size is not None:
+ for i in range(scores_map.shape[0]):
+ w, h = image_size[i].long()
+ nms_scores[i, :, h.item() - self.radius :, :] = 0
+ nms_scores[i, :, :, w.item() - self.radius :] = 0
+ else:
+ nms_scores[:, :, -self.radius :, :] = 0
+ nms_scores[:, :, :, -self.radius :] = 0
+
+ # detect keypoints without grad
+ if self.top_k > 0:
+ topk = torch.topk(nms_scores.view(b, -1), self.top_k)
+ indices_keypoints = [topk.indices[i] for i in range(b)] # B x top_k
+ else:
+ if self.scores_th > 0:
+ masks = nms_scores > self.scores_th
+ if masks.sum() == 0:
+ th = scores_nograd.reshape(b, -1).mean(dim=1) # th = self.scores_th
+ masks = nms_scores > th.reshape(b, 1, 1, 1)
+ else:
+ th = scores_nograd.reshape(b, -1).mean(dim=1) # th = self.scores_th
+ masks = nms_scores > th.reshape(b, 1, 1, 1)
+ masks = masks.reshape(b, -1)
+
+ indices_keypoints = [] # list, B x (any size)
+ scores_view = scores_nograd.reshape(b, -1)
+ for mask, scores in zip(masks, scores_view):
+ indices = mask.nonzero()[:, 0]
+ if len(indices) > self.n_limit:
+ kpts_sc = scores[indices]
+ sort_idx = kpts_sc.sort(descending=True)[1]
+ sel_idx = sort_idx[: self.n_limit]
+ indices = indices[sel_idx]
+ indices_keypoints.append(indices)
+ wh = torch.tensor([w - 1, h - 1], device=scores_nograd.device)
+
+ keypoints = []
+ scoredispersitys = []
+ kptscores = []
+ if sub_pixel:
+ # detect soft keypoints with grad backpropagation
+ patches = self.unfold(scores_map) # B x (kernel**2) x (H*W)
+ # print(patches.shape)
+ self.hw_grid = self.hw_grid.to(scores_map) # to device
+ for b_idx in range(b):
+ patch = patches[b_idx].t() # (H*W) x (kernel**2)
+ indices_kpt = indices_keypoints[
+ b_idx
+ ] # one dimension vector, say its size is M
+ patch_scores = patch[indices_kpt] # M x (kernel**2)
+ keypoints_xy_nms = torch.stack(
+ [indices_kpt % w, torch.div(indices_kpt, w, rounding_mode="trunc")],
+ dim=1,
+ ) # Mx2
+
+ # max is detached to prevent undesired backprop loops in the graph
+ max_v = patch_scores.max(dim=1).values.detach()[:, None]
+ x_exp = (
+ (patch_scores - max_v) / self.temperature
+ ).exp() # M * (kernel**2), in [0, 1]
+
+ # \frac{ \sum{(i,j) \times \exp(x/T)} }{ \sum{\exp(x/T)} }
+ xy_residual = (
+ x_exp @ self.hw_grid / x_exp.sum(dim=1)[:, None]
+ ) # Soft-argmax, Mx2
+
+ hw_grid_dist2 = (
+ torch.norm(
+ (self.hw_grid[None, :, :] - xy_residual[:, None, :])
+ / self.radius,
+ dim=-1,
+ )
+ ** 2
+ )
+ scoredispersity = (x_exp * hw_grid_dist2).sum(dim=1) / x_exp.sum(dim=1)
+
+ # compute result keypoints
+ keypoints_xy = keypoints_xy_nms + xy_residual
+ keypoints_xy = keypoints_xy / wh * 2 - 1 # (w,h) -> (-1~1,-1~1)
+
+ kptscore = torch.nn.functional.grid_sample(
+ scores_map[b_idx].unsqueeze(0),
+ keypoints_xy.view(1, 1, -1, 2),
+ mode="bilinear",
+ align_corners=True,
+ )[0, 0, 0, :] # CxN
+
+ keypoints.append(keypoints_xy)
+ scoredispersitys.append(scoredispersity)
+ kptscores.append(kptscore)
+ else:
+ for b_idx in range(b):
+ indices_kpt = indices_keypoints[
+ b_idx
+ ] # one dimension vector, say its size is M
+ # To avoid warning: UserWarning: __floordiv__ is deprecated
+ keypoints_xy_nms = torch.stack(
+ [indices_kpt % w, torch.div(indices_kpt, w, rounding_mode="trunc")],
+ dim=1,
+ ) # Mx2
+ keypoints_xy = keypoints_xy_nms / wh * 2 - 1 # (w,h) -> (-1~1,-1~1)
+ kptscore = torch.nn.functional.grid_sample(
+ scores_map[b_idx].unsqueeze(0),
+ keypoints_xy.view(1, 1, -1, 2),
+ mode="bilinear",
+ align_corners=True,
+ )[0, 0, 0, :] # CxN
+ keypoints.append(keypoints_xy)
+ scoredispersitys.append(kptscore) # for jit.script compatability
+ kptscores.append(kptscore)
+
+ return keypoints, scoredispersitys, kptscores
+
+
+class InputPadder(object):
+ """Pads images such that dimensions are divisible by 8"""
+
+ def __init__(self, h: int, w: int, divis_by: int = 8):
+ self.ht = h
+ self.wd = w
+ pad_ht = (((self.ht // divis_by) + 1) * divis_by - self.ht) % divis_by
+ pad_wd = (((self.wd // divis_by) + 1) * divis_by - self.wd) % divis_by
+ self._pad = [
+ pad_wd // 2,
+ pad_wd - pad_wd // 2,
+ pad_ht // 2,
+ pad_ht - pad_ht // 2,
+ ]
+
+ def pad(self, x: torch.Tensor):
+ assert x.ndim == 4
+ return F.pad(x, self._pad, mode="replicate")
+
+ def unpad(self, x: torch.Tensor):
+ assert x.ndim == 4
+ ht = x.shape[-2]
+ wd = x.shape[-1]
+ c = [self._pad[2], ht - self._pad[3], self._pad[0], wd - self._pad[1]]
+ return x[..., c[0] : c[1], c[2] : c[3]]
+
+
+class DeformableConv2d(nn.Module):
+ def __init__(
+ self,
+ in_channels,
+ out_channels,
+ kernel_size=3,
+ stride=1,
+ padding=1,
+ bias=False,
+ mask=False,
+ ):
+ super(DeformableConv2d, self).__init__()
+
+ self.padding = padding
+ self.mask = mask
+
+ self.channel_num = (
+ 3 * kernel_size * kernel_size if mask else 2 * kernel_size * kernel_size
+ )
+ self.offset_conv = nn.Conv2d(
+ in_channels,
+ self.channel_num,
+ kernel_size=kernel_size,
+ stride=stride,
+ padding=self.padding,
+ bias=True,
+ )
+
+ self.regular_conv = nn.Conv2d(
+ in_channels=in_channels,
+ out_channels=out_channels,
+ kernel_size=kernel_size,
+ stride=stride,
+ padding=self.padding,
+ bias=bias,
+ )
+
+ def forward(self, x):
+ h, w = x.shape[2:]
+ max_offset = max(h, w) / 4.0
+
+ out = self.offset_conv(x)
+ if self.mask:
+ o1, o2, mask = torch.chunk(out, 3, dim=1)
+ offset = torch.cat((o1, o2), dim=1)
+ mask = torch.sigmoid(mask)
+ else:
+ offset = out
+ mask = None
+ offset = offset.clamp(-max_offset, max_offset)
+ x = torchvision.ops.deform_conv2d(
+ input=x,
+ offset=offset,
+ weight=self.regular_conv.weight,
+ bias=self.regular_conv.bias,
+ padding=self.padding,
+ mask=mask,
+ )
+ return x
+
+
+def get_conv(
+ inplanes,
+ planes,
+ kernel_size=3,
+ stride=1,
+ padding=1,
+ bias=False,
+ conv_type="conv",
+ mask=False,
+):
+ if conv_type == "conv":
+ conv = nn.Conv2d(
+ inplanes,
+ planes,
+ kernel_size=kernel_size,
+ stride=stride,
+ padding=padding,
+ bias=bias,
+ )
+ elif conv_type == "dcn":
+ conv = DeformableConv2d(
+ inplanes,
+ planes,
+ kernel_size=kernel_size,
+ stride=stride,
+ padding=_pair(padding),
+ bias=bias,
+ mask=mask,
+ )
+ else:
+ raise TypeError
+ return conv
+
+
+class ConvBlock(nn.Module):
+ def __init__(
+ self,
+ in_channels,
+ out_channels,
+ gate: Optional[Callable[..., nn.Module]] = None,
+ norm_layer: Optional[Callable[..., nn.Module]] = None,
+ conv_type: str = "conv",
+ mask: bool = False,
+ ):
+ super().__init__()
+ if gate is None:
+ self.gate = nn.ReLU(inplace=True)
+ else:
+ self.gate = gate
+ if norm_layer is None:
+ norm_layer = nn.BatchNorm2d
+ self.conv1 = get_conv(
+ in_channels, out_channels, kernel_size=3, conv_type=conv_type, mask=mask
+ )
+ self.bn1 = norm_layer(out_channels)
+ self.conv2 = get_conv(
+ out_channels, out_channels, kernel_size=3, conv_type=conv_type, mask=mask
+ )
+ self.bn2 = norm_layer(out_channels)
+
+ def forward(self, x):
+ x = self.gate(self.bn1(self.conv1(x))) # B x in_channels x H x W
+ x = self.gate(self.bn2(self.conv2(x))) # B x out_channels x H x W
+ return x
+
+
+# modified based on torchvision\models\resnet.py#27->BasicBlock
+class ResBlock(nn.Module):
+ expansion: int = 1
+
+ def __init__(
+ self,
+ inplanes: int,
+ planes: int,
+ stride: int = 1,
+ downsample: Optional[nn.Module] = None,
+ groups: int = 1,
+ base_width: int = 64,
+ dilation: int = 1,
+ gate: Optional[Callable[..., nn.Module]] = None,
+ norm_layer: Optional[Callable[..., nn.Module]] = None,
+ conv_type: str = "conv",
+ mask: bool = False,
+ ) -> None:
+ super(ResBlock, self).__init__()
+ if gate is None:
+ self.gate = nn.ReLU(inplace=True)
+ else:
+ self.gate = gate
+ if norm_layer is None:
+ norm_layer = nn.BatchNorm2d
+ if groups != 1 or base_width != 64:
+ raise ValueError("ResBlock only supports groups=1 and base_width=64")
+ if dilation > 1:
+ raise NotImplementedError("Dilation > 1 not supported in ResBlock")
+ # Both self.conv1 and self.downsample layers
+ # downsample the input when stride != 1
+ self.conv1 = get_conv(
+ inplanes, planes, kernel_size=3, conv_type=conv_type, mask=mask
+ )
+ self.bn1 = norm_layer(planes)
+ self.conv2 = get_conv(
+ planes, planes, kernel_size=3, conv_type=conv_type, mask=mask
+ )
+ self.bn2 = norm_layer(planes)
+ self.downsample = downsample
+ self.stride = stride
+
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
+ identity = x
+
+ out = self.conv1(x)
+ out = self.bn1(out)
+ out = self.gate(out)
+
+ out = self.conv2(out)
+ out = self.bn2(out)
+
+ if self.downsample is not None:
+ identity = self.downsample(x)
+
+ out += identity
+ out = self.gate(out)
+
+ return out
+
+
+class SDDH(nn.Module):
+ def __init__(
+ self,
+ dims: int,
+ kernel_size: int = 3,
+ n_pos: int = 8,
+ gate=nn.ReLU(),
+ conv2D=False,
+ mask=False,
+ ):
+ super(SDDH, self).__init__()
+ self.kernel_size = kernel_size
+ self.n_pos = n_pos
+ self.conv2D = conv2D
+ self.mask = mask
+
+ self.get_patches_func = get_patches
+
+ # estimate offsets
+ self.channel_num = 3 * n_pos if mask else 2 * n_pos
+ self.offset_conv = nn.Sequential(
+ nn.Conv2d(
+ dims,
+ self.channel_num,
+ kernel_size=kernel_size,
+ stride=1,
+ padding=0,
+ bias=True,
+ ),
+ gate,
+ nn.Conv2d(
+ self.channel_num,
+ self.channel_num,
+ kernel_size=1,
+ stride=1,
+ padding=0,
+ bias=True,
+ ),
+ )
+
+ # sampled feature conv
+ self.sf_conv = nn.Conv2d(
+ dims, dims, kernel_size=1, stride=1, padding=0, bias=False
+ )
+
+ # convM
+ if not conv2D:
+ # deformable desc weights
+ agg_weights = torch.nn.Parameter(torch.rand(n_pos, dims, dims))
+ self.register_parameter("agg_weights", agg_weights)
+ else:
+ self.convM = nn.Conv2d(
+ dims * n_pos, dims, kernel_size=1, stride=1, padding=0, bias=False
+ )
+
+ def forward(self, x, keypoints):
+ # x: [B,C,H,W]
+ # keypoints: list, [[N_kpts,2], ...] (w,h)
+ b, c, h, w = x.shape
+ wh = torch.tensor([[w - 1, h - 1]], device=x.device)
+ max_offset = max(h, w) / 4.0
+
+ offsets = []
+ descriptors = []
+ # get offsets for each keypoint
+ for ib in range(b):
+ xi, kptsi = x[ib], keypoints[ib]
+ kptsi_wh = (kptsi / 2 + 0.5) * wh
+ N_kpts = len(kptsi)
+
+ if self.kernel_size > 1:
+ patch = self.get_patches_func(
+ xi, kptsi_wh.long(), self.kernel_size
+ ) # [N_kpts, C, K, K]
+ else:
+ kptsi_wh_long = kptsi_wh.long()
+ patch = (
+ xi[:, kptsi_wh_long[:, 1], kptsi_wh_long[:, 0]]
+ .permute(1, 0)
+ .reshape(N_kpts, c, 1, 1)
+ )
+
+ offset = self.offset_conv(patch).clamp(
+ -max_offset, max_offset
+ ) # [N_kpts, 2*n_pos, 1, 1]
+ if self.mask:
+ offset = (
+ offset[:, :, 0, 0].view(N_kpts, 3, self.n_pos).permute(0, 2, 1)
+ ) # [N_kpts, n_pos, 3]
+ offset = offset[:, :, :-1] # [N_kpts, n_pos, 2]
+ mask_weight = torch.sigmoid(offset[:, :, -1]) # [N_kpts, n_pos]
+ else:
+ offset = (
+ offset[:, :, 0, 0].view(N_kpts, 2, self.n_pos).permute(0, 2, 1)
+ ) # [N_kpts, n_pos, 2]
+ offsets.append(offset) # for visualization
+
+ # get sample positions
+ pos = kptsi_wh.unsqueeze(1) + offset # [N_kpts, n_pos, 2]
+ pos = 2.0 * pos / wh[None] - 1
+ pos = pos.reshape(1, N_kpts * self.n_pos, 1, 2)
+
+ # sample features
+ features = F.grid_sample(
+ xi.unsqueeze(0), pos, mode="bilinear", align_corners=True
+ ) # [1,C,(N_kpts*n_pos),1]
+ features = features.reshape(c, N_kpts, self.n_pos, 1).permute(
+ 1, 0, 2, 3
+ ) # [N_kpts, C, n_pos, 1]
+ if self.mask:
+ features = torch.einsum("ncpo,np->ncpo", features, mask_weight)
+
+ features = torch.selu_(self.sf_conv(features)).squeeze(
+ -1
+ ) # [N_kpts, C, n_pos]
+ # convM
+ if not self.conv2D:
+ descs = torch.einsum(
+ "ncp,pcd->nd", features, self.agg_weights
+ ) # [N_kpts, C]
+ else:
+ features = features.reshape(N_kpts, -1)[
+ :, :, None, None
+ ] # [N_kpts, C*n_pos, 1, 1]
+ descs = self.convM(features).squeeze() # [N_kpts, C]
+
+ # normalize
+ descs = F.normalize(descs, p=2.0, dim=1)
+ descriptors.append(descs)
+
+ return descriptors, offsets
+
+
+class ALIKED(Extractor):
+ default_conf = {
+ "model_name": "aliked-n16",
+ "max_num_keypoints": -1,
+ "detection_threshold": 0.2,
+ "nms_radius": 2,
+ }
+
+ checkpoint_url = "https://github.com/Shiaoming/ALIKED/raw/main/models/{}.pth"
+
+ n_limit_max = 20000
+
+ # c1, c2, c3, c4, dim, K, M
+ cfgs = {
+ "aliked-t16": [8, 16, 32, 64, 64, 3, 16],
+ "aliked-n16": [16, 32, 64, 128, 128, 3, 16],
+ "aliked-n16rot": [16, 32, 64, 128, 128, 3, 16],
+ "aliked-n32": [16, 32, 64, 128, 128, 3, 32],
+ }
+ preprocess_conf = {
+ "resize": 1024,
+ }
+
+ required_data_keys = ["image"]
+
+ def __init__(self, **conf):
+ super().__init__(**conf) # Update with default configuration.
+ conf = self.conf
+ c1, c2, c3, c4, dim, K, M = self.cfgs[conf.model_name]
+ conv_types = ["conv", "conv", "dcn", "dcn"]
+ conv2D = False
+ mask = False
+
+ # build model
+ self.pool2 = nn.AvgPool2d(kernel_size=2, stride=2)
+ self.pool4 = nn.AvgPool2d(kernel_size=4, stride=4)
+ self.norm = nn.BatchNorm2d
+ self.gate = nn.SELU(inplace=True)
+ self.block1 = ConvBlock(3, c1, self.gate, self.norm, conv_type=conv_types[0])
+ self.block2 = self.get_resblock(c1, c2, conv_types[1], mask)
+ self.block3 = self.get_resblock(c2, c3, conv_types[2], mask)
+ self.block4 = self.get_resblock(c3, c4, conv_types[3], mask)
+
+ self.conv1 = resnet.conv1x1(c1, dim // 4)
+ self.conv2 = resnet.conv1x1(c2, dim // 4)
+ self.conv3 = resnet.conv1x1(c3, dim // 4)
+ self.conv4 = resnet.conv1x1(dim, dim // 4)
+ self.upsample2 = nn.Upsample(
+ scale_factor=2, mode="bilinear", align_corners=True
+ )
+ self.upsample4 = nn.Upsample(
+ scale_factor=4, mode="bilinear", align_corners=True
+ )
+ self.upsample8 = nn.Upsample(
+ scale_factor=8, mode="bilinear", align_corners=True
+ )
+ self.upsample32 = nn.Upsample(
+ scale_factor=32, mode="bilinear", align_corners=True
+ )
+ self.score_head = nn.Sequential(
+ resnet.conv1x1(dim, 8),
+ self.gate,
+ resnet.conv3x3(8, 4),
+ self.gate,
+ resnet.conv3x3(4, 4),
+ self.gate,
+ resnet.conv3x3(4, 1),
+ )
+ self.desc_head = SDDH(dim, K, M, gate=self.gate, conv2D=conv2D, mask=mask)
+ self.dkd = DKD(
+ radius=conf.nms_radius,
+ top_k=-1 if conf.detection_threshold > 0 else conf.max_num_keypoints,
+ scores_th=conf.detection_threshold,
+ n_limit=conf.max_num_keypoints
+ if conf.max_num_keypoints > 0
+ else self.n_limit_max,
+ )
+
+ state_dict = torch.hub.load_state_dict_from_url(
+ self.checkpoint_url.format(conf.model_name), map_location="cpu"
+ )
+ self.load_state_dict(state_dict, strict=True)
+
+ def get_resblock(self, c_in, c_out, conv_type, mask):
+ return ResBlock(
+ c_in,
+ c_out,
+ 1,
+ nn.Conv2d(c_in, c_out, 1),
+ gate=self.gate,
+ norm_layer=self.norm,
+ conv_type=conv_type,
+ mask=mask,
+ )
+
+ def extract_dense_map(self, image):
+ # Pads images such that dimensions are divisible by
+ div_by = 2**5
+ padder = InputPadder(image.shape[-2], image.shape[-1], div_by)
+ image = padder.pad(image)
+
+ # ================================== feature encoder
+ x1 = self.block1(image) # B x c1 x H x W
+ x2 = self.pool2(x1)
+ x2 = self.block2(x2) # B x c2 x H/2 x W/2
+ x3 = self.pool4(x2)
+ x3 = self.block3(x3) # B x c3 x H/8 x W/8
+ x4 = self.pool4(x3)
+ x4 = self.block4(x4) # B x dim x H/32 x W/32
+ # ================================== feature aggregation
+ x1 = self.gate(self.conv1(x1)) # B x dim//4 x H x W
+ x2 = self.gate(self.conv2(x2)) # B x dim//4 x H//2 x W//2
+ x3 = self.gate(self.conv3(x3)) # B x dim//4 x H//8 x W//8
+ x4 = self.gate(self.conv4(x4)) # B x dim//4 x H//32 x W//32
+ x2_up = self.upsample2(x2) # B x dim//4 x H x W
+ x3_up = self.upsample8(x3) # B x dim//4 x H x W
+ x4_up = self.upsample32(x4) # B x dim//4 x H x W
+ x1234 = torch.cat([x1, x2_up, x3_up, x4_up], dim=1)
+ # ================================== score head
+ score_map = torch.sigmoid(self.score_head(x1234))
+ feature_map = torch.nn.functional.normalize(x1234, p=2, dim=1)
+
+ # Unpads images
+ feature_map = padder.unpad(feature_map)
+ score_map = padder.unpad(score_map)
+
+ return feature_map, score_map
+
+ def forward(self, data: dict) -> dict:
+ # need to set here unfortunately
+ self.dkd.n_limit = (
+ self.conf.max_num_keypoints
+ if self.conf.max_num_keypoints > 0
+ else self.n_limit_max
+ )
+ image = data["image"]
+ if image.shape[1] == 1:
+ image = grayscale_to_rgb(image)
+ feature_map, score_map = self.extract_dense_map(image)
+ keypoints, kptscores, scoredispersitys = self.dkd(
+ score_map, image_size=data.get("image_size")
+ )
+ # descriptors, offsets = self.desc_head(feature_map, keypoints)
+
+ _, _, h, w = image.shape
+ wh = torch.tensor([w - 1, h - 1], device=image.device)
+ # no padding required
+ # we can set detection_threshold=-1 and conf.max_num_keypoints > 0
+ return {
+ "keypoints": wh * (torch.stack(keypoints) + 1) / 2.0, # B x N x 2
+ # "descriptors": torch.stack(descriptors), # B x N x D
+ "keypoint_scores": torch.stack(kptscores), # B x N
+ "scoremap": score_map, # B x 1 x H x W
+ }
+
+
+class ALIKEDROT(ALIKED):
+ default_conf = {
+ "model_name": "aliked-n16rot",
+ "max_num_keypoints": -1,
+ "detection_threshold": 0.2,
+ "nms_radius": 2,
+ }
diff --git a/imcui/third_party/dad/dad/detectors/third_party/lightglue/disk.py b/imcui/third_party/dad/dad/detectors/third_party/lightglue/disk.py
new file mode 100644
index 0000000000000000000000000000000000000000..13dd07b4acf12bef9d116e5c46741ebf8c153e8c
--- /dev/null
+++ b/imcui/third_party/dad/dad/detectors/third_party/lightglue/disk.py
@@ -0,0 +1,48 @@
+import kornia
+import torch
+
+from .utils import Extractor
+
+
+class DISK(Extractor):
+ default_conf = {
+ "weights": "depth",
+ "max_num_keypoints": None,
+ "desc_dim": 128,
+ "nms_window_size": 5,
+ "detection_threshold": 0.0,
+ "pad_if_not_divisible": True,
+ }
+
+ preprocess_conf = {
+ "resize": 1024,
+ "grayscale": False,
+ }
+
+ required_data_keys = ["image"]
+
+ def __init__(self, **conf) -> None:
+ super().__init__(**conf) # Update with default configuration.
+ self.model = kornia.feature.DISK.from_pretrained(self.conf.weights)
+
+ def forward(self, data: dict) -> dict:
+ """Compute keypoints, scores, descriptors for image"""
+ for key in self.required_data_keys:
+ assert key in data, f"Missing key {key} in data"
+ image = data["image"]
+ if image.shape[1] == 1:
+ image = kornia.color.grayscale_to_rgb(image)
+ features = self.model(
+ image,
+ n=self.conf.max_num_keypoints,
+ window_size=self.conf.nms_window_size,
+ score_threshold=self.conf.detection_threshold,
+ pad_if_not_divisible=self.conf.pad_if_not_divisible,
+ )
+ keypoints = [f.keypoints for f in features]
+
+ keypoints = torch.stack(keypoints, 0)
+
+ return {
+ "keypoints": keypoints.to(image).contiguous(),
+ }
diff --git a/imcui/third_party/dad/dad/detectors/third_party/lightglue/dog_hardnet.py b/imcui/third_party/dad/dad/detectors/third_party/lightglue/dog_hardnet.py
new file mode 100644
index 0000000000000000000000000000000000000000..cce307ae1f11e2066312fd44ecac8884d1de3358
--- /dev/null
+++ b/imcui/third_party/dad/dad/detectors/third_party/lightglue/dog_hardnet.py
@@ -0,0 +1,41 @@
+import torch
+from kornia.color import rgb_to_grayscale
+from kornia.feature import HardNet, LAFDescriptor, laf_from_center_scale_ori
+
+from .sift import SIFT
+
+
+class DoGHardNet(SIFT):
+ required_data_keys = ["image"]
+
+ def __init__(self, **conf):
+ super().__init__(**conf)
+ self.laf_desc = LAFDescriptor(HardNet(True)).eval()
+
+ def forward(self, data: dict) -> dict:
+ image = data["image"]
+ if image.shape[1] == 3:
+ image = rgb_to_grayscale(image)
+ device = image.device
+ self.laf_desc = self.laf_desc.to(device)
+ self.laf_desc.descriptor = self.laf_desc.descriptor.eval()
+ pred = []
+ if "image_size" in data.keys():
+ im_size = data.get("image_size").long()
+ else:
+ im_size = None
+ for k in range(len(image)):
+ img = image[k]
+ if im_size is not None:
+ w, h = data["image_size"][k]
+ img = img[:, : h.to(torch.int32), : w.to(torch.int32)]
+ p = self.extract_single_image(img)
+ lafs = laf_from_center_scale_ori(
+ p["keypoints"].reshape(1, -1, 2),
+ 6.0 * p["scales"].reshape(1, -1, 1, 1),
+ torch.rad2deg(p["oris"]).reshape(1, -1, 1),
+ ).to(device)
+ p["descriptors"] = self.laf_desc(img[None], lafs).reshape(-1, 128)
+ pred.append(p)
+ pred = {k: torch.stack([p[k] for p in pred], 0).to(device) for k in pred[0]}
+ return pred
diff --git a/imcui/third_party/dad/dad/detectors/third_party/lightglue/lightglue.py b/imcui/third_party/dad/dad/detectors/third_party/lightglue/lightglue.py
new file mode 100644
index 0000000000000000000000000000000000000000..37c65adcef928ef8bdfb8a10bd2da1f6327430f6
--- /dev/null
+++ b/imcui/third_party/dad/dad/detectors/third_party/lightglue/lightglue.py
@@ -0,0 +1,655 @@
+import warnings
+from pathlib import Path
+from types import SimpleNamespace
+from typing import Callable, List, Optional, Tuple
+
+import numpy as np
+import torch
+import torch.nn.functional as F
+from torch import nn
+
+try:
+ from flash_attn.modules.mha import FlashCrossAttention
+except ModuleNotFoundError:
+ FlashCrossAttention = None
+
+if FlashCrossAttention or hasattr(F, "scaled_dot_product_attention"):
+ FLASH_AVAILABLE = True
+else:
+ FLASH_AVAILABLE = False
+
+torch.backends.cudnn.deterministic = True
+
+
+@torch.amp.custom_fwd(device_type="cuda", cast_inputs=torch.float32)
+def normalize_keypoints(
+ kpts: torch.Tensor, size: Optional[torch.Tensor] = None
+) -> torch.Tensor:
+ if size is None:
+ size = 1 + kpts.max(-2).values - kpts.min(-2).values
+ elif not isinstance(size, torch.Tensor):
+ size = torch.tensor(size, device=kpts.device, dtype=kpts.dtype)
+ size = size.to(kpts)
+ shift = size / 2
+ scale = size.max(-1).values / 2
+ kpts = (kpts - shift[..., None, :]) / scale[..., None, None]
+ return kpts
+
+
+def pad_to_length(x: torch.Tensor, length: int) -> Tuple[torch.Tensor]:
+ if length <= x.shape[-2]:
+ return x, torch.ones_like(x[..., :1], dtype=torch.bool)
+ pad = torch.ones(
+ *x.shape[:-2], length - x.shape[-2], x.shape[-1], device=x.device, dtype=x.dtype
+ )
+ y = torch.cat([x, pad], dim=-2)
+ mask = torch.zeros(*y.shape[:-1], 1, dtype=torch.bool, device=x.device)
+ mask[..., : x.shape[-2], :] = True
+ return y, mask
+
+
+def rotate_half(x: torch.Tensor) -> torch.Tensor:
+ x = x.unflatten(-1, (-1, 2))
+ x1, x2 = x.unbind(dim=-1)
+ return torch.stack((-x2, x1), dim=-1).flatten(start_dim=-2)
+
+
+def apply_cached_rotary_emb(freqs: torch.Tensor, t: torch.Tensor) -> torch.Tensor:
+ return (t * freqs[0]) + (rotate_half(t) * freqs[1])
+
+
+class LearnableFourierPositionalEncoding(nn.Module):
+ def __init__(self, M: int, dim: int, F_dim: int = None, gamma: float = 1.0) -> None:
+ super().__init__()
+ F_dim = F_dim if F_dim is not None else dim
+ self.gamma = gamma
+ self.Wr = nn.Linear(M, F_dim // 2, bias=False)
+ nn.init.normal_(self.Wr.weight.data, mean=0, std=self.gamma**-2)
+
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
+ """encode position vector"""
+ projected = self.Wr(x)
+ cosines, sines = torch.cos(projected), torch.sin(projected)
+ emb = torch.stack([cosines, sines], 0).unsqueeze(-3)
+ return emb.repeat_interleave(2, dim=-1)
+
+
+class TokenConfidence(nn.Module):
+ def __init__(self, dim: int) -> None:
+ super().__init__()
+ self.token = nn.Sequential(nn.Linear(dim, 1), nn.Sigmoid())
+
+ def forward(self, desc0: torch.Tensor, desc1: torch.Tensor):
+ """get confidence tokens"""
+ return (
+ self.token(desc0.detach()).squeeze(-1),
+ self.token(desc1.detach()).squeeze(-1),
+ )
+
+
+class Attention(nn.Module):
+ def __init__(self, allow_flash: bool) -> None:
+ super().__init__()
+ if allow_flash and not FLASH_AVAILABLE:
+ warnings.warn(
+ "FlashAttention is not available. For optimal speed, "
+ "consider installing torch >= 2.0 or flash-attn.",
+ stacklevel=2,
+ )
+ self.enable_flash = allow_flash and FLASH_AVAILABLE
+ self.has_sdp = hasattr(F, "scaled_dot_product_attention")
+ if allow_flash and FlashCrossAttention:
+ self.flash_ = FlashCrossAttention()
+ if self.has_sdp:
+ torch.backends.cuda.enable_flash_sdp(allow_flash)
+
+ def forward(self, q, k, v, mask: Optional[torch.Tensor] = None) -> torch.Tensor:
+ if q.shape[-2] == 0 or k.shape[-2] == 0:
+ return q.new_zeros((*q.shape[:-1], v.shape[-1]))
+ if self.enable_flash and q.device.type == "cuda":
+ # use torch 2.0 scaled_dot_product_attention with flash
+ if self.has_sdp:
+ args = [x.half().contiguous() for x in [q, k, v]]
+ v = F.scaled_dot_product_attention(*args, attn_mask=mask).to(q.dtype)
+ return v if mask is None else v.nan_to_num()
+ else:
+ assert mask is None
+ q, k, v = [x.transpose(-2, -3).contiguous() for x in [q, k, v]]
+ m = self.flash_(q.half(), torch.stack([k, v], 2).half())
+ return m.transpose(-2, -3).to(q.dtype).clone()
+ elif self.has_sdp:
+ args = [x.contiguous() for x in [q, k, v]]
+ v = F.scaled_dot_product_attention(*args, attn_mask=mask)
+ return v if mask is None else v.nan_to_num()
+ else:
+ s = q.shape[-1] ** -0.5
+ sim = torch.einsum("...id,...jd->...ij", q, k) * s
+ if mask is not None:
+ sim.masked_fill(~mask, -float("inf"))
+ attn = F.softmax(sim, -1)
+ return torch.einsum("...ij,...jd->...id", attn, v)
+
+
+class SelfBlock(nn.Module):
+ def __init__(
+ self, embed_dim: int, num_heads: int, flash: bool = False, bias: bool = True
+ ) -> None:
+ super().__init__()
+ self.embed_dim = embed_dim
+ self.num_heads = num_heads
+ assert self.embed_dim % num_heads == 0
+ self.head_dim = self.embed_dim // num_heads
+ self.Wqkv = nn.Linear(embed_dim, 3 * embed_dim, bias=bias)
+ self.inner_attn = Attention(flash)
+ self.out_proj = nn.Linear(embed_dim, embed_dim, bias=bias)
+ self.ffn = nn.Sequential(
+ nn.Linear(2 * embed_dim, 2 * embed_dim),
+ nn.LayerNorm(2 * embed_dim, elementwise_affine=True),
+ nn.GELU(),
+ nn.Linear(2 * embed_dim, embed_dim),
+ )
+
+ def forward(
+ self,
+ x: torch.Tensor,
+ encoding: torch.Tensor,
+ mask: Optional[torch.Tensor] = None,
+ ) -> torch.Tensor:
+ qkv = self.Wqkv(x)
+ qkv = qkv.unflatten(-1, (self.num_heads, -1, 3)).transpose(1, 2)
+ q, k, v = qkv[..., 0], qkv[..., 1], qkv[..., 2]
+ q = apply_cached_rotary_emb(encoding, q)
+ k = apply_cached_rotary_emb(encoding, k)
+ context = self.inner_attn(q, k, v, mask=mask)
+ message = self.out_proj(context.transpose(1, 2).flatten(start_dim=-2))
+ return x + self.ffn(torch.cat([x, message], -1))
+
+
+class CrossBlock(nn.Module):
+ def __init__(
+ self, embed_dim: int, num_heads: int, flash: bool = False, bias: bool = True
+ ) -> None:
+ super().__init__()
+ self.heads = num_heads
+ dim_head = embed_dim // num_heads
+ self.scale = dim_head**-0.5
+ inner_dim = dim_head * num_heads
+ self.to_qk = nn.Linear(embed_dim, inner_dim, bias=bias)
+ self.to_v = nn.Linear(embed_dim, inner_dim, bias=bias)
+ self.to_out = nn.Linear(inner_dim, embed_dim, bias=bias)
+ self.ffn = nn.Sequential(
+ nn.Linear(2 * embed_dim, 2 * embed_dim),
+ nn.LayerNorm(2 * embed_dim, elementwise_affine=True),
+ nn.GELU(),
+ nn.Linear(2 * embed_dim, embed_dim),
+ )
+ if flash and FLASH_AVAILABLE:
+ self.flash = Attention(True)
+ else:
+ self.flash = None
+
+ def map_(self, func: Callable, x0: torch.Tensor, x1: torch.Tensor):
+ return func(x0), func(x1)
+
+ def forward(
+ self, x0: torch.Tensor, x1: torch.Tensor, mask: Optional[torch.Tensor] = None
+ ) -> List[torch.Tensor]:
+ qk0, qk1 = self.map_(self.to_qk, x0, x1)
+ v0, v1 = self.map_(self.to_v, x0, x1)
+ qk0, qk1, v0, v1 = map(
+ lambda t: t.unflatten(-1, (self.heads, -1)).transpose(1, 2),
+ (qk0, qk1, v0, v1),
+ )
+ if self.flash is not None and qk0.device.type == "cuda":
+ m0 = self.flash(qk0, qk1, v1, mask)
+ m1 = self.flash(
+ qk1, qk0, v0, mask.transpose(-1, -2) if mask is not None else None
+ )
+ else:
+ qk0, qk1 = qk0 * self.scale**0.5, qk1 * self.scale**0.5
+ sim = torch.einsum("bhid, bhjd -> bhij", qk0, qk1)
+ if mask is not None:
+ sim = sim.masked_fill(~mask, -float("inf"))
+ attn01 = F.softmax(sim, dim=-1)
+ attn10 = F.softmax(sim.transpose(-2, -1).contiguous(), dim=-1)
+ m0 = torch.einsum("bhij, bhjd -> bhid", attn01, v1)
+ m1 = torch.einsum("bhji, bhjd -> bhid", attn10.transpose(-2, -1), v0)
+ if mask is not None:
+ m0, m1 = m0.nan_to_num(), m1.nan_to_num()
+ m0, m1 = self.map_(lambda t: t.transpose(1, 2).flatten(start_dim=-2), m0, m1)
+ m0, m1 = self.map_(self.to_out, m0, m1)
+ x0 = x0 + self.ffn(torch.cat([x0, m0], -1))
+ x1 = x1 + self.ffn(torch.cat([x1, m1], -1))
+ return x0, x1
+
+
+class TransformerLayer(nn.Module):
+ def __init__(self, *args, **kwargs):
+ super().__init__()
+ self.self_attn = SelfBlock(*args, **kwargs)
+ self.cross_attn = CrossBlock(*args, **kwargs)
+
+ def forward(
+ self,
+ desc0,
+ desc1,
+ encoding0,
+ encoding1,
+ mask0: Optional[torch.Tensor] = None,
+ mask1: Optional[torch.Tensor] = None,
+ ):
+ if mask0 is not None and mask1 is not None:
+ return self.masked_forward(desc0, desc1, encoding0, encoding1, mask0, mask1)
+ else:
+ desc0 = self.self_attn(desc0, encoding0)
+ desc1 = self.self_attn(desc1, encoding1)
+ return self.cross_attn(desc0, desc1)
+
+ # This part is compiled and allows padding inputs
+ def masked_forward(self, desc0, desc1, encoding0, encoding1, mask0, mask1):
+ mask = mask0 & mask1.transpose(-1, -2)
+ mask0 = mask0 & mask0.transpose(-1, -2)
+ mask1 = mask1 & mask1.transpose(-1, -2)
+ desc0 = self.self_attn(desc0, encoding0, mask0)
+ desc1 = self.self_attn(desc1, encoding1, mask1)
+ return self.cross_attn(desc0, desc1, mask)
+
+
+def sigmoid_log_double_softmax(
+ sim: torch.Tensor, z0: torch.Tensor, z1: torch.Tensor
+) -> torch.Tensor:
+ """create the log assignment matrix from logits and similarity"""
+ b, m, n = sim.shape
+ certainties = F.logsigmoid(z0) + F.logsigmoid(z1).transpose(1, 2)
+ scores0 = F.log_softmax(sim, 2)
+ scores1 = F.log_softmax(sim.transpose(-1, -2).contiguous(), 2).transpose(-1, -2)
+ scores = sim.new_full((b, m + 1, n + 1), 0)
+ scores[:, :m, :n] = scores0 + scores1 + certainties
+ scores[:, :-1, -1] = F.logsigmoid(-z0.squeeze(-1))
+ scores[:, -1, :-1] = F.logsigmoid(-z1.squeeze(-1))
+ return scores
+
+
+class MatchAssignment(nn.Module):
+ def __init__(self, dim: int) -> None:
+ super().__init__()
+ self.dim = dim
+ self.matchability = nn.Linear(dim, 1, bias=True)
+ self.final_proj = nn.Linear(dim, dim, bias=True)
+
+ def forward(self, desc0: torch.Tensor, desc1: torch.Tensor):
+ """build assignment matrix from descriptors"""
+ mdesc0, mdesc1 = self.final_proj(desc0), self.final_proj(desc1)
+ _, _, d = mdesc0.shape
+ mdesc0, mdesc1 = mdesc0 / d**0.25, mdesc1 / d**0.25
+ sim = torch.einsum("bmd,bnd->bmn", mdesc0, mdesc1)
+ z0 = self.matchability(desc0)
+ z1 = self.matchability(desc1)
+ scores = sigmoid_log_double_softmax(sim, z0, z1)
+ return scores, sim
+
+ def get_matchability(self, desc: torch.Tensor):
+ return torch.sigmoid(self.matchability(desc)).squeeze(-1)
+
+
+def filter_matches(scores: torch.Tensor, th: float):
+ """obtain matches from a log assignment matrix [Bx M+1 x N+1]"""
+ max0, max1 = scores[:, :-1, :-1].max(2), scores[:, :-1, :-1].max(1)
+ m0, m1 = max0.indices, max1.indices
+ indices0 = torch.arange(m0.shape[1], device=m0.device)[None]
+ indices1 = torch.arange(m1.shape[1], device=m1.device)[None]
+ mutual0 = indices0 == m1.gather(1, m0)
+ mutual1 = indices1 == m0.gather(1, m1)
+ max0_exp = max0.values.exp()
+ zero = max0_exp.new_tensor(0)
+ mscores0 = torch.where(mutual0, max0_exp, zero)
+ mscores1 = torch.where(mutual1, mscores0.gather(1, m1), zero)
+ valid0 = mutual0 & (mscores0 > th)
+ valid1 = mutual1 & valid0.gather(1, m1)
+ m0 = torch.where(valid0, m0, -1)
+ m1 = torch.where(valid1, m1, -1)
+ return m0, m1, mscores0, mscores1
+
+
+class LightGlue(nn.Module):
+ default_conf = {
+ "name": "lightglue", # just for interfacing
+ "input_dim": 256, # input descriptor dimension (autoselected from weights)
+ "descriptor_dim": 256,
+ "add_scale_ori": False,
+ "n_layers": 9,
+ "num_heads": 4,
+ "flash": True, # enable FlashAttention if available.
+ "mp": False, # enable mixed precision
+ "depth_confidence": 0.95, # early stopping, disable with -1
+ "width_confidence": 0.99, # point pruning, disable with -1
+ "filter_threshold": 0.1, # match threshold
+ "weights": None,
+ }
+
+ # Point pruning involves an overhead (gather).
+ # Therefore, we only activate it if there are enough keypoints.
+ pruning_keypoint_thresholds = {
+ "cpu": -1,
+ "mps": -1,
+ "cuda": 1024,
+ "flash": 1536,
+ }
+
+ required_data_keys = ["image0", "image1"]
+
+ version = "v0.1_arxiv"
+ url = "https://github.com/cvg/LightGlue/releases/download/{}/{}_lightglue.pth"
+
+ features = {
+ "superpoint": {
+ "weights": "superpoint_lightglue",
+ "input_dim": 256,
+ },
+ "disk": {
+ "weights": "disk_lightglue",
+ "input_dim": 128,
+ },
+ "aliked": {
+ "weights": "aliked_lightglue",
+ "input_dim": 128,
+ },
+ "sift": {
+ "weights": "sift_lightglue",
+ "input_dim": 128,
+ "add_scale_ori": True,
+ },
+ "doghardnet": {
+ "weights": "doghardnet_lightglue",
+ "input_dim": 128,
+ "add_scale_ori": True,
+ },
+ }
+
+ def __init__(self, features="superpoint", **conf) -> None:
+ super().__init__()
+ self.conf = conf = SimpleNamespace(**{**self.default_conf, **conf})
+ if features is not None:
+ if features not in self.features:
+ raise ValueError(
+ f"Unsupported features: {features} not in "
+ f"{{{','.join(self.features)}}}"
+ )
+ for k, v in self.features[features].items():
+ setattr(conf, k, v)
+
+ if conf.input_dim != conf.descriptor_dim:
+ self.input_proj = nn.Linear(conf.input_dim, conf.descriptor_dim, bias=True)
+ else:
+ self.input_proj = nn.Identity()
+
+ head_dim = conf.descriptor_dim // conf.num_heads
+ self.posenc = LearnableFourierPositionalEncoding(
+ 2 + 2 * self.conf.add_scale_ori, head_dim, head_dim
+ )
+
+ h, n, d = conf.num_heads, conf.n_layers, conf.descriptor_dim
+
+ self.transformers = nn.ModuleList(
+ [TransformerLayer(d, h, conf.flash) for _ in range(n)]
+ )
+
+ self.log_assignment = nn.ModuleList([MatchAssignment(d) for _ in range(n)])
+ self.token_confidence = nn.ModuleList(
+ [TokenConfidence(d) for _ in range(n - 1)]
+ )
+ self.register_buffer(
+ "confidence_thresholds",
+ torch.Tensor(
+ [self.confidence_threshold(i) for i in range(self.conf.n_layers)]
+ ),
+ )
+
+ state_dict = None
+ if features is not None:
+ fname = f"{conf.weights}_{self.version.replace('.', '-')}.pth"
+ state_dict = torch.hub.load_state_dict_from_url(
+ self.url.format(self.version, features), file_name=fname
+ )
+ self.load_state_dict(state_dict, strict=False)
+ elif conf.weights is not None:
+ path = Path(__file__).parent
+ path = path / "weights/{}.pth".format(self.conf.weights)
+ state_dict = torch.load(str(path), map_location="cpu")
+
+ if state_dict:
+ # rename old state dict entries
+ for i in range(self.conf.n_layers):
+ pattern = f"self_attn.{i}", f"transformers.{i}.self_attn"
+ state_dict = {k.replace(*pattern): v for k, v in state_dict.items()}
+ pattern = f"cross_attn.{i}", f"transformers.{i}.cross_attn"
+ state_dict = {k.replace(*pattern): v for k, v in state_dict.items()}
+ self.load_state_dict(state_dict, strict=False)
+
+ # static lengths LightGlue is compiled for (only used with torch.compile)
+ self.static_lengths = None
+
+ def compile(
+ self, mode="reduce-overhead", static_lengths=[256, 512, 768, 1024, 1280, 1536]
+ ):
+ if self.conf.width_confidence != -1:
+ warnings.warn(
+ "Point pruning is partially disabled for compiled forward.",
+ stacklevel=2,
+ )
+
+ torch._inductor.cudagraph_mark_step_begin()
+ for i in range(self.conf.n_layers):
+ self.transformers[i].masked_forward = torch.compile(
+ self.transformers[i].masked_forward, mode=mode, fullgraph=True
+ )
+
+ self.static_lengths = static_lengths
+
+ def forward(self, data: dict) -> dict:
+ """
+ Match keypoints and descriptors between two images
+
+ Input (dict):
+ image0: dict
+ keypoints: [B x M x 2]
+ descriptors: [B x M x D]
+ image: [B x C x H x W] or image_size: [B x 2]
+ image1: dict
+ keypoints: [B x N x 2]
+ descriptors: [B x N x D]
+ image: [B x C x H x W] or image_size: [B x 2]
+ Output (dict):
+ matches0: [B x M]
+ matching_scores0: [B x M]
+ matches1: [B x N]
+ matching_scores1: [B x N]
+ matches: List[[Si x 2]]
+ scores: List[[Si]]
+ stop: int
+ prune0: [B x M]
+ prune1: [B x N]
+ """
+ with torch.autocast(enabled=self.conf.mp, device_type="cuda"):
+ return self._forward(data)
+
+ def _forward(self, data: dict) -> dict:
+ for key in self.required_data_keys:
+ assert key in data, f"Missing key {key} in data"
+ data0, data1 = data["image0"], data["image1"]
+ kpts0, kpts1 = data0["keypoints"], data1["keypoints"]
+ b, m, _ = kpts0.shape
+ b, n, _ = kpts1.shape
+ device = kpts0.device
+ size0, size1 = data0.get("image_size"), data1.get("image_size")
+ kpts0 = normalize_keypoints(kpts0, size0).clone()
+ kpts1 = normalize_keypoints(kpts1, size1).clone()
+
+ if self.conf.add_scale_ori:
+ kpts0 = torch.cat(
+ [kpts0] + [data0[k].unsqueeze(-1) for k in ("scales", "oris")], -1
+ )
+ kpts1 = torch.cat(
+ [kpts1] + [data1[k].unsqueeze(-1) for k in ("scales", "oris")], -1
+ )
+ desc0 = data0["descriptors"].detach().contiguous()
+ desc1 = data1["descriptors"].detach().contiguous()
+
+ assert desc0.shape[-1] == self.conf.input_dim
+ assert desc1.shape[-1] == self.conf.input_dim
+
+ if torch.is_autocast_enabled():
+ desc0 = desc0.half()
+ desc1 = desc1.half()
+
+ mask0, mask1 = None, None
+ c = max(m, n)
+ do_compile = self.static_lengths and c <= max(self.static_lengths)
+ if do_compile:
+ kn = min([k for k in self.static_lengths if k >= c])
+ desc0, mask0 = pad_to_length(desc0, kn)
+ desc1, mask1 = pad_to_length(desc1, kn)
+ kpts0, _ = pad_to_length(kpts0, kn)
+ kpts1, _ = pad_to_length(kpts1, kn)
+ desc0 = self.input_proj(desc0)
+ desc1 = self.input_proj(desc1)
+ # cache positional embeddings
+ encoding0 = self.posenc(kpts0)
+ encoding1 = self.posenc(kpts1)
+
+ # GNN + final_proj + assignment
+ do_early_stop = self.conf.depth_confidence > 0
+ do_point_pruning = self.conf.width_confidence > 0 and not do_compile
+ pruning_th = self.pruning_min_kpts(device)
+ if do_point_pruning:
+ ind0 = torch.arange(0, m, device=device)[None]
+ ind1 = torch.arange(0, n, device=device)[None]
+ # We store the index of the layer at which pruning is detected.
+ prune0 = torch.ones_like(ind0)
+ prune1 = torch.ones_like(ind1)
+ token0, token1 = None, None
+ for i in range(self.conf.n_layers):
+ if desc0.shape[1] == 0 or desc1.shape[1] == 0: # no keypoints
+ break
+ desc0, desc1 = self.transformers[i](
+ desc0, desc1, encoding0, encoding1, mask0=mask0, mask1=mask1
+ )
+ if i == self.conf.n_layers - 1:
+ continue # no early stopping or adaptive width at last layer
+
+ if do_early_stop:
+ token0, token1 = self.token_confidence[i](desc0, desc1)
+ if self.check_if_stop(token0[..., :m], token1[..., :n], i, m + n):
+ break
+ if do_point_pruning and desc0.shape[-2] > pruning_th:
+ scores0 = self.log_assignment[i].get_matchability(desc0)
+ prunemask0 = self.get_pruning_mask(token0, scores0, i)
+ keep0 = torch.where(prunemask0)[1]
+ ind0 = ind0.index_select(1, keep0)
+ desc0 = desc0.index_select(1, keep0)
+ encoding0 = encoding0.index_select(-2, keep0)
+ prune0[:, ind0] += 1
+ if do_point_pruning and desc1.shape[-2] > pruning_th:
+ scores1 = self.log_assignment[i].get_matchability(desc1)
+ prunemask1 = self.get_pruning_mask(token1, scores1, i)
+ keep1 = torch.where(prunemask1)[1]
+ ind1 = ind1.index_select(1, keep1)
+ desc1 = desc1.index_select(1, keep1)
+ encoding1 = encoding1.index_select(-2, keep1)
+ prune1[:, ind1] += 1
+
+ if desc0.shape[1] == 0 or desc1.shape[1] == 0: # no keypoints
+ m0 = desc0.new_full((b, m), -1, dtype=torch.long)
+ m1 = desc1.new_full((b, n), -1, dtype=torch.long)
+ mscores0 = desc0.new_zeros((b, m))
+ mscores1 = desc1.new_zeros((b, n))
+ matches = desc0.new_empty((b, 0, 2), dtype=torch.long)
+ mscores = desc0.new_empty((b, 0))
+ if not do_point_pruning:
+ prune0 = torch.ones_like(mscores0) * self.conf.n_layers
+ prune1 = torch.ones_like(mscores1) * self.conf.n_layers
+ return {
+ "matches0": m0,
+ "matches1": m1,
+ "matching_scores0": mscores0,
+ "matching_scores1": mscores1,
+ "stop": i + 1,
+ "matches": matches,
+ "scores": mscores,
+ "prune0": prune0,
+ "prune1": prune1,
+ }
+
+ desc0, desc1 = desc0[..., :m, :], desc1[..., :n, :] # remove padding
+ scores, _ = self.log_assignment[i](desc0, desc1)
+ m0, m1, mscores0, mscores1 = filter_matches(scores, self.conf.filter_threshold)
+ matches, mscores = [], []
+ for k in range(b):
+ valid = m0[k] > -1
+ m_indices_0 = torch.where(valid)[0]
+ m_indices_1 = m0[k][valid]
+ if do_point_pruning:
+ m_indices_0 = ind0[k, m_indices_0]
+ m_indices_1 = ind1[k, m_indices_1]
+ matches.append(torch.stack([m_indices_0, m_indices_1], -1))
+ mscores.append(mscores0[k][valid])
+
+ # TODO: Remove when hloc switches to the compact format.
+ if do_point_pruning:
+ m0_ = torch.full((b, m), -1, device=m0.device, dtype=m0.dtype)
+ m1_ = torch.full((b, n), -1, device=m1.device, dtype=m1.dtype)
+ m0_[:, ind0] = torch.where(m0 == -1, -1, ind1.gather(1, m0.clamp(min=0)))
+ m1_[:, ind1] = torch.where(m1 == -1, -1, ind0.gather(1, m1.clamp(min=0)))
+ mscores0_ = torch.zeros((b, m), device=mscores0.device)
+ mscores1_ = torch.zeros((b, n), device=mscores1.device)
+ mscores0_[:, ind0] = mscores0
+ mscores1_[:, ind1] = mscores1
+ m0, m1, mscores0, mscores1 = m0_, m1_, mscores0_, mscores1_
+ else:
+ prune0 = torch.ones_like(mscores0) * self.conf.n_layers
+ prune1 = torch.ones_like(mscores1) * self.conf.n_layers
+
+ return {
+ "matches0": m0,
+ "matches1": m1,
+ "matching_scores0": mscores0,
+ "matching_scores1": mscores1,
+ "stop": i + 1,
+ "matches": matches,
+ "scores": mscores,
+ "prune0": prune0,
+ "prune1": prune1,
+ }
+
+ def confidence_threshold(self, layer_index: int) -> float:
+ """scaled confidence threshold"""
+ threshold = 0.8 + 0.1 * np.exp(-4.0 * layer_index / self.conf.n_layers)
+ return np.clip(threshold, 0, 1)
+
+ def get_pruning_mask(
+ self, confidences: torch.Tensor, scores: torch.Tensor, layer_index: int
+ ) -> torch.Tensor:
+ """mask points which should be removed"""
+ keep = scores > (1 - self.conf.width_confidence)
+ if confidences is not None: # Low-confidence points are never pruned.
+ keep |= confidences <= self.confidence_thresholds[layer_index]
+ return keep
+
+ def check_if_stop(
+ self,
+ confidences0: torch.Tensor,
+ confidences1: torch.Tensor,
+ layer_index: int,
+ num_points: int,
+ ) -> torch.Tensor:
+ """evaluate stopping condition"""
+ confidences = torch.cat([confidences0, confidences1], -1)
+ threshold = self.confidence_thresholds[layer_index]
+ ratio_confident = 1.0 - (confidences < threshold).float().sum() / num_points
+ return ratio_confident > self.conf.depth_confidence
+
+ def pruning_min_kpts(self, device: torch.device):
+ if self.conf.flash and FLASH_AVAILABLE and device.type == "cuda":
+ return self.pruning_keypoint_thresholds["flash"]
+ else:
+ return self.pruning_keypoint_thresholds[device.type]
diff --git a/imcui/third_party/dad/dad/detectors/third_party/lightglue/sift.py b/imcui/third_party/dad/dad/detectors/third_party/lightglue/sift.py
new file mode 100644
index 0000000000000000000000000000000000000000..d172e7444743f400ddb45944dd72e74eb19944ce
--- /dev/null
+++ b/imcui/third_party/dad/dad/detectors/third_party/lightglue/sift.py
@@ -0,0 +1,216 @@
+import warnings
+
+import cv2
+import numpy as np
+import torch
+from kornia.color import rgb_to_grayscale
+from packaging import version
+
+try:
+ import pycolmap
+except ImportError:
+ pycolmap = None
+
+from .utils import Extractor
+
+
+def filter_dog_point(points, scales, angles, image_shape, nms_radius, scores=None):
+ h, w = image_shape
+ ij = np.round(points - 0.5).astype(int).T[::-1]
+
+ # Remove duplicate points (identical coordinates).
+ # Pick highest scale or score
+ s = scales if scores is None else scores
+ buffer = np.zeros((h, w))
+ np.maximum.at(buffer, tuple(ij), s)
+ keep = np.where(buffer[tuple(ij)] == s)[0]
+
+ # Pick lowest angle (arbitrary).
+ ij = ij[:, keep]
+ buffer[:] = np.inf
+ o_abs = np.abs(angles[keep])
+ np.minimum.at(buffer, tuple(ij), o_abs)
+ mask = buffer[tuple(ij)] == o_abs
+ ij = ij[:, mask]
+ keep = keep[mask]
+
+ if nms_radius > 0:
+ # Apply NMS on the remaining points
+ buffer[:] = 0
+ buffer[tuple(ij)] = s[keep] # scores or scale
+
+ local_max = torch.nn.functional.max_pool2d(
+ torch.from_numpy(buffer).unsqueeze(0),
+ kernel_size=nms_radius * 2 + 1,
+ stride=1,
+ padding=nms_radius,
+ ).squeeze(0)
+ is_local_max = buffer == local_max.numpy()
+ keep = keep[is_local_max[tuple(ij)]]
+ return keep
+
+
+def sift_to_rootsift(x: torch.Tensor, eps=1e-6) -> torch.Tensor:
+ x = torch.nn.functional.normalize(x, p=1, dim=-1, eps=eps)
+ x.clip_(min=eps).sqrt_()
+ return torch.nn.functional.normalize(x, p=2, dim=-1, eps=eps)
+
+
+def run_opencv_sift(features: cv2.Feature2D, image: np.ndarray) -> np.ndarray:
+ """
+ Detect keypoints using OpenCV Detector.
+ Optionally, perform description.
+ Args:
+ features: OpenCV based keypoints detector and descriptor
+ image: Grayscale image of uint8 data type
+ Returns:
+ keypoints: 1D array of detected cv2.KeyPoint
+ scores: 1D array of responses
+ descriptors: 1D array of descriptors
+ """
+ detections, descriptors = features.detectAndCompute(image, None)
+ points = np.array([k.pt for k in detections], dtype=np.float32)
+ scores = np.array([k.response for k in detections], dtype=np.float32)
+ scales = np.array([k.size for k in detections], dtype=np.float32)
+ angles = np.deg2rad(np.array([k.angle for k in detections], dtype=np.float32))
+ return points, scores, scales, angles, descriptors
+
+
+class SIFT(Extractor):
+ default_conf = {
+ "rootsift": True,
+ "nms_radius": 0, # None to disable filtering entirely.
+ "max_num_keypoints": 4096,
+ "backend": "opencv", # in {opencv, pycolmap, pycolmap_cpu, pycolmap_cuda}
+ "detection_threshold": 0.0066667, # from COLMAP
+ "edge_threshold": 10,
+ "first_octave": -1, # only used by pycolmap, the default of COLMAP
+ "num_octaves": 4,
+ }
+
+ preprocess_conf = {
+ "resize": 1024,
+ }
+
+ required_data_keys = ["image"]
+
+ def __init__(self, **conf):
+ super().__init__(**conf) # Update with default configuration.
+ backend = self.conf.backend
+ if backend.startswith("pycolmap"):
+ if pycolmap is None:
+ raise ImportError(
+ "Cannot find module pycolmap: install it with pip"
+ "or use backend=opencv."
+ )
+ options = {
+ "peak_threshold": self.conf.detection_threshold,
+ "edge_threshold": self.conf.edge_threshold,
+ "first_octave": self.conf.first_octave,
+ "num_octaves": self.conf.num_octaves,
+ "normalization": pycolmap.Normalization.L2, # L1_ROOT is buggy.
+ }
+ device = (
+ "auto" if backend == "pycolmap" else backend.replace("pycolmap_", "")
+ )
+ if (
+ backend == "pycolmap_cpu" or not pycolmap.has_cuda
+ ) and pycolmap.__version__ < "0.5.0":
+ warnings.warn(
+ "The pycolmap CPU SIFT is buggy in version < 0.5.0, "
+ "consider upgrading pycolmap or use the CUDA version.",
+ stacklevel=1,
+ )
+ else:
+ options["max_num_features"] = self.conf.max_num_keypoints
+ self.sift = pycolmap.Sift(options=options, device=device)
+ elif backend == "opencv":
+ self.sift = cv2.SIFT_create(
+ contrastThreshold=self.conf.detection_threshold,
+ nfeatures=self.conf.max_num_keypoints,
+ edgeThreshold=self.conf.edge_threshold,
+ nOctaveLayers=self.conf.num_octaves,
+ )
+ else:
+ backends = {"opencv", "pycolmap", "pycolmap_cpu", "pycolmap_cuda"}
+ raise ValueError(
+ f"Unknown backend: {backend} not in {{{','.join(backends)}}}."
+ )
+
+ def extract_single_image(self, image: torch.Tensor):
+ image_np = image.cpu().numpy().squeeze(0)
+
+ if self.conf.backend.startswith("pycolmap"):
+ if version.parse(pycolmap.__version__) >= version.parse("0.5.0"):
+ detections, descriptors = self.sift.extract(image_np)
+ scores = None # Scores are not exposed by COLMAP anymore.
+ else:
+ detections, scores, descriptors = self.sift.extract(image_np)
+ keypoints = detections[:, :2] # Keep only (x, y).
+ scales, angles = detections[:, -2:].T
+ if scores is not None and (
+ self.conf.backend == "pycolmap_cpu" or not pycolmap.has_cuda
+ ):
+ # Set the scores as a combination of abs. response and scale.
+ scores = np.abs(scores) * scales
+ elif self.conf.backend == "opencv":
+ # TODO: Check if opencv keypoints are already in corner convention
+ keypoints, scores, scales, angles, descriptors = run_opencv_sift(
+ self.sift, (image_np * 255.0).astype(np.uint8)
+ )
+ pred = {
+ "keypoints": keypoints,
+ "scales": scales,
+ "oris": angles,
+ "descriptors": descriptors,
+ }
+ if scores is not None:
+ pred["keypoint_scores"] = scores
+
+ # sometimes pycolmap returns points outside the image. We remove them
+ if self.conf.backend.startswith("pycolmap"):
+ is_inside = (
+ pred["keypoints"] + 0.5 < np.array([image_np.shape[-2:][::-1]])
+ ).all(-1)
+ pred = {k: v[is_inside] for k, v in pred.items()}
+
+ if self.conf.nms_radius is not None:
+ keep = filter_dog_point(
+ pred["keypoints"],
+ pred["scales"],
+ pred["oris"],
+ image_np.shape,
+ self.conf.nms_radius,
+ scores=pred.get("keypoint_scores"),
+ )
+ pred = {k: v[keep] for k, v in pred.items()}
+
+ pred = {k: torch.from_numpy(v) for k, v in pred.items()}
+ if scores is not None:
+ # Keep the k keypoints with highest score
+ num_points = self.conf.max_num_keypoints
+ if num_points is not None and len(pred["keypoints"]) > num_points:
+ indices = torch.topk(pred["keypoint_scores"], num_points).indices
+ pred = {k: v[indices] for k, v in pred.items()}
+
+ return pred
+
+ def forward(self, data: dict) -> dict:
+ image = data["image"]
+ if image.shape[1] == 3:
+ image = rgb_to_grayscale(image)
+ device = image.device
+ image = image.cpu()
+ pred = []
+ for k in range(len(image)):
+ img = image[k]
+ if "image_size" in data.keys():
+ # avoid extracting points in padded areas
+ w, h = data["image_size"][k]
+ img = img[:, :h, :w]
+ p = self.extract_single_image(img)
+ pred.append(p)
+ pred = {k: torch.stack([p[k] for p in pred], 0).to(device) for k in pred[0]}
+ if self.conf.rootsift:
+ pred["descriptors"] = sift_to_rootsift(pred["descriptors"])
+ return pred
diff --git a/imcui/third_party/dad/dad/detectors/third_party/lightglue/superpoint.py b/imcui/third_party/dad/dad/detectors/third_party/lightglue/superpoint.py
new file mode 100644
index 0000000000000000000000000000000000000000..221b693e3aaa10d806d221f54bad0d00f7758686
--- /dev/null
+++ b/imcui/third_party/dad/dad/detectors/third_party/lightglue/superpoint.py
@@ -0,0 +1,233 @@
+# %BANNER_BEGIN%
+# ---------------------------------------------------------------------
+# %COPYRIGHT_BEGIN%
+#
+# Magic Leap, Inc. ("COMPANY") CONFIDENTIAL
+#
+# Unpublished Copyright (c) 2020
+# Magic Leap, Inc., All Rights Reserved.
+#
+# NOTICE: All information contained herein is, and remains the property
+# of COMPANY. The intellectual and technical concepts contained herein
+# are proprietary to COMPANY and may be covered by U.S. and Foreign
+# Patents, patents in process, and are protected by trade secret or
+# copyright law. Dissemination of this information or reproduction of
+# this material is strictly forbidden unless prior written permission is
+# obtained from COMPANY. Access to the source code contained herein is
+# hereby forbidden to anyone except current COMPANY employees, managers
+# or contractors who have executed Confidentiality and Non-disclosure
+# agreements explicitly covering such access.
+#
+# The copyright notice above does not evidence any actual or intended
+# publication or disclosure of this source code, which includes
+# information that is confidential and/or proprietary, and is a trade
+# secret, of COMPANY. ANY REPRODUCTION, MODIFICATION, DISTRIBUTION,
+# PUBLIC PERFORMANCE, OR PUBLIC DISPLAY OF OR THROUGH USE OF THIS
+# SOURCE CODE WITHOUT THE EXPRESS WRITTEN CONSENT OF COMPANY IS
+# STRICTLY PROHIBITED, AND IN VIOLATION OF APPLICABLE LAWS AND
+# INTERNATIONAL TREATIES. THE RECEIPT OR POSSESSION OF THIS SOURCE
+# CODE AND/OR RELATED INFORMATION DOES NOT CONVEY OR IMPLY ANY RIGHTS
+# TO REPRODUCE, DISCLOSE OR DISTRIBUTE ITS CONTENTS, OR TO MANUFACTURE,
+# USE, OR SELL ANYTHING THAT IT MAY DESCRIBE, IN WHOLE OR IN PART.
+#
+# %COPYRIGHT_END%
+# ----------------------------------------------------------------------
+# %AUTHORS_BEGIN%
+#
+# Originating Authors: Paul-Edouard Sarlin
+#
+# %AUTHORS_END%
+# --------------------------------------------------------------------*/
+# %BANNER_END%
+
+# Adapted by Remi Pautrat, Philipp Lindenberger
+
+import torch
+from kornia.color import rgb_to_grayscale
+from torch import nn
+
+from .utils import Extractor
+
+
+def simple_nms(scores, nms_radius: int):
+ """Fast Non-maximum suppression to remove nearby points"""
+ assert nms_radius >= 0
+
+ def max_pool(x):
+ return torch.nn.functional.max_pool2d(
+ x, kernel_size=nms_radius * 2 + 1, stride=1, padding=nms_radius
+ )
+
+ zeros = torch.zeros_like(scores)
+ max_mask = scores == max_pool(scores)
+ for _ in range(2):
+ supp_mask = max_pool(max_mask.float()) > 0
+ supp_scores = torch.where(supp_mask, zeros, scores)
+ new_max_mask = supp_scores == max_pool(supp_scores)
+ max_mask = max_mask | (new_max_mask & (~supp_mask))
+ return torch.where(max_mask, scores, zeros)
+
+
+def top_k_keypoints(keypoints, scores, k):
+ if k >= len(keypoints):
+ return keypoints, scores
+ scores, indices = torch.topk(scores, k, dim=0, sorted=True)
+ return keypoints[indices], scores
+
+
+def sample_descriptors(keypoints, descriptors, s: int = 8):
+ """Interpolate descriptors at keypoint locations"""
+ b, c, h, w = descriptors.shape
+ keypoints = keypoints - s / 2 + 0.5
+ keypoints /= torch.tensor(
+ [(w * s - s / 2 - 0.5), (h * s - s / 2 - 0.5)],
+ ).to(keypoints)[None]
+ keypoints = keypoints * 2 - 1 # normalize to (-1, 1)
+ args = {"align_corners": True} if torch.__version__ >= "1.3" else {}
+ descriptors = torch.nn.functional.grid_sample(
+ descriptors, keypoints.view(b, 1, -1, 2), mode="bilinear", **args
+ )
+ descriptors = torch.nn.functional.normalize(
+ descriptors.reshape(b, c, -1), p=2, dim=1
+ )
+ return descriptors
+
+
+class SuperPoint(Extractor):
+ """SuperPoint Convolutional Detector and Descriptor
+
+ SuperPoint: Self-Supervised Interest Point Detection and
+ Description. Daniel DeTone, Tomasz Malisiewicz, and Andrew
+ Rabinovich. In CVPRW, 2019. https://arxiv.org/abs/1712.07629
+
+ """
+
+ default_conf = {
+ "descriptor_dim": 256,
+ "nms_radius": 4,
+ "max_num_keypoints": None,
+ # TODO: detection threshold
+ "detection_threshold": 0.0005,
+ "remove_borders": 4,
+ }
+
+ preprocess_conf = {
+ "resize": 1024,
+ }
+
+ required_data_keys = ["image"]
+
+ def __init__(self, **conf):
+ super().__init__(**conf) # Update with default configuration.
+ self.relu = nn.ReLU(inplace=True)
+ self.pool = nn.MaxPool2d(kernel_size=2, stride=2)
+ c1, c2, c3, c4, c5 = 64, 64, 128, 128, 256
+
+ self.conv1a = nn.Conv2d(1, c1, kernel_size=3, stride=1, padding=1)
+ self.conv1b = nn.Conv2d(c1, c1, kernel_size=3, stride=1, padding=1)
+ self.conv2a = nn.Conv2d(c1, c2, kernel_size=3, stride=1, padding=1)
+ self.conv2b = nn.Conv2d(c2, c2, kernel_size=3, stride=1, padding=1)
+ self.conv3a = nn.Conv2d(c2, c3, kernel_size=3, stride=1, padding=1)
+ self.conv3b = nn.Conv2d(c3, c3, kernel_size=3, stride=1, padding=1)
+ self.conv4a = nn.Conv2d(c3, c4, kernel_size=3, stride=1, padding=1)
+ self.conv4b = nn.Conv2d(c4, c4, kernel_size=3, stride=1, padding=1)
+
+ self.convPa = nn.Conv2d(c4, c5, kernel_size=3, stride=1, padding=1)
+ self.convPb = nn.Conv2d(c5, 65, kernel_size=1, stride=1, padding=0)
+
+ self.convDa = nn.Conv2d(c4, c5, kernel_size=3, stride=1, padding=1)
+ self.convDb = nn.Conv2d(
+ c5, self.conf.descriptor_dim, kernel_size=1, stride=1, padding=0
+ )
+
+ url = "https://github.com/cvg/LightGlue/releases/download/v0.1_arxiv/superpoint_v1.pth" # noqa
+ self.load_state_dict(torch.hub.load_state_dict_from_url(url))
+
+ if self.conf.max_num_keypoints is not None and self.conf.max_num_keypoints <= 0:
+ raise ValueError("max_num_keypoints must be positive or None")
+
+ def forward(self, data: dict) -> dict:
+ """Compute keypoints, scores, descriptors for image"""
+ for key in self.required_data_keys:
+ assert key in data, f"Missing key {key} in data"
+ image = data["image"]
+ if image.shape[1] == 3:
+ image = rgb_to_grayscale(image)
+
+ # Shared Encoder
+ x = self.relu(self.conv1a(image))
+ x = self.relu(self.conv1b(x))
+ x = self.pool(x)
+ x = self.relu(self.conv2a(x))
+ x = self.relu(self.conv2b(x))
+ x = self.pool(x)
+ x = self.relu(self.conv3a(x))
+ x = self.relu(self.conv3b(x))
+ x = self.pool(x)
+ x = self.relu(self.conv4a(x))
+ x = self.relu(self.conv4b(x))
+
+ # Compute the dense keypoint scores
+ cPa = self.relu(self.convPa(x))
+ scores = self.convPb(cPa)
+ scores = torch.nn.functional.softmax(scores, 1)[:, :-1]
+ b, _, h, w = scores.shape
+ scores = scores.permute(0, 2, 3, 1).reshape(b, h, w, 8, 8)
+ scores = scores.permute(0, 1, 3, 2, 4).reshape(b, h * 8, w * 8)
+ scores = simple_nms(scores, self.conf.nms_radius)
+
+ # Discard keypoints near the image borders
+ if self.conf.remove_borders:
+ pad = self.conf.remove_borders
+ scores[:, :pad] = -1
+ scores[:, :, :pad] = -1
+ scores[:, -pad:] = -1
+ scores[:, :, -pad:] = -1
+
+ # Extract keypoints
+ best_kp = torch.where(scores > self.conf.detection_threshold)
+ scores = scores[best_kp]
+
+ # Separate into batches
+ keypoints = [
+ torch.stack(best_kp[1:3], dim=-1)[best_kp[0] == i] for i in range(b)
+ ]
+ scores = [scores[best_kp[0] == i] for i in range(b)]
+
+ # Keep the k keypoints with highest score
+ if self.conf.max_num_keypoints is not None:
+ keypoints, scores = list(
+ zip(
+ *[
+ top_k_keypoints(k, s, self.conf.max_num_keypoints)
+ for k, s in zip(keypoints, scores)
+ ]
+ )
+ )
+
+ # Convert (h, w) to (x, y)
+ keypoints = [torch.flip(k, [1]).float() for k in keypoints]
+
+ # Compute the dense descriptors
+ cDa = self.relu(self.convDa(x))
+ descriptors = self.convDb(cDa)
+ descriptors = torch.nn.functional.normalize(descriptors, p=2, dim=1)
+
+ # Extract descriptors
+ descriptors = [
+ sample_descriptors(k[None], d[None], 8)[0]
+ for k, d in zip(keypoints, descriptors)
+ ]
+
+ return {
+ "keypoints": torch.stack(keypoints, 0),
+ "keypoint_scores": torch.stack(scores, 0),
+ "descriptors": torch.stack(descriptors, 0).transpose(-1, -2).contiguous(),
+ }
+
+
+class ReinforcedFP(SuperPoint):
+ def __init__(self, **conf):
+ super().__init__(**conf) # Update with default configuration.
+ url = "https://github.com/aritrabhowmik/Reinforced-Feature-Points/raw/refs/heads/master/weights/baseline_mixed_loss.pth" # noqa
+ self.load_state_dict(torch.hub.load_state_dict_from_url(url))
diff --git a/imcui/third_party/dad/dad/detectors/third_party/lightglue/utils.py b/imcui/third_party/dad/dad/detectors/third_party/lightglue/utils.py
new file mode 100644
index 0000000000000000000000000000000000000000..7e774e94953fba0f47ec1d2f69aee213f8677148
--- /dev/null
+++ b/imcui/third_party/dad/dad/detectors/third_party/lightglue/utils.py
@@ -0,0 +1,158 @@
+import collections.abc as collections
+from pathlib import Path
+from types import SimpleNamespace
+from typing import Callable, List, Optional, Tuple, Union
+
+import cv2
+import kornia
+import numpy as np
+import torch
+
+
+class ImagePreprocessor:
+ default_conf = {
+ "resize": None, # target edge length, None for no resizing
+ "side": "long",
+ "interpolation": "bilinear",
+ "align_corners": None,
+ "antialias": True,
+ }
+
+ def __init__(self, **conf) -> None:
+ super().__init__()
+ self.conf = {**self.default_conf, **conf}
+ self.conf = SimpleNamespace(**self.conf)
+
+ def __call__(self, img: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]:
+ """Resize and preprocess an image, return image and resize scale"""
+ h, w = img.shape[-2:]
+ if self.conf.resize is not None:
+ img = kornia.geometry.transform.resize(
+ img,
+ self.conf.resize,
+ side=self.conf.side,
+ antialias=self.conf.antialias,
+ align_corners=self.conf.align_corners,
+ )
+ scale = torch.Tensor([img.shape[-1] / w, img.shape[-2] / h]).to(img)
+ return img, scale
+
+
+def map_tensor(input_, func: Callable):
+ string_classes = (str, bytes)
+ if isinstance(input_, string_classes):
+ return input_
+ elif isinstance(input_, collections.Mapping):
+ return {k: map_tensor(sample, func) for k, sample in input_.items()}
+ elif isinstance(input_, collections.Sequence):
+ return [map_tensor(sample, func) for sample in input_]
+ elif isinstance(input_, torch.Tensor):
+ return func(input_)
+ else:
+ return input_
+
+
+def batch_to_device(batch: dict, device: str = "cpu", non_blocking: bool = True):
+ """Move batch (dict) to device"""
+
+ def _func(tensor):
+ return tensor.to(device=device, non_blocking=non_blocking).detach()
+
+ return map_tensor(batch, _func)
+
+
+def rbd(data: dict) -> dict:
+ """Remove batch dimension from elements in data"""
+ return {
+ k: v[0] if isinstance(v, (torch.Tensor, np.ndarray, list)) else v
+ for k, v in data.items()
+ }
+
+
+def numpy_image_to_torch(image: np.ndarray) -> torch.Tensor:
+ """Normalize the image tensor and reorder the dimensions."""
+ if image.ndim == 3:
+ image = image.transpose((2, 0, 1)) # HxWxC to CxHxW
+ elif image.ndim == 2:
+ image = image[None] # add channel axis
+ else:
+ raise ValueError(f"Not an image: {image.shape}")
+ return torch.tensor(image / 255.0, dtype=torch.float)
+
+
+def resize_image(
+ image: np.ndarray,
+ size: Union[List[int], int],
+ fn: str = "max",
+ interp: Optional[str] = "area",
+) -> np.ndarray:
+ """Resize an image to a fixed size, or according to max or min edge."""
+ h, w = image.shape[:2]
+
+ fn = {"max": max, "min": min}[fn]
+ if isinstance(size, int):
+ scale = size / fn(h, w)
+ h_new, w_new = int(round(h * scale)), int(round(w * scale))
+ scale = (w_new / w, h_new / h)
+ elif isinstance(size, (tuple, list)):
+ h_new, w_new = size
+ scale = (w_new / w, h_new / h)
+ else:
+ raise ValueError(f"Incorrect new size: {size}")
+ mode = {
+ "linear": cv2.INTER_LINEAR,
+ "cubic": cv2.INTER_CUBIC,
+ "nearest": cv2.INTER_NEAREST,
+ "area": cv2.INTER_AREA,
+ }[interp]
+ return cv2.resize(image, (w_new, h_new), interpolation=mode), scale
+
+
+def load_image(path: Path, resize: int = None, **kwargs) -> torch.Tensor:
+ if not Path(path).exists():
+ raise FileNotFoundError(f"No image at path {path}.")
+ mode = cv2.IMREAD_COLOR
+ image = cv2.imread(str(path), mode)
+ if image is None:
+ raise IOError(f"Could not read image at {path}.")
+ image = image[..., ::-1]
+ if resize is not None:
+ image, _ = resize_image(image, resize, **kwargs)
+ return numpy_image_to_torch(image)
+
+
+class Extractor(torch.nn.Module):
+ def __init__(self, **conf):
+ super().__init__()
+ self.conf = SimpleNamespace(**{**self.default_conf, **conf})
+
+ @torch.no_grad()
+ def extract(self, img: torch.Tensor, **conf) -> dict:
+ """Perform extraction with online resizing"""
+ if img.dim() == 3:
+ img = img[None] # add batch dim
+ assert img.dim() == 4 and img.shape[0] == 1
+ shape = img.shape[-2:][::-1]
+ img, scales = ImagePreprocessor(**{**self.preprocess_conf, **conf})(img)
+ feats = self.forward({"image": img})
+ feats["image_size"] = torch.tensor(shape)[None].to(img).float()
+ feats["keypoints"] = (feats["keypoints"] + 0.5) / scales[None] - 0.5
+ return feats
+
+
+def match_pair(
+ extractor,
+ matcher,
+ image0: torch.Tensor,
+ image1: torch.Tensor,
+ device: str = "cpu",
+ **preprocess,
+):
+ """Match a pair of images (image0, image1) with an extractor and matcher"""
+ feats0 = extractor.extract(image0, **preprocess)
+ feats1 = extractor.extract(image1, **preprocess)
+ matches01 = matcher({"image0": feats0, "image1": feats1})
+ data = [feats0, feats1, matches01]
+ # remove batch dim and move to target device
+ feats0, feats1, matches01 = [batch_to_device(rbd(x), device) for x in data]
+ return feats0, feats1, matches01
diff --git a/imcui/third_party/dad/dad/detectors/third_party/lightglue_detector.py b/imcui/third_party/dad/dad/detectors/third_party/lightglue_detector.py
new file mode 100644
index 0000000000000000000000000000000000000000..68134089b0eb25779227065ef78221c6d7c3375f
--- /dev/null
+++ b/imcui/third_party/dad/dad/detectors/third_party/lightglue_detector.py
@@ -0,0 +1,42 @@
+from pathlib import Path
+from typing import Union
+import torch
+from .lightglue.utils import load_image
+from dad.utils import (
+ get_best_device,
+)
+from dad.types import Detector
+
+
+class LightGlueDetector(Detector):
+ def __init__(self, model, resize=None, **kwargs):
+ super().__init__()
+ self.model = model(**kwargs).eval().to(get_best_device())
+ if resize is not None:
+ self.model.preprocess_conf["resize"] = resize
+
+ @property
+ def topleft(self):
+ return 0.0
+
+ def load_image(self, im_path: Union[str, Path]):
+ return {"image": load_image(im_path).to(get_best_device())}
+
+ @torch.inference_mode()
+ def detect(
+ self,
+ batch: dict[str, torch.Tensor],
+ *,
+ num_keypoints: int,
+ return_dense_probs: bool = False,
+ ):
+ image = batch["image"]
+ self.model.conf.max_num_keypoints = num_keypoints
+ ret = self.model.extract(image)
+ kpts = self.to_normalized_coords(
+ ret["keypoints"], ret["image_size"][0, 1], ret["image_size"][0, 0]
+ )
+ result = {"keypoints": kpts, "keypoint_probs": None}
+ if return_dense_probs:
+ result["dense_probs"] = ret["dense_probs"] if "dense_probs" in ret else None
+ return result
diff --git a/imcui/third_party/dad/dad/detectors/third_party/rekd/config.py b/imcui/third_party/dad/dad/detectors/third_party/rekd/config.py
new file mode 100644
index 0000000000000000000000000000000000000000..a831c3eaedd203223ef917ec6c61ff89ae38e954
--- /dev/null
+++ b/imcui/third_party/dad/dad/detectors/third_party/rekd/config.py
@@ -0,0 +1,206 @@
+import argparse
+
+## for fix seed
+import random
+import torch
+import numpy
+
+
+def get_config(jupyter=False):
+ parser = argparse.ArgumentParser(description="Train REKD Architecture")
+
+ ## basic configuration
+ parser.add_argument(
+ "--data_dir",
+ type=str,
+ default="../ImageNet2012/ILSVRC2012_img_val", # default='path-to-ImageNet',
+ help="The root path to the data from which the synthetic dataset will be created.",
+ )
+ parser.add_argument(
+ "--synth_dir",
+ type=str,
+ default="",
+ help="The path to save the generated sythetic image pairs.",
+ )
+ parser.add_argument(
+ "--log_dir",
+ type=str,
+ default="trained_models/weights",
+ help="The path to save the REKD weights.",
+ )
+ parser.add_argument(
+ "--load_dir",
+ type=str,
+ default="",
+ help="Set saved model parameters if resume training is desired.",
+ )
+ parser.add_argument(
+ "--exp_name",
+ type=str,
+ default="REKD",
+ help="The Rotaton-equivaraiant Keypoint Detection (REKD) experiment name",
+ )
+ ## network architecture
+ parser.add_argument(
+ "--factor_scaling_pyramid",
+ type=float,
+ default=1.2,
+ help="The scale factor between the multi-scale pyramid levels in the architecture.",
+ )
+ parser.add_argument(
+ "--group_size",
+ type=int,
+ default=36,
+ help="The number of groups for the group convolution.",
+ )
+ parser.add_argument(
+ "--dim_first",
+ type=int,
+ default=2,
+ help="The number of channels of the first layer",
+ )
+ parser.add_argument(
+ "--dim_second",
+ type=int,
+ default=2,
+ help="The number of channels of the second layer",
+ )
+ parser.add_argument(
+ "--dim_third",
+ type=int,
+ default=2,
+ help="The number of channels of the thrid layer",
+ )
+ ## network training
+ parser.add_argument(
+ "--batch_size", type=int, default=16, help="The batch size for training."
+ )
+ parser.add_argument(
+ "--num_epochs", type=int, default=20, help="Number of epochs for training."
+ )
+ ## Loss function
+ parser.add_argument(
+ "--init_initial_learning_rate",
+ type=float,
+ default=1e-3,
+ help="The init initial learning rate value.",
+ )
+ parser.add_argument(
+ "--MSIP_sizes", type=str, default="8,16,24,32,40", help="MSIP sizes."
+ )
+ parser.add_argument(
+ "--MSIP_factor_loss",
+ type=str,
+ default="256.0,64.0,16.0,4.0,1.0",
+ help="MSIP loss balancing parameters.",
+ )
+ parser.add_argument("--ori_loss_balance", type=float, default=100.0, help="")
+ ## Dataset generation
+ parser.add_argument(
+ "--patch_size",
+ type=int,
+ default=192,
+ help="The patch size of the generated dataset.",
+ )
+ parser.add_argument(
+ "--max_angle",
+ type=int,
+ default=180,
+ help="The max angle value for generating a synthetic view to train REKD.",
+ )
+ parser.add_argument(
+ "--min_scale",
+ type=float,
+ default=1.0,
+ help="The min scale value for generating a synthetic view to train REKD.",
+ )
+ parser.add_argument(
+ "--max_scale",
+ type=float,
+ default=1.0,
+ help="The max scale value for generating a synthetic view to train REKD.",
+ )
+ parser.add_argument(
+ "--max_shearing",
+ type=float,
+ default=0.0,
+ help="The max shearing value for generating a synthetic view to train REKD.",
+ )
+ parser.add_argument(
+ "--num_training_data",
+ type=int,
+ default=9000,
+ help="The number of the generated dataset.",
+ )
+ parser.add_argument(
+ "--is_debugging",
+ type=bool,
+ default=False,
+ help="Set variable to True if you desire to train network on a smaller dataset.",
+ )
+ ## For eval/inference
+ parser.add_argument(
+ "--num_points",
+ type=int,
+ default=1500,
+ help="the number of points at evaluation time.",
+ )
+ parser.add_argument(
+ "--pyramid_levels", type=int, default=5, help="downsampling pyramid levels."
+ )
+ parser.add_argument(
+ "--upsampled_levels", type=int, default=2, help="upsampling image levels."
+ )
+ parser.add_argument(
+ "--nms_size",
+ type=int,
+ default=15,
+ help="The NMS size for computing the validation repeatability.",
+ )
+ parser.add_argument(
+ "--border_size",
+ type=int,
+ default=15,
+ help="The number of pixels to remove from the borders to compute the repeatability.",
+ )
+ ## For HPatches evaluation
+ parser.add_argument(
+ "--hpatches_path",
+ type=str,
+ default="./datasets/hpatches-sequences-release",
+ help="dataset ",
+ )
+ parser.add_argument(
+ "--eval_split",
+ type=str,
+ default="debug",
+ help="debug, view, illum, full, debug_view, debug_illum ...",
+ )
+ parser.add_argument(
+ "--descriptor", type=str, default="hardnet", help="hardnet, sosnet, hynet"
+ )
+
+ args, weird_args = (
+ parser.parse_known_args() if not jupyter else parser.parse_args(args=[])
+ )
+
+ fix_randseed(12345)
+
+ if args.synth_dir == "":
+ args.synth_dir = "datasets/synth_data"
+
+ args.MSIP_sizes = [int(i) for i in args.MSIP_sizes.split(",")]
+ args.MSIP_factor_loss = [float(i) for i in args.MSIP_factor_loss.split(",")]
+
+ return args
+
+
+def fix_randseed(randseed):
+ r"""Fix random seed"""
+ random.seed(randseed)
+ numpy.random.seed(randseed)
+ torch.manual_seed(randseed)
+ torch.cuda.manual_seed(randseed)
+ torch.cuda.manual_seed_all(randseed)
+ torch.backends.cudnn.benchmark, torch.backends.cudnn.deterministic = False, True
+ # torch.backends.cudnn.benchmark, torch.backends.cudnn.deterministic = True, False
diff --git a/imcui/third_party/dad/dad/detectors/third_party/rekd/geometry_tools.py b/imcui/third_party/dad/dad/detectors/third_party/rekd/geometry_tools.py
new file mode 100644
index 0000000000000000000000000000000000000000..daec436e40ec0941c24111e427dd11e89dd89f26
--- /dev/null
+++ b/imcui/third_party/dad/dad/detectors/third_party/rekd/geometry_tools.py
@@ -0,0 +1,204 @@
+from cv2 import warpPerspective as applyH
+import numpy as np
+import torch
+
+
+def apply_nms(score_map, size):
+ from scipy.ndimage.filters import maximum_filter
+
+ score_map = score_map * (
+ score_map == maximum_filter(score_map, footprint=np.ones((size, size)))
+ )
+ return score_map
+
+
+def remove_borders(images, borders):
+ ## input [B,C,H,W]
+ shape = images.shape
+
+ if len(shape) == 4:
+ for batch_id in range(shape[0]):
+ images[batch_id, :, 0:borders, :] = 0
+ images[batch_id, :, :, 0:borders] = 0
+ images[batch_id, :, shape[2] - borders : shape[2], :] = 0
+ images[batch_id, :, :, shape[3] - borders : shape[3]] = 0
+ elif len(shape) == 2:
+ images[0:borders, :] = 0
+ images[:, 0:borders] = 0
+ images[shape[0] - borders : shape[0], :] = 0
+ images[:, shape[1] - borders : shape[1]] = 0
+ else:
+ print("Not implemented")
+ exit()
+
+ return images
+
+
+def create_common_region_masks(h_dst_2_src, shape_src, shape_dst):
+ # Create mask. Only take into account pixels in the two images
+ inv_h = np.linalg.inv(h_dst_2_src)
+ inv_h = inv_h / inv_h[2, 2]
+
+ # Applies mask to destination. Where there is no 1, we can no find a point in source.
+ ones_dst = np.ones((shape_dst[0], shape_dst[1]))
+ ones_dst = remove_borders(ones_dst, borders=15)
+ mask_src = applyH(ones_dst, h_dst_2_src, (shape_src[1], shape_src[0]))
+ mask_src = np.where(mask_src >= 0.75, 1.0, 0.0)
+ mask_src = remove_borders(mask_src, borders=15)
+
+ ones_src = np.ones((shape_src[0], shape_src[1]))
+ ones_src = remove_borders(ones_src, borders=15)
+ mask_dst = applyH(ones_src, inv_h, (shape_dst[1], shape_dst[0]))
+ mask_dst = np.where(mask_dst >= 0.75, 1.0, 0.0)
+ mask_dst = remove_borders(mask_dst, borders=15)
+
+ return mask_src, mask_dst
+
+
+def prepare_homography(hom):
+ if len(hom.shape) == 1:
+ h = np.zeros((3, 3))
+ for j in range(3):
+ for i in range(3):
+ if j == 2 and i == 2:
+ h[j, i] = 1.0
+ else:
+ h[j, i] = hom[j * 3 + i]
+ elif len(hom.shape) == 2: ## batch
+ ones = torch.ones(hom.shape[0]).unsqueeze(1)
+ h = torch.cat([hom, ones], dim=1).reshape(-1, 3, 3).type(torch.float32)
+
+ return h
+
+
+def getAff(x, y, H):
+ h11 = H[0, 0]
+ h12 = H[0, 1]
+ h13 = H[0, 2]
+ h21 = H[1, 0]
+ h22 = H[1, 1]
+ h23 = H[1, 2]
+ h31 = H[2, 0]
+ h32 = H[2, 1]
+ h33 = H[2, 2]
+ fxdx = (
+ h11 / (h31 * x + h32 * y + h33)
+ - (h11 * x + h12 * y + h13) * h31 / (h31 * x + h32 * y + h33) ** 2
+ )
+ fxdy = (
+ h12 / (h31 * x + h32 * y + h33)
+ - (h11 * x + h12 * y + h13) * h32 / (h31 * x + h32 * y + h33) ** 2
+ )
+
+ fydx = (
+ h21 / (h31 * x + h32 * y + h33)
+ - (h21 * x + h22 * y + h23) * h31 / (h31 * x + h32 * y + h33) ** 2
+ )
+ fydy = (
+ h22 / (h31 * x + h32 * y + h33)
+ - (h21 * x + h22 * y + h23) * h32 / (h31 * x + h32 * y + h33) ** 2
+ )
+
+ Aff = [[fxdx, fxdy], [fydx, fydy]]
+
+ return np.asarray(Aff)
+
+
+def apply_homography_to_points(points, h):
+ new_points = []
+
+ for point in points:
+ new_point = h.dot([point[0], point[1], 1.0])
+
+ tmp = point[2] ** 2 + np.finfo(np.float32).eps
+
+ Mi1 = [[1 / tmp, 0], [0, 1 / tmp]]
+ Mi1_inv = np.linalg.inv(Mi1)
+ Aff = getAff(point[0], point[1], h)
+
+ BMB = np.linalg.inv(np.dot(Aff, np.dot(Mi1_inv, np.matrix.transpose(Aff))))
+
+ [e, _] = np.linalg.eig(BMB)
+ new_radious = 1 / ((e[0] * e[1]) ** 0.5) ** 0.5
+
+ new_point = [
+ new_point[0] / new_point[2],
+ new_point[1] / new_point[2],
+ new_radious,
+ point[3],
+ ]
+ new_points.append(new_point)
+
+ return np.asarray(new_points)
+
+
+def find_index_higher_scores(map, num_points=1000, threshold=-1):
+ # Best n points
+ if threshold == -1:
+ flatten = map.flatten()
+ order_array = np.sort(flatten)
+
+ order_array = np.flip(order_array, axis=0)
+
+ if order_array.shape[0] < num_points:
+ num_points = order_array.shape[0]
+
+ threshold = order_array[num_points - 1]
+
+ if threshold <= 0.0:
+ ### This is the problem case which derive smaller number of keypoints than the argument "num_points".
+ indexes = np.argwhere(order_array > 0.0)
+
+ if len(indexes) == 0:
+ threshold = 0.0
+ else:
+ threshold = order_array[indexes[len(indexes) - 1]]
+
+ indexes = np.argwhere(map >= threshold)
+
+ return indexes[:num_points]
+
+
+def get_point_coordinates(
+ map, scale_value=1.0, num_points=1000, threshold=-1, order_coord="xysr"
+):
+ ## input numpy array score map : [H, W]
+ indexes = find_index_higher_scores(map, num_points=num_points, threshold=threshold)
+ new_indexes = []
+ for ind in indexes:
+ scores = map[ind[0], ind[1]]
+ if order_coord == "xysr":
+ tmp = [ind[1], ind[0], scale_value, scores]
+ elif order_coord == "yxsr":
+ tmp = [ind[0], ind[1], scale_value, scores]
+
+ new_indexes.append(tmp)
+
+ indexes = np.asarray(new_indexes)
+
+ return np.asarray(indexes)
+
+
+def get_point_coordinates3D(
+ map,
+ scale_factor=1.0,
+ up_levels=0,
+ num_points=1000,
+ threshold=-1,
+ order_coord="xysr",
+):
+ indexes = find_index_higher_scores(map, num_points=num_points, threshold=threshold)
+ new_indexes = []
+ for ind in indexes:
+ scale_value = scale_factor ** (ind[2] - up_levels)
+ scores = map[ind[0], ind[1], ind[2]]
+ if order_coord == "xysr":
+ tmp = [ind[1], ind[0], scale_value, scores]
+ elif order_coord == "yxsr":
+ tmp = [ind[0], ind[1], scale_value, scores]
+
+ new_indexes.append(tmp)
+
+ indexes = np.asarray(new_indexes)
+
+ return np.asarray(indexes)
diff --git a/imcui/third_party/dad/dad/detectors/third_party/rekd/model/REKD.py b/imcui/third_party/dad/dad/detectors/third_party/rekd/model/REKD.py
new file mode 100644
index 0000000000000000000000000000000000000000..40b9db6c4ad4874e35c7db3319b5a7f401d9bbd2
--- /dev/null
+++ b/imcui/third_party/dad/dad/detectors/third_party/rekd/model/REKD.py
@@ -0,0 +1,234 @@
+import torch
+import torch.nn.functional as F
+
+
+from .kernels import gaussian_multiple_channels
+
+
+class REKD(torch.nn.Module):
+ def __init__(self, args, device):
+ super(REKD, self).__init__()
+ from e2cnn import gspaces
+ from e2cnn import nn
+
+ self.pyramid_levels = 3
+ self.factor_scaling = args.factor_scaling_pyramid
+
+ # Smooth Gausian Filter
+ num_channels = 1 ## gray scale image
+ self.gaussian_avg = gaussian_multiple_channels(num_channels, 1.5)
+
+ r2_act = gspaces.Rot2dOnR2(N=args.group_size)
+
+ self.feat_type_in = nn.FieldType(
+ r2_act, num_channels * [r2_act.trivial_repr]
+ ) ## input 1 channels (gray scale image)
+
+ feat_type_out1 = nn.FieldType(r2_act, args.dim_first * [r2_act.regular_repr])
+ feat_type_out2 = nn.FieldType(r2_act, args.dim_second * [r2_act.regular_repr])
+ feat_type_out3 = nn.FieldType(r2_act, args.dim_third * [r2_act.regular_repr])
+
+ feat_type_ori_est = nn.FieldType(r2_act, [r2_act.regular_repr])
+
+ self.block1 = nn.SequentialModule(
+ nn.R2Conv(
+ self.feat_type_in, feat_type_out1, kernel_size=5, padding=2, bias=False
+ ),
+ nn.InnerBatchNorm(feat_type_out1),
+ nn.ReLU(feat_type_out1, inplace=True),
+ )
+ self.block2 = nn.SequentialModule(
+ nn.R2Conv(
+ feat_type_out1, feat_type_out2, kernel_size=5, padding=2, bias=False
+ ),
+ nn.InnerBatchNorm(feat_type_out2),
+ nn.ReLU(feat_type_out2, inplace=True),
+ )
+ self.block3 = nn.SequentialModule(
+ nn.R2Conv(
+ feat_type_out2, feat_type_out3, kernel_size=5, padding=2, bias=False
+ ),
+ nn.InnerBatchNorm(feat_type_out3),
+ nn.ReLU(feat_type_out3, inplace=True),
+ )
+
+ self.ori_learner = nn.SequentialModule(
+ nn.R2Conv(
+ feat_type_out3, feat_type_ori_est, kernel_size=1, padding=0, bias=False
+ ) ## Channel pooling by 8*G -> 1*G conv.
+ )
+ self.softmax = torch.nn.Softmax(dim=1)
+
+ self.gpool = nn.GroupPooling(feat_type_out3)
+ self.last_layer_learner = torch.nn.Sequential(
+ torch.nn.BatchNorm2d(num_features=args.dim_third * self.pyramid_levels),
+ torch.nn.Conv2d(
+ in_channels=args.dim_third * self.pyramid_levels,
+ out_channels=1,
+ kernel_size=1,
+ bias=True,
+ ),
+ torch.nn.ReLU(inplace=True), ## clamp to make the scores positive values.
+ )
+
+ self.dim_third = args.dim_third
+ self.group_size = args.group_size
+ self.exported = False
+
+ def export(self):
+ from e2cnn import nn
+
+ for name, module in dict(self.named_modules()).copy().items():
+ if isinstance(module, nn.EquivariantModule):
+ # print(name, "--->", module)
+ module = module.export()
+ setattr(self, name, module)
+
+ self.exported = True
+
+ def forward(self, input_data):
+ features_key, features_o = self.compute_features(input_data)
+
+ return features_key, features_o
+
+ def compute_features(self, input_data):
+ B, _, H, W = input_data.shape
+
+ for idx_level in range(self.pyramid_levels):
+ with torch.no_grad():
+ input_data_resized = self._resize_input_image(
+ input_data, idx_level, H, W
+ )
+
+ if H > 2500 or W > 2500:
+ features_t, features_o = self._forwarding_networks_divide_grid(
+ input_data_resized
+ )
+ else:
+ features_t, features_o = self._forwarding_networks(input_data_resized)
+
+ features_t = F.interpolate(
+ features_t, size=(H, W), align_corners=True, mode="bilinear"
+ )
+ features_o = F.interpolate(
+ features_o, size=(H, W), align_corners=True, mode="bilinear"
+ )
+
+ if idx_level == 0:
+ features_key = features_t
+ features_ori = features_o
+ else:
+ features_key = torch.cat([features_key, features_t], axis=1)
+ features_ori = torch.add(features_ori, features_o)
+
+ features_key = self.last_layer_learner(features_key)
+ features_ori = self.softmax(features_ori)
+
+ return features_key, features_ori
+
+ def _forwarding_networks(self, input_data_resized):
+ from e2cnn import nn
+
+ # wrap the input tensor in a GeometricTensor (associate it with the input type)
+ features_t = (
+ nn.GeometricTensor(input_data_resized, self.feat_type_in)
+ if not self.exported
+ else input_data_resized
+ )
+
+ ## Geometric tensor feed forwarding
+ features_t = self.block1(features_t)
+ features_t = self.block2(features_t)
+ features_t = self.block3(features_t)
+
+ ## orientation pooling
+ features_o = self.ori_learner(features_t) ## self.cpool
+ features_o = features_o.tensor if not self.exported else features_o
+
+ ## keypoint pooling
+ features_t = self.gpool(features_t)
+ features_t = features_t.tensor if not self.exported else features_t
+
+ return features_t, features_o
+
+ def _forwarding_networks_divide_grid(self, input_data_resized):
+ ## for inference time high resolution image. # spatial grid 4
+ B, _, H_resized, W_resized = input_data_resized.shape
+ features_t = torch.zeros(B, self.dim_third, H_resized, W_resized).cuda()
+ features_o = torch.zeros(B, self.group_size, H_resized, W_resized).cuda()
+ h_divide = 2
+ w_divide = 2
+ for idx in range(h_divide):
+ for jdx in range(w_divide):
+ ## compute the start and end spatial index
+ h_start = H_resized // h_divide * idx
+ w_start = W_resized // w_divide * jdx
+ h_end = H_resized // h_divide * (idx + 1)
+ w_end = W_resized // w_divide * (jdx + 1)
+ ## crop the input image
+ input_data_divided = input_data_resized[
+ :, :, h_start:h_end, w_start:w_end
+ ]
+ features_t_temp, features_o_temp = self._forwarding_networks(
+ input_data_divided
+ )
+ ## take into the values.
+ features_t[:, :, h_start:h_end, w_start:w_end] = features_t_temp
+ features_o[:, :, h_start:h_end, w_start:w_end] = features_o_temp
+
+ return features_t, features_o
+
+ def _resize_input_image(self, input_data, idx_level, H, W):
+ if idx_level == 0:
+ input_data_smooth = input_data
+ else:
+ ## (7,7) size gaussian kernel.
+ input_data_smooth = F.conv2d(
+ input_data, self.gaussian_avg.to(input_data.device), padding=[3, 3]
+ )
+
+ target_resize = (
+ int(H / (self.factor_scaling**idx_level)),
+ int(W / (self.factor_scaling**idx_level)),
+ )
+
+ input_data_resized = F.interpolate(
+ input_data_smooth, size=target_resize, align_corners=True, mode="bilinear"
+ )
+
+ input_data_resized = self.local_norm_image(input_data_resized)
+
+ return input_data_resized
+
+ def local_norm_image(self, x, k_size=65, eps=1e-10):
+ pad = int(k_size / 2)
+
+ x_pad = F.pad(x, (pad, pad, pad, pad), mode="reflect")
+ x_mean = F.avg_pool2d(
+ x_pad, kernel_size=[k_size, k_size], stride=[1, 1], padding=0
+ ) ## padding='valid'==0
+ x2_mean = F.avg_pool2d(
+ torch.pow(x_pad, 2.0),
+ kernel_size=[k_size, k_size],
+ stride=[1, 1],
+ padding=0,
+ )
+
+ x_std = torch.sqrt(torch.abs(x2_mean - x_mean * x_mean)) + eps
+ x_norm = (x - x_mean) / (1.0 + x_std)
+
+ return x_norm
+
+
+def count_model_parameters(model):
+ ## Count the number of learnable parameters.
+ print("================ List of Learnable model parameters ================ ")
+ for n, p in model.named_parameters():
+ if p.requires_grad:
+ print("{} {}".format(n, p.data.shape))
+ else:
+ print("\n\n\n None learnable params {} {}".format(n, p.data.shape))
+ model_parameters = filter(lambda p: p.requires_grad, model.parameters())
+ params = sum([torch.prod(torch.tensor(p.size())) for p in model_parameters])
+ print("The number of learnable parameters : {} ".format(params.data))
+ print("==================================================================== ")
diff --git a/imcui/third_party/dad/dad/detectors/third_party/rekd/model/kernels.py b/imcui/third_party/dad/dad/detectors/third_party/rekd/model/kernels.py
new file mode 100644
index 0000000000000000000000000000000000000000..55f4be1072f658c70c6c06d8723050c0ec15776b
--- /dev/null
+++ b/imcui/third_party/dad/dad/detectors/third_party/rekd/model/kernels.py
@@ -0,0 +1,118 @@
+import math
+import torch
+
+
+def gaussian_multiple_channels(num_channels, sigma):
+ r = 2 * sigma
+ size = 2 * r + 1
+ size = int(math.ceil(size))
+ x = torch.arange(0, size, 1, dtype=torch.float)
+ y = x.unsqueeze(1)
+ x0 = y0 = r
+
+ gaussian = torch.exp(-1 * (((x - x0) ** 2 + (y - y0) ** 2) / (2 * (sigma**2)))) / (
+ (2 * math.pi * (sigma**2)) ** 0.5
+ )
+ gaussian = gaussian.to(dtype=torch.float32)
+
+ weights = torch.zeros((num_channels, num_channels, size, size), dtype=torch.float32)
+ for i in range(num_channels):
+ weights[i, i, :, :] = gaussian
+
+ return weights
+
+
+def ones_multiple_channels(size, num_channels):
+ ones = torch.ones((size, size))
+ weights = torch.zeros((num_channels, num_channels, size, size), dtype=torch.float32)
+
+ for i in range(num_channels):
+ weights[i, i, :, :] = ones
+
+ return weights
+
+
+def grid_indexes(size):
+ weights = torch.zeros((2, 1, size, size), dtype=torch.float32)
+
+ columns = []
+ for idx in range(1, 1 + size):
+ columns.append(torch.ones((size)) * idx)
+ columns = torch.stack(columns)
+
+ rows = []
+ for idx in range(1, 1 + size):
+ rows.append(torch.tensor(range(1, 1 + size)))
+ rows = torch.stack(rows)
+
+ weights[0, 0, :, :] = columns
+ weights[1, 0, :, :] = rows
+
+ return weights
+
+
+def get_kernel_size(factor):
+ """
+ Find the kernel size given the desired factor of upsampling.
+ """
+ return 2 * factor - factor % 2
+
+
+def linear_upsample_weights(half_factor, number_of_classes):
+ """
+ Create weights matrix for transposed convolution with linear filter
+ initialization.
+ """
+
+ filter_size = get_kernel_size(half_factor)
+
+ weights = torch.zeros(
+ (
+ number_of_classes,
+ number_of_classes,
+ filter_size,
+ filter_size,
+ ),
+ dtype=torch.float32,
+ )
+
+ upsample_kernel = torch.ones((filter_size, filter_size))
+ for i in range(number_of_classes):
+ weights[i, i, :, :] = upsample_kernel
+
+ return weights
+
+
+class Kernels_custom:
+ def __init__(self, args, MSIP_sizes=[]):
+ self.batch_size = args.batch_size
+ # create_kernels
+ self.kernels = {}
+
+ if MSIP_sizes != []:
+ self.create_kernels(MSIP_sizes)
+
+ if 8 not in MSIP_sizes:
+ self.create_kernels([8])
+
+ def create_kernels(self, MSIP_sizes):
+ # Grid Indexes for MSIP
+ for ksize in MSIP_sizes:
+ ones_kernel = ones_multiple_channels(ksize, 1)
+ indexes_kernel = grid_indexes(ksize)
+ upsample_filter_np = linear_upsample_weights(int(ksize / 2), 1)
+
+ self.ones_kernel = ones_kernel.requires_grad_(False)
+ self.kernels["ones_kernel_" + str(ksize)] = self.ones_kernel
+
+ self.upsample_filter_np = upsample_filter_np.requires_grad_(False)
+ self.kernels["upsample_filter_np_" + str(ksize)] = self.upsample_filter_np
+
+ self.indexes_kernel = indexes_kernel.requires_grad_(False)
+ self.kernels["indexes_kernel_" + str(ksize)] = self.indexes_kernel
+
+ def get_kernels(self, device):
+ kernels = {}
+ for k, v in self.kernels.items():
+ kernels[k] = v.to(device)
+ return kernels
diff --git a/imcui/third_party/dad/dad/detectors/third_party/rekd/model/load_models.py b/imcui/third_party/dad/dad/detectors/third_party/rekd/model/load_models.py
new file mode 100644
index 0000000000000000000000000000000000000000..99fb731ef6e6203f6ac8dc1f2939ee59e16ffd31
--- /dev/null
+++ b/imcui/third_party/dad/dad/detectors/third_party/rekd/model/load_models.py
@@ -0,0 +1,25 @@
+import torch
+from .REKD import REKD
+
+
+def load_detector(args, device):
+ args.group_size, args.dim_first, args.dim_second, args.dim_third = model_parsing(
+ args
+ )
+ model1 = REKD(args, device)
+ model1.load_state_dict(torch.load(args.load_dir, weights_only=True))
+ model1.export()
+ model1.eval()
+ model1.to(device) ## use GPU
+
+ return model1
+
+
+## Load our model
+def model_parsing(args):
+ group_size = args.load_dir.split("_group")[1].split("_")[0]
+ dim_first = args.load_dir.split("_f")[1].split("_")[0]
+ dim_second = args.load_dir.split("_s")[1].split("_")[0]
+ dim_third = args.load_dir.split("_t")[1].split(".log")[0]
+
+ return int(group_size), int(dim_first), int(dim_second), int(dim_third)
diff --git a/imcui/third_party/dad/dad/detectors/third_party/rekd/rekd.py b/imcui/third_party/dad/dad/detectors/third_party/rekd/rekd.py
new file mode 100644
index 0000000000000000000000000000000000000000..b8bdd01d531f8a600cbabf9dfca3a9922543ddf5
--- /dev/null
+++ b/imcui/third_party/dad/dad/detectors/third_party/rekd/rekd.py
@@ -0,0 +1,207 @@
+import torch
+
+from .config import get_config
+from .model.load_models import load_detector
+import cv2
+import numpy as np
+
+from . import geometry_tools as geo_tools
+from dad.utils import get_best_device
+
+from dad.types import Detector
+
+
+def upsample_pyramid(image, upsampled_levels, scale_factor_levels):
+ ## image np.array([C, H, W]), upsampled_levels int
+ up_pyramid = []
+ for j in range(upsampled_levels):
+ factor = scale_factor_levels ** (upsampled_levels - j)
+ up_image = cv2.resize(
+ image.transpose(1, 2, 0),
+ dsize=(0, 0),
+ fx=factor,
+ fy=factor,
+ interpolation=cv2.INTER_LINEAR,
+ )
+ up_pyramid.append(up_image[np.newaxis])
+
+ return up_pyramid
+
+
+class MultiScaleFeatureExtractor(Detector):
+ def __init__(self, args):
+ super().__init__()
+ ## configurations
+ self.default_num_points = args.num_points
+ self.pyramid_levels = args.pyramid_levels
+ self.upsampled_levels = args.upsampled_levels
+ self.resize = None # TODO: should be working with args.resize but not sure
+ self.border_size = args.border_size
+ self.nms_size = args.nms_size
+ self.desc_scale_factor = 2.0
+ self.scale_factor_levels = np.sqrt(2)
+
+ device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
+
+ self.model = load_detector(args, device)
+
+ ## points level define (Image Pyramid level)
+
+ self.levels = self.pyramid_levels + self.upsampled_levels + 1
+ ## GPU
+ self.device = device
+
+ @property
+ def topleft(self):
+ return 0.0
+
+ def load_image(self, path):
+ im = cv2.cvtColor(cv2.imread(path), cv2.COLOR_BGR2GRAY) ## (1, H, W)
+ # Get current dimensions
+ h, w = im.shape
+ if self.resize is not None:
+ # Determine which dimension is longer
+ if h > w:
+ # Height is longer, calculate new width to maintain aspect ratio
+ new_h = self.resize
+ new_w = int(w * (self.resize / h))
+ else:
+ # Width is longer, calculate new height to maintain aspect ratio
+ new_w = self.resize
+ new_h = int(h * (self.resize / w))
+ # Resize the image
+ im = cv2.resize(im, (new_w, new_h))
+ im = im.astype(float)[np.newaxis, :, :] / im.max()
+ return {"image": im}
+
+ @torch.inference_mode()
+ def detect(self, batch, *, num_keypoints, return_dense_probs=False):
+ image = batch["image"]
+ one, H, W = image.shape
+ score_maps, ori_maps = self._compute_score_maps(image)
+ im_pts = self._estimate_keypoint_coordinates(
+ score_maps, num_points=num_keypoints
+ )
+ pixel_coords = im_pts[..., :2]
+ # print(pixel_coords)
+ # maybe_scale = im_pts[...,2]
+ # maybe_score = im_pts[...,3]
+ im_pts_n = (
+ self.to_normalized_coords(torch.from_numpy(pixel_coords)[None], H, W)
+ .to(get_best_device())
+ .float()
+ )
+ result = {"keypoints": im_pts_n}
+ if return_dense_probs:
+ result["scoremap"] = None
+ return result
+
+ def _compute_score_maps(self, image):
+ from skimage.transform import pyramid_gaussian
+
+ pyramid = pyramid_gaussian(
+ image, max_layer=self.pyramid_levels, downscale=self.scale_factor_levels
+ )
+ up_pyramid = upsample_pyramid(
+ image,
+ upsampled_levels=self.upsampled_levels,
+ scale_factor_levels=self.scale_factor_levels,
+ )
+
+ score_maps = {}
+ ori_maps = {}
+ for j, down_image in enumerate(pyramid): ## Pyramid is downsampling images.
+ key_idx = j + 1 + self.upsampled_levels
+ score_maps, ori_maps = self._obtain_feature_maps(
+ down_image, key_idx, score_maps, ori_maps
+ )
+
+ if self.upsampled_levels:
+ for j, up_image in enumerate(
+ up_pyramid
+ ): ## Upsample levels is for upsampling images.
+ key_idx = j + 1
+ score_maps, ori_maps = self._obtain_feature_maps(
+ up_image, key_idx, score_maps, ori_maps
+ )
+
+ return score_maps, ori_maps
+
+ def _obtain_feature_maps(self, im, key_idx, score_maps, ori_maps):
+ im = torch.tensor(im).unsqueeze(0).to(torch.float32).cuda()
+ im_scores, ori_map = self.model(im)
+ im_scores = geo_tools.remove_borders(
+ im_scores[0, 0, :, :].cpu().detach().numpy(), borders=self.border_size
+ )
+
+ score_maps["map_" + str(key_idx)] = im_scores
+ ori_maps["map_" + str(key_idx)] = ori_map
+
+ return score_maps, ori_maps
+
+ def _estimate_keypoint_coordinates(self, score_maps, num_points=None):
+ num_points = num_points if num_points is not None else self.default_num_points
+ point_level = []
+ tmp = 0.0
+ factor_points = self.scale_factor_levels**2
+ for idx_level in range(self.levels):
+ tmp += factor_points ** (-1 * (idx_level - self.upsampled_levels))
+ point_level.append(
+ self.default_num_points
+ * factor_points ** (-1 * (idx_level - self.upsampled_levels))
+ )
+
+ point_level = np.asarray(list(map(lambda x: int(x / tmp) + 1, point_level)))
+
+ im_pts = []
+ for idx_level in range(self.levels):
+ scale_value = self.scale_factor_levels ** (
+ idx_level - self.upsampled_levels
+ )
+ scale_factor = 1.0 / scale_value
+
+ h_scale = np.asarray(
+ [[scale_factor, 0.0, 0.0], [0.0, scale_factor, 0.0], [0.0, 0.0, 1.0]]
+ )
+ h_scale_inv = np.linalg.inv(h_scale)
+ h_scale_inv = h_scale_inv / h_scale_inv[2, 2]
+
+ num_points_level = point_level[idx_level]
+ if idx_level > 0:
+ res_points = int(
+ np.asarray([point_level[a] for a in range(0, idx_level + 1)]).sum()
+ - len(im_pts)
+ )
+ num_points_level = res_points
+
+ ## to make the output score map derive more keypoints
+ score_map = score_maps["map_" + str(idx_level + 1)]
+
+ im_scores = geo_tools.apply_nms(score_map, self.nms_size)
+ im_pts_tmp = geo_tools.get_point_coordinates(
+ im_scores, num_points=num_points_level
+ )
+ im_pts_tmp = geo_tools.apply_homography_to_points(im_pts_tmp, h_scale_inv)
+
+ if not idx_level:
+ im_pts = im_pts_tmp
+ else:
+ im_pts = np.concatenate((im_pts, im_pts_tmp), axis=0)
+
+ im_pts = im_pts[(-1 * im_pts[:, 3]).argsort()]
+ im_pts = im_pts[:num_points]
+
+ return im_pts
+
+ def get_save_feat_dir(self):
+ return self.save_feat_dir
+
+
+def load_REKD(resize=None):
+ args = get_config()
+ args.load_dir = "release_group36_f2_s2_t2.log/best_model.pt"
+ args.resize = resize
+ model = MultiScaleFeatureExtractor(args)
+
+ print("Model paramter : {} is loaded.".format(args.load_dir))
+ return model
diff --git a/imcui/third_party/dad/dad/logging.py b/imcui/third_party/dad/dad/logging.py
new file mode 100644
index 0000000000000000000000000000000000000000..4265b36cd976b8f01c9d850e5a10e8c1c9d81dab
--- /dev/null
+++ b/imcui/third_party/dad/dad/logging.py
@@ -0,0 +1,53 @@
+import logging
+import sys
+from logging.handlers import RotatingFileHandler
+
+# First, create logger for your package
+logger = logging.getLogger("DaD")
+logger.propagate = False # Prevent propagation to avoid double logging
+logger.addHandler(logging.NullHandler()) # Default null handler
+
+
+def configure_logger(
+ level=logging.INFO,
+ log_format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
+ date_format="%Y-%m-%d %H:%M:%S",
+ file_path=None,
+ file_max_bytes=10485760, # 10MB
+ file_backup_count=3,
+ stream=sys.stderr,
+ propagate=False, # Default to False to prevent double logging
+):
+ """
+ Configure the package logger with handlers similar to basicConfig.
+ This does NOT use basicConfig() and only affects this package's logger.
+ """
+ # Clear any existing handlers
+ for handler in logger.handlers[:]:
+ if not isinstance(handler, logging.NullHandler):
+ logger.removeHandler(handler)
+
+ # Set propagation
+ logger.propagate = propagate
+
+ # Set level
+ logger.setLevel(level)
+
+ # Create formatter
+ formatter = logging.Formatter(log_format, date_format)
+
+ # Add console handler if stream is specified
+ if stream:
+ console_handler = logging.StreamHandler(stream)
+ console_handler.setFormatter(formatter)
+ logger.addHandler(console_handler)
+
+ # Add file handler if file path is specified
+ if file_path:
+ file_handler = RotatingFileHandler(
+ file_path, maxBytes=file_max_bytes, backupCount=file_backup_count
+ )
+ file_handler.setFormatter(formatter)
+ logger.addHandler(file_handler)
+
+ return logger
diff --git a/imcui/third_party/dad/dad/loss.py b/imcui/third_party/dad/dad/loss.py
new file mode 100644
index 0000000000000000000000000000000000000000..58d276878bb639bede6cd770444e7c07a7415ab2
--- /dev/null
+++ b/imcui/third_party/dad/dad/loss.py
@@ -0,0 +1,254 @@
+from typing import Callable
+import wandb
+import torch
+import torch.nn as nn
+import torch.nn.functional as F
+
+import dad
+from dad.utils import (
+ get_gt_warp,
+ masked_log_softmax,
+ sample_keypoints,
+ kl_div,
+)
+
+
+class RLLoss(nn.Module):
+ def __init__(
+ self,
+ *,
+ reward_function: Callable[[torch.Tensor], torch.Tensor],
+ smoothing_size: int,
+ sampling_kde_size: int,
+ nms_size: int,
+ num_sparse: int,
+ regularization_loss_weight: float,
+ coverage_pow: float,
+ topk: bool = True,
+ device: str = "cuda",
+ ) -> None:
+ super().__init__()
+ X = torch.linspace(-1, 1, smoothing_size, device=device)
+ G = (-(X**2) / (2 * 1 / 2**2)).exp()
+ G = G / G.sum()
+ self.smoothing_kernel = G[None, None, None, :]
+ self.smoothing_size = smoothing_size
+ self.regularization_loss_weight = regularization_loss_weight
+ self.nms_size = nms_size
+ self.num_sparse = num_sparse
+ self.reward_function = reward_function
+ self.sampling_kde_size = sampling_kde_size
+ self.coverage_pow = coverage_pow
+ self.topk = topk
+
+ def compute_matchability(self, keypoint_p, has_depth, B, K, H, W, device="cuda"):
+ smooth_keypoint_p = F.conv2d(
+ keypoint_p.reshape(B, 1, H, W),
+ weight=self.smoothing_kernel,
+ padding=(self.smoothing_size // 2, 0),
+ )
+ smooth_keypoint_p = F.conv2d(
+ smooth_keypoint_p,
+ weight=self.smoothing_kernel.mT,
+ padding=(0, self.smoothing_size // 2),
+ )
+ log_p_hat = (
+ (smooth_keypoint_p + 1e-8).log().reshape(B, H * W).log_softmax(dim=-1)
+ )
+ smooth_has_depth = F.conv2d(
+ has_depth.reshape(B, 1, H, W),
+ weight=self.smoothing_kernel,
+ padding=(0, self.smoothing_size // 2),
+ )
+ smooth_has_depth = F.conv2d(
+ smooth_has_depth,
+ weight=self.smoothing_kernel.mT,
+ padding=(self.smoothing_size // 2, 0),
+ ).reshape(B, H * W)
+ p = smooth_has_depth / smooth_has_depth.sum(dim=-1, keepdim=True)
+ return kl_div(p, log_p_hat)
+
+ def compute_loss(self, batch, model):
+ outputs = model(batch)
+ keypoint_logits_A, keypoint_logits_B = outputs["scoremap"].chunk(2)
+ B, K, H, W = keypoint_logits_A.shape
+
+ gt_warp_A_to_B, valid_mask_A_to_B = get_gt_warp(
+ batch["im_A_depth"],
+ batch["im_B_depth"],
+ batch["T_1to2"],
+ batch["K1"],
+ batch["K2"],
+ H=H,
+ W=W,
+ )
+ gt_warp_B_to_A, valid_mask_B_to_A = get_gt_warp(
+ batch["im_B_depth"],
+ batch["im_A_depth"],
+ batch["T_1to2"].inverse(),
+ batch["K2"],
+ batch["K1"],
+ H=H,
+ W=W,
+ )
+ keypoint_logits_A = keypoint_logits_A.reshape(B, K, H * W)
+ keypoint_logits_B = keypoint_logits_B.reshape(B, K, H * W)
+ keypoint_logits = torch.cat((keypoint_logits_A, keypoint_logits_B))
+
+ B = 2 * B
+ gt_warp = torch.cat((gt_warp_A_to_B, gt_warp_B_to_A))
+ valid_mask = torch.cat((valid_mask_A_to_B, valid_mask_B_to_A))
+ valid_mask = valid_mask.reshape(B, H * W)
+ keypoint_logits_backwarped = F.grid_sample(
+ torch.cat((keypoint_logits_B, keypoint_logits_A)).reshape(B, K, H, W),
+ gt_warp[..., -2:].reshape(B, H, W, 2).float(),
+ align_corners=False,
+ mode="bicubic",
+ )
+
+ keypoint_logits_backwarped = (keypoint_logits_backwarped).reshape(B, K, H * W)
+
+ depth = F.interpolate(
+ torch.cat(
+ (batch["im_A_depth"][:, None], batch["im_B_depth"][:, None]), dim=0
+ ),
+ size=(H, W),
+ mode="bilinear",
+ align_corners=False,
+ )
+ has_depth = (depth > 0).float().reshape(B, H * W)
+ keypoint_p = (
+ keypoint_logits.reshape(B, K * H * W)
+ .softmax(dim=-1)
+ .reshape(B, K, H * W)
+ .sum(dim=1)
+ )
+ matchability_loss = self.compute_matchability(
+ keypoint_p, has_depth, B, K, H, W
+ ).mean()
+ B = B // 2
+ M = self.num_sparse
+ torch.set_grad_enabled(False)
+ kpts_A = sample_keypoints(
+ keypoint_p[:B].reshape(B, H, W),
+ use_nms=True,
+ nms_size=self.nms_size,
+ sample_topk=self.topk,
+ num_samples=M,
+ coverage_size=self.sampling_kde_size,
+ increase_coverage=True,
+ coverage_pow=self.coverage_pow,
+ subpixel=False,
+ scoremap=keypoint_logits[:B].reshape(B, H, W),
+ )
+ kpts_B = sample_keypoints(
+ keypoint_p[B:].reshape(B, H, W),
+ use_nms=True,
+ nms_size=self.nms_size,
+ sample_topk=self.topk,
+ num_samples=M,
+ coverage_size=self.sampling_kde_size,
+ increase_coverage=True,
+ coverage_pow=self.coverage_pow,
+ subpixel=False,
+ scoremap=keypoint_logits[B:].reshape(B, H, W),
+ )
+ kpts_A_to_B = F.grid_sample(
+ gt_warp_A_to_B[..., 2:].float().permute(0, 3, 1, 2),
+ kpts_A[..., None, :],
+ align_corners=False,
+ mode="bilinear",
+ )[..., 0].mT
+ legit_A_to_B = (
+ F.grid_sample(
+ valid_mask_A_to_B.reshape(B, 1, H, W),
+ kpts_A[..., None, :],
+ align_corners=False,
+ mode="bilinear",
+ )[..., 0, :, 0]
+ > 0
+ )
+ kpts_B_to_A = F.grid_sample(
+ gt_warp_B_to_A[..., 2:].float().permute(0, 3, 1, 2),
+ kpts_B[..., None, :],
+ align_corners=False,
+ mode="bilinear",
+ )[..., 0].mT
+ legit_B_to_A = (
+ F.grid_sample(
+ valid_mask_B_to_A.reshape(B, 1, H, W),
+ kpts_B[..., None, :],
+ align_corners=False,
+ mode="bilinear",
+ )[..., 0, :, 0]
+ > 0
+ )
+ D_A_to_B = torch.cdist(kpts_A_to_B, kpts_B)
+ D_B_to_A = torch.cdist(kpts_B_to_A, kpts_A)
+
+ min_dist_A_to_B = D_A_to_B.amin(dim=-1)
+ min_dist_B_to_A = D_B_to_A.amin(dim=-1)
+ torch.set_grad_enabled(True)
+
+ inlier_threshold = 0.005
+ inliers_A_to_B = min_dist_A_to_B < inlier_threshold
+ percent_inliers_A_to_B = inliers_A_to_B[legit_A_to_B].float().mean()
+ wandb.log(
+ {"mega_percent_inliers": percent_inliers_A_to_B.item()},
+ step=dad.GLOBAL_STEP,
+ )
+
+ reward_A_to_B = self.reward_function(min_dist_A_to_B)
+ reward_B_to_A = self.reward_function(min_dist_B_to_A)
+ sparse_kpt_logits_A = F.grid_sample(
+ keypoint_logits_A.reshape(B, 1, H, W),
+ kpts_A[:, None].detach(),
+ mode="bilinear",
+ align_corners=False,
+ ).reshape(B, M)
+ sparse_kpt_logits_B = F.grid_sample(
+ keypoint_logits_B.reshape(B, 1, H, W),
+ kpts_B[:, None].detach(),
+ mode="bilinear",
+ align_corners=False,
+ ).reshape(B, M)
+ sparse_kpt_log_p_A = masked_log_softmax(sparse_kpt_logits_A, legit_A_to_B)
+ sparse_kpt_log_p_B = masked_log_softmax(sparse_kpt_logits_B, legit_B_to_A)
+
+ tot_loss = 0.0
+ sparse_loss = (
+ -(reward_A_to_B[legit_A_to_B] * sparse_kpt_log_p_A[legit_A_to_B]).sum()
+ - (reward_B_to_A[legit_B_to_A] * sparse_kpt_log_p_B[legit_B_to_A]).sum()
+ )
+ tot_loss = tot_loss + sparse_loss
+ tot_loss = tot_loss + self.regularization_loss_weight * matchability_loss
+ return tot_loss
+
+ def forward(self, batch, model):
+ return self.compute_loss(batch, model)
+
+
+class MaxDistillLoss(nn.Module):
+ def __init__(self, *teachers: list[dad.Detector]):
+ self.teachers = teachers
+
+ def forward(self, batch, student):
+ p_teachers = []
+ with torch.inference_mode():
+ for teacher in self.teachers:
+ scoremap: torch.Tensor = teacher(batch)["scoremap"]
+ B, one, H, W = scoremap.shape
+ p_teachers.append(
+ scoremap.reshape(B, H * W).softmax(dim=1).reshape(B, 1, H, W)
+ )
+ p_max = torch.maximum(*p_teachers).clone()
+ p_max = p_max / p_max.sum(dim=(-2, -1), keepdim=True)
+ scoremap_student = student(batch)
+ scoremap: torch.Tensor = scoremap_student["scoremap"]
+ B, one, H, W = scoremap.shape
+ log_p_model = scoremap.reshape(B, H * W).log_softmax(dim=1).reshape(B, 1, H, W)
+ kl = (
+ -(p_max * log_p_model).sum() / B + (p_max * (p_max + 1e-10).log()).sum() / B
+ )
+ wandb.log({"distill_kl": kl.item()}, step=dad.GLOBAL_STEP)
+ return kl
diff --git a/imcui/third_party/dad/dad/matchers/__init__.py b/imcui/third_party/dad/dad/matchers/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..f5e6baea305c469338f4b92ea0a87ce38d2679d1
--- /dev/null
+++ b/imcui/third_party/dad/dad/matchers/__init__.py
@@ -0,0 +1 @@
+from .roma import load_roma_matcher as load_roma_matcher
diff --git a/imcui/third_party/dad/dad/matchers/roma.py b/imcui/third_party/dad/dad/matchers/roma.py
new file mode 100644
index 0000000000000000000000000000000000000000..b32bd39cbd6ec3d775cb66fd193817b601f0608a
--- /dev/null
+++ b/imcui/third_party/dad/dad/matchers/roma.py
@@ -0,0 +1,9 @@
+from dad.types import Matcher
+
+
+def load_roma_matcher() -> Matcher:
+ from romatch import roma_outdoor
+
+ roma_matcher = roma_outdoor("cuda")
+ roma_matcher.symmetric = False
+ return roma_matcher
diff --git a/imcui/third_party/dad/dad/reward_functions/__init__.py b/imcui/third_party/dad/dad/reward_functions/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..dc8f058fad94ffbade06fbb930fde39023d52a42
--- /dev/null
+++ b/imcui/third_party/dad/dad/reward_functions/__init__.py
@@ -0,0 +1 @@
+from .constant_reward import ConstantReward as ConstantReward
diff --git a/imcui/third_party/dad/dad/reward_functions/constant_reward.py b/imcui/third_party/dad/dad/reward_functions/constant_reward.py
new file mode 100644
index 0000000000000000000000000000000000000000..f2eb0f8656da230455979bad8ce30655c4605fb5
--- /dev/null
+++ b/imcui/third_party/dad/dad/reward_functions/constant_reward.py
@@ -0,0 +1,16 @@
+import torch
+from typing import Optional
+
+
+class ConstantReward:
+ def __init__(self, *, th: float, eps: Optional[float] = 0.01):
+ self.th = th
+ self.eps = eps
+
+ def __call__(self, distances: torch.Tensor):
+ B, K = distances.shape
+ good = distances.detach() < self.th
+ pos_reward = good.float() / (good.float().mean(dim=1, keepdim=True) + self.eps)
+ neg_reward = 0
+ reward = pos_reward * good + neg_reward * good.logical_not()
+ return reward
diff --git a/imcui/third_party/dad/dad/train.py b/imcui/third_party/dad/dad/train.py
new file mode 100644
index 0000000000000000000000000000000000000000..a75109969d82a2f192023905169f236bf677a17d
--- /dev/null
+++ b/imcui/third_party/dad/dad/train.py
@@ -0,0 +1,51 @@
+from typing import Iterable, Callable, Optional
+import torch
+from tqdm import tqdm
+from dad.utils import to_best_device
+import dad
+
+
+def train_step(
+ train_batch: dict[str, torch.Tensor],
+ model: dad.Detector,
+ objective: Callable[[dict, dict], torch.Tensor],
+ optimizer: torch.optim.Optimizer,
+ grad_scaler: Optional[torch.amp.GradScaler] = None,
+):
+ optimizer.zero_grad()
+ loss = objective(train_batch, model)
+ if grad_scaler is not None:
+ grad_scaler.scale(loss).backward()
+ grad_scaler.unscale_(optimizer)
+ torch.nn.utils.clip_grad_norm_(model.parameters(), 0.01)
+ grad_scaler.step(optimizer)
+ grad_scaler.update()
+ else:
+ loss.backward()
+ optimizer.step()
+
+
+def train_k_steps(
+ n_0: int,
+ k: int,
+ dataloader: Iterable[dict[str, torch.Tensor]],
+ model: dad.Detector,
+ objective: Callable[[dict, dict], torch.Tensor],
+ optimizer: torch.optim.Optimizer,
+ lr_scheduler: torch.optim.lr_scheduler.LRScheduler,
+ grad_scaler: Optional[torch.amp.GradScaler] = None,
+ progress_bar: bool = True,
+):
+ for n in tqdm(range(n_0, n_0 + k), disable=not progress_bar, mininterval=10.0):
+ batch = next(dataloader)
+ model.train(True)
+ batch = to_best_device(batch)
+ train_step(
+ train_batch=batch,
+ model=model,
+ objective=objective,
+ optimizer=optimizer,
+ grad_scaler=grad_scaler,
+ )
+ lr_scheduler.step()
+ dad.GLOBAL_STEP += 1
diff --git a/imcui/third_party/dad/dad/types.py b/imcui/third_party/dad/dad/types.py
new file mode 100644
index 0000000000000000000000000000000000000000..c3621f1d28865220beaafc3825e3de5916e192db
--- /dev/null
+++ b/imcui/third_party/dad/dad/types.py
@@ -0,0 +1,155 @@
+from pathlib import Path
+from typing import Optional, Union
+
+import numpy as np
+import torch
+import torch.nn as nn
+from abc import ABC, abstractmethod
+
+
+class Detector(ABC, nn.Module):
+ @property
+ @abstractmethod
+ def topleft(self) -> float:
+ pass
+
+ @abstractmethod
+ def load_image(im_path: Union[str, Path]) -> dict[str, torch.Tensor]:
+ pass
+
+ @abstractmethod
+ def detect(
+ self, batch: dict[str, torch.Tensor], *, num_keypoints, return_dense_probs=False
+ ) -> dict[str, torch.Tensor]:
+ pass
+
+ @torch.inference_mode
+ def detect_from_path(
+ self,
+ im_path: Union[str, Path],
+ *,
+ num_keypoints: int,
+ return_dense_probs: bool = False,
+ ) -> dict[str, torch.Tensor]:
+ return self.detect(
+ self.load_image(im_path),
+ num_keypoints=num_keypoints,
+ return_dense_probs=return_dense_probs,
+ )
+
+ def to_pixel_coords(
+ self, normalized_coords: torch.Tensor, h: int, w: int
+ ) -> torch.Tensor:
+ if normalized_coords.shape[-1] != 2:
+ raise ValueError(
+ f"Expected shape (..., 2), but got {normalized_coords.shape}"
+ )
+ pixel_coords = torch.stack(
+ (
+ w * (normalized_coords[..., 0] + 1) / 2,
+ h * (normalized_coords[..., 1] + 1) / 2,
+ ),
+ axis=-1,
+ )
+ return pixel_coords
+
+ def to_normalized_coords(
+ self, pixel_coords: torch.Tensor, h: int, w: int
+ ) -> torch.Tensor:
+ if pixel_coords.shape[-1] != 2:
+ raise ValueError(f"Expected shape (..., 2), but got {pixel_coords.shape}")
+ normalized_coords = torch.stack(
+ (
+ 2 * (pixel_coords[..., 0]) / w - 1,
+ 2 * (pixel_coords[..., 1]) / h - 1,
+ ),
+ axis=-1,
+ )
+ return normalized_coords
+
+
+class Matcher(ABC, nn.Module):
+ @abstractmethod
+ def match(
+ self, im_A_path: Union[str | Path], im_B_path: Union[str | Path]
+ ) -> tuple[torch.Tensor, torch.Tensor]:
+ pass
+
+ @abstractmethod
+ def match_keypoints(
+ self,
+ keypoints_A: torch.Tensor,
+ keypoints_B: torch.Tensor,
+ warp: torch.Tensor,
+ certainty: torch.Tensor,
+ return_tuple: bool = False,
+ ) -> torch.Tensor:
+ pass
+
+ @abstractmethod
+ def to_pixel_coordinates(
+ self, matches: torch.Tensor, h1: int, w1: int, h2: int, w2: int
+ ) -> tuple[torch.Tensor, torch.Tensor]:
+ pass
+
+
+class Benchmark(ABC):
+ def __init__(
+ self,
+ *,
+ data_root: str,
+ thresholds: list[int],
+ sample_every: int = 1,
+ num_ransac_runs: int = 5,
+ num_keypoints: Optional[list[int] | int] = None,
+ ) -> None:
+ self.num_keypoints = (
+ [512, 1024, 2048, 4096, 8192] if num_keypoints is None else num_keypoints
+ )
+ if isinstance(self.num_keypoints, int):
+ self.num_keypoints = [self.num_keypoints]
+ self.data_root = data_root
+ self.sample_every = sample_every
+ self.num_ransac_runs = num_ransac_runs
+ self.thresholds = thresholds
+
+ @abstractmethod
+ def benchmark(self, *, matcher: Matcher, detector: Detector) -> dict[str, float]:
+ pass
+
+ def pose_auc(self, errors):
+ sort_idx = np.argsort(errors)
+ errors = np.array(errors.copy())[sort_idx]
+ recall = (np.arange(len(errors)) + 1) / len(errors)
+ errors = np.r_[0.0, errors]
+ recall = np.r_[0.0, recall]
+ aucs = []
+ for t in self.thresholds:
+ last_index = np.searchsorted(errors, t)
+ r = np.r_[recall[:last_index], recall[last_index - 1]]
+ e = np.r_[errors[:last_index], t]
+ aucs.append(np.trapz(r, x=e).item() / t)
+ return aucs
+
+ def compute_auc(self, errors: np.ndarray) -> dict[str, float]:
+ # errors.shape = (len(benchmark)*num_keypoints*num_ransac_runs,)
+ errors = (
+ errors.reshape((-1, len(self.num_keypoints), self.num_ransac_runs))
+ .transpose(0, 2, 1)
+ .reshape(-1, len(self.num_keypoints))
+ )
+ results: dict[str, float] = {}
+ for idx in range(len(self.num_keypoints)):
+ aucs = self.pose_auc(errors[:, idx])
+ for auc, th in zip(aucs, self.thresholds):
+ key = (
+ f"{type(self).__name__}_auc_{th}_num_kps_{self.num_keypoints[idx]}"
+ )
+ results[key] = auc
+ return results
+
+ def __call__(self, *, matcher: Matcher, detector: Detector) -> dict[str, float]:
+ return self.benchmark(
+ matcher=matcher,
+ detector=detector,
+ )
diff --git a/imcui/third_party/dad/dad/utils.py b/imcui/third_party/dad/dad/utils.py
new file mode 100644
index 0000000000000000000000000000000000000000..41eb14a58e9acec279b4c08f4bef1572dd12de23
--- /dev/null
+++ b/imcui/third_party/dad/dad/utils.py
@@ -0,0 +1,1059 @@
+import math
+import warnings
+from pathlib import Path
+from typing import Optional
+from dad.types import Benchmark, Detector, Matcher
+import torch.nn as nn
+import cv2
+import numpy as np
+import torch
+import torch.nn.functional as F
+from PIL import Image
+
+
+def get_best_device(verbose=False):
+ device = torch.device("cpu")
+ if torch.cuda.is_available():
+ device = torch.device("cuda")
+ elif torch.backends.mps.is_available():
+ device = torch.device("mps")
+ else:
+ device = torch.device("cpu")
+ if verbose:
+ print(f"Fastest device found is: {device}")
+ return device
+
+
+def recover_pose(E, kpts0, kpts1, K0, K1, mask):
+ best_num_inliers = 0
+ K0inv = np.linalg.inv(K0[:2, :2])
+ K1inv = np.linalg.inv(K1[:2, :2])
+
+ kpts0_n = (K0inv @ (kpts0 - K0[None, :2, 2]).T).T
+ kpts1_n = (K1inv @ (kpts1 - K1[None, :2, 2]).T).T
+
+ for _E in np.split(E, len(E) / 3):
+ n, R, t, _ = cv2.recoverPose(_E, kpts0_n, kpts1_n, np.eye(3), 1e9, mask=mask)
+ if n > best_num_inliers:
+ best_num_inliers = n
+ ret = (R, t, mask.ravel() > 0)
+ return ret
+
+
+# Code taken from https://github.com/PruneTruong/DenseMatching/blob/40c29a6b5c35e86b9509e65ab0cd12553d998e5f/validation/utils_pose_estimation.py
+# --- GEOMETRY ---
+def estimate_pose(kpts0, kpts1, K0, K1, norm_thresh, conf=0.99999):
+ if len(kpts0) < 5:
+ return None
+ K0inv = np.linalg.inv(K0[:2, :2])
+ K1inv = np.linalg.inv(K1[:2, :2])
+
+ kpts0 = (K0inv @ (kpts0 - K0[None, :2, 2]).T).T
+ kpts1 = (K1inv @ (kpts1 - K1[None, :2, 2]).T).T
+ E, mask = cv2.findEssentialMat(
+ kpts0, kpts1, np.eye(3), threshold=norm_thresh, prob=conf
+ )
+
+ ret = None
+ if E is not None:
+ best_num_inliers = 0
+
+ for _E in np.split(E, len(E) / 3):
+ n, R, t, _ = cv2.recoverPose(_E, kpts0, kpts1, np.eye(3), 1e9, mask=mask)
+ if n > best_num_inliers:
+ best_num_inliers = n
+ ret = (R, t, mask.ravel() > 0)
+ return ret
+
+
+def get_grid(B, H, W, device=get_best_device()):
+ x1_n = torch.meshgrid(
+ *[torch.linspace(-1 + 1 / n, 1 - 1 / n, n, device=device) for n in (B, H, W)],
+ indexing="ij",
+ )
+ x1_n = torch.stack((x1_n[2], x1_n[1]), dim=-1).reshape(B, H * W, 2)
+ return x1_n
+
+
+def fast_inv_2x2(matrix, eps=1e-10):
+ return (
+ 1
+ / (torch.linalg.det(matrix)[..., None, None] + eps)
+ * torch.stack(
+ (
+ matrix[..., 1, 1],
+ -matrix[..., 0, 1],
+ -matrix[..., 1, 0],
+ matrix[..., 0, 0],
+ ),
+ dim=-1,
+ ).reshape(*matrix.shape)
+ )
+
+
+def extract_patches_from_inds(x: torch.Tensor, inds: torch.Tensor, patch_size: int):
+ B, H, W = x.shape
+ B, N = inds.shape
+ unfolder = nn.Unfold(kernel_size=patch_size, padding=patch_size // 2, stride=1)
+ unfolded_x: torch.Tensor = unfolder(x[:, None]) # B x K_H * K_W x H * W
+ patches = torch.gather(
+ unfolded_x,
+ dim=2,
+ index=inds[:, None, :].expand(B, patch_size**2, N),
+ ) # B x K_H * K_W x N
+ return patches
+
+
+def extract_patches_from_coords(x: torch.Tensor, coords: torch.Tensor, patch_size: int):
+ # NOTE: we could also do this by just adding extra coords and grid_sampling more
+ # but this is easy, and the results should be similar
+ B, H, W = x.shape
+ B, N, two = coords.shape
+ unfolder = nn.Unfold(kernel_size=patch_size, padding=patch_size // 2, stride=1)
+ unfolded_x: torch.Tensor = unfolder(x[:, None]) # B x K_H * K_W x H * W
+ patches = F.grid_sample(
+ unfolded_x.reshape(B, patch_size**2, H, W),
+ coords[:, None],
+ mode="bilinear",
+ align_corners=False,
+ )[:, 0] # B x K_H * K_W x N
+ return patches
+
+
+def sample_keypoints(
+ keypoint_probs: torch.Tensor,
+ num_samples=8192,
+ device=get_best_device(),
+ use_nms=True,
+ nms_size=1,
+ sample_topk=True,
+ increase_coverage=True,
+ remove_borders=False,
+ return_probs=False,
+ coverage_pow=1 / 2,
+ coverage_size=51,
+ subpixel=False,
+ scoremap=None, # required for subpixel
+ subpixel_temp=0.5,
+):
+ B, H, W = keypoint_probs.shape
+ if increase_coverage:
+ weights = (
+ -(torch.linspace(-2, 2, steps=coverage_size, device=device) ** 2)
+ ).exp()[None, None]
+ # 10000 is just some number for maybe numerical stability, who knows. :), result is invariant anyway
+ local_density_x = F.conv2d(
+ (keypoint_probs[:, None] + 1e-6) * 10000,
+ weights[..., None, :],
+ padding=(0, coverage_size // 2),
+ )
+ local_density = F.conv2d(
+ local_density_x, weights[..., None], padding=(coverage_size // 2, 0)
+ )[:, 0]
+ keypoint_probs = keypoint_probs * (local_density + 1e-8) ** (-coverage_pow)
+ grid = get_grid(B, H, W, device=device).reshape(B, H * W, 2)
+ if use_nms:
+ keypoint_probs = keypoint_probs * (
+ keypoint_probs
+ == F.max_pool2d(keypoint_probs, nms_size, stride=1, padding=nms_size // 2)
+ )
+ if remove_borders:
+ frame = torch.zeros_like(keypoint_probs)
+ # we hardcode 4px, could do it nicer, but whatever
+ frame[..., 4:-4, 4:-4] = 1
+ keypoint_probs = keypoint_probs * frame
+ if sample_topk:
+ inds = torch.topk(keypoint_probs.reshape(B, H * W), k=num_samples).indices
+ else:
+ inds = torch.multinomial(
+ keypoint_probs.reshape(B, H * W), num_samples=num_samples, replacement=False
+ )
+ kps = torch.gather(grid, dim=1, index=inds[..., None].expand(B, num_samples, 2))
+ if subpixel:
+ offsets = get_grid(B, nms_size, nms_size).reshape(
+ B, nms_size**2, 2
+ ) # B x K_H x K_W x 2
+ offsets[..., 0] = offsets[..., 0] * nms_size / W
+ offsets[..., 1] = offsets[..., 1] * nms_size / H
+ keypoint_patch_scores = extract_patches_from_inds(scoremap, inds, nms_size)
+ keypoint_patch_probs = (keypoint_patch_scores / subpixel_temp).softmax(
+ dim=1
+ ) # B x K_H * K_W x N
+ keypoint_offsets = torch.einsum("bkn, bkd ->bnd", keypoint_patch_probs, offsets)
+ kps = kps + keypoint_offsets
+ if return_probs:
+ return kps, torch.gather(keypoint_probs.reshape(B, H * W), dim=1, index=inds)
+ return kps
+
+
+def get_gt_warp(
+ depth1,
+ depth2,
+ T_1to2,
+ K1,
+ K2,
+ depth_interpolation_mode="bilinear",
+ relative_depth_error_threshold=0.05,
+ H=None,
+ W=None,
+) -> tuple[torch.Tensor, torch.Tensor]:
+ if H is None:
+ B, H, W = depth1.shape
+ else:
+ B = depth1.shape[0]
+ with torch.no_grad():
+ x1_n = torch.meshgrid(
+ *[
+ torch.linspace(-1 + 1 / n, 1 - 1 / n, n, device=depth1.device)
+ for n in (B, H, W)
+ ],
+ indexing="ij",
+ )
+ x1_n = torch.stack((x1_n[2], x1_n[1]), dim=-1).reshape(B, H * W, 2)
+ mask, x2 = warp_kpts(
+ x1_n.double(),
+ depth1.double(),
+ depth2.double(),
+ T_1to2.double(),
+ K1.double(),
+ K2.double(),
+ depth_interpolation_mode=depth_interpolation_mode,
+ relative_depth_error_threshold=relative_depth_error_threshold,
+ )
+ prob = mask.float().reshape(B, H, W)
+ x2 = x2.reshape(B, H, W, 2)
+ return torch.cat((x1_n.reshape(B, H, W, 2), x2), dim=-1), prob
+
+
+def unnormalize_coords(x_n, h, w):
+ x = torch.stack(
+ (w * (x_n[..., 0] + 1) / 2, h * (x_n[..., 1] + 1) / 2), dim=-1
+ ) # [-1+1/h, 1-1/h] -> [0.5, h-0.5]
+ return x
+
+
+def normalize_coords(x, h, w):
+ x = torch.stack(
+ (2 * (x[..., 0] / w) - 1, 2 * (x[..., 1] / h) - 1), dim=-1
+ ) # [-1+1/h, 1-1/h] -> [0.5, h-0.5]
+ return x
+
+
+def rotate_intrinsic(K, n):
+ base_rot = np.array([[0, 1, 0], [-1, 0, 0], [0, 0, 1]])
+ rot = np.linalg.matrix_power(base_rot, n)
+ return rot @ K
+
+
+def rotate_pose_inplane(i_T_w, rot):
+ rotation_matrices = [
+ np.array(
+ [
+ [np.cos(r), -np.sin(r), 0.0, 0.0],
+ [np.sin(r), np.cos(r), 0.0, 0.0],
+ [0.0, 0.0, 1.0, 0.0],
+ [0.0, 0.0, 0.0, 1.0],
+ ],
+ dtype=np.float32,
+ )
+ for r in [np.deg2rad(d) for d in (0, 270, 180, 90)]
+ ]
+ return np.dot(rotation_matrices[rot], i_T_w)
+
+
+def scale_intrinsics(K, scales):
+ scales = np.diag([1.0 / scales[0], 1.0 / scales[1], 1.0])
+ return np.dot(scales, K)
+
+
+def angle_error_mat(R1, R2):
+ cos = (np.trace(np.dot(R1.T, R2)) - 1) / 2
+ cos = np.clip(cos, -1.0, 1.0) # numercial errors can make it out of bounds
+ return np.rad2deg(np.abs(np.arccos(cos)))
+
+
+def angle_error_vec(v1, v2):
+ n = np.linalg.norm(v1) * np.linalg.norm(v2)
+ return np.rad2deg(np.arccos(np.clip(np.dot(v1, v2) / n, -1.0, 1.0)))
+
+
+def compute_pose_error(T_0to1, R, t):
+ R_gt = T_0to1[:3, :3]
+ t_gt = T_0to1[:3, 3]
+ error_t = angle_error_vec(t.squeeze(), t_gt)
+ error_t = np.minimum(error_t, 180 - error_t) # ambiguity of E estimation
+ error_R = angle_error_mat(R, R_gt)
+ return error_t, error_R
+
+
+@torch.no_grad()
+def warp_kpts(
+ kpts0,
+ depth0,
+ depth1,
+ T_0to1,
+ K0,
+ K1,
+ smooth_mask=False,
+ return_relative_depth_error=False,
+ depth_interpolation_mode="bilinear",
+ relative_depth_error_threshold=0.05,
+):
+ """Warp kpts0 from I0 to I1 with depth, K and Rt
+ Also check covisibility and depth consistency.
+ Depth is consistent if relative error < 0.2 (hard-coded).
+ # https://github.com/zju3dv/LoFTR/blob/94e98b695be18acb43d5d3250f52226a8e36f839/src/loftr/utils/geometry.py adapted from here
+ Args:
+ kpts0 (torch.Tensor): [N, L, 2] - , should be normalized in (-1,1)
+ depth0 (torch.Tensor): [N, H, W],
+ depth1 (torch.Tensor): [N, H, W],
+ T_0to1 (torch.Tensor): [N, 3, 4],
+ K0 (torch.Tensor): [N, 3, 3],
+ K1 (torch.Tensor): [N, 3, 3],
+ Returns:
+ calculable_mask (torch.Tensor): [N, L]
+ warped_keypoints0 (torch.Tensor): [N, L, 2]
+ """
+ (
+ n,
+ h,
+ w,
+ ) = depth0.shape
+ if depth_interpolation_mode == "combined":
+ # Inspired by approach in inloc, try to fill holes from bilinear interpolation by nearest neighbour interpolation
+ if smooth_mask:
+ raise NotImplementedError("Combined bilinear and NN warp not implemented")
+ valid_bilinear, warp_bilinear = warp_kpts(
+ kpts0,
+ depth0,
+ depth1,
+ T_0to1,
+ K0,
+ K1,
+ smooth_mask=smooth_mask,
+ return_relative_depth_error=return_relative_depth_error,
+ depth_interpolation_mode="bilinear",
+ relative_depth_error_threshold=relative_depth_error_threshold,
+ )
+ valid_nearest, warp_nearest = warp_kpts(
+ kpts0,
+ depth0,
+ depth1,
+ T_0to1,
+ K0,
+ K1,
+ smooth_mask=smooth_mask,
+ return_relative_depth_error=return_relative_depth_error,
+ depth_interpolation_mode="nearest-exact",
+ relative_depth_error_threshold=relative_depth_error_threshold,
+ )
+ nearest_valid_bilinear_invalid = (~valid_bilinear).logical_and(valid_nearest)
+ warp = warp_bilinear.clone()
+ warp[nearest_valid_bilinear_invalid] = warp_nearest[
+ nearest_valid_bilinear_invalid
+ ]
+ valid = valid_bilinear | valid_nearest
+ return valid, warp
+
+ kpts0_depth = F.grid_sample(
+ depth0[:, None],
+ kpts0[:, :, None],
+ mode=depth_interpolation_mode,
+ align_corners=False,
+ )[:, 0, :, 0]
+ kpts0 = torch.stack(
+ (w * (kpts0[..., 0] + 1) / 2, h * (kpts0[..., 1] + 1) / 2), dim=-1
+ ) # [-1+1/h, 1-1/h] -> [0.5, h-0.5]
+ # Sample depth, get calculable_mask on depth != 0
+ nonzero_mask = kpts0_depth != 0
+
+ # Unproject
+ kpts0_h = (
+ torch.cat([kpts0, torch.ones_like(kpts0[:, :, [0]])], dim=-1)
+ * kpts0_depth[..., None]
+ ) # (N, L, 3)
+ kpts0_n = K0.inverse() @ kpts0_h.transpose(2, 1) # (N, 3, L)
+ kpts0_cam = kpts0_n
+
+ # Rigid Transform
+ w_kpts0_cam = T_0to1[:, :3, :3] @ kpts0_cam + T_0to1[:, :3, [3]] # (N, 3, L)
+ w_kpts0_depth_computed = w_kpts0_cam[:, 2, :]
+
+ # Project
+ w_kpts0_h = (K1 @ w_kpts0_cam).transpose(2, 1) # (N, L, 3)
+ w_kpts0 = w_kpts0_h[:, :, :2] / (
+ w_kpts0_h[:, :, [2]] + 1e-4
+ ) # (N, L, 2), +1e-4 to avoid zero depth
+
+ # Covisible Check
+ h, w = depth1.shape[1:3]
+ covisible_mask = (
+ (w_kpts0[:, :, 0] > 0)
+ * (w_kpts0[:, :, 0] < w - 1)
+ * (w_kpts0[:, :, 1] > 0)
+ * (w_kpts0[:, :, 1] < h - 1)
+ )
+ w_kpts0 = torch.stack(
+ (2 * w_kpts0[..., 0] / w - 1, 2 * w_kpts0[..., 1] / h - 1), dim=-1
+ ) # from [0.5,h-0.5] -> [-1+1/h, 1-1/h]
+ # w_kpts0[~covisible_mask, :] = -5 # xd
+
+ w_kpts0_depth = F.grid_sample(
+ depth1[:, None],
+ w_kpts0[:, :, None],
+ mode=depth_interpolation_mode,
+ align_corners=False,
+ )[:, 0, :, 0]
+
+ relative_depth_error = (
+ (w_kpts0_depth - w_kpts0_depth_computed) / w_kpts0_depth
+ ).abs()
+ if not smooth_mask:
+ consistent_mask = relative_depth_error < relative_depth_error_threshold
+ else:
+ consistent_mask = (-relative_depth_error / smooth_mask).exp()
+ valid_mask = nonzero_mask * covisible_mask * consistent_mask
+ if return_relative_depth_error:
+ return relative_depth_error, w_kpts0
+ else:
+ return valid_mask, w_kpts0
+
+
+imagenet_mean = torch.tensor([0.485, 0.456, 0.406])
+imagenet_std = torch.tensor([0.229, 0.224, 0.225])
+
+
+def numpy_to_pil(x: np.ndarray):
+ """
+ Args:
+ x: Assumed to be of shape (h,w,c)
+ """
+ if isinstance(x, torch.Tensor):
+ x = x.detach().cpu().numpy()
+ if x.max() <= 1.01:
+ x *= 255
+ x = x.astype(np.uint8)
+ return Image.fromarray(x)
+
+
+def imgnet_unnormalize(x: torch.Tensor) -> torch.Tensor:
+ return x * (imagenet_std[:, None, None].to(x.device)) + (
+ imagenet_mean[:, None, None].to(x.device)
+ )
+
+
+def imgnet_normalize(x: torch.Tensor) -> torch.Tensor:
+ return (x - imagenet_mean[:, None, None].to(x.device)) / (
+ imagenet_std[:, None, None].to(x.device)
+ )
+
+
+def tensor_to_pil(x, unnormalize=False, autoscale=False):
+ if unnormalize:
+ x = imgnet_unnormalize(x)
+ if autoscale:
+ if x.max() == x.min():
+ warnings.warn("x max == x min, cant autoscale")
+ else:
+ x = (x - x.min()) / (x.max() - x.min())
+
+ x = x.detach()
+ if len(x.shape) > 2:
+ x = x.permute(1, 2, 0)
+ x = x.cpu().numpy()
+ x = np.clip(x, 0.0, 1.0)
+ return numpy_to_pil(x)
+
+
+def to_cuda(batch):
+ for key, value in batch.items():
+ if isinstance(value, torch.Tensor):
+ batch[key] = value.cuda()
+ return batch
+
+
+def to_best_device(batch, device=get_best_device()):
+ for key, value in batch.items():
+ if isinstance(value, torch.Tensor):
+ batch[key] = value.to(device)
+ return batch
+
+
+def to_cpu(batch):
+ for key, value in batch.items():
+ if isinstance(value, torch.Tensor):
+ batch[key] = value.cpu()
+ return batch
+
+
+def get_pose(calib):
+ w, h = np.array(calib["imsize"])[0]
+ return np.array(calib["K"]), np.array(calib["R"]), np.array(calib["T"]).T, h, w
+
+
+def compute_relative_pose(R1, t1, R2, t2):
+ rots = R2 @ (R1.T)
+ trans = -rots @ t1 + t2
+ return rots, trans
+
+
+def to_pixel_coords(normalized_coords, h, w) -> torch.Tensor:
+ if normalized_coords.shape[-1] != 2:
+ raise ValueError(f"Expected shape (..., 2), but got {normalized_coords.shape}")
+ pixel_coords = torch.stack(
+ (
+ w * (normalized_coords[..., 0] + 1) / 2,
+ h * (normalized_coords[..., 1] + 1) / 2,
+ ),
+ axis=-1,
+ )
+ return pixel_coords
+
+
+def to_normalized_coords(pixel_coords, h, w) -> torch.Tensor:
+ if pixel_coords.shape[-1] != 2:
+ raise ValueError(f"Expected shape (..., 2), but got {pixel_coords.shape}")
+ normalized_coords = torch.stack(
+ (
+ 2 * (pixel_coords[..., 0]) / w - 1,
+ 2 * (pixel_coords[..., 1]) / h - 1,
+ ),
+ axis=-1,
+ )
+ return normalized_coords
+
+
+def warp_to_pixel_coords(warp, h1, w1, h2, w2):
+ warp1 = warp[..., :2]
+ warp1 = torch.stack(
+ (
+ w1 * (warp1[..., 0] + 1) / 2,
+ h1 * (warp1[..., 1] + 1) / 2,
+ ),
+ axis=-1,
+ )
+ warp2 = warp[..., 2:]
+ warp2 = torch.stack(
+ (
+ w2 * (warp2[..., 0] + 1) / 2,
+ h2 * (warp2[..., 1] + 1) / 2,
+ ),
+ axis=-1,
+ )
+ return torch.cat((warp1, warp2), dim=-1)
+
+
+def to_homogeneous(x):
+ ones = torch.ones_like(x[..., -1:])
+ return torch.cat((x, ones), dim=-1)
+
+
+to_hom = to_homogeneous # alias
+
+
+def from_homogeneous(xh, eps=1e-12):
+ return xh[..., :-1] / (xh[..., -1:] + eps)
+
+
+from_hom = from_homogeneous # alias
+
+
+def homog_transform(Homog, x):
+ xh = to_homogeneous(x)
+ yh = (Homog @ xh.mT).mT
+ y = from_homogeneous(yh)
+ return y
+
+
+def get_homog_warp(Homog, H, W, device=get_best_device()):
+ grid = torch.meshgrid(
+ torch.linspace(-1 + 1 / H, 1 - 1 / H, H, device=device),
+ torch.linspace(-1 + 1 / W, 1 - 1 / W, W, device=device),
+ indexing="ij",
+ )
+
+ x_A = torch.stack((grid[1], grid[0]), dim=-1)[None]
+ x_A_to_B = homog_transform(Homog, x_A)
+ mask = ((x_A_to_B > -1) * (x_A_to_B < 1)).prod(dim=-1).float()
+ return torch.cat((x_A.expand(*x_A_to_B.shape), x_A_to_B), dim=-1), mask
+
+
+def dual_log_softmax_matcher(desc_A, desc_B, inv_temperature=1, normalize=False):
+ B, N, C = desc_A.shape
+ if normalize:
+ desc_A = desc_A / desc_A.norm(dim=-1, keepdim=True)
+ desc_B = desc_B / desc_B.norm(dim=-1, keepdim=True)
+ corr = torch.einsum("b n c, b m c -> b n m", desc_A, desc_B) * inv_temperature
+ else:
+ corr = torch.einsum("b n c, b m c -> b n m", desc_A, desc_B) * inv_temperature
+ logP = corr.log_softmax(dim=-2) + corr.log_softmax(dim=-1)
+ return logP
+
+
+def dual_softmax_matcher(desc_A, desc_B, inv_temperature=1, normalize=False):
+ if len(desc_A.shape) < 3:
+ desc_A, desc_B = desc_A[None], desc_B[None]
+ B, N, C = desc_A.shape
+ if normalize:
+ desc_A = desc_A / desc_A.norm(dim=-1, keepdim=True)
+ desc_B = desc_B / desc_B.norm(dim=-1, keepdim=True)
+ corr = torch.einsum("b n c, b m c -> b n m", desc_A, desc_B) * inv_temperature
+ else:
+ corr = torch.einsum("b n c, b m c -> b n m", desc_A, desc_B) * inv_temperature
+ P = corr.softmax(dim=-2) * corr.softmax(dim=-1)
+ return P
+
+
+def conditional_softmax_matcher(desc_A, desc_B, inv_temperature=1, normalize=False):
+ if len(desc_A.shape) < 3:
+ desc_A, desc_B = desc_A[None], desc_B[None]
+ B, N, C = desc_A.shape
+ if normalize:
+ desc_A = desc_A / desc_A.norm(dim=-1, keepdim=True)
+ desc_B = desc_B / desc_B.norm(dim=-1, keepdim=True)
+ corr = torch.einsum("b n c, b m c -> b n m", desc_A, desc_B) * inv_temperature
+ else:
+ corr = torch.einsum("b n c, b m c -> b n m", desc_A, desc_B) * inv_temperature
+ P_B_cond_A = corr.softmax(dim=-1)
+ P_A_cond_B = corr.softmax(dim=-2)
+
+ return P_A_cond_B, P_B_cond_A
+
+
+def draw_kpts(im, kpts, radius=2, width=1):
+ im = np.array(im)
+ # Convert keypoints to numpy array
+ kpts_np = kpts.cpu().numpy()
+
+ # Create a copy of the image to draw on
+ ret = im.copy()
+
+ # Define green color (BGR format in OpenCV)
+ green_color = (0, 255, 0)
+
+ # Draw green plus signs for each keypoint
+ for x, y in kpts_np:
+ # Convert to integer coordinates
+ x, y = int(x), int(y)
+
+ # Draw horizontal line of the plus sign
+ cv2.line(ret, (x - radius, y), (x + radius, y), green_color, width)
+ # Draw vertical line of the plus sign
+ cv2.line(ret, (x, y - radius), (x, y + radius), green_color, width)
+
+ return ret
+
+
+def masked_log_softmax(logits, mask):
+ masked_logits = torch.full_like(logits, -torch.inf)
+ masked_logits[mask] = logits[mask]
+ log_p = masked_logits.log_softmax(dim=-1)
+ return log_p
+
+
+def masked_softmax(logits, mask):
+ masked_logits = torch.full_like(logits, -torch.inf)
+ masked_logits[mask] = logits[mask]
+ log_p = masked_logits.softmax(dim=-1)
+ return log_p
+
+
+def kde(x, std=0.1, half=True, down=None):
+ # use a gaussian kernel to estimate density
+ if half:
+ x = x.half() # Do it in half precision TODO: remove hardcoding
+ if down is not None:
+ scores = (-(torch.cdist(x, x[::down]) ** 2) / (2 * std**2)).exp()
+ else:
+ scores = (-(torch.cdist(x, x) ** 2) / (2 * std**2)).exp()
+ density = scores.sum(dim=-1)
+ return density
+
+
+def midpoint_triangulation_unbatched(v1s_local, v2s_local, T1, T2, return_angles=False):
+ R1 = T1[:3, :3] # 3x3 rotation matrix
+ R2 = T2[:3, :3]
+ t1 = T1[:3, 3] # 3x1 translation vector
+ t2 = T2[:3, 3]
+
+ # Calculate camera centers (single position for each camera)
+ C1 = -torch.matmul(R1.T, t1) # (3,)
+ C2 = -torch.matmul(R2.T, t2) # (3,)
+
+ # # Transform view vectors from local to world coordinates
+ # # World vector = R * local_vector
+
+ v1s_world = F.normalize(v1s_local @ R1) # (N x 3)
+ v2s_world = F.normalize(v2s_local @ R2) # (N x 3)
+
+ # # Vector between camera centers (broadcast to match number of points)
+ b = C2 - C1 # (3,)
+ num_points = v1s_local.shape[0]
+ bs = b.unsqueeze(0).expand(num_points, -1) # (N x 3)
+
+ # Compute direction vectors between closest points on rays
+ cross1 = torch.cross(v1s_world, v2s_world) # N x 3
+ cross2 = torch.cross(bs, v2s_world) # N x 3
+
+ # Calculate parameters using cross products
+ s = torch.sum(cross2 * cross1, dim=1) / torch.sum(cross1 * cross1, dim=1)
+ t = torch.sum(torch.cross(bs, v1s_world) * cross1, dim=1) / torch.sum(
+ cross1 * cross1, dim=1
+ )
+
+ # Find points on each ray in world coordinates
+ P1s = C1.unsqueeze(0) + s.unsqueeze(1) * v1s_world # (N x 3)
+ P2s = C2.unsqueeze(0) + t.unsqueeze(1) * v2s_world # (N x 3)
+
+ # For parallel rays, use camera midpoints
+ # midpoint = (C1 + C2) / 2
+ # midpoints = midpoint.unsqueeze(0).expand(num_points, -1)
+ midpoint = (P1s + P2s) / 2
+ if not return_angles:
+ return midpoint
+ tri_angles = (
+ 180 / torch.pi * torch.acos((v1s_world * v2s_world).sum(dim=1).clip(0, 1.0))
+ )
+ return midpoint, tri_angles
+
+
+def midpoint_triangulation(
+ x_A: torch.Tensor,
+ x_B: torch.Tensor,
+ T_A: torch.Tensor,
+ T_B: torch.Tensor,
+ return_angles=False,
+):
+ batch, num_points, three = x_A.shape
+ assert three == 3
+ # rotation matrix
+ R_A = T_A[..., :3, :3] # (B x 3 x 3)
+ R_B = T_B[..., :3, :3]
+ # translation vector
+ t_A = T_A[..., :3, 3] # (B x 3)
+ t_B = T_B[..., :3, 3]
+
+ # Calculate camera centers (single position for each camera)
+ C_A = (R_A.mT @ -t_A[..., None])[..., 0] # (B x 3 x 3) * (B x 3 x 1) -> (B x 3)
+ C_B = (R_B.mT @ -t_B[..., None])[..., 0] # (B x 3 x 3) * (B x 3 x 1) -> (B x 3)
+
+ # # Transform view vectors from local to world coordinates
+ # # World vector = R * local_vector
+ ray_A_world = F.normalize(x_A @ R_A, dim=-1) # (B x N x 3)
+ ray_B_world = F.normalize(x_B @ R_B, dim=-1) # (B x N x 3)
+
+ # # Vector between camera centers (broadcast to match number of points)
+ b = C_B - C_A # (B x 3 x 1)
+ bs = b.reshape(batch, 1, three).expand(batch, num_points, three) # (B x N x 3)
+
+ # Compute direction vectors between closest points on rays
+ cross1 = torch.linalg.cross(ray_A_world, ray_B_world) # B x N x 3
+ cross2 = torch.linalg.cross(bs, ray_B_world) # B x N x 3
+ cross3 = torch.linalg.cross(bs, ray_A_world) # B x N x 3
+
+ # Calculate parameters using cross products
+ denom = torch.sum(cross1 * cross1, dim=-1) # (B x N x 3) -> (B x N)
+ s = torch.sum(cross2 * cross1, dim=-1) / denom # B x N
+ t = torch.sum(cross3 * cross1, dim=-1) / denom # B x N
+
+ # Find points on each ray in world coordinates
+ P_A = (
+ C_A[:, None] + s[..., None] * ray_A_world
+ ) # (B x 1 x 3), (B x N x 1), (B x N x 3) -> (B, N, 3)
+ P_B = (
+ C_B[:, None] + t[..., None] * ray_B_world
+ ) # (B x 1 x 3), (B x N x 1), (B x N x 3) -> (B, N, 3)
+
+ # For parallel rays, use camera midpoints
+ midpoint = (P_A + P_B) / 2 # (B x N x 3)
+ if not return_angles:
+ return midpoint
+ tri_angles = (
+ 180
+ / torch.pi
+ * torch.acos((ray_A_world * ray_B_world).sum(dim=-1).clip(0, 1.0))
+ ) # B x N
+ return midpoint, tri_angles
+
+
+class SkillIssue(NotImplementedError):
+ pass
+
+
+def calibrate(x: torch.Tensor, K: torch.Tensor):
+ # x: ..., 2
+ # K: ..., 3, 3
+ return to_homogeneous(x) @ K.inverse().mT
+
+
+def project(X: torch.Tensor, T: torch.Tensor, K: torch.Tensor):
+ # X: ..., 3
+ # T: ..., 4, 4
+ # K: ..., 3, 3
+ return from_homogeneous(from_homogeneous(to_homogeneous(X) @ T.mT) @ K.mT)
+
+
+def eye_like(x):
+ C, D = x.shape[-2:]
+ if C != D:
+ raise ValueError(f"Shape not square: {x.shape}")
+ e = torch.eye(D).to(x).expand_as(x)
+ return e
+
+
+def triangulate(x_A, x_B, T_A_to_B, K_A, K_B, method="midpoint", return_angles=False):
+ if method != "midpoint":
+ raise SkillIssue("You should use midpoint instead")
+ T_B = T_A_to_B
+ T_A = eye_like(T_B)
+ x_A_calib = calibrate(x_A, K_A)
+ x_B_calib = calibrate(x_B, K_B)
+ result = midpoint_triangulation(
+ x_A_calib, x_B_calib, T_A, T_B, return_angles=return_angles
+ )
+ return result
+
+
+def visualize_keypoints(img_path, vis_path, detector: Detector, num_keypoints: int):
+ img_path, vis_path = Path(img_path), Path(vis_path).with_suffix(".png")
+ img = Image.open(img_path)
+ detections = detector.detect_from_path(
+ img_path, num_keypoints=num_keypoints, return_dense_probs=True
+ )
+ W, H = img.size
+ kps = detections["keypoints"]
+ kps = detector.to_pixel_coords(kps, H, W)
+ (vis_path).parent.mkdir(parents=True, exist_ok=True)
+ Image.fromarray(draw_kpts(img, kps[0])).save(vis_path)
+ if detections["dense_probs"] is not None:
+ tensor_to_pil(detections["dense_probs"].squeeze().cpu(), autoscale=True).save(
+ vis_path.as_posix().replace(".png", "_dense_probs.png")
+ )
+
+
+def run_qualitative_examples(
+ *, model: Detector, workspace_path: str | Path, test_num_keypoints
+):
+ import dad
+
+ workspace_path = Path(workspace_path)
+ torch.cuda.empty_cache()
+ for im_path in [
+ "assets/0015_A.jpg",
+ "assets/0015_B.jpg",
+ "assets/0032_A.jpg",
+ "assets/0032_B.jpg",
+ "assets/apprentices.jpg",
+ "assets/rectangles_and_circles.png",
+ ]:
+ visualize_keypoints(
+ im_path,
+ workspace_path / "vis" / str(dad.GLOBAL_STEP) / im_path,
+ model,
+ num_keypoints=test_num_keypoints,
+ )
+ torch.cuda.empty_cache()
+
+
+def get_experiment_name(experiment_file: str):
+ return (
+ Path(experiment_file)
+ .relative_to(Path("experiments").absolute())
+ .with_suffix("")
+ .as_posix()
+ )
+
+
+def get_data_iterator(dataset, sample_weights, batch_size, num_steps):
+ sampler = torch.utils.data.WeightedRandomSampler(
+ sample_weights, num_samples=batch_size * num_steps, replacement=False
+ )
+ return iter(
+ torch.utils.data.DataLoader(
+ dataset,
+ batch_size=batch_size,
+ sampler=sampler,
+ num_workers=batch_size,
+ )
+ )
+
+
+def run_benchmarks(
+ benchmarks: list[Benchmark],
+ matcher: Matcher,
+ detector: Detector,
+ *,
+ step: int,
+ num_keypoints: Optional[list[int] | int] = None,
+ sample_every: Optional[int] = 1,
+):
+ import wandb
+
+ torch.cuda.empty_cache()
+ if isinstance(num_keypoints, int):
+ num_keypoints = [num_keypoints]
+
+ for bench in benchmarks:
+ wandb.log(
+ bench(num_keypoints=num_keypoints, sample_every=sample_every)(
+ matcher=matcher,
+ detector=detector,
+ ),
+ step=step,
+ )
+ torch.cuda.empty_cache()
+
+
+def estimate_pose_essential(
+ kps_A: np.ndarray,
+ kps_B: np.ndarray,
+ w_A: int,
+ h_A: int,
+ K_A: np.ndarray,
+ w_B: int,
+ h_B: int,
+ K_B: np.ndarray,
+ th: float,
+) -> tuple[np.ndarray, np.ndarray]:
+ import poselib
+
+ camera1 = {
+ "model": "PINHOLE",
+ "width": w_A,
+ "height": h_A,
+ "params": K_A[[0, 1, 0, 1], [0, 1, 2, 2]],
+ }
+ camera2 = {
+ "model": "PINHOLE",
+ "width": w_B,
+ "height": h_B,
+ "params": K_B[[0, 1, 0, 1], [0, 1, 2, 2]],
+ }
+
+ pose, res = poselib.estimate_relative_pose(
+ kps_A,
+ kps_B,
+ camera1,
+ camera2,
+ ransac_opt={
+ "max_epipolar_error": th,
+ },
+ )
+ return pose.R, pose.t
+
+
+def poselib_fundamental(x1, x2, opt):
+ import poselib
+
+ F, info = poselib.estimate_fundamental(x1, x2, opt, {})
+ inl = info["inliers"]
+ return F, inl
+
+
+def estimate_pose_fundamental(
+ kps_A: np.ndarray,
+ kps_B: np.ndarray,
+ w_A: int,
+ h_A: int,
+ K_A: np.ndarray,
+ w_B: int,
+ h_B: int,
+ K_B: np.ndarray,
+ th: float,
+) -> tuple[np.ndarray, np.ndarray]:
+ if len(kps_A) < 8:
+ return np.eye(3), np.zeros(3)
+ F, inl = poselib_fundamental(
+ kps_A,
+ kps_B,
+ opt={
+ "max_epipolar_error": th,
+ },
+ )
+ E: np.ndarray = K_B.T @ F @ K_A
+ kps_calib_A = from_hom(
+ calibrate(torch.from_numpy(kps_A).float(), torch.from_numpy(K_A).float())
+ ).numpy()
+ kps_calib_B = from_hom(
+ calibrate(torch.from_numpy(kps_B).float(), torch.from_numpy(K_B).float())
+ ).numpy()
+ E = E.astype(np.float64)
+ _, R, t, good = cv2.recoverPose(E, kps_calib_A, kps_calib_B)
+ t = t[:, 0]
+ return R, t
+
+
+def so2(radians):
+ return torch.tensor(
+ [
+ [math.cos(radians), math.sin(radians), 0],
+ [-math.sin(radians), math.cos(radians), 0],
+ [0, 0, 1.0],
+ ]
+ )
+
+
+def rotate_normalized_points(points: torch.Tensor, angle: float):
+ # points are between -1, 1, Nx2
+ # angle is float [0, 360]
+ radians = angle * math.pi / 180
+ rot_mat = so2(radians).to(points)
+ return points @ rot_mat[:2, :2].T
+
+
+def compute_detector_correlation(dets1: torch.Tensor, dets2: torch.Tensor, th: float):
+ # det1.shape = (K, 2)
+ # K = num keypoints
+ d = torch.cdist(dets1, dets2, compute_mode="donot_use_mm_for_euclid_dist")
+ d12 = d.amin(dim=1)
+ d21 = d.amin(dim=0)
+ mnn = (d == d12) * (d == d21)
+ corr = mnn.float()
+ corr[d > th] = 0.0
+ return corr.sum(dim=1).mean(), corr.sum(dim=0).mean()
+
+
+def cross_entropy(log_p_hat: torch.Tensor, p: torch.Tensor):
+ return -(log_p_hat * p).sum(dim=-1)
+
+
+def kl_div(p: torch.Tensor, log_p_hat: torch.Tensor):
+ return cross_entropy(log_p_hat, p) - cross_entropy((p + 1e-12).log(), p)
+
+
+def generalized_mean(r, p1, p2):
+ return (1 / 2 * (p1**r + p2**r)) ** (1 / r)
+
+
+def setup_experiment(experiment_file, root_workspace_path="workspace", disable_wandb=False):
+ import wandb
+
+ experiment_name = get_experiment_name(experiment_file)
+ wandb.init(
+ project="dad",
+ mode="online" if not disable_wandb else "disabled",
+ name=experiment_name.replace("/", "-"),
+ )
+ workspace_path = Path(root_workspace_path) / experiment_name
+ workspace_path.mkdir(parents=True, exist_ok=True)
+ return workspace_path
+
+
+def check_not_i16(im):
+ if im.mode == "I;16":
+ raise NotImplementedError("Can't handle 16 bit images")
+
+def wrap_in_sbatch(command, account, time_alloc = "2-23:00:00"):
+ sbatch_command = f"""#!/bin/bash
+#SBATCH -A {account}
+#SBATCH -t {time_alloc}
+#SBATCH -o %j.out
+#SBATCH --gpus 1
+#SBATCH --nodes 1
+
+# Job script commands follow
+# Print some GPU info" \
+source .venv/bin/activate
+{command}
+"""
+ return sbatch_command
\ No newline at end of file
diff --git a/imcui/third_party/dad/data/.gitignore b/imcui/third_party/dad/data/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..c96a04f008ee21e260b28f7701595ed59e2839e3
--- /dev/null
+++ b/imcui/third_party/dad/data/.gitignore
@@ -0,0 +1,2 @@
+*
+!.gitignore
\ No newline at end of file
diff --git a/imcui/third_party/dad/licenses/aliked/LICENSE b/imcui/third_party/dad/licenses/aliked/LICENSE
new file mode 100644
index 0000000000000000000000000000000000000000..3f1c35e2a29273aa6b19baef973196d387b371d3
--- /dev/null
+++ b/imcui/third_party/dad/licenses/aliked/LICENSE
@@ -0,0 +1,29 @@
+BSD 3-Clause License
+
+Copyright (c) 2022, Zhao Xiaoming
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+
+3. Neither the name of the copyright holder nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
\ No newline at end of file
diff --git a/imcui/third_party/dad/licenses/superpoint/LICENSE b/imcui/third_party/dad/licenses/superpoint/LICENSE
new file mode 100644
index 0000000000000000000000000000000000000000..23e5612355962235c187ce7eede36d3ef69b5485
--- /dev/null
+++ b/imcui/third_party/dad/licenses/superpoint/LICENSE
@@ -0,0 +1,27 @@
+Magic Leap, Inc. ("COMPANY") CONFIDENTIAL
+
+Unpublished Copyright (c) 2020
+Magic Leap, Inc., All Rights Reserved.
+
+NOTICE: All information contained herein is, and remains the property
+of COMPANY. The intellectual and technical concepts contained herein
+are proprietary to COMPANY and may be covered by U.S. and Foreign
+Patents, patents in process, and are protected by trade secret or
+copyright law. Dissemination of this information or reproduction of
+this material is strictly forbidden unless prior written permission is
+obtained from COMPANY. Access to the source code contained herein is
+hereby forbidden to anyone except current COMPANY employees, managers
+or contractors who have executed Confidentiality and Non-disclosure
+agreements explicitly covering such access.
+
+The copyright notice above does not evidence any actual or intended
+publication or disclosure of this source code, which includes
+information that is confidential and/or proprietary, and is a trade
+secret, of COMPANY. ANY REPRODUCTION, MODIFICATION, DISTRIBUTION,
+PUBLIC PERFORMANCE, OR PUBLIC DISPLAY OF OR THROUGH USE OF THIS
+SOURCE CODE WITHOUT THE EXPRESS WRITTEN CONSENT OF COMPANY IS
+STRICTLY PROHIBITED, AND IN VIOLATION OF APPLICABLE LAWS AND
+INTERNATIONAL TREATIES. THE RECEIPT OR POSSESSION OF THIS SOURCE
+CODE AND/OR RELATED INFORMATION DOES NOT CONVEY OR IMPLY ANY RIGHTS
+TO REPRODUCE, DISCLOSE OR DISTRIBUTE ITS CONTENTS, OR TO MANUFACTURE,
+USE, OR SELL ANYTHING THAT IT MAY DESCRIBE, IN WHOLE OR IN PART.
diff --git a/imcui/third_party/dad/new_day.py b/imcui/third_party/dad/new_day.py
new file mode 100644
index 0000000000000000000000000000000000000000..c27605970acd06806a3431c743c72911144f767a
--- /dev/null
+++ b/imcui/third_party/dad/new_day.py
@@ -0,0 +1,28 @@
+from pathlib import Path
+from datetime import datetime
+import dad
+
+
+def create_folder_structure():
+ dad.logger.info("New day, have fun!")
+
+ # Get the current date
+ current_date = datetime.now()
+
+ # Calculate the current week number (1-52)
+ week_number = current_date.isocalendar()[1]
+
+ # Get the current day name (e.g., "Monday", "Tuesday", etc.)
+ day_name = current_date.strftime("%A")
+
+ # Create the folder structure using pathlib
+ folder_path = Path(__file__).parent / "experiments" / f"w{week_number:02d}" / day_name.lower()
+
+ # Create the folders if they don't exist
+ folder_path.mkdir(parents=True, exist_ok=True)
+
+ dad.logger.info(f"Folder structure created: {folder_path}")
+
+
+if __name__ == "__main__":
+ create_folder_structure()
diff --git a/imcui/third_party/dad/pyproject.toml b/imcui/third_party/dad/pyproject.toml
new file mode 100644
index 0000000000000000000000000000000000000000..acb25f152274945e6614cb42624df78971618c79
--- /dev/null
+++ b/imcui/third_party/dad/pyproject.toml
@@ -0,0 +1,35 @@
+[project]
+name = "dad"
+version = "0.1.0"
+description = "Definitely a Detector, DeDoDe and DISK, DaD and DaD."
+readme = "README.md"
+requires-python = ">=3.10"
+dependencies = [
+ "einops>=0.8.1",
+ "numpy>=1.24",
+ "opencv-python>=4.11.0.86",
+ "pillow>=11.1.0",
+ "torch>=2.4",
+ "torchvision>=0.21.0",
+]
+[dependency-groups]
+dev = [
+ "opencv-contrib-python>=4.11.0.86",
+ "matplotlib>=3.10.1",
+ "poselib==2.0.4",
+ "h5py>=3.13.0",
+ "pyhesaff>=2.1.1",
+ "romatch>=0.0.2",
+ "ruff>=0.9.10",
+ "tqdm>=4.67.1",
+ "wandb>=0.19.8",
+]
+
+
+[build-system]
+requires = ["hatchling"]
+build-backend = "hatchling.build"
+
+
+[tool.uv.sources]
+romatch = { git = "https://github.com/Parskatt/RoMa.git" }
diff --git a/imcui/third_party/dad/uv.lock b/imcui/third_party/dad/uv.lock
new file mode 100644
index 0000000000000000000000000000000000000000..88fcba662590ccda7a380dd85ac99fc5f57d20c4
--- /dev/null
+++ b/imcui/third_party/dad/uv.lock
@@ -0,0 +1,1837 @@
+version = 1
+revision = 1
+requires-python = ">=3.10"
+resolution-markers = [
+ "python_full_version < '3.11' and sys_platform == 'darwin'",
+ "python_version < '0'",
+ "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'",
+ "python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux'",
+ "python_full_version < '3.11' and platform_machine != 'aarch64' and sys_platform == 'linux'",
+ "python_full_version == '3.11.*' and sys_platform == 'darwin'",
+ "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'",
+ "python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux'",
+ "python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux'",
+ "python_full_version >= '3.12' and sys_platform == 'darwin'",
+ "python_full_version >= '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux'",
+ "python_full_version >= '3.12' and sys_platform != 'darwin' and sys_platform != 'linux'",
+ "python_full_version >= '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux'",
+]
+
+[[package]]
+name = "albucore"
+version = "0.0.23"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "numpy" },
+ { name = "opencv-python-headless" },
+ { name = "simsimd" },
+ { name = "stringzilla" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/c0/64/78d1716dd1496734d58705d68e02a9eadf4c10edd32c3ad641dde949efca/albucore-0.0.23.tar.gz", hash = "sha256:57823982b954913b84a9e2cf71058c4577b02397a62c41885be2d9b295efa8ab", size = 16437 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/3d/de/4d9298befa6ae0f21230378f55100dca364816e3734028ca2766f2eca263/albucore-0.0.23-py3-none-any.whl", hash = "sha256:99274ac0c15a1a7d9a726df9d54d5ab70d9d0c189e2a935399dba3d4bafad415", size = 14717 },
+]
+
+[[package]]
+name = "albumentations"
+version = "2.0.5"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "albucore" },
+ { name = "numpy" },
+ { name = "opencv-python-headless" },
+ { name = "pydantic" },
+ { name = "pyyaml" },
+ { name = "scipy" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/8d/ad/89050e1222a57e7b834368f83bb2644b428a18c1e078a5c7762abb6beea0/albumentations-2.0.5.tar.gz", hash = "sha256:e19e1c0f14c903c3c230f3d83f14814b84f1180393189bf96779f653031f3278", size = 281195 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/97/d3/cf3aab593209d1be5e4bca54aeea297225708bd25f06426d6b8ec3630a76/albumentations-2.0.5-py3-none-any.whl", hash = "sha256:1fc253942d34dd7c07652bf6511049c8bb7d522baec7f1fe355df16293c3c7b6", size = 290588 },
+]
+
+[[package]]
+name = "annotated-types"
+version = "0.7.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 },
+]
+
+[[package]]
+name = "certifi"
+version = "2025.1.31"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/1c/ab/c9f1e32b7b1bf505bf26f0ef697775960db7932abeb7b516de930ba2705f/certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651", size = 167577 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/38/fc/bce832fd4fd99766c04d1ee0eead6b0ec6486fb100ae5e74c1d91292b982/certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe", size = 166393 },
+]
+
+[[package]]
+name = "charset-normalizer"
+version = "3.4.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/16/b0/572805e227f01586461c80e0fd25d65a2115599cc9dad142fee4b747c357/charset_normalizer-3.4.1.tar.gz", hash = "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3", size = 123188 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/0d/58/5580c1716040bc89206c77d8f74418caf82ce519aae06450393ca73475d1/charset_normalizer-3.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:91b36a978b5ae0ee86c394f5a54d6ef44db1de0815eb43de826d41d21e4af3de", size = 198013 },
+ { url = "https://files.pythonhosted.org/packages/d0/11/00341177ae71c6f5159a08168bcb98c6e6d196d372c94511f9f6c9afe0c6/charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7461baadb4dc00fd9e0acbe254e3d7d2112e7f92ced2adc96e54ef6501c5f176", size = 141285 },
+ { url = "https://files.pythonhosted.org/packages/01/09/11d684ea5819e5a8f5100fb0b38cf8d02b514746607934134d31233e02c8/charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e218488cd232553829be0664c2292d3af2eeeb94b32bea483cf79ac6a694e037", size = 151449 },
+ { url = "https://files.pythonhosted.org/packages/08/06/9f5a12939db324d905dc1f70591ae7d7898d030d7662f0d426e2286f68c9/charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:80ed5e856eb7f30115aaf94e4a08114ccc8813e6ed1b5efa74f9f82e8509858f", size = 143892 },
+ { url = "https://files.pythonhosted.org/packages/93/62/5e89cdfe04584cb7f4d36003ffa2936681b03ecc0754f8e969c2becb7e24/charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b010a7a4fd316c3c484d482922d13044979e78d1861f0e0650423144c616a46a", size = 146123 },
+ { url = "https://files.pythonhosted.org/packages/a9/ac/ab729a15c516da2ab70a05f8722ecfccc3f04ed7a18e45c75bbbaa347d61/charset_normalizer-3.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4532bff1b8421fd0a320463030c7520f56a79c9024a4e88f01c537316019005a", size = 147943 },
+ { url = "https://files.pythonhosted.org/packages/03/d2/3f392f23f042615689456e9a274640c1d2e5dd1d52de36ab8f7955f8f050/charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d973f03c0cb71c5ed99037b870f2be986c3c05e63622c017ea9816881d2dd247", size = 142063 },
+ { url = "https://files.pythonhosted.org/packages/f2/e3/e20aae5e1039a2cd9b08d9205f52142329f887f8cf70da3650326670bddf/charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3a3bd0dcd373514dcec91c411ddb9632c0d7d92aed7093b8c3bbb6d69ca74408", size = 150578 },
+ { url = "https://files.pythonhosted.org/packages/8d/af/779ad72a4da0aed925e1139d458adc486e61076d7ecdcc09e610ea8678db/charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:d9c3cdf5390dcd29aa8056d13e8e99526cda0305acc038b96b30352aff5ff2bb", size = 153629 },
+ { url = "https://files.pythonhosted.org/packages/c2/b6/7aa450b278e7aa92cf7732140bfd8be21f5f29d5bf334ae987c945276639/charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:2bdfe3ac2e1bbe5b59a1a63721eb3b95fc9b6817ae4a46debbb4e11f6232428d", size = 150778 },
+ { url = "https://files.pythonhosted.org/packages/39/f4/d9f4f712d0951dcbfd42920d3db81b00dd23b6ab520419626f4023334056/charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:eab677309cdb30d047996b36d34caeda1dc91149e4fdca0b1a039b3f79d9a807", size = 146453 },
+ { url = "https://files.pythonhosted.org/packages/49/2b/999d0314e4ee0cff3cb83e6bc9aeddd397eeed693edb4facb901eb8fbb69/charset_normalizer-3.4.1-cp310-cp310-win32.whl", hash = "sha256:c0429126cf75e16c4f0ad00ee0eae4242dc652290f940152ca8c75c3a4b6ee8f", size = 95479 },
+ { url = "https://files.pythonhosted.org/packages/2d/ce/3cbed41cff67e455a386fb5e5dd8906cdda2ed92fbc6297921f2e4419309/charset_normalizer-3.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:9f0b8b1c6d84c8034a44893aba5e767bf9c7a211e313a9605d9c617d7083829f", size = 102790 },
+ { url = "https://files.pythonhosted.org/packages/72/80/41ef5d5a7935d2d3a773e3eaebf0a9350542f2cab4eac59a7a4741fbbbbe/charset_normalizer-3.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8bfa33f4f2672964266e940dd22a195989ba31669bd84629f05fab3ef4e2d125", size = 194995 },
+ { url = "https://files.pythonhosted.org/packages/7a/28/0b9fefa7b8b080ec492110af6d88aa3dea91c464b17d53474b6e9ba5d2c5/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28bf57629c75e810b6ae989f03c0828d64d6b26a5e205535585f96093e405ed1", size = 139471 },
+ { url = "https://files.pythonhosted.org/packages/71/64/d24ab1a997efb06402e3fc07317e94da358e2585165930d9d59ad45fcae2/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f08ff5e948271dc7e18a35641d2f11a4cd8dfd5634f55228b691e62b37125eb3", size = 149831 },
+ { url = "https://files.pythonhosted.org/packages/37/ed/be39e5258e198655240db5e19e0b11379163ad7070962d6b0c87ed2c4d39/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:234ac59ea147c59ee4da87a0c0f098e9c8d169f4dc2a159ef720f1a61bbe27cd", size = 142335 },
+ { url = "https://files.pythonhosted.org/packages/88/83/489e9504711fa05d8dde1574996408026bdbdbd938f23be67deebb5eca92/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd4ec41f914fa74ad1b8304bbc634b3de73d2a0889bd32076342a573e0779e00", size = 143862 },
+ { url = "https://files.pythonhosted.org/packages/c6/c7/32da20821cf387b759ad24627a9aca289d2822de929b8a41b6241767b461/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eea6ee1db730b3483adf394ea72f808b6e18cf3cb6454b4d86e04fa8c4327a12", size = 145673 },
+ { url = "https://files.pythonhosted.org/packages/68/85/f4288e96039abdd5aeb5c546fa20a37b50da71b5cf01e75e87f16cd43304/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c96836c97b1238e9c9e3fe90844c947d5afbf4f4c92762679acfe19927d81d77", size = 140211 },
+ { url = "https://files.pythonhosted.org/packages/28/a3/a42e70d03cbdabc18997baf4f0227c73591a08041c149e710045c281f97b/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4d86f7aff21ee58f26dcf5ae81a9addbd914115cdebcbb2217e4f0ed8982e146", size = 148039 },
+ { url = "https://files.pythonhosted.org/packages/85/e4/65699e8ab3014ecbe6f5c71d1a55d810fb716bbfd74f6283d5c2aa87febf/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:09b5e6733cbd160dcc09589227187e242a30a49ca5cefa5a7edd3f9d19ed53fd", size = 151939 },
+ { url = "https://files.pythonhosted.org/packages/b1/82/8e9fe624cc5374193de6860aba3ea8070f584c8565ee77c168ec13274bd2/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:5777ee0881f9499ed0f71cc82cf873d9a0ca8af166dfa0af8ec4e675b7df48e6", size = 149075 },
+ { url = "https://files.pythonhosted.org/packages/3d/7b/82865ba54c765560c8433f65e8acb9217cb839a9e32b42af4aa8e945870f/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:237bdbe6159cff53b4f24f397d43c6336c6b0b42affbe857970cefbb620911c8", size = 144340 },
+ { url = "https://files.pythonhosted.org/packages/b5/b6/9674a4b7d4d99a0d2df9b215da766ee682718f88055751e1e5e753c82db0/charset_normalizer-3.4.1-cp311-cp311-win32.whl", hash = "sha256:8417cb1f36cc0bc7eaba8ccb0e04d55f0ee52df06df3ad55259b9a323555fc8b", size = 95205 },
+ { url = "https://files.pythonhosted.org/packages/1e/ab/45b180e175de4402dcf7547e4fb617283bae54ce35c27930a6f35b6bef15/charset_normalizer-3.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:d7f50a1f8c450f3925cb367d011448c39239bb3eb4117c36a6d354794de4ce76", size = 102441 },
+ { url = "https://files.pythonhosted.org/packages/0a/9a/dd1e1cdceb841925b7798369a09279bd1cf183cef0f9ddf15a3a6502ee45/charset_normalizer-3.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:73d94b58ec7fecbc7366247d3b0b10a21681004153238750bb67bd9012414545", size = 196105 },
+ { url = "https://files.pythonhosted.org/packages/d3/8c/90bfabf8c4809ecb648f39794cf2a84ff2e7d2a6cf159fe68d9a26160467/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dad3e487649f498dd991eeb901125411559b22e8d7ab25d3aeb1af367df5efd7", size = 140404 },
+ { url = "https://files.pythonhosted.org/packages/ad/8f/e410d57c721945ea3b4f1a04b74f70ce8fa800d393d72899f0a40526401f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c30197aa96e8eed02200a83fba2657b4c3acd0f0aa4bdc9f6c1af8e8962e0757", size = 150423 },
+ { url = "https://files.pythonhosted.org/packages/f0/b8/e6825e25deb691ff98cf5c9072ee0605dc2acfca98af70c2d1b1bc75190d/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2369eea1ee4a7610a860d88f268eb39b95cb588acd7235e02fd5a5601773d4fa", size = 143184 },
+ { url = "https://files.pythonhosted.org/packages/3e/a2/513f6cbe752421f16d969e32f3583762bfd583848b763913ddab8d9bfd4f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d", size = 145268 },
+ { url = "https://files.pythonhosted.org/packages/74/94/8a5277664f27c3c438546f3eb53b33f5b19568eb7424736bdc440a88a31f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616", size = 147601 },
+ { url = "https://files.pythonhosted.org/packages/7c/5f/6d352c51ee763623a98e31194823518e09bfa48be2a7e8383cf691bbb3d0/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:804a4d582ba6e5b747c625bf1255e6b1507465494a40a2130978bda7b932c90b", size = 141098 },
+ { url = "https://files.pythonhosted.org/packages/78/d4/f5704cb629ba5ab16d1d3d741396aec6dc3ca2b67757c45b0599bb010478/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f55e69f030f7163dffe9fd0752b32f070566451afe180f99dbeeb81f511ad8d", size = 149520 },
+ { url = "https://files.pythonhosted.org/packages/c5/96/64120b1d02b81785f222b976c0fb79a35875457fa9bb40827678e54d1bc8/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c4c3e6da02df6fa1410a7680bd3f63d4f710232d3139089536310d027950696a", size = 152852 },
+ { url = "https://files.pythonhosted.org/packages/84/c9/98e3732278a99f47d487fd3468bc60b882920cef29d1fa6ca460a1fdf4e6/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:5df196eb874dae23dcfb968c83d4f8fdccb333330fe1fc278ac5ceeb101003a9", size = 150488 },
+ { url = "https://files.pythonhosted.org/packages/13/0e/9c8d4cb99c98c1007cc11eda969ebfe837bbbd0acdb4736d228ccaabcd22/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e358e64305fe12299a08e08978f51fc21fac060dcfcddd95453eabe5b93ed0e1", size = 146192 },
+ { url = "https://files.pythonhosted.org/packages/b2/21/2b6b5b860781a0b49427309cb8670785aa543fb2178de875b87b9cc97746/charset_normalizer-3.4.1-cp312-cp312-win32.whl", hash = "sha256:9b23ca7ef998bc739bf6ffc077c2116917eabcc901f88da1b9856b210ef63f35", size = 95550 },
+ { url = "https://files.pythonhosted.org/packages/21/5b/1b390b03b1d16c7e382b561c5329f83cc06623916aab983e8ab9239c7d5c/charset_normalizer-3.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:6ff8a4a60c227ad87030d76e99cd1698345d4491638dfa6673027c48b3cd395f", size = 102785 },
+ { url = "https://files.pythonhosted.org/packages/38/94/ce8e6f63d18049672c76d07d119304e1e2d7c6098f0841b51c666e9f44a0/charset_normalizer-3.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda", size = 195698 },
+ { url = "https://files.pythonhosted.org/packages/24/2e/dfdd9770664aae179a96561cc6952ff08f9a8cd09a908f259a9dfa063568/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313", size = 140162 },
+ { url = "https://files.pythonhosted.org/packages/24/4e/f646b9093cff8fc86f2d60af2de4dc17c759de9d554f130b140ea4738ca6/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9", size = 150263 },
+ { url = "https://files.pythonhosted.org/packages/5e/67/2937f8d548c3ef6e2f9aab0f6e21001056f692d43282b165e7c56023e6dd/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b", size = 142966 },
+ { url = "https://files.pythonhosted.org/packages/52/ed/b7f4f07de100bdb95c1756d3a4d17b90c1a3c53715c1a476f8738058e0fa/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11", size = 144992 },
+ { url = "https://files.pythonhosted.org/packages/96/2c/d49710a6dbcd3776265f4c923bb73ebe83933dfbaa841c5da850fe0fd20b/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f", size = 147162 },
+ { url = "https://files.pythonhosted.org/packages/b4/41/35ff1f9a6bd380303dea55e44c4933b4cc3c4850988927d4082ada230273/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd", size = 140972 },
+ { url = "https://files.pythonhosted.org/packages/fb/43/c6a0b685fe6910d08ba971f62cd9c3e862a85770395ba5d9cad4fede33ab/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2", size = 149095 },
+ { url = "https://files.pythonhosted.org/packages/4c/ff/a9a504662452e2d2878512115638966e75633519ec11f25fca3d2049a94a/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886", size = 152668 },
+ { url = "https://files.pythonhosted.org/packages/6c/71/189996b6d9a4b932564701628af5cee6716733e9165af1d5e1b285c530ed/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601", size = 150073 },
+ { url = "https://files.pythonhosted.org/packages/e4/93/946a86ce20790e11312c87c75ba68d5f6ad2208cfb52b2d6a2c32840d922/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd", size = 145732 },
+ { url = "https://files.pythonhosted.org/packages/cd/e5/131d2fb1b0dddafc37be4f3a2fa79aa4c037368be9423061dccadfd90091/charset_normalizer-3.4.1-cp313-cp313-win32.whl", hash = "sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407", size = 95391 },
+ { url = "https://files.pythonhosted.org/packages/27/f2/4f9a69cc7712b9b5ad8fdb87039fd89abba997ad5cbe690d1835d40405b0/charset_normalizer-3.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971", size = 102702 },
+ { url = "https://files.pythonhosted.org/packages/0e/f6/65ecc6878a89bb1c23a086ea335ad4bf21a588990c3f535a227b9eea9108/charset_normalizer-3.4.1-py3-none-any.whl", hash = "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85", size = 49767 },
+]
+
+[[package]]
+name = "click"
+version = "8.1.8"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "colorama", marker = "sys_platform == 'win32'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188 },
+]
+
+[[package]]
+name = "colorama"
+version = "0.4.6"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 },
+]
+
+[[package]]
+name = "contourpy"
+version = "1.3.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "numpy" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/25/c2/fc7193cc5383637ff390a712e88e4ded0452c9fbcf84abe3de5ea3df1866/contourpy-1.3.1.tar.gz", hash = "sha256:dfd97abd83335045a913e3bcc4a09c0ceadbe66580cf573fe961f4a825efa699", size = 13465753 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b2/a3/80937fe3efe0edacf67c9a20b955139a1a622730042c1ea991956f2704ad/contourpy-1.3.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a045f341a77b77e1c5de31e74e966537bba9f3c4099b35bf4c2e3939dd54cdab", size = 268466 },
+ { url = "https://files.pythonhosted.org/packages/82/1d/e3eaebb4aa2d7311528c048350ca8e99cdacfafd99da87bc0a5f8d81f2c2/contourpy-1.3.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:500360b77259914f7805af7462e41f9cb7ca92ad38e9f94d6c8641b089338124", size = 253314 },
+ { url = "https://files.pythonhosted.org/packages/de/f3/d796b22d1a2b587acc8100ba8c07fb7b5e17fde265a7bb05ab967f4c935a/contourpy-1.3.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2f926efda994cdf3c8d3fdb40b9962f86edbc4457e739277b961eced3d0b4c1", size = 312003 },
+ { url = "https://files.pythonhosted.org/packages/bf/f5/0e67902bc4394daee8daa39c81d4f00b50e063ee1a46cb3938cc65585d36/contourpy-1.3.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:adce39d67c0edf383647a3a007de0a45fd1b08dedaa5318404f1a73059c2512b", size = 351896 },
+ { url = "https://files.pythonhosted.org/packages/1f/d6/e766395723f6256d45d6e67c13bb638dd1fa9dc10ef912dc7dd3dcfc19de/contourpy-1.3.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:abbb49fb7dac584e5abc6636b7b2a7227111c4f771005853e7d25176daaf8453", size = 320814 },
+ { url = "https://files.pythonhosted.org/packages/a9/57/86c500d63b3e26e5b73a28b8291a67c5608d4aa87ebd17bd15bb33c178bc/contourpy-1.3.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a0cffcbede75c059f535725c1680dfb17b6ba8753f0c74b14e6a9c68c29d7ea3", size = 324969 },
+ { url = "https://files.pythonhosted.org/packages/b8/62/bb146d1289d6b3450bccc4642e7f4413b92ebffd9bf2e91b0404323704a7/contourpy-1.3.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ab29962927945d89d9b293eabd0d59aea28d887d4f3be6c22deaefbb938a7277", size = 1265162 },
+ { url = "https://files.pythonhosted.org/packages/18/04/9f7d132ce49a212c8e767042cc80ae390f728060d2eea47058f55b9eff1c/contourpy-1.3.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:974d8145f8ca354498005b5b981165b74a195abfae9a8129df3e56771961d595", size = 1324328 },
+ { url = "https://files.pythonhosted.org/packages/46/23/196813901be3f97c83ababdab1382e13e0edc0bb4e7b49a7bff15fcf754e/contourpy-1.3.1-cp310-cp310-win32.whl", hash = "sha256:ac4578ac281983f63b400f7fe6c101bedc10651650eef012be1ccffcbacf3697", size = 173861 },
+ { url = "https://files.pythonhosted.org/packages/e0/82/c372be3fc000a3b2005061ca623a0d1ecd2eaafb10d9e883a2fc8566e951/contourpy-1.3.1-cp310-cp310-win_amd64.whl", hash = "sha256:174e758c66bbc1c8576992cec9599ce8b6672b741b5d336b5c74e35ac382b18e", size = 218566 },
+ { url = "https://files.pythonhosted.org/packages/12/bb/11250d2906ee2e8b466b5f93e6b19d525f3e0254ac8b445b56e618527718/contourpy-1.3.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3e8b974d8db2c5610fb4e76307e265de0edb655ae8169e8b21f41807ccbeec4b", size = 269555 },
+ { url = "https://files.pythonhosted.org/packages/67/71/1e6e95aee21a500415f5d2dbf037bf4567529b6a4e986594d7026ec5ae90/contourpy-1.3.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:20914c8c973f41456337652a6eeca26d2148aa96dd7ac323b74516988bea89fc", size = 254549 },
+ { url = "https://files.pythonhosted.org/packages/31/2c/b88986e8d79ac45efe9d8801ae341525f38e087449b6c2f2e6050468a42c/contourpy-1.3.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:19d40d37c1c3a4961b4619dd9d77b12124a453cc3d02bb31a07d58ef684d3d86", size = 313000 },
+ { url = "https://files.pythonhosted.org/packages/c4/18/65280989b151fcf33a8352f992eff71e61b968bef7432fbfde3a364f0730/contourpy-1.3.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:113231fe3825ebf6f15eaa8bc1f5b0ddc19d42b733345eae0934cb291beb88b6", size = 352925 },
+ { url = "https://files.pythonhosted.org/packages/f5/c7/5fd0146c93220dbfe1a2e0f98969293b86ca9bc041d6c90c0e065f4619ad/contourpy-1.3.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4dbbc03a40f916a8420e420d63e96a1258d3d1b58cbdfd8d1f07b49fcbd38e85", size = 323693 },
+ { url = "https://files.pythonhosted.org/packages/85/fc/7fa5d17daf77306840a4e84668a48ddff09e6bc09ba4e37e85ffc8e4faa3/contourpy-1.3.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a04ecd68acbd77fa2d39723ceca4c3197cb2969633836ced1bea14e219d077c", size = 326184 },
+ { url = "https://files.pythonhosted.org/packages/ef/e7/104065c8270c7397c9571620d3ab880558957216f2b5ebb7e040f85eeb22/contourpy-1.3.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c414fc1ed8ee1dbd5da626cf3710c6013d3d27456651d156711fa24f24bd1291", size = 1268031 },
+ { url = "https://files.pythonhosted.org/packages/e2/4a/c788d0bdbf32c8113c2354493ed291f924d4793c4a2e85b69e737a21a658/contourpy-1.3.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:31c1b55c1f34f80557d3830d3dd93ba722ce7e33a0b472cba0ec3b6535684d8f", size = 1325995 },
+ { url = "https://files.pythonhosted.org/packages/a6/e6/a2f351a90d955f8b0564caf1ebe4b1451a3f01f83e5e3a414055a5b8bccb/contourpy-1.3.1-cp311-cp311-win32.whl", hash = "sha256:f611e628ef06670df83fce17805c344710ca5cde01edfdc72751311da8585375", size = 174396 },
+ { url = "https://files.pythonhosted.org/packages/a8/7e/cd93cab453720a5d6cb75588cc17dcdc08fc3484b9de98b885924ff61900/contourpy-1.3.1-cp311-cp311-win_amd64.whl", hash = "sha256:b2bdca22a27e35f16794cf585832e542123296b4687f9fd96822db6bae17bfc9", size = 219787 },
+ { url = "https://files.pythonhosted.org/packages/37/6b/175f60227d3e7f5f1549fcb374592be311293132207e451c3d7c654c25fb/contourpy-1.3.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0ffa84be8e0bd33410b17189f7164c3589c229ce5db85798076a3fa136d0e509", size = 271494 },
+ { url = "https://files.pythonhosted.org/packages/6b/6a/7833cfae2c1e63d1d8875a50fd23371394f540ce809d7383550681a1fa64/contourpy-1.3.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:805617228ba7e2cbbfb6c503858e626ab528ac2a32a04a2fe88ffaf6b02c32bc", size = 255444 },
+ { url = "https://files.pythonhosted.org/packages/7f/b3/7859efce66eaca5c14ba7619791b084ed02d868d76b928ff56890d2d059d/contourpy-1.3.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ade08d343436a94e633db932e7e8407fe7de8083967962b46bdfc1b0ced39454", size = 307628 },
+ { url = "https://files.pythonhosted.org/packages/48/b2/011415f5e3f0a50b1e285a0bf78eb5d92a4df000553570f0851b6e309076/contourpy-1.3.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:47734d7073fb4590b4a40122b35917cd77be5722d80683b249dac1de266aac80", size = 347271 },
+ { url = "https://files.pythonhosted.org/packages/84/7d/ef19b1db0f45b151ac78c65127235239a8cf21a59d1ce8507ce03e89a30b/contourpy-1.3.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2ba94a401342fc0f8b948e57d977557fbf4d515f03c67682dd5c6191cb2d16ec", size = 318906 },
+ { url = "https://files.pythonhosted.org/packages/ba/99/6794142b90b853a9155316c8f470d2e4821fe6f086b03e372aca848227dd/contourpy-1.3.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:efa874e87e4a647fd2e4f514d5e91c7d493697127beb95e77d2f7561f6905bd9", size = 323622 },
+ { url = "https://files.pythonhosted.org/packages/3c/0f/37d2c84a900cd8eb54e105f4fa9aebd275e14e266736778bb5dccbf3bbbb/contourpy-1.3.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1bf98051f1045b15c87868dbaea84f92408337d4f81d0e449ee41920ea121d3b", size = 1266699 },
+ { url = "https://files.pythonhosted.org/packages/3a/8a/deb5e11dc7d9cc8f0f9c8b29d4f062203f3af230ba83c30a6b161a6effc9/contourpy-1.3.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:61332c87493b00091423e747ea78200659dc09bdf7fd69edd5e98cef5d3e9a8d", size = 1326395 },
+ { url = "https://files.pythonhosted.org/packages/1a/35/7e267ae7c13aaf12322ccc493531f1e7f2eb8fba2927b9d7a05ff615df7a/contourpy-1.3.1-cp312-cp312-win32.whl", hash = "sha256:e914a8cb05ce5c809dd0fe350cfbb4e881bde5e2a38dc04e3afe1b3e58bd158e", size = 175354 },
+ { url = "https://files.pythonhosted.org/packages/a1/35/c2de8823211d07e8a79ab018ef03960716c5dff6f4d5bff5af87fd682992/contourpy-1.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:08d9d449a61cf53033612cb368f3a1b26cd7835d9b8cd326647efe43bca7568d", size = 220971 },
+ { url = "https://files.pythonhosted.org/packages/9a/e7/de62050dce687c5e96f946a93546910bc67e483fe05324439e329ff36105/contourpy-1.3.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a761d9ccfc5e2ecd1bf05534eda382aa14c3e4f9205ba5b1684ecfe400716ef2", size = 271548 },
+ { url = "https://files.pythonhosted.org/packages/78/4d/c2a09ae014ae984c6bdd29c11e74d3121b25eaa117eca0bb76340efd7e1c/contourpy-1.3.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:523a8ee12edfa36f6d2a49407f705a6ef4c5098de4f498619787e272de93f2d5", size = 255576 },
+ { url = "https://files.pythonhosted.org/packages/ab/8a/915380ee96a5638bda80cd061ccb8e666bfdccea38d5741cb69e6dbd61fc/contourpy-1.3.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece6df05e2c41bd46776fbc712e0996f7c94e0d0543af1656956d150c4ca7c81", size = 306635 },
+ { url = "https://files.pythonhosted.org/packages/29/5c/c83ce09375428298acd4e6582aeb68b1e0d1447f877fa993d9bf6cd3b0a0/contourpy-1.3.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:573abb30e0e05bf31ed067d2f82500ecfdaec15627a59d63ea2d95714790f5c2", size = 345925 },
+ { url = "https://files.pythonhosted.org/packages/29/63/5b52f4a15e80c66c8078a641a3bfacd6e07106835682454647aca1afc852/contourpy-1.3.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a9fa36448e6a3a1a9a2ba23c02012c43ed88905ec80163f2ffe2421c7192a5d7", size = 318000 },
+ { url = "https://files.pythonhosted.org/packages/9a/e2/30ca086c692691129849198659bf0556d72a757fe2769eb9620a27169296/contourpy-1.3.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ea9924d28fc5586bf0b42d15f590b10c224117e74409dd7a0be3b62b74a501c", size = 322689 },
+ { url = "https://files.pythonhosted.org/packages/6b/77/f37812ef700f1f185d348394debf33f22d531e714cf6a35d13d68a7003c7/contourpy-1.3.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5b75aa69cb4d6f137b36f7eb2ace9280cfb60c55dc5f61c731fdf6f037f958a3", size = 1268413 },
+ { url = "https://files.pythonhosted.org/packages/3f/6d/ce84e79cdd128542ebeb268f84abb4b093af78e7f8ec504676673d2675bc/contourpy-1.3.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:041b640d4ec01922083645a94bb3b2e777e6b626788f4095cf21abbe266413c1", size = 1326530 },
+ { url = "https://files.pythonhosted.org/packages/72/22/8282f4eae20c73c89bee7a82a19c4e27af9b57bb602ecaa00713d5bdb54d/contourpy-1.3.1-cp313-cp313-win32.whl", hash = "sha256:36987a15e8ace5f58d4d5da9dca82d498c2bbb28dff6e5d04fbfcc35a9cb3a82", size = 175315 },
+ { url = "https://files.pythonhosted.org/packages/e3/d5/28bca491f65312b438fbf076589dcde7f6f966b196d900777f5811b9c4e2/contourpy-1.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:a7895f46d47671fa7ceec40f31fae721da51ad34bdca0bee83e38870b1f47ffd", size = 220987 },
+ { url = "https://files.pythonhosted.org/packages/2f/24/a4b285d6adaaf9746e4700932f579f1a7b6f9681109f694cfa233ae75c4e/contourpy-1.3.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:9ddeb796389dadcd884c7eb07bd14ef12408aaae358f0e2ae24114d797eede30", size = 285001 },
+ { url = "https://files.pythonhosted.org/packages/48/1d/fb49a401b5ca4f06ccf467cd6c4f1fd65767e63c21322b29b04ec40b40b9/contourpy-1.3.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:19c1555a6801c2f084c7ddc1c6e11f02eb6a6016ca1318dd5452ba3f613a1751", size = 268553 },
+ { url = "https://files.pythonhosted.org/packages/79/1e/4aef9470d13fd029087388fae750dccb49a50c012a6c8d1d634295caa644/contourpy-1.3.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:841ad858cff65c2c04bf93875e384ccb82b654574a6d7f30453a04f04af71342", size = 310386 },
+ { url = "https://files.pythonhosted.org/packages/b0/34/910dc706ed70153b60392b5305c708c9810d425bde12499c9184a1100888/contourpy-1.3.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4318af1c925fb9a4fb190559ef3eec206845f63e80fb603d47f2d6d67683901c", size = 349806 },
+ { url = "https://files.pythonhosted.org/packages/31/3c/faee6a40d66d7f2a87f7102236bf4780c57990dd7f98e5ff29881b1b1344/contourpy-1.3.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:14c102b0eab282427b662cb590f2e9340a9d91a1c297f48729431f2dcd16e14f", size = 321108 },
+ { url = "https://files.pythonhosted.org/packages/17/69/390dc9b20dd4bb20585651d7316cc3054b7d4a7b4f8b710b2b698e08968d/contourpy-1.3.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:05e806338bfeaa006acbdeba0ad681a10be63b26e1b17317bfac3c5d98f36cda", size = 327291 },
+ { url = "https://files.pythonhosted.org/packages/ef/74/7030b67c4e941fe1e5424a3d988080e83568030ce0355f7c9fc556455b01/contourpy-1.3.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4d76d5993a34ef3df5181ba3c92fabb93f1eaa5729504fb03423fcd9f3177242", size = 1263752 },
+ { url = "https://files.pythonhosted.org/packages/f0/ed/92d86f183a8615f13f6b9cbfc5d4298a509d6ce433432e21da838b4b63f4/contourpy-1.3.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:89785bb2a1980c1bd87f0cb1517a71cde374776a5f150936b82580ae6ead44a1", size = 1318403 },
+ { url = "https://files.pythonhosted.org/packages/b3/0e/c8e4950c77dcfc897c71d61e56690a0a9df39543d2164040301b5df8e67b/contourpy-1.3.1-cp313-cp313t-win32.whl", hash = "sha256:8eb96e79b9f3dcadbad2a3891672f81cdcab7f95b27f28f1c67d75f045b6b4f1", size = 185117 },
+ { url = "https://files.pythonhosted.org/packages/c1/31/1ae946f11dfbd229222e6d6ad8e7bd1891d3d48bde5fbf7a0beb9491f8e3/contourpy-1.3.1-cp313-cp313t-win_amd64.whl", hash = "sha256:287ccc248c9e0d0566934e7d606201abd74761b5703d804ff3df8935f523d546", size = 236668 },
+ { url = "https://files.pythonhosted.org/packages/3e/4f/e56862e64b52b55b5ddcff4090085521fc228ceb09a88390a2b103dccd1b/contourpy-1.3.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:b457d6430833cee8e4b8e9b6f07aa1c161e5e0d52e118dc102c8f9bd7dd060d6", size = 265605 },
+ { url = "https://files.pythonhosted.org/packages/b0/2e/52bfeeaa4541889f23d8eadc6386b442ee2470bd3cff9baa67deb2dd5c57/contourpy-1.3.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cb76c1a154b83991a3cbbf0dfeb26ec2833ad56f95540b442c73950af2013750", size = 315040 },
+ { url = "https://files.pythonhosted.org/packages/52/94/86bfae441707205634d80392e873295652fc313dfd93c233c52c4dc07874/contourpy-1.3.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:44a29502ca9c7b5ba389e620d44f2fbe792b1fb5734e8b931ad307071ec58c53", size = 218221 },
+]
+
+[[package]]
+name = "cycler"
+version = "0.12.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/a9/95/a3dbbb5028f35eafb79008e7522a75244477d2838f38cbb722248dabc2a8/cycler-0.12.1.tar.gz", hash = "sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c", size = 7615 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30", size = 8321 },
+]
+
+[[package]]
+name = "dad"
+version = "0.1.0"
+source = { editable = "." }
+dependencies = [
+ { name = "einops" },
+ { name = "numpy" },
+ { name = "opencv-python" },
+ { name = "pillow" },
+ { name = "torch" },
+ { name = "torchvision" },
+]
+
+[package.dev-dependencies]
+dev = [
+ { name = "h5py" },
+ { name = "matplotlib" },
+ { name = "opencv-contrib-python" },
+ { name = "poselib" },
+ { name = "pyhesaff" },
+ { name = "romatch" },
+ { name = "ruff" },
+ { name = "tqdm" },
+ { name = "wandb" },
+]
+
+[package.metadata]
+requires-dist = [
+ { name = "einops", specifier = ">=0.8.1" },
+ { name = "numpy", specifier = ">=1.24" },
+ { name = "opencv-python", specifier = ">=4.11.0.86" },
+ { name = "pillow", specifier = ">=11.1.0" },
+ { name = "torch", specifier = ">=2.4" },
+ { name = "torchvision", specifier = ">=0.21.0" },
+]
+
+[package.metadata.requires-dev]
+dev = [
+ { name = "h5py", specifier = ">=3.13.0" },
+ { name = "matplotlib", specifier = ">=3.10.1" },
+ { name = "opencv-contrib-python", specifier = ">=4.11.0.86" },
+ { name = "poselib", specifier = "==2.0.4" },
+ { name = "pyhesaff", specifier = ">=2.1.1" },
+ { name = "romatch", git = "https://github.com/Parskatt/RoMa.git" },
+ { name = "ruff", specifier = ">=0.9.10" },
+ { name = "tqdm", specifier = ">=4.67.1" },
+ { name = "wandb", specifier = ">=0.19.8" },
+]
+
+[[package]]
+name = "docker-pycreds"
+version = "0.4.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "six" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/c5/e6/d1f6c00b7221e2d7c4b470132c931325c8b22c51ca62417e300f5ce16009/docker-pycreds-0.4.0.tar.gz", hash = "sha256:6ce3270bcaf404cc4c3e27e4b6c70d3521deae82fb508767870fdbf772d584d4", size = 8754 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/f5/e8/f6bd1eee09314e7e6dee49cbe2c5e22314ccdb38db16c9fc72d2fa80d054/docker_pycreds-0.4.0-py2.py3-none-any.whl", hash = "sha256:7266112468627868005106ec19cd0d722702d2b7d5912a28e19b826c3d37af49", size = 8982 },
+]
+
+[[package]]
+name = "einops"
+version = "0.8.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/e5/81/df4fbe24dff8ba3934af99044188e20a98ed441ad17a274539b74e82e126/einops-0.8.1.tar.gz", hash = "sha256:de5d960a7a761225532e0f1959e5315ebeafc0cd43394732f103ca44b9837e84", size = 54805 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/87/62/9773de14fe6c45c23649e98b83231fffd7b9892b6cf863251dc2afa73643/einops-0.8.1-py3-none-any.whl", hash = "sha256:919387eb55330f5757c6bea9165c5ff5cfe63a642682ea788a6d472576d81737", size = 64359 },
+]
+
+[[package]]
+name = "filelock"
+version = "3.17.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/dc/9c/0b15fb47b464e1b663b1acd1253a062aa5feecb07d4e597daea542ebd2b5/filelock-3.17.0.tar.gz", hash = "sha256:ee4e77401ef576ebb38cd7f13b9b28893194acc20a8e68e18730ba9c0e54660e", size = 18027 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/89/ec/00d68c4ddfedfe64159999e5f8a98fb8442729a63e2077eb9dcd89623d27/filelock-3.17.0-py3-none-any.whl", hash = "sha256:533dc2f7ba78dc2f0f531fc6c4940addf7b70a481e269a5a3b93be94ffbe8338", size = 16164 },
+]
+
+[[package]]
+name = "fonttools"
+version = "4.56.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/1c/8c/9ffa2a555af0e5e5d0e2ed7fdd8c9bef474ed676995bb4c57c9cd0014248/fonttools-4.56.0.tar.gz", hash = "sha256:a114d1567e1a1586b7e9e7fc2ff686ca542a82769a296cef131e4c4af51e58f4", size = 3462892 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/1e/5e/6ac30c2cc6a29454260f13c9c6422fc509b7982c13cd4597041260d8f482/fonttools-4.56.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:331954d002dbf5e704c7f3756028e21db07097c19722569983ba4d74df014000", size = 2752190 },
+ { url = "https://files.pythonhosted.org/packages/92/3a/ac382a8396d1b420ee45eeb0f65b614a9ca7abbb23a1b17524054f0f2200/fonttools-4.56.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8d1613abd5af2f93c05867b3a3759a56e8bf97eb79b1da76b2bc10892f96ff16", size = 2280624 },
+ { url = "https://files.pythonhosted.org/packages/8a/ae/00b58bfe20e9ff7fbc3dda38f5d127913942b5e252288ea9583099a31bf5/fonttools-4.56.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:705837eae384fe21cee5e5746fd4f4b2f06f87544fa60f60740007e0aa600311", size = 4562074 },
+ { url = "https://files.pythonhosted.org/packages/46/d0/0004ca8f6a200252e5bd6982ed99b5fe58c4c59efaf5f516621c4cd8f703/fonttools-4.56.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc871904a53a9d4d908673c6faa15689874af1c7c5ac403a8e12d967ebd0c0dc", size = 4604747 },
+ { url = "https://files.pythonhosted.org/packages/45/ea/c8862bd3e09d143ef8ed8268ec8a7d477828f960954889e65288ac050b08/fonttools-4.56.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:38b947de71748bab150259ee05a775e8a0635891568e9fdb3cdd7d0e0004e62f", size = 4559025 },
+ { url = "https://files.pythonhosted.org/packages/8f/75/bb88a9552ec1de31a414066257bfd9f40f4ada00074f7a3799ea39b5741f/fonttools-4.56.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:86b2a1013ef7a64d2e94606632683f07712045ed86d937c11ef4dde97319c086", size = 4728482 },
+ { url = "https://files.pythonhosted.org/packages/2a/5f/80a2b640df1e1bb7d459d62c8b3f37fe83fd413897e549106d4ebe6371f5/fonttools-4.56.0-cp310-cp310-win32.whl", hash = "sha256:133bedb9a5c6376ad43e6518b7e2cd2f866a05b1998f14842631d5feb36b5786", size = 2155557 },
+ { url = "https://files.pythonhosted.org/packages/8f/85/0904f9dbe51ac70d878d3242a8583b9453a09105c3ed19c6301247fd0d3a/fonttools-4.56.0-cp310-cp310-win_amd64.whl", hash = "sha256:17f39313b649037f6c800209984a11fc256a6137cbe5487091c6c7187cae4685", size = 2200017 },
+ { url = "https://files.pythonhosted.org/packages/35/56/a2f3e777d48fcae7ecd29de4d96352d84e5ea9871e5f3fc88241521572cf/fonttools-4.56.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7ef04bc7827adb7532be3d14462390dd71287644516af3f1e67f1e6ff9c6d6df", size = 2753325 },
+ { url = "https://files.pythonhosted.org/packages/71/85/d483e9c4e5ed586b183bf037a353e8d766366b54fd15519b30e6178a6a6e/fonttools-4.56.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ffda9b8cd9cb8b301cae2602ec62375b59e2e2108a117746f12215145e3f786c", size = 2281554 },
+ { url = "https://files.pythonhosted.org/packages/09/67/060473b832b2fade03c127019794df6dc02d9bc66fa4210b8e0d8a99d1e5/fonttools-4.56.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e2e993e8db36306cc3f1734edc8ea67906c55f98683d6fd34c3fc5593fdbba4c", size = 4869260 },
+ { url = "https://files.pythonhosted.org/packages/28/e9/47c02d5a7027e8ed841ab6a10ca00c93dadd5f16742f1af1fa3f9978adf4/fonttools-4.56.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:003548eadd674175510773f73fb2060bb46adb77c94854af3e0cc5bc70260049", size = 4898508 },
+ { url = "https://files.pythonhosted.org/packages/bf/8a/221d456d1afb8ca043cfd078f59f187ee5d0a580f4b49351b9ce95121f57/fonttools-4.56.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bd9825822e7bb243f285013e653f6741954d8147427aaa0324a862cdbf4cbf62", size = 4877700 },
+ { url = "https://files.pythonhosted.org/packages/a4/8c/e503863adf7a6aeff7b960e2f66fa44dd0c29a7a8b79765b2821950d7b05/fonttools-4.56.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b23d30a2c0b992fb1c4f8ac9bfde44b5586d23457759b6cf9a787f1a35179ee0", size = 5045817 },
+ { url = "https://files.pythonhosted.org/packages/2b/50/79ba3b7e42f4eaa70b82b9e79155f0f6797858dc8a97862428b6852c6aee/fonttools-4.56.0-cp311-cp311-win32.whl", hash = "sha256:47b5e4680002ae1756d3ae3b6114e20aaee6cc5c69d1e5911f5ffffd3ee46c6b", size = 2154426 },
+ { url = "https://files.pythonhosted.org/packages/3b/90/4926e653041c4116ecd43e50e3c79f5daae6dcafc58ceb64bc4f71dd4924/fonttools-4.56.0-cp311-cp311-win_amd64.whl", hash = "sha256:14a3e3e6b211660db54ca1ef7006401e4a694e53ffd4553ab9bc87ead01d0f05", size = 2200937 },
+ { url = "https://files.pythonhosted.org/packages/39/32/71cfd6877999576a11824a7fe7bc0bb57c5c72b1f4536fa56a3e39552643/fonttools-4.56.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d6f195c14c01bd057bc9b4f70756b510e009c83c5ea67b25ced3e2c38e6ee6e9", size = 2747757 },
+ { url = "https://files.pythonhosted.org/packages/15/52/d9f716b072c5061a0b915dd4c387f74bef44c68c069e2195c753905bd9b7/fonttools-4.56.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fa760e5fe8b50cbc2d71884a1eff2ed2b95a005f02dda2fa431560db0ddd927f", size = 2279007 },
+ { url = "https://files.pythonhosted.org/packages/d1/97/f1b3a8afa9a0d814a092a25cd42f59ccb98a0bb7a295e6e02fc9ba744214/fonttools-4.56.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d54a45d30251f1d729e69e5b675f9a08b7da413391a1227781e2a297fa37f6d2", size = 4783991 },
+ { url = "https://files.pythonhosted.org/packages/95/70/2a781bedc1c45a0c61d29c56425609b22ed7f971da5d7e5df2679488741b/fonttools-4.56.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:661a8995d11e6e4914a44ca7d52d1286e2d9b154f685a4d1f69add8418961563", size = 4855109 },
+ { url = "https://files.pythonhosted.org/packages/0c/02/a2597858e61a5e3fb6a14d5f6be9e6eb4eaf090da56ad70cedcbdd201685/fonttools-4.56.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9d94449ad0a5f2a8bf5d2f8d71d65088aee48adbe45f3c5f8e00e3ad861ed81a", size = 4762496 },
+ { url = "https://files.pythonhosted.org/packages/f2/00/aaf00100d6078fdc73f7352b44589804af9dc12b182a2540b16002152ba4/fonttools-4.56.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f59746f7953f69cc3290ce2f971ab01056e55ddd0fb8b792c31a8acd7fee2d28", size = 4990094 },
+ { url = "https://files.pythonhosted.org/packages/bf/dc/3ff1db522460db60cf3adaf1b64e0c72b43406717d139786d3fa1eb20709/fonttools-4.56.0-cp312-cp312-win32.whl", hash = "sha256:bce60f9a977c9d3d51de475af3f3581d9b36952e1f8fc19a1f2254f1dda7ce9c", size = 2142888 },
+ { url = "https://files.pythonhosted.org/packages/6f/e3/5a181a85777f7809076e51f7422e0dc77eb04676c40ec8bf6a49d390d1ff/fonttools-4.56.0-cp312-cp312-win_amd64.whl", hash = "sha256:300c310bb725b2bdb4f5fc7e148e190bd69f01925c7ab437b9c0ca3e1c7cd9ba", size = 2189734 },
+ { url = "https://files.pythonhosted.org/packages/a5/55/f06b48d48e0b4ec3a3489efafe9bd4d81b6e0802ac51026e3ee4634e89ba/fonttools-4.56.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f20e2c0dfab82983a90f3d00703ac0960412036153e5023eed2b4641d7d5e692", size = 2735127 },
+ { url = "https://files.pythonhosted.org/packages/59/db/d2c7c9b6dd5cbd46f183e650a47403ffb88fca17484eb7c4b1cd88f9e513/fonttools-4.56.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f36a0868f47b7566237640c026c65a86d09a3d9ca5df1cd039e30a1da73098a0", size = 2272519 },
+ { url = "https://files.pythonhosted.org/packages/4d/a2/da62d779c34a0e0c06415f02eab7fa3466de5d46df459c0275a255cefc65/fonttools-4.56.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:62b4c6802fa28e14dba010e75190e0e6228513573f1eeae57b11aa1a39b7e5b1", size = 4762423 },
+ { url = "https://files.pythonhosted.org/packages/be/6a/fd4018e0448c8a5e12138906411282c5eab51a598493f080a9f0960e658f/fonttools-4.56.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a05d1f07eb0a7d755fbe01fee1fd255c3a4d3730130cf1bfefb682d18fd2fcea", size = 4834442 },
+ { url = "https://files.pythonhosted.org/packages/6d/63/fa1dec8efb35bc11ef9c39b2d74754b45d48a3ccb2cf78c0109c0af639e8/fonttools-4.56.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0073b62c3438cf0058488c002ea90489e8801d3a7af5ce5f7c05c105bee815c3", size = 4742800 },
+ { url = "https://files.pythonhosted.org/packages/dd/f4/963247ae8c73ccc4cf2929e7162f595c81dbe17997d1d0ea77da24a217c9/fonttools-4.56.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e2cad98c94833465bcf28f51c248aaf07ca022efc6a3eba750ad9c1e0256d278", size = 4963746 },
+ { url = "https://files.pythonhosted.org/packages/ea/e0/46f9600c39c644b54e4420f941f75fa200d9288c9ae171e5d80918b8cbb9/fonttools-4.56.0-cp313-cp313-win32.whl", hash = "sha256:d0cb73ccf7f6d7ca8d0bc7ea8ac0a5b84969a41c56ac3ac3422a24df2680546f", size = 2140927 },
+ { url = "https://files.pythonhosted.org/packages/27/6d/3edda54f98a550a0473f032d8050315fbc8f1b76a0d9f3879b72ebb2cdd6/fonttools-4.56.0-cp313-cp313-win_amd64.whl", hash = "sha256:62cc1253827d1e500fde9dbe981219fea4eb000fd63402283472d38e7d8aa1c6", size = 2186709 },
+ { url = "https://files.pythonhosted.org/packages/bf/ff/44934a031ce5a39125415eb405b9efb76fe7f9586b75291d66ae5cbfc4e6/fonttools-4.56.0-py3-none-any.whl", hash = "sha256:1088182f68c303b50ca4dc0c82d42083d176cba37af1937e1a976a31149d4d14", size = 1089800 },
+]
+
+[[package]]
+name = "fsspec"
+version = "2025.3.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/34/f4/5721faf47b8c499e776bc34c6a8fc17efdf7fdef0b00f398128bc5dcb4ac/fsspec-2025.3.0.tar.gz", hash = "sha256:a935fd1ea872591f2b5148907d103488fc523295e6c64b835cfad8c3eca44972", size = 298491 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/56/53/eb690efa8513166adef3e0669afd31e95ffde69fb3c52ec2ac7223ed6018/fsspec-2025.3.0-py3-none-any.whl", hash = "sha256:efb87af3efa9103f94ca91a7f8cb7a4df91af9f74fc106c9c7ea0efd7277c1b3", size = 193615 },
+]
+
+[[package]]
+name = "gitdb"
+version = "4.0.12"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "smmap" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/72/94/63b0fc47eb32792c7ba1fe1b694daec9a63620db1e313033d18140c2320a/gitdb-4.0.12.tar.gz", hash = "sha256:5ef71f855d191a3326fcfbc0d5da835f26b13fbcba60c32c21091c349ffdb571", size = 394684 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a0/61/5c78b91c3143ed5c14207f463aecfc8f9dbb5092fb2869baf37c273b2705/gitdb-4.0.12-py3-none-any.whl", hash = "sha256:67073e15955400952c6565cc3e707c554a4eea2e428946f7a4c162fab9bd9bcf", size = 62794 },
+]
+
+[[package]]
+name = "gitpython"
+version = "3.1.44"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "gitdb" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/c0/89/37df0b71473153574a5cdef8f242de422a0f5d26d7a9e231e6f169b4ad14/gitpython-3.1.44.tar.gz", hash = "sha256:c87e30b26253bf5418b01b0660f818967f3c503193838337fe5e573331249269", size = 214196 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/1d/9a/4114a9057db2f1462d5c8f8390ab7383925fe1ac012eaa42402ad65c2963/GitPython-3.1.44-py3-none-any.whl", hash = "sha256:9e0e10cda9bed1ee64bc9a6de50e7e38a9c9943241cd7f585f6df3ed28011110", size = 207599 },
+]
+
+[[package]]
+name = "h5py"
+version = "3.13.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "numpy" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/03/2e/a22d6a8bfa6f8be33e7febd985680fba531562795f0a9077ed1eb047bfb0/h5py-3.13.0.tar.gz", hash = "sha256:1870e46518720023da85d0895a1960ff2ce398c5671eac3b1a41ec696b7105c3", size = 414876 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/02/8a/bc76588ff1a254e939ce48f30655a8f79fac614ca8bd1eda1a79fa276671/h5py-3.13.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5540daee2b236d9569c950b417f13fd112d51d78b4c43012de05774908dff3f5", size = 3413286 },
+ { url = "https://files.pythonhosted.org/packages/19/bd/9f249ecc6c517b2796330b0aab7d2351a108fdbd00d4bb847c0877b5533e/h5py-3.13.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:10894c55d46df502d82a7a4ed38f9c3fdbcb93efb42e25d275193e093071fade", size = 2915673 },
+ { url = "https://files.pythonhosted.org/packages/72/71/0dd079208d7d3c3988cebc0776c2de58b4d51d8eeb6eab871330133dfee6/h5py-3.13.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb267ce4b83f9c42560e9ff4d30f60f7ae492eacf9c7ede849edf8c1b860e16b", size = 4283822 },
+ { url = "https://files.pythonhosted.org/packages/d8/fa/0b6a59a1043c53d5d287effa02303bd248905ee82b25143c7caad8b340ad/h5py-3.13.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2cf6a231a07c14acd504a945a6e9ec115e0007f675bde5e0de30a4dc8d86a31", size = 4548100 },
+ { url = "https://files.pythonhosted.org/packages/12/42/ad555a7ff7836c943fe97009405566dc77bcd2a17816227c10bd067a3ee1/h5py-3.13.0-cp310-cp310-win_amd64.whl", hash = "sha256:851ae3a8563d87a5a0dc49c2e2529c75b8842582ccaefbf84297d2cfceeacd61", size = 2950547 },
+ { url = "https://files.pythonhosted.org/packages/86/2b/50b15fdefb577d073b49699e6ea6a0a77a3a1016c2b67e2149fc50124a10/h5py-3.13.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8a8e38ef4ceb969f832cc230c0cf808c613cc47e31e768fd7b1106c55afa1cb8", size = 3422922 },
+ { url = "https://files.pythonhosted.org/packages/94/59/36d87a559cab9c59b59088d52e86008d27a9602ce3afc9d3b51823014bf3/h5py-3.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f35640e81b03c02a88b8bf99fb6a9d3023cc52f7c627694db2f379e0028f2868", size = 2921619 },
+ { url = "https://files.pythonhosted.org/packages/37/ef/6f80b19682c0b0835bbee7b253bec9c16af9004f2fd6427b1dd858100273/h5py-3.13.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:337af114616f3656da0c83b68fcf53ecd9ce9989a700b0883a6e7c483c3235d4", size = 4259366 },
+ { url = "https://files.pythonhosted.org/packages/03/71/c99f662d4832c8835453cf3476f95daa28372023bda4aa1fca9e97c24f09/h5py-3.13.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:782ff0ac39f455f21fd1c8ebc007328f65f43d56718a89327eec76677ebf238a", size = 4509058 },
+ { url = "https://files.pythonhosted.org/packages/56/89/e3ff23e07131ff73a72a349be9639e4de84e163af89c1c218b939459a98a/h5py-3.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:22ffe2a25770a2d67213a1b94f58006c14dce06933a42d2aaa0318c5868d1508", size = 2966428 },
+ { url = "https://files.pythonhosted.org/packages/d8/20/438f6366ba4ded80eadb38f8927f5e2cd6d2e087179552f20ae3dbcd5d5b/h5py-3.13.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:477c58307b6b9a2509c59c57811afb9f598aedede24a67da808262dfa0ee37b4", size = 3384442 },
+ { url = "https://files.pythonhosted.org/packages/10/13/cc1cb7231399617d9951233eb12fddd396ff5d4f7f057ee5d2b1ca0ee7e7/h5py-3.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:57c4c74f627c616f02b7aec608a8c706fe08cb5b0ba7c08555a4eb1dde20805a", size = 2917567 },
+ { url = "https://files.pythonhosted.org/packages/9e/d9/aed99e1c858dc698489f916eeb7c07513bc864885d28ab3689d572ba0ea0/h5py-3.13.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:357e6dc20b101a805ccfd0024731fbaf6e8718c18c09baf3b5e4e9d198d13fca", size = 4669544 },
+ { url = "https://files.pythonhosted.org/packages/a7/da/3c137006ff5f0433f0fb076b1ebe4a7bf7b5ee1e8811b5486af98b500dd5/h5py-3.13.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d6f13f9b5ce549448c01e4dfe08ea8d1772e6078799af2c1c8d09e941230a90d", size = 4932139 },
+ { url = "https://files.pythonhosted.org/packages/25/61/d897952629cae131c19d4c41b2521e7dd6382f2d7177c87615c2e6dced1a/h5py-3.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:21daf38171753899b5905f3d82c99b0b1ec2cbbe282a037cad431feb620e62ec", size = 2954179 },
+ { url = "https://files.pythonhosted.org/packages/60/43/f276f27921919a9144074320ce4ca40882fc67b3cfee81c3f5c7df083e97/h5py-3.13.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e520ec76de00943dd017c8ea3f354fa1d2f542eac994811943a8faedf2a7d5cb", size = 3358040 },
+ { url = "https://files.pythonhosted.org/packages/1b/86/ad4a4cf781b08d4572be8bbdd8f108bb97b266a14835c640dc43dafc0729/h5py-3.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e79d8368cd9295045956bfb436656bea3f915beaa11d342e9f79f129f5178763", size = 2892766 },
+ { url = "https://files.pythonhosted.org/packages/69/84/4c6367d6b58deaf0fa84999ec819e7578eee96cea6cbd613640d0625ed5e/h5py-3.13.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:56dd172d862e850823c4af02dc4ddbc308f042b85472ffdaca67f1598dff4a57", size = 4664255 },
+ { url = "https://files.pythonhosted.org/packages/fd/41/bc2df86b72965775f6d621e0ee269a5f3ac23e8f870abf519de9c7d93b4d/h5py-3.13.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:be949b46b7388074c5acae017fbbe3e5ba303fd9daaa52157fdfef30bbdacadd", size = 4927580 },
+ { url = "https://files.pythonhosted.org/packages/97/34/165b87ea55184770a0c1fcdb7e017199974ad2e271451fd045cfe35f3add/h5py-3.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:4f97ecde7ac6513b21cd95efdfc38dc6d19f96f6ca6f2a30550e94e551458e0a", size = 2940890 },
+]
+
+[[package]]
+name = "huggingface-hub"
+version = "0.29.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "filelock" },
+ { name = "fsspec" },
+ { name = "packaging" },
+ { name = "pyyaml" },
+ { name = "requests" },
+ { name = "tqdm" },
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/58/b2/f8b3c9842a794e8203448725aefa02d7c9e0da42d5f22f4ed806057cc36e/huggingface_hub-0.29.2.tar.gz", hash = "sha256:590b29c0dcbd0ee4b7b023714dc1ad8563fe4a68a91463438b74e980d28afaf3", size = 389816 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/13/5f/088ff08dc41808fcd99d9972b9bcfa7e3a35e30e8b0a3155b57938f1611c/huggingface_hub-0.29.2-py3-none-any.whl", hash = "sha256:c56f20fca09ef19da84dcde2b76379ecdaddf390b083f59f166715584953307d", size = 468087 },
+]
+
+[[package]]
+name = "idna"
+version = "3.10"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 },
+]
+
+[[package]]
+name = "jinja2"
+version = "3.1.6"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "markupsafe" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899 },
+]
+
+[[package]]
+name = "kiwisolver"
+version = "1.4.8"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/82/59/7c91426a8ac292e1cdd53a63b6d9439abd573c875c3f92c146767dd33faf/kiwisolver-1.4.8.tar.gz", hash = "sha256:23d5f023bdc8c7e54eb65f03ca5d5bb25b601eac4d7f1a042888a1f45237987e", size = 97538 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/47/5f/4d8e9e852d98ecd26cdf8eaf7ed8bc33174033bba5e07001b289f07308fd/kiwisolver-1.4.8-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:88c6f252f6816a73b1f8c904f7bbe02fd67c09a69f7cb8a0eecdbf5ce78e63db", size = 124623 },
+ { url = "https://files.pythonhosted.org/packages/1d/70/7f5af2a18a76fe92ea14675f8bd88ce53ee79e37900fa5f1a1d8e0b42998/kiwisolver-1.4.8-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c72941acb7b67138f35b879bbe85be0f6c6a70cab78fe3ef6db9c024d9223e5b", size = 66720 },
+ { url = "https://files.pythonhosted.org/packages/c6/13/e15f804a142353aefd089fadc8f1d985561a15358c97aca27b0979cb0785/kiwisolver-1.4.8-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ce2cf1e5688edcb727fdf7cd1bbd0b6416758996826a8be1d958f91880d0809d", size = 65413 },
+ { url = "https://files.pythonhosted.org/packages/ce/6d/67d36c4d2054e83fb875c6b59d0809d5c530de8148846b1370475eeeece9/kiwisolver-1.4.8-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:c8bf637892dc6e6aad2bc6d4d69d08764166e5e3f69d469e55427b6ac001b19d", size = 1650826 },
+ { url = "https://files.pythonhosted.org/packages/de/c6/7b9bb8044e150d4d1558423a1568e4f227193662a02231064e3824f37e0a/kiwisolver-1.4.8-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:034d2c891f76bd3edbdb3ea11140d8510dca675443da7304205a2eaa45d8334c", size = 1628231 },
+ { url = "https://files.pythonhosted.org/packages/b6/38/ad10d437563063eaaedbe2c3540a71101fc7fb07a7e71f855e93ea4de605/kiwisolver-1.4.8-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d47b28d1dfe0793d5e96bce90835e17edf9a499b53969b03c6c47ea5985844c3", size = 1408938 },
+ { url = "https://files.pythonhosted.org/packages/52/ce/c0106b3bd7f9e665c5f5bc1e07cc95b5dabd4e08e3dad42dbe2faad467e7/kiwisolver-1.4.8-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eb158fe28ca0c29f2260cca8c43005329ad58452c36f0edf298204de32a9a3ed", size = 1422799 },
+ { url = "https://files.pythonhosted.org/packages/d0/87/efb704b1d75dc9758087ba374c0f23d3254505edaedd09cf9d247f7878b9/kiwisolver-1.4.8-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d5536185fce131780ebd809f8e623bf4030ce1b161353166c49a3c74c287897f", size = 1354362 },
+ { url = "https://files.pythonhosted.org/packages/eb/b3/fd760dc214ec9a8f208b99e42e8f0130ff4b384eca8b29dd0efc62052176/kiwisolver-1.4.8-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:369b75d40abedc1da2c1f4de13f3482cb99e3237b38726710f4a793432b1c5ff", size = 2222695 },
+ { url = "https://files.pythonhosted.org/packages/a2/09/a27fb36cca3fc01700687cc45dae7a6a5f8eeb5f657b9f710f788748e10d/kiwisolver-1.4.8-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:641f2ddf9358c80faa22e22eb4c9f54bd3f0e442e038728f500e3b978d00aa7d", size = 2370802 },
+ { url = "https://files.pythonhosted.org/packages/3d/c3/ba0a0346db35fe4dc1f2f2cf8b99362fbb922d7562e5f911f7ce7a7b60fa/kiwisolver-1.4.8-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:d561d2d8883e0819445cfe58d7ddd673e4015c3c57261d7bdcd3710d0d14005c", size = 2334646 },
+ { url = "https://files.pythonhosted.org/packages/41/52/942cf69e562f5ed253ac67d5c92a693745f0bed3c81f49fc0cbebe4d6b00/kiwisolver-1.4.8-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:1732e065704b47c9afca7ffa272f845300a4eb959276bf6970dc07265e73b605", size = 2467260 },
+ { url = "https://files.pythonhosted.org/packages/32/26/2d9668f30d8a494b0411d4d7d4ea1345ba12deb6a75274d58dd6ea01e951/kiwisolver-1.4.8-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:bcb1ebc3547619c3b58a39e2448af089ea2ef44b37988caf432447374941574e", size = 2288633 },
+ { url = "https://files.pythonhosted.org/packages/98/99/0dd05071654aa44fe5d5e350729961e7bb535372935a45ac89a8924316e6/kiwisolver-1.4.8-cp310-cp310-win_amd64.whl", hash = "sha256:89c107041f7b27844179ea9c85d6da275aa55ecf28413e87624d033cf1f6b751", size = 71885 },
+ { url = "https://files.pythonhosted.org/packages/6c/fc/822e532262a97442989335394d441cd1d0448c2e46d26d3e04efca84df22/kiwisolver-1.4.8-cp310-cp310-win_arm64.whl", hash = "sha256:b5773efa2be9eb9fcf5415ea3ab70fc785d598729fd6057bea38d539ead28271", size = 65175 },
+ { url = "https://files.pythonhosted.org/packages/da/ed/c913ee28936c371418cb167b128066ffb20bbf37771eecc2c97edf8a6e4c/kiwisolver-1.4.8-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a4d3601908c560bdf880f07d94f31d734afd1bb71e96585cace0e38ef44c6d84", size = 124635 },
+ { url = "https://files.pythonhosted.org/packages/4c/45/4a7f896f7467aaf5f56ef093d1f329346f3b594e77c6a3c327b2d415f521/kiwisolver-1.4.8-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:856b269c4d28a5c0d5e6c1955ec36ebfd1651ac00e1ce0afa3e28da95293b561", size = 66717 },
+ { url = "https://files.pythonhosted.org/packages/5f/b4/c12b3ac0852a3a68f94598d4c8d569f55361beef6159dce4e7b624160da2/kiwisolver-1.4.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c2b9a96e0f326205af81a15718a9073328df1173a2619a68553decb7097fd5d7", size = 65413 },
+ { url = "https://files.pythonhosted.org/packages/a9/98/1df4089b1ed23d83d410adfdc5947245c753bddfbe06541c4aae330e9e70/kiwisolver-1.4.8-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c5020c83e8553f770cb3b5fc13faac40f17e0b205bd237aebd21d53d733adb03", size = 1343994 },
+ { url = "https://files.pythonhosted.org/packages/8d/bf/b4b169b050c8421a7c53ea1ea74e4ef9c335ee9013216c558a047f162d20/kiwisolver-1.4.8-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dace81d28c787956bfbfbbfd72fdcef014f37d9b48830829e488fdb32b49d954", size = 1434804 },
+ { url = "https://files.pythonhosted.org/packages/66/5a/e13bd341fbcf73325ea60fdc8af752addf75c5079867af2e04cc41f34434/kiwisolver-1.4.8-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:11e1022b524bd48ae56c9b4f9296bce77e15a2e42a502cceba602f804b32bb79", size = 1450690 },
+ { url = "https://files.pythonhosted.org/packages/9b/4f/5955dcb376ba4a830384cc6fab7d7547bd6759fe75a09564910e9e3bb8ea/kiwisolver-1.4.8-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b9b4d2892fefc886f30301cdd80debd8bb01ecdf165a449eb6e78f79f0fabd6", size = 1376839 },
+ { url = "https://files.pythonhosted.org/packages/3a/97/5edbed69a9d0caa2e4aa616ae7df8127e10f6586940aa683a496c2c280b9/kiwisolver-1.4.8-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a96c0e790ee875d65e340ab383700e2b4891677b7fcd30a699146f9384a2bb0", size = 1435109 },
+ { url = "https://files.pythonhosted.org/packages/13/fc/e756382cb64e556af6c1809a1bbb22c141bbc2445049f2da06b420fe52bf/kiwisolver-1.4.8-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:23454ff084b07ac54ca8be535f4174170c1094a4cff78fbae4f73a4bcc0d4dab", size = 2245269 },
+ { url = "https://files.pythonhosted.org/packages/76/15/e59e45829d7f41c776d138245cabae6515cb4eb44b418f6d4109c478b481/kiwisolver-1.4.8-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:87b287251ad6488e95b4f0b4a79a6d04d3ea35fde6340eb38fbd1ca9cd35bbbc", size = 2393468 },
+ { url = "https://files.pythonhosted.org/packages/e9/39/483558c2a913ab8384d6e4b66a932406f87c95a6080112433da5ed668559/kiwisolver-1.4.8-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:b21dbe165081142b1232a240fc6383fd32cdd877ca6cc89eab93e5f5883e1c25", size = 2355394 },
+ { url = "https://files.pythonhosted.org/packages/01/aa/efad1fbca6570a161d29224f14b082960c7e08268a133fe5dc0f6906820e/kiwisolver-1.4.8-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:768cade2c2df13db52475bd28d3a3fac8c9eff04b0e9e2fda0f3760f20b3f7fc", size = 2490901 },
+ { url = "https://files.pythonhosted.org/packages/c9/4f/15988966ba46bcd5ab9d0c8296914436720dd67fca689ae1a75b4ec1c72f/kiwisolver-1.4.8-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d47cfb2650f0e103d4bf68b0b5804c68da97272c84bb12850d877a95c056bd67", size = 2312306 },
+ { url = "https://files.pythonhosted.org/packages/2d/27/bdf1c769c83f74d98cbc34483a972f221440703054894a37d174fba8aa68/kiwisolver-1.4.8-cp311-cp311-win_amd64.whl", hash = "sha256:ed33ca2002a779a2e20eeb06aea7721b6e47f2d4b8a8ece979d8ba9e2a167e34", size = 71966 },
+ { url = "https://files.pythonhosted.org/packages/4a/c9/9642ea855604aeb2968a8e145fc662edf61db7632ad2e4fb92424be6b6c0/kiwisolver-1.4.8-cp311-cp311-win_arm64.whl", hash = "sha256:16523b40aab60426ffdebe33ac374457cf62863e330a90a0383639ce14bf44b2", size = 65311 },
+ { url = "https://files.pythonhosted.org/packages/fc/aa/cea685c4ab647f349c3bc92d2daf7ae34c8e8cf405a6dcd3a497f58a2ac3/kiwisolver-1.4.8-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d6af5e8815fd02997cb6ad9bbed0ee1e60014438ee1a5c2444c96f87b8843502", size = 124152 },
+ { url = "https://files.pythonhosted.org/packages/c5/0b/8db6d2e2452d60d5ebc4ce4b204feeb16176a851fd42462f66ade6808084/kiwisolver-1.4.8-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bade438f86e21d91e0cf5dd7c0ed00cda0f77c8c1616bd83f9fc157fa6760d31", size = 66555 },
+ { url = "https://files.pythonhosted.org/packages/60/26/d6a0db6785dd35d3ba5bf2b2df0aedc5af089962c6eb2cbf67a15b81369e/kiwisolver-1.4.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b83dc6769ddbc57613280118fb4ce3cd08899cc3369f7d0e0fab518a7cf37fdb", size = 65067 },
+ { url = "https://files.pythonhosted.org/packages/c9/ed/1d97f7e3561e09757a196231edccc1bcf59d55ddccefa2afc9c615abd8e0/kiwisolver-1.4.8-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:111793b232842991be367ed828076b03d96202c19221b5ebab421ce8bcad016f", size = 1378443 },
+ { url = "https://files.pythonhosted.org/packages/29/61/39d30b99954e6b46f760e6289c12fede2ab96a254c443639052d1b573fbc/kiwisolver-1.4.8-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:257af1622860e51b1a9d0ce387bf5c2c4f36a90594cb9514f55b074bcc787cfc", size = 1472728 },
+ { url = "https://files.pythonhosted.org/packages/0c/3e/804163b932f7603ef256e4a715e5843a9600802bb23a68b4e08c8c0ff61d/kiwisolver-1.4.8-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:69b5637c3f316cab1ec1c9a12b8c5f4750a4c4b71af9157645bf32830e39c03a", size = 1478388 },
+ { url = "https://files.pythonhosted.org/packages/8a/9e/60eaa75169a154700be74f875a4d9961b11ba048bef315fbe89cb6999056/kiwisolver-1.4.8-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:782bb86f245ec18009890e7cb8d13a5ef54dcf2ebe18ed65f795e635a96a1c6a", size = 1413849 },
+ { url = "https://files.pythonhosted.org/packages/bc/b3/9458adb9472e61a998c8c4d95cfdfec91c73c53a375b30b1428310f923e4/kiwisolver-1.4.8-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc978a80a0db3a66d25767b03688f1147a69e6237175c0f4ffffaaedf744055a", size = 1475533 },
+ { url = "https://files.pythonhosted.org/packages/e4/7a/0a42d9571e35798de80aef4bb43a9b672aa7f8e58643d7bd1950398ffb0a/kiwisolver-1.4.8-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:36dbbfd34838500a31f52c9786990d00150860e46cd5041386f217101350f0d3", size = 2268898 },
+ { url = "https://files.pythonhosted.org/packages/d9/07/1255dc8d80271400126ed8db35a1795b1a2c098ac3a72645075d06fe5c5d/kiwisolver-1.4.8-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:eaa973f1e05131de5ff3569bbba7f5fd07ea0595d3870ed4a526d486fe57fa1b", size = 2425605 },
+ { url = "https://files.pythonhosted.org/packages/84/df/5a3b4cf13780ef6f6942df67b138b03b7e79e9f1f08f57c49957d5867f6e/kiwisolver-1.4.8-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:a66f60f8d0c87ab7f59b6fb80e642ebb29fec354a4dfad687ca4092ae69d04f4", size = 2375801 },
+ { url = "https://files.pythonhosted.org/packages/8f/10/2348d068e8b0f635c8c86892788dac7a6b5c0cb12356620ab575775aad89/kiwisolver-1.4.8-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:858416b7fb777a53f0c59ca08190ce24e9abbd3cffa18886a5781b8e3e26f65d", size = 2520077 },
+ { url = "https://files.pythonhosted.org/packages/32/d8/014b89fee5d4dce157d814303b0fce4d31385a2af4c41fed194b173b81ac/kiwisolver-1.4.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:085940635c62697391baafaaeabdf3dd7a6c3643577dde337f4d66eba021b2b8", size = 2338410 },
+ { url = "https://files.pythonhosted.org/packages/bd/72/dfff0cc97f2a0776e1c9eb5bef1ddfd45f46246c6533b0191887a427bca5/kiwisolver-1.4.8-cp312-cp312-win_amd64.whl", hash = "sha256:01c3d31902c7db5fb6182832713d3b4122ad9317c2c5877d0539227d96bb2e50", size = 71853 },
+ { url = "https://files.pythonhosted.org/packages/dc/85/220d13d914485c0948a00f0b9eb419efaf6da81b7d72e88ce2391f7aed8d/kiwisolver-1.4.8-cp312-cp312-win_arm64.whl", hash = "sha256:a3c44cb68861de93f0c4a8175fbaa691f0aa22550c331fefef02b618a9dcb476", size = 65424 },
+ { url = "https://files.pythonhosted.org/packages/79/b3/e62464a652f4f8cd9006e13d07abad844a47df1e6537f73ddfbf1bc997ec/kiwisolver-1.4.8-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:1c8ceb754339793c24aee1c9fb2485b5b1f5bb1c2c214ff13368431e51fc9a09", size = 124156 },
+ { url = "https://files.pythonhosted.org/packages/8d/2d/f13d06998b546a2ad4f48607a146e045bbe48030774de29f90bdc573df15/kiwisolver-1.4.8-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:54a62808ac74b5e55a04a408cda6156f986cefbcf0ada13572696b507cc92fa1", size = 66555 },
+ { url = "https://files.pythonhosted.org/packages/59/e3/b8bd14b0a54998a9fd1e8da591c60998dc003618cb19a3f94cb233ec1511/kiwisolver-1.4.8-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:68269e60ee4929893aad82666821aaacbd455284124817af45c11e50a4b42e3c", size = 65071 },
+ { url = "https://files.pythonhosted.org/packages/f0/1c/6c86f6d85ffe4d0ce04228d976f00674f1df5dc893bf2dd4f1928748f187/kiwisolver-1.4.8-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:34d142fba9c464bc3bbfeff15c96eab0e7310343d6aefb62a79d51421fcc5f1b", size = 1378053 },
+ { url = "https://files.pythonhosted.org/packages/4e/b9/1c6e9f6dcb103ac5cf87cb695845f5fa71379021500153566d8a8a9fc291/kiwisolver-1.4.8-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ddc373e0eef45b59197de815b1b28ef89ae3955e7722cc9710fb91cd77b7f47", size = 1472278 },
+ { url = "https://files.pythonhosted.org/packages/ee/81/aca1eb176de671f8bda479b11acdc42c132b61a2ac861c883907dde6debb/kiwisolver-1.4.8-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:77e6f57a20b9bd4e1e2cedda4d0b986ebd0216236f0106e55c28aea3d3d69b16", size = 1478139 },
+ { url = "https://files.pythonhosted.org/packages/49/f4/e081522473671c97b2687d380e9e4c26f748a86363ce5af48b4a28e48d06/kiwisolver-1.4.8-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:08e77738ed7538f036cd1170cbed942ef749137b1311fa2bbe2a7fda2f6bf3cc", size = 1413517 },
+ { url = "https://files.pythonhosted.org/packages/8f/e9/6a7d025d8da8c4931522922cd706105aa32b3291d1add8c5427cdcd66e63/kiwisolver-1.4.8-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a5ce1e481a74b44dd5e92ff03ea0cb371ae7a0268318e202be06c8f04f4f1246", size = 1474952 },
+ { url = "https://files.pythonhosted.org/packages/82/13/13fa685ae167bee5d94b415991c4fc7bb0a1b6ebea6e753a87044b209678/kiwisolver-1.4.8-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:fc2ace710ba7c1dfd1a3b42530b62b9ceed115f19a1656adefce7b1782a37794", size = 2269132 },
+ { url = "https://files.pythonhosted.org/packages/ef/92/bb7c9395489b99a6cb41d502d3686bac692586db2045adc19e45ee64ed23/kiwisolver-1.4.8-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:3452046c37c7692bd52b0e752b87954ef86ee2224e624ef7ce6cb21e8c41cc1b", size = 2425997 },
+ { url = "https://files.pythonhosted.org/packages/ed/12/87f0e9271e2b63d35d0d8524954145837dd1a6c15b62a2d8c1ebe0f182b4/kiwisolver-1.4.8-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7e9a60b50fe8b2ec6f448fe8d81b07e40141bfced7f896309df271a0b92f80f3", size = 2376060 },
+ { url = "https://files.pythonhosted.org/packages/02/6e/c8af39288edbce8bf0fa35dee427b082758a4b71e9c91ef18fa667782138/kiwisolver-1.4.8-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:918139571133f366e8362fa4a297aeba86c7816b7ecf0bc79168080e2bd79957", size = 2520471 },
+ { url = "https://files.pythonhosted.org/packages/13/78/df381bc7b26e535c91469f77f16adcd073beb3e2dd25042efd064af82323/kiwisolver-1.4.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e063ef9f89885a1d68dd8b2e18f5ead48653176d10a0e324e3b0030e3a69adeb", size = 2338793 },
+ { url = "https://files.pythonhosted.org/packages/d0/dc/c1abe38c37c071d0fc71c9a474fd0b9ede05d42f5a458d584619cfd2371a/kiwisolver-1.4.8-cp313-cp313-win_amd64.whl", hash = "sha256:a17b7c4f5b2c51bb68ed379defd608a03954a1845dfed7cc0117f1cc8a9b7fd2", size = 71855 },
+ { url = "https://files.pythonhosted.org/packages/a0/b6/21529d595b126ac298fdd90b705d87d4c5693de60023e0efcb4f387ed99e/kiwisolver-1.4.8-cp313-cp313-win_arm64.whl", hash = "sha256:3cd3bc628b25f74aedc6d374d5babf0166a92ff1317f46267f12d2ed54bc1d30", size = 65430 },
+ { url = "https://files.pythonhosted.org/packages/34/bd/b89380b7298e3af9b39f49334e3e2a4af0e04819789f04b43d560516c0c8/kiwisolver-1.4.8-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:370fd2df41660ed4e26b8c9d6bbcad668fbe2560462cba151a721d49e5b6628c", size = 126294 },
+ { url = "https://files.pythonhosted.org/packages/83/41/5857dc72e5e4148eaac5aa76e0703e594e4465f8ab7ec0fc60e3a9bb8fea/kiwisolver-1.4.8-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:84a2f830d42707de1d191b9490ac186bf7997a9495d4e9072210a1296345f7dc", size = 67736 },
+ { url = "https://files.pythonhosted.org/packages/e1/d1/be059b8db56ac270489fb0b3297fd1e53d195ba76e9bbb30e5401fa6b759/kiwisolver-1.4.8-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:7a3ad337add5148cf51ce0b55642dc551c0b9d6248458a757f98796ca7348712", size = 66194 },
+ { url = "https://files.pythonhosted.org/packages/e1/83/4b73975f149819eb7dcf9299ed467eba068ecb16439a98990dcb12e63fdd/kiwisolver-1.4.8-cp313-cp313t-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7506488470f41169b86d8c9aeff587293f530a23a23a49d6bc64dab66bedc71e", size = 1465942 },
+ { url = "https://files.pythonhosted.org/packages/c7/2c/30a5cdde5102958e602c07466bce058b9d7cb48734aa7a4327261ac8e002/kiwisolver-1.4.8-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f0121b07b356a22fb0414cec4666bbe36fd6d0d759db3d37228f496ed67c880", size = 1595341 },
+ { url = "https://files.pythonhosted.org/packages/ff/9b/1e71db1c000385aa069704f5990574b8244cce854ecd83119c19e83c9586/kiwisolver-1.4.8-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d6d6bd87df62c27d4185de7c511c6248040afae67028a8a22012b010bc7ad062", size = 1598455 },
+ { url = "https://files.pythonhosted.org/packages/85/92/c8fec52ddf06231b31cbb779af77e99b8253cd96bd135250b9498144c78b/kiwisolver-1.4.8-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:291331973c64bb9cce50bbe871fb2e675c4331dab4f31abe89f175ad7679a4d7", size = 1522138 },
+ { url = "https://files.pythonhosted.org/packages/0b/51/9eb7e2cd07a15d8bdd976f6190c0164f92ce1904e5c0c79198c4972926b7/kiwisolver-1.4.8-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:893f5525bb92d3d735878ec00f781b2de998333659507d29ea4466208df37bed", size = 1582857 },
+ { url = "https://files.pythonhosted.org/packages/0f/95/c5a00387a5405e68ba32cc64af65ce881a39b98d73cc394b24143bebc5b8/kiwisolver-1.4.8-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b47a465040146981dc9db8647981b8cb96366fbc8d452b031e4f8fdffec3f26d", size = 2293129 },
+ { url = "https://files.pythonhosted.org/packages/44/83/eeb7af7d706b8347548313fa3a3a15931f404533cc54fe01f39e830dd231/kiwisolver-1.4.8-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:99cea8b9dd34ff80c521aef46a1dddb0dcc0283cf18bde6d756f1e6f31772165", size = 2421538 },
+ { url = "https://files.pythonhosted.org/packages/05/f9/27e94c1b3eb29e6933b6986ffc5fa1177d2cd1f0c8efc5f02c91c9ac61de/kiwisolver-1.4.8-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:151dffc4865e5fe6dafce5480fab84f950d14566c480c08a53c663a0020504b6", size = 2390661 },
+ { url = "https://files.pythonhosted.org/packages/d9/d4/3c9735faa36ac591a4afcc2980d2691000506050b7a7e80bcfe44048daa7/kiwisolver-1.4.8-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:577facaa411c10421314598b50413aa1ebcf5126f704f1e5d72d7e4e9f020d90", size = 2546710 },
+ { url = "https://files.pythonhosted.org/packages/4c/fa/be89a49c640930180657482a74970cdcf6f7072c8d2471e1babe17a222dc/kiwisolver-1.4.8-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:be4816dc51c8a471749d664161b434912eee82f2ea66bd7628bd14583a833e85", size = 2349213 },
+ { url = "https://files.pythonhosted.org/packages/1f/f9/ae81c47a43e33b93b0a9819cac6723257f5da2a5a60daf46aa5c7226ea85/kiwisolver-1.4.8-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:e7a019419b7b510f0f7c9dceff8c5eae2392037eae483a7f9162625233802b0a", size = 60403 },
+ { url = "https://files.pythonhosted.org/packages/58/ca/f92b5cb6f4ce0c1ebfcfe3e2e42b96917e16f7090e45b21102941924f18f/kiwisolver-1.4.8-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:286b18e86682fd2217a48fc6be6b0f20c1d0ed10958d8dc53453ad58d7be0bf8", size = 58657 },
+ { url = "https://files.pythonhosted.org/packages/80/28/ae0240f732f0484d3a4dc885d055653c47144bdf59b670aae0ec3c65a7c8/kiwisolver-1.4.8-pp310-pypy310_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4191ee8dfd0be1c3666ccbac178c5a05d5f8d689bbe3fc92f3c4abec817f8fe0", size = 84948 },
+ { url = "https://files.pythonhosted.org/packages/5d/eb/78d50346c51db22c7203c1611f9b513075f35c4e0e4877c5dde378d66043/kiwisolver-1.4.8-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7cd2785b9391f2873ad46088ed7599a6a71e762e1ea33e87514b1a441ed1da1c", size = 81186 },
+ { url = "https://files.pythonhosted.org/packages/43/f8/7259f18c77adca88d5f64f9a522792e178b2691f3748817a8750c2d216ef/kiwisolver-1.4.8-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c07b29089b7ba090b6f1a669f1411f27221c3662b3a1b7010e67b59bb5a6f10b", size = 80279 },
+ { url = "https://files.pythonhosted.org/packages/3a/1d/50ad811d1c5dae091e4cf046beba925bcae0a610e79ae4c538f996f63ed5/kiwisolver-1.4.8-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:65ea09a5a3faadd59c2ce96dc7bf0f364986a315949dc6374f04396b0d60e09b", size = 71762 },
+]
+
+[[package]]
+name = "kornia"
+version = "0.8.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "kornia-rs" },
+ { name = "packaging" },
+ { name = "torch" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/5d/e8/38cfab1ed0aeb421406f8e127b169b457ed8000fe3e292bbdc74de8b7b2b/kornia-0.8.0.tar.gz", hash = "sha256:a0ffc31106e8d777a8df693572ad5ea11f7236b8bc1d452754f5e57de012ea9a", size = 651982 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/f2/33/7721a4f69dd5f020c30de456d7b948fea8d3897d9f29a51f7538948ee7e2/kornia-0.8.0-py2.py3-none-any.whl", hash = "sha256:028711b0902dd7c0c79ddd20b6299b96f280eb2e475e9717fc8e0a0aac629bc2", size = 1078141 },
+]
+
+[[package]]
+name = "kornia-rs"
+version = "0.1.8"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/c6/8f/931f6273d712ba80e2d4cd83f4d43c406fcbc7a8f2758ff69f4ed62a1eb0/kornia_rs-0.1.8.tar.gz", hash = "sha256:519e05f51deb4c8e849889292b9c109e0ea0943ae5024685781c35018effafd9", size = 75377 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a1/46/f420afb7b83a5b4f8f29cc8050c39ba218f815089b6e11c28276b3db7af4/kornia_rs-0.1.8-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:1380edbbb841f9579bc8677d388e326b7363e1d0d49e8bab567ec9ef1aec782f", size = 1926031 },
+ { url = "https://files.pythonhosted.org/packages/6a/0d/dd8f2cc4a6efcf72214d6b55f67713652a4b9b0bd76108c569a6c16a8829/kornia_rs-0.1.8-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b82cf759df6f5fd935c1afd25aa3a145fd47f14af3650ad37c71189f49171bd8", size = 1720169 },
+ { url = "https://files.pythonhosted.org/packages/c8/20/d7239226a6654e2438f075b5fc523d54847cbf43f04de4555005a9dceca8/kornia_rs-0.1.8-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f12aeaf672493b456f2d35b4b3c88eda3dd8284807430d0b173cb3272c7ef61", size = 1824036 },
+ { url = "https://files.pythonhosted.org/packages/25/81/ea7b30aeabd1c2666fcc25d34b58e48ac635a774aa79c649173f438cb9a3/kornia_rs-0.1.8-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b57fd6262ef932a3131dd211764bf184380742a2aea0a12c54949af7c61c2ac", size = 2050407 },
+ { url = "https://files.pythonhosted.org/packages/18/06/554954f6fcf752b3cba3b63b08eafe04fe485d069938f524180db28e0b2c/kornia_rs-0.1.8-cp310-cp310-win_amd64.whl", hash = "sha256:06f60ff032ce9824b5fe746d1e1cca06ea3f5ba72b71a907a1c48f0e27094333", size = 1694118 },
+ { url = "https://files.pythonhosted.org/packages/83/8f/9fec1b99f484e41e680cd1d7eb0948532d3fbf55547f53496019bf304fa7/kornia_rs-0.1.8-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:61b9822a68556198c5b526da939ddc3f9c630cab37c2d6bcf613c2de1bb3d088", size = 1921457 },
+ { url = "https://files.pythonhosted.org/packages/86/6b/f8b257bf88b0e167e9732c9190746a3a71fe4b9b6c8831529664285dedc4/kornia_rs-0.1.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2dc98296aeeccf2536c1f8efa99d3c273962c7a07a8ae7c088de09ecc19543c4", size = 1718902 },
+ { url = "https://files.pythonhosted.org/packages/0b/17/34501f53b4ce7608d5a43fb9e81e605433c0751367445a450a990e06d676/kornia_rs-0.1.8-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4968efcd26ca190977cfe84d38492a912ad95f13222473dbeb90f330aab51d82", size = 1823731 },
+ { url = "https://files.pythonhosted.org/packages/eb/b9/46ffae8b1acfb00d08110440ce7ee00f0a92c0856829b76c0e10be394042/kornia_rs-0.1.8-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b64be28fbac1f2e1bab3903b5016e1a957968fe43141ee7866c2ec5ebafc71ab", size = 2050393 },
+ { url = "https://files.pythonhosted.org/packages/fe/34/2270ec8702206a5a298ec2342b224148caf92421adac144f4e2362a9c676/kornia_rs-0.1.8-cp311-cp311-win_amd64.whl", hash = "sha256:2886f3a586728fe4a3586b3cc1df1dbea5d8984c74f77e23f5ab198441ec6e3c", size = 1692739 },
+ { url = "https://files.pythonhosted.org/packages/b9/80/a38fc51df8bccd14b710a71163aa848cab867cab5d478769bc5020df18cb/kornia_rs-0.1.8-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:983200f2b336dd832d81154295ff152195ade0228054ecbe7ac9ed7d5bf3b031", size = 1917921 },
+ { url = "https://files.pythonhosted.org/packages/c7/4f/ffd54d9096ccac335e94b58d1b5c55b49c98d0de280e522e0c70b383b2fc/kornia_rs-0.1.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bf8a78b1fac32fe05974272c5659c6a2f8754d1c15372aa529e0b5802ea2daed", size = 1713055 },
+ { url = "https://files.pythonhosted.org/packages/ba/4e/9568a115bc69230fb43fed126ba1794ba42fb68354888a59bff879bcc960/kornia_rs-0.1.8-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ca82f982d92d3b90f462848557ebd1500ea02d65b38b032305d1966c3bbc153", size = 1823184 },
+ { url = "https://files.pythonhosted.org/packages/0a/84/3bd78e98468665be72087b5669c4e02991b0ba82e5ec0c5bcbe0142f02d2/kornia_rs-0.1.8-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:297e48f800c93e7cc8b089e472b77a272f9887509ce9d8756fab0fa7714f8439", size = 2049591 },
+ { url = "https://files.pythonhosted.org/packages/f3/6e/2976bf8c182cced282ba8c6583b0d1f008fecbe3b0ca6324ed367872e58a/kornia_rs-0.1.8-cp312-cp312-win_amd64.whl", hash = "sha256:dba6d86df9d3bb3e99f2d6017b9939b9e2683929277e959d11ea86fb3153eaec", size = 1693398 },
+ { url = "https://files.pythonhosted.org/packages/8e/c7/a086f0f48e25c7a00fc376d41c20821293acd030d07b2329382a314eb6d9/kornia_rs-0.1.8-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:9197fc690b79562ff745a9ebda05c1408b9938045aecbbdafeaa8aed1f238b31", size = 1918007 },
+ { url = "https://files.pythonhosted.org/packages/dc/b2/a75a260d5f0ae2623a4fd3ee8f844b9b54bdd157566e25e95b2b698a9a7d/kornia_rs-0.1.8-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1014eac46dd75c8ba9ca61579593d77b84918236877fcae9dca362ff5d6960e4", size = 1713206 },
+ { url = "https://files.pythonhosted.org/packages/bb/e6/9f3e1798718b5988c761a79f37782065c49464e4324fd49c5b0ab2e57610/kornia_rs-0.1.8-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c7d7c90c6244a37e0d1994e532ddf3484b3e7f767c54121d514feda83974a934", size = 1823437 },
+ { url = "https://files.pythonhosted.org/packages/b7/71/9b37dd1f60bd486e1b786df1a0c82696b1bc0992d2de7b281134618c0486/kornia_rs-0.1.8-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ef0c4a19103ff9c3c7e7acb2a7db0a276a0ab1ea1c19fe151aea384a98cd63c", size = 2049635 },
+ { url = "https://files.pythonhosted.org/packages/27/b6/fb26cce38f7cfc887c9c967a0467c1ed348fa6d1e0f1d02c063b8f482043/kornia_rs-0.1.8-cp313-cp313-win_amd64.whl", hash = "sha256:434fb087e2caef5b2ecd5222ea54cc443e907851b708be15142bc65ae82cef63", size = 1693865 },
+]
+
+[[package]]
+name = "loguru"
+version = "0.7.3"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "colorama", marker = "sys_platform == 'win32'" },
+ { name = "win32-setctime", marker = "sys_platform == 'win32'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/3a/05/a1dae3dffd1116099471c643b8924f5aa6524411dc6c63fdae648c4f1aca/loguru-0.7.3.tar.gz", hash = "sha256:19480589e77d47b8d85b2c827ad95d49bf31b0dcde16593892eb51dd18706eb6", size = 63559 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/0c/29/0348de65b8cc732daa3e33e67806420b2ae89bdce2b04af740289c5c6c8c/loguru-0.7.3-py3-none-any.whl", hash = "sha256:31a33c10c8e1e10422bfd431aeb5d351c7cf7fa671e3c4df004162264b28220c", size = 61595 },
+]
+
+[[package]]
+name = "markupsafe"
+version = "3.0.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/04/90/d08277ce111dd22f77149fd1a5d4653eeb3b3eaacbdfcbae5afb2600eebd/MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8", size = 14357 },
+ { url = "https://files.pythonhosted.org/packages/04/e1/6e2194baeae0bca1fae6629dc0cbbb968d4d941469cbab11a3872edff374/MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158", size = 12393 },
+ { url = "https://files.pythonhosted.org/packages/1d/69/35fa85a8ece0a437493dc61ce0bb6d459dcba482c34197e3efc829aa357f/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579", size = 21732 },
+ { url = "https://files.pythonhosted.org/packages/22/35/137da042dfb4720b638d2937c38a9c2df83fe32d20e8c8f3185dbfef05f7/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d", size = 20866 },
+ { url = "https://files.pythonhosted.org/packages/29/28/6d029a903727a1b62edb51863232152fd335d602def598dade38996887f0/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb", size = 20964 },
+ { url = "https://files.pythonhosted.org/packages/cc/cd/07438f95f83e8bc028279909d9c9bd39e24149b0d60053a97b2bc4f8aa51/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b", size = 21977 },
+ { url = "https://files.pythonhosted.org/packages/29/01/84b57395b4cc062f9c4c55ce0df7d3108ca32397299d9df00fedd9117d3d/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c", size = 21366 },
+ { url = "https://files.pythonhosted.org/packages/bd/6e/61ebf08d8940553afff20d1fb1ba7294b6f8d279df9fd0c0db911b4bbcfd/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171", size = 21091 },
+ { url = "https://files.pythonhosted.org/packages/11/23/ffbf53694e8c94ebd1e7e491de185124277964344733c45481f32ede2499/MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50", size = 15065 },
+ { url = "https://files.pythonhosted.org/packages/44/06/e7175d06dd6e9172d4a69a72592cb3f7a996a9c396eee29082826449bbc3/MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a", size = 15514 },
+ { url = "https://files.pythonhosted.org/packages/6b/28/bbf83e3f76936960b850435576dd5e67034e200469571be53f69174a2dfd/MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d", size = 14353 },
+ { url = "https://files.pythonhosted.org/packages/6c/30/316d194b093cde57d448a4c3209f22e3046c5bb2fb0820b118292b334be7/MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93", size = 12392 },
+ { url = "https://files.pythonhosted.org/packages/f2/96/9cdafba8445d3a53cae530aaf83c38ec64c4d5427d975c974084af5bc5d2/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832", size = 23984 },
+ { url = "https://files.pythonhosted.org/packages/f1/a4/aefb044a2cd8d7334c8a47d3fb2c9f328ac48cb349468cc31c20b539305f/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84", size = 23120 },
+ { url = "https://files.pythonhosted.org/packages/8d/21/5e4851379f88f3fad1de30361db501300d4f07bcad047d3cb0449fc51f8c/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca", size = 23032 },
+ { url = "https://files.pythonhosted.org/packages/00/7b/e92c64e079b2d0d7ddf69899c98842f3f9a60a1ae72657c89ce2655c999d/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798", size = 24057 },
+ { url = "https://files.pythonhosted.org/packages/f9/ac/46f960ca323037caa0a10662ef97d0a4728e890334fc156b9f9e52bcc4ca/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e", size = 23359 },
+ { url = "https://files.pythonhosted.org/packages/69/84/83439e16197337b8b14b6a5b9c2105fff81d42c2a7c5b58ac7b62ee2c3b1/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4", size = 23306 },
+ { url = "https://files.pythonhosted.org/packages/9a/34/a15aa69f01e2181ed8d2b685c0d2f6655d5cca2c4db0ddea775e631918cd/MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d", size = 15094 },
+ { url = "https://files.pythonhosted.org/packages/da/b8/3a3bd761922d416f3dc5d00bfbed11f66b1ab89a0c2b6e887240a30b0f6b/MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b", size = 15521 },
+ { url = "https://files.pythonhosted.org/packages/22/09/d1f21434c97fc42f09d290cbb6350d44eb12f09cc62c9476effdb33a18aa/MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf", size = 14274 },
+ { url = "https://files.pythonhosted.org/packages/6b/b0/18f76bba336fa5aecf79d45dcd6c806c280ec44538b3c13671d49099fdd0/MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225", size = 12348 },
+ { url = "https://files.pythonhosted.org/packages/e0/25/dd5c0f6ac1311e9b40f4af06c78efde0f3b5cbf02502f8ef9501294c425b/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028", size = 24149 },
+ { url = "https://files.pythonhosted.org/packages/f3/f0/89e7aadfb3749d0f52234a0c8c7867877876e0a20b60e2188e9850794c17/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8", size = 23118 },
+ { url = "https://files.pythonhosted.org/packages/d5/da/f2eeb64c723f5e3777bc081da884b414671982008c47dcc1873d81f625b6/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c", size = 22993 },
+ { url = "https://files.pythonhosted.org/packages/da/0e/1f32af846df486dce7c227fe0f2398dc7e2e51d4a370508281f3c1c5cddc/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557", size = 24178 },
+ { url = "https://files.pythonhosted.org/packages/c4/f6/bb3ca0532de8086cbff5f06d137064c8410d10779c4c127e0e47d17c0b71/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22", size = 23319 },
+ { url = "https://files.pythonhosted.org/packages/a2/82/8be4c96ffee03c5b4a034e60a31294daf481e12c7c43ab8e34a1453ee48b/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48", size = 23352 },
+ { url = "https://files.pythonhosted.org/packages/51/ae/97827349d3fcffee7e184bdf7f41cd6b88d9919c80f0263ba7acd1bbcb18/MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30", size = 15097 },
+ { url = "https://files.pythonhosted.org/packages/c1/80/a61f99dc3a936413c3ee4e1eecac96c0da5ed07ad56fd975f1a9da5bc630/MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87", size = 15601 },
+ { url = "https://files.pythonhosted.org/packages/83/0e/67eb10a7ecc77a0c2bbe2b0235765b98d164d81600746914bebada795e97/MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", size = 14274 },
+ { url = "https://files.pythonhosted.org/packages/2b/6d/9409f3684d3335375d04e5f05744dfe7e9f120062c9857df4ab490a1031a/MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", size = 12352 },
+ { url = "https://files.pythonhosted.org/packages/d2/f5/6eadfcd3885ea85fe2a7c128315cc1bb7241e1987443d78c8fe712d03091/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", size = 24122 },
+ { url = "https://files.pythonhosted.org/packages/0c/91/96cf928db8236f1bfab6ce15ad070dfdd02ed88261c2afafd4b43575e9e9/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", size = 23085 },
+ { url = "https://files.pythonhosted.org/packages/c2/cf/c9d56af24d56ea04daae7ac0940232d31d5a8354f2b457c6d856b2057d69/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", size = 22978 },
+ { url = "https://files.pythonhosted.org/packages/2a/9f/8619835cd6a711d6272d62abb78c033bda638fdc54c4e7f4272cf1c0962b/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", size = 24208 },
+ { url = "https://files.pythonhosted.org/packages/f9/bf/176950a1792b2cd2102b8ffeb5133e1ed984547b75db47c25a67d3359f77/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", size = 23357 },
+ { url = "https://files.pythonhosted.org/packages/ce/4f/9a02c1d335caabe5c4efb90e1b6e8ee944aa245c1aaaab8e8a618987d816/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", size = 23344 },
+ { url = "https://files.pythonhosted.org/packages/ee/55/c271b57db36f748f0e04a759ace9f8f759ccf22b4960c270c78a394f58be/MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", size = 15101 },
+ { url = "https://files.pythonhosted.org/packages/29/88/07df22d2dd4df40aba9f3e402e6dc1b8ee86297dddbad4872bd5e7b0094f/MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", size = 15603 },
+ { url = "https://files.pythonhosted.org/packages/62/6a/8b89d24db2d32d433dffcd6a8779159da109842434f1dd2f6e71f32f738c/MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", size = 14510 },
+ { url = "https://files.pythonhosted.org/packages/7a/06/a10f955f70a2e5a9bf78d11a161029d278eeacbd35ef806c3fd17b13060d/MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", size = 12486 },
+ { url = "https://files.pythonhosted.org/packages/34/cf/65d4a571869a1a9078198ca28f39fba5fbb910f952f9dbc5220afff9f5e6/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", size = 25480 },
+ { url = "https://files.pythonhosted.org/packages/0c/e3/90e9651924c430b885468b56b3d597cabf6d72be4b24a0acd1fa0e12af67/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", size = 23914 },
+ { url = "https://files.pythonhosted.org/packages/66/8c/6c7cf61f95d63bb866db39085150df1f2a5bd3335298f14a66b48e92659c/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", size = 23796 },
+ { url = "https://files.pythonhosted.org/packages/bb/35/cbe9238ec3f47ac9a7c8b3df7a808e7cb50fe149dc7039f5f454b3fba218/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", size = 25473 },
+ { url = "https://files.pythonhosted.org/packages/e6/32/7621a4382488aa283cc05e8984a9c219abad3bca087be9ec77e89939ded9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", size = 24114 },
+ { url = "https://files.pythonhosted.org/packages/0d/80/0985960e4b89922cb5a0bac0ed39c5b96cbc1a536a99f30e8c220a996ed9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", size = 24098 },
+ { url = "https://files.pythonhosted.org/packages/82/78/fedb03c7d5380df2427038ec8d973587e90561b2d90cd472ce9254cf348b/MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", size = 15208 },
+ { url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739 },
+]
+
+[[package]]
+name = "matplotlib"
+version = "3.10.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "contourpy" },
+ { name = "cycler" },
+ { name = "fonttools" },
+ { name = "kiwisolver" },
+ { name = "numpy" },
+ { name = "packaging" },
+ { name = "pillow" },
+ { name = "pyparsing" },
+ { name = "python-dateutil" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/2f/08/b89867ecea2e305f408fbb417139a8dd941ecf7b23a2e02157c36da546f0/matplotlib-3.10.1.tar.gz", hash = "sha256:e8d2d0e3881b129268585bf4765ad3ee73a4591d77b9a18c214ac7e3a79fb2ba", size = 36743335 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ee/b1/f70e27cf1cd76ce2a5e1aa5579d05afe3236052c6d9b9a96325bc823a17e/matplotlib-3.10.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:ff2ae14910be903f4a24afdbb6d7d3a6c44da210fc7d42790b87aeac92238a16", size = 8163654 },
+ { url = "https://files.pythonhosted.org/packages/26/af/5ec3d4636106718bb62503a03297125d4514f98fe818461bd9e6b9d116e4/matplotlib-3.10.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0721a3fd3d5756ed593220a8b86808a36c5031fce489adb5b31ee6dbb47dd5b2", size = 8037943 },
+ { url = "https://files.pythonhosted.org/packages/a1/3d/07f9003a71b698b848c9925d05979ffa94a75cd25d1a587202f0bb58aa81/matplotlib-3.10.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0673b4b8f131890eb3a1ad058d6e065fb3c6e71f160089b65f8515373394698", size = 8449510 },
+ { url = "https://files.pythonhosted.org/packages/12/87/9472d4513ff83b7cd864311821793ab72234fa201ab77310ec1b585d27e2/matplotlib-3.10.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e875b95ac59a7908978fe307ecdbdd9a26af7fa0f33f474a27fcf8c99f64a19", size = 8586585 },
+ { url = "https://files.pythonhosted.org/packages/31/9e/fe74d237d2963adae8608faeb21f778cf246dbbf4746cef87cffbc82c4b6/matplotlib-3.10.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2589659ea30726284c6c91037216f64a506a9822f8e50592d48ac16a2f29e044", size = 9397911 },
+ { url = "https://files.pythonhosted.org/packages/b6/1b/025d3e59e8a4281ab463162ad7d072575354a1916aba81b6a11507dfc524/matplotlib-3.10.1-cp310-cp310-win_amd64.whl", hash = "sha256:a97ff127f295817bc34517255c9db6e71de8eddaab7f837b7d341dee9f2f587f", size = 8052998 },
+ { url = "https://files.pythonhosted.org/packages/a5/14/a1b840075be247bb1834b22c1e1d558740b0f618fe3a823740181ca557a1/matplotlib-3.10.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:057206ff2d6ab82ff3e94ebd94463d084760ca682ed5f150817b859372ec4401", size = 8174669 },
+ { url = "https://files.pythonhosted.org/packages/0a/e4/300b08e3e08f9c98b0d5635f42edabf2f7a1d634e64cb0318a71a44ff720/matplotlib-3.10.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a144867dd6bf8ba8cb5fc81a158b645037e11b3e5cf8a50bd5f9917cb863adfe", size = 8047996 },
+ { url = "https://files.pythonhosted.org/packages/75/f9/8d99ff5a2498a5f1ccf919fb46fb945109623c6108216f10f96428f388bc/matplotlib-3.10.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:56c5d9fcd9879aa8040f196a235e2dcbdf7dd03ab5b07c0696f80bc6cf04bedd", size = 8461612 },
+ { url = "https://files.pythonhosted.org/packages/40/b8/53fa08a5eaf78d3a7213fd6da1feec4bae14a81d9805e567013811ff0e85/matplotlib-3.10.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f69dc9713e4ad2fb21a1c30e37bd445d496524257dfda40ff4a8efb3604ab5c", size = 8602258 },
+ { url = "https://files.pythonhosted.org/packages/40/87/4397d2ce808467af86684a622dd112664553e81752ea8bf61bdd89d24a41/matplotlib-3.10.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4c59af3e8aca75d7744b68e8e78a669e91ccbcf1ac35d0102a7b1b46883f1dd7", size = 9408896 },
+ { url = "https://files.pythonhosted.org/packages/d7/68/0d03098b3feb786cbd494df0aac15b571effda7f7cbdec267e8a8d398c16/matplotlib-3.10.1-cp311-cp311-win_amd64.whl", hash = "sha256:11b65088c6f3dae784bc72e8d039a2580186285f87448babb9ddb2ad0082993a", size = 8061281 },
+ { url = "https://files.pythonhosted.org/packages/7c/1d/5e0dc3b59c034e43de16f94deb68f4ad8a96b3ea00f4b37c160b7474928e/matplotlib-3.10.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:66e907a06e68cb6cfd652c193311d61a12b54f56809cafbed9736ce5ad92f107", size = 8175488 },
+ { url = "https://files.pythonhosted.org/packages/7a/81/dae7e14042e74da658c3336ab9799128e09a1ee03964f2d89630b5d12106/matplotlib-3.10.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e9b4bb156abb8fa5e5b2b460196f7db7264fc6d62678c03457979e7d5254b7be", size = 8046264 },
+ { url = "https://files.pythonhosted.org/packages/21/c4/22516775dcde10fc9c9571d155f90710761b028fc44f660508106c363c97/matplotlib-3.10.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1985ad3d97f51307a2cbfc801a930f120def19ba22864182dacef55277102ba6", size = 8452048 },
+ { url = "https://files.pythonhosted.org/packages/63/23/c0615001f67ce7c96b3051d856baedc0c818a2ed84570b9bf9bde200f85d/matplotlib-3.10.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c96f2c2f825d1257e437a1482c5a2cf4fee15db4261bd6fc0750f81ba2b4ba3d", size = 8597111 },
+ { url = "https://files.pythonhosted.org/packages/ca/c0/a07939a82aed77770514348f4568177d7dadab9787ebc618a616fe3d665e/matplotlib-3.10.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:35e87384ee9e488d8dd5a2dd7baf471178d38b90618d8ea147aced4ab59c9bea", size = 9402771 },
+ { url = "https://files.pythonhosted.org/packages/a6/b6/a9405484fb40746fdc6ae4502b16a9d6e53282ba5baaf9ebe2da579f68c4/matplotlib-3.10.1-cp312-cp312-win_amd64.whl", hash = "sha256:cfd414bce89cc78a7e1d25202e979b3f1af799e416010a20ab2b5ebb3a02425c", size = 8063742 },
+ { url = "https://files.pythonhosted.org/packages/60/73/6770ff5e5523d00f3bc584acb6031e29ee5c8adc2336b16cd1d003675fe0/matplotlib-3.10.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c42eee41e1b60fd83ee3292ed83a97a5f2a8239b10c26715d8a6172226988d7b", size = 8176112 },
+ { url = "https://files.pythonhosted.org/packages/08/97/b0ca5da0ed54a3f6599c3ab568bdda65269bc27c21a2c97868c1625e4554/matplotlib-3.10.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4f0647b17b667ae745c13721602b540f7aadb2a32c5b96e924cd4fea5dcb90f1", size = 8046931 },
+ { url = "https://files.pythonhosted.org/packages/df/9a/1acbdc3b165d4ce2dcd2b1a6d4ffb46a7220ceee960c922c3d50d8514067/matplotlib-3.10.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa3854b5f9473564ef40a41bc922be978fab217776e9ae1545c9b3a5cf2092a3", size = 8453422 },
+ { url = "https://files.pythonhosted.org/packages/51/d0/2bc4368abf766203e548dc7ab57cf7e9c621f1a3c72b516cc7715347b179/matplotlib-3.10.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e496c01441be4c7d5f96d4e40f7fca06e20dcb40e44c8daa2e740e1757ad9e6", size = 8596819 },
+ { url = "https://files.pythonhosted.org/packages/ab/1b/8b350f8a1746c37ab69dda7d7528d1fc696efb06db6ade9727b7887be16d/matplotlib-3.10.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5d45d3f5245be5b469843450617dcad9af75ca50568acf59997bed9311131a0b", size = 9402782 },
+ { url = "https://files.pythonhosted.org/packages/89/06/f570373d24d93503988ba8d04f213a372fa1ce48381c5eb15da985728498/matplotlib-3.10.1-cp313-cp313-win_amd64.whl", hash = "sha256:8e8e25b1209161d20dfe93037c8a7f7ca796ec9aa326e6e4588d8c4a5dd1e473", size = 8063812 },
+ { url = "https://files.pythonhosted.org/packages/fc/e0/8c811a925b5a7ad75135f0e5af46408b78af88bbb02a1df775100ef9bfef/matplotlib-3.10.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:19b06241ad89c3ae9469e07d77efa87041eac65d78df4fcf9cac318028009b01", size = 8214021 },
+ { url = "https://files.pythonhosted.org/packages/4a/34/319ec2139f68ba26da9d00fce2ff9f27679fb799a6c8e7358539801fd629/matplotlib-3.10.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:01e63101ebb3014e6e9f80d9cf9ee361a8599ddca2c3e166c563628b39305dbb", size = 8090782 },
+ { url = "https://files.pythonhosted.org/packages/77/ea/9812124ab9a99df5b2eec1110e9b2edc0b8f77039abf4c56e0a376e84a29/matplotlib-3.10.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f06bad951eea6422ac4e8bdebcf3a70c59ea0a03338c5d2b109f57b64eb3972", size = 8478901 },
+ { url = "https://files.pythonhosted.org/packages/c9/db/b05bf463689134789b06dea85828f8ebe506fa1e37593f723b65b86c9582/matplotlib-3.10.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a3dfb036f34873b46978f55e240cff7a239f6c4409eac62d8145bad3fc6ba5a3", size = 8613864 },
+ { url = "https://files.pythonhosted.org/packages/c2/04/41ccec4409f3023a7576df3b5c025f1a8c8b81fbfe922ecfd837ac36e081/matplotlib-3.10.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dc6ab14a7ab3b4d813b88ba957fc05c79493a037f54e246162033591e770de6f", size = 9409487 },
+ { url = "https://files.pythonhosted.org/packages/ac/c2/0d5aae823bdcc42cc99327ecdd4d28585e15ccd5218c453b7bcd827f3421/matplotlib-3.10.1-cp313-cp313t-win_amd64.whl", hash = "sha256:bc411ebd5889a78dabbc457b3fa153203e22248bfa6eedc6797be5df0164dbf9", size = 8134832 },
+ { url = "https://files.pythonhosted.org/packages/c8/f6/10adb696d8cbeed2ab4c2e26ecf1c80dd3847bbf3891f4a0c362e0e08a5a/matplotlib-3.10.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:648406f1899f9a818cef8c0231b44dcfc4ff36f167101c3fd1c9151f24220fdc", size = 8158685 },
+ { url = "https://files.pythonhosted.org/packages/3f/84/0603d917406072763e7f9bb37747d3d74d7ecd4b943a8c947cc3ae1cf7af/matplotlib-3.10.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:02582304e352f40520727984a5a18f37e8187861f954fea9be7ef06569cf85b4", size = 8035491 },
+ { url = "https://files.pythonhosted.org/packages/fd/7d/6a8b31dd07ed856b3eae001c9129670ef75c4698fa1c2a6ac9f00a4a7054/matplotlib-3.10.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d3809916157ba871bcdd33d3493acd7fe3037db5daa917ca6e77975a94cef779", size = 8590087 },
+]
+
+[[package]]
+name = "mpmath"
+version = "1.3.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/e0/47/dd32fa426cc72114383ac549964eecb20ecfd886d1e5ccf5340b55b02f57/mpmath-1.3.0.tar.gz", hash = "sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f", size = 508106 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c", size = 536198 },
+]
+
+[[package]]
+name = "networkx"
+version = "3.4.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/fd/1d/06475e1cd5264c0b870ea2cc6fdb3e37177c1e565c43f56ff17a10e3937f/networkx-3.4.2.tar.gz", hash = "sha256:307c3669428c5362aab27c8a1260aa8f47c4e91d3891f48be0141738d8d053e1", size = 2151368 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b9/54/dd730b32ea14ea797530a4479b2ed46a6fb250f682a9cfb997e968bf0261/networkx-3.4.2-py3-none-any.whl", hash = "sha256:df5d4365b724cf81b8c6a7312509d0c22386097011ad1abe274afd5e9d3bbc5f", size = 1723263 },
+]
+
+[[package]]
+name = "numpy"
+version = "2.2.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/fb/90/8956572f5c4ae52201fdec7ba2044b2c882832dcec7d5d0922c9e9acf2de/numpy-2.2.3.tar.gz", hash = "sha256:dbdc15f0c81611925f382dfa97b3bd0bc2c1ce19d4fe50482cb0ddc12ba30020", size = 20262700 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/5e/e1/1816d5d527fa870b260a1c2c5904d060caad7515637bd54f495a5ce13ccd/numpy-2.2.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:cbc6472e01952d3d1b2772b720428f8b90e2deea8344e854df22b0618e9cce71", size = 21232911 },
+ { url = "https://files.pythonhosted.org/packages/29/46/9f25dc19b359f10c0e52b6bac25d3181eb1f4b4d04c9846a32cf5ea52762/numpy-2.2.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cdfe0c22692a30cd830c0755746473ae66c4a8f2e7bd508b35fb3b6a0813d787", size = 14371955 },
+ { url = "https://files.pythonhosted.org/packages/72/d7/de941296e6b09a5c81d3664ad912f1496a0ecdd2f403318e5e35604ff70f/numpy-2.2.3-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:e37242f5324ffd9f7ba5acf96d774f9276aa62a966c0bad8dae692deebec7716", size = 5410476 },
+ { url = "https://files.pythonhosted.org/packages/36/ce/55f685995110f8a268fdca0f198c9a84fa87b39512830965cc1087af6391/numpy-2.2.3-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:95172a21038c9b423e68be78fd0be6e1b97674cde269b76fe269a5dfa6fadf0b", size = 6945730 },
+ { url = "https://files.pythonhosted.org/packages/4f/84/abdb9f6e22576d89c259401c3234d4755b322539491bbcffadc8bcb120d3/numpy-2.2.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d5b47c440210c5d1d67e1cf434124e0b5c395eee1f5806fdd89b553ed1acd0a3", size = 14350752 },
+ { url = "https://files.pythonhosted.org/packages/e9/88/3870cfa9bef4dffb3a326507f430e6007eeac258ebeef6b76fc542aef66d/numpy-2.2.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0391ea3622f5c51a2e29708877d56e3d276827ac5447d7f45e9bc4ade8923c52", size = 16399386 },
+ { url = "https://files.pythonhosted.org/packages/02/10/3f629682dd0b457525c131945329c4e81e2dadeb11256e6ce4c9a1a6fb41/numpy-2.2.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f6b3dfc7661f8842babd8ea07e9897fe3d9b69a1d7e5fbb743e4160f9387833b", size = 15561826 },
+ { url = "https://files.pythonhosted.org/packages/da/18/fd35673ba9751eba449d4ce5d24d94e3b612cdbfba79348da71488c0b7ac/numpy-2.2.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:1ad78ce7f18ce4e7df1b2ea4019b5817a2f6a8a16e34ff2775f646adce0a5027", size = 18188593 },
+ { url = "https://files.pythonhosted.org/packages/ce/4c/c0f897b580ea59484b4cc96a441fea50333b26675a60a1421bc912268b5f/numpy-2.2.3-cp310-cp310-win32.whl", hash = "sha256:5ebeb7ef54a7be11044c33a17b2624abe4307a75893c001a4800857956b41094", size = 6590421 },
+ { url = "https://files.pythonhosted.org/packages/e5/5b/aaabbfc7060c5c8f0124c5deb5e114a3b413a548bbc64e372c5b5db36165/numpy-2.2.3-cp310-cp310-win_amd64.whl", hash = "sha256:596140185c7fa113563c67c2e894eabe0daea18cf8e33851738c19f70ce86aeb", size = 12925667 },
+ { url = "https://files.pythonhosted.org/packages/96/86/453aa3949eab6ff54e2405f9cb0c01f756f031c3dc2a6d60a1d40cba5488/numpy-2.2.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:16372619ee728ed67a2a606a614f56d3eabc5b86f8b615c79d01957062826ca8", size = 21237256 },
+ { url = "https://files.pythonhosted.org/packages/20/c3/93ecceadf3e155d6a9e4464dd2392d8d80cf436084c714dc8535121c83e8/numpy-2.2.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5521a06a3148686d9269c53b09f7d399a5725c47bbb5b35747e1cb76326b714b", size = 14408049 },
+ { url = "https://files.pythonhosted.org/packages/8d/29/076999b69bd9264b8df5e56f2be18da2de6b2a2d0e10737e5307592e01de/numpy-2.2.3-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:7c8dde0ca2f77828815fd1aedfdf52e59071a5bae30dac3b4da2a335c672149a", size = 5408655 },
+ { url = "https://files.pythonhosted.org/packages/e2/a7/b14f0a73eb0fe77cb9bd5b44534c183b23d4229c099e339c522724b02678/numpy-2.2.3-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:77974aba6c1bc26e3c205c2214f0d5b4305bdc719268b93e768ddb17e3fdd636", size = 6949996 },
+ { url = "https://files.pythonhosted.org/packages/72/2f/8063da0616bb0f414b66dccead503bd96e33e43685c820e78a61a214c098/numpy-2.2.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d42f9c36d06440e34226e8bd65ff065ca0963aeecada587b937011efa02cdc9d", size = 14355789 },
+ { url = "https://files.pythonhosted.org/packages/e6/d7/3cd47b00b8ea95ab358c376cf5602ad21871410950bc754cf3284771f8b6/numpy-2.2.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2712c5179f40af9ddc8f6727f2bd910ea0eb50206daea75f58ddd9fa3f715bb", size = 16411356 },
+ { url = "https://files.pythonhosted.org/packages/27/c0/a2379e202acbb70b85b41483a422c1e697ff7eee74db642ca478de4ba89f/numpy-2.2.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c8b0451d2ec95010d1db8ca733afc41f659f425b7f608af569711097fd6014e2", size = 15576770 },
+ { url = "https://files.pythonhosted.org/packages/bc/63/a13ee650f27b7999e5b9e1964ae942af50bb25606d088df4229283eda779/numpy-2.2.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d9b4a8148c57ecac25a16b0e11798cbe88edf5237b0df99973687dd866f05e1b", size = 18200483 },
+ { url = "https://files.pythonhosted.org/packages/4c/87/e71f89935e09e8161ac9c590c82f66d2321eb163893a94af749dfa8a3cf8/numpy-2.2.3-cp311-cp311-win32.whl", hash = "sha256:1f45315b2dc58d8a3e7754fe4e38b6fce132dab284a92851e41b2b344f6441c5", size = 6588415 },
+ { url = "https://files.pythonhosted.org/packages/b9/c6/cd4298729826af9979c5f9ab02fcaa344b82621e7c49322cd2d210483d3f/numpy-2.2.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f48ba6f6c13e5e49f3d3efb1b51c8193215c42ac82610a04624906a9270be6f", size = 12929604 },
+ { url = "https://files.pythonhosted.org/packages/43/ec/43628dcf98466e087812142eec6d1c1a6c6bdfdad30a0aa07b872dc01f6f/numpy-2.2.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:12c045f43b1d2915eca6b880a7f4a256f59d62df4f044788c8ba67709412128d", size = 20929458 },
+ { url = "https://files.pythonhosted.org/packages/9b/c0/2f4225073e99a5c12350954949ed19b5d4a738f541d33e6f7439e33e98e4/numpy-2.2.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:87eed225fd415bbae787f93a457af7f5990b92a334e346f72070bf569b9c9c95", size = 14115299 },
+ { url = "https://files.pythonhosted.org/packages/ca/fa/d2c5575d9c734a7376cc1592fae50257ec95d061b27ee3dbdb0b3b551eb2/numpy-2.2.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:712a64103d97c404e87d4d7c47fb0c7ff9acccc625ca2002848e0d53288b90ea", size = 5145723 },
+ { url = "https://files.pythonhosted.org/packages/eb/dc/023dad5b268a7895e58e791f28dc1c60eb7b6c06fcbc2af8538ad069d5f3/numpy-2.2.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:a5ae282abe60a2db0fd407072aff4599c279bcd6e9a2475500fc35b00a57c532", size = 6678797 },
+ { url = "https://files.pythonhosted.org/packages/3f/19/bcd641ccf19ac25abb6fb1dcd7744840c11f9d62519d7057b6ab2096eb60/numpy-2.2.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5266de33d4c3420973cf9ae3b98b54a2a6d53a559310e3236c4b2b06b9c07d4e", size = 14067362 },
+ { url = "https://files.pythonhosted.org/packages/39/04/78d2e7402fb479d893953fb78fa7045f7deb635ec095b6b4f0260223091a/numpy-2.2.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b787adbf04b0db1967798dba8da1af07e387908ed1553a0d6e74c084d1ceafe", size = 16116679 },
+ { url = "https://files.pythonhosted.org/packages/d0/a1/e90f7aa66512be3150cb9d27f3d9995db330ad1b2046474a13b7040dfd92/numpy-2.2.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:34c1b7e83f94f3b564b35f480f5652a47007dd91f7c839f404d03279cc8dd021", size = 15264272 },
+ { url = "https://files.pythonhosted.org/packages/dc/b6/50bd027cca494de4fa1fc7bf1662983d0ba5f256fa0ece2c376b5eb9b3f0/numpy-2.2.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4d8335b5f1b6e2bce120d55fb17064b0262ff29b459e8493d1785c18ae2553b8", size = 17880549 },
+ { url = "https://files.pythonhosted.org/packages/96/30/f7bf4acb5f8db10a96f73896bdeed7a63373137b131ca18bd3dab889db3b/numpy-2.2.3-cp312-cp312-win32.whl", hash = "sha256:4d9828d25fb246bedd31e04c9e75714a4087211ac348cb39c8c5f99dbb6683fe", size = 6293394 },
+ { url = "https://files.pythonhosted.org/packages/42/6e/55580a538116d16ae7c9aa17d4edd56e83f42126cb1dfe7a684da7925d2c/numpy-2.2.3-cp312-cp312-win_amd64.whl", hash = "sha256:83807d445817326b4bcdaaaf8e8e9f1753da04341eceec705c001ff342002e5d", size = 12626357 },
+ { url = "https://files.pythonhosted.org/packages/0e/8b/88b98ed534d6a03ba8cddb316950fe80842885709b58501233c29dfa24a9/numpy-2.2.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7bfdb06b395385ea9b91bf55c1adf1b297c9fdb531552845ff1d3ea6e40d5aba", size = 20916001 },
+ { url = "https://files.pythonhosted.org/packages/d9/b4/def6ec32c725cc5fbd8bdf8af80f616acf075fe752d8a23e895da8c67b70/numpy-2.2.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:23c9f4edbf4c065fddb10a4f6e8b6a244342d95966a48820c614891e5059bb50", size = 14130721 },
+ { url = "https://files.pythonhosted.org/packages/20/60/70af0acc86495b25b672d403e12cb25448d79a2b9658f4fc45e845c397a8/numpy-2.2.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:a0c03b6be48aaf92525cccf393265e02773be8fd9551a2f9adbe7db1fa2b60f1", size = 5130999 },
+ { url = "https://files.pythonhosted.org/packages/2e/69/d96c006fb73c9a47bcb3611417cf178049aae159afae47c48bd66df9c536/numpy-2.2.3-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:2376e317111daa0a6739e50f7ee2a6353f768489102308b0d98fcf4a04f7f3b5", size = 6665299 },
+ { url = "https://files.pythonhosted.org/packages/5a/3f/d8a877b6e48103733ac224ffa26b30887dc9944ff95dffdfa6c4ce3d7df3/numpy-2.2.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8fb62fe3d206d72fe1cfe31c4a1106ad2b136fcc1606093aeab314f02930fdf2", size = 14064096 },
+ { url = "https://files.pythonhosted.org/packages/e4/43/619c2c7a0665aafc80efca465ddb1f260287266bdbdce517396f2f145d49/numpy-2.2.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:52659ad2534427dffcc36aac76bebdd02b67e3b7a619ac67543bc9bfe6b7cdb1", size = 16114758 },
+ { url = "https://files.pythonhosted.org/packages/d9/79/ee4fe4f60967ccd3897aa71ae14cdee9e3c097e3256975cc9575d393cb42/numpy-2.2.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1b416af7d0ed3271cad0f0a0d0bee0911ed7eba23e66f8424d9f3dfcdcae1304", size = 15259880 },
+ { url = "https://files.pythonhosted.org/packages/fb/c8/8b55cf05db6d85b7a7d414b3d1bd5a740706df00bfa0824a08bf041e52ee/numpy-2.2.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1402da8e0f435991983d0a9708b779f95a8c98c6b18a171b9f1be09005e64d9d", size = 17876721 },
+ { url = "https://files.pythonhosted.org/packages/21/d6/b4c2f0564b7dcc413117b0ffbb818d837e4b29996b9234e38b2025ed24e7/numpy-2.2.3-cp313-cp313-win32.whl", hash = "sha256:136553f123ee2951bfcfbc264acd34a2fc2f29d7cdf610ce7daf672b6fbaa693", size = 6290195 },
+ { url = "https://files.pythonhosted.org/packages/97/e7/7d55a86719d0de7a6a597949f3febefb1009435b79ba510ff32f05a8c1d7/numpy-2.2.3-cp313-cp313-win_amd64.whl", hash = "sha256:5b732c8beef1d7bc2d9e476dbba20aaff6167bf205ad9aa8d30913859e82884b", size = 12619013 },
+ { url = "https://files.pythonhosted.org/packages/a6/1f/0b863d5528b9048fd486a56e0b97c18bf705e88736c8cea7239012119a54/numpy-2.2.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:435e7a933b9fda8126130b046975a968cc2d833b505475e588339e09f7672890", size = 20944621 },
+ { url = "https://files.pythonhosted.org/packages/aa/99/b478c384f7a0a2e0736177aafc97dc9152fc036a3fdb13f5a3ab225f1494/numpy-2.2.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:7678556eeb0152cbd1522b684dcd215250885993dd00adb93679ec3c0e6e091c", size = 14142502 },
+ { url = "https://files.pythonhosted.org/packages/fb/61/2d9a694a0f9cd0a839501d362de2a18de75e3004576a3008e56bdd60fcdb/numpy-2.2.3-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:2e8da03bd561504d9b20e7a12340870dfc206c64ea59b4cfee9fceb95070ee94", size = 5176293 },
+ { url = "https://files.pythonhosted.org/packages/33/35/51e94011b23e753fa33f891f601e5c1c9a3d515448659b06df9d40c0aa6e/numpy-2.2.3-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:c9aa4496fd0e17e3843399f533d62857cef5900facf93e735ef65aa4bbc90ef0", size = 6691874 },
+ { url = "https://files.pythonhosted.org/packages/ff/cf/06e37619aad98a9d03bd8d65b8e3041c3a639be0f5f6b0a0e2da544538d4/numpy-2.2.3-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4ca91d61a4bf61b0f2228f24bbfa6a9facd5f8af03759fe2a655c50ae2c6610", size = 14036826 },
+ { url = "https://files.pythonhosted.org/packages/0c/93/5d7d19955abd4d6099ef4a8ee006f9ce258166c38af259f9e5558a172e3e/numpy-2.2.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:deaa09cd492e24fd9b15296844c0ad1b3c976da7907e1c1ed3a0ad21dded6f76", size = 16096567 },
+ { url = "https://files.pythonhosted.org/packages/af/53/d1c599acf7732d81f46a93621dab6aa8daad914b502a7a115b3f17288ab2/numpy-2.2.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:246535e2f7496b7ac85deffe932896a3577be7af8fb7eebe7146444680297e9a", size = 15242514 },
+ { url = "https://files.pythonhosted.org/packages/53/43/c0f5411c7b3ea90adf341d05ace762dad8cb9819ef26093e27b15dd121ac/numpy-2.2.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:daf43a3d1ea699402c5a850e5313680ac355b4adc9770cd5cfc2940e7861f1bf", size = 17872920 },
+ { url = "https://files.pythonhosted.org/packages/5b/57/6dbdd45ab277aff62021cafa1e15f9644a52f5b5fc840bc7591b4079fb58/numpy-2.2.3-cp313-cp313t-win32.whl", hash = "sha256:cf802eef1f0134afb81fef94020351be4fe1d6681aadf9c5e862af6602af64ef", size = 6346584 },
+ { url = "https://files.pythonhosted.org/packages/97/9b/484f7d04b537d0a1202a5ba81c6f53f1846ae6c63c2127f8df869ed31342/numpy-2.2.3-cp313-cp313t-win_amd64.whl", hash = "sha256:aee2512827ceb6d7f517c8b85aa5d3923afe8fc7a57d028cffcd522f1c6fd082", size = 12706784 },
+ { url = "https://files.pythonhosted.org/packages/0a/b5/a7839f5478be8f859cb880f13d90fcfe4b0ec7a9ebaff2bcc30d96760596/numpy-2.2.3-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:3c2ec8a0f51d60f1e9c0c5ab116b7fc104b165ada3f6c58abf881cb2eb16044d", size = 21064244 },
+ { url = "https://files.pythonhosted.org/packages/29/e8/5da32ffcaa7a72f7ecd82f90c062140a061eb823cb88e90279424e515cf4/numpy-2.2.3-pp310-pypy310_pp73-macosx_14_0_x86_64.whl", hash = "sha256:ed2cf9ed4e8ebc3b754d398cba12f24359f018b416c380f577bbae112ca52fc9", size = 6809418 },
+ { url = "https://files.pythonhosted.org/packages/a8/a9/68aa7076c7656a7308a0f73d0a2ced8c03f282c9fd98fa7ce21c12634087/numpy-2.2.3-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39261798d208c3095ae4f7bc8eaeb3481ea8c6e03dc48028057d3cbdbdb8937e", size = 16215461 },
+ { url = "https://files.pythonhosted.org/packages/17/7f/d322a4125405920401450118dbdc52e0384026bd669939484670ce8b2ab9/numpy-2.2.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:783145835458e60fa97afac25d511d00a1eca94d4a8f3ace9fe2043003c678e4", size = 12839607 },
+]
+
+[[package]]
+name = "nvidia-cublas-cu12"
+version = "12.4.5.8"
+source = { registry = "https://pypi.org/simple" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ae/71/1c91302526c45ab494c23f61c7a84aa568b8c1f9d196efa5993957faf906/nvidia_cublas_cu12-12.4.5.8-py3-none-manylinux2014_x86_64.whl", hash = "sha256:2fc8da60df463fdefa81e323eef2e36489e1c94335b5358bcb38360adf75ac9b", size = 363438805 },
+]
+
+[[package]]
+name = "nvidia-cuda-cupti-cu12"
+version = "12.4.127"
+source = { registry = "https://pypi.org/simple" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/67/42/f4f60238e8194a3106d06a058d494b18e006c10bb2b915655bd9f6ea4cb1/nvidia_cuda_cupti_cu12-12.4.127-py3-none-manylinux2014_x86_64.whl", hash = "sha256:9dec60f5ac126f7bb551c055072b69d85392b13311fcc1bcda2202d172df30fb", size = 13813957 },
+]
+
+[[package]]
+name = "nvidia-cuda-nvrtc-cu12"
+version = "12.4.127"
+source = { registry = "https://pypi.org/simple" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/2c/14/91ae57cd4db3f9ef7aa99f4019cfa8d54cb4caa7e00975df6467e9725a9f/nvidia_cuda_nvrtc_cu12-12.4.127-py3-none-manylinux2014_x86_64.whl", hash = "sha256:a178759ebb095827bd30ef56598ec182b85547f1508941a3d560eb7ea1fbf338", size = 24640306 },
+]
+
+[[package]]
+name = "nvidia-cuda-runtime-cu12"
+version = "12.4.127"
+source = { registry = "https://pypi.org/simple" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ea/27/1795d86fe88ef397885f2e580ac37628ed058a92ed2c39dc8eac3adf0619/nvidia_cuda_runtime_cu12-12.4.127-py3-none-manylinux2014_x86_64.whl", hash = "sha256:64403288fa2136ee8e467cdc9c9427e0434110899d07c779f25b5c068934faa5", size = 883737 },
+]
+
+[[package]]
+name = "nvidia-cudnn-cu12"
+version = "9.1.0.70"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "nvidia-cublas-cu12", marker = "platform_machine != 'aarch64' and sys_platform == 'linux'" },
+]
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/9f/fd/713452cd72343f682b1c7b9321e23829f00b842ceaedcda96e742ea0b0b3/nvidia_cudnn_cu12-9.1.0.70-py3-none-manylinux2014_x86_64.whl", hash = "sha256:165764f44ef8c61fcdfdfdbe769d687e06374059fbb388b6c89ecb0e28793a6f", size = 664752741 },
+]
+
+[[package]]
+name = "nvidia-cufft-cu12"
+version = "11.2.1.3"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "nvidia-nvjitlink-cu12", marker = "platform_machine != 'aarch64' and sys_platform == 'linux'" },
+]
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/27/94/3266821f65b92b3138631e9c8e7fe1fb513804ac934485a8d05776e1dd43/nvidia_cufft_cu12-11.2.1.3-py3-none-manylinux2014_x86_64.whl", hash = "sha256:f083fc24912aa410be21fa16d157fed2055dab1cc4b6934a0e03cba69eb242b9", size = 211459117 },
+]
+
+[[package]]
+name = "nvidia-curand-cu12"
+version = "10.3.5.147"
+source = { registry = "https://pypi.org/simple" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/8a/6d/44ad094874c6f1b9c654f8ed939590bdc408349f137f9b98a3a23ccec411/nvidia_curand_cu12-10.3.5.147-py3-none-manylinux2014_x86_64.whl", hash = "sha256:a88f583d4e0bb643c49743469964103aa59f7f708d862c3ddb0fc07f851e3b8b", size = 56305206 },
+]
+
+[[package]]
+name = "nvidia-cusolver-cu12"
+version = "11.6.1.9"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "nvidia-cublas-cu12", marker = "platform_machine != 'aarch64' and sys_platform == 'linux'" },
+ { name = "nvidia-cusparse-cu12", marker = "platform_machine != 'aarch64' and sys_platform == 'linux'" },
+ { name = "nvidia-nvjitlink-cu12", marker = "platform_machine != 'aarch64' and sys_platform == 'linux'" },
+]
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/3a/e1/5b9089a4b2a4790dfdea8b3a006052cfecff58139d5a4e34cb1a51df8d6f/nvidia_cusolver_cu12-11.6.1.9-py3-none-manylinux2014_x86_64.whl", hash = "sha256:19e33fa442bcfd085b3086c4ebf7e8debc07cfe01e11513cc6d332fd918ac260", size = 127936057 },
+]
+
+[[package]]
+name = "nvidia-cusparse-cu12"
+version = "12.3.1.170"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "nvidia-nvjitlink-cu12", marker = "platform_machine != 'aarch64' and sys_platform == 'linux'" },
+]
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/db/f7/97a9ea26ed4bbbfc2d470994b8b4f338ef663be97b8f677519ac195e113d/nvidia_cusparse_cu12-12.3.1.170-py3-none-manylinux2014_x86_64.whl", hash = "sha256:ea4f11a2904e2a8dc4b1833cc1b5181cde564edd0d5cd33e3c168eff2d1863f1", size = 207454763 },
+]
+
+[[package]]
+name = "nvidia-cusparselt-cu12"
+version = "0.6.2"
+source = { registry = "https://pypi.org/simple" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/78/a8/bcbb63b53a4b1234feeafb65544ee55495e1bb37ec31b999b963cbccfd1d/nvidia_cusparselt_cu12-0.6.2-py3-none-manylinux2014_x86_64.whl", hash = "sha256:df2c24502fd76ebafe7457dbc4716b2fec071aabaed4fb7691a201cde03704d9", size = 150057751 },
+]
+
+[[package]]
+name = "nvidia-nccl-cu12"
+version = "2.21.5"
+source = { registry = "https://pypi.org/simple" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/df/99/12cd266d6233f47d00daf3a72739872bdc10267d0383508b0b9c84a18bb6/nvidia_nccl_cu12-2.21.5-py3-none-manylinux2014_x86_64.whl", hash = "sha256:8579076d30a8c24988834445f8d633c697d42397e92ffc3f63fa26766d25e0a0", size = 188654414 },
+]
+
+[[package]]
+name = "nvidia-nvjitlink-cu12"
+version = "12.4.127"
+source = { registry = "https://pypi.org/simple" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ff/ff/847841bacfbefc97a00036e0fce5a0f086b640756dc38caea5e1bb002655/nvidia_nvjitlink_cu12-12.4.127-py3-none-manylinux2014_x86_64.whl", hash = "sha256:06b3b9b25bf3f8af351d664978ca26a16d2c5127dbd53c0497e28d1fb9611d57", size = 21066810 },
+]
+
+[[package]]
+name = "nvidia-nvtx-cu12"
+version = "12.4.127"
+source = { registry = "https://pypi.org/simple" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/87/20/199b8713428322a2f22b722c62b8cc278cc53dffa9705d744484b5035ee9/nvidia_nvtx_cu12-12.4.127-py3-none-manylinux2014_x86_64.whl", hash = "sha256:781e950d9b9f60d8241ccea575b32f5105a5baf4c2351cab5256a24869f12a1a", size = 99144 },
+]
+
+[[package]]
+name = "opencv-contrib-python"
+version = "4.11.0.86"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "numpy" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/ef/51/3ceb85ecff5f26994b7aae2922b1aa38148dbfe88cab13d63bc6facbac88/opencv-contrib-python-4.11.0.86.tar.gz", hash = "sha256:4ff773dab44911da366b906621c9592d4eb96f6ad3777098933a23f064aab38e", size = 150559874 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/f3/78/b504ca8f7a312918d184e0b8093c62bc9a110d8154f658b591ef5c020d65/opencv_contrib_python-4.11.0.86-cp37-abi3-macosx_13_0_arm64.whl", hash = "sha256:d911cedc511d98f79994580b245d59fc97f57f0f9923a99945d8b92c7ac671f6", size = 46276766 },
+ { url = "https://files.pythonhosted.org/packages/8c/07/68e0b24217671b65c23e105bb7afd4ef4fd01507670cf5e61373d9efd6b5/opencv_contrib_python-4.11.0.86-cp37-abi3-macosx_13_0_x86_64.whl", hash = "sha256:e10a293af18aa5f842d012fa14e87345b3ee06db4c29bd592ff94b51f7ffca2b", size = 66524088 },
+ { url = "https://files.pythonhosted.org/packages/ae/7b/7e1471aa92f9f3c1bd8dbe624622b62add6f734db34fbbb9974e2ec70c34/opencv_contrib_python-4.11.0.86-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f21034bc8b00eb286a0a0a92b99767bf596bfe426cf4bc2e79647d64ad0dd6da", size = 47870560 },
+ { url = "https://files.pythonhosted.org/packages/f7/13/756b13b8d5d417a0b4c3bf6ceafb59df0ed05cec7fedc2490bbbf5e60ebc/opencv_contrib_python-4.11.0.86-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c47c0ef1098461cdc6fa1cdce4c942b8ec974c87423f4b5951443d26bb9ae407", size = 69098423 },
+ { url = "https://files.pythonhosted.org/packages/fd/8b/4f63d2fdcfceab528bff10c9d8d2a4e6230098e0b0af54e3e8e91b420ea0/opencv_contrib_python-4.11.0.86-cp37-abi3-win32.whl", hash = "sha256:194841c664ceaa0692410b4ed0af557425608e33db3a181ded28b87acb66748d", size = 35156028 },
+ { url = "https://files.pythonhosted.org/packages/0d/c6/146487546adc4726f0be591a65b466973feaa58cc3db711087e802e940fb/opencv_contrib_python-4.11.0.86-cp37-abi3-win_amd64.whl", hash = "sha256:654758a9ae8ca9a75fca7b64b19163636534f0eedffe1e14c3d7218988625c8d", size = 46185163 },
+]
+
+[[package]]
+name = "opencv-python"
+version = "4.11.0.86"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "numpy" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/17/06/68c27a523103dad5837dc5b87e71285280c4f098c60e4fe8a8db6486ab09/opencv-python-4.11.0.86.tar.gz", hash = "sha256:03d60ccae62304860d232272e4a4fda93c39d595780cb40b161b310244b736a4", size = 95171956 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/05/4d/53b30a2a3ac1f75f65a59eb29cf2ee7207ce64867db47036ad61743d5a23/opencv_python-4.11.0.86-cp37-abi3-macosx_13_0_arm64.whl", hash = "sha256:432f67c223f1dc2824f5e73cdfcd9db0efc8710647d4e813012195dc9122a52a", size = 37326322 },
+ { url = "https://files.pythonhosted.org/packages/3b/84/0a67490741867eacdfa37bc18df96e08a9d579583b419010d7f3da8ff503/opencv_python-4.11.0.86-cp37-abi3-macosx_13_0_x86_64.whl", hash = "sha256:9d05ef13d23fe97f575153558653e2d6e87103995d54e6a35db3f282fe1f9c66", size = 56723197 },
+ { url = "https://files.pythonhosted.org/packages/f3/bd/29c126788da65c1fb2b5fb621b7fed0ed5f9122aa22a0868c5e2c15c6d23/opencv_python-4.11.0.86-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1b92ae2c8852208817e6776ba1ea0d6b1e0a1b5431e971a2a0ddd2a8cc398202", size = 42230439 },
+ { url = "https://files.pythonhosted.org/packages/2c/8b/90eb44a40476fa0e71e05a0283947cfd74a5d36121a11d926ad6f3193cc4/opencv_python-4.11.0.86-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b02611523803495003bd87362db3e1d2a0454a6a63025dc6658a9830570aa0d", size = 62986597 },
+ { url = "https://files.pythonhosted.org/packages/fb/d7/1d5941a9dde095468b288d989ff6539dd69cd429dbf1b9e839013d21b6f0/opencv_python-4.11.0.86-cp37-abi3-win32.whl", hash = "sha256:810549cb2a4aedaa84ad9a1c92fbfdfc14090e2749cedf2c1589ad8359aa169b", size = 29384337 },
+ { url = "https://files.pythonhosted.org/packages/a4/7d/f1c30a92854540bf789e9cd5dde7ef49bbe63f855b85a2e6b3db8135c591/opencv_python-4.11.0.86-cp37-abi3-win_amd64.whl", hash = "sha256:085ad9b77c18853ea66283e98affefe2de8cc4c1f43eda4c100cf9b2721142ec", size = 39488044 },
+]
+
+[[package]]
+name = "opencv-python-headless"
+version = "4.11.0.86"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "numpy" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/36/2f/5b2b3ba52c864848885ba988f24b7f105052f68da9ab0e693cc7c25b0b30/opencv-python-headless-4.11.0.86.tar.gz", hash = "sha256:996eb282ca4b43ec6a3972414de0e2331f5d9cda2b41091a49739c19fb843798", size = 95177929 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/dc/53/2c50afa0b1e05ecdb4603818e85f7d174e683d874ef63a6abe3ac92220c8/opencv_python_headless-4.11.0.86-cp37-abi3-macosx_13_0_arm64.whl", hash = "sha256:48128188ade4a7e517237c8e1e11a9cdf5c282761473383e77beb875bb1e61ca", size = 37326460 },
+ { url = "https://files.pythonhosted.org/packages/3b/43/68555327df94bb9b59a1fd645f63fafb0762515344d2046698762fc19d58/opencv_python_headless-4.11.0.86-cp37-abi3-macosx_13_0_x86_64.whl", hash = "sha256:a66c1b286a9de872c343ee7c3553b084244299714ebb50fbdcd76f07ebbe6c81", size = 56723330 },
+ { url = "https://files.pythonhosted.org/packages/45/be/1438ce43ebe65317344a87e4b150865c5585f4c0db880a34cdae5ac46881/opencv_python_headless-4.11.0.86-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6efabcaa9df731f29e5ea9051776715b1bdd1845d7c9530065c7951d2a2899eb", size = 29487060 },
+ { url = "https://files.pythonhosted.org/packages/dd/5c/c139a7876099916879609372bfa513b7f1257f7f1a908b0bdc1c2328241b/opencv_python_headless-4.11.0.86-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e0a27c19dd1f40ddff94976cfe43066fbbe9dfbb2ec1907d66c19caef42a57b", size = 49969856 },
+ { url = "https://files.pythonhosted.org/packages/95/dd/ed1191c9dc91abcc9f752b499b7928aacabf10567bb2c2535944d848af18/opencv_python_headless-4.11.0.86-cp37-abi3-win32.whl", hash = "sha256:f447d8acbb0b6f2808da71fddd29c1cdd448d2bc98f72d9bb78a7a898fc9621b", size = 29324425 },
+ { url = "https://files.pythonhosted.org/packages/86/8a/69176a64335aed183529207ba8bc3d329c2999d852b4f3818027203f50e6/opencv_python_headless-4.11.0.86-cp37-abi3-win_amd64.whl", hash = "sha256:6c304df9caa7a6a5710b91709dd4786bf20a74d57672b3c31f7033cc638174ca", size = 39402386 },
+]
+
+[[package]]
+name = "packaging"
+version = "24.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/d0/63/68dbb6eb2de9cb10ee4c9c14a0148804425e13c4fb20d61cce69f53106da/packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f", size = 163950 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451 },
+]
+
+[[package]]
+name = "pillow"
+version = "11.1.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/f3/af/c097e544e7bd278333db77933e535098c259609c4eb3b85381109602fb5b/pillow-11.1.0.tar.gz", hash = "sha256:368da70808b36d73b4b390a8ffac11069f8a5c85f29eff1f1b01bcf3ef5b2a20", size = 46742715 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/50/1c/2dcea34ac3d7bc96a1fd1bd0a6e06a57c67167fec2cff8d95d88229a8817/pillow-11.1.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:e1abe69aca89514737465752b4bcaf8016de61b3be1397a8fc260ba33321b3a8", size = 3229983 },
+ { url = "https://files.pythonhosted.org/packages/14/ca/6bec3df25e4c88432681de94a3531cc738bd85dea6c7aa6ab6f81ad8bd11/pillow-11.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c640e5a06869c75994624551f45e5506e4256562ead981cce820d5ab39ae2192", size = 3101831 },
+ { url = "https://files.pythonhosted.org/packages/d4/2c/668e18e5521e46eb9667b09e501d8e07049eb5bfe39d56be0724a43117e6/pillow-11.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a07dba04c5e22824816b2615ad7a7484432d7f540e6fa86af60d2de57b0fcee2", size = 4314074 },
+ { url = "https://files.pythonhosted.org/packages/02/80/79f99b714f0fc25f6a8499ecfd1f810df12aec170ea1e32a4f75746051ce/pillow-11.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e267b0ed063341f3e60acd25c05200df4193e15a4a5807075cd71225a2386e26", size = 4394933 },
+ { url = "https://files.pythonhosted.org/packages/81/aa/8d4ad25dc11fd10a2001d5b8a80fdc0e564ac33b293bdfe04ed387e0fd95/pillow-11.1.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:bd165131fd51697e22421d0e467997ad31621b74bfc0b75956608cb2906dda07", size = 4353349 },
+ { url = "https://files.pythonhosted.org/packages/84/7a/cd0c3eaf4a28cb2a74bdd19129f7726277a7f30c4f8424cd27a62987d864/pillow-11.1.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:abc56501c3fd148d60659aae0af6ddc149660469082859fa7b066a298bde9482", size = 4476532 },
+ { url = "https://files.pythonhosted.org/packages/8f/8b/a907fdd3ae8f01c7670dfb1499c53c28e217c338b47a813af8d815e7ce97/pillow-11.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:54ce1c9a16a9561b6d6d8cb30089ab1e5eb66918cb47d457bd996ef34182922e", size = 4279789 },
+ { url = "https://files.pythonhosted.org/packages/6f/9a/9f139d9e8cccd661c3efbf6898967a9a337eb2e9be2b454ba0a09533100d/pillow-11.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:73ddde795ee9b06257dac5ad42fcb07f3b9b813f8c1f7f870f402f4dc54b5269", size = 4413131 },
+ { url = "https://files.pythonhosted.org/packages/a8/68/0d8d461f42a3f37432203c8e6df94da10ac8081b6d35af1c203bf3111088/pillow-11.1.0-cp310-cp310-win32.whl", hash = "sha256:3a5fe20a7b66e8135d7fd617b13272626a28278d0e578c98720d9ba4b2439d49", size = 2291213 },
+ { url = "https://files.pythonhosted.org/packages/14/81/d0dff759a74ba87715509af9f6cb21fa21d93b02b3316ed43bda83664db9/pillow-11.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:b6123aa4a59d75f06e9dd3dac5bf8bc9aa383121bb3dd9a7a612e05eabc9961a", size = 2625725 },
+ { url = "https://files.pythonhosted.org/packages/ce/1f/8d50c096a1d58ef0584ddc37e6f602828515219e9d2428e14ce50f5ecad1/pillow-11.1.0-cp310-cp310-win_arm64.whl", hash = "sha256:a76da0a31da6fcae4210aa94fd779c65c75786bc9af06289cd1c184451ef7a65", size = 2375213 },
+ { url = "https://files.pythonhosted.org/packages/dd/d6/2000bfd8d5414fb70cbbe52c8332f2283ff30ed66a9cde42716c8ecbe22c/pillow-11.1.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:e06695e0326d05b06833b40b7ef477e475d0b1ba3a6d27da1bb48c23209bf457", size = 3229968 },
+ { url = "https://files.pythonhosted.org/packages/d9/45/3fe487010dd9ce0a06adf9b8ff4f273cc0a44536e234b0fad3532a42c15b/pillow-11.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:96f82000e12f23e4f29346e42702b6ed9a2f2fea34a740dd5ffffcc8c539eb35", size = 3101806 },
+ { url = "https://files.pythonhosted.org/packages/e3/72/776b3629c47d9d5f1c160113158a7a7ad177688d3a1159cd3b62ded5a33a/pillow-11.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3cd561ded2cf2bbae44d4605837221b987c216cff94f49dfeed63488bb228d2", size = 4322283 },
+ { url = "https://files.pythonhosted.org/packages/e4/c2/e25199e7e4e71d64eeb869f5b72c7ddec70e0a87926398785ab944d92375/pillow-11.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f189805c8be5ca5add39e6f899e6ce2ed824e65fb45f3c28cb2841911da19070", size = 4402945 },
+ { url = "https://files.pythonhosted.org/packages/c1/ed/51d6136c9d5911f78632b1b86c45241c712c5a80ed7fa7f9120a5dff1eba/pillow-11.1.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:dd0052e9db3474df30433f83a71b9b23bd9e4ef1de13d92df21a52c0303b8ab6", size = 4361228 },
+ { url = "https://files.pythonhosted.org/packages/48/a4/fbfe9d5581d7b111b28f1d8c2762dee92e9821bb209af9fa83c940e507a0/pillow-11.1.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:837060a8599b8f5d402e97197d4924f05a2e0d68756998345c829c33186217b1", size = 4484021 },
+ { url = "https://files.pythonhosted.org/packages/39/db/0b3c1a5018117f3c1d4df671fb8e47d08937f27519e8614bbe86153b65a5/pillow-11.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:aa8dd43daa836b9a8128dbe7d923423e5ad86f50a7a14dc688194b7be5c0dea2", size = 4287449 },
+ { url = "https://files.pythonhosted.org/packages/d9/58/bc128da7fea8c89fc85e09f773c4901e95b5936000e6f303222490c052f3/pillow-11.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0a2f91f8a8b367e7a57c6e91cd25af510168091fb89ec5146003e424e1558a96", size = 4419972 },
+ { url = "https://files.pythonhosted.org/packages/5f/bb/58f34379bde9fe197f51841c5bbe8830c28bbb6d3801f16a83b8f2ad37df/pillow-11.1.0-cp311-cp311-win32.whl", hash = "sha256:c12fc111ef090845de2bb15009372175d76ac99969bdf31e2ce9b42e4b8cd88f", size = 2291201 },
+ { url = "https://files.pythonhosted.org/packages/3a/c6/fce9255272bcf0c39e15abd2f8fd8429a954cf344469eaceb9d0d1366913/pillow-11.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:fbd43429d0d7ed6533b25fc993861b8fd512c42d04514a0dd6337fb3ccf22761", size = 2625686 },
+ { url = "https://files.pythonhosted.org/packages/c8/52/8ba066d569d932365509054859f74f2a9abee273edcef5cd75e4bc3e831e/pillow-11.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:f7955ecf5609dee9442cbface754f2c6e541d9e6eda87fad7f7a989b0bdb9d71", size = 2375194 },
+ { url = "https://files.pythonhosted.org/packages/95/20/9ce6ed62c91c073fcaa23d216e68289e19d95fb8188b9fb7a63d36771db8/pillow-11.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2062ffb1d36544d42fcaa277b069c88b01bb7298f4efa06731a7fd6cc290b81a", size = 3226818 },
+ { url = "https://files.pythonhosted.org/packages/b9/d8/f6004d98579a2596c098d1e30d10b248798cceff82d2b77aa914875bfea1/pillow-11.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a85b653980faad27e88b141348707ceeef8a1186f75ecc600c395dcac19f385b", size = 3101662 },
+ { url = "https://files.pythonhosted.org/packages/08/d9/892e705f90051c7a2574d9f24579c9e100c828700d78a63239676f960b74/pillow-11.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9409c080586d1f683df3f184f20e36fb647f2e0bc3988094d4fd8c9f4eb1b3b3", size = 4329317 },
+ { url = "https://files.pythonhosted.org/packages/8c/aa/7f29711f26680eab0bcd3ecdd6d23ed6bce180d82e3f6380fb7ae35fcf3b/pillow-11.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7fdadc077553621911f27ce206ffcbec7d3f8d7b50e0da39f10997e8e2bb7f6a", size = 4412999 },
+ { url = "https://files.pythonhosted.org/packages/c8/c4/8f0fe3b9e0f7196f6d0bbb151f9fba323d72a41da068610c4c960b16632a/pillow-11.1.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:93a18841d09bcdd774dcdc308e4537e1f867b3dec059c131fde0327899734aa1", size = 4368819 },
+ { url = "https://files.pythonhosted.org/packages/38/0d/84200ed6a871ce386ddc82904bfadc0c6b28b0c0ec78176871a4679e40b3/pillow-11.1.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:9aa9aeddeed452b2f616ff5507459e7bab436916ccb10961c4a382cd3e03f47f", size = 4496081 },
+ { url = "https://files.pythonhosted.org/packages/84/9c/9bcd66f714d7e25b64118e3952d52841a4babc6d97b6d28e2261c52045d4/pillow-11.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3cdcdb0b896e981678eee140d882b70092dac83ac1cdf6b3a60e2216a73f2b91", size = 4296513 },
+ { url = "https://files.pythonhosted.org/packages/db/61/ada2a226e22da011b45f7104c95ebda1b63dcbb0c378ad0f7c2a710f8fd2/pillow-11.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:36ba10b9cb413e7c7dfa3e189aba252deee0602c86c309799da5a74009ac7a1c", size = 4431298 },
+ { url = "https://files.pythonhosted.org/packages/e7/c4/fc6e86750523f367923522014b821c11ebc5ad402e659d8c9d09b3c9d70c/pillow-11.1.0-cp312-cp312-win32.whl", hash = "sha256:cfd5cd998c2e36a862d0e27b2df63237e67273f2fc78f47445b14e73a810e7e6", size = 2291630 },
+ { url = "https://files.pythonhosted.org/packages/08/5c/2104299949b9d504baf3f4d35f73dbd14ef31bbd1ddc2c1b66a5b7dfda44/pillow-11.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:a697cd8ba0383bba3d2d3ada02b34ed268cb548b369943cd349007730c92bddf", size = 2626369 },
+ { url = "https://files.pythonhosted.org/packages/37/f3/9b18362206b244167c958984b57c7f70a0289bfb59a530dd8af5f699b910/pillow-11.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:4dd43a78897793f60766563969442020e90eb7847463eca901e41ba186a7d4a5", size = 2375240 },
+ { url = "https://files.pythonhosted.org/packages/b3/31/9ca79cafdce364fd5c980cd3416c20ce1bebd235b470d262f9d24d810184/pillow-11.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ae98e14432d458fc3de11a77ccb3ae65ddce70f730e7c76140653048c71bfcbc", size = 3226640 },
+ { url = "https://files.pythonhosted.org/packages/ac/0f/ff07ad45a1f172a497aa393b13a9d81a32e1477ef0e869d030e3c1532521/pillow-11.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cc1331b6d5a6e144aeb5e626f4375f5b7ae9934ba620c0ac6b3e43d5e683a0f0", size = 3101437 },
+ { url = "https://files.pythonhosted.org/packages/08/2f/9906fca87a68d29ec4530be1f893149e0cb64a86d1f9f70a7cfcdfe8ae44/pillow-11.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:758e9d4ef15d3560214cddbc97b8ef3ef86ce04d62ddac17ad39ba87e89bd3b1", size = 4326605 },
+ { url = "https://files.pythonhosted.org/packages/b0/0f/f3547ee15b145bc5c8b336401b2d4c9d9da67da9dcb572d7c0d4103d2c69/pillow-11.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b523466b1a31d0dcef7c5be1f20b942919b62fd6e9a9be199d035509cbefc0ec", size = 4411173 },
+ { url = "https://files.pythonhosted.org/packages/b1/df/bf8176aa5db515c5de584c5e00df9bab0713548fd780c82a86cba2c2fedb/pillow-11.1.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:9044b5e4f7083f209c4e35aa5dd54b1dd5b112b108648f5c902ad586d4f945c5", size = 4369145 },
+ { url = "https://files.pythonhosted.org/packages/de/7c/7433122d1cfadc740f577cb55526fdc39129a648ac65ce64db2eb7209277/pillow-11.1.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:3764d53e09cdedd91bee65c2527815d315c6b90d7b8b79759cc48d7bf5d4f114", size = 4496340 },
+ { url = "https://files.pythonhosted.org/packages/25/46/dd94b93ca6bd555588835f2504bd90c00d5438fe131cf01cfa0c5131a19d/pillow-11.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:31eba6bbdd27dde97b0174ddf0297d7a9c3a507a8a1480e1e60ef914fe23d352", size = 4296906 },
+ { url = "https://files.pythonhosted.org/packages/a8/28/2f9d32014dfc7753e586db9add35b8a41b7a3b46540e965cb6d6bc607bd2/pillow-11.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b5d658fbd9f0d6eea113aea286b21d3cd4d3fd978157cbf2447a6035916506d3", size = 4431759 },
+ { url = "https://files.pythonhosted.org/packages/33/48/19c2cbe7403870fbe8b7737d19eb013f46299cdfe4501573367f6396c775/pillow-11.1.0-cp313-cp313-win32.whl", hash = "sha256:f86d3a7a9af5d826744fabf4afd15b9dfef44fe69a98541f666f66fbb8d3fef9", size = 2291657 },
+ { url = "https://files.pythonhosted.org/packages/3b/ad/285c556747d34c399f332ba7c1a595ba245796ef3e22eae190f5364bb62b/pillow-11.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:593c5fd6be85da83656b93ffcccc2312d2d149d251e98588b14fbc288fd8909c", size = 2626304 },
+ { url = "https://files.pythonhosted.org/packages/e5/7b/ef35a71163bf36db06e9c8729608f78dedf032fc8313d19bd4be5c2588f3/pillow-11.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:11633d58b6ee5733bde153a8dafd25e505ea3d32e261accd388827ee987baf65", size = 2375117 },
+ { url = "https://files.pythonhosted.org/packages/79/30/77f54228401e84d6791354888549b45824ab0ffde659bafa67956303a09f/pillow-11.1.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:70ca5ef3b3b1c4a0812b5c63c57c23b63e53bc38e758b37a951e5bc466449861", size = 3230060 },
+ { url = "https://files.pythonhosted.org/packages/ce/b1/56723b74b07dd64c1010fee011951ea9c35a43d8020acd03111f14298225/pillow-11.1.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:8000376f139d4d38d6851eb149b321a52bb8893a88dae8ee7d95840431977081", size = 3106192 },
+ { url = "https://files.pythonhosted.org/packages/e1/cd/7bf7180e08f80a4dcc6b4c3a0aa9e0b0ae57168562726a05dc8aa8fa66b0/pillow-11.1.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ee85f0696a17dd28fbcfceb59f9510aa71934b483d1f5601d1030c3c8304f3c", size = 4446805 },
+ { url = "https://files.pythonhosted.org/packages/97/42/87c856ea30c8ed97e8efbe672b58c8304dee0573f8c7cab62ae9e31db6ae/pillow-11.1.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:dd0e081319328928531df7a0e63621caf67652c8464303fd102141b785ef9547", size = 4530623 },
+ { url = "https://files.pythonhosted.org/packages/ff/41/026879e90c84a88e33fb00cc6bd915ac2743c67e87a18f80270dfe3c2041/pillow-11.1.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e63e4e5081de46517099dc30abe418122f54531a6ae2ebc8680bcd7096860eab", size = 4465191 },
+ { url = "https://files.pythonhosted.org/packages/e5/fb/a7960e838bc5df57a2ce23183bfd2290d97c33028b96bde332a9057834d3/pillow-11.1.0-cp313-cp313t-win32.whl", hash = "sha256:dda60aa465b861324e65a78c9f5cf0f4bc713e4309f83bc387be158b077963d9", size = 2295494 },
+ { url = "https://files.pythonhosted.org/packages/d7/6c/6ec83ee2f6f0fda8d4cf89045c6be4b0373ebfc363ba8538f8c999f63fcd/pillow-11.1.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ad5db5781c774ab9a9b2c4302bbf0c1014960a0a7be63278d13ae6fdf88126fe", size = 2631595 },
+ { url = "https://files.pythonhosted.org/packages/cf/6c/41c21c6c8af92b9fea313aa47c75de49e2f9a467964ee33eb0135d47eb64/pillow-11.1.0-cp313-cp313t-win_arm64.whl", hash = "sha256:67cd427c68926108778a9005f2a04adbd5e67c442ed21d95389fe1d595458756", size = 2377651 },
+ { url = "https://files.pythonhosted.org/packages/fa/c5/389961578fb677b8b3244fcd934f720ed25a148b9a5cc81c91bdf59d8588/pillow-11.1.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:8c730dc3a83e5ac137fbc92dfcfe1511ce3b2b5d7578315b63dbbb76f7f51d90", size = 3198345 },
+ { url = "https://files.pythonhosted.org/packages/c4/fa/803c0e50ffee74d4b965229e816af55276eac1d5806712de86f9371858fd/pillow-11.1.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:7d33d2fae0e8b170b6a6c57400e077412240f6f5bb2a342cf1ee512a787942bb", size = 3072938 },
+ { url = "https://files.pythonhosted.org/packages/dc/67/2a3a5f8012b5d8c63fe53958ba906c1b1d0482ebed5618057ef4d22f8076/pillow-11.1.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a8d65b38173085f24bc07f8b6c505cbb7418009fa1a1fcb111b1f4961814a442", size = 3400049 },
+ { url = "https://files.pythonhosted.org/packages/e5/a0/514f0d317446c98c478d1872497eb92e7cde67003fed74f696441e647446/pillow-11.1.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:015c6e863faa4779251436db398ae75051469f7c903b043a48f078e437656f83", size = 3422431 },
+ { url = "https://files.pythonhosted.org/packages/cd/00/20f40a935514037b7d3f87adfc87d2c538430ea625b63b3af8c3f5578e72/pillow-11.1.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:d44ff19eea13ae4acdaaab0179fa68c0c6f2f45d66a4d8ec1eda7d6cecbcc15f", size = 3446208 },
+ { url = "https://files.pythonhosted.org/packages/28/3c/7de681727963043e093c72e6c3348411b0185eab3263100d4490234ba2f6/pillow-11.1.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:d3d8da4a631471dfaf94c10c85f5277b1f8e42ac42bade1ac67da4b4a7359b73", size = 3509746 },
+ { url = "https://files.pythonhosted.org/packages/41/67/936f9814bdd74b2dfd4822f1f7725ab5d8ff4103919a1664eb4874c58b2f/pillow-11.1.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:4637b88343166249fe8aa94e7c4a62a180c4b3898283bb5d3d2fd5fe10d8e4e0", size = 2626353 },
+]
+
+[[package]]
+name = "platformdirs"
+version = "4.3.6"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/13/fc/128cc9cb8f03208bdbf93d3aa862e16d376844a14f9a0ce5cf4507372de4/platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907", size = 21302 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/3c/a6/bc1012356d8ece4d66dd75c4b9fc6c1f6650ddd5991e421177d9f8f671be/platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb", size = 18439 },
+]
+
+[[package]]
+name = "poselib"
+version = "2.0.4"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "numpy" },
+]
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/1c/9a/cb950fc87016a473389e34db627f6da7efd22513192b5784ebdf6be81a33/poselib-2.0.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7f03adb3ebf56560d907c48d8923575f9d6f60062fc7ef42a4d4d59d8fd0219d", size = 1019185 },
+ { url = "https://files.pythonhosted.org/packages/43/d9/05b4bfb253b644226e092d79fcba970514460b9e62861ecb730e74af0b8f/poselib-2.0.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ee49a881bad44d89635905bd1d03fe32d02482e54f481c59a64f75ee807e9e5b", size = 781388 },
+ { url = "https://files.pythonhosted.org/packages/99/cb/e840016ba433ab379ff51a671fac4f9ac469083d40fb0c0ba2cd8175738f/poselib-2.0.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8def5aec5f147df7a8f1fae4ca87e5997b403192dbff39c088575c7a3401b97a", size = 1155705 },
+ { url = "https://files.pythonhosted.org/packages/b7/f1/496c24caae9d71865a4cb6a8f177166095020d30a45defc5035d15fb041b/poselib-2.0.4-cp310-cp310-win_amd64.whl", hash = "sha256:2435e17c5947dc123c387b0ef7011e2fdd8157913978deabe4cf086f8d501816", size = 831642 },
+ { url = "https://files.pythonhosted.org/packages/69/b2/1cc58f6ae08d0db555e23e58d77e3d54cf73a5639184533afe1b2ec2e6f4/poselib-2.0.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c170206f66b839ca45ba90b96cba8e8a46c727abbbedaff9b1514804fc74533e", size = 1020704 },
+ { url = "https://files.pythonhosted.org/packages/0f/d5/b0ded0f226919a70bb50a92c88d752bce40e0bda1dc62ed87ea828007bc9/poselib-2.0.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b0d4c98981e947ed10043fc68d2de0ef4d20610142f22d7f0dae570c9dd6cf3b", size = 782559 },
+ { url = "https://files.pythonhosted.org/packages/c5/c5/d67e9e7fc4588821085810eeb07590f9275038134c4b52d880d8769d5f44/poselib-2.0.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c96d08a402ceb8ff29e0d9b63ac88755a5d55820bcafd76b5fe4cd1b6631ab19", size = 1157065 },
+ { url = "https://files.pythonhosted.org/packages/e3/29/6abde732ddf8ce505374ca94ce87fac2768902a678f5e202597d3347d2ab/poselib-2.0.4-cp311-cp311-win_amd64.whl", hash = "sha256:ab5e4493029e245a35535a5fcdcc099dc21a55141d3040bc450be17cd8ece824", size = 832941 },
+ { url = "https://files.pythonhosted.org/packages/05/ed/43f01ab0179970989c3dca1d985eba748dba92d58436d56e995bf0eef7cd/poselib-2.0.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:0a7f361d63d334530eb31a254a91aa13c946518b6ffc604dd20b28a2aad880a7", size = 1023554 },
+ { url = "https://files.pythonhosted.org/packages/34/9e/8b389e571bea06ade55429f684559d71aebcd514ff0dc8383afd19d3e389/poselib-2.0.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ced221bec9d451e08efd53876045333acf3f1b0f3eae035dce3d65378b8c8c47", size = 783047 },
+ { url = "https://files.pythonhosted.org/packages/0e/98/69d919c882de74d6876844d944f7ee7b8cf77c2435143763061a1579e82d/poselib-2.0.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:48c54a29ab08c3623b8e10d1c94a057b39eea7b39095ef6656138eb59602329c", size = 1155968 },
+ { url = "https://files.pythonhosted.org/packages/9a/7a/a22a3dbfe68a09565c27d6c2489bee2d85f9b8f76b37ba55ec8f834ecdd7/poselib-2.0.4-cp312-cp312-win_amd64.whl", hash = "sha256:89072889fe2190553c3ffc5a8a22520fe54ff07711c6f15baab8b797508663d2", size = 832553 },
+]
+
+[[package]]
+name = "protobuf"
+version = "5.29.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/f7/d1/e0a911544ca9993e0f17ce6d3cc0932752356c1b0a834397f28e63479344/protobuf-5.29.3.tar.gz", hash = "sha256:5da0f41edaf117bde316404bad1a486cb4ededf8e4a54891296f648e8e076620", size = 424945 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/dc/7a/1e38f3cafa022f477ca0f57a1f49962f21ad25850c3ca0acd3b9d0091518/protobuf-5.29.3-cp310-abi3-win32.whl", hash = "sha256:3ea51771449e1035f26069c4c7fd51fba990d07bc55ba80701c78f886bf9c888", size = 422708 },
+ { url = "https://files.pythonhosted.org/packages/61/fa/aae8e10512b83de633f2646506a6d835b151edf4b30d18d73afd01447253/protobuf-5.29.3-cp310-abi3-win_amd64.whl", hash = "sha256:a4fa6f80816a9a0678429e84973f2f98cbc218cca434abe8db2ad0bffc98503a", size = 434508 },
+ { url = "https://files.pythonhosted.org/packages/dd/04/3eaedc2ba17a088961d0e3bd396eac764450f431621b58a04ce898acd126/protobuf-5.29.3-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:a8434404bbf139aa9e1300dbf989667a83d42ddda9153d8ab76e0d5dcaca484e", size = 417825 },
+ { url = "https://files.pythonhosted.org/packages/4f/06/7c467744d23c3979ce250397e26d8ad8eeb2bea7b18ca12ad58313c1b8d5/protobuf-5.29.3-cp38-abi3-manylinux2014_aarch64.whl", hash = "sha256:daaf63f70f25e8689c072cfad4334ca0ac1d1e05a92fc15c54eb9cf23c3efd84", size = 319573 },
+ { url = "https://files.pythonhosted.org/packages/a8/45/2ebbde52ad2be18d3675b6bee50e68cd73c9e0654de77d595540b5129df8/protobuf-5.29.3-cp38-abi3-manylinux2014_x86_64.whl", hash = "sha256:c027e08a08be10b67c06bf2370b99c811c466398c357e615ca88c91c07f0910f", size = 319672 },
+ { url = "https://files.pythonhosted.org/packages/fd/b2/ab07b09e0f6d143dfb839693aa05765257bceaa13d03bf1a696b78323e7a/protobuf-5.29.3-py3-none-any.whl", hash = "sha256:0a18ed4a24198528f2333802eb075e59dea9d679ab7a6c5efb017a59004d849f", size = 172550 },
+]
+
+[[package]]
+name = "psutil"
+version = "7.0.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/2a/80/336820c1ad9286a4ded7e845b2eccfcb27851ab8ac6abece774a6ff4d3de/psutil-7.0.0.tar.gz", hash = "sha256:7be9c3eba38beccb6495ea33afd982a44074b78f28c434a1f51cc07fd315c456", size = 497003 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ed/e6/2d26234410f8b8abdbf891c9da62bee396583f713fb9f3325a4760875d22/psutil-7.0.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:101d71dc322e3cffd7cea0650b09b3d08b8e7c4109dd6809fe452dfd00e58b25", size = 238051 },
+ { url = "https://files.pythonhosted.org/packages/04/8b/30f930733afe425e3cbfc0e1468a30a18942350c1a8816acfade80c005c4/psutil-7.0.0-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:39db632f6bb862eeccf56660871433e111b6ea58f2caea825571951d4b6aa3da", size = 239535 },
+ { url = "https://files.pythonhosted.org/packages/2a/ed/d362e84620dd22876b55389248e522338ed1bf134a5edd3b8231d7207f6d/psutil-7.0.0-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1fcee592b4c6f146991ca55919ea3d1f8926497a713ed7faaf8225e174581e91", size = 275004 },
+ { url = "https://files.pythonhosted.org/packages/bf/b9/b0eb3f3cbcb734d930fdf839431606844a825b23eaf9a6ab371edac8162c/psutil-7.0.0-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b1388a4f6875d7e2aff5c4ca1cc16c545ed41dd8bb596cefea80111db353a34", size = 277986 },
+ { url = "https://files.pythonhosted.org/packages/eb/a2/709e0fe2f093556c17fbafda93ac032257242cabcc7ff3369e2cb76a97aa/psutil-7.0.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5f098451abc2828f7dc6b58d44b532b22f2088f4999a937557b603ce72b1993", size = 279544 },
+ { url = "https://files.pythonhosted.org/packages/50/e6/eecf58810b9d12e6427369784efe814a1eec0f492084ce8eb8f4d89d6d61/psutil-7.0.0-cp37-abi3-win32.whl", hash = "sha256:ba3fcef7523064a6c9da440fc4d6bd07da93ac726b5733c29027d7dc95b39d99", size = 241053 },
+ { url = "https://files.pythonhosted.org/packages/50/1b/6921afe68c74868b4c9fa424dad3be35b095e16687989ebbb50ce4fceb7c/psutil-7.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:4cf3d4eb1aa9b348dec30105c55cd9b7d4629285735a102beb4441e38db90553", size = 244885 },
+]
+
+[[package]]
+name = "pydantic"
+version = "2.10.6"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "annotated-types" },
+ { name = "pydantic-core" },
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/b7/ae/d5220c5c52b158b1de7ca89fc5edb72f304a70a4c540c84c8844bf4008de/pydantic-2.10.6.tar.gz", hash = "sha256:ca5daa827cce33de7a42be142548b0096bf05a7e7b365aebfa5f8eeec7128236", size = 761681 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/f4/3c/8cc1cc84deffa6e25d2d0c688ebb80635dfdbf1dbea3e30c541c8cf4d860/pydantic-2.10.6-py3-none-any.whl", hash = "sha256:427d664bf0b8a2b34ff5dd0f5a18df00591adcee7198fbd71981054cef37b584", size = 431696 },
+]
+
+[[package]]
+name = "pydantic-core"
+version = "2.27.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/fc/01/f3e5ac5e7c25833db5eb555f7b7ab24cd6f8c322d3a3ad2d67a952dc0abc/pydantic_core-2.27.2.tar.gz", hash = "sha256:eb026e5a4c1fee05726072337ff51d1efb6f59090b7da90d30ea58625b1ffb39", size = 413443 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/3a/bc/fed5f74b5d802cf9a03e83f60f18864e90e3aed7223adaca5ffb7a8d8d64/pydantic_core-2.27.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2d367ca20b2f14095a8f4fa1210f5a7b78b8a20009ecced6b12818f455b1e9fa", size = 1895938 },
+ { url = "https://files.pythonhosted.org/packages/71/2a/185aff24ce844e39abb8dd680f4e959f0006944f4a8a0ea372d9f9ae2e53/pydantic_core-2.27.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:491a2b73db93fab69731eaee494f320faa4e093dbed776be1a829c2eb222c34c", size = 1815684 },
+ { url = "https://files.pythonhosted.org/packages/c3/43/fafabd3d94d159d4f1ed62e383e264f146a17dd4d48453319fd782e7979e/pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7969e133a6f183be60e9f6f56bfae753585680f3b7307a8e555a948d443cc05a", size = 1829169 },
+ { url = "https://files.pythonhosted.org/packages/a2/d1/f2dfe1a2a637ce6800b799aa086d079998959f6f1215eb4497966efd2274/pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3de9961f2a346257caf0aa508a4da705467f53778e9ef6fe744c038119737ef5", size = 1867227 },
+ { url = "https://files.pythonhosted.org/packages/7d/39/e06fcbcc1c785daa3160ccf6c1c38fea31f5754b756e34b65f74e99780b5/pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e2bb4d3e5873c37bb3dd58714d4cd0b0e6238cebc4177ac8fe878f8b3aa8e74c", size = 2037695 },
+ { url = "https://files.pythonhosted.org/packages/7a/67/61291ee98e07f0650eb756d44998214231f50751ba7e13f4f325d95249ab/pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:280d219beebb0752699480fe8f1dc61ab6615c2046d76b7ab7ee38858de0a4e7", size = 2741662 },
+ { url = "https://files.pythonhosted.org/packages/32/90/3b15e31b88ca39e9e626630b4c4a1f5a0dfd09076366f4219429e6786076/pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47956ae78b6422cbd46f772f1746799cbb862de838fd8d1fbd34a82e05b0983a", size = 1993370 },
+ { url = "https://files.pythonhosted.org/packages/ff/83/c06d333ee3a67e2e13e07794995c1535565132940715931c1c43bfc85b11/pydantic_core-2.27.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:14d4a5c49d2f009d62a2a7140d3064f686d17a5d1a268bc641954ba181880236", size = 1996813 },
+ { url = "https://files.pythonhosted.org/packages/7c/f7/89be1c8deb6e22618a74f0ca0d933fdcb8baa254753b26b25ad3acff8f74/pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:337b443af21d488716f8d0b6164de833e788aa6bd7e3a39c005febc1284f4962", size = 2005287 },
+ { url = "https://files.pythonhosted.org/packages/b7/7d/8eb3e23206c00ef7feee17b83a4ffa0a623eb1a9d382e56e4aa46fd15ff2/pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:03d0f86ea3184a12f41a2d23f7ccb79cdb5a18e06993f8a45baa8dfec746f0e9", size = 2128414 },
+ { url = "https://files.pythonhosted.org/packages/4e/99/fe80f3ff8dd71a3ea15763878d464476e6cb0a2db95ff1c5c554133b6b83/pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7041c36f5680c6e0f08d922aed302e98b3745d97fe1589db0a3eebf6624523af", size = 2155301 },
+ { url = "https://files.pythonhosted.org/packages/2b/a3/e50460b9a5789ca1451b70d4f52546fa9e2b420ba3bfa6100105c0559238/pydantic_core-2.27.2-cp310-cp310-win32.whl", hash = "sha256:50a68f3e3819077be2c98110c1f9dcb3817e93f267ba80a2c05bb4f8799e2ff4", size = 1816685 },
+ { url = "https://files.pythonhosted.org/packages/57/4c/a8838731cb0f2c2a39d3535376466de6049034d7b239c0202a64aaa05533/pydantic_core-2.27.2-cp310-cp310-win_amd64.whl", hash = "sha256:e0fd26b16394ead34a424eecf8a31a1f5137094cabe84a1bcb10fa6ba39d3d31", size = 1982876 },
+ { url = "https://files.pythonhosted.org/packages/c2/89/f3450af9d09d44eea1f2c369f49e8f181d742f28220f88cc4dfaae91ea6e/pydantic_core-2.27.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:8e10c99ef58cfdf2a66fc15d66b16c4a04f62bca39db589ae8cba08bc55331bc", size = 1893421 },
+ { url = "https://files.pythonhosted.org/packages/9e/e3/71fe85af2021f3f386da42d291412e5baf6ce7716bd7101ea49c810eda90/pydantic_core-2.27.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:26f32e0adf166a84d0cb63be85c562ca8a6fa8de28e5f0d92250c6b7e9e2aff7", size = 1814998 },
+ { url = "https://files.pythonhosted.org/packages/a6/3c/724039e0d848fd69dbf5806894e26479577316c6f0f112bacaf67aa889ac/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c19d1ea0673cd13cc2f872f6c9ab42acc4e4f492a7ca9d3795ce2b112dd7e15", size = 1826167 },
+ { url = "https://files.pythonhosted.org/packages/2b/5b/1b29e8c1fb5f3199a9a57c1452004ff39f494bbe9bdbe9a81e18172e40d3/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5e68c4446fe0810e959cdff46ab0a41ce2f2c86d227d96dc3847af0ba7def306", size = 1865071 },
+ { url = "https://files.pythonhosted.org/packages/89/6c/3985203863d76bb7d7266e36970d7e3b6385148c18a68cc8915fd8c84d57/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d9640b0059ff4f14d1f37321b94061c6db164fbe49b334b31643e0528d100d99", size = 2036244 },
+ { url = "https://files.pythonhosted.org/packages/0e/41/f15316858a246b5d723f7d7f599f79e37493b2e84bfc789e58d88c209f8a/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:40d02e7d45c9f8af700f3452f329ead92da4c5f4317ca9b896de7ce7199ea459", size = 2737470 },
+ { url = "https://files.pythonhosted.org/packages/a8/7c/b860618c25678bbd6d1d99dbdfdf0510ccb50790099b963ff78a124b754f/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c1fd185014191700554795c99b347d64f2bb637966c4cfc16998a0ca700d048", size = 1992291 },
+ { url = "https://files.pythonhosted.org/packages/bf/73/42c3742a391eccbeab39f15213ecda3104ae8682ba3c0c28069fbcb8c10d/pydantic_core-2.27.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d81d2068e1c1228a565af076598f9e7451712700b673de8f502f0334f281387d", size = 1994613 },
+ { url = "https://files.pythonhosted.org/packages/94/7a/941e89096d1175d56f59340f3a8ebaf20762fef222c298ea96d36a6328c5/pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1a4207639fb02ec2dbb76227d7c751a20b1a6b4bc52850568e52260cae64ca3b", size = 2002355 },
+ { url = "https://files.pythonhosted.org/packages/6e/95/2359937a73d49e336a5a19848713555605d4d8d6940c3ec6c6c0ca4dcf25/pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:3de3ce3c9ddc8bbd88f6e0e304dea0e66d843ec9de1b0042b0911c1663ffd474", size = 2126661 },
+ { url = "https://files.pythonhosted.org/packages/2b/4c/ca02b7bdb6012a1adef21a50625b14f43ed4d11f1fc237f9d7490aa5078c/pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:30c5f68ded0c36466acede341551106821043e9afaad516adfb6e8fa80a4e6a6", size = 2153261 },
+ { url = "https://files.pythonhosted.org/packages/72/9d/a241db83f973049a1092a079272ffe2e3e82e98561ef6214ab53fe53b1c7/pydantic_core-2.27.2-cp311-cp311-win32.whl", hash = "sha256:c70c26d2c99f78b125a3459f8afe1aed4d9687c24fd677c6a4436bc042e50d6c", size = 1812361 },
+ { url = "https://files.pythonhosted.org/packages/e8/ef/013f07248041b74abd48a385e2110aa3a9bbfef0fbd97d4e6d07d2f5b89a/pydantic_core-2.27.2-cp311-cp311-win_amd64.whl", hash = "sha256:08e125dbdc505fa69ca7d9c499639ab6407cfa909214d500897d02afb816e7cc", size = 1982484 },
+ { url = "https://files.pythonhosted.org/packages/10/1c/16b3a3e3398fd29dca77cea0a1d998d6bde3902fa2706985191e2313cc76/pydantic_core-2.27.2-cp311-cp311-win_arm64.whl", hash = "sha256:26f0d68d4b235a2bae0c3fc585c585b4ecc51382db0e3ba402a22cbc440915e4", size = 1867102 },
+ { url = "https://files.pythonhosted.org/packages/d6/74/51c8a5482ca447871c93e142d9d4a92ead74de6c8dc5e66733e22c9bba89/pydantic_core-2.27.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9e0c8cfefa0ef83b4da9588448b6d8d2a2bf1a53c3f1ae5fca39eb3061e2f0b0", size = 1893127 },
+ { url = "https://files.pythonhosted.org/packages/d3/f3/c97e80721735868313c58b89d2de85fa80fe8dfeeed84dc51598b92a135e/pydantic_core-2.27.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:83097677b8e3bd7eaa6775720ec8e0405f1575015a463285a92bfdfe254529ef", size = 1811340 },
+ { url = "https://files.pythonhosted.org/packages/9e/91/840ec1375e686dbae1bd80a9e46c26a1e0083e1186abc610efa3d9a36180/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:172fce187655fece0c90d90a678424b013f8fbb0ca8b036ac266749c09438cb7", size = 1822900 },
+ { url = "https://files.pythonhosted.org/packages/f6/31/4240bc96025035500c18adc149aa6ffdf1a0062a4b525c932065ceb4d868/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:519f29f5213271eeeeb3093f662ba2fd512b91c5f188f3bb7b27bc5973816934", size = 1869177 },
+ { url = "https://files.pythonhosted.org/packages/fa/20/02fbaadb7808be578317015c462655c317a77a7c8f0ef274bc016a784c54/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:05e3a55d124407fffba0dd6b0c0cd056d10e983ceb4e5dbd10dda135c31071d6", size = 2038046 },
+ { url = "https://files.pythonhosted.org/packages/06/86/7f306b904e6c9eccf0668248b3f272090e49c275bc488a7b88b0823444a4/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9c3ed807c7b91de05e63930188f19e921d1fe90de6b4f5cd43ee7fcc3525cb8c", size = 2685386 },
+ { url = "https://files.pythonhosted.org/packages/8d/f0/49129b27c43396581a635d8710dae54a791b17dfc50c70164866bbf865e3/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fb4aadc0b9a0c063206846d603b92030eb6f03069151a625667f982887153e2", size = 1997060 },
+ { url = "https://files.pythonhosted.org/packages/0d/0f/943b4af7cd416c477fd40b187036c4f89b416a33d3cc0ab7b82708a667aa/pydantic_core-2.27.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:28ccb213807e037460326424ceb8b5245acb88f32f3d2777427476e1b32c48c4", size = 2004870 },
+ { url = "https://files.pythonhosted.org/packages/35/40/aea70b5b1a63911c53a4c8117c0a828d6790483f858041f47bab0b779f44/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:de3cd1899e2c279b140adde9357c4495ed9d47131b4a4eaff9052f23398076b3", size = 1999822 },
+ { url = "https://files.pythonhosted.org/packages/f2/b3/807b94fd337d58effc5498fd1a7a4d9d59af4133e83e32ae39a96fddec9d/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:220f892729375e2d736b97d0e51466252ad84c51857d4d15f5e9692f9ef12be4", size = 2130364 },
+ { url = "https://files.pythonhosted.org/packages/fc/df/791c827cd4ee6efd59248dca9369fb35e80a9484462c33c6649a8d02b565/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a0fcd29cd6b4e74fe8ddd2c90330fd8edf2e30cb52acda47f06dd615ae72da57", size = 2158303 },
+ { url = "https://files.pythonhosted.org/packages/9b/67/4e197c300976af185b7cef4c02203e175fb127e414125916bf1128b639a9/pydantic_core-2.27.2-cp312-cp312-win32.whl", hash = "sha256:1e2cb691ed9834cd6a8be61228471d0a503731abfb42f82458ff27be7b2186fc", size = 1834064 },
+ { url = "https://files.pythonhosted.org/packages/1f/ea/cd7209a889163b8dcca139fe32b9687dd05249161a3edda62860430457a5/pydantic_core-2.27.2-cp312-cp312-win_amd64.whl", hash = "sha256:cc3f1a99a4f4f9dd1de4fe0312c114e740b5ddead65bb4102884b384c15d8bc9", size = 1989046 },
+ { url = "https://files.pythonhosted.org/packages/bc/49/c54baab2f4658c26ac633d798dab66b4c3a9bbf47cff5284e9c182f4137a/pydantic_core-2.27.2-cp312-cp312-win_arm64.whl", hash = "sha256:3911ac9284cd8a1792d3cb26a2da18f3ca26c6908cc434a18f730dc0db7bfa3b", size = 1885092 },
+ { url = "https://files.pythonhosted.org/packages/41/b1/9bc383f48f8002f99104e3acff6cba1231b29ef76cfa45d1506a5cad1f84/pydantic_core-2.27.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7d14bd329640e63852364c306f4d23eb744e0f8193148d4044dd3dacdaacbd8b", size = 1892709 },
+ { url = "https://files.pythonhosted.org/packages/10/6c/e62b8657b834f3eb2961b49ec8e301eb99946245e70bf42c8817350cbefc/pydantic_core-2.27.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82f91663004eb8ed30ff478d77c4d1179b3563df6cdb15c0817cd1cdaf34d154", size = 1811273 },
+ { url = "https://files.pythonhosted.org/packages/ba/15/52cfe49c8c986e081b863b102d6b859d9defc63446b642ccbbb3742bf371/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71b24c7d61131bb83df10cc7e687433609963a944ccf45190cfc21e0887b08c9", size = 1823027 },
+ { url = "https://files.pythonhosted.org/packages/b1/1c/b6f402cfc18ec0024120602bdbcebc7bdd5b856528c013bd4d13865ca473/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fa8e459d4954f608fa26116118bb67f56b93b209c39b008277ace29937453dc9", size = 1868888 },
+ { url = "https://files.pythonhosted.org/packages/bd/7b/8cb75b66ac37bc2975a3b7de99f3c6f355fcc4d89820b61dffa8f1e81677/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce8918cbebc8da707ba805b7fd0b382816858728ae7fe19a942080c24e5b7cd1", size = 2037738 },
+ { url = "https://files.pythonhosted.org/packages/c8/f1/786d8fe78970a06f61df22cba58e365ce304bf9b9f46cc71c8c424e0c334/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eda3f5c2a021bbc5d976107bb302e0131351c2ba54343f8a496dc8783d3d3a6a", size = 2685138 },
+ { url = "https://files.pythonhosted.org/packages/a6/74/d12b2cd841d8724dc8ffb13fc5cef86566a53ed358103150209ecd5d1999/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd8086fa684c4775c27f03f062cbb9eaa6e17f064307e86b21b9e0abc9c0f02e", size = 1997025 },
+ { url = "https://files.pythonhosted.org/packages/a0/6e/940bcd631bc4d9a06c9539b51f070b66e8f370ed0933f392db6ff350d873/pydantic_core-2.27.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8d9b3388db186ba0c099a6d20f0604a44eabdeef1777ddd94786cdae158729e4", size = 2004633 },
+ { url = "https://files.pythonhosted.org/packages/50/cc/a46b34f1708d82498c227d5d80ce615b2dd502ddcfd8376fc14a36655af1/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7a66efda2387de898c8f38c0cf7f14fca0b51a8ef0b24bfea5849f1b3c95af27", size = 1999404 },
+ { url = "https://files.pythonhosted.org/packages/ca/2d/c365cfa930ed23bc58c41463bae347d1005537dc8db79e998af8ba28d35e/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:18a101c168e4e092ab40dbc2503bdc0f62010e95d292b27827871dc85450d7ee", size = 2130130 },
+ { url = "https://files.pythonhosted.org/packages/f4/d7/eb64d015c350b7cdb371145b54d96c919d4db516817f31cd1c650cae3b21/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ba5dd002f88b78a4215ed2f8ddbdf85e8513382820ba15ad5ad8955ce0ca19a1", size = 2157946 },
+ { url = "https://files.pythonhosted.org/packages/a4/99/bddde3ddde76c03b65dfd5a66ab436c4e58ffc42927d4ff1198ffbf96f5f/pydantic_core-2.27.2-cp313-cp313-win32.whl", hash = "sha256:1ebaf1d0481914d004a573394f4be3a7616334be70261007e47c2a6fe7e50130", size = 1834387 },
+ { url = "https://files.pythonhosted.org/packages/71/47/82b5e846e01b26ac6f1893d3c5f9f3a2eb6ba79be26eef0b759b4fe72946/pydantic_core-2.27.2-cp313-cp313-win_amd64.whl", hash = "sha256:953101387ecf2f5652883208769a79e48db18c6df442568a0b5ccd8c2723abee", size = 1990453 },
+ { url = "https://files.pythonhosted.org/packages/51/b2/b2b50d5ecf21acf870190ae5d093602d95f66c9c31f9d5de6062eb329ad1/pydantic_core-2.27.2-cp313-cp313-win_arm64.whl", hash = "sha256:ac4dbfd1691affb8f48c2c13241a2e3b60ff23247cbcf981759c768b6633cf8b", size = 1885186 },
+ { url = "https://files.pythonhosted.org/packages/46/72/af70981a341500419e67d5cb45abe552a7c74b66326ac8877588488da1ac/pydantic_core-2.27.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:2bf14caea37e91198329b828eae1618c068dfb8ef17bb33287a7ad4b61ac314e", size = 1891159 },
+ { url = "https://files.pythonhosted.org/packages/ad/3d/c5913cccdef93e0a6a95c2d057d2c2cba347815c845cda79ddd3c0f5e17d/pydantic_core-2.27.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:b0cb791f5b45307caae8810c2023a184c74605ec3bcbb67d13846c28ff731ff8", size = 1768331 },
+ { url = "https://files.pythonhosted.org/packages/f6/f0/a3ae8fbee269e4934f14e2e0e00928f9346c5943174f2811193113e58252/pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:688d3fd9fcb71f41c4c015c023d12a79d1c4c0732ec9eb35d96e3388a120dcf3", size = 1822467 },
+ { url = "https://files.pythonhosted.org/packages/d7/7a/7bbf241a04e9f9ea24cd5874354a83526d639b02674648af3f350554276c/pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d591580c34f4d731592f0e9fe40f9cc1b430d297eecc70b962e93c5c668f15f", size = 1979797 },
+ { url = "https://files.pythonhosted.org/packages/4f/5f/4784c6107731f89e0005a92ecb8a2efeafdb55eb992b8e9d0a2be5199335/pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:82f986faf4e644ffc189a7f1aafc86e46ef70372bb153e7001e8afccc6e54133", size = 1987839 },
+ { url = "https://files.pythonhosted.org/packages/6d/a7/61246562b651dff00de86a5f01b6e4befb518df314c54dec187a78d81c84/pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:bec317a27290e2537f922639cafd54990551725fc844249e64c523301d0822fc", size = 1998861 },
+ { url = "https://files.pythonhosted.org/packages/86/aa/837821ecf0c022bbb74ca132e117c358321e72e7f9702d1b6a03758545e2/pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:0296abcb83a797db256b773f45773da397da75a08f5fcaef41f2044adec05f50", size = 2116582 },
+ { url = "https://files.pythonhosted.org/packages/81/b0/5e74656e95623cbaa0a6278d16cf15e10a51f6002e3ec126541e95c29ea3/pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:0d75070718e369e452075a6017fbf187f788e17ed67a3abd47fa934d001863d9", size = 2151985 },
+ { url = "https://files.pythonhosted.org/packages/63/37/3e32eeb2a451fddaa3898e2163746b0cffbbdbb4740d38372db0490d67f3/pydantic_core-2.27.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:7e17b560be3c98a8e3aa66ce828bdebb9e9ac6ad5466fba92eb74c4c95cb1151", size = 2004715 },
+]
+
+[[package]]
+name = "pyhesaff"
+version = "2.1.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "numpy", marker = "python_full_version < '4.0'" },
+ { name = "ubelt" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/25/2a/fb03a8272b7e673fd121dc00e65e932c372271c7219955221bbae2982c7e/pyhesaff-2.1.1.tar.gz", hash = "sha256:1d513ec66bd2a9799d7ea98c89f80618dddd52711f06c412d8defa22b650b428", size = 108390 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e8/31/eabd04bdc32ba46459d2c869f73fc37523ec6e59133300a78cee8a6e11bd/pyhesaff-2.1.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8a34314e8d78f3395b6d6b6506ba960efc47ed0ec0cbc227bb5b46d5ddeb2fb4", size = 8834668 },
+ { url = "https://files.pythonhosted.org/packages/bb/30/ad07f6b3a5d544b36a4412835ddc380875a0f9c82a79423a990ca938604a/pyhesaff-2.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:46f1e5b34eb271456246e675b80e7cfb5be5fda1e275ad8fbf35429262e3cbe1", size = 32773548 },
+ { url = "https://files.pythonhosted.org/packages/8c/0e/fe8e23108e082e5372c371ad9d6780f512a456c6df6c154bf6aa636ad9c9/pyhesaff-2.1.1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1f7ff2f48bccae8394e2b1bd6a7d360315ed99dfcde4a658e35ab366feb659b5", size = 8834667 },
+ { url = "https://files.pythonhosted.org/packages/02/61/4c2e897defd357cfcdcb3a16d2490ceaa20a6aeb6edba9e43c05243816e1/pyhesaff-2.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd0ebb98de94ece860c04f6a746ab2bc5374b326b847caa953256bd9a1fc5c62", size = 32773549 },
+]
+
+[[package]]
+name = "pyparsing"
+version = "3.2.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/8b/1a/3544f4f299a47911c2ab3710f534e52fea62a633c96806995da5d25be4b2/pyparsing-3.2.1.tar.gz", hash = "sha256:61980854fd66de3a90028d679a954d5f2623e83144b5afe5ee86f43d762e5f0a", size = 1067694 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/1c/a7/c8a2d361bf89c0d9577c934ebb7421b25dc84bf3a8e3ac0a40aed9acc547/pyparsing-3.2.1-py3-none-any.whl", hash = "sha256:506ff4f4386c4cec0590ec19e6302d3aedb992fdc02c761e90416f158dacf8e1", size = 107716 },
+]
+
+[[package]]
+name = "python-dateutil"
+version = "2.9.0.post0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "six" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892 },
+]
+
+[[package]]
+name = "pyyaml"
+version = "6.0.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/9b/95/a3fac87cb7158e231b5a6012e438c647e1a87f09f8e0d123acec8ab8bf71/PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086", size = 184199 },
+ { url = "https://files.pythonhosted.org/packages/c7/7a/68bd47624dab8fd4afbfd3c48e3b79efe09098ae941de5b58abcbadff5cb/PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf", size = 171758 },
+ { url = "https://files.pythonhosted.org/packages/49/ee/14c54df452143b9ee9f0f29074d7ca5516a36edb0b4cc40c3f280131656f/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237", size = 718463 },
+ { url = "https://files.pythonhosted.org/packages/4d/61/de363a97476e766574650d742205be468921a7b532aa2499fcd886b62530/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b", size = 719280 },
+ { url = "https://files.pythonhosted.org/packages/6b/4e/1523cb902fd98355e2e9ea5e5eb237cbc5f3ad5f3075fa65087aa0ecb669/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed", size = 751239 },
+ { url = "https://files.pythonhosted.org/packages/b7/33/5504b3a9a4464893c32f118a9cc045190a91637b119a9c881da1cf6b7a72/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180", size = 695802 },
+ { url = "https://files.pythonhosted.org/packages/5c/20/8347dcabd41ef3a3cdc4f7b7a2aff3d06598c8779faa189cdbf878b626a4/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68", size = 720527 },
+ { url = "https://files.pythonhosted.org/packages/be/aa/5afe99233fb360d0ff37377145a949ae258aaab831bde4792b32650a4378/PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99", size = 144052 },
+ { url = "https://files.pythonhosted.org/packages/b5/84/0fa4b06f6d6c958d207620fc60005e241ecedceee58931bb20138e1e5776/PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e", size = 161774 },
+ { url = "https://files.pythonhosted.org/packages/f8/aa/7af4e81f7acba21a4c6be026da38fd2b872ca46226673c89a758ebdc4fd2/PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", size = 184612 },
+ { url = "https://files.pythonhosted.org/packages/8b/62/b9faa998fd185f65c1371643678e4d58254add437edb764a08c5a98fb986/PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", size = 172040 },
+ { url = "https://files.pythonhosted.org/packages/ad/0c/c804f5f922a9a6563bab712d8dcc70251e8af811fce4524d57c2c0fd49a4/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", size = 736829 },
+ { url = "https://files.pythonhosted.org/packages/51/16/6af8d6a6b210c8e54f1406a6b9481febf9c64a3109c541567e35a49aa2e7/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317", size = 764167 },
+ { url = "https://files.pythonhosted.org/packages/75/e4/2c27590dfc9992f73aabbeb9241ae20220bd9452df27483b6e56d3975cc5/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85", size = 762952 },
+ { url = "https://files.pythonhosted.org/packages/9b/97/ecc1abf4a823f5ac61941a9c00fe501b02ac3ab0e373c3857f7d4b83e2b6/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4", size = 735301 },
+ { url = "https://files.pythonhosted.org/packages/45/73/0f49dacd6e82c9430e46f4a027baa4ca205e8b0a9dce1397f44edc23559d/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e", size = 756638 },
+ { url = "https://files.pythonhosted.org/packages/22/5f/956f0f9fc65223a58fbc14459bf34b4cc48dec52e00535c79b8db361aabd/PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5", size = 143850 },
+ { url = "https://files.pythonhosted.org/packages/ed/23/8da0bbe2ab9dcdd11f4f4557ccaf95c10b9811b13ecced089d43ce59c3c8/PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44", size = 161980 },
+ { url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873 },
+ { url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302 },
+ { url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154 },
+ { url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223 },
+ { url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542 },
+ { url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164 },
+ { url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611 },
+ { url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591 },
+ { url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338 },
+ { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309 },
+ { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679 },
+ { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428 },
+ { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361 },
+ { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523 },
+ { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660 },
+ { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597 },
+ { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527 },
+ { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446 },
+]
+
+[[package]]
+name = "requests"
+version = "2.32.3"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "certifi" },
+ { name = "charset-normalizer" },
+ { name = "idna" },
+ { name = "urllib3" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928 },
+]
+
+[[package]]
+name = "romatch"
+version = "0.0.2"
+source = { git = "https://github.com/Parskatt/RoMa.git#edd1b8b35a94e0d6c26c73b1bb3117db8d0b04fa" }
+dependencies = [
+ { name = "albumentations" },
+ { name = "einops" },
+ { name = "h5py" },
+ { name = "kornia" },
+ { name = "loguru" },
+ { name = "matplotlib" },
+ { name = "opencv-python" },
+ { name = "poselib" },
+ { name = "timm" },
+ { name = "torch" },
+ { name = "torchvision" },
+ { name = "tqdm" },
+ { name = "wandb" },
+]
+
+[[package]]
+name = "ruff"
+version = "0.9.10"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/20/8e/fafaa6f15c332e73425d9c44ada85360501045d5ab0b81400076aff27cf6/ruff-0.9.10.tar.gz", hash = "sha256:9bacb735d7bada9cfb0f2c227d3658fc443d90a727b47f206fb33f52f3c0eac7", size = 3759776 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/73/b2/af7c2cc9e438cbc19fafeec4f20bfcd72165460fe75b2b6e9a0958c8c62b/ruff-0.9.10-py3-none-linux_armv6l.whl", hash = "sha256:eb4d25532cfd9fe461acc83498361ec2e2252795b4f40b17e80692814329e42d", size = 10049494 },
+ { url = "https://files.pythonhosted.org/packages/6d/12/03f6dfa1b95ddd47e6969f0225d60d9d7437c91938a310835feb27927ca0/ruff-0.9.10-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:188a6638dab1aa9bb6228a7302387b2c9954e455fb25d6b4470cb0641d16759d", size = 10853584 },
+ { url = "https://files.pythonhosted.org/packages/02/49/1c79e0906b6ff551fb0894168763f705bf980864739572b2815ecd3c9df0/ruff-0.9.10-py3-none-macosx_11_0_arm64.whl", hash = "sha256:5284dcac6b9dbc2fcb71fdfc26a217b2ca4ede6ccd57476f52a587451ebe450d", size = 10155692 },
+ { url = "https://files.pythonhosted.org/packages/5b/01/85e8082e41585e0e1ceb11e41c054e9e36fed45f4b210991052d8a75089f/ruff-0.9.10-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:47678f39fa2a3da62724851107f438c8229a3470f533894b5568a39b40029c0c", size = 10369760 },
+ { url = "https://files.pythonhosted.org/packages/a1/90/0bc60bd4e5db051f12445046d0c85cc2c617095c0904f1aa81067dc64aea/ruff-0.9.10-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:99713a6e2766b7a17147b309e8c915b32b07a25c9efd12ada79f217c9c778b3e", size = 9912196 },
+ { url = "https://files.pythonhosted.org/packages/66/ea/0b7e8c42b1ec608033c4d5a02939c82097ddcb0b3e393e4238584b7054ab/ruff-0.9.10-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:524ee184d92f7c7304aa568e2db20f50c32d1d0caa235d8ddf10497566ea1a12", size = 11434985 },
+ { url = "https://files.pythonhosted.org/packages/d5/86/3171d1eff893db4f91755175a6e1163c5887be1f1e2f4f6c0c59527c2bfd/ruff-0.9.10-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:df92aeac30af821f9acf819fc01b4afc3dfb829d2782884f8739fb52a8119a16", size = 12155842 },
+ { url = "https://files.pythonhosted.org/packages/89/9e/700ca289f172a38eb0bca752056d0a42637fa17b81649b9331786cb791d7/ruff-0.9.10-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de42e4edc296f520bb84954eb992a07a0ec5a02fecb834498415908469854a52", size = 11613804 },
+ { url = "https://files.pythonhosted.org/packages/f2/92/648020b3b5db180f41a931a68b1c8575cca3e63cec86fd26807422a0dbad/ruff-0.9.10-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d257f95b65806104b6b1ffca0ea53f4ef98454036df65b1eda3693534813ecd1", size = 13823776 },
+ { url = "https://files.pythonhosted.org/packages/5e/a6/cc472161cd04d30a09d5c90698696b70c169eeba2c41030344194242db45/ruff-0.9.10-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b60dec7201c0b10d6d11be00e8f2dbb6f40ef1828ee75ed739923799513db24c", size = 11302673 },
+ { url = "https://files.pythonhosted.org/packages/6c/db/d31c361c4025b1b9102b4d032c70a69adb9ee6fde093f6c3bf29f831c85c/ruff-0.9.10-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:d838b60007da7a39c046fcdd317293d10b845001f38bcb55ba766c3875b01e43", size = 10235358 },
+ { url = "https://files.pythonhosted.org/packages/d1/86/d6374e24a14d4d93ebe120f45edd82ad7dcf3ef999ffc92b197d81cdc2a5/ruff-0.9.10-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:ccaf903108b899beb8e09a63ffae5869057ab649c1e9231c05ae354ebc62066c", size = 9886177 },
+ { url = "https://files.pythonhosted.org/packages/00/62/a61691f6eaaac1e945a1f3f59f1eea9a218513139d5b6c2b8f88b43b5b8f/ruff-0.9.10-py3-none-musllinux_1_2_i686.whl", hash = "sha256:f9567d135265d46e59d62dc60c0bfad10e9a6822e231f5b24032dba5a55be6b5", size = 10864747 },
+ { url = "https://files.pythonhosted.org/packages/ee/94/2c7065e1d92a8a8a46d46d9c3cf07b0aa7e0a1e0153d74baa5e6620b4102/ruff-0.9.10-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:5f202f0d93738c28a89f8ed9eaba01b7be339e5d8d642c994347eaa81c6d75b8", size = 11360441 },
+ { url = "https://files.pythonhosted.org/packages/a7/8f/1f545ea6f9fcd7bf4368551fb91d2064d8f0577b3079bb3f0ae5779fb773/ruff-0.9.10-py3-none-win32.whl", hash = "sha256:bfb834e87c916521ce46b1788fbb8484966e5113c02df216680102e9eb960029", size = 10247401 },
+ { url = "https://files.pythonhosted.org/packages/4f/18/fb703603ab108e5c165f52f5b86ee2aa9be43bb781703ec87c66a5f5d604/ruff-0.9.10-py3-none-win_amd64.whl", hash = "sha256:f2160eeef3031bf4b17df74e307d4c5fb689a6f3a26a2de3f7ef4044e3c484f1", size = 11366360 },
+ { url = "https://files.pythonhosted.org/packages/35/85/338e603dc68e7d9994d5d84f24adbf69bae760ba5efd3e20f5ff2cec18da/ruff-0.9.10-py3-none-win_arm64.whl", hash = "sha256:5fd804c0327a5e5ea26615550e706942f348b197d5475ff34c19733aee4b2e69", size = 10436892 },
+]
+
+[[package]]
+name = "safetensors"
+version = "0.5.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/71/7e/2d5d6ee7b40c0682315367ec7475693d110f512922d582fef1bd4a63adc3/safetensors-0.5.3.tar.gz", hash = "sha256:b6b0d6ecacec39a4fdd99cc19f4576f5219ce858e6fd8dbe7609df0b8dc56965", size = 67210 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/18/ae/88f6c49dbd0cc4da0e08610019a3c78a7d390879a919411a410a1876d03a/safetensors-0.5.3-cp38-abi3-macosx_10_12_x86_64.whl", hash = "sha256:bd20eb133db8ed15b40110b7c00c6df51655a2998132193de2f75f72d99c7073", size = 436917 },
+ { url = "https://files.pythonhosted.org/packages/b8/3b/11f1b4a2f5d2ab7da34ecc062b0bc301f2be024d110a6466726bec8c055c/safetensors-0.5.3-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:21d01c14ff6c415c485616b8b0bf961c46b3b343ca59110d38d744e577f9cce7", size = 418419 },
+ { url = "https://files.pythonhosted.org/packages/5d/9a/add3e6fef267658075c5a41573c26d42d80c935cdc992384dfae435feaef/safetensors-0.5.3-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:11bce6164887cd491ca75c2326a113ba934be596e22b28b1742ce27b1d076467", size = 459493 },
+ { url = "https://files.pythonhosted.org/packages/df/5c/bf2cae92222513cc23b3ff85c4a1bb2811a2c3583ac0f8e8d502751de934/safetensors-0.5.3-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4a243be3590bc3301c821da7a18d87224ef35cbd3e5f5727e4e0728b8172411e", size = 472400 },
+ { url = "https://files.pythonhosted.org/packages/58/11/7456afb740bd45782d0f4c8e8e1bb9e572f1bf82899fb6ace58af47b4282/safetensors-0.5.3-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8bd84b12b1670a6f8e50f01e28156422a2bc07fb16fc4e98bded13039d688a0d", size = 522891 },
+ { url = "https://files.pythonhosted.org/packages/57/3d/fe73a9d2ace487e7285f6e157afee2383bd1ddb911b7cb44a55cf812eae3/safetensors-0.5.3-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:391ac8cab7c829452175f871fcaf414aa1e292b5448bd02620f675a7f3e7abb9", size = 537694 },
+ { url = "https://files.pythonhosted.org/packages/a6/f8/dae3421624fcc87a89d42e1898a798bc7ff72c61f38973a65d60df8f124c/safetensors-0.5.3-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cead1fa41fc54b1e61089fa57452e8834f798cb1dc7a09ba3524f1eb08e0317a", size = 471642 },
+ { url = "https://files.pythonhosted.org/packages/ce/20/1fbe16f9b815f6c5a672f5b760951e20e17e43f67f231428f871909a37f6/safetensors-0.5.3-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1077f3e94182d72618357b04b5ced540ceb71c8a813d3319f1aba448e68a770d", size = 502241 },
+ { url = "https://files.pythonhosted.org/packages/5f/18/8e108846b506487aa4629fe4116b27db65c3dde922de2c8e0cc1133f3f29/safetensors-0.5.3-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:799021e78287bac619c7b3f3606730a22da4cda27759ddf55d37c8db7511c74b", size = 638001 },
+ { url = "https://files.pythonhosted.org/packages/82/5a/c116111d8291af6c8c8a8b40628fe833b9db97d8141c2a82359d14d9e078/safetensors-0.5.3-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:df26da01aaac504334644e1b7642fa000bfec820e7cef83aeac4e355e03195ff", size = 734013 },
+ { url = "https://files.pythonhosted.org/packages/7d/ff/41fcc4d3b7de837963622e8610d998710705bbde9a8a17221d85e5d0baad/safetensors-0.5.3-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:32c3ef2d7af8b9f52ff685ed0bc43913cdcde135089ae322ee576de93eae5135", size = 670687 },
+ { url = "https://files.pythonhosted.org/packages/40/ad/2b113098e69c985a3d8fbda4b902778eae4a35b7d5188859b4a63d30c161/safetensors-0.5.3-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:37f1521be045e56fc2b54c606d4455573e717b2d887c579ee1dbba5f868ece04", size = 643147 },
+ { url = "https://files.pythonhosted.org/packages/0a/0c/95aeb51d4246bd9a3242d3d8349c1112b4ee7611a4b40f0c5c93b05f001d/safetensors-0.5.3-cp38-abi3-win32.whl", hash = "sha256:cfc0ec0846dcf6763b0ed3d1846ff36008c6e7290683b61616c4b040f6a54ace", size = 296677 },
+ { url = "https://files.pythonhosted.org/packages/69/e2/b011c38e5394c4c18fb5500778a55ec43ad6106126e74723ffaee246f56e/safetensors-0.5.3-cp38-abi3-win_amd64.whl", hash = "sha256:836cbbc320b47e80acd40e44c8682db0e8ad7123209f69b093def21ec7cafd11", size = 308878 },
+]
+
+[[package]]
+name = "scipy"
+version = "1.15.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "numpy" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/b7/b9/31ba9cd990e626574baf93fbc1ac61cf9ed54faafd04c479117517661637/scipy-1.15.2.tar.gz", hash = "sha256:cd58a314d92838f7e6f755c8a2167ead4f27e1fd5c1251fd54289569ef3495ec", size = 59417316 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/95/df/ef233fff6838fe6f7840d69b5ef9f20d2b5c912a8727b21ebf876cb15d54/scipy-1.15.2-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:a2ec871edaa863e8213ea5df811cd600734f6400b4af272e1c011e69401218e9", size = 38692502 },
+ { url = "https://files.pythonhosted.org/packages/5c/20/acdd4efb8a68b842968f7bc5611b1aeb819794508771ad104de418701422/scipy-1.15.2-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:6f223753c6ea76983af380787611ae1291e3ceb23917393079dcc746ba60cfb5", size = 30085508 },
+ { url = "https://files.pythonhosted.org/packages/42/55/39cf96ca7126f1e78ee72a6344ebdc6702fc47d037319ad93221063e6cf4/scipy-1.15.2-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:ecf797d2d798cf7c838c6d98321061eb3e72a74710e6c40540f0e8087e3b499e", size = 22359166 },
+ { url = "https://files.pythonhosted.org/packages/51/48/708d26a4ab8a1441536bf2dfcad1df0ca14a69f010fba3ccbdfc02df7185/scipy-1.15.2-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:9b18aa747da280664642997e65aab1dd19d0c3d17068a04b3fe34e2559196cb9", size = 25112047 },
+ { url = "https://files.pythonhosted.org/packages/dd/65/f9c5755b995ad892020381b8ae11f16d18616208e388621dfacc11df6de6/scipy-1.15.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:87994da02e73549dfecaed9e09a4f9d58a045a053865679aeb8d6d43747d4df3", size = 35536214 },
+ { url = "https://files.pythonhosted.org/packages/de/3c/c96d904b9892beec978562f64d8cc43f9cca0842e65bd3cd1b7f7389b0ba/scipy-1.15.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:69ea6e56d00977f355c0f84eba69877b6df084516c602d93a33812aa04d90a3d", size = 37646981 },
+ { url = "https://files.pythonhosted.org/packages/3d/74/c2d8a24d18acdeae69ed02e132b9bc1bb67b7bee90feee1afe05a68f9d67/scipy-1.15.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:888307125ea0c4466287191e5606a2c910963405ce9671448ff9c81c53f85f58", size = 37230048 },
+ { url = "https://files.pythonhosted.org/packages/42/19/0aa4ce80eca82d487987eff0bc754f014dec10d20de2f66754fa4ea70204/scipy-1.15.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9412f5e408b397ff5641080ed1e798623dbe1ec0d78e72c9eca8992976fa65aa", size = 40010322 },
+ { url = "https://files.pythonhosted.org/packages/d0/d2/f0683b7e992be44d1475cc144d1f1eeae63c73a14f862974b4db64af635e/scipy-1.15.2-cp310-cp310-win_amd64.whl", hash = "sha256:b5e025e903b4f166ea03b109bb241355b9c42c279ea694d8864d033727205e65", size = 41233385 },
+ { url = "https://files.pythonhosted.org/packages/40/1f/bf0a5f338bda7c35c08b4ed0df797e7bafe8a78a97275e9f439aceb46193/scipy-1.15.2-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:92233b2df6938147be6fa8824b8136f29a18f016ecde986666be5f4d686a91a4", size = 38703651 },
+ { url = "https://files.pythonhosted.org/packages/de/54/db126aad3874601048c2c20ae3d8a433dbfd7ba8381551e6f62606d9bd8e/scipy-1.15.2-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:62ca1ff3eb513e09ed17a5736929429189adf16d2d740f44e53270cc800ecff1", size = 30102038 },
+ { url = "https://files.pythonhosted.org/packages/61/d8/84da3fffefb6c7d5a16968fe5b9f24c98606b165bb801bb0b8bc3985200f/scipy-1.15.2-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:4c6676490ad76d1c2894d77f976144b41bd1a4052107902238047fb6a473e971", size = 22375518 },
+ { url = "https://files.pythonhosted.org/packages/44/78/25535a6e63d3b9c4c90147371aedb5d04c72f3aee3a34451f2dc27c0c07f/scipy-1.15.2-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:a8bf5cb4a25046ac61d38f8d3c3426ec11ebc350246a4642f2f315fe95bda655", size = 25142523 },
+ { url = "https://files.pythonhosted.org/packages/e0/22/4b4a26fe1cd9ed0bc2b2cb87b17d57e32ab72c346949eaf9288001f8aa8e/scipy-1.15.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a8e34cf4c188b6dd004654f88586d78f95639e48a25dfae9c5e34a6dc34547e", size = 35491547 },
+ { url = "https://files.pythonhosted.org/packages/32/ea/564bacc26b676c06a00266a3f25fdfe91a9d9a2532ccea7ce6dd394541bc/scipy-1.15.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28a0d2c2075946346e4408b211240764759e0fabaeb08d871639b5f3b1aca8a0", size = 37634077 },
+ { url = "https://files.pythonhosted.org/packages/43/c2/bfd4e60668897a303b0ffb7191e965a5da4056f0d98acfb6ba529678f0fb/scipy-1.15.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:42dabaaa798e987c425ed76062794e93a243be8f0f20fff6e7a89f4d61cb3d40", size = 37231657 },
+ { url = "https://files.pythonhosted.org/packages/4a/75/5f13050bf4f84c931bcab4f4e83c212a36876c3c2244475db34e4b5fe1a6/scipy-1.15.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6f5e296ec63c5da6ba6fa0343ea73fd51b8b3e1a300b0a8cae3ed4b1122c7462", size = 40035857 },
+ { url = "https://files.pythonhosted.org/packages/b9/8b/7ec1832b09dbc88f3db411f8cdd47db04505c4b72c99b11c920a8f0479c3/scipy-1.15.2-cp311-cp311-win_amd64.whl", hash = "sha256:597a0c7008b21c035831c39927406c6181bcf8f60a73f36219b69d010aa04737", size = 41217654 },
+ { url = "https://files.pythonhosted.org/packages/4b/5d/3c78815cbab499610f26b5bae6aed33e227225a9fa5290008a733a64f6fc/scipy-1.15.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c4697a10da8f8765bb7c83e24a470da5797e37041edfd77fd95ba3811a47c4fd", size = 38756184 },
+ { url = "https://files.pythonhosted.org/packages/37/20/3d04eb066b471b6e171827548b9ddb3c21c6bbea72a4d84fc5989933910b/scipy-1.15.2-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:869269b767d5ee7ea6991ed7e22b3ca1f22de73ab9a49c44bad338b725603301", size = 30163558 },
+ { url = "https://files.pythonhosted.org/packages/a4/98/e5c964526c929ef1f795d4c343b2ff98634ad2051bd2bbadfef9e772e413/scipy-1.15.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:bad78d580270a4d32470563ea86c6590b465cb98f83d760ff5b0990cb5518a93", size = 22437211 },
+ { url = "https://files.pythonhosted.org/packages/1d/cd/1dc7371e29195ecbf5222f9afeedb210e0a75057d8afbd942aa6cf8c8eca/scipy-1.15.2-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:b09ae80010f52efddb15551025f9016c910296cf70adbf03ce2a8704f3a5ad20", size = 25232260 },
+ { url = "https://files.pythonhosted.org/packages/f0/24/1a181a9e5050090e0b5138c5f496fee33293c342b788d02586bc410c6477/scipy-1.15.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5a6fd6eac1ce74a9f77a7fc724080d507c5812d61e72bd5e4c489b042455865e", size = 35198095 },
+ { url = "https://files.pythonhosted.org/packages/c0/53/eaada1a414c026673eb983f8b4a55fe5eb172725d33d62c1b21f63ff6ca4/scipy-1.15.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2b871df1fe1a3ba85d90e22742b93584f8d2b8e6124f8372ab15c71b73e428b8", size = 37297371 },
+ { url = "https://files.pythonhosted.org/packages/e9/06/0449b744892ed22b7e7b9a1994a866e64895363572677a316a9042af1fe5/scipy-1.15.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:03205d57a28e18dfd39f0377d5002725bf1f19a46f444108c29bdb246b6c8a11", size = 36872390 },
+ { url = "https://files.pythonhosted.org/packages/6a/6f/a8ac3cfd9505ec695c1bc35edc034d13afbd2fc1882a7c6b473e280397bb/scipy-1.15.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:601881dfb761311045b03114c5fe718a12634e5608c3b403737ae463c9885d53", size = 39700276 },
+ { url = "https://files.pythonhosted.org/packages/f5/6f/e6e5aff77ea2a48dd96808bb51d7450875af154ee7cbe72188afb0b37929/scipy-1.15.2-cp312-cp312-win_amd64.whl", hash = "sha256:e7c68b6a43259ba0aab737237876e5c2c549a031ddb7abc28c7b47f22e202ded", size = 40942317 },
+ { url = "https://files.pythonhosted.org/packages/53/40/09319f6e0f276ea2754196185f95cd191cb852288440ce035d5c3a931ea2/scipy-1.15.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:01edfac9f0798ad6b46d9c4c9ca0e0ad23dbf0b1eb70e96adb9fa7f525eff0bf", size = 38717587 },
+ { url = "https://files.pythonhosted.org/packages/fe/c3/2854f40ecd19585d65afaef601e5e1f8dbf6758b2f95b5ea93d38655a2c6/scipy-1.15.2-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:08b57a9336b8e79b305a143c3655cc5bdbe6d5ece3378578888d2afbb51c4e37", size = 30100266 },
+ { url = "https://files.pythonhosted.org/packages/dd/b1/f9fe6e3c828cb5930b5fe74cb479de5f3d66d682fa8adb77249acaf545b8/scipy-1.15.2-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:54c462098484e7466362a9f1672d20888f724911a74c22ae35b61f9c5919183d", size = 22373768 },
+ { url = "https://files.pythonhosted.org/packages/15/9d/a60db8c795700414c3f681908a2b911e031e024d93214f2d23c6dae174ab/scipy-1.15.2-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:cf72ff559a53a6a6d77bd8eefd12a17995ffa44ad86c77a5df96f533d4e6c6bb", size = 25154719 },
+ { url = "https://files.pythonhosted.org/packages/37/3b/9bda92a85cd93f19f9ed90ade84aa1e51657e29988317fabdd44544f1dd4/scipy-1.15.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9de9d1416b3d9e7df9923ab23cd2fe714244af10b763975bea9e4f2e81cebd27", size = 35163195 },
+ { url = "https://files.pythonhosted.org/packages/03/5a/fc34bf1aa14dc7c0e701691fa8685f3faec80e57d816615e3625f28feb43/scipy-1.15.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fb530e4794fc8ea76a4a21ccb67dea33e5e0e60f07fc38a49e821e1eae3b71a0", size = 37255404 },
+ { url = "https://files.pythonhosted.org/packages/4a/71/472eac45440cee134c8a180dbe4c01b3ec247e0338b7c759e6cd71f199a7/scipy-1.15.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5ea7ed46d437fc52350b028b1d44e002646e28f3e8ddc714011aaf87330f2f32", size = 36860011 },
+ { url = "https://files.pythonhosted.org/packages/01/b3/21f890f4f42daf20e4d3aaa18182dddb9192771cd47445aaae2e318f6738/scipy-1.15.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:11e7ad32cf184b74380f43d3c0a706f49358b904fa7d5345f16ddf993609184d", size = 39657406 },
+ { url = "https://files.pythonhosted.org/packages/0d/76/77cf2ac1f2a9cc00c073d49e1e16244e389dd88e2490c91d84e1e3e4d126/scipy-1.15.2-cp313-cp313-win_amd64.whl", hash = "sha256:a5080a79dfb9b78b768cebf3c9dcbc7b665c5875793569f48bf0e2b1d7f68f6f", size = 40961243 },
+ { url = "https://files.pythonhosted.org/packages/4c/4b/a57f8ddcf48e129e6054fa9899a2a86d1fc6b07a0e15c7eebff7ca94533f/scipy-1.15.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:447ce30cee6a9d5d1379087c9e474628dab3db4a67484be1b7dc3196bfb2fac9", size = 38870286 },
+ { url = "https://files.pythonhosted.org/packages/0c/43/c304d69a56c91ad5f188c0714f6a97b9c1fed93128c691148621274a3a68/scipy-1.15.2-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:c90ebe8aaa4397eaefa8455a8182b164a6cc1d59ad53f79943f266d99f68687f", size = 30141634 },
+ { url = "https://files.pythonhosted.org/packages/44/1a/6c21b45d2548eb73be9b9bff421aaaa7e85e22c1f9b3bc44b23485dfce0a/scipy-1.15.2-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:def751dd08243934c884a3221156d63e15234a3155cf25978b0a668409d45eb6", size = 22415179 },
+ { url = "https://files.pythonhosted.org/packages/74/4b/aefac4bba80ef815b64f55da06f62f92be5d03b467f2ce3668071799429a/scipy-1.15.2-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:302093e7dfb120e55515936cb55618ee0b895f8bcaf18ff81eca086c17bd80af", size = 25126412 },
+ { url = "https://files.pythonhosted.org/packages/b1/53/1cbb148e6e8f1660aacd9f0a9dfa2b05e9ff1cb54b4386fe868477972ac2/scipy-1.15.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7cd5b77413e1855351cdde594eca99c1f4a588c2d63711388b6a1f1c01f62274", size = 34952867 },
+ { url = "https://files.pythonhosted.org/packages/2c/23/e0eb7f31a9c13cf2dca083828b97992dd22f8184c6ce4fec5deec0c81fcf/scipy-1.15.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d0194c37037707b2afa7a2f2a924cf7bac3dc292d51b6a925e5fcb89bc5c776", size = 36890009 },
+ { url = "https://files.pythonhosted.org/packages/03/f3/e699e19cabe96bbac5189c04aaa970718f0105cff03d458dc5e2b6bd1e8c/scipy-1.15.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:bae43364d600fdc3ac327db99659dcb79e6e7ecd279a75fe1266669d9a652828", size = 36545159 },
+ { url = "https://files.pythonhosted.org/packages/af/f5/ab3838e56fe5cc22383d6fcf2336e48c8fe33e944b9037fbf6cbdf5a11f8/scipy-1.15.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f031846580d9acccd0044efd1a90e6f4df3a6e12b4b6bd694a7bc03a89892b28", size = 39136566 },
+ { url = "https://files.pythonhosted.org/packages/0a/c8/b3f566db71461cabd4b2d5b39bcc24a7e1c119535c8361f81426be39bb47/scipy-1.15.2-cp313-cp313t-win_amd64.whl", hash = "sha256:fe8a9eb875d430d81755472c5ba75e84acc980e4a8f6204d402849234d3017db", size = 40477705 },
+]
+
+[[package]]
+name = "sentry-sdk"
+version = "2.22.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "certifi" },
+ { name = "urllib3" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/81/b6/662988ecd2345bf6c3a5c306a9a3590852742eff91d0a78a143398b816f3/sentry_sdk-2.22.0.tar.gz", hash = "sha256:b4bf43bb38f547c84b2eadcefbe389b36ef75f3f38253d7a74d6b928c07ae944", size = 303539 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/12/7f/0e4459173e9671ba5f75a48dda2442bcc48a12c79e54e5789381c8c6a9bc/sentry_sdk-2.22.0-py2.py3-none-any.whl", hash = "sha256:3d791d631a6c97aad4da7074081a57073126c69487560c6f8bffcf586461de66", size = 325815 },
+]
+
+[[package]]
+name = "setproctitle"
+version = "1.3.5"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/c4/4d/6a840c8d2baa07b57329490e7094f90aac177a1d5226bc919046f1106860/setproctitle-1.3.5.tar.gz", hash = "sha256:1e6eaeaf8a734d428a95d8c104643b39af7d247d604f40a7bebcf3960a853c5e", size = 26737 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/9d/e1/9ccff2682c38061baa07e128b60712bc18e3398aa7d5471c51a704f9d24c/setproctitle-1.3.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:02870e0cb0de7f68a7a8a5b23c2bc0ce63821cab3d9b126f9be80bb6cd674c80", size = 17256 },
+ { url = "https://files.pythonhosted.org/packages/ed/64/936c1f92d60052f11a8de9f90a4b7ec4996b8ebd6d67ba425ed214c80771/setproctitle-1.3.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:55b278135be742b8901067479626d909f6613bd2d2c4fd0de6bb46f80e07a919", size = 11893 },
+ { url = "https://files.pythonhosted.org/packages/01/2d/abc817b3778d9b1f7675020030379a0c39e0bf74b36af211b26191a63da3/setproctitle-1.3.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:53fc971f7bf7a674f571a23cdec70f2f0ac88152c59c06aa0808d0be6d834046", size = 31295 },
+ { url = "https://files.pythonhosted.org/packages/03/4d/e2055dfb1b492fd3a3b27deeaa642d81c580d48a16bc9b07afc3504af677/setproctitle-1.3.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fb0500e1bc6f00b8ba696c3743ddff14c8679e3c2ca9d292c008ac51488d17cf", size = 32637 },
+ { url = "https://files.pythonhosted.org/packages/89/28/a1f23d7d127dff59fe75ad671d1d5c83ab8cba10d0e343820b96d5d8a2f7/setproctitle-1.3.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:995b3ac1b5fe510f4e1d1c19ebf19f4bceb448f2d6e8d99ea23f33cb6f1a277e", size = 29772 },
+ { url = "https://files.pythonhosted.org/packages/df/46/2ea4d436c7d664d41df7e60fbd3103f1139a931638e998f478e870e72255/setproctitle-1.3.5-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a5a05e2c3fdfbda32b9c9da72d0506398d1efb5bd2c5981b9e12d3622eb3d4f9", size = 30811 },
+ { url = "https://files.pythonhosted.org/packages/45/60/4c17211c2d80e6fe9fa486fa3214d565d0cd9a6eff0b67e6219ddb2ba49c/setproctitle-1.3.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:310c7f4ca4c8476a9840b2cd4b22ee602a49a3c902fdcd2dd8284685abd10a9a", size = 30442 },
+ { url = "https://files.pythonhosted.org/packages/7e/bf/65a8f8f2d03cd9a9429cfa0d6b22282ff7a609a4d08602bcb8351a271bec/setproctitle-1.3.5-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:867af4a5c3d85484fbcc50ea88bcd375acf709cff88a3259575361849c0da351", size = 29492 },
+ { url = "https://files.pythonhosted.org/packages/c6/96/56f45f0b81fcc776f925c34e2699040df39cfc6b3cc7520d9b378314435b/setproctitle-1.3.5-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8ec0a7fe9f1ba90900144489bc93ce7dd4dec3f3df1e7f188c9e58364fe4a4c5", size = 31947 },
+ { url = "https://files.pythonhosted.org/packages/ec/9d/6b697c1562b21368e579d820bca2a607e565638fd332247841eb65dec4b2/setproctitle-1.3.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:aaee7acba2733a14a886488b7495bfec4a8d6407124c04a0946dbde1684230a3", size = 29863 },
+ { url = "https://files.pythonhosted.org/packages/ba/0f/4551cbb120d003fa1284ee35d559366e09b513a87dfee02f804da1936054/setproctitle-1.3.5-cp310-cp310-win32.whl", hash = "sha256:bd2cccd972e4282af4ce2c13cd9ebdf07be157eabafd8ce648fffdc8ae6fbe28", size = 11471 },
+ { url = "https://files.pythonhosted.org/packages/a6/f4/2dd926687b7a3bdaa83533e2898f929e1ff3bdeb6aa271bdb1d4d5923c7e/setproctitle-1.3.5-cp310-cp310-win_amd64.whl", hash = "sha256:81f2328ac34c9584e1e5f87eea916c0bc48476a06606a07debae07acdd7ab5ea", size = 12196 },
+ { url = "https://files.pythonhosted.org/packages/ec/4a/9e0243c5df221102fb834a947f5753d9da06ad5f84e36b0e2e93f7865edb/setproctitle-1.3.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:1c8dcc250872385f2780a5ea58050b58cbc8b6a7e8444952a5a65c359886c593", size = 17256 },
+ { url = "https://files.pythonhosted.org/packages/c7/a1/76ad2ba6f5bd00609238e3d64eeded4598e742a5f25b5cc1a0efdae5f674/setproctitle-1.3.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ca82fae9eb4800231dd20229f06e8919787135a5581da245b8b05e864f34cc8b", size = 11893 },
+ { url = "https://files.pythonhosted.org/packages/47/3a/75d11fedff5b21ba9a4c5fe3dfa5e596f831d094ef1896713a72e9e38833/setproctitle-1.3.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0424e1d33232322541cb36fb279ea5242203cd6f20de7b4fb2a11973d8e8c2ce", size = 31631 },
+ { url = "https://files.pythonhosted.org/packages/5a/12/58220de5600e0ed2e5562297173187d863db49babb03491ffe9c101299bc/setproctitle-1.3.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fec8340ab543144d04a9d805d80a0aad73fdeb54bea6ff94e70d39a676ea4ec0", size = 32975 },
+ { url = "https://files.pythonhosted.org/packages/fa/c4/fbb308680d83c1c7aa626950308318c6e6381a8273779163a31741f3c752/setproctitle-1.3.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eab441c89f181271ab749077dcc94045a423e51f2fb0b120a1463ef9820a08d0", size = 30126 },
+ { url = "https://files.pythonhosted.org/packages/31/6e/baaf70bd9a881dd8c12cbccdd7ca0ff291024a37044a8245e942e12e7135/setproctitle-1.3.5-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2c371550a2288901a0dcd84192691ebd3197a43c95f3e0b396ed6d1cedf5c6c", size = 31135 },
+ { url = "https://files.pythonhosted.org/packages/a6/dc/d8ab6b1c3d844dc14f596e3cce76604570848f8a67ba6a3812775ed2c015/setproctitle-1.3.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:78288ff5f9c415c56595b2257ad218936dd9fa726b36341b373b31ca958590fe", size = 30874 },
+ { url = "https://files.pythonhosted.org/packages/d4/84/62a359b3aa51228bd88f78b44ebb0256a5b96dd2487881c1e984a59b617d/setproctitle-1.3.5-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f1f13a25fc46731acab518602bb1149bfd8b5fabedf8290a7c0926d61414769d", size = 29893 },
+ { url = "https://files.pythonhosted.org/packages/e2/d6/b3c52c03ee41e7f006e1a737e0db1c58d1dc28e258b83548e653d0c34f1c/setproctitle-1.3.5-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:1534d6cd3854d035e40bf4c091984cbdd4d555d7579676d406c53c8f187c006f", size = 32293 },
+ { url = "https://files.pythonhosted.org/packages/55/09/c0ba311879d9c05860503a7e2708ace85913b9a816786402a92c664fe930/setproctitle-1.3.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:62a01c76708daac78b9688ffb95268c57cb57fa90b543043cda01358912fe2db", size = 30247 },
+ { url = "https://files.pythonhosted.org/packages/9e/43/cc7155461f0b5a48aebdb87d78239ff3a51ebda0905de478d9fa6ab92d9c/setproctitle-1.3.5-cp311-cp311-win32.whl", hash = "sha256:ea07f29735d839eaed985990a0ec42c8aecefe8050da89fec35533d146a7826d", size = 11476 },
+ { url = "https://files.pythonhosted.org/packages/e7/57/6e937ac7aa52db69225f02db2cfdcb66ba1db6fdc65a4ddbdf78e214f72a/setproctitle-1.3.5-cp311-cp311-win_amd64.whl", hash = "sha256:ab3ae11e10d13d514d4a5a15b4f619341142ba3e18da48c40e8614c5a1b5e3c3", size = 12189 },
+ { url = "https://files.pythonhosted.org/packages/2b/19/04755958495de57e4891de50f03e77b3fe9ca6716a86de00faa00ad0ee5a/setproctitle-1.3.5-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:523424b9be4dea97d95b8a584b183f35c7bab2d0a3d995b01febf5b8a8de90e4", size = 17250 },
+ { url = "https://files.pythonhosted.org/packages/b9/3d/2ca9df5aa49b975296411dcbbe272cdb1c5e514c43b8be7d61751bb71a46/setproctitle-1.3.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b6ec1d86c1b4d7b5f2bdceadf213310cf24696b82480a2a702194b8a0bfbcb47", size = 11878 },
+ { url = "https://files.pythonhosted.org/packages/36/d6/e90e23b4627e016a4f862d4f892be92c9765dd6bf1e27a48e52cd166d4a3/setproctitle-1.3.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ea6c505264275a43e9b2acd2acfc11ac33caf52bc3167c9fced4418a810f6b1c", size = 31940 },
+ { url = "https://files.pythonhosted.org/packages/15/13/167cdd55e00a8e10b36aad79646c3bf3c23fba0c08a9b8db9b74622c1b13/setproctitle-1.3.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0b91e68e6685998e6353f296100ecabc313a6cb3e413d66a03d74b988b61f5ff", size = 33370 },
+ { url = "https://files.pythonhosted.org/packages/9b/22/574a110527df133409a75053b7d6ff740993ccf30b8713d042f26840d351/setproctitle-1.3.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bc1fda208ae3a2285ad27aeab44c41daf2328abe58fa3270157a739866779199", size = 30628 },
+ { url = "https://files.pythonhosted.org/packages/52/79/78b05c7d792c9167b917acdab1773b1ff73b016560f45d8155be2baa1a82/setproctitle-1.3.5-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:828727d220e46f048b82289018300a64547b46aaed96bf8810c05fe105426b41", size = 31672 },
+ { url = "https://files.pythonhosted.org/packages/b0/62/4509735be062129694751ac55d5e1fbb6d86fa46a8689b7d5e2c23dae5b0/setproctitle-1.3.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:83b016221cf80028b2947be20630faa14e3e72a403e35f0ba29550b4e856767b", size = 31378 },
+ { url = "https://files.pythonhosted.org/packages/72/e7/b394c55934b89f00c2ef7d5e6f18cca5d8dfa26ef628700c4de0c85e3f3d/setproctitle-1.3.5-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:6d8a411e752e794d052434139ca4234ffeceeb8d8d8ddc390a9051d7942b2726", size = 30370 },
+ { url = "https://files.pythonhosted.org/packages/13/ee/e1f27bf52d2bec7060bb6311ab0ccede8de98ed5394e3a59e7a14a453fb5/setproctitle-1.3.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:50cfbf86b9c63a2c2903f1231f0a58edeb775e651ae1af84eec8430b0571f29b", size = 32875 },
+ { url = "https://files.pythonhosted.org/packages/6e/08/13b561085d2de53b9becfa5578545d99114e9ff2aa3dc151bcaadf80b17e/setproctitle-1.3.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f3b5e2eacd572444770026c9dd3ddc7543ce427cdf452d40a408d1e95beefb30", size = 30903 },
+ { url = "https://files.pythonhosted.org/packages/65/f0/6cd06fffff2553be7b0571447d0c0ef8b727ef44cc2d6a33452677a311c8/setproctitle-1.3.5-cp312-cp312-win32.whl", hash = "sha256:cf4e3ded98027de2596c6cc5bbd3302adfb3ca315c848f56516bb0b7e88de1e9", size = 11468 },
+ { url = "https://files.pythonhosted.org/packages/c1/8c/e8a7cb568c4552618838941b332203bfc77ab0f2d67c1cb8f24dee0370ec/setproctitle-1.3.5-cp312-cp312-win_amd64.whl", hash = "sha256:f7a8c01ffd013dda2bed6e7d5cb59fbb609e72f805abf3ee98360f38f7758d9b", size = 12190 },
+ { url = "https://files.pythonhosted.org/packages/ab/78/d6b5aa3af2dd64f6c32e78fb85797b9725a3cdcbdf17dffc5838019918c3/setproctitle-1.3.5-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:162fd76781f57f42ddf27c475e5fef6a8df4fdd69b28dd554e53e2eb2bfe0f95", size = 17238 },
+ { url = "https://files.pythonhosted.org/packages/3d/00/14781f0ac28c7a37fe2ba321c276188ddd5ca73d69dab8a0f739d57b776b/setproctitle-1.3.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4969d996bdfbe23bbd023cd0bae6c73a27371615c4ec5296a60cecce268659ef", size = 11867 },
+ { url = "https://files.pythonhosted.org/packages/f0/22/8430c879a8e3201508924a6cf45dba92b9a7b105fac8eebd0ef62e60fba9/setproctitle-1.3.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd70c95a94473216e7c7a7a1f7d8ecbaca5b16d4ba93ddbfd32050fc485a8451", size = 32001 },
+ { url = "https://files.pythonhosted.org/packages/01/f2/b00fe72c20897695f85932d193a5c57ecf94cbf825c0fd4082e3fa3e00bd/setproctitle-1.3.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7a887582bfdb6dcbc482db0ef9e630ad23ca95875806ef2b444bf6fbd7b7d7ca", size = 33415 },
+ { url = "https://files.pythonhosted.org/packages/11/5b/e497bf702ea5d553a331ca879e73a18bbd8f7d66d18d275cb2324e4144c4/setproctitle-1.3.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:755671c39a9e70834eeec6dc6b61e344399c49881d2e7ea3534a1c69669dd9cc", size = 30606 },
+ { url = "https://files.pythonhosted.org/packages/16/99/1bcb837134c71f332bfeaf923e68279566362b7d1504aa106af8046696e8/setproctitle-1.3.5-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ab52b4c2ce056a1b60d439991a81ca90f019488d4b4f64b2779e6badd3677e6", size = 31679 },
+ { url = "https://files.pythonhosted.org/packages/77/55/72af3dbb0b1304bad54ea3b7cf1b524a8a2868da0b4c38bc18290f0097f7/setproctitle-1.3.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:36178b944019ec7fc52bb967ffeee296a11d373734a7be276755bedb3db5c141", size = 31388 },
+ { url = "https://files.pythonhosted.org/packages/f3/08/fa13f2da6bd10ca756a45f8fed2888f439e9ce7d6402258e87ceef2d4c71/setproctitle-1.3.5-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:269d41cd4f085b69821d1ee6599124f02dbbc79962b256e260b6c9021d037994", size = 30370 },
+ { url = "https://files.pythonhosted.org/packages/25/4b/83575bb403967f1069b68a8799979fe7979b5a7c17703d2984965d8f4e92/setproctitle-1.3.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:d880630fd81d1b3bde121c352ca7ea2f2ff507ef40c3c011d0928ed491f912c9", size = 32897 },
+ { url = "https://files.pythonhosted.org/packages/1a/71/0c1e151ef6899260da4009e7170f56261486d3149e9bad40990b52bdd620/setproctitle-1.3.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8a7fed67ab49f60bd51f3b4cffff3f8d754d1bb0a40e42869911301ec6519b65", size = 30944 },
+ { url = "https://files.pythonhosted.org/packages/38/34/a3bdaeaee03e11aef82b45014738f1210f90e37359c41eda3e49b4ce891c/setproctitle-1.3.5-cp313-cp313-win32.whl", hash = "sha256:e9c0d0cfcf715631b10d5950d04a9978f63bc46535724ef7c2eaf1dca9988642", size = 11463 },
+ { url = "https://files.pythonhosted.org/packages/ef/f1/a19cde9f3f4054aed7c6077e7fc3420a5151ec6173cf3235fe000722ccb8/setproctitle-1.3.5-cp313-cp313-win_amd64.whl", hash = "sha256:e1d28eb98c91fbebd3e443a45c7da5d84974959851ef304c330eabd654a386f1", size = 12182 },
+ { url = "https://files.pythonhosted.org/packages/4a/ba/2524329ce958599069f0d0e4cfd3d6fbb7c58a4408b9e5609698e47353ec/setproctitle-1.3.5-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:dc66b84beb0d5eb03abf0c3140c6d2cbe3d67ae9f0824a09dfa8c6ff164319a6", size = 11418 },
+ { url = "https://files.pythonhosted.org/packages/a6/5f/a049640b05c609585ad0f471e667be0fd9ab533219127b455826d31587d5/setproctitle-1.3.5-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:31dc9b330e7cac7685bdef790747c07914081c11ee1066eb0c597303dfb52010", size = 13425 },
+ { url = "https://files.pythonhosted.org/packages/a9/15/caa47039e267ea67316b285e2e308ae529872ad6a143edf03a7d8edf6175/setproctitle-1.3.5-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4028639b511f5e641d116b3b54ad70c637ebd1b4baac0948283daf11b104119f", size = 13026 },
+ { url = "https://files.pythonhosted.org/packages/c1/a2/1fb0647a251f4c788b94f751cf23171b2a905758fd13ef8d126222d41428/setproctitle-1.3.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:6bddef4e27d0ed74e44b58bf050bc3108591bf17d20d461fc59cd141282f849c", size = 12222 },
+]
+
+[[package]]
+name = "setuptools"
+version = "76.0.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/32/d2/7b171caf085ba0d40d8391f54e1c75a1cda9255f542becf84575cfd8a732/setuptools-76.0.0.tar.gz", hash = "sha256:43b4ee60e10b0d0ee98ad11918e114c70701bc6051662a9a675a0496c1a158f4", size = 1349387 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/37/66/d2d7e6ad554f3a7c7297c3f8ef6e22643ad3d35ef5c63bf488bc89f32f31/setuptools-76.0.0-py3-none-any.whl", hash = "sha256:199466a166ff664970d0ee145839f5582cb9bca7a0a3a2e795b6a9cb2308e9c6", size = 1236106 },
+]
+
+[[package]]
+name = "simsimd"
+version = "6.2.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/da/1c/90e6ec0f0de20108fdd7d5665ac2916b1e8c893ce2f8d7481fd37eabbb97/simsimd-6.2.1.tar.gz", hash = "sha256:5e202c5386a4141946b7aee05faac8ebc2e36bca0a360b24080e57b59bc4ef6a", size = 165828 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/36/95/66c0485fd0734c6d77a96a11b7ec52a21c8a368b48f8400dcc8b5593685e/simsimd-6.2.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:9c79486cf75eb06c5e1f623e8315f9fb73620ac63b846d5a6c843f14905de43f", size = 170242 },
+ { url = "https://files.pythonhosted.org/packages/fb/c1/7c535b65aa1bcb0aef18407859f188ec5afc9404f6ad57e79e6ce74321a4/simsimd-6.2.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:104d53f2489dcbf569b8260d678e2183af605510115dc2b22ed0340aa47fe892", size = 102331 },
+ { url = "https://files.pythonhosted.org/packages/44/c5/fe1915c70f82733782f57e9410bd92936a51ba6f5d2408aa98204a16885c/simsimd-6.2.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fef886c8220d3566b9f43d441226ca267a11682dea5496bb6e007f655eee1fd1", size = 93455 },
+ { url = "https://files.pythonhosted.org/packages/a7/b0/9a7df126e36bf1397c31f1e2482857183b5eac61141cf72041d730fd5b4d/simsimd-6.2.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:522e56451481bff3468653c2818ad1240b4cb13cff0ec76bc88d8860bfc775c9", size = 251045 },
+ { url = "https://files.pythonhosted.org/packages/16/6a/15578d772bb4b5506b5617d078557296fce74b7206bb1c9d3fe6db0e47c8/simsimd-6.2.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a5dfb02fa141a6e039803044930753aef1df5ed05cae8b14fe348cdc160cef1e", size = 302448 },
+ { url = "https://files.pythonhosted.org/packages/49/51/cbf5f43c8cb1c9e173a040004ebb7726b87936e5110b15916510c1b7fa32/simsimd-6.2.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:39eb6abdd44adfddec181a713e9cfad8742d03abbc6247c4e5ca2caee38e4775", size = 227246 },
+ { url = "https://files.pythonhosted.org/packages/9e/56/3f3609cbeaf9393158ef5ee5cf60b8e2190bb87925e21a43dd321c52a05f/simsimd-6.2.1-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:9ca68b9d2cc1c19af6afe6f01a764861fc8bb919d688a64cf0b0ac0abae7e0fa", size = 432346 },
+ { url = "https://files.pythonhosted.org/packages/56/53/13629d84b95b9373b7ce1447c43fc09da448d521bfa93eb02a8806ec0a50/simsimd-6.2.1-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:2b56b1ca7b76c0d4515938a036e688b73a866b19e6f6eb743596144fdf498a0c", size = 632661 },
+ { url = "https://files.pythonhosted.org/packages/d7/52/6361628a462b6e753f1ed9d5de9c4e1f3d35ced2922c7e196ce4e45d81fa/simsimd-6.2.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:02d7b7c7afecc63ddf501460f09c1da90625bfd59b4da5fda126c1aa5c54bb95", size = 468411 },
+ { url = "https://files.pythonhosted.org/packages/ef/f1/f56395d5885a3a19268d8f62589e3cc5b37b7c0f407fcf89bacf1d57397c/simsimd-6.2.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:8abc529daf0a61649ca4a237cd9e63723f3355394686898654c643bd63846cf5", size = 268931 },
+ { url = "https://files.pythonhosted.org/packages/b1/90/597c8756697b7fdb7f4b6e7d7e4c85207b449c286b6bf8a6c3815798bc33/simsimd-6.2.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:9ea60422d0f45d3a1899984c3fc3a14dbd248cfca8f67c24751029441464a806", size = 344281 },
+ { url = "https://files.pythonhosted.org/packages/16/fb/9b976f87db319ad95b541f94232a1cc6d0d3c16b01f910e1f8b967b241d5/simsimd-6.2.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:98e38a0ca4805c1de2882d0641b54e249eabca4ed2980c82465822130d7f8c98", size = 389374 },
+ { url = "https://files.pythonhosted.org/packages/da/e1/d3e41accb2a4a3b6fd46c7900c49e36b7d426e20e49e06b3418316eba2b9/simsimd-6.2.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:cbbc2434286493b88f3b8211e922d37b46588b34d4cc28f3262f154c8ca1141c", size = 316688 },
+ { url = "https://files.pythonhosted.org/packages/28/1f/c8cc75df5d386071e067ca22d54b6629eb6d600879e223bba3ddf96849d7/simsimd-6.2.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:4f2ecd459f4917facdb287c42c5e68030b21cb98edac0fec9919a7215968e38a", size = 669697 },
+ { url = "https://files.pythonhosted.org/packages/ab/cc/d4a0f90706432fa3b5cbde390ec7f213e7639ce6cf87be0f9f19ff8a23d9/simsimd-6.2.1-cp310-cp310-win32.whl", hash = "sha256:4ec31c076dc839114bff5d83526ddf46551d4720cc8cd0f16516896809a4fca6", size = 55008 },
+ { url = "https://files.pythonhosted.org/packages/9b/e6/33ea89f17e83a8743f9461c85f926203ef5a82782c4a72263571b7186427/simsimd-6.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:94282e040be985c993d415290371f6b22bec3eeadafe747a6d8dfbd2c317f35e", size = 86852 },
+ { url = "https://files.pythonhosted.org/packages/ad/30/65252e79ef62807c33e22f1df04b3dbd16ceda5ecc88bf46de239a4516c3/simsimd-6.2.1-cp310-cp310-win_arm64.whl", hash = "sha256:0784e98ca48a0075fb0cbd7782df11eaa17ce15c60f09a65e8477864208afb8a", size = 60194 },
+ { url = "https://files.pythonhosted.org/packages/a7/5f/361cee272fd6c88f33e14e233792f59dd58836ea8c776344f7445a829ca2/simsimd-6.2.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:e9614309af75be4d08a051dc61ed5cf41b5239b8303b37dc2f9c8a7223534392", size = 170254 },
+ { url = "https://files.pythonhosted.org/packages/b8/88/edf4442ec655765d570bfb6cef81dfb12c8829c28e580459bac8a4847fb5/simsimd-6.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ea4f0f68be5f85bbcf4322bfdd1b449176cf5fdd99960c546514457635632443", size = 102331 },
+ { url = "https://files.pythonhosted.org/packages/5d/2b/9e7d42ac54bdb32d76953db3bc83eec29bd5d5c9a4069d380b18e200d6bd/simsimd-6.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:12a8d60ccc8991dfbbf056c221ce4f02135f5892492894972f421a6f155015d9", size = 93455 },
+ { url = "https://files.pythonhosted.org/packages/13/9c/fac1167e80328d1e332f515c9cd62da4a0e12b9aa8ee90d448eb4ad5a47f/simsimd-6.2.1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a74142ea21a6fd3ec5c64e4d4acf1ec6f4d80c0bb1a5989d68af6e84f7ac612e", size = 251040 },
+ { url = "https://files.pythonhosted.org/packages/31/93/b374e5538fc65cf381920bdba7603769b1b71e42afe2bb4939e9c338c423/simsimd-6.2.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:298f7c793fc2a1eeedcefa1278eb2ef6f52ce0b36aaa8780885f96a39ce1a4e8", size = 302428 },
+ { url = "https://files.pythonhosted.org/packages/e6/42/2733a0e11b660c6b10f3ec90d7fac6f96267368b961b1a43dda0456fa9f2/simsimd-6.2.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4025ebad36fb3fa5cffcd48d33375d5e5decc59c1129a259b74fed097eab1ab5", size = 227200 },
+ { url = "https://files.pythonhosted.org/packages/eb/ae/40e0804d06a351efe27bb6f8e4d332daeb1681d3f398ca10d8a2b087ab78/simsimd-6.2.1-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:f486682aa7a8918d86df411d3c11c635db4b67d514cb6bb499c0edab7fb8ec58", size = 432333 },
+ { url = "https://files.pythonhosted.org/packages/a7/eb/a823b0227b5dc43de8125f502237dd8e844b1e803a74e46aa7c3d0f24f83/simsimd-6.2.1-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:173e66699597a4fcf6fa50b52cced40216fdcfba15f60b761a2bd9cb1d98a444", size = 632659 },
+ { url = "https://files.pythonhosted.org/packages/0a/aa/aee48063c4a98aaea062316dedf598d0d9e09fa9edc28baab6886ae0afa8/simsimd-6.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5b5c6f79f797cc020a2ff64950162dfb6d130c51a07cdac5ad97ec836e85ce50", size = 468407 },
+ { url = "https://files.pythonhosted.org/packages/d4/84/e89bc71456aa2d48e5acf3795b2384f597de643f17d00d752aa8217af233/simsimd-6.2.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:25812637f43feaef1a33ae00b81a4d2b0116aadae3a08267486c1e57236fc368", size = 268908 },
+ { url = "https://files.pythonhosted.org/packages/94/eb/774debec7ee727f436f15e5b5416b781c78564fff97c81a5fb3b636b4298/simsimd-6.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:592a578c788a9cb7877eff41487cc7f50474e00f774de74bea8590fa95c804ae", size = 344256 },
+ { url = "https://files.pythonhosted.org/packages/62/03/fec040e7fbb66fa4766ca959cfd766a22d7a00a4e9371f046d8fcc62d846/simsimd-6.2.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:191c020f312350ac06eee829376b11d8c1282da8fefb4381fe0625edfb678d8d", size = 389403 },
+ { url = "https://files.pythonhosted.org/packages/55/f0/ad441d90a4dde6e100155931fa4468e33cc23276c3caef6330d2a34b866c/simsimd-6.2.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e9ad2c247ed58ba9bb170a01295cb315a45c817775cc7e51ad342f70978a1057", size = 316665 },
+ { url = "https://files.pythonhosted.org/packages/05/27/843adbc6a468a58178dcb7907e72c670c8a7c36a06d8a4c5eac9573f5d2d/simsimd-6.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0ff603134600da12175e66b842b7a7331c827fa070d1d8b63386a40bc8d09fcd", size = 669697 },
+ { url = "https://files.pythonhosted.org/packages/6d/db/d2369e0d3b9ca469b923bc81d57dcfed922193e4e4d7cf5f7637df14dd51/simsimd-6.2.1-cp311-cp311-win32.whl", hash = "sha256:99dff4e04663c82284152ecc2e8bf76b2825f3f17e179abf7892e06196061056", size = 55007 },
+ { url = "https://files.pythonhosted.org/packages/73/9f/13d6fca5a32a062e84db0a68433ae416073986c8e1d20b5b936cad18bece/simsimd-6.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:0efc6343c440a26cf16463c4c667655af9597bcbd55ad66f33a80b2b84de7412", size = 86855 },
+ { url = "https://files.pythonhosted.org/packages/64/e9/7e0514f32c9a0e42261f598775b34a858477e0fcffccf32cc11f94e78ee2/simsimd-6.2.1-cp311-cp311-win_arm64.whl", hash = "sha256:2d364f2c24dd38578bf0eec436c4b901c900ae1893680f46eb5632e01330d814", size = 60195 },
+ { url = "https://files.pythonhosted.org/packages/81/87/1f521d471d9079d89dd6860b9dd5d0f39c1633675a30b71acd0bd37cbba5/simsimd-6.2.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9b3315e41bb759dc038ecd6f4fa7bcf278bf72ee7d982f752482cdc732aea271", size = 169397 },
+ { url = "https://files.pythonhosted.org/packages/4b/1a/b0627589737dc75ccd2ed58893e9e7f8b8e082531bd34d319481d88018d5/simsimd-6.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8d476c874bafa0d12d4c8c5c47faf17407f3c96140616384421c2aa980342b6f", size = 101478 },
+ { url = "https://files.pythonhosted.org/packages/e0/b7/e766f0ce9b595927ae1c534f1409b768187e8af567f4412ca220b67c1155/simsimd-6.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e9d4f15c06cc221d29e181197c7bbf92c5e829220cbeb3cd1cf080de78b04f2a", size = 93439 },
+ { url = "https://files.pythonhosted.org/packages/ae/48/3b5ec9b3a6063bae2f280f5168aca7099a44fa7ec8b42875b98c79c1d49b/simsimd-6.2.1-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d286fd4538cb1a1c70e69da00a3acee301519d578931b41161f4f1379d1195c6", size = 251469 },
+ { url = "https://files.pythonhosted.org/packages/70/86/16e8d5b9bdd34f75c7515adfad249f394653131bd1a1366076cf6113e84b/simsimd-6.2.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:050f68cfa85f1fb2cfa156280928e42926e3977034b755023ce1315bf59e87ff", size = 302974 },
+ { url = "https://files.pythonhosted.org/packages/02/09/3f4240f2b43957aa0d72a2203b2549c0326c7baf97b7f78c72d48d4cd3d2/simsimd-6.2.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:67bb4b17e04919545f29c7b708faaccbe027f164f8b5c9f4328604fa8f5560ea", size = 227864 },
+ { url = "https://files.pythonhosted.org/packages/07/4a/8c46806493c3a98025f01d81d9f55e0e574f11279c2ad77be919262ea9eb/simsimd-6.2.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:3d6bffd999dbb36e606b065e0180365efac2606049c4f7818e4cba2d34c3678f", size = 432491 },
+ { url = "https://files.pythonhosted.org/packages/13/44/b56f207031405af52c6158c40e9f1121fe3a716d98946d9fa5919cf00266/simsimd-6.2.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:25adb244fb75dbf49af0d1bcac4ed4a3fef8e847d78449faa5595af0a3e20d61", size = 633061 },
+ { url = "https://files.pythonhosted.org/packages/4c/ad/241f87641af09a1789af8df559aa86b45218d087e09c37c2dd8c013819d6/simsimd-6.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b4542cee77e801a9c27370fc36ae271514fc0fb2ce14a35f8b25f47989e3d267", size = 468544 },
+ { url = "https://files.pythonhosted.org/packages/e2/3e/357aca7df85ed1092dfa50b91cf1b7c0df6f70b384a0e3798132dd824b5c/simsimd-6.2.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:4f665228f8ff4911790b485e74b00fa9586a141dde6011970be71bb303b5a22f", size = 269133 },
+ { url = "https://files.pythonhosted.org/packages/f0/67/079ca2c58bbc5812802c6ac1b332a6ef889d73cf1188726f36edc27898f6/simsimd-6.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:783b4308f80ae00763b0eaa0dac26196958f9c2df60d35a0347ebd2f82ece46d", size = 344412 },
+ { url = "https://files.pythonhosted.org/packages/3c/f0/500c9002276259c17e3a6a13a7c7f84e5119602decadbf40429c978655b0/simsimd-6.2.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:95055e72cfe313c1c8694783bf8a631cc15673b3b775abef367e396d931db0b8", size = 389546 },
+ { url = "https://files.pythonhosted.org/packages/55/a2/d3f4c6aabba0430758367b3de5bbab59b979bf3525c039b882001f1d2ade/simsimd-6.2.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:a98f2b383f51b4f4ee568a637fc7958a347fdae0bd184cff8faa8030b6454a39", size = 316912 },
+ { url = "https://files.pythonhosted.org/packages/f8/a3/2514189c3aaa1beb1714b36be86e2d3af7067c3c95152d78cc4cffff6d87/simsimd-6.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2e474fd10ceb38e2c9f826108a7762f8ff7912974846d86f08c4e7b19cd35ed4", size = 670006 },
+ { url = "https://files.pythonhosted.org/packages/ef/23/dbf7c4aed7542260784dc7bc2056a4e5b6d716a14a9b40989d5c3096990a/simsimd-6.2.1-cp312-cp312-win32.whl", hash = "sha256:b2530ea44fffeab25e5752bec6a5991f30fbc430b04647980db5b195c0971d48", size = 55019 },
+ { url = "https://files.pythonhosted.org/packages/a0/d8/57304c2317822634abd475f5912584a3cfa13363740e9ec72c0622c894f1/simsimd-6.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:dc23283235d5b8f0373b95a547e26da2d7785647a5d0fa15c282fc8c49c0dcb0", size = 87133 },
+ { url = "https://files.pythonhosted.org/packages/3f/7b/ca333232a8bc87d1e846fa2feb9f0d4778500c30493726cb48f04551dfab/simsimd-6.2.1-cp312-cp312-win_arm64.whl", hash = "sha256:5692ce7e56253178eea9dbd58191734918409b83d54b07cfdcecf868d0150a73", size = 60401 },
+ { url = "https://files.pythonhosted.org/packages/9b/f2/4ec7ed52c910a58a07043c5f3355adf4055246dafb79be57d0726e1a4aa0/simsimd-6.2.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:76b32fdc7142c9714e94651ece8bc00dd5139c554813211552aa358e44af0e07", size = 169399 },
+ { url = "https://files.pythonhosted.org/packages/61/d3/5af24e4f42e2b5bc3a06456ea9068d0fbcd23d8ceeb0e09fe54ed72cfdba/simsimd-6.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f44e5e2319427f94db658c6f75caae78850da505902874a1664a83ef5713f333", size = 101484 },
+ { url = "https://files.pythonhosted.org/packages/cf/86/816050f0fd0767e960c6b900e3c97fd6a4ae54a6aa5b8ef24846757a3f7d/simsimd-6.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:05323cbad7200592c2e53fbcc759e615594e8ca444ef5eddf9f3fb196ad4de9c", size = 93447 },
+ { url = "https://files.pythonhosted.org/packages/e9/7e/61dc3392eafd9fc20357b448aac5f84c84ad61289ab0ab3e5a4aaa1ca3ef/simsimd-6.2.1-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b1f3cbe5c39db2bb64f30999104de1215ba3805d6059af7bc5a9d662d50f4707", size = 251501 },
+ { url = "https://files.pythonhosted.org/packages/06/55/99d3cf2c2d844c1a57d81379acaebac2e0a0efdf1e73a53990cd84c1d719/simsimd-6.2.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eaa94e0932ae2a48b7e4df8c29204dc9fe59f72b1faeb08e9d5015bf51fb9f21", size = 302991 },
+ { url = "https://files.pythonhosted.org/packages/6f/99/597b322835147f407e6f611810cb8232055711398fbbd47e6a14bfc0995f/simsimd-6.2.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:508465f8d4e3e0fff07c939921aeedf55b0ade9f56f64e938c350c283dea42fb", size = 227917 },
+ { url = "https://files.pythonhosted.org/packages/ba/8a/6a6596a97d1cc7068a26935bbdd7f170a889240b8081e000aef09b6d0549/simsimd-6.2.1-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:ca67f6273ef544c74c48b134af756de7c98a711ccf69cd0791225f26dd449281", size = 432527 },
+ { url = "https://files.pythonhosted.org/packages/46/0e/5c6e82fa9fe9a21481fe0f6546b4986e07e42bd4d8b6f04f4475b8d7564e/simsimd-6.2.1-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:d470b43ce606f21f54a23fc19ad6928333e17d0956b02eb27b7b112edc156a10", size = 633095 },
+ { url = "https://files.pythonhosted.org/packages/ae/53/2e17bd16e2ca2a73cd447b89fa7059ae7275c82840f229bf917936ee800a/simsimd-6.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:59518b9834c167a1dd8900600718e95cdadc9d74525452f426aa8455a38c55ef", size = 468561 },
+ { url = "https://files.pythonhosted.org/packages/86/8b/1319605c630973741bc749b6e432e56dded2b6a7db0744b659c0de613ab3/simsimd-6.2.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:59c2978c4e402097d8a4b38f076ff98cc43e6b059d53f89736404f26e9a9bd5a", size = 269157 },
+ { url = "https://files.pythonhosted.org/packages/53/50/1cac5113a542c82d5b5399d454c578a65ba14951bfff38aef297104f72fe/simsimd-6.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:edc68e727d53ed2866dcfb625f15e52be8f1e6809f4be2147bf8d2115a2542b7", size = 344437 },
+ { url = "https://files.pythonhosted.org/packages/9a/72/44905ee0e2ed999c52ad1eebf2c8705ce2776212a6387d77355df2c76704/simsimd-6.2.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:9e5e82551d75c0e2cd0d4b8af8db1cae7b5ac6dcc076c0c760870ff81f78135b", size = 389569 },
+ { url = "https://files.pythonhosted.org/packages/ee/d6/9b4a9141ceb29150d86698553c8e0193256b069bc755e875836c14a6f12e/simsimd-6.2.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:2fa19f8c9786757d19afcbda9f8fb68de55e4f5562725ae8727f887d01bf0e4d", size = 316923 },
+ { url = "https://files.pythonhosted.org/packages/ce/c0/de6aebd58b8de8f0177395b8fd68afb9a27ec010427c4ccd6104b94b6569/simsimd-6.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5b0748aa6bd4df4c5a3f5e979aec14b26588f1b2e0d44075dcc9eaf4d555e15b", size = 670038 },
+ { url = "https://files.pythonhosted.org/packages/77/32/4c74664656231ccb43be4328dba40e9ada63d3cc1e557b1785ae0b9560b5/simsimd-6.2.1-cp313-cp313-win32.whl", hash = "sha256:7f43721e1a4ebe8d2245b0e85dd7de7153d1bf22839579d5f69a345909c68d9e", size = 55017 },
+ { url = "https://files.pythonhosted.org/packages/76/7f/57e02f6b2d09a1d42697e739b002bbe2112f8b8384d15d166154ec4cec44/simsimd-6.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:6af1565e0ef7060bc52a38e3273a8e6e92aff47835965dc5311298563475935e", size = 87138 },
+ { url = "https://files.pythonhosted.org/packages/38/b9/941876e98dd1f98c158cd5e6633dc1573d1be6daf8f2e3ad5d15e6a8024d/simsimd-6.2.1-cp313-cp313-win_arm64.whl", hash = "sha256:e690b41377c8dd157d585713b0bc35c845aee7742334bf12d1f087fc8a65b6c3", size = 60408 },
+]
+
+[[package]]
+name = "six"
+version = "1.17.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050 },
+]
+
+[[package]]
+name = "smmap"
+version = "5.0.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/44/cd/a040c4b3119bbe532e5b0732286f805445375489fceaec1f48306068ee3b/smmap-5.0.2.tar.gz", hash = "sha256:26ea65a03958fa0c8a1c7e8c7a58fdc77221b8910f6be2131affade476898ad5", size = 22329 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/04/be/d09147ad1ec7934636ad912901c5fd7667e1c858e19d355237db0d0cd5e4/smmap-5.0.2-py3-none-any.whl", hash = "sha256:b30115f0def7d7531d22a0fb6502488d879e75b260a9db4d0819cfb25403af5e", size = 24303 },
+]
+
+[[package]]
+name = "stringzilla"
+version = "3.12.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/9b/a4/220986fea350eb6cc4639e44256186f0b793a8b749df522f155f89543cf2/stringzilla-3.12.3.tar.gz", hash = "sha256:33ed7cb71724373474d387a0e17751bd9ad21caa08a1b8b74b961dea4b890a66", size = 186813 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d1/ac/ba364717b7ff792f95681c861984fab4774ddc73dc206f6b5c8cbdd6a44b/stringzilla-3.12.3-cp310-cp310-macosx_10_11_universal2.whl", hash = "sha256:8ec8f95af09d62b4ca5dc8c6f557035acadfe794edd2291f515b54f3afb59260", size = 121534 },
+ { url = "https://files.pythonhosted.org/packages/c9/dd/e27978a5ce695311f6474781968e34b8d43e0384f6e5df8509912d4787d2/stringzilla-3.12.3-cp310-cp310-macosx_10_11_x86_64.whl", hash = "sha256:d305ed6f35132852844f59964910b202675f76ad48060fea8c6c959b67959c3d", size = 79220 },
+ { url = "https://files.pythonhosted.org/packages/48/1e/677c2d365138e94654b9839eae5ccb0cbf8e9ee64d5eec7557fb0159bcf8/stringzilla-3.12.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8d1eb356ca15400b1410187bc7fde421f6e20460f05ae1dece4c60821bfffba6", size = 79218 },
+ { url = "https://files.pythonhosted.org/packages/a7/99/3cfb0762968cbfb83544d64f2832f2be797464018a251a6b542a703f3dc3/stringzilla-3.12.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d0f7c5bccff1125fb77c01d7166e525c8dbf40f700f4120a00ad2f4ccfb3f3d3", size = 228991 },
+ { url = "https://files.pythonhosted.org/packages/d0/24/588d9b1959c151f4ab70a091c839e4f90410abd9b774dd482e53fe01d52f/stringzilla-3.12.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:db02c9baa70eceb6f1362237411ef4b6b59064e17008da339f378717050c2bdb", size = 231755 },
+ { url = "https://files.pythonhosted.org/packages/4f/ad/2ec6aa456927d529eea87190b5037562dcd6ce3219ffa424274acd7020d2/stringzilla-3.12.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.manylinux_2_28_s390x.whl", hash = "sha256:da5938ba65d0f7bff83479662b7f397d71cac3e84fc37b4504616712e37d0417", size = 203377 },
+ { url = "https://files.pythonhosted.org/packages/4e/f1/5aad5b9c090b298a73105d8b6456416c27ab3c3886e7dde193b3128fbc32/stringzilla-3.12.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3ce27645cbe2593ede23c581a50428f1d0ba1d666af06d445ba89cef6613df0", size = 208918 },
+ { url = "https://files.pythonhosted.org/packages/0d/37/91af6c262da63c117f92b63d9a59cf8a3b0a44988e6e11effb822233348a/stringzilla-3.12.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:680db2ce49bee340904a8c3149b0bca527cd9dc322a97071e4500f646d87404e", size = 304510 },
+ { url = "https://files.pythonhosted.org/packages/b2/2f/eb5d648b61653685aac1a6d5ca569d584397c1438abf12751211cc30f5f0/stringzilla-3.12.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:32537a5165f473cd99440baa23e9202ab2185d72896960daa4f8bddac5c80544", size = 224351 },
+ { url = "https://files.pythonhosted.org/packages/81/2f/7b919b0bb1bc79a60cf21c5b39572236ba81f36aefa343dc94dfa24411a7/stringzilla-3.12.3-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:c39d612f26ea7385b6a134292b097efe7444cae5b2dafbfccd3b65ce23d73ccc", size = 197900 },
+ { url = "https://files.pythonhosted.org/packages/77/94/7d0958f28b5483f98c6f9dbd25abbab77412bc796da84473f896d024f873/stringzilla-3.12.3-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:dea7c8ca195f8c79e85163fd5b76495b4a11effd2e17956e6825b4fffa3c915b", size = 210267 },
+ { url = "https://files.pythonhosted.org/packages/d6/37/ce2a90e437c41a55912d7abaffde7812dfdcde1381d0c473e20b8c3b7482/stringzilla-3.12.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:0d21de495a84b05196832e5aec0d5ee83b0667f9c165d0e326a753022625adfa", size = 229176 },
+ { url = "https://files.pythonhosted.org/packages/e1/21/dff54b548021c10151f8a1393595478e27f5fb31481371e0ed0ea678005b/stringzilla-3.12.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:7c836c28a75cd4cccb0737854ed780444c45cce93012ed100dd4a431b60ebd86", size = 203032 },
+ { url = "https://files.pythonhosted.org/packages/3c/b5/9125a02bc127ec9e52b65dca5aff2f08925884f4906cb9debff5e6be5666/stringzilla-3.12.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:745f144c039c2c2787ef0d63296f06d4d0d776b141288a74670aea38d86b3078", size = 298448 },
+ { url = "https://files.pythonhosted.org/packages/0c/28/9c9a22fb234ae9ce4025aab09742a9d8f26fd3b642270f42943c1a472b37/stringzilla-3.12.3-cp310-cp310-win32.whl", hash = "sha256:2d4803357a07592a7fdbea599e93cfd4366c4e887290cfa5988bc7ec44da93b5", size = 68503 },
+ { url = "https://files.pythonhosted.org/packages/6c/70/02340a425be0bd7e797cd5787ab044ee092db65fb3e18c43fb152019ab71/stringzilla-3.12.3-cp310-cp310-win_amd64.whl", hash = "sha256:0f41b85c38445f7a1fed677984590c943b16cbc00727e2b093b2f0b2bdbfcac5", size = 80094 },
+ { url = "https://files.pythonhosted.org/packages/44/40/a2080966e7f483e67bce971b6f720500eb0cc0942072b2a859562820076f/stringzilla-3.12.3-cp310-cp310-win_arm64.whl", hash = "sha256:088ca8105ff027172277d2221ea0241d5ed21cc10ee91d5f45c7961ddab3d12a", size = 69753 },
+ { url = "https://files.pythonhosted.org/packages/7f/4c/b8a5fee15b4bf20588d64f2e1650e1e25c4caa280c943ce44bf3cf58d158/stringzilla-3.12.3-cp311-cp311-macosx_10_11_universal2.whl", hash = "sha256:d0e79931ae66cd4566f25d77ccf646e9d180ead603fab4278a6ecdae7570e85b", size = 121529 },
+ { url = "https://files.pythonhosted.org/packages/bd/b7/756fbad92e8959b9eb19c90e05dd8b08b95ab024a40ccae95f192935eb1e/stringzilla-3.12.3-cp311-cp311-macosx_10_11_x86_64.whl", hash = "sha256:b3a2f047dfe21468f90e8cab3f6a4b8e46e876b6563b78dc54ba154a56f1e383", size = 79216 },
+ { url = "https://files.pythonhosted.org/packages/1b/90/77e857203cd5d18523a81467f7c87fc102cefeafea0892fc98acb8e92ae7/stringzilla-3.12.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8b8404b55fa180d1e6da1fc10f739890af427d93afd02a408e229be8d7383a5e", size = 79217 },
+ { url = "https://files.pythonhosted.org/packages/c9/e0/12754b438ad9c3cdcd7d9137d656a3017b2dc23e5bec3621be6ff7678de0/stringzilla-3.12.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bdbbb9f0dd3f8d1ec3201a3fa7de3e0c92d056da9ca61ada2af8ca662cab4834", size = 231896 },
+ { url = "https://files.pythonhosted.org/packages/f4/91/6909afd0597b2f62d4c9d1a958a7a1230dcc81ef7ab5ca1b58c9f47f7c92/stringzilla-3.12.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:162e68d44e87f3b7591a9c18f8a7794bc9bbf8ab9b16705bfe5c552c676b2d8c", size = 235212 },
+ { url = "https://files.pythonhosted.org/packages/42/16/926df205c9aa8b0fc12f615e5e0dd481cba18101fb2e22dfcb9c88632bed/stringzilla-3.12.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ee7284d1c62cc4d4cf7772178c02cae91933a38e8b11390da6e8a8b4f20e0663", size = 206333 },
+ { url = "https://files.pythonhosted.org/packages/b3/ae/4fb8076de02890d86d9b4ec72881d7c6066b9d5067a9e1ac5c75abcb8ab7/stringzilla-3.12.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4d08a9fda6b9489667dfba38dcc8ebe7a94e4ebbe8e741557cccd5b09f864ed7", size = 211874 },
+ { url = "https://files.pythonhosted.org/packages/05/ea/9f67f16b246561a2025e9259c38d565f740e9aa63917a2fa5c05c1563f25/stringzilla-3.12.3-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c11a94e9123e411e8d74cd0ea860dca58bec6e48a95b5ff3707b595eaf71eecd", size = 307630 },
+ { url = "https://files.pythonhosted.org/packages/88/68/b9303804b4a09c4853da53ada44ca2584d753f287ae898bd543ada119587/stringzilla-3.12.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:639844f35f0be5ade5849686067076add2214ce93a01ffd2ab64b2a6c506f666", size = 227050 },
+ { url = "https://files.pythonhosted.org/packages/53/d5/083c642cf3b60d25c9c9381bfcaf702b83d7ec364aa7cca4dc0fb70d21e9/stringzilla-3.12.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:98579b756f10aa515b97b58d69a6aff3daefb7267cdf84e225e5b7fda584a431", size = 201126 },
+ { url = "https://files.pythonhosted.org/packages/a3/6f/29c2612bf31fb7de37a0cab1429358a71359128ed72dbb392c83d862bdfd/stringzilla-3.12.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:dd95a2f8782183340fd706f75aa8615c21a2eacc0c11684fd6b3ee17b1ba3542", size = 213500 },
+ { url = "https://files.pythonhosted.org/packages/47/2d/eaf7c55e1d650968dd07593f701443fba3f5bb98747ba24c5096d77d2b49/stringzilla-3.12.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:06d543e4949e43c43a8b4334fda8c162999249daeb787af5ea6b0e8d0682ce79", size = 232358 },
+ { url = "https://files.pythonhosted.org/packages/61/6d/81962e6970affa600d3caa5f2a7f09755508ea2487805c5f1b9c70afe659/stringzilla-3.12.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:783ff26460fc8028cf53ec08ddacf438d0efffed78f75f34131cdfc77d15c2cf", size = 206295 },
+ { url = "https://files.pythonhosted.org/packages/ad/4d/614cf81b8362bd7ea82acad8cc8f02580fd8ad174e9829345c4b19627d39/stringzilla-3.12.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:39cfca4f26c70f262016ed16a035f42d1ed0917e28f78db6c61670690983b96b", size = 301876 },
+ { url = "https://files.pythonhosted.org/packages/56/e2/dcd375930e21ef146fa803bcd9e2c288341c1a1a9e4dc99178dab47297f1/stringzilla-3.12.3-cp311-cp311-win32.whl", hash = "sha256:bb593a0e809451254a819e156a8180cb53a239f1f427a8bdb2a39f7c52e85a43", size = 68503 },
+ { url = "https://files.pythonhosted.org/packages/0f/7e/ee8666d79a4097bec6cf0c79afd738bce80286c45ea8d336834a3f493f42/stringzilla-3.12.3-cp311-cp311-win_amd64.whl", hash = "sha256:587f1934ef615b5e11ce1b1779cf391d40e0ead6f6be6083d313dc6b4cc7f4dd", size = 80094 },
+ { url = "https://files.pythonhosted.org/packages/d4/14/a4b5d9abd4bc3f5875eae1724bf906a6287f18e83af42c1ad9682623b30c/stringzilla-3.12.3-cp311-cp311-win_arm64.whl", hash = "sha256:329d734c4eb943d9746d8bb2fc2008063b8b33b8f9af27833abea876b6027aeb", size = 69749 },
+ { url = "https://files.pythonhosted.org/packages/02/d8/1ee03cfd47231c50764e22f95ae2365e0c0719d5e6728c3c00ff85da8234/stringzilla-3.12.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e28c644def937fd0baa887c1e4e758778d59773e996ac830013b3468671d96aa", size = 121849 },
+ { url = "https://files.pythonhosted.org/packages/e8/15/69b0a222135b402cc64904acbf330d5f79f9d35727d93975e9376e29a873/stringzilla-3.12.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:773da6ad2993c3bbbdbff87b583eb1cc4b655c33809bfd3f913f749be12bfdd0", size = 79405 },
+ { url = "https://files.pythonhosted.org/packages/a8/c4/03e4a3e0b13f0c844dd1728245dd1ea1977c225041b86d4c655cc6666bf6/stringzilla-3.12.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e43e92b66d12c6d6487bccbb0b35e6dca670932a92ebb5e59b62e7212aaf739f", size = 79363 },
+ { url = "https://files.pythonhosted.org/packages/d6/ee/50941a8119cb97ffff9d26e15a1600af2054ab75a4222ed4311f9c43d2d2/stringzilla-3.12.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:27d29eb61ced7a2dcf44195eea058e5aa44d4c8b73c2095a435ca9533418f6d7", size = 231741 },
+ { url = "https://files.pythonhosted.org/packages/df/99/6daddbea6ea49f9ddc83244a6dc6392fa5296aa9ae81c5f1a7b6c3b6745e/stringzilla-3.12.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:07d51f4c20cbc7d68f5291ef43c2c6b6b772f4be27adb6c9a6f895ad06004fd8", size = 234639 },
+ { url = "https://files.pythonhosted.org/packages/1b/99/106f0217542fb1a5552e27e04d7b6445662013edb9fffdef792304d14d14/stringzilla-3.12.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e914d237b4d1829974cbda70e70b058373fef6a5fef90e36953d531f5bdc1063", size = 206289 },
+ { url = "https://files.pythonhosted.org/packages/31/80/b20dea17f96344b4cf415bb00d6632ee2e7e7593a4c4d7dc3c75ee58a28d/stringzilla-3.12.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:32999c1c21eb799a97d0659b54cbdd01bfdd1b7ffc53a8c16183b5542e5052c9", size = 211874 },
+ { url = "https://files.pythonhosted.org/packages/08/93/40c342f2d9a0c5c574e72625358594b6b427bfd55757bc6626a80763a9d5/stringzilla-3.12.3-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e6e18d9604b7a083c3da5df8e1029c692e9b69e4f17e08b773cfbf8c6a141106", size = 308163 },
+ { url = "https://files.pythonhosted.org/packages/da/8b/62328c33ccffa4ec35d50c191156d3bb2b90347a6deb54f8b87e4294467b/stringzilla-3.12.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:cd8f488f281d8e198c28c77ef8638e062622d1c0ce18e9bee5380c0d848b248d", size = 226840 },
+ { url = "https://files.pythonhosted.org/packages/11/63/a85202a838044d14c8751ea717c8717cde73a6084f5ab9c489abbf8b5a71/stringzilla-3.12.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:4dd8cb6730f230a76ea623fc111372e06f07fdea3b27471ba1a8cf23e6751eda", size = 201765 },
+ { url = "https://files.pythonhosted.org/packages/58/c3/8c0d45bcb68383fd76678144e221930e2dae507999b987dd35982effb217/stringzilla-3.12.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5469ae13ffeb03fb30a99241af6f5584ee193c549d2635616ce7558415d13f22", size = 213369 },
+ { url = "https://files.pythonhosted.org/packages/41/71/e0ab1c7fc320affbac6119731d0e088fac91d188d5cbd12732c7e6da63fc/stringzilla-3.12.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:95f245d8e526fc25691329996a67b661acf0ea7baef9c3a402d555e32aa49863", size = 231815 },
+ { url = "https://files.pythonhosted.org/packages/79/a5/ff559e0da20b806790b52a234c8687ae75557965a12c3783addb1868c072/stringzilla-3.12.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:2b1884630696cf51ac2ea53867b629aa80e01dead3a47c021b7d643eb0595f68", size = 206909 },
+ { url = "https://files.pythonhosted.org/packages/03/fd/b5ff7c4431aa1a1ff3abd4a398e2ef2420ed99ce351c2eada2f3ab9e35d2/stringzilla-3.12.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5e492370b5de18ce68c96213544124221dc52f833477e177a4052ec704506e58", size = 302548 },
+ { url = "https://files.pythonhosted.org/packages/d8/f8/007f88311d3899f7f912f7afcb16af67ac2e987cc75b789181486ea552f5/stringzilla-3.12.3-cp312-cp312-win32.whl", hash = "sha256:5f20f8679ce88dad1eaae8effff8262694c65b359618e8ed6476a389eaf172a7", size = 68604 },
+ { url = "https://files.pythonhosted.org/packages/da/d9/cb7a4b496855eb34c94998a95c7ee4c6944c8ac8b9a184a708911f64395f/stringzilla-3.12.3-cp312-cp312-win_amd64.whl", hash = "sha256:291c024cc4365d6c0099d9ee7e61142392ab1315a6d4a8097e3b63af71d0d97c", size = 80104 },
+ { url = "https://files.pythonhosted.org/packages/d1/95/956441f983d27ab7fbc5fdd8bc64bc460b6ef121736a996f92a181ba168d/stringzilla-3.12.3-cp312-cp312-win_arm64.whl", hash = "sha256:3cf28d68273ba12ee970c682e67410516cdde087d207a2bb0cdd44ab2f533421", size = 69715 },
+ { url = "https://files.pythonhosted.org/packages/57/41/7e3aee11858fae9c0089384bccf06b1b36fecb91c2b0de0a8e9035c1d284/stringzilla-3.12.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:c29727cf895aef132ee7d1196bc04cc7900bbacf9ce08980c2557399cbb83222", size = 121861 },
+ { url = "https://files.pythonhosted.org/packages/6b/54/98ecf1abd6f0682cd5f589220bcdf0264c0e8384b5a22dd0a05b73940a1e/stringzilla-3.12.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:32631978e9fa79e9a579923a7c97a4603143c5fda5b63f9103507762604bd098", size = 79410 },
+ { url = "https://files.pythonhosted.org/packages/59/38/b8d537273ab497773e4d74fe1fab019919ef6d7c8f6a34f29c5692b0242a/stringzilla-3.12.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3df1a74419732a18a7885474c858bcb1270bdf5c35888d97d3b16c3646d539c5", size = 79367 },
+ { url = "https://files.pythonhosted.org/packages/31/76/8ac58cd611c5929aaa7b7c5533893d05ade167d32167aab07ceea8306fad/stringzilla-3.12.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9366d7a903f617472107767716502d4897a272512ead3a5e3c5e32d217f3a2e8", size = 231756 },
+ { url = "https://files.pythonhosted.org/packages/de/ae/a0910a1a11681a8a23712b1f85634502dcea0c9eae14e54c7c9c4f8e1e57/stringzilla-3.12.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0fcebbea3ea4fe58b2bb0dc85a6be705e7d2cc4746113a79940d8bc2755df87d", size = 234648 },
+ { url = "https://files.pythonhosted.org/packages/d4/c2/94b7b02e9e46d454e08a28ac72d4bf2d913b459b99aa2e3ec3d1c9a6f144/stringzilla-3.12.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e2a1219e4c79842893d6ace2e2875c879affdb14107877d3037e3373bedc8a56", size = 206322 },
+ { url = "https://files.pythonhosted.org/packages/d0/ee/d0e12dae2154e5c81944d610348a76f31a9dc508e1f49a49ebda8d6e529c/stringzilla-3.12.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7b14251c50ec73eb75ce83aa07673f68c4f187b53b8b868ebc0311f3012ee71b", size = 211933 },
+ { url = "https://files.pythonhosted.org/packages/b1/df/fd455997adf97e3b1a058be086b62233364fdc80f663d512ddcba6753ccd/stringzilla-3.12.3-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9cba1e18d1d167edf67fdfc5fa11ece06ec376ae55dd65401125d546e3d2150b", size = 308192 },
+ { url = "https://files.pythonhosted.org/packages/b5/a3/e4aff30b8e23c0ea0bcde89a664d4278295573da2abb03ec6f861c35ad72/stringzilla-3.12.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:43467714a52103f819ffbdef2d58aa39c938fcd42023ff6100a77bbb3f6cb398", size = 226850 },
+ { url = "https://files.pythonhosted.org/packages/80/9c/04e1ff235661513893665020f79318373b73bd33e889b88fbc5b0ceae22f/stringzilla-3.12.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:3d5c7fe1f2d04d595dd3fedf7776f8ab9819e4f4c681ae1f0fb3e28bb29247b3", size = 201778 },
+ { url = "https://files.pythonhosted.org/packages/f2/9f/71a37e5275d8b5bd76ec1f7af8784d08603409976dfd40ae8c517a02b9d8/stringzilla-3.12.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:bbd85919eaf7eb01ca25304dfe085b21d6db693499cd25da0a915d348ec42c38", size = 213413 },
+ { url = "https://files.pythonhosted.org/packages/02/87/990541a198a58afa9042e222a23dd31e6f224376426dd1155fb4a831a3b3/stringzilla-3.12.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:57867d712049b8222311e32fd93ebfd60864b48b35aefa860788f6eafba61bc2", size = 231873 },
+ { url = "https://files.pythonhosted.org/packages/20/30/e83f78332db41a76df8e50c442ddc8c462ffdf087ac1bf2344aa041c54e0/stringzilla-3.12.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:1db2e8092c72ca7750d7b5a0d367a80efb8d3831a7ea60deeb5885f301aac035", size = 206899 },
+ { url = "https://files.pythonhosted.org/packages/cd/da/8ffe2ebe2978eeb8be114412a89a67bdb30c660016ba5528770a704709f3/stringzilla-3.12.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:793b8acfa4f1dae7d070742b69f423d8d073643eaa56eb89078e781d031caada", size = 302585 },
+ { url = "https://files.pythonhosted.org/packages/4e/4b/543f62ee7acad1e9d421cf6880cae6edcd0dc043900d9bc4ae0ca9f8bc64/stringzilla-3.12.3-cp313-cp313-win32.whl", hash = "sha256:7af63431268f018af41b15adeab7a732585f397a0941adaf5d2fc624f1f3a790", size = 68606 },
+ { url = "https://files.pythonhosted.org/packages/24/70/c4e2d998924a95167dbfbbcdc1af2cd90093c4e5097af08f9b272e8957ba/stringzilla-3.12.3-cp313-cp313-win_amd64.whl", hash = "sha256:0a3dcd565d833e7c5814eeba2ebfcbf9d06a4ae32467423d4071702c1084e74a", size = 80110 },
+ { url = "https://files.pythonhosted.org/packages/28/74/54979574d63fccd9f1b0e0420f0700aa41e7e3cdbfc6d78734a5ea27f249/stringzilla-3.12.3-cp313-cp313-win_arm64.whl", hash = "sha256:bcce4ed759cee812d5fcec3b87eafa9996641483cbc1b0f81006ca15bf6a16b6", size = 69718 },
+]
+
+[[package]]
+name = "sympy"
+version = "1.13.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "mpmath" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/ca/99/5a5b6f19ff9f083671ddf7b9632028436167cd3d33e11015754e41b249a4/sympy-1.13.1.tar.gz", hash = "sha256:9cebf7e04ff162015ce31c9c6c9144daa34a93bd082f54fd8f12deca4f47515f", size = 7533040 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b2/fe/81695a1aa331a842b582453b605175f419fe8540355886031328089d840a/sympy-1.13.1-py3-none-any.whl", hash = "sha256:db36cdc64bf61b9b24578b6f7bab1ecdd2452cf008f34faa33776680c26d66f8", size = 6189177 },
+]
+
+[[package]]
+name = "timm"
+version = "1.0.15"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "huggingface-hub" },
+ { name = "pyyaml" },
+ { name = "safetensors" },
+ { name = "torch" },
+ { name = "torchvision" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/bc/0c/66b0f9b4a4cb9ffdac7b52b17b37c7d3c4f75623b469e388b0c6d89b4e88/timm-1.0.15.tar.gz", hash = "sha256:756a3bc30c96565f056e608a9b559daed904617eaadb6be536f96874879b1055", size = 2230258 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/6c/d0/179abca8b984b3deefd996f362b612c39da73b60f685921e6cd58b6125b4/timm-1.0.15-py3-none-any.whl", hash = "sha256:5a3dc460c24e322ecc7fd1f3e3eb112423ddee320cb059cc1956fbc9731748ef", size = 2361373 },
+]
+
+[[package]]
+name = "torch"
+version = "2.6.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "filelock" },
+ { name = "fsspec" },
+ { name = "jinja2" },
+ { name = "networkx" },
+ { name = "nvidia-cublas-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
+ { name = "nvidia-cuda-cupti-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
+ { name = "nvidia-cuda-nvrtc-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
+ { name = "nvidia-cuda-runtime-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
+ { name = "nvidia-cudnn-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
+ { name = "nvidia-cufft-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
+ { name = "nvidia-curand-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
+ { name = "nvidia-cusolver-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
+ { name = "nvidia-cusparse-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
+ { name = "nvidia-cusparselt-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
+ { name = "nvidia-nccl-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
+ { name = "nvidia-nvjitlink-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
+ { name = "nvidia-nvtx-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
+ { name = "setuptools", marker = "python_full_version >= '3.12'" },
+ { name = "sympy" },
+ { name = "triton", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
+ { name = "typing-extensions" },
+]
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/37/81/aa9ab58ec10264c1abe62c8b73f5086c3c558885d6beecebf699f0dbeaeb/torch-2.6.0-cp310-cp310-manylinux1_x86_64.whl", hash = "sha256:6860df13d9911ac158f4c44031609700e1eba07916fff62e21e6ffa0a9e01961", size = 766685561 },
+ { url = "https://files.pythonhosted.org/packages/86/86/e661e229df2f5bfc6eab4c97deb1286d598bbeff31ab0cdb99b3c0d53c6f/torch-2.6.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:c4f103a49830ce4c7561ef4434cc7926e5a5fe4e5eb100c19ab36ea1e2b634ab", size = 95751887 },
+ { url = "https://files.pythonhosted.org/packages/20/e0/5cb2f8493571f0a5a7273cd7078f191ac252a402b5fb9cb6091f14879109/torch-2.6.0-cp310-cp310-win_amd64.whl", hash = "sha256:56eeaf2ecac90da5d9e35f7f35eb286da82673ec3c582e310a8d1631a1c02341", size = 204165139 },
+ { url = "https://files.pythonhosted.org/packages/e5/16/ea1b7842413a7b8a5aaa5e99e8eaf3da3183cc3ab345ad025a07ff636301/torch-2.6.0-cp310-none-macosx_11_0_arm64.whl", hash = "sha256:09e06f9949e1a0518c5b09fe95295bc9661f219d9ecb6f9893e5123e10696628", size = 66520221 },
+ { url = "https://files.pythonhosted.org/packages/78/a9/97cbbc97002fff0de394a2da2cdfa859481fdca36996d7bd845d50aa9d8d/torch-2.6.0-cp311-cp311-manylinux1_x86_64.whl", hash = "sha256:7979834102cd5b7a43cc64e87f2f3b14bd0e1458f06e9f88ffa386d07c7446e1", size = 766715424 },
+ { url = "https://files.pythonhosted.org/packages/6d/fa/134ce8f8a7ea07f09588c9cc2cea0d69249efab977707cf67669431dcf5c/torch-2.6.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:ccbd0320411fe1a3b3fec7b4d3185aa7d0c52adac94480ab024b5c8f74a0bf1d", size = 95759416 },
+ { url = "https://files.pythonhosted.org/packages/11/c5/2370d96b31eb1841c3a0883a492c15278a6718ccad61bb6a649c80d1d9eb/torch-2.6.0-cp311-cp311-win_amd64.whl", hash = "sha256:46763dcb051180ce1ed23d1891d9b1598e07d051ce4c9d14307029809c4d64f7", size = 204164970 },
+ { url = "https://files.pythonhosted.org/packages/0b/fa/f33a4148c6fb46ca2a3f8de39c24d473822d5774d652b66ed9b1214da5f7/torch-2.6.0-cp311-none-macosx_11_0_arm64.whl", hash = "sha256:94fc63b3b4bedd327af588696559f68c264440e2503cc9e6954019473d74ae21", size = 66530713 },
+ { url = "https://files.pythonhosted.org/packages/e5/35/0c52d708144c2deb595cd22819a609f78fdd699b95ff6f0ebcd456e3c7c1/torch-2.6.0-cp312-cp312-manylinux1_x86_64.whl", hash = "sha256:2bb8987f3bb1ef2675897034402373ddfc8f5ef0e156e2d8cfc47cacafdda4a9", size = 766624563 },
+ { url = "https://files.pythonhosted.org/packages/01/d6/455ab3fbb2c61c71c8842753b566012e1ed111e7a4c82e0e1c20d0c76b62/torch-2.6.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:b789069020c5588c70d5c2158ac0aa23fd24a028f34a8b4fcb8fcb4d7efcf5fb", size = 95607867 },
+ { url = "https://files.pythonhosted.org/packages/18/cf/ae99bd066571656185be0d88ee70abc58467b76f2f7c8bfeb48735a71fe6/torch-2.6.0-cp312-cp312-win_amd64.whl", hash = "sha256:7e1448426d0ba3620408218b50aa6ada88aeae34f7a239ba5431f6c8774b1239", size = 204120469 },
+ { url = "https://files.pythonhosted.org/packages/81/b4/605ae4173aa37fb5aa14605d100ff31f4f5d49f617928c9f486bb3aaec08/torch-2.6.0-cp312-none-macosx_11_0_arm64.whl", hash = "sha256:9a610afe216a85a8b9bc9f8365ed561535c93e804c2a317ef7fabcc5deda0989", size = 66532538 },
+ { url = "https://files.pythonhosted.org/packages/24/85/ead1349fc30fe5a32cadd947c91bda4a62fbfd7f8c34ee61f6398d38fb48/torch-2.6.0-cp313-cp313-manylinux1_x86_64.whl", hash = "sha256:4874a73507a300a5d089ceaff616a569e7bb7c613c56f37f63ec3ffac65259cf", size = 766626191 },
+ { url = "https://files.pythonhosted.org/packages/dd/b0/26f06f9428b250d856f6d512413e9e800b78625f63801cbba13957432036/torch-2.6.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:a0d5e1b9874c1a6c25556840ab8920569a7a4137afa8a63a32cee0bc7d89bd4b", size = 95611439 },
+ { url = "https://files.pythonhosted.org/packages/c2/9c/fc5224e9770c83faed3a087112d73147cd7c7bfb7557dcf9ad87e1dda163/torch-2.6.0-cp313-cp313-win_amd64.whl", hash = "sha256:510c73251bee9ba02ae1cb6c9d4ee0907b3ce6020e62784e2d7598e0cfa4d6cc", size = 204126475 },
+ { url = "https://files.pythonhosted.org/packages/88/8b/d60c0491ab63634763be1537ad488694d316ddc4a20eaadd639cedc53971/torch-2.6.0-cp313-none-macosx_11_0_arm64.whl", hash = "sha256:ff96f4038f8af9f7ec4231710ed4549da1bdebad95923953a25045dcf6fd87e2", size = 66536783 },
+]
+
+[[package]]
+name = "torchvision"
+version = "0.21.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "numpy" },
+ { name = "pillow" },
+ { name = "torch" },
+]
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/8e/0d/143bd264876fad17c82096b6c2d433f1ac9b29cdc69ee45023096976ee3d/torchvision-0.21.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:044ea420b8c6c3162a234cada8e2025b9076fa82504758cd11ec5d0f8cd9fa37", size = 1784140 },
+ { url = "https://files.pythonhosted.org/packages/5e/44/32e2d2d174391374d5ff3c4691b802e8efda9ae27ab9062eca2255b006af/torchvision-0.21.0-cp310-cp310-manylinux1_x86_64.whl", hash = "sha256:b0c0b264b89ab572888244f2e0bad5b7eaf5b696068fc0b93e96f7c3c198953f", size = 7237187 },
+ { url = "https://files.pythonhosted.org/packages/0e/6b/4fca9373eda42c1b04096758306b7bd55f7d8f78ba273446490855a0f25d/torchvision-0.21.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:54815e0a56dde95cc6ec952577f67e0dc151eadd928e8d9f6a7f821d69a4a734", size = 14699067 },
+ { url = "https://files.pythonhosted.org/packages/aa/f7/799ddd538b21017cbf80294c92e9efbf6db08dff6efee37c3be114a81845/torchvision-0.21.0-cp310-cp310-win_amd64.whl", hash = "sha256:abbf1d7b9d52c00d2af4afa8dac1fb3e2356f662a4566bd98dfaaa3634f4eb34", size = 1560542 },
+ { url = "https://files.pythonhosted.org/packages/29/88/00c69db213ee2443ada8886ec60789b227e06bb869d85ee324578221a7f7/torchvision-0.21.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:110d115333524d60e9e474d53c7d20f096dbd8a080232f88dddb90566f90064c", size = 1784141 },
+ { url = "https://files.pythonhosted.org/packages/be/a2/b0cedf0a411f1a5d75cfc0b87cde56dd1ddc1878be46a42c905cd8580220/torchvision-0.21.0-cp311-cp311-manylinux1_x86_64.whl", hash = "sha256:3891cd086c5071bda6b4ee9d266bb2ac39c998c045c2ebcd1e818b8316fb5d41", size = 7237719 },
+ { url = "https://files.pythonhosted.org/packages/8c/a1/ee962ef9d0b2bf7a6f8b14cb95acb70e05cd2101af521032a09e43f8582f/torchvision-0.21.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:54454923a50104c66a9ab6bd8b73a11c2fc218c964b1006d5d1fe5b442c3dcb6", size = 14700617 },
+ { url = "https://files.pythonhosted.org/packages/88/53/4ad334b9b1d8dd99836869fec139cb74a27781298360b91b9506c53f1d10/torchvision-0.21.0-cp311-cp311-win_amd64.whl", hash = "sha256:49bcfad8cfe2c27dee116c45d4f866d7974bcf14a5a9fbef893635deae322f2f", size = 1560523 },
+ { url = "https://files.pythonhosted.org/packages/6e/1b/28f527b22d5e8800184d0bc847f801ae92c7573a8c15979d92b7091c0751/torchvision-0.21.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:97a5814a93c793aaf0179cfc7f916024f4b63218929aee977b645633d074a49f", size = 1784140 },
+ { url = "https://files.pythonhosted.org/packages/36/63/0722e153fd27d64d5b0af45b5c8cb0e80b35a68cf0130303bc9a8bb095c7/torchvision-0.21.0-cp312-cp312-manylinux1_x86_64.whl", hash = "sha256:b578bcad8a4083b40d34f689b19ca9f7c63e511758d806510ea03c29ac568f7b", size = 7238673 },
+ { url = "https://files.pythonhosted.org/packages/bb/ea/03541ed901cdc30b934f897060d09bbf7a98466a08ad1680320f9ce0cbe0/torchvision-0.21.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:5083a5b1fec2351bf5ea9900a741d54086db75baec4b1d21e39451e00977f1b1", size = 14701186 },
+ { url = "https://files.pythonhosted.org/packages/4c/6a/c7752603060d076dfed95135b78b047dc71792630cbcb022e3693d6f32ef/torchvision-0.21.0-cp312-cp312-win_amd64.whl", hash = "sha256:6eb75d41e3bbfc2f7642d0abba9383cc9ae6c5a4ca8d6b00628c225e1eaa63b3", size = 1560520 },
+ { url = "https://files.pythonhosted.org/packages/f9/56/47d456b61c3bbce7bed4af3925c83d405bb87468e659fd3cf3d9840c3b51/torchvision-0.21.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:659b76c86757cb2ee4ca2db245e0740cfc3081fef46f0f1064d11adb4a8cee31", size = 1784141 },
+ { url = "https://files.pythonhosted.org/packages/cb/4c/99880813aa50e64447fb1c4c6c804a793d2d78f7f7c53e99ddee7fa175fa/torchvision-0.21.0-cp313-cp313-manylinux1_x86_64.whl", hash = "sha256:084ac3f5a1f50c70d630a488d19bf62f323018eae1b1c1232f2b7047d3a7b76d", size = 7238714 },
+ { url = "https://files.pythonhosted.org/packages/0b/2d/3c3ee10608310a395594aac7da8640372ed79c6585910ccae6919658dcdc/torchvision-0.21.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:5045a3a5f21ec3eea6962fa5f2fa2d4283f854caec25ada493fcf4aab2925467", size = 2281252 },
+ { url = "https://files.pythonhosted.org/packages/ed/b4/fc60e3bc003879d3de842baea258fffc3586f4b49cd435a5ba1e09c33315/torchvision-0.21.0-cp313-cp313-win_amd64.whl", hash = "sha256:9147f5e096a9270684e3befdee350f3cacafd48e0c54ab195f45790a9c146d67", size = 1560519 },
+]
+
+[[package]]
+name = "tqdm"
+version = "4.67.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "colorama", marker = "sys_platform == 'win32'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/a8/4b/29b4ef32e036bb34e4ab51796dd745cdba7ed47ad142a9f4a1eb8e0c744d/tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2", size = 169737 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540 },
+]
+
+[[package]]
+name = "triton"
+version = "3.2.0"
+source = { registry = "https://pypi.org/simple" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/01/65/3ffa90e158a2c82f0716eee8d26a725d241549b7d7aaf7e4f44ac03ebd89/triton-3.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b3e54983cd51875855da7c68ec05c05cf8bb08df361b1d5b69e05e40b0c9bd62", size = 253090354 },
+ { url = "https://files.pythonhosted.org/packages/a7/2e/757d2280d4fefe7d33af7615124e7e298ae7b8e3bc4446cdb8e88b0f9bab/triton-3.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8009a1fb093ee8546495e96731336a33fb8856a38e45bb4ab6affd6dbc3ba220", size = 253157636 },
+ { url = "https://files.pythonhosted.org/packages/06/00/59500052cb1cf8cf5316be93598946bc451f14072c6ff256904428eaf03c/triton-3.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d9b215efc1c26fa7eefb9a157915c92d52e000d2bf83e5f69704047e63f125c", size = 253159365 },
+ { url = "https://files.pythonhosted.org/packages/c7/30/37a3384d1e2e9320331baca41e835e90a3767303642c7a80d4510152cbcf/triton-3.2.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e5dfa23ba84541d7c0a531dfce76d8bcd19159d50a4a8b14ad01e91734a5c1b0", size = 253154278 },
+]
+
+[[package]]
+name = "typing-extensions"
+version = "4.12.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 },
+]
+
+[[package]]
+name = "ubelt"
+version = "1.3.7"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/14/5f/6a36942971b379c254e4cc9970b40ec1eb0a3ccebdc980c070eaf47a1dbe/ubelt-1.3.7.tar.gz", hash = "sha256:41837abc852ce01bbaea8eb3da15f49c4c0d68340f546c6e2ee00fbc5be75f39", size = 298930 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/85/60/be60c12ffea9d9d5220097e2490c714c10f2030aea5b4bace56cdd4d8698/ubelt-1.3.7-py3-none-any.whl", hash = "sha256:a16203fc5c5b311c11b26e2b22c361fc1f45e449f87a16665705e33b5f35b5a9", size = 232975 },
+]
+
+[[package]]
+name = "urllib3"
+version = "2.3.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/aa/63/e53da845320b757bf29ef6a9062f5c669fe997973f966045cb019c3f4b66/urllib3-2.3.0.tar.gz", hash = "sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d", size = 307268 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c8/19/4ec628951a74043532ca2cf5d97b7b14863931476d117c471e8e2b1eb39f/urllib3-2.3.0-py3-none-any.whl", hash = "sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df", size = 128369 },
+]
+
+[[package]]
+name = "wandb"
+version = "0.19.8"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "click" },
+ { name = "docker-pycreds" },
+ { name = "gitpython" },
+ { name = "platformdirs" },
+ { name = "protobuf" },
+ { name = "psutil" },
+ { name = "pydantic" },
+ { name = "pyyaml" },
+ { name = "requests" },
+ { name = "sentry-sdk" },
+ { name = "setproctitle" },
+ { name = "setuptools" },
+ { name = "typing-extensions", marker = "python_full_version < '3.12'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/22/f2/001aee271c0665afc7424c14ea2fa6fd9987d9d4e186d187cd0bac2d11db/wandb-0.19.8.tar.gz", hash = "sha256:3a4844bb38758657b94b090e72ee355fe5b926e3a048232f0ca4248f801d8d80", size = 39244743 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d4/79/058be304cddf78e53ebaddeefbfeec66c3d67d6f733653f9f7de48efcfe0/wandb-0.19.8-py3-none-any.whl", hash = "sha256:75dea834d579f38e0e1f857e644020e22c851f9b920e9c6c6345bacb98c3f3fc", size = 6305883 },
+ { url = "https://files.pythonhosted.org/packages/3c/df/e8e0ec80afd0a437e3ddc10da3e2286d9bab2169b48fd0f768a455d49971/wandb-0.19.8-py3-none-macosx_10_14_x86_64.whl", hash = "sha256:6556147ba33b7ff4a0111bb6bf5ea485e4974c22f520f1e2a5eaad670a058c80", size = 20474304 },
+ { url = "https://files.pythonhosted.org/packages/9a/6e/171701d80f0f20e53c74e8e0ecab06c31a59d53cab295ec108ac39140fef/wandb-0.19.8-py3-none-macosx_11_0_arm64.whl", hash = "sha256:f68517c2059d12912a90ae32ce95a2711e39f6c157c759eb191527739a12db8b", size = 19942528 },
+ { url = "https://files.pythonhosted.org/packages/59/24/24720683f6b9c19dd41b081e32d4585dc9a2f1e2d0b7a9cb63cde690868e/wandb-0.19.8-py3-none-macosx_11_0_x86_64.whl", hash = "sha256:96cb534b19c2d301ac4fb0e7cfbc32198a704e29e87337133d6b71fdad33cf2f", size = 20471015 },
+ { url = "https://files.pythonhosted.org/packages/22/0a/a9f6dcc96a6ee7cd5365af3a8e4b896cd373e4a11cbb1468b6d9aaac37f3/wandb-0.19.8-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1781b36434d494d6b34e2149201bae8cab960cb31571f11b981c4a62462d5af8", size = 19460731 },
+ { url = "https://files.pythonhosted.org/packages/e0/71/7b7050ecab7288782ae0c7560f1ca06f4cf854a5ae08abeaf643785af1a0/wandb-0.19.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c25f0e40025b838b7a424b51837a2a5fd071686c59e1c46d73f04e760d305f79", size = 20792273 },
+ { url = "https://files.pythonhosted.org/packages/45/54/8b6f1f41cf4a8b67439d4f0842de80084709cad2939152503046b42d863c/wandb-0.19.8-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:068eb0154f80be973ab291346d831e9cc80a9de1b8752bdeb48a997c3506fec4", size = 19470793 },
+ { url = "https://files.pythonhosted.org/packages/d7/bb/28d94b0369f0055dc4aef704971858a414490f6eb23b9bbfa70d090f4b59/wandb-0.19.8-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:82a956150e53df0b4c193933b3e62c3e8255dc8b43bb187270939ef35b03fda3", size = 20872380 },
+ { url = "https://files.pythonhosted.org/packages/80/82/9d653fe043d48075342bed7a545611391fc62095fb1e77d6574a8f2091e3/wandb-0.19.8-py3-none-win32.whl", hash = "sha256:9d71f153cb9330e307b1b054be01971a1bd164fb9bd4190d7f57989c2d6b86e8", size = 20165481 },
+ { url = "https://files.pythonhosted.org/packages/b6/90/038a64abcbe5f991468f057bd21bead84a5c39d9b0409b652893263a47b4/wandb-0.19.8-py3-none-win_amd64.whl", hash = "sha256:f7da8e6fc6693014c72fb7db3ecd5e1116066198d2aca96f6eb7220cea03081c", size = 20165486 },
+]
+
+[[package]]
+name = "win32-setctime"
+version = "1.2.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/b3/8f/705086c9d734d3b663af0e9bb3d4de6578d08f46b1b101c2442fd9aecaa2/win32_setctime-1.2.0.tar.gz", hash = "sha256:ae1fdf948f5640aae05c511ade119313fb6a30d7eabe25fef9764dca5873c4c0", size = 4867 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e1/07/c6fe3ad3e685340704d314d765b7912993bcb8dc198f0e7a89382d37974b/win32_setctime-1.2.0-py3-none-any.whl", hash = "sha256:95d644c4e708aba81dc3704a116d8cbc974d70b3bdb8be1d150e36be6e9d1390", size = 4083 },
+]