Skip to content

Instantly share code, notes, and snippets.

@ireun
Last active April 14, 2026 06:42
Show Gist options
  • Select an option

  • Save ireun/c54f6ad4de6bd6f4ff691bb104c9f983 to your computer and use it in GitHub Desktop.

Select an option

Save ireun/c54f6ad4de6bd6f4ff691bb104c9f983 to your computer and use it in GitHub Desktop.

Hello!

I present You a simple script, that can help you to scan Game Cards, or Photos, multiple at once. I've made it as a project for my university, it was meant to preserve old images and cards I had.

It basically finds the individual cards/photos, extracts them and rotates them to straight orientation.

I share it with You under Creative Commons Licence. Please respect that.

Few assumptions:

  • Place game cards/Photos in ( more or less ) straight orientation, avoid 45° rotation (0° = best quality, 45° = worst quality)
  • Scan to multi-page PDF, place the file name in line 93
  • DO NOT overlap images, about 3-5 mm margin is safe, otherwise script may detect two images as one!
  • Remember - usually scanners are not scanning anything that's placed to about 3 mm from edges
  • If you'd like to scan cards with white borders - do not close the lid, change white_cards to True
  • For Photos you may need to change the cv.threshold(...) on line 17 to something like cv.threshold(gray, 240, 255, cv.THRESH_BINARY_INV)

Needed packages:

  • pip install opencv-python PyMuPDF numpy

Showdown ( Those are Munchkin 3 cards, blurred due to copyright. )

Before ( original scan )

image

After ( extracted images )

image

import cv2 as cv
import numpy as np
import fitz
import os
from datetime import datetime
from pathlib import Path
white_cards = True # DO NOT CLOSE THE LID WHEN SCANNING WHITE CARDS!
def create_mask(img):
gray = cv.cvtColor(img, cv.COLOR_BGR2GRAY)
if white_cards:
gray = cv.bitwise_not(gray)
_, threshold = cv.threshold(gray, 250, 255, cv.THRESH_BINARY_INV + cv.THRESH_OTSU)
contours, _ = cv.findContours(threshold, cv.RETR_CCOMP, cv.CHAIN_APPROX_SIMPLE)
for cnt in contours:
cv.drawContours(threshold, [cnt], 0, 255, -1)
threshold = cv.bitwise_not(threshold)
kernel = cv.getStructuringElement(cv.MORPH_RECT, (5, 5))
mask = cv.bitwise_not(cv.morphologyEx(threshold, cv.MORPH_CLOSE, kernel, iterations=3))
return mask
def trim_border(array):
return np.mean(array) > 127.5 if not white_cards else np.mean(array) < 127.5
def detect_and_cut(mask, img):
contours, _ = cv.findContours(mask, 1, 2)
img_return = [] # array with processed images.
for cnt in contours:
# get boundigRect ( w/o rotation )
x, y, w, h = cv.boundingRect(cnt)
# get minAreaRect ( w/ rotation )
rect = cv.minAreaRect(cnt)
angle = rect[2]
if angle > 45:
angle -= 90
image = img[y:y + h, x:x + w] # crop image by boudingRect
image_center = tuple(np.array(image.shape[1::-1]) / 2) # get center point of img
rot_mat = cv.getRotationMatrix2D(image_center, angle, 1.0) # get rotationMatrix from angle
result = cv.warpAffine(image, rot_mat, image.shape[1::-1], flags=cv.INTER_LINEAR) # warp by rotationMatrix
# fill conrers after rotation
height, width, _ = result.shape
for point in [(0, 0), (width - 1, 0), (0, height - 1), (width - 1, height - 1)]:
for _ in range(2):
cv.floodFill(result, None, seedPoint=point, newVal=(0,) * 3 if white_cards else (255,) * 3, loDiff=(0,) * 4, upDiff=(10,) * 4)
# final croping
mask2 = create_mask(result)
contours2, _ = cv.findContours(mask2, 1, 2)
for cnt2 in contours2:
x2, y2, w2, h2 = cv.boundingRect(cnt2) # get final boundingRect
result = result[y2:y2 + h2, x2:x2 + w2]
untrimmed = True
while untrimmed:
untrimmed = False
if trim_border(result[:1]):
result = result[1:, :]
untrimmed = True
if trim_border(result[-1:]):
result = result[:-1]
untrimmed = True
if trim_border(result[:, :1]):
result = result[:, 1:]
untrimmed = True
if trim_border(result[:, -1:]):
result = result[:, :-1]
untrimmed = True
img_return.append(result)
return img_return
file = "filename.pdf"
# open the file
pdf_file = fitz.Document(file)
image_count = 0
folder_name = datetime.now().strftime("extracted %m-%d-%y %H-%M-%S")
for page_index in range(len(pdf_file)):
# get the page itself
page = pdf_file[page_index]
image_list = page.get_images()
for img in page.get_images():
# get the XREF of the image
xref = img[0]
# extract the image bytes
base_image = pdf_file.extract_image(xref)
image_bytes = base_image["image"]
nparr = np.frombuffer(image_bytes, np.uint8)
img = cv.imdecode(nparr, cv.IMREAD_COLOR)
mask = create_mask(img) # create a mask
images = detect_and_cut(mask, img) # frame images
for img in images:
Path(folder_name).mkdir(exist_ok=True)
cv.imwrite(os.getcwd() + "\\" + folder_name + "\\" + str(image_count) + ".jpg", img)
image_count += 1
@ireun
Copy link
Copy Markdown
Author

ireun commented Nov 15, 2024

And what is the input?

@BurzumBlacker
Copy link
Copy Markdown

BurzumBlacker commented Nov 15, 2024

Seems that the chat don't support PDFs

@ireun
Copy link
Copy Markdown
Author

ireun commented Nov 15, 2024

You can probably create a secret gist here, and share a link with me, or contact me via discord:)

@SilkBC
Copy link
Copy Markdown

SilkBC commented Apr 13, 2026

What is the proper usage of this script? I have JPG of six cards that I want to run it against and I put the path to the JPG file on line 93, but there is no output that I can see. Once I added the path on like 93, I just simply ran 'python CardScanExtractor.py", which then just returned me back to my command prompt with no apparent output.

Thanks! :-)

@ireun
Copy link
Copy Markdown
Author

ireun commented Apr 13, 2026

Please read the readme:

Scan to multi-page PDF, place the file name in line 93

This script is intended to be used with a scanner. Your best bet (other than modifying) the script is to just create multi-page-pdf from the JPGs you have and toss the PDF into line 93.

@SilkBC
Copy link
Copy Markdown

SilkBC commented Apr 13, 2026

This script is intended to be used with a scanner. Your best bet (other than modifying) the script is to just create multi-page-pdf from the JPGs you have and toss the PDF into line 93.

I just "printed" the JPG file to PDF and put that in the filename line. I now get this error:

/usr/lib64/python3.14/site-packages/numpy/_core/fromnumeric.py:3860: RuntimeWarning: Mean of empty slice.
return _methods._mean(a, axis=axis, dtype=dtype,
/usr/lib64/python3.14/site-packages/numpy/_core/_methods.py:144: RuntimeWarning: invalid value encountered in scalar divide
ret = ret.dtype.type(ret / rcount)
Traceback (most recent call last):
File "/home/murra1/Downloads/../bin/CardScanExtractor.py", line 121, in
cv.imwrite(os.getcwd() + "\" + folder_name + "\" + str(image_count) + ".jpg", img)
~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
cv2.error: OpenCV(4.13.0) /io/opencv/modules/imgcodecs/src/loadsave.cpp:1193: error: (-215:Assertion failed) !_img.empty() in function 'imwrite'

I am running this on a Linux PC.

When you say "multi-page PDF", is the script expecting to find one image per page? My scan has six cards in it.

@ireun
Copy link
Copy Markdown
Author

ireun commented Apr 13, 2026

one-pdf-page should contain one-scan-image
one-scan-image can contain many cards on it, just like in my example from readme
there may be multiple pdf pages in the pdf

I'm not sure about your error, there is probably something wrong with the PDF you've created - seems like img was empty for whatever reason

@SilkBC
Copy link
Copy Markdown

SilkBC commented Apr 13, 2026

one-pdf-page should contain one-scan-image one-scan-image can contain many cards on it, just like in my example from readme there may be multiple pdf pages in the pdf

I'm not sure about your error, there is probably something wrong with the PDF you've created - seems like img was empty for whatever reason

OK, thanks for that. I will try scanning the cards again but "directly" to a PDF instead of a JPEG (or "printing" a JPEG to a PDF) and give that a try. It could be that "printing" a JPEG to PDF messes with the PDF information a bit that your script is unable to interpret (and/or possibly a scan to PDF has some different information within that a "print to PDF" might have). I dunno, just speculating.

I will let you know either way what the outcome is of the scan-to-PDF from my scanner.

Also, as far as you know, does your script care if it is run on a Linux system vs. a Windows system?

@ireun
Copy link
Copy Markdown
Author

ireun commented Apr 13, 2026

I have never tested in under linux, sorry

@SilkBC
Copy link
Copy Markdown

SilkBC commented Apr 14, 2026

OK, so I scanned my cards directly to PDF and I get the same error. I am even running it under Python for Windows. I even tried scanning just a single card to PDF and rand the script against that but same error.

The images definitely appear when I open the PDF. Could the scanning application have anything to do with it? I am using the "Scan" app from the Windows store and a Brother MFC as my scanner.

**EDIT: Looks like the scanner app is at least part of the problem. I downloaded and used "NAPS2" and when I saved as PD, the script ran against it without error, and extracted a bunch of JPGs, even though there was only one image. It "extracted" 9 images, none of which are the card I scanned. The card is white, and I have "white_cards = True", and I also scanned with my lid open but the background the card is on is grey rather than black. Will try again with a clack "something" over top of the card so a black background does get scanned.

**Edit2: OK, I just don't know anymore. I put the folded gameboard on top of the card on my scanner, as it is black. Scanned it in using NAPS2, saved it as a PDF, but when I run the script against the PDF, I ma getting that error again, like it isn't detecting a card :-(

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment