From b1acd8caeecbbe218ebfac47c2356be8f5f12001 Mon Sep 17 00:00:00 2001 From: Sergiy Popovich Date: Thu, 19 Mar 2026 09:39:48 -0400 Subject: [PATCH 1/6] Update internal submodule to latest main (downsample_factor + rigidity weights) --- zetta_utils/internal | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zetta_utils/internal b/zetta_utils/internal index bb0e1032c..67bca2d6e 160000 --- a/zetta_utils/internal +++ b/zetta_utils/internal @@ -1 +1 @@ -Subproject commit bb0e1032ce81467db17d786d7fcefbeb41434cc5 +Subproject commit 67bca2d6edf1a8f86d7032420c7af23b61dd011d From 5028b35d7587980dd1366afa70e75745a1cef5c4 Mon Sep 17 00:00:00 2001 From: Sergiy Popovich Date: Thu, 19 Mar 2026 10:00:54 -0400 Subject: [PATCH 2/6] Add misd post-processing and update internal submodule --- web_api/app/alignment.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/web_api/app/alignment.py b/web_api/app/alignment.py index ec3350cfb..fb00eb13b 100644 --- a/web_api/app/alignment.py +++ b/web_api/app/alignment.py @@ -13,6 +13,8 @@ from fastapi import FastAPI, HTTPException, Request from fastapi.responses import Response, StreamingResponse from pydantic import BaseModel, Field +from scipy.ndimage import binary_closing +from scipy.ndimage import label as scipy_label from zetta_utils.internal.alignment.manual_correspondence import ( apply_correspondences_to_image, @@ -94,6 +96,11 @@ class ApplyCorrespondencesRequest(BaseModel): description="Weight for MSE loss between warped source and target images. " "Only used when tgt_image is provided.", ) + downsample_factor: int = Field( + 16, + description="Multi-scale downsampling factor for field propagation. " + "1 = no downsampling. 8 or 16 for faster initialization.", + ) class ApplyCorrespondencesResponse(BaseModel): @@ -154,6 +161,7 @@ def _parse_json_request(body: dict, device: torch.device): "lr": req.lr, "optimizer_type": req.optimizer_type, "mse_weight": req.mse_weight, + "downsample_factor": req.downsample_factor, } return ( correspondences_dict, @@ -277,6 +285,7 @@ async def _parse_multipart_request(request: Request, device: torch.device): "lr": metadata.get("lr", 1e-3), "optimizer_type": metadata.get("optimizer_type", "adam"), "mse_weight": metadata.get("mse_weight", 1.0), + "downsample_factor": metadata.get("downsample_factor", 16), } return ( correspondences_dict, @@ -370,6 +379,26 @@ def _run_misd_detection(warped_image, tgt_image_tensor, input_dtype): dim=0, keepdim=True ) misd_mask[zero_mask] = 0 + + structure = np.ones((3, 3), dtype=bool) + misd_np = misd_mask.cpu().numpy() # (1, X, Y, Z) + out_np = np.zeros_like(misd_np) + for z in range(misd_np.shape[3]): + sl = misd_np[0, :, :, z] > 128 + if not sl.any(): + continue + sl = binary_closing(sl, structure=structure, iterations=1) + if not sl.any(): + continue + labels, _ = scipy_label(sl) + ids, counts = np.unique(labels, return_counts=True) + keep_mask = np.zeros_like(sl) + for seg_id, count in zip(ids, counts): + if seg_id != 0 and count >= 100: + keep_mask |= labels == seg_id + out_np[0, :, :, z][keep_mask] = 255 + misd_mask = torch.from_numpy(out_np).to(misd_mask.device) + print( f"[apply_correspondences] misd uint8: " f"min={misd_mask.min().item()} " From 8015778d4e583c2b40e7c25f916f0901339ba70d Mon Sep 17 00:00:00 2001 From: Sergiy Popovich Date: Thu, 19 Mar 2026 20:09:23 -0400 Subject: [PATCH 3/6] Increase misd connected component filter from 100 to 300 pixels Co-Authored-By: Claude Opus 4.6 (1M context) --- web_api/app/alignment.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web_api/app/alignment.py b/web_api/app/alignment.py index fb00eb13b..9c5652dd7 100644 --- a/web_api/app/alignment.py +++ b/web_api/app/alignment.py @@ -394,7 +394,7 @@ def _run_misd_detection(warped_image, tgt_image_tensor, input_dtype): ids, counts = np.unique(labels, return_counts=True) keep_mask = np.zeros_like(sl) for seg_id, count in zip(ids, counts): - if seg_id != 0 and count >= 100: + if seg_id != 0 and count >= 300: keep_mask |= labels == seg_id out_np[0, :, :, z][keep_mask] = 255 misd_mask = torch.from_numpy(out_np).to(misd_mask.device) From 993aca3ab510517d1de5867231f146b3e440a3d7 Mon Sep 17 00:00:00 2001 From: Sergiy Popovich Date: Fri, 20 Mar 2026 09:11:21 -0400 Subject: [PATCH 4/6] Increase misd connected component filter from 300 to 600 pixels Co-Authored-By: Claude Opus 4.6 (1M context) --- web_api/app/alignment.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web_api/app/alignment.py b/web_api/app/alignment.py index 9c5652dd7..c31c8cb09 100644 --- a/web_api/app/alignment.py +++ b/web_api/app/alignment.py @@ -394,7 +394,7 @@ def _run_misd_detection(warped_image, tgt_image_tensor, input_dtype): ids, counts = np.unique(labels, return_counts=True) keep_mask = np.zeros_like(sl) for seg_id, count in zip(ids, counts): - if seg_id != 0 and count >= 300: + if seg_id != 0 and count >= 600: keep_mask |= labels == seg_id out_np[0, :, :, z][keep_mask] = 255 misd_mask = torch.from_numpy(out_np).to(misd_mask.device) From 193b66800e70e5c89011d54f05b12cd924dda212 Mon Sep 17 00:00:00 2001 From: Sergiy Popovich Date: Fri, 20 Mar 2026 17:46:50 -0400 Subject: [PATCH 5/6] SIFT filtering improvements and existing correspondences API - Accept existing_correspondences in auto-detect endpoint - Spatial consistency filter for local outlier rejection - Min distance filter (32px) from existing correspondences - Existing-aware farthest-point sampling - Ratio test default 0.7, RANSAC enabled with threshold 5 Co-Authored-By: Claude Opus 4.6 (1M context) --- web_api/app/alignment.py | 35 +++++++++++++++++++++++++++++------ zetta_utils/internal | 2 +- 2 files changed, 30 insertions(+), 7 deletions(-) diff --git a/web_api/app/alignment.py b/web_api/app/alignment.py index c31c8cb09..9de0d5965 100644 --- a/web_api/app/alignment.py +++ b/web_api/app/alignment.py @@ -544,6 +544,11 @@ def _run_computation(): return _build_json_response(relaxed_field_np, warped_image_np, misd_image_np) +class ExistingCorrespondenceItem(BaseModel): + start: list[float] = Field(..., description="Start point [y, x]") + end: list[float] = Field(..., description="End point [y, x]") + + class ComputeSiftCorrespondencesRequest(BaseModel): src_image: str = Field(..., description="Base64-encoded uint8 image bytes") tgt_image: str = Field(..., description="Base64-encoded uint8 image bytes") @@ -565,6 +570,11 @@ class ComputeSiftCorrespondencesRequest(BaseModel): True, description="If true, output [y, x] (Portal convention). If false, output [x, y].", ) + existing_correspondences: list[ExistingCorrespondenceItem] | None = Field( + None, + description="Existing correspondence arrows for deduplication/refiltering. " + "Each item has start [y, x] and end [y, x] in pixel coords.", + ) class ComputeSiftCorrespondencesResponse(BaseModel): @@ -593,8 +603,8 @@ class ComputeSiftCorrespondencesResponse(BaseModel): "edge_threshold": 10, "sigma": 1.6, "ratio_test_fraction": 0.7, - "ransac_threshold": 3.0, - "use_ransac": False, + "ransac_threshold": 5.0, + "use_ransac": True, "spatial_weight": 0.7, "swap_xy": True, } @@ -602,9 +612,10 @@ class ComputeSiftCorrespondencesResponse(BaseModel): async def _parse_sift_request( request: Request, use_ransac_default: bool -) -> tuple[np.ndarray, np.ndarray, dict]: +) -> tuple[np.ndarray, np.ndarray, dict, list[dict] | None]: content_type = request.headers.get("content-type", "") defaults = {**_SIFT_PARAM_DEFAULTS, "use_ransac": use_ransac_default} + existing_correspondences = None if "multipart/form-data" in content_type: form = await request.form() @@ -636,6 +647,7 @@ async def _parse_sift_request( tgt_image = np.frombuffer(tgt_bytes, dtype=np.uint8).reshape(metadata["tgt_image_shape"]) sift_params = {k: metadata.get(k, defaults[k]) for k in _SIFT_PARAM_KEYS} + existing_correspondences = metadata.get("existing_correspondences") else: body = await request.json() req = ComputeSiftCorrespondencesRequest( @@ -650,16 +662,27 @@ async def _parse_sift_request( ) sift_params = {k: getattr(req, k) for k in _SIFT_PARAM_KEYS} + if req.existing_correspondences is not None: + existing_correspondences = [ + {"start": ec.start, "end": ec.end} for ec in req.existing_correspondences + ] - return src_image, tgt_image, sift_params + return src_image, tgt_image, sift_params, existing_correspondences async def _run_sift_correspondences( request: Request, use_ransac_default: bool ) -> ComputeSiftCorrespondencesResponse: - src_image, tgt_image, sift_params = await _parse_sift_request(request, use_ransac_default) + src_image, tgt_image, sift_params, existing_correspondences = await _parse_sift_request( + request, use_ransac_default + ) - result = compute_sift_correspondences(src=src_image, tgt=tgt_image, **sift_params) + result = compute_sift_correspondences( + src=src_image, + tgt=tgt_image, + existing_correspondences=existing_correspondences, + **sift_params, + ) return ComputeSiftCorrespondencesResponse( lines=[CorrespondenceLine(**line) for line in result["lines"]], diff --git a/zetta_utils/internal b/zetta_utils/internal index 67bca2d6e..e5f72d9bb 160000 --- a/zetta_utils/internal +++ b/zetta_utils/internal @@ -1 +1 @@ -Subproject commit 67bca2d6edf1a8f86d7032420c7af23b61dd011d +Subproject commit e5f72d9bbb47449fe0c4d349490d1aef7ad3bd78 From 81e50ad892dcb9d0c9e16c33f4783ef0b204f293 Mon Sep 17 00:00:00 2001 From: Sergiy Popovich Date: Fri, 20 Mar 2026 20:32:33 -0400 Subject: [PATCH 6/6] Update internal submodule to latest main (sift filtering merged) --- zetta_utils/internal | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zetta_utils/internal b/zetta_utils/internal index e5f72d9bb..67fd231e7 160000 --- a/zetta_utils/internal +++ b/zetta_utils/internal @@ -1 +1 @@ -Subproject commit e5f72d9bbb47449fe0c4d349490d1aef7ad3bd78 +Subproject commit 67fd231e7f6c9f0e1fdacd249efd488b4c5ca40f