家裡使用的全戶軟水機,控制器上有一個LCD螢幕,裡面的數字以位元數字(digit number)呈現,控制器待機時會有幾種資訊輪流顯示,例如現在時間、重洗時間、瞬時流量以及軟水水量,這次想做的就是把瞬時流量以及軟水水量給取出來,給Home Assistant做紀錄。
試過一些OCR的模組做文字辨識,但結果都不太理想,主要原因可能是這種位元數字並不連續,例如2就被分成了五個直線和橫線,再加上還要判斷是否為需要的資訊,那不如自己刻一支,也不需要花太多額外的資源,像是GPU。不過ESP32_CAM還沒來,先拍一張照片來寫程式。
方法是判斷固定座標點像素的亮度,一個數字以七塊直線和橫線組合而成,示意圖如下
針對每個數字去判斷這七個位置的亮度,若亮度大於brightness_threshold為1,反之為0,這樣一組數字會有七個0和1的組合,例如數字2就是1011101,數字9就是1111011,依此類推。
所以先讀取影像並做必要的處理,像是裁切、降低解析度、轉黑白、降低亮度與拉高對比,目的就是要把雜訊給去乾淨,盡量留下一個非黑即白的影像。
from PIL import Image
import numpy as np
image_path = "IMG_0608.jpg" # 圖檔位置
crop_area = (1580, 1000, 3640, 1635) # 影像裁切範圍
resolution = 5 # 解析度縮小的倍數
brightness_offset = 50 # 亮度偏移
contrast_factor = 4 # 對比倍率
def process_image():
image = Image.open(image_path)
image = image.crop(crop_area)
new_resolution = ((crop_area[2] - crop_area[0]) // resolution, (crop_area[3] - crop_area[1]) // resolution)
bw_image = image.resize(new_resolution).convert("L")
average_brightness = np.mean(np.array(bw_image))
bw_image = Image.eval(bw_image, lambda x: max(0, x - average_brightness - brightness_offset) * contrast_factor)
bw_image.save('processed_image.jpg')
return bw_image
其中降低亮度的部分,是以整張裁切完並轉為黑白後的影像,取所有像素的亮度平均值後,再將每個像素亮度扣除這個平均值以及一個自定義的偏移值brightness_offset。至於能不能留下一個非黑即白的影像,可以從偏移值調整。
這邊可以得到一張黑白的影像如下
再來定義座標,四種輪流顯示的資訊中,瞬時流量以及軟水水量在第二碼與第三碼中間有一個小數點以及第四碼後面也有個小數點,而這二種各別的識別方式為
- 右下方m/n的燈號亮起為瞬時流量
- 右上方m3的燈號亮起軟水水量
所以總共有三個數字和三個標記要定義,每個數字如同前述取七個座標,每個標記取一個座標。
digit_offset = 87
digit_1_coordinate = [(43, 47), (20, 64), (61, 64), (37, 81), (11, 99), (55, 99), (31, 116)]
digit_2_coordinate = [(x + digit_offset, y) for x, y in digit_1_coordinate]
digit_3_coordinate = [(x + digit_offset*2, y) for x, y in digit_1_coordinate]
mark_1_coordinate = [(64, 115)]
mark_2_coordinate = [(240, 115)]
mark_3_coordinate = [(366, 31)]
digit_offset是每個數字之間的x偏移量是固定的,所以我只需要定義第一個數字的座標,再加上digit_offset就可以得到第二個和第三個數字的座標。這裡要注意的是座標原點是影像的左上角。
再來就開始判斷三個數字和三個標記的亮度、映射成數字並判斷資料類型,寫法如下
brightness_threshold = 200
def analyze_image(image):
result = {}
# 檢查每個數字的亮度
for i, coordinates in enumerate([digit_1_coordinate, digit_2_coordinate, digit_3_coordinate], start=1):
digit_value = 0
for coord in coordinates:
brightness = get_brightness(image, coord)
if brightness > brightness_threshold:
digit_value = (digit_value << 1) | 1
else:
digit_value = (digit_value << 1)
binary_digit = bin(digit_value)[2:].zfill(7)
result[f"digit_{i}"] = binary_digit
# 檢查每個標記的亮度
for i, coordinates in enumerate([mark_1_coordinate, mark_2_coordinate, mark_3_coordinate], start=1):
brightness = get_brightness(image, coordinates[0])
if brightness > brightness_threshold:
result[f"mark_{i}"] = 1
else:
result[f"mark_{i}"] = 0
# 將 binary_digit 映射到相應的數字
for key, value in result.items():
if "digit" in key:
if value == "0010010":
result[key] = 1
elif value == "1011101":
result[key] = 2
elif value == "1011011":
result[key] = 3
elif value == "0111010":
result[key] = 4
elif value == "1101011":
result[key] = 5
elif value == "1101111":
result[key] = 6
elif value == "1010010":
result[key] = 7
elif value == "1111111":
result[key] = 8
elif value == "1111011":
result[key] = 9
elif value == "1110111":
result[key] = 0
# 判斷瞬時流量與軟水水量
if result["mark_1"] == 1 and result["mark_2"] == 1 and result["mark_3"] == 1:
return f"volume {result['digit_1']}.{result['digit_2']}{result['digit_3']}"
elif result["mark_1"] == 1 and result["mark_2"] == 1 and result["mark_3"] == 0:
return f"flow {result['digit_1']}.{result['digit_2']}{result['digit_3']}"
else:
return "N/A"
def get_brightness(image, coordinates):
return image.getpixel(coordinates)
在判斷數字亮度這裡,digit_value的值就是我要的七個0與1組合而成的值,一開始先將digit_value設成0,每判斷一個區塊亮度,就將digit_value左移一位再加上判斷的結果,最後會得到八個數字,再將前面的0去掉即可。如果將result給print出來,會得到
{'digit_1': '1011101', 'digit_2': '1111011', 'digit_3': '1101011'}
分別代表2、9和5,最後將這些0和1的組合去做映射即可。
完整的程式碼為
from PIL import Image
import json
import numpy as np
image_path = "IMG_0608.jpg"
digit_offset = 87
crop_area = (1580, 1000, 3640, 1635)
resolution = 5
brightness_offset = 50
brightness_threshold = 200
contrast_factor = 4
digit_1_coordinate = [(43, 47), (20, 64), (61, 64), (37, 81), (11, 99), (55, 99), (31, 116)]
digit_2_coordinate = [(x + digit_offset, y) for x, y in digit_1_coordinate]
digit_3_coordinate = [(x + digit_offset*2, y) for x, y in digit_1_coordinate]
mark_1_coordinate = [(64, 115)]
mark_2_coordinate = [(240, 115)]
mark_3_coordinate = [(366, 31)]
def main():
processed_image = process_image()
result = analyze_image(processed_image)
json_result = json.dumps(result, indent=4)
print(json_result)
def get_brightness(image, coordinates):
return image.getpixel(coordinates)
def process_image():
image = Image.open(image_path)
image = image.crop(crop_area)
new_resolution = ((crop_area[2] - crop_area[0]) // resolution, (crop_area[3] - crop_area[1]) // resolution)
bw_image = image.resize(new_resolution).convert("L")
average_brightness = np.mean(np.array(bw_image))
bw_image = Image.eval(bw_image, lambda x: max(0, x - average_brightness - brightness_offset) * contrast_factor)
bw_image.save('processed_image.jpg')
return bw_image
def analyze_image(image):
result = {}
for i, coordinates in enumerate([digit_1_coordinate, digit_2_coordinate, digit_3_coordinate], start=1):
digit_value = 0
for coord in coordinates:
brightness = get_brightness(image, coord)
if brightness > brightness_threshold:
digit_value = (digit_value << 1) | 1
else:
digit_value = (digit_value << 1)
binary_digit = bin(digit_value)[2:].zfill(7)
result[f"digit_{i}"] = binary_digit
for i, coordinates in enumerate([mark_1_coordinate, mark_2_coordinate, mark_3_coordinate], start=1):
brightness = get_brightness(image, coordinates[0])
if brightness > brightness_threshold:
result[f"mark_{i}"] = 1
else:
result[f"mark_{i}"] = 0
for key, value in result.items():
if "digit" in key:
if value == "0010010":
result[key] = 1
elif value == "1011101":
result[key] = 2
elif value == "1011011":
result[key] = 3
elif value == "0111010":
result[key] = 4
elif value == "1101011":
result[key] = 5
elif value == "1101111":
result[key] = 6
elif value == "1010010":
result[key] = 7
elif value == "1111111":
result[key] = 8
elif value == "1111011":
result[key] = 9
elif value == "1110111":
result[key] = 0
if result["mark_1"] == 1 and result["mark_2"] == 1 and result["mark_3"] == 1:
return f"volume {result['digit_1']}.{result['digit_2']}{result['digit_3']}"
elif result["mark_1"] == 1 and result["mark_2"] == 1 and result["mark_3"] == 0:
return f"flow {result['digit_1']}.{result['digit_2']}{result['digit_3']}"
else:
return "N/A"
if __name__ == "__main__":
main()
得到的結果為
"volume 2.95"
最後再接入Home Assistant的sensor即可。