2up1down commited on
Commit
cb0dfa0
1 Parent(s): 24ce39c

Upload 8 files

Browse files
Files changed (7) hide show
  1. .gitignore +2 -0
  2. app.py +367 -0
  3. corners-best.pt +3 -0
  4. example1.jpg +0 -0
  5. example2.jpg +0 -0
  6. keypoints-best.pt +3 -0
  7. requirements.txt +8 -0
.gitignore ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ gradio_cached_examples/
2
+ temp*.jpg
app.py ADDED
@@ -0,0 +1,367 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #
2
+ import gradio as gr
3
+
4
+ from ultralytics import YOLO
5
+
6
+ from google.cloud import vision
7
+
8
+ client = vision.ImageAnnotatorClient()
9
+
10
+ import math
11
+ from scipy.spatial import KDTree
12
+ import io
13
+ from time import time
14
+ from PIL import Image, ImageDraw
15
+ import numpy as np
16
+ import cv2
17
+
18
+ from typing import Union
19
+
20
+ modelPh = r'corners-best.pt'
21
+
22
+ model1DIM = 640
23
+ keypointModel = r'keypoints-best.pt'
24
+
25
+
26
+ _examples = ["example1.jpg", "example2.jpg"]
27
+
28
+
29
+ def unwarp_image(warped_image, src_points, dst_points, output_width, output_height):
30
+ src_pts = np.array(src_points).astype(np.float64)
31
+ dst_pts = np.array(dst_points).astype(np.float64)
32
+
33
+ homography, mask = cv2.findHomography(src_pts, dst_pts)
34
+ unwarped_image = cv2.warpPerspective(
35
+ np.array(warped_image), homography, (output_width, output_height)
36
+ )
37
+ unwarped_image = Image.fromarray(unwarped_image)
38
+
39
+ return unwarped_image
40
+
41
+
42
+ model0 = None
43
+
44
+ def get_load_PhModel():
45
+ global model0
46
+ if model0 ==None:
47
+ tic = time()
48
+ model0 = YOLO(modelPh) # load a custom model
49
+ print(f"model0 load took: {time()-tic:.2g}")
50
+ return model0
51
+
52
+
53
+ def get_corners(results:list, img):
54
+ global model1DIM
55
+ # keypoints ie corners for homography
56
+ KP = "topLeft topRight bottomRight bottomLeft".split()
57
+ r = results[0]
58
+ kpco = r.keypoints.xy.cpu().squeeze()
59
+ assert len(kpco)>0, "not found"
60
+ keypoints = {k:v.numpy() for v,k in zip(kpco,KP)}
61
+ sz = model1DIM
62
+ dstCorners = np.array([(0,0),(sz,0),(sz,sz),(0,sz)])
63
+ planar = unwarp_image(img, np.array(list(keypoints.values())),dstCorners, sz,sz)
64
+ # planar.save("temp-ph.jpg")
65
+ return planar, keypoints
66
+
67
+
68
+ model = None
69
+
70
+ def get_load_KpModel():
71
+ global model
72
+ if model == None:
73
+ tic = time()
74
+ model = YOLO(keypointModel) # load a custom model
75
+ print(f"model load took: {time()-tic:.2g}")
76
+ return model
77
+
78
+ def preprocessImg(planar):
79
+ img = planar.convert('RGB').copy()
80
+ w,h = img.size
81
+ smalldl = abs(w-h)/h <0.05
82
+ _ = max(w,h)
83
+ DIM = w
84
+ if w!=h and smalldl:
85
+ img = img.resize((_,_))
86
+ elif w!=h:
87
+ img = img.resize((_,_))
88
+
89
+ return img
90
+
91
+
92
+ def get_keypoints(results:list):
93
+ assert len(results) ==1, "found multiple dials. expected only 1"
94
+ r = results[0]
95
+ # ordering
96
+ kp = "start_kp center end_kp tip".split()
97
+ kpco = r.keypoints.xy.cpu().squeeze()
98
+ keypoints = {k:v.numpy() for v,k in zip(kpco,kp)}
99
+ assert len(keypoints["center"])==2, "center keypoint not found"
100
+ assert len(keypoints["tip"])==2, "tip keypoint not found"
101
+ return keypoints
102
+
103
+ def cosangle(a,b, ignoreRot=False):
104
+ na = np.linalg.norm(a)
105
+ nb = np.linalg.norm(b)
106
+ angle2tip = np.rad2deg(np.arccos(np.dot(a, b)/(na*nb)))
107
+ angle2tip
108
+ rotdir = np.cross(a,b) < 0
109
+ if rotdir and not ignoreRot:
110
+ return 360-angle2tip
111
+ return angle2tip
112
+
113
+
114
+ def calculate_sweep_angles(keypoints:dict):
115
+ # get sweep angles start->tip
116
+ a = keypoints["start_kp"] - keypoints["center"]
117
+ b = keypoints["tip"] - keypoints["center"]
118
+ angle2tip = cosangle(a, b)
119
+ # get sweep angles start->end
120
+ b = keypoints["end_kp"] - keypoints["center"]
121
+ totalAngle = cosangle(a, b)
122
+ return angle2tip, totalAngle
123
+
124
+
125
+ def get_text_from_image(client, path_or_img)->Union[list[dict],Exception ]:
126
+ if type(path_or_img)==str:
127
+ with open(path_or_img, "rb") as image_file:
128
+ content = image_file.read()
129
+ else:
130
+ buf = io.BytesIO()
131
+ path_or_img.save(buf, format="JPEG")
132
+ content = buf.getvalue()
133
+
134
+ image = vision.Image(content=content)
135
+
136
+ response = client.text_detection(image=image)
137
+ if response.error.message:
138
+ raise Exception(
139
+ "{}\nFor more info on error messages, check: "
140
+ "https://cloud.google.com/apis/design/errors".format(response.error.message)
141
+ )
142
+
143
+ texts = response.text_annotations
144
+ contents = [ {"text": found.description, "boxCorners": [ (vert.x, vert.y) for vert in found.bounding_poly.vertices]} for found in texts]
145
+ return contents
146
+
147
+ def median_point_of_bounding_box(x1, y1, x2, y2, x3, y3, x4, y4):
148
+ x_coords = [x1, x2, x3, x4]
149
+ y_coords = [y1, y2, y3, y4]
150
+ x_median = sum(x_coords) / len(x_coords)
151
+ y_median = sum(y_coords) / len(y_coords)
152
+ return x_median, y_median
153
+
154
+ def to_numeric(text:str):
155
+ try:
156
+ return float(text)
157
+ except:
158
+ pass
159
+ return None
160
+
161
+ def result_as_validvalue(contents:list[dict])->tuple[list[dict], list[str]]:
162
+ # only valid values and sort min to max
163
+ valid = []
164
+ other = []
165
+ for f in contents:
166
+ t = f["text"]
167
+ value = to_numeric(t)
168
+ if "\n" in t:
169
+ continue
170
+ elif value == None and t!="":
171
+ other.append(t)
172
+ continue
173
+ b = f["boxCorners"]
174
+ m = median_point_of_bounding_box(*np.array(b).flatten())
175
+ valid.append({"text":f["text"], "value": value, "mid": m})
176
+
177
+ valid.sort(key=lambda e: e["value"])
178
+ return valid, other
179
+
180
+
181
+ distance = lambda a,b : np.sqrt(np.square(np.array(a)-np.array(b)).sum())
182
+
183
+ def determine_ocr_neighbors(center, valid:list[dict])->tuple[ list, float ]:
184
+ def cosangle(a,b):
185
+ na = np.linalg.norm(a)
186
+ nb = np.linalg.norm(b)
187
+ ang = np.rad2deg(np.arccos(np.dot(a, b)/(na*nb)))
188
+ rotdir = -1 if np.cross(a,b) < 0 else 1
189
+ return ang , rotdir
190
+ # compute angles between values
191
+ values = [valid[0]]
192
+ values[0]["dang"] = 0
193
+ rates = []
194
+ angS = 0
195
+ for v in valid[1:]:
196
+ u = v.copy()
197
+ u["dv"] = v["value"] - values[-1]["value"]
198
+ a = np.array(values[-1]["mid"]) - center
199
+ b = np.array(v["mid"]) - center
200
+ ang,_ = cosangle(a,b)
201
+ if _ <0:
202
+ Warning(f"skipping {u['value']} rot:{_}")
203
+ continue
204
+ angS += ang
205
+ u["dang"] = ang
206
+ u["dvda"] = u["dv"] / ang
207
+ rates.append(u["dvda"])
208
+ values.append(u)
209
+
210
+ rates = np.array(rates)
211
+ meanAng = angS/len(valid)
212
+ if len(rates)>=6:
213
+ ix = np.bitwise_and(rates> np.quantile(rates, 0.05) , rates<np.quantile(rates, 0.95))
214
+ if not np.all(~ix):
215
+ rates = rates[ix]
216
+ rate = rates.mean()
217
+ rate, meanAng
218
+ return values, rate
219
+
220
+
221
+ def vec_angle(v1, v2)->tuple[float, bool]:
222
+ vector1 = v1/np.linalg.norm(v1)
223
+ vector2 = v2/np.linalg.norm(v2)
224
+ angle_rad = np.arctan2(np.cross(vector1, vector2), np.dot(vector1, vector2))
225
+ return math.degrees(angle_rad)
226
+
227
+
228
+ def angles_from_tip(keypoints, values, nearestIx):
229
+ center = keypoints["center"]
230
+ tip = keypoints["tip"] - center
231
+ v = values[nearestIx[0]]
232
+ a = v["mid"] - center
233
+ ang = vec_angle(a,tip)
234
+ cumsum = 0
235
+ for i in range(nearestIx[0],-1,-1):
236
+ values[i]["before"] = abs(ang)+cumsum
237
+ cumsum += values[i]["dang"]
238
+
239
+ v = values[nearestIx[1]]
240
+ a = v["mid"] - center
241
+ ang = vec_angle(a,tip)
242
+ values[nearestIx[1]]["dang"] = 0
243
+ cumsum = 0
244
+ for i in range(nearestIx[1], len(values)):
245
+ cumsum -= values[i]["dang"]
246
+ values[i]["before"] = -abs(ang)+cumsum
247
+
248
+ return values
249
+
250
+
251
+ def get_needle_value(img, keypoints):
252
+
253
+ tic2 = time()
254
+ contents = get_text_from_image(client, img)
255
+ toc = time()
256
+ print(f"ocr took: {toc-tic2:.1g}")
257
+
258
+ assert len(contents)
259
+ valid,other = result_as_validvalue(contents)
260
+ assert len(valid)
261
+ center = np.array(keypoints["center"])
262
+ values, rate = determine_ocr_neighbors(center, valid)
263
+ assert len(values)>=2, "failed to find at least 2 OCR values"
264
+
265
+ # import pandas as pd
266
+ # print(pd.DataFrame.from_dict(values))
267
+
268
+ tree = KDTree([v["mid"] for v in values])
269
+ # find bounding ocr values of tip
270
+ dist, nearestIx = tree.query(keypoints["tip"],k=2)
271
+ nearestIx.sort()
272
+ dist, nearestIx
273
+
274
+ values = angles_from_tip(keypoints, values, nearestIx)
275
+ # compare against start and end
276
+ c = keypoints["center"]
277
+ tip = keypoints["tip"] - c
278
+
279
+ tipValues = []
280
+ for i in range(len(values)):
281
+ v = values[i]
282
+ a = v["mid"] - c
283
+ ang = vec_angle(a,tip)
284
+ before = v["before"]
285
+ startValue = v["value"]
286
+ angle2tip = ang
287
+ needleVal = -1
288
+
289
+ angle2tip = before
290
+
291
+ needleVal = angle2tip * rate + startValue # tip value from nearest Ix
292
+ tipValues.append(needleVal)
293
+ print(f"{i}, {ang:.2f}, {before:.2f}, @{needleVal:.2f}, {startValue}")
294
+
295
+ # print(f"total took: {toc-tic:.1g}")
296
+ tipValues = np.array(tipValues)
297
+
298
+ debug(img, contents, keypoints)
299
+
300
+ startValue= float(values[0]["value"])
301
+ tipvalue= round(tipValues[nearestIx].mean(),2)
302
+ endValue= float(values[-1]["value"])
303
+
304
+ return {"startValue": startValue, "tipvalue": tipvalue, "endValue": endValue, "unitPerDeg": float(rate), "otherText": list(set(other))}
305
+
306
+
307
+ # debug draw
308
+ def corners2bbox(C):
309
+ p = np.array(C)
310
+ s,e = p.min(axis=0).astype(int), p.max(axis=0).astype(int)
311
+ return s, e
312
+
313
+ def debug(img, contents, keypoints):
314
+ draw = ImageDraw.Draw(img)
315
+
316
+ for f in contents:
317
+ b = f["boxCorners"]
318
+ s,e = corners2bbox(b)
319
+ c = (255,0,0)
320
+ draw.rectangle((*s,*e), fill=None, outline=c, width=1)
321
+ m = median_point_of_bounding_box(*np.array(b).flatten())
322
+ draw.point(m, (255,0,255))
323
+ img
324
+
325
+ for v,c in zip(keypoints.values(), [(255,0,0), (0,255,0), (0,0,255),(255,0,255)]):
326
+ s = np.array(v)-1
327
+ e = np.array(v)+1
328
+ draw.rectangle((*s,*e), c)
329
+ img.save("temp-ocr.jpg")
330
+ print("saved debug img")
331
+
332
+
333
+ def predict(img, detect_gauge_first):
334
+ if detect_gauge_first:
335
+ model0 = get_load_PhModel()
336
+ results = model0.predict(img)
337
+ phimg,_ = get_corners(results, img)
338
+ else:
339
+ phimg = img.copy()
340
+
341
+ model = get_load_KpModel()
342
+ phimg = preprocessImg(phimg)
343
+ results = model.predict(phimg)
344
+ keypoints = get_keypoints(results)
345
+
346
+ angle2tip, totalAngle = calculate_sweep_angles(keypoints)
347
+
348
+ payload = get_needle_value(phimg, keypoints)
349
+ payload["angleToTip"] = round(angle2tip,2)
350
+ payload["totalAngle"] = round(totalAngle,2)
351
+
352
+ return payload
353
+
354
+
355
+ def test(img, detect_gauge_first):
356
+ return {"msg":str(img.size), "other": detect_gauge_first}
357
+
358
+
359
+ gr.Interface(fn=predict,
360
+ inputs=[
361
+ gr.Image(type="pil", sources=["upload","clipboard"],streaming=False, min_width=640),
362
+ gr.Checkbox(True, label="detect gauge first", info="if input image is zoomed in on only one gauge, uncheck box")
363
+ ],
364
+ outputs="json",
365
+ examples=[_examples],
366
+ cache_examples=True)\
367
+ .launch()
corners-best.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:a88502e86a40941aec69fe4d48e03c675a9381500fbf4c1ca8e3d1a89db089a9
3
+ size 37732202
example1.jpg ADDED
example2.jpg ADDED
keypoints-best.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:d583485a30cd58e986231e7a02b84ce86e117d7eb48d4b5a901e4bada55319ac
3
+ size 6408962
requirements.txt ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ ultralytics==8.1.2
2
+ opencv-python==4.9.0.80
3
+ opencv-python-headless==4.8.0.76
4
+ numpy==1.24.1
5
+ scipy==1.11.2
6
+ gradio_client==0.8.0
7
+ google-cloud-vision==3.5.0
8
+ Pillow==9.3.0