mwxlib 0.93.1__py3-none-any.whl → 0.93.7__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.
Potentially problematic release.
This version of mwxlib might be problematic. Click here for more details.
- mwx/controls.py +1 -1
- mwx/framework.py +3 -4
- mwx/graphman.py +86 -66
- mwx/matplot2.py +11 -16
- mwx/matplot2g.py +11 -9
- mwx/matplot2lg.py +41 -15
- mwx/nutshell.py +5 -5
- mwx/py/__init__.py +2 -0
- mwx/py/ffmpeg_view.py +257 -0
- mwx/py/fft_view.py +79 -0
- mwx/py/frame_listview.py +283 -0
- mwx/py/line_profile.py +27 -0
- mwx/utilus.py +39 -34
- mwx/wxpdb.py +2 -2
- mwx/wxwil.py +1 -1
- {mwxlib-0.93.1.dist-info → mwxlib-0.93.7.dist-info}/METADATA +1 -1
- mwxlib-0.93.7.dist-info/RECORD +27 -0
- {mwxlib-0.93.1.dist-info → mwxlib-0.93.7.dist-info}/WHEEL +1 -1
- mwxlib-0.93.1.dist-info/RECORD +0 -23
- {mwxlib-0.93.1.dist-info → mwxlib-0.93.7.dist-info}/LICENSE +0 -0
- {mwxlib-0.93.1.dist-info → mwxlib-0.93.7.dist-info}/top_level.txt +0 -0
mwx/matplot2lg.py
CHANGED
|
@@ -31,7 +31,7 @@ class LinePlot(MatplotPanel):
|
|
|
31
31
|
'delete pressed' : (NORMAL, self.OnEscapeSelection),
|
|
32
32
|
'M-a pressed' : (NORMAL, self.OnHomePosition),
|
|
33
33
|
'C-a pressed' : (NORMAL, self.OnHomePosition),
|
|
34
|
-
'Lbutton dblclick' : (NORMAL, self.OnEscapeSelection),
|
|
34
|
+
'Lbutton dblclick' : (NORMAL, self.OnEscapeSelection, self.OnDragLock),
|
|
35
35
|
'*Lbutton pressed' : (NORMAL, self.OnDragLock),
|
|
36
36
|
'*Ldrag begin' : (REGION, self.OnDragBegin),
|
|
37
37
|
},
|
|
@@ -166,7 +166,7 @@ class LinePlot(MatplotPanel):
|
|
|
166
166
|
if x < l: x = l
|
|
167
167
|
elif x > r: x = r
|
|
168
168
|
self.region = (self.__lastpoint, x)
|
|
169
|
-
|
|
169
|
+
elif self.region is not None:
|
|
170
170
|
a, b = self.region
|
|
171
171
|
d = x - self.__lastpoint
|
|
172
172
|
if self.boundary is not None:
|
|
@@ -181,6 +181,8 @@ class LinePlot(MatplotPanel):
|
|
|
181
181
|
else:
|
|
182
182
|
self.region = (a+d, b+d)
|
|
183
183
|
self.__lastpoint = x
|
|
184
|
+
else:
|
|
185
|
+
self.message("- No region.") #<FSM logic-error>
|
|
184
186
|
self.draw()
|
|
185
187
|
|
|
186
188
|
def OnDragEnd(self, evt):
|
|
@@ -282,7 +284,8 @@ class Histogram(LinePlot):
|
|
|
282
284
|
self.xlim = x.min(), x.max()
|
|
283
285
|
self.ylim = 0, y.max()
|
|
284
286
|
self.region = None
|
|
285
|
-
self.
|
|
287
|
+
self.toolbar.update()
|
|
288
|
+
self.toolbar.push_current()
|
|
286
289
|
self.draw()
|
|
287
290
|
|
|
288
291
|
def hreplot(self, frame):
|
|
@@ -308,7 +311,8 @@ class Histogram(LinePlot):
|
|
|
308
311
|
self.__plot.set_data([], [])
|
|
309
312
|
self.region = None
|
|
310
313
|
|
|
311
|
-
self.
|
|
314
|
+
self.toolbar.update()
|
|
315
|
+
self.toolbar.push_current()
|
|
312
316
|
self.draw()
|
|
313
317
|
|
|
314
318
|
def writeln(self):
|
|
@@ -392,9 +396,9 @@ class LineProfile(LinePlot):
|
|
|
392
396
|
'*Ldrag begin' : (REGION, self.OnDragBegin),
|
|
393
397
|
},
|
|
394
398
|
REGION : {
|
|
395
|
-
'S-Ldrag move' : (REGION+LINE, self.OnRegionLock),
|
|
399
|
+
'S-Ldrag move' : (REGION+LINE, self.OnRegionLock, self.OnDragLineBegin),
|
|
396
400
|
'M-Ldrag move' : (REGION+MARK, self.OnMarkPeaks, self.OnMarkSelectionBegin),
|
|
397
|
-
'*Ldrag move' : (REGION, self.OnDragMove),
|
|
401
|
+
'*Ldrag move' : (REGION, self.OnDragMove, self.OnDragTrace),
|
|
398
402
|
'*Ldrag end' : (NORMAL, self.OnDragEnd),
|
|
399
403
|
},
|
|
400
404
|
LINE: {
|
|
@@ -431,7 +435,7 @@ class LineProfile(LinePlot):
|
|
|
431
435
|
|
|
432
436
|
self.menu += [
|
|
433
437
|
(mwx.ID_(510), "&Copy data", "Copy data to clipboard",
|
|
434
|
-
lambda v: self.
|
|
438
|
+
lambda v: self.write_data_to_clipboard()),
|
|
435
439
|
(),
|
|
436
440
|
(mwx.ID_(511), "Logic length", "Set axis-unit in logic base", wx.ITEM_RADIO,
|
|
437
441
|
lambda v: self.set_logic(1),
|
|
@@ -516,6 +520,14 @@ class LineProfile(LinePlot):
|
|
|
516
520
|
"""Plotted (xdata, ydata) in single plot."""
|
|
517
521
|
return self.__plot.get_data(orig=0)
|
|
518
522
|
|
|
523
|
+
def calc_average(self):
|
|
524
|
+
X, Y = self.plotdata
|
|
525
|
+
if self.region is not None:
|
|
526
|
+
a, b = self.region
|
|
527
|
+
Y = Y[(a <= X) & (X <= b)]
|
|
528
|
+
if Y.size:
|
|
529
|
+
return Y.mean()
|
|
530
|
+
|
|
519
531
|
def linplot(self, frame, fit=True, force=True):
|
|
520
532
|
if not force:
|
|
521
533
|
if frame is self.__frame:
|
|
@@ -572,7 +584,8 @@ class LineProfile(LinePlot):
|
|
|
572
584
|
self.xlim = ls[0], ls[-1]
|
|
573
585
|
self.ylim = ly[0], max(ly[1], max(zs))
|
|
574
586
|
|
|
575
|
-
self.
|
|
587
|
+
self.toolbar.update()
|
|
588
|
+
self.toolbar.push_current()
|
|
576
589
|
self.draw()
|
|
577
590
|
|
|
578
591
|
def writeln(self):
|
|
@@ -594,14 +607,14 @@ class LineProfile(LinePlot):
|
|
|
594
607
|
else:
|
|
595
608
|
self.modeline.write("")
|
|
596
609
|
|
|
597
|
-
def
|
|
598
|
-
"""
|
|
599
|
-
self.message("Copy data to clipboard.")
|
|
610
|
+
def write_data_to_clipboard(self):
|
|
611
|
+
"""Write plot data to clipboard."""
|
|
600
612
|
X, Y = self.plotdata
|
|
601
613
|
with io.StringIO() as o:
|
|
602
614
|
for x, y in zip(X, Y):
|
|
603
615
|
o.write("{:g}\t{:g}\n".format(x, y))
|
|
604
616
|
Clipboard.write(o.getvalue(), verbose=0)
|
|
617
|
+
self.message("Write data to clipboard.")
|
|
605
618
|
|
|
606
619
|
## --------------------------------
|
|
607
620
|
## Motion/Drag actions (override)
|
|
@@ -629,7 +642,7 @@ class LineProfile(LinePlot):
|
|
|
629
642
|
if x.size:
|
|
630
643
|
self.__fil.set_xy(list(chain([(x[0],0)], zip(x,y), [(x[-1],0)])))
|
|
631
644
|
self.writeln()
|
|
632
|
-
|
|
645
|
+
|
|
633
646
|
def OnLineWidth(self, evt):
|
|
634
647
|
n = -2 if evt.key[-1] == '-' else 2
|
|
635
648
|
self.set_linewidth(self.__linewidth + n)
|
|
@@ -648,7 +661,17 @@ class LineProfile(LinePlot):
|
|
|
648
661
|
def OnDragLineBegin(self, evt):
|
|
649
662
|
self.set_wxcursor(wx.CURSOR_SIZENS)
|
|
650
663
|
|
|
664
|
+
def OnDragTrace(self, evt):
|
|
665
|
+
"""Show average value."""
|
|
666
|
+
y = self.calc_average()
|
|
667
|
+
if y is not None:
|
|
668
|
+
## self.__hline.set_ydata([y])
|
|
669
|
+
## self.__hline.set_visible(1)
|
|
670
|
+
## self.canvas.draw_idle()
|
|
671
|
+
self.message(f"ya = {y:g}")
|
|
672
|
+
|
|
651
673
|
def OnRegionLock(self, evt):
|
|
674
|
+
"""Show FWHM region."""
|
|
652
675
|
x, y = self.plotdata
|
|
653
676
|
if x.size:
|
|
654
677
|
xc, yc = evt.xdata, evt.ydata
|
|
@@ -676,12 +699,13 @@ class LineProfile(LinePlot):
|
|
|
676
699
|
else:
|
|
677
700
|
self.region = x[[0,-1]] # all y > yc
|
|
678
701
|
|
|
679
|
-
self.__hline.set_ydata(yc)
|
|
702
|
+
self.__hline.set_ydata([yc])
|
|
680
703
|
self.__hline.set_visible(1)
|
|
681
|
-
self.message("y = {:g}, xr = {}".format(yc, self.region))
|
|
682
704
|
self.draw()
|
|
705
|
+
self.message(f"yc = {yc:g}")
|
|
683
706
|
|
|
684
707
|
def OnMarkPeaks(self, evt):
|
|
708
|
+
"""Set markers on peaks."""
|
|
685
709
|
x, y = self.plotdata
|
|
686
710
|
if x.size:
|
|
687
711
|
lw = 5
|
|
@@ -699,7 +723,9 @@ class LineProfile(LinePlot):
|
|
|
699
723
|
self.Selector = x[peaks], y[peaks]
|
|
700
724
|
|
|
701
725
|
def OnMarkErase(self, evt):
|
|
702
|
-
|
|
726
|
+
"""Erase markers on peaks."""
|
|
727
|
+
## del self.Selector
|
|
728
|
+
self.OnEscapeSelection(evt)
|
|
703
729
|
|
|
704
730
|
def OnMarkSelectionBegin(self, evt):
|
|
705
731
|
org = self.p_event
|
mwx/nutshell.py
CHANGED
|
@@ -1682,7 +1682,7 @@ class Buffer(EditWindow, EditorInterface):
|
|
|
1682
1682
|
self.EnsureCaretVisible()
|
|
1683
1683
|
self.AnnotationSetStyle(lx, stc.STC_STYLE_ANNOTATION)
|
|
1684
1684
|
self.AnnotationSetText(lx, msg)
|
|
1685
|
-
self.message(
|
|
1685
|
+
self.message(e)
|
|
1686
1686
|
## print(msg, file=sys.__stderr__)
|
|
1687
1687
|
else:
|
|
1688
1688
|
self.code = code
|
|
@@ -2299,7 +2299,7 @@ class Nautilus(Shell, EditorInterface):
|
|
|
2299
2299
|
obj.this = inspect.getmodule(obj)
|
|
2300
2300
|
obj.shell = self # overwrite the facade <wx.py.shell.ShellFacade>
|
|
2301
2301
|
except AttributeError:
|
|
2302
|
-
## print("- cannot overwrite target vars:
|
|
2302
|
+
## print("- cannot overwrite target vars:", e)
|
|
2303
2303
|
pass
|
|
2304
2304
|
self.parent.handler('title_window', obj)
|
|
2305
2305
|
|
|
@@ -2354,7 +2354,7 @@ class Nautilus(Shell, EditorInterface):
|
|
|
2354
2354
|
wx.py.shell.USE_MAGIC = True
|
|
2355
2355
|
wx.py.shell.magic = self.magic # called when USE_MAGIC
|
|
2356
2356
|
|
|
2357
|
-
## cf. sys.modules
|
|
2357
|
+
## cf. sys.modules
|
|
2358
2358
|
if not self.modules:
|
|
2359
2359
|
force = wx.GetKeyState(wx.WXK_CONTROL)\
|
|
2360
2360
|
& wx.GetKeyState(wx.WXK_SHIFT)
|
|
@@ -3292,7 +3292,7 @@ class Nautilus(Shell, EditorInterface):
|
|
|
3292
3292
|
if lines:
|
|
3293
3293
|
region = self.get_region(self.cline)
|
|
3294
3294
|
self.pointer = region[0] + lines[-1] - 1
|
|
3295
|
-
self.message(
|
|
3295
|
+
self.message(e)
|
|
3296
3296
|
## print(msg, file=sys.__stderr__)
|
|
3297
3297
|
else:
|
|
3298
3298
|
del self.pointer
|
|
@@ -3461,7 +3461,7 @@ class Nautilus(Shell, EditorInterface):
|
|
|
3461
3461
|
try:
|
|
3462
3462
|
modules = set(dir(import_module(text)))
|
|
3463
3463
|
except ImportError as e:
|
|
3464
|
-
self.message("\b failed:
|
|
3464
|
+
self.message("\b failed:", e)
|
|
3465
3465
|
return
|
|
3466
3466
|
## Add unimported module names.
|
|
3467
3467
|
p = "{}.{}".format(text, hint)
|
mwx/py/__init__.py
CHANGED
mwx/py/ffmpeg_view.py
ADDED
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
#! python3
|
|
2
|
+
"""FFmpeg wrapper.
|
|
3
|
+
"""
|
|
4
|
+
from subprocess import Popen, PIPE
|
|
5
|
+
import numpy as np
|
|
6
|
+
import os
|
|
7
|
+
import wx
|
|
8
|
+
import wx.media
|
|
9
|
+
|
|
10
|
+
from mwx.graphman import Layer, Frame
|
|
11
|
+
from mwx.controls import LParam, Icon, Button, TextCtrl
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def read_info(path):
|
|
15
|
+
command = ['ffprobe',
|
|
16
|
+
'-i', path,
|
|
17
|
+
'-loglevel', 'quiet', # no verbose
|
|
18
|
+
'-print_format', 'json', # -format json
|
|
19
|
+
'-show_streams', # -streams info
|
|
20
|
+
]
|
|
21
|
+
with Popen(command, stdout=PIPE, stderr=PIPE) as fp:
|
|
22
|
+
ret, err = fp.communicate()
|
|
23
|
+
if not err:
|
|
24
|
+
return eval(ret)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def capture_video(path, ss=0):
|
|
28
|
+
command = ['ffmpeg',
|
|
29
|
+
'-ss', f"{ss}", # Note: placing -ss before -i will be faster,
|
|
30
|
+
'-i', path, # but maybe not accurate.
|
|
31
|
+
'-frames:v', '1', # -frame one shot
|
|
32
|
+
'-f', 'rawvideo', # -format raw
|
|
33
|
+
'-pix_fmt', 'rgb24', # rgb24, gray, etc.
|
|
34
|
+
'pipe:' # pipe to stdout: '-'
|
|
35
|
+
]
|
|
36
|
+
bufsize = 4096 # w * h * 3
|
|
37
|
+
buf = b"" # bytearray()
|
|
38
|
+
with Popen(command, stdout=PIPE) as fp:
|
|
39
|
+
while 1:
|
|
40
|
+
s = fp.stdout.read(bufsize)
|
|
41
|
+
buf += s
|
|
42
|
+
if len(s) < bufsize:
|
|
43
|
+
break
|
|
44
|
+
return np.frombuffer(buf, np.uint8)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def export_video(path, crop, ss, to, filename):
|
|
48
|
+
command = ['ffmpeg',
|
|
49
|
+
'-i', path,
|
|
50
|
+
'-vf', f"{crop=}",
|
|
51
|
+
'-ss', f"{ss}",
|
|
52
|
+
'-to', f"{to}",
|
|
53
|
+
'-y', filename,
|
|
54
|
+
]
|
|
55
|
+
print('>', ' '.join(command))
|
|
56
|
+
with Popen(command) as fp:
|
|
57
|
+
ret, err = fp.communicate()
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class MyFileDropLoader(wx.FileDropTarget):
|
|
61
|
+
def __init__(self, target):
|
|
62
|
+
wx.FileDropTarget.__init__(self)
|
|
63
|
+
self.target = target
|
|
64
|
+
|
|
65
|
+
def OnDropFiles(self, x, y, filenames):
|
|
66
|
+
path = filenames[-1] # Only the last one will be loaded.
|
|
67
|
+
if len(filenames) > 1:
|
|
68
|
+
print("- Drop only one file please."
|
|
69
|
+
"Loading {!r} ...".format(path))
|
|
70
|
+
self.target.load_media(path)
|
|
71
|
+
return True
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class Plugin(Layer):
|
|
75
|
+
"""Media loader using FFMpeg (installation required).
|
|
76
|
+
"""
|
|
77
|
+
menukey = "Plugins/Extensions/FFMpeg viewer"
|
|
78
|
+
## menukey = "FFMpeg/"
|
|
79
|
+
dockable = False
|
|
80
|
+
|
|
81
|
+
def Init(self):
|
|
82
|
+
self.mc = wx.media.MediaCtrl()
|
|
83
|
+
self.mc.Create(self, size=(300,300),
|
|
84
|
+
style=wx.SIMPLE_BORDER,
|
|
85
|
+
szBackend=wx.media.MEDIABACKEND_WMP10
|
|
86
|
+
## szBackend=wx.media.MEDIABACKEND_DIRECTSHOW
|
|
87
|
+
)
|
|
88
|
+
self.mc.ShowPlayerControls()
|
|
89
|
+
self.mc.Bind(wx.media.EVT_MEDIA_LOADED, self.OnMediaLoaded)
|
|
90
|
+
self.mc.Bind(wx.media.EVT_MEDIA_PAUSE, self.OnMediaPause)
|
|
91
|
+
|
|
92
|
+
self.mc.SetDropTarget(MyFileDropLoader(self))
|
|
93
|
+
|
|
94
|
+
self._path = None
|
|
95
|
+
|
|
96
|
+
self.ss = LParam("ss:", # range/value will be set when loaded later.
|
|
97
|
+
handler=self.set_offset,
|
|
98
|
+
updater=self.get_offset,
|
|
99
|
+
)
|
|
100
|
+
self.to = LParam("to:", # range/value will be set when loaded later.
|
|
101
|
+
handler=self.set_offset,
|
|
102
|
+
updater=self.get_offset,
|
|
103
|
+
)
|
|
104
|
+
self.crop = TextCtrl(self, icon="cut", size=(130,-1),
|
|
105
|
+
handler=self.set_crop,
|
|
106
|
+
updater=self.get_crop,
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
self.snp = Button(self, handler=self.snapshot, tip='Snapshot', icon='clock')
|
|
110
|
+
self.exp = Button(self, handler=self.export, tip="Export", icon='save')
|
|
111
|
+
|
|
112
|
+
self.rw = Button(self, handler=lambda v: self.seekdelta(-100), icon='|<-')
|
|
113
|
+
self.fw = Button(self, handler=lambda v: self.seekdelta(+100), icon='->|')
|
|
114
|
+
|
|
115
|
+
self.layout((self.mc,), expand=2)
|
|
116
|
+
self.layout((self.ss, self.to, self.rw, self.fw,
|
|
117
|
+
self.snp, self.crop, self.exp),
|
|
118
|
+
expand=0, row=7, style='button', lw=12, cw=0, tw=64)
|
|
119
|
+
|
|
120
|
+
self.menu[0:5] = [
|
|
121
|
+
(1, "&Load file", Icon('open'),
|
|
122
|
+
lambda v: self.load_media()),
|
|
123
|
+
|
|
124
|
+
(2, "&Snapshot", Icon('clock'),
|
|
125
|
+
lambda v: self.snapshot(),
|
|
126
|
+
lambda v: v.Enable(self._path is not None)),
|
|
127
|
+
(),
|
|
128
|
+
]
|
|
129
|
+
|
|
130
|
+
self.parent.handler.bind("unknown_format", self.load_media)
|
|
131
|
+
|
|
132
|
+
def Destroy(self):
|
|
133
|
+
try:
|
|
134
|
+
self.parent.handler.unbind("unknown_format", self.load_media)
|
|
135
|
+
self.mc.Stop()
|
|
136
|
+
finally:
|
|
137
|
+
return Layer.Destroy(self)
|
|
138
|
+
|
|
139
|
+
def OnMediaLoaded(self, evt):
|
|
140
|
+
self.ss.range = (0, self.video_dur, 0.01)
|
|
141
|
+
self.to.range = (0, self.video_dur, 0.01)
|
|
142
|
+
self.Show()
|
|
143
|
+
evt.Skip()
|
|
144
|
+
|
|
145
|
+
def OnMediaPause(self, evt):
|
|
146
|
+
evt.Skip()
|
|
147
|
+
|
|
148
|
+
def load_media(self, path=None):
|
|
149
|
+
if path is None:
|
|
150
|
+
with wx.FileDialog(self, "Choose a media file",
|
|
151
|
+
style=wx.FD_OPEN|wx.FD_CHANGE_DIR|wx.FD_FILE_MUST_EXIST) as dlg:
|
|
152
|
+
if dlg.ShowModal() != wx.ID_OK:
|
|
153
|
+
return None
|
|
154
|
+
path = dlg.Path
|
|
155
|
+
self.mc.Load(path) # -> True (always)
|
|
156
|
+
self.info = read_info(path)
|
|
157
|
+
if self.info:
|
|
158
|
+
v = next(x for x in self.info['streams'] if x['codec_type'] == 'video')
|
|
159
|
+
## self.video_fps = eval(v['r_frame_rate']) # real base frame rate
|
|
160
|
+
self.video_fps = eval(v['avg_frame_rate']) # averaged frame rate
|
|
161
|
+
self.video_dur = eval(v['duration']) # duration [s]
|
|
162
|
+
w, h = v['width'], v['height']
|
|
163
|
+
if v['tags'].get('rotate') in ('90', '270'):
|
|
164
|
+
w, h = h, w # transpose
|
|
165
|
+
self.video_size = w, h
|
|
166
|
+
self._path = path
|
|
167
|
+
self.message(f"Loaded {path!r} successfully.")
|
|
168
|
+
return True
|
|
169
|
+
else:
|
|
170
|
+
self.message(f"Failed to load file {path!r}.")
|
|
171
|
+
return False
|
|
172
|
+
|
|
173
|
+
DELTA = 1000 # correction ▲理由は不明 (WMP10 backend only?)
|
|
174
|
+
|
|
175
|
+
def set_offset(self, tc):
|
|
176
|
+
"""Set offset value by referring to ss/to value."""
|
|
177
|
+
try:
|
|
178
|
+
self.mc.Seek(self.DELTA + int(tc.value * 1000))
|
|
179
|
+
except Exception:
|
|
180
|
+
pass
|
|
181
|
+
|
|
182
|
+
def get_offset(self, tc):
|
|
183
|
+
"""Get offset value and assigns it to ss/to value."""
|
|
184
|
+
try:
|
|
185
|
+
tc.value = round(self.mc.Tell()) / 1000
|
|
186
|
+
except Exception:
|
|
187
|
+
pass
|
|
188
|
+
|
|
189
|
+
def set_crop(self):
|
|
190
|
+
"""Set crop area (W:H:Left:Top) to roi."""
|
|
191
|
+
frame = self.graph.frame
|
|
192
|
+
if frame:
|
|
193
|
+
try:
|
|
194
|
+
w, h, xo, yo = eval(self.crop.Value.replace(':', ','))
|
|
195
|
+
xo -= 0.5 # Correction with half-pixel
|
|
196
|
+
yo -= 0.5 # to select left-top (not center) position
|
|
197
|
+
nx = xo, xo+w
|
|
198
|
+
ny = yo, yo+h
|
|
199
|
+
frame.region = frame.xyfrompixel(nx, ny)
|
|
200
|
+
except Exception:
|
|
201
|
+
self.message("Failed to evaluate crop text.")
|
|
202
|
+
|
|
203
|
+
def get_crop(self):
|
|
204
|
+
"""Get crop area (W:H:Left:Top) from roi."""
|
|
205
|
+
crop = ''
|
|
206
|
+
frame = self.graph.frame
|
|
207
|
+
if frame:
|
|
208
|
+
nx, ny = frame.xytopixel(*frame.region)
|
|
209
|
+
if nx.size:
|
|
210
|
+
xo, yo = nx[0], ny[1]
|
|
211
|
+
xp, yp = nx[1], ny[0]
|
|
212
|
+
crop = "{}:{}:{}:{}".format(xp-xo, yp-yo, xo, yo)
|
|
213
|
+
if self._path and not crop:
|
|
214
|
+
crop = "{}:{}:0:0".format(*self.video_size)
|
|
215
|
+
self.crop.Value = crop
|
|
216
|
+
|
|
217
|
+
def seekdelta(self, offset):
|
|
218
|
+
"""Seek relative position [ms]."""
|
|
219
|
+
if wx.GetKeyState(wx.WXK_SHIFT):
|
|
220
|
+
offset /= 10
|
|
221
|
+
try:
|
|
222
|
+
t = self.to.value + offset/1000
|
|
223
|
+
except Exception as e:
|
|
224
|
+
print(e)
|
|
225
|
+
else:
|
|
226
|
+
if self._path and 0 <= t < self.video_dur:
|
|
227
|
+
self.to.value = round(t, 3)
|
|
228
|
+
self.set_offset(self.to) # => seek
|
|
229
|
+
|
|
230
|
+
def snapshot(self):
|
|
231
|
+
"""Create a snapshot of the current frame.
|
|
232
|
+
Load the snapshot image into the graph window.
|
|
233
|
+
"""
|
|
234
|
+
if not self._path:
|
|
235
|
+
return
|
|
236
|
+
t = self.mc.Tell()
|
|
237
|
+
w, h = self.video_size
|
|
238
|
+
buf = capture_video(self._path, t/1000).reshape((h,w,3))
|
|
239
|
+
name = "{}-ss{}".format(os.path.basename(self._path), int(t))
|
|
240
|
+
self.graph.load(buf, name)
|
|
241
|
+
|
|
242
|
+
def export(self):
|
|
243
|
+
"""Export the cropped / clipped data to a media file."""
|
|
244
|
+
if not self._path:
|
|
245
|
+
return
|
|
246
|
+
fout = "{}_clip".format(os.path.splitext(self._path)[0])
|
|
247
|
+
with wx.FileDialog(self, "Save as",
|
|
248
|
+
defaultFile=os.path.basename(fout),
|
|
249
|
+
wildcard="Media file (*.mp4)|*.mp4|"
|
|
250
|
+
"Animiation (*.gif)|*.gif",
|
|
251
|
+
style=wx.FD_SAVE|wx.FD_OVERWRITE_PROMPT) as dlg:
|
|
252
|
+
if dlg.ShowModal() != wx.ID_OK:
|
|
253
|
+
return
|
|
254
|
+
fout = dlg.Path
|
|
255
|
+
export_video(self._path,
|
|
256
|
+
self.crop.Value or "{}:{}:0:0".format(*self.video_size),
|
|
257
|
+
self.ss.value, self.to.value, fout)
|
mwx/py/fft_view.py
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
#! python3
|
|
2
|
+
"""View of FFT/iFFT.
|
|
3
|
+
"""
|
|
4
|
+
import wx
|
|
5
|
+
import numpy as np
|
|
6
|
+
from numpy.fft import fft2,ifft2,fftshift,ifftshift
|
|
7
|
+
## from scipy.fftpack import fft,ifft,fft2,ifft2 Memory Leak? <scipy 0.16.1>
|
|
8
|
+
## import cv2
|
|
9
|
+
|
|
10
|
+
from jgdk import Layer, Param
|
|
11
|
+
import editor as edi
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Plugin(Layer):
|
|
15
|
+
"""FFT view.
|
|
16
|
+
|
|
17
|
+
FFT src (graph.buffer) to dst (output.buffer).
|
|
18
|
+
Note:
|
|
19
|
+
Rectangular regions will result in distorted patterns.
|
|
20
|
+
長方形のリージョンは歪んだパターンになるので要注意
|
|
21
|
+
"""
|
|
22
|
+
menukey = "Plugins/Extensions/&FFT view"
|
|
23
|
+
caption = "FFT view"
|
|
24
|
+
|
|
25
|
+
def Init(self):
|
|
26
|
+
self.pchk = wx.CheckBox(self, label="logical unit")
|
|
27
|
+
self.pchk.Value = True
|
|
28
|
+
|
|
29
|
+
self.ftor = Param("mask", (2,4,8,16,32,64)) # masking area factor of 1/2
|
|
30
|
+
|
|
31
|
+
self.layout((self.pchk,), title="normal FFT")
|
|
32
|
+
self.layout((self.ftor,), title="inverse FFT", style='chkbox', tw=32)
|
|
33
|
+
|
|
34
|
+
self.parent.define_key('C-f', self.newfft)
|
|
35
|
+
self.parent.define_key('C-S-f', self.newifft)
|
|
36
|
+
|
|
37
|
+
def Destroy(self):
|
|
38
|
+
self.parent.define_key('C-f', None)
|
|
39
|
+
self.parent.define_key('C-S-f', None)
|
|
40
|
+
return Layer.Destroy(self)
|
|
41
|
+
|
|
42
|
+
def newfft(self, evt):
|
|
43
|
+
"""New FFT of graph to output."""
|
|
44
|
+
frame = self.graph.frame
|
|
45
|
+
if frame:
|
|
46
|
+
self.message("FFT execution...")
|
|
47
|
+
src = edi.fftcrop(frame.roi)
|
|
48
|
+
h, w = src.shape
|
|
49
|
+
|
|
50
|
+
dst = fftshift(fft2(src))
|
|
51
|
+
|
|
52
|
+
self.message("\b Loading image...")
|
|
53
|
+
u = 1 / w
|
|
54
|
+
if self.pchk.Value:
|
|
55
|
+
u /= frame.unit
|
|
56
|
+
self.output.load(dst, "*fft of {}*".format(frame.name),
|
|
57
|
+
localunit=u)
|
|
58
|
+
self.message("\b done")
|
|
59
|
+
|
|
60
|
+
def newifft(self, evt):
|
|
61
|
+
"""New inverse FFT of output to graph."""
|
|
62
|
+
frame = self.output.frame
|
|
63
|
+
if frame:
|
|
64
|
+
self.message("iFFT execution...")
|
|
65
|
+
src = frame.roi
|
|
66
|
+
h, w = src.shape
|
|
67
|
+
|
|
68
|
+
if self.ftor.check:
|
|
69
|
+
y, x = np.ogrid[-h/2:h/2, -w/2:w/2]
|
|
70
|
+
mask = np.hypot(y,x) > w / self.ftor.value
|
|
71
|
+
src = src.copy() # apply mask to the copy
|
|
72
|
+
src[mask] = 0
|
|
73
|
+
|
|
74
|
+
dst = ifft2(ifftshift(src))
|
|
75
|
+
|
|
76
|
+
self.message("\b Loading image...")
|
|
77
|
+
self.graph.load(dst.real, "*ifft of {}*".format(frame.name),
|
|
78
|
+
localunit=1/w/frame.unit)
|
|
79
|
+
self.message("\b done")
|