Tutorial: Covid Vaccination Card Verification -- Part 2 of 2
Part 2: Verification Checks
Tutorial Part 2: Verification Checks
After alignment is performed, we need to check to see if the image is actually a COVID vaccination card. We do this by verifying the following three things are true: there is a sufficient number of RANSAC-inliers, the title contains the expected text, the CDC logo is present in the top right.
Check #1: Minimum RANSAC-inlier requirement
In the previous section, we discussed how RANSAC is used to estimate the homography which is used to align the input image. We also discussed how the estimation is supported by certain keypoint matches (inliers), while the rest are discarded (outliers). One way to verify that the alignment was successful is to require a certain number of inliers to be present. This is because if the image contains a vaccination card, we can expect there to be a higher number of keypoint matches that agree with a single homography.
In our testing, we found that images containing vaccination cards tend to contain >15 inliers, while images not containing vaccination cards tend to have < 10 inliers. So we perform the following check:
if RANSAC_inliers < 11:
print("Failed RANSAC inlier verification!")
However, we also found that there are exceptions to this rule, so we perform two additional checks.
Check #2: Verify the title is correct
As an additional verification measure, we verify the title of the card is correct by performing optical character recognition (OCR). OCR is another well-studied computer vision task, in which images of text are converted to computer-encoded text. To do this, we use pytesseract, a popular open-source python OCR package.
First, we must extract the portion of our aligned image which contains the title. We do this by referring to the template image to predefine a region in which we expect the title to be. This is done by manually obtaining the bounding box coordinates via an image editing tool, such as GIMP.
This image is then provided as input to pytesseract’s image_to_string()
method. The full code of our read_title()
method is:
def read_title(aligned, fileTag=None, debug=False) -> str:
# get title ROI
(x, y, w, h) = (0, 0, 1487, 160)
roi = aligned[y:y + h, x:x + w]
...
# OCR the ROI using Tesseract
rgb = cv2.cvtColor(roi, cv2.COLOR_BGR2RGB)
text = pytesseract.image_to_string(rgb)
return text
Now that we have the extracted title, we can check to see if the title string contains the expected text, “COVID-19 Vaccination Record Card”, by comparing against a regex. We find it necessary to allow for variable amounts of whitespace between keywords. The test fails if the correct title is not detected.
expectedTitleRegex = re.compile(r'(COVID\s*-\s*1\s*9\s*Vaccination\s*Record\s*Card)', re.DOTALL)
match = expectedTitleRegex.search(title)
if match is None:
print('Failed title verification!')
Check #3: Verify the logos are present
As the final verification check, we check to see if we can detect the CDC and Department of Health and Human Services logos in the top right of the image. We do this via a process known as template matching.
Again, we begin by converting both the template and scan image to grayscale.
aligned_gray = cv2.cvtColor(aligned, cv2.COLOR_BGR2GRAY)
logo_template_gray = cv2.cvtColor(template_img, cv2.COLOR_BGR2GRAY)
Next, we then utilize OpenCV’s matchTemplate()
method to perform template matching. This method essentially overlays the template over part of the image and computes a similarity score. It repeats this process for every pixel by sliding the template over one pixel at a time and recomputing the similarity score. The function returns a 2D array filled with similarity scores, one corresponding to every pixel.
tm_result = cv2.matchTemplate(aligned_gray, logo_template_gray, cv2.TM_CCOEFF_NORMED)
The matchTemplate()
method can use a number of similarity measures to compute its scores: squared difference, cross-correlation, or zero-mean cross-correlation (also referred to as the correlation coefficient in statistics literature). Normalized versions of each measure are also provided.
For most template matching applications, zero-mean normalized cross-correlation (known as TM_CCOEFF_NORMED
in OpenCV) is the most appropriate. Cross-correlation is directly related to the squared difference, but contains fewer terms and is thus easier to compute. Shifting the image distribution to have zero-mean enables the measure to be insensitive to image brightness. Adding normalization provides insensitivity to image contrast, and constrains the output to be within [-1, 1]. If you’re interested in learning more about template matching similarity measures, I recommend course notes from Duke’s Carlo Tomasi, and Lisa Brown’s publication from CSUR 1992, “A survey of image registration techniques.”
Because we are using the correlation coefficient as our similarity measure, the highest value in the output represents the highest correlation (most similar). We use OpenCV’s minMaxLoc()
method to get the max value and its location.
_, max_zncc_val, _, max_zncc_loc = cv2.minMaxLoc(tm_result)
Given this output, we verify the logo is in the expected location by checking to see if it is within 10 pixels of the top right corner.
if max_zncc_loc[0] < 1470 or max_zncc_loc[1] > 10:
print("Failed CDC logo check. Location should be in top right corner!")
We also check to see if the maximum correlation coefficient is at least 0.4. We found that fraudulent images tend to produce max similarity scores in the range (0.15–0.25), while images of valid vaccination cards produced max values of >0.5.
elif max_zncc_val < 0.4:
print("Failed CDC logo check. Similarity score too low!")
If both above checks pass, then the logo has been detected successfully!
If all three verification checks pass, then the program indicates that a valid COVID vaccination card has been detected!