Source code for kwcoco.coco_objects1d

"""
Vectorized ORM-like objects used in conjunction with coco_dataset.

This powers the ``.images()``, ``.videos()``, and ``.annotation()`` methods of
:class:`kwcoco.CocoDataset`.

TODO:
    - [ ] The use of methods vs properties is inconsistent. This needs to be fixed, but backwards compatability is a consideration.

See:
    :func:`kwcoco.coco_dataset.MixinCocoObjects.categories`
    :func:`kwcoco.coco_dataset.MixinCocoObjects.videos`
    :func:`kwcoco.coco_dataset.MixinCocoObjects.images`
    :func:`kwcoco.coco_dataset.MixinCocoObjects.annots`
    :func:`kwcoco.coco_dataset.MixinCocoObjects.tracks`

"""
from os.path import join
import numpy as np
import ubelt as ub


__docstubs__ = """
from kwcoco.coco_dataset import CocoDataset
from typing import Dict
ObjT = Dict
"""


[docs] class ObjectList1D(ub.NiceRepr): """ Vectorized access to lists of dictionary objects Lightweight reference to a set of object (e.g. annotations, images) that allows for convenient property access. Types: ObjT = Ann | Img | Cat # can be one of these types ObjectList1D gives us access to a List[ObjT] Example: >>> import kwcoco >>> dset = kwcoco.CocoDataset.demo() >>> # Both annots and images are object lists >>> self = dset.annots() >>> self = dset.images() >>> # can call with a list of ids or not, for everything >>> self = dset.annots([1, 2, 11]) >>> self = dset.images([1, 2, 3]) >>> self.lookup('id') >>> self.lookup(['id']) """ def __init__(self, ids, dset, key): """ Args: ids (List[int]): list of ids dset (CocoDataset): parent dataset key (str): main object name (e.g. 'images', 'annotations') """ self._key = key self._ids = ids self._dset = dset def __nice__(self) -> str: return 'num={!r}'.format(len(self)) def __iter__(self): return iter(self._ids) def __len__(self) -> int: return len(self._ids) @property def _id_to_obj(self): return self._dset.index._id_lookup[self._key] def __getitem__(self, index): """ Args: index (int | slice): positional index or slice Returns: ObjectList1D | int """ if isinstance(index, slice): subids = self._ids[index] newself = self.__class__(subids, self._dset) return newself else: return self._ids[index]
[docs] def unique(self): """ Removes any duplicates entries in this object Returns: ObjectList1D """ subids = list(ub.unique(self._ids)) newself = self.__class__(subids, self._dset) return newself
@property def ids(self): """ Returns: List[int] """ return self._ids @property def objs(self): """ Get the underlying object dictionary for each object. Returns: List[ObjT]: all object dictionaries """ return list(ub.take(self._id_to_obj, self._ids))
[docs] def take(self, idxs): """ Take a subset by index Returns: ObjectList1D Example: >>> import kwcoco >>> self = kwcoco.CocoDataset.demo().annots() >>> assert len(self.take([0, 2, 3])) == 3 """ subids = list(ub.take(self._ids, idxs)) newself = self.__class__(subids, self._dset) return newself
[docs] def compress(self, flags): """ Take a subset by flags Returns: ObjectList1D Example: >>> import kwcoco >>> self = kwcoco.CocoDataset.demo().images() >>> assert len(self.compress([True, False, True])) == 2 """ subids = list(ub.compress(self._ids, flags)) newself = self.__class__(subids, self._dset) return newself
[docs] def peek(self): """ Return the first object dictionary Returns: ObjT: object dictionary Example: >>> import kwcoco >>> dset = kwcoco.CocoDataset.demo() >>> self = dset.images() >>> assert self.peek()['id'] == 1 >>> # Check that subsets return correct items >>> sub0 = self.compress([i % 2 == 0 for i in range(len(self))]) >>> sub1 = self.compress([i % 2 == 1 for i in range(len(self))]) >>> assert sub0.peek()['id'] == 1 >>> assert sub1.peek()['id'] == 2 """ id_ = self._ids[0] obj = self._id_to_obj[id_] return obj
[docs] def lookup(self, key, default=ub.NoParam, keepid=False): """ Lookup a list of object attributes Args: key (str | Iterable): name of the property you want to lookup can also be a list of names, in which case we return a dict default : if specified, uses this value if it doesn't exist in an ObjT. keepid: if True, return a mapping from ids to the property Returns: List[ObjT]: a list of whatever type the object is Dict[str, ObjT] Example: >>> import kwcoco >>> dset = kwcoco.CocoDataset.demo() >>> self = dset.annots() >>> self.lookup('id') >>> key = ['id'] >>> default = None >>> self.lookup(key=['id', 'image_id']) >>> self.lookup(key=['id', 'image_id']) >>> self.lookup(key='foo', default=None, keepid=True) >>> self.lookup(key=['foo'], default=None, keepid=True) >>> self.lookup(key=['id', 'image_id'], keepid=True) """ # Note: while the old _lookup code was slightly faster than this, the # difference is extremely negligable (179us vs 178us). if ub.iterable(key): return {k: self.lookup(k, default, keepid) for k in key} else: return self.get(key, default=default, keepid=keepid)
[docs] def sort_values(self, by, reverse=False, key=None): """ Reorders the items by an attribute. Args: by (str): The column attribute to sort by key (Callable | None) : Apply the key function to the values before sorting. Returns: ObjectList1D : copy of this object with new ids Example: >>> import kwcoco >>> dset = kwcoco.CocoDataset.demo('vidshapes8') >>> self = dset.images() >>> new = self.sort_values('frame_index') >>> frame_idxs = new.lookup('frame_index') >>> assert sorted(frame_idxs) == frame_idxs """ attrs = self.lookup(by) sortx = ub.argsort(attrs, reverse=reverse, key=key) new = self.take(sortx) return new
[docs] def get(self, key, default=ub.NoParam, keepid=False): """ Lookup a list of object attributes Args: key (str): name of the property you want to lookup default : if specified, uses this value if it doesn't exist in an ObjT. keepid: if True, return a mapping from ids to the property Returns: Dict[int, Any] | List[Any]: a list of whatever type the object is Example: >>> import kwcoco >>> dset = kwcoco.CocoDataset.demo() >>> self = dset.annots() >>> self.get('id') >>> self.get(key='foo', default=None, keepid=True) Example: >>> # xdoctest: +REQUIRES(module:sqlalchemy) >>> import kwcoco >>> dct_dset = kwcoco.CocoDataset.demo('vidshapes8', rng=303232) >>> dct_dset.anns[3]['blorgo'] = 3 >>> dct_dset.annots().lookup('blorgo', default=None) >>> for a in dct_dset.anns.values(): ... a['wizard'] = '10!' >>> dset = dct_dset.view_sql(force_rewrite=1) >>> assert dset.anns[3]['blorgo'] == 3 >>> assert dset.anns[3]['wizard'] == '10!' >>> assert 'blorgo' not in dset.anns[2] >>> dset.annots().lookup('blorgo', default=None) >>> dset.annots().lookup('wizard', default=None) >>> import pytest >>> with pytest.raises(KeyError): >>> dset.annots().lookup('blorgo') >>> dset.annots().lookup('wizard') >>> #self = dset.annots() """ if hasattr(self._dset, '_column_lookup') and default is ub.NoParam: # Special case for SQL speed. This only works on schema columns. try: # TODO: check if the column is in the stuctured schema return self._dset._column_lookup( tablename=self._key, key=key, rowids=self._ids) except KeyError: # We can read only the unstructured bit, which is the best we # can do in this case. _lutv = self._dset._column_lookup( tablename=self._key, key='_unstructured', rowids=self._ids) _lut = dict(zip(self._ids, _lutv)) # TODO: optimize the case where default is given else: _lut = self._id_to_obj if keepid: if default is ub.NoParam: attr_list = {_id: _lut[_id][key] for _id in self._ids} else: attr_list = {_id: _lut[_id].get(key, default) for _id in self._ids} else: if default is ub.NoParam: attr_list = [_lut[_id][key] for _id in self._ids] else: attr_list = [_lut[_id].get(key, default) for _id in self._ids] return attr_list
[docs] def _iter_get(self, key, default=ub.NoParam): """ Iterator version of get, not in stable API yet. """ # TODO: sql variant _lut = self._id_to_obj if default is ub.NoParam: attr_iter = (_lut[_id][key] for _id in self._ids) else: attr_iter = (_lut[_id].get(key, default) for _id in self._ids) return attr_iter
[docs] def set(self, key, values): """ Assign a value to each annotation Args: key (str): the annotation property to modify values (Iterable | Any): an iterable of values to set for each annot in the dataset. If the item is not iterable, it is assigned to all objects. Example: >>> import kwcoco >>> dset = kwcoco.CocoDataset.demo() >>> self = dset.annots() >>> self.set('my-key1', 'my-scalar-value') >>> self.set('my-key2', np.random.rand(len(self))) >>> print('dset.imgs = {}'.format(ub.urepr(dset.imgs, nl=1))) >>> self.get('my-key2') """ if not ub.iterable(values): values = [values] * len(self) elif not isinstance(values, list): values = list(values) assert len(self) == len(values) self._set(key, values)
[docs] def _set(self, key, values): """ faster less safe version of set """ objs = ub.take(self._id_to_obj, self._ids) for obj, value in zip(objs, values): obj[key] = value
[docs] def _lookup(self, key, default=ub.NoParam): """ Example: >>> # xdoctest: +REQUIRES(--benchmark) >>> import kwcoco >>> dset = kwcoco.CocoDataset.demo('shapes256') >>> self = annots = dset.annots() >>> # >>> import timerit >>> ti = timerit.Timerit(100, bestof=10, verbose=2) >>> # >>> for timer in ti.reset('lookup'): >>> with timer: >>> self.lookup('image_id') >>> # >>> for timer in ti.reset('_lookup'): >>> with timer: >>> self._lookup('image_id') >>> # >>> for timer in ti.reset('image_id'): >>> with timer: >>> self.image_id >>> # >>> for timer in ti.reset('raw1'): >>> with timer: >>> key = 'image_id' >>> [self._dset.anns[_id][key] for _id in self._ids] >>> # >>> for timer in ti.reset('raw2'): >>> with timer: >>> anns = self._dset.anns >>> key = 'image_id' >>> [anns[_id][key] for _id in self._ids] >>> # >>> for timer in ti.reset('lut-gen'): >>> with timer: >>> _lut = self._obj_lut >>> objs = (_lut[_id] for _id in self._ids) >>> [obj[key] for obj in objs] >>> # >>> for timer in ti.reset('lut-gen-single'): >>> with timer: >>> _lut = self._obj_lut >>> [_lut[_id][key] for _id in self._ids] """ return self.lookup(key, default=default)
[docs] def attribute_frequency(self): """ Compute the number of times each key is used in a dictionary Returns: Dict[str, int] Example: >>> import kwcoco >>> dset = kwcoco.CocoDataset.demo() >>> self = dset.annots() >>> attrs = self.attribute_frequency() >>> print('attrs = {}'.format(ub.urepr(attrs, nl=1))) """ attrs = ub.ddict(lambda: 0) for obj in self._id_to_obj.values(): for key, value in obj.items(): attrs[key] += 1 return attrs
[docs] class ObjectGroups(ub.NiceRepr): """ An object for holding a groups of :class:`ObjectList1D` objects """ def __init__(self, groups, dset): """ Args: groups (List[ObjectList1D]): list of object lists dset (CocoDataset): parent dataset """ self._groups = groups
[docs] def _lookup(self, key): return self._lookup(key) # broken?
def __getitem__(self, index): if isinstance(index, slice): subgroups = self._groups[index] newself = self.__class__(subgroups, self._dset) return newself else: return self._groups[index]
[docs] def lookup(self, key, default=ub.NoParam): return [group.lookup(key, default) for group in self._groups]
def __nice__(self) -> str: # import timerit # mu = timerit.core._trychar('μ', 'm') # sigma = timerit.core._trychar('σ', 's') mu = 'm' sigma = 's' len_list = list(map(len, self._groups)) num = len(self._groups) mean = np.mean(len_list) std = np.std(len_list) nice = 'n={!r}, {}={:.1f}, {}={:.1f}'.format( num, mu, mean, sigma, std) return nice
[docs] class Categories(ObjectList1D): """ Vectorized access to category attributes SeeAlso: :func:`kwcoco.coco_dataset.MixinCocoObjects.categories` Example: >>> from kwcoco.coco_objects1d import Categories # NOQA >>> import kwcoco >>> dset = kwcoco.CocoDataset.demo() >>> ids = list(dset.cats.keys()) >>> self = Categories(ids, dset) >>> print('self.name = {!r}'.format(self.name)) >>> print('self.supercategory = {!r}'.format(self.supercategory)) """ def __init__(self, ids, dset): """ Args: ids (List[int]): list of category ids dset (CocoDataset): parent dataset """ super().__init__(ids, dset, 'categories') @property def cids(self): return self.lookup('id') @property def name(self): return self.lookup('name') @property def supercategory(self): return self.lookup('supercategory', None)
[docs] class Videos(ObjectList1D): """ Vectorized access to video attributes SeeAlso: :func:`kwcoco.coco_dataset.MixinCocoObjects.videos` Example: >>> from kwcoco.coco_objects1d import Videos # NOQA >>> import kwcoco >>> dset = kwcoco.CocoDataset.demo('vidshapes5') >>> ids = list(dset.index.videos.keys()) >>> self = Videos(ids, dset) >>> print('self = {!r}'.format(self)) self = <Videos(num=5) at ...> """ def __init__(self, ids, dset): """ Args: ids (List[int]): list of video ids dset (CocoDataset): parent dataset """ super().__init__(ids, dset, 'videos') @property def images(self): """ Returns: ImageGroups Example: >>> import kwcoco >>> self = kwcoco.CocoDataset.demo('vidshapes8').videos() >>> print(self.images) <ImageGroups(n=8, m=2.0, s=0.0)> """ return ImageGroups( [self._dset.images(video_id=vidid) for vidid in self._ids], self._dset)
[docs] class Images(ObjectList1D): """ Vectorized access to image attributes Example: >>> import kwcoco >>> dset = kwcoco.CocoDataset.demo('photos') >>> images = dset.images() >>> print('images = {}'.format(images)) images = <Images(num=3)...> >>> print('images.gname = {}'.format(images.gname)) images.gname = ['astro.png', 'carl.jpg', 'stars.png'] SeeAlso: :func:`kwcoco.coco_dataset.MixinCocoObjects.images` """ def __init__(self, ids, dset): """ Args: ids (List[int]): list of image ids dset (CocoDataset): parent dataset """ super().__init__(ids, dset, 'images') @property def coco_images(self): return [self._dset.coco_image(gid) for gid in self] @property def gids(self): return self._ids @property def gname(self): return self.lookup('file_name') @property def gpath(self): root = self._dset.bundle_dpath return [join(root, gname) for gname in self.gname] @property def width(self): return self.lookup('width') @property def height(self): return self.lookup('height') @property def size(self): """ Example: >>> import kwcoco >>> self = kwcoco.CocoDataset.demo().images() >>> self._dset._ensure_imgsize() ... >>> print(self.size) [(512, 512), (328, 448), (256, 256)] """ return list(zip(self.lookup('width'), self.lookup('height'))) @property def area(self): """ Returns: List[float] Example: >>> import kwcoco >>> self = kwcoco.CocoDataset.demo().images() >>> self._dset._ensure_imgsize() ... >>> print(self.area) [262144, 146944, 65536] """ return [w * h for w, h in zip(self.lookup('width'), self.lookup('height'))] @property def n_annots(self): """ Returns: List[int] Example: >>> import kwcoco >>> self = kwcoco.CocoDataset.demo().images() >>> print(ub.urepr(self.n_annots, nl=0)) [9, 2, 0] """ return list(map(len, ub.take(self._dset.index.gid_to_aids, self._ids))) @property def aids(self): """ Returns: List[set] Example: >>> import kwcoco >>> self = kwcoco.CocoDataset.demo().images() >>> print(ub.urepr(list(map(list, self.aids)), nl=0)) [[1, 2, 3, 4, 5, 6, 7, 8, 9], [10, 11], []] """ return list(ub.take(self._dset.index.gid_to_aids, self._ids)) @property def annots(self): """ Returns: AnnotGroups Example: >>> import kwcoco >>> self = kwcoco.CocoDataset.demo().images() >>> print(self.annots) <AnnotGroups(n=3, m=3.7, s=3.9)> """ return AnnotGroups([self._dset.annots(sorted(aids)) for aids in self.aids], self._dset)
[docs] class Annots(ObjectList1D): """ Vectorized access to annotation attributes SeeAlso: :func:`kwcoco.coco_dataset.MixinCocoObjects.annots` Example: >>> import kwcoco >>> dset = kwcoco.CocoDataset.demo('photos') >>> annots = dset.annots() >>> print('annots = {}'.format(annots)) annots = <Annots(num=11)> >>> image_ids = annots.lookup('image_id') >>> print('image_ids = {}'.format(image_ids)) image_ids = [1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2] """ def __init__(self, ids, dset): """ Args: ids (List[int]): list of annotation ids dset (CocoDataset): parent dataset """ super().__init__(ids, dset, 'annotations') @property def aids(self): """ The annotation ids of this column of annotations """ return self._ids @property def images(self): """ Get the column of images Returns: Images """ return self._dset.images(self.gids) @property def image_id(self): return self.lookup('image_id') @property def category_id(self): return self.lookup('category_id') @property def gids(self): """ Get the column of image-ids Returns: List[int]: list of image ids """ return self.lookup('image_id') @property def cids(self): """ Get the column of category-ids Returns: List[int] """ return self.lookup('category_id') @property def cnames(self): """ Get the column of category names Returns: List[str] """ # TODO: deprecate cnames and use category_names instead return [cat['name'] for cat in ub.take(self._dset.cats, self.cids)] @cnames.setter def cnames(self, cnames): """ Args: cnames (List[str]): Example: >>> import kwcoco >>> self = kwcoco.CocoDataset.demo().annots([1, 2, 11]) >>> print('self.cnames = {!r}'.format(self.cnames)) >>> print('self.cids = {!r}'.format(self.cids)) >>> cnames = ['boo', 'bar', 'rocket'] >>> list(map(self._dset.ensure_category, set(cnames))) >>> self.cnames = cnames >>> print('self.cnames = {!r}'.format(self.cnames)) >>> print('self.cids = {!r}'.format(self.cids)) """ cats = map(self._dset._alias_to_cat, cnames) cids = (cat['id'] for cat in cats) self.set('category_id', cids) @property def category_names(self): """ Get the column of category names Returns: List[str] """ return self.cnames @category_names.setter def category_names(self, names): """ Get the column of category names Returns: List[str] """ self.cnames = names @property def detections(self): """ Get the kwimage-style detection objects Returns: kwimage.Detections Example: >>> # xdoctest: +REQUIRES(module:kwimage) >>> import kwcoco >>> self = kwcoco.CocoDataset.demo('shapes32').annots([1, 2, 11]) >>> dets = self.detections >>> print('dets.data = {!r}'.format(dets.data)) >>> print('dets.meta = {!r}'.format(dets.meta)) """ import kwimage anns = [self._id_to_obj[aid] for aid in self.aids] dets = kwimage.Detections.from_coco_annots(anns, dset=self._dset) # dets.data['aids'] = np.array(self.aids) return dets @property def boxes(self): """ Get the column of kwimage-style bounding boxes Returns: kwimage.Boxes Example: >>> import kwcoco >>> self = kwcoco.CocoDataset.demo().annots([1, 2, 11]) >>> print(self.boxes) <Boxes(xywh, array([[ 10, 10, 360, 490], [350, 5, 130, 290], [156, 130, 45, 18]]))> """ import kwimage xywh = self.lookup('bbox') boxes = kwimage.Boxes(xywh, 'xywh') return boxes @boxes.setter def boxes(self, boxes): """ Args: boxes (kwimage.Boxes): Example: >>> import kwimage >>> import kwcoco >>> self = kwcoco.CocoDataset.demo().annots([1, 2, 11]) >>> print('self.boxes = {!r}'.format(self.boxes)) >>> boxes = kwimage.Boxes.random(3).scale(512).astype(int) >>> self.boxes = boxes >>> print('self.boxes = {!r}'.format(self.boxes)) """ anns = ub.take(self._dset.anns, self.aids) xywh = boxes.to_xywh().data.tolist() for ann, xywh in zip(anns, xywh): ann['bbox'] = xywh @property def xywh(self): """ Returns raw boxes DEPRECATED. Returns: List[List[int]]: raw boxes in xywh format Example: >>> import kwcoco >>> self = kwcoco.CocoDataset.demo().annots([1, 2, 11]) >>> print(self.xywh) """ ub.schedule_deprecation( 'kwcoco', name='Annots.xywh', type='property', deprecate='0.4.0', error='1.0.0', remove='1.1.0', migration=( 'use `Annots.lookup("bbox")`.' ) ) xywh = self.lookup('bbox') return xywh
[docs] class Tracks(ObjectList1D): """ Vectorized access to track attributes SeeAlso: :func:`kwcoco.coco_dataset.MixinCocoObjects.tracks` Example: >>> import kwcoco >>> dset = kwcoco.CocoDataset.demo('vidshapes1', num_tracks=4) >>> tracks = dset.tracks() >>> print('tracks = {}'.format(tracks)) tracks = <Tracks(num=4)> >>> tracks.name ['track_001', 'track_002', 'track_003', 'track_004'] """ def __init__(self, ids, dset): """ Args: ids (List[int]): list of track ids dset (CocoDataset): parent dataset """ super().__init__(ids, dset, 'tracks') @property def track_ids(self): """ The annotation ids of this column of annotations """ return self._ids @property def name(self): return self.lookup('name') @property def annots(self): """ Example: >>> import kwcoco >>> dset = kwcoco.CocoDataset.demo('vidshapes1', num_tracks=4) >>> self = dset.tracks() >>> print(self.annots) <AnnotGroups(n=4, m=2.0, s=0.0)> """ aids_iter = ub.take(self._dset.index.trackid_to_aids, self._ids) return AnnotGroups([self._dset.annots(aids) for aids in aids_iter], self._dset)
[docs] class AnnotGroups(ObjectGroups): """ Annotation groups are vectorized lists of lists. Each item represents a set of annotations that corresopnds with something (i.e. belongs to a particular image). Example: >>> from kwcoco.coco_objects1d import ImageGroups >>> import kwcoco >>> dset = kwcoco.CocoDataset.demo('photos') >>> images = dset.images() >>> # Requesting the "annots" property from a Images object >>> # will return an AnnotGroups object >>> group: AnnotGroups = images.annots >>> # Printing the group gives info on the mean/std of the number >>> # of items per group. >>> print(group) <AnnotGroups(n=3, m=3.7, s=3.9)...> >>> # Groups are fairly restrictive, they dont provide property level >>> # access in many cases, but the lookup method is available >>> print(group.lookup('id')) [[1, 2, 3, 4, 5, 6, 7, 8, 9], [10, 11], []] >>> print(group.lookup('image_id')) [[1, 1, 1, 1, 1, 1, 1, 1, 1], [2, 2], []] >>> print(group.lookup('category_id')) [[1, 2, 3, 4, 5, 5, 5, 5, 5], [6, 4], []] """ @property def cids(self): """ Get the grouped category ids for annotations in this group Returns: List[List[int]]: Example: >>> import kwcoco >>> self = kwcoco.CocoDataset.demo('photos').images().annots >>> print('self.cids = {}'.format(ub.urepr(self.cids, nl=0))) self.cids = [[1, 2, 3, 4, 5, 5, 5, 5, 5], [6, 4], []] """ return self.lookup('category_id') @property def cnames(self): """ Get the grouped category names for annotations in this group Returns: List[List[str]]: Example: >>> import kwcoco >>> self = kwcoco.CocoDataset.demo('photos').images().annots >>> print('self.cnames = {}'.format(ub.urepr(self.cnames, nl=0))) self.cnames = [['astronaut', 'rocket', 'helmet', 'mouth', 'star', 'star', 'star', 'star', 'star'], ['astronomer', 'mouth'], []] """ return [getattr(group, 'cnames') for group in self._groups]
[docs] class ImageGroups(ObjectGroups): """ Image groups are vectorized lists of other Image objects. Each item represents a set of images that corresopnds with something (i.e. belongs to a particular video). Example: >>> from kwcoco.coco_objects1d import ImageGroups >>> import kwcoco >>> dset = kwcoco.CocoDataset.demo('vidshapes8') >>> videos = dset.videos() >>> # Requesting the "images" property from a Videos object >>> # will return an ImageGroups object >>> group: ImageGroups = videos.images >>> # Printing the group gives info on the mean/std of the number >>> # of items per group. >>> print(group) <ImageGroups(n=8, m=2.0, s=0.0)...> >>> # Groups are fairly restrictive, they dont provide property level >>> # access in many cases, but the lookup method is available >>> print(group.lookup('id')) [[1, 2], [3, 4], [5, 6], [7, 8], [9, 10], [11, 12], [13, 14], [15, 16]] >>> print(group.lookup('video_id')) [[1, 1], [2, 2], [3, 3], [4, 4], [5, 5], [6, 6], [7, 7], [8, 8]] >>> print(group.lookup('frame_index')) [[0, 1], [0, 1], [0, 1], [0, 1], [0, 1], [0, 1], [0, 1], [0, 1]] """ ...