-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathutils.py
More file actions
608 lines (495 loc) · 19.4 KB
/
Copy pathutils.py
File metadata and controls
608 lines (495 loc) · 19.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
import time
from io import StringIO
import numpy as np
from PIL import Image, ImageColor
import open3d as o3d
from tkinter import colorchooser
import tkinter as tk
import tkinter.ttk as ttk
from tkinter import StringVar
from tkinter import filedialog as fd
def getShapeOfDataset(inputDataset):
"""When given a dataset filename of either a .tif, .json, or .txt dataset, returns the shape as a tuple
Parameters
----------
inputDataset : str
The filepath of the dataset you want to get the shape of
Returns
-------
tuple
A tuple of ints representing the shape of the dataset
Raises
------
Exception
If the filename passed does not end in '.tif', '.tiff', '.json', or '.txt'
"""
if inputDataset[-4:] == '.tif' or inputDataset[-5:] == '.tiff':
width, height = img.size
count = 0
while True:
try:
img.seek(count)
except EOFError:
break
count += 1
return count, height, width
elif inputDataset[-5:] == '.json':
with open(inputDataset, 'r') as inFile:
d = json.load(inFile)
return d['depth'], d['height'], d['width']
elif inputDataset[-4:] == '.txt':
filteredImages = []
with open(inputDataset, 'r') as inFile:
images = inFile.readlines()
for image in images:
if len(image.strip()) > 3:
filteredImages.append(image)
img = Image.open(filteredImages[0].strip())
width, height = img.size
count = len(filteredImages)
return count, height, width
else:
raise Exception('Unknown Dataset filetype ' + inputDataset[inputDataset.rindex('.'):] + ' Passed to getShapeOfDataset')
def getNumpyFromDataset(inputDataset):
"""When given a dataset filename of either a .tif, .json, or .txt dataset, returns the dataset as a numpy array
Parameters
----------
inputDataset : str
The filepath of the dataset you want to get as a numpy array
Returns
-------
numpy.ndarray
A numpy array representation of the dataset
Raises
------
Exception
If the filename passed does not end in '.tif', '.tiff', '.json', or '.txt'
"""
shape = getShapeOfDataset(inputDataset)
imList = []
if inputDataset[-4:] == '.tif' or inputDataset[-5:] == '.tiff':
img = Image.open(inputDataset)
for i in range(shape[0]):
img.seek(i)
imList.append(np.asarray(img))
elif inputDataset[-5:] == '.json':
with open(inputDataset, 'r') as inFile:
d = json.load(inFile)
for file in d['image']:
img = Image.open(file)
imList.append(np.asarray(img))
elif inputDataset[-4:] == '.txt':
filteredImages = []
with open(inputDataset, 'r') as inFile:
images = inFile.readlines()
for image in images:
if len(image.strip()) > 3:
img = Image.open(image.strip())
imList.append(np.asarray(img))
else:
raise Exception('Unknown Dataset filetype ' + inputDataset[inputDataset.rindex('.'):] + ' Passed to getNumpyFromDataset')
imList = np.array(imList)
return imList
def getPointCloudImageSliceFromDataset(dataset, axis, index):
"""Returns a two dimensional slice of a dataset, represented as a point cloud
Parameters
----------
dataset : str
The filepath of the dataset you want to get a slice of
axis : {'x', 'y', 'z'}
The axis along with you want to get a slice
index : int
How far along the axis to get the slice from. Can range from 0 to the end of the axis
Returns
-------
o3d.geometry.PointCloud
A point cloud that when rendered looks like a 2D image from the dataset. Can be rendered in 3D space
Raises
------
Exception
If `axis` is not equal to 'x', 'y', or 'z'
"""
dataset = getNumpyFromDataset(dataset)
if axis == 'x':
slice_ = dataset[index,:,:]
elif axis == 'y':
slice_ = dataset[:,index,:]
elif axis == 'z':
slice_ = dataset[:,:,index]
else:
raise Exception('Unknown axis passed to getPointCloudImageSliceFromDataset. Options are "x", "y", or "z"')
im = Image.fromarray(slice_).convert('RGB')
im = np.asarray(im)
im = im[::1,::1]
points = []
colors = []
pcd = o3d.geometry.PointCloud()
for i in range(im.shape[0] ):
for j in range(im.shape[1]):
color = np.array(im[i,j]) / 255
point = [0, i, j]
points.append(point)
colors.append(color)
points = o3d.utility.Vector3dVector(points)
colors = o3d.utility.Vector3dVector(colors)
pcd.points = points
pcd.colors = colors
if axis == 'x':
pcd.translate((index, 0, 0))
elif axis == 'y':
s1 = dataset.shape[0]
R = pcd.get_rotation_matrix_from_xyz((0, 0, -np.pi/2))
pcd.rotate(R)
pcd.translate((s1/2, -s1/2 + index, 0))
elif axis == 'z':
s0 = dataset.shape[0]
s1 = dataset.shape[1]
s2 = dataset.shape[2]
print(dataset.shape)
R = pcd.get_rotation_matrix_from_xyz((-np.pi/2, 0, -np.pi/2))
pcd.rotate(R)
pcd.translate((s0/2, s1/2 - s0/2, index - s1/2))
return pcd
def complimentColor(hexValue=None, rgbTuple=None):
"""When given a color, returns either the white or black hex code, whichever would show up better as text over the passed color
This function is supposed to be used to pick which text color to use over a given color.
You pass in the background color as either a hex string or rgbTuple.
The function returns the hex color for either white or black
This return value is the text color you should use over the background color to have best visibility.
Parameters
----------
hexValue : str
Hex representation of a color, ex white is #FFFFFF
rgbTuple : tuple of ints
Red, green, blue representation of a color, ex white is (255, 255, 255)
Returns
-------
str
A hex representation of the text color you should use
"""
if hexValue and rgbTuple:
raise Exception("Must provide either hexValue or rgbTuple, not both")
if not hexValue and not rgbTuple:
raise Exception("Must provide either hexValue or rgbTuple")
if rgbTuple:
r, g, b = rgbTuple
if hexValue:
r, g, b = ImageColor.getcolor(hexValue, "RGB")
if (r * 0.299 + g * 0.587 + b * 0.114) > 186:
return "#000000"
else:
return "#FFFFFF"
def whereToArray(where):
"""Turns the results of np.where(), back into an array
np.where(array=value) will return a tuple of lists representing coordinates where the array = that value
This function takes in this result `where`, and turns it back into a binary array
This returned array is 1 for the points listed in `where`, and 0 everywhere else
Parameters
----------
where : tuple of lists
A tuple containing lists. For example, a 3D array would return 3 lists when numpy.where() is used
The first list would be x coordinates, the second y, and the third z
Returns
-------
numpy.ndarray
A numpy array that is equal to 1 at the points from the np.where list, and 0 elsewhere
"""
maxs = []
numDimensions = len(where)
print('numDimenstions',numDimensions)
for i in range(numDimensions):
maxs.append(np.max(where[i]) + 1)
print(len(where[i]))
toReturn = np.zeros(maxs, dtype=int)
toReturn[where] = 1
return toReturn
def cloudToSemanticArray(where):
"""Very similar to whereToArray.
The only difference is this is recieving a open3d.geometry.PointCloud.points as input, which is different slightly from the output of np.where
it retuns
Parameters
----------
where : open3d.geometry.PointCloud.points
A list of points from an open3d PointCloud object
Returns
-------
numpy.ndarray
A numpy array that is equal to 1 where there are points in the PointCloud, and 0 elsewhere
"""
where = np.array(where, dtype=int)
where = np.transpose(where)
whereTup = []
maxs = []
numDimensions = where.shape[0]
for i in range(numDimensions):
maxs.append(np.max(where[i,:]) + 1)
whereTup.append(where[i])
whereTup = tuple(whereTup)
toReturn = np.zeros(maxs, dtype=int)
toReturn[whereTup] = 1
return toReturn
def rgb2hex(colorTuple):
"""Converts an RGB tuple to a hex string
Parameters
----------
colorTuple : tuple of ints
Red, green, blue representation of a color, ex white is (255, 255, 255)
Returns
-------
str
A hex representation of `colorTuple`
"""
r, g, b = colorTuple
return "#{:02x}{:02x}{:02x}".format(r,g,b)
def MessageBox(message, title=None):
"""Makes a message box pop up in tkinter
Parameters
----------
message : str
The message to be displayed in the message box
title : str, optional
The title of the message box. Can be ommited
"""
print(message)
tk.messagebox.showinfo(title=title, message=message)
class TimeCounter:
"""A class that is used to calculate time remaining for long tasks with known number of incremental steps
This class is used to calculate time left in long tasks and print them out to the user.
Time can be displayed in hours, minutes, or seconds.
To use the TimeCounter class, make an instance of the class. To do this, you need to know how many steps the process you are tracking will be
Each step of the calculation, call the tick function of the instance.
Anytime you want to display the estimated time remaining, call the print function of the
Example, if you are doing 10 long calculations and it takes a while:
counter = TimeCounter(10) #Ten because there are ten steps or calculations
for i in range(10):
long_calculation() #Placeholder, any function or block of code would work
counter.tick() #Updates the TimeCounter, so it knows how far along the total task you are
counter.print() #Prints out the remaining time left, as calculated by the TimeCounter
This will print out the estimated time left after calculating each number
"""
def __init__(self, numSteps, timeUnits = 'hours', prefix=''):
"""Initiallizes a TimeCounter instance
Parameters
----------
numSteps : int
The number of steps that your long calculation / process has, after each step you must call tick()
timeUnits : {'hours', 'minutes', 'seconds'}
Sets the time unit that you want to use when displaying progress through the print function
prefix : str, optional
If you use this parameter, when printing this prefix will be included before the rest of the output.
Could use this to provide more detail to the output, or change prefix midway to show in more detail what step the process is on.
"""
self.numSteps = numSteps
self.startTime = time.time()
self.index = 0
self.timeUnits = timeUnits
if timeUnits == 'hours':
self.scaleFactor = 3600
if timeUnits == 'minutes':
self.scaleFactor = 60
if timeUnits == 'seconds':
self.scaleFactor = 1
self.prefix=prefix
self.remainingTime = None
def tick(self):
"""Must be called every time a step is made in the long calculation
For example, if your long calculation has 10 long steps, and you want to use them to calculate time left:
Initiallize a TimeCounter with 10 steps, then after each step make sure to call tick()
You can then use print to print out progress whenever you want.
"""
self.index += 1
currentTime = time.time()
fractionComplete = self.index / self.numSteps
timeSofar = currentTime - self.startTime
totalTime = 1/fractionComplete * timeSofar
self.remainingTime = totalTime - timeSofar
def print(self):
"""Prints the current progress of the TimeCounter
"""
print(self.__str__())
def __str__ (self):
"""Returns the string value that would be printed by print()
"""
if self.remainingTime:
return self.prefix + ' ' + "{:.2f}".format(self.remainingTime / self.scaleFactor) + ' ' + self.timeUnits + ' left'
else:
return "Cannot calculate time left, either just started or close to end"
class LayerVisualizerRow(ttk.Frame):
def __init__(self, master, color, index, changeCallback=False, **kw):
ttk.Frame.__init__(self, master, **kw)
self.fileChooser = FileChooser(self, changeCallback = changeCallback)
self.fileChooser.grid(column='0', row='0')
self.colorButton = tk.Button(self)
self.colorButton.configure(cursor='arrow', text='Choose Color')
self.colorButton.grid(column='1', row='0')
self.colorButton.configure(command=self.ChooseColor, bg=color, fg=complimentColor(hexValue=color))
self.color = color
self.master = master
self.index = index
def ChooseColor(self):
colorTuple = colorchooser.askcolor(title ="Choose Color For Layer " + str(self.index))[0]
self.color = rgb2hex(colorTuple)
self.colorButton.configure(bg=self.color, fg = complimentColor(hexValue=self.color))
def GetColor(self):
return self.color
def GetFile(self):
return self.fileChooser.getFilepath().strip()
class LayerVisualizerContainer(ttk.Frame):
def __init__(self, master=None, **kw):
ttk.Frame.__init__(self, master, **kw)
self.frameToExpand = ttk.Frame(self)
self.frameToExpand.configure(height='200', width='200')
self.frameToExpand.pack(side='top')
self.LayerVisualizerRows = []
firstVisualizerRow = LayerVisualizerRow(master = self.frameToExpand, color = self.getSuggestedColor(0), index=0, changeCallback = self.changeCallback)
firstVisualizerRow.grid(column='0', row='0')
self.LayerVisualizerRows.append(firstVisualizerRow)
def changeCallback(self):
if len(self.LayerVisualizerRows) == 0:
return
if self.LayerVisualizerRows[-1] == None:
return
lastFilename = self.LayerVisualizerRows[-1].GetFile()
twoBackFilename = None
if len(self.LayerVisualizerRows) > 1:
twoBackFilename = self.LayerVisualizerRows[-2].GetFile()
if (not twoBackFilename == None) and (twoBackFilename.strip() == lastFilename.strip()) and (lastFilename.strip() == ''):
self.LayerVisualizerRows[-1].grid_forget()
del(self.LayerVisualizerRows[-1])
elif not lastFilename.strip() == '':
newIndex = len(self.LayerVisualizerRows)
self.LayerVisualizerRows.append(None)
nextVisualizerRow = LayerVisualizerRow(master = self.frameToExpand, color = self.getSuggestedColor(newIndex), index=newIndex, changeCallback = self.changeCallback)
nextVisualizerRow.grid(column='0', row=str(newIndex))
self.LayerVisualizerRows[-1] = nextVisualizerRow
def getSuggestedColor(self, index):
colors = ['#ffe119', '#4363d8', '#f58231', '#dcbeff', '#800000', '#000075', '#a9a9a9', '#ffffff', '#000000']
if index < len(colors):
colorToReturn = colors[index]
else:
colorToReturn = "#000000"
return colorToReturn
def getFiles(self):
filesToReturn = []
for layer in self.LayerVisualizerRows:
fileToAdd = layer.GetFile()
colorToAdd = layer.GetColor()
if type(colorToAdd) == type('test'):
colorToAdd = np.array(ImageColor.getcolor(colorToAdd, "RGB"))
if not fileToAdd == '':
filesToReturn.append((fileToAdd, colorToAdd))
return filesToReturn
class FileChooser(ttk.Frame):
"""My Implementation of a file chooser in tkinter
Since it inherits from ttk.Frame, it is essentially a standalone tkinter widget you can place anywhere you can place a frame
If you want to get the selected filepath, use the method getFilepath
If there are multiple, use getMultiFilepahts (I'm aware of the mispelling, but may be used in the code so I don't want to change it until I can do a complete refactor somehow)
Methods
-------
getFilepath()
Returns the filename that has been selected by the FileChooser
getMultiFilepahts()
Returns the multiple filepaths as a list if the mode is openMultiple
"""
def __init__(self, master=None, labelText='File: ', changeCallback=False, mode='open', title='', buttonText='Choose File', **kw):
"""Initiallizer of the FileChooser Class
Parameters
----------
master : tk or ttk widget
The master of this widget in tkinter. What this widget will be inside of
labelText : str, optional
Sets the text of the label in the FileChooser. Default is "File: "
changeCallback : function, optional
If set, everytime that the entry has its value changed, this function is called. It should not take any parameters
mode : {'open', 'create', 'openMultiple', 'folder'}
open - Opens one file
create - Creates a new file
openMultiple - can open multiple files
folder - opens a folder
title : str, optional
Sets the title for the popup of the file chooser
buttonText : str, optional
Sets the text of the button, default is 'Choose File'
"""
self.changeCallback = changeCallback
ttk.Frame.__init__(self, master, **kw)
self.label = ttk.Label(self)
self.label.configure(text=labelText)
self.label.grid(column='0', row='0')
self.sv = StringVar()
self.sv.trace_add("write", self.entryChangeCallback)
self.entry = ttk.Entry(self, textvariable=self.sv)
self.entry.grid(column='1', row='0')
self.sv.set('')
self.button = ttk.Button(self)
self.button.configure(cursor='arrow', text=buttonText)
self.button.grid(column='3', row='0')
self.button.configure(command=self.ChooseFileButtonPress)
self.filepath = self.entry.get()
self.filepaths = None
self.mode = mode
self.title=title
if self.title == '':
self.title = 'Select File(s)'
def entryChangeCallback(self, sv, three, four):
self.filepath = self.getFilepath()
if self.changeCallback != False:
self.changeCallback()
def ChooseFileButtonPress(self):
filename = ''
if self.mode == 'open':
filename = fd.askopenfilename(title=self.title)
elif self.mode == 'create':
filename = fd.asksaveasfilename(title=self.title)
elif self.mode == 'openMultiple':
filename = fd.askopenfilenames(title=self.title)
self.filepaths = filename
elif self.mode == 'folder':
filename = fd.askdirectory(title=self.title)
self.filepath = str(filename)
self.sv.set(str(self.filepath))
self.entry.xview("end")
def getFilepath(self):
return self.entry.get()
def getMultiFilepahts(self):
return self.filepaths
class MemoryStream(StringIO):
"""For now, look at examples of where MemoryStream is used in gui.py
#TODO add better documentation
"""
def __init__(self):
super().__init__()
self.text = ''
def write(self, string):
self.text = self.text + string
class ScrollableFrame(ttk.Frame):
"""Can be used like a regular tkinter frame, but has a scrollbar on the side
Origional inspiration came from https://blog.teclado.com/tkinter-scrollable-frames/
I have modified it a bit, but started using the code from above
This should be able to be used interchangably with regular ttk or tk frames
Just be sure that you set scrollMin and scrollMax to values that work well
"""
def __init__(self, container, scrollMin = -250, scrollMax = 750, *args, **kwargs):
"""
Parameters
----------
container : tk widget
The master of this scroll frame in tkinter
scrollMin : int
The minimum value that can be scrolled in this scroll bar. If set too low, the frame will start empty, and you will scroll down to be able to see anything.
scrollMax : int
The maximum value that can be scrolled in this scroll bar. If set to high, you can scroll down way past where there is any content in the frame, leaving it empty.
"""
super().__init__(container, *args, **kwargs)
ContainerOne = ttk.Frame(container)
ContainerOne.pack(fill=tk.BOTH, expand=True)
canvas1 = tk.Canvas(ContainerOne, width=500, height=1000, bg='white')
scroll = ttk.Scrollbar(ContainerOne, command=canvas1.yview)
canvas1.config(yscrollcommand=scroll.set, scrollregion=(0,scrollMin,100,scrollMax))
canvas1.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
scroll.pack(side=tk.RIGHT, fill=tk.Y)
frameOne = ttk.Frame(canvas1, width=800, height=450)
canvas1.create_window(250, 125, window=frameOne)
self.scrollable_frame = frameOne
self.container = ContainerOne
canvas1.yview_moveto(0)