| 
          from __future__ import print_function | 
        
        
           | 
          
 | 
        
        
           | 
          import cv2 | 
        
        
           | 
          import copy | 
        
        
           | 
          import math | 
        
        
           | 
          import numpy as np | 
        
        
           | 
          import os | 
        
        
           | 
          import sys | 
        
        
           | 
          
 | 
        
        
           | 
          # Detect OpenCV 2.x vs 3.x | 
        
        
           | 
          from pkg_resources import parse_version | 
        
        
           | 
          IS_OPENCV_2 = parse_version(cv2.__version__) < parse_version('3.0.0') | 
        
        
           | 
          
 | 
        
        
           | 
          # Alias BoxPoints as this lives in a different place in OpenCV 2 and 3 | 
        
        
           | 
          if IS_OPENCV_2: | 
        
        
           | 
              BoxPoints = cv2.cv.BoxPoints | 
        
        
           | 
          else: | 
        
        
           | 
              BoxPoints = cv2.boxPoints | 
        
        
           | 
          
 | 
        
        
           | 
          
 | 
        
        
           | 
          # Detection settings | 
        
        
           | 
          MAX_COVERAGE = 0.98 | 
        
        
           | 
          INSET_PERCENT = 0.005 | 
        
        
           | 
          
 | 
        
        
           | 
          def thresholdImage(img, lowerThresh, ignoreMask): | 
        
        
           | 
              _, binary = cv2.threshold(img, lowerThresh, 255, cv2.THRESH_BINARY_INV) # THRESH_TOZERO_INV | 
        
        
           | 
              # binary = cv2.bitwise_not(binary) | 
        
        
           | 
          
 | 
        
        
           | 
              binary = cv2.bitwise_and(ignoreMask, binary) | 
        
        
           | 
          
 | 
        
        
           | 
              # Prevent tiny outlier collections of pixels spoiling the rect fitting | 
        
        
           | 
              kernel = np.ones((5,5),np.uint8) | 
        
        
           | 
              binary = cv2.dilate(binary, kernel, iterations = 3) | 
        
        
           | 
              binary = cv2.erode(binary, kernel, iterations = 3) | 
        
        
           | 
          
 | 
        
        
           | 
              return binary | 
        
        
           | 
          
 | 
        
        
           | 
          def findLargestContourRect(binary): | 
        
        
           | 
              largestRect = None | 
        
        
           | 
              largestArea = 0 | 
        
        
           | 
          
 | 
        
        
           | 
              # Find external contours of all shapes | 
        
        
           | 
              if IS_OPENCV_2: | 
        
        
           | 
                  contours,_ = cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) | 
        
        
           | 
              else: | 
        
        
           | 
                  _, contours, hierarchy = cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) | 
        
        
           | 
          
 | 
        
        
           | 
              for cnt in contours: | 
        
        
           | 
                  area = cv2.contourArea(cnt) | 
        
        
           | 
          
 | 
        
        
           | 
                  # Keep track of the largest area seen | 
        
        
           | 
                  if area > largestArea: | 
        
        
           | 
                      largestArea = area | 
        
        
           | 
                      largestRect = cv2.minAreaRect(cnt) | 
        
        
           | 
          
 | 
        
        
           | 
              return largestRect, largestArea | 
        
        
           | 
          
 | 
        
        
           | 
          def findNonZeroPixelsRect(binary): | 
        
        
           | 
              edges = copy.copy(binary) | 
        
        
           | 
          
 | 
        
        
           | 
              if IS_OPENCV_2: | 
        
        
           | 
                  contours,_ = cv2.findContours(edges, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) | 
        
        
           | 
              else: | 
        
        
           | 
                  _, contours, hierarchy = cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) | 
        
        
           | 
          
 | 
        
        
           | 
              nonZero = cv2.findNonZero(edges) | 
        
        
           | 
          
 | 
        
        
           | 
              if nonZero is None: | 
        
        
           | 
                  return None, 0, binary | 
        
        
           | 
          
 | 
        
        
           | 
              rect = cv2.minAreaRect(nonZero) | 
        
        
           | 
          
 | 
        
        
           | 
              area = rect[1][0] * rect[1][1] | 
        
        
           | 
          
 | 
        
        
           | 
              return rect, area | 
        
        
           | 
          
 | 
        
        
           | 
          def normaliseRectRotation(rawRects): | 
        
        
           | 
              """ | 
        
        
           | 
              Normalize rect orientation to have an angle between -45 and 45 degrees | 
        
        
           | 
          
 | 
        
        
           | 
              Rects generated by OpenCV are can be "portrait" with a near -90 angle and flipped height/width | 
        
        
           | 
              To combine and compare rects meaningfully, they need to all have the same orientation. | 
        
        
           | 
              """ | 
        
        
           | 
              rects = [] | 
        
        
           | 
          
 | 
        
        
           | 
              for rect in rawRects: | 
        
        
           | 
                  center = rect[0] | 
        
        
           | 
                  size = rect[1] | 
        
        
           | 
                  angle = rect[2] | 
        
        
           | 
          
 | 
        
        
           | 
                  if angle < -45: | 
        
        
           | 
                      rect = ( | 
        
        
           | 
                          center, | 
        
        
           | 
                          (size[1], size[0]), | 
        
        
           | 
                          angle + 90 | 
        
        
           | 
                      ) | 
        
        
           | 
          
 | 
        
        
           | 
                  rects.append(rect) | 
        
        
           | 
          
 | 
        
        
           | 
              return rects | 
        
        
           | 
          
 | 
        
        
           | 
          def medianRect(rects): | 
        
        
           | 
              if len(rects) == 0: | 
        
        
           | 
                  return None | 
        
        
           | 
          
 | 
        
        
           | 
              rects = normaliseRectRotation(rects) | 
        
        
           | 
          
 | 
        
        
           | 
              # Sort rects by area | 
        
        
           | 
              rects.sort(key=lambda rect: rect[1][0] * rect[1][1]) | 
        
        
           | 
          
 | 
        
        
           | 
              median = ( | 
        
        
           | 
                  (np.median([r[0][0] for r in rects]), np.median([r[0][1] for r in rects])), | 
        
        
           | 
                  (np.median([r[1][0] for r in rects]), np.median([r[1][1] for r in rects])), | 
        
        
           | 
                  np.median([r[2] for r in rects]) | 
        
        
           | 
              ) | 
        
        
           | 
          
 | 
        
        
           | 
              return median | 
        
        
           | 
          
 | 
        
        
           | 
          def correctAspectRatio(rect, targetRatio = 1.5, maxDifference = 0.3): | 
        
        
           | 
              """ | 
        
        
           | 
              Return an aspect-ratio corrected rect (and success flag) | 
        
        
           | 
          
 | 
        
        
           | 
              Args: | 
        
        
           | 
                  rect (OpenCV RotatedRect struct) | 
        
        
           | 
                  targetRatio (float): Ratio represented as the larger image dimension divided by the smaller one | 
        
        
           | 
          
 | 
        
        
           | 
              """ | 
        
        
           | 
              # Indexes into the rect nested tuple | 
        
        
           | 
              CENTER = 0; SIZE = 1; ANGLE = 2 | 
        
        
           | 
              X = 0; Y = 1; | 
        
        
           | 
          
 | 
        
        
           | 
              size = rect[SIZE] | 
        
        
           | 
          
 | 
        
        
           | 
              aspectRatio = max(size[X], size[Y]) / float(min(size[X], size[Y])) | 
        
        
           | 
              aspectError = targetRatio - aspectRatio | 
        
        
           | 
          
 | 
        
        
           | 
              # Factor out orientation to simplify logic below | 
        
        
           | 
              # This assumes the larger dimension as X | 
        
        
           | 
              if size[X] == max(size[X], size[Y]): | 
        
        
           | 
                  rectWidth = size[X] | 
        
        
           | 
                  rectHeight = size[Y] | 
        
        
           | 
                  widthDim = X | 
        
        
           | 
                  heightDim = Y | 
        
        
           | 
              else: | 
        
        
           | 
                  rectHeight = size[X] | 
        
        
           | 
                  rectWidth = size[Y] | 
        
        
           | 
                  widthDim = Y | 
        
        
           | 
                  heightDim = X | 
        
        
           | 
          
 | 
        
        
           | 
              # Only attempt to correct aspect ratio where the ROI is roughly right already | 
        
        
           | 
              # This prevents odd results for poor outline detection | 
        
        
           | 
              if abs(aspectError) > maxDifference: | 
        
        
           | 
                  return rect, False | 
        
        
           | 
          
 | 
        
        
           | 
              # Shrink width if the ratio was too wide | 
        
        
           | 
              if aspectRatio > targetRatio: | 
        
        
           | 
                  print("ratio too large", aspectError) | 
        
        
           | 
                  rectWidth = size[heightDim] * targetRatio | 
        
        
           | 
          
 | 
        
        
           | 
              # Shrink height if the ratio was too tall | 
        
        
           | 
              elif aspectRatio < targetRatio: | 
        
        
           | 
                  print("ratio too small", aspectError) | 
        
        
           | 
                  # rectWidth = size[heightDim] * targetRatio | 
        
        
           | 
                  rectHeight = size[widthDim] / targetRatio | 
        
        
           | 
          
 | 
        
        
           | 
              # Apply new width/height in the original orientation | 
        
        
           | 
              if widthDim == X: | 
        
        
           | 
                  newSize = (rectWidth, rectHeight) | 
        
        
           | 
              else: | 
        
        
           | 
                  newSize = (rectHeight, rectWidth) | 
        
        
           | 
          
 | 
        
        
           | 
              newRect = (rect[CENTER], newSize, rect[ANGLE]) | 
        
        
           | 
          
 | 
        
        
           | 
              return newRect, True | 
        
        
           | 
          
 | 
        
        
           | 
          def findExposureBounds(img, showOutputWindow=False): | 
        
        
           | 
              gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) | 
        
        
           | 
          
 | 
        
        
           | 
              # Smooth out noise | 
        
        
           | 
              # gray = cv2.GaussianBlur(gray,(5,5),0) | 
        
        
           | 
              gray = cv2.bilateralFilter(gray, 11, 17, 17) | 
        
        
           | 
          
 | 
        
        
           | 
              # Maximise brightness range | 
        
        
           | 
              gray = cv2.equalizeHist(gray) | 
        
        
           | 
          
 | 
        
        
           | 
          
 | 
        
        
           | 
              # Create a mask to ignore the brightest spots | 
        
        
           | 
              # These are usually where there is no film covering the light source | 
        
        
           | 
              _, ignoreMask = cv2.threshold(gray, 240, 255, cv2.THRESH_BINARY) | 
        
        
           | 
          
 | 
        
        
           | 
              # Expand masked out area slightly to include adjacent edges | 
        
        
           | 
              kernel = np.ones((3,3),np.uint8) | 
        
        
           | 
              ignoreMask = cv2.dilate(ignoreMask, kernel, iterations = 3) | 
        
        
           | 
          
 | 
        
        
           | 
          
 | 
        
        
           | 
              # Create a mask to ignore areas of low saturation | 
        
        
           | 
              # When white balanced against the film stock, this is usually low saturation | 
        
        
           | 
              hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV) | 
        
        
           | 
              hsv = cv2.GaussianBlur(hsv, (5,5), 0) | 
        
        
           | 
          
 | 
        
        
           | 
              satMask = cv2.inRange(hsv, (0, 0, 0), (255, 7, 255)) | 
        
        
           | 
          
 | 
        
        
           | 
              # Combine saturation and brightness masks, then flip | 
        
        
           | 
              ignoreMask = cv2.bitwise_or(ignoreMask, satMask) | 
        
        
           | 
              ignoreMask = cv2.bitwise_not(ignoreMask) | 
        
        
           | 
          
 | 
        
        
           | 
          
 | 
        
        
           | 
              # Get min/max region of interest areas | 
        
        
           | 
              height, width, _  = img.shape | 
        
        
           | 
              maxArea = (height * MAX_COVERAGE)  * (width * MAX_COVERAGE) | 
        
        
           | 
          
 | 
        
        
           | 
              minCaptureArea = maxArea * 0.65 | 
        
        
           | 
          
 | 
        
        
           | 
              # algos = [findNonZeroPixelsRect] | 
        
        
           | 
              algos = [findLargestContourRect] | 
        
        
           | 
          
 | 
        
        
           | 
              results = [] | 
        
        
           | 
          
 | 
        
        
           | 
              for func in algos: | 
        
        
           | 
                  lowerThreshold = 0 | 
        
        
           | 
                  while lowerThreshold < 240: | 
        
        
           | 
                      binary = thresholdImage(gray, lowerThreshold, ignoreMask) | 
        
        
           | 
          
 | 
        
        
           | 
                      debugImg = cv2.cvtColor(binary, cv2.COLOR_GRAY2BGR) | 
        
        
           | 
                      rect, area = func(binary) | 
        
        
           | 
          
 | 
        
        
           | 
                      # Stop once a valid result is returned | 
        
        
           | 
                      if area >= maxArea: | 
        
        
           | 
                          break | 
        
        
           | 
          
 | 
        
        
           | 
                      if area >= minCaptureArea: | 
        
        
           | 
                          results.append(rect) | 
        
        
           | 
                          lowerThreshold += 5 | 
        
        
           | 
          
 | 
        
        
           | 
                          # Draw in green for results that are collected | 
        
        
           | 
                          debugLineColour = (0, 255, 0) | 
        
        
           | 
          
 | 
        
        
           | 
                      else: | 
        
        
           | 
                          lowerThreshold += 5 | 
        
        
           | 
          
 | 
        
        
           | 
                          # Draw in red for areas that were too small | 
        
        
           | 
                          debugLineColour = (0, 0, 255) | 
        
        
           | 
          
 | 
        
        
           | 
          
 | 
        
        
           | 
                      if showOutputWindow: | 
        
        
           | 
                          if rect is not None: | 
        
        
           | 
                              # Get a rectangle around the contour | 
        
        
           | 
          
 | 
        
        
           | 
                              rectPoints = BoxPoints(rect) | 
        
        
           | 
                              rectPoints = np.int0(rectPoints) | 
        
        
           | 
          
 | 
        
        
           | 
                              cv2.drawContours(debugImg, [rectPoints], -1, debugLineColour, 3) | 
        
        
           | 
          
 | 
        
        
           | 
                          # Draw threshold on debug output | 
        
        
           | 
                          cv2.putText( | 
        
        
           | 
                              img=debugImg, | 
        
        
           | 
                              text='Threshold: ' + str(lowerThreshold), | 
        
        
           | 
                              org=(20, 30), | 
        
        
           | 
                              fontFace=cv2.FONT_HERSHEY_PLAIN, | 
        
        
           | 
                              fontScale=2, | 
        
        
           | 
                              color=(0, 150, 255), | 
        
        
           | 
                              lineType=4 | 
        
        
           | 
                          ) | 
        
        
           | 
          
 | 
        
        
           | 
                          cv2.imshow('image', cv2.resize(debugImg, (0,0), fx=0.75, fy=0.75) ) | 
        
        
           | 
                          cv2.waitKey(1) | 
        
        
           | 
          
 | 
        
        
           | 
              return medianRect(results) | 
        
        
           | 
          
 | 
        
        
           | 
          
 | 
        
        
           | 
          if __name__ == '__main__': | 
        
        
           | 
          
 | 
        
        
           | 
              import argparse | 
        
        
           | 
          
 | 
        
        
           | 
              parser = argparse.ArgumentParser(description='Find crop for film negative scan') | 
        
        
           | 
          
 | 
        
        
           | 
              parser.add_argument('files', nargs='+', help='Image files to perform detection on (JPG, PNG, etc)') | 
        
        
           | 
          
 | 
        
        
           | 
              args = parser.parse_args() | 
        
        
           | 
          
 | 
        
        
           | 
              hasDisplay = os.getenv('DISPLAY') != None | 
        
        
           | 
          
 | 
        
        
           | 
              for filename in args.files: | 
        
        
           | 
                  if not os.path.exists(filename): | 
        
        
           | 
                      print("ERROR:") | 
        
        
           | 
                      print("Could not find file '%s'" % filename) | 
        
        
           | 
                      sys.exit(5) | 
        
        
           | 
          
 | 
        
        
           | 
                  # read image and convert to gray | 
        
        
           | 
                  img = cv2.imread(filename, cv2.IMREAD_UNCHANGED) | 
        
        
           | 
          
 | 
        
        
           | 
                  # cv2.imshow('image', cv2.resize(img, (0,0), fx=0.75, fy=0.75) ) | 
        
        
           | 
                  # cv2.waitKey(0) | 
        
        
           | 
          
 | 
        
        
           | 
                  rawRect = findExposureBounds(img, showOutputWindow=hasDisplay) | 
        
        
           | 
          
 | 
        
        
           | 
                  # Outputs for Lightroom | 
        
        
           | 
                  cropLeft = 0 | 
        
        
           | 
                  cropRight = 1.0 | 
        
        
           | 
                  cropTop = 0 | 
        
        
           | 
                  cropBottom = 1.0 | 
        
        
           | 
                  rotation = 0 | 
        
        
           | 
          
 | 
        
        
           | 
                  if rawRect is not None: | 
        
        
           | 
                      # Average height and width of the detected area to get a constant inset | 
        
        
           | 
                      insetPixels = ((rawRect[1][0] + rawRect[1][1]) / 2.0) * INSET_PERCENT | 
        
        
           | 
          
 | 
        
        
           | 
                      insetRect = ( | 
        
        
           | 
                          rawRect[0], # Center | 
        
        
           | 
                          (rawRect[1][0] - insetPixels, rawRect[1][1] - insetPixels), # Size | 
        
        
           | 
                          rawRect[2] # Rotation | 
        
        
           | 
                      ) | 
        
        
           | 
          
 | 
        
        
           | 
                      rect, aspectChanged = correctAspectRatio(insetRect) | 
        
        
           | 
          
 | 
        
        
           | 
                      boxWidth = rect[1][0] | 
        
        
           | 
                      boxHeight = rect[1][1] | 
        
        
           | 
          
 | 
        
        
           | 
                      box = np.int0(BoxPoints(rect)) | 
        
        
           | 
          
 | 
        
        
           | 
          
 | 
        
        
           | 
                      # # Create a mask that excludes areas that are probably the directly visible light source | 
        
        
           | 
                      # _, wbMask = cv2.threshold(gray, 253, 0, cv2.THRESH_TOZERO) | 
        
        
           | 
                      # wbMask = cv2.bitwise_not(wbMask) | 
        
        
           | 
          
 | 
        
        
           | 
                      # # Mask out the detected frame - we only want to look at the base film layer | 
        
        
           | 
                      # cv2.fillConvexPoly(wbMask, box, 0) | 
        
        
           | 
          
 | 
        
        
           | 
                      # # cv2.imshow('image', wbMask ) | 
        
        
           | 
                      # # cv2.waitKey(0) | 
        
        
           | 
          
 | 
        
        
           | 
                      # # bgr = cv2.mean(img, wbMask) | 
        
        
           | 
                      # lab = cv2.mean(cv2.cvtColor(img, cv2.COLOR_BGR2LAB), wbMask) | 
        
        
           | 
          
 | 
        
        
           | 
                      # # print [i for i in reversed(bgr)] | 
        
        
           | 
                      # tint = lab[1] - 127 | 
        
        
           | 
                      # temperature = lab[2] - 127 | 
        
        
           | 
                      # print (lab[0]/255.0)*100, temperature, tint | 
        
        
           | 
          
 | 
        
        
           | 
          
 | 
        
        
           | 
                      # Lightroom doesn't support rotation more than 45 degrees | 
        
        
           | 
                      # The detected rect usually includes a 90 degree rotation for landscape images | 
        
        
           | 
                      rotation = -rect[2] | 
        
        
           | 
          
 | 
        
        
           | 
                      if rotation > 45: | 
        
        
           | 
                          rotation -= 90 | 
        
        
           | 
                      elif rotation < -90: | 
        
        
           | 
                          rotation += 45 | 
        
        
           | 
          
 | 
        
        
           | 
                      # Calculate crops in a format for Lightroom (0.0 to 1.0 for each edge) | 
        
        
           | 
                      centerX = rect[0][0] | 
        
        
           | 
                      centerY = rect[0][1] | 
        
        
           | 
          
 | 
        
        
           | 
                      # Use the average distance from each side as the crop in Lightroom | 
        
        
           | 
                      imgHeight, imgWidth, _  = img.shape | 
        
        
           | 
          
 | 
        
        
           | 
                      top = []; left = []; right = []; bottom =[] | 
        
        
           | 
          
 | 
        
        
           | 
                      for point in box: | 
        
        
           | 
                          # point = rotateAroundPoint(point, math.radians(rotation)) | 
        
        
           | 
          
 | 
        
        
           | 
                          if point[0] > centerX: | 
        
        
           | 
                              right.append( point[0] ) | 
        
        
           | 
                          else: | 
        
        
           | 
                              left.append( point[0] ) | 
        
        
           | 
          
 | 
        
        
           | 
                          if point[1] > centerY: | 
        
        
           | 
                              bottom.append( point[1] ) | 
        
        
           | 
                          else: | 
        
        
           | 
                              top.append( point[1] ) | 
        
        
           | 
          
 | 
        
        
           | 
                      cropRight = (min(right)) / float(imgWidth) | 
        
        
           | 
                      cropLeft = (max(left)) / float(imgWidth) | 
        
        
           | 
                      cropBottom = (min(bottom)) / float(imgHeight) | 
        
        
           | 
                      cropTop = (max(top)) / float(imgHeight) | 
        
        
           | 
          
 | 
        
        
           | 
                      # Draw original detected area | 
        
        
           | 
                      rawBox = np.int0(BoxPoints(rawRect)) | 
        
        
           | 
                      cv2.drawContours(img, [rawBox], -1, (255, 0, 0), 1) | 
        
        
           | 
          
 | 
        
        
           | 
                      # Draw inset area | 
        
        
           | 
                      insetBox = np.int0(BoxPoints(insetRect)) | 
        
        
           | 
                      cv2.drawContours(img, [insetBox], -1, (0, 255, 255), 1) | 
        
        
           | 
          
 | 
        
        
           | 
                      # Draw adjusted aspect ratio area | 
        
        
           | 
                      cv2.drawContours(img, [box], -1, (0, 255, 0), 2) | 
        
        
           | 
          
 | 
        
        
           | 
                      cv2.circle(img, (int(rect[0][0]), int(rect[0][1])), 3, (0, 255, 0), 3) | 
        
        
           | 
          
 | 
        
        
           | 
                  # Write result to disk for Lightroom plugin to pick up | 
        
        
           | 
                  # (The Lightroom API doesn't appear to allow streaming in output from a program) | 
        
        
           | 
                  cropData = [ | 
        
        
           | 
                      cropLeft, | 
        
        
           | 
                      cropRight, | 
        
        
           | 
                      cropTop, | 
        
        
           | 
                      cropBottom, | 
        
        
           | 
                      rotation | 
        
        
           | 
                  ] | 
        
        
           | 
          
 | 
        
        
           | 
                  for v in cropData: | 
        
        
           | 
                      print(v) | 
        
        
           | 
          
 | 
        
        
           | 
                  with open(filename + ".txt", 'w') as out: | 
        
        
           | 
                      out.write("\r\n".join(str(x) for x in cropData)) | 
        
        
           | 
          
 | 
        
        
           | 
                  cv2.imwrite(filename + "-analysis.jpg", img) | 
        
        
           | 
          
 | 
        
        
           | 
                  # if hasDisplay: | 
        
        
           | 
                  #     cv2.imshow('image', cv2.resize(img, (0,0), fx=0.75, fy=0.75) ) | 
        
        
           | 
                  #     cv2.waitKey(0) | 
        
  
Any chance of making this work for LR CC? I've got the script running but it just toggles the crop tool on and closes it without doing anything.