From 16b06c88b4712bbdf2b154461182674f80d2b531 Mon Sep 17 00:00:00 2001 From: Piotr Skalski Date: Mon, 15 Jun 2020 23:52:15 +0200 Subject: [PATCH] 1.6.0-alpha relese merge (#93) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Merge pull request #70 from SkalskiP/develop (#71) * new gif with ssd and posenet * Add Docker Support (#74) * add Dockerfile for make-sense * Update README for Docker * README updated * README updated with docker logs * readme updated * Update Dockerfile * Update README.md * basic stats * README.md update (#78) (#79) * Merge pull request #70 from SkalskiP/develop (#71) * new gif with ssd and posenet * Add Docker Support (#74) * add Dockerfile for make-sense * Update README for Docker * README updated * README updated with docker logs * readme updated * Update Dockerfile * Update README.md * basic stats Co-authored-by: Fatih Baltacı Co-authored-by: Fatih Baltacı * add cross hair (#90) * Piotr | Line labels creation and export (#89) * initial changes: adding line labels to redux + addling line tab to right side navigation bar * adding new lines and base rendering * up * line style + snapping to rect added * highlight logic added * line rendering engine is working * line rendering engine update + marking line labeled images added * serializing to CSV * up * after PR * after PR * quick fix * Piotr | Image recognition (#92) * image recognition initial commit * setup before image recognition tagging * base tag assignment added * default screen when empty label list * image recognition added * after CR Co-authored-by: PLE12366003 Co-authored-by: Fatih Baltacı --- public/ico/cross-hair.png | Bin 0 -> 1555 bytes public/ico/line.png | Bin 0 -> 1362 bytes public/ico/object.png | Bin 1268 -> 1593 bytes public/ico/point.png | Bin 3044 -> 5578 bytes src/data/enums/LabelType.ts | 3 +- src/data/enums/LineAnchorType.ts | 4 + src/data/export/LineExportFormatData.ts | 9 + src/data/export/TagExportFormatData.ts | 9 + src/data/info/EditorFeatureData.ts | 2 +- src/data/info/LabelToolkitData.ts | 8 +- src/logic/actions/EditorActions.ts | 4 + src/logic/actions/LabelActions.ts | 13 +- src/logic/context/EditorContext.ts | 13 +- src/logic/export/LineLabelExport.ts | 59 ++++ src/logic/export/PointLabelsExport.ts | 8 +- src/logic/export/RectLabelsExporter.ts | 20 +- src/logic/export/TagLabelsExport.ts | 53 ++++ .../__tests__/PolygonLabelsExporter.test.ts | 15 +- src/logic/render/LineRenderEngine.ts | 297 ++++++++++++++++++ src/logic/render/PolygonRenderEngine.ts | 29 +- src/logic/render/PrimaryEditorRenderEngine.ts | 43 ++- src/settings/RenderEngineConfig.ts | 2 + src/store/Actions.ts | 1 + src/store/general/actionCreators.ts | 9 + src/store/general/reducer.ts | 7 + src/store/general/types.ts | 9 + src/store/labels/types.ts | 9 + src/store/selectors/GeneralSelector.ts | 4 + src/store/selectors/LabelsSelector.ts | 11 +- src/utils/FileUtil.ts | 2 + src/utils/LineUtil.ts | 4 + src/utils/PointUtil.ts | 4 - src/utils/RenderEngineUtil.ts | 43 ++- .../EditorContainer/EditorContainer.tsx | 24 +- .../EditorTopNavigationBar.scss | 4 +- .../EditorTopNavigationBar.tsx | 30 +- .../ImagesList/ImagesList.tsx | 24 +- .../LabelsToolkit/LabelsToolkit.tsx | 17 + .../LineLabelsList/LineLabelsList.scss | 17 + .../LineLabelsList/LineLabelsList.tsx | 135 ++++++++ .../PointLabelsList/PointLabelsList.tsx | 13 +- .../RectLabelsList/RectLabelsList.tsx | 2 +- .../TagLabelsList/TagLabelsList.scss | 89 ++++++ .../TagLabelsList/TagLabelsList.tsx | 131 ++++++++ src/views/EditorView/StateBar/StateBar.tsx | 12 + .../TopNavigationBar/TopNavigationBar.scss | 2 +- .../ImagesDropZone/ImagesDropZone.tsx | 14 +- .../ExportLabelsPopup/ExportLabelPopup.scss | 4 +- .../ExportLabelsPopup/ExportLabelPopup.tsx | 37 ++- .../InsertLabelNamesPopup.scss | 4 +- .../InsertLabelNamesPopup.tsx | 20 +- .../LoadLabelNamesPopup.tsx | 1 + 52 files changed, 1186 insertions(+), 88 deletions(-) create mode 100644 public/ico/cross-hair.png create mode 100644 public/ico/line.png create mode 100644 src/data/enums/LineAnchorType.ts create mode 100644 src/data/export/LineExportFormatData.ts create mode 100644 src/data/export/TagExportFormatData.ts create mode 100644 src/logic/export/LineLabelExport.ts create mode 100644 src/logic/export/TagLabelsExport.ts create mode 100644 src/logic/render/LineRenderEngine.ts create mode 100644 src/views/EditorView/SideNavigationBar/LineLabelsList/LineLabelsList.scss create mode 100644 src/views/EditorView/SideNavigationBar/LineLabelsList/LineLabelsList.tsx create mode 100644 src/views/EditorView/SideNavigationBar/TagLabelsList/TagLabelsList.scss create mode 100644 src/views/EditorView/SideNavigationBar/TagLabelsList/TagLabelsList.tsx diff --git a/public/ico/cross-hair.png b/public/ico/cross-hair.png new file mode 100644 index 0000000000000000000000000000000000000000..9aaf82c4567fda683582d831ac6dd60428656da3 GIT binary patch literal 1555 zcmb7^`9ISS9LGP7xwbiyVG?uXOwJr7b00HbUsuKynGiw`diWwo%ax7EU8G1O*S1n} zP6u-|mXu7jM69G0iaGl72Yi3{KHiVl`}M=?hu1H!$1~5gwX)wd=S& ziHYp~xY<&lT@yKja&`dz?A@&%axnma7_JVsK1o&Is!-9sjtYb6^$HI%B0W=Oe4eSw zUAcODO|CW=Vx|DoedaJL2s0q$b($a2kAi^F?!?%HraJ>fC6%y`va)sBuV9pQtb&Sk zbWB_R_oLRokC7i+|7MzpGrw(JbG3S5`62!6&hPHep!sllo+wYAhwKl_1Nwl{bNogQ z%@iHT0>vR6=TSjqkfXe+7s^z_uojUi0~IGkxCA@c3#!BSDX>WN+-zZ||BeQp)PhNP z$y@RrP=6sT6k8cTQ%;wn8yX58Xxxb%M(G4rp)vwd7Vghw za)#}#a7m*tinVJOg=)g8PTHQeVSd%CmyO)Q)ulqCnKJ5y2-jux%|VKqGoftG_bfaV zu&`O!Ai@PH`z`3BdTUXZW3jH&B)!_E{*@a>xa%XC?a-TFOT5PLEwyOv#Ol;vVTr&S z?_Lo$fb~m2U?=jMKQ^eI=6sV3?dk{%V~KQo6E}`fCPJ3%Foc9(no2EiLQ?hY&wIwF zgUT!krxipSTBd~G_BS|?I13G_dZawOA)}P?~=~vUsSk$PD9JM z5l3(kUY4ydSa5G0UtreFLU&S^&Bm#w!of=1raNrp;DL83o0&xg?FdyNoGa0sm@wKQ z_K&Z--|Tty!z$UUIz$tfN2tgt=gz0&U-08Oh#1Cts#kbn7HP7)^A!D$$VK8jzM1`M zSxw@l*aD(F;U}VF*mG=9PLSfk-O6Jh6g5e1IK2LQAh{PJ@K#5yaegaiWVS5v{;-r{ z#Qx_YrG1m_8Ci(r`k!U*4)s2EnokV6G4O;`tlKxeThr>hp%Mi4SGeFlzcM-jRkjWs=&WIi z-)QI=+lg*oSW&!yjeLh|Ubu1*7m!Zl5Si?1a4&=Y9Ui|c30 zhounSRc`%HU+rAa*7+m7Z0;7`R#YaADl(|VfgUPFZW!XaURpSdlS_LYV>DM|M&(XG zHasJ{&)f-hGw05qV?z>}p>Hd1Q(4GCF#eU_oVKy`gl1WG0x50s5}TTRGTG&{=B9F# zKl~@*skd4uARzQ zGVr;p{A;x0rlL)OzI2b_!p4gk8caRV0$sCtLbLD5^)OAYh_cY~m`vT@_D73P+4v)@ zbuHTb?UI*pR+kDA*l)Usb7pfeoR;Nwe}F)m^ZVA%W`RXRf|$ELBqKh#i1oOcJ%oi| z1dh8VJGmPZRqLu{=G?gC9cCRlv%F}aN3;@oyAYFA(JhrEv)<}4DnUE!J=0=}UvtTs z(^yktIr7DB99%35h4K#2>YJiu4@plvN@_xbS$U&O8<$QS{lDPOB8-C|gC7C-suFH> zia=bbS&N6pw(1ECT8s3LcTN%d$QWNY!lYz~^fot0OXk@BN8BF$0{Tg^d410Lx(>MP h6F5HCf9<>1icOa^!1@woOxUG7;Ogk%&}tWa`Cn(A%#r{A literal 0 HcmV?d00001 diff --git a/public/ico/line.png b/public/ico/line.png new file mode 100644 index 0000000000000000000000000000000000000000..3bd66e370f53e44281a7f35a949f0bb79afc2740 GIT binary patch literal 1362 zcmV-Y1+DstP)EX>4Tx04R}tkv&MmKpe$iQ?()$2Rn#3M5s;{ii$XD6^c+H)C#RSm|XfHG-*gu zTpR`0f`cE6RR;V+0880*#vEd>=bb;{*sk16O*>U#SDrpQP7X zTJ#9$+XgPKTbi;5T(02y>eSad^gZEa<4bO1wgWnpw> zWFU8GbZ8()Nlj2!fese{00SyXL_t(|+U=deaoaEuMZY4APbt7|Aml|I=wPXbG{7oc zf~#Ny>Ood!)PXNa8n6n;&CxgqGDF7}0XU+dCGh@H#`u6Yi^Yd_R}<9-Av^lS761eR z1Rw|?06_o&2m%N|5I_(>0D=ILfM$dImuf0Wb4iQ#lbxhTp7t*Rq?&z}R7?8$<}Yhi zvr6^vUu<)%BdeN9dN~F|&1Wxl8$Bpe4w?7Bcm+jswmBY<#XxIN#9P)C+Z+$bE?~`_ zs%gnq#{u#vY3*5aEoq}_`i1R2H<0=uX;KA1xskL5ieHmK%-g@l@JiBM8yRo)Bx$Ak z@SUwcCy@7mN=chGGLAu6sy_T+`(xd*nh3aUH~-l2^+2MJeeJCeKLel?Ac*Nh8YrtC zDAz3%5`9!-*c@L=N2GisQsO#wwF!JBY2OnmTcE@Q;!pH{p_BlLsU}ix9g&i~qmTa; zZg>0II~;uh5Id2QwNJTRkwXJziBGWtghX!;NS9NV4k(+PBE?2smfNS?6v}nS zSJt;LTa}9>dV{5;{Yc(nAlF*IG-HlV{y? zWQAXe_nwu5%(@52$IRoCAIfR7?g8>KDE9#?7XXQ;XN;jO?FL!{kb&fzb-!-c03=c# zkz>|DwRs5QmIrbl}hCWO|NSK zD{v9sh&K&vBVLM^-~wDS_}(!*kEd}62k{8@;ZdBPsOoco8C->LV?WNIKc=0*@9-_W zecY4|IZF#8{RRI$fkffd`vvzrIS0%_Td&>TsYQFn8#Ke)?I>^+hKei zYs-Z_InEb%&mr9>c9=bbFNmk@oaFuZi|!LSs{IM?Es}{sT!y=JpSZU6Q!$mCYCei1 zx=+@)b{HSYOib2-Wi7rZSKM=W5IgXD{83B=PYB&>#6o5xHse|`AI}_X@4_eXY~nRr zvl)-eyPF=y&HlRrY{I4D<+~?Q&tursLQ5();iQ~^_Tb%#+HMeY<_vAm;zNnrMy7|a zV6lPNVRjOq#(4#F&f->_O3=B8FBi}_lnd~~1mS%gz~vLre1#}FWoZ6^$U#x&a8Hup zhp*}qs$bp zE$7LbXZLZpmMiV8i6Z)P7b97`M{bN?Z7Sdi__}kesw$*HxAus=L z7I4eSLz@Ee(Bspg`Px{0r|~ax#y< z6>`+a>l+4rXM)c=8v5K+AnqmmtHCn;_JU6On%6ZBB=~&1q0bEk;y&AM?};YClI6-d z(Tq4M$BkbVJE2*zXHMCs$fsvaUjtx&O0JQ~%nW{9*h$ke;@=Z|KGE>|X{F8~{Df#0 zIi|i0OU3^WEX;^8y5y$g#)jX&JHcnuYc$o^gxf@aA7Y%&+D}_$&TTcB~EK ziW-9jaWE(=p8IfNmbf)?C-u8^*W~z63%$c^mxOk(<;=@nrtdb{J9|NHlJ8gWW?6$@ zB#J#Kn#4iv zyq;K$WX_Wp#o6&re&ggu?HXNHv%R8CYh4RH>qYG%ds%&S5Nm5P{yVQ0bM5Q~^XJ7W zgYDwZJSbYNjt@Sc_rC`^ufyBqALQAK4~qIv=Uga$K_=tZW2VK<3}Q-~BWH4j+*7Mv z*?v)2n`+L-mz3S`EoJ}27Y8w%GprYnUWx6oYn{;*CU@HYvooTnW3xEjx(P26mA8e#=Sv%($3*k|GXr}Pe-J~{ezCwDwRs5QmIrbl}e>jsSM{oi@+4*M>m9600000NkvXXu0mjfm6;); literal 1268 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 literal 3044 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 { + 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} />