Skip to main content

Download notebook · View source · Open in Colab

Interact with PyTorch Objects

ApertureDB's PyTorch integration translates dataset items into database commands and makes stored images available through a PyTorch DataLoader. This example ingests five COCO validation images with annotations, then retrieves them as labeled arrays.

Prerequisites​

  • An ApertureDB connection profile with permission to add, read, and delete images, bounding boxes, polygons, and entities. Configure client access before running the cells. An APERTUREDB_JSON profile also works for a local server.
  • Python 3.10 with the package versions below. PyTorch and torchvision are optional dependencies; the basic SDK installation does not install them. No GPU is required.
  • Public HTTPS access and about 300 MB of working space. Setup downloads the official COCO 2017 annotation archive (253 MB) and five validation images. It checks cached files before reuse.

A unique dataset marker separates each run's objects from other data. Cleanup deletes only those images, annotations, and category entities. Run cleanup before closing the notebook, including after an interrupted run.

Install the SDK and PyTorch dependencies​

The installation cell includes the SDK and PyTorch dependencies. Restart the kernel after installation before importing these packages.

%pip install --quiet "aperturedb" "numpy==1.26.4" "torch==2.2.2" "torchvision==0.17.2" "pycocotools==2.0.10" "opencv-python-headless==4.11.0.86" "python-dotenv==1.1.1"
Note: you may need to restart the kernel to use updated packages.

Download a reproducible COCO subset​

The example downloads five images, the annotation archive, and a helper from a fixed SDK revision. SHA-256 checks identify the expected files; downloads remain in coco/ for reuse. The helper imports the first annotation for each image, including its bounding box and segmentation polygons.

from pathlib import Path
import hashlib
import json
import sys
import urllib.request
import zipfile

coco_dir = Path("coco")
coco_dir.mkdir(exist_ok=True)
fixture_manifest = {'annotations_trainval2017.zip': {'url': 'https://s3.amazonaws.com/images.cocodataset.org/annotations/annotations_trainval2017.zip', 'sha256': '113a836d90195ee1f884e704da6304dfaaecff1f023f49b6ca93c4aaae470268'}, 'CocoDataPyTorch.py': {'url': 'https://raw.githubusercontent.com/aperture-data/aperturedb-python/827399dc18baa0fbfca9f7c4abdecc990211f9e8/examples/CocoDataPyTorch.py', 'sha256': '585e3d8b89967bbff26517a442c1db86c715cdac4131b3933272d164f6fba8d5'}, '000000000724.jpg': {'url': 'https://s3.amazonaws.com/images.cocodataset.org/val2017/000000000724.jpg', 'sha256': '5c0e559c75d3969c8e3e297b61f61063f78045c9d4802b526ba616361f3823fd'}, '000000000776.jpg': {'url': 'https://s3.amazonaws.com/images.cocodataset.org/val2017/000000000776.jpg', 'sha256': '1dd31e9059c491992be2f562624eb4093e17aee08b4f7baf5ff9ea24543b0a33'}, '000000000285.jpg': {'url': 'https://s3.amazonaws.com/images.cocodataset.org/val2017/000000000285.jpg', 'sha256': 'f3a2974ce3686332609124c70e3e6a2e3aca43fccf1cd1bd7c5c03820977f57d'}, '000000000139.jpg': {'url': 'https://s3.amazonaws.com/images.cocodataset.org/val2017/000000000139.jpg', 'sha256': 'ffe0f0cec3b2e27aab1967229cdf0a0d7751dcdd5800322f0b8ac0dffb3b8a8d'}, '000000000632.jpg': {'url': 'https://s3.amazonaws.com/images.cocodataset.org/val2017/000000000632.jpg', 'sha256': 'a4cd7f45ac1ce27eaafb254b23af7c0b18a064be08870ceaaf03b2147f2ce550'}}
image_ids = [139, 285, 632, 724, 776]

def verified_download(name, asset):
path = coco_dir / name
if not path.exists():
temporary = path.with_suffix(path.suffix + ".download")
urllib.request.urlretrieve(asset["url"], temporary)
assert hashlib.sha256(temporary.read_bytes()).hexdigest() == asset["sha256"], name
temporary.replace(path)
assert hashlib.sha256(path.read_bytes()).hexdigest() == asset["sha256"], name
return path

for name, asset in fixture_manifest.items():
verified_download(name, asset)
with zipfile.ZipFile(coco_dir / "annotations_trainval2017.zip") as archive:
annotations = json.loads(archive.read("annotations/instances_val2017.json"))
subset = {key: value for key, value in annotations.items() if key not in ("images", "annotations")}
subset["images"] = [item for item in annotations["images"] if item["id"] in image_ids]
subset["annotations"] = [item for item in annotations["annotations"] if item["image_id"] in image_ids]
assert len(subset["images"]) == len(image_ids)
(coco_dir / "instances_sample.json").write_text(json.dumps(subset), encoding="utf-8")
sys.path.insert(0, str(coco_dir.resolve()))
print("Verified COCO fixture image IDs:", image_ids)
Verified COCO fixture image IDs: [139, 285, 632, 724, 776]

Load the PyTorch dataset into ApertureDB​

CocoDataPyTorch subclasses aperturedb.PyTorchData. Its generate_query method translates each CocoDetection item into commands and image bytes. The subclass below adds a run marker to every generated object and scopes category deduplication to this run.

from collections import Counter
from uuid import uuid4
from dotenv import load_dotenv
from aperturedb.ParallelLoader import ParallelLoader
from aperturedb.CommonLibrary import create_connector
from CocoDataPyTorch import CocoDataPyTorch

load_dotenv()
client = create_connector()
run_id = "pytorch-loader-" + uuid4().hex
scope = ["$documentation_run", "==", run_id]
dataset_name = "coco_validation_" + run_id

def checked_query(query):
response, blobs = client.query(query)
assert len(response) == len(query), response
assert all(next(iter(item.values()))["status"] == 0 for item in response), response
return response, blobs

checked_query([{"GetStatus": {}}])

class ScopedCocoDataPyTorch(CocoDataPyTorch):
def generate_query(self, idx):
query, blobs = super().generate_query(idx)
for command in query:
name, body = next(iter(command.items()))
body.setdefault("properties", {})["documentation_run"] = run_id
if "if_not_found" in body:
body["if_not_found"] = ["all", scope, body["if_not_found"]]
return query, blobs

coco_detection = ScopedCocoDataPyTorch(
dataset_name=dataset_name, root=str(coco_dir),
annotationsFile=str(coco_dir / "instances_sample.json"),
)
records = list(coco_detection)
assert len(records) == len(image_ids) == 5
expected = Counter(next(iter(command)) for query, _ in records for command in query)
assert expected["AddImage"] == 5 and expected["AddPolygon"] > 0
loader = ParallelLoader(client)
loader.ingest(records, batchsize=1, numthreads=2, stats=True)
assert loader.error_counter == 0
assert loader.get_succeeded_queries() == len(records)
for kind in ("Image", "BoundingBox", "Polygon"):
body = {"constraints": scope, "results": {"count": True}}
if kind in ("Image", "BoundingBox"):
body["blobs"] = False
response, _ = checked_query([{"Find" + kind: body}])
assert response[0]["Find" + kind]["count"] == expected["Add" + kind], response
print("Verified object counts:", dict(expected))
loading annotations into memory...
Done (t=0.00s)
creating index...
index created!
Progress:   0%|          | 0.00/5.00 [00:00<?, ?items/s]
Progress: 100%|██████████| 5.00/5.00 [00:01<00:00, 4.99items/s]
============ ApertureDB Loader Stats ============
Total time (s): 1.0016632080078125
Total queries executed: 5
Avg Query time (s): 0.004410791397094727
Query time std: 0.001719249181867689
Avg Query Throughput (q/s): 453.43336828791035
Overall insertion throughput (element/s): 4.991697768299185
Total inserted elements: 5
Total successful commands: 25
=================================================
Verified object counts: {'AddEntity': 5, 'AddImage': 5, 'AddBoundingBox': 10, 'AddPolygon': 5}

The database contains five images and their first-annotation geometry, identified by this run's dataset marker. The displayed counts report the images, bounding boxes, and polygons stored by the loader.

Inspect a sample of the data that has been ingested into ApertureDB​

The image wrapper selects records by this run's dataset name, displays their annotations, and returns their metadata as a table.

from aperturedb import Images
import pandas as pd

images = Images.Images(client)
constraints = Images.Constraints()
constraints.equal("dataset_name", dataset_name)
images.search(limit=5, constraints=constraints)
images.display(show_bboxes=True)
properties = images.get_properties(images.get_props_names())
assert len(properties) == 5, properties
assert {row["image_id"] for row in properties.values()} == set(image_ids), properties
assert all(row["documentation_run"] == run_id for row in properties.values())
pd.json_normalize(properties.values())
.dataframe tbody tr th {
vertical-align: top;
}

.dataframe thead th {
text-align: right;
}
areabboxcategory_iddataset_namedocumentation_runidimage_idiscrowdsegmentation
0531.80710236.98 142.51 24.7 69.564coco_validation_pytorch-loader-59ca5f806dbf4c4...pytorch-loader-59ca5f806dbf4c4680fe75622880e7c9265471390[240.86, 211.31, 240.16, 197.19, 236.98, 192.2...
116433.58065120.07 71.83 134.49 153.0813coco_validation_pytorch-loader-59ca5f806dbf4c4...pytorch-loader-59ca5f806dbf4c4680fe75622880e7c92689887240[122.51, 174.51, 120.07, 110.94, 159.68, 71.83...
2275709.811051.43 68.81 584.57 563.9423coco_validation_pytorch-loader-59ca5f806dbf4c4...pytorch-loader-59ca5f806dbf4c4680fe75622880e7c95875622850[37.31, 373.02, 57.4, 216.61, 67.44, 159.21, 7...
365916.274201.44 56.22 344.51 471.3588coco_validation_pytorch-loader-59ca5f806dbf4c4...pytorch-loader-59ca5f806dbf4c4680fe75622880e7c911593547760[2.88, 129.73, 15.86, 109.55, 25.95, 102.34, 3...
464019.879403.27 266.85 401.23 208.2565coco_validation_pytorch-loader-59ca5f806dbf4c4...pytorch-loader-59ca5f806dbf4c4680fe75622880e7c93157246320[5.45, 269.03, 25.08, 273.39, 44.7, 289.75, 28...

png

png

png

png

png

Read ApertureDB images as a PyTorch dataset​

ApertureDBDataset fetches matching image blobs and returns decoded RGB arrays with labels. This example uses category_id as the label. The returned labels and image dimensions correspond to the selected COCO records.

from aperturedb import PyTorchDataset
from IPython.display import Image, display
import cv2
import time

query = [{"FindImage": {
"constraints": ["$dataset_name", "==", dataset_name],
"sort": "image_id", "blobs": True,
}}]
dataset = PyTorchDataset.ApertureDBDataset(client, query, label_prop="category_id")
assert len(dataset) == len(image_ids)
source_images = {item["id"]: item for item in subset["images"]}
source_labels = {image_id: next(item["category_id"] for item in subset["annotations"]
if item["image_id"] == image_id) for image_id in image_ids}
started = time.perf_counter()
processed = 0
for image_id, (pixels, label) in zip(image_ids, dataset):
original = source_images[image_id]
assert pixels.shape == (original["height"], original["width"], 3)
assert label == source_labels[image_id]
ok, encoded = cv2.imencode(".jpeg", cv2.cvtColor(pixels, cv2.COLOR_RGB2BGR))
assert ok
display(Image(data=encoded.tobytes(), format="JPEG"))
processed += 1
assert processed == len(image_ids)
print("Decoded images per second (including display):", processed / (time.perf_counter() - started))

jpeg

jpeg

jpeg

jpeg

jpeg

Decoded images per second (including display): 104.65310518802988

Create a dataset with resized images​

FindImage operations resize images during retrieval. Giving every image the same width and height allows the default DataLoader to combine them into a batch.

query = [{"FindImage": {
"constraints": ["$dataset_name", "==", dataset_name],
"sort": "image_id",
"operations": [{"type": "resize", "width": 224, "height": 224}],
"blobs": True,
}}]
dataset = PyTorchDataset.ApertureDBDataset(client, query, label_prop="category_id")
assert len(dataset) == len(image_ids)
for image_id, (pixels, label) in zip(image_ids, dataset):
assert pixels.shape == (224, 224, 3)
assert label == source_labels[image_id]
print("Verified five 224 x 224 RGB images and their labels.")
Verified five 224 x 224 RGB images and their labels.

Create a DataLoader from the dataset to use in other PyTorch methods.​

The PyTorch DataLoader supports batching, shuffling, and worker processes. This example keeps workers in the notebook process. Before passing these tensors to a typical vision model, permute HWC to CHW and apply that model's input scaling and normalization.

from torch.utils.data import DataLoader

# Default collation preserves the dataset's HWC array layout.
dl = DataLoader(dataset=dataset, batch_size=2, shuffle=False, num_workers=0)
assert len(dl) == 3 # Two full batches, then the fifth image.
seen_labels = []
for pixels, labels in dl:
assert tuple(pixels.shape[1:]) == (224, 224, 3)
assert len(pixels) == len(labels)
seen_labels.extend(labels.tolist())
assert seen_labels == [source_labels[image_id] for image_id in image_ids]
print("Verified DataLoader batches and labels for", len(seen_labels), "images.")
Verified DataLoader batches and labels for 5 images.

Remove this run's data​

Delete annotations before their images, and remove only the category entities with this run's marker. The local downloaded COCO files are retained for reuse.

for kind in ("Polygon", "BoundingBox", "Image", "Entity"):
body = {"constraints": scope}
if kind == "Entity":
body["with_class"] = "SuperCategory"
checked_query([{"Delete" + kind: body}])
body["results"] = {"count": True}
if kind in ("Image", "BoundingBox"):
body["blobs"] = False
response, _ = checked_query([{"Find" + kind: body}])
assert response[0]["Find" + kind]["count"] == 0, response
print("Verified zero remaining images, annotations, and category entities for this run.")
Verified zero remaining images, annotations, and category entities for this run.