小松鼠嚇了一跳,有了魔法眼鏡後,這世界看起來完全不一樣了

2012年11月19日 星期一

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

早上搭車的時候,在手機上看到了這個 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()

2012年11月10日 星期六

Diamond Dash 的 Python BOT


Diamond Dash 是 Facebook 以及 iOS 上面常見的社群 Puzzle Game,其實沒有怎麼在玩,所以也只有十幾級而已。玩的時候想起了以前寫的寶石方塊機器人。發現我已經記不太清楚那時候是在什麼平台下面,用什麼語言寫的了。翻了很久硬碟,才找到程式碼。我的收納習慣太糟,如果沒有放在 blog 上,大概就找不到了。 所以改寫了一下,成為 Diamond Dash 版,放在這裡。
功能很簡單,就是抓螢幕的圖,然後用白點在遊戲視窗標出可以消去的方塊。
程式寫得很草率,只是剛好能動。需要 pywin32,只適用於 windows 下的 firefox 。
而且最重要的是,其實對分數沒有什麼幫助。
import win32ui, win32gui, win32con
import time

def find_windows_by_title(title):
    wins=[]
    win32gui.EnumWindows(lambda i,e:e.append(i) ,wins)
    return [w for w in wins if title in win32gui.GetWindowText(w)]

def get_child_windows(w):
    wins=[]    
    try:
        win32gui.EnumChildWindows(w, lambda i,e:e.append(i) ,wins)
    except:
        pass    
    return wins

def find_game_windows():    
    browser_windows=find_windows_by_title("Diamond Dash")    
    child_windows=sum(map(get_child_windows, browser_windows),[])
    cond1=lambda r:(r[2]-r[0],r[3]-r[1])==(760,500)
    cond2=lambda i:win32gui.IsWindowVisible(i)
    cond=lambda i:cond1(win32gui.GetWindowRect(i)) and cond2(i)
    return filter(cond, child_windows)

def get_color(bits):
    return ord(bits[2])|(ord(bits[1])<<8)|(ord(bits[0])<<16)

def do_the_math(m):
    gmap=[[[(j,i)] for i in range(10)] for j in range(9)]    
    for j in range(9):
        for i in range(10):            
            if j>0 and m[j][i]==m[j-1][i]:
                gmap[j-1][i].extend(gmap[j][i])
                gmap[j][i]=gmap[j-1][i]
            if i>0 and m[j][i]==m[j][i-1]:
                gmap[j][i-1].extend(gmap[j][i])
                for j2,i2 in gmap[j][i]:
                    gmap[j2][i2]=gmap[j][i-1]
    for j in range(9):
        for i in range(10):
            gmap[j][i]=len(gmap[j][i])
    return gmap

def main_loop(win):
    dc=win32ui.CreateDCFromHandle(win32gui.GetDC(win))
    mem_dc=dc.CreateCompatibleDC()
    bitmap = win32ui.CreateBitmap()
    # 400 = 40 pixels * 10 diamonds
    # 360 = 40 pixels * 9 diamonds
    bitmap.CreateCompatibleBitmap(dc,400,360)
    mem_dc.SelectObject(bitmap)
    # 160=40 pixels * 4 colors(RGBA).
    # 1600=160 * 10 diamonds
    row=lambda s:[get_color(bits[i:i+3]) for i in range(s,s+1600,160)]
    while 1:
        time.sleep(0.005)
        mem_dc.BitBlt((0, 0), (400, 360), dc, (81+15, 92+15), win32con.SRCCOPY)
        bits = bitmap.GetBitmapBits(True)            
        # 64000 = 1600 * 40 576000=64000 * 9
        matrix=map(row, range(0, 576000, 64000))
        rtn=do_the_math(matrix)
        for j,y in enumerate(range(92,450,40)):
            for i,x in enumerate(range(81,480,40)):
                box=(x+20,y+20,x+30,y+30)
                if rtn[j][i]>=3:
                    dc.FillSolidRect(box, 0xffffff)
                    
wins=find_game_windows()
print "find", wins
if wins:
    main_loop(wins[-1]) # Using the last window works for me.
else:
    print "Unable to find games."

2012年11月7日 星期三

GeoGebra 開箱

在臉書上看到蔡炎龍老師利用 GeoGebra 做出的這個圖片。題目看起來還挺有趣的,所以就想了一下。第一個想法是用複數,在白板上面稍微算了一下果然可行。但直覺這種東西應該有幾何上的意義,所以盯著這個圖片看了一陣子,總算看出了圖形化的證明。
但一來憑空想像的圖形有時會出錯,二來要怎麼把這個解法貼出?
於是,我想不妨試看看這個蔡老師推廣的 GeoGebra 好了。
第一步當然是先用 google 找到 GeoGebra 網站,發現是 Java 軟體,點一下然後等一下就能打開。稍微摸索一下,就上手畫出跟上面一樣的圖形。這也多虧有上面的圖當範本,讓我知道 GeoGebra 能畫出這樣的圖形,才能往正確的方向嘗試,幾乎可以說是打開就會用了。
接著就要做圖形化的證明。傳統的方式,我們會用輔助線的方式來做圖形證明。像是過某點作與某線段平行的直線之類的,然後再說新作出來多邊形和原來的哪個全等。
GeoGebra 也能做同樣的事,不過還有更好的做法。與其弄一堆平行的輔助線,我把原來的圖形繞著一個點旋轉 180 度,這樣平行以及全等就是自然而然的事情了。
準備儲存圖片時,發現可以輸出 GIF 動畫。心想這樣更好,但是找不到動畫時間軸的要怎麼弄,所以便宜行事 Google 了一下,了解 slider 的用法。
最後的結果如下:

由這個動畫,可以很清楚的了解兩個正方形中心相連的那個線段的幾何意義,當然也就清楚知道為什麼會垂直了。
也由於是 Java,什麼平台都能跑,拿來畫畫圖看起來還挺方便的。

2012年11月1日 星期四

Win8 安裝 (x32升x64 無 DVD/USB)與心得


在 preview 及 RTM 版本時,已經在 VirtualBox 上裝過了 Win8。因為主力筆電換了台 MBA,所以這次就放心地將之前的筆電來實機灌正式版 Win8 Pro。
不過問題是我原本的 Win7 是 32bit。原因也是當初選擇直接升級 Win7,所以只能 x32 升 x32。這次反正重要資料大多已經轉移了,就放心地完整升級 x64 版本。
先用一台 Win7 64 版的 PC 購買及下載了 Win8 x64 的 DVD ISO,接著,就發現糟糕了。
手邊沒有空白 DVD,也沒有 USB 隨身碟。實際上就算有空白DVD 也沒用,筆電的 DVD driver 已經拆下了。
直接用虛擬光碟跑 setup 是不行的,因為 x32 的 win7 根本不讓你跑 x64 的 setup。
靈機一動,想到手機裡有 Micro SD,然後剛好有轉接卡,所以就 bootsect /nt60 f: /force 之後,將 Win8 DVD 的檔案放到 SD 卡內。沒想到我的 LifeBook T4215 無法用 SD 開機。
如果能將 Android 手機當成外接硬碟開機也不錯,不過似乎也找不到簡單的現成解法。
隨便借個隨身碟也行,但是不知哪根筋不對,就是突然很執著想不用隨身碟來安裝。
所以決定用 grub 或者 windows 的 boot menu 來啟動光碟。 先嘗試用 boot menu,所以抓了 EasyBCD 來弄。果然順利啟動了光碟,但是會卡在找不到驅動程式。後來在安裝過程中的 X: 磁碟機,試了一些目錄讓他找驅動程式,結果搜尋了半天卡住了,只好重開機。
其實即使找到驅動程式也不是很放心,因為照理來說是不會發生這種問題的,所以很可能是 EasyBCD 沒有完全處理好光碟的模擬。 所以改使用 unetbootin。 用 unetbootin 將 ISO 安裝成從 C: 啟動,結果更慘,無法啟動光碟。 本來已經放棄了,重開機想確認一下還能回到 win7,結果不小心按錯,換成使用 EasyBCD 來啟動 ISO,結果這次居然順利啟動了安裝光碟,然後順利完成安裝。
想來是找來 ubootin, EasyBCD 也會怕競爭,終於肯認真工作的緣故。
回想起來,後來就再也沒機會完成原本想重新啟動進 win7 的動作了,因為 win7 很快就被我刪除了。 不禁感到一股淡淡的哀傷。

win8 的使用邏輯和之前有點不同。對於沒有多點觸控螢幕的電腦來說,鍵盤快捷鍵的使用機會大增。
比方大多數人第一個會注意到的,就是開始功能表不見了,所以會讓人有不知從何開始的感覺。實際上取代開始功能表的是在 Metro 桌面按 Windows+Q 的快捷鍵或者直接打字。跑出來像是上面的東西,然後你在搜尋框中,可以打程式名稱來搜尋。
過去的 Alt-F4, Alt-Tab 及 Ctrl-Alt-Del 都可用,外加 Windows+C,Z,Q,Tab,I,K,F,G 一堆新的。Screenshot 用 Windows+Scr。

第二個注意到的,可能是 Chrome 或者 Firefox 不見了。不過其實大多數使用者可能因為用得太習慣,忘記 Chrome 和 Firefox 本來就是另外抓的。裡面的 IE 一樣保有上網抓 Chrome 和 Firefox 的功能。商店裡面也能抓到 Google Search。不過 IE10 其實看起來還不差就是了。
IE10 有兩種介面,Metro UI 和傳統 windows UI。 就像是當年 Win 3.x 那種 GUI 配上 DOS 模式的感覺,  Win8 也是 Metro UI 背後配上 Win7 模式。可以靠 Windows 鍵跳回 Metro 磚頭頁,也可以用 Windows+D 跳到傳統桌面。
兩種雖然都是 GUI,但是使用上的感覺並不一致。比方我的 T4215 是 Tablet PC,所以有隻觸控筆。在傳統 UI 下,可以使用觸控筆的手勢拉網頁。但是在 Metro UI 下卻不行。
Metro App 的使用邏輯還沒有完全統一。有的時候搜尋可以用 Windows+Q 或直接打字(有時兩者皆可,有時則只有 Windows+Q),有時則是滑鼠右鍵或 Windows-Z 叫出選單(像是地圖)。
優點也有一些。Live 帳號的整合度不錯,遊戲方面還能與 Xbox live 整合,雖然有時要回確認馬有點麻煩。檔案複製時的資訊非常完整,中文手寫輸入法很不錯。硬碟重組感覺很快。商店內也有些不錯的免費軟體,比方 Onenote 就是免費的。