mwxlib 1.0.0__py3-none-any.whl → 1.8.0__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.
- mwx/__init__.py +6 -4
- mwx/bookshelf.py +144 -68
- mwx/controls.py +444 -378
- mwx/framework.py +567 -546
- mwx/graphman.py +745 -726
- mwx/images.py +16 -0
- mwx/matplot2.py +244 -235
- mwx/matplot2g.py +812 -751
- mwx/matplot2lg.py +218 -209
- mwx/mgplt.py +20 -22
- mwx/nutshell.py +1119 -1015
- mwx/plugins/ffmpeg_view.py +96 -90
- mwx/plugins/fft_view.py +13 -15
- mwx/plugins/frame_listview.py +68 -75
- mwx/plugins/line_profile.py +1 -1
- mwx/py/filling.py +13 -14
- mwx/testsuite.py +38 -0
- mwx/utilus.py +284 -219
- mwx/wxmon.py +43 -44
- mwx/wxpdb.py +83 -84
- mwx/wxwil.py +19 -18
- mwx/wxwit.py +38 -45
- {mwxlib-1.0.0.dist-info → mwxlib-1.8.0.dist-info}/METADATA +12 -5
- mwxlib-1.8.0.dist-info/RECORD +28 -0
- {mwxlib-1.0.0.dist-info → mwxlib-1.8.0.dist-info}/WHEEL +1 -1
- mwxlib-1.0.0.dist-info/LICENSE +0 -21
- mwxlib-1.0.0.dist-info/RECORD +0 -28
- {mwxlib-1.0.0.dist-info → mwxlib-1.8.0.dist-info}/top_level.txt +0 -0
mwx/plugins/ffmpeg_view.py
CHANGED
|
@@ -1,16 +1,15 @@
|
|
|
1
1
|
#! python3
|
|
2
2
|
"""FFmpeg wrapper.
|
|
3
3
|
"""
|
|
4
|
-
from functools import partial
|
|
5
4
|
from subprocess import Popen, PIPE
|
|
6
5
|
import numpy as np
|
|
7
6
|
import os
|
|
8
7
|
import wx
|
|
9
8
|
import wx.media
|
|
10
9
|
|
|
11
|
-
from mwx.framework import _F
|
|
10
|
+
from mwx.framework import _F
|
|
12
11
|
from mwx.graphman import Layer
|
|
13
|
-
from mwx.controls import LParam, Icon, Button,
|
|
12
|
+
from mwx.controls import Param, LParam, Icon, Button, TextBox
|
|
14
13
|
|
|
15
14
|
|
|
16
15
|
def read_info(path):
|
|
@@ -35,8 +34,8 @@ def capture_video(path, ss=0):
|
|
|
35
34
|
'-pix_fmt', 'rgb24', # rgb24, gray, etc.
|
|
36
35
|
'pipe:' # pipe to stdout: '-'
|
|
37
36
|
]
|
|
38
|
-
bufsize = 4096
|
|
39
|
-
buf = b""
|
|
37
|
+
bufsize = 4096 # w * h * 3
|
|
38
|
+
buf = b""
|
|
40
39
|
with Popen(command, stdout=PIPE) as fp:
|
|
41
40
|
while 1:
|
|
42
41
|
s = fp.stdout.read(bufsize)
|
|
@@ -63,9 +62,9 @@ class MyFileDropLoader(wx.FileDropTarget):
|
|
|
63
62
|
def __init__(self, target):
|
|
64
63
|
wx.FileDropTarget.__init__(self)
|
|
65
64
|
self.target = target
|
|
66
|
-
|
|
65
|
+
|
|
67
66
|
def OnDropFiles(self, x, y, filenames):
|
|
68
|
-
path = filenames[-1]
|
|
67
|
+
path = filenames[-1] # Only the last one will be loaded.
|
|
69
68
|
if len(filenames) > 1:
|
|
70
69
|
print("- Drop only one file please."
|
|
71
70
|
"Loading {!r} ...".format(path))
|
|
@@ -78,13 +77,13 @@ class Plugin(Layer):
|
|
|
78
77
|
"""
|
|
79
78
|
menukey = "Plugins/Extensions/FFMpeg viewer"
|
|
80
79
|
dockable = False
|
|
81
|
-
|
|
80
|
+
|
|
82
81
|
def Init(self):
|
|
83
82
|
self.mc = wx.media.MediaCtrl()
|
|
84
83
|
self.mc.Create(self, size=(300,300),
|
|
85
84
|
style=wx.SIMPLE_BORDER,
|
|
86
85
|
szBackend=wx.media.MEDIABACKEND_WMP10
|
|
87
|
-
|
|
86
|
+
# szBackend=wx.media.MEDIABACKEND_DIRECTSHOW
|
|
88
87
|
)
|
|
89
88
|
self.mc.ShowPlayerControls()
|
|
90
89
|
self.mc.Bind(wx.media.EVT_MEDIA_LOADED, self.OnMediaLoaded)
|
|
@@ -93,29 +92,32 @@ class Plugin(Layer):
|
|
|
93
92
|
|
|
94
93
|
self._path = None
|
|
95
94
|
|
|
96
|
-
self.ss = LParam("ss:",
|
|
95
|
+
self.ss = LParam("ss:", # range/value will be set when loaded later.
|
|
97
96
|
handler=self.set_offset,
|
|
98
97
|
updater=self.get_offset,
|
|
99
98
|
)
|
|
100
|
-
self.to = LParam("to:",
|
|
99
|
+
self.to = LParam("to:", # range/value will be set when loaded later.
|
|
101
100
|
handler=self.set_offset,
|
|
102
101
|
updater=self.get_offset,
|
|
103
102
|
)
|
|
104
|
-
self.crop =
|
|
103
|
+
self.crop = TextBox(self, icon="cut", size=(140,-1),
|
|
105
104
|
handler=self.set_crop,
|
|
106
105
|
updater=self.get_crop,
|
|
107
106
|
)
|
|
107
|
+
self.rate = Param("rate", (1/8,1/4,1/2,1,2,4,8),
|
|
108
|
+
handler=self.set_rate,
|
|
109
|
+
)
|
|
108
110
|
|
|
109
111
|
self.snp = Button(self, handler=self.snapshot, icon='clip')
|
|
110
112
|
self.exp = Button(self, handler=self.export, icon='save')
|
|
111
113
|
|
|
112
|
-
self.rw = Button(self, handler=lambda v: self.
|
|
113
|
-
self.fw = Button(self, handler=lambda v: self.
|
|
114
|
+
self.rw = Button(self, handler=lambda v: self.seekto(-100), icon='|<-')
|
|
115
|
+
self.fw = Button(self, handler=lambda v: self.seekto(+100), icon='->|')
|
|
114
116
|
|
|
115
117
|
self.layout((self.mc,), expand=2)
|
|
116
118
|
self.layout((self.ss, self.to, self.rw, self.fw,
|
|
117
|
-
self.snp, self.crop, self.exp),
|
|
118
|
-
expand=0, row=
|
|
119
|
+
self.snp, self.crop, self.rate, self.exp),
|
|
120
|
+
expand=0, row=8, type='vspin', style='button', lw=32, cw=-1, tw=64)
|
|
119
121
|
|
|
120
122
|
self.menu[0:5] = [
|
|
121
123
|
(1, "&Load file", Icon('open'),
|
|
@@ -129,55 +131,51 @@ class Plugin(Layer):
|
|
|
129
131
|
|
|
130
132
|
self.parent.handler.bind("unknown_format", self.load_media)
|
|
131
133
|
|
|
132
|
-
self.handler.update({
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
134
|
+
self.handler.update({ # DNA<ffmpeg_viewer>
|
|
135
|
+
None : {
|
|
136
|
+
'C-left pressed' : (None, _F(self.seekd, -1000)),
|
|
137
|
+
'C-right pressed' : (None, _F(self.seekd, 1000)),
|
|
138
|
+
},
|
|
139
|
+
0 : { # MEDIASTATE_STOPPED
|
|
140
|
+
'play' : (2, ),
|
|
141
|
+
'space pressed' : (2, _F(self.mc.Play)),
|
|
136
142
|
},
|
|
137
|
-
1 : {
|
|
143
|
+
1 : { # MEDIASTATE_PAUSED
|
|
138
144
|
'stop' : (0, ),
|
|
139
|
-
|
|
145
|
+
'space pressed' : (2, _F(self.mc.Play)),
|
|
146
|
+
},
|
|
147
|
+
2 : { # MEDIASTATE_PLAYING
|
|
148
|
+
'stop' : (0, ),
|
|
149
|
+
'pause' : (1, ),
|
|
140
150
|
'space pressed' : (1, _F(self.mc.Pause)),
|
|
141
151
|
},
|
|
142
152
|
})
|
|
143
153
|
|
|
144
|
-
self.mc.Bind(wx.media.EVT_MEDIA_PAUSE,
|
|
145
|
-
self.mc.Bind(wx.media.EVT_MEDIA_PLAY,
|
|
146
|
-
self.mc.Bind(wx.media.EVT_MEDIA_STOP,
|
|
147
|
-
|
|
148
|
-
## self.mc.Bind(wx.EVT_KEY_DOWN, self.on_hotkey_down)
|
|
149
|
-
## self.mc.Bind(wx.EVT_KEY_UP, self.on_hotkey_up)
|
|
150
|
-
self.mc.Bind(wx.EVT_KEY_DOWN, self.OnKeyDown)
|
|
151
|
-
self.mc.Bind(wx.EVT_KEY_UP, self.OnKeyUp)
|
|
154
|
+
self.mc.Bind(wx.media.EVT_MEDIA_PAUSE, lambda v: self.handler('pause', v))
|
|
155
|
+
self.mc.Bind(wx.media.EVT_MEDIA_PLAY, lambda v: self.handler('play', v))
|
|
156
|
+
self.mc.Bind(wx.media.EVT_MEDIA_STOP, lambda v: self.handler('stop', v))
|
|
152
157
|
|
|
153
|
-
self.Bind(wx.
|
|
154
|
-
|
|
158
|
+
self.mc.Bind(wx.EVT_KEY_DOWN, self.on_hotkey_down)
|
|
159
|
+
self.mc.Bind(wx.EVT_KEY_UP, self.on_hotkey_up)
|
|
160
|
+
|
|
155
161
|
def Destroy(self):
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
def OnKeyDown(self, evt):
|
|
162
|
-
if self.handler('{} pressed'.format(hotkey(evt)), evt) is None:
|
|
163
|
-
evt.Skip()
|
|
164
|
-
|
|
165
|
-
def OnKeyUp(self, evt):
|
|
166
|
-
if self.handler('{} released'.format(hotkey(evt)), evt) is None:
|
|
167
|
-
evt.Skip()
|
|
168
|
-
|
|
162
|
+
self.parent.handler.unbind("unknown_format", self.load_media)
|
|
163
|
+
if self.mc:
|
|
164
|
+
self.mc.Destroy()
|
|
165
|
+
return Layer.Destroy(self)
|
|
166
|
+
|
|
169
167
|
def OnShow(self, evt):
|
|
170
168
|
if not evt.IsShown():
|
|
171
169
|
if self.mc:
|
|
172
170
|
self.mc.Stop()
|
|
173
|
-
|
|
174
|
-
|
|
171
|
+
Layer.OnShow(self, evt)
|
|
172
|
+
|
|
175
173
|
def OnMediaLoaded(self, evt):
|
|
176
174
|
self.ss.range = (0, self.video_dur, 0.01)
|
|
177
175
|
self.to.range = (0, self.video_dur, 0.01)
|
|
178
176
|
self.Show()
|
|
179
177
|
evt.Skip()
|
|
180
|
-
|
|
178
|
+
|
|
181
179
|
def load_media(self, path=None):
|
|
182
180
|
if path is None:
|
|
183
181
|
with wx.FileDialog(self, "Choose a media file",
|
|
@@ -185,11 +183,11 @@ class Plugin(Layer):
|
|
|
185
183
|
if dlg.ShowModal() != wx.ID_OK:
|
|
186
184
|
return None
|
|
187
185
|
path = dlg.Path
|
|
188
|
-
self.mc.Load(path)
|
|
186
|
+
self.mc.Load(path) # -> True (always)
|
|
189
187
|
self.info = read_info(path)
|
|
190
188
|
if self.info:
|
|
191
189
|
v = next(x for x in self.info['streams'] if x['codec_type'] == 'video')
|
|
192
|
-
|
|
190
|
+
# self.video_fps = eval(v['r_frame_rate']) # real base frame rate
|
|
193
191
|
self.video_fps = eval(v['avg_frame_rate']) # averaged frame rate
|
|
194
192
|
self.video_dur = eval(v['duration']) # duration [s]
|
|
195
193
|
w, h = v['width'], v['height']
|
|
@@ -205,64 +203,72 @@ class Plugin(Layer):
|
|
|
205
203
|
else:
|
|
206
204
|
self.message(f"Failed to load file {path!r}.")
|
|
207
205
|
return False
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
206
|
+
|
|
207
|
+
## Correction for seek position. ▲理由は不明 (WMP10 backend only?)
|
|
208
|
+
@property
|
|
209
|
+
def DELTA(self):
|
|
210
|
+
return int(1000 / self.mc.PlaybackRate)
|
|
211
|
+
|
|
211
212
|
def set_offset(self, tc):
|
|
212
213
|
"""Set offset value by referring to ss/to value."""
|
|
213
|
-
|
|
214
|
+
if self._path:
|
|
214
215
|
self.mc.Seek(self.DELTA + int(tc.value * 1000))
|
|
215
|
-
|
|
216
|
-
pass
|
|
217
|
-
|
|
216
|
+
|
|
218
217
|
def get_offset(self, tc):
|
|
219
218
|
"""Get offset value and assigns it to ss/to value."""
|
|
220
|
-
|
|
219
|
+
if self._path:
|
|
221
220
|
tc.value = round(self.mc.Tell()) / 1000
|
|
222
|
-
|
|
223
|
-
pass
|
|
224
|
-
|
|
221
|
+
|
|
225
222
|
def set_crop(self):
|
|
226
|
-
"""Set crop area (W:H:Left:Top) to
|
|
223
|
+
"""Set crop area (W:H:Left:Top) to ROI."""
|
|
227
224
|
frame = self.graph.frame
|
|
228
225
|
if frame:
|
|
229
226
|
try:
|
|
230
|
-
w, h, xo, yo =
|
|
231
|
-
xo -= 0.5 # Correction with half-pixel
|
|
232
|
-
yo -= 0.5 #
|
|
227
|
+
w, h, xo, yo = map(float, self.crop.Value.split(':'))
|
|
228
|
+
xo -= 0.5 # Correction with half-pixel offset.
|
|
229
|
+
yo -= 0.5 # Select left-top corner position.
|
|
233
230
|
nx = xo, xo+w
|
|
234
231
|
ny = yo, yo+h
|
|
235
232
|
frame.region = frame.xyfrompixel(nx, ny)
|
|
236
|
-
except Exception:
|
|
237
|
-
self.message("Failed to evaluate crop text
|
|
238
|
-
|
|
233
|
+
except Exception as e:
|
|
234
|
+
self.message("Failed to evaluate crop text;", e)
|
|
235
|
+
|
|
239
236
|
def get_crop(self):
|
|
240
|
-
"""Get crop area (W:H:Left:Top) from
|
|
241
|
-
crop = ''
|
|
237
|
+
"""Get crop area (W:H:Left:Top) from ROI."""
|
|
242
238
|
frame = self.graph.frame
|
|
243
239
|
if frame:
|
|
244
|
-
nx, ny = frame.xytopixel(
|
|
240
|
+
nx, ny = frame.xytopixel(frame.region)
|
|
245
241
|
if nx.size:
|
|
246
|
-
xo,
|
|
247
|
-
|
|
248
|
-
crop = "{}:{}:{}:{}"
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
def
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
242
|
+
xo, xp = nx
|
|
243
|
+
yp, yo = ny
|
|
244
|
+
self.crop.Value = f"{xp-xo}:{yp-yo}:{xo}:{yo}" # (W:H:left:top)
|
|
245
|
+
return
|
|
246
|
+
if self._path:
|
|
247
|
+
self.crop.Value = "{}:{}:0:0".format(*self.video_size)
|
|
248
|
+
|
|
249
|
+
def set_rate(self, rate):
|
|
250
|
+
if self._path:
|
|
251
|
+
self.mc.PlaybackRate = rate
|
|
252
|
+
|
|
253
|
+
def get_rate(self):
|
|
254
|
+
if self._path:
|
|
255
|
+
return self.mc.PlaybackRate
|
|
256
|
+
|
|
257
|
+
def seekto(self, offset):
|
|
258
|
+
"""Seek position with offset [ms] from the `to` position."""
|
|
259
|
+
if self._path:
|
|
258
260
|
t = self.to.value + offset/1000
|
|
259
|
-
|
|
260
|
-
print(e)
|
|
261
|
-
else:
|
|
262
|
-
if self._path and 0 <= t < self.video_dur:
|
|
261
|
+
if 0 <= t < self.video_dur:
|
|
263
262
|
self.to.value = round(t, 3)
|
|
264
|
-
self.set_offset(self.to)
|
|
265
|
-
|
|
263
|
+
self.set_offset(self.to)
|
|
264
|
+
|
|
265
|
+
def seekd(self, offset):
|
|
266
|
+
"""Seek position with offset [ms] from the current position."""
|
|
267
|
+
if self._path:
|
|
268
|
+
t = self.mc.Tell() + offset
|
|
269
|
+
if 0 <= t < self.video_dur * 1000:
|
|
270
|
+
self.mc.Seek(self.DELTA + t)
|
|
271
|
+
|
|
266
272
|
def snapshot(self):
|
|
267
273
|
"""Create a snapshot of the current frame.
|
|
268
274
|
Load the snapshot image into the graph window.
|
|
@@ -274,7 +280,7 @@ class Plugin(Layer):
|
|
|
274
280
|
buf = capture_video(self._path, t/1000).reshape((h,w,3))
|
|
275
281
|
name = "{}-ss{}".format(os.path.basename(self._path), int(t))
|
|
276
282
|
self.graph.load(buf, name)
|
|
277
|
-
|
|
283
|
+
|
|
278
284
|
def export(self):
|
|
279
285
|
"""Export the cropped / clipped data to a media file."""
|
|
280
286
|
if not self._path:
|
mwx/plugins/fft_view.py
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"""
|
|
4
4
|
import wx
|
|
5
5
|
import numpy as np
|
|
6
|
-
from numpy.fft import fft2,ifft2,fftshift,ifftshift
|
|
6
|
+
from numpy.fft import fft2, ifft2, fftshift, ifftshift
|
|
7
7
|
|
|
8
8
|
from mwx.graphman import Layer
|
|
9
9
|
from mwx.controls import Param
|
|
@@ -13,7 +13,7 @@ def fftcrop(src):
|
|
|
13
13
|
"""Crop src image in 2**N square ROI centered at (x, y)."""
|
|
14
14
|
h, w = src.shape
|
|
15
15
|
m = min(h, w)
|
|
16
|
-
n = 1 if m < 2 else 2 ** int(np.log2(m) - 1)
|
|
16
|
+
n = 1 if m < 2 else 2 ** int(np.log2(m) - 1) # +-m/2
|
|
17
17
|
x, y = w//2, h//2
|
|
18
18
|
return src[y-n:y+n, x-n:x+n]
|
|
19
19
|
|
|
@@ -28,30 +28,30 @@ class Plugin(Layer):
|
|
|
28
28
|
"""
|
|
29
29
|
menukey = "Plugins/Extensions/&FFT view\tAlt+f"
|
|
30
30
|
caption = "FFT view"
|
|
31
|
-
|
|
31
|
+
|
|
32
32
|
def Init(self):
|
|
33
33
|
self.pchk = wx.CheckBox(self, label="logical unit")
|
|
34
34
|
self.pchk.Value = True
|
|
35
35
|
|
|
36
|
-
self.ftor = Param("mask", (2,4,8,16,32,64))
|
|
36
|
+
self.ftor = Param("mask", (2,4,8,16,32,64)) # masking area factor of 1/2
|
|
37
37
|
|
|
38
38
|
self.layout((self.pchk,), title="normal FFT")
|
|
39
39
|
self.layout((self.ftor,), title="inverse FFT", style='chkbox', tw=32)
|
|
40
40
|
|
|
41
41
|
self.parent.define_key('C-f', self.newfft)
|
|
42
42
|
self.parent.define_key('C-S-f', self.newifft)
|
|
43
|
-
|
|
43
|
+
|
|
44
44
|
def Destroy(self):
|
|
45
45
|
self.parent.undefine_key('C-f')
|
|
46
46
|
self.parent.undefine_key('C-S-f')
|
|
47
47
|
return Layer.Destroy(self)
|
|
48
|
-
|
|
48
|
+
|
|
49
49
|
def newfft(self):
|
|
50
50
|
"""New FFT of graph to output."""
|
|
51
51
|
frame = self.graph.frame
|
|
52
52
|
if frame:
|
|
53
53
|
self.message("FFT execution...")
|
|
54
|
-
src = fftcrop(frame.
|
|
54
|
+
src = fftcrop(frame.roi_or_buffer)
|
|
55
55
|
h, w = src.shape
|
|
56
56
|
|
|
57
57
|
dst = fftshift(fft2(src))
|
|
@@ -60,27 +60,25 @@ class Plugin(Layer):
|
|
|
60
60
|
u = 1 / w
|
|
61
61
|
if self.pchk.Value:
|
|
62
62
|
u /= frame.unit
|
|
63
|
-
self.output.load(dst, "*fft of {}*"
|
|
64
|
-
localunit=u)
|
|
63
|
+
self.output.load(dst, f"*fft of {frame.name}*", localunit=u)
|
|
65
64
|
self.message("\b done")
|
|
66
|
-
|
|
65
|
+
|
|
67
66
|
def newifft(self):
|
|
68
67
|
"""New inverse FFT of output to graph."""
|
|
69
68
|
frame = self.output.frame
|
|
70
69
|
if frame:
|
|
71
70
|
self.message("iFFT execution...")
|
|
72
|
-
src = frame.
|
|
71
|
+
src = frame.buffer # Don't crop fft region
|
|
73
72
|
h, w = src.shape
|
|
74
73
|
|
|
75
74
|
if self.ftor.check:
|
|
76
75
|
y, x = np.ogrid[-h/2:h/2, -w/2:w/2]
|
|
77
|
-
mask = np.hypot(y,x) > w / self.ftor.value
|
|
78
|
-
src = src.copy()
|
|
76
|
+
mask = np.hypot(y, x) > w / self.ftor.value
|
|
77
|
+
src = src.copy() # apply mask to the copy
|
|
79
78
|
src[mask] = 0
|
|
80
79
|
|
|
81
80
|
dst = ifft2(ifftshift(src))
|
|
82
81
|
|
|
83
82
|
self.message("\b Loading image...")
|
|
84
|
-
self.graph.load(dst.real, "*ifft of {}*"
|
|
85
|
-
localunit=1/w/frame.unit)
|
|
83
|
+
self.graph.load(dst.real, f"*ifft of {frame.name}*", localunit=1/w/frame.unit)
|
|
86
84
|
self.message("\b done")
|