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

Fix tile bug & add more tests #3012

Merged
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
25 changes: 13 additions & 12 deletions src/otx/core/data/dataset/tile.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from __future__ import annotations

import logging as log
from itertools import product
from typing import TYPE_CHECKING, Callable

import numpy as np
Expand Down Expand Up @@ -92,22 +93,22 @@ def _extract_rois(self, image: Image) -> list[BboxIntCoords]:
img_h, img_w = image.size
tile_h, tile_w = self._tile_size
h_ovl, w_ovl = self._overlap
stride_h, stride_w = max(int(tile_h * (1 - h_ovl)), 1), max(int(tile_w * (1 - w_ovl)), 1)
n_row, n_col = (img_h + stride_h - 1) // stride_h, (img_w + stride_w - 1) // stride_w

rois: list[BboxIntCoords] = []
cols = range(0, img_w, int(tile_w * (1 - w_ovl)))
rows = range(0, img_h, int(tile_h * (1 - h_ovl)))

for offset_x, offset_y in product(cols, rows):
x2 = min(offset_x + tile_w, img_w)
y2 = min(offset_y + tile_h, img_h)
c_x, c_y, w, h = x1y1x2y2_to_cxcywh(offset_x, offset_y, x2, y2)
x1, y1, x2, y2 = cxcywh_to_x1y1x2y2(c_x, c_y, w, h)
x1, y1, x2, y2 = clip_x1y1x2y2(x1, y1, x2, y2, img_w, img_h)
x1, y1, x2, y2 = (int(v) for v in [x1, y1, x2, y2])
rois += [x1y1x2y2_to_xywh(x1, y1, x2, y2)]

for r in range(n_row):
for c in range(n_col):
y1, x1 = stride_h * r, stride_w * c
y2, x2 = y1 + stride_h, x1 + stride_w

c_x, c_y, w, h = x1y1x2y2_to_cxcywh(x1, y1, x2, y2)
x1, y1, x2, y2 = cxcywh_to_x1y1x2y2(c_x, c_y, w, h)
x1, y1, x2, y2 = clip_x1y1x2y2(x1, y1, x2, y2, img_w, img_h)
rois += [x1y1x2y2_to_xywh(x1, y1, x2, y2)]
log.info(f"image: {img_h}x{img_w} ~ tile_size: {self._tile_size}")
log.info(f"{n_row}x{n_col} tiles -> {len(rois)} tiles")
log.info(f"{len(rows)}x{len(cols)} tiles -> {len(rois)} tiles")
return rois


Expand Down
2 changes: 1 addition & 1 deletion src/otx/engine/engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -644,7 +644,7 @@ def from_config(

return cls(
work_dir=instantiated_config.get("work_dir", work_dir),
datamodule=instantiated_config.get("datamodule"),
datamodule=instantiated_config.get("data"),
model=instantiated_config.get("model"),
optimizer=instantiated_config.get("optimizer"),
scheduler=instantiated_config.get("scheduler"),
Expand Down
32 changes: 32 additions & 0 deletions tests/integration/api/test_engine_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from pathlib import Path

import pytest
from openvino.model_api.tilers import Tiler
from otx.core.data.module import OTXDataModule
from otx.core.model.entity.base import OTXModel
from otx.core.types.task import OTXTaskType
Expand Down Expand Up @@ -71,3 +72,34 @@ def test_engine_from_config(
if task in OVMODEL_PER_TASK:
test_metric_from_ov_model = engine.test(checkpoint=exported_model_path, accelerator="cpu")
assert len(test_metric_from_ov_model) > 0


@pytest.mark.parametrize("recipe", pytest.TILE_RECIPE_LIST)
def test_engine_from_tile_recipe(
recipe: str,
tmp_path: Path,
fxt_accelerator: str,
fxt_target_dataset_per_task: dict,
):
task = OTXTaskType.DETECTION if "detection" in recipe else OTXTaskType.INSTANCE_SEGMENTATION

engine = Engine.from_config(
config_path=recipe,
data_root=fxt_target_dataset_per_task[task.value.lower()],
work_dir=tmp_path / task,
device=fxt_accelerator,
)
engine.train(max_epochs=1)
exported_model_path = engine.export()
assert exported_model_path.exists()
metric = engine.test(exported_model_path, accelerator="cpu")
assert len(metric) > 0

# Check OVModel & OVTiler is set correctly
ov_model = engine._auto_configurator.get_ov_model(
model_name=exported_model_path,
label_info=engine.datamodule.label_info,
)
assert isinstance(ov_model.model, Tiler), "Model should be an instance of Tiler"
assert engine.datamodule.config.tile_config.tile_size[0] == ov_model.model.tile_size
assert engine.datamodule.config.tile_config.overlap == ov_model.model.tiles_overlap
2 changes: 2 additions & 0 deletions tests/integration/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,10 +97,12 @@ def pytest_configure(config):

target_recipe_list.extend(recipe_list)
target_ov_recipe_list.extend(recipe_ov_list)
tile_recipe_list = [recipe for recipe in target_recipe_list if "tile" in recipe]

pytest.TASK_LIST = task_list
pytest.RECIPE_LIST = target_recipe_list
pytest.RECIPE_OV_LIST = target_ov_recipe_list
pytest.TILE_RECIPE_LIST = tile_recipe_list


@pytest.fixture(scope="session")
Expand Down
41 changes: 41 additions & 0 deletions tests/unit/core/utils/test_tile.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Copyright (C) 2024 Intel Corporation
# SPDX-License-Identifier: Apache-2.0
#

from __future__ import annotations

from unittest.mock import MagicMock

import numpy as np
from datumaro import Image
from datumaro.plugins.tiling.util import xywh_to_x1y1x2y2
from openvino.model_api.models import Model
from openvino.model_api.tilers import Tiler
from otx.core.data.dataset.tile import OTXTileTransform


def test_tile_transform_consistency(mocker):
# Test that the tiler and tile transform are consistent
rng = np.random.default_rng()
rnd_tile_size = rng.integers(low=100, high=500)
rnd_tile_overlap = rng.random()
image_size = rng.integers(low=1000, high=5000)
np_image = np.zeros((image_size, image_size, 3), dtype=np.uint8)
dm_image = Image.from_numpy(np_image)

mock_model = MagicMock(spec=Model)
mocker.patch("openvino.model_api.tilers.tiler.Tiler.__init__", return_value=None)
mocker.patch.multiple(Tiler, __abstractmethods__=set())

tiler = Tiler(model=mock_model)
tiler.tile_size = rnd_tile_size
tiler.tiles_overlap = rnd_tile_overlap

mocker.patch("otx.core.data.dataset.tile.OTXTileTransform.__init__", return_value=None)
tile_transform = OTXTileTransform()
tile_transform._tile_size = (rnd_tile_size, rnd_tile_size)
tile_transform._overlap = (rnd_tile_overlap, rnd_tile_overlap)

dm_rois = [xywh_to_x1y1x2y2(*roi) for roi in tile_transform._extract_rois(dm_image)]
# 0 index in tiler is the full image so we skip it
assert np.allclose(dm_rois, tiler._tile(np_image)[1:])
Loading