Unable to decode Aztec barcode - python

I'm trying to decode an Aztec barcode using the following script:
import zxing
reader = zxing.BarCodeReader()
barcode = reader.decode("test.png")
print(barcode)
Here is the input image:
Following is the output:
BarCode(raw=None, parsed=None,
path='/Users/dhiwatdg/Desktop/test.png', format=None, type=None,
points=None)
I'm sure it a valid Aztec barcode. Not the script is not able to decode.

The barcode is not detected because there is a strong "ringing" artifact around the black bars (could be result of "defocus" issue).
We may apply binary threshold for getting higher contrast between black and while.
Finding the correct threshold is difficult...
For finding the correct threshold, we may start with the automatic threshold value returned by cv2.THRESH_OTSU, and increasing the threshold until the barcode is detected.
(Note that need to increase [and not decrease] the threshold is specific to the image above).
Note:
The suggested solution is a bit of an overfit, and it's not very efficient.
Other solution I tried, like sharpening were not working...
Code sample:
import cv2
import zxing
reader = zxing.BarCodeReader()
img = cv2.imread('test.png', cv2.IMREAD_GRAYSCALE) # Read the image as grayscale.
thresh, bw = cv2.threshold(img, 0, 255, cv2.THRESH_OTSU) # Apply automatic binary thresholding.
while thresh < 245:
print(f'thresh = {thresh}')
cv2.imshow('bw', bw) # Show bw image for testing
cv2.waitKey(1000) # Wait 1 second (for testing)
cv2.imwrite('test1.png', bw) # Save bw as input image to the barcode reader.
barcode = reader.decode("test1.png", try_harder=True, possible_formats=['AZTEC'], pure_barcode=True) # Try to read the barcode
if barcode.type is not None:
break # Break the loop when barcode is detected.
thresh += 10 # Increase the threshold in steps of 10
thresh, bw = cv2.threshold(img, thresh, 255, cv2.THRESH_BINARY) # Apply binary threshold
cv2.imwrite('bw.png', bw) # Save bw for debugging
cv2.destroyAllWindows()
print(barcode)
Last value of thresh = 164.
Last bw image:

#Rotem's solution works perfectly. I also found the below solution workable by applying Wiener filter.
import cv2
import zxing
img = cv2.imread("test.png")
dst = cv2.fastNlMeansDenoisingColored(img, None, 10, 10, 7, 21)
cv2.imwrite("test_tmp.png", dst)
reader = zxing.BarCodeReader()
barcode = reader.decode("test_tmp.png")
print(barcode)

Related

Pyzbar Can't Decode QRCode

Have a bunch of QR Code labels printed from the same label printer, all can be read except for this one.
Have tried all solutions from Preprocessing images for QR detection in python
Losing my mind... any help appreciated!
Code is here:
import cv2
import numpy as np
from pyzbar.pyzbar import decode
from pyzbar.pyzbar import ZBarSymbol
from kraken import binarization
from PIL import Image
from qreader import QReader
image_path = r"C:\Users\ASinger\Pictures\hdi_pdfs\page1.png"
# Method 1
im = cv2.imread(image_path, cv2.IMREAD_GRAYSCALE)
ret, bw_im = cv2.threshold(im, 127, 255, cv2.THRESH_BINARY)
barcodes = decode(bw_im, symbols=[ZBarSymbol.QRCODE])
print(f'barcodes: {barcodes}')
# Method 2
im = Image.open(image_path)
bw_im = binarization.nlbin(im)
decoded = decode(bw_im, symbols=[ZBarSymbol.QRCODE])
print(f'decoded: {decoded}')
# Method 3
im = cv2.imread(image_path, cv2.IMREAD_GRAYSCALE)
blur = cv2.GaussianBlur(im, (5, 5), 0)
ret, bw_im = cv2.threshold(blur, 0, 255, cv2.THRESH_BINARY+cv2.THRESH_OTSU)
decoded = decode(bw_im, symbols=[ZBarSymbol.QRCODE])
print(f'decoded: {decoded}')
# Method 4
qreader = QReader()
image = cv2.imread(image_path)
decoded_text = qreader.detect_and_decode(image=image)
print(f'decoded_text: {decoded_text}')
# Method 5
cropped_image = image_path
im2 = Image.open(cropped_image)
im2 = im2.resize((2800, 2800))
im2.save(cropped_image, quality=500)
im2.show()
im3 = cv2.imread(cropped_image, cv2.IMREAD_GRAYSCALE)
ret, bw_im = cv2.threshold(im3, 127, 255, cv2.THRESH_BINARY)
decoded = decode(bw_im, symbols=[ZBarSymbol.QRCODE])
print(f'decoded: {decoded}')
It's difficult to tell why Pyzbar fails, but we may guess that the issue is related to low quality scanning artifacts, and maybe compression artifacts.
Here is a small ROI in native resolution:
As you can see there is a lot of noise and artifacts.
For improving the quality I recommend using cv2.medianBlur filter:
clean_im = cv2.medianBlur(im, 25)
Median filter was selected because it applies fine threshold between black and white.
The size of the filter was selected to be 25 (relatively large) because the resolution of the image is relatively high compared to the details of the QR Code.
Same ROI after filtering:
As you can see the noise is much lower, but the details are blurred.
For improving the issue, we may downscale the image using cv2.resize:
small_clean_im = cv2.resize(clean_im, (512, 512), interpolation=cv2.INTER_AREA)
Downscaling the image with cv2.INTER_AREA interpolation is merging multiple pixels into one pixel (kind of concentrating the data), and also remove noise.
The size 512x512 seems like a good tradeoff between keeping details and removing noise.
Image after medianBlur and resize:
Same image with resize only (without medianBlur) for comparison:
I suppose it's better not to apply a threshold before using Pyzbar decode method.
I assume the decode method uses an internal thresholding algorithm that may be better than our own thresholding.
Complete code sample:
import cv2
from pyzbar.pyzbar import decode
from pyzbar.pyzbar import ZBarSymbol
im = cv2.imread('page1.png', cv2.IMREAD_GRAYSCALE)
clean_im = cv2.medianBlur(im, 25) # Apply median blur for reducing noise
small_clean_im = cv2.resize(clean_im, (512, 512), interpolation=cv2.INTER_AREA) # Downscale the image
barcodes = decode(small_clean_im, symbols=[ZBarSymbol.QRCODE])
print(f'barcodes: {barcodes}')
# Show image for testing
cv2.imshow('small_clean_im', small_clean_im)
cv2.waitKey()
cv2.destroyAllWindows()
Output:
barcodes: [Decoded(data=b'P1693921.001', type='QRCODE', rect=Rect(left=137, top=112, width=175, height=175), polygon=[Point(x=137, y=280), Point(x=304, y=287), Point(x=312, y=119), Point(x=143, y=112)])]
Note:
The processing worked with the sample image, but it is not guaranteed to work with other images.
You may try different filter sizes, and different image sizes for improving the success rate.

opencv threshold problem for finding diff images

I have 2 images like below. I want to have differences two of them.
I tried some codes with threshold. But no threshold different. two images threshold image is all black. How can I make differences are with color white on thresh?
(the difference is just on top-left like a small dark)
before = cv2.imread('1.png')
after = cv2.imread('2.png')
threshb = cv2.threshold(before, 0, 255, cv2.THRESH_BINARY_INV)[1]
thresha = cv2.threshold(after, 0, 255, cv2.THRESH_BINARY_INV)[1]
cv.imshow("before",threshb)
cv.imshow("after",thresha)
Not: I used "structural_similarity" link here for finding differences but it founds a lot of differences :(
I don't need small pixel differences. I need differences like seen with human eyes.
The way to handle that is to do absdiff followed by some gain in Python/OpenCV. When you do the absdiff with these images, the difference is small so will not be much above black. So you have to increase the gain to see the difference. (Or you could set an appropriate threshold)
Input 1:
Input 2:
import cv2
import numpy as np
from skimage import exposure as exposure
# read image 1
img1 = cv2.imread('gray1.png', cv2.IMREAD_GRAYSCALE)
# read image 2
img2 = cv2.imread('gray2.png', cv2.IMREAD_GRAYSCALE)
# do absdiff
diff = cv2.absdiff(img1,img2)
# apply gain
result1 = cv2.multiply(diff, 5)
# or do threshold
result2 = cv2.threshold(diff, 10, 255, cv2.THRESH_BINARY)[1]
# save result
cv2.imwrite('gray1_gray2_diff1.png', result1)
cv2.imwrite('gray1_gray2_diff2.png', result2)
# display result
cv2.imshow('diff', diff)
cv2.imshow('result1', result1)
cv2.imshow('result2', result2)
cv2.waitKey(0)
cv2.destroyAllWindows()
Result 1 (gain):
Result 2 (threshold):

Opencv, Python - How to remove the gray pixels around the date text

I am trying to remove the grayish “noise” surrounding the dates using Python/OpenCV to help the OCR (Optical Character Recognition) to recognize the dates.
The original image looks like this: https://static.mothership.sg/1/2017/03/10-Feb-MC-1.jpg
The python script I tried looked as below. However, I have other similar images in which the contrast or lighting coditions varies.
import cv2
import numpy as np
img = cv2.imread("mc.jpeg")
img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
alpha = 3.5
beta = -2
new = alpha * img + beta
new = np.clip(new, 0, 255).astype(np.uint8)
cv2.imwrite("cleaned.png", new)
I also tried Thresholding and/or adaptiveThresholding and some time, I was able to separate the dates from the grayish background. Sometimes it was very challenging. I wonder is there an automatic way to determine the threshold value ?
Below are example of what I hope to achieve.
Blurry Image:
Otsu's Binarization automatically calculates a threshold value from an image histogram.
# Otsu's thresholding after Gaussian filtering
blur = cv2.GaussianBlur(img,(5,5),0)
ret,Otsu = cv2.threshold(blur,0,255,cv2.THRESH_BINARY+cv2.THRESH_OTSU)
cv2.imwrite("Otsu's_thresholding", Otsu)
see this link
You can try to build a model of the background and then weight each input pixel by that model. The output gain should be relatively constant during most of the image. These are the steps for this method:
Apply a soft median blur filter to get rid of small noise
Get the model of the background via local maximum. Apply a very strong close operation, with a big structuring element (I’m using a rectangular kernel of size 15)
Perform gain adjustment by dividing 255 between each local maximum pixel. Weight this value with each input image pixel.
You should get a nice image where the background illumination is pretty much normalized, threshold this image to get a binary mask of the text
This is the code:
import numpy as np
import cv2
# image path
path = "C:/opencvImages/sheet01.jpg"
# Read an image in default mode:
inputImage = cv2.imread(path)
# Remove small noise via median:
filterSize = 5
imageMedian = cv2.medianBlur(inputImage, filterSize)
# Get local maximum:
kernelSize = 15
maxKernel = cv2.getStructuringElement(cv2.MORPH_RECT, (kernelSize, kernelSize))
localMax = cv2.morphologyEx(imageMedian, cv2.MORPH_CLOSE, maxKernel, None, None, 1, cv2.BORDER_REFLECT101)
# Adjust image gain:
height, width, depth = localMax.shape
# Create output Mat:
outputImage = np.zeros(shape=[height, width, depth], dtype=np.uint8)
for i in range(0, height):
for j in range(0, width):
# Get current BGR pixels:
v1 = inputImage[i, j]
v2 = localMax[i, j]
# Gain adjust:
tempArray = []
for c in range(0, 3):
currentPixel = v2[c]
if currentPixel != 0:
gain = 255 / v2[c]
gain = v1[c] * gain
else:
gain = 0
# Gain set and clamp:
tempArray.append(np.clip(gain, 0, 255))
# Set pixel vec to out image:
outputImage[i, j] = tempArray
# Convert RGB to grayscale:
grayscaleImage = cv2.cvtColor(outputImage, cv2.COLOR_BGR2GRAY)
# Threshold:
threshValue = 110
_, binaryImage = cv2.threshold(grayscaleImage, threshValue, 255, cv2.THRESH_BINARY)
# Write image:
imageFilename = "C:/opencvImages/binaryMask2.png"
cv2.imwrite(imageFilename, binaryImage)
I get the following results testing the complete image:
And the cropped text:
Please note that the gain adjustment operations are not vectorized. The script is slow, mainly because I'm starting with Python and don’t know the proper Numpy syntax to speed-up this operation. I've been using C++ for a long time, so feel free to further improve the code.
Edit:
Please, be aware that your result can only be as good as the quality of your input. See your input and ask yourself "Is this a good input for an automated process?" (Automated processes are usually not very smart). The second picture you posted is very low quality. Not only is blurry but also is low res and has compression artifacts. All these factors will hinder automated processing.
With that said, here's an improvement you can include in the original:
Try to normalize brightness-contrast on the grayscale output:
grayscaleImage = np.uint8(cv2.normalize(grayscaleImage, grayscaleImage, 0, 255, cv2.NORM_MINMAX))
Your grayscale image goes from this:
to this:
A little bit darker and improved on contrast. Let's try to compute the optimal threshold value automatically via Otsu thresholding:
threshValue, binaryImage = cv2.threshold(grayscaleImage, 0, 255, cv2.THRESH_BINARY+cv2.THRESH_OTSU)
It gets you this:
However, we can adjust the result if we add bias to Otsu's threshold, like this:
threshValue, binaryImage = cv2.threshold(grayscaleImage, 0, 255, cv2.THRESH_BINARY+cv2.THRESH_OTSU)
bias = 0.9
threshValue = bias * threshValue
_, binaryImage = cv2.threshold(grayscaleImage, threshValue, 255, cv2.THRESH_BINARY)
That's the best quality you can get with these images using this method.
If you find these suggestions and tips useful, please, at least up-vote my answer.

Reading numbers using PyTesseract

I am trying to read numbers from images and cannot find a way to get it to work consistently (not all images have numbers). These are the images:
(here is the link to the album in case the images are not working)
This is the command I'm using to run tesseract on the images: pytesseract.image_to_string(image, timeout=2, config='--psm 13 --oem 3 -c tessedit_char_whitelist=0123456789'). I have tried multiple configurations, but this seems to work best.
As far as preprocessing goes, this works the best:
gray = cv2.cvtColor(np.array(img), cv2.COLOR_RGB2GRAY)
gray = cv2.bilateralFilter(gray, 11, 17, 17)
im_bw = cv2.threshold(gray, thresh, 255, cv2.THRESH_BINARY_INV)[1]
This works for all images except the 3rd one. To solve the problem of lines in the 3rd image, i tried getting the edges with cv2.Canny and a pretty large threshold which works, but when drawing them back, even though it gets more than 95% of each number's edges, tesseract does not read them correctly.
I have also tried resizing the image, using cv2.morphologyEx, blurring it etc. I cannot find a way to get it to work for each case.
Thank you.
cv2.resize has consistently worked for me with INTER_CUBIC interpolation.
Adding this last step to pre-processing would most likely solve your problem.
im_bw_scaled = cv2.resize(im_bw, (0, 0), fx=4, fy=4, interpolation=cv2.INTER_CUBIC)
You could play around with the scale. I have used '4' above.
EDIT:
The following code worked with your images very well, even special characters. Please try it out with the rest of your dataset. Scaling, OTSU and erosion was the best combination.
import cv2
import numpy
import pytesseract
pytesseract.pytesseract.tesseract_cmd = "<path to tesseract.exe>"
# Page segmentation mode, PSM was changed to 6 since each page is a single uniform text block.
custom_config = r'--psm 6 --oem 3 -c tessedit_char_whitelist=0123456789'
# load the image as grayscale
img = cv2.imread("5.png",cv2.IMREAD_GRAYSCALE)
# Change all pixels to black, if they aren't white already (since all characters were white)
img[img != 255] = 0
# Scale it 10x
scaled = cv2.resize(img, (0,0), fx=10, fy=10, interpolation = cv2.INTER_CUBIC)
# Retained your bilateral filter
filtered = cv2.bilateralFilter(scaled, 11, 17, 17)
# Thresholded OTSU method
thresh = cv2.threshold(filtered, 0, 255, cv2.THRESH_BINARY_INV | cv2.THRESH_OTSU)[1]
# Erode the image to bulk it up for tesseract
kernel = numpy.ones((5,5),numpy.uint8)
eroded = cv2.erode(thresh, kernel, iterations = 2)
pre_processed = eroded
# Feed the pre-processed image to tesseract and print the output.
ocr_text = pytesseract.image_to_string(pre_processed, config=custom_config)
if len(ocr_text) != 0:
print(ocr_text)
else: print("No string detected")

How to implement imbinarize in OpenCV

I developed script in Matlab which is analysing engraved text on a colour steal. I'm using range of morphological techniques to extract the text and read it with OCR. I need to implement it on Raspberry Pi therefore I decided to transfer my Matlab code into OpenCV (in python). I tried to transfer some methods and they work similarly but how do I implement imreconstruct and imbinarize (shown below) to OpenCV? (the challenge here is appropriate differentiate foreground and background).
Maybe I should try adding grabCut or getStructuringElement or morphologyEx or dilate? I tried them in range of combinations but have not found a perfect solution.
I will put the whole script for both if anyone could give me suggestions on how to generally improve this extraction and accuracy of OCR process I would greatly appreciate it.
Based on bin values of grey-scale image. I change some parameters in
those functions:
Matlab:
se = strel('disk', 300);
img = imtophat(img, se);
maker = imerode(img, strel('line',100,0)); %for whiter ones
maker = imerode(img, strel('line',85,0)); %for medium
maker = imerode(img, strel('line',5,0));
imgClear = imreconstruct(maker, img);
imgBlur = imgaussfilt(imgClear,1); %less blur for whiter frames
BW = imbinarize(imgBlur,'adaptive','ForegroundPolarity','Bright',...
'Sensitivity',0.7); %process for medium
BW = imbinarize(imgBlur, 'adaptive', 'ForegroundPolarity',...
'Dark', 'Sensitivity', 0.4); % process for black and white
res = ocr(BW, 'CharacterSet', '0123456789', 'TextLayout', 'Block');
res.Text;
OpenCv
kernel = numpy.ones((5,5),numpy.uint8)
blur = cv2.GaussianBlur(img,(5,5),0)
erosion = cv2.erode(blur,kernel,iterations = 1)
opening = cv2.morphologyEx(erosion, cv2.MORPH_OPEN, kernel)
#bremove = cv2.grabCut(opening,mask,rect,bgdModelmode==GC_INIT_WITH_MASK)
#th3 = cv2.adaptiveThreshold(opening,255,cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY_INV+cv2.THRESH_OTSU,11,2)
ret, thresh= cv2.threshold(opening,0,255,cv2.THRESH_BINARY+cv2.THRESH_OTSU)
ocr = pytesseract.image_to_string(Image.open('image2.png'),config='stdout -c tessedit_char_whitelist=0123456789')
Here is the input image:
I am surprised at how much difference between matlab and opencv there is when they both appear to use the same algorithm. Why do you run imbinarize twice? What does the sensitivity keyword actually do (mathematically, behind the background). Because they obviously have several steps more than just the bare OTSU.
import cv2
import numpy as np
import matplotlib.pyplot as plt
def show(img):
plt.imshow(img, cmap="gray")
plt.show()
img = cv2.imread("letters.jpg", cv2.IMREAD_GRAYSCALE)
kernel = np.ones((3,3), np.uint8)
blur = cv2.GaussianBlur(img,(3,3), 0)
erosion = cv2.erode(blur, kernel, iterations=3)
opening = cv2.dilate(erosion, kernel)
th3 = cv2.adaptiveThreshold(opening, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
cv2.THRESH_BINARY, 45, 2)
show(th3)
kernel2 = cv2.getGaussianKernel(6, 2) #np.ones((6,6))
kernel2 = np.outer(kernel2, kernel2)
th3 = cv2.dilate(th3, kernel2)
th3 = cv2.erode(th3, kernel)
show(th3)
The images that get displayed are:
After a bit of cleaning up:
So all in all not the same and certainly not as nice as matlab. But the basic principle seems the same, it's just that the numbers need playing with.
A better approach would probably be to do a threshold by the mean of the image and then use the output of that as a mask to adaptive threshold the original image. Hopefully then the results would be better than both opencv and matlab.
Try doing it with ADAPTIVE_THRESH_MEAN_C you can get some really nice results but there's more trash lying around. Again, maybe if you can use it as a mask to isolate the text and then do tresholding again it might turn out to be better. Also the shape of the erosion and dilation kernels will make a big difference here.
I worked out the code to have a positive result based on your engraved text sample.
import cv2
import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline
def show(img):
plt.imshow(img, cmap="gray")
plt.show()
# load the input image
img = cv2.imread('./imagesStackoverflow/engraved_text.jpg',0);
show(img)
ret, mask = cv2.threshold(img, 60, 120, cv2.THRESH_BINARY) # turn 60, 120 for the best OCR results
kernel = np.ones((5,3),np.uint8)
mask = cv2.erode(mask,kernel,iterations = 1)
show(mask)
# I used a version of OpenCV with Tesseract, you may use your pytesseract and set the modes as:
# OCR Enginer Mode (OEM) = 3 (defualt = 3)
# Page Segmentation mode (PSmode) = 11 (defualt = 3)
tesser = cv2.text.OCRTesseract_create('C:/Program Files/Tesseract 4.0.0/tessdata/','eng','0123456789',11,3)
retval = tesser.run(mask, 0) # return string type
print 'OCR:' + retval
Processed image and OCR output:
It would be great if you can feedback your test results with more sample images.
opencvpythontesseractocr
What I can see from your code is you have used tophat filtering in your Matlab code as the first step. However, I couldn't see the same in your python OpenCV code.
Python has built in tophat filter try applying that for getting similar result
kernel = np.ones((5,5),np.uint8)
tophat = cv2.morphologyEx(img, cv2.MORPH_TOPHAT, kernel)
Also, try using CLAHE it gives better contrast to your image and then apply blackhat to filter out small details.
clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8,8))
cl1 = clahe.apply(img)
I have got better results by applying these transformations.
Tried below, it works to recognize the lighter engraved text sample. Hope it helps.
def show(img):
plt.imshow(img, cmap="gray")
plt.show()
# load the input image
img = cv2.imread('./imagesStackoverflow/engraved_text2.jpg',0);
show(img)
# apply CLAHE to adjust the contrast
clahe = cv2.createCLAHE(clipLimit=5.1, tileGridSize=(5,3))
cl1 = clahe.apply(img)
img = cl1.copy()
show(img)
img = cv2.GaussianBlur(img,(3,3), 1)
ret, mask = cv2.threshold(img, 125, 150, cv2.THRESH_BINARY) # turn 125, 150 for the best OCR results
kernel = np.ones((5,3),np.uint8)
mask = cv2.erode(mask,kernel,iterations = 1)
show(mask)
# I used a version of OpenCV with Tesseract, you may use your pytesseract and set the modes as:
# Page Segmentation mode (PSmode) = 11 (defualt = 3)
# OCR Enginer Mode (OEM) = 3 (defualt = 3)
tesser = cv2.text.OCRTesseract_create('C:/Program Files/Tesseract 4.0.0/tessdata/','eng','0123456789',11,3)
retval = tesser.run(mask, 0) # return string type
print 'OCR:' + retval

Categories