Skip to content

Commit

Permalink
feature/issue-171_invalid_YOLO_annotations_exported_for_bboxes_on_the… (
Browse files Browse the repository at this point in the history
#188)

* feature/issue-171_invalid_YOLO_annotations_exported_for_bboxes_on_the_edge_of_an_image

* done
  • Loading branch information
SkalskiP committed Aug 30, 2021
1 parent 02564af commit e75991a
Show file tree
Hide file tree
Showing 6 changed files with 200 additions and 14 deletions.
107 changes: 107 additions & 0 deletions src/logic/__tests__/export/RectLabelExport.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import {LabelName, LabelRect} from "../../../store/labels/types";
import {LabelStatus} from "../../../data/enums/LabelStatus";
import {ISize} from "../../../interfaces/ISize";
import {RectLabelsExporter} from "../../export/RectLabelsExporter";

const imageSize: ISize = {
width: 1920,
height: 1080
}
const labelNames: LabelName[] = [
{
id: "label-000",
name: "label-name-000"
},
{
id: "label-001",
name: "label-name-001"
},
{
id: "label-002",
name: "label-name-002"
},
{
id: "label-003",
name: "label-name-003"
},
]

describe('RectLabelsExporter wrapRectLabelIntoYOLO method', () => {
it('should produce correct single label entry given issue #171 example 1', () => {
// given
const labelRect: LabelRect = {
id: "label-rect-000",
labelId: "label-002",
rect: {
x: 444,
y: 998,
width: 90,
height: 82
},
isCreatedByAI: false,
status: LabelStatus.ACCEPTED,
suggestedLabel: "label-000"
}
// when
const result = RectLabelsExporter.wrapRectLabelIntoYOLO(labelRect, labelNames, imageSize)
// then
let [classIdx, x, y, width, height] = result.split(" ").map((value: string) => parseFloat(value))
expect(classIdx).toBe(2)
expect(x + width / 2 <= 1).toBeTruthy()
expect(x - width / 2 >= 0).toBeTruthy()
expect(y + height / 2 <= 1).toBeTruthy()
expect(y - height / 2 >= 0).toBeTruthy()
})

it('should produce correct single label entry given issue #171 example 2', () => {
// given
const labelRect: LabelRect = {
id: "label-rect-000",
labelId: "label-002",
rect: {
x: 1828,
y: 710,
width: 92,
height: 104
},
isCreatedByAI: false,
status: LabelStatus.ACCEPTED,
suggestedLabel: "label-000"
}
// when
const result = RectLabelsExporter.wrapRectLabelIntoYOLO(labelRect, labelNames, imageSize)
// then
let [classIdx, x, y, width, height] = result.split(" ").map((value: string) => parseFloat(value))
expect(classIdx).toBe(2)
expect(x + width / 2 <= 1).toBeTruthy()
expect(x - width / 2 >= 0).toBeTruthy()
expect(y + height / 2 <= 1).toBeTruthy()
expect(y - height / 2 >= 0).toBeTruthy()
})

it('should produce correct single label entry given issue #171 example 3', () => {
// given
const labelRect: LabelRect = {
id: "label-rect-000",
labelId: "label-002",
rect: {
x: 0,
y: 138,
width: 80,
height: 70
},
isCreatedByAI: false,
status: LabelStatus.ACCEPTED,
suggestedLabel: "label-000"
}
// when
const result = RectLabelsExporter.wrapRectLabelIntoYOLO(labelRect, labelNames, imageSize)
// then
let [classIdx, x, y, width, height] = result.split(" ").map((value: string) => parseFloat(value))
expect(classIdx).toBe(2)
expect(x + width / 2 <= 1).toBeTruthy()
expect(x - width / 2 >= 0).toBeTruthy()
expect(y + height / 2 <= 1).toBeTruthy()
expect(y - height / 2 >= 0).toBeTruthy()
})
})
38 changes: 29 additions & 9 deletions src/logic/export/RectLabelsExporter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ import {XMLSanitizerUtil} from "../../utils/XMLSanitizerUtil";
import {ExporterUtil} from "../../utils/ExporterUtil";
import {GeneralSelector} from "../../store/selectors/GeneralSelector";
import {findIndex, findLast} from "lodash";
import {ISize} from "../../interfaces/ISize";
import {NumberUtil} from "../../utils/NumberUtil";
import {RectUtil} from "../../utils/RectUtil";

export class RectLabelsExporter {
public static export(exportFormatType: AnnotationFormatType): void {
Expand Down Expand Up @@ -51,7 +54,30 @@ export class RectLabelsExporter {
// TODO
throw new Error(error);
}
}

public static wrapRectLabelIntoYOLO(labelRect: LabelRect, labelNames: LabelName[], imageSize: ISize): string {
const snapAndFix = (value: number) => NumberUtil.snapValueToRange(value,0, 1).toFixed(6)
const classIdx: string = findIndex(labelNames, {id: labelRect.labelId}).toString()
const rectCenter = RectUtil.getCenter(labelRect.rect)
const rectSize = RectUtil.getSize(labelRect.rect)
const rawBBox: number[] = [
rectCenter.x / imageSize.width,
rectCenter.y / imageSize.height,
rectSize.width / imageSize.width,
rectSize.height / imageSize.height
]

let [x, y, width, height] = rawBBox.map((value: number) => parseFloat(snapAndFix(value)))

if (x + width / 2 > 1) { width = 2 * (1 - x) }
if (x - width / 2 < 0) { width = 2 * x }
if (y + height / 2 > 1) { height = 2 * (1 - y) }
if (y - height / 2 < 0) { height = 2 * y }

const processedBBox = [x, y, width, height].map((value: number) => snapAndFix(value))

return [classIdx, ...processedBBox].join(" ")
}

private static wrapRectLabelsIntoYOLO(imageData: ImageData): string {
Expand All @@ -60,15 +86,9 @@ export class RectLabelsExporter {

const labelNames: LabelName[] = LabelsSelector.getLabelNames();
const image: HTMLImageElement = ImageRepository.getById(imageData.id);
const imageSize: ISize = {width: image.width, height: image.height}
const labelRectsString: string[] = imageData.labelRects.map((labelRect: LabelRect) => {
const labelFields = [
findIndex(labelNames, {id: labelRect.labelId}).toString(),
((labelRect.rect.x + labelRect.rect.width / 2) / image.width).toFixed(6).toString(),
((labelRect.rect.y + labelRect.rect.height / 2) / image.height).toFixed(6).toString(),
(labelRect.rect.width / image.width).toFixed(6).toString(),
(labelRect.rect.height / image.height).toFixed(6).toString()
];
return labelFields.join(" ")
return RectLabelsExporter.wrapRectLabelIntoYOLO(labelRect, labelNames, imageSize)
});
return labelRectsString.join("\n");
}
Expand Down Expand Up @@ -185,4 +205,4 @@ export class RectLabelsExporter {
});
return labelRectsString.join("\n");
}
}
}
3 changes: 1 addition & 2 deletions src/utils/NumberUtil.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,10 @@ export class NumberUtil {
return min;
if (value > max)
return max;

return value;
}

public static isValueInRange(value: number, min: number, max: number): boolean {
return value >= min && value <= max;
}
}
}
16 changes: 15 additions & 1 deletion src/utils/RectUtil.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,4 +162,18 @@ export class RectUtil {
y: NumberUtil.snapValueToRange(point.y, rect.y, rect.y + rect.height)
}
}
}

public static getCenter(rect: IRect): IPoint {
return {
x: rect.x + rect.width / 2,
y: rect.y + rect.height / 2
}
}

public static getSize(rect: IRect): ISize {
return {
width: rect.width,
height: rect.height
}
}
}
16 changes: 16 additions & 0 deletions src/utils/__tests__/NumberUtil.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import {NumberUtil} from "../NumberUtil";

describe('NumberUtil snapValueToRange method', () => {
it('should return value rounded to the upper bound', () => {
const result: number = NumberUtil.snapValueToRange(1.0000005, 0, 1)
expect(result).toBe(1);
});
it('should return value rounded to the lower bound', () => {
const result: number = NumberUtil.snapValueToRange(-0.0000005, 0, 1)
expect(result).toBe(0);
});
it('should return unmodified value', () => {
const result: number = NumberUtil.snapValueToRange(0.5, 0, 1)
expect(result).toBe(0.5);
});
});
34 changes: 32 additions & 2 deletions src/utils/__tests__/RectUtil.test.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,43 @@
import {RectUtil} from "../RectUtil";
import {IRect} from "../../interfaces/IRect";
import {IPoint} from "../../interfaces/IPoint";
import {ISize} from "../../interfaces/ISize";

describe('RectUtil getRatio method', () => {
it('should return correct value of rect ratio', () => {
// given
const rect: IRect = {x: 0, y: 0, width: 10, height: 5};
expect(RectUtil.getRatio(rect)).toBe(2);
// when
const result = RectUtil.getRatio(rect)
// then
expect(result).toBe(2);
});

it('should return null', () => {
expect(RectUtil.getRatio(null)).toBe(null);
});
});
});

describe('RectUtil getCenter method', () => {
it('should return correct center value', () => {
// given
const rect: IRect = {x: 0, y: 0, width: 10, height: 20};
const expectedResult: IPoint = {x: 5, y: 10};
// when
const result = RectUtil.getCenter(rect)
// then
expect(result).toMatchObject(expectedResult);
})
})

describe('RectUtil getSize method', () => {
it('should return correct size value', () => {
// given
const rect: IRect = {x: 0, y: 0, width: 10, height: 20};
const expectedSize: ISize = {width: 10, height: 20};
// when
const result = RectUtil.getSize(rect)
// then
expect(result).toMatchObject(expectedSize);
})
})

0 comments on commit e75991a

Please sign in to comment.