nettracer3d 0.9.9__py3-none-any.whl → 1.1.5__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- nettracer3d/community_extractor.py +24 -8
- nettracer3d/morphology.py +113 -17
- nettracer3d/neighborhoods.py +201 -66
- nettracer3d/nettracer.py +516 -103
- nettracer3d/nettracer_gui.py +2072 -592
- nettracer3d/network_draw.py +9 -3
- nettracer3d/node_draw.py +41 -58
- nettracer3d/segmenter.py +67 -25
- nettracer3d/segmenter_GPU.py +67 -29
- nettracer3d/stats.py +861 -0
- {nettracer3d-0.9.9.dist-info → nettracer3d-1.1.5.dist-info}/METADATA +3 -4
- nettracer3d-1.1.5.dist-info/RECORD +26 -0
- nettracer3d-0.9.9.dist-info/RECORD +0 -25
- {nettracer3d-0.9.9.dist-info → nettracer3d-1.1.5.dist-info}/WHEEL +0 -0
- {nettracer3d-0.9.9.dist-info → nettracer3d-1.1.5.dist-info}/entry_points.txt +0 -0
- {nettracer3d-0.9.9.dist-info → nettracer3d-1.1.5.dist-info}/licenses/LICENSE +0 -0
- {nettracer3d-0.9.9.dist-info → nettracer3d-1.1.5.dist-info}/top_level.txt +0 -0
nettracer3d/network_draw.py
CHANGED
|
@@ -77,7 +77,12 @@ def draw_line_inplace(start, end, array):
|
|
|
77
77
|
"""
|
|
78
78
|
|
|
79
79
|
# Calculate the distances between start and end coordinates
|
|
80
|
-
|
|
80
|
+
try:
|
|
81
|
+
distances = end - start
|
|
82
|
+
except:
|
|
83
|
+
end = np.array(end)
|
|
84
|
+
start = np.array(start)
|
|
85
|
+
distances = end - start
|
|
81
86
|
|
|
82
87
|
# Determine the number of steps along the line
|
|
83
88
|
num_steps = int(max(np.abs(distances)) + 1)
|
|
@@ -242,7 +247,8 @@ def draw_network_from_centroids(nodes, network, centroids, twod_bool, directory
|
|
|
242
247
|
centroid_dic[item] = centroid
|
|
243
248
|
|
|
244
249
|
except KeyError:
|
|
245
|
-
|
|
250
|
+
pass
|
|
251
|
+
#print(f"Centroid {item} missing")
|
|
246
252
|
output_stack = np.zeros(np.shape(nodes), dtype=np.uint8)
|
|
247
253
|
|
|
248
254
|
for i, pair1_val in enumerate(pair1):
|
|
@@ -253,7 +259,7 @@ def draw_network_from_centroids(nodes, network, centroids, twod_bool, directory
|
|
|
253
259
|
pair2_centroid = centroid_dic[pair2_val]
|
|
254
260
|
draw_line_inplace(pair1_centroid, pair2_centroid, output_stack)
|
|
255
261
|
except KeyError:
|
|
256
|
-
print(f"Missing centroid {i}")
|
|
262
|
+
#print(f"Missing centroid {i}")
|
|
257
263
|
pass
|
|
258
264
|
|
|
259
265
|
if twod_bool:
|
nettracer3d/node_draw.py
CHANGED
|
@@ -3,7 +3,7 @@ import tifffile
|
|
|
3
3
|
from scipy import ndimage
|
|
4
4
|
from PIL import Image, ImageDraw, ImageFont
|
|
5
5
|
from scipy.ndimage import zoom
|
|
6
|
-
|
|
6
|
+
import cv2
|
|
7
7
|
|
|
8
8
|
def downsample(data, factor, directory=None, order=0):
|
|
9
9
|
"""
|
|
@@ -121,83 +121,66 @@ def draw_nodes(nodes, num_nodes):
|
|
|
121
121
|
# Save the draw_array as a 3D TIFF file
|
|
122
122
|
tifffile.imwrite("labelled_nodes.tif", draw_array)
|
|
123
123
|
|
|
124
|
-
def draw_from_centroids(nodes, num_nodes, centroids, twod_bool, directory
|
|
125
|
-
"""
|
|
126
|
-
print("Drawing node IDs
|
|
127
|
-
# Create a new 3D array to draw on with the same dimensions as the original array
|
|
124
|
+
def draw_from_centroids(nodes, num_nodes, centroids, twod_bool, directory=None):
|
|
125
|
+
"""Optimized version using OpenCV"""
|
|
126
|
+
print("Drawing node IDs...")
|
|
128
127
|
draw_array = np.zeros_like(nodes, dtype=np.uint8)
|
|
129
|
-
|
|
130
|
-
#
|
|
131
|
-
|
|
132
|
-
# Iterate through each centroid
|
|
128
|
+
|
|
129
|
+
# Draw text using OpenCV (no PIL conversions needed)
|
|
133
130
|
for idx in centroids.keys():
|
|
134
131
|
centroid = centroids[idx]
|
|
135
|
-
z, y, x = centroid.astype(int)
|
|
136
|
-
|
|
137
|
-
try:
|
|
138
|
-
draw_array = _draw_at_plane(z, y, x, draw_array, idx)
|
|
139
|
-
except IndexError:
|
|
140
|
-
pass
|
|
141
132
|
|
|
142
133
|
try:
|
|
143
|
-
|
|
144
|
-
except
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
134
|
+
z, y, x = centroid.astype(int)
|
|
135
|
+
except:
|
|
136
|
+
z, y, x = centroid
|
|
137
|
+
|
|
138
|
+
for z_offset in [0, 1, -1]:
|
|
139
|
+
z_target = z + z_offset
|
|
140
|
+
if 0 <= z_target < draw_array.shape[0]:
|
|
141
|
+
cv2.putText(draw_array[z_target], str(idx), (x, y),
|
|
142
|
+
cv2.FONT_HERSHEY_SIMPLEX, 0.4, 255, 1, cv2.LINE_AA)
|
|
143
|
+
|
|
152
144
|
if twod_bool:
|
|
153
145
|
draw_array = draw_array[0,:,:] | draw_array[1,:,:]
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
if directory is None:
|
|
157
|
-
filename = 'labelled_node_indices.tif'
|
|
158
|
-
else:
|
|
159
|
-
filename = f'{directory}/labelled_node_indices.tif'
|
|
160
|
-
|
|
146
|
+
|
|
147
|
+
filename = f'{directory}/labelled_node_indices.tif' if directory else 'labelled_node_indices.tif'
|
|
161
148
|
try:
|
|
162
|
-
|
|
163
|
-
# Save the draw_array as a 3D TIFF file
|
|
164
149
|
tifffile.imwrite(filename, draw_array)
|
|
165
|
-
|
|
166
150
|
except Exception as e:
|
|
167
151
|
print(f"Could not save node indices to {filename}")
|
|
168
|
-
|
|
152
|
+
|
|
169
153
|
return draw_array
|
|
170
154
|
|
|
171
155
|
def degree_draw(degree_dict, centroid_dict, nodes):
|
|
156
|
+
"""Draw node degrees at centroid locations using OpenCV"""
|
|
172
157
|
# Create a new 3D array to draw on with the same dimensions as the original array
|
|
173
158
|
draw_array = np.zeros_like(nodes, dtype=np.uint8)
|
|
174
|
-
|
|
175
|
-
|
|
159
|
+
|
|
176
160
|
for node in centroid_dict:
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
degree = degree_dict[node]
|
|
180
|
-
except:
|
|
161
|
+
# Skip if node not in degree_dict
|
|
162
|
+
if node not in degree_dict:
|
|
181
163
|
continue
|
|
182
164
|
|
|
165
|
+
degree = degree_dict[node]
|
|
183
166
|
z, y, x = centroid_dict[node].astype(int)
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
167
|
+
|
|
168
|
+
# Draw on current z-plane and adjacent planes
|
|
169
|
+
for z_offset in [0, 1, -1]:
|
|
170
|
+
z_target = z + z_offset
|
|
171
|
+
# Check bounds
|
|
172
|
+
if 0 <= z_target < draw_array.shape[0]:
|
|
173
|
+
cv2.putText(
|
|
174
|
+
draw_array[z_target], # Image to draw on
|
|
175
|
+
str(degree), # Text to draw
|
|
176
|
+
(x, y), # Position (x, y)
|
|
177
|
+
cv2.FONT_HERSHEY_SIMPLEX, # Font
|
|
178
|
+
0.4, # Font scale
|
|
179
|
+
255, # Color (white)
|
|
180
|
+
1, # Thickness
|
|
181
|
+
cv2.LINE_AA # Anti-aliasing
|
|
182
|
+
)
|
|
183
|
+
|
|
201
184
|
return draw_array
|
|
202
185
|
|
|
203
186
|
def degree_infect(degree_dict, nodes, make_floats = False):
|
nettracer3d/segmenter.py
CHANGED
|
@@ -1181,12 +1181,22 @@ class InteractiveSegmenter:
|
|
|
1181
1181
|
(x[0][2] - curr_x) ** 2))
|
|
1182
1182
|
return nearest[0]
|
|
1183
1183
|
else:
|
|
1184
|
-
# 3D chunks:
|
|
1185
|
-
nearest
|
|
1184
|
+
# 3D chunks: find chunks on nearest Z-plane, then closest in X/Y
|
|
1185
|
+
# First find the nearest Z-plane among available chunks
|
|
1186
|
+
nearest_z = min(unprocessed_chunks,
|
|
1187
|
+
key=lambda x: abs(x[1]['center'][0] - curr_z))[1]['center'][0]
|
|
1188
|
+
|
|
1189
|
+
# Get all chunks on that nearest Z-plane
|
|
1190
|
+
nearest_z_chunks = [chunk for chunk in unprocessed_chunks
|
|
1191
|
+
if chunk[1]['center'][0] == nearest_z]
|
|
1192
|
+
|
|
1193
|
+
# From those chunks, find closest in X/Y
|
|
1194
|
+
nearest = min(nearest_z_chunks,
|
|
1186
1195
|
key=lambda x: sum((a - b) ** 2 for a, b in
|
|
1187
|
-
zip(x[1]['center'], (
|
|
1196
|
+
zip(x[1]['center'][1:], (curr_y, curr_x))))
|
|
1197
|
+
|
|
1188
1198
|
return nearest[0]
|
|
1189
|
-
|
|
1199
|
+
|
|
1190
1200
|
return None
|
|
1191
1201
|
|
|
1192
1202
|
while True:
|
|
@@ -1284,12 +1294,14 @@ class InteractiveSegmenter:
|
|
|
1284
1294
|
|
|
1285
1295
|
return foreground_features, background_features
|
|
1286
1296
|
|
|
1287
|
-
def compute_3d_chunks(self, chunk_size=None):
|
|
1297
|
+
def compute_3d_chunks(self, chunk_size=None, thickness=49):
|
|
1288
1298
|
"""
|
|
1289
|
-
Compute 3D chunks with consistent logic across all operations.
|
|
1299
|
+
Compute 3D chunks as rectangular prisms with consistent logic across all operations.
|
|
1300
|
+
Creates chunks that are thin in Z and square-like in X/Y dimensions.
|
|
1290
1301
|
|
|
1291
1302
|
Args:
|
|
1292
|
-
chunk_size: Optional chunk size, otherwise uses dynamic calculation
|
|
1303
|
+
chunk_size: Optional chunk size for volume calculation, otherwise uses dynamic calculation
|
|
1304
|
+
thickness: Z-dimension thickness of chunks (default: 9)
|
|
1293
1305
|
|
|
1294
1306
|
Returns:
|
|
1295
1307
|
list: List of chunk coordinates [z_start, z_end, y_start, y_end, x_start, x_end]
|
|
@@ -1313,27 +1325,57 @@ class InteractiveSegmenter:
|
|
|
1313
1325
|
except:
|
|
1314
1326
|
depth, height, width, rgb = self.image_3d.shape
|
|
1315
1327
|
|
|
1316
|
-
# Calculate chunk
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1328
|
+
# Calculate target volume per chunk (same as original cube)
|
|
1329
|
+
target_volume = chunk_size ** 3
|
|
1330
|
+
|
|
1331
|
+
# Calculate XY side length based on thickness and target volume
|
|
1332
|
+
# Volume = thickness * xy_side * xy_side
|
|
1333
|
+
# So xy_side = sqrt(volume / thickness)
|
|
1334
|
+
xy_side = int(np.sqrt(target_volume / thickness))
|
|
1335
|
+
xy_side = max(1, xy_side) # Ensure minimum size of 1
|
|
1320
1336
|
|
|
1321
|
-
#
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
np.arange(y_chunks) * chunk_size,
|
|
1325
|
-
np.arange(x_chunks) * chunk_size,
|
|
1326
|
-
indexing='ij'
|
|
1327
|
-
)).reshape(3, -1).T
|
|
1337
|
+
# Calculate actual chunk dimensions for grid calculation
|
|
1338
|
+
z_chunk_size = thickness
|
|
1339
|
+
xy_chunk_size = xy_side
|
|
1328
1340
|
|
|
1329
|
-
#
|
|
1341
|
+
# Calculate number of chunks in each dimension
|
|
1342
|
+
z_chunks = (depth + z_chunk_size - 1) // z_chunk_size
|
|
1343
|
+
y_chunks = (height + xy_chunk_size - 1) // xy_chunk_size
|
|
1344
|
+
x_chunks = (width + xy_chunk_size - 1) // xy_chunk_size
|
|
1345
|
+
|
|
1346
|
+
# Calculate actual chunk sizes to distribute remainder evenly
|
|
1347
|
+
# This ensures all chunks are roughly the same size
|
|
1348
|
+
z_sizes = np.full(z_chunks, depth // z_chunks)
|
|
1349
|
+
z_remainder = depth % z_chunks
|
|
1350
|
+
z_sizes[:z_remainder] += 1
|
|
1351
|
+
|
|
1352
|
+
y_sizes = np.full(y_chunks, height // y_chunks)
|
|
1353
|
+
y_remainder = height % y_chunks
|
|
1354
|
+
y_sizes[:y_remainder] += 1
|
|
1355
|
+
|
|
1356
|
+
x_sizes = np.full(x_chunks, width // x_chunks)
|
|
1357
|
+
x_remainder = width % x_chunks
|
|
1358
|
+
x_sizes[:x_remainder] += 1
|
|
1359
|
+
|
|
1360
|
+
# Calculate cumulative positions
|
|
1361
|
+
z_positions = np.concatenate([[0], np.cumsum(z_sizes)])
|
|
1362
|
+
y_positions = np.concatenate([[0], np.cumsum(y_sizes)])
|
|
1363
|
+
x_positions = np.concatenate([[0], np.cumsum(x_sizes)])
|
|
1364
|
+
|
|
1365
|
+
# Generate all chunk coordinates
|
|
1330
1366
|
chunks = []
|
|
1331
|
-
for
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1367
|
+
for z_idx in range(z_chunks):
|
|
1368
|
+
for y_idx in range(y_chunks):
|
|
1369
|
+
for x_idx in range(x_chunks):
|
|
1370
|
+
z_start = z_positions[z_idx]
|
|
1371
|
+
z_end = z_positions[z_idx + 1]
|
|
1372
|
+
y_start = y_positions[y_idx]
|
|
1373
|
+
y_end = y_positions[y_idx + 1]
|
|
1374
|
+
x_start = x_positions[x_idx]
|
|
1375
|
+
x_end = x_positions[x_idx + 1]
|
|
1376
|
+
|
|
1377
|
+
coords = [z_start, z_end, y_start, y_end, x_start, x_end]
|
|
1378
|
+
chunks.append(coords)
|
|
1337
1379
|
|
|
1338
1380
|
return chunks
|
|
1339
1381
|
|
nettracer3d/segmenter_GPU.py
CHANGED
|
@@ -1055,19 +1055,18 @@ class InteractiveSegmenter:
|
|
|
1055
1055
|
self.realtimechunks = chunk_dict
|
|
1056
1056
|
print("Ready!")
|
|
1057
1057
|
|
|
1058
|
-
def compute_3d_chunks(self, chunk_size=None):
|
|
1058
|
+
def compute_3d_chunks(self, chunk_size=None, thickness=49):
|
|
1059
1059
|
"""
|
|
1060
|
-
Compute 3D chunks with consistent logic across all operations
|
|
1060
|
+
Compute 3D chunks as rectangular prisms with consistent logic across all operations.
|
|
1061
|
+
Creates chunks that are thin in Z and square-like in X/Y dimensions.
|
|
1061
1062
|
|
|
1062
1063
|
Args:
|
|
1063
|
-
chunk_size: Optional chunk size, otherwise uses dynamic calculation
|
|
1064
|
+
chunk_size: Optional chunk size for volume calculation, otherwise uses dynamic calculation
|
|
1065
|
+
thickness: Z-dimension thickness of chunks (default: 9)
|
|
1064
1066
|
|
|
1065
1067
|
Returns:
|
|
1066
1068
|
list: List of chunk coordinates [z_start, z_end, y_start, y_end, x_start, x_end]
|
|
1067
1069
|
"""
|
|
1068
|
-
import cupy as cp
|
|
1069
|
-
import multiprocessing
|
|
1070
|
-
|
|
1071
1070
|
# Use consistent chunk size calculation
|
|
1072
1071
|
if chunk_size is None:
|
|
1073
1072
|
if hasattr(self, 'master_chunk') and self.master_chunk is not None:
|
|
@@ -1075,10 +1074,10 @@ class InteractiveSegmenter:
|
|
|
1075
1074
|
else:
|
|
1076
1075
|
# Dynamic calculation (same as segmentation)
|
|
1077
1076
|
total_cores = multiprocessing.cpu_count()
|
|
1078
|
-
total_volume =
|
|
1077
|
+
total_volume = np.prod(self.image_3d.shape)
|
|
1079
1078
|
target_volume_per_chunk = total_volume / (total_cores * 4)
|
|
1080
1079
|
|
|
1081
|
-
chunk_size = int(
|
|
1080
|
+
chunk_size = int(np.cbrt(target_volume_per_chunk))
|
|
1082
1081
|
chunk_size = max(16, min(chunk_size, min(self.image_3d.shape) // 2))
|
|
1083
1082
|
chunk_size = ((chunk_size + 7) // 16) * 16
|
|
1084
1083
|
|
|
@@ -1087,28 +1086,57 @@ class InteractiveSegmenter:
|
|
|
1087
1086
|
except:
|
|
1088
1087
|
depth, height, width, rgb = self.image_3d.shape
|
|
1089
1088
|
|
|
1090
|
-
# Calculate chunk
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1089
|
+
# Calculate target volume per chunk (same as original cube)
|
|
1090
|
+
target_volume = chunk_size ** 3
|
|
1091
|
+
|
|
1092
|
+
# Calculate XY side length based on thickness and target volume
|
|
1093
|
+
# Volume = thickness * xy_side * xy_side
|
|
1094
|
+
# So xy_side = sqrt(volume / thickness)
|
|
1095
|
+
xy_side = int(np.sqrt(target_volume / thickness))
|
|
1096
|
+
xy_side = max(1, xy_side) # Ensure minimum size of 1
|
|
1097
|
+
|
|
1098
|
+
# Calculate actual chunk dimensions for grid calculation
|
|
1099
|
+
z_chunk_size = thickness
|
|
1100
|
+
xy_chunk_size = xy_side
|
|
1101
|
+
|
|
1102
|
+
# Calculate number of chunks in each dimension
|
|
1103
|
+
z_chunks = (depth + z_chunk_size - 1) // z_chunk_size
|
|
1104
|
+
y_chunks = (height + xy_chunk_size - 1) // xy_chunk_size
|
|
1105
|
+
x_chunks = (width + xy_chunk_size - 1) // xy_chunk_size
|
|
1106
|
+
|
|
1107
|
+
# Calculate actual chunk sizes to distribute remainder evenly
|
|
1108
|
+
# This ensures all chunks are roughly the same size
|
|
1109
|
+
z_sizes = np.full(z_chunks, depth // z_chunks)
|
|
1110
|
+
z_remainder = depth % z_chunks
|
|
1111
|
+
z_sizes[:z_remainder] += 1
|
|
1094
1112
|
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
cp.arange(y_chunks) * chunk_size,
|
|
1099
|
-
cp.arange(x_chunks) * chunk_size,
|
|
1100
|
-
indexing='ij'
|
|
1101
|
-
)).reshape(3, -1).T
|
|
1113
|
+
y_sizes = np.full(y_chunks, height // y_chunks)
|
|
1114
|
+
y_remainder = height % y_chunks
|
|
1115
|
+
y_sizes[:y_remainder] += 1
|
|
1102
1116
|
|
|
1117
|
+
x_sizes = np.full(x_chunks, width // x_chunks)
|
|
1118
|
+
x_remainder = width % x_chunks
|
|
1119
|
+
x_sizes[:x_remainder] += 1
|
|
1103
1120
|
|
|
1104
|
-
#
|
|
1121
|
+
# Calculate cumulative positions
|
|
1122
|
+
z_positions = np.concatenate([[0], np.cumsum(z_sizes)])
|
|
1123
|
+
y_positions = np.concatenate([[0], np.cumsum(y_sizes)])
|
|
1124
|
+
x_positions = np.concatenate([[0], np.cumsum(x_sizes)])
|
|
1125
|
+
|
|
1126
|
+
# Generate all chunk coordinates
|
|
1105
1127
|
chunks = []
|
|
1106
|
-
for
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1128
|
+
for z_idx in range(z_chunks):
|
|
1129
|
+
for y_idx in range(y_chunks):
|
|
1130
|
+
for x_idx in range(x_chunks):
|
|
1131
|
+
z_start = z_positions[z_idx]
|
|
1132
|
+
z_end = z_positions[z_idx + 1]
|
|
1133
|
+
y_start = y_positions[y_idx]
|
|
1134
|
+
y_end = y_positions[y_idx + 1]
|
|
1135
|
+
x_start = x_positions[x_idx]
|
|
1136
|
+
x_end = x_positions[x_idx + 1]
|
|
1137
|
+
|
|
1138
|
+
coords = [z_start, z_end, y_start, y_end, x_start, x_end]
|
|
1139
|
+
chunks.append(coords)
|
|
1112
1140
|
|
|
1113
1141
|
return chunks
|
|
1114
1142
|
|
|
@@ -1196,10 +1224,20 @@ class InteractiveSegmenter:
|
|
|
1196
1224
|
(x[0][2] - curr_x) ** 2))
|
|
1197
1225
|
return nearest[0]
|
|
1198
1226
|
else:
|
|
1199
|
-
# 3D chunks:
|
|
1200
|
-
nearest
|
|
1227
|
+
# 3D chunks: find chunks on nearest Z-plane, then closest in X/Y
|
|
1228
|
+
# First find the nearest Z-plane among available chunks
|
|
1229
|
+
nearest_z = min(unprocessed_chunks,
|
|
1230
|
+
key=lambda x: abs(x[1]['center'][0] - curr_z))[1]['center'][0]
|
|
1231
|
+
|
|
1232
|
+
# Get all chunks on that nearest Z-plane
|
|
1233
|
+
nearest_z_chunks = [chunk for chunk in unprocessed_chunks
|
|
1234
|
+
if chunk[1]['center'][0] == nearest_z]
|
|
1235
|
+
|
|
1236
|
+
# From those chunks, find closest in X/Y
|
|
1237
|
+
nearest = min(nearest_z_chunks,
|
|
1201
1238
|
key=lambda x: sum((a - b) ** 2 for a, b in
|
|
1202
|
-
zip(x[1]['center'], (
|
|
1239
|
+
zip(x[1]['center'][1:], (curr_y, curr_x))))
|
|
1240
|
+
|
|
1203
1241
|
return nearest[0]
|
|
1204
1242
|
|
|
1205
1243
|
return None
|