用 python 解碼實價登錄的地址圖片

Posted by TJ Wei on 星期一, 11月 19, 2012 with 9 comments
早上搭車的時候,在手機上看到了這個 twitter:
我在痞客邦 PIXNET 新增了篇文章:2012年9月實價登錄已經爬完了2012年9月實價登錄資料已經爬完了
聯結提到地址部份還沒有 OCR。
爺爺曾經說過:「如果手邊沒有好用的 OCR 工具,就自己寫一個」。所以我下班後就寫了一個。七拼八湊的,但勉強能跑就是了。
程式碼在 https://sites.google.com/site/xmktjw/Home/files/img2txt.zip&d=1
內含解碼結果。

需要榮尼王的地址原始圖片
Dropbox 22.37M: https://www.dropbox.com/s/4p9ol2xjib6v9zk/images-address-20121117.zip
解開放在 address 目錄。
然後還需要把新細明體 mingliu.ttc 及 ARIALN.TTF 放在 font 目錄下。

個人建議內政部和廠商可以進一步將地址及價格的圖片利用 CAPTCHA 的方式處理,就能簡單防止別人抓資料了。

更新:這不是一個一般性的OCR解法,如 Y. Chao 提到的,有開源的 OCR :tesseract-ocrocropus. 也有現成中文的訓練資料和 python module,不然自己訓練也成。
和本文作法的差異有點像是手排與自排的差異。
不,仔細想想,應該是腳踏車與汽車的差異。
改過的腳踏車可以順便處理價格的圖形,還沒有跟榮尼王的資料比對過。
 https://sites.google.com/site/xmktjw/Home/files/img2txt.zip



需要的 python module 是 numpy, opencv, pygame, pil 。然後用 python 2.7 的 idle 跑即可。
演算法很簡單的就是把新細明體的字體解出來,然後用 opencv 暴力跟實價登錄的圖片地址比對。 阿拉柏數字不曉得是什麼字體,所以先用 ARIALN.TTF粗略的抓到圖片檔中的位置,然後把 bitmap 擷取下來。
pygame 是用來弄出 TTF 字型的 bitmap。PIL 是因為我不曉得怎麼直接把 pygame 的 surface 轉成可用的 numpy array。
freq.txt 是之前中文手寫輸入法蒐集到的常用字表,不過有點壞掉的樣子,所以補了幾個字回去。


# encoding: utf8
import Image
import pygame
import numpy as np
import sys
import os
import cv2.cv as cv
import cv2
import shutil

def normalize(im):    
    im=cv2.cvtColor(im, cv2.CV_32F)
    im=cv2.cvtColor(im, cv.CV_RGB2GRAY)
    im=cv2.equalizeHist(im)
    return im

# load bitmap of a char from TTF
pygame.init()
default_font1= pygame.font.Font("font/mingliu.ttc", 12)
cache={}
internal_digit={}
def get_char(txt, fname=None, size=None):
    if txt.isdigit() and fname==None and size==None:
        return internal_digit[txt]
    if fname==None:
        if txt in cache:
            return cache[txt]
        font= default_font1
    else:
        if size == None:
            size=12
        font = pygame.font.Font(fname, size)    
    t = font.render(txt, False, (255, 255, 255), (0,0,0))
    wh=(t.get_width(), t.get_height())
    t_str=pygame.image.tostring(t, "RGBA")
    rtn=normalize(np.array(Image.fromstring("RGBA", wh, t_str)))
    if fname==None:
        cache[txt]=rtn
    return rtn

# find the bitmap of a charater in an image
def find_match(img, txt, fname=None, size=None):
    template=get_char(txt, fname, size)
    result=cv2.matchTemplate(img, template, cv2.TM_SQDIFF)
    r=result.min()    
    c=np.unravel_index(result.argmin(),result.shape)
    pt2=(c[1]+template.shape[1], c[0]+template.shape[0])
    return r, (c[1],c[0]), pt2

# built internal digit image bitmaps
for i,nums in enumerate([u"41760", u"25",  u"9", u"8", u"3"]):
    im0=normalize(cv2.imread("num%d.png"%(i+1)))
    for c in nums:
        r, pt1, pt2=find_match(im0, c, "font/ARIALN.TTF", 13 if i<3 else 12)
        internal_digit[c]=im0[pt1[1]:pt2[1], pt1[0]:pt2[0]]

# load char table
uchars=u"0123456789~一強沂"+open("freq.txt").read().decode("big5")[3:]
uchars={x:0 for x in uchars if x not in u"\n "}

# decode an image
def decode_img(img):
    h,w=img.shape
    x=0
    rtn=u""
    uitems=list(reversed(sorted([(v,k) for k,v in uchars.items()])))
    while x < w-8:        
        part_img=img[0:h, x:x+14]
        if part_img.max()<0.1:            
            x+=14
            continue
        best_c=None        
        for v,c in uitems:
            r, pt1, pt2=find_match(part_img, c)            
            if  pt1[0]<5 and  4> r:
                rtn+=c
                uchars[c]+=1 
                x+=(pt2[0]-1)
                best_c=c
                break
        if not best_c:
            rtn+="???"            
            break 
    return rtn

# main
cv2.namedWindow("win")
for f in os.listdir("address"):    
    im0=normalize(cv2.imread("address/"+f))
    im1=cv2.cvtColor(im0, cv.CV_GRAY2RGB)
    cv2.imshow("win", im1)
    if cv2.waitKey(10)==27:
        break
    addr=decode_img(im0)    
    print f, addr
    if len(addr)<3 or addr[-3:]=="???":
        shutil.copy("address/"+f, "error/"+f)
cv2.destroyAllWindows()
Categories: