├── mturk ├── __init__.py ├── migrations │ ├── __init__.py │ ├── 0006_auto_20160821_2258.py │ ├── 0010_auto_20161129_1806.py │ ├── 0005_auto_20160816_1512.py │ ├── 0003_auto_20160815_1525.py │ ├── 0007_auto_20161122_1349.py │ ├── 0011_auto_20161129_1859.py │ ├── 0009_auto_20161128_0119.py │ ├── 0001_initial.py │ ├── 0008_auto_20161127_1907.py │ ├── 0004_auto_20160816_0109.py │ ├── 0002_auto_20160814_1053.py │ └── 0012_auto_20170417_1814.py ├── apps.py ├── queries.py ├── scripts │ ├── publish.py │ ├── pay_confirmed_bonuses.py │ └── playground.py ├── utils.py ├── admin.py ├── tests.py ├── models.py └── mturk_api.py ├── annotator ├── __init__.py ├── migrations │ ├── __init__.py │ ├── 0005_image_lists.py │ ├── 0006_video_rejected.py │ ├── 0003_video_verified.py │ ├── 0008_video_labels.py │ ├── 0009_auto_20170815_1341.py │ ├── 0004_add_label_model.py │ ├── 0002_auto_20160720_1851.py │ ├── 0001_initial.py │ ├── 0010_state.py │ └── 0007_auto_20170417_1814.py ├── static │ ├── videos │ │ └── .gitignore │ ├── vendor │ │ ├── .gitattributes │ │ ├── bootstrap │ │ │ └── fonts │ │ │ │ ├── glyphicons-halflings-regular.eot │ │ │ │ ├── glyphicons-halflings-regular.ttf │ │ │ │ ├── glyphicons-halflings-regular.woff │ │ │ │ └── glyphicons-halflings-regular.woff2 │ │ ├── imgplay │ │ │ └── jquery.imgplay.min.js │ │ └── randomColor │ │ │ └── randomColor.js │ ├── img │ │ └── loading.gif │ ├── app.js │ ├── .jshintrc │ ├── views │ │ ├── keyframebar.js │ │ ├── annotationbar.js │ │ └── framePlayers.js │ ├── bounds.js │ ├── app.css │ ├── README.md │ ├── datasources.js │ ├── annotation.js │ └── misc.js ├── apps.py ├── tests.py ├── templates │ ├── modals │ │ ├── instructions.html │ │ ├── generic_modal.html │ │ ├── delete_annotation_modal.html │ │ ├── edit_state_modal.html │ │ ├── edit_label_modal.html │ │ ├── email_form.html │ │ └── accept_reject_form.html │ ├── turk_ready_to_pay.html │ ├── video_list.html │ ├── base.html │ └── video.html ├── management │ └── commands │ │ ├── import_images_from_dir.py │ │ └── export_annotations.py ├── fixtures │ └── initialdata.yaml ├── admin.py ├── models.py ├── services.py └── views.py ├── beaverdam ├── __init__.py ├── wsgi.py ├── urls.py └── settings.py ├── logs └── .gitignore ├── requirements.txt ├── .gitignore ├── manage.py ├── scripts ├── convert-to-h264 ├── setup ├── segment_video ├── serve └── seed ├── deployment ├── README.md ├── nginx.conf ├── supervisor.conf └── playbook.yaml ├── LICENSE ├── default-instructions.md └── README.md /mturk/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /annotator/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /beaverdam/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /mturk/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /annotator/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /logs/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /annotator/static/videos/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore -------------------------------------------------------------------------------- /annotator/static/vendor/.gitattributes: -------------------------------------------------------------------------------- 1 | # Don't diff non-dotfiles in this directory 2 | * -diff 3 | .* diff 4 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Django==1.10 2 | sqlparse==0.2.2 3 | tqdm==4.8.4 4 | uWSGI==2.0.13.1 5 | markdown==2.6.7 6 | -------------------------------------------------------------------------------- /mturk/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class MturkConfig(AppConfig): 5 | name = 'mturk' 6 | -------------------------------------------------------------------------------- /annotator/static/img/loading.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/antingshen/BeaverDam/HEAD/annotator/static/img/loading.gif -------------------------------------------------------------------------------- /annotator/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class AnnotatorConfig(AppConfig): 5 | name = 'annotator' 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *db.sqlite3 2 | venv 3 | __pycache__ 4 | deploy_settings.py 5 | static 6 | s3cfg 7 | 8 | #virtuallenv files 9 | .Python 10 | bin/ 11 | include/ 12 | lib/ 13 | pip-selfcheck.json 14 | -------------------------------------------------------------------------------- /annotator/static/vendor/bootstrap/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/antingshen/BeaverDam/HEAD/annotator/static/vendor/bootstrap/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /annotator/static/vendor/bootstrap/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/antingshen/BeaverDam/HEAD/annotator/static/vendor/bootstrap/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /annotator/static/vendor/bootstrap/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/antingshen/BeaverDam/HEAD/annotator/static/vendor/bootstrap/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /annotator/static/vendor/bootstrap/fonts/glyphicons-halflings-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/antingshen/BeaverDam/HEAD/annotator/static/vendor/bootstrap/fonts/glyphicons-halflings-regular.woff2 -------------------------------------------------------------------------------- /beaverdam/wsgi.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from django.core.wsgi import get_wsgi_application 4 | 5 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "beaverdam.settings") 6 | 7 | application = get_wsgi_application() 8 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "beaverdam.settings") 7 | 8 | from django.core.management import execute_from_command_line 9 | 10 | execute_from_command_line(sys.argv) 11 | -------------------------------------------------------------------------------- /scripts/convert-to-h264: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | verbose() { 5 | echo $ "$@" 6 | "$@" 7 | } 8 | 9 | # Example for converting folder: 10 | # for i in *; do path/to/convert_to_h264.sh "$i"; done 11 | 12 | verbose ffmpeg -y -i "$1" -c:v libx264 /tmp/converting.mp4 13 | verbose mv /tmp/converting.mp4 "$1" 14 | -------------------------------------------------------------------------------- /scripts/setup: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | cd "$(echo "$0" | xargs dirname | xargs dirname)" 4 | 5 | verbose() { 6 | echo $ "$@" 7 | "$@" 8 | } 9 | 10 | verbose virtualenv -p python3 venv 11 | verbose . venv/bin/activate 12 | verbose pip3 install --upgrade pip 13 | verbose pip3 install --upgrade -r requirements.txt 14 | -------------------------------------------------------------------------------- /mturk/queries.py: -------------------------------------------------------------------------------- 1 | from mturk.models import FullVideoTask 2 | 3 | def get_active_video_turk_task(video_id): 4 | tasks = FullVideoTask.objects.filter(video__id = video_id, closed = False) 5 | 6 | if len(tasks) > 1: 7 | raise Exception('More than one full video task for video {}'.format(self.id)) 8 | elif len(tasks) == 1: 9 | return tasks[0] 10 | 11 | return None -------------------------------------------------------------------------------- /scripts/segment_video: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | verbose() { 5 | echo $ "$@" 6 | "$@" 7 | } 8 | 9 | # This assumes video is already in H264 10 | 11 | for f in "$@" do 12 | verbose ffmpeg \ 13 | -i "$f" \ 14 | -c copy \ 15 | -an \ 16 | -segment_time 30 \ 17 | -f segment \ 18 | -reset_timestamps 1 \ 19 | -segment_list "$f".list \ 20 | "$f"_%02d.mp4 21 | done 22 | -------------------------------------------------------------------------------- /scripts/serve: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | cd "$(echo "$0" | xargs dirname | xargs dirname)" 4 | 5 | verbose() { 6 | echo $ "$@" 7 | "$@" 8 | } 9 | 10 | if [ "$PORT" = "" ]; then 11 | PORT=5000 12 | fi 13 | 14 | verbose source venv/bin/activate 15 | 16 | if [ "$VIRTUAL_ENV" = "" ]; then 17 | echo "Must be inside virtual env" 18 | echo "To do so, run \"source venv/bin/activate\"" 19 | exit 1 20 | fi 21 | 22 | verbose ./manage.py runserver "0.0.0.0:$PORT" 23 | -------------------------------------------------------------------------------- /deployment/README.md: -------------------------------------------------------------------------------- 1 | Example config files for the recommended production toolchain are provided here. 2 | Tweaking is needed to get them working on your environment. 3 | 4 | - `playbook.yaml` contains an Ansible playbook to deploy BeaverDam. 5 | - `deploy_settings.py` contains example deploy settings file 6 | - `nginx.conf` is a sample nginx configuration using letsencrypt certs. Note SSL is required for MTurk 7 | - `supervisor.conf` is example supervisor conf for BeaverDam. 8 | 9 | See `playbook.yaml` on deployment instructions. 10 | -------------------------------------------------------------------------------- /mturk/scripts/publish.py: -------------------------------------------------------------------------------- 1 | from mturk.models import FullVideoTask 2 | from beaverdam import settings 3 | 4 | 5 | print(settings.MTURK_SANDBOX) 6 | 7 | def publish(arr): 8 | count = 0 9 | total = 0 10 | for x in arr: 11 | print("Publishing {}".format(x.video.filename)) 12 | if x.sandbox : 13 | x.sandbox = False; 14 | x.save() 15 | x.publish() 16 | 17 | arr = FullVideoTask.objects.filter(video__pk__in = [1541,1676,1774,1812,2010], video__verified = False) 18 | publish(arr) 19 | -------------------------------------------------------------------------------- /annotator/migrations/0005_image_lists.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10 on 2016-11-17 06:07 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', '0004_add_label_model'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AddField( 16 | model_name='video', 17 | name='image_list', 18 | field=models.TextField(blank=True), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /annotator/migrations/0006_video_rejected.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10 on 2016-11-28 03:07 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', '0005_image_lists'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AddField( 16 | model_name='video', 17 | name='rejected', 18 | field=models.BooleanField(default=False), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /annotator/migrations/0003_video_verified.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.9.7 on 2016-07-21 19:56 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', '0002_auto_20160720_1851'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AddField( 16 | model_name='video', 17 | name='verified', 18 | field=models.BooleanField(default=False), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /annotator/migrations/0008_video_labels.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10 on 2017-07-25 18:49 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', '0007_auto_20170417_1814'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AddField( 16 | model_name='video', 17 | name='labels', 18 | field=models.ManyToManyField(to='annotator.Label'), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /annotator/migrations/0009_auto_20170815_1341.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10 on 2017-08-15 20:41 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', '0008_video_labels'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name='video', 17 | name='labels', 18 | field=models.ManyToManyField(blank=True, to='annotator.Label'), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /annotator/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | from .models import Video 4 | 5 | class VideoTestCase(TestCase): 6 | def setUp(self): 7 | Video.objects.create(filename='test/video1') 8 | 9 | def test_count_keyframes(self): 10 | v = Video.objects.get(filename='test/video1') 11 | v.annotation = '{"color": "#137819", "keyframes": [{"h": 43.50453172205437, "frame": 0, "x": 919, "y": 694, "w": 84}], "type": "car"},]' 12 | self.assertEqual(v.count_keyframes(), 1) 13 | self.assertEqual(v.count_keyframes(at_time=0), 1) 14 | self.assertEqual(v.count_keyframes(at_time=0.5), 0) 15 | 16 | -------------------------------------------------------------------------------- /deployment/nginx.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | server_name {{ server_domain }}; 4 | return 301 https://$host$request_uri; 5 | } 6 | 7 | server { 8 | listen 443 ssl; 9 | 10 | server_name {{ server_domain }}; 11 | ssl_certificate /etc/letsencrypt/live/{{ server_domain }}/fullchain.pem; 12 | ssl_certificate_key /etc/letsencrypt/live/{{ server_domain }}/privkey.pem; 13 | 14 | add_header Strict-Transport-Security "max-age=31536000"; 15 | 16 | location /static { 17 | alias {{ path }}static; 18 | } 19 | 20 | location / { 21 | include uwsgi_params; 22 | uwsgi_pass 127.0.0.1:4242; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /annotator/templates/modals/instructions.html: -------------------------------------------------------------------------------- 1 | 15 | -------------------------------------------------------------------------------- /annotator/templates/modals/generic_modal.html: -------------------------------------------------------------------------------- 1 | 19 | -------------------------------------------------------------------------------- /annotator/static/app.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | 4 | $(() => { 5 | // Some sanity checks that can't be used 6 | 7 | // Firefox doesn't allow this 8 | // Misc.preventExtensions(document); 9 | 10 | // Chrome extensions rely on window being extensible 11 | // Misc.preventExtensions(window); 12 | 13 | // Make the player 14 | window.p = new Player({ 15 | $container: $("#player"), 16 | videoSrc: window.video.location, 17 | videoId: window.video.id, 18 | videoStart: window.video.start_time, 19 | videoEnd: window.video.end_time, 20 | isImageSequence: window.video.is_image_sequence, 21 | turkMetadata: window.video.turk_task, 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /mturk/migrations/0006_auto_20160821_2258.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10 on 2016-08-22 05:58 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', '0005_auto_20160816_1512'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AddField( 16 | model_name='fullvideotask', 17 | name='metrics', 18 | field=models.TextField(blank=True), 19 | ), 20 | migrations.AddField( 21 | model_name='singleframetask', 22 | name='metrics', 23 | field=models.TextField(blank=True), 24 | ), 25 | ] 26 | -------------------------------------------------------------------------------- /mturk/migrations/0010_auto_20161129_1806.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10 on 2016-11-30 02:06 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', '0009_auto_20161128_0119'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AddField( 16 | model_name='fullvideotask', 17 | name='email_trail', 18 | field=models.TextField(blank=True), 19 | ), 20 | migrations.AddField( 21 | model_name='singleframetask', 22 | name='email_trail', 23 | field=models.TextField(blank=True), 24 | ), 25 | ] 26 | -------------------------------------------------------------------------------- /annotator/migrations/0004_add_label_model.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10 on 2016-11-17 02:46 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', '0003_video_verified'), 12 | ] 13 | 14 | operations = [ 15 | migrations.CreateModel( 16 | name='Label', 17 | fields=[ 18 | ('id', models.AutoField(primary_key=True, serialize=False)), 19 | ('name', models.CharField(blank=True, max_length=100, unique=True)), 20 | ('color', models.CharField(blank=True, max_length=6)), 21 | ], 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /mturk/migrations/0005_auto_20160816_1512.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.9 on 2016-08-16 22:12 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', '0004_auto_20160816_0109'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AddField( 16 | model_name='fullvideotask', 17 | name='time_completed', 18 | field=models.DateTimeField(null=True), 19 | ), 20 | migrations.AddField( 21 | model_name='singleframetask', 22 | name='time_completed', 23 | field=models.DateTimeField(null=True), 24 | ), 25 | ] 26 | -------------------------------------------------------------------------------- /mturk/migrations/0003_auto_20160815_1525.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.9 on 2016-08-15 22:25 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', '0002_auto_20160814_1053'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AddField( 16 | model_name='fullvideotask', 17 | name='assignment_id', 18 | field=models.CharField(blank=True, max_length=64), 19 | ), 20 | migrations.AddField( 21 | model_name='singleframetask', 22 | name='assignment_id', 23 | field=models.CharField(blank=True, max_length=64), 24 | ), 25 | ] 26 | -------------------------------------------------------------------------------- /mturk/migrations/0007_auto_20161122_1349.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10 on 2016-11-22 21:49 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', '0006_auto_20160821_2258'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name='fullvideotask', 17 | name='time_completed', 18 | field=models.DateTimeField(blank=True, null=True), 19 | ), 20 | migrations.AlterField( 21 | model_name='singleframetask', 22 | name='time_completed', 23 | field=models.DateTimeField(blank=True, null=True), 24 | ), 25 | ] 26 | -------------------------------------------------------------------------------- /mturk/migrations/0011_auto_20161129_1859.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10 on 2016-11-30 02:59 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', '0010_auto_20161129_1806'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AddField( 16 | model_name='fullvideotask', 17 | name='last_email_sent_date', 18 | field=models.DateTimeField(blank=True, null=True), 19 | ), 20 | migrations.AddField( 21 | model_name='singleframetask', 22 | name='last_email_sent_date', 23 | field=models.DateTimeField(blank=True, null=True), 24 | ), 25 | ] 26 | -------------------------------------------------------------------------------- /mturk/migrations/0009_auto_20161128_0119.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10 on 2016-11-28 09:19 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', '0008_auto_20161127_1907'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name='fullvideotask', 17 | name='bonus', 18 | field=models.DecimalField(decimal_places=2, default=0, max_digits=4), 19 | ), 20 | migrations.AlterField( 21 | model_name='singleframetask', 22 | name='bonus', 23 | field=models.DecimalField(decimal_places=2, default=0, max_digits=4), 24 | ), 25 | ] 26 | -------------------------------------------------------------------------------- /scripts/seed: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | cd "$(echo "$0" | xargs dirname | xargs dirname)" 4 | 5 | verbose() { 6 | echo $ "$@" 7 | "$@" 8 | } 9 | 10 | if [[ "$1" != "-f" ]]; then 11 | echo "Existing data may be overwritten. Run as $0 -f to acknowledge this." 12 | exit 1 13 | fi 14 | 15 | verbose wget https://s3-us-west-2.amazonaws.com/beaverdam/db.sqlite3 -O db.sqlite3 16 | verbose wget https://s3-us-west-2.amazonaws.com/beaverdam/videos/test_vid2.mp4 -O annotator/static/videos/0.mp4 17 | 18 | ./manage.py migrate 19 | 20 | pip install pyyaml 21 | ./manage.py loaddata annotator/fixtures/initialdata.yaml 22 | 23 | echo "from django.contrib.auth.models import User; User.objects.create_superuser('test', 'test@test.com', 'password')" | python manage.py shell 24 | echo "Log in with username 'test' and password 'password'" 25 | -------------------------------------------------------------------------------- /annotator/templates/turk_ready_to_pay.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block body %} 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | {% for video_task in tasks %} 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | {% endfor %} 24 | 25 |
Video IdHit IdCalculated BonusFileLink
{{ video_task.video.id }}{{ video_task.hit_id }}{{ video_task.bonus }}{{ video_task.filename }}View Video | View Task
26 | {% endblock %} 27 | -------------------------------------------------------------------------------- /annotator/migrations/0002_auto_20160720_1851.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.9.7 on 2016-07-21 01:51 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', '0001_initial'), 12 | ] 13 | 14 | operations = [ 15 | migrations.RenameField( 16 | model_name='video', 17 | old_name='url', 18 | new_name='host', 19 | ), 20 | migrations.RemoveField( 21 | model_name='video', 22 | name='name', 23 | ), 24 | migrations.AddField( 25 | model_name='video', 26 | name='filename', 27 | field=models.CharField(blank=True, max_length=100, unique=True), 28 | ), 29 | ] 30 | -------------------------------------------------------------------------------- /annotator/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.9 on 2016-07-11 23:25 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | initial = True 11 | 12 | dependencies = [ 13 | ] 14 | 15 | operations = [ 16 | migrations.CreateModel( 17 | name='Video', 18 | fields=[ 19 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 20 | ('name', models.CharField(blank=True, max_length=100)), 21 | ('annotation', models.TextField(blank=True)), 22 | ('source', models.CharField(blank=True, max_length=1048)), 23 | ('url', models.CharField(blank=True, max_length=1048)), 24 | ], 25 | ), 26 | ] 27 | -------------------------------------------------------------------------------- /annotator/templates/modals/delete_annotation_modal.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /annotator/templates/modals/edit_state_modal.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /annotator/migrations/0010_state.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10 on 2018-01-09 10:33 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', '0009_auto_20170815_1341'), 13 | ] 14 | 15 | operations = [ 16 | migrations.CreateModel( 17 | name='State', 18 | fields=[ 19 | ('id', models.AutoField(primary_key=True, serialize=False)), 20 | ('name', models.CharField(blank=True, help_text='Name of class label option.', max_length=100)), 21 | ('color', models.CharField(blank=True, help_text='6 digit hex.', max_length=6)), 22 | ('label_name', models.ForeignKey(blank=True, on_delete=django.db.models.deletion.CASCADE, to='annotator.Label', to_field='name')), 23 | ], 24 | ), 25 | ] 26 | -------------------------------------------------------------------------------- /annotator/static/.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "node": true, 3 | "esnext": true, 4 | "bitwise": true, 5 | "camelcase": true, 6 | // "curly": true, 7 | "eqeqeq": false, 8 | "eqnull": true, 9 | "immed": true, 10 | "indent": 4, 11 | "latedef": true, 12 | "newcap": false, 13 | "noarg": true, 14 | "predef": [ 15 | // browser 16 | "document", 17 | "window", 18 | "navigator", 19 | "Headers", 20 | "fetch", 21 | 22 | // plugins 23 | "$", 24 | "_", 25 | "Raphael", 26 | 27 | // app 28 | "Annotation", 29 | "Bounds", 30 | "CreationRect", 31 | "DataSources", 32 | "Misc", 33 | "Player", 34 | 35 | // views 36 | "Keyframebar", 37 | "PlayerView", 38 | "Rect", 39 | "VideoFramePlayer", 40 | "AbstractFramePlayer", 41 | "ImageFramePlayer" 42 | ], 43 | // "quotmark": "double", 44 | "regexp": true, 45 | "undef": true, 46 | "unused": true, 47 | "strict": true, 48 | "trailing": true, 49 | "smarttabs": true, 50 | "white": true 51 | } 52 | -------------------------------------------------------------------------------- /mturk/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.9 on 2016-07-11 23:25 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 | initial = True 12 | 13 | dependencies = [ 14 | ('annotator', '0001_initial'), 15 | ] 16 | 17 | operations = [ 18 | migrations.CreateModel( 19 | name='FullVideoTask', 20 | fields=[ 21 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 22 | ('hit_id', models.CharField(blank=True, max_length=64)), 23 | ('hit_group', models.CharField(blank=True, max_length=64)), 24 | ('video', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='annotator.Video')), 25 | ], 26 | options={ 27 | 'abstract': False, 28 | }, 29 | ), 30 | ] 31 | -------------------------------------------------------------------------------- /mturk/migrations/0008_auto_20161127_1907.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10 on 2016-11-28 03:07 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', '0007_auto_20161122_1349'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AddField( 16 | model_name='fullvideotask', 17 | name='closed', 18 | field=models.BooleanField(default=False), 19 | ), 20 | migrations.AddField( 21 | model_name='fullvideotask', 22 | name='message', 23 | field=models.CharField(blank=True, max_length=256), 24 | ), 25 | migrations.AddField( 26 | model_name='singleframetask', 27 | name='closed', 28 | field=models.BooleanField(default=False), 29 | ), 30 | migrations.AddField( 31 | model_name='singleframetask', 32 | name='message', 33 | field=models.CharField(blank=True, max_length=256), 34 | ), 35 | ] 36 | -------------------------------------------------------------------------------- /mturk/utils.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | 3 | from .models import Task 4 | 5 | def authenticate_hit(request): 6 | assignment_id = request.GET.get('assignmentId', None) or request.GET.get('mturk', None) 7 | if assignment_id == 'ASSIGNMENT_ID_NOT_AVAILABLE': 8 | preview = True 9 | assignment_id = None 10 | else: 11 | preview = bool(request.GET.get('preview', False)) 12 | 13 | hit_id = request.GET.get('hitId', '') 14 | worker_id = request.GET.get('workerId', '') 15 | if not preview: 16 | if assignment_id is not None: 17 | if not Task.valid_hit_id(hit_id): 18 | # TODO: Log this error 19 | return {'error': 'No HIT found with ID {}. Please return this HIT. Sorry for the inconvenience.'.format(hit_id)} 20 | else: 21 | return {'authenticated': False, 'preview': False} 22 | 23 | return { 24 | 'authenticated': True, 25 | 'preview': preview, 26 | 'assignment_id': assignment_id, 27 | 'hit_id': hit_id, 28 | 'worker_id': worker_id, 29 | 'sandbox': settings.MTURK_SANDBOX, 30 | } 31 | -------------------------------------------------------------------------------- /annotator/templates/modals/edit_label_modal.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /mturk/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from .models import * 3 | import logging 4 | 5 | 6 | logger = logging.getLogger() 7 | 8 | 9 | def recalculate_bonus(modeladmin, request, videos): 10 | logger.error("racalcing bonuses") 11 | for video_task in videos: 12 | logger.error("calc bonus for {}".format(video_task.id)) 13 | video_task.bonus = video_task.calculate_bonus() 14 | logger.error("new bonus is {}".format(video_task.bonus)) 15 | video_task.save() 16 | 17 | class FullVideoTaskAdmin(admin.ModelAdmin): 18 | list_display =('id','video_url','worker_id', 'hit_id', 'bonus', 'closed', 'paid', 'last_email_sent_date') 19 | search_fields=['id', 'worker_id', 'hit_id', 'assignment_id'] 20 | list_filter=['paid', 'closed', 'worker_id'] 21 | actions=[recalculate_bonus] 22 | 23 | def video_url(self, obj): 24 | return '/video/{}/'.format(obj.video.id, obj.video.id) 25 | video_url.allow_tags = True 26 | video_url.short_description = 'Video' 27 | 28 | 29 | admin.site.register(FullVideoTask, FullVideoTaskAdmin) 30 | admin.site.register(SingleFrameTask) 31 | -------------------------------------------------------------------------------- /annotator/templates/video_list.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block head %} 4 | 5 | {% endblock %} 6 | 7 | {% block body %} 8 | 9 |
10 |

{{title}}

11 |

{{videos|length}} videos

12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | {% for video in videos %} 25 | 26 | 27 | 28 | 29 | 30 | {% if video.image_list %} 31 | 32 | {% else %} 33 | 34 | {% endif %} 35 | 36 | 37 | {% endfor %} 38 | 39 |
IdAnnotationsVerifiedFileTypeLink
{{ video.id }}{{ video.annotation|yesno }}{{ video.verified|yesno }}{{ video.filename }}SequenceVideoview
40 |
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 | 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 | ![](https://raw.githubusercontent.com/wiki/xysense/BeaverDam/images/good_1.png) 15 | ![](https://raw.githubusercontent.com/wiki/xysense/BeaverDam/images/good_2.png) 16 | ### Bad: 17 | ![](https://raw.githubusercontent.com/wiki/xysense/BeaverDam/images/bad_1.png) 18 | ![](https://raw.githubusercontent.com/wiki/xysense/BeaverDam/images/bad_2.png) 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 | ` 8 | 9 | `, 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 | 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 | `
`, 7 | }; 8 | 9 | class Annotationbar { 10 | constructor({classBaseName}) { 11 | // This container of the annotation bar 12 | this.$container = null; 13 | 14 | // Namespaced className generator 15 | this.classBaseName = classBaseName.add('annotationbar'); 16 | 17 | // Duration of the video 18 | this.duration = 0; 19 | 20 | // Prevent adding new properties 21 | $(this).on('dummy', $.noop); 22 | Object.preventExtensions(this, Annotationbar); 23 | } 24 | 25 | attach($container) { 26 | // Don't add twice 27 | if (this.$container != null) { 28 | throw new Error("Annotationbar.attach: already attached to container"); 29 | } 30 | 31 | // Actually do the attaching 32 | this.$container = $container; 33 | 34 | // Apply appearance 35 | this.resetWithDuration(0); 36 | 37 | // Trigger event 38 | $(this).triggerHandler('attach', this.$container); 39 | 40 | return this; 41 | } 42 | 43 | detach() { 44 | this.$container.empty(); 45 | 46 | // Trigger event 47 | $(this).triggerHandler('detach', this.$container); 48 | 49 | this.$container = undefined; 50 | 51 | return this; 52 | } 53 | 54 | resetWithDuration(duration) { 55 | this.$container.empty(); 56 | 57 | this.duration = duration; 58 | } 59 | 60 | addAnnotation(annotation, classNameExtBooleans) { 61 | var classBaseName = this.classBaseName.add('annotation'); 62 | var classNames = Misc.getClassNamesFromExts([classBaseName], classBaseName, classNameExtBooleans); 63 | 64 | var id = 'collapse' + annotation.id; 65 | var selected = classNameExtBooleans.selected ? ' in" aria-expanded="true' : ''; 66 | 67 | var html = $.parseHTML( 68 | '
' 69 | + '
' 70 | + '

' 71 | + '' 72 | + annotation.type 73 | + '' 74 | + '

' 75 | + '
' 76 | + '
' 77 | + '
' 78 | + '
' 79 | + '
'); 80 | 81 | var editLabel = $("") 83 | .click(() => { $(this).triggerHandler('control-edit-label', {"annotation": annotation}); }); 84 | 85 | var deleteAnnotation = $("") 87 | .click(() => { $(this).triggerHandler('control-delete-annotation', {"annotation": annotation}); }); 88 | 89 | $(html).find('h4').append(editLabel); 90 | $(html).find('h4').append(deleteAnnotation); 91 | 92 | var keyframesList = $(html).find("ul"); 93 | 94 | for (let keyframe of annotation.keyframes) { 95 | var editState = $("") 97 | .click(() => { $(this).triggerHandler('control-edit-state', {"annotation": annotation, "keyframe": keyframe}); }); 98 | 99 | var link = $("
  • " + keyframe.time + ": " + keyframe.state + "
  • "); 100 | $(link).find('a').click(() => { $(this).triggerHandler('jump-to-time', keyframe.time); }); 101 | 102 | $(keyframesList).append(link); 103 | $(keyframesList).find("li:last").append(editState); 104 | } 105 | 106 | $(html).addClass(classNames.join(' ')).appendTo(this.$container); 107 | } 108 | } 109 | 110 | // Mix-in constants 111 | Misc.mixinClassConstants(Annotationbar, AnnotationbarConstants); 112 | void Annotationbar; 113 | -------------------------------------------------------------------------------- /beaverdam/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for beaverdam project. 3 | 4 | Generated by 'django-admin startproject' using Django 1.9.7. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.9/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/1.9/ref/settings/ 11 | """ 12 | 13 | import os 14 | 15 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 16 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 17 | 18 | # Use different key for production 19 | SECRET_KEY = '8pje5%pxibt2c=&j_c+ly5v@x)$r77%h-x3%jluq-@)4^75)ak' 20 | DEBUG = True 21 | 22 | HELP_URL = os.environ.get('HELP_URL', 'https://raw.githubusercontent.com/antingshen/BeaverDam/master/default-instructions.md') 23 | # this will show in a popup instead of the external HELP_URL 24 | HELP_USE_MARKDOWN = True 25 | HELP_EMBED = True 26 | URL_ROOT = os.environ.get('URL_ROOT', 'url_root') 27 | AWS_ID = os.environ.get('AWS_ID', 'aws_id') 28 | AWS_KEY = os.environ.get('AWS_KEY', 'aws_key') 29 | 30 | MTURK_TITLE = "Video annotation" 31 | MTURK_DESCRIPTION = "Draw accurate boxes around every person in the video, we will pay a $0.02 bonus per accurate box drawn. Most of the payment is in the bonus" 32 | MTURK_SANDBOX = True 33 | MTURK_BONUS_MESSAGE = "Thanks for your work" 34 | MTURK_REJECTION_MESSAGE = "Your work has not been accepted. You must follow the instructions of the task precisely to complete this task." 35 | MTURK_BLOCK_MESSAGE = "I'm sorry but we have blocked you from working on our HITs. We have limited time and unfortunately your work accuracy was not up to the standards required." 36 | MTURK_BONUS_PER_BOX = 0.02 37 | MTURK_BASE_PAY = 0.04 38 | MTURK_EMAIL_SUBJECT = "Question about your work" 39 | MTURK_EMAIL_MESSAGE = """Thanks for your submission. 40 | 41 | Unfortunately, we're not able to accept this work as it does not meet the standards required. 42 | 43 | If you'd like to have another go at it, can you please carefully read the instructions and make sure you enter information for the entire video. 44 | 45 | Otherwise, we will reject the task in 24 hours. 46 | 47 | Please let us know if you've encountered any problems. 48 | 49 | Regards 50 | """ 51 | 52 | ALLOWED_HOSTS=["*"] 53 | 54 | assert MTURK_SANDBOX or not DEBUG 55 | 56 | # Application definition 57 | 58 | INSTALLED_APPS = [ 59 | 'annotator', 60 | 'mturk', 61 | 'django.contrib.admin', 62 | 'django.contrib.auth', 63 | 'django.contrib.contenttypes', 64 | 'django.contrib.sessions', 65 | 'django.contrib.messages', 66 | 'django.contrib.staticfiles', 67 | ] 68 | 69 | MIDDLEWARE_CLASSES = [ 70 | 'django.middleware.security.SecurityMiddleware', 71 | 'django.contrib.sessions.middleware.SessionMiddleware', 72 | 'django.middleware.common.CommonMiddleware', 73 | 'django.middleware.csrf.CsrfViewMiddleware', 74 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 75 | 'django.contrib.auth.middleware.SessionAuthenticationMiddleware', 76 | 'django.contrib.messages.middleware.MessageMiddleware', 77 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 78 | ] 79 | 80 | ROOT_URLCONF = 'beaverdam.urls' 81 | 82 | TEMPLATES = [ 83 | { 84 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 85 | 'DIRS': [], 86 | 'APP_DIRS': True, 87 | 'OPTIONS': { 88 | 'context_processors': [ 89 | 'django.template.context_processors.debug', 90 | 'django.template.context_processors.request', 91 | 'django.contrib.auth.context_processors.auth', 92 | 'django.contrib.messages.context_processors.messages', 93 | ], 94 | }, 95 | }, 96 | ] 97 | 98 | WSGI_APPLICATION = 'beaverdam.wsgi.application' 99 | 100 | 101 | # Database 102 | # https://docs.djangoproject.com/en/1.9/ref/settings/#databases 103 | 104 | DATABASES = { 105 | 'default': { 106 | 'ENGINE': 'django.db.backends.sqlite3', 107 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 108 | } 109 | } 110 | 111 | 112 | # Password validation 113 | # https://docs.djangoproject.com/en/1.9/ref/settings/#auth-password-validators 114 | 115 | AUTH_PASSWORD_VALIDATORS = [ 116 | { 117 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 118 | }, 119 | { 120 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 121 | }, 122 | { 123 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 124 | }, 125 | ] 126 | 127 | LOGIN_REDIRECT_URL = '/' 128 | LOGIN_URL = '/login/' 129 | 130 | CSRF_COOKIE_SECURE = not DEBUG 131 | 132 | 133 | # Internationalization 134 | # https://docs.djangoproject.com/en/1.9/topics/i18n/ 135 | 136 | LANGUAGE_CODE = 'en-us' 137 | 138 | TIME_ZONE = 'America/Los_Angeles' 139 | 140 | USE_I18N = True 141 | 142 | USE_L10N = True 143 | 144 | USE_TZ = True 145 | 146 | 147 | # Static files (CSS, JavaScript, Images) 148 | # https://docs.djangoproject.com/en/1.9/howto/static-files/ 149 | 150 | STATIC_URL = '/static/' 151 | 152 | STATIC_ROOT = os.path.join(BASE_DIR, "annotator/static/") 153 | 154 | try: 155 | from beaverdam.deploy_settings import * 156 | except ImportError as e: 157 | pass 158 | -------------------------------------------------------------------------------- /annotator/static/app.css: -------------------------------------------------------------------------------- 1 | .panel-body { 2 | display: flex; 3 | flex-direction: column; 4 | height: 100%; 5 | padding: 0; 6 | } 7 | 8 | .top-controls { 9 | padding: 15px 15px 15px 20px; 10 | } 11 | 12 | .bottom-controls { 13 | margin-bottom: 0; 14 | border: 0; 15 | border-radius: 0; 16 | border-top-left-radius: 0; 17 | border-top-right-radius: 0; 18 | } 19 | 20 | /* Player */ 21 | 22 | .player-container { 23 | margin: 0; 24 | border-width: 0px; 25 | text-align: left; 26 | } 27 | 28 | .player-screen { 29 | margin: 0; 30 | flex-grow: 1; 31 | display: flex; 32 | flex-direction: column; 33 | justify-content: center; 34 | position: relative; 35 | text-align: center; 36 | flex-basis: 200px; 37 | overflow-y: scroll; 38 | } 39 | 40 | .player-video-container { 41 | display: flex; 42 | } 43 | 44 | .player-video { 45 | position: absolute; 46 | top: 0; 47 | } 48 | 49 | .player-video canvas { 50 | background-color: #fff; 51 | } 52 | 53 | .player-video img { 54 | display: none; 55 | } 56 | 57 | div.imgplay { 58 | padding-bottom: 0; 59 | background: #fff; 60 | } 61 | 62 | .player-paper { 63 | position: absolute; 64 | top: 0; 65 | bottom: 0; 66 | left: 0; 67 | right: 0; 68 | } 69 | 70 | .player-loader { 71 | position: absolute; 72 | display: flex; 73 | justify-content: center; 74 | top: 0; 75 | left: 0; 76 | bottom: 0; 77 | right: 0; 78 | background-color: #000; 79 | opacity: 0.7; 80 | display: none; 81 | align-items: center; 82 | } 83 | 84 | .player-loader > div { 85 | width: 32px; 86 | height: 32px; 87 | background-color: #fff; 88 | 89 | border-radius: 100%; 90 | display: inline-block; 91 | animation: sk-bouncedelay 1.4s infinite ease-in-out both; 92 | } 93 | 94 | .player-loader .bounce1 { 95 | animation-delay: -0.32s; 96 | } 97 | 98 | .player-loader .bounce2 { 99 | animation-delay: -0.16s; 100 | } 101 | 102 | @keyframes sk-bouncedelay { 103 | 0%, 80%, 100% { 104 | transform: scale(0); 105 | } 40% { 106 | transform: scale(1.0); 107 | } 108 | } 109 | 110 | .player-keyframebar { 111 | position: relative; 112 | margin-left: 6px; 113 | margin-right: 6px; 114 | height: 2.5em; 115 | } 116 | .player-keyframebar-keyframe { 117 | position: absolute; 118 | color: orange; 119 | width: auto; 120 | height: 1.4em; 121 | top: 0; 122 | margin-left: -.7em; 123 | } 124 | .player-keyframebar-keyframe-noselected { 125 | top: 1.1em; 126 | opacity: 0.3; 127 | } 128 | .player-keyframebar-keyframe-noselected:hover { 129 | opacity: 0.7; 130 | } 131 | .player-keyframebar-keyframe circle { 132 | stroke: #902B00; 133 | stroke-width: 7px; 134 | fill: #FF8000; 135 | } 136 | .player-keyframebar-keyframe:hover:not(:active) circle { 137 | fill: #C25305; 138 | } 139 | 140 | .player-rect-normal { 141 | stroke: white; 142 | stroke-width: 1px; 143 | } 144 | .player-rect-selected { 145 | stroke-opacity: 1; 146 | fill-opacity: 0.2; 147 | } 148 | .player-rect-noselected { 149 | stroke-opacity: 0.5; 150 | fill-opacity: 0.5; 151 | } 152 | .player-rect-real { 153 | stroke-dasharray: none; 154 | } 155 | .player-rect-noreal { 156 | stroke-dasharray: 8, 4; 157 | } 158 | .player-rect-noselected.player-rect-noreal.player-rect-nosinglekeyframe { 159 | display: none; 160 | } 161 | 162 | .player-rect-active { 163 | fill: black; 164 | fill-opacity: 0.3; 165 | opacity: 0.5; 166 | stroke: white; 167 | stroke-opacity: 1; 168 | stroke-width: 1px; 169 | } 170 | .player-rect-noactive { 171 | fill: transparent; 172 | stroke: transparent; 173 | stroke-width: 0; 174 | opacity: 0; 175 | } 176 | 177 | .list-group-item { 178 | cursor: pointer; 179 | } 180 | 181 | .control-edit-label, .control-delete-annotation, .control-edit-state { 182 | float: right; 183 | } 184 | 185 | .control-delete-annotation { 186 | padding-right: 10px; 187 | } 188 | 189 | /* Uncategorized rules below */ 190 | 191 | #left-container { 192 | padding-left: 0px; 193 | margin-left: 0px; 194 | width: 841px; 195 | display: inline-block; 196 | 197 | margin-top: 0; 198 | } 199 | 200 | #frame-number { 201 | display: inline-block; 202 | width: 60px; 203 | } 204 | 205 | #submit-container { 206 | display: inline-block; 207 | position: absolute; 208 | margin-left: 300px; 209 | top: 10px; 210 | right: 20px; 211 | } 212 | 213 | .glyphicon:hover { 214 | color:#AAAAAA; 215 | } 216 | 217 | /* stop the control buttons from being selectable*/ 218 | .noselect { 219 | -webkit-touch-callout: none; /* iOS Safari */ 220 | -webkit-user-select: none; /* Chrome/Safari/Opera */ 221 | -khtml-user-select: none; /* Konqueror */ 222 | -moz-user-select: none; /* Firefox */ 223 | -ms-user-select: none; /* Internet Explorer/Edge */ 224 | user-select: none; /* Non-prefixed version, currently 225 | not supported by any browser */ 226 | } 227 | 228 | #instructionModal { 229 | overflow: scroll; 230 | } 231 | 232 | #instructionModal div.modal-dialog { 233 | width: 80%; 234 | } 235 | 236 | #instructionModal .modal-body { 237 | margin: 0 2rem; 238 | } 239 | 240 | .row { 241 | padding-top:4px; 242 | padding-bottom: 4px; 243 | } 244 | 245 | .modal-content { 246 | min-width: 760px; 247 | } 248 | 249 | .video-list { 250 | padding: 0 50px 25px 50px; 251 | } 252 | 253 | .video-list h3 { 254 | font-weight: 300; 255 | font-size: 18px; 256 | } -------------------------------------------------------------------------------- /annotator/static/README.md: -------------------------------------------------------------------------------- 1 | Front-end hacking guide 2 | ======================= 3 | 4 | Setting up a linter 5 | ------------------- 6 | 7 | Please set up a linter first. A linter is a program that checks your files for correct style and syntax – it's like spellcheck and grammar check for your code, and you'll be warned of many common errors before you even save your file. 8 | 9 | Instructions for Sublime Text 3: 10 | 11 | 1. Install [Package Control](https://packagecontrol.io/installation) 12 | 2. Tools » Command Palette » Install Package » `SublimeLinter` 13 | 3. Tools » Command Palette » Install Package » `SublimeLinter-jshint` 14 | 4. Make sure you have Node.js installed. 15 | If not: `brew install node` (Mac) or [read here](https://nodejs.org/en/download/package-manager/#debian-and-ubuntu-based-linux-distributions) (Ubuntu) 16 | 4. `npm install -g jshint` 17 | 18 | 19 | 20 | Organization of JS files in `static` 21 | ------------------------------------ 22 | 23 | | Filename | Class | Purpose | 24 | | :-------- | :----- | :------- | 25 | | annotation.js | `Annotation` | Annotation model. This is an object that sits there and does noannotation. It does not know about any of the views. It cannot do anyannotation on its own. However, when you change its keyframes, it will trigger an event that you can listen to. | 26 | | app.js | | Starts the app. | 27 | | bounds.js | `Bounds` | `Bounds` indicates the position and dimensions of a rectangle. This file has a spec for `Bounds` objects and has functions for manipulating bounds. | 28 | | datasources.js | `DataSources` | Functions for interacting with external data sources (URLs and JSON). | 29 | | misc.js | `Misc` | Utility functions that aren't deeply related to this app. | 30 | | player.js | `Player` | Player controller code. Binds together `Annotation` and `PlayerView`. | 31 | | views/ | | Here lie the views. Views show annotations and maintain some internal state (a scrubber maintains its current position). They do not interact with anyannotation other than sub-views, but they trigger events that you can listen to. | 32 | | views/keyframebar.js | `Keyframebar` | Displays keyframes. Clicking a keyframe triggers an event. | 33 | | views/annotationbar.js | `Annotationbar` | Displays annotations as a list. The annotations can be edited. | 34 | | views/player.js | `PlayerView` | View for the entire player. It comes with a `Keyframebar`, a `CreationRect`, and has methods for adding `Rect`s. | 35 | | views/rect.js | `CreationRect` | A special rectangle/box that normally sits behind all the other rects. When it is dragged, it changes shape to form a new rect. When the drag is finished, it triggers and event and reverts to its initial state. | 36 | | views/rect.js | `Rect` | A rectangle/box on-screen. | 37 | | views/framePlayers.js | `AbstractFramePlayer` | An abstract class that represents a video player. Has events and methods such as pause and play | 38 | | views/framePlayers.js | `VideoFramePlayer` | A concrete implementation of AbstractFramePlayer linking to a