metaview / src /PRoPE.py
multimodalart's picture
multimodalart HF Staff
Initial MetaView novel view synthesis demo
ccfee12 verified
Raw
History Blame Contribute Delete
23.3 kB
# MIT License
#
# Adapted from the official implementation of PRoPE
# "Cameras as Relative Positional Encoding" https://arxiv.org/pdf/2507.10496
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
from functools import partial
from typing import Callable, Optional, Tuple, List
import torch
import torch.nn.functional as F
from einops import rearrange
class PropeDotProductAttention(torch.nn.Module):
"""PRoPE attention with precomputed RoPE coefficients."""
coeffs_x_0: torch.Tensor
coeffs_x_1: torch.Tensor
coeffs_y_0: torch.Tensor
coeffs_y_1: torch.Tensor
def __init__(
self,
head_dim: int,
patches_x: int,
patches_y: int,
image_width: int,
image_height: int,
freq_base: float = 10000.0, # qwen 10000
freq_scale: float = 1.0,
dim_arrange = [16, 56, 56], # (frame ,height, width) default for qwen.
depth = None,
):
super().__init__()
self.head_dim = head_dim
self.patches_x = patches_x
self.patches_y = patches_y
self.image_width = image_width
self.image_height = image_height
self.freq_base = freq_base
self.freq_scale = freq_scale
self.use_PRoPE = False
self.dim_arrange = dim_arrange
# fit Qwen scale-rope
pos_index_x = torch.arange(patches_x)
neg_index_x = torch.arange(patches_x).flip(0) * -1 - 1
index_x = torch.cat([neg_index_x[-(patches_x - patches_x // 2) :], pos_index_x[: patches_x // 2]], dim=0)
# print(index_x)
## Qwen rope apply order is frame, height, width!
# coeffs_x
coeffs_y: Tuple[torch.Tensor, torch.Tensor] = _rope_precompute_coeffs( #
# torch.tile(torch.arange(patches_x), (patches_y,)),
torch.tile(index_x, (patches_y,)),
freq_base=freq_base,
freq_scale=freq_scale,
# feat_dim=head_dim // 4,
feat_dim=dim_arrange[2],
)
# fit Qwen scale-rope
pos_index_y = torch.arange(patches_y)
neg_index_y = torch.arange(patches_y).flip(0) * -1 - 1
index_y = torch.cat([neg_index_y[-(patches_y - patches_y // 2) :], pos_index_y[: patches_y // 2]], dim=0)
# print(index_y)
## Qwen rope apply order is frame, height, width!
# coeffs_y
coeffs_x: Tuple[torch.Tensor, torch.Tensor] = _rope_precompute_coeffs(
# torch.repeat_interleave(torch.arange(patches_y), patches_x),
torch.repeat_interleave(index_y, patches_x),
freq_base=freq_base,
freq_scale=freq_scale,
# feat_dim=head_dim // 4,
feat_dim=dim_arrange[1],
)
# Do not save coeffs to checkpoint as `cameras` might change during testing.
self.register_buffer("coeffs_x_0", coeffs_x[0], persistent=False)
self.register_buffer("coeffs_x_1", coeffs_x[1], persistent=False)
self.register_buffer("coeffs_y_0", coeffs_y[0], persistent=False)
self.register_buffer("coeffs_y_1", coeffs_y[1], persistent=False)
# override load_state_dict to not load coeffs if they exist (for backward compatibility)
def load_state_dict(self, state_dict, strict=True):
# remove coeffs from state_dict
state_dict.pop("coeffs_x_0", None)
state_dict.pop("coeffs_x_1", None)
state_dict.pop("coeffs_y_0", None)
state_dict.pop("coeffs_y_1", None)
super().load_state_dict(state_dict, strict)
def forward(
self,
q: torch.Tensor, # (batch, num_heads, seqlen, head_dim)
k: torch.Tensor, # (batch, num_heads, seqlen, head_dim)
v: torch.Tensor, # (batch, num_heads, seqlen, head_dim)
viewmats: torch.Tensor, # (batch, cameras, 4, 4)
Ks: Optional[torch.Tensor], # (batch, cameras, 3, 3)
**kwargs,
) -> torch.Tensor:
return prope_dot_product_attention(
q,
k,
v,
viewmats=viewmats,
Ks=Ks,
patches_x=self.patches_x,
patches_y=self.patches_y,
image_width=self.image_width,
image_height=self.image_height,
coeffs_x=(self.coeffs_x_0, self.coeffs_x_1),
coeffs_y=(self.coeffs_y_0, self.coeffs_y_1),
**kwargs,
)
def _precompute_and_cache_apply_fns(
self,
viewmats: torch.Tensor,
Ks: Optional[torch.Tensor],
depth = None,
):
(batch, cameras, _, _) = viewmats.shape
assert viewmats.shape == (batch, cameras, 4, 4)
assert Ks is None or Ks.shape == (batch, cameras, 3, 3)
self.cameras = cameras
self.use_PRoPE = True
self.apply_fn_q, self.apply_fn_kv, self.apply_fn_o = _prepare_apply_fns(
head_dim=self.head_dim,
viewmats=viewmats,
Ks=Ks,
patches_x=self.patches_x,
patches_y=self.patches_y,
image_width=self.image_width,
image_height=self.image_height,
coeffs_x=(self.coeffs_x_0, self.coeffs_x_1),
coeffs_y=(self.coeffs_y_0, self.coeffs_y_1),
dim_arrange=self.dim_arrange,
freq_base=self.freq_base,
freq_scale=self.freq_scale,
depth=depth
)
def _apply_to_q(self, q: torch.Tensor) -> torch.Tensor:
(batch, num_heads, seqlen, head_dim) = q.shape
# print("!!!", q.shape)
# print(self.cameras, self.patches_x, self.patches_y)
assert seqlen == self.cameras * self.patches_x * self.patches_y, f"seqlen:{seqlen}, {self.cameras}, {self.patches_x}, {self.patches_y}"
assert head_dim == self.head_dim
assert q.shape == (batch, num_heads, seqlen, head_dim)
assert self.apply_fn_q is not None
return self.apply_fn_q(q)
def _apply_to_kv(self, kv: torch.Tensor) -> torch.Tensor:
(batch, num_heads, seqlen, head_dim) = kv.shape
assert seqlen == self.cameras * self.patches_x * self.patches_y, f"seqlen:{seqlen}, {self.cameras}, {self.patches_x}, {self.patches_y}"
assert head_dim == self.head_dim
assert kv.shape == (batch, num_heads, seqlen, head_dim)
assert self.apply_fn_kv is not None
return self.apply_fn_kv(kv)
def _apply_to_o(self, o: torch.Tensor) -> torch.Tensor:
(batch, num_heads, seqlen, head_dim) = o.shape
assert seqlen == self.cameras * self.patches_x * self.patches_y
assert head_dim == self.head_dim
assert o.shape == (batch, num_heads, seqlen, head_dim)
assert self.apply_fn_o is not None
return self.apply_fn_o(o)
def prope_dot_product_attention(
q: torch.Tensor, # (batch, num_heads, seqlen, head_dim)
k: torch.Tensor, # (batch, num_heads, seqlen, head_dim)
v: torch.Tensor, # (batch, num_heads, seqlen, head_dim)
*,
viewmats: torch.Tensor, # (batch, cameras, 4, 4)
Ks: Optional[torch.Tensor], # (batch, cameras, 3, 3)
patches_x: int, # How many patches wide is each image?
patches_y: int, # How many patches tall is each image?
image_width: int, # Width of the image. Used to normalize intrinsics.
image_height: int, # Height of the image. Used to normalize intrinsics.
coeffs_x: Optional[Tuple[torch.Tensor, torch.Tensor]] = None,
coeffs_y: Optional[Tuple[torch.Tensor, torch.Tensor]] = None,
**kwargs,
) -> torch.Tensor:
"""Similar to torch.nn.functional.scaled_dot_product_attention, but applies PRoPE-style
positional encoding.
Currently, we assume that the sequence length is equal to:
cameras * patches_x * patches_y
And token ordering allows the `(seqlen,)` axis to be reshaped into
`(cameras, patches_x, patches_y)`.
"""
# We're going to assume self-attention: all inputs are the same shape.
(batch, num_heads, seqlen, head_dim) = q.shape
cameras = viewmats.shape[1]
assert q.shape == k.shape == v.shape
assert viewmats.shape == (batch, cameras, 4, 4)
assert Ks is None or Ks.shape == (batch, cameras, 3, 3)
assert seqlen == cameras * patches_x * patches_y
apply_fn_q, apply_fn_kv, apply_fn_o = _prepare_apply_fns(
head_dim=head_dim,
viewmats=viewmats,
Ks=Ks,
patches_x=patches_x,
patches_y=patches_y,
image_width=image_width,
image_height=image_height,
coeffs_x=coeffs_x,
coeffs_y=coeffs_y,
)
out = F.scaled_dot_product_attention(
query=apply_fn_q(q),
key=apply_fn_kv(k),
value=apply_fn_kv(v),
**kwargs,
)
out = apply_fn_o(out)
assert out.shape == (batch, num_heads, seqlen, head_dim)
return out
def _prepare_apply_fns(
head_dim: int, # Q/K/V will have this last dimension
viewmats: torch.Tensor, # (batch, cameras, 4, 4)
Ks: Optional[torch.Tensor], # (batch, cameras, 3, 3)
patches_x: int, # How many patches wide is each image?
patches_y: int, # How many patches tall is each image?
image_width: int, # Width of the image. Used to normalize intrinsics.
image_height: int, # Height of the image. Used to normalize intrinsics.
coeffs_x: Optional[torch.Tensor] = None,
coeffs_y: Optional[torch.Tensor] = None,
coeffs_z: Optional[torch.Tensor] = None,
dim_arrange = None,
freq_base = None,
freq_scale = None,
depth = None,
) -> Tuple[
Callable[[torch.Tensor], torch.Tensor],
Callable[[torch.Tensor], torch.Tensor],
Callable[[torch.Tensor], torch.Tensor],
]:
"""Prepare transforms for PRoPE-style positional encoding."""
device = viewmats.device
(batch, cameras, _, _) = viewmats.shape
viewmats = viewmats.to(torch.float32)
Ks = Ks.to(torch.float32)
# Normalize camera intrinsics.
if Ks is not None:
# Ks has been normalized in the dataset getitem !!
Ks_norm = Ks
# Compute the camera projection matrices we use in PRoPE.
# - K is an `image<-camera` transform.
# - viewmats is a `camera<-world` transform.
# - P = lift(K) @ viewmats is an `image<-world` transform.
P = torch.einsum("...ij,...jk->...ik", _lift_K(Ks_norm), viewmats)
P_T = P.transpose(-1, -2)
P_inv = torch.einsum(
"...ij,...jk->...ik",
_invert_SE3(viewmats),
_lift_K(_invert_K(Ks_norm)),
)
else:
# GTA formula. P is `camera<-world` transform.
P = viewmats
P_T = P.transpose(-1, -2)
P_inv = _invert_SE3(viewmats)
assert P.shape == P_inv.shape == (batch, cameras, 4, 4)
# Precompute cos/sin terms for RoPE. We use tiles/repeats for 'row-major'
# broadcasting.
assert coeffs_x is not None
if coeffs_x is None:
coeffs_x = _rope_precompute_coeffs(
torch.tile(torch.arange(patches_x, device=device), (patches_y * cameras,)),
freq_base=100.0,
freq_scale=1.0,
# feat_dim=head_dim // 4,
feat_dim=dim_arrange[1],
)
assert coeffs_y is not None
if coeffs_y is None:
coeffs_y = _rope_precompute_coeffs(
torch.tile(
torch.repeat_interleave(
torch.arange(patches_y, device=device), patches_x
),
(cameras,),
),
freq_base=100.0,
freq_scale=1.0,
# feat_dim=head_dim // 4,
feat_dim=dim_arrange[2],
)
if torch.isnan(P_inv).any():
print("!!P_inv has NaN!!!")
exit(0)
if torch.isnan(P_T).any():
print("!!P_T has NaN!!!")
exit(0)
if torch.isnan(coeffs_x[0]).any() or torch.isnan(coeffs_x[1]).any():
print("!!coeffs_x has NaN!!!")
exit(0)
if torch.isnan(coeffs_y[0]).any() or torch.isnan(coeffs_y[1]).any():
print("!!coeffs_y has NaN!!!")
exit(0)
# Block-diagonal transforms to the inputs and outputs of the attention operator.
assert head_dim % 4 == 0
transforms_q = [
(partial(_apply_tiled_projmat, matrix=P_T), dim_arrange[0]),
(partial(_rope_apply_coeffs, coeffs=coeffs_x), dim_arrange[1]),
(partial(_rope_apply_coeffs, coeffs=coeffs_y), dim_arrange[2]),
]
transforms_kv = [
(partial(_apply_tiled_projmat, matrix=P_inv), dim_arrange[0]),
(partial(_rope_apply_coeffs, coeffs=coeffs_x), dim_arrange[1]),
(partial(_rope_apply_coeffs, coeffs=coeffs_y), dim_arrange[2]),
]
transforms_o = [
(partial(_apply_tiled_projmat, matrix=P), dim_arrange[0]),
(partial(_rope_apply_coeffs, coeffs=coeffs_x, inverse=True), dim_arrange[1]),
(partial(_rope_apply_coeffs, coeffs=coeffs_y, inverse=True), dim_arrange[2]),
]
if len(dim_arrange) == 4:
index_z = rearrange(depth, 'b n h w -> b n (h w)') # (batch, frame, seq_len)
coeffs_z: Tuple[torch.Tensor, torch.Tensor] = _rope_precompute_coeffs_z(
index_z,
freq_base=freq_base,
freq_scale=freq_scale,
feat_dim=dim_arrange[3],
)
coeffs_z_0 = coeffs_z[0]
coeffs_z_1 = coeffs_z[1]
coeffs_z = (coeffs_z_0, coeffs_z_1)
transforms_q += [(partial(_rope_apply_coeffs_z, coeffs=coeffs_z), dim_arrange[3])]
transforms_kv += [(partial(_rope_apply_coeffs_z, coeffs=coeffs_z), dim_arrange[3])]
transforms_o += [(partial(_rope_apply_coeffs_z, coeffs=coeffs_z, inverse=True), dim_arrange[3])]
apply_fn_q = partial(_apply_block_diagonal, func_size_pairs=transforms_q)
apply_fn_kv = partial(_apply_block_diagonal, func_size_pairs=transforms_kv)
apply_fn_o = partial(_apply_block_diagonal, func_size_pairs=transforms_o)
return apply_fn_q, apply_fn_kv, apply_fn_o
def _apply_tiled_projmat(
feats: torch.Tensor, # (batch, num_heads, seqlen, feat_dim)
matrix: torch.Tensor, # (batch, cameras, D, D)
) -> torch.Tensor:
"""Apply projection matrix to features."""
# - seqlen => (cameras, patches_x * patches_y)
# - feat_dim => (feat_dim // 4, 4)
matrix = matrix.to(feats.dtype)
(batch, num_heads, seqlen, feat_dim) = feats.shape
cameras = matrix.shape[1]
assert seqlen > cameras and seqlen % cameras == 0
D = matrix.shape[-1]
assert matrix.shape == (batch, cameras, D, D)
assert feat_dim % D == 0
# print(matrix.device, feats.device)
return torch.einsum(
"bcij,bncpkj->bncpki",
matrix,
feats.reshape((batch, num_heads, cameras, -1, feat_dim // D, D)),
).reshape(feats.shape)
def _rope_apply_coeffs_z(
feats: torch.Tensor, # (batch, num_heads, seqlen_total, feat_dim)
coeffs: Tuple[torch.Tensor, torch.Tensor], # (batch, 1, frame, seqlen_img, num_freqs)
inverse: bool = False,
) -> torch.Tensor:
"""Apply RoPE coefficients to features. We adopt a 'split' ordering
convention. (in contrast to 'interleaved')"""
# print("Inject z rope!!")
#TODO change to interleaved same as Qwen?
cos, sin = coeffs
batch, num_heads, total_seq_len, feat_dim = feats.shape
_, __, frames, seq_len_per_img, num_freqs = cos.shape
cos = cos.to(feats.dtype)
sin = sin.to(feats.dtype)
# We allow (cos, sin) to be either with shape (1, 1, seqlen, feat_dim // 2),
# or (1, 1, seqlen_per_image, feat_dim // 2) and we repeat it to
# match the shape of feats.
feats = feats.reshape((batch, num_heads, frames, seq_len_per_img, feat_dim))
assert feats.shape[3] * frames == total_seq_len
# if cos.shape[2] != feats.shape[2]:
# n_repeats = feats.shape[2] // cos.shape[2]
# cos = cos.repeat(1, 1, n_repeats, 1)
# sin = sin.repeat(1, 1, n_repeats, 1)
assert len(cos.shape) == len(sin.shape) == len(feats.shape) == 5
assert cos.shape[-1] == sin.shape[-1] == feats.shape[-1] // 2
# cos (batch, 1, frame, seqlen_img, feat_dim)
x_in = feats[..., ::2] # even # (batch, num_heads, frames, seqlen_img, feat_dim)
y_in = feats[..., 1::2]
if inverse == False: # for qkv
x_out = cos * x_in - sin * y_in # broadcast on "num_heads"
y_out = sin * x_in + cos * y_in
else: # for out
x_out = cos * x_in + sin * y_in
y_out = -sin * x_in + cos * y_in
res = torch.stack((x_out, y_out), dim=-1).flatten(start_dim=-2)
res = rearrange(res, 'b n f s d -> b n (f s) d')
# print(res.shape)
return res
def _rope_precompute_coeffs_z(
positions: torch.Tensor, # (batch, frame, seq_len)
freq_base: float,
freq_scale: float,
feat_dim: int,
) -> Tuple[torch.Tensor, torch.Tensor]:
"""Precompute RoPE coefficients."""
assert len(positions.shape) == 3
assert feat_dim % 2 == 0
num_freqs = feat_dim // 2
freqs = freq_scale * (
freq_base
** (
-torch.arange(num_freqs, device=positions.device)[None, None, None, :]
/ num_freqs
)
)
# print(freqs.shape)
# print(positions[:128])
angles = positions[:, None, :, :, None] * freqs
# Shape should be: `(batch, num_heads, frame, seqlen, num_freqs)`; we're
# broadcasting across `num_heads`.
assert angles.shape == (positions.shape[0], 1, positions.shape[1], positions.shape[2], num_freqs)
return torch.cos(angles), torch.sin(angles)
def _rope_precompute_coeffs(
positions: torch.Tensor, # (seqlen,)
freq_base: float,
freq_scale: float,
feat_dim: int,
) -> Tuple[torch.Tensor, torch.Tensor]:
"""Precompute RoPE coefficients."""
assert len(positions.shape) == 1
assert feat_dim % 2 == 0
num_freqs = feat_dim // 2
freqs = freq_scale * (
freq_base
** (
-torch.arange(num_freqs, device=positions.device)[None, None, None, :]
/ num_freqs
)
)
# print(freqs.shape)
# print(positions[:128])
angles = positions[None, None, :, None] * freqs
# Shape should be: `(batch, num_heads, seqlen, num_freqs)`; we're
# broadcasting across `batch` and `num_heads`.
assert angles.shape == (1, 1, positions.shape[0], num_freqs)
return torch.cos(angles), torch.sin(angles)
if __name__ == '__main__':
patches_x = 64
patches_y = 64
freq_base = 1
freq_scale = 10000
head_dim = 128
pos_index = torch.arange(patches_x)
neg_index = torch.arange(patches_x).flip(0) * -1 - 1
index = torch.cat([neg_index[-(patches_x - patches_x // 2) :], pos_index[: patches_x // 2]], dim=0)
print(index)
print(torch.arange(patches_x))
coeffs_x: Tuple[torch.Tensor, torch.Tensor] = _rope_precompute_coeffs(
# torch.tile(torch.arange(patches_x), (patches_y,)),
torch.tile(index, (patches_y,)),
freq_base=freq_base,
freq_scale=freq_scale,
# feat_dim=head_dim // 4,
feat_dim=56,
)
coeffs_y: Tuple[torch.Tensor, torch.Tensor] = _rope_precompute_coeffs(
torch.repeat_interleave(torch.arange(patches_y), patches_x),
freq_base=freq_base,
freq_scale=freq_scale,
# feat_dim=head_dim // 4,
feat_dim=56,
)
def _rope_apply_coeffs(
feats: torch.Tensor, # (batch, num_heads, seqlen, feat_dim)
coeffs: Tuple[torch.Tensor, torch.Tensor],
inverse: bool = False,
) -> torch.Tensor:
"""Apply RoPE coefficients to features. We adopt a 'split' ordering
convention. (in contrast to 'interleaved')"""
#TODO change to interleaved same as Qwen?
cos, sin = coeffs
cos = cos.to(feats.dtype)
sin = sin.to(feats.dtype)
# We allow (cos, sin) to be either with shape (1, 1, seqlen, feat_dim // 2),
# or (1, 1, seqlen_per_image, feat_dim // 2) and we repeat it to
# match the shape of feats.
if cos.shape[2] != feats.shape[2]:
n_repeats = feats.shape[2] // cos.shape[2]
cos = cos.repeat(1, 1, n_repeats, 1)
sin = sin.repeat(1, 1, n_repeats, 1)
assert len(feats.shape) == len(cos.shape) == len(sin.shape) == 4
assert cos.shape[-1] == sin.shape[-1] == feats.shape[-1] // 2
x_in = feats[..., ::2] # even # (batch, num_heads, seqlen, feat_dim)
y_in = feats[..., 1::2]
if inverse == False: # for qkv
x_out = cos * x_in - sin * y_in
y_out = sin * x_in + cos * y_in
else: # for out
x_out = cos * x_in + sin * y_in
y_out = -sin * x_in + cos * y_in
res = torch.stack((x_out, y_out), dim=-1).flatten(start_dim=-2)
# print(res.shape)
return res
def _apply_block_diagonal(
feats: torch.Tensor, # (..., dim)
func_size_pairs: List[Tuple[Callable[[torch.Tensor], torch.Tensor], int]],
) -> torch.Tensor:
"""Apply a block-diagonal function to an input array.
Each function is specified as a tuple with form:
((Tensor) -> Tensor, int)
Where the integer is the size of the input to the function.
"""
funcs, block_sizes = zip(*func_size_pairs)
assert feats.shape[-1] == sum(block_sizes)
x_blocks = torch.split(feats, block_sizes, dim=-1)
out = torch.cat(
[f(x_block) for f, x_block in zip(funcs, x_blocks)],
dim=-1,
)
assert out.shape == feats.shape, "Input/output shapes should match."
return out
def _invert_SE3(transforms: torch.Tensor) -> torch.Tensor:
"""Invert a 4x4 SE(3) matrix."""
assert transforms.shape[-2:] == (4, 4)
Rinv = transforms[..., :3, :3].transpose(-1, -2)
out = torch.zeros_like(transforms)
out[..., :3, :3] = Rinv
out[..., :3, 3] = -torch.einsum("...ij,...j->...i", Rinv, transforms[..., :3, 3])
out[..., 3, 3] = 1.0
return out
def _lift_K(Ks: torch.Tensor) -> torch.Tensor:
"""Lift 3x3 matrices to homogeneous 4x4 matrices."""
assert Ks.shape[-2:] == (3, 3)
out = torch.zeros(Ks.shape[:-2] + (4, 4), device=Ks.device)
out[..., :3, :3] = Ks
out[..., 3, 3] = 1.0
return out
def _invert_K(Ks: torch.Tensor) -> torch.Tensor:
"""Invert 3x3 intrinsics matrices. Assumes no skew."""
assert Ks.shape[-2:] == (3, 3)
out = torch.zeros_like(Ks)
out[..., 0, 0] = 1.0 / Ks[..., 0, 0]
out[..., 1, 1] = 1.0 / Ks[..., 1, 1]
out[..., 0, 2] = -Ks[..., 0, 2] / Ks[..., 0, 0]
out[..., 1, 2] = -Ks[..., 1, 2] / Ks[..., 1, 1]
out[..., 2, 2] = 1.0
return out