#Custom_LPR_OCR_detection_v20210713_wAutoYoloLabeling.py
#created by u\shallowstack for the purpose of either: autolabeling for YOLO deepstack custom model for OCR,
# or for logging the detected characters in images within a file directory, with a number of filtering capabilties.
# before running this code, first run the below call in Powershell (not CMD) to start Deepstack custom model:
#PS C:\Windows\system32> deepstack --MODELSTORE-DETECTION "C:\AI_models\lpr_ocr\" --PORT 97
#change port and dir to whereever your custom (character recognition) AI model is...
#major changes to v20210712 -
# deals with overlapping detected characters. as i think these are common with custom OCR models.
# if too many detections, it now filters down to an expended number of Characters.
# now skips any left over yolo text files in the directory without error. note: deletes txt file only when it needs to write a new one of the same name.
# better debugging info and code comments for usability
import requests
import os
import PIL
import sys
from PIL import Image
#CHANGE THESE GLOBAL VARIABLES for customized control
overlapping_threshold = 4 #how close the characters can be before the inferior one is ignored
min_conf_thres = 0.6 #if worst confidence is less than this, the result wont be written to output_logfile
min_len_thres = 3 #if there is not more than this number of characters detected on the plate, the result wont be written to output_logfile
plate_len_threshold = 6 #if there is more character detected than this number, program will cut out the weakest confidence characters until value met.
#CHANGE THESE DIRECTORIES TO MATCH YOUR FILESYSTEM
input_directory = 'J:\\BlueIris\\Deepstack_LPR_dataset\\results_2021' #where large dataset of cropped plates live
output_logfile = 'C:\\Users\\BlueIrisServer\\Desktop\\logfile.txt' #where python will save the log file
#change this file pointer to whereever your YOLO class file is. it will sync up the character labels with the class numbering scheme
classYoloFilename = 'J:\\BlueIris\\Deepstack_LPR_dataset\\OCR_testing\\QLD Model\\train\\classes.txt'
#reads 'class' txt file, and puts into a list to be searched every time a label is given, with the index value of the list returned so it can be written to the YOLO file.
classLabels = tuple(open(classYoloFilename).read().split('\n'))
print(classLabels)
#get # of x pixels for yolo file conversion from pixel # to %
def get_xnum_pixels(filepath):
width, height = Image.open(filepath).size
return width
#get # of y pixels for yolo file conversion from pixel # to %
def get_ynum_pixels(filepath):
width, height = Image.open(filepath).size
return height
#cleanup variables before run, just in case
resp_label = [0]
resp_conf = [0]
resp_pos = [0]
resp_data2write = [""]
resp_label.clear()
resp_conf.clear()
resp_pos.clear()
resp_data2write.clear()
YoloLabelFilepath = ""
data2write = ""
i = 0
#goes through entire input_directory and processes each file to look for alphanumeric characters
for filename in os.listdir(input_directory):
#store full filepath of current file under inspection
filepath = os.path.join(input_directory, filename)
#skip file if it is a txt file
if filename.rsplit('.',1)[1] == 'txt' :
print(filename + " is a txt file, SKIPPED OCR DETECTION ON THIS FILE.")
#os.remove(filepath) #uncomment this if you want it to delete the text file from directory. (untested)
continue
else :
print(filename + " is not a txt file, sending to OCR detection AI...")
#get # of x pixels for yolo file conversion from pixel # to %
xSize = get_xnum_pixels(filepath)
#get # of y pixels for yolo file conversion from pixel # to %
ySize = get_ynum_pixels(filepath)
#create new text file to store label annotations
YoloLabelFilepath = filepath.rsplit('.',1)[0] + '.txt'
image_data = open(filepath,"rb").read()
#clear variables from last image data
resp_label.clear()
resp_conf.clear()
resp_pos.clear()
resp_data2write.clear()
#posts to deepstack custom server and logs result
response = requests.post("http://localhost:97/v1/vision/custom/yolo5m_best_20210623",files={"image":image_data}).json() #change port 97 to whatever your deepstack custom server is on, and custom model name
#print(response)
#go through all detections in image and store to temporary lists for comparisons
for detection in response["predictions"]:
resp_label.append(detection["label"])
resp_conf.append(round(detection["confidence"],2))
resp_pos.append(detection["x_min"])
#print annotation results for assisted labeling - for future incorporation into model
#this will print a YOLO type txt file for each image processed, according to classes.txt file
#some maths to get xy pixel values into YOLO format: x center, y center, x width, y width (all as a % of total image size)
xCenter = float(detection["x_min"] + detection["x_max"]) / float(2)
yCenter = float(detection["y_min"] + detection["y_max"]) / float(2)
xWidth = detection["x_max"] - detection["x_min"]
yWidth = detection["y_max"] - detection["y_min"]
xCenter = format(round(xCenter / xSize, 6), '.6f')
yCenter = format(round(yCenter / ySize, 6), '.6f')
xWidth = format(round(xWidth / xSize, 6), '.6f')
yWidth = format(round(yWidth / ySize, 6), '.6f')
#check the class list to see what index value the label is that was returned from detection
ClassValue = classLabels.index(detection["label"])
#format is : class (not label) xcenter% ycenter% xwidth% ywidth%
data2write = str(ClassValue) + " " + str(xCenter) + " " + str(yCenter) + " " + str(xWidth) + " " + str(yWidth) + " "
# resp_xCenter.append(xCenter)
# resp_yCenter.append(yCenter)
# resp_xWidth.append(xWidth)
# resp_yWidth.append(yWidth)
# resp_ClassValue.append(ClassValue)
resp_data2write.append(data2write)
#sort all stored arrays (label, confidence, YoloData) according to x_min position array (so it reads left to right like we see it)
B1=resp_pos
B2=resp_pos
B3=resp_pos
A=resp_label
C=resp_conf
D=resp_data2write
#if NO detections made exist with debug info
if len(C) == 0:
print("No Char was found in image " + filename)
continue
else:
pass
#sort resp_label array
zipped_lists1 = zip(B1,A)
sorted_pairs1 = sorted(zipped_lists1)
tuples = zip(*sorted_pairs1)
B1, A = [list(tuple) for tuple in tuples]
#sort resp_conf array
zipped_lists2 = zip(B2,C)
sorted_pairs2 = sorted(zipped_lists2)
tuples = zip(*sorted_pairs2)
B2, C = [list(tuple) for tuple in tuples]
#sort resp_data2write array
zipped_lists2 = zip(B3,D)
sorted_pairs2 = sorted(zipped_lists2)
tuples = zip(*sorted_pairs2)
B3, D = [list(tuple) for tuple in tuples]
#debug info only
print(str(A) + ", " + str(B1) + ", " + str(C) + ", " + str(D))
k=0
#go through each position in the lists
for m in B1 :
i = 0
if k == len(B1) :
print("max of list reached already.")
continue
#check if any CHARs are overlapping according to their xmin value
upperLim = m + overlapping_threshold
lowerLim = m - overlapping_threshold
for n in B1 :
if i == len(B1) :
print("max of list reached already.")
continue
if n < upperLim and n > lowerLim and i != k :
if C[i] > C[k] :
#debug info only
print("deleted an overlapping CHAR: '" + str(A[k]) + "', " + str(B1[k]) + ", " + str(C[k]) + ", with yolo info: " + str(D[k]))
#remove detection # [n] from all lists
del B1[k]
del B2[k]
del B3[k]
del A[k]
del C[k]
del D[k]
else:
#remove detection # [m] from all lists
#debug info only
print("deleted an overlapping CHAR: '" + str(A[i]) + "', " + str(B1[i]) + ", " + str(C[i]) + ", with yolo info: " + str(D[i]))
del B1[i]
del B2[i]
del B3[i]
del A[i]
del C[i]
del D[i]
else:
pass
i=i+1
k=k+1
#if results still more CHARs than expected, being deleting the lowest confidence items
check_len = len(C)
while check_len > plate_len_threshold :
#remove detection # [i] from all lists
i = C.index(min(C))
#debug info only
print("more detections than expected (plate_len_threhold = " + str(plate_len_threshold) + "), so the following (low conf) CHAR was deleted : '" + str(A[i]) + "', " + str(B1[i]) + ", " + str(C[i]) + ", with yolo info: " + str(D[i]))
#delete index i
del B1[i]
del B2[i]
del B3[i]
del A[i]
del C[i]
del D[i]
check_len = len(C)
#delete any exisiting yolo text file by that name
if os.path.exists(YoloLabelFilepath) :
os.remove(YoloLabelFilepath)
print(YoloLabelFilepath + " file removed, so a new yolo txt file can be written.")
else:
pass
#write to yolo text file
text_file = open(YoloLabelFilepath,"a")
j=0
for o in D :
text_file.write(D[j] + '\n')
j=j+1
text_file.close()
#debug
print("I wrote all lines to Yolo .txt : " + str(D))
#print results to log file (if looks OK) change directory to where ever you like, or add info
if C != [] and len(C)>min_len_thres:
if min(C) > min_conf_thres :
text_file = open(output_logfile,"a")
text_file.write(filename)
text_file.write("\n")
text_file.write(str(A))
text_file.write("\n")
text_file.write(str(B1))
text_file.write("\n")
text_file.write(str(C))
text_file.write("\n")
text_file.write(str(D))
text_file.write("\n")
text_file.close()
else:
pass
else :
pass
#if best found character really sucks, notify
if max(C) < 0.1:
print("No Char was found in image " + filename)
else:
print("COMPLETED : " + filename + " *** " + str(A) + " *** " + str(B1) + " *** " + str(C) + " *** " + str(D))