release infer and demo

This commit is contained in:
pq-yang
2026-03-06 10:00:32 +00:00
parent 57d038288c
commit b2ae4b1360
85 changed files with 6520 additions and 3 deletions
View File
@@ -0,0 +1,56 @@
import warnings
from typing import Iterable
import torch
from matanyone2.model.matanyone2 import MatAnyone2
class ImageFeatureStore:
"""
A cache for image features.
These features might be reused at different parts of the inference pipeline.
This class provide an interface for reusing these features.
It is the user's responsibility to delete redundant features.
Feature of a frame should be associated with a unique index -- typically the frame id.
"""
def __init__(self, network: MatAnyone2, no_warning: bool = False):
self.network = network
self._store = {}
self.no_warning = no_warning
def _encode_feature(self, index: int, image: torch.Tensor, last_feats=None) -> None:
ms_features, pix_feat = self.network.encode_image(image, last_feats=last_feats)
key, shrinkage, selection = self.network.transform_key(ms_features[0])
self._store[index] = (ms_features, pix_feat, key, shrinkage, selection)
def get_all_features(self, images: torch.Tensor) -> (Iterable[torch.Tensor], torch.Tensor):
seq_length = images.shape[0]
ms_features, pix_feat = self.network.encode_image(images, seq_length)
key, shrinkage, selection = self.network.transform_key(ms_features[0])
for index in range(seq_length):
self._store[index] = ([f[index].unsqueeze(0) for f in ms_features], pix_feat[index].unsqueeze(0), key[index].unsqueeze(0), shrinkage[index].unsqueeze(0), selection[index].unsqueeze(0))
def get_features(self, index: int,
image: torch.Tensor, last_feats=None) -> (Iterable[torch.Tensor], torch.Tensor):
if index not in self._store:
self._encode_feature(index, image, last_feats)
return self._store[index][:2]
def get_key(self, index: int,
image: torch.Tensor, last_feats=None) -> (torch.Tensor, torch.Tensor, torch.Tensor):
if index not in self._store:
self._encode_feature(index, image, last_feats)
return self._store[index][2:]
def delete(self, index: int) -> None:
if index in self._store:
del self._store[index]
def __len__(self):
return len(self._store)
def __del__(self):
if len(self._store) > 0 and not self.no_warning:
warnings.warn(f'Leaking {self._store.keys()} in the image feature store')
+550
View File
@@ -0,0 +1,550 @@
import logging
from omegaconf import DictConfig
from typing import List, Optional, Iterable, Union,Tuple
import os
import cv2
import torch
import imageio
import tempfile
import numpy as np
from tqdm import tqdm
from PIL import Image
import torch.nn.functional as F
from matanyone2.inference.memory_manager import MemoryManager
from matanyone2.inference.object_manager import ObjectManager
from matanyone2.inference.image_feature_store import ImageFeatureStore
from matanyone2.model.matanyone2 import MatAnyone2
from matanyone2.utils.tensor_utils import pad_divide_by, unpad, aggregate
from matanyone2.utils.inference_utils import gen_dilate, gen_erosion, read_frame_from_videos
from matanyone2.utils.device import get_default_device, safe_autocast
log = logging.getLogger()
class InferenceCore:
def __init__(self,
network: Union[MatAnyone2,str],
cfg: DictConfig = None,
*,
image_feature_store: ImageFeatureStore = None,
device: Optional[Union[str, torch.device]] = None
):
if device is None:
device = get_default_device()
self.device = device
if isinstance(network, str):
network = MatAnyone2.from_pretrained(network)
network.to(device)
network.eval()
self.network = network
cfg = cfg if cfg is not None else network.cfg
self.cfg = cfg
self.mem_every = cfg.mem_every
stagger_updates = cfg.stagger_updates
self.chunk_size = cfg.chunk_size
self.save_aux = cfg.save_aux
self.max_internal_size = cfg.max_internal_size
self.flip_aug = cfg.flip_aug
self.curr_ti = -1
self.last_mem_ti = 0
# at which time indices should we update the sensory memory
if stagger_updates >= self.mem_every:
self.stagger_ti = set(range(1, self.mem_every + 1))
else:
self.stagger_ti = set(
np.round(np.linspace(1, self.mem_every, stagger_updates)).astype(int))
self.object_manager = ObjectManager()
self.memory = MemoryManager(cfg=cfg, object_manager=self.object_manager)
if image_feature_store is None:
self.image_feature_store = ImageFeatureStore(self.network)
else:
self.image_feature_store = image_feature_store
self.last_mask = None
self.last_pix_feat = None
self.last_msk_value = None
def clear_memory(self):
self.curr_ti = -1
self.last_mem_ti = 0
self.memory = MemoryManager(cfg=self.cfg, object_manager=self.object_manager)
def clear_non_permanent_memory(self):
self.curr_ti = -1
self.last_mem_ti = 0
self.memory.clear_non_permanent_memory()
def clear_sensory_memory(self):
self.curr_ti = -1
self.last_mem_ti = 0
self.memory.clear_sensory_memory()
def update_config(self, cfg):
self.mem_every = cfg['mem_every']
self.memory.update_config(cfg)
def clear_temp_mem(self):
self.memory.clear_work_mem()
# self.object_manager = ObjectManager()
self.memory.clear_obj_mem()
# self.memory.clear_sensory_memory()
def _add_memory(self,
image: torch.Tensor,
pix_feat: torch.Tensor,
prob: torch.Tensor,
key: torch.Tensor,
shrinkage: torch.Tensor,
selection: torch.Tensor,
*,
is_deep_update: bool = True,
force_permanent: bool = False) -> None:
"""
Memorize the given segmentation in all memory stores.
The batch dimension is 1 if flip augmentation is not used.
image: RGB image, (1/2)*3*H*W
pix_feat: from the key encoder, (1/2)*_*H*W
prob: (1/2)*num_objects*H*W, in [0, 1]
key/shrinkage/selection: for anisotropic l2, (1/2)*_*H*W
selection can be None if not using long-term memory
is_deep_update: whether to use deep update (e.g. with the mask encoder)
force_permanent: whether to force the memory to be permanent
"""
if prob.shape[1] == 0:
# nothing to add
log.warn('Trying to add an empty object mask to memory!')
return
if force_permanent:
as_permanent = 'all'
else:
as_permanent = 'first'
self.memory.initialize_sensory_if_needed(key, self.object_manager.all_obj_ids)
msk_value, sensory, obj_value, _ = self.network.encode_mask(
image,
pix_feat,
self.memory.get_sensory(self.object_manager.all_obj_ids),
prob,
deep_update=is_deep_update,
chunk_size=self.chunk_size,
need_weights=self.save_aux)
self.memory.add_memory(key,
shrinkage,
msk_value,
obj_value,
self.object_manager.all_obj_ids,
selection=selection,
as_permanent=as_permanent)
self.last_mem_ti = self.curr_ti
if is_deep_update:
self.memory.update_sensory(sensory, self.object_manager.all_obj_ids)
self.last_msk_value = msk_value
def _segment(self,
key: torch.Tensor,
selection: torch.Tensor,
pix_feat: torch.Tensor,
ms_features: Iterable[torch.Tensor],
update_sensory: bool = True) -> torch.Tensor:
"""
Produce a segmentation using the given features and the memory
The batch dimension is 1 if flip augmentation is not used.
key/selection: for anisotropic l2: (1/2) * _ * H * W
pix_feat: from the key encoder, (1/2) * _ * H * W
ms_features: an iterable of multiscale features from the encoder, each is (1/2)*_*H*W
with strides 16, 8, and 4 respectively
update_sensory: whether to update the sensory memory
Returns: (num_objects+1)*H*W normalized probability; the first channel is the background
"""
bs = key.shape[0]
if self.flip_aug:
assert bs == 2
else:
assert bs == 1
if not self.memory.engaged:
log.warn('Trying to segment without any memory!')
return torch.zeros((1, key.shape[-2] * 16, key.shape[-1] * 16),
device=key.device,
dtype=key.dtype)
uncert_output = None
if self.curr_ti == 0: # ONLY for the first frame for prediction
memory_readout = self.memory.read_first_frame(self.last_msk_value, pix_feat, self.last_mask, self.network, uncert_output=uncert_output)
else:
memory_readout = self.memory.read(pix_feat, key, selection, self.last_mask, self.network, uncert_output=uncert_output, last_msk_value=self.last_msk_value, ti=self.curr_ti,
last_pix_feat=self.last_pix_feat, last_pred_mask=self.last_mask)
memory_readout = self.object_manager.realize_dict(memory_readout)
sensory, _, pred_prob_with_bg = self.network.segment(ms_features,
memory_readout,
self.memory.get_sensory(
self.object_manager.all_obj_ids),
chunk_size=self.chunk_size,
update_sensory=update_sensory)
# remove batch dim
if self.flip_aug:
# average predictions of the non-flipped and flipped version
pred_prob_with_bg = (pred_prob_with_bg[0] +
torch.flip(pred_prob_with_bg[1], dims=[-1])) / 2
else:
pred_prob_with_bg = pred_prob_with_bg[0]
if update_sensory:
self.memory.update_sensory(sensory, self.object_manager.all_obj_ids)
return pred_prob_with_bg
def pred_all_flow(self, images):
self.total_len = images.shape[0]
images, self.pad = pad_divide_by(images, 16)
images = images.unsqueeze(0) # add the batch dimension: (1,t,c,h,w)
self.flows_forward, self.flows_backward = self.network.pred_forward_backward_flow(images)
def encode_all_images(self, images):
images, self.pad = pad_divide_by(images, 16)
self.image_feature_store.get_all_features(images) # t c h w
return images
def step(self,
image: torch.Tensor,
mask: Optional[torch.Tensor] = None,
objects: Optional[List[int]] = None,
*,
idx_mask: bool = False,
end: bool = False,
delete_buffer: bool = True,
force_permanent: bool = False,
matting: bool = True,
first_frame_pred: bool = False) -> torch.Tensor:
"""
Take a step with a new incoming image.
If there is an incoming mask with new objects, we will memorize them.
If there is no incoming mask, we will segment the image using the memory.
In both cases, we will update the memory and return a segmentation.
image: 3*H*W
mask: H*W (if idx mask) or len(objects)*H*W or None
objects: list of object ids that are valid in the mask Tensor.
The ids themselves do not need to be consecutive/in order, but they need to be
in the same position in the list as the corresponding mask
in the tensor in non-idx-mask mode.
objects is ignored if the mask is None.
If idx_mask is False and objects is None, we sequentially infer the object ids.
idx_mask: if True, mask is expected to contain an object id at every pixel.
If False, mask should have multiple channels with each channel representing one object.
end: if we are at the end of the sequence, we do not need to update memory
if unsure just set it to False
delete_buffer: whether to delete the image feature buffer after this step
force_permanent: the memory recorded this frame will be added to the permanent memory
"""
if objects is None and mask is not None:
assert not idx_mask
objects = list(range(1, mask.shape[0] + 1))
# resize input if needed -- currently only used for the GUI
resize_needed = False
if self.max_internal_size > 0:
h, w = image.shape[-2:]
min_side = min(h, w)
if min_side > self.max_internal_size:
resize_needed = True
new_h = int(h / min_side * self.max_internal_size)
new_w = int(w / min_side * self.max_internal_size)
image = F.interpolate(image.unsqueeze(0),
size=(new_h, new_w),
mode='bilinear',
align_corners=False)[0]
if mask is not None:
if idx_mask:
mask = F.interpolate(mask.unsqueeze(0).unsqueeze(0).float(),
size=(new_h, new_w),
mode='nearest-exact',
align_corners=False)[0, 0].round().long()
else:
mask = F.interpolate(mask.unsqueeze(0),
size=(new_h, new_w),
mode='bilinear',
align_corners=False)[0]
self.curr_ti += 1
image, self.pad = pad_divide_by(image, 16) # DONE alreay for 3DCNN!!
image = image.unsqueeze(0) # add the batch dimension
if self.flip_aug:
image = torch.cat([image, torch.flip(image, dims=[-1])], dim=0)
# whether to update the working memory
is_mem_frame = ((self.curr_ti - self.last_mem_ti >= self.mem_every) or
(mask is not None)) and (not end)
# segment when there is no input mask or when the input mask is incomplete
need_segment = (mask is None) or (self.object_manager.num_obj > 0
and not self.object_manager.has_all(objects))
update_sensory = ((self.curr_ti - self.last_mem_ti) in self.stagger_ti) and (not end)
# reinit if it is the first frame for prediction
if first_frame_pred:
self.curr_ti = 0
self.last_mem_ti = 0
is_mem_frame = True
need_segment = True
update_sensory = True
# encoding the image
ms_feat, pix_feat = self.image_feature_store.get_features(self.curr_ti, image)
key, shrinkage, selection = self.image_feature_store.get_key(self.curr_ti, image)
# segmentation from memory if needed
if need_segment:
pred_prob_with_bg = self._segment(key,
selection,
pix_feat,
ms_feat,
update_sensory=update_sensory)
# use the input mask if provided
if mask is not None:
# inform the manager of the new objects, and get a list of temporary id
# temporary ids -- indicates the position of objects in the tensor
# (starts with 1 due to the background channel)
corresponding_tmp_ids, _ = self.object_manager.add_new_objects(objects)
mask, _ = pad_divide_by(mask, 16)
if need_segment:
# merge predicted mask with the incomplete input mask
pred_prob_no_bg = pred_prob_with_bg[1:]
# use the mutual exclusivity of segmentation
if idx_mask:
pred_prob_no_bg[:, mask > 0] = 0
else:
pred_prob_no_bg[:, mask.max(0) > 0.5] = 0
new_masks = []
for mask_id, tmp_id in enumerate(corresponding_tmp_ids):
if idx_mask:
this_mask = (mask == objects[mask_id]).type_as(pred_prob_no_bg)
else:
this_mask = mask[tmp_id]
if tmp_id > pred_prob_no_bg.shape[0]:
new_masks.append(this_mask.unsqueeze(0))
else:
# +1 for padding the background channel
pred_prob_no_bg[tmp_id - 1] = this_mask
# new_masks are always in the order of tmp_id
mask = torch.cat([pred_prob_no_bg, *new_masks], dim=0)
elif idx_mask:
# simply convert cls to one-hot representation
if len(objects) == 0:
if delete_buffer:
self.image_feature_store.delete(self.curr_ti)
log.warn('Trying to insert an empty mask as memory!')
return torch.zeros((1, key.shape[-2] * 16, key.shape[-1] * 16),
device=key.device,
dtype=key.dtype)
mask = torch.stack(
[mask == objects[mask_id] for mask_id, _ in enumerate(corresponding_tmp_ids)],
dim=0)
if matting:
mask = mask.unsqueeze(0).float() / 255.
pred_prob_with_bg = torch.cat([1-mask, mask], 0)
else:
pred_prob_with_bg = aggregate(mask, dim=0)
pred_prob_with_bg = torch.softmax(pred_prob_with_bg, dim=0)
self.last_mask = pred_prob_with_bg[1:].unsqueeze(0)
if self.flip_aug:
self.last_mask = torch.cat(
[self.last_mask, torch.flip(self.last_mask, dims=[-1])], dim=0)
self.last_pix_feat = pix_feat
# save as memory if needed
if is_mem_frame or force_permanent:
# clear the memory for given mask and add the first predicted mask
if first_frame_pred:
self.clear_temp_mem()
self._add_memory(image,
pix_feat,
self.last_mask,
key,
shrinkage,
selection,
force_permanent=force_permanent,
is_deep_update=True)
else: # compute self.last_msk_value for non-memory frame
msk_value, _, _, _ = self.network.encode_mask(
image,
pix_feat,
self.memory.get_sensory(self.object_manager.all_obj_ids),
self.last_mask,
deep_update=False,
chunk_size=self.chunk_size,
need_weights=self.save_aux)
self.last_msk_value = msk_value
if delete_buffer:
self.image_feature_store.delete(self.curr_ti)
output_prob = unpad(pred_prob_with_bg, self.pad)
if resize_needed:
# restore output to the original size
output_prob = F.interpolate(output_prob.unsqueeze(0),
size=(h, w),
mode='bilinear',
align_corners=False)[0]
return output_prob
def delete_objects(self, objects: List[int]) -> None:
"""
Delete the given objects from the memory.
"""
self.object_manager.delete_objects(objects)
self.memory.purge_except(self.object_manager.all_obj_ids)
def output_prob_to_mask(self, output_prob: torch.Tensor, matting: bool = True) -> torch.Tensor:
if matting:
new_mask = output_prob[1:].squeeze(0)
else:
mask = torch.argmax(output_prob, dim=0)
# index in tensor != object id -- remap the ids here
new_mask = torch.zeros_like(mask)
for tmp_id, obj in self.object_manager.tmp_id_to_obj.items():
new_mask[mask == tmp_id] = obj.id
return new_mask
@torch.inference_mode()
@safe_autocast()
def process_video(
self,
input_path: str,
mask_path: str,
output_path: str = None,
n_warmup: int = 10,
r_erode: int = 10,
r_dilate: int = 10,
suffix: str = "",
save_image: bool = False,
max_size: int = -1,
) -> Tuple:
"""
Process a video for object segmentation and matting.
This method processes a video file by performing object segmentation and matting on each frame.
It supports warmup frames, mask erosion/dilation, and various output options.
Args:
input_path (str): Path to the input video file
mask_path (str): Path to the mask image file used for initial segmentation
output_path (str, optional): Directory path where output files will be saved. Defaults to a temporary directory
n_warmup (int, optional): Number of warmup frames to use. Defaults to 10
r_erode (int, optional): Erosion radius for mask processing. Defaults to 10
r_dilate (int, optional): Dilation radius for mask processing. Defaults to 10
suffix (str, optional): Suffix to append to output filename. Defaults to ""
save_image (bool, optional): Whether to save individual frames. Defaults to False
max_size (int, optional): Maximum size for frame dimension. Use -1 for no limit. Defaults to -1
Returns:
Tuple[str, str]: A tuple containing:
- Path to the output foreground video file (str)
- Path to the output alpha matte video file (str)
Output:
- Saves processed video files with foreground (_fgr) and alpha matte (_pha)
- If save_image=True, saves individual frames in separate directories
"""
output_path = output_path if output_path is not None else tempfile.TemporaryDirectory().name
r_erode = int(r_erode)
r_dilate = int(r_dilate)
n_warmup = int(n_warmup)
max_size = int(max_size)
vframes, fps, length, video_name = read_frame_from_videos(input_path)
repeated_frames = vframes[0].unsqueeze(0).repeat(n_warmup, 1, 1, 1)
vframes = torch.cat([repeated_frames, vframes], dim=0).float()
length += n_warmup
new_h, new_w = vframes.shape[-2:]
if max_size > 0:
h, w = new_h, new_w
min_side = min(h, w)
if min_side > max_size:
new_h = int(h / min_side * max_size)
new_w = int(w / min_side * max_size)
vframes = F.interpolate(vframes, size=(new_h, new_w), mode="area")
os.makedirs(output_path, exist_ok=True)
if suffix:
video_name = f"{video_name}_{suffix}"
if save_image:
os.makedirs(f"{output_path}/{video_name}", exist_ok=True)
os.makedirs(f"{output_path}/{video_name}/pha", exist_ok=True)
os.makedirs(f"{output_path}/{video_name}/fgr", exist_ok=True)
mask = np.array(Image.open(mask_path).convert("L"))
if r_dilate > 0:
mask = gen_dilate(mask, r_dilate, r_dilate)
if r_erode > 0:
mask = gen_erosion(mask, r_erode, r_erode)
mask = torch.from_numpy(mask).float().to(self.device)
if max_size > 0:
mask = F.interpolate(
mask.unsqueeze(0).unsqueeze(0), size=(new_h, new_w), mode="nearest"
)[0, 0]
bgr = (np.array([120, 255, 155], dtype=np.float32) / 255).reshape((1, 1, 3))
objects = [1]
phas = []
fgrs = []
for ti in tqdm(range(length)):
image = vframes[ti]
image_np = np.array(image.permute(1, 2, 0))
image = (image / 255.0).float().to(self.device)
if ti == 0:
output_prob = self.step(image, mask, objects=objects)
output_prob = self.step(image, first_frame_pred=True)
else:
if ti <= n_warmup:
output_prob = self.step(image, first_frame_pred=True)
else:
output_prob = self.step(image)
mask = self.output_prob_to_mask(output_prob)
pha = mask.unsqueeze(2).cpu().numpy()
com_np = image_np / 255.0 * pha + bgr * (1 - pha)
if ti > (n_warmup - 1):
com_np = (com_np * 255).astype(np.uint8)
pha = (pha * 255).astype(np.uint8)
fgrs.append(com_np)
phas.append(pha)
if save_image:
cv2.imwrite(
f"{output_path}/{video_name}/pha/{str(ti - n_warmup).zfill(5)}.png",
pha,
)
cv2.imwrite(
f"{output_path}/{video_name}/fgr/{str(ti - n_warmup).zfill(5)}.png",
com_np[..., [2, 1, 0]],
)
fgrs = np.array(fgrs)
phas = np.array(phas)
fgr_filename = f"{output_path}/{video_name}_fgr.mp4"
alpha_filename = f"{output_path}/{video_name}_pha.mp4"
imageio.mimwrite(fgr_filename, fgrs, fps=fps, quality=7)
imageio.mimwrite(alpha_filename, phas, fps=fps, quality=7)
return (fgr_filename,alpha_filename)
+348
View File
@@ -0,0 +1,348 @@
from typing import Dict, List, Optional, Literal
from collections import defaultdict
import torch
def _add_last_dim(dictionary, key, new_value, prepend=False):
# append/prepend a new value to the last dimension of a tensor in a dictionary
# if the key does not exist, put the new value in
# append by default
if key in dictionary:
dictionary[key] = torch.cat([dictionary[key], new_value], -1)
else:
dictionary[key] = new_value
class KeyValueMemoryStore:
"""
Works for key/value pairs type storage
e.g., working and long-term memory
"""
def __init__(self, save_selection: bool = False, save_usage: bool = False):
"""
We store keys and values of objects that first appear in the same frame in a bucket.
Each bucket contains a set of object ids.
Each bucket is associated with a single key tensor
and a dictionary of value tensors indexed by object id.
The keys and values are stored as the concatenation of a permanent part and a temporary part.
"""
self.save_selection = save_selection
self.save_usage = save_usage
self.global_bucket_id = 0 # does not reduce even if buckets are removed
self.buckets: Dict[int, List[int]] = {} # indexed by bucket id
self.k: Dict[int, torch.Tensor] = {} # indexed by bucket id
self.v: Dict[int, torch.Tensor] = {} # indexed by object id
# indexed by bucket id; the end point of permanent memory
self.perm_end_pt: Dict[int, int] = defaultdict(int)
# shrinkage and selection are just like the keys
self.s = {}
if self.save_selection:
self.e = {} # does not contain the permanent memory part
# usage
if self.save_usage:
self.use_cnt = {} # indexed by bucket id, does not contain the permanent memory part
self.life_cnt = {} # indexed by bucket id, does not contain the permanent memory part
def add(self,
key: torch.Tensor,
values: Dict[int, torch.Tensor],
shrinkage: torch.Tensor,
selection: torch.Tensor,
supposed_bucket_id: int = -1,
as_permanent: Literal['no', 'first', 'all'] = 'no') -> None:
"""
key: (1/2)*C*N
values: dict of values ((1/2)*C*N), object ids are used as keys
shrinkage: (1/2)*1*N
selection: (1/2)*C*N
supposed_bucket_id: used to sync the bucket id between working and long-term memory
if provided, the input should all be in a single bucket indexed by this id
as_permanent: whether to store the input as permanent memory
'no': don't
'first': only store it as permanent memory if the bucket is empty
'all': always store it as permanent memory
"""
bs = key.shape[0]
ne = key.shape[-1]
assert len(key.shape) == 3
assert len(shrinkage.shape) == 3
assert not self.save_selection or len(selection.shape) == 3
assert as_permanent in ['no', 'first', 'all']
# add the value and create new buckets if necessary
if supposed_bucket_id >= 0:
enabled_buckets = [supposed_bucket_id]
bucket_exist = supposed_bucket_id in self.buckets
for obj, value in values.items():
if bucket_exist:
assert obj in self.v
assert obj in self.buckets[supposed_bucket_id]
_add_last_dim(self.v, obj, value, prepend=(as_permanent == 'all'))
else:
assert obj not in self.v
self.v[obj] = value
self.buckets[supposed_bucket_id] = list(values.keys())
else:
new_bucket_id = None
enabled_buckets = set()
for obj, value in values.items():
assert len(value.shape) == 3
if obj in self.v:
_add_last_dim(self.v, obj, value, prepend=(as_permanent == 'all'))
bucket_used = [
bucket_id for bucket_id, object_ids in self.buckets.items()
if obj in object_ids
]
assert len(bucket_used) == 1 # each object should only be in one bucket
enabled_buckets.add(bucket_used[0])
else:
self.v[obj] = value
if new_bucket_id is None:
# create new bucket
new_bucket_id = self.global_bucket_id
self.global_bucket_id += 1
self.buckets[new_bucket_id] = []
# put the new object into the corresponding bucket
self.buckets[new_bucket_id].append(obj)
enabled_buckets.add(new_bucket_id)
# increment the permanent size if necessary
add_as_permanent = {} # indexed by bucket id
for bucket_id in enabled_buckets:
add_as_permanent[bucket_id] = False
if as_permanent == 'all':
self.perm_end_pt[bucket_id] += ne
add_as_permanent[bucket_id] = True
elif as_permanent == 'first':
if self.perm_end_pt[bucket_id] == 0:
self.perm_end_pt[bucket_id] = ne
add_as_permanent[bucket_id] = True
# create new counters for usage if necessary
if self.save_usage and as_permanent != 'all':
new_count = torch.zeros((bs, ne), device=key.device, dtype=torch.float32)
new_life = torch.zeros((bs, ne), device=key.device, dtype=torch.float32) + 1e-7
# add the key to every bucket
for bucket_id in self.buckets:
if bucket_id not in enabled_buckets:
# if we are not adding new values to a bucket, we should skip it
continue
_add_last_dim(self.k, bucket_id, key, prepend=add_as_permanent[bucket_id])
_add_last_dim(self.s, bucket_id, shrinkage, prepend=add_as_permanent[bucket_id])
if not add_as_permanent[bucket_id]:
if self.save_selection:
_add_last_dim(self.e, bucket_id, selection)
if self.save_usage:
_add_last_dim(self.use_cnt, bucket_id, new_count)
_add_last_dim(self.life_cnt, bucket_id, new_life)
def update_bucket_usage(self, bucket_id: int, usage: torch.Tensor) -> None:
# increase all life count by 1
# increase use of indexed elements
if not self.save_usage:
return
usage = usage[:, self.perm_end_pt[bucket_id]:]
if usage.shape[-1] == 0:
# if there is no temporary memory, we don't need to update
return
self.use_cnt[bucket_id] += usage.view_as(self.use_cnt[bucket_id])
self.life_cnt[bucket_id] += 1
def sieve_by_range(self, bucket_id: int, start: int, end: int, min_size: int) -> None:
# keep only the temporary elements *outside* of this range (with some boundary conditions)
# the permanent elements are ignored in this computation
# i.e., concat (a[:start], a[end:])
# bucket with size <= min_size are not modified
assert start >= 0
assert end <= 0
object_ids = self.buckets[bucket_id]
bucket_num_elements = self.k[bucket_id].shape[-1] - self.perm_end_pt[bucket_id]
if bucket_num_elements <= min_size:
return
if end == 0:
# negative 0 would not work as the end index!
# effectively make the second part an empty slice
end = self.k[bucket_id].shape[-1] + 1
p_size = self.perm_end_pt[bucket_id]
start = start + p_size
k = self.k[bucket_id]
s = self.s[bucket_id]
if self.save_selection:
e = self.e[bucket_id]
if self.save_usage:
use_cnt = self.use_cnt[bucket_id]
life_cnt = self.life_cnt[bucket_id]
self.k[bucket_id] = torch.cat([k[:, :, :start], k[:, :, end:]], -1)
self.s[bucket_id] = torch.cat([s[:, :, :start], s[:, :, end:]], -1)
if self.save_selection:
self.e[bucket_id] = torch.cat([e[:, :, :start - p_size], e[:, :, end:]], -1)
if self.save_usage:
self.use_cnt[bucket_id] = torch.cat([use_cnt[:, :start - p_size], use_cnt[:, end:]], -1)
self.life_cnt[bucket_id] = torch.cat([life_cnt[:, :start - p_size], life_cnt[:, end:]],
-1)
for obj_id in object_ids:
v = self.v[obj_id]
self.v[obj_id] = torch.cat([v[:, :, :start], v[:, :, end:]], -1)
def remove_old_memory(self, bucket_id: int, max_len: int) -> None:
self.sieve_by_range(bucket_id, 0, -max_len, max_len)
def remove_obsolete_features(self, bucket_id: int, max_size: int) -> None:
# for long-term memory only
object_ids = self.buckets[bucket_id]
assert self.perm_end_pt[bucket_id] == 0 # permanent memory should be empty in LT memory
# normalize with life duration
usage = self.get_usage(bucket_id)
bs = usage.shape[0]
survivals = []
for bi in range(bs):
_, survived = torch.topk(usage[bi], k=max_size)
survivals.append(survived.flatten())
assert survived.shape[-1] == survivals[0].shape[-1]
self.k[bucket_id] = torch.stack(
[self.k[bucket_id][bi, :, survived] for bi, survived in enumerate(survivals)], 0)
self.s[bucket_id] = torch.stack(
[self.s[bucket_id][bi, :, survived] for bi, survived in enumerate(survivals)], 0)
if self.save_selection:
# Long-term memory does not store selection so this should not be needed
self.e[bucket_id] = torch.stack(
[self.e[bucket_id][bi, :, survived] for bi, survived in enumerate(survivals)], 0)
for obj_id in object_ids:
self.v[obj_id] = torch.stack(
[self.v[obj_id][bi, :, survived] for bi, survived in enumerate(survivals)], 0)
self.use_cnt[bucket_id] = torch.stack(
[self.use_cnt[bucket_id][bi, survived] for bi, survived in enumerate(survivals)], 0)
self.life_cnt[bucket_id] = torch.stack(
[self.life_cnt[bucket_id][bi, survived] for bi, survived in enumerate(survivals)], 0)
def get_usage(self, bucket_id: int) -> torch.Tensor:
# return normalized usage
if not self.save_usage:
raise RuntimeError('I did not count usage!')
else:
usage = self.use_cnt[bucket_id] / self.life_cnt[bucket_id]
return usage
def get_all_sliced(
self, bucket_id: int, start: int, end: int
) -> (torch.Tensor, torch.Tensor, torch.Tensor, Dict[int, torch.Tensor], torch.Tensor):
# return k, sk, ek, value, normalized usage in order, sliced by start and end
# this only queries the temporary memory
assert start >= 0
assert end <= 0
p_size = self.perm_end_pt[bucket_id]
start = start + p_size
if end == 0:
# negative 0 would not work as the end index!
k = self.k[bucket_id][:, :, start:]
sk = self.s[bucket_id][:, :, start:]
ek = self.e[bucket_id][:, :, start - p_size:] if self.save_selection else None
value = {obj_id: self.v[obj_id][:, :, start:] for obj_id in self.buckets[bucket_id]}
usage = self.get_usage(bucket_id)[:, start - p_size:] if self.save_usage else None
else:
k = self.k[bucket_id][:, :, start:end]
sk = self.s[bucket_id][:, :, start:end]
ek = self.e[bucket_id][:, :, start - p_size:end] if self.save_selection else None
value = {obj_id: self.v[obj_id][:, :, start:end] for obj_id in self.buckets[bucket_id]}
usage = self.get_usage(bucket_id)[:, start - p_size:end] if self.save_usage else None
return k, sk, ek, value, usage
def purge_except(self, obj_keep_idx: List[int]):
# purge certain objects from the memory except the one listed
obj_keep_idx = set(obj_keep_idx)
# remove objects that are not in the keep list from the buckets
buckets_to_remove = []
for bucket_id, object_ids in self.buckets.items():
self.buckets[bucket_id] = [obj_id for obj_id in object_ids if obj_id in obj_keep_idx]
if len(self.buckets[bucket_id]) == 0:
buckets_to_remove.append(bucket_id)
# remove object values that are not in the keep list
self.v = {k: v for k, v in self.v.items() if k in obj_keep_idx}
# remove buckets that are empty
for bucket_id in buckets_to_remove:
del self.buckets[bucket_id]
del self.k[bucket_id]
del self.s[bucket_id]
if self.save_selection:
del self.e[bucket_id]
if self.save_usage:
del self.use_cnt[bucket_id]
del self.life_cnt[bucket_id]
def clear_non_permanent_memory(self):
# clear all non-permanent memory
for bucket_id in self.buckets:
self.sieve_by_range(bucket_id, 0, 0, 0)
def get_v_size(self, obj_id: int) -> int:
return self.v[obj_id].shape[-1]
def size(self, bucket_id: int) -> int:
if bucket_id not in self.k:
return 0
else:
return self.k[bucket_id].shape[-1]
def perm_size(self, bucket_id: int) -> int:
return self.perm_end_pt[bucket_id]
def non_perm_size(self, bucket_id: int) -> int:
return self.size(bucket_id) - self.perm_size(bucket_id)
def engaged(self, bucket_id: Optional[int] = None) -> bool:
if bucket_id is None:
return len(self.buckets) > 0
else:
return bucket_id in self.buckets
@property
def num_objects(self) -> int:
return len(self.v)
@property
def key(self) -> Dict[int, torch.Tensor]:
return self.k
@property
def value(self) -> Dict[int, torch.Tensor]:
return self.v
@property
def shrinkage(self) -> Dict[int, torch.Tensor]:
return self.s
@property
def selection(self) -> Dict[int, torch.Tensor]:
return self.e
def __contains__(self, key):
return key in self.v
+453
View File
@@ -0,0 +1,453 @@
import logging
from omegaconf import DictConfig
from typing import List, Dict
import torch
from matanyone2.inference.object_manager import ObjectManager
from matanyone2.inference.kv_memory_store import KeyValueMemoryStore
from matanyone2.model.matanyone2 import MatAnyone2
from matanyone2.model.utils.memory_utils import get_similarity, do_softmax
log = logging.getLogger()
class MemoryManager:
"""
Manages all three memory stores and the transition between working/long-term memory
"""
def __init__(self, cfg: DictConfig, object_manager: ObjectManager):
self.object_manager = object_manager
self.sensory_dim = cfg.model.sensory_dim
self.top_k = cfg.top_k
self.chunk_size = cfg.chunk_size
self.save_aux = cfg.save_aux
self.use_long_term = cfg.use_long_term
self.count_long_term_usage = cfg.long_term.count_usage
# subtract 1 because the first-frame is now counted as "permanent memory"
# and is not counted towards max_mem_frames
# but we want to keep the hyperparameters consistent as before for the same behavior
if self.use_long_term:
self.max_mem_frames = cfg.long_term.max_mem_frames - 1
self.min_mem_frames = cfg.long_term.min_mem_frames - 1
self.num_prototypes = cfg.long_term.num_prototypes
self.max_long_tokens = cfg.long_term.max_num_tokens
self.buffer_tokens = cfg.long_term.buffer_tokens
else:
self.max_mem_frames = cfg.max_mem_frames - 1
# dimensions will be inferred from input later
self.CK = self.CV = None
self.H = self.W = None
# The sensory memory is stored as a dictionary indexed by object ids
# each of shape bs * C^h * H * W
self.sensory = {}
# a dictionary indexed by object ids, each of shape bs * T * Q * C
self.obj_v = {}
self.work_mem = KeyValueMemoryStore(save_selection=self.use_long_term,
save_usage=self.use_long_term)
if self.use_long_term:
self.long_mem = KeyValueMemoryStore(save_usage=self.count_long_term_usage)
self.config_stale = True
self.engaged = False
def update_config(self, cfg: DictConfig) -> None:
self.config_stale = True
self.top_k = cfg['top_k']
assert self.use_long_term == cfg.use_long_term, 'cannot update this'
assert self.count_long_term_usage == cfg.long_term.count_usage, 'cannot update this'
self.use_long_term = cfg.use_long_term
self.count_long_term_usage = cfg.long_term.count_usage
if self.use_long_term:
self.max_mem_frames = cfg.long_term.max_mem_frames - 1
self.min_mem_frames = cfg.long_term.min_mem_frames - 1
self.num_prototypes = cfg.long_term.num_prototypes
self.max_long_tokens = cfg.long_term.max_num_tokens
self.buffer_tokens = cfg.long_term.buffer_tokens
else:
self.max_mem_frames = cfg.max_mem_frames - 1
def _readout(self, affinity, v, uncert_mask=None) -> torch.Tensor:
# affinity: bs*N*HW
# v: bs*C*N or bs*num_objects*C*N
# returns bs*C*HW or bs*num_objects*C*HW
if len(v.shape) == 3:
# single object
if uncert_mask is not None:
return v @ affinity * uncert_mask
else:
return v @ affinity
else:
bs, num_objects, C, N = v.shape
v = v.view(bs, num_objects * C, N)
out = v @ affinity
if uncert_mask is not None:
uncert_mask = uncert_mask.flatten(start_dim=2).expand(-1, C, -1)
out = out * uncert_mask
return out.view(bs, num_objects, C, -1)
def _get_mask_by_ids(self, mask: torch.Tensor, obj_ids: List[int]) -> torch.Tensor:
# -1 because the mask does not contain the background channel
return mask[:, [self.object_manager.find_tmp_by_id(obj) - 1 for obj in obj_ids]]
def _get_sensory_by_ids(self, obj_ids: List[int]) -> torch.Tensor:
return torch.stack([self.sensory[obj] for obj in obj_ids], dim=1)
def _get_object_mem_by_ids(self, obj_ids: List[int]) -> torch.Tensor:
return torch.stack([self.obj_v[obj] for obj in obj_ids], dim=1)
def _get_visual_values_by_ids(self, obj_ids: List[int]) -> torch.Tensor:
# All the values that the object ids refer to should have the same shape
value = torch.stack([self.work_mem.value[obj] for obj in obj_ids], dim=1)
if self.use_long_term and obj_ids[0] in self.long_mem.value:
lt_value = torch.stack([self.long_mem.value[obj] for obj in obj_ids], dim=1)
value = torch.cat([lt_value, value], dim=-1)
return value
def read_first_frame(self, last_msk_value, pix_feat: torch.Tensor,
last_mask: torch.Tensor, network: MatAnyone2, uncert_output=None) -> Dict[int, torch.Tensor]:
"""
Read from all memory stores and returns a single memory readout tensor for each object
pix_feat: (1/2) x C x H x W
query_key: (1/2) x C^k x H x W
selection: (1/2) x C^k x H x W
last_mask: (1/2) x num_objects x H x W (at stride 16)
return a dict of memory readouts, indexed by object indices. Each readout is C*H*W
"""
h, w = pix_feat.shape[-2:]
bs = pix_feat.shape[0]
assert last_mask.shape[0] == bs
"""
Compute affinity and perform readout
"""
all_readout_mem = {}
buckets = self.work_mem.buckets
for bucket_id, bucket in buckets.items():
if self.chunk_size < 1:
object_chunks = [bucket]
else:
object_chunks = [
bucket[i:i + self.chunk_size] for i in range(0, len(bucket), self.chunk_size)
]
for objects in object_chunks:
this_sensory = self._get_sensory_by_ids(objects)
this_last_mask = self._get_mask_by_ids(last_mask, objects)
this_msk_value = self._get_visual_values_by_ids(objects) # (1/2)*num_objects*C*N
pixel_readout = network.pixel_fusion(pix_feat, last_msk_value, this_sensory,
this_last_mask)
this_obj_mem = self._get_object_mem_by_ids(objects).unsqueeze(2)
readout_memory, aux_features = network.readout_query(pixel_readout, this_obj_mem)
for i, obj in enumerate(objects):
all_readout_mem[obj] = readout_memory[:, i]
if self.save_aux:
aux_output = {
# 'sensory': this_sensory,
# 'pixel_readout': pixel_readout,
'q_logits': aux_features['logits'] if aux_features else None,
# 'q_weights': aux_features['q_weights'] if aux_features else None,
# 'p_weights': aux_features['p_weights'] if aux_features else None,
# 'attn_mask': aux_features['attn_mask'].float() if aux_features else None,
}
self.aux = aux_output
return all_readout_mem
def read(self, pix_feat: torch.Tensor, query_key: torch.Tensor, selection: torch.Tensor,
last_mask: torch.Tensor, network: MatAnyone2, uncert_output=None, last_msk_value=None, ti=None,
last_pix_feat=None, last_pred_mask=None) -> Dict[int, torch.Tensor]:
"""
Read from all memory stores and returns a single memory readout tensor for each object
pix_feat: (1/2) x C x H x W
query_key: (1/2) x C^k x H x W
selection: (1/2) x C^k x H x W
last_mask: (1/2) x num_objects x H x W (at stride 16)
return a dict of memory readouts, indexed by object indices. Each readout is C*H*W
"""
h, w = pix_feat.shape[-2:]
bs = pix_feat.shape[0]
assert query_key.shape[0] == bs
assert selection.shape[0] == bs
assert last_mask.shape[0] == bs
uncert_mask = uncert_output["mask"] if uncert_output is not None else None
query_key = query_key.flatten(start_dim=2) # bs*C^k*HW
selection = selection.flatten(start_dim=2) # bs*C^k*HW
"""
Compute affinity and perform readout
"""
all_readout_mem = {}
buckets = self.work_mem.buckets
for bucket_id, bucket in buckets.items():
if self.use_long_term and self.long_mem.engaged(bucket_id):
# Use long-term memory
long_mem_size = self.long_mem.size(bucket_id)
memory_key = torch.cat([self.long_mem.key[bucket_id], self.work_mem.key[bucket_id]],
-1)
shrinkage = torch.cat(
[self.long_mem.shrinkage[bucket_id], self.work_mem.shrinkage[bucket_id]], -1)
similarity = get_similarity(memory_key, shrinkage, query_key, selection)
affinity, usage = do_softmax(similarity,
top_k=self.top_k,
inplace=True,
return_usage=True)
"""
Record memory usage for working and long-term memory
"""
# ignore the index return for long-term memory
work_usage = usage[:, long_mem_size:]
self.work_mem.update_bucket_usage(bucket_id, work_usage)
if self.count_long_term_usage:
# ignore the index return for working memory
long_usage = usage[:, :long_mem_size]
self.long_mem.update_bucket_usage(bucket_id, long_usage)
else:
# no long-term memory
memory_key = self.work_mem.key[bucket_id]
shrinkage = self.work_mem.shrinkage[bucket_id]
similarity = get_similarity(memory_key, shrinkage, query_key, selection, uncert_mask=uncert_mask)
if self.use_long_term:
affinity, usage = do_softmax(similarity,
top_k=self.top_k,
inplace=True,
return_usage=True)
self.work_mem.update_bucket_usage(bucket_id, usage)
else:
affinity = do_softmax(similarity, top_k=self.top_k, inplace=True)
if self.chunk_size < 1:
object_chunks = [bucket]
else:
object_chunks = [
bucket[i:i + self.chunk_size] for i in range(0, len(bucket), self.chunk_size)
]
for objects in object_chunks:
this_sensory = self._get_sensory_by_ids(objects)
this_last_mask = self._get_mask_by_ids(last_mask, objects)
this_msk_value = self._get_visual_values_by_ids(objects) # (1/2)*num_objects*C*N
visual_readout = self._readout(affinity,
this_msk_value, uncert_mask).view(bs, len(objects), self.CV, h, w)
uncert_output = network.pred_uncertainty(last_pix_feat, pix_feat, last_pred_mask, visual_readout[:,0]-last_msk_value[:,0])
if uncert_output is not None:
uncert_prob = uncert_output["prob"].unsqueeze(1) # b n 1 h w
visual_readout = visual_readout*uncert_prob + last_msk_value*(1-uncert_prob)
pixel_readout = network.pixel_fusion(pix_feat, visual_readout, this_sensory,
this_last_mask)
this_obj_mem = self._get_object_mem_by_ids(objects).unsqueeze(2)
readout_memory, aux_features = network.readout_query(pixel_readout, this_obj_mem)
for i, obj in enumerate(objects):
all_readout_mem[obj] = readout_memory[:, i]
if self.save_aux:
aux_output = {
# 'sensory': this_sensory,
# 'pixel_readout': pixel_readout,
'q_logits': aux_features['logits'] if aux_features else None,
# 'q_weights': aux_features['q_weights'] if aux_features else None,
# 'p_weights': aux_features['p_weights'] if aux_features else None,
# 'attn_mask': aux_features['attn_mask'].float() if aux_features else None,
}
self.aux = aux_output
return all_readout_mem
def add_memory(self,
key: torch.Tensor,
shrinkage: torch.Tensor,
msk_value: torch.Tensor,
obj_value: torch.Tensor,
objects: List[int],
selection: torch.Tensor = None,
*,
as_permanent: bool = False) -> None:
# key: (1/2)*C*H*W
# msk_value: (1/2)*num_objects*C*H*W
# obj_value: (1/2)*num_objects*Q*C
# objects contains a list of object ids corresponding to the objects in msk_value/obj_value
bs = key.shape[0]
assert shrinkage.shape[0] == bs
assert msk_value.shape[0] == bs
assert obj_value.shape[0] == bs
self.engaged = True
if self.H is None or self.config_stale:
self.config_stale = False
self.H, self.W = msk_value.shape[-2:]
self.HW = self.H * self.W
# convert from num. frames to num. tokens
self.max_work_tokens = self.max_mem_frames * self.HW
if self.use_long_term:
self.min_work_tokens = self.min_mem_frames * self.HW
# key: bs*C*N
# value: bs*num_objects*C*N
key = key.flatten(start_dim=2)
shrinkage = shrinkage.flatten(start_dim=2)
self.CK = key.shape[1]
msk_value = msk_value.flatten(start_dim=3)
self.CV = msk_value.shape[2]
if selection is not None:
# not used in non-long-term mode
selection = selection.flatten(start_dim=2)
# insert object values into object memory
for obj_id, obj in enumerate(objects):
if obj in self.obj_v:
"""streaming average
each self.obj_v[obj] is (1/2)*num_summaries*(embed_dim+1)
first embed_dim keeps track of the sum of embeddings
the last dim keeps the total count
averaging in done inside the object transformer
incoming obj_value is (1/2)*num_objects*num_summaries*(embed_dim+1)
self.obj_v[obj] = torch.cat([self.obj_v[obj], obj_value[:, obj_id]], dim=0)
"""
last_acc = self.obj_v[obj][:, :, -1]
new_acc = last_acc + obj_value[:, obj_id, :, -1]
self.obj_v[obj][:, :, :-1] = (self.obj_v[obj][:, :, :-1] +
obj_value[:, obj_id, :, :-1])
self.obj_v[obj][:, :, -1] = new_acc
else:
self.obj_v[obj] = obj_value[:, obj_id]
# convert mask value tensor into a dict for insertion
msk_values = {obj: msk_value[:, obj_id] for obj_id, obj in enumerate(objects)}
self.work_mem.add(key,
msk_values,
shrinkage,
selection=selection,
as_permanent=as_permanent)
for bucket_id in self.work_mem.buckets.keys():
# long-term memory cleanup
if self.use_long_term:
# Do memory compressed if needed
if self.work_mem.non_perm_size(bucket_id) >= self.max_work_tokens:
# Remove obsolete features if needed
if self.long_mem.non_perm_size(bucket_id) >= (self.max_long_tokens -
self.num_prototypes):
self.long_mem.remove_obsolete_features(
bucket_id,
self.max_long_tokens - self.num_prototypes - self.buffer_tokens)
self.compress_features(bucket_id)
else:
# FIFO
self.work_mem.remove_old_memory(bucket_id, self.max_work_tokens)
def purge_except(self, obj_keep_idx: List[int]) -> None:
# purge certain objects from the memory except the one listed
self.work_mem.purge_except(obj_keep_idx)
if self.use_long_term and self.long_mem.engaged():
self.long_mem.purge_except(obj_keep_idx)
self.sensory = {k: v for k, v in self.sensory.items() if k in obj_keep_idx}
if not self.work_mem.engaged():
# everything is removed!
self.engaged = False
def compress_features(self, bucket_id: int) -> None:
# perform memory consolidation
prototype_key, prototype_value, prototype_shrinkage = self.consolidation(
*self.work_mem.get_all_sliced(bucket_id, 0, -self.min_work_tokens))
# remove consolidated working memory
self.work_mem.sieve_by_range(bucket_id,
0,
-self.min_work_tokens,
min_size=self.min_work_tokens)
# add to long-term memory
self.long_mem.add(prototype_key,
prototype_value,
prototype_shrinkage,
selection=None,
supposed_bucket_id=bucket_id)
def consolidation(self, candidate_key: torch.Tensor, candidate_shrinkage: torch.Tensor,
candidate_selection: torch.Tensor, candidate_value: Dict[int, torch.Tensor],
usage: torch.Tensor) -> (torch.Tensor, Dict[int, torch.Tensor], torch.Tensor):
# find the indices with max usage
bs = candidate_key.shape[0]
assert bs in [1, 2]
prototype_key = []
prototype_selection = []
for bi in range(bs):
_, max_usage_indices = torch.topk(usage[bi], k=self.num_prototypes, dim=-1, sorted=True)
prototype_indices = max_usage_indices.flatten()
prototype_key.append(candidate_key[bi, :, prototype_indices])
prototype_selection.append(candidate_selection[bi, :, prototype_indices])
prototype_key = torch.stack(prototype_key, dim=0)
prototype_selection = torch.stack(prototype_selection, dim=0)
"""
Potentiation step
"""
similarity = get_similarity(candidate_key, candidate_shrinkage, prototype_key,
prototype_selection)
affinity = do_softmax(similarity)
# readout the values
prototype_value = {k: self._readout(affinity, v) for k, v in candidate_value.items()}
# readout the shrinkage term
prototype_shrinkage = self._readout(affinity, candidate_shrinkage)
return prototype_key, prototype_value, prototype_shrinkage
def initialize_sensory_if_needed(self, sample_key: torch.Tensor, ids: List[int]):
for obj in ids:
if obj not in self.sensory:
# also initializes the sensory memory
bs, _, h, w = sample_key.shape
self.sensory[obj] = torch.zeros((bs, self.sensory_dim, h, w),
device=sample_key.device)
def update_sensory(self, sensory: torch.Tensor, ids: List[int]):
# sensory: 1*num_objects*C*H*W
for obj_id, obj in enumerate(ids):
self.sensory[obj] = sensory[:, obj_id]
def get_sensory(self, ids: List[int]):
# returns (1/2)*num_objects*C*H*W
return self._get_sensory_by_ids(ids)
def clear_non_permanent_memory(self):
self.work_mem.clear_non_permanent_memory()
if self.use_long_term:
self.long_mem.clear_non_permanent_memory()
def clear_sensory_memory(self):
self.sensory = {}
def clear_work_mem(self):
self.work_mem = KeyValueMemoryStore(save_selection=self.use_long_term,
save_usage=self.use_long_term)
def clear_obj_mem(self):
self.obj_v = {}
+24
View File
@@ -0,0 +1,24 @@
class ObjectInfo:
"""
Store meta information for an object
"""
def __init__(self, id: int):
self.id = id
self.poke_count = 0 # count number of detections missed
def poke(self) -> None:
self.poke_count += 1
def unpoke(self) -> None:
self.poke_count = 0
def __hash__(self):
return hash(self.id)
def __eq__(self, other):
if type(other) == int:
return self.id == other
return self.id == other.id
def __repr__(self):
return f'(ID: {self.id})'
+149
View File
@@ -0,0 +1,149 @@
from typing import Union, List, Dict
import torch
from matanyone2.inference.object_info import ObjectInfo
class ObjectManager:
"""
Object IDs are immutable. The same ID always represent the same object.
Temporary IDs are the positions of each object in the tensor. It changes as objects get removed.
Temporary IDs start from 1.
"""
def __init__(self):
self.obj_to_tmp_id: Dict[ObjectInfo, int] = {}
self.tmp_id_to_obj: Dict[int, ObjectInfo] = {}
self.obj_id_to_obj: Dict[int, ObjectInfo] = {}
self.all_historical_object_ids: List[int] = []
def _recompute_obj_id_to_obj_mapping(self) -> None:
self.obj_id_to_obj = {obj.id: obj for obj in self.obj_to_tmp_id}
def add_new_objects(
self, objects: Union[List[ObjectInfo], ObjectInfo,
List[int]]) -> (List[int], List[int]):
if not isinstance(objects, list):
objects = [objects]
corresponding_tmp_ids = []
corresponding_obj_ids = []
for obj in objects:
if isinstance(obj, int):
obj = ObjectInfo(id=obj)
if obj in self.obj_to_tmp_id:
# old object
corresponding_tmp_ids.append(self.obj_to_tmp_id[obj])
corresponding_obj_ids.append(obj.id)
else:
# new object
new_obj = ObjectInfo(id=obj.id)
# new object
new_tmp_id = len(self.obj_to_tmp_id) + 1
self.obj_to_tmp_id[new_obj] = new_tmp_id
self.tmp_id_to_obj[new_tmp_id] = new_obj
self.all_historical_object_ids.append(new_obj.id)
corresponding_tmp_ids.append(new_tmp_id)
corresponding_obj_ids.append(new_obj.id)
self._recompute_obj_id_to_obj_mapping()
assert corresponding_tmp_ids == sorted(corresponding_tmp_ids)
return corresponding_tmp_ids, corresponding_obj_ids
def delete_objects(self, obj_ids_to_remove: Union[int, List[int]]) -> None:
# delete an object or a list of objects
# re-sort the tmp ids
if isinstance(obj_ids_to_remove, int):
obj_ids_to_remove = [obj_ids_to_remove]
new_tmp_id = 1
total_num_id = len(self.obj_to_tmp_id)
local_obj_to_tmp_id = {}
local_tmp_to_obj_id = {}
for tmp_iter in range(1, total_num_id + 1):
obj = self.tmp_id_to_obj[tmp_iter]
if obj.id not in obj_ids_to_remove:
local_obj_to_tmp_id[obj] = new_tmp_id
local_tmp_to_obj_id[new_tmp_id] = obj
new_tmp_id += 1
self.obj_to_tmp_id = local_obj_to_tmp_id
self.tmp_id_to_obj = local_tmp_to_obj_id
self._recompute_obj_id_to_obj_mapping()
def purge_inactive_objects(self,
max_missed_detection_count: int) -> (bool, List[int], List[int]):
# remove tmp ids of objects that are removed
obj_id_to_be_deleted = []
tmp_id_to_be_deleted = []
tmp_id_to_keep = []
obj_id_to_keep = []
for obj in self.obj_to_tmp_id:
if obj.poke_count > max_missed_detection_count:
obj_id_to_be_deleted.append(obj.id)
tmp_id_to_be_deleted.append(self.obj_to_tmp_id[obj])
else:
tmp_id_to_keep.append(self.obj_to_tmp_id[obj])
obj_id_to_keep.append(obj.id)
purge_activated = len(obj_id_to_be_deleted) > 0
if purge_activated:
self.delete_objects(obj_id_to_be_deleted)
return purge_activated, tmp_id_to_keep, obj_id_to_keep
def tmp_to_obj_cls(self, mask) -> torch.Tensor:
# remap tmp id cls representation to the true object id representation
new_mask = torch.zeros_like(mask)
for tmp_id, obj in self.tmp_id_to_obj.items():
new_mask[mask == tmp_id] = obj.id
return new_mask
def get_tmp_to_obj_mapping(self) -> Dict[int, ObjectInfo]:
# returns the mapping in a dict format for saving it with pickle
return {obj.id: tmp_id for obj, tmp_id in self.tmp_id_to_obj.items()}
def realize_dict(self, obj_dict, dim=1) -> torch.Tensor:
# turns a dict indexed by obj id into a tensor, ordered by tmp IDs
output = []
for _, obj in self.tmp_id_to_obj.items():
if obj.id not in obj_dict:
raise NotImplementedError
output.append(obj_dict[obj.id])
output = torch.stack(output, dim=dim)
return output
def make_one_hot(self, cls_mask) -> torch.Tensor:
output = []
for _, obj in self.tmp_id_to_obj.items():
output.append(cls_mask == obj.id)
if len(output) == 0:
output = torch.zeros((0, *cls_mask.shape), dtype=torch.bool, device=cls_mask.device)
else:
output = torch.stack(output, dim=0)
return output
@property
def all_obj_ids(self) -> List[int]:
return [k.id for k in self.obj_to_tmp_id]
@property
def num_obj(self) -> int:
return len(self.obj_to_tmp_id)
def has_all(self, objects: List[int]) -> bool:
for obj in objects:
if obj not in self.obj_to_tmp_id:
return False
return True
def find_object_by_id(self, obj_id) -> ObjectInfo:
return self.obj_id_to_obj[obj_id]
def find_tmp_by_id(self, obj_id) -> int:
return self.obj_to_tmp_id[self.obj_id_to_obj[obj_id]]
+30
View File
@@ -0,0 +1,30 @@
import logging
from omegaconf import DictConfig
log = logging.getLogger()
def get_dataset_cfg(cfg: DictConfig):
dataset_name = cfg.dataset
data_cfg = cfg.datasets[dataset_name]
potential_overrides = [
'image_directory',
'mask_directory',
'json_directory',
'size',
'save_all',
'use_all_masks',
'use_long_term',
'mem_every',
]
for override in potential_overrides:
if cfg[override] is not None:
log.info(f'Overriding config {override} from {data_cfg[override]} to {cfg[override]}')
data_cfg[override] = cfg[override]
# escalte all potential overrides to the top-level config
if override in data_cfg:
cfg[override] = data_cfg[override]
return data_cfg