基于JavaFX+JTransforms从零实现图像卷积遇到的频域相乘问题求助
基于JavaFX+JTransforms从零实现图像卷积遇到的频域相乘问题求助
兄弟我太懂你这种从头撸图像卷积的痛苦了!之前用DFT做卷积的时候也踩过一模一样的坑——频域相乘后结果全是方块artifact,图像几乎认不出来,当时头都大了!结合你说的情况,大概率是这几个关键步骤没处理对,给你捋捋:
最可能导致方块artifact的几个坑
1. 卷积核没做「零填充+FFT移位」
卷积定理要求输入图像和卷积核的尺寸必须完全一致,而且核的中心要对齐频域原点,不然做出来的是「循环卷积」而非我们需要的「线性卷积」,直接就会出现周期重叠的方块 artifact!
- 先把你的小核补零到和输入图像一样的宽高(w*h);
- 对补零后的核做fftshift(把核的中心从数组中间移到左上角)——DFT的频域原点在数组开头,不做移位的话,频域相乘等于循环卷积,必然出问题。
2. 复数相乘的规则搞错了
很多人会犯这个低级错误:直接把实部乘实部、虚部乘虚部,这完全不是复数乘法!正确的复数乘法应该是:
假设图像DFT的某点是 a + bi,核DFT的对应点是 c + di,相乘结果应该是:(a*c - b*d) + (a*d + b*c)i
一定要严格按照这个规则写循环,别偷懒!
3. IDFT后没做归一化
JTransforms的逆DFT不会自动帮你归一化,做完IDFT后必须把结果除以总像素数(w*h),不然像素值会溢出,要么全白要么出现奇怪的亮块。
给你补全关键步骤的代码示例
第一步:把JavaFX图像转成灰度复数数组
IntStream.range(0, w).forEach(i -> { IntStream.range(0, h).forEach(j -> { Color color = img.getPixelReader().getColor(i, j); // 转灰度(ITU-R BT.601标准) double gray = color.getRed() * 0.299 + color.getGreen() * 0.587 + color.getBlue() * 0.114; int idx = 2 * (j * w + i); input[idx] = gray; input[idx + 1] = 0.0; // 虚部初始为0 }); });
第二步:处理卷积核(补零+fftshift)
假设你的核是double[][] kernel,先转成和图像同尺寸的复数数组:
double[] kernelDFT = new double[w * h * 2]; int kernelW = kernel.length; int kernelH = kernel[0].length; int startX = (w - kernelW) / 2; int startY = (h - kernelH) / 2; // 把核放到补零数组的中心 for (int y = 0; y < kernelH; y++) { for (int x = 0; x < kernelW; x++) { int idx = 2 * ((startY + y) * w + (startX + x)); kernelDFT[idx] = kernel[y][x]; kernelDFT[idx + 1] = 0.0; } } // 对核做fftshift fftShift(kernelDFT, w, h);
这里的fftShift实现可以用这个:
private static void fftShift(double[] arr, int w, int h) { int halfW = w / 2; int halfH = h / 2; // 交换四个象限 swapQuadrants(arr, w, h, 0, 0, halfW, halfH, halfW, halfH); swapQuadrants(arr, w, h, halfW, 0, w - halfW, halfH, 0, halfH); swapQuadrants(arr, w, h, 0, halfH, halfW, h - halfH, halfW, 0); swapQuadrants(arr, w, h, halfW, halfH, w - halfW, h - halfH, 0, 0); } private static void swapQuadrants(double[] arr, int w, int h, int x1, int y1, int w1, int h1, int x2, int y2) { for (int y = 0; y < h1; y++) { for (int x = 0; x < w1; x++) { int idx1 = 2 * ((y1 + y) * w + (x1 + x)); int idx2 = 2 * ((y2 + y) * w + (x2 + x)); // 交换实部 double temp = arr[idx1]; arr[idx1] = arr[idx2]; arr[idx2] = temp; // 交换虚部 temp = arr[idx1 + 1]; arr[idx1 + 1] = arr[idx2 + 1]; arr[idx2 + 1] = temp; } } }
第三步:DFT+复数相乘+IDFT
// 初始化JTransforms的2D FFT DoubleFFT_2D fft = new DoubleFFT_2D(h, w); // 对图像和核做正变换 fft.complexForward(input); fft.complexForward(kernelDFT); // 复数元素-wise相乘 for (int k = 0; k < w * h * 2; k += 2) { double a = input[k]; double b = input[k + 1]; double c = kernelDFT[k]; double d = kernelDFT[k + 1]; // 正确的复数乘法 output[k] = a * c - b * d; output[k + 1] = a * d + b * c; } // 逆变换 fft.complexInverse(output, false); // false表示不自动归一化 // 归一化并写入结果图像 PixelWriter writer = res.getPixelWriter(); IntStream.range(0, w).forEach(i -> { IntStream.range(0, h).forEach(j -> { int idx = 2 * (j * w + i); // 取实部,除以总点数归一化, clamp到0-255 double val = Math.max(0, Math.min(255, output[idx] / (w * h))); writer.setColor(i, j, Color.gray(val / 255)); }); });
额外提醒
如果想得到和空间域完全一致的线性卷积结果,最好把图像和核都补零到w + kernelW -1、h + kernelH -1的尺寸(而不是原图像尺寸),做完IDFT后再裁剪回原图像的大小,这样能彻底避免边缘的循环卷积 artifact。
你可以先检查下自己的代码是不是没做这些步骤,调整后应该就能解决方块问题了!
备注:内容来源于stack exchange,提问作者Alejandro Quiroz
相关产品推荐
相关产品推荐

