graxpert 3.2.0a0.dev4__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.
Files changed (58) hide show
  1. graxpert/AstroImageRepository.py +114 -0
  2. graxpert/__init__.py +1 -0
  3. graxpert/ai_model_handling.py +215 -0
  4. graxpert/app_state.py +10 -0
  5. graxpert/application/__init__.py +0 -0
  6. graxpert/application/app.py +741 -0
  7. graxpert/application/app_events.py +96 -0
  8. graxpert/application/eventbus.py +24 -0
  9. graxpert/astroimage.py +308 -0
  10. graxpert/background_extraction.py +290 -0
  11. graxpert/background_flood_selection.py +240 -0
  12. graxpert/background_grid_selection.py +64 -0
  13. graxpert/cmdline_tools.py +547 -0
  14. graxpert/commands.py +200 -0
  15. graxpert/deconvolution.py +176 -0
  16. graxpert/denoising.py +178 -0
  17. graxpert/grid_utils.py +38 -0
  18. graxpert/img/GraXpert_LOGO_Hauptvariante.png +0 -0
  19. graxpert/img/Icon.png +0 -0
  20. graxpert/img/LDN1235_original.jpg +0 -0
  21. graxpert/img/LDN1235_processed.jpg +0 -0
  22. graxpert/img/__init__.py +0 -0
  23. graxpert/img/gfx_numbers.png +0 -0
  24. graxpert/img/gfx_numbers.svg +737 -0
  25. graxpert/img/graXpert_Startbadge_Umbriel.png +0 -0
  26. graxpert/img/hourglass.png +0 -0
  27. graxpert/locales/__init__.py +0 -0
  28. graxpert/locales/de_DE/LC_MESSAGES/base.mo +0 -0
  29. graxpert/localization.py +39 -0
  30. graxpert/main.py +441 -0
  31. graxpert/mp_logging.py +97 -0
  32. graxpert/parallel_processing.py +3 -0
  33. graxpert/preferences.py +139 -0
  34. graxpert/radialbasisinterpolation.py +215 -0
  35. graxpert/resource_utils.py +35 -0
  36. graxpert/s3_secrets.py +16 -0
  37. graxpert/skyall.py +152 -0
  38. graxpert/stretch.py +151 -0
  39. graxpert/theme/__init__.py +0 -0
  40. graxpert/theme/graxpert-dark-blue.json +367 -0
  41. graxpert/ui/__init__.py +1 -0
  42. graxpert/ui/application_frame.py +115 -0
  43. graxpert/ui/canvas.py +558 -0
  44. graxpert/ui/left_menu.py +437 -0
  45. graxpert/ui/loadingframe.py +142 -0
  46. graxpert/ui/right_menu.py +315 -0
  47. graxpert/ui/statusbar.py +102 -0
  48. graxpert/ui/styling.py +23 -0
  49. graxpert/ui/tooltip.py +194 -0
  50. graxpert/ui/ui_events.py +19 -0
  51. graxpert/ui/widgets.py +297 -0
  52. graxpert/ui_scaling.py +13 -0
  53. graxpert/version.py +2 -0
  54. graxpert-3.2.0a0.dev4.dist-info/METADATA +219 -0
  55. graxpert-3.2.0a0.dev4.dist-info/RECORD +58 -0
  56. graxpert-3.2.0a0.dev4.dist-info/WHEEL +5 -0
  57. graxpert-3.2.0a0.dev4.dist-info/entry_points.txt +2 -0
  58. graxpert-3.2.0a0.dev4.dist-info/top_level.txt +1 -0
@@ -0,0 +1,290 @@
1
+ import multiprocessing
2
+
3
+ multiprocessing.freeze_support()
4
+
5
+ import logging
6
+ from concurrent.futures import wait
7
+ from multiprocessing import shared_memory
8
+
9
+ import cv2
10
+ import numpy as np
11
+ from astropy.stats import sigma_clipped_stats
12
+ from pykrige.ok import OrdinaryKriging
13
+ from scipy import interpolate, linalg
14
+
15
+ from graxpert.ai_model_handling import get_execution_providers_ordered
16
+ from graxpert.mp_logging import get_logging_queue, worker_configurer
17
+ from graxpert.parallel_processing import executor
18
+ from graxpert.radialbasisinterpolation import RadialBasisInterpolation
19
+
20
+
21
+ def gaussian_kernel(sigma=1.0, truncate=4.0): # follow simulate skimage.filters.gaussian defaults
22
+ ksize = round(sigma * truncate) - 1 if round(sigma * truncate) % 2 == 0 else round(sigma * truncate)
23
+ return (ksize, ksize)
24
+
25
+
26
+ def extract_background(in_imarray, background_points, interpolation_type, smoothing, downscale_factor, sample_size, RBF_kernel, spline_order, corr_type, ai_path, progress=None, ai_gpu_acceleration=True):
27
+ num_colors = in_imarray.shape[-1]
28
+
29
+ shm_imarray = None
30
+ shm_background = None
31
+
32
+ if interpolation_type == "AI":
33
+ imarray = np.ndarray(in_imarray.shape, dtype=np.float32)
34
+ background = np.ndarray(in_imarray.shape, dtype=np.float32)
35
+ np.copyto(imarray, in_imarray)
36
+
37
+ # Shrink and pad to avoid artifacts on borders
38
+ padding = 8
39
+ imarray_shrink = cv2.resize(imarray, dsize=(256 - 2 * padding, 256 - 2 * padding), interpolation=cv2.INTER_LINEAR)
40
+
41
+ if len(imarray_shrink.shape) == 2:
42
+ imarray_shrink = np.expand_dims(imarray_shrink, -1)
43
+
44
+ imarray_shrink = np.pad(imarray_shrink, ((padding, padding), (padding, padding), (0, 0)), mode="edge")
45
+
46
+ median = []
47
+ mad = []
48
+
49
+ if progress is not None:
50
+ progress.update(8)
51
+
52
+ for c in range(num_colors):
53
+ median.append(np.median(imarray_shrink[:, :, c]))
54
+ mad.append(np.median(np.abs(imarray_shrink[:, :, c] - median[c])))
55
+
56
+ if progress is not None:
57
+ progress.update(8)
58
+
59
+ imarray_shrink = (imarray_shrink - median) / mad * 0.04
60
+ imarray_shrink = np.clip(imarray_shrink, -1.0, 1.0)
61
+
62
+ if progress is not None:
63
+ progress.update(8)
64
+
65
+ if num_colors == 1:
66
+ imarray_shrink = np.array([imarray_shrink[:, :, 0], imarray_shrink[:, :, 0], imarray_shrink[:, :, 0]])
67
+ imarray_shrink = np.moveaxis(imarray_shrink, 0, -1)
68
+
69
+ if progress is not None:
70
+ progress.update(8)
71
+
72
+ providers = get_execution_providers_ordered(ai_gpu_acceleration)
73
+ import onnxruntime as ort # Must be after get_execution_providers_ordered
74
+ session = ort.InferenceSession(ai_path, providers=providers)
75
+
76
+ logging.info(f"Providers : {providers}")
77
+ logging.info(f"Used providers : {session.get_providers()}")
78
+
79
+ background = session.run(None, {"gen_input_image": np.expand_dims(imarray_shrink, axis=0)})[0][0]
80
+
81
+ background = background / 0.04 * mad + median
82
+
83
+ if progress is not None:
84
+ progress.update(8)
85
+
86
+ if smoothing != 0:
87
+ sigma = smoothing * 20
88
+ background = cv2.GaussianBlur(background, ksize=gaussian_kernel(sigma), sigmaX=sigma, sigmaY=sigma)
89
+
90
+ if progress is not None:
91
+ progress.update(8)
92
+
93
+ if num_colors == 1:
94
+ background = np.array([background[:, :, 0]])
95
+ background = np.moveaxis(background, 0, -1)
96
+
97
+ if progress is not None:
98
+ progress.update(8)
99
+
100
+ # Slice to unpadded size of shrinked image, then resize to original size
101
+ if padding != 0:
102
+ background = background[padding:-padding, padding:-padding, :]
103
+
104
+ if progress is not None:
105
+ progress.update(8)
106
+
107
+ sigma = 3.0
108
+ background = cv2.GaussianBlur(background, ksize=gaussian_kernel(sigma), sigmaX=sigma, sigmaY=sigma)
109
+ background = cv2.resize(background, dsize=(in_imarray.shape[1], in_imarray.shape[0]), interpolation=cv2.INTER_LINEAR)
110
+
111
+ if len(background.shape) == 2:
112
+ background = np.expand_dims(background, -1)
113
+
114
+ if progress is not None:
115
+ progress.update(8)
116
+
117
+ else:
118
+ shm_imarray = shared_memory.SharedMemory(create=True, size=in_imarray.nbytes)
119
+ shm_background = shared_memory.SharedMemory(create=True, size=in_imarray.nbytes)
120
+ imarray = np.ndarray(in_imarray.shape, dtype=np.float32, buffer=shm_imarray.buf)
121
+ background = np.ndarray(in_imarray.shape, dtype=np.float32, buffer=shm_background.buf)
122
+ np.copyto(imarray, in_imarray)
123
+
124
+ x_sub = np.array(background_points[:, 0], dtype=int)
125
+ y_sub = np.array(background_points[:, 1], dtype=int)
126
+
127
+ if progress is not None:
128
+ progress.update(24)
129
+
130
+ futures = []
131
+ logging_queue = get_logging_queue()
132
+ for c in range(num_colors):
133
+ futures.insert(
134
+ c,
135
+ executor.submit(
136
+ interpol,
137
+ shm_imarray.name,
138
+ shm_background.name,
139
+ c,
140
+ x_sub,
141
+ y_sub,
142
+ in_imarray.shape,
143
+ interpolation_type,
144
+ smoothing,
145
+ downscale_factor,
146
+ sample_size,
147
+ RBF_kernel,
148
+ spline_order,
149
+ imarray.dtype,
150
+ logging_queue,
151
+ worker_configurer,
152
+ ),
153
+ )
154
+ wait(futures)
155
+
156
+ if progress is not None:
157
+ progress.update(48)
158
+
159
+ # Correction
160
+ if corr_type == "Subtraction":
161
+ mean = np.mean(background)
162
+ imarray[:, :, :] = imarray[:, :, :] - background[:, :, :] + mean
163
+ elif corr_type == "Division":
164
+ for c in range(num_colors):
165
+ mean = np.mean(imarray[:, :, c])
166
+ imarray[:, :, c] = imarray[:, :, c] / background[:, :, c] * mean
167
+
168
+ if progress is not None:
169
+ progress.update(8)
170
+
171
+ # clip image
172
+ imarray[:, :, :] = imarray.clip(min=0.0, max=1.0)
173
+
174
+ in_imarray[:] = imarray[:]
175
+
176
+ if progress is not None:
177
+ progress.update(8)
178
+
179
+ if shm_imarray is not None:
180
+ shm_imarray.close()
181
+ shm_imarray.unlink()
182
+ if shm_background is not None:
183
+ background = np.copy(background)
184
+ shm_background.close()
185
+ shm_background.unlink()
186
+
187
+ return background
188
+
189
+
190
+ def calc_mode_dataset(data, x_sub, y_sub, halfsize):
191
+
192
+ n = x_sub.shape[0]
193
+ data_padded = np.pad(array=data, pad_width=(halfsize,), mode="reflect")
194
+ subsample = np.zeros(n)
195
+
196
+ for i in range(n):
197
+ data_footprint = data_padded[y_sub[i] : y_sub[i] + 2 * halfsize, x_sub[i] : x_sub[i] + 2 * halfsize]
198
+ subsample[i] = sigma_clipped_stats(data=data_footprint, cenfunc="median", stdfunc="std", grow=4)[1]
199
+
200
+ return subsample
201
+
202
+
203
+ def interpol(shm_imarray_name, shm_background_name, c, x_sub, y_sub, shape, kind, smoothing, downscale_factor, sample_size, RBF_kernel, spline_order, dtype, logging_queue, logging_configurer):
204
+
205
+ logging_configurer(logging_queue)
206
+ logging.info("background_extraction.interpol started")
207
+
208
+ try:
209
+ existing_shm_imarray = shared_memory.SharedMemory(name=shm_imarray_name)
210
+ existing_shm_background = shared_memory.SharedMemory(name=shm_background_name)
211
+ imarray = np.ndarray(shape, dtype, buffer=existing_shm_imarray.buf) # [:,:,channel_idx]
212
+ imarray = imarray[:, :, c]
213
+ background = np.ndarray(shape, dtype, buffer=existing_shm_background.buf)
214
+ shape = imarray.shape
215
+
216
+ subsample = calc_mode_dataset(imarray, x_sub, y_sub, sample_size)
217
+
218
+ if downscale_factor != 1:
219
+ x_sub = x_sub / shape[1]
220
+ y_sub = y_sub / shape[0]
221
+
222
+ shape_scaled = (shape[0] // downscale_factor, shape[1] // downscale_factor)
223
+
224
+ x_sub = x_sub * shape_scaled[1]
225
+ y_sub = y_sub * shape_scaled[0]
226
+
227
+ else:
228
+ shape_scaled = shape
229
+
230
+ if kind == "RBF":
231
+ points_stacked = np.stack([x_sub, y_sub], -1)
232
+ interp = RadialBasisInterpolation(points_stacked, subsample, kernel=RBF_kernel, smooth=smoothing * linalg.norm(subsample) / np.sqrt(len(subsample)))
233
+
234
+ # Create background from interpolation
235
+ x_new = np.arange(0, shape_scaled[1], 1)
236
+ y_new = np.arange(0, shape_scaled[0], 1)
237
+
238
+ xx, yy = np.meshgrid(x_new, y_new)
239
+ points_new_stacked = np.stack([xx.ravel(), yy.ravel()], -1)
240
+
241
+ result = interp(points_new_stacked).reshape(shape_scaled)
242
+
243
+ elif kind == "Splines":
244
+ interp = interpolate.bisplrep(y_sub, x_sub, subsample, w=np.ones(len(x_sub)) / np.std(subsample), s=smoothing * len(x_sub), kx=spline_order, ky=spline_order)
245
+
246
+ # Create background from interpolation
247
+ x_new = np.arange(0, shape_scaled[1], 1)
248
+ y_new = np.arange(0, shape_scaled[0], 1)
249
+ result = interpolate.bisplev(y_new, x_new, interp)
250
+
251
+ elif kind == "Kriging":
252
+ OK = OrdinaryKriging(
253
+ x=x_sub,
254
+ y=y_sub,
255
+ z=subsample,
256
+ variogram_model="spherical",
257
+ verbose=False,
258
+ enable_plotting=False,
259
+ )
260
+
261
+ # Create background from interpolation
262
+ x_new = np.arange(0, shape_scaled[1], 1).astype("float64")
263
+ y_new = np.arange(0, shape_scaled[0], 1).astype("float64")
264
+
265
+ result = np.zeros(shape_scaled, dtype=np.float32)
266
+
267
+ num_it = shape_scaled[0] // 50
268
+
269
+ for i in range(num_it):
270
+ result_i, var = OK.execute("grid", xpoints=x_new, ypoints=y_new[i * 50 : (i + 1) * 50], backend="vectorized")
271
+ result[i * 50 : (i + 1) * 50, :] = result_i
272
+
273
+ result_i, var = OK.execute("grid", xpoints=x_new, ypoints=y_new[num_it * 50 :], backend="vectorized")
274
+ result[num_it * 50 :, :] = result_i
275
+
276
+ else:
277
+ logging.warning("Interpolation method not recognized")
278
+ return
279
+
280
+ if downscale_factor != 1:
281
+ result = cv2.resize(src=result, dsize=(shape[1], shape[0]), interpolation=cv2.INTER_LINEAR)
282
+
283
+ background[:, :, c] = result
284
+ except Exception as e:
285
+ logging.exception("Error occured during background_extraction.interpol")
286
+
287
+ existing_shm_imarray.close()
288
+ existing_shm_background.close()
289
+
290
+ logging.info("background_extraction.interpol finished")
@@ -0,0 +1,240 @@
1
+ import logging
2
+ from concurrent.futures.process import _ResultItem
3
+
4
+ import numpy as np
5
+ from scipy.spatial import KDTree
6
+ from skimage.color import rgb2gray
7
+
8
+ from graxpert.astroimage import AstroImage
9
+ from graxpert.grid_utils import find_darkest_quadrant
10
+
11
+
12
+ def idx_to_coords(idx, dist):
13
+ return int(idx[0] * dist + dist / 2), int(idx[1] * dist + dist / 2)
14
+
15
+
16
+ def candidate_visited(candidate_idx, found_row_segments):
17
+
18
+ for segment in found_row_segments:
19
+ if (
20
+ candidate_idx[1] == segment["y"]
21
+ and segment["xl"] <= candidate_idx[0]
22
+ and candidate_idx[0] <= segment["xr"]
23
+ ):
24
+ return True
25
+
26
+ return False
27
+
28
+
29
+ def candidate_valid(
30
+ candidate_idx,
31
+ width,
32
+ height,
33
+ dist,
34
+ selected_median,
35
+ tol,
36
+ mad,
37
+ data_mono_padded,
38
+ halfsize,
39
+ ):
40
+ x_pt, y_pt = idx_to_coords(candidate_idx, dist)
41
+
42
+ if x_pt < 0 or x_pt >= width or y_pt < 0 or y_pt >= height:
43
+ return False
44
+
45
+ pt, local_median = find_darkest_quadrant(x_pt, y_pt, data_mono_padded, halfsize)
46
+
47
+ if (selected_median - tol * mad / 10) <= local_median and local_median <= (selected_median + tol * mad / 10):
48
+ return True
49
+
50
+ return False
51
+
52
+
53
+ def overlap(p1, p2, sample_size):
54
+
55
+ if abs(p1[0] - p2[0]) <= sample_size * 2 and abs(p1[1] - p2[1]) <= sample_size * 2:
56
+ return True
57
+ return False
58
+
59
+
60
+ def background_flood_selection(
61
+ selected_point,
62
+ current_background_points,
63
+ tol,
64
+ bg_pts,
65
+ sample_size,
66
+ image: AstroImage,
67
+ ):
68
+ # Convert to mono
69
+ data_mono = np.copy(image.img_display)
70
+ if data_mono.shape[-1] == 3:
71
+ data_mono = rgb2gray(data_mono)
72
+
73
+ global_median = np.median(data_mono)
74
+
75
+ grid_pts = []
76
+ dist = data_mono.shape[1] / bg_pts
77
+
78
+ # Create grid
79
+ x_start = int(0.5 * dist)
80
+ y_start = int(0.5 * (data_mono.shape[0] % dist))
81
+ x = x_start
82
+ y = y_start
83
+
84
+ while y < data_mono.shape[0]:
85
+ x = x_start
86
+ while x < data_mono.shape[1]:
87
+ grid_pts.append([y, x])
88
+ x = int(x + dist)
89
+ y = int(y + dist)
90
+
91
+ # Calculate median around each grid point
92
+ local_median = np.zeros(len(grid_pts))
93
+ halfsize = sample_size
94
+ data_mono_padded = np.pad(array=data_mono, pad_width=(halfsize,), mode="reflect")
95
+
96
+ r = range(len(grid_pts))
97
+ for i in r:
98
+ x_pt = grid_pts[i][0]
99
+ y_pt = grid_pts[i][1]
100
+
101
+ pt, median = find_darkest_quadrant(x_pt, y_pt, data_mono_padded, halfsize)
102
+
103
+ grid_pts[i][0] = pt[0]
104
+ grid_pts[i][1] = pt[1]
105
+ local_median[i] = median
106
+
107
+ # Calculate median average deviation
108
+ mad = np.median(np.abs(local_median - global_median))
109
+
110
+ pt, candidate_median = find_darkest_quadrant(
111
+ int(selected_point[0]), int(selected_point[1]), data_mono_padded, sample_size
112
+ )
113
+
114
+ width = image.width
115
+ height = image.height
116
+
117
+ # distance between grid points
118
+ dist = width / bg_pts
119
+
120
+ # first candidate row index
121
+ x_candidate_idx = int(((selected_point[0] - x_start) / dist))
122
+ y_candidate_idx = int(((selected_point[1] - y_start) / dist))
123
+
124
+ # stack that contains candidate bg_point indices
125
+ candidate_idxs = [
126
+ [x_candidate_idx + 1, y_candidate_idx + 1],
127
+ [x_candidate_idx, y_candidate_idx + 1],
128
+ [x_candidate_idx + 1, y_candidate_idx],
129
+ [x_candidate_idx, y_candidate_idx],
130
+ ]
131
+
132
+ # list of valid row segments
133
+ found_row_segments = []
134
+
135
+ # step 1: compute row segments with valid bg_point indices
136
+ # valid means: bg_point is in tolerance and in image bounds
137
+ # row segments are computed by scanning each grid line to left and right, starting from the manually selected point as reference
138
+ # candidate bg_point indices are stored on a stack
139
+
140
+ while candidate_idxs:
141
+
142
+ candidate_idx = candidate_idxs.pop()
143
+
144
+ if candidate_visited(candidate_idx, found_row_segments):
145
+ logging.debug("candidate_valid")
146
+ continue
147
+
148
+ if not candidate_valid(
149
+ candidate_idx,
150
+ width,
151
+ height,
152
+ dist,
153
+ candidate_median,
154
+ tol,
155
+ mad,
156
+ data_mono_padded,
157
+ halfsize,
158
+ ):
159
+ logging.debug("candidate_valid")
160
+ continue
161
+
162
+ row_segment = {
163
+ "xl": candidate_idx[0],
164
+ "xr": candidate_idx[0],
165
+ "y": candidate_idx[1],
166
+ }
167
+
168
+ next_candidate_idx = [candidate_idx[0] - 1, candidate_idx[1]]
169
+ while candidate_valid(
170
+ next_candidate_idx,
171
+ width,
172
+ height,
173
+ dist,
174
+ candidate_median,
175
+ tol,
176
+ mad,
177
+ data_mono_padded,
178
+ halfsize,
179
+ ):
180
+ row_segment["xl"] = next_candidate_idx[0]
181
+ candidate_idxs.append([next_candidate_idx[0], next_candidate_idx[1] - 1])
182
+ candidate_idxs.append([next_candidate_idx[0], next_candidate_idx[1] + 1])
183
+ next_candidate_idx = [next_candidate_idx[0] - 1, next_candidate_idx[1]]
184
+
185
+ next_candidate_idx = [candidate_idx[0] + 1, candidate_idx[1]]
186
+ while candidate_valid(
187
+ next_candidate_idx,
188
+ width,
189
+ height,
190
+ dist,
191
+ candidate_median,
192
+ tol,
193
+ mad,
194
+ data_mono_padded,
195
+ halfsize,
196
+ ):
197
+ row_segment["xr"] = next_candidate_idx[0]
198
+ candidate_idxs.append([next_candidate_idx[0], next_candidate_idx[1] - 1])
199
+ candidate_idxs.append([next_candidate_idx[0], next_candidate_idx[1] + 1])
200
+ next_candidate_idx = [next_candidate_idx[0] + 1, next_candidate_idx[1]]
201
+
202
+ found_row_segments.append(row_segment)
203
+
204
+ # step 2: compute actual points from found row segments
205
+
206
+ found_points = [selected_point]
207
+
208
+ for segment in found_row_segments:
209
+ y_idx = segment["y"]
210
+ for x_idx in range(segment["xl"], segment["xr"] + 1):
211
+ x, y = idx_to_coords([x_idx, y_idx], dist)
212
+ pt, median = find_darkest_quadrant(x, y, data_mono_padded, sample_size)
213
+ found_points.append([pt[0], pt[1], 1])
214
+
215
+ # step 3: check for and eliminate duplicates
216
+
217
+ if current_background_points is None or len(current_background_points) == 0:
218
+ result = []
219
+ for p in found_points:
220
+ result.append(np.array(p, dtype=int))
221
+ return result
222
+
223
+ confirmed_points = []
224
+
225
+ background_tree = KDTree(current_background_points)
226
+
227
+ for f in found_points:
228
+ f_neighbors = background_tree.query_ball_point(f, dist * 2)
229
+ overlaps = False
230
+ for n_idx in f_neighbors:
231
+ if overlap(current_background_points[n_idx], f, sample_size):
232
+ overlaps = True
233
+ if not overlaps:
234
+ confirmed_points.append(f)
235
+
236
+ result = []
237
+ for p in confirmed_points:
238
+ result.append(np.array(p, dtype=int))
239
+
240
+ return result
@@ -0,0 +1,64 @@
1
+ import numpy as np
2
+ from skimage import color
3
+
4
+ import graxpert.skyall
5
+ import graxpert.stretch
6
+ from graxpert.grid_utils import find_darkest_quadrant
7
+
8
+
9
+ def background_grid_selection(data, num_pts_per_row, tol, sample_size):
10
+
11
+ # Convert to mono
12
+ data_mono = np.copy(data)
13
+ if(data_mono.shape[-1] == 3):
14
+ data_mono = color.rgb2gray(data_mono)
15
+ else:
16
+ data_mono = data_mono[:,:,0]
17
+
18
+ global_median = np.median(data_mono)
19
+
20
+ background_pts = []
21
+ dist = data_mono.shape[1] / num_pts_per_row
22
+
23
+ # Create grid
24
+ x_start = int(0.5 * dist)
25
+ y_start = int(0.5 * (data_mono.shape[0] % dist))
26
+ x = x_start
27
+ y = y_start
28
+
29
+ while(y < data_mono.shape[0]):
30
+ x = x_start
31
+ while(x < data_mono.shape[1]):
32
+ background_pts.append([x,y,1])
33
+ x = int(x + dist)
34
+ y = int(y+dist)
35
+
36
+ # Calculate median around each grid point
37
+ local_median = np.zeros(len(background_pts))
38
+ halfsize = sample_size
39
+ data_mono_padded = np.pad(array=data_mono, pad_width=(halfsize,), mode="reflect")
40
+
41
+
42
+ for i in range(len(background_pts)):
43
+ x_pt = background_pts[i][0]
44
+ y_pt = background_pts[i][1]
45
+
46
+ pt, median = find_darkest_quadrant(x_pt, y_pt, data_mono_padded, halfsize)
47
+
48
+ background_pts[i][0] = pt[0]
49
+ background_pts[i][1] = pt[1]
50
+ local_median[i] = median
51
+
52
+ # Calculate median average deviation and remove points not within tolerance
53
+ mad = np.median(np.abs(local_median - global_median))
54
+
55
+ background_pts_sliced = []
56
+ for i in range(len(background_pts)):
57
+ if(local_median[i] < global_median + tol*mad):
58
+ background_pts_sliced.append(background_pts[i])
59
+
60
+ result = []
61
+ for p in background_pts_sliced:
62
+ result.append(np.array(p, dtype=int))
63
+
64
+ return result