Batch ID cards printing with Python

Tai Vong
6 min readJun 15, 2022

Covid-19 may be the largest pandemic that ever happened in this century. After holding the line for about 2 years, my country was then suffered from an expansion of the virus, followed by a streak of 4 months locking down.

My company industry is about ride-hailing, which requires a lot of transportation and human interaction. So we must speed up printing the ID card that provides enough information about the driver for the policeman to fasten the validation process. Normally, this thing is done manually, but having lockdowns and the batch processing needed puts the process into a rush. Hence we should develop a tool that can fast print the cards in order to make the strategy work at the fastest pace.

To start printing ID cards, we should have a ready-to-edit prototype. Like in this example, we will load the ready for inputting CSV file that has some ready-to-be-inserted data into this image

Loading the data

As the job’s data comes from the end-user. So we are using CSV as our data format because it is a good format that offers the end-user the ability easy to edit while can be easily readable by anyone. To load a CSV file into python code, we can just simply call the native CSV reader. Each row in the CSV file is a piece of user data, we will load it into a dictionary that is able to handle it later

with open('data.csv', newline='', encoding='utf-8') as csvfile:
spamreader = csv.reader(csvfile, delimiter=',', quotechar='|'):
for row in spamreader:
name=row[0]
phone=row[1]
position=row[2]

Fulfill the data

Drawing the text

The text data is rather easier to handle compared to image one. We will just need a tool to put on a canvas. Then from some fixed coordinates, we land down the predefined text. In this case, I decided to use Pillow, because it’s the most common library when processing images with Python.

img = Image.open("prototype.jpg")
draw.text((210, 570), name, (0, 0, 0), font=font) # this will draw text with Blackcolor and 16 size
draw.text((580, 960), " " * ((8 - len(driver_id)) // 2) + driver_id + " " * ((7 - len(driver_id)) // 2), (0, 0, 0),
font=small_font) # this will draw text with Blackcolor and 16 size

Downloading the profile image

For each user, we have a URL of the portrait image. We will download the image and process it. Also, we implement a simple cache directory with the strategy so that when we rerun some images processed with an error, we don’t have to download the image again.

def download_resource(image_path, image_url: str):
if "http" not in image_url:
return
while image_url[-1] == ';' or image_url[-1] == "'":
image_url = image_url[:-1]
if not os.path.exists(image_path):
img_data = requests.get(image_url).content
with open(image_path, 'wb') as handler:
handler.write(img_data)

Intuitive center crop

Users’ profile images can be at different ratios and in different formats. In order to fit them with our card placeholder, we need to use some image tricks in order to get our goal done. The first thing that comes to my mind at that moment is after taking a quick look over those images, I found that they are in different sizes but all capture the user’s portrait at the picture center.

So the simplest trick that I can do is first to scale the placeholder size (keeping ratio) to an outer box that covers the user’s image. After that, we will check which dimension (height or width) is getting over the outer box. Then we process a center crop at that dimension, resize the image to fit the placeholder, and place it over there.

def center_crop_2(im, new_width, new_height):
im = ImageOps.exif_transpose(im)
im = trim2(im)
width, height = im.size # Get dimensions
new_width_x = height / new_height * new_width
new_height_x = new_width_x / new_width * new_height
print(new_width, new_height, new_width_x, new_height_x, width, height)
if not (new_width_x - width <= 1 and new_height_x - height <= 1):
new_height_x = width / new_width * new_height
new_width_x = new_height_x / new_height * new_width
print(new_width, new_height, new_width_x, new_height_x, width, height)
left = (width - new_width_x) / 2
top = (height - new_height_x) / 2
right = (width + new_width_x) / 2
bottom = (height + new_height_x) / 2
# Crop the center of the image
return im.crop((left, top, right, bottom)).resize((new_width, new_height), Image.ANTIALIAS)

The center crop worked with over 80% of the dataset. However, you cannot expect everybody is a perfectionist. In the dataset, there is still a lot of case the people were not centered. Applying the intuitive method cannot help in those cases.

Apply simple face detection

The center crop strategy takes many downsides that were aforementioned. How about improving it with a litter AI magic trick? Looking over the dataset again, despite the data being big, the photo-capturing method has been standardized without the blurring or diagonal shooting angle. We will need little work to achieve our target. Therefore, I chose to Haar Cascade Classifier to perform a quick face detection over the image. The model is small, quick to run, and less effort to implement.

After running the classifier, we get the bounding box of the detected face, and perform the center crop using the previous method, we got a perfectly cut-off portrait of the user. Finally, I added some salt and pepper padding the image with some background using some adjustment tricks to make the portrait zoom out from the target and have a much more natural feeling.

def center_crop_3(im, new_width, new_height):
im = ImageOps.exif_transpose(im)
im = trim2(im)
width, height = im.size # Get dimensions
if width / new_width > 2 and height / new_height > 2:
# too large
ratio = width / new_width
if ratio > height / new_height:
ratio = height / new_height
ratio = ratio / 2
im = im.resize((int(width // ratio), int(height // ratio)), Image.ANTIALIAS)
open_cv_image = numpy.array(im.convert('RGB'))
open_cv_image = open_cv_image[:, :, ::-1].copy()
faces = face_cascade.detectMultiScale(open_cv_image, 1.1, 4)
(x, y, w, h) = faces[0]
center_x, center_y = (x + w / 2, y + h / 2)
width, height = im.size # Get dimensions
new_width_x = height / new_height * new_width
new_height_x = new_width_x / new_width * new_height
if not (new_width_x - width <= 1 and new_height_x - height <= 1):
new_height_x = width / new_width * new_height
new_width_x = new_height_x / new_height * new_width
left = center_x - new_width_x / 2
top = center_y - new_height_x / 2
right = center_x + new_width_x / 2
bottom = center_y + new_height_x / 2
while left < -1 or right > width or top < -1 or bottom > height:
print(left, right, top, bottom)
if left < -1:
right -= left
left -= left
if right > width:
left -= right - width
right = width
if top < -1:
bottom -= top
top -= top
if bottom > height:
top -= bottom - height
bottom = height
print(left, right, top, bottom)
# Crop the center of the image
return im.crop((left, top, right, bottom)).resize((new_width, new_height), Image.ANTIALIAS)

Combine with a QR

A good addition to the process would be the ability to quickly perform the identification project. So we decide to add a QR code as it is easier for the surveillance officer to scan and record the history. They can also re-check the information that was printed on the card for validation. It’s easy using the python default QR code library.

def generate_qr(file_path: str, name: str, phone: str, identity_number: str, vehicle_number: str, user_id: str):
if not os.path.exists(file_path):
im = qrcode.make(f'Name: {name}\n'
f'Phone: {phone}\n'
f'Identity Number: {identity_number}\n'
f'Vehicle Number: {vehicle_number}\n'
f'User ID: {user_id}')
im.save(file_path)

Conclusion

After a few simple steps, we have managed to create a quick script that can work for thousands of data pieces without a doubt. This script can be moved into a pipeline to create an Id card immediately for a new user. In this article, I tried to demonstrate the process of how to break a task that may be large and hard to be done at the first glance into smaller steps and resolve them one by one. Going in the right direction, sooner or later, the bigger problem will be solved at no time.

--

--