1. 程式人生 > 程式設計 >OpenCV 錶盤指標自動讀數的示例程式碼

OpenCV 錶盤指標自動讀數的示例程式碼

前段時間參加了一個錶盤指標讀數的比賽,今天來總結一下

資料集一共有一千張圖片:

OpenCV 錶盤指標自動讀數的示例程式碼

方法一:徑向灰度求和

基本原理:

將影象以錶盤圓心轉換成極座標,然後通過矩陣按行求和找到二值圖最大值即為指標尖端

匯入需要用到的包

import cv2 as cv
import numpy as np
import math
from matplotlib import pyplot as plt
import os

影象預處理

去除背景:利用提取紅色實現

def extract_red(image):
  """
  通過紅色過濾提取出指標
  """
  red_lower1 = np.array([0,43,46])
  red_upper1 = np.array([10,255,255])
  red_lower2 = np.array([156,46])
  red_upper2 = np.array([180,255])
  dst = cv.cvtColor(image,cv.COLOR_BGR2HSV)
  mask1 = cv.inRange(dst,lowerb=red_lower1,upperb=red_upper1)
  mask2 = cv.inRange(dst,lowerb=red_lower2,upperb=red_upper2)
  mask = cv.add(mask1,mask2)
  return mask

OpenCV 錶盤指標自動讀數的示例程式碼

獲得鐘錶中心:輪廓查詢,取出輪廓的外接矩形,根據矩形面積找出圓心

def get_center(image):
  """
  獲取鐘錶中心
  """ 
  edg_output = cv.Canny(image,100,150,2) # canny運算元提取邊緣
  cv.imshow('dsd',edg_output)
  # 獲取圖片輪廓
  contours,hireachy = cv.findContours(edg_output,cv.RETR_TREE,cv.CHAIN_APPROX_SIMPLE)
  center = []
  cut=[0,0]
  for i,contour in enumerate(contours):
    x,y,w,h = cv.boundingRect(contour) # 外接矩形
    area = w * h # 面積
    if area < 100 or area > 4000:
      continue
    cv.rectangle(image,(x,y),(x + w,y + h),(255,0),1)
    cx = w / 2
    cy = h / 2
    cv.circle(image,(np.int(x + cx),np.int(y + cy)),1,0)) ## 在圖上標出圓心
    center = [np.int(x + cx),np.int(y + cy)]
    break
  return center[::-1]

OpenCV 錶盤指標自動讀數的示例程式碼

由上面的影象可以看出,圓心定位還是非常準確的

圖片裁剪

def ChangeImage(image):
  """
  影象裁剪
  """
  # 指標提取
  mask = extract_red(image)
  mask = cv.medianBlur(mask,ksize=5)#去噪
  # 獲取中心
  center = get_center(mask)
  # 去除多餘黑色邊框
  [y,x] = center
  cut = mask[y-300:y+300,x-300:x+300]
  # 因為mask處理後已經是二值影象,故不用轉化為灰度影象
  return cut

剪裁後的影象如下圖所示:

OpenCV 錶盤指標自動讀數的示例程式碼

極座標轉換

注意:需要將圖片裁剪成正方形

def polar(image):
  """
  轉換成極座標
  """
  x,y = 300,300
  maxRadius = 300*math.sqrt(2)
  linear_polar = cv.linearPolar(image,(y,x),maxRadius,cv.WARP_FILL_OUTLIERS + cv.INTER_LINEAR)
  mypolar = linear_polar.copy()
  #將圖片調整為從0度開始
  mypolar[:150,:] = linear_polar[450:,:]
  mypolar[150:,:] = linear_polar[:450,:]
  cv.imshow("linear_polar",linear_polar)
  cv.imshow("mypolar",mypolar)
  return mypolar

OpenCV 錶盤指標自動讀數的示例程式碼

由影象就可以很容易發現指標的頂點

計算角度

def Get_Reading(sumdata):
  """
  讀數並輸出
  """
  peak = []
  # s記錄遍歷時波是否在上升
  s = sumdata[0] < sumdata[1]
  for i in range(599):
    # 上升階段
    if s==True and sumdata[i] > sumdata[i+1] and sumdata[i] > 70000:
      peak.append(sumdata[i])
      s=False
    # 下降階段
    if s==False and sumdata[i] < sumdata[i+1]:
      s=True
  peak.sort()
  a = sumdata[0]
  b = sumdata[-1]
  if not peak or max(a,b) > peak[-1]:
    peak.append(max(a,b))
  longindex = (sumdata.index(peak[-1]))%599
  longnum = (longindex + 1)//25*50
  # 先初始化和長的同一刻度
  #shortindex = longindex
  shortnum = round(longindex / 6)
  try:
    shortindex = sumdata.index(peak[-2])
    shortnum = round(shortindex / 6)
  except IndexError:
    i=0
    while i<300:
      i += 1
      l = sumdata[(longindex-i)%600]
      r = sumdata[(longindex+i)%600]
      possibleshort = max(l,r)
      # 在短指標可能範圍內尋找插值符合條件的值
      if possibleshort > 80000:
        continue
      elif possibleshort < 60000:
        break
      else:
        if abs(l-r) > 17800:
          shortindex = sumdata.index(possibleshort) - 1
          shortnum = round(shortindex / 6)
          break
  return [longnum,shortnum%100]
def test():
  """
  RGS法測試
  """
  image = cv.imread("./BONC/1_{0:0>4d}".format(400) + ".jpg")
  newimg = ChangeImage(image)
  polarimg = polar(newimg)
  psum = polarimg.sum(axis=1,dtype = 'int32')
  result = Get_Reading(list(psum))
  print(result)
if __name__ == "__main__":
  test()
  k = cv.waitKey(0)
  if k == 27:
    cv.destroyAllWindows()
  elif k == ord('s'):
    cv.imwrite('new.jpg',src)
    cv.destroyAllWindows()

[1050,44]

方法二:Hough直線檢測

原理:利用Hough變換檢測出指標的兩條邊,從而兩條邊的中線角度即為指標刻度

資料預處理與上面的方法類似

OpenCV 錶盤指標自動讀數的示例程式碼

可以看到分別檢測出了兩個指標的左右兩條邊,然後可以由這四個角度算出兩個指標中線的角度,具體計算過程寫的有點複雜

class Apparatus:
  def __init__(self,name):
    self.name = name
    self.angle = []
    self.src = cv.imread(name)


  def line_detect_possible_demo(self,image,center,tg):
    '''
    :param image: 二值圖
    :param center: 圓心
    :param tg: 直線檢測maxLineGap
    '''
    res = {} # 存放線段的斜率和資訊
    edges = cv.Canny(image,50,apertureSize=7)
    cv.imshow("abcdefg",edges)
    lines = cv.HoughLinesP(edges,np.pi/360,13,minLineLength=20,maxLineGap=tg)
    for line in lines:
      x_1,y_1,x_2,y_2 = line[0]
      # 將座標原點移動到圓心
      x1 = x_1 - center[0]
      y1 = center[1] - y_1
      x2 = x_2 - center[0]
      y2 = center[1] - y_2

      # 計算斜率
      if x2 - x1 == 0:
        k = float('inf')
      else:
        k = (y2-y1)/(x2-x1)
      d1 = np.sqrt(max(abs(x2),abs(x1)) ** 2 + (max(abs(y2),abs(y1))) ** 2) # 線段長度
      d2 = np.sqrt(min(abs(x2),abs(x1)) ** 2 + (min(abs(y2),abs(y1))) ** 2)
      # 將長指標與短指標做標記
      if d1 < 155 and d1 > 148 and d2 > 115:
        res[k] = [1]
      elif d1 < 110 and d1 > 100 and d2 > 75:
        res[k] = [2]
      else:
        continue
      res[k].append(1) if (x2 + x1) /2 > 0 else res[k].append(0) # 將14象限與23象限分離
      cv.line(self.src,(x1 + center[0],center[1] - y1),(x2 + center[0],center[1] - y2),1)
      cv.imshow("line_detect-posssible_demo",self.src)


      # 計算線段中點的梯度來判斷是指標的左側線段還是右側線段
      middle_x = int((x_1 + x_2) / 2)
      middle_y = int((y_1 + y_2) / 2)
      grad_mat = image[middle_y-5:middle_y+6,middle_x-5:middle_x+6]
      cv.imshow("grad_mat",grad_mat)
      grad_x = cv.Sobel(grad_mat,cv.CV_32F,0)
      grad_y = cv.Sobel(grad_mat,1)
      gradx = np.max(grad_x) if np.max(grad_x) != 0 else np.min(grad_x)
      grady = np.max(grad_y) if np.max(grad_y) != 0 else np.min(grad_y)
      if ((gradx >=0 and grady >= 0) or (gradx <= 0 and grady >= 0)) and res[k][1] == 1:
        res[k].append(1) # 右測
      elif ((gradx <= 0 and grady <= 0) or (gradx >= 0 and grady <= 0)) and res[k][1] == 0:
        res[k].append(1)
      else:
        res[k].append(0) # 左側
    # 計算角度
    angle1 = [i for i in res if res[i][0] == 1]
    angle2 = [i for i in res if res[i][0] == 2]
    # 長指標
    a = np.arctan(angle1[0])
    b = np.arctan(angle1[1])
    if a * b < 0 and max(abs(a),abs(b)) > np.pi / 4:
      if a + b < 0:
        self.angle.append(math.degrees(-(a + b) / 2)) if res[angle1[1]][1] == 1 else self.angle.append(
          math.degrees(-(a + b) / 2) + 180)
      else:
        self.angle.append(math.degrees(np.pi - (a + b) / 2)) if res[angle1[1]][1] == 1 else self.angle.append(
          math.degrees(np.pi - (a + b) / 2) + 180)
    else:
      self.angle.append(math.degrees(np.pi / 2 - (a + b) / 2)) if res[angle1[1]][1] == 1 else self.angle.append(math.degrees(np.pi / 2 - (a + b) / 2) + 180)
    print('長指標讀數:%f' % self.angle[0])


    # 短指標
    a = np.arctan(angle2[0])
    b = np.arctan(angle2[1])
    if a * b < 0 and max(abs(a),abs(b)) > np.pi / 4:
      if a + b < 0:
        self.angle.append(math.degrees(-(a + b) / 2)) if res[angle2[1]][1] == 1 else self.angle.append(
          math.degrees(-(a + b) / 2) + 180)
      else:
        self.angle.append(math.degrees(np.pi - (a + b) / 2)) if res[angle2[1]][1] == 1 else self.angle.append(
          math.degrees(np.pi - (a + b) / 2) + 180)
    else:
      self.angle.append(math.degrees(np.pi / 2 - (a + b) / 2)) if res[angle2[1]][1] == 1 else self.angle.append(math.degrees(np.pi / 2 - (a + b) / 2) + 180)
    print('短指標讀數:%f' % self.angle[1])



  def get_center(self,mask):
    edg_output = cv.Canny(mask,66,2)
    cv.imshow('edg',edg_output)
    # 外接矩形
    contours,cv.CHAIN_APPROX_SIMPLE)
    center = []
    for i,contour in enumerate(contours):
      x,h = cv.boundingRect(contour) # 外接矩形
      area = w * h # 面積
      if area > 1000 or area < 40:
        continue
      #print(area)
      # cv.circle(src,(np.int(cx),np.int(cy)),3,(255),-1)
      cv.rectangle(self.src,1)
      cx = w / 2
      cy = h / 2
      cv.circle(self.src,0))
      center.extend([np.int(x + cx),np.int(y + cy)])
      break

    cv.imshow('center',self.src)
    return center


  def extract(self,image):
    red_lower1 = np.array([0,46])
    red_lower2 = np.array([156,46])
    red_upper1 = np.array([10,255])
    red_upper2 = np.array([180,255])
    frame = cv.cvtColor(image,cv.COLOR_BGR2HSV)
    mask1 = cv.inRange(frame,upperb=red_upper1)
    mask2 = cv.inRange(frame,upperb=red_upper2)
    mask = cv.add(mask1,mask2)
    mask = cv.bitwise_not(mask)
    cv.imshow('mask',mask)
    return mask


  def test(self):
    self.src = cv.resize(self.src,dsize=None,fx=0.5,fy=0.5) # 此處可以修改插值方式interpolation
    mask = self.extract(self.src)
    mask = cv.medianBlur(mask,ksize=5) # 去噪
    # 獲取中心
    center = self.get_center(mask)
    # 去除多餘黑色邊框
    [y,x] = center
    mask = mask[x - 155:x + 155,y - 155:y + 155]
    cv.imshow('mask',mask)
    #self.find_short(center,mask)
    try:
      self.line_detect_possible_demo(mask,20)
    except IndexError:
      try:
        self.src = cv.imread(self.name)
        self.src = cv.resize(self.src,fy=0.5) # 此處可以修改插值方式interpolation
        self.src = cv.convertScaleAbs(self.src,alpha=1.4,beta=0)
        blur = cv.pyrMeanShiftFiltering(self.src,10,17)
        mask = self.extract(blur)
        self.line_detect_possible_demo(mask,20)
      except IndexError:
        self.src = cv.imread(self.name)
        self.src = cv.resize(self.src,fy=0.5) # 此處可以修改插值方式interpolation
        self.src = cv.normalize(self.src,dst=None,alpha=200,beta=10,norm_type=cv.NORM_MINMAX)
    
        blur = cv.pyrMeanShiftFiltering(self.src,20)


if __name__ == '__main__':
  apparatus = Apparatus('./BONC/1_0555.jpg')
  # 讀取圖片
  apparatus.test()
  k = cv.waitKey(0)
  if k == 27:
    cv.destroyAllWindows()
  elif k == ord('s'):
    cv.imwrite('new.jpg',apparatus.src)
    cv.destroyAllWindows()

長指標讀數:77.070291
短指標讀數:218.896747

由結果可以看出精確度還是挺高的,但是這種方法有三個缺點:

  • 當兩個指標重合時候不太好處理
  • 有時候hough直線檢測只能檢測出箭頭的一條邊,這時候就會報錯,可以利用影象增強、角點檢測和影象梯度來輔助解決,但是效果都不太好
  • 計算角度很複雜!!(也可能是我想複雜了,不過這段程式碼確實花了大量時間)

程式碼裡可能還有很多問題,希望大家多多指出

到此這篇關於OpenCV 錶盤指標自動讀數的示例程式碼的文章就介紹到這了,更多相關OpenCV 錶盤自動讀數內容請搜尋我們以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援我們!