diff --git a/public/ico/camera.png b/public/ico/camera.png new file mode 100644 index 00000000..39fd878d Binary files /dev/null and b/public/ico/camera.png differ diff --git a/public/ico/tags.png b/public/ico/tags.png index 9d1ddfc8..1d8f7305 100644 Binary files a/public/ico/tags.png and b/public/ico/tags.png differ diff --git a/src/data/enums/ExportFormatType.ts b/src/data/enums/ExportFormatType.ts index 8465da26..c513cc35 100644 --- a/src/data/enums/ExportFormatType.ts +++ b/src/data/enums/ExportFormatType.ts @@ -2,6 +2,7 @@ export enum ExportFormatType { YOLO = "YOLO", COCO_JSON = "COCO_JSON", CSV = "CSV", + JSON = "JSON", VOC = "VOC", VGG_JSON = "VGG_JSON" } \ No newline at end of file diff --git a/src/data/enums/PopupWindowType.ts b/src/data/enums/PopupWindowType.ts index b1aebd81..40268b5a 100644 --- a/src/data/enums/PopupWindowType.ts +++ b/src/data/enums/PopupWindowType.ts @@ -1,10 +1,10 @@ export enum PopupWindowType { LOAD_LABEL_NAMES = "LOAD_LABEL_NAMES", - UPDATE_LABEL_NAMES = "UPDATE_LABEL_NAMES", + UPDATE_LABEL = "UPDATE_LABEL", SUGGEST_LABEL_NAMES = "SUGGEST_LABEL_NAMES", - LOAD_IMAGES = "LOAD_IMAGES", + IMPORT_IMAGES = "IMPORT_IMAGES", LOAD_AI_MODEL = "LOAD_AI_MODEL", - EXPORT_LABELS = "EXPORT_LABELS", + EXPORT_ANNOTATIONS = "EXPORT_ANNOTATIONS", INSERT_LABEL_NAMES = 'INSERT_LABEL_NAMES', EXIT_PROJECT = 'EXIT_PROJECT', LOADER = 'LOADER' diff --git a/src/data/export/TagExportFormatData.ts b/src/data/export/TagExportFormatData.ts index 7e52d797..faeb1460 100644 --- a/src/data/export/TagExportFormatData.ts +++ b/src/data/export/TagExportFormatData.ts @@ -5,5 +5,9 @@ export const TagExportFormatData: IExportFormat[] = [ { type: ExportFormatType.CSV, label: "Single CSV file." + }, + { + type: ExportFormatType.JSON, + label: "Single JSON file." } ]; \ No newline at end of file diff --git a/src/logic/actions/LabelActions.ts b/src/logic/actions/LabelActions.ts index 594306a4..3ddfac36 100644 --- a/src/logic/actions/LabelActions.ts +++ b/src/logic/actions/LabelActions.ts @@ -110,6 +110,9 @@ export class LabelActions { } else { return labelPolygon } + }), + labelNameIds: imageData.labelNameIds.filter((labelNameId: string) => { + return !labelNamesIds.includes(labelNameId) }) } } diff --git a/src/logic/export/TagLabelsExport.ts b/src/logic/export/TagLabelsExport.ts index 21a3b46d..45a6e998 100644 --- a/src/logic/export/TagLabelsExport.ts +++ b/src/logic/export/TagLabelsExport.ts @@ -2,7 +2,6 @@ import {ExportFormatType} from "../../data/enums/ExportFormatType"; import {LabelsSelector} from "../../store/selectors/LabelsSelector"; import {ImageData, LabelName} from "../../store/labels/types"; import {ExporterUtil} from "../../utils/ExporterUtil"; -import {ImageRepository} from "../imageRepository/ImageRepository"; import {findLast} from "lodash"; export class TagLabelsExporter { @@ -11,6 +10,9 @@ export class TagLabelsExporter { case ExportFormatType.CSV: TagLabelsExporter.exportAsCSV(); break; + case ExportFormatType.JSON: + TagLabelsExporter.exportAsJSON(); + break; default: return; } @@ -18,29 +20,52 @@ export class TagLabelsExporter { private static exportAsCSV(): void { const content: string = LabelsSelector.getImagesData() + .filter((imageData: ImageData) => { + return imageData.labelNameIds.length > 0 + }) .map((imageData: ImageData) => { - return TagLabelsExporter.wrapLineLabelsIntoCSV(imageData)}) - .filter((imageLabelData: string) => { - return !!imageLabelData}) + return TagLabelsExporter.wrapLabelNamesIntoCSV(imageData)}) .join("\n"); const fileName: string = `${ExporterUtil.getExportFileName()}.csv`; ExporterUtil.saveAs(content, fileName); } - private static wrapLineLabelsIntoCSV(imageData: ImageData): string { - if (imageData.labelTagId === null || !imageData.loadStatus) + private static exportAsJSON(): void { + const contentObjects: object[] = LabelsSelector.getImagesData() + .filter((imageData: ImageData) => { + return imageData.labelNameIds.length > 0 + }) + .map((imageData: ImageData) => { + return { + "image": imageData.fileData.name, + "annotations": TagLabelsExporter.wrapLabelNamesIntoJSON(imageData) + }}) + const content: string = JSON.stringify(contentObjects); + const fileName: string = `${ExporterUtil.getExportFileName()}.json`; + ExporterUtil.saveAs(content, fileName); + } + + private static wrapLabelNamesIntoCSV(imageData: ImageData): string { + if (imageData.labelNameIds.length === 0 || !imageData.loadStatus) return null; - const image: HTMLImageElement = ImageRepository.getById(imageData.id); const labelNames: LabelName[] = LabelsSelector.getLabelNames(); - const labelName: LabelName = findLast(labelNames, {id: imageData.labelTagId}); - const labelFields = !!labelName ? [ - labelName.name, + const annotations: string[] = imageData.labelNameIds.map((labelNameId: string) => { + return findLast(labelNames, {id: labelNameId}).name; + }) + const labelFields = annotations.length !== 0 ? [ imageData.fileData.name, - image.width.toString(), - image.height.toString() + `"[${annotations.toString()}]"` ] : []; return labelFields.join(",") + } + private static wrapLabelNamesIntoJSON(imageData: ImageData): string[] { + if (imageData.labelNameIds.length === 0 || !imageData.loadStatus) + return []; + const labelNames: LabelName[] = LabelsSelector.getLabelNames(); + return imageData.labelNameIds.map((labelNameId: string) => { + return findLast(labelNames, {id: labelNameId}).name; + }) } } \ No newline at end of file diff --git a/src/logic/export/polygon/__tests__/VGGExporter.test.ts b/src/logic/export/polygon/__tests__/VGGExporter.test.ts index 28c7e674..6a0a0e2e 100644 --- a/src/logic/export/polygon/__tests__/VGGExporter.test.ts +++ b/src/logic/export/polygon/__tests__/VGGExporter.test.ts @@ -35,7 +35,7 @@ describe("VGGExporter mapImageDataToVGG method", () => { labelRects: [], labelPolygons: [], labelLines: [], - labelTagId: null, + labelNameIds: [], fileData: {} as File, isVisitedByObjectDetector: true, isVisitedByPoseDetector: true @@ -73,7 +73,7 @@ describe("VGGExporter mapImageDataToVGG method", () => { } ], labelLines: [], - labelTagId: null, + labelNameIds: [], fileData: {} as File, isVisitedByObjectDetector: true, isVisitedByPoseDetector: true @@ -145,7 +145,7 @@ describe("VGGExporter mapImageDataToVGG method", () => { } ], labelLines: [], - labelTagId: null, + labelNameIds: [], fileData: {} as File, isVisitedByObjectDetector: true, isVisitedByPoseDetector: true diff --git a/src/logic/render/PrimaryEditorRenderEngine.ts b/src/logic/render/PrimaryEditorRenderEngine.ts index 7b517a5b..b028c640 100644 --- a/src/logic/render/PrimaryEditorRenderEngine.ts +++ b/src/logic/render/PrimaryEditorRenderEngine.ts @@ -9,6 +9,7 @@ import {RenderEngineConfig} from "../../settings/RenderEngineConfig"; import {IPoint} from "../../interfaces/IPoint"; import {GeneralSelector} from "../../store/selectors/GeneralSelector"; import {ProjectType} from "../../data/enums/ProjectType"; +import {PopupWindowType} from "../../data/enums/PopupWindowType"; export class PrimaryEditorRenderEngine extends BaseRenderEngine { private config: RenderEngineConfig = new RenderEngineConfig(); @@ -31,40 +32,49 @@ export class PrimaryEditorRenderEngine extends BaseRenderEngine { public render(data: EditorData): void { this.drawImage(EditorModel.image, ViewPortActions.calculateViewPortContentImageRect()); - this.renderCursor(data); + this.renderCrossHair(data); } - public renderCursor(data: EditorData): void { + public renderCrossHair(data: EditorData): void { + if (!this.shouldRenderCrossHair(data)) return; + + const mouse = RenderEngineUtil.setPointBetweenPixels(data.mousePositionOnViewPortContent); const drawLine = (startPoint: IPoint, endPoint: IPoint) => { - DrawUtil.drawLine(this.canvas, startPoint, endPoint, this.config.crossHairLineColor, 1) + DrawUtil.drawLine(this.canvas, startPoint, endPoint, this.config.crossHairLineColor, 2) } + drawLine( + {x: mouse.x, y: 0}, + {x: mouse.x - 1, y: mouse.y - this.config.crossHairPadding} + ) + drawLine( + {x: mouse.x, y: mouse.y + this.config.crossHairPadding}, + {x: mouse.x - 1, y: data.viewPortContentSize.height} + ) + drawLine( + {x: 0, y: mouse.y}, + {x: mouse.x - this.config.crossHairPadding, y: mouse.y - 1} + ) + drawLine( + {x: mouse.x + this.config.crossHairPadding, y: mouse.y}, + {x: data.viewPortContentSize.width, y: mouse.y - 1} + ) + } - const crossHairVisible = GeneralSelector.getCrossHairVisibleStatus(); - const imageDragMode = GeneralSelector.getImageDragModeStatus(); + public shouldRenderCrossHair(data: EditorData): boolean { + const isCrossHairVisible = GeneralSelector.getCrossHairVisibleStatus(); + const isImageInDragMode = GeneralSelector.getImageDragModeStatus(); const projectType: ProjectType = GeneralSelector.getProjectType(); - - if (!this.canvas || !crossHairVisible || imageDragMode || projectType === ProjectType.IMAGE_RECOGNITION) return; - + const activePopupType: PopupWindowType = GeneralSelector.getActivePopupType(); const isMouseOverCanvas: boolean = RenderEngineUtil.isMouseOverCanvas(data); - if (isMouseOverCanvas) { - const mouse = RenderEngineUtil.setPointBetweenPixels(data.mousePositionOnViewPortContent); - drawLine( - {x: mouse.x, y: 0}, - {x: mouse.x - 1, y: mouse.y - this.config.crossHairPadding} - ) - drawLine( - {x: mouse.x, y: mouse.y + this.config.crossHairPadding}, - {x: mouse.x - 1, y: data.viewPortContentSize.height} - ) - drawLine( - {x: 0, y: mouse.y}, - {x: mouse.x - this.config.crossHairPadding, y: mouse.y - 1} - ) - drawLine( - {x: mouse.x + this.config.crossHairPadding, y: mouse.y}, - {x: data.viewPortContentSize.width, y: mouse.y - 1} - ) - } + + return [ + !!this.canvas, + isCrossHairVisible, + !isImageInDragMode, + projectType !== ProjectType.IMAGE_RECOGNITION, + !activePopupType, + isMouseOverCanvas + ].every(Boolean) } public drawImage(image: HTMLImageElement, imageRect: IRect) { diff --git a/src/settings/Settings.ts b/src/settings/Settings.ts index 0cde8ec2..20add771 100644 --- a/src/settings/Settings.ts +++ b/src/settings/Settings.ts @@ -31,9 +31,9 @@ export class Settings { public static readonly RESIZE_HANDLE_HOVER_DIMENSION_PX = 16; public static readonly CLOSEABLE_POPUPS: PopupWindowType[] = [ - PopupWindowType.LOAD_IMAGES, - PopupWindowType.EXPORT_LABELS, + PopupWindowType.IMPORT_IMAGES, + PopupWindowType.EXPORT_ANNOTATIONS, PopupWindowType.EXIT_PROJECT, - PopupWindowType.UPDATE_LABEL_NAMES + PopupWindowType.UPDATE_LABEL ]; } \ No newline at end of file diff --git a/src/store/labels/types.ts b/src/store/labels/types.ts index ef91b288..9605121f 100644 --- a/src/store/labels/types.ts +++ b/src/store/labels/types.ts @@ -54,7 +54,7 @@ export type ImageData = { labelPoints: LabelPoint[]; labelLines: LabelLine[]; labelPolygons: LabelPolygon[]; - labelTagId: string; + labelNameIds: string[]; // SSD isVisitedByObjectDetector: boolean; diff --git a/src/utils/FileUtil.ts b/src/utils/FileUtil.ts index f6e9dd1e..ec65afba 100644 --- a/src/utils/FileUtil.ts +++ b/src/utils/FileUtil.ts @@ -11,7 +11,7 @@ export class FileUtil { labelPoints: [], labelLines: [], labelPolygons: [], - labelTagId: null, + labelNameIds: [], isVisitedByObjectDetector: false, isVisitedByPoseDetector: false } diff --git a/src/views/EditorView/EditorContainer/EditorContainer.tsx b/src/views/EditorView/EditorContainer/EditorContainer.tsx index 958d66c6..ccfda6df 100644 --- a/src/views/EditorView/EditorContainer/EditorContainer.tsx +++ b/src/views/EditorView/EditorContainer/EditorContainer.tsx @@ -63,7 +63,7 @@ const EditorContainer: React.FC = ( return <> { case LabelType.LINE: return imageData.labelLines.length > 0 case LabelType.NAME: - return imageData.labelTagId !== null + return imageData.labelNameIds.length > 0 case LabelType.POINT: return imageData.labelPoints .filter((labelPoint: LabelPoint) => labelPoint.status === LabelStatus.ACCEPTED) diff --git a/src/views/EditorView/SideNavigationBar/LabelInputField/LabelInputField.tsx b/src/views/EditorView/SideNavigationBar/LabelInputField/LabelInputField.tsx index 76f2fb28..f1970f78 100644 --- a/src/views/EditorView/SideNavigationBar/LabelInputField/LabelInputField.tsx +++ b/src/views/EditorView/SideNavigationBar/LabelInputField/LabelInputField.tsx @@ -69,7 +69,7 @@ class LabelInputField extends React.Component { private openDropdown = () => { if (LabelsSelector.getLabelNames().length === 0) { - this.props.updateActivePopupType(PopupWindowType.UPDATE_LABEL_NAMES); + this.props.updateActivePopupType(PopupWindowType.UPDATE_LABEL); } else { this.setState({isOpen: true}); window.addEventListener(EventType.MOUSE_DOWN, this.closeDropdown); diff --git a/src/views/EditorView/SideNavigationBar/TagLabelsList/TagLabelsList.tsx b/src/views/EditorView/SideNavigationBar/TagLabelsList/TagLabelsList.tsx index 02cb5dfa..dca5b50c 100644 --- a/src/views/EditorView/SideNavigationBar/TagLabelsList/TagLabelsList.tsx +++ b/src/views/EditorView/SideNavigationBar/TagLabelsList/TagLabelsList.tsx @@ -5,6 +5,7 @@ import Scrollbars from "react-custom-scrollbars"; import {updateImageDataById} from "../../../../store/labels/actionCreators"; import {AppState} from "../../../../store"; import {connect} from "react-redux"; +import {remove} from "lodash"; import './TagLabelsList.scss'; import classNames from "classnames"; import {ImageButton} from "../../../Common/ImageButton/ImageButton"; @@ -37,15 +38,15 @@ const TagLabelsList: React.FC = ( }; const onTagClick = (labelId: string) => { - if (imageData.labelTagId === labelId) { + if (imageData.labelNameIds.includes(labelId)) { updateImageDataById(imageData.id, { ...imageData, - labelTagId: null + labelNameIds: remove(imageData.labelNameIds, (element: string) => element !== labelId) }) } else { updateImageDataById(imageData.id, { ...imageData, - labelTagId: labelId + labelNameIds: imageData.labelNameIds.concat(labelId) }) } } @@ -54,13 +55,13 @@ const TagLabelsList: React.FC = ( return classNames( "TagItem", { - "active": imageData.labelTagId === labelId + "active": imageData.labelNameIds.includes(labelId) } ); }; const addNewOnClick = () => { - updateActivePopupType(PopupWindowType.UPDATE_LABEL_NAMES) + updateActivePopupType(PopupWindowType.UPDATE_LABEL) } const getChildren = () => { diff --git a/src/views/EditorView/StateBar/StateBar.tsx b/src/views/EditorView/StateBar/StateBar.tsx index d3759f05..120e4d6b 100644 --- a/src/views/EditorView/StateBar/StateBar.tsx +++ b/src/views/EditorView/StateBar/StateBar.tsx @@ -29,7 +29,7 @@ const StateBar: React.FC = ({imagesData, activeLabelType}) => { }, 0); const tagLabeledImages = imagesData.reduce((currentCount: number, currentImage: ImageData) => { - return currentCount + (currentImage.labelTagId !== null ? 1 : 0); + return currentCount + (currentImage.labelNameIds.length !== 0 ? 1 : 0); }, 0); const getProgress = () => { diff --git a/src/views/EditorView/TopNavigationBar/TopNavigationBar.tsx b/src/views/EditorView/TopNavigationBar/TopNavigationBar.tsx index 1f798da7..0c75ba17 100644 --- a/src/views/EditorView/TopNavigationBar/TopNavigationBar.tsx +++ b/src/views/EditorView/TopNavigationBar/TopNavigationBar.tsx @@ -62,19 +62,19 @@ const TopNavigationBar: React.FC = ({updateActivePopupType, updateProjec
updateActivePopupType(PopupWindowType.UPDATE_LABEL_NAMES)} + onClick={() => updateActivePopupType(PopupWindowType.UPDATE_LABEL)} /> updateActivePopupType(PopupWindowType.LOAD_IMAGES)} + onClick={() => updateActivePopupType(PopupWindowType.IMPORT_IMAGES)} /> updateActivePopupType(PopupWindowType.EXPORT_LABELS)} + onClick={() => updateActivePopupType(PopupWindowType.EXPORT_ANNOTATIONS)} /> = ({updateActiveImageIndex, addImageData, alt={"upload"} src={"img/box-opened.png"} /> -

Drop some images

+

Drop images

or

Click here to select them

; diff --git a/src/views/PopupView/ExportLabelsPopup/ExportLabelPopup.tsx b/src/views/PopupView/ExportLabelsPopup/ExportLabelPopup.tsx index 5b054ddd..ba53d828 100644 --- a/src/views/PopupView/ExportLabelsPopup/ExportLabelPopup.tsx +++ b/src/views/PopupView/ExportLabelsPopup/ExportLabelPopup.tsx @@ -145,7 +145,7 @@ const ExportLabelPopup: React.FC = ({activeLabelType}) => { return( = (
{ isUpdate ? - "You can now edit the label names you use to describe the objects in the photos. " : - "Before you start, you can create a list of labels you would like to use in your project. " + - "You can also load labels list from a file or create it along the way." + "You can now edit the label names you use to describe the objects in the photos. Use the + " + + "button to add a new empty text field." : + "Before you start, you can create a list of labels you plan to assign to objects in your " + + "project. You can also choose to skip that part for now and define label names as you go." } - Use the + button to add a new empty text field.
{Object.keys(labelNames).length !== 0 ? @@ -150,7 +150,7 @@ const InsertLabelNamesPopup: React.FC = ( return( = ({activePopupType}) => { switch (activePopupType) { case PopupWindowType.LOAD_LABEL_NAMES: return ; - case PopupWindowType.EXPORT_LABELS: + case PopupWindowType.EXPORT_ANNOTATIONS: return ; case PopupWindowType.INSERT_LABEL_NAMES: return ; - case PopupWindowType.UPDATE_LABEL_NAMES: + case PopupWindowType.UPDATE_LABEL: return ; case PopupWindowType.EXIT_PROJECT: return ; - case PopupWindowType.LOAD_IMAGES: + case PopupWindowType.IMPORT_IMAGES: return ; case PopupWindowType.LOAD_AI_MODEL: return ;