diff --git a/CHANGELOG.md b/CHANGELOG.md index 21cc1932..1d99f096 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added Python SDK package (`cvat-sdk`) - Previews for jobs - Documentation for LDAP authentication () +- OpenCV.js caching and autoload () - Publishing dev version of CVAT docker images () ### Changed diff --git a/cvat-ui/package.json b/cvat-ui/package.json index b5aaddf3..36e245ba 100644 --- a/cvat-ui/package.json +++ b/cvat-ui/package.json @@ -1,6 +1,6 @@ { "name": "cvat-ui", - "version": "1.40.0", + "version": "1.40.1", "description": "CVAT single-page application", "main": "src/index.tsx", "scripts": { diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/handle-popover-visibility.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/handle-popover-visibility.tsx index 1c35b208..23850c86 100644 --- a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/handle-popover-visibility.tsx +++ b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/handle-popover-visibility.tsx @@ -5,10 +5,15 @@ import React, { useState } from 'react'; import Popover, { PopoverProps } from 'antd/lib/popover'; +interface OwnProps { + overlayClassName?: string; + onVisibleChange?: (visible: boolean) => void; +} + export default function withVisibilityHandling(WrappedComponent: typeof Popover, popoverType: string) { - return (props: PopoverProps): JSX.Element => { + return (props: OwnProps & PopoverProps): JSX.Element => { const [visible, setVisible] = useState(false); - const { overlayClassName, ...rest } = props; + const { overlayClassName, onVisibleChange, ...rest } = props; const overlayClassNames = typeof overlayClassName === 'string' ? overlayClassName.split(/\s+/) : []; const popoverClassName = `cvat-${popoverType}-popover`; overlayClassNames.push(popoverClassName); @@ -34,6 +39,7 @@ export default function withVisibilityHandling(WrappedComponent: typeof Popover, } } setVisible(_visible); + if (onVisibleChange) onVisibleChange(_visible); }} /> ); diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/opencv-control.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/opencv-control.tsx index 3101e367..511a56cf 100644 --- a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/opencv-control.tsx +++ b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/opencv-control.tsx @@ -210,6 +210,7 @@ class OpenCVControlComponent extends React.PureComponent => { @@ -286,7 +287,7 @@ class OpenCVControlComponent extends React.PureComponent { shape.shapePoints = points; - }).catch((error) => { + }).catch((error: any) => { reject(error); }); } @@ -590,6 +591,33 @@ class OpenCVControlComponent extends React.PureComponent { + try { + this.setState({ + initializationError: false, + initializationProgress: 0, + }); + await openCVWrapper.initialize((progress: number) => { + this.setState({ initializationProgress: progress }); + }); + const trackers = Object.values(openCVWrapper.tracking); + this.setState({ + libraryInitialized: true, + activeTracker: trackers[0], + trackers, + }); + } catch (error: any) { + notification.error({ + description: error.toString(), + message: 'Could not initialize OpenCV library', + }); + this.setState({ + initializationError: true, + initializationProgress: -1, + }); + } + } + private renderDrawingContent(): JSX.Element { const { activeLabelID } = this.state; const { labels, canvasInstance, onInteractionStart } = this.props; @@ -773,42 +801,21 @@ class OpenCVControlComponent extends React.PureComponent - = 0 ? 17 : 24}> - + + { + initializationProgress >= 0 ? + OpenCV is loading : ( + + ) + } {initializationProgress >= 0 && ( - + { - if (libraryInitialized !== openCVWrapper.isInitialized) { + onVisibleChange={(visible: boolean) => { + const { initializationProgress } = this.state; + if (!visible || initializationProgress >= 0) return; + + if (!openCVWrapper.isInitialized || openCVWrapper.initializationInProgress) { + this.initializeOpenCV(); + } else if (libraryInitialized !== openCVWrapper.isInitialized) { this.setState({ libraryInitialized: openCVWrapper.isInitialized, }); diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/styles.scss b/cvat-ui/src/components/annotation-page/standard-workspace/styles.scss index a3fc7e59..fc785379 100644 --- a/cvat-ui/src/components/annotation-page/standard-workspace/styles.scss +++ b/cvat-ui/src/components/annotation-page/standard-workspace/styles.scss @@ -226,6 +226,10 @@ width: $grid-unit-size * 14; justify-content: center; } + + .ant-progress { + margin-left: $grid-unit-size; + } } .cvat-opencv-initialization-button { diff --git a/cvat-ui/src/utils/opencv-wrapper/opencv-wrapper.ts b/cvat-ui/src/utils/opencv-wrapper/opencv-wrapper.ts index cd6097a9..589168b4 100644 --- a/cvat-ui/src/utils/opencv-wrapper/opencv-wrapper.ts +++ b/cvat-ui/src/utils/opencv-wrapper/opencv-wrapper.ts @@ -30,13 +30,17 @@ export interface Tracking { export class OpenCVWrapper { private initialized: boolean; private cv: any; + private onProgress: ((percent: number) => void) | null; + private injectionProcess: Promise | null; public constructor() { this.initialized = false; this.cv = null; + this.onProgress = null; + this.injectionProcess = null; } - public async initialize(onProgress: (percent: number) => void): Promise { + private async inject(): Promise { const response = await fetch(`${baseURL}/opencv/opencv.js`); if (response.status !== 200) { throw new Error(`Response status ${response.status}. ${response.statusText}`); @@ -67,7 +71,7 @@ export class OpenCVWrapper { // Cypress workaround: content-length is always zero in cypress, it is done optional here // Just progress bar will be disabled const percentage = contentLength ? (receivedLength * 100) / +(contentLength as string) : 0; - onProgress(+percentage.toFixed(0)); + if (this.onProgress) this.onProgress(+percentage.toFixed(0)); } } @@ -79,13 +83,32 @@ export class OpenCVWrapper { const global = window as any; this.cv = await global.cv; + } + + public async initialize(onProgress: (percent: number) => void): Promise { + this.onProgress = onProgress; + + if (!this.injectionProcess) { + this.injectionProcess = this.inject(); + } + await this.injectionProcess; + + this.injectionProcess = null; this.initialized = true; } + public removeProgressCallback(): void { + this.onProgress = null; + } + public get isInitialized(): boolean { return this.initialized; } + public get initializationInProgress(): boolean { + return !!this.injectionProcess; + } + public get contours(): Contours { if (!this.initialized) { throw new Error('Need to initialize OpenCV first'); diff --git a/cvat/apps/opencv/views.py b/cvat/apps/opencv/views.py index d0b3b265..3090ad68 100644 --- a/cvat/apps/opencv/views.py +++ b/cvat/apps/opencv/views.py @@ -7,4 +7,6 @@ def OpenCVLibrary(request): dirname = os.path.join(settings.STATIC_ROOT, 'opencv', 'js') pattern = os.path.join(dirname, 'opencv_*.js') path = glob.glob(pattern)[0] - return sendfile(request, path) + response = sendfile(request, path) + response['Cache-Control'] = "public, max-age=604800" + return response diff --git a/site/content/en/docs/manual/advanced/opencv-tools.md b/site/content/en/docs/manual/advanced/opencv-tools.md index 1b5093b9..c0484037 100644 --- a/site/content/en/docs/manual/advanced/opencv-tools.md +++ b/site/content/en/docs/manual/advanced/opencv-tools.md @@ -9,7 +9,8 @@ The tool based on [Open CV](https://opencv.org/) Computer Vision library which is an open-source product that includes many CV algorithms. Some of these algorithms can be used to simplify the annotation process. -First step to work with OpenCV is to load it into CVAT. Click on the toolbar icon, then click `Load OpenCV`. +First step to work with OpenCV is to load it into CVAT. +Click on the toolbar icon, library will be downloaded automatically. ![](/images/image198.jpg) diff --git a/site/content/en/images/image198.jpg b/site/content/en/images/image198.jpg index 049e22c0..05aecae9 100644 Binary files a/site/content/en/images/image198.jpg and b/site/content/en/images/image198.jpg differ diff --git a/site/content/en/images/image199.jpg b/site/content/en/images/image199.jpg index 797476d0..d4f352bf 100644 Binary files a/site/content/en/images/image199.jpg and b/site/content/en/images/image199.jpg differ diff --git a/site/content/en/images/image221.jpg b/site/content/en/images/image221.jpg index eb1bf352..31c69529 100644 Binary files a/site/content/en/images/image221.jpg and b/site/content/en/images/image221.jpg differ diff --git a/tests/cypress/integration/actions_tasks2/case_101_opencv_basic_actions.js b/tests/cypress/integration/actions_tasks2/case_101_opencv_basic_actions.js index 25341b8f..cbfd1b09 100644 --- a/tests/cypress/integration/actions_tasks2/case_101_opencv_basic_actions.js +++ b/tests/cypress/integration/actions_tasks2/case_101_opencv_basic_actions.js @@ -80,7 +80,9 @@ context('OpenCV. Intelligent scissors. Histogram Equalization. TrackerMIL.', () describe(`Testing case "${caseId}"`, () => { it('Load OpenCV.', () => { cy.interactOpenCVControlButton(); - cy.get('.cvat-opencv-control-popover').find('.cvat-opencv-initialization-button').click(); + cy.get('.cvat-opencv-control-popover').within(() => { + cy.contains('OpenCV is loading').should('not.exist'); + }); // Intelligent cissors button be visible cy.get('.cvat-opencv-drawing-tool').should('exist').and('be.visible'); });