├── images ├── result.png ├── example.png ├── example-mask.png ├── tubingen_orig.png ├── example-spectrum.png └── tubingen_result.png ├── LICENSE ├── README.md └── descreen.py /images/result.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/6o6o/fft-descreen/HEAD/images/result.png -------------------------------------------------------------------------------- /images/example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/6o6o/fft-descreen/HEAD/images/example.png -------------------------------------------------------------------------------- /images/example-mask.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/6o6o/fft-descreen/HEAD/images/example-mask.png -------------------------------------------------------------------------------- /images/tubingen_orig.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/6o6o/fft-descreen/HEAD/images/tubingen_orig.png -------------------------------------------------------------------------------- /images/example-spectrum.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/6o6o/fft-descreen/HEAD/images/example-spectrum.png -------------------------------------------------------------------------------- /images/tubingen_result.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/6o6o/fft-descreen/HEAD/images/tubingen_result.png -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Bogdan Boyko 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Fourier transform descreen filter 2 | 3 | An implementation of GIMP [descreen plugin](http://web.archive.org/web/20161118075437/http://registry.gimp.org/node/24411) in python with OpenCV. It utilizes a custom normalization of magnitude spectrum, found in [fft plugin](http://web.archive.org/web/20161118081031/http://registry.gimp.org/node/19596), which assigns more energy to pixels further away from the center, thus allowing to use regular binary threshold to localize high frequency areas and create a mask automatically. It turned out to be very helpful in case of periodic pattern removal, therefor I adapted it for python. 4 | 5 | ## Usage 6 | 7 | ``` 8 | python descreen.py images/example.png images/result.png 9 | ``` 10 | 11 | ![example](images/example.png) ![result](images/result.png) 12 | 13 | Optional arguments include 14 | 15 | * `--thresh INT, -t INT` - Threshold level for normalized magnitude spectrum 16 | * `--radius INT, -r INT` - Radius to expand the area of mask pixels 17 | * `--middle INT, -m INT` - Ratio for middle preservation 18 | 19 | The normalized spectrum and its multi-channel mask of the above example respectively 20 | 21 | ![norm-spec](images/example-spectrum.png) ![spec-mask](images/example-mask.png) 22 | 23 | ## Motivation 24 | 25 | Originally intended to clean up deconvolution [checkerboard artifacts](http://distill.pub/2016/deconv-checkerboard), found in style transferred images, I thought it may have its uses in other areas, like cleaning low-dpi scans and therefor would be better off as a separate script. I excluded the optional despeckle step, found in original plugin for being too destructive. Instead, I find [waifu2x](//github.com/nagadomi/waifu2x) to give a superior result in case of artistically styled images, thus tend to use it with maximum noise reduction as a second step. 26 | 27 | Original image 28 | 29 | ![orig](images/tubingen_orig.png) 30 | 31 | fft-descreen + waifu noise removal 32 | 33 | ![result](images/tubingen_result.png) -------------------------------------------------------------------------------- /descreen.py: -------------------------------------------------------------------------------- 1 | import cv2 2 | import numpy as np 3 | import argparse 4 | 5 | parser = argparse.ArgumentParser(description='An fft-based descreen filter') 6 | parser.add_argument('input') 7 | parser.add_argument('output') 8 | parser.add_argument('--thresh', '-t', default=92, type=int, 9 | help='Threshold level for normalized magnitude spectrum') 10 | parser.add_argument('--radius', '-r', default=6, type=int, 11 | help='Radius to expand the area of mask pixels') 12 | parser.add_argument('--middle', '-m', default=4, type=int, 13 | help='Ratio for middle preservation') 14 | args = parser.parse_args() 15 | 16 | def normalize(h, w): 17 | x = np.arange(w) 18 | y = np.arange(h) 19 | cx = np.abs(x - w//2) ** 0.5 20 | cy = np.abs(y - h//2) ** 0.5 21 | energy = cx[None,:] + cy[:,None] 22 | return np.maximum(energy*energy, 0.01) 23 | 24 | def ellipse(w, h): 25 | offset = (w+h)/2./(w*h) 26 | y, x = np.ogrid[-h: h+1., -w: w+1.] 27 | return np.uint8((x/w)**2 + (y/h)**2 - offset <= 1) 28 | 29 | img = np.float32(cv2.imread(args.input).transpose(2, 0, 1)) 30 | rows, cols = img.shape[-2:] 31 | coefs = normalize(rows, cols) 32 | mid = args.middle*2 33 | rad = args.radius 34 | ew, eh = cols//mid, rows//mid 35 | pw, ph = (cols-ew*2)//2, (rows-eh*2)//2 36 | middle = np.pad(ellipse(ew, eh), ((ph,rows-ph-eh*2-1), (pw,cols-pw-ew*2-1)), 'constant') 37 | 38 | for i in range(3): 39 | fftimg = cv2.dft(img[i],flags = 18) 40 | fftimg = np.fft.fftshift(fftimg) 41 | spectrum = 20*np.log(cv2.magnitude(fftimg[:,:,0],fftimg[:,:,1]) * coefs) 42 | 43 | ret, thresh = cv2.threshold(np.float32(np.maximum(0, spectrum)), args.thresh, 255, cv2.THRESH_BINARY) 44 | thresh *= 1-middle 45 | thresh = cv2.dilate(thresh, ellipse(rad,rad)) 46 | thresh = cv2.GaussianBlur(thresh, (0,0), rad/3., 0, 0, cv2.BORDER_REPLICATE) 47 | thresh = 1 - thresh / 255 48 | 49 | img_back = fftimg * np.repeat(thresh[...,None], 2, axis = 2) 50 | img_back = np.fft.ifftshift(img_back) 51 | img_back = cv2.idft(img_back) 52 | img[i] = cv2.magnitude(img_back[:,:,0],img_back[:,:,1]) 53 | 54 | cv2.imwrite(args.output, img.transpose(1, 2, 0)) 55 | --------------------------------------------------------------------------------