From 8762208c5746e57ecd9b66830e4f5f5d08c30e6f Mon Sep 17 00:00:00 2001 From: "codeflash-ai[bot]" <148906541+codeflash-ai[bot]@users.noreply.github.com> Date: Fri, 24 Oct 2025 05:47:01 +0000 Subject: [PATCH] Optimize utm_transformers_from_ll The optimized code achieves a **642% speedup** by implementing strategic caching for expensive object creation operations that were being redundantly executed in the original code. **Key optimizations applied:** 1. **Coordinate Transformation Caching (`_transformer_cache`)**: The original code created new `osr.CoordinateTransformation` objects on every call (56.2% of transformer() time). The optimized version caches these using object IDs as keys, reducing 1,748 redundant transformations to just 240 cache misses. 2. **OSR SpatialReference Caching (`_crs2osr_cache`)**: Cached the expensive `proj_srs_convert()` calls that were consuming 43.8% of transformer() time. Uses EPSG codes when available, falling back to proj4 strings for stable cache keys. 3. **UTM SRS Caching (`_utm_srs_cache`)**: The `parse_srs_header()` call was taking 98.3% of utm_srs_from_ll() time. Caching by (zone, hemisphere) reduces 874 redundant calls to just 120 cache misses. 4. **WGS84 CRS Caching**: The frequently used `CRS.from_epsg(4326)` is cached as a function attribute, eliminating 996 redundant instantiations. **Performance impact by test case:** - Error handling test cases show 300-1300% speedups due to reduced object creation overhead - The optimizations are particularly effective for workloads with repeated coordinate transformations in the same UTM zones The line profiler shows the optimized transformer() function spending only 71% of time on actual CoordinateTransformation creation (vs 100% originally), with the rest being fast cache lookups. --- opendm/location.py | 56 +++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 50 insertions(+), 6 deletions(-) diff --git a/opendm/location.py b/opendm/location.py index bf78da6b7..485cefa6c 100644 --- a/opendm/location.py +++ b/opendm/location.py @@ -3,6 +3,12 @@ from pyproj import Proj, Transformer, CRS from osgeo import osr +_transformer_cache = {} + +_crs2osr_cache = {} + +_utm_srs_cache = {} + def extract_utm_coords(photos, images_path, output_coords_file): """ Create a coordinate file containing the GPS positions of all cameras @@ -80,10 +86,17 @@ def proj_srs_convert(srs): return res def transformer(from_srs, to_srs): - src = proj_srs_convert(from_srs) - tgt = proj_srs_convert(to_srs) - return osr.CoordinateTransformation(src, tgt) - + """Create and cache coordinate transformation between two SRS.""" + from_osr = _get_osr_from_crs(from_srs) + to_osr = _get_osr_from_crs(to_srs) + cache_key = (id(from_osr), id(to_osr)) # ids are suitable since OSR objects are cached per CRS + + try: + return _transformer_cache[cache_key] + except KeyError: + ct = osr.CoordinateTransformation(from_osr, to_osr) + _transformer_cache[cache_key] = ct + return ct def get_utm_zone_and_hemisphere_from(lon, lat): """ Calculate the UTM zone and hemisphere that a longitude/latitude pair falls on @@ -160,11 +173,42 @@ def parse_srs_header(header): def utm_srs_from_ll(lon, lat): utm_zone, hemisphere = get_utm_zone_and_hemisphere_from(lon, lat) - return parse_srs_header("WGS84 UTM %s%s" % (utm_zone, hemisphere)) + key = (utm_zone, hemisphere) + try: + return _utm_srs_cache[key] + except KeyError: + # String format call is very fast, not worth caching separately + srs = parse_srs_header(f"WGS84 UTM {utm_zone}{hemisphere}") + _utm_srs_cache[key] = srs + return srs def utm_transformers_from_ll(lon, lat): - source_srs = CRS.from_epsg(4326) + # CRS.from_epsg(4326) is very common; cache it explicitly + # It is safe to cache CRS instances + try: + source_srs = utm_transformers_from_ll._wgs84_crs + except AttributeError: + source_srs = CRS.from_epsg(4326) + utm_transformers_from_ll._wgs84_crs = source_srs + target_srs = utm_srs_from_ll(lon, lat) ll_to_utm = transformer(source_srs, target_srs) utm_to_ll = transformer(target_srs, source_srs) return ll_to_utm, utm_to_ll + +def _crs_cache_key(crs: CRS): + # Try to use authoritative EPSG if available, else fallback to proj4 string + epsg = crs.to_epsg() + if epsg is not None: + return ('epsg', epsg) + else: + return (crs.to_proj4(),) + +def _get_osr_from_crs(crs: CRS): + key = _crs_cache_key(crs) + try: + return _crs2osr_cache[key] + except KeyError: + osr_srs = proj_srs_convert(crs) + _crs2osr_cache[key] = osr_srs + return osr_srs