Spaces:
Configuration error
Configuration error
Upload 3 files
Browse files- keypoint_ops.py +366 -0
- label_map_util.py +166 -0
- main.ipynb +197 -0
keypoint_ops.py
ADDED
@@ -0,0 +1,366 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Copyright 2017 The TensorFlow Authors. All Rights Reserved.
|
2 |
+
#
|
3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
4 |
+
# you may not use this file except in compliance with the License.
|
5 |
+
# You may obtain a copy of the License at
|
6 |
+
#
|
7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
8 |
+
#
|
9 |
+
# Unless required by applicable law or agreed to in writing, software
|
10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12 |
+
# See the License for the specific language governing permissions and
|
13 |
+
# limitations under the License.
|
14 |
+
# ==============================================================================
|
15 |
+
|
16 |
+
"""Keypoint operations.
|
17 |
+
|
18 |
+
Keypoints are represented as tensors of shape [num_instances, num_keypoints, 2],
|
19 |
+
where the last dimension holds rank 2 tensors of the form [y, x] representing
|
20 |
+
the coordinates of the keypoint.
|
21 |
+
"""
|
22 |
+
import numpy as np
|
23 |
+
import tensorflow as tf
|
24 |
+
|
25 |
+
|
26 |
+
def scale(keypoints, y_scale, x_scale, scope=None):
|
27 |
+
"""Scales keypoint coordinates in x and y dimensions.
|
28 |
+
|
29 |
+
Args:
|
30 |
+
keypoints: a tensor of shape [num_instances, num_keypoints, 2]
|
31 |
+
y_scale: (float) scalar tensor
|
32 |
+
x_scale: (float) scalar tensor
|
33 |
+
scope: name scope.
|
34 |
+
|
35 |
+
Returns:
|
36 |
+
new_keypoints: a tensor of shape [num_instances, num_keypoints, 2]
|
37 |
+
"""
|
38 |
+
with tf.name_scope(scope, 'Scale'):
|
39 |
+
y_scale = tf.cast(y_scale, tf.float32)
|
40 |
+
x_scale = tf.cast(x_scale, tf.float32)
|
41 |
+
new_keypoints = keypoints * [[[y_scale, x_scale]]]
|
42 |
+
return new_keypoints
|
43 |
+
|
44 |
+
|
45 |
+
def clip_to_window(keypoints, window, scope=None):
|
46 |
+
"""Clips keypoints to a window.
|
47 |
+
|
48 |
+
This op clips any input keypoints to a window.
|
49 |
+
|
50 |
+
Args:
|
51 |
+
keypoints: a tensor of shape [num_instances, num_keypoints, 2]
|
52 |
+
window: a tensor of shape [4] representing the [y_min, x_min, y_max, x_max]
|
53 |
+
window to which the op should clip the keypoints.
|
54 |
+
scope: name scope.
|
55 |
+
|
56 |
+
Returns:
|
57 |
+
new_keypoints: a tensor of shape [num_instances, num_keypoints, 2]
|
58 |
+
"""
|
59 |
+
with tf.name_scope(scope, 'ClipToWindow'):
|
60 |
+
y, x = tf.split(value=keypoints, num_or_size_splits=2, axis=2)
|
61 |
+
win_y_min, win_x_min, win_y_max, win_x_max = tf.unstack(window)
|
62 |
+
y = tf.maximum(tf.minimum(y, win_y_max), win_y_min)
|
63 |
+
x = tf.maximum(tf.minimum(x, win_x_max), win_x_min)
|
64 |
+
new_keypoints = tf.concat([y, x], 2)
|
65 |
+
return new_keypoints
|
66 |
+
|
67 |
+
|
68 |
+
def prune_outside_window(keypoints, window, scope=None):
|
69 |
+
"""Prunes keypoints that fall outside a given window.
|
70 |
+
|
71 |
+
This function replaces keypoints that fall outside the given window with nan.
|
72 |
+
See also clip_to_window which clips any keypoints that fall outside the given
|
73 |
+
window.
|
74 |
+
|
75 |
+
Args:
|
76 |
+
keypoints: a tensor of shape [num_instances, num_keypoints, 2]
|
77 |
+
window: a tensor of shape [4] representing the [y_min, x_min, y_max, x_max]
|
78 |
+
window outside of which the op should prune the keypoints.
|
79 |
+
scope: name scope.
|
80 |
+
|
81 |
+
Returns:
|
82 |
+
new_keypoints: a tensor of shape [num_instances, num_keypoints, 2]
|
83 |
+
"""
|
84 |
+
with tf.name_scope(scope, 'PruneOutsideWindow'):
|
85 |
+
y, x = tf.split(value=keypoints, num_or_size_splits=2, axis=2)
|
86 |
+
win_y_min, win_x_min, win_y_max, win_x_max = tf.unstack(window)
|
87 |
+
|
88 |
+
valid_indices = tf.logical_and(
|
89 |
+
tf.logical_and(y >= win_y_min, y <= win_y_max),
|
90 |
+
tf.logical_and(x >= win_x_min, x <= win_x_max))
|
91 |
+
|
92 |
+
new_y = tf.where(valid_indices, y, np.nan * tf.ones_like(y))
|
93 |
+
new_x = tf.where(valid_indices, x, np.nan * tf.ones_like(x))
|
94 |
+
new_keypoints = tf.concat([new_y, new_x], 2)
|
95 |
+
|
96 |
+
return new_keypoints
|
97 |
+
|
98 |
+
|
99 |
+
def change_coordinate_frame(keypoints, window, scope=None):
|
100 |
+
"""Changes coordinate frame of the keypoints to be relative to window's frame.
|
101 |
+
|
102 |
+
Given a window of the form [y_min, x_min, y_max, x_max], changes keypoint
|
103 |
+
coordinates from keypoints of shape [num_instances, num_keypoints, 2]
|
104 |
+
to be relative to this window.
|
105 |
+
|
106 |
+
An example use case is data augmentation: where we are given groundtruth
|
107 |
+
keypoints and would like to randomly crop the image to some window. In this
|
108 |
+
case we need to change the coordinate frame of each groundtruth keypoint to be
|
109 |
+
relative to this new window.
|
110 |
+
|
111 |
+
Args:
|
112 |
+
keypoints: a tensor of shape [num_instances, num_keypoints, 2]
|
113 |
+
window: a tensor of shape [4] representing the [y_min, x_min, y_max, x_max]
|
114 |
+
window we should change the coordinate frame to.
|
115 |
+
scope: name scope.
|
116 |
+
|
117 |
+
Returns:
|
118 |
+
new_keypoints: a tensor of shape [num_instances, num_keypoints, 2]
|
119 |
+
"""
|
120 |
+
with tf.name_scope(scope, 'ChangeCoordinateFrame'):
|
121 |
+
win_height = window[2] - window[0]
|
122 |
+
win_width = window[3] - window[1]
|
123 |
+
new_keypoints = scale(keypoints - [window[0], window[1]], 1.0 / win_height,
|
124 |
+
1.0 / win_width)
|
125 |
+
return new_keypoints
|
126 |
+
|
127 |
+
|
128 |
+
def keypoints_to_enclosing_bounding_boxes(keypoints):
|
129 |
+
"""Creates enclosing bounding boxes from keypoints.
|
130 |
+
|
131 |
+
Args:
|
132 |
+
keypoints: a [num_instances, num_keypoints, 2] float32 tensor with keypoints
|
133 |
+
in [y, x] format.
|
134 |
+
|
135 |
+
Returns:
|
136 |
+
A [num_instances, 4] float32 tensor that tightly covers all the keypoints
|
137 |
+
for each instance.
|
138 |
+
"""
|
139 |
+
ymin = tf.math.reduce_min(keypoints[:, :, 0], axis=1)
|
140 |
+
xmin = tf.math.reduce_min(keypoints[:, :, 1], axis=1)
|
141 |
+
ymax = tf.math.reduce_max(keypoints[:, :, 0], axis=1)
|
142 |
+
xmax = tf.math.reduce_max(keypoints[:, :, 1], axis=1)
|
143 |
+
return tf.stack([ymin, xmin, ymax, xmax], axis=1)
|
144 |
+
|
145 |
+
|
146 |
+
def to_normalized_coordinates(keypoints, height, width,
|
147 |
+
check_range=True, scope=None):
|
148 |
+
"""Converts absolute keypoint coordinates to normalized coordinates in [0, 1].
|
149 |
+
|
150 |
+
Usually one uses the dynamic shape of the image or conv-layer tensor:
|
151 |
+
keypoints = keypoint_ops.to_normalized_coordinates(keypoints,
|
152 |
+
tf.shape(images)[1],
|
153 |
+
tf.shape(images)[2]),
|
154 |
+
|
155 |
+
This function raises an assertion failed error at graph execution time when
|
156 |
+
the maximum coordinate is smaller than 1.01 (which means that coordinates are
|
157 |
+
already normalized). The value 1.01 is to deal with small rounding errors.
|
158 |
+
|
159 |
+
Args:
|
160 |
+
keypoints: A tensor of shape [num_instances, num_keypoints, 2].
|
161 |
+
height: Maximum value for y coordinate of absolute keypoint coordinates.
|
162 |
+
width: Maximum value for x coordinate of absolute keypoint coordinates.
|
163 |
+
check_range: If True, checks if the coordinates are normalized.
|
164 |
+
scope: name scope.
|
165 |
+
|
166 |
+
Returns:
|
167 |
+
tensor of shape [num_instances, num_keypoints, 2] with normalized
|
168 |
+
coordinates in [0, 1].
|
169 |
+
"""
|
170 |
+
with tf.name_scope(scope, 'ToNormalizedCoordinates'):
|
171 |
+
height = tf.cast(height, tf.float32)
|
172 |
+
width = tf.cast(width, tf.float32)
|
173 |
+
|
174 |
+
if check_range:
|
175 |
+
max_val = tf.reduce_max(keypoints)
|
176 |
+
max_assert = tf.Assert(tf.greater(max_val, 1.01),
|
177 |
+
['max value is lower than 1.01: ', max_val])
|
178 |
+
with tf.control_dependencies([max_assert]):
|
179 |
+
width = tf.identity(width)
|
180 |
+
|
181 |
+
return scale(keypoints, 1.0 / height, 1.0 / width)
|
182 |
+
|
183 |
+
|
184 |
+
def to_absolute_coordinates(keypoints, height, width,
|
185 |
+
check_range=True, scope=None):
|
186 |
+
"""Converts normalized keypoint coordinates to absolute pixel coordinates.
|
187 |
+
|
188 |
+
This function raises an assertion failed error when the maximum keypoint
|
189 |
+
coordinate value is larger than 1.01 (in which case coordinates are already
|
190 |
+
absolute).
|
191 |
+
|
192 |
+
Args:
|
193 |
+
keypoints: A tensor of shape [num_instances, num_keypoints, 2]
|
194 |
+
height: Maximum value for y coordinate of absolute keypoint coordinates.
|
195 |
+
width: Maximum value for x coordinate of absolute keypoint coordinates.
|
196 |
+
check_range: If True, checks if the coordinates are normalized or not.
|
197 |
+
scope: name scope.
|
198 |
+
|
199 |
+
Returns:
|
200 |
+
tensor of shape [num_instances, num_keypoints, 2] with absolute coordinates
|
201 |
+
in terms of the image size.
|
202 |
+
|
203 |
+
"""
|
204 |
+
with tf.name_scope(scope, 'ToAbsoluteCoordinates'):
|
205 |
+
height = tf.cast(height, tf.float32)
|
206 |
+
width = tf.cast(width, tf.float32)
|
207 |
+
|
208 |
+
# Ensure range of input keypoints is correct.
|
209 |
+
if check_range:
|
210 |
+
max_val = tf.reduce_max(keypoints)
|
211 |
+
max_assert = tf.Assert(tf.greater_equal(1.01, max_val),
|
212 |
+
['maximum keypoint coordinate value is larger '
|
213 |
+
'than 1.01: ', max_val])
|
214 |
+
with tf.control_dependencies([max_assert]):
|
215 |
+
width = tf.identity(width)
|
216 |
+
|
217 |
+
return scale(keypoints, height, width)
|
218 |
+
|
219 |
+
|
220 |
+
def flip_horizontal(keypoints, flip_point, flip_permutation, scope=None):
|
221 |
+
"""Flips the keypoints horizontally around the flip_point.
|
222 |
+
|
223 |
+
This operation flips the x coordinate for each keypoint around the flip_point
|
224 |
+
and also permutes the keypoints in a manner specified by flip_permutation.
|
225 |
+
|
226 |
+
Args:
|
227 |
+
keypoints: a tensor of shape [num_instances, num_keypoints, 2]
|
228 |
+
flip_point: (float) scalar tensor representing the x coordinate to flip the
|
229 |
+
keypoints around.
|
230 |
+
flip_permutation: rank 1 int32 tensor containing the keypoint flip
|
231 |
+
permutation. This specifies the mapping from original keypoint indices
|
232 |
+
to the flipped keypoint indices. This is used primarily for keypoints
|
233 |
+
that are not reflection invariant. E.g. Suppose there are 3 keypoints
|
234 |
+
representing ['head', 'right_eye', 'left_eye'], then a logical choice for
|
235 |
+
flip_permutation might be [0, 2, 1] since we want to swap the 'left_eye'
|
236 |
+
and 'right_eye' after a horizontal flip.
|
237 |
+
scope: name scope.
|
238 |
+
|
239 |
+
Returns:
|
240 |
+
new_keypoints: a tensor of shape [num_instances, num_keypoints, 2]
|
241 |
+
"""
|
242 |
+
with tf.name_scope(scope, 'FlipHorizontal'):
|
243 |
+
keypoints = tf.transpose(keypoints, [1, 0, 2])
|
244 |
+
keypoints = tf.gather(keypoints, flip_permutation)
|
245 |
+
v, u = tf.split(value=keypoints, num_or_size_splits=2, axis=2)
|
246 |
+
u = flip_point * 2.0 - u
|
247 |
+
new_keypoints = tf.concat([v, u], 2)
|
248 |
+
new_keypoints = tf.transpose(new_keypoints, [1, 0, 2])
|
249 |
+
return new_keypoints
|
250 |
+
|
251 |
+
|
252 |
+
def flip_vertical(keypoints, flip_point, flip_permutation, scope=None):
|
253 |
+
"""Flips the keypoints vertically around the flip_point.
|
254 |
+
|
255 |
+
This operation flips the y coordinate for each keypoint around the flip_point
|
256 |
+
and also permutes the keypoints in a manner specified by flip_permutation.
|
257 |
+
|
258 |
+
Args:
|
259 |
+
keypoints: a tensor of shape [num_instances, num_keypoints, 2]
|
260 |
+
flip_point: (float) scalar tensor representing the y coordinate to flip the
|
261 |
+
keypoints around.
|
262 |
+
flip_permutation: rank 1 int32 tensor containing the keypoint flip
|
263 |
+
permutation. This specifies the mapping from original keypoint indices
|
264 |
+
to the flipped keypoint indices. This is used primarily for keypoints
|
265 |
+
that are not reflection invariant. E.g. Suppose there are 3 keypoints
|
266 |
+
representing ['head', 'right_eye', 'left_eye'], then a logical choice for
|
267 |
+
flip_permutation might be [0, 2, 1] since we want to swap the 'left_eye'
|
268 |
+
and 'right_eye' after a horizontal flip.
|
269 |
+
scope: name scope.
|
270 |
+
|
271 |
+
Returns:
|
272 |
+
new_keypoints: a tensor of shape [num_instances, num_keypoints, 2]
|
273 |
+
"""
|
274 |
+
with tf.name_scope(scope, 'FlipVertical'):
|
275 |
+
keypoints = tf.transpose(keypoints, [1, 0, 2])
|
276 |
+
keypoints = tf.gather(keypoints, flip_permutation)
|
277 |
+
v, u = tf.split(value=keypoints, num_or_size_splits=2, axis=2)
|
278 |
+
v = flip_point * 2.0 - v
|
279 |
+
new_keypoints = tf.concat([v, u], 2)
|
280 |
+
new_keypoints = tf.transpose(new_keypoints, [1, 0, 2])
|
281 |
+
return new_keypoints
|
282 |
+
|
283 |
+
|
284 |
+
def rot90(keypoints, scope=None):
|
285 |
+
"""Rotates the keypoints counter-clockwise by 90 degrees.
|
286 |
+
|
287 |
+
Args:
|
288 |
+
keypoints: a tensor of shape [num_instances, num_keypoints, 2]
|
289 |
+
scope: name scope.
|
290 |
+
|
291 |
+
Returns:
|
292 |
+
new_keypoints: a tensor of shape [num_instances, num_keypoints, 2]
|
293 |
+
"""
|
294 |
+
with tf.name_scope(scope, 'Rot90'):
|
295 |
+
keypoints = tf.transpose(keypoints, [1, 0, 2])
|
296 |
+
v, u = tf.split(value=keypoints[:, :, ::-1], num_or_size_splits=2, axis=2)
|
297 |
+
v = 1.0 - v
|
298 |
+
new_keypoints = tf.concat([v, u], 2)
|
299 |
+
new_keypoints = tf.transpose(new_keypoints, [1, 0, 2])
|
300 |
+
return new_keypoints
|
301 |
+
|
302 |
+
|
303 |
+
def keypoint_weights_from_visibilities(keypoint_visibilities,
|
304 |
+
per_keypoint_weights=None):
|
305 |
+
"""Returns a keypoint weights tensor.
|
306 |
+
|
307 |
+
During training, it is often beneficial to consider only those keypoints that
|
308 |
+
are labeled. This function returns a weights tensor that combines default
|
309 |
+
per-keypoint weights, as well as the visibilities of individual keypoints.
|
310 |
+
|
311 |
+
The returned tensor satisfies:
|
312 |
+
keypoint_weights[i, k] = per_keypoint_weights[k] * keypoint_visibilities[i, k]
|
313 |
+
where per_keypoint_weights[k] is set to 1 if not provided.
|
314 |
+
|
315 |
+
Args:
|
316 |
+
keypoint_visibilities: A [num_instances, num_keypoints] boolean tensor
|
317 |
+
indicating whether a keypoint is labeled (and perhaps even visible).
|
318 |
+
per_keypoint_weights: A list or 1-d tensor of length `num_keypoints` with
|
319 |
+
per-keypoint weights. If None, will use 1 for each visible keypoint
|
320 |
+
weight.
|
321 |
+
|
322 |
+
Returns:
|
323 |
+
A [num_instances, num_keypoints] float32 tensor with keypoint weights. Those
|
324 |
+
keypoints deemed visible will have the provided per-keypoint weight, and
|
325 |
+
all others will be set to zero.
|
326 |
+
"""
|
327 |
+
if per_keypoint_weights is None:
|
328 |
+
num_keypoints = keypoint_visibilities.shape.as_list()[1]
|
329 |
+
per_keypoint_weight_mult = tf.ones((1, num_keypoints,), dtype=tf.float32)
|
330 |
+
else:
|
331 |
+
per_keypoint_weight_mult = tf.expand_dims(per_keypoint_weights, axis=0)
|
332 |
+
return per_keypoint_weight_mult * tf.cast(keypoint_visibilities, tf.float32)
|
333 |
+
|
334 |
+
|
335 |
+
def set_keypoint_visibilities(keypoints, initial_keypoint_visibilities=None):
|
336 |
+
"""Sets keypoint visibilities based on valid/invalid keypoints.
|
337 |
+
|
338 |
+
Some keypoint operations set invisible keypoints (e.g. cropped keypoints) to
|
339 |
+
NaN, without affecting any keypoint "visibility" variables. This function is
|
340 |
+
used to update (or create) keypoint visibilities to agree with visible /
|
341 |
+
invisible keypoint coordinates.
|
342 |
+
|
343 |
+
Args:
|
344 |
+
keypoints: a float32 tensor of shape [num_instances, num_keypoints, 2].
|
345 |
+
initial_keypoint_visibilities: a boolean tensor of shape
|
346 |
+
[num_instances, num_keypoints]. If provided, will maintain the visibility
|
347 |
+
designation of a keypoint, so long as the corresponding coordinates are
|
348 |
+
not NaN. If not provided, will create keypoint visibilities directly from
|
349 |
+
the values in `keypoints` (i.e. NaN coordinates map to False, otherwise
|
350 |
+
they map to True).
|
351 |
+
|
352 |
+
Returns:
|
353 |
+
keypoint_visibilities: a bool tensor of shape [num_instances, num_keypoints]
|
354 |
+
indicating whether a keypoint is visible or not.
|
355 |
+
"""
|
356 |
+
if initial_keypoint_visibilities is not None:
|
357 |
+
keypoint_visibilities = tf.cast(initial_keypoint_visibilities, tf.bool)
|
358 |
+
else:
|
359 |
+
keypoint_visibilities = tf.ones_like(keypoints[:, :, 0], dtype=tf.bool)
|
360 |
+
|
361 |
+
keypoints_with_nan = tf.math.reduce_any(tf.math.is_nan(keypoints), axis=2)
|
362 |
+
keypoint_visibilities = tf.where(
|
363 |
+
keypoints_with_nan,
|
364 |
+
tf.zeros_like(keypoint_visibilities, dtype=tf.bool),
|
365 |
+
keypoint_visibilities)
|
366 |
+
return keypoint_visibilities
|
label_map_util.py
ADDED
@@ -0,0 +1,166 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Copyright 2017 The TensorFlow Authors. All Rights Reserved.
|
2 |
+
#
|
3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
4 |
+
# you may not use this file except in compliance with the License.
|
5 |
+
# You may obtain a copy of the License at
|
6 |
+
#
|
7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
8 |
+
#
|
9 |
+
# Unless required by applicable law or agreed to in writing, software
|
10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12 |
+
# See the License for the specific language governing permissions and
|
13 |
+
# limitations under the License.
|
14 |
+
# ==============================================================================
|
15 |
+
|
16 |
+
"""Label map utility functions."""
|
17 |
+
|
18 |
+
import logging
|
19 |
+
|
20 |
+
import tensorflow as tf
|
21 |
+
from google.protobuf import text_format
|
22 |
+
import string_int_label_map_pb2
|
23 |
+
|
24 |
+
|
25 |
+
def _validate_label_map(label_map):
|
26 |
+
"""Checks if a label map is valid.
|
27 |
+
|
28 |
+
Args:
|
29 |
+
label_map: StringIntLabelMap to validate.
|
30 |
+
|
31 |
+
Raises:
|
32 |
+
ValueError: if label map is invalid.
|
33 |
+
"""
|
34 |
+
for item in label_map.item:
|
35 |
+
if item.id < 1:
|
36 |
+
raise ValueError('Label map ids should be >= 1.')
|
37 |
+
|
38 |
+
|
39 |
+
def create_category_index(categories):
|
40 |
+
"""Creates dictionary of COCO compatible categories keyed by category id.
|
41 |
+
|
42 |
+
Args:
|
43 |
+
categories: a list of dicts, each of which has the following keys:
|
44 |
+
'id': (required) an integer id uniquely identifying this category.
|
45 |
+
'name': (required) string representing category name
|
46 |
+
e.g., 'cat', 'dog', 'pizza'.
|
47 |
+
|
48 |
+
Returns:
|
49 |
+
category_index: a dict containing the same entries as categories, but keyed
|
50 |
+
by the 'id' field of each category.
|
51 |
+
"""
|
52 |
+
category_index = {}
|
53 |
+
for cat in categories:
|
54 |
+
category_index[cat['id']] = cat
|
55 |
+
return category_index
|
56 |
+
|
57 |
+
|
58 |
+
def convert_label_map_to_categories(label_map,
|
59 |
+
max_num_classes,
|
60 |
+
use_display_name=True):
|
61 |
+
"""Loads label map proto and returns categories list compatible with eval.
|
62 |
+
|
63 |
+
This function loads a label map and returns a list of dicts, each of which
|
64 |
+
has the following keys:
|
65 |
+
'id': (required) an integer id uniquely identifying this category.
|
66 |
+
'name': (required) string representing category name
|
67 |
+
e.g., 'cat', 'dog', 'pizza'.
|
68 |
+
We only allow class into the list if its id-label_id_offset is
|
69 |
+
between 0 (inclusive) and max_num_classes (exclusive).
|
70 |
+
If there are several items mapping to the same id in the label map,
|
71 |
+
we will only keep the first one in the categories list.
|
72 |
+
|
73 |
+
Args:
|
74 |
+
label_map: a StringIntLabelMapProto or None. If None, a default categories
|
75 |
+
list is created with max_num_classes categories.
|
76 |
+
max_num_classes: maximum number of (consecutive) label indices to include.
|
77 |
+
use_display_name: (boolean) choose whether to load 'display_name' field
|
78 |
+
as category name. If False or if the display_name field does not exist,
|
79 |
+
uses 'name' field as category names instead.
|
80 |
+
Returns:
|
81 |
+
categories: a list of dictionaries representing all possible categories.
|
82 |
+
"""
|
83 |
+
categories = []
|
84 |
+
list_of_ids_already_added = []
|
85 |
+
if not label_map:
|
86 |
+
label_id_offset = 1
|
87 |
+
for class_id in range(max_num_classes):
|
88 |
+
categories.append({
|
89 |
+
'id': class_id + label_id_offset,
|
90 |
+
'name': 'category_{}'.format(class_id + label_id_offset)
|
91 |
+
})
|
92 |
+
return categories
|
93 |
+
for item in label_map.item:
|
94 |
+
if not 0 < item.id <= max_num_classes:
|
95 |
+
logging.info('Ignore item %d since it falls outside of requested '
|
96 |
+
'label range.', item.id)
|
97 |
+
continue
|
98 |
+
if use_display_name and item.HasField('display_name'):
|
99 |
+
name = item.display_name
|
100 |
+
else:
|
101 |
+
name = item.name
|
102 |
+
if item.id not in list_of_ids_already_added:
|
103 |
+
list_of_ids_already_added.append(item.id)
|
104 |
+
categories.append({'id': item.id, 'name': name})
|
105 |
+
return categories
|
106 |
+
|
107 |
+
|
108 |
+
def load_labelmap(path):
|
109 |
+
"""Loads label map proto.
|
110 |
+
|
111 |
+
Args:
|
112 |
+
path: path to StringIntLabelMap proto text file.
|
113 |
+
Returns:
|
114 |
+
a StringIntLabelMapProto
|
115 |
+
"""
|
116 |
+
with tf.compat.v2.io.gfile.GFile(path, 'r') as fid:
|
117 |
+
label_map_string = fid.read()
|
118 |
+
label_map = string_int_label_map_pb2.StringIntLabelMap()
|
119 |
+
try:
|
120 |
+
text_format.Merge(label_map_string, label_map)
|
121 |
+
except text_format.ParseError:
|
122 |
+
label_map.ParseFromString(label_map_string)
|
123 |
+
_validate_label_map(label_map)
|
124 |
+
return label_map
|
125 |
+
|
126 |
+
|
127 |
+
def get_label_map_dict(label_map_path, use_display_name=False):
|
128 |
+
"""Reads a label map and returns a dictionary of label names to id.
|
129 |
+
|
130 |
+
Args:
|
131 |
+
label_map_path: path to label_map.
|
132 |
+
use_display_name: whether to use the label map items' display names as keys.
|
133 |
+
|
134 |
+
Returns:
|
135 |
+
A dictionary mapping label names to id.
|
136 |
+
"""
|
137 |
+
label_map = load_labelmap(label_map_path)
|
138 |
+
label_map_dict = {}
|
139 |
+
for item in label_map.item:
|
140 |
+
if use_display_name:
|
141 |
+
label_map_dict[item.display_name] = item.id
|
142 |
+
else:
|
143 |
+
label_map_dict[item.name] = item.id
|
144 |
+
return label_map_dict
|
145 |
+
|
146 |
+
|
147 |
+
def create_category_index_from_labelmap(label_map_path):
|
148 |
+
"""Reads a label map and returns a category index.
|
149 |
+
|
150 |
+
Args:
|
151 |
+
label_map_path: Path to `StringIntLabelMap` proto text file.
|
152 |
+
|
153 |
+
Returns:
|
154 |
+
A category index, which is a dictionary that maps integer ids to dicts
|
155 |
+
containing categories, e.g.
|
156 |
+
{1: {'id': 1, 'name': 'dog'}, 2: {'id': 2, 'name': 'cat'}, ...}
|
157 |
+
"""
|
158 |
+
label_map = load_labelmap(label_map_path)
|
159 |
+
max_num_classes = max(item.id for item in label_map.item)
|
160 |
+
categories = convert_label_map_to_categories(label_map, max_num_classes)
|
161 |
+
return create_category_index(categories)
|
162 |
+
|
163 |
+
|
164 |
+
def create_class_agnostic_category_index():
|
165 |
+
"""Creates a category index with a single `object` class."""
|
166 |
+
return {1: {'id': 1, 'name': 'object'}}
|
main.ipynb
ADDED
@@ -0,0 +1,197 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
"cells": [
|
3 |
+
{
|
4 |
+
"cell_type": "code",
|
5 |
+
"execution_count": 1,
|
6 |
+
"id": "fb70944c",
|
7 |
+
"metadata": {},
|
8 |
+
"outputs": [
|
9 |
+
{
|
10 |
+
"ename": "ModuleNotFoundError",
|
11 |
+
"evalue": "No module named 'simplejson'",
|
12 |
+
"output_type": "error",
|
13 |
+
"traceback": [
|
14 |
+
"\u001b[1;31m---------------------------------------------------------------------------\u001b[0m",
|
15 |
+
"\u001b[1;31mModuleNotFoundError\u001b[0m Traceback (most recent call last)",
|
16 |
+
"\u001b[1;32m~\\AppData\\Local\\Temp/ipykernel_23568/1068728291.py\u001b[0m in \u001b[0;36m<module>\u001b[1;34m\u001b[0m\n\u001b[0;32m 1\u001b[0m \u001b[1;32mfrom\u001b[0m \u001b[0mflask\u001b[0m \u001b[1;32mimport\u001b[0m \u001b[0mFlask\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mrequest\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mResponse\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m----> 2\u001b[1;33m \u001b[1;32mimport\u001b[0m \u001b[0msimplejson\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m\u001b[0;32m 3\u001b[0m \u001b[1;32mimport\u001b[0m \u001b[0mtensorflow\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 4\u001b[0m \u001b[1;32mimport\u001b[0m \u001b[0mvisualization_utils\u001b[0m \u001b[1;32mas\u001b[0m \u001b[0mvis_util\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 5\u001b[0m \u001b[1;32mfrom\u001b[0m \u001b[0mPIL\u001b[0m \u001b[1;32mimport\u001b[0m \u001b[0mImage\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n",
|
17 |
+
"\u001b[1;31mModuleNotFoundError\u001b[0m: No module named 'simplejson'"
|
18 |
+
]
|
19 |
+
}
|
20 |
+
],
|
21 |
+
"source": [
|
22 |
+
"from flask import Flask, request, Response\n",
|
23 |
+
"import simplejson\n",
|
24 |
+
"import tensorflow\n",
|
25 |
+
"import visualization_utils as vis_util\n",
|
26 |
+
"from PIL import Image\n",
|
27 |
+
"import numpy as np\n",
|
28 |
+
"from PIL import Image\n",
|
29 |
+
"import numpy as np\n",
|
30 |
+
"import label_map_util\n",
|
31 |
+
"import tensorflow as tf\n",
|
32 |
+
"%matplotlib inline\n",
|
33 |
+
"from matplotlib import pyplot as plt\n",
|
34 |
+
"import time\n",
|
35 |
+
"import cv2\n",
|
36 |
+
"from numpy import asarray\n",
|
37 |
+
"\n",
|
38 |
+
"# Creation of the Flask app\n",
|
39 |
+
"app = Flask(__name__)\n",
|
40 |
+
"# Flask route for Liveness checks\n",
|
41 |
+
"\n",
|
42 |
+
"\n",
|
43 |
+
"@app.route(\"/isalive\")\n",
|
44 |
+
"def isalive():\n",
|
45 |
+
" print(\"/isalive request\")\n",
|
46 |
+
" status_code = Response(status=200)\n",
|
47 |
+
" return status_code\n",
|
48 |
+
"\n",
|
49 |
+
"\n",
|
50 |
+
"# Flask route for predictions\n",
|
51 |
+
"\n",
|
52 |
+
"\n",
|
53 |
+
"@app.route('/predict', methods=['GET', 'POST'])\n",
|
54 |
+
"def prediction():\n",
|
55 |
+
" total_time_start = time.time()\n",
|
56 |
+
"\n",
|
57 |
+
"\n",
|
58 |
+
" def loadImageIntoNumpyArray(image):\n",
|
59 |
+
" (im_width, im_height) = image.size\n",
|
60 |
+
" if image.getdata().mode == \"RGBA\":\n",
|
61 |
+
" image = image.convert('RGB')\n",
|
62 |
+
" return asarray(image).reshape((im_height, im_width, 3)).astype(np.uint8)\n",
|
63 |
+
"\n",
|
64 |
+
" def main(image_path,model_path,model_PATH_TO_CKPT,path_to_labels):\n",
|
65 |
+
" image = Image.open(image_path)\n",
|
66 |
+
" image_np = loadImageIntoNumpyArray(image)\n",
|
67 |
+
" image_np_expanded = np.expand_dims(image_np, axis=0)\n",
|
68 |
+
" label_map = label_map_util.load_labelmap(path_to_labels)\n",
|
69 |
+
" # print(\"label_map------->\",type(label_map))\n",
|
70 |
+
" categories = label_map_util.convert_label_map_to_categories(label_map, max_num_classes=100, use_display_name=True)\n",
|
71 |
+
" category_index = label_map_util.create_category_index(categories)\n",
|
72 |
+
" # print(\"category index-->\",category_index)\n",
|
73 |
+
"\n",
|
74 |
+
" detection_graph = tf.Graph()\n",
|
75 |
+
" with detection_graph.as_default():\n",
|
76 |
+
" od_graph_def = tf.compat.v1.GraphDef()\n",
|
77 |
+
" with tf.compat.v2.io.gfile.GFile(model_PATH_TO_CKPT, 'rb') as fid:\n",
|
78 |
+
" serialized_graph = fid.read()\n",
|
79 |
+
" od_graph_def.ParseFromString(serialized_graph)\n",
|
80 |
+
" tf.import_graph_def(od_graph_def, name='')\n",
|
81 |
+
" sess = tf.compat.v1.Session(graph=detection_graph)\n",
|
82 |
+
" # Input tensor is the image\n",
|
83 |
+
" image_tensor = detection_graph.get_tensor_by_name('image_tensor:0')\n",
|
84 |
+
" # Output tensors are the detection boxes, scores, and classes\n",
|
85 |
+
" # Each box represents a part of the image where a particular object was detected\n",
|
86 |
+
" detection_boxes = detection_graph.get_tensor_by_name('detection_boxes:0')\n",
|
87 |
+
" # Each score represents level of confidence for each of the objects.\n",
|
88 |
+
" # The score is shown on the result image, together with the class label.\n",
|
89 |
+
" detection_scores = detection_graph.get_tensor_by_name('detection_scores:0')\n",
|
90 |
+
" detection_classes = detection_graph.get_tensor_by_name('detection_classes:0')\n",
|
91 |
+
" # Number of objects detected\n",
|
92 |
+
" num_detections = detection_graph.get_tensor_by_name('num_detections:0')\n",
|
93 |
+
" (boxes, scores, classes, num) = sess.run(\n",
|
94 |
+
" [detection_boxes, detection_scores, detection_classes, num_detections],\n",
|
95 |
+
" feed_dict={image_tensor: image_np_expanded})\n",
|
96 |
+
" vis_util.visualize_boxes_and_labels_on_image_array(\n",
|
97 |
+
" image_np,\n",
|
98 |
+
" np.squeeze(boxes),\n",
|
99 |
+
" np.squeeze(classes).astype(np.int32),\n",
|
100 |
+
" np.squeeze(scores),\n",
|
101 |
+
" category_index,\n",
|
102 |
+
" use_normalized_coordinates=True,\n",
|
103 |
+
" line_thickness=8,\n",
|
104 |
+
" min_score_thresh=0.1)\n",
|
105 |
+
" %matplotlib inline\n",
|
106 |
+
" from matplotlib import pyplot as plt\n",
|
107 |
+
" # print(\"boxes:\",boxes)\n",
|
108 |
+
" # print(\"class:\",classes)\n",
|
109 |
+
" objects = []\n",
|
110 |
+
" threshold = 0.5\n",
|
111 |
+
" # print(\"category:\",category_index)\n",
|
112 |
+
" boxes = boxes[0]\n",
|
113 |
+
" for index, value in enumerate(classes[0]):\n",
|
114 |
+
" object_dict = {}\n",
|
115 |
+
" if scores[0, index] > threshold:\n",
|
116 |
+
" object_dict[\"class\"] = (category_index.get(value)).get('name')\n",
|
117 |
+
" object_dict[\"score\"] = round(scores[0, index] * 100,2)\n",
|
118 |
+
" box = tuple(boxes[index].tolist())\n",
|
119 |
+
" ymin, xmin, ymax, xmax= box\n",
|
120 |
+
" im_width,im_height = 360,360\n",
|
121 |
+
" left, right, top, bottom = (xmin * im_width, xmax * im_width, \n",
|
122 |
+
" ymin * im_height, ymax * im_height)\n",
|
123 |
+
" object_dict[\"box\"] = (int(left), int(right), int(top), int(bottom))\n",
|
124 |
+
" objects.append(object_dict)\n",
|
125 |
+
"\n",
|
126 |
+
" image_orignal = Image.open(image_path)\n",
|
127 |
+
" image_np_orignal = loadImageIntoNumpyArray(image_orignal)\n",
|
128 |
+
"\n",
|
129 |
+
"\n",
|
130 |
+
" fig, ax = plt.subplots(1,2)\n",
|
131 |
+
"\n",
|
132 |
+
" fig.suptitle('Tag Deciphering')\n",
|
133 |
+
"\n",
|
134 |
+
" ax[0].imshow(image_np_orignal,aspect='auto');\n",
|
135 |
+
" ax[1].imshow(image_np,aspect='auto');\n",
|
136 |
+
"\n",
|
137 |
+
"\n",
|
138 |
+
" return objects\n",
|
139 |
+
"\n",
|
140 |
+
" image_path = \"C://Users//thirdeye//Documents//ytag_gcp//test_images//33102340_20221005_1.JPG\"\n",
|
141 |
+
" model_path = \"C://Users//thirdeye//Documents//ytag_gcp//ytag//yellow-black-28-may-22-inc-30-april-21\"\n",
|
142 |
+
" model_PATH_TO_CKPT = model_path+\"//inference//frozen_inference_graph.pb\"\n",
|
143 |
+
" path_to_labels = \"C://Users//thirdeye//Documents//ytag_gcp//ytag//tf_label_map.pbtxt\"\n",
|
144 |
+
"\n",
|
145 |
+
" result = main(image_path,model_path,model_PATH_TO_CKPT,path_to_labels)\n",
|
146 |
+
" # print(\"result-\",result)\n",
|
147 |
+
" # list_to_be_sorted= [{'class': 'Y', 'score': 99.97, 'box': (157, 191, 269, 288)}, {'class': '6', 'score': 99.93, 'box': (158, 191, 247, 267)}, {'class': '9', 'score': 99.88, 'box': (156, 190, 179, 196)}, {'class': '4', 'score': 99.8, 'box': (156, 189, 198, 219)}, {'class': '1', 'score': 99.65, 'box': (157, 189, 222, 244)}, {'class': 'F', 'score': 63.4, 'box': (155, 185, 157, 175)}]\n",
|
148 |
+
" newlist = sorted(result, key=lambda k: k['box'][3],reverse=False)\n",
|
149 |
+
"\n",
|
150 |
+
" text =''\n",
|
151 |
+
" for each in newlist:\n",
|
152 |
+
" if(each['score']>65):\n",
|
153 |
+
" text += each['class']\n",
|
154 |
+
" # print(\"text:\",text)\n",
|
155 |
+
" if(text!=\"\"):\n",
|
156 |
+
" text = text.replace(\"yellowTag\", \"\") \n",
|
157 |
+
" result = text\n",
|
158 |
+
" else:\n",
|
159 |
+
" result = \"No Vertical Tag Detected\"\n",
|
160 |
+
" response = {\"predictions\": [result]}\n",
|
161 |
+
" total_time_end = time.time()\n",
|
162 |
+
" print(\"total time : \",round((total_time_end-total_time_start),2))\n",
|
163 |
+
" return simplejson.dumps(response)\n",
|
164 |
+
"\n",
|
165 |
+
"\n",
|
166 |
+
"if __name__ == \"__main__\":\n",
|
167 |
+
" app.run(debug=True, host='0.0.0.0', port=8087)"
|
168 |
+
]
|
169 |
+
}
|
170 |
+
],
|
171 |
+
"metadata": {
|
172 |
+
"kernelspec": {
|
173 |
+
"display_name": "Python 3",
|
174 |
+
"language": "python",
|
175 |
+
"name": "python3"
|
176 |
+
},
|
177 |
+
"language_info": {
|
178 |
+
"codemirror_mode": {
|
179 |
+
"name": "ipython",
|
180 |
+
"version": 3
|
181 |
+
},
|
182 |
+
"file_extension": ".py",
|
183 |
+
"mimetype": "text/x-python",
|
184 |
+
"name": "python",
|
185 |
+
"nbconvert_exporter": "python",
|
186 |
+
"pygments_lexer": "ipython3",
|
187 |
+
"version": "3.9.7 (tags/v3.9.7:1016ef3, Aug 30 2021, 20:19:38) [MSC v.1929 64 bit (AMD64)]"
|
188 |
+
},
|
189 |
+
"vscode": {
|
190 |
+
"interpreter": {
|
191 |
+
"hash": "c58a6b68d966fd9b37abe1a881a7bc4a5fe187b07fe812e6c998975c787534e1"
|
192 |
+
}
|
193 |
+
}
|
194 |
+
},
|
195 |
+
"nbformat": 4,
|
196 |
+
"nbformat_minor": 5
|
197 |
+
}
|