Skip to content

Commit cb98ac5

Browse files
vividfclaude
andcommitted
feat(models): add StreamPETR building blocks
VoVNet-99 multiscale backbone, grid-mask augmentation, the NMS-free 3D bbox coder, box L1 match cost, and the temporal-query helper library (positional embeddings, memory refresh, top-k gather, ego-frame point transforms). Signed-off-by: vividf <yihsiang.fang@tier4.jp> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 225753f commit cb98ac5

6 files changed

Lines changed: 802 additions & 1 deletion

File tree

Lines changed: 397 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,397 @@
1+
# Copyright 2026 TIER IV, Inc.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""VoVNet backbones reused by camera-based detection models.
16+
17+
This module provides a lightweight native VoVNet implementation.
18+
The exported class follows the same multi-scale tuple interface used
19+
by other Autoware-ML image backbones.
20+
"""
21+
22+
from __future__ import annotations
23+
24+
from collections import OrderedDict
25+
from collections.abc import Sequence
26+
27+
import torch
28+
import torch.nn as nn
29+
30+
_STAGE_SPECS = {
31+
"V-19-slim-dw-eSE": {
32+
"stem": [64, 64, 64],
33+
"stage_conv_ch": [64, 80, 96, 112],
34+
"stage_out_ch": [112, 256, 384, 512],
35+
"layer_per_block": 3,
36+
"block_per_stage": [1, 1, 1, 1],
37+
"dw": True,
38+
},
39+
"V-19-dw-eSE": {
40+
"stem": [64, 64, 64],
41+
"stage_conv_ch": [128, 160, 192, 224],
42+
"stage_out_ch": [256, 512, 768, 1024],
43+
"layer_per_block": 3,
44+
"block_per_stage": [1, 1, 1, 1],
45+
"dw": True,
46+
},
47+
"V-19-slim-eSE": {
48+
"stem": [64, 64, 128],
49+
"stage_conv_ch": [64, 80, 96, 112],
50+
"stage_out_ch": [112, 256, 384, 512],
51+
"layer_per_block": 3,
52+
"block_per_stage": [1, 1, 1, 1],
53+
"dw": False,
54+
},
55+
"V-19-eSE": {
56+
"stem": [64, 64, 128],
57+
"stage_conv_ch": [128, 160, 192, 224],
58+
"stage_out_ch": [256, 512, 768, 1024],
59+
"layer_per_block": 3,
60+
"block_per_stage": [1, 1, 1, 1],
61+
"dw": False,
62+
},
63+
"V-39-eSE": {
64+
"stem": [64, 64, 128],
65+
"stage_conv_ch": [128, 160, 192, 224],
66+
"stage_out_ch": [256, 512, 768, 1024],
67+
"layer_per_block": 5,
68+
"block_per_stage": [1, 1, 2, 2],
69+
"dw": False,
70+
},
71+
"V-57-eSE": {
72+
"stem": [64, 64, 128],
73+
"stage_conv_ch": [128, 160, 192, 224],
74+
"stage_out_ch": [256, 512, 768, 1024],
75+
"layer_per_block": 5,
76+
"block_per_stage": [1, 1, 4, 3],
77+
"dw": False,
78+
},
79+
"V-99-eSE": {
80+
"stem": [64, 64, 128],
81+
"stage_conv_ch": [128, 160, 192, 224],
82+
"stage_out_ch": [256, 512, 768, 1024],
83+
"layer_per_block": 5,
84+
"block_per_stage": [1, 3, 9, 3],
85+
"dw": False,
86+
},
87+
}
88+
89+
90+
def _conv3x3(
91+
in_channels: int,
92+
out_channels: int,
93+
module_name: str,
94+
postfix: str,
95+
stride: int = 1,
96+
groups: int = 1,
97+
kernel_size: int = 3,
98+
padding: int = 1,
99+
) -> list[tuple[str, nn.Module]]:
100+
"""Build a convolution, batchnorm, ReLU block."""
101+
return [
102+
(
103+
f"{module_name}_{postfix}/conv",
104+
nn.Conv2d(
105+
in_channels,
106+
out_channels,
107+
kernel_size=kernel_size,
108+
stride=stride,
109+
padding=padding,
110+
groups=groups,
111+
bias=False,
112+
),
113+
),
114+
(f"{module_name}_{postfix}/norm", nn.BatchNorm2d(out_channels)),
115+
(f"{module_name}_{postfix}/relu", nn.ReLU(inplace=True)),
116+
]
117+
118+
119+
def _conv1x1(
120+
in_channels: int, out_channels: int, module_name: str, postfix: str
121+
) -> list[tuple[str, nn.Module]]:
122+
"""Build a 1x1 convolution, batchnorm, ReLU block."""
123+
return _conv3x3(
124+
in_channels=in_channels,
125+
out_channels=out_channels,
126+
module_name=module_name,
127+
postfix=postfix,
128+
kernel_size=1,
129+
padding=0,
130+
)
131+
132+
133+
def _dw_conv3x3(
134+
in_channels: int, out_channels: int, module_name: str, postfix: str, stride: int = 1
135+
) -> list[tuple[str, nn.Module]]:
136+
"""Build a depthwise separable 3x3 block."""
137+
return [
138+
(
139+
f"{module_name}_{postfix}/dw_conv3x3",
140+
nn.Conv2d(
141+
in_channels,
142+
out_channels,
143+
kernel_size=3,
144+
stride=stride,
145+
padding=1,
146+
groups=out_channels,
147+
bias=False,
148+
),
149+
),
150+
(
151+
f"{module_name}_{postfix}/pw_conv1x1",
152+
nn.Conv2d(in_channels, out_channels, kernel_size=1, bias=False),
153+
),
154+
(f"{module_name}_{postfix}/pw_norm", nn.BatchNorm2d(out_channels)),
155+
(f"{module_name}_{postfix}/pw_relu", nn.ReLU(inplace=True)),
156+
]
157+
158+
159+
class _eSEModule(nn.Module):
160+
"""Effective squeeze-excitation block."""
161+
162+
def __init__(self, channels: int) -> None:
163+
"""Initialize the eSE block."""
164+
super().__init__()
165+
self.avg_pool = nn.AdaptiveAvgPool2d(1)
166+
self.fc = nn.Conv2d(channels, channels, kernel_size=1)
167+
self.hsigmoid = nn.Hardsigmoid(inplace=True)
168+
169+
def forward(self, x: torch.Tensor) -> torch.Tensor:
170+
"""Re-weight feature responses channel-wise."""
171+
weight = self.hsigmoid(self.fc(self.avg_pool(x)))
172+
return x * weight
173+
174+
175+
class _OSAModule(nn.Module):
176+
"""One-Shot Aggregation block used by VoVNet stages."""
177+
178+
def __init__(
179+
self,
180+
in_channels: int,
181+
stage_channels: int,
182+
concat_channels: int,
183+
layers_per_block: int,
184+
module_name: str,
185+
identity: bool = False,
186+
depthwise: bool = False,
187+
) -> None:
188+
"""Initialize one OSA module."""
189+
super().__init__()
190+
self.identity = identity
191+
self.depthwise = depthwise
192+
self.is_reduced = depthwise and in_channels != stage_channels
193+
self.layers = nn.ModuleList()
194+
195+
if self.is_reduced:
196+
self.conv_reduction = nn.Sequential(
197+
OrderedDict(_conv1x1(in_channels, stage_channels, f"{module_name}_reduction", "0"))
198+
)
199+
200+
current_channels = stage_channels if self.is_reduced else in_channels
201+
for layer_index in range(layers_per_block):
202+
block = _dw_conv3x3 if depthwise else _conv3x3
203+
self.layers.append(
204+
nn.Sequential(
205+
OrderedDict(
206+
block(current_channels, stage_channels, module_name, str(layer_index))
207+
)
208+
)
209+
)
210+
current_channels = stage_channels
211+
212+
aggregated_channels = in_channels + layers_per_block * stage_channels
213+
self.concat = nn.Sequential(
214+
OrderedDict(_conv1x1(aggregated_channels, concat_channels, module_name, "concat"))
215+
)
216+
self.ese = _eSEModule(concat_channels)
217+
218+
def forward(self, x: torch.Tensor) -> torch.Tensor:
219+
"""Aggregate intermediate features into one stage output."""
220+
identity = x
221+
outputs = [x]
222+
if self.depthwise and self.is_reduced:
223+
x = self.conv_reduction(x)
224+
for layer in self.layers:
225+
x = layer(x)
226+
outputs.append(x)
227+
x = self.concat(torch.cat(outputs, dim=1))
228+
x = self.ese(x)
229+
if self.identity:
230+
x = x + identity
231+
return x
232+
233+
234+
class _OSAStage(nn.Sequential):
235+
"""Stack one or more OSA modules into a VoVNet stage."""
236+
237+
def __init__(
238+
self,
239+
in_channels: int,
240+
stage_channels: int,
241+
concat_channels: int,
242+
blocks_per_stage: int,
243+
layers_per_block: int,
244+
stage_num: int,
245+
depthwise: bool = False,
246+
) -> None:
247+
"""Initialize one VoVNet stage."""
248+
super().__init__()
249+
if stage_num != 2:
250+
self.add_module("Pooling", nn.MaxPool2d(kernel_size=3, stride=2, ceil_mode=True))
251+
module_name = f"OSA{stage_num}_1"
252+
self.add_module(
253+
module_name,
254+
_OSAModule(
255+
in_channels,
256+
stage_channels,
257+
concat_channels,
258+
layers_per_block,
259+
module_name,
260+
depthwise=depthwise,
261+
),
262+
)
263+
for block_index in range(blocks_per_stage - 1):
264+
module_name = f"OSA{stage_num}_{block_index + 2}"
265+
self.add_module(
266+
module_name,
267+
_OSAModule(
268+
concat_channels,
269+
stage_channels,
270+
concat_channels,
271+
layers_per_block,
272+
module_name,
273+
identity=True,
274+
depthwise=depthwise,
275+
),
276+
)
277+
278+
279+
class VoVNetMultiScale(nn.Module):
280+
"""Expose VoVNet intermediate feature maps for downstream tasks."""
281+
282+
def __init__(
283+
self,
284+
spec_name: str,
285+
input_ch: int = 3,
286+
out_features: Sequence[str] = ("stage4", "stage5"),
287+
frozen_stages: int = -1,
288+
norm_eval: bool = True,
289+
) -> None:
290+
"""Initialize the multi-scale VoVNet backbone.
291+
292+
Args:
293+
spec_name: VoVNet stage specification name.
294+
input_ch: Number of channels in the input tensor.
295+
out_features: Names of feature stages to return.
296+
frozen_stages: Last stage index to freeze.
297+
norm_eval: Whether batchnorm layers should stay in eval mode during training.
298+
"""
299+
super().__init__()
300+
if spec_name not in _STAGE_SPECS:
301+
raise ValueError(f"Unsupported VoVNet spec: {spec_name}")
302+
self.norm_eval = norm_eval
303+
self.frozen_stages = frozen_stages
304+
self.out_features = tuple(out_features)
305+
306+
stage_specs = _STAGE_SPECS[spec_name]
307+
stem_channels = stage_specs["stem"]
308+
stage_conv_channels = stage_specs["stage_conv_ch"]
309+
stage_out_channels = stage_specs["stage_out_ch"]
310+
blocks_per_stage = stage_specs["block_per_stage"]
311+
layers_per_block = stage_specs["layer_per_block"]
312+
depthwise = stage_specs["dw"]
313+
314+
conv_block = _dw_conv3x3 if depthwise else _conv3x3
315+
stem = _conv3x3(input_ch, stem_channels[0], "stem", "1", stride=2)
316+
stem += conv_block(stem_channels[0], stem_channels[1], "stem", "2", stride=1)
317+
stem += conv_block(stem_channels[1], stem_channels[2], "stem", "3", stride=2)
318+
self.stem = nn.Sequential(OrderedDict(stem))
319+
320+
in_channels_per_stage = [stem_channels[2], *stage_out_channels[:-1]]
321+
self.stage_names: list[str] = []
322+
for stage_index in range(4):
323+
name = f"stage{stage_index + 2}"
324+
self.stage_names.append(name)
325+
self.add_module(
326+
name,
327+
_OSAStage(
328+
in_channels=in_channels_per_stage[stage_index],
329+
stage_channels=stage_conv_channels[stage_index],
330+
concat_channels=stage_out_channels[stage_index],
331+
blocks_per_stage=blocks_per_stage[stage_index],
332+
layers_per_block=layers_per_block,
333+
stage_num=stage_index + 2,
334+
depthwise=depthwise,
335+
),
336+
)
337+
338+
def _freeze_stages(self) -> None:
339+
"""Freeze the configured early stages."""
340+
if self.frozen_stages >= 0:
341+
self.stem.eval()
342+
for parameter in self.stem.parameters():
343+
parameter.requires_grad = False
344+
for stage_index in range(1, self.frozen_stages + 1):
345+
stage = getattr(self, f"stage{stage_index + 1}")
346+
stage.eval()
347+
for parameter in stage.parameters():
348+
parameter.requires_grad = False
349+
350+
def train(self, mode: bool = True) -> VoVNetMultiScale:
351+
"""Set training mode while honoring stage freezing and norm_eval."""
352+
super().train(mode)
353+
self._freeze_stages()
354+
if mode and self.norm_eval:
355+
for module in self.modules():
356+
if isinstance(module, nn.BatchNorm2d):
357+
module.eval()
358+
return self
359+
360+
def forward(self, x: torch.Tensor) -> tuple[torch.Tensor, ...]:
361+
"""Extract configured intermediate feature maps.
362+
363+
Args:
364+
x: Input tensor of shape ``(B, C, H, W)``.
365+
366+
Returns:
367+
Tuple of selected feature maps ordered by stage.
368+
"""
369+
outputs = []
370+
x = self.stem(x)
371+
if "stem" in self.out_features:
372+
outputs.append(x)
373+
for name in self.stage_names:
374+
x = getattr(self, name)(x)
375+
if name in self.out_features:
376+
outputs.append(x)
377+
return tuple(outputs)
378+
379+
380+
class VoVNet99MultiScale(VoVNetMultiScale):
381+
"""Expose VoVNet-99 stage outputs for multiview camera models."""
382+
383+
def __init__(
384+
self,
385+
input_ch: int = 3,
386+
out_features: Sequence[str] = ("stage4", "stage5"),
387+
frozen_stages: int = -1,
388+
norm_eval: bool = True,
389+
) -> None:
390+
"""Initialize the multi-scale VoVNet-99 backbone."""
391+
super().__init__(
392+
spec_name="V-99-eSE",
393+
input_ch=input_ch,
394+
out_features=out_features,
395+
frozen_stages=frozen_stages,
396+
norm_eval=norm_eval,
397+
)

0 commit comments

Comments
 (0)