Skip to content

Commit

Permalink
Revert "Optimize dataset first/head and last/tail (#5407)" (#5429)
Browse files Browse the repository at this point in the history
This reverts commit d306a36.
  • Loading branch information
kaixi-wang authored Jan 24, 2025
1 parent 2b843b9 commit 448177d
Show file tree
Hide file tree
Showing 5 changed files with 75 additions and 239 deletions.
2 changes: 1 addition & 1 deletion fiftyone/__public__.py
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@
Run,
RunResults,
)
from .core.sample import Sample, SampleView
from .core.sample import Sample
from .core.threed import (
BoxGeometry,
CylinderGeometry,
Expand Down
68 changes: 57 additions & 11 deletions fiftyone/core/collections.py
Original file line number Diff line number Diff line change
Expand Up @@ -923,7 +923,10 @@ def first(self):
a :class:`fiftyone.core.sample.Sample` or
:class:`fiftyone.core.sample.SampleView`
"""
raise NotImplementedError("Subclass must implement first()")
try:
return next(iter(self))
except StopIteration:
raise ValueError("%s is empty" % self.__class__.__name__)

def last(self):
"""Returns the last sample in the collection.
Expand All @@ -932,7 +935,7 @@ def last(self):
a :class:`fiftyone.core.sample.Sample` or
:class:`fiftyone.core.sample.SampleView`
"""
raise NotImplementedError("Subclass must implement last()")
return self[-1:].first()

def head(self, num_samples=3):
"""Returns a list of the first few samples in the collection.
Expand All @@ -944,10 +947,9 @@ def head(self, num_samples=3):
num_samples (3): the number of samples
Returns:
a list of :class:`fiftyone.core.sample.Sample` or
:class:`fiftyone.core.sample.SampleView` objects
a list of :class:`fiftyone.core.sample.Sample` objects
"""
raise NotImplementedError("Subclass must implement head()")
return [s for s in self[:num_samples]]

def tail(self, num_samples=3):
"""Returns a list of the last few samples in the collection.
Expand All @@ -959,14 +961,41 @@ def tail(self, num_samples=3):
num_samples (3): the number of samples
Returns:
a list of :class:`fiftyone.core.sample.Sample` or
:class:`fiftyone.core.sample.SampleView` objects
a list of :class:`fiftyone.core.sample.Sample` objects
"""
raise NotImplementedError("Subclass must implement tail()")
return [s for s in self[-num_samples:]]

def one(self, expr, exact=False):
"""Returns a single sample in this collection matching the expression.
Examples::
import fiftyone as fo
import fiftyone.zoo as foz
from fiftyone import ViewField as F
dataset = foz.load_zoo_dataset("quickstart")
#
# Get a sample by filepath
#
# A random filepath in the dataset
filepath = dataset.take(1).first().filepath
# Get sample by filepath
sample = dataset.one(F("filepath") == filepath)
#
# Dealing with multiple matches
#
# Get a sample whose image is JPEG
sample = dataset.one(F("filepath").ends_with(".jpg"))
# Raises an error since there are multiple JPEGs
dataset.one(F("filepath").ends_with(".jpg"), exact=True)
Args:
expr: a :class:`fiftyone.core.expressions.ViewExpression` or
`MongoDB expression <https://docs.mongodb.com/manual/meta/aggregation-quick-reference/#aggregation-expressions>`_
Expand All @@ -979,10 +1008,27 @@ def one(self, expr, exact=False):
and multiple samples match the expression
Returns:
a :class:`fiftyone.core.sample.Sample` or
:class:`fiftyone.core.sample.SampleView`
a :class:`fiftyone.core.sample.SampleView`
"""
raise NotImplementedError("Subclass must implement one()")
view = self.match(expr)
matches = iter(view)

try:
sample = next(matches)
except StopIteration:
raise ValueError("No samples match the given expression")

if exact:
try:
next(matches)
raise ValueError(
"Expected one matching sample, but found %d matches"
% len(view)
)
except StopIteration:
pass

return sample

def view(self):
"""Returns a :class:`fiftyone.core.view.DatasetView` containing the
Expand Down
53 changes: 17 additions & 36 deletions fiftyone/core/dataset.py
Original file line number Diff line number Diff line change
Expand Up @@ -1219,43 +1219,26 @@ def _frame_collstats(self):

return _get_collstats(self._frame_collection)

def _get_first(self, limit=1, reverse=False):
direction = -1 if reverse else 1
pipeline = [{"$sort": {"_id": direction}}, {"$limit": limit}]

return self._aggregate(
pipeline=pipeline, detach_frames=True, detach_groups=True
)

def first(self):
"""Returns the first sample in the dataset.
Returns:
a :class:`fiftyone.core.sample.Sample`
"""
cursor = self._get_first()

try:
d = next(cursor)
except StopIteration:
raise ValueError("%s is empty" % self.__class__.__name__)

return self._make_sample(d)
return super().first()

def last(self):
"""Returns the last sample in the dataset.
Returns:
a :class:`fiftyone.core.sample.Sample`
"""
cursor = self._get_first(reverse=True)

try:
d = next(cursor)
except StopIteration:
sample_view = self[-1:].first()
except ValueError:
raise ValueError("%s is empty" % self.__class__.__name__)

return self._make_sample(d)
return fos.Sample.from_doc(sample_view._doc, dataset=self)

def head(self, num_samples=3):
"""Returns a list of the first few samples in the dataset.
Expand All @@ -1269,8 +1252,10 @@ def head(self, num_samples=3):
Returns:
a list of :class:`fiftyone.core.sample.Sample` objects
"""
cursor = self._get_first(limit=num_samples)
return [self._make_sample(d) for d in cursor]
return [
fos.Sample.from_doc(sv._doc, dataset=self)
for sv in self[:num_samples]
]

def tail(self, num_samples=3):
"""Returns a list of the last few samples in the dataset.
Expand All @@ -1284,10 +1269,10 @@ def tail(self, num_samples=3):
Returns:
a list of :class:`fiftyone.core.sample.Sample` objects
"""
cursor = self._get_first(reverse=True, limit=num_samples)
samples = [self._make_sample(d) for d in cursor]
samples.reverse()
return samples
return [
fos.Sample.from_doc(sv._doc, dataset=self)
for sv in self[-num_samples:]
]

def one(self, expr, exact=False):
"""Returns a single sample in this dataset matching the expression.
Expand Down Expand Up @@ -1318,7 +1303,7 @@ def one(self, expr, exact=False):
sample = dataset.one(F("filepath").ends_with(".jpg"))
# Raises an error since there are multiple JPEGs
_ = dataset.one(F("filepath").ends_with(".jpg"), exact=True)
dataset.one(F("filepath").ends_with(".jpg"), exact=True)
Args:
expr: a :class:`fiftyone.core.expressions.ViewExpression` or
Expand All @@ -1327,16 +1312,11 @@ def one(self, expr, exact=False):
exact (False): whether to raise an error if multiple samples match
the expression
Raises:
ValueError: if no samples match the expression or if ``exact=True``
and multiple samples match the expression
Returns:
a :class:`fiftyone.core.sample.Sample`
"""
limit = 2 if exact else 1
view = self.match(expr).limit(limit)
matches = iter(view._aggregate(detach_frames=True, detach_groups=True))
view = self.match(expr)
matches = iter(view._aggregate())

try:
d = next(matches)
Expand All @@ -1347,7 +1327,8 @@ def one(self, expr, exact=False):
try:
next(matches)
raise ValueError(
"Expected one matching sample, but found multiple"
"Expected one matching sample, but found %d matches"
% len(view)
)
except StopIteration:
pass
Expand Down
113 changes: 0 additions & 113 deletions fiftyone/core/view.py
Original file line number Diff line number Diff line change
Expand Up @@ -439,119 +439,6 @@ def _make_view_stages_str(self):
]
)

def first(self):
"""Returns the first sample in the view.
Returns:
a :class:`fiftyone.core.sample.SampleView`
"""
try:
return next(iter(self))
except StopIteration:
raise ValueError("%s is empty" % self.__class__.__name__)

def last(self):
"""Returns the last sample in the view.
Returns:
a :class:`fiftyone.core.sample.SampleView`
"""
return self[-1:].first()

def head(self, num_samples=3):
"""Returns a list of the first few samples in the view.
If fewer than ``num_samples`` samples are in the view, only
the available samples are returned.
Args:
num_samples (3): the number of samples
Returns:
a list of :class:`fiftyone.core.sample.SampleView` objects
"""
return [s for s in self[:num_samples]]

def tail(self, num_samples=3):
"""Returns a list of the last few samples in the view.
If fewer than ``num_samples`` samples are in the view, only
the available samples are returned.
Args:
num_samples (3): the number of samples
Returns:
a list of :class:`fiftyone.core.sample.SampleView` objects
"""
return [s for s in self[-num_samples:]]

def one(self, expr, exact=False):
"""Returns a single sample in this view matching the expression.
Examples::
import fiftyone as fo
import fiftyone.zoo as foz
from fiftyone import ViewField as F
dataset = foz.load_zoo_dataset("quickstart")
view = dataset.select_fields()
#
# Get a sample by filepath
#
# A random filepath in the view
filepath = view.take(1).first().filepath
# Get sample by filepath
sample = view.one(F("filepath") == filepath)
#
# Dealing with multiple matches
#
# Get a sample whose image is JPEG
sample = view.one(F("filepath").ends_with(".jpg"))
# Raises an error since there are multiple JPEGs
_ = view.one(F("filepath").ends_with(".jpg"), exact=True)
Args:
expr: a :class:`fiftyone.core.expressions.ViewExpression` or
`MongoDB expression <https://docs.mongodb.com/manual/meta/aggregation-quick-reference/#aggregation-expressions>`_
that evaluates to ``True`` for the sample to match
exact (False): whether to raise an error if multiple samples match
the expression
Raises:
ValueError: if no samples match the expression or if ``exact=True``
and multiple samples match the expression
Returns:
a :class:`fiftyone.core.sample.SampleView`
"""
limit = 2 if exact else 1
view = self.match(expr).limit(limit)
matches = iter(view._aggregate(detach_frames=True, detach_groups=True))

try:
d = next(matches)
except StopIteration:
raise ValueError("No samples match the given expression")

if exact:
try:
next(matches)
raise ValueError(
"Expected one matching sample, but found multiple"
)
except StopIteration:
pass

return self._make_sample(d)

def view(self):
"""Returns a copy of this view.
Expand Down
Loading

0 comments on commit 448177d

Please sign in to comment.