Jatin7860 commited on
Commit
fcd5579
·
verified ·
1 Parent(s): 48c7b9c

Upload 226 files

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .gitattributes +4 -0
  2. CODEGUIDELINES +5 -0
  3. DFLIMG/DFLIMG.py +12 -0
  4. DFLIMG/DFLJPG.py +324 -0
  5. DFLIMG/__init__.py +2 -0
  6. LICENSE +674 -0
  7. README.md +429 -3
  8. XSegEditor/QCursorDB.py +10 -0
  9. XSegEditor/QIconDB.py +26 -0
  10. XSegEditor/QImageDB.py +8 -0
  11. XSegEditor/QStringDB.py +102 -0
  12. XSegEditor/XSegEditor - Copy.py +1522 -0
  13. XSegEditor/XSegEditor.py +1494 -0
  14. XSegEditor/gfx/cursors/cross_blue.png +0 -0
  15. XSegEditor/gfx/cursors/cross_green.png +0 -0
  16. XSegEditor/gfx/cursors/cross_red.png +0 -0
  17. XSegEditor/gfx/fonts/NotoSans-Medium.ttf +0 -0
  18. XSegEditor/gfx/icons/app_icon.png +0 -0
  19. XSegEditor/gfx/icons/delete_poly.png +0 -0
  20. XSegEditor/gfx/icons/down.png +0 -0
  21. XSegEditor/gfx/icons/left.png +0 -0
  22. XSegEditor/gfx/icons/poly_color.psd +0 -0
  23. XSegEditor/gfx/icons/poly_color_blue.png +0 -0
  24. XSegEditor/gfx/icons/poly_color_green.png +0 -0
  25. XSegEditor/gfx/icons/poly_color_red.png +0 -0
  26. XSegEditor/gfx/icons/poly_type_exclude.png +0 -0
  27. XSegEditor/gfx/icons/poly_type_include.png +0 -0
  28. XSegEditor/gfx/icons/poly_type_source.psd +0 -0
  29. XSegEditor/gfx/icons/pt_edit_mode.png +0 -0
  30. XSegEditor/gfx/icons/pt_edit_mode_source.psd +0 -0
  31. XSegEditor/gfx/icons/redo_pt.png +0 -0
  32. XSegEditor/gfx/icons/redo_pt_source.psd +0 -0
  33. XSegEditor/gfx/icons/right.png +0 -0
  34. XSegEditor/gfx/icons/trashcan.png +0 -0
  35. XSegEditor/gfx/icons/undo_pt.png +0 -0
  36. XSegEditor/gfx/icons/undo_pt_source.psd +0 -0
  37. XSegEditor/gfx/icons/up.png +0 -0
  38. XSegEditor/gfx/icons/view_baked.png +0 -0
  39. XSegEditor/gfx/icons/view_lock_center.png +0 -0
  40. XSegEditor/gfx/icons/view_xseg.png +0 -0
  41. XSegEditor/gfx/icons/view_xseg_overlay.png +0 -0
  42. XSegEditor/gfx/images/intro.png +0 -0
  43. XSegEditor/gfx/images/intro_source.psd +0 -0
  44. _config.yml +9 -0
  45. core/cv2ex.py +40 -0
  46. core/imagelib/SegIEPolys.py +158 -0
  47. core/imagelib/__init__.py +32 -0
  48. core/imagelib/blursharpen.py +38 -0
  49. core/imagelib/color_transfer.py +336 -0
  50. core/imagelib/common.py +58 -0
.gitattributes CHANGED
@@ -33,3 +33,7 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ doc/deepfake_progress_source.psd filter=lfs diff=lfs merge=lfs -text
37
+ doc/deepfake_progress.png filter=lfs diff=lfs merge=lfs -text
38
+ merger/gfx/help_merger_face_avatar_source.psd filter=lfs diff=lfs merge=lfs -text
39
+ merger/gfx/help_merger_masked_source.psd filter=lfs diff=lfs merge=lfs -text
CODEGUIDELINES ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ Please don't ruin the code and this good (as I think) architecture.
2
+
3
+ Please follow the same logic and brevity/pithiness.
4
+
5
+ Don't abstract the code into huge classes if you only win some lines of code in one place, because this can prevent programmers from understanding it quickly.
DFLIMG/DFLIMG.py ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pathlib import Path
2
+
3
+ from .DFLJPG import DFLJPG
4
+
5
+ class DFLIMG():
6
+
7
+ @staticmethod
8
+ def load(filepath, loader_func=None):
9
+ if filepath.suffix == '.jpg':
10
+ return DFLJPG.load ( str(filepath), loader_func=loader_func )
11
+ else:
12
+ return None
DFLIMG/DFLJPG.py ADDED
@@ -0,0 +1,324 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pickle
2
+ import struct
3
+ import traceback
4
+
5
+ import cv2
6
+ import numpy as np
7
+
8
+ from core import imagelib
9
+ from core.cv2ex import *
10
+ from core.imagelib import SegIEPolys
11
+ from core.interact import interact as io
12
+ from core.structex import *
13
+ from facelib import FaceType
14
+
15
+
16
+ class DFLJPG(object):
17
+ def __init__(self, filename):
18
+ self.filename = filename
19
+ self.data = b""
20
+ self.length = 0
21
+ self.chunks = []
22
+ self.dfl_dict = None
23
+ self.shape = None
24
+ self.img = None
25
+
26
+ @staticmethod
27
+ def load_raw(filename, loader_func=None):
28
+ try:
29
+ if loader_func is not None:
30
+ data = loader_func(filename)
31
+ else:
32
+ with open(filename, "rb") as f:
33
+ data = f.read()
34
+ except:
35
+ raise FileNotFoundError(filename)
36
+
37
+ try:
38
+ inst = DFLJPG(filename)
39
+ inst.data = data
40
+ inst.length = len(data)
41
+ inst_length = inst.length
42
+ chunks = []
43
+ data_counter = 0
44
+ while data_counter < inst_length:
45
+ chunk_m_l, chunk_m_h = struct.unpack ("BB", data[data_counter:data_counter+2])
46
+ data_counter += 2
47
+
48
+ if chunk_m_l != 0xFF:
49
+ raise ValueError(f"No Valid JPG info in {filename}")
50
+
51
+ chunk_name = None
52
+ chunk_size = None
53
+ chunk_data = None
54
+ chunk_ex_data = None
55
+ is_unk_chunk = False
56
+
57
+ if chunk_m_h & 0xF0 == 0xD0:
58
+ n = chunk_m_h & 0x0F
59
+
60
+ if n >= 0 and n <= 7:
61
+ chunk_name = "RST%d" % (n)
62
+ chunk_size = 0
63
+ elif n == 0x8:
64
+ chunk_name = "SOI"
65
+ chunk_size = 0
66
+ if len(chunks) != 0:
67
+ raise Exception("")
68
+ elif n == 0x9:
69
+ chunk_name = "EOI"
70
+ chunk_size = 0
71
+ elif n == 0xA:
72
+ chunk_name = "SOS"
73
+ elif n == 0xB:
74
+ chunk_name = "DQT"
75
+ elif n == 0xD:
76
+ chunk_name = "DRI"
77
+ chunk_size = 2
78
+ else:
79
+ is_unk_chunk = True
80
+ elif chunk_m_h & 0xF0 == 0xC0:
81
+ n = chunk_m_h & 0x0F
82
+ if n == 0:
83
+ chunk_name = "SOF0"
84
+ elif n == 2:
85
+ chunk_name = "SOF2"
86
+ elif n == 4:
87
+ chunk_name = "DHT"
88
+ else:
89
+ is_unk_chunk = True
90
+ elif chunk_m_h & 0xF0 == 0xE0:
91
+ n = chunk_m_h & 0x0F
92
+ chunk_name = "APP%d" % (n)
93
+ else:
94
+ is_unk_chunk = True
95
+
96
+ #if is_unk_chunk:
97
+ # #raise ValueError(f"Unknown chunk {chunk_m_h} in {filename}")
98
+ # io.log_info(f"Unknown chunk {chunk_m_h} in {filename}")
99
+
100
+ if chunk_size == None: #variable size
101
+ chunk_size, = struct.unpack (">H", data[data_counter:data_counter+2])
102
+ chunk_size -= 2
103
+ data_counter += 2
104
+
105
+ if chunk_size > 0:
106
+ chunk_data = data[data_counter:data_counter+chunk_size]
107
+ data_counter += chunk_size
108
+
109
+ if chunk_name == "SOS":
110
+ c = data_counter
111
+ while c < inst_length and (data[c] != 0xFF or data[c+1] != 0xD9):
112
+ c += 1
113
+
114
+ chunk_ex_data = data[data_counter:c]
115
+ data_counter = c
116
+
117
+ chunks.append ({'name' : chunk_name,
118
+ 'm_h' : chunk_m_h,
119
+ 'data' : chunk_data,
120
+ 'ex_data' : chunk_ex_data,
121
+ })
122
+ inst.chunks = chunks
123
+
124
+ return inst
125
+ except Exception as e:
126
+ raise Exception (f"Corrupted JPG file {filename} {e}")
127
+
128
+ @staticmethod
129
+ def load(filename, loader_func=None):
130
+ try:
131
+ inst = DFLJPG.load_raw (filename, loader_func=loader_func)
132
+ inst.dfl_dict = {}
133
+
134
+ for chunk in inst.chunks:
135
+ if chunk['name'] == 'APP0':
136
+ d, c = chunk['data'], 0
137
+ c, id, _ = struct_unpack (d, c, "=4sB")
138
+
139
+ if id == b"JFIF":
140
+ c, ver_major, ver_minor, units, Xdensity, Ydensity, Xthumbnail, Ythumbnail = struct_unpack (d, c, "=BBBHHBB")
141
+ else:
142
+ raise Exception("Unknown jpeg ID: %s" % (id) )
143
+ elif chunk['name'] == 'SOF0' or chunk['name'] == 'SOF2':
144
+ d, c = chunk['data'], 0
145
+ c, precision, height, width = struct_unpack (d, c, ">BHH")
146
+ inst.shape = (height, width, 3)
147
+
148
+ elif chunk['name'] == 'APP15':
149
+ if type(chunk['data']) == bytes:
150
+ inst.dfl_dict = pickle.loads(chunk['data'])
151
+
152
+ return inst
153
+ except Exception as e:
154
+ io.log_err (f'Exception occured while DFLJPG.load : {traceback.format_exc()}')
155
+ return None
156
+
157
+ def has_data(self):
158
+ return len(self.dfl_dict.keys()) != 0
159
+
160
+ def save(self):
161
+ try:
162
+ with open(self.filename, "wb") as f:
163
+ f.write ( self.dump() )
164
+ except:
165
+ raise Exception( f'cannot save {self.filename}' )
166
+
167
+ def dump(self):
168
+ data = b""
169
+
170
+ dict_data = self.dfl_dict
171
+
172
+ # Remove None keys
173
+ for key in list(dict_data.keys()):
174
+ if dict_data[key] is None:
175
+ dict_data.pop(key)
176
+
177
+ for chunk in self.chunks:
178
+ if chunk['name'] == 'APP15':
179
+ self.chunks.remove(chunk)
180
+ break
181
+
182
+ last_app_chunk = 0
183
+ for i, chunk in enumerate (self.chunks):
184
+ if chunk['m_h'] & 0xF0 == 0xE0:
185
+ last_app_chunk = i
186
+
187
+ dflchunk = {'name' : 'APP15',
188
+ 'm_h' : 0xEF,
189
+ 'data' : pickle.dumps(dict_data),
190
+ 'ex_data' : None,
191
+ }
192
+ self.chunks.insert (last_app_chunk+1, dflchunk)
193
+
194
+
195
+ for chunk in self.chunks:
196
+ data += struct.pack ("BB", 0xFF, chunk['m_h'] )
197
+ chunk_data = chunk['data']
198
+ if chunk_data is not None:
199
+ data += struct.pack (">H", len(chunk_data)+2 )
200
+ data += chunk_data
201
+
202
+ chunk_ex_data = chunk['ex_data']
203
+ if chunk_ex_data is not None:
204
+ data += chunk_ex_data
205
+
206
+ return data
207
+
208
+ def get_img(self):
209
+ if self.img is None:
210
+ self.img = cv2_imread(self.filename)
211
+ return self.img
212
+
213
+ def get_shape(self):
214
+ if self.shape is None:
215
+ img = self.get_img()
216
+ if img is not None:
217
+ self.shape = img.shape
218
+ return self.shape
219
+
220
+ def get_height(self):
221
+ for chunk in self.chunks:
222
+ if type(chunk) == IHDR:
223
+ return chunk.height
224
+ return 0
225
+
226
+ def get_dict(self):
227
+ return self.dfl_dict
228
+
229
+ def set_dict (self, dict_data=None):
230
+ self.dfl_dict = dict_data
231
+
232
+ def get_face_type(self): return self.dfl_dict.get('face_type', FaceType.toString (FaceType.FULL) )
233
+ def set_face_type(self, face_type): self.dfl_dict['face_type'] = face_type
234
+
235
+ def get_landmarks(self): return np.array ( self.dfl_dict['landmarks'] )
236
+ def set_landmarks(self, landmarks): self.dfl_dict['landmarks'] = landmarks
237
+
238
+ def get_eyebrows_expand_mod(self): return self.dfl_dict.get ('eyebrows_expand_mod', 1.0)
239
+ def set_eyebrows_expand_mod(self, eyebrows_expand_mod): self.dfl_dict['eyebrows_expand_mod'] = eyebrows_expand_mod
240
+
241
+ def get_source_filename(self): return self.dfl_dict.get ('source_filename', None)
242
+ def set_source_filename(self, source_filename): self.dfl_dict['source_filename'] = source_filename
243
+
244
+ def get_source_rect(self): return self.dfl_dict.get ('source_rect', None)
245
+ def set_source_rect(self, source_rect): self.dfl_dict['source_rect'] = source_rect
246
+
247
+ def get_source_landmarks(self): return np.array ( self.dfl_dict.get('source_landmarks', None) )
248
+ def set_source_landmarks(self, source_landmarks): self.dfl_dict['source_landmarks'] = source_landmarks
249
+
250
+ def get_image_to_face_mat(self):
251
+ mat = self.dfl_dict.get ('image_to_face_mat', None)
252
+ if mat is not None:
253
+ return np.array (mat)
254
+ return None
255
+ def set_image_to_face_mat(self, image_to_face_mat): self.dfl_dict['image_to_face_mat'] = image_to_face_mat
256
+
257
+ def has_seg_ie_polys(self):
258
+ return self.dfl_dict.get('seg_ie_polys',None) is not None
259
+
260
+ def get_seg_ie_polys(self):
261
+ d = self.dfl_dict.get('seg_ie_polys',None)
262
+ if d is not None:
263
+ d = SegIEPolys.load(d)
264
+ else:
265
+ d = SegIEPolys()
266
+
267
+ return d
268
+
269
+ def set_seg_ie_polys(self, seg_ie_polys):
270
+ if seg_ie_polys is not None:
271
+ if not isinstance(seg_ie_polys, SegIEPolys):
272
+ raise ValueError('seg_ie_polys should be instance of SegIEPolys')
273
+
274
+ if seg_ie_polys.has_polys():
275
+ seg_ie_polys = seg_ie_polys.dump()
276
+ else:
277
+ seg_ie_polys = None
278
+
279
+ self.dfl_dict['seg_ie_polys'] = seg_ie_polys
280
+
281
+ def has_xseg_mask(self):
282
+ return self.dfl_dict.get('xseg_mask',None) is not None
283
+
284
+ def get_xseg_mask_compressed(self):
285
+ mask_buf = self.dfl_dict.get('xseg_mask',None)
286
+ if mask_buf is None:
287
+ return None
288
+
289
+ return mask_buf
290
+
291
+ def get_xseg_mask(self):
292
+ mask_buf = self.dfl_dict.get('xseg_mask',None)
293
+ if mask_buf is None:
294
+ return None
295
+
296
+ img = cv2.imdecode(mask_buf, cv2.IMREAD_UNCHANGED)
297
+ if len(img.shape) == 2:
298
+ img = img[...,None]
299
+
300
+ return img.astype(np.float32) / 255.0
301
+
302
+
303
+ def set_xseg_mask(self, mask_a):
304
+ if mask_a is None:
305
+ self.dfl_dict['xseg_mask'] = None
306
+ return
307
+
308
+ mask_a = imagelib.normalize_channels(mask_a, 1)
309
+ img_data = np.clip( mask_a*255, 0, 255 ).astype(np.uint8)
310
+
311
+ data_max_len = 50000
312
+
313
+ ret, buf = cv2.imencode('.png', img_data)
314
+
315
+ if not ret or len(buf) > data_max_len:
316
+ for jpeg_quality in range(100,-1,-1):
317
+ ret, buf = cv2.imencode( '.jpg', img_data, [int(cv2.IMWRITE_JPEG_QUALITY), jpeg_quality] )
318
+ if ret and len(buf) <= data_max_len:
319
+ break
320
+
321
+ if not ret:
322
+ raise Exception("set_xseg_mask: unable to generate image data for set_xseg_mask")
323
+
324
+ self.dfl_dict['xseg_mask'] = buf
DFLIMG/__init__.py ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ from .DFLIMG import DFLIMG
2
+ from .DFLJPG import DFLJPG
LICENSE ADDED
@@ -0,0 +1,674 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ GNU GENERAL PUBLIC LICENSE
2
+ Version 3, 29 June 2007
3
+
4
+ Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
5
+ Everyone is permitted to copy and distribute verbatim copies
6
+ of this license document, but changing it is not allowed.
7
+
8
+ Preamble
9
+
10
+ The GNU General Public License is a free, copyleft license for
11
+ software and other kinds of works.
12
+
13
+ The licenses for most software and other practical works are designed
14
+ to take away your freedom to share and change the works. By contrast,
15
+ the GNU General Public License is intended to guarantee your freedom to
16
+ share and change all versions of a program--to make sure it remains free
17
+ software for all its users. We, the Free Software Foundation, use the
18
+ GNU General Public License for most of our software; it applies also to
19
+ any other work released this way by its authors. You can apply it to
20
+ your programs, too.
21
+
22
+ When we speak of free software, we are referring to freedom, not
23
+ price. Our General Public Licenses are designed to make sure that you
24
+ have the freedom to distribute copies of free software (and charge for
25
+ them if you wish), that you receive source code or can get it if you
26
+ want it, that you can change the software or use pieces of it in new
27
+ free programs, and that you know you can do these things.
28
+
29
+ To protect your rights, we need to prevent others from denying you
30
+ these rights or asking you to surrender the rights. Therefore, you have
31
+ certain responsibilities if you distribute copies of the software, or if
32
+ you modify it: responsibilities to respect the freedom of others.
33
+
34
+ For example, if you distribute copies of such a program, whether
35
+ gratis or for a fee, you must pass on to the recipients the same
36
+ freedoms that you received. You must make sure that they, too, receive
37
+ or can get the source code. And you must show them these terms so they
38
+ know their rights.
39
+
40
+ Developers that use the GNU GPL protect your rights with two steps:
41
+ (1) assert copyright on the software, and (2) offer you this License
42
+ giving you legal permission to copy, distribute and/or modify it.
43
+
44
+ For the developers' and authors' protection, the GPL clearly explains
45
+ that there is no warranty for this free software. For both users' and
46
+ authors' sake, the GPL requires that modified versions be marked as
47
+ changed, so that their problems will not be attributed erroneously to
48
+ authors of previous versions.
49
+
50
+ Some devices are designed to deny users access to install or run
51
+ modified versions of the software inside them, although the manufacturer
52
+ can do so. This is fundamentally incompatible with the aim of
53
+ protecting users' freedom to change the software. The systematic
54
+ pattern of such abuse occurs in the area of products for individuals to
55
+ use, which is precisely where it is most unacceptable. Therefore, we
56
+ have designed this version of the GPL to prohibit the practice for those
57
+ products. If such problems arise substantially in other domains, we
58
+ stand ready to extend this provision to those domains in future versions
59
+ of the GPL, as needed to protect the freedom of users.
60
+
61
+ Finally, every program is threatened constantly by software patents.
62
+ States should not allow patents to restrict development and use of
63
+ software on general-purpose computers, but in those that do, we wish to
64
+ avoid the special danger that patents applied to a free program could
65
+ make it effectively proprietary. To prevent this, the GPL assures that
66
+ patents cannot be used to render the program non-free.
67
+
68
+ The precise terms and conditions for copying, distribution and
69
+ modification follow.
70
+
71
+ TERMS AND CONDITIONS
72
+
73
+ 0. Definitions.
74
+
75
+ "This License" refers to version 3 of the GNU General Public License.
76
+
77
+ "Copyright" also means copyright-like laws that apply to other kinds of
78
+ works, such as semiconductor masks.
79
+
80
+ "The Program" refers to any copyrightable work licensed under this
81
+ License. Each licensee is addressed as "you". "Licensees" and
82
+ "recipients" may be individuals or organizations.
83
+
84
+ To "modify" a work means to copy from or adapt all or part of the work
85
+ in a fashion requiring copyright permission, other than the making of an
86
+ exact copy. The resulting work is called a "modified version" of the
87
+ earlier work or a work "based on" the earlier work.
88
+
89
+ A "covered work" means either the unmodified Program or a work based
90
+ on the Program.
91
+
92
+ To "propagate" a work means to do anything with it that, without
93
+ permission, would make you directly or secondarily liable for
94
+ infringement under applicable copyright law, except executing it on a
95
+ computer or modifying a private copy. Propagation includes copying,
96
+ distribution (with or without modification), making available to the
97
+ public, and in some countries other activities as well.
98
+
99
+ To "convey" a work means any kind of propagation that enables other
100
+ parties to make or receive copies. Mere interaction with a user through
101
+ a computer network, with no transfer of a copy, is not conveying.
102
+
103
+ An interactive user interface displays "Appropriate Legal Notices"
104
+ to the extent that it includes a convenient and prominently visible
105
+ feature that (1) displays an appropriate copyright notice, and (2)
106
+ tells the user that there is no warranty for the work (except to the
107
+ extent that warranties are provided), that licensees may convey the
108
+ work under this License, and how to view a copy of this License. If
109
+ the interface presents a list of user commands or options, such as a
110
+ menu, a prominent item in the list meets this criterion.
111
+
112
+ 1. Source Code.
113
+
114
+ The "source code" for a work means the preferred form of the work
115
+ for making modifications to it. "Object code" means any non-source
116
+ form of a work.
117
+
118
+ A "Standard Interface" means an interface that either is an official
119
+ standard defined by a recognized standards body, or, in the case of
120
+ interfaces specified for a particular programming language, one that
121
+ is widely used among developers working in that language.
122
+
123
+ The "System Libraries" of an executable work include anything, other
124
+ than the work as a whole, that (a) is included in the normal form of
125
+ packaging a Major Component, but which is not part of that Major
126
+ Component, and (b) serves only to enable use of the work with that
127
+ Major Component, or to implement a Standard Interface for which an
128
+ implementation is available to the public in source code form. A
129
+ "Major Component", in this context, means a major essential component
130
+ (kernel, window system, and so on) of the specific operating system
131
+ (if any) on which the executable work runs, or a compiler used to
132
+ produce the work, or an object code interpreter used to run it.
133
+
134
+ The "Corresponding Source" for a work in object code form means all
135
+ the source code needed to generate, install, and (for an executable
136
+ work) run the object code and to modify the work, including scripts to
137
+ control those activities. However, it does not include the work's
138
+ System Libraries, or general-purpose tools or generally available free
139
+ programs which are used unmodified in performing those activities but
140
+ which are not part of the work. For example, Corresponding Source
141
+ includes interface definition files associated with source files for
142
+ the work, and the source code for shared libraries and dynamically
143
+ linked subprograms that the work is specifically designed to require,
144
+ such as by intimate data communication or control flow between those
145
+ subprograms and other parts of the work.
146
+
147
+ The Corresponding Source need not include anything that users
148
+ can regenerate automatically from other parts of the Corresponding
149
+ Source.
150
+
151
+ The Corresponding Source for a work in source code form is that
152
+ same work.
153
+
154
+ 2. Basic Permissions.
155
+
156
+ All rights granted under this License are granted for the term of
157
+ copyright on the Program, and are irrevocable provided the stated
158
+ conditions are met. This License explicitly affirms your unlimited
159
+ permission to run the unmodified Program. The output from running a
160
+ covered work is covered by this License only if the output, given its
161
+ content, constitutes a covered work. This License acknowledges your
162
+ rights of fair use or other equivalent, as provided by copyright law.
163
+
164
+ You may make, run and propagate covered works that you do not
165
+ convey, without conditions so long as your license otherwise remains
166
+ in force. You may convey covered works to others for the sole purpose
167
+ of having them make modifications exclusively for you, or provide you
168
+ with facilities for running those works, provided that you comply with
169
+ the terms of this License in conveying all material for which you do
170
+ not control copyright. Those thus making or running the covered works
171
+ for you must do so exclusively on your behalf, under your direction
172
+ and control, on terms that prohibit them from making any copies of
173
+ your copyrighted material outside their relationship with you.
174
+
175
+ Conveying under any other circumstances is permitted solely under
176
+ the conditions stated below. Sublicensing is not allowed; section 10
177
+ makes it unnecessary.
178
+
179
+ 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
180
+
181
+ No covered work shall be deemed part of an effective technological
182
+ measure under any applicable law fulfilling obligations under article
183
+ 11 of the WIPO copyright treaty adopted on 20 December 1996, or
184
+ similar laws prohibiting or restricting circumvention of such
185
+ measures.
186
+
187
+ When you convey a covered work, you waive any legal power to forbid
188
+ circumvention of technological measures to the extent such circumvention
189
+ is effected by exercising rights under this License with respect to
190
+ the covered work, and you disclaim any intention to limit operation or
191
+ modification of the work as a means of enforcing, against the work's
192
+ users, your or third parties' legal rights to forbid circumvention of
193
+ technological measures.
194
+
195
+ 4. Conveying Verbatim Copies.
196
+
197
+ You may convey verbatim copies of the Program's source code as you
198
+ receive it, in any medium, provided that you conspicuously and
199
+ appropriately publish on each copy an appropriate copyright notice;
200
+ keep intact all notices stating that this License and any
201
+ non-permissive terms added in accord with section 7 apply to the code;
202
+ keep intact all notices of the absence of any warranty; and give all
203
+ recipients a copy of this License along with the Program.
204
+
205
+ You may charge any price or no price for each copy that you convey,
206
+ and you may offer support or warranty protection for a fee.
207
+
208
+ 5. Conveying Modified Source Versions.
209
+
210
+ You may convey a work based on the Program, or the modifications to
211
+ produce it from the Program, in the form of source code under the
212
+ terms of section 4, provided that you also meet all of these conditions:
213
+
214
+ a) The work must carry prominent notices stating that you modified
215
+ it, and giving a relevant date.
216
+
217
+ b) The work must carry prominent notices stating that it is
218
+ released under this License and any conditions added under section
219
+ 7. This requirement modifies the requirement in section 4 to
220
+ "keep intact all notices".
221
+
222
+ c) You must license the entire work, as a whole, under this
223
+ License to anyone who comes into possession of a copy. This
224
+ License will therefore apply, along with any applicable section 7
225
+ additional terms, to the whole of the work, and all its parts,
226
+ regardless of how they are packaged. This License gives no
227
+ permission to license the work in any other way, but it does not
228
+ invalidate such permission if you have separately received it.
229
+
230
+ d) If the work has interactive user interfaces, each must display
231
+ Appropriate Legal Notices; however, if the Program has interactive
232
+ interfaces that do not display Appropriate Legal Notices, your
233
+ work need not make them do so.
234
+
235
+ A compilation of a covered work with other separate and independent
236
+ works, which are not by their nature extensions of the covered work,
237
+ and which are not combined with it such as to form a larger program,
238
+ in or on a volume of a storage or distribution medium, is called an
239
+ "aggregate" if the compilation and its resulting copyright are not
240
+ used to limit the access or legal rights of the compilation's users
241
+ beyond what the individual works permit. Inclusion of a covered work
242
+ in an aggregate does not cause this License to apply to the other
243
+ parts of the aggregate.
244
+
245
+ 6. Conveying Non-Source Forms.
246
+
247
+ You may convey a covered work in object code form under the terms
248
+ of sections 4 and 5, provided that you also convey the
249
+ machine-readable Corresponding Source under the terms of this License,
250
+ in one of these ways:
251
+
252
+ a) Convey the object code in, or embodied in, a physical product
253
+ (including a physical distribution medium), accompanied by the
254
+ Corresponding Source fixed on a durable physical medium
255
+ customarily used for software interchange.
256
+
257
+ b) Convey the object code in, or embodied in, a physical product
258
+ (including a physical distribution medium), accompanied by a
259
+ written offer, valid for at least three years and valid for as
260
+ long as you offer spare parts or customer support for that product
261
+ model, to give anyone who possesses the object code either (1) a
262
+ copy of the Corresponding Source for all the software in the
263
+ product that is covered by this License, on a durable physical
264
+ medium customarily used for software interchange, for a price no
265
+ more than your reasonable cost of physically performing this
266
+ conveying of source, or (2) access to copy the
267
+ Corresponding Source from a network server at no charge.
268
+
269
+ c) Convey individual copies of the object code with a copy of the
270
+ written offer to provide the Corresponding Source. This
271
+ alternative is allowed only occasionally and noncommercially, and
272
+ only if you received the object code with such an offer, in accord
273
+ with subsection 6b.
274
+
275
+ d) Convey the object code by offering access from a designated
276
+ place (gratis or for a charge), and offer equivalent access to the
277
+ Corresponding Source in the same way through the same place at no
278
+ further charge. You need not require recipients to copy the
279
+ Corresponding Source along with the object code. If the place to
280
+ copy the object code is a network server, the Corresponding Source
281
+ may be on a different server (operated by you or a third party)
282
+ that supports equivalent copying facilities, provided you maintain
283
+ clear directions next to the object code saying where to find the
284
+ Corresponding Source. Regardless of what server hosts the
285
+ Corresponding Source, you remain obligated to ensure that it is
286
+ available for as long as needed to satisfy these requirements.
287
+
288
+ e) Convey the object code using peer-to-peer transmission, provided
289
+ you inform other peers where the object code and Corresponding
290
+ Source of the work are being offered to the general public at no
291
+ charge under subsection 6d.
292
+
293
+ A separable portion of the object code, whose source code is excluded
294
+ from the Corresponding Source as a System Library, need not be
295
+ included in conveying the object code work.
296
+
297
+ A "User Product" is either (1) a "consumer product", which means any
298
+ tangible personal property which is normally used for personal, family,
299
+ or household purposes, or (2) anything designed or sold for incorporation
300
+ into a dwelling. In determining whether a product is a consumer product,
301
+ doubtful cases shall be resolved in favor of coverage. For a particular
302
+ product received by a particular user, "normally used" refers to a
303
+ typical or common use of that class of product, regardless of the status
304
+ of the particular user or of the way in which the particular user
305
+ actually uses, or expects or is expected to use, the product. A product
306
+ is a consumer product regardless of whether the product has substantial
307
+ commercial, industrial or non-consumer uses, unless such uses represent
308
+ the only significant mode of use of the product.
309
+
310
+ "Installation Information" for a User Product means any methods,
311
+ procedures, authorization keys, or other information required to install
312
+ and execute modified versions of a covered work in that User Product from
313
+ a modified version of its Corresponding Source. The information must
314
+ suffice to ensure that the continued functioning of the modified object
315
+ code is in no case prevented or interfered with solely because
316
+ modification has been made.
317
+
318
+ If you convey an object code work under this section in, or with, or
319
+ specifically for use in, a User Product, and the conveying occurs as
320
+ part of a transaction in which the right of possession and use of the
321
+ User Product is transferred to the recipient in perpetuity or for a
322
+ fixed term (regardless of how the transaction is characterized), the
323
+ Corresponding Source conveyed under this section must be accompanied
324
+ by the Installation Information. But this requirement does not apply
325
+ if neither you nor any third party retains the ability to install
326
+ modified object code on the User Product (for example, the work has
327
+ been installed in ROM).
328
+
329
+ The requirement to provide Installation Information does not include a
330
+ requirement to continue to provide support service, warranty, or updates
331
+ for a work that has been modified or installed by the recipient, or for
332
+ the User Product in which it has been modified or installed. Access to a
333
+ network may be denied when the modification itself materially and
334
+ adversely affects the operation of the network or violates the rules and
335
+ protocols for communication across the network.
336
+
337
+ Corresponding Source conveyed, and Installation Information provided,
338
+ in accord with this section must be in a format that is publicly
339
+ documented (and with an implementation available to the public in
340
+ source code form), and must require no special password or key for
341
+ unpacking, reading or copying.
342
+
343
+ 7. Additional Terms.
344
+
345
+ "Additional permissions" are terms that supplement the terms of this
346
+ License by making exceptions from one or more of its conditions.
347
+ Additional permissions that are applicable to the entire Program shall
348
+ be treated as though they were included in this License, to the extent
349
+ that they are valid under applicable law. If additional permissions
350
+ apply only to part of the Program, that part may be used separately
351
+ under those permissions, but the entire Program remains governed by
352
+ this License without regard to the additional permissions.
353
+
354
+ When you convey a copy of a covered work, you may at your option
355
+ remove any additional permissions from that copy, or from any part of
356
+ it. (Additional permissions may be written to require their own
357
+ removal in certain cases when you modify the work.) You may place
358
+ additional permissions on material, added by you to a covered work,
359
+ for which you have or can give appropriate copyright permission.
360
+
361
+ Notwithstanding any other provision of this License, for material you
362
+ add to a covered work, you may (if authorized by the copyright holders of
363
+ that material) supplement the terms of this License with terms:
364
+
365
+ a) Disclaiming warranty or limiting liability differently from the
366
+ terms of sections 15 and 16 of this License; or
367
+
368
+ b) Requiring preservation of specified reasonable legal notices or
369
+ author attributions in that material or in the Appropriate Legal
370
+ Notices displayed by works containing it; or
371
+
372
+ c) Prohibiting misrepresentation of the origin of that material, or
373
+ requiring that modified versions of such material be marked in
374
+ reasonable ways as different from the original version; or
375
+
376
+ d) Limiting the use for publicity purposes of names of licensors or
377
+ authors of the material; or
378
+
379
+ e) Declining to grant rights under trademark law for use of some
380
+ trade names, trademarks, or service marks; or
381
+
382
+ f) Requiring indemnification of licensors and authors of that
383
+ material by anyone who conveys the material (or modified versions of
384
+ it) with contractual assumptions of liability to the recipient, for
385
+ any liability that these contractual assumptions directly impose on
386
+ those licensors and authors.
387
+
388
+ All other non-permissive additional terms are considered "further
389
+ restrictions" within the meaning of section 10. If the Program as you
390
+ received it, or any part of it, contains a notice stating that it is
391
+ governed by this License along with a term that is a further
392
+ restriction, you may remove that term. If a license document contains
393
+ a further restriction but permits relicensing or conveying under this
394
+ License, you may add to a covered work material governed by the terms
395
+ of that license document, provided that the further restriction does
396
+ not survive such relicensing or conveying.
397
+
398
+ If you add terms to a covered work in accord with this section, you
399
+ must place, in the relevant source files, a statement of the
400
+ additional terms that apply to those files, or a notice indicating
401
+ where to find the applicable terms.
402
+
403
+ Additional terms, permissive or non-permissive, may be stated in the
404
+ form of a separately written license, or stated as exceptions;
405
+ the above requirements apply either way.
406
+
407
+ 8. Termination.
408
+
409
+ You may not propagate or modify a covered work except as expressly
410
+ provided under this License. Any attempt otherwise to propagate or
411
+ modify it is void, and will automatically terminate your rights under
412
+ this License (including any patent licenses granted under the third
413
+ paragraph of section 11).
414
+
415
+ However, if you cease all violation of this License, then your
416
+ license from a particular copyright holder is reinstated (a)
417
+ provisionally, unless and until the copyright holder explicitly and
418
+ finally terminates your license, and (b) permanently, if the copyright
419
+ holder fails to notify you of the violation by some reasonable means
420
+ prior to 60 days after the cessation.
421
+
422
+ Moreover, your license from a particular copyright holder is
423
+ reinstated permanently if the copyright holder notifies you of the
424
+ violation by some reasonable means, this is the first time you have
425
+ received notice of violation of this License (for any work) from that
426
+ copyright holder, and you cure the violation prior to 30 days after
427
+ your receipt of the notice.
428
+
429
+ Termination of your rights under this section does not terminate the
430
+ licenses of parties who have received copies or rights from you under
431
+ this License. If your rights have been terminated and not permanently
432
+ reinstated, you do not qualify to receive new licenses for the same
433
+ material under section 10.
434
+
435
+ 9. Acceptance Not Required for Having Copies.
436
+
437
+ You are not required to accept this License in order to receive or
438
+ run a copy of the Program. Ancillary propagation of a covered work
439
+ occurring solely as a consequence of using peer-to-peer transmission
440
+ to receive a copy likewise does not require acceptance. However,
441
+ nothing other than this License grants you permission to propagate or
442
+ modify any covered work. These actions infringe copyright if you do
443
+ not accept this License. Therefore, by modifying or propagating a
444
+ covered work, you indicate your acceptance of this License to do so.
445
+
446
+ 10. Automatic Licensing of Downstream Recipients.
447
+
448
+ Each time you convey a covered work, the recipient automatically
449
+ receives a license from the original licensors, to run, modify and
450
+ propagate that work, subject to this License. You are not responsible
451
+ for enforcing compliance by third parties with this License.
452
+
453
+ An "entity transaction" is a transaction transferring control of an
454
+ organization, or substantially all assets of one, or subdividing an
455
+ organization, or merging organizations. If propagation of a covered
456
+ work results from an entity transaction, each party to that
457
+ transaction who receives a copy of the work also receives whatever
458
+ licenses to the work the party's predecessor in interest had or could
459
+ give under the previous paragraph, plus a right to possession of the
460
+ Corresponding Source of the work from the predecessor in interest, if
461
+ the predecessor has it or can get it with reasonable efforts.
462
+
463
+ You may not impose any further restrictions on the exercise of the
464
+ rights granted or affirmed under this License. For example, you may
465
+ not impose a license fee, royalty, or other charge for exercise of
466
+ rights granted under this License, and you may not initiate litigation
467
+ (including a cross-claim or counterclaim in a lawsuit) alleging that
468
+ any patent claim is infringed by making, using, selling, offering for
469
+ sale, or importing the Program or any portion of it.
470
+
471
+ 11. Patents.
472
+
473
+ A "contributor" is a copyright holder who authorizes use under this
474
+ License of the Program or a work on which the Program is based. The
475
+ work thus licensed is called the contributor's "contributor version".
476
+
477
+ A contributor's "essential patent claims" are all patent claims
478
+ owned or controlled by the contributor, whether already acquired or
479
+ hereafter acquired, that would be infringed by some manner, permitted
480
+ by this License, of making, using, or selling its contributor version,
481
+ but do not include claims that would be infringed only as a
482
+ consequence of further modification of the contributor version. For
483
+ purposes of this definition, "control" includes the right to grant
484
+ patent sublicenses in a manner consistent with the requirements of
485
+ this License.
486
+
487
+ Each contributor grants you a non-exclusive, worldwide, royalty-free
488
+ patent license under the contributor's essential patent claims, to
489
+ make, use, sell, offer for sale, import and otherwise run, modify and
490
+ propagate the contents of its contributor version.
491
+
492
+ In the following three paragraphs, a "patent license" is any express
493
+ agreement or commitment, however denominated, not to enforce a patent
494
+ (such as an express permission to practice a patent or covenant not to
495
+ sue for patent infringement). To "grant" such a patent license to a
496
+ party means to make such an agreement or commitment not to enforce a
497
+ patent against the party.
498
+
499
+ If you convey a covered work, knowingly relying on a patent license,
500
+ and the Corresponding Source of the work is not available for anyone
501
+ to copy, free of charge and under the terms of this License, through a
502
+ publicly available network server or other readily accessible means,
503
+ then you must either (1) cause the Corresponding Source to be so
504
+ available, or (2) arrange to deprive yourself of the benefit of the
505
+ patent license for this particular work, or (3) arrange, in a manner
506
+ consistent with the requirements of this License, to extend the patent
507
+ license to downstream recipients. "Knowingly relying" means you have
508
+ actual knowledge that, but for the patent license, your conveying the
509
+ covered work in a country, or your recipient's use of the covered work
510
+ in a country, would infringe one or more identifiable patents in that
511
+ country that you have reason to believe are valid.
512
+
513
+ If, pursuant to or in connection with a single transaction or
514
+ arrangement, you convey, or propagate by procuring conveyance of, a
515
+ covered work, and grant a patent license to some of the parties
516
+ receiving the covered work authorizing them to use, propagate, modify
517
+ or convey a specific copy of the covered work, then the patent license
518
+ you grant is automatically extended to all recipients of the covered
519
+ work and works based on it.
520
+
521
+ A patent license is "discriminatory" if it does not include within
522
+ the scope of its coverage, prohibits the exercise of, or is
523
+ conditioned on the non-exercise of one or more of the rights that are
524
+ specifically granted under this License. You may not convey a covered
525
+ work if you are a party to an arrangement with a third party that is
526
+ in the business of distributing software, under which you make payment
527
+ to the third party based on the extent of your activity of conveying
528
+ the work, and under which the third party grants, to any of the
529
+ parties who would receive the covered work from you, a discriminatory
530
+ patent license (a) in connection with copies of the covered work
531
+ conveyed by you (or copies made from those copies), or (b) primarily
532
+ for and in connection with specific products or compilations that
533
+ contain the covered work, unless you entered into that arrangement,
534
+ or that patent license was granted, prior to 28 March 2007.
535
+
536
+ Nothing in this License shall be construed as excluding or limiting
537
+ any implied license or other defenses to infringement that may
538
+ otherwise be available to you under applicable patent law.
539
+
540
+ 12. No Surrender of Others' Freedom.
541
+
542
+ If conditions are imposed on you (whether by court order, agreement or
543
+ otherwise) that contradict the conditions of this License, they do not
544
+ excuse you from the conditions of this License. If you cannot convey a
545
+ covered work so as to satisfy simultaneously your obligations under this
546
+ License and any other pertinent obligations, then as a consequence you may
547
+ not convey it at all. For example, if you agree to terms that obligate you
548
+ to collect a royalty for further conveying from those to whom you convey
549
+ the Program, the only way you could satisfy both those terms and this
550
+ License would be to refrain entirely from conveying the Program.
551
+
552
+ 13. Use with the GNU Affero General Public License.
553
+
554
+ Notwithstanding any other provision of this License, you have
555
+ permission to link or combine any covered work with a work licensed
556
+ under version 3 of the GNU Affero General Public License into a single
557
+ combined work, and to convey the resulting work. The terms of this
558
+ License will continue to apply to the part which is the covered work,
559
+ but the special requirements of the GNU Affero General Public License,
560
+ section 13, concerning interaction through a network will apply to the
561
+ combination as such.
562
+
563
+ 14. Revised Versions of this License.
564
+
565
+ The Free Software Foundation may publish revised and/or new versions of
566
+ the GNU General Public License from time to time. Such new versions will
567
+ be similar in spirit to the present version, but may differ in detail to
568
+ address new problems or concerns.
569
+
570
+ Each version is given a distinguishing version number. If the
571
+ Program specifies that a certain numbered version of the GNU General
572
+ Public License "or any later version" applies to it, you have the
573
+ option of following the terms and conditions either of that numbered
574
+ version or of any later version published by the Free Software
575
+ Foundation. If the Program does not specify a version number of the
576
+ GNU General Public License, you may choose any version ever published
577
+ by the Free Software Foundation.
578
+
579
+ If the Program specifies that a proxy can decide which future
580
+ versions of the GNU General Public License can be used, that proxy's
581
+ public statement of acceptance of a version permanently authorizes you
582
+ to choose that version for the Program.
583
+
584
+ Later license versions may give you additional or different
585
+ permissions. However, no additional obligations are imposed on any
586
+ author or copyright holder as a result of your choosing to follow a
587
+ later version.
588
+
589
+ 15. Disclaimer of Warranty.
590
+
591
+ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
592
+ APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
593
+ HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
594
+ OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
595
+ THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
596
+ PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
597
+ IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
598
+ ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
599
+
600
+ 16. Limitation of Liability.
601
+
602
+ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
603
+ WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
604
+ THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
605
+ GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
606
+ USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
607
+ DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
608
+ PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
609
+ EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
610
+ SUCH DAMAGES.
611
+
612
+ 17. Interpretation of Sections 15 and 16.
613
+
614
+ If the disclaimer of warranty and limitation of liability provided
615
+ above cannot be given local legal effect according to their terms,
616
+ reviewing courts shall apply local law that most closely approximates
617
+ an absolute waiver of all civil liability in connection with the
618
+ Program, unless a warranty or assumption of liability accompanies a
619
+ copy of the Program in return for a fee.
620
+
621
+ END OF TERMS AND CONDITIONS
622
+
623
+ How to Apply These Terms to Your New Programs
624
+
625
+ If you develop a new program, and you want it to be of the greatest
626
+ possible use to the public, the best way to achieve this is to make it
627
+ free software which everyone can redistribute and change under these terms.
628
+
629
+ To do so, attach the following notices to the program. It is safest
630
+ to attach them to the start of each source file to most effectively
631
+ state the exclusion of warranty; and each file should have at least
632
+ the "copyright" line and a pointer to where the full notice is found.
633
+
634
+ <one line to give the program's name and a brief idea of what it does.>
635
+ Copyright (C) <year> <name of author>
636
+
637
+ This program is free software: you can redistribute it and/or modify
638
+ it under the terms of the GNU General Public License as published by
639
+ the Free Software Foundation, either version 3 of the License, or
640
+ (at your option) any later version.
641
+
642
+ This program is distributed in the hope that it will be useful,
643
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
644
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
645
+ GNU General Public License for more details.
646
+
647
+ You should have received a copy of the GNU General Public License
648
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
649
+
650
+ Also add information on how to contact you by electronic and paper mail.
651
+
652
+ If the program does terminal interaction, make it output a short
653
+ notice like this when it starts in an interactive mode:
654
+
655
+ <program> Copyright (C) <year> <name of author>
656
+ This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
657
+ This is free software, and you are welcome to redistribute it
658
+ under certain conditions; type `show c' for details.
659
+
660
+ The hypothetical commands `show w' and `show c' should show the appropriate
661
+ parts of the General Public License. Of course, your program's commands
662
+ might be different; for a GUI interface, you would use an "about box".
663
+
664
+ You should also get your employer (if you work as a programmer) or school,
665
+ if any, to sign a "copyright disclaimer" for the program, if necessary.
666
+ For more information on this, and how to apply and follow the GNU GPL, see
667
+ <http://www.gnu.org/licenses/>.
668
+
669
+ The GNU General Public License does not permit incorporating your program
670
+ into proprietary programs. If your program is a subroutine library, you
671
+ may consider it more useful to permit linking proprietary applications with
672
+ the library. If this is what you want to do, use the GNU Lesser General
673
+ Public License instead of this License. But first, please read
674
+ <http://www.gnu.org/philosophy/why-not-lgpl.html>.
README.md CHANGED
@@ -1,3 +1,429 @@
1
- ---
2
- license: apache-2.0
3
- ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <table align="center" border="0">
2
+
3
+ <tr><td colspan=2 align="center">
4
+
5
+ # DeepFaceLab
6
+
7
+ <a href="https://arxiv.org/abs/2005.05535">
8
+
9
+ <img src="https://static.arxiv.org/static/browse/0.3.0/images/icons/favicon.ico" width=14></img>
10
+ https://arxiv.org/abs/2005.05535</a>
11
+
12
+
13
+ ### the leading software for creating deepfakes
14
+
15
+ <img src="doc/DFL_welcome.png" align="center">
16
+
17
+ </td></tr>
18
+ <tr><td colspan=2 align="center">
19
+
20
+ <p align="center">
21
+
22
+ ![](doc/logo_tensorflow.png)
23
+ ![](doc/logo_cuda.png)
24
+ ![](doc/logo_directx.png)
25
+
26
+ </p>
27
+
28
+ More than 95% of deepfake videos are created with DeepFaceLab.
29
+
30
+ DeepFaceLab is used by such popular youtube channels as
31
+
32
+ |![](doc/tiktok_icon.png) [deeptomcruise](https://www.tiktok.com/@deeptomcruise)|![](doc/tiktok_icon.png) [1facerussia](https://www.tiktok.com/@1facerussia)|![](doc/tiktok_icon.png) [arnoldschwarzneggar](https://www.tiktok.com/@arnoldschwarzneggar)
33
+ |---|---|---|
34
+
35
+ |![](doc/tiktok_icon.png) [mariahcareyathome?](https://www.tiktok.com/@mariahcareyathome?)|![](doc/tiktok_icon.png) [diepnep](https://www.tiktok.com/@diepnep)|![](doc/tiktok_icon.png) [mr__heisenberg](https://www.tiktok.com/@mr__heisenberg)|![](doc/tiktok_icon.png) [deepcaprio](https://www.tiktok.com/@deepcaprio)
36
+ |---|---|---|---|
37
+
38
+ |![](doc/youtube_icon.png) [VFXChris Ume](https://www.youtube.com/channel/UCGf4OlX_aTt8DlrgiH3jN3g/videos)|![](doc/youtube_icon.png) [Sham00k](https://www.youtube.com/channel/UCZXbWcv7fSZFTAZV4beckyw/videos)|
39
+ |---|---|
40
+
41
+ |![](doc/youtube_icon.png) [Collider videos](https://www.youtube.com/watch?v=A91P2qtPT54&list=PLayt6616lBclvOprvrC8qKGCO-mAhPRux)|![](doc/youtube_icon.png) [iFake](https://www.youtube.com/channel/UCC0lK2Zo2BMXX-k1Ks0r7dg/videos)|![](doc/youtube_icon.png) [NextFace](https://www.youtube.com/channel/UCFh3gL0a8BS21g-DHvXZEeQ/videos)|
42
+ |---|---|---|
43
+
44
+ |![](doc/youtube_icon.png) [Futuring Machine](https://www.youtube.com/channel/UCC5BbFxqLQgfnWPhprmQLVg)|![](doc/youtube_icon.png) [RepresentUS](https://www.youtube.com/channel/UCRzgK52MmetD9aG8pDOID3g)|![](doc/youtube_icon.png) [Corridor Crew](https://www.youtube.com/c/corridorcrew/videos)|
45
+ |---|---|---|
46
+
47
+ |![](doc/youtube_icon.png) [DeepFaker](https://www.youtube.com/channel/UCkHecfDTcSazNZSKPEhtPVQ)|![](doc/youtube_icon.png) [DeepFakes in movie](https://www.youtube.com/c/DeepFakesinmovie/videos)|
48
+ |---|---|
49
+
50
+ |![](doc/youtube_icon.png) [DeepFakeCreator](https://www.youtube.com/channel/UCkNFhcYNLQ5hr6A6lZ56mKA)|![](doc/youtube_icon.png) [Jarkan](https://www.youtube.com/user/Jarkancio/videos)|
51
+ |---|---|
52
+
53
+ </td></tr>
54
+
55
+ <tr><td colspan=2 align="center">
56
+
57
+ # What can I do using DeepFaceLab?
58
+
59
+ </td></tr>
60
+ <tr><td colspan=2 align="center">
61
+
62
+ ## Replace the face
63
+
64
+ <img src="doc/replace_the_face.jpg" align="center">
65
+
66
+ </td></tr>
67
+
68
+ <tr><td colspan=2 align="center">
69
+
70
+ ## De-age the face
71
+
72
+ </td></tr>
73
+
74
+ <tr><td align="center" width="50%">
75
+
76
+ <img src="doc/deage_0_1.jpg" align="center">
77
+
78
+ </td>
79
+ <td align="center" width="50%">
80
+
81
+ <img src="doc/deage_0_2.jpg" align="center">
82
+
83
+ </td></tr>
84
+
85
+ <tr><td colspan=2 align="center">
86
+
87
+ ![](doc/youtube_icon.png) https://www.youtube.com/watch?v=Ddx5B-84ebo
88
+
89
+ </td></tr>
90
+
91
+ <tr><td colspan=2 align="center">
92
+
93
+ ## Replace the head
94
+
95
+ </td></tr>
96
+
97
+
98
+ <tr><td align="center" width="50%">
99
+
100
+ <img src="doc/head_replace_0_1.jpg" align="center">
101
+
102
+ </td>
103
+ <td align="center" width="50%">
104
+
105
+ <img src="doc/head_replace_0_2.jpg" align="center">
106
+
107
+ </td></tr>
108
+
109
+ <tr><td colspan=2 align="center">
110
+
111
+ ![](doc/youtube_icon.png) https://www.youtube.com/watch?v=xr5FHd0AdlQ
112
+
113
+ </td></tr>
114
+
115
+ <tr><td align="center" width="50%">
116
+
117
+ <img src="doc/head_replace_1_1.jpg" align="center">
118
+
119
+ </td>
120
+ <td align="center" width="50%">
121
+
122
+ <img src="doc/head_replace_1_2.jpg" align="center">
123
+
124
+ </td></tr>
125
+
126
+ <tr><td colspan=2 align="center">
127
+
128
+ ![](doc/youtube_icon.png) https://www.youtube.com/watch?v=RTjgkhMugVw
129
+
130
+ </td></tr>
131
+
132
+ <tr><td align="center" width="50%">
133
+
134
+ <img src="doc/head_replace_2_1.jpg" align="center">
135
+
136
+ </td>
137
+ <td align="center" width="50%">
138
+
139
+ <img src="doc/head_replace_2_2.jpg" align="center">
140
+
141
+ </td></tr>
142
+
143
+ <tr><td colspan=2 align="center">
144
+
145
+ ![](doc/youtube_icon.png) https://www.youtube.com/watch?v=R9f7WD0gKPo
146
+
147
+ </td></tr>
148
+
149
+
150
+ <tr><td colspan=2 align="center">
151
+
152
+ ## Manipulate politicians lips
153
+ (voice replacement is not included!)
154
+ (also requires a skill in video editors such as *Adobe After Effects* or *Davinci Resolve*)
155
+
156
+ <img src="doc/political_speech2.jpg" align="center">
157
+
158
+ ![](doc/youtube_icon.png) https://www.youtube.com/watch?v=IvY-Abd2FfM
159
+
160
+ <img src="doc/political_speech3.jpg" align="center">
161
+
162
+ ![](doc/youtube_icon.png) https://www.youtube.com/watch?v=ERQlaJ_czHU
163
+
164
+ </td></tr>
165
+ <tr><td colspan=2 align="center">
166
+
167
+ # Deepfake native resolution progress
168
+
169
+ </td></tr>
170
+ <tr><td colspan=2 align="center">
171
+
172
+ <img src="doc/deepfake_progress.png" align="center">
173
+
174
+ </td></tr>
175
+ <tr><td colspan=2 align="center">
176
+
177
+ <img src="doc/make_everything_ok.png" align="center">
178
+
179
+ Unfortunately, there is no "make everything ok" button in DeepFaceLab. You should spend time studying the workflow and growing your skills. A skill in programs such as *AfterEffects* or *Davinci Resolve* is also desirable.
180
+
181
+ </td></tr>
182
+ <tr><td colspan=2 align="center">
183
+
184
+ ## Mini tutorial
185
+
186
+ <a href="https://www.youtube.com/watch?v=kOIMXt8KK8M">
187
+
188
+ <img src="doc/mini_tutorial.jpg" align="center">
189
+
190
+ </a>
191
+
192
+ </td></tr>
193
+ <tr><td colspan=2 align="center">
194
+
195
+ ## Releases
196
+
197
+ </td></tr>
198
+
199
+ <tr><td align="right">
200
+ <a href="https://tinyurl.com/4vwvjuen">Windows (magnet link)</a>
201
+ </td><td align="center">Last release. Use torrent client to download.</td></tr>
202
+
203
+ <tr><td align="right">
204
+ <a href="https://mega.nz/folder/Po0nGQrA#dbbttiNWojCt8jzD4xYaPw">Windows (Mega.nz)</a>
205
+ </td><td align="center">Contains new and prev releases.</td></tr>
206
+
207
+ <tr><td align="right">
208
+ <a href="https://github.com/chervonij/DFL-Colab">Google Colab (github)</a>
209
+ </td><td align="center">by @chervonij . You can train fakes for free using Google Colab.</td></tr>
210
+
211
+ <tr><td align="right">
212
+ <a href="https://github.com/nagadit/DeepFaceLab_Linux">Linux (github)</a>
213
+ </td><td align="center">by @nagadit</td></tr>
214
+
215
+ <tr><td align="right">
216
+ <a href="https://github.com/elemantalcode/dfl">CentOS Linux (github)</a>
217
+ </td><td align="center">May be outdated. By @elemantalcode</td></tr>
218
+
219
+ </table>
220
+
221
+ <table align="center" border="0">
222
+
223
+ <tr><td colspan=2 align="center">
224
+
225
+ ## Links
226
+
227
+ </td></tr>
228
+
229
+ <tr><td colspan=2 align="center">
230
+
231
+ ### Guides and tutorials
232
+
233
+ </td></tr>
234
+
235
+ <tr><td align="right">
236
+ <a href="https://mrdeepfakes.com/forums/thread-guide-deepfacelab-2-0-guide">DeepFaceLab guide</a>
237
+ </td><td align="center">Main guide</td></tr>
238
+
239
+ <tr><td align="right">
240
+ <a href="https://mrdeepfakes.com/forums/thread-guide-deepfacelab-2-0-guide?pid=18459#pid18459">Faceset creation guide</a>
241
+ </td><td align="center">How to create the right faceset</td></tr>
242
+
243
+ <tr><td align="right">
244
+ <a href="https://mrdeepfakes.com/forums/thread-guide-deepfacelab-google-colab-tutorial">Google Colab guide</a>
245
+ </td><td align="center">Guide how to train the fake on Google Colab</td></tr>
246
+
247
+ <tr><td align="right">
248
+ <a href="https://mrdeepfakes.com/forums/thread-deepfacelab-2-0-compositing-in-davinci-resolve-vegas-pro-and-after-effects">Compositing</a>
249
+ </td><td align="center">To achieve the highest quality, compose deepfake manually in video editors such as Davinci Resolve or Adobe AfterEffects</td></tr>
250
+
251
+ <tr><td align="right">
252
+ <a href="https://mrdeepfakes.com/forums/thread-deepfacelab-2-0-discussion-tips-suggestions">Discussion and suggestions</a>
253
+ </td><td align="center"></td></tr>
254
+
255
+ <tr><td colspan=2 align="center">
256
+
257
+ ### Supplementary material
258
+
259
+ </td></tr>
260
+
261
+ <tr><td align="right">
262
+ <a href="https://mrdeepfakes.com/forums/forum-celebrity-facesets">Ready to work facesets</a>
263
+ </td><td align="center">Celebrity facesets made by community</td></tr>
264
+
265
+ <tr><td align="right">
266
+ <a href="https://mrdeepfakes.com/forums/forum-trained-models">Pretrained models</a>
267
+ </td><td align="center">Pretrained models made by community</td></tr>
268
+
269
+ <tr><td colspan=2 align="center">
270
+
271
+ ### Communication groups
272
+
273
+ </td></tr>
274
+
275
+ <tr><td align="right">
276
+ <a href="https://discord.gg/S2h7kPySQp">Discord</a>
277
+ </td><td align="center">Official discord channel. English / Russian.</td></tr>
278
+
279
+ <tr><td align="right">
280
+ <a href="https://t.me/joinchat/ElkhqlgJ0I5HhdJyFar80w">Telegram group</a>
281
+ </td><td align="center">Official telegram group. English / Russian. For anonymous communication. Don't forget to hide your phone number</td></tr>
282
+
283
+ <tr><td align="right">
284
+ <a href="https://mrdeepfakes.com/forums/forum-russian-community">Русский форум</a>
285
+ </td><td align="center"></td></tr>
286
+
287
+ <tr><td align="right">
288
+ <a href="https://mrdeepfakes.com/forums/">mrdeepfakes</a>
289
+ </td><td align="center">the biggest NSFW English community</td></tr>
290
+
291
+ <tr><td align="right">
292
+ <a href="https://www.reddit.com/r/DeepFakesSFW/new/">reddit r/DeepFakesSFW/</a>
293
+ </td><td align="center">Post your deepfakes there !</td></tr>
294
+
295
+ <tr><td align="right">
296
+ <a href="https://www.reddit.com/r/RUdeepfakes/new/">reddit r/RUdeepfakes/</a>
297
+ </td><td align="center">Постим русские дипфейки сюда !</td></tr>
298
+
299
+ <tr><td align="right">
300
+ QQ群1095077489
301
+ </td><td align="center">中文交流QQ群,商务合作找群主</td></tr>
302
+
303
+ <tr><td align="right">
304
+ <a href="https://www.dfldata.xyz">dfldata.xyz</a>
305
+ </td><td align="center">中文交流论坛,免费软件教程、模型、人脸数据</td></tr>
306
+
307
+ <tr><td align="right">
308
+ <a href="https://www.deepfaker.xyz/">deepfaker.xyz</a>
309
+ </td><td align="center">中文学习站(非官方)</td></tr>
310
+
311
+ <tr><td colspan=2 align="center">
312
+
313
+ ## Related works
314
+
315
+ </td></tr>
316
+
317
+ <tr><td align="right">
318
+ <a href="https://github.com/iperov/DeepFaceLive">DeepFaceLive</a>
319
+ </td><td align="center">Real-time face swap for PC streaming or video calls</td></tr>
320
+
321
+ <tr><td align="right">
322
+ <a href="https://github.com/neuralchen/SimSwap">neuralchen/SimSwap</a>
323
+ </td><td align="center">Swapping face using ONE single photo 一张图免训练换脸</td></tr>
324
+
325
+ <tr><td align="right">
326
+ <a href="https://github.com/deepfakes/faceswap">deepfakes/faceswap</a>
327
+ </td><td align="center">Something that was before DeepFaceLab and still remains in the past</td></tr>
328
+
329
+ </td></tr>
330
+ </table>
331
+
332
+ <table align="center" border="0">
333
+
334
+ <tr><td colspan=2 align="center">
335
+
336
+ ## How I can help the project?
337
+
338
+ </td></tr>
339
+
340
+ <tr><td colspan=2 align="center">
341
+
342
+ ### Sponsor deepfake research and DeepFaceLab development.
343
+
344
+ </td></tr>
345
+
346
+ <tr><td colspan=2 align="center">
347
+ <a href="https://www.paypal.com/paypalme/DeepFaceLab">Donate via Paypal</a>
348
+ </td></tr>
349
+
350
+ <tr><td colspan=2 align="center">
351
+ <a href="https://money.yandex.ru/to/41001142318065">Donate via Yandex.Money</a>
352
+ </td></tr>
353
+
354
+ <tr><td colspan=2 align="center">
355
+ bitcoin:bc1qkhh7h0gwwhxgg6h6gpllfgstkd645fefrd5s6z
356
+ </td></tr>
357
+
358
+ <tr><td colspan=2 align="center">
359
+
360
+ ### Collect facesets
361
+
362
+ </td></tr>
363
+
364
+ <tr><td colspan=2 align="center">
365
+
366
+ You can collect faceset of any celebrity that can be used in DeepFaceLab and share it <a href="https://mrdeepfakes.com/forums/forum-celebrity-facesets">in the community</a>
367
+ </td></tr>
368
+
369
+ <tr><td colspan=2 align="center">
370
+
371
+ ### Star this repo
372
+
373
+ </td></tr>
374
+
375
+ <tr><td colspan=2 align="center">
376
+
377
+ Register github account and push "Star" button.
378
+
379
+ </td></tr>
380
+
381
+ </table>
382
+
383
+
384
+
385
+ <table align="center" border="0">
386
+ <tr><td colspan=2 align="center">
387
+
388
+ ## Meme zone
389
+
390
+ </td></tr>
391
+
392
+ <tr><td align="center" width="50%">
393
+
394
+ <img src="doc/meme1.jpg" align="center">
395
+
396
+ </td>
397
+
398
+ <td align="center" width="50%">
399
+
400
+ <img src="doc/meme2.jpg" align="center">
401
+
402
+ </td></tr>
403
+
404
+ <tr><td colspan=2>
405
+ <img src="doc/meme3.jpg" align="center">
406
+ </td></tr>
407
+
408
+ <tr><td align="center" width="50%">
409
+
410
+ ## You don't need deepfake detector. You need to stop lying.
411
+
412
+
413
+
414
+ </td><td align="center" width="10%">
415
+
416
+ <img src="https://i.imgur.com/z0e0xFB.jpg" align="center">
417
+
418
+ V.I. Lenin
419
+ </td></tr>
420
+
421
+ <tr><td colspan=2 align="center">
422
+
423
+ <sub>#deepfacelab #deepfakes #faceswap #face-swap #deep-learning #deeplearning #deep-neural-networks #deepface #deep-face-swap #fakeapp #fake-app #neural-networks #neural-nets #tensorflow #cuda #nvidia</sub>
424
+
425
+ </td></tr>
426
+
427
+
428
+
429
+ </table>
XSegEditor/QCursorDB.py ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ from PyQt5.QtCore import *
2
+ from PyQt5.QtGui import *
3
+ from PyQt5.QtWidgets import *
4
+
5
+ class QCursorDB():
6
+ @staticmethod
7
+ def initialize(cursor_path):
8
+ QCursorDB.cross_red = QCursor ( QPixmap ( str(cursor_path / 'cross_red.png') ) )
9
+ QCursorDB.cross_green = QCursor ( QPixmap ( str(cursor_path / 'cross_green.png') ) )
10
+ QCursorDB.cross_blue = QCursor ( QPixmap ( str(cursor_path / 'cross_blue.png') ) )
XSegEditor/QIconDB.py ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from PyQt5.QtCore import *
2
+ from PyQt5.QtGui import *
3
+ from PyQt5.QtWidgets import *
4
+
5
+
6
+ class QIconDB():
7
+ @staticmethod
8
+ def initialize(icon_path):
9
+ QIconDB.app_icon = QIcon ( str(icon_path / 'app_icon.png') )
10
+ QIconDB.delete_poly = QIcon ( str(icon_path / 'delete_poly.png') )
11
+ QIconDB.undo_pt = QIcon ( str(icon_path / 'undo_pt.png') )
12
+ QIconDB.redo_pt = QIcon ( str(icon_path / 'redo_pt.png') )
13
+ QIconDB.poly_color_red = QIcon ( str(icon_path / 'poly_color_red.png') )
14
+ QIconDB.poly_color_green = QIcon ( str(icon_path / 'poly_color_green.png') )
15
+ QIconDB.poly_color_blue = QIcon ( str(icon_path / 'poly_color_blue.png') )
16
+ QIconDB.poly_type_include = QIcon ( str(icon_path / 'poly_type_include.png') )
17
+ QIconDB.poly_type_exclude = QIcon ( str(icon_path / 'poly_type_exclude.png') )
18
+ QIconDB.left = QIcon ( str(icon_path / 'left.png') )
19
+ QIconDB.right = QIcon ( str(icon_path / 'right.png') )
20
+ QIconDB.trashcan = QIcon ( str(icon_path / 'trashcan.png') )
21
+ QIconDB.pt_edit_mode = QIcon ( str(icon_path / 'pt_edit_mode.png') )
22
+ QIconDB.view_lock_center = QIcon ( str(icon_path / 'view_lock_center.png') )
23
+ QIconDB.view_baked = QIcon ( str(icon_path / 'view_baked.png') )
24
+ QIconDB.view_xseg = QIcon ( str(icon_path / 'view_xseg.png') )
25
+ QIconDB.view_xseg_overlay = QIcon ( str(icon_path / 'view_xseg_overlay.png') )
26
+
XSegEditor/QImageDB.py ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ from PyQt5.QtCore import *
2
+ from PyQt5.QtGui import *
3
+ from PyQt5.QtWidgets import *
4
+
5
+ class QImageDB():
6
+ @staticmethod
7
+ def initialize(image_path):
8
+ QImageDB.intro = QImage ( str(image_path / 'intro.png') )
XSegEditor/QStringDB.py ADDED
@@ -0,0 +1,102 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from localization import system_language
2
+
3
+
4
+ class QStringDB():
5
+
6
+ @staticmethod
7
+ def initialize():
8
+ lang = system_language
9
+
10
+ if lang not in ['en','ru','zh']:
11
+ lang = 'en'
12
+
13
+ QStringDB.btn_poly_color_red_tip = { 'en' : 'Poly color scheme red',
14
+ 'ru' : 'Красная цветовая схема полигонов',
15
+ 'zh' : '选区配色方案红色',
16
+ }[lang]
17
+
18
+ QStringDB.btn_poly_color_green_tip = { 'en' : 'Poly color scheme green',
19
+ 'ru' : 'Зелёная цветовая схема полигонов',
20
+ 'zh' : '选区配色方案绿色',
21
+ }[lang]
22
+
23
+ QStringDB.btn_poly_color_blue_tip = { 'en' : 'Poly color scheme blue',
24
+ 'ru' : 'Синяя цветовая схема полигонов',
25
+ 'zh' : '选区配色方案蓝色',
26
+ }[lang]
27
+
28
+ QStringDB.btn_view_baked_mask_tip = { 'en' : 'View baked mask',
29
+ 'ru' : 'Посмотреть запечёную маску',
30
+ 'zh' : '查看遮罩通道',
31
+ }[lang]
32
+
33
+ QStringDB.btn_view_xseg_mask_tip = { 'en' : 'View trained XSeg mask',
34
+ 'ru' : 'Посмотреть тренированную XSeg маску',
35
+ 'zh' : '查看导入后的XSeg遮罩',
36
+ }[lang]
37
+
38
+ QStringDB.btn_view_xseg_overlay_mask_tip = { 'en' : 'View trained XSeg mask overlay face',
39
+ 'ru' : 'Посмотреть тренированную XSeg маску поверх лица',
40
+ 'zh' : '查看导入后的XSeg遮罩于脸上方',
41
+ }[lang]
42
+
43
+ QStringDB.btn_poly_type_include_tip = { 'en' : 'Poly include mode',
44
+ 'ru' : 'Режим полигонов - включение',
45
+ 'zh' : '包含选区模式',
46
+ }[lang]
47
+
48
+ QStringDB.btn_poly_type_exclude_tip = { 'en' : 'Poly exclude mode',
49
+ 'ru' : 'Режим полигонов - исключение',
50
+ 'zh' : '排除选区模式',
51
+ }[lang]
52
+
53
+ QStringDB.btn_undo_pt_tip = { 'en' : 'Undo point',
54
+ 'ru' : 'Отменить точку',
55
+ 'zh' : '撤消点',
56
+ }[lang]
57
+
58
+ QStringDB.btn_redo_pt_tip = { 'en' : 'Redo point',
59
+ 'ru' : 'Повторить точку',
60
+ 'zh' : '重做点',
61
+ }[lang]
62
+
63
+ QStringDB.btn_delete_poly_tip = { 'en' : 'Delete poly',
64
+ 'ru' : 'Удалить полигон',
65
+ 'zh' : '删除选区',
66
+ }[lang]
67
+
68
+ QStringDB.btn_pt_edit_mode_tip = { 'en' : 'Add/delete point mode ( HOLD CTRL )',
69
+ 'ru' : 'Режим добавления/удаления точек ( удерживайте CTRL )',
70
+ 'zh' : '点加/删除模式 ( 按住CTRL )',
71
+ }[lang]
72
+
73
+ QStringDB.btn_view_lock_center_tip = { 'en' : 'Lock cursor at the center ( HOLD SHIFT )',
74
+ 'ru' : 'Заблокировать курсор в центре ( удерживайте SHIFT )',
75
+ 'zh' : '将光标锁定在中心 ( 按住SHIFT )',
76
+ }[lang]
77
+
78
+
79
+ QStringDB.btn_prev_image_tip = { 'en' : 'Save and Prev image\nHold SHIFT : accelerate\nHold CTRL : skip non masked\n',
80
+ 'ru' : 'Сохранить и предыдущее изображение\nУдерживать SHIFT : ускорить\nУдерживать CTRL : пропустить неразмеченные\n',
81
+ 'zh' : '保存并转到上一张图片\n按住SHIFT : 加快\n按住CTRL : 跳过未标记的\n',
82
+ }[lang]
83
+ QStringDB.btn_next_image_tip = { 'en' : 'Save and Next image\nHold SHIFT : accelerate\nHold CTRL : skip non masked\n',
84
+ 'ru' : 'Сохранить и следующее изображение\nУдерживать SHIFT : ускорить\nУдерживать CTRL : пропустить неразмеченные\n',
85
+ 'zh' : '保存并转到下一张图片\n按住SHIFT : 加快\n按住CTRL : 跳过未标记的\n',
86
+ }[lang]
87
+
88
+ QStringDB.btn_delete_image_tip = { 'en' : 'Move to _trash and Next image\n',
89
+ 'ru' : 'Переместить в _trash и следующее изображение\n',
90
+ 'zh' : '移至_trash,转到下一张图片 ',
91
+ }[lang]
92
+
93
+ QStringDB.loading_tip = {'en' : 'Loading',
94
+ 'ru' : 'Загрузка',
95
+ 'zh' : '正在载入',
96
+ }[lang]
97
+
98
+ QStringDB.labeled_tip = {'en' : 'labeled',
99
+ 'ru' : 'размечено',
100
+ 'zh' : '标记的',
101
+ }[lang]
102
+
XSegEditor/XSegEditor - Copy.py ADDED
@@ -0,0 +1,1522 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import multiprocessing
3
+ import os
4
+ import pickle
5
+ import sys
6
+ import tempfile
7
+ import time
8
+ import traceback
9
+ from enum import IntEnum
10
+ from types import SimpleNamespace as sn
11
+
12
+ import cv2
13
+ import numpy as np
14
+ import numpy.linalg as npla
15
+ from PyQt5.QtCore import *
16
+ from PyQt5.QtGui import *
17
+ from PyQt5.QtWidgets import *
18
+
19
+ from core import imagelib, pathex
20
+ from core.cv2ex import *
21
+ from core.imagelib import SegIEPoly, SegIEPolys, SegIEPolyType, sd
22
+ from core.qtex import *
23
+ from DFLIMG import *
24
+ from localization import StringsDB, system_language
25
+ from samplelib import PackedFaceset
26
+
27
+ from .QCursorDB import QCursorDB
28
+ from .QIconDB import QIconDB
29
+ from .QStringDB import QStringDB
30
+ from .QImageDB import QImageDB
31
+
32
+ class OpMode(IntEnum):
33
+ NONE = 0
34
+ DRAW_PTS = 1
35
+ EDIT_PTS = 2
36
+ VIEW_BAKED = 3
37
+ VIEW_XSEG_MASK = 4
38
+ VIEW_XSEG_OVERLAY_MASK = 5
39
+
40
+ class PTEditMode(IntEnum):
41
+ MOVE = 0
42
+ ADD_DEL = 1
43
+
44
+ class DragType(IntEnum):
45
+ NONE = 0
46
+ IMAGE_LOOK = 1
47
+ POLY_PT = 2
48
+
49
+ class ViewLock(IntEnum):
50
+ NONE = 0
51
+ CENTER = 1
52
+
53
+ class QUIConfig():
54
+ @staticmethod
55
+ def initialize(icon_size = 48, icon_spacer_size=16, preview_bar_icon_size=64):
56
+ QUIConfig.icon_q_size = QSize(icon_size, icon_size)
57
+ QUIConfig.icon_spacer_q_size = QSize(icon_spacer_size, icon_spacer_size)
58
+ QUIConfig.preview_bar_icon_q_size = QSize(preview_bar_icon_size, preview_bar_icon_size)
59
+
60
+ class ImagePreviewSequenceBar(QFrame):
61
+ def __init__(self, preview_images_count, icon_size):
62
+ super().__init__()
63
+ self.preview_images_count = preview_images_count = max(1, preview_images_count + (preview_images_count % 2 -1) )
64
+
65
+ self.icon_size = icon_size
66
+
67
+ black_q_img = QImage(np.zeros( (icon_size,icon_size,3) ).data, icon_size, icon_size, 3*icon_size, QImage.Format_RGB888)
68
+ self.black_q_pixmap = QPixmap.fromImage(black_q_img)
69
+
70
+ self.image_containers = [ QLabel() for i in range(preview_images_count)]
71
+
72
+ main_frame_l_cont_hl = QGridLayout()
73
+ main_frame_l_cont_hl.setContentsMargins(0,0,0,0)
74
+ #main_frame_l_cont_hl.setSpacing(0)
75
+
76
+
77
+
78
+ for i in range(len(self.image_containers)):
79
+ q_label = self.image_containers[i]
80
+ q_label.setScaledContents(True)
81
+ if i == preview_images_count//2:
82
+ q_label.setMinimumSize(icon_size+16, icon_size+16 )
83
+ q_label.setMaximumSize(icon_size+16, icon_size+16 )
84
+ else:
85
+ q_label.setMinimumSize(icon_size, icon_size )
86
+ q_label.setMaximumSize(icon_size, icon_size )
87
+ opacity_effect = QGraphicsOpacityEffect()
88
+ opacity_effect.setOpacity(0.5)
89
+ q_label.setGraphicsEffect(opacity_effect)
90
+
91
+ q_label.setSizePolicy (QSizePolicy.Fixed, QSizePolicy.Fixed)
92
+
93
+ main_frame_l_cont_hl.addWidget (q_label, 0, i)
94
+
95
+ self.setLayout(main_frame_l_cont_hl)
96
+
97
+ self.prev_img_conts = self.image_containers[(preview_images_count//2) -1::-1]
98
+ self.next_img_conts = self.image_containers[preview_images_count//2:]
99
+
100
+ self.update_images()
101
+
102
+ def get_preview_images_count(self):
103
+ return self.preview_images_count
104
+
105
+ def update_images(self, prev_imgs=None, next_imgs=None):
106
+ # Fix arrays
107
+ if prev_imgs is None:
108
+ prev_imgs = []
109
+ prev_img_conts_len = len(self.prev_img_conts)
110
+ prev_q_imgs_len = len(prev_imgs)
111
+ if prev_q_imgs_len < prev_img_conts_len:
112
+ for i in range ( prev_img_conts_len - prev_q_imgs_len ):
113
+ prev_imgs.append(None)
114
+ elif prev_q_imgs_len > prev_img_conts_len:
115
+ prev_imgs = prev_imgs[:prev_img_conts_len]
116
+
117
+ if next_imgs is None:
118
+ next_imgs = []
119
+ next_img_conts_len = len(self.next_img_conts)
120
+ next_q_imgs_len = len(next_imgs)
121
+ if next_q_imgs_len < next_img_conts_len:
122
+ for i in range ( next_img_conts_len - next_q_imgs_len ):
123
+ next_imgs.append(None)
124
+ elif next_q_imgs_len > next_img_conts_len:
125
+ next_imgs = next_imgs[:next_img_conts_len]
126
+
127
+ for i,img in enumerate(prev_imgs):
128
+ self.prev_img_conts[i].setPixmap( QPixmap.fromImage( QImage_from_np(img) ) if img is not None else self.black_q_pixmap )
129
+
130
+ for i,img in enumerate(next_imgs):
131
+ self.next_img_conts[i].setPixmap( QPixmap.fromImage( QImage_from_np(img) ) if img is not None else self.black_q_pixmap )
132
+
133
+ class ColorScheme():
134
+ def __init__(self, unselected_color, selected_color, outline_color, outline_width, pt_outline_color, cross_cursor):
135
+ self.poly_unselected_brush = QBrush(unselected_color)
136
+ self.poly_selected_brush = QBrush(selected_color)
137
+
138
+ self.poly_outline_solid_pen = QPen(outline_color, outline_width, Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin)
139
+ self.poly_outline_dot_pen = QPen(outline_color, outline_width, Qt.DotLine, Qt.RoundCap, Qt.RoundJoin)
140
+
141
+ self.pt_outline_pen = QPen(pt_outline_color)
142
+ self.cross_cursor = cross_cursor
143
+
144
+ class CanvasConfig():
145
+
146
+ def __init__(self,
147
+ pt_radius=4,
148
+ pt_select_radius=8,
149
+ color_schemes=None,
150
+ **kwargs):
151
+ self.pt_radius = pt_radius
152
+ self.pt_select_radius = pt_select_radius
153
+
154
+ if color_schemes is None:
155
+ color_schemes = [
156
+ ColorScheme( QColor(192,0,0,alpha=0), QColor(192,0,0,alpha=72), QColor(192,0,0), 2, QColor(255,255,255), QCursorDB.cross_red ),
157
+ ColorScheme( QColor(0,192,0,alpha=0), QColor(0,192,0,alpha=72), QColor(0,192,0), 2, QColor(255,255,255), QCursorDB.cross_green ),
158
+ ColorScheme( QColor(0,0,192,alpha=0), QColor(0,0,192,alpha=72), QColor(0,0,192), 2, QColor(255,255,255), QCursorDB.cross_blue ),
159
+ ]
160
+ self.color_schemes = color_schemes
161
+
162
+ class QCanvasControlsLeftBar(QFrame):
163
+
164
+ def __init__(self):
165
+ super().__init__()
166
+ #==============================================
167
+ btn_poly_type_include = QToolButton()
168
+ self.btn_poly_type_include_act = QActionEx( QIconDB.poly_type_include, QStringDB.btn_poly_type_include_tip, shortcut='Q', shortcut_in_tooltip=True, is_checkable=True)
169
+ btn_poly_type_include.setDefaultAction(self.btn_poly_type_include_act)
170
+ btn_poly_type_include.setIconSize(QUIConfig.icon_q_size)
171
+
172
+ btn_poly_type_exclude = QToolButton()
173
+ self.btn_poly_type_exclude_act = QActionEx( QIconDB.poly_type_exclude, QStringDB.btn_poly_type_exclude_tip, shortcut='W', shortcut_in_tooltip=True, is_checkable=True)
174
+ btn_poly_type_exclude.setDefaultAction(self.btn_poly_type_exclude_act)
175
+ btn_poly_type_exclude.setIconSize(QUIConfig.icon_q_size)
176
+
177
+ self.btn_poly_type_act_grp = QActionGroup (self)
178
+ self.btn_poly_type_act_grp.addAction(self.btn_poly_type_include_act)
179
+ self.btn_poly_type_act_grp.addAction(self.btn_poly_type_exclude_act)
180
+ self.btn_poly_type_act_grp.setExclusive(True)
181
+ #==============================================
182
+ btn_undo_pt = QToolButton()
183
+ self.btn_undo_pt_act = QActionEx( QIconDB.undo_pt, QStringDB.btn_undo_pt_tip, shortcut='Ctrl+Z', shortcut_in_tooltip=True, is_auto_repeat=True)
184
+ btn_undo_pt.setDefaultAction(self.btn_undo_pt_act)
185
+ btn_undo_pt.setIconSize(QUIConfig.icon_q_size)
186
+
187
+ btn_redo_pt = QToolButton()
188
+ self.btn_redo_pt_act = QActionEx( QIconDB.redo_pt, QStringDB.btn_redo_pt_tip, shortcut='Ctrl+Shift+Z', shortcut_in_tooltip=True, is_auto_repeat=True)
189
+ btn_redo_pt.setDefaultAction(self.btn_redo_pt_act)
190
+ btn_redo_pt.setIconSize(QUIConfig.icon_q_size)
191
+
192
+ btn_delete_poly = QToolButton()
193
+ self.btn_delete_poly_act = QActionEx( QIconDB.delete_poly, QStringDB.btn_delete_poly_tip, shortcut='Delete', shortcut_in_tooltip=True)
194
+ btn_delete_poly.setDefaultAction(self.btn_delete_poly_act)
195
+ btn_delete_poly.setIconSize(QUIConfig.icon_q_size)
196
+ #==============================================
197
+ btn_pt_edit_mode = QToolButton()
198
+ self.btn_pt_edit_mode_act = QActionEx( QIconDB.pt_edit_mode, QStringDB.btn_pt_edit_mode_tip, shortcut_in_tooltip=True, is_checkable=True)
199
+ btn_pt_edit_mode.setDefaultAction(self.btn_pt_edit_mode_act)
200
+ btn_pt_edit_mode.setIconSize(QUIConfig.icon_q_size)
201
+ #==============================================
202
+
203
+ controls_bar_frame2_l = QVBoxLayout()
204
+ controls_bar_frame2_l.addWidget ( btn_poly_type_include )
205
+ controls_bar_frame2_l.addWidget ( btn_poly_type_exclude )
206
+ controls_bar_frame2 = QFrame()
207
+ controls_bar_frame2.setFrameShape(QFrame.StyledPanel)
208
+ controls_bar_frame2.setSizePolicy (QSizePolicy.Fixed, QSizePolicy.Fixed)
209
+ controls_bar_frame2.setLayout(controls_bar_frame2_l)
210
+
211
+ controls_bar_frame3_l = QVBoxLayout()
212
+ controls_bar_frame3_l.addWidget ( btn_undo_pt )
213
+ controls_bar_frame3_l.addWidget ( btn_redo_pt )
214
+ controls_bar_frame3_l.addWidget ( btn_delete_poly )
215
+ controls_bar_frame3 = QFrame()
216
+ controls_bar_frame3.setFrameShape(QFrame.StyledPanel)
217
+ controls_bar_frame3.setSizePolicy (QSizePolicy.Fixed, QSizePolicy.Fixed)
218
+ controls_bar_frame3.setLayout(controls_bar_frame3_l)
219
+
220
+ controls_bar_frame4_l = QVBoxLayout()
221
+ controls_bar_frame4_l.addWidget ( btn_pt_edit_mode )
222
+ controls_bar_frame4 = QFrame()
223
+ controls_bar_frame4.setFrameShape(QFrame.StyledPanel)
224
+ controls_bar_frame4.setSizePolicy (QSizePolicy.Fixed, QSizePolicy.Fixed)
225
+ controls_bar_frame4.setLayout(controls_bar_frame4_l)
226
+
227
+ btn_view_lock_center = QToolButton()
228
+ self.btn_view_lock_center_act = QActionEx( QIconDB.view_lock_center, QStringDB.btn_view_lock_center_tip, shortcut_in_tooltip=True, is_checkable=True)
229
+ btn_view_lock_center.setDefaultAction(self.btn_view_lock_center_act)
230
+ btn_view_lock_center.setIconSize(QUIConfig.icon_q_size)
231
+
232
+ controls_bar_frame5_l = QVBoxLayout()
233
+ controls_bar_frame5_l.addWidget ( btn_view_lock_center )
234
+ controls_bar_frame5 = QFrame()
235
+ controls_bar_frame5.setFrameShape(QFrame.StyledPanel)
236
+ controls_bar_frame5.setSizePolicy (QSizePolicy.Fixed, QSizePolicy.Fixed)
237
+ controls_bar_frame5.setLayout(controls_bar_frame5_l)
238
+
239
+
240
+ controls_bar_l = QVBoxLayout()
241
+ controls_bar_l.setContentsMargins(0,0,0,0)
242
+ controls_bar_l.addWidget(controls_bar_frame2)
243
+ controls_bar_l.addWidget(controls_bar_frame3)
244
+ controls_bar_l.addWidget(controls_bar_frame4)
245
+ controls_bar_l.addWidget(controls_bar_frame5)
246
+
247
+ self.setSizePolicy ( QSizePolicy.Fixed, QSizePolicy.Expanding )
248
+ self.setLayout(controls_bar_l)
249
+
250
+ class QCanvasControlsRightBar(QFrame):
251
+
252
+ def __init__(self):
253
+ super().__init__()
254
+ #==============================================
255
+ btn_poly_color_red = QToolButton()
256
+ self.btn_poly_color_red_act = QActionEx( QIconDB.poly_color_red, QStringDB.btn_poly_color_red_tip, shortcut='1', shortcut_in_tooltip=True, is_checkable=True)
257
+ btn_poly_color_red.setDefaultAction(self.btn_poly_color_red_act)
258
+ btn_poly_color_red.setIconSize(QUIConfig.icon_q_size)
259
+
260
+ btn_poly_color_green = QToolButton()
261
+ self.btn_poly_color_green_act = QActionEx( QIconDB.poly_color_green, QStringDB.btn_poly_color_green_tip, shortcut='2', shortcut_in_tooltip=True, is_checkable=True)
262
+ btn_poly_color_green.setDefaultAction(self.btn_poly_color_green_act)
263
+ btn_poly_color_green.setIconSize(QUIConfig.icon_q_size)
264
+
265
+ btn_poly_color_blue = QToolButton()
266
+ self.btn_poly_color_blue_act = QActionEx( QIconDB.poly_color_blue, QStringDB.btn_poly_color_blue_tip, shortcut='3', shortcut_in_tooltip=True, is_checkable=True)
267
+ btn_poly_color_blue.setDefaultAction(self.btn_poly_color_blue_act)
268
+ btn_poly_color_blue.setIconSize(QUIConfig.icon_q_size)
269
+
270
+ btn_view_baked_mask = QToolButton()
271
+ self.btn_view_baked_mask_act = QActionEx( QIconDB.view_baked, QStringDB.btn_view_baked_mask_tip, shortcut='4', shortcut_in_tooltip=True, is_checkable=True)
272
+ btn_view_baked_mask.setDefaultAction(self.btn_view_baked_mask_act)
273
+ btn_view_baked_mask.setIconSize(QUIConfig.icon_q_size)
274
+
275
+ btn_view_xseg_mask = QToolButton()
276
+ self.btn_view_xseg_mask_act = QActionEx( QIconDB.view_xseg, QStringDB.btn_view_xseg_mask_tip, shortcut='5', shortcut_in_tooltip=True, is_checkable=True)
277
+ btn_view_xseg_mask.setDefaultAction(self.btn_view_xseg_mask_act)
278
+ btn_view_xseg_mask.setIconSize(QUIConfig.icon_q_size)
279
+
280
+ btn_view_xseg_overlay_mask = QToolButton()
281
+ self.btn_view_xseg_overlay_mask_act = QActionEx( QIconDB.view_xseg_overlay, QStringDB.btn_view_xseg_overlay_mask_tip, shortcut='6', shortcut_in_tooltip=True, is_checkable=True)
282
+ btn_view_xseg_overlay_mask.setDefaultAction(self.btn_view_xseg_overlay_mask_act)
283
+ btn_view_xseg_overlay_mask.setIconSize(QUIConfig.icon_q_size)
284
+
285
+ self.btn_poly_color_act_grp = QActionGroup (self)
286
+ self.btn_poly_color_act_grp.addAction(self.btn_poly_color_red_act)
287
+ self.btn_poly_color_act_grp.addAction(self.btn_poly_color_green_act)
288
+ self.btn_poly_color_act_grp.addAction(self.btn_poly_color_blue_act)
289
+ self.btn_poly_color_act_grp.addAction(self.btn_view_baked_mask_act)
290
+ self.btn_poly_color_act_grp.addAction(self.btn_view_xseg_mask_act)
291
+ self.btn_poly_color_act_grp.addAction(self.btn_view_xseg_overlay_mask_act)
292
+ self.btn_poly_color_act_grp.setExclusive(True)
293
+ #==============================================
294
+
295
+ btn_xseg_to_poly = QToolButton()
296
+ self.btn_xseg_to_poly_act = QActionEx( QIconDB.view_lock_center, QStringDB.btn_view_lock_center_tip, shortcut_in_tooltip=True, is_checkable=False)
297
+ btn_xseg_to_poly.setDefaultAction(self.btn_xseg_to_poly_act)
298
+ btn_xseg_to_poly.setIconSize(QUIConfig.icon_q_size)
299
+
300
+ controls_bar_frame1_l = QVBoxLayout()
301
+ controls_bar_frame1_l.addWidget ( btn_poly_color_red )
302
+ controls_bar_frame1_l.addWidget ( btn_poly_color_green )
303
+ controls_bar_frame1_l.addWidget ( btn_poly_color_blue )
304
+ controls_bar_frame1_l.addWidget ( btn_view_baked_mask )
305
+ controls_bar_frame1_l.addWidget ( btn_view_xseg_mask )
306
+ controls_bar_frame1_l.addWidget ( btn_view_xseg_overlay_mask )
307
+ controls_bar_frame1 = QFrame()
308
+ controls_bar_frame1.setFrameShape(QFrame.StyledPanel)
309
+ controls_bar_frame1.setSizePolicy (QSizePolicy.Fixed, QSizePolicy.Fixed)
310
+ controls_bar_frame1.setLayout(controls_bar_frame1_l)
311
+
312
+ controls_bar_frame2_l = QVBoxLayout()
313
+ controls_bar_frame2_l.addWidget ( btn_xseg_to_poly )
314
+ controls_bar_frame2 = QFrame()
315
+ controls_bar_frame2.setFrameShape(QFrame.StyledPanel)
316
+ controls_bar_frame2.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
317
+ controls_bar_frame2.setLayout(controls_bar_frame2_l)
318
+
319
+ controls_bar_l = QVBoxLayout()
320
+ controls_bar_l.setContentsMargins(0,0,0,0)
321
+ controls_bar_l.addWidget(controls_bar_frame1)
322
+ controls_bar_l.addWidget(controls_bar_frame2)
323
+
324
+ self.setSizePolicy ( QSizePolicy.Fixed, QSizePolicy.Expanding )
325
+ self.setLayout(controls_bar_l)
326
+
327
+ class QCanvasOperator(QWidget):
328
+ def __init__(self, cbar):
329
+ super().__init__()
330
+ self.cbar = cbar
331
+
332
+ self.set_cbar_disabled()
333
+
334
+ self.cbar.btn_poly_color_red_act.triggered.connect ( lambda : self.set_color_scheme_id(0) )
335
+ self.cbar.btn_poly_color_green_act.triggered.connect ( lambda : self.set_color_scheme_id(1) )
336
+ self.cbar.btn_poly_color_blue_act.triggered.connect ( lambda : self.set_color_scheme_id(2) )
337
+ self.cbar.btn_view_baked_mask_act.toggled.connect ( lambda : self.set_op_mode(OpMode.VIEW_BAKED) )
338
+ self.cbar.btn_view_xseg_mask_act.toggled.connect ( self.set_view_xseg_mask )
339
+ self.cbar.btn_view_xseg_overlay_mask_act.toggled.connect ( self.set_view_xseg_overlay_mask )
340
+
341
+ self.cbar.btn_poly_type_include_act.triggered.connect ( lambda : self.set_poly_include_type(SegIEPolyType.INCLUDE) )
342
+ self.cbar.btn_poly_type_exclude_act.triggered.connect ( lambda : self.set_poly_include_type(SegIEPolyType.EXCLUDE) )
343
+
344
+ self.cbar.btn_undo_pt_act.triggered.connect ( lambda : self.action_undo_pt() )
345
+ self.cbar.btn_redo_pt_act.triggered.connect ( lambda : self.action_redo_pt() )
346
+
347
+ self.cbar.btn_delete_poly_act.triggered.connect ( lambda : self.action_delete_poly() )
348
+
349
+ self.cbar.btn_pt_edit_mode_act.toggled.connect ( lambda is_checked: self.set_pt_edit_mode( PTEditMode.ADD_DEL if is_checked else PTEditMode.MOVE ) )
350
+ self.cbar.btn_view_lock_center_act.toggled.connect ( lambda is_checked: self.set_view_lock( ViewLock.CENTER if is_checked else ViewLock.NONE ) )
351
+
352
+ self.cbar.btn_xseg_to_poly_act.triggered.connect ( lambda : self.action_xseg_to_poly() )
353
+
354
+
355
+ self.mouse_in_widget = False
356
+
357
+ QXMainWindow.inst.add_keyPressEvent_listener ( self.on_keyPressEvent )
358
+ QXMainWindow.inst.add_keyReleaseEvent_listener ( self.on_keyReleaseEvent )
359
+
360
+ self.qp = QPainter()
361
+ self.initialized = False
362
+ self.last_state = None
363
+
364
+ def initialize(self, img, img_look_pt=None, view_scale=None, ie_polys=None, xseg_mask=None, canvas_config=None ):
365
+ self.img = img
366
+ q_img = self.q_img = QImage_from_np(img)
367
+ self.img_pixmap = QPixmap.fromImage(q_img)
368
+
369
+ self.xseg_mask_in = imagelib.normalize_channels(xseg_mask, 1)
370
+ self.xseg_mask_pixmap = None
371
+ self.xseg_overlay_mask_pixmap = None
372
+
373
+ if xseg_mask is not None:
374
+ h,w,c = img.shape
375
+ xseg_mask = cv2.resize(xseg_mask, (w,h), cv2.INTER_CUBIC)
376
+ xseg_mask = imagelib.normalize_channels(xseg_mask, 1)
377
+ xseg_img = img.astype(np.float32)/255.0
378
+ xseg_overlay_mask = xseg_img*(1-xseg_mask)*0.5 + xseg_img*xseg_mask
379
+ xseg_overlay_mask = np.clip(xseg_overlay_mask*255, 0, 255).astype(np.uint8)
380
+ xseg_mask = np.clip(xseg_mask*255, 0, 255).astype(np.uint8)
381
+ self.xseg_mask_pixmap = QPixmap.fromImage(QImage_from_np(xseg_mask))
382
+ self.xseg_overlay_mask_pixmap = QPixmap.fromImage(QImage_from_np(xseg_overlay_mask))
383
+
384
+ self.img_size = QSize_to_np (self.img_pixmap.size())
385
+
386
+ self.img_look_pt = img_look_pt
387
+ self.view_scale = view_scale
388
+
389
+ if ie_polys is None:
390
+ ie_polys = SegIEPolys()
391
+ self.ie_polys = ie_polys
392
+
393
+ if canvas_config is None:
394
+ canvas_config = CanvasConfig()
395
+ self.canvas_config = canvas_config
396
+
397
+ # UI init
398
+ self.set_cbar_disabled()
399
+ self.cbar.btn_poly_color_act_grp.setDisabled(False)
400
+ self.cbar.btn_poly_type_act_grp.setDisabled(False)
401
+
402
+ # Initial vars
403
+ self.current_cursor = None
404
+ self.mouse_hull_poly = None
405
+ self.mouse_wire_poly = None
406
+ self.drag_type = DragType.NONE
407
+ self.mouse_cli_pt = np.zeros((2,), np.float32 )
408
+
409
+ # Initial state
410
+ self.set_op_mode(OpMode.NONE)
411
+ self.set_color_scheme_id(1)
412
+ self.set_poly_include_type(SegIEPolyType.INCLUDE)
413
+ self.set_pt_edit_mode(PTEditMode.MOVE)
414
+ self.set_view_lock(ViewLock.NONE)
415
+
416
+ # Apply last state
417
+ if self.last_state is not None:
418
+ self.set_color_scheme_id(self.last_state.color_scheme_id)
419
+ if self.last_state.op_mode is not None:
420
+ self.set_op_mode(self.last_state.op_mode)
421
+
422
+ self.initialized = True
423
+
424
+ self.setMouseTracking(True)
425
+ self.update_cursor()
426
+ self.update()
427
+
428
+
429
+ def finalize(self):
430
+ if self.initialized:
431
+ if self.op_mode == OpMode.DRAW_PTS:
432
+ self.set_op_mode(OpMode.EDIT_PTS)
433
+
434
+ self.last_state = sn(op_mode = self.op_mode if self.op_mode in [OpMode.VIEW_BAKED, OpMode.VIEW_XSEG_MASK, OpMode.VIEW_XSEG_OVERLAY_MASK] else None,
435
+ color_scheme_id = self.color_scheme_id,
436
+ )
437
+
438
+ self.img_pixmap = None
439
+ self.update_cursor(is_finalize=True)
440
+ self.setMouseTracking(False)
441
+ self.setFocusPolicy(Qt.NoFocus)
442
+ self.set_cbar_disabled()
443
+ self.initialized = False
444
+ self.update()
445
+
446
+ # ====================================================================================
447
+ # ====================================================================================
448
+ # ====================================== GETTERS =====================================
449
+ # ====================================================================================
450
+ # ====================================================================================
451
+
452
+ def is_initialized(self):
453
+ return self.initialized
454
+
455
+ def get_ie_polys(self):
456
+ return self.ie_polys
457
+
458
+ def get_cli_center_pt(self):
459
+ return np.round(QSize_to_np(self.size())/2.0)
460
+
461
+ def get_img_look_pt(self):
462
+ img_look_pt = self.img_look_pt
463
+ if img_look_pt is None:
464
+ img_look_pt = self.img_size / 2
465
+ return img_look_pt
466
+
467
+ def get_view_scale(self):
468
+ view_scale = self.view_scale
469
+ if view_scale is None:
470
+ # Calc as scale to fit
471
+ min_cli_size = np.min(QSize_to_np(self.size()))
472
+ max_img_size = np.max(self.img_size)
473
+ view_scale = min_cli_size / max_img_size
474
+
475
+ return view_scale
476
+
477
+ def get_current_color_scheme(self):
478
+ return self.canvas_config.color_schemes[self.color_scheme_id]
479
+
480
+ def get_poly_pt_id_under_pt(self, poly, cli_pt):
481
+ w = np.argwhere ( npla.norm ( cli_pt - self.img_to_cli_pt( poly.get_pts() ), axis=1 ) <= self.canvas_config.pt_select_radius )
482
+ return None if len(w) == 0 else w[-1][0]
483
+
484
+ def get_poly_edge_id_pt_under_pt(self, poly, cli_pt):
485
+ cli_pts = self.img_to_cli_pt(poly.get_pts())
486
+ if len(cli_pts) >= 3:
487
+ edge_dists, projs = sd.dist_to_edges(cli_pts, cli_pt, is_closed=True)
488
+ edge_id = np.argmin(edge_dists)
489
+ dist = edge_dists[edge_id]
490
+ pt = projs[edge_id]
491
+ if dist <= self.canvas_config.pt_select_radius:
492
+ return edge_id, pt
493
+ return None, None
494
+
495
+ def get_poly_by_pt_near_wire(self, cli_pt):
496
+ pt_select_radius = self.canvas_config.pt_select_radius
497
+
498
+ for poly in reversed(self.ie_polys.get_polys()):
499
+ pts = poly.get_pts()
500
+ if len(pts) >= 3:
501
+ cli_pts = self.img_to_cli_pt(pts)
502
+
503
+ edge_dists, _ = sd.dist_to_edges(cli_pts, cli_pt, is_closed=True)
504
+
505
+ if np.min(edge_dists) <= pt_select_radius or \
506
+ any( npla.norm ( cli_pt - cli_pts, axis=1 ) <= pt_select_radius ):
507
+ return poly
508
+ return None
509
+
510
+ def get_poly_by_pt_in_hull(self, cli_pos):
511
+ img_pos = self.cli_to_img_pt(cli_pos)
512
+
513
+ for poly in reversed(self.ie_polys.get_polys()):
514
+ pts = poly.get_pts()
515
+ if len(pts) >= 3:
516
+ if cv2.pointPolygonTest( pts, tuple(img_pos), False) >= 0:
517
+ return poly
518
+
519
+ return None
520
+
521
+ def img_to_cli_pt(self, p):
522
+ return (p - self.get_img_look_pt()) * self.get_view_scale() + self.get_cli_center_pt()# QSize_to_np(self.size())/2.0
523
+
524
+ def cli_to_img_pt(self, p):
525
+ return (p - self.get_cli_center_pt() ) / self.get_view_scale() + self.get_img_look_pt()
526
+
527
+ def img_to_cli_rect(self, rect):
528
+ tl = QPoint_to_np(rect.topLeft())
529
+ xy = self.img_to_cli_pt(tl)
530
+ xy2 = self.img_to_cli_pt(tl + QSize_to_np(rect.size()) ) - xy
531
+ return QRect ( *xy.astype(np.int), *xy2.astype(np.int) )
532
+
533
+ # ====================================================================================
534
+ # ====================================================================================
535
+ # ====================================== SETTERS =====================================
536
+ # ====================================================================================
537
+ # ====================================================================================
538
+ def set_op_mode(self, op_mode, op_poly=None):
539
+ if not hasattr(self,'op_mode'):
540
+ self.op_mode = None
541
+ self.op_poly = None
542
+
543
+ if self.op_mode != op_mode:
544
+ # Finalize prev mode
545
+ if self.op_mode == OpMode.NONE:
546
+ self.cbar.btn_poly_type_act_grp.setDisabled(True)
547
+ elif self.op_mode == OpMode.DRAW_PTS:
548
+ self.cbar.btn_undo_pt_act.setDisabled(True)
549
+ self.cbar.btn_redo_pt_act.setDisabled(True)
550
+ self.cbar.btn_view_lock_center_act.setDisabled(True)
551
+ # Reset view_lock when exit from DRAW_PTS
552
+ self.set_view_lock(ViewLock.NONE)
553
+ # Remove unfinished poly
554
+ if self.op_poly.get_pts_count() < 3:
555
+ self.ie_polys.remove_poly(self.op_poly)
556
+
557
+ elif self.op_mode == OpMode.EDIT_PTS:
558
+ self.cbar.btn_pt_edit_mode_act.setDisabled(True)
559
+ self.cbar.btn_delete_poly_act.setDisabled(True)
560
+ # Reset pt_edit_move when exit from EDIT_PTS
561
+ self.set_pt_edit_mode(PTEditMode.MOVE)
562
+ elif self.op_mode == OpMode.VIEW_BAKED:
563
+ self.cbar.btn_view_baked_mask_act.setChecked(False)
564
+ elif self.op_mode == OpMode.VIEW_XSEG_MASK:
565
+ self.cbar.btn_view_xseg_mask_act.setChecked(False)
566
+ self.cbar.btn_xseg_to_poly_act.setDisabled(True)
567
+ elif self.op_mode == OpMode.VIEW_XSEG_OVERLAY_MASK:
568
+ self.cbar.btn_view_xseg_overlay_mask_act.setChecked(False)
569
+ self.cbar.btn_xseg_to_poly_act.setDisabled(True)
570
+ self.op_mode = op_mode
571
+
572
+ # Initialize new mode
573
+ if op_mode == OpMode.NONE:
574
+ self.cbar.btn_poly_type_act_grp.setDisabled(False)
575
+ elif op_mode == OpMode.DRAW_PTS:
576
+ self.cbar.btn_undo_pt_act.setDisabled(False)
577
+ self.cbar.btn_redo_pt_act.setDisabled(False)
578
+ self.cbar.btn_view_lock_center_act.setDisabled(False)
579
+ elif op_mode == OpMode.EDIT_PTS:
580
+ self.cbar.btn_pt_edit_mode_act.setDisabled(False)
581
+ self.cbar.btn_delete_poly_act.setDisabled(False)
582
+ elif op_mode == OpMode.VIEW_BAKED:
583
+ self.cbar.btn_view_baked_mask_act.setChecked(True )
584
+ n = QImage_to_np ( self.q_img ).astype(np.float32) / 255.0
585
+ h,w,c = n.shape
586
+ mask = np.zeros( (h,w,1), dtype=np.float32 )
587
+ self.ie_polys.overlay_mask(mask)
588
+ n = (mask*255).astype(np.uint8)
589
+ self.img_baked_pixmap = QPixmap.fromImage(QImage_from_np(n))
590
+ elif op_mode == OpMode.VIEW_XSEG_MASK:
591
+ self.cbar.btn_view_xseg_mask_act.setChecked(True)
592
+ if self.xseg_mask_in is not None:
593
+ self.cbar.btn_xseg_to_poly_act.setDisabled(False)
594
+ elif op_mode == OpMode.VIEW_XSEG_OVERLAY_MASK:
595
+ self.cbar.btn_view_xseg_overlay_mask_act.setChecked(True)
596
+ if self.xseg_mask_in is not None:
597
+ self.cbar.btn_xseg_to_poly_act.setDisabled(False)
598
+
599
+ if op_mode in [OpMode.DRAW_PTS, OpMode.EDIT_PTS]:
600
+ self.mouse_op_poly_pt_id = None
601
+ self.mouse_op_poly_edge_id = None
602
+ self.mouse_op_poly_edge_id_pt = None
603
+
604
+ self.op_poly = op_poly
605
+ if op_poly is not None:
606
+ self.update_mouse_info()
607
+
608
+ self.update_cursor()
609
+ self.update()
610
+
611
+ def set_pt_edit_mode(self, pt_edit_mode):
612
+ if not hasattr(self, 'pt_edit_mode') or self.pt_edit_mode != pt_edit_mode:
613
+ self.pt_edit_mode = pt_edit_mode
614
+ self.update_cursor()
615
+ self.update()
616
+ self.cbar.btn_pt_edit_mode_act.setChecked( self.pt_edit_mode == PTEditMode.ADD_DEL )
617
+
618
+ def set_view_lock(self, view_lock):
619
+ if not hasattr(self, 'view_lock') or self.view_lock != view_lock:
620
+ if hasattr(self, 'view_lock') and self.view_lock != view_lock:
621
+ if view_lock == ViewLock.CENTER:
622
+ self.img_look_pt = self.mouse_img_pt
623
+ QCursor.setPos ( self.mapToGlobal( QPoint_from_np(self.img_to_cli_pt(self.img_look_pt)) ))
624
+
625
+ self.view_lock = view_lock
626
+ self.update()
627
+ self.cbar.btn_view_lock_center_act.setChecked( self.view_lock == ViewLock.CENTER )
628
+
629
+ def set_cbar_disabled(self):
630
+ self.cbar.btn_delete_poly_act.setDisabled(True)
631
+ self.cbar.btn_undo_pt_act.setDisabled(True)
632
+ self.cbar.btn_redo_pt_act.setDisabled(True)
633
+ self.cbar.btn_pt_edit_mode_act.setDisabled(True)
634
+ self.cbar.btn_view_lock_center_act.setDisabled(True)
635
+ self.cbar.btn_poly_color_act_grp.setDisabled(True)
636
+ self.cbar.btn_poly_type_act_grp.setDisabled(True)
637
+ self.cbar.btn_xseg_to_poly_act.setDisabled(True)
638
+
639
+ def set_color_scheme_id(self, id):
640
+ if self.op_mode == OpMode.VIEW_BAKED:
641
+ self.set_op_mode(OpMode.NONE)
642
+
643
+ if not hasattr(self, 'color_scheme_id') or self.color_scheme_id != id:
644
+ self.color_scheme_id = id
645
+ self.update_cursor()
646
+ self.update()
647
+
648
+ if self.color_scheme_id == 0:
649
+ self.cbar.btn_poly_color_red_act.setChecked( True )
650
+ elif self.color_scheme_id == 1:
651
+ self.cbar.btn_poly_color_green_act.setChecked( True )
652
+ elif self.color_scheme_id == 2:
653
+ self.cbar.btn_poly_color_blue_act.setChecked( True )
654
+
655
+ def set_poly_include_type(self, poly_include_type):
656
+ if not hasattr(self, 'poly_include_type' ) or \
657
+ ( self.poly_include_type != poly_include_type and \
658
+ self.op_mode in [OpMode.NONE, OpMode.EDIT_PTS] ):
659
+ self.poly_include_type = poly_include_type
660
+ self.update()
661
+
662
+ self.cbar.btn_poly_type_include_act.setChecked(self.poly_include_type == SegIEPolyType.INCLUDE)
663
+ self.cbar.btn_poly_type_exclude_act.setChecked(self.poly_include_type == SegIEPolyType.EXCLUDE)
664
+
665
+ def set_view_xseg_mask(self, is_checked):
666
+ if is_checked:
667
+ self.set_op_mode(OpMode.VIEW_XSEG_MASK)
668
+ else:
669
+ self.set_op_mode(OpMode.NONE)
670
+
671
+ self.cbar.btn_view_xseg_mask_act.setChecked(is_checked )
672
+
673
+ def set_view_xseg_overlay_mask(self, is_checked):
674
+ if is_checked:
675
+ self.set_op_mode(OpMode.VIEW_XSEG_OVERLAY_MASK)
676
+ else:
677
+ self.set_op_mode(OpMode.NONE)
678
+
679
+ self.cbar.btn_view_xseg_overlay_mask_act.setChecked(is_checked )
680
+
681
+ # ====================================================================================
682
+ # ====================================================================================
683
+ # ====================================== METHODS =====================================
684
+ # ====================================================================================
685
+ # ====================================================================================
686
+
687
+ def update_cursor(self, is_finalize=False):
688
+ if not self.initialized:
689
+ return
690
+
691
+ if not self.mouse_in_widget or is_finalize:
692
+ if self.current_cursor is not None:
693
+ QApplication.restoreOverrideCursor()
694
+ self.current_cursor = None
695
+ else:
696
+ color_cc = self.get_current_color_scheme().cross_cursor
697
+ nc = Qt.ArrowCursor
698
+
699
+ if self.drag_type == DragType.IMAGE_LOOK:
700
+ nc = Qt.ClosedHandCursor
701
+ else:
702
+
703
+ if self.op_mode == OpMode.NONE:
704
+ nc = color_cc
705
+ if self.mouse_wire_poly is not None:
706
+ nc = Qt.PointingHandCursor
707
+
708
+ elif self.op_mode == OpMode.DRAW_PTS:
709
+ nc = color_cc
710
+ elif self.op_mode == OpMode.EDIT_PTS:
711
+ nc = Qt.ArrowCursor
712
+
713
+ if self.mouse_op_poly_pt_id is not None:
714
+ nc = Qt.PointingHandCursor
715
+
716
+ if self.pt_edit_mode == PTEditMode.ADD_DEL:
717
+
718
+ if self.mouse_op_poly_edge_id is not None and \
719
+ self.mouse_op_poly_pt_id is None:
720
+ nc = color_cc
721
+ if self.current_cursor != nc:
722
+ if self.current_cursor is None:
723
+ QApplication.setOverrideCursor(nc)
724
+ else:
725
+ QApplication.changeOverrideCursor(nc)
726
+ self.current_cursor = nc
727
+
728
+ def update_mouse_info(self, mouse_cli_pt=None):
729
+ """
730
+ Update selected polys/edges/points by given mouse position
731
+ """
732
+ if mouse_cli_pt is not None:
733
+ self.mouse_cli_pt = mouse_cli_pt.astype(np.float32)
734
+
735
+ self.mouse_img_pt = self.cli_to_img_pt(self.mouse_cli_pt)
736
+
737
+ new_mouse_hull_poly = self.get_poly_by_pt_in_hull(self.mouse_cli_pt)
738
+
739
+ if self.mouse_hull_poly != new_mouse_hull_poly:
740
+ self.mouse_hull_poly = new_mouse_hull_poly
741
+ self.update_cursor()
742
+ self.update()
743
+
744
+ new_mouse_wire_poly = self.get_poly_by_pt_near_wire(self.mouse_cli_pt)
745
+
746
+ if self.mouse_wire_poly != new_mouse_wire_poly:
747
+ self.mouse_wire_poly = new_mouse_wire_poly
748
+ self.update_cursor()
749
+ self.update()
750
+
751
+ if self.op_mode in [OpMode.DRAW_PTS, OpMode.EDIT_PTS]:
752
+ new_mouse_op_poly_pt_id = self.get_poly_pt_id_under_pt (self.op_poly, self.mouse_cli_pt)
753
+ if self.mouse_op_poly_pt_id != new_mouse_op_poly_pt_id:
754
+ self.mouse_op_poly_pt_id = new_mouse_op_poly_pt_id
755
+ self.update_cursor()
756
+ self.update()
757
+
758
+ new_mouse_op_poly_edge_id,\
759
+ new_mouse_op_poly_edge_id_pt = self.get_poly_edge_id_pt_under_pt (self.op_poly, self.mouse_cli_pt)
760
+ if self.mouse_op_poly_edge_id != new_mouse_op_poly_edge_id:
761
+ self.mouse_op_poly_edge_id = new_mouse_op_poly_edge_id
762
+ self.update_cursor()
763
+ self.update()
764
+
765
+ if (self.mouse_op_poly_edge_id_pt.__class__ != new_mouse_op_poly_edge_id_pt.__class__) or \
766
+ (isinstance(self.mouse_op_poly_edge_id_pt, np.ndarray) and \
767
+ all(self.mouse_op_poly_edge_id_pt != new_mouse_op_poly_edge_id_pt)):
768
+
769
+ self.mouse_op_poly_edge_id_pt = new_mouse_op_poly_edge_id_pt
770
+ self.update_cursor()
771
+ self.update()
772
+
773
+
774
+ def action_undo_pt(self):
775
+ if self.drag_type == DragType.NONE:
776
+ if self.op_mode == OpMode.DRAW_PTS:
777
+ if self.op_poly.undo() == 0:
778
+ self.ie_polys.remove_poly (self.op_poly)
779
+ self.set_op_mode(OpMode.NONE)
780
+ self.update()
781
+
782
+ def action_redo_pt(self):
783
+ if self.drag_type == DragType.NONE:
784
+ if self.op_mode == OpMode.DRAW_PTS:
785
+ self.op_poly.redo()
786
+ self.update()
787
+
788
+ def action_delete_poly(self):
789
+ if self.op_mode == OpMode.EDIT_PTS and \
790
+ self.drag_type == DragType.NONE and \
791
+ self.pt_edit_mode == PTEditMode.MOVE:
792
+ # Delete current poly
793
+ self.ie_polys.remove_poly (self.op_poly)
794
+ self.set_op_mode(OpMode.NONE)
795
+
796
+ def action_xseg_to_poly(self):
797
+
798
+ cnts = cv2.findContours( (self.xseg_mask_in*255).astype(np.uint8), cv2.RETR_LIST, cv2.CHAIN_APPROX_TC89_KCOS)
799
+ # Sort by countour area
800
+ cnts = sorted(cnts[0], key = cv2.contourArea, reverse = True)
801
+ if len(cnts) != 0:
802
+ h,w,c = self.img.shape
803
+ mh,mw,mc = self.xseg_mask_in.shape
804
+
805
+ dh = h / mh
806
+ dw = w / mw
807
+
808
+ new_poly = self.ie_polys.add_poly(SegIEPolyType.INCLUDE)
809
+ for pt in cnts[0].squeeze():
810
+ new_poly.add_pt( pt[0]*dw, pt[1]*dh )
811
+
812
+ self.set_op_mode(OpMode.EDIT_PTS, op_poly=new_poly)
813
+
814
+
815
+ # ====================================================================================
816
+ # ====================================================================================
817
+ # ================================== OVERRIDE QT METHODS =============================
818
+ # ====================================================================================
819
+ # ====================================================================================
820
+ def on_keyPressEvent(self, ev):
821
+ if not self.initialized:
822
+ return
823
+ key = ev.key()
824
+ key_mods = int(ev.modifiers())
825
+ if self.op_mode == OpMode.DRAW_PTS:
826
+ self.set_view_lock(ViewLock.CENTER if key_mods == Qt.ShiftModifier else ViewLock.NONE )
827
+ elif self.op_mode == OpMode.EDIT_PTS:
828
+ self.set_pt_edit_mode(PTEditMode.ADD_DEL if key_mods == Qt.ControlModifier else PTEditMode.MOVE )
829
+
830
+ def on_keyReleaseEvent(self, ev):
831
+ if not self.initialized:
832
+ return
833
+ key = ev.key()
834
+ key_mods = int(ev.modifiers())
835
+ if self.op_mode == OpMode.DRAW_PTS:
836
+ self.set_view_lock(ViewLock.CENTER if key_mods == Qt.ShiftModifier else ViewLock.NONE )
837
+ elif self.op_mode == OpMode.EDIT_PTS:
838
+ self.set_pt_edit_mode(PTEditMode.ADD_DEL if key_mods == Qt.ControlModifier else PTEditMode.MOVE )
839
+
840
+ def enterEvent(self, ev):
841
+ super().enterEvent(ev)
842
+ self.mouse_in_widget = True
843
+ self.update_cursor()
844
+
845
+ def leaveEvent(self, ev):
846
+ super().leaveEvent(ev)
847
+ self.mouse_in_widget = False
848
+ self.update_cursor()
849
+
850
+ def mousePressEvent(self, ev):
851
+ super().mousePressEvent(ev)
852
+ if not self.initialized:
853
+ return
854
+
855
+ self.update_mouse_info(QPoint_to_np(ev.pos()))
856
+
857
+ btn = ev.button()
858
+
859
+ if btn == Qt.LeftButton:
860
+ if self.op_mode == OpMode.NONE:
861
+ # Clicking in NO OPERATION mode
862
+ if self.mouse_wire_poly is not None:
863
+ # Click on wire on any poly -> switch to EDIT_MODE
864
+ self.set_op_mode(OpMode.EDIT_PTS, op_poly=self.mouse_wire_poly)
865
+ else:
866
+ # Click on empty space -> create new poly with one point
867
+ new_poly = self.ie_polys.add_poly(self.poly_include_type)
868
+ self.ie_polys.sort()
869
+ new_poly.add_pt(*self.mouse_img_pt)
870
+ self.set_op_mode(OpMode.DRAW_PTS, op_poly=new_poly )
871
+
872
+ elif self.op_mode == OpMode.DRAW_PTS:
873
+ # Clicking in DRAW_PTS mode
874
+ if len(self.op_poly.get_pts()) >= 3 and self.mouse_op_poly_pt_id == 0:
875
+ # Click on first point -> close poly and switch to edit mode
876
+ self.set_op_mode(OpMode.EDIT_PTS, op_poly=self.op_poly)
877
+ else:
878
+ # Click on empty space -> add point to current poly
879
+ self.op_poly.add_pt(*self.mouse_img_pt)
880
+ self.update()
881
+
882
+ elif self.op_mode == OpMode.EDIT_PTS:
883
+ # Clicking in EDIT_PTS mode
884
+
885
+ if self.mouse_op_poly_pt_id is not None:
886
+ # Click on point of op_poly
887
+ if self.pt_edit_mode == PTEditMode.ADD_DEL:
888
+ # with mode -> delete point
889
+ self.op_poly.remove_pt(self.mouse_op_poly_pt_id)
890
+ if self.op_poly.get_pts_count() < 3:
891
+ # not enough points -> remove poly
892
+ self.ie_polys.remove_poly (self.op_poly)
893
+ self.set_op_mode(OpMode.NONE)
894
+ self.update()
895
+
896
+ elif self.drag_type == DragType.NONE:
897
+ # otherwise -> start drag
898
+ self.drag_type = DragType.POLY_PT
899
+ self.drag_cli_pt = self.mouse_cli_pt
900
+ self.drag_poly_pt_id = self.mouse_op_poly_pt_id
901
+ self.drag_poly_pt = self.op_poly.get_pts()[ self.drag_poly_pt_id ]
902
+ elif self.mouse_op_poly_edge_id is not None:
903
+ # Click on edge of op_poly
904
+ if self.pt_edit_mode == PTEditMode.ADD_DEL:
905
+ # with mode -> insert new point
906
+ edge_img_pt = self.cli_to_img_pt(self.mouse_op_poly_edge_id_pt)
907
+ self.op_poly.insert_pt (self.mouse_op_poly_edge_id+1, edge_img_pt)
908
+ self.update()
909
+ else:
910
+ # Otherwise do nothing
911
+ pass
912
+ else:
913
+ # other cases -> unselect poly
914
+ self.set_op_mode(OpMode.NONE)
915
+
916
+ elif btn == Qt.MiddleButton:
917
+ if self.drag_type == DragType.NONE:
918
+ # Start image drag
919
+ self.drag_type = DragType.IMAGE_LOOK
920
+ self.drag_cli_pt = self.mouse_cli_pt
921
+ self.drag_img_look_pt = self.get_img_look_pt()
922
+ self.update_cursor()
923
+
924
+
925
+ def mouseReleaseEvent(self, ev):
926
+ super().mouseReleaseEvent(ev)
927
+ if not self.initialized:
928
+ return
929
+
930
+ self.update_mouse_info(QPoint_to_np(ev.pos()))
931
+
932
+ btn = ev.button()
933
+
934
+ if btn == Qt.LeftButton:
935
+ if self.op_mode == OpMode.EDIT_PTS:
936
+ if self.drag_type == DragType.POLY_PT:
937
+ self.drag_type = DragType.NONE
938
+ self.update()
939
+
940
+ elif btn == Qt.MiddleButton:
941
+ if self.drag_type == DragType.IMAGE_LOOK:
942
+ self.drag_type = DragType.NONE
943
+ self.update_cursor()
944
+ self.update()
945
+
946
+ def mouseMoveEvent(self, ev):
947
+ super().mouseMoveEvent(ev)
948
+ if not self.initialized:
949
+ return
950
+
951
+ prev_mouse_cli_pt = self.mouse_cli_pt
952
+ self.update_mouse_info(QPoint_to_np(ev.pos()))
953
+
954
+ if self.view_lock == ViewLock.CENTER:
955
+ if npla.norm(self.mouse_cli_pt - prev_mouse_cli_pt) >= 1:
956
+ self.img_look_pt = self.mouse_img_pt
957
+ QCursor.setPos ( self.mapToGlobal( QPoint_from_np(self.img_to_cli_pt(self.img_look_pt)) ))
958
+
959
+ self.update()
960
+
961
+ if self.drag_type == DragType.IMAGE_LOOK:
962
+ delta_pt = self.cli_to_img_pt(self.mouse_cli_pt) - self.cli_to_img_pt(self.drag_cli_pt)
963
+ self.img_look_pt = self.drag_img_look_pt - delta_pt
964
+ self.update()
965
+
966
+ if self.op_mode == OpMode.DRAW_PTS:
967
+ self.update()
968
+ elif self.op_mode == OpMode.EDIT_PTS:
969
+ if self.drag_type == DragType.POLY_PT:
970
+ delta_pt = self.cli_to_img_pt(self.mouse_cli_pt) - self.cli_to_img_pt(self.drag_cli_pt)
971
+ self.op_poly.set_point(self.drag_poly_pt_id, self.drag_poly_pt + delta_pt)
972
+ self.update()
973
+
974
+ def wheelEvent(self, ev):
975
+ super().wheelEvent(ev)
976
+
977
+ if not self.initialized:
978
+ return
979
+
980
+ mods = int(ev.modifiers())
981
+ delta = ev.angleDelta()
982
+
983
+ cli_pt = QPoint_to_np(ev.pos())
984
+
985
+ if self.drag_type == DragType.NONE:
986
+ sign = np.sign( delta.y() )
987
+ prev_img_pos = self.cli_to_img_pt (cli_pt)
988
+ delta_scale = sign*0.2 + sign * self.get_view_scale() / 10.0
989
+ self.view_scale = np.clip(self.get_view_scale() + delta_scale, 1.0, 20.0)
990
+ new_img_pos = self.cli_to_img_pt (cli_pt)
991
+ if sign > 0:
992
+ self.img_look_pt = self.get_img_look_pt() + (prev_img_pos-new_img_pos)#*1.5
993
+ else:
994
+ QCursor.setPos ( self.mapToGlobal(QPoint_from_np(self.img_to_cli_pt(prev_img_pos))) )
995
+ self.update()
996
+
997
+ def paintEvent(self, event):
998
+ super().paintEvent(event)
999
+ if not self.initialized:
1000
+ return
1001
+
1002
+ qp = self.qp
1003
+ qp.begin(self)
1004
+ qp.setRenderHint(QPainter.Antialiasing)
1005
+ qp.setRenderHint(QPainter.HighQualityAntialiasing)
1006
+ qp.setRenderHint(QPainter.SmoothPixmapTransform)
1007
+
1008
+ src_rect = QRect(0, 0, *self.img_size)
1009
+ dst_rect = self.img_to_cli_rect( src_rect )
1010
+
1011
+ if self.op_mode == OpMode.VIEW_BAKED:
1012
+ qp.drawPixmap(dst_rect, self.img_baked_pixmap, src_rect)
1013
+ elif self.op_mode == OpMode.VIEW_XSEG_MASK:
1014
+ if self.xseg_mask_pixmap is not None:
1015
+ qp.drawPixmap(dst_rect, self.xseg_mask_pixmap, src_rect)
1016
+ elif self.op_mode == OpMode.VIEW_XSEG_OVERLAY_MASK:
1017
+ if self.xseg_overlay_mask_pixmap is not None:
1018
+ qp.drawPixmap(dst_rect, self.xseg_overlay_mask_pixmap, src_rect)
1019
+ else:
1020
+ if self.img_pixmap is not None:
1021
+ qp.drawPixmap(dst_rect, self.img_pixmap, src_rect)
1022
+
1023
+ polys = self.ie_polys.get_polys()
1024
+ polys_len = len(polys)
1025
+
1026
+ color_scheme = self.get_current_color_scheme()
1027
+
1028
+ pt_rad = self.canvas_config.pt_radius
1029
+ pt_rad_x2 = pt_rad*2
1030
+
1031
+ pt_select_radius = self.canvas_config.pt_select_radius
1032
+
1033
+ op_mode = self.op_mode
1034
+ op_poly = self.op_poly
1035
+
1036
+ for i,poly in enumerate(polys):
1037
+
1038
+ selected_pt_path = QPainterPath()
1039
+ poly_line_path = QPainterPath()
1040
+ pts_line_path = QPainterPath()
1041
+
1042
+ pt_remove_cli_pt = None
1043
+ poly_pts = poly.get_pts()
1044
+ for pt_id, img_pt in enumerate(poly_pts):
1045
+ cli_pt = self.img_to_cli_pt(img_pt)
1046
+ q_cli_pt = QPoint_from_np(cli_pt)
1047
+
1048
+ if pt_id == 0:
1049
+ poly_line_path.moveTo(q_cli_pt)
1050
+ else:
1051
+ poly_line_path.lineTo(q_cli_pt)
1052
+
1053
+
1054
+ if poly == op_poly:
1055
+ if self.op_mode == OpMode.DRAW_PTS or \
1056
+ (self.op_mode == OpMode.EDIT_PTS and \
1057
+ (self.pt_edit_mode == PTEditMode.MOVE) or \
1058
+ (self.pt_edit_mode == PTEditMode.ADD_DEL and self.mouse_op_poly_pt_id == pt_id) \
1059
+ ):
1060
+ pts_line_path.moveTo( QPoint_from_np(cli_pt + np.float32([0,-pt_rad])) )
1061
+ pts_line_path.lineTo( QPoint_from_np(cli_pt + np.float32([0,pt_rad])) )
1062
+ pts_line_path.moveTo( QPoint_from_np(cli_pt + np.float32([-pt_rad,0])) )
1063
+ pts_line_path.lineTo( QPoint_from_np(cli_pt + np.float32([pt_rad,0])) )
1064
+
1065
+ if (self.op_mode == OpMode.EDIT_PTS and \
1066
+ self.pt_edit_mode == PTEditMode.ADD_DEL and \
1067
+ self.mouse_op_poly_pt_id == pt_id):
1068
+ pt_remove_cli_pt = cli_pt
1069
+
1070
+ if self.op_mode == OpMode.DRAW_PTS and \
1071
+ len(op_poly.get_pts()) >= 3 and pt_id == 0 and self.mouse_op_poly_pt_id == pt_id:
1072
+ # Circle around poly point
1073
+ selected_pt_path.addEllipse(q_cli_pt, pt_rad_x2, pt_rad_x2)
1074
+
1075
+
1076
+ if poly == op_poly:
1077
+ if op_mode == OpMode.DRAW_PTS:
1078
+ # Line from last point to mouse
1079
+ poly_line_path.lineTo( QPoint_from_np(self.mouse_cli_pt) )
1080
+
1081
+ if self.mouse_op_poly_pt_id is not None:
1082
+ pass
1083
+
1084
+ if self.mouse_op_poly_edge_id_pt is not None:
1085
+ if self.pt_edit_mode == PTEditMode.ADD_DEL and self.mouse_op_poly_pt_id is None:
1086
+ # Ready to insert point on edge
1087
+ m_cli_pt = self.mouse_op_poly_edge_id_pt
1088
+ pts_line_path.moveTo( QPoint_from_np(m_cli_pt + np.float32([0,-pt_rad])) )
1089
+ pts_line_path.lineTo( QPoint_from_np(m_cli_pt + np.float32([0,pt_rad])) )
1090
+ pts_line_path.moveTo( QPoint_from_np(m_cli_pt + np.float32([-pt_rad,0])) )
1091
+ pts_line_path.lineTo( QPoint_from_np(m_cli_pt + np.float32([pt_rad,0])) )
1092
+
1093
+ if len(poly_pts) >= 2:
1094
+ # Closing poly line
1095
+ poly_line_path.lineTo( QPoint_from_np(self.img_to_cli_pt(poly_pts[0])) )
1096
+
1097
+ # Draw calls
1098
+ qp.setPen(color_scheme.pt_outline_pen)
1099
+ qp.setBrush(QBrush())
1100
+ qp.drawPath(selected_pt_path)
1101
+
1102
+ qp.setPen(color_scheme.poly_outline_solid_pen)
1103
+ qp.setBrush(QBrush())
1104
+ qp.drawPath(pts_line_path)
1105
+
1106
+ if poly.get_type() == SegIEPolyType.INCLUDE:
1107
+ qp.setPen(color_scheme.poly_outline_solid_pen)
1108
+ else:
1109
+ qp.setPen(color_scheme.poly_outline_dot_pen)
1110
+
1111
+ qp.setBrush(color_scheme.poly_unselected_brush)
1112
+ if op_mode == OpMode.NONE:
1113
+ if poly == self.mouse_wire_poly:
1114
+ qp.setBrush(color_scheme.poly_selected_brush)
1115
+ #else:
1116
+ # if poly == op_poly:
1117
+ # qp.setBrush(color_scheme.poly_selected_brush)
1118
+
1119
+ qp.drawPath(poly_line_path)
1120
+
1121
+ if pt_remove_cli_pt is not None:
1122
+ qp.setPen(color_scheme.poly_outline_solid_pen)
1123
+ qp.setBrush(QBrush())
1124
+
1125
+ qp.drawLine( *(pt_remove_cli_pt + np.float32([-pt_rad_x2,-pt_rad_x2])), *(pt_remove_cli_pt + np.float32([pt_rad_x2,pt_rad_x2])) )
1126
+ qp.drawLine( *(pt_remove_cli_pt + np.float32([-pt_rad_x2,pt_rad_x2])), *(pt_remove_cli_pt + np.float32([pt_rad_x2,-pt_rad_x2])) )
1127
+
1128
+ qp.end()
1129
+
1130
+ class QCanvas(QFrame):
1131
+ def __init__(self):
1132
+ super().__init__()
1133
+
1134
+ self.canvas_control_left_bar = QCanvasControlsLeftBar()
1135
+ self.canvas_control_right_bar = QCanvasControlsRightBar()
1136
+
1137
+ cbar = sn( btn_poly_color_red_act = self.canvas_control_right_bar.btn_poly_color_red_act,
1138
+ btn_poly_color_green_act = self.canvas_control_right_bar.btn_poly_color_green_act,
1139
+ btn_poly_color_blue_act = self.canvas_control_right_bar.btn_poly_color_blue_act,
1140
+ btn_view_baked_mask_act = self.canvas_control_right_bar.btn_view_baked_mask_act,
1141
+ btn_view_xseg_mask_act = self.canvas_control_right_bar.btn_view_xseg_mask_act,
1142
+ btn_view_xseg_overlay_mask_act = self.canvas_control_right_bar.btn_view_xseg_overlay_mask_act,
1143
+ btn_poly_color_act_grp = self.canvas_control_right_bar.btn_poly_color_act_grp,
1144
+ btn_xseg_to_poly_act = self.canvas_control_right_bar.btn_xseg_to_poly_act,
1145
+
1146
+ btn_poly_type_include_act = self.canvas_control_left_bar.btn_poly_type_include_act,
1147
+ btn_poly_type_exclude_act = self.canvas_control_left_bar.btn_poly_type_exclude_act,
1148
+ btn_poly_type_act_grp = self.canvas_control_left_bar.btn_poly_type_act_grp,
1149
+ btn_undo_pt_act = self.canvas_control_left_bar.btn_undo_pt_act,
1150
+ btn_redo_pt_act = self.canvas_control_left_bar.btn_redo_pt_act,
1151
+ btn_delete_poly_act = self.canvas_control_left_bar.btn_delete_poly_act,
1152
+ btn_pt_edit_mode_act = self.canvas_control_left_bar.btn_pt_edit_mode_act,
1153
+ btn_view_lock_center_act = self.canvas_control_left_bar.btn_view_lock_center_act, )
1154
+
1155
+ self.op = QCanvasOperator(cbar)
1156
+ self.l = QHBoxLayout()
1157
+ self.l.setContentsMargins(0,0,0,0)
1158
+ self.l.addWidget(self.canvas_control_left_bar)
1159
+ self.l.addWidget(self.op)
1160
+ self.l.addWidget(self.canvas_control_right_bar)
1161
+ self.setLayout(self.l)
1162
+
1163
+ class LoaderQSubprocessor(QSubprocessor):
1164
+ def __init__(self, image_paths, q_label, q_progressbar, on_finish_func ):
1165
+
1166
+ self.image_paths = image_paths
1167
+ self.image_paths_len = len(image_paths)
1168
+ self.idxs = [*range(self.image_paths_len)]
1169
+
1170
+ self.filtered_image_paths = self.image_paths.copy()
1171
+
1172
+ self.image_paths_has_ie_polys = { image_path : False for image_path in self.image_paths }
1173
+
1174
+ self.q_label = q_label
1175
+ self.q_progressbar = q_progressbar
1176
+ self.q_progressbar.setRange(0, self.image_paths_len)
1177
+ self.q_progressbar.setValue(0)
1178
+ self.q_progressbar.update()
1179
+ self.on_finish_func = on_finish_func
1180
+ self.done_count = 0
1181
+ super().__init__('LoaderQSubprocessor', LoaderQSubprocessor.Cli, 60)
1182
+
1183
+ def get_data(self, host_dict):
1184
+ if len (self.idxs) > 0:
1185
+ idx = self.idxs.pop(0)
1186
+ image_path = self.image_paths[idx]
1187
+ self.q_label.setText(f'{QStringDB.loading_tip}... {image_path.name}')
1188
+
1189
+ return idx, image_path
1190
+
1191
+ return None
1192
+
1193
+ def on_clients_finalized(self):
1194
+ self.on_finish_func([x for x in self.filtered_image_paths if x is not None], self.image_paths_has_ie_polys)
1195
+
1196
+ def on_data_return (self, host_dict, data):
1197
+ self.idxs.insert(0, data[0])
1198
+
1199
+ def on_result (self, host_dict, data, result):
1200
+ idx, has_dflimg, has_ie_polys = result
1201
+
1202
+ if not has_dflimg:
1203
+ self.filtered_image_paths[idx] = None
1204
+ self.image_paths_has_ie_polys[self.image_paths[idx]] = has_ie_polys
1205
+
1206
+ self.done_count += 1
1207
+ if self.q_progressbar is not None:
1208
+ self.q_progressbar.setValue(self.done_count)
1209
+
1210
+ class Cli(QSubprocessor.Cli):
1211
+ def process_data(self, data):
1212
+ idx, filename = data
1213
+ dflimg = DFLIMG.load(filename)
1214
+ if dflimg is not None and dflimg.has_data():
1215
+ ie_polys = dflimg.get_seg_ie_polys()
1216
+
1217
+ return idx, True, ie_polys.has_polys()
1218
+ return idx, False, False
1219
+
1220
+ class MainWindow(QXMainWindow):
1221
+
1222
+ def __init__(self, input_dirpath, cfg_root_path):
1223
+ self.loading_frame = None
1224
+ self.help_frame = None
1225
+
1226
+ super().__init__()
1227
+
1228
+ self.input_dirpath = input_dirpath
1229
+ self.cfg_root_path = cfg_root_path
1230
+
1231
+ self.cfg_path = cfg_root_path / 'MainWindow_cfg.dat'
1232
+ self.cfg_dict = pickle.loads(self.cfg_path.read_bytes()) if self.cfg_path.exists() else {}
1233
+
1234
+ self.cached_images = {}
1235
+ self.cached_has_ie_polys = {}
1236
+
1237
+ self.initialize_ui()
1238
+
1239
+ # Loader
1240
+ self.loading_frame = QFrame(self.main_canvas_frame)
1241
+ self.loading_frame.setAutoFillBackground(True)
1242
+ self.loading_frame.setFrameShape(QFrame.StyledPanel)
1243
+ self.loader_label = QLabel()
1244
+ self.loader_progress_bar = QProgressBar()
1245
+
1246
+ intro_image = QLabel()
1247
+ intro_image.setPixmap( QPixmap.fromImage(QImageDB.intro) )
1248
+
1249
+ intro_image_frame_l = QVBoxLayout()
1250
+ intro_image_frame_l.addWidget(intro_image, alignment=Qt.AlignCenter)
1251
+ intro_image_frame = QFrame()
1252
+ intro_image_frame.setSizePolicy (QSizePolicy.Expanding, QSizePolicy.Expanding)
1253
+ intro_image_frame.setLayout(intro_image_frame_l)
1254
+
1255
+ loading_frame_l = QVBoxLayout()
1256
+ loading_frame_l.addWidget (intro_image_frame)
1257
+ loading_frame_l.addWidget (self.loader_label)
1258
+ loading_frame_l.addWidget (self.loader_progress_bar)
1259
+ self.loading_frame.setLayout(loading_frame_l)
1260
+
1261
+ self.loader_subprocessor = LoaderQSubprocessor( image_paths=pathex.get_image_paths(input_dirpath, return_Path_class=True),
1262
+ q_label=self.loader_label,
1263
+ q_progressbar=self.loader_progress_bar,
1264
+ on_finish_func=self.on_loader_finish )
1265
+
1266
+
1267
+ def on_loader_finish(self, image_paths, image_paths_has_ie_polys):
1268
+ self.image_paths_done = []
1269
+ self.image_paths = image_paths
1270
+ self.image_paths_has_ie_polys = image_paths_has_ie_polys
1271
+ self.set_has_ie_polys_count ( len([ 1 for x in self.image_paths_has_ie_polys if self.image_paths_has_ie_polys[x] == True]) )
1272
+ self.loading_frame.hide()
1273
+ self.loading_frame = None
1274
+
1275
+ self.process_next_image(first_initialization=True)
1276
+
1277
+ def closeEvent(self, ev):
1278
+ self.cfg_dict['geometry'] = self.saveGeometry().data()
1279
+ self.cfg_path.write_bytes( pickle.dumps(self.cfg_dict) )
1280
+
1281
+
1282
+ def update_cached_images (self, count=5):
1283
+ d = self.cached_images
1284
+
1285
+ for image_path in self.image_paths_done[:-count]+self.image_paths[count:]:
1286
+ if image_path in d:
1287
+ del d[image_path]
1288
+
1289
+ for image_path in self.image_paths[:count]+self.image_paths_done[-count:]:
1290
+ if image_path not in d:
1291
+ img = cv2_imread(image_path)
1292
+ if img is not None:
1293
+ d[image_path] = img
1294
+
1295
+ def load_image(self, image_path):
1296
+ try:
1297
+ img = self.cached_images.get(image_path, None)
1298
+ if img is None:
1299
+ img = cv2_imread(image_path)
1300
+ self.cached_images[image_path] = img
1301
+ if img is None:
1302
+ io.log_err(f'Unable to load {image_path}')
1303
+ except:
1304
+ img = None
1305
+
1306
+ return img
1307
+
1308
+ def update_preview_bar(self):
1309
+ count = self.image_bar.get_preview_images_count()
1310
+ d = self.cached_images
1311
+ prev_imgs = [ d.get(image_path, None) for image_path in self.image_paths_done[-1:-count:-1] ]
1312
+ next_imgs = [ d.get(image_path, None) for image_path in self.image_paths[:count] ]
1313
+ self.image_bar.update_images(prev_imgs, next_imgs)
1314
+
1315
+
1316
+ def canvas_initialize(self, image_path, only_has_polys=False):
1317
+ if only_has_polys and not self.image_paths_has_ie_polys[image_path]:
1318
+ return False
1319
+
1320
+ dflimg = DFLIMG.load(image_path)
1321
+ if not dflimg or not dflimg.has_data():
1322
+ return False
1323
+
1324
+ ie_polys = dflimg.get_seg_ie_polys()
1325
+ xseg_mask = dflimg.get_xseg_mask()
1326
+ img = self.load_image(image_path)
1327
+ if img is None:
1328
+ return False
1329
+
1330
+ self.canvas.op.initialize ( img, ie_polys=ie_polys, xseg_mask=xseg_mask )
1331
+
1332
+ self.filename_label.setText(f"{image_path.name}")
1333
+
1334
+ return True
1335
+
1336
+ def canvas_finalize(self, image_path):
1337
+ self.canvas.op.finalize()
1338
+
1339
+ if image_path.exists():
1340
+ dflimg = DFLIMG.load(image_path)
1341
+ ie_polys = dflimg.get_seg_ie_polys()
1342
+ new_ie_polys = self.canvas.op.get_ie_polys()
1343
+
1344
+ if not new_ie_polys.identical(ie_polys):
1345
+ new_has_ie_polys = new_ie_polys.has_polys()
1346
+ self.set_has_ie_polys_count ( self.get_has_ie_polys_count() + (1 if new_has_ie_polys else -1) )
1347
+ self.image_paths_has_ie_polys[image_path] = new_has_ie_polys
1348
+ dflimg.set_seg_ie_polys( new_ie_polys )
1349
+ dflimg.save()
1350
+
1351
+ self.filename_label.setText(f"")
1352
+
1353
+ def process_prev_image(self):
1354
+ key_mods = QApplication.keyboardModifiers()
1355
+ step = 5 if key_mods == Qt.ShiftModifier else 1
1356
+ only_has_polys = key_mods == Qt.ControlModifier
1357
+
1358
+ if self.canvas.op.is_initialized():
1359
+ self.canvas_finalize(self.image_paths[0])
1360
+
1361
+ while True:
1362
+ for _ in range(step):
1363
+ if len(self.image_paths_done) != 0:
1364
+ self.image_paths.insert (0, self.image_paths_done.pop(-1))
1365
+ else:
1366
+ break
1367
+ if len(self.image_paths) == 0:
1368
+ break
1369
+
1370
+ ret = self.canvas_initialize(self.image_paths[0], len(self.image_paths_done) != 0 and only_has_polys)
1371
+
1372
+ if ret or len(self.image_paths_done) == 0:
1373
+ break
1374
+
1375
+ self.update_cached_images()
1376
+ self.update_preview_bar()
1377
+
1378
+ def process_next_image(self, first_initialization=False):
1379
+ key_mods = QApplication.keyboardModifiers()
1380
+
1381
+ step = 0 if first_initialization else 5 if key_mods == Qt.ShiftModifier else 1
1382
+ only_has_polys = False if first_initialization else key_mods == Qt.ControlModifier
1383
+
1384
+ if self.canvas.op.is_initialized():
1385
+ self.canvas_finalize(self.image_paths[0])
1386
+
1387
+ while True:
1388
+ for _ in range(step):
1389
+ if len(self.image_paths) != 0:
1390
+ self.image_paths_done.append(self.image_paths.pop(0))
1391
+ else:
1392
+ break
1393
+ if len(self.image_paths) == 0:
1394
+ break
1395
+ if self.canvas_initialize(self.image_paths[0], only_has_polys):
1396
+ break
1397
+
1398
+ self.update_cached_images()
1399
+ self.update_preview_bar()
1400
+
1401
+ def initialize_ui(self):
1402
+
1403
+ self.canvas = QCanvas()
1404
+
1405
+ image_bar = self.image_bar = ImagePreviewSequenceBar(preview_images_count=9, icon_size=QUIConfig.preview_bar_icon_q_size.width())
1406
+ image_bar.setSizePolicy ( QSizePolicy.Fixed, QSizePolicy.Fixed )
1407
+
1408
+
1409
+ btn_prev_image = QXIconButton(QIconDB.left, QStringDB.btn_prev_image_tip, shortcut='A', click_func=self.process_prev_image)
1410
+ btn_prev_image.setIconSize(QUIConfig.preview_bar_icon_q_size)
1411
+
1412
+ btn_next_image = QXIconButton(QIconDB.right, QStringDB.btn_next_image_tip, shortcut='D', click_func=self.process_next_image)
1413
+ btn_next_image.setIconSize(QUIConfig.preview_bar_icon_q_size)
1414
+
1415
+
1416
+ preview_image_bar_frame_l = QHBoxLayout()
1417
+ preview_image_bar_frame_l.setContentsMargins(0,0,0,0)
1418
+ preview_image_bar_frame_l.addWidget ( btn_prev_image, alignment=Qt.AlignCenter)
1419
+ preview_image_bar_frame_l.addWidget ( image_bar)
1420
+ preview_image_bar_frame_l.addWidget ( btn_next_image, alignment=Qt.AlignCenter)
1421
+
1422
+ preview_image_bar_frame = QFrame()
1423
+ preview_image_bar_frame.setSizePolicy ( QSizePolicy.Fixed, QSizePolicy.Fixed )
1424
+ preview_image_bar_frame.setLayout(preview_image_bar_frame_l)
1425
+
1426
+ preview_image_bar_l = QHBoxLayout()
1427
+ preview_image_bar_l.addWidget (preview_image_bar_frame)
1428
+
1429
+ preview_image_bar = QFrame()
1430
+ preview_image_bar.setFrameShape(QFrame.StyledPanel)
1431
+ preview_image_bar.setSizePolicy ( QSizePolicy.Expanding, QSizePolicy.Fixed )
1432
+ preview_image_bar.setLayout(preview_image_bar_l)
1433
+
1434
+ label_font = QFont('Courier New')
1435
+ self.filename_label = QLabel()
1436
+ self.filename_label.setFont(label_font)
1437
+
1438
+ self.has_ie_polys_count_label = QLabel()
1439
+
1440
+ status_frame_l = QHBoxLayout()
1441
+ status_frame_l.setContentsMargins(0,0,0,0)
1442
+ status_frame_l.addWidget ( QLabel(), alignment=Qt.AlignCenter)
1443
+ status_frame_l.addWidget (self.filename_label, alignment=Qt.AlignCenter)
1444
+ status_frame_l.addWidget (self.has_ie_polys_count_label, alignment=Qt.AlignCenter)
1445
+ status_frame = QFrame()
1446
+ status_frame.setLayout(status_frame_l)
1447
+
1448
+ main_canvas_l = QVBoxLayout()
1449
+ main_canvas_l.setContentsMargins(0,0,0,0)
1450
+ main_canvas_l.addWidget (self.canvas)
1451
+ main_canvas_l.addWidget (status_frame)
1452
+ main_canvas_l.addWidget (preview_image_bar)
1453
+
1454
+ self.main_canvas_frame = QFrame()
1455
+ self.main_canvas_frame.setLayout(main_canvas_l)
1456
+
1457
+ self.main_l = QHBoxLayout()
1458
+ self.main_l.setContentsMargins(0,0,0,0)
1459
+ self.main_l.addWidget (self.main_canvas_frame)
1460
+
1461
+ self.setLayout(self.main_l)
1462
+
1463
+ geometry = self.cfg_dict.get('geometry', None)
1464
+ if geometry is not None:
1465
+ self.restoreGeometry(geometry)
1466
+ else:
1467
+ self.move( QPoint(0,0))
1468
+
1469
+ def get_has_ie_polys_count(self):
1470
+ return self.has_ie_polys_count
1471
+
1472
+ def set_has_ie_polys_count(self, c):
1473
+ self.has_ie_polys_count = c
1474
+ self.has_ie_polys_count_label.setText(f"{c} {QStringDB.labeled_tip}")
1475
+
1476
+ def resizeEvent(self, ev):
1477
+ if self.loading_frame is not None:
1478
+ self.loading_frame.resize( ev.size() )
1479
+ if self.help_frame is not None:
1480
+ self.help_frame.resize( ev.size() )
1481
+
1482
+ def start(input_dirpath):
1483
+ """
1484
+ returns exit_code
1485
+ """
1486
+ io.log_info("Running XSeg editor.")
1487
+
1488
+ if PackedFaceset.path_contains(input_dirpath):
1489
+ io.log_info (f'\n{input_dirpath} contains packed faceset! Unpack it first.\n')
1490
+ return 1
1491
+
1492
+ root_path = Path(__file__).parent
1493
+ cfg_root_path = Path(tempfile.gettempdir())
1494
+
1495
+ QApplication.setAttribute(Qt.AA_EnableHighDpiScaling, True)
1496
+ QApplication.setAttribute(Qt.AA_UseHighDpiPixmaps, True)
1497
+
1498
+ app = QApplication([])
1499
+ app.setApplicationName("XSegEditor")
1500
+ app.setStyle('Fusion')
1501
+
1502
+ QFontDatabase.addApplicationFont( str(root_path / 'gfx' / 'fonts' / 'NotoSans-Medium.ttf') )
1503
+
1504
+ app.setFont( QFont('NotoSans'))
1505
+
1506
+ QUIConfig.initialize()
1507
+ QStringDB.initialize()
1508
+
1509
+ QIconDB.initialize( root_path / 'gfx' / 'icons' )
1510
+ QCursorDB.initialize( root_path / 'gfx' / 'cursors' )
1511
+ QImageDB.initialize( root_path / 'gfx' / 'images' )
1512
+
1513
+ app.setWindowIcon(QIconDB.app_icon)
1514
+ app.setPalette( QDarkPalette() )
1515
+
1516
+ win = MainWindow( input_dirpath=input_dirpath, cfg_root_path=cfg_root_path)
1517
+
1518
+ win.show()
1519
+ win.raise_()
1520
+
1521
+ app.exec_()
1522
+ return 0
XSegEditor/XSegEditor.py ADDED
@@ -0,0 +1,1494 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import multiprocessing
3
+ import os
4
+ import pickle
5
+ import sys
6
+ import tempfile
7
+ import time
8
+ import traceback
9
+ from enum import IntEnum
10
+ from types import SimpleNamespace as sn
11
+
12
+ import cv2
13
+ import numpy as np
14
+ import numpy.linalg as npla
15
+ from PyQt5.QtCore import *
16
+ from PyQt5.QtGui import *
17
+ from PyQt5.QtWidgets import *
18
+
19
+ from core import imagelib, pathex
20
+ from core.cv2ex import *
21
+ from core.imagelib import SegIEPoly, SegIEPolys, SegIEPolyType, sd
22
+ from core.qtex import *
23
+ from DFLIMG import *
24
+ from localization import StringsDB, system_language
25
+ from samplelib import PackedFaceset
26
+
27
+ from .QCursorDB import QCursorDB
28
+ from .QIconDB import QIconDB
29
+ from .QStringDB import QStringDB
30
+ from .QImageDB import QImageDB
31
+
32
+ class OpMode(IntEnum):
33
+ NONE = 0
34
+ DRAW_PTS = 1
35
+ EDIT_PTS = 2
36
+ VIEW_BAKED = 3
37
+ VIEW_XSEG_MASK = 4
38
+
39
+ class PTEditMode(IntEnum):
40
+ MOVE = 0
41
+ ADD_DEL = 1
42
+
43
+ class DragType(IntEnum):
44
+ NONE = 0
45
+ IMAGE_LOOK = 1
46
+ POLY_PT = 2
47
+
48
+ class ViewLock(IntEnum):
49
+ NONE = 0
50
+ CENTER = 1
51
+
52
+ class QUIConfig():
53
+ @staticmethod
54
+ def initialize(icon_size = 48, icon_spacer_size=16, preview_bar_icon_size=64):
55
+ QUIConfig.icon_q_size = QSize(icon_size, icon_size)
56
+ QUIConfig.icon_spacer_q_size = QSize(icon_spacer_size, icon_spacer_size)
57
+ QUIConfig.preview_bar_icon_q_size = QSize(preview_bar_icon_size, preview_bar_icon_size)
58
+
59
+ class ImagePreviewSequenceBar(QFrame):
60
+ def __init__(self, preview_images_count, icon_size):
61
+ super().__init__()
62
+ self.preview_images_count = preview_images_count = max(1, preview_images_count + (preview_images_count % 2 -1) )
63
+
64
+ self.icon_size = icon_size
65
+
66
+ black_q_img = QImage(np.zeros( (icon_size,icon_size,3) ).data, icon_size, icon_size, 3*icon_size, QImage.Format_RGB888)
67
+ self.black_q_pixmap = QPixmap.fromImage(black_q_img)
68
+
69
+ self.image_containers = [ QLabel() for i in range(preview_images_count)]
70
+
71
+ main_frame_l_cont_hl = QGridLayout()
72
+ main_frame_l_cont_hl.setContentsMargins(0,0,0,0)
73
+ #main_frame_l_cont_hl.setSpacing(0)
74
+
75
+
76
+
77
+ for i in range(len(self.image_containers)):
78
+ q_label = self.image_containers[i]
79
+ q_label.setScaledContents(True)
80
+ if i == preview_images_count//2:
81
+ q_label.setMinimumSize(icon_size+16, icon_size+16 )
82
+ q_label.setMaximumSize(icon_size+16, icon_size+16 )
83
+ else:
84
+ q_label.setMinimumSize(icon_size, icon_size )
85
+ q_label.setMaximumSize(icon_size, icon_size )
86
+ opacity_effect = QGraphicsOpacityEffect()
87
+ opacity_effect.setOpacity(0.5)
88
+ q_label.setGraphicsEffect(opacity_effect)
89
+
90
+ q_label.setSizePolicy (QSizePolicy.Fixed, QSizePolicy.Fixed)
91
+
92
+ main_frame_l_cont_hl.addWidget (q_label, 0, i)
93
+
94
+ self.setLayout(main_frame_l_cont_hl)
95
+
96
+ self.prev_img_conts = self.image_containers[(preview_images_count//2) -1::-1]
97
+ self.next_img_conts = self.image_containers[preview_images_count//2:]
98
+
99
+ self.update_images()
100
+
101
+ def get_preview_images_count(self):
102
+ return self.preview_images_count
103
+
104
+ def update_images(self, prev_imgs=None, next_imgs=None):
105
+ # Fix arrays
106
+ if prev_imgs is None:
107
+ prev_imgs = []
108
+ prev_img_conts_len = len(self.prev_img_conts)
109
+ prev_q_imgs_len = len(prev_imgs)
110
+ if prev_q_imgs_len < prev_img_conts_len:
111
+ for i in range ( prev_img_conts_len - prev_q_imgs_len ):
112
+ prev_imgs.append(None)
113
+ elif prev_q_imgs_len > prev_img_conts_len:
114
+ prev_imgs = prev_imgs[:prev_img_conts_len]
115
+
116
+ if next_imgs is None:
117
+ next_imgs = []
118
+ next_img_conts_len = len(self.next_img_conts)
119
+ next_q_imgs_len = len(next_imgs)
120
+ if next_q_imgs_len < next_img_conts_len:
121
+ for i in range ( next_img_conts_len - next_q_imgs_len ):
122
+ next_imgs.append(None)
123
+ elif next_q_imgs_len > next_img_conts_len:
124
+ next_imgs = next_imgs[:next_img_conts_len]
125
+
126
+ for i,img in enumerate(prev_imgs):
127
+ self.prev_img_conts[i].setPixmap( QPixmap.fromImage( QImage_from_np(img) ) if img is not None else self.black_q_pixmap )
128
+
129
+ for i,img in enumerate(next_imgs):
130
+ self.next_img_conts[i].setPixmap( QPixmap.fromImage( QImage_from_np(img) ) if img is not None else self.black_q_pixmap )
131
+
132
+ class ColorScheme():
133
+ def __init__(self, unselected_color, selected_color, outline_color, outline_width, pt_outline_color, cross_cursor):
134
+ self.poly_unselected_brush = QBrush(unselected_color)
135
+ self.poly_selected_brush = QBrush(selected_color)
136
+
137
+ self.poly_outline_solid_pen = QPen(outline_color, outline_width, Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin)
138
+ self.poly_outline_dot_pen = QPen(outline_color, outline_width, Qt.DotLine, Qt.RoundCap, Qt.RoundJoin)
139
+
140
+ self.pt_outline_pen = QPen(pt_outline_color)
141
+ self.cross_cursor = cross_cursor
142
+
143
+ class CanvasConfig():
144
+
145
+ def __init__(self,
146
+ pt_radius=4,
147
+ pt_select_radius=8,
148
+ color_schemes=None,
149
+ **kwargs):
150
+ self.pt_radius = pt_radius
151
+ self.pt_select_radius = pt_select_radius
152
+
153
+ if color_schemes is None:
154
+ color_schemes = [
155
+ ColorScheme( QColor(192,0,0,alpha=0), QColor(192,0,0,alpha=72), QColor(192,0,0), 2, QColor(255,255,255), QCursorDB.cross_red ),
156
+ ColorScheme( QColor(0,192,0,alpha=0), QColor(0,192,0,alpha=72), QColor(0,192,0), 2, QColor(255,255,255), QCursorDB.cross_green ),
157
+ ColorScheme( QColor(0,0,192,alpha=0), QColor(0,0,192,alpha=72), QColor(0,0,192), 2, QColor(255,255,255), QCursorDB.cross_blue ),
158
+ ]
159
+ self.color_schemes = color_schemes
160
+
161
+ class QCanvasControlsLeftBar(QFrame):
162
+
163
+ def __init__(self):
164
+ super().__init__()
165
+ #==============================================
166
+ btn_poly_type_include = QToolButton()
167
+ self.btn_poly_type_include_act = QActionEx( QIconDB.poly_type_include, QStringDB.btn_poly_type_include_tip, shortcut='Q', shortcut_in_tooltip=True, is_checkable=True)
168
+ btn_poly_type_include.setDefaultAction(self.btn_poly_type_include_act)
169
+ btn_poly_type_include.setIconSize(QUIConfig.icon_q_size)
170
+
171
+ btn_poly_type_exclude = QToolButton()
172
+ self.btn_poly_type_exclude_act = QActionEx( QIconDB.poly_type_exclude, QStringDB.btn_poly_type_exclude_tip, shortcut='W', shortcut_in_tooltip=True, is_checkable=True)
173
+ btn_poly_type_exclude.setDefaultAction(self.btn_poly_type_exclude_act)
174
+ btn_poly_type_exclude.setIconSize(QUIConfig.icon_q_size)
175
+
176
+ self.btn_poly_type_act_grp = QActionGroup (self)
177
+ self.btn_poly_type_act_grp.addAction(self.btn_poly_type_include_act)
178
+ self.btn_poly_type_act_grp.addAction(self.btn_poly_type_exclude_act)
179
+ self.btn_poly_type_act_grp.setExclusive(True)
180
+ #==============================================
181
+ btn_undo_pt = QToolButton()
182
+ self.btn_undo_pt_act = QActionEx( QIconDB.undo_pt, QStringDB.btn_undo_pt_tip, shortcut='Ctrl+Z', shortcut_in_tooltip=True, is_auto_repeat=True)
183
+ btn_undo_pt.setDefaultAction(self.btn_undo_pt_act)
184
+ btn_undo_pt.setIconSize(QUIConfig.icon_q_size)
185
+
186
+ btn_redo_pt = QToolButton()
187
+ self.btn_redo_pt_act = QActionEx( QIconDB.redo_pt, QStringDB.btn_redo_pt_tip, shortcut='Ctrl+Shift+Z', shortcut_in_tooltip=True, is_auto_repeat=True)
188
+ btn_redo_pt.setDefaultAction(self.btn_redo_pt_act)
189
+ btn_redo_pt.setIconSize(QUIConfig.icon_q_size)
190
+
191
+ btn_delete_poly = QToolButton()
192
+ self.btn_delete_poly_act = QActionEx( QIconDB.delete_poly, QStringDB.btn_delete_poly_tip, shortcut='Delete', shortcut_in_tooltip=True)
193
+ btn_delete_poly.setDefaultAction(self.btn_delete_poly_act)
194
+ btn_delete_poly.setIconSize(QUIConfig.icon_q_size)
195
+ #==============================================
196
+ btn_pt_edit_mode = QToolButton()
197
+ self.btn_pt_edit_mode_act = QActionEx( QIconDB.pt_edit_mode, QStringDB.btn_pt_edit_mode_tip, shortcut_in_tooltip=True, is_checkable=True)
198
+ btn_pt_edit_mode.setDefaultAction(self.btn_pt_edit_mode_act)
199
+ btn_pt_edit_mode.setIconSize(QUIConfig.icon_q_size)
200
+ #==============================================
201
+
202
+ controls_bar_frame2_l = QVBoxLayout()
203
+ controls_bar_frame2_l.addWidget ( btn_poly_type_include )
204
+ controls_bar_frame2_l.addWidget ( btn_poly_type_exclude )
205
+ controls_bar_frame2 = QFrame()
206
+ controls_bar_frame2.setFrameShape(QFrame.StyledPanel)
207
+ controls_bar_frame2.setSizePolicy (QSizePolicy.Fixed, QSizePolicy.Fixed)
208
+ controls_bar_frame2.setLayout(controls_bar_frame2_l)
209
+
210
+ controls_bar_frame3_l = QVBoxLayout()
211
+ controls_bar_frame3_l.addWidget ( btn_undo_pt )
212
+ controls_bar_frame3_l.addWidget ( btn_redo_pt )
213
+ controls_bar_frame3_l.addWidget ( btn_delete_poly )
214
+ controls_bar_frame3 = QFrame()
215
+ controls_bar_frame3.setFrameShape(QFrame.StyledPanel)
216
+ controls_bar_frame3.setSizePolicy (QSizePolicy.Fixed, QSizePolicy.Fixed)
217
+ controls_bar_frame3.setLayout(controls_bar_frame3_l)
218
+
219
+ controls_bar_frame4_l = QVBoxLayout()
220
+ controls_bar_frame4_l.addWidget ( btn_pt_edit_mode )
221
+ controls_bar_frame4 = QFrame()
222
+ controls_bar_frame4.setFrameShape(QFrame.StyledPanel)
223
+ controls_bar_frame4.setSizePolicy (QSizePolicy.Fixed, QSizePolicy.Fixed)
224
+ controls_bar_frame4.setLayout(controls_bar_frame4_l)
225
+
226
+ controls_bar_l = QVBoxLayout()
227
+ controls_bar_l.setContentsMargins(0,0,0,0)
228
+ controls_bar_l.addWidget(controls_bar_frame2)
229
+ controls_bar_l.addWidget(controls_bar_frame3)
230
+ controls_bar_l.addWidget(controls_bar_frame4)
231
+
232
+ self.setSizePolicy ( QSizePolicy.Fixed, QSizePolicy.Expanding )
233
+ self.setLayout(controls_bar_l)
234
+
235
+ class QCanvasControlsRightBar(QFrame):
236
+
237
+ def __init__(self):
238
+ super().__init__()
239
+ #==============================================
240
+ btn_poly_color_red = QToolButton()
241
+ self.btn_poly_color_red_act = QActionEx( QIconDB.poly_color_red, QStringDB.btn_poly_color_red_tip, shortcut='1', shortcut_in_tooltip=True, is_checkable=True)
242
+ btn_poly_color_red.setDefaultAction(self.btn_poly_color_red_act)
243
+ btn_poly_color_red.setIconSize(QUIConfig.icon_q_size)
244
+
245
+ btn_poly_color_green = QToolButton()
246
+ self.btn_poly_color_green_act = QActionEx( QIconDB.poly_color_green, QStringDB.btn_poly_color_green_tip, shortcut='2', shortcut_in_tooltip=True, is_checkable=True)
247
+ btn_poly_color_green.setDefaultAction(self.btn_poly_color_green_act)
248
+ btn_poly_color_green.setIconSize(QUIConfig.icon_q_size)
249
+
250
+ btn_poly_color_blue = QToolButton()
251
+ self.btn_poly_color_blue_act = QActionEx( QIconDB.poly_color_blue, QStringDB.btn_poly_color_blue_tip, shortcut='3', shortcut_in_tooltip=True, is_checkable=True)
252
+ btn_poly_color_blue.setDefaultAction(self.btn_poly_color_blue_act)
253
+ btn_poly_color_blue.setIconSize(QUIConfig.icon_q_size)
254
+
255
+ btn_view_baked_mask = QToolButton()
256
+ self.btn_view_baked_mask_act = QActionEx( QIconDB.view_baked, QStringDB.btn_view_baked_mask_tip, shortcut='4', shortcut_in_tooltip=True, is_checkable=True)
257
+ btn_view_baked_mask.setDefaultAction(self.btn_view_baked_mask_act)
258
+ btn_view_baked_mask.setIconSize(QUIConfig.icon_q_size)
259
+
260
+ btn_view_xseg_mask = QToolButton()
261
+ self.btn_view_xseg_mask_act = QActionEx( QIconDB.view_xseg, QStringDB.btn_view_xseg_mask_tip, shortcut='5', shortcut_in_tooltip=True, is_checkable=True)
262
+ btn_view_xseg_mask.setDefaultAction(self.btn_view_xseg_mask_act)
263
+ btn_view_xseg_mask.setIconSize(QUIConfig.icon_q_size)
264
+
265
+ btn_view_xseg_overlay_mask = QToolButton()
266
+ self.btn_view_xseg_overlay_mask_act = QActionEx( QIconDB.view_xseg_overlay, QStringDB.btn_view_xseg_overlay_mask_tip, shortcut='`', shortcut_in_tooltip=True, is_checkable=True)
267
+ btn_view_xseg_overlay_mask.setDefaultAction(self.btn_view_xseg_overlay_mask_act)
268
+ btn_view_xseg_overlay_mask.setIconSize(QUIConfig.icon_q_size)
269
+
270
+ self.btn_poly_color_act_grp = QActionGroup (self)
271
+ self.btn_poly_color_act_grp.addAction(self.btn_poly_color_red_act)
272
+ self.btn_poly_color_act_grp.addAction(self.btn_poly_color_green_act)
273
+ self.btn_poly_color_act_grp.addAction(self.btn_poly_color_blue_act)
274
+ self.btn_poly_color_act_grp.addAction(self.btn_view_baked_mask_act)
275
+ self.btn_poly_color_act_grp.addAction(self.btn_view_xseg_mask_act)
276
+ self.btn_poly_color_act_grp.setExclusive(True)
277
+ #==============================================
278
+ btn_view_lock_center = QToolButton()
279
+ self.btn_view_lock_center_act = QActionEx( QIconDB.view_lock_center, QStringDB.btn_view_lock_center_tip, shortcut_in_tooltip=True, is_checkable=True)
280
+ btn_view_lock_center.setDefaultAction(self.btn_view_lock_center_act)
281
+ btn_view_lock_center.setIconSize(QUIConfig.icon_q_size)
282
+
283
+ controls_bar_frame2_l = QVBoxLayout()
284
+ controls_bar_frame2_l.addWidget ( btn_view_xseg_overlay_mask )
285
+ controls_bar_frame2 = QFrame()
286
+ controls_bar_frame2.setFrameShape(QFrame.StyledPanel)
287
+ controls_bar_frame2.setSizePolicy (QSizePolicy.Fixed, QSizePolicy.Fixed)
288
+ controls_bar_frame2.setLayout(controls_bar_frame2_l)
289
+
290
+ controls_bar_frame1_l = QVBoxLayout()
291
+ controls_bar_frame1_l.addWidget ( btn_poly_color_red )
292
+ controls_bar_frame1_l.addWidget ( btn_poly_color_green )
293
+ controls_bar_frame1_l.addWidget ( btn_poly_color_blue )
294
+ controls_bar_frame1_l.addWidget ( btn_view_baked_mask )
295
+ controls_bar_frame1_l.addWidget ( btn_view_xseg_mask )
296
+ controls_bar_frame1 = QFrame()
297
+ controls_bar_frame1.setFrameShape(QFrame.StyledPanel)
298
+ controls_bar_frame1.setSizePolicy (QSizePolicy.Fixed, QSizePolicy.Fixed)
299
+ controls_bar_frame1.setLayout(controls_bar_frame1_l)
300
+
301
+ controls_bar_frame3_l = QVBoxLayout()
302
+ controls_bar_frame3_l.addWidget ( btn_view_lock_center )
303
+ controls_bar_frame3 = QFrame()
304
+ controls_bar_frame3.setFrameShape(QFrame.StyledPanel)
305
+ controls_bar_frame3.setSizePolicy (QSizePolicy.Fixed, QSizePolicy.Fixed)
306
+ controls_bar_frame3.setLayout(controls_bar_frame3_l)
307
+
308
+ controls_bar_l = QVBoxLayout()
309
+ controls_bar_l.setContentsMargins(0,0,0,0)
310
+ controls_bar_l.addWidget(controls_bar_frame2)
311
+ controls_bar_l.addWidget(controls_bar_frame1)
312
+ controls_bar_l.addWidget(controls_bar_frame3)
313
+
314
+ self.setSizePolicy ( QSizePolicy.Fixed, QSizePolicy.Expanding )
315
+ self.setLayout(controls_bar_l)
316
+
317
+ class QCanvasOperator(QWidget):
318
+ def __init__(self, cbar):
319
+ super().__init__()
320
+ self.cbar = cbar
321
+
322
+ self.set_cbar_disabled()
323
+
324
+ self.cbar.btn_poly_color_red_act.triggered.connect ( lambda : self.set_color_scheme_id(0) )
325
+ self.cbar.btn_poly_color_green_act.triggered.connect ( lambda : self.set_color_scheme_id(1) )
326
+ self.cbar.btn_poly_color_blue_act.triggered.connect ( lambda : self.set_color_scheme_id(2) )
327
+ self.cbar.btn_view_baked_mask_act.triggered.connect ( lambda : self.set_op_mode(OpMode.VIEW_BAKED) )
328
+ self.cbar.btn_view_xseg_mask_act.triggered.connect ( lambda : self.set_op_mode(OpMode.VIEW_XSEG_MASK) )
329
+
330
+ self.cbar.btn_view_xseg_overlay_mask_act.toggled.connect ( lambda is_checked: self.update() )
331
+
332
+ self.cbar.btn_poly_type_include_act.triggered.connect ( lambda : self.set_poly_include_type(SegIEPolyType.INCLUDE) )
333
+ self.cbar.btn_poly_type_exclude_act.triggered.connect ( lambda : self.set_poly_include_type(SegIEPolyType.EXCLUDE) )
334
+
335
+ self.cbar.btn_undo_pt_act.triggered.connect ( lambda : self.action_undo_pt() )
336
+ self.cbar.btn_redo_pt_act.triggered.connect ( lambda : self.action_redo_pt() )
337
+
338
+ self.cbar.btn_delete_poly_act.triggered.connect ( lambda : self.action_delete_poly() )
339
+
340
+ self.cbar.btn_pt_edit_mode_act.toggled.connect ( lambda is_checked: self.set_pt_edit_mode( PTEditMode.ADD_DEL if is_checked else PTEditMode.MOVE ) )
341
+ self.cbar.btn_view_lock_center_act.toggled.connect ( lambda is_checked: self.set_view_lock( ViewLock.CENTER if is_checked else ViewLock.NONE ) )
342
+
343
+ self.mouse_in_widget = False
344
+
345
+ QXMainWindow.inst.add_keyPressEvent_listener ( self.on_keyPressEvent )
346
+ QXMainWindow.inst.add_keyReleaseEvent_listener ( self.on_keyReleaseEvent )
347
+
348
+ self.qp = QPainter()
349
+ self.initialized = False
350
+ self.last_state = None
351
+
352
+ def initialize(self, img, img_look_pt=None, view_scale=None, ie_polys=None, xseg_mask=None, canvas_config=None ):
353
+ q_img = self.q_img = QImage_from_np(img)
354
+ self.img_pixmap = QPixmap.fromImage(q_img)
355
+
356
+ self.xseg_mask_pixmap = None
357
+ self.xseg_overlay_mask_pixmap = None
358
+ if xseg_mask is not None:
359
+ h,w,c = img.shape
360
+ xseg_mask = cv2.resize(xseg_mask, (w,h), interpolation=cv2.INTER_CUBIC)
361
+ xseg_mask = imagelib.normalize_channels(xseg_mask, 1)
362
+ xseg_img = img.astype(np.float32)/255.0
363
+ xseg_overlay_mask = xseg_img*(1-xseg_mask)*0.5 + xseg_img*xseg_mask
364
+ xseg_overlay_mask = np.clip(xseg_overlay_mask*255, 0, 255).astype(np.uint8)
365
+ xseg_mask = np.clip(xseg_mask*255, 0, 255).astype(np.uint8)
366
+ self.xseg_mask_pixmap = QPixmap.fromImage(QImage_from_np(xseg_mask))
367
+ self.xseg_overlay_mask_pixmap = QPixmap.fromImage(QImage_from_np(xseg_overlay_mask))
368
+
369
+ self.img_size = QSize_to_np (self.img_pixmap.size())
370
+
371
+ self.img_look_pt = img_look_pt
372
+ self.view_scale = view_scale
373
+
374
+ if ie_polys is None:
375
+ ie_polys = SegIEPolys()
376
+ self.ie_polys = ie_polys
377
+
378
+ if canvas_config is None:
379
+ canvas_config = CanvasConfig()
380
+ self.canvas_config = canvas_config
381
+
382
+ # UI init
383
+ self.set_cbar_disabled()
384
+ self.cbar.btn_poly_color_act_grp.setDisabled(False)
385
+ self.cbar.btn_view_xseg_overlay_mask_act.setDisabled(False)
386
+ self.cbar.btn_poly_type_act_grp.setDisabled(False)
387
+
388
+ # Initial vars
389
+ self.current_cursor = None
390
+ self.mouse_hull_poly = None
391
+ self.mouse_wire_poly = None
392
+ self.drag_type = DragType.NONE
393
+ self.mouse_cli_pt = np.zeros((2,), np.float32 )
394
+
395
+ # Initial state
396
+ self.set_op_mode(OpMode.NONE)
397
+ self.set_color_scheme_id(1)
398
+ self.set_poly_include_type(SegIEPolyType.INCLUDE)
399
+ self.set_pt_edit_mode(PTEditMode.MOVE)
400
+ self.set_view_lock(ViewLock.NONE)
401
+
402
+ # Apply last state
403
+ if self.last_state is not None:
404
+ self.set_color_scheme_id(self.last_state.color_scheme_id)
405
+ if self.last_state.op_mode is not None:
406
+ self.set_op_mode(self.last_state.op_mode)
407
+
408
+ self.initialized = True
409
+
410
+ self.setMouseTracking(True)
411
+ self.update_cursor()
412
+ self.update()
413
+
414
+
415
+ def finalize(self):
416
+ if self.initialized:
417
+ if self.op_mode == OpMode.DRAW_PTS:
418
+ self.set_op_mode(OpMode.EDIT_PTS)
419
+
420
+ self.last_state = sn(op_mode = self.op_mode if self.op_mode in [OpMode.VIEW_BAKED, OpMode.VIEW_XSEG_MASK] else None,
421
+ color_scheme_id = self.color_scheme_id)
422
+
423
+ self.img_pixmap = None
424
+ self.update_cursor(is_finalize=True)
425
+ self.setMouseTracking(False)
426
+ self.setFocusPolicy(Qt.NoFocus)
427
+ self.set_cbar_disabled()
428
+ self.initialized = False
429
+ self.update()
430
+
431
+ # ====================================================================================
432
+ # ====================================================================================
433
+ # ====================================== GETTERS =====================================
434
+ # ====================================================================================
435
+ # ====================================================================================
436
+ def is_initialized(self):
437
+ return self.initialized
438
+
439
+ def get_ie_polys(self):
440
+ return self.ie_polys
441
+
442
+ def get_cli_center_pt(self):
443
+ return np.round(QSize_to_np(self.size())/2.0)
444
+
445
+ def get_img_look_pt(self):
446
+ img_look_pt = self.img_look_pt
447
+ if img_look_pt is None:
448
+ img_look_pt = self.img_size / 2
449
+ return img_look_pt
450
+
451
+ def get_view_scale(self):
452
+ view_scale = self.view_scale
453
+ if view_scale is None:
454
+ # Calc as scale to fit
455
+ min_cli_size = np.min(QSize_to_np(self.size()))
456
+ max_img_size = np.max(self.img_size)
457
+ view_scale = min_cli_size / max_img_size
458
+
459
+ return view_scale
460
+
461
+ def get_current_color_scheme(self):
462
+ return self.canvas_config.color_schemes[self.color_scheme_id]
463
+
464
+ def get_poly_pt_id_under_pt(self, poly, cli_pt):
465
+ w = np.argwhere ( npla.norm ( cli_pt - self.img_to_cli_pt( poly.get_pts() ), axis=1 ) <= self.canvas_config.pt_select_radius )
466
+ return None if len(w) == 0 else w[-1][0]
467
+
468
+ def get_poly_edge_id_pt_under_pt(self, poly, cli_pt):
469
+ cli_pts = self.img_to_cli_pt(poly.get_pts())
470
+ if len(cli_pts) >= 3:
471
+ edge_dists, projs = sd.dist_to_edges(cli_pts, cli_pt, is_closed=True)
472
+ edge_id = np.argmin(edge_dists)
473
+ dist = edge_dists[edge_id]
474
+ pt = projs[edge_id]
475
+ if dist <= self.canvas_config.pt_select_radius:
476
+ return edge_id, pt
477
+ return None, None
478
+
479
+ def get_poly_by_pt_near_wire(self, cli_pt):
480
+ pt_select_radius = self.canvas_config.pt_select_radius
481
+
482
+ for poly in reversed(self.ie_polys.get_polys()):
483
+ pts = poly.get_pts()
484
+ if len(pts) >= 3:
485
+ cli_pts = self.img_to_cli_pt(pts)
486
+
487
+ edge_dists, _ = sd.dist_to_edges(cli_pts, cli_pt, is_closed=True)
488
+
489
+ if np.min(edge_dists) <= pt_select_radius or \
490
+ any( npla.norm ( cli_pt - cli_pts, axis=1 ) <= pt_select_radius ):
491
+ return poly
492
+ return None
493
+
494
+ def get_poly_by_pt_in_hull(self, cli_pos):
495
+ img_pos = self.cli_to_img_pt(cli_pos)
496
+
497
+ for poly in reversed(self.ie_polys.get_polys()):
498
+ pts = poly.get_pts()
499
+ if len(pts) >= 3:
500
+ if cv2.pointPolygonTest( pts, tuple(img_pos), False) >= 0:
501
+ return poly
502
+
503
+ return None
504
+
505
+ def img_to_cli_pt(self, p):
506
+ return (p - self.get_img_look_pt()) * self.get_view_scale() + self.get_cli_center_pt()# QSize_to_np(self.size())/2.0
507
+
508
+ def cli_to_img_pt(self, p):
509
+ return (p - self.get_cli_center_pt() ) / self.get_view_scale() + self.get_img_look_pt()
510
+
511
+ def img_to_cli_rect(self, rect):
512
+ tl = QPoint_to_np(rect.topLeft())
513
+ xy = self.img_to_cli_pt(tl)
514
+ xy2 = self.img_to_cli_pt(tl + QSize_to_np(rect.size()) ) - xy
515
+ return QRect ( *xy.astype(np.int), *xy2.astype(np.int) )
516
+
517
+ # ====================================================================================
518
+ # ====================================================================================
519
+ # ====================================== SETTERS =====================================
520
+ # ====================================================================================
521
+ # ====================================================================================
522
+ def set_op_mode(self, op_mode, op_poly=None):
523
+ if not hasattr(self,'op_mode'):
524
+ self.op_mode = None
525
+ self.op_poly = None
526
+
527
+ if self.op_mode != op_mode:
528
+ # Finalize prev mode
529
+ if self.op_mode == OpMode.NONE:
530
+ self.cbar.btn_poly_type_act_grp.setDisabled(True)
531
+ elif self.op_mode == OpMode.DRAW_PTS:
532
+ self.cbar.btn_undo_pt_act.setDisabled(True)
533
+ self.cbar.btn_redo_pt_act.setDisabled(True)
534
+ self.cbar.btn_view_lock_center_act.setDisabled(True)
535
+ # Reset view_lock when exit from DRAW_PTS
536
+ self.set_view_lock(ViewLock.NONE)
537
+ # Remove unfinished poly
538
+ if self.op_poly.get_pts_count() < 3:
539
+ self.ie_polys.remove_poly(self.op_poly)
540
+
541
+ elif self.op_mode == OpMode.EDIT_PTS:
542
+ self.cbar.btn_pt_edit_mode_act.setDisabled(True)
543
+ self.cbar.btn_delete_poly_act.setDisabled(True)
544
+ # Reset pt_edit_move when exit from EDIT_PTS
545
+ self.set_pt_edit_mode(PTEditMode.MOVE)
546
+ elif self.op_mode == OpMode.VIEW_BAKED:
547
+ self.cbar.btn_view_baked_mask_act.setChecked(False)
548
+ elif self.op_mode == OpMode.VIEW_XSEG_MASK:
549
+ self.cbar.btn_view_xseg_mask_act.setChecked(False)
550
+
551
+ self.op_mode = op_mode
552
+
553
+ # Initialize new mode
554
+ if op_mode == OpMode.NONE:
555
+ self.cbar.btn_poly_type_act_grp.setDisabled(False)
556
+ elif op_mode == OpMode.DRAW_PTS:
557
+ self.cbar.btn_undo_pt_act.setDisabled(False)
558
+ self.cbar.btn_redo_pt_act.setDisabled(False)
559
+ self.cbar.btn_view_lock_center_act.setDisabled(False)
560
+ elif op_mode == OpMode.EDIT_PTS:
561
+ self.cbar.btn_pt_edit_mode_act.setDisabled(False)
562
+ self.cbar.btn_delete_poly_act.setDisabled(False)
563
+ elif op_mode == OpMode.VIEW_BAKED:
564
+ self.cbar.btn_view_baked_mask_act.setChecked(True )
565
+ n = QImage_to_np ( self.q_img ).astype(np.float32) / 255.0
566
+ h,w,c = n.shape
567
+ mask = np.zeros( (h,w,1), dtype=np.float32 )
568
+ self.ie_polys.overlay_mask(mask)
569
+ n = (mask*255).astype(np.uint8)
570
+ self.img_baked_pixmap = QPixmap.fromImage(QImage_from_np(n))
571
+ elif op_mode == OpMode.VIEW_XSEG_MASK:
572
+ self.cbar.btn_view_xseg_mask_act.setChecked(True)
573
+
574
+ if op_mode in [OpMode.DRAW_PTS, OpMode.EDIT_PTS]:
575
+ self.mouse_op_poly_pt_id = None
576
+ self.mouse_op_poly_edge_id = None
577
+ self.mouse_op_poly_edge_id_pt = None
578
+
579
+ self.op_poly = op_poly
580
+ if op_poly is not None:
581
+ self.update_mouse_info()
582
+
583
+ self.update_cursor()
584
+ self.update()
585
+
586
+ def set_pt_edit_mode(self, pt_edit_mode):
587
+ if not hasattr(self, 'pt_edit_mode') or self.pt_edit_mode != pt_edit_mode:
588
+ self.pt_edit_mode = pt_edit_mode
589
+ self.update_cursor()
590
+ self.update()
591
+ self.cbar.btn_pt_edit_mode_act.setChecked( self.pt_edit_mode == PTEditMode.ADD_DEL )
592
+
593
+ def set_view_lock(self, view_lock):
594
+ if not hasattr(self, 'view_lock') or self.view_lock != view_lock:
595
+ if hasattr(self, 'view_lock') and self.view_lock != view_lock:
596
+ if view_lock == ViewLock.CENTER:
597
+ self.img_look_pt = self.mouse_img_pt
598
+ QCursor.setPos ( self.mapToGlobal( QPoint_from_np(self.img_to_cli_pt(self.img_look_pt)) ))
599
+
600
+ self.view_lock = view_lock
601
+ self.update()
602
+ self.cbar.btn_view_lock_center_act.setChecked( self.view_lock == ViewLock.CENTER )
603
+
604
+ def set_cbar_disabled(self):
605
+ self.cbar.btn_delete_poly_act.setDisabled(True)
606
+ self.cbar.btn_undo_pt_act.setDisabled(True)
607
+ self.cbar.btn_redo_pt_act.setDisabled(True)
608
+ self.cbar.btn_pt_edit_mode_act.setDisabled(True)
609
+ self.cbar.btn_view_lock_center_act.setDisabled(True)
610
+ self.cbar.btn_poly_color_act_grp.setDisabled(True)
611
+ self.cbar.btn_view_xseg_overlay_mask_act.setDisabled(True)
612
+ self.cbar.btn_poly_type_act_grp.setDisabled(True)
613
+
614
+
615
+ def set_color_scheme_id(self, id):
616
+ if self.op_mode == OpMode.VIEW_BAKED or self.op_mode == OpMode.VIEW_XSEG_MASK:
617
+ self.set_op_mode(OpMode.NONE)
618
+
619
+ if not hasattr(self, 'color_scheme_id') or self.color_scheme_id != id:
620
+ self.color_scheme_id = id
621
+ self.update_cursor()
622
+ self.update()
623
+
624
+ if self.color_scheme_id == 0:
625
+ self.cbar.btn_poly_color_red_act.setChecked( True )
626
+ elif self.color_scheme_id == 1:
627
+ self.cbar.btn_poly_color_green_act.setChecked( True )
628
+ elif self.color_scheme_id == 2:
629
+ self.cbar.btn_poly_color_blue_act.setChecked( True )
630
+
631
+ def set_poly_include_type(self, poly_include_type):
632
+ if not hasattr(self, 'poly_include_type' ) or \
633
+ ( self.poly_include_type != poly_include_type and \
634
+ self.op_mode in [OpMode.NONE, OpMode.EDIT_PTS] ):
635
+ self.poly_include_type = poly_include_type
636
+ self.update()
637
+ self.cbar.btn_poly_type_include_act.setChecked(self.poly_include_type == SegIEPolyType.INCLUDE)
638
+ self.cbar.btn_poly_type_exclude_act.setChecked(self.poly_include_type == SegIEPolyType.EXCLUDE)
639
+
640
+ # ====================================================================================
641
+ # ====================================================================================
642
+ # ====================================== METHODS =====================================
643
+ # ====================================================================================
644
+ # ====================================================================================
645
+
646
+ def update_cursor(self, is_finalize=False):
647
+ if not self.initialized:
648
+ return
649
+
650
+ if not self.mouse_in_widget or is_finalize:
651
+ if self.current_cursor is not None:
652
+ QApplication.restoreOverrideCursor()
653
+ self.current_cursor = None
654
+ else:
655
+ color_cc = self.get_current_color_scheme().cross_cursor
656
+ nc = Qt.ArrowCursor
657
+
658
+ if self.drag_type == DragType.IMAGE_LOOK:
659
+ nc = Qt.ClosedHandCursor
660
+ else:
661
+
662
+ if self.op_mode == OpMode.NONE:
663
+ nc = color_cc
664
+ if self.mouse_wire_poly is not None:
665
+ nc = Qt.PointingHandCursor
666
+
667
+ elif self.op_mode == OpMode.DRAW_PTS:
668
+ nc = color_cc
669
+ elif self.op_mode == OpMode.EDIT_PTS:
670
+ nc = Qt.ArrowCursor
671
+
672
+ if self.mouse_op_poly_pt_id is not None:
673
+ nc = Qt.PointingHandCursor
674
+
675
+ if self.pt_edit_mode == PTEditMode.ADD_DEL:
676
+
677
+ if self.mouse_op_poly_edge_id is not None and \
678
+ self.mouse_op_poly_pt_id is None:
679
+ nc = color_cc
680
+ if self.current_cursor != nc:
681
+ if self.current_cursor is None:
682
+ QApplication.setOverrideCursor(nc)
683
+ else:
684
+ QApplication.changeOverrideCursor(nc)
685
+ self.current_cursor = nc
686
+
687
+ def update_mouse_info(self, mouse_cli_pt=None):
688
+ """
689
+ Update selected polys/edges/points by given mouse position
690
+ """
691
+ if mouse_cli_pt is not None:
692
+ self.mouse_cli_pt = mouse_cli_pt.astype(np.float32)
693
+
694
+ self.mouse_img_pt = self.cli_to_img_pt(self.mouse_cli_pt)
695
+
696
+ new_mouse_hull_poly = self.get_poly_by_pt_in_hull(self.mouse_cli_pt)
697
+
698
+ if self.mouse_hull_poly != new_mouse_hull_poly:
699
+ self.mouse_hull_poly = new_mouse_hull_poly
700
+ self.update_cursor()
701
+ self.update()
702
+
703
+ new_mouse_wire_poly = self.get_poly_by_pt_near_wire(self.mouse_cli_pt)
704
+
705
+ if self.mouse_wire_poly != new_mouse_wire_poly:
706
+ self.mouse_wire_poly = new_mouse_wire_poly
707
+ self.update_cursor()
708
+ self.update()
709
+
710
+ if self.op_mode in [OpMode.DRAW_PTS, OpMode.EDIT_PTS]:
711
+ new_mouse_op_poly_pt_id = self.get_poly_pt_id_under_pt (self.op_poly, self.mouse_cli_pt)
712
+ if self.mouse_op_poly_pt_id != new_mouse_op_poly_pt_id:
713
+ self.mouse_op_poly_pt_id = new_mouse_op_poly_pt_id
714
+ self.update_cursor()
715
+ self.update()
716
+
717
+ new_mouse_op_poly_edge_id,\
718
+ new_mouse_op_poly_edge_id_pt = self.get_poly_edge_id_pt_under_pt (self.op_poly, self.mouse_cli_pt)
719
+ if self.mouse_op_poly_edge_id != new_mouse_op_poly_edge_id:
720
+ self.mouse_op_poly_edge_id = new_mouse_op_poly_edge_id
721
+ self.update_cursor()
722
+ self.update()
723
+
724
+ if (self.mouse_op_poly_edge_id_pt.__class__ != new_mouse_op_poly_edge_id_pt.__class__) or \
725
+ (isinstance(self.mouse_op_poly_edge_id_pt, np.ndarray) and \
726
+ all(self.mouse_op_poly_edge_id_pt != new_mouse_op_poly_edge_id_pt)):
727
+
728
+ self.mouse_op_poly_edge_id_pt = new_mouse_op_poly_edge_id_pt
729
+ self.update_cursor()
730
+ self.update()
731
+
732
+
733
+ def action_undo_pt(self):
734
+ if self.drag_type == DragType.NONE:
735
+ if self.op_mode == OpMode.DRAW_PTS:
736
+ if self.op_poly.undo() == 0:
737
+ self.ie_polys.remove_poly (self.op_poly)
738
+ self.set_op_mode(OpMode.NONE)
739
+ self.update()
740
+
741
+ def action_redo_pt(self):
742
+ if self.drag_type == DragType.NONE:
743
+ if self.op_mode == OpMode.DRAW_PTS:
744
+ self.op_poly.redo()
745
+ self.update()
746
+
747
+ def action_delete_poly(self):
748
+ if self.op_mode == OpMode.EDIT_PTS and \
749
+ self.drag_type == DragType.NONE and \
750
+ self.pt_edit_mode == PTEditMode.MOVE:
751
+ # Delete current poly
752
+ self.ie_polys.remove_poly (self.op_poly)
753
+ self.set_op_mode(OpMode.NONE)
754
+
755
+ # ====================================================================================
756
+ # ====================================================================================
757
+ # ================================== OVERRIDE QT METHODS =============================
758
+ # ====================================================================================
759
+ # ====================================================================================
760
+ def on_keyPressEvent(self, ev):
761
+ if not self.initialized:
762
+ return
763
+ key = ev.key()
764
+ key_mods = int(ev.modifiers())
765
+ if self.op_mode == OpMode.DRAW_PTS:
766
+ self.set_view_lock(ViewLock.CENTER if key_mods == Qt.ShiftModifier else ViewLock.NONE )
767
+ elif self.op_mode == OpMode.EDIT_PTS:
768
+ self.set_pt_edit_mode(PTEditMode.ADD_DEL if key_mods == Qt.ControlModifier else PTEditMode.MOVE )
769
+
770
+ def on_keyReleaseEvent(self, ev):
771
+ if not self.initialized:
772
+ return
773
+ key = ev.key()
774
+ key_mods = int(ev.modifiers())
775
+ if self.op_mode == OpMode.DRAW_PTS:
776
+ self.set_view_lock(ViewLock.CENTER if key_mods == Qt.ShiftModifier else ViewLock.NONE )
777
+ elif self.op_mode == OpMode.EDIT_PTS:
778
+ self.set_pt_edit_mode(PTEditMode.ADD_DEL if key_mods == Qt.ControlModifier else PTEditMode.MOVE )
779
+
780
+ def enterEvent(self, ev):
781
+ super().enterEvent(ev)
782
+ self.mouse_in_widget = True
783
+ self.update_cursor()
784
+
785
+ def leaveEvent(self, ev):
786
+ super().leaveEvent(ev)
787
+ self.mouse_in_widget = False
788
+ self.update_cursor()
789
+
790
+ def mousePressEvent(self, ev):
791
+ super().mousePressEvent(ev)
792
+ if not self.initialized:
793
+ return
794
+
795
+ self.update_mouse_info(QPoint_to_np(ev.pos()))
796
+
797
+ btn = ev.button()
798
+
799
+ if btn == Qt.LeftButton:
800
+ if self.op_mode == OpMode.NONE:
801
+ # Clicking in NO OPERATION mode
802
+ if self.mouse_wire_poly is not None:
803
+ # Click on wire on any poly -> switch to EDIT_MODE
804
+ self.set_op_mode(OpMode.EDIT_PTS, op_poly=self.mouse_wire_poly)
805
+ else:
806
+ # Click on empty space -> create new poly with one point
807
+ new_poly = self.ie_polys.add_poly(self.poly_include_type)
808
+ self.ie_polys.sort()
809
+ new_poly.add_pt(*self.mouse_img_pt)
810
+ self.set_op_mode(OpMode.DRAW_PTS, op_poly=new_poly )
811
+
812
+ elif self.op_mode == OpMode.DRAW_PTS:
813
+ # Clicking in DRAW_PTS mode
814
+ if len(self.op_poly.get_pts()) >= 3 and self.mouse_op_poly_pt_id == 0:
815
+ # Click on first point -> close poly and switch to edit mode
816
+ self.set_op_mode(OpMode.EDIT_PTS, op_poly=self.op_poly)
817
+ else:
818
+ # Click on empty space -> add point to current poly
819
+ self.op_poly.add_pt(*self.mouse_img_pt)
820
+ self.update()
821
+
822
+ elif self.op_mode == OpMode.EDIT_PTS:
823
+ # Clicking in EDIT_PTS mode
824
+
825
+ if self.mouse_op_poly_pt_id is not None:
826
+ # Click on point of op_poly
827
+ if self.pt_edit_mode == PTEditMode.ADD_DEL:
828
+ # in mode 'delete point'
829
+ self.op_poly.remove_pt(self.mouse_op_poly_pt_id)
830
+ if self.op_poly.get_pts_count() < 3:
831
+ # not enough points after delete -> remove poly
832
+ self.ie_polys.remove_poly (self.op_poly)
833
+ self.set_op_mode(OpMode.NONE)
834
+ self.update()
835
+
836
+ elif self.drag_type == DragType.NONE:
837
+ # otherwise -> start drag
838
+ self.drag_type = DragType.POLY_PT
839
+ self.drag_cli_pt = self.mouse_cli_pt
840
+ self.drag_poly_pt_id = self.mouse_op_poly_pt_id
841
+ self.drag_poly_pt = self.op_poly.get_pts()[ self.drag_poly_pt_id ]
842
+ elif self.mouse_op_poly_edge_id is not None:
843
+ # Click on edge of op_poly
844
+ if self.pt_edit_mode == PTEditMode.ADD_DEL:
845
+ # in mode 'insert new point'
846
+ edge_img_pt = self.cli_to_img_pt(self.mouse_op_poly_edge_id_pt)
847
+ self.op_poly.insert_pt (self.mouse_op_poly_edge_id+1, edge_img_pt)
848
+ self.update()
849
+ else:
850
+ # Otherwise do nothing
851
+ pass
852
+ else:
853
+ # other cases -> unselect poly
854
+ self.set_op_mode(OpMode.NONE)
855
+
856
+ elif btn == Qt.MiddleButton:
857
+ if self.drag_type == DragType.NONE:
858
+ # Start image drag
859
+ self.drag_type = DragType.IMAGE_LOOK
860
+ self.drag_cli_pt = self.mouse_cli_pt
861
+ self.drag_img_look_pt = self.get_img_look_pt()
862
+ self.update_cursor()
863
+
864
+
865
+ def mouseReleaseEvent(self, ev):
866
+ super().mouseReleaseEvent(ev)
867
+ if not self.initialized:
868
+ return
869
+
870
+ self.update_mouse_info(QPoint_to_np(ev.pos()))
871
+
872
+ btn = ev.button()
873
+
874
+ if btn == Qt.LeftButton:
875
+ if self.op_mode == OpMode.EDIT_PTS:
876
+ if self.drag_type == DragType.POLY_PT:
877
+ self.drag_type = DragType.NONE
878
+ self.update()
879
+
880
+ elif btn == Qt.MiddleButton:
881
+ if self.drag_type == DragType.IMAGE_LOOK:
882
+ self.drag_type = DragType.NONE
883
+ self.update_cursor()
884
+ self.update()
885
+
886
+ def mouseMoveEvent(self, ev):
887
+ super().mouseMoveEvent(ev)
888
+ if not self.initialized:
889
+ return
890
+
891
+ prev_mouse_cli_pt = self.mouse_cli_pt
892
+ self.update_mouse_info(QPoint_to_np(ev.pos()))
893
+
894
+ if self.view_lock == ViewLock.CENTER:
895
+ if npla.norm(self.mouse_cli_pt - prev_mouse_cli_pt) >= 1:
896
+ self.img_look_pt = self.mouse_img_pt
897
+ QCursor.setPos ( self.mapToGlobal( QPoint_from_np(self.img_to_cli_pt(self.img_look_pt)) ))
898
+ self.update()
899
+
900
+ if self.drag_type == DragType.IMAGE_LOOK:
901
+ delta_pt = self.cli_to_img_pt(self.mouse_cli_pt) - self.cli_to_img_pt(self.drag_cli_pt)
902
+ self.img_look_pt = self.drag_img_look_pt - delta_pt
903
+ self.update()
904
+
905
+ if self.op_mode == OpMode.DRAW_PTS:
906
+ self.update()
907
+ elif self.op_mode == OpMode.EDIT_PTS:
908
+ if self.drag_type == DragType.POLY_PT:
909
+ delta_pt = self.cli_to_img_pt(self.mouse_cli_pt) - self.cli_to_img_pt(self.drag_cli_pt)
910
+ self.op_poly.set_point(self.drag_poly_pt_id, self.drag_poly_pt + delta_pt)
911
+ self.update()
912
+
913
+ def wheelEvent(self, ev):
914
+ super().wheelEvent(ev)
915
+
916
+ if not self.initialized:
917
+ return
918
+
919
+ mods = int(ev.modifiers())
920
+ delta = ev.angleDelta()
921
+
922
+ cli_pt = QPoint_to_np(ev.pos())
923
+
924
+ if self.drag_type == DragType.NONE:
925
+ sign = np.sign( delta.y() )
926
+ prev_img_pos = self.cli_to_img_pt (cli_pt)
927
+ delta_scale = sign*0.2 + sign * self.get_view_scale() / 10.0
928
+ self.view_scale = np.clip(self.get_view_scale() + delta_scale, 1.0, 20.0)
929
+ new_img_pos = self.cli_to_img_pt (cli_pt)
930
+ if sign > 0:
931
+ self.img_look_pt = self.get_img_look_pt() + (prev_img_pos-new_img_pos)#*1.5
932
+ else:
933
+ QCursor.setPos ( self.mapToGlobal(QPoint_from_np(self.img_to_cli_pt(prev_img_pos))) )
934
+ self.update()
935
+
936
+ def paintEvent(self, event):
937
+ super().paintEvent(event)
938
+ if not self.initialized:
939
+ return
940
+
941
+ qp = self.qp
942
+ qp.begin(self)
943
+ qp.setRenderHint(QPainter.Antialiasing)
944
+ qp.setRenderHint(QPainter.HighQualityAntialiasing)
945
+ qp.setRenderHint(QPainter.SmoothPixmapTransform)
946
+
947
+ src_rect = QRect(0, 0, *self.img_size)
948
+ dst_rect = self.img_to_cli_rect( src_rect )
949
+
950
+ if self.op_mode == OpMode.VIEW_BAKED:
951
+ qp.drawPixmap(dst_rect, self.img_baked_pixmap, src_rect)
952
+ elif self.op_mode == OpMode.VIEW_XSEG_MASK:
953
+ if self.xseg_mask_pixmap is not None:
954
+ qp.drawPixmap(dst_rect, self.xseg_mask_pixmap, src_rect)
955
+ else:
956
+ if self.cbar.btn_view_xseg_overlay_mask_act.isChecked() and \
957
+ self.xseg_overlay_mask_pixmap is not None:
958
+ qp.drawPixmap(dst_rect, self.xseg_overlay_mask_pixmap, src_rect)
959
+ elif self.img_pixmap is not None:
960
+ qp.drawPixmap(dst_rect, self.img_pixmap, src_rect)
961
+
962
+ polys = self.ie_polys.get_polys()
963
+ polys_len = len(polys)
964
+
965
+ color_scheme = self.get_current_color_scheme()
966
+
967
+ pt_rad = self.canvas_config.pt_radius
968
+ pt_rad_x2 = pt_rad*2
969
+
970
+ pt_select_radius = self.canvas_config.pt_select_radius
971
+
972
+ op_mode = self.op_mode
973
+ op_poly = self.op_poly
974
+
975
+ for i,poly in enumerate(polys):
976
+
977
+ selected_pt_path = QPainterPath()
978
+ poly_line_path = QPainterPath()
979
+ pts_line_path = QPainterPath()
980
+
981
+ pt_remove_cli_pt = None
982
+ poly_pts = poly.get_pts()
983
+ for pt_id, img_pt in enumerate(poly_pts):
984
+ cli_pt = self.img_to_cli_pt(img_pt)
985
+ q_cli_pt = QPoint_from_np(cli_pt)
986
+
987
+ if pt_id == 0:
988
+ poly_line_path.moveTo(q_cli_pt)
989
+ else:
990
+ poly_line_path.lineTo(q_cli_pt)
991
+
992
+
993
+ if poly == op_poly:
994
+ if self.op_mode == OpMode.DRAW_PTS or \
995
+ (self.op_mode == OpMode.EDIT_PTS and \
996
+ (self.pt_edit_mode == PTEditMode.MOVE) or \
997
+ (self.pt_edit_mode == PTEditMode.ADD_DEL and self.mouse_op_poly_pt_id == pt_id) \
998
+ ):
999
+ pts_line_path.moveTo( QPoint_from_np(cli_pt + np.float32([0,-pt_rad])) )
1000
+ pts_line_path.lineTo( QPoint_from_np(cli_pt + np.float32([0,pt_rad])) )
1001
+ pts_line_path.moveTo( QPoint_from_np(cli_pt + np.float32([-pt_rad,0])) )
1002
+ pts_line_path.lineTo( QPoint_from_np(cli_pt + np.float32([pt_rad,0])) )
1003
+
1004
+ if (self.op_mode == OpMode.EDIT_PTS and \
1005
+ self.pt_edit_mode == PTEditMode.ADD_DEL and \
1006
+ self.mouse_op_poly_pt_id == pt_id):
1007
+ pt_remove_cli_pt = cli_pt
1008
+
1009
+ if self.op_mode == OpMode.DRAW_PTS and \
1010
+ len(op_poly.get_pts()) >= 3 and pt_id == 0 and self.mouse_op_poly_pt_id == pt_id:
1011
+ # Circle around poly point
1012
+ selected_pt_path.addEllipse(q_cli_pt, pt_rad_x2, pt_rad_x2)
1013
+
1014
+
1015
+ if poly == op_poly:
1016
+ if op_mode == OpMode.DRAW_PTS:
1017
+ # Line from last point to mouse
1018
+ poly_line_path.lineTo( QPoint_from_np(self.mouse_cli_pt) )
1019
+
1020
+ if self.mouse_op_poly_pt_id is not None:
1021
+ pass
1022
+
1023
+ if self.mouse_op_poly_edge_id_pt is not None:
1024
+ if self.pt_edit_mode == PTEditMode.ADD_DEL and self.mouse_op_poly_pt_id is None:
1025
+ # Ready to insert point on edge
1026
+ m_cli_pt = self.mouse_op_poly_edge_id_pt
1027
+ pts_line_path.moveTo( QPoint_from_np(m_cli_pt + np.float32([0,-pt_rad])) )
1028
+ pts_line_path.lineTo( QPoint_from_np(m_cli_pt + np.float32([0,pt_rad])) )
1029
+ pts_line_path.moveTo( QPoint_from_np(m_cli_pt + np.float32([-pt_rad,0])) )
1030
+ pts_line_path.lineTo( QPoint_from_np(m_cli_pt + np.float32([pt_rad,0])) )
1031
+
1032
+ if len(poly_pts) >= 2:
1033
+ # Closing poly line
1034
+ poly_line_path.lineTo( QPoint_from_np(self.img_to_cli_pt(poly_pts[0])) )
1035
+
1036
+ # Draw calls
1037
+ qp.setPen(color_scheme.pt_outline_pen)
1038
+ qp.setBrush(QBrush())
1039
+ qp.drawPath(selected_pt_path)
1040
+
1041
+ qp.setPen(color_scheme.poly_outline_solid_pen)
1042
+ qp.setBrush(QBrush())
1043
+ qp.drawPath(pts_line_path)
1044
+
1045
+ if poly.get_type() == SegIEPolyType.INCLUDE:
1046
+ qp.setPen(color_scheme.poly_outline_solid_pen)
1047
+ else:
1048
+ qp.setPen(color_scheme.poly_outline_dot_pen)
1049
+
1050
+ qp.setBrush(color_scheme.poly_unselected_brush)
1051
+ if op_mode == OpMode.NONE:
1052
+ if poly == self.mouse_wire_poly:
1053
+ qp.setBrush(color_scheme.poly_selected_brush)
1054
+ #else:
1055
+ # if poly == op_poly:
1056
+ # qp.setBrush(color_scheme.poly_selected_brush)
1057
+
1058
+ qp.drawPath(poly_line_path)
1059
+
1060
+ if pt_remove_cli_pt is not None:
1061
+ qp.setPen(color_scheme.poly_outline_solid_pen)
1062
+ qp.setBrush(QBrush())
1063
+
1064
+ qp.drawLine( *(pt_remove_cli_pt + np.float32([-pt_rad_x2,-pt_rad_x2])), *(pt_remove_cli_pt + np.float32([pt_rad_x2,pt_rad_x2])) )
1065
+ qp.drawLine( *(pt_remove_cli_pt + np.float32([-pt_rad_x2,pt_rad_x2])), *(pt_remove_cli_pt + np.float32([pt_rad_x2,-pt_rad_x2])) )
1066
+
1067
+ qp.end()
1068
+
1069
+ class QCanvas(QFrame):
1070
+ def __init__(self):
1071
+ super().__init__()
1072
+
1073
+ self.canvas_control_left_bar = QCanvasControlsLeftBar()
1074
+ self.canvas_control_right_bar = QCanvasControlsRightBar()
1075
+
1076
+ cbar = sn( btn_poly_color_red_act = self.canvas_control_right_bar.btn_poly_color_red_act,
1077
+ btn_poly_color_green_act = self.canvas_control_right_bar.btn_poly_color_green_act,
1078
+ btn_poly_color_blue_act = self.canvas_control_right_bar.btn_poly_color_blue_act,
1079
+ btn_view_baked_mask_act = self.canvas_control_right_bar.btn_view_baked_mask_act,
1080
+ btn_view_xseg_mask_act = self.canvas_control_right_bar.btn_view_xseg_mask_act,
1081
+ btn_view_xseg_overlay_mask_act = self.canvas_control_right_bar.btn_view_xseg_overlay_mask_act,
1082
+ btn_poly_color_act_grp = self.canvas_control_right_bar.btn_poly_color_act_grp,
1083
+ btn_view_lock_center_act = self.canvas_control_right_bar.btn_view_lock_center_act,
1084
+
1085
+ btn_poly_type_include_act = self.canvas_control_left_bar.btn_poly_type_include_act,
1086
+ btn_poly_type_exclude_act = self.canvas_control_left_bar.btn_poly_type_exclude_act,
1087
+ btn_poly_type_act_grp = self.canvas_control_left_bar.btn_poly_type_act_grp,
1088
+ btn_undo_pt_act = self.canvas_control_left_bar.btn_undo_pt_act,
1089
+ btn_redo_pt_act = self.canvas_control_left_bar.btn_redo_pt_act,
1090
+ btn_delete_poly_act = self.canvas_control_left_bar.btn_delete_poly_act,
1091
+ btn_pt_edit_mode_act = self.canvas_control_left_bar.btn_pt_edit_mode_act )
1092
+
1093
+ self.op = QCanvasOperator(cbar)
1094
+ self.l = QHBoxLayout()
1095
+ self.l.setContentsMargins(0,0,0,0)
1096
+ self.l.addWidget(self.canvas_control_left_bar)
1097
+ self.l.addWidget(self.op)
1098
+ self.l.addWidget(self.canvas_control_right_bar)
1099
+ self.setLayout(self.l)
1100
+
1101
+ class LoaderQSubprocessor(QSubprocessor):
1102
+ def __init__(self, image_paths, q_label, q_progressbar, on_finish_func ):
1103
+
1104
+ self.image_paths = image_paths
1105
+ self.image_paths_len = len(image_paths)
1106
+ self.idxs = [*range(self.image_paths_len)]
1107
+
1108
+ self.filtered_image_paths = self.image_paths.copy()
1109
+
1110
+ self.image_paths_has_ie_polys = { image_path : False for image_path in self.image_paths }
1111
+
1112
+ self.q_label = q_label
1113
+ self.q_progressbar = q_progressbar
1114
+ self.q_progressbar.setRange(0, self.image_paths_len)
1115
+ self.q_progressbar.setValue(0)
1116
+ self.q_progressbar.update()
1117
+ self.on_finish_func = on_finish_func
1118
+ self.done_count = 0
1119
+ super().__init__('LoaderQSubprocessor', LoaderQSubprocessor.Cli, 60)
1120
+
1121
+ def get_data(self, host_dict):
1122
+ if len (self.idxs) > 0:
1123
+ idx = self.idxs.pop(0)
1124
+ image_path = self.image_paths[idx]
1125
+ self.q_label.setText(f'{QStringDB.loading_tip}... {image_path.name}')
1126
+
1127
+ return idx, image_path
1128
+
1129
+ return None
1130
+
1131
+ def on_clients_finalized(self):
1132
+ self.on_finish_func([x for x in self.filtered_image_paths if x is not None], self.image_paths_has_ie_polys)
1133
+
1134
+ def on_data_return (self, host_dict, data):
1135
+ self.idxs.insert(0, data[0])
1136
+
1137
+ def on_result (self, host_dict, data, result):
1138
+ idx, has_dflimg, has_ie_polys = result
1139
+
1140
+ if not has_dflimg:
1141
+ self.filtered_image_paths[idx] = None
1142
+ self.image_paths_has_ie_polys[self.image_paths[idx]] = has_ie_polys
1143
+
1144
+ self.done_count += 1
1145
+ if self.q_progressbar is not None:
1146
+ self.q_progressbar.setValue(self.done_count)
1147
+
1148
+ class Cli(QSubprocessor.Cli):
1149
+ def process_data(self, data):
1150
+ idx, filename = data
1151
+ dflimg = DFLIMG.load(filename)
1152
+ if dflimg is not None and dflimg.has_data():
1153
+ ie_polys = dflimg.get_seg_ie_polys()
1154
+
1155
+ return idx, True, ie_polys.has_polys()
1156
+ return idx, False, False
1157
+
1158
+ class MainWindow(QXMainWindow):
1159
+
1160
+ def __init__(self, input_dirpath, cfg_root_path):
1161
+ self.loading_frame = None
1162
+ self.help_frame = None
1163
+
1164
+ super().__init__()
1165
+
1166
+ self.input_dirpath = input_dirpath
1167
+ self.trash_dirpath = input_dirpath.parent / (input_dirpath.name + '_trash')
1168
+ self.cfg_root_path = cfg_root_path
1169
+
1170
+ self.cfg_path = cfg_root_path / 'MainWindow_cfg.dat'
1171
+ self.cfg_dict = pickle.loads(self.cfg_path.read_bytes()) if self.cfg_path.exists() else {}
1172
+
1173
+ self.cached_images = {}
1174
+ self.cached_has_ie_polys = {}
1175
+
1176
+ self.initialize_ui()
1177
+
1178
+ # Loader
1179
+ self.loading_frame = QFrame(self.main_canvas_frame)
1180
+ self.loading_frame.setAutoFillBackground(True)
1181
+ self.loading_frame.setFrameShape(QFrame.StyledPanel)
1182
+ self.loader_label = QLabel()
1183
+ self.loader_progress_bar = QProgressBar()
1184
+
1185
+ intro_image = QLabel()
1186
+ intro_image.setPixmap( QPixmap.fromImage(QImageDB.intro) )
1187
+
1188
+ intro_image_frame_l = QVBoxLayout()
1189
+ intro_image_frame_l.addWidget(intro_image, alignment=Qt.AlignCenter)
1190
+ intro_image_frame = QFrame()
1191
+ intro_image_frame.setSizePolicy (QSizePolicy.Expanding, QSizePolicy.Expanding)
1192
+ intro_image_frame.setLayout(intro_image_frame_l)
1193
+
1194
+ loading_frame_l = QVBoxLayout()
1195
+ loading_frame_l.addWidget (intro_image_frame)
1196
+ loading_frame_l.addWidget (self.loader_label)
1197
+ loading_frame_l.addWidget (self.loader_progress_bar)
1198
+ self.loading_frame.setLayout(loading_frame_l)
1199
+
1200
+ self.loader_subprocessor = LoaderQSubprocessor( image_paths=pathex.get_image_paths(input_dirpath, return_Path_class=True),
1201
+ q_label=self.loader_label,
1202
+ q_progressbar=self.loader_progress_bar,
1203
+ on_finish_func=self.on_loader_finish )
1204
+
1205
+
1206
+ def on_loader_finish(self, image_paths, image_paths_has_ie_polys):
1207
+ self.image_paths_done = []
1208
+ self.image_paths = image_paths
1209
+ self.image_paths_has_ie_polys = image_paths_has_ie_polys
1210
+ self.set_has_ie_polys_count ( len([ 1 for x in self.image_paths_has_ie_polys if self.image_paths_has_ie_polys[x] == True]) )
1211
+ self.loading_frame.hide()
1212
+ self.loading_frame = None
1213
+
1214
+ self.process_next_image(first_initialization=True)
1215
+
1216
+ def closeEvent(self, ev):
1217
+ self.cfg_dict['geometry'] = self.saveGeometry().data()
1218
+ self.cfg_path.write_bytes( pickle.dumps(self.cfg_dict) )
1219
+
1220
+
1221
+ def update_cached_images (self, count=5):
1222
+ d = self.cached_images
1223
+
1224
+ for image_path in self.image_paths_done[:-count]+self.image_paths[count:]:
1225
+ if image_path in d:
1226
+ del d[image_path]
1227
+
1228
+ for image_path in self.image_paths[:count]+self.image_paths_done[-count:]:
1229
+ if image_path not in d:
1230
+ img = cv2_imread(image_path)
1231
+ if img is not None:
1232
+ d[image_path] = img
1233
+
1234
+ def load_image(self, image_path):
1235
+ try:
1236
+ img = self.cached_images.get(image_path, None)
1237
+ if img is None:
1238
+ img = cv2_imread(image_path)
1239
+ self.cached_images[image_path] = img
1240
+ if img is None:
1241
+ io.log_err(f'Unable to load {image_path}')
1242
+ except:
1243
+ img = None
1244
+
1245
+ return img
1246
+
1247
+ def update_preview_bar(self):
1248
+ count = self.image_bar.get_preview_images_count()
1249
+ d = self.cached_images
1250
+ prev_imgs = [ d.get(image_path, None) for image_path in self.image_paths_done[-1:-count:-1] ]
1251
+ next_imgs = [ d.get(image_path, None) for image_path in self.image_paths[:count] ]
1252
+ self.image_bar.update_images(prev_imgs, next_imgs)
1253
+
1254
+
1255
+ def canvas_initialize(self, image_path, only_has_polys=False):
1256
+ if only_has_polys and not self.image_paths_has_ie_polys[image_path]:
1257
+ return False
1258
+
1259
+ dflimg = DFLIMG.load(image_path)
1260
+ if not dflimg or not dflimg.has_data():
1261
+ return False
1262
+
1263
+ ie_polys = dflimg.get_seg_ie_polys()
1264
+ xseg_mask = dflimg.get_xseg_mask()
1265
+ img = self.load_image(image_path)
1266
+ if img is None:
1267
+ return False
1268
+
1269
+ self.canvas.op.initialize ( img, ie_polys=ie_polys, xseg_mask=xseg_mask )
1270
+
1271
+ self.filename_label.setText(f"{image_path.name}")
1272
+
1273
+ return True
1274
+
1275
+ def canvas_finalize(self, image_path):
1276
+ self.canvas.op.finalize()
1277
+
1278
+ if image_path.exists():
1279
+ dflimg = DFLIMG.load(image_path)
1280
+ ie_polys = dflimg.get_seg_ie_polys()
1281
+ new_ie_polys = self.canvas.op.get_ie_polys()
1282
+
1283
+ if not new_ie_polys.identical(ie_polys):
1284
+ prev_has_polys = self.image_paths_has_ie_polys[image_path]
1285
+ self.image_paths_has_ie_polys[image_path] = new_ie_polys.has_polys()
1286
+ new_has_polys = self.image_paths_has_ie_polys[image_path]
1287
+
1288
+ if not prev_has_polys and new_has_polys:
1289
+ self.set_has_ie_polys_count ( self.get_has_ie_polys_count() +1)
1290
+ elif prev_has_polys and not new_has_polys:
1291
+ self.set_has_ie_polys_count ( self.get_has_ie_polys_count() -1)
1292
+
1293
+ dflimg.set_seg_ie_polys( new_ie_polys )
1294
+ dflimg.save()
1295
+
1296
+ self.filename_label.setText(f"")
1297
+
1298
+ def process_prev_image(self):
1299
+ key_mods = QApplication.keyboardModifiers()
1300
+ step = 5 if key_mods == Qt.ShiftModifier else 1
1301
+ only_has_polys = key_mods == Qt.ControlModifier
1302
+
1303
+ if self.canvas.op.is_initialized():
1304
+ self.canvas_finalize(self.image_paths[0])
1305
+
1306
+ while True:
1307
+ for _ in range(step):
1308
+ if len(self.image_paths_done) != 0:
1309
+ self.image_paths.insert (0, self.image_paths_done.pop(-1))
1310
+ else:
1311
+ break
1312
+ if len(self.image_paths) == 0:
1313
+ break
1314
+
1315
+ ret = self.canvas_initialize(self.image_paths[0], len(self.image_paths_done) != 0 and only_has_polys)
1316
+
1317
+ if ret or len(self.image_paths_done) == 0:
1318
+ break
1319
+
1320
+ self.update_cached_images()
1321
+ self.update_preview_bar()
1322
+
1323
+ def process_next_image(self, first_initialization=False):
1324
+ key_mods = QApplication.keyboardModifiers()
1325
+
1326
+ step = 0 if first_initialization else 5 if key_mods == Qt.ShiftModifier else 1
1327
+ only_has_polys = False if first_initialization else key_mods == Qt.ControlModifier
1328
+
1329
+ if self.canvas.op.is_initialized():
1330
+ self.canvas_finalize(self.image_paths[0])
1331
+
1332
+ while True:
1333
+ for _ in range(step):
1334
+ if len(self.image_paths) != 0:
1335
+ self.image_paths_done.append(self.image_paths.pop(0))
1336
+ else:
1337
+ break
1338
+ if len(self.image_paths) == 0:
1339
+ break
1340
+ if self.canvas_initialize(self.image_paths[0], only_has_polys):
1341
+ break
1342
+
1343
+ self.update_cached_images()
1344
+ self.update_preview_bar()
1345
+
1346
+ def trash_current_image(self):
1347
+ self.process_next_image()
1348
+
1349
+ img_path = self.image_paths_done.pop(-1)
1350
+ img_path = Path(img_path)
1351
+ self.trash_dirpath.mkdir(parents=True, exist_ok=True)
1352
+ img_path.rename( self.trash_dirpath / img_path.name )
1353
+
1354
+ self.update_cached_images()
1355
+ self.update_preview_bar()
1356
+
1357
+ def initialize_ui(self):
1358
+
1359
+ self.canvas = QCanvas()
1360
+
1361
+ image_bar = self.image_bar = ImagePreviewSequenceBar(preview_images_count=9, icon_size=QUIConfig.preview_bar_icon_q_size.width())
1362
+ image_bar.setSizePolicy ( QSizePolicy.Fixed, QSizePolicy.Fixed )
1363
+
1364
+
1365
+ btn_prev_image = QXIconButton(QIconDB.left, QStringDB.btn_prev_image_tip, shortcut='A', click_func=self.process_prev_image)
1366
+ btn_prev_image.setIconSize(QUIConfig.preview_bar_icon_q_size)
1367
+
1368
+ btn_next_image = QXIconButton(QIconDB.right, QStringDB.btn_next_image_tip, shortcut='D', click_func=self.process_next_image)
1369
+ btn_next_image.setIconSize(QUIConfig.preview_bar_icon_q_size)
1370
+
1371
+ btn_delete_image = QXIconButton(QIconDB.trashcan, QStringDB.btn_delete_image_tip, shortcut='X', click_func=self.trash_current_image)
1372
+ btn_delete_image.setIconSize(QUIConfig.preview_bar_icon_q_size)
1373
+
1374
+ pad_image = QWidget()
1375
+ pad_image.setFixedSize(QUIConfig.preview_bar_icon_q_size)
1376
+
1377
+ preview_image_bar_frame_l = QHBoxLayout()
1378
+ preview_image_bar_frame_l.setContentsMargins(0,0,0,0)
1379
+ preview_image_bar_frame_l.addWidget ( pad_image, alignment=Qt.AlignCenter)
1380
+ preview_image_bar_frame_l.addWidget ( btn_prev_image, alignment=Qt.AlignCenter)
1381
+ preview_image_bar_frame_l.addWidget ( image_bar)
1382
+ preview_image_bar_frame_l.addWidget ( btn_next_image, alignment=Qt.AlignCenter)
1383
+ #preview_image_bar_frame_l.addWidget ( btn_delete_image, alignment=Qt.AlignCenter)
1384
+
1385
+ preview_image_bar_frame = QFrame()
1386
+ preview_image_bar_frame.setSizePolicy ( QSizePolicy.Fixed, QSizePolicy.Fixed )
1387
+ preview_image_bar_frame.setLayout(preview_image_bar_frame_l)
1388
+
1389
+ preview_image_bar_frame2_l = QHBoxLayout()
1390
+ preview_image_bar_frame2_l.setContentsMargins(0,0,0,0)
1391
+ preview_image_bar_frame2_l.addWidget ( btn_delete_image, alignment=Qt.AlignCenter)
1392
+
1393
+ preview_image_bar_frame2 = QFrame()
1394
+ preview_image_bar_frame2.setSizePolicy ( QSizePolicy.Fixed, QSizePolicy.Fixed )
1395
+ preview_image_bar_frame2.setLayout(preview_image_bar_frame2_l)
1396
+
1397
+ preview_image_bar_l = QHBoxLayout()
1398
+ preview_image_bar_l.addWidget (preview_image_bar_frame, alignment=Qt.AlignCenter)
1399
+ preview_image_bar_l.addWidget (preview_image_bar_frame2)
1400
+
1401
+ preview_image_bar = QFrame()
1402
+ preview_image_bar.setFrameShape(QFrame.StyledPanel)
1403
+ preview_image_bar.setSizePolicy ( QSizePolicy.Expanding, QSizePolicy.Fixed )
1404
+ preview_image_bar.setLayout(preview_image_bar_l)
1405
+
1406
+ label_font = QFont('Courier New')
1407
+ self.filename_label = QLabel()
1408
+ self.filename_label.setFont(label_font)
1409
+
1410
+ self.has_ie_polys_count_label = QLabel()
1411
+
1412
+ status_frame_l = QHBoxLayout()
1413
+ status_frame_l.setContentsMargins(0,0,0,0)
1414
+ status_frame_l.addWidget ( QLabel(), alignment=Qt.AlignCenter)
1415
+ status_frame_l.addWidget (self.filename_label, alignment=Qt.AlignCenter)
1416
+ status_frame_l.addWidget (self.has_ie_polys_count_label, alignment=Qt.AlignCenter)
1417
+ status_frame = QFrame()
1418
+ status_frame.setLayout(status_frame_l)
1419
+
1420
+ main_canvas_l = QVBoxLayout()
1421
+ main_canvas_l.setContentsMargins(0,0,0,0)
1422
+ main_canvas_l.addWidget (self.canvas)
1423
+ main_canvas_l.addWidget (status_frame)
1424
+ main_canvas_l.addWidget (preview_image_bar)
1425
+
1426
+ self.main_canvas_frame = QFrame()
1427
+ self.main_canvas_frame.setLayout(main_canvas_l)
1428
+
1429
+ self.main_l = QHBoxLayout()
1430
+ self.main_l.setContentsMargins(0,0,0,0)
1431
+ self.main_l.addWidget (self.main_canvas_frame)
1432
+
1433
+ self.setLayout(self.main_l)
1434
+
1435
+ geometry = self.cfg_dict.get('geometry', None)
1436
+ if geometry is not None:
1437
+ self.restoreGeometry(geometry)
1438
+ else:
1439
+ self.move( QPoint(0,0))
1440
+
1441
+ def get_has_ie_polys_count(self):
1442
+ return self.has_ie_polys_count
1443
+
1444
+ def set_has_ie_polys_count(self, c):
1445
+ self.has_ie_polys_count = c
1446
+ self.has_ie_polys_count_label.setText(f"{c} {QStringDB.labeled_tip}")
1447
+
1448
+ def resizeEvent(self, ev):
1449
+ if self.loading_frame is not None:
1450
+ self.loading_frame.resize( ev.size() )
1451
+ if self.help_frame is not None:
1452
+ self.help_frame.resize( ev.size() )
1453
+
1454
+ def start(input_dirpath):
1455
+ """
1456
+ returns exit_code
1457
+ """
1458
+ io.log_info("Running XSeg editor.")
1459
+
1460
+ if PackedFaceset.path_contains(input_dirpath):
1461
+ io.log_info (f'\n{input_dirpath} contains packed faceset! Unpack it first.\n')
1462
+ return 1
1463
+
1464
+ root_path = Path(__file__).parent
1465
+ cfg_root_path = Path(tempfile.gettempdir())
1466
+
1467
+ QApplication.setAttribute(Qt.AA_EnableHighDpiScaling, True)
1468
+ QApplication.setAttribute(Qt.AA_UseHighDpiPixmaps, True)
1469
+
1470
+ app = QApplication([])
1471
+ app.setApplicationName("XSegEditor")
1472
+ app.setStyle('Fusion')
1473
+
1474
+ QFontDatabase.addApplicationFont( str(root_path / 'gfx' / 'fonts' / 'NotoSans-Medium.ttf') )
1475
+
1476
+ app.setFont( QFont('NotoSans'))
1477
+
1478
+ QUIConfig.initialize()
1479
+ QStringDB.initialize()
1480
+
1481
+ QIconDB.initialize( root_path / 'gfx' / 'icons' )
1482
+ QCursorDB.initialize( root_path / 'gfx' / 'cursors' )
1483
+ QImageDB.initialize( root_path / 'gfx' / 'images' )
1484
+
1485
+ app.setWindowIcon(QIconDB.app_icon)
1486
+ app.setPalette( QDarkPalette() )
1487
+
1488
+ win = MainWindow( input_dirpath=input_dirpath, cfg_root_path=cfg_root_path)
1489
+
1490
+ win.show()
1491
+ win.raise_()
1492
+
1493
+ app.exec_()
1494
+ return 0
XSegEditor/gfx/cursors/cross_blue.png ADDED
XSegEditor/gfx/cursors/cross_green.png ADDED
XSegEditor/gfx/cursors/cross_red.png ADDED
XSegEditor/gfx/fonts/NotoSans-Medium.ttf ADDED
Binary file (453 kB). View file
 
XSegEditor/gfx/icons/app_icon.png ADDED
XSegEditor/gfx/icons/delete_poly.png ADDED
XSegEditor/gfx/icons/down.png ADDED
XSegEditor/gfx/icons/left.png ADDED
XSegEditor/gfx/icons/poly_color.psd ADDED
XSegEditor/gfx/icons/poly_color_blue.png ADDED
XSegEditor/gfx/icons/poly_color_green.png ADDED
XSegEditor/gfx/icons/poly_color_red.png ADDED
XSegEditor/gfx/icons/poly_type_exclude.png ADDED
XSegEditor/gfx/icons/poly_type_include.png ADDED
XSegEditor/gfx/icons/poly_type_source.psd ADDED
XSegEditor/gfx/icons/pt_edit_mode.png ADDED
XSegEditor/gfx/icons/pt_edit_mode_source.psd ADDED
XSegEditor/gfx/icons/redo_pt.png ADDED
XSegEditor/gfx/icons/redo_pt_source.psd ADDED
XSegEditor/gfx/icons/right.png ADDED
XSegEditor/gfx/icons/trashcan.png ADDED
XSegEditor/gfx/icons/undo_pt.png ADDED
XSegEditor/gfx/icons/undo_pt_source.psd ADDED
XSegEditor/gfx/icons/up.png ADDED
XSegEditor/gfx/icons/view_baked.png ADDED
XSegEditor/gfx/icons/view_lock_center.png ADDED
XSegEditor/gfx/icons/view_xseg.png ADDED
XSegEditor/gfx/icons/view_xseg_overlay.png ADDED
XSegEditor/gfx/images/intro.png ADDED
XSegEditor/gfx/images/intro_source.psd ADDED
_config.yml ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ theme: jekyll-theme-cayman
2
+ plugins:
3
+ - jekyll-relative-links
4
+ relative_links:
5
+ enabled: true
6
+ collections: true
7
+
8
+ include:
9
+ - README.md
core/cv2ex.py ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import cv2
2
+ import numpy as np
3
+ from pathlib import Path
4
+ from core.interact import interact as io
5
+ from core import imagelib
6
+ import traceback
7
+
8
+ def cv2_imread(filename, flags=cv2.IMREAD_UNCHANGED, loader_func=None, verbose=True):
9
+ """
10
+ allows to open non-english characters path
11
+ """
12
+ try:
13
+ if loader_func is not None:
14
+ bytes = bytearray(loader_func(filename))
15
+ else:
16
+ with open(filename, "rb") as stream:
17
+ bytes = bytearray(stream.read())
18
+ numpyarray = np.asarray(bytes, dtype=np.uint8)
19
+ return cv2.imdecode(numpyarray, flags)
20
+ except:
21
+ if verbose:
22
+ io.log_err(f"Exception occured in cv2_imread : {traceback.format_exc()}")
23
+ return None
24
+
25
+ def cv2_imwrite(filename, img, *args):
26
+ ret, buf = cv2.imencode( Path(filename).suffix, img, *args)
27
+ if ret == True:
28
+ try:
29
+ with open(filename, "wb") as stream:
30
+ stream.write( buf )
31
+ except:
32
+ pass
33
+
34
+ def cv2_resize(x, *args, **kwargs):
35
+ h,w,c = x.shape
36
+ x = cv2.resize(x, *args, **kwargs)
37
+
38
+ x = imagelib.normalize_channels(x, c)
39
+ return x
40
+
core/imagelib/SegIEPolys.py ADDED
@@ -0,0 +1,158 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import numpy as np
2
+ import cv2
3
+ from enum import IntEnum
4
+
5
+
6
+ class SegIEPolyType(IntEnum):
7
+ EXCLUDE = 0
8
+ INCLUDE = 1
9
+
10
+
11
+
12
+ class SegIEPoly():
13
+ def __init__(self, type=None, pts=None, **kwargs):
14
+ self.type = type
15
+
16
+ if pts is None:
17
+ pts = np.empty( (0,2), dtype=np.float32 )
18
+ else:
19
+ pts = np.float32(pts)
20
+ self.pts = pts
21
+ self.n_max = self.n = len(pts)
22
+
23
+ def dump(self):
24
+ return {'type': int(self.type),
25
+ 'pts' : self.get_pts(),
26
+ }
27
+
28
+ def identical(self, b):
29
+ if self.n != b.n:
30
+ return False
31
+ return (self.pts[0:self.n] == b.pts[0:b.n]).all()
32
+
33
+ def get_type(self):
34
+ return self.type
35
+
36
+ def add_pt(self, x, y):
37
+ self.pts = np.append(self.pts[0:self.n], [ ( float(x), float(y) ) ], axis=0).astype(np.float32)
38
+ self.n_max = self.n = self.n + 1
39
+
40
+ def undo(self):
41
+ self.n = max(0, self.n-1)
42
+ return self.n
43
+
44
+ def redo(self):
45
+ self.n = min(len(self.pts), self.n+1)
46
+ return self.n
47
+
48
+ def redo_clip(self):
49
+ self.pts = self.pts[0:self.n]
50
+ self.n_max = self.n
51
+
52
+ def insert_pt(self, n, pt):
53
+ if n < 0 or n > self.n:
54
+ raise ValueError("insert_pt out of range")
55
+ self.pts = np.concatenate( (self.pts[0:n], pt[None,...].astype(np.float32), self.pts[n:]), axis=0)
56
+ self.n_max = self.n = self.n+1
57
+
58
+ def remove_pt(self, n):
59
+ if n < 0 or n >= self.n:
60
+ raise ValueError("remove_pt out of range")
61
+ self.pts = np.concatenate( (self.pts[0:n], self.pts[n+1:]), axis=0)
62
+ self.n_max = self.n = self.n-1
63
+
64
+ def get_last_point(self):
65
+ return self.pts[self.n-1].copy()
66
+
67
+ def get_pts(self):
68
+ return self.pts[0:self.n].copy()
69
+
70
+ def get_pts_count(self):
71
+ return self.n
72
+
73
+ def set_point(self, id, pt):
74
+ self.pts[id] = pt
75
+
76
+ def set_points(self, pts):
77
+ self.pts = np.array(pts)
78
+ self.n_max = self.n = len(pts)
79
+
80
+ def mult_points(self, val):
81
+ self.pts *= val
82
+
83
+
84
+
85
+ class SegIEPolys():
86
+ def __init__(self):
87
+ self.polys = []
88
+
89
+ def identical(self, b):
90
+ polys_len = len(self.polys)
91
+ o_polys_len = len(b.polys)
92
+ if polys_len != o_polys_len:
93
+ return False
94
+
95
+ return all ([ a_poly.identical(b_poly) for a_poly, b_poly in zip(self.polys, b.polys) ])
96
+
97
+ def add_poly(self, ie_poly_type):
98
+ poly = SegIEPoly(ie_poly_type)
99
+ self.polys.append (poly)
100
+ return poly
101
+
102
+ def remove_poly(self, poly):
103
+ if poly in self.polys:
104
+ self.polys.remove(poly)
105
+
106
+ def has_polys(self):
107
+ return len(self.polys) != 0
108
+
109
+ def get_poly(self, id):
110
+ return self.polys[id]
111
+
112
+ def get_polys(self):
113
+ return self.polys
114
+
115
+ def get_pts_count(self):
116
+ return sum([poly.get_pts_count() for poly in self.polys])
117
+
118
+ def sort(self):
119
+ poly_by_type = { SegIEPolyType.EXCLUDE : [], SegIEPolyType.INCLUDE : [] }
120
+
121
+ for poly in self.polys:
122
+ poly_by_type[poly.type].append(poly)
123
+
124
+ self.polys = poly_by_type[SegIEPolyType.INCLUDE] + poly_by_type[SegIEPolyType.EXCLUDE]
125
+
126
+ def __iter__(self):
127
+ for poly in self.polys:
128
+ yield poly
129
+
130
+ def overlay_mask(self, mask):
131
+ h,w,c = mask.shape
132
+ white = (1,)*c
133
+ black = (0,)*c
134
+ for poly in self.polys:
135
+ pts = poly.get_pts().astype(np.int32)
136
+ if len(pts) != 0:
137
+ cv2.fillPoly(mask, [pts], white if poly.type == SegIEPolyType.INCLUDE else black )
138
+
139
+ def dump(self):
140
+ return {'polys' : [ poly.dump() for poly in self.polys ] }
141
+
142
+ def mult_points(self, val):
143
+ for poly in self.polys:
144
+ poly.mult_points(val)
145
+
146
+ @staticmethod
147
+ def load(data=None):
148
+ ie_polys = SegIEPolys()
149
+ if data is not None:
150
+ if isinstance(data, list):
151
+ # Backward comp
152
+ ie_polys.polys = [ SegIEPoly(type=type, pts=pts) for (type, pts) in data ]
153
+ elif isinstance(data, dict):
154
+ ie_polys.polys = [ SegIEPoly(**poly_cfg) for poly_cfg in data['polys'] ]
155
+
156
+ ie_polys.sort()
157
+
158
+ return ie_polys
core/imagelib/__init__.py ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from .estimate_sharpness import estimate_sharpness
2
+
3
+ from .equalize_and_stack_square import equalize_and_stack_square
4
+
5
+ from .text import get_text_image, get_draw_text_lines
6
+
7
+ from .draw import draw_polygon, draw_rect
8
+
9
+ from .morph import morph_by_points
10
+
11
+ from .warp import gen_warp_params, warp_by_params
12
+
13
+ from .reduce_colors import reduce_colors
14
+
15
+ from .color_transfer import color_transfer, color_transfer_mix, color_transfer_sot, color_transfer_mkl, color_transfer_idt, color_hist_match, reinhard_color_transfer, linear_color_transfer
16
+
17
+ from .common import random_crop, normalize_channels, cut_odd_image, overlay_alpha_image
18
+
19
+ from .SegIEPolys import *
20
+
21
+ from .blursharpen import LinearMotionBlur, blursharpen
22
+
23
+ from .filters import apply_random_rgb_levels, \
24
+ apply_random_overlay_triangle, \
25
+ apply_random_hsv_shift, \
26
+ apply_random_sharpen, \
27
+ apply_random_motion_blur, \
28
+ apply_random_gaussian_blur, \
29
+ apply_random_nearest_resize, \
30
+ apply_random_bilinear_resize, \
31
+ apply_random_jpeg_compress, \
32
+ apply_random_relight
core/imagelib/blursharpen.py ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import cv2
2
+ import numpy as np
3
+
4
+ def LinearMotionBlur(image, size, angle):
5
+ k = np.zeros((size, size), dtype=np.float32)
6
+ k[ (size-1)// 2 , :] = np.ones(size, dtype=np.float32)
7
+ k = cv2.warpAffine(k, cv2.getRotationMatrix2D( (size / 2 -0.5 , size / 2 -0.5 ) , angle, 1.0), (size, size) )
8
+ k = k * ( 1.0 / np.sum(k) )
9
+ return cv2.filter2D(image, -1, k)
10
+
11
+ def blursharpen (img, sharpen_mode=0, kernel_size=3, amount=100):
12
+ if kernel_size % 2 == 0:
13
+ kernel_size += 1
14
+ if amount > 0:
15
+ if sharpen_mode == 1: #box
16
+ kernel = np.zeros( (kernel_size, kernel_size), dtype=np.float32)
17
+ kernel[ kernel_size//2, kernel_size//2] = 1.0
18
+ box_filter = np.ones( (kernel_size, kernel_size), dtype=np.float32) / (kernel_size**2)
19
+ kernel = kernel + (kernel - box_filter) * amount
20
+ return cv2.filter2D(img, -1, kernel)
21
+ elif sharpen_mode == 2: #gaussian
22
+ blur = cv2.GaussianBlur(img, (kernel_size, kernel_size) , 0)
23
+ img = cv2.addWeighted(img, 1.0 + (0.5 * amount), blur, -(0.5 * amount), 0)
24
+ return img
25
+ elif amount < 0:
26
+ n = -amount
27
+ while n > 0:
28
+
29
+ img_blur = cv2.medianBlur(img, 5)
30
+ if int(n / 10) != 0:
31
+ img = img_blur
32
+ else:
33
+ pass_power = (n % 10) / 10.0
34
+ img = img*(1.0-pass_power)+img_blur*pass_power
35
+ n = max(n-10,0)
36
+
37
+ return img
38
+ return img
core/imagelib/color_transfer.py ADDED
@@ -0,0 +1,336 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import cv2
2
+ import numexpr as ne
3
+ import numpy as np
4
+ import scipy as sp
5
+ from numpy import linalg as npla
6
+
7
+
8
+ def color_transfer_sot(src,trg, steps=10, batch_size=5, reg_sigmaXY=16.0, reg_sigmaV=5.0):
9
+ """
10
+ Color Transform via Sliced Optimal Transfer
11
+ ported by @iperov from https://github.com/dcoeurjo/OTColorTransfer
12
+
13
+ src - any float range any channel image
14
+ dst - any float range any channel image, same shape as src
15
+ steps - number of solver steps
16
+ batch_size - solver batch size
17
+ reg_sigmaXY - apply regularization and sigmaXY of filter, otherwise set to 0.0
18
+ reg_sigmaV - sigmaV of filter
19
+
20
+ return value - clip it manually
21
+ """
22
+ if not np.issubdtype(src.dtype, np.floating):
23
+ raise ValueError("src value must be float")
24
+ if not np.issubdtype(trg.dtype, np.floating):
25
+ raise ValueError("trg value must be float")
26
+
27
+ if len(src.shape) != 3:
28
+ raise ValueError("src shape must have rank 3 (h,w,c)")
29
+
30
+ if src.shape != trg.shape:
31
+ raise ValueError("src and trg shapes must be equal")
32
+
33
+ src_dtype = src.dtype
34
+ h,w,c = src.shape
35
+ new_src = src.copy()
36
+
37
+ advect = np.empty ( (h*w,c), dtype=src_dtype )
38
+ for step in range (steps):
39
+ advect.fill(0)
40
+ for batch in range (batch_size):
41
+ dir = np.random.normal(size=c).astype(src_dtype)
42
+ dir /= npla.norm(dir)
43
+
44
+ projsource = np.sum( new_src*dir, axis=-1).reshape ((h*w))
45
+ projtarget = np.sum( trg*dir, axis=-1).reshape ((h*w))
46
+
47
+ idSource = np.argsort (projsource)
48
+ idTarget = np.argsort (projtarget)
49
+
50
+ a = projtarget[idTarget]-projsource[idSource]
51
+ for i_c in range(c):
52
+ advect[idSource,i_c] += a * dir[i_c]
53
+ new_src += advect.reshape( (h,w,c) ) / batch_size
54
+
55
+ if reg_sigmaXY != 0.0:
56
+ src_diff = new_src-src
57
+ src_diff_filt = cv2.bilateralFilter (src_diff, 0, reg_sigmaV, reg_sigmaXY )
58
+ if len(src_diff_filt.shape) == 2:
59
+ src_diff_filt = src_diff_filt[...,None]
60
+ new_src = src + src_diff_filt
61
+ return new_src
62
+
63
+ def color_transfer_mkl(x0, x1):
64
+ eps = np.finfo(float).eps
65
+
66
+ h,w,c = x0.shape
67
+ h1,w1,c1 = x1.shape
68
+
69
+ x0 = x0.reshape ( (h*w,c) )
70
+ x1 = x1.reshape ( (h1*w1,c1) )
71
+
72
+ a = np.cov(x0.T)
73
+ b = np.cov(x1.T)
74
+
75
+ Da2, Ua = np.linalg.eig(a)
76
+ Da = np.diag(np.sqrt(Da2.clip(eps, None)))
77
+
78
+ C = np.dot(np.dot(np.dot(np.dot(Da, Ua.T), b), Ua), Da)
79
+
80
+ Dc2, Uc = np.linalg.eig(C)
81
+ Dc = np.diag(np.sqrt(Dc2.clip(eps, None)))
82
+
83
+ Da_inv = np.diag(1./(np.diag(Da)))
84
+
85
+ t = np.dot(np.dot(np.dot(np.dot(np.dot(np.dot(Ua, Da_inv), Uc), Dc), Uc.T), Da_inv), Ua.T)
86
+
87
+ mx0 = np.mean(x0, axis=0)
88
+ mx1 = np.mean(x1, axis=0)
89
+
90
+ result = np.dot(x0-mx0, t) + mx1
91
+ return np.clip ( result.reshape ( (h,w,c) ).astype(x0.dtype), 0, 1)
92
+
93
+ def color_transfer_idt(i0, i1, bins=256, n_rot=20):
94
+ import scipy.stats
95
+
96
+ relaxation = 1 / n_rot
97
+ h,w,c = i0.shape
98
+ h1,w1,c1 = i1.shape
99
+
100
+ i0 = i0.reshape ( (h*w,c) )
101
+ i1 = i1.reshape ( (h1*w1,c1) )
102
+
103
+ n_dims = c
104
+
105
+ d0 = i0.T
106
+ d1 = i1.T
107
+
108
+ for i in range(n_rot):
109
+
110
+ r = sp.stats.special_ortho_group.rvs(n_dims).astype(np.float32)
111
+
112
+ d0r = np.dot(r, d0)
113
+ d1r = np.dot(r, d1)
114
+ d_r = np.empty_like(d0)
115
+
116
+ for j in range(n_dims):
117
+
118
+ lo = min(d0r[j].min(), d1r[j].min())
119
+ hi = max(d0r[j].max(), d1r[j].max())
120
+
121
+ p0r, edges = np.histogram(d0r[j], bins=bins, range=[lo, hi])
122
+ p1r, _ = np.histogram(d1r[j], bins=bins, range=[lo, hi])
123
+
124
+ cp0r = p0r.cumsum().astype(np.float32)
125
+ cp0r /= cp0r[-1]
126
+
127
+ cp1r = p1r.cumsum().astype(np.float32)
128
+ cp1r /= cp1r[-1]
129
+
130
+ f = np.interp(cp0r, cp1r, edges[1:])
131
+
132
+ d_r[j] = np.interp(d0r[j], edges[1:], f, left=0, right=bins)
133
+
134
+ d0 = relaxation * np.linalg.solve(r, (d_r - d0r)) + d0
135
+
136
+ return np.clip ( d0.T.reshape ( (h,w,c) ).astype(i0.dtype) , 0, 1)
137
+
138
+ def reinhard_color_transfer(target : np.ndarray, source : np.ndarray, target_mask : np.ndarray = None, source_mask : np.ndarray = None, mask_cutoff=0.5) -> np.ndarray:
139
+ """
140
+ Transfer color using rct method.
141
+
142
+ target np.ndarray H W 3C (BGR) np.float32
143
+ source np.ndarray H W 3C (BGR) np.float32
144
+
145
+ target_mask(None) np.ndarray H W 1C np.float32
146
+ source_mask(None) np.ndarray H W 1C np.float32
147
+
148
+ mask_cutoff(0.5) float
149
+
150
+ masks are used to limit the space where color statistics will be computed to adjust the target
151
+
152
+ reference: Color Transfer between Images https://www.cs.tau.ac.il/~turkel/imagepapers/ColorTransfer.pdf
153
+ """
154
+ source = cv2.cvtColor(source, cv2.COLOR_BGR2LAB)
155
+ target = cv2.cvtColor(target, cv2.COLOR_BGR2LAB)
156
+
157
+ source_input = source
158
+ if source_mask is not None:
159
+ source_input = source_input.copy()
160
+ source_input[source_mask[...,0] < mask_cutoff] = [0,0,0]
161
+
162
+ target_input = target
163
+ if target_mask is not None:
164
+ target_input = target_input.copy()
165
+ target_input[target_mask[...,0] < mask_cutoff] = [0,0,0]
166
+
167
+ target_l_mean, target_l_std, target_a_mean, target_a_std, target_b_mean, target_b_std, \
168
+ = target_input[...,0].mean(), target_input[...,0].std(), target_input[...,1].mean(), target_input[...,1].std(), target_input[...,2].mean(), target_input[...,2].std()
169
+
170
+ source_l_mean, source_l_std, source_a_mean, source_a_std, source_b_mean, source_b_std, \
171
+ = source_input[...,0].mean(), source_input[...,0].std(), source_input[...,1].mean(), source_input[...,1].std(), source_input[...,2].mean(), source_input[...,2].std()
172
+
173
+ # not as in the paper: scale by the standard deviations using reciprocal of paper proposed factor
174
+ target_l = target[...,0]
175
+ target_l = ne.evaluate('(target_l - target_l_mean) * source_l_std / target_l_std + source_l_mean')
176
+
177
+ target_a = target[...,1]
178
+ target_a = ne.evaluate('(target_a - target_a_mean) * source_a_std / target_a_std + source_a_mean')
179
+
180
+ target_b = target[...,2]
181
+ target_b = ne.evaluate('(target_b - target_b_mean) * source_b_std / target_b_std + source_b_mean')
182
+
183
+ np.clip(target_l, 0, 100, out=target_l)
184
+ np.clip(target_a, -127, 127, out=target_a)
185
+ np.clip(target_b, -127, 127, out=target_b)
186
+
187
+ return cv2.cvtColor(np.stack([target_l,target_a,target_b], -1), cv2.COLOR_LAB2BGR)
188
+
189
+
190
+ def linear_color_transfer(target_img, source_img, mode='pca', eps=1e-5):
191
+ '''
192
+ Matches the colour distribution of the target image to that of the source image
193
+ using a linear transform.
194
+ Images are expected to be of form (w,h,c) and float in [0,1].
195
+ Modes are chol, pca or sym for different choices of basis.
196
+ '''
197
+ mu_t = target_img.mean(0).mean(0)
198
+ t = target_img - mu_t
199
+ t = t.transpose(2,0,1).reshape( t.shape[-1],-1)
200
+ Ct = t.dot(t.T) / t.shape[1] + eps * np.eye(t.shape[0])
201
+ mu_s = source_img.mean(0).mean(0)
202
+ s = source_img - mu_s
203
+ s = s.transpose(2,0,1).reshape( s.shape[-1],-1)
204
+ Cs = s.dot(s.T) / s.shape[1] + eps * np.eye(s.shape[0])
205
+ if mode == 'chol':
206
+ chol_t = np.linalg.cholesky(Ct)
207
+ chol_s = np.linalg.cholesky(Cs)
208
+ ts = chol_s.dot(np.linalg.inv(chol_t)).dot(t)
209
+ if mode == 'pca':
210
+ eva_t, eve_t = np.linalg.eigh(Ct)
211
+ Qt = eve_t.dot(np.sqrt(np.diag(eva_t))).dot(eve_t.T)
212
+ eva_s, eve_s = np.linalg.eigh(Cs)
213
+ Qs = eve_s.dot(np.sqrt(np.diag(eva_s))).dot(eve_s.T)
214
+ ts = Qs.dot(np.linalg.inv(Qt)).dot(t)
215
+ if mode == 'sym':
216
+ eva_t, eve_t = np.linalg.eigh(Ct)
217
+ Qt = eve_t.dot(np.sqrt(np.diag(eva_t))).dot(eve_t.T)
218
+ Qt_Cs_Qt = Qt.dot(Cs).dot(Qt)
219
+ eva_QtCsQt, eve_QtCsQt = np.linalg.eigh(Qt_Cs_Qt)
220
+ QtCsQt = eve_QtCsQt.dot(np.sqrt(np.diag(eva_QtCsQt))).dot(eve_QtCsQt.T)
221
+ ts = np.linalg.inv(Qt).dot(QtCsQt).dot(np.linalg.inv(Qt)).dot(t)
222
+ matched_img = ts.reshape(*target_img.transpose(2,0,1).shape).transpose(1,2,0)
223
+ matched_img += mu_s
224
+ matched_img[matched_img>1] = 1
225
+ matched_img[matched_img<0] = 0
226
+ return np.clip(matched_img.astype(source_img.dtype), 0, 1)
227
+
228
+ def lab_image_stats(image):
229
+ # compute the mean and standard deviation of each channel
230
+ (l, a, b) = cv2.split(image)
231
+ (lMean, lStd) = (l.mean(), l.std())
232
+ (aMean, aStd) = (a.mean(), a.std())
233
+ (bMean, bStd) = (b.mean(), b.std())
234
+
235
+ # return the color statistics
236
+ return (lMean, lStd, aMean, aStd, bMean, bStd)
237
+
238
+ def _scale_array(arr, clip=True):
239
+ if clip:
240
+ return np.clip(arr, 0, 255)
241
+
242
+ mn = arr.min()
243
+ mx = arr.max()
244
+ scale_range = (max([mn, 0]), min([mx, 255]))
245
+
246
+ if mn < scale_range[0] or mx > scale_range[1]:
247
+ return (scale_range[1] - scale_range[0]) * (arr - mn) / (mx - mn) + scale_range[0]
248
+
249
+ return arr
250
+
251
+ def channel_hist_match(source, template, hist_match_threshold=255, mask=None):
252
+ # Code borrowed from:
253
+ # https://stackoverflow.com/questions/32655686/histogram-matching-of-two-images-in-python-2-x
254
+ masked_source = source
255
+ masked_template = template
256
+
257
+ if mask is not None:
258
+ masked_source = source * mask
259
+ masked_template = template * mask
260
+
261
+ oldshape = source.shape
262
+ source = source.ravel()
263
+ template = template.ravel()
264
+ masked_source = masked_source.ravel()
265
+ masked_template = masked_template.ravel()
266
+ s_values, bin_idx, s_counts = np.unique(source, return_inverse=True,
267
+ return_counts=True)
268
+ t_values, t_counts = np.unique(template, return_counts=True)
269
+
270
+ s_quantiles = np.cumsum(s_counts).astype(np.float64)
271
+ s_quantiles = hist_match_threshold * s_quantiles / s_quantiles[-1]
272
+ t_quantiles = np.cumsum(t_counts).astype(np.float64)
273
+ t_quantiles = 255 * t_quantiles / t_quantiles[-1]
274
+ interp_t_values = np.interp(s_quantiles, t_quantiles, t_values)
275
+
276
+ return interp_t_values[bin_idx].reshape(oldshape)
277
+
278
+ def color_hist_match(src_im, tar_im, hist_match_threshold=255):
279
+ h,w,c = src_im.shape
280
+ matched_R = channel_hist_match(src_im[:,:,0], tar_im[:,:,0], hist_match_threshold, None)
281
+ matched_G = channel_hist_match(src_im[:,:,1], tar_im[:,:,1], hist_match_threshold, None)
282
+ matched_B = channel_hist_match(src_im[:,:,2], tar_im[:,:,2], hist_match_threshold, None)
283
+
284
+ to_stack = (matched_R, matched_G, matched_B)
285
+ for i in range(3, c):
286
+ to_stack += ( src_im[:,:,i],)
287
+
288
+
289
+ matched = np.stack(to_stack, axis=-1).astype(src_im.dtype)
290
+ return matched
291
+
292
+ def color_transfer_mix(img_src,img_trg):
293
+ img_src = np.clip(img_src*255.0, 0, 255).astype(np.uint8)
294
+ img_trg = np.clip(img_trg*255.0, 0, 255).astype(np.uint8)
295
+
296
+ img_src_lab = cv2.cvtColor(img_src, cv2.COLOR_BGR2LAB)
297
+ img_trg_lab = cv2.cvtColor(img_trg, cv2.COLOR_BGR2LAB)
298
+
299
+ rct_light = np.clip ( linear_color_transfer(img_src_lab[...,0:1].astype(np.float32)/255.0,
300
+ img_trg_lab[...,0:1].astype(np.float32)/255.0 )[...,0]*255.0,
301
+ 0, 255).astype(np.uint8)
302
+
303
+ img_src_lab[...,0] = (np.ones_like (rct_light)*100).astype(np.uint8)
304
+ img_src_lab = cv2.cvtColor(img_src_lab, cv2.COLOR_LAB2BGR)
305
+
306
+ img_trg_lab[...,0] = (np.ones_like (rct_light)*100).astype(np.uint8)
307
+ img_trg_lab = cv2.cvtColor(img_trg_lab, cv2.COLOR_LAB2BGR)
308
+
309
+ img_rct = color_transfer_sot( img_src_lab.astype(np.float32), img_trg_lab.astype(np.float32) )
310
+ img_rct = np.clip(img_rct, 0, 255).astype(np.uint8)
311
+
312
+ img_rct = cv2.cvtColor(img_rct, cv2.COLOR_BGR2LAB)
313
+ img_rct[...,0] = rct_light
314
+ img_rct = cv2.cvtColor(img_rct, cv2.COLOR_LAB2BGR)
315
+
316
+
317
+ return (img_rct / 255.0).astype(np.float32)
318
+
319
+ def color_transfer(ct_mode, img_src, img_trg):
320
+ """
321
+ color transfer for [0,1] float32 inputs
322
+ """
323
+ if ct_mode == 'lct':
324
+ out = linear_color_transfer (img_src, img_trg)
325
+ elif ct_mode == 'rct':
326
+ out = reinhard_color_transfer(img_src, img_trg)
327
+ elif ct_mode == 'mkl':
328
+ out = color_transfer_mkl (img_src, img_trg)
329
+ elif ct_mode == 'idt':
330
+ out = color_transfer_idt (img_src, img_trg)
331
+ elif ct_mode == 'sot':
332
+ out = color_transfer_sot (img_src, img_trg)
333
+ out = np.clip( out, 0.0, 1.0)
334
+ else:
335
+ raise ValueError(f"unknown ct_mode {ct_mode}")
336
+ return out
core/imagelib/common.py ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import numpy as np
2
+
3
+ def random_crop(img, w, h):
4
+ height, width = img.shape[:2]
5
+
6
+ h_rnd = height - h
7
+ w_rnd = width - w
8
+
9
+ y = np.random.randint(0, h_rnd) if h_rnd > 0 else 0
10
+ x = np.random.randint(0, w_rnd) if w_rnd > 0 else 0
11
+
12
+ return img[y:y+height, x:x+width]
13
+
14
+ def normalize_channels(img, target_channels):
15
+ img_shape_len = len(img.shape)
16
+ if img_shape_len == 2:
17
+ h, w = img.shape
18
+ c = 0
19
+ elif img_shape_len == 3:
20
+ h, w, c = img.shape
21
+ else:
22
+ raise ValueError("normalize: incorrect image dimensions.")
23
+
24
+ if c == 0 and target_channels > 0:
25
+ img = img[...,np.newaxis]
26
+ c = 1
27
+
28
+ if c == 1 and target_channels > 1:
29
+ img = np.repeat (img, target_channels, -1)
30
+ c = target_channels
31
+
32
+ if c > target_channels:
33
+ img = img[...,0:target_channels]
34
+ c = target_channels
35
+
36
+ return img
37
+
38
+ def cut_odd_image(img):
39
+ h, w, c = img.shape
40
+ wm, hm = w % 2, h % 2
41
+ if wm + hm != 0:
42
+ img = img[0:h-hm,0:w-wm,:]
43
+ return img
44
+
45
+ def overlay_alpha_image(img_target, img_source, xy_offset=(0,0) ):
46
+ (h,w,c) = img_source.shape
47
+ if c != 4:
48
+ raise ValueError("overlay_alpha_image, img_source must have 4 channels")
49
+
50
+ x1, x2 = xy_offset[0], xy_offset[0] + w
51
+ y1, y2 = xy_offset[1], xy_offset[1] + h
52
+
53
+ alpha_s = img_source[:, :, 3] / 255.0
54
+ alpha_l = 1.0 - alpha_s
55
+
56
+ for c in range(0, 3):
57
+ img_target[y1:y2, x1:x2, c] = (alpha_s * img_source[:, :, c] +
58
+ alpha_l * img_target[y1:y2, x1:x2, c])