Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

enhance(sdk): add more tool methods for BoundingBox type #3068

Merged
merged 1 commit into from
Dec 8, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
57 changes: 56 additions & 1 deletion client/starwhale/base/data_type.py
Original file line number Diff line number Diff line change
Expand Up @@ -564,8 +564,63 @@ def to_tensor(self) -> t.Any:

return convert_list_to_tensor(self.to_list())

@classmethod
def from_xywh(cls, x: float, y: float, width: float, height: float) -> BoundingBox:
"""Build BoundingBox from (x, y, width, height) format. (x, y) is the top-left corner, width is the width, and height is the height."""
return cls(x, y, width, height)

@classmethod
def from_xyxy(cls, x1: float, y1: float, x2: float, y2: float) -> BoundingBox:
"""Build BoundingBox from (x1, y1, x2, y2) format. (x1, y1) is the top-left corner, (x2, y2) is the bottom-right corner."""
return cls(x1, y1, x2 - x1, y2 - y1)

@classmethod
def from_ccwh(
cls, cx: float, cy: float, width: float, height: float
) -> BoundingBox:
"""Build BoundingBox from (cx, cy, width, height) format. (cx, cy) is the center of the box, width is the width, and height is the height."""
return cls(cx - width / 2, cy - height / 2, width, height)

@classmethod
def from_darknet(
cls,
ncx: float,
ncy: float,
nw: float,
nh: float,
image_width: float,
image_height: float,
) -> BoundingBox:
"""Build BoundingBox from darknet format: (ncx, ncy, nw, nh).
(ncx, ncy) is the normalized center of the box, nw is the normalized width, and nh is the normalized height.
image_width and image_height are the width and height of the image.
"""
return cls.from_ccwh(
ncx * image_width, ncy * image_height, nw * image_width, nh * image_height
)

def to_xyxy(self) -> t.List[float]:
"""Return (x1, y1, x2, y2) format. (x1, y1) is the top-left corner, (x2, y2) is the bottom-right corner."""
return [self.x, self.y, self.x + self.width, self.y + self.height]

def to_ccwh(self) -> t.List[float]:
"""Return (cx, cy, w, h) format. (cx, cy) is the center of the box, w is the width, and h is the height."""
return [
self.x + self.width / 2,
self.y + self.height / 2,
self.width,
self.height,
]

def to_darknet(self, image_width: float, image_height: float) -> t.List[float]:
"""Return darknet format: (ncx, ncy, nw, nh), for YOLO.
(ncx, ncy) is the normalized center of the box, nw is the normalized width, and nh is the normalized height.
"""
cx, cy, w, h = self.to_ccwh()
return [cx / image_width, cy / image_height, w / image_width, h / image_height]

def __str__(self) -> str:
return f"BoundingBox: point:({self.x}, {self.y}), width: {self.width}, height: {self.height})"
return f"BoundingBox[XYWH]- x:{self.x}, y:{self.y}, width:{self.width}, height:{self.height}"

__repr__ = __str__

Expand Down
9 changes: 9 additions & 0 deletions client/tests/base/test_data_type.py
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,15 @@ def test_bbox(self) -> None:
_array = numpy.frombuffer(_bout, dtype=numpy.float64)
assert numpy.array_equal(_array, numpy.array([1, 2, 3, 4], dtype=numpy.float64))

assert bbox.to_xyxy() == [1, 2, 4, 6]
assert BoundingBox.from_xyxy(*bbox.to_xyxy()) == bbox
assert bbox.to_ccwh() == [2.5, 4.0, 3, 4]
assert BoundingBox.from_ccwh(*bbox.to_ccwh()) == bbox
assert BoundingBox.from_xywh(*bbox.to_list()) == bbox
darknet = bbox.to_darknet(100, 100)
assert darknet == [0.025, 0.04, 0.03, 0.04]
assert BoundingBox.from_darknet(*(darknet + [100, 100])) == bbox

def test_bbox3d(self) -> None:
bbox_a = BoundingBox(1, 2, 3, 4)
bbox_b = BoundingBox(3, 4, 3, 4)
Expand Down
7 changes: 1 addition & 6 deletions example/object-detection/datasets/coco128.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,12 +55,7 @@ def build() -> None:
"class_id": class_id,
"class_name": get_name_by_coco_category_id(class_id),
"darknet_bbox": [x, y, w, h],
"bbox": BoundingBox(
x=(x - w / 2) * i_width,
y=(y - h / 2) * i_height,
width=w * i_width,
height=h * i_height,
),
"bbox": BoundingBox.from_darknet(x, y, w, h, i_width, i_height),
}
)

Expand Down
10 changes: 3 additions & 7 deletions example/object-detection/datasets/coco_val2017.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,13 +55,9 @@ def build() -> None:
for image in tqdm(content["images"]):
name = image["file_name"].split(".jpg")[0]
for ann in annotations[image["id"]]:
bbox = ann["bbox"]
ann["darknet_bbox"] = [
(bbox.x + bbox.width / 2) / image["width"],
(bbox.y + bbox.height / 2) / image["height"],
bbox.width / image["width"],
bbox.height / image["height"],
]
ann["darknet_bbox"] = ann["bbox"].to_darknet(
image["width"], image["height"]
)
ds[name] = {
"image": Image(DATA_DIR / "val2017" / image["file_name"]),
"annotations": annotations[image["id"]],
Expand Down
16 changes: 2 additions & 14 deletions example/object-detection/models/yolo/evaluation.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,10 +79,9 @@ def predict_image(data: t.Dict, external: t.Dict) -> t.Dict:
}

if label_bboxes_cnt:
label_xywh_bboxes = torch.as_tensor(
[ann["bbox"].to_list() for ann in data["annotations"]], device=device
label_xyxy_bboxes = torch.as_tensor(
[ann["bbox"].to_xyxy() for ann in data["annotations"]], device=device
)
label_xyxy_bboxes = _bbox2xyxy(label_xywh_bboxes)

correct_bboxes = detection_validator._process_batch(
detections=result.boxes.data,
Expand Down Expand Up @@ -135,14 +134,3 @@ def summary_detection(predict_result_iter: t.Iterator) -> None:
def web_detect_image(file: str) -> Path | str:
result = _load_model().predict(file, save=True)[0]
return Path(result.save_dir) / Path(result.path).name


def _bbox2xyxy(x: torch.Tensor) -> torch.Tensor:
y = torch.empty_like(x)
w = x[..., 2]
h = x[..., 3]
y[..., 0] = x[..., 0]
y[..., 1] = x[..., 1]
y[..., 2] = x[..., 0] + w
y[..., 3] = x[..., 1] + h
return y
Loading