@@ -294,7 +294,10 @@ def run_reconstruction(
294294 pipeline_name : str = "default_pipeline" ,
295295 input_resize_width : int = 3024 ,
296296 use_masks : bool = True ,
297+ matching_mode : str = 'exhaustive' ,
297298 window_radius : int | None = None ,
299+ num_neighbors : int | None = None ,
300+ locations : List [Tuple [float , float , float ]] | None = None ,
298301 return_durations : bool = False ,
299302 progress_callback : Callable [[int ,str ],None ] | None = None ,
300303) -> Tuple [Photoscan , Path ] | Tuple [Photoscan , Path , Dict [str , float ]]:
@@ -305,8 +308,17 @@ def run_reconstruction(
305308 See also `get_meshroom_pipeline_names`.
306309 input_resize_width (int): Width to which input images will be resized, in pixels.
307310 use_masks (bool): Whether to include a background removal step to filter the dense reconstruction.
308- window_radius (Optional[int]): number of images forward and backward in the sequence to try and
309- match with, if None match each images to all others.
311+ matching_mode (str): Strategy for generating image pairs. One of:
312+ - 'exhaustive': Match every image with every other image.
313+ - 'sequential': Match each image with the previous and next `window_radius` images (no wrap-around).
314+ - 'sequential_loop': Like 'sequential' but wraps around at the end of the sequence.
315+ - 'spatial': Match each image with its `num_neighbors` nearest neighbors based on 3D location.
316+ window_radius (int | None): Required for 'sequential' and 'sequential_loop' matching_mode. Number of
317+ images forward and backward in the sequence to try and match with.
318+ num_neighbors (int | None): Required for 'spatial' matching_mode. Number of nearest neighbors to match
319+ based on 3D distance in `locations`.
320+ locations (List[Tuple[float, float, float]]] | None): Required for 'spatial'. Must be the same length
321+ as `images`. Provides 3D coordinates for spatial matching.
310322 return_durations (bool): If True, also return a dictionary mapping node names to durations in seconds.
311323 progress_callback: An optional function that will be called to report progress. The function should accept two arguments:
312324 an integer progress value from 0 to 100 followed by a string message describing the step currently being worked on.
@@ -316,6 +328,32 @@ def run_reconstruction(
316328 - If return_durations is False: returns the Photoscan and the data directory path.
317329 - If return_durations is True: also returns a dictionary of node execution times.
318330 """
331+ image_indices = list (range (len (images )))
332+ valid_modes = {'exhaustive' , 'sequential' , 'sequential_loop' , 'spatial' }
333+ if matching_mode == 'exhaustive' :
334+ pairs = _make_pairs_sequential (image_indices , len (image_indices ))
335+
336+ elif matching_mode == 'sequential' :
337+ if window_radius is None :
338+ raise ValueError (f"A window radius is required for matching mode: '{ matching_mode } '." )
339+ pairs = _make_pairs_sequential (image_indices , window_radius )
340+
341+ elif matching_mode == 'sequential_loop' :
342+ if window_radius is None :
343+ raise ValueError (f"A window radius is required for matching mode: '{ matching_mode } '." )
344+ pairs = _make_pairs_sequential_loop (image_indices , window_radius )
345+
346+ elif matching_mode == 'spatial' :
347+ if locations is None :
348+ raise ValueError ("Spatial matching requires `locations`, but none were provided." )
349+ if len (locations ) != len (images ):
350+ raise ValueError ("`locations` must be the same length as `images`." )
351+ if num_neighbors is None :
352+ raise ValueError ("Spatial matching requires `num_neighbors`, but it was not provided." )
353+ pairs = _make_pairs_spatial (image_indices , num_neighbors , locations )
354+
355+ else :
356+ raise ValueError (f"Invalid matching mode: '{ matching_mode } '. Must be one of { valid_modes } ." )
319357
320358 if progress_callback is None :
321359 def progress_callback (progress_percent : int , step_description : str ): # noqa: ARG001
@@ -404,7 +442,7 @@ def progress_callback(progress_percent : int, step_description : str): # noqa: A
404442 subprocess_stream_output (command_camera_init , logger_meshroom .info , logger_meshroom .warning )
405443
406444 camera_init_path = next (cache_dir .glob ("CameraInit/*/cameraInit.sfm" ))
407- write_pair_file (new_paths , camera_init_path , pair_file_path , window_radius = window_radius )
445+ write_pair_file (new_paths , pairs , camera_init_path , pair_file_path )
408446
409447 number_of_nodes = len ([node for node in _nodes if node in config_nodes ])
410448 pipeline_progress_start = 10.0
@@ -687,7 +725,7 @@ def _read_path_to_view_ids(input_path: Path) -> Dict[Path, str]:
687725 return basename_to_viewid
688726
689727
690- def _make_pairs (view_ids : List [str ], window_radius : int | None = None ) -> List [List [str ]]:
728+ def _make_pairs_sequential_loop (view_ids : List [Any ], window_radius : int | None = None ) -> List [List [Any ]]:
691729 """
692730 Generate image match pairs from a list of view IDs. Assumes view_ids are sequential and
693731 wrap around. Each view is matched with the `window_radius` views before and after it.
@@ -715,20 +753,88 @@ def _make_pairs(view_ids: List[str], window_radius: int | None = None) -> List[L
715753 rows [i ].append (view_ids [k ])
716754 else :
717755 rows [k % num_views ].append (view_ids [i ])
718-
719756 rows = rows [:- 1 ]
720757 return rows
721758
722- def write_pair_file (image_paths : List [Path ], camera_init_file : Path , output_path : Path , window_radius : int | None = None ) -> None :
759+ def _make_pairs_sequential (view_ids : List [Any ], window_radius : int ) -> List [List [Any ]]:
760+ """
761+ Generate image match pairs from a list of view IDs using a fixed sliding window.
762+ Each view is matched with the `window_radius` subsequent views, without wrapping
763+ around at the end of the list.
764+
765+ Args:
766+ view_ids (List[Any]): Ordered list of view identifiers.
767+ window_radius (int): Number of forward neighbors to match for each view.
768+
769+ Returns:
770+ List[List[Any]]: Each row is of the form [src, tgt1, tgt2, ...], meaning src
771+ is matched to tgt1, tgt2, etc.
723772 """
724- Write the imagePairsList file for Meshroom to perform sequential matching with wrap around.
725- Assumes that `image_paths` are ordered sequentially and wrap around.
773+ num_views = len (view_ids )
774+ rows = []
775+ for i in range (num_views - 1 ):
776+ rows .append ([view_ids [i ]])
777+ for j in range (i + 1 , min (i + window_radius + 1 , num_views )):
778+ rows [i ].append (view_ids [j ])
779+ return rows
780+
781+ def _make_pairs_spatial (view_ids : List [Any ], num_neighbors : int , locations : List [Tuple [float , float , float ]]) -> List [List [Any ]]:
726782 """
727- path_to_viewid = _read_path_to_view_ids (camera_init_file )
728- view_ids = [path_to_viewid [im_path ] for im_path in image_paths ]
783+ Generate image match pairs from a list of view IDs based on spatial proximity.
784+ Each view is matched with its `num_neighbors` nearest neighbors using 3D Euclidean
785+ distance.
729786
730- rows = _make_pairs (view_ids , window_radius )
787+ Args:
788+ view_ids (List[Any]): List of view identifiers.
789+ num_neighbors (int): Number of nearest neighbors to match each view with.
790+ locations (List[Tuple[float, float, float]]): 3D coordinates corresponding to each view.
791+
792+ Returns:
793+ List[List[Any]]: Each row is of the form [src, tgt1, tgt2, ...], where src is matched
794+ to tgt1, tgt2, etc.
795+ """
796+ assert len (view_ids ) == len (locations )
797+ num_views = len (view_ids )
798+ locations = np .array (locations )
799+ neighbors = [set () for _ in range (len (view_ids ))]
800+ for i in range (len (view_ids )):
801+ dists = np .linalg .norm (locations - locations [i ], axis = 1 )
802+ dists [i ] = np .inf
803+ nearest = np .argsort (dists )[:min (num_neighbors , num_views - 1 )]
804+ for j in nearest :
805+ if j > i :
806+ neighbors [i ].add (j )
807+ else :
808+ neighbors [j ].add (i )
809+
810+ #turn sets into list
811+ rows = []
812+ for i , neigh_set in enumerate (neighbors ):
813+ if neigh_set :
814+ row = [view_ids [i ]]
815+ for j in sorted (neigh_set ):
816+ row .append (view_ids [j ])
817+ rows .append (row )
818+ return rows
731819
820+
821+ def write_pair_file (images : List [Path ],
822+ pairs : List [List [int ]],
823+ camera_init_file : Path ,
824+ output_path : Path ) -> None :
825+ """
826+ Convert image index-based pairs to Meshroom internal view ID pairs using the provided
827+ camera_init_file. Write the result to a Meshroom compatible file.
828+
829+ Args:
830+ images: A list of image Paths (ordered by index).
831+ pairs: A list of lists of indices into the `images` list.
832+ camera_init_file: Path to Meshroom's cameraInit.sfm or similar file containing view IDs.
833+ output_path: Path where the resulting pair file will be written.
834+ """
835+ path_to_viewid = _read_path_to_view_ids (camera_init_file )
836+ #convert the image id to Meshrooms view id
837+ rows = [[path_to_viewid [images [image_id ]] for image_id in sublist ] for sublist in pairs ]
732838 with open (output_path , "w" ) as f :
733839 for row in rows :
734840 f .write (" " .join (map (str , row )) + "\n " )
0 commit comments