1
+ from flask import Flask , Response , render_template , request , jsonify
2
+ import requests
3
+ #from apriltag import Detector
4
+ import apriltag
5
+ import cv2
6
+ import numpy as np
7
+ from threading import Lock
8
+ import socket
9
+ import math
10
+
11
+ # Create the Flask app
12
+ app2 = Flask (__name__ )
13
+
14
+ # Create an AprilTag detector
15
+ options = apriltag .DetectorOptions (families = "tag36h11" )
16
+ detector = apriltag .Detector (options , searchpath = apriltag ._get_dll_path ())
17
+
18
+ # Lock for thread-safe access to shared variables
19
+ tags_lock = Lock ()
20
+
21
+ # Detected tags and tracking tag
22
+ detected_tags = []
23
+ tracking_tag = None
24
+
25
+ # Camera parameters
26
+ focal_length_x = 547.7837
27
+ focal_length_y = 547.8070
28
+ principal_point_x = 303.9048
29
+ principal_point_y = 243.7748
30
+
31
+ tag_height_meters = 0.065 # TODO: make sure to change for specific april tag
32
+ @app2 .route ('/april_tag_click' , methods = ['POST' ])
33
+ def april_tag_click ():
34
+ global tracking_tag
35
+ with tags_lock :
36
+ data = request .get_json ()
37
+ if not data :
38
+ return jsonify ({'error' : 'Invalid data' }), 400
39
+
40
+ x , y = data .get ('x' ), data .get ('y' )
41
+
42
+ for tag in detected_tags :
43
+ if x >= tag ['xMin' ] and x <= tag ['xMax' ] and y >= tag ['yMin' ] and y <= tag ['yMax' ]:
44
+ tracking_tag = tag
45
+ #
46
+ return jsonify ({'message' : 'Switched to tag centering mode.' })
47
+
48
+ return jsonify ({'message' : 'Clicked outside of all tags' })
49
+
50
+ @app2 .route ('/' )
51
+ def index ():
52
+ return render_template ('index.html' )
53
+
54
+ @app2 .route ('/video' )
55
+ def video ():
56
+ return Response (get_frames (), mimetype = 'multipart/x-mixed-replace;boundary=frame' )
57
+
58
+ def get_frames ():
59
+ url = 'http://192.168.1.3:5000/video_feed'
60
+ response = requests .get (url , stream = True )
61
+
62
+ frame_buffer = b''
63
+
64
+ for chunk in response .iter_content (chunk_size = 1024 ):
65
+ frame_buffer += chunk
66
+ frame_end = frame_buffer .find (b'\xff \xd9 ' )
67
+ while frame_end != - 1 :
68
+ frame_start = frame_buffer .find (b'\xff \xd8 ' )
69
+ if frame_start != - 1 :
70
+ frame = frame_buffer [frame_start :frame_end + 2 ]
71
+
72
+ processed_frame = process_frame (frame )
73
+
74
+ yield (b'--frame\r \n '
75
+ b'Content-Type: image/jpeg\r \n \r \n ' + processed_frame + b'\r \n ' )
76
+
77
+ frame_buffer = frame_buffer [frame_end + 2 :]
78
+
79
+ frame_end = frame_buffer .find (b'\xff \xd9 ' )
80
+ def process_frame (frame ):
81
+ global detected_tags
82
+ global tracking_tag
83
+
84
+ nparr = np .frombuffer (frame , np .uint8 )
85
+ image = cv2 .imdecode (nparr , cv2 .IMREAD_COLOR )
86
+
87
+ gray = cv2 .cvtColor (image , cv2 .COLOR_BGR2GRAY )
88
+ # results = detector.detect(gray)
89
+
90
+ results , overlay = apriltag .detect_tags (gray ,
91
+ detector ,
92
+ camera_params = (focal_length_x , focal_length_y , principal_point_x , principal_point_y ),
93
+ tag_size = tag_height_meters ,
94
+ vizualization = 3 ,
95
+ verbose = 3 ,
96
+ annotation = True
97
+ )
98
+
99
+ with tags_lock :
100
+ if tracking_tag is None :
101
+ detected_tags .clear ()
102
+ # for r in results:
103
+ # # print("curr: ", r.tag_id)
104
+ # corners = np.array(r.corners, dtype=np.int32)
105
+ # cv2.polylines(image, [corners], True, (0, 255, 0), 2)
106
+ # tag = {
107
+ # 'xMin': int(corners[:, 0].min()),
108
+ # 'yMin': int(corners[:, 1].min()),
109
+ # 'xMax': int(corners[:, 0].max()),
110
+ # 'yMax': int(corners[:, 1].max()),
111
+ # 'id' : r.tag_id,
112
+
113
+ # }
114
+ # detected_tags.append(tag)
115
+ i = 0
116
+ while (i < len (results )):
117
+ (ptA , ptB , ptC , ptD ) = results [i ].corners
118
+ corners = results [i ].corners
119
+ corners = np .array (results [i ].corners , dtype = np .int32 )
120
+ cv2 .polylines (image , [corners ], True , (0 , 255 , 0 ), 2 )
121
+ ptB = (int (ptB [0 ]), int (ptB [1 ]))
122
+ ptC = (int (ptC [0 ]), int (ptC [1 ]))
123
+ ptD = (int (ptD [0 ]), int (ptD [1 ]))
124
+ ptA = (int (ptA [0 ]), int (ptA [1 ]))
125
+
126
+ cX = int (results [i ].center [0 ])
127
+ cY = int (results [i ].center [1 ])
128
+ distance , angle = get_distance_and_angle (results [i + 1 ], results [i ].center )
129
+
130
+ tag = {
131
+ 'xMin' : int (corners [:, 0 ].min ()),
132
+ 'yMin' : int (corners [:, 1 ].min ()),
133
+ 'xMax' : int (corners [:, 0 ].max ()),
134
+ 'yMax' : int (corners [:, 1 ].max ()),
135
+ 'id' : results [i ].tag_id ,
136
+ 'pose' : results [i + 1 ],
137
+ 'center' : results [i ].center
138
+
139
+ }
140
+ detected_tags .append (tag )
141
+ i += 4
142
+ else :
143
+ # Handle tracking mode
144
+ i = 0
145
+ while (i < len (results )):
146
+ corners = np .array (results [i ].corners , dtype = np .int32 )
147
+ if tracking_tag ['id' ] == results [i ].tag_id : # Assuming each tag has a unique 'id'
148
+ cv2 .polylines (image , [corners ], True , (0 , 0 , 255 ), 2 )
149
+ # distance = calculate_distance(corners)
150
+ distance , angle = get_distance_and_angle (results [i + 1 ], results [i ].center )
151
+ tracking_tag ['corners' ] = corners
152
+ send_tag_data_to_remote_two (angle , distance , results [i + 1 ])
153
+ if distance < 0.1 :
154
+ # tag_meet_distance = True
155
+ send_message ('DISTANCE MET' )
156
+ # Send head-on data
157
+ if is_tag_head_on (angle ):
158
+ # tag_is_head_on = True
159
+ send_message ('HEAD ON MET' )
160
+ if distance < 0.1 and is_tag_head_on (angle ):
161
+ tracking_tag = None # Stop tracking
162
+ print ("DONE TRACKING TAG" )
163
+ send_tag_done_to_remote ()
164
+
165
+ else :
166
+ send_tag_data_to_remote (tracking_tag , distance , angle )
167
+ break
168
+
169
+ ret , buffer = cv2 .imencode ('.jpg' , image )
170
+ return buffer .tobytes ()
171
+
172
+ def calculate_distance (corners ):
173
+ y_min , y_max = corners [:, 1 ].min (), corners [:, 1 ].max ()
174
+ distance = - 1 * (tag_height_meters * focal_length_y ) / (y_min - y_max )
175
+ return distance
176
+
177
+ def send_tag_data_to_remote_two (angle , distance , pose ):
178
+ print ("sending info..." )
179
+ head_on = False
180
+ if (is_tag_head_on (angle )):
181
+ head_on = True
182
+ data = {
183
+ 'angle' : angle ,
184
+ 'distance' : distance ,
185
+ 'headon' : head_on ,
186
+ }
187
+ remote_url = 'http://192.168.1.3:5003'
188
+ try :
189
+ # Send the data to the remote server
190
+ requests .post (remote_url , json = data )
191
+
192
+ except requests .RequestException as e :
193
+ print (f"Error sending tag data to remote: { e } " )
194
+
195
+ def yaw_angle (R , center , distance ):
196
+ # Convert the rotation matrix to euler angles
197
+ # beta = -np.arcsin(R[2,0]) # equivalent to y axis
198
+ # gamma = np.arctan2(R[1,0]/np.cos(beta), R[0,0]/np.cos(beta)) # equivalent to z axis
199
+
200
+ #yaw = np.degrees(math.atan2(R[2, 1], R[1, 1]))
201
+ # yaw = np.degrees(math.atan2(R[0, 2], R[2, 2]))
202
+ # diff = center[0] - principal_point_x
203
+
204
+ # return (np.degrees(np.arctan2(diff, distance)))
205
+
206
+ # # scale = diff / distance
207
+ # return np.degrees(np.arctan2(diff, distance))
208
+ return center [0 ]
209
+
210
+ def get_distance_and_angle (pose_matrix , center ):
211
+ # # Extract translation vector (first 3 elements of the last column)
212
+ # translation_vector = pose_matrix[:3, 3]
213
+ # distance = np.linalg.norm(translation_vector)
214
+ # rotation_matrix = pose_matrix[:3, :3]
215
+
216
+ # euler_angles_rad = np.arccos((np.trace(rotation_matrix) - 1) / 2)
217
+ # angle_degrees = np.degrees(euler_angles_rad)
218
+
219
+ # return distance, angle_degrees
220
+
221
+ # Calculate the Yaw angle
222
+ translation_vector = np .linalg .norm (pose_matrix [0 :3 , 3 ])
223
+ distance = np .linalg .norm (translation_vector )
224
+ angle = yaw_angle (pose_matrix , center , distance )
225
+
226
+
227
+ # Calculate the distance
228
+
229
+
230
+
231
+ return distance , angle
232
+
233
+
234
+ def is_tag_head_on (angle ):
235
+
236
+ # Compute each corner's angle and check if they are within the tolerance
237
+ tolerance = 10 # degrees
238
+ expected_angle = 0 # degrees
239
+
240
+ if not (expected_angle - tolerance <= angle <= expected_angle + tolerance ):
241
+ return False
242
+
243
+ # If we reach here, all angles are within tolerance. The tag is considered head-on.
244
+ return True
245
+
246
+
247
+ def send_message (message ):
248
+ print ("sending info..." )
249
+ data = {
250
+ 'status' : message
251
+ }
252
+ remote_url = 'http://192.168.1.3:5003'
253
+ try :
254
+ requests .post (remote_url , json = data )
255
+ except requests .RequestException as e :
256
+ print (f"Error sending done message to remote: { e } " )
257
+
258
+
259
+ def send_tag_data_to_remote (tag , distance , angle ):
260
+ # You were passing corners and distance which would not be available here.
261
+ # We need to fetch or calculate them within this function now.
262
+ print ("sending info..." )
263
+ # corners = tag['corners'] # Assuming the 'corners' are stored in the tag dictionary
264
+ head_on = False
265
+ if (is_tag_head_on (angle )):
266
+ head_on = True
267
+
268
+ data = {
269
+ 'angle' : angle ,
270
+ 'distance' : distance ,
271
+ 'headon' : head_on ,
272
+ }
273
+ remote_url = 'http://192.168.1.3:5003'
274
+ try :
275
+ requests .post (remote_url , json = data )
276
+ except requests .RequestException as e :
277
+ print (f"Error sending tag data to remote: { e } " )
278
+
279
+ def send_tag_done_to_remote ():
280
+ print ("sending info..." )
281
+ data = {
282
+ 'status' : 'DONE'
283
+ }
284
+ remote_url = 'http://192.168.1.3:5003'
285
+ try :
286
+ requests .post (remote_url , json = data )
287
+ except requests .RequestException as e :
288
+ print (f"Error sending done message to remote: { e } " )
289
+
290
+ if __name__ == '__main__' :
291
+ hostname = socket .gethostname ()
292
+ IP = socket .gethostbyname (hostname )
293
+ app2 .run (host = IP , port = 5001 , threaded = True )
0 commit comments