├── .gitattributes ├── README.md ├── oil.jpg └── oilmeter.py /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | *.sln merge=union 7 | *.csproj merge=union 8 | *.vbproj merge=union 9 | *.fsproj merge=union 10 | *.dbproj merge=union 11 | 12 | # Standard to msysgit 13 | *.doc diff=astextplain 14 | *.DOC diff=astextplain 15 | *.docx diff=astextplain 16 | *.DOCX diff=astextplain 17 | *.dot diff=astextplain 18 | *.DOT diff=astextplain 19 | *.pdf diff=astextplain 20 | *.PDF diff=astextplain 21 | *.rtf diff=astextplain 22 | *.RTF diff=astextplain 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | OilLevelReader 2 | ============== 3 | 4 | OpenCV based oil meter level reader for Raspberry Pi 5 | 6 | Uses a picamera and openCV on a Raspberry PI for reading an analog dial meter of an home heating oil tank. 7 | The angle of the needle is determined and converted to a percentage full for the tank 8 | 9 | The number can then be logged or sent to an openHAB server for logging and other events. 10 | -------------------------------------------------------------------------------- /oil.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techsavi/OilLevelReader/2763b583aadcd7d546734f28f2aae351343d8a9d/oil.jpg -------------------------------------------------------------------------------- /oilmeter.py: -------------------------------------------------------------------------------- 1 | __author__ = 'Chris' 2 | import picamera 3 | import math 4 | import datetime 5 | import cv2 6 | import io 7 | import numpy as np 8 | import httplib 9 | import time 10 | 11 | stream = io.BytesIO() 12 | 13 | CAMERA_WIDTH = 2592 14 | CAMERA_HEIGHT = 1944 15 | 16 | # Take a picture using picamera 17 | with picamera.PiCamera() as camera: 18 | camera.resolution = (CAMERA_WIDTH, CAMERA_HEIGHT) 19 | camera.awb_mode = 'shade' 20 | camera.start_preview() 21 | time.sleep(5) 22 | camera.capture(stream, format='jpeg') 23 | 24 | data = np.fromstring(stream.getvalue(), dtype=np.uint8) 25 | 26 | #import the picture into openCV 27 | image = cv2.imdecode(data, 1) 28 | 29 | # write original image if needed for off-line processing 30 | #cv2.imwrite("oilorig.jpg",image) 31 | 32 | # uncomment for off-line processing to use saved image 33 | #image = cv2.imread("oilorig.jpg") 34 | 35 | # due to physical mounting requirements of camera, rotate image for meter readable orientation 36 | image = cv2.transpose(image) 37 | image = cv2.flip(image, 0) 38 | 39 | # making a copy of the image for processing, process in grayscale 40 | img = image 41 | imggray = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY) 42 | 43 | imggray = cv2.blur(imggray,(5,5)) 44 | ret,imgbinary = cv2.threshold(imggray, 50, 255, cv2.THRESH_BINARY+cv2.THRESH_OTSU) 45 | ret,imgbinary = cv2.threshold(imggray, ret + 30, 255, cv2.THRESH_BINARY) 46 | 47 | # write out or show processed image for debugging 48 | #cv2.imwrite("bin.jpg",imgbinary) 49 | #cv2.imshow("thresh", imgbinary) 50 | 51 | #find largest blob, the white background of the meter 52 | # switch for pc/pi, depending if running on pi library or PC return value may require 2 or 3 vars 53 | # imgcont, contours,hierarchy = cv2.findContours(imgbinary, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE) 54 | contours,hierarchy = cv2.findContours(imgbinary, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE) 55 | maxarea = 0 56 | index = 0 57 | meterContour = 0 58 | for c in contours: 59 | area = cv2.contourArea(c) 60 | if (area > maxarea): 61 | maxarea = area 62 | meterContour = index 63 | index = index + 1 64 | 65 | # find the largest child blob of the white background, should be the needle 66 | maxarea = 0 67 | index = hierarchy[0, meterContour, 2] 68 | needleContour = 0 69 | while (index >= 0): 70 | c = contours[index] 71 | area = cv2.contourArea(c) 72 | if (area > maxarea): 73 | maxarea = area 74 | needleContour = index 75 | index = hierarchy[0,index,0] 76 | 77 | # find the largest child blob of the needle contour, should be only one, the pivot point 78 | maxarea = 0 79 | index = hierarchy[0, needleContour, 2] 80 | pivotContour = 0 81 | while (index >= 0): 82 | c = contours[index] 83 | area = cv2.contourArea(c) 84 | if (area > maxarea): 85 | maxarea = area 86 | pivotContour = index 87 | index = hierarchy[0,index,0] 88 | 89 | # compute line from contour and point of needle, from this we will get the measurement angle 90 | # the line however lacks direction and may be off +/- 180 degrees, use the pivot centroid to fix 91 | [line_vx,line_vy,line_x,line_y] = cv2.fitLine(contours[needleContour],2,0,0.01,0.01) 92 | needlePt = (line_x,line_y) 93 | 94 | # moments of the pivot contour, and the centroid of the pivot contour 95 | pivotMoments = cv2.moments(contours[pivotContour]) 96 | pivotPt = (int(pivotMoments['m10'] / pivotMoments['m00']), int(pivotMoments['m01'] / pivotMoments['m00'])) 97 | 98 | # find the vector from the pivot centroid to the needle line center 99 | dx = needlePt[0] - pivotPt[0] 100 | dy = needlePt[1] - pivotPt[1] 101 | 102 | # if dot product of needle-pivot vector and line is negative, flip the line direction 103 | # so the line angle will be oriented correctly 104 | if (line_vx * dx + line_vy * dy < 0): 105 | line_vx = -line_vx; 106 | line_vy = -line_vy; 107 | 108 | # with the corrected line vector, compute the angle and convert to degrees 109 | line_angle = math.atan2(line_vy, line_vx) * 180 / math.pi 110 | print line_angle 111 | 112 | # normalize the angle of the meter 113 | # the needle will go from approx 135 on the low end to 35 degrees on the high end 114 | normangle = line_angle 115 | # adjust the ranage so it doesn't wrap around, 135 to 395 116 | if (normangle < 90): normangle = normangle + 360 117 | # set the low end to 0, 0 to 260 118 | normangle = normangle - 135 119 | # normalize to percentage 120 | pct = normangle / 260.0 121 | print pct 122 | 123 | # for display / archive purposes crop the image to the meter view using bounding box 124 | minRect = cv2.minAreaRect(contours[meterContour]) 125 | box = cv2.cv.BoxPoints(minRect) 126 | box = np.int0(box) 127 | 128 | # draw the graphics of the box and needle 129 | cv2.drawContours(img,[box],0,(0,0,255),4) 130 | cv2.drawContours(img,contours,meterContour,(0,255,0),4) 131 | cv2.drawContours(img,contours,needleContour,(255,0,0),4) 132 | nsize = 120 133 | cv2.line(img,(line_x-line_vx*nsize,line_y-line_vy*nsize),(line_x+line_vx*nsize,line_y+line_vy*nsize),(0,0,255),4) 134 | 135 | #find min/max xy for cropping 136 | minx = box[0][0] 137 | miny = box[0][1] 138 | maxx = minx 139 | maxy = miny 140 | for i in (1, 3): 141 | if (box[i][0] < minx): minx = box[i][0] 142 | if (box[i][1] < miny): miny = box[i][1] 143 | if (box[i][0] > maxx): maxx = box[i][0] 144 | if (box[i][1] > maxy): maxy = box[i][1] 145 | 146 | # display the percentage above the bounding box 147 | cv2.putText(img,"{:4.1f}%".format(pct * 100),(minx+150,miny-30),cv2.FONT_HERSHEY_SIMPLEX,3.0,(0,255,255),4) 148 | 149 | # scale the extents for some background in the cropping 150 | cropscale = 1.5 151 | len2x = cropscale * (maxx - minx) / 2; 152 | len2y = cropscale * (maxy - miny) / 2 ; 153 | len2x = len2y / 3 * 4 154 | avgx = (minx + maxx) / 2 155 | avgy = (miny + maxy) / 2 156 | 157 | # find the top-left, bottom-right crop points 158 | cminx = int(avgx - len2x); 159 | cminy = int(avgy - len2y); 160 | cmaxx = int(avgx + len2x); 161 | cmaxy = int(avgy + len2y); 162 | 163 | # crop the image and output 164 | imgcrop = img[cminy:cmaxy, cminx:cmaxx] 165 | cv2.imwrite("oil.jpg", imgcrop) 166 | 167 | # display for debugging 168 | #imgscaled = cv2.resize(img, (0, 0), 0, 0.2, 0.2) 169 | #imgcropscaled = cv2.resize(imgcrop, (0, 0), 0, 0.5, 0.5) 170 | #cv2.imshow("output", imgscaled) 171 | #cv2.imshow("outputcrop", imgcropscaled) 172 | 173 | # create a timestamp for logging 174 | def timestr(fmt="%Y-%m-%d %H:%M:%S "): 175 | return datetime.datetime.now().strftime(fmt) 176 | 177 | # log the result 178 | with open('angle.log','a') as outf: 179 | outf.write(timestr()) 180 | outf.write('{:5.1f} deg {:4.1%}\n'.format(line_angle, pct)) 181 | 182 | # the following code will post the percentage to an openHAB server 183 | # in openHAB the item oilLevel is defined as a Number 184 | msg = '{:4.1f}'.format(pct * 100) 185 | 186 | web = httplib.HTTP('192.168.2.20:8080') 187 | web.putrequest('POST', '/rest/items/oilLevel') 188 | web.putheader('Content-type', 'text/plain') 189 | web.putheader('Content-length', '%d' % len(msg)) 190 | web.endheaders() 191 | web.send(msg) 192 | statuscode, statusmessage, header = web.getreply() 193 | result = web.getfile().read() 194 | 195 | # uncomment if debugging with opencv window views 196 | #cv2.waitKey() 197 | #cv2.destroyAllWindows() 198 | --------------------------------------------------------------------------------