Real zero-mean Gaussian white noise, independent of a clean signal $x$ and of known variance is added to $x$ producing a noisy signal $y.$ Discrete Fourier transform (DFT) $Y$ of the noisy signal is calculated by:
$$Y_k = \frac{1}{N}\sum_{n=0}^{N-1}e^{-i2\pi kn/N}y_n.\tag{1}$$
This is just for context, and we will define noise variance in the frequency domain, so the normalization (or lack thereof) is not important. Gaussian white noise in time domain is Gaussian white noise in frequency domain, see question: "What is the statistics of the discrete Fourier transform of white Gaussian noise?". Therefore we can write:
$$Y_k = X_k + Z_k,$$
where $X$ and $Z$ are the DFT's of the clean signal and noise, and $Z_k$ the noise bin that follows a circularly symmetric complex Gaussian distribution of variance $\sigma^2$. Each the real and the imaginary part of $Z_k$ independently follows a Gaussian distribution of variance $\frac{1}{2}\sigma^2$. We define signal-to-noise ratio (SNR) of bin $Y_k$ as:
$$\mathrm{SNR} = \frac{\sigma^2}{|X_k|^2}.$$
An attempt to reduce noise is then made by spectral subtraction, whereby the magnitude of each bin $Y_k$ is independently reduced while retaining the original phase (unless the bin value goes to zero in the magnitude reduction). The reduction forms an estimate $\widehat{|X_k|^2}$ of the square $|X_k|^2$ of the absolute value of each bin of the DFT of the clean signal:
$$\widehat{|X_k|^2} = |Y_k|^2 - \sigma^2,\tag{2}$$
where $\sigma^2$ is the known variance of noise in each DFT bin. For simplicty, we are not considering $k = 0,$ or $k = N/2$ for even $N$, which are special cases for real $x.$ At a low SNR, the formulation in (2) could sometimes result in negative $\widehat{| X_k|^2}.$ We can remove this problem by clamping the estimate to zero from below, redefining:
$$\widehat{|X_k|^2} = \max\left(|Y_k|^2 - \sigma^2,\,0\right).\tag{3}$$
Figure 1. Monte Carlo estimations with a sample size of $10^5,$ of: Solid: gain of sum of square error in estimating $|X_k|$ by $\widehat{|X_k|}$ as compared to estimating it with $|Y_k|,$
dashed: gain of sum of square error in estimating $|X_k|^2$ by $\widehat{|X_k|^2}$ as compared to estimating it with $|Y_k|^2,$ dotted: gain of sum of square error in estimating $X_k$ by $\widehat{|X_k|}e^{i\arg(Y_k)}$ as compared to estimating it with $Y_k.$ The definition of $\widehat{|X_k|^2}$ from (3) is used.
Question: Is there another estimate of $|X_k|$ or $|X_k|^2$ that improves upon (2) and (3) without relying on the distribution of $Y_k$?
I think the problem is equivalent to estimating the square of the parameter $\displaystyle{\nu_\mathrm{Rice}}$ of a Rice distribution (Fig. 2) with known parameter $\sigma_\mathrm{Rice} = \frac{\sqrt{2}}{2}\sigma,$ given a single observation.
Figure 2. Rice distribution is the distribution of the distance $R$ to origin from a point that follows a bivariate circularly symmetric normal distribution with an absolute value of the mean $\nu_\mathrm{Rice},$ variance $2\sigma_\mathrm{Rice}^2 = \sigma^2$ and component variance $\sigma_\mathrm{Rice}^2 = \frac{1}{2}\sigma^2.$
I found some literature that seems relevant:
- Jan Sijbers, Arnold J. den Dekker, Paul Scheunders and Dirk Van Dyck, "Maximum Likelihood estimation of Rician distribution parameters", IEEE Transactions on Medical Imaging (Volume: 17, Issue: 3, June 1998) (doi, pdf).
Python script A for estimator curves
This script can be extended for plotting estimator curves in the answers.
import numpy as np
from mpmath import mp
import matplotlib.pyplot as plt
def plot_est(ms, est_as):
fig = plt.figure(figsize=(4,4))
ax = fig.add_subplot(1, 1, 1)
if len(np.shape(est_as)) == 2:
for i in range(np.shape(est_as)[0]):
plt.plot(ms, est_as[i])
else:
plt.plot(ms, est_as)
plt.axis([ms[0], ms[-1], ms[0], ms[-1]])
if ms[-1]-ms[0] < 5:
ax.set_xticks(np.arange(np.int(ms[0]), np.int(ms[-1]) + 1, 1))
ax.set_yticks(np.arange(np.int(ms[0]), np.int(ms[-1]) + 1, 1))
plt.grid(True)
plt.xlabel('$m$')
h = plt.ylabel('$\hat a$')
h.set_rotation(0)
plt.show()
Python script B for Fig. 1
This script can be extended for error gain curves in the answers.
import math
import numpy as np
import matplotlib.pyplot as plt
def est_a_sub_fast(m):
if m > 1:
return np.sqrt(m*m - 1)
else:
return 0
def est_gain_SSE_a(est_a, a, N):
SSE = 0
SSE_ref = 0
for k in range(N): #Noise std. dev = 1, |X_k| = a
m = abs(complex(np.random.normal(a, np.sqrt(2)/2), np.random.normal(0, np.sqrt(2)/2)))
SSE += (a - est_a(m))**2
SSE_ref += (a - m)**2
return SSE/SSE_ref
def est_gain_SSE_a2(est_a, a, N):
SSE = 0
SSE_ref = 0
for k in range(N): #Noise std. dev = 1, |X_k| = a
m = abs(complex(np.random.normal(a, np.sqrt(2)/2), np.random.normal(0, np.sqrt(2)/2)))
SSE += (a**2 - est_a(m)**2)**2
SSE_ref += (a**2 - m**2)**2
return SSE/SSE_ref
def est_gain_SSE_complex(est_a, a, N):
SSE = 0
SSE_ref = 0
for k in range(N): #Noise std. dev = 1, X_k = a
Y = complex(np.random.normal(a, np.sqrt(2)/2), np.random.normal(0, np.sqrt(2)/2))
SSE += abs(a - est_a(abs(Y))*Y/abs(Y))**2
SSE_ref += abs(a - Y)**2
return SSE/SSE_ref
def plot_gains_SSE(as_dB, gains_SSE_a, gains_SSE_a2, gains_SSE_complex, color_number = 0):
colors = plt.rcParams['axes.prop_cycle'].by_key()['color']
fig = plt.figure(figsize=(7,4))
ax = fig.add_subplot(1, 1, 1)
if len(np.shape(gains_SSE_a)) == 2:
for i in range(np.shape(gains_SSE_a)[0]):
plt.plot(as_dB, gains_SSE_a[i], color=colors[i], )
plt.plot(as_dB, gains_SSE_a2[i], color=colors[i], linestyle='--')
plt.plot(as_dB, gains_SSE_complex[i], color=colors[i], linestyle=':')
else:
plt.plot(as_dB, gains_SSE_a, color=colors[color_number])
plt.plot(as_dB, gains_SSE_a2, color=colors[color_number], linestyle='--')
plt.plot(as_dB, gains_SSE_complex, color=colors[color_number], linestyle=':')
plt.grid(True)
plt.axis([as_dB[0], as_dB[-1], 0, 2])
plt.xlabel('SNR (dB)')
plt.ylabel('SSE gain')
plt.show()
as_dB = range(-40, 41)
as_ = [10**(a_dB/20) for a_dB in as_dB]
gains_SSE_a_sub = [est_gain_SSE_a(est_a_sub_fast, a, 10**5) for a in as_]
gains_SSE_a2_sub = [est_gain_SSE_a2(est_a_sub_fast, a, 10**5) for a in as_]
gains_SSE_complex_sub = [est_gain_SSE_complex(est_a_sub_fast, a, 10**5) for a in as_]
plot_gains_SSE(as_dB, gains_SSE_a_sub, gains_SSE_a2_sub, gains_SSE_complex_sub, 1)