blog

NASA Pose Bowl - Benchmark


by Michael Schlauch

spacecraft with bounding box

Welcome to the benchmark notebook for the Pose Bowl: Object Detection challenge!

If you are just getting started, first checkout the competition homepage and problem description.

Pose Bowl: Object Detection

In this challenge, you will help to develop new methods for conducting spacecraft inspections by identifying the position and orientation of a target spacecraft in an image.

The goal of this $12,000 challenge is to help NASA address key operational hurdles in spacecraft inspection, including the ability to detect a variety of different types of target spacecraft, as well as being able to run this code efficiently on relatively low-cost hardware. For the latter reason, this challenge is a code execution challenge, and your submission will be run on our code execution platform in an environment that simulates a small, off-the-shelf computer board used on R5 NASA spacecraft.

If you're not familiar with code execution challenges, don't worry! This benchmark notebook/blog post will help you get comfortable with the setup and on your way to producing your own challenge submissions.

We'll cover two main areas in this post:

  • Section 1. Getting started: An introduction to the challenge data, including image examples and some explanation about the type of variation to expect across the dataset.

  • Section 2. Demo submission: A demonstration of how to run the benchmark example and produce a valid code submission.

Section 1: Getting started

Get this notebook

First, clone the runtime repository with:

git clone https://github.com/drivendataorg/spacecraft-pose-object-detection-runtime.git

The repository includes a copy of this notebook in /notebooks/data-exploration-and-benchmark.ipynb. We'd encourage you to run this notebook yourself as part of this exercise.

You will also need to set up your environment. For the purposes of this notebook, you will just need to the following libraries, which you can install with the following pip command.

pip install jupyterlab pandas pyarrow opencv-python matplotlib ultralytics

Download some data

Let's first download some challenge imagery from the Data Download page. The imagery dataset comes in the form of .tar files, each of which is about 2.5 GB. For this benchmark example, we'll just download the 0.tar file.

Downloading imagery may take some time, so feel free to do something else for a little while. (The code submission format page and this runtime repository's README are relevant background reading for what we're about to do next.)

Once the tar file has been downloaded, you can extract it to the data_dev directory (the command below should work on a Unix-based system). We suggest saving the images to the data_dev directory (or something similarly named) rather than the data directory because the data directory plays a special role in simulating the test data that is available when your submission runs in the code execution environment.

tar -xzvf 0.tar -C data_dev/

You'll also want to download the submission_format.csv, train_labels.csv and train_metadata.csv from the Data Download page and save these in the same data_dev directory.

Once everything is downloaded, your data_dev directory should look like this.

spacecraft-pose-object-detection-runtime/
└── data_dev
    ├── images
    │   ├── 0001954c9f4a58f7ac05358b3cda8d20.png
    │   ├── 00054819240f9d46378288b215dbcd3a.png
    │   ├── 000dbf763348037b46558bbcb6a032ac.png
    │   ...
    │
    ├── submission_format.csv
    ├── train_labels.csv
    └── train_metadata.csv

Explore the data

Now it's time to get acquainted with the challenge data. Below we define locations for some of the important files.

In [1]:
from pathlib import Path
import numpy as np
import pandas as pd

PROJ_DIRECTORY = Path.cwd().parent
DATA_DIRECTORY = PROJ_DIRECTORY / "data"
DEV_DIRECTORY = PROJ_DIRECTORY / "data_dev"
IMAGES_DIRECTORY = DEV_DIRECTORY / "images"

Let's take a look at two of the metadata files.

In [2]:
train_meta = pd.read_csv(DEV_DIRECTORY / "train_metadata.csv", index_col="image_id")
train_labels = pd.read_csv(DEV_DIRECTORY / "train_labels.csv", index_col="image_id")

The train_metadata.csv contains information about the type of spacecraft and background used to generate the image. You should consider ways to use this information effectively in stratifying your train and test splits. The test data that you will be evaluated on includes spacecraft types that are not included in the training set, so you will want to consider strategies for ensuring your model generalizes well. You may also want to consider ways of generating additional training data, e.g. to generate a more diverse variety of background imagery.

The train_metadata.csv contains information about all images in the training set. But since we haven't downloaded all the images yet, let's filter the dataset down to just the images we've saved locally.

In [3]:
# we didn't download the full training set, so add a column indicating which images are saved locally
train_meta["exists"] = train_meta.index.to_series().map(lambda x: (IMAGES_DIRECTORY / f"{x}.png").exists())
# filter our metadata down to only the images we have locally
train_meta = train_meta[train_meta.exists]

train_meta.head()
Out[3]:
spacecraft_id background_id exists
image_id
0001954c9f4a58f7ac05358b3cda8d20 24 247 True
00054819240f9d46378288b215dbcd3a 14 10 True
000dbf763348037b46558bbcb6a032ac 19 17 True
000e79208bebd8e84ce6c22fd8612a0d 14 15 True
000f13aff94499d03e3997afc55b0aa0 28 15 True

The train_labels.csv contains the bounding box information for the target spacecraft in each image.

In [4]:
train_labels.head()
Out[4]:
xmin ymin xmax ymax
image_id
0001954c9f4a58f7ac05358b3cda8d20 0 277 345 709
00054819240f9d46378288b215dbcd3a 753 602 932 725
000dbf763348037b46558bbcb6a032ac 160 434 203 481
000e79208bebd8e84ce6c22fd8612a0d 70 534 211 586
000f13aff94499d03e3997afc55b0aa0 103 0 312 193

Let's look at a few example images to get a feel for what's in this dataset.

In [5]:
import cv2
import matplotlib.pyplot as plt
from matplotlib.patches import Rectangle

def get_bbox(image_id, labels):
    """Get bbox coordinates as list from dataframe for given image id."""
    return labels.loc[image_id].loc[["xmin", "ymin", "xmax", "ymax"]].values.tolist()

def display_image(image_id, images_dir=IMAGES_DIRECTORY, show_bbox=False, labels=train_labels):
    """Display image given image ID. Annotate with bounding box if `show_bbox` is True."""
    img = cv2.imread(str(images_dir / f"{image_id}.png"))
    fig, ax = plt.subplots()
    # cv2 reads images as BGR order; we should flip them to RGB for matplotlib
    # ref: https://stackoverflow.com/questions/54959387/rgb-image-display-in-matplotlib-plt-imshow-returns-a-blue-image
    ax.imshow(np.flip(img, axis=-1))

    if show_bbox:
        xmin, ymin, xmax, ymax = get_bbox(image_id, labels)
        patch = Rectangle((xmin, ymin), xmax - xmin, ymax - ymin, fill=False, edgecolor='white', linewidth=1)
        ax.add_patch(patch)

Some images are relatively straightforward with an easily identifiable spacecraft. Below is one example. Note that the spacecraft is not necessarily fully in view. Also note that the background in this case consists entirely of our planet, with no view of outer space, while in other images it may be entirely space, or more commonly a mix of planet and space.

In [6]:
display_image("07dd7b5a0b7b224abc0a7fea8e78de76", show_bbox=True)

There are many examples where the spacecraft will be more difficult to detect.

One challenging type of image involves target spacecraft that appear very small, due to their distance from the chaser spacecraft. Here is one example of that situation.

In [7]:
display_image("0d4b4eda4bf7c0251399047d71cc4188", show_bbox=True)

Lens flare and other visual artifacts from the refraction of light on the camera lens may also complicate your detection algorithm, as in the example below.

In [8]:
display_image("01fcd95cdcf8bb84ec4dfa7b87bf2abc", show_bbox=True)

Some target spacecraft may have long, thin appendages that are difficult or even impossible to see. Remember that your bounding box needs to encompass these appendages.

In [9]:
display_image("0263257179fcf14793f40430713ece44", show_bbox=True)

Finally, there will be situations where parts of the spacecraft are obscured by shadow. This can make it particularly difficult to detect the edges of thin appendages like the ones we saw above.

In [10]:
display_image("0eee2fcf59ec1e7641db1f8284c1d935", show_bbox=True)

Explore for yourself

The code below will pull and display a random image. Feel free to run this as many times as you like to develop your intuition for what types of images are in this dataset.

In [11]:
image_id = train_meta.sample(1).index[0]
print(image_id)

ax = display_image(image_id, show_bbox=True)
069a74eff3943d8c096bb37c7c674832

Section 2: Demo submission

Now that we've had a chance to get a feel for the data, it's time to walk through the steps for creating a competition submission.

This is a code submission competition, so our focus for now will be on creating that submission in the correct format, and less so on the accuracy of the predictions. To get you started, we'll be using a YOLO model to try to identify the target spacecraft in 100 local test images.

If you haven't already read through the following resources, now would be a great time to do that:

Using the /data directory

The /data directory in this runtime repository plays two important roles:

  • When you are testing your solution locally, the contents of /data will be mounted on the Docker container that simulates our code execution platform (further below, we'll walk through the specifics of how to do these test runs)
  • When you make a submission on the DrivenData platform, the contents of /data that get mounted to the Docker container will be the unseen test data that ultimately determines your rank on the leaderboard.

To develop your submission locally, you should add a subset of the challenge data to /data and treat this as your local test set. We'll demonstrate one way to do that next...

First, let's select a random subset of 100 images in /data_dev to treat as our local test set.

In [12]:
test_images = train_meta.sample(100, random_state=1).index.tolist()
test_images[:5]
Out[12]:
['059af2a88bfa9e8f99de278154acad12',
 '0384647db5822c58e10acb2f6e9cae88',
 '0e24bf82001019665f91889c24d733ca',
 '01cb19b86b408a71407f9dd293d43e4a',
 '02a88fc07c2709f50807da1cfb29612c']

Next, we'll move these files into the /data directory.

In [13]:
import shutil

dest_dir = DATA_DIRECTORY / "images"
if not dest_dir.exists():
    dest_dir.mkdir(parents=True, exist_ok=True)

for img in test_images:
    shutil.copy2(IMAGES_DIRECTORY / f"{img}.png", dest_dir / f"{img}.png")

Next we'll add a "submission format" CSV file to /data.

Note: A submission format file in this case is simply a CSV file that has the correct column and row indices required for a valid submission. Using this type of standard file is a useful way to ensure that all participants understand how their submission needs to be formatted in order to be accepted, and we tend to use these in most of our challenges.

A submission format file for the full training set should already be present in /data_dev. We'll make a copy of this that only includes rows pertaining to the images in our local test set, and save it in /data.

In [14]:
submission_format = pd.read_csv(DEV_DIRECTORY / "submission_format.csv", index_col="image_id")
submission_format_val = submission_format.loc[test_images]
submission_format_val.to_csv(DATA_DIRECTORY / "submission_format.csv")

Let's also create a version of the labels file that only includes rows pertaining to our local test set. We can use this later for local scoring (see below).

In [15]:
train_labels = pd.read_csv(DEV_DIRECTORY / "train_labels.csv", index_col="image_id")
test_labels = train_labels.loc[test_images]
test_labels.to_csv(DATA_DIRECTORY / "test_labels.csv")

Download a pretrained model

For this benchmark example, we will rely on YOLO, a commonly used algorithm for object detection. This will suit our purposes for demonstrating a very basic baseline approach, but you should explore more sophisticated methods yourself.

To download the latest v8 YOLO model, we'll use the ultralytics library. The code below will download the model and move it into the example_benchmark directory, where we're assembling the components of our submission.

In [16]:
from ultralytics import YOLO
model = YOLO("yolov8n.pt")

shutil.move('yolov8n.pt', PROJ_DIRECTORY / "example_benchmark" / "yolov8n.pt")
Downloading https://github.com/ultralytics/assets/releases/download/v8.1.0/yolov8n.pt to 'yolov8n.pt'...
100%|██████████| 6.23M/6.23M [00:00<00:00, 11.7MB/s]
Out[16]:
PosixPath('/home/isaac/Documents/spacecraft-pose-object-detection-runtime/example_benchmark/yolov8n.pt')

Review the benchmark submission scripts

Now let's take a look at the 2 files in /example_benchmark. This is the directory we are going to convert into a submission.zip file, which you can submit for the challenge.

  • The main.sh shell script is a required file for any submission to this challenge. Our code execution platform will run this script, and you can have it call other scripts and resources as needed for your submission.
  • The main.py Python script is called by main.sh in this benchmark example, and this is where the work of generating predictions actually happens. There is no requirement that you also use Python here, but that's the approach we've taken since it is such a common one. The main.py script will iterate through all the images in the submission_format.csv and generate predictions using YOLO. If YOLO doesn't return a prediction, we simply generate a bounding box for the center of the image.

The example_benchmark directory should now contain the following files. Note that we do need to include the yolov8n.pt file because the submission will have no internet access when running on our platform.

spacecraft-pose-object-detection-runtime/
└── example_benchmark/
    ├── main.py
    ├── main.sh
    └── yolov8n.pt

Three commands to prepare your submission

To run and test the benchmark example, you just need to execute the following 3 commands:

  1. make pull
  2. make pack-benchmark
  3. make test-submission

These are defined in the project Makefile here. We'll walk through what each one does now.

make pull

To ensure that all participants are using the same runtime environment, we have a publicly accessible docker image hosted on Azure Container Registry.

The make pull command pulls the official version of the docker image and stores it locally. Having a local version of the competition image allows you to test your submission using the same image that is used during code execution.

Note: This command can take a little while to run the first time you pull the image. But after that it will be relatively quick since you'll have all the layers cached locally. You don't need to pull the image again each time you test your submission, unless the image has changed.

In [17]:
!cd {PROJ_DIRECTORY} && make pull
docker pull spacecraftpose.azurecr.io/spacecraft-pose-object-detection:latest
latest: Pulling from spacecraft-pose-object-detection
Digest: sha256:025731e47e851c8f57363fc4d5a2e3fd44ec1a64f2089ba9d728ad7c8057f3a7
Status: Image is up to date for spacecraftpose.azurecr.io/spacecraft-pose-object-detection:latest
spacecraftpose.azurecr.io/spacecraft-pose-object-detection:latest

You should now have a local copy of the docker image, which you can verify by running:

In [18]:
!docker images | grep spacecraft-pose-object-detection
spacecraftpose.azurecr.io/spacecraft-pose-object-detection   latest               87b5f8364f26   2 days ago     5.73GB

make pack-benchmark

This command simply goes to your example_benchmark directory, zips the contents, and writes the zip archive to submission/submission.zip.

In [19]:
!cd {PROJ_DIRECTORY} && make pack-benchmark
mkdir -p submission/
cd example_benchmark; zip -r ../submission/submission.zip ./*
  adding: main.py (deflated 61%)
  adding: main.sh (deflated 26%)
  adding: yolov8n.pt (deflated 9%)

Note: The make pack-benchmark command will check to see if you already have a submission/submission.zip and error if you do, so as not to overwrite existing work. If you already have this file, you'll need to manually remove it before running the command.

After running the above command, we should now have a new submission/submission.zip.

spacecraft-pose-object-detection-runtime/
├── benchmark_src/
│   ├── main.py
│   ├── main.sh
│   └── yolov8n.pt
└── submission/
    └── submission.zip   <---- new file, this is what gets submitted on platform

This is the file that we will eventually upload to the competition platform for code execution. But before doing that, we want to test it locally.

make test-submission

This command simulates what happens during actual code execution, launching an instance of the official Docker image and running the same inference process that runs on the competition platform. The required host directories are mounted on the container, and the entrypoint script main.sh is executed. Note that when testing locally the contents of your local data/ directory will be mounted on the container, whereas when your submission is running on our platform, the unseen test set will be mounted as data/.

In [20]:
!cd {PROJ_DIRECTORY} && make test-submission
mkdir -p submission/
chmod -R 0777 submission/
docker run \
	-it \
	--network none \
	--mount type=bind,source="/home/isaac/Documents/spacecraft-pose-object-detection-runtime"/data,target=/code_execution/data,readonly \
	--mount type=bind,source="/home/isaac/Documents/spacecraft-pose-object-detection-runtime"/submission,target=/code_execution/submission \
	--shm-size 8g \
	--name spacecraft-pose-object-detection \
	--rm \
	87b5f8364f26
+ main
+ tee /code_execution/submission/log.txt
+ cd /code_execution
+ curl --silent --connect-timeout 10 --max-time 12 www.example.com
+ data_directory=/code_execution/data
+ format_filename=/code_execution/data/submission_format.csv
+ (( i=0 ))
+ (( i<=5 ))
+ t=0
+ '[' -f /code_execution/data/submission_format.csv ']'
+ echo 'found /code_execution/data/submission_format.csv after 0 seconds; data is mounted'
found /code_execution/data/submission_format.csv after 0 seconds; data is mounted
+ break
+ '[' '!' -f /code_execution/data/submission_format.csv ']'
+ expected_filename=main.sh
++ zip -sf ./submission/submission.zip
+ submission_files='Archive contains:
  main.py
  main.sh
  yolov8n.pt
Total 3 entries (6538117 bytes)'
+ grep -q main.sh
+ echo Unpacking submission
Unpacking submission
+ unzip ./submission/submission.zip -d ./workdir
Archive:  ./submission/submission.zip
  inflating: ./workdir/main.py       
  inflating: ./workdir/main.sh       
  inflating: ./workdir/yolov8n.pt    
+ echo Printing submission contents
Printing submission contents
+ find workdir
workdir
workdir/main.py
workdir/yolov8n.pt
workdir/main.sh
+ pushd workdir
/code_execution/workdir /code_execution
+ sh main.sh
2024-02-20 00:35:55.414 | INFO     | __main__:main:57 - using data dir: /code_execution/data
2024-02-20 00:35:55.472 | INFO     | __main__:main:76 -   0%|          | 0/100 [00:00<?, ?it/s]
2024-02-20 00:35:57.597 | INFO     | __main__:main:76 -  10%|█         | 10/100 [00:02<00:19,  4.70it/s]
2024-02-20 00:35:58.131 | INFO     | __main__:main:76 -  20%|██        | 20/100 [00:02<00:09,  8.41it/s]
2024-02-20 00:35:58.667 | INFO     | __main__:main:76 -  30%|███       | 30/100 [00:03<00:06, 11.22it/s]
2024-02-20 00:35:59.204 | INFO     | __main__:main:76 -  40%|████      | 40/100 [00:03<00:04, 13.32it/s]
2024-02-20 00:35:59.746 | INFO     | __main__:main:76 -  50%|█████     | 50/100 [00:04<00:03, 14.80it/s]
2024-02-20 00:36:00.290 | INFO     | __main__:main:76 -  60%|██████    | 60/100 [00:04<00:02, 15.85it/s]
2024-02-20 00:36:00.809 | INFO     | __main__:main:76 -  70%|███████   | 70/100 [00:05<00:01, 16.82it/s]
2024-02-20 00:36:01.347 | INFO     | __main__:main:76 -  80%|████████  | 80/100 [00:05<00:01, 17.35it/s]
2024-02-20 00:36:01.885 | INFO     | __main__:main:76 -  90%|█████████ | 90/100 [00:06<00:00, 17.72it/s]
+ popd
/code_execution
+ pytest --no-header -vv tests/test_submission.py
============================= test session starts ==============================
collecting ... collected 2 items

tests/test_submission.py::test_submission_exists PASSED                  [ 50%]
tests/test_submission.py::test_submission_matches_submission_format PASSED [100%]

============================== 2 passed in 0.94s ===============================
+ exit_code=0
+ cp /code_execution/submission/log.txt /tmp/log
+ exit 0

Once the test run has completed, we should now have a new file with our predictions at submission/submission.csv.

spacecraft-pose-object-detection-runtime/
├── benchmark_src/
│   ├── main.py
│   ├── main.sh
│   └── yolov8n.pt
└── submission/
    ├── submission.zip   <---- this is what gets submitted on platform
    └── submission.csv   <---- new file, predictions on test set

We also provide a scoring script that computes your score using the same calculation that's used for the public leaderboard. You can generate a score for your local testing with a command like the one below. Remember that this score will be computed on your local test set, and your score on the public leaderboard will be based on an unseen test set.

python scripts/score.py submission/submission.csv data/test_labels.csv

Submitting to the platform

We're almost done. Assuming that our test run completed and the submission.csv looks correct, it's time to submit the code on the platform.

  • Go to the competition submissions page and upload your submission/submission.zip.
  • Please be patient while your submission is uploaded and executed. Your job may be queued if other jobs are still pending.
  • You can track the status of your submission on the Code Execution Status page. Logs will become available once the submission begins processing. To see them click on "View Log".

Once your submission has been successfully uploaded, you will see something like this on the Code Execution Status page:

code execution status

Please be patient while your code is running. You may want to follow the links to check the logs for your job, which are live updated as your code job progresses.

Once your job has completed, head over to the Submissions page where you should be able to see your score. It will look something like this, except that we're sure you can do better than the benchmark!

score

That's it! You're on your way to creating your own code submission!

Head over to the competition homepage to get started. And have fun! We can't wait to see what you build!

Images courtesy of NASA.