Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/blank.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
39 changes: 35 additions & 4 deletions client/platform/desktop/backend/serializers/viame.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,18 +15,26 @@ 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',
],
},
{
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',
],
},
{
Expand All @@ -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',
],
},
];


Expand Down Expand Up @@ -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
Expand Down
62 changes: 42 additions & 20 deletions client/platform/desktop/backend/serializers/viame.ts
Original file line number Diff line number Diff line change
Expand Up @@ -246,44 +246,64 @@ async function parse(input: Readable, imageMap?: Map<string, number>): Promise<A
});
let fps: number | undefined;
const dataMap = new Map<number, TrackData>();
const missingImages: string[] = [];
let reordered = false;
let anyImageMatched = false;
let error: Error;

return new Promise<AnnotationFileData>((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;
}
}

Expand All @@ -301,9 +321,11 @@ async function parse(input: Readable, imageMap?: Map<string, number>): Promise<A
dataMap.set(rowInfo.trackId, track);
} else if (reordered) {
// trackId was already in dataMap, so the track has more than 1 detection.
throw new Error(
error = new Error(
'annotations were provided in an unexpected order and dataset contains multi-frame tracks',
);
// eslint-disable-next-line no-continue
continue;
}
track.begin = Math.min(rowInfo.frame, track.begin);
track.end = Math.max(rowInfo.frame, track.end);
Expand All @@ -317,22 +339,22 @@ async function parse(input: Readable, imageMap?: Map<string, number>): Promise<A
});
} catch (err) {
if (!(err instanceof Error)) {
throw new Error(`Caught unexpected error ${err}`);
error = new Error(`Caught unexpected error ${err}`);
// eslint-disable-next-line no-continue
continue;
}
if (err.toString().includes('comment row')) {
// parse comment row
fps = fps || parseCommentRow(record).fps;
} else if (!err.toString().includes('malformed row')) {
// Allow malformed row errors
throw err;
error = err;
// eslint-disable-next-line no-continue
continue;
}
}
}
});
parser.on('error', (err) => {
console.error(err);
reject(err);
});
});
}

Expand Down
8 changes: 4 additions & 4 deletions client/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
46 changes: 26 additions & 20 deletions server/dive_utils/serializers/viame.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Expand All @@ -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


Expand All @@ -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:
Expand Down
37 changes: 34 additions & 3 deletions server/tests/test_serialize_viame_csv.py
Original file line number Diff line number Diff line change
Expand Up @@ -278,18 +278,26 @@
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',
],
},
{
'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',
],
},
{
Expand All @@ -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',
],
},
]


Expand Down