Merge branch 'develop' of https://github.com/opencv/cvat into upstream/develop

main
Maya 6 years ago
commit 0fe2e1875c

@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- [Datumaro] Dataset statistics (<https://github.com/opencv/cvat/pull/1668>)
- Ability to change label color in tasks and predefined labels (<https://github.com/opencv/cvat/pull/2014>)
- [Datumaro] Multi-dataset merge (https://github.com/opencv/cvat/pull/1695)
- Ability to configure email verification for new users (<https://github.com/opencv/cvat/pull/1929>)
- Link to django admin page from UI (<https://github.com/opencv/cvat/pull/2068>)
- Notification message when users use wrong browser (<https://github.com/opencv/cvat/pull/2070>)
@ -27,6 +28,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed
- Issue loading openvino models for semi-automatic and automatic annotation (<https://github.com/opencv/cvat/pull/1996>)
- Basic functions of CVAT works without activated nuclio dashboard
- Fixed a case in which exported masks could have wrong color order (<https://github.com/opencv/cvat/issues/2032>)
- Fixed error with creating task with labels with the same name (<https://github.com/opencv/cvat/pull/2031>)
- Django RQ dashboard view (<https://github.com/opencv/cvat/pull/2069>)

@ -21,6 +21,8 @@ COPY cvat-canvas/package*.json /tmp/cvat-canvas/
COPY cvat-ui/package*.json /tmp/cvat-ui/
COPY cvat-data/package*.json /tmp/cvat-data/
RUN npm config set loglevel info
# Install cvat-data dependencies
WORKDIR /tmp/cvat-data/
RUN npm install

@ -32,10 +32,11 @@ annotation team. Try it online [cvat.org](https://cvat.org).
- [Annotation mode](https://youtu.be/vH_639N67HI)
- [Interpolation of bounding boxes](https://youtu.be/Hc3oudNuDsY)
- [Interpolation of polygons](https://youtu.be/K4nis9lk92s)
- [Tag_annotation_video](https://youtu.be/62bI4mF-Xfk)
- [Attribute mode](https://youtu.be/iIkJsOkDzVA)
- [Segmentation mode](https://youtu.be/Fh8oKuSUIPs)
- [Tutorial for polygons](https://www.youtube.com/watch?v=XTwfXDh4clI)
- [Semi-automatic segmentation](https://www.youtube.com/watch?v=vnqXZ-Z-VTQ)
- [Segmentation mode](https://youtu.be/9Fe_GzMLo3E)
- [Tutorial for polygons](https://youtu.be/C7-r9lZbjBw)
- [Semi-automatic segmentation](https://youtu.be/9HszWP_qsRQ)
## Supported annotation formats

@ -81,8 +81,10 @@
cvat.server.register.implementation = async (username, firstName, lastName,
email, password1, password2, userConfirmations) => {
await serverProxy.server.register(username, firstName, lastName, email,
password1, password2, userConfirmations);
const user = await serverProxy.server.register(username, firstName,
lastName, email, password1, password2, userConfirmations);
return new User(user);
};
cvat.server.login.implementation = async (username, password) => {

@ -148,6 +148,7 @@ function build() {
* @param {string} password1 A password for the new account
* @param {string} password2 The confirmation password for the new account
* @param {Object} userConfirmations An user confirmations of terms of use if needed
* @returns {Object} response data
* @throws {module:API.cvat.exceptions.PluginError}
* @throws {module:API.cvat.exceptions.ServerError}
*/

@ -23,6 +23,7 @@
is_staff: null,
is_superuser: null,
is_active: null,
email_verification_required: null,
};
for (const property in data) {
@ -143,6 +144,16 @@
*/
get: () => data.is_active,
},
isVerified: {
/**
* @name isVerified
* @type {boolean}
* @memberof module:API.cvat.classes.User
* @readonly
* @instance
*/
get: () => !data.email_verification_required,
},
}));
}
}

@ -1,6 +1,6 @@
{
"name": "cvat-ui",
"version": "1.8.1",
"version": "1.8.4",
"lockfileVersion": 1,
"requires": true,
"dependencies": {

@ -1,6 +1,6 @@
{
"name": "cvat-ui",
"version": "1.8.1",
"version": "1.8.4",
"description": "CVAT single-page application",
"main": "src/index.tsx",
"scripts": {

@ -75,11 +75,10 @@ export const registerAsync = (
dispatch(authActions.register());
try {
await cvat.server.register(username, firstName, lastName, email, password1, password2,
const user = await cvat.server.register(username, firstName, lastName, email, password1, password2,
confirmations);
const users = await cvat.users.get({ self: true });
dispatch(authActions.registerSuccess(users[0]));
dispatch(authActions.registerSuccess(user));
} catch (error) {
dispatch(authActions.registerFailed(error));
}

@ -94,7 +94,7 @@ class AdvancedConfigurationForm extends React.PureComponent<Props> {
delete filteredValues.frameStep;
if (values.overlapSize && +values.segmentSize <= +values.overlapSize) {
reject(new Error('Overlap size must be more than segment size'));
reject(new Error('Segment size must be more than overlap size'));
}
if (typeof (values.startFrame) !== 'undefined' && typeof (values.stopFrame) !== 'undefined'

@ -129,7 +129,7 @@ class CVATApplication extends React.PureComponent<CVATAppProps & RouteComponentP
return;
}
if (user == null) {
if (user == null || !user.isVerified) {
return;
}
@ -249,7 +249,7 @@ class CVATApplication extends React.PureComponent<CVATAppProps & RouteComponentP
keyMap,
} = this.props;
const readyForRender = (userInitialized && user == null)
const readyForRender = (userInitialized && (user == null || !user.isVerified))
|| (userInitialized && formatsInitialized
&& pluginsInitialized && usersInitialized && aboutInitialized);
@ -302,7 +302,7 @@ class CVATApplication extends React.PureComponent<CVATAppProps & RouteComponentP
if (readyForRender) {
if (user) {
if (user && user.isVerified) {
return (
<GlobalErrorBoundary>
<Layout>

@ -44,6 +44,11 @@ function validateParsedAttribute(attr: Attribute): void {
+ `Attribute values must be an array. Got type ${typeof (attr.values)}`);
}
if (!attr.values.length) {
throw new Error(`Attribute: "${attr.name}". Attribute values array mustn't be empty`);
}
for (const value of attr.values) {
if (typeof (value) !== 'string') {
throw new Error(`Attribute: "${attr.name}". `
@ -62,6 +67,11 @@ export function validateParsedLabel(label: Label): void {
+ `Type of label id must be only a number or undefined. Got value ${label.id}`);
}
if (typeof (label.color) !== 'string') {
throw new Error(`Label "${label.name}". `
+ `Label color must be a string. Got ${typeof (label.color)}`);
}
if (!label.color.match(/^#[0-9a-f]{6}$|^$/)) {
throw new Error(`Label "${label.name}". `
+ `Type of label color must be only a valid color string. Got value ${label.color}`);

@ -144,20 +144,17 @@ ReactDOM.render(
document.getElementById('root'),
);
window.onerror = (
message: Event | string,
source?: string,
lineno?: number,
colno?: number,
error?: Error,
) => {
if (typeof (message) === 'string' && source && typeof (lineno) === 'number' && (typeof (colno) === 'number') && error) {
window.addEventListener('error', (errorEvent: ErrorEvent) => {
if (errorEvent.filename
&& typeof (errorEvent.lineno) === 'number'
&& typeof (errorEvent.colno) === 'number'
&& errorEvent.error) {
const logPayload = {
filename: source,
line: lineno,
message: error.message,
column: colno,
stack: error.stack,
filename: errorEvent.filename,
line: errorEvent.lineno,
message: errorEvent.error.message,
column: errorEvent.colno,
stack: errorEvent.error.stack,
};
const store = getCVATStore();
@ -171,4 +168,4 @@ window.onerror = (
logger.log(LogType.sendException, logPayload);
}
}
};
});

@ -252,6 +252,7 @@ export interface NotificationsState {
};
auth: {
changePasswordDone: string;
registerDone: string;
};
};
}

@ -97,6 +97,7 @@ const defaultState: NotificationsState = {
},
auth: {
changePasswordDone: '',
registerDone: '',
},
},
};
@ -163,6 +164,25 @@ export default function (state = defaultState, action: AnyAction): Notifications
},
};
}
case AuthActionTypes.REGISTER_SUCCESS: {
if (!action.payload.user.isVerified) {
return {
...state,
messages: {
...state.messages,
auth: {
...state.messages.auth,
registerDone: `To use your account, you need to confirm the email address. \
We have sent an email with a confirmation link to ${action.payload.user.email}.`,
},
},
};
}
return {
...state,
};
}
case AuthActionTypes.CHANGE_PASSWORD_SUCCESS: {
return {
...state,

@ -1,8 +1,12 @@
# Copyright (C) 2018 Intel Corporation
# Copyright (C) 2018-2020 Intel Corporation
#
# SPDX-License-Identifier: MIT
from . import AUTH_ROLE
from django.conf import settings
from allauth.account import app_settings as allauth_settings
from allauth.account.models import EmailAddress
from . import AUTH_ROLE
def create_user(sender, instance, created, **kwargs):
from django.contrib.auth.models import Group
@ -10,6 +14,11 @@ def create_user(sender, instance, created, **kwargs):
if instance.is_superuser and instance.is_staff:
db_group = Group.objects.get(name=AUTH_ROLE.ADMIN)
instance.groups.add(db_group)
# create and verify EmailAdress for superuser accounts
if allauth_settings.EMAIL_REQUIRED:
EmailAddress.objects.get_or_create(user=instance, email=instance.email, primary=True, verified=True)
for group_name in settings.DJANGO_AUTH_DEFAULT_GROUPS:
db_group = Group.objects.get(name=getattr(AUTH_ROLE, group_name))
instance.groups.add(db_group)

@ -0,0 +1,8 @@
{% load account %}{% user_display user as user_display %}{% load i18n %}{% autoescape off %}{% blocktrans with site_name=current_site.name site_domain=current_site.domain %}Hello from {{ site_name }}!
<p>You're receiving this e-mail because user <strong>{{ user_display }}</strong> has given yours as an e-mail address to connect their account.</p>
<p>To confirm this is correct, go to <a href="{{ activate_url }}">{{ activate_url }}</a></p>
{% endblocktrans %}
{% blocktrans with site_name=current_site.name site_domain=current_site.domain %}<strong>{{ site_domain }}</strong>{% endblocktrans %}
{% endautoescape %}

@ -2,13 +2,15 @@
#
# SPDX-License-Identifier: MIT
from django.urls import path
from django.urls import path, re_path
from django.conf import settings
from rest_auth.views import (
LoginView, LogoutView, PasswordChangeView,
PasswordResetView, PasswordResetConfirmView)
from rest_auth.registration.views import RegisterView
from .views import SigningView
from allauth.account.views import ConfirmEmailView, EmailVerificationSentView
from allauth.account import app_settings as allauth_settings
from cvat.apps.authentication.views import SigningView, RegisterView
urlpatterns = [
path('login', LoginView.as_view(), name='rest_login'),
@ -26,3 +28,11 @@ if settings.DJANGO_AUTH_TYPE == 'BASIC':
path('password/change', PasswordChangeView.as_view(),
name='rest_password_change'),
]
if allauth_settings.EMAIL_VERIFICATION != \
allauth_settings.EmailVerificationMethod.NONE:
urlpatterns += [
re_path(r'^account-confirm-email/(?P<key>[-:\w]+)/$', ConfirmEmailView.as_view(),
name='account_confirm_email'),
path('register/account-email-verification-sent', EmailVerificationSentView.as_view(),
name='account_email_verification_sent'),
]

@ -5,6 +5,8 @@
from rest_framework import views
from rest_framework.exceptions import ValidationError
from rest_framework.response import Response
from rest_auth.registration.views import RegisterView as _RegisterView
from allauth.account import app_settings as allauth_settings
from furl import furl
from . import signature
@ -43,3 +45,12 @@ class SigningView(views.APIView):
url = furl(url).add({signature.QUERY_PARAM: sign}).url
return Response(url)
class RegisterView(_RegisterView):
def get_response_data(self, user):
data = self.get_serializer(user).data
data['email_verification_required'] = allauth_settings.EMAIL_VERIFICATION == \
allauth_settings.EmailVerificationMethod.MANDATORY
return data

@ -9,6 +9,7 @@
- [Stop all containers](#stop-all-containers)
- [Advanced settings](#advanced-settings)
- [Share path](#share-path)
- [Email verification](#email-verification)
- [Serving over HTTPS](#serving-over-https)
- [Prerequisites](#prerequisites)
- [Roadmap](#roadmap)
@ -362,6 +363,26 @@ You can change the share device path to your actual share. For user convenience
we have defined the environment variable $CVAT_SHARE_URL. This variable
contains a text (url for example) which is shown in the client-share browser.
### Email verification
You can enable email verification for newly registered users.
Specify these options in the [settings file](../../settings/base.py) to configure Django allauth
to enable email verification (ACCOUNT_EMAIL_VERIFICATION = 'mandatory').
Access is denied until the user's email address is verified.
```python
ACCOUNT_AUTHENTICATION_METHOD = 'username'
ACCOUNT_CONFIRM_EMAIL_ON_GET = True
ACCOUNT_EMAIL_REQUIRED = True
ACCOUNT_EMAIL_VERIFICATION = 'mandatory'
# Email backend settings for Django
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
```
Also you need to configure the Django email backend to send emails.
This depends on the email server you are using and is not covered in this tutorial, please see
[Django SMTP backend configuration](https://docs.djangoproject.com/en/3.1/topics/email/#django.core.mail.backends.smtp.EmailBackend)
for details.
### Serving over HTTPS
We will add [letsencrypt.org](https://letsencrypt.org/) issued certificate to secure

@ -33,7 +33,7 @@ djangorestframework==3.11.1
Pygments==2.6.1
drf-yasg==1.17.1
Shapely==1.7.1
pdf2image==1.13.1
pdf2image==1.14.0
pascal_voc_writer==0.1.4
django-rest-auth[with_social]==0.9.5
cython==0.29.21

@ -207,15 +207,17 @@ DJANGO_AUTH_TYPE = 'BASIC'
DJANGO_AUTH_DEFAULT_GROUPS = []
LOGIN_URL = 'rest_login'
LOGIN_REDIRECT_URL = '/'
AUTH_LOGIN_NOTE = '<p>Have not registered yet? <a href="/auth/register">Register here</a>.</p>'
AUTHENTICATION_BACKENDS = [
'rules.permissions.ObjectPermissionBackend',
'django.contrib.auth.backends.ModelBackend'
'django.contrib.auth.backends.ModelBackend',
'allauth.account.auth_backends.AuthenticationBackend',
]
# https://github.com/pennersr/django-allauth
ACCOUNT_EMAIL_VERIFICATION = 'none'
# set UI url to redirect after a successful e-mail confirmation
ACCOUNT_EMAIL_CONFIRMATION_ANONYMOUS_REDIRECT_URL = '/auth/login'
OLD_PASSWORD_FIELD_ENABLED = True
# Django-RQ

@ -37,6 +37,8 @@ UI_URL = '{}://{}'.format(UI_SCHEME, UI_HOST)
if UI_PORT and UI_PORT != '80':
UI_URL += ':{}'.format(UI_PORT)
# set UI url to redirect to after successful e-mail confirmation
ACCOUNT_EMAIL_CONFIRMATION_ANONYMOUS_REDIRECT_URL = '{}/auth/login'.format(UI_URL)
CORS_ORIGIN_WHITELIST = [UI_URL]
CORS_REPLACE_HTTPS_REFERER = True

@ -27,7 +27,6 @@ urlpatterns = [
path('admin/', admin.site.urls),
path('', include('cvat.apps.engine.urls')),
path('django-rq/', include('django_rq.urls')),
path('auth/', include('cvat.apps.authentication.urls')),
path('documentation/', include('cvat.apps.documentation.urls')),
]

@ -47,7 +47,7 @@ def _write_xml_bbox(bbox, parent_elem):
return bbox_elem
LabelmapType = Enum('LabelmapType', ['voc', 'source', 'guess'])
LabelmapType = Enum('LabelmapType', ['voc', 'source'])
class VocConverter(Converter):
DEFAULT_IMAGE_EXT = VocPath.IMAGE_EXT
@ -102,6 +102,8 @@ class VocConverter(Converter):
self._apply_colormap = apply_colormap
self._allow_attributes = allow_attributes
if label_map is None:
label_map = LabelmapType.source
self._load_categories(label_map)
def apply(self):
@ -446,7 +448,7 @@ class VocConverter(Converter):
path = osp.join(self._save_dir, VocPath.LABELMAP_FILE)
write_label_map(path, self._label_map)
def _load_categories(self, label_map_source=None):
def _load_categories(self, label_map_source):
if label_map_source == LabelmapType.voc.name:
# use the default VOC colormap
label_map = make_voc_label_map()
@ -456,10 +458,8 @@ class VocConverter(Converter):
# generate colormap for input labels
labels = self._extractor.categories() \
.get(AnnotationType.label, LabelCategories())
label_map = OrderedDict()
label_map['background'] = [None, [], []]
for item in labels.items:
label_map[item.name] = [None, [], []]
label_map = OrderedDict((item.name, [None, [], []])
for item in labels.items)
elif label_map_source == LabelmapType.source.name and \
AnnotationType.mask in self._extractor.categories():
@ -467,60 +467,45 @@ class VocConverter(Converter):
labels = self._extractor.categories()[AnnotationType.label]
colors = self._extractor.categories()[AnnotationType.mask]
label_map = OrderedDict()
has_black = False
for idx, item in enumerate(labels.items):
color = colors.colormap.get(idx)
if idx is not None:
if color == (0, 0, 0):
has_black = True
if color is not None:
label_map[item.name] = [color, [], []]
if not has_black and 'background' not in label_map:
label_map['background'] = [(0, 0, 0), [], []]
label_map.move_to_end('background', last=False)
elif label_map_source in [LabelmapType.guess.name, None]:
# generate colormap for union of VOC and input dataset labels
label_map = make_voc_label_map()
rebuild_colormap = False
source_labels = self._extractor.categories() \
.get(AnnotationType.label, LabelCategories())
for label in source_labels.items:
if label.name not in label_map:
rebuild_colormap = True
if label.attributes or label.name not in label_map:
label_map[label.name] = [None, [], label.attributes]
if rebuild_colormap:
for item in label_map.values():
item[0] = None
elif isinstance(label_map_source, dict):
label_map = label_map_source
label_map = OrderedDict(
sorted(label_map_source.items(), key=lambda e: e[0]))
elif isinstance(label_map_source, str) and osp.isfile(label_map_source):
label_map = parse_label_map(label_map_source)
has_black = find(label_map.items(),
lambda e: e[0] == 'background' or e[1][0] == (0, 0, 0))
if not has_black and 'background' not in label_map:
label_map['background'] = [(0, 0, 0), [], []]
label_map.move_to_end('background', last=False)
else:
raise Exception("Wrong labelmap specified, "
"expected one of %s or a file path" % \
', '.join(t.name for t in LabelmapType))
# There must always be a label with color (0, 0, 0) at index 0
bg_label = find(label_map.items(), lambda x: x[1][0] == (0, 0, 0))
if bg_label is not None:
bg_label = bg_label[0]
else:
bg_label = 'background'
if bg_label not in label_map:
has_colors = any(v[0] is not None for v in label_map.values())
color = (0, 0, 0) if has_colors else None
label_map[bg_label] = [color, [], []]
label_map.move_to_end(bg_label, last=False)
self._categories = make_voc_categories(label_map)
self._label_map = label_map
# Update colors with assigned values
colormap = self._categories[AnnotationType.mask].colormap
for label_id, color in colormap.items():
label_desc = label_map[
self._categories[AnnotationType.label].items[label_id].name]
label_desc[0] = color
self._label_map = label_map
self._label_id_mapping = self._make_label_id_map()
def _is_label(self, s):

@ -137,6 +137,9 @@ def parse_label_map(path):
label_desc = line.strip().split(':')
name = label_desc[0]
if name in label_map:
raise ValueError("Label '%s' is already defined" % name)
if 1 < len(label_desc) and len(label_desc[1]) != 0:
color = label_desc[1].split(',')
assert len(color) == 3, \
@ -173,7 +176,6 @@ def write_label_map(path, label_map):
f.write('%s\n' % ':'.join([label_name, color_rgb, parts, actions]))
# pylint: disable=pointless-statement
def make_voc_categories(label_map=None):
if label_map is None:
label_map = make_voc_label_map()
@ -190,16 +192,15 @@ def make_voc_categories(label_map=None):
label_categories.add(part)
categories[AnnotationType.label] = label_categories
has_colors = sum(v[0] is not None for v in label_map.values())
if not has_colors:
has_colors = any(v[0] is not None for v in label_map.values())
if not has_colors: # generate new colors
colormap = generate_colormap(len(label_map))
else:
else: # only copy defined colors
label_id = lambda label: label_categories.find(label)[0]
colormap = { label_id(name): desc[0]
for name, desc in label_map.items() }
for name, desc in label_map.items() if desc[0] is not None }
mask_categories = MaskCategories(colormap)
mask_categories.inverse_colormap # force init
mask_categories.inverse_colormap # pylint: disable=pointless-statement
categories[AnnotationType.mask] = mask_categories
return categories
# pylint: enable=pointless-statement

@ -472,53 +472,6 @@ class VocConverterTest(TestCase):
partial(VocConverter.convert, label_map='voc'),
test_dir, target_dataset=DstExtractor())
def test_dataset_with_guessed_labelmap(self):
class SrcExtractor(TestExtractorBase):
def __iter__(self):
yield DatasetItem(id=1, annotations=[
Bbox(2, 3, 4, 5, label=0, id=1),
Bbox(1, 2, 3, 4, label=1, id=2),
])
def categories(self):
label_cat = LabelCategories()
label_cat.add(VOC.VocLabel(1).name)
label_cat.add('non_voc_label')
return {
AnnotationType.label: label_cat,
}
class DstExtractor(TestExtractorBase):
def __iter__(self):
yield DatasetItem(id=1, annotations=[
Bbox(2, 3, 4, 5, label=self._label(VOC.VocLabel(1).name),
id=1, group=1, attributes={
'truncated': False,
'difficult': False,
'occluded': False,
}
),
Bbox(1, 2, 3, 4, label=self._label('non_voc_label'),
id=2, group=2, attributes={
'truncated': False,
'difficult': False,
'occluded': False,
}
),
])
def categories(self):
label_map = VOC.make_voc_label_map()
label_map['non_voc_label'] = [None, [], []]
for label_desc in label_map.values():
label_desc[0] = None # rebuild colormap
return VOC.make_voc_categories(label_map)
with TestDir() as test_dir:
self._test_save_and_load(SrcExtractor(),
partial(VocConverter.convert, label_map='guess'),
test_dir, target_dataset=DstExtractor())
def test_dataset_with_source_labelmap_undefined(self):
class SrcExtractor(TestExtractorBase):
def __iter__(self):
@ -602,8 +555,8 @@ class VocConverterTest(TestCase):
def categories(self):
label_map = OrderedDict()
label_map['label_1'] = [(1, 2, 3), [], []]
label_map['background'] = [(0, 0, 0), [], []]
label_map['label_1'] = [(1, 2, 3), [], []]
label_map['label_2'] = [(3, 2, 1), [], []]
return VOC.make_voc_categories(label_map)
@ -616,11 +569,11 @@ class VocConverterTest(TestCase):
class SrcExtractor(TestExtractorBase):
def __iter__(self):
yield DatasetItem(id=1, annotations=[
Bbox(2, 3, 4, 5, label=0, id=1),
Bbox(1, 2, 3, 4, label=1, id=2, group=2,
Bbox(2, 3, 4, 5, label=self._label('foreign_label'), id=1),
Bbox(1, 2, 3, 4, label=self._label('label'), id=2, group=2,
attributes={'act1': True}),
Bbox(2, 3, 4, 5, label=2, id=3, group=2),
Bbox(2, 3, 4, 6, label=3, id=4, group=2),
Bbox(2, 3, 4, 5, label=self._label('label_part1'), group=2),
Bbox(2, 3, 4, 6, label=self._label('label_part2'), group=2),
])
def categories(self):
@ -633,14 +586,19 @@ class VocConverterTest(TestCase):
AnnotationType.label: label_cat,
}
label_map = {
'label': [None, ['label_part1', 'label_part2'], ['act1', 'act2']]
}
label_map = OrderedDict([
('label', [None, ['label_part1', 'label_part2'], ['act1', 'act2']])
])
dst_label_map = OrderedDict([
('background', [None, [], []]),
('label', [None, ['label_part1', 'label_part2'], ['act1', 'act2']])
])
class DstExtractor(TestExtractorBase):
def __iter__(self):
yield DatasetItem(id=1, annotations=[
Bbox(1, 2, 3, 4, label=0, id=1, group=1,
Bbox(1, 2, 3, 4, label=self._label('label'), id=1, group=1,
attributes={
'act1': True,
'act2': False,
@ -649,12 +607,12 @@ class VocConverterTest(TestCase):
'occluded': False,
}
),
Bbox(2, 3, 4, 5, label=1, group=1),
Bbox(2, 3, 4, 6, label=2, group=1),
Bbox(2, 3, 4, 5, label=self._label('label_part1'), group=1),
Bbox(2, 3, 4, 6, label=self._label('label_part2'), group=1),
])
def categories(self):
return VOC.make_voc_categories(label_map)
return VOC.make_voc_categories(dst_label_map)
with TestDir() as test_dir:
self._test_save_and_load(SrcExtractor(),

@ -0,0 +1,66 @@
/*
* Copyright (C) 2020 Intel Corporation
*
* SPDX-License-Identifier: MIT
*/
/// <reference types="cypress" />
context('Hidden objects mustn\'t consider when we want to group visible objects only and use an grouping area for it.', () => {
const issueId = '1841'
const labelName = `Issue ${issueId}`
const taskName = `New annotation task for ${labelName}`
const attrName = `Attr for ${labelName}`
const textDefaultValue = 'Some default value for type Text'
const image = `image_${issueId}.png`
const width = 800
const height = 800
const posX = 10
const posY = 10
const color = 'white'
let bgcolor = ''
before(() => {
cy.visit('auth/login')
cy.login()
cy.imageGenerator('cypress/fixtures', image, width, height, color, posX, posY, labelName)
cy.createAnnotationTask(taskName, labelName, attrName, textDefaultValue, image)
cy.openTaskJob(taskName)
})
describe(`Testing issue "${issueId}"`, () => {
it('Change appearance to "Group"', () => {
cy.changeAppearance('Group')
})
it('Create three points as different objects', () => {
cy.createPoint(300, 410)
cy.get('#cvat-objects-sidebar-state-item-1')
.should('contain', '1').and('contain', 'POINTS SHAPE')
cy.createPoint(350, 410)
cy.get('#cvat-objects-sidebar-state-item-2')
.should('contain', '2').and('contain', 'POINTS SHAPE')
cy.createPoint(400, 410)
cy.get('#cvat-objects-sidebar-state-item-3')
.should('contain', '3').and('contain', 'POINTS SHAPE')
.should('have.attr', 'style').then(($bgcolor) => {
bgcolor = $bgcolor // Get style attr "background-color"
})
})
it('Hide the last point', () => {
cy.get('#cvat-objects-sidebar-state-item-3')
.find('.anticon-eye')
.click()
cy.get('#cvat-objects-sidebar-state-item-3')
.find('.anticon-eye-invisible')
.should('exist')
})
it('Group the created points', () => {
cy.shapeGrouping(250, 380, 430, 450)
})
it('The hidden point is not grouping', () => {
cy.get('#cvat-objects-sidebar-state-item-3')
.should('have.attr', 'style', bgcolor) // "background-color" should not be changed
})
})
})

@ -99,3 +99,33 @@ Cypress.Commands.add('createTrack', (firstX, firstY, lastX, lastY) => {
cy.get('.cvat-canvas-container')
.click(lastX, lastY)
})
Cypress.Commands.add('createPoint', (posX, posY) => {
cy.get('.cvat-draw-points-control').click()
cy.get('.cvat-draw-shape-popover-content')
.find('button')
.contains('Shape')
.click({force: true})
cy.get('.cvat-canvas-container')
.click(posX, posY)
.trigger('keydown', {key: 'n'})
.trigger('keyup', {key: 'n'})
})
Cypress.Commands.add('changeAppearance', (colorBy) => {
cy.get('.cvat-objects-appearance-content').within(() => {
cy.get('[type="radio"]')
.check(colorBy, {force: true})
})
})
Cypress.Commands.add('shapeGrouping', (firstX, firstY, lastX, lastY) => {
cy.get('.cvat-canvas-container')
.trigger('keydown', {key: 'g'})
.trigger('keyup', {key: 'g'})
.trigger('mousedown', firstX, firstY, {which: 1})
.trigger('mousemove', lastX, lastY)
.trigger('mouseup', lastX, lastY)
.trigger('keydown', {key: 'g'})
.trigger('keyup', {key: 'g'})
})

Loading…
Cancel
Save