Skip to content

Commit

Permalink
Merge branch 'develop' into bs/fixed_default_attr_val
Browse files Browse the repository at this point in the history
  • Loading branch information
bsekachev authored Jul 19, 2023
2 parents 255f1a2 + 8de7722 commit 9ae8897
Show file tree
Hide file tree
Showing 25 changed files with 618 additions and 219 deletions.
4 changes: 2 additions & 2 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,10 @@
/components/ @azhavoro

# Component: Tests
/tests/ @yasakova-anastasia
/tests/ @kirill-sizov

# Component: Serverless functions
/serverless/ @yasakova-anastasia
/serverless/ @kirill-sizov

# Infrastructure
Dockerfile* @azhavoro
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/hadolint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ jobs:
- name: Run checks
env:
HADOLINT: "${{ github.workspace }}/hadolint"
HADOLINT_VER: "2.1.0"
HADOLINT_VER: "2.12.0"
VERIFICATION_LEVEL: "error"
run: |
CHANGED_FILES="${{steps.files.outputs.all_changed_files}}"
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/helm.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ on:
- 'master'
- 'develop'
pull_request:
types: [edited, ready_for_review, opened, synchronize, reopened]
types: [ready_for_review, opened, synchronize, reopened]
paths-ignore:
- 'site/**'
- '**/*.md'
Expand Down
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Multi-line text attributes supported (<https://github.com/opencv/cvat/pull/6458>)
- Now you can configure default attribute value for SELECT, RADIO types on UI
(<https://github.com/opencv/cvat/pull/6474>)
- \{SDK\] `cvat_sdk.datasets`, a framework-agnostic equivalent of `cvat_sdk.pytorch`
(<https://github.com/opencv/cvat/pull/6428>)

### Changed
- TDB
Expand All @@ -21,9 +23,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- TDB

### Fixed
- Calculating number of objects on annotation view when frames are deleted
(<https://github.com/opencv/cvat/pull/6493>)
- \[SDK\] Ability to create attributes with blank default values
(<https://github.com/opencv/cvat/pull/6454>)
- \[SDK\] SDK should not change input data in models (<https://github.com/opencv/cvat/pull/6455>)
- 3D job can not be opened in validation mode (<https://github.com/opencv/cvat/pull/6507>)
- Memory leak related to unclosed av container (<https://github.com/opencv/cvat/pull/6501>)
- Using initial frame from query parameter to open specific frame in a job
(<https://github.com/opencv/cvat/pull/6506>)

### Security
- TDB
Expand Down
2 changes: 1 addition & 1 deletion cvat-core/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "cvat-core",
"version": "9.2.0",
"version": "9.2.1",
"description": "Part of Computer Vision Tool which presents an interface for client-side integration",
"main": "src/api.ts",
"scripts": {
Expand Down
24 changes: 19 additions & 5 deletions cvat-core/src/annotations-collection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -659,18 +659,32 @@ export default class Collection {
fillBody(Object.values(this.labels).filter((label) => !label.hasParent));

const scanTrack = (track, prefix = ''): void => {
const countInterpolatedFrames = (start: number, stop: number, lastIsKeyframe: boolean): number => {
let count = stop - start;
if (lastIsKeyframe) {
count -= 1;
}
for (let i = start + 1; lastIsKeyframe ? i < stop : i <= stop; i++) {
if (this.frameMeta.deleted_frames[i]) {
count--;
}
}
return count;
};

const pref = prefix ? `${prefix}${sep}` : '';
const label = `${pref}${track.label.name}`;
labels[label][track.shapeType].track++;
const keyframes = Object.keys(track.shapes)
.sort((a, b) => +a - +b)
.map((el) => +el);
.map((el) => +el)
.filter((frame) => !this.frameMeta.deleted_frames[frame]);

let prevKeyframe = keyframes[0];
let visible = false;
for (const keyframe of keyframes) {
if (visible) {
const interpolated = keyframe - prevKeyframe - 1;
const interpolated = countInterpolatedFrames(prevKeyframe, keyframe, true);
labels[label].interpolated += interpolated;
labels[label].total += interpolated;
}
Expand All @@ -692,7 +706,7 @@ export default class Collection {
}

if (lastKey !== this.stopFrame && !track.get(lastKey).outside) {
const interpolated = this.stopFrame - lastKey;
const interpolated = countInterpolatedFrames(lastKey, this.stopFrame, false);
labels[label].interpolated += interpolated;
labels[label].total += interpolated;
}
Expand All @@ -719,13 +733,13 @@ export default class Collection {
}

const { name: label } = object.label;
if (objectType === 'tag') {
if (objectType === 'tag' && !this.frameMeta.deleted_frames[object.frame]) {
labels[label].tag++;
labels[label].manually++;
labels[label].total++;
} else if (objectType === 'track') {
scanTrack(object);
} else {
} else if (!this.frameMeta.deleted_frames[object.frame]) {
const { shapeType } = object as Shape;
labels[label][shapeType].shape++;
labels[label].manually++;
Expand Down
22 changes: 22 additions & 0 deletions cvat-core/tests/api/annotations.js
Original file line number Diff line number Diff line change
Expand Up @@ -872,7 +872,29 @@ describe('Feature: get statistics', () => {
expect(statistics.label[labelName].manually).toBe(2);
expect(statistics.label[labelName].interpolated).toBe(3);
expect(statistics.label[labelName].total).toBe(5);
});

test('get statistics from a job with skeletons', async () => {
const job = (await window.cvat.jobs.get({ jobID: 102 }))[0];
await job.annotations.clear(true);
let statistics = await job.annotations.statistics();
expect(statistics.total.manually).toBe(5);
expect(statistics.total.interpolated).toBe(443);
expect(statistics.total.tag).toBe(1);
expect(statistics.total.rectangle.shape).toBe(1);
expect(statistics.total.rectangle.track).toBe(1);
await job.frames.delete(500); // track frame
await job.frames.delete(510); // rectangle shape frame
await job.frames.delete(550); // the first keyframe of a track
statistics = await job.annotations.statistics();
expect(statistics.total.manually).toBe(2);
expect(statistics.total.tag).toBe(0);
expect(statistics.total.rectangle.shape).toBe(0);
expect(statistics.total.interpolated).toBe(394);
await job.frames.delete(650); // intermediate frame in a track
statistics = await job.annotations.statistics();
expect(statistics.total.interpolated).toBe(393);
await job.close();
});
});

Expand Down
7 changes: 7 additions & 0 deletions cvat-sdk/cvat_sdk/datasets/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Copyright (C) 2023 CVAT.ai Corporation
#
# SPDX-License-Identifier: MIT

from .caching import UpdatePolicy
from .common import FrameAnnotations, MediaElement, Sample, UnsupportedDatasetError
from .task_dataset import TaskDataset
File renamed without changes.
57 changes: 57 additions & 0 deletions cvat-sdk/cvat_sdk/datasets/common.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# Copyright (C) 2022-2023 CVAT.ai Corporation
#
# SPDX-License-Identifier: MIT

import abc
from typing import List

import attrs
import attrs.validators
import PIL.Image

import cvat_sdk.core
import cvat_sdk.core.exceptions
import cvat_sdk.models as models


class UnsupportedDatasetError(cvat_sdk.core.exceptions.CvatSdkException):
pass


@attrs.frozen
class FrameAnnotations:
"""
Contains annotations that pertain to a single frame.
"""

tags: List[models.LabeledImage] = attrs.Factory(list)
shapes: List[models.LabeledShape] = attrs.Factory(list)


class MediaElement(metaclass=abc.ABCMeta):
"""
The media part of a dataset sample.
"""

@abc.abstractmethod
def load_image(self) -> PIL.Image.Image:
"""
Loads the media data and returns it as a PIL Image object.
"""
...


@attrs.frozen
class Sample:
"""
Represents an element of a dataset.
"""

frame_index: int
"""Index of the corresponding frame in its task."""

annotations: FrameAnnotations
"""Annotations belonging to the frame."""

media: MediaElement
"""Media data of the frame."""
164 changes: 164 additions & 0 deletions cvat-sdk/cvat_sdk/datasets/task_dataset.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
# Copyright (C) 2022-2023 CVAT.ai Corporation
#
# SPDX-License-Identifier: MIT

from __future__ import annotations

import zipfile
from concurrent.futures import ThreadPoolExecutor
from typing import Sequence

import PIL.Image

import cvat_sdk.core
import cvat_sdk.core.exceptions
import cvat_sdk.models as models
from cvat_sdk.datasets.caching import UpdatePolicy, make_cache_manager
from cvat_sdk.datasets.common import FrameAnnotations, MediaElement, Sample, UnsupportedDatasetError

_NUM_DOWNLOAD_THREADS = 4


class TaskDataset:
"""
Represents a task on a CVAT server as a collection of samples.
Each sample corresponds to one frame in the task, and provides access to
the corresponding annotations and media data. Deleted frames are omitted.
This class caches all data and annotations for the task on the local file system
during construction.
Limitations:
* Only tasks with image (not video) data are supported at the moment.
* Track annotations are currently not accessible.
"""

class _TaskMediaElement(MediaElement):
def __init__(self, dataset: TaskDataset, frame_index: int) -> None:
self._dataset = dataset
self._frame_index = frame_index

def load_image(self) -> PIL.Image.Image:
return self._dataset._load_frame_image(self._frame_index)

def __init__(
self,
client: cvat_sdk.core.Client,
task_id: int,
*,
update_policy: UpdatePolicy = UpdatePolicy.IF_MISSING_OR_STALE,
) -> None:
"""
Creates a dataset corresponding to the task with ID `task_id` on the
server that `client` is connected to.
`update_policy` determines when and if the local cache will be updated.
"""

self._logger = client.logger

cache_manager = make_cache_manager(client, update_policy)
self._task = cache_manager.retrieve_task(task_id)

if not self._task.size or not self._task.data_chunk_size:
raise UnsupportedDatasetError("The task has no data")

if self._task.data_original_chunk_type != "imageset":
raise UnsupportedDatasetError(
f"{self.__class__.__name__} only supports tasks with image chunks;"
f" current chunk type is {self._task.data_original_chunk_type!r}"
)

self._logger.info("Fetching labels...")
self._labels = tuple(self._task.get_labels())

data_meta = cache_manager.ensure_task_model(
self._task.id,
"data_meta.json",
models.DataMetaRead,
self._task.get_meta,
"data metadata",
)

active_frame_indexes = set(range(self._task.size)) - set(data_meta.deleted_frames)

self._logger.info("Downloading chunks...")

self._chunk_dir = cache_manager.chunk_dir(task_id)
self._chunk_dir.mkdir(exist_ok=True, parents=True)

needed_chunks = {index // self._task.data_chunk_size for index in active_frame_indexes}

with ThreadPoolExecutor(_NUM_DOWNLOAD_THREADS) as pool:

def ensure_chunk(chunk_index):
cache_manager.ensure_chunk(self._task, chunk_index)

for _ in pool.map(ensure_chunk, sorted(needed_chunks)):
# just need to loop through all results so that any exceptions are propagated
pass

self._logger.info("All chunks downloaded")

annotations = cache_manager.ensure_task_model(
self._task.id,
"annotations.json",
models.LabeledData,
self._task.get_annotations,
"annotations",
)

self._frame_annotations = {
frame_index: FrameAnnotations() for frame_index in sorted(active_frame_indexes)
}

for tag in annotations.tags:
# Some annotations may belong to deleted frames; skip those.
if tag.frame in self._frame_annotations:
self._frame_annotations[tag.frame].tags.append(tag)

for shape in annotations.shapes:
if shape.frame in self._frame_annotations:
self._frame_annotations[shape.frame].shapes.append(shape)

# TODO: tracks?

self._samples = [
Sample(frame_index=k, annotations=v, media=self._TaskMediaElement(self, k))
for k, v in self._frame_annotations.items()
]

@property
def labels(self) -> Sequence[models.ILabel]:
"""
Returns the labels configured in the task.
Clients must not modify the object returned by this property or its components.
"""
return self._labels

@property
def samples(self) -> Sequence[Sample]:
"""
Returns a sequence of all samples, in order of their frame indices.
Note that the frame indices may not be contiguous, as deleted frames will not be included.
Clients must not modify the object returned by this property or its components.
"""
return self._samples

def _load_frame_image(self, frame_index: int) -> PIL.Image:
assert frame_index in self._frame_annotations

chunk_index = frame_index // self._task.data_chunk_size
member_index = frame_index % self._task.data_chunk_size

with zipfile.ZipFile(self._chunk_dir / f"{chunk_index}.zip", "r") as chunk_zip:
with chunk_zip.open(chunk_zip.infolist()[member_index]) as chunk_member:
image = PIL.Image.open(chunk_member)
image.load()

return image
8 changes: 6 additions & 2 deletions cvat-sdk/cvat_sdk/pytorch/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,12 @@
#
# SPDX-License-Identifier: MIT

from .caching import UpdatePolicy
from .common import FrameAnnotations, Target, UnsupportedDatasetError
from .common import Target
from .project_dataset import ProjectVisionDataset
from .task_dataset import TaskVisionDataset
from .transforms import ExtractBoundingBoxes, ExtractSingleLabelIndex, LabeledBoxes

# isort: split
# Compatibility imports
from ..datasets.caching import UpdatePolicy
from ..datasets.common import FrameAnnotations, UnsupportedDatasetError
Loading

0 comments on commit 9ae8897

Please sign in to comment.