1

I am trying to remove the checkered background (which represents transparent background in Adobe Illustrator and Photoshop) with transparent color (alpha channel) in some PNGs with Python script.

First, I use template matching:

import cv2
import numpy as np
from matplotlib import pyplot as plt

img_rgb = cv2.imread('testimages/fake1.png', cv2.IMREAD_UNCHANGED)
img_gray = cv2.cvtColor(img_rgb, cv2.COLOR_BGR2GRAY)
template = cv2.imread('pattern.png', 0)

w, h = template.shape[::-1]
res = cv2.matchTemplate(img_gray, template, cv2.TM_CCOEFF_NORMED)
threshold = 0.8
loc = np.where( res >= threshold)

for pt in zip(*loc[::-1]):
    if len(img_rgb[0][0]) == 3:
        # add alpha channel
        rgba = cv2.cvtColor(img_rgb, cv2.COLOR_RGB2RGBA)
        rgba[:, :, 3] = 255 # default not transparent
        img_rgb = rgba
    # replace the area with a transparent rectangle
    cv2.rectangle(img_rgb, pt, (pt[0] + w, pt[1] + h), (255, 255, 255, 0), -1) 

cv2.imwrite('result.png', img_rgb)

Source Image: fake1.png

Source Image

Pattern Template: pattern.png

Pattern Template

Output: result.png (the gray area is actually transparent; enlarge a bit for viewing easier)

Output Image

I know this approach has problems, as the in some cases, the template cannot be identified fully, as part of the pattern is hidden by the graphics in the PNG image.

My question is: How can I match such a pattern perfectly using OpenCV? via FFT Filtering?

References:

Raptor
  • 53,206
  • 45
  • 230
  • 366
  • Not an FFT solution. But perhaps determine the checks spacing and intensities. Then create your own tiled checkerboard image. Then subtract or divide the two images. – fmw42 Oct 20 '22 at 04:12
  • Possible. The checkerboard size varies among those PNGs, maybe I have to manually check the size of the checkers first. Also, I noticed in some cases, the edges of some checkers are blurry. Maybe I have to sharpen the images too. – Raptor Oct 20 '22 at 05:43

3 Answers3

2

Here is one way to do that in Python/OpenCV simply by thresholding on the checks color range.

Input:

enter image description here

import cv2
import numpy as np

# read input
img = cv2.imread("fake.png")

# threshold on checks
low = (230,230,230)
high = (255,255,255)
mask = cv2.inRange(img, low, high)

# invert alpha
alpha = 255 - mask

# convert img to BGRA
result = cv2.cvtColor(img, cv2.COLOR_BGR2BGRA)
result[:,:,3] = alpha

# save output
cv2.imwrite('fake_transparent.png', result)

cv2.imshow('img', img)
cv2.imshow('mask', mask)
cv2.imshow('result', result)
cv2.waitKey(0)
cv2.destroyAllWindows()

enter image description here

Download the resulting image to see that it is actually transparent.

fmw42
  • 46,825
  • 10
  • 62
  • 80
  • 1
    Or, rather than downloading, click your own StackOverflow profile - beside the *"Search"* box, then *"Settings"* (beside *"Profile"* and *"Activity"*) and choose **Dark** *"Theme"*. – Mark Setchell Oct 20 '22 at 18:55
  • the only problem left is, if the artwork contains the two colors (gray & white), it will also be masked and cleared to transparent. – Raptor Oct 21 '22 at 01:55
  • You have too high expectations. In that case, you need more than simple image processing. Try a background removal AI/Deep Learning tool such as http://remove.bg – fmw42 Oct 21 '22 at 02:40
  • I briefly looked at the FFT spectrum (log of magnitude) and it does not show any obvious signs of the checkerboard pattern likely because the checkerboard pattern is so low contrast. – fmw42 Oct 21 '22 at 02:45
  • Increasing the contrast (to black and white checkerboard) allows the spectrum to bring out the pattern but it is also a dense grid of dots. However, knowing the checkerboard spacing, one could predict where to mask the magnitude. – fmw42 Oct 21 '22 at 02:51
1

since you're working on PNG's with transparent backgrounds, it would probably be equally viable to instead of trying to detect the checkered background, you try to extract the stuff that isn't checkered. This could probably be achieved using a color check on all pixels. You could use opencv's inRange() function. I'll link a StackOverflow link below that tries to detect dark spots on a image. Inrange example

1

Here is one way to use DFT to process the image in Python/OpenCV/Numpy. One does need to know the size of the checkerboard pattern (light or dark square size).

  • Read the input
  • Separate channels
  • Apply DFT to each channel
  • Shift origin from top left to center of each channel
  • Extract magnitude and phase images from each channel
  • Define the checkerboard pattern size
  • Create a black and white checkerboard image of the same size
  • Apply similar DFT processing to the checkerboard image
  • Get the spectrum from the log(magnitude)
  • Threshold the spectrum to form a mask
  • Zero out the DC center point in the mask
  • OPTION: If needed apply morphology dilate to thicken the white dots. But does not seem to be needed here
  • Invert the mask so the background is white and the dots are black
  • Convert the mask to range 0 to 1 and make 2 channels
  • Apply the two-channel mask to the center shifted DFT channels
  • Shift the center back to the top left in each masked image
  • Do the IDFT to get back from complex domain to real domain on each channel
  • Merge the resulting channels back to a BGR image as the final reconstituted image
  • Save results

Input:

enter image description here

import numpy as np
import cv2
import math

# read input 
# note: opencv fft only works on grayscale
img = cv2.imread('fake.png')
hh, ww = img.shape[:2]

# separate channels
b,g,r = cv2.split(img)

# convert images to floats and do dft saving as complex output
dft_b = cv2.dft(np.float32(b), flags = cv2.DFT_COMPLEX_OUTPUT)
dft_g = cv2.dft(np.float32(g), flags = cv2.DFT_COMPLEX_OUTPUT)
dft_r = cv2.dft(np.float32(r), flags = cv2.DFT_COMPLEX_OUTPUT)

# apply shift of origin from upper left corner to center of image
dft_b_shift = np.fft.fftshift(dft_b)
dft_g_shift = np.fft.fftshift(dft_g)
dft_r_shift = np.fft.fftshift(dft_r)

# extract magnitude and phase images
mag_b, phase_b = cv2.cartToPolar(dft_b_shift[:,:,0], dft_b_shift[:,:,1])
mag_g, phase_g = cv2.cartToPolar(dft_g_shift[:,:,0], dft_g_shift[:,:,1])
mag_r, phase_r = cv2.cartToPolar(dft_r_shift[:,:,0], dft_r_shift[:,:,1])

# set check size (size of either dark or light square)
check_size = 15

# create checkerboard pattern
white = np.full((check_size,check_size), 255, dtype=np.uint8)
black = np.full((check_size,check_size), 0, dtype=np.uint8)
checks1 = np.hstack([white,black])
checks2 = np.hstack([black,white])
checks3 = np.vstack([checks1,checks2])
numht = math.ceil(hh / (2*check_size))
numwd = math.ceil(ww / (2*check_size))
checks = np.tile(checks3, (numht,numwd))
checks = checks[0:hh, 0:ww]

# apply dft to checkerboard pattern
dft_c = cv2.dft(np.float32(checks), flags = cv2.DFT_COMPLEX_OUTPUT)
dft_c_shift = np.fft.fftshift(dft_c)
mag_c, phase_c = cv2.cartToPolar(dft_c_shift[:,:,0], dft_c_shift[:,:,1])

# get spectrum from magnitude (add tiny amount to avoid divide by zero error)
spec = np.log(mag_c + 0.00000001)

# theshold spectrum
mask = cv2.threshold(spec, 1, 255, cv2.THRESH_BINARY)[1]

# mask DC point (center spot)
centx = int(ww/2)
centy = int(hh/2)
dot = np.zeros((3,3), dtype=np.uint8)
mask[centy-1:centy+2, centx-1:centx+2] = dot

# If needed do morphology dilate by small amount. 
# But does not seem to be needed in this case

# invert mask
mask = 255 - mask

# apply mask to real and imaginary components
mask1 = (mask/255).astype(np.float32)
mask2 = cv2.merge([mask1,mask1])
complex_b = dft_b_shift*mask2
complex_g = dft_g_shift*mask2
complex_r = dft_r_shift*mask2

# shift origin from center to upper left corner
complex_ishift_b = np.fft.ifftshift(complex_b)
complex_ishift_g = np.fft.ifftshift(complex_g)
complex_ishift_r = np.fft.ifftshift(complex_r)

# do idft with normalization saving as real output and crop to original size
img_notch_b = cv2.idft(complex_ishift_b, flags=cv2.DFT_SCALE+cv2.DFT_REAL_OUTPUT)
img_notch_b = img_notch_b.clip(0,255).astype(np.uint8)
img_notch_b = img_notch_b[0:hh, 0:ww]
img_notch_g = cv2.idft(complex_ishift_g, flags=cv2.DFT_SCALE+cv2.DFT_REAL_OUTPUT)
img_notch_g = img_notch_g.clip(0,255).astype(np.uint8)
img_notch_g = img_notch_g[0:hh, 0:ww]
img_notch_r = cv2.idft(complex_ishift_r, flags=cv2.DFT_SCALE+cv2.DFT_REAL_OUTPUT)
img_notch_r = img_notch_r.clip(0,255).astype(np.uint8)
img_notch_r = img_notch_r[0:hh, 0:ww]

# combine b,g,r components
img_notch = cv2.merge([img_notch_b, img_notch_g, img_notch_r])

# write result to disk
cv2.imwrite("fake_checks.png", checks)
cv2.imwrite("fake_spectrum.png", (255*spec).clip(0,255).astype(np.uint8))
cv2.imwrite("fake_mask.png", mask)
cv2.imwrite("fake_notched.png", img_notch)

# show results
cv2.imshow("ORIGINAL", img)
cv2.imshow("CHECKS", checks)
cv2.imshow("SPECTRUM", spec)
cv2.imshow("MASK", mask)
cv2.imshow("NOTCH", img_notch)
cv2.waitKey(0)
cv2.destroyAllWindows()

Checkerboard image:

enter image description here

Spectrum of checkerboard:

enter image description here

Mask:

enter image description here

Result (notch filtered image):

enter image description here

The checkerboard pattern in the result is mitigated from the original, but still there upon close inspection.

From here one needs to threshold on the white background and invert to make an image for the alpha channel. Then convert the image to 4 BGRA and insert the alpha channel into the BGRA image as I described in my other answer below.

fmw42
  • 46,825
  • 10
  • 62
  • 80
  • But still the checker box is still in the result image... – Raptor Oct 24 '22 at 08:11
  • Yes, I said that it only mitigated the pattern and did not completely remove it. You probably need AI/Deep Learning approach to remove it. Try http://remove.bg – fmw42 Oct 24 '22 at 15:36