Skip to main content

Download notebook · View source · Open in Colab

Image and Video Preprocessing or Augmentation

ApertureDB applies preprocessing and augmentation operations when storing or retrieving images, videos, and regions of interest. Operations on retrieval produce a transformed result without requiring a separately stored copy. The examples convert an image format, retrieve a thumbnail, and sample and resize a video.

The examples require Python 3.10 or later, a configured ApertureDB connection, and permission to create, read, and delete objects. The sample media comes from a fixed repository revision; SHA-256 checks identify incomplete or changed downloads. A unique documentation_run property limits queries and cleanup to this run. Keep the setup variables available until cleanup completes.

# Install the required client packages if needed
%pip install --quiet "aperturedb==0.5.1"
Note: you may need to restart the kernel to use updated packages.
from aperturedb.CommonLibrary import create_connector

# Create the connector for ApertureDB
client = create_connector()

def checked_query(query, input_blobs=None):
response, output_blobs = client.query(query, input_blobs or [])
assert isinstance(response, list) and len(response) == len(query), response
for command in response:
assert next(iter(command.values())).get("status") == 0, response
return response, output_blobs

import uuid
documentation_run = uuid.uuid4().hex
# Simple query to see how the database is doing
# https://docs.aperturedata.io/query_language/Reference/db_commands/GetStatus
query = [{
"GetStatus": {
}
}]

# Execute the query to get back a JSON response for GetStatus
response, blobs = checked_query(query)

client.print_last_response()
[
{
"GetStatus": {
"info": "OK",
"status": 0,
"system": "ApertureDB",
"version": "0.19.15"
}
}
]

Add a reformatted Image of a Dish to ApertureDB​

The source image is JPEG. Setting format to png in AddImage stores it as PNG.

from pathlib import Path
from urllib.request import urlopen
import hashlib

fixtures = {'focaccia_brittany.jpg': '4b104ba601ab3bf1126579e9dc795842551fc25be9ab2640795da2fb461f7061', 'crepe_flambe.mp4': '53275e11da0dcef74044be4ec9fd219d56e83fc8f57ae900da0120bf8551eaf1'}
for filename, expected_sha256 in fixtures.items():
path = Path("data") / filename
path.parent.mkdir(exist_ok=True)
url = "https://raw.githubusercontent.com/aperture-data/Cookbook/e333f6c59070b9165033d9ddd5af852a6b9624ba/notebooks/simple/data/" + filename
if not path.exists() or hashlib.sha256(path.read_bytes()).hexdigest() != expected_sha256:
with urlopen(url, timeout=60) as response:
path.write_bytes(response.read())
assert hashlib.sha256(path.read_bytes()).hexdigest() == expected_sha256
print("Verified fixture:", filename)
Verified fixture: focaccia_brittany.jpg
Verified fixture: crepe_flambe.mp4
query = [{
"AddImage": {
# Notice the missing "class" property since we already know its an Image (represented as _Image in ApertureDB)
"properties": {
'documentation_run': documentation_run,
"name": "Focaccia",
"id": 35,
"contributor": "Vishakha",
"category": "starter",
"cuisine": "Italian",
"location": "Brittany",
"caption": "fresh made focaccia with herbs from the garden"
},
"format": "png",
"if_not_found": ['all', ['$id', '==', 35], ['$documentation_run', '==', documentation_run]]
}
}]

# Read the image data as a binary blob
fd = open("data/focaccia_brittany.jpg", 'rb')
image_arr = [ fd.read() ]
fd.close()

response, blobs = checked_query(query, image_arr)

client.print_last_response()

from io import BytesIO
from PIL import Image as PILImage
stored, stored_blobs = checked_query([{"FindImage": {"constraints": ["$documentation_run", "==", documentation_run], "blobs": True}}])
assert stored[0]["FindImage"]["returned"] == 1
assert PILImage.open(BytesIO(stored_blobs[0])).format == "PNG"
[
{
"AddImage": {
"status": 0
}
}
]

Retrieve an image thumbnail​

The query resizes the stored image to 100 × 100 pixels and returns JPEG bytes. The stored PNG remains available for other retrieval operations.

from aperturedb import NotebookHelpers as nh   # Our helper package for image displays and other utilities

query = [{
"FindImage": {
"constraints": ['all', ['all', ['$name', '==', 'Focaccia'], ['$location', '>', 'A']], ['$documentation_run', '==', documentation_run]],
"operations": [{
"type": "resize",
"width": 100,
"height": 100
} ],
"as_format": "jpg",
"blobs": True
}
}]

response, blobs = checked_query(query)

# Instead of using nh.display, which automatically reformats an image
# for display, we use IPython.display to show how its returned from the query.
from IPython.display import display,Image
num_images = response[0]["FindImage"]["returned"]
print(num_images)
for count in range(num_images):
image = Image(blobs[count], format="JPEG")
display(image)

assert num_images == 1 and len(blobs) == 1
thumbnail = PILImage.open(BytesIO(blobs[0]))
assert thumbnail.size == (100, 100) and thumbnail.format == "JPEG"
1

jpeg

Videos can similarly be pre-processed when adding or searching​

Video commands also accept operations during ingestion or retrieval. This query stores the original cooking video so the following queries can return either the original or a transformed result.

query = [{
"AddVideo": {
# Notice the missing "class" property since we already know its a Video (represented as _Video in ApertureDB)
"properties": {
'documentation_run': documentation_run,
"name": "crepe_flambe",
"id": 45,
"category": "dessert",
"cuisine": "French",
"location": "Brittany",
"caption": "Special Brittany flambe crepe"
},
"if_not_found": ['all', ['$id', '==', 45], ['$documentation_run', '==', documentation_run]]
}
}]

# Read the image data as a binary blob
fd = open("data/crepe_flambe.mp4", 'rb')
array = [ fd.read() ]
fd.close()

response, blobs = checked_query(query, array)

client.print_last_response()
[
{
"AddVideo": {
"status": 0
}
}
]

Original video​

FindVideo with blobs: True returns the stored video bytes.

from aperturedb import NotebookHelpers as nh   # Our helper package for image displays and other utilities

query = [{
"FindVideo": {
"constraints": ['all', ['all', ['$name', '>=', 'crepe'], ['$location', '>', 'A']], ['$documentation_run', '==', documentation_run]],
"blobs": True, # This is set to False by default
"results": {
"all_properties": True
}
}
}]

response, blobs = checked_query(query)

client.print_last_response()
num_videos = response[0]["FindVideo"]["returned"]
for count in range(num_videos):
nh.display_video_mp4(blobs[count])


import cv2
import tempfile
from pathlib import Path

def video_info(video_blob):
with tempfile.TemporaryDirectory() as temporary:
path = Path(temporary) / "video.mp4"
path.write_bytes(video_blob)
capture = cv2.VideoCapture(str(path))
try:
assert capture.isOpened(), "Returned video could not be decoded"
return {
"width": int(capture.get(cv2.CAP_PROP_FRAME_WIDTH)),
"height": int(capture.get(cv2.CAP_PROP_FRAME_HEIGHT)),
"frames": int(capture.get(cv2.CAP_PROP_FRAME_COUNT)),
}
finally:
capture.release()

assert num_videos == 1 and len(blobs) == 1
original_video = video_info(blobs[0])
assert original_video["frames"] > 150
[
{
"FindVideo": {
"blobs_start": 0,
"entities": [
{
"_blob_index": 0,
"_duration_us": 5500000,
"_fps": 30,
"_frame_count": 165,
"_frame_height": 480,
"_frame_width": 852,
"_uniqueid": "5.0.100",
"caption": "Special Brittany flambe crepe",
"category": "dessert",
"cuisine": "French",
"documentation_run": "beac3ae3ddfc445da883bb8c17e4248f",
"id": 45,
"location": "Brittany",
"name": "crepe_flambe"
}
],
"returned": 1,
"status": 0
}
}
]

Sample and pre-process the cooking video when retrieving​

The query resizes the video to 300 × 300 pixels and returns every fifth frame from frame 30 through frame 150. The result contains 25 frames.

query = [{
"FindVideo": {
"constraints": ['all', ['all', ['$name', '>=', 'crepe'], ['$location', '>', 'A']], ['$documentation_run', '==', documentation_run]],
"blobs": True,
"operations": [{ # Original videos are longer. We just want to sample and prepare them
"type": "resize", # performance depends on the video complexity
"height": 300,
"width": 300
},{
"type": "interval", # Interval Operation, performance depends on the video complexity
"start": 30, # Start at frame 30 (frame numbering begins at zero)
"stop": 150, # Include frame 150
"step": 5 # Read every 5th frame
}]
}
}]

response, blobs = checked_query(query)

print(client.get_last_response_str())
nh.display_video_mp4(blobs[0])

assert response[0]["FindVideo"]["returned"] == 1 and len(blobs) == 1
transformed_video = video_info(blobs[0])
assert (transformed_video["width"], transformed_video["height"]) == (300, 300)
assert transformed_video["frames"] == len(range(30, 151, 5)), transformed_video
print("Transformed video:", transformed_video)
[
{
"FindVideo": {
"blobs_start": 0,
"returned": 1,
"status": 0
}
}
]
Transformed video: {'width': 300, 'height': 300, 'frames': 25}

Cleanup​

The cleanup queries remove only the image and video marked with this run's identifier.

query = [{
"DeleteVideo": {
"constraints": ['all', ['$name', '==', 'crepe_flambe'], ['$documentation_run', '==', documentation_run]]
}
},{
"DeleteImage": {
"constraints": ['all', ['$name', '==', 'Focaccia'], ['$documentation_run', '==', documentation_run]]
}
}]

response, blobs = checked_query(query)

client.print_last_response()
# Verify cleanup for every object type this run created.
remaining, _ = checked_query([
{"FindImage": {"constraints": ["$documentation_run", "==", documentation_run], "results": {"all_properties": True}}},
{"FindVideo": {"constraints": ["$documentation_run", "==", documentation_run], "results": {"all_properties": True}}},
])
assert all(next(iter(command.values()))["returned"] == 0 for command in remaining)
[
{
"DeleteVideo": {
"count": 1,
"status": 0
}
},
{
"DeleteImage": {
"count": 1,
"status": 0
}
}
]

What's next?​