remi.cozot il y a 3 ans
Parent
commit
1a0a498967
67 fichiers modifiés avec 4486 ajouts et 0 suppressions
  1. 150 0
      _miamG/_miamG.pyproj
  2. 1 0
      _miamG/gui/__init__.py
  3. 126 0
      _miamG/gui/guiController/MainWindowController.py
  4. 1 0
      _miamG/gui/guiController/__init__.py
  5. 44 0
      _miamG/gui/guiModel/ImageModel.py
  6. 84 0
      _miamG/gui/guiModel/MainWindowModel.py
  7. 1 0
      _miamG/gui/guiModel/__init__.py
  8. 29 0
      _miamG/gui/guiView/FigureWidget.py
  9. 44 0
      _miamG/gui/guiView/ImageWidget.py
  10. 267 0
      _miamG/gui/guiView/MainWindow.py
  11. 1 0
      _miamG/gui/guiView/__init__.py
  12. 1 0
      _miamG/miam/__init__.py
  13. 92 0
      _miamG/miam/aesthetics/Composition.py
  14. 109 0
      _miamG/miam/aesthetics/LightnessAesthetics.py
  15. 169 0
      _miamG/miam/aesthetics/Palette.py
  16. 1 0
      _miamG/miam/aesthetics/__init__.py
  17. 12 0
      _miamG/miam/classification/KmeanDisplay.py
  18. 1 0
      _miamG/miam/classification/__init__.py
  19. 64 0
      _miamG/miam/classification/colorPaletteKmeanDisplay.py
  20. 37 0
      _miamG/miam/classification/histCosineKmeanDisplay.py
  21. 315 0
      _miamG/miam/classification/kmeans.py
  22. 196 0
      _miamG/miam/histogram/Histogram.py
  23. 1 0
      _miamG/miam/histogram/__init__.py
  24. 1 0
      _miamG/miam/html/__init__.py
  25. 60 0
      _miamG/miam/html/generator.py
  26. 71 0
      _miamG/miam/image/ColorSpace.py
  27. 62 0
      _miamG/miam/image/Exif.py
  28. 56 0
      _miamG/miam/image/channel.py
  29. 15 0
      _miamG/miam/image/imageType.py
  30. 21 0
      _miamG/miam/imageDB/Builder.py
  31. 15 0
      _miamG/miam/imageDB/Checker.py
  32. 67 0
      _miamG/miam/imageDB/HwHDRBuilder.py
  33. 64 0
      _miamG/miam/imageDB/ImageDB.py
  34. 122 0
      _miamG/miam/imageDB/ImageDB_HDD.py
  35. 53 0
      _miamG/miam/imageDB/POGChecker.py
  36. 1 0
      _miamG/miam/imageDB/__init__.py
  37. 115 0
      _miamG/miam/imageDB/utils.py
  38. 65 0
      _miamG/miam/math/Distance.py
  39. 37 0
      _miamG/miam/math/Normalize.py
  40. 1 0
      _miamG/miam/math/__init__.py
  41. 139 0
      _miamG/miam/myQtApp.py
  42. 76 0
      _miamG/miam/pointcloud/PointCloud2D.py
  43. 1 0
      _miamG/miam/pointcloud/__init__.py
  44. 46 0
      _miamG/miam/processing/Blend.py
  45. 215 0
      _miamG/miam/processing/ColorSpaceTransform.py
  46. 85 0
      _miamG/miam/processing/ContrastControl.py
  47. 41 0
      _miamG/miam/processing/Duplicate.py
  48. 87 0
      _miamG/miam/processing/ExposureControl.py
  49. 38 0
      _miamG/miam/processing/Fuse.py
  50. 43 0
      _miamG/miam/processing/GaussianFilter.py
  51. 41 0
      _miamG/miam/processing/LaplaceFilter.py
  52. 43 0
      _miamG/miam/processing/MaskSegmentPercentile.py
  53. 21 0
      _miamG/miam/processing/NoOp.py
  54. 16 0
      _miamG/miam/processing/Processing.py
  55. 69 0
      _miamG/miam/processing/SumSquaredLaplace.py
  56. 49 0
      _miamG/miam/processing/TMO_CCTF.py
  57. 90 0
      _miamG/miam/processing/TMO_Lightness.py
  58. 65 0
      _miamG/miam/processing/TMO_Linear.py
  59. 54 0
      _miamG/miam/processing/ToOne.py
  60. 30 0
      _miamG/miam/processing/Ymap.py
  61. 0 0
      _miamG/miam/processing/__init__.py
  62. 33 0
      _miamG/miam/utils.py
  63. 41 0
      _miamG/miam/workflow/WFConnector.py
  64. 33 0
      _miamG/miam/workflow/WFNode.py
  65. 107 0
      _miamG/miam/workflow/WFProcess.py
  66. 450 0
      _miamG/miam/workflow/WFWorkflow.py
  67. 1 0
      _miamG/miam/workflow/__init__.py

+ 150 - 0
_miamG/_miamG.pyproj

@@ -21,18 +21,168 @@
     <EnableUnmanagedDebugging>false</EnableUnmanagedDebugging>
   </PropertyGroup>
   <ItemGroup>
+    <Compile Include="gui\guiController\MainWindowController.py" />
+    <Compile Include="gui\guiController\__init__.py" />
+    <Compile Include="gui\guiModel\ImageModel.py" />
+    <Compile Include="gui\guiModel\MainWindowModel.py" />
+    <Compile Include="gui\guiModel\__init__.py" />
+    <Compile Include="gui\guiView\FigureWidget.py" />
+    <Compile Include="gui\guiView\ImageWidget.py" />
+    <Compile Include="gui\guiView\MainWindow.py" />
+    <Compile Include="gui\guiView\__init__.py" />
+    <Compile Include="gui\__init__.py" />
+    <Compile Include="miam\aesthetics\Composition.py" />
+    <Compile Include="miam\aesthetics\LightnessAesthetics.py" />
+    <Compile Include="miam\aesthetics\Palette.py" />
+    <Compile Include="miam\aesthetics\__init__.py" />
+    <Compile Include="miam\classification\colorPaletteKmeanDisplay.py" />
+    <Compile Include="miam\classification\histCosineKmeanDisplay.py" />
+    <Compile Include="miam\classification\KmeanDisplay.py" />
+    <Compile Include="miam\classification\kmeans.py" />
+    <Compile Include="miam\classification\__init__.py" />
+    <Compile Include="miam\histogram\Histogram.py" />
+    <Compile Include="miam\histogram\__init__.py" />
+    <Compile Include="miam\html\generator.py" />
+    <Compile Include="miam\html\__init__.py" />
+    <Compile Include="miam\imageDB\Builder.py" />
+    <Compile Include="miam\imageDB\Checker.py" />
+    <Compile Include="miam\imageDB\HwHDRBuilder.py" />
+    <Compile Include="miam\imageDB\ImageDB.py" />
+    <Compile Include="miam\imageDB\ImageDB_HDD.py" />
+    <Compile Include="miam\imageDB\POGChecker.py" />
+    <Compile Include="miam\imageDB\utils.py" />
+    <Compile Include="miam\imageDB\__init__.py" />
+    <Compile Include="miam\image\channel.py" />
+    <Compile Include="miam\image\ColorSpace.py" />
+    <Compile Include="miam\image\Exif.py" />
     <Compile Include="miam\image\Image.py" />
+    <Compile Include="miam\image\imageType.py" />
     <Compile Include="miam\image\__init__.py">
       <SubType>Code</SubType>
     </Compile>
+    <Compile Include="miam\math\Distance.py" />
+    <Compile Include="miam\math\Normalize.py" />
+    <Compile Include="miam\math\__init__.py" />
+    <Compile Include="miam\myQtApp.py" />
+    <Compile Include="miam\pointcloud\PointCloud2D.py" />
+    <Compile Include="miam\pointcloud\__init__.py" />
+    <Compile Include="miam\processing\Blend.py" />
+    <Compile Include="miam\processing\ColorSpaceTransform.py" />
+    <Compile Include="miam\processing\ContrastControl.py" />
+    <Compile Include="miam\processing\Duplicate.py" />
+    <Compile Include="miam\processing\ExposureControl.py" />
+    <Compile Include="miam\processing\Fuse.py" />
+    <Compile Include="miam\processing\GaussianFilter.py" />
+    <Compile Include="miam\processing\LaplaceFilter.py" />
+    <Compile Include="miam\processing\MaskSegmentPercentile.py" />
+    <Compile Include="miam\processing\NoOp.py" />
+    <Compile Include="miam\processing\Processing.py" />
+    <Compile Include="miam\processing\SumSquaredLaplace.py" />
+    <Compile Include="miam\processing\TMO_CCTF.py" />
+    <Compile Include="miam\processing\TMO_Lightness.py" />
+    <Compile Include="miam\processing\TMO_Linear.py" />
+    <Compile Include="miam\processing\ToOne.py" />
+    <Compile Include="miam\processing\Ymap.py" />
+    <Compile Include="miam\processing\__init__.py" />
+    <Compile Include="miam\utils.py" />
+    <Compile Include="miam\workflow\WFConnector.py" />
+    <Compile Include="miam\workflow\WFNode.py" />
+    <Compile Include="miam\workflow\WFProcess.py" />
+    <Compile Include="miam\workflow\WFWorkflow.py" />
+    <Compile Include="miam\workflow\__init__.py" />
     <Compile Include="miam\__init__.py">
       <SubType>Code</SubType>
     </Compile>
     <Compile Include="_miamG.py" />
   </ItemGroup>
   <ItemGroup>
+    <Folder Include="gui\" />
+    <Folder Include="gui\guiController\" />
+    <Folder Include="gui\guiController\__pycache__\" />
+    <Folder Include="gui\guiModel\" />
+    <Folder Include="gui\guiModel\__pycache__\" />
+    <Folder Include="gui\guiView\" />
+    <Folder Include="gui\guiView\__pycache__\" />
+    <Folder Include="gui\__pycache__\" />
     <Folder Include="miam\" />
+    <Folder Include="miam\aesthetics\" />
+    <Folder Include="miam\aesthetics\__pycache__\" />
+    <Folder Include="miam\classification\" />
+    <Folder Include="miam\classification\__pycache__\" />
+    <Folder Include="miam\histogram\" />
+    <Folder Include="miam\histogram\__pycache__\" />
+    <Folder Include="miam\html\" />
+    <Folder Include="miam\html\__pycache__\" />
+    <Folder Include="miam\imageDB\" />
+    <Folder Include="miam\imageDB\__pycache__\" />
     <Folder Include="miam\image\" />
+    <Folder Include="miam\math\" />
+    <Folder Include="miam\math\__pycache__\" />
+    <Folder Include="miam\pointcloud\" />
+    <Folder Include="miam\pointcloud\__pycache__\" />
+    <Folder Include="miam\processing\" />
+    <Folder Include="miam\processing\__pycache__\" />
+    <Folder Include="miam\workflow\" />
+    <Folder Include="miam\workflow\__pycache__\" />
+  </ItemGroup>
+  <ItemGroup>
+    <Content Include="gui\guiController\__pycache__\MainWindowController.cpython-37.pyc" />
+    <Content Include="gui\guiController\__pycache__\__init__.cpython-37.pyc" />
+    <Content Include="gui\guiModel\__pycache__\MainWindowModel.cpython-37.pyc" />
+    <Content Include="gui\guiModel\__pycache__\__init__.cpython-37.pyc" />
+    <Content Include="gui\guiView\__pycache__\FigureWidget.cpython-37.pyc" />
+    <Content Include="gui\guiView\__pycache__\ImageWidget.cpython-37.pyc" />
+    <Content Include="gui\guiView\__pycache__\MainWindow.cpython-37.pyc" />
+    <Content Include="gui\guiView\__pycache__\__init__.cpython-37.pyc" />
+    <Content Include="gui\__pycache__\__init__.cpython-37.pyc" />
+    <Content Include="miam\aesthetics\__pycache__\Composition.cpython-37.pyc" />
+    <Content Include="miam\aesthetics\__pycache__\LightnessAesthetics.cpython-37.pyc" />
+    <Content Include="miam\aesthetics\__pycache__\Palette.cpython-37.pyc" />
+    <Content Include="miam\aesthetics\__pycache__\__init__.cpython-37.pyc" />
+    <Content Include="miam\classification\__pycache__\colorPaletteKmeanDisplay.cpython-37.pyc" />
+    <Content Include="miam\classification\__pycache__\KmeanDisplay.cpython-37.pyc" />
+    <Content Include="miam\classification\__pycache__\kmeans.cpython-37.pyc" />
+    <Content Include="miam\classification\__pycache__\__init__.cpython-37.pyc" />
+    <Content Include="miam\histogram\__pycache__\Histogram.cpython-37.pyc" />
+    <Content Include="miam\histogram\__pycache__\__init__.cpython-37.pyc" />
+    <Content Include="miam\html\__pycache__\generator.cpython-37.pyc" />
+    <Content Include="miam\html\__pycache__\__init__.cpython-37.pyc" />
+    <Content Include="miam\imageDB\__pycache__\Builder.cpython-37.pyc" />
+    <Content Include="miam\imageDB\__pycache__\Checker.cpython-37.pyc" />
+    <Content Include="miam\imageDB\__pycache__\HwHDRBuilder.cpython-37.pyc" />
+    <Content Include="miam\imageDB\__pycache__\ImageDB.cpython-37.pyc" />
+    <Content Include="miam\imageDB\__pycache__\ImageDB_HDD.cpython-37.pyc" />
+    <Content Include="miam\imageDB\__pycache__\POGChecker.cpython-37.pyc" />
+    <Content Include="miam\imageDB\__pycache__\utils.cpython-37.pyc" />
+    <Content Include="miam\imageDB\__pycache__\__init__.cpython-37.pyc" />
+    <Content Include="miam\math\__pycache__\Distance.cpython-37.pyc" />
+    <Content Include="miam\math\__pycache__\Normalize.cpython-37.pyc" />
+    <Content Include="miam\math\__pycache__\__init__.cpython-37.pyc" />
+    <Content Include="miam\pointcloud\__pycache__\PointCloud2D.cpython-37.pyc" />
+    <Content Include="miam\pointcloud\__pycache__\__init__.cpython-37.pyc" />
+    <Content Include="miam\processing\__pycache__\Blend.cpython-37.pyc" />
+    <Content Include="miam\processing\__pycache__\ColorSpaceTransform.cpython-37.pyc" />
+    <Content Include="miam\processing\__pycache__\ContrastControl.cpython-37.pyc" />
+    <Content Include="miam\processing\__pycache__\Duplicate.cpython-37.pyc" />
+    <Content Include="miam\processing\__pycache__\ExposureControl.cpython-37.pyc" />
+    <Content Include="miam\processing\__pycache__\Fuse.cpython-37.pyc" />
+    <Content Include="miam\processing\__pycache__\GaussianFilter.cpython-37.pyc" />
+    <Content Include="miam\processing\__pycache__\LaplaceFilter.cpython-37.pyc" />
+    <Content Include="miam\processing\__pycache__\MaskSegmentPercentile.cpython-37.pyc" />
+    <Content Include="miam\processing\__pycache__\NoOp.cpython-37.pyc" />
+    <Content Include="miam\processing\__pycache__\Processing.cpython-37.pyc" />
+    <Content Include="miam\processing\__pycache__\SumSquaredLaplace.cpython-37.pyc" />
+    <Content Include="miam\processing\__pycache__\TMO.cpython-37.pyc" />
+    <Content Include="miam\processing\__pycache__\TMO_CCTF.cpython-37.pyc" />
+    <Content Include="miam\processing\__pycache__\TMO_Lightness.cpython-37.pyc" />
+    <Content Include="miam\processing\__pycache__\TMO_Linear.cpython-37.pyc" />
+    <Content Include="miam\processing\__pycache__\Ymap.cpython-37.pyc" />
+    <Content Include="miam\processing\__pycache__\__init__.cpython-37.pyc" />
+    <Content Include="miam\workflow\__pycache__\WFConnector.cpython-37.pyc" />
+    <Content Include="miam\workflow\__pycache__\WFNode.cpython-37.pyc" />
+    <Content Include="miam\workflow\__pycache__\WFProcess.cpython-37.pyc" />
+    <Content Include="miam\workflow\__pycache__\WFWorkflow.cpython-37.pyc" />
+    <Content Include="miam\workflow\__pycache__\__init__.cpython-37.pyc" />
   </ItemGroup>
   <Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\Python Tools\Microsoft.PythonTools.targets" />
   <!-- Uncomment the CoreCompile target to enable the Build command in

+ 1 - 0
_miamG/gui/__init__.py

@@ -0,0 +1 @@
+

+ 126 - 0
_miamG/gui/guiController/MainWindowController.py

@@ -0,0 +1,126 @@
+# import
+import enum, sys
+
+# pyQT5 import
+#from PyQt5.QtWidgets import QWidget, QLabel, QFileDialog 
+from PyQt5.QtWidgets import QFileDialog, QApplication
+# gui import
+import gui.guiView.MainWindow as MWView
+import gui.guiModel.MainWindowModel as MWModel
+
+# ------------------------------------------------------------------------------------------
+# MIAM project 2020
+# ------------------------------------------------------------------------------------------
+# author: remi.cozot@univ-littoral.fr
+# ------------------------------------------------------------------------------------------
+
+# local function
+def getScreenSize(app):
+    screens = app.screens()
+    res = list(map(lambda x: x.size(), screens))
+    # debug
+    print(res)
+    return res
+
+# local class
+class mainWidgetDisplayMode(enum.Enum):
+    """ Enum  """
+
+    IMG         = 0         # single display image
+    HIST        = 1         # single display image
+    IMGnHIST    = 2         # dual display image and its histogram
+
+
+class MainWindowController(object):
+    """controller for MainWindow"""
+
+    def __init__(self, app):
+
+        # get screens size
+        self.screenSize = getScreenSize(app)
+
+        # attributes
+        self.displayMode = mainWidgetDisplayMode.IMGnHIST # default display mode
+        self.dispHDR_linear = True
+
+        # build vue
+        self.view =  MWView.MainWindow(self)
+        self.view.show()
+
+        # build model
+        self.model = MWModel.MainWindowModel(self)
+
+    # utils methods
+    def statusMessage(self, s): self.view.statusBar().showMessage(s)
+
+    # callBack methods
+    # --------------------------------------------------------------
+
+    # menu: file
+
+    # open image file
+    def openImage(self):
+        # status message
+        self.statusMessage('open image file ...')
+        
+        # open file dialog
+        fname = QFileDialog.getOpenFileName(None, 'Open file', '../images/')[0]
+        self.statusMessage('open image file:'+fname)
+
+        # ask model to load image
+        imgs = self.model.readImage(fname)
+        self.view.setImage(imgs)
+
+    # open workflow
+    def openWorkflow(self):
+        # status message
+        self.statusMessage('load workflow file ...')
+        
+        # open file dialog
+        fname = QFileDialog.getOpenFileName(None, 'Open file', '../workflows/')[0]
+        self.statusMessage('load workflow file:'+fname)
+
+        # ask model to load workflow
+        self.model.readWorkflow(fname)
+
+    # menu: workflow
+
+    # compute
+    def compute(self):
+        # status message
+        self.statusMessage('compute ...')
+        imgs = self.model.compute()
+        self.statusMessage('compute ...'+ str(len(imgs))+' images have benn computed')
+
+        if len(imgs)>0: self.view.setImage(imgs)
+
+    # menu: display
+
+    # image only
+    def displayIMG(self):
+        if self.displayMode == mainWidgetDisplayMode.IMGnHIST:
+            self.statusMessage('display set to histogram ...')
+            self.displayMode = mainWidgetDisplayMode.HIST
+            self.view.setDislaytoHIST()
+        elif self.displayMode == mainWidgetDisplayMode.HIST:
+            self.statusMessage('display set to image and histogram ...')
+            self.displayMode = mainWidgetDisplayMode.IMGnHIST
+            self.view.setDislaytoIMGnHIST()
+        elif self.displayMode == mainWidgetDisplayMode.IMG:
+            self.statusMessage('...')
+            pass
+ 
+    # hist only
+    def displayHIST(self):
+        if self.displayMode == mainWidgetDisplayMode.IMGnHIST:
+            self.statusMessage('display set to image ...')
+            self.displayMode = mainWidgetDisplayMode.IMG
+            self.view.setDislaytoIMG()
+        elif self.displayMode == mainWidgetDisplayMode.HIST:
+            self.statusMessage('...')
+        elif self.displayMode == mainWidgetDisplayMode.IMG:
+            self.statusMessage('display set to image and histogram ...')
+            self.displayMode = mainWidgetDisplayMode.IMGnHIST
+            self.view.setDislaytoIMGnHIST()
+            pass
+

+ 1 - 0
_miamG/gui/guiController/__init__.py

@@ -0,0 +1 @@
+

+ 44 - 0
_miamG/gui/guiModel/ImageModel.py

@@ -0,0 +1,44 @@
+# miam import
+import miam.image.Image as MIMG
+import miam.image.imageType as MTYPE
+
+class ImageModel(object):
+    """description of class"""
+
+    def __init__(self, controller):
+        # reference to image controller
+        self.controller = controller
+
+        # image gui model
+
+        # a list of miam.image.Image
+        # each new process give a new image append to the list self.image
+        # the image displayed is self.currentImage
+        self.currentImage =None # index of current image
+        self.image = []
+        # list of 'process'
+        # version 0.1: simple dict list
+        self.history = []
+        pass
+
+    # methods called by controller
+    def readImage(self,filename):
+        # read image
+        self.image.append(MIMG.Image.readImage(filename))
+        self.history.append({'readImage':filename})
+        # default processing for HDR iamge
+        if self.image[-1].type == MTYPE.imageType.HDR:
+            self.image.append(self.image[-1].removeZeros(0.5))
+            self.history.append({'removeZeros':'0.5'})
+
+        self.currentImage = len(self.image) -1
+
+    def getColorData(self):
+        return self.image[self.currentImage].colorData
+
+
+    def resize(self,size,anti_aliasing=True):
+        #resize(self,size=(None,None),anti_aliasing=False)
+        pass
+
+

+ 84 - 0
_miamG/gui/guiModel/MainWindowModel.py

@@ -0,0 +1,84 @@
+# Qt import
+
+
+# import
+import numpy as np
+
+# miam import
+import miam.image.Image as MIMG
+import miam.image.imageType as MTYPE
+import miam.workflow.WFWorkflow as MWF
+
+class MainWindowModel(object):
+    """
+    class for main window model
+    """
+
+    def __init__(self, controller):
+
+        # attributes
+        # ----------------------------------------
+
+        # reference to MainWindowController
+        self.controller = controller
+
+        # input image (miam.image.Image)
+        # only change when load called
+        self.inputImage =  None 
+
+        # list of processes
+        self.workflow = None
+
+        # smaller images to faster computation display
+        self.image = None
+
+    # method(s)
+    # ------------------------------------------------------------------------
+    def readImage(self, filename):
+
+        # read image
+        # store input : no processing applyed 
+        self.inputImage = MIMG.Image.readImage(filename)
+
+        # reset self.image list and self.process
+        self.image = None
+
+        # compute smaller image 4 interactive
+        width = self.controller.screenSize[0].width()
+        self.image = self.inputImage.resize((width,None))
+         
+        # default processing for HDR iamge
+        if self.inputImage.type == MTYPE.imageType.HDR:
+            self.image = self.image.removeZeros(0.5)
+
+        return [self.image]
+
+    def readWorkflow(self, filename):
+        # read workflow
+        # note: read include compile
+        self.workflow = MWF.WFWorkflow.readWorkflow(filename)
+
+    # ------------------------------------------------------------------------
+    def compute(self):
+        # return list of image
+        resImgs = []
+
+        if self.workflow:
+            # check if input image
+            if self.inputImage:
+                # compute with self.image for faster computation
+                self.workflow.compute(self.image)
+                for leaf in self.workflow.leafs:
+                    resImgs.append(leaf.image)
+        
+            else: # no input image
+                self.controller.statusMessage("compute: no input image ! Open image first ...")
+        else : # no workflow
+            self.controller.statusMessage("compute: no workflow ! Load workflow first ...")
+
+        return resImgs
+    # ------------------------------------------------------------------------
+
+
+
+

+ 1 - 0
_miamG/gui/guiModel/__init__.py

@@ -0,0 +1 @@
+

+ 29 - 0
_miamG/gui/guiView/FigureWidget.py

@@ -0,0 +1,29 @@
+# import
+# ------------------------------------------------------------------------------------------
+
+
+# import Qt
+from PyQt5 import QtCore, QtWidgets
+
+
+# QT matplotlib
+from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
+from matplotlib.figure import Figure
+
+
+
+class FigureWidget(FigureCanvas):
+    """
+        description of class
+    """
+    def __init__(self, parent=None, width=5, height=5, dpi=100):
+        # create Figure
+        fig = Figure(figsize=(width, height), dpi=dpi)
+        self.axes = fig.add_subplot(111)
+
+        # explicite call of super controller
+        FigureCanvas.__init__(self, fig)
+        self.setParent(parent)
+
+        FigureCanvas.updateGeometry(self)
+

+ 44 - 0
_miamG/gui/guiView/ImageWidget.py

@@ -0,0 +1,44 @@
+# Qt import
+from PyQt5.QtWidgets import QWidget, QLabel
+from PyQt5.QtGui import QPixmap, QImage
+from PyQt5.QtCore import Qt
+
+# import
+import numpy as np
+
+# miam import
+import miam.image.Image as MIMG
+
+class ImageWidget(QWidget):
+    """description of class"""
+    def __init__(self,image):
+        super().__init__()
+
+        # create a QtLabel for pixmap
+        self.label = QLabel(self)
+
+        # image content
+        self.image = None
+        self.set(image)
+
+    def resize(self):
+        self.label.resize(self.size())
+        self.label.setPixmap(self.imagePixmap.scaled(self.size(),Qt.KeepAspectRatio))
+
+    def set(self,image):
+        # read image
+        self.image = image
+
+        # compute pixmap
+        height, width, channel = self.image.shape
+        bytesPerLine = channel * width
+        
+        # mask
+        colorData =  self.image.colorData*self.image.mask[...,np.newaxis] if self.image.hasMask() else self.image.colorData
+
+        # QImage
+        qImg = QImage((colorData*255).astype(np.uint8), width, height, bytesPerLine, QImage.Format_RGB888)
+        self.imagePixmap = QPixmap.fromImage(qImg)
+
+        self.resize()
+

+ 267 - 0
_miamG/gui/guiView/MainWindow.py

@@ -0,0 +1,267 @@
+# import
+# ------------------------------------------------------------------------------------------
+import os, sys, math
+import multiprocessing as mp
+import matplotlib
+import numpy as np
+import easygui
+import colour
+
+# import Qt
+from PyQt5.QtWidgets import QMainWindow, QAction, QApplication, QMenu
+from PyQt5.QtWidgets import QWidget, QLabel, QFileDialog 
+from PyQt5.QtWidgets import QHBoxLayout # QSlider
+from PyQt5.QtGui import QIcon, QPixmap, QImage
+from PyQt5 import QtCore, QtWidgets
+
+# QT matplotlib
+
+# miam import
+import miam.image.Image as MIMG
+import miam.histogram.Histogram as MHIST
+import miam.image.channel
+import miam.utils
+
+# gui import
+import gui.guiController.MainWindowController as gMWC
+import gui.guiView.ImageWidget as gIW
+import gui.guiView.FigureWidget as gFW
+
+# ------------------------------------------------------------------------------------------
+# MIAM project 2020
+# ------------------------------------------------------------------------------------------
+# author: remi.cozot@univ-littoral.fr
+# ------------------------------------------------------------------------------------------
+
+
+class MainWindow(QMainWindow):
+    """ 
+        MainWindow(Vue)
+    """
+    
+    # version 0.1
+    # features:
+    # load image
+    # switch between display mode
+    #     
+    def __init__(self, controller = None):
+        super().__init__()
+
+        # attributes
+        self.controller = controller
+
+        # build menu
+        self.buildFileMenu()
+        self.buildDisplayMenu()
+        self.buildWorkflowMenu()
+
+        # setGeometry
+        self.setWindowGeometry()
+        # title
+        self.setWindowTitle('MIAM - Rémi Cozot (c) 2020')   
+        # status bar
+        self.statusBar().showMessage('Welcome to MIAM: Multidimensional Image Aesthetics Model!')
+
+        # centralWidgets
+        self.imageWidgets = []
+
+        self.layout = QHBoxLayout()
+        self.container = QWidget()
+
+        #self.setCentralWidget(self.container)
+
+    # methods
+    def resizeEvent(self, event):
+        if self.controller.displayMode != gMWC.mainWidgetDisplayMode.HIST :
+            for cw in self.imageWidgets:
+                cw.resize()
+        pass
+
+    # build menu
+    # ---------------------------------------------------------
+    def buildFileMenu(self):
+        """ 
+           XXX XXX XXX XXX 
+        """
+        # get menubar
+        menubar = self.menuBar()
+
+        # file menu
+        fileMenu = menubar.addMenu('&File')
+
+        # Create Open  image action
+        openAction = QAction('&Open image', self)        
+        openAction.setShortcut('Ctrl+N')
+        openAction.setStatusTip('Open image')
+        openAction.triggered.connect(self.controller.openImage)
+        fileMenu.addAction(openAction)
+
+        fileMenu.addSeparator()
+
+        # Create Open workflow action
+        openWFAction = QAction('&Load workflow', self)        
+        openWFAction.setShortcut('Ctrl+W')
+        openWFAction.setStatusTip('Load worflow')
+        openWFAction.triggered.connect(self.controller.openWorkflow)
+        fileMenu.addAction(openWFAction)
+
+    def buildDisplayMenu(self):
+        """ 
+           XXX XXX XXX XXX 
+        """
+        # get menubar
+        menubar = self.menuBar()
+
+        # file menu
+        displayMenu = menubar.addMenu('&Display')
+
+        # Create Display Input action
+        displayInputAction = QAction('&Image', self)        
+        displayInputAction.setShortcut('Ctrl+I')
+        displayInputAction.setStatusTip('Display image on/off')
+        displayInputAction.triggered.connect(self.controller.displayIMG)
+        displayMenu.addAction(displayInputAction)
+
+        # Create Display Current action
+        displayCurrentAction = QAction('&Histogram', self)        
+        displayCurrentAction.setShortcut('Ctrl+H')
+        displayCurrentAction.setStatusTip('Display histogram on/off')
+        displayCurrentAction.triggered.connect(self.controller.displayHIST)
+        displayMenu.addAction(displayCurrentAction)   
+
+    def buildWorkflowMenu(self):
+        """ 
+           XXX XXX XXX XXX 
+        """
+        # get menubar
+        menubar = self.menuBar()
+
+        # file menu
+        workflowMenu = menubar.addMenu('&Workflow')
+
+        # Create compute workflow action
+        computeAction = QAction('&Compute', self)        
+        computeAction.setShortcut('Ctrl+C')
+        computeAction.setStatusTip('Compute workflow')
+        computeAction.triggered.connect(self.controller.compute)
+        workflowMenu.addAction(computeAction)
+        
+
+    # setWindowGeometry
+    def setWindowGeometry(self, scale=0.8):
+
+        width = self.controller.screenSize[0].width()
+        height = self.controller.screenSize[0].height()
+
+        # geometry
+        self.setGeometry(0, 0, math.floor(width*scale), math.floor(height*scale))
+
+    # setImage
+    def setImage(self, imgs):
+        # get display mode from controller
+        self.controller.dispHDR_linear      # display mode for HDR linear | cctf  
+
+        # reset
+        self.images = imgs
+        self.imageWidgets = []
+        self.layout = QtWidgets.QHBoxLayout()
+        self.container = QtWidgets.QWidget()
+
+        for img in self.images:
+            # create an imageWidget
+            imgW = gIW.ImageWidget(img)
+            self.imageWidgets.append(imgW)
+
+            # add ImageWidget
+            self.layout.addWidget(imgW)
+
+            if self.controller.displayMode == gMWC.mainWidgetDisplayMode.IMGnHIST:
+                # create histogram
+                if img.isHDR():
+                    ch = miam.image.channel.channel.Y
+                else:
+                    ch = miam.image.channel.channel.L
+                imgHist = MHIST.Histogram.build(img,ch)
+
+                histW = gFW.FigureWidget()
+                imgHist.plot(histW.axes)
+                self.layout.addWidget(histW)
+
+        self.container.setLayout(self.layout)
+        self.setCentralWidget(self.container)
+
+    # setDislaytoIMG
+    def setDislaytoIMG(self):
+        # reset
+        self.layout = QtWidgets.QHBoxLayout()
+        self.container = QtWidgets.QWidget()
+        self.imageWidgets =[]
+
+        for img in self.images:
+            # create an imageWidget
+            imgW = gIW.ImageWidget(img)
+            self.imageWidgets.append(imgW)
+            self.layout.addWidget(imgW)
+
+        self.container.setLayout(self.layout)
+        self.setCentralWidget(self.container)
+
+    # setDislaytoIMGnHIST
+    def setDislaytoIMGnHIST(self):
+        # reset
+        self.layout = QtWidgets.QHBoxLayout()
+        self.container = QtWidgets.QWidget()
+        self.imageWidgets =[]
+
+        for img in self.images:
+            # create an imageWidget
+            imgW = gIW.ImageWidget(img)
+            self.imageWidgets.append(imgW)
+    
+            # add ImageWidget
+            self.layout.addWidget(imgW)
+
+            # create histogram
+            if imgW.image.isHDR():
+                ch = miam.image.channel.channel.Y
+            else:
+                ch = miam.image.channel.channel.L
+            imgHist = MHIST.Histogram.build(imgW.image,ch)
+
+            histW = gFW.FigureWidget()
+            imgHist.plot(histW.axes)
+            self.layout.addWidget(histW)
+
+        self.container.setLayout(self.layout)
+        self.setCentralWidget(self.container)
+
+    # setDislaytoIMGnHIST
+    def setDislaytoHIST(self):
+        # reset
+        self.layout = QtWidgets.QHBoxLayout()
+        self.container = QtWidgets.QWidget()
+        self.imageWidgets =[]
+
+        for img in self.images:
+            # create an imageWidget
+            imgW = gIW.ImageWidget(img)
+            self.imageWidgets.append(imgW)
+
+        for imgW in self.imageWidgets:
+            # create histogram
+            if imgW.image.isHDR():
+                ch = miam.image.channel.channel.Y
+            else:
+                ch = miam.image.channel.channel.L
+            imgHist = MHIST.Histogram.build(imgW.image,ch)
+
+            histW = gFW.FigureWidget()
+            imgHist.plot(histW.axes)
+            self.layout.addWidget(histW)
+
+        self.container.setLayout(self.layout)
+        self.setCentralWidget(self.container)
+
+
+
+

+ 1 - 0
_miamG/gui/guiView/__init__.py

@@ -0,0 +1 @@
+

+ 1 - 0
_miamG/miam/__init__.py

@@ -1 +1,2 @@
+pltcolor = ['r','g','b','c', 'm', 'y', 'k']
 

+ 92 - 0
_miamG/miam/aesthetics/Composition.py

@@ -0,0 +1,92 @@
+# import
+# ------------------------------------------------------------------------------------------
+import copy
+import numpy as np
+# local
+import miam.utils
+import miam.image.Image as MIMG
+import miam.processing.SumSquaredLaplace as MSSL
+import miam.pointcloud.PointCloud2D as MPC2D
+
+# ------------------------------------------------------------------------------------------
+# MIAM project 2020
+# ------------------------------------------------------------------------------------------
+# author: remi.cozot@univ-littoral.fr
+# ------------------------------------------------------------------------------------------
+class Composition(object):
+    """ 
+    class Composition:
+        attribute(s):
+            name:       object name
+            nbPoints:   number of point in composition
+            points:     list points[(x,y)]*nbPoints
+    """    
+    # constructor
+    def __init__(self, name, points,imgShape):
+        self.name: name                         # name (str)
+        self.points = points                    # list of tunple [(x,y)] * npPoints in [0..1] range
+        self.nbPoints = len(self.points)        # number of Points
+        self.shape = imgShape                # shape of image from which it is
+
+    # build
+    def build(image, nbPoint=10, nbZone=9, **kwargs):
+        # taking into account additional parameters
+        if not kwargs: kwargs = {'preGaussian': True, 'postScaling': True}  # default value
+        preGaussian, postScaling = kwargs['preGaussian'], kwargs['postScaling']
+
+        # compute Sum of Squared Laplace map
+        sslMap = image.process(MSSL.SumSquaredLaplace(),
+                               nbZone= nbZone, 
+                               preGaussian = preGaussian, 
+                               postScaling= postScaling)
+
+        # extract maximum focus (ie max sum squared Laplacien)
+        points = []
+        sslArray = np.argsort(sslMap.colorData.flatten())
+        for i in range(-1,-(nbPoint+1),-1):
+            p = np.divmod(sslArray[i],nbZone)
+            x,y = (p[1]+0.5)/nbZone,  (p[0]+0.5)/nbZone
+            points.append((x,y))
+
+        (path,name, zxt) = miam.utils.splitFileName(image.name)
+        return Composition(name+"(cpmposition)", points, image.shape[0:2])
+
+    # plot
+    def plot(self,ax, marker = None,shortName=True, title=True, keepRawPoints = False):
+        marker = 'go-' if not marker else marker
+        # to image space
+        W, H =self.shape[1], self.shape[0]
+        pImageSpace = []
+        for p in self.points:
+                xIS, yIS = p
+                xIS = xIS*W
+                yIS = yIS*H
+                pImageSpace.append((xIS,yIS))
+        # to X, Y array
+        X, Y, XRAW, YRAW = [],[], [], []
+        for p in pImageSpace:
+            x,y = p
+            X.append(x)
+            Y.append(y)
+        XRAW = copy.deepcopy(X)
+        YRAW = copy.deepcopy(Y)
+
+        if self.nbPoints == 3: # display triangle
+            X.append(X[0])
+            Y.append(Y[0])
+            keepRawPoints = False
+
+        else: # display convex Hull
+            pc = MPC2D.PointCloud2D(X,Y)
+            X,Y = MPC2D.PointCloud2D.toXYarray(pc.convexHull())
+
+        if keepRawPoints : ax.plot(XRAW,YRAW,'ro')
+        ax.plot(X,Y,marker)
+        ax.set_xlim(0,W)
+        ax.set_ylim(H,0)
+        name = self.name if not shortName else "(comp.)"
+        if title: ax.set_title(name)
+
+        ax.axis("on")
+
+

+ 109 - 0
_miamG/miam/aesthetics/LightnessAesthetics.py

@@ -0,0 +1,109 @@
+# import
+# ------------------------------------------------------------------------------------------
+from .. import image
+import miam
+import copy
+import numpy as np
+import matplotlib.pyplot as plt
+# miam import
+import miam.image.Image as MIMG
+import miam.processing.ColorSpaceTransform as MCST
+import miam.histogram.Histogram  as MHIST
+import miam.aesthetics.LightnessAesthetics as MLAC
+# ------------------------------------------------------------------------------------------
+# MIAM project 2020
+# ------------------------------------------------------------------------------------------
+# author: remi.cozot@univ-littoral.fr
+# ------------------------------------------------------------------------------------------
+class LightnessAesthetics(object):
+    """description of class"""
+    def __init__(self, centroids, **kwargs):
+        self.lightnessClasses =  centroids
+
+        self.nbClass = len(self.lightnessClasses)
+        self.nbBin = self.lightnessClasses[0].histValue.shape[0]
+
+    def plot(self,ax):
+        for j,aestheticsClass in enumerate(self.lightnessClasses):
+            sum, sumB= 0.0,0.0
+            # compute mean
+            for lightnessValue, binValue in enumerate(aestheticsClass.histValue):
+                sumB = sumB + binValue
+                sum= sum + binValue*aestheticsClass.edgeValue[lightnessValue+1]
+            ax.plot(aestheticsClass.edgeValue[1:],aestheticsClass.histValue,miam.pltcolor[j])
+            ax.plot(sum/sumB,0,miam.pltcolor[j]+'o')
+
+    def computeImageClass(self, image):
+        # if required transform in Lab
+        # note lightness class are in Lab
+        image = MCST.ColorSpaceTransform().compute(image,dest='Lab')
+        # compute lightness histogram
+        hist = MHIST.Histogram.build(image,MIMG.channel.channel.L,nbBins=self.nbBin)
+        # compute distance with classes
+        distance = []
+        for i, aestheticsSignature in enumerate(self.lightnessClasses) :
+            dist = MHIST.Histogram.computeDistance(aestheticsSignature,hist)
+            distance.append(dist)
+        return distance
+
+    def projectImage(self,image,nbClass='all'):
+        distance = self.computeImageClass(image)
+        hLab = MHIST.Histogram.build(MCST.ColorSpaceTransform().compute(image,dest='Lab'),MIMG.channel.channel.L,nbBins=self.nbBin)
+        res = copy.deepcopy(hLab)
+        res.histValue = np.zeros(res.histValue.shape)
+        if not nbClass: nbClass ="all"
+        if isinstance(nbClass, str):
+            if nbClass == "all":
+                for i, aestheticsSignature in enumerate(self.lightnessClasses) :
+                    res = MHIST.Histogram.add(res, MHIST.Histogram.scale((1.0 - distance[i]),aestheticsSignature))
+            elif nbClass =="first":
+                idxSort = np.argsort(np.asarray(distance))
+                res = MHIST.Histogram.scale((1.0 - distance[idxSort[0]]), self.lightnessClasses[idxSort[0]])
+        if isinstance(nbClass,range):
+            idxSort = np.argsort(np.asarray(distance))
+            for i in nbClass:
+                res = MHIST.Histogram.add(res,MHIST.Histogram.scale((1.0 - distance[idxSort[i]]), self.lightnessClasses[idxSort[i]]))
+        return res.normalise(norm='dot')
+
+    def readLightnessClasses(fileName, className =  None):
+        """ form np.load recover histogram of lightness classes"""
+        if not className: className=['black','shadow','medium','highlight','white']
+
+        # from np.histogram to Histograms class
+        centroids = np.load(fileName)
+
+        nbClass, nbBin =centroids.shape
+        edges = np.linspace(0,100,nbBin+1)
+
+        # compute min to set name
+        mean= []
+        for j,c in enumerate(centroids):
+            sum, sumB= 0.0,0.0
+            # compute mean
+            for i, binValue in enumerate(c):
+                sumB = sumB + binValue
+                sum= sum + binValue*edges[i+1]
+            mean.append(sum/sumB)
+        
+        # sortIndex mean
+        sortedIndex = np.argsort(mean)
+
+        # build Histogram object
+        lightnessClasses = []
+        for i, j in enumerate(sortedIndex):
+            name = className[i]
+            histValue = centroids[j]
+            edgeValue = copy.deepcopy(edges)
+            channel = MIMG.channel.channel.L
+            colorSpace = MIMG.ColorSpace.ColorSpace.buildLab()
+            log       = False
+
+            # build a histogram
+            lightnessClasses.append(MHIST.Histogram(histValue, edgeValue, name,channel,logSpace = log))
+
+        return LightnessAesthetics(lightnessClasses)
+
+
+
+
+

+ 169 - 0
_miamG/miam/aesthetics/Palette.py

@@ -0,0 +1,169 @@
+# import
+# ------------------------------------------------------------------------------------------
+import os, colour, sklearn.cluster, skimage.color, copy
+import numpy as np
+# local
+import miam.image.Image as MIMG
+import miam.processing.ColorSpaceTransform as MCST
+import miam.image.ColorSpace as MICS
+import miam.math.Distance as MDST
+# ------------------------------------------------------------------------------------------
+# MIAM project 2020
+# ------------------------------------------------------------------------------------------
+# author: remi.cozot@univ-littoral.fr
+# ------------------------------------------------------------------------------------------
+class Palette(object):
+    """ 
+    class Palette:
+        attribute(s):
+            name:       object name
+            colorSpace: colorspace (colour.models.RGB_COLOURSPACES, Lab, etc.)
+            nbColors:   number of colors in the Palette
+            colorData:  array of pixels (np.ndarray)
+            colors:     np array colors[0:nbColors,0:2]
+                        sorted according to distance to black (in the palette colorSpace)
+    """    
+    # constructor
+    def __init__(self, name, colors, colorSpace):
+        self.name       = name
+        self.colorSpace = colorSpace
+        self.nbColors   = colors.shape[0]
+        self.colors     = np.asarray(sorted(colors.tolist(), key = lambda u  : np.sqrt(np.dot(u,u))))
+        # DEBUG
+        # print("Palette.__init__():", self.name)
+
+    # methods
+
+    def build(image, nbColors, fast=True, method='kmean-Lab', **kwargs):
+        # according to method
+        if method == 'kmean-Lab':
+            # with 'remove black or not'
+            if 'removeBlack' in kwargs: removeBlack = kwargs['removeBlack']
+            else:
+                print('removeBlack set to True')
+                removeBlack = True
+            # to Lab then to Vector
+            if fast: image = image.smartResize()
+            imageLab = MCST.ColorSpaceTransform().compute(image,dest='Lab')
+            imgLabDataVector = MIMG.Image.array2vector(imageLab.colorData)
+
+            if removeBlack:
+                # k-means: nb cluster = nbColors + 1
+                kmeans_cluster_Lab = sklearn.cluster.KMeans(n_clusters=nbColors+1)
+                kmeans_cluster_Lab.fit(imgLabDataVector)
+
+                cluster_centers_Lab = kmeans_cluster_Lab.cluster_centers_
+                
+                # remove darkness one
+                idxLmin = np.argmin(cluster_centers_Lab[:,0])                           # idx of darkness
+                cluster_centers_Lab = np.delete(cluster_centers_Lab, idxLmin, axis=0)   # remove min from cluster_centers_Lab
+
+            else:
+                # k-means: nb cluster = nbColors
+                kmeans_cluster_Lab = sklearn.cluster.KMeans(n_clusters=nbColors)
+                kmeans_cluster_Lab.fit(imgLabDataVector)
+                cluster_centers_Lab = kmeans_cluster_Lab.cluster_centers_
+
+            colors = cluster_centers_Lab
+        else:
+            print('unknow palette method')
+            colors = None
+
+        return Palette('Palette_'+image.name,colors, MICS.ColorSpace.buildLab())
+
+    def createImageOfPalette(self, colorWidth=100):
+        if self.colorSpace.name =='Lab':
+           cRGB = MCST.Lab_to_sRGB(self.colors, apply_cctf_encoding=True)
+        elif self.colorSpace.name=='sRGB':
+            cRGB = self.colors
+        width = colorWidth*cRGB.shape[0]
+        height=colorWidth
+        # return image
+        img = np.ones((height,width,3))
+
+        for i in range(cRGB.shape[0]):
+            xMin= i*colorWidth
+            xMax= xMin+colorWidth
+            yMin=0
+            yMax= colorWidth
+            img[yMin:yMax, xMin:xMax,0]=cRGB[i,0]
+            img[yMin:yMax, xMin:xMax,1]=cRGB[i,1]
+            img[yMin:yMax, xMin:xMax,2]=cRGB[i,2]
+        # colorData, name, type, linear, colorspace, scalingFactor
+        # DEBUG
+        # print("createImageOfPalette:", self.name)
+        return MIMG.Image(img, self.name, MIMG.imageType.imageType.SDR, False, MICS.ColorSpace.buildsRGB(),1.0)
+
+    # create image of multiple palettes
+    def createImageOfPalettes(palettes, colorWidth=100):
+        # return image
+        width = colorWidth*palettes[0].colors.shape[0]
+        height=colorWidth*len(palettes)
+        img = np.ones((height,width,3))
+
+        for j,palette in enumerate(palettes):
+            if palette.colorSpace.name =='Lab':
+                cRGB = MCST.Lab_to_sRGB(palette.colors, apply_cctf_encoding=True)
+            elif palette.colorSpace.name=='sRGB':
+                cRGB = palette.colors
+
+            for i in range(cRGB.shape[0]):
+                xMin= i*colorWidth
+                xMax= xMin+colorWidth
+                yMin= j*colorWidth
+                yMax= yMin+colorWidth
+                img[yMin:yMax, xMin:xMax,0]=cRGB[i,0]
+                img[yMin:yMax, xMin:xMax,1]=cRGB[i,1]
+                img[yMin:yMax, xMin:xMax,2]=cRGB[i,2]
+
+        return MIMG.Image(img, "palettes", MIMG.imageType.imageType.SDR, False, MICS.ColorSpace.buildsRGB(),1.0)
+
+    # magic operators
+
+    def __add__(self, other):
+        # print("DEBUG[Palette.__add__(",self.name,",",other.name,") ]")
+
+        # create a copy
+        res = copy.deepcopy(self)
+
+        if isinstance(other,type(self)): # both are Palette
+            # check if we can add
+            # 1 - color space should be the same
+            if self.colorSpace.name == other.colorSpace.name: # in the sma ecolor space
+                # 2 - number of colors
+                if self.nbColors == other.nbColors:
+                    # add
+                    res.colors= self.colors + other.colors
+                    res.colors = np.asarray(sorted(res.colors.tolist(), key = lambda u  : np.sqrt(np.dot(u,u))))
+                else:
+                    # error message
+                    print("WARNING[Palette.__add__(self,other): both image palette  must have the same number of colors ! return a copy of ",self,"]")
+            else:
+                # error message
+                print("WARNING[Palette.__add__(self,other): both image palette  must have the same color space ! return a copy of ",self,"]")
+        # return 
+        return res
+    
+    def __radd__(self, other): 
+        # print("DEBUG[Palette.__radd__(",self.name,",",other.name,") ]")
+
+        return self.__add__(other)
+
+    def __mul__ (self, other):
+        # print("DEBUG[Palette.__mul__(",self.name,",",other,") ]")
+
+        # create a copy
+        res = copy.deepcopy(self)
+
+        if  isinstance(other, (int, float)):
+            res.colors = self.colors * other
+        else:
+            # error message
+            print("WARNING[Palette.__mul__(self,other): other must be int or float ! return a copy of ",self,"]")
+
+        return res
+
+    def __rmul__ (self, other): 
+        # print("DEBUG[Palette.__rmul__(",self.name,",",other,") ]")
+
+        return self.__mul__(other)

+ 1 - 0
_miamG/miam/aesthetics/__init__.py

@@ -0,0 +1 @@
+

+ 12 - 0
_miamG/miam/classification/KmeanDisplay.py

@@ -0,0 +1,12 @@
+
+import matplotlib.pyplot as plt
+
+class KmeanDisplay(object):
+	"""description of class"""
+	
+	def __init__(self):
+		pass
+
+
+	def plot(self, centroids, assigmentsIdx, iter):
+		pass

+ 1 - 0
_miamG/miam/classification/__init__.py

@@ -0,0 +1 @@
+

+ 64 - 0
_miamG/miam/classification/colorPaletteKmeanDisplay.py

@@ -0,0 +1,64 @@
+import matplotlib.pyplot as plt
+import numpy as np
+
+from . import KmeanDisplay
+
+import miam.aesthetics.Palette as MPAL
+import miam.image.ColorSpace as MICS
+import miam.pointcloud.PointCloud2D as MPC2D
+
+
+
+class colorPaletteKmeanDisplay(KmeanDisplay.KmeanDisplay):
+	""" xxx """
+	def __init__(self, pcolor= None):
+
+		# colors
+		self.colors = ['b', 'g','r','c','m','y','k'] if pcolor == None else pcolor
+
+		# figure and axes
+		fig, ax = plt.subplots(2,2) # 1 row x 2 columns
+		fig.suptitle('Palette Classification by k-means[color palette distance]')
+
+		wm = plt.get_current_fig_manager()
+		wm.window.state('zoomed')
+
+		self.fig = fig
+		self.axes = ax
+
+	def plot(self, centroids, assigmentsIdx, iter, convergence,nbSamples):
+		# display centroids shape
+		self.axes[0,0].cla()
+		_range = max([np.amax(np.abs(centroids[:,:,1])),np.amax(np.abs(centroids[:,:,2]))])
+		for i,c in enumerate(centroids): 
+			# plot palette
+			x = c[:,1] # a -> x
+			y = c[:,2] # b -> y
+			pc = MPC2D.PointCloud2D(x,y)
+			xx,yy = MPC2D.PointCloud2D.toXYarray(pc.convexHull())
+			self.axes[0,0].plot(xx,yy,self.colors[i%len(self.colors)]+'o--', linewidth=1, markersize=3)
+			self.axes[0,0].plot([- _range, _range],[0,0],'k', linewidth=0.1)
+			self.axes[0,0].plot([0,0],[-_range,_range],'k', linewidth=0.1)
+		self.axes[0,0].set_title("centroids (iter:"+str(iter)+")"+"['b', 'g','r','c','m','y','k']")
+
+		# display image of palette
+		self.axes[0,1].cla()
+		self.axes[0,1].set_title("palettes("+str(centroids.shape[0])+")")
+		palettes = []
+		for c in centroids: 
+			palettes.append(MPAL.Palette("",c, MICS.ColorSpace.buildLab()))
+		img = MPAL.Palette.createImageOfPalettes(palettes)
+		img.plot(self.axes[0,1],title=False)
+
+		# distance
+		numberOfChange, numberOfRemain, meanDistance = convergence
+		self.axes[1,0].cla()
+		self.axes[1,0].set_title("mean distance:"+str(meanDistance[-1]/centroids.shape[0]))
+		self.axes[1,0].plot(np.asarray(meanDistance)/centroids.shape[0],'r')
+
+		# change
+		self.axes[1,1].cla()
+		self.axes[1,1].set_title("convergence index (% change)/ absolute:"+str(numberOfChange[-1]))
+		self.axes[1,1].plot(100*np.asarray(numberOfChange)/nbSamples,'g')
+		# pause
+		plt.pause(0.05)

+ 37 - 0
_miamG/miam/classification/histCosineKmeanDisplay.py

@@ -0,0 +1,37 @@
+import matplotlib.pyplot as plt
+from . import KmeanDisplay
+
+class histCosineKmeanDisplay(KmeanDisplay.KmeanDisplay):
+	"""description of class"""
+	
+	def __init__(self, L_mean, L_std, pcolor= None):
+		# map of image according to Lightness mean and std
+		self.L_mean = L_mean
+		self.L_std  = L_std
+		# colors
+		self.colors = ['b', 'g','r','c','m','y','k'] if pcolor == None else pcolor
+
+		# figure and axes
+		fig, ax = plt.subplots(1,2)
+		fig.suptitle('Histogram Classification by k-means[cosine distance]')
+
+		self.fig = fig
+		self.axes = ax
+
+	def plot(self, centroids, assigmentsIdx, iter):
+		# display centroids shape
+		self.axes[0].cla()
+
+		for i,c in enumerate(centroids): self.axes[0].plot(c,self.colors[i%len(self.colors)])
+
+		self.axes[0].set_title("centroids (iter:"+str(iter)+")")
+
+		# display assigments
+		if isinstance(L_mean, np.ndarray) and isinstance(L_std,np.ndarray):
+			self.axes[1].cla()
+			self.axes[1].set_title("assigments map")
+
+			for i,assigmentId in enumerate(assigmentsIdx):
+				self.axes[1].plot(L_mean[assigmentId],L_std[assigmentId],self.colors[i%len(self.colors)]+'o', markersize=5)
+
+		plt.pause(0.05)

+ 315 - 0
_miamG/miam/classification/kmeans.py

@@ -0,0 +1,315 @@
+# import
+# ------------------------------------------------------------------------------------------
+import os
+import matplotlib.pyplot as plt
+import numpy as np
+import copy
+
+# multiprocessing and functools
+# import multiprocessing as mp
+from pathos.multiprocessing import ProcessingPool as Pool
+from functools import partial
+
+# miam import
+import miam.utils
+import miam.image.Image as MIMG
+import miam.processing.ColorSpaceTransform as MCST
+import miam.histogram.Histogram  as MHIST
+import miam.math as MMATH
+
+# ------------------------------------------------------------------------------------------
+# MIAM project 2020
+# ------------------------------------------------------------------------------------------
+# author: remi.cozot@univ-littoral.fr
+# ------------------------------------------------------------------------------------------
+
+class kmeans(object):
+
+	# set a random seed for reproductability
+	#np.random.seed(1968)
+	np.random.seed(1968)
+
+	def __init__(self, distance, normalize):
+		# distance use for computation between samples
+		self.distance = distance
+		self.normalize = normalize
+
+	def MPassignSamplesToCentroids(self,centroids, samples, previousAssigmentIdx):
+		# parallel function
+		def assignSampleToCentroid(previousAssigmentIdx, centroids, distance,isamp):
+			""" return (jdist, samp, i, dist,  change, remain) """
+			# recovering data from parameters
+			i, samp = isamp
+			# init data in centroids loop
+			dist = 0.0		# distance to centroid
+			jdist = 0		# minimal distance
+			for j,cent in enumerate(centroids):
+				#compute distance samps[i] et cents[j]
+				if j==0:
+					# first iteration
+					dist = distance.eval(samp,cent)
+					jdist =0
+				else:
+					# other iteration
+					d= distance.eval(samp,cent)
+					# compare dist to current minimal dist
+					if d<dist:
+						dist =d
+						jdist=j
+			# end for cents
+			if not i in previousAssigmentIdx[jdist]: 
+				change, remain = 1, 0
+			else:
+				change, remain = 0, 1
+			#return data
+			return (jdist, samp, i, dist,  change, remain)
+
+		# create partial to avoid multiple input parameters
+		pAss = partial(assignSampleToCentroid, previousAssigmentIdx, centroids, self.distance)
+
+		# prepare input
+		isamps = list(enumerate(samples))
+
+		# parallel computation
+		_pool = Pool()
+		rawResults = _pool.map(pAss, isamps)	# launching on all samples
+		results =list(rawResults)
+
+		# formatting results
+		numberSamples = len(samples)
+		numberOfChange, numberOfRemain = 0,0	# number of samples that change of/remain in centroid
+		sumDistance = 0							# sum of distance to centroids
+		assigments, assigmentsIdx= [[]], [[]]	# return list
+		for i in range(len(centroids)-1):
+			assigments.append([])
+			assigmentsIdx.append([])
+
+		for res in results:
+			jdist, samp, i, dist,  change, remain = res
+			assigments[jdist].append(samp)
+			assigmentsIdx[jdist].append(i)
+			sumDistance += dist
+			numberOfChange += change
+			numberOfRemain += remain
+
+		# add data to follow convergence
+		conv = (numberOfChange,numberOfRemain, sumDistance/numberSamples)
+
+		return (assigments, assigmentsIdx, conv)
+
+	def assignSamplesToCentroids(self,centroids, samples, previousAssigmentIdx):
+		# assChanged set to True if at least one assignment changes of centroid
+		assChanged = False
+		# number of samples that change of/remain in centroid
+		numberOfChange, numberOfRemain = 0,0
+		# sum of distance to centroids
+		sumDistance = 0
+		# return list
+		assigments, assigmentsIdx= [[]], [[]]
+		for i in range(len(centroids)-1):
+			assigments.append([])
+			assigmentsIdx.append([])
+
+		numberSamples = len(samples)
+		# DEBUG
+		print("")
+		# END DEBUG
+		for i,samp in enumerate(samples):
+			# DEBUG
+			print("\r kmeans.assignSamplesToCentroids: ",i,"/",numberSamples,"[remains:",numberOfRemain,"][changes:",numberOfChange,"][mean distance:",sumDistance/(i+1),"]   ", end = '\r')
+			# END DEBUG
+			dist = 0.0
+			jdist = 0
+			for j,cent in enumerate(centroids):
+				#compute distance samps[i] et cents[j]
+				if j==0:
+					# first iteration
+					dist = self.distance.eval(samp,cent)
+					jdist =0
+				else:
+					# other iteration
+					d= self.distance.eval(samp,cent)
+					# compare dist to current minimal dist
+					if d<dist:
+						dist =d
+						jdist=j
+					# end if
+				#end if
+			# end for cents
+			assigments[jdist].append(samp)
+			assigmentsIdx[jdist].append(i)
+			sumDistance += dist
+			if not i in previousAssigmentIdx[jdist]: 
+				assChanged = True
+				numberOfChange += 1
+			else:
+				numberOfRemain +=1
+
+		# end for samp
+
+		# add dta to follow convergence
+		conv = (numberOfChange,numberOfRemain, sumDistance/numberSamples)
+
+		return (assigments, assigmentsIdx, conv)
+
+	def averageAssigments(self, assignements):
+		# debug
+		# print("kmeans.averageAssigment>> start")
+		# end debug	
+
+ 		# return list
+		assigmentAverage = [[]]
+		for i in range(len(assignements)-1): assigmentAverage.append([])
+
+		for i,assigment_i in enumerate(assignements):
+
+			# debug
+			# print("kmeans.averageAssigment::sassigment_i.size>>",np.asarray(assigment_i).size)
+			# end debug
+
+			if np.asarray(assigment_i).size >0 : 
+				assavi=np.mean(np.asarray(assigment_i),axis=0)
+				assavi = self.normalize.eval(assavi) 
+				assigmentAverage[i]= assavi
+
+		# debug
+		# print("kmeans.averageAssigment>> end")
+		# end debug	
+				
+		return assigmentAverage
+
+	def kmeans(self,samples, nbClusters, nbIter, display = None, initRange=None, multiPro=False):
+		""" 
+		method keams:
+        attribute(s):
+			self: insytance method
+            samples: samples to cluster (np.ndarray) 
+					samples.shape[0] : number of samples
+					samples.shape[1:]: dimension of sample,		if samples.shape[1:] is int then sample are vectors and amples.shape[1:] is vector size
+																if samples.shape[1:] is tuple are matrices or tensors and amples.shape[1:] is matrix/tensor dimension
+																exemple samples.shape[1:] =(5,3) sample is matrix 5x3
+			nbClusters: number of cluster to compute (int)
+			nbIter: number of iteration of k-means (int)
+			display: class with init and plot method, plot is called at each iteration						!!!!!!!!!!!!!!!!!!!! require refactoring !!!!!!!!!!!!!!!!!!!!
+			initRange: None or list of tuple
+						random centroids are used to init the k-means
+						centroids are stored in a np.ndarray which shape is (number of clusters, *samples.shape[1:])
+						samples.shape[-1] is size of "atomic" vector for example is centroids is 5x3 it means 5 vector of size 3 (the case for color palettes)
+						init should be [{minRange0,maxRange0}, {minRange1,maxRange1}, {minRange2,maxRange2}]*5 with initRange =  [(minRange0,maxRange0), (minRange1,maxRange1), (minRange2,maxRange2)]
+																													initRange=None range in 0..1				          
+    """
+		# dimension of centroids
+		dimSamp = samples.shape
+		dimCentroid = dimSamp[1:]	# palette
+		# dimCentroid = dimSamp[1]	# histo
+
+		# init centroids
+		if isinstance(dimSamp,tuple): 
+			u = np.random.rand(nbClusters,*dimCentroid)
+			if initRange:
+				minRange, maxRange = [],[]
+				for _range in initRange:
+					minr, maxr = _range
+					minRange.append(minr)
+					maxRange.append(maxr)
+				v = (1-u)*np.asarray(minRange) + u*np.asarray(maxRange)
+			else:
+				v =u
+			centroids = self.normalize.evals(v) # palette
+		else: #integer
+			u = np.random.rand(nbClusters,dimCentroid)
+			if initRange:
+				minRange, maxRange = [],[]
+				for _range in initRange:
+					minr, maxr = _range
+					minRange.append(minr)
+					maxRange.append(maxr)
+				v = (1-u)*np.asarray(minRange) + u*np.asarray(maxRange)
+			else:
+				v =u			
+			centroids = self.normalize.eval(v) # histo
+
+		# return assigments and assigments index
+		previousAssigmentsIdx = [[]]
+		assigments,assigmentsIdx = [[]], [[]]
+		for i in range(nbClusters-1):
+			assigments.append([])
+			assigmentsIdx.append([])
+
+			previousAssigmentsIdx.append([])
+		# convergence
+		changes = []
+		remains=[]
+		meanDistances= []
+
+		# MAIN LOOP 
+		# -----------------------------------------------------------------------------------------
+		# for iter in range(nbIter)
+		for iter in range(nbIter):
+			print("\r","kmeans(iteration): ",iter,"/",nbIter,":",iter*100//nbIter," %      ",end = '\r')
+
+			# assign sample to centoids
+			if multiPro:
+				(assigments,assigmentsIdx,conv) = self.MPassignSamplesToCentroids(centroids,samples, previousAssigmentsIdx)
+			else:
+				(assigments,assigmentsIdx,conv) = self.assignSamplesToCentroids(centroids,samples, previousAssigmentsIdx)
+
+			# recover data from results
+			change, remain, meanDist = conv
+			changes.append(change)
+			remains.append(remain)
+			meanDistances.append(meanDist)
+
+			# compute mean of (assigment) cluster
+			assigmentsAverage = self.averageAssigments(assigments)
+				
+			# update centroids and stopping criteria
+			canBreak= True
+			for i,ass_av in enumerate(assigmentsAverage):
+				emptyAss  = True
+				if isinstance(ass_av,np.ndarray):
+					if (ass_av.size!=0):
+						emptyAss  = False
+						centroids[i] = ass_av
+				if emptyAss:
+					canBreak= False
+					if isinstance(dimSamp,tuple):
+						u = np.random.rand(1,*dimCentroid)
+						if initRange:
+							minRange, maxRange = [],[]
+							for _range in initRange:
+								minr, maxr = _range
+								minRange.append(minr)
+								maxRange.append(maxr)
+							v = (1-u)*np.asarray(minRange) + u*np.asarray(maxRange)
+						else: v =u
+						newcentroid = self.normalize.evals(v)	# palette
+					else:										# histogram					
+						u = np.random.rand(nbClusters,dimCentroid)
+						if initRange:
+							minRange, maxRange = [],[]
+							for _range in initRange:
+								minr, maxr = _range
+								minRange.append(minr)
+								maxRange.append(maxr)
+							v = (1-u)*np.asarray(minRange) + u*np.asarray(maxRange)
+						else:
+							v =u			
+						newcentroid = self.normalize.eval(v) # histo					
+					centroids[i] = newcentroid
+					print("")
+					print("WARNING[miam.classification.kmeans(): (iteration:",iter,"/centroid:",i,"): no assigment! >> compute new centroid]")
+
+			# display
+			if display: display.plot(centroids, assigmentsIdx, iter,(changes,remains,meanDistances), len(samples))
+
+			# memory
+			previousAssigmentsIdx = copy.deepcopy(assigmentsIdx)
+
+			# break iteration if change=0
+			if (change==0) and(canBreak): break
+		# -----------------------------------------------------------------------------------------
+		# return centroids
+		print(" ")
+		return (centroids,assigments,assigmentsIdx)
+

+ 196 - 0
_miamG/miam/histogram/Histogram.py

@@ -0,0 +1,196 @@
+# import
+# ------------------------------------------------------------------------------------------
+from .. import image
+import copy, functools
+import numpy as np
+
+import matplotlib.pyplot as plt
+# miam import
+import miam.math.Distance
+# ------------------------------------------------------------------------------------------
+# MIAM project 2020
+# ------------------------------------------------------------------------------------------
+# author: remi.cozot@univ-littoral.fr
+# ------------------------------------------------------------------------------------------
+class Histogram(object):
+    """description of class"""
+    
+    def __init__(self, histValue, edgeValue, name, channel, logSpace = False):
+        """ constructor """
+
+        self.name           = name
+        self.channel        = channel
+        self.histValue      = histValue
+        self.edgeValue      = edgeValue
+        self.logSpace       = logSpace    
+
+    def __repr__(self):
+        res =   " Histogram{ name:"         + self.name                 + "\n"  + \
+                "         nb bins: "        + str(len(self.histValue))  + "\n"  + \
+                "         channel: "        + str(self.channel.name)+"("+self.channel.colorSpace()+")"    + "\n"  + \
+                "         logSpace: "       + str(self.logSpace)        + "\n }"  
+        return res
+
+    def __str__(self): return self.__repr__()
+
+    def normalise(self, norm=None):
+        """ normalise histogram according to norm='probability' | 'dot' """
+        res = copy.deepcopy(self)
+        if not norm: norm = 'probability'
+        if norm == 'probability':
+            sum = np.sum(res.histValue)
+            res.histValue = res.histValue/sum
+        elif norm == 'dot':
+            dot2 = np.dot(res.histValue,res.histValue)
+            res.histValue = res.histValue/np.sqrt(dot2)
+        else:
+            print("WARNING[miam.hisrogram.Histogram.normalise(",self.name,"): unknown norm:", norm,"!]")
+        return res
+
+    def build(img, channel, nbBins=100, range= None, logSpace = None):
+        """
+        build an Histogram object from image
+        @params:
+            img         - Required  : input image from witch hsitogram will be build (miam.image.Image.Image)
+            channel     - Required  : image channel used to build histogram  (miam.image.channel.channel)
+            nbBins      - Optional  : histogram number of bins  (Int)
+            range       - Optional  : range of histogram, if None min max of channel (Float,Float)
+            logSpace    - Optional  : compute in log space if True, if None guess from image (Boolean)
+        """
+        # logSpace
+        if not logSpace: logSpace = 'auto'
+        if isinstance(logSpace,str):
+            if logSpace=='auto':
+                if img.type == image.imageType.imageType.SDR : logSpace = False
+                if img.type == image.imageType.imageType.HDR : logSpace = True
+        elif not isinstance(logSpace,bool):
+            logSpace = False
+
+        channelVector = img.getChannelVector(channel)
+        # range
+        if not range: 
+            if channel.colorSpace() == 'Lab':
+                range= (0.0,100.0)
+            elif channel.colorSpace() == 'sRGB'or channel.colorSpace() == 'XYZ':
+                range= (0.0,1.0)
+            else:
+                range= (0.0,1.0)
+                print("WARNING[miam.hisrogram.Histogram.build(",img.name,"):", 
+                      "colour space:",channel.colorSpace(), "not yet implemented > range(0.0,1.0)!]")
+        # compute bins
+        if logSpace:
+            ((minR,maxR),(minG,maxG),(minB,maxB)) = img.getMinMaxPerChannel()
+            minRGB = min(minR, minG, minB)
+            maxRGB = max(maxR, maxG, maxB)
+            #bins
+            bins = 10 ** np.linspace(np.log10(minRGB), np.log10(maxRGB), nbBins+1)
+        else:
+            bins = np.linspace(range[0],range[1],nbBins+1)
+
+        nphist, npedges = np.histogram(channelVector, bins)
+
+        nphist = nphist/channelVector.shape
+        return Histogram(nphist, 
+                         npedges, 
+                         img.name, 
+                         #copy.deepcopy(img.colorSpace),
+                         channel,
+                         logSpace = logSpace
+                         )
+
+    def plot(self, ax,color='r', shortName =True,title=True):
+        if not color : color = 'r'
+        ax.plot(self.edgeValue[1:],self.histValue,color)
+        if self.logSpace: ax.set_xscale("log")
+        name = self.name.split("/")[-1]+"(H("+self.channel.name+"))"if shortName else self.name+"(Histogram:"+self.channel.name+")"
+        if title: ax.set_title(name)
+
+    def scale(alpha,h):
+        res = copy.deepcopy(h)
+        res.histValue      = res.histValue * alpha
+        return res
+
+    def add(hu,hv):
+        res = copy.deepcopy(hu)
+        # check edges
+        if (hu.edgeValue==hv.edgeValue).all():
+            # porceed to summation
+            res.histValue = hu.histValue + hv.histValue
+        else:
+            # remap
+            pass
+        return res
+
+    def computeDistance(hu,hv,distance=None):
+        """ compute distance between histograms 'hu' and 'hv' according to distance 'distance' """
+        if not distance:
+            # default distance is cosine
+            distance = miam.math.Distance.Distance(miam.math.Distance.cosineDistance)
+        res = 1
+        # some checking
+        # histogram must have the same color space
+        if hu.channel.name == hv.channel.name :
+            # histogram must have the same number of bin
+            if len(hu.histValue) == len(hv.histValue):
+                res = distance.eval( hu.histValue, hv.histValue)
+            else:
+                print("WARNING[miam.hisrogram.Histogram.computeDistance(",str(hu),",",str(hv),"):", "have different length: return distance=1 !]")
+
+        else :
+            print("WARNING[miam.hisrogram.Histogram.computeDistance(",str(hu),",",str(hv),"):", "have different channels: return distance=1!]")
+        return res
+
+    def segmentPics(self, nbSegs=3):
+        """
+        segment histogram by pics
+        """
+
+        # local functions
+        def isMaxWindow(a3): return  ((a3[0] <= a3[1]) and (a3[1] >= a3[2]))  
+        def isMinWindow(a3): return  ((a3[0] >= a3[1]) and (a3[1] <= a3[2]))
+
+        def filterWindow(a3,weights): return (a3[0]*weights[0]+a3[1]*weights[1]+a3[2]*weights[2])/(weights[0]+weights[1]+weights[2])
+
+        def filter(v, weights):
+            res = copy.deepcopy(v)
+            res[0] = filterWindow([v[0],v[0],v[1]],weights)
+            res[-1] = filterWindow([v[-2],v[-1],v[-1]],weights)
+            for i in range(1, len(v)-1): res[i] = filterWindow(v[(i-1):(i+2)],weights)
+            return res
+
+        def getSegmentBoundaries(v):
+            seg = []
+            seg.append(0) # add fist
+            for i in range(1,len(v)-2):
+                isMin = isMinWindow(v[i-1:i+2])
+                isMax = isMaxWindow(v[i-1:i+2])
+                if isMin and not isMax:
+                    seg.append(i)
+            seg.append(len(v)-1) # add last
+            return seg
+
+        value = copy.deepcopy(self.histValue)
+
+        while (len(getSegmentBoundaries(value))-1)> nbSegs:
+            weights = [1,1,1]
+            newValue = filter(value, weights)
+            newnbs = len(getSegmentBoundaries(newValue))-1
+
+            # next iter
+            value = newValue
+
+        # plot for debug
+        #segmentBoundaries = getSegmentBoundaries(value)
+        #plt.figure("segments")
+        #plt.plot(self.histValue,'k--')
+        #for i in range(len(segmentBoundaries)): 
+        #    plt.plot(segmentBoundaries[i],self.histValue[segmentBoundaries[i]],'ro')
+        #plt.show(block=True)
+        #print(segmentBoundaries)
+
+        # index of boundaries
+        segmentBoundaries = getSegmentBoundaries(value) 
+        # values of boundaries
+        segmentBoundariesValues = list(map(lambda i: self.edgeValue[i] if i< len(self.edgeValue)/2 else self.edgeValue[i+1] ,segmentBoundaries))
+
+        return segmentBoundariesValues

+ 1 - 0
_miamG/miam/histogram/__init__.py

@@ -0,0 +1 @@
+

+ 1 - 0
_miamG/miam/html/__init__.py

@@ -0,0 +1 @@
+

+ 60 - 0
_miamG/miam/html/generator.py

@@ -0,0 +1,60 @@
+from functools import partial
+
+class generator(object):
+    """description of class"""
+
+    def tag(tagName, content, attributeName= None, attributeValue=None, close=True):
+        # taking attribute into account
+        if attributeName and attributeValue:
+            if isinstance(attributeName, list) and isinstance(attributeValue, list):
+                minLen = min(len(attributeName),len(attributeName))
+                attrib =''
+                for i in range(minLen):
+                    attrib = attrib+attributeName[i]+'="'+attributeValue[i]+'" '
+            else:
+                attrib = attributeName+'="'+attributeValue+'"'
+            bBalise= '<'+tagName+' '+attrib+'>'
+        else:
+            bBalise= '<'+tagName+'>'
+        
+        # taking close into account
+        eBalise= '</'+tagName+'>' if close else ''
+
+        # return
+        return bBalise+content+eBalise
+
+    imgTagWidth = partial(tag, tagName='img', content='', attributeName= ['src','width'], close=False)
+    imgTag = partial(tag, tagName='img', content='', attributeName= 'src', close=False)
+    bold = partial(tag, tagName='b',  attributeName= None, attributeValue=None, close=True)
+    li = partial(tag, tagName='li',  attributeName= None, attributeValue=None, close=True)
+    ul = partial(tag, tagName='ul',  attributeName= None, attributeValue=None, close=True)
+    td = partial(tag, tagName='td',  attributeName= None, attributeValue=None, close=True)
+    tr = partial(tag, tagName='tr',  attributeName= None, attributeValue=None, close=True)
+
+    def listItem(nameItem_item):
+        res = ''
+        for it in nameItem_item:
+            res+=generator.li(content=generator.bold(content=it[0]+':')+it[1])+'\n'
+        return generator.ul(content='\n'+res)
+
+    def table(tableElt):
+        r = ''
+        for row in tableElt:
+            d = ''
+            for elt in row: d+=generator.td(content=elt)
+            r += generator.tr(content=d)+'\n'
+        return generator.tag('table', '\n'+r)
+
+# --------------------------------------------------------------------------------------
+# test
+# --------------------------------------------------------------------------------------
+def test():
+    print(generator.tag('H1', 'HDR DataBase'))
+    print(generator.imgTag(attributeValue='../image/test.jpg'))
+    print(generator.listItem([['toto','titi'],['momo','mimi'],['roro','riri']]))
+    print(generator.table([['toto','titi'],['momo','mimi'],['roro','riri']]))
+
+
+
+
+

+ 71 - 0
_miamG/miam/image/ColorSpace.py

@@ -0,0 +1,71 @@
+# import
+# ------------------------------------------------------------------------------------------
+import os, colour
+# local
+from . import Exif
+# ------------------------------------------------------------------------------------------
+# MIAM project 2020
+# ------------------------------------------------------------------------------------------
+# author: remi.cozot@univ-littoral.fr
+# ------------------------------------------------------------------------------------------
+class ColorSpace(object):
+    """mapping to colour.models.RGB_COLOURSPACES"""
+
+    def buildFromExifData(dictExif, defaultCS = 'sRGB'):
+        """ build colour.models from exif dict (dict) """
+
+        # default color space
+        colourspace= None
+
+        exifColorSpace = ['Color Space', 'Profile Description']
+
+        # first 'Color Space'
+        if exifColorSpace[0] in dictExif:
+            value = dictExif[exifColorSpace[0]]
+            if 'sRGB' in value : 
+                colourspace= colour.models.RGB_COLOURSPACES['sRGB'].copy()
+            elif 'Adobe' in value : 
+                colourspace= colour.models.RGB_COLOURSPACES['Adobe RGB (1998)'].copy()
+
+        # no color space found, try profile description
+        if not colourspace:
+            if exifColorSpace[1] in dictExif:
+                value = dictExif[exifColorSpace[1]]
+                if 'sRGB' in value : 
+                    colourspace= colour.models.RGB_COLOURSPACES['sRGB'].copy()
+                elif 'Adobe' in value : 
+                    colourspace= colour.models.RGB_COLOURSPACES['Adobe RGB (1998)'].copy()
+
+        # no color space found, use default
+        if not colourspace:
+            # warning
+            # print("WARNING[miam.image.ColorSpace.buildFromExif(",dictExif['File Name'],"):", "no colour space found in Exif Metadata >> ", defaultCS, " used by default]")
+            colourspace = colour.models.RGB_COLOURSPACES[defaultCS].copy()
+
+        return colourspace
+
+    def buildFromExif(e, defaultCS = 'sRGB'): 
+        """ build colour.models from Exif (object) """
+        return ColorSpace.buildFromExifData(e.exif, defaultCS)
+
+    def buildLab():
+        return colour.RGB_Colourspace('Lab', primaries = None, whitepoint = None, whitepoint_name=None, 
+                                      RGB_to_XYZ_matrix=None, XYZ_to_RGB_matrix=None, 
+                                      cctf_encoding=None, cctf_decoding=None,
+                                      use_derived_RGB_to_XYZ_matrix=False, use_derived_XYZ_to_RGB_matrix=False)
+
+    def buildsRGB(): return colour.models.RGB_COLOURSPACES['sRGB'].copy()
+
+    def buildXYZ():
+        return colour.RGB_Colourspace('XYZ', primaries = None, whitepoint = None, whitepoint_name=None, 
+                                      RGB_to_XYZ_matrix=None, XYZ_to_RGB_matrix=None, 
+                                      cctf_encoding=None, cctf_decoding=None,
+                                      use_derived_RGB_to_XYZ_matrix=False, use_derived_XYZ_to_RGB_matrix=False)
+    def build(name='sRGB'):
+        cs  = None
+        if name== 'sRGB': cs =  colour.models.RGB_COLOURSPACES['sRGB'].copy()
+        if name== 'Lab' : cs =  buildLab()
+        if name== 'XYZ' : cs =  buildXYZ()
+        return cs
+
+

+ 62 - 0
_miamG/miam/image/Exif.py

@@ -0,0 +1,62 @@
+# import
+# ------------------------------------------------------------------------------------------
+import subprocess, os, sys
+# ------------------------------------------------------------------------------------------
+# MIAM project 2020
+# ------------------------------------------------------------------------------------------
+# author: remi.cozot@univ-littoral.fr
+# ------------------------------------------------------------------------------------------
+
+class Exif(object):
+    """store exif data read image file with exiftool"""
+
+    def __init__(self):
+        """ constructor """
+        # attributes
+        self.imageName = None               # image file name   (String)
+        self.exif = None                    # exif data         (dict)
+
+    def buildFromFileName(filename):
+        """ return Exif (object) from image file """
+        res = Exif()
+        exifDict = dict()
+
+        # reading metadata
+        if os.path.exists(filename):
+            try:
+                exifdata = subprocess.check_output(['exiftool.exe','-a',filename],
+                                            shell=True,
+                                            universal_newlines=True,
+                                            stdin=subprocess.PIPE,
+                                            stderr=subprocess.PIPE)
+                exifdata = exifdata.splitlines()
+            except:
+                print("ERROR[miam.iamge.Exif.buildFromFileName(",filename,"): can not be read!]")
+                exifdata =[]
+            # buid exif dict
+            for each in exifdata:
+                # tags and values are separated by a colon
+                tag,val = each.split(':',1) # '1' only allows one split
+                exifDict[tag.strip()] = val.strip()
+
+            # set attributes
+            res.imageName = filename
+            res.exif = exifDict
+        else:
+            print("ERROR[miam.iamge.Exif.buildFromFileName(",filename,"): file not found]")
+            sys.exit()
+
+        return res
+
+    def __repr__(self):
+        resKeys = ""
+        for k in self.exif:
+            resKeys = resKeys+"        "+k+": "+self.exif[k]+"\n"
+        res =   "Exif{ \n " + \
+                "   .imageName:" + self.imageName +"\n" + \
+                "    .exifDict:{ \n" + \
+                resKeys + "}}"
+        return res
+
+    def __str__(self): return self.__repr__()
+

+ 56 - 0
_miamG/miam/image/channel.py

@@ -0,0 +1,56 @@
+# import
+# ------------------------------------------------------------------------------------------
+import enum
+# ------------------------------------------------------------------------------------------
+# MIAM project 2020
+# ------------------------------------------------------------------------------------------
+# author: remi.cozot@univ-littoral.fr
+# ------------------------------------------------------------------------------------------
+
+class channel(enum.Enum):
+    """ easy definition of channel """
+    
+    sR      = 0
+    sG      = 1
+    sB      = 2
+
+    sRGB    = 3
+
+    X       = 4
+    Y       = 5
+    Z       = 6
+
+    XYZ     = 7
+
+    L       = 8
+    a       = 9
+    b       = 10
+
+    Lab     = 11
+
+    def colorSpace(self):
+        csIdx = self.value // 4
+        res = None
+        if csIdx   == 0:    res ='sRGB'
+        elif csIdx == 1:    res = 'XYZ'
+        elif csIdx == 2:    res = 'Lab'
+
+        return res
+
+    def getValue(self): return self.value % 4
+
+    def toChannel(s):
+        if s=='sR' :    return channel.sR
+        elif s=='sG' :  return channel.sG
+        elif s=='sB' :  return channel.sB
+
+        elif s=='X' :   return channel.X
+        elif s=='Y' :   return channel.Y
+        elif s=='Z' :   return channel.Z
+
+        elif s=='L' :   return channel.L
+        elif s=='a' :   return channel.a
+        elif s=='b' :   return channel.b
+
+        else:           return channel.L
+

+ 15 - 0
_miamG/miam/image/imageType.py

@@ -0,0 +1,15 @@
+# import
+# ------------------------------------------------------------------------------------------
+import enum
+# ------------------------------------------------------------------------------------------
+# MIAM project 2020
+# ------------------------------------------------------------------------------------------
+# author: remi.cozot@univ-littoral.fr
+# ------------------------------------------------------------------------------------------
+
+class imageType(enum.Enum):
+	""" Enum image type: SDR, RAW, HDR """
+
+	SDR = 0 # SDR image: (.jpg)
+	RAW = 1 # raw image file: sony ARW (.arw)
+	HDR = 2 # hdr file: (.hdr)

+ 21 - 0
_miamG/miam/imageDB/Builder.py

@@ -0,0 +1,21 @@
+from . import ImageDB
+import os, json
+class Builder(object):
+    """description of class"""
+
+    def __init__(self):
+        self.config = None
+
+    def compute(self,db,src):
+
+
+        # CSV file should NOT still exist
+        if db.csvFileName:
+            if  os.path.isfile(db.csvFileName): print("WARNING[miam.imageDB.Builder.compute(...):",db.csvFileName," already exists! It will be overwritten!]")
+
+        # config file
+        if db.jsonConfigFile:
+            with open(db.jsonConfigFile) as json_file: 
+                self.config = json.load(json_file)
+        else:
+            print("ERROR[miam.imageDB.Builder.compute(...):",db.jsonConfigFile," does not exist !]")

+ 15 - 0
_miamG/miam/imageDB/Checker.py

@@ -0,0 +1,15 @@
+from . import ImageDB
+import os, json
+
+class Checker(object):
+    """Enable to check if images in csv file exist"""
+
+    def __init__(self):
+        pass
+
+    def compute(self,db):
+        pass
+
+    
+
+

+ 67 - 0
_miamG/miam/imageDB/HwHDRBuilder.py

@@ -0,0 +1,67 @@
+import os, json
+import numpy as np
+from . import Builder, ImageDB, utils
+
+class HwHDRBuilder(Builder.Builder):
+    """description of class"""
+
+    def __init__(self):
+        super().__init__()
+
+    def compute(self,db, src):
+        super().compute(db,src)
+
+        # recover data from config
+        imagePATH = self.config["imagePATH"]
+        cvsFilePATH = self.config["cvsFilePATH"]
+
+        # get database compenents
+        components = self.config['components']
+
+        # build is done according to a source
+        for c in components:
+            if src in c: configSRC= c
+        # source path
+        srcPath = imagePATH + configSRC[src]['path']
+
+        # get file in source path
+        l = os.listdir(srcPath)
+
+        # get only files with right name
+        srcList = []
+        for f in l:
+           if utils.checkString(f,configSRC[src]) : srcList.append(f)
+
+        # srcList contains src file with correct format
+        # check other compoents
+        requiredFiles = []
+
+        # for all file in scrList (source file)
+        for srcfile in srcList:
+            # listPerSourceFile
+            listPerSourceFile = []
+            # get number
+            n = utils.filename_to_number(srcfile,configSRC[src])
+            listPerSourceFile.append(configSRC[src]['path']+srcfile)
+            # for other components
+            for comp in components:
+                if not src in comp: # if not the source
+                    currentConfig = comp
+                    # get name of compenent
+                    k = list(currentConfig.keys())[0]
+                    # get config compoenent
+                    kv = currentConfig[k]
+                    listPerSourceFile.extend(utils.number_to_filenames(n,kv))
+
+            requiredFiles.append(listPerSourceFile)
+
+        # save as csv file
+        np.savetxt( db.csvFileName, requiredFiles, delimiter=";",fmt="%s",encoding="utf8")
+
+        # update db in db
+        db.db = requiredFiles
+
+
+
+
+

+ 64 - 0
_miamG/miam/imageDB/ImageDB.py

@@ -0,0 +1,64 @@
+import os, json
+import numpy as np
+
+class ImageDB(object):
+    """Generic Class for image-database
+    main assumption:
+    DB: CSV files with image link (path or uri)
+    CSV file structure: per row (uri; uri; uri; ...) or (filename; filename; filename; ...)
+    """
+    def __init__(self, jsonConfigFile = None):
+        """
+        json file format:
+            {
+	            "name": "my_DB",                            <- database name                                                       
+	            "imagePATH": "../images/",                  <- where are local copy of images
+	            "csvFilePATH": "../imagefile_utf8.csv",     <- image path or uri list in csv format
+            }
+        """
+
+        # attribute
+        self.jsonConfigFile = jsonConfigFile
+
+        # load config
+        with open(self.jsonConfigFile) as json_file: config = json.load(json_file)
+
+        # recover cvsFileName from config file  
+        self.name = config["name"]
+        self.csvFileName = config["csvFilePATH"]
+        self.imagePATH = config["imagePATH"]
+
+        # DEBUG
+        # print("DEBUG[ImageDB.name::",self.name,"]")
+        # print("DEBUG[ImageDB.csvFileName::",self.csvFileName,"]")
+        # print("DEBUG[ImageDB.imagePATH::",self.imagePATH,"]")
+
+        # database set to None
+        self.db = None
+
+        if  os.path.isfile(self.csvFileName):
+            # DEBUG
+            # print("DEBUG[ImageDB.csvFileName::",self.csvFileName,"> load csv]")
+
+            self.db = np.loadtxt(self.csvFileName, dtype=np.unicode_, delimiter=";",encoding="utf8")
+
+            #DEBUG
+            # print("DEBUG[ImageDB.db.shape::",self.db.shape,"]")
+        pass
+
+    def build(self,builder,src=""):
+        """ build cvs file """
+        builder.compute(self,src)
+
+    def check(self,checker):
+        """ check if images in cvs are in local storage """
+        checker.compute(self)
+
+    def lookForUpdate(self):
+        pass
+
+    def update(self):
+        pass
+
+
+

+ 122 - 0
_miamG/miam/imageDB/ImageDB_HDD.py

@@ -0,0 +1,122 @@
+from . import ImageDB
+
+import os, json, functools, datetime, imageio
+import numpy as np
+import matplotlib.pyplot as plt
+
+import miam.html.generator as MHTML
+import miam.image.Image as MIMG
+import miam.histogram.Histogram as MHIST
+import miam.processing.TMO_CCTF
+
+class ImageDB_HDD(ImageDB.ImageDB):
+    """description of class"""
+    def __init__(self,name=None, csvFile=None, jsonConfigFile = None):
+        super().__init__(name, csvFile, jsonConfigFile)
+
+    def check(self,uri2file=None):
+        checked = []
+        missingItems = []
+        missingFiles = []
+
+        # load config
+        with open(self.jsonConfigFile) as json_file: config = json.load(json_file)
+        path = config["imagePATH"]
+
+
+        # for all item in db
+        for item in self.db:
+            itemOk = list(map(lambda x : os.path.isfile(path+x), item))
+            ok = functools.reduce(lambda x,y : x and y,itemOk)
+            if ok:
+                checked.append(item)
+            else:
+                missingItems.append(item)
+                for i, fileOk in enumerate(itemOk):
+                    if not fileOk: missingFiles.append(item[i])
+        # results
+        path = self.csvFileName[0: len(self.csvFileName) - len(self.csvFileName.split("/")[-1])]
+        np.savetxt(path+name+"_"+"missingItems.csv",missingItems,delimiter=";",fmt="%s",encoding="utf8")
+        np.savetxt(path+name+"_"+"missingImages.csv",missingFiles,delimiter=";",fmt="%s",encoding="utf8")
+
+        self.db = np.asarray(checked)
+
+    def buildHTMLPage(self):
+        # get date
+        todayString = datetime.date.today().strftime("%Y/%m/%d")
+
+        # load config
+        with open(self.jsonConfigFile) as json_file: config = json.load(json_file)
+        path = config["imagePATH"]
+
+        # <!DOCTYPE html>
+        # <html>
+        # <head><title>HDR databse</title></head>
+        # <BODY>
+        # <H1>HDR DataBase</H1>
+        # </BODY>
+        # </html>
+
+        head = MHTML.generator.tag('head', content=MHTML.generator.tag('title', content='hdr database'))
+        h1 = MHTML.generator.tag('h1', content='HDR DATABASE')
+        date = MHTML.generator.tag('h3', content='Rémi Cozot - '+ todayString)
+        pageBody =''
+        # for all items
+        for item in self.db:
+
+            # load HDR image
+            hdr =  MIMG.Image.readImage(path+item[0]).resize((None,480)).removeZeros().removeZeros(0.5)
+            # basic tone mapping
+            hdrCCTF = hdr.process(miam.processing.TMO_CCTF.TMO_CCTF(), function='sRGB')
+            # read Hand Tone Mapped image
+            image = MIMG.Image.readImage(path+item[-1]).resize((None,480))
+
+            # compute Y(HDR) and L(SDR) histograms
+            hdrhistoY = MHIST.Histogram.build(hdr,MIMG.channel.channel.Y, nbBins=50)
+            imagehistoL = MHIST.Histogram.build(image,MIMG.channel.channel.L, nbBins=50)
+            
+            # create name
+            imageName = (item[-1].split('/')[-1]).split(".")[0]
+
+            # create and save histogram images
+            fig, ax = plt.subplots()
+            hdrhistoY.plot(ax)
+            plt.show(block=False)
+            plt.savefig ( "../HTML/images/"+"HDR_"+imageName+"_YHIST.png" )
+            plt.close(fig)
+            
+            fig, ax = plt.subplots()
+            imagehistoL.plot(ax)
+            plt.show(block=False)
+            plt.savefig ( "../HTML/images/"+"SDR_"+imageName+"_LHIST.png" )    
+            plt.close(fig)
+
+            # save CCTF and HTM images
+            imageio.imwrite('../HTML/images/'+"HTM_"+imageName+".jpg", (image.colorData*256).astype(np.uint8))
+            imageio.imwrite('../HTML/images/'+"CCTF_"+imageName+".jpg", (hdrCCTF.colorData*256).astype(np.uint8))
+
+            # uri for web page
+            name_CCTF_image = "images/"+"CCTF_"+imageName+".jpg"
+            name_HTM_image  = "images/"+"HTM_"+imageName+".jpg"
+            name_HDR_Yhist  = "images/"+"HDR_"+imageName+"_YHIST.png"
+            name_SDR_Lhist  = "images/"+"SDR_"+imageName+"_LHIST.png"
+
+            # create <img> table
+            pageBody = pageBody +   MHTML.generator.tag('h2', imageName)+'\n' + \
+                                    MHTML.generator.table([[ MHTML.generator.imgTagWidth(attributeValue=[name_CCTF_image,'50%']),MHTML.generator.imgTagWidth(attributeValue=[name_HDR_Yhist,'50%'])], 
+                                                           [MHTML.generator.imgTagWidth(attributeValue=[name_HTM_image,'50%']),MHTML.generator.imgTagWidth(attributeValue=[name_SDR_Lhist,'50%'])]])+ '\n'
+            
+            print(">",imageName)
+            # end loop
+        all =  MHTML.generator.tag('html', content=     head + '\n' + \
+                                                        MHTML.generator.tag('body', content= h1 + '\n' + date + '\n' + \
+                                                        pageBody
+                                  ))
+
+        fileHTML = open("../HTML/hdr-database.html","w") 
+        fileHTML.write(all) 
+        fileHTML.close() 
+ 
+
+
+

+ 53 - 0
_miamG/miam/imageDB/POGChecker.py

@@ -0,0 +1,53 @@
+from . import ImageDB, Checker, utils
+import os, json
+import numpy as np
+
+class POGChecker(Checker.Checker):
+    """description of class"""
+    def __init__(self, local=True):
+        """ 
+
+            https://www.portraitsofgirls.com/wp-content/uploads/2020/02/alexandra-ola-by-maxim-gagarin-14.jpg
+        """
+        self.local = local  # if local local copy are checked else check online images
+
+    def compute(self, db):
+        if self.local:
+            # check local copy
+            # get path to local images
+            path = db.imagePATH
+
+            # results of checking
+            checked = []
+            missingFiles = []
+            duplicateFiles= []
+
+            # for all item in db
+            for imageURI in db.db:
+                # check if locol copy of image exist
+                filename, ext =utils.img_url_2_file_ext(path,imageURI,minRange=-3,maxRange=-1) 
+                filename = filename+'.'+ext
+                imgOK = os.path.isfile(filename)
+
+                if imgOK:
+                    # check for duplicate
+                    if not filename in checked:
+                        checked.append(filename)
+                    else:
+                        duplicateFiles.append(filename)
+                else:
+                    missingFiles.append(filename)
+
+            # results
+            if len(duplicateFiles)>0: np.savetxt(db.name+"_"+"duplicateImages.csv",duplicateFiles,delimiter=";",fmt="%s",encoding="utf8")
+            if len(missingFiles)>0:np.savetxt(db.name+"_"+"missingImages.csv",missingFiles,delimiter=";",fmt="%s",encoding="utf8")
+
+            # DEBUG
+            # print("DEBUG[POGChecker.compute(",db.name,")::missing images>",len(missingFiles),"]")
+            # print("DEBUG[POGChecker.compute(",db.name,")::duplicate images>",len(duplicateFiles),"]")
+            # print("DEBUG[POGChecker.compute(",db.name,")::",len(db.db)," uri tested ->",len(checked)," local images OK]")
+
+            db.db = np.asarray(checked)
+        else:
+            # check online images
+            pass

+ 1 - 0
_miamG/miam/imageDB/__init__.py

@@ -0,0 +1 @@
+

+ 115 - 0
_miamG/miam/imageDB/utils.py

@@ -0,0 +1,115 @@
+
+def toStringDigit(number, digit):
+    snumber = str(number)
+    return '0'*(digit-len(snumber)) +snumber
+
+def checkString(s,dictConfig):
+    res = []
+    begin=0
+    # prefix
+    prefixes = dictConfig['prefix']
+    for p in prefixes:
+        lenp =len(p)
+        if s[0:lenp]==p:
+            res.append(True)
+            begin = begin + lenp
+            break
+    
+    # digit
+    nbDigit = int(dictConfig['numberDigit'])
+    sDigit =s[begin:begin+nbDigit]
+    if sDigit.isdigit():
+        res.append(True)
+        begin = begin + nbDigit
+
+    # suffix
+    suffixes = dictConfig['suffix']
+    for su in suffixes:
+        lens =len(su)
+        if s[begin:begin+lens]==su:
+            res.append(True)
+            begin =  begin + lens
+
+    # dot
+    if s[begin:begin+1]== '.':
+        res.append(True)
+        begin = begin+1
+
+    # ext
+    exts = dictConfig['ext']
+    for e in exts:
+        if e == s[begin:]:
+            res.append(True)
+            break
+
+    if len(res) == 5:
+        return True
+    else:
+        return False
+
+def filename_to_number(s,dictConfig):
+    res = None
+    # prefix
+    prefix = dictConfig['prefix'][0]
+    lenp =len(prefix)
+
+    # digit
+    nbDigit = int(dictConfig['numberDigit'])
+    sDigit =s[lenp:lenp+nbDigit]
+
+    if sDigit.isdigit():
+        res = int(sDigit)
+
+    return res
+
+def number_to_filenames(num,dictConfig):
+    res = []
+
+    # prefix
+    prefixes = dictConfig['prefix']
+    for p in prefixes:
+        res.append(dictConfig['path']+p)
+    
+    # digit and expo
+    nbDigit = int(dictConfig['numberDigit'])
+    expo = dictConfig['expo']
+
+    newres = []
+    for f in res:
+        for e in expo:
+            expoNum = int(e)
+            sDigit =toStringDigit(num+expoNum, nbDigit)
+            newres.append(f+sDigit)
+    res = newres
+
+    # suffix
+    suffixes = dictConfig['suffix']
+    newres = []
+    for f in res:
+        for su in suffixes: newres.append(f+su)
+    res = newres
+
+    # dot
+    newres = []
+    for f in res: newres.append(f+'.')
+    res= newres
+
+    # ext
+    exts = dictConfig['ext']
+    newres = []
+    for f in res:
+       for e in exts: newres.append(f+e)
+    res = newres
+
+    return res
+
+def img_url_2_file_ext(imagePath,url,minRange=-3,maxRange=-1):
+    dat = url.split('/')[minRange:maxRange]     # split url file name (/) and keep the part between minRange and maxRange
+    retStr = url.split("/")[-1].split(".")      # last element id the image file name
+    retStr = retStr[0:-1]
+    file = '.'.join(retStr)
+    ext = url.split("/")[-1].split(".")[-1]     # image extension
+    retStr = dat+[file]
+    retStr = '_'.join(retStr)
+    retStr = imagePath+"/"+retStr
+    return retStr,ext

+ 65 - 0
_miamG/miam/math/Distance.py

@@ -0,0 +1,65 @@
+# import
+# ------------------------------------------------------------------------------------------
+import miam
+import copy
+import numpy as np
+# ------------------------------------------------------------------------------------------
+# MIAM project 2020
+# ------------------------------------------------------------------------------------------
+# author: remi.cozot@univ-littoral.fr
+# ------------------------------------------------------------------------------------------
+
+class Distance(object):
+    """description of class"""
+    def __init__(self,f):
+        self.distanceFunction = f
+
+    def eval(self, u,v): return self.distanceFunction(u,v)
+
+def cosineDistance(u,v):
+	# normalise u, v
+	u = u / np.sqrt(np.dot(u,u))
+	v = v / np.sqrt(np.dot(v,v))
+	# cosine
+	uv = np.dot(u,v)
+	# return
+	return np.maximum(1 - uv, 0.0)
+
+def L2Distance(u,v):
+    u = np.asarray(u)
+    v = np.asarray(v)
+    # return
+    return np.sqrt(np.dot(u-v,u-v))
+
+def cL2distance(c0,c1):
+    # sorted L2 distanec between palette.colors
+    # no border effect
+    c0, c1 = copy.deepcopy(c0), copy.deepcopy(c1)
+
+    # init iteration
+    totalDist = 0.0
+
+    while (len(c0)>0):
+
+        # init find mininal distance between two colors
+        u, v = c0[0], c1[0]
+        uMv = u-v
+        distMin = np.sqrt(np.dot(uMv,uMv))
+        iMin, jMin =0, 0
+
+        for i in range(len(c0)):
+            for j in range(len(c1)):
+                u, v = c0[i], c1[j]
+                uMv = u-v
+                dist = np.sqrt(np.dot(uMv,uMv))
+
+                if dist < distMin:
+                    distMin, iMin, jMin = dist,i,j
+                else:
+                    pass
+        # remove colors that are closest
+        c0, c1 = np.delete(c0,iMin, axis=0), np.delete(c1,jMin, axis=0)
+        # add to distance
+        totalDist = totalDist + distMin
+    return totalDist
+

+ 37 - 0
_miamG/miam/math/Normalize.py

@@ -0,0 +1,37 @@
+# import
+# ------------------------------------------------------------------------------------------
+import miam
+import copy
+import numpy as np
+# ------------------------------------------------------------------------------------------
+# MIAM project 2020
+# ------------------------------------------------------------------------------------------
+# author: remi.cozot@univ-littoral.fr
+# ------------------------------------------------------------------------------------------
+
+class Normalize(object):
+	"""description of class"""
+	def __init__(self,f):
+		self.normFunction = f
+
+	def eval(self, u):  return self.normFunction(u)	# single vector
+
+	def evals(self, u):								# array of vector 
+		res = np.zeros(u.shape)								
+		for i in range(u.shape[0]): res[i] = self.normFunction(u[i])
+
+		return res
+
+def cosineNorm(u):
+	u1 = copy.deepcopy(u)
+
+	unorm = np.sqrt(np.dot(u1,u1))
+	u1 =u1/unorm if unorm != 0.0 else u1
+
+	return u1
+
+def noNorm(u): return u
+
+def sortNorm(u):
+	u1 = copy.deepcopy(u)
+	return np.asarray(sorted(u1.tolist(), key = lambda u  : np.sqrt(np.dot(u,u))))

+ 1 - 0
_miamG/miam/math/__init__.py

@@ -0,0 +1 @@
+

+ 139 - 0
_miamG/miam/myQtApp.py

@@ -0,0 +1,139 @@
+# import
+# ------------------------------------------------------------------------------------------
+import os, sys, math
+import multiprocessing as mp
+import matplotlib.pyplot as plt
+import numpy as np
+import easygui
+import colour
+
+# import Qt
+from PyQt5.QtWidgets import QMainWindow, QAction, QApplication, QMenu
+from PyQt5.QtWidgets import QWidget, QLabel, QFileDialog 
+from PyQt5.QtWidgets import QHBoxLayout # QSlider
+from PyQt5.QtGui import QIcon, QPixmap, QImage
+from PyQt5 import QtCore
+
+# miam import
+import miam.image.Image as MIMG
+import miam.palette.Palette as MPAL
+import miam.utils
+
+# new import
+import gui.guiView.ImageWidget as gIW
+
+# ------------------------------------------------------------------------------------------
+# MIAM project 2020
+# ------------------------------------------------------------------------------------------
+# author: remi.cozot@univ-littoral.fr
+# ------------------------------------------------------------------------------------------
+
+def getScreenSize():
+    app = QApplication(sys.argv)
+    screens = app.screens()
+
+    for s in screens:
+        n = s.name()
+        size = s.size()
+
+        x = size.width()
+        y = size.height()
+
+        print("screen(",n,"):",x,"x",y)
+
+    app.quit()
+
+    return None
+
+
+def testQt():
+    getScreenSize()
+    pass
+
+class QMainApp(QMainWindow):
+    
+    def __init__(self):
+        super().__init__()
+        
+        self.initUI()
+        
+    def initUI(self):         
+        
+        menubar = self.menuBar()
+        # file menu
+        fileMenu = menubar.addMenu('&File')
+
+        # Create Open action
+        openAction = QAction('&Open', self)        
+        openAction.setShortcut('Ctrl+N')
+        openAction.setStatusTip('Open image')
+        openAction.triggered.connect(self._open)
+        
+        fileMenu.addAction(openAction)
+
+        # Create Save action
+        saveAction = QAction('&Save', self)        
+        saveAction.setShortcut('Ctrl+S')
+        saveAction.setStatusTip('Save image')
+        saveAction.triggered.connect(self._save)
+        
+        fileMenu.addAction(saveAction)
+
+        # Create Quit action
+        quitAction = QAction('&Quit', self)        
+        quitAction.setShortcut('Alt+F4')
+        quitAction.setStatusTip('Quit')
+        quitAction.triggered.connect(self._quit)
+        
+        fileMenu.addAction(quitAction)
+
+        # status bar
+        self.statusBar().showMessage('Welcome to MIAM: Multidimensional Image Aesthetics Model!')
+
+        # geometry
+        base, ratio, scale= 1920, 16/9, 0.5
+        self.setGeometry(0, 0, math.floor(base*scale), math.floor(base/ratio*scale))
+
+        # title
+        self.setWindowTitle('miam')    
+
+        #
+        self.img0 = gIW.ImageWidget(None,filename= '../images/DSC01314-HDR.jpg', mother=self)
+        self.img1 = gIW.ImageWidget(None,filename= '../images/DSC01314.JPG', mother=self)
+        imgLayout = QHBoxLayout()
+        imgLayout.addWidget(self.img0)
+        imgLayout.addWidget(self.img1)
+
+        widget = QWidget()
+        widget.setLayout(imgLayout)
+        self.setCentralWidget(widget)
+
+        # show
+        self.show()
+
+    def _statusMessage(self, s): self.statusBar().showMessage(s)
+
+    def _open(self):
+        self._statusMessage('open!!')
+        fname = QFileDialog.getOpenFileName(self, 'Open file', '../images/')
+        print(fname[0])
+        self.centralWidget()._new(fname[0])
+
+    def _save(self):
+        self._statusMessage('save!!')
+
+    def _quit(self):
+        self._statusMessage('quit!!')
+        sys.exit()
+
+    def resizeEvent(self, event):
+
+        self.img0._resize()
+        self.img1._resize()
+        pass
+
+
+def qtTest2():
+    app = QApplication(sys.argv)
+    ex = QMainApp()
+    sys.exit(app.exec_())

+ 76 - 0
_miamG/miam/pointcloud/PointCloud2D.py

@@ -0,0 +1,76 @@
+import numpy as np
+import alphashape
+import shapely
+#import shapely.geometry
+import matplotlib.pyplot as plt
+
+class PointCloud2D(object):
+    """description of class"""
+    
+    # constructor
+    # -----------------------------------------------------------------------------
+    def __init__(self,x,y):
+        
+        # attibutes
+        self.X = x
+        self.Y = y
+        
+    # methods
+    # -----------------------------------------------------------------------------     
+    def removeIsolatedPoint(self, dist, nbPoint):
+
+        keepX = []
+        keepY = []
+
+        removeX = []
+        removeY = []
+
+        for i in range(len(self.X)):
+
+            x, y=self.X[i], self.Y[i]
+
+            idx = (self.X >( x-dist/2)) & (self.X < (x+dist/2)) & (self.Y > (y-dist/2)) & (self.Y < (y+dist/2))
+            if len(idx[idx==True])> nbPoint:
+                keepX.append(x)
+                keepY.append(y)
+            else:
+                removeX.append(x)
+                removeY.append(y)
+
+        return PointCloud2D(keepX,keepY),PointCloud2D(removeX,removeY)
+
+    def toPoint(self):
+        res = []
+        for i, x in enumerate(self.X):
+            res.append((x,self.Y[i]))
+        return res
+    
+    def contour(self, alpha):
+        return alphashape.alphashape(self.toPoint(), alpha)
+
+    def convexHull(self):
+        return shapely.geometry.MultiPoint(self.toPoint()).convex_hull
+    
+    def plot(self, mark):
+        plt.plot(self.X,self.Y,mark)
+    
+    # class methods
+    # -----------------------------------------------------------------------------
+    def toXYarray(poly):
+        
+        X, Y = [], []
+ 
+        listOfPoints = list(poly.exterior.coords)
+    
+        for xy in listOfPoints:
+            x,y = xy
+            X.append(x)
+            Y.append(y)
+        
+        return X,Y
+
+
+
+
+
+

+ 1 - 0
_miamG/miam/pointcloud/__init__.py

@@ -0,0 +1 @@
+

+ 46 - 0
_miamG/miam/processing/Blend.py

@@ -0,0 +1,46 @@
+# import
+# ------------------------------------------------------------------------------------------
+from . import Processing
+from . import ColorSpaceTransform
+from .. import image
+import colour, copy, skimage
+import numpy as np
+# ------------------------------------------------------------------------------------------
+# MIAM project 2020
+# ------------------------------------------------------------------------------------------
+# author: remi.cozot@univ-littoral.fr
+# ------------------------------------------------------------------------------------------
+
+class Blend(Processing.Processing):
+    """description of class"""
+
+    def __init__(self):
+        pass
+
+    def compute(self, img, **kwargs): 
+        """ 
+            Blend([img], {'alpha':alpha})
+                len([img]) = 2 / alpha in [0,1]
+                img[0]*alpha + img[1]*(1 - alpha)
+
+        """
+        # kwargs
+        alpha = 0.5
+        if kwargs and ('alpha' in kwargs) :
+            alpha = kwargs['alpha']
+
+        if isinstance(img,list) :
+            if len(img) == 2: # two images in the list
+                img0, img1 = img[0], img[1]
+                res = img0*alpha + img1*(1-alpha)
+
+                # HDR or SDR
+                if img0.isHDR() or img1.isHDR():
+                    res.type = image.imageType.imageType.HDR
+
+            else: # not  two images in the list
+                return copy.deepcopy(img[0])
+        else: # not list
+            res = copy.deepcopy(img)
+
+        return res

+ 215 - 0
_miamG/miam/processing/ColorSpaceTransform.py

@@ -0,0 +1,215 @@
+# import
+# ------------------------------------------------------------------------------------------
+from . import Processing
+import colour, copy, os
+import miam.image.Image as MIMG
+import miam.image.ColorSpace as MICS
+import miam.image.imageType
+import miam.histogram.Histogram as MHIST
+import miam.aesthetics.LightnessAesthetics as MAE
+import matplotlib.pyplot as plt
+import numpy as np
+# could be remove after check
+# import skimage.color
+
+# ------------------------------------------------------------------------------------------
+# MIAM project 2020
+# ------------------------------------------------------------------------------------------
+# author: remi.cozot@univ-littoral.fr
+# ------------------------------------------------------------------------------------------
+class ColorSpaceTransform(object):
+    """description of class"""
+
+    def __init__(self):
+        pass
+
+    def compute(self, img, **kwargs): 
+
+        # first create a copy
+        res = copy.deepcopy(img)
+
+        if not kwargs:
+            # " print WARNING message"
+             print("WARNING[miam.processing.ColorSpaceTransform(",img.name,"):", "no destination colour space >> return a copy of image]")
+            
+        else:
+            if not 'dest'in kwargs:
+                print("WARNING[miam.processing.ColorSpaceTransform(",img.name,"):", "no 'dest' colour space >> return a copy of image]")
+
+            else: # -> 'colorSpace' found
+                if  kwargs['dest'] == 'Lab':  
+                    # ---------------------------------------------------------------------------------------------------------------
+                    # DEST: Lab
+                    # ----------------------------------------------------------------------------------------------------------------
+                    currentCS = img.colorSpace.name
+                    # sRGB to Lab
+                    if currentCS=="sRGB":                                                          
+                        # ---------------------------------------------------------------------------------------------------------------
+                        # sRGB to Lab
+                        # ---------------------------------------------------------------------------------------------------------------
+                        if not img.linear: apply_cctf_decoding=True 
+                        else: apply_cctf_decoding=False
+                        
+                        # DEBUG
+                        #print("DEBUG[sRGB > Lab]:apply_cctf_decoding>>",apply_cctf_decoding)
+
+                        # sRGB to XYZ
+                        RGB = res.colorData
+                        XYZ = colour.sRGB_to_XYZ(RGB, illuminant=np.array([ 0.3127, 0.329 ]), chromatic_adaptation_method='CAT02', apply_cctf_decoding=apply_cctf_decoding)           
+                        # XYZ to Lab
+                        Lab = colour.XYZ_to_Lab(XYZ, illuminant=np.array([ 0.3127, 0.329 ]))
+
+                        # update res
+                        res.colorData = Lab
+                        res.linear = None
+                        res.colorSpace = MICS.ColorSpace.buildLab()
+
+                    # XYZ to Lab
+                    elif currentCS=="XYZ":                                                            # XYZ -> Lab
+                        # ---------------------------------------------------------------------------------------------------------------
+                        # XYZ to Lab
+                        # ---------------------------------------------------------------------------------------------------------------
+                           
+                        XYZ = res.colorData
+                        # XYZ to Lab
+                        Lab = colour.XYZ_to_Lab(XYZ, illuminant=np.array([ 0.3127, 0.329 ]))
+
+                        # update res
+                        res.colorData = Lab
+                        res.linear = None
+                        res.colorSpace = MICS.ColorSpace.buildXYZ()
+                    
+                    # Lab to Lab
+                    elif currentCS == "Lab":
+                        # ---------------------------------------------------------------------------------------------------------------
+                        # Lab to Lab
+                        # ---------------------------------------------------------------------------------------------------------------
+                        # return a copy
+                        pass
+
+                elif kwargs['dest'] == 'sRGB':
+                    # ---------------------------------------------------------------------------------------------------------------
+                    # DEST: sRGB
+                    # ----------------------------------------------------------------------------------------------------------------
+                    currentCS = img.colorSpace.name
+
+                    # Lab to sRGB
+                    if currentCS=="Lab":                                         
+                        # ---------------------------------------------------------------------------------------------------------------
+                        # Lab to sRGB
+                        # ---------------------------------------------------------------------------------------------------------------                        
+                        Lab = res.colorData
+                        # Lab to XYZ
+                        XYZ = colour.Lab_to_XYZ(Lab, illuminant=np.array([ 0.3127, 0.329 ]))
+
+                        # XYZ to sRGB
+                        if img.type == MIMG.imageType.imageType.HDR : apply_cctf_encoding = False
+                        else : apply_cctf_encoding = True
+
+                        sRGB = colour.colour.XYZ_to_sRGB(XYZ, illuminant=np.array([ 0.3127, 0.329 ]), chromatic_adaptation_transform='CAT02', apply_cctf_encoding=apply_cctf_encoding)
+
+                        # update res
+                        res.colorData = sRGB
+                        res.colorSpace = MICS.ColorSpace.buildsRGB()
+                        res.linear = not apply_cctf_encoding
+                    
+                    # XYZ to sRGB
+                    elif currentCS == "XYZ":
+                        # ---------------------------------------------------------------------------------------------------------------
+                        # XYZ to sRGB
+                        # --------------------------------------------------------------------------------------------------------------- 
+                        XYZ = res.colorData
+                        # XYZ to sRGB
+                        if img.type == MIMG.imageType.imageType.HDR : apply_cctf_encoding = False
+                        else: apply_cctf_encoding = True
+                        sRGB = colour.colour.XYZ_to_sRGB(XYZ, illuminant=np.array([ 0.3127, 0.329 ]), chromatic_adaptation_transform='CAT02', apply_cctf_encoding=apply_cctf_encoding)
+
+                        # update res
+                        res.colorData = sRGB
+                        res.colorSpace = MICS.ColorSpace.buildsRGB()
+                        res.linear = not apply_cctf_encoding
+    
+                        # sRGB to sRGB
+                    elif currentCS == "sRGB":
+                        # return a copy
+                        pass              
+                    else:
+                        print("WARNING[miam.processing.ColorSpaceTransform(",img.name,"):", "'dest' colour space:",kwargs['dest'] , "not yet implemented !]")
+
+                elif kwargs['dest'] == 'XYZ':
+                    # ---------------------------------------------------------------------------------------------------------------
+                    # DEST: XYZ
+                    # ----------------------------------------------------------------------------------------------------------------
+                    currentCS = img.colorSpace.name
+                    # sRGB to XYZ
+                    if currentCS=="sRGB":                                                          
+                        # ---------------------------------------------------------------------------------------------------------------
+                        # sRGB to XYZ
+                        # ---------------------------------------------------------------------------------------------------------------
+                        if  img.type == MIMG.imageType.imageType.SDR : 
+                            apply_cctf_decoding=True 
+                        else: 
+                            apply_cctf_decoding=False
+                        
+                        # DEBUG
+                        #print("DEBUG[sRGB > XYZ]:apply_cctf_decoding>>",apply_cctf_decoding)
+                        
+                        # sRGB to XYZ
+                        RGB = res.colorData
+                        XYZ = colour.sRGB_to_XYZ(RGB, illuminant=np.array([ 0.3127, 0.329 ]), chromatic_adaptation_method='CAT02', apply_cctf_decoding=apply_cctf_decoding)           
+
+                        # update res
+                        res.colorData = XYZ
+                        res.linear = True
+                        res.colorSpace = MICS.ColorSpace.buildXYZ()
+
+                    # Lab to XYZ
+                    elif currentCS=="XYZ":                                                         
+                        # ---------------------------------------------------------------------------------------------------------------
+                        # XYZ to XYZ
+                        # ---------------------------------------------------------------------------------------------------------------
+                         # return a copy
+                         pass
+                    
+                    # Lab to XYZ
+                    elif currentCS == "Lab":
+                        # ---------------------------------------------------------------------------------------------------------------
+                        # Lab to XYZ
+                        # ---------------------------------------------------------------------------------------------------------------
+                        Lab = res.colorData
+                        # Lab to XYZ
+                        XYZ = colour.Lab_to_XYZ(Lab, illuminant=np.array([ 0.3127, 0.329 ]))
+
+                        # update res
+                        res.colorData = XYZ
+                        res.linear = True
+                        res.colorSpace = MICS.ColorSpace.buildXYZ()
+
+        return res
+
+def XYZ_to_sRGB(XYZ, apply_cctf_encoding=True):
+    RGB =  colour.XYZ_to_sRGB(XYZ, illuminant=np.array([ 0.3127, 0.329 ]), chromatic_adaptation_transform='CAT02', apply_cctf_encoding=apply_cctf_encoding)
+    return RGB
+
+def sRGB_to_XYZ(RGB, apply_cctf_decoding=True):
+    XYZ = colour.sRGB_to_XYZ(RGB, illuminant=np.array([ 0.3127, 0.329 ]), chromatic_adaptation_method='CAT02', apply_cctf_decoding=apply_cctf_decoding)           
+    return XYZ
+
+def Lab_to_XYZ(Lab, apply_cctf_encoding=True):
+    XYZ = colour.Lab_to_XYZ(Lab, illuminant=np.array([ 0.3127, 0.329 ]))
+    return XYZ
+
+def XYZ_to_Lab(XYZ, apply_cctf_decoding=True):
+    Lab = colour.XYZ_to_Lab(XYZ, illuminant=np.array([ 0.3127, 0.329 ]))
+    return Lab
+
+def Lab_to_sRGB(Lab, apply_cctf_encoding=True):
+    XYZ = colour.Lab_to_XYZ(Lab, illuminant=np.array([ 0.3127, 0.329 ]))
+    RGB =  colour.XYZ_to_sRGB(XYZ, illuminant=np.array([ 0.3127, 0.329 ]), chromatic_adaptation_transform='CAT02', apply_cctf_encoding=apply_cctf_encoding)
+    return RGB
+
+def sRGB_to_Lab(RGB, apply_cctf_decoding=True):
+    XYZ = colour.sRGB_to_XYZ(RGB, illuminant=np.array([ 0.3127, 0.329 ]), chromatic_adaptation_method='CAT02', apply_cctf_decoding=apply_cctf_decoding)           
+    Lab = colour.XYZ_to_Lab(XYZ, illuminant=np.array([ 0.3127, 0.329 ]))
+    return Lab
+

+ 85 - 0
_miamG/miam/processing/ContrastControl.py

@@ -0,0 +1,85 @@
+# import
+# ------------------------------------------------------------------------------------------
+from . import Processing
+from . import ColorSpaceTransform
+from .. import image
+import colour, copy 
+import skimage, skimage.morphology, skimage.color, skimage.util, skimage.filters
+import numpy as np
+# ------------------------------------------------------------------------------------------
+# MIAM project 2020
+# ------------------------------------------------------------------------------------------
+# author: remi.cozot@univ-littoral.fr
+# ------------------------------------------------------------------------------------------
+class ContrastControl(Processing.Processing):
+    """description of class"""
+
+    def __init__(self):
+        pass
+
+    def compute(self, img, **kwargs): 
+        res = copy.deepcopy(img)
+        if kwargs:
+            if 'method' in kwargs:
+                if kwargs['method'] == 'stretch':
+                    if 'range' in kwargs:
+                        range = kwargs['range']
+                    else:
+                        range = (2,98)
+                     
+                    p2, p98 = np.percentile(res.colorData, (2, 98))
+                    res.colorData = skimage.exposure.rescale_intensity(res.colorData, in_range=(p2, p98))
+                    
+                elif kwargs['method'] == 'globalEqualization':
+                    res.colorData = skimage.exposure.equalize_hist(res.colorData)
+
+                elif kwargs['method'] == 'adaptativeEqualization':
+                    if 'limit' in kwargs:
+                        limit = kwargs['limit']
+                    else:
+                        limit =0.03
+                    res.colorData = skimage.exposure.equalize_adapthist(res.colorData, clip_limit=limit)
+                elif kwargs['method'] == 'localEqualization':
+                    if 'size' in kwargs:
+                        size = kwargs['size']
+                    else:
+                        size =min(img.shape[0],img.shape[1])
+
+                    #print("ContrastControl.compute():'method'=localEqualization, size=",size)
+                    yuv = skimage.color.rgb2yuv(res.colorData)
+                    y256 = skimage.util.img_as_ubyte(yuv[:,:,0])
+
+                    # equalization with skimage
+                    selem = skimage.morphology.disk(size)
+                    y256_eq = skimage.filters.rank.equalize(y256, selem=selem)
+                    y_eq = skimage.util.img_as_float(y256_eq)
+
+                    # transfert new y
+                    yuv_new = copy.deepcopy(yuv)
+
+                    y_new = yuv_new[:,:,0]
+                    u_new = yuv_new[:,:,1]
+                    v_new = yuv_new[:,:,2]
+
+                    yuv_old = copy.deepcopy(yuv)
+                    y_old = yuv_old[:,:,0]
+                    u_old = yuv_old[:,:,1]
+                    v_old = yuv_old[:,:,2]
+
+                    y_new  = y_eq
+                    y_positive = y_old>0 
+                    u_new[y_positive] = y_eq[y_positive]*u_old[y_positive]/y_old[y_positive]
+                    v_new[y_positive] = y_eq[y_positive]*v_old[y_positive]/y_old[y_positive]
+
+                    yuv_new[:,:,0] = y_new
+                    yuv_new[:,:,1] = u_new
+                    yuv_new[:,:,2] = v_new
+
+                    # go back to rgb
+                    newRGB = skimage.color.yuv2rgb(yuv_new)
+                    newRGB[newRGB>1] =1
+                    newRGB[newRGB<0] = 0
+
+                    res.colorData = newRGB
+                    pass
+        return res

+ 41 - 0
_miamG/miam/processing/Duplicate.py

@@ -0,0 +1,41 @@
+# import
+# ------------------------------------------------------------------------------------------
+from . import Processing
+import copy
+import numpy as np
+# ------------------------------------------------------------------------------------------
+# MIAM project 2020
+# ------------------------------------------------------------------------------------------
+# author: remi.cozot@univ-littoral.fr
+# ------------------------------------------------------------------------------------------
+class Duplicate(Processing.Processing):
+    """description of class"""
+
+    def __init__(self):
+        pass
+
+    def compute(self, img, **kwargs): 
+        """
+        Duplicate image
+        """
+ 
+        nbDuplicate = 1
+
+        if not kwargs: kwargs = {'nb': 1}
+
+        # number of duplicate
+        if 'nb' in kwargs: 
+            # auto or manual mode
+            nbDuplicate = kwargs['nb']
+        else:
+            # if not auto in kwargs -> manual mode
+            nbDuplicate = 1
+
+        res = [] 
+        for i in range(nbDuplicate):
+            res.append(copy.deepcopy(img))
+
+        return res
+
+
+

+ 87 - 0
_miamG/miam/processing/ExposureControl.py

@@ -0,0 +1,87 @@
+# import
+# ------------------------------------------------------------------------------------------
+from . import Processing, Ymap
+from . import ColorSpaceTransform
+from .. import image
+import colour, copy, skimage, math
+import numpy as np
+# ------------------------------------------------------------------------------------------
+# MIAM project 2020
+# ------------------------------------------------------------------------------------------
+# author: remi.cozot@univ-littoral.fr
+# ------------------------------------------------------------------------------------------
+class ExposureControl(Processing.Processing):
+    """description of class"""
+
+    def __init__(self):
+        pass
+
+    def compute(self, img, **kwargs): 
+        """
+        Linear tone mapping operator
+        @params:
+            img         - Required  : hdr image (miam.image.Image)
+            kwargs      - Optionnal : optionnal parameter (dict)
+                'min'               : minimum value under which output is zero (float)
+                'max'               : maximum value above which output is one (float)
+        """
+ 
+        minValue = 0.0
+        maxValue = 1.0
+
+        if not kwargs: kwargs = {'auto': True, 'target': 0.5, 'EV': 0}  # default value
+
+        # auto
+        if 'auto' in kwargs: 
+            # auto or manual mode
+            auto = kwargs['auto']
+        else:
+            # if not auto in kwargs -> manual mode
+            auto = False
+
+        # target or EV value
+        if auto:
+            # auto: target value
+            if 'target' in kwargs:
+                target = kwargs['target']
+            else:
+                target = 0.5
+        
+        # EV value
+        if 'EV' in kwargs:
+            EV = kwargs['EV']
+        else:
+            EV = 0
+        
+        res = copy.deepcopy(img)
+        rgbIn = res.colorData
+
+        if auto: 
+            # compute mean Y (Luminance)
+            y = res.getChannel(image.channel.channel.Y)
+            if res.hasMask():
+                if res.binaryMask(): 
+                    # DEBUG
+                    print("ExposureControl.compute(): BINARY mask taken into account")
+                    ymean = np.mean(y[res.mask==1])
+                else:
+                    # DEBUG
+                    print("ExposureControl.compute(): NON BINARY mask taken into account")
+                    ymean = np.mean(y*res.mask[...,np.newaxis])
+            else: # no mask
+                ymean = np.mean(y)
+            rgbOut = rgbIn*(target/ymean)*math.pow(2,EV)
+        else: 
+            rgbOut = rgbIn*math.pow(2,EV)
+
+
+        # clip if SDR 
+        if (img.type == image.imageType.imageType.SDR):
+            rgbOut[rgbOut>=1.0] = 1.0
+            rgbOut[rgbOut<=0.0] = 0.0
+
+        # update attributes
+        res.colorData       = rgbOut
+
+        return res
+

+ 38 - 0
_miamG/miam/processing/Fuse.py

@@ -0,0 +1,38 @@
+# import
+# ------------------------------------------------------------------------------------------
+from . import Processing
+from . import ColorSpaceTransform
+from .. import image
+import colour, copy, skimage
+import numpy as np
+# ------------------------------------------------------------------------------------------
+# MIAM project 2020
+# ------------------------------------------------------------------------------------------
+# author: remi.cozot@univ-littoral.fr
+# ------------------------------------------------------------------------------------------
+class Fuse(Processing.Processing):
+    """description of class"""
+
+    def __init__(self):
+        pass
+
+    def compute(self, img, **kwargs): 
+        """ 
+            Add([img], {})
+                !! assert !! len([img]) = 2 
+                img[0] + img[1]
+
+        """
+        # kwargs
+
+        if isinstance(img,list) :
+            res = copy.deepcopy(img[0])
+
+            if len(img) > 1: #  at least two images in the list
+                for image in img[1:]:
+                    res= res+image
+
+        else: # not list
+            res = copy.deepcopy(img)
+
+        return res

+ 43 - 0
_miamG/miam/processing/GaussianFilter.py

@@ -0,0 +1,43 @@
+# import
+# ------------------------------------------------------------------------------------------
+from . import Processing
+from .. import image
+import colour, copy 
+from scipy import ndimage
+import numpy as np
+# ------------------------------------------------------------------------------------------
+# MIAM project 2020
+# ------------------------------------------------------------------------------------------
+# author: remi.cozot@univ-littoral.fr
+# ------------------------------------------------------------------------------------------
+class GaussianFilter(Processing.Processing):
+    """description of class"""
+
+    def __init__(self):
+        pass
+
+    def compute(self, img, **kwargs): 
+        """
+        compute method:
+            @params:
+            self    - Required: (MIAM.processing.LaplaceFilter)
+            img:    - Required: image on which Laplace is computed (MIAM.image.IMAGE)
+            kwargs  - Optionnal: optionnal parameter (dict)
+                 'sigma'= scalar or sequence of scalars
+        """
+
+        res = copy.deepcopy(img)
+
+        # taking into account optional parameters
+        if not kwargs: kwargs = {'sigma': 10}  # default value
+
+        sigma = kwargs['sigma']
+
+        # compute lapalce filter
+        res.colorData = imgGaussian=ndimage.gaussian_filter(res.colorData,sigma, mode='reflect', cval=0.0)
+
+
+        return res
+
+
+

+ 41 - 0
_miamG/miam/processing/LaplaceFilter.py

@@ -0,0 +1,41 @@
+# import
+# ------------------------------------------------------------------------------------------
+from . import Processing
+from .. import image
+import colour, copy 
+from scipy import ndimage
+import numpy as np
+# ------------------------------------------------------------------------------------------
+# MIAM project 2020
+# ------------------------------------------------------------------------------------------
+# author: remi.cozot@univ-littoral.fr
+# ------------------------------------------------------------------------------------------
+class LaplaceFilter(Processing.Processing):
+    """description of class"""
+
+    def __init__(self):
+        pass
+
+    def compute(self, img, **kwargs): 
+        """ 
+        compute method:
+        @params:
+            self    - Required: (MIAM.processing.LaplaceFilter)
+            img:    - Required: image on which Laplace is computed (MIAM.image.IMAGE)
+            kwargs  - Optionnal: optionnal parameter (dict)
+    """
+        res = copy.deepcopy(img)
+
+        # taking into account optional parameters
+        if kwargs:
+            pass
+        else:
+            pass
+
+        # compute lapalce filter
+        res.colorData = ndimage.laplace(res.colorData, output=None, mode='reflect', cval=0.0)
+
+        return res
+
+
+

+ 43 - 0
_miamG/miam/processing/MaskSegmentPercentile.py

@@ -0,0 +1,43 @@
+# import
+# ------------------------------------------------------------------------------------------
+from . import Processing
+from . import ColorSpaceTransform
+from .. import image
+import colour, copy, skimage, functools
+import numpy as np
+# ------------------------------------------------------------------------------------------
+# MIAM project 2020
+# ------------------------------------------------------------------------------------------
+# author: remi.cozot@univ-littoral.fr
+# ------------------------------------------------------------------------------------------
+class MaskSegmentPercentile(Processing.Processing):
+    """description of class"""
+
+    def __init__(self):
+        pass
+
+    def compute(self, img, **kwargs): 
+        """ 
+            MaskSegmentPercentile(img, {'percent':[percentileValue], 'channel':'Y'})
+        """
+        # kwargs
+        # percentile values
+        percentileValues = [0,50,100]   # default value
+        if kwargs and ('percent' in kwargs) :  percentileValues = kwargs['percent']
+        # percentile values
+        channel = 'Y' if img.isHDR() else 'L'   # default value
+        if kwargs and ('channel' in kwargs) : channel = kwargs['channel']
+
+        res = []
+
+        for pValmin,pValmax in zip(percentileValues, percentileValues[1:]):
+            seg = copy.deepcopy(img)
+            seg.addMask(one=False)
+            # getChannel
+            c = seg.getChannel(image.channel.channel.toChannel(channel))
+            pMin, pMax = np.percentile(c, (pValmin, pValmax))
+            seg.mask[(c>= pMin) & (c<=pMax) ] = 1
+            res.append(seg)
+        return res
+
+

+ 21 - 0
_miamG/miam/processing/NoOp.py

@@ -0,0 +1,21 @@
+# import
+# ------------------------------------------------------------------------------------------
+from . import Processing
+import copy
+# ------------------------------------------------------------------------------------------
+class NoOp(Processing.Processing):
+    """description of class"""
+
+    def __init__(self):
+        pass
+
+    def compute(self, img, **kwargs): 
+        """
+        Duplicate image
+        """
+
+        res = copy.deepcopy(img)
+
+        return res
+
+

+ 16 - 0
_miamG/miam/processing/Processing.py

@@ -0,0 +1,16 @@
+# import
+# ------------------------------------------------------------------------------------------
+import copy
+
+# ------------------------------------------------------------------------------------------
+# MIAM project 2020
+# ------------------------------------------------------------------------------------------
+# author: remi.cozot@univ-littoral.fr
+# ------------------------------------------------------------------------------------------
+class Processing(object):
+    """tone mapping operator"""
+
+    def compute(self, image, **kwargs):         
+        return copy.deepcopy(image)
+
+

+ 69 - 0
_miamG/miam/processing/SumSquaredLaplace.py

@@ -0,0 +1,69 @@
+# import
+# ------------------------------------------------------------------------------------------
+from . import Processing, LaplaceFilter, GaussianFilter
+from .. import image
+import colour, copy 
+from scipy import ndimage
+import numpy as np
+# ------------------------------------------------------------------------------------------
+# MIAM project 2020
+# ------------------------------------------------------------------------------------------
+# author: remi.cozot@univ-littoral.fr
+# ------------------------------------------------------------------------------------------
+class SumSquaredLaplace(Processing.Processing):
+    """description of class"""
+
+    def __init__(self):
+        pass
+
+    def compute(self, img, **kwargs): 
+        """ 
+        compute method:
+        @params:
+            self    - Required: (MIAM.processing.LaplaceFilter)
+            img:    - Required: image on which Laplace is computed (MIAM.image.IMAGE)
+            kwargs  - Optionnal: optionnal parameter (dict)
+                 TO DO
+   """
+        # taking into account additional parameters
+        if not kwargs: kwargs = {'nbZone': 9, 'preGaussian': True, 'postScaling': True}  # default value
+
+        nbZone, preGaussian, postScaling = kwargs['nbZone'], kwargs['nbZone'], kwargs['postScaling']
+    
+        # pre-processing: gaussian filter
+        if preGaussian:
+            sigmaValue = max(img.colorData.shape[0],img.colorData.shape[1])/nbZone**2
+            img = img.process(GaussianFilter.GaussianFilter(),sigma=sigmaValue)
+                    
+        # laplace filter
+        img =  img.process(LaplaceFilter.LaplaceFilter())
+        # squared laplace
+        img = img**2
+    
+        # sum of channels WARNING: miam.image.Image to numpy
+        sumLaplace2 = np.zeros(img.colorData.shape[0:2])
+        sumLaplace2 = img.colorData[:,:,0]+img.colorData[:,:,1]+img.colorData[:,:,2]
+        sumLaplace2 = sumLaplace2 / np.max(sumLaplace2)
+    
+        # zone and sum
+        af = np.zeros((nbZone,nbZone))          # autofocus map
+        stepW = sumLaplace2.shape[1]/nbZone
+        stepH = sumLaplace2.shape[0]/nbZone
+        for i in range(nbZone):
+            for j in range(nbZone):
+                af[j,i] = np.sum(sumLaplace2[(int(j*stepH)):(int((j+1)*stepH)-1),(int(i*stepW)):(int((i+1)*stepW)-1)])
+
+        # post processing: scaling to One 
+        if postScaling: 
+            af = af/np.max(af)
+        else: #average per pixel
+            nbPixelPerZone = img.colorData.shape[0]*img.colorData.shape[1]
+            af = af/nbPixelPerZone
+     
+        return image.Image.Image(af,
+                                 "SumSquaredLaplace",
+                                 type = image.imageType.imageType.SDR,                  # dot the best idea !
+                                 linear = True,                                         # dot the best idea !
+                                 colorspace = image.ColorSpace.ColorSpace.buildsRGB(),  # dot the best idea !
+                                 scalingFactor = 1)
+

+ 49 - 0
_miamG/miam/processing/TMO_CCTF.py

@@ -0,0 +1,49 @@
+# import
+# ------------------------------------------------------------------------------------------
+from . import Processing
+from .. import image
+import colour, copy
+# ------------------------------------------------------------------------------------------
+# MIAM project 2020
+# ------------------------------------------------------------------------------------------
+# author: remi.cozot@univ-littoral.fr
+# ------------------------------------------------------------------------------------------
+class TMO_CCTF(Processing.Processing):
+    """description of class"""
+
+    def __init__(self):
+        pass
+
+    def compute(self, img, **kwargs): 
+        """
+        CCTF tone mapping operator
+        @params:
+            img         - Required  : hdr image (miam.image.Image)
+            kwargs      - Optionnal : optionnal parameter (dict)
+                'function'          : 'sRGB' (string)
+        """
+
+        if not kwargs:
+            function = 'sRGB'
+        elif 'function' in kwargs:
+            function = kwargs['function']
+        else:
+            function = 'sRGB'
+ 
+        res = copy.deepcopy(img)
+
+        # can tone map HDR only 
+        if (img.type == image.imageType.imageType.HDR):
+
+            # encode
+            imgRGBprime = colour.cctf_encoding(res.colorData,function=function)
+
+            # update attributes
+            res.colorData       = imgRGBprime
+            res.type            = image.imageType.imageType.SDR
+            res.linear          = False
+            res.scalingFactor   = 1.0
+            res.colorSpace      = colour.models.RGB_COLOURSPACES[function].copy()
+
+        return res.clip()
+

+ 90 - 0
_miamG/miam/processing/TMO_Lightness.py

@@ -0,0 +1,90 @@
+# import
+# ------------------------------------------------------------------------------------------
+from . import Processing, Ymap
+from . import ColorSpaceTransform
+from .. import image
+import colour, copy, skimage
+import numpy as np
+# ------------------------------------------------------------------------------------------
+# MIAM project 2020
+# ------------------------------------------------------------------------------------------
+# author: remi.cozot@univ-littoral.fr
+# ------------------------------------------------------------------------------------------
+class TMO_Lightness(Processing.Processing):
+    """description of class"""
+
+    def __init__(self):
+        pass
+
+    def compute(self, img, **kwargs): 
+        """
+        Lightness tone mapping operator
+        @params:
+            img         - Required  : hdr image (miam.image.Image)
+            kwargs      - Optionnal : optionnal parameter (dict)
+                'removeZeros'       : true|false (boolean)
+                'zerosPercentile'   : pertencile of min/max values removed (float)
+        """
+        # take into account optionnal parameters given as dict
+
+        removeZeros = False
+        zerosPercentile = None
+        if kwargs:
+            if 'removeZeros' in kwargs:
+                removeZeros =  kwargs['removeZeros']
+                if removeZeros:
+                    if 'zerosPercentile' in kwags:
+                        zerosPercentile = kwargs['zerosPercentile']
+                    else:
+                        print("WARNING[miam.processing.TMO_Lightness.compute(...): 'removeZeros'=True BUT no 'zerosPercentile' found! >> set to 0.0]")
+                        zerosPercentile = 0.0
+
+        # result: first copy image
+        res = copy.deepcopy(img)
+
+        # can tone map HDR only 
+        if (img.type == image.imageType.imageType.HDR):
+
+            # image preprocessing
+            if removeZeros:
+                if zerosPercentile==0:
+                    res =  res.removeZeros()
+                else:   
+                    res =  res.removeZeros().removeZeros(zerosPercentile)
+
+            # Y cnannel 
+            Ychannel = ColorSpaceTransform.ColorSpaceTransform().compute(res,dest='XYZ').colorData[:,:,1]
+
+            # min max 
+            minY,maxY = np.amin(Ychannel), np.amax(Ychannel)
+            
+            # use Log(Y) as L
+            Lchannel = 100*(np.log10(Ychannel)-np.log10(minY))/(np.log10(maxY)-np.log10(minY))
+
+            # go back to Y
+            Lab = np.zeros(res.shape)
+            Lab[:,:,0] = Lchannel
+            sdrYchannel = colour.Lab_to_XYZ(Lab, illuminant=np.array([ 0.3127, 0.329 ]))[:,:,1]
+
+            # remap Y
+            res = res.process(Ymap.Ymap(),oldY=Ychannel,newY=sdrYchannel)
+
+            # equalize
+            #colorDataEQ = skimage.exposure.equalize_hist(res.colorData)
+
+            # merge
+            #alpha =0.50
+            #res.colorData = alpha*res.colorData + (1-alpha)*colorDataEQ
+            # encode
+            imgRGBprime = colour.cctf_encoding(res.colorData,function='sRGB')
+
+            # update attributes
+            res.colorData       = imgRGBprime
+            res.type            = image.imageType.imageType.SDR
+            res.linear          = False
+            res.scalingFactor   = 1.0
+            res.colorSpace      = colour.models.RGB_COLOURSPACES['sRGB'].copy()
+
+        # reurn results
+        return res.clip()
+

+ 65 - 0
_miamG/miam/processing/TMO_Linear.py

@@ -0,0 +1,65 @@
+# import
+# ------------------------------------------------------------------------------------------
+from . import Processing
+from .. import image
+import colour, copy
+# ------------------------------------------------------------------------------------------
+# MIAM project 2020
+# ------------------------------------------------------------------------------------------
+# author: remi.cozot@univ-littoral.fr
+# ------------------------------------------------------------------------------------------
+class TMO_Linear(Processing.Processing):
+    """description of class"""
+    def __init__(self):
+        pass
+
+    def compute(self, img, **kwargs): 
+        """
+        Linear tone mapping operator
+        @params:
+            img         - Required  : hdr image (miam.image.Image)
+            kwargs      - Optionnal : optionnal parameter (dict)
+                'min'               : minimum value under which output is zero (float)
+                'max'               : maximum value above which output is one (float)
+        """
+ 
+        minValue = 0.0
+        maxValue = 1.0
+
+        if not kwargs:
+            kwargs = {'min': 0.0, 'max': 1.0}
+
+        # min value
+        if 'min' in kwargs:
+            minValue = kwargs['min']
+        else:
+            minValue = 0.0
+
+        # max value
+        if 'max' in kwargs:
+            maxValue = kwargs['max']
+        else:
+            maxValue = 1.0
+ 
+        res = copy.deepcopy(img)
+
+        # can tone map HDR only 
+        if (img.type == image.imageType.imageType.HDR):
+
+            # value between [min,max] -> [0,1]
+            imgRGB = (res.colorData-minValue)/(maxValue-minValue)
+
+            imgRGB[imgRGB>=1.0] = 1.0
+            imgRGB[imgRGB<=0.0] = 0.0
+
+            # encode
+            imgRGBprime = colour.cctf_encoding(imgRGB,function='sRGB')
+
+            # update attributes
+            res.colorData       = imgRGBprime
+            res.type            = image.imageType.imageType.SDR
+            res.linear          = False
+            res.scalingFactor   = 1.0   # res.scalingFactor/(max-min)
+
+        return res
+

+ 54 - 0
_miamG/miam/processing/ToOne.py

@@ -0,0 +1,54 @@
+# import
+# ------------------------------------------------------------------------------------------
+from . import Processing
+from .. import image
+import  copy 
+import numpy as np
+# ------------------------------------------------------------------------------------------
+# MIAM project 2020
+# ------------------------------------------------------------------------------------------
+# author: remi.cozot@univ-littoral.fr
+# ------------------------------------------------------------------------------------------
+class ToOne(Processing.Processing):
+    """description of class"""
+
+    def __init__(self):
+        pass
+
+    def compute(self, img, **kwargs): 
+        """ 
+        compute method:
+        @params:
+            self    - Required: (MIAM.processing.LaplaceFilter)
+            img:    - Required: image on which Laplace is computed (MIAM.image.IMAGE)
+            kwargs  - Optionnal: optionnal parameter (dict)
+                'method'= 'scalingMax' | scalingMinMax | 'crop'
+                    if scalingMax img.colorData = img.colorData/max(img.colorData)
+                    if scalingMinMax img.colorData = (img.colorData  - min(img.colorData))/(max(img.colorData) - min(img.colorData))
+                    if crop img.colorData[img.colorData>1]=1 and img.colorData[img.colorData<0]=0
+
+    """
+        res = copy.deepcopy(img)
+
+        if not kwargs: kwargs = {'method': 'scalingMax'}  # default value
+
+        # taking into account optional parameters
+        if kwargs['method'] == 'scalingMax':
+            minIMG = np.min(res.colorData)
+            res.colorData = res.colorData/maxIMG        
+        elif kwargs['method'] == 'scalingMinMax':
+            minIMG = np.min(res.colorData)
+            maxIMG = np.max(res.colorData)
+            res.colorData = (res.colorData-minIMG)/(maxIMG-minIMG)
+        elif kwargs['method'] == 'crop':
+            res.colorData[res.colorData>1] = 1
+            res.colorData[res.colorData<0] = 0
+        else:
+            print("WARNING[miam.processing.ToOne: unknown 'method'=",kwargs['method'],"]")
+
+        return res
+
+
+
+
+

+ 30 - 0
_miamG/miam/processing/Ymap.py

@@ -0,0 +1,30 @@
+# import
+# ------------------------------------------------------------------------------------------
+from . import Processing
+from . import ColorSpaceTransform
+from .. import image
+import colour, copy, skimage
+import numpy as np
+# ------------------------------------------------------------------------------------------
+# MIAM project 2020
+# ------------------------------------------------------------------------------------------
+# author: remi.cozot@univ-littoral.fr
+# ------------------------------------------------------------------------------------------
+
+class Ymap(Processing.Processing):
+    """description of class"""
+
+    def __init__(self):
+        pass
+
+    def compute(self, img, **kwargs): 
+        res = copy.deepcopy(img)
+        if kwargs and ('oldY' in kwargs) and ('newY' in kwargs):
+            Yold = kwargs['oldY']
+            Ynew = kwargs['newY']
+
+            res.colorData[:,:,0] = res.colorData[:,:,0]*Ynew/Yold
+            res.colorData[:,:,1] = res.colorData[:,:,1]*Ynew/Yold
+            res.colorData[:,:,2] = res.colorData[:,:,2]*Ynew/Yold
+
+        return res

+ 0 - 0
_miamG/miam/processing/__init__.py


+ 33 - 0
_miamG/miam/utils.py

@@ -0,0 +1,33 @@
+import os
+#def printProgressBar (iteration, total, prefix = '', suffix = '', decimals = 1, length = 100, fill = '█'):
+def printProgressBar (iteration, total, prefix = '', suffix = '', decimals = 1, length = 100, fill = '█'):
+        """
+        Call in a loop to create terminal progress bar
+        @params:
+            iteration   - Required  : current iteration (Int)
+            total       - Required  : total iterations (Int)
+            prefix      - Optional  : prefix string (Str)
+            suffix      - Optional  : suffix string (Str)
+            decimals    - Optional  : positive number of decimals in percent complete (Int)
+            length      - Optional  : character length of bar (Int)
+            fill        - Optional  : bar fill character (Str)
+        """
+        percent = ("{0:." + str(decimals) + "f}").format(100 * (iteration / float(total)))
+        filledLength = int(length * iteration // total)
+        bar = fill * filledLength + '-' * (length - filledLength)
+        print('\r%s |%s| %s%% %s' % (prefix, bar, percent, suffix), end = '\r')
+        # Print New Line on Complete
+        if iteration == total: 
+            print()
+
+def splitFileName(filename):
+    path = None
+    name = None
+    ext = None
+
+    (path, name) =os.path.split(filename)
+
+    ext = name.split('.')[-1]
+    name = name.split('.')[0]
+
+    return (path,name, ext)

+ 41 - 0
_miamG/miam/workflow/WFConnector.py

@@ -0,0 +1,41 @@
+# import
+# ------------------------------------------------------------------------------------------
+import os, sys, math
+import multiprocessing as mp
+import matplotlib
+import numpy as np
+import easygui
+import colour
+
+# miam import
+from . import WFNode
+
+import miam.image.Image as MIMG
+import miam.histogram.Histogram as MHIST
+import miam.image.channel
+import miam.utils
+
+# gui import
+
+
+# ------------------------------------------------------------------------------------------
+# MIAM project 2020
+# ------------------------------------------------------------------------------------------
+# author: remi.cozot@univ-littoral.fr
+# ------------------------------------------------------------------------------------------
+class WFConnector(WFNode.WFNode):
+    """description of class"""
+    def __init__(self, name ='image:noname'):
+        super().__init__(name)
+
+        # attributes
+        self.isRoot         = False
+        self.isLeaf         = False
+
+        self.outputOf       = None  # WFProcess
+        self.inputOf        = None  # WFProcess
+
+        self.image          = None  # single Image or list of Image
+
+        self.ready          = False
+

+ 33 - 0
_miamG/miam/workflow/WFNode.py

@@ -0,0 +1,33 @@
+# import
+# ------------------------------------------------------------------------------------------
+import os, sys, math
+import multiprocessing as mp
+import matplotlib
+import numpy as np
+import easygui
+import colour
+
+# miam import
+import miam.image.Image as MIMG
+import miam.histogram.Histogram as MHIST
+import miam.image.channel
+import miam.utils
+
+# gui import
+
+
+# ------------------------------------------------------------------------------------------
+# MIAM project 2020
+# ------------------------------------------------------------------------------------------
+# author: remi.cozot@univ-littoral.fr
+# ------------------------------------------------------------------------------------------
+class WFNode(object):
+    """description of class"""
+    id = 0
+    # constructor
+    def __init__(self,name='node:noname'):
+        self.id = WFNode.id
+        WFNode.id += 1
+        self.name = name
+    pass
+

+ 107 - 0
_miamG/miam/workflow/WFProcess.py

@@ -0,0 +1,107 @@
+# import
+# ------------------------------------------------------------------------------------------
+import os, sys, math, functools
+import multiprocessing as mp
+import matplotlib
+import numpy as np
+import easygui
+import colour
+
+# import Qt
+
+
+# QT matplotlib
+
+# miam import
+from . import WFNode
+
+import miam.image.Image as MIMG
+import miam.histogram.Histogram as MHIST
+import miam.image.channel
+import miam.utils
+
+# gui import
+
+
+# ------------------------------------------------------------------------------------------
+# MIAM project 2020
+# ------------------------------------------------------------------------------------------
+# author: remi.cozot@univ-littoral.fr
+# ------------------------------------------------------------------------------------------
+class WFProcess(WFNode.WFNode):
+    """description of class"""
+    def __init__(self, name = 'processing:noname', process=None):
+        super().__init__(name)
+
+        # attibutes
+        self.inputs = []
+        self.process = process
+        self.parameters ={}
+        self.outputs = []
+
+    def setParameters(self,p):
+        self.parameters = p
+        return self
+
+    def isReady(self):
+        res =  functools.reduce(
+            lambda x,y : x and y,
+            list(map(lambda x: x.ready, self.inputs)),
+            True)
+        print("WFProcess[",self.name," is ready for computation:", res,"]")
+        return res
+
+    def compute(self):
+        # managing input
+        # -----------------------------------------------------------------------------------------
+        # if single input:      take self.inputs[0].image !! note that self.inputs[0].image could be a list
+        # if multiple inputs:    create a list of each input
+        # -----------------------------------------------------------------------------------------
+        if len(self.inputs)== 1:
+            # single input
+            img = self.inputs[0].image
+        else:
+            # multiple inputs
+            img =[]
+            for input in self.inputs:
+                img.append(input.image)
+
+        # compute process object
+        # -----------------------------------------------------------------------------------------
+        # img is single Image or Image list
+        # -----------------------------------------------------------------------------------------
+        print('WFProcess[',self.name,'].compute(',self.parameters,')')
+        resImg = self.process.compute(img,**self.parameters)
+
+        # push results
+        # -----------------------------------------------------------------------------------------
+        # two cases:
+        #   1 - len of resImg is be equal to len of outputs: push one image to output
+        #   2 - len of resImg > 1 and len of output is equal to 1: push image list to single output
+        # -----------------------------------------------------------------------------------------
+        if not isinstance(resImg,list):
+            if len(self.outputs) == 1 :
+                # case 1: just a single Image, just a single ouput
+                self.outputs[0].image = resImg
+                self.outputs[0].ready = True
+            else:
+                # error
+                print("[ERROR] WFProcess(WFNode.WFNode).compute:: resImg is single Image but multiple outputs!")
+        else:
+            # a list of Image
+            if len(resImg) == len(self.outputs):
+                # case 1
+                for i in range(len(self.outputs)):
+                    self.outputs[i].image = resImg[i]
+                    self.outputs[i].ready = True
+
+            else:
+                # case 2
+                self.outputs[0].image = resImg
+                self.outputs[0].ready = True
+
+
+
+
+
+

+ 450 - 0
_miamG/miam/workflow/WFWorkflow.py

@@ -0,0 +1,450 @@
+# import
+# ------------------------------------------------------------------------------------------
+import os, sys, math, json
+import matplotlib.pyplot as plt
+import numpy as np
+import easygui
+
+# miam import
+from . import WFNode, WFProcess, WFConnector
+import miam.image.Image as MIMG
+import miam.histogram.Histogram as MHIST
+import miam.image.channel
+import miam.utils
+
+from miam.processing import (ColorSpaceTransform, ContrastControl, Duplicate, ExposureControl, NoOp,
+                             TMO_CCTF, TMO_Lightness, TMO_Linear, Ymap, Blend, MaskSegmentPercentile,
+                             Fuse
+                             )
+
+# ------------------------------------------------------------------------------------------
+# MIAM project 2020
+# ------------------------------------------------------------------------------------------
+# author: remi.cozot@univ-littoral.fr
+# ------------------------------------------------------------------------------------------
+class WFWorkflow(object):
+    """container of workflow"""
+
+    def __init__(self, name='workflow:noname'):
+        # attributes
+        self.name = name
+
+        # list of processes
+        self.processes =  []
+        # list of images
+        self.connectors = []
+        # root
+        self.root = None
+        # end
+        self.leafs = []
+
+    # methods
+    # ---------------------------------------------
+    # add process, connect, root and leaf
+    # ---------------------------------------------
+    def addProcess(self,p):
+        self.processes.append(p)
+
+        return p
+
+    # connecting processes
+    def connect(self, outputProcess=None, inputProcess=None, name=None):
+        if (not outputProcess) or (not inputProcess):
+            if not outputProcess: print("[ERROR] WFWorkflow.connect(): unknown output !")
+            if not inputProcess: print("[ERROR] WFWorkflow.connect(): unknown input !")
+            return None
+        else: 
+            # outputProcess -- WFImage --> inputProcess
+            outputProcess = self.getByName(outputProcess) if isinstance(outputProcess,str) else outputProcess
+            inputProcess = self.getByName(inputProcess) if isinstance(inputProcess,str) else inputProcess
+
+        if not outputProcess: print("[ERROR] WFWorkflow.connect(): unknown output !")
+        if not inputProcess: print("[ERROR] WFWorkflow.connect(): unknown input !")
+
+        if not name:
+            name = name=outputProcess.name+"->"+inputProcess.name
+        connector = WFConnector.WFConnector(name=name)
+        # link components
+        outputProcess.outputs.append(connector)
+        inputProcess.inputs.append(connector)
+        connector.outputOf = outputProcess
+        connector.inputOf = inputProcess
+
+        self.connectors.append(connector)
+
+        return connector
+
+    def setRoot(self,p):
+        p = self.getByName(p) if isinstance(p,str) else p
+
+        if not p: print("[ERROR] WFWorkflow.setRoot(): unknown process !")
+
+        connector = WFConnector.WFConnector(name="root"+"->"+p.name)
+        connector.isRoot = True
+        self.connectors.append(connector)
+        self.root = connector
+        # link components
+        p.inputs.append(connector)
+        connector.inputOf = p
+
+        return connector
+
+    def setLeaf(self,p):
+        p = self.getByName(p) if isinstance(p,str) else p
+
+        if not p: print("[ERROR] WFWorkflow.setLeaf(): unknown process !")
+
+        connector = WFConnector.WFConnector(name=p.name+"->"+"leaf")
+        connector.isLeaf = True
+        self.connectors.append(connector)
+        self.leafs.append(connector)
+        # link components
+        p.outputs.append(connector)
+        connector.outputOf = p
+
+        return connector
+
+    # ---------------------------------------------
+    # utils method 
+    # ---------------------------------------------
+    def checkName(self, name):
+
+        res = []
+
+        for p in self.processes:
+            if p.name == name: res.append(p)
+
+        for i in self.connectors:
+            if i.name == name: res.append(i)
+
+        return res
+
+    def getByName(self, name):
+        res = None
+
+        asName = self.checkName(name)
+
+        if len(asName)>0: res = asName[0]
+
+        return res
+
+    # ---------------------------------------------
+    # computing
+    # ---------------------------------------------
+    def compute(self, input=None):
+        #
+        #
+        if not input:
+            filename = easygui.fileopenbox(msg="select  image.")
+            print("selected image:", filename)
+            # reading image
+            input= MIMG.Image.readImage(filename)
+            if input.isHDR(): input = input.removeZeros(0.5)
+        self.root.image=input
+
+        # main loop: 
+        # -------------------------------------------------
+        awaitingProcess = []
+        # reset ready for compute
+        for con in self.connectors:
+            con.ready = False
+        # start with root
+        self.root.ready = True
+        # put all processes in awaiting Process
+        awaitingProcess = self.processes
+        # -------------------------------------------------
+        while len(awaitingProcess)>0 : 
+            # ---------------------------------------------
+            # looking for first process in list that all inputs are ready
+            pReady = None
+            pNotReady = []
+            for p in awaitingProcess:
+                if p.isReady() and (not pReady):
+                    # take first ready for computation
+                    pReady = p
+                else: # not ready or firest ready already find
+                    pNotReady.append(p)
+            awaitingProcess = pNotReady
+            pReady.compute()
+
+        
+    def compile(self):
+        # check process input and output
+        print("+-----------------------------------")
+        print("| COMPILE WORKFLOW")        
+        print("+-----------------------------------")
+        print("| check process input and output")
+        print("+-----------------------------------")
+
+        noInput =[]
+        noOutput =[]
+
+        for p in self.processes:
+            print("| process:",p.name)
+            print("|-----------------------------------")
+            print("| input(s)  -> ")
+
+            if len(p.inputs) == 0:
+                # no input
+                noInput.append(p)
+                print("|              ", "no input -> root")
+
+            for input in p.inputs:
+                print("|              ",input.name)
+            print("| output(s) -> ")
+
+            if len(p.outputs) == 0:
+                # no output
+                noOutput.append(p)
+                print("|              ", "no output -> leaf")
+    
+            for output in p.outputs:
+                print("|              ",output.name)            
+            print("+-----------------------------------")
+
+        # root
+        if self.root and (len(noInput)== 0):
+            print("| root:",self.root.name, ": OK")
+            print("+-----------------------------------")
+        if (not self.root) and (len(noInput)== 1):
+            # add Root
+            self.setRoot(noInput[0])
+            print("| root -> pocess:",noInput[0].name)
+            print("| root:",self.root.name, ": OK")
+            print("+-----------------------------------")
+            noInput.pop()
+
+        if not self.root or (len(noInput)>1):
+            print("[ERROR] root error !")
+
+        # leaf
+        for p in noOutput:
+            self.setLeaf(p)
+            print("| process:",p.name, "- > leaf")
+        if len(self.leafs)>0:
+            for c in self.leafs:
+                print("+-----------------------------------")
+                print("| leaf:",c.name)
+            print("| leaf: OK")
+            print("+-----------------------------------")
+        else:
+            print("[ERROR] leaf error !")
+
+        # simulate processing
+        # -------------------------------------------------
+        print("| check workflow")
+        print("+-----------------------------------")
+        awaitingProcess = []
+        # reset ready for compute
+        for con in self.connectors:
+            con.ready = False
+        # start with root
+        self.root.ready = True
+        # put all processes in awaiting Process
+        awaitingProcess = self.processes
+        # -------------------------------------------------
+        while len(awaitingProcess)>0 : 
+            # ---------------------------------------------
+            # looking for first process in list that all inputs are ready
+            pReady = None
+            pNotReady = []
+            for p in awaitingProcess:
+                if p.isReady() and (not pReady):
+                    # take first ready for computation
+                    pReady = p
+                else: # not ready or firest ready already find
+                    pNotReady.append(p)
+            awaitingProcess = pNotReady
+
+            # 'compute'
+            print("+-----------------------------------")
+            print("| computing:", pReady.name)        
+            # set ouputs to ready
+            for con in pReady.outputs:
+                con.ready = True
+                print("|    +---->", con.name, "ready")        
+
+        print("+-----------------------------------")
+        print("| check workflow: end reached: OK")
+        print("+-----------------------------------")
+
+        return self
+
+    # ---------------------------------------------
+    # read
+    # ---------------------------------------------
+    def readWorkflow(filename):
+
+        # read filename
+        with open(filename) as json_file: 
+            jsonData = json.load(json_file)
+
+        # recover data
+        # name
+        wf_name = jsonData['name'] if 'name' in jsonData else filename
+        # processes and connectors
+        processesJSON = jsonData['processes'] if 'processes' in jsonData else []
+        connectorsJSON = jsonData['connectors'] if 'connectors' in jsonData else []
+
+        # build process and connectors
+        error = False
+
+        # create wf
+        wf = WFWorkflow(name=wf_name)
+
+        # build process
+        for processJSON in processesJSON:
+            # type, name, params
+            if 'type' in processJSON :
+                type= processJSON['type']
+            else:
+                print("ERROR[miam.workflow.WFWorflow.readWorkflow(",processJSON,"): no type found !]")
+                error = True
+            if 'name' in processJSON:
+                process_name= processJSON['name']
+            else:
+                print("ERROR[miam.workflow.WFWorflow.readWorkflow(",processJSON,"): no name found !]")
+                error = True
+            if 'parameters' in processJSON:
+                parameters= processJSON['parameters']
+            else:
+                print("ERROR[miam.workflow.WFWorflow.readWorkflow(",processJSON,"): no parameters found !]")
+                error = True
+            # build process
+            if not error:
+
+                # add here all new processing class
+                # --------------------------------------------
+                # ColorSpaceTransform
+                # ContrastControl
+                # Duplicate
+                # ExposureControl
+                # NoOp
+                # TMO_CCTF
+                # TMO_Lightness
+                # TMO_Linear
+                # Ymap
+                # Blend
+                # MaskSegmentPercentile
+                # Fuse
+                # --------------------------------------------
+
+                if type =='ColorSpaceTransform':
+                   wf.addProcess(WFProcess.WFProcess(
+                        name = process_name, 
+                        process=ColorSpaceTransform.ColorSpaceTransform()).setParameters(eval(parameters))
+                    )
+                elif type == 'ContrastControl':
+                   wf.addProcess(WFProcess.WFProcess(
+                        name = process_name, 
+                        process=ContrastControl.ContrastControl()).setParameters(eval(parameters))
+                    )
+                elif type == 'Duplicate':
+                   wf.addProcess(WFProcess.WFProcess(
+                        name = process_name, 
+                        process=Duplicate.Duplicate()).setParameters(eval(parameters))
+                    )
+                elif type == 'ExposureControl':
+                   wf.addProcess(WFProcess.WFProcess(
+                        name = process_name, 
+                        process=ExposureControl.ExposureControl()).setParameters(eval(parameters))
+                    )
+                elif type == 'NoOp':
+                    wf.addProcess(WFProcess.WFProcess(
+                        name = process_name, 
+                        process=NoOp.NoOp())
+                    )
+                elif type == 'TMO_CCTF':
+                    wf.addProcess(WFProcess.WFProcess(
+                        name = process_name, 
+                        process=TMO_CCTF.TMO_CCTF()).setParameters(eval(parameters))
+                    )
+                elif type == 'TMO_Lightness':
+                    wf.addProcess(WFProcess.WFProcess(
+                        name = process_name, 
+                        process=TMO_Lightness.TMO_Lightness()).setParameters(eval(parameters))
+                    )
+                elif type == 'TMO_Linear':
+                    wf.addProcess(WFProcess.WFProcess(
+                        name = process_name, 
+                        process=TMO_Linear.TMO_Linear()).setParameters(eval(parameters))
+                    )
+                elif type == 'Ymap':
+                    wf.addProcess(WFProcess.WFProcess(
+                        name = process_name, 
+                        process=Ymap.Ymap()).setParameters(eval(parameters))
+                    )
+                elif type == 'Blend':
+                    wf.addProcess(WFProcess.WFProcess(
+                        name = process_name, 
+                        process=Blend.Blend()).setParameters(eval(parameters))
+                    )
+                elif type == 'MaskSegmentPercentile':
+                    wf.addProcess(WFProcess.WFProcess(
+                        name = process_name, 
+                        process=MaskSegmentPercentile.MaskSegmentPercentile()).setParameters(eval(parameters))
+                    )
+                elif type == 'Fuse':
+                    wf.addProcess(WFProcess.WFProcess(
+                        name = process_name, 
+                        process=Fuse.Fuse()).setParameters(eval(parameters))
+                    )
+                else:
+                    print("ERROR[miam.workflow.WFWorflow.readWorkflow(",filename,"): unkown process type: ",type," !]")
+                    error = True
+
+        # build connector
+        for connectorJSON in connectorsJSON:
+            # outputProcess -> inputProcess
+            if 'outputProcess' in connectorJSON :
+                outputProcess = connectorJSON['outputProcess']
+            else:
+                print("ERROR[miam.workflow.WFWorflow.readWorkflow(",connectorJSON,"): no outputProcess found !]")
+                error = True
+
+            if 'inputProcess' in connectorJSON:
+                inputProcess= connectorJSON['inputProcess']
+            else:
+                print("ERROR[miam.workflow.WFWorflow.readWorkflow(",connectorJSON,"): no inputProcess found !]")
+                error = True
+
+            # find output and input
+            pout = wf.getByName(outputProcess)
+            pin = wf.getByName(inputProcess)
+
+            wf.connect(outputProcess=pout, inputProcess=pin, name=outputProcess+"->"+inputProcess)
+
+        # compile and return
+        return  wf.compile()
+
+    # ---------------------------------------------
+    # display
+    # ---------------------------------------------
+    def display(self, withHistogram=True):
+        # number of leaf image
+        nbRes = len(self.leafs)
+        # dual display
+        dd = 2 if withHistogram else 1
+
+        fig, ax = plt.subplots(2,nbRes*dd)
+
+        # display input
+        self.root.image.plot(ax[0,0])
+        if withHistogram:
+            if self.root.image.isHDR():
+                ch = miam.image.channel.channel.Y
+            else:
+                ch = miam.image.channel.channel.L
+            MHIST.Histogram.build(self.root.image, ch).plot(ax[0,1])
+        
+        # display results
+        for i, leaf in enumerate(self.leafs):
+            leaf.image.plot(ax[1,i*dd])
+            if withHistogram:
+                if leaf.image.isHDR():
+                    ch = miam.image.channel.channel.Y
+                else:
+                    ch = miam.image.channel.channel.L
+            MHIST.Histogram.build(leaf.image, ch).plot(ax[1,i*dd+1])
+
+        plt.show(block=True)

+ 1 - 0
_miamG/miam/workflow/__init__.py

@@ -0,0 +1 @@
+