Spaces:
Sleeping
Sleeping
Upload 6 files
Browse files- example_captcha1.png.png +0 -0
- example_captcha2.png.png +0 -0
- example_captcha3.png.png +0 -0
- extract_single_letters_from_captchas.py +92 -0
- helpers.py +40 -0
- solve_captchas_with_model.py +112 -0
example_captcha1.png.png
ADDED
|
example_captcha2.png.png
ADDED
|
example_captcha3.png.png
ADDED
|
extract_single_letters_from_captchas.py
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import os.path
|
| 3 |
+
import cv2
|
| 4 |
+
import glob
|
| 5 |
+
import imutils
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
CAPTCHA_IMAGE_FOLDER = "generated_captcha_images"
|
| 9 |
+
OUTPUT_FOLDER = "extracted_letter_images"
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
# Get a list of all the captcha images we need to process
|
| 13 |
+
captcha_image_files = glob.glob(os.path.join(CAPTCHA_IMAGE_FOLDER, "*"))
|
| 14 |
+
counts = {}
|
| 15 |
+
|
| 16 |
+
# loop over the image paths
|
| 17 |
+
for (i, captcha_image_file) in enumerate(captcha_image_files):
|
| 18 |
+
print("[INFO] processing image {}/{}".format(i + 1, len(captcha_image_files)))
|
| 19 |
+
|
| 20 |
+
# Since the filename contains the captcha text (i.e. "2A2X.png" has the text "2A2X"),
|
| 21 |
+
# grab the base filename as the text
|
| 22 |
+
filename = os.path.basename(captcha_image_file)
|
| 23 |
+
captcha_correct_text = os.path.splitext(filename)[0]
|
| 24 |
+
|
| 25 |
+
# Load the image and convert it to grayscale
|
| 26 |
+
image = cv2.imread(captcha_image_file)
|
| 27 |
+
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
|
| 28 |
+
|
| 29 |
+
# Add some extra padding around the image
|
| 30 |
+
gray = cv2.copyMakeBorder(gray, 8, 8, 8, 8, cv2.BORDER_REPLICATE)
|
| 31 |
+
|
| 32 |
+
# threshold the image (convert it to pure black and white)
|
| 33 |
+
thresh = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV | cv2.THRESH_OTSU)[1]
|
| 34 |
+
|
| 35 |
+
# find the contours (continuous blobs of pixels) the image
|
| 36 |
+
contours = cv2.findContours(thresh.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
|
| 37 |
+
|
| 38 |
+
# Hack for compatibility with different OpenCV versions
|
| 39 |
+
contours = contours[1] if imutils.is_cv3() else contours[0]
|
| 40 |
+
|
| 41 |
+
letter_image_regions = []
|
| 42 |
+
|
| 43 |
+
# Now we can loop through each of the four contours and extract the letter
|
| 44 |
+
# inside of each one
|
| 45 |
+
for contour in contours:
|
| 46 |
+
# Get the rectangle that contains the contour
|
| 47 |
+
(x, y, w, h) = cv2.boundingRect(contour)
|
| 48 |
+
|
| 49 |
+
# Compare the width and height of the contour to detect letters that
|
| 50 |
+
# are conjoined into one chunk
|
| 51 |
+
if w / h > 1.25:
|
| 52 |
+
# This contour is too wide to be a single letter!
|
| 53 |
+
# Split it in half into two letter regions!
|
| 54 |
+
half_width = int(w / 2)
|
| 55 |
+
letter_image_regions.append((x, y, half_width, h))
|
| 56 |
+
letter_image_regions.append((x + half_width, y, half_width, h))
|
| 57 |
+
else:
|
| 58 |
+
# This is a normal letter by itself
|
| 59 |
+
letter_image_regions.append((x, y, w, h))
|
| 60 |
+
|
| 61 |
+
# If we found more or less than 4 letters in the captcha, our letter extraction
|
| 62 |
+
# didn't work correcly. Skip the image instead of saving bad training data!
|
| 63 |
+
if len(letter_image_regions) != 4:
|
| 64 |
+
continue
|
| 65 |
+
|
| 66 |
+
# Sort the detected letter images based on the x coordinate to make sure
|
| 67 |
+
# we are processing them from left-to-right so we match the right image
|
| 68 |
+
# with the right letter
|
| 69 |
+
letter_image_regions = sorted(letter_image_regions, key=lambda x: x[0])
|
| 70 |
+
|
| 71 |
+
# Save out each letter as a single image
|
| 72 |
+
for letter_bounding_box, letter_text in zip(letter_image_regions, captcha_correct_text):
|
| 73 |
+
# Grab the coordinates of the letter in the image
|
| 74 |
+
x, y, w, h = letter_bounding_box
|
| 75 |
+
|
| 76 |
+
# Extract the letter from the original image with a 2-pixel margin around the edge
|
| 77 |
+
letter_image = gray[y - 2:y + h + 2, x - 2:x + w + 2]
|
| 78 |
+
|
| 79 |
+
# Get the folder to save the image in
|
| 80 |
+
save_path = os.path.join(OUTPUT_FOLDER, letter_text)
|
| 81 |
+
|
| 82 |
+
# if the output directory does not exist, create it
|
| 83 |
+
if not os.path.exists(save_path):
|
| 84 |
+
os.makedirs(save_path)
|
| 85 |
+
|
| 86 |
+
# write the letter image to a file
|
| 87 |
+
count = counts.get(letter_text, 1)
|
| 88 |
+
p = os.path.join(save_path, "{}.png".format(str(count).zfill(6)))
|
| 89 |
+
cv2.imwrite(p, letter_image)
|
| 90 |
+
|
| 91 |
+
# increment the count for the current key
|
| 92 |
+
counts[letter_text] = count + 1
|
helpers.py
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import imutils
|
| 2 |
+
import cv2
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
def resize_to_fit(image, width, height):
|
| 6 |
+
"""
|
| 7 |
+
A helper function to resize an image to fit within a given size
|
| 8 |
+
:param image: image to resize
|
| 9 |
+
:param width: desired width in pixels
|
| 10 |
+
:param height: desired height in pixels
|
| 11 |
+
:return: the resized image
|
| 12 |
+
"""
|
| 13 |
+
|
| 14 |
+
# grab the dimensions of the image, then initialize
|
| 15 |
+
# the padding values
|
| 16 |
+
(h, w) = image.shape[:2]
|
| 17 |
+
|
| 18 |
+
# if the width is greater than the height then resize along
|
| 19 |
+
# the width
|
| 20 |
+
if w > h:
|
| 21 |
+
image = imutils.resize(image, width=width)
|
| 22 |
+
|
| 23 |
+
# otherwise, the height is greater than the width so resize
|
| 24 |
+
# along the height
|
| 25 |
+
else:
|
| 26 |
+
image = imutils.resize(image, height=height)
|
| 27 |
+
|
| 28 |
+
# determine the padding values for the width and height to
|
| 29 |
+
# obtain the target dimensions
|
| 30 |
+
padW = int((width - image.shape[1]) / 2.0)
|
| 31 |
+
padH = int((height - image.shape[0]) / 2.0)
|
| 32 |
+
|
| 33 |
+
# pad the image then apply one more resizing to handle any
|
| 34 |
+
# rounding issues
|
| 35 |
+
image = cv2.copyMakeBorder(image, padH, padH, padW, padW,
|
| 36 |
+
cv2.BORDER_REPLICATE)
|
| 37 |
+
image = cv2.resize(image, (width, height))
|
| 38 |
+
|
| 39 |
+
# return the pre-processed image
|
| 40 |
+
return image
|
solve_captchas_with_model.py
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from keras.models import load_model
|
| 2 |
+
from helpers import resize_to_fit
|
| 3 |
+
from imutils import paths
|
| 4 |
+
import numpy as np
|
| 5 |
+
import imutils
|
| 6 |
+
import cv2
|
| 7 |
+
import pickle
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
MODEL_FILENAME = "captcha_model.hdf5"
|
| 11 |
+
MODEL_LABELS_FILENAME = "model_labels.dat"
|
| 12 |
+
CAPTCHA_IMAGE_FOLDER = "generated_captcha_images"
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
# Load up the model labels (so we can translate model predictions to actual letters)
|
| 16 |
+
with open(MODEL_LABELS_FILENAME, "rb") as f:
|
| 17 |
+
lb = pickle.load(f)
|
| 18 |
+
|
| 19 |
+
# Load the trained neural network
|
| 20 |
+
model = load_model(MODEL_FILENAME)
|
| 21 |
+
|
| 22 |
+
# Grab some random CAPTCHA images to test against.
|
| 23 |
+
# In the real world, you'd replace this section with code to grab a real
|
| 24 |
+
# CAPTCHA image from a live website.
|
| 25 |
+
captcha_image_files = list(paths.list_images(CAPTCHA_IMAGE_FOLDER))
|
| 26 |
+
captcha_image_files = np.random.choice(captcha_image_files, size=(10,), replace=False)
|
| 27 |
+
|
| 28 |
+
# loop over the image paths
|
| 29 |
+
for image_file in captcha_image_files:
|
| 30 |
+
# Load the image and convert it to grayscale
|
| 31 |
+
image = cv2.imread(image_file)
|
| 32 |
+
image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
|
| 33 |
+
|
| 34 |
+
# Add some extra padding around the image
|
| 35 |
+
image = cv2.copyMakeBorder(image, 20, 20, 20, 20, cv2.BORDER_REPLICATE)
|
| 36 |
+
|
| 37 |
+
# threshold the image (convert it to pure black and white)
|
| 38 |
+
thresh = cv2.threshold(image, 0, 255, cv2.THRESH_BINARY_INV | cv2.THRESH_OTSU)[1]
|
| 39 |
+
|
| 40 |
+
# find the contours (continuous blobs of pixels) the image
|
| 41 |
+
contours = cv2.findContours(thresh.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
|
| 42 |
+
|
| 43 |
+
# Hack for compatibility with different OpenCV versions
|
| 44 |
+
contours = contours[1] if imutils.is_cv3() else contours[0]
|
| 45 |
+
|
| 46 |
+
letter_image_regions = []
|
| 47 |
+
|
| 48 |
+
# Now we can loop through each of the four contours and extract the letter
|
| 49 |
+
# inside of each one
|
| 50 |
+
for contour in contours:
|
| 51 |
+
# Get the rectangle that contains the contour
|
| 52 |
+
(x, y, w, h) = cv2.boundingRect(contour)
|
| 53 |
+
|
| 54 |
+
# Compare the width and height of the contour to detect letters that
|
| 55 |
+
# are conjoined into one chunk
|
| 56 |
+
if w / h > 1.25:
|
| 57 |
+
# This contour is too wide to be a single letter!
|
| 58 |
+
# Split it in half into two letter regions!
|
| 59 |
+
half_width = int(w / 2)
|
| 60 |
+
letter_image_regions.append((x, y, half_width, h))
|
| 61 |
+
letter_image_regions.append((x + half_width, y, half_width, h))
|
| 62 |
+
else:
|
| 63 |
+
# This is a normal letter by itself
|
| 64 |
+
letter_image_regions.append((x, y, w, h))
|
| 65 |
+
|
| 66 |
+
# If we found more or less than 4 letters in the captcha, our letter extraction
|
| 67 |
+
# didn't work correcly. Skip the image instead of saving bad training data!
|
| 68 |
+
if len(letter_image_regions) != 4:
|
| 69 |
+
continue
|
| 70 |
+
|
| 71 |
+
# Sort the detected letter images based on the x coordinate to make sure
|
| 72 |
+
# we are processing them from left-to-right so we match the right image
|
| 73 |
+
# with the right letter
|
| 74 |
+
letter_image_regions = sorted(letter_image_regions, key=lambda x: x[0])
|
| 75 |
+
|
| 76 |
+
# Create an output image and a list to hold our predicted letters
|
| 77 |
+
output = cv2.merge([image] * 3)
|
| 78 |
+
predictions = []
|
| 79 |
+
|
| 80 |
+
# loop over the lektters
|
| 81 |
+
for letter_bounding_box in letter_image_regions:
|
| 82 |
+
# Grab the coordinates of the letter in the image
|
| 83 |
+
x, y, w, h = letter_bounding_box
|
| 84 |
+
|
| 85 |
+
# Extract the letter from the original image with a 2-pixel margin around the edge
|
| 86 |
+
letter_image = image[y - 2:y + h + 2, x - 2:x + w + 2]
|
| 87 |
+
|
| 88 |
+
# Re-size the letter image to 20x20 pixels to match training data
|
| 89 |
+
letter_image = resize_to_fit(letter_image, 20, 20)
|
| 90 |
+
|
| 91 |
+
# Turn the single image into a 4d list of images to make Keras happy
|
| 92 |
+
letter_image = np.expand_dims(letter_image, axis=2)
|
| 93 |
+
letter_image = np.expand_dims(letter_image, axis=0)
|
| 94 |
+
|
| 95 |
+
# Ask the neural network to make a prediction
|
| 96 |
+
prediction = model.predict(letter_image)
|
| 97 |
+
|
| 98 |
+
# Convert the one-hot-encoded prediction back to a normal letter
|
| 99 |
+
letter = lb.inverse_transform(prediction)[0]
|
| 100 |
+
predictions.append(letter)
|
| 101 |
+
|
| 102 |
+
# draw the prediction on the output image
|
| 103 |
+
cv2.rectangle(output, (x - 2, y - 2), (x + w + 4, y + h + 4), (0, 255, 0), 1)
|
| 104 |
+
cv2.putText(output, letter, (x - 5, y - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.55, (0, 255, 0), 2)
|
| 105 |
+
|
| 106 |
+
# Print the captcha's text
|
| 107 |
+
captcha_text = "".join(predictions)
|
| 108 |
+
print("CAPTCHA text is: {}".format(captcha_text))
|
| 109 |
+
|
| 110 |
+
# Show the annotated image
|
| 111 |
+
cv2.imshow("Output", output)
|
| 112 |
+
cv2.waitKey()
|