मीटर रीडिंग को टेलीफ़ोन पर कैप्चर करें, उसके बाद पहचान

प्रविष्टि


ऐसा हुआ कि मैं एक झोपड़ी गांव में रहता हूं जहां कोई केंद्रीय हीटिंग नहीं है, जिसका अर्थ है कि हर कोई अपने अपार्टमेंट को अपने दम पर गर्म करता है। सबसे अधिक बार, गैस बॉयलर का उपयोग इन उद्देश्यों के लिए किया जाता है, विधि काफी सस्ती है, इसके बारे में शिकायत करने के लिए कुछ भी नहीं है, लेकिन एक सूक्ष्मता है। गैस बॉयलर (अचानक) के सही संचालन के लिए, पाइप में गैस की उपस्थिति आवश्यक है।

शायद सभी बॉयलर इस तरह से व्यवहार नहीं करते हैं, लेकिन गैस आपूर्ति के साथ एक छोटी रुकावट के दौरान भी हमारा बंद हो जाता है और आपूर्ति बहाल होने पर वापस नहीं आता है। यदि कोई घर पर है, तो यह कोई समस्या नहीं है, उसने एक बटन दबाया और बायलर आगे गर्म हो गया, लेकिन अगर यह अचानक हुआ कि हमने पूरे परिवार के साथ छुट्टी पर जाने का फैसला किया, और यार्ड में यह सर्दियों का ऐसा अच्छा तापमान -20 डिग्री सेल्सियस है, तो परिणाम दुस्साहसी हो सकता है।

समाधान सरल है - रिश्तेदारों / दोस्तों / पड़ोसियों की चाबियाँ छोड़ दें ताकि वे किसी भी परेशानी की स्थिति में बायलर को चालू और चालू कर सकें। ठीक है, अगर कोई पड़ोसी है जो हर दिन आएगा और जांच करेगा कि सब कुछ क्रम में है या नहीं। और अगर नहीं? या वह सप्ताहांत के लिए कहीं जाने का फैसला भी करेगा?

इसलिए, मैंने मीटर की रीडिंग को कहीं न कहीं इंटरनेट पर डालने का फैसला किया, ताकि एक लंबी यात्रा के दौरान मैं समय-समय पर जांच कर सकूं कि क्या गैस बर्बाद हो रही है, और अगर मैं अचानक बर्बाद करना बंद कर दूं, तो तुरंत रिश्तेदारों / दोस्तों / पड़ोसियों (या जो कोई भी मुझे फोन करें) बटन छोड़ कर आने के लिए)

बेशक, इंटरनेट पर केवल प्रशंसापत्र अपलोड करने के बाद, मैंने वहां नहीं रुकने का फैसला किया और प्रशंसापत्र और ग्राफिक प्रस्तुति की मान्यता को गड़बड़ कर दिया, इस विषय के भाग 2 में इसके बारे में पढ़ा।

भाग 1. मीटर से पढ़ना और उन्हें इंटरनेट पर डालना


यहां यह आरक्षण करना आवश्यक है कि मीटर पूरी तरह से प्रकृति में भिन्न हैं, उनमें से कुछ में स्वचालित पढ़ने के लिए विशेष बसें और इंटरफेस हैं। यदि आपके पास एक है, तो आप शायद आगे नहीं पढ़ सकते हैं। लेकिन मेरे पास इस तरह के इंटरफेस के बिना सबसे आम है (कम से कम मुझे नहीं मिला, शायद मैं बुरी तरह से देख रहा था), गैलस आईवी पीएससी मॉडल। इसलिए, एक विकल्प रहता है - दृश्य रीडिंग। नेटवर्क तैयार किए गए समाधान प्रदान करता है, लेकिन उनके पास बहुत पैसा खर्च होता है, और सबसे महत्वपूर्ण बात, यह बिल्कुल स्पोर्टी नहीं है, इसलिए हम सब कुछ खुद करेंगे।

हमें क्या चाहिए?

मीटर से रीडिंग लेने के लिए और फिर इन रीडिंग को इंटरनेट पर भेजने के लिए, हमें किसी भी अनावश्यक एंड्रॉइड स्मार्टफोन की आवश्यकता होती है। उदाहरण के लिए, मैंने इन उद्देश्यों के लिए सैमसंग गैलेक्सी एस III (SCH-I535) का उपयोग किया। हां, शायद, हर पाठक के पास एक तीसरी आकाशगंगा बेकार नहीं है, लेकिन आपको यह समझने की आवश्यकता है कि स्मार्टफोन के लिए आवश्यकताएं बहुत शानदार हैं:

बस इतना ही, वर्किंग स्क्रीन, टच स्क्रीन, माइक्रोफोन, स्पीकर आदि की उपस्थिति। बिलकुल नहीं। यह तथ्य लागत को काफी कम कर देता है।

EBay पर विभिन्न टूटे हुए फोन खरीदने और उनमें से काम करने वाले लोगों को इकट्ठा करने का शौक रखते हुए, मैंने आसानी से एक निष्क्रिय माइक्रोफोन (~ $ 10) के साथ एक sgs3 मदरबोर्ड, साथ ही एक इस्तेमाल किया कैमरा (~ $ 10) और एक चीनी बैटरी (~ 300 रगड़)। इसके अलावा, बोर्ड को बैटरी संलग्न करने की सुविधा के लिए, मैंने एक टूटे हुए डिस्प्ले के साथ एक फ्रेम का उपयोग किया।


पहले मुझे लगा कि मैं केवल मदरबोर्ड और कैमरे के साथ ही काम कर सकता हूं, लेकिन यह पता चला कि चार्जिंग से कनेक्ट होने पर भी, बोर्ड बिना बैटरी के चालू नहीं होता है, इसलिए मुझे अभी भी एक फ्रेम और एक बैटरी जोड़ना पड़ा। लेकिन इस मामले में, बजट लगभग $ 30 निकला, यदि आप sgs3 की तुलना में सरल उपकरणों का उपयोग करते हैं, तो आप एक छोटी राशि को पूरा कर सकते हैं।

सच है, इस समाधान में इसकी कमियां हैं, एक डिस्प्ले के बिना एक स्मार्टफोन और टचस्क्रीन कॉन्फ़िगर करने के लिए इतना सुविधाजनक नहीं है, इसलिए मैं इस समस्या को हल करने के बारे में थोड़ी बात करूंगा।

मशीन की सेटिंग

हम सबसे खराब स्थिति से आगे बढ़ेंगे। मान लीजिए कि कोई डिस्प्ले या टचस्क्रीन नहीं है, स्मार्टफोन में रूट नहीं है, एडीबी डीबगिंग अक्षम है, फर्मवेयर अज्ञात है।

रीएनिमेशन

चेतावनी! निम्न निर्देश डिवाइस सैमसंग गैलेक्सी एस III (SCH-I535) के लिए उपयुक्त हैं, यदि आपके पास एक अलग स्मार्टफोन है, तो क्रियाएं भिन्न हो सकती हैं।

यह माना जाता है कि आप adb, फर्मवेयर आदि जैसी अवधारणाओं से परिचित हैं।

हमारे लिए स्मार्टफोन को अधिक या कम प्रसिद्ध स्थिति में लाने के लिए, शुरुआत के लिए, हम ओडिन का उपयोग करके यहां से वीआरबीएमबी 1 स्टॉक फर्मवेयर फ्लैश करेंगे। मैं विस्तार से वर्णन नहीं करूंगा कि यह कैसे किया जाता है, इंटरनेट ओडिन का उपयोग करने के निर्देशों से भरा है। हमारे मामले में ओडिन अच्छा है क्योंकि स्मार्टफोन की स्क्रीन का उपयोग किए बिना इसके साथ काम करना आसान है, आपको बस बूट मोड में स्मार्टफोन को चालू करना होगा (वॉल्यूम डाउन + होम + पावर - इसे कुछ सेकंड के लिए दबाए रखें, फिर वॉल्यूम अप, विंडोज के माध्यम से यूएसबी से कनेक्ट करें और यही वह है। ओडिन-एक)।

ओडिन नाली को चमकाने के बाद, फोन सिस्टम को बूट करेगा, इसे यूएसबी से डिस्कनेक्ट करेगा और इसे बंद करने के लिए बैटरी को हटा देगा। यह ऑपरेशन ओडिन द्वारा फर्मवेयर के पूरा होने के बाद हर बार बंद राज्य से अगला ऑपरेशन शुरू करने के लिए किया जाना चाहिए।

अगला, हम निर्देशों के अनुसार सीडब्ल्यूएम रिकवरी और रूट को सीवे करते हैं। संक्षेप में, तब:


अगला, हमें स्मार्टफोन पर यूएसबी डिबगिंग सक्षम करने की आवश्यकता है, इसके लिए हम स्मार्टफोन को सीडब्ल्यूएम-रिकवरी मोड में लॉन्च करते हैं, चेक करें:
 malefic@lepeshka:~$ adb devices List of devices attached 64cb5c59 recovery 

माउंट प्रणाली:
 malefic@lepeshka:~$ adb shell mount -o rw -t ext4 /dev/block/platform/msm_sdcc.1/by-name/system /system 

/System/build.prop में एक पंक्ति जोड़ें:
 malefic@lepeshka:~$ adb shell "echo \"persist.service.adb.enable=1\" >> /system/build.prop" 

रीबूट:
 malefic@lepeshka:~$ adb reboot 

हम डाउनलोड की प्रतीक्षा कर रहे हैं, हम टर्मिनल में adb स्टेटस की जाँच करते हैं:
 malefic@lepeshka:~$ adb devices List of devices attached 64cb5c59 device 

बिंगो! डिबगिंग चालू है, देखते हैं कि हमारे स्मार्टफोन पर क्या चल रहा है, इसके लिए हम जावा वेब स्टार्ट का उपयोग करके AndroidScreenCast लॉन्च करते हैं और देखते हैं:


यह Verizon SIM कार्ड सक्रियण स्क्रीन है, मेरे पास ऐसा कोई सिम कार्ड नहीं है, इसलिए मैं निर्देशों का पालन करके सक्रियण को छोड़ देता हूं:
भाषा चयन स्क्रीन पर, स्क्रीन पर निचले बाएं कोने (आपातकालीन कॉल बटन के ऊपर), निचले दाएं कोने, निचले बाएं, निचले दाएं और वॉल्यूम + पर टैप करें

अर्थात्:
 malefic@lepeshka:~$ adb shell input tap 10 1150 malefic@lepeshka:~$ adb shell input tap 710 1150 malefic@lepeshka:~$ adb shell input tap 10 1150 malefic@lepeshka:~$ adb shell input tap 710 1150 

तब मैं स्मार्टफोन पर Vol Up बटन दबाता हूं, अब हम देखते हैं:


एक टिक लगाएं और ओके पर क्लिक करें:
 malefic@lepeshka:~$ adb shell input tap 50 600 malefic@lepeshka:~$ adb shell input tap 650 600 



स्क्रीन अनलॉक करने के लिए स्वाइप करें:
 malefic@lepeshka:~$ adb shell input swipe 100 100 500 100 



अब आपको एंड्रॉइड के लिए कुछ प्रकार के vnc सर्वर को स्थापित करने की आवश्यकता है, उदाहरण के लिए, Android VNC सर्वर । इसे स्मार्टफोन पर इंस्टॉल करें:
 malefic@lepeshka:~$ adb install droid+VNC+server+v1.1RC0.apk 4055 KB/s (2084419 bytes in 0.501s) pkg: /data/local/tmp/droid+VNC+server+v1.1RC0.apk Success 

जब हम vnc सर्वर को स्थापित कर रहे थे, और स्क्रीन अनलॉक करने के लिए स्वाइप करें: हम स्मार्टफोन को जगाएंगे, क्योंकि यह सबसे अधिक सोता था।
 malefic@lepeshka:~$ adb shell input keyevent 26 malefic@lepeshka:~$ adb shell input swipe 100 100 500 100 

हम vnc सर्वर शुरू करते हैं:
 malefic@lepeshka:~$ adb shell am start -a android.intent.action.Main -n org.onaips.vnc/.MainActivity 



ठीक क्लिक करें:
 malefic@lepeshka:~$ adb shell input tap 50 900 



प्रारंभ पर क्लिक करें:
 malefic@lepeshka:~$ adb shell input tap 350 300 



पहुँच प्रदान करने के लिए क्लिक करें:
 malefic@lepeshka:~$ adb shell input tap 600 1000 



ठीक है, अब हम adb के माध्यम से बंदरगाहों को अग्रेषित करते हैं:
 malefic@lepeshka:~$ adb forward tcp:5801 tcp:5801 malefic@lepeshka:~$ adb forward tcp:5901 tcp:5901 

और एक ब्राउज़र या अपने पसंदीदा vnc क्लाइंट के माध्यम से स्मार्टफोन पर जाएं।


अगला, हम एक नियमित रूप से एंड्रॉइड फोन के साथ काम करते हैं, केवल एक कंप्यूटर के माध्यम से, यह वाईफाई कनेक्शन को तुरंत कॉन्फ़िगर करने के लिए सुविधाजनक है, फिर आप वाईफाई के माध्यम से एक्सेस कर सकते हैं और फोन को हर समय कंप्यूटर से कनेक्ट नहीं रख सकते हैं (आखिरकार, गैस मीटर हमेशा कंप्यूटर के करीब नहीं स्थित है)।

अब जब डिवाइस के साथ इंटरैक्शन पूरी तरह से स्थापित हो गया है, तो आप फोटोग्राफी को कॉन्फ़िगर करने और इंटरनेट पर डेटा प्रकाशित करने के लिए जा सकते हैं।

आवधिक फोटोग्राफी

टास्कर एप्लिकेशन इंस्टॉल करें, इसमें एक अस्थायी प्रोफ़ाइल बनाएं 00:00 से 23:59 तक हर 30 मिनट में एक क्रिया करने के लिए - एक फोटो लें। हम शूटिंग मापदंडों का चयन करते हैं जो फोन और काउंटर के स्थान के लिए सबसे उपयुक्त हैं। मेरे पास यह मैक्रो शॉट अनिवार्य फ्लैश के साथ है।

इसलिए, वास्तव में, मैंने अपना फोन (शीर्ष दृश्य) रखा:


रस्सी के साथ काउंटर से बंधा एक कार्डबोर्ड बॉक्स, एक स्मार्टफोन उसमें रहता है, एक ईमानदार स्थिति में स्मार्टफोन को ठीक करने के लिए अंडे से पैकिंग होती है। फिर मैंने टेप और कार्डबोर्ड का उपयोग करके डिजाइन को और विकसित किया, ताकि फ्लैश सीधे डायल पर हिट न हो, इससे गंभीर चमक मिलती है जो मान्यता के साथ हस्तक्षेप करती है। मैंने ऊपर से एक ढक्कन के साथ सब कुछ कवर किया ताकि यह अंदर अंधेरा हो, अन्यथा, उज्ज्वल बाहरी प्रकाश व्यवस्था में, ऑटोफोकस हमेशा सही ढंग से काम नहीं करता है।

डेवलपर के टूल में स्मार्टफोन की सेटिंग्स में, बॉक्स को जांचना आवश्यक है ताकि चार्जिंग से कनेक्ट होने पर स्मार्टफोन सो न जाए, अन्यथा कुछ बिंदु पर यह तस्वीरें लेना बंद कर देता है और केवल तभी जारी रहता है जब यह परेशान होता है।


इंटरनेट पर फैल गया

मीटर की कैप्चर की गई छवियों को इंटरनेट पर ले जाने के लिए, मैंने पहले एप्लिकेशन का उपयोग किया जो कि भर में आया था - FolderSync लाइट । यह एक फ़ोल्डर पर एक स्मार्टफोन के साथ एक फ़ोल्डर को सिंक्रनाइज़ कर सकता है, उदाहरण के लिए, Google ड्राइव पर।

इस प्रकार, मैं अब दुनिया के किसी भी स्थान से जा सकता हूं, जहां मेरे Google ड्राइव पर जाने के लिए इंटरनेट एक्सेस है और यह जांचें कि गैस बॉयलर सामान्य रूप से काम कर रहा है।

भाग 2. मान्यता


इसलिए, मीटर रीडिंग को इंटरनेट पर भेजने के बाद, मुझे रीडिंग की स्वचालित पहचान की संभावना में दिलचस्पी थी। यह अनुमति देगा:


पायथन को विकास भाषा के रूप में चुना गया था, OpenCV लाइब्रेरी का उपयोग छवियों के साथ काम करने के लिए किया गया था।

यहाँ प्रति घंटे एक बार ताज पर चलने वाले मुख्य कार्यक्रम के लिए कोड दिया गया है:
 import sys import os from models import getImage, sess from gdrive import getImagesFromGDrive, createImageFromGDriveObject if __name__ == '__main__': #        images, http = getImagesFromGDrive() #      for img_info in images: #   img = createImageFromGDriveObject (img_info, http) file_name = img_info['title'] #     try: dbimage = getImage(os.path.basename(file_name)) dbimage.img = img dbimage.download_url = img_info["downloadUrl"] dbimage.img_link = img_info['webContentLink'].replace('&export=download','') except ValueError as e: print e continue #   dbimage.identifyDigits() #     sess.commit() 

यह फ़ंक्शंस का उपयोग करता है, जिसका कोड मैं नीचे दूंगा:

Google डिस्क के साथ काम करें

पहली चीज़ जो हम करते हैं वह है Google ड्राइव से छवियों की एक सूची प्राप्त करना:
 import os from datetime import tzinfo, timedelta, date from dateutil.relativedelta import relativedelta from apiclient.discovery import build from models import getLastRecognizedImage def getImagesFromGDrive(): #  id  Google ,     FOLDER_ID = '0B5mI3ROgk0mJcHJKTm95Ri1mbVU' #     http = getAuthorizedHttp() #    drive_service = build('drive', 'v2', http=http) #         ,      month_ago = date.today() + relativedelta( months = -1 ) q = "'%s' in parents and mimeType = 'image/jpeg' and trashed = false and modifiedDate<'%s'" % (FOLDER_ID, month_ago.isoformat()) files = drive_service.files().list(q = q, maxResults=1000).execute() for image in files.get('items'): drive_service.files().trash(fileId=image['id']).execute() #     ,     last_image = getLastRecognizedImage() #     ,          page_size = 1000 result = [] pt = None #   API        1000 , #      1000       while True: q = "'%s' in parents and trashed = false and mimeType = 'image/jpeg' and modifiedDate>'%s'" % (FOLDER_ID, last_image.check_time.replace(tzinfo=TZ()).isoformat('T')) files = drive_service.files().list(q = q, maxResults=page_size, pageToken=pt).execute() result.extend(files.get('items')) pt = files.get('nextPageToken') if not pt: break #  ,        result.reverse() return result, http 

अधिकृत ड्राइव क्लाइंट निम्नानुसार बनाया गया है:
 import httplib2 import ConfigParser from oauth2client.client import OAuth2WebServerFlow from oauth2client.file import Storage def getAuthorizedHttp(): #    config.ini   CLIENT_ID  CLIENT_SECRET config = ConfigParser.ConfigParser() config.read([os.path.dirname(__file__)+'/config.ini']) CLIENT_ID = config.get('gdrive','CLIENT_ID') CLIENT_SECRET = config.get('gdrive','CLIENT_SECRET') # OAuth 2.0 scope that will be authorized. # Check https://developers.google.com/drive/scopes for all available scopes. OAUTH_SCOPE = 'https://www.googleapis.com/auth/drive' # Redirect URI for installed apps REDIRECT_URI = 'urn:ietf:wg:oauth:2.0:oob' #   client_secrets.json    storage = Storage(os.path.dirname(__file__) + '/client_secrets.json') credentials = storage.get() #     ,     if not credentials: # Perform OAuth2.0 authorization flow. flow = OAuth2WebServerFlow(CLIENT_ID, CLIENT_SECRET, OAUTH_SCOPE, REDIRECT_URI) authorize_url = flow.step1_get_authorize_url() #    ,       print 'Go to the following link in your browser: ' + authorize_url #   code = raw_input('Enter verification code: ').strip() credentials = flow.step2_exchange(code) #   storage.put(credentials) #  http     http = httplib2.Http() credentials.authorize(http) return http 

Google डेवलपर्स कंसोल में CLIENT_ID और CLIENT_SECRET प्राप्त करने के लिए, आपको एक प्रोजेक्ट बनाने की आवश्यकता है और इस प्रोजेक्ट के लिए API और विशेषाधिकार - क्रेडेंशियल्स - OAuth अनुभाग में, NEW CLIENT ID पर क्लिक करें, इंस्टॉल किए गए एप्लिकेशन का चयन करें - अन्य वहाँ :


पहली शुरुआत में, स्क्रिप्ट कंसोल में लिखेंगी url जिस पर आपको टोकन प्राप्त करने की आवश्यकता है, इसे ब्राउज़र के एड्रेस बार में डालें, एप्लिकेशन को Google ड्राइव तक पहुंचने की अनुमति दें, ब्राउज़र से Google द्वारा जारी सत्यापन कोड को कॉपी करें और इसे स्क्रिप्ट को दें। उसके बाद, स्क्रिप्ट client_secrets.json फ़ाइल में आपकी ज़रूरत की सभी client_secrets.json को client_secrets.json और बाद के लॉन्च के दौरान कुछ भी नहीं पूछेगा।

छवि डाउनलोड समारोह बेहद सरल है:
 import cv2 import numpy as np def downloadImageFromGDrive (downloadUrl, http=None): if http==None: http = getAuthorizedHttp() #   resp, content = http.request(downloadUrl) #    OpenCV img_array = np.asarray(bytearray(content), dtype=np.uint8) return cv2.imdecode(img_array, cv2.IMREAD_COLOR) def createImageFromGDriveObject (img_info, http=None): return downloadImageFromGDrive(img_info['downloadUrl'], http) 


फोटो में रीडिंग के लिए खोजें

फ़ोटो प्राप्त करने के बाद पहली बात यह है कि उस पर संख्याओं को खोजना है जिसे हम पहचानेंगे। extractDigitsFromImage विधि extractDigitsFromImage :
  def extractDigitsFromImage (self): img = self.img 

प्रारंभ में, फोटो इस तरह दिखता है:


इसलिए, पहले हम इसे चालू करते हैं ताकि यह वांछित अभिविन्यास प्राप्त करे।
  #   90  h, w, k = img.shape M = cv2.getRotationMatrix2D((w/2,h/2),270,1) img = cv2.warpAffine(img,M,(w,h)) 



  #   ,    img = img[0:h, (wh)/2:h+(wh)/2] h, w, k = img.shape 

अब एक लाल फ्रेम से घिरी छवि के एक टुकड़े पर विचार करें। यह पूरी तस्वीर के भीतर काफी अनोखी है, आप इसका उपयोग डायल की खोज करने के लिए कर सकते हैं। मैंने इसे sample.jpg फ़ाइल में डाला और इसके निर्देशांक खोजने के लिए निम्न कोड लिखा:
  #       sample = cv2.imread(os.path.dirname(__file__)+"/sample.jpg") sample_h, sample_w, sample_k = sample.shape #       res = cv2.matchTemplate(img,sample,cv2.TM_CCORR_NORMED) min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(res) #      x_center = max_loc[0] + sample_w/2 y_center = max_loc[1] + sample_h/2 #        ,      , #        if x_center>w*0.6: img = img[0:h, 0.2*w:w] h, w, k = img.shape x_center = x_center-0.2*w 



आकृति में बिंदी उस निर्देशांक को इंगित करती है जो हमें मिला था, जो हम चाहते थे। अगला, हम सीमा खोज एल्गोरिथ्म शुरू करते हैं, छवि को ग्रे टन में अनुवाद करने के बाद। क्रमिक रूप से चयनित 100 और 200 दहलीज मूल्य हैं।
  #      gray = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY) #    Canny edges = cv2.Canny(gray, 100, 200) 



अब हम सीमाओं के साथ प्राप्त छवि पर लाइनों को खोजने के लिए एल्गोरिथ्म शुरू करते हैं। छवि के अलावा, HoughLines विधि भी दूरी के कोण और रोटेशन के माध्यम से खोज चरणों की परिमाण के रूप में लेती है और न्यूनतम सीमा के लिए जिम्मेदार एक थ्रेशोल्ड मान है जो एक लाइन का निर्माण करना चाहिए। यह सीमा जितनी कम होगी, एल्गोरिथम उतनी ही अधिक रेखाएं पाएगा।
  #    lines = cv2.HoughLines(edges, 1, np.pi/180, threshold=100) 



पाई गई सभी लाइनों में से, हम केवल अधिक या कम क्षैतिज रेखाओं पर विचार करते हैं और दोनों को पहले खोजे गए केंद्र (एक ऊपर और एक नीचे) के सबसे करीब पाते हैं।
  #    rho_below = rho_above = np.sqrt(h*h+w*w) line_above = None line_below = None for line in lines: rho,theta = line[0] sin = np.sin(theta) cos = np.cos(theta) #     if (sin<0.7): continue #       ,    ""  rho_center = x_center*cos + y_center*sin #      if rho_center>rho and rho_center-rho<rho_above: rho_above = rho_center-rho line_above = {"rho":rho, "theta":theta, "sin":sin, "cos":cos} #      if rho_center<rho and rho-rho_center<rho_below: rho_below = rho-rho_center line_below = {"rho":rho, "theta":theta, "sin":sin, "cos":cos} # ,      if line_below==None or line_above==None: mylogger.warn("No lines found") return False # ,           if rho_below/rho_above>1.7 or rho_below/rho_above<0.6: mylogger.warn("Wrong lines found: %f" % (rho_below/rho_above)) return False 



हम छवि को घुमाते हैं ताकि मिली हुई रेखाएं पूरी तरह से क्षैतिज हो जाएं:
  #  M = cv2.getRotationMatrix2D((0,(line_below["rho"]-line_above["rho"])/2+line_above["rho"]),line_above["theta"]/np.pi*180-90,1) img = cv2.warpAffine(img,M,(w,h)) 



अब जो कुछ पाया जाता है उसके पीछे सब कुछ काट दें:
  #  img = img[line_above["rho"]:line_below["rho"], 0:w] h, w, k = img.shape 



अगला, हमें डायल के बाएं और दाएं किनारों को खोजने की आवश्यकता है, हम छवि को काले और सफेद में अनुवादित करते हैं:
  #   gray = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY) thres = cv2.adaptiveThreshold(gray,255,cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, 31, 2) 



हम "केंद्रीय" बिंदु के समान तकनीक का उपयोग करके दाहिने किनारे की तलाश कर रहे हैं, टेम्पलेट लाल रंग में परिचालित है:
  sample_right = cv2.imread(os.path.dirname(__file__)+"/sample_right.jpg",cv2.IMREAD_GRAYSCALE) #      res = cv2.matchTemplate(thres,sample_right,cv2.TM_CCORR_NORMED) min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(res) #    x_right = max_loc[0]-6 

बाईं सीमा की खोज करने के लिए, हम शोर को हटाने के लिए समापन परिवर्तन लागू करते हैं:
  #   kernel = np.ones((7,7),np.uint8) thres = cv2.morphologyEx(thres, cv2.MORPH_CLOSE, kernel) 



अगला, हम बाईं ओर से शुरू होने वाले सभी पिक्सेल पर पुनरावृति करेंगे, जब तक हम काले से मिलते हैं, यह बाएं किनारे होगा:
  #    x_left=0 while x_left<w : if thres[h/2,x_left]==0: break x_left+=1 



छवि को बाईं और दाईं ओर क्रॉप करें:
  #     img = img[:, x_left:x_right] h, w, k = img.shape 



आइए एक छोटा सा चेक बनाएं कि पहलू अनुपात के संदर्भ में मिली छवि डायल से मेल खाती है:
  #    if float(w)/float(h)<6.5 or float(w)/float(h)>9.5: mylogger.warn("Image has bad ratio: %f" % (float(w)/float(h))) return False self.digits_img = img return True 


नंबरों पर विभाजन

splitDigits विधि का उपयोग पिछले फ़ंक्शन द्वारा चयनित डायल को अलग-अलग अंकों में splitDigits करने के लिए किया जाता है:
  def splitDigits (self): # ,     ,    if None == self.digits_img: if not self.extractDigitsFromImage(): return False img = self.digits_img h, w, k = img.shape 

आरंभ करने के लिए, बस हमारी डायल को 8 बराबर भागों में काटें:


हम केवल पहले 7 भागों की प्रक्रिया करेंगे, क्योंकि 8 वां अंक लगातार घूम रहा है, इसे पहचानना बेकार है।
हम प्रत्येक भाग को b / w में adaptiveThreshold विधि का उपयोग करके अनुवाद करते हैं, पैरामीटर अनुभवजन्य रूप से चुने जाते हैं:
  #    8       for i in range(1,8): digit = img[0:h, (i-1)*w/8:i*w/8] dh, dw, dk = digit.shape #   / digit_gray = cv2.cvtColor(digit,cv2.COLOR_BGR2GRAY) digit_bin = cv2.adaptiveThreshold(digit_gray,255,cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, 9, 0) 



हम उद्घाटन परिवर्तन की मदद से शोर को थोड़ा दूर करते हैं (केवल एक 2x2 कर्नेल का उपयोग किया जाता है)। इसके बिना करना संभव होगा, लेकिन कभी-कभी यह पतले कूदने वालों से जुड़े बड़े सफेद टुकड़ों को संख्या से काटने में मदद करता है:
  #   kernel = np.ones((2,2),np.uint8) digit_bin = cv2.morphologyEx(digit_bin, cv2.MORPH_OPEN, kernel) 



समोच्च खोज एल्गोरिथ्म चलाएँ
  #   other, contours, hierarhy = cv2.findContours(digit_bin.copy(),cv2.RETR_TREE,cv2.CHAIN_APPROX_SIMPLE) 



इसके बाद, छवि के किनारों के साथ सभी बहुत छोटे आकृति और आकृति को बाहर फेंक दें, फिर हम शेष भाग का सबसे बड़ा समोच्च पाएंगे:
  #   biggest_contour = None biggest_contour_area = 0 for cnt in contours: M = cv2.moments(cnt) #       if cv2.contourArea(cnt)<30: continue #       if cv2.arcLength(cnt,True)<30: continue #     cx = M['m10']/M['m00'] cy = M['m01']/M['m00'] #  ,     -   if cx/dw<0.3 or cx/dw>0.7: continue #    if cv2.contourArea(cnt)>biggest_contour_area: biggest_contour = cnt biggest_contour_area = cv2.contourArea(cnt) biggest_contour_cx = cx biggest_contour_cy = cy #       ,      if biggest_contour==None: digit = self.dbDigit(i, digit_bin) digit.markDigitForManualRecognize (use_for_training=False) mylogger.warn("Digit %d: no biggest contour found" % i) continue 



सबसे बड़ा समोच्च हमारी संख्या है, चलो एक मुखौटा लागू करने से परे सब कुछ बाहर फेंक दें:
  #  ,        mask = np.zeros(digit_bin.shape,np.uint8) cv2.drawContours(mask,[biggest_contour],0,255,-1) digit_bin = cv2.bitwise_and(digit_bin,digit_bin,mask = mask) 



अब हम प्रत्येक अंक के चारों ओर समोच्च के द्रव्यमान के केंद्र में एक मानक आकार की आयत का वर्णन करते हैं:
  #     rw = dw/2.0 rh = dh/1.4 # ,        if biggest_contour_cy-rh/2 < 0: biggest_contour_cy = rh/2 if biggest_contour_cx-rw/2 < 0: biggest_contour_cx = rw/2 



हम छवि को एक आयत में digit_base_h = 2 और इसे दिए गए आकार में स्केल करते हैं, मेरे लिए यह digit_base_h = 2 4, digit_base_w = 16 । परिणाम डेटाबेस में सहेजा गया है।
  #   digit_bin = digit_bin[int(biggest_contour_cy-rh/2):int(biggest_contour_cy+rh/2), int(biggest_contour_cx-rw/2):int(biggest_contour_cx+rw/2)] #     digit_bin = cv2.resize(digit_bin,(digit_base_w, digit_base_h)) digit_bin = cv2.threshold(digit_bin, 128, 255, cv2.THRESH_BINARY)[1] #    digit = self.dbDigit(i, digit_bin) return True 



अंक मान्यता

यहाँ identifyDigits विधि है कि प्रत्येक छवि के लिए मुख्य कार्यक्रम से कहा जाता है:
  def identifyDigits(self): #    ,     if self.result!='': return True #      if len(self.digits)==0: #    ,     if self.img == None: return False #   if not self.splitDigits(): return False #    ,      sess.commit() #     for digit in self.digits: digit.identifyDigit() #     str_digits = map(str,self.digits) #       ,        if '?' in str_digits: return False #       self.result = ''.join(str_digits) return True 

यहाँ सब कुछ तुच्छ है, केवल identifyDigit पद्धति को छोड़कर:
  def identifyDigit (self): #    ,     if self.result!='?': return True if not KNN.recognize(self): #     ,       self.markDigitForManualRecognize() #   7- ,     "0",        ,   ,     if self.i==7: self.result = 0 return True return False else: self.use_for_training = True return True 

identifyDigit विधि भी तुच्छ है, मान्यता KNN.recognize विधि में होती है, OpenCV से निकटतम पड़ोसियों को खोजने के लिए एल्गोरिथ्म का उपयोग किया जाता है:
  @staticmethod def recognize(dbdigit): # ,     if not KNN._trained: KNN.train() #   ,   ,     h,w = dbdigit.body.shape if h!=digit_base_h or w!=digit_base_w: dbdigit.markDigitForManualRecognize(use_for_training=False) mylogger.warn("Digit %d has bad resolution: %dx %d" % (dbdigit.i,h,w)) return False #        sample = dbdigit.body.reshape(digit_base_h*digit_base_w).astype(np.float32) test_data = np.array([sample]) #     , -  - 5 knn = KNN.getKNN() ret,result,neighbours,dist = knn.find_nearest(test_data,k=5) #     if result[0,0]!=neighbours[0,0]: #       dbdigit.markDigitForManualRecognize() return False if neighbours[0,1]!=neighbours[0,0] or neighbours[0,2]!=neighbours[0,0]: #         dbdigit.markDigitForManualRecognize() return False if dist[0,0]>3000000 or dist[0,1]>3500000 or dist[0,2]>4000000: #         dbdigit.markDigitForManualRecognize() return False #    ,        dbdigit.result = str(int(ret)) return True 

प्रशिक्षण विधि में वर्णित है KNN.train:
  @staticmethod def getKNN(): #      cv2.KNearest if KNN._knn==None: KNN._knn = cv2.KNearest() return KNN._knn @staticmethod def train(): knn = KNN.getKNN() #        train_digits = sess.query(Digit).filter(Digit.result!='?').filter_by(use_for_training=True).all() train_data = [] responses = [] for dbdigit in train_digits: h,w = dbdigit.body.shape #     if h*w != digit_base_h*digit_base_w: continue #     sample = dbdigit.body.reshape(digit_base_h*digit_base_w).astype(np.float32) train_data.append(sample) responses.append(int(dbdigit.result)) #  KNN knn.train(np.array(train_data), np.array(responses)) KNN._trained = True 


models.pyअगर फ़ाइल में कुछ उपयोग किए गए कार्यों के संचालन के बारे में प्रश्न हैं, तो मैं फ़ाइल से एक अंश देता हूं , लेकिन वर्णित कार्यों में नहीं।
लेख में कार्यों और विधियों का विवरण गुम है
 import datetime from sqlalchemy import Column, Integer, String, Text, Boolean, ForeignKey, DateTime, PickleType from sqlalchemy.orm import relationship from sqlalchemy.ext.declarative import declarative_base from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker import base64 import cv2 import numpy as np import os import logging import sys dbengine = create_engine('sqlite:///' + os.path.dirname(__file__) + '/../db/images.db', echo=False) Session = sessionmaker(bind=dbengine) sess = Session() Base = declarative_base() # image class class Image(Base): __tablename__ = 'images' id = Column(Integer, primary_key=True) file_name = Column(String) img_link = Column(Text) download_url = Column(Text) check_time = Column(DateTime) result = Column(String(8)) digits = relationship("Digit", backref="image") img = None # source image digits_img = None # cropped source image def __init__(self, file_name): self.file_name = file_name self.check_time = datetime.datetime.strptime(file_name, "gaz.%Y-%m-%d.%H.%M.%S.jpg") self.result = "" def __repr__(self): return "<Image ('%s','%s','%s')>" % (self.id, self.file_name, self.result) def dbDigit(self, i, digit_img): digit = sess.query(Digit).filter_by(image_id=self.id).filter_by(i=i).first() if not digit: digit = Digit(self, i, digit_img) sess.add(digit) else: digit.body = digit_img return digit ##    # digit class class Digit(Base): __tablename__ = 'digits' id = Column(Integer, primary_key=True) image_id = Column(Integer, ForeignKey("images.id")) i = Column(Integer) body = Column(PickleType) result = Column(String(1)) use_for_training = Column(Boolean) def __init__(self, image, i, digit_img): self.image_id = image.id self.i = i self.body = digit_img self.markDigitForManualRecognize() def __repr__(self): return "%s" % self.result def markDigitForManualRecognize (self, use_for_training=False): self.result = '?' self.use_for_training = use_for_training def getEncodedBody (self): enc = cv2.imencode('.png',self.body)[1] b64 = base64.b64encode(enc) return b64 ##    Base.metadata.create_all(bind=dbengine) # function to get Image object by file_name and img def getImage(file_name): image = sess.query(Image).filter_by(file_name=file_name).first() if not image: image = Image(file_name) sess.add(image) # store image object to base sess.commit() image.digits_img = None return image def getLastRecognizedImage(): return sess.query(Image).filter(Image.result!='').order_by(Image.check_time.desc()).first() def dgDigitById(digit_id): digit = sess.query(Digit).get(digit_id) return digit 


रीडिंग और मैनुअल मान्यता के विश्लेषण के लिए, मैंने फ्लास्क फ्रेमवर्क पर एक छोटा वेब-आधारित इंटरफ़ेस भी लिखा । मुझे यहां कोड नहीं मिला, जो कोई भी इच्छुक है वह इसे देख सकता है, साथ ही साथ शेष कोड भी जीथब पर देख सकता है

इंटरफ़ेस में केवल दो पृष्ठ हैं, एक ग्राफ़ में रीडिंग देखने के लिए, उदाहरण के लिए, एक दिन या एक सप्ताह के लिए:



संख्याओं की मैन्युअल मान्यता के लिए दूसरा पृष्ठ। जब मैंने अपने हाथों से पहली 20-30 रीडिंग चलाई, तो रोबोट ने खुद को रीडिंग को सही ढंग से पहचानना शुरू कर दिया। कभी-कभी, अपवाद अभी भी होते हैं और संख्या को मान्यता नहीं दी जा सकती है, यह सबसे अधिक बार डायल के रोटेशन के कारण होता है:




फिर आपको लापता संख्याओं को हाथ से दर्ज करना होगा:

या आप बस ऐसे सबूतों को अनदेखा कर सकते हैं, उन्हें चार्ट पर छोड़ दिया जाएगा, और कुछ भी बुरा नहीं होगा।

स्क्रिप्ट को अंतिम रूप देने की योजना है ताकि यह कई हालिया रीडिंग के संयोग के मामले में ई-मेल भेजेगा।

अगर मैं अंत तक पढ़ना चाहता हूं, तो मैं इसके बारे में बात करना चाहता हूं।

Source: https://habr.com/ru/post/In220869/


All Articles