Set up Label Studio
Now let’s spin up a Label Studio server process. (If you’re already running Label Studio, you can choose to skip this step, and instead enter your existing Label Studio URL and access token in the subsequent step.) Be patient, as it may take a minute or two to start. This will open a new browser window containing the Label Studio interface. If you’ve never run Label Studio before, you’ll need to create an account; a link to create one will appear in the Label Studio browser window. Everything is running locally in this tutorial, so the account will exist only on your local system.Configure Pixeltable
Next, we configure Pixeltable to communicate with Label Studio. Run the following command, pasting in the API key that you copied from the Label Studio interface.Create a Table to Store Videos
Now we create the master table that will hold our videos to be annotated. This only needs to be done once, when we initially set up the workflow.Populate It with Data
Now let’s add some videos to the table to populate it. For this tutorial, we’ll use some randomly selected videos from the Multimedia Commons archive. The table also contains adate field, for which we’ll
use a fixed date (but in a production setting, it would typically be the
date on which the video was imported).
| video | date |
|---|---|
| 2024-04-22 | |
| 2024-04-22 | |
| 2024-04-22 |
Create a Label Studio project
Next we’ll create a new Label Studio project and link it to a new view on the Pixeltable table. You can link a Label Studio project to either a table or a view. For tables that are expecting a lot of input data, it’s often easier to link to views. In this example, we’ll create a view that filters the table down by date.videos_2022_04_22, with three tasks, one for
each of the videos in the view. If you want to create the project
without populating it with tasks (yet), you can set
sync_immediately=False in the call to create_label_studio_project().
You can always sync the table and project by calling v.sync().
Note also that we didn’t have to specify an explicit mapping between
Pixeltable columns and Label Studio data fields. This is because, by
default, Pixeltable assumes the Pixeltable and Label Studio field names
coincide. The data field in the Label Studio project has the name
$video, which Pixeltable maps, by default, to the column in
ls_demo.videos_2024_02_22 that is also called video. If you want to
override this behavior to specify an explicit mapping of columns to
fields, you can do that with the col_mapping parameter of
create_label_studio_project().
Inspecting the view, we also see that Pixeltable created an additional
column on the view, annotations, which will hold the output of our
annotations workflow. The name of the output column can also be
overridden by specifying a dict entry in col_mapping of the form
{'my_col_name': 'annotations'}.
| Column Name | Type | Computed With |
|---|---|---|
| annotations | json | |
| video | video | |
| date | timestamp |
Add Some Annotations
Now, let’s add some annotations to our Label Studio project to simulate a human-in-the-loop workflow. In the Label Studio UI, click on the newvideos_2024_02_22 project, and click on any of the three tasks. Select
the appropriate category (“city”, “food”, or “sports”), and click
“Submit”.
Import the Annotations Back To Pixeltable
Now let’s try importing annotations from Label Studio back to our view.annotations field populated in the view.
| video | annotations |
|---|---|
| [{"id": 35, "task": 141, "result": [{"id": "E_L95THtiU", "type": "choices", "value": {"choices": ["sports"]}, "origin": "manual", "to_name": "video", "from_name": "video-category"}], "project": 106, "import_id": null, "lead_time": 2.586, "created_at": "2024-08-14T04:25:17.201212Z", "updated_at": "2024-08-14T04:25:17.201239Z", "updated_by": 2, "created_ago": "0 minutes", "last_action": null, "completed_by": 2, "ground_truth": false, "was_cancelled": false, "last_created_by": null, "created_username": " [email protected], 2", "draft_created_at": null, "parent_annotation": null, "parent_prediction": null}] | |
| None | |
| None |
Parse Annotations with a Computed Column
Pixeltable pulls in all sorts of metadata from Label Studio during a sync: everything that Label Studio reports back about the annotations, including things like the user account that created the annotations. Let’s say that all we care about is the annotation value. We can add a computed column to our table to pull it out.| video | annotations | video_category |
|---|---|---|
| [{"id": 35, "task": 141, "result": [{"id": "E_L95THtiU", "type": "choices", "value": {"choices": ["sports"]}, "origin": "manual", "to_name": "video", "from_name": "video-category"}], "project": 106, "import_id": null, "lead_time": 2.586, "created_at": "2024-08-14T04:25:17.201212Z", "updated_at": "2024-08-14T04:25:17.201239Z", "updated_by": 2, "created_ago": "0 minutes", "last_action": null, "completed_by": 2, "ground_truth": false, "was_cancelled": false, "last_created_by": null, "created_username": " [email protected], 2", "draft_created_at": null, "parent_annotation": null, "parent_prediction": null}] | sports | |
| None | None | |
| None | None |
get_metadata function, which returns
information about the video itself, such as the resolution and codec
(independent of Label Studio). Let’s add another computed column to hold
such metadata.
| video | annotations | video_category | video_metadata |
|---|---|---|---|
| [{"id": 35, "task": 141, "result": [{"id": "E_L95THtiU", "type": "choices", "value": {"choices": ["sports"]}, "origin": "manual", "to_name": "video", "from_name": "video-category"}], "project": 106, "import_id": null, "lead_time": 2.586, "created_at": "2024-08-14T04:25:17.201212Z", "updated_at": "2024-08-14T04:25:17.201239Z", "updated_by": 2, "created_ago": "0 minutes", "last_action": null, "completed_by": 2, "ground_truth": false, "was_cancelled": false, "last_created_by": null, "created_username": " [email protected], 2", "draft_created_at": null, "parent_annotation": null, "parent_prediction": null}] | sports | {"size": 815026, "streams": [{"type": "video", "width": 640, "frames": 235, "height": 480, "duration": 235235, "metadata": {"encoder": "AVC Coding", "language": "eng", "vendor_id": "[0][0][0][0]", "handler_name": "MP4 Video Media Handler", "creation_time": "2010-04-27T16:40:32.000000Z"}, "base_rate": 29.97, "time_base": 3.333e-05, "average_rate": 29.97, "guessed_rate": 29.97, "codec_context": {"name": "h264", "pix_fmt": "yuv420p", "profile": "High", "codec_tag": "avc1"}, "duration_seconds": 7.841}, {"type": "audio", "frames": 339, "duration": 347135, "metadata": {"language": "eng", "vendor_id": "[0][0][0][0]", "handler_name": "MP4 Sound Media Handler", "creation_time": "2010-04-27T16:40:32.000000Z"}, "time_base": 2.268e-05, "codec_context": {"name": "aac", "profile": "LC", "channels": 2, "codec_tag": "mp4a"}, "duration_seconds": 7.872}], "bit_rate": 828326, "metadata": {"major_brand": "mp42", "creation_time": "2010-04-27T16:40:32.000000Z", "minor_version": "0", "compatible_brands": "isom"}, "bit_exact": false} | |
| None | None | {"size": 1558736, "streams": [{"type": "video", "width": 640, "frames": 450, "height": 480, "duration": 450450, "metadata": {"encoder": "AVC Coding", "language": "eng", "vendor_id": "[0][0][0][0]", "handler_name": "MP4 Video Media Handler", "creation_time": "2009-05-20T00:53:00.000000Z"}, "base_rate": 29.97, "time_base": 3.333e-05, "average_rate": 29.97, "guessed_rate": 29.97, "codec_context": {"name": "h264", "pix_fmt": "yuv420p", "profile": "High", "codec_tag": "avc1"}, "duration_seconds": 15.015}, {"type": "audio", "frames": 648, "duration": 663551, "metadata": {"language": "eng", "vendor_id": "[0][0][0][0]", "handler_name": "MP4 Sound Media Handler", "creation_time": "2009-05-20T00:53:00.000000Z"}, "time_base": 2.268e-05, "codec_context": {"name": "aac", "profile": "LC", "channels": 2, "codec_tag": "mp4a"}, "duration_seconds": 15.047}], "bit_rate": 828756, "metadata": {"major_brand": "mp42", "creation_time": "2009-05-20T00:53:00.000000Z", "minor_version": "0", "compatible_brands": "isom"}, "bit_exact": false} | |
| None | None | {"size": 2099014, "streams": [{"type": "video", "width": 640, "frames": 600, "height": 360, "duration": 600600, "metadata": {"language": "eng", "vendor_id": "[0][0][0][0]", "handler_name": "VideoHandler"}, "base_rate": 29.97, "time_base": 3.333e-05, "average_rate": 29.97, "guessed_rate": 29.97, "codec_context": {"name": "h264", "pix_fmt": "yuv420p", "profile": "High", "codec_tag": "avc1"}, "duration_seconds": 20.02}, {"type": "audio", "frames": 863, "duration": 883712, "metadata": {"language": "eng", "vendor_id": "[0][0][0][0]", "handler_name": "SoundHandler"}, "time_base": 2.268e-05, "codec_context": {"name": "aac", "profile": "LC", "channels": 2, "codec_tag": "mp4a"}, "duration_seconds": 20.039}], "bit_rate": 836844, "metadata": {"encoder": "Lavf54.63.104", "major_brand": "isom", "minor_version": "512", "compatible_brands": "isomiso2avc1mp41"}, "bit_exact": false} |
Preannotations with Pixeltable and Label Studio
Frame extraction is another common operation in labeling workflows. In this example, we’ll extract frames from our videos into a view, then use an object detection model to generate preannotations for each frame. The following code uses a PixeltableFrameIterator to automatically
extract frames into a new view, which we’ll call frames_2024_04_22.
| frame |
|---|
| frame | detections |
|---|---|
| {"boxes": [[584.916, 0.75, 639.989, 321.319], [46.766, 93.549, 294.693, 465.584]], "labels": [1, 1], "scores": [0.995, 0.999], "label_text": ["person", "person"]} | |
| {"boxes": [[562.879, 197., 640.206, 229.308], [415.185, 149.31, 427.469, 168.121], [182.444, 160.578, 219.743, 248.855], [413.599, 139.345, 426.33, 160.366]], "labels": [15, 4, 1, 1], "scores": [0.981, 0.995, 1., 0.988], "label_text": ["bench", "motorcycle", "person", "person"]} | |
| {"boxes": [[387.795, 196.154, 505.848, 372.086]], "labels": [1], "scores": [1.], "label_text": ["person"]} |
detr_to_coco function
to do the conversion, using another computed column.
| frame | detections | preannotations |
|---|---|---|
| {"boxes": [[584.916, 0.75, 639.989, 321.319], [46.766, 93.549, 294.693, 465.584]], "labels": [1, 1], "scores": [0.995, 0.999], "label_text": ["person", "person"]} | {"image": {"width": 640, "height": 480}, "annotations": [{"bbox": [584.916, 0.75, 55.073, 320.57], "category": 1}, {"bbox": [46.766, 93.549, 247.927, 372.035], "category": 1}]} | |
| {"boxes": [[562.879, 197., 640.206, 229.308], [415.185, 149.31, 427.469, 168.121], [182.444, 160.578, 219.743, 248.855], [413.599, 139.345, 426.33, 160.366]], "labels": [15, 4, 1, 1], "scores": [0.981, 0.995, 1., 0.988], "label_text": ["bench", "motorcycle", "person", "person"]} | {"image": {"width": 640, "height": 480}, "annotations": [{"bbox": [562.879, 197., 77.326, 32.308], "category": 15}, {"bbox": [415.185, 149.31, 12.283, 18.811], "category": 4}, {"bbox": [182.444, 160.578, 37.299, 88.276], "category": 1}, {"bbox": [413.599, 139.345, 12.73, 21.021], "category": 1}]} | |
| {"boxes": [[387.795, 196.154, 505.848, 372.086]], "labels": [1], "scores": [1.], "label_text": ["person"]} | {"image": {"width": 640, "height": 480}, "annotations": [{"bbox": [387.795, 196.154, 118.053, 175.932], "category": 1}]} |
Create a Label Studio Project for Frames
With our data workflow set up and the COCO preannotations prepared, all that’s left is to create a corresponding Label Studio project. Note how Pixeltable automatically mapsRectangleLabels preannotation fields to
columns, just like it does with data fields. Here, Pixeltable interprets
the name="preannotations" attribute in RectangleLabels to mean, “map
these rectangle labels to the preannotations column in my linked table
or view”.
The Label values car, person, and train are standard COCO object
identifiers used by many off-the-shelf object detection models. You can
find the complete list of them here, and include as many as you wish:
https://raw.githubusercontent.com/pixeltable/pixeltable/release/docs/resources/coco-categories.csv
Incremental Updates
As we saw in the Pixeltable Basics tutorial, adding new data to Pixeltable results in incremental updates of everything downstream. We can see this by inserting a new video into our base videos table: all of the downstream views and computed columns are updated automatically, including the video metadata, frames, and preannotations. The update may take some time, so please be patient (it involves a sequence of operations, including frame extraction and object detection).Table with the remote Label Studio projects. To issue a sync, we have
to call the sync() methods separately. Note that tasks will be created
only for the newly added rows in the videos and frames views, not the
existing ones.
Deleting a Project
To remove a Label Studio project from a table or view, useunlink_external_stores(), as demonstrated by the following example. If
you specify delete_external_data=True, then the Label Studio project
will also be deleted, along with all existing data and annotations (be
careful!) If delete_external_data=False, then the Label Studio project
will be unlinked from Pixeltable, but the project and data will remain
in Label Studio (so you’ll need to delete the project manually if you
later want to get rid of it).
Configuring media_import_method
All of the examples so far in this tutorial use HTTP file uploads to
send media data to Label Studio. This is the simplest method and the
easiest to configure, but it’s undesirable for complex projects or
projects with a lot of data. In fact, the Label Studio documentation
includes this specific warning: “Uploading data works fine for proof of
concept projects, but it is not recommended for larger projects.”
In Pixeltable, you can configure linked Label Studio projects to use
URLs for media data (instead of file uploads) by specifying the
media_import_method='url' argument in create_label_studio_project.
This is recommended for all production applications, and is mandatory
for projects whose input configuration is more complex than a single
media file (in the Label Studio parlance, projects with more than one
“data key”).
If media_import_method='url', then Pixeltable will simply pass the
media data URLs directly to Label Studio. If the URLs are http:// or
https:// URLs, then nothing more needs to be done.
Label Studio also supports s3:// URLs with credentialed access. To use
them, you’ll need to configure access to your bucket in the project
configuration. The simplest way to do this is by specifying an
s3_configuration in create_label_studio_project. Here’s an example,
though it won’t work directly in this demo notebook, since it relies on
having an access key. (If your AWS credentials are stored in
~/.aws/credentials, then you can omit the access key and secret, and
Pixeltable will fill them in automatically.)
create_label_studio_project usage,
see: - Pixeltable API Docs:
create_label_studio_project