Skip to content

Fix --boundary crash on fiona 1.10 (CRSError: Invalid PROJ string: EPSG:4326)#2040

Merged
smathermather merged 6 commits into
OpenDroneMap:masterfrom
EmirTD:fix-boundary-crs-fiona110
Jul 3, 2026
Merged

Fix --boundary crash on fiona 1.10 (CRSError: Invalid PROJ string: EPSG:4326)#2040
smathermather merged 6 commits into
OpenDroneMap:masterfrom
EmirTD:fix-boundary-crs-fiona110

Conversation

@EmirTD

@EmirTD EmirTD commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

What

load_boundary() re-parsed the boundary's source CRS via CRS.from_proj4(fiona.crs.to_string(src.crs)). On fiona 1.10, fiona.crs.to_string() is deprecated and returns the authority string "EPSG:4326" instead of a PROJ4 string, so pyproj's CRS.from_proj4("EPSG:4326") raises CRSError: Invalid PROJ string: EPSG:4326 and any --boundary run aborts during the dataset stage.

GeoJSON coordinates are WGS84 by specification (RFC 7946), so the source CRS is always EPSG:4326 and does not need to be read back from the file. This replaces the deprecated, broken call with an explicit CRS.from_epsg(4326).

Related

Ref #2039 (auto-closed by the bot, but this is a genuine reproducible bug — details and root cause are there).

Reproduction (before this PR)

docker run --rm --entrypoint /code/venv/bin/python opendronemap/odm:gpu -c "
from opendm import boundary
geo = {'type':'Feature','properties':{},'geometry':{'type':'Polygon',
       'coordinates':[[[29.02,41.10],[29.03,41.10],[29.03,41.11],[29.02,41.11],[29.02,41.10]]]}}
boundary.load_boundary(geo, '+proj=utm +zone=35 +datum=WGS84 +units=m +no_defs')
"
# -> pyproj.exceptions.CRSError: Invalid PROJ string: EPSG:4326

Change

         if reproject_to_proj4 is not None:
-            t = transformer(CRS.from_proj4(fiona.crs.to_string(src.crs)),
-                            CRS.from_proj4(reproject_to_proj4))
+            t = transformer(CRS.from_epsg(4326),
+                            CRS.from_proj4(reproject_to_proj4))
             coords = [t.TransformPoint(*c)[:dimensions] for c in coords]

Verification

With the patch, the same input transforms correctly to UTM zone 35N (669632.9, 4551824.3, …), and a full --boundary run completes past the dataset stage.

Environment

ODM 3.6.0, opendronemap/odm:gpu, fiona 1.10.1, pyproj 3.7.1.

…SG:4326)

fiona.crs.to_string() is deprecated in fiona 1.10 and returns "EPSG:4326"
instead of a PROJ4 string. GeoJSON is always WGS84 (RFC 7946), so use
CRS.from_epsg(4326) directly.
@EmirTD

EmirTD commented Jun 25, 2026

Copy link
Copy Markdown
Contributor Author

Additional confirmation that this is a fiona-version regression, not a code change — opendm/boundary.py has been unchanged for ~4 years, yet the same --boundary call works on an older ODM and crashes on the current one:

ODM fiona pyproj --boundary
3.5.6 1.8.17 3.3.1 ✅ works
3.6.0 1.10.1 3.7.1 CRSError: Invalid PROJ string: EPSG:4326

In fiona 1.8.x, fiona.crs.to_string(src.crs) returned a PROJ4 string that CRS.from_proj4() accepts. In fiona 1.10 it returns the authority string "EPSG:4326" (and emits a FionaDeprecationWarning pointing to CRS.to_string()), which is not a valid PROJ4 string — hence the crash.

Since GeoJSON coordinates are always WGS84 by spec (RFC 7946), reading the source CRS back from the file is unnecessary, and CRS.from_epsg(4326) is both correct and version-independent.

@EmirTD

EmirTD commented Jun 26, 2026

Copy link
Copy Markdown
Contributor Author

Added a regression test in tests/test_boundary.py covering this exact case
(load_boundary with a GeoJSON boundary + a UTM reprojection target). It fails
on the current code with CRSError: Invalid PROJ string: EPSG:4326 and passes
with this fix — also addresses the request in #9.

@smathermather

smathermather commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

GeoJSON coordinates are WGS84 by specification (RFC 7946), so the source CRS is always EPSG:4326 and does not need to be read back from the file. This replaces the deprecated, broken call with an explicit CRS.from_epsg(4326).

I would double check the spec. I'm afraid we cannot assume CRS of WGS84. The draft spec officially allowed it:
https://geojson.org/geojson-spec.html#coordinate-reference-system-objects

And the final spec, strictly speaking, allows for it:
https://www.rfc-editor.org/info/rfc7946/#section-4

Moreover, in practice, many prominent tools, like QGIS.org, will allow one to write to an alternative CRS.

If this were a new feature, I would be really comfortable with requiring WGS84 CRS. But as this is an existing feature with possible use cases, we unfortunately probably need to continue to support other CRSs.

@smathermather smathermather linked an issue Jun 26, 2026 that may be closed by this pull request
@smathermather

Copy link
Copy Markdown
Contributor

Added a regression test in tests/test_boundary.py covering this exact case
(load_boundary with a GeoJSON boundary + a UTM reprojection target). It fails
on the current code with CRSError: Invalid PROJ string: EPSG:4326 and passes
with this fix — also addresses the request in #9.

Excellent! I have migrated that issue here so it can close on merge. It does make more sense here as a proper regression test.

@EmirTD

EmirTD commented Jun 26, 2026

Copy link
Copy Markdown
Contributor Author

GeoJSON coordinates are WGS84 by specification (RFC 7946), so the source CRS is always EPSG:4326 and does not need to be read back from the file. This replaces the deprecated, broken call with an explicit CRS.from_epsg(4326).

I would double check the spec. I'm afraid we cannot assume CRS of WGS84. The draft spec officially allowed it: https://geojson.org/geojson-spec.html#coordinate-reference-system-objects

And the final spec, strictly speaking, allows for it: https://www.rfc-editor.org/info/rfc7946/#section-4

Moreover, in practice, many prominent tools, like QGIS.org, will allow one to write to an alternative CRS.

If this were a new feature, I would be really comfortable with requiring WGS84 CRS. But as this is an existing feature with possible use cases, we unfortunately probably need to continue to support other CRSs.

You're right, we shouldn't assume WGS 84. I updated the fix to keep reading
the source CRS from the file, just via a fiona-1.10-compatible call:

t = transformer(CRS.from_user_input(src.crs),
                CRS.from_proj4(reproject_to_proj4))

CRS.from_user_input(src.crs) takes the fiona CRS object directly, so it works for both
the WGS 84 default and an explicitly declared CRS (verified with a boundary in EPSG:32635).

I also extended tests/test_boundary.py with a case that loads a non-WGS 84 boundary
(EPSG:32635) and checks it reprojects from that CRS rather than assuming WGS 84.

@smathermather

Copy link
Copy Markdown
Contributor

Yes. That's the better route. We don't need it to be backwards compatible, so best to use the newer API.

@smathermather smathermather left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code change looks great. @MJohnson459 -- can you double-check the tests? They look fine to me, but it would be better to get better eyes on them.

@MJohnson459 MJohnson459 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good apart from the duplicate test. Once that is removed I will accept.

Comment thread tests/test_boundary.py Outdated
Comment on lines +80 to +126
import unittest

from opendm.boundary import load_boundary


class TestBoundary(unittest.TestCase):
def setUp(self):
# A simple WGS84 (EPSG:4326) GeoJSON polygon
self.boundary = {
'type': 'Feature',
'properties': {'name': 'boundary'},
'geometry': {
'type': 'Polygon',
'coordinates': [[
[-91.99544, 46.84260],
[-91.99417, 46.84260],
[-91.99417, 46.84337],
[-91.99544, 46.84337],
[-91.99544, 46.84260],
]]
}
}

def test_load_boundary_without_reprojection(self):
# Without a target CRS the coordinates are returned unchanged
coords = load_boundary(self.boundary)
self.assertEqual(len(coords), 5)
self.assertAlmostEqual(coords[0][0], -91.99544, places=5)
self.assertAlmostEqual(coords[0][1], 46.84260, places=5)

def test_load_boundary_reprojects_to_utm(self):
# Regression test for https://github.com/OpenDroneMap/ODM/issues/2039
# On fiona >= 1.10, fiona.crs.to_string() returns "EPSG:4326" instead of a
# PROJ4 string, which made CRS.from_proj4() raise
# "CRSError: Invalid PROJ string: EPSG:4326". GeoJSON is always WGS84
# (RFC 7946), so load_boundary must reproject without raising.
utm15n = '+proj=utm +zone=15 +datum=WGS84 +units=m +no_defs'
coords = load_boundary(self.boundary, utm15n)
self.assertEqual(len(coords), 5)
# Reprojected coordinates are in meters (UTM), not degrees
for x, y in coords:
self.assertGreater(abs(x), 1000)
self.assertGreater(abs(y), 1000)


if __name__ == '__main__':
unittest.main()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems to be a duplicate of the above?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed the duplicated test block, thanks for catching it.

@EmirTD
EmirTD requested a review from MJohnson459 July 2, 2026 09:40
@smathermather
smathermather merged commit cd2206c into OpenDroneMap:master Jul 3, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add test case for boundary

3 participants