Making a PIL Gauge#
If the tkinter drawing is too slow we can use PIL, so as not to have endless backgrounds I have limited the choice to four, one colour for each of the main ranges with a standard size. Making the background is very similar to the tkinter gauge. When inserting the text for the gauge type, the width given required a different multiplier dependant on how many lines of text was used. In order to make a better looking background it has been enlarged then reduced.
Show/Hide Code class_pil.py
from PIL import Image, ImageDraw, ImageFont
from math import pi, sin, cos
from DialUtils import colour_choice,create_pieslice,create_chord,create_circle,\
draw_text,draw_delta,draw_tick
class LCDpil:
def __init__(self,select,enlargement=9): # ,unit='Temp C'
self.select=select
self.enlargement=enlargement
#self.unit=unit
# lcd gauge type; min,max scale extent; st,end start end degrees;
# upt units per tick; w width pixels; tw tick width; farbe colour
# mess gauge type
self.lcds=lcds={
1:{'lcd':'lcd70','min':-30,'max':70,'st':120,'end':60,'upt':1,
'w':201,'tw':3,'mess':'temperature','farbe': 'blue'},
2:{'lcd':'lcd100','min':0,'max':100,'st':120,'end':60,'upt':1,
'w':201,'tw':3,'mess':'general\npurpose','farbe': 'purple'},
3:{'lcd':'lcd255','min':0,'max':270,'st':135,'end':45,'upt':2,
'w':201,'tw':2,'mess':'analogue\n output','farbe': 'red'},
4:{'lcd':'lcd1023','min':0,'max':1030,'st':117,'end':63,'upt':10,
'w':201,'tw':4,'mess':'analogue\n input','farbe': 'green'}
}
self.e=e=enlargement
we=lcds[select]['w']*e
he=lcds[select]['w']*e
self.ce=ce=we//2,he//2
self.re=re=we//2
lwe=re/25*e
background='#090B0E'
self.max_value=max_value=int(lcds[select]['lcd'].strip('lcd')) #70
self.min_value=min_value=lcds[select]['min'] #-30
val=(max_value+min_value)//2
#print(val)
max_scale=lcds[select]['max']
self.unitspertick=unitspertick=lcds[select]['upt']
self.dial=lcds[select]['lcd']
mess=lcds[select]['mess']
# angular calculations
# 300° scale extent, 100 divisions therefore 1 tick interval 3 degrees
degree_extent=360-lcds[select]['st']+lcds[select]['end'] # 300
scale_extent=max_scale-min_value # 100
self.tick_extent=tick_extent=degree_extent/scale_extent
self.tick_width=tick_width=lcds[select]['tw']
# trig_start=240 # scale degrees corresponding to 0° trig
self.start_scale=start_scale=lcds[select]['st'] # physical start scale in degrees
self.end_scale=end_scale=lcds[select]['end'] # physical end scale in degrees
colour=lcds[select]['farbe']
bdial,fdial,gdial,sdial=colour_choice(colour)
self.fdial=fdial
self.dfont=dfont = 'digital-7 (mono italic)'
img = Image.new('RGBA', (we,he), '#00000000') # need transparency
idraw = ImageDraw.Draw(img)
# create bezel, add 20 degrees to scale extent
create_pieslice(idraw,ce,re,fill=sdial,outline=None,start=start_scale-10,
end=end_scale+10)
create_pieslice(idraw,ce,re-4*e,fill=bdial,outline=None,
start=start_scale-10,end=end_scale+10)
create_chord(idraw,ce,re,fill=sdial,outline=None,start=start_scale-10,
end=end_scale+10)
create_chord(idraw,ce,re-4*e,fill=bdial,outline=None,start=start_scale-10,
end=end_scale+10)
for j in range(0,scale_extent//unitspertick+1):
angle=j*tick_extent*unitspertick + start_scale # measured angle
draw_tick(idraw,ce,re-8*e,2*e,angle,fill=fdial,width=2*e)
create_pieslice(idraw,ce,re-10*e,fill=gdial,outline=None,
start=angle-0.5,end=angle+0.5)
create_chord(idraw,ce,re-18*e,fill=bdial,outline=None,start=start_scale,
end=end_scale)
# inner ticks need to be before any deltas
if max_value != 1023:
for j in range(0,scale_extent//unitspertick+1):
angle=j*tick_extent*unitspertick + start_scale
draw_tick(idraw,ce,re-20*e,2*e,angle,fill=sdial,width=1*e)
else:
for j in range(0,scale_extent//unitspertick+1):
angle=j*tick_extent*unitspertick + start_scale
draw_tick(idraw,ce,re-20*e,2*e,angle,fill=sdial,width=1*e)
# deltas and scale numbers
for j in range(0,scale_extent//unitspertick+1):
angle=j*tick_extent*unitspertick + start_scale
if j % 10==0:
tangle=str(int(j+min_value)*unitspertick)
draw_delta(idraw,angle,ce,re-19*e,e,fill=fdial)
draw_text(idraw,tangle,ce,re-29*e,angle,fill=fdial,
font=dfont,size=10*e)
# ghost value
if max_value != 1023:
draw_text(idraw,'888',ce,0,0,fill=gdial,font=dfont,size=60*e)
else:
draw_text(idraw,'8888',ce,0,0,fill=gdial,font=dfont,size=45*e)
# gauge type
y=(re*4//10 if max_value in (70,255,1023) else re//2)
ttffont = ImageFont.truetype(dfont+'.ttf', size=10*e)
(width, height), (offset_x, offset_y) = ttffont.font.getsize(mess)
dx=(-width//2 if max_value==70 else -width//4)
idraw.text((ce[0]+dx-offset_x,ce[1]-y-height//2-offset_y),
mess,fill=fdial,font=ttffont)
w=lcds[select]['w']
gtype=lcds[select]['lcd']
# better looking than just drawing original size
imgr=img.resize((w,w),resample=Image.Resampling.LANCZOS)
imgr.save('../figures/'+gtype+'.png')
if __name__ =='__main__':
LCDpil(4)
You should see background images as follows:-
Temperature |
General |
Analogue Output |
Analogue Input |
|---|---|---|---|
-30 to 70 |
0 to 100 |
0 to 255 |
0 to 1023 |
Show/Hide Code lcd_pil.py
from PIL import Image, ImageDraw, ImageTk
from tkinter import Tk, Label, PhotoImage
from DialUtils import colour_choice,create_circle,draw_text,create_pieslice
def pilpointer(select,val,unit='Temp C'):
# lcd gauge type; min,max scale extent; st,end start end degrees;
# upt units per tick; w width pixels; tw tick width; farbe colour
# mess gauge type
lcds={
1:{'lcd':'lcd70','min':-30,'max':70,'st':120,'end':60,'upt':1,
'w':201,'tw':3,'mess':'temperature','farbe': 'blue'},
2:{'lcd':'lcd100','min':0,'max':100,'st':120,'end':60,'upt':1,
'w':201,'tw':3,'mess':'general\npurpose','farbe': 'purple'},
3:{'lcd':'lcd255','min':0,'max':270,'st':135,'end':45,'upt':2,
'w':201,'tw':2,'mess':'analogue\n output','farbe': 'red'},
4:{'lcd':'lcd1023','min':0,'max':1030,'st':117,'end':63,'upt':10,
'w':201,'tw':4,'mess':'analogue\n input','farbe': 'green'}
}
h=w=lcds[select]['w']
c=w//2,h//2
r=w//2
max_value=lcds[select]['max']
min_value=lcds[select]['min']
max_scale=lcds[select]['max']
unitspertick=lcds[select]['upt']
# 300° scale extent, 100 divisions therefore 1 tick interval 3 degrees
degree_extent=360-lcds[select]['st']+lcds[select]['end'] # 300
scale_extent=max_scale-min_value # 100
tick_extent=degree_extent/scale_extent
# trig_start=240 # scale degrees corresponding to 0° trig
start_scale=lcds[select]['st'] # physical start scale in degrees
colour=lcds[select]['farbe']
fdial=colour_choice(colour)[1]
dfont = 'digital-7 (mono italic)'
img = Image.new('RGBA', (w,h), '#00000000') # need transparency
idraw = ImageDraw.Draw(img)
for j in range(0,(val-min_value)//unitspertick+1): # pointer leds, add dial starting value
angle=j*tick_extent*unitspertick + start_scale # measured angle
create_pieslice(idraw,c,r-10,fill=fdial,outline=None,start=angle-0.5,
end=angle+0.5)
create_circle(idraw,c,r-18,fill='#00000000',outline=None)
# display draw_text needs 3 or 4 letters or spaces
sval=str(val)
if max_value != 1023:
pinput=(3-len(sval))*' '+sval
draw_text(idraw,pinput,c,0,0,fill=fdial,font=dfont,
size=60)
else:
pinput=(4-len(sval))*' '+sval
draw_text(idraw,pinput,c,0,0,fill=fdial,font=dfont,
size=45)
y=(r*7//10 if max_value==255 else r*3//5)
draw_text(idraw,unit,(c[0],c[1]+y),0,0,fill=fdial,font=dfont,size=10)
gtype=lcds[select]['lcd']
bimg=Image.open('../figures/'+gtype+'.png')
cimg=Image.alpha_composite(bimg,img)
# cimg.save('lcd_comp_point_test.png', quality=95)
return cimg
if __name__ =='__main__':
root=Tk()
# select 1,2,3,4 -30:70,0:100,0:255,0:1023
select=1
# val value to be shown
val=25
image=pilpointer(select,val)
tki=ImageTk.PhotoImage(image)
l=Label(root,image=tki)
l.image=tki
l.grid()
root.mainloop()
After the background is saved we can retreive it and superimpose the displayed value and any unit description, This means that the realtime operations are limited, but the downside is that we need to first work with an image, then retreive the background image from disk, combine these two images before taking the result and load into tkinter to display. With so many image operations including a disk read the overall speed was almost 3 times slower than using tkinter direct.
Say we open the background file, then keep it open and make a copy and use
this copy to work with, then everytime the value changed, close the old copy
and create a new copy. This would save the disk read, (apart from the initial
time) , we would draw on the copy, so there is no alpha composite operation.
This in turn means we cannot use pieslice for the large ticks, but must use
draw_tick instead - this is because we cannot clean up the centres of
background required when using pieslices.
Show/Hide Code lcd_pil_rev.py
from PIL import Image, ImageDraw, ImageTk
from tkinter import Tk, Label, PhotoImage
from DialUtils import colour_choice,draw_text,draw_tick
class lcd_pil:
def __init__(self,select,unit='Temp C'):
# lcd gauge type; min,max scale extent; st,end start end degrees;
# upt units per tick; w width pixels; tw tick width; farbe colour
# mess gauge type
lcds={
1:{'lcd':'lcd70','min':-30,'max':70,'st':120,'end':60,'upt':1,
'w':201,'tw':3,'mess':'temperature','farbe': 'blue'},
2:{'lcd':'lcd100','min':0,'max':100,'st':120,'end':60,'upt':1,
'w':201,'tw':3,'mess':'general\npurpose','farbe': 'purple'},
3:{'lcd':'lcd255','min':0,'max':270,'st':135,'end':45,'upt':2,
'w':201,'tw':2,'mess':'analogue\n output','farbe': 'red'},
4:{'lcd':'lcd1023','min':0,'max':1030,'st':117,'end':63,'upt':10,
'w':201,'tw':4,'mess':'analogue\n input','farbe': 'green'}
}
self.unit=unit
h=w=lcds[select]['w']
self.c=w//2,h//2
self.r=w//2
#max_value=lcds[select]['max']
self.min_value=lcds[select]['min']
self.max_scale=max_scale=lcds[select]['max']
self.unitspertick=lcds[select]['upt']
# 300° scale extent, 100 divisions therefore 1 tick interval 3 degrees
degree_extent=360-lcds[select]['st']+lcds[select]['end'] # 300
scale_extent=max_scale-self.min_value # 100
self.tick_extent=degree_extent/scale_extent
self.tick_width=lcds[select]['tw']
# trig_start=240 # scale degrees corresponding to 0° trig
self.start_scale=lcds[select]['st'] # physical start scale in degrees
colour=lcds[select]['farbe']
self.fdial=colour_choice(colour)[1]
self.dfont = 'digital-7 (mono italic)'
gtype=lcds[select]['lcd']
self.bimg=Image.open('../figures/'+gtype+'.png')
def pilpointer(self,val):
img = self.bimg
dfont=self.dfont
c=self.c
r=self.r
fdial=self.fdial
#img = Image.new('RGBA', (w,h), '#00000000') # need transparency
idraw = ImageDraw.Draw(img)
for j in range(0,(val-self.min_value)//self.unitspertick+1): # pointer leds, add dial starting value
angle=j*self.tick_extent*self.unitspertick + self.start_scale # measured angle
draw_tick(idraw,c,r-17,8,angle,fill=self.fdial,
width=self.tick_width)
#create_circle(idraw,c,r-18,fill='#00000000',outline=None)
# display draw_text needs 3 or 4 letters or spaces
sval=str(val)
if self.max_scale != 1023:
pinput=(3-len(sval))*' '+sval
draw_text(idraw,pinput,c,0,0,fill=fdial,font=dfont,size=60)
else:
pinput=(4-len(sval))*' '+sval
draw_text(idraw,pinput,c,0,0,fill=fdial,font=dfont,size=45)
y=(r*7//10 if self.max_scale==255 else r*3//5)
draw_text(idraw,self.unit,(c[0],c[1]+y),0,0,fill=fdial,font=dfont,size=10)
#gtype=lcds[select]['lcd']
#bimg=Image.open('../figures/'+gtype+'.png')
#cimg=Image.alpha_composite(bimg,img)
#img.save('lcd_comp_point_test.png', quality=95)
return img
if __name__ =='__main__':
root=Tk()
# select 1,2,3,4 -30:70,0:100,0:255,0:1023
select=1
# val value to be shown
val=35
lp=lcd_pil(select)
im=lp.pilpointer(val)
tki=ImageTk.PhotoImage(im)
l=Label(root,image=tki)
l.image=tki
l.grid()
root.mainloop()
Note
Only the major changes have been highlighted, the changes needed for creating a class are not shown.
After all that the revised PIL script took about 60% of the time for tkinter to draw on a canvas just the changes associated with a new value.
It should be emphasized that whether we wish to use any python program the interface to the Arduino remains much the same, in fact we could use the inbuilt serial window or plotter instead of the python programs. This gives us a useful testing point, if the serial window or plotter work, then we can use the sketch to hook up to another program.



