diff --git a/README.md b/README.md index dde2bad..e26f7f3 100755 --- a/README.md +++ b/README.md @@ -1,11 +1,27 @@ # video-labeling-tool Demo: http://smoke.createlab.org -A tool for labeling video clips (both front-end and back-end). The back-end depends on a [thumbnail server](https://github.com/CMU-CREATE-Lab/timemachine-thumbnail-server) to provides video urls. The back-end is based on [flask](http://flask.pocoo.org/). A flask tutorial can be found on [this blog](https://blog.miguelgrinberg.com/post/the-flask-mega-tutorial-part-i-hello-world). +A tool for labeling video clips (both front-end and back-end). The back-end depends on the [thumbnail server](https://github.com/CMU-CREATE-Lab/timemachine-thumbnail-server) to provide video urls. The back-end is based on [flask](http://flask.pocoo.org/). A flask tutorial can be found on [this blog](https://blog.miguelgrinberg.com/post/the-flask-mega-tutorial-part-i-hello-world). This tool is tested and worked on: +- macOS Mojave + - Chrome 76 + - Safari 12 + - Firefox 68 +- Windows 10 + - Chrome 76 + - Firefox 68 + - Edge 44 +- Android 8 + - Chrome 76 + - Firefox 68 +- iOS 12 + - Chrome 76 + - Safari 12 + - Firefox 18 ### Table of Content - [Install MySQL](#install-mysql) - [Setup back-end](#setup-back-end) +- [Prepare gold standards for quality check](#prepare-gold-standards) - [Dump and import MySQL database](#dump-and-import-mysql) - [Deploy back-end using uwsgi](#deploy-back-end-using-uwsgi) - [Connect uwsgi to apache](#connect-uwsgi-to-apache) @@ -79,10 +95,15 @@ echo 'export PATH="/usr/local/miniconda3/bin:$PATH"' >> ~/.bash_profile echo '. /usr/local/miniconda3/etc/profile.d/conda.sh' >> ~/.bash_profile source ~/.bash_profile ``` -Clone this repository. +Clone this repository and set the permission. ```sh git clone https://github.com/CMU-CREATE-Lab/video-labeling-tool.git -sudo chown -R $USER video-labeling-tool +sudo chown -R $USER video-labeling-tool/ +sudo addgroup [group_name] +sudo usermod -a -G [group_name] [user_name] +groups [user_name] +sudo chmod -R 775 video-labeling-tool/ +sudo chgrp -R [group_name] video-labeling-tool/ ``` Create conda environment and install packages. It is important to install pip first inside the newly created conda environment. ```sh @@ -141,6 +162,20 @@ Run server in the conda environment for development purpose. sh development.sh ``` +# Prepare gold standards for quality check +The system uses gold standards (videos with known labels) to check the quality of each labeled batch. If a user did not label the gold standards correctly, the corresponding batch would be discarded. Initially, there are no gold standards, and the backend will not return videos for labeling. To solve this issue, give yourself the researcher permission by using +```sh +python set_client_type.py [user_id] 0 +``` +where user_id can be found on the "Account" tab on the top right of the "label.html" page after logging in with Google. The number 0 that follows the user_id is the researcher permission. For more information about the permission, please refer to the client_type variable in the "User" class in the "application.py" file. The system will not run the quality check for users with the researcher permission. In this way, you can start labeling first. + +To assign gold standards videos, go to the "gallery.html" page when logging in with the account that has the researcher permission. On the gallery, you will find "P*" and "N*" buttons. Clicking on these buttons shows the positive and negative videos that the researcher labeled. You can now use the dropdown below each video to change the label to Gold Pos (positive gold standards) or Gold Neg (negative gold standards). Once there is a sufficient number of gold standards (more than 4), normal users will be able to label videos. I recommend having at least 100 gold standards to start. + +If you found that some videos are not suitable for labeling (e.g., due to incorrect image stitching), you can get the url of the video and use the following command to mark similar ones (with the same date and bounding box) as "bad" videos. This process does not remove videos. Instead it gives all bad videos a label state -2. +```sh +python set_client_type.py [video_url] +``` + # Dump and import MySQL database This section assumes that you want to dump the production database to a file and import it to the development database. First, SSH to the production server and dump the database to the /tmp/ directory. ```sh @@ -244,7 +279,7 @@ sudo vim /etc/apache2/sites-available/[BACK_END_DOMAIN].conf ServerName [BACK_END_DOMAIN] Header always set Access-Control-Allow-Origin "http://[FRONT_END_DOMAIN]" Header set Access-Control-Allow-Headers "Content-Type" - Header set Cache-Control "max-age=60, public, must-revalidate" + Header set Cache-Control "max-age=5, public, must-revalidate" ProxyPreserveHost On ProxyRequests Off ProxyVia Off @@ -270,7 +305,7 @@ sudo vim /etc/apache2/sites-available/[FRONT_END_DOMAIN].conf ServerName [FRONT_END_DOMAIN] DocumentRoot /[PATH]/video-labeling-tool/front-end Header always set Access-Control-Allow-Origin "*" - Header set Cache-Control "max-age=60, public, must-revalidate" + Header set Cache-Control "max-age=5, public, must-revalidate" Options FollowSymLinks AllowOverride None @@ -280,7 +315,7 @@ sudo vim /etc/apache2/sites-available/[FRONT_END_DOMAIN].conf CustomLog ${APACHE_LOG_DIR}/[FRONT_END_DOMAIN].access.log combined ``` -Use the following if you only want to access the server from an IP address with a port. Remember to tell the apache server to listen to the port number. +Use the following if you only want to access the server from an IP address with a port (e.g., http://192.168.1.72:8080). Remember to tell the apache server to listen to the port number. ```sh sudo vim /etc/apache2/sites-available/video-labeling-tool-front-end.conf # Add the following lines to this file @@ -314,8 +349,14 @@ Give permissions so that the Certbot and apache can modify the website. This ass cd /var/www/ sudo mkdir html # only run this if the html directory did not exist sudo chmod 775 html -sudo chgrp www-data html -sudo chgrp www-data [CLONED_REPOSITORY] +sudo chmod 775 [CLONED_REPOSITORY] +sudo chgrp -R www-data html +sudo chgrp -R www-data [CLONED_REPOSITORY] +``` +If other users need to modify this repository, add them to the www-data group. +```sh +sudo usermod -a -G www-data [user_name] +groups [user_name] ``` Run the Certbot. ```sh @@ -338,7 +379,7 @@ sudo vim /etc/apache2/sites-available/[BACK_END_DOMAIN].conf Header always set Access-Control-Allow-Origin "https://[FRONT_END_DOMAIN]" Header set Access-Control-Allow-Headers "Content-Type" # The following line forces the browser to break the cache - Header set Cache-Control "max-age=60, public, must-revalidate" + Header set Cache-Control "max-age=5, public, must-revalidate" # Reverse proxy to the uwsgi server ProxyPreserveHost On ProxyRequests Off @@ -374,7 +415,7 @@ sudo vim /etc/apache2/sites-available/[FRONT_END_DOMAIN].conf # The following line enables cors Header always set Access-Control-Allow-Origin "*" # The following line forces the browser to break the cache - Header set Cache-Control "max-age=60, public, must-revalidate" + Header set Cache-Control "max-age=5, public, must-revalidate" Options FollowSymLinks AllowOverride None @@ -612,3 +653,23 @@ $.ajax({ # curl example curl -d 'user_token=your_user_token' -H 'Content-Type: application/x-www-form-urlencoded; charset=UTF-8' -X POST http://localhost:5000/api/v1/get_all_labels ``` +### Get the statistics of labels +Get the number of all videos (excluding the videos that were marked as "bad" data), the number of fully labeled videos (confirmed by multiple users), and the number of partially labeled videos. +- Paths: + - **/api/v1/get_label_statistics** +- Available methods: + - GET +- Returned fields: + - "num_all_videos": number of all videos (excluding bad data) + - "num_fully_labeled": number of fully labeled videos + - "num_partially_labeled": number of partially labeled videos +```JavaScript +// jQuery examples +$.getJSON("http://localhost:5000/api/v1/get_label_statistics", function (data) { + console.log(data); +}); +``` +```sh +# curl example +curl http://localhost:5000/api/v1/get_label_statistics +``` diff --git a/TODO b/TODO new file mode 100644 index 0000000..41f17e8 --- /dev/null +++ b/TODO @@ -0,0 +1,35 @@ +TODO: add interactive tutorial + +TODO: design user feedback system after labeling a batch (e.g., dialog box) + - poor performance when labels are rejected + - if the user made too many bad batches, ask the user to retake the tutorial + - detect and mark spamming (e.g., all not selected, all selected) in the database + +TODO: design features for sharing achievements + - add a leaderboard for showing user id and scores + - generate PDF certificates of label statistics for each user upon request + - allow users to share the badge with their achievement on social media + +TODO: add the feature for labeling smoke opacity + - add a compass to show sun direction for each video (when labeling opacity) +TODO: add the feature for cropping videos to a region + +TODO: refactor code based on https://codeburst.io/jwt-authorization-in-flask-c63c1acf4eeb + +TODO: as users gain enough scores, advance them to the harder mode + - laypeople mode: select videos that have smoke + - amateur mode: label smoke opacity (low, medium, high) + - expert mode: crop videos to a region + - how to invalidate previous user tokens with different permissions? + - need to add a table to record the promotion history + - need to encode client type in the user token, and check if this matches the database record + - for a user that did not login via google, always treat them as laypeople + +BUG: when changing the client type of a user, previous tokens with different permissions are still working + - need to invalidate previous issued tokens? + +OPTIONAL: graphically display the label statistics (instead of using text) +OPTIONAL: add a playback timeline bar to show the video playback time +OPTIONAL: add a link back to time machine viewer (or a larger video) on the labeling page (also gallery page) +OPTIONAL: prevent the case that multiple people label the same data + - add the last_queried_time to video and query the ones with last_queried_time <= current_time - lock_time \ No newline at end of file diff --git a/back-end/data/video_samples/3.json b/back-end/data/video_samples/3.json new file mode 100644 index 0000000..93a7f4d --- /dev/null +++ b/back-end/data/video_samples/3.json @@ -0,0 +1,196 @@ +{ + "clairton1": [ + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2018-05-11.timemachine/&width=180&height=180&startFrame=9716&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=6304,944,6807,1447&labelsFromDataset&nframes=36", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2018-05-11.timemachine/&width=180&height=180&startFrame=9716&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=6007,988,6509,1490&labelsFromDataset&nframes=36", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2018-05-11.timemachine/&width=180&height=180&startFrame=9716&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=5648,1004,6150,1506&labelsFromDataset&nframes=36", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2018-05-11.timemachine/&width=180&height=180&startFrame=9716&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=5329,1033,5831,1535&labelsFromDataset&nframes=36", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2018-05-11.timemachine/&width=180&height=180&startFrame=9716&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=4897,1034,5400,1537&labelsFromDataset&nframes=36", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2018-05-11.timemachine/&width=180&height=180&startFrame=9716&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=4365,1074,4867,1576&labelsFromDataset&nframes=36", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2018-05-11.timemachine/&width=180&height=180&startFrame=9706&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=3981,1084,4484,1587&labelsFromDataset&nframes=36", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2018-05-11.timemachine/&width=180&height=180&startFrame=9506&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=3544,1109,4026,1591&labelsFromDataset&nframes=36", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2018-05-11.timemachine/&width=180&height=180&startFrame=9506&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=3012,1145,3515,1648&labelsFromDataset&nframes=36", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2018-05-11.timemachine/&width=180&height=180&startFrame=9506&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=3271,1116,3774,1619&labelsFromDataset&nframes=36", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2018-05-11.timemachine/&width=180&height=180&startFrame=9506&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=2583,1211,3086,1714&labelsFromDataset&nframes=36", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2018-05-11.timemachine/&width=180&height=180&startFrame=4552&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=2053,1173,2556,1676&labelsFromDataset&nframes=36", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2018-05-11.timemachine/&width=180&height=180&startFrame=4675&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=1626,1265,2129,1768&labelsFromDataset&nframes=36", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2018-05-11.timemachine/&width=180&height=180&startFrame=4675&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=1196,1205,1699,1708&labelsFromDataset&nframes=36", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2018-05-11.timemachine/&width=180&height=180&startFrame=4675&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=763,1252,1265,1754&labelsFromDataset&nframes=36", + "", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2018-09-03.timemachine/&width=180&height=180&startFrame=9716&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=6304,944,6807,1447&labelsFromDataset&nframes=36", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2018-09-03.timemachine/&width=180&height=180&startFrame=9716&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=6007,988,6509,1490&labelsFromDataset&nframes=36", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2018-09-03.timemachine/&width=180&height=180&startFrame=9716&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=5648,1004,6150,1506&labelsFromDataset&nframes=36", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2018-09-03.timemachine/&width=180&height=180&startFrame=9716&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=5329,1033,5831,1535&labelsFromDataset&nframes=36", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2018-09-03.timemachine/&width=180&height=180&startFrame=9716&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=4897,1034,5400,1537&labelsFromDataset&nframes=36", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2018-09-03.timemachine/&width=180&height=180&startFrame=9716&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=4365,1074,4867,1576&labelsFromDataset&nframes=36", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2018-09-03.timemachine/&width=180&height=180&startFrame=9706&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=3981,1084,4484,1587&labelsFromDataset&nframes=36", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2018-09-03.timemachine/&width=180&height=180&startFrame=9506&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=3544,1109,4026,1591&labelsFromDataset&nframes=36", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2018-09-03.timemachine/&width=180&height=180&startFrame=9506&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=3012,1145,3515,1648&labelsFromDataset&nframes=36", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2018-09-03.timemachine/&width=180&height=180&startFrame=9506&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=3271,1116,3774,1619&labelsFromDataset&nframes=36", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2018-09-03.timemachine/&width=180&height=180&startFrame=9506&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=2583,1211,3086,1714&labelsFromDataset&nframes=36", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2018-09-03.timemachine/&width=180&height=180&startFrame=4552&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=2053,1173,2556,1676&labelsFromDataset&nframes=36", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2018-09-03.timemachine/&width=180&height=180&startFrame=4675&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=1626,1265,2129,1768&labelsFromDataset&nframes=36", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2018-09-03.timemachine/&width=180&height=180&startFrame=4675&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=1196,1205,1699,1708&labelsFromDataset&nframes=36", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2018-09-03.timemachine/&width=180&height=180&startFrame=4675&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=763,1252,1265,1754&labelsFromDataset&nframes=36", + "", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2018-10-07.timemachine/&width=180&height=180&startFrame=9716&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=6304,884,6807,1387&labelsFromDataset&nframes=36", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2018-10-07.timemachine/&width=180&height=180&startFrame=9716&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=6007,928,6509,1430&labelsFromDataset&nframes=36", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2018-10-07.timemachine/&width=180&height=180&startFrame=9716&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=5648,924,6150,1426&labelsFromDataset&nframes=36", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2018-10-07.timemachine/&width=180&height=180&startFrame=9716&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=5329,953,5831,1455&labelsFromDataset&nframes=36", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2018-10-07.timemachine/&width=180&height=180&startFrame=9716&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=4897,954,5400,1457&labelsFromDataset&nframes=36", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2018-10-07.timemachine/&width=180&height=180&startFrame=9716&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=4365,994,4867,1496&labelsFromDataset&nframes=36", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2018-10-07.timemachine/&width=180&height=180&startFrame=9706&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=3981,1004,4484,1507&labelsFromDataset&nframes=36", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2018-10-07.timemachine/&width=180&height=180&startFrame=9506&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=3544,899,4026,1381&labelsFromDataset&nframes=36", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2018-10-07.timemachine/&width=180&height=180&startFrame=9506&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=3012,1045,3515,1548&labelsFromDataset&nframes=36", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2018-10-07.timemachine/&width=180&height=180&startFrame=9506&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=3271,1016,3774,1519&labelsFromDataset&nframes=36", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2018-10-07.timemachine/&width=180&height=180&startFrame=9506&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=2583,1011,3086,1514&labelsFromDataset&nframes=36", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2018-10-07.timemachine/&width=180&height=180&startFrame=4552&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=2053,1023,2556,1526&labelsFromDataset&nframes=36", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2018-10-07.timemachine/&width=180&height=180&startFrame=4675&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=1626,1015,2129,1518&labelsFromDataset&nframes=36", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2018-10-07.timemachine/&width=180&height=180&startFrame=4675&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=1196,1035,1699,1538&labelsFromDataset&nframes=36", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2018-10-07.timemachine/&width=180&height=180&startFrame=4675&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=763,1032,1265,1534&labelsFromDataset&nframes=36", + "", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2018-11-12.timemachine/&width=180&height=180&startFrame=9716&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=6304,884,6807,1387&labelsFromDataset&nframes=36", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2018-11-12.timemachine/&width=180&height=180&startFrame=9716&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=6007,928,6509,1430&labelsFromDataset&nframes=36", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2018-11-12.timemachine/&width=180&height=180&startFrame=9716&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=5648,924,6150,1426&labelsFromDataset&nframes=36", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2018-11-12.timemachine/&width=180&height=180&startFrame=9716&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=5329,953,5831,1455&labelsFromDataset&nframes=36", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2018-11-12.timemachine/&width=180&height=180&startFrame=9716&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=4897,954,5400,1457&labelsFromDataset&nframes=36", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2018-11-12.timemachine/&width=180&height=180&startFrame=9716&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=4365,994,4867,1496&labelsFromDataset&nframes=36", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2018-11-12.timemachine/&width=180&height=180&startFrame=9706&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=3981,1004,4484,1507&labelsFromDataset&nframes=36", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2018-11-12.timemachine/&width=180&height=180&startFrame=9506&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=3544,899,4026,1381&labelsFromDataset&nframes=36", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2018-11-12.timemachine/&width=180&height=180&startFrame=9506&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=3012,1045,3515,1548&labelsFromDataset&nframes=36", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2018-11-12.timemachine/&width=180&height=180&startFrame=9506&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=3271,1016,3774,1519&labelsFromDataset&nframes=36", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2018-11-12.timemachine/&width=180&height=180&startFrame=9506&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=2583,1011,3086,1514&labelsFromDataset&nframes=36", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2018-11-12.timemachine/&width=180&height=180&startFrame=4552&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=2053,1023,2556,1526&labelsFromDataset&nframes=36", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2018-11-12.timemachine/&width=180&height=180&startFrame=4675&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=1626,1015,2129,1518&labelsFromDataset&nframes=36", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2018-11-12.timemachine/&width=180&height=180&startFrame=4675&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=1196,1035,1699,1538&labelsFromDataset&nframes=36", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2018-11-12.timemachine/&width=180&height=180&startFrame=4675&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=763,1032,1265,1534&labelsFromDataset&nframes=36", + "", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2018-12-28.timemachine/&width=180&height=180&startFrame=9716&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=6304,884,6807,1387&labelsFromDataset&nframes=36", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2018-12-28.timemachine/&width=180&height=180&startFrame=9716&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=6007,928,6509,1430&labelsFromDataset&nframes=36", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2018-12-28.timemachine/&width=180&height=180&startFrame=9716&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=5648,924,6150,1426&labelsFromDataset&nframes=36", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2018-12-28.timemachine/&width=180&height=180&startFrame=9716&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=5329,953,5831,1455&labelsFromDataset&nframes=36", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2018-12-28.timemachine/&width=180&height=180&startFrame=9716&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=4897,954,5400,1457&labelsFromDataset&nframes=36", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2018-12-28.timemachine/&width=180&height=180&startFrame=9716&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=4365,994,4867,1496&labelsFromDataset&nframes=36", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2018-12-28.timemachine/&width=180&height=180&startFrame=9706&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=3981,1004,4484,1507&labelsFromDataset&nframes=36", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2018-12-28.timemachine/&width=180&height=180&startFrame=9506&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=3544,899,4026,1381&labelsFromDataset&nframes=36", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2018-12-28.timemachine/&width=180&height=180&startFrame=9506&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=3012,1045,3515,1548&labelsFromDataset&nframes=36", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2018-12-28.timemachine/&width=180&height=180&startFrame=9506&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=3271,1016,3774,1519&labelsFromDataset&nframes=36", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2018-12-28.timemachine/&width=180&height=180&startFrame=9506&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=2583,1011,3086,1514&labelsFromDataset&nframes=36", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2018-12-28.timemachine/&width=180&height=180&startFrame=4552&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=2053,1023,2556,1526&labelsFromDataset&nframes=36", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2018-12-28.timemachine/&width=180&height=180&startFrame=4675&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=1626,1015,2129,1518&labelsFromDataset&nframes=36", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2018-12-28.timemachine/&width=180&height=180&startFrame=4675&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=1196,1035,1699,1538&labelsFromDataset&nframes=36", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2018-12-28.timemachine/&width=180&height=180&startFrame=4675&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=763,1032,1265,1534&labelsFromDataset&nframes=36", + "", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2019-03-14.timemachine/&width=180&height=180&startFrame=9716&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=6304,884,6807,1387&labelsFromDataset&nframes=36", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2019-03-14.timemachine/&width=180&height=180&startFrame=9716&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=6007,928,6509,1430&labelsFromDataset&nframes=36", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2019-03-14.timemachine/&width=180&height=180&startFrame=9716&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=5648,924,6150,1426&labelsFromDataset&nframes=36", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2019-03-14.timemachine/&width=180&height=180&startFrame=9716&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=5329,953,5831,1455&labelsFromDataset&nframes=36", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2019-03-14.timemachine/&width=180&height=180&startFrame=9716&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=4897,954,5400,1457&labelsFromDataset&nframes=36", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2019-03-14.timemachine/&width=180&height=180&startFrame=9716&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=4365,994,4867,1496&labelsFromDataset&nframes=36", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2019-03-14.timemachine/&width=180&height=180&startFrame=9706&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=3981,1004,4484,1507&labelsFromDataset&nframes=36", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2019-03-14.timemachine/&width=180&height=180&startFrame=9506&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=3544,899,4026,1381&labelsFromDataset&nframes=36", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2019-03-14.timemachine/&width=180&height=180&startFrame=9506&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=3012,1045,3515,1548&labelsFromDataset&nframes=36", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2019-03-14.timemachine/&width=180&height=180&startFrame=9506&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=3271,1016,3774,1519&labelsFromDataset&nframes=36", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2019-03-14.timemachine/&width=180&height=180&startFrame=9506&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=2583,1011,3086,1514&labelsFromDataset&nframes=36", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2019-03-14.timemachine/&width=180&height=180&startFrame=4552&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=2053,1023,2556,1526&labelsFromDataset&nframes=36", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2019-03-14.timemachine/&width=180&height=180&startFrame=4675&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=1626,1015,2129,1518&labelsFromDataset&nframes=36", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2019-03-14.timemachine/&width=180&height=180&startFrame=4675&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=1196,1035,1699,1538&labelsFromDataset&nframes=36", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2019-03-14.timemachine/&width=180&height=180&startFrame=4675&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=763,1032,1265,1534&labelsFromDataset&nframes=36", + "", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2019-04-01.timemachine/&width=180&height=180&startFrame=9716&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=6304,884,6807,1387&labelsFromDataset&nframes=36", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2019-04-01.timemachine/&width=180&height=180&startFrame=9716&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=6007,928,6509,1430&labelsFromDataset&nframes=36", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2019-04-01.timemachine/&width=180&height=180&startFrame=9716&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=5648,924,6150,1426&labelsFromDataset&nframes=36", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2019-04-01.timemachine/&width=180&height=180&startFrame=9716&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=5329,953,5831,1455&labelsFromDataset&nframes=36", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2019-04-01.timemachine/&width=180&height=180&startFrame=9716&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=4897,954,5400,1457&labelsFromDataset&nframes=36", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2019-04-01.timemachine/&width=180&height=180&startFrame=9716&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=4365,994,4867,1496&labelsFromDataset&nframes=36", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2019-04-01.timemachine/&width=180&height=180&startFrame=9706&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=3981,1004,4484,1507&labelsFromDataset&nframes=36", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2019-04-01.timemachine/&width=180&height=180&startFrame=9506&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=3544,899,4026,1381&labelsFromDataset&nframes=36", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2019-04-01.timemachine/&width=180&height=180&startFrame=9506&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=3012,1045,3515,1548&labelsFromDataset&nframes=36", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2019-04-01.timemachine/&width=180&height=180&startFrame=9506&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=3271,1016,3774,1519&labelsFromDataset&nframes=36", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2019-04-01.timemachine/&width=180&height=180&startFrame=9506&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=2583,1011,3086,1514&labelsFromDataset&nframes=36", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2019-04-01.timemachine/&width=180&height=180&startFrame=4552&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=2053,1023,2556,1526&labelsFromDataset&nframes=36", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2019-04-01.timemachine/&width=180&height=180&startFrame=4675&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=1626,1015,2129,1518&labelsFromDataset&nframes=36", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2019-04-01.timemachine/&width=180&height=180&startFrame=4675&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=1196,1035,1699,1538&labelsFromDataset&nframes=36", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2019-04-01.timemachine/&width=180&height=180&startFrame=4675&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=763,1032,1265,1534&labelsFromDataset&nframes=36", + "", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2019-04-07.timemachine/&width=180&height=180&startFrame=9716&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=6304,884,6807,1387&labelsFromDataset&nframes=36", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2019-04-07.timemachine/&width=180&height=180&startFrame=9716&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=6007,928,6509,1430&labelsFromDataset&nframes=36", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2019-04-07.timemachine/&width=180&height=180&startFrame=9716&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=5648,924,6150,1426&labelsFromDataset&nframes=36", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2019-04-07.timemachine/&width=180&height=180&startFrame=9716&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=5329,953,5831,1455&labelsFromDataset&nframes=36", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2019-04-07.timemachine/&width=180&height=180&startFrame=9716&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=4897,954,5400,1457&labelsFromDataset&nframes=36", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2019-04-07.timemachine/&width=180&height=180&startFrame=9716&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=4365,994,4867,1496&labelsFromDataset&nframes=36", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2019-04-07.timemachine/&width=180&height=180&startFrame=9706&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=3981,1004,4484,1507&labelsFromDataset&nframes=36", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2019-04-07.timemachine/&width=180&height=180&startFrame=9506&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=3544,899,4026,1381&labelsFromDataset&nframes=36", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2019-04-07.timemachine/&width=180&height=180&startFrame=9506&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=3012,1045,3515,1548&labelsFromDataset&nframes=36", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2019-04-07.timemachine/&width=180&height=180&startFrame=9506&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=3271,1016,3774,1519&labelsFromDataset&nframes=36", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2019-04-07.timemachine/&width=180&height=180&startFrame=9506&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=2583,1011,3086,1514&labelsFromDataset&nframes=36", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2019-04-07.timemachine/&width=180&height=180&startFrame=4552&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=2053,1023,2556,1526&labelsFromDataset&nframes=36", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2019-04-07.timemachine/&width=180&height=180&startFrame=4675&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=1626,1015,2129,1518&labelsFromDataset&nframes=36", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2019-04-07.timemachine/&width=180&height=180&startFrame=4675&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=1196,1035,1699,1538&labelsFromDataset&nframes=36", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2019-04-07.timemachine/&width=180&height=180&startFrame=4675&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=763,1032,1265,1534&labelsFromDataset&nframes=36", + "", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2019-05-15.timemachine/&width=180&height=180&startFrame=9716&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=6304,884,6807,1387&labelsFromDataset&nframes=36", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2019-05-15.timemachine/&width=180&height=180&startFrame=9716&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=6007,928,6509,1430&labelsFromDataset&nframes=36", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2019-05-15.timemachine/&width=180&height=180&startFrame=9716&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=5648,924,6150,1426&labelsFromDataset&nframes=36", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2019-05-15.timemachine/&width=180&height=180&startFrame=9716&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=5329,953,5831,1455&labelsFromDataset&nframes=36", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2019-05-15.timemachine/&width=180&height=180&startFrame=9716&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=4897,954,5400,1457&labelsFromDataset&nframes=36", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2019-05-15.timemachine/&width=180&height=180&startFrame=9716&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=4365,994,4867,1496&labelsFromDataset&nframes=36", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2019-05-15.timemachine/&width=180&height=180&startFrame=9706&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=3981,1004,4484,1507&labelsFromDataset&nframes=36", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2019-05-15.timemachine/&width=180&height=180&startFrame=9506&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=3544,899,4026,1381&labelsFromDataset&nframes=36", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2019-05-15.timemachine/&width=180&height=180&startFrame=9506&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=3012,1045,3515,1548&labelsFromDataset&nframes=36", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2019-05-15.timemachine/&width=180&height=180&startFrame=9506&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=3271,1016,3774,1519&labelsFromDataset&nframes=36", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2019-05-15.timemachine/&width=180&height=180&startFrame=9506&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=2583,1011,3086,1514&labelsFromDataset&nframes=36", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2019-05-15.timemachine/&width=180&height=180&startFrame=4552&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=2053,1023,2556,1526&labelsFromDataset&nframes=36", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2019-05-15.timemachine/&width=180&height=180&startFrame=4675&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=1626,1015,2129,1518&labelsFromDataset&nframes=36", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2019-05-15.timemachine/&width=180&height=180&startFrame=4675&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=1196,1035,1699,1538&labelsFromDataset&nframes=36", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2019-05-15.timemachine/&width=180&height=180&startFrame=4675&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=763,1032,1265,1534&labelsFromDataset&nframes=36", + "", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2019-07-26.timemachine/&width=180&height=180&startFrame=9716&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=6282,1154,6769,1641&labelsFromDataset&nframes=36", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2019-07-26.timemachine/&width=180&height=180&startFrame=9716&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=5989,1127,6538,1675&labelsFromDataset&nframes=36", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2019-07-26.timemachine/&width=180&height=180&startFrame=9716&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=5596,1165,6144,1714&labelsFromDataset&nframes=36", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2019-07-26.timemachine/&width=180&height=180&startFrame=9716&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=5298,1167,5846,1715&labelsFromDataset&nframes=36", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2019-07-26.timemachine/&width=180&height=180&startFrame=9716&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=4869,1126,5417,1674&labelsFromDataset&nframes=36", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2019-07-26.timemachine/&width=180&height=180&startFrame=9716&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=4365,1130,4914,1678&labelsFromDataset&nframes=36", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2019-07-26.timemachine/&width=180&height=180&startFrame=9706&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=3978,1166,4495,1683&labelsFromDataset&nframes=36", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2019-07-26.timemachine/&width=180&height=180&startFrame=9506&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=3504,1067,4125,1688&labelsFromDataset&nframes=36", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2019-07-26.timemachine/&width=180&height=180&startFrame=9506&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=3022,1135,3539,1652&labelsFromDataset&nframes=36", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2019-07-26.timemachine/&width=180&height=180&startFrame=9506&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=3237,1143,3768,1674&labelsFromDataset&nframes=36", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2019-07-26.timemachine/&width=180&height=180&startFrame=9506&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=2614,1123,3116,1625&labelsFromDataset&nframes=36", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2019-07-26.timemachine/&width=180&height=180&startFrame=4552&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=2102,1101,2605,1603&labelsFromDataset&nframes=36", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2019-07-26.timemachine/&width=180&height=180&startFrame=4675&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=1650,1074,2212,1636&labelsFromDataset&nframes=36", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2019-07-26.timemachine/&width=180&height=180&startFrame=4675&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=1242,1084,1736,1578&labelsFromDataset&nframes=36", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2019-07-26.timemachine/&width=180&height=180&startFrame=4675&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=814,1076,1308,1570&labelsFromDataset&nframes=36", + "", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2019-06-24.timemachine/&width=180&height=180&startFrame=9716&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=6282,1154,6769,1641&labelsFromDataset&nframes=36", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2019-06-24.timemachine/&width=180&height=180&startFrame=9716&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=5989,1127,6538,1675&labelsFromDataset&nframes=36", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2019-06-24.timemachine/&width=180&height=180&startFrame=9716&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=5596,1165,6144,1714&labelsFromDataset&nframes=36", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2019-06-24.timemachine/&width=180&height=180&startFrame=9716&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=5298,1167,5846,1715&labelsFromDataset&nframes=36", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2019-06-24.timemachine/&width=180&height=180&startFrame=9716&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=4869,1126,5417,1674&labelsFromDataset&nframes=36", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2019-06-24.timemachine/&width=180&height=180&startFrame=9716&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=4365,1130,4914,1678&labelsFromDataset&nframes=36", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2019-06-24.timemachine/&width=180&height=180&startFrame=9706&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=3978,1166,4495,1683&labelsFromDataset&nframes=36", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2019-06-24.timemachine/&width=180&height=180&startFrame=9506&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=3504,1067,4125,1688&labelsFromDataset&nframes=36", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2019-06-24.timemachine/&width=180&height=180&startFrame=9506&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=3022,1135,3539,1652&labelsFromDataset&nframes=36", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2019-06-24.timemachine/&width=180&height=180&startFrame=9506&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=3237,1143,3768,1674&labelsFromDataset&nframes=36", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2019-06-24.timemachine/&width=180&height=180&startFrame=9506&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=2614,1123,3116,1625&labelsFromDataset&nframes=36", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2019-06-24.timemachine/&width=180&height=180&startFrame=4552&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=2102,1101,2605,1603&labelsFromDataset&nframes=36", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2019-06-24.timemachine/&width=180&height=180&startFrame=4675&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=1650,1074,2212,1636&labelsFromDataset&nframes=36", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2019-06-24.timemachine/&width=180&height=180&startFrame=4675&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=1242,1084,1736,1578&labelsFromDataset&nframes=36", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2019-06-24.timemachine/&width=180&height=180&startFrame=4675&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=814,1076,1308,1570&labelsFromDataset&nframes=36", + "", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2019-08-11.timemachine/&width=180&height=180&startFrame=9716&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=6282,1154,6769,1641&labelsFromDataset&nframes=36", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2019-08-11.timemachine/&width=180&height=180&startFrame=9716&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=5989,1127,6538,1675&labelsFromDataset&nframes=36", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2019-08-11.timemachine/&width=180&height=180&startFrame=9716&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=5596,1165,6144,1714&labelsFromDataset&nframes=36", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2019-08-11.timemachine/&width=180&height=180&startFrame=9716&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=5298,1167,5846,1715&labelsFromDataset&nframes=36", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2019-08-11.timemachine/&width=180&height=180&startFrame=9716&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=4869,1126,5417,1674&labelsFromDataset&nframes=36", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2019-08-11.timemachine/&width=180&height=180&startFrame=9716&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=4365,1130,4914,1678&labelsFromDataset&nframes=36", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2019-08-11.timemachine/&width=180&height=180&startFrame=9706&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=3978,1166,4495,1683&labelsFromDataset&nframes=36", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2019-08-11.timemachine/&width=180&height=180&startFrame=9506&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=3504,1067,4125,1688&labelsFromDataset&nframes=36", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2019-08-11.timemachine/&width=180&height=180&startFrame=9506&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=3022,1135,3539,1652&labelsFromDataset&nframes=36", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2019-08-11.timemachine/&width=180&height=180&startFrame=9506&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=3237,1143,3768,1674&labelsFromDataset&nframes=36", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2019-08-11.timemachine/&width=180&height=180&startFrame=9506&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=2614,1123,3116,1625&labelsFromDataset&nframes=36", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2019-08-11.timemachine/&width=180&height=180&startFrame=4552&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=2102,1101,2605,1603&labelsFromDataset&nframes=36", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2019-08-11.timemachine/&width=180&height=180&startFrame=4675&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=1650,1074,2212,1636&labelsFromDataset&nframes=36", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2019-08-11.timemachine/&width=180&height=180&startFrame=4675&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=1242,1084,1736,1578&labelsFromDataset&nframes=36", + "https://thumbnails-v2.createlab.org/thumbnail?root=https://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2019-08-11.timemachine/&width=180&height=180&startFrame=4675&format=mp4&fps=12&tileFormat=mp4&startDwell=0&endDwell=0&boundsLTRB=814,1076,1308,1570&labelsFromDataset&nframes=36", + "" + ] +} \ No newline at end of file diff --git a/back-end/install_packages.sh b/back-end/install_packages.sh index 64fbc83..d19c10c 100755 --- a/back-end/install_packages.sh +++ b/back-end/install_packages.sh @@ -18,7 +18,7 @@ pip install numpy==1.15.4 pip install pyjwt==1.7.1 # http related -pip install requests==2.21.0 +pip install requests==2.22.0 # MySQL pip install mysqlclient==1.3.14 diff --git a/back-end/www/add_community_videos.py b/back-end/www/add_community_videos.py index c0103cc..21d22e7 100644 --- a/back-end/www/add_community_videos.py +++ b/back-end/www/add_community_videos.py @@ -1,4 +1,4 @@ -from application import add_video +from application import add_video, get_all_url_part import requests from datetime import datetime import pytz @@ -8,7 +8,7 @@ import re video_size = 180 -videos_path = "../data/video_samples/2.json" +videos_path = "../data/video_samples/3.json" # Get video samples with open(videos_path) as f: @@ -33,7 +33,6 @@ def parse_cam_data(data): for d in data: camera_id = d["camera_id"] if camera_id in camera_id_list: - #dt = pytz.utc.localize(datetime.strptime(d["begin_time"], "%Y-%m-%dT%H:%M:%SZ")) dt = datetime.strptime(d["begin_time"][:10], "%Y-%m-%d") if camera_id not in dt_map: dt_map[camera_id] = [dt] @@ -169,6 +168,7 @@ def get_datetime_str_from_url(url): return m.group(0).split(".")[0] def add_videos(): + url_part_list = [u[0] for u in get_all_url_part()] for k in video_samples: for url in video_samples[k]: if url == "": continue @@ -183,6 +183,9 @@ def add_videos(): for i in range(len(sf_list)): sf = sf_list[i] url_part = get_url_part(cam_id=k, ds=ds, b=b, sf=sf, w=video_size, h=video_size) + if url_part in url_part_list: + print("Video already in database: " + url_part) + continue if check_url(url_part): s = (b["R"] - b["L"]) / video_size st = int(sf_dt_list[i].timestamp()) @@ -190,6 +193,8 @@ def add_videos(): fn = "%s-%s-%r-%r-%r-%r-%r-%r-%r-%r-%r" % (k, ds, b["L"], b["T"], b["R"], b["B"], video_size, video_size, sf, st, et) video = add_video(file_name=fn, start_time=st, end_time=et, width=video_size, height=video_size, scale=s, left=b["L"], top=b["T"], url_part=url_part) print(video) + else: + print("Problem getting video: " + url_part) def add_videos_sampling(dt_map, n_sf=1, n_b=50): for k in dt_map: diff --git a/back-end/www/application.py b/back-end/www/application.py index afef6ea..60d1ce1 100644 --- a/back-end/www/application.py +++ b/back-end/www/application.py @@ -1,12 +1,3 @@ -#TODO: fix the bug when changing the client type of a user, previous tokens with different permissions are still working (need to invalidate previous ones) -#TODO: force a user to go to the tutorial if doing the batches wrong for too many times, mark the user as spam if continue to do so -#TODO: how to promote the client to a different rank when it is changed, and invalidate previous user tokens with different permissions? -# (need to add a table to record the promotion history) -# (need to encode client type in the user token, and check if this matches the database record) -# (for a user that did not login via google, always treat them as laypeople) -#TODO: add the last_queried_time to video and query the ones with last_queried_time <= current_time - lock_time -#TODO: refactor code based on https://codeburst.io/jwt-authorization-in-flask-c63c1acf4eeb - from flask import Flask, render_template, jsonify, request, abort, g, make_response, has_request_context from flask_cors import CORS from flask_sqlalchemy import SQLAlchemy @@ -39,6 +30,7 @@ video_jwt_nbf_duration = 5 # cooldown duration (seconds) before the jwt can be accepted (to prevent spam) max_page_size = 1000 # the max page size allowed for getting videos gold_standard_in_batch = 4 # the number of gold standard videos added the batch for citizens (not reseacher) +if gold_standard_in_batch < 2: gold_standard_in_batch = 2 # must be larger than 2 """ Set Formatter @@ -160,14 +152,16 @@ class User(db.Model): client_type = db.Column(db.Integer, nullable=False, default=3) # The epochtime (in seconds) when the user was added register_time = db.Column(db.Integer, nullable=False, default=get_current_time) - # The score that the user obtained so far (number of labeled videos) + # The score that the user obtained so far (number of effectively labeled videos that passed the system's check) score = db.Column(db.Integer, nullable=False, default=0) + # The raw score that the user obtained so far (number of unlabeled video that the user went through so far) + raw_score = db.Column(db.Integer, nullable=False, default=0) # Relationships label = db.relationship("Label", backref=db.backref("user", lazy=True), lazy=True) connection = db.relationship("Connection", backref=db.backref("user", lazy=True), lazy=True) def __repr__(self): - return ("") % (self.id, self.client_id, self.client_type, self.register_time, self.score) + return ("") % (self.id, self.client_id, self.client_type, self.register_time, self.score, self.raw_score) """ The class for the label history table @@ -199,14 +193,12 @@ class Connection(db.Model): client_type = db.Column(db.Integer, nullable=False) # The user id in the User table (the user who connected to the server) user_id = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=False) - # Current score of the user - user_score = db.Column(db.Integer) # null means no information, added on 4/26/2019 # Relationships batch = db.relationship("Batch", backref=db.backref("connection", lazy=True), lazy=True) view = db.relationship("View", backref=db.backref("connection", lazy=True), lazy=True) def __repr__(self): - return ("") % (self.id, self.time, self.client_type, self.user_id, self.user_score) + return ("") % (self.id, self.time, self.client_type, self.user_id) """ The class for the issued video batch history table (for tracking video batches) @@ -223,13 +215,15 @@ class Batch(db.Model): # The number of gold standards and unlabeled videos in this batch num_unlabeled = db.Column(db.Integer, nullable=False, default=0) num_gold_standard = db.Column(db.Integer, nullable=False, default=0) - # Current score of the user + # Current score of the user (User.score) user_score = db.Column(db.Integer) # null means no information, added on 4/26/2019 + # Current raw score of the user (User.raw_score) + user_raw_score = db.Column(db.Integer) # null means no information, added on 8/9/2019 # Relationships label = db.relationship("Label", backref=db.backref("batch", lazy=True), lazy=True) def __repr__(self): - return ("") % (self.id, self.request_time, self.return_time, self.connection_id, self.score, self.num_unlabeled, self.num_gold_standard, self.user_score) + return ("") % (self.id, self.request_time, self.return_time, self.connection_id, self.score, self.num_unlabeled, self.num_gold_standard, self.user_score, self.user_raw_score) """ The table for tracking viewed videos @@ -526,6 +520,22 @@ def get_bad_labels(): def get_all_labels(): return get_video_labels(None, only_admin=True) +""" +Get statistics of the labels +""" +@app.route("/api/v1/get_label_statistics", methods=["GET"]) +def get_label_statistics(): + fully_labeled = pos_labels + pos_gold_labels + neg_labels + neg_gold_labels + q = Video.query + num_all_videos = q.filter(~Video.label_state.in_(bad_labels)).count() + num_fully_labeled = q.filter(Video.label_state.in_(fully_labeled)).count() + num_partially_labeled = q.filter(Video.label_state.in_(partial_labels)).count() + return_json = { + "num_all_videos": num_all_videos, + "num_fully_labeled": num_fully_labeled, + "num_partially_labeled": num_partially_labeled} + return jsonify(return_json) + """ Log after each request """ @@ -704,16 +714,21 @@ def update_labels(labels, user_id, connection_id, batch_id, client_type): batch_score = compute_video_batch_score(video_batch_hashed, labels) batch.score = batch_score batch.user_score = user.score + batch.user_raw_score = user.raw_score log("Update batch: %r" % batch) # Add labeling history and update the video label state # If the batch score is 0, do not update the label history since this batch is not reliable user_score = None - if batch_score != 0: + user_raw_score = None + if batch_score is not None: + user_raw_score = user.raw_score + batch.num_unlabeled + user.raw_score = user_raw_score # Update user score if client_type != 0: # do not update the score for reseacher user_score = user.score + batch_score user.score = user_score - log("Update user: %r" % user) + log("Update user: %r" % user) + if batch_score != 0: # batch_score can be None if from the dashboard when updating labels # Update labels for v in labels: v["user_id"] = user_id @@ -737,7 +752,7 @@ def update_labels(labels, user_id, connection_id, batch_id, client_type): log_warning("No next state for video: %r" % video) # Update database update_db() - return {"batch": batch_score, "user": user_score} + return {"batch": batch_score, "user": user_score, "raw": user_raw_score} """ A finite state machine to infer the new label state based on current label state and some inputs @@ -888,34 +903,34 @@ def add_view(**kwargs): def query_video_batch(user_id, use_admin_label_state=False): # Get the video ids labeled by the user before v_ids = Label.query.filter(Label.user_id==user_id).from_self(Video).join(Video).distinct().with_entities(Video.id).all() - undefined_labels = (-1, 0b11, 0b100, 0b101) labeled_video_ids = [v[0] for v in v_ids] if use_admin_label_state: + # For admin researcher, do not add gold standards # Exclude the videos that were labeled by the same user - q = Video.query.filter(and_(Video.label_state_admin.in_(undefined_labels), Video.id.notin_(labeled_video_ids))) + q = Video.query.filter(and_(Video.label_state_admin.in_((-1, 0b11, 0b100, 0b101)), Video.id.notin_(labeled_video_ids))) return q.order_by(func.random()).limit(batch_size).all() else: - if gold_standard_in_batch == 0: - # For admin researcher, do not add gold standards - # Exclude the videos that were labeled by the same user - q = Video.query.filter(and_(Video.label_state.in_(undefined_labels), Video.id.notin_(labeled_video_ids))) - return q.order_by(func.random()).limit(batch_size).all() + # Select gold standards (at least one pos and neg to prevent spamming) + # Spamming patterns include ignoring or selecting all videos + num_gold_pos = np.random.choice(range(1, gold_standard_in_batch)) + num_gold_neg = gold_standard_in_batch - num_gold_pos + gold_pos = Video.query.filter(Video.label_state_admin==0b101111).order_by(func.random()).limit(num_gold_pos).all() + gold_neg = Video.query.filter(Video.label_state_admin==0b100000).order_by(func.random()).limit(num_gold_neg).all() + # Exclude videos that were labeled by the same user, also the gold standards + gold_v_ids = Video.query.filter(Video.label_state_admin.in_((0b101111, 0b100000))).with_entities(Video.id).all() + q = Video.query.filter(Video.id.notin_(labeled_video_ids + gold_v_ids)) + # Try to include some partially labeled videos in this batch + num_unlabeled = batch_size - gold_standard_in_batch + num_partially_labeled = int(num_unlabeled/2) + partially_labeled = q.filter(Video.label_state.in_((0b11, 0b100, 0b101))).order_by(func.random()).limit(num_partially_labeled).all() + not_labeled = q.filter(Video.label_state==-1).order_by(func.random()).limit(num_unlabeled - len(partially_labeled)).all() + if (len(gold_pos + gold_neg) != gold_standard_in_batch): + # This means that there are not enough or no gold standard videos + return None else: - q_gold = Video.query.filter(Video.label_state_admin.in_((0b101111, 0b100000))) - q_gold_pos = q_gold.filter(Video.label_state_admin==0b101111) - gold_v_ids = q_gold.with_entities(Video.id).all() - # Exclude videos that were labeled by the same user, also the gold standards - q = Video.query.filter(and_(Video.label_state.in_(undefined_labels), Video.id.notin_(labeled_video_ids + gold_v_ids))) - gold_pos = q_gold_pos.order_by(func.random()).limit(1).all() # use at least on gold pos to prevent spamming - gold = q_gold.order_by(func.random()).limit(gold_standard_in_batch - 1).all() - unlabeled = q.order_by(func.random()).limit(batch_size - gold_standard_in_batch).all() - if (len(gold) != gold_standard_in_batch - 1): - # This means that there are not enough or no gold standard videos - return make_response("", 204) - else: - videos = gold + unlabeled + gold_pos - shuffle(videos) - return videos + videos = gold_pos + gold_neg + not_labeled + partially_labeled + shuffle(videos) + return videos """ Get user token by using client id @@ -927,15 +942,16 @@ def get_user_token_by_client_id(client_id): user_id = user.id client_type = user.client_type user_score = user.score - connection = add_connection(user_id=user_id, client_type=client_type, user_score=user_score) + user_raw_score = user.raw_score + connection = add_connection(user_id=user_id, client_type=client_type) ct = connection.time cid = connection.id if client_type == -1: return (None, None) # a blacklisted user does not get the token else: - # Field user_score is for the client to display the user score when loggin in + # Field user_score and user_raw_score is for the client to display the user score when loggin in # Field connection_id is for updating the batch information when the client sends labels back - user_token = encode_user_jwt(user_id=user_id, client_type=client_type, connection_id=cid, iat=ct, user_score=user_score) + user_token = encode_user_jwt(user_id=user_id, client_type=client_type, connection_id=cid, iat=ct, user_score=user_score, user_raw_score=user_raw_score) # This is the token for other app to access video labels from API calls user_token_for_other_app = encode_user_jwt(user_id=user_id, client_type=client_type, connection_id=-1, iat=ct) return (user_token, user_token_for_other_app) @@ -992,6 +1008,12 @@ def encode_jwt(payload={}): def decode_jwt(token): return jwt.decode(token, private_key, algorithms=["HS256"]) +""" +Get all the url_part in the Video table +""" +def get_all_url_part(): + return Video.query.with_entities(Video.url_part).all() + """ Custom logs """ diff --git a/back-end/www/mark_bad_videos.py b/back-end/www/mark_bad_videos.py new file mode 100644 index 0000000..dd446af --- /dev/null +++ b/back-end/www/mark_bad_videos.py @@ -0,0 +1,46 @@ +import sys +from urllib.parse import urlparse, parse_qs +from application import * +from datetime import datetime, timedelta +import pytz + +# An example video url is https://thumbnails-v2.createlab.org/thumbnail?root=http://tiles.cmucreatelab.org/ecam/timemachines/clairton1/2018-06-14.timemachine/&boundsLTRB=3544,1009,4026,1491&width=180&height=180&startFrame=12137&format=mp4&fps=12&tileFormat=mp4&nframes=36&labelsFromDataset +# The function will remove all videos generated from this bounding box (3544,1009,4026,1491) at the same date (2018-06-14) +def mark_bad_videos(video_url, do_update=False): + v = parse_qs(urlparse(video_url).query) + if "root" not in v or "boundsLTRB" not in v: + print("Error! Cannot fine url root and bounding box.") + return + r = v["root"][0] + b = v["boundsLTRB"][0] + b_split = list(map(int, b.split(","))) + b = {"L": b_split[0], "T": b_split[1], "R": b_split[2], "B": b_split[3]} + d = r.split("/")[6].split(".")[0] + dt = datetime.strptime(d, "%Y-%m-%d") + dt = pytz.timezone("US/Eastern").localize(dt) + from_t = int((dt - timedelta(days=1)).timestamp()) + to_t = int((dt + timedelta(days=2)).timestamp()) + videos = Video.query.filter(and_(Video.left==b["L"], Video.top==b["T"], Video.width==180, Video.height==180, Video.start_timefrom_t, Video.end_timefrom_t)) + for vid in videos: + if d in vid.url_part: + vid.label_state = -2 + vid.label_state_admin = -2 + print("Mark bad video: " + vid.url_part) + if do_update: + update_db() + print("Database updated.") + +def main(argv): + if len(argv) > 1: + video_url = argv[1] + if len(argv) > 2 and argv[2] == "confirm": + mark_bad_videos(video_url, do_update=True) + else: + mark_bad_videos(video_url, do_update=False) + print("Add the confirm at the end to update the database.") + print("Usage: python mark_bad_videos.py [video_url] confirm") + else: + print("Usage: python mark_bad_videos.py [video_url]") + +if __name__ == "__main__": + main(sys.argv) diff --git a/back-end/www/migrations/versions/38b7b9099636_remove_score_and_raw_score_from_the_.py b/back-end/www/migrations/versions/38b7b9099636_remove_score_and_raw_score_from_the_.py new file mode 100644 index 0000000..ed064aa --- /dev/null +++ b/back-end/www/migrations/versions/38b7b9099636_remove_score_and_raw_score_from_the_.py @@ -0,0 +1,30 @@ +"""remove score and raw score from the connection table + +Revision ID: 38b7b9099636 +Revises: 56f15f7e1d7a +Create Date: 2019-08-09 19:11:13.455707 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import mysql + +# revision identifiers, used by Alembic. +revision = '38b7b9099636' +down_revision = '56f15f7e1d7a' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('connection', 'user_score') + op.drop_column('connection', 'user_raw_score') + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('connection', sa.Column('user_raw_score', mysql.INTEGER(display_width=11), autoincrement=False, nullable=True)) + op.add_column('connection', sa.Column('user_score', mysql.INTEGER(display_width=11), autoincrement=False, nullable=True)) + # ### end Alembic commands ### diff --git a/back-end/www/migrations/versions/56f15f7e1d7a_add_viewed_unlabeled_videos_as_the_user_.py b/back-end/www/migrations/versions/56f15f7e1d7a_add_viewed_unlabeled_videos_as_the_user_.py new file mode 100644 index 0000000..fddb959 --- /dev/null +++ b/back-end/www/migrations/versions/56f15f7e1d7a_add_viewed_unlabeled_videos_as_the_user_.py @@ -0,0 +1,42 @@ +"""add viewed unlabeled videos as the user raw score + +Revision ID: 56f15f7e1d7a +Revises: 5a5220e7bac1 +Create Date: 2019-08-09 15:25:35.966502 + +""" +from alembic import op +import sqlalchemy as sa +import traceback + +# revision identifiers, used by Alembic. +revision = '56f15f7e1d7a' +down_revision = '5a5220e7bac1' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('batch', sa.Column('user_raw_score', sa.Integer(), nullable=True)) + op.add_column('connection', sa.Column('user_raw_score', sa.Integer(), nullable=True)) + op.add_column('user', sa.Column('raw_score', sa.Integer(), nullable=False)) + db = op.get_bind() + user = sa.sql.table('user', sa.sql.column('id'), sa.sql.column('raw_score')) + connection = sa.sql.table('connection', sa.sql.column('id'), sa.sql.column('user_id'), sa.sql.column('user_raw_score')) + batch = sa.sql.table('batch', sa.sql.column('id'), sa.sql.column('num_unlabeled'), sa.sql.column('connection_id'), sa.sql.column('user_raw_score')) + for b in db.execute(batch.select()): + for c in db.execute(connection.select(connection.c.id == b.connection_id)): + for u in db.execute(user.select(user.c.id == c.user_id)): + print("Process batch: " + str(b.id)) + db.execute(batch.update().where(batch.c.id == b.id).values(user_raw_score=u.raw_score)) + db.execute(user.update().where(user.c.id == c.user_id).values(raw_score=u.raw_score+b.num_unlabeled)) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('user', 'raw_score') + op.drop_column('connection', 'user_raw_score') + op.drop_column('batch', 'user_raw_score') + # ### end Alembic commands ### diff --git a/back-end/www/migrations/versions/5a5220e7bac1_add_recent_updated_time_of_the_label_to_.py b/back-end/www/migrations/versions/5a5220e7bac1_add_recent_updated_time_of_the_label_to_.py index 5fc2910..5390c37 100644 --- a/back-end/www/migrations/versions/5a5220e7bac1_add_recent_updated_time_of_the_label_to_.py +++ b/back-end/www/migrations/versions/5a5220e7bac1_add_recent_updated_time_of_the_label_to_.py @@ -19,9 +19,12 @@ def upgrade(): # ### commands auto generated by Alembic - please adjust! ### op.add_column('video', sa.Column('label_update_time', sa.Integer(), nullable=True)) + db = op.get_bind() video = sa.sql.table('video', sa.sql.column('id', sa.Integer), sa.sql.column('label_update_time', sa.Integer)) - t = sa.sql.case(value=video.c.id, whens=((op.inline_literal(b.video_id), op.inline_literal(b.time)) for b in Label.query)) - op.execute(video.update().values(**{'label_update_time': t})) + label = sa.sql.table('label', sa.sql.column('id'), sa.sql.column('time'), sa.sql.column('video_id')) + for b in db.execute(label.select()): + print("Update label: " + str(b.id)) + db.execute(video.update().where(video.c.id == b.video_id).values(label_update_time=b.time)) # ### end Alembic commands ### diff --git a/front-end/css/VideoLabelingTool.css b/front-end/css/VideoLabelingTool.css index 3f1ee3f..9705c44 100644 --- a/front-end/css/VideoLabelingTool.css +++ b/front-end/css/VideoLabelingTool.css @@ -23,7 +23,8 @@ padding: 0; } -.video-labeling-tool .error-text { +.video-labeling-tool .error-text, +.video-labeling-tool .not-supported-text { color: #ff5e5e; text-align: center; flex: 1 0 100%; diff --git a/front-end/css/controls.css b/front-end/css/controls.css index d3d9ab2..7a5bfc1 100755 --- a/front-end/css/controls.css +++ b/front-end/css/controls.css @@ -1,6 +1,6 @@ /************************************************************************* * GitHub: https://github.com/yenchiah/project-website-template - * Version: v3.7 + * Version: v3.12 * This CSS file has control elements that do not require JavaScript * If you want to keep this template updated, avoid modifying this file * Instead, add your own CSS in the index.css diff --git a/front-end/css/custom.css b/front-end/css/custom.css index 59515a3..126cab8 100644 --- a/front-end/css/custom.css +++ b/front-end/css/custom.css @@ -24,6 +24,15 @@ body, color: white !important; } +.content-table ul { + color: white !important; + font-size: 18px; +} + +.content-table hr { + background: #3be4ff !important; +} + .text { font-size: 18px; } @@ -158,10 +167,18 @@ body, font-weight: bold; } +#user-score-container { + display: none; +} + #google-sign-out-button { display: none; } +#label-statistics { + display: none; +} + .force-hidden { display: none !important; } @@ -187,7 +204,8 @@ body, padding: 10px 0; } -.gallery-error-text { +.gallery-error-text, +.gallery-not-supported-text { color: #ff5e5e !important; text-align: center; flex: 1 0 100%; @@ -226,6 +244,9 @@ body, font-style: normal; font-weight: normal; width: 100%; +} + +.gallery a .label-control p:first-child { margin-top: 10px; } @@ -239,26 +260,19 @@ body, display: block; } -.gallery a .label-control i:last-child { - margin-top: 5px; -} - -.custom-select { - margin: 5px 0; +.gallery a .label-control p a { font-family: 'Open Sans', Helvetica, Arial, sans-serif !important; font-size: 14px; + line-height: 14px; font-style: normal; font-weight: normal; width: 100%; - height: 39px; - text-indent: 5px; -} - -.custom-select:focus { - outline: none; + margin: 0; + padding: 0; + cursor: pointer; } -.gallery-videos { +.gallery .gallery-videos { display: flex; flex-direction: row; flex-wrap: wrap; @@ -270,6 +284,35 @@ body, padding: 0; } +.gallery .gallery-loading-text { + color: #3be4ff; + text-align: center; + flex: 1 0 100%; + max-width: 100%; + font-size: 18px; + padding: 10px 0; + min-height: 100px; + background-image: url(../img/loading.gif); + background-repeat: no-repeat; + background-size: 50px 50px; + background-position: center; +} + +.custom-select { + margin: 5px 0; + font-family: 'Open Sans', Helvetica, Arial, sans-serif !important; + font-size: 14px; + font-style: normal; + font-weight: normal; + width: 100%; + height: 39px; + text-indent: 5px; +} + +.custom-select:focus { + outline: none; +} + .paginationjs.paginationjs-custom { font-size: 18px; display: flex; diff --git a/front-end/css/frame.css b/front-end/css/frame.css index adc5ff3..b76fa2c 100755 --- a/front-end/css/frame.css +++ b/front-end/css/frame.css @@ -1,6 +1,6 @@ /************************************************************************* * GitHub: https://github.com/yenchiah/project-website-template - * Version: v3.7 + * Version: v3.12 * This CSS file is for the website frame * If you want to keep this template updated, avoid modifying this file * Instead, add your own CSS in the index.css @@ -12,6 +12,9 @@ body { padding: 0; background-color: white; font-family: 'Open Sans', Helvetica, Arial, sans-serif; + left: 0; + right: 0; + width: auto; } a { @@ -287,6 +290,7 @@ hr { -moz-user-select: none; -ms-user-select: none; user-select: none; + display: flex; } .image img, @@ -343,7 +347,7 @@ ol.publication li { ol.publication li p:before { line-height: 1; - margin-right: 0; + margin-right: 5px; } ol.publication.C-list { @@ -427,7 +431,7 @@ ol.publication.F-list li p { } ol.publication.F-list li p:before { - content: "[Featured.F"counter(F-counter)"]"; + content: "[F"counter(F-counter)"]"; } ol.publication.A-list { diff --git a/front-end/css/widgets.css b/front-end/css/widgets.css index b6cbcc4..a5eb023 100644 --- a/front-end/css/widgets.css +++ b/front-end/css/widgets.css @@ -1,6 +1,6 @@ /************************************************************************* * GitHub: https://github.com/yenchiah/project-website-template - * Version: v3.7 + * Version: v3.12 * This CSS file has widgets for building interactive web applications * Use this file with widgets.js * If you want to keep this template updated, avoid modifying this file @@ -248,7 +248,8 @@ .custom-dialog-flat { font-size: 16px; font-family: 'Open Sans', Helvetica, Arial, sans-serif; - background: white; + background-color: white; + background-image: none; padding: 0; margin: 0; border-radius: 2px; @@ -262,6 +263,8 @@ .custom-dialog-flat .ui-dialog-content { padding: 0 1.2em 0 1.2em; + background-color: white; + background-image: none; } .custom-dialog-flat .ui-dialog-content p { @@ -313,6 +316,10 @@ border: 0; } +.custom-dialog-flat .ui-dialog-titlebar .ui-dialog-titlebar-close .fa-lg { + vertical-align: -0.15em; +} + .custom-dialog-flat .ui-dialog-buttonpane { border-width: 0; margin: 0; @@ -353,7 +360,7 @@ } .ui-widget-overlay { - opacity: 0.6; + opacity: 0.7; background-image: none; background-color: black; } @@ -371,5 +378,18 @@ } .no-scroll { - overflow: hidden; + overflow-x: hidden; + overflow-y: scroll; + position: fixed; + height: 100vh; +} + +.fit-parent { + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; + width: auto; + height: auto; } \ No newline at end of file diff --git a/front-end/faq.html b/front-end/faq.html new file mode 100644 index 0000000..798e832 --- /dev/null +++ b/front-end/faq.html @@ -0,0 +1,158 @@ + + + + + Smoke Hunting + + + + + + + + + + + + + + + + + + + + +
+
+
+
+
+

+ We listed and answered frequently asked questions below. + Please also let us know which aspects are working well and which could be improved via the Feedback Form. + We really appreciate your input! +

+

+ + Q1: Why sometimes a dialog box popped up and asked me to enable video autoplay? + +

+
+

+ A1: During labeling, videos need to play and loop automatically. + However, if a mobile device has data saver enabled, videos will stop autoplay. + Also, some mobile devices pause videos after users wake it up from sleeping mode. + In order to enable autoplay, browsers require user interactions, which is why the system shows the dialog box. +

+

+ + Q2: Why my labels did not pass the quality check? How did you define the quality? + +

+
+

+ A2: For each batch (16 videos) on the page, the system randomly placed several videos with known answers, also called gold standards. + A batch will pass the quality check if you label these gold standards correctly. +

+

+ + Q3: Why sometimes I saw similar videos? Were they the same? + +

+
+

+ A3: There can be two reasons. + Firstly, videos that have closer times (e.g., 8 and 8:10 am) can look similar due to the same weather and lighting conditions. + Secondly, gold standard videos for the quality check can appear again if you label many batches. +

+

+ + Q4: What are you planning to do with all these labeled data? + +

+
+

+ A4: We will use these labeled videos to train a deep neural network for detecting smoke emissions. + While deep neural networks have been proven to outperform traditional models in various applications, training such large networks requires a considerable amount of labeled data, which is why we need volunteers' help. +

+

+ + Q5: Where do these video clips come from? + +

+
+

+ A5: We selected and cropped several windows into videos from our camera network (as shown in the following image). + Most videos are from the the Clairton Coke Works camera, and some videos are from the Edgar Thomson Steel Works camera. + Each video contains 36 frames, which represent about 6 minutes in real-world time. +

+
+

+ + Q6: Can I build a similar system with your code? + +

+
+

+ A6: This project is open-sourced on GitHub. Please feel free to reuse the code. +

+

+ + Q7: Are there other actions that I can take to advocate for better air quality? + +

+
+

+ A7: If you are interested in smoke reading (visual emissions observation using EPA Method 9), we recommend checking the smoke school training materials. +

+

+ + Q8: Why does this tool not support systems that are older than Android 7 and iOS 11? + +

+
+

+ A8: When labeling smoke, this tool shows 16 videos at the same time. + Older devices have difficulties in playing these videos, which results in poor user experiences. +

+
+
+
+
+
+
+

Video autoplay is disabled. Please enable it.

+ +
+ + + \ No newline at end of file diff --git a/front-end/gallery.html b/front-end/gallery.html index 50dc91b..0cec47e 100644 --- a/front-end/gallery.html +++ b/front-end/gallery.html @@ -2,8 +2,8 @@ - Smoke Labeling - + Smoke Hunting + @@ -35,7 +35,7 @@ @@ -60,29 +61,29 @@

We greatly appreciate your effort to label smoke emissions. - These are the community-labeled videos with smoke. + These are the community-labeled videos with smoke, confirmed by multiple users.

- You found the following smoke videos. - Symbol "A" and "D" on the bottom-right corner means that others agreed or disagreed with your findings respectively. + You contributed the following smoke videos. + Symbol "A" and "D" on the bottom-right corner of each video means that others agreed or disagreed with your findings respectively. Videos that have not been verified by others will not show symbols.

- P - N - P* - N* - GP* - GN* - M - ? - ! - + P + N + P* + N* + GP* + GN* + M + ? + ! +
@@ -102,7 +103,7 @@
-

Videos on this page did not autoplay. Please enable it.

+

Video autoplay is disabled. Please enable it.

diff --git a/front-end/index.html b/front-end/index.html index cd3a0b9..8f85f22 100644 --- a/front-end/index.html +++ b/front-end/index.html @@ -2,8 +2,8 @@ - Smoke Labeling - + Smoke Hunting + @@ -27,7 +27,7 @@ @@ -45,15 +46,24 @@

- The CMU CREATE Lab developed this tool to engage citizens in labeling smoke videos, which are obtained from our Pittsburgh Mon Valley monitoring camera network. + The CMU CREATE Lab needs volunteers' help to label smoke videos obtained from our monitoring cameras. With sufficient labeled videos and state-of-the-art artificial intelligence, we aim to train a computer system that can detect smoke emissions automatically. + A brief introduction about this tool can be found at this video.

+

+ The system currently supports Android 7+, iOS 11+, and recent up-to-date desktop browsers. Before you start, note the following characteristics of smoke: +

+
    +
  • Smoke disappears slower than steam.
  • +
  • Smoke shows various colors.
  • +
  • Smoke has unclear edges.
  • +
  • Smoke has various opacities.
  • +

- Smoke disappears slower and has unclear edges when compared to steam. - Smoke can also have various opacities. - High-opacity smoke can block most of its background. - Here are examples that show high opacity smoke. + We need to label both high and low opacity smoke. + High-opacity smoke (the first and second videos below) can block most of the background. + For low-opacity smoke (the third and fourth videos below), the background is mostly visible.

@@ -70,73 +80,73 @@

- For low-opacity smoke, its background is mostly visible. - Here are examples that show low opacity smoke, which also needs to be identified. + Steam disappears faster and has sharp edges when compared to smoke. + Steam also has extremely high opacity, which makes its background not visible. + Here are examples that show mainly steam, which should not be selected.

- Steam disappears faster and has sharp edges when compared to smoke. - Steam also has extremely high opacity, which makes its background not visible. - Here are examples that show mainly steam, which should not be selected. + Smoke and steam can appear at the same time. + Here are examples that show both smoke and steam, which also needs to be selected.

@@ -169,9 +179,19 @@ +
+
+

+ We need your help to train the computer system to recognize smoke emissions! + There are [...] videos in the dataset. + Among them, [...] are fully labeled and confirmed by multiple users so far. + Also, [...] are partially labeled. +

+
+
- @@ -180,7 +200,7 @@
-

Videos on this page did not autoplay. Please enable it.

+

Video autoplay is disabled. Please enable it.

diff --git a/front-end/js/GoogleAccountDialog.js b/front-end/js/GoogleAccountDialog.js index 5fa0e35..195971f 100644 --- a/front-end/js/GoogleAccountDialog.js +++ b/front-end/js/GoogleAccountDialog.js @@ -18,7 +18,6 @@ var $sign_in_text; var $hello_text; var $user_name_text; - var $user_score_text; var $use_id_text; var widgets = new edaplotjs.Widgets(); var sign_in_success = settings["sign_in_success"]; @@ -48,7 +47,6 @@ $sign_in_text = $("#sign-in-text"); $hello_text = $("#hello-text"); $user_name_text = $("#user-name-text"); - $user_score_text = $("#user-score-text"); $use_id_text = $("#user-id-text"); $google_sign_out_button = $("#google-sign-out-button"); $google_sign_in_button = $("#google-sign-in-button"); @@ -123,20 +121,27 @@ // Public methods // var isAuthenticatedWithGoogle = function (callback) { + callback = safeGet(callback, {}); if (typeof gapi !== "undefined" && typeof gapi.auth2 === "undefined") { gapi.load("auth2", function () { gapi.auth2.init().then(function () { isAuthenticatedWithGoogle(callback); + }, function (error) { + if (typeof error !== "undefined") { + if (typeof callback["error"] === "function") { + callback["error"](error); + } + } }); }); } else { - if (typeof callback === "function") { + if (typeof callback["success"] === "function") { var auth2 = gapi.auth2.getAuthInstance(); var is_signed_in = auth2.isSignedIn.get(); if (is_signed_in) { - callback(is_signed_in, auth2.currentUser.get()); + callback["success"](is_signed_in, auth2.currentUser.get()); } else { - callback(is_signed_in); + callback["success"](is_signed_in); } } } @@ -144,16 +149,23 @@ this.isAuthenticatedWithGoogle = isAuthenticatedWithGoogle; this.silentSignInWithGoogle = function (callback) { + callback = safeGet(callback, {}); gapi.load("auth2", function () { // gapi.auth2.init() will automatically sign in a user to the application if previously signed in gapi.auth2.init().then(function () { - if (typeof callback === "function") { + if (typeof callback["success"] === "function") { var auth2 = gapi.auth2.getAuthInstance(); var is_signed_in = auth2.isSignedIn.get(); if (is_signed_in) { - callback(is_signed_in, auth2.currentUser.get()); + callback["success"](is_signed_in, auth2.currentUser.get()); } else { - callback(is_signed_in); + callback["success"](is_signed_in); + } + } + }, function (error) { + if (typeof error !== "undefined") { + if (typeof callback["error"] === "function") { + callback["error"](error); } } }); @@ -164,16 +176,6 @@ return $account_dialog; }; - this.updateUserScore = function (score) { - if (typeof $user_score_text !== "undefined") { - if (typeof score !== "undefined") { - $user_score_text.text(score); - } else { - $user_score_text.text("(researcher)"); - } - } - }; - this.updateUserId = function (user_id) { if (typeof $use_id_text !== "undefined") { if (typeof user_id !== "undefined") { diff --git a/front-end/js/GoogleAnalyticsTracker.js b/front-end/js/GoogleAnalyticsTracker.js index dc944b2..d81c726 100644 --- a/front-end/js/GoogleAnalyticsTracker.js +++ b/front-end/js/GoogleAnalyticsTracker.js @@ -26,23 +26,36 @@ settings = safeGet(settings, {}); var tracker_id = settings["tracker_id"]; var ready = settings["ready"]; - var client_id; + var client_id = util.getUniqueId(); + var is_ga_ready_event_called = false; //////////////////////////////////////////////////////////////////////////////////////////////////////////// // // Private methods // function init() { - if (typeof tracker_id !== "undefined") { - ga("create", tracker_id, "auto"); - ga(function (tracker) { - client_id = "ga." + tracker.get("clientId"); // prepend "ga" to indicate Google Analytics - ga("send", "pageview"); + setTimeout(function () { + if (typeof window.ga !== "undefined" && typeof ga.create !== "undefined" && typeof tracker_id !== "undefined") { + ga("create", tracker_id, "auto"); + ga(function (tracker) { + client_id = "ga." + tracker.get("clientId"); // prepend "ga" to indicate Google Analytics + ga("send", "pageview"); + if (typeof ready === "function") ready(client_id); + is_ga_ready_event_called = true + }); + setTimeout(function () { + if (!is_ga_ready_event_called) { + // This means that maybe some third party plugin blocks the ga tracker (e.g., duckduckgo) + console.warn("The Google Analytics tracker may be blocked. Use the system generated uuid for the client id instead.") + if (typeof ready === "function") ready(client_id); + } + }, 5000); + } else { + // When tracking protection is on or no tracker id, use the generated uuid for the client_id + console.warn("The Google Analytics tracker may be blocked. Use the system generated uuid for the client id instead.") if (typeof ready === "function") ready(client_id); - }); - } else { - ready(client_id); - } + } + }, 500); // give some time in case the browser still did not finish loading the ga tracker } function safeGet(v, default_val) { @@ -53,7 +66,7 @@ // // Public methods // - this.getClientId = function() { + this.getClientId = function () { return client_id; }; @@ -74,4 +87,4 @@ window.edaplotjs = {}; window.edaplotjs.GoogleAnalyticsTracker = GoogleAnalyticsTracker; } -})(); \ No newline at end of file +})(); diff --git a/front-end/js/Util.js b/front-end/js/Util.js index 3fa1fda..69123c1 100644 --- a/front-end/js/Util.js +++ b/front-end/js/Util.js @@ -10,12 +10,43 @@ // // Variables // + var ua = navigator.userAgent; + var isChromeOS = ua.match(/CrOS/) != null; + var isMobileDevice = !isChromeOS && (ua.match(/Android/i) || ua.match(/webOS/i) || ua.match(/iPhone/i) || ua.match(/iPad/i) || ua.match(/iPod/i) || ua.match(/BlackBerry/i) || ua.match(/Windows Phone/i) || ua.match(/Mobile/i)) != null; + var isIOSDevice = ua.match(/iPad|iPhone|iPod/) != null; + var matchIOSVersionString = ua.match(/OS (\d+)_(\d+)_?(\d+)?/); + var isSupportedIOSVersion = isIOSDevice && parseInt(matchIOSVersionString[1]) >= 11; + var isAndroidDevice = ua.match(/Android/) != null; + var matchAndroidVersionString = ua.match(/Android (\d+(?:\.*\d*){1,2})/); + var isSupportedAndroidVersion = isAndroidDevice && parseFloat(matchAndroidVersionString[1]) >= 7 + var isMSIEUserAgent = ua.match(/MSIE|Trident|Edge/) != null; + var isOperaUserAgent = ua.match(/OPR/) != null; + var isChromeUserAgent = ua.match(/Chrome/) != null && !isMSIEUserAgent && !isOperaUserAgent; + var matchChromeVersionString = ua.match(/Chrome\/([0-9.]+)/); + var isSupportedChromeMobileVersion = matchChromeVersionString && matchChromeVersionString.length > 1 && parseInt(matchChromeVersionString[1]) >= 73; + var isSamsungInternetUserAgent = ua.match(/SamsungBrowser/) != null; + var isIEEdgeUserAgent = !!(isMSIEUserAgent && ua.match(/Edge\/([\d]+)/)); //////////////////////////////////////////////////////////////////////////////////////////////////////////// // // Private methods // + // This code is from https://github.com/CMU-CREATE-Lab/timemachine-viewer/blob/master/js/org/gigapan/util.js + function isMobileSupported() { + /* The following mobile browsers do not currently support autoplay of videos: + * - Samsung Internet (Last checked Mar 2019) + */ + var isSupported = false; + if (isMobileDevice && (isSupportedIOSVersion || isSupportedAndroidVersion)) { + isSupported = true; + if ((isChromeUserAgent && !isSupportedChromeMobileVersion) || isSamsungInternetUserAgent) { + isSupported = false; + } + } + return isSupported; + } + //////////////////////////////////////////////////////////////////////////////////////////////////////////// // // Privileged methods @@ -48,6 +79,44 @@ }; this.getRootApiUrl = getRootApiUrl; + // Play or pause the videos properly + // See https://developers.google.com/web/updates/2017/06/play-request-was-interrupted + var handleVideoPromise = function (video, actionType, error_callback) { + if (!video) return; + if (actionType == "play" && video.paused && !video.playPromise) { + if (video.readyState > 1) { + video.playPromise = video.play(); + } else { + console.warn("This video is not ready to play, will try later."); + setTimeout(function () { + handleVideoPromise(video, actionType); + }, 500); + return; + } + } + // HTML5 video does not return Promises in <= IE 11, so we create a fake one. + // Also note that <= IE11 does not support Promises, so we need to include a polyfill. + if (isMSIEUserAgent && !isIEEdgeUserAgent) { + video.playPromise = Promise.resolve(true); + } + if (video.playPromise !== undefined) { + video.playPromise.then(function (_) { + if (actionType == "pause" && video.played.length && !video.paused) { + video.pause(); + } else if (actionType == "load") { + video.load(); + } + if (actionType != "play") { + video.playPromise = undefined; + } + }).catch(function (error) { + console.error(error.name, error.message); + if (typeof error_callback === "function") error_callback(); + }); + } + }; + this.handleVideoPromise = handleVideoPromise; + //////////////////////////////////////////////////////////////////////////////////////////////////////////// // // Public methods @@ -141,6 +210,62 @@ this.hasSubString = function (str, sub_str) { return str.indexOf(sub_str) !== -1; }; + + // Parse variables in the format of a hash url string + this.parseVars = function (str, keep_null_or_undefined_vars) { + var vars = {}; + if (str) { + var keyvals = str.split(/[#?&]/); + for (var i = 0; i < keyvals.length; i++) { + var keyval = keyvals[i].split('='); + vars[keyval[0]] = keyval[1]; + } + } + // Delete keys with null/undefined values + if (!keep_null_or_undefined_vars) { + Object.keys(vars).forEach(function (key) { + return (vars[key] == null || key == "") && delete vars[key]; + }); + } + return vars; + }; + + // This code is from https://github.com/CMU-CREATE-Lab/timemachine-viewer/blob/master/js/org/gigapan/util.js + this.browserSupported = function () { + var v = document.createElement('video'); + + // Restrictions on which mobile devices work + if (isMobileDevice && !isMobileSupported()) return false; + + // Check if the video tag is supported + if (!!!v.canPlayType) return false; + + // See what video formats are actually supported + var supportedMediaTypes = []; + if (!!v.canPlayType('video/webm; codecs="vp8"').replace(/no/, '')) { + supportedMediaTypes.push(".webm"); + } + if (!!v.canPlayType('video/mp4; codecs="avc1.42E01E, mp4a.40.2"').replace(/no/, '')) { + supportedMediaTypes.push(".mp4"); + } + + // The current video format returned by the database is mp4, and is the only supported format now + if (supportedMediaTypes.indexOf(".mp4") < 0) return false; + + // The viewer is supported by the browser + return true; + }; + + // Is a DOM element on screen + this.isScrolledIntoView = function (elem) { + var docViewTop = $(window).scrollTop(); + var docViewBottom = docViewTop + $(window).height(); + + var elemTop = $(elem).offset().top; + var elemBottom = elemTop + $(elem).height(); + + return ((docViewTop < elemBottom) && (elemTop < docViewBottom)); + }; }; //////////////////////////////////////////////////////////////////////////////////////////////////////////// diff --git a/front-end/js/VideoLabelingTool.js b/front-end/js/VideoLabelingTool.js index 917aff2..945c322 100644 --- a/front-end/js/VideoLabelingTool.js +++ b/front-end/js/VideoLabelingTool.js @@ -1,20 +1,3 @@ -/* - * TODO: add a playback timeline bar to show the video playback time - * TODO: add a link back to time machine viewer on the labeling page (also gallery page) - * TODO: design user feedback system after labeling a batch (e.g., inform performance, correct or wrong labels for gold standards) - * TODO: show a bar (with badge) about how many videos are correctly labeled (use gold standard videos to verify this) - * TODO: add interactive tutorial - * TODO: if the labels are rejected due to poor quality, need to let user know (e.g., dialog box) - * TODO: add a leaderboard for showing user id and scores - * TODO: if the user made too many bad batches, ask the user to retake the tutorial - * TODO: wording check with Paul - * TODO: allow users to share the badge with the achievement on social media - * TODO: as users gain enough scores, advance them to the harder mode - * - laypeople mode: select videos that have smoke - * - amateur mode: select smoke opacity (low, medium, high) - * - expert mode: crop images to a region - */ - (function () { "use strict"; @@ -33,16 +16,18 @@ var $tool; var $tool_videos; var video_items = []; - var $bad_video_text = $('Oops!
Some video links are broken.
Please press "Keep Going" to skip this video batch.
'); + var $bad_video_text = $('Oops!
Some video links are broken.
Please refresh this page.
'); var $error_text = $('Oops!
Server may be down or busy.
Please come back later.
'); - var $no_data_text = $('Thank you!
Available videos are all labeled.
Please come back tomorrow.
'); + var $no_data_text = $('Thank you!
Videos are all labeled.
Please come back later.
'); var $loading_text = $(''); + var $not_supported_text = $('We are sorry!
Your browser is not supported.
'); var api_url_root = util.getRootApiUrl(); var user_id; var video_token; var user_token; var this_obj = this; var user_score; + var user_raw_score; var on_user_score_update = settings["on_user_score_update"]; var is_admin; @@ -54,6 +39,7 @@ $tool = $('
'); $tool_videos = $('
'); $container.append($tool.append($tool_videos)); + showLoadingMsg(); } // Get the user id from the server @@ -157,6 +143,7 @@ // Create a video label element // IMPORTANT: Safari on iPhone only allows displaying maximum 16 videos at once + // UPDATE: starting from Safari 12, more videos are allowed function createVideo(i) { var $item = $(""); var $caption = $("
" + (i + 1) + "
"); @@ -190,9 +177,8 @@ $item.data("id", v["id"]); var $vid = $item.find("video"); $vid.one("canplay", function () { - if (this.paused) { - this.play(); // play if the autoplay tag fails - } + // Play the video + util.handleVideoPromise(this, "play"); }); if (!$vid.complete) { var deferred = $.Deferred(); @@ -213,6 +199,7 @@ } } // Load and show videos + callback = safeGet(callback, {}); resolvePromises(deferreds, { success: function (data) { $tool.empty().append($tool_videos); @@ -243,6 +230,12 @@ } } + // Show not supported message + function showNotSupportedMsg() { + $tool_videos.detach(); + $tool.empty().append($not_supported_text); + } + // Show error message function showErrorMsg() { $tool_videos.detach(); @@ -314,9 +307,10 @@ // When sending the current batch of video labels successfully, get a new batch of videos function onSendVideoBatchSuccess(data, callback) { // Update the user score - if (typeof data !== "undefined" && data["data"]["score"]["user"] != null) { + if (typeof data !== "undefined") { user_score = data["data"]["score"]["user"]; - if (typeof on_user_score_update === "function") on_user_score_update(user_score); + user_raw_score = data["data"]["score"]["raw"]; + if (typeof on_user_score_update === "function") on_user_score_update(user_score, user_raw_score); } // Get a new batch getVideoBatch({ @@ -332,23 +326,40 @@ }); } + // When the user ID is updated successfully + function onUserIdUpdateSuccess(data) { + user_token = data["user_token"]; + var user_payload = getJwtPayload(user_token); + user_id = user_payload["user_id"]; + user_score = user_payload["user_score"]; + user_raw_score = user_payload["user_raw_score"]; + is_admin = user_payload["client_type"] == 0 ? true : false; + if (typeof on_user_score_update === "function") on_user_score_update(user_score, user_raw_score); + } + //////////////////////////////////////////////////////////////////////////////////////////////////////////// // // Public methods // this.next = function (callback, options) { callback = safeGet(callback, {}); - sendVideoBatch({ - success: function (data) { - onSendVideoBatchSuccess(data, callback); - }, - error: function (xhr) { - if (typeof callback["error"] === "function") callback["error"](xhr); - }, - abort: function (xhr) { - onSendVideoBatchSuccess(xhr.responseJSON, callback); - } - }, options); + if (util.browserSupported()) { + sendVideoBatch({ + success: function (data) { + onSendVideoBatchSuccess(data, callback); + }, + error: function (xhr) { + if (typeof callback["error"] === "function") callback["error"](xhr); + }, + abort: function (xhr) { + onSendVideoBatchSuccess(xhr.responseJSON, callback); + } + }, options); + } else { + showNotSupportedMsg(); + console.warn("Browser not supported.") + if (typeof callback["error"] === "function") callback["error"]("Browser not supported."); + } }; this.userId = function () { @@ -361,12 +372,7 @@ google_id_token: google_id_token }, { success: function (data) { - user_token = data["user_token"]; - var user_payload = getJwtPayload(user_token); - user_id = user_payload["user_id"]; - user_score = user_payload["user_score"]; - is_admin = user_payload["client_type"] == 0 ? true : false; - if (typeof on_user_score_update === "function") on_user_score_update(user_score); + onUserIdUpdateSuccess(data); if (typeof callback["success"] === "function") callback["success"](this_obj); }, error: function (xhr) { @@ -381,12 +387,7 @@ client_id: safeGet(new_client_id, util.getUniqueId()) }, { success: function (data) { - user_token = data["user_token"]; - var user_payload = getJwtPayload(user_token); - user_id = user_payload["user_id"]; - user_score = user_payload["user_score"]; - is_admin = user_payload["client_type"] == 0 ? true : false; - if (typeof on_user_score_update === "function") on_user_score_update(user_score); + onUserIdUpdateSuccess(data); if (typeof callback["success"] === "function") callback["success"](this_obj); }, error: function (xhr) { diff --git a/front-end/js/VideoTestDialog.js b/front-end/js/VideoTestDialog.js index e62abfd..e13cf47 100644 --- a/front-end/js/VideoTestDialog.js +++ b/front-end/js/VideoTestDialog.js @@ -13,12 +13,16 @@ settings = safeGet(settings, {}); var $video_test_dialog; var widgets = new edaplotjs.Widgets(); + var hidden, visibilityChange; + var should_do_video_play_test = true; //////////////////////////////////////////////////////////////////////////////////////////////////////////// // // Private methods // + function init() { + // The dialog for users to manually enable video autoplay $video_test_dialog = widgets.createCustomDialog({ selector: "#video-test-dialog", show_cancel_btn: false, @@ -26,46 +30,127 @@ show_close_button: false }); $("#play-video-button").on("click", function () { - $("video").each(function () { - this.play(); + $("video:visible").each(function () { + this.playPromise = this.play(); }); $video_test_dialog.dialog("close"); }); + // The code is modified from https://developer.mozilla.org/en-US/docs/Web/API/Page_Visibility_API + // Set the name of the hidden property and the change event for visibility + if (typeof document.hidden !== "undefined") { // Opera 12.10 and Firefox 18 and later support + hidden = "hidden"; + visibilityChange = "visibilitychange"; + } else if (typeof document.msHidden !== "undefined") { + hidden = "msHidden"; + visibilityChange = "msvisibilitychange"; + } else if (typeof document.webkitHidden !== "undefined") { + hidden = "webkitHidden"; + visibilityChange = "webkitvisibilitychange"; + } + // Warn if the browser doesn't support addEventListener or the Page Visibility API + if (typeof document.addEventListener === "undefined" || hidden === undefined) { + console.warn("The browser does not support the Page Visibility API."); + } else { + // Handle page visibility change + document.addEventListener(visibilityChange, handleVisibilityChange, false); + } + // Add a scroll event to the document to play videos that are in view + // , and pause videos that are not in view + $(document).on("scroll", function () { + $("video:visible").each(function () { + var vid = $(this)[0]; + if (util.isScrolledIntoView(vid)) { + util.handleVideoPromise(vid, "play", function () { + startVideoPlayTest(1000); + }); + } else { + util.handleVideoPromise(vid, "pause"); + } + }); + }); } function safeGet(v, default_val) { return util.safeGet(v, default_val); } + // If the page is shown, attemp to play the video, and then run the check + function handleVisibilityChange() { + if (!document[hidden]) { + should_do_video_play_test = true; + // Attemp to play the videos that are in view + $("video:visible").each(function () { + var vid = $(this).get(0); + if (util.isScrolledIntoView(vid)) { + util.handleVideoPromise(vid, "play"); + } + }); + startVideoPlayTest(1000); + } + } + //////////////////////////////////////////////////////////////////////////////////////////////////////////// // // Public methods // - this.startVideoPlayTest = function (delay) { - // Give some time for the browser to load videos - window.setTimeout(function () { - // Test if the video plays. If not, show a dialog for users to click and play. - var v = []; - var t = []; - $("video:visible").each(function () { - var element = $(this).get(0); - v.push(element); - t.push(element.currentTime); - }); - window.setTimeout(function () { - var is_autoplay_enabled = false; - for (var i = 0; i < v.length; i++) { - if (v[i].currentTime != t[i]) { - is_autoplay_enabled = true; - break; + var startVideoPlayTest = function (delay) { + if (!should_do_video_play_test) return; + should_do_video_play_test = false; + // We need this timeout to properly get document[hidden] + setTimeout(function () { + // Check if the page is hidden + if (document[hidden]) { + console.warn("The page is hidden. Ignore video play check."); + should_do_video_play_test = true; + return; + } + var $videos = $("video:visible"); + // Check if videos on screen are ready to play + // Check if there is at least one video on screen + var is_at_least_one_video_on_screen = false; + var is_all_videos_on_screen_ready_to_play = true; + $videos.each(function () { + var vid = $(this).get(0); + if (util.isScrolledIntoView(vid)) { + is_at_least_one_video_on_screen = true; + if (vid.readyState < 2) { + is_all_videos_on_screen_ready_to_play = false; + return false; } } - if (!is_autoplay_enabled) { - $video_test_dialog.dialog("open"); + }); + if (!is_at_least_one_video_on_screen) { + console.warn("No videos are on screen. The video play test will be handeled by the scroll event if video autoplay errors occur."); + should_do_video_play_test = true; + return; + } + if (!is_all_videos_on_screen_ready_to_play) { + console.warn("Some videos on the screen are not ready to play, will try the video play test later."); + setTimeout(function () { + should_do_video_play_test = true; + startVideoPlayTest(1000); + }, 5000); + return; + } + // Check if videos on screen plays + var is_all_video_on_screen_playing = true; + $videos.each(function () { + var vid = $(this).get(0); + if (util.isScrolledIntoView(vid) && vid.paused) { + is_all_video_on_screen_playing = false; + return false; } - }, delay); - }, 1000); + }); + if (!is_all_video_on_screen_playing) { + // If not, show a dialog for users to click and play + console.warn("Video autoplay is disabled. Give a dialog box for users to manually enable autoplay."); + $video_test_dialog.dialog("open"); + } else { + console.log("Video autoplay is enabled. Great!"); + } + }, delay); }; + this.startVideoPlayTest = startVideoPlayTest; //////////////////////////////////////////////////////////////////////////////////////////////////////////// // @@ -84,4 +169,4 @@ window.edaplotjs = {}; window.edaplotjs.VideoTestDialog = VideoTestDialog; } -})(); +})(); \ No newline at end of file diff --git a/front-end/js/gallery.js b/front-end/js/gallery.js index 3b99bf3..88e3b44 100644 --- a/front-end/js/gallery.js +++ b/front-end/js/gallery.js @@ -9,6 +9,8 @@ var api_url_path_get = "get_pos_labels"; var $gallery_no_data_text = $('No videos are found.'); var $gallery_error_text = $('Oops!
Server may be down or busy.
Please come back later.
'); + var $gallery_loading_text = $(''); + var $gallery_not_supported_text = $('We are sorry!
Your browser is not supported.
'); var $gallery; var $gallery_videos; var video_items = []; @@ -17,7 +19,7 @@ var $page_next; var $page_control; var user_id; - var is_admin = false; + var is_admin = false; // including expert and researcher var is_researcher = false; var user_token; var user_token_for_other_app; @@ -63,12 +65,22 @@ function showNoGalleryMsg() { $gallery_videos.detach(); - $gallery.append($gallery_no_data_text); + $gallery.empty().append($gallery_no_data_text); } function showGalleryErrorMsg() { $gallery_videos.detach(); - $gallery.append($gallery_error_text); + $gallery.empty().append($gallery_error_text); + } + + function showGalleryLoadingMsg() { + $gallery_videos.detach(); + $gallery.empty().append($gallery_loading_text); + } + + function showGalleryNotSupportedMsg() { + $gallery_videos.detach(); + $gallery.empty().append($gallery_not_supported_text); } // IMPORTANT: Safari on iPhone only allows displaying maximum 16 videos at once @@ -78,11 +90,16 @@ $item.append($vid); if (typeof user_id === "undefined") { if (is_admin) { - // Add the display of label states + // Add the display of label states and the dropdown for changing the label states var $control = $("
"); - var $label_state = $("

"); - $control.append($label_state); - // Add the function for setting label states + var $video_id = $("

"); + $control.append($video_id); + var $label_state_researcher = $("

"); + $control.append($label_state_researcher); + var $label_state_citizen = $("

"); + $control.append($label_state_citizen); + var $link_to_viewer = $("

Link to Viewer

"); + $control.append($link_to_viewer); if (is_researcher) { var $desired_state_select = createLabelStateSelect(); $desired_state_select.on("change", function () { @@ -93,7 +110,7 @@ label: parseInt(label_str) }]; admin_marked_item["select"] = $desired_state_select; - admin_marked_item["p"] = $label_state; + admin_marked_item["p"] = $label_state_researcher; $set_label_confirm_dialog.find("p").text("Set the label of video (id=" + v_id + ") to " + label_state_map[label_str] + "?"); $set_label_confirm_dialog.dialog("open"); }); @@ -122,23 +139,43 @@ return $(html); } + function safeGet(v, default_val) { + return util.safeGet(v, default_val); + } + function updateItem($item, v) { if (typeof user_id === "undefined") { if (is_admin) { + // Update label information var $i = $item.find("i").removeClass(); - var label_admin = util.safeGet(label_state_map[v["label_state_admin"]], "Undefined"); - var label = util.safeGet(label_state_map[v["label_state"]], "Undefined"); - $($i.get(0)).text(v["id"] + ": " + label_admin).addClass("custom-text-info-dark-theme"); - $($i.get(1)).text("Citizen: " + label).addClass("custom-text-info-dark-theme"); + $($i.get(0)).text("ID: " + v["id"]).addClass("custom-text-info-dark-theme"); + var label_researcher = safeGet(label_state_map[v["label_state_admin"]], "Undefined"); + $($i.get(1)).text("Scientist: " + label_researcher).addClass("custom-text-info-dark-theme"); + var label_citizen = safeGet(label_state_map[v["label_state"]], "Undefined"); + $($i.get(2)).text("Citizen: " + label_citizen).addClass("custom-text-info-dark-theme"); + // Update link + var parsed_url = util.parseVars(v["url_part"]); + var b = parsed_url["boundsLTRB"]; + var t = parseInt(parsed_url["startFrame"]) / parseInt(parsed_url["fps"]); + t = Math.round(t * 1000) / 1000 + var parsed_root = parsed_url["root"].split("/"); + var s = parsed_root[5]; + var d = parsed_root[6].split(".")[0]; + var href = "http://mon.createlab.org/#v=" + b + ",pts&t=" + t + "&ps=25&d=" + d + "&s=" + s; + var $a = $item.find("a").removeClass(); + $($a.get(0)).prop("href", href); + // Save data to DOM $item.find("select").data("v", v).val("default"); } } else { var $i = $item.find("i").removeClass(); var s = v["label_state"]; if ([19, 15, 23, 47].indexOf(s) != -1) { - $i.text("Y").addClass("custom-text-primary-dark-theme"); + $i.text("A").addClass("custom-text-primary-dark-theme"); } else if ([20, 12, 16, 32].indexOf(s) != -1) { - $i.text("N").addClass("custom-text-info-dark-theme"); + $i.text("D").addClass("custom-text-info-dark-theme"); + } else { + $i.text(""); } } $item.find("video").prop("src", v["url_root"] + v["url_part"] + "&labelsFromDataset"); @@ -204,6 +241,7 @@ callback: function (data, pagination) { if (typeof data !== "undefined" && data.length > 0) { $(window).scrollTop(0); + $gallery.empty().append($gallery_videos); updateVideos(data); if (!is_video_autoplay_tested) { video_test_dialog.startVideoPlayTest(1000); @@ -239,15 +277,18 @@ }); $page_back = $("#page-back"); $page_back.on("click", function () { + showGalleryLoadingMsg(); $page_nav.pagination("previous"); }); $page_next = $("#page-next"); $page_next.on("click", function () { + showGalleryLoadingMsg(); $page_nav.pagination("next"); }); } function setLabelState(labels, callback) { + callback = safeGet(callback, {}); $.ajax({ url: api_url_root + "set_label_state", type: "POST", @@ -280,20 +321,20 @@ action_text: "Confirm", action_callback: function () { setLabelState(admin_marked_item["data"], { - "success": function () { + success: function () { console.log("Set label state successfully:"); console.log(admin_marked_item["data"]); var v_id = admin_marked_item["data"][0]["video_id"]; var v_label = admin_marked_item["data"][0]["label"]; - var txt = v_id + ": " + util.safeGet(label_state_map[v_label], "Undefined"); + var txt = v_id + ": " + safeGet(label_state_map[v_label], "Undefined"); $(admin_marked_item["p"].find("i").get(0)).text(txt).removeClass().addClass("custom-text-primary-dark-theme"); }, - "error": function () { + error: function () { console.log("Error when setting label state:"); console.log(admin_marked_item["data"]); $(admin_marked_item["p"].find("i").get(0)).removeClass().addClass("custom-text-danger-dark-theme"); }, - "complete": function () { + complete: function () { admin_marked_item["select"].val("default"); admin_marked_item = {}; } @@ -410,29 +451,46 @@ no_ui: true }); initConfirmDialog(); - var ga_tracker = new edaplotjs.GoogleAnalyticsTracker({ - tracker_id: util.getGoogleAnalyticsId(), - ready: function (client_id) { - google_account_dialog.silentSignInWithGoogle(function (is_signed_in, google_user) { - if (is_signed_in) { - util.login({ - google_id_token: google_user.getAuthResponse().id_token - }, { - success: onLoginSuccess, - complete: onLoginComplete - }); - } else { - util.login({ - client_id: client_id - }, { - success: onLoginSuccess, - complete: onLoginComplete - }); - } - }); - } - }); - video_test_dialog = new edaplotjs.VideoTestDialog(); + if (util.browserSupported()) { + showGalleryLoadingMsg(); + var ga_tracker = new edaplotjs.GoogleAnalyticsTracker({ + tracker_id: util.getGoogleAnalyticsId(), + ready: function (client_id) { + google_account_dialog.silentSignInWithGoogle({ + success: function (is_signed_in, google_user) { + if (is_signed_in) { + util.login({ + google_id_token: google_user.getAuthResponse().id_token + }, { + success: onLoginSuccess, + complete: onLoginComplete + }); + } else { + util.login({ + client_id: client_id + }, { + success: onLoginSuccess, + complete: onLoginComplete + }); + } + }, + error: function (error) { + console.error("Error with Google sign-in: ", error); + util.login({ + client_id: client_id + }, { + success: onLoginSuccess, + complete: onLoginComplete + }); + } + }); + } + }); + video_test_dialog = new edaplotjs.VideoTestDialog(); + } else { + console.warn("Browser not supported."); + showGalleryNotSupportedMsg(); + } } $(init); diff --git a/front-end/js/index.js b/front-end/js/index.js index 164164e..3360a17 100644 --- a/front-end/js/index.js +++ b/front-end/js/index.js @@ -3,6 +3,7 @@ var util = new edaplotjs.Util(); var is_video_autoplay_tested = false; + var api_url_root = util.getRootApiUrl(); function init() { var video_test_dialog = new edaplotjs.VideoTestDialog(); @@ -13,6 +14,17 @@ var ga_tracker = new edaplotjs.GoogleAnalyticsTracker({ tracker_id: util.getGoogleAnalyticsId() }); + $.getJSON(api_url_root + "get_label_statistics", function (data) { + var num_all_videos = data["num_all_videos"]; + $(".num-all-videos-text").text(num_all_videos); + var num_fully_labeled = data["num_fully_labeled"]; + var num_fully_labeled_p = Math.round(num_fully_labeled / num_all_videos * 10000) / 100; + $(".num-fully-labeled-text").text(num_fully_labeled + " (" + num_fully_labeled_p + "%)"); + var num_partially_labeled = data["num_partially_labeled"]; + var num_partially_labeled_p = Math.round(num_partially_labeled / num_all_videos * 10000) / 100; + $(".num-partially-labeled-text").text(num_partially_labeled + " (" + num_partially_labeled_p + "%)"); + $("#label-statistics").show(); + }); } $(init); diff --git a/front-end/js/label.js b/front-end/js/label.js index 6cfae67..e754e83 100644 --- a/front-end/js/label.js +++ b/front-end/js/label.js @@ -8,16 +8,28 @@ var $next; var counter = 0; var max_counter = 10; - var count_down_duration = 500; // in milliseconds + var count_down_duration = 1000; // in milliseconds var is_first_time = true; var is_video_autoplay_tested = false; var ga_tracker; + var $sign_in_prompt; + var $user_score_container; + var $user_score_text; + var $user_raw_score_text; + var api_url_root = util.getRootApiUrl(); + var count_down_timeout; + + function resetCountDown() { + clearTimeout(count_down_timeout); + $next.removeClass("count-down-" + counter); + counter = 0; + } function countDown() { if (counter == 0) { $next.addClass("count-down-0"); } - setTimeout(function () { + count_down_timeout = setTimeout(function () { $next.removeClass("count-down-" + counter); if (counter == max_counter) { $next.prop("disabled", false); @@ -32,6 +44,7 @@ function nextBatch(ignore_labels) { $next.prop("disabled", true); + resetCountDown(); $(window).scrollTop(0); video_labeling_tool.next({ success: function () { @@ -40,6 +53,7 @@ is_video_autoplay_tested = true; } countDown(); + updateLabelStatistics(); }, abort: function () { $next.prop("disabled", false); @@ -66,10 +80,11 @@ nextBatch(); }); nextBatch(); - var $account_dialog = google_account_dialog.getDialog(); - google_account_dialog.isAuthenticatedWithGoogle(function (is_signed_in) { - if (!is_signed_in) { - $account_dialog.dialog("open"); + google_account_dialog.isAuthenticatedWithGoogle({ + success: function (is_signed_in) { + if (!is_signed_in) { + google_account_dialog.getDialog().dialog("open"); + } } }); is_first_time = false; @@ -81,13 +96,54 @@ } } + function updateLabelStatistics() { + $.getJSON(api_url_root + "get_label_statistics", function (data) { + var num_all_videos = data["num_all_videos"]; + $(".num-all-videos-text").text(num_all_videos); + var num_fully_labeled = data["num_fully_labeled"]; + var num_fully_labeled_p = Math.round(num_fully_labeled / num_all_videos * 10000) / 100; + $(".num-fully-labeled-text").text(num_fully_labeled + " (" + num_fully_labeled_p + "%)"); + var num_partially_labeled = data["num_partially_labeled"]; + var num_partially_labeled_p = Math.round(num_partially_labeled / num_all_videos * 10000) / 100; + $(".num-partially-labeled-text").text(num_partially_labeled + " (" + num_partially_labeled_p + "%)"); + $("#label-statistics").show(); + }); + } + + function onUserNotSignedIn(client_id) { + video_labeling_tool.updateUserIdByClientId(client_id, { + success: function (obj) { + onUserIdChangeSuccess(obj.userId()); + }, + error: function (xhr) { + console.error("Error when updating user id when updating user id by client id!"); + printServerErrorMsg(xhr); + $("#start").prop("disabled", true).find("span").text("Error when connecting to server"); + } + }); + } + function init() { + $user_score_text = $(".user-score-text"); + $user_raw_score_text = $(".user-raw-score-text"); video_labeling_tool = new edaplotjs.VideoLabelingTool("#labeling-tool-container", { - on_user_score_update: function (score) { - if (video_labeling_tool.isAdmin()) { - score = undefined; + on_user_score_update: function (score, raw_score) { + if (typeof $user_score_text !== "undefined") { + if (video_labeling_tool.isAdmin()) { + $user_score_text.text("(researcher)"); + } else { + if (typeof score !== "undefined" && score !== null) { + $user_score_text.text(score / 12); + } + } + } + if (typeof $user_raw_score_text !== "undefined" && typeof raw_score !== "undefined" && raw_score !== null) { + if (video_labeling_tool.isAdmin()) { + $user_raw_score_text.text(raw_score / 16); + } else { + $user_raw_score_text.text(raw_score / 12); + } } - google_account_dialog.updateUserScore(score); } }); google_account_dialog = new edaplotjs.GoogleAccountDialog({ @@ -95,6 +151,8 @@ video_labeling_tool.updateUserIdByGoogleIdToken(google_user.getAuthResponse().id_token, { success: function (obj) { onUserIdChangeSuccess(obj.userId()); + $sign_in_prompt.hide(); + $user_score_container.show(); }, error: function (xhr) { console.error("Error when updating user id by using google token!"); @@ -106,6 +164,8 @@ video_labeling_tool.updateUserIdByClientId(ga_tracker.getClientId(), { success: function (obj) { onUserIdChangeSuccess(obj.userId()); + $sign_in_prompt.show(); + $user_score_container.hide(); }, error: function (xhr) { console.error("Error when updating user id when signing out from google!"); @@ -114,27 +174,30 @@ }); } }); + $sign_in_prompt = $("#sign-in-prompt"); + $sign_in_prompt.on("click", function () { + google_account_dialog.getDialog().dialog("open"); + }); + $user_score_container = $("#user-score-container"); video_test_dialog = new edaplotjs.VideoTestDialog(); ga_tracker = new edaplotjs.GoogleAnalyticsTracker({ tracker_id: util.getGoogleAnalyticsId(), ready: function (client_id) { - google_account_dialog.isAuthenticatedWithGoogle(function (is_signed_in) { - // If signed in, will be handled by the callback function of initGoogleSignIn() in the GoogleAccountDialog object - if (!is_signed_in) { - video_labeling_tool.updateUserIdByClientId(client_id, { - success: function (obj) { - onUserIdChangeSuccess(obj.userId()); - }, - error: function (xhr) { - console.error("Error when updating user id when updating user id by client id!"); - printServerErrorMsg(xhr); - $("#start").prop("disabled", true).find("span").text("Error when connecting to server"); - } - }); + google_account_dialog.isAuthenticatedWithGoogle({ + success: function (is_signed_in) { + // If signed in, will be handled by the callback function of initGoogleSignIn() in the GoogleAccountDialog object + if (!is_signed_in) { + onUserNotSignedIn(client_id); + } + }, + error: function (error) { + console.error("Error with Google sign-in: ", error); + onUserNotSignedIn(client_id); } }); } }); + updateLabelStatistics(); } $(init); diff --git a/front-end/js/widgets.js b/front-end/js/widgets.js index 871f33c..ee89394 100644 --- a/front-end/js/widgets.js +++ b/front-end/js/widgets.js @@ -1,6 +1,6 @@ /************************************************************************* * GitHub: https://github.com/yenchiah/project-website-template - * Version: v3.7 + * Version: v3.12 * This JS file has widgets for building interactive web applications * Use this file with widgets.css * If you want to keep this template updated, avoid modifying this file @@ -24,6 +24,7 @@ // // Private methods // + // Safely get the value from a variable, return a default value if undefined function safeGet(v, default_val) { if (typeof default_val === "undefined") default_val = ""; @@ -34,6 +35,7 @@ // // Privileged methods // + function createCustomDialog(settings) { settings = safeGet(settings, {}); @@ -65,9 +67,6 @@ // Specify if full width buttons var full_width_button = safeGet(settings["full_width_button"], false); - // Prevent scrolling of the body element - var no_body_scroll = safeGet(settings["no_body_scroll"], false); - // Show the close button or not var show_close_button = safeGet(settings["show_close_button"], true); @@ -101,6 +100,7 @@ } // Create dialog + var $selector_container; var dialog_settings = { autoOpen: false, resizable: false, @@ -115,38 +115,84 @@ buttons: buttons, closeText: "", open: function (event, ui) { - var $body = $("body"); - if (no_body_scroll && !$body.hasClass("no-scroll")) { - $body.addClass("no-scroll"); + var num_opened_dialog = 0; + $(".ui-dialog-content").each(function () { + if ($(this).dialog("isOpen")) num_opened_dialog += 1; + }); + // Larger than 1 after opening means that there exists other opened dialog boxes + var is_other_dialog_opened = num_opened_dialog > 1; + // Check if parent element is specified + if (!is_other_dialog_opened) { + if (typeof settings["parent"] === "undefined") { + var $body = $("body"); + if (!$body.hasClass("no-scroll")) { + // When the modal is open, we want to set the top of the body to the scroll position + document.body.style.top = -window.scrollY + "px"; + $body.addClass("no-scroll"); + } + $selector_container.css({ + position: "fixed", + top: "calc(50% - " + ($selector_container.height() / 2) + "px)", + margin: "0 auto", + left: "0", + right: "0", + overflow: "hidden" + }); + } else { + // If there is a parent, need to fit the overlay to the parent element + var $overlay = $(".ui-widget-overlay"); + if (!$overlay.hasClass("fit-parent")) { + $overlay.addClass("fit-parent"); + } + } } }, close: function (event, ui) { - var $body = $("body"); - if (no_body_scroll && $body.hasClass("no-scroll")) { - $body.removeClass("no-scroll"); + var num_opened_dialog = 0; + $(".ui-dialog-content").each(function () { + if ($(this).dialog("isOpen")) num_opened_dialog += 1; + }); + // Larger than 0 after closing means that there exists other opened dialog boxes + var is_other_dialog_opened = num_opened_dialog > 0; + if (!is_other_dialog_opened) { + // Check if parent element is specified + if (typeof settings["parent"] === "undefined") { + var $body = $("body"); + if ($body.hasClass("no-scroll")) { + // When the modal is hidden, we want to remain at the top of the scroll position + $body.removeClass("no-scroll"); + var scrollY = document.body.style.top; + document.body.style.top = ""; + window.scrollTo(0, parseInt(scrollY || "0") * -1); + } + } else { + // If there is a parent, need to remove the class that fits the overlay to the parent + var $overlay = $(".ui-widget-overlay"); + if ($overlay.hasClass("fit-parent")) { + $overlay.removeClass("fit-parent"); + } + } } } }; - // Specify the parent of the dialog, need to be a jQuery object - if (typeof settings["parent"] !== "undefined") { - dialog_settings["appendTo"] = settings["parent"]; + + if (typeof settings["parent"] === "undefined") { dialog_settings["position"] = { my: "center", at: "center", - of: settings["parent"] + of: window }; } else { + dialog_settings["appendTo"] = settings["parent"]; dialog_settings["position"] = { my: "center", at: "center", - of: window + of: settings["parent"] }; } var $dialog = $selector.dialog(dialog_settings); - $dialog.closest(".ui-dialog").find(".ui-dialog-titlebar-close").empty().append(""); - $(window).on("resize", function () { - $dialog.dialog("option", "position", dialog_settings["position"]); - }); + $selector_container = $selector.closest(".ui-dialog"); + $selector_container.find(".ui-dialog-titlebar-close").empty().append(""); if (!show_close_button) { $dialog.on("dialogopen", function () { $(this).parent().find(".ui-dialog-titlebar-close").hide(); diff --git a/front-end/label.html b/front-end/label.html index f0782ae..c22b544 100644 --- a/front-end/label.html +++ b/front-end/label.html @@ -2,8 +2,8 @@ - Smoke Labeling - + Smoke Hunting + @@ -35,7 +35,7 @@ @@ -82,16 +92,13 @@ Hi , thank you for signing in. Your user ID is .

-

- Your score is , which is the number of videos that you labeled reliably. -

-

Videos on this page did not autoplay. Please enable it.

+

Video autoplay is disabled. Please enable it.