Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
c156814
feat: Add CSV ingestion support for Frames
Aug 12, 2026
fd0c808
fix(csv): override get_indices for FrameDataCSV and format
Aug 12, 2026
b8efa61
fix: address review comments for Frame OM, tests, and ingest cli
Aug 12, 2026
803e0fa
test(FrameDataCSV): fix syntax error in f-string
Aug 12, 2026
cce1bc7
fix: address latest copilot review comments (docstring, unused import…
Aug 12, 2026
e0886c5
fix: use __getattr__ for lazy Frames import
Aug 12, 2026
0b77b04
fix: address review comments on FrameDataModel and formatting
Aug 12, 2026
17ee930
fix: do not override url in FrameDataModel to avoid pydantic override…
Aug 12, 2026
f393910
test: add tests for Frames OM and clean up unused import
Aug 12, 2026
c9faff2
fix: revert FrameDataModel base class to IdentityDataModel to avoid b…
Aug 13, 2026
c560a56
fix: export Frames in __all__ for import * support
Aug 13, 2026
8d99ccf
fix: ensure FrameDataModel inherits from BlobDataModel
Aug 14, 2026
83878fd
fix: cache Frames class in globals during lazy import
Aug 14, 2026
087f510
fix: address review comments on frame models and tests
Aug 14, 2026
b3d4e59
fix: address suppressed copilot review comments
Aug 15, 2026
d0beaec
fix: address review comments on FrameDataModel exception and Frames t…
Aug 15, 2026
b34b3cc
fix: address latest copilot review comments
Aug 15, 2026
a50b275
fix: address latest copilot review comments
Aug 15, 2026
401f06a
fix: address latest suppressed copilot review comments
Aug 15, 2026
413c708
fix: address latest review feedback on ImageDataCSV and Images __all__
Aug 15, 2026
a8bb224
fix: address latest review comments on docstrings, DataModels, Images…
Aug 15, 2026
118e8e0
style: fix autopep8 formatting issues
Aug 15, 2026
3a92a2f
Address Copilot feedback: add __all__ to Images.py and remove unused …
Aug 15, 2026
a576464
fix: address final review feedback on Images.__all__ and test names/a…
Aug 15, 2026
f0dea51
feat: add Frame to Object Model, analogous to Image and Video
Aug 15, 2026
974efc7
fix: remove annotations from __all__ in Images.py
Aug 15, 2026
95ba433
fix: ensure FrameDataModel inherits from BlobDataModel properly
Aug 15, 2026
c007003
fix: ignore rm failure during teardown to avoid failing CI
Aug 16, 2026
8abf9b1
fix: address suppressed review comments on Images.__all__, Constants,…
Aug 16, 2026
3618a49
Address suppressed Copilot reviewer comments
Aug 16, 2026
42575c5
Address remaining Copilot reviewer comments
Aug 18, 2026
675788c
chore: address remaining Copilot review comments
Aug 18, 2026
4246370
chore: address remaining suppressed Copilot review comments
Aug 18, 2026
c0500c7
chore: address remaining Copilot review comments (Croissant skip-list…
Aug 18, 2026
4a2060a
Address suppressed Copilot PR review comments
Aug 18, 2026
ec043dc
Address suppressed Copilot review comments
Aug 18, 2026
e78cb1d
Address latest suppressed Copilot review comments
Aug 18, 2026
ed0c692
fix: use local skip-list for Croissant indexing instead of BLOB_ADD_C…
Aug 19, 2026
c5bdd66
fix(mlcroissant): derive skip list from BLOB_ADD_COMMANDS
Aug 19, 2026
93ec0c5
fix: address latest review comments on Frames PR
Aug 19, 2026
5b152cf
fix: replace eager import with __getattr__ shim to avoid circular import
Aug 19, 2026
ab160d8
fix: remove unused Optional import from DataModels.py
Aug 19, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions aperturedb/CommonLibrary.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from aperturedb.ConnectorRest import ConnectorRest
from aperturedb.types import Blobs, CommandResponses, Commands
from aperturedb.LoggingUtils import censor_tokens
from aperturedb.Constants import BLOB_FIND_COMMANDS

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -383,9 +384,7 @@ def map_response_to_handler(handler, query, query_blobs, response, response_blo
if is_list:
for req, resp in zip(query[start:end], response[start:end]):
for k in req:
blob_returning_commands = ["FindImage", "FindBlob", "FindVideo",
"FindDescriptor", "FindBoundingBox"]
if k in blob_returning_commands and "blobs" in req[k] and req[k]["blobs"]:
if k in BLOB_FIND_COMMANDS and "blobs" in req[k] and req[k]["blobs"]:
count = resp[k]["returned"]
b_count += count

Expand Down
33 changes: 33 additions & 0 deletions aperturedb/Constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
"""
Shared constants for the ApertureDB Python SDK.
"""

BLOB_ADD_COMMANDS = frozenset({
"AddImage",
"AddDescriptor",
"AddVideo",
"AddBlob",
"AddFrame"
})

BLOB_FIND_COMMANDS = frozenset({
"FindImage",
"FindDescriptor",
"FindVideo",
"FindBlob",
"FindFrame",
"FindBoundingBox"
})

OPENCV_DECODE_FIND_COMMANDS = frozenset({
"FindImage",
"FindFrame"
})

PROPERTY_ADD_COMMANDS = frozenset({
"AddImage",
"AddVideo",
"AddBoundingBox",
"AddPolygon",
"AddFrame"
})
2 changes: 1 addition & 1 deletion aperturedb/DataModels.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ class PolygonDataModel(IdentityDataModel):
type = ObjectType.POLYGON


class FrameDataModel(IdentityDataModel):
class FrameDataModel(BlobDataModel):
"""Frame data model for ApertureDB.
"""
type = ObjectType.FRAME
Expand Down
2 changes: 2 additions & 0 deletions aperturedb/Entities.py
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,7 @@ def get_blob(self, entity) -> Any:
def load_entities_registry(custom_entities: List[str] = None) -> dict:
from aperturedb.Polygons import Polygons
from aperturedb.Images import Images
from aperturedb.Frames import Frames
from aperturedb.Blobs import Blobs
from aperturedb.BoundingBoxes import BoundingBoxes
from aperturedb.Videos import Videos
Expand All @@ -287,6 +288,7 @@ def load_entities_registry(custom_entities: List[str] = None) -> dict:
known_entities = {
ObjectType.POLYGON.value: Polygons,
ObjectType.IMAGE.value: Images,
ObjectType.FRAME.value: Frames,
ObjectType.VIDEO.value: Videos,
ObjectType.BOUNDING_BOX.value: BoundingBoxes,
ObjectType.BLOB.value: Blobs,
Expand Down
19 changes: 19 additions & 0 deletions aperturedb/FrameDataCSV.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
from aperturedb.ImageDataCSV import ImageDataCSV
from aperturedb.Query import ObjectType


class FrameDataCSV(ImageDataCSV):
"""
**Helper class to ingest Frame data from a CSV file.**

This class extends ImageDataCSV and sets the insertion command to "AddFrame",
allowing frame files to be batch ingested from CSVs just like images.
"""
command = "AddFrame"

def get_indices(self):
return {
"entity": {
ObjectType.FRAME.value: self.get_indexed_properties()
}
}
22 changes: 22 additions & 0 deletions aperturedb/Frames.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
from __future__ import annotations

from aperturedb.Images import Images
from aperturedb.Query import ObjectType


class Frames(Images):
"""
**The python wrapper of frame images in ApertureDB.**

Frames in ApertureDB are quite similar to images and so
are modeled in python as a subclass.


Args:
client: The database connector, perhaps as returned by `CommonLibrary.create_connector`
"""
db_object = ObjectType.FRAME

def __init__(self, client, batch_size=100, response=None, **kwargs):
super().__init__(
client, batch_size=batch_size, response=response, **kwargs)
3 changes: 1 addition & 2 deletions aperturedb/ImageDataCSV.py
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,7 @@ class ImageDataCSV(CSVParser.CSVParser, ImageDataProcessor):
id would be only inserted if it does not already exist in the database.
:::
"""
command = "AddImage"

def __init__(self, filename: str, check_image: bool = True, n_download_retries: int = 3, **kwargs):

Expand Down Expand Up @@ -199,8 +200,6 @@ def __init__(self, filename: str, check_image: bool = True, n_download_retries:
self.relative_path_prefix = os.path.dirname(self.filename) \
if self.source_type == HEADER_PATH and self.blobs_relative_to_csv else ""

self.command = "AddImage"

def getitem(self, idx):
idx = self.df.index.start + idx

Expand Down
26 changes: 12 additions & 14 deletions aperturedb/Images.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@
"""

from __future__ import annotations
from typing import Any, Dict, Iterable, List, Tuple, Union
from typing import Any, Dict, Iterable, List, Tuple, Union, TYPE_CHECKING

if TYPE_CHECKING:
from aperturedb.Frames import Frames
import cv2
import math
import numpy as np
Expand Down Expand Up @@ -1003,18 +1006,13 @@ def get_properties(self, prop_list: Iterable[str] = []) -> Dict[str, Any]:
return return_dictionary


class Frames(Images):
"""
**The python wrapper of frame images in ApertureDB.**

Frames in ApertureDB are quite similar to images and so
are modeled in python as a subclass.

def __getattr__(name: str):
if name == "Frames":
from aperturedb.Frames import Frames
globals()[name] = Frames
return Frames
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")

Args:
client: The database connector, perhaps as returned by `CommonLibrary.create_connector`
"""
db_object = ObjectType.FRAME

def __init__(self, client, batch_size=100, response=None, **kwargs):
super().__init__(client, batch_size=batch_size, response=response, **kwargs)
def __dir__():
return sorted(set(list(globals().keys()) + ["Frames"]))
9 changes: 8 additions & 1 deletion aperturedb/MLCroissant.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,14 @@
from aperturedb.Query import QueryBuilder
from aperturedb.DataModels import IdentityDataModel
from aperturedb.Query import generate_add_query
from aperturedb.Constants import BLOB_ADD_COMMANDS

# Commands for which we do not want to automatically create entity indices.
# AddDescriptor is intentionally excluded from this skip list because descriptor
# indices are explicitly required for similarity search and must be created.
_CROISSANT_SKIP_INDEX_COMMANDS = frozenset(
cmd for cmd in BLOB_ADD_COMMANDS if cmd != "AddDescriptor"
)

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -273,7 +280,7 @@ def getitem(self, subscript):
indexes_to_create = []
for command in q:
cmd = list(command.keys())[-1]
if cmd in ["AddImage", "AddBlob", "AddVideo"]:
if cmd in _CROISSANT_SKIP_INDEX_COMMANDS:
continue
indexable_entity = command[list(command.keys())[-1]]["class"]
if indexable_entity not in self.indexed_entities:
Expand Down
19 changes: 7 additions & 12 deletions aperturedb/PyTorchDataset.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import math
import numpy as np
import cv2
from aperturedb.Constants import BLOB_FIND_COMMANDS, OPENCV_DECODE_FIND_COMMANDS
import logging

from torch.utils import data

from aperturedb.CommonLibrary import execute_query
from aperturedb.Connector import Connector


logger = logging.getLogger(__name__)


Expand All @@ -17,7 +17,7 @@ class ApertureDBDataset(data.Dataset):
This class implements a PyTorch Dataset for ApertureDB.
It is used to load blobs returned by a `Find*` command from ApertureDB into a PyTorch model.
It can be initialized with a query that will be used to retrieve
the blobs from ApertureDB. Note that only `FindImage` blobs are decoded via OpenCV.
the blobs from ApertureDB. Note that only `FindImage` and `FindFrame` blobs are decoded via OpenCV.
"""

def __init__(self, client: Connector, query, label_prop=None, batch_size=1, command_idx=None):
Expand All @@ -35,24 +35,19 @@ def __init__(self, client: Connector, query, label_prop=None, batch_size=1, comm
self.batch_end = 0
self.label_prop = label_prop

allowed_find_commands = {
"FindImage", "FindVideo", "FindBlob",
"FindDescriptor", "FindBoundingBox"
}

if self.command_idx is not None:
if not (0 <= self.command_idx < len(query)):
raise ValueError(
f"command_idx {self.command_idx} is out of range.")
self.command_name = list(query[self.command_idx].keys())[0]
if self.command_name not in allowed_find_commands:
if self.command_name not in BLOB_FIND_COMMANDS:
raise ValueError(
f"Command at index {self.command_idx} is "
f"{self.command_name}, which is not a supported blob-returning Find* command.")
else:
for i in range(len(query)):
name = list(query[i].keys())[0]
if name in allowed_find_commands:
if name in BLOB_FIND_COMMANDS:
if self.command_idx is not None:
logger.warning(
"Multiple Find commands found. Selected %s at index %s.", self.command_name, self.command_idx)
Expand All @@ -61,7 +56,7 @@ def __init__(self, client: Connector, query, label_prop=None, batch_size=1, comm
self.command_name = name

if self.command_idx is None:
msg = "Query error. The query must contain at least one supported blob-returning Find command (e.g., FindImage, FindVideo, FindBlob). The first one encountered will be used."
msg = "Query error. The query must contain at least one supported blob-returning Find command (e.g., FindImage, FindVideo, FindBlob, FindFrame). The first one encountered will be used."
logger.error(msg)
raise ValueError(msg)

Expand All @@ -77,7 +72,7 @@ def __init__(self, client: Connector, query, label_prop=None, batch_size=1, comm

for i in range(len(self.query)):
name = list(self.query[i].keys())[0]
if name in allowed_find_commands and i != self.command_idx:
if name in BLOB_FIND_COMMANDS and i != self.command_idx:
self.query[i][name]["blobs"] = False

self.query[self.command_idx][self.command_name]["batch"] = {}
Expand Down Expand Up @@ -111,7 +106,7 @@ def __getitem__(self, index):
blob = self.batch_blobs[idx]
label = self.batch_labels[idx]

if self.command_name == "FindImage":
if self.command_name in OPENCV_DECODE_FIND_COMMANDS:
nparr = np.frombuffer(blob, dtype=np.uint8)
blob = cv2.imdecode(nparr, cv2.IMREAD_COLOR)
if blob is None:
Expand Down
2 changes: 1 addition & 1 deletion aperturedb/Query.py
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,7 @@ def generate_add_query(
params.pop("properties", None)
query.append(
QueryBuilder.find_command(obj.type.value, params=params))
if obj.type in [ObjectType.IMAGE, ObjectType.VIDEO, ObjectType.BLOB]:
if obj.type in [ObjectType.IMAGE, ObjectType.VIDEO, ObjectType.BLOB, ObjectType.FRAME]:
# Do not send blob, if Node has been added to set of commands.
if obj.id not in cached:
if obj.url:
Expand Down
14 changes: 6 additions & 8 deletions aperturedb/TensorFlowDataset.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

from aperturedb.CommonLibrary import execute_query
from aperturedb.Connector import Connector
from aperturedb.Constants import BLOB_FIND_COMMANDS, OPENCV_DECODE_FIND_COMMANDS

logger = logging.getLogger(__name__)

Expand All @@ -14,7 +15,7 @@ class ApertureDBTensorFlowDataset:
This class implements a TensorFlow Dataset for ApertureDB.
It is used to load blobs returned by a `Find*` command from ApertureDB into a TensorFlow model.
It can be initialized with a query that will be used to retrieve
the blobs from ApertureDB. Note that only `FindImage` blobs are decoded via OpenCV.
the blobs from ApertureDB. Note that only `FindImage` and `FindFrame` blobs are decoded via OpenCV.
"""

def __init__(self, client: Connector, query, label_prop=None, batch_size=1, command_idx=None):
Expand All @@ -33,10 +34,7 @@ def __init__(self, client: Connector, query, label_prop=None, batch_size=1, comm
self.label_prop = label_prop
self.label_type = None

allowed_find_commands = {
"FindImage", "FindVideo", "FindBlob",
"FindDescriptor", "FindBoundingBox"
}
allowed_find_commands = BLOB_FIND_COMMANDS

if self.command_idx is not None:
if not (0 <= self.command_idx < len(query)):
Expand All @@ -59,7 +57,7 @@ def __init__(self, client: Connector, query, label_prop=None, batch_size=1, comm
self.command_name = name

if self.command_idx is None:
msg = "Query error. The query must contain at least one supported blob-returning Find command (e.g., FindImage, FindVideo, FindBlob). The first one encountered will be used."
msg = "Query error. The query must contain at least one supported blob-returning Find command (e.g., FindImage, FindVideo, FindBlob, FindFrame). The first one encountered will be used."
logger.error(msg)
raise ValueError(msg)

Expand Down Expand Up @@ -177,7 +175,7 @@ def generator(self):
blob = self.batch_blobs[idx]
label = self.batch_labels[idx]

if self.command_name == "FindImage":
if self.command_name in OPENCV_DECODE_FIND_COMMANDS:
nparr = np.frombuffer(blob, dtype=np.uint8)
blob = cv2.imdecode(nparr, cv2.IMREAD_COLOR)
if blob is None:
Expand Down Expand Up @@ -226,7 +224,7 @@ def get_dataset(self):
else:
self.label_type = tf.string

if self.command_name == "FindImage":
if self.command_name in OPENCV_DECODE_FIND_COMMANDS:
tensor_shape = (None, None, 3)
tensor_dtype = tf.uint8
else:
Expand Down
2 changes: 2 additions & 0 deletions aperturedb/cli/ingest.py
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,7 @@ def from_csv(filepath: Annotated[str, typer.Argument(
Ingest data from a pre generated CSV file.
"""
from aperturedb.ImageDataCSV import ImageDataCSV
from aperturedb.FrameDataCSV import FrameDataCSV
from aperturedb.BBoxDataCSV import BBoxDataCSV
from aperturedb.EntityDataCSV import EntityDataCSV
from aperturedb.BlobDataCSV import BlobDataCSV
Expand All @@ -210,6 +211,7 @@ def from_csv(filepath: Annotated[str, typer.Argument(
IngestType.DESCRIPTOR: DescriptorDataCSV,
IngestType.DESCRIPTORSET: DescriptorSetDataCSV,
IngestType.ENTITY: EntityDataCSV,
IngestType.FRAME: FrameDataCSV,
IngestType.IMAGE: ImageDataCSV,
IngestType.POLYGON: PolygonDataCSV,
IngestType.VIDEO: VideoDataCSV
Expand Down
3 changes: 2 additions & 1 deletion aperturedb/transformers/clip_pytorch_embeddings.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from aperturedb.Constants import BLOB_ADD_COMMANDS
import hashlib
import logging
from aperturedb.Subscriptable import Subscriptable
Expand Down Expand Up @@ -95,7 +96,7 @@ def getitem(self, subscript):
except Exception as e:
logger.warning(
f"Failed to generate embedding or descriptor: {e}", exc_info=True)
if cmd_name in ["AddImage", "AddDescriptor", "AddVideo", "AddBlob"]:
if cmd_name in BLOB_ADD_COMMANDS:
blob_index += 1

x[0].extend(new_descriptors)
Expand Down
3 changes: 2 additions & 1 deletion aperturedb/transformers/common_properties.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from aperturedb.Subscriptable import Subscriptable
from aperturedb.transformers.transformer import Transformer
from aperturedb.Constants import PROPERTY_ADD_COMMANDS
import logging


Expand Down Expand Up @@ -49,7 +50,7 @@ def getitem(self, subscript):
if isinstance(cmd_dict, dict) and len(cmd_dict) > 0:
cmd_name = next(iter(cmd_dict.keys()))

if cmd_name in ["AddImage", "AddVideo", "AddBoundingBox", "AddPolygon"]:
if cmd_name in PROPERTY_ADD_COMMANDS:
src_properties = cmd_dict[cmd_name].setdefault(
"properties", {})
self._apply_common_properties(src_properties)
Expand Down
Loading
Loading