creating a neon-glow with python numpy

Question:

I’m trying to create a neon-effect w/ a source image. I have included three images, the source, my current attempt & a target. The program takes the image, finds the white-edges, & calculates the distance from each pixel to the nearest white-edge (these parts both work fine); from there, I am struggling to find the right saturation and value parameters to create the neon-glow.

From the target image, what I need to happen is basically for the saturation to be 0 on a white-edge, then to dramatically increase the further away it gets from an edge; for value, I need it to be 1 on a white-edge, then to dramatically decrease. I can’t figure out the best way to manipulate distance_image (which holds each pixel’s distance from the nearest white-edge) such as to achieve these two results with saturation and value.

from PIL import Image
import cv2
import numpy as np
from scipy.ndimage import binary_erosion
from scipy.spatial import KDTree

def find_closest_distance(img):
    white_pixel_points = np.array(np.where(img))
    tree = KDTree(white_pixel_points.T)
    img_meshgrid = np.array(np.meshgrid(np.arange(img.shape[0]),
                                        np.arange(img.shape[1]))).T
    distances, _ = tree.query(img_meshgrid)
    return distances

def find_edges(img):
    img_np = np.array(img)
    kernel = np.ones((3,3))
    return img_np - binary_erosion(img_np, kernel)*255

img = Image.open('a.png').convert('L')
edge_image = find_edges(img)
distance_image = find_closest_distance(edge_image)
max_dist = np.max(distance_image)
distance_image = distance_image / max_dist

hue = np.full(distance_image.shape, 0.44*180)
saturation = distance_image * 255
value = np.power(distance_image, 0.2)
value = 255 * (1 - value**2)

new_tups = np.dstack((hue, saturation, value)).astype('uint8')
new_tups = cv2.cvtColor(new_tups, cv2.COLOR_HSV2BGR)
new_img = Image.fromarray(new_tups, 'RGB').save('out.png')

The following images show the source data (left), the current result (middle), and the desired result (right).

source
current
target

Asked By: user18615293

||

Answers:

I think I would do this with convolution instead. Convolving an image with a Gaussian kernel is a common way to blur an image. You can do it in various ways, but maybe the easiest to use is scipy.ndimage.gaussian_filter. Here’s one way to implement all this, see if you like the result.

from PIL import Image
from io import BytesIO
import requests
import numpy as np

r = requests.get('https://i.stack.imgur.com/MhUQZ.png')
img = Image.open(BytesIO(r.content))
imarray = np.asarray(img)[..., 0] / 255

This is your first image, the white rectangles.

Now I’ll make those outlines, do the blur, create the colour images, and combine them:

from scipy.ndimage import binary_erosion
from scipy.ndimage import gaussian_filter

eroded = binary_erosion(imarray, iterations=3)

# Make the outlined rectangles.
outlines = imarray - eroded

# Convolve with a Gaussian to effect a blur.
blur = gaussian_filter(outlines, sigma=11)

# Make binary images into neon green.
neon_green_rgb = [0.224, 1.0, 0.0784]
outlines = outlines[:, :, None] * neon_green_rgb
blur = blur[:, :, None] * neon_green_rgb

# Combine the images and constrain to [0, 1].
blur_strength = 3
glow = np.clip(outlines + blur_strength*blur, 0, 1)

And look at it:

import matplotlib.pyplot as plt

plt.imshow(glow)

Glowing rectangle result

You’ll want to adjust the sigma of the Gaussian (its width), the colours, blur strength, and so on. Hope it helps.

Answered By: kwinkunks

Here is one way to do that in Python/OpenCV.

  • Read the input
  • Convert to grayscale
  • Threshold to binary
  • Get edges of desired thickness using morphology gradient
  • Invert the edges so black on white background
  • Do distance transform
  • Stretch to full dynamic range
  • Invert
  • Normalize to range 0 to 1 by dividing by the maximum value
  • Attenuate using a power law to control distance roll-off (ramping)
  • Create a color image of the size of the input and the desired color
  • Multiply the attenuated image by the color image
  • Save results

Input:

enter image description here

import cv2
import numpy as np
import skimage.exposure

# read input
img = cv2.imread('rectangles.png')

# convert to grayscale
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

# threshold
thresh = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY+cv2.THRESH_OTSU)[1]

# do morphology gradient to get edges and invert so black edges on white background
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (3,3))
edges = cv2.morphologyEx(thresh, cv2.MORPH_GRADIENT, kernel)
edges = 255 - edges

# get distance transform
dist = edges.copy()
distance = cv2.distanceTransform(dist, distanceType=cv2.DIST_L2, maskSize=3)
print(np.amin(distance), np.amax(distance))

# stretch to full dynamic range and convert to uint8 as 3 channels
stretch = skimage.exposure.rescale_intensity(distance, in_range=('image'), out_range=(0,255))

# invert 
stretch = (255 - stretch)
max_stretch = np.amax(stretch)

# normalize to range 0 to 1 by dividing byk max_stretch
stretch = (stretch/max_stretch)

# attenuate with power law 
pow = 4
attenuate = np.power(stretch, pow)
attenuate = cv2.merge([attenuate,attenuate,attenuate])

# create a green image the size of the input
color_img = np.full_like(img, (0,255,0), dtype=np.float32)

# multiply the color image with the stretched distance image
glow = (color_img * attenuate).clip(0,255).astype(np.uint8)

# save results
cv2.imwrite('rectangles_edges.png', edges)
cv2.imwrite('rectangles_stretch.png', (255*stretch).clip(0,255).astype(np.uint8))
cv2.imwrite('rectangles_attenuate.png', (255*attenuate).clip(0,255).astype(np.uint8))
cv2.imwrite('rectangles_glow.png', glow)

# view results
cv2.imshow("EDGES", edges)
cv2.imshow("STRETCH", stretch)
cv2.imshow("ATTENUATE", attenuate)
cv2.imshow("RESULT", glow)
cv2.waitKey(0)

Edges (inverted):

enter image description here

Stretched Distance Transform:

enter image description here

Attenuated Distance Transform:

enter image description here

Glow Result:

enter image description here

Answered By: fmw42