diff --git a/public/ico/cross-hair.png b/public/ico/cross-hair.png new file mode 100644 index 00000000..9aaf82c4 Binary files /dev/null and b/public/ico/cross-hair.png differ diff --git a/public/ico/line.png b/public/ico/line.png new file mode 100644 index 00000000..3bd66e37 Binary files /dev/null and b/public/ico/line.png differ diff --git a/public/ico/object.png b/public/ico/object.png index 0517470d..77024493 100644 Binary files a/public/ico/object.png and b/public/ico/object.png differ diff --git a/public/ico/point.png b/public/ico/point.png index 059c0fcf..b16db69d 100644 Binary files a/public/ico/point.png and b/public/ico/point.png differ diff --git a/src/data/enums/LabelType.ts b/src/data/enums/LabelType.ts index 5e3d1d03..5fc0bc15 100644 --- a/src/data/enums/LabelType.ts +++ b/src/data/enums/LabelType.ts @@ -2,5 +2,6 @@ export enum LabelType { NAME = "NAME", POINT = "POINT", RECTANGLE = "RECTANGLE", - POLYGON = "POLYGON" + POLYGON = "POLYGON", + LINE = "LINE" } \ No newline at end of file diff --git a/src/data/enums/LineAnchorType.ts b/src/data/enums/LineAnchorType.ts new file mode 100644 index 00000000..97b9a89d --- /dev/null +++ b/src/data/enums/LineAnchorType.ts @@ -0,0 +1,4 @@ +export enum LineAnchorType { + START = "START", + END = "END" +} \ No newline at end of file diff --git a/src/data/export/LineExportFormatData.ts b/src/data/export/LineExportFormatData.ts new file mode 100644 index 00000000..08e91599 --- /dev/null +++ b/src/data/export/LineExportFormatData.ts @@ -0,0 +1,9 @@ +import {IExportFormat} from "../../interfaces/IExportFormat"; +import {ExportFormatType} from "../enums/ExportFormatType"; + +export const LineExportFormatData: IExportFormat[] = [ + { + type: ExportFormatType.CSV, + label: "Single CSV file." + } +]; \ No newline at end of file diff --git a/src/data/export/TagExportFormatData.ts b/src/data/export/TagExportFormatData.ts new file mode 100644 index 00000000..7e52d797 --- /dev/null +++ b/src/data/export/TagExportFormatData.ts @@ -0,0 +1,9 @@ +import {IExportFormat} from "../../interfaces/IExportFormat"; +import {ExportFormatType} from "../enums/ExportFormatType"; + +export const TagExportFormatData: IExportFormat[] = [ + { + type: ExportFormatType.CSV, + label: "Single CSV file." + } +]; \ No newline at end of file diff --git a/src/data/info/EditorFeatureData.ts b/src/data/info/EditorFeatureData.ts index 9894ab88..c0ef5391 100644 --- a/src/data/info/EditorFeatureData.ts +++ b/src/data/info/EditorFeatureData.ts @@ -21,7 +21,7 @@ export const EditorFeatureData: IEditorFeature[] = [ imageAlt: "private", }, { - displayText: "Support multiple label types - bounding box, polygon, point", + displayText: "Support multiple label types - rects, lines, points and polygons", imageSrc: "img/labels.png", imageAlt: "labels", }, diff --git a/src/data/info/LabelToolkitData.ts b/src/data/info/LabelToolkitData.ts index 462bc711..528e2121 100644 --- a/src/data/info/LabelToolkitData.ts +++ b/src/data/info/LabelToolkitData.ts @@ -16,7 +16,7 @@ export const LabelToolkitData: ILabelToolkit[] = [ }, { labelType: LabelType.RECTANGLE, - headerText: "Bounding box", + headerText: "Rect", imageSrc: "ico/rectangle.png", imageAlt: "rectangle", }, @@ -26,6 +26,12 @@ export const LabelToolkitData: ILabelToolkit[] = [ imageSrc: "ico/point.png", imageAlt: "point", }, + { + labelType: LabelType.LINE, + headerText: "Line", + imageSrc: "ico/line.png", + imageAlt: "line", + }, { labelType: LabelType.POLYGON, headerText: "Polygon", diff --git a/src/logic/actions/EditorActions.ts b/src/logic/actions/EditorActions.ts index bbcff600..7725aab5 100644 --- a/src/logic/actions/EditorActions.ts +++ b/src/logic/actions/EditorActions.ts @@ -19,6 +19,7 @@ import {ImageUtil} from "../../utils/ImageUtil"; import {GeneralSelector} from "../../store/selectors/GeneralSelector"; import {ViewPortHelper} from "../helpers/ViewPortHelper"; import {CustomCursorStyle} from "../../data/enums/CustomCursorStyle"; +import {LineRenderEngine} from "../render/LineRenderEngine"; export class EditorActions { @@ -34,6 +35,9 @@ export class EditorActions { case LabelType.POINT: EditorModel.supportRenderingEngine = new PointRenderEngine(EditorModel.canvas); break; + case LabelType.LINE: + EditorModel.supportRenderingEngine = new LineRenderEngine(EditorModel.canvas); + break; case LabelType.POLYGON: EditorModel.supportRenderingEngine = new PolygonRenderEngine(EditorModel.canvas); break; diff --git a/src/logic/actions/LabelActions.ts b/src/logic/actions/LabelActions.ts index 90b2091b..594306a4 100644 --- a/src/logic/actions/LabelActions.ts +++ b/src/logic/actions/LabelActions.ts @@ -1,5 +1,5 @@ import {LabelsSelector} from "../../store/selectors/LabelsSelector"; -import {ImageData, LabelName, LabelPoint, LabelPolygon, LabelRect} from "../../store/labels/types"; +import {ImageData, LabelLine, LabelName, LabelPoint, LabelPolygon, LabelRect} from "../../store/labels/types"; import {filter} from "lodash"; import {store} from "../../index"; import {updateImageData, updateImageDataById} from "../../store/labels/actionCreators"; @@ -48,6 +48,17 @@ export class LabelActions { store.dispatch(updateImageDataById(imageData.id, newImageData)); } + public static deleteLineLabelById(imageId: string, labelLineId: string) { + const imageData: ImageData = LabelsSelector.getImageDataById(imageId); + const newImageData = { + ...imageData, + labelLines: filter(imageData.labelLines, (currentLabel: LabelLine) => { + return currentLabel.id !== labelLineId; + }) + }; + store.dispatch(updateImageDataById(imageData.id, newImageData)); + } + public static deletePolygonLabelById(imageId: string, labelPolygonId: string) { const imageData: ImageData = LabelsSelector.getImageDataById(imageId); const newImageData = { diff --git a/src/logic/context/EditorContext.ts b/src/logic/context/EditorContext.ts index f416b2bf..8e80ce6f 100644 --- a/src/logic/context/EditorContext.ts +++ b/src/logic/context/EditorContext.ts @@ -10,6 +10,7 @@ import {ViewPortActions} from "../actions/ViewPortActions"; import {Direction} from "../../data/enums/Direction"; import {PlatformUtil} from "../../utils/PlatformUtil"; import {LabelActions} from "../actions/LabelActions"; +import {LineRenderEngine} from "../render/LineRenderEngine"; export class EditorContext extends BaseContext { public static actions: HotKeyAction[] = [ @@ -26,8 +27,16 @@ export class EditorContext extends BaseContext { { keyCombo: ["Escape"], action: (event: KeyboardEvent) => { - if (EditorModel.supportRenderingEngine && EditorModel.supportRenderingEngine.labelType === LabelType.POLYGON) - (EditorModel.supportRenderingEngine as PolygonRenderEngine).cancelLabelCreation(); + if (EditorModel.supportRenderingEngine) { + switch (EditorModel.supportRenderingEngine.labelType) { + case LabelType.POLYGON: + (EditorModel.supportRenderingEngine as PolygonRenderEngine).cancelLabelCreation(); + break; + case LabelType.LINE: + (EditorModel.supportRenderingEngine as LineRenderEngine).cancelLabelCreation(); + break; + } + } EditorActions.fullRender(); } }, diff --git a/src/logic/export/LineLabelExport.ts b/src/logic/export/LineLabelExport.ts new file mode 100644 index 00000000..5a89e887 --- /dev/null +++ b/src/logic/export/LineLabelExport.ts @@ -0,0 +1,59 @@ +import {ExportFormatType} from "../../data/enums/ExportFormatType"; +import {LabelsSelector} from "../../store/selectors/LabelsSelector"; +import {ImageData, LabelLine, LabelName} from "../../store/labels/types"; +import {saveAs} from "file-saver"; +import {ExporterUtil} from "../../utils/ExporterUtil"; +import {ImageRepository} from "../imageRepository/ImageRepository"; +import {findLast} from "lodash"; + +export class LineLabelsExporter { + public static export(exportFormatType: ExportFormatType): void { + switch (exportFormatType) { + case ExportFormatType.CSV: + LineLabelsExporter.exportAsCSV(); + break; + default: + return; + } + } + + private static exportAsCSV(): void { + const content: string = LabelsSelector.getImagesData() + .map((imageData: ImageData) => { + return LineLabelsExporter.wrapLineLabelsIntoCSV(imageData)}) + .filter((imageLabelData: string) => { + return !!imageLabelData}) + .join("\n"); + + const blob = new Blob([content], {type: "text/plain;charset=utf-8"}); + try { + saveAs(blob, `${ExporterUtil.getExportFileName()}.csv`); + } catch (error) { + // TODO + throw new Error(error); + } + } + + private static wrapLineLabelsIntoCSV(imageData: ImageData): string { + if (imageData.labelLines.length === 0 || !imageData.loadStatus) + return null; + + const image: HTMLImageElement = ImageRepository.getById(imageData.id); + const labelNames: LabelName[] = LabelsSelector.getLabelNames(); + const labelLinesString: string[] = imageData.labelLines.map((labelLine: LabelLine) => { + const labelName: LabelName = findLast(labelNames, {id: labelLine.labelId}); + const labelFields = !!labelName ? [ + labelName.name, + Math.round(labelLine.line.start.x).toString(), + Math.round(labelLine.line.start.y).toString(), + Math.round(labelLine.line.end.x).toString(), + Math.round(labelLine.line.end.y).toString(), + imageData.fileData.name, + image.width.toString(), + image.height.toString() + ] : []; + return labelFields.join(",") + }); + return labelLinesString.join("\n"); + } +} \ No newline at end of file diff --git a/src/logic/export/PointLabelsExport.ts b/src/logic/export/PointLabelsExport.ts index 88335111..c2c9fbfd 100644 --- a/src/logic/export/PointLabelsExport.ts +++ b/src/logic/export/PointLabelsExport.ts @@ -44,11 +44,11 @@ export class PointLabelsExporter { const labelName: LabelName = findLast(labelNames, {id: labelPoint.labelId}); const labelFields = !!labelName ? [ labelName.name, - Math.round(labelPoint.point.x) + "", - Math.round(labelPoint.point.y) + "", + Math.round(labelPoint.point.x).toString(), + Math.round(labelPoint.point.y).toString(), imageData.fileData.name, - image.width + "", - image.height + "" + image.width.toString(), + image.height.toString() ] : []; return labelFields.join(",") }); diff --git a/src/logic/export/RectLabelsExporter.ts b/src/logic/export/RectLabelsExporter.ts index f4d154d2..466dcdd0 100644 --- a/src/logic/export/RectLabelsExporter.ts +++ b/src/logic/export/RectLabelsExporter.ts @@ -63,10 +63,10 @@ export class RectLabelsExporter { 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) + "", - ((labelRect.rect.y + labelRect.rect.height / 2) / image.height).toFixed(6) + "", - (labelRect.rect.width / image.width).toFixed(6) + "", - (labelRect.rect.height / image.height).toFixed(6) + "" + ((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(" ") }); @@ -179,13 +179,13 @@ export class RectLabelsExporter { const labelName: LabelName = findLast(labelNames, {id: labelRect.labelId}); const labelFields = !!labelName ? [ labelName.name, - Math.round(labelRect.rect.x) + "", - Math.round(labelRect.rect.y) + "", - Math.round(labelRect.rect.width) + "", - Math.round(labelRect.rect.height) + "", + Math.round(labelRect.rect.x).toString(), + Math.round(labelRect.rect.y).toString(), + Math.round(labelRect.rect.width).toString(), + Math.round(labelRect.rect.height).toString(), imageData.fileData.name, - image.width + "", - image.height + "" + image.width.toString(), + image.height.toString() ] : []; return labelFields.join(",") }); diff --git a/src/logic/export/TagLabelsExport.ts b/src/logic/export/TagLabelsExport.ts new file mode 100644 index 00000000..d2478c07 --- /dev/null +++ b/src/logic/export/TagLabelsExport.ts @@ -0,0 +1,53 @@ +import {ExportFormatType} from "../../data/enums/ExportFormatType"; +import {LabelsSelector} from "../../store/selectors/LabelsSelector"; +import {ImageData, LabelName} from "../../store/labels/types"; +import {saveAs} from "file-saver"; +import {ExporterUtil} from "../../utils/ExporterUtil"; +import {ImageRepository} from "../imageRepository/ImageRepository"; +import {findLast} from "lodash"; + +export class TagLabelsExporter { + public static export(exportFormatType: ExportFormatType): void { + switch (exportFormatType) { + case ExportFormatType.CSV: + TagLabelsExporter.exportAsCSV(); + break; + default: + return; + } + } + + private static exportAsCSV(): void { + const content: string = LabelsSelector.getImagesData() + .map((imageData: ImageData) => { + return TagLabelsExporter.wrapLineLabelsIntoCSV(imageData)}) + .filter((imageLabelData: string) => { + return !!imageLabelData}) + .join("\n"); + + const blob = new Blob([content], {type: "text/plain;charset=utf-8"}); + try { + saveAs(blob, `${ExporterUtil.getExportFileName()}.csv`); + } catch (error) { + // TODO + throw new Error(error); + } + } + + private static wrapLineLabelsIntoCSV(imageData: ImageData): string { + if (imageData.labelTagId === null || !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, + imageData.fileData.name, + image.width.toString(), + image.height.toString() + ] : []; + return labelFields.join(",") + + } +} \ No newline at end of file diff --git a/src/logic/export/__tests__/PolygonLabelsExporter.test.ts b/src/logic/export/__tests__/PolygonLabelsExporter.test.ts index 8df74e68..2853d79e 100644 --- a/src/logic/export/__tests__/PolygonLabelsExporter.test.ts +++ b/src/logic/export/__tests__/PolygonLabelsExporter.test.ts @@ -34,8 +34,11 @@ describe("PolygonLabelsExporter mapImageDataToVGG method", () => { labelPoints: [], labelRects: [], labelPolygons: [], + labelLines: [], + labelTagId: null, fileData: {} as File, - isVisitedByObjectDetector: true + isVisitedByObjectDetector: true, + isVisitedByPoseDetector: true }; expect(PolygonLabelsExporter.mapImageDataToVGG(givenImageData, [])).toBeNull(); }); @@ -69,8 +72,11 @@ describe("PolygonLabelsExporter mapImageDataToVGG method", () => { ] } ], + labelLines: [], + labelTags: [], fileData: {} as File, - isVisitedByObjectDetector: true + isVisitedByObjectDetector: true, + isVisitedByPoseDetector: true }; const givenLabelNames: LabelName[] = [ @@ -138,8 +144,11 @@ describe("PolygonLabelsExporter mapImageDataToVGG method", () => { ] } ], + labelLines: [], + labelTags: [], fileData: {} as File, - isVisitedByObjectDetector: true + isVisitedByObjectDetector: true, + isVisitedByPoseDetector: true }; const givenLabelNames: LabelName[] = [ diff --git a/src/logic/render/LineRenderEngine.ts b/src/logic/render/LineRenderEngine.ts new file mode 100644 index 00000000..bf300549 --- /dev/null +++ b/src/logic/render/LineRenderEngine.ts @@ -0,0 +1,297 @@ +import {BaseRenderEngine} from "./BaseRenderEngine"; +import {RenderEngineConfig} from "../../settings/RenderEngineConfig"; +import {LabelType} from "../../data/enums/LabelType"; +import {EditorData} from "../../data/EditorData"; +import {RenderEngineUtil} from "../../utils/RenderEngineUtil"; +import {ImageData, LabelLine} from "../../store/labels/types"; +import {IPoint} from "../../interfaces/IPoint"; +import {RectUtil} from "../../utils/RectUtil"; +import {store} from "../../index"; +import { + updateActiveLabelId, + updateFirstLabelCreatedFlag, + updateHighlightedLabelId, + updateImageDataById +} from "../../store/labels/actionCreators"; +import {EditorActions} from "../actions/EditorActions"; +import {LabelsSelector} from "../../store/selectors/LabelsSelector"; +import {DrawUtil} from "../../utils/DrawUtil"; +import {GeneralSelector} from "../../store/selectors/GeneralSelector"; +import uuidv1 from "uuid/v1"; +import {ILine} from "../../interfaces/ILine"; +import {LineUtil} from "../../utils/LineUtil"; +import {IRect} from "../../interfaces/IRect"; +import {updateCustomCursorStyle} from "../../store/general/actionCreators"; +import {CustomCursorStyle} from "../../data/enums/CustomCursorStyle"; +import {LineAnchorType} from "../../data/enums/LineAnchorType"; + +export class LineRenderEngine extends BaseRenderEngine { + private config: RenderEngineConfig = new RenderEngineConfig(); + + // ================================================================================================================= + // STATE + // ================================================================================================================= + + private lineCreationStartPoint: IPoint; + private lineUpdateAnchorType: LineAnchorType; + + public constructor(canvas: HTMLCanvasElement) { + super(canvas); + this.labelType = LabelType.LINE; + } + + // ================================================================================================================= + // EVENT HANDLERS + // ================================================================================================================= + + public mouseDownHandler(data: EditorData): void { + const isMouseOverImage: boolean = RenderEngineUtil.isMouseOverImage(data); + const isMouseOverCanvas: boolean = RenderEngineUtil.isMouseOverCanvas(data); + const anchorTypeUnderMouse = this.getAnchorTypeUnderMouse(data); + const labelLineUnderMouse: LabelLine = this.getLineUnderMouse(data); + + if (isMouseOverCanvas) { + if (!!anchorTypeUnderMouse && !this.isResizeInProgress()) { + const labelLine: LabelLine = this.getLineUnderMouse(data); + this.startExistingLabelUpdate(labelLine.id, anchorTypeUnderMouse) + } else if (!!labelLineUnderMouse) { + store.dispatch(updateActiveLabelId(labelLineUnderMouse.id)); + } else if (!this.isInProgress() && isMouseOverImage) { + this.startNewLabelCreation(data) + } else { + this.finishNewLabelCreation(data); + } + } + } + + public mouseUpHandler(data: EditorData): void { + if (this.isResizeInProgress()) { + this.endExistingLabelUpdate(data) + } + } + + public mouseMoveHandler(data: EditorData): void { + const isOverImage: boolean = RenderEngineUtil.isMouseOverImage(data); + if (isOverImage) { + const labelLine: LabelLine = this.getLineUnderMouse(data); + if (!!labelLine) { + if (LabelsSelector.getHighlightedLabelId() !== labelLine.id) { + store.dispatch(updateHighlightedLabelId(labelLine.id)) + } + } else { + if (LabelsSelector.getHighlightedLabelId() !== null) { + store.dispatch(updateHighlightedLabelId(null)); + } + } + } + } + + // ================================================================================================================= + // RENDERING + // ================================================================================================================= + + public render(data: EditorData): void { + this.drawExistingLabels(data); + this.drawActivelyCreatedLabel(data) + this.drawActivelyResizeLabel(data) + this.updateCursorStyle(data); + } + + private drawExistingLabels(data: EditorData) { + const activeLabelId: string = LabelsSelector.getActiveLabelId(); + const highlightedLabelId: string = LabelsSelector.getHighlightedLabelId(); + const imageData: ImageData = LabelsSelector.getActiveImageData(); + imageData.labelLines.forEach((labelLine: LabelLine) => { + const isActive: boolean = labelLine.id === activeLabelId || labelLine.id === highlightedLabelId; + const lineOnCanvas = RenderEngineUtil.transferLineFromImageToViewPortContent(labelLine.line, data) + if (!(labelLine.id === activeLabelId && this.isResizeInProgress())) { + this.drawLine(lineOnCanvas, isActive) + } + }); + } + + private drawActivelyCreatedLabel(data: EditorData) { + if (this.isInProgress()) { + const line = {start: this.lineCreationStartPoint, end: data.mousePositionOnViewPortContent} + DrawUtil.drawLine(this.canvas, line.start, line.end, this.config.lineActiveColor, this.config.lineThickness); + const lineStartHandle = RectUtil.getRectWithCenterAndSize(this.lineCreationStartPoint, this.config.anchorSize); + DrawUtil.drawRectWithFill(this.canvas, lineStartHandle, this.config.activeAnchorColor); + } + } + + private drawActivelyResizeLabel(data: EditorData) { + const activeLabelLine: LabelLine = LabelsSelector.getActiveLineLabel(); + if (!!activeLabelLine && this.isResizeInProgress()) { + const snappedMousePosition: IPoint = + RectUtil.snapPointToRect(data.mousePositionOnViewPortContent, data.viewPortContentImageRect); + const lineOnCanvas = RenderEngineUtil.transferLineFromImageToViewPortContent(activeLabelLine.line, data) + const lineToDraw = { + start: this.lineUpdateAnchorType === LineAnchorType.START ? snappedMousePosition : lineOnCanvas.start, + end: this.lineUpdateAnchorType === LineAnchorType.END ? snappedMousePosition : lineOnCanvas.end + } + this.drawLine(lineToDraw, true) + } + } + + private updateCursorStyle(data: EditorData) { + if (!!this.canvas && !!data.mousePositionOnViewPortContent && !GeneralSelector.getImageDragModeStatus()) { + const isMouseOverCanvas: boolean = RenderEngineUtil.isMouseOverCanvas(data); + if (isMouseOverCanvas) { + const anchorTypeUnderMouse = this.getAnchorTypeUnderMouse(data); + if (!this.isInProgress() && !!anchorTypeUnderMouse) { + store.dispatch(updateCustomCursorStyle(CustomCursorStyle.MOVE)); + } else if (this.isResizeInProgress()) { + store.dispatch(updateCustomCursorStyle(CustomCursorStyle.MOVE)); + } else { + RenderEngineUtil.wrapDefaultCursorStyleInCancel(data); + } + this.canvas.style.cursor = "none"; + } else { + this.canvas.style.cursor = "default"; + } + } + } + + private drawLine(line: ILine, isActive: boolean) { + const color: string = isActive ? this.config.lineActiveColor : this.config.lineInactiveColor; + const standardizedLine: ILine = { + start: RenderEngineUtil.setPointBetweenPixels(line.start), + end: RenderEngineUtil.setPointBetweenPixels(line.end) + } + DrawUtil.drawLine(this.canvas, standardizedLine.start, standardizedLine.end, color, this.config.lineThickness); + if (isActive) { + LineUtil + .getPoints(line) + .map((point: IPoint) => RectUtil.getRectWithCenterAndSize(point, this.config.anchorSize)) + .forEach((handleRect: IRect) => { + DrawUtil.drawRectWithFill(this.canvas, handleRect, this.config.activeAnchorColor); + }) + } + } + + // ================================================================================================================= + // VALIDATORS + // ================================================================================================================= + + public isInProgress(): boolean { + return !!this.lineCreationStartPoint + } + + public isResizeInProgress(): boolean { + return !!this.lineUpdateAnchorType; + } + + private isMouseOverAnchor(mouse: IPoint, anchor: IPoint): boolean { + if (!mouse || !anchor) return null; + return RectUtil.isPointInside(RectUtil.getRectWithCenterAndSize(anchor, this.config.anchorSize), mouse); + } + + // ================================================================================================================= + // CREATION + // ================================================================================================================= + + private startNewLabelCreation = (data: EditorData) => { + this.lineCreationStartPoint = RenderEngineUtil.setPointBetweenPixels(data.mousePositionOnViewPortContent) + EditorActions.setViewPortActionsDisabledStatus(true); + } + + private finishNewLabelCreation = (data: EditorData) => { + const mousePositionOnCanvasSnapped: IPoint = RectUtil.snapPointToRect( + data.mousePositionOnViewPortContent, data.viewPortContentImageRect + ); + const lineOnCanvas = {start: this.lineCreationStartPoint, end: mousePositionOnCanvasSnapped} + const lineOnImage = RenderEngineUtil.transferLineFromViewPortContentToImage(lineOnCanvas, data); + const activeLabelId = LabelsSelector.getActiveLabelNameId(); + const imageData: ImageData = LabelsSelector.getActiveImageData(); + const labelLine: LabelLine = { + id: uuidv1(), + labelId: activeLabelId, + line: lineOnImage + }; + imageData.labelLines.push(labelLine); + store.dispatch(updateImageDataById(imageData.id, imageData)); + store.dispatch(updateFirstLabelCreatedFlag(true)); + store.dispatch(updateActiveLabelId(labelLine.id)); + this.lineCreationStartPoint = null + EditorActions.setViewPortActionsDisabledStatus(false); + }; + + public cancelLabelCreation() { + this.lineCreationStartPoint = null + EditorActions.setViewPortActionsDisabledStatus(false); + } + + // ================================================================================================================= + // UPDATE + // ================================================================================================================= + + private startExistingLabelUpdate(labelId: string, anchorType: LineAnchorType) { + store.dispatch(updateActiveLabelId(labelId)); + this.lineUpdateAnchorType = anchorType; + EditorActions.setViewPortActionsDisabledStatus(true); + } + + private endExistingLabelUpdate(data: EditorData) { + this.applyUpdateToLineLabel(data); + this.lineUpdateAnchorType = null; + EditorActions.setViewPortActionsDisabledStatus(false); + } + + private applyUpdateToLineLabel(data: EditorData) { + const imageData: ImageData = LabelsSelector.getActiveImageData(); + const activeLabel: LabelLine = LabelsSelector.getActiveLineLabel(); + imageData.labelLines = imageData.labelLines.map((lineLabel: LabelLine) => { + if (lineLabel.id !== activeLabel.id) { + return lineLabel + } else { + const snappedMousePosition: IPoint = + RectUtil.snapPointToRect(data.mousePositionOnViewPortContent, data.viewPortContentImageRect); + const mousePositionOnImage = RenderEngineUtil.transferPointFromViewPortContentToImage( + snappedMousePosition, data + ); + return { + ...lineLabel, + line: { + start: this.lineUpdateAnchorType === LineAnchorType.START ? mousePositionOnImage : lineLabel.line.start, + end: this.lineUpdateAnchorType === LineAnchorType.END ? mousePositionOnImage : lineLabel.line.end + } + } + } + }); + + store.dispatch(updateImageDataById(imageData.id, imageData)); + store.dispatch(updateActiveLabelId(activeLabel.id)); + } + + // ================================================================================================================= + // GETTERS + // ================================================================================================================= + + private getLineUnderMouse(data: EditorData): LabelLine { + const labelLines: LabelLine[] = LabelsSelector.getActiveImageData().labelLines; + for (let i = 0; i < labelLines.length; i++) { + const lineOnCanvas: ILine = RenderEngineUtil.transferLineFromImageToViewPortContent(labelLines[i].line, data); + const mouseOverLine = RenderEngineUtil.isMouseOverLine( + data.mousePositionOnViewPortContent, + lineOnCanvas, + this.config.anchorHoverSize.width / 2 + ) + if (mouseOverLine) return labelLines[i] + } + return null; + } + + private getAnchorTypeUnderMouse(data: EditorData): LineAnchorType { + const labelLines: LabelLine[] = LabelsSelector.getActiveImageData().labelLines; + for (let i = 0; i < labelLines.length; i++) { + const lineOnCanvas: ILine = RenderEngineUtil.transferLineFromImageToViewPortContent(labelLines[i].line, data); + if (this.isMouseOverAnchor(data.mousePositionOnViewPortContent, lineOnCanvas.start)) { + return LineAnchorType.START + } + if (this.isMouseOverAnchor(data.mousePositionOnViewPortContent, lineOnCanvas.end)) { + return LineAnchorType.END + } + } + return null; + } +} \ No newline at end of file diff --git a/src/logic/render/PolygonRenderEngine.ts b/src/logic/render/PolygonRenderEngine.ts index 632a5c64..20b75918 100644 --- a/src/logic/render/PolygonRenderEngine.ts +++ b/src/logic/render/PolygonRenderEngine.ts @@ -69,7 +69,8 @@ export class PolygonRenderEngine extends BaseRenderEngine { const isMouseOverCanvas: boolean = RenderEngineUtil.isMouseOverCanvas(data); if (isMouseOverCanvas) { if (this.isCreationInProgress()) { - const isMouseOverStartAnchor: boolean = this.isMouseOverAnchor(data.mousePositionOnViewPortContent, this.activePath[0]); + const isMouseOverStartAnchor: boolean = RenderEngineUtil.isMouseOverAnchor( + data.mousePositionOnViewPortContent, this.activePath[0], this.config.anchorSize); if (isMouseOverStartAnchor) { this.addLabelAndFinishCreation(data); } else { @@ -123,7 +124,12 @@ export class PolygonRenderEngine extends BaseRenderEngine { const linesOnCanvas: ILine[] = this.mapPointsToLines(pathOnCanvas.concat(pathOnCanvas[0])); for (let j = 0; j < linesOnCanvas.length; j++) { - if (this.isMouseOverLine(data.mousePositionOnViewPortContent, linesOnCanvas[j])) { + const mouseOverLine = RenderEngineUtil.isMouseOverLine( + data.mousePositionOnViewPortContent, + linesOnCanvas[j], + this.config.anchorHoverSize.width / 2 + ) + if (mouseOverLine) { this.suggestedAnchorPositionOnCanvas = LineUtil.getCenter(linesOnCanvas[j]); this.suggestedAnchorIndexInPolygon = j + 1; break; @@ -396,18 +402,6 @@ export class PolygonRenderEngine extends BaseRenderEngine { return RectUtil.isPointInside(RectUtil.getRectWithCenterAndSize(anchor, this.config.anchorSize), mouse); } - private isMouseOverLine(mouse: IPoint, l: ILine): boolean { - const hoverReach: number = this.config.anchorHoverSize.width / 2; - const minX: number = Math.min(l.start.x, l.end.x); - const maxX: number = Math.max(l.start.x, l.end.x); - const minY: number = Math.min(l.start.y, l.end.y); - const maxY: number = Math.max(l.start.y, l.end.y); - - return (minX - hoverReach <= mouse.x && maxX + hoverReach >= mouse.x) && - (minY - hoverReach <= mouse.y && maxY + hoverReach >= mouse.y) && - LineUtil.getDistanceFromLine(l, mouse) < hoverReach; - } - // ================================================================================================================= // MAPPERS // ================================================================================================================= @@ -435,7 +429,12 @@ export class PolygonRenderEngine extends BaseRenderEngine { const linesOnCanvas: ILine[] = this.mapPointsToLines(pathOnCanvas.concat(pathOnCanvas[0])); for (let j = 0; j < linesOnCanvas.length; j++) { - if (this.isMouseOverLine(data.mousePositionOnViewPortContent, linesOnCanvas[j])) + const mouseOverLine = RenderEngineUtil.isMouseOverLine( + data.mousePositionOnViewPortContent, + linesOnCanvas[j], + this.config.anchorHoverSize.width / 2 + ) + if (mouseOverLine) return labelPolygons[i]; } for (let j = 0; j < pathOnCanvas.length; j ++) { diff --git a/src/logic/render/PrimaryEditorRenderEngine.ts b/src/logic/render/PrimaryEditorRenderEngine.ts index 45383dba..7b517a5b 100644 --- a/src/logic/render/PrimaryEditorRenderEngine.ts +++ b/src/logic/render/PrimaryEditorRenderEngine.ts @@ -3,8 +3,15 @@ import {BaseRenderEngine} from "./BaseRenderEngine"; import {EditorData} from "../../data/EditorData"; import {EditorModel} from "../../staticModels/EditorModel"; import {ViewPortActions} from "../actions/ViewPortActions"; +import {DrawUtil} from "../../utils/DrawUtil"; +import {RenderEngineUtil} from "../../utils/RenderEngineUtil"; +import {RenderEngineConfig} from "../../settings/RenderEngineConfig"; +import {IPoint} from "../../interfaces/IPoint"; +import {GeneralSelector} from "../../store/selectors/GeneralSelector"; +import {ProjectType} from "../../data/enums/ProjectType"; export class PrimaryEditorRenderEngine extends BaseRenderEngine { + private config: RenderEngineConfig = new RenderEngineConfig(); public constructor(canvas: HTMLCanvasElement) { super(canvas); @@ -23,7 +30,41 @@ export class PrimaryEditorRenderEngine extends BaseRenderEngine { // ================================================================================================================= public render(data: EditorData): void { - EditorModel.primaryRenderingEngine.drawImage(EditorModel.image, ViewPortActions.calculateViewPortContentImageRect()); + this.drawImage(EditorModel.image, ViewPortActions.calculateViewPortContentImageRect()); + this.renderCursor(data); + } + + public renderCursor(data: EditorData): void { + const drawLine = (startPoint: IPoint, endPoint: IPoint) => { + DrawUtil.drawLine(this.canvas, startPoint, endPoint, this.config.crossHairLineColor, 1) + } + + const crossHairVisible = GeneralSelector.getCrossHairVisibleStatus(); + const imageDragMode = GeneralSelector.getImageDragModeStatus(); + const projectType: ProjectType = GeneralSelector.getProjectType(); + + if (!this.canvas || !crossHairVisible || imageDragMode || projectType === ProjectType.IMAGE_RECOGNITION) return; + + 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} + ) + } } public drawImage(image: HTMLImageElement, imageRect: IRect) { diff --git a/src/settings/RenderEngineConfig.ts b/src/settings/RenderEngineConfig.ts index 63ce0867..17d9c61d 100644 --- a/src/settings/RenderEngineConfig.ts +++ b/src/settings/RenderEngineConfig.ts @@ -5,6 +5,8 @@ export class RenderEngineConfig { public readonly lineThickness: number = 2; public readonly lineActiveColor: string = Settings.PRIMARY_COLOR; public readonly lineInactiveColor: string = "#fff"; + public readonly crossHairLineColor: string = "#fff"; + public readonly crossHairPadding: number = 25; public readonly anchorSize: ISize = { width: Settings.RESIZE_HANDLE_DIMENSION_PX, height: Settings.RESIZE_HANDLE_DIMENSION_PX diff --git a/src/store/Actions.ts b/src/store/Actions.ts index 986c9d46..e20504ed 100644 --- a/src/store/Actions.ts +++ b/src/store/Actions.ts @@ -14,6 +14,7 @@ export enum Action { UPDATE_CONTEXT = '@@UPDATE_CONTEXT', UPDATE_PREVENT_CUSTOM_CURSOR_STATUS = '@@UPDATE_PREVENT_CUSTOM_CURSOR_STATUS', UPDATE_IMAGE_DRAG_MODE_STATUS = '@@UPDATE_IMAGE_DRAG_MODE_STATUS', + UPDATE_CROSS_HAIR_VISIBLE_STATUS = '@@UPDATE_CROSS_HAIR_VISIBLE_STATUS', UPDATE_ZOOM = '@@UPDATE_ZOOM', // LABELS diff --git a/src/store/general/actionCreators.ts b/src/store/general/actionCreators.ts index c89ee102..ce739163 100644 --- a/src/store/general/actionCreators.ts +++ b/src/store/general/actionCreators.ts @@ -59,6 +59,15 @@ export function updateImageDragModeStatus(imageDragMode: boolean): GeneralAction }; } +export function updateCrossHairVisibleStatus(crossHairVisible: boolean): GeneralActionTypes { + return { + type: Action.UPDATE_CROSS_HAIR_VISIBLE_STATUS, + payload: { + crossHairVisible, + }, + }; +} + export function updateProjectData(projectData: ProjectData): GeneralActionTypes { return { type: Action.UPDATE_PROJECT_DATA, diff --git a/src/store/general/reducer.ts b/src/store/general/reducer.ts index efe1f161..876e0d31 100644 --- a/src/store/general/reducer.ts +++ b/src/store/general/reducer.ts @@ -10,6 +10,7 @@ const initialState: GeneralState = { activeContext: null, preventCustomCursor: false, imageDragMode: false, + crossHairVisible: true, projectData: { type: null, name: "my-project-name", @@ -58,6 +59,12 @@ export function generalReducer( imageDragMode: action.payload.imageDragMode } } + case Action.UPDATE_CROSS_HAIR_VISIBLE_STATUS: { + return { + ...state, + crossHairVisible: action.payload.crossHairVisible + } + } case Action.UPDATE_PROJECT_DATA: { return { ...state, diff --git a/src/store/general/types.ts b/src/store/general/types.ts index 9da99486..0de2fdbf 100644 --- a/src/store/general/types.ts +++ b/src/store/general/types.ts @@ -16,6 +16,7 @@ export type GeneralState = { customCursorStyle: CustomCursorStyle; preventCustomCursor: boolean; imageDragMode: boolean; + crossHairVisible: boolean; activeContext: ContextType; projectData: ProjectData; zoom: number; @@ -70,6 +71,13 @@ interface UpdateImageDragModeStatus { } } +interface UpdateCrossHairVisibleStatus { + type: typeof Action.UPDATE_CROSS_HAIR_VISIBLE_STATUS; + payload: { + crossHairVisible: boolean; + } +} + interface UpdateZoom { type: typeof Action.UPDATE_ZOOM, payload: { @@ -84,4 +92,5 @@ export type GeneralActionTypes = UpdateProjectData | UpdateActiveContext | UpdatePreventCustomCursorStatus | UpdateImageDragModeStatus + | UpdateCrossHairVisibleStatus | UpdateZoom \ No newline at end of file diff --git a/src/store/labels/types.ts b/src/store/labels/types.ts index 4a26fdb9..ef91b288 100644 --- a/src/store/labels/types.ts +++ b/src/store/labels/types.ts @@ -3,6 +3,7 @@ import {Action} from "../Actions"; import {LabelType} from "../../data/enums/LabelType"; import {IPoint} from "../../interfaces/IPoint"; import {LabelStatus} from "../../data/enums/LabelStatus"; +import {ILine} from "../../interfaces/ILine"; export type LabelRect = { // GENERAL @@ -34,6 +35,12 @@ export type LabelPolygon = { vertices: IPoint[]; } +export type LabelLine = { + id: string; + labelId: string; + line: ILine +} + export type LabelName = { name: string; id: string; @@ -45,7 +52,9 @@ export type ImageData = { loadStatus: boolean; labelRects: LabelRect[]; labelPoints: LabelPoint[]; + labelLines: LabelLine[]; labelPolygons: LabelPolygon[]; + labelTagId: string; // SSD isVisitedByObjectDetector: boolean; diff --git a/src/store/selectors/GeneralSelector.ts b/src/store/selectors/GeneralSelector.ts index 88301987..fab9e1ad 100644 --- a/src/store/selectors/GeneralSelector.ts +++ b/src/store/selectors/GeneralSelector.ts @@ -21,6 +21,10 @@ export class GeneralSelector { return store.getState().general.imageDragMode; } + public static getCrossHairVisibleStatus(): boolean { + return store.getState().general.crossHairVisible; + } + public static getCustomCursorStyle(): CustomCursorStyle { return store.getState().general.customCursorStyle; } diff --git a/src/store/selectors/LabelsSelector.ts b/src/store/selectors/LabelsSelector.ts index a2f878a0..c8f760d1 100644 --- a/src/store/selectors/LabelsSelector.ts +++ b/src/store/selectors/LabelsSelector.ts @@ -1,5 +1,5 @@ import {store} from "../.."; -import {ImageData, LabelName, LabelPoint, LabelPolygon, LabelRect} from "../labels/types"; +import {ImageData, LabelLine, LabelName, LabelPoint, LabelPolygon, LabelRect} from "../labels/types"; import {find} from "lodash"; import {LabelType} from "../../data/enums/LabelType"; @@ -77,4 +77,13 @@ export class LabelsSelector { return find(LabelsSelector.getActiveImageData().labelPolygons, {id: activeLabelId}); } + + public static getActiveLineLabel(): LabelLine | null { + const activeLabelId: string | null = LabelsSelector.getActiveLabelId(); + + if (activeLabelId === null) + return null; + + return find(LabelsSelector.getActiveImageData().labelLines, {id: activeLabelId}); + } } \ No newline at end of file diff --git a/src/utils/FileUtil.ts b/src/utils/FileUtil.ts index d02cbd12..f6e9dd1e 100644 --- a/src/utils/FileUtil.ts +++ b/src/utils/FileUtil.ts @@ -9,7 +9,9 @@ export class FileUtil { loadStatus: false, labelRects: [], labelPoints: [], + labelLines: [], labelPolygons: [], + labelTagId: null, isVisitedByObjectDetector: false, isVisitedByPoseDetector: false } diff --git a/src/utils/LineUtil.ts b/src/utils/LineUtil.ts index dcbc129b..e6943b52 100644 --- a/src/utils/LineUtil.ts +++ b/src/utils/LineUtil.ts @@ -17,4 +17,8 @@ export class LineUtil { y: (l.start.y + l.end.y) / 2 } } + + public static getPoints(l: ILine): IPoint[] { + return [l.start, l.end] + } } \ No newline at end of file diff --git a/src/utils/PointUtil.ts b/src/utils/PointUtil.ts index 581dcd86..799376a9 100644 --- a/src/utils/PointUtil.ts +++ b/src/utils/PointUtil.ts @@ -25,8 +25,4 @@ export class PointUtil { y: p1.y * factor } } - - public static getEuclidianDistance(p1: IPoint, p2: IPoint): number { - return Math.sqrt(Math.pow((p1.x - p2.x), 2) + Math.pow((p1.y - p2.y), 2)); - } } \ No newline at end of file diff --git a/src/utils/RenderEngineUtil.ts b/src/utils/RenderEngineUtil.ts index b1cd73f4..dd9d1231 100644 --- a/src/utils/RenderEngineUtil.ts +++ b/src/utils/RenderEngineUtil.ts @@ -6,6 +6,9 @@ import {updateCustomCursorStyle} from "../store/general/actionCreators"; import {IPoint} from "../interfaces/IPoint"; import {PointUtil} from "./PointUtil"; import {IRect} from "../interfaces/IRect"; +import {ILine} from "../interfaces/ILine"; +import {LineUtil} from "./LineUtil"; +import {ISize} from "../interfaces/ISize"; export class RenderEngineUtil { public static calculateImageScale(data: EditorData): number { @@ -20,22 +23,36 @@ export class RenderEngineUtil { return RectUtil.isPointInside({x: 0, y: 0, ...data.viewPortContentSize}, data.mousePositionOnViewPortContent); } + public static transferPointFromImageToViewPortContent(point: IPoint, data: EditorData): IPoint { + const scale = RenderEngineUtil.calculateImageScale(data); + return PointUtil.add(PointUtil.multiply(point, 1/scale), data.viewPortContentImageRect); + } + public static transferPolygonFromImageToViewPortContent(polygon: IPoint[], data: EditorData): IPoint[] { return polygon.map((point: IPoint) => RenderEngineUtil.transferPointFromImageToViewPortContent(point, data)); } - public static transferPointFromImageToViewPortContent(point: IPoint, data: EditorData): IPoint { + public static transferLineFromImageToViewPortContent(line: ILine, data: EditorData): ILine { + return { + start: RenderEngineUtil.transferPointFromImageToViewPortContent(line.start, data), + end: RenderEngineUtil.transferPointFromImageToViewPortContent(line.end, data) + } + } + + public static transferPointFromViewPortContentToImage(point: IPoint, data: EditorData): IPoint { const scale = RenderEngineUtil.calculateImageScale(data); - return PointUtil.add(PointUtil.multiply(point, 1/scale), data.viewPortContentImageRect); + return PointUtil.multiply(PointUtil.subtract(point, data.viewPortContentImageRect), scale); } public static transferPolygonFromViewPortContentToImage(polygon: IPoint[], data: EditorData): IPoint[] { return polygon.map((point: IPoint) => RenderEngineUtil.transferPointFromViewPortContentToImage(point, data)); } - public static transferPointFromViewPortContentToImage(point: IPoint, data: EditorData): IPoint { - const scale = RenderEngineUtil.calculateImageScale(data); - return PointUtil.multiply(PointUtil.subtract(point, data.viewPortContentImageRect), scale); + public static transferLineFromViewPortContentToImage(line: ILine, data: EditorData): ILine { + return { + start: RenderEngineUtil.transferPointFromViewPortContentToImage(line.start, data), + end: RenderEngineUtil.transferPointFromViewPortContentToImage(line.end, data) + } } public static transferRectFromViewPortContentToImage(rect: IRect, data: EditorData): IRect { @@ -90,4 +107,20 @@ export class RenderEngineUtil { height: bottomRightBetweenPixels.y - topLeftBetweenPixels.y } } + + public static isMouseOverLine(mouse: IPoint, l: ILine, radius: number): boolean { + const minX: number = Math.min(l.start.x, l.end.x); + const maxX: number = Math.max(l.start.x, l.end.x); + const minY: number = Math.min(l.start.y, l.end.y); + const maxY: number = Math.max(l.start.y, l.end.y); + + return (minX - radius <= mouse.x && maxX + radius >= mouse.x) && + (minY - radius <= mouse.y && maxY + radius >= mouse.y) && + LineUtil.getDistanceFromLine(l, mouse) < radius; + } + + public static isMouseOverAnchor(mouse: IPoint, anchor: IPoint, size: ISize): boolean { + if (!mouse || !anchor) return null; + return RectUtil.isPointInside(RectUtil.getRectWithCenterAndSize(anchor, size), mouse); + } } \ No newline at end of file diff --git a/src/views/EditorView/EditorContainer/EditorContainer.tsx b/src/views/EditorView/EditorContainer/EditorContainer.tsx index 74bb6146..958d66c6 100644 --- a/src/views/EditorView/EditorContainer/EditorContainer.tsx +++ b/src/views/EditorView/EditorContainer/EditorContainer.tsx @@ -15,17 +15,24 @@ import {ContextManager} from "../../../logic/context/ContextManager"; import {ContextType} from "../../../data/enums/ContextType"; import EditorBottomNavigationBar from "../EditorBottomNavigationBar/EditorBottomNavigationBar"; import EditorTopNavigationBar from "../EditorTopNavigationBar/EditorTopNavigationBar"; -import {LabelType} from "../../../data/enums/LabelType"; +import {ProjectType} from "../../../data/enums/ProjectType"; interface IProps { windowSize: ISize; activeImageIndex: number; imagesData: ImageData[]; activeContext: ContextType; - activeLabelType: LabelType; + projectType: ProjectType; } -const EditorContainer: React.FC = ({windowSize, activeImageIndex, imagesData, activeContext}) => { +const EditorContainer: React.FC = ( + { + windowSize, + activeImageIndex, + imagesData, + activeContext, + projectType + }) => { const [leftTabStatus, setLeftTabStatus] = useState(true); const [rightTabStatus, setRightTabStatus] = useState(true); @@ -101,19 +108,25 @@ const EditorContainer: React.FC = ({windowSize, activeImageIndex, images isWithContext={activeContext === ContextType.LEFT_NAVBAR} renderCompanion={leftSideBarCompanionRender} renderContent={leftSideBarRender} + key="left-side-navigation-bar" />
ContextManager.switchCtx(ContextType.EDITOR)} + key="editor-wrapper" > - + {projectType === ProjectType.OBJECT_DETECTION && }
= ({windowSize, activeImageIndex, images isWithContext={activeContext === ContextType.RIGHT_NAVBAR} renderCompanion={rightSideBarCompanionRender} renderContent={rightSideBarRender} + key="right-side-navigation-bar" /> ); @@ -132,7 +146,7 @@ const mapStateToProps = (state: AppState) => ({ activeImageIndex: state.labels.activeImageIndex, imagesData: state.labels.imagesData, activeContext: state.general.activeContext, - activeLabelType: state.labels.activeLabelType + projectType: state.general.projectData.type }); export default connect( diff --git a/src/views/EditorView/EditorTopNavigationBar/EditorTopNavigationBar.scss b/src/views/EditorView/EditorTopNavigationBar/EditorTopNavigationBar.scss index 53495602..d27b1ed2 100644 --- a/src/views/EditorView/EditorTopNavigationBar/EditorTopNavigationBar.scss +++ b/src/views/EditorView/EditorTopNavigationBar/EditorTopNavigationBar.scss @@ -45,12 +45,12 @@ &:hover { border-radius: 3px; - background-color: rgba(0, 0, 0, 0.1); + background-color: rgba(0, 0, 0, 0.2); } &.active { border-radius: 3px; - background-color: rgba(0, 0, 0, 0.1); + background-color: rgba(0, 0, 0, 0.4); } } } \ No newline at end of file diff --git a/src/views/EditorView/EditorTopNavigationBar/EditorTopNavigationBar.tsx b/src/views/EditorView/EditorTopNavigationBar/EditorTopNavigationBar.tsx index c51fa2f7..b76e2991 100644 --- a/src/views/EditorView/EditorTopNavigationBar/EditorTopNavigationBar.tsx +++ b/src/views/EditorView/EditorTopNavigationBar/EditorTopNavigationBar.tsx @@ -4,7 +4,7 @@ import React from "react"; import classNames from "classnames"; import {AppState} from "../../../store"; import {connect} from "react-redux"; -import {updateImageDragModeStatus} from "../../../store/general/actionCreators"; +import {updateCrossHairVisibleStatus, updateImageDragModeStatus} from "../../../store/general/actionCreators"; import {GeneralSelector} from "../../../store/selectors/GeneralSelector"; import {ViewPointSettings} from "../../../settings/ViewPointSettings"; import {ImageButton} from "../../Common/ImageButton/ImageButton"; @@ -18,11 +18,21 @@ import {AIActions} from "../../../logic/actions/AIActions"; interface IProps { activeContext: ContextType; updateImageDragModeStatus: (imageDragMode: boolean) => any; + updateCrossHairVisibleStatus: (crossHairVisible: boolean) => any; imageDragMode: boolean; + crossHairVisible: boolean; activeLabelType: LabelType; } -const EditorTopNavigationBar: React.FC = ({activeContext, updateImageDragModeStatus, imageDragMode, activeLabelType}) => { +const EditorTopNavigationBar: React.FC = ( + { + activeContext, + updateImageDragModeStatus, + updateCrossHairVisibleStatus, + imageDragMode, + crossHairVisible, + activeLabelType + }) => { const buttonSize: ISize = {width: 30, height: 30}; const buttonPadding: number = 10; @@ -44,6 +54,10 @@ const EditorTopNavigationBar: React.FC = ({activeContext, updateImageDra } }; + const crossHairOnClick = () => { + updateCrossHairVisibleStatus(!crossHairVisible); + } + return (
@@ -85,6 +99,14 @@ const EditorTopNavigationBar: React.FC = ({activeContext, updateImageDra onClick={imageDragOnClick} isActive={imageDragMode} /> +
{((activeLabelType === LabelType.RECTANGLE && AISelector.isAIObjectDetectorModelLoaded()) || (activeLabelType === LabelType.POINT && AISelector.isAIPoseDetectorModelLoaded())) &&
@@ -111,12 +133,14 @@ const EditorTopNavigationBar: React.FC = ({activeContext, updateImageDra }; const mapDispatchToProps = { - updateImageDragModeStatus + updateImageDragModeStatus, + updateCrossHairVisibleStatus }; const mapStateToProps = (state: AppState) => ({ activeContext: state.general.activeContext, imageDragMode: state.general.imageDragMode, + crossHairVisible: state.general.crossHairVisible, activeLabelType: state.labels.activeLabelType }); diff --git a/src/views/EditorView/SideNavigationBar/ImagesList/ImagesList.tsx b/src/views/EditorView/SideNavigationBar/ImagesList/ImagesList.tsx index 40d7135d..a98d2aa3 100644 --- a/src/views/EditorView/SideNavigationBar/ImagesList/ImagesList.tsx +++ b/src/views/EditorView/SideNavigationBar/ImagesList/ImagesList.tsx @@ -57,13 +57,23 @@ class ImagesList extends React.Component { }; private isImageChecked = (index:number): boolean => { - return (this.props.activeLabelType === LabelType.RECTANGLE && - this.props.imagesData[index].labelRects - .filter((labelRect: LabelRect) => labelRect.status === LabelStatus.ACCEPTED).length > 0) || - (this.props.activeLabelType === LabelType.POINT && - this.props.imagesData[index].labelPoints - .filter((labelPoint: LabelPoint) => labelPoint.status === LabelStatus.ACCEPTED).length > 0) || - (this.props.activeLabelType === LabelType.POLYGON && this.props.imagesData[index].labelPolygons.length > 0) + const imageData = this.props.imagesData[index] + switch (this.props.activeLabelType) { + case LabelType.LINE: + return imageData.labelLines.length > 0 + case LabelType.NAME: + return imageData.labelTagId !== null + case LabelType.POINT: + return imageData.labelPoints + .filter((labelPoint: LabelPoint) => labelPoint.status === LabelStatus.ACCEPTED) + .length > 0 + case LabelType.POLYGON: + return imageData.labelPolygons.length > 0 + case LabelType.RECTANGLE: + return imageData.labelRects + .filter((labelRect: LabelRect) => labelRect.status === LabelStatus.ACCEPTED) + .length > 0 + } }; private onClickHandler = (index: number) => { diff --git a/src/views/EditorView/SideNavigationBar/LabelsToolkit/LabelsToolkit.tsx b/src/views/EditorView/SideNavigationBar/LabelsToolkit/LabelsToolkit.tsx index f649fda2..6770f5a2 100644 --- a/src/views/EditorView/SideNavigationBar/LabelsToolkit/LabelsToolkit.tsx +++ b/src/views/EditorView/SideNavigationBar/LabelsToolkit/LabelsToolkit.tsx @@ -17,6 +17,8 @@ import PolygonLabelsList from "../PolygonLabelsList/PolygonLabelsList"; import {ContextManager} from "../../../../logic/context/ContextManager"; import {ContextType} from "../../../../data/enums/ContextType"; import {EventType} from "../../../../data/enums/EventType"; +import LineLabelsList from "../LineLabelsList/LineLabelsList"; +import TagLabelsList from "../TagLabelsList/TagLabelsList"; interface IProps { activeImageIndex:number, @@ -50,6 +52,7 @@ class LabelsToolkit extends React.Component { [ LabelType.RECTANGLE, LabelType.POINT, + LabelType.LINE, LabelType.POLYGON ]; @@ -145,6 +148,13 @@ class LabelsToolkit extends React.Component { }} imageData={imagesData[activeImageIndex]} />} + {labelType === LabelType.LINE && } {labelType === LabelType.POLYGON && { }} imageData={imagesData[activeImageIndex]} />} + {labelType === LabelType.NAME && }
; children.push([header, content]); diff --git a/src/views/EditorView/SideNavigationBar/LineLabelsList/LineLabelsList.scss b/src/views/EditorView/SideNavigationBar/LineLabelsList/LineLabelsList.scss new file mode 100644 index 00000000..d1c008e4 --- /dev/null +++ b/src/views/EditorView/SideNavigationBar/LineLabelsList/LineLabelsList.scss @@ -0,0 +1,17 @@ +.LineLabelsList { + display: flex; + flex-direction: column; + flex-wrap: nowrap; + justify-content: center; + align-items: center; + align-content: center; + + .LineLabelsListContent { + display: flex; + flex-direction: column; + flex-wrap: nowrap; + justify-content: flex-start; + align-items: center; + align-content: flex-start; + } +} \ No newline at end of file diff --git a/src/views/EditorView/SideNavigationBar/LineLabelsList/LineLabelsList.tsx b/src/views/EditorView/SideNavigationBar/LineLabelsList/LineLabelsList.tsx new file mode 100644 index 00000000..45c15839 --- /dev/null +++ b/src/views/EditorView/SideNavigationBar/LineLabelsList/LineLabelsList.tsx @@ -0,0 +1,135 @@ +import React from 'react'; +import './LineLabelsList.scss'; +import {ISize} from "../../../../interfaces/ISize"; +import {ImageData, LabelLine, LabelName} from "../../../../store/labels/types"; +import {LabelActions} from "../../../../logic/actions/LabelActions"; +import LabelInputField from "../LabelInputField/LabelInputField"; +import {findLast} from "lodash"; +import EmptyLabelList from "../EmptyLabelList/EmptyLabelList"; +import Scrollbars from "react-custom-scrollbars"; +import { + updateActiveLabelId, + updateActiveLabelNameId, + updateImageDataById +} from "../../../../store/labels/actionCreators"; +import {AppState} from "../../../../store"; +import {connect} from "react-redux"; + +interface IProps { + size: ISize; + imageData: ImageData; + updateImageDataById: (id: string, newImageData: ImageData) => any; + activeLabelId: string; + highlightedLabelId: string; + updateActiveLabelNameId: (activeLabelId: string) => any; + labelNames: LabelName[]; + updateActiveLabelId: (activeLabelId: string) => any; +} + +const LineLabelsList: React.FC = ( + { + size, + imageData, + updateImageDataById, + labelNames, + updateActiveLabelNameId, + activeLabelId, + highlightedLabelId, + updateActiveLabelId + } +) => { + const labelInputFieldHeight = 40; + const listStyle: React.CSSProperties = { + width: size.width, + height: size.height + }; + const listStyleContent: React.CSSProperties = { + width: size.width, + height: imageData.labelLines.length * labelInputFieldHeight + }; + + const deleteLineLabelById = (labelLineId: string) => { + LabelActions.deleteLineLabelById(imageData.id, labelLineId); + }; + + const updateLineLabel = (labelLineId: string, labelNameId: string) => { + const newImageData = { + ...imageData, + labelLines: imageData.labelLines.map((labelLine: LabelLine) => { + if (labelLine.id === labelLineId) { + return { + ...labelLine, + labelId: labelNameId + } + } + return labelLine + }) + }; + updateImageDataById(imageData.id, newImageData); + updateActiveLabelNameId(labelNameId); + }; + + const onClickHandler = () => { + updateActiveLabelId(null); + }; + + const getChildren = () => { + return imageData.labelLines + .map((labelLine: LabelLine) => { + return + }); + }; + + return ( +
+ {imageData.labelLines.length === 0 ? + : + +
+ {getChildren()} +
+
+ } +
+ ); +}; + +const mapDispatchToProps = { + updateImageDataById, + updateActiveLabelNameId, + updateActiveLabelId +}; + +const mapStateToProps = (state: AppState) => ({ + activeLabelId: state.labels.activeLabelId, + highlightedLabelId: state.labels.highlightedLabelId, + labelNames : state.labels.labels +}); + +export default connect( + mapStateToProps, + mapDispatchToProps +)(LineLabelsList); \ No newline at end of file diff --git a/src/views/EditorView/SideNavigationBar/PointLabelsList/PointLabelsList.tsx b/src/views/EditorView/SideNavigationBar/PointLabelsList/PointLabelsList.tsx index 5d2d03c8..68f57d6c 100644 --- a/src/views/EditorView/SideNavigationBar/PointLabelsList/PointLabelsList.tsx +++ b/src/views/EditorView/SideNavigationBar/PointLabelsList/PointLabelsList.tsx @@ -27,7 +27,18 @@ interface IProps { updateActiveLabelId: (activeLabelId: string) => any; } -const PointLabelsList: React.FC = ({size, imageData, updateImageDataById, labelNames, updateActiveLabelNameId, activeLabelId, highlightedLabelId, updateActiveLabelId}) => { +const PointLabelsList: React.FC = ( + { + size, + imageData, + updateImageDataById, + labelNames, + updateActiveLabelNameId, + activeLabelId, + highlightedLabelId, + updateActiveLabelId + } +) => { const labelInputFieldHeight = 40; const listStyle: React.CSSProperties = { width: size.width, diff --git a/src/views/EditorView/SideNavigationBar/RectLabelsList/RectLabelsList.tsx b/src/views/EditorView/SideNavigationBar/RectLabelsList/RectLabelsList.tsx index f46db8ea..b3b4840c 100644 --- a/src/views/EditorView/SideNavigationBar/RectLabelsList/RectLabelsList.tsx +++ b/src/views/EditorView/SideNavigationBar/RectLabelsList/RectLabelsList.tsx @@ -95,7 +95,7 @@ const RectLabelsList: React.FC = ({size, imageData, updateImageDataById, > {imageData.labelRects.filter((labelRect: LabelRect) => labelRect.status === LabelStatus.ACCEPTED).length === 0 ? : diff --git a/src/views/EditorView/SideNavigationBar/TagLabelsList/TagLabelsList.scss b/src/views/EditorView/SideNavigationBar/TagLabelsList/TagLabelsList.scss new file mode 100644 index 00000000..893faa33 --- /dev/null +++ b/src/views/EditorView/SideNavigationBar/TagLabelsList/TagLabelsList.scss @@ -0,0 +1,89 @@ +@import '../../../../settings/Settings'; + +.TagLabelsList { + display: flex; + flex-direction: column; + flex-wrap: nowrap; + justify-content: center; + align-items: center; + align-content: center; + + .EmptyLabelList { + display: flex; + flex-direction: column; + flex-wrap: nowrap; + justify-content: center; + align-items: center; + align-content: center; + text-align: center; + width: 150px; + color: white; + font-size: 16px; + user-select: none; + + > img { + filter: brightness(0) invert(1); + max-width: 80px; + max-height: 80px; + margin-bottom: 20px; + user-select: none; + } + + > p { + &.extraBold { + font-size: 16px; + font-weight: 600; + } + } + } + + .TagLabelsListContent { + display: flex; + flex-direction: row; + flex-wrap: wrap; + justify-content: flex-start; + align-items: center; + align-content: flex-start; + flex: 1 1 auto; + } + + .TagItem { + margin-right: 10px; + margin-bottom: 10px; + padding: 5px 20px; + border-radius: 3px; + background-color: rgba(0, 0, 0, 0.2); + font-size: 14px; + font-weight: 700; + cursor: pointer; + user-select: none; + text-decoration: none; + color: white; + + &:hover { + background-color: rgba(0, 0, 0, 0.4); + } + + &.active { + background-color: $secondaryColor; // fallback if new css variables are not supported by browser + background-color: var(--leading-color); + } + } + + .ImageButton { + margin: 0 0 10px 0; + border-radius: 3px; + background-color: rgba(0, 0, 0, 0.2); + + img { + filter: brightness(0) invert(1); + user-select: none; + width: 16px; + height: 16px; + } + + &:hover { + background-color: rgba(0, 0, 0, 0.4); + } + } +} \ No newline at end of file diff --git a/src/views/EditorView/SideNavigationBar/TagLabelsList/TagLabelsList.tsx b/src/views/EditorView/SideNavigationBar/TagLabelsList/TagLabelsList.tsx new file mode 100644 index 00000000..02cb5dfa --- /dev/null +++ b/src/views/EditorView/SideNavigationBar/TagLabelsList/TagLabelsList.tsx @@ -0,0 +1,131 @@ +import {ISize} from "../../../../interfaces/ISize"; +import {ImageData, LabelName} from "../../../../store/labels/types"; +import React from "react"; +import Scrollbars from "react-custom-scrollbars"; +import {updateImageDataById} from "../../../../store/labels/actionCreators"; +import {AppState} from "../../../../store"; +import {connect} from "react-redux"; +import './TagLabelsList.scss'; +import classNames from "classnames"; +import {ImageButton} from "../../../Common/ImageButton/ImageButton"; +import {PopupWindowType} from "../../../../data/enums/PopupWindowType"; +import {updateActivePopupType} from "../../../../store/general/actionCreators"; +interface IProps { + size: ISize; + imageData: ImageData; + updateImageDataById: (id: string, newImageData: ImageData) => any; + labelNames: LabelName[]; + updateActivePopupType: (activePopupType: PopupWindowType) => any; +} + +const TagLabelsList: React.FC = ( + { + size, + imageData, + updateImageDataById, + labelNames, + updateActivePopupType + }) => { + const labelInputFieldHeight = 40; + const listStyle: React.CSSProperties = { + width: size.width, + height: size.height + }; + const listStyleContent: React.CSSProperties = { + width: size.width, + height: imageData.labelPolygons.length * labelInputFieldHeight + }; + + const onTagClick = (labelId: string) => { + if (imageData.labelTagId === labelId) { + updateImageDataById(imageData.id, { + ...imageData, + labelTagId: null + }) + } else { + updateImageDataById(imageData.id, { + ...imageData, + labelTagId: labelId + }) + } + } + + const getClassName = (labelId: string) => { + return classNames( + "TagItem", + { + "active": imageData.labelTagId === labelId + } + ); + }; + + const addNewOnClick = () => { + updateActivePopupType(PopupWindowType.UPDATE_LABEL_NAMES) + } + + const getChildren = () => { + return [ + ...labelNames.map((labelName: LabelName) => { + return
onTagClick(labelName.id)} + key={labelName.id} + > + {labelName.name} +
+ }), + + ] + }; + + return ( +
+ {labelNames.length === 0 ? +
+ {"upload"} +

Your label list is empty

+
: + +
+ {getChildren()} +
+
+ } +
+ ); +}; + +const mapDispatchToProps = { + updateImageDataById, + updateActivePopupType +}; + +const mapStateToProps = (state: AppState) => ({ + labelNames : state.labels.labels +}); + +export default connect( + mapStateToProps, + mapDispatchToProps +)(TagLabelsList); \ No newline at end of file diff --git a/src/views/EditorView/StateBar/StateBar.tsx b/src/views/EditorView/StateBar/StateBar.tsx index 1a13c04f..d3759f05 100644 --- a/src/views/EditorView/StateBar/StateBar.tsx +++ b/src/views/EditorView/StateBar/StateBar.tsx @@ -24,6 +24,14 @@ const StateBar: React.FC = ({imagesData, activeLabelType}) => { return currentCount + (currentImage.labelPolygons.length > 0 ? 1 : 0); }, 0); + const lineLabeledImages = imagesData.reduce((currentCount: number, currentImage: ImageData) => { + return currentCount + (currentImage.labelLines.length > 0 ? 1 : 0); + }, 0); + + const tagLabeledImages = imagesData.reduce((currentCount: number, currentImage: ImageData) => { + return currentCount + (currentImage.labelTagId !== null ? 1 : 0); + }, 0); + const getProgress = () => { switch (activeLabelType) { case LabelType.POINT: @@ -32,6 +40,10 @@ const StateBar: React.FC = ({imagesData, activeLabelType}) => { return (100 * rectLabeledImages) / imagesData.length; case LabelType.POLYGON: return (100 * polygonLabeledImages) / imagesData.length; + case LabelType.LINE: + return (100 * lineLabeledImages) / imagesData.length; + case LabelType.NAME: + return (100 * tagLabeledImages) / imagesData.length; default: return 0; } diff --git a/src/views/EditorView/TopNavigationBar/TopNavigationBar.scss b/src/views/EditorView/TopNavigationBar/TopNavigationBar.scss index b09b0af2..790870d8 100644 --- a/src/views/EditorView/TopNavigationBar/TopNavigationBar.scss +++ b/src/views/EditorView/TopNavigationBar/TopNavigationBar.scss @@ -19,7 +19,7 @@ color: white; align-self: stretch; flex: 1; - + height: calc(#{$topNavigationBarHeight} - #{$stateBarHeight}); display: flex; flex-wrap: nowrap; justify-content: space-between; diff --git a/src/views/MainView/ImagesDropZone/ImagesDropZone.tsx b/src/views/MainView/ImagesDropZone/ImagesDropZone.tsx index ff339070..4ba21be8 100644 --- a/src/views/MainView/ImagesDropZone/ImagesDropZone.tsx +++ b/src/views/MainView/ImagesDropZone/ImagesDropZone.tsx @@ -1,6 +1,6 @@ import React from "react"; import './ImagesDropZone.scss'; -import {useDropzone} from "react-dropzone"; +import {useDropzone,DropzoneOptions} from "react-dropzone"; import {TextButton} from "../../Common/TextButton/TextButton"; import {ImageData} from "../../../store/labels/types"; import {connect} from "react-redux"; @@ -24,7 +24,7 @@ interface IProps { const ImagesDropZone: React.FC = ({updateActiveImageIndex, addImageData, updateProjectData, updateActivePopupType, projectData}) => { const {acceptedFiles, getRootProps, getInputProps} = useDropzone({ accept: AcceptedFileType.IMAGE - }); + } as DropzoneOptions); const startEditor = (projectType: ProjectType) => { if (acceptedFiles.length > 0) { @@ -79,16 +79,16 @@ const ImagesDropZone: React.FC = ({updateActiveImageIndex, addImageData, {getDropZoneContent()}
- {}} - /> startEditor(ProjectType.OBJECT_DETECTION)} /> + startEditor(ProjectType.IMAGE_RECOGNITION)} + />
) diff --git a/src/views/PopupView/ExportLabelsPopup/ExportLabelPopup.scss b/src/views/PopupView/ExportLabelsPopup/ExportLabelPopup.scss index 3081b623..231b4b20 100644 --- a/src/views/PopupView/ExportLabelsPopup/ExportLabelPopup.scss +++ b/src/views/PopupView/ExportLabelsPopup/ExportLabelPopup.scss @@ -33,12 +33,12 @@ &:hover { border-radius: 3px; - background-color: rgba(0, 0, 0, 0.1); + background-color: rgba(0, 0, 0, 0.2); } &.active { border-radius: 3px; - background-color: rgba(0, 0, 0, 0.1); + background-color: rgba(0, 0, 0, 0.4); } } } diff --git a/src/views/PopupView/ExportLabelsPopup/ExportLabelPopup.tsx b/src/views/PopupView/ExportLabelsPopup/ExportLabelPopup.tsx index cf2a4216..c48ff822 100644 --- a/src/views/PopupView/ExportLabelsPopup/ExportLabelPopup.tsx +++ b/src/views/PopupView/ExportLabelsPopup/ExportLabelPopup.tsx @@ -14,9 +14,17 @@ import {PointLabelsExporter} from "../../../logic/export/PointLabelsExport"; import {PolygonExportFormatData} from "../../../data/export/PolygonExportFormatData"; import {PolygonLabelsExporter} from "../../../logic/export/PolygonLabelsExporter"; import {PopupActions} from "../../../logic/actions/PopupActions"; +import {LineExportFormatData} from "../../../data/export/LineExportFormatData"; +import {LineLabelsExporter} from "../../../logic/export/LineLabelExport"; +import {TagExportFormatData} from "../../../data/export/TagExportFormatData"; +import {TagLabelsExporter} from "../../../logic/export/TagLabelsExport"; -const ExportLabelPopup: React.FC = () => { - const [exportLabelType, setExportLabelType] = useState(LabelType.RECTANGLE); +interface IProps { + activeLabelType: LabelType +} + +const ExportLabelPopup: React.FC = ({activeLabelType}) => { + const [exportLabelType, setExportLabelType] = useState(activeLabelType); const [exportFormatType, setExportFormatType] = useState(null); const onAccept = () => { @@ -28,9 +36,15 @@ const ExportLabelPopup: React.FC = () => { case LabelType.POINT: PointLabelsExporter.export(exportFormatType); break; + case LabelType.LINE: + LineLabelsExporter.export(exportFormatType); + break; case LabelType.POLYGON: PolygonLabelsExporter.export(exportFormatType); break; + case LabelType.NAME: + TagLabelsExporter.export(exportFormatType); + break; } PopupActions.close(); }; @@ -68,7 +82,7 @@ const ExportLabelPopup: React.FC = () => { const renderContent = () => { return(
-
+ {activeLabelType !== LabelType.NAME &&
{ }} isActive={exportLabelType === LabelType.POINT} /> + { + setExportLabelType(LabelType.LINE); + setExportFormatType(null); + }} + isActive={exportLabelType === LabelType.LINE} + /> { }} isActive={exportLabelType === LabelType.POLYGON} /> -
+
}
Select label type and the file format you would like to use for exporting labels. @@ -110,7 +135,9 @@ const ExportLabelPopup: React.FC = () => {
{exportLabelType === LabelType.RECTANGLE && getOptions(RectExportFormatData)} {exportLabelType === LabelType.POINT && getOptions(PointExportFormatData)} + {exportLabelType === LabelType.LINE && getOptions(LineExportFormatData)} {exportLabelType === LabelType.POLYGON && getOptions(PolygonExportFormatData)} + {exportLabelType === LabelType.NAME && getOptions(TagExportFormatData)}
); @@ -131,7 +158,7 @@ const ExportLabelPopup: React.FC = () => { const mapDispatchToProps = {}; const mapStateToProps = (state: AppState) => ({ - imagesData: state.labels.imagesData + activeLabelType: state.labels.activeLabelType }); export default connect( diff --git a/src/views/PopupView/InsertLabelNamesPopup/InsertLabelNamesPopup.scss b/src/views/PopupView/InsertLabelNamesPopup/InsertLabelNamesPopup.scss index 3afa1868..f5bfa156 100644 --- a/src/views/PopupView/InsertLabelNamesPopup/InsertLabelNamesPopup.scss +++ b/src/views/PopupView/InsertLabelNamesPopup/InsertLabelNamesPopup.scss @@ -34,12 +34,12 @@ &:hover { border-radius: 3px; - background-color: rgba(0, 0, 0, 0.1); + background-color: rgba(0, 0, 0, 0.2); } &.active { border-radius: 3px; - background-color: rgba(0, 0, 0, 0.1); + background-color: rgba(0, 0, 0, 0.4); } } } diff --git a/src/views/PopupView/InsertLabelNamesPopup/InsertLabelNamesPopup.tsx b/src/views/PopupView/InsertLabelNamesPopup/InsertLabelNamesPopup.tsx index 004c3277..1c4d1446 100644 --- a/src/views/PopupView/InsertLabelNamesPopup/InsertLabelNamesPopup.tsx +++ b/src/views/PopupView/InsertLabelNamesPopup/InsertLabelNamesPopup.tsx @@ -14,14 +14,22 @@ import {LabelName} from "../../../store/labels/types"; import {LabelUtil} from "../../../utils/LabelUtil"; import {LabelsSelector} from "../../../store/selectors/LabelsSelector"; import {LabelActions} from "../../../logic/actions/LabelActions"; +import {ProjectType} from "../../../data/enums/ProjectType"; interface IProps { + projectType: ProjectType; updateActivePopupType: (activePopupType: PopupWindowType) => any; updateLabelNames: (labels: LabelName[]) => any; isUpdate: boolean; } -const InsertLabelNamesPopup: React.FC = ({updateActivePopupType, updateLabelNames, isUpdate}) => { +const InsertLabelNamesPopup: React.FC = ( + { + projectType, + updateActivePopupType, + updateLabelNames, + isUpdate + }) => { const initialLabels = LabelUtil.convertLabelNamesListToMap(LabelsSelector.getLabelNames()); const [labelNames, setLabelNames] = useState(initialLabels); @@ -64,7 +72,11 @@ const InsertLabelNamesPopup: React.FC = ({updateActivePopupType, updateL if (labelNamesList.length > 0) { updateLabelNames(LabelUtil.convertMapToLabelNamesList(labelNames)); } - updateActivePopupType(PopupWindowType.LOAD_AI_MODEL); + + if (projectType === ProjectType.OBJECT_DETECTION) + updateActivePopupType(PopupWindowType.LOAD_AI_MODEL); + else + updateActivePopupType(null); }; const onUpdateAccept = () => { @@ -152,7 +164,9 @@ const mapDispatchToProps = { updateLabelNames }; -const mapStateToProps = (state: AppState) => ({}); +const mapStateToProps = (state: AppState) => ({ + projectType: state.general.projectData.type +}); export default connect( mapStateToProps, diff --git a/src/views/PopupView/LoadLabelNamesPopup/LoadLabelNamesPopup.tsx b/src/views/PopupView/LoadLabelNamesPopup/LoadLabelNamesPopup.tsx index 37e16e5d..5ff29372 100644 --- a/src/views/PopupView/LoadLabelNamesPopup/LoadLabelNamesPopup.tsx +++ b/src/views/PopupView/LoadLabelNamesPopup/LoadLabelNamesPopup.tsx @@ -114,6 +114,7 @@ const LoadLabelNamesPopup: React.FC = ({updateActivePopupType, updateLab renderContent={renderContent} acceptLabel={"Start project"} onAccept={onAccept} + disableAcceptButton={labelsList.length === 0} rejectLabel={"Create labels list"} onReject={onReject} />