# Stroke Algorithm in python

Here is my next Numpy practice, I am not sure is it more accurate to call it "Stroke Algorithm" or "Draw edge stuff", xD.

Anyway I did in steps:

• calculate each pixel's RGB absolute distance to pixels on its right and down side, the distance is simple measured by absolute different in each color value and sum them up.

• if the absolute distance on both directions all larger than some particular value, then it is an edge pixel, and color it black, else white

### Suggestions I am looking for

• Any suggestions in Numpy, I have some duplicate code this time, and didn't find good way to remove them.

### Full code:

# -*- coding: utf-8 -*-
import numpy as np
from PIL import Image

def stroke(image_path, output, level=80, edge_color=[255,255,255], blackground_color=[0,0,0]):
img = Image.open(image_path)
data = np.asarray(img, dtype="int32")

w, h, k = data.shape
dirright_data = np.concatenate((data[:, 1:], data[:,-1:]), axis=1)
dirdown_data = np.concatenate((data[1:,:], data[-1:,:]), axis=0)

disRight = np.absolute(np.sum(data - dirright_data, axis=2))
disDown = np.absolute(np.sum(data - dirdown_data, axis=2))

level = min(max(1,level), 255)
D_right = np.asarray(disRight<=level, dtype="int32")
D_down = np.asarray(disDown<=level, dtype="int32")

D = (D_right+D_down) > 1
neg_D = (D_right+D_down) <= 1
data[D] = edge_color
data[neg_D] = blackground_color

img = Image.fromarray(np.asarray(np.clip(data, 0, 255), dtype="uint8"), "RGB")
img.save(output)

if __name__ == "__main__":
stroke("images/bob.png", "new_test.jpg")


I find your numpy code to be quite rational as far as my skills go, but your script have several things to improve on besides that.

### Improvable in current scope :

• Removing duplicate code thanks to a helper function
• Painting the whole background to simplify
• Removing (yet) unecessary line w, h, k = data.shape
• Naming error, or logic error ? Background and edge color seem swapped. Changing <= level to > level
• Some non-compliant and unexplicit variable names

### Code showing this

# -*- coding: utf-8 -*-
import numpy as np
from PIL import Image

def get_dist_array(data, shifted_data, level):
distance_map = np.absolute(np.sum(data - shifted_data, axis=2))
return np.asarray(distance_map > level, dtype="int32")

def stroke(image_path, output, level=80, edge_color=[255,255,255], blackground_color=[0,0,0]):
img = Image.open(image_path)
data = np.asarray(img, dtype="int32")
level = min(max(1,level), 255)
edges_right = get_dist_array(data, np.concatenate((data[:, 1:], data[:,-1:]), axis=1), level)
edges_down = get_dist_array(data, np.concatenate((data[1:,:], data[-1:,:]), axis=0), level)
data[:] = blackground_color
edges = (edges_right+edges_down) > 1
data[edges] = edge_color
img = Image.fromarray(np.asarray(np.clip(data, 0, 255), dtype="uint8"), "RGB")
img.save(output)

if __name__ == "__main__":
stroke("images/bob.png", "new_test.jpg")


### Other things to consider worth improving :

• Get rid of hardcoded file names. You could use argparse here
• RGBA support ?
• Thanks a lot for your reply, w, h, k = data.shape is unecessary right, and <= level yeah that's a mistake thanks for that. argparse and RGBA all good suggestions. hmm I am looking for numpy suggestion, I wonder if these code about distance to right and distance to down side can change color can be replace by some "one line" numpy code Nov 18, 2018 at 3:15

In the future you might want to apply more than one transformation to an image. In order to be able to (efficiently) do this, you should put the image reading and writing in their own functions and just pass a numpy.array to your function:

import numpy as np
from PIL import Image

img = Image.open(image_path)
return np.asarray(img, dtype="int32")

def write_image(img, image_path):
img = Image.fromarray(np.asarray(np.clip(img, 0, 255), dtype="uint8"), "RGB")
img.save(image_path)

def stroke(img, level=80, edge_color=[255,255,255], blackground_color=[0,0,0]):
...

if __name__ == "__main__":

One additional point, currently you are using numpy.clip to ensure that the output is in the right format to be written as an image file. I think this should be the responsibility of the caller and you would want it to explicitly error out if the caller did not take care of it instead of silently clipping. Depending on the use case, e.g. np.interp1d(img, (img.min(), img.max()), (0, 255)) (i.e. rescaling the values to the correct range) might be more correct.