Floyd dithering with KMeans - python-3.x

I am trying to implement Floyd-Steinberg dithering in Python after using KMeans. I realised, that after dithering I receive colours which are not included in the reduced palette, so I modify the image again with KMeans. However, when trying with this picture, I see no dithering at all. I got stucked, I got tired - please, help me. My ideas become almost extinct.
from PIL import Image
import numpy as np
import matplotlib.pyplot as plt
from sklearn.cluster import KMeans
k = 16
im = Image.open('Image.png').convert('RGB') #Image converted to RGB
pic = np.array(im, dtype = np.float)/255 #Enables imshow()
im.close()
def kmeans(pic): #Prepares algorithmic data
v, c, s = pic.shape
repic = np.resize(pic, (c*v, 3))
kme = KMeans(n_clusters = k).fit(repic)
cl = kme.cluster_centers_
return kme, cl, repic, v, c
kme, cl, repic, v, c = kmeans(pic)
pred = kme.predict(repic)
def picture(v, c, cl, pred): #Creates a picture with reduced colors
image = np.ones((v, c, 3))
ind = 0
for i in range(v):
for j in range(c):
image[i][j] = cl[pred[ind]]
ind+=1
return image
image = picture(v, c, cl, pred)
def dither(pic, image): #Floyd-Steinberg dithering
v, c, s = pic.shape
Floyd = np.copy(image)
for i in range(1, v-1):
for j in range(1, c-1):
quan = pic[i][j] - image[i][j]
Floyd[i][j + 1] = quan * (np.float(7 / 16)) + pic[i][j + 1]
Floyd[i + 1][j - 1] = quan * (np.float(5 / 16)) + pic[i + 1][j - 1]
Floyd[i + 1][j] = quan * (np.float(3 / 16)) + pic[i + 1][j]
Floyd[i + 1][j + 1] = quan * (np.float(1 / 16)) + pic[i + 1][j + 1]
return Floyd
fld = dither(pic, image)
a1, a2, reim, a3, a4 = kmeans(fld)
lab = kme.predict(reim)
Floyd = picture(v, c, cl, lab)
plt.imshow(Floyd)
plt.show()

Related

How to fill between two lines with different x and y?

How to fill between two lines with different x and y? Now, the filling is for two y functions with the common x-axis, which is not true. When I tried x1, x2, y1, y2 I have got a worse result than displayed below.
import matplotlib.pyplot as plt
import numpy as np
from numpy import exp, sin
def g(y):
amp = 0.6
return amp*exp(-2.5*y)*sin(9.8*y)
def g_e(y):
amp = 0.66
return amp*exp(-2.5*y_e)*sin(8.1*y_e)
y = np.linspace(0, 0.83, 501)
y_e = np.linspace(0, 1.08, 501)
values = g(y)
values_e = g_e(y)
theta = np.radians(-65.9)
c, s = np.cos(theta), np.sin(theta)
rot_matrix = np.array(((c, s), (-s, c)))
xy = np.array([y, values]).T # rot_matrix
theta_e = np.radians(-60)
c_e, s_e = np.cos(theta_e), np.sin(theta_e)
rot_matrix_e = np.array(((c_e, s_e), (-s_e, c_e)))
xy_e = np.array([y, values_e]).T # rot_matrix_e
fig, ax = plt.subplots(figsize=(5,5))
ax.axis('equal')
x_shift = 0.59
y_shift = 0.813
x_shift_e = 0.54
y_shift_e = 0.83
ax.plot(xy[:, 0]+x_shift, xy[:, 1]+y_shift, c='red')
ax.plot(xy_e[:, 0]+x_shift_e, xy_e[:, 1]+y_shift_e, c='black')
ax.fill_between(xy[:, 0]+x_shift, xy[:, 1]+y_shift, xy_e[:, 1]+y_shift_e)
plt.show()
Script for additional question:
for i in range(len(x)-1):
for j in range(i-1):
xs_ys = intersection(x[i],x[i+1],x[j],x[j+1],y[i],y[i+1],y[j],y[j+1])
if xs_ys in not None:
xs.append(xs_ys[0])
ys.append(xs_ys[1])
I got an error:
if xs_ys in not None:
^
SyntaxError: invalid syntax
Here is an approach creating a "polygon" by concatenating the reverse of one curve to the other curve. ax.fill() can be used to fill the polygon. Note that fill_between() can look strange when the x-values aren't nicely ordered (as is the case here after the rotation). Also, the mirror function fill_betweenx() wouldn't be adequate in this case.
import matplotlib.pyplot as plt
import numpy as np
def g(y):
amp = 0.6
return amp * np.exp(-2.5 * y) * np.sin(9.8 * y)
def g_e(y):
amp = 0.66
return amp * np.exp(-2.5 * y_e) * np.sin(8.1 * y_e)
y = np.linspace(0, 0.83, 501)
y_e = np.linspace(0, 1.08, 501)
values = g(y)
values_e = g_e(y)
theta = np.radians(-65.9)
c, s = np.cos(theta), np.sin(theta)
rot_matrix = np.array(((c, s), (-s, c)))
xy = np.array([y, values]).T # rot_matrix
theta_e = np.radians(-60)
c_e, s_e = np.cos(theta_e), np.sin(theta_e)
rot_matrix_e = np.array(((c_e, s_e), (-s_e, c_e)))
xy_e = np.array([y, values_e]).T # rot_matrix_e
fig, ax = plt.subplots(figsize=(5, 5))
ax.axis('equal')
x_shift = 0.59
y_shift = 0.813
x_shift_e = 0.54
y_shift_e = 0.83
xf = np.concatenate([xy[:, 0] + x_shift, xy_e[::-1, 0] + x_shift_e])
yf = np.concatenate([xy[:, 1] + y_shift, xy_e[::-1, 1] + y_shift_e])
ax.plot(xy[:, 0] + x_shift, xy[:, 1] + y_shift, c='red')
ax.plot(xy_e[:, 0] + x_shift_e, xy_e[:, 1] + y_shift_e, c='black')
ax.fill(xf, yf, color='dodgerblue', alpha=0.3)
plt.show()

Failure to achieve an effective speed increase while Cythonizing the python 3 code

I cythonized the Python 3 code, but I failed to speeding up it. Time elapsed during the pure Python 3 code's execution is ~29 seconds while the cythonized code's is ~25 seconds (details are given below). Where did I go wrong in the cythonized code. I will be glad if you help me. I added below the pure Python 3 code, the cythonized code and the setup file, respectively.
Python version: 3.7.5
Cython version: 0.29.14
Editor: Pycharm
OS: Windows 10
The code runs 100 times in for loop. Size of the used arrays at each loop are below:
velos = 3300
V = 3300
S = 3300 x 3300
vels = 201
line_centers (in masks) = ~100
If necessary, I can add a sample data to this post.
import numpy as np
import numpy.linalg as la
def lsd(velos, V, S, vels, masks, Lambda=0.):
m, n = len(vels), len(velos)
Nmask = len(masks)
V = V - 1
M = np.zeros((n, m * len(masks)))
for N, (line_centers, weights) in enumerate(masks):
for l, lc in enumerate(line_centers):
vi = velos - lc
for j in range(m - 1):
w = np.argwhere((vi < vels[j + 1]) & (vi > vels[j])).T[0]
if len(w) == 0: continue
M[w, j + N * m] = weights[l] * (vels[j + 1] - vi[w]) / (vels[j + 1] - vels[j])
M[w, j + 1 + N * m] = weights[l] * (vi[w] - vels[j]) / (vels[j + 1] - vels[j])
if np.abs(np.sum(M)) < 1e-8:
return np.zeros((1, len(vels)))
if Lambda:
R = np.zeros((m * Nmask, m * Nmask))
for i in range(1, m-1):
R[i, i] = 2
R[i-1, i] = -1
R[i+1, i] = -1
R[0, 0] = 1
R[1, 0] = -1
R[-1, -1] = 1
R[-2, -1] = -1
X = np.matmul(M.T, (S**2))
XM = np.matmul(X, M)
if Lambda:
XM = XM + Lambda * R
cc = np.matmul(X, V)
Z, res, rank, s = la.lstsq(XM, cc, rcond=None)
# ZT = Z.T
# ccT = cc.T
# Z_ = []
# C_ = []
# for i in range(len(Z)):
# Z_.append([])
# C_.append([])
# for N in range(Nmask):
# Z_[-1].append(Z[i][N * m: (N + 1) * m])
# C_[-1].append(cc[i][N * m: (N + 1) * m])
return Z.T
import numpy as np
cimport numpy as np
import cython
# from libcpp.vector cimport vector
DTYPE = np.float
ctypedef np.double_t DTYPE_t
#cython.boundscheck(False)
# #cython.wraparound(False)
#cython.cdivision(False)
#cython.initializedcheck(True)
cpdef lsd(np.ndarray[DTYPE_t, ndim=1] velos, np.ndarray[DTYPE_t, ndim=2] V, np.ndarray[DTYPE_t, ndim=2] S,
np.ndarray[DTYPE_t, ndim=1] vels, np.ndarray[DTYPE_t, ndim=3] masks, float Lambda=0.):
cdef int m = vels.shape[0]
cdef int n = velos.shape[0]
cdef int Nmask = masks.shape[0]
cdef int N, l, j, i
cdef np.ndarray[DTYPE_t, ndim=2, mode='c'] M = np.zeros((n, m * Nmask), dtype=DTYPE)
cdef np.ndarray[DTYPE_t, ndim=2, mode='c'] R = np.zeros((m * Nmask, m * Nmask), dtype=DTYPE)
cdef np.ndarray[DTYPE_t, ndim=2, mode='c'] X
cdef np.ndarray[DTYPE_t, ndim=2, mode='c'] XM
cdef np.ndarray[DTYPE_t, ndim=2, mode='c'] cc
cdef np.ndarray[DTYPE_t, ndim=2, mode='c'] Z
cdef np.ndarray[DTYPE_t, ndim=1, mode='c'] line_centers, weights, vi
cdef np.ndarray[DTYPE_t, ndim=2, mode='c'] zeros = np.zeros((1, m), dtype=DTYPE)
cdef np.ndarray w
# cdef double lc
V = V - 1
for N in range(Nmask):
line_centers = masks[N][0]
weights = masks[N][1]
for l in range(len(line_centers)):
vi = velos - line_centers[l]
for j in range(m - 1):
# print(np.argwhere((vi < vels[j + 1]) & (vi > vels[j])).T[0])
w = np.argwhere((vi < vels[j + 1]) & (vi > vels[j])).T[0]
if len(w) == 0: continue
M[w, j + N * m] = weights[l] * (vels[j + 1] - vi[w]) / (vels[j + 1] - vels[j])
M[w, j + 1 + N * m] = weights[l] * (vi[w] - vels[j]) / (vels[j + 1] - vels[j])
if np.abs(np.sum(M)) < 1e-8:
return zeros
if Lambda:
for i in range(1, m-1):
R[i, i] = 2
R[i-1, i] = -1
R[i+1, i] = -1
R[0, 0] = 1
R[1, 0] = -1
R[-1, -1] = 1
R[-2, -1] = -1
X = np.matmul(M.T, (S**2))
XM = np.matmul(X, M)
if Lambda:
XM = XM + Lambda * R
cc = np.matmul(X, V)
Z, _, _, _ = np.linalg.lstsq(XM, cc, rcond=None)
# ZT = Z.T
# ccT = cc.T
# Z_ = []
# C_ = []
# for i in range(len(Z)):
# Z_.append([])
# C_.append([])
# for N in range(Nmask):
# Z_[-1].append(Z[i][N * m: (N + 1) * m])
# C_[-1].append(cc[i][N * m: (N + 1) * m])
return Z.T
from setuptools import setup
from Cython.Build import cythonize
import sys
import numpy
setup(
ext_modules=cythonize('LSD_Cythonize.pyx',
compiler_directives={'language_level' : sys.version_info[0]}),
include_dirs=[numpy.get_include()])

stylegan encoder print image is too small

I am trying to print a stylemix encoder image however my printed images are too small, I am not sure where am I doing wrong.
my latent space
jon = np.load('latent_representations/example0.npy')
drogo = np.load('latent_representations/example1.npy')
# Loading already learned latent directions
smile_direction = np.load('ffhq_dataset/latent_directions/smile.npy')
gender_direction = np.load('ffhq_dataset/latent_directions/gender.npy')
age_direction = np.load('ffhq_dataset/latent_directions/age.npy'
)
my draw style mix loop
def draw_style_mixing_figure(png, Gs, w, h, src_dlatents, dst_dlatents, style_ranges):
print(png)
#src_dlatents = Gs.components.mapping.run(src_latents, None) # [seed, layer, component]
#dst_dlatents = Gs.components.mapping.run(dst_latents, None)
src_images = Gs.components.synthesis.run(src_dlatents, randomize_noise=False, **synthesis_kwargs)
dst_images = Gs.components.synthesis.run(dst_dlatents, randomize_noise=False, **synthesis_kwargs)
canvas = PIL.Image.new('RGB', (w * (len(src_dlatents) + 1), h * (len(dst_dlatents) + 1)), 'white')
for col, src_image in enumerate(list(src_images)):
canvas.paste(PIL.Image.fromarray(src_image, 'RGB'), ((col + 1) * w, 0))
for row, dst_image in enumerate(list(dst_images)):
canvas.paste(PIL.Image.fromarray(dst_image, 'RGB'), (0, (row + 1) * h))
row_dlatents = np.stack([dst_dlatents[row]] * len(src_dlatents))
row_dlatents[:, style_ranges[row]] = src_dlatents[:, style_ranges[row]]
row_images = Gs.components.synthesis.run(row_dlatents, randomize_noise=False, **synthesis_kwargs)
for col, image in enumerate(list(row_images)):
canvas.paste(PIL.Image.fromarray(image, 'RGB'), ((col + 1) * w, (row + 1) * h))
canvas.save(png)
return canvas.resize((512,512))
my printing image order
tflib.init_tf()
synthesis_kwargs = dict(output_transform=dict(func=tflib.convert_images_to_uint8, nchw_to_nhwc=True), minibatch_size=1)
_Gs_cache = dict()
draw_style_mixing_figure(os.path.join(config.result_dir, 'style-mixing.png'), Gs, w=1024, h=1024, src_dlatents=jon.reshape((1, 12, 512)), dst_dlatents=drogo.reshape((1, 12, 512)), style_ranges=[range(1,1)]),
But resulting pictures are too small
any idea how to make them bigger?

Why can't I get this Runge-Kutta solver to converge as the time step decreases?

For reasons, I need to implement the Runge-Kutta4 method in PyTorch (so no, I'm not going to use scipy.odeint). I tried and I get weird results on the simplest test case, solving x'=x with x(0)=1 (analytical solution: x=exp(t)). Basically, as I reduce the time step, I cannot get the numerical error to go down. I'm able to do it with a simpler Euler method, but not with the Runge-Kutta 4 method, which makes me suspect some floating point issue here (maybe I'm missing some hidden conversion from double precision to single)?
import torch
import numpy as np
import matplotlib.pyplot as plt
def Euler(f, IC, time_grid):
y0 = torch.tensor([IC])
time_grid = time_grid.to(y0[0])
values = y0
for i in range(0, time_grid.shape[0] - 1):
t_i = time_grid[i]
t_next = time_grid[i+1]
y_i = values[i]
dt = t_next - t_i
dy = f(t_i, y_i) * dt
y_next = y_i + dy
y_next = y_next.unsqueeze(0)
values = torch.cat((values, y_next), dim=0)
return values
def RungeKutta4(f, IC, time_grid):
y0 = torch.tensor([IC])
time_grid = time_grid.to(y0[0])
values = y0
for i in range(0, time_grid.shape[0] - 1):
t_i = time_grid[i]
t_next = time_grid[i+1]
y_i = values[i]
dt = t_next - t_i
dtd2 = 0.5 * dt
f1 = f(t_i, y_i)
f2 = f(t_i + dtd2, y_i + dtd2 * f1)
f3 = f(t_i + dtd2, y_i + dtd2 * f2)
f4 = f(t_next, y_i + dt * f3)
dy = 1/6 * dt * (f1 + 2 * (f2 + f3) +f4)
y_next = y_i + dy
y_next = y_next.unsqueeze(0)
values = torch.cat((values, y_next), dim=0)
return values
# differential equation
def f(T, X):
return X
# initial condition
IC = 1.
# integration interval
def integration_interval(steps, ND=1):
return torch.linspace(0, ND, steps)
# analytical solution
def analytical_solution(t_range):
return np.exp(t_range)
# test a numerical method
def test_method(method, t_range, analytical_solution):
numerical_solution = method(f, IC, t_range)
L_inf_err = torch.dist(numerical_solution, analytical_solution, float('inf'))
return L_inf_err
if __name__ == '__main__':
Euler_error = np.array([0.,0.,0.])
RungeKutta4_error = np.array([0.,0.,0.])
indices = np.arange(1, Euler_error.shape[0]+1)
n_steps = np.power(10, indices)
for i, n in np.ndenumerate(n_steps):
t_range = integration_interval(steps=n)
solution = analytical_solution(t_range)
Euler_error[i] = test_method(Euler, t_range, solution).numpy()
RungeKutta4_error[i] = test_method(RungeKutta4, t_range, solution).numpy()
plots_path = "./plots"
a = plt.figure()
plt.xscale('log')
plt.yscale('log')
plt.plot(n_steps, Euler_error, label="Euler error", linestyle='-')
plt.plot(n_steps, RungeKutta4_error, label="RungeKutta 4 error", linestyle='-.')
plt.legend()
plt.savefig(plots_path + "/errors.png")
The result:
As you can see, the Euler method converges (slowly, as expected of a first order method). However, the Runge-Kutta4 method does not converge as the time step gets smaller and smaller. The error goes down initially, and then up again. What's the issue here?
The reason is indeed a floating point precision issue. torch defaults to single precision, so once the truncation error becomes small enough, the total error is basically determined by the roundoff error, and reducing the truncation error further by increasing the number of steps <=> decreasing the time step doesn't lead to any decrease in the total error.
To fix this, we need to enforce double precision 64bit floats for all floating point torch tensors and numpy arrays. Note that the right way to do this is to use respectively torch.float64 and np.float64 rather than, e.g., torch.double and np.double, because the former are fixed-sized float values, (always 64bit) while the latter depend on the machine and/or compiler. Here's the fixed code:
import torch
import numpy as np
import matplotlib.pyplot as plt
def Euler(f, IC, time_grid):
y0 = torch.tensor([IC], dtype=torch.float64)
time_grid = time_grid.to(y0[0])
values = y0
for i in range(0, time_grid.shape[0] - 1):
t_i = time_grid[i]
t_next = time_grid[i+1]
y_i = values[i]
dt = t_next - t_i
dy = f(t_i, y_i) * dt
y_next = y_i + dy
y_next = y_next.unsqueeze(0)
values = torch.cat((values, y_next), dim=0)
return values
def RungeKutta4(f, IC, time_grid):
y0 = torch.tensor([IC], dtype=torch.float64)
time_grid = time_grid.to(y0[0])
values = y0
for i in range(0, time_grid.shape[0] - 1):
t_i = time_grid[i]
t_next = time_grid[i+1]
y_i = values[i]
dt = t_next - t_i
dtd2 = 0.5 * dt
f1 = f(t_i, y_i)
f2 = f(t_i + dtd2, y_i + dtd2 * f1)
f3 = f(t_i + dtd2, y_i + dtd2 * f2)
f4 = f(t_next, y_i + dt * f3)
dy = 1/6 * dt * (f1 + 2 * (f2 + f3) +f4)
y_next = y_i + dy
y_next = y_next.unsqueeze(0)
values = torch.cat((values, y_next), dim=0)
return values
# differential equation
def f(T, X):
return X
# initial condition
IC = 1.
# integration interval
def integration_interval(steps, ND=1):
return torch.linspace(0, ND, steps, dtype=torch.float64)
# analytical solution
def analytical_solution(t_range):
return np.exp(t_range, dtype=np.float64)
# test a numerical method
def test_method(method, t_range, analytical_solution):
numerical_solution = method(f, IC, t_range)
L_inf_err = torch.dist(numerical_solution, analytical_solution, float('inf'))
return L_inf_err
if __name__ == '__main__':
Euler_error = np.array([0.,0.,0.], dtype=np.float64)
RungeKutta4_error = np.array([0.,0.,0.], dtype=np.float64)
indices = np.arange(1, Euler_error.shape[0]+1)
n_steps = np.power(10, indices)
for i, n in np.ndenumerate(n_steps):
t_range = integration_interval(steps=n)
solution = analytical_solution(t_range)
Euler_error[i] = test_method(Euler, t_range, solution).numpy()
RungeKutta4_error[i] = test_method(RungeKutta4, t_range, solution).numpy()
plots_path = "./plots"
a = plt.figure()
plt.xscale('log')
plt.yscale('log')
plt.plot(n_steps, Euler_error, label="Euler error", linestyle='-')
plt.plot(n_steps, RungeKutta4_error, label="RungeKutta 4 error", linestyle='-.')
plt.legend()
plt.savefig(plots_path + "/errors.png")
Result:
Now, as we decrease the time step, the error of the RungeKutta4 approximation decreases with the correct rate.

Floyd-Steinberg implementation Python

I use Floyd-Steinberg dithering in order to diffuse the quantization error after processing an image with KMeans from scipy. The given data is RGB file - both for grayscale and color. The problem is the visualisation - I get no dithering.
from PIL import Image
import numpy as np
import matplotlib.pyplot as plt
from sklearn.cluster import KMeans
im = Image.open('file.png').convert('RGB')
pic = np.array(im, dtype = np.float)/255
im.close()
I would like to omit the KMeans part and focus on Floyd-Steinberg:
"""pic - as above, original array; image - processed image"""
def dither(pic, image):
v, c, s = pic.shape
Floyd = np.copy(image)
for i in range(1, v-1):
for j in range(1, c-1):
quan = pic[i][j] - image[i][j] #Quantization error
Floyd[i][j + 1] = quan * (np.float(7 / 16)) + Floyd[i][j + 1]
Floyd[i + 1][j - 1] = quan * (np.float(3 / 16)) + Floyd[i + 1][j - 1]
Floyd[i + 1][j] = quan * (np.float(5 / 16)) + Floyd[i + 1][j]
Floyd[i + 1][j + 1] = quan * (np.float(1 / 16)) + Floyd[i + 1][j + 1]
return Floyd
Floyd = dither(pic, image)
plt.imshow(Floyd)
plt.show()
I receive a little dithering when I replace Floyd with pic, i.e. Floyd[i + 1][j] = quan * (np.float(5 / 16)) + pic[i + 1][j]. However, this is improper code! Additionally, I have to deal with colours out of the clusters, thus I again assess the new pixels to clusters. How can I make it work? Where is THIS crucial mistake?
from PIL import Image
import cv2
import numpy as np
##################################################### Solution 1 ##############################################################
#PIL.Image.convert parametrs :
#https://pillow.readthedocs.io/en/4.2.x/reference/Image.html?highlight=image.convert#PIL.Image.Image.convert
#PIL.Image.convert Modes :
#https://pillow.readthedocs.io/en/4.2.x/handbook/concepts.html#concept-modes
#image convert to 1-bit pixels, black and white, stored with one pixel per byte and Dithering
imageConvert = Image.open('Image.PNG').convert(mode='1',dither=Image.FLOYDSTEINBERG)
imageConvert.save('DitheringWithPIL.png')
##################################################### Solution 2 ##############################################################
Image = cv2.imread('Image.PNG')
GrayImage = cv2.cvtColor(Image, cv2.COLOR_BGR2GRAY)
cv2.imwrite('GracyImage.PNG', GrayImage)
Height = GrayImage.shape[0]
Width = GrayImage.shape[1]
for y in range(0, Height):
for x in range(0, Width):
old_value = GrayImage[y, x]
new_value = 0
if (old_value > 128) :
new_value = 255
GrayImage[y, x] = new_value
Error = old_value - new_value
if (x<Width-1):
NewNumber = GrayImage[y, x+1] + Error * 7 / 16
if (NewNumber>255) : NewNumber=255
elif (NewNumber<0) : NewNumber=0
GrayImage[y, x+1] = NewNumber
if (x>0 and y<Height-1):
NewNumber = GrayImage[y+1, x-1] + Error * 3 / 16
if (NewNumber>255) : NewNumber=255
elif (NewNumber<0) : NewNumber=0
GrayImage[y+1, x-1] = NewNumber
if (y<Height-1):
NewNumber= GrayImage[y+1, x] + Error * 5 / 16
if (NewNumber>255) : NewNumber=255
elif (NewNumber<0) : NewNumber=0
GrayImage[y+1, x] = NewNumber
if (y<Height-1 and x<Width-1):
NewNumber = GrayImage[y+1, x+1] + Error * 1 / 16
if (NewNumber>255) : NewNumber=255
elif (NewNumber<0) : NewNumber=0
GrayImage[y+1, x+1] = NewNumber
cv2.imwrite('DitheringWithAlgorithm.PNG', GrayImage)

Resources