Django

Django Image Processing Webapp Part III
In third part, we will process the image according to the options selected by the user.
Table of Contents
Basic filters
Main Module
We will create separate file Image/basic.py to handle all the functions related to basic filters.
#Install numpy
pip install numpy
from django.core.files import File
import numpy as np
from PIL import Image
from PIL import ImageFilter
from io import BytesIO
def Bit_Plane(input_image,bit=7):
img = Image.open(input_image).convert('L')
img = img.resize((400,400))
numpy_image = np.array(img)
#Iterate over each pixel and change pixel value to binary using np.binary_repr() and store it in a list.
lst = []
for i in range(numpy_image.shape[0]):
for j in range(numpy_image.shape[1]):
lst.append(np.binary_repr(numpy_image[i][j] ,width=8)) # width = no. of bits
value = (pow(2,(bit-1)))
# We have a list of strings where each string represents binary pixel value. To extract bit planes we need to iterate over the strings and store the characters corresponding to bit planes into lists.
# # Multiply with 2^(n-1) and reshape to reconstruct the bit image.
eight_bit_img = (np.array([int(i[0]) for i in lst],dtype = np.uint8) * value).reshape(400,400)
bit_sliced_image = Image.fromarray(eight_bit_img)
final_image = bit_sliced_image .convert("L")
out_io = BytesIO()
final_image.save(out_io,'PNG',quality=85)
out_final = File(out_io,name=input_image.name)
in_io = BytesIO()
img.save(in_io,'PNG',quality=85)
img = File(in_io,name=input_image.name)
return [img,out_final]
def Log_transform(input_image):
img = Image.open(input_image).convert('L')
img = img.resize((400,400))
#img = input_image.resize((400,400), Image.ANTIALIAS)
numpy_image = np.array(img)
numpy_image = np.array(img)
numpy_image = numpy_image/255
numpy_image = numpy_image + 1
numpy_image = np.log(numpy_image)
print(type(numpy_image))
numpy_image = numpy_image * 255
numpy_image = np.around(numpy_image,decimals=0)
log_image = Image.fromarray(numpy_image)
log_image = log_image.convert("L")
out_io = BytesIO()
log_image.save(out_io,'PNG',quality=85)
out_final = File(out_io,name=input_image.name)
in_io = BytesIO()
img.save(in_io,'PNG',quality=85)
img = File(in_io,name=input_image.name)
return [img,out_final]
def Power_transform(input_image,gamma=2):
img = Image.open(input_image).convert('L')
img = img.resize((400,400))
numpy_image = np.array(img)
numpy_image = numpy_image/255
numpy_image_a = np.power(numpy_image,gamma)
numpy_image_a = numpy_image_a * 255
numpy_image_a = np.around(numpy_image_a,decimals=0)
power_image = Image.fromarray(numpy_image_a)
power_image = power_image.convert("L")
out_io = BytesIO()
power_image.save(out_io,'PNG',quality=85)
out_final = File(out_io,name=input_image.name)
in_io = BytesIO()
img.save(in_io,'PNG',quality=85)
img = File(in_io,name=input_image.name)
return [img,out_final]
def Threshold(input_image,threshold=150):
img = Image.open(input_image).convert('L')
img = img.resize((400,400))
numpy_image = np.array(img)
row = numpy_image.shape[0]
column = numpy_image.shape[1]
new_array = np.zeros(shape=(row,column))
for i in range(row):
for j in range(column):
if(numpy_image[i][j]>=threshold):
new_array[i][j] = 255
else:
new_array[i][j] = 0
#converting array back to image
threshold_image = Image.fromarray(new_array)
threshold_image = threshold_image.convert("L")
out_io = BytesIO()
threshold_image.save(out_io,'PNG',quality=85)
out_final = File(out_io,name=input_image.name)
in_io = BytesIO()
img.save(in_io,'PNG',quality=85)
img = File(in_io,name=input_image.name)
return [img,out_final]
def Negative(input_image):
img = Image.open(input_image).convert('L')
img = img.resize((400,400))
numpy_image = np.array(img)
row = numpy_image.shape[0]
column = numpy_image.shape[1]
new_array = np.zeros(shape=(row,column))
for i in range(row):
for j in range(column):
new_array[i][j] = 255 - numpy_image[i][j]
negative_image = Image.fromarray(new_array)
negative_image = negative_image.convert("L")
out_io = BytesIO()
negative_image.save(out_io,'PNG',quality=85)
out_final = File(out_io,name=input_image.name)
in_io = BytesIO()
img.save(in_io,'PNG',quality=85)
img = File(in_io,name=input_image.name)
return [img,out_final]
def Histogram_equalization(input_image):
img_main = Image.open(input_image).convert('L')
img_main = img_main.resize((400,400))
# # convert our image into a numpy array
img = np.asarray(img_main)
# put pixels in a 1D array by flattening out img array
flat = img.flatten()
# show the histogram
#plt.hist(flat, bins=256)
histogram = np.zeros(256)
# loop through pixels and sum up counts of pixels
for pixel in flat:
histogram[pixel] += 1
#plt.plot(histogram)
a = iter(histogram)
b = [next(a)]
for i in a:
b.append(b[-1] + i)
cs = np.array(b)
# numerator & denomenator
nj = (cs - cs.min()) * 255
N = cs.max() - cs.min()
# re-normalize the cumsum
cs = nj / N
# cast it back to uint8 since we can't use floating point values in images
cs = cs.astype('uint8')
# get the value from cumulative sum for every index in flat, and set that as img_new
img_new = cs[flat]
# put array back into original shape since we flattened it
img_new = np.reshape(img_new, img.shape)
histo_image = Image.fromarray(img_new)
histo_image = histo_image.convert("L")
out_io = BytesIO()
histo_image.save(out_io,'PNG',quality=85)
out_final = File(out_io,name=input_image.name)
in_io = BytesIO()
img_main.save(in_io,'PNG',quality=85)
img = File(in_io,name=input_image.name)
return [img,out_final]
Adding method for bit plane slicing in model
To handle the request for filters in our model, edit Image/models.py file.
..........................................
from . import basic
...........................
# Create your models here.
class ImageEnhance(models.Model):
title = models.CharField(max_length=50)
image = models.ImageField(upload_to="",blank=True,null=True)
image_enhanced = models.ImageField(upload_to="",blank=True,null=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now_add=True)
def rotateMe(self,deg=90,*args,**kwargs):
self.image_enhanced = imageRotate(self.image,deg)
super().save(*args,**kwargs)
def transposeMe(self,*args,**kwargs):
self.image_enhanced = imageTranspose(self.image)
super().save(*args,**kwargs)
#Handling basic operations
def bitPlane(self,bit=3,*args,**kwargs):#new
#new
self.image,self.image_enhanced = basic.Bit_Plane(self.image,bit)
super().save(*args,**kwargs)#new
def __str__(self):
return self.title
In above code, we added method for bit plane slicing.
Bit Plane view
We need to edit Image/views.py to read and respond the request of bitPlane slicing.
..................................................
def imageForm(request):
if request.method == 'POST':
form = ImageForm(request.POST,request.FILES)
if form.is_valid():
#save the model instance
instance = form.save(commit=False)
instance.save()
return render(request,"image_process.html",{"form":form,'object':instance})
else:
latest_object = ImageEnhance.objects.latest('id')
process = request.POST["first-dropdown"]
try:
sub_process = request.POST["second-dropdown"]
tuning_value = request.POST["num-Input"]
except:
pass
if process == '1':
print("************Basic Selected")
print(sub_process)
if sub_process == '1B':
print("********* Bit Plane Slicing Selected")
tuning_value = int(tuning_value)
latest_object.bitPlane(tuning_value)
return render(request,"image_process.html",{"form":form,"object":latest_object})
return HttpResponse("Basic Selected")
...........................................
The result of Bit Plane Slicing


Adding methods for rest of the basic image processing function
......................................................................
# Create your models here.
class ImageEnhance(models.Model):
title = models.CharField(max_length=50)
image = models.ImageField(upload_to="",blank=True,null=True)
image_enhanced = models.ImageField(upload_to="",blank=True,null=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now_add=True)
def rotateMe(self,deg=90,*args,**kwargs):
self.image_enhanced = imageRotate(self.image,deg)
super().save(*args,**kwargs)
def transposeMe(self,*args,**kwargs):
self.image_enhanced = imageTranspose(self.image)
super().save(*args,**kwargs)
#Handling basic operations
def bitPlane(self,bit=3,*args,**kwargs):
self.image,self.image_enhanced = basic.Bit_Plane(self.image,bit)
super().save(*args,**kwargs)
def powerTransform(self,gamma=2,*args,**kwargs):
self.image,self.image_enhanced = basic.Power_transform(self.image,gamma)
super().save(*args,**kwargs)
def thresholdImage(self,threshold=150,*args,**kwargs):
self.image,self.image_enhanced = basic.Threshold(self.image,threshold)
super().save(*args,**kwargs)
def negativeImage(self,*args,**kwargs):
self.image,self.image_enhanced = basic.Negative(self.image)
super().save(*args,**kwargs)
def histogramEqImage(self,*args,**kwargs):
self.image,self.image_enhanced = basic.Histogram_equalization(self.image)
super().save(*args,**kwargs)
def __str__(self):
return self.title
Receiving and responding to request in Image/views.py
........................................................
if process == '1':
print("************Basic Selected")
print(sub_process)
if sub_process == '1B':
print("********* Bit Plane Slicing Selected")
tuning_value = int(tuning_value)
latest_object.bitPlane(tuning_value)
return render(request,"image_process.html",{"form":form,"object":latest_object})
elif sub_process == "1C":
print("********* Power Transform Selected")
tuning_value = float(tuning_value)
latest_object.powerTransform(tuning_value)
return render(request,"image_process.html",{"form":form,"object":latest_object})
elif sub_process == "1D":
print("******************* Image Threshold selected")
tuning_value = float(tuning_value)
latest_object.thresholdImage(tuning_value)
return render(request,"image_process.html",{"form":form,"object":latest_object})
elif sub_process == "1E":
print("******************* Negative Image selected")
latest_object.negativeImage()
return render(request,"image_process.html",{"form":form,"object":latest_object})
elif sub_process == "1F":
print("******************* Histogram Equalization selected")
latest_object.histogramEqImage()
return render(request,"image_process.html",{"form":form,"object":latest_object})
return render(request,"image_process.html",{"form":form,"object":latest_object})
................................................................
The Power Transform Output


Thresholding the same image at 56


Generating the negative


Result of Histogram Equalization


Spatial filters
Module
Add Image/spatial.py file and fill it with following code.
from django.core.files import File
import numpy as np
from PIL import Image
from PIL import ImageFilter
from io import BytesIO
def Smooth_Filter(input_image,kernel_size = 3):
img = Image.open(input_image).convert('L')
img = img.resize((400,400))
# convert to numpy array
numpy_image = np.array(img)
#array for padding
#array for padding
x = 400+(kernel_size-1)
y = 400+(kernel_size-1)
array_b = np.zeros((x,y))
#to pad initial array with zeros
array_b[1:401,1:401] = numpy_image
#defining filter
#defining filter
filter_array = np.full((kernel_size, kernel_size), 1/9)
#creating an empty list
lst = []
for i in range(400):
for j in range(400):
#extracting part of array equal to filter size
array_c = array_b[i:(kernel_size+i),j:(kernel_size+j)]
#applying filter
array_mul = np.multiply(filter_array,array_c)
array_sum = np.sum(array_mul)
# putting calculated value in list
lst.append(array_sum)
#resizing lst to shape of original array
final_array = np.resize(lst,(400,400))
final_image = Image.fromarray(final_array)
final_image = final_image.convert("L")
out_io = BytesIO()
final_image.save(out_io,'PNG',quality=85)
out_final = File(out_io,name=input_image.name)
in_io = BytesIO()
img.save(in_io,'PNG',quality=85)
img = File(in_io,name=input_image.name)
return [img,out_final]
def Sharp_Filter(input_image,kernel_size=3):
img = Image.open(input_image).convert('L')
img = img.resize((400,400))
# convert to numpy array
numpy_image = np.array(img)
#array for padding
x = 400+(kernel_size-1)
y = 400+(kernel_size-1)
array_b = np.zeros((x,y))
#to pad initial array with zeros
array_b[1:401,1:401] = numpy_image
denominator = (kernel_size*kernel_size)
filter_array = np.full((kernel_size, kernel_size), -1/denominator)
center = int((kernel_size-1)/2)
filter_array[center,center] = (denominator-1)/denominator
#creating an empty list
lst = []
for i in range(400):
for j in range(400):
#extracting part of array equal to filter size
array_c = array_b[i:(kernel_size+i),j:(kernel_size+j)]
#applying filter
array_mul = np.multiply(filter_array,array_c)
array_sum = np.sum(array_mul)
# putting calculated value in list
lst.append(array_sum)
#resizing lst to shape of original array
final_array = np.resize(lst,(400,400))
final_image = Image.fromarray(final_array)
final_image = final_image.convert("L")
out_io = BytesIO()
final_image.save(out_io,'PNG',quality=85)
out_final = File(out_io,name=input_image.name)
in_io = BytesIO()
img.save(in_io,'PNG',quality=85)
img = File(in_io,name=input_image.name)
return [img,out_final,final_image]
def Min_Filter(input_image,kernel_size=3):
img = Image.open(input_image).convert('L')
img = img.resize((400,400))
# convert to numpy array
numpy_image = np.array(img)
#array for padding
x = 400+(kernel_size-1)
y = 400+(kernel_size-1)
array_b = np.zeros((x,y))
#to pad initial array with zeros
array_b[1:401,1:401] = numpy_image
#defining filter
filter_array = np.full((kernel_size, kernel_size), kernel_size)
#creating an empty list
lst = []
for i in range(400):
for j in range(400):
#extracting part of array equal to filter size
array_c = array_b[i:(kernel_size+i),j:(kernel_size+j)]
#applying filter
array_mul = np.multiply(filter_array,array_c)
array_sum = np.min(array_mul)
# putting calculated value in list
lst.append(array_sum)
#resizing lst to shape of original array
final_array = np.resize(lst,(400,400))
final_image = Image.fromarray(final_array)
final_image = final_image.convert("L")
out_io = BytesIO()
final_image.save(out_io,'PNG',quality=85)
out_final = File(out_io,name=input_image.name)
in_io = BytesIO()
img.save(in_io,'PNG',quality=85)
img = File(in_io,name=input_image.name)
return [img,out_final]
def Max_Filter(input_image,kernel_size=3):
img = Image.open(input_image).convert('L')
img = img.resize((400,400))
# convert to numpy array
numpy_image = np.array(img)
#array for padding
#array for padding
x = 400+(kernel_size-1)
y = 400+(kernel_size-1)
array_b = np.zeros((x,y))
#to pad initial array with zeros
array_b[1:401,1:401] = numpy_image
#defining filter
filter_array = np.full((kernel_size, kernel_size), kernel_size)
#creating an empty list
lst = []
for i in range(400):
for j in range(400):
#extracting part of array equal to filter size
array_c = array_b[i:(kernel_size+i),j:(kernel_size+j)]
#applying filter
array_mul = np.multiply(filter_array,array_c)
array_sum = np.max(array_mul)
# putting calculated value in list
lst.append(array_sum)
#resizing lst to shape of original array
final_array = np.resize(lst,(400,400))
final_image = Image.fromarray(final_array)
final_image = final_image.convert("L")
out_io = BytesIO()
final_image.save(out_io,'PNG',quality=85)
out_final = File(out_io,name=input_image.name)
in_io = BytesIO()
img.save(in_io,'PNG',quality=85)
img = File(in_io,name=input_image.name)
return [img,out_final]
def Median_Filter(input_image,kernel_size=3):
img = Image.open(input_image).convert('L')
img = img.resize((400,400))
# convert to numpy array
numpy_image = np.array(img)
#array for padding
x = 400+(kernel_size-1)
y = 400+(kernel_size-1)
array_b = np.zeros((x,y))
#to pad initial array with zeros
array_b[1:401,1:401] = numpy_image
#defining filter
filter_array = np.full((kernel_size, kernel_size), kernel_size)
#creating an empty list
lst = []
for i in range(400):
for j in range(400):
#extracting part of array equal to filter size
array_c = array_b[i:(kernel_size+i),j:(kernel_size+j)]
#applying filter
array_mul = np.multiply(filter_array,array_c)
array_sum = np.median(array_mul)
# putting calculated value in list
lst.append(array_sum)
#resizing lst to shape of original array
final_array = np.resize(lst,(400,400))
final_image = Image.fromarray(final_array)
final_image = final_image.convert("L")
out_io = BytesIO()
final_image.save(out_io,'PNG',quality=85)
out_final = File(out_io,name=input_image.name)
in_io = BytesIO()
img.save(in_io,'PNG',quality=85)
img = File(in_io,name=input_image.name)
return [img,out_final]
def High_Boost(input_image,kernel_size=3):
high_pass = Sharp_Filter(input_image,kernel_size)
high_pass = high_pass[2]
img = Image.open(input_image).convert('L')
img = img.resize((400,400))
# convert to numpy array
numpy_image = np.array(img)
A = 3
high_boost = (A-1)*numpy_image + high_pass
final_image = Image.fromarray(high_boost)
final_image= final_image.convert("L")
out_io = BytesIO()
final_image.save(out_io,'PNG',quality=85)
out_final = File(out_io,name=input_image.name)
in_io = BytesIO()
img.save(in_io,'PNG',quality=85)
img = File(in_io,name=input_image.name)
return [img,out_final]
Adding methods for spatial filters in models.py
......................
from . import spatial
........................
#methods for spatial filters
def smoothFilter(self,kernel_size=3,*args,**kwargs):
self.image,self.image_enhanced = spatial.Smooth_Filter(self.image,kernel_size)
super().save(*args,**kwargs)
def sharpFilter(self,kernel_size=3,*args,**kwargs):
self.image,self.image_enhanced,_ = spatial.Sharp_Filter(self.image,kernel_size)
super().save(*args,**kwargs)
def minFilter(self,kernel_size=3,*args,**kwargs):
self.image,self.image_enhanced = spatial.Min_Filter(self.image,kernel_size)
super().save(*args,**kwargs)
def maxFilter(self,kernel_size=3,*args,**kwargs):
self.image,self.image_enhanced = spatial.Max_Filter(self.image,kernel_size)
super().save(*args,**kwargs)
def medianFilter(self,kernel_size=3,*args,**kwargs):
self.image,self.image_enhanced = spatial.Median_Filter(self.image,kernel_size)
super().save(*args,**kwargs)
def highBoostFilter(self,kernel_size=3,*args,**kwargs):
self.image,self.image_enhanced = spatial.High_Boost(self.image,kernel_size)
super().save(*args,**kwargs)
Receiving and responding to request in Image/views.py
.......................................................
elif process == '2':
print("************ Spatial Filter Selcted")
print(sub_process)
if sub_process == "2B":
print("************************Smooth filter selected")
tuning_value = int(tuning_value)
latest_object.smoothFilter(tuning_value)
return render(request,"image_process.html",{"form":form,"object":latest_object,"filter":"Smooth filter"})
elif sub_process == "2C":
print("************************ Sharp filter selected")
tuning_value = int(tuning_value)
latest_object.sharpFilter(tuning_value)
return render(request,"image_process.html",{"form":form,"object":latest_object,"filter":"Sharp filter"})
elif sub_process == '2D':
print("************************ Min filter selected")
tuning_value = int(tuning_value)
latest_object.minFilter(tuning_value)
return render(request,"image_process.html",{"form":form,"object":latest_object,"filter":"Min filter"})
elif sub_process == '2E':
print("************************ Max filter selected")
tuning_value = int(tuning_value)
latest_object.maxFilter(tuning_value)
return render(request,"image_process.html",{"form":form,"object":latest_object,"filter":"Max filter"})
elif sub_process == '2F':
print("************************ Median filter selected")
tuning_value = int(tuning_value)
latest_object.medianFilter(tuning_value)
return render(request,"image_process.html",{"form":form,"object":latest_object,"filter":"Median filter"})
elif sub_process == '2G':
print("************************ High boost filter selected")
tuning_value = int(tuning_value)
latest_object.highBoostFilter(tuning_value)
return render(request,"image_process.html",{"form":form,"object":latest_object,"filter":"High Boost"})
.............................................................
Result of Spatial Filter
Output of Smooth filter


Output of Sharp filter

Output of Min filter

Output of Max Filter

Output of Median Filter

Output of High Boost

Frequency Filters
The Module
Create Image/frequency.py
#terminal
pip install scipy
from django.core.files import File
import numpy as np
from PIL import Image,ImageDraw
from PIL import ImageFilter
from io import BytesIO
from scipy import fftpack
def low_pass_filter(input_image):
img = Image.open(input_image).convert('L')
img = img.resize((400,400))
#convert image to numpy array
image1_np=np.array(img)
#fft of image
fft1 = fftpack.fftshift(fftpack.fft2(image1_np))
#Create a low pass filter image
x,y = image1_np.shape[0],image1_np.shape[1]
#defining filter
#size of circle
e_x,e_y=50,50
#create a box
bbox=((x/2)-(e_x/2),(y/2)-(e_y/2),(x/2)+(e_x/2),(y/2)+(e_y/2))
low_pass=Image.new("L",(image1_np.shape[0],image1_np.shape[1]),color=0)
draw1=ImageDraw.Draw(low_pass)
draw1.ellipse(bbox, fill=1)
low_pass_np=np.array(low_pass)
low_pass_np = low_pass_np.T
#end of defining filter
#multiply both the images
filtered=np.multiply(fft1,low_pass_np)
#inverse fft
ifft2 = np.real(fftpack.ifft2(fftpack.ifftshift(filtered)))
ifft2 = np.maximum(0, np.minimum(ifft2, 255))
data = Image.fromarray(ifft2)
low_pass_array = data
data = data.convert("L")
final_image = data
out_io = BytesIO()
final_image.save(out_io,'PNG',quality=85)
out_final = File(out_io,name=input_image.name)
in_io = BytesIO()
img.save(in_io,'PNG',quality=85)
img = File(in_io,name=input_image.name)
return [img,out_final,low_pass_array]
def high_pass_filter(input_image):
img = Image.open(input_image).convert('L')
img = img.resize((400,400))
#converting image to array
image_array = np.array(img)
#sending image to low pass filter
lowpass_image_array = low_pass_filter(input_image)[2]
#subtracting lowpass image from original to obtain highpass image
high_pass_array = image_array - lowpass_image_array
print(high_pass_array.shape)
#array to image
high_pass_image = Image.fromarray(high_pass_array)
high_pass_image = high_pass_image.convert("L")
final_image = high_pass_image
out_io = BytesIO()
final_image.save(out_io,'PNG',quality=85)
out_final = File(out_io,name=input_image.name)
in_io = BytesIO()
img.save(in_io,'PNG',quality=85)
img = File(in_io,name=input_image.name)
return [img,out_final]
Created methods for frequency filters in models.py
................................................
#Frequency filters
def lowpassFilter(self,*args,**kwargs):
self.image,self.image_enhanced,_ = frequency.low_pass_filter(self.image)
super().save(*args,**kwargs)
def highpassFilter(self,*args,**kwargs):
self.image,self.image_enhanced = frequency.high_pass_filter(self.image)
super().save(*args,**kwargs)
............................................
Handling response in views.py
..........................................................................
elif process == '3':
print("************ Freuency Filter Selected")
if sub_process == "3B":
print("*********************Low pass filter selected")
latest_object.lowpassFilter()
return render(request,"image_process.html",{"form":form,"object":latest_object,"filter":"Low pass filter"})
elif sub_process == "3C":
print("*********************High pass filter selected")
latest_object.highpassFilter()
return render(request,"image_process.html",{"form":form,"object":latest_object,"filter":"High pass filter"})
return render(request,"image_process.html",{"form":form,"object":latest_object})
................................................................
Output of Frequency Filters
Output of low pass filter

Output of High Pass filter

Git push
#terminal
git add .
git commit -m "Modules of IP added"
git push
In part IV, we will add pagination to our home page.
pontu
0
Tags :