2

I am trying to compute the contrast around each pixel in an NxN window and saving the results in a new image where each pixel in the new image is the contrast of the area around it in the old image. From another post I got this:

1) Convert the image to say LAB and get the L channel
2) Compute the max for an NxN neighborhood around each pixel
3) Compute the min for an NxN neighborhood around each pixel
4) Compute the contrast from the equation above at each pixel.
5) Insert the contrast as a pixel value in new image.

Currently I have the following:

def cmap(roi):
    max = roi.reshape((roi.shape[0] * roi.shape[1], 3)).max(axis=0)
    min = roi.reshape((roi.shape[0] * roi.shape[1], 3)).min(axis=0)
    contrast = (max - min) / (max + min)
    return contrast


def cm(img):
    # convert to LAB color space
    lab = cv2.cvtColor(img, cv2.COLOR_BGR2LAB)

    # separate channels
    L, A, B = cv2.split(lab)

    img_shape = L.shape

    size = 5

    shape = (L.shape[0] - size + 1, L.shape[1] - size + 1, size, size)
    strides = 2 * L.strides
    patches = np.lib.stride_tricks.as_strided(L, shape=shape, strides=strides)
    patches = patches.reshape(-1, size, size)

    output_img = np.array([cmap(roi) for roi in patches])
    cv2.imwrite("labtest.png", output_img)

The code complains about the size of roi. Is there a better (pythonic) way of doing what I want?

user574362
  • 67
  • 1
  • 7

1 Answers1

5

You may use Dilation and Erosion morphological operations for finding the max and min for NxN neighborhood.

  • Dilation of NxN is equivalent to maximum of NxN neighborhood.
  • Erosion of NxN is equivalent to minimum of NxN neighborhood.

Using morphological operations makes the solution much simpler than "manually" dividing the image into small blocks.

You may use the following stages:

  • Convert to LAB color space and get L channel.
  • Use "dilate" morphological operation (dilate is equivalent to finding maximum pixel in NxN neighborhood).
  • Use "erode" morphological operation (dilate is equivalent to finding maximum pixel in NxN neighborhood).
  • Convert images to type float (required before using division operation).
  • Compute contrast map (range of contrast map is [0, 1]).
  • Convert contrast map to type uint8 with rounding - the conversion loosed accuracy, so I can't recommend it (but I assume you need the conversion for getting the output as an image).

Here is a complete code sample:

import numpy as np
import cv2

size_n = 5 # NxN neighborhood around each pixel

# Read input image
img = cv2.imread('chelsea.png')

# Convert to LAB color space
lab = cv2.cvtColor(img, cv2.COLOR_BGR2LAB)

# Get the L channel
L = lab[:, :, 0]

# Use "dilate" morphological operation (dilate is equivalent to finding maximum pixel in NxN neighborhood)
img_max = cv2.morphologyEx(L, cv2.MORPH_DILATE, np.ones((size_n, size_n)))

# Use "erode" morphological operation (dilate is equivalent to finding maximum pixel in NxN neighborhood)
img_min = cv2.morphologyEx(L, cv2.MORPH_ERODE, np.ones((size_n, size_n)))

# Convert to type float (required before using division operation)
img_max = img_max.astype(float)
img_min = img_min.astype(float)

# Compute contrast map (range of img_contrast is [0, 1])
img_contrast = (img_max - img_min) / (img_max + img_min)

# Convert contrast map to type uint8 with rounding - the conversion loosed accuracy, so I can't recommend it.
# Note: img_contrast_uint8 is scaled by 255 (scaled by 255 relative to the original formula).
img_contrast_uint8 = np.round(img_contrast*255).astype(np.uint8)

# Show img_contrast as output
cv2.imshow('img_contrast', img_contrast_uint8)
cv2.waitKey()
cv2.destroyAllWindows()

Input image:
enter image description here

L image:
enter image description here

img_max:
enter image description here

img_min:
enter image description here

Contrast map img_contrast_uint8:
enter image description here

Rotem
  • 30,366
  • 4
  • 32
  • 65
  • What do you recommend instead of using rounding? – user574362 Feb 25 '21 at 20:47
  • It's depends... I assume the contrast map is not the goal, but just an intermediate stage to be used in some other algorithm. In case it's an exercise, and you want to display the contrast map, converting to `uint8` is the way to go. In case you need to use the contrast map, just keep it in `float` format (you may save it as NumPy array, or as a binary file). – Rotem Feb 25 '21 at 21:11
  • got it, one last thing, how does this loop over all pixels? by the looks of it it takes a global maximum and minimum and then uses those to build the map. I am trying to make it such that the computation is done in the sliding window per max/min values for that area, and then move to the next pixel, etc. – user574362 Feb 25 '21 at 21:22
  • It's not global maximum and minimum, but maximum (and minimum) of every 5x5 pixels. For each pixels, get 5x5 neighbors, and find the maximum. Start with horizontal pass: Maximum of every 5 pixels single row: max([a0, a1, a2, a3, a4]) store in `dst_row[2]`, move to max([a1, a2, a3, a4, a5]) store in dst_row[3]... After finishing all rows iterate the column (use the rows from the first pass as input). – Rotem Feb 25 '21 at 21:57
  • Note: the binary erode and dilate may use "sliding window" implementation, but the minimum and maximum implementation is not a true "sliding window". The minimum and maximum may be implemented as "separable" filter - iterate rows (1x5 window) then iterate columns (5x1 window). That's 10 operations per pixel (instead of 25). [I suppose there is a better solution]. – Rotem Feb 25 '21 at 22:08