41 | {% endblock %}
42 |
--------------------------------------------------------------------------------
/mturk/migrations/0004_auto_20160816_0109.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # Generated by Django 1.9 on 2016-08-16 08:09
3 | from __future__ import unicode_literals
4 |
5 | from django.db import migrations, models
6 |
7 |
8 | class Migration(migrations.Migration):
9 |
10 | dependencies = [
11 | ('mturk', '0003_auto_20160815_1525'),
12 | ]
13 |
14 | operations = [
15 | migrations.AddField(
16 | model_name='fullvideotask',
17 | name='bonus',
18 | field=models.DecimalField(decimal_places=2, default=0, max_digits=3),
19 | ),
20 | migrations.AddField(
21 | model_name='fullvideotask',
22 | name='paid',
23 | field=models.BooleanField(default=False),
24 | ),
25 | migrations.AddField(
26 | model_name='singleframetask',
27 | name='bonus',
28 | field=models.DecimalField(decimal_places=2, default=0, max_digits=3),
29 | ),
30 | migrations.AddField(
31 | model_name='singleframetask',
32 | name='paid',
33 | field=models.BooleanField(default=False),
34 | ),
35 | ]
36 |
--------------------------------------------------------------------------------
/mturk/scripts/pay_confirmed_bonuses.py:
--------------------------------------------------------------------------------
1 | from mturk.models import FullVideoTask
2 | from mturk.mturk_api import Server
3 | from beaverdam import settings
4 |
5 | mturk = Server(settings.AWS_ID, settings.AWS_KEY, settings.URL_ROOT, settings.MTURK_SANDBOX)
6 |
7 | tasks = FullVideoTask.objects.filter(paid = False, video__verified = True).exclude(hit_id = '')
8 |
9 | def calc_bonus(task):
10 | res = mturk.request('GetAssignmentsForHIT', {"HITId":tasks[0].hit_id})
11 | if res.has_path("GetAssignmentsForHITResult/Assignment") :
12 | res.store("GetAssignmentsForHITResult/Request/IsValid", "IsValid", bool)
13 | res.store("GetAssignmentsForHITResult/Assignment/AssignmentId", "AssignmentId")
14 | res.store("GetAssignmentsForHITResult/Assignment/WorkerId", "WorkerId")
15 | print("Is valid = " + str(res.IsValid))
16 | print("Assignment id = " + res.AssignmentId)
17 | print("worker id = " + res.WorkerId)
18 | task.complete(res.WorkerId, res.AssignmentId, 'Thanks for completing this - your bonus has been paid as {}'.format(task.calculate_bonus()))
19 |
20 | def calc_bonuses(tasks):
21 | print("{} tasks to process".format(len(tasks)))
22 | for task in tasks:
23 | print("Paying {}".format(task.video.filename))
24 | calc_bonus(task)
25 |
26 | calc_bonuses(tasks)
27 |
--------------------------------------------------------------------------------
/deployment/supervisor.conf:
--------------------------------------------------------------------------------
1 | ; supervisor config file
2 |
3 | [unix_http_server]
4 | file=/var/run/supervisor.sock ; (the path to the socket file)
5 | chmod=0766 ; sockef file mode (default 0700)
6 |
7 | [supervisord]
8 | logfile=/var/log/supervisor/supervisord.log ; (main log file;default $CWD/supervisord.log)
9 | pidfile=/var/run/supervisord.pid ; (supervisord pidfile;default supervisord.pid)
10 | childlogdir=/var/log/supervisor ; ('AUTO' child log dir, default $TEMP)
11 |
12 | ; the below section must remain in the config file for RPC
13 | ; (supervisorctl/web interface) to work, additional interfaces may be
14 | ; added by defining them in separate rpcinterface: sections
15 | [rpcinterface:supervisor]
16 | supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface
17 |
18 | [supervisorctl]
19 | serverurl=unix:///var/run/supervisor.sock ; use a unix:// URL for a unix socket
20 |
21 | [program:django]
22 | command={{ path }}venv/bin/uwsgi --chdir {{ path }} --socket 127.0.0.1:4242 --logto {{ path }}logs/uwsgi.log --module beaverdam.wsgi
23 | redirect_stderr=true
24 | stdout_logfile={{ path }}/logs/stdout.log
25 | stdout_logfile_maxbytes=2MB
26 | stderr_logfile={{ path }}/logs/stderr.log
27 | stderr_logfile_maxbytes=2MB
28 | user=ubuntu
29 | autostart=true
30 | autorestart=true
31 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2016, Anting Shen
2 | All rights reserved.
3 |
4 | Redistribution and use in source and binary forms, with or without
5 | modification, are permitted provided that the following conditions are met:
6 |
7 | * Redistributions of source code must retain the above copyright notice, this
8 | list of conditions and the following disclaimer.
9 |
10 | * Redistributions in binary form must reproduce the above copyright notice,
11 | this list of conditions and the following disclaimer in the documentation
12 | and/or other materials provided with the distribution.
13 |
14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
15 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
16 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
17 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
18 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
19 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
20 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
21 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
22 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
23 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
24 |
--------------------------------------------------------------------------------
/beaverdam/urls.py:
--------------------------------------------------------------------------------
1 | from django.conf.urls import url
2 | from django.conf.urls.static import static
3 | from django.contrib import admin
4 | from django.contrib.auth.views import login, logout
5 | from django.views.generic.base import RedirectView
6 |
7 | from annotator.views import *
8 | from annotator.services import *
9 |
10 | admin.site.site_header = 'BeaverDam'
11 |
12 | urlpatterns = [
13 | url(r'^$', home),
14 | url(r'^verify/$', verify_list),
15 | url(r'^verified/$', verified_list),
16 | url(r'^readytopay/$', ready_to_pay),
17 |
18 | url(r'^get_states/$', get_states, name='get_states'),
19 |
20 | url(r'^video/(\d+)/$', video, name='video'),
21 | url(r'^video/(\d+)/next/$', next_unannotated),
22 | url(r'^video/(\d+)/verify/$', verify),
23 | url(r'^annotation/(\d+)/$', AnnotationView.as_view()),
24 | url(r'^accept\-annotation/(\d+)/$', ReceiveCommand.as_view()),
25 | url(r'^reject\-annotation/(\d+)/$', ReceiveCommand.as_view()),
26 | url(r'^email-worker/(\d+)/$', ReceiveCommand.as_view()),
27 |
28 | url(r'^login/$', login,
29 | {'template_name': 'admin/login.html',
30 | 'extra_context': {'site_header': 'BeaverDam Login'}
31 | }, name='login'),
32 | url(r'^logout/$', logout),
33 | url(r'^accounts/', RedirectView.as_view(url='/')),
34 | url(r'^admin/', admin.site.urls),
35 | ] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
36 |
--------------------------------------------------------------------------------
/annotator/templates/base.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | BeaverDam
4 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 | {% block head %}{% endblock %}
34 |
35 |
36 |
37 | {% block body %}{% endblock %}
38 |
39 |
40 |
--------------------------------------------------------------------------------
/mturk/migrations/0002_auto_20160814_1053.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # Generated by Django 1.9.7 on 2016-08-14 17:53
3 | from __future__ import unicode_literals
4 |
5 | from django.db import migrations, models
6 | import django.db.models.deletion
7 |
8 |
9 | class Migration(migrations.Migration):
10 |
11 | dependencies = [
12 | ('annotator', '0003_video_verified'),
13 | ('mturk', '0001_initial'),
14 | ]
15 |
16 | operations = [
17 | migrations.CreateModel(
18 | name='SingleFrameTask',
19 | fields=[
20 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
21 | ('hit_id', models.CharField(blank=True, max_length=64)),
22 | ('hit_group', models.CharField(blank=True, max_length=64)),
23 | ('worker_id', models.CharField(blank=True, max_length=64)),
24 | ('sandbox', models.BooleanField(default=True)),
25 | ('time', models.FloatField()),
26 | ('video', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='annotator.Video')),
27 | ],
28 | options={
29 | 'abstract': False,
30 | },
31 | ),
32 | migrations.AddField(
33 | model_name='fullvideotask',
34 | name='sandbox',
35 | field=models.BooleanField(default=True),
36 | ),
37 | migrations.AddField(
38 | model_name='fullvideotask',
39 | name='worker_id',
40 | field=models.CharField(blank=True, max_length=64),
41 | ),
42 | ]
43 |
--------------------------------------------------------------------------------
/mturk/tests.py:
--------------------------------------------------------------------------------
1 | from django.test import TestCase, RequestFactory
2 | from django.contrib.auth.models import AnonymousUser, User
3 |
4 | from .utils import authenticate_hit
5 | from .models import FullVideoTask
6 | from annotator.models import Video
7 |
8 |
9 | class AuthenticateHitTest(TestCase):
10 | def setUp(self):
11 | self.factory = RequestFactory()
12 | video = Video.objects.create()
13 | FullVideoTask.objects.create(hit_id='real_hit_id', video=video)
14 |
15 | def test_normal_hit(self):
16 | request = self.factory.get('/?foo=bar&assignmentId=real_asmt_id&hitId=real_hit_id&workerId=real_worker_id')
17 | data = authenticate_hit(request)
18 | self.assertEqual(data['authenticated'], True)
19 | self.assertEqual(data['preview'], False)
20 | self.assertEqual(data['assignment_id'], 'real_asmt_id')
21 |
22 | def test_preview(self):
23 | mturk_request = self.factory.get('/?assignmentId=ASSIGNMENT_ID_NOT_AVAILABLE')
24 | test_request = self.factory.get('/?preview=True')
25 | for request in (mturk_request, test_request):
26 | data = authenticate_hit(request)
27 | self.assertEqual(data['authenticated'], True)
28 | self.assertEqual(data['preview'], True)
29 |
30 | def test_error(self):
31 | request = self.factory.get('/?foo=bar&assignmentId=real_asmt_id&hitId=fake_hit_id&workerId=real_worker_id')
32 | data = authenticate_hit(request)
33 | self.assertEqual('error' in data, True)
34 |
35 | def test_non_mturk(self):
36 | request = self.factory.get('/')
37 | data = authenticate_hit(request)
38 | self.assertEqual(data['authenticated'], False)
39 | self.assertEqual(data['preview'], False)
40 |
--------------------------------------------------------------------------------
/annotator/management/commands/import_images_from_dir.py:
--------------------------------------------------------------------------------
1 | from django.core.management.base import BaseCommand, CommandError
2 | from annotator.models import Video
3 | import os
4 | import json
5 | import re
6 | import os
7 | import shutil
8 |
9 | # referring to https://stackoverflow.com/questions/5967500/how-to-correctly-sort-a-string-with-a-number-inside for "human sorting"
10 | def atoi(text):
11 | return int(text) if text.isdigit() else text
12 |
13 | def natural_keys(text):
14 | return [ atoi(c) for c in re.split('(\d+)', text) ]
15 |
16 | class Command(BaseCommand):
17 | help = "Imports a directory of images as a new view object"
18 |
19 | def add_arguments(self, parser):
20 | parser.add_argument('directory')
21 |
22 | def handle(self, *args, **options):
23 | if (options['directory']):
24 | self.create_entry_from_dir(options['directory'])
25 |
26 | def create_entry_from_dir(self, directory):
27 | working_dir = os.getcwd()
28 | if os.path.basename(working_dir) != "BeaverDam":
29 | self.stdout.write(f"Make the working directory BeaverDam root. Current working directory is {working_dir}")
30 | i = 1
31 | host = f'/static/image_import{i}'
32 | dest_folder = 'annotator'+host
33 | while os.path.isdir(dest_folder):
34 | i+=1
35 | host = f'/static/image_import{i}'
36 | dest_folder = 'annotator'+host
37 |
38 | os.makedirs(dest_folder)
39 | dir_list = os.listdir(directory)
40 | file_list = []
41 | for file in dir_list:
42 | file_path = os.path.join(directory, file)
43 | if (os.path.isfile(file_path)):
44 | file_list.append(file)
45 | shutil.copy(file_path, dest_folder)
46 |
47 | file_list.sort(key = natural_keys)
48 | image_list_json = json.dumps(file_list)
49 | v = Video(image_list = image_list_json, host = host)
50 | v.save()
51 | self.stdout.write(f"Video {v.id} was made")
52 |
--------------------------------------------------------------------------------
/mturk/scripts/playground.py:
--------------------------------------------------------------------------------
1 | from mturk.models import FullVideoTask
2 | from annotator.models import Video
3 | from mturk.mturk_api import Server
4 | from beaverdam import settings
5 |
6 | mturk = Server(settings.AWS_ID, settings.AWS_KEY, settings.URL_ROOT, settings.MTURK_SANDBOX)
7 |
8 | def get_hit_for_video(full_video_task):
9 | res = mturk.request('GetHIT', {"HITId":full_video_task.hit_id})
10 | res.store("HIT/HITStatus", "status")
11 | res.store("HIT/HITId", "hitID")
12 | print(full_video_task.video.filename)
13 | print(" - status={}, hitid={}".format(res.status, res.hitID))
14 | # if res.has_path("GetAssignmentsForHITResult/Assignment") :
15 | # res.store("GetAssignmentsForHITResult/Request/IsValid", "IsValid", bool)
16 | # res.store("GetAssignmentsForHITResult/Assignment/AssignmentId", "AssignmentId")
17 | # res.store("GetAssignmentsForHITResult/Assignment/WorkerId", "WorkerId")
18 | # print("Is valid = " + str(res.IsValid))
19 | # print("Assignment id = " + res.AssignmentId)
20 | # print("worker id = " + res.WorkerId)
21 | # task.complete(res.WorkerId, res.AssignmentId, 'Thanks for completing this - your bonus has been paid as {}'.format(task.calculate_bonus()))
22 |
23 | def get_hits_for_video(video_tasks):
24 | print("{} tasks to process".format(len(video_tasks)))
25 | for task in video_tasks:
26 | get_hit_for_video(task)
27 |
28 | def get_completed_videos():
29 | vids = Video.objects.filter(verified=True)
30 | for vid in vids:
31 | print(vid.id)
32 |
33 | def get_tasks_by_hit_id(hitid):
34 | fvs = FullVideoTask.objects.filter(hit_id = hitid)
35 | print("Returned: " + str(len(fvs)))
36 |
37 | def dump_all_tasks():
38 | fvs = FullVideoTask.objects.exclude(hit_id = '')
39 | for x in fvs:
40 | print(x.hit_id)
41 |
42 | tasks = FullVideoTask.objects.filter(sandbox = False).exclude(hit_id = '')
43 | get_hits_for_video(tasks)
44 | #get_completed_videos()
45 |
46 | #get_tasks_by_hit_id("3DQYSJDTYL1LBN7L0I2B6FA1K3YXEA")
47 | #dump_all_tasks()
48 |
--------------------------------------------------------------------------------
/annotator/templates/modals/email_form.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
Send Worker Email
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
46 |
47 |
48 |
49 |
--------------------------------------------------------------------------------
/default-instructions.md:
--------------------------------------------------------------------------------
1 | Welcome to our Video Mapping Tool.
2 |
3 | Your task is to accurately draw rectangles around people in each video. We need this so that we can train a computer to understand people's location indoors from a fisheye lenses.
4 |
5 | ## Quick Instructions
6 | 1. Draw a box accurately around a person (covering every part you can see)
7 | 2. Hit 's' to go step through frames
8 | 3. Adjust the box to keep it accurate
9 | 4. Go back to the first frame and repeat for the next person
10 |
11 |
12 | For example:
13 | ### Good:
14 | 
15 | 
16 | ### Bad:
17 | 
18 | 
19 |
20 |
21 | ## Keyboard Shortcuts
22 |
23 | - `a` step back 1 frame
24 | - `s` step forward 1 frame
25 | - `space` play/pause
26 | - `d` or 'delete' to delete selected rectangle
27 |
28 | ## Detailed Instructions
29 | 1. We need you to draw an accurate box around each person you see in the video
30 | 2. We recommend you draw *one* person at a time from each video end to end and then rewind to the start for the next person
31 | 3. Best way to do this is frame by frame - hit 's' to move to the next frame and 'a' to move backwards 1 frame
32 | 4. You can skip frames when a person does not move. You will see a dotted line around that person.
33 | 4. If you don't see any people in the video for that frame then don't draw any rectangles
34 | 5. If the person disappears then delete the rectangle that represented that person (press 'd', or 'delete' with the box selected)
35 |
36 | ## Bonus Payment
37 | 1. The time to complete one HIT will depend on the number of people in the frame and the amount of movement.
38 | 2. As such we will pay a bonus of $0.02c per accurate frame mapped.
39 | 3. Note if the person does not move at all or if the dotted rectangle is accurate, please do NOT create a rectangle in that frame. We will reject the HIT if you do this.
40 |
41 | ## How accurate do you need to be?
42 | We require a tight rectangle covering all of the person visible in that video frame.
43 |
44 |
45 |
--------------------------------------------------------------------------------
/annotator/static/views/keyframebar.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 |
4 | // Constants. ES6 doesn't support class constants yet, thus this hack.
5 | var KeyframebarConstants = {
6 | KEYFRAME_SVG:
7 | ``,
10 | };
11 |
12 |
13 | class Keyframebar {
14 | constructor({classBaseName}) {
15 | // This container of the keyframe bar
16 | this.$container = null;
17 |
18 | // Namespaced className generator
19 | this.classBaseName = classBaseName.add('keyframebar');
20 |
21 | // Duration of the video
22 | this.duration = 0;
23 |
24 | // Prevent adding new properties
25 | $(this).on('dummy', $.noop);
26 | Object.preventExtensions(this, Keyframebar);
27 | }
28 |
29 | attach($container) {
30 | // Don't add twice
31 | if (this.$container != null) {
32 | throw new Error("Keyframebar.attach: already attached to container");
33 | }
34 |
35 | // Actually do the attaching
36 | this.$container = $container;
37 |
38 | // Apply appearance
39 | this.resetWithDuration(0);
40 |
41 | // Trigger event
42 | $(this).triggerHandler('attach', this.$container);
43 |
44 | return this;
45 | }
46 |
47 | detach() {
48 | this.$container.empty();
49 |
50 | // Trigger event
51 | $(this).triggerHandler('detach', this.$container);
52 |
53 | this.$container = undefined;
54 |
55 | return this;
56 | }
57 |
58 | resetWithDuration(duration) {
59 | this.$container.empty();
60 |
61 | this.duration = duration;
62 | }
63 |
64 | addKeyframeAt(time, classNameExtBooleans) {
65 | var frac = time / this.duration;
66 | var classBaseName = this.classBaseName.add('keyframe');
67 | var classNames = Misc.getClassNamesFromExts([classBaseName], classBaseName, classNameExtBooleans);
68 |
69 | $(this.KEYFRAME_SVG)
70 | .attr({class: classNames.join(' ')})
71 | .css({'left': `${frac * 100}%`})
72 | .click(() => { $(this).triggerHandler('jump-to-time', time); })
73 | .appendTo(this.$container);
74 | }
75 | }
76 |
77 | // Mix-in constants
78 | Misc.mixinClassConstants(Keyframebar, KeyframebarConstants);
79 | void Keyframebar;
80 |
--------------------------------------------------------------------------------
/mturk/migrations/0012_auto_20170417_1814.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # Generated by Django 1.10 on 2017-04-18 01:14
3 | from __future__ import unicode_literals
4 |
5 | from django.db import migrations, models
6 |
7 |
8 | class Migration(migrations.Migration):
9 |
10 | dependencies = [
11 | ('mturk', '0011_auto_20161129_1859'),
12 | ]
13 |
14 | operations = [
15 | migrations.AlterField(
16 | model_name='fullvideotask',
17 | name='assignment_id',
18 | field=models.CharField(blank=True, help_text='ID of matching between this task and a worker. Used for authentication', max_length=64),
19 | ),
20 | migrations.AlterField(
21 | model_name='fullvideotask',
22 | name='hit_group',
23 | field=models.CharField(blank=True, help_text='Group ID on Mturk. HITs with same title/description usually are placed in the same group by MTurk. Auto-populated when publishing.', max_length=64),
24 | ),
25 | migrations.AlterField(
26 | model_name='fullvideotask',
27 | name='hit_id',
28 | field=models.CharField(blank=True, help_text='ID on MTurk. Auto-populated when publishing', max_length=64),
29 | ),
30 | migrations.AlterField(
31 | model_name='fullvideotask',
32 | name='worker_id',
33 | field=models.CharField(blank=True, help_text='ID of worker who submitted this HIT', max_length=64),
34 | ),
35 | migrations.AlterField(
36 | model_name='singleframetask',
37 | name='assignment_id',
38 | field=models.CharField(blank=True, help_text='ID of matching between this task and a worker. Used for authentication', max_length=64),
39 | ),
40 | migrations.AlterField(
41 | model_name='singleframetask',
42 | name='hit_group',
43 | field=models.CharField(blank=True, help_text='Group ID on Mturk. HITs with same title/description usually are placed in the same group by MTurk. Auto-populated when publishing.', max_length=64),
44 | ),
45 | migrations.AlterField(
46 | model_name='singleframetask',
47 | name='hit_id',
48 | field=models.CharField(blank=True, help_text='ID on MTurk. Auto-populated when publishing', max_length=64),
49 | ),
50 | migrations.AlterField(
51 | model_name='singleframetask',
52 | name='worker_id',
53 | field=models.CharField(blank=True, help_text='ID of worker who submitted this HIT', max_length=64),
54 | ),
55 | ]
56 |
--------------------------------------------------------------------------------
/annotator/migrations/0007_auto_20170417_1814.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # Generated by Django 1.10 on 2017-04-18 01:14
3 | from __future__ import unicode_literals
4 |
5 | from django.db import migrations, models
6 |
7 |
8 | class Migration(migrations.Migration):
9 |
10 | dependencies = [
11 | ('annotator', '0006_video_rejected'),
12 | ]
13 |
14 | operations = [
15 | migrations.AlterField(
16 | model_name='label',
17 | name='color',
18 | field=models.CharField(blank=True, help_text='6 digit hex.', max_length=6),
19 | ),
20 | migrations.AlterField(
21 | model_name='label',
22 | name='name',
23 | field=models.CharField(blank=True, help_text='Name of class label option.', max_length=100, unique=True),
24 | ),
25 | migrations.AlterField(
26 | model_name='video',
27 | name='annotation',
28 | field=models.TextField(blank=True, help_text='A JSON blob containing all user annotation sent from client.'),
29 | ),
30 | migrations.AlterField(
31 | model_name='video',
32 | name='filename',
33 | field=models.CharField(blank=True, help_text='Name of the video file.The video should be publically accessible by at .', max_length=100),
34 | ),
35 | migrations.AlterField(
36 | model_name='video',
37 | name='host',
38 | field=models.CharField(blank=True, help_text='Path to prepend to filenames to form the url for this video or the images in `image_list`.', max_length=1048),
39 | ),
40 | migrations.AlterField(
41 | model_name='video',
42 | name='image_list',
43 | field=models.TextField(blank=True, help_text='List of filenames of images to be used as video frames, in JSON format.When present, image list is assumed and is ignored.'),
44 | ),
45 | migrations.AlterField(
46 | model_name='video',
47 | name='rejected',
48 | field=models.BooleanField(default=False, help_text='Rejected by expert.'),
49 | ),
50 | migrations.AlterField(
51 | model_name='video',
52 | name='source',
53 | field=models.CharField(blank=True, help_text='Name of video source or type, for easier grouping/searching of videos.This field is not used by BeaverDam and only facilitates querying on videos by type.', max_length=1048),
54 | ),
55 | migrations.AlterField(
56 | model_name='video',
57 | name='verified',
58 | field=models.BooleanField(default=False, help_text='Verified as correct by expert.'),
59 | ),
60 | ]
61 |
--------------------------------------------------------------------------------
/deployment/playbook.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | - hosts: webservers
3 |
4 | vars:
5 | path: /home/anting/BeaverDam/
6 | repo: https://github.com/antingshen/BeaverDam.git
7 | branch: master
8 | server_domain: SERVER_DOMAIN
9 | python3: "{{path}}venv/bin/python"
10 | db_filename: prod.db.sqlite3
11 | aws_id: AWS_ID
12 | aws_key: AWS_KEY
13 |
14 | tasks:
15 | - name: Install package list
16 | apt: pkg={{item}} state=installed update_cache=yes
17 | become: true
18 | with_items:
19 | - git
20 | - python3-pip
21 | - supervisor
22 | - nginx
23 | - s3cmd
24 | notify: Start supervisor
25 |
26 | - name: Install virtualenv
27 | pip:
28 | name: virtualenv
29 | executable: pip3
30 | become: true
31 |
32 | - name: Fetch code
33 | git: repo={{repo}} dest={{path}} version={{branch}} accept_hostkey=yes
34 | notify:
35 | - Install python packages
36 | - Collect static & migrate db
37 | - Restart django
38 | tags: pull
39 |
40 | - name: Copy configurations
41 | template: src={{item.src}} dest={{item.dest}}
42 | with_items:
43 | - {src: 'supervisor.conf', dest: '/etc/supervisor/conf.d/supervisor.conf'}
44 | - {src: 'nginx.conf', dest: '/etc/nginx/conf.d/BeaverDam.conf'}
45 | - {src: 'deploy_settings.py', dest: '{{path}}beaverdam/deploy_settings.py'}
46 | notify:
47 | - Restart supervisor
48 | - Restart nginx
49 | - Restart django
50 | become: true
51 | tags: config
52 |
53 | - name: Create log
54 | copy:
55 | content: ""
56 | dest: '{{path}}/logs/uwsgi.log'
57 | force: no
58 | mode: 0666
59 |
60 | - name: Run handlers
61 | debug: msg="running handlers"
62 | changed_when: true
63 | notify:
64 | # - Start supervisor
65 | - Install python packages
66 | - Restart supervisor
67 | - Restart nginx
68 | - Collect static & migrate db
69 | - Restart django
70 |
71 |
72 | handlers:
73 | - name: Start supervisor
74 | command: supervisord -c /etc/supervisor/conf.d/supervisor.conf
75 | become: true
76 |
77 | - name: Install python packages
78 | pip: virtualenv={{path}}venv
79 | requirements={{path}}requirements.txt
80 | virtualenv_python=python3.5
81 |
82 | - name: Restart supervisor
83 | command: supervisorctl reload
84 | notify: Restart django
85 | become: true
86 |
87 | - name: Restart nginx
88 | service: name=nginx state=restarted
89 | become: true
90 |
91 | - name: Collect static & migrate db
92 | command: "{{python3}} {{path}}manage.py {{item}} --settings beaverdam.deploy_settings --noinput"
93 | with_items:
94 | - collectstatic
95 | - migrate
96 |
97 | - name: Restart django
98 | supervisorctl: name=django state=restarted
99 |
--------------------------------------------------------------------------------
/annotator/static/bounds.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | /**
4 | * A Bounds object describes a bounding box. The class has 4 members: xMin,
5 | * xMax, yMin, yMax. In Bounds objects, it is guaranteed that xMin <= xMax &&
6 | * yMin <= yMax.
7 | *
8 | * We don't need a constructor for Bounds objects, so we just construct it
9 | * using object notation:
10 | *
11 | * var bounds = {
12 | * xMin: ...
13 | * xMax: ...
14 | * yMin: ...
15 | * yMax: ...
16 | * };
17 | *
18 | * The Bounds class holds utility/math functions for Bounds objects.
19 | */
20 | class Bounds {
21 | static normalize(bounds) {
22 | var {xMin, xMax, yMin, yMax} = bounds;
23 | return {
24 | xMin: Math.min(xMin, xMax),
25 | xMax: Math.max(xMin, xMax),
26 | yMin: Math.min(yMin, yMax),
27 | yMax: Math.max(yMin, yMax),
28 | };
29 | }
30 |
31 | static fromAttrs(attrs) {
32 | var {x, y, width, height} = attrs;
33 | return Bounds.normalize({
34 | xMin: x,
35 | xMax: x + width,
36 | yMin: y,
37 | yMax: y + height,
38 | });
39 | }
40 |
41 | static toAttrs(bounds) {
42 | var {xMin, xMax, yMin, yMax} = bounds;
43 | return {
44 | x: xMin,
45 | y: yMin,
46 | width: xMax - xMin,
47 | height: yMax - yMin,
48 | };
49 | }
50 |
51 | static resize(bounds, dxMin = 0, dxMax = 0, dyMin = 0, dyMax = 0) {
52 | var {xMin, xMax, yMin, yMax} = bounds;
53 | return Bounds.normalize({
54 | xMin: xMin + dxMin,
55 | xMax: xMax + dxMax,
56 | yMin: yMin + dyMin,
57 | yMax: yMax + dyMax,
58 | });
59 | }
60 |
61 | static move(bounds, dx, dy) {
62 | return Bounds.resize(bounds, dx, dx, dy, dy);
63 | }
64 |
65 | static interpolate(bounds0, bounds1, frac) {
66 | if (frac < 0 || frac > 1)
67 | throw new Error("Bounds.interpolate: invalid argument: frac");
68 |
69 | var ifrac = 1.0 - frac;
70 | return {
71 | xMin: bounds0.xMin * ifrac + bounds1.xMin * frac,
72 | xMax: bounds0.xMax * ifrac + bounds1.xMax * frac,
73 | yMin: bounds0.yMin * ifrac + bounds1.yMin * frac,
74 | yMax: bounds0.yMax * ifrac + bounds1.yMax * frac,
75 | };
76 | }
77 |
78 | static equals(bounds0, bounds1) {
79 | if (bounds0.xMin != bounds1.xMin) return false;
80 | if (bounds0.xMax != bounds1.xMax) return false;
81 | if (bounds0.yMin != bounds1.yMin) return false;
82 | if (bounds0.yMax != bounds1.yMax) return false;
83 | return true;
84 | }
85 |
86 | static greaterOrEqualTo(bounds, minDimensions) {
87 | var {xMin, xMax, yMin, yMax} = bounds;
88 | var {width, height} = minDimensions;
89 |
90 | return (xMax - xMin) >= width && (yMax - yMin) >= height;
91 | }
92 | }
93 |
94 | void Bounds;
95 |
--------------------------------------------------------------------------------
/annotator/fixtures/initialdata.yaml:
--------------------------------------------------------------------------------
1 | - model: annotator.Label
2 | fields:
3 | name: "Car"
4 | color: ba6cf8
5 | - model: annotator.State
6 | fields:
7 | name: "Red"
8 | color: 5865a6
9 | label_name: "Car"
10 | - model: annotator.State
11 | fields:
12 | name: "Black"
13 | color: 59350a
14 | label_name: "Car"
15 | - model: annotator.State
16 | fields:
17 | name: "Grey"
18 | color: 483770
19 | label_name: "Car"
20 | - model: annotator.State
21 | fields:
22 | name: "Parked"
23 | color: 3061c5
24 | label_name: "Car"
25 | - model: annotator.State
26 | fields:
27 | name: "Moving"
28 | color: 9367f7
29 | label_name: "Car"
30 | - model: annotator.Label
31 | fields:
32 | name: "Pedestrian"
33 | color: a2bb11
34 | - model: annotator.State
35 | fields:
36 | name: "Man"
37 | color: e71506
38 | label_name: "Pedestrian"
39 | - model: annotator.State
40 | fields:
41 | name: "Woman"
42 | color: ee218c
43 | label_name: "Pedestrian"
44 | - model: annotator.State
45 | fields:
46 | name: "Child"
47 | color: 4cc67e
48 | label_name: "Pedestrian"
49 | - model: annotator.Label
50 | fields:
51 | name: "Bicycle"
52 | color: 102b25
53 | - model: annotator.State
54 | fields:
55 | name: "Parked"
56 | color: f47b16
57 | label_name: "Bicycle"
58 | - model: annotator.State
59 | fields:
60 | name: "Moving"
61 | color: 98e9d5
62 | label_name: "Bicycle"
63 | - model: annotator.Label
64 | fields:
65 | name: "Traffic Light"
66 | color: 0a2ecf
67 | - model: annotator.State
68 | fields:
69 | name: "Red"
70 | color: c2800e
71 | label_name: "Traffic Light"
72 | - model: annotator.State
73 | fields:
74 | name: "Green"
75 | color: 5c729e
76 | label_name: "Traffic Light"
77 | - model: annotator.State
78 | fields:
79 | name: "Yellow"
80 | color: 5ae867
81 | label_name: "Traffic Light"
82 | - model: annotator.State
83 | fields:
84 | name: "Unclear"
85 | color: 93af3b
86 | label_name: "Traffic Light"
87 | - model: annotator.Label
88 | fields:
89 | name: "Pedestrian Signal"
90 | color: c54dff
91 | - model: annotator.State
92 | fields:
93 | name: "Red"
94 | color: 66b157
95 | label_name: "Pedestrian Signal"
96 | - model: annotator.State
97 | fields:
98 | name: "Green"
99 | color: a9c47a
100 | label_name: "Pedestrian Signal"
101 | - model: annotator.State
102 | fields:
103 | name: "Unclear"
104 | color: 278360
105 | label_name: "Pedestrian Signal"
106 | - model: annotator.Label
107 | fields:
108 | name: "Crosswalk"
109 | color: 1ebf81
110 | - model: annotator.State
111 | fields:
112 | name: "Empty"
113 | color: 4b7523
114 | label_name: "Crosswalk"
115 | - model: annotator.State
116 | fields:
117 | name: "Occupied"
118 | color: 5b800d
119 | label_name: "Crosswalk"
120 |
--------------------------------------------------------------------------------
/annotator/templates/modals/accept_reject_form.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
Accept Work
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
62 |
63 |
64 |
65 |
--------------------------------------------------------------------------------
/annotator/admin.py:
--------------------------------------------------------------------------------
1 | from django.contrib import admin
2 | from django.contrib.admin import SimpleListFilter
3 | from .models import Video, Label, State
4 | from mturk.models import FullVideoTask
5 | from mturk.queries import get_active_video_turk_task
6 | from django.db.models import Count, Sum, Q, Case, When, IntegerField
7 | import logging
8 |
9 | logger = logging.getLogger()
10 |
11 | def publish_to_turk(modeladmin, request, videos):
12 | for video in videos:
13 | video_task = get_active_video_turk_task(video.id)
14 |
15 | if video_task != None:
16 | raise Exception('video {} already has an active FullVideoTask'.format(id))
17 |
18 | video_task = FullVideoTask(video = video)
19 | video_task.publish()
20 |
21 | class PublishedFilter(SimpleListFilter):
22 | title = 'Currently Published' # or use _('country') for translated title
23 | parameter_name = 'Published'
24 | default_value = 2
25 |
26 | def lookups(self, request, model_admin):
27 | return (
28 | ("1", 'Yes'),
29 | ("0", 'No'),
30 | )
31 |
32 | def queryset(self, request, queryset):
33 | if self.value() is None:
34 | return queryset
35 | else:
36 | self.used_parameters[self.parameter_name] = self.value()
37 |
38 | if self.value() == "0":
39 | return queryset.annotate(num_video_tasks=
40 | Sum(
41 | Case(
42 | When(Q(fullvideotask__id=None) | Q(fullvideotask__closed=True), then=0),
43 | default=1,
44 | output_field=IntegerField())
45 | )).filter(num_video_tasks = 0)
46 |
47 | elif self.value() == "1":
48 | return queryset.annotate(num_video_tasks=
49 | Sum(
50 | Case(
51 | When(Q(fullvideotask__id=None) | Q(fullvideotask__closed=True), then=0),
52 | default=1,
53 | output_field=IntegerField())
54 | )).filter(num_video_tasks__gt = 0)
55 | else:
56 | return queryset
57 |
58 | class VideoAdmin(admin.ModelAdmin):
59 | list_display =('id', 'video_url','filename','verified', 'is_published')
60 | list_filter=[PublishedFilter, 'verified']
61 | search_fields=['filename', 'id']
62 | actions=[publish_to_turk]
63 |
64 | def is_published(self, obj):
65 | task = get_active_video_turk_task(obj.id)
66 | if task == None:
67 | return False
68 | if task.hit_id == '':
69 | return False
70 | return True
71 |
72 | is_published.short_description = "Currently Published"
73 | is_published.boolean = True
74 |
75 | def video_url(self, obj):
76 | return '/video/{}/'.format(obj.id, obj.id)
77 | video_url.allow_tags = True
78 | video_url.short_description = 'Video'
79 |
80 |
81 | class StateAdmin(admin.ModelAdmin):
82 | list_display = ['name', 'label_name']
83 |
84 |
85 | admin.site.register(Video, VideoAdmin)
86 | admin.site.register(Label)
87 | admin.site.register(State, StateAdmin)
88 |
--------------------------------------------------------------------------------
/annotator/models.py:
--------------------------------------------------------------------------------
1 | from django.db import models
2 | from django.contrib.staticfiles import finders
3 |
4 |
5 | class Label(models.Model):
6 | """The classes available for workers to choose from for each object."""
7 | id = models.AutoField(primary_key=True)
8 | name = models.CharField(blank=True, max_length=100, unique=True,
9 | help_text="Name of class label option.")
10 | color = models.CharField(blank=True, max_length=6,
11 | help_text="6 digit hex.")
12 |
13 | def __str__(self):
14 | return self.name
15 |
16 |
17 | class State(models.Model):
18 | """The states available for each label."""
19 | id = models.AutoField(primary_key=True)
20 | name = models.CharField(blank=True, max_length=100,
21 | help_text="Name of class label option.")
22 | color = models.CharField(blank=True, max_length=6,
23 | help_text="6 digit hex.")
24 | label_name = models.ForeignKey(Label, blank=True, to_field='name')
25 |
26 | def __str__(self):
27 | return self.name
28 |
29 |
30 | class Video(models.Model):
31 | annotation = models.TextField(blank=True,
32 | help_text="A JSON blob containing all user annotation sent from client.")
33 | source = models.CharField(max_length=1048, blank=True,
34 | help_text=("Name of video source or type, for easier grouping/searching of videos."
35 | "This field is not used by BeaverDam and only facilitates querying on videos by type."))
36 | filename = models.CharField(max_length=100, blank=True,
37 | help_text=("Name of the video file."
38 | "The video should be publically accessible by at ."))
39 | image_list = models.TextField(blank=True,
40 | help_text=("List of filenames of images to be used as video frames, in JSON format."
41 | "When present, image list is assumed and is ignored."))
42 | host = models.CharField(max_length=1048, blank=True,
43 | help_text="Path to prepend to filenames to form the url for this video or the images in `image_list`.")
44 | verified = models.BooleanField(default=False, help_text="Verified as correct by expert.")
45 | rejected = models.BooleanField(default=False, help_text="Rejected by expert.")
46 | labels = models.ManyToManyField(Label, blank=True)
47 |
48 | @classmethod
49 | def from_list(cls, path_to_list, *, source, host, filename_prefix=''):
50 | created = []
51 | for line in open(path_to_list, 'r'):
52 | if line:
53 | video = cls(source=source, filename=filename_prefix + line.strip(), host=host)
54 | video.save()
55 | created.append(video)
56 | return created
57 |
58 | def __str__(self):
59 | return '/video/{}'.format(self.id)
60 |
61 | @property
62 | def url(self):
63 | if self.image_list:
64 | return 'Image List'
65 | elif finders.find('videos/{}.mp4'.format(self.id)):
66 | return '/static/videos/{}.mp4'.format(self.id)
67 | elif self.filename and self.host:
68 | return self.host + self.filename
69 | else:
70 | raise Exception('Video {0} does not have a filename, host or image_list. Possible fixes: \n1) Place {0}.mp4 into static/videos to serve locally. \n2) Update the filename & host fields of the Video with id={0}'.format(self.id))
71 |
72 | def count_keyframes(self, at_time=None):
73 | if at_time is None:
74 | return self.annotation.count('"frame"')
75 | else:
76 | return self.annotation.count('"frame": {}'.format(at_time))
77 |
--------------------------------------------------------------------------------
/annotator/services.py:
--------------------------------------------------------------------------------
1 | import mturk.queries
2 | import logging
3 | from django.http import HttpResponse, Http404
4 |
5 | from django.contrib.admin.views.decorators import staff_member_required
6 | from mturk.models import Task, FullVideoTask
7 | from .models import *
8 | from mturk.queries import *
9 | from decimal import Decimal
10 |
11 | logger = logging.getLogger()
12 |
13 | @staff_member_required
14 | def publish_videos_to_turk(videos):
15 | for video in videos:
16 | video_task = get_active_video_turk_task(id)
17 |
18 | if video_task != None:
19 | raise Exception('video {} already has an active FullVideoTask'.format(id))
20 |
21 | video_task = FullVideoTask(video = video)
22 | video_task.publish()
23 |
24 | @staff_member_required
25 | def verify(request, video_id):
26 | body = request.body.decode('utf-8')
27 | video = Video.objects.get(id=video_id)
28 | if body == 'true':
29 | video.verified = True
30 | elif body == 'false':
31 | video.verified = False
32 | else:
33 | print(body)
34 | return HttpResponseBadRequest()
35 | video.save()
36 | return HttpResponse('video verification state saved')
37 |
38 | @staff_member_required
39 | def accept_video(request, video_id, bonus, message, reopen, clear_boxes, blockWorker, updatedAnnotation):
40 | video = Video.objects.get(pk=video_id)
41 | video.verified = True
42 |
43 | video_task = get_active_video_turk_task(video.id)
44 |
45 | if video_task != None:
46 | # accept on turk
47 | video_task.approve_assignment(bonus, message)
48 |
49 | if blockWorker:
50 | video_task.blockWorker()
51 |
52 | # delete from Turk
53 | video_task.archive_turk_hit()
54 |
55 | video_task.bonus = Decimal(bonus)
56 | video_task.message = message
57 | video_task.paid = True
58 | video_task.closed = True
59 | video_task.save()
60 |
61 | # create a new HIT for this instaed
62 | if reopen:
63 | new_task = FullVideoTask(video = video)
64 | new_task.publish()
65 |
66 | # now mark the video as unverified as we're asking somebody else to fill this in
67 | # why would we do this? Sometimes it's a better strategy to accept somebody's work,
68 | # and block the worker but then get somebody else to do the work
69 | video.verified = False
70 |
71 | # clear the boxes as specified
72 | if clear_boxes:
73 | video.annotation = ''
74 | else:
75 | video.annotation = updatedAnnotation
76 |
77 | video.rejected = False
78 | video.save()
79 |
80 | @staff_member_required
81 | def reject_video(request, video_id, message, reopen, clear_boxes, blockWorker, updatedAnnotation):
82 | video = Video.objects.get(pk=video_id)
83 | video_task = get_active_video_turk_task(video.id)
84 |
85 | if video_task != None:
86 | # reject on turk
87 | video_task.reject_assignment(message)
88 |
89 | if blockWorker:
90 | video_task.blockWorker()
91 |
92 | # update the task
93 | video_task.message = message
94 | video_task.rejected = True
95 | video_task.bonus = 0
96 | video_task.closed = True
97 | video_task.save()
98 |
99 | # delete from Turk
100 | video_task.archive_turk_hit()
101 |
102 | # create a new HIT for this instaed
103 | if reopen:
104 | new_task = FullVideoTask(video = video)
105 | new_task.publish()
106 |
107 | # clear the boxes as specified
108 | if clear_boxes:
109 | video.annotation = ''
110 | else:
111 | video.annotation = updatedAnnotation
112 |
113 | video.verified = False
114 | video.rejected = True
115 | video.save()
116 |
117 |
118 | @staff_member_required
119 | def email_worker(request, video_id, subject, message):
120 | video = Video.objects.get(pk=video_id)
121 | video_task = get_active_video_turk_task(video.id)
122 |
123 | if video_task == None:
124 | raise Exception("No video task to send email for {}".format(video_id))
125 |
126 | video_task.send_email(subject, message)
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | BeaverDam
2 | =========
3 |
4 | Video annotation tool for deep learning training labels
5 |
6 | ## About
7 |
8 | This tool is for drawing object bounding boxes in videos. It also includes support for Amazon Mechanical Turk. See the [paper](https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-193.html).
9 |
10 | With small amount of changes, you can also:
11 | - Draw bounding boxes in images
12 | - Add additional attributes in bounding boxes
13 | - Use a custom keyframe scheduler instead of user-scheduled keyframes
14 |
15 | This tool currently does not support semantic segmentation.
16 |
17 | ## Installation
18 |
19 | 1. Clone this repository.
20 | 2. `cd BeaverDam`
21 | 3. Make sure Python 3 is installed.
22 | If not: `brew install python3` (Mac) or `sudo apt-get install python3` (Ubuntu)
23 | 3. Make sure virtualenv is installed.
24 | If not: `pip3 install virtualenv` or maybe `sudo pip3 install virtualenv`
25 | 4. Make the Python virtualenv for this project:
26 | `scripts/setup`
27 | 5. Download sample data:
28 | `scripts/seed -f`
29 |
30 | When running any `./manage.py` commands, use `source venv/bin/activate` to enter venv first.
31 |
32 | See `/deployment` for tips on using BeaverDam for production.
33 |
34 | ### If using mturk
35 |
36 | Replace the credentials below with your own:
37 |
38 | ```bash
39 | export AWS_ID="AKIAAAAYOURIDHERE"
40 | export AWS_KEY="YOURmturkKEYhere5DyUrkm/81SRSMG+5174"
41 | ```
42 |
43 | When ready for real turkers, edit `MTURK_SANDBOX` to `False` in `settings.py`.
44 |
45 | It is recommended to use IAM keys with only mturk permissions instead of root key.
46 |
47 |
48 | ## Running the server
49 |
50 | ```shell
51 | scripts/serve
52 | ```
53 |
54 | Then navigate to [localhost:5000](http://localhost:5000/) in your browser.
55 |
56 | Need to run on a custom port? `env PORT=1234 scripts/serve`
57 |
58 | For actual production deployment, we recommend using standard Django deployment procedures. Sample scripts using uWSGI & nginx are provided in `/deployment`. Remember to set `DEBUG=False` in `settings.py`.
59 |
60 | ### Making accounts
61 |
62 | Login is required to authenticate any changes. Turkers do not require accounts and are authenticated by BeaverDam via Mechanical Turk.
63 |
64 | To make a superuser account, run inside venv `./manage.py createsuperuser`
65 | If you are using sample data, login with username `test` and password `password`.
66 | Additional non-turker worker accounts can be created via `/admin`.
67 |
68 | ### Videos
69 |
70 | To add videos, one must upload the video to a CDN (or use `/annotator/static/videos` to serve on the same server), then create a Django video object that contains the url (`host` + `filename`) to the video file.
71 |
72 | To add video objects via web UI, navigate to `/admin` and create Video objects.
73 | Alternatively, use `./manage.py shell`, and create `annotator.Video` objects and call `video.save()`.
74 | Helper methods exist to create large number of video objects at once, see `annotator/models.py`.
75 |
76 | Video objects can either use H.264 encoded video (See `scripts/convert-to-h264`), or a list of frames provided in the attribute `image_list` in JSON format (e.g. `video.image_list = '["20170728_085435.jpg"]'`).
77 | By using single-frame videos, BeaverDam can be used for image annotation.
78 |
79 | Video annotations can be accessed via admin, `/annotation/video_id`, or through the Video objects' annotation attribute through the shell.
80 |
81 | ### Tasks
82 |
83 | Tasks are created in the same way as Videos.
84 | Only the `video` attribute needs to be filled out at creation time.
85 | They can be published to mturk by calling `task.publish()`.
86 |
87 | ### Simulating mturk view in debug
88 |
89 | To see what video pages look like on mturk preview mode, set url param `preview=true`.
90 | For mturk's HIT accepted mode, set url param `mturk=true`.
91 |
92 | Example: `localhost:5000/video/0/?mturk=true`
93 |
94 | ### MacOS uWSGI
95 |
96 | For MacOS, you may need to do uWSGI==2.0.17
97 |
98 | ### Running tests
99 |
100 | Inside venv, run `./manage.py test`
101 |
102 | ## Contributing
103 |
104 | Pull requests and contributions are welcome.
105 | See [annotator/static/README.md](annotator/static) for more info on frontend architecture.
106 |
107 | ## Support
108 |
109 | For help setting up BeaverDam for your application/company, please contact me or leave an issue.
110 |
--------------------------------------------------------------------------------
/annotator/static/views/annotationbar.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | // Constants. ES6 doesn't support class constants yet, thus this hack.
4 | var AnnotationbarConstants = {
5 | ANNOTATION_DIV:
6 | `