diff --git a/src/plenoptic/simulate/canonical_computations/filters.py b/src/plenoptic/simulate/canonical_computations/filters.py index 4e4d804d..2575b19c 100644 --- a/src/plenoptic/simulate/canonical_computations/filters.py +++ b/src/plenoptic/simulate/canonical_computations/filters.py @@ -6,7 +6,7 @@ __all__ = ["gaussian1d", "circular_gaussian2d"] -def gaussian1d(kernel_size: int = 11, std: Union[float, Tensor] = 1.5) -> Tensor: +def gaussian1d(kernel_size: int = 11, std: Union[int, float, Tensor] = 1.5) -> Tensor: """Normalized 1D Gaussian. 1d Gaussian of size `kernel_size`, centered half-way, with variable std @@ -27,9 +27,15 @@ def gaussian1d(kernel_size: int = 11, std: Union[float, Tensor] = 1.5) -> Tensor filt: 1d Gaussian with `Size([kernel_size])`. """ - assert std > 0.0, "std must be positive" - if isinstance(std, float): - std = torch.as_tensor(std) + try: + dtype = std.dtype + except AttributeError: + dtype = torch.float32 + std = torch.as_tensor(std, dtype=dtype) + if std.numel() != 1: + raise ValueError("std must have only one element!") + if std <= 0: + raise ValueError("std must be positive!") device = std.device x = torch.arange(kernel_size).to(device) diff --git a/src/plenoptic/simulate/models/frontend.py b/src/plenoptic/simulate/models/frontend.py index a049a174..0880c9e0 100644 --- a/src/plenoptic/simulate/models/frontend.py +++ b/src/plenoptic/simulate/models/frontend.py @@ -32,6 +32,8 @@ class LinearNonlinear(nn.Module): """Linear-Nonlinear model, applies a difference of Gaussians filter followed by an activation function. Model is described in [1]_ and [2]_. + This model is called LN in Berardino et al. 2017 [1]_. + Parameters ---------- kernel_size: @@ -39,49 +41,64 @@ class LinearNonlinear(nn.Module): on_center: Dictates whether center is on or off; surround will be the opposite of center (i.e. on-off or off-on). - width_ratio_limit: - Sets a lower bound on the ratio of `surround_std` over `center_std`. - The surround Gaussian must be wider than the center Gaussian in order to be a - proper Difference of Gaussians. `surround_std` will be clamped to `ratio_limit` - times `center_std`. amplitude_ratio: Ratio of center/surround amplitude. Applied before filter normalization. pad_mode: Padding for convolution, defaults to "reflect". - + pretrained: + Whether or not to load model params from [3]_. See Notes for details. activation: Activation function following linear convolution. + cache_filt: + Whether or not to cache the filter. Avoids regenerating filt with each + forward pass. Attributes ---------- center_surround: nn.Module `CenterSurround` difference of Gaussians filter. + Notes + ----- + These 2 parameters (standard deviations) were taken from Table 2, page 149 + from [3]_ and are the values used [1]_. Please use these pretrained weights + at your own discretion. + References ---------- .. [1] A Berardino, J Ballé, V Laparra, EP Simoncelli, Eigen-distortions of hierarchical - representations, NeurIPS 2017; https://arxiv.org/abs/1710.02266 + representations, NeurIPS 2017; https://arxiv.org/abs/1710.02266 .. [2] https://www.cns.nyu.edu/~lcv/eigendistortions/ModelsIQA.html + .. [3] A Berardino, Hierarchically normalized models of visual distortion + sensitivity: Physiology, perception, and application; Ph.D. Thesis, + 2018; https://www.cns.nyu.edu/pub/lcv/berardino-phd.pdf """ def __init__( self, kernel_size: Union[int, Tuple[int, int]], on_center: bool = True, - width_ratio_limit: float = 4.0, amplitude_ratio: float = 1.25, pad_mode: str = "reflect", - + pretrained: bool = False, activation: Callable[[Tensor], Tensor] = F.softplus, + cache_filt: bool = False, ): super().__init__() + if pretrained: + assert kernel_size in [31, (31, 31)], "pretrained model has kernel_size (31, 31)" + if cache_filt is False: + warn("pretrained is True but cache_filt is False. Set cache_filt to " + "True for efficiency unless you are fine-tuning.") self.center_surround = CenterSurround( kernel_size, on_center, - width_ratio_limit, amplitude_ratio, pad_mode=pad_mode, + cache_filt=cache_filt, ) + if pretrained: + self.load_state_dict(self._pretrained_state_dict()) self.activation = activation def forward(self, x: Tensor) -> Tensor: @@ -110,11 +127,24 @@ def display_filters(self, zoom=5.0, **kwargs): return fig + @staticmethod + def _pretrained_state_dict() -> OrderedDict: + """Copied from Table 2 in Berardino, 2018""" + state_dict = OrderedDict( + [ + ("center_surround.center_std", torch.as_tensor([.5339])), + ("center_surround.surround_std", torch.as_tensor([6.148])), + ("center_surround.amplitude_ratio", torch.as_tensor([1.25])), + ] + ) + return state_dict class LuminanceGainControl(nn.Module): - """ Linear center-surround followed by luminance gain control and activation. + """Linear center-surround followed by luminance gain control and activation. Model is described in [1]_ and [2]_. + This model is called LG in Berardino et al. 2017 [1]_. + Parameters ---------- kernel_size: @@ -122,18 +152,17 @@ class LuminanceGainControl(nn.Module): on_center: Dictates whether center is on or off; surround will be the opposite of center (i.e. on-off or off-on). - width_ratio_limit: - Sets a lower bound on the ratio of `surround_std` over `center_std`. - The surround Gaussian must be wider than the center Gaussian in order to be a - proper Difference of Gaussians. `surround_std` will be clamped to `ratio_limit` - times `center_std`. amplitude_ratio: Ratio of center/surround amplitude. Applied before filter normalization. pad_mode: Padding for convolution, defaults to "reflect". - + pretrained: + Whether or not to load model params from [3]_. See Notes for details. activation: Activation function following linear convolution. + cache_filt: + Whether or not to cache the filter. Avoids regenerating filt with each + forward pass. Attributes ---------- @@ -144,32 +173,52 @@ class LuminanceGainControl(nn.Module): luminance_scalar: nn.Parameter Scale factor for luminance normalization. + Notes + ----- + These 4 parameters (standard deviations and scalar constants) were taken + from Table 2, page 149 from [3]_ and are the values used [1]_. Please use + these pretrained weights at your own discretion. + References ---------- .. [1] A Berardino, J Ballé, V Laparra, EP Simoncelli, Eigen-distortions of hierarchical representations, NeurIPS 2017; https://arxiv.org/abs/1710.02266 .. [2] https://www.cns.nyu.edu/~lcv/eigendistortions/ModelsIQA.html + .. [3] A Berardino, Hierarchically normalized models of visual distortion + sensitivity: Physiology, perception, and application; Ph.D. Thesis, + 2018; https://www.cns.nyu.edu/pub/lcv/berardino-phd.pdf """ def __init__( self, kernel_size: Union[int, Tuple[int, int]], on_center: bool = True, - width_ratio_limit: float = 4.0, amplitude_ratio: float = 1.25, pad_mode: str = "reflect", - + pretrained: bool = False, activation: Callable[[Tensor], Tensor] = F.softplus, + cache_filt: bool = False, ): super().__init__() + if pretrained: + assert kernel_size in [31, (31, 31)], "pretrained model has kernel_size (31, 31)" + if cache_filt is False: + warn("pretrained is True but cache_filt is False. Set cache_filt to " + "True for efficiency unless you are fine-tuning.") self.center_surround = CenterSurround( kernel_size, on_center, - width_ratio_limit, amplitude_ratio, pad_mode=pad_mode, + cache_filt=cache_filt, + ) + self.luminance = Gaussian( + kernel_size=kernel_size, + pad_mode=pad_mode, + cache_filt=cache_filt, ) - self.luminance = Gaussian(kernel_size=kernel_size) self.luminance_scalar = nn.Parameter(torch.rand(1) * 10) + if pretrained: + self.load_state_dict(self._pretrained_state_dict()) self.activation = activation def forward(self, x: Tensor) -> Tensor: @@ -209,11 +258,27 @@ def display_filters(self, zoom=5.0, **kwargs): return fig + @staticmethod + def _pretrained_state_dict() -> OrderedDict: + """Copied from Table 2 in Berardino, 2018""" + state_dict = OrderedDict( + [ + ("luminance_scalar", torch.as_tensor([14.95])), + ("center_surround.center_std", torch.as_tensor([1.962])), + ("center_surround.surround_std", torch.as_tensor([4.235])), + ("center_surround.amplitude_ratio", torch.as_tensor([1.25])), + ("luminance.std", torch.as_tensor([4.235])), + ] + ) + return state_dict + class LuminanceContrastGainControl(nn.Module): - """ Linear center-surround followed by luminance and contrast gain control, + """Linear center-surround followed by luminance and contrast gain control, and activation function. Model is described in [1]_ and [2]_. + This model is called LGG in Berardino et al. 2017 [1]_. + Parameters ---------- kernel_size: @@ -221,17 +286,17 @@ class LuminanceContrastGainControl(nn.Module): on_center: Dictates whether center is on or off; surround will be the opposite of center (i.e. on-off or off-on). - width_ratio_limit: - Sets a lower bound on the ratio of `surround_std` over `center_std`. - The surround Gaussian must be wider than the center Gaussian in order to be a - proper Difference of Gaussians. `surround_std` will be clamped to `ratio_limit` - times `center_std`. amplitude_ratio: Ratio of center/surround amplitude. Applied before filter normalization. pad_mode: Padding for convolution, defaults to "reflect". + pretrained: + Whether or not to load model params from [3]_. See Notes for details. activation: Activation function following linear convolution. + cache_filt: + Whether or not to cache the filter. Avoids regenerating filt with each + forward pass. Attributes ---------- @@ -246,38 +311,60 @@ class LuminanceContrastGainControl(nn.Module): contrast_scalar: nn.Parameter Scale factor for contrast normalization. + Notes + ----- + These 6 parameters (standard deviations and constants) were taken from + Table 2, page 149 from [3]_ and are the values used [1]_. Please use these + pretrained weights at your own discretion. + References ---------- .. [1] A Berardino, J Ballé, V Laparra, EP Simoncelli, Eigen-distortions of hierarchical representations, NeurIPS 2017; https://arxiv.org/abs/1710.02266 .. [2] https://www.cns.nyu.edu/~lcv/eigendistortions/ModelsIQA.html + .. [3] A Berardino, Hierarchically normalized models of visual distortion + sensitivity: Physiology, perception, and application; Ph.D. Thesis, + 2018; https://www.cns.nyu.edu/pub/lcv/berardino-phd.pdf """ def __init__( self, kernel_size: Union[int, Tuple[int, int]], on_center: bool = True, - width_ratio_limit: float = 4.0, amplitude_ratio: float = 1.25, pad_mode: str = "reflect", - + pretrained: bool = False, activation: Callable[[Tensor], Tensor] = F.softplus, + cache_filt: bool = False, ): super().__init__() - + if pretrained: + assert kernel_size in [31, (31, 31)], "pretrained model has kernel_size (31, 31)" + if cache_filt is False: + warn("pretrained is True but cache_filt is False. Set cache_filt to " + "True for efficiency unless you are fine-tuning.") self.center_surround = CenterSurround( kernel_size, on_center, - width_ratio_limit, amplitude_ratio, pad_mode=pad_mode, + cache_filt=cache_filt, + ) + self.luminance = Gaussian( + kernel_size=kernel_size, + pad_mode=pad_mode, + cache_filt=cache_filt, + ) + self.contrast = Gaussian( + kernel_size=kernel_size, + pad_mode=pad_mode, + cache_filt=cache_filt, ) - self.luminance = Gaussian(kernel_size) - self.contrast = Gaussian(kernel_size) self.luminance_scalar = nn.Parameter(torch.rand(1) * 10) self.contrast_scalar = nn.Parameter(torch.rand(1) * 10) - + if pretrained: + self.load_state_dict(self._pretrained_state_dict()) self.activation = activation def forward(self, x: Tensor) -> Tensor: @@ -321,22 +408,34 @@ def display_filters(self, zoom=5.0, **kwargs): return fig + @staticmethod + def _pretrained_state_dict() -> OrderedDict: + """Copied from Table 2 in Berardino, 2018""" + state_dict = OrderedDict( + [ + ("luminance_scalar", torch.as_tensor([2.94])), + ("contrast_scalar", torch.as_tensor([34.03])), + ("center_surround.center_std", torch.as_tensor([.7363])), + ("center_surround.surround_std", torch.as_tensor([48.37])), + ("center_surround.amplitude_ratio", torch.as_tensor([1.25])), + ("luminance.std", torch.as_tensor([170.99])), + ("contrast.std", torch.as_tensor([2.658])), + + ] + ) + return state_dict + class OnOff(nn.Module): """Two-channel on-off and off-on center-surround model with local contrast and luminance gain control. - This model is called OnOff in Berardino et al 2017. + This model is called OnOff in Berardino et al 2017 [1]_. Parameters ---------- kernel_size: Shape of convolutional kernel. - width_ratio_limit: - Sets a lower bound on the ratio of `surround_std` over `center_std`. - The surround Gaussian must be wider than the center Gaussian in order to be a - proper Difference of Gaussians. `surround_std` will be clamped to `ratio_limit` - times `center_std`. amplitude_ratio: Ratio of center/surround amplitude. Applied before filter normalization. pad_mode: @@ -356,41 +455,39 @@ class OnOff(nn.Module): Notes ----- - These 12 parameters (standard deviations & scalar constants) were reverse-engineered - from model from [1]_, [2]_. Please use these pretrained weights at your own - discretion. + These 12 parameters (standard deviations & scalar constants) were taken + from Table 2, page 149 from [3]_ and are the values used [1]_. Please use + these pretrained weights at your own discretion. References ---------- .. [1] A Berardino, J Ballé, V Laparra, EP Simoncelli, Eigen-distortions of - hierarchical representations, NeurIPS 2017; https://arxiv.org/abs/1710.02266 + hierarchical representations, NeurIPS 2017; https://arxiv.org/abs/1710.02266 .. [2] https://www.cns.nyu.edu/~lcv/eigendistortions/ModelsIQA.html + .. [3] A Berardino, Hierarchically normalized models of visual distortion + sensitivity: Physiology, perception, and application; Ph.D. Thesis, + 2018; https://www.cns.nyu.edu/pub/lcv/berardino-phd.pdf """ def __init__( self, kernel_size: Union[int, Tuple[int, int]], - width_ratio_limit: float = 4.0, amplitude_ratio: float = 1.25, pad_mode: str = "reflect", - pretrained=False, + pretrained: bool = False, activation: Callable[[Tensor], Tensor] = F.softplus, apply_mask: bool = False, cache_filt: bool = False, - ): super().__init__() - if isinstance(kernel_size, int): - kernel_size = (kernel_size, kernel_size) if pretrained: - assert kernel_size == (31, 31), "pretrained model has kernel_size (31, 31)" + assert kernel_size in [31, (31, 31)], "pretrained model has kernel_size (31, 31)" if cache_filt is False: warn("pretrained is True but cache_filt is False. Set cache_filt to " "True for efficiency unless you are fine-tuning.") self.center_surround = CenterSurround( kernel_size=kernel_size, - width_ratio_limit=width_ratio_limit, on_center=[True, False], amplitude_ratio=amplitude_ratio, out_channels=2, @@ -484,17 +581,16 @@ def display_filters(self, zoom=5.0, **kwargs): @staticmethod def _pretrained_state_dict() -> OrderedDict: - """Roughly interpreted from trained weights in Berardino et al 2017""" + """Copied from Table 2 in Berardino, 2018""" state_dict = OrderedDict( [ ("luminance_scalar", torch.as_tensor([3.2637, 14.3961])), ("contrast_scalar", torch.as_tensor([7.3405, 16.7423])), - ("center_surround.center_std", torch.as_tensor([1.15, 0.56])), - ("center_surround.surround_std", torch.as_tensor([5.0, 1.6])), + ("center_surround.center_std", torch.as_tensor([1.237, 0.3233])), + ("center_surround.surround_std", torch.as_tensor([30.12, 2.184])), ("center_surround.amplitude_ratio", torch.as_tensor([1.25])), - ("luminance.std", torch.as_tensor([8.7366, 1.4751])), - ("contrast.std", torch.as_tensor([2.7353, 1.5583])), - + ("luminance.std", torch.as_tensor([76.4, 2.184])), + ("contrast.std", torch.as_tensor([7.49, 2.43])), ] ) return state_dict diff --git a/src/plenoptic/simulate/models/naive.py b/src/plenoptic/simulate/models/naive.py index 16263abe..d760e393 100644 --- a/src/plenoptic/simulate/models/naive.py +++ b/src/plenoptic/simulate/models/naive.py @@ -66,7 +66,6 @@ def __init__( if isinstance(kernel_size, int): kernel_size = (kernel_size, kernel_size) - self.kernel_size = kernel_size self.pad_mode = pad_mode @@ -160,9 +159,7 @@ class CenterSurround(nn.Module): f &= amplitude_ratio * center - surround \\ f &= f/f.sum() - The signs of center and surround are determined by `center` argument. The standard - deviation of the surround Gaussian is constrained to be at least `width_ratio_limit` - times that of the center Gaussian. + The signs of center and surround are determined by ``on_center`` argument. Parameters ---------- @@ -173,11 +170,6 @@ class CenterSurround(nn.Module): (i.e. on-off or off-on). If List of bools, then list length must equal `out_channels`, if just a single bool, then all `out_channels` will be assumed to be all on-off or off-on. - width_ratio_limit: - Sets a lower bound on the ratio of `surround_std` over `center_std`. - The surround Gaussian must be wider than the center Gaussian in order to be a - proper Difference of Gaussians. `surround_std` will be clamped to `ratio_limit` - times `center_std`. amplitude_ratio: Ratio of center/surround amplitude. Applied before filter normalization. center_std: @@ -198,7 +190,6 @@ def __init__( self, kernel_size: Union[int, Tuple[int, int]], on_center: Union[bool, List[bool, ]] = True, - width_ratio_limit: float = 2.0, amplitude_ratio: float = 1.25, center_std: Union[float, Tensor] = 1.0, surround_std: Union[float, Tensor] = 4.0, @@ -219,13 +210,13 @@ def __init__( if isinstance(surround_std, float) or surround_std.shape == torch.Size([]): surround_std = torch.ones(out_channels) * surround_std assert len(center_std) == out_channels and len(surround_std) == out_channels, "stds must correspond to each out_channel" - assert width_ratio_limit > 1.0, "stdev of surround must be greater than center" assert amplitude_ratio >= 1.0, "ratio of amplitudes must at least be 1." self.on_center = on_center + if isinstance(kernel_size, int): + kernel_size = (kernel_size, kernel_size) self.kernel_size = kernel_size - self.width_ratio_limit = width_ratio_limit self.register_buffer("amplitude_ratio", torch.as_tensor(amplitude_ratio)) self.center_std = nn.Parameter(torch.ones(out_channels) * center_std) @@ -259,15 +250,8 @@ def filt(self) -> Tensor: self.register_buffer('_filt', filt) return filt - def _clamp_surround_std(self): - """Clamps surround standard deviation to ratio_limit times center_std""" - lower_bound = self.width_ratio_limit * self.center_std - for i, lb in enumerate(lower_bound): - self.surround_std[i].data = self.surround_std[i].data.clamp(min=float(lb)) - def forward(self, x: Tensor) -> Tensor: x = same_padding(x, self.kernel_size, pad_mode=self.pad_mode) - self._clamp_surround_std() # clip the surround stdev y = F.conv2d(x, self.filt, bias=None) return y diff --git a/src/plenoptic/tools/conv.py b/src/plenoptic/tools/conv.py index 70832efd..a9e35a2d 100644 --- a/src/plenoptic/tools/conv.py +++ b/src/plenoptic/tools/conv.py @@ -123,9 +123,9 @@ def _get_same_padding( def same_padding( x: Tensor, - kernel_size: Union[int, Tuple[int, int]], - stride: Union[int, Tuple[int, int]] = (1, 1), - dilation: Union[int, Tuple[int, int]] = (1, 1), + kernel_size: Tuple[int, int], + stride: Tuple[int, int] = (1, 1), + dilation: Tuple[int, int] = (1, 1), pad_mode: str = "circular", ) -> Tensor: """Pad a tensor so that 2D convolution will result in output with same dims.""" diff --git a/tests/test_metamers.py b/tests/test_metamers.py index d5abf4fa..5ba1e246 100644 --- a/tests/test_metamers.py +++ b/tests/test_metamers.py @@ -225,6 +225,6 @@ def test_stop_criterion(self, einstein_img, model): po.tools.set_seed(0) met = po.synth.Metamer(einstein_img, model) # takes different numbers of iter to converge on GPU and CPU - met.synthesize(max_iter=30, stop_criterion=1e-5, stop_iters_to_check=5) + met.synthesize(max_iter=35, stop_criterion=1e-5, stop_iters_to_check=5) assert abs(met.losses[-5]-met.losses[-1]) < 1e-5, "Didn't stop when hit criterion!" assert abs(met.losses[-6]-met.losses[-2]) > 1e-5, "Stopped after hit criterion!" diff --git a/tests/test_models.py b/tests/test_models.py index a20ee487..cf99e641 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -198,6 +198,22 @@ def test_frontend_display_filters(self, model): fig = model.display_filters() plt.close(fig) + @pytest.mark.parametrize("mdl", all_models) + def test_kernel_size(self, mdl, einstein_img): + kernel_size = 31 + if mdl == "frontend.LinearNonlinear": + model = po.simul.LinearNonlinear(kernel_size, pretrained=True).to(DEVICE) + model2 = po.simul.LinearNonlinear((kernel_size, kernel_size), pretrained=True).to(DEVICE) + elif mdl == "frontend.LuminanceGainControl": + model = po.simul.LuminanceGainControl(kernel_size, pretrained=True).to(DEVICE) + model2 = po.simul.LuminanceGainControl((kernel_size, kernel_size), pretrained=True).to(DEVICE) + elif mdl == "frontend.LuminanceContrastGainControl": + model = po.simul.LuminanceContrastGainControl(kernel_size, pretrained=True).to(DEVICE) + model2 = po.simul.LuminanceContrastGainControl((kernel_size, kernel_size), pretrained=True).to(DEVICE) + elif mdl == "frontend.OnOff": + model = po.simul.OnOff(kernel_size, pretrained=True).to(DEVICE) + model2 = po.simul.OnOff((kernel_size, kernel_size), pretrained=True).to(DEVICE) + assert torch.allclose(model(einstein_img), model2(einstein_img)), "Kernels somehow different!" class TestNaive(object): @@ -214,6 +230,21 @@ def test_gradient_flow(self, model): y = model(img) assert y.requires_grad + @pytest.mark.parametrize("mdl",["naive.Linear", "naive.Gaussian", "naive.CenterSurround"]) + def test_kernel_size(self, mdl, einstein_img): + kernel_size = 10 + if mdl == "naive.Gaussian": + model = po.simul.Gaussian(kernel_size, 1.).to(DEVICE) + model2 = po.simul.Gaussian((kernel_size, kernel_size), 1.).to(DEVICE) + elif mdl == "naive.Linear": + model = po.simul.Linear(kernel_size).to(DEVICE) + model2 = po.simul.Linear((kernel_size, kernel_size)).to(DEVICE) + elif mdl == "naive.CenterSurround": + model = po.simul.CenterSurround(kernel_size).to(DEVICE) + model2 = po.simul.CenterSurround((kernel_size, kernel_size)).to(DEVICE) + assert torch.allclose(model(einstein_img), model2(einstein_img)), "Kernels somehow different!" + + @pytest.mark.parametrize("mdl", ["naive.Gaussian", "naive.CenterSurround"]) @pytest.mark.parametrize("cache_filt", [False, True]) def test_cache_filt(self, cache_filt, mdl): @@ -988,12 +1019,18 @@ def test_circular_gaussian2d_wrong_std_length(self): circular_gaussian2d((7, 7), std, out_channels) @pytest.mark.parametrize("kernel_size", [5, 11, 20]) - @pytest.mark.parametrize("std", [1., 20., 0.]) - def test_gaussian1d(self, kernel_size, std): - if std <= 0: - with pytest.raises(AssertionError): - gaussian1d(kernel_size, std) - else: + @pytest.mark.parametrize("std,expectation", [ + (1., does_not_raise()), + (20., does_not_raise()), + (0., pytest.raises(ValueError, match="must be positive")), + (1, does_not_raise()), + ([1, 1], pytest.raises(ValueError, match="must have only one element")), + (torch.tensor(1), does_not_raise()), + (torch.tensor([1]), does_not_raise()), + (torch.tensor([1, 1]), pytest.raises(ValueError, match="must have only one element")), + ]) + def test_gaussian1d(self, kernel_size, std, expectation): + with expectation: filt = gaussian1d(kernel_size, std) assert filt.sum().isclose(torch.ones(1)) assert filt.shape == torch.Size([kernel_size])