-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathMarkedImg.py
1350 lines (1229 loc) · 48.3 KB
/
MarkedImg.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
"""
This File is part of bLUe software.
Copyright (C) 2017 Bernard Virot <[email protected]>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as
published by the Free Software Foundation, version 3.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Lesser Public License for more details.
You should have received a copy of the GNU Lesser General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
import numpy as np
import gc
from PySide2.QtCore import Qt, QDataStream, QFile, QIODevice, QSize, QPoint
import cv2
from copy import copy
from PySide2.QtGui import QTransform, QColor
from PySide2.QtWidgets import QApplication
from PySide2.QtGui import QPixmap, QImage, QPainter
from PySide2.QtCore import QRect
import exiftool
from bLUeGui.memory import weakProxy
from colorManagement import icc, convertQImage
from bLUeGui.bLUeImage import QImageBuffer, ndarrayToQImage
from bLUeGui.dialog import dlgWarn
from time import time
from lutUtils import LUT3DIdentity
from bLUeGui.baseSignal import baseSignal_bool, baseSignal_Int2
from utils import qColorToRGB
from versatileImg import vImage
class ColorSpace:
notSpecified = -1; sRGB = 1
class mImage(vImage):
"""
Multi-layer image : base class for editable images.
A mImage holds a presentation layer
for color management and a stack containing at least a
background layer. All layers share the same metadata instance.
To correctly render a mImage, widgets should override their
paint event handler.
"""
@classmethod
def restoreMeta(cls, srcFile, destFile, defaultorientation=True, thumbfile=None):
"""
# copy metadata from sidecar to image file. The sidecar is not removed.
If defaultorientaion is True the orientaion of the destination file is
set to "no change" (1). This way, saved images are shown as they were edited.
@param srcFile: source image or sidecar (the extension is replaced by .mie).
@type srcFile: str
@param destFile: image file
@type destFile: str
@param defaultorientation
@type defaultorientation: bool
@param thumbfile: thumbnail file
@type thumbfile: str
"""
with exiftool.ExifTool() as e:
e.copySidecar(srcFile, destFile)
if defaultorientation:
e.writeOrientation(destFile, '1')
if thumbfile is not None:
e.writeThumbnail(destFile, thumbfile)
def __init__(self, *args, **kwargs):
# as updatePixmap uses layersStack, it must be initialized
# before the call to super(). __init__
self.layersStack = []
# link back to QLayerView window
self.layerView = None
super().__init__(*args, **kwargs) # must be done before prLayer init.
# add background layer
bgLayer = QLayer.fromImage(self, parentImage=self)
bgLayer.isClipping = True
self.setModified(False)
self.activeLayerIndex = None
self.addLayer(bgLayer, name='Background')
# color management : we assume that the image profile is the working profile
self.colorTransformation = icc.workToMonTransform
# presentation layer
prLayer = QPresentationLayer(QImg=self, parentImage=self)
prLayer.name = 'presentation'
prLayer.role ='presentation'
prLayer.execute = lambda l=prLayer, pool=None: prLayer.applyNone()
prLayer.updatePixmap() # mandatory here as vImage init. can't do it
self.prLayer = prLayer
self.isModified = False
# rawpy object
self.rawImage = None
def bTransformed(self, transformation):
"""
Applies transformation to all layers in stack and returns
the new mImage
@param transformation:
@type transformation: Qtransform
@return:
@rtype: mimage
"""
img = mImage(QImg=self.transformed(transformation))
img.meta = self.meta
img.onImageChanged = self.onImageChanged
img.useThumb = self.useThumb
img.useHald = self.useHald
stack = []
for layer in self.layersStack:
tLayer = layer.bTransformed(transformation, img)
stack.append(tLayer)
img.layersStack = stack
gc.collect()
return img
def getActiveLayer(self):
"""
Returns the currently active layer.
@return: The active layer
@rtype: QLayer
"""
if self.activeLayerIndex is not None:
return self.layersStack[self.activeLayerIndex]
else:
return None
def setActiveLayer(self, stackIndex):
"""
Assigns stackIndex value to activeLayerIndex and
updates the layer view and tools
@param stackIndex: index in stack for the layer to select
@type stackIndex: int
@return: active layer
@rtype: QLayer
"""
lgStack = len(self.layersStack)
if stackIndex < 0 or stackIndex >= lgStack:
return
# clean old active layer
active = self.getActiveLayer()
if active is not None:
if active.tool is not None:
active.tool.hideTool()
# set new active layer
self.activeLayerIndex = stackIndex
if self.layerView is not None:
self.layerView.selectRow(lgStack - 1 - stackIndex)
active = self.getActiveLayer()
if active.tool is not None and active.visible:
active.tool.showTool()
return active
def getActivePixel(self,x, y, fromInputImg=True):
"""
Reads the RGB colors of the pixel at (x, y) from the active layer.
If fromInputImg is True (default), the pixel is taken from
the input image, otherwise from the current image.
Coordinates are relative to the full sized image.
If (x,y) is outside the image, (0, 0, 0) is returned.
@param x, y: coordinates of pixel, relative to the full-sized image
@return: pixel RGB colors
@rtype: 3-uple of int
"""
x, y = self.full2CurrentXY(x, y)
activeLayer = self.getActiveLayer()
qClr = activeLayer.inputImg().pixelColor(x, y) if fromInputImg else activeLayer.getCurrentImage().pixelColor(x, y)
# pixelColor returns an invalid color if (x,y) is out of range
# we return black
if not qClr.isValid():
qClr = QColor(0,0,0)
return qColorToRGB(qClr)
def getPrPixel(self, x, y):
"""
Reads the RGB colors of the pixel at (x, y) from
the presentation layer. They are the (non color managed)
colors of the displayed pixel.
Coordinates are relative to the full sized image.
If (x,y) is outside the image, (0, 0, 0) is returned.
@param x, y: coordinates of pixel, relative to the full-sized image
@return: pixel RGB colors
@rtype: 3-uple of int
"""
x, y = self.full2CurrentXY(x, y)
qClr = self.prLayer.getCurrentImage().pixelColor(x, y)
if not qClr.isValid():
qClr = QColor(0,0,0)
return qColorToRGB(qClr)
def cacheInvalidate(self):
"""
Invalidate cache buffers. The method is
called by applyToStack for each layer after layer.execute
"""
vImage.cacheInvalidate(self)
for layer in self.layersStack:
layer.cacheInvalidate() # As Qlayer doesn't inherit from mImage, we call vImage.cacheInvalidate(layer)
def setThumbMode(self, value):
if value == self.useThumb:
return
self.useThumb = value
# recalculate the whole stack
self.layerStack[0].apply()
def updatePixmap(self):
"""
Updates all pixmaps
Overrides vImage.updatePixmap()
"""
vImage.updatePixmap(self)
for layer in self.layersStack:
vImage.updatePixmap(layer)
self.prLayer.updatePixmap()
def getStackIndex(self, layer):
p = id(layer)
i = -1
for i,l in enumerate(self.layersStack):
if id(l) == p:
break
return i
def addLayer(self, layer, name='', index=None):
"""
Adds a layer.
@param layer: layer to add (if None, add a fresh layer)
@type layer: QLayer
@param name:
@type name: str
@param index: index of insertion in layersStack (top of active layer if index=None)
@type index: int
@return: the layer added
@rtype: QLayer
"""
# build a unique name
usedNames = [l.name for l in self.layersStack]
a = 1
trialname = name if len(name) > 0 else 'noname'
while trialname in usedNames:
trialname = name + '_'+ str(a)
a = a+1
if layer is None:
layer = QLayer(QImg=self, parentImage=self) # TODO 11/09/18 added parentImage validate
layer.fill(Qt.white)
layer.name = trialname
#layer.parentImage = self # TODO 07/06/18 validate suppression
if index is None:
if self.activeLayerIndex is not None:
# add on top of active layer if any
index = self.activeLayerIndex + 1 #TODO +1 added 03/05/18 validate
else:
# empty stack
index = 0
self.layersStack.insert(index, layer)
self.setActiveLayer(index)
layer.meta = self.meta
layer.parentImage = self
self.setModified(True)
return layer
def removeLayer(self, index=None):
if index is None:
return
self.layersStack.pop(index)
def addAdjustmentLayer(self, layerType=None, name='', role='', index=None, sourceImg=None):
"""
Adds an adjustment layer to the layer stack, at
position index (default is top of active layer).
The parameter layerType controls the class of the layer; it should
be a subclass of QLayer, default is QLayer itself.
If the parameter sourceImg is given, the layer is a
QLayerImage object built from sourceImg, and layerType has no effect.
@param layerType: layer class
@type layerType: QLayer subclass
@param name:
@type name: str
@param role:
@type role: str
@param index:
@type index: int
@param sourceImg: source image
@type sourceImg: QImage
@return: the new layer
@rtype: subclass of QLayer
"""
if index is None:
# adding on top of active layer
index = self.activeLayerIndex
if sourceImg is None:
# set layer from active layer
if layerType is None:
layer = QLayer.fromImage(self.layersStack[index], parentImage=self)
else:
layer = layerType.fromImage(self.layersStack[index], parentImage=self)
else:
# set layer from image :
layer = QLayerImage.fromImage(self.layersStack[index], parentImage=self, sourceImg=sourceImg)
layer.role = role
self.addLayer(layer, name=name, index=index + 1)
# add autoSpline attribute to contrast layer only
if role in ['CONTRAST', 'RAW']:
layer.autoSpline = True
# init thumb
if layer.parentImage.useThumb:
layer.thumb = layer.inputImg().copy()
group = self.layersStack[index].group
if group:
layer.group = group
layer.mask = self.layersStack[index].mask
layer.maskIsEnabled = True
# sync caches
layer.updatePixmap()
return layer
def addSegmentationLayer(self, name='', index=None):
if index is None:
index = self.activeLayerIndex
layer = QLayer.fromImage(self.layersStack[index], parentImage=self)
layer.role = 'SEGMENT'
layer.inputImg = lambda: self.layersStack[layer.getLowerVisibleStackIndex()].getCurrentMaskedImage() # TODO 13/06/18 is it different from other layers?
self.addLayer(layer, name=name, index=index + 1)
layer.maskIsEnabled = True
layer.maskIsSelected = True
# mask pixels are not yet painted as FG or BG
# so we mark them as invalid
layer.mask.fill(vImage.defaultColor_Invalid)
layer.paintedMask = layer.mask.copy()
# add noSegment flag. It blocks/allows the execution of grabcut algorithm
# in applyGrabcut : if True, further stack updates
# do not redo the segmentation. The flag is toggled by the Apply Button
# slot of segmentForm.
layer.noSegment = False
layer.updatePixmap()
return layer
def dupLayer(self, index=None):
"""
inserts in layersStack at position index+1 a copy of the layer
at position index. If index is None (default value), the layer is inserted
on top of the stack. Adjustment layers are not duplicated.
@param index:
@type index: int
@return:
"""
if index is None:
index = len(self.layersStack) - 1
layer0 = self.layersStack[index]
if layer0.isAdjustLayer():
return
layer1 = QLayer.fromImage(layer0, parentImage=self)
self.addLayer(layer1, name=layer0.name, index=index+1)
def mergeVisibleLayers(self):
"""
Merges the visible masked images and returns the
resulting QImage, eventually scaled to fit the image size.
@return: image
@rtype: QImage
"""
# init a new image
img = QImage(self.width(), self.height(), self.format())
# Image may contain transparent pixels, hence we
# fill the image with a background color.
img.fill(vImage.defaultBgColor)
# draw layers with (eventually) masked areas.
qp = QPainter(img)
qp.drawImage(QRect(0, 0, self.width(), self.height()), self.layersStack[-1].getCurrentMaskedImage())
qp.end()
return img
def save(self, filename, quality=-1, compression=-1, crops=None):
"""
Overrides QImage.save().
Writes the presentation layer to a file and returns a
thumbnail with standard size (160x120 or 120x160).
Raises IOError if the saving fails.
@param filename:
@type filename: str
@param quality: integer value in range 0..100, or -1
@type quality: int
@return: thumbnail of the saved image
@rtype: QImage
"""
def transparencyCheck(buf):
if np.any(buf[:,:,3] < 255):
dlgWarn('Transparency will be lost. Use PNG format instead')
# don't save thumbnails
if self.useThumb:
return None
# get the final image from the presentation layer.
# This image is NOT color managed (prLayer.qPixmap
# only is color managed)
img = self.prLayer.getCurrentImage()
# imagewriter and QImage.save are unusable for tif files,
# due to bugs in libtiff, hence we use opencv imwrite.
fileFormat = filename[-3:].upper()
buf = QImageBuffer(img)
if fileFormat == 'JPG':
transparencyCheck(buf)
buf = buf[:, :, :3]
params = [cv2.IMWRITE_JPEG_QUALITY, quality] # quality range 0..100
elif fileFormat == 'PNG':
params = [cv2.IMWRITE_PNG_COMPRESSION, compression] # compression range 0..9
elif fileFormat == 'TIF':
transparencyCheck(buf)
buf = buf[:, :, :3]
params = []
else:
raise IOError("Invalid File Format\nValid formats are jpg, png, tif ")
if self.isCropped:
# make slices
w, h = self.width(), self.height()
w1, w2 = int(self.cropLeft), w - int(self.cropRight)
h1, h2 = int(self.cropTop), h - int(self.cropBottom)
buf = buf[h1:h2, w1:w2,:]
# build thumbnail from (evenyually) cropped image
# choose thumb size
wf, hf = buf.shape[1], buf.shape[0]
if wf > hf:
wt, ht = 160, 120
else:
wt, ht = 120, 160
thumb = ndarrayToQImage(np.ascontiguousarray(buf[:,:,:3][:,:,::-1]), format=QImage.Format_RGB888).scaled(wt,ht, Qt.KeepAspectRatio)
#wr, hr = thumb.width(), thumb.height()
#thumb = thumb.copy(QRect((wr-wt)//2,(hr-ht)//2, wt, ht))
written = cv2.imwrite(filename, buf, params) #BGR order
if not written:
raise IOError("Cannot write file %s " % filename)
# self.setModified(False) # cannot be reset if the image is modified again
return thumb
def writeStackToStream(self, dataStream):
dataStream.writeInt32(len(self.layersStack))
for layer in self.layersStack:
"""
dataStream.writeQString('menuLayer(None, "%s")' % layer.actionName)
dataStream.writeQString('if "%s" != "actionNull":\n dataStream=window.label.img.layersStack[-1].readFromStream(dataStream)' % layer.actionName)
"""
dataStream.writeQString(layer.actionName)
for layer in self.layersStack:
grForm = layer.getGraphicsForm()
if grForm is not None:
grForm.writeToStream(dataStream)
def readStackFromStream(self, dataStream):
# stack length
count = dataStream.readInt32()
script = []
for i in range(count):
actionName = dataStream.readQString()
script.append('menuLayer(None, "%s")' % actionName)
script.append('if "%s" != "actionNull":\n dataStream=window.label.img.layersStack[-1].readFromStream(dataStream)' % actionName)
return script
def saveStackToFile(self, filename):
qf = QFile(filename)
if not qf.open(QIODevice.WriteOnly):
raise IOError('cannot open file %s' % filename)
dataStream = QDataStream(qf)
self.writeStackToStream(dataStream)
qf.close()
def loadStackFromFile(self, filename):
qf = QFile(filename)
if not qf.open(QIODevice.ReadOnly):
raise IOError('cannot open file %s' % filename)
dataStream = QDataStream(qf)
script = self.readStackFromStream(dataStream)
#qf.close()
return script, qf, dataStream
class imImage(mImage) :
"""
Zoomable and draggable multi-layer image :
this is the base class for bLUe documents
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Zoom coeff :
# Zoom_coeff = 1.0 displays an image fitting the
# size of the current window ( NOT the actual pixels of the image).
self.Zoom_coeff = 1.0
self.xOffset, self.yOffset = 0, 0
self.isMouseSelectable =True
self.isModified = False
def bTransformed(self, transformation):
"""
Applies transformation to all layers in stack
and returns the new imImage.
@param transformation:
@type transformation: QTransform
@return:
@rtype: imImage
"""
img = imImage(QImg=self.transformed(transformation))
img.meta = self.meta
img.onImageChanged = self.onImageChanged
img.useThumb = self.useThumb
img.useHald = self.useHald
stack = []
# apply transformation to the stack. Note that
# the presentation layer is automatically rebuilt
for layer in self.layersStack:
tLayer = layer.bTransformed(transformation, img)
# keep ref to original layer (needed by execute)
tLayer.parentLayer = layer.parentLayer
# record new transformed layer in original layer (needed by execute)
tLayer.parentLayer.tLayer = tLayer
# link back grWindow to tLayer
# using weak ref for back links
if tLayer.view is not None:
# for historical reasons, graphic forms inheriting
# from QGraphicsView use form.scene().layer attribute,
# others use form.layer
"""
# use weak refs for back links
if type(tLayer) in weakref.ProxyTypes:
wtLayer = tLayer
else:
wtLayer = weakref.proxy(tLayer)
"""
grForm = tLayer.getGraphicsForm()
# the grForm.layer property handles weak refs
grForm.layer = tLayer
grForm.scene().layer = grForm.layer # wtLayer
stack.append(tLayer)
img.layersStack = stack
gc.collect()
return img
def resize(self, pixels, interpolation=cv2.INTER_CUBIC):
"""
Resize image and layers
@param pixels:
@param interpolation:
@return: resized imImage object
@rtype: imImage
"""
# resized vImage
rszd0 = super().resize(pixels, interpolation=interpolation)
# resized imImage
rszd = imImage(QImg=rszd0,meta=copy(self.meta))
rszd.rect = rszd0.rect
for k, l in enumerate(self.layersStack):
if l.name != "background" and l.name != 'drawlayer':
img = QLayer.fromImage(l.resize(pixels, interpolation=interpolation), parentImage=rszd)
rszd.layersStack.append(img)
#rszd._layers[l.name] = img
self.isModified = True
return rszd
def view(self):
return self.Zoom_coeff, self.xOffset, self.yOffset
def setView(self, zoom=1.0, xOffset=0.0, yOffset=0.0):
"""
Sets viewing conditions: zoom, offset
@param zoom: zoom coefficient
@type zoom: float
@param xOffset: x-offset
@type xOffset: int
@param yOffset: y-offset
@type yOffset: int
@return:
"""
self.Zoom_coeff, self.xOffset, self.yOffset = zoom, xOffset, yOffset
def fit_window(self, win):
"""
reset Zoom_coeff and offset
@param win:
@type win:
"""
self.Zoom_coeff = 1.0
self.xOffset, self.yOffset = 0.0, 0.0
class QLayer(vImage):
"""
Base class for image layers
"""
@classmethod
def fromImage(cls, mImg, parentImage=None):
"""
Returns a QLayer object initialized with mImg.
@param mImg:
@type mImg: QImage
@param parentImage:
@type parentImage: mImage
@return:
@rtype: Qlayer
"""
layer = QLayer(QImg=mImg, parentImage=parentImage)
layer.parentImage = parentImage
return layer
def __init__(self, *args, **kwargs):
############################################################
# Signals
# Making QLayer inherit from QObject leads to
# a buggy behavior of hasattr and getattr.
# So, we don't add signals as first level class attributes.
# Instead, we use instances of ad hoc signal containers (cf. utils.py)
self.visibilityChanged = baseSignal_bool()
self.colorPicked = baseSignal_Int2()
###########################################################
# when a geometric transformation is applied to the whole image
# each layer must be replaced with a transformed layer, recorded in tLayer
# and tLayer.parentLayer keeps a reference to the original layer.
self.tLayer = self
self.parentLayer = self
self.modified = False
self.name='noname'
self.visible = True
# add signal instance (layer visibility change,..)
#self.signals = baseSignals()
self.isClipping = False
self.role = kwargs.pop('role', '')
self.tool = None
# back link to parent image
parentImage = kwargs.pop('parentImage', None)
self.parentImage = weakProxy(parentImage)
"""
if type(parentImage) in weakref.ProxyTypes: # TODO 21/11/18 added weakref for back link
self.parentImage = parentImage
else:
self.parentImage = weakref.proxy(parentImage)
"""
#self.parentImage = kwargs.pop('parentImage', None)
# layer opacity is used by QPainter operations.
# Its value must be in the range 0.0...1.0
self.opacity = 1.0
self.compositionMode = QPainter.CompositionMode_SourceOver
# The next two attributes are used by adjustment layers only.
self.execute = lambda l=None, pool=None: l.updatePixmap() if l is not None else None
self.options = {}
# actionName is used by methods graphics***.writeToStream()
self.actionName = 'actionNull'
# view is the dock widget containing
# the graphics form associated with the layer
self.view = None
# containers are initialized (only once) by
# getCurrentMaskedImage. Their type is QLayer
self.maskedImageContainer = None
self.maskedThumbContainer = None
# consecutive layers can be grouped.
# A group is a list of QLayer objects
self.group = []
# layer offsets
self.xOffset, self.yOffset = 0, 0
self.Zoom_coeff = 1.0
# clone dup layer shift and zoom relative to current layer
self.xAltOffset, self.yAltOffset = 0, 0
self.AltZoom_coeff = 1.0
super().__init__(*args, **kwargs)
self.updatePixmap()
def getGraphicsForm(self):
"""
Return the graphics form associated with the layer
@return:
@rtype: QWidget
"""
if self.view is not None:
return self.view.widget()
return None
def isActiveLayer(self):
if self.parentImage.getActiveLayer() is self:
return True
return False
def getMmcSpline(self):
"""
Returns the spline used for multimode contrast
correction if it is initialized, and None otherwise.
@return:
@rtype: activeSpline
"""
# get layer graphic form
grf = self.getGraphicsForm()
# manual curve form
if grf.contrastForm is not None:
return grf.contrastForm.scene().cubicItem
return None
def addTool(self, tool):
"""
Adds tool to layer
@param tool:
@type tool: rotatingTool
"""
self.tool = tool
tool.modified = False
tool.layer = self
try:
tool.layer.visibilityChanged.sig.disconnect() # TODO signals removed 30/09/18 validate
except:
pass
tool.layer.visibilityChanged.sig.connect(tool.setVisible) # TODO signals removed 30/09/18 validate
tool.img = self.parentImage
w, h = tool.img.width(), tool.img.height()
for role, pos in zip(['topLeft', 'topRight', 'bottomRight', 'bottomLeft'],
[QPoint(0, 0), QPoint(w, 0), QPoint(w, h), QPoint(0, h)]):
tool.btnDict[role].posRelImg = pos
tool.btnDict[role].posRelImg_ori = pos
tool.btnDict[role].posRelImg_frozen = pos
tool.moveRotatingTool()
def setVisible(self, value):
"""
Sets self.visible to value and emit visibilityChanged.sig
@param value:
@type value: bool
"""
self.visible = value
self.visibilityChanged.sig.emit(value) # TODO signals removed 30/09/18 validate
def bTransformed(self, transformation, parentImage):
"""
Applies transformation to a copy of layer and returns the copy.
@param transformation:
@type transformation: QTransform
@param parentImage:
@type parentImage: vImage
@return: transformed layer
@rtype: QLayer
"""
# init a new layer from transformed image :
# all static attributes (caches...) are reset to default, but thumb
tLayer = QLayer.fromImage(self.transformed(transformation), parentImage=parentImage)
# copy dynamic attributes from old layer
for a in self.__dict__.keys():
if a not in tLayer.__dict__.keys():
tLayer.__dict__[a] = self.__dict__[a]
tLayer.name = self.name
tLayer.actionName = self.actionName
tLayer.view = self.view
tLayer.visible = self.visible # TODO added 25/09/18 validate
# cut link from old layer to graphic form
# self.view = None # TODO 04/12/17 validate
tLayer.execute = self.execute
tLayer.mask = self.mask.transformed(transformation)
tLayer.maskIsEnabled, tLayer.maskIsSelected = self.maskIsEnabled, self.maskIsSelected
return tLayer
def initThumb(self):
"""
Override vImage.initThumb, to set the parentImage attribute
"""
super().initThumb()
self.thumb.parentImage = self.parentImage
def initHald(self):
"""
Build a hald image (as a QImage) from identity 3D LUT.
"""
if not self.cachesEnabled:
return
s = int((LUT3DIdentity.size) ** (3.0 / 2.0)) + 1
buf0 = LUT3DIdentity.toHaldArray(s, s).haldBuffer
#self.hald = QLayer(QImg=QImage(QSize(190,190), QImage.Format_ARGB32))
self.hald = QImage(QSize(s, s), QImage.Format_ARGB32)
buf1 = QImageBuffer(self.hald)
buf1[:, :, :3] = buf0
buf1[:,:,3] = 255
self.hald.parentImage = self.parentImage
def getHald(self):
if not self.cachesEnabled:
s = int((LUT3DIdentity.size) ** (3.0 / 2.0)) + 1
buf0 = LUT3DIdentity.toHaldArray(s, s).haldBuffer
# self.hald = QLayer(QImg=QImage(QSize(190,190), QImage.Format_ARGB32))
hald = QImage(QSize(s, s), QImage.Format_ARGB32)
buf1 = QImageBuffer(hald)
buf1[:, :, :3] = buf0
buf1[:, :, 3] = 255
hald.parentImage = self.parentImage
return hald
if self.hald is None:
self.initHald()
return self.hald
def getCurrentImage(self):
"""
Returns current (full, preview or hald) image, according to
the value of the flags useThumb and useHald. The thumbnail and hald
are computed if they are not initialized.
Otherwise, they are not updated unless self.thumb is
None or purgeThumb is True.
Overrides vImage method
@return: current image
@rtype: QLayer
"""
if self.parentImage.useHald:
return self.getHald()
if self.parentImage.useThumb:
return self.getThumb()
else:
return self
def inputImg(self):
"""
Updates and returns maskedImageContainer and maskedThumbContainer
@return:
@rtype: Qlayer
"""
# TODO 29/05/18 optimize to avoid useless updates of containers (add a valid flag to each container and invalidate it in cacheInvalidate())
return self.parentImage.layersStack[self.getLowerVisibleStackIndex()].getCurrentMaskedImage()
def full2CurrentXY(self, x, y):
"""
Maps x,y coordinates of pixel in the full image to
coordinates in current image.
@param x:
@type x: int or float
@param y:
@type y: int or float
@return:
@rtype: 2uple of int
"""
if self.parentImage.useThumb:
currentImg = self.getThumb()
x = (x * currentImg.width()) / self.width()
y = (y * currentImg.height()) / self.height()
return int(x), int(y)
def getCurrentMaskedImage(self):
"""
Reduces the layer stack up to self (included),
taking into account the masks. if self.isClipping is True
self.mask applies to all lower layers and to self only otherwise.
The method uses the non color managed rPixmaps to build the masked image.
For convenience, mainly to be able to use its color space buffers,
the built image is of type QLayer. It is drawn on a container image,
created only once.
@return: masked image
@rtype: QLayer
"""
# init containers if needed
if self.parentImage.useHald:
return self.getHald()
if self.maskedThumbContainer is None:
self.maskedThumbContainer = QLayer.fromImage(self.getThumb(), parentImage=self.parentImage)
if self.maskedImageContainer is None:
self.maskedImageContainer = QLayer.fromImage(self, parentImage=self.parentImage)
if self.parentImage.useThumb:
img = self.maskedThumbContainer
else:
img = self.maskedImageContainer
# no thumbnails for containers
img.getThumb = lambda: img
# draw lower stack
qp = QPainter(img)
top = self.parentImage.getStackIndex(self)
bottom = 0
for i, layer in enumerate(self.parentImage.layersStack[bottom:top+1]):
if layer.visible:
if i == 0:
qp.setCompositionMode(QPainter.CompositionMode_Source)
else:
qp.setOpacity(layer.opacity)
qp.setCompositionMode(layer.compositionMode)
if layer.rPixmap is not None:
qp.drawPixmap(QRect(0,0,img.width(), img.height()), layer.rPixmap)
else:
qp.drawImage(QRect(0,0,img.width(), img.height()), layer.getCurrentImage())
# clipping
if layer.isClipping and layer.maskIsEnabled: #TODO modified 23/06/18
# draw mask as opacity mask
# mode DestinationIn (set dest opacity to source opacity)
qp.setCompositionMode(QPainter.CompositionMode_DestinationIn)
omask = vImage.color2OpacityMask(layer.mask)
qp.drawImage(QRect(0, 0, img.width(), img.height()), omask)
qp.end()
return img
def applyToStack(self):
"""
Apply new layer parameters and propagate changes to upper layers.
"""
# recursive function
def applyToStack_(layer, pool=None):
# apply transformation
if layer.visible:
start = time()
layer.execute(l=layer)
layer.cacheInvalidate()
print("%s %.2f" %(layer.name, time()-start))
stack = layer.parentImage.layersStack
lg = len(stack)
ind = layer.getStackIndex() + 1
# update histograms displayed
# on the layer form, if any
if ind < lg:
grForm = stack[ind].getGraphicsForm()
if grForm is not None:
grForm.updateHists()
# get next upper visible layer
while ind < lg:
if stack[ind].visible:
break
ind += 1
if ind < lg:
layer1 = stack[ind]
applyToStack_(layer1, pool=pool)
try:
QApplication.setOverrideCursor(Qt.WaitCursor)
QApplication.processEvents()
applyToStack_(self, pool=None)
# update the presentation layer
self.parentImage.prLayer.execute(l=None, pool=None)
finally:
self.parentImage.setModified(True)
QApplication.restoreOverrideCursor()
QApplication.processEvents()
"""
def applyToStackIter(self):
#iterative version of applyToStack
stack = self.parentImage.layersStack
ind = self.getStackIndex() + 1
try:
QApplication.setOverrideCursor(Qt.WaitCursor)
QApplication.processEvents()
self.execute()
for layer in stack[ind:]:
if layer.visible:
layer.cacheInvalidate()
# for hald friendly layer compute output hald, otherwise compute output image
layer.execute()
finally:
QApplication.restoreOverrideCursor()
QApplication.processEvents()
"""
def isAdjustLayer(self):
return self.view is not None #hasattr(self, 'view')
def isSegmentLayer(self):
return 'SEGMENT' in self.role
def isCloningLayer(self):
return 'CLONING' in self.role
def isGeomLayer(self):
return 'GEOM' in self.role
def is3DLUTLayer(self):
return '3DLUT' in self.role
def isRawLayer(self):
return 'RAW' in self.role
def updatePixmap(self, maskOnly=False):
"""
Synchronize rPixmap with the layer image and mask.
if maskIsEnabled is False, the mask is not used.
If maskIsEnabled is True, then
- if maskIsSelected is True, the mask is drawn over
the layer as a color mask.
- if maskIsSelected is False, the mask is drawn as an
opacity mask, setting the image opacity to that of the mask
(mode DestinationIn).
@param maskOnly: not used : for consistency with overriding method signature
@type maskOnly: boolean
"""
rImg = self.getCurrentImage()
# apply layer transformation. Missing pixels are set to QColor(0,0,0,0)
if self.xOffset != 0 or self.yOffset != 0:
x,y = self.full2CurrentXY(self.xOffset, self.yOffset)
rImg = rImg.copy(QRect(-x, -y, rImg.width()*self.Zoom_coeff, rImg.height()*self.Zoom_coeff))
if self.maskIsEnabled:
rImg = vImage.visualizeMask(rImg, self.mask, color=self.maskIsSelected, clipping=True) #self.isClipping)
self.rPixmap = QPixmap.fromImage(rImg)
self.setModified(True)