-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathview.py
1877 lines (1599 loc) · 82.8 KB
/
view.py
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
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
'''
This is opencv module for Python/vapoursynth scripts that previews VideoNode (clip) within script itself by just running script.
It is a pythonic solution to play or compare vapoursynth clips by using opencv module.
'''
import os
import sys
import platform
import timeit
import vapoursynth as vs
from vapoursynth import core
import numpy as np
import cv2
imported_cv2_ver = tuple(map(int, cv2.__version__.split('.')))[0:3]
if imported_cv2_ver < (3,4,1):
raise ImportError(f'openCV version is {cv2.__version__}, it needs to be at least 3.4.1')
#optional for windows or linux but needed for darwin platform to figure out free RAM
try:
import psutil
except ImportError:
pass
try:
isAPI4 = vs.__api_version__.api_major >= 4
except AttributeError:
isAPI4 = False
RESPECT_X_SUBSAMPLING = True #leave both True if wanting snapping to legit cropping values for Vapoursynth based on clip subsampling
RESPECT_Y_SUBSAMPLING = True #user can override these with: Preview([clip], ignore_subsampling = True)
#assigning keys '1','2','3',...'9', '0' to rgb clip indexes 0,1,2,..., 8, 9
CLIP_KEYMAP = [ ord('1'), ord('2'), ord('3'), ord('4'), ord('5'), ord('6'), ord('7'), ord('8') ,ord('9'), ord('0') ]
WINDOWS_KEYMAP = {
ord(' ') : 'pause_play', #spacebar as a switch for play and pause
ord(',') : 'left_arrow', #key ',' or '<'
ord('.') : 'right_arrow', #key '.' or '>'
13 : 'execute_cropping', #key 'Enter' , right mouse click also executes cropping
2359296 : 'home', #key 'Home' - first video frame
2293760 : 'end', #key 'End'- last video frame
ord('y') : 'object_step_up', #key 'y' selected object to move step up
ord('n') : 'object_step_down', #key 'h' selected object to move step down
ord('g') : 'object_step_left', #key 'g' selected object to move step left
ord('j') : 'object_step_right', #key 'j' selected object to move step right
ord('p') : 'frame_props', #key 'p' to print frame properties
ord('z') : 'quick_2x_zoom_in', #key 'z' zooming 2x
ord('i') : 'pixel_info', #key 'i' print pixel info under mouse: pixel coordinates, frame#, YUV and RGB values
ord('r') : 'reset_preview', #key 'r' reseting preview
ord('e') : 'write_image', #key 'e' to write showing frame you have on screen as png to hardisk, what you see
ord('w') : 'write_image_1_to_1', #key 'w' to write showing frame you have on screen as png to hardisk, image is 1:1, ignores zooms in blowup
ord('q') : 'closing', #key 'q' to quit
27 : 'zoom_out', #key 'Esc' to go back to previous zoom or crop
ord('s') : 'slider_switch', #key 's' slider on/off, to show slider or to destroy it
ord('f') : 'fullscreen_switch', #key 'f' fullscreen on/off
ord('h') : 'help' #key 'h' help, shows hotkeys for keybinding
}
LINUX_KEYMAP = {
## 65361 : 'left_arrow', #81
## 65362 : 'up_arrow', #82
## 65363 : 'right_arrow',#83
## 65364 : 'down_arrow', #84
ord(' ') : 'pause_play',
ord(',') : 'left_arrow',
ord('.') : 'right_arrow',
13 : 'execute_cropping',
65360 : 'home',
65367 : 'end',
ord('y') : 'object_step_up',
ord('n') : 'object_step_down',
ord('g') : 'object_step_left',
ord('j') : 'object_step_right',
ord('p') : 'frame_props',
ord('z') : 'quick_2x_zoom_in',
ord('i') : 'pixel_info',
ord('r') : 'reset_preview',
ord('e') : 'write_image',
ord('w') : 'write_image_1_to_1',
ord('q') : 'closing',
27 : 'zoom_out',
ord('s') : 'slider_switch',
ord('f') : 'fullscreen_switch',
ord('h') : 'help'
}
DARWIN_KEYMAP = {
ord(' ') : 'pause_play',
ord(',') : 'left_arrow',
ord('.') : 'right_arrow',
13 : 'execute_cropping',
65360 : 'home',
65367 : 'end',
ord('y') : 'object_step_up',
ord('n') : 'object_step_down',
ord('g') : 'object_step_left',
ord('j') : 'object_step_right',
ord('p') : 'frame_props',
ord('z') : 'quick_2x_zoom_in',
ord('i') : 'pixel_info',
ord('r') : 'reset_preview',
ord('e') : 'write_image',
ord('w') : 'write_image_1_to_1',
ord('q') : 'closing',
27 : 'zoom_out',
ord('s') : 'slider_switch',
ord('f') : 'fullscreen_switch',
ord('h') : 'help'
}
TRANSFER = {
#transfer_in or transfer : transfer_in_s or transfer_s
0:'reserved',
1:'709',
2:'unspec',
3:'reserved',
4:'470m',
5:'470bg',
6:'601',
7:'240m',
8:'linear',
9:'log100',
10:'log316',
11:'xvycc',
13:'srgb',
14:'2020_10',
15:'2020_12',
16:'st2084',
18:'std-b67'
}
MATRIX = {
#matrix_in or matrix : matrix_in_s or matrix_s
0:'rgb',
1:'709',
2:'unspec',
3:'reserved',
4:'fcc',
5:'470bg',
6:'170m',
7:'240m',
8:'ycgco',
9:'2020ncl',
10:'2020cl' ,
12:'chromancl',
13:'chromacl',
14:'ictcp'
}
PRIMARIES = {
#primaries_in or primaries : primaries_in_s or primaries_s
1 : '709' ,
2 : 'unspec' ,
4 : '470m' ,
5 : '470bg' ,
6 : '170m' ,
7 : '240m' ,
8 : 'film' ,
9 : '2020' ,
10 : 'st428' , #'xyz'
11 : 'st431-2',
12 : 'st432-1',
22 : 'jedec-p22'
}
PROPS = {
'_ChromaLocation': {0:'left', 1:'center', 2:'topleft', 3:'top', 4:'bottomleft', 5:'bottom'},
'_ColorRange': {0:'full range', 1:'limited range'},
'_Matrix': MATRIX ,
'_Primaries': PRIMARIES ,
'_Transfer': TRANSFER ,
'_FieldBased': {0:'progressive', 1:'bottom field first', 2:'top field first'},
'_AbsoluteTime': {},
'_DurationNum': {},
'_DurationDen': {},
'_Combed': {},
'_Field': {0:'from bottom field, if frame was generated by SeparateFields',
1:'from top field, if frame was generated by SeparateFields'},
'_PictType': {},
'_SARNum': {},
'_SARDen': {},
'_SceneChangeNext': {0:'nope',1:'LAST FRAME of the current scene'},
'_SceneChangePrev': {0:'nope',1:'FRAME STARTS a new scene'},
'_Alpha': {}
}
ERROR_FILENAME_LOG = 'error_NO_output_window.txt'
HOTKEYS_HELP ='''KEYBINDING:
'1' to '9' or '0' to switch between clips to compare them if loading more clips.
So there is max 10 clips. But technically,
it could be more if keybinding is adjusted.
MOUSE LEFT DOUBLECLICK zooms in 2x, centers on mouse position
'Z' zooms in 2x as well, again centers on mouse position
MOUSE LEFT CLICK and MOVE initiates crop mode, selecting rectangle with mouse,
confirm selection with ENTER KEY or doubleclicking within selection or with RIGHT MOUSE CLICK,
while touching first point, it snaps to to a pixel following subsumpling (max from loaded clips),
also while drawing a rectangle , it snaps to mods passed as argument, (recomended values 2,4,8 or 16)
default mods: mod_x=2, mod_y=2
'R' RESETs preview to original
',' '<' step one frame back
'.' '>' step one frame forward
'Home' go to first frame
'End' go to last frame
Seeking is source plugin dependant so it could take a time to request a frame.
'Q' quit app, but if zoom or crop was applied, it just resets to original clip first
'Esc' preview goes back to previous zoom or crop
'I' prints YUV or RGB values for pixel under mouse pointer in preview window
printing is in this format:
clip number, pictur type, frame number ,absolute pixel coordinates, original clip YUV or RGB values
or preview RGB values
'P' prints all available frame properties (_PictType, _Matrix, _Primaries ...etc.)
http://www.vapoursynth.com/doc/apireference.html#reserved-frame-properties
'W' save PNG image, what you just preview and it will be saved on hardisk as 8bit PNG as 1:1, ingnoring zoom that you have on screen
'E' save PNG image, what you just preview and it will be saved on hardisk as 8bit PNG, it will be saved as you see on screen, respecting zoom, pixel blocks
'Spacebar' Play/Pause switch
'S' Slider on/off,
Using slider - grab slider , move it to a frame.
Seeking is video and vapoursynth source plugin dependant or its argument selection,
you could experiance conciderable delay and freeze
'F' Fullscreen on/off switch
'H' help, prints this KEYBINDING text
During cropping and just before confirming that crop,
any selected object is defined by clicking on a 'corner' or 'line' or 'all' (clicking within selected rectangle)
So selected rentagle could be manualy re-defined by moving that object up, down, lef or right one step
'Y' selected object to move step up
'N' selected object to move step down
'G' selected object to move step left
'J' selected object to move step right
'''
class Preview:
'''
--- previewing vapoursynth videonodes/clips by opencv(at least 3.4.1 version needed)
--- comparing, swapping, previewing vapoursynth clips by pressing assigned keys on keyboard (1 - 9)
--- clip switching is smooth, seamless during playback
--- with slider or without
--- printing , previewing pixel values for YUV, RGB or other vapoursynth formats, readings could be for example:
clip 1: I Frame: 2345 Pixel: 411,129 CompatYUY2: y:171 u:104 v:160 RGB24: r:233 g:164 b:128
--- printing all available properties for a frame (props)
--- QUICK zooming/cropping, 2x, by mouse doubleclick, centered to mouse position
--- or MANUAL cropping/zooming by drawing cropping area on screen, when all corners, lines could be adjusted
or selected area could be moved/panned,
--- all crops and zoom snap to mods and subsumpling for YUV clips (could be turned off: mod_x=1, mod_y=1, ignore_subsampling=True)
--- using SHIFT key while selecting, cropping area is snapping to original clip's aspect ratio
--- all crops and zoom show real core.std.CropAbs(), vapoursynth command, to obtain that crop, or even live feedback during selection
--- returning back to previous zoom or crop (pressing 'Esc')
--- writing PNG images to hardisk (what you see, gets saved (with blow-up pixels) or what you see 1:1),
--- when writing PNG images during playback it writes subsequent PNG's (for gif creation or other purposes)
'''
def __init__(self, clips,
frames=None, delay = None, img_dir=None, matrix_in_s=None, kernel='Point',
mod_x=2, mod_y=2, ignore_subsampling=False,
position = (60,60), preview_width = None, preview_height = None,
output_window=False, fullscreen=False, play=False, slider=False):
#setting output print first
self.validate_boolean(dict(output_window=output_window))
error_message = ''
if output_window:
try:
import tkinter
except ImportError:
raise Exception("No standard library tkinter module in PATH\n output_window=True needs python's tkinter module to create output window")
try:
import output_window
except ImportError:
error_message = ( 'No module output_window.py in PATH\n'
'Using with Vapoursynth Editor:\n'
'you can put output_window.py into script\'s directory or add that directory to sys.path:\n'
'import sys\n'
'import os\n'
'sys.path.append(os.path.abspath("...your path to directory ..."))\n'
)
self.log('No module output_window.py in PATH\n')
with open(ERROR_FILENAME_LOG, 'a') as info:
info.write('[view]\n' + error_message )
self.clips_orig = clips
self.frames = frames
self.delay = delay
self.matrix_in_s = matrix_in_s
self.kernel = kernel
self.img_dir = img_dir
self.modx = mod_x
self.mody = mod_y
self.position = position
self.init_preview_width = preview_width
self.init_preview_height = preview_height
self.fullscreen = fullscreen
self.play = play
self.slider = slider
self.ignore_subsampling = ignore_subsampling
try:
self.validate_clips()
except ValueError as err:
raise ValueError('[Preview]:', err)
self.validate_frames()
self.validate_delay()
self.validate_img_dir()
self.validate_matrix()
self.validate_kernel()
self.validate_position()
self.validate_preview_dimensions()
self.validate_boolean(dict(fullscreen=fullscreen, play=play, slider=slider, ignore_subsampling=ignore_subsampling))
#limiting Vapoursynth cache if not enough RAM'''
available = None
available_RAM = self.freeRAM()
vapoursynth_cache = core.max_cache_size
self.log(f'Vapoursynth cache is set to: {vapoursynth_cache}MB')
if available_RAM:
self.log(f'free RAM: {available_RAM}MB')
cache = self.limit_cache(vapoursynth_cache, available_RAM)
if not cache == vapoursynth_cache:
self.log(f'setting Vapoursynth cache to: {cache}MB\n')
core.max_cache_size = cache
else:
self.log('\nWARNING, failed to get available free RAM,')
self.log(' Vapoursynth cache was not limited if needed,')
self.log(' RAM overrun or freeze possible\n')
#converting clips to RGB clips for opencv preview
self.rgbs = [] #currently previewing rgb clips
self.rgbs_orig = [] #back ups of original rgb clips
self.rgbs_error = [] #list of booleans, True if rgb had errors
convert = Conversions()
depth = 8 #openCV would scale 16bit int or 32bit float to 0-255 anyway
sample_type = vs.INTEGER
def error_clip(err):
err_clip = core.std.BlankClip(self.clips_orig[i], format=vs.RGB24)
err_clip = core.text.Text(err_clip, err)
self.rgbs.append(err_clip)
self.rgbs_error.append(True)
for i, clip in enumerate(self.clips_orig):
rgb, log = convert.toRGB(clip, matrix_in_s=self.matrix_in_s, depth=depth, kernel=self.kernel, sample_type = sample_type)
log = 'clip {} to RGB for preview:\n'.format(i+1) + log
try:
rgb.get_frame(0)
except vs.Error as err:
log += '\n[toRGB]'+ str(err)
error_clip(err)
else:
if isinstance(rgb, vs.VideoNode):
self.rgbs.append(rgb)
self.rgbs_error.append(False)
else:
err = '\n[toRGB] converted RGB is not vs.VideoNode'
log += err
error_clip(err)
self.log(log)
if self.rgbs:
self.modx, self.mody, self.modx_subs, self.mody_subs = self.validate_mod(self.modx, self.mody)
self.rgbs_orig = self.rgbs.copy()
self.show()
else:
self.log('[Preview.__init__] no clips loaded ')
def show(self):
'''
setting up show loop
'''
#self.log('\n[Preview.show]')
#getting keymap and indexMap depending on OS
OS = self.get_platform()
self.windows_keymap = WINDOWS_KEYMAP
self.linux_keymap = LINUX_KEYMAP
self.darwin_keymap = DARWIN_KEYMAP
KEYMAP = getattr(self, OS + '_keymap')
#loop properties
self.close = False # True will exit a show, app
self.frame = self.frames[0] # starting frame
self.i = 0 # self.i is current previewing clip index
j = 0 # tracks clip changes, if it matches self.i
if self.play: self.play = 1 # make bint from bool
else: self.play = 0
self.previewData_reset() #makes first stored crop data (width, height, left, top)
self.width = self.rgbs_orig[self.i].width
self.height = self.rgbs_orig[self.i].height
self.left = 0
self.top = 0
#mouseAction() properties
self.ix, self.iy = (-1 , -1) #assuming mouse off preview area so no readings yet
self.tx, self.ty = (-10,-10) #first touchdown coordinates while drawing rectangle
self.isCropping = False #initiates cropping
self.drawing = False #True after left mouse button clicks so cropping drawing animation is activated
self.panning = False #selected rectangle object for moving any direction - panning
self.execute_crop = False #True executes selected cropping for all videos
self.object = None #name of object, used in manual step correction using keys
self.proximity = 10 #while clicking down it will pick up corner, line or selection with this pixel proximity in that order
self.good_c = (0,255,0) #BGR color for selection lines if crop is ok in vapoursynth
self.bad_c = (0,0,255) #lines turn into this BGR color if cropping would give error in vapoursynth
self.color = self.good_c
self.flash_color = (255,255,255) #flashing color after selecting an object (a line, lines, corner)
self.x1 = None #left selection x coordinate and also a flag if there was a selection
#opencv window
text=''
for i , rgb in enumerate(self.rgbs):
text +='clip{} {} '.format(i+1, self.clips_orig[i].format.name)
clip_KEYMAP = CLIP_KEYMAP[:len(self.rgbs)]
self.title = 'VideoNodes: {}'.format(text)
self.build_window(self.title, self.mouseAction)
self.log('OpenCV version: ' + cv2.__version__)
try:
cv2.displayStatusBar(self.title, '')
self.Qt = True
except:
self.print_info(' No Status Bar, This OpenCV was compiled without QT library')
self.Qt = False
self.placement = (self.position[0], self.position[1], self.init_preview_width, self.init_preview_height)
if not self.fullscreen:
cv2.resizeWindow(self.title, self.init_preview_width, self.init_preview_height)
cv2.moveWindow(self.title, self.position[0],self.position[1])
if self.slider:
self.build_slider()
#print clip resolutions if different in clips
if not len(set(self.resolutions)) <= 1:
self.log(f"Clips DO NOT HAVE THE SAME RESOLUTIONS, expect weird behaviour if cropping")
#init print
self.print_info(self.print_clip_name() +': {}'.format(self.i+1))
if self.play: self.ref = timeit.default_timer() #starting time reference for timing frames
'''
main openCV playback loop
'''
while True:
self.show_frame()
if self.slider:
cv2.setTrackbarPos('Frames', self.title, self.frame)
key = cv2.waitKeyEx(self.play)
#print(key)
if key != -1: #if a key was pressed
try:
getattr(self, KEYMAP[key])() #execute functions for hotkeys
except KeyError:
try:
self.i = clip_KEYMAP.index(key) #if key was pressed that suppose to change clips, change index for clips
except ValueError:
pass
else:
#print this only one time
if self.i!= j:
self.print_info(self.cropping_line_text(*self.previewData[-1]))
j=self.i
if self.close:
break #exiting loop and app
self.frame = self.update_frame(self.frame)
if cv2.getWindowProperty(self.title, cv2.WND_PROP_VISIBLE) < 1: #canceling window clicking 'x'
break
cv2.destroyAllWindows()
def update_frame(self, f):
if self.play :
f += 1
if f >= self.frames[1]:
self.play = 0
f = self.frames[1]-1
elif f < self.frames[0]:
self.play = 0
f = self.frames[0]
return f
def show_frame(self):
'''
Vapoursynth frame is converted to numpy arrays for opencv to show
delay is handled here, not in cv2.waitKey() because timeit.default_timer() takes app&system time overhead into an account
'''
try:
f = self.rgbs[self.i].get_frame(self.frame)
except:
f = self.error_frame()
if isAPI4: self.img = np.dstack([np.array(f[p], copy=False) for p in [2,1,0]])
else: self.img = np.dstack([np.array(f.get_read_array(p), copy=False) for p in [2,1,0]])
if self.isCropping and self.x1 is not None:
img = self.img_and_selection(self.img, (self.x1,self.y1,self.x2,self.y2),self.color)
if self.play: self.delay_it()
cv2.imshow(self.title, img)
else:
if self.play: self.delay_it()
cv2.imshow(self.title, self.img)
def error_frame(self):
self.play = 0
def log_err():
err = '\n' + str(sys.exc_info()[0])+'\n'
err += 'in line ' + str(sys.exc_info()[2].tb_lineno)+'\n'
err += str(sys.exc_info()[1])+'\n'
return err
err = log_err()
info = '\nclip: {} Frame: {} ,Frame could not be rendered for this clip'.format(self.i, self.frame)
self.log(err+info)
if self.Qt: self.print_statusBar(info)
err_clip = core.std.BlankClip(self.clips_orig[self.i], format=vs.RGB24, length=1).text.Text(err+info)
return err_clip.get_frame(0)
def delay_it(self):
while True:
new = timeit.default_timer()
if new >= self.ref + self.delay:
self.ref = new
break
def get_platform(self):
'''
sys.platform gets 'Linux', 'Windows' or 'Darwin'
retuns: 'linux', 'windows' or 'darwin'
'''
OS = None
if sys.platform.startswith('linux'):
OS = 'linux'
elif sys.platform.startswith('win'):
OS = 'windows'
elif sys.platform == 'darwin':
OS = 'darwin'
else:
try:
OS = platform.system()
except:
OS = None
if OS: return OS.lower()
else: return None
def execute_cropping(self):
if self.execute_crop:
self.crop_to_new(self.width, self.height, *self.get_absolute_offsets(self.x1, self.y1))
self.isCropping = False
self.execute_crop = False
def pause_play(self):
if self.play:
self.play = 0
else:
self.play = 1
self.ref = timeit.default_timer()
def log(self, *args):
'''
if
argument output_window = True
and
output_window modul is imported,
then print is redirected to tkinter window
'''
text =''
for item in args:
try:
text += str(item) + ' '
except:
pass
print(text[:-1])
def frame_props(self):
self.log(f'\nclip{self.i+1} {self.clips_orig[self.i].format.name}, properties of frame {self.frame}:')
self.log(self.get_frame_props(self.clips_orig[self.i], self.frame))
def get_frame_props(self, clip, frame):
'''
prints all available frame properties (_PictType, _Matrix, _Primaries ...etc.)
http://www.vapoursynth.com/doc/apireference.html#reserved-frame-properties
'''
info = []
props_dict = dict(clip.get_frame(frame).props)
for prop, prop_value in props_dict.items():
if isinstance(prop_value, bytes):
prop_value = prop_value.decode()
elif isinstance(prop_value, vs.VideoFrame): #this is a wild guess for alpha, did not look into it yet
prop_value = 'yes'
info.append(' {: <25}{}'.format(prop, prop_value))
try:
info.append('={}'.format(PROPS[prop][prop_value]))
except:
pass
info.append('\n')
return ''.join(info)
def mouseAction(self,event,x,y,flags,p=None):
'''
Mouse click initiates drawing of a cropping rectangle.
While holding SHIFT, new rectangle snaps to aspect ration of a clip.
Drawing of rectangle respects and snaps to original YUV clip's subsampling.
(if not deactivated by: mod_x=1, mod_y=1, ignore_subsampling=True)
Clicking outside of selected rentangle cancels cropping.
(or initiates new cropping selection if mouse keeps moving)
Double click inside of selected rentangle (or keyboard ENTER) confirms and performs crop.
Clicking once inside of selected rentangle activates selection for moving,
that could be done by mouse or just using keyboard ('g','y','j','n' - one step to left,top,right or down),
to move it in smallest subsampling steps.
Clicking on particular single object (corner or a line) also activates moving but only for that object.
mouseAction needs globals so using Preview class attributes for that purpose to store values:
ix, iy mouse position
xa,ya first anchor point for drawing selection
x1,x2,y1,y2 current selection points (rectangle)
width, height width which is (x2-x1) and height (y2-y1) ,for current rectangle
'''
if event == cv2.EVENT_LBUTTONDOWN:
self.useX = True
self.useY = True
if not self.isCropping:
self.isCropping = True
self.drawing = True
self.execute_crop = False
self.init_new_selection(x,y)
elif self.isCropping:
self.drawing = True
self.object = self.select_object(x,y)
if self.object == 'all': #whole selection selected, set for panning
self.panning = True
elif self.object is None: #none object selected, initiate new crop
self.execute_crop = False
self.x1 = None #for show_frame() to not show selection, cannot use isCropping switch, it could be True
self.init_new_selection(x,y)
elif event == cv2.EVENT_MOUSEMOVE:
self.ix = x
self.iy = y
if self.isCropping and self.drawing and not self.panning:
rectangle = self.new_rectangle(x,y,flags&cv2.EVENT_FLAG_SHIFTKEY)
self.live_crop_info(rectangle)
if not self.play:
cv2.imshow(self.title, self.img_and_selection(self.img,rectangle,self.color))
elif self.panning:
rectangle = self.move_rectangle(x,y,flags&cv2.EVENT_FLAG_SHIFTKEY)
self.live_crop_info(rectangle)
if not self.play:
cv2.imshow(self.title, self.img_and_selection(self.img,rectangle,self.color))
elif event == cv2.EVENT_LBUTTONUP:
self.panning = False
self.drawing = False
if self.tx == x and self.ty == y: #mouse touched screen but did not moved, no drawing happened, quit cropping
self.isCropping = False
self.print_info(self.cropping_line_text(*self.previewData[-1]))
self.show_frame()
self.ix = x
self.iy = y
elif self.isCropping: #rectangle is selected, relevant atributes ready for crop: self.x1,self.y1,self.x2,self.y2
self.execute_crop = True #but self.isCropping is still True because cropping can be modified
#self.isCropping becomes False only after user executes cropping (key Enter or dbl click or right mouse click)
elif event == cv2.EVENT_LBUTTONDBLCLK:
if self.isCropping: #doubleclick into selected area would make crop
self.execute_crop = False
self.isCropping = False
self.crop_to_new(self.width, self.height, *self.get_absolute_offsets(self.x1, self.y1))
self.show_frame()
self.ix = x
self.iy = y
else: #doubleclick outside of selected area or if there is no selection
self.quick_2x_zoom_in(x,y) #quick 2x zoom, centered to mouse position x,y
#self.show_frame() #cv2.EVENT_LBUTTONUP renders frame because self.tx == x and self.ty == y
elif event == cv2.EVENT_RBUTTONDOWN: #if rectangle is drawn for crop, right doubleclick executes crop
if self.execute_crop:
self.execute_crop = False
self.isCropping = False
self.crop_to_new(self.width, self.height, *self.get_absolute_offsets(self.x1, self.y1))
self.show_frame()
self.ix = x
self.iy = y
def init_new_selection(self, x,y):
'''initiate drawing of selection area by creating first anchor point and other properties'''
self.tx = x
self.ty = y
#self.play = 0 #stopping playback while starting to crop
self.w = self.rgbs[self.i].width
self.h = self.rgbs[self.i].height
self.origw = self.rgbs_orig[self.i].width
self.origh = self.rgbs_orig[self.i].height
self.xa = x - x % self.modx_subs #snapping to correct subsumpling column
self.ya = y - y % self.mody_subs #snapping to correct subsumpling line
def new_rectangle(self,x,y, flags=0):
'''
draw a rectangle selection x1,y1; x2,y2 , snapping to video resolution mods and subsampling mods
keep selection within rgb's dimensions w,h
if SHIFT is pressed keep clips aspect ratio (and of course keep mods as well!)
also xa,ya is always needed, that is anchor point for selection, first point of selection
'''
if self.useX:
if x>=self.xa:
x1 = self.xa
x2 = min(x, self.w)
else:
x1 = max(x, 0)
x2 = self.xa
w = x2 - x1
w = w - w % self.modx
else:
x1 = self.x1
x2 = self.x2
if self.useY:
if y>=self.ya:
y1 = self.ya
y2 = min(y, self.h)
else:
y1 = max(y, 0)
y2 = self.ya
h = y2 - y1
h = h - h % self.mody
else:
y1 = self.y1
y2 = self.y2
if flags == 16:
'''SHIFT key is pressed, snap rectangle into aspect ratio '''
t_w = w
ar = self.origh/self.origw
while t_w > 0:
t_h = t_w*ar
if t_h.is_integer() and t_h % self.mody == 0 and t_h + y1 <= self.h and t_h <= y2:
h = int(t_h)
w = t_w
break
t_w -= self.modx
if t_w <= 0: w = 0
#final correction
if self.useX:
if x>=self.xa: x2 = x1 + w
else: x1 = x2 - w
self.width = w
if self.useY:
if y>=self.ya: y2 = y1 + h
else: y1 = y2 - h
self.height = h
self.x1 = self.left = x1
self.y1 = self.top = y1
self.x2 = x2
self.y2 = y2
return (x1,y1,x2,y2)
def move_rectangle(self,x,y, flags=0):
'''
move object 'all' (all lines ergo all selected area)
which technically is making new rectangle x1,y1; x2,y2 but same width and height and snapping to subsampling mods,
keep selection within rgb's dimensions w,h
if SHIFT key is pressed while moving, slide selection horizontaly or verticaly only,
dx (x - self.x1) and dy (y - self.y1) are introduced to imitate mouse always dragging x1,y1 to not have weird delays if dragging off screen
'''
x1 = x - self.dx
y1 = y - self.dy
x1 = max((x1 - x1 % self.modx_subs), 0)
if x1 + self.width > self.w:
x1 = self.w-self.width
y1 = max((y1 - y1 % self.mody_subs), 0)
if y1 + self.height > self.h:
y1 = self.h-self.height
if flags == 16:
'''SHIFT key is pressed'''
if abs(x1-self.xa) > abs(y1-self.ya):
y1 = self.ya #anchor/freeze y
else:
x1 = self.xa #anchor/freeze x
x2 = x1+self.width
y2 = y1+self.height
self.x1 = self.left = x1
self.y1 = self.top = y1
self.x2 = x2
self.y2 = y2
return (x1,y1,x2,y2)
def select_object(self, x,y):
'''
locate object with mouse click on x,y coordinates, it could be: a corner, line or middle of selection(object 'all')
set that object for drawing/moving and flash particular selected object,
priority for selections: corner, then line, then selected area(object 'all')
return found name of object
'''
p = max(self.proximity,1) #proximity to "see" an object from that pixel distance when clicking
f = 2 #flashing line proximity in pixels
c = 5 #flashing corner border proximity in pixels
x1 = self.x1
y1 = self.y1
x2 = self.x2
y2 = self.y2
r = (x1,y1,x2,y2)
#self.play = 0 #stopping playback while modifying crop
if x > x1-p and x < x1+p:
if y > y1-p and y < y1+p:
self.set_object_left_top_corner()
self.flash_object(r,[((x1-c,y1-c),(x1+c, y1+c))])
return 'left_top_corner'
elif y > y2-p-1 and y < y2+p-1:
self.set_object_left_bottom_corner()
self.flash_object(r,[((x1-c,y2-c-1),(x1+c, y2+c-1))])
return 'left_bottom_corner'
else:
self.set_object_left_line()
self.flash_object(r,[((x1-f,0),(x1+f, self.h))])
return 'left_line'
elif x > x2-p-1 and x < x2+p-1:
if y > y1-p-1 and y < y1+p-1:
self.set_object_right_top_corner()
self.flash_object(r,[((x2-c-1,y1-c),(x2+c-1, y1+c))])
return 'right_top_corner'
elif y > y2-p-1 and y < y2+p-1:
self.set_object_right_bottom_corner()
self.flash_object(r,[((x2-c-1,y2-c-1),(x2+c-1, y2+c-1))])
return 'right_bottom_corner'
else:
self.set_object_right_line()
self.flash_object(r,[((x2-f-1,0),(x2+f-1,self.h))])
return 'right_line'
elif y > y1-p and y < y1+p:
self.set_object_top_line()
self.flash_object(r,[((0,y1-f),(self.w, y1+f))])
return 'top_line'
elif y > y2-p-1 and y < y2+p-1:
self.set_object_bottom_line()
self.flash_object(r,[((0,y2-f-1),(self.w, y2+f-1))])
return 'bottom_line'
elif x > x1 and x < x2 and y > y1 and y < y2:
self.set_object_all(x, y)
self.flash_object(r,[((x1-f,y1-f),(x2+f-1, y2+f-1)),((x1+f,y1+f),(x2-f-1, y2-f-1))])
return 'all'
else:
return None
def set_object_left_top_corner(self,*_):
self.xa, self.ya = self.x2, self.y2
def set_object_left_bottom_corner(self,*_):
self.xa, self.ya = self.x2, self.y1
def set_object_left_line(self,*_):
self.xa, self.ya = self.x2, self.y1
self.useY = False
def set_object_right_top_corner(self,*_):
self.xa, self.ya = self.x1, self.y2
def set_object_right_bottom_corner(self,*_):
self.xa, self.ya = self.x1, self.y1
def set_object_right_line(self,*_):
self.xa, self.ya = self.x1, self.y1
self.useY = False
def set_object_top_line(self,*_):
self.xa, self.ya = self.x2, self.y2
self.useX = False
def set_object_bottom_line(self,*_):
self.xa, self.ya = self.x1, self.y1
self.useX = False
def set_object_all(self, x, y):
self.xa, self.ya = self.x1,self.y1
self.dx = x - self.x1
self.dy = y - self.y1
'''
object_step_up()down, left and right gets a step value, smallest by resolution mods and subsumpling mods,
this is for a keyboard stepping only,it never gets here if using mouse
'''
def object_step_up(self):
if self.object == 'all': self.move_object(0, -self.mody_subs)
else: self.move_object(0, -max(self.mody, self.mody_subs))
def object_step_down(self):
if self.object == 'all': self.move_object(0, self.mody_subs)
else: self.move_object(0, max(self.mody, self.mody_subs))
def object_step_left(self):
if self.object == 'all': self.move_object(-self.modx_subs, 0)
else: self.move_object(-max(self.modx, self.modx_subs), 0)
def object_step_right(self):
if self.object == 'all': self.move_object(self.modx_subs, 0)
else: self.move_object(max(self.modx, self.modx_subs), 0)
def move_object(self,x,y):
'''
move_object() is for keyboard stepping to simulate mouse movement,
it never gets here if using mouse,
x,y is step increment for moving
'''
if self.object is None: return
if self.object == 'all':
new_position = (self.x1+x, self.y1+y)
self.set_object_all(self.x1, self.y1)
elif self.object == 'top_line': new_position = (self.x1+x, self.y1+y)
elif self.object == 'bottom_line': new_position = (self.x2+x, self.y2+y)
elif self.object == 'right_line': new_position = (self.x2+x, self.y2+y)
elif self.object == 'right_bottom_corner': new_position = (self.x2+x, self.y2+y)
elif self.object == 'right_top_corner': new_position = (self.x2+x, self.y1+y)
elif self.object == 'left_line': new_position = (self.x1+x, self.y1+y)
elif self.object == 'left_bottom_corner': new_position = (self.x1+x, self.y2+y)
elif self.object == 'left_top_corner': new_position = (self.x1+x, self.y1+y)
if self.object == 'all': rectangle = self.move_rectangle(*new_position)
else: rectangle = self.new_rectangle(*new_position)
self.live_crop_info(rectangle)
if not self.play:
cv2.imshow(self.title, self.img_and_selection(self.img, rectangle, self.color))
getattr(self, f'set_object_{self.object}')(self.x1,self.y1) #set object for another move if there is
def flash_object(self, r, flash_rectangles):
img = self.img_and_selection(self.img, r, self.color)
for tuple_pair in flash_rectangles:
cv2.rectangle(img, *tuple_pair, self.flash_color, 1, cv2.LINE_AA)
cv2.imshow(self.title, img)
def img_and_selection(self, img, r, c):
x1,y1,x2,y2 = r
final = cv2.bitwise_not(img)
#crop = cv2.UMat(img, [y1, y2], [x1, x2]) #to do accelerating
final[y1:y2, x1:x2] = img[y1:y2, x1:x2]
cv2.line(final, (x1, 0), (x1, self.h), c, 1, cv2.LINE_AA)
cv2.line(final, (0, y1), (self.w, y1), c, 1, cv2.LINE_AA)
cv2.line(final, (max(x1,x2-1), 0), (max(x1,x2-1), self.h), c, 1, cv2.LINE_AA)
cv2.line(final, (0, max(y1,y2-1)), (self.w, max(y1,y2-1)), c, 1, cv2.LINE_AA)
return final
def live_crop_info(self, r):
x1,y1,x2,y2 = r
self.print_info(self.cropping_line_text(x2-x1,y2-y1,*self.get_absolute_offsets(x1,y1)))
#### cv2.putText(dist, info, (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 0), 4) #fontScale=0.4
def trackbar_change(self, pos):
self.frame = int(pos)
if self.play == 0:
self.show_frame()
def reset_preview(self):
self.reset_preview()
def build_window(self, title, mouseAction):
if self.fullscreen:
self.set_window_fullscreen(title)
else: