diff --git a/README.md b/README.md
index d3cb5d21d..5976a1f36 100644
--- a/README.md
+++ b/README.md
@@ -23,7 +23,7 @@ You can find out more about our tool from the newly released [documentation][14]
[makesense.ai][1] strives to significantly reduce the time you have to spend on photo labeling. We are doing our best to integrate the latest and greatest AI models, that can give you recommendations as well as automate repetitive and tedious activities.
* [YOLOv5][16] is our most powerful integration yet. Thanks to the use of [yolov5js][17] you can load not only pretrained models from [yolov5js-zoo](18), but above all your own models trained thanks to YOLOv5 and [exported](19) to tfjs format.
-* [SSD][8] pretrained on the [COCO dataset][9], which will do some of the work for you in drawing bounding boxes on photos and also (in some cases) suggest a label.
+* [SSD][8] pretrained on the [COCO dataset][9], which will do some of the work for you in drawing bounding boxes on photos and also (in some cases) suggest a label.
* [PoseNet][11] is a vision model that can be used to estimate the pose of a person in an image or video by estimating where key body joints are.
The engine that drives our AI functionalities is [TensorFlow.js][10] - JS version of the most popular framework for training neural networks. This choice allows us not only to speed up your work but also to care about the privacy of your data, because unlike with other commercial and open-source tools, your photos do not have to be transferred to the server. This time AI comes to your device!
@@ -59,7 +59,11 @@ docker run -dit -p 3000:3000 --restart=always --name=make-sense make-sense
# Get Docker Container Logs
docker logs make-sense
+# Stop
+docker rm make-sense -f
+
# Access make-sense: http://localhost:3000/
+
```
## ⌨️ Keyboard Shortcuts
@@ -86,13 +90,13 @@ docker logs make-sense
| **Point** | ✓ | ✗ | ☐ | ☐ | ☐ | ✗ |
| **Line** | ✓ | ✗ | ✗ | ✗ | ✗ | ✗ |
| **Rect** | ✓ | ✓ | ✓ | ☐ | ☐ | ✗ |
-| **Polygon** | ☐ | ✗ | ☐ | ✓ | ✓ | ☐ |
+| **Polygon** | ☐ | ✓ | ☐ | ✓ | ✓ | ☐ |
| **Label** | ✓ | ✗ | ✗ | ✗ | ✗ | ✗ |
**Table 2.** The matrix of supported labels export formats, where:
* ✓ - supported format
* ☐ - not yet supported format
-* ✗ - format does not make sense for a given label type
+* ✗ - format does not make sense for a given label type
You can find examples of export files along with a description and schema on our [Wiki][7].
@@ -103,13 +107,13 @@ You can find examples of export files along with a description and schema on our
| **Point** | ☐ | ✗ | ☐ | ☐ | ☐ | ✗ |
| **Line** | ☐ | ✗ | ✗ | ✗ | ✗ | ✗ |
| **Rect** | ☐ | ✓ | ✓ | ☐ | ✓ | ✗ |
-| **Polygon** | ☐ | ✗ | ☐ | ☐ | ✓ | ☐ |
+| **Polygon** | ☐ | ✓ | ☐ | ☐ | ✓ | ☐ |
| **Label** | ☐ | ✗ | ✗ | ✗ | ✗ | ✗ |
**Table 3.** The matrix of supported labels import formats
* ✓ - supported format
* ☐ - not yet supported format
-* ✗ - format does not make sense for a given label type
+* ✗ - format does not make sense for a given label type
## 🔐 Privacy
@@ -122,7 +126,7 @@ If you are just starting your adventure with deep learning and would like to lea
## 🏆 Contribution
-
+
@@ -161,6 +165,6 @@ This project is licensed under the GPL-3.0 License - see the [LICENSE][2] file f
[14]: https://skalskip.github.io/make-sense/
[15]: https://github.com/SkalskiP/make-sense/issues
[16]: https://github.com/ultralytics/yolov5
-[17]: https://github.com/SkalskiP/yolov5js
+[17]: https://github.com/SkalskiP/yolov5js
[18]: https://github.com/SkalskiP/yolov5js-zoo
[19]: https://github.com/ultralytics/yolov5/blob/master/export.py
diff --git a/docker/Dockerfile b/docker/Dockerfile
index 69029205f..f16e428cc 100644
--- a/docker/Dockerfile
+++ b/docker/Dockerfile
@@ -1,6 +1,5 @@
-FROM node:16.16.0
-
-RUN apt-get update && apt-get -y install git && rm -rf /var/lib/apt/lists/*
+FROM node:20-alpine
+RUN apk add --no-cache git
COPY ./ /make-sense
diff --git a/src/data/ExportFormatData.ts b/src/data/ExportFormatData.ts
index 540536a58..47e6d6052 100644
--- a/src/data/ExportFormatData.ts
+++ b/src/data/ExportFormatData.ts
@@ -39,6 +39,10 @@ export const ExportFormatData: ExportFormatDataMap = {
{
type: AnnotationFormatType.COCO,
label: 'Single file in COCO JSON format.'
+ },
+ {
+ type: AnnotationFormatType.YOLO_POLYGON,
+ label: 'A .zip package containing files in YOLO polygon format.'
}
],
[LabelType.IMAGE_RECOGNITION]: [
diff --git a/src/data/ImportFormatData.ts b/src/data/ImportFormatData.ts
index c2ae4f16b..8399e15b3 100644
--- a/src/data/ImportFormatData.ts
+++ b/src/data/ImportFormatData.ts
@@ -25,6 +25,10 @@ export const ImportFormatData: ImportFormatDataMap = {
{
type: AnnotationFormatType.COCO,
label: 'Single file in COCO JSON format.'
+ },
+ {
+ type: AnnotationFormatType.YOLO_POLYGON,
+ label: 'Multiple files in YOLO polygon format along with labels names definition - labels.txt file.'
}
],
[LabelType.IMAGE_RECOGNITION]: []
diff --git a/src/data/ImporterSpecData.ts b/src/data/ImporterSpecData.ts
index 174e12be4..02b598ae6 100644
--- a/src/data/ImporterSpecData.ts
+++ b/src/data/ImporterSpecData.ts
@@ -3,6 +3,7 @@ import {AnnotationImporter} from '../logic/import/AnnotationImporter';
import {COCOImporter} from '../logic/import/coco/COCOImporter';
import {YOLOImporter} from '../logic/import/yolo/YOLOImporter';
import {VOCImporter} from '../logic/import/voc/VOCImporter';
+import {YOLOPolygonImporter} from '../logic/import/yolo/YOLOPolygonImporter';
export type ImporterSpecDataMap = Record;
@@ -13,5 +14,6 @@ export const ImporterSpecData: ImporterSpecDataMap = {
[AnnotationFormatType.JSON]: undefined,
[AnnotationFormatType.VGG]: undefined,
[AnnotationFormatType.VOC]: VOCImporter,
- [AnnotationFormatType.YOLO]: YOLOImporter
+ [AnnotationFormatType.YOLO]: YOLOImporter,
+ [AnnotationFormatType.YOLO_POLYGON]: YOLOPolygonImporter,
}
diff --git a/src/data/enums/AnnotationFormatType.ts b/src/data/enums/AnnotationFormatType.ts
index 2e98b2485..51cb6771e 100644
--- a/src/data/enums/AnnotationFormatType.ts
+++ b/src/data/enums/AnnotationFormatType.ts
@@ -4,5 +4,6 @@ export enum AnnotationFormatType {
CSV = 'CSV',
JSON = 'JSON',
VOC = 'VOC',
- VGG = 'VGG'
-}
\ No newline at end of file
+ VGG = 'VGG',
+ YOLO_POLYGON = 'YOLO_POLYGON',
+}
diff --git a/src/logic/export/polygon/PolygonLabelsExporter.ts b/src/logic/export/polygon/PolygonLabelsExporter.ts
index 01ad7f829..12a60bc15 100644
--- a/src/logic/export/polygon/PolygonLabelsExporter.ts
+++ b/src/logic/export/polygon/PolygonLabelsExporter.ts
@@ -1,6 +1,7 @@
import {AnnotationFormatType} from "../../../data/enums/AnnotationFormatType";
import {VGGExporter} from "./VGGExporter";
import {COCOExporter} from "./COCOExporter";
+import {YOLOPolygonExporter} from "./YOLOPolygonExporter";
export class PolygonLabelsExporter {
public static export(exportFormatType: AnnotationFormatType): void {
@@ -11,8 +12,11 @@ export class PolygonLabelsExporter {
case AnnotationFormatType.COCO:
COCOExporter.export();
break;
+ case AnnotationFormatType.YOLO_POLYGON:
+ YOLOPolygonExporter.export();
+ break;
default:
return;
}
}
-}
\ No newline at end of file
+}
diff --git a/src/logic/export/polygon/YOLOPolygonExporter.ts b/src/logic/export/polygon/YOLOPolygonExporter.ts
new file mode 100644
index 000000000..554dd3789
--- /dev/null
+++ b/src/logic/export/polygon/YOLOPolygonExporter.ts
@@ -0,0 +1,89 @@
+import {ImageData, LabelName, LabelPolygon} from '../../../store/labels/types';
+import {ImageRepository} from '../../imageRepository/ImageRepository';
+import JSZip from 'jszip';
+import {saveAs} from 'file-saver';
+import {LabelsSelector} from '../../../store/selectors/LabelsSelector';
+import {ExporterUtil} from '../../../utils/ExporterUtil';
+import {findIndex} from 'lodash';
+import {ISize} from '../../../interfaces/ISize';
+import {NumberUtil} from '../../../utils/NumberUtil';
+import {IPoint} from '../../../interfaces/IPoint';
+
+export class YOLOPolygonExporter {
+ public static export(): void {
+ const zip = new JSZip();
+ LabelsSelector.getImagesData()
+ .forEach((imageData: ImageData) => {
+ const fileContent: string = YOLOPolygonExporter.wrapPolygonLabelsIntoYOLO(imageData);
+ if (fileContent) {
+ const fileName: string = imageData.fileData.name.replace(/\.[^/.]+$/, '.txt');
+ try {
+ zip.file(fileName, fileContent);
+ } catch (error) {
+ throw new Error(error as string);
+ }
+ }
+ });
+
+ try {
+ zip.generateAsync({type: 'blob'})
+ .then((content: Blob) => {
+ saveAs(content, `${ExporterUtil.getExportFileName()}.zip`);
+ });
+ } catch (error) {
+ throw new Error(error as string);
+ }
+ }
+
+ public static wrapPolygonLabelIntoYOLO(
+ labelPolygon: LabelPolygon,
+ labelNames: LabelName[],
+ imageSize: ISize
+ ): string {
+ const snapAndFix = (value: number) => NumberUtil.snapValueToRange(value, 0, 1).toFixed(6);
+ const classIdx: string = findIndex(labelNames, {id: labelPolygon.labelId}).toString();
+
+ // Convert vertices to normalized coordinates
+ const normalizedCoords: string[] = [];
+
+ // Process vertices, excluding the last one if it's a duplicate of the first (closing point)
+ const vertices = labelPolygon.vertices;
+ const vertexCount = vertices.length;
+ const lastIndex = vertexCount - 1;
+
+ // Check if last vertex is duplicate of first (polygon closed)
+ const isDuplicate = vertexCount > 1 &&
+ vertices[0].x === vertices[lastIndex].x &&
+ vertices[0].y === vertices[lastIndex].y;
+
+ const endIndex = isDuplicate ? lastIndex : vertexCount;
+
+ for (let i = 0; i < endIndex; i++) {
+ const vertex = vertices[i];
+ const normalizedX = vertex.x / imageSize.width;
+ const normalizedY = vertex.y / imageSize.height;
+
+ normalizedCoords.push(snapAndFix(normalizedX));
+ normalizedCoords.push(snapAndFix(normalizedY));
+ }
+
+ return [classIdx, ...normalizedCoords].join(' ');
+ }
+
+ private static wrapPolygonLabelsIntoYOLO(imageData: ImageData): string {
+ if (imageData.labelPolygons.length === 0 || !imageData.loadStatus)
+ return null;
+
+ const labelNames: LabelName[] = LabelsSelector.getLabelNames();
+ const image: HTMLImageElement = ImageRepository.getById(imageData.id);
+ const imageSize: ISize = {width: image.width, height: image.height};
+
+ const labelPolygonsString: string[] = imageData.labelPolygons
+ .filter((labelPolygon: LabelPolygon) => labelPolygon.labelId !== null)
+ .map((labelPolygon: LabelPolygon) => {
+ return YOLOPolygonExporter.wrapPolygonLabelIntoYOLO(labelPolygon, labelNames, imageSize);
+ });
+
+ return labelPolygonsString.join('\n');
+ }
+}
diff --git a/src/logic/import/yolo/YOLOPolygonImporter.ts b/src/logic/import/yolo/YOLOPolygonImporter.ts
new file mode 100644
index 000000000..f8e43a534
--- /dev/null
+++ b/src/logic/import/yolo/YOLOPolygonImporter.ts
@@ -0,0 +1,148 @@
+// YOLOPolygonImporter.ts
+import { v4 as uuidv4 } from 'uuid';
+import {LabelsSelector} from '../../../store/selectors/LabelsSelector';
+import {ImageData, LabelName} from '../../../store/labels/types';
+import {ArrayUtil, PartitionResult} from '../../../utils/ArrayUtil';
+import {ImageDataUtil} from '../../../utils/ImageDataUtil';
+import {LabelUtil} from '../../../utils/LabelUtil';
+import {LabelType} from '../../../data/enums/LabelType';
+import {AnnotationImporter, ImportResult} from '../AnnotationImporter';
+import {
+ NoLabelNamesFileProvidedError,
+ YOLOLabelsReadingError,
+ YOLOAnnotationsLoadingError
+} from './YOLOErrors';
+import {YOLOUtils} from './YOLOUtils';
+import {ISize} from '../../../interfaces/ISize';
+import {Settings} from '../../../settings/Settings';
+import {ImageRepository} from '../../imageRepository/ImageRepository';
+
+type LabelNameMap = { [classIdx: number]: LabelName; };
+type FileByName = { [name: string]: File; };
+
+export class YOLOPolygonImporter extends AnnotationImporter {
+ public import(
+ filesData: File[],
+ onSuccess: (imagesData: ImageData[], labelNames: LabelName[]) => any,
+ onFailure: (error?: Error) => any
+ ): void {
+ (async () => {
+ try {
+ // 1) Separate labels.txt and annotation .txt files
+ const labelsFile = filesData.find(f => f.name.toLowerCase() === 'labels.txt');
+ if (!labelsFile) throw new NoLabelNamesFileProvidedError();
+
+ const annotationFiles = filesData.filter(f =>
+ f !== labelsFile && f.name.toLowerCase().endsWith('.txt')
+ );
+ // Allow case when user imported only labels first; still useful to resolve label palette.
+ // But we’ll continue if there are annotation files.
+
+ // 2) Read label names
+ const labelNames = await this.readLabels(labelsFile);
+
+ // Map class index -> LabelName with deterministic colors
+ const labelNameMap: LabelNameMap = this.toLabelNameMap(labelNames);
+
+ // 3) Prepare image data maps
+ const inputImagesData: ImageData[] = LabelsSelector.getImagesData();
+ const cleanImageData: ImageData[] = inputImagesData.map((item: ImageData) =>
+ ImageDataUtil.cleanAnnotations(item)
+ );
+
+ // Build a quick lookup: annotationFileName -> File
+ const annByName: FileByName = annotationFiles.reduce((acc: FileByName, f: File) => {
+ acc[f.name] = f;
+ return acc;
+ }, {});
+
+ // 4) Load images into ImageRepository before accessing them
+ await ImageDataUtil.loadMissingImages(cleanImageData);
+
+ // 5) Apply polygons to images where we find a matching .txt
+ // A matching file is ".txt"
+ const updatedImages: ImageData[] = await this.applyYOLOPolygons(cleanImageData, annByName, labelNames);
+
+ // 6) Done
+ onSuccess(
+ ImageDataUtil.arrange(updatedImages, inputImagesData.map(i => i.id)),
+ Object.values(labelNameMap)
+ );
+ } catch (err) {
+ onFailure(err as Error);
+ }
+ })();
+ }
+
+ private async readLabels(labelsFile: File): Promise {
+ try {
+ const content = await this.readFileText(labelsFile);
+ return YOLOUtils.parseLabelsNamesFromString(content);
+ } catch {
+ throw new YOLOLabelsReadingError();
+ }
+ }
+
+ private toLabelNameMap(labelNames: LabelName[]): LabelNameMap {
+ // Preserve incoming ids/colors; if you want fresh palette per class index, regenerate here.
+ // Also ensure they have stable colors across sessions.
+ const map: LabelNameMap = {};
+ labelNames.forEach((ln, idx) => {
+ map[idx] = {
+ id: ln.id ?? uuidv4(),
+ name: ln.name,
+ color: ln.color ?? ArrayUtil.getByInfiniteIndex(Settings.LABEL_COLORS_PALETTE, idx)
+ };
+ });
+ return map;
+ }
+
+ private async applyYOLOPolygons(
+ images: ImageData[],
+ annByName: FileByName,
+ labelNames: LabelName[]
+ ): Promise {
+ const promises = images.map(async (img) => {
+ const stem = this.stemOf(img.fileData.name);
+ const annFile = annByName[`${stem}.txt`];
+ if (!annFile) return img; // no annotations for this image
+
+ const text = await this.readFileText(annFile);
+
+ // Get the actual loaded image from the repository
+ const image: HTMLImageElement = ImageRepository.getById(img.id);
+ const imageSize: ISize = {
+ width: image.width,
+ height: image.height
+ };
+
+ // Parse as polygons; each line is one instance polygon
+ const polygons = YOLOUtils.parseYOLOPolygonAnnotationsFromString(
+ text, labelNames, imageSize, img.fileData.name
+ );
+
+ // Push to image
+ polygons.forEach(poly => {
+ img.labelPolygons.push(poly);
+ });
+
+ return img;
+ });
+
+ return Promise.all(promises);
+ }
+
+ private stemOf(filename: string): string {
+ const idx = filename.lastIndexOf('.');
+ return idx === -1 ? filename : filename.slice(0, idx);
+ }
+
+ private readFileText(file: File): Promise {
+ return new Promise((resolve, reject) => {
+ const reader = new FileReader();
+ reader.onerror = () => reject(new YOLOAnnotationsLoadingError(`Unable to read ${file.name}`));
+ reader.onloadend = (evt: any) => resolve(evt.target.result as string);
+ reader.readAsText(file);
+ });
+ }
+}
diff --git a/src/logic/import/yolo/YOLOUtils.ts b/src/logic/import/yolo/YOLOUtils.ts
index 7d540f37d..65c9c1041 100644
--- a/src/logic/import/yolo/YOLOUtils.ts
+++ b/src/logic/import/yolo/YOLOUtils.ts
@@ -1,7 +1,9 @@
-import {LabelName, LabelRect} from '../../../store/labels/types';
+// YOLOUtils.ts
+import {LabelName, LabelRect, LabelPolygon} from '../../../store/labels/types';
import {LabelUtil} from '../../../utils/LabelUtil';
import {AnnotationsParsingError, LabelNamesNotUniqueError} from './YOLOErrors';
import {ISize} from '../../../interfaces/ISize';
+import {IPoint} from '../../../interfaces/IPoint';
import {uniq} from 'lodash';
export class YOLOUtils {
@@ -9,14 +11,13 @@ export class YOLOUtils {
const labelNames: string[] = content
.split(/[\r\n]/)
.filter(Boolean)
- .map((name: string) => name.replace(/\s/g, ''))
+ .map((name: string) => name.replace(/\s/g, ''));
if (uniq(labelNames).length !== labelNames.length) {
- throw new LabelNamesNotUniqueError()
+ throw new LabelNamesNotUniqueError();
}
- return labelNames
- .map((name: string) => LabelUtil.createLabelName(name))
+ return labelNames.map((name: string) => LabelUtil.createLabelName(name));
}
public static loadLabelsList(
@@ -27,11 +28,11 @@ export class YOLOUtils {
const reader = new FileReader();
reader.onloadend = (evt: ProgressEvent) => {
try {
- const content: string = evt.target.result as string;
+ const content: string = evt.target!.result as string;
const labelNames = YOLOUtils.parseLabelsNamesFromString(content);
onSuccess(labelNames);
} catch (error) {
- onFailure(error as Error)
+ onFailure(error as Error);
}
};
reader.readAsText(fileData);
@@ -46,9 +47,11 @@ export class YOLOUtils {
return rawAnnotations
.split(/[\r\n]/)
.filter(Boolean)
- .map((rawAnnotation: string) => YOLOUtils.parseYOLOAnnotationFromString(
- rawAnnotation, labelNames, imageSize, imageName
- ));
+ .map((rawAnnotation: string) =>
+ YOLOUtils.parseYOLOAnnotationFromString(
+ rawAnnotation, labelNames, imageSize, imageName
+ )
+ );
}
public static parseYOLOAnnotationFromString(
@@ -57,34 +60,35 @@ export class YOLOUtils {
imageSize: ISize,
imageName: string
): LabelRect {
- const components = rawAnnotation.split(' ');
- if (!YOLOUtils.validateYOLOAnnotationComponents(components, labelNames.length)) {
+ const components = rawAnnotation.trim().split(/\s+/);
+ if (!YOLOUtils.validateYOLOBboxComponents(components, labelNames.length)) {
throw new AnnotationsParsingError(imageName);
}
- const labelIndex: number = parseInt(components[0]);
+ const labelIndex: number = parseInt(components[0], 10);
const labelId: string = labelNames[labelIndex].id;
const rectX: number = parseFloat(components[1]);
const rectY: number = parseFloat(components[2]);
const rectWidth: number = parseFloat(components[3]);
const rectHeight: number = parseFloat(components[4]);
+
const rect = {
- x: (rectX - rectWidth /2) * imageSize.width,
- y: (rectY - rectHeight /2) * imageSize.height,
+ x: (rectX - rectWidth / 2) * imageSize.width,
+ y: (rectY - rectHeight / 2) * imageSize.height,
width: rectWidth * imageSize.width,
height: rectHeight * imageSize.height
- }
+ };
return LabelUtil.createLabelRect(labelId, rect);
}
- public static validateYOLOAnnotationComponents(components: string[], labelNamesCount: number): boolean {
+ public static validateYOLOBboxComponents(components: string[], labelNamesCount: number): boolean {
const validateCoordinateValue = (rawValue: string): boolean => {
const floatValue: number = Number(rawValue);
return !isNaN(floatValue) && 0.0 <= floatValue && floatValue <= 1.0;
- }
+ };
const validateLabelIdx = (rawValue: string): boolean => {
- const intValue: number = parseInt(rawValue);
+ const intValue: number = parseInt(rawValue, 10);
return !isNaN(intValue) && 0 <= intValue && intValue < labelNamesCount;
- }
+ };
return [
components.length === 5,
@@ -93,6 +97,88 @@ export class YOLOUtils {
validateCoordinateValue(components[2]),
validateCoordinateValue(components[3]),
validateCoordinateValue(components[4])
- ].every(Boolean)
+ ].every(Boolean);
+ }
+
+ // --------------------------
+ // polygon annotations
+ // format: " x1 y1 x2 y2 ... xn yn", normalized [0,1]
+ // n >= 3, and (components.length - 1) is even
+ // --------------------------
+ public static parseYOLOPolygonAnnotationsFromString(
+ rawAnnotations: string,
+ labelNames: LabelName[],
+ imageSize: ISize,
+ imageName: string
+ ): LabelPolygon[] {
+ return rawAnnotations
+ .split(/[\r\n]/)
+ .filter(Boolean)
+ .map((rawAnnotation: string) =>
+ YOLOUtils.parseYOLOPolygonAnnotationFromString(
+ rawAnnotation, labelNames, imageSize, imageName
+ )
+ );
+ }
+
+ public static parseYOLOPolygonAnnotationFromString(
+ rawAnnotation: string,
+ labelNames: LabelName[],
+ imageSize: ISize,
+ imageName: string
+ ): LabelPolygon {
+ const components = rawAnnotation.trim().split(/\s+/);
+
+ if (!YOLOUtils.validateYOLOPolygonComponents(components, labelNames.length)) {
+ throw new AnnotationsParsingError(imageName);
+ }
+
+ const labelIndex: number = parseInt(components[0], 10);
+ const labelId: string = labelNames[labelIndex].id;
+
+ const vertices: IPoint[] = YOLOUtils.componentsToVertices(components, imageSize);
+ if (vertices.length >= 3) {
+ const first = vertices[0];
+ const last = vertices[vertices.length - 1];
+ if (first.x !== last.x || first.y !== last.y) {
+ vertices.push({ x: first.x, y: first.y });
+ }
+ }
+ return LabelUtil.createLabelPolygon(labelId, vertices);
+ }
+
+ public static validateYOLOPolygonComponents(components: string[], labelNamesCount: number): boolean {
+ if (components.length < 1 + 6) return false; // class + at least 3 points
+ if ((components.length - 1) % 2 !== 0) return false; // pairs after class id
+
+ const validateLabelIdx = (rawValue: string): boolean => {
+ const intValue: number = parseInt(rawValue, 10);
+ return !isNaN(intValue) && 0 <= intValue && intValue < labelNamesCount;
+ };
+ const validateNorm = (rawValue: string): boolean => {
+ const v = Number(rawValue);
+ return !isNaN(v) && 0.0 <= v && v <= 1.0;
+ };
+
+ if (!validateLabelIdx(components[0])) return false;
+
+ for (let i = 1; i < components.length; i++) {
+ if (!validateNorm(components[i])) return false;
+ }
+ return true;
+ }
+
+ private static componentsToVertices(components: string[], imageSize: ISize): IPoint[] {
+ const clamp = (v: number, lo: number, hi: number) => Math.min(hi, Math.max(lo, v));
+ const verts: IPoint[] = [];
+ for (let i = 1; i < components.length; i += 2) {
+ const nx = parseFloat(components[i]);
+ const ny = parseFloat(components[i + 1]);
+ verts.push({
+ x: clamp(nx * imageSize.width, 0, imageSize.width),
+ y: clamp(ny * imageSize.height, 0, imageSize.height)
+ });
+ }
+ return verts;
}
}
diff --git a/src/settings/RenderEngineSettings.ts b/src/settings/RenderEngineSettings.ts
index 91c05c4ec..06f7e43ef 100644
--- a/src/settings/RenderEngineSettings.ts
+++ b/src/settings/RenderEngineSettings.ts
@@ -4,9 +4,9 @@ import {Settings} from './Settings';
export class RenderEngineSettings {
public static readonly LINE_THICKNESS: number = 2;
public static readonly lineActiveColor: string = Settings.PRIMARY_COLOR;
- public static readonly defaultLineColor: string = '#ffffff';
- public static readonly CROSS_HAIR_LINE_COLOR: string = '#ffffff';
- public static readonly crossHairPadding: number = 25;
+ public static readonly defaultLineColor: string = '#000000';
+ public static readonly CROSS_HAIR_LINE_COLOR: string = '#000000';
+ public static readonly crossHairPadding: number = 1;
public static readonly anchorSize: ISize = {
width: Settings.RESIZE_HANDLE_DIMENSION_PX,
height: Settings.RESIZE_HANDLE_DIMENSION_PX
@@ -19,14 +19,14 @@ export class RenderEngineSettings {
width: 100,
height: 100
};
- public static readonly defaultAnchorColor: string = '#ffffff';
+ public static readonly defaultAnchorColor: string = '#000000';
public static readonly inactiveAnchorColor: string = Settings.DARK_THEME_SECOND_COLOR;
- public static readonly DEFAULT_ANCHOR_COLOR: string = '#ffffff';
+ public static readonly DEFAULT_ANCHOR_COLOR: string = '#000000';
public static readonly ACTIVE_ANCHOR_COLOR: string = Settings.SECONDARY_COLOR;
public static readonly INACTIVE_ANCHOR_COLOR: string = Settings.DARK_THEME_SECOND_COLOR;
- public static readonly DEFAULT_LINE_COLOR: string = '#ffffff';
+ public static readonly DEFAULT_LINE_COLOR: string = '#000000';
public static readonly ACTIVE_LINE_COLOR: string = Settings.PRIMARY_COLOR;
- public static readonly INACTIVE_LINE_COLOR: string = '#ffffff';
+ public static readonly INACTIVE_LINE_COLOR: string = '#000000';
}
diff --git a/src/utils/FileUtil.ts b/src/utils/FileUtil.ts
index 4434af103..0bf5adcec 100644
--- a/src/utils/FileUtil.ts
+++ b/src/utils/FileUtil.ts
@@ -11,11 +11,15 @@ export class FileUtil {
public static loadImage(fileData: File): Promise {
return new Promise((resolve, reject) => {
- const url = URL.createObjectURL(fileData);
- const image = new Image();
- image.src = url;
- image.onload = () => resolve(image);
- image.onerror = reject;
+ const reader = new FileReader();
+ reader.onload = (e: ProgressEvent) => {
+ const image = new Image();
+ image.onload = () => resolve(image);
+ image.onerror = reject;
+ image.src = e.target.result as string;
+ };
+ reader.onerror = reject;
+ reader.readAsDataURL(fileData);
});
}
diff --git a/src/views/EditorView/Editor/Editor.tsx b/src/views/EditorView/Editor/Editor.tsx
index 671c1771b..70f48e9f7 100644
--- a/src/views/EditorView/Editor/Editor.tsx
+++ b/src/views/EditorView/Editor/Editor.tsx
@@ -166,19 +166,21 @@ class Editor extends React.Component {
EditorActions.fullRender();
};
+
private handleZoom = (event: WheelEvent) => {
+ // Zoom only when ctrl (Windows/Linux) or meta (Mac) is held
if (event.ctrlKey || (PlatformModel.isMac && event.metaKey)) {
- const scrollSign: number = Math.sign(event.deltaY);
- if ((PlatformModel.isMac && scrollSign === -1) || (!PlatformModel.isMac && scrollSign === 1)) {
- ViewPortActions.zoomOut();
- }
- else if ((PlatformModel.isMac && scrollSign === 1) || (!PlatformModel.isMac && scrollSign === -1)) {
- ViewPortActions.zoomIn();
+ const scrollSign = Math.sign(event.deltaY);
+ if (scrollSign > 0) {
+ ViewPortActions.zoomOut(); // Scrolling down or pinching in
+ } else if (scrollSign < 0) {
+ ViewPortActions.zoomIn(); // Scrolling up or pinching out
}
}
EditorModel.mousePositionOnViewPortContent = CanvasUtil.getMousePositionOnCanvasFromEvent(event, EditorModel.canvas);
};
+
private getOptionsPanels = () => {
const editorData: EditorData = EditorActions.getEditorData();
if (this.props.activeLabelType === LabelType.RECT) {