Skip to content

Housegan

House-GAN Transformers-style package.

HouseGanConfig

Bases: PretrainedConfig

Configuration for the House-GAN graph-conditioned generator.

Parameters:

Name Type Description Default
dataset_name str

Dataset identifier for the vectorized floorplan assets.

'housegan_floorplan_vectorized'
target_set str

House-GAN split target set, one of A through E.

'D'
checkpoint_step int

Original checkpoint training step.

200000
id2label Id2LabelMapping | None

Public zero-based room label map.

None
relation_id2label Id2LabelMapping | None

Signed relation label map.

None
latent_dim int

Per-room latent vector dimension.

128
node_feature_dim int

One-hot room feature dimension.

10
graph_edge_values tuple[int, int]

Supported signed edge values.

(-1, 1)
mask_size int

Generated square room-mask size.

32
canvas_size tuple[int, int]

Original floorplan canvas size as (width, height).

(256, 256)
cmp_channels int

CMP feature channel count.

16
num_cmp_layers int

Number of CMP layers in the generator.

2
postprocess_threshold float

Threshold used by mask-to-box postprocessing.

0.0
bbox_source str

Source of public boxes.

'generated_mask'
source_checkpoint str | None

Original checkpoint path or name.

None
conversion_report HouseGanConversionReport | None

Measured conversion metadata.

None
license_note str

Upstream license and research-purpose warning.

'GPL-3.0 with upstream research-purpose notice'

Examples:

>>> config = HouseGanConfig()
>>> config.id2label[0]
'living_room'
Source code in models/housegan/src/housegan/configuration_housegan.py
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
class HouseGanConfig(PretrainedConfig):
    """Configuration for the House-GAN graph-conditioned generator.

    Args:
        dataset_name: Dataset identifier for the vectorized floorplan assets.
        target_set: House-GAN split target set, one of ``A`` through ``E``.
        checkpoint_step: Original checkpoint training step.
        id2label: Public zero-based room label map.
        relation_id2label: Signed relation label map.
        latent_dim: Per-room latent vector dimension.
        node_feature_dim: One-hot room feature dimension.
        graph_edge_values: Supported signed edge values.
        mask_size: Generated square room-mask size.
        canvas_size: Original floorplan canvas size as ``(width, height)``.
        cmp_channels: CMP feature channel count.
        num_cmp_layers: Number of CMP layers in the generator.
        postprocess_threshold: Threshold used by mask-to-box postprocessing.
        bbox_source: Source of public boxes.
        source_checkpoint: Original checkpoint path or name.
        conversion_report: Measured conversion metadata.
        license_note: Upstream license and research-purpose warning.

    Examples:
        >>> config = HouseGanConfig()
        >>> config.id2label[0]
        'living_room'
    """

    model_type = "housegan"

    def __init__(
        self,
        *,
        dataset_name: str = "housegan_floorplan_vectorized",
        target_set: str = "D",
        checkpoint_step: int = 200000,
        id2label: Id2LabelMapping | None = None,
        relation_id2label: Id2LabelMapping | None = None,
        latent_dim: int = 128,
        node_feature_dim: int = 10,
        graph_edge_values: tuple[int, int] = (-1, 1),
        mask_size: int = 32,
        canvas_size: tuple[int, int] = (256, 256),
        cmp_channels: int = 16,
        num_cmp_layers: int = 2,
        postprocess_threshold: float = 0.0,
        bbox_source: str = "generated_mask",
        source_checkpoint: str | None = None,
        conversion_report: HouseGanConversionReport | None = None,
        license_note: str = "GPL-3.0 with upstream research-purpose notice",
        **kwargs: str | int | float | bool | None,
    ) -> None:
        """Initialize a House-GAN config."""
        kwargs.pop("label2id", None)
        kwargs.pop("num_labels", None)
        kwargs.pop("max_supported_room_type_id", None)
        self.dataset_name = dataset_name
        self.target_set = target_set
        self.checkpoint_step = checkpoint_step
        self.id2label = {
            int(key): value for key, value in (id2label or DEFAULT_ID2LABEL).items()
        }
        self.label2id = {value: key for key, value in self.id2label.items()}
        self.relation_id2label = {
            int(key): value
            for key, value in (relation_id2label or DEFAULT_RELATION_ID2LABEL).items()
        }

        self.latent_dim = latent_dim
        self.node_feature_dim = node_feature_dim
        self.graph_edge_values = tuple(graph_edge_values)
        self.mask_size = mask_size
        self.canvas_size = tuple(canvas_size)

        self.cmp_channels = cmp_channels
        self.num_cmp_layers = num_cmp_layers
        self.postprocess_threshold = postprocess_threshold
        self.bbox_source = bbox_source
        self.source_checkpoint = source_checkpoint

        self.conversion_report = conversion_report or {}
        self.license_note = license_note
        self.num_labels = len(self.id2label)
        self.max_supported_room_type_id = self.num_labels - 1

        super().__init__(id2label=self.id2label, label2id=self.label2id)
        for key, value in kwargs.items():
            setattr(self, key, value)

__init__

__init__(
    *,
    dataset_name: str = "housegan_floorplan_vectorized",
    target_set: str = "D",
    checkpoint_step: int = 200000,
    id2label: Id2LabelMapping | None = None,
    relation_id2label: Id2LabelMapping | None = None,
    latent_dim: int = 128,
    node_feature_dim: int = 10,
    graph_edge_values: tuple[int, int] = (-1, 1),
    mask_size: int = 32,
    canvas_size: tuple[int, int] = (256, 256),
    cmp_channels: int = 16,
    num_cmp_layers: int = 2,
    postprocess_threshold: float = 0.0,
    bbox_source: str = "generated_mask",
    source_checkpoint: str | None = None,
    conversion_report: HouseGanConversionReport
    | None = None,
    license_note: str = "GPL-3.0 with upstream research-purpose notice",
    **kwargs: str | int | float | bool | None,
) -> None

Initialize a House-GAN config.

Source code in models/housegan/src/housegan/configuration_housegan.py
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
def __init__(
    self,
    *,
    dataset_name: str = "housegan_floorplan_vectorized",
    target_set: str = "D",
    checkpoint_step: int = 200000,
    id2label: Id2LabelMapping | None = None,
    relation_id2label: Id2LabelMapping | None = None,
    latent_dim: int = 128,
    node_feature_dim: int = 10,
    graph_edge_values: tuple[int, int] = (-1, 1),
    mask_size: int = 32,
    canvas_size: tuple[int, int] = (256, 256),
    cmp_channels: int = 16,
    num_cmp_layers: int = 2,
    postprocess_threshold: float = 0.0,
    bbox_source: str = "generated_mask",
    source_checkpoint: str | None = None,
    conversion_report: HouseGanConversionReport | None = None,
    license_note: str = "GPL-3.0 with upstream research-purpose notice",
    **kwargs: str | int | float | bool | None,
) -> None:
    """Initialize a House-GAN config."""
    kwargs.pop("label2id", None)
    kwargs.pop("num_labels", None)
    kwargs.pop("max_supported_room_type_id", None)
    self.dataset_name = dataset_name
    self.target_set = target_set
    self.checkpoint_step = checkpoint_step
    self.id2label = {
        int(key): value for key, value in (id2label or DEFAULT_ID2LABEL).items()
    }
    self.label2id = {value: key for key, value in self.id2label.items()}
    self.relation_id2label = {
        int(key): value
        for key, value in (relation_id2label or DEFAULT_RELATION_ID2LABEL).items()
    }

    self.latent_dim = latent_dim
    self.node_feature_dim = node_feature_dim
    self.graph_edge_values = tuple(graph_edge_values)
    self.mask_size = mask_size
    self.canvas_size = tuple(canvas_size)

    self.cmp_channels = cmp_channels
    self.num_cmp_layers = num_cmp_layers
    self.postprocess_threshold = postprocess_threshold
    self.bbox_source = bbox_source
    self.source_checkpoint = source_checkpoint

    self.conversion_report = conversion_report or {}
    self.license_note = license_note
    self.num_labels = len(self.id2label)
    self.max_supported_room_type_id = self.num_labels - 1

    super().__init__(id2label=self.id2label, label2id=self.label2id)
    for key, value in kwargs.items():
        setattr(self, key, value)

HouseGanRelation dataclass

Adjacency relation between two room nodes.

Source code in models/housegan/src/housegan/graph_schema.py
24
25
26
27
28
29
30
31
@dataclass(frozen=True)
class HouseGanRelation:
    """Adjacency relation between two room nodes."""

    source: int
    target: int
    adjacent: bool
    weight: float | None = None

HouseGanRoomNode dataclass

Room node in a House-GAN scene graph.

Source code in models/housegan/src/housegan/graph_schema.py
14
15
16
17
18
19
20
21
@dataclass(frozen=True)
class HouseGanRoomNode:
    """Room node in a House-GAN scene graph."""

    id: int
    label: int | str
    bbox: tuple[float, float, float, float] | None = None
    attributes: Mapping[str, object] | None = None

HouseGanSceneGraph dataclass

Flat room relation graph used by House-GAN.

Source code in models/housegan/src/housegan/graph_schema.py
34
35
36
37
38
39
@dataclass(frozen=True)
class HouseGanSceneGraph:
    """Flat room relation graph used by House-GAN."""

    nodes: tuple[HouseGanRoomNode, ...]
    relations: tuple[HouseGanRelation, ...] | None = None

HouseGanGenerator

Bases: PreTrainedModel

Transformers-compatible House-GAN generator.

Parameters:

Name Type Description Default
config HouseGanConfig

House-GAN configuration.

required

Examples:

>>> model = HouseGanGenerator(HouseGanConfig())
>>> latents = torch.zeros(2, 128)
>>> nodes = torch.eye(10)[:2]
>>> edges = torch.tensor([[0, 1, 1]])
>>> tuple(model(latents, nodes, edges).masks.shape)
(2, 32, 32)
Source code in models/housegan/src/housegan/modeling_housegan.py
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
class HouseGanGenerator(PreTrainedModel):
    """Transformers-compatible House-GAN generator.

    Args:
        config: House-GAN configuration.

    Examples:
        >>> model = HouseGanGenerator(HouseGanConfig())
        >>> latents = torch.zeros(2, 128)
        >>> nodes = torch.eye(10)[:2]
        >>> edges = torch.tensor([[0, 1, 1]])
        >>> tuple(model(latents, nodes, edges).masks.shape)
        (2, 32, 32)
    """

    config_class = HouseGanConfig
    base_model_prefix = "housegan"
    main_input_name = "latents"
    supports_gradient_checkpointing = False

    def __init__(self, config: HouseGanConfig) -> None:
        """Initialize generator layers."""
        super().__init__(config)
        init_size = config.mask_size // 4
        in_features = config.latent_dim + config.node_feature_dim
        channels = config.cmp_channels
        self.init_size = init_size
        self.l1 = nn.Sequential(nn.Linear(in_features, channels * init_size**2))
        self.upsample_1 = nn.Sequential(
            *_conv_block(channels, channels, 4, 2, 1, act="leaky", upsample=True)
        )
        self.upsample_2 = nn.Sequential(
            *_conv_block(channels, channels, 4, 2, 1, act="leaky", upsample=True)
        )
        self.cmp_1 = CMP(channels)
        self.cmp_2 = CMP(channels)
        self.decoder = nn.Sequential(
            *_conv_block(channels, 256, 3, 1, 1, act="leaky"),
            *_conv_block(256, 128, 3, 1, 1, act="leaky"),
            *_conv_block(128, 1, 3, 1, 1, act="tanh"),
        )
        self.post_init()

    def forward(
        self,
        latents: Float[torch.Tensor, "elements latent"],
        node_features: Float[torch.Tensor, "elements room_labels"],
        edges: Int[torch.Tensor, "edges 3"],
        return_dict: bool | None = None,
    ) -> HouseGanModelOutput | tuple[Float[torch.Tensor, "elements height width"]]:
        """Run a House-GAN forward pass.

        Args:
            latents: Per-room latent vectors.
            node_features: Per-room one-hot room features.
            edges: Signed complete graph triples.
            return_dict: Whether to return ``HouseGanModelOutput``.

        Returns:
            Raw generated room masks.
        """
        if latents.ndim != 2 or latents.shape[-1] != self.config.latent_dim:
            raise ValueError("latents must have shape (elements, latent_dim)")

        if node_features.shape != (latents.shape[0], self.config.node_feature_dim):
            raise ValueError(
                "node_features must have shape (elements, node_feature_dim)"
            )

        if edges.ndim != 2 or edges.shape[-1] != 3:
            raise ValueError("edges must have shape (edges, 3)")

        dtype = next(self.parameters()).dtype
        latents = latents.to(dtype=dtype, device=self.device)
        node_features = node_features.to(dtype=dtype, device=self.device)
        edges = edges.to(dtype=torch.long, device=self.device)
        hidden = torch.cat(
            [latents.view(-1, self.config.latent_dim), node_features], dim=1
        )
        hidden = self.l1(hidden)
        hidden = hidden.view(
            -1, self.config.cmp_channels, self.init_size, self.init_size
        )
        hidden = self.cmp_1(hidden, edges).view(-1, *hidden.shape[1:])
        hidden = self.upsample_1(hidden)
        hidden = self.cmp_2(hidden, edges).view(-1, *hidden.shape[1:])
        hidden = self.upsample_2(hidden)
        masks = self.decoder(hidden.view(-1, hidden.shape[1], *hidden.shape[2:]))
        masks = masks.view(-1, *masks.shape[2:])
        if return_dict is False:
            return (masks,)
        return HouseGanModelOutput(
            masks=masks, node_features=node_features, edges=edges
        )

__init__

__init__(config: HouseGanConfig) -> None

Initialize generator layers.

Source code in models/housegan/src/housegan/modeling_housegan.py
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
def __init__(self, config: HouseGanConfig) -> None:
    """Initialize generator layers."""
    super().__init__(config)
    init_size = config.mask_size // 4
    in_features = config.latent_dim + config.node_feature_dim
    channels = config.cmp_channels
    self.init_size = init_size
    self.l1 = nn.Sequential(nn.Linear(in_features, channels * init_size**2))
    self.upsample_1 = nn.Sequential(
        *_conv_block(channels, channels, 4, 2, 1, act="leaky", upsample=True)
    )
    self.upsample_2 = nn.Sequential(
        *_conv_block(channels, channels, 4, 2, 1, act="leaky", upsample=True)
    )
    self.cmp_1 = CMP(channels)
    self.cmp_2 = CMP(channels)
    self.decoder = nn.Sequential(
        *_conv_block(channels, 256, 3, 1, 1, act="leaky"),
        *_conv_block(256, 128, 3, 1, 1, act="leaky"),
        *_conv_block(128, 1, 3, 1, 1, act="tanh"),
    )
    self.post_init()

forward

forward(
    latents: Float[Tensor, "elements latent"],
    node_features: Float[Tensor, "elements room_labels"],
    edges: Int[Tensor, "edges 3"],
    return_dict: bool | None = None,
) -> (
    HouseGanModelOutput
    | tuple[Float[torch.Tensor, "elements height width"]]
)

Run a House-GAN forward pass.

Parameters:

Name Type Description Default
latents Float[Tensor, 'elements latent']

Per-room latent vectors.

required
node_features Float[Tensor, 'elements room_labels']

Per-room one-hot room features.

required
edges Int[Tensor, 'edges 3']

Signed complete graph triples.

required
return_dict bool | None

Whether to return HouseGanModelOutput.

None

Returns:

Type Description
HouseGanModelOutput | tuple[Float[Tensor, 'elements height width']]

Raw generated room masks.

Source code in models/housegan/src/housegan/modeling_housegan.py
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
def forward(
    self,
    latents: Float[torch.Tensor, "elements latent"],
    node_features: Float[torch.Tensor, "elements room_labels"],
    edges: Int[torch.Tensor, "edges 3"],
    return_dict: bool | None = None,
) -> HouseGanModelOutput | tuple[Float[torch.Tensor, "elements height width"]]:
    """Run a House-GAN forward pass.

    Args:
        latents: Per-room latent vectors.
        node_features: Per-room one-hot room features.
        edges: Signed complete graph triples.
        return_dict: Whether to return ``HouseGanModelOutput``.

    Returns:
        Raw generated room masks.
    """
    if latents.ndim != 2 or latents.shape[-1] != self.config.latent_dim:
        raise ValueError("latents must have shape (elements, latent_dim)")

    if node_features.shape != (latents.shape[0], self.config.node_feature_dim):
        raise ValueError(
            "node_features must have shape (elements, node_feature_dim)"
        )

    if edges.ndim != 2 or edges.shape[-1] != 3:
        raise ValueError("edges must have shape (edges, 3)")

    dtype = next(self.parameters()).dtype
    latents = latents.to(dtype=dtype, device=self.device)
    node_features = node_features.to(dtype=dtype, device=self.device)
    edges = edges.to(dtype=torch.long, device=self.device)
    hidden = torch.cat(
        [latents.view(-1, self.config.latent_dim), node_features], dim=1
    )
    hidden = self.l1(hidden)
    hidden = hidden.view(
        -1, self.config.cmp_channels, self.init_size, self.init_size
    )
    hidden = self.cmp_1(hidden, edges).view(-1, *hidden.shape[1:])
    hidden = self.upsample_1(hidden)
    hidden = self.cmp_2(hidden, edges).view(-1, *hidden.shape[1:])
    hidden = self.upsample_2(hidden)
    masks = self.decoder(hidden.view(-1, hidden.shape[1], *hidden.shape[2:]))
    masks = masks.view(-1, *masks.shape[2:])
    if return_dict is False:
        return (masks,)
    return HouseGanModelOutput(
        masks=masks, node_features=node_features, edges=edges
    )

HouseGanModelOutput dataclass

Bases: ModelOutput

Raw House-GAN model output.

Source code in models/housegan/src/housegan/modeling_housegan.py
16
17
18
19
20
21
22
@dataclass
class HouseGanModelOutput(ModelOutput):
    """Raw House-GAN model output."""

    masks: Float[torch.Tensor, "elements height width"]
    node_features: Float[torch.Tensor, "elements room_labels"] | None = None
    edges: Int[torch.Tensor, "edges 3"] | None = None

HouseGanPipeline

Bases: LayoutGenerationPipeline

Transformers-side House-GAN layout generation pipeline.

Source code in models/housegan/src/housegan/pipeline_housegan.py
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
class HouseGanPipeline(LayoutGenerationPipeline):
    """Transformers-side House-GAN layout generation pipeline."""

    config_class: ClassVar[type[PretrainedConfig]] = HouseGanConfig
    component_specs: ClassVar[dict[str, PipelineComponentSpec]] = (
        model_processor_component_specs(
            model_loader=_load_model_component,
            processor_loader=_load_processor_component,
        )
    )

    config: HouseGanConfig
    model: HouseGanGenerator
    processor: HouseGanProcessor

    def __init__(
        self,
        model: HouseGanGenerator,
        processor: HouseGanProcessor | None = None,
        config: HouseGanConfig | None = None,
        device: int | torch.device | None = None,
    ) -> None:
        """Initialize the pipeline."""
        super().__init__(config or model.config)
        self.config = config or model.config
        self.model = model
        self.processor = processor or HouseGanProcessor(
            config=self.config,
        )
        if device is not None:
            resolved = (
                torch.device("cpu")
                if isinstance(device, int) and device < 0
                else torch.device(f"cuda:{device}")
                if isinstance(device, int)
                else device
            )
            self.to(resolved)

    @classmethod
    def _from_pretrained_components(
        cls,
        *,
        config: PretrainedConfig,
        components: Mapping[str, PipelineComponent | None],
    ) -> "HouseGanPipeline":
        """Build a pipeline from loaded model and processor components."""
        return cls(
            config=cast(HouseGanConfig, config),
            model=cast(HouseGanGenerator, components["model"]),
            processor=cast(HouseGanProcessor, components["processor"]),
        )

    @torch.no_grad()
    def __call__(  # ty: ignore[invalid-method-override]
        self,
        *,
        batch_size: int = 1,
        seed: int | None = None,
        generator: torch.Generator | None = None,
        condition_type: ConditionType | str = ConditionType.relation,
        labels: Int[torch.Tensor, "..."]
        | Int[np.ndarray, "..."]
        | NestedIntList
        | None = None,
        bbox: Float[torch.Tensor, "... 4"]
        | Float[np.ndarray, "... 4"]
        | NestedFloatList
        | None = None,
        mask: Bool[torch.Tensor, "..."]
        | Bool[np.ndarray, "..."]
        | NestedBoolList
        | None = None,
        num_elements: int | list[int] | Int[torch.Tensor, "..."] | None = None,
        box_format: BoxFormat | str = BoxFormat.xywh,
        normalized: bool = True,
        canvas_size: tuple[int, int] | None = None,
        num_inference_steps: int | None = None,
        scene_graph: HouseGanSceneGraph
        | HouseGanSceneGraphPayload
        | list[HouseGanSceneGraphPayload]
        | None = None,
        relations: Sequence[HouseGanRelationPayload] | None = None,
        latents: Float[torch.Tensor, "elements latent"] | None = None,
        output_type: OutputType = "dataclass",
        return_intermediates: bool = False,
    ) -> (
        LayoutGenerationOutput
        | dict[
            str,
            Shaped[torch.Tensor, "..."]
            | dict[int, str]
            | list[dict[str, str | int | float | bool | None]]
            | None,
        ]
    ):
        """Generate a floorplan layout from room relation constraints."""
        del num_elements, num_inference_steps
        if batch_size < 1:
            raise ValueError("batch_size must be positive")

        graph_batch = _expand_graph_batch(scene_graph, batch_size)
        outputs: list[LayoutGenerationOutput] = []
        torch_generator = self.prepare_generator(
            generator=generator,
            seed=seed,
            device=self.device or next(self.model.parameters()).device,
        )
        for graph_index, graph_item in enumerate(graph_batch):
            condition = self.processor(
                condition_type=condition_type,
                scene_graph=graph_item,
                relations=relations,
                labels=labels,
                bbox=bbox,
                mask=mask,
                box_format=box_format,
                normalized=normalized,
                canvas_size=canvas_size,
            )
            node_features = condition["node_features"].to(self.model.device)
            edges = condition["edges"].to(self.model.device)
            labels_t = condition["labels"].to(self.model.device)
            room_count = node_features.shape[0]
            graph_latents = latents
            if graph_latents is None:
                graph_latents = torch.randn(
                    room_count,
                    self.model.config.latent_dim,
                    generator=torch_generator,
                    device=self.model.device,
                    dtype=next(self.model.parameters()).dtype,
                )
            elif graph_latents.ndim == 3:
                graph_latents = graph_latents[graph_index]
            model_output = self.model(
                latents=graph_latents.to(self.model.device),
                node_features=node_features,
                edges=edges,
            )
            decoded = self.processor.post_process_masks(
                model_output.masks,
                labels=labels_t,
                edges=edges,
                node_features=node_features,
                scene_graph=condition["scene_graph"],
                output_type="dataclass",
                return_intermediates=return_intermediates,
            )
            outputs.append(cast(LayoutGenerationOutput, decoded))
        merged = _merge_outputs(outputs, output_type=output_type)
        return merged

__init__

__init__(
    model: HouseGanGenerator,
    processor: HouseGanProcessor | None = None,
    config: HouseGanConfig | None = None,
    device: int | device | None = None,
) -> None

Initialize the pipeline.

Source code in models/housegan/src/housegan/pipeline_housegan.py
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
def __init__(
    self,
    model: HouseGanGenerator,
    processor: HouseGanProcessor | None = None,
    config: HouseGanConfig | None = None,
    device: int | torch.device | None = None,
) -> None:
    """Initialize the pipeline."""
    super().__init__(config or model.config)
    self.config = config or model.config
    self.model = model
    self.processor = processor or HouseGanProcessor(
        config=self.config,
    )
    if device is not None:
        resolved = (
            torch.device("cpu")
            if isinstance(device, int) and device < 0
            else torch.device(f"cuda:{device}")
            if isinstance(device, int)
            else device
        )
        self.to(resolved)

__call__

__call__(
    *,
    batch_size: int = 1,
    seed: int | None = None,
    generator: Generator | None = None,
    condition_type: ConditionType
    | str = ConditionType.relation,
    labels: Int[Tensor, "..."]
    | Int[ndarray, "..."]
    | NestedIntList
    | None = None,
    bbox: Float[Tensor, "... 4"]
    | Float[ndarray, "... 4"]
    | NestedFloatList
    | None = None,
    mask: Bool[Tensor, "..."]
    | Bool[ndarray, "..."]
    | NestedBoolList
    | None = None,
    num_elements: int
    | list[int]
    | Int[Tensor, "..."]
    | None = None,
    box_format: BoxFormat | str = BoxFormat.xywh,
    normalized: bool = True,
    canvas_size: tuple[int, int] | None = None,
    num_inference_steps: int | None = None,
    scene_graph: HouseGanSceneGraph
    | HouseGanSceneGraphPayload
    | list[HouseGanSceneGraphPayload]
    | None = None,
    relations: Sequence[HouseGanRelationPayload]
    | None = None,
    latents: Float[Tensor, "elements latent"] | None = None,
    output_type: OutputType = "dataclass",
    return_intermediates: bool = False,
) -> (
    LayoutGenerationOutput
    | dict[
        str,
        Shaped[torch.Tensor, "..."]
        | dict[int, str]
        | list[dict[str, str | int | float | bool | None]]
        | None,
    ]
)

Generate a floorplan layout from room relation constraints.

Source code in models/housegan/src/housegan/pipeline_housegan.py
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
@torch.no_grad()
def __call__(  # ty: ignore[invalid-method-override]
    self,
    *,
    batch_size: int = 1,
    seed: int | None = None,
    generator: torch.Generator | None = None,
    condition_type: ConditionType | str = ConditionType.relation,
    labels: Int[torch.Tensor, "..."]
    | Int[np.ndarray, "..."]
    | NestedIntList
    | None = None,
    bbox: Float[torch.Tensor, "... 4"]
    | Float[np.ndarray, "... 4"]
    | NestedFloatList
    | None = None,
    mask: Bool[torch.Tensor, "..."]
    | Bool[np.ndarray, "..."]
    | NestedBoolList
    | None = None,
    num_elements: int | list[int] | Int[torch.Tensor, "..."] | None = None,
    box_format: BoxFormat | str = BoxFormat.xywh,
    normalized: bool = True,
    canvas_size: tuple[int, int] | None = None,
    num_inference_steps: int | None = None,
    scene_graph: HouseGanSceneGraph
    | HouseGanSceneGraphPayload
    | list[HouseGanSceneGraphPayload]
    | None = None,
    relations: Sequence[HouseGanRelationPayload] | None = None,
    latents: Float[torch.Tensor, "elements latent"] | None = None,
    output_type: OutputType = "dataclass",
    return_intermediates: bool = False,
) -> (
    LayoutGenerationOutput
    | dict[
        str,
        Shaped[torch.Tensor, "..."]
        | dict[int, str]
        | list[dict[str, str | int | float | bool | None]]
        | None,
    ]
):
    """Generate a floorplan layout from room relation constraints."""
    del num_elements, num_inference_steps
    if batch_size < 1:
        raise ValueError("batch_size must be positive")

    graph_batch = _expand_graph_batch(scene_graph, batch_size)
    outputs: list[LayoutGenerationOutput] = []
    torch_generator = self.prepare_generator(
        generator=generator,
        seed=seed,
        device=self.device or next(self.model.parameters()).device,
    )
    for graph_index, graph_item in enumerate(graph_batch):
        condition = self.processor(
            condition_type=condition_type,
            scene_graph=graph_item,
            relations=relations,
            labels=labels,
            bbox=bbox,
            mask=mask,
            box_format=box_format,
            normalized=normalized,
            canvas_size=canvas_size,
        )
        node_features = condition["node_features"].to(self.model.device)
        edges = condition["edges"].to(self.model.device)
        labels_t = condition["labels"].to(self.model.device)
        room_count = node_features.shape[0]
        graph_latents = latents
        if graph_latents is None:
            graph_latents = torch.randn(
                room_count,
                self.model.config.latent_dim,
                generator=torch_generator,
                device=self.model.device,
                dtype=next(self.model.parameters()).dtype,
            )
        elif graph_latents.ndim == 3:
            graph_latents = graph_latents[graph_index]
        model_output = self.model(
            latents=graph_latents.to(self.model.device),
            node_features=node_features,
            edges=edges,
        )
        decoded = self.processor.post_process_masks(
            model_output.masks,
            labels=labels_t,
            edges=edges,
            node_features=node_features,
            scene_graph=condition["scene_graph"],
            output_type="dataclass",
            return_intermediates=return_intermediates,
        )
        outputs.append(cast(LayoutGenerationOutput, decoded))
    merged = _merge_outputs(outputs, output_type=output_type)
    return merged

HouseGanProcessor

Bases: ProcessorMixin

Normalize House-GAN scene graphs and decode generated masks.

Source code in models/housegan/src/housegan/processing_housegan.py
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
class HouseGanProcessor(ProcessorMixin):
    """Normalize House-GAN scene graphs and decode generated masks."""

    attributes: list[str] = []
    config_name = "processor_config.json"

    def __init__(
        self,
        *,
        config: HouseGanConfig,
        default_missing_relation: Literal["not_adjacent", "error"] = "not_adjacent",
    ) -> None:
        """Initialize processor metadata."""
        self.config = config
        self.id2label = {
            int(key): value
            for key, value in cast(Id2LabelMapping, self.config.id2label).items()
        }
        self.label2id = {value: key for key, value in self.id2label.items()}
        self.relation_id2label = {
            int(key): value
            for key, value in cast(
                Id2LabelMapping, self.config.relation_id2label
            ).items()
        }
        self.canvas_size = tuple(self.config.canvas_size)
        self.mask_size = self.config.mask_size
        self.default_missing_relation = default_missing_relation
        self.chat_template = None

    def save_pretrained(
        self,
        save_directory: str | Path,
        push_to_hub: bool = False,
        **kwargs: str | int | float | bool | None,
    ) -> None:
        """Save processor metadata."""
        del push_to_hub, kwargs
        root = Path(save_directory)
        root.mkdir(parents=True, exist_ok=True)
        payload = {
            "config": self.config.to_dict(),
            "processor_class": self.__class__.__name__,
            "id2label": self.id2label,
            "relation_id2label": self.relation_id2label,
            "canvas_size": self.canvas_size,
            "mask_size": self.mask_size,
            "default_missing_relation": self.default_missing_relation,
        }
        (root / self.config_name).write_text(
            json.dumps(payload, indent=2, sort_keys=True), encoding="utf-8"
        )

    @classmethod
    def from_pretrained(
        cls,
        pretrained_model_name_or_path: str | PathLike[str],
        cache_dir: str | PathLike[str] | None = None,
        force_download: bool = False,
        local_files_only: bool = False,
        token: str | bool | None = None,
        revision: str = "main",
        subfolder: str | None = None,
        **kwargs: str | int | float | bool | None,
    ) -> Self:
        """Load processor metadata from ``processor_config.json``."""
        del cache_dir, force_download, local_files_only, token, revision, kwargs
        root = Path(pretrained_model_name_or_path)
        if subfolder is not None:
            root = root / subfolder
        payload = json.loads((root / cls.config_name).read_text(encoding="utf-8"))
        config_payload = payload.get("config")
        if not isinstance(config_payload, dict):
            raise TypeError("processor config payload must be a dictionary")

        return cls(
            config=HouseGanConfig.from_dict(config_payload),
            default_missing_relation=payload.get(
                "default_missing_relation", "not_adjacent"
            ),
        )

    def __call__(
        self,
        *,
        condition_type: ConditionType | str = ConditionType.relation,
        scene_graph: HouseGanSceneGraph | HouseGanSceneGraphPayload | None = None,
        relations: Sequence[HouseGanRelationPayload] | None = None,
        labels: Int[torch.Tensor, "..."]
        | Int[np.ndarray, "..."]
        | Sequence[Sequence[int]]
        | Sequence[int]
        | None = None,
        bbox: Float[torch.Tensor, "... 4"]
        | Float[np.ndarray, "... 4"]
        | Sequence[Sequence[Sequence[float]]]
        | Sequence[Sequence[float]]
        | Sequence[ArrayLikeInput]
        | None = None,
        mask: Bool[torch.Tensor, "..."]
        | Bool[np.ndarray, "..."]
        | Sequence[Sequence[bool]]
        | Sequence[bool]
        | Sequence[ArrayLikeInput]
        | None = None,
        box_format: BoxFormat | str = BoxFormat.xywh,
        normalized: bool = True,
        canvas_size: tuple[int, int] | None = None,
        return_tensors: Literal["pt"] = "pt",
    ) -> BatchEncoding:
        """Encode public relation inputs into House-GAN tensors."""
        if return_tensors != "pt":
            raise ValueError("HouseGanProcessor only supports return_tensors='pt'")

        self.normalize_condition_type(condition_type)
        relation_payload = relations
        if relation_payload is None and bbox is not None and labels is not None:
            bbox_t, labels_t, _ = prepare_layout_tensors(
                bbox=cast(
                    Float[torch.Tensor, "... 4"]
                    | Float[np.ndarray, "... 4"]
                    | Sequence[Sequence[Sequence[float]]]
                    | Sequence[Sequence[float]]
                    | Sequence[ArrayLikeInput],
                    bbox,
                ),
                labels=cast(
                    Int[torch.Tensor, "..."]
                    | Int[np.ndarray, "..."]
                    | Sequence[Sequence[int]]
                    | Sequence[int]
                    | Sequence[ArrayLikeInput],
                    labels,
                ),
                mask=cast(
                    Bool[torch.Tensor, "..."]
                    | Bool[np.ndarray, "..."]
                    | Sequence[Sequence[bool]]
                    | Sequence[bool]
                    | Sequence[ArrayLikeInput]
                    | None,
                    mask,
                ),
                box_format=normalize_box_format(box_format),
                normalized=normalized,
                canvas_size=canvas_size or self.canvas_size,
                clamp_converted_normalized=True,
            )
            relation_payload = relation_from_bboxes(
                _xywh_to_ltrb_list(bbox_t[0]),
            )
            labels = labels_t[0]
        graph = normalize_scene_graph(
            scene_graph,
            labels=labels,
            relations=relation_payload,
            id2label=self.id2label,
        )
        if self.default_missing_relation == "error" and not graph.relations:
            raise ValueError(
                "House-GAN requires relations when missing-pair policy is 'error'"
            )

        node_features = graph_to_node_features(
            graph.nodes,
            label2id=self.label2id,
            num_labels=len(self.id2label),
        )
        edges = complete_signed_edges(
            graph.nodes,
            graph.relations,
            default_adjacent=False,
        )
        label_ids = torch.tensor(
            [
                self.label2id[node.label]
                if isinstance(node.label, str)
                else int(node.label)
                for node in graph.nodes
            ],
            dtype=torch.long,
        )
        return BatchEncoding(
            {
                "node_features": node_features,
                "edges": edges,
                "labels": label_ids,
                "scene_graph": graph,
            }
        )

    def normalize_condition_type(
        self, condition_type: ConditionType | str
    ) -> ConditionType:
        """Normalize and validate the House-GAN condition type."""
        condition = normalize_condition_type(condition_type)
        if condition is not ConditionType.relation:
            raise NotImplementedError(
                "House-GAN supports only condition_type='relation' and aliases "
                "'scene_graph', 'graph', or 'gen_r'."
            )

        return condition

    def post_process_masks(
        self,
        masks: Float[torch.Tensor, "elements height width"],
        *,
        labels: Int[torch.Tensor, "elements"],
        edges: Int[torch.Tensor, "edges 3"] | None = None,
        node_features: Float[torch.Tensor, "elements room_labels"] | None = None,
        scene_graph: HouseGanSceneGraph | None = None,
        output_type: OutputType = "dataclass",
        return_intermediates: bool = False,
    ) -> (
        LayoutGenerationOutput
        | dict[
            str,
            Shaped[torch.Tensor, "..."]
            | dict[int, str]
            | dict[str, Shaped[torch.Tensor, "..."] | HouseGanSceneGraph | None]
            | None,
        ]
    ):
        """Convert generated room masks to public normalized boxes."""
        bbox_ltrb = mask_to_ltrb(masks, threshold=0.0)
        bbox_xywh = (
            ltrb_to_xywh(bbox_ltrb / float(self.mask_size)).unsqueeze(0).clamp(0.0, 1.0)
        )
        labels_b = labels.to(dtype=torch.long).unsqueeze(0)
        valid = torch.ones(labels_b.shape, dtype=torch.bool, device=labels_b.device)
        intermediates = None
        if return_intermediates:
            intermediates = {
                "room_masks": masks.detach(),
                "bbox_ltrb_32": bbox_ltrb,
                "signed_edges": edges,
                "node_features": node_features,
                "scene_graph": scene_graph,
            }
        if output_type == "dict":
            return {
                "bbox": bbox_xywh,
                "labels": labels_b,
                "mask": valid,
                "id2label": self.id2label,
                "sequences": edges,
                "scores": None,
                "trajectory": None,
                "intermediates": intermediates,
            }
        return LayoutGenerationOutput(
            bbox=bbox_xywh,
            labels=labels_b,
            mask=valid,
            id2label=self.id2label,
            sequences=edges,
            intermediates=intermediates,
        )

__init__

__init__(
    *,
    config: HouseGanConfig,
    default_missing_relation: Literal[
        "not_adjacent", "error"
    ] = "not_adjacent",
) -> None

Initialize processor metadata.

Source code in models/housegan/src/housegan/processing_housegan.py
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
def __init__(
    self,
    *,
    config: HouseGanConfig,
    default_missing_relation: Literal["not_adjacent", "error"] = "not_adjacent",
) -> None:
    """Initialize processor metadata."""
    self.config = config
    self.id2label = {
        int(key): value
        for key, value in cast(Id2LabelMapping, self.config.id2label).items()
    }
    self.label2id = {value: key for key, value in self.id2label.items()}
    self.relation_id2label = {
        int(key): value
        for key, value in cast(
            Id2LabelMapping, self.config.relation_id2label
        ).items()
    }
    self.canvas_size = tuple(self.config.canvas_size)
    self.mask_size = self.config.mask_size
    self.default_missing_relation = default_missing_relation
    self.chat_template = None

save_pretrained

save_pretrained(
    save_directory: str | Path,
    push_to_hub: bool = False,
    **kwargs: str | int | float | bool | None,
) -> None

Save processor metadata.

Source code in models/housegan/src/housegan/processing_housegan.py
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
def save_pretrained(
    self,
    save_directory: str | Path,
    push_to_hub: bool = False,
    **kwargs: str | int | float | bool | None,
) -> None:
    """Save processor metadata."""
    del push_to_hub, kwargs
    root = Path(save_directory)
    root.mkdir(parents=True, exist_ok=True)
    payload = {
        "config": self.config.to_dict(),
        "processor_class": self.__class__.__name__,
        "id2label": self.id2label,
        "relation_id2label": self.relation_id2label,
        "canvas_size": self.canvas_size,
        "mask_size": self.mask_size,
        "default_missing_relation": self.default_missing_relation,
    }
    (root / self.config_name).write_text(
        json.dumps(payload, indent=2, sort_keys=True), encoding="utf-8"
    )

from_pretrained classmethod

from_pretrained(
    pretrained_model_name_or_path: str | PathLike[str],
    cache_dir: str | PathLike[str] | None = None,
    force_download: bool = False,
    local_files_only: bool = False,
    token: str | bool | None = None,
    revision: str = "main",
    subfolder: str | None = None,
    **kwargs: str | int | float | bool | None,
) -> Self

Load processor metadata from processor_config.json.

Source code in models/housegan/src/housegan/processing_housegan.py
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
@classmethod
def from_pretrained(
    cls,
    pretrained_model_name_or_path: str | PathLike[str],
    cache_dir: str | PathLike[str] | None = None,
    force_download: bool = False,
    local_files_only: bool = False,
    token: str | bool | None = None,
    revision: str = "main",
    subfolder: str | None = None,
    **kwargs: str | int | float | bool | None,
) -> Self:
    """Load processor metadata from ``processor_config.json``."""
    del cache_dir, force_download, local_files_only, token, revision, kwargs
    root = Path(pretrained_model_name_or_path)
    if subfolder is not None:
        root = root / subfolder
    payload = json.loads((root / cls.config_name).read_text(encoding="utf-8"))
    config_payload = payload.get("config")
    if not isinstance(config_payload, dict):
        raise TypeError("processor config payload must be a dictionary")

    return cls(
        config=HouseGanConfig.from_dict(config_payload),
        default_missing_relation=payload.get(
            "default_missing_relation", "not_adjacent"
        ),
    )

__call__

__call__(
    *,
    condition_type: ConditionType
    | str = ConditionType.relation,
    scene_graph: HouseGanSceneGraph
    | HouseGanSceneGraphPayload
    | None = None,
    relations: Sequence[HouseGanRelationPayload]
    | None = None,
    labels: Int[Tensor, "..."]
    | Int[ndarray, "..."]
    | Sequence[Sequence[int]]
    | Sequence[int]
    | None = None,
    bbox: Float[Tensor, "... 4"]
    | Float[ndarray, "... 4"]
    | Sequence[Sequence[Sequence[float]]]
    | Sequence[Sequence[float]]
    | Sequence[ArrayLikeInput]
    | None = None,
    mask: Bool[Tensor, "..."]
    | Bool[ndarray, "..."]
    | Sequence[Sequence[bool]]
    | Sequence[bool]
    | Sequence[ArrayLikeInput]
    | None = None,
    box_format: BoxFormat | str = BoxFormat.xywh,
    normalized: bool = True,
    canvas_size: tuple[int, int] | None = None,
    return_tensors: Literal["pt"] = "pt",
) -> BatchEncoding

Encode public relation inputs into House-GAN tensors.

Source code in models/housegan/src/housegan/processing_housegan.py
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
def __call__(
    self,
    *,
    condition_type: ConditionType | str = ConditionType.relation,
    scene_graph: HouseGanSceneGraph | HouseGanSceneGraphPayload | None = None,
    relations: Sequence[HouseGanRelationPayload] | None = None,
    labels: Int[torch.Tensor, "..."]
    | Int[np.ndarray, "..."]
    | Sequence[Sequence[int]]
    | Sequence[int]
    | None = None,
    bbox: Float[torch.Tensor, "... 4"]
    | Float[np.ndarray, "... 4"]
    | Sequence[Sequence[Sequence[float]]]
    | Sequence[Sequence[float]]
    | Sequence[ArrayLikeInput]
    | None = None,
    mask: Bool[torch.Tensor, "..."]
    | Bool[np.ndarray, "..."]
    | Sequence[Sequence[bool]]
    | Sequence[bool]
    | Sequence[ArrayLikeInput]
    | None = None,
    box_format: BoxFormat | str = BoxFormat.xywh,
    normalized: bool = True,
    canvas_size: tuple[int, int] | None = None,
    return_tensors: Literal["pt"] = "pt",
) -> BatchEncoding:
    """Encode public relation inputs into House-GAN tensors."""
    if return_tensors != "pt":
        raise ValueError("HouseGanProcessor only supports return_tensors='pt'")

    self.normalize_condition_type(condition_type)
    relation_payload = relations
    if relation_payload is None and bbox is not None and labels is not None:
        bbox_t, labels_t, _ = prepare_layout_tensors(
            bbox=cast(
                Float[torch.Tensor, "... 4"]
                | Float[np.ndarray, "... 4"]
                | Sequence[Sequence[Sequence[float]]]
                | Sequence[Sequence[float]]
                | Sequence[ArrayLikeInput],
                bbox,
            ),
            labels=cast(
                Int[torch.Tensor, "..."]
                | Int[np.ndarray, "..."]
                | Sequence[Sequence[int]]
                | Sequence[int]
                | Sequence[ArrayLikeInput],
                labels,
            ),
            mask=cast(
                Bool[torch.Tensor, "..."]
                | Bool[np.ndarray, "..."]
                | Sequence[Sequence[bool]]
                | Sequence[bool]
                | Sequence[ArrayLikeInput]
                | None,
                mask,
            ),
            box_format=normalize_box_format(box_format),
            normalized=normalized,
            canvas_size=canvas_size or self.canvas_size,
            clamp_converted_normalized=True,
        )
        relation_payload = relation_from_bboxes(
            _xywh_to_ltrb_list(bbox_t[0]),
        )
        labels = labels_t[0]
    graph = normalize_scene_graph(
        scene_graph,
        labels=labels,
        relations=relation_payload,
        id2label=self.id2label,
    )
    if self.default_missing_relation == "error" and not graph.relations:
        raise ValueError(
            "House-GAN requires relations when missing-pair policy is 'error'"
        )

    node_features = graph_to_node_features(
        graph.nodes,
        label2id=self.label2id,
        num_labels=len(self.id2label),
    )
    edges = complete_signed_edges(
        graph.nodes,
        graph.relations,
        default_adjacent=False,
    )
    label_ids = torch.tensor(
        [
            self.label2id[node.label]
            if isinstance(node.label, str)
            else int(node.label)
            for node in graph.nodes
        ],
        dtype=torch.long,
    )
    return BatchEncoding(
        {
            "node_features": node_features,
            "edges": edges,
            "labels": label_ids,
            "scene_graph": graph,
        }
    )

normalize_condition_type

normalize_condition_type(
    condition_type: ConditionType | str,
) -> ConditionType

Normalize and validate the House-GAN condition type.

Source code in models/housegan/src/housegan/processing_housegan.py
232
233
234
235
236
237
238
239
240
241
242
243
def normalize_condition_type(
    self, condition_type: ConditionType | str
) -> ConditionType:
    """Normalize and validate the House-GAN condition type."""
    condition = normalize_condition_type(condition_type)
    if condition is not ConditionType.relation:
        raise NotImplementedError(
            "House-GAN supports only condition_type='relation' and aliases "
            "'scene_graph', 'graph', or 'gen_r'."
        )

    return condition

post_process_masks

post_process_masks(
    masks: Float[Tensor, "elements height width"],
    *,
    labels: Int[Tensor, "elements"],
    edges: Int[Tensor, "edges 3"] | None = None,
    node_features: Float[Tensor, "elements room_labels"]
    | None = None,
    scene_graph: HouseGanSceneGraph | None = None,
    output_type: OutputType = "dataclass",
    return_intermediates: bool = False,
) -> (
    LayoutGenerationOutput
    | dict[
        str,
        Shaped[torch.Tensor, "..."]
        | dict[int, str]
        | dict[
            str,
            Shaped[torch.Tensor, "..."]
            | HouseGanSceneGraph
            | None,
        ]
        | None,
    ]
)

Convert generated room masks to public normalized boxes.

Source code in models/housegan/src/housegan/processing_housegan.py
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
def post_process_masks(
    self,
    masks: Float[torch.Tensor, "elements height width"],
    *,
    labels: Int[torch.Tensor, "elements"],
    edges: Int[torch.Tensor, "edges 3"] | None = None,
    node_features: Float[torch.Tensor, "elements room_labels"] | None = None,
    scene_graph: HouseGanSceneGraph | None = None,
    output_type: OutputType = "dataclass",
    return_intermediates: bool = False,
) -> (
    LayoutGenerationOutput
    | dict[
        str,
        Shaped[torch.Tensor, "..."]
        | dict[int, str]
        | dict[str, Shaped[torch.Tensor, "..."] | HouseGanSceneGraph | None]
        | None,
    ]
):
    """Convert generated room masks to public normalized boxes."""
    bbox_ltrb = mask_to_ltrb(masks, threshold=0.0)
    bbox_xywh = (
        ltrb_to_xywh(bbox_ltrb / float(self.mask_size)).unsqueeze(0).clamp(0.0, 1.0)
    )
    labels_b = labels.to(dtype=torch.long).unsqueeze(0)
    valid = torch.ones(labels_b.shape, dtype=torch.bool, device=labels_b.device)
    intermediates = None
    if return_intermediates:
        intermediates = {
            "room_masks": masks.detach(),
            "bbox_ltrb_32": bbox_ltrb,
            "signed_edges": edges,
            "node_features": node_features,
            "scene_graph": scene_graph,
        }
    if output_type == "dict":
        return {
            "bbox": bbox_xywh,
            "labels": labels_b,
            "mask": valid,
            "id2label": self.id2label,
            "sequences": edges,
            "scores": None,
            "trajectory": None,
            "intermediates": intermediates,
        }
    return LayoutGenerationOutput(
        bbox=bbox_xywh,
        labels=labels_b,
        mask=valid,
        id2label=self.id2label,
        sequences=edges,
        intermediates=intermediates,
    )

configuration_housegan

Configuration for House-GAN generator conversion.

HouseGanConfig

Bases: PretrainedConfig

Configuration for the House-GAN graph-conditioned generator.

Parameters:

Name Type Description Default
dataset_name str

Dataset identifier for the vectorized floorplan assets.

'housegan_floorplan_vectorized'
target_set str

House-GAN split target set, one of A through E.

'D'
checkpoint_step int

Original checkpoint training step.

200000
id2label Id2LabelMapping | None

Public zero-based room label map.

None
relation_id2label Id2LabelMapping | None

Signed relation label map.

None
latent_dim int

Per-room latent vector dimension.

128
node_feature_dim int

One-hot room feature dimension.

10
graph_edge_values tuple[int, int]

Supported signed edge values.

(-1, 1)
mask_size int

Generated square room-mask size.

32
canvas_size tuple[int, int]

Original floorplan canvas size as (width, height).

(256, 256)
cmp_channels int

CMP feature channel count.

16
num_cmp_layers int

Number of CMP layers in the generator.

2
postprocess_threshold float

Threshold used by mask-to-box postprocessing.

0.0
bbox_source str

Source of public boxes.

'generated_mask'
source_checkpoint str | None

Original checkpoint path or name.

None
conversion_report HouseGanConversionReport | None

Measured conversion metadata.

None
license_note str

Upstream license and research-purpose warning.

'GPL-3.0 with upstream research-purpose notice'

Examples:

>>> config = HouseGanConfig()
>>> config.id2label[0]
'living_room'
Source code in models/housegan/src/housegan/configuration_housegan.py
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
class HouseGanConfig(PretrainedConfig):
    """Configuration for the House-GAN graph-conditioned generator.

    Args:
        dataset_name: Dataset identifier for the vectorized floorplan assets.
        target_set: House-GAN split target set, one of ``A`` through ``E``.
        checkpoint_step: Original checkpoint training step.
        id2label: Public zero-based room label map.
        relation_id2label: Signed relation label map.
        latent_dim: Per-room latent vector dimension.
        node_feature_dim: One-hot room feature dimension.
        graph_edge_values: Supported signed edge values.
        mask_size: Generated square room-mask size.
        canvas_size: Original floorplan canvas size as ``(width, height)``.
        cmp_channels: CMP feature channel count.
        num_cmp_layers: Number of CMP layers in the generator.
        postprocess_threshold: Threshold used by mask-to-box postprocessing.
        bbox_source: Source of public boxes.
        source_checkpoint: Original checkpoint path or name.
        conversion_report: Measured conversion metadata.
        license_note: Upstream license and research-purpose warning.

    Examples:
        >>> config = HouseGanConfig()
        >>> config.id2label[0]
        'living_room'
    """

    model_type = "housegan"

    def __init__(
        self,
        *,
        dataset_name: str = "housegan_floorplan_vectorized",
        target_set: str = "D",
        checkpoint_step: int = 200000,
        id2label: Id2LabelMapping | None = None,
        relation_id2label: Id2LabelMapping | None = None,
        latent_dim: int = 128,
        node_feature_dim: int = 10,
        graph_edge_values: tuple[int, int] = (-1, 1),
        mask_size: int = 32,
        canvas_size: tuple[int, int] = (256, 256),
        cmp_channels: int = 16,
        num_cmp_layers: int = 2,
        postprocess_threshold: float = 0.0,
        bbox_source: str = "generated_mask",
        source_checkpoint: str | None = None,
        conversion_report: HouseGanConversionReport | None = None,
        license_note: str = "GPL-3.0 with upstream research-purpose notice",
        **kwargs: str | int | float | bool | None,
    ) -> None:
        """Initialize a House-GAN config."""
        kwargs.pop("label2id", None)
        kwargs.pop("num_labels", None)
        kwargs.pop("max_supported_room_type_id", None)
        self.dataset_name = dataset_name
        self.target_set = target_set
        self.checkpoint_step = checkpoint_step
        self.id2label = {
            int(key): value for key, value in (id2label or DEFAULT_ID2LABEL).items()
        }
        self.label2id = {value: key for key, value in self.id2label.items()}
        self.relation_id2label = {
            int(key): value
            for key, value in (relation_id2label or DEFAULT_RELATION_ID2LABEL).items()
        }

        self.latent_dim = latent_dim
        self.node_feature_dim = node_feature_dim
        self.graph_edge_values = tuple(graph_edge_values)
        self.mask_size = mask_size
        self.canvas_size = tuple(canvas_size)

        self.cmp_channels = cmp_channels
        self.num_cmp_layers = num_cmp_layers
        self.postprocess_threshold = postprocess_threshold
        self.bbox_source = bbox_source
        self.source_checkpoint = source_checkpoint

        self.conversion_report = conversion_report or {}
        self.license_note = license_note
        self.num_labels = len(self.id2label)
        self.max_supported_room_type_id = self.num_labels - 1

        super().__init__(id2label=self.id2label, label2id=self.label2id)
        for key, value in kwargs.items():
            setattr(self, key, value)

__init__

__init__(
    *,
    dataset_name: str = "housegan_floorplan_vectorized",
    target_set: str = "D",
    checkpoint_step: int = 200000,
    id2label: Id2LabelMapping | None = None,
    relation_id2label: Id2LabelMapping | None = None,
    latent_dim: int = 128,
    node_feature_dim: int = 10,
    graph_edge_values: tuple[int, int] = (-1, 1),
    mask_size: int = 32,
    canvas_size: tuple[int, int] = (256, 256),
    cmp_channels: int = 16,
    num_cmp_layers: int = 2,
    postprocess_threshold: float = 0.0,
    bbox_source: str = "generated_mask",
    source_checkpoint: str | None = None,
    conversion_report: HouseGanConversionReport
    | None = None,
    license_note: str = "GPL-3.0 with upstream research-purpose notice",
    **kwargs: str | int | float | bool | None,
) -> None

Initialize a House-GAN config.

Source code in models/housegan/src/housegan/configuration_housegan.py
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
def __init__(
    self,
    *,
    dataset_name: str = "housegan_floorplan_vectorized",
    target_set: str = "D",
    checkpoint_step: int = 200000,
    id2label: Id2LabelMapping | None = None,
    relation_id2label: Id2LabelMapping | None = None,
    latent_dim: int = 128,
    node_feature_dim: int = 10,
    graph_edge_values: tuple[int, int] = (-1, 1),
    mask_size: int = 32,
    canvas_size: tuple[int, int] = (256, 256),
    cmp_channels: int = 16,
    num_cmp_layers: int = 2,
    postprocess_threshold: float = 0.0,
    bbox_source: str = "generated_mask",
    source_checkpoint: str | None = None,
    conversion_report: HouseGanConversionReport | None = None,
    license_note: str = "GPL-3.0 with upstream research-purpose notice",
    **kwargs: str | int | float | bool | None,
) -> None:
    """Initialize a House-GAN config."""
    kwargs.pop("label2id", None)
    kwargs.pop("num_labels", None)
    kwargs.pop("max_supported_room_type_id", None)
    self.dataset_name = dataset_name
    self.target_set = target_set
    self.checkpoint_step = checkpoint_step
    self.id2label = {
        int(key): value for key, value in (id2label or DEFAULT_ID2LABEL).items()
    }
    self.label2id = {value: key for key, value in self.id2label.items()}
    self.relation_id2label = {
        int(key): value
        for key, value in (relation_id2label or DEFAULT_RELATION_ID2LABEL).items()
    }

    self.latent_dim = latent_dim
    self.node_feature_dim = node_feature_dim
    self.graph_edge_values = tuple(graph_edge_values)
    self.mask_size = mask_size
    self.canvas_size = tuple(canvas_size)

    self.cmp_channels = cmp_channels
    self.num_cmp_layers = num_cmp_layers
    self.postprocess_threshold = postprocess_threshold
    self.bbox_source = bbox_source
    self.source_checkpoint = source_checkpoint

    self.conversion_report = conversion_report or {}
    self.license_note = license_note
    self.num_labels = len(self.id2label)
    self.max_supported_room_type_id = self.num_labels - 1

    super().__init__(id2label=self.id2label, label2id=self.label2id)
    for key, value in kwargs.items():
        setattr(self, key, value)

conversion

Checkpoint conversion utilities for House-GAN.

sha256_file

sha256_file(path: str | Path) -> str

Compute a SHA256 digest for a file.

Source code in models/housegan/src/housegan/conversion.py
18
19
20
21
22
23
24
def sha256_file(path: str | Path) -> str:
    """Compute a SHA256 digest for a file."""
    digest = hashlib.sha256()
    with Path(path).open("rb") as handle:
        for chunk in iter(lambda: handle.read(1024 * 1024), b""):
            digest.update(chunk)
    return digest.hexdigest()

convert_original_checkpoint

convert_original_checkpoint(
    *,
    checkpoint: str | Path,
    output_dir: str | Path,
    target_set: str = "D",
    checkpoint_step: int = 200000,
) -> HouseGanConversionReport

Convert a raw House-GAN generator state dict into HF files.

Source code in models/housegan/src/housegan/conversion.py
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
def convert_original_checkpoint(
    *,
    checkpoint: str | Path,
    output_dir: str | Path,
    target_set: str = "D",
    checkpoint_step: int = 200000,
) -> HouseGanConversionReport:
    """Convert a raw House-GAN generator state dict into HF files."""
    checkpoint_path = Path(checkpoint)
    raw_state = torch.load(checkpoint_path, map_location="cpu")
    converted, report = convert_state_dict(raw_state)
    config = HouseGanConfig(
        target_set=target_set,
        checkpoint_step=checkpoint_step,
        source_checkpoint=checkpoint_path.name,
        conversion_report={
            **report.to_dict(),
            "source_sha256": sha256_file(checkpoint_path),
        },
    )
    model = HouseGanGenerator(config)
    load_result = model.load_state_dict(converted, strict=True)
    measured = {
        **config.conversion_report,
        "missing_keys": list(load_result.missing_keys),
        "unexpected_keys": list(load_result.unexpected_keys),
    }
    model.config.conversion_report = measured
    output_path = Path(output_dir)
    processor = HouseGanProcessor(config=config)
    HouseGanPipeline(model=model, processor=processor, config=config).save_pretrained(
        output_path
    )
    (output_path / "conversion_report.json").write_text(
        json.dumps(measured, indent=2, sort_keys=True),
        encoding="utf-8",
    )
    return measured

datasets

Local dataset adapters for House-GAN vectorized floorplan assets.

load_housegan_numpy

load_housegan_numpy(
    path: str | Path,
) -> Shaped[np.ndarray, "..."]

Load a local House-GAN .npy asset without downloading data.

Source code in models/housegan/src/housegan/datasets.py
28
29
30
def load_housegan_numpy(path: str | Path) -> Shaped[np.ndarray, "..."]:
    """Load a local House-GAN ``.npy`` asset without downloading data."""
    return np.load(Path(path), allow_pickle=True)

normalize_graph_row

normalize_graph_row(
    row: tuple[Sequence[int], Sequence[Sequence[float]]],
    *,
    canvas_size: tuple[int, int] = (256, 256),
) -> HouseGanSceneGraph

Convert one [room_types, ltrb_boxes] row to a scene graph.

Source code in models/housegan/src/housegan/datasets.py
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
def normalize_graph_row(
    row: tuple[Sequence[int], Sequence[Sequence[float]]],
    *,
    canvas_size: tuple[int, int] = (256, 256),
) -> HouseGanSceneGraph:
    """Convert one ``[room_types, ltrb_boxes]`` row to a scene graph."""
    del canvas_size
    room_types, room_bbs = row
    bboxes = np.asarray(room_bbs, dtype=np.float32) / 256.0
    labels = [int(value) - 1 for value in room_types]
    nodes = tuple(
        HouseGanRoomNode(
            id=index,
            label=label,
            bbox=cast(tuple[float, float, float, float], tuple(map(float, bbox))),
        )
        for index, (label, bbox) in enumerate(zip(labels, bboxes, strict=True))
    )
    return HouseGanSceneGraph(
        nodes=nodes, relations=relation_from_bboxes(bboxes.tolist())
    )

split_target_set

split_target_set(
    graphs: list[HouseGanSceneGraph],
    *,
    target_set: Literal["A", "B", "C", "D", "E"],
    split: Literal["train", "eval"],
) -> list[HouseGanSceneGraph]

Apply House-GAN target-set graph-size splits.

Source code in models/housegan/src/housegan/datasets.py
56
57
58
59
60
61
62
63
64
65
66
67
68
69
def split_target_set(
    graphs: list[HouseGanSceneGraph],
    *,
    target_set: Literal["A", "B", "C", "D", "E"],
    split: Literal["train", "eval"],
) -> list[HouseGanSceneGraph]:
    """Apply House-GAN target-set graph-size splits."""
    low, high = TARGET_SETS[target_set]
    rows: list[HouseGanSceneGraph] = []
    for graph in graphs:
        in_range = low <= len(graph.nodes) <= high
        if (split == "eval" and in_range) or (split == "train" and not in_range):
            rows.append(graph)
    return rows

build_edges_from_bboxes

build_edges_from_bboxes(
    bbox_ltrb: Float[ndarray, "elements 4"],
    *,
    threshold: float = 0.03,
) -> list[HouseGanRelation]

Build public adjacency relations from normalized ltrb boxes.

Source code in models/housegan/src/housegan/datasets.py
72
73
74
75
76
77
78
def build_edges_from_bboxes(
    bbox_ltrb: Float[np.ndarray, "elements 4"],
    *,
    threshold: float = 0.03,
) -> list[HouseGanRelation]:
    """Build public adjacency relations from normalized ``ltrb`` boxes."""
    return list(relation_from_bboxes(bbox_ltrb.tolist(), threshold=threshold))

graph_schema

Scene-graph schema normalization for House-GAN.

HouseGanRoomNode dataclass

Room node in a House-GAN scene graph.

Source code in models/housegan/src/housegan/graph_schema.py
14
15
16
17
18
19
20
21
@dataclass(frozen=True)
class HouseGanRoomNode:
    """Room node in a House-GAN scene graph."""

    id: int
    label: int | str
    bbox: tuple[float, float, float, float] | None = None
    attributes: Mapping[str, object] | None = None

HouseGanRelation dataclass

Adjacency relation between two room nodes.

Source code in models/housegan/src/housegan/graph_schema.py
24
25
26
27
28
29
30
31
@dataclass(frozen=True)
class HouseGanRelation:
    """Adjacency relation between two room nodes."""

    source: int
    target: int
    adjacent: bool
    weight: float | None = None

HouseGanSceneGraph dataclass

Flat room relation graph used by House-GAN.

Source code in models/housegan/src/housegan/graph_schema.py
34
35
36
37
38
39
@dataclass(frozen=True)
class HouseGanSceneGraph:
    """Flat room relation graph used by House-GAN."""

    nodes: tuple[HouseGanRoomNode, ...]
    relations: tuple[HouseGanRelation, ...] | None = None

normalize_scene_graph

normalize_scene_graph(
    scene_graph: HouseGanSceneGraph
    | HouseGanSceneGraphPayload
    | None,
    *,
    labels: Int[Tensor, "..."]
    | Int[ndarray, "..."]
    | Sequence[int | str]
    | Sequence[Sequence[int | str]]
    | None,
    relations: Sequence[HouseGanRelationPayload] | None,
    id2label: Mapping[int, str],
) -> HouseGanSceneGraph

Normalize public scene-graph payloads.

Parameters:

Name Type Description Default
scene_graph HouseGanSceneGraph | HouseGanSceneGraphPayload | None

Dataclass or mapping with nodes and edges fields.

required
labels Int[Tensor, '...'] | Int[ndarray, '...'] | Sequence[int | str] | Sequence[Sequence[int | str]] | None

Optional labels used when no full scene graph is supplied.

required
relations Sequence[HouseGanRelationPayload] | None

Optional relation payload.

required
id2label Mapping[int, str]

Public label map.

required

Returns:

Type Description
HouseGanSceneGraph

Normalized scene graph preserving node order.

Raises:

Type Description
ValueError

If no nodes can be resolved or labels are invalid.

Examples:

>>> graph = normalize_scene_graph(None, labels=[0, 1], relations=[], id2label={0: "a", 1: "b"})
>>> len(graph.nodes)
2
Source code in models/housegan/src/housegan/graph_schema.py
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
def normalize_scene_graph(
    scene_graph: HouseGanSceneGraph | HouseGanSceneGraphPayload | None,
    *,
    labels: Int[torch.Tensor, "..."]
    | Int[np.ndarray, "..."]
    | Sequence[int | str]
    | Sequence[Sequence[int | str]]
    | None,
    relations: Sequence[HouseGanRelationPayload] | None,
    id2label: Mapping[int, str],
) -> HouseGanSceneGraph:
    """Normalize public scene-graph payloads.

    Args:
        scene_graph: Dataclass or mapping with ``nodes`` and ``edges`` fields.
        labels: Optional labels used when no full scene graph is supplied.
        relations: Optional relation payload.
        id2label: Public label map.

    Returns:
        Normalized scene graph preserving node order.

    Raises:
        ValueError: If no nodes can be resolved or labels are invalid.

    Examples:
        >>> graph = normalize_scene_graph(None, labels=[0, 1], relations=[], id2label={0: "a", 1: "b"})
        >>> len(graph.nodes)
        2
    """
    if isinstance(scene_graph, HouseGanSceneGraph):
        return scene_graph
    if scene_graph is None:
        if labels is None:
            raise ValueError("scene_graph or labels must be provided")

        label_values = _to_label_sequence(labels)
        nodes = tuple(
            HouseGanRoomNode(id=index, label=label)
            for index, label in enumerate(label_values)
        )
        return HouseGanSceneGraph(
            nodes=nodes,
            relations=_normalize_relations(relations),
        )
    nodes_payload = cast(
        Sequence[HouseGanNodePayload],
        scene_graph.get("nodes", scene_graph.get("rooms", ())),
    )
    nodes: list[HouseGanRoomNode] = []
    for index, raw_node in enumerate(nodes_payload):
        item = raw_node
        raw_id = item.get("id", index)
        raw_label = item.get("label_id", item.get("label"))
        if raw_label is None:
            raise ValueError("Each House-GAN node requires a label")

        raw_bbox = item.get("bbox")
        bbox = tuple(cast(Sequence[float], raw_bbox)) if raw_bbox is not None else None
        nodes.append(
            HouseGanRoomNode(
                id=int(cast(int | str, raw_id)),
                label=cast(int | str, raw_label),
                bbox=cast(tuple[float, float, float, float] | None, bbox),
                attributes=cast(
                    Mapping[str, HouseGanScalar] | None, item.get("attributes")
                ),
            )
        )
    edges = scene_graph.get("edges", scene_graph.get("relations", relations))
    if not nodes:
        raise ValueError("House-GAN relation graphs require at least one node")

    _ = id2label
    return HouseGanSceneGraph(
        nodes=tuple(nodes),
        relations=_normalize_relations(
            cast(Sequence[HouseGanRelationPayload] | None, edges)
        ),
    )

complete_signed_edges

complete_signed_edges(
    nodes: Sequence[HouseGanRoomNode],
    relations: Sequence[HouseGanRelation] | None,
    *,
    default_adjacent: bool = False,
    device: device | None = None,
) -> Int[torch.Tensor, "edges 3"]

Build signed complete graph triples.

Parameters:

Name Type Description Default
nodes Sequence[HouseGanRoomNode]

Room nodes in preserved graph order.

required
relations Sequence[HouseGanRelation] | None

Sparse public relations.

required
default_adjacent bool

Whether missing pairs become adjacent.

False
device device | None

Optional tensor device.

None

Returns:

Type Description
Int[Tensor, 'edges 3']

LongTensor with rows [source_index, sign, target_index].

Source code in models/housegan/src/housegan/graph_schema.py
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
def complete_signed_edges(
    nodes: Sequence[HouseGanRoomNode],
    relations: Sequence[HouseGanRelation] | None,
    *,
    default_adjacent: bool = False,
    device: torch.device | None = None,
) -> Int[torch.Tensor, "edges 3"]:
    """Build signed complete graph triples.

    Args:
        nodes: Room nodes in preserved graph order.
        relations: Sparse public relations.
        default_adjacent: Whether missing pairs become adjacent.
        device: Optional tensor device.

    Returns:
        ``LongTensor`` with rows ``[source_index, sign, target_index]``.
    """
    node_index = {node.id: index for index, node in enumerate(nodes)}
    relation_map: dict[tuple[int, int], bool] = {}
    for relation in relations or ():
        left = node_index[relation.source]
        right = node_index[relation.target]
        key = (left, right) if left < right else (right, left)
        relation_map[key] = relation.adjacent
    edges: list[list[int]] = []
    for left in range(len(nodes)):
        for right in range(left + 1, len(nodes)):
            adjacent = relation_map.get((left, right), default_adjacent)
            edges.append([left, 1 if adjacent else -1, right])
    return cast(torch.LongTensor, torch.tensor(edges, dtype=torch.long, device=device))

graph_to_node_features

graph_to_node_features(
    nodes: Sequence[HouseGanRoomNode],
    *,
    label2id: Mapping[str, int],
    num_labels: int,
    device: device | None = None,
) -> Float[torch.Tensor, "elements room_labels"]

Convert public room labels to 10-way one-hot features.

Source code in models/housegan/src/housegan/graph_schema.py
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
def graph_to_node_features(
    nodes: Sequence[HouseGanRoomNode],
    *,
    label2id: Mapping[str, int],
    num_labels: int,
    device: torch.device | None = None,
) -> Float[torch.Tensor, "elements room_labels"]:
    """Convert public room labels to 10-way one-hot features."""
    label_ids = [_label_to_id(node.label, label2id=label2id) for node in nodes]
    labels_t = torch.tensor(label_ids, dtype=torch.long, device=device)
    if labels_t.numel() and (
        int(labels_t.min().item()) < 0 or int(labels_t.max().item()) >= num_labels
    ):
        raise ValueError("House-GAN labels must be dataset-local ids in range")

    return cast(
        torch.FloatTensor,
        torch.nn.functional.one_hot(labels_t, num_classes=num_labels).to(
            dtype=torch.float32
        ),
    )

relation_from_bboxes

relation_from_bboxes(
    bbox_ltrb: Sequence[Sequence[float]],
    *,
    threshold: float = 0.03,
) -> tuple[HouseGanRelation, ...]

Derive House-GAN adjacency relations from normalized ltrb boxes.

Source code in models/housegan/src/housegan/graph_schema.py
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
def relation_from_bboxes(
    bbox_ltrb: Sequence[Sequence[float]],
    *,
    threshold: float = 0.03,
) -> tuple[HouseGanRelation, ...]:
    """Derive House-GAN adjacency relations from normalized ``ltrb`` boxes."""
    relations: list[HouseGanRelation] = []
    for left in range(len(bbox_ltrb)):
        for right in range(left + 1, len(bbox_ltrb)):
            relations.append(
                HouseGanRelation(
                    source=left,
                    target=right,
                    adjacent=_is_adjacent(bbox_ltrb[left], bbox_ltrb[right], threshold),
                )
            )
    return tuple(relations)

modeling_housegan

PyTorch House-GAN generator in Transformers PreTrainedModel form.

HouseGanModelOutput dataclass

Bases: ModelOutput

Raw House-GAN model output.

Source code in models/housegan/src/housegan/modeling_housegan.py
16
17
18
19
20
21
22
@dataclass
class HouseGanModelOutput(ModelOutput):
    """Raw House-GAN model output."""

    masks: Float[torch.Tensor, "elements height width"]
    node_features: Float[torch.Tensor, "elements room_labels"] | None = None
    edges: Int[torch.Tensor, "edges 3"] | None = None

CMP

Bases: Module

House-GAN convolutional message passing block.

Source code in models/housegan/src/housegan/modeling_housegan.py
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
class CMP(nn.Module):
    """House-GAN convolutional message passing block."""

    def __init__(self, in_channels: int) -> None:
        """Initialize the CMP block."""
        super().__init__()
        self.encoder = nn.Sequential(
            *_conv_block(3 * in_channels, 2 * in_channels, 3, 1, 1, act="leaky"),
            *_conv_block(2 * in_channels, 2 * in_channels, 3, 1, 1, act="leaky"),
            *_conv_block(2 * in_channels, in_channels, 3, 1, 1, act="leaky"),
        )

    def forward(
        self,
        feats: Float[torch.Tensor, "elements channels height width"],
        edges: Int[torch.Tensor, "edges 3"],
    ) -> Float[torch.Tensor, "elements channels height width"]:
        """Pool positive and negative edge-neighbor features."""
        edges = edges.view(-1, 3)
        elements = feats.size(0)
        pooled_pos = torch.zeros_like(feats)
        pooled_neg = torch.zeros_like(feats)
        if edges.numel() > 0:
            pos_edges = edges[edges[:, 1] > 0]
            neg_edges = edges[edges[:, 1] < 0]
            pooled_pos = _pool_edges(feats, pos_edges, elements)
            pooled_neg = _pool_edges(feats, neg_edges, elements)
        return self.encoder(torch.cat([feats, pooled_pos, pooled_neg], dim=1))

__init__

__init__(in_channels: int) -> None

Initialize the CMP block.

Source code in models/housegan/src/housegan/modeling_housegan.py
67
68
69
70
71
72
73
74
def __init__(self, in_channels: int) -> None:
    """Initialize the CMP block."""
    super().__init__()
    self.encoder = nn.Sequential(
        *_conv_block(3 * in_channels, 2 * in_channels, 3, 1, 1, act="leaky"),
        *_conv_block(2 * in_channels, 2 * in_channels, 3, 1, 1, act="leaky"),
        *_conv_block(2 * in_channels, in_channels, 3, 1, 1, act="leaky"),
    )

forward

forward(
    feats: Float[Tensor, "elements channels height width"],
    edges: Int[Tensor, "edges 3"],
) -> Float[torch.Tensor, "elements channels height width"]

Pool positive and negative edge-neighbor features.

Source code in models/housegan/src/housegan/modeling_housegan.py
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
def forward(
    self,
    feats: Float[torch.Tensor, "elements channels height width"],
    edges: Int[torch.Tensor, "edges 3"],
) -> Float[torch.Tensor, "elements channels height width"]:
    """Pool positive and negative edge-neighbor features."""
    edges = edges.view(-1, 3)
    elements = feats.size(0)
    pooled_pos = torch.zeros_like(feats)
    pooled_neg = torch.zeros_like(feats)
    if edges.numel() > 0:
        pos_edges = edges[edges[:, 1] > 0]
        neg_edges = edges[edges[:, 1] < 0]
        pooled_pos = _pool_edges(feats, pos_edges, elements)
        pooled_neg = _pool_edges(feats, neg_edges, elements)
    return self.encoder(torch.cat([feats, pooled_pos, pooled_neg], dim=1))

HouseGanGenerator

Bases: PreTrainedModel

Transformers-compatible House-GAN generator.

Parameters:

Name Type Description Default
config HouseGanConfig

House-GAN configuration.

required

Examples:

>>> model = HouseGanGenerator(HouseGanConfig())
>>> latents = torch.zeros(2, 128)
>>> nodes = torch.eye(10)[:2]
>>> edges = torch.tensor([[0, 1, 1]])
>>> tuple(model(latents, nodes, edges).masks.shape)
(2, 32, 32)
Source code in models/housegan/src/housegan/modeling_housegan.py
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
class HouseGanGenerator(PreTrainedModel):
    """Transformers-compatible House-GAN generator.

    Args:
        config: House-GAN configuration.

    Examples:
        >>> model = HouseGanGenerator(HouseGanConfig())
        >>> latents = torch.zeros(2, 128)
        >>> nodes = torch.eye(10)[:2]
        >>> edges = torch.tensor([[0, 1, 1]])
        >>> tuple(model(latents, nodes, edges).masks.shape)
        (2, 32, 32)
    """

    config_class = HouseGanConfig
    base_model_prefix = "housegan"
    main_input_name = "latents"
    supports_gradient_checkpointing = False

    def __init__(self, config: HouseGanConfig) -> None:
        """Initialize generator layers."""
        super().__init__(config)
        init_size = config.mask_size // 4
        in_features = config.latent_dim + config.node_feature_dim
        channels = config.cmp_channels
        self.init_size = init_size
        self.l1 = nn.Sequential(nn.Linear(in_features, channels * init_size**2))
        self.upsample_1 = nn.Sequential(
            *_conv_block(channels, channels, 4, 2, 1, act="leaky", upsample=True)
        )
        self.upsample_2 = nn.Sequential(
            *_conv_block(channels, channels, 4, 2, 1, act="leaky", upsample=True)
        )
        self.cmp_1 = CMP(channels)
        self.cmp_2 = CMP(channels)
        self.decoder = nn.Sequential(
            *_conv_block(channels, 256, 3, 1, 1, act="leaky"),
            *_conv_block(256, 128, 3, 1, 1, act="leaky"),
            *_conv_block(128, 1, 3, 1, 1, act="tanh"),
        )
        self.post_init()

    def forward(
        self,
        latents: Float[torch.Tensor, "elements latent"],
        node_features: Float[torch.Tensor, "elements room_labels"],
        edges: Int[torch.Tensor, "edges 3"],
        return_dict: bool | None = None,
    ) -> HouseGanModelOutput | tuple[Float[torch.Tensor, "elements height width"]]:
        """Run a House-GAN forward pass.

        Args:
            latents: Per-room latent vectors.
            node_features: Per-room one-hot room features.
            edges: Signed complete graph triples.
            return_dict: Whether to return ``HouseGanModelOutput``.

        Returns:
            Raw generated room masks.
        """
        if latents.ndim != 2 or latents.shape[-1] != self.config.latent_dim:
            raise ValueError("latents must have shape (elements, latent_dim)")

        if node_features.shape != (latents.shape[0], self.config.node_feature_dim):
            raise ValueError(
                "node_features must have shape (elements, node_feature_dim)"
            )

        if edges.ndim != 2 or edges.shape[-1] != 3:
            raise ValueError("edges must have shape (edges, 3)")

        dtype = next(self.parameters()).dtype
        latents = latents.to(dtype=dtype, device=self.device)
        node_features = node_features.to(dtype=dtype, device=self.device)
        edges = edges.to(dtype=torch.long, device=self.device)
        hidden = torch.cat(
            [latents.view(-1, self.config.latent_dim), node_features], dim=1
        )
        hidden = self.l1(hidden)
        hidden = hidden.view(
            -1, self.config.cmp_channels, self.init_size, self.init_size
        )
        hidden = self.cmp_1(hidden, edges).view(-1, *hidden.shape[1:])
        hidden = self.upsample_1(hidden)
        hidden = self.cmp_2(hidden, edges).view(-1, *hidden.shape[1:])
        hidden = self.upsample_2(hidden)
        masks = self.decoder(hidden.view(-1, hidden.shape[1], *hidden.shape[2:]))
        masks = masks.view(-1, *masks.shape[2:])
        if return_dict is False:
            return (masks,)
        return HouseGanModelOutput(
            masks=masks, node_features=node_features, edges=edges
        )

__init__

__init__(config: HouseGanConfig) -> None

Initialize generator layers.

Source code in models/housegan/src/housegan/modeling_housegan.py
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
def __init__(self, config: HouseGanConfig) -> None:
    """Initialize generator layers."""
    super().__init__(config)
    init_size = config.mask_size // 4
    in_features = config.latent_dim + config.node_feature_dim
    channels = config.cmp_channels
    self.init_size = init_size
    self.l1 = nn.Sequential(nn.Linear(in_features, channels * init_size**2))
    self.upsample_1 = nn.Sequential(
        *_conv_block(channels, channels, 4, 2, 1, act="leaky", upsample=True)
    )
    self.upsample_2 = nn.Sequential(
        *_conv_block(channels, channels, 4, 2, 1, act="leaky", upsample=True)
    )
    self.cmp_1 = CMP(channels)
    self.cmp_2 = CMP(channels)
    self.decoder = nn.Sequential(
        *_conv_block(channels, 256, 3, 1, 1, act="leaky"),
        *_conv_block(256, 128, 3, 1, 1, act="leaky"),
        *_conv_block(128, 1, 3, 1, 1, act="tanh"),
    )
    self.post_init()

forward

forward(
    latents: Float[Tensor, "elements latent"],
    node_features: Float[Tensor, "elements room_labels"],
    edges: Int[Tensor, "edges 3"],
    return_dict: bool | None = None,
) -> (
    HouseGanModelOutput
    | tuple[Float[torch.Tensor, "elements height width"]]
)

Run a House-GAN forward pass.

Parameters:

Name Type Description Default
latents Float[Tensor, 'elements latent']

Per-room latent vectors.

required
node_features Float[Tensor, 'elements room_labels']

Per-room one-hot room features.

required
edges Int[Tensor, 'edges 3']

Signed complete graph triples.

required
return_dict bool | None

Whether to return HouseGanModelOutput.

None

Returns:

Type Description
HouseGanModelOutput | tuple[Float[Tensor, 'elements height width']]

Raw generated room masks.

Source code in models/housegan/src/housegan/modeling_housegan.py
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
def forward(
    self,
    latents: Float[torch.Tensor, "elements latent"],
    node_features: Float[torch.Tensor, "elements room_labels"],
    edges: Int[torch.Tensor, "edges 3"],
    return_dict: bool | None = None,
) -> HouseGanModelOutput | tuple[Float[torch.Tensor, "elements height width"]]:
    """Run a House-GAN forward pass.

    Args:
        latents: Per-room latent vectors.
        node_features: Per-room one-hot room features.
        edges: Signed complete graph triples.
        return_dict: Whether to return ``HouseGanModelOutput``.

    Returns:
        Raw generated room masks.
    """
    if latents.ndim != 2 or latents.shape[-1] != self.config.latent_dim:
        raise ValueError("latents must have shape (elements, latent_dim)")

    if node_features.shape != (latents.shape[0], self.config.node_feature_dim):
        raise ValueError(
            "node_features must have shape (elements, node_feature_dim)"
        )

    if edges.ndim != 2 or edges.shape[-1] != 3:
        raise ValueError("edges must have shape (edges, 3)")

    dtype = next(self.parameters()).dtype
    latents = latents.to(dtype=dtype, device=self.device)
    node_features = node_features.to(dtype=dtype, device=self.device)
    edges = edges.to(dtype=torch.long, device=self.device)
    hidden = torch.cat(
        [latents.view(-1, self.config.latent_dim), node_features], dim=1
    )
    hidden = self.l1(hidden)
    hidden = hidden.view(
        -1, self.config.cmp_channels, self.init_size, self.init_size
    )
    hidden = self.cmp_1(hidden, edges).view(-1, *hidden.shape[1:])
    hidden = self.upsample_1(hidden)
    hidden = self.cmp_2(hidden, edges).view(-1, *hidden.shape[1:])
    hidden = self.upsample_2(hidden)
    masks = self.decoder(hidden.view(-1, hidden.shape[1], *hidden.shape[2:]))
    masks = masks.view(-1, *masks.shape[2:])
    if return_dict is False:
        return (masks,)
    return HouseGanModelOutput(
        masks=masks, node_features=node_features, edges=edges
    )

pipeline_housegan

Pipeline interface for House-GAN relation-conditioned generation.

HouseGanPipeline

Bases: LayoutGenerationPipeline

Transformers-side House-GAN layout generation pipeline.

Source code in models/housegan/src/housegan/pipeline_housegan.py
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
class HouseGanPipeline(LayoutGenerationPipeline):
    """Transformers-side House-GAN layout generation pipeline."""

    config_class: ClassVar[type[PretrainedConfig]] = HouseGanConfig
    component_specs: ClassVar[dict[str, PipelineComponentSpec]] = (
        model_processor_component_specs(
            model_loader=_load_model_component,
            processor_loader=_load_processor_component,
        )
    )

    config: HouseGanConfig
    model: HouseGanGenerator
    processor: HouseGanProcessor

    def __init__(
        self,
        model: HouseGanGenerator,
        processor: HouseGanProcessor | None = None,
        config: HouseGanConfig | None = None,
        device: int | torch.device | None = None,
    ) -> None:
        """Initialize the pipeline."""
        super().__init__(config or model.config)
        self.config = config or model.config
        self.model = model
        self.processor = processor or HouseGanProcessor(
            config=self.config,
        )
        if device is not None:
            resolved = (
                torch.device("cpu")
                if isinstance(device, int) and device < 0
                else torch.device(f"cuda:{device}")
                if isinstance(device, int)
                else device
            )
            self.to(resolved)

    @classmethod
    def _from_pretrained_components(
        cls,
        *,
        config: PretrainedConfig,
        components: Mapping[str, PipelineComponent | None],
    ) -> "HouseGanPipeline":
        """Build a pipeline from loaded model and processor components."""
        return cls(
            config=cast(HouseGanConfig, config),
            model=cast(HouseGanGenerator, components["model"]),
            processor=cast(HouseGanProcessor, components["processor"]),
        )

    @torch.no_grad()
    def __call__(  # ty: ignore[invalid-method-override]
        self,
        *,
        batch_size: int = 1,
        seed: int | None = None,
        generator: torch.Generator | None = None,
        condition_type: ConditionType | str = ConditionType.relation,
        labels: Int[torch.Tensor, "..."]
        | Int[np.ndarray, "..."]
        | NestedIntList
        | None = None,
        bbox: Float[torch.Tensor, "... 4"]
        | Float[np.ndarray, "... 4"]
        | NestedFloatList
        | None = None,
        mask: Bool[torch.Tensor, "..."]
        | Bool[np.ndarray, "..."]
        | NestedBoolList
        | None = None,
        num_elements: int | list[int] | Int[torch.Tensor, "..."] | None = None,
        box_format: BoxFormat | str = BoxFormat.xywh,
        normalized: bool = True,
        canvas_size: tuple[int, int] | None = None,
        num_inference_steps: int | None = None,
        scene_graph: HouseGanSceneGraph
        | HouseGanSceneGraphPayload
        | list[HouseGanSceneGraphPayload]
        | None = None,
        relations: Sequence[HouseGanRelationPayload] | None = None,
        latents: Float[torch.Tensor, "elements latent"] | None = None,
        output_type: OutputType = "dataclass",
        return_intermediates: bool = False,
    ) -> (
        LayoutGenerationOutput
        | dict[
            str,
            Shaped[torch.Tensor, "..."]
            | dict[int, str]
            | list[dict[str, str | int | float | bool | None]]
            | None,
        ]
    ):
        """Generate a floorplan layout from room relation constraints."""
        del num_elements, num_inference_steps
        if batch_size < 1:
            raise ValueError("batch_size must be positive")

        graph_batch = _expand_graph_batch(scene_graph, batch_size)
        outputs: list[LayoutGenerationOutput] = []
        torch_generator = self.prepare_generator(
            generator=generator,
            seed=seed,
            device=self.device or next(self.model.parameters()).device,
        )
        for graph_index, graph_item in enumerate(graph_batch):
            condition = self.processor(
                condition_type=condition_type,
                scene_graph=graph_item,
                relations=relations,
                labels=labels,
                bbox=bbox,
                mask=mask,
                box_format=box_format,
                normalized=normalized,
                canvas_size=canvas_size,
            )
            node_features = condition["node_features"].to(self.model.device)
            edges = condition["edges"].to(self.model.device)
            labels_t = condition["labels"].to(self.model.device)
            room_count = node_features.shape[0]
            graph_latents = latents
            if graph_latents is None:
                graph_latents = torch.randn(
                    room_count,
                    self.model.config.latent_dim,
                    generator=torch_generator,
                    device=self.model.device,
                    dtype=next(self.model.parameters()).dtype,
                )
            elif graph_latents.ndim == 3:
                graph_latents = graph_latents[graph_index]
            model_output = self.model(
                latents=graph_latents.to(self.model.device),
                node_features=node_features,
                edges=edges,
            )
            decoded = self.processor.post_process_masks(
                model_output.masks,
                labels=labels_t,
                edges=edges,
                node_features=node_features,
                scene_graph=condition["scene_graph"],
                output_type="dataclass",
                return_intermediates=return_intermediates,
            )
            outputs.append(cast(LayoutGenerationOutput, decoded))
        merged = _merge_outputs(outputs, output_type=output_type)
        return merged

__init__

__init__(
    model: HouseGanGenerator,
    processor: HouseGanProcessor | None = None,
    config: HouseGanConfig | None = None,
    device: int | device | None = None,
) -> None

Initialize the pipeline.

Source code in models/housegan/src/housegan/pipeline_housegan.py
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
def __init__(
    self,
    model: HouseGanGenerator,
    processor: HouseGanProcessor | None = None,
    config: HouseGanConfig | None = None,
    device: int | torch.device | None = None,
) -> None:
    """Initialize the pipeline."""
    super().__init__(config or model.config)
    self.config = config or model.config
    self.model = model
    self.processor = processor or HouseGanProcessor(
        config=self.config,
    )
    if device is not None:
        resolved = (
            torch.device("cpu")
            if isinstance(device, int) and device < 0
            else torch.device(f"cuda:{device}")
            if isinstance(device, int)
            else device
        )
        self.to(resolved)

__call__

__call__(
    *,
    batch_size: int = 1,
    seed: int | None = None,
    generator: Generator | None = None,
    condition_type: ConditionType
    | str = ConditionType.relation,
    labels: Int[Tensor, "..."]
    | Int[ndarray, "..."]
    | NestedIntList
    | None = None,
    bbox: Float[Tensor, "... 4"]
    | Float[ndarray, "... 4"]
    | NestedFloatList
    | None = None,
    mask: Bool[Tensor, "..."]
    | Bool[ndarray, "..."]
    | NestedBoolList
    | None = None,
    num_elements: int
    | list[int]
    | Int[Tensor, "..."]
    | None = None,
    box_format: BoxFormat | str = BoxFormat.xywh,
    normalized: bool = True,
    canvas_size: tuple[int, int] | None = None,
    num_inference_steps: int | None = None,
    scene_graph: HouseGanSceneGraph
    | HouseGanSceneGraphPayload
    | list[HouseGanSceneGraphPayload]
    | None = None,
    relations: Sequence[HouseGanRelationPayload]
    | None = None,
    latents: Float[Tensor, "elements latent"] | None = None,
    output_type: OutputType = "dataclass",
    return_intermediates: bool = False,
) -> (
    LayoutGenerationOutput
    | dict[
        str,
        Shaped[torch.Tensor, "..."]
        | dict[int, str]
        | list[dict[str, str | int | float | bool | None]]
        | None,
    ]
)

Generate a floorplan layout from room relation constraints.

Source code in models/housegan/src/housegan/pipeline_housegan.py
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
@torch.no_grad()
def __call__(  # ty: ignore[invalid-method-override]
    self,
    *,
    batch_size: int = 1,
    seed: int | None = None,
    generator: torch.Generator | None = None,
    condition_type: ConditionType | str = ConditionType.relation,
    labels: Int[torch.Tensor, "..."]
    | Int[np.ndarray, "..."]
    | NestedIntList
    | None = None,
    bbox: Float[torch.Tensor, "... 4"]
    | Float[np.ndarray, "... 4"]
    | NestedFloatList
    | None = None,
    mask: Bool[torch.Tensor, "..."]
    | Bool[np.ndarray, "..."]
    | NestedBoolList
    | None = None,
    num_elements: int | list[int] | Int[torch.Tensor, "..."] | None = None,
    box_format: BoxFormat | str = BoxFormat.xywh,
    normalized: bool = True,
    canvas_size: tuple[int, int] | None = None,
    num_inference_steps: int | None = None,
    scene_graph: HouseGanSceneGraph
    | HouseGanSceneGraphPayload
    | list[HouseGanSceneGraphPayload]
    | None = None,
    relations: Sequence[HouseGanRelationPayload] | None = None,
    latents: Float[torch.Tensor, "elements latent"] | None = None,
    output_type: OutputType = "dataclass",
    return_intermediates: bool = False,
) -> (
    LayoutGenerationOutput
    | dict[
        str,
        Shaped[torch.Tensor, "..."]
        | dict[int, str]
        | list[dict[str, str | int | float | bool | None]]
        | None,
    ]
):
    """Generate a floorplan layout from room relation constraints."""
    del num_elements, num_inference_steps
    if batch_size < 1:
        raise ValueError("batch_size must be positive")

    graph_batch = _expand_graph_batch(scene_graph, batch_size)
    outputs: list[LayoutGenerationOutput] = []
    torch_generator = self.prepare_generator(
        generator=generator,
        seed=seed,
        device=self.device or next(self.model.parameters()).device,
    )
    for graph_index, graph_item in enumerate(graph_batch):
        condition = self.processor(
            condition_type=condition_type,
            scene_graph=graph_item,
            relations=relations,
            labels=labels,
            bbox=bbox,
            mask=mask,
            box_format=box_format,
            normalized=normalized,
            canvas_size=canvas_size,
        )
        node_features = condition["node_features"].to(self.model.device)
        edges = condition["edges"].to(self.model.device)
        labels_t = condition["labels"].to(self.model.device)
        room_count = node_features.shape[0]
        graph_latents = latents
        if graph_latents is None:
            graph_latents = torch.randn(
                room_count,
                self.model.config.latent_dim,
                generator=torch_generator,
                device=self.model.device,
                dtype=next(self.model.parameters()).dtype,
            )
        elif graph_latents.ndim == 3:
            graph_latents = graph_latents[graph_index]
        model_output = self.model(
            latents=graph_latents.to(self.model.device),
            node_features=node_features,
            edges=edges,
        )
        decoded = self.processor.post_process_masks(
            model_output.masks,
            labels=labels_t,
            edges=edges,
            node_features=node_features,
            scene_graph=condition["scene_graph"],
            output_type="dataclass",
            return_intermediates=return_intermediates,
        )
        outputs.append(cast(LayoutGenerationOutput, decoded))
    merged = _merge_outputs(outputs, output_type=output_type)
    return merged

processing_housegan

Processor for House-GAN relation graphs and mask decoding.

HouseGanProcessor

Bases: ProcessorMixin

Normalize House-GAN scene graphs and decode generated masks.

Source code in models/housegan/src/housegan/processing_housegan.py
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
class HouseGanProcessor(ProcessorMixin):
    """Normalize House-GAN scene graphs and decode generated masks."""

    attributes: list[str] = []
    config_name = "processor_config.json"

    def __init__(
        self,
        *,
        config: HouseGanConfig,
        default_missing_relation: Literal["not_adjacent", "error"] = "not_adjacent",
    ) -> None:
        """Initialize processor metadata."""
        self.config = config
        self.id2label = {
            int(key): value
            for key, value in cast(Id2LabelMapping, self.config.id2label).items()
        }
        self.label2id = {value: key for key, value in self.id2label.items()}
        self.relation_id2label = {
            int(key): value
            for key, value in cast(
                Id2LabelMapping, self.config.relation_id2label
            ).items()
        }
        self.canvas_size = tuple(self.config.canvas_size)
        self.mask_size = self.config.mask_size
        self.default_missing_relation = default_missing_relation
        self.chat_template = None

    def save_pretrained(
        self,
        save_directory: str | Path,
        push_to_hub: bool = False,
        **kwargs: str | int | float | bool | None,
    ) -> None:
        """Save processor metadata."""
        del push_to_hub, kwargs
        root = Path(save_directory)
        root.mkdir(parents=True, exist_ok=True)
        payload = {
            "config": self.config.to_dict(),
            "processor_class": self.__class__.__name__,
            "id2label": self.id2label,
            "relation_id2label": self.relation_id2label,
            "canvas_size": self.canvas_size,
            "mask_size": self.mask_size,
            "default_missing_relation": self.default_missing_relation,
        }
        (root / self.config_name).write_text(
            json.dumps(payload, indent=2, sort_keys=True), encoding="utf-8"
        )

    @classmethod
    def from_pretrained(
        cls,
        pretrained_model_name_or_path: str | PathLike[str],
        cache_dir: str | PathLike[str] | None = None,
        force_download: bool = False,
        local_files_only: bool = False,
        token: str | bool | None = None,
        revision: str = "main",
        subfolder: str | None = None,
        **kwargs: str | int | float | bool | None,
    ) -> Self:
        """Load processor metadata from ``processor_config.json``."""
        del cache_dir, force_download, local_files_only, token, revision, kwargs
        root = Path(pretrained_model_name_or_path)
        if subfolder is not None:
            root = root / subfolder
        payload = json.loads((root / cls.config_name).read_text(encoding="utf-8"))
        config_payload = payload.get("config")
        if not isinstance(config_payload, dict):
            raise TypeError("processor config payload must be a dictionary")

        return cls(
            config=HouseGanConfig.from_dict(config_payload),
            default_missing_relation=payload.get(
                "default_missing_relation", "not_adjacent"
            ),
        )

    def __call__(
        self,
        *,
        condition_type: ConditionType | str = ConditionType.relation,
        scene_graph: HouseGanSceneGraph | HouseGanSceneGraphPayload | None = None,
        relations: Sequence[HouseGanRelationPayload] | None = None,
        labels: Int[torch.Tensor, "..."]
        | Int[np.ndarray, "..."]
        | Sequence[Sequence[int]]
        | Sequence[int]
        | None = None,
        bbox: Float[torch.Tensor, "... 4"]
        | Float[np.ndarray, "... 4"]
        | Sequence[Sequence[Sequence[float]]]
        | Sequence[Sequence[float]]
        | Sequence[ArrayLikeInput]
        | None = None,
        mask: Bool[torch.Tensor, "..."]
        | Bool[np.ndarray, "..."]
        | Sequence[Sequence[bool]]
        | Sequence[bool]
        | Sequence[ArrayLikeInput]
        | None = None,
        box_format: BoxFormat | str = BoxFormat.xywh,
        normalized: bool = True,
        canvas_size: tuple[int, int] | None = None,
        return_tensors: Literal["pt"] = "pt",
    ) -> BatchEncoding:
        """Encode public relation inputs into House-GAN tensors."""
        if return_tensors != "pt":
            raise ValueError("HouseGanProcessor only supports return_tensors='pt'")

        self.normalize_condition_type(condition_type)
        relation_payload = relations
        if relation_payload is None and bbox is not None and labels is not None:
            bbox_t, labels_t, _ = prepare_layout_tensors(
                bbox=cast(
                    Float[torch.Tensor, "... 4"]
                    | Float[np.ndarray, "... 4"]
                    | Sequence[Sequence[Sequence[float]]]
                    | Sequence[Sequence[float]]
                    | Sequence[ArrayLikeInput],
                    bbox,
                ),
                labels=cast(
                    Int[torch.Tensor, "..."]
                    | Int[np.ndarray, "..."]
                    | Sequence[Sequence[int]]
                    | Sequence[int]
                    | Sequence[ArrayLikeInput],
                    labels,
                ),
                mask=cast(
                    Bool[torch.Tensor, "..."]
                    | Bool[np.ndarray, "..."]
                    | Sequence[Sequence[bool]]
                    | Sequence[bool]
                    | Sequence[ArrayLikeInput]
                    | None,
                    mask,
                ),
                box_format=normalize_box_format(box_format),
                normalized=normalized,
                canvas_size=canvas_size or self.canvas_size,
                clamp_converted_normalized=True,
            )
            relation_payload = relation_from_bboxes(
                _xywh_to_ltrb_list(bbox_t[0]),
            )
            labels = labels_t[0]
        graph = normalize_scene_graph(
            scene_graph,
            labels=labels,
            relations=relation_payload,
            id2label=self.id2label,
        )
        if self.default_missing_relation == "error" and not graph.relations:
            raise ValueError(
                "House-GAN requires relations when missing-pair policy is 'error'"
            )

        node_features = graph_to_node_features(
            graph.nodes,
            label2id=self.label2id,
            num_labels=len(self.id2label),
        )
        edges = complete_signed_edges(
            graph.nodes,
            graph.relations,
            default_adjacent=False,
        )
        label_ids = torch.tensor(
            [
                self.label2id[node.label]
                if isinstance(node.label, str)
                else int(node.label)
                for node in graph.nodes
            ],
            dtype=torch.long,
        )
        return BatchEncoding(
            {
                "node_features": node_features,
                "edges": edges,
                "labels": label_ids,
                "scene_graph": graph,
            }
        )

    def normalize_condition_type(
        self, condition_type: ConditionType | str
    ) -> ConditionType:
        """Normalize and validate the House-GAN condition type."""
        condition = normalize_condition_type(condition_type)
        if condition is not ConditionType.relation:
            raise NotImplementedError(
                "House-GAN supports only condition_type='relation' and aliases "
                "'scene_graph', 'graph', or 'gen_r'."
            )

        return condition

    def post_process_masks(
        self,
        masks: Float[torch.Tensor, "elements height width"],
        *,
        labels: Int[torch.Tensor, "elements"],
        edges: Int[torch.Tensor, "edges 3"] | None = None,
        node_features: Float[torch.Tensor, "elements room_labels"] | None = None,
        scene_graph: HouseGanSceneGraph | None = None,
        output_type: OutputType = "dataclass",
        return_intermediates: bool = False,
    ) -> (
        LayoutGenerationOutput
        | dict[
            str,
            Shaped[torch.Tensor, "..."]
            | dict[int, str]
            | dict[str, Shaped[torch.Tensor, "..."] | HouseGanSceneGraph | None]
            | None,
        ]
    ):
        """Convert generated room masks to public normalized boxes."""
        bbox_ltrb = mask_to_ltrb(masks, threshold=0.0)
        bbox_xywh = (
            ltrb_to_xywh(bbox_ltrb / float(self.mask_size)).unsqueeze(0).clamp(0.0, 1.0)
        )
        labels_b = labels.to(dtype=torch.long).unsqueeze(0)
        valid = torch.ones(labels_b.shape, dtype=torch.bool, device=labels_b.device)
        intermediates = None
        if return_intermediates:
            intermediates = {
                "room_masks": masks.detach(),
                "bbox_ltrb_32": bbox_ltrb,
                "signed_edges": edges,
                "node_features": node_features,
                "scene_graph": scene_graph,
            }
        if output_type == "dict":
            return {
                "bbox": bbox_xywh,
                "labels": labels_b,
                "mask": valid,
                "id2label": self.id2label,
                "sequences": edges,
                "scores": None,
                "trajectory": None,
                "intermediates": intermediates,
            }
        return LayoutGenerationOutput(
            bbox=bbox_xywh,
            labels=labels_b,
            mask=valid,
            id2label=self.id2label,
            sequences=edges,
            intermediates=intermediates,
        )

__init__

__init__(
    *,
    config: HouseGanConfig,
    default_missing_relation: Literal[
        "not_adjacent", "error"
    ] = "not_adjacent",
) -> None

Initialize processor metadata.

Source code in models/housegan/src/housegan/processing_housegan.py
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
def __init__(
    self,
    *,
    config: HouseGanConfig,
    default_missing_relation: Literal["not_adjacent", "error"] = "not_adjacent",
) -> None:
    """Initialize processor metadata."""
    self.config = config
    self.id2label = {
        int(key): value
        for key, value in cast(Id2LabelMapping, self.config.id2label).items()
    }
    self.label2id = {value: key for key, value in self.id2label.items()}
    self.relation_id2label = {
        int(key): value
        for key, value in cast(
            Id2LabelMapping, self.config.relation_id2label
        ).items()
    }
    self.canvas_size = tuple(self.config.canvas_size)
    self.mask_size = self.config.mask_size
    self.default_missing_relation = default_missing_relation
    self.chat_template = None

save_pretrained

save_pretrained(
    save_directory: str | Path,
    push_to_hub: bool = False,
    **kwargs: str | int | float | bool | None,
) -> None

Save processor metadata.

Source code in models/housegan/src/housegan/processing_housegan.py
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
def save_pretrained(
    self,
    save_directory: str | Path,
    push_to_hub: bool = False,
    **kwargs: str | int | float | bool | None,
) -> None:
    """Save processor metadata."""
    del push_to_hub, kwargs
    root = Path(save_directory)
    root.mkdir(parents=True, exist_ok=True)
    payload = {
        "config": self.config.to_dict(),
        "processor_class": self.__class__.__name__,
        "id2label": self.id2label,
        "relation_id2label": self.relation_id2label,
        "canvas_size": self.canvas_size,
        "mask_size": self.mask_size,
        "default_missing_relation": self.default_missing_relation,
    }
    (root / self.config_name).write_text(
        json.dumps(payload, indent=2, sort_keys=True), encoding="utf-8"
    )

from_pretrained classmethod

from_pretrained(
    pretrained_model_name_or_path: str | PathLike[str],
    cache_dir: str | PathLike[str] | None = None,
    force_download: bool = False,
    local_files_only: bool = False,
    token: str | bool | None = None,
    revision: str = "main",
    subfolder: str | None = None,
    **kwargs: str | int | float | bool | None,
) -> Self

Load processor metadata from processor_config.json.

Source code in models/housegan/src/housegan/processing_housegan.py
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
@classmethod
def from_pretrained(
    cls,
    pretrained_model_name_or_path: str | PathLike[str],
    cache_dir: str | PathLike[str] | None = None,
    force_download: bool = False,
    local_files_only: bool = False,
    token: str | bool | None = None,
    revision: str = "main",
    subfolder: str | None = None,
    **kwargs: str | int | float | bool | None,
) -> Self:
    """Load processor metadata from ``processor_config.json``."""
    del cache_dir, force_download, local_files_only, token, revision, kwargs
    root = Path(pretrained_model_name_or_path)
    if subfolder is not None:
        root = root / subfolder
    payload = json.loads((root / cls.config_name).read_text(encoding="utf-8"))
    config_payload = payload.get("config")
    if not isinstance(config_payload, dict):
        raise TypeError("processor config payload must be a dictionary")

    return cls(
        config=HouseGanConfig.from_dict(config_payload),
        default_missing_relation=payload.get(
            "default_missing_relation", "not_adjacent"
        ),
    )

__call__

__call__(
    *,
    condition_type: ConditionType
    | str = ConditionType.relation,
    scene_graph: HouseGanSceneGraph
    | HouseGanSceneGraphPayload
    | None = None,
    relations: Sequence[HouseGanRelationPayload]
    | None = None,
    labels: Int[Tensor, "..."]
    | Int[ndarray, "..."]
    | Sequence[Sequence[int]]
    | Sequence[int]
    | None = None,
    bbox: Float[Tensor, "... 4"]
    | Float[ndarray, "... 4"]
    | Sequence[Sequence[Sequence[float]]]
    | Sequence[Sequence[float]]
    | Sequence[ArrayLikeInput]
    | None = None,
    mask: Bool[Tensor, "..."]
    | Bool[ndarray, "..."]
    | Sequence[Sequence[bool]]
    | Sequence[bool]
    | Sequence[ArrayLikeInput]
    | None = None,
    box_format: BoxFormat | str = BoxFormat.xywh,
    normalized: bool = True,
    canvas_size: tuple[int, int] | None = None,
    return_tensors: Literal["pt"] = "pt",
) -> BatchEncoding

Encode public relation inputs into House-GAN tensors.

Source code in models/housegan/src/housegan/processing_housegan.py
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
def __call__(
    self,
    *,
    condition_type: ConditionType | str = ConditionType.relation,
    scene_graph: HouseGanSceneGraph | HouseGanSceneGraphPayload | None = None,
    relations: Sequence[HouseGanRelationPayload] | None = None,
    labels: Int[torch.Tensor, "..."]
    | Int[np.ndarray, "..."]
    | Sequence[Sequence[int]]
    | Sequence[int]
    | None = None,
    bbox: Float[torch.Tensor, "... 4"]
    | Float[np.ndarray, "... 4"]
    | Sequence[Sequence[Sequence[float]]]
    | Sequence[Sequence[float]]
    | Sequence[ArrayLikeInput]
    | None = None,
    mask: Bool[torch.Tensor, "..."]
    | Bool[np.ndarray, "..."]
    | Sequence[Sequence[bool]]
    | Sequence[bool]
    | Sequence[ArrayLikeInput]
    | None = None,
    box_format: BoxFormat | str = BoxFormat.xywh,
    normalized: bool = True,
    canvas_size: tuple[int, int] | None = None,
    return_tensors: Literal["pt"] = "pt",
) -> BatchEncoding:
    """Encode public relation inputs into House-GAN tensors."""
    if return_tensors != "pt":
        raise ValueError("HouseGanProcessor only supports return_tensors='pt'")

    self.normalize_condition_type(condition_type)
    relation_payload = relations
    if relation_payload is None and bbox is not None and labels is not None:
        bbox_t, labels_t, _ = prepare_layout_tensors(
            bbox=cast(
                Float[torch.Tensor, "... 4"]
                | Float[np.ndarray, "... 4"]
                | Sequence[Sequence[Sequence[float]]]
                | Sequence[Sequence[float]]
                | Sequence[ArrayLikeInput],
                bbox,
            ),
            labels=cast(
                Int[torch.Tensor, "..."]
                | Int[np.ndarray, "..."]
                | Sequence[Sequence[int]]
                | Sequence[int]
                | Sequence[ArrayLikeInput],
                labels,
            ),
            mask=cast(
                Bool[torch.Tensor, "..."]
                | Bool[np.ndarray, "..."]
                | Sequence[Sequence[bool]]
                | Sequence[bool]
                | Sequence[ArrayLikeInput]
                | None,
                mask,
            ),
            box_format=normalize_box_format(box_format),
            normalized=normalized,
            canvas_size=canvas_size or self.canvas_size,
            clamp_converted_normalized=True,
        )
        relation_payload = relation_from_bboxes(
            _xywh_to_ltrb_list(bbox_t[0]),
        )
        labels = labels_t[0]
    graph = normalize_scene_graph(
        scene_graph,
        labels=labels,
        relations=relation_payload,
        id2label=self.id2label,
    )
    if self.default_missing_relation == "error" and not graph.relations:
        raise ValueError(
            "House-GAN requires relations when missing-pair policy is 'error'"
        )

    node_features = graph_to_node_features(
        graph.nodes,
        label2id=self.label2id,
        num_labels=len(self.id2label),
    )
    edges = complete_signed_edges(
        graph.nodes,
        graph.relations,
        default_adjacent=False,
    )
    label_ids = torch.tensor(
        [
            self.label2id[node.label]
            if isinstance(node.label, str)
            else int(node.label)
            for node in graph.nodes
        ],
        dtype=torch.long,
    )
    return BatchEncoding(
        {
            "node_features": node_features,
            "edges": edges,
            "labels": label_ids,
            "scene_graph": graph,
        }
    )

normalize_condition_type

normalize_condition_type(
    condition_type: ConditionType | str,
) -> ConditionType

Normalize and validate the House-GAN condition type.

Source code in models/housegan/src/housegan/processing_housegan.py
232
233
234
235
236
237
238
239
240
241
242
243
def normalize_condition_type(
    self, condition_type: ConditionType | str
) -> ConditionType:
    """Normalize and validate the House-GAN condition type."""
    condition = normalize_condition_type(condition_type)
    if condition is not ConditionType.relation:
        raise NotImplementedError(
            "House-GAN supports only condition_type='relation' and aliases "
            "'scene_graph', 'graph', or 'gen_r'."
        )

    return condition

post_process_masks

post_process_masks(
    masks: Float[Tensor, "elements height width"],
    *,
    labels: Int[Tensor, "elements"],
    edges: Int[Tensor, "edges 3"] | None = None,
    node_features: Float[Tensor, "elements room_labels"]
    | None = None,
    scene_graph: HouseGanSceneGraph | None = None,
    output_type: OutputType = "dataclass",
    return_intermediates: bool = False,
) -> (
    LayoutGenerationOutput
    | dict[
        str,
        Shaped[torch.Tensor, "..."]
        | dict[int, str]
        | dict[
            str,
            Shaped[torch.Tensor, "..."]
            | HouseGanSceneGraph
            | None,
        ]
        | None,
    ]
)

Convert generated room masks to public normalized boxes.

Source code in models/housegan/src/housegan/processing_housegan.py
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
def post_process_masks(
    self,
    masks: Float[torch.Tensor, "elements height width"],
    *,
    labels: Int[torch.Tensor, "elements"],
    edges: Int[torch.Tensor, "edges 3"] | None = None,
    node_features: Float[torch.Tensor, "elements room_labels"] | None = None,
    scene_graph: HouseGanSceneGraph | None = None,
    output_type: OutputType = "dataclass",
    return_intermediates: bool = False,
) -> (
    LayoutGenerationOutput
    | dict[
        str,
        Shaped[torch.Tensor, "..."]
        | dict[int, str]
        | dict[str, Shaped[torch.Tensor, "..."] | HouseGanSceneGraph | None]
        | None,
    ]
):
    """Convert generated room masks to public normalized boxes."""
    bbox_ltrb = mask_to_ltrb(masks, threshold=0.0)
    bbox_xywh = (
        ltrb_to_xywh(bbox_ltrb / float(self.mask_size)).unsqueeze(0).clamp(0.0, 1.0)
    )
    labels_b = labels.to(dtype=torch.long).unsqueeze(0)
    valid = torch.ones(labels_b.shape, dtype=torch.bool, device=labels_b.device)
    intermediates = None
    if return_intermediates:
        intermediates = {
            "room_masks": masks.detach(),
            "bbox_ltrb_32": bbox_ltrb,
            "signed_edges": edges,
            "node_features": node_features,
            "scene_graph": scene_graph,
        }
    if output_type == "dict":
        return {
            "bbox": bbox_xywh,
            "labels": labels_b,
            "mask": valid,
            "id2label": self.id2label,
            "sequences": edges,
            "scores": None,
            "trajectory": None,
            "intermediates": intermediates,
        }
    return LayoutGenerationOutput(
        bbox=bbox_xywh,
        labels=labels_b,
        mask=valid,
        id2label=self.id2label,
        sequences=edges,
        intermediates=intermediates,
    )

mask_to_ltrb

mask_to_ltrb(
    masks: Float[Tensor, "elements height width"],
    *,
    threshold: float = 0.0,
) -> Float[torch.Tensor, "elements 4"]

Convert thresholded masks to inclusive-exclusive ltrb boxes.

Source code in models/housegan/src/housegan/processing_housegan.py
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
def mask_to_ltrb(
    masks: Float[torch.Tensor, "elements height width"],
    *,
    threshold: float = 0.0,
) -> Float[torch.Tensor, "elements 4"]:
    """Convert thresholded masks to inclusive-exclusive ``ltrb`` boxes."""
    boxes: list[list[float]] = []
    for mask in masks.detach().cpu():
        inds = torch.nonzero(mask > threshold, as_tuple=False)
        if inds.numel() == 0:
            boxes.append([0.0, 0.0, 0.0, 0.0])
            continue
        y0 = int(inds[:, 0].min().item())
        x0 = int(inds[:, 1].min().item())
        y1 = int(inds[:, 0].max().item())
        x1 = int(inds[:, 1].max().item())
        boxes.append([float(x0), float(y0), float(x1 + 1), float(y1 + 1)])
    return torch.tensor(boxes, dtype=torch.float32, device=masks.device)

vendor_state_dict

State-dict validation helpers for original House-GAN checkpoints.

ConversionReport dataclass

Measured state-dict conversion metadata.

Source code in models/housegan/src/housegan/vendor_state_dict.py
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@dataclass(frozen=True)
class ConversionReport:
    """Measured state-dict conversion metadata."""

    key_count: int
    tensor_shapes: dict[str, tuple[int, ...]]
    missing_keys: tuple[str, ...] = ()
    unexpected_keys: tuple[str, ...] = ()

    def to_dict(self) -> HouseGanConversionReport:
        """Serialize report to JSON-compatible values."""
        return {
            "key_count": self.key_count,
            "tensor_shapes": self.tensor_shapes,
            "missing_keys": self.missing_keys,
            "unexpected_keys": self.unexpected_keys,
        }

to_dict

to_dict() -> HouseGanConversionReport

Serialize report to JSON-compatible values.

Source code in models/housegan/src/housegan/vendor_state_dict.py
24
25
26
27
28
29
30
31
def to_dict(self) -> HouseGanConversionReport:
    """Serialize report to JSON-compatible values."""
    return {
        "key_count": self.key_count,
        "tensor_shapes": self.tensor_shapes,
        "missing_keys": self.missing_keys,
        "unexpected_keys": self.unexpected_keys,
    }

convert_state_dict

convert_state_dict(
    source: Mapping[str, Shaped[Tensor, "..."]],
) -> tuple[
    OrderedDict[str, Shaped[torch.Tensor, "..."]],
    ConversionReport,
]

Validate and copy an original raw generator state dict.

Source code in models/housegan/src/housegan/vendor_state_dict.py
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
def convert_state_dict(
    source: Mapping[str, Shaped[torch.Tensor, "..."]],
) -> tuple[OrderedDict[str, Shaped[torch.Tensor, "..."]], ConversionReport]:
    """Validate and copy an original raw generator state dict."""
    converted: OrderedDict[str, Shaped[torch.Tensor, "..."]] = OrderedDict()
    for key, tensor in source.items():
        if not key.startswith(EXPECTED_PREFIXES):
            raise KeyError(key)

        converted[key] = tensor
    report = ConversionReport(
        key_count=len(converted),
        tensor_shapes={key: tuple(value.shape) for key, value in converted.items()},
    )
    return converted, report

visualization

Visualization helpers for House-GAN outputs.

render_layout

render_layout(
    bbox: Float[Tensor, "elements 4"],
    labels: Int[Tensor, "elements"],
    *,
    id2label: Mapping[int, str],
    canvas_size: tuple[int, int] = (256, 256),
) -> Image.Image

Render normalized center xywh boxes for debugging.

Source code in models/housegan/src/housegan/visualization.py
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
def render_layout(
    bbox: Float[torch.Tensor, "elements 4"],
    labels: Int[torch.Tensor, "elements"],
    *,
    id2label: Mapping[int, str],
    canvas_size: tuple[int, int] = (256, 256),
) -> Image.Image:
    """Render normalized center ``xywh`` boxes for debugging."""
    width, height = canvas_size
    image = Image.new("RGB", canvas_size, "white")
    draw = ImageDraw.Draw(image)
    for box, label in zip(bbox.detach().cpu(), labels.detach().cpu(), strict=True):
        cx, cy, bw, bh = box.tolist()
        left = (cx - bw / 2.0) * width
        top = (cy - bh / 2.0) * height
        right = (cx + bw / 2.0) * width
        bottom = (cy + bh / 2.0) * height
        _ = id2label.get(int(label.item()), str(int(label.item())))
        draw.rectangle((left, top, right, bottom), outline="black", width=2)
    return image