forked from SkalskiP/make-sense
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request SkalskiP#280 from hartmannr76/rmh_VOCXMLImport
Initial working version of VOC XML import
- Loading branch information
Showing
9 changed files
with
310 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,6 @@ | ||
export enum AcceptedFileType { | ||
IMAGE = 'image/jpeg, image/png', | ||
TEXT = 'text/plain', | ||
JSON = 'application/json' | ||
JSON = 'application/json', | ||
XML = 'application/xml', | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,137 @@ | ||
|
||
import { ImageData, LabelName, LabelRect} from '../../../../store/labels/types'; | ||
import { AcceptedFileType } from '../../../../data/enums/AcceptedFileType'; | ||
import { v4 as uuidv4 } from 'uuid'; | ||
import { VOCImporter } from '../../../import/voc/VOCImporter'; | ||
import { isEqual } from 'lodash'; | ||
|
||
const getDummyImageData = (fileName: string): ImageData => { | ||
return { | ||
id: uuidv4(), | ||
fileData: new File([''], fileName, { type: AcceptedFileType.IMAGE }), | ||
loadStatus: true, | ||
labelRects: [], | ||
labelPoints: [], | ||
labelLines: [], | ||
labelPolygons: [], | ||
labelNameIds: [], | ||
isVisitedByYOLOObjectDetector: false, | ||
isVisitedBySSDObjectDetector: false, | ||
isVisitedByPoseDetector: false | ||
}; | ||
}; | ||
|
||
const getDummyFileData = (fileName: string): File => { | ||
return new File([''], fileName, { type: AcceptedFileType.TEXT }); | ||
}; | ||
|
||
class TestableVOCImporter extends VOCImporter { | ||
public static testableParseAnnotationsFromFileString(document: Document, labelNames: Record<string, LabelName>) | ||
:[LabelRect[], Record<string, LabelName>] { | ||
return TestableVOCImporter.parseAnnotationsFromFileString(document, labelNames); | ||
} | ||
} | ||
|
||
const parser = new DOMParser(); | ||
const validTestDocument = parser.parseFromString( | ||
` | ||
<annotation> | ||
<filename>test1.jpeg</filename> | ||
<path>\\some-test-path\\test1.jpeg</path> | ||
<size> | ||
<width>100</width> | ||
<height>200</height> | ||
<depth>3</depth> | ||
</size> | ||
<segmented>0</segmented> | ||
<object> | ||
<name>annotation1</name> | ||
<pose>Unspecified</pose> | ||
<truncated>0</truncated> | ||
<difficult>0</difficult> | ||
<bndbox> | ||
<xmin>10</xmin> | ||
<ymin>20</ymin> | ||
<xmax>30</xmax> | ||
<ymax>50</ymax> | ||
</bndbox> | ||
</object> | ||
<object> | ||
<name>annotation2</name> | ||
<pose>Unspecified</pose> | ||
<truncated>0</truncated> | ||
<difficult>0</difficult> | ||
<bndbox> | ||
<xmin>20</xmin> | ||
<ymin>30</ymin> | ||
<xmax>30</xmax> | ||
<ymax>50</ymax> | ||
</bndbox> | ||
</object> | ||
</annotation> | ||
`, | ||
'application/xml' | ||
); | ||
|
||
describe('VOCImporter parseAnnotationsFromFileString method', () => { | ||
it('should return correctly for multiple annotations', () => { | ||
// given | ||
const emptyRecordSet: Record<string, LabelName> = {}; | ||
|
||
// when | ||
const [annotations, newRecordSet] = TestableVOCImporter.testableParseAnnotationsFromFileString(validTestDocument, emptyRecordSet); | ||
|
||
// then | ||
expect(Object.keys(emptyRecordSet).length).toBe(0); | ||
expect(newRecordSet).toEqual(expect.objectContaining({ | ||
'annotation1': expect.objectContaining({ name: 'annotation1'}), | ||
'annotation2': expect.objectContaining({ name: 'annotation2'}), | ||
})) | ||
expect(annotations).toEqual([ | ||
expect.objectContaining({ | ||
rect: { | ||
x: 10, | ||
y: 20, | ||
height: 30, | ||
width: 20 | ||
}, | ||
labelId: newRecordSet['annotation1'].id | ||
}), | ||
expect.objectContaining({ | ||
rect: { | ||
x: 20, | ||
y: 30, | ||
height: 20, | ||
width: 10 | ||
}, | ||
labelId: newRecordSet['annotation2'].id | ||
}) | ||
]); | ||
}); | ||
|
||
it('should reuse existing labels', () => { | ||
// given | ||
const existingRecordSet: Record<string, LabelName> = { | ||
'annotation2': { | ||
id: 'foobar', | ||
name: 'annotation2' | ||
} | ||
}; | ||
|
||
// when | ||
const [annotations, newRecordSet] = TestableVOCImporter.testableParseAnnotationsFromFileString(validTestDocument, existingRecordSet); | ||
|
||
// then | ||
expect(Object.keys(existingRecordSet).length).toBe(1); | ||
expect(newRecordSet).toEqual(expect.objectContaining({ | ||
'annotation1': expect.objectContaining({ name: 'annotation1' }), | ||
'annotation2': expect.objectContaining({ name: 'annotation2', id: 'foobar' }), | ||
})); | ||
expect(annotations.length).toBe(2); | ||
expect(annotations).toEqual(expect.arrayContaining([ | ||
expect.objectContaining({ | ||
labelId: 'foobar' | ||
}) | ||
])); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,142 @@ | ||
import {ImageData, LabelName, LabelRect} from '../../../store/labels/types'; | ||
import {LabelUtil} from "../../../utils/LabelUtil"; | ||
import {AnnotationImporter} from '../AnnotationImporter'; | ||
import {LabelsSelector} from '../../../store/selectors/LabelsSelector'; | ||
|
||
type FileParseResult = { | ||
filename: string, | ||
labeledBoxes: LabelRect[] | ||
}; | ||
|
||
type VOCImportResult = { | ||
labelNames: Record<string, LabelName>, | ||
fileParseResults: FileParseResult[], | ||
}; | ||
|
||
export class DocumentParsingError extends Error { | ||
constructor(message?: string) { | ||
super(message); | ||
this.name = "DocumentParsingError"; | ||
} | ||
} | ||
|
||
export class AnnotationAssertionError extends Error { | ||
constructor(message?: string) { | ||
super(message); | ||
this.name = "AnnotationAssertionError"; | ||
} | ||
} | ||
|
||
const parser = new DOMParser(); | ||
|
||
export class VOCImporter extends AnnotationImporter { | ||
public import( | ||
filesData: File[], | ||
onSuccess: (imagesData: ImageData[], labelNames: LabelName[]) => any, | ||
onFailure: (error?:Error) => any | ||
): void { | ||
try { | ||
const inputImagesData: Record<string, ImageData> = VOCImporter.mapImageData(); | ||
|
||
this.loadAndParseFiles(filesData).then(results => { | ||
for (const result of results.fileParseResults) { | ||
if (inputImagesData[result.filename]) { | ||
inputImagesData[result.filename].labelRects = result.labeledBoxes; | ||
} | ||
} | ||
|
||
onSuccess( | ||
Array.from(Object.values(inputImagesData)), | ||
Array.from(Object.values(results.labelNames)) | ||
); | ||
}).catch((error: Error) => onFailure(error)); | ||
} catch (error) { | ||
onFailure(error as Error) | ||
} | ||
} | ||
|
||
private loadAndParseFiles(files: File[]): Promise<VOCImportResult> { | ||
return Promise.all(files.map((file: File) => file.text())).then((fileTexts: string[]) => | ||
fileTexts.reduce((current: VOCImportResult, fileText: string, currentIndex: number) => | ||
{ | ||
const fileName = files[currentIndex].name; | ||
try { | ||
return VOCImporter.parseDocumentIntoImageData(VOCImporter.tryParseVOCDocument(fileText), current); | ||
} catch (e) { | ||
if (e instanceof DocumentParsingError) { | ||
throw new DocumentParsingError(`Failed trying to parse ${fileName} as VOC XML document.`) | ||
} else if (e instanceof AnnotationAssertionError) { | ||
throw new AnnotationAssertionError(`Failed trying to find required VOC annotations for ${fileName}.`) | ||
} else { | ||
throw e; | ||
} | ||
} | ||
}, | ||
{ | ||
labelNames: {}, | ||
fileParseResults: [], | ||
} as VOCImportResult) | ||
); | ||
} | ||
|
||
private static tryParseVOCDocument(fileText: string): Document { | ||
try { | ||
return parser.parseFromString(fileText, 'application/xml'); | ||
} catch { | ||
throw new DocumentParsingError(); | ||
} | ||
} | ||
|
||
protected static parseDocumentIntoImageData(document: Document, { fileParseResults, labelNames }: VOCImportResult): VOCImportResult { | ||
try { | ||
const root = document.getElementsByTagName('annotation')[0]; | ||
const filename = root.getElementsByTagName('filename')[0].textContent; | ||
const [labeledBoxes, newLabelNames] = this.parseAnnotationsFromFileString(document, labelNames); | ||
|
||
return { | ||
labelNames: newLabelNames, | ||
fileParseResults: fileParseResults.concat({ | ||
filename, | ||
labeledBoxes | ||
}), | ||
}; | ||
} catch { | ||
throw new AnnotationAssertionError(); | ||
} | ||
} | ||
|
||
protected static parseAnnotationsFromFileString(document: Document, labelNames: Record<string, LabelName>): | ||
[LabelRect[], Record<string, LabelName>] { | ||
const newLabelNames: Record<string, LabelName> = Object.assign({}, labelNames); | ||
return [Array.from(document.getElementsByTagName('object')).map(d => { | ||
const labelName = d.getElementsByTagName('name')[0].textContent; | ||
const bbox = d.getElementsByTagName('bndbox')[0]; | ||
const xmin = parseInt(bbox.getElementsByTagName('xmin')[0].textContent); | ||
const xmax = parseInt(bbox.getElementsByTagName('xmax')[0].textContent); | ||
const ymin = parseInt(bbox.getElementsByTagName('ymin')[0].textContent); | ||
const ymax = parseInt(bbox.getElementsByTagName('ymax')[0].textContent); | ||
const rect = { | ||
x: xmin, | ||
y: ymin, | ||
height: ymax - ymin, | ||
width: xmax - xmin, | ||
}; | ||
|
||
if (!newLabelNames[labelName]) { | ||
newLabelNames[labelName] = LabelUtil.createLabelName(labelName); | ||
} | ||
|
||
const labelId = newLabelNames[labelName].id; | ||
return LabelUtil.createLabelRect(labelId, rect); | ||
}), newLabelNames]; | ||
} | ||
|
||
private static mapImageData(): Record<string, ImageData> { | ||
return LabelsSelector.getImagesData().reduce( | ||
(imageDataMap: Record<string, ImageData>, imageData: ImageData) => { | ||
imageDataMap[imageData.fileData.name] = imageData; | ||
return imageDataMap; | ||
}, {} | ||
); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters