Repository navigation
Expand file tree
/
Copy pathcutie.py
More file actions
259 lines (228 loc) · 10.7 KB
/
Copy pathcutie.py
File metadata and controls
259 lines (228 loc) · 10.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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
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
import logging
from omegaconf import DictConfig
import torch
import torch.nn as nn
from cutie.model.modules import *
from cutie.model.big_modules import *
from cutie.model.aux_modules import AuxComputer
from cutie.model.utils.memory_utils import *
from cutie.model.transformer.object_transformer import QueryTransformer
from cutie.model.transformer.object_summarizer import ObjectSummarizer
from cutie.utils.tensor_utils import aggregate
log = logging.getLogger()
class CUTIE(nn.Module):
def __init__(self, cfg: DictConfig, *, single_object=False):
super().__init__()
self.cfg = cfg
model_cfg = cfg.model
self.ms_dims = model_cfg.pixel_encoder.ms_dims
self.key_dim = model_cfg.key_dim
self.value_dim = model_cfg.value_dim
self.sensory_dim = model_cfg.sensory_dim
self.pixel_dim = model_cfg.pixel_dim
self.embed_dim = model_cfg.embed_dim
self.single_object = single_object
self.object_transformer_enabled = model_cfg.object_transformer.num_blocks > 0
log.info(f'Single object: {self.single_object}')
log.info(f'Object transformer enabled: {self.object_transformer_enabled}')
self.pixel_encoder = PixelEncoder(model_cfg)
self.pix_feat_proj = nn.Conv2d(self.ms_dims[0], self.pixel_dim, kernel_size=1)
self.key_proj = KeyProjection(model_cfg)
self.mask_encoder = MaskEncoder(model_cfg, single_object=single_object)
self.mask_decoder = MaskDecoder(model_cfg)
self.pixel_fuser = PixelFeatureFuser(model_cfg, single_object=single_object)
if self.object_transformer_enabled:
self.object_transformer = QueryTransformer(model_cfg)
self.object_summarizer = ObjectSummarizer(model_cfg)
self.aux_computer = AuxComputer(cfg)
self.register_buffer('pixel_mean', torch.Tensor(model_cfg.pixel_mean).view(-1, 1, 1), False)
self.register_buffer('pixel_std', torch.Tensor(model_cfg.pixel_std).view(-1, 1, 1), False)
def _get_others(self, masks: torch.Tensor) -> torch.Tensor:
# for each object, return the sum of masks of all other objects
if self.single_object:
return None
num_objects = masks.shape[1]
return (masks.sum(dim=1, keepdim=True) - masks).clamp(0, 1) if num_objects >= 1 else torch.zeros_like(masks)
def encode_image(self, image: torch.Tensor) -> (Iterable[torch.Tensor], torch.Tensor):
# sub() copies (never mutates the caller's frame); only the fresh copy is div_'d in place
image = image.sub(self.pixel_mean).div_(self.pixel_std)
ms_image_feat = self.pixel_encoder(image)
return ms_image_feat, self.pix_feat_proj(ms_image_feat[0])
def encode_mask(
self,
image: torch.Tensor,
ms_features: list[torch.Tensor],
sensory: torch.Tensor,
masks: torch.Tensor,
*,
deep_update: bool = True,
chunk_size: int = -1,
need_weights: bool = False,
) -> (torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor):
image = image.sub(self.pixel_mean).div_(self.pixel_std)
others = self._get_others(masks)
mask_value, new_sensory = self.mask_encoder(
image,
ms_features,
sensory,
masks,
others,
deep_update=deep_update,
chunk_size=chunk_size,
)
if self.object_transformer_enabled:
object_summaries, object_logits = self.object_summarizer(masks, mask_value, need_weights)
else:
object_summaries, object_logits = None, None
return mask_value, new_sensory, object_summaries, object_logits
def transform_key(
self, final_pix_feat: torch.Tensor, *, need_sk: bool = True, need_ek: bool = True
) -> (torch.Tensor, torch.Tensor, torch.Tensor):
key, shrinkage, selection = self.key_proj(final_pix_feat, need_s=need_sk, need_e=need_ek)
return key, shrinkage, selection
# Used in training only.
# This step is replaced by MemoryManager in test time
def read_memory(
self,
query_key: torch.Tensor,
query_selection: torch.Tensor,
memory_key: torch.Tensor,
memory_shrinkage: torch.Tensor,
msk_value: torch.Tensor,
obj_memory: torch.Tensor,
pix_feat: torch.Tensor,
sensory: torch.Tensor,
last_mask: torch.Tensor,
selector: torch.Tensor,
) -> (torch.Tensor, dict[str, torch.Tensor]):
"""
query_key : B * CK * H * W
query_selection : B * CK * H * W
memory_key : B * CK * T * H * W
memory_shrinkage: B * 1 * T * H * W
msk_value : B * num_objects * CV * T * H * W
obj_memory : B * num_objects * T * num_summaries * C
pixel_feature : B * C * H * W
"""
batch_size, num_objects = msk_value.shape[:2]
# read using visual attention
with torch.cuda.amp.autocast(enabled=False):
affinity = get_affinity(
memory_key.float(),
memory_shrinkage.float(),
query_key.float(),
query_selection.float(),
)
msk_value = msk_value.flatten(start_dim=1, end_dim=2).float()
# B * (num_objects*CV) * H * W
pixel_readout = readout(affinity, msk_value)
pixel_readout = pixel_readout.view(batch_size, num_objects, self.value_dim, *pixel_readout.shape[-2:])
pixel_readout = self.pixel_fusion(pix_feat, pixel_readout, sensory, last_mask)
# read from query transformer
mem_readout, aux_features = self.readout_query(pixel_readout, obj_memory, selector=selector)
aux_output = {
'sensory': sensory,
'q_logits': aux_features['logits'] if aux_features else None,
'attn_mask': aux_features['attn_mask'] if aux_features else None,
}
return mem_readout, aux_output
def pixel_fusion(
self,
pix_feat: torch.Tensor,
pixel: torch.Tensor,
sensory: torch.Tensor,
last_mask: torch.Tensor,
*,
chunk_size: int = -1,
) -> torch.Tensor:
last_mask = F.interpolate(last_mask, size=sensory.shape[-2:], mode='area')
last_others = self._get_others(last_mask)
return self.pixel_fuser(pix_feat, pixel, sensory, last_mask, last_others, chunk_size=chunk_size)
def readout_query(
self, pixel_readout, obj_memory, *, selector=None, need_weights=False
) -> (torch.Tensor, dict[str, torch.Tensor]):
if not self.object_transformer_enabled:
return pixel_readout, None
return self.object_transformer(pixel_readout, obj_memory, selector=selector, need_weights=need_weights)
def segment(
self,
ms_image_feat: list[torch.Tensor],
memory_readout: torch.Tensor,
sensory: torch.Tensor,
*,
selector: bool = None,
chunk_size: int = -1,
update_sensory: bool = True,
) -> (torch.Tensor, torch.Tensor, torch.Tensor):
"""
multi_scale_features is from the key encoder for skip-connection
memory_readout is from working/long-term memory
sensory is the sensory memory
last_mask is the mask from the last frame, supplementing sensory memory
selector is 1 if an object exists, and 0 otherwise. We use it to filter padded objects
during training.
"""
sensory, logits = self.mask_decoder(
ms_image_feat,
memory_readout,
sensory,
chunk_size=chunk_size,
update_sensory=update_sensory,
)
prob = torch.sigmoid(logits)
if selector is not None:
prob = prob * selector
# Softmax over all objects[]
logits = aggregate(prob, dim=1)
logits = F.interpolate(logits, scale_factor=4, mode='bilinear', align_corners=False)
prob = F.softmax(logits, dim=1)
return sensory, logits, prob
def compute_aux(
self, pix_feat: torch.Tensor, aux_inputs: dict[str, torch.Tensor], selector: torch.Tensor
) -> dict[str, torch.Tensor]:
return self.aux_computer(pix_feat, aux_inputs, selector)
def forward(self, *args, **kwargs):
raise NotImplementedError
def load_weights(self, src_dict, init_as_zero_if_needed=False) -> None:
if not self.single_object:
# Map single-object weight to multi-object weight (4->5 out channels in conv1)
for k in list(src_dict.keys()):
if k == 'mask_encoder.conv1.weight':
if src_dict[k].shape[1] == 4:
log.info(f'Converting {k} from single object to multiple objects.')
pads = torch.zeros((64, 1, 7, 7), device=src_dict[k].device)
if not init_as_zero_if_needed:
nn.init.orthogonal_(pads)
log.info(f'Randomly initialized padding for {k}.')
else:
log.info(f'Zero-initialized padding for {k}.')
src_dict[k] = torch.cat([src_dict[k], pads], 1)
elif k == 'pixel_fuser.sensory_compress.weight' and src_dict[k].shape[1] == self.sensory_dim + 1:
log.info(f'Converting {k} from single object to multiple objects.')
pads = torch.zeros((self.value_dim, 1, 1, 1), device=src_dict[k].device)
if not init_as_zero_if_needed:
nn.init.orthogonal_(pads)
log.info(f'Randomly initialized padding for {k}.')
else:
log.info(f'Zero-initialized padding for {k}.')
src_dict[k] = torch.cat([src_dict[k], pads], 1)
elif self.single_object:
# Support finetuning a multi-object model on single-object datasets by dropping conv1's
# final channel. Standard single-object training does not enter this compatibility path.
k = 'mask_encoder.conv1.weight'
if src_dict[k].shape[1] == 5:
log.warning(
f'Converting {k} from multiple objects to single object.'
'This is not supposed to happen in standard training.'
)
src_dict[k] = src_dict[k][:, :-1]
for k in src_dict:
if k not in self.state_dict():
log.info(f'Key {k} found in src_dict but not in self.state_dict()!!!')
for k in self.state_dict():
if k not in src_dict:
log.info(f'Key {k} found in self.state_dict() but not in src_dict!!!')
self.load_state_dict(src_dict, strict=False)
@property
def device(self) -> torch.device:
return self.pixel_mean.device