From df4806c64c48c2cd2cee063611b3193a47c069c8 Mon Sep 17 00:00:00 2001 From: Tim Starling Date: Thu, 25 Sep 2014 20:56:50 +1000 Subject: [PATCH] Improve FancyCaptcha resistance to OCR Tesseract is a popular open source OCR package. Running it on FancyCaptcha images, with no training or configuration, yielded a 56% break rate. By restricting the character set, the OCR break rate was improved to 66%. So: * Reduce k, increase wob scale, increase rr fuzz. The net effect of these three changes is to more reliably bend the baseline. In the old captcha, the baseline would often be bent by chance, but when it wasn't bent, it provided a very easy challenge for the OCR engine. This reduced the break rate from 66% to around 40%. * Introduce additive noise, based on a bilinear upscale of a random greyscale image. This, combined with the above change, reduces the Tesseract break rate to 6%. Change-Id: I05b5bb6475de9378cd89cce13b1b2f28b32cd405 --- captcha.py | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/captcha.py b/captcha.py index 558b9fab8..22ecd36f9 100644 --- a/captcha.py +++ b/captcha.py @@ -39,6 +39,7 @@ try: import ImageDraw import ImageEnhance import ImageOps + import ImageMath except: sys.exit("This script requires the Python Imaging Library - http://www.pythonware.com/products/pil/") @@ -49,7 +50,7 @@ def wobbly_copy(src, wob, col, scale, ang): x, y = src.size f = random.uniform(4*scale, 5*scale) p = random.uniform(0, math.pi*2) - rr = ang+random.uniform(-30, 30) # vary, but not too much + rr = ang+random.uniform(-10, 10) # vary, but not too much int_d = Image.new('RGB', src.size, 0) # a black rectangle rot = src.rotate(rr, Image.BILINEAR) # Do a cheap bounding-box op here to try to limit work below @@ -86,8 +87,8 @@ def gen_captcha(text, fontname, fontsize, file_name): x, y = im.size # add the text to the image d.text((x/2-dim[0]/2, y/2-dim[1]/2), text, font=font, fill=fgcolor) - k = 3 - wob = 0.20*dim[1]/k + k = 2 + wob = 0.09*dim[1] rot = 45 # Apply lots of small stirring operations, rather than a few large ones # in order to get some uniformity of treatment, whilst @@ -102,9 +103,24 @@ def gen_captcha(text, fontname, fontsize, file_name): bbox = im.getbbox() bord = min(dim[0], dim[1])/4 # a bit of a border im = im.crop((bbox[0]-bord, bbox[1]-bord, bbox[2]+bord, bbox[3]+bord)) + + # Create noise + nblock = 4 + nsize = (im.size[0] / nblock, im.size[1] / nblock) + noise = Image.new('L', nsize, bgcolor) + data = noise.load() + for x in range(nsize[0]): + for y in range(nsize[1]): + r = random.randint(0, 100) + data[x, y] = r + # Turn speckles into blobs + noise = noise.resize(im.size, Image.BILINEAR) + # Add to the image + im = ImageMath.eval('convert(convert(a, "L") / 2 + b, "RGB")', a=im, b=noise) + # and turn into black on white im = ImageOps.invert(im) - + # save the image, in format determined from filename im.save(file_name)