Skip to content

Texture analysis using Haralick Descriptors for Computer Vision tasks #8004

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 28 commits into from
Sep 5, 2023
Merged
Changes from 8 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
802cd20
Create haralick_descriptors
rzimmerdev Oct 19, 2022
88b014f
Working on creating Unit Testing for Haralick Descriptors module
Oct 19, 2022
bc6a189
Type hinting for Haralick descriptors
Oct 19, 2022
dff5498
Merge branch 'TheAlgorithms:master' into master
rzimmerdev Nov 27, 2022
6e23cc0
Fixed docstrings, unit testing and formatting choices
rzimmerdev Nov 27, 2022
eca7118
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Nov 27, 2022
9d2cca7
Fixed line size formatting
rzimmerdev Nov 27, 2022
cd250cf
Merge remote-tracking branch 'origin/master'
rzimmerdev Nov 27, 2022
5e73aa6
Added final doctests
rzimmerdev Nov 27, 2022
aacb27e
Changed main callable
rzimmerdev Nov 27, 2022
5fb933d
Updated requirements.txt
rzimmerdev Nov 27, 2022
c9ec63e
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Nov 27, 2022
bfd0fd7
Update computer_vision/haralick_descriptors.py
rzimmerdev Dec 13, 2022
6f52bff
Undone wrong commit
rzimmerdev Dec 13, 2022
ca90e9a
Merge branch 'TheAlgorithms:master' into master
rzimmerdev Dec 19, 2022
408b0fb
Merge branch 'TheAlgorithms:master' into master
rzimmerdev Dec 20, 2022
fc2fa0b
Update haralick_descriptors.py
rzimmerdev Dec 20, 2022
40da555
Apply suggestions from code review
tianyizheng02 Sep 5, 2023
ba2f474
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Sep 5, 2023
9faef36
Fix ruff errors in haralick_descriptors.py
tianyizheng02 Sep 5, 2023
7eda2b8
Add type hint to haralick_descriptors.py to fix ruff error
tianyizheng02 Sep 5, 2023
fd085df
Update haralick_descriptors.py
tianyizheng02 Sep 5, 2023
40daa68
Update haralick_descriptors.py
tianyizheng02 Sep 5, 2023
8da35eb
Update haralick_descriptors.py
tianyizheng02 Sep 5, 2023
5795fc4
Update haralick_descriptors.py
tianyizheng02 Sep 5, 2023
44d6bd1
Try to fix mypy errors in haralick_descriptors.py
tianyizheng02 Sep 5, 2023
21efc62
Update haralick_descriptors.py
tianyizheng02 Sep 5, 2023
5fa7520
Fix type hint in haralick_descriptors.py
tianyizheng02 Sep 5, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
390 changes: 390 additions & 0 deletions computer_vision/haralick_descriptors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,390 @@
"""
https://en.wikipedia.org/wiki/Image_texture
https://en.wikipedia.org/wiki/Co-occurrence_matrix#Application_to_image_analysis
"""
from typing import Any

import imageio.v2 as imageio
import numpy as np


def root_mean_square_error(original: np.ndarray, reference: np.ndarray) -> float:
"""Simple implementation of Root Mean Squared Error
for two N dimensional numpy arrays.

Examples:
>>> root_mean_square_error(np.array([1, 2, 3]), np.array([1, 2, 3]))
0.0
>>> root_mean_square_error(np.array([1, 2, 3]), np.array([2, 2, 2]))
0.816496580927726
>>> root_mean_square_error(np.array([1, 2, 3]), np.array([6, 4, 2]))
3.1622776601683795
"""
return np.sqrt(((original - reference) ** 2).mean())


def normalize_image(
image: np.ndarray, cap: float = 255.0, data_type=np.uint8

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please provide type hint for the parameter: data_type

) -> np.ndarray:
"""
Normalizes image in Numpy 2D array format, between ranges 0-cap,
as to fit uint8 type.

Args:
image: 2D numpy array representing image as matrix, with values in any range
cap: Maximum cap amount for normalization
data_type: numpy data type to set output variable to
Returns:
return 2D numpy array of type uint8, corresponding to limited range matrix

Examples:
>>> normalize_image(np.array([[1, 2, 3], [4, 5, 10]]),
... cap=1.0, data_type=np.float64)
array([[0. , 0.11111111, 0.22222222],
[0.33333333, 0.44444444, 1. ]])
>>> normalize_image(np.array([[4, 4, 3], [1, 7, 2]]))
array([[127, 127, 85],
[ 0, 255, 42]], dtype=uint8)
"""
normalized = (image - np.min(image)) / (np.max(image) - np.min(image)) * cap
return normalized.astype(data_type)


def normalize_array(array: np.ndarray, cap: float = 1) -> np.ndarray:
"""Normalizes a 1D array, between ranges 0-cap.

Args:
array: List containing values to be normalized between cap range.
cap: Maximum cap amount for normalization.
Returns:
return 1D numpy array, corresponding to limited range array

Examples:
>>> normalize_array(np.array([2, 3, 5, 7]))
array([0. , 0.2, 0.6, 1. ])
>>> normalize_array(np.array([[5], [7], [11], [13]]))
array([[0. ],
[0.25],
[0.75],
[1. ]])
"""
return (array - np.min(array)) / (np.max(array) - np.min(array)) * cap


def grayscale(image: np.ndarray) -> np.ndarray:
"""
Uses luminance weights to transform RGB channel to greyscale, by
taking the dot product between the channel and the weights.

Example:
>>> grayscale(np.array([[[108, 201, 72], [255, 11, 127]],
... [[56, 56, 56], [128, 255, 107]]]))
array([[158, 97],
[ 56, 200]], dtype=uint8)
"""
return np.dot(image[:, :, 0:3], [0.299, 0.587, 0.114]).astype(np.uint8)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you include a source (e.g., Wikipedia) for those luminance weight values?



def binarize(image: np.ndarray, threshold: float = 127.0) -> np.ndarray:
"""
Binarizes a grayscale image based on a given threshold value,
setting values to 1 or 0 accordingly.

Examples:
>>> binarize(np.array([[128, 255], [101, 156]]))
array([[1, 1],
[0, 1]])
>>> binarize(np.array([[0.07, 1], [0.51, 0.3]]), threshold=0.5)
array([[0, 1],
[1, 0]])
"""
return np.where(image > threshold, 1, 0)


def transform(image: np.ndarray, kind: str, kernel=None) -> np.ndarray:

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please provide type hint for the parameter: kernel

"""
Simple image transformation using one of two available filter functions:
Erosion and Dilation.

Args:
image: binarized input image, onto which to apply transformation
kind: Can be either 'erosion', in which case the :func:np.max
function is called, or 'dilation', when :func:np.min is used instead.
kernel: n x n kernel with shape < :attr:image.shape,
to be used when applying convolution to original image

Returns:
returns a numpy array with same shape as input image,
corresponding to applied binary transformation.

Examples:
>>> img = np.array([[1, 0.5], [0.2, 0.7]])
>>> img = binarize(img, threshold=0.5)
>>> transform(img, 'erosion')
array([[1, 1],
[1, 1]], dtype=uint8)
>>> transform(img, 'dilation')
array([[0, 0],
[0, 0]], dtype=uint8)
"""
if not kernel:
kernel = np.ones((3, 3))

if kind == "erosion":
constant = 1
apply = np.max
else:
constant = 0
apply = np.min

center_x, center_y = (x // 2 for x in kernel.shape)

# Use padded image when applying convolotion
# to not go out of bounds of the original the image
transformed = np.zeros(image.shape, dtype=np.uint8)
padded = np.pad(image, 1, "constant", constant_values=constant)

for x in range(center_x, padded.shape[0] - center_x):
for y in range(center_y, padded.shape[1] - center_y):
center = padded[
x - center_x : x + center_x + 1, y - center_y : y + center_y + 1
]
# Apply transformation method to the centered section of the image
transformed[x - center_x, y - center_y] = apply(center[kernel == 1])

return transformed


def opening_filter(image: np.ndarray, kernel: np.ndarray = None) -> np.ndarray:
"""
Opening filter, defined as the sequence of
erosion and then a dilation filter on the same image.

Examples:
>>> img = np.array([[1, 0.5], [0.2, 0.7]])
>>> img = binarize(img, threshold=0.5)
>>> opening_filter(img)
array([[1, 1],
[1, 1]], dtype=uint8)
"""
if not kernel:
np.ones((3, 3))

return transform(transform(image, "dilation", kernel), "erosion", kernel)


def closing_filter(image: np.ndarray, kernel: np.ndarray = None) -> np.ndarray:
"""
Opening filter, defined as the sequence of
dilation and then erosion filter on the same image.

Examples:
>>> img = np.array([[1, 0.5], [0.2, 0.7]])
>>> img = binarize(img, threshold=0.5)
>>> closing_filter(img)
array([[0, 0],
[0, 0]], dtype=uint8)
"""
if kernel is None:
np.ones((3, 3))

return transform(transform(image, "erosion", kernel), "dilation", kernel)


def binary_mask(
image_gray: np.ndarray, image_map: np.ndarray
) -> tuple[np.ndarray, np.ndarray]:
"""
Apply binary mask, or thresholding based
on bit mask value (mapping mask is binary).

Returns the mapped true value mask and its complementary false value mask.

Example:
>>> img = np.array([[[108, 201, 72], [255, 11, 127]],
... [[56, 56, 56], [128, 255, 107]]])
>>> gray = grayscale(img)
>>> binary = binarize(gray)
>>> morphological = opening_filter(binary)
>>> binary_mask(gray, morphological)
(array([[1, 1],
[1, 1]], dtype=uint8), array([[158, 97],
[ 56, 200]], dtype=uint8))
"""
true_mask, false_mask = np.array(image_gray, copy=True), np.array(
image_gray, copy=True
)
true_mask[image_map == 1] = 1
false_mask[image_map == 0] = 0

return true_mask, false_mask


def matrix_concurrency(image: np.ndarray, coordinate) -> np.ndarray:

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As there is no test file in this pull request nor any test function or class in the file computer_vision/haralick_descriptors.py, please provide doctest for the function matrix_concurrency

Please provide type hint for the parameter: coordinate

"""
Calculate sample co-occurrence matrix based on input image
as well as selected coordinates on image.

Implementation is made using basic iteration,
as function to be performed (np.max) is non-linear and therefore
not callable on the frequency domain.
"""
matrix = np.zeros([np.max(image) + 1, np.max(image) + 1])

offset_x, offset_y = coordinate[0], coordinate[1]

for x in range(1, image.shape[0] - 1):
for y in range(1, image.shape[1] - 1):
base_pixel = image[x, y]
offset_pixel = image[x + offset_x, y + offset_y]

matrix[base_pixel, offset_pixel] += 1

return matrix / np.sum(matrix)


def haralick_descriptors(matrix: np.ndarray) -> list:

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As there is no test file in this pull request nor any test function or class in the file computer_vision/haralick_descriptors.py, please provide doctest for the function haralick_descriptors

"""Calculates all 8 Haralick descriptors based on co-occurence input matrix.
All descriptors are as follows:
Maximum probability, Inverse Difference, Homogeneity, Entropy,
Energy, Dissimilarity, Contrast and Correlation

Args:
matrix: Co-occurence matrix to use as base for calculating descriptors.

Returns:
Reverse ordered list of resulting descriptors
"""
# Function np.indices could be used for bigger input types,
# but np.ogrid works just fine
i, j = np.ogrid[0 : matrix.shape[0], 0 : matrix.shape[1]] # np.indices()

# Pre-calculate frequent multiplication and subtraction
prod = np.multiply(i, j)
sub = np.subtract(i, j)

# Calculate numerical value of Maximum Probability
maximum_prob = np.max(matrix)
# Using the definition for each descriptor individually to calculate its matrix
correlation = prod * matrix
energy = np.power(matrix, 2)
contrast = matrix * np.power(sub, 2)

dissimilarity = matrix * np.abs(sub)
inverse_difference = matrix / (1 + np.abs(sub))
homogeneity = matrix / (1 + np.power(sub, 2))
entropy = -(matrix[matrix > 0] * np.log(matrix[matrix > 0]))

# Sum values for descriptors ranging from the first one to the last,
# as all are their respective origin matrix and not the resulting value yet.
return [
maximum_prob,
correlation.sum(),
energy.sum(),
contrast.sum(),
dissimilarity.sum(),
inverse_difference.sum(),
homogeneity.sum(),
entropy.sum(),
]


def get_descriptors(masks: tuple[np.ndarray, np.ndarray], coordinate) -> np.ndarray:

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As there is no test file in this pull request nor any test function or class in the file computer_vision/haralick_descriptors.py, please provide doctest for the function get_descriptors

Please provide type hint for the parameter: coordinate

"""
Calculate all Haralick descriptors for a sequence of
different co-occurrence matrices, given input masks and coordinates.
"""
descriptors = np.zeros((len(masks), 8))
for idx, mask in enumerate(masks):
descriptors[idx] = haralick_descriptors(matrix_concurrency(mask, coordinate))

# Concatenate each individual descriptor into
# one single list containing sequence of descriptors
return np.concatenate(descriptors, axis=None)


def euclidean(point_1: np.ndarray, point_2: np.ndarray) -> np.float32:
"""
Simple method for calculating the euclidean distance between two points,
with type np.ndarray.

Example:
>>> a = np.array([1, 0, -2])
>>> b = np.array([2, -1, 1])
>>> euclidean(a, b)
3.3166247903554
"""
return np.sqrt(np.sum(np.square(point_1 - point_2)))


def get_distances(descriptors, base) -> list[Any]:

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As there is no test file in this pull request nor any test function or class in the file computer_vision/haralick_descriptors.py, please provide doctest for the function get_distances

Please provide type hint for the parameter: descriptors

Please provide type hint for the parameter: base

"""
Calculate all Euclidean distances between a selected base descriptor
and all other Haralick descriptors
The resulting comparison is return in decreasing order,
showing which descriptor is the most similar to the selected base.

Args:
descriptors: Haralick descriptors to compare with base index
base: Haralick descriptor index to use as base when calculating respective
euclidean distance to other descriptors.

Returns:
Ordered distances between descriptors
"""
distances = np.zeros(descriptors.shape[0])

for idx, description in enumerate(descriptors):
distances[idx] = euclidean(description, descriptors[base])
# Normalize distances between range [0, 1]
distances = normalize_array(distances, 1)
return sorted(enumerate(distances), key=lambda tup: tup[1])


def main():

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please provide return type hint for the function: main. If the function does not return a value, please provide the type hint as: def function() -> None:

As there is no test file in this pull request nor any test function or class in the file computer_vision/haralick_descriptors.py, please provide doctest for the function main

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please provide return type hint for the function: main. If the function does not return a value, please provide the type hint as: def function() -> None:

As there is no test file in this pull request nor any test function or class in the file computer_vision/haralick_descriptors.py, please provide doctest for the function main

# Index to compare haralick descriptors to
index = int(input())
q_value = [int(value) for value in input().split()]

# Format is the respective filter to apply,
# can be either 1 for the opening filter or else for the closing
parameters = {"format": int(input()), "threshold": int(input())}

# Number of images to perform methods on
b_number = int(input())

files, descriptors = ([], [])

for _ in range(b_number):
file = input().rstrip()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we have prompt messages for each of the input calls

files.append(file)

# Open given image and calculate morphological filter,
# respective masks and correspondent Harralick Descriptors.
image = imageio.imread(file).astype(np.float32)
gray = grayscale(image)
threshold = binarize(gray, parameters["threshold"])

morphological = (
opening_filter(threshold)
if parameters["format"] == 1
else closing_filter(threshold)
)
masks = binary_mask(gray, morphological)
descriptors.append(get_descriptors(masks, q_value))

# Transform ordered distances array into a sequence of indexes
# corresponding to original file position
distances = get_distances(np.array(descriptors), index)
indexed_distances = np.array(distances).astype(np.uint8)[:, 0]

# Finally, print distances considering the Haralick descriptions from the base
# file to all other images using the morphology method of choice.
print(f"Query: {files[index]}")
print("Ranking:")
for idx, file_idx in enumerate(indexed_distances):
print(f"({idx}) {files[file_idx]}", end="\n")


if __name__ == "__main__":
main()