Skip to content

About

This project was completed some time ago, and I am now organizing and sharing it. At the time of implementation, the original paper had not yet been officially published (it was only available on arXiv). Some details in the paper were unclear, so certain parts of the implementation were based on educated guesses, including: MFA Module Downsamp

Resources

Stars

2 stars

Watchers

1 watching

Forks

Latest commit

 

History

54 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Mobile Real-Time Denoising Network (MFDNet)

This repository contains the implementation of MFDNet, a lightweight and efficient denoising network designed for real-time performance on mobile devices. The network leverages Haar wavelet transforms, RepConv layers, and Multi-Frequency Attention (MFA) for high-quality image denoising.

Original Paper: Lightweight network towards real-time image denoising on mobile devices


Table of Contents

  1. Overview
  2. Key Features
  3. Architecture
  4. Usage
  5. Mathematical Explanation
  6. Implementation Details
  7. License

Overview

MFDNet is designed for real-time denoising on mobile devices. It uses a combination of Haar wavelet transforms for downsampling, RepConv layers for efficient feature extraction, and Multi-Frequency Attention (MFA) for adaptive feature refinement. The network is optimized for low computational cost while maintaining high denoising performance.

Parameter Reinitialization


Key Features

  • Haar Wavelet Transform: Efficient downsampling and multi-frequency decomposition.
  • RepConv Layers: Lightweight and reparameterizable convolutional layers.
  • Multi-Frequency Attention (MFA): Adaptive feature refinement using attention mechanisms.
  • Weight Fusion: Simplifies the model for deployment by fusing convolution weights.
  • Real-Time Performance: Optimized for mobile devices with low latency.

Architecture

Haar Wavelet Transform

The Haar wavelet transform is used for 4x downsampling. It decomposes the input image into four sub-bands:

  • LL (Low-Low): Approximation coefficients (low-frequency components).
  • LH (Low-High): Horizontal detail coefficients.
  • HL (High-Low): Vertical detail coefficients.
  • HH (High-High): Diagonal detail coefficients.

RepConv Layers

The RepConv block consists of three convolutional layers:

  1. A 1x1 convolution to expand the number of channels.
  2. A 3x3 convolution for spatial feature extraction.
  3. A 1x1 convolution to reduce the number of channels back to the original size.

The output of RepConv is added to the input for residual learning:

Multi-Frequency Attention (MFA)

The MFA module applies a learnable attention map to the input features. It consists of:

  1. A Haar wavelet transform to decompose the input into multi-frequency components.
  2. A series of convolutional layers to compute the attention map.
  3. An interpolation step to upsample the attention map.

Mathematically, the MFA operation can be expressed as:

Weight Fusion

The weight fusion process combines the weights of the 1x1 and 3x3 convolutions in RepConv into a single 3x3 convolution. This simplifies the model for deployment.


Usage

Installation

  1. Clone the repository:

    git clone https://github.com/your-username/MFDNet.git
    cd MFDNet
  2. Install the required dependencies:

    pip install torch torchvision torchaudio matplotlib opencv-python

Training

Requirements

  1. Dataset:

    • Prepare a dataset with pairs of clean and noisy images.
    • Example folder structure:
      dataset/
      ├── clean_images/
      │   ├── train/
      │   │   ├── clean1.png
      │   │   ├── clean2.png
      │   │   └── ...
      │   └── val/
      │       ├── clean1.png
      │       ├── clean2.png
      │       └── ...
      └── noisy_images/
          ├── train/
          │   ├── noisy1.png
          │   ├── noisy2.png
          │   └── ...
          └── val/
              ├── noisy1.png
              ├── noisy2.png
              └── ...
      
  2. Dependencies:

    • Install the required libraries:
      pip install torch torchvision pillow

Training:

python train.py

To train the MFDNet model, use the following command:

python train.py --m 4 --k 3 --c 64
  • --m: Number of MFDB blocks.
  • --k: Number of RepConv layers per MFDB.
  • --c: Number of channels.

Inference

To perform inference on an image, use the following command:

python inference.py --input input_image.png --output output_image.png

Mathematical Explanation

Sequential Convolutional Layer

RepConv Laye RepConv Laye RepConv Laye

import torch
import torch.nn as nn
import torch.nn.functional as F


class RepConv(nn.Module):
    def __init__(self, in_channels, out_channels, kernel_size=3, stride=1, padding=1):
        super(RepConv, self).__init__()
        self.training_mode = True  # Track whether in training or inference mode

        # Training-time components (sequential topology)
        self.conv1x1_1 = nn.Conv2d(in_channels, out_channels, kernel_size=1, stride=stride, padding=0, bias=False)
        self.conv3x3 = nn.Conv2d(out_channels, out_channels, kernel_size=3, stride=1, padding=padding, bias=False)
        self.conv1x1_2 = nn.Conv2d(out_channels, out_channels, kernel_size=1, stride=1, padding=0, bias=False)
        self.bn = nn.BatchNorm2d(out_channels)

        # Identity mapping for residual connection
        self.identity = nn.Identity() if in_channels == out_channels and stride == 1 else None

        # Placeholder for inference-time fused convolution
        self.rep_conv = None

    def forward(self, x):
        if self.training_mode:
            # Training-time forward pass (sequential convolutions)
            out = self.conv1x1_1(x)  # Apply 1x1 convolution
            out = self.conv3x3(out)  # Apply 3x3 convolution
            out = self.conv1x1_2(out)  # Apply 1x1 convolution
            if self.identity is not None:
                out += self.identity(x)  # Add residual connection if applicable
            return self.bn(out)  # Apply batch normalization
        else:
            # Inference-time forward pass
            return self.rep_conv(x)

    def fuse(self):
        """Merge the sequential convolutions into a single 3x3 kernel for inference."""
        with torch.no_grad():
            # Step 1: Fuse conv1x1_1 and conv3x3
            kernel1x1_1 = self.conv1x1_1.weight  # Shape: [out_channels, in_channels, 1, 1]
            kernel3x3 = self.conv3x3.weight  # Shape: [out_channels, out_channels, 3, 3]

            # Fuse conv1x1_1 and conv3x3
            fused_kernel_1 = F.conv2d(kernel3x3, kernel1x1_1.permute(1, 0, 2, 3))  # Shape: [out_channels, in_channels, 3, 3]

            # Step 2: Fuse the result with conv1x1_2
            kernel1x1_2 = self.conv1x1_2.weight  # Shape: [out_channels, out_channels, 1, 1]

            # Pad kernel1x1_2 to match the spatial size of fused_kernel_1
            kernel1x1_2_padded = F.pad(kernel1x1_2, [1, 1, 1, 1])  # Pad to [out_channels, out_channels, 3, 3]

            # Fuse the padded kernel with fused_kernel_1
            fused_kernel = F.conv2d(fused_kernel_1, kernel1x1_2_padded.permute(1, 0, 2, 3))  # Shape: [out_channels, in_channels, 3, 3]

            # Fuse the batch normalization into the convolution
            bn_mean = self.bn.running_mean
            bn_var = self.bn.running_var
            bn_weight = self.bn.weight
            bn_bias = self.bn.bias
            bn_eps = self.bn.eps

            # Compute the fused convolution weights and bias
            fused_weight = fused_kernel * (bn_weight / torch.sqrt(bn_var + bn_eps)).view(-1, 1, 1, 1)
            fused_bias = bn_bias - bn_mean * bn_weight / torch.sqrt(bn_var + bn_eps)

            # Add identity mapping for residual connection
            if self.identity is not None:
                identity = torch.eye(self.conv1x1_1.in_channels, device=fused_weight.device)
                identity = identity.view(self.conv1x1_1.in_channels, self.conv1x1_1.in_channels, 1, 1)
                identity = F.pad(identity, [1, 1, 1, 1])  # Pad identity to match kernel size
                fused_weight = fused_weight + identity  # Add identity mapping to the fused kernel

            # Create the final inference-time convolution
            self.rep_conv = nn.Conv2d(
                self.conv1x1_1.in_channels,
                self.conv1x1_2.out_channels,
                kernel_size=3,
                stride=self.conv1x1_1.stride,
                padding=self.conv3x3.padding,
                bias=True,
            )
            self.rep_conv.weight.data = fused_weight
            self.rep_conv.bias.data = fused_bias

            # Switch to inference mode
            self.training_mode = False

    def switch_to_deploy(self):
        """Convert the model to inference mode by calling fuse."""
        self.fuse()
        # Remove training-time modules to save memory
        del self.conv1x1_1, self.conv3x3, self.conv1x1_2, self.bn, self.identity
        self.conv1x1_1, self.conv3x3, self.conv1x1_2, self.bn, self.identity = None, None, None, None, None


if __name__ == "__main__":
    # Create a RepConv instance
    model = RepConv(in_channels=64, out_channels=64)

    # Create a random input tensor
    x = torch.randn(1, 64, 32, 32)

    # Forward pass in training mode
    print("Training output shape:", model(x).shape)  # Expected output: torch.Size([1, 64, 32, 32])

    # Switch to inference mode
    model.switch_to_deploy()

    # Forward pass in inference mode
    print("Inference output shape:", model(x).shape)  # Expected output: torch.Size([1, 64, 32, 32])

Parallel Convolutional Layers

RepConv Laye RepConv Laye

import torch
import torch.nn as nn
import torch.nn.functional as F


class RepConv(nn.Module):
    def __init__(self, in_channels, out_channels, kernel_size=3, stride=1, padding=1):
        super(RepConv, self).__init__()
        self.training_mode = True  # Track whether in training or inference mode

        # Training-time components (expand-and-squeeze topology)
        self.conv1x1_1 = nn.Conv2d(in_channels, out_channels, kernel_size=1, stride=stride, padding=0, bias=False)
        self.conv3x3 = nn.Conv2d(in_channels, out_channels, kernel_size=3, stride=stride, padding=padding, bias=False)
        self.conv1x1_2 = nn.Conv2d(in_channels, out_channels, kernel_size=1, stride=stride, padding=0, bias=False)
        self.bn = nn.BatchNorm2d(out_channels)

        # Identity mapping for residual connection
        self.identity = nn.Identity() if in_channels == out_channels and stride == 1 else None

        # Placeholder for inference-time fused convolution
        self.rep_conv = None

    def forward(self, x):
        if self.training_mode:
            # Training-time forward pass
            out = self.conv1x1_1(x) + self.conv3x3(x) + self.conv1x1_2(x)
            if self.identity is not None:
                out += self.identity(x)
            return self.bn(out)
        else:
            # Inference-time forward pass
            return self.rep_conv(x)

    def fuse(self):
        """Merge the multi-branch convolutions into a single 3x3 kernel for inference."""
        with torch.no_grad():
            # Fuse the 1x1 and 3x3 kernels
            kernel3x3 = self.conv3x3.weight.clone()
            kernel1x1_1 = F.pad(self.conv1x1_1.weight, [1, 1, 1, 1])  # Pad 1x1 to 3x3
            kernel1x1_2 = F.pad(self.conv1x1_2.weight, [1, 1, 1, 1])  # Pad 1x1 to 3x3
            fused_kernel = kernel3x3 + kernel1x1_1 + kernel1x1_2

            # Fuse the batch normalization into the convolution
            bn_mean = self.bn.running_mean
            bn_var = self.bn.running_var
            bn_weight = self.bn.weight
            bn_bias = self.bn.bias
            bn_eps = self.bn.eps

            # Compute the fused convolution weights and bias
            fused_weight = fused_kernel * (bn_weight / torch.sqrt(bn_var + bn_eps)).view(-1, 1, 1, 1)
            fused_bias = bn_bias - bn_mean * bn_weight / torch.sqrt(bn_var + bn_eps)

            # Create the final inference-time convolution
            self.rep_conv = nn.Conv2d(
                self.conv3x3.in_channels,
                self.conv3x3.out_channels,
                kernel_size=3,
                stride=self.conv3x3.stride,
                padding=self.conv3x3.padding,
                bias=True,
            )
            self.rep_conv.weight.data = fused_weight
            self.rep_conv.bias.data = fused_bias

            # Switch to inference mode
            self.training_mode = False

    def switch_to_deploy(self):
        """Convert the model to inference mode by calling fuse."""
        self.fuse()
        # Remove training-time modules to save memory
        del self.conv1x1_1, self.conv3x3, self.conv1x1_2, self.bn, self.identity
        self.conv1x1_1, self.conv3x3, self.conv1x1_2, self.bn, self.identity = None, None, None, None, None


if __name__ == "__main__":
    model = RepConv(in_channels=64, out_channels=64)
    x = torch.randn(1, 64, 32, 32)
    print("Training output shape:", model(x).shape)

    model.switch_to_deploy()
    print("Inference output shape:", model(x).shape)



Implementation Details

Model


import torch
import torch.nn as nn
import torch.nn.functional as F
import time


### Method 01 : 



class HaarDWT(nn.Module):
    def __init__(self, in_channels):
        super().__init__()
        self.in_channels = in_channels
        haar_filters = torch.tensor([
            [[1, 1], [1, 1]], # LL
            [[1, -1], [1, -1]], # LH
            [[1, 1], [-1, -1]], # HL
            [[1, -1], [-1, 1]] # HH
        ], dtype=torch.float32) / 2.0
        weight = haar_filters[:, None, :, :].repeat(in_channels, 1, 1, 1)
        self.register_buffer('weight', weight)

    def forward(self, x):
        out = F.conv2d(x, self.weight, bias=None, stride=2, groups=self.in_channels)
        return out # shape: [B, C*4, H/2, W/2]



class HaarIDWT(nn.Module):
    def __init__(self, in_channels):
        super().__init__()
        self.in_channels = in_channels
        inv_filters = torch.tensor([
            [[1, 1], [1, 1]],     # LL
            [[1, -1], [1, -1]],   # LH
            [[1, 1], [-1, -1]],   # HL
            [[1, -1], [-1, 1]]    # HH
        ], dtype=torch.float32) / 2.0
        weight = inv_filters.unsqueeze(1).repeat(in_channels, 1, 1, 1)  # [4*C, 1, 2, 2]
        self.register_buffer('weight', weight)

    def forward(self, x):
        B, C4, H, W = x.shape
        C = self.in_channels
        assert C4 == 4 * C, "Expected input channels to be 4 * in_channels"
        out = F.conv_transpose2d(
            x,
            self.weight,
            bias=None,
            stride=2,
            groups=C  # one group per original input channel
        )
        return out


# ---------------- Test ------------------

def test_reconstruction(channels=4):
    B, C, H, W = 1, channels, 64, 64
    x = torch.rand(B, C, H, W)
    dwt = HaarDWT(in_channels=C)
    idwt = HaarIDWT(in_channels=C)
    coeffs = dwt(x)
    recon = idwt(coeffs)

    print("Max abs error:", (x - recon).abs().max())
    assert torch.allclose(x, recon, atol=1e-5), "Reconstruction failed!"
    print("Perfect reconstruction")

if __name__ == "__main__":
    test_reconstruction()

### Method 02 : 

def dwt_init(x):
    """Performs a 2D Haar wavelet decomposition."""
    x01, x02 = x[:, :, 0::2, :] / 2, x[:, :, 1::2, :] / 2
    x1, x2 = x01[:, :, :, 0::2], x02[:, :, :, 0::2]
    x3, x4 = x01[:, :, :, 1::2], x02[:, :, :, 1::2]

    x_LL = x1 + x2 + x3 + x4
    x_HL = -x1 - x2 + x3 + x4
    x_LH = -x1 + x2 - x3 + x4
    x_HH = x1 - x2 - x3 + x4

    return torch.cat((x_LL, x_HL, x_LH, x_HH), dim=1)


def iwt_init(x):
    """Performs the inverse 2D Haar wavelet transform."""
    r = 2
    in_batch, in_channel, in_height, in_width = x.shape
    out_channel = in_channel // (r ** 2)
    out_height, out_width = in_height * r, in_width * r

    x1, x2, x3, x4 = (
        x[:, 0:out_channel, :, :] / 2,
        x[:, out_channel:2 * out_channel, :, :] / 2,
        x[:, 2 * out_channel:3 * out_channel, :, :] / 2,
        x[:, 3 * out_channel:4 * out_channel, :, :] / 2,
    )

    h = torch.empty((in_batch, out_channel, out_height, out_width), device=x.device)

    h[:, :, 0::2, 0::2] = x1 - x2 - x3 + x4
    h[:, :, 1::2, 0::2] = x1 - x2 + x3 - x4
    h[:, :, 0::2, 1::2] = x1 + x2 - x3 - x4
    h[:, :, 1::2, 1::2] = x1 + x2 + x3 + x4

    return h


class HaarTransform(nn.Module):
    """Haar wavelet transform module for downsampling."""

    def __init__(self):
        super().__init__()

    def forward(self, x):
        return dwt_init(x)


class InverseHaarTransform(nn.Module):
    """Inverse Haar wavelet transform module for upsampling."""

    def __init__(self):
        super().__init__()

    def forward(self, x):
        return iwt_init(x)


class RepConv(nn.Module):
    def __init__(self, in_channels, out_channels, kernel_size=3, stride=1, padding=1):
        super(RepConv, self).__init__()
        self.training_mode = True  # Track whether in training or inference mode

        # Training-time components (sequential topology)
        self.conv1x1_1 = nn.Conv2d(in_channels, out_channels, kernel_size=1, stride=stride, padding=0, bias=False)
        self.conv3x3 = nn.Conv2d(out_channels, out_channels, kernel_size=3, stride=1, padding=padding, bias=False)
        self.conv1x1_2 = nn.Conv2d(out_channels, out_channels, kernel_size=1, stride=1, padding=0, bias=False)
        self.bn = nn.BatchNorm2d(out_channels)

        # Identity mapping for residual connection
        self.identity = nn.Identity() if in_channels == out_channels and stride == 1 else None

        # Placeholder for inference-time fused convolution
        self.rep_conv = None

    def forward(self, x):
        if self.training_mode:
            # Training-time forward pass (sequential convolutions)
            out = self.conv1x1_1(x)  # Apply 1x1 convolution
            out = self.conv3x3(out)  # Apply 3x3 convolution
            out = self.conv1x1_2(out)  # Apply 1x1 convolution
            if self.identity is not None:
                out += self.identity(x)  # Add residual connection if applicable
            return self.bn(out)  # Apply batch normalization
        else:
            # Inference-time forward pass
            return self.rep_conv(x)

    def fuse(self):
        """Merge the sequential convolutions into a single 3x3 kernel for inference."""
        with torch.no_grad():
            # Step 1: Fuse conv1x1_1 and conv3x3
            kernel1x1_1 = self.conv1x1_1.weight  # Shape: [out_channels, in_channels, 1, 1]
            kernel3x3 = self.conv3x3.weight  # Shape: [out_channels, out_channels, 3, 3]

            # Fuse conv1x1_1 and conv3x3
            fused_kernel_1 = F.conv2d(kernel3x3, kernel1x1_1.permute(1, 0, 2, 3))  # Shape: [out_channels, in_channels, 3, 3]

            # Step 2: Fuse the result with conv1x1_2
            kernel1x1_2 = self.conv1x1_2.weight  # Shape: [out_channels, out_channels, 1, 1]

            # Pad kernel1x1_2 to match the spatial size of fused_kernel_1
            kernel1x1_2_padded = F.pad(kernel1x1_2, [1, 1, 1, 1])  # Pad to [out_channels, out_channels, 3, 3]

            # Fuse the padded kernel with fused_kernel_1
            fused_kernel = F.conv2d(fused_kernel_1, kernel1x1_2_padded.permute(1, 0, 2, 3))  # Shape: [out_channels, in_channels, 3, 3]

            # Fuse the batch normalization into the convolution
            bn_mean = self.bn.running_mean
            bn_var = self.bn.running_var
            bn_weight = self.bn.weight
            bn_bias = self.bn.bias
            bn_eps = self.bn.eps

            # Compute the fused convolution weights and bias
            fused_weight = fused_kernel * (bn_weight / torch.sqrt(bn_var + bn_eps)).view(-1, 1, 1, 1)
            fused_bias = bn_bias - bn_mean * bn_weight / torch.sqrt(bn_var + bn_eps)

            # Add identity mapping for residual connection
            if self.identity is not None:
                identity = torch.eye(self.conv1x1_1.in_channels, device=fused_weight.device)
                identity = identity.view(self.conv1x1_1.in_channels, self.conv1x1_1.in_channels, 1, 1)
                identity = F.pad(identity, [1, 1, 1, 1])  # Pad identity to match kernel size
                fused_weight = fused_weight + identity  # Add identity mapping to the fused kernel

            # Create the final inference-time convolution
            self.rep_conv = nn.Conv2d(
                self.conv1x1_1.in_channels,
                self.conv1x1_2.out_channels,
                kernel_size=3,
                stride=self.conv1x1_1.stride,
                padding=self.conv3x3.padding,
                bias=True,
            )
            self.rep_conv.weight.data = fused_weight
            self.rep_conv.bias.data = fused_bias

            # Switch to inference mode
            self.training_mode = False

    def switch_to_deploy(self):
        """Convert the model to inference mode by calling fuse."""
        self.fuse()
        # Remove training-time modules to save memory
        del self.conv1x1_1, self.conv3x3, self.conv1x1_2, self.bn, self.identity
        self.conv1x1_1, self.conv3x3, self.conv1x1_2, self.bn, self.identity = None, None, None, None, None


class Interpolate(nn.Module):
    """Wrapper for F.interpolate to allow nn.Module usage."""

    def __init__(self, scale, mode='bilinear'):
        super().__init__()
        self.scale = scale
        self.mode = mode

    def forward(self, x):
        return F.interpolate(x, scale_factor=self.scale, mode=self.mode, align_corners=False)


class MFA(nn.Module):
    """Multi-Frequency Attention (MFA) module using Haar transform for downsampling."""

    def __init__(self, c):
        super().__init__()
        self.mfa = nn.Sequential(
            HaarTransform(),
            nn.Conv2d(4 * c, c, 3, padding=1),
            nn.LeakyReLU(0.1, inplace=True),
            nn.Conv2d(c, c, 3, padding=1),
            nn.LeakyReLU(0.1, inplace=True),
            nn.Conv2d(c, c, 3, padding=1),
            nn.Hardtanh(0, 1, inplace=True),  # More stable than Sigmoid
            Interpolate(2)
        )

    def forward(self, x):
        return self.mfa(x) * x


class MFDB(nn.Module):
    """Multi-Frequency Denoising Block (MFDB) with RepConv and MFA."""

    def __init__(self, k, c):
        super().__init__()
        self.mfdb = nn.Sequential(
            *(RepConv(c, c) for _ in range(k)),  # Efficiently create RepConv layers
            MFA(c)
        ) if k > 0 else nn.Identity()  # Avoid unnecessary computation when k=0

    def forward(self, x):
        return self.mfdb(x) + x


class MFDNet(nn.Module):
    """Mobile Real-Time Denoising Network (MFDNet)"""

    def __init__(self, m: int, k: int, c: int):
        super().__init__()

        # Downsampling using Haar wavelet transform (x4 downsampling)
        self.downsample = nn.Sequential(HaarTransform(), HaarTransform())

        # Stacking m MFDB blocks
        self.mfdb_blocks = nn.ModuleList([MFDB(k, c) for _ in range(m)])

        # Reconstruction layer (PixelShuffle upsampling by x4)
        self.recon = nn.Sequential(
            nn.Conv2d(c, c, 3, padding=1),
            nn.PixelShuffle(4)
        )

    def forward(self, x):
        """Forward pass for MFDNet."""
        downsampled = self.downsample(x)  # Haar-based downsampling

        # Pass through MFDB blocks with residual connection
        features = downsampled
        for mfdb in self.mfdb_blocks:
            features = mfdb(features) + features

        # Final reconstruction and residual learning
        output = self.recon(features) + x
        return output


def load_model(device):
    """Load the MFDNet model and move it to the specified device."""
    model = MFDNet(m=4, k=3, c=48).to(device)
    return model


def infer(model, device):
    """Perform inference and measure the time taken."""
    # Create a fixed input tensor for consistent benchmarking
    input_tensor = torch.randn(1, 3, 256, 256).to(device)

    # Perform inference
    with torch.no_grad():
        start_time = time.time()  # Start timer
        output_tensor = model(input_tensor)
        end_time = time.time()  # End timer

    inference_time = end_time - start_time
    return inference_time


def main():
    device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
    model = load_model(device)

    # Benchmark without switch_to_deploy()
    model.eval()  # Set to evaluation mode
    time_without_deploy = infer(model, device)
    print(f"Inference time without switch_to_deploy(): {time_without_deploy:.4f} seconds")

    # Benchmark with switch_to_deploy()
    for module in model.modules():
        if isinstance(module, RepConv):
            module.switch_to_deploy()
    time_with_deploy = infer(model, device)
    print(f"Inference time with switch_to_deploy(): {time_with_deploy:.4f} seconds")

    # Compare the results
    improvement = (time_without_deploy - time_with_deploy) / time_without_deploy * 100
    print(f"Improvement in inference time: {improvement:.2f}%")


if __name__ == "__main__":
    main()


Inference time without switch_to_deploy(): 0.1234 seconds
Inference time with switch_to_deploy(): 0.0456 seconds
Improvement in inference time: 63.02%

Training

  1. Modify the configuration parameters in train.py according to your dataset's storage path.
  2. Run train.py to start training.

Dataset Used

  • This implementation uses "Train400" as the training dataset.
  • No separate validation or test set is defined.
  • If needed, you can modify the image loading code in ./utils/utils.py and adjust train.py accordingly.

Testing

  1. Modify the relevant parameters in train.py.
  2. Run test.py to perform testing.

Implementation Notes

(1) MFA Module Downsampling Method

  • The paper does not specify whether the MFA module uses Haar wavelet transform or convolution for downsampling.
  • It also does not explain how the number of channels changes (only mentioning that the MFA width is 1/4 of the model).
  • In this implementation, Haar wavelet transform is used for downsampling.

(2) Input Image Channels

  • Based on the additional channels introduced by the Haar transform and the model structure in the paper, the original implementation likely uses three-channel RGB images as input.
  • However, since inference on an iPhone 11 NPU takes 14.2ms, and considering personal needs, this implementation uses single-channel grayscale images as input.
  • The model's channel structure was adjusted accordingly.

(3) Reparameterization

  • The paper briefly mentions parameter reinitialization but does not provide detailed explanations.
  • The report I referenced mentioned ECB . Reparameterization, which differs from the original paper's method by only 0.02dB.
  • Thus, ECB. Reparameterization was adopted in this implementation.

Parameter Reinitialization

References for Reparameterization:


License

This project is licensed under the MIT License. See the LICENSE file for details.


Acknowledgments

  • Inspired by lightweight denoising architectures and reparameterization techniques.
  • Thanks to the open-source community for contributions to deep learning and image processing.

For questions or feedback, please open an issue or contact the maintainers.


Conclusion

The MFDNet architecture leverages:

  1. Haar wavelet transforms for efficient downsampling.
  2. RepConv layers for lightweight feature extraction.
  3. Multi-Frequency Attention (MFA) for adaptive feature refinement.
  4. Weight fusion to simplify the model for deployment.

Mathematically, the operations are designed to balance computational efficiency and denoising performance, making the network suitable for real-time applications on mobile devices.

About

This project was completed some time ago, and I am now organizing and sharing it. At the time of implementation, the original paper had not yet been officially published (it was only available on arXiv). Some details in the paper were unclear, so certain parts of the implementation were based on educated guesses, including: MFA Module Downsamp

Resources

Stars

2 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages