Skip to content
Open
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
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
2 changes: 1 addition & 1 deletion .github/workflows/docs_latest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ jobs:
sudo apt-get install pandoc graphviz
- name: Build-Docs
run: |
uv pip install -r docs/requirements.txt
uv pip install -r docs/requirements.txt --system
cd docs
make clean
make html
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/docs_stable.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ jobs:
sudo apt-get install pandoc graphviz
- name: Build-Docs
run: |
uv pip install -r docs/requirements.txt
uv pip install -r docs/requirements.txt --system
cd docs
make clean
make html
Expand Down
2 changes: 1 addition & 1 deletion src/datumaro/experimental/converter_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ class ConversionPaths(NamedTuple):
"""
Container for separated batch and lazy conversion paths.

The batch converters can be applied immediately to the entire DataFrame,
The converters can be applied immediately to the entire DataFrame,
while lazy converters must be deferred and applied at sample access time.
"""

Expand Down
297 changes: 295 additions & 2 deletions src/datumaro/experimental/converters.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
from .converter_registry import AttributeSpec, Converter, converter
from .fields import (
BBoxField,
BBoxFormat,
ImageBytesField,
ImageCallableField,
ImageField,
Expand All @@ -33,7 +34,9 @@
MaskCallableField,
MaskField,
PolygonField,
PolygonFormat,
RotatedBBoxField,
RotatedBBoxFormat,
)
from .type_registry import polars_to_numpy_dtype

Expand Down Expand Up @@ -1011,10 +1014,10 @@ def convert(self, df: pl.DataFrame) -> pl.DataFrame:
)

# Format according to output bbox format
if self.output_bbox.field.format == "x1y1x2y2":
if self.output_bbox.field.format == BBoxFormat.X1Y1X2Y2:
# Already in this format
pass
elif self.output_bbox.field.format == "xywh":
elif self.output_bbox.field.format == BBoxFormat.XYWH:
df = df.with_columns(
pl.col(output_column_name).list.eval(
pl.concat_arr(
Expand Down Expand Up @@ -1361,3 +1364,293 @@ def rotate_corner(expr: pl.Expr):
)

return df


@converter
class BBoxFormatConverter(Converter):
"""
Converter for switching between different bounding box coordinate formats.

Supports conversion between:
- X1Y1X2Y2: (x1, y1, x2, y2) - top-left and bottom-right corners
- XYWH: (x, y, w, h) - top-left corner and dimensions
"""

input_bbox: AttributeSpec[BBoxField]
output_bbox: AttributeSpec[BBoxField]

def filter_output_spec(self) -> bool:
"""
Check if this converter should be applied for bbox format conversion.

Returns True if input and output have different bbox formats.
"""
input_format = self.input_bbox.field.format
output_format = self.output_bbox.field.format

# Only apply if formats are different
if input_format == output_format:
return False

# Configure output specification
self.output_bbox = AttributeSpec(
name=self.output_bbox.name,
field=BBoxField(
semantic=self.input_bbox.field.semantic,
dtype=self.input_bbox.field.dtype,
format=output_format,
normalize=self.input_bbox.field.normalize,
),
)

# Check if conversion is supported
supported_conversions = {
(BBoxFormat.X1Y1X2Y2, BBoxFormat.XYWH),
(BBoxFormat.XYWH, BBoxFormat.X1Y1X2Y2),
}

return (input_format, output_format) in supported_conversions

def convert(self, df: pl.DataFrame) -> pl.DataFrame:
"""
Convert between bbox coordinate formats.

Args:
df: DataFrame with bbox coordinates in input format

Returns:
DataFrame with bbox coordinates in output format
"""
input_col = self.input_bbox.name
output_col = self.output_bbox.name
input_format = self.input_bbox.field.format
output_format = self.output_bbox.field.format

if input_format == BBoxFormat.X1Y1X2Y2 and output_format == BBoxFormat.XYWH:
# Convert (x1, y1, x2, y2) to (x, y, w, h)
df = df.with_columns(
pl.col(input_col)
.list.eval(
pl.concat_arr(
[
pl.element().arr.get(0), # x = x1
pl.element().arr.get(1), # y = y1
pl.element().arr.get(2) - pl.element().arr.get(0), # w = x2 - x1
pl.element().arr.get(3) - pl.element().arr.get(1), # h = y2 - y1
]
)
)
.alias(output_col)
)
elif input_format == BBoxFormat.XYWH and output_format == BBoxFormat.X1Y1X2Y2:
# Convert (x, y, w, h) to (x1, y1, x2, y2)
df = df.with_columns(
pl.col(input_col)
.list.eval(
pl.concat_arr(
[
pl.element().arr.get(0), # x1 = x
pl.element().arr.get(1), # y1 = y
pl.element().arr.get(0) + pl.element().arr.get(2), # x2 = x + w
pl.element().arr.get(1) + pl.element().arr.get(3), # y2 = y + h
]
)
)
.alias(output_col)
)
else:
raise NotImplementedError(
f"Conversion from {input_format} to {output_format} is not yet implemented"
)

return df


@converter
class RotatedBBoxFormatConverter(Converter):
"""
Converter for switching between different rotated bounding box formats.

Supports conversion between:
- CXCYWHR: (cx, cy, w, h, r) - rotation in radians
- CXCYWHA: (cx, cy, w, h, a) - rotation in degrees
"""

input_rotated_bbox: AttributeSpec[RotatedBBoxField]
output_rotated_bbox: AttributeSpec[RotatedBBoxField]

def filter_output_spec(self) -> bool:
"""
Check if this converter should be applied for rotated bbox format conversion.

Returns True if input and output have different rotated bbox formats.
"""
input_format = self.input_rotated_bbox.field.format
output_format = self.output_rotated_bbox.field.format

# Only apply if formats are different
if input_format == output_format:
return False

# Configure output specification
self.output_rotated_bbox = AttributeSpec(
name=self.output_rotated_bbox.name,
field=RotatedBBoxField(
semantic=self.input_rotated_bbox.field.semantic,
dtype=self.input_rotated_bbox.field.dtype,
format=output_format,
normalize=self.input_rotated_bbox.field.normalize,
),
)

# Check if conversion is supported
supported_conversions = {
(RotatedBBoxFormat.CXCYWHR, RotatedBBoxFormat.CXCYWHA),
(RotatedBBoxFormat.CXCYWHA, RotatedBBoxFormat.CXCYWHR),
}

return (input_format, output_format) in supported_conversions

def convert(self, df: pl.DataFrame) -> pl.DataFrame:
"""
Convert between rotated bbox formats (radians ↔ degrees).

Args:
df: DataFrame with rotated bbox coordinates in input format

Returns:
DataFrame with rotated bbox coordinates in output format
"""
input_col = self.input_rotated_bbox.name
output_col = self.output_rotated_bbox.name
input_format = self.input_rotated_bbox.field.format
output_format = self.output_rotated_bbox.field.format

if input_format == RotatedBBoxFormat.CXCYWHR and output_format == RotatedBBoxFormat.CXCYWHA:
# Convert radians to degrees: multiply rotation by 180/π
df = df.with_columns(
pl.col(input_col)
.list.eval(
pl.concat_arr(
[
pl.element().arr.get(0), # cx unchanged
pl.element().arr.get(1), # cy unchanged
pl.element().arr.get(2), # w unchanged
pl.element().arr.get(3), # h unchanged
pl.element().arr.get(4) * 180.0 / np.pi, # r (radians) -> a (degrees)
]
)
)
.alias(output_col)
)
elif (
input_format == RotatedBBoxFormat.CXCYWHA and output_format == RotatedBBoxFormat.CXCYWHR
):
# Convert degrees to radians: multiply rotation by π/180
df = df.with_columns(
pl.col(input_col)
.list.eval(
pl.concat_arr(
[
pl.element().arr.get(0), # cx unchanged
pl.element().arr.get(1), # cy unchanged
pl.element().arr.get(2), # w unchanged
pl.element().arr.get(3), # h unchanged
pl.element().arr.get(4) * np.pi / 180.0, # a (degrees) -> r (radians)
]
)
)
.alias(output_col)
)
else:
raise NotImplementedError(
f"Conversion from {input_format} to {output_format} is not yet implemented"
)

return df


@converter
class PolygonFormatConverter(Converter):
"""
Converter for switching between different polygon coordinate formats.

Supports conversion between:
- XY: (x, y) coordinate pairs
- YX: (y, x) coordinate pairs (swaps x and y coordinates)
"""

input_polygon: AttributeSpec[PolygonField]
output_polygon: AttributeSpec[PolygonField]

def filter_output_spec(self) -> bool:
"""
Check if this converter should be applied for polygon format conversion.

Returns True if input and output have different polygon formats.
"""
input_format = self.input_polygon.field.format
output_format = self.output_polygon.field.format

# Only apply if formats are different
if input_format == output_format:
return False

# Configure output specification
self.output_polygon = AttributeSpec(
name=self.output_polygon.name,
field=PolygonField(
semantic=self.input_polygon.field.semantic,
dtype=self.input_polygon.field.dtype,
format=output_format,
normalize=self.input_polygon.field.normalize,
),
)

# Check if conversion is supported
supported_conversions = {
(PolygonFormat.XY, PolygonFormat.YX),
(PolygonFormat.YX, PolygonFormat.XY),
}

return (input_format, output_format) in supported_conversions

def convert(self, df: pl.DataFrame) -> pl.DataFrame:
"""
Convert between polygon coordinate formats by swapping x and y coordinates.

Args:
df: DataFrame with polygon coordinates in input format

Returns:
DataFrame with polygon coordinates in output format
"""
input_col = self.input_polygon.name
output_col = self.output_polygon.name
input_format = self.input_polygon.field.format
output_format = self.output_polygon.field.format

if (input_format == PolygonFormat.XY and output_format == PolygonFormat.YX) or (
input_format == PolygonFormat.YX and output_format == PolygonFormat.XY
):
# Swap x and y coordinates: [x, y] ↔ [y, x]
df = df.with_columns(
pl.col(input_col)
.list.eval(
pl.element().list.eval(
pl.concat_arr(
[
pl.element().arr.get(1), # y becomes first
pl.element().arr.get(0), # x becomes second
]
)
)
)
.alias(output_col)
)
else:
raise NotImplementedError(
f"Conversion from {input_format} to {output_format} is not yet implemented"
)

return df
Loading
Loading