From 7ad3718fe3214a3d2a41135759f0734c6d3d266e Mon Sep 17 00:00:00 2001 From: Matt Dawkins Date: Fri, 11 Sep 2026 00:19:38 -0400 Subject: [PATCH] Ignore nonpositive detection lengths when importing VIAME CSV --- .../desktop/backend/serializers/viame.spec.ts | 29 +++++++++++++++++++ .../desktop/backend/serializers/viame.ts | 5 +++- server/dive_utils/serializers/viame.py | 5 +++- server/tests/test_deserialize_viame_csv.py | 22 ++++++++++++++ 4 files changed, 59 insertions(+), 2 deletions(-) diff --git a/client/platform/desktop/backend/serializers/viame.spec.ts b/client/platform/desktop/backend/serializers/viame.spec.ts index 01268e03d..ac74a5a0a 100644 --- a/client/platform/desktop/backend/serializers/viame.spec.ts +++ b/client/platform/desktop/backend/serializers/viame.spec.ts @@ -246,6 +246,35 @@ describe('VIAME Python Compatibility Check', () => { }); }); +describe('Detection length import', () => { + [0, -1, -2.5, 12.5].forEach((columnLength) => { + [undefined, 0, -1, -2.5, 7.5].forEach((attributeLength) => { + it(`imports positive lengths from column ${columnLength} and attribute ${attributeLength}`, async () => { + let csv = `0,1.png,0,10,10,20,20,1,${columnLength},fish,0.9,(atr) other 3`; + if (attributeLength !== undefined) { + csv += `,(atr) length ${attributeLength}`; + } + const [data] = await parse(Readable.from([csv])); + const track = Object.values(data.tracks)[0]; + const feature = track.features[0]; + const expected = attributeLength !== undefined && attributeLength > 0 + ? attributeLength : columnLength; + expect(feature.attributes?.other).toBe(3); + const { attributes } = processTrackAttributes([track]); + if (expected > 0) { + expect(feature.attributes?.length).toBe(expected); + expect(feature.fishLength).toBe(expected); + expect(attributes).toHaveProperty('detection_length'); + } else { + expect(feature.attributes).not.toHaveProperty('length'); + expect(feature).not.toHaveProperty('fishLength'); + expect(attributes).not.toHaveProperty('detection_length'); + } + }); + }); + }); +}); + describe('Attribute value parsing', () => { it('keeps filename-like attribute values as full strings', async () => { const csv = [ diff --git a/client/platform/desktop/backend/serializers/viame.ts b/client/platform/desktop/backend/serializers/viame.ts index 5fbfe9d84..6faace009 100644 --- a/client/platform/desktop/backend/serializers/viame.ts +++ b/client/platform/desktop/backend/serializers/viame.ts @@ -379,12 +379,15 @@ function _parseFeature(row: string[]) { frame: rowInfo.frame, bounds: rowInfo.bounds, }; - if (rowInfo.fishLength !== -1 && Number.isFinite(rowInfo.fishLength)) { + if (rowInfo.fishLength > 0 && Number.isFinite(rowInfo.fishLength)) { feature.fishLength = rowInfo.fishLength; } if (rowData.attributes) { feature.attributes = rowData.attributes; } + if (feature.attributes?.length !== undefined && Number(feature.attributes.length) <= 0) { + delete feature.attributes.length; + } const syncedFeature = syncDetectionLengthFields(feature); if (rowData.geoFeatureCollection.features.length > 0) { syncedFeature.geometry = rowData.geoFeatureCollection; diff --git a/server/dive_utils/serializers/viame.py b/server/dive_utils/serializers/viame.py index f8978fff7..e4409d640 100644 --- a/server/dive_utils/serializers/viame.py +++ b/server/dive_utils/serializers/viame.py @@ -68,12 +68,15 @@ def _resolve_detection_length( fish_length_from_column: float, ) -> Tuple[Dict[str, Any], Optional[float]]: """Resolve length from attributes.length or the VIAME length column.""" + attributes = dict(attributes or {}) attr_length: Optional[float] = None if attributes and 'length' in attributes: try: candidate = float(attributes['length']) - if candidate == candidate: # not NaN + if candidate > 0: attr_length = candidate + elif candidate <= 0: + attributes.pop('length') except (TypeError, ValueError): attr_length = None diff --git a/server/tests/test_deserialize_viame_csv.py b/server/tests/test_deserialize_viame_csv.py index b5d85ccef..c46cb7aca 100644 --- a/server/tests/test_deserialize_viame_csv.py +++ b/server/tests/test_deserialize_viame_csv.py @@ -22,3 +22,25 @@ def test_read_viame_csv( expected_tracks, sort_keys=True ) assert json.dumps(attributes, sort_keys=True) == json.dumps(expected_attributes, sort_keys=True) + + +@pytest.mark.parametrize("column_length", [0, -1, -2.5, 12.5]) +@pytest.mark.parametrize("attribute_length", [None, 0, -1, -2.5, 7.5]) +def test_import_detection_length(column_length, attribute_length): + row = f"0,1.png,0,10,10,20,20,1,{column_length},fish,0.9,(atr) other 3" + if attribute_length is not None: + row += f",(atr) length {attribute_length}" + converted, attributes, *_ = viame.load_csv_as_tracks_and_attributes([row]) + feature = converted['tracks']['0']['features'][0] + expected = ( + attribute_length if attribute_length is not None and attribute_length > 0 else column_length + ) + assert feature['attributes']['other'] == 3 + if expected > 0: + assert feature['attributes']['length'] == expected + assert feature['fishLength'] == expected + assert 'detection_length' in attributes + else: + assert 'length' not in feature['attributes'] + assert 'fishLength' not in feature + assert 'detection_length' not in attributes