From 32abd1f0500d0be07c6154769f88aeacee33f39d Mon Sep 17 00:00:00 2001 From: Boris Sekachev Date: Thu, 17 Oct 2024 20:27:50 +0300 Subject: [PATCH 1/3] updated allocation report --- cvat-core/src/api.ts | 5 +- cvat-core/src/index.ts | 5 +- cvat-core/src/server-proxy.ts | 4 +- cvat-core/src/server-response-types.ts | 8 +- cvat-core/src/session-implementation.ts | 15 +- cvat-core/src/session.ts | 6 +- cvat-core/src/validation-layout.ts | 55 +++- .../quality-control/quality-control-page.tsx | 243 ++++++++---------- .../task-quality/allocation-table.tsx | 63 +---- .../task-quality/quality-magement-tab.tsx | 15 +- .../quality-control/task-quality/summary.tsx | 23 +- cvat-ui/src/cvat-core-wrapper.ts | 5 +- cvat-ui/src/reducers/index.ts | 4 +- 13 files changed, 218 insertions(+), 233 deletions(-) diff --git a/cvat-core/src/api.ts b/cvat-core/src/api.ts index 5f624ad0e8ae..ca33f431c43e 100644 --- a/cvat-core/src/api.ts +++ b/cvat-core/src/api.ts @@ -26,7 +26,7 @@ import QualityReport from './quality-report'; import QualityConflict from './quality-conflict'; import QualitySettings from './quality-settings'; import AnalyticsReport from './analytics-report'; -import ValidationLayout from './validation-layout'; +import { JobValidationLayout, TaskValidationLayout } from './validation-layout'; import { Request } from './request'; import * as enums from './enums'; @@ -427,7 +427,8 @@ function build(): CVATCore { QualityReport, Request, FramesMetaData, - ValidationLayout, + JobValidationLayout, + TaskValidationLayout, }, utils: { mask2Rle, diff --git a/cvat-core/src/index.ts b/cvat-core/src/index.ts index f361f194df73..8a4c9e8bfb53 100644 --- a/cvat-core/src/index.ts +++ b/cvat-core/src/index.ts @@ -32,7 +32,7 @@ import QualityConflict from './quality-conflict'; import QualitySettings from './quality-settings'; import AnalyticsReport from './analytics-report'; import AnnotationGuide from './guide'; -import ValidationLayout from './validation-layout'; +import { JobValidationLayout, TaskValidationLayout } from './validation-layout'; import { Request } from './request'; import BaseSingleFrameAction, { listActions, registerAction, runActions } from './annotations-actions'; import { @@ -216,7 +216,8 @@ export default interface CVATCore { AnalyticsReport: typeof AnalyticsReport; Request: typeof Request; FramesMetaData: typeof FramesMetaData; - ValidationLayout: typeof ValidationLayout; + JobValidationLayout: typeof JobValidationLayout; + TaskValidationLayout: typeof TaskValidationLayout; }; utils: { mask2Rle: typeof mask2Rle; diff --git a/cvat-core/src/server-proxy.ts b/cvat-core/src/server-proxy.ts index 7e8819808649..be8d3d4fb636 100644 --- a/cvat-core/src/server-proxy.ts +++ b/cvat-core/src/server-proxy.ts @@ -19,7 +19,7 @@ import { SerializedInvitationData, SerializedCloudStorage, SerializedFramesMetaData, SerializedCollection, SerializedQualitySettingsData, APIQualitySettingsFilter, SerializedQualityConflictData, APIQualityConflictsFilter, SerializedQualityReportData, APIQualityReportsFilter, SerializedAnalyticsReport, APIAnalyticsReportFilter, - SerializedRequest, SerializedValidationLayout, + SerializedRequest, SerializedJobValidationLayout, SerializedTaskValidationLayout, } from './server-response-types'; import { PaginatedResource } from './core-types'; import { Request } from './request'; @@ -1384,7 +1384,7 @@ async function deleteJob(jobID: number): Promise { const validationLayout = (instance: 'tasks' | 'jobs') => async ( id: number, -): Promise => { +): Promise => { const { backendAPI } = config; try { diff --git a/cvat-core/src/server-response-types.ts b/cvat-core/src/server-response-types.ts index 5dd8cc3d54d2..e28a6f9ec71a 100644 --- a/cvat-core/src/server-response-types.ts +++ b/cvat-core/src/server-response-types.ts @@ -524,8 +524,14 @@ export interface SerializedRequest { owner?: any; } -export interface SerializedValidationLayout { +export interface SerializedJobValidationLayout { honeypot_count?: number; honeypot_frames?: number[]; honeypot_real_frames?: number[]; } + +export interface SerializedTaskValidationLayout extends SerializedJobValidationLayout { + mode: 'gt' | 'gt_pool' | null; + validation_frames?: number[]; + disabled_frames?: number[]; +} diff --git a/cvat-core/src/session-implementation.ts b/cvat-core/src/session-implementation.ts index 38728a409448..47810637db37 100644 --- a/cvat-core/src/session-implementation.ts +++ b/cvat-core/src/session-implementation.ts @@ -27,7 +27,10 @@ import { decodePreview, } from './frames'; import Issue from './issue'; -import { SerializedLabel, SerializedTask, SerializedValidationLayout } from './server-response-types'; +import { + SerializedLabel, SerializedTask, SerializedJobValidationLayout, + SerializedTaskValidationLayout, +} from './server-response-types'; import { checkInEnum, checkObjectType } from './common'; import { getCollection, getSaver, clearAnnotations, getAnnotations, @@ -37,7 +40,7 @@ import AnnotationGuide from './guide'; import requestsManager from './requests-manager'; import { Request } from './request'; import User from './user'; -import ValidationLayout from './validation-layout'; +import { JobValidationLayout, TaskValidationLayout } from './validation-layout'; // must be called with task/job context async function deleteFrameWrapper(jobID, frame): Promise { @@ -171,7 +174,7 @@ export function implementJob(Job: typeof JobClass): typeof JobClass { ): ReturnType { const result = await serverProxy.jobs.validationLayout(this.id); if (Object.keys(result).length) { - return new ValidationLayout(result as Required); + return new JobValidationLayout(result as SerializedJobValidationLayout); } return null; @@ -641,9 +644,9 @@ export function implementTask(Task: typeof TaskClass): typeof TaskClass { value: async function validationLayoutImplementation( this: TaskClass, ): ReturnType { - const result = await serverProxy.tasks.validationLayout(this.id); - if (Object.keys(result).length) { - return new ValidationLayout(result as Required); + const result = await serverProxy.tasks.validationLayout(this.id) as SerializedTaskValidationLayout; + if (result.mode !== null) { + return new TaskValidationLayout(result); } return null; diff --git a/cvat-core/src/session.ts b/cvat-core/src/session.ts index cf82aa9a050c..8ecef7e0e632 100644 --- a/cvat-core/src/session.ts +++ b/cvat-core/src/session.ts @@ -28,7 +28,7 @@ import { Request } from './request'; import logger from './logger'; import Issue from './issue'; import ObjectState from './object-state'; -import ValidationLayout from './validation-layout'; +import { JobValidationLayout, TaskValidationLayout } from './validation-layout'; function buildDuplicatedAPI(prototype) { Object.defineProperties(prototype, { @@ -686,7 +686,7 @@ export class Job extends Session { return result; } - async validationLayout(): Promise { + async validationLayout(): Promise { const result = await PluginRegistry.apiWrapper.call(this, Job.prototype.validationLayout); return result; } @@ -1186,7 +1186,7 @@ export class Task extends Session { return result; } - async validationLayout(): Promise { + async validationLayout(): Promise { const result = await PluginRegistry.apiWrapper.call(this, Task.prototype.validationLayout); return result; } diff --git a/cvat-core/src/validation-layout.ts b/cvat-core/src/validation-layout.ts index ba5a94aa03a9..064af13b2514 100644 --- a/cvat-core/src/validation-layout.ts +++ b/cvat-core/src/validation-layout.ts @@ -2,37 +2,43 @@ // // SPDX-License-Identifier: MIT -import { SerializedValidationLayout } from 'server-response-types'; +import { SerializedJobValidationLayout, SerializedTaskValidationLayout } from 'server-response-types'; import PluginRegistry from './plugins'; -export default class ValidationLayout { - #honeypotFrames: number[]; - #honeypotRealFrames: number[]; +export class JobValidationLayout { + #honeypotCount: JobValidationLayout['honeypotCount']; + #honeypotFrames: JobValidationLayout['honeypotFrames']; + #honeypotRealFrames: JobValidationLayout['honeypotRealFrames']; - public constructor(data: Required) { - this.#honeypotFrames = [...data.honeypot_frames]; - this.#honeypotRealFrames = [...data.honeypot_real_frames]; + public constructor(data: SerializedJobValidationLayout) { + this.#honeypotCount = data.honeypot_count ?? 0; + this.#honeypotFrames = [...(data.honeypot_frames ?? [])]; + this.#honeypotRealFrames = [...(data.honeypot_real_frames ?? [])]; } - public get honeypotFrames() { + public get honeypotCount(): number { + return this.#honeypotCount; + } + + public get honeypotFrames(): number[] { return [...this.#honeypotFrames]; } - public get honeypotRealFrames() { + public get honeypotRealFrames(): number[] { return [...this.#honeypotRealFrames]; } async getRealFrame(frame: number): Promise { - const result = await PluginRegistry.apiWrapper.call(this, ValidationLayout.prototype.getRealFrame, frame); + const result = await PluginRegistry.apiWrapper.call(this, JobValidationLayout.prototype.getRealFrame, frame); return result; } } -Object.defineProperties(ValidationLayout.prototype.getRealFrame, { +Object.defineProperties(JobValidationLayout.prototype.getRealFrame, { implementation: { writable: false, enumerable: false, - value: function implementation(this: ValidationLayout, frame: number): number | null { + value: function implementation(this: JobValidationLayout, frame: number): number | null { const index = this.honeypotFrames.indexOf(frame); if (index !== -1) { return this.honeypotRealFrames[index]; @@ -42,3 +48,28 @@ Object.defineProperties(ValidationLayout.prototype.getRealFrame, { }, }, }); + +export class TaskValidationLayout extends JobValidationLayout { + #mode: TaskValidationLayout['mode']; + #validationFrames: TaskValidationLayout['validationFrames']; + #disabledFrames: TaskValidationLayout['disabledFrames']; + + public constructor(data: SerializedTaskValidationLayout) { + super(data); + this.#mode = data.mode; + this.#validationFrames = [...(data.validation_frames ?? [])]; + this.#disabledFrames = [...(data.disabled_frames ?? [])]; + } + + public get mode(): NonNullable { + return this.#mode; + } + + public get validationFrames(): number[] { + return [...this.#validationFrames]; + } + + public get disabledFrames(): number[] { + return [...this.#disabledFrames]; + } +} diff --git a/cvat-ui/src/components/quality-control/quality-control-page.tsx b/cvat-ui/src/components/quality-control/quality-control-page.tsx index 64e475321cfa..b452f34ea09b 100644 --- a/cvat-ui/src/components/quality-control/quality-control-page.tsx +++ b/cvat-ui/src/components/quality-control/quality-control-page.tsx @@ -13,10 +13,10 @@ import { Row, Col } from 'antd/lib/grid'; import Tabs, { TabsProps } from 'antd/lib/tabs'; import Title from 'antd/lib/typography/Title'; import notification from 'antd/lib/notification'; -import { useIsMounted } from 'utils/hooks'; + import { - Job, JobType, QualityReport, QualitySettings, Task, getCore, FramesMetaData, - TargetMetric, + Job, JobType, QualityReport, QualitySettings, Task, + TargetMetric, TaskValidationLayout, getCore, FramesMetaData, } from 'cvat-core-wrapper'; import CVATLoadingSpinner from 'components/common/loading-spinner'; import GoBackButton from 'components/common/go-back-button'; @@ -32,20 +32,17 @@ function getTabFromHash(supportedTabs: string[]): string { return supportedTabs.includes(tab) ? tab : supportedTabs[0]; } -type InstanceType = 'task'; - interface State { fetching: boolean; reportRefreshingStatus: string | null; - gtJob: { - instance: Job | null, - meta: FramesMetaData | null, - }, + validationLayout: TaskValidationLayout | null; + gtJobInstance: Job | null; + gtJobMeta: FramesMetaData | null; qualitySettings: { settings: QualitySettings | null; fetching: boolean; targetMetric: TargetMetric | null; - }, + }; } enum ReducerActionType { @@ -57,6 +54,7 @@ enum ReducerActionType { SET_REPORT_REFRESHING_STATUS = 'SET_REPORT_REFRESHING_STATUS', SET_GT_JOB = 'SET_GT_JOB', SET_GT_JOB_META = 'SET_GT_JOB_META', + SET_VALIDATION_LAYOUT = 'SET_VALIDATION_LAYOUT', } export const reducerActions = { @@ -78,11 +76,14 @@ export const reducerActions = { setReportRefreshingStatus: (status: string | null) => ( createAction(ReducerActionType.SET_REPORT_REFRESHING_STATUS, { status }) ), - setGtJob: (job: Job | null) => ( - createAction(ReducerActionType.SET_GT_JOB, { job }) + setGtJob: (gtJobInstance: Job | null) => ( + createAction(ReducerActionType.SET_GT_JOB, { gtJobInstance }) + ), + setGtJobMeta: (gtJobMeta: FramesMetaData | null) => ( + createAction(ReducerActionType.SET_GT_JOB_META, { gtJobMeta }) ), - setGtJobMeta: (meta: FramesMetaData | null) => ( - createAction(ReducerActionType.SET_GT_JOB_META, { meta }) + setValidationLayout: (validationLayout: TaskValidationLayout | null) => ( + createAction(ReducerActionType.SET_VALIDATION_LAYOUT, { validationLayout }) ), }; @@ -125,20 +126,21 @@ const reducer = (state: State, action: ActionUnion): Stat if (action.type === ReducerActionType.SET_GT_JOB) { return { ...state, - gtJob: { - ...state.gtJob, - instance: action.payload.job, - }, + gtJobInstance: action.payload.gtJobInstance, }; } if (action.type === ReducerActionType.SET_GT_JOB_META) { return { ...state, - gtJob: { - ...state.gtJob, - meta: action.payload.meta, - }, + gtJobMeta: action.payload.gtJobMeta, + }; + } + + if (action.type === ReducerActionType.SET_VALIDATION_LAYOUT) { + return { + ...state, + validationLayout: action.payload.validationLayout, }; } @@ -146,13 +148,13 @@ const reducer = (state: State, action: ActionUnion): Stat }; function QualityControlPage(): JSX.Element { + const supportedTabs = ['overview', 'settings', 'management']; const [state, dispatch] = useReducer(reducer, { fetching: true, reportRefreshingStatus: null, - gtJob: { - instance: null, - meta: null, - }, + gtJobInstance: null, + gtJobMeta: null, + validationLayout: null, qualitySettings: { settings: null, fetching: true, @@ -160,73 +162,45 @@ function QualityControlPage(): JSX.Element { }, }); - const requestedInstanceType: InstanceType = 'task'; const requestedInstanceID = +useParams<{ tid: string }>().tid; - const [instanceType, setInstanceType] = useState(null); - const [instance, setInstance] = useState(null); - const isMounted = useIsMounted(); - - const supportedTabs = ['overview', 'settings', 'management']; const [activeTab, setActiveTab] = useState(getTabFromHash(supportedTabs)); - const receiveInstance = async (type: InstanceType, id: number): Promise => { - let receivedInstance: Task | null = null; - let gtJob: Job | null = null; - let gtJobMeta: FramesMetaData | null = null; + const [instance, setInstance] = useState(null); + const initializeData = async (id: number): Promise => { try { - if (type === 'task') { - [receivedInstance] = await core.tasks.get({ id }); - gtJob = receivedInstance.jobs.find((job: Job) => job.type === JobType.GROUND_TRUTH) ?? null; - if (gtJob) { - gtJobMeta = await core.frames.getMeta('job', gtJob.id) as FramesMetaData; - } + const [taskInstance] = await core.tasks.get({ id }); + if (!taskInstance) { + throw new Error('Could not receive requested task from the server'); } else { - return null; + setInstance(taskInstance); + dispatch(reducerActions.setQualitySettingsFetching(true)); + try { + const settings = await core.analytics.quality.settings.get({ taskID: taskInstance.id }); + dispatch(reducerActions.setQualitySettings(settings)); + } finally { + dispatch(reducerActions.setQualitySettingsFetching(false)); + } } - if (isMounted()) { + const gtJob = taskInstance.jobs.find((job: Job) => job.type === JobType.GROUND_TRUTH) ?? null; + if (gtJob) { + const validationLayout: TaskValidationLayout | null = await taskInstance.validationLayout(); + const gtJobMeta = await core.frames.getMeta('job', gtJob.id) as FramesMetaData; dispatch(reducerActions.setGtJob(gtJob)); dispatch(reducerActions.setGtJobMeta(gtJobMeta)); - setInstance(receivedInstance); - setInstanceType(type); + dispatch(reducerActions.setValidationLayout(validationLayout)); } - return receivedInstance; + + dispatch(reducerActions.setFetching(false)); } catch (error: unknown) { notification.error({ - message: `Could not receive requested ${type}`, + message: 'Could not initialize the page', description: `${error instanceof Error ? error.message : ''}`, }); - return null; } }; - const receiveSettings = useCallback(async (taskInstance: Task) => { - dispatch(reducerActions.setQualitySettingsFetching(true)); - - function handleError(error: Error): void { - if (isMounted()) { - notification.error({ - description: error.toString(), - message: 'Could not initialize quality control page', - }); - } - } - - try { - const settingsRequest = core.analytics.quality.settings.get({ taskID: taskInstance.id }); - - await Promise.all([settingsRequest]).then(([settings]) => { - dispatch(reducerActions.setQualitySettings(settings)); - }).catch(handleError).finally(() => { - dispatch(reducerActions.setQualitySettingsFetching(false)); - dispatch(reducerActions.setFetching(false)); - }); - } catch (error: unknown) { - handleError(error as Error); - } - }, [instance]); - const onSaveQualitySettings = useCallback(async (values) => { try { const { settings } = state.qualitySettings; @@ -274,48 +248,41 @@ function QualityControlPage(): JSX.Element { } }, [state.qualitySettings.settings]); - const updateMeta = (action: (frameID: number) => void) => async (frameIDs: number[]): Promise => { - const { instance: gtJob } = state.gtJob; - if (gtJob) { - dispatch(reducerActions.setFetching(true)); - await Promise.all(frameIDs.map((frameID: number): void => action(frameID))); - const [newMeta] = await gtJob.frames.save(); + const updateMeta = async (): Promise => { + dispatch(reducerActions.setFetching(true)); + try { + const [newMeta] = await (state.gtJobInstance as Job).frames.save(); + const validationLayout: TaskValidationLayout | null = await (instance as Task).validationLayout(); dispatch(reducerActions.setGtJobMeta(newMeta)); + dispatch(reducerActions.setValidationLayout(validationLayout)); + } finally { dispatch(reducerActions.setFetching(false)); } }; - const onDeleteFrames = useCallback( - updateMeta((frameID: number) => (state.gtJob.instance?.frames.delete(frameID))), - [state.gtJob.instance], - ); - - const onRestoreFrames = useCallback( - updateMeta((frameID: number) => (state.gtJob.instance?.frames.restore(frameID))), - [state.gtJob.instance], - ); + const onDeleteFrames = useCallback((frameIDs: number[]): void => { + if (state.gtJobInstance && instance) { + for (const frameID of frameIDs) { + state.gtJobInstance.frames.delete(frameID); + } - useEffect(() => { - if (Number.isInteger(requestedInstanceID) && ['task'].includes(requestedInstanceType)) { - dispatch(reducerActions.setFetching(true)); - receiveInstance(requestedInstanceType, requestedInstanceID).then((task) => { - if (task) { - receiveSettings(task); - } - }); - } else { - notification.error({ - message: 'Could not load this page', - description: `Not valid resource ${requestedInstanceType} #${requestedInstanceID}`, - }); + updateMeta(); } + }, [state.gtJobInstance]); - return () => { - if (isMounted()) { - setInstance(null); + const onRestoreFrames = useCallback((frameIDs: number[]): void => { + if (state.gtJobInstance && instance) { + for (const frameID of frameIDs) { + state.gtJobInstance.frames.restore(frameID); } - }; - }, [requestedInstanceType, requestedInstanceID]); + + updateMeta(); + } + }, [state.gtJobInstance]); + + useEffect(() => { + initializeData(requestedInstanceID); + }, [requestedInstanceID]); useEffect(() => { window.addEventListener('hashchange', () => { @@ -338,10 +305,9 @@ function QualityControlPage(): JSX.Element { const { fetching, - gtJob: { - instance: gtJobInstance, - meta: gtJobMeta, - }, + gtJobInstance, + gtJobMeta, + validationLayout, qualitySettings: { settings: qualitySettings, fetching: qualitySettingsFetching, @@ -350,7 +316,7 @@ function QualityControlPage(): JSX.Element { } = state; const settingsInitialized = qualitySettings && targetMetric; - if (instanceType && instance && settingsInitialized) { + if (instance && settingsInitialized) { backNavigation = ( @@ -359,43 +325,44 @@ function QualityControlPage(): JSX.Element { ); - const qualityControlFor = {`Task #${instance.id}`}; title = ( Quality control for - {' '} - {qualityControlFor} + <Link to={`/tasks/${instance.id}`}>{` Task #${instance.id}`}</Link> ); - const tabsItems: [NonNullable[0], number][] = []; - tabsItems.push([{ + const tabsItems: NonNullable[0][] = []; + tabsItems.push({ key: 'overview', label: 'Overview', children: ( ), - }, 10]); + }); if (gtJobInstance && gtJobMeta) { - tabsItems.push([{ - key: 'management', - label: 'Management', - children: ( - - ), - }, 20]); + if (validationLayout) { + tabsItems.push({ + key: 'management', + label: 'Management', + children: ( + + ), + }); + } - tabsItems.push([{ + tabsItems.push({ key: 'settings', label: 'Settings', children: ( @@ -405,11 +372,9 @@ function QualityControlPage(): JSX.Element { setQualitySettings={onSaveQualitySettings} /> ), - }, 30]); + }); } - tabsItems.sort((item1, item2) => item1[1] - item2[1]); - tabs = ( item[0])} + items={tabsItems} /> ); } return (
- {fetching && qualitySettingsFetching ? ( + {fetching || qualitySettingsFetching ? (
diff --git a/cvat-ui/src/components/quality-control/task-quality/allocation-table.tsx b/cvat-ui/src/components/quality-control/task-quality/allocation-table.tsx index 363d08374001..43ac48e4f335 100644 --- a/cvat-ui/src/components/quality-control/task-quality/allocation-table.tsx +++ b/cvat-ui/src/components/quality-control/task-quality/allocation-table.tsx @@ -2,7 +2,6 @@ // // SPDX-License-Identifier: MIT -import { range } from 'lodash'; import React, { useState } from 'react'; import { useHistory } from 'react-router'; import { useSelector } from 'react-redux'; @@ -15,14 +14,15 @@ import { Key } from 'antd/lib/table/interface'; import Icon, { DeleteOutlined } from '@ant-design/icons'; import { RestoreIcon } from 'icons'; -import { Task, Job, FramesMetaData } from 'cvat-core-wrapper'; +import { Task, FramesMetaData, TaskValidationLayout } from 'cvat-core-wrapper'; import CVATTooltip from 'components/common/cvat-tooltip'; import { sorter } from 'utils/quality'; interface Props { task: Task; - gtJob: Job; + gtJobId: number; gtJobMeta: FramesMetaData; + validationLayout: TaskValidationLayout; onDeleteFrames: (frames: number[]) => void; onRestoreFrames: (frames: number[]) => void; } @@ -33,52 +33,9 @@ interface RowData { active: boolean; } -interface TableRowData extends RowData { - key: Key; -} - -// Temporary solution: this function is necessary in one of plugins which imports it directly from CVAT code -// Further this solution should be re-designed -// Until then, *DO NOT RENAME/REMOVE* this exported function -export function getAllocationTableContents(gtJobMeta: FramesMetaData, gtJob: Job): TableRowData[] { - // A workaround for meta "includedFrames" using source data numbers - // TODO: remove once meta is migrated to relative frame numbers - - const jobFrameNumbers = gtJobMeta.getDataFrameNumbers().map((dataFrameNumber: number) => ( - gtJobMeta.getJobRelativeFrameNumber(dataFrameNumber) + gtJob.startFrame - )); - - const jobDataSegmentFrameNumbers = range( - gtJobMeta.startFrame, gtJobMeta.stopFrame + 1, gtJobMeta.frameStep, - ); - - let includedIndex = 0; - const result: TableRowData[] = []; - for (let index = 0; index < jobDataSegmentFrameNumbers.length; ++index) { - const dataFrameID = jobDataSegmentFrameNumbers[index]; - - if (gtJobMeta.includedFrames && !gtJobMeta.includedFrames.includes(dataFrameID)) { - continue; - } - - const frameID = jobFrameNumbers[includedIndex]; - - result.push({ - key: frameID, - frame: frameID, - name: gtJobMeta.frames[index]?.name ?? gtJobMeta.frames[0].name, - active: !(frameID in gtJobMeta.deletedFrames), - }); - - ++includedIndex; - } - - return result; -} - function AllocationTable(props: Readonly): JSX.Element { const { - task, gtJob, gtJobMeta, + task, gtJobId, gtJobMeta, validationLayout, onDeleteFrames, onRestoreFrames, } = props; @@ -88,7 +45,13 @@ function AllocationTable(props: Readonly): JSX.Element { selectedRows: [], }); - const data = getAllocationTableContents(gtJobMeta, gtJob); + const { disabledFrames } = validationLayout; + const data = validationLayout.validationFrames.map((frame: number, index: number) => ({ + key: frame, + frame, + name: gtJobMeta.frames[index]?.name ?? gtJobMeta.frames[0].name, + active: !disabledFrames.includes(frame), + })); const columns = [ { @@ -104,7 +67,7 @@ function AllocationTable(props: Readonly): JSX.Element { type='link' onClick={(e: React.MouseEvent): void => { e.preventDefault(); - history.push(`/tasks/${task.id}/jobs/${gtJob.id}?frame=${frame}`); + history.push(`/tasks/${task.id}/jobs/${gtJobId}?frame=${frame}`); }} > {`#${frame}`} @@ -125,7 +88,7 @@ function AllocationTable(props: Readonly): JSX.Element { type='link' onClick={(e: React.MouseEvent): void => { e.preventDefault(); - history.push(`/tasks/${task.id}/jobs/${gtJob.id}?frame=${record.frame}`); + history.push(`/tasks/${task.id}/jobs/${gtJobId}?frame=${record.frame}`); }} > {name} diff --git a/cvat-ui/src/components/quality-control/task-quality/quality-magement-tab.tsx b/cvat-ui/src/components/quality-control/task-quality/quality-magement-tab.tsx index 03e6103db861..b54fc19e94af 100644 --- a/cvat-ui/src/components/quality-control/task-quality/quality-magement-tab.tsx +++ b/cvat-ui/src/components/quality-control/task-quality/quality-magement-tab.tsx @@ -6,14 +6,15 @@ import React from 'react'; import { Row, Col } from 'antd/es/grid'; import Spin from 'antd/lib/spin'; -import { FramesMetaData, Job, Task } from 'cvat-core-wrapper'; +import { FramesMetaData, Task, TaskValidationLayout } from 'cvat-core-wrapper'; import AllocationTable from './allocation-table'; import SummaryComponent from './summary'; interface Props { task: Task; - gtJob: Job; + gtJobId: number; gtJobMeta: FramesMetaData; + validationLayout: TaskValidationLayout; fetching: boolean; onDeleteFrames: (frames: number[]) => void; onRestoreFrames: (frames: number[]) => void; @@ -21,12 +22,12 @@ interface Props { function QualityManagementTab(props: Readonly): JSX.Element { const { - task, gtJob, gtJobMeta, fetching, + task, gtJobId, gtJobMeta, fetching, validationLayout, onDeleteFrames, onRestoreFrames, } = props; - const totalCount = gtJobMeta.getDataFrameNumbers().length; - const excludedCount = Object.keys(gtJobMeta.deletedFrames).length; + const totalCount = validationLayout.validationFrames.length; + const excludedCount = validationLayout.disabledFrames.length; const activeCount = totalCount - excludedCount; return ( @@ -41,6 +42,7 @@ function QualityManagementTab(props: Readonly): JSX.Element { ): JSX.Element { diff --git a/cvat-ui/src/components/quality-control/task-quality/summary.tsx b/cvat-ui/src/components/quality-control/task-quality/summary.tsx index 59767192ed0d..30fc859feb54 100644 --- a/cvat-ui/src/components/quality-control/task-quality/summary.tsx +++ b/cvat-ui/src/components/quality-control/task-quality/summary.tsx @@ -5,40 +5,51 @@ import React from 'react'; import { Row, Col } from 'antd/es/grid'; import Text from 'antd/lib/typography/Text'; + import AnalyticsCard from 'components/analytics-page/views/analytics-card'; export interface Props { + mode: 'gt' | 'gt_pool' excludedCount: number; totalCount: number; activeCount: number; } export default function SummaryComponent(props: Readonly): JSX.Element { - const { excludedCount, totalCount, activeCount } = props; + const { + excludedCount, totalCount, activeCount, mode, + } = props; const reportInfo = ( - + - Excluded count: + Validation mode: {' '} - {excludedCount} + {mode === 'gt' ? 'Ground Truth' : 'Honeypots'} - Total count: + Total validation frames: {' '} {totalCount} + + + Excluded validation frames: + {' '} + {excludedCount} + + - Active count: + Active validation frames: {' '} {activeCount} diff --git a/cvat-ui/src/cvat-core-wrapper.ts b/cvat-ui/src/cvat-core-wrapper.ts index dac86011953a..275cedcc8ab9 100644 --- a/cvat-ui/src/cvat-core-wrapper.ts +++ b/cvat-ui/src/cvat-core-wrapper.ts @@ -35,7 +35,7 @@ import Comment from 'cvat-core/src/comment'; import User from 'cvat-core/src/user'; import Organization, { Membership, Invitation } from 'cvat-core/src/organization'; import AnnotationGuide from 'cvat-core/src/guide'; -import ValidationLayout from 'cvat-core/src/validation-layout'; +import { JobValidationLayout, TaskValidationLayout } from 'cvat-core/src/validation-layout'; import AnalyticsReport, { AnalyticsEntryViewType, AnalyticsEntry } from 'cvat-core/src/analytics-report'; import { Dumper } from 'cvat-core/src/annotation-formats'; import { Event } from 'cvat-core/src/event'; @@ -106,7 +106,8 @@ export { ActionParameterType, FrameSelectionType, Request, - ValidationLayout, + JobValidationLayout, + TaskValidationLayout, }; export type { diff --git a/cvat-ui/src/reducers/index.ts b/cvat-ui/src/reducers/index.ts index 999d7d6c5419..a9b89d20cff7 100644 --- a/cvat-ui/src/reducers/index.ts +++ b/cvat-ui/src/reducers/index.ts @@ -8,7 +8,7 @@ import { Canvas, RectDrawingMethod, CuboidDrawingMethod } from 'cvat-canvas-wrap import { Webhook, MLModel, Organization, Job, Task, Project, Label, User, QualityConflict, FramesMetaData, RQStatus, Event, Invitation, SerializedAPISchema, - Request, TargetMetric, ValidationLayout, + Request, TargetMetric, JobValidationLayout, } from 'cvat-core-wrapper'; import { IntelligentScissors } from 'utils/opencv-wrapper/intelligent-scissors'; import { KeyMap, KeyMapItem } from 'utils/mousetrap-react'; @@ -730,7 +730,7 @@ export interface AnnotationState { defaultPointsCount: number | null; }; groundTruthInfo: { - validationLayout: ValidationLayout | null; + validationLayout: JobValidationLayout | null; groundTruthJobFramesMeta: FramesMetaData | null; groundTruthInstance: Job | null; }, From 8e47fe804ff62a61db2fe769d8c458adcd7c03c2 Mon Sep 17 00:00:00 2001 From: Boris Sekachev Date: Mon, 21 Oct 2024 10:19:13 +0300 Subject: [PATCH 2/3] Missing line --- cvat-ui/src/actions/annotation-actions.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cvat-ui/src/actions/annotation-actions.ts b/cvat-ui/src/actions/annotation-actions.ts index 9e3eeb8176b3..670ace099e5a 100644 --- a/cvat-ui/src/actions/annotation-actions.ts +++ b/cvat-ui/src/actions/annotation-actions.ts @@ -12,7 +12,7 @@ import { } from 'cvat-canvas-wrapper'; import { getCore, MLModel, JobType, Job, QualityConflict, - ObjectState, JobState, ValidationLayout, + ObjectState, JobState, JobValidationLayout, } from 'cvat-core-wrapper'; import logger, { EventScope } from 'cvat-logger'; import { getCVATStore } from 'cvat-store'; @@ -38,7 +38,7 @@ interface AnnotationsParameters { showGroundTruth: boolean; jobInstance: Job; groundTruthInstance: Job | null; - validationLayout: ValidationLayout | null; + validationLayout: JobValidationLayout | null; } const cvat = getCore(); From b598ff04336008552807d3d44805231a53a5446d Mon Sep 17 00:00:00 2001 From: Boris Sekachev Date: Mon, 21 Oct 2024 13:23:41 +0300 Subject: [PATCH 3/3] Refactoring --- .../quality-control/quality-control-page.tsx | 135 +++++++++++------- .../components/quality-control/styles.scss | 7 +- 2 files changed, 88 insertions(+), 54 deletions(-) diff --git a/cvat-ui/src/components/quality-control/quality-control-page.tsx b/cvat-ui/src/components/quality-control/quality-control-page.tsx index b452f34ea09b..cb00382db30f 100644 --- a/cvat-ui/src/components/quality-control/quality-control-page.tsx +++ b/cvat-ui/src/components/quality-control/quality-control-page.tsx @@ -13,6 +13,7 @@ import { Row, Col } from 'antd/lib/grid'; import Tabs, { TabsProps } from 'antd/lib/tabs'; import Title from 'antd/lib/typography/Title'; import notification from 'antd/lib/notification'; +import Result from 'antd/lib/result'; import { Job, JobType, QualityReport, QualitySettings, Task, @@ -38,6 +39,7 @@ interface State { validationLayout: TaskValidationLayout | null; gtJobInstance: Job | null; gtJobMeta: FramesMetaData | null; + error: Error | null; qualitySettings: { settings: QualitySettings | null; fetching: boolean; @@ -55,6 +57,7 @@ enum ReducerActionType { SET_GT_JOB = 'SET_GT_JOB', SET_GT_JOB_META = 'SET_GT_JOB_META', SET_VALIDATION_LAYOUT = 'SET_VALIDATION_LAYOUT', + SET_ERROR = 'SET_ERROR', } export const reducerActions = { @@ -85,6 +88,9 @@ export const reducerActions = { setValidationLayout: (validationLayout: TaskValidationLayout | null) => ( createAction(ReducerActionType.SET_VALIDATION_LAYOUT, { validationLayout }) ), + setError: (error: Error) => ( + createAction(ReducerActionType.SET_ERROR, { error }) + ), }; const reducer = (state: State, action: ActionUnion): State => { @@ -144,6 +150,13 @@ const reducer = (state: State, action: ActionUnion): Stat }; } + if (action.type === ReducerActionType.SET_ERROR) { + return { + ...state, + error: action.payload.error, + }; + } + return state; }; @@ -155,9 +168,10 @@ function QualityControlPage(): JSX.Element { gtJobInstance: null, gtJobMeta: null, validationLayout: null, + error: null, qualitySettings: { settings: null, - fetching: true, + fetching: false, targetMetric: null, }, }); @@ -169,18 +183,20 @@ function QualityControlPage(): JSX.Element { const initializeData = async (id: number): Promise => { try { - const [taskInstance] = await core.tasks.get({ id }); - if (!taskInstance) { - throw new Error('Could not receive requested task from the server'); - } else { - setInstance(taskInstance); + let taskInstance = null; + try { + [taskInstance] = await core.tasks.get({ id }); + } catch (error: unknown) { + throw new Error('The task was not found on the server'); + } + + setInstance(taskInstance); + try { dispatch(reducerActions.setQualitySettingsFetching(true)); - try { - const settings = await core.analytics.quality.settings.get({ taskID: taskInstance.id }); - dispatch(reducerActions.setQualitySettings(settings)); - } finally { - dispatch(reducerActions.setQualitySettingsFetching(false)); - } + const settings = await core.analytics.quality.settings.get({ taskID: taskInstance.id }); + dispatch(reducerActions.setQualitySettings(settings)); + } finally { + dispatch(reducerActions.setQualitySettingsFetching(false)); } const gtJob = taskInstance.jobs.find((job: Job) => job.type === JobType.GROUND_TRUTH) ?? null; @@ -191,13 +207,10 @@ function QualityControlPage(): JSX.Element { dispatch(reducerActions.setGtJobMeta(gtJobMeta)); dispatch(reducerActions.setValidationLayout(validationLayout)); } - - dispatch(reducerActions.setFetching(false)); } catch (error: unknown) { - notification.error({ - message: 'Could not initialize the page', - description: `${error instanceof Error ? error.message : ''}`, - }); + dispatch(reducerActions.setError(error instanceof Error ? error : new Error('Unknown error'))); + } finally { + dispatch(reducerActions.setFetching(false)); } }; @@ -299,7 +312,13 @@ function QualityControlPage(): JSX.Element { setActiveTab(key); }, []); - let backNavigation: JSX.Element | null = null; + const backNavigation: JSX.Element | null = ( + + + + + + ); let title: JSX.Element | null = null; let tabs: JSX.Element | null = null; @@ -308,6 +327,7 @@ function QualityControlPage(): JSX.Element { gtJobInstance, gtJobMeta, validationLayout, + error, qualitySettings: { settings: qualitySettings, fetching: qualitySettingsFetching, @@ -315,16 +335,32 @@ function QualityControlPage(): JSX.Element { }, } = state; - const settingsInitialized = qualitySettings && targetMetric; - if (instance && settingsInitialized) { - backNavigation = ( - - - - - + if (error) { + return ( +
+
+ +
+
); + } + if (fetching || qualitySettingsFetching) { + return ( +
+
+ +
+
+ ); + } + + if (instance) { title = ( @@ -335,13 +371,16 @@ function QualityControlPage(): JSX.Element { ); const tabsItems: NonNullable<TabsProps['items']>[0][] = []; - tabsItems.push({ - key: 'overview', - label: 'Overview', - children: ( - <QualityOverviewTab task={instance} targetMetric={targetMetric} /> - ), - }); + + if (targetMetric) { + tabsItems.push({ + key: 'overview', + label: 'Overview', + children: ( + <QualityOverviewTab task={instance} targetMetric={targetMetric} /> + ), + }); + } if (gtJobInstance && gtJobMeta) { if (validationLayout) { @@ -389,23 +428,17 @@ function QualityControlPage(): JSX.Element { return ( <div className='cvat-quality-control-page'> - {fetching || qualitySettingsFetching ? ( - <div className='cvat-quality-control-loading'> - <CVATLoadingSpinner /> - </div> - ) : ( - <Row className='cvat-quality-control-wrapper'> - <Col span={24}> - {backNavigation} - <Row justify='center'> - <Col span={22} xl={18} xxl={14} className='cvat-quality-control-inner'> - {title} - {tabs} - </Col> - </Row> - </Col> - </Row> - )} + <Row className='cvat-quality-control-wrapper'> + <Col span={24}> + {backNavigation} + <Row justify='center'> + <Col span={22} xl={18} xxl={14} className='cvat-quality-control-inner'> + {title} + {tabs} + </Col> + </Row> + </Col> + </Row> </div> ); } diff --git a/cvat-ui/src/components/quality-control/styles.scss b/cvat-ui/src/components/quality-control/styles.scss index 6149d317d105..70a8e2fcea81 100644 --- a/cvat-ui/src/components/quality-control/styles.scss +++ b/cvat-ui/src/components/quality-control/styles.scss @@ -115,10 +115,11 @@ $excluded-background: #d9d9d973; } } -.cvat-quality-control-loading { +.cvat-quality-control-loading, .cvat-quality-control-page-error { position: absolute; - right: 50%; - margin-top: 20%; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); } .cvat-quality-control-overview-tab {