diff --git a/.github/workflows/blank.yml b/.github/workflows/blank.yml index 82b6d9a6e..266fedda3 100644 --- a/.github/workflows/blank.yml +++ b/.github/workflows/blank.yml @@ -19,9 +19,9 @@ jobs: - uses: actions/checkout@v2 - - uses: actions/setup-node@v1 + uses: actions/setup-node@v2 with: - node-version: '16.x' + node-version: '12.18.3' - name: Get yarn cache directory path id: yarn-cache-dir-path diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d5f01bb6e..f7613d58d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -22,10 +22,10 @@ jobs: # "ref" specifies the branch to check out. # "github.event.release.target_commitish" is a global variable and specifies the branch the release targeted ref: ${{ github.event.release.target_commitish }} - - name: Use Node 14 - uses: actions/setup-node@v1 + - name: Use Node 12 + uses: actions/setup-node@v2 with: - node-version: '16.x' + node-version: '12.18.3' # Specifies the registry, this field is required! registry-url: https://registry.npmjs.org/ - run: yarn install --frozen-lockfile diff --git a/client/package.json b/client/package.json index 78cf10aa2..726bf568a 100644 --- a/client/package.json +++ b/client/package.json @@ -76,7 +76,7 @@ "@types/mime-types": "^2.1.0", "@types/mock-fs": "^4.13.1", "@types/mousetrap": "^1.6.3", - "@types/node": "^14.0.5", + "@types/node": "^12.18.3", "@types/proper-lockfile": "^4.1.1", "@types/pump": "^1.1.0", "@types/range-parser": "^1.2.3", diff --git a/client/platform/desktop/backend/serializers/viame.spec.ts b/client/platform/desktop/backend/serializers/viame.spec.ts index 419a16b93..8f7dc10cf 100644 --- a/client/platform/desktop/backend/serializers/viame.spec.ts +++ b/client/platform/desktop/backend/serializers/viame.spec.ts @@ -15,7 +15,7 @@ const testData: testPairs[] = fs.readJSONSync('../testutils/viame.spec.json'); const imageFilenameTests = [ { pass: false, - error: 'There was a mixture of fields that specified image names and fields that did not. Please check the CSV', + error: 'CSV import was found to have a mix of missing images and images that were found in the data. This usually indicates a problem with the annotation file, but if you want to force the import to proceed, you can set all values in the Image Name column to be blank. Then DIVE will not attempt to validate image names. Missing images include: ...', csv: [ '0, ,1,884.66,510,1219.66,737.66,1,-1,ignored,0.98', '1,2.png,0,111,222,3333,444,1,-1,typestring,0.55', @@ -23,10 +23,18 @@ const imageFilenameTests = [ }, { pass: false, - error: 'There was a mixture of fields that specified image names and fields that did not. Please check the CSV', + error: 'CSV import was found to have a mix of missing images and images that were found in the data. This usually indicates a problem with the annotation file, but if you want to force the import to proceed, you can set all values in the Image Name column to be blank. Then DIVE will not attempt to validate image names. Missing images include: invalid...', csv: [ '0,1.png,1,884.66,510,1219.66,737.66,1,-1,ignored,0.98', - '1,,0,111,222,3333,444,1,-1,typestring,0.55', + '1,invalid,0,111,222,3333,444,1,-1,typestring,0.55', + ], + }, + { + pass: true, + csv: [ + '0,invalid1,1,884.66,510,1219.66,737.66,1,-1,ignored,0.98', + '', + '1,invalid2,0,111,222,3333,444,1,-1,typestring,0.55', ], }, { @@ -37,6 +45,29 @@ const imageFilenameTests = [ '1,,0,111,222,3333,444,1,-1,typestring,0.55', ], }, + { + pass: true, + csv: [ + '0,1.png,1,884.66,510,1219.66,737.66,1,-1,ignored,0.98', + '', + '1,1.png,0,111,222,3333,444,1,-1,typestring,0.55', + ], + }, + { + pass: false, + error: 'Error: annotations were provided in an unexpected order and dataset contains multi-frame tracks', + csv: [ + '99,1.png,0,884.66,510,1219.66,737.66,1,-1,ignored,0.98', + '99,3.png,1,111,222,3333,444,1,-1,typestring,0.55', + ], + }, + { + pass: true, + csv: [ + '99,unknown1,2,884.66,510,1219.66,737.66,1,-1,ignored,0.98', + '99,unknown2,2,111,222,3333,444,1,-1,typestring,0.55', + ], + }, ]; @@ -273,7 +304,7 @@ describe('Test Image Filenames', () => { // eslint-disable-next-line no-await-in-loop await parseFile(testPath, imageMap); } catch (err) { - expect(err).toBe(imageOrderData.error); + expect((err as Error).toString()).toBe(imageOrderData.error); } } else { // eslint-disable-next-line no-await-in-loop diff --git a/client/platform/desktop/backend/serializers/viame.ts b/client/platform/desktop/backend/serializers/viame.ts index 9239ad13c..b6eccf35d 100644 --- a/client/platform/desktop/backend/serializers/viame.ts +++ b/client/platform/desktop/backend/serializers/viame.ts @@ -246,44 +246,64 @@ async function parse(input: Readable, imageMap?: Map): Promise(); + const missingImages: string[] = []; let reordered = false; + let anyImageMatched = false; + let error: Error; return new Promise((resolve, reject) => { - pipeline(input, parser, (err) => { + pipeline([input, parser], (err) => { // undefined err indicates successful exit if (err !== undefined) { reject(err); } - resolve({ tracks: Array.from(dataMap.values()), fps }); + if (error !== undefined) { + reject(error); + } + const tracks = Array.from(dataMap.values()); + + if (imageMap !== undefined && missingImages.length > 0 && anyImageMatched) { + /** + * If any image from CSV was not missing, then some number of images + * from column 2 were actually valid and some were not. This indicates that the dataset + * being loaded is probably corrupt. + * + * If all images were missing, then every single image was missing, which indicates + * that the dataset either had all empty values in column 2 or some other type of invalid + * string that should not prevent import. + */ + reject([ + 'CSV import was found to have a mix of missing images and images that were found', + 'in the data. This usually indicates a problem with the annotation file, but if', + 'you want to force the import to proceed, you can set all values in the', + 'Image Name column to be blank. Then DIVE will not attempt to validate image names.', + `Missing images include: ${missingImages.slice(0, 5)}...`, + ].join(' ')); + } + resolve({ tracks, fps }); }); parser.on('readable', () => { let record: string[]; - let hasFilenames: undefined | boolean; // eslint-disable-next-line no-cond-assign while (record = parser.read()) { try { const { rowInfo, feature, trackAttributes, confidencePairs, } = _parseFeature(record); - const currentHasFileName = rowInfo.filename.trim() !== ''; - if (imageMap !== undefined && hasFilenames === undefined) { - hasFilenames = currentHasFileName; - } else if (imageMap !== undefined && hasFilenames !== currentHasFileName) { - throw new Error('Image Filenames specified in the Column 2 of the CSV must either be all set or all empty. Encountered a mixture of set and empty filenames'); - } - if (imageMap !== undefined && hasFilenames) { + if (imageMap !== undefined) { // validate image ordering if the imageMap is provided and a non-whitespace filename const [imageName] = splitExt(rowInfo.filename); const expectedFrameNumber = imageMap.get(imageName); if (expectedFrameNumber === undefined) { - throw new Error( - `encountered annotation for image not found in dataset: ${rowInfo.filename}`, - ); + missingImages.push(rowInfo.filename); } else if (expectedFrameNumber !== feature.frame) { // force reorder the annotations reordered = true; + anyImageMatched = true; feature.frame = expectedFrameNumber; rowInfo.frame = expectedFrameNumber; + } else { + anyImageMatched = true; } } @@ -301,9 +321,11 @@ async function parse(input: Readable, imageMap?: Map): Promise): Promise { - console.error(err); - reject(err); - }); }); } diff --git a/client/yarn.lock b/client/yarn.lock index cc22e6b9e..86bf664d8 100644 --- a/client/yarn.lock +++ b/client/yarn.lock @@ -2770,10 +2770,10 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-12.20.36.tgz#5bd54d2383e714fc4d2c258107ee70c5bad86d0c" integrity sha512-+5haRZ9uzI7rYqzDznXgkuacqb6LJhAti8mzZKWxIXn/WEtvB+GHVJ7AuMwcN1HMvXOSJcrvA6PPoYHYOYYebA== -"@types/node@^14.0.5": - version "14.0.5" - resolved "https://registry.yarnpkg.com/@types/node/-/node-14.0.5.tgz#3d03acd3b3414cf67faf999aed11682ed121f22b" - integrity sha512-90hiq6/VqtQgX8Sp0EzeIsv3r+ellbGj4URKj5j30tLlZvRUpnAe9YbYnjl3pJM93GyXU0tghHhvXHq+5rnCKA== +"@types/node@^12.18.3": + version "12.20.46" + resolved "https://registry.yarnpkg.com/@types/node/-/node-12.20.46.tgz#7e49dee4c54fd19584e6a9e0da5f3dc2e9136bc7" + integrity sha512-cPjLXj8d6anFPzFvOPxS3fvly3Shm5nTfl6g8X5smexixbuGUf7hfr21J5tX9JW+UPStp/5P5R8qrKL5IyVJ+A== "@types/normalize-package-data@^2.4.0": version "2.4.0" diff --git a/server/dive_utils/serializers/viame.py b/server/dive_utils/serializers/viame.py index 0990c4fc6..051891902 100644 --- a/server/dive_utils/serializers/viame.py +++ b/server/dive_utils/serializers/viame.py @@ -248,7 +248,8 @@ def load_csv_as_tracks_and_attributes( metadata_attributes: Dict[str, Dict[str, Any]] = {} test_vals: Dict[str, Dict[str, int]] = {} reordered = False - has_image_filenames: Union[None, bool] = None + anyImageMatched = False + missingImages: List[str] = [] for row in reader: if len(row) == 0 or row[0].startswith('#'): # This is not a data row @@ -261,27 +262,20 @@ def load_csv_as_tracks_and_attributes( ) = _parse_row_for_tracks(row) trackId, imageFile, _, _, _ = row_info(row) - current_has_filename = imageFile.strip() != '' - if has_image_filenames is None and imageMap: - has_image_filenames = current_has_filename - elif imageMap and has_image_filenames != current_has_filename: - raise ValueError( - 'There was a mixture of fields that specified image names and fields that' - ' did not. Please check the CSV' - ) - return - if imageMap and has_image_filenames: + + if imageMap: # validate image ordering if the imageMap is provided imageName, _ = os.path.splitext(os.path.basename(imageFile)) expectedFrameNumber = imageMap.get(imageName) if expectedFrameNumber is None: - raise ValueError( - f'encountered annotation for image not found in dataset: {imageFile}' - ) + missingImages.append(imageFile) elif expectedFrameNumber is not feature.frame: # force reorder the annotations reordered = True + anyImageMatched = True feature.frame = expectedFrameNumber + else: + anyImageMatched = True if trackId not in tracks: tracks[trackId] = Track(begin=feature.frame, end=feature.frame, trackId=trackId) @@ -305,10 +299,27 @@ def load_csv_as_tracks_and_attributes( create_attributes(metadata_attributes, test_vals, 'track', key, val) for (key, val) in attributes.items(): create_attributes(metadata_attributes, test_vals, 'detection', key, val) + + trackarr = tracks.items() + + if imageMap and len(missingImages) and anyImageMatched: + examples = ', '.join(missingImages[:3]) + raise ValueError( + ' '.join( + [ + 'CSV import was found to have a mix of missing images and images that', + 'were found in the data. This usually indicates a problem with the', + 'annotation file, but if you want to force the import to proceed, you', + 'can set all values in the Image Name column to be blank. Then DIVE', + 'will not attempt to validate image names.', + f'Missing images include: {examples}...', + ] + ) + ) # Now we process all the metadata_attributes for the types calculate_attribute_types(metadata_attributes, test_vals) - track_json = {trackId: track.dict(exclude_none=True) for trackId, track in tracks.items()} + track_json = {trackId: track.dict(exclude_none=True) for trackId, track in trackarr} return track_json, metadata_attributes @@ -325,15 +336,10 @@ def export_tracks_as_csv( Export track json to a CSV format. :param excludeBelowThreshold: omit tracks below a certain confidence. Requires thresholds. - :param thresholds: key/value pairs with threshold values - :param filenames: list of string file names. filenames[n] should be the image at frame n - :param fps: if FPS is set, column 2 will be video timestamp derived from (frame / fps) - :param header: include or omit header - :param typeFilter: set of track types to only export if not empty """ if thresholds is None: diff --git a/server/tests/test_serialize_viame_csv.py b/server/tests/test_serialize_viame_csv.py index 2902d602f..d3663aa53 100644 --- a/server/tests/test_serialize_viame_csv.py +++ b/server/tests/test_serialize_viame_csv.py @@ -278,7 +278,7 @@ image_filename_tests = [ { 'pass': False, - 'error': 'There was a mixture of fields that specified image names and fields that did not. Please check the CSV', + 'error': 'CSV import was found to have a mix of missing images and images that were found in the data. This usually indicates a problem with the annotation file, but if you want to force the import to proceed, you can set all values in the Image Name column to be blank. Then DIVE will not attempt to validate image names. Missing images include: ...', 'csv': [ '0, ,1,884.66,510,1219.66,737.66,1,-1,ignored,0.98', '1,2.png,0,111,222,3333,444,1,-1,typestring,0.55', @@ -286,10 +286,18 @@ }, { 'pass': False, - 'error': 'There was a mixture of fields that specified image names and fields that did not. Please check the CSV', + 'error': 'CSV import was found to have a mix of missing images and images that were found in the data. This usually indicates a problem with the annotation file, but if you want to force the import to proceed, you can set all values in the Image Name column to be blank. Then DIVE will not attempt to validate image names. Missing images include: invalid...', 'csv': [ '0,1.png,1,884.66,510,1219.66,737.66,1,-1,ignored,0.98', - '1,,0,111,222,3333,444,1,-1,typestring,0.55', + '1,invalid,0,111,222,3333,444,1,-1,typestring,0.55', + ], + }, + { + 'pass': True, + 'csv': [ + '0,invalid1,1,884.66,510,1219.66,737.66,1,-1,ignored,0.98', + '', + '1,invalid2,0,111,222,3333,444,1,-1,typestring,0.55', ], }, { @@ -300,6 +308,29 @@ '1,,0,111,222,3333,444,1,-1,typestring,0.55', ], }, + { + 'pass': True, + 'csv': [ + '0,1.png,1,884.66,510,1219.66,737.66,1,-1,ignored,0.98', + '', + '1,1.png,0,111,222,3333,444,1,-1,typestring,0.55', + ], + }, + { + 'pass': False, + 'error': 'images were provided in an unexpected order and dataset contains multi-frame tracks.', + 'csv': [ + '99,1.png,0,884.66,510,1219.66,737.66,1,-1,ignored,0.98', + '99,3.png,1,111,222,3333,444,1,-1,typestring,0.55', + ], + }, + { + 'pass': True, + 'csv': [ + '99,unknown1,2,884.66,510,1219.66,737.66,1,-1,ignored,0.98', + '99,unknown2,2,111,222,3333,444,1,-1,typestring,0.55', + ], + }, ]