layout-overlap / layout-overlap.py
shunk031's picture
deploy: fb8481effdf5a0b23ff86fad414906046d7620bd
241b6f5
Raw
History Blame Contribute Delete
7.24 kB
from typing import Dict, List, Tuple, TypedDict, Union
import datasets as ds
import evaluate
import numpy as np
import numpy.typing as npt
from evaluate.utils.file_utils import add_start_docstrings
_DESCRIPTION = """\
Some overlap metrics that are different to each other in previous works.
"""
_KWARGS_DESCRIPTION = """\
Args:
bbox (`list` of `list` of `float`): A list of lists of floats representing bounding boxes in `xywh` format (center_x, center_y, width, height), all normalized to [0, 1].
mask (`list` of `list` of `bool`): A list of lists of boolean values indicating which bounding boxes are valid (True) or padding (False).
Returns:
dict: A dictionary containing three overlap metrics from different papers:
- `overlap-ACLayoutGAN`: Overlap metric from AC-LayoutGAN (Li et al., 2020)
- `overlap-LayoutGAN++`: Normalized overlap metric from LayoutGAN++
- `overlap-LayoutGAN`: Overlap metric from LayoutGAN (Li et al., 2019)
Each metric returns an array of scores per batch. Lower values indicate less overlap between elements.
Examples:
>>> import evaluate
>>> metric = evaluate.load("creative-graphic-design/layout-overlap")
>>> # Bounding boxes in xywh format (center_x, center_y, width, height)
>>> bbox = [[[0.25, 0.25, 0.3, 0.3], [0.75, 0.75, 0.3, 0.3]]] # Two non-overlapping boxes
>>> mask = [[True, True]] # Both boxes are valid
>>> result = metric.compute(bbox=bbox, mask=mask)
>>> print(f"AC-LayoutGAN overlap: {result['overlap-ACLayoutGAN']}")
>>> print(f"LayoutGAN++ overlap: {result['overlap-LayoutGAN++']}")
>>> print(f"LayoutGAN overlap: {result['overlap-LayoutGAN']}")
"""
_CITATION = """\
@inproceedings{li2018layoutgan,
title={LayoutGAN: Generating Graphic Layouts with Wireframe Discriminators},
author={Li, Jianan and Yang, Jimei and Hertzmann, Aaron and Zhang, Jianming and Xu, Tingfa},
booktitle={International Conference on Learning Representations},
year={2019}
}
@article{li2020attribute,
title={Attribute-conditioned layout gan for automatic graphic design},
author={Li, Jianan and Yang, Jimei and Zhang, Jianming and Liu, Chang and Wang, Christina and Xu, Tingfa},
journal={IEEE Transactions on Visualization and Computer Graphics},
volume={27},
number={10},
pages={4039--4048},
year={2020},
publisher={IEEE}
}
@inproceedings{kikuchi2021constrained,
title={Constrained graphic layout generation via latent optimization},
author={Kikuchi, Kotaro and Simo-Serra, Edgar and Otani, Mayu and Yamaguchi, Kota},
booktitle={Proceedings of the 29th ACM International Conference on Multimedia},
pages={88--96},
year={2021}
}
"""
def convert_xywh_to_ltrb(
batch_bbox: npt.NDArray[np.float64],
) -> Tuple[
npt.NDArray[np.float64],
npt.NDArray[np.float64],
npt.NDArray[np.float64],
npt.NDArray[np.float64],
]:
xc, yc, w, h = batch_bbox
x1 = xc - w / 2
y1 = yc - h / 2
x2 = xc + w / 2
y2 = yc + h / 2
return (x1, y1, x2, y2)
class A(TypedDict):
a1: npt.NDArray[np.float64]
ai: npt.NDArray[np.float64]
@add_start_docstrings(_DESCRIPTION, _KWARGS_DESCRIPTION)
class LayoutOverlap(evaluate.Metric):
def _info(self) -> evaluate.EvaluationModuleInfo:
return evaluate.MetricInfo(
description=_DESCRIPTION,
citation=_CITATION,
inputs_description=_KWARGS_DESCRIPTION,
features=ds.Features(
{
"bbox": ds.Sequence(ds.Sequence(ds.Value("float64"))),
"mask": ds.Sequence(ds.Value("bool")),
}
),
codebase_urls=[
"https://github.com/ktrk115/const_layout/blob/master/metric.py#L138-L164",
"https://github.com/CyberAgentAILab/layout-dm/blob/main/src/trainer/trainer/helpers/metric.py#L150-L203",
],
)
def __calculate_a1_ai(self, batch_bbox: npt.NDArray[np.float64]) -> A:
l1, t1, r1, b1 = convert_xywh_to_ltrb(batch_bbox[:, :, :, None])
l2, t2, r2, b2 = convert_xywh_to_ltrb(batch_bbox[:, :, None, :])
a1 = (r1 - l1) * (b1 - t1)
# shape: (B, S, S)
l_max = np.maximum(l1, l2)
r_min = np.minimum(r1, r2)
t_max = np.maximum(t1, t2)
b_min = np.minimum(b1, b2)
cond = (l_max < r_min) & (t_max < b_min)
ai = np.where(cond, (r_min - l_max) * (b_min - t_max), 0.0)
return {"a1": a1, "ai": ai}
def _compute_ac_layout_gan(
self,
S: int,
ai: npt.NDArray[np.float64],
a1: npt.NDArray[np.float64],
batch_mask: npt.NDArray[np.bool_],
) -> npt.NDArray[np.float64]:
# shape: (B, S) -> (B, S, S)
batch_mask = ~batch_mask[:, None, :] | ~batch_mask[:, :, None]
indices = np.arange(S)
batch_mask[:, indices, indices] = True
ai[batch_mask] = 0.0
# shape: (B, S, S)
ar = np.nan_to_num(ai / a1)
score = ar.sum(axis=(1, 2))
return score
def _compute_layout_gan_pp(
self,
score_ac_layout_gan: npt.NDArray[np.float64],
batch_mask: npt.NDArray[np.bool_],
) -> npt.NDArray[np.float64]:
# shape: (B, S) -> (B,)
batch_mask = batch_mask.sum(axis=1)
# shape: (B,)
score_normalized = score_ac_layout_gan / batch_mask
score_normalized[np.isnan(score_normalized)] = 0.0
return score_normalized
def _compute_layout_gan(
self, S: int, B: int, ai: npt.NDArray[np.float64]
) -> npt.NDArray[np.float64]:
indices = np.arange(S)
ii, jj = np.meshgrid(indices, indices, indexing="ij")
# shape: ii (S, S) -> (1, S, S), jj (S, S) -> (1, S, S)
# shape: (1, S, S) -> (B, S, S)
ai[np.repeat((ii[None, :] >= jj[None, :]), axis=0, repeats=B)] = 0.0
# shape: (B, S, S) -> (B,)
score = ai.sum(axis=(1, 2))
return score
def _compute(
self,
*,
bbox: Union[npt.NDArray[np.float64], List[List[int]]],
mask: Union[npt.NDArray[np.bool_], List[List[bool]]],
) -> Dict[str, npt.NDArray[np.float64]]:
# shape: (B, model_max_length, C)
bbox = np.array(bbox)
# shape: (B, model_max_length)
mask = np.array(mask)
assert bbox.ndim == 3
assert mask.ndim == 2
# S: model_max_length
B, S, C = bbox.shape
# shape: batch_bbox (B, S, C), batch_mask (B, S) -> (B, S, 1) -> (B, S, C)
bbox[np.repeat(~mask[:, :, None], axis=2, repeats=C)] = 0.0
# shape: (C, B, S)
bbox = bbox.transpose(2, 0, 1)
A = self.__calculate_a1_ai(bbox)
# shape: (B,)
score_ac_layout_gan = self._compute_ac_layout_gan(S=S, batch_mask=mask, **A)
# shape: (B,)
score_layout_gan_pp = self._compute_layout_gan_pp(
score_ac_layout_gan=score_ac_layout_gan, batch_mask=mask
)
# shape: (B,)
score_layout_gan = self._compute_layout_gan(B=B, S=S, ai=A["ai"])
return {
"overlap-ACLayoutGAN": score_ac_layout_gan,
"overlap-LayoutGAN++": score_layout_gan_pp,
"overlap-LayoutGAN": score_layout_gan,
}