Skip to content

Commit

Permalink
Merge branch 'develop' into zm/refactor-quality-reports-impl
Browse files Browse the repository at this point in the history
  • Loading branch information
zhiltsov-max authored Jan 31, 2025
2 parents b94d430 + 9892390 commit 95ad74c
Show file tree
Hide file tree
Showing 82 changed files with 1,704 additions and 1,192 deletions.
4 changes: 2 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ __pycache__
.coverage
.husky/
.python-version
tmp*cvat/
temp*/
/tmp*cvat/
/temp*/

# Ignore generated test files
docker-compose.tests.yml
Expand Down
1 change: 0 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,6 @@ up to 10x. Here is a list of the algorithms we support, and the platforms they c
| [HRNet](/serverless/pytorch/saic-vul/hrnet/nuclio) | interactor | PyTorch | | ✔️ |
| [Inside-Outside Guidance](/serverless/pytorch/shiyinzhang/iog/nuclio) | interactor | PyTorch | ✔️ | |
| [Faster RCNN](/serverless/tensorflow/faster_rcnn_inception_v2_coco/nuclio) | detector | TensorFlow | ✔️ | ✔️ |
| [Mask RCNN](/serverless/tensorflow/matterport/mask_rcnn/nuclio) | detector | TensorFlow | ✔️ | ✔️ |
| [RetinaNet](serverless/pytorch/facebookresearch/detectron2/retinanet_r101/nuclio) | detector | PyTorch | ✔️ | ✔️ |
| [Face Detection](/serverless/openvino/omz/intel/face-detection-0205/nuclio) | detector | OpenVINO | ✔️ | |

Expand Down
5 changes: 5 additions & 0 deletions changelog.d/20250128_144218_roman_aa_fun_label_types.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
### Added

- \[SDK\] The shapes output by auto-annotation functions are now checked
for compatibility with the function's and the task's label specs
(<https://github.com/cvat-ai/cvat/pull/9005>)
4 changes: 4 additions & 0 deletions changelog.d/20250129_140847_klakhov_add_detector_threshold.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
### Added

- A `threshold` parameter to UI detector runner
(<https://github.com/cvat-ai/cvat/pull/9011>)
5 changes: 5 additions & 0 deletions changelog.d/20250130_185457_roman.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
### Fixed

- \[Compose\] An outdated version of Traefik is no longer used in
deployments with HTTPS enabled
(<https://github.com/cvat-ai/cvat/pull/9028>)
80 changes: 54 additions & 26 deletions cvat-sdk/cvat_sdk/auto_annotation/driver.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,12 +56,28 @@ class _AnnotationMapper:
class _LabelIdMapping:
id: int
sublabels: Mapping[int, Optional[_AnnotationMapper._SublabelIdMapping]]
expected_num_elements: int = 0
expected_num_elements: int
expected_type: str

_SpecIdMapping: TypeAlias = Mapping[int, Optional[_LabelIdMapping]]

_spec_id_mapping: _SpecIdMapping

def _get_expected_function_output_type(self, fun_label, ds_label):
fun_output_type = getattr(fun_label, "type", "any")
if fun_output_type == "any":
return ds_label.type

if self._conv_mask_to_poly and fun_output_type == "mask":
fun_output_type = "polygon"

if not self._are_label_types_compatible(fun_output_type, ds_label.type):
raise BadFunctionError(
f"label {fun_label.name!r} has type {fun_output_type!r} in the function,"
f" but {ds_label.type!r} in the dataset"
)
return fun_output_type

def _build_label_id_mapping(
self,
fun_label: models.ILabel,
Expand All @@ -70,36 +86,37 @@ def _build_label_id_mapping(
label_nm: _LabelNameMapping,
allow_unmatched_labels: bool,
) -> Optional[_LabelIdMapping]:
sl_map = {}

if getattr(fun_label, "sublabels", []):
ds_sublabels_by_name = {ds_sl.name: ds_sl for ds_sl in ds_label.sublabels}

def sublabel_mapping(fun_sl: models.ILabel) -> Optional[int]:
sublabel_nm = label_nm.map_sublabel(fun_sl.name)
if sublabel_nm is None:
return None

ds_sl = ds_sublabels_by_name.get(sublabel_nm.name)
if not ds_sl:
if not allow_unmatched_labels:
raise BadFunctionError(
f"sublabel {fun_sl.name!r} of label {fun_label.name!r} is not in dataset"
)

self._logger.info(
"sublabel %r of label %r is not in dataset; any annotations using it will be ignored",
fun_sl.name,
fun_label.name,
ds_sublabels_by_name = {ds_sl.name: ds_sl for ds_sl in ds_label.sublabels}

def sublabel_mapping(fun_sl: models.ILabel) -> Optional[int]:
sublabel_nm = label_nm.map_sublabel(fun_sl.name)
if sublabel_nm is None:
return None

ds_sl = ds_sublabels_by_name.get(sublabel_nm.name)
if not ds_sl:
if not allow_unmatched_labels:
raise BadFunctionError(
f"sublabel {fun_sl.name!r} of label {fun_label.name!r} is not in dataset"
)
return None

return ds_sl.id
self._logger.info(
"sublabel %r of label %r is not in dataset; any annotations using it will be ignored",
fun_sl.name,
fun_label.name,
)
return None

sl_map = {fun_sl.id: sublabel_mapping(fun_sl) for fun_sl in fun_label.sublabels}
return ds_sl.id

return self._LabelIdMapping(
ds_label.id, sublabels=sl_map, expected_num_elements=len(ds_label.sublabels)
ds_label.id,
sublabels={
fun_sl.id: sublabel_mapping(fun_sl)
for fun_sl in getattr(fun_label, "sublabels", [])
},
expected_num_elements=len(ds_label.sublabels),
expected_type=self._get_expected_function_output_type(fun_label, ds_label),
)

def _build_spec_id_mapping(
Expand Down Expand Up @@ -254,6 +271,12 @@ def _remap_shape(self, shape: models.LabeledShapeRequest, ds_frame: int) -> bool

shape.label_id = label_id_mapping.id

if not self._are_label_types_compatible(shape.type.value, label_id_mapping.expected_type):
raise BadFunctionError(
f"function output shape of type {shape.type.value!r}"
f" (expected {label_id_mapping.expected_type!r})"
)

if shape.type.value == "mask" and self._conv_mask_to_poly:
raise BadFunctionError("function output mask shape despite conv_mask_to_poly=True")

Expand All @@ -269,6 +292,11 @@ def _remap_shape(self, shape: models.LabeledShapeRequest, ds_frame: int) -> bool
def validate_and_remap(self, shapes: list[models.LabeledShapeRequest], ds_frame: int) -> None:
shapes[:] = [shape for shape in shapes if self._remap_shape(shape, ds_frame)]

@staticmethod
def _are_label_types_compatible(source_type: str, destination_type: str) -> bool:
assert source_type != "any"
return destination_type == "any" or destination_type == source_type


@attrs.frozen(kw_only=True)
class _DetectionFunctionContextImpl(DetectionFunctionContext):
Expand Down
4 changes: 3 additions & 1 deletion cvat-sdk/cvat_sdk/auto_annotation/functions/_torchvision.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@


class TorchvisionFunction:
_label_type = "any"

def __init__(self, model_name: str, weights_name: str = "DEFAULT", **kwargs) -> None:
weights_enum = torchvision.models.get_model_weights(model_name)
self._weights = weights_enum[weights_name]
Expand All @@ -21,7 +23,7 @@ def __init__(self, model_name: str, weights_name: str = "DEFAULT", **kwargs) ->
def spec(self) -> cvataa.DetectionFunctionSpec:
return cvataa.DetectionFunctionSpec(
labels=[
cvataa.label_spec(cat, i)
cvataa.label_spec(cat, i, type=self._label_type)
for i, cat in enumerate(self._weights.meta["categories"])
if cat != "N/A"
]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@


class _TorchvisionDetectionFunction(TorchvisionFunction):
_label_type = "rectangle"

def detect(
self, context: cvataa.DetectionFunctionContext, image: PIL.Image.Image
) -> list[models.LabeledShapeRequest]:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ def _generate_shapes(


class _TorchvisionInstanceSegmentationFunction(TorchvisionFunction):
_label_type = "mask"

def detect(
self, context: cvataa.DetectionFunctionContext, image: PIL.Image.Image
) -> list[models.LabeledShapeRequest]:
Expand Down
92 changes: 52 additions & 40 deletions cvat-ui/src/components/job-item/job-actions-menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,60 +17,72 @@ interface Props {
job: Job;
}

export enum Actions {
TASK = 'task',
PROJECT = 'project',
BUG_TRACKER = 'bug_tracker',
IMPORT_JOB = 'import_job',
EXPORT_JOB = 'export_job',
VIEW_ANALYTICS = 'view_analytics',
DELETE = 'delete',
}

function JobActionsMenu(props: Props): JSX.Element {
const { job } = props;

const dispatch = useDispatch();
const history = useHistory();

const onDelete = useCallback(() => {
Modal.confirm({
title: `The job ${job.id} will be deleted`,
content: 'All related data (annotations) will be lost. Continue?',
className: 'cvat-modal-confirm-delete-job',
onOk: () => {
dispatch(deleteJobAsync(job));
},
okButtonProps: {
type: 'primary',
danger: true,
},
okText: 'Delete',
});
}, [job]);
const onClickMenu = useCallback(
(action: MenuInfo) => {
if (action.key === Actions.TASK) {
history.push(`/tasks/${job.taskId}`);
} else if (action.key === Actions.PROJECT) {
history.push(`/projects/${job.projectId}`);
} else if (action.key === Actions.BUG_TRACKER) {
if (job.bugTracker) {
window.open(job.bugTracker, '_blank', 'noopener noreferrer');
}
} else if (action.key === Actions.IMPORT_JOB) {
dispatch(importActions.openImportDatasetModal(job));
} else if (action.key === Actions.EXPORT_JOB) {
dispatch(exportActions.openExportDatasetModal(job));
} else if (action.key === Actions.VIEW_ANALYTICS) {
history.push(`/tasks/${job.taskId}/jobs/${job.id}/analytics`);
} else if (action.key === Actions.DELETE) {
Modal.confirm({
title: `The job ${job.id} will be deleted`,
content: 'All related data (annotations) will be lost. Continue?',
className: 'cvat-modal-confirm-delete-job',
onOk: () => {
dispatch(deleteJobAsync(job));
},
okButtonProps: {
type: 'primary',
danger: true,
},
okText: 'Delete',
});
}
},
[job],
);

return (
<Menu
className='cvat-job-item-menu'
onClick={(action: MenuInfo) => {
if (action.key === 'task') {
history.push(`/tasks/${job.taskId}`);
} else if (action.key === 'project') {
history.push(`/projects/${job.projectId}`);
} else if (action.key === 'bug_tracker') {
if (job.bugTracker) {
window.open(job.bugTracker, '_blank', 'noopener noreferrer');
}
} else if (action.key === 'import_job') {
dispatch(importActions.openImportDatasetModal(job));
} else if (action.key === 'export_job') {
dispatch(exportActions.openExportDatasetModal(job));
} else if (action.key === 'view_analytics') {
history.push(`/tasks/${job.taskId}/jobs/${job.id}/analytics`);
}
}}
onClick={onClickMenu}
>
<Menu.Item key='task' disabled={job.taskId === null}>Go to the task</Menu.Item>
<Menu.Item key='project' disabled={job.projectId === null}>Go to the project</Menu.Item>
<Menu.Item key='bug_tracker' disabled={!job.bugTracker}>Go to the bug tracker</Menu.Item>
<Menu.Item key='import_job'>Import annotations</Menu.Item>
<Menu.Item key='export_job'>Export annotations</Menu.Item>
<Menu.Item key='view_analytics'>View analytics</Menu.Item>
<Menu.Item key={Actions.TASK} disabled={job.taskId === null}>Go to the task</Menu.Item>
<Menu.Item key={Actions.PROJECT} disabled={job.projectId === null}>Go to the project</Menu.Item>
<Menu.Item key={Actions.BUG_TRACKER} disabled={!job.bugTracker}>Go to the bug tracker</Menu.Item>
<Menu.Item key={Actions.IMPORT_JOB}>Import annotations</Menu.Item>
<Menu.Item key={Actions.EXPORT_JOB}>Export annotations</Menu.Item>
<Menu.Item key={Actions.VIEW_ANALYTICS}>View analytics</Menu.Item>
<Menu.Divider />
<Menu.Item
key='delete'
key={Actions.DELETE}
disabled={job.type !== JobType.GROUND_TRUTH}
onClick={() => onDelete()}
>
Delete
</Menu.Item>
Expand Down
2 changes: 1 addition & 1 deletion cvat-ui/src/components/labels-editor/label-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -425,7 +425,7 @@ export default class LabelForm extends React.Component<Props> {
className: 'cvat-modal-delete-label-attribute',
icon: <ExclamationCircleOutlined />,
title: `Do you want to remove the "${attr.name}" attribute?`,
content: 'This action is undone. All annotations associated to the attribute will be removed',
content: 'This action cannot be undone. All annotations associated to the attribute will be removed',
type: 'warning',
okButtonProps: { type: 'primary', danger: true },
onOk: () => {
Expand Down
2 changes: 1 addition & 1 deletion cvat-ui/src/components/labels-editor/labels-editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ export default class LabelsEditor extends React.PureComponent<LabelsEditorProps,
className: 'cvat-modal-delete-label',
icon: <ExclamationCircleOutlined />,
title: `Do you want to delete "${label.name}" label?`,
content: 'This action is undone. All annotations associated to the label will be deleted.',
content: 'This action cannot be undone. All annotations associated to the label will be deleted.',
type: 'warning',
okButtonProps: { type: 'primary', danger: true },
onOk() {
Expand Down
27 changes: 26 additions & 1 deletion cvat-ui/src/components/model-runner-modal/detector-runner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import Button from 'antd/lib/button';
import Switch from 'antd/lib/switch';
import Tag from 'antd/lib/tag';
import notification from 'antd/lib/notification';
import { ArrowRightOutlined } from '@ant-design/icons';
import { ArrowRightOutlined, QuestionCircleOutlined } from '@ant-design/icons';

import CVATTooltip from 'components/common/cvat-tooltip';
import { clamp } from 'utils/math';
Expand Down Expand Up @@ -72,6 +72,7 @@ function DetectorRunner(props: Props): JSX.Element {
const [cleanup, setCleanup] = useState<boolean>(false);
const [mapping, setMapping] = useState<FullMapping>([]);
const [convertMasksToPolygons, setConvertMasksToPolygons] = useState<boolean>(false);
const [detectorThreshold, setDetectorThreshold] = useState<number | null>(null);
const [modelLabels, setModelLabels] = useState<LabelInterface[]>([]);
const [taskLabels, setTaskLabels] = useState<LabelInterface[]>([]);

Expand Down Expand Up @@ -179,6 +180,29 @@ function DetectorRunner(props: Props): JSX.Element {
<Text>Clean previous annotations</Text>
</div>
)}
{isDetector && (
<div className='cvat-detector-runner-threshold-wrapper'>
<Row align='middle' justify='start'>
<Col>
<InputNumber
min={0.01}
step={0.01}
max={1}
value={detectorThreshold}
onChange={(value: number | null) => {
setDetectorThreshold(value);
}}
/>
</Col>
<Col>
<Text>Threshold</Text>
<CVATTooltip title='Minimum confidence threshold for detections. Leave empty to use the default value specified in the model settings'>
<QuestionCircleOutlined className='cvat-info-circle-icon' />
</CVATTooltip>
</Col>
</Row>
</div>
)}
{isReId ? (
<div>
<Row align='middle' justify='start'>
Expand Down Expand Up @@ -236,6 +260,7 @@ function DetectorRunner(props: Props): JSX.Element {
mapping: serverMapping,
cleanup,
conv_mask_to_poly: convertMasksToPolygons,
...(detectorThreshold !== null ? { threshold: detectorThreshold } : {}),
});
} else if (model.kind === ModelKind.REID) {
runInference(model, { threshold, max_distance: distance });
Expand Down
11 changes: 9 additions & 2 deletions cvat-ui/src/components/model-runner-modal/styles.scss
Original file line number Diff line number Diff line change
Expand Up @@ -105,8 +105,15 @@
}

.cvat-detector-runner-clean-previous-annotations-wrapper,
.cvat-detector-runner-convert-masks-to-polygons-wrapper {
span {
.cvat-detector-runner-convert-masks-to-polygons-wrapper,
.cvat-detector-runner-threshold-wrapper {
.ant-typography {
margin-left: $grid-unit-size;
}
}

.cvat-detector-runner-threshold-wrapper {
.cvat-info-circle-icon {
margin-left: $grid-unit-size;
}
}
Loading

0 comments on commit 95ad74c

Please sign in to comment.