最近在做储能项目时需要用户提供负荷数据,大部分客户提供的都是 excel 或者 pdf 格式的文件。但是这次遇到一个客户因为特殊原因无法提供可读文件,只给我们提供了一年 365 天每天的负荷曲线图片,其中某一天的图片如下
所以我们要通过技术手段提取折线图中的每个小时所对应的数据,初步思路如下:
- 使用 ocr 提取最大值 max 和最小值 min;
- 截取曲线图部分,利用 pillow 读取每个像素点的颜色,然后找出曲线;
- 根据曲线图宽度 / 24,得到整点的分段,然后去匹配对应的 y 轴的位置;
- 结合 min、max、y 计算得到具体的数值 y1;
使用 tesseract 识别出 min 和 max
环境准备:
makefile
python: v3.11.6
pytesseract: 0.3.10
pillow: 10.1.0
根据坐标裁剪图片,只保留最大值、最小值部分,去除干扰,大致代码如下
python
from PIL import Image
image = Image.open("./images/4.png").convert("RGB")
width, height = image.size
crop_area = (368, 96, 675, 110)
cropped_image = image.crop(crop_area)
cropped_image.show()
于是就得到了下面的图片
接着对图片进行二值化和放大处理,更利于提高 ocr 的准确率
python
gray_image = cropped_image.convert("L")
gray_image = gray_image.resize((gray_image.width * 3, gray_image.height * 3))
gray_image.show()
尝试使用 pytesseract ocr 图片内容
python
text = pytesseract.image_to_string(
gray_image,
lang="eng",
config="--psm 7 --oem 3 -c tessedit_char_whitelist='0123456789.: '",
)
print(text)
输出如下:
编写提取 min、max 的分割逻辑
python
# 对 text 按空格分隔, 并过滤掉空字符串
text = text.replace("\n", "").replace(":", " ").split(" ")
text = list(filter(lambda x: "." in x, text))
min = 0
max = 0
if len(text) == 2:
min = float(text[0])
max = float(text[1])
elif len(text) == 1:
max = float(text[0])
print("min: {}, max: {}".format(min, max))
输出如下:
基于像素色值提取曲线
先对图片进行裁剪,截取出曲线区域,为了后续计算位置
python
chart_area = (70, 158, width - 28, height - 77)
chart_image = image.crop(chart_area)
chart_image.show();
曲线颜色大致是红色,可以遍历像素点,将红色的点修改为绿色,然后观察是否找对位置
python
color_to_change = (255, 0, 0)
target_color = (0, 255, 0)
pixels = chart_image.load()
width, height = chart_image.size
# 因为图片质量问题,其实曲线并不是红色,也不是一个颜色,经过一番调试,得到了以下的代码
for x in range(width):
for y in range(height):
if (
abs(pixels[x, y][0] - color_to_change[0]) < 30
and abs(pixels[x, y][1] - color_to_change[1]) < 180
and abs(pixels[x, y][2] - color_to_change[2]) < 180
):
pixels[x, y] = target_color
chart_image.show()
运行一下,可以得到下面的结果
我们在之前已经获取到了曲线的 min、max,为了后续计算,我们还需要知道 min、max 值出现的位置,对上面的代码进行改造
python
minXPoint = ()
maxXPoint = ()
minYPoint = ()
maxYPoint = ()
pixels = chart_image.load()
width, height = chart_image.size
points = []
for x in range(width):
for y in range(height):
if (
abs(pixels[x, y][0] - color_to_change[0]) < 30
and abs(pixels[x, y][1] - color_to_change[1]) < 180
and abs(pixels[x, y][2] - color_to_change[2]) < 180
):
pixels[x, y] = target_color
if len(minXPoint) > 0:
minXPoint = (x, y) if minXPoint[0] > x else minXPoint
else:
minXPoint = (x, y)
if len(maxXPoint) > 0:
maxXPoint = (x, y) if maxXPoint[0] < x else maxXPoint
else:
maxXPoint = (x, y)
if len(minYPoint) > 0:
minYPoint = (x, y) if minYPoint[1] > y else minYPoint
else:
minYPoint = (x, y)
if len(maxYPoint) > 0:
maxYPoint = (x, y) if maxYPoint[1] < y else maxYPoint
else:
maxYPoint = (x, y)
points.append((x, y))
print("minXPoint: ", minXPoint, "maxPoint: ", maxXPoint, "minYPoint: ", minYPoint, "maxYPoint: ", maxYPoint)
执行结果如下,我们获取到了 x 和 y 最大、最小值的坐标
分割x轴并计算
python
interval = width / 24
start = round(minXPoint[0] / interval)
end = 24
# 用于存放 24 小时的数据坐标
points24 = [0 * i for i in range(0, 24)]
# 根据 x 在 points 中尝试寻找 y 的值,如果有多个,取第一个
for i in range(start, end):
x = round(i * interval)
p = [tup for tup in points if tup[0] == x]
points24[i] = p[0] if len(p) > 0 else 0
# 每一像素代表多少
yInterval = (max - min) / (maxYPoint[1] - minYPoint[1])
for i in range(len(points24)):
p = points24[i]
if p == 0:
continue
y = format((maxYPoint[1] - p[1]) * yInterval + min, ".2f")
print("{}:00, {}".format(i, y))
运行一下,结果如下
至此,我们就提取出了曲线上的整点的数据。
最后一步
读取 images 下的所有文件,然后 for loop 一下,将输出结构化合并整理成 excel 就 👌 了,基操没啥难度,就不写了。