diff --git a/mkdocs.yml b/mkdocs.yml index fa518bec..dedabd4d 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -15,7 +15,7 @@ copyright: Copyright © 2019 Piotr Skalski # Customization extra: - version: 1.8.0-alpha + version: 1.9.0-alpha social: - icon: fontawesome/brands/github link: https://github.com/SkalskiP diff --git a/package-lock.json b/package-lock.json index db672389..679e84f7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "make-sense", - "version": "1.0.0", + "version": "1.9.0-alpha", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "make-sense", - "version": "1.0.0", + "version": "1.9.0-alpha", "dependencies": { "@emotion/react": "^11.9.3", "@emotion/styled": "^11.9.3", diff --git a/package.json b/package.json index 90865e16..72335fae 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "make-sense", - "version": "1.0.0", + "version": "1.9.0-alpha", "private": true, "dependencies": { "@emotion/react": "^11.9.3", diff --git a/public/ico/poll.png b/public/ico/poll.png new file mode 100644 index 00000000..f0428cb0 Binary files /dev/null and b/public/ico/poll.png differ diff --git a/public/ico/stats.png b/public/ico/stats.png new file mode 100644 index 00000000..d60ca5eb Binary files /dev/null and b/public/ico/stats.png differ diff --git a/src/data/enums/Notification.ts b/src/data/enums/Notification.ts index 6071540a..bf41069e 100644 --- a/src/data/enums/Notification.ts +++ b/src/data/enums/Notification.ts @@ -1,4 +1,6 @@ export enum Notification { EMPTY_LABEL_NAME_ERROR = 0, - NON_UNIQUE_LABEL_NAMES_ERROR = 1 + NON_UNIQUE_LABEL_NAMES_ERROR = 1, + ABOUT_TO_REMOVE_USED_LABEL_NAME_WARNING = 2, + REMOVED_USED_LABEL_NAME_WARNING = 3 } diff --git a/src/data/enums/PopupWindowType.ts b/src/data/enums/PopupWindowType.ts index 46be29ea..3f5a35c7 100644 --- a/src/data/enums/PopupWindowType.ts +++ b/src/data/enums/PopupWindowType.ts @@ -8,5 +8,6 @@ export enum PopupWindowType { IMPORT_ANNOTATIONS = 'IMPORT_ANNOTATIONS', INSERT_LABEL_NAMES = 'INSERT_LABEL_NAMES', EXIT_PROJECT = 'EXIT_PROJECT', - LOADER = 'LOADER' + LOADER = 'LOADER', + PER_LABEL_ID_COUNTS_STATISTICS = 'PER_LABEL_ID_COUNTS_STATISTICS' } diff --git a/src/data/info/DropDownMenuData.ts b/src/data/info/DropDownMenuData.ts index 1fa011d4..e62d5b78 100644 --- a/src/data/info/DropDownMenuData.ts +++ b/src/data/info/DropDownMenuData.ts @@ -1,13 +1,14 @@ import {updateActivePopupType} from '../../store/general/actionCreators'; import {PopupWindowType} from '../enums/PopupWindowType'; import {store} from '../../index'; +import {LabelsSelector} from '../../store/selectors/LabelsSelector'; export type DropDownMenuNode = { name: string description?: string imageSrc: string imageAlt: string - disabled: boolean + disabled: (() => boolean) | boolean onClick?: () => void children?: DropDownMenuNode[] } @@ -20,7 +21,7 @@ export const DropDownMenuData: DropDownMenuNode[] = [ disabled: false, children: [ { - name: 'Edit Labels', + name: 'Edit labels', description: 'Modify labels list', imageSrc: 'ico/tags.png', imageAlt: 'labels', @@ -28,7 +29,7 @@ export const DropDownMenuData: DropDownMenuNode[] = [ onClick: () => store.dispatch(updateActivePopupType(PopupWindowType.UPDATE_LABEL)) }, { - name: 'Import Images', + name: 'Import images', description: 'Load more images', imageSrc: 'ico/camera.png', imageAlt: 'images', @@ -36,7 +37,7 @@ export const DropDownMenuData: DropDownMenuNode[] = [ onClick: () => store.dispatch(updateActivePopupType(PopupWindowType.IMPORT_IMAGES)) }, { - name: 'Import Annotations', + name: 'Import annotations', description: 'Import annotations from file', imageSrc: 'ico/import-labels.png', imageAlt: 'import-labels', @@ -44,7 +45,7 @@ export const DropDownMenuData: DropDownMenuNode[] = [ onClick: () => store.dispatch(updateActivePopupType(PopupWindowType.IMPORT_ANNOTATIONS)) }, { - name: 'Export Annotations', + name: 'Export annotations', description: 'Export annotations to file', imageSrc: 'ico/export-labels.png', imageAlt: 'export-labels', @@ -52,7 +53,7 @@ export const DropDownMenuData: DropDownMenuNode[] = [ onClick: () => store.dispatch(updateActivePopupType(PopupWindowType.EXPORT_ANNOTATIONS)) }, { - name: 'Load AI Model', + name: 'Load AI model', description: 'Load our pre-trained annotation models', imageSrc: 'ico/ai.png', imageAlt: 'load-ai-model', @@ -61,6 +62,22 @@ export const DropDownMenuData: DropDownMenuNode[] = [ }, ] }, + { + name: 'Insights', + imageSrc: 'ico/stats.png', + imageAlt: 'insights', + disabled: false, + children: [ + { + name: 'Label distribution', + description: 'Display per label name annotations count', + imageSrc: 'ico/tags.png', + imageAlt: 'label-distribution', + disabled: () => LabelsSelector.getLabelNames().length === 0, + onClick: () => store.dispatch(updateActivePopupType(PopupWindowType.PER_LABEL_ID_COUNTS_STATISTICS)) + }, + ] + }, { name: 'Community', imageSrc: 'ico/plant.png', @@ -76,12 +93,20 @@ export const DropDownMenuData: DropDownMenuNode[] = [ onClick: () => window.open('https://skalskip.github.io/make-sense', '_blank') }, { - name: 'Bugs and Features', + name: 'Bugs and features', description: 'Report a bug or propose a new feature', imageSrc: 'ico/bug.png', imageAlt: 'bug', disabled: false, onClick: () => window.open('https://github.com/SkalskiP/make-sense/issues', '_blank') + }, + { + name: 'Vote for next BIG feature', + description: 'Vote for next big feature that we will add to Make Sense', + imageSrc: 'ico/poll.png', + imageAlt: 'vote', + disabled: false, + onClick: () => window.open('https://github.com/SkalskiP/make-sense/discussions/269', '_blank') } ] } diff --git a/src/data/info/NotificationsData.ts b/src/data/info/NotificationsData.ts index 30ab2108..8c6ae7d4 100644 --- a/src/data/info/NotificationsData.ts +++ b/src/data/info/NotificationsData.ts @@ -17,5 +17,11 @@ export const NotificationsDataMap = { header: 'Non unique label names', description: 'Looks like not all your label names are unique. Unique names are necessary to guarantee correct' + ' data export when you complete your work. Make your names unique and try again.' + }, + [Notification.ABOUT_TO_REMOVE_USED_LABEL_NAME_WARNING]: { + header: 'Used label names', + description: 'Looks like you are about to remove label name that is currently used by some of your annotations. ' + + 'Keep in mind that annotations without specified label name can not be exported. You can still abort by ' + + 'pressing "Cancel".' } } diff --git a/src/logic/render/LineRenderEngine.ts b/src/logic/render/LineRenderEngine.ts index 008654f8..84afd435 100644 --- a/src/logic/render/LineRenderEngine.ts +++ b/src/logic/render/LineRenderEngine.ts @@ -17,13 +17,13 @@ import {EditorActions} from '../actions/EditorActions'; import {LabelsSelector} from '../../store/selectors/LabelsSelector'; import {DrawUtil} from '../../utils/DrawUtil'; import {GeneralSelector} from '../../store/selectors/GeneralSelector'; -import { v4 as uuidv4 } from 'uuid'; import {ILine} from '../../interfaces/ILine'; import {LineUtil} from '../../utils/LineUtil'; import {updateCustomCursorStyle} from '../../store/general/actionCreators'; import {CustomCursorStyle} from '../../data/enums/CustomCursorStyle'; import {LineAnchorType} from '../../data/enums/LineAnchorType'; import {Settings} from '../../settings/Settings'; +import {LabelUtil} from '../../utils/LabelUtil'; export class LineRenderEngine extends BaseRenderEngine { @@ -203,12 +203,7 @@ export class LineRenderEngine extends BaseRenderEngine { const lineOnImage = RenderEngineUtil.transferLineFromViewPortContentToImage(lineOnCanvas, data); const activeLabelId = LabelsSelector.getActiveLabelNameId(); const imageData: ImageData = LabelsSelector.getActiveImageData(); - const labelLine: LabelLine = { - id: uuidv4(), - labelId: activeLabelId, - line: lineOnImage, - isVisible: true - }; + const labelLine: LabelLine = LabelUtil.createLabelLine(activeLabelId, lineOnImage); imageData.labelLines.push(labelLine); store.dispatch(updateImageDataById(imageData.id, imageData)); store.dispatch(updateFirstLabelCreatedFlag(true)); diff --git a/src/utils/LabelUtil.ts b/src/utils/LabelUtil.ts index feec03d6..ec5dacdc 100644 --- a/src/utils/LabelUtil.ts +++ b/src/utils/LabelUtil.ts @@ -1,11 +1,20 @@ -import {Annotation, LabelName, LabelPoint, LabelPolygon, LabelRect} from '../store/labels/types'; -import { v4 as uuidv4 } from 'uuid'; -import {find} from 'lodash'; +import {Annotation, ImageData, LabelLine, LabelName, LabelPoint, LabelPolygon, LabelRect} from '../store/labels/types'; +import {v4 as uuidv4} from 'uuid'; +import {find, sample} from 'lodash'; import {IRect} from '../interfaces/IRect'; import {LabelStatus} from '../data/enums/LabelStatus'; import {IPoint} from '../interfaces/IPoint'; -import { sample } from 'lodash'; import {Settings} from '../settings/Settings'; +import {ILine} from 'src/interfaces/ILine'; + +export type LabelCount = { + point: number; + line: number; + polygon: number; + rect: number; +} + +export type PerLabelIdCountSummary = Record; export class LabelUtil { public static createLabelName(name: string): LabelName { @@ -16,19 +25,19 @@ export class LabelUtil { } } - public static createLabelRect(labelId: string, rect: IRect): LabelRect { + public static createLabelRect(labelId: string | null, rect: IRect, status: LabelStatus = LabelStatus.ACCEPTED): LabelRect { return { id: uuidv4(), labelId, rect, isVisible: true, isCreatedByAI: false, - status: LabelStatus.ACCEPTED, + status, suggestedLabel: null } } - public static createLabelPolygon(labelId: string, vertices: IPoint[]): LabelPolygon { + public static createLabelPolygon(labelId: string | null, vertices: IPoint[]): LabelPolygon { return { id: uuidv4(), labelId, @@ -37,18 +46,27 @@ export class LabelUtil { } } - public static createLabelPoint(labelId: string, point: IPoint): LabelPoint { + public static createLabelPoint(labelId: string | null, point: IPoint, status: LabelStatus = LabelStatus.ACCEPTED): LabelPoint { return { id: uuidv4(), labelId, point, isVisible: true, isCreatedByAI: false, - status: LabelStatus.ACCEPTED, + status, suggestedLabel: null } } + public static createLabelLine(labelId: string | null, line: ILine): LabelLine { + return { + id: uuidv4(), + labelId, + line, + isVisible: true + } + } + public static toggleAnnotationVisibility(annotation: AnnotationType): AnnotationType { return { ...annotation, @@ -56,7 +74,32 @@ export class LabelUtil { } } - public static labelNamesIdsDiff(oldLabelNames: LabelName[], newLabelNames: LabelName[]): string[] { + public static calculatePerLabelIdCountSummary(labels: LabelName[], imagesData: ImageData[]): PerLabelIdCountSummary { + let labelCount = labels.reduce((acc: PerLabelIdCountSummary, label: LabelName) => { + acc[label.id] = { point: 0, line: 0, polygon: 0, rect: 0} + return acc; + }, {}); + labelCount = imagesData.reduce((acc: PerLabelIdCountSummary, imageData: ImageData) => { + for (const labelRect of imageData.labelRects) { + if (labelRect.labelId !== null && labelRect.status === LabelStatus.ACCEPTED) + acc[labelRect.labelId].rect += 1 + } + for (const labelPoint of imageData.labelPoints) { + if (labelPoint.labelId !== null && labelPoint.status === LabelStatus.ACCEPTED) + acc[labelPoint.labelId].point += 1 + } + for (const labelLine of imageData.labelLines) { + if (labelLine.labelId !== null) acc[labelLine.labelId].line += 1 + } + for (const labelPolygon of imageData.labelPolygons) { + if (labelPolygon.labelId !== null) acc[labelPolygon.labelId].polygon += 1 + } + return acc; + }, labelCount) + return labelCount + } + + public static calculateMissingLabelNamesIds(oldLabelNames: LabelName[], newLabelNames: LabelName[]): string[] { return oldLabelNames.reduce((missingIds: string[], labelName: LabelName) => { if (!find(newLabelNames, { 'id': labelName.id })) { missingIds.push(labelName.id); diff --git a/src/utils/RectUtil.ts b/src/utils/RectUtil.ts index 4129ca03..28baa5ac 100644 --- a/src/utils/RectUtil.ts +++ b/src/utils/RectUtil.ts @@ -1,9 +1,9 @@ -import {IRect} from "../interfaces/IRect"; -import {IPoint} from "../interfaces/IPoint"; -import {ISize} from "../interfaces/ISize"; -import {RectAnchor} from "../data/RectAnchor"; -import {NumberUtil} from "./NumberUtil"; -import {Direction} from "../data/enums/Direction"; +import {IRect} from '../interfaces/IRect'; +import {IPoint} from '../interfaces/IPoint'; +import {ISize} from '../interfaces/ISize'; +import {RectAnchor} from '../data/RectAnchor'; +import {NumberUtil} from './NumberUtil'; +import {Direction} from '../data/enums/Direction'; export class RectUtil { public static getRatio(rect: IRect): number { @@ -61,7 +61,7 @@ export class RectUtil { } } } - + public static resizeRect(inputRect: IRect, rectAnchor: Direction, delta): IRect { const rect: IRect = {...inputRect}; switch (rectAnchor) { @@ -100,17 +100,17 @@ export class RectUtil { rect.height += delta.y; break; } - + if (rect.width < 0) { rect.x = rect.x + rect.width; rect.width = -rect.width; } - + if (rect.height < 0) { rect.y = rect.y + rect.height; rect.height = -rect.height; } - + return rect; } diff --git a/src/utils/__tests__/LabelUtil.test.ts b/src/utils/__tests__/LabelUtil.test.ts index 13a0e599..8a3e58f2 100644 --- a/src/utils/__tests__/LabelUtil.test.ts +++ b/src/utils/__tests__/LabelUtil.test.ts @@ -1,15 +1,36 @@ -import { IRect } from '../../interfaces/IRect'; -import { LabelUtil } from '../LabelUtil'; -import {LabelPoint, LabelPolygon, LabelRect} from '../../store/labels/types'; +import {IRect} from '../../interfaces/IRect'; +import {LabelUtil} from '../LabelUtil'; +import {ImageData, LabelLine, LabelPoint, LabelPolygon, LabelRect} from '../../store/labels/types'; import {LabelStatus} from '../../data/enums/LabelStatus'; import {IPoint} from '../../interfaces/IPoint'; +import {ILine} from '../../interfaces/ILine'; const mockUUID: string = '123e4567-e89b-12d3-a456-426614174000' jest.mock('uuid', () => ({ v4: () => mockUUID })); -describe('LabelUtil createLabelRect method', () => { - it('return correct LabelRect object', () => { +const mockImageData = ( + labelRects: LabelRect[] = [], + labelPoints: LabelPoint[] = [], + labelLines: LabelLine[] = [], + labelPolygons: LabelPolygon[] = [] +): ImageData => { + return { + id: mockUUID, + fileData: null, + loadStatus: true, + labelRects, + labelPoints, + labelLines, + labelPolygons, + labelNameIds: [], + isVisitedByObjectDetector: false, + isVisitedByPoseDetector: false + } +} + +describe('LabelUtil.createLabelRect tests', () => { + test('return correct LabelRect object', () => { // given const labelId: string = '1'; const rect: IRect = { @@ -36,8 +57,8 @@ describe('LabelUtil createLabelRect method', () => { }); }); -describe('LabelUtil createLabelPolygon method', () => { - it('return correct LabelPolygon object', () => { +describe('LabelUtil.createLabelPolygon tests', () => { + test('return correct LabelPolygon object', () => { // given const labelId: string = '1'; const vertices: IPoint[] = [ @@ -69,8 +90,8 @@ describe('LabelUtil createLabelPolygon method', () => { }); }); -describe('LabelUtil createLabelPoint method', () => { - it('return correct LabelPoint object', () => { +describe('LabelUtil.createLabelPoint tests', () => { + test('return correct LabelPoint object', () => { // given const labelId: string = '1'; const point: IPoint = { @@ -94,3 +115,519 @@ describe('LabelUtil createLabelPoint method', () => { expect(result).toEqual(expectedResult); }); }); + +describe('LabelUtil.createLabelLine tests', () => { + test('return correct LabelLine object', () => { + // given + const labelId: string = '1'; + const line: ILine = { + start: {x: 0, y: 0}, + end: {x: 10, y: 10} + }; + + // when + const result = LabelUtil.createLabelLine(labelId, line); + + // then + const expectedResult: LabelLine = { + id: mockUUID, + labelId, + line, + isVisible: true + } + expect(result).toEqual(expectedResult); + }); +}); + +describe('LabelUtil.calculateMissingLabelNamesIds tests', () => { + test('return empty list when oldLabelNames and newLabelNames are empty', () => { + // given + const oldLabelNames = []; + const newLabelNames = []; + + // when + const result = LabelUtil.calculateMissingLabelNamesIds(oldLabelNames, newLabelNames); + + // then + const expectedResult = []; + expect(result).toEqual(expectedResult); + }); + + test('return correct result when oldLabelNames contains items and newLabelNames is empty', () => { + // given + const oldLabelNames = [ + { + id: 'label-id-1', + name: 'label-name-1' + }, + { + id: 'label-id-2', + name: 'label-name-2' + }, + { + id: 'label-id-3', + name: 'label-name-3' + } + ]; + const newLabelNames = []; + + // when + const result = LabelUtil.calculateMissingLabelNamesIds(oldLabelNames, newLabelNames); + + // then + const expectedResult = ['label-id-1', 'label-id-2', 'label-id-3']; + expect(result).toEqual(expectedResult); + }); + + test('return empty list when oldLabelNames is empty and newLabelNames contains items', () => { + // given + const oldLabelNames = []; + const newLabelNames = [ + { + id: 'label-id-1', + name: 'label-name-1' + }, + { + id: 'label-id-2', + name: 'label-name-2' + }, + { + id: 'label-id-3', + name: 'label-name-3' + } + ]; + + + // when + const result = LabelUtil.calculateMissingLabelNamesIds(oldLabelNames, newLabelNames); + + // then + const expectedResult = []; + expect(result).toEqual(expectedResult); + }); + + test('return empty list when oldLabelNames and newLabelNames are the same', () => { + // given + const oldLabelNames = [ + { + id: 'label-id-1', + name: 'label-name-1' + }, + { + id: 'label-id-2', + name: 'label-name-2' + }, + { + id: 'label-id-3', + name: 'label-name-3' + } + ]; + const newLabelNames = [ + { + id: 'label-id-1', + name: 'label-name-1' + }, + { + id: 'label-id-2', + name: 'label-name-2' + }, + { + id: 'label-id-3', + name: 'label-name-3' + } + ]; + + // when + const result = LabelUtil.calculateMissingLabelNamesIds(oldLabelNames, newLabelNames); + + // then + const expectedResult = []; + expect(result).toEqual(expectedResult); + }); + + test('return correct list with oldLabelNames and newLabelNames ids diff', () => { + // given + const oldLabelNames = [ + { + id: 'label-id-1', + name: 'label-name-1' + }, + { + id: 'label-id-2', + name: 'label-name-2' + }, + { + id: 'label-id-3', + name: 'label-name-3' + } + ]; + const newLabelNames = [ + { + id: 'label-id-1', + name: 'label-name-1' + }, + { + id: 'label-id-3', + name: 'label-name-3' + }, + { + id: 'label-id-4', + name: 'label-name-3' + } + ]; + + // when + const result = LabelUtil.calculateMissingLabelNamesIds(oldLabelNames, newLabelNames); + + // then + const expectedResult = ['label-id-2']; + expect(result).toEqual(expectedResult); + }); +}); + +describe('LabelUtil.calculatePerLabelIdCountSummary tests', () => { + test('return empty summary object when labelNames is empty', () => { + // given + const labelNames = []; + const imagesData = []; + + // when + const result = LabelUtil.calculatePerLabelIdCountSummary(labelNames, imagesData); + + // then + const expectedResult = {}; + expect(result).toEqual(expectedResult); + }); + + test('return summary object with empty counts when imagesData is empty', () => { + // given + const labelNames = [ + { + id: 'label-id-1', + name: 'label-name-1' + }, + { + id: 'label-id-2', + name: 'label-name-2' + }, + { + id: 'label-id-3', + name: 'label-name-3' + } + ]; + const imagesData = []; + + // when + const result = LabelUtil.calculatePerLabelIdCountSummary(labelNames, imagesData); + + // then + const expectedResult = { + 'label-id-1': { point: 0, line: 0, polygon: 0, rect: 0}, + 'label-id-2': { point: 0, line: 0, polygon: 0, rect: 0}, + 'label-id-3': { point: 0, line: 0, polygon: 0, rect: 0}, + }; + expect(result).toEqual(expectedResult); + }); + + test('return summary object with empty counts when imagesData does not contain any annotations', () => { + // given + const labelNames = [ + { + id: 'label-id-1', + name: 'label-name-1' + }, + { + id: 'label-id-2', + name: 'label-name-2' + }, + { + id: 'label-id-3', + name: 'label-name-3' + } + ]; + const imagesData = [ + mockImageData(), + mockImageData(), + mockImageData() + ]; + + // when + const result = LabelUtil.calculatePerLabelIdCountSummary(labelNames, imagesData); + + // then + const expectedResult = { + 'label-id-1': { point: 0, line: 0, polygon: 0, rect: 0}, + 'label-id-2': { point: 0, line: 0, polygon: 0, rect: 0}, + 'label-id-3': { point: 0, line: 0, polygon: 0, rect: 0}, + }; + expect(result).toEqual(expectedResult); + }); + + test('return summary object with only rect counts when imagesData does contain only rect annotations', () => { + // given + const labelNames = [ + { + id: 'label-id-1', + name: 'label-name-1' + }, + { + id: 'label-id-2', + name: 'label-name-2' + }, + { + id: 'label-id-3', + name: 'label-name-3' + } + ]; + const imagesData = [ + mockImageData( + [ + LabelUtil.createLabelRect('label-id-1', {x: 0, y: 0, width: 1, height: 1}), + LabelUtil.createLabelRect('label-id-1', {x: 0, y: 0, width: 1, height: 1}), + LabelUtil.createLabelRect('label-id-2', {x: 0, y: 0, width: 1, height: 1}), + ] + ), + mockImageData( + [ + LabelUtil.createLabelRect('label-id-1', {x: 0, y: 0, width: 1, height: 1}), + LabelUtil.createLabelRect('label-id-2', {x: 0, y: 0, width: 1, height: 1}), + LabelUtil.createLabelRect('label-id-2', {x: 0, y: 0, width: 1, height: 1}), + LabelUtil.createLabelRect('label-id-3', {x: 0, y: 0, width: 1, height: 1}), + ] + ), + mockImageData() + ]; + + // when + const result = LabelUtil.calculatePerLabelIdCountSummary(labelNames, imagesData); + + // then + const expectedResult = { + 'label-id-1': { point: 0, line: 0, polygon: 0, rect: 3}, + 'label-id-2': { point: 0, line: 0, polygon: 0, rect: 3}, + 'label-id-3': { point: 0, line: 0, polygon: 0, rect: 1}, + }; + expect(result).toEqual(expectedResult); + }); + + test('return summary object with only rect counts when imagesData does contain only rect annotations but some labelIds are null', () => { + // given + const labelNames = [ + { + id: 'label-id-1', + name: 'label-name-1' + }, + { + id: 'label-id-2', + name: 'label-name-2' + }, + { + id: 'label-id-3', + name: 'label-name-3' + } + ]; + const imagesData = [ + mockImageData( + [ + LabelUtil.createLabelRect(null, {x: 0, y: 0, width: 1, height: 1}), + LabelUtil.createLabelRect('label-id-1', {x: 0, y: 0, width: 1, height: 1}), + LabelUtil.createLabelRect('label-id-2', {x: 0, y: 0, width: 1, height: 1}), + ] + ), + mockImageData( + [ + LabelUtil.createLabelRect('label-id-1', {x: 0, y: 0, width: 1, height: 1}), + LabelUtil.createLabelRect('label-id-2', {x: 0, y: 0, width: 1, height: 1}), + LabelUtil.createLabelRect(null, {x: 0, y: 0, width: 1, height: 1}), + LabelUtil.createLabelRect('label-id-3', {x: 0, y: 0, width: 1, height: 1}), + ] + ), + mockImageData() + ]; + + // when + const result = LabelUtil.calculatePerLabelIdCountSummary(labelNames, imagesData); + + // then + const expectedResult = { + 'label-id-1': { point: 0, line: 0, polygon: 0, rect: 2}, + 'label-id-2': { point: 0, line: 0, polygon: 0, rect: 2}, + 'label-id-3': { point: 0, line: 0, polygon: 0, rect: 1}, + }; + expect(result).toEqual(expectedResult); + }); + + test('return summary object with only rect counts when imagesData does contain only rect annotations but labels are rejected', () => { + // given + const labelNames = [ + { + id: 'label-id-1', + name: 'label-name-1' + }, + { + id: 'label-id-2', + name: 'label-name-2' + }, + { + id: 'label-id-3', + name: 'label-name-3' + } + ]; + const imagesData = [ + mockImageData( + [ + LabelUtil.createLabelRect('label-id-1', {x: 0, y: 0, width: 1, height: 1}, LabelStatus.REJECTED), + LabelUtil.createLabelRect('label-id-1', {x: 0, y: 0, width: 1, height: 1}), + LabelUtil.createLabelRect('label-id-2', {x: 0, y: 0, width: 1, height: 1}), + ] + ), + mockImageData( + [ + LabelUtil.createLabelRect('label-id-1', {x: 0, y: 0, width: 1, height: 1}), + LabelUtil.createLabelRect('label-id-2', {x: 0, y: 0, width: 1, height: 1}), + LabelUtil.createLabelRect('label-id-2', {x: 0, y: 0, width: 1, height: 1}, LabelStatus.REJECTED), + LabelUtil.createLabelRect('label-id-3', {x: 0, y: 0, width: 1, height: 1}), + ] + ), + mockImageData() + ]; + + // when + const result = LabelUtil.calculatePerLabelIdCountSummary(labelNames, imagesData); + + // then + const expectedResult = { + 'label-id-1': { point: 0, line: 0, polygon: 0, rect: 2}, + 'label-id-2': { point: 0, line: 0, polygon: 0, rect: 2}, + 'label-id-3': { point: 0, line: 0, polygon: 0, rect: 1}, + }; + expect(result).toEqual(expectedResult); + }); + + test('return summary object with only point counts when imagesData does contain only point annotations', () => { + // given + const labelNames = [ + { + id: 'label-id-1', + name: 'label-name-1' + }, + { + id: 'label-id-2', + name: 'label-name-2' + }, + { + id: 'label-id-3', + name: 'label-name-3' + } + ]; + const imagesData = [ + mockImageData([], + [ + LabelUtil.createLabelPoint('label-id-1', {x: 0, y: 0}), + LabelUtil.createLabelPoint('label-id-2', {x: 0, y: 0}), + ] + ), + mockImageData(), + mockImageData([], + [ + LabelUtil.createLabelPoint('label-id-3', {x: 0, y: 0}), + LabelUtil.createLabelPoint('label-id-2', {x: 0, y: 0}), + LabelUtil.createLabelPoint('label-id-2', {x: 0, y: 0}), + ] + ), + ]; + + // when + const result = LabelUtil.calculatePerLabelIdCountSummary(labelNames, imagesData); + + // then + const expectedResult = { + 'label-id-1': { point: 1, line: 0, polygon: 0, rect: 0}, + 'label-id-2': { point: 3, line: 0, polygon: 0, rect: 0}, + 'label-id-3': { point: 1, line: 0, polygon: 0, rect: 0}, + }; + expect(result).toEqual(expectedResult); + }); + + test('return summary object with only line counts when imagesData does contain only line annotations', () => { + // given + const labelNames = [ + { + id: 'label-id-1', + name: 'label-name-1' + }, + { + id: 'label-id-2', + name: 'label-name-2' + }, + { + id: 'label-id-3', + name: 'label-name-3' + } + ]; + const imagesData = [ + mockImageData([], [], + [ + LabelUtil.createLabelLine('label-id-1', {start: {x: 0, y: 0}, end: {x: 1, y: 1}}), + LabelUtil.createLabelLine('label-id-1', {start: {x: 0, y: 0}, end: {x: 1, y: 1}}), + LabelUtil.createLabelLine('label-id-1', {start: {x: 0, y: 0}, end: {x: 1, y: 1}}), + ] + ), + mockImageData([], [], + [ + LabelUtil.createLabelLine('label-id-2', {start: {x: 0, y: 0}, end: {x: 1, y: 1}}), + LabelUtil.createLabelLine('label-id-2', {start: {x: 0, y: 0}, end: {x: 1, y: 1}}) + ] + ), + ]; + + // when + const result = LabelUtil.calculatePerLabelIdCountSummary(labelNames, imagesData); + + // then + const expectedResult = { + 'label-id-1': { point: 0, line: 3, polygon: 0, rect: 0}, + 'label-id-2': { point: 0, line: 2, polygon: 0, rect: 0}, + 'label-id-3': { point: 0, line: 0, polygon: 0, rect: 0}, + }; + expect(result).toEqual(expectedResult); + }); + + test('return summary object with only polygon counts when imagesData does contain only polygon annotations', () => { + // given + const labelNames = [ + { + id: 'label-id-1', + name: 'label-name-1' + }, + { + id: 'label-id-2', + name: 'label-name-2' + }, + { + id: 'label-id-3', + name: 'label-name-3' + } + ]; + const imagesData = [ + mockImageData([], [], [], + [ + LabelUtil.createLabelPolygon('label-id-1', [{x: 0, y: 0}, {x: 1, y: 0}, {x: 0, y: 1}]), + LabelUtil.createLabelPolygon('label-id-2', [{x: 0, y: 0}, {x: 1, y: 0}, {x: 0, y: 1}]), + LabelUtil.createLabelPolygon('label-id-3', [{x: 0, y: 0}, {x: 1, y: 0}, {x: 0, y: 1}]), + ] + ) + ]; + + // when + const result = LabelUtil.calculatePerLabelIdCountSummary(labelNames, imagesData); + + // then + const expectedResult = { + 'label-id-1': { point: 0, line: 0, polygon: 1, rect: 0}, + 'label-id-2': { point: 0, line: 0, polygon: 1, rect: 0}, + 'label-id-3': { point: 0, line: 0, polygon: 1, rect: 0}, + }; + expect(result).toEqual(expectedResult); + }); +}); diff --git a/src/views/Common/UnderlineTextButton/UnderlineTextButton.tsx b/src/views/Common/UnderlineTextButton/UnderlineTextButton.tsx index ed58f5fe..8d4448a2 100644 --- a/src/views/Common/UnderlineTextButton/UnderlineTextButton.tsx +++ b/src/views/Common/UnderlineTextButton/UnderlineTextButton.tsx @@ -17,9 +17,9 @@ export const UnderlineTextButton = (props: IProps) => { const getClassName = () => { return classNames('UnderlineTextButton', { - under: under, - over: over, - active: active, + under, + over, + active, }) }; diff --git a/src/views/EditorView/SideNavigationBar/ImagePreview/ImagePreview.scss b/src/views/EditorView/SideNavigationBar/ImagePreview/ImagePreview.scss index f1161bfa..c49ced12 100644 --- a/src/views/EditorView/SideNavigationBar/ImagePreview/ImagePreview.scss +++ b/src/views/EditorView/SideNavigationBar/ImagePreview/ImagePreview.scss @@ -29,15 +29,22 @@ user-select: none; } - .CheckBox { + .annotations-count { position: absolute; z-index: 1000; - max-width: 20px; - max-height: 20px; - bottom: -10px; - left: -10px; - filter: invert(1) brightness(35%) sepia(100%) hue-rotate(172deg) saturate(2000%); // fallback if new css variables are not supported by browser - filter: invert(1) brightness(35%) sepia(100%) hue-rotate(var(--hue-value)) saturate(2000%); + width: 24px; + height: 24px; + bottom: -12px; + left: -12px; + border-radius: 2px; + background-color: $secondaryColor; // fallback if new css variables are not supported by browser + background-color: var(--leading-color); + color: white; + font-weight: 700; + + display: flex; + justify-content: center; + align-items: center; } } @@ -67,4 +74,4 @@ transform: translate(2px, -2px); } } -} \ No newline at end of file +} diff --git a/src/views/EditorView/SideNavigationBar/ImagePreview/ImagePreview.tsx b/src/views/EditorView/SideNavigationBar/ImagePreview/ImagePreview.tsx index 36f573d2..58f45462 100644 --- a/src/views/EditorView/SideNavigationBar/ImagePreview/ImagePreview.tsx +++ b/src/views/EditorView/SideNavigationBar/ImagePreview/ImagePreview.tsx @@ -1,26 +1,26 @@ -import classNames from "classnames"; +import classNames from 'classnames'; import React from 'react'; -import { connect } from "react-redux"; -import { ClipLoader } from "react-spinners"; -import { ImageLoadManager } from "../../../../logic/imageRepository/ImageLoadManager"; -import { IRect } from "../../../../interfaces/IRect"; -import { ISize } from "../../../../interfaces/ISize"; -import { ImageRepository } from "../../../../logic/imageRepository/ImageRepository"; -import { AppState } from "../../../../store"; -import { updateImageDataById } from "../../../../store/labels/actionCreators"; -import { ImageData } from "../../../../store/labels/types"; -import { FileUtil } from "../../../../utils/FileUtil"; -import { RectUtil } from "../../../../utils/RectUtil"; +import { connect } from 'react-redux'; +import { ClipLoader } from 'react-spinners'; +import { ImageLoadManager } from '../../../../logic/imageRepository/ImageLoadManager'; +import { IRect } from '../../../../interfaces/IRect'; +import { ISize } from '../../../../interfaces/ISize'; +import { ImageRepository } from '../../../../logic/imageRepository/ImageRepository'; +import { AppState } from '../../../../store'; +import { updateImageDataById } from '../../../../store/labels/actionCreators'; +import { ImageData } from '../../../../store/labels/types'; +import { FileUtil } from '../../../../utils/FileUtil'; +import { RectUtil } from '../../../../utils/RectUtil'; import './ImagePreview.scss'; -import { CSSHelper } from "../../../../logic/helpers/CSSHelper"; +import { CSSHelper } from '../../../../logic/helpers/CSSHelper'; interface IProps { imageData: ImageData; style: React.CSSProperties; size: ISize; isScrolling?: boolean; - isChecked?: boolean; - onClick?: () => any; + annotationsCount?: number; + onClick?: () => void; isSelected?: boolean; updateImageDataById: (id: string, newImageData: ImageData) => any; } @@ -64,7 +64,7 @@ class ImagePreview extends React.Component { this.props.imageData.id !== nextProps.imageData.id || this.state.image !== nextState.image || this.props.isSelected !== nextProps.isSelected || - this.props.isChecked !== nextProps.isChecked + this.props.annotationsCount !== nextProps.annotationsCount ) } @@ -126,16 +126,16 @@ class ImagePreview extends React.Component { private getClassName = () => { return classNames( - "ImagePreview", + 'ImagePreview', { - "selected": this.props.isSelected, + 'selected': this.props.isSelected, } ); }; public render() { const { - isChecked, + annotationsCount, style, onClick } = this.props; @@ -149,27 +149,26 @@ class ImagePreview extends React.Component { {(!!this.state.image) ? [
{this.state.image.alt} - {isChecked && {"checkbox"}} + {annotationsCount &&
+ {annotationsCount} +
}
,
] : @@ -191,4 +190,4 @@ const mapStateToProps = (state: AppState) => ({}); export default connect( mapStateToProps, mapDispatchToProps -)(ImagePreview); \ No newline at end of file +)(ImagePreview); diff --git a/src/views/EditorView/SideNavigationBar/ImagesList/ImagesList.tsx b/src/views/EditorView/SideNavigationBar/ImagesList/ImagesList.tsx index eb425014..27239853 100644 --- a/src/views/EditorView/SideNavigationBar/ImagesList/ImagesList.tsx +++ b/src/views/EditorView/SideNavigationBar/ImagesList/ImagesList.tsx @@ -1,22 +1,23 @@ import React from 'react'; -import {connect} from "react-redux"; -import {LabelType} from "../../../../data/enums/LabelType"; -import {ISize} from "../../../../interfaces/ISize"; -import {AppState} from "../../../../store"; -import {ImageData, LabelPoint, LabelRect} from "../../../../store/labels/types"; -import {VirtualList} from "../../../Common/VirtualList/VirtualList"; -import ImagePreview from "../ImagePreview/ImagePreview"; +import {connect} from 'react-redux'; +import {LabelType} from '../../../../data/enums/LabelType'; +import {ISize} from '../../../../interfaces/ISize'; +import {AppState} from '../../../../store'; +import {ImageData, LabelName, LabelPoint, LabelRect} from '../../../../store/labels/types'; +import {VirtualList} from '../../../Common/VirtualList/VirtualList'; +import ImagePreview from '../ImagePreview/ImagePreview'; import './ImagesList.scss'; -import {ContextManager} from "../../../../logic/context/ContextManager"; -import {ContextType} from "../../../../data/enums/ContextType"; -import {ImageActions} from "../../../../logic/actions/ImageActions"; -import {EventType} from "../../../../data/enums/EventType"; -import {LabelStatus} from "../../../../data/enums/LabelStatus"; +import {ContextManager} from '../../../../logic/context/ContextManager'; +import {ContextType} from '../../../../data/enums/ContextType'; +import {ImageActions} from '../../../../logic/actions/ImageActions'; +import {EventType} from '../../../../data/enums/EventType'; +import {LabelStatus} from '../../../../data/enums/LabelStatus'; interface IProps { activeImageIndex: number; imagesData: ImageData[]; activeLabelType: LabelType; + labels: LabelName[]; } interface IState { @@ -56,50 +57,57 @@ class ImagesList extends React.Component { }) }; - private isImageChecked = (index:number): boolean => { + private getAnnotationCount = (index:number): number => { const imageData = this.props.imagesData[index] switch (this.props.activeLabelType) { case LabelType.LINE: - return imageData.labelLines.length > 0 + return imageData.labelLines.length case LabelType.IMAGE_RECOGNITION: - return imageData.labelNameIds.length > 0 + return imageData.labelNameIds.length case LabelType.POINT: return imageData.labelPoints .filter((labelPoint: LabelPoint) => labelPoint.status === LabelStatus.ACCEPTED) - .length > 0 + .length case LabelType.POLYGON: - return imageData.labelPolygons.length > 0 + return imageData.labelPolygons.length case LabelType.RECT: return imageData.labelRects .filter((labelRect: LabelRect) => labelRect.status === LabelStatus.ACCEPTED) - .length > 0 + .length } }; - private onClickHandler = (index: number) => { + private onClickHandler = (index: number): void => { ImageActions.getImageByIndex(index) }; - private renderImagePreview = (index: number, isScrolling: boolean, isVisible: boolean, style: React.CSSProperties) => { + private renderImagePreview = ( + index: number, + isScrolling: boolean, + isVisible: boolean, + style: React.CSSProperties + ) => { + const imagePreviewOnClickHandler = () => this.onClickHandler(index) return this.onClickHandler(index)} + onClick={imagePreviewOnClickHandler} isSelected={this.props.activeImageIndex === index} /> }; public render() { const { size } = this.state; + const imageListOnnClickHandler = () => ContextManager.switchCtx(ContextType.LEFT_NAVBAR) return(
this.imagesListRef = ref} - onClick={() => ContextManager.switchCtx(ContextType.LEFT_NAVBAR)} + onClick={imageListOnnClickHandler} > {!!size && ({ activeImageIndex: state.labels.activeImageIndex, imagesData: state.labels.imagesData, - activeLabelType: state.labels.activeLabelType + activeLabelType: state.labels.activeLabelType, + labels: state.labels.labels }); export default connect( mapStateToProps, mapDispatchToProps -)(ImagesList); \ No newline at end of file +)(ImagesList); diff --git a/src/views/EditorView/SideNavigationBar/LabelInputField/LabelInputField.scss b/src/views/EditorView/SideNavigationBar/LabelInputField/LabelInputField.scss index a9f328b7..a16aa469 100644 --- a/src/views/EditorView/SideNavigationBar/LabelInputField/LabelInputField.scss +++ b/src/views/EditorView/SideNavigationBar/LabelInputField/LabelInputField.scss @@ -18,10 +18,11 @@ align-items: center; align-content: space-between; - .Marker { + .marker { margin-left: 10px; width: 24px; height: 24px; + border-radius: 2px; } .Content { diff --git a/src/views/EditorView/SideNavigationBar/LabelInputField/LabelInputField.tsx b/src/views/EditorView/SideNavigationBar/LabelInputField/LabelInputField.tsx index 786472f1..cf3c12f7 100644 --- a/src/views/EditorView/SideNavigationBar/LabelInputField/LabelInputField.tsx +++ b/src/views/EditorView/SideNavigationBar/LabelInputField/LabelInputField.tsx @@ -180,8 +180,8 @@ class LabelInputField extends React.Component { }} >
diff --git a/src/views/EditorView/TopNavigationBar/DropDownMenu/DropDownMenu.tsx b/src/views/EditorView/TopNavigationBar/DropDownMenu/DropDownMenu.tsx index b5dec111..f2c5a79e 100644 --- a/src/views/EditorView/TopNavigationBar/DropDownMenu/DropDownMenu.tsx +++ b/src/views/EditorView/TopNavigationBar/DropDownMenu/DropDownMenu.tsx @@ -68,20 +68,6 @@ const DropDownMenu: React.FC = ({updatePreventCustomCursorStatusAction}) ); } - const getDropDownContent = () => { - return DropDownMenuData.map((data: DropDownMenuNode, index: number) => getDropDownTab(data, index)) - } - - const wrapOnClick = (onClick?: () => void, disabled?: boolean): () => void => { - return () => { - if (!!disabled) return; - if (!!onClick) onClick(); - setActiveTabIdx(null); - updatePreventCustomCursorStatusAction(false); - document.removeEventListener(EventType.MOUSE_DOWN, onMouseDownBeyondDropDown); - } - } - const getDropDownTab = (data: DropDownMenuNode, index: number) => { return
= ({updatePreventCustomCursorStatusAction})
} + const getDropDownContent = () => { + return DropDownMenuData.map((data: DropDownMenuNode, index: number) => getDropDownTab(data, index)) + } + + const wrapOnClick = (onClick?: () => void, disabled?: boolean): () => void => { + return () => { + if (!!disabled) return; + if (!!onClick) onClick(); + setActiveTabIdx(null); + updatePreventCustomCursorStatusAction(false); + document.removeEventListener(EventType.MOUSE_DOWN, onMouseDownBeyondDropDown); + } + } + const getDropDownWindow = (data: DropDownMenuNode) => { if (activeTabIdx !== null) { const style: React.CSSProperties = { @@ -112,8 +112,9 @@ const DropDownMenu: React.FC = ({updatePreventCustomCursorStatusAction}) onMouseLeave={onMouseLeaveWindow} > {data.children.map((element: DropDownMenuNode, index: number) => { - return
diff --git a/src/views/PopupView/ExportLabelsPopup/ExportLabelPopup.scss b/src/views/PopupView/ExportLabelsPopup/ExportLabelPopup.scss index 2aee0acb..58c2bbac 100644 --- a/src/views/PopupView/ExportLabelsPopup/ExportLabelPopup.scss +++ b/src/views/PopupView/ExportLabelsPopup/ExportLabelPopup.scss @@ -3,6 +3,8 @@ .GenericLabelTypePopupContent { .RightContainer { + width: 500px; + display: flex; flex-direction: column; flex-wrap: nowrap; @@ -54,4 +56,4 @@ } } } -} \ No newline at end of file +} diff --git a/src/views/PopupView/GenericLabelTypePopup/GenericLabelTypePopup.tsx b/src/views/PopupView/GenericLabelTypePopup/GenericLabelTypePopup.tsx index e22f082c..21dfc732 100644 --- a/src/views/PopupView/GenericLabelTypePopup/GenericLabelTypePopup.tsx +++ b/src/views/PopupView/GenericLabelTypePopup/GenericLabelTypePopup.tsx @@ -1,25 +1,25 @@ import React, {useState} from 'react' import './GenericLabelTypePopup.scss' -import {LabelType} from "../../../data/enums/LabelType"; -import {AppState} from "../../../store"; -import {connect} from "react-redux"; -import {ImageButton} from "../../Common/ImageButton/ImageButton"; -import {GenericYesNoPopup} from "../GenericYesNoPopup/GenericYesNoPopup"; -import {ILabelToolkit, LabelToolkitData} from "../../../data/info/LabelToolkitData"; -import {ProjectType} from "../../../data/enums/ProjectType"; +import {LabelType} from '../../../data/enums/LabelType'; +import {AppState} from '../../../store'; +import {connect} from 'react-redux'; +import {ImageButton} from '../../Common/ImageButton/ImageButton'; +import {GenericYesNoPopup} from '../GenericYesNoPopup/GenericYesNoPopup'; +import {ILabelToolkit, LabelToolkitData} from '../../../data/info/LabelToolkitData'; +import {ProjectType} from '../../../data/enums/ProjectType'; interface IProps { title: string, activeLabelType: LabelType, projectType: ProjectType; - onLabelTypeChange?: (labelType: LabelType) => any; + onLabelTypeChange?: (labelType: LabelType) => void; acceptLabel: string; - onAccept: (labelType: LabelType) => any; + onAccept: (labelType: LabelType) => void; skipAcceptButton?: boolean; disableAcceptButton?: boolean; rejectLabel: string; - onReject: (labelType: LabelType) => any; - renderInternalContent: (labelType: LabelType) => any; + onReject: (labelType: LabelType) => void; + renderInternalContent: (labelType: LabelType) => JSX.Element; } const GenericLabelTypePopup: React.FC = ( @@ -59,11 +59,11 @@ const GenericLabelTypePopup: React.FC = ( } const renderContent = () => { - return (
-
+ return (
+
{getSidebarButtons()}
-
+
{renderInternalContent(labelType)}
); @@ -92,4 +92,4 @@ const mapStateToProps = (state: AppState) => ({ export default connect( mapStateToProps, mapDispatchToProps -)(GenericLabelTypePopup); \ No newline at end of file +)(GenericLabelTypePopup); diff --git a/src/views/PopupView/GenericYesNoPopup/GenericYesNoPopup.scss b/src/views/PopupView/GenericYesNoPopup/GenericYesNoPopup.scss index 7232882c..3f814a15 100644 --- a/src/views/PopupView/GenericYesNoPopup/GenericYesNoPopup.scss +++ b/src/views/PopupView/GenericYesNoPopup/GenericYesNoPopup.scss @@ -1,9 +1,6 @@ @import '../../../settings/Settings'; .GenericYesNoPopup { - max-height: 600px; - max-width: 500px; - width: 50%; border-radius: 5px; user-select: none; box-shadow: 0px 0px 10px 3px rgba(255, 255, 255, 0.02); @@ -87,4 +84,4 @@ } } } -} \ No newline at end of file +} diff --git a/src/views/PopupView/GenericYesNoPopup/GenericYesNoPopup.tsx b/src/views/PopupView/GenericYesNoPopup/GenericYesNoPopup.tsx index 6de51899..3a8facc6 100644 --- a/src/views/PopupView/GenericYesNoPopup/GenericYesNoPopup.tsx +++ b/src/views/PopupView/GenericYesNoPopup/GenericYesNoPopup.tsx @@ -1,8 +1,8 @@ import React, {useEffect, useState} from 'react' import './GenericYesNoPopup.scss' -import {TextButton} from "../../Common/TextButton/TextButton"; -import {ContextManager} from "../../../logic/context/ContextManager"; -import {ContextType} from "../../../data/enums/ContextType"; +import {TextButton} from '../../Common/TextButton/TextButton'; +import {ContextManager} from '../../../logic/context/ContextManager'; +import {ContextType} from '../../../data/enums/ContextType'; interface IProps { title: string; @@ -30,7 +30,6 @@ export const GenericYesNoPopup: React.FC = ( skipRejectButton, disableRejectButton }) => { - const [status, setMountStatus] = useState(false); useEffect(() => { if (!status) { @@ -40,27 +39,27 @@ export const GenericYesNoPopup: React.FC = ( }, [status]); return ( -
-
+
+
{title}
-
+
{renderContent()}
-
+
{!skipAcceptButton && } {!skipRejectButton && }
) -}; \ No newline at end of file +}; diff --git a/src/views/PopupView/ImportLabelPopup/ImportLabelPopup.scss b/src/views/PopupView/ImportLabelPopup/ImportLabelPopup.scss index cef30593..cc4d9b71 100644 --- a/src/views/PopupView/ImportLabelPopup/ImportLabelPopup.scss +++ b/src/views/PopupView/ImportLabelPopup/ImportLabelPopup.scss @@ -3,6 +3,8 @@ .GenericLabelTypePopupContent { .RightContainer { + width: 500px; + display: flex; flex-direction: column; flex-wrap: nowrap; @@ -56,4 +58,4 @@ } } } -} \ No newline at end of file +} diff --git a/src/views/PopupView/InsertLabelNamesPopup/InsertLabelNamesPopup.scss b/src/views/PopupView/InsertLabelNamesPopup/InsertLabelNamesPopup.scss index 6d235288..448aa36f 100644 --- a/src/views/PopupView/InsertLabelNamesPopup/InsertLabelNamesPopup.scss +++ b/src/views/PopupView/InsertLabelNamesPopup/InsertLabelNamesPopup.scss @@ -1,6 +1,8 @@ @import '../../../settings/Settings'; -.InsertLabelNamesPopup { +.insert-label-names-popup { + min-height: 400px; + display: flex; flex-direction: row; flex-wrap: nowrap; @@ -10,9 +12,8 @@ align-self: stretch; flex: 1; - min-height: 400px; - .LeftContainer { + .left-container { width: 50px; align-self: stretch; border-right: solid 1px $darkThemeFirstColor; @@ -46,10 +47,11 @@ } } - .RightContainer { + .right-container { + width: 500px; + align-self: stretch; flex: 1; - display: flex; flex-direction: column; flex-wrap: nowrap; @@ -57,7 +59,7 @@ align-items: center; align-content: flex-start; - .Message { + .message { align-self: stretch; color: white; font-size: 15px; @@ -65,7 +67,7 @@ border-bottom: solid 1px $darkThemeFirstColor; } - .LabelsContainer { + .labels-container { align-self: stretch; flex: 1; @@ -96,7 +98,7 @@ } } - .EmptyList { + .empty-list { align-self: stretch; flex: 1; @@ -132,12 +134,12 @@ } } - .InsertLabelNamesPopupContent { + .insert-label-names-popup-content { margin-left: 40px; margin-right: 10px; margin-top: 20px; - .LabelEntry { + .label-entry { width: 100%; display: flex; flex-direction: row; diff --git a/src/views/PopupView/InsertLabelNamesPopup/InsertLabelNamesPopup.tsx b/src/views/PopupView/InsertLabelNamesPopup/InsertLabelNamesPopup.tsx index 73d9da0e..7af137e5 100644 --- a/src/views/PopupView/InsertLabelNamesPopup/InsertLabelNamesPopup.tsx +++ b/src/views/PopupView/InsertLabelNamesPopup/InsertLabelNamesPopup.tsx @@ -68,6 +68,11 @@ const InsertLabelNamesPopup: React.FC = ( }) => { const [labelNames, setLabelNames] = useState(LabelsSelector.getLabelNames()); + const labelsCountSummary = LabelUtil.calculatePerLabelIdCountSummary( + LabelsSelector.getLabelNames(), + LabelsSelector.getImagesData() + ) + const validateEmptyLabelNames = (): boolean => { const emptyLabelNames = filter(labelNames, (labelName: LabelName) => labelName.name === ''); return emptyLabelNames.length === 0; @@ -107,6 +112,10 @@ const InsertLabelNamesPopup: React.FC = ( const deleteLabelNameCallback = (id: string) => { const newLabelNames = reject(labelNames, { id }); setLabelNames(newLabelNames); + if (id in labelsCountSummary && !Object.values(labelsCountSummary[id]).every((value: number) => value === 0)) { + submitNewNotificationAction(NotificationUtil + .createWarningNotification(NotificationsDataMap[Notification.ABOUT_TO_REMOVE_USED_LABEL_NAME_WARNING])); + } }; const togglePerClassColorationCallback = () => { @@ -140,7 +149,7 @@ const InsertLabelNamesPopup: React.FC = ( onChange(labelName.id, event.target.value); const onDeleteCallback = () => deleteLabelNameCallback(labelName.id); const onChangeColorCallback = () => changeLabelNameColorCallback(labelName.id); - return
+ return
= ( const onUpdateAcceptCallback = () => { const nonEmptyLabelNames: LabelName[] = reject(labelNames, (labelName: LabelName) => labelName.name.length === 0); - const missingIds: string[] = LabelUtil.labelNamesIdsDiff(LabelsSelector.getLabelNames(), nonEmptyLabelNames); + const missingIds: string[] = LabelUtil.calculateMissingLabelNamesIds( + LabelsSelector.getLabelNames(), nonEmptyLabelNames); LabelActions.removeLabelNames(missingIds); updateLabelNamesAction(nonEmptyLabelNames); updateActivePopupTypeAction(null); @@ -201,8 +211,8 @@ const InsertLabelNamesPopup: React.FC = ( }; const renderContent = () => { - return (
-
+ return (
+
= ( externalClassName={enablePerClassColoration ? '' : 'monochrome'} />}
-
-
+
+
{ isUpdate ? 'You can now edit the label names you use to describe the objects in the photos. Use the ' + @@ -231,16 +241,16 @@ const InsertLabelNamesPopup: React.FC = ( 'project. You can also choose to skip that part for now and define label names as you go.' }
-
+
{Object.keys(labelNames).length !== 0 ?
{labelInputs}
:
any; @@ -29,7 +29,7 @@ const LoadLabelNamesPopup: React.FC = ({ updateActivePopupType, updateLa }; const { acceptedFiles, getRootProps, getInputProps } = useDropzone({ - accept: { "text/plain": [".txt"] }, + accept: { 'text/plain': ['.txt'] }, multiple: false, onDrop: (accepted) => { if (accepted.length === 1) { @@ -55,47 +55,47 @@ const LoadLabelNamesPopup: React.FC = ({ updateActivePopupType, updateLa {"upload"} -

Loading of labels file was unsuccessful

-

Try again

+

Loading of labels file was unsuccessful

+

Try again

; else if (acceptedFiles.length === 0) return <> {"upload"} -

Drop labels file

+

Drop labels file

or

-

Click here to select it

+

Click here to select it

; else if (labelsList.length === 1) return <> {"uploaded"} -

only 1 label found

+

only 1 label found

; else return <> {"uploaded"} -

{labelsList.length} labels found

+

{labelsList.length} labels found

; }; const renderContent = () => { - return (
-
+ return (
+
Load a text file with a list of labels you are planning to use. The names of each label should be separated by new line. If you don't have a prepared file, no problem. You can create your own list now. @@ -108,12 +108,12 @@ const LoadLabelNamesPopup: React.FC = ({ updateActivePopupType, updateLa return ( ); diff --git a/src/views/PopupView/LoadModelPopup/LoadModelPopup.scss b/src/views/PopupView/LoadModelPopup/LoadModelPopup.scss index 7cc09721..ea0e5fd1 100644 --- a/src/views/PopupView/LoadModelPopup/LoadModelPopup.scss +++ b/src/views/PopupView/LoadModelPopup/LoadModelPopup.scss @@ -1,6 +1,7 @@ @import '../../../settings/Settings'; .LoadModelPopupContent { + width: 550px; display: flex; flex-direction: column; flex-wrap: nowrap; @@ -58,4 +59,4 @@ } } } -} \ No newline at end of file +} diff --git a/src/views/PopupView/LoadMoreImagesPopup/LoadMoreImagesPopup.scss b/src/views/PopupView/LoadMoreImagesPopup/LoadMoreImagesPopup.scss index 93966bda..72fa7351 100644 --- a/src/views/PopupView/LoadMoreImagesPopup/LoadMoreImagesPopup.scss +++ b/src/views/PopupView/LoadMoreImagesPopup/LoadMoreImagesPopup.scss @@ -1,6 +1,7 @@ @import '../../../settings/Settings'; .LoadMoreImagesPopupContent { + width: 550px; display: flex; flex-direction: column; flex-wrap: nowrap; @@ -52,4 +53,4 @@ } } } -} \ No newline at end of file +} diff --git a/src/views/PopupView/PerLabelIdCountsStatisticsPopup/PerLabelIdCountsStatisticsPopup.scss b/src/views/PopupView/PerLabelIdCountsStatisticsPopup/PerLabelIdCountsStatisticsPopup.scss new file mode 100644 index 00000000..5b342aca --- /dev/null +++ b/src/views/PopupView/PerLabelIdCountsStatisticsPopup/PerLabelIdCountsStatisticsPopup.scss @@ -0,0 +1,59 @@ +@import '../../../settings/Settings'; + +.per-label-id-counts-statistics-popup-content { + min-height: calc(100vh - 200px); + min-width: calc(100vw - 100px); + + display: flex; + flex-direction: row; + flex-wrap: nowrap; + justify-content: start; + align-items: center; + align-content: center; + + align-self: stretch; + flex: 1; + + .left-container { + width: 50px; + align-self: stretch; + border-right: solid 1px $darkThemeFirstColor; + + display: flex; + flex-direction: column; + flex-wrap: nowrap; + justify-content: flex-start; + align-items: center; + align-content: flex-start; + padding: 3px 0; + + .ImageButton { + transition: transform 0.3s; + + &:hover { + border-radius: 3px; + background-color: rgba(0, 0, 0, 0.2); + } + + &.active { + border-radius: 3px; + background-color: rgba(0, 0, 0, 0.4); + } + + &.monochrome { + img { + filter: brightness(0) invert(1); + } + } + } + } + + .right-container { + align-self: stretch; + flex: 1; + padding: 20px; + table { + border: 1px solid white; + } + } +} diff --git a/src/views/PopupView/PerLabelIdCountsStatisticsPopup/PerLabelIdCountsStatisticsPopup.tsx b/src/views/PopupView/PerLabelIdCountsStatisticsPopup/PerLabelIdCountsStatisticsPopup.tsx new file mode 100644 index 00000000..e4d73d07 --- /dev/null +++ b/src/views/PopupView/PerLabelIdCountsStatisticsPopup/PerLabelIdCountsStatisticsPopup.tsx @@ -0,0 +1,78 @@ +import React from 'react'; +import './PerLabelIdCountsStatisticsPopup.scss'; +import {PopupWindowType} from '../../../data/enums/PopupWindowType'; +import {AppState} from '../../../store'; +import {connect} from 'react-redux'; +import {updateActivePopupType as storeUpdateActivePopupType} from '../../../store/general/actionCreators'; +import {GenericYesNoPopup} from '../GenericYesNoPopup/GenericYesNoPopup'; +import {ImageButton} from '../../Common/ImageButton/ImageButton'; +import {LabelsSelector} from '../../../store/selectors/LabelsSelector'; + +interface IProps { + updateActivePopupTypeAction: (activePopupType: PopupWindowType) => void; +} + +const PerLabelIdCountsStatisticsPopup: React.FC = ({ updateActivePopupTypeAction }) => { + + const labelNames = LabelsSelector.getLabelNames() + const imageData = LabelsSelector.getImagesData() + + const onReject = () => { + updateActivePopupTypeAction(null); + }; + + const renderContent = () => { + return
+
+ +
+
+ + + + + + + + + + + + + + + + +
CompanyContactCountry
Alfreds FutterkisteMaria AndersGermany
Centro comercial MoctezumaFrancisco ChangMexico
+
+
+ } + + return ( + + ); +} + +const mapDispatchToProps = { + updateActivePopupTypeAction: storeUpdateActivePopupType +}; + +const mapStateToProps = (state: AppState) => ({}); + +export default connect( + mapStateToProps, + mapDispatchToProps +)(PerLabelIdCountsStatisticsPopup); diff --git a/src/views/PopupView/PopupView.tsx b/src/views/PopupView/PopupView.tsx index 700b47b5..7315f39b 100644 --- a/src/views/PopupView/PopupView.tsx +++ b/src/views/PopupView/PopupView.tsx @@ -1,18 +1,19 @@ import React from 'react'; import './PopupView.scss'; -import { PopupWindowType } from "../../data/enums/PopupWindowType"; -import { AppState } from "../../store"; -import { connect } from "react-redux"; -import LoadLabelsPopup from "./LoadLabelNamesPopup/LoadLabelNamesPopup"; -import InsertLabelNamesPopup from "./InsertLabelNamesPopup/InsertLabelNamesPopup"; -import ExitProjectPopup from "./ExitProjectPopup/ExitProjectPopup"; -import LoadMoreImagesPopup from "./LoadMoreImagesPopup/LoadMoreImagesPopup"; -import { LoadModelPopup } from "./LoadModelPopup/LoadModelPopup"; -import SuggestLabelNamesPopup from "./SuggestLabelNamesPopup/SuggestLabelNamesPopup"; -import { CSSHelper } from "../../logic/helpers/CSSHelper"; -import { ClipLoader } from "react-spinners"; -import ImportLabelPopup from "./ImportLabelPopup/ImportLabelPopup"; -import ExportLabelPopup from "./ExportLabelsPopup/ExportLabelPopup"; +import { PopupWindowType } from '../../data/enums/PopupWindowType'; +import { AppState } from '../../store'; +import { connect } from 'react-redux'; +import LoadLabelsPopup from './LoadLabelNamesPopup/LoadLabelNamesPopup'; +import InsertLabelNamesPopup from './InsertLabelNamesPopup/InsertLabelNamesPopup'; +import ExitProjectPopup from './ExitProjectPopup/ExitProjectPopup'; +import LoadMoreImagesPopup from './LoadMoreImagesPopup/LoadMoreImagesPopup'; +import { LoadModelPopup } from './LoadModelPopup/LoadModelPopup'; +import SuggestLabelNamesPopup from './SuggestLabelNamesPopup/SuggestLabelNamesPopup'; +import { CSSHelper } from '../../logic/helpers/CSSHelper'; +import { ClipLoader } from 'react-spinners'; +import ImportLabelPopup from './ImportLabelPopup/ImportLabelPopup'; +import ExportLabelPopup from './ExportLabelsPopup/ExportLabelPopup'; +import PerLabelIdCountsStatisticsPopup from "./PerLabelIdCountsStatisticsPopup/PerLabelIdCountsStatisticsPopup"; interface IProps { activePopupType: PopupWindowType; @@ -44,6 +45,8 @@ const PopupView: React.FC = ({ activePopupType }) => { return ; case PopupWindowType.SUGGEST_LABEL_NAMES: return ; + case PopupWindowType.PER_LABEL_ID_COUNTS_STATISTICS: + return ; case PopupWindowType.LOADER: return = ({ activePopupType }) => { }; return ( - activePopupType &&
+ activePopupType &&
{selectPopup()}
); @@ -68,4 +71,4 @@ const mapStateToProps = (state: AppState) => ({ export default connect( mapStateToProps -)(PopupView); \ No newline at end of file +)(PopupView); diff --git a/src/views/PopupView/SuggestLabelNamesPopup/SuggestLabelNamesPopup.scss b/src/views/PopupView/SuggestLabelNamesPopup/SuggestLabelNamesPopup.scss index 5c01d0a4..6840b1bc 100644 --- a/src/views/PopupView/SuggestLabelNamesPopup/SuggestLabelNamesPopup.scss +++ b/src/views/PopupView/SuggestLabelNamesPopup/SuggestLabelNamesPopup.scss @@ -1,6 +1,7 @@ @import '../../../settings/Settings'; .SuggestLabelNamesPopupContent { + max-width: 550px; display: flex; flex-direction: column; flex-wrap: nowrap; diff --git a/vite.config.ts b/vite.config.ts index 94342289..04bd6948 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -3,39 +3,39 @@ import { loadEnv, UserConfig, UserConfigExport, -} from "vite"; +} from 'vite'; -import react from "@vitejs/plugin-react"; +import react from '@vitejs/plugin-react'; export default ({ mode }: UserConfig): UserConfigExport => { - process.env = { ...process.env, ...loadEnv(mode || "development", process.cwd()) }; + process.env = { ...process.env, ...loadEnv(mode || 'development', process.cwd()) }; return defineConfig({ plugins: [react()], build: { - minify: "terser", - sourcemap: mode === "development", + minify: 'terser', + sourcemap: mode === 'development', chunkSizeWarningLimit: 1024 * 1024, rollupOptions: { treeshake: true, maxParallelFileReads: 4, output: { manualChunks: { - lodash: ["lodash"], - classnames: ["classnames"], - runtime: ["react", "react-is"], - "runtime-dom": ["react-dom"], + lodash: ['lodash'], + classnames: ['classnames'], + runtime: ['react', 'react-is'], + 'runtime-dom': ['react-dom'], - ai: ["@tensorflow/tfjs", - "@tensorflow/tfjs-backend-cpu", - "@tensorflow/tfjs-backend-webgl", - "@tensorflow/tfjs-core", - "@tensorflow/tfjs-node"], + ai: ['@tensorflow/tfjs', + '@tensorflow/tfjs-backend-cpu', + '@tensorflow/tfjs-backend-webgl', + '@tensorflow/tfjs-core', + '@tensorflow/tfjs-node'], models: [ - "@tensorflow-models/coco-ssd", - "@tensorflow-models/posenet", + '@tensorflow-models/coco-ssd', + '@tensorflow-models/posenet', ], - ui: ["@mui/material", "@mui/system"], - moment: ["moment"] + ui: ['@mui/material', '@mui/system'], + moment: ['moment'] }, }, @@ -46,17 +46,17 @@ export default ({ mode }: UserConfig): UserConfigExport => { }, css: { modules: { - generateScopedName: mode === "development" ? "[name]__[local]___[hash:base64:5]" : "[hash:base64:8]", - scopeBehaviour: "local", - localsConvention: "camelCase", + generateScopedName: mode === 'development' ? '[name]__[local]___[hash:base64:5]' : '[hash:base64:8]', + scopeBehaviour: 'local', + localsConvention: 'camelCase', }, postcss: { plugins: [ { - postcssPlugin: "internal:charset-removal", + postcssPlugin: 'internal:charset-removal', AtRule: { charset: (atRule) => { - if (atRule.name === "charset") { + if (atRule.name === 'charset') { atRule.remove(); } }, @@ -65,6 +65,6 @@ export default ({ mode }: UserConfig): UserConfigExport => { ], }, }, - + }); };