melanoma_classification / src /scripts /estimate_ita.py
Mhara's picture
Upload folder using huggingface_hub
dae5c90 verified
Raw
History Blame Contribute Delete
4.81 kB
import argparse
import csv
import os
import warnings
import cv2 as cv
import numpy as np
import kneed
from tqdm import tqdm
CSV_HEADER = ["image", "ita", "color_r", "color_g", "color_b"]
def get_ita_angle(color_rgb: np.ndarray) -> float:
color_lab = cv.cvtColor(np.uint8([[color_rgb]]), cv.COLOR_RGB2LAB)[0][0]
return np.arctan((color_lab[0] - 50) / color_lab[2]) * 180 / np.pi
def kmeans_dominant_color_lab(processed_img, k):
processed_img_lab = cv.cvtColor(processed_img, cv.COLOR_BGR2LAB)
pixel_values = processed_img_lab.reshape((-1, 3))
# remove black pixels
pixel_values = pixel_values[np.where(pixel_values[:, 0] > 0)]
# keep only a and b channels
# pixel_values = pixel_values[:, 1:]
pixel_values = np.float32(pixel_values)
criteria = (cv.TERM_CRITERIA_EPS + cv.TERM_CRITERIA_MAX_ITER, 100, 0.2)
compactness, labels, (centers) = cv.kmeans(
pixel_values, k, None, criteria, 10, cv.KMEANS_PP_CENTERS
)
centers = np.uint8(centers)
labels = labels.flatten()
dominant_label = np.argmax(np.bincount(labels))
dominant_color = centers[dominant_label]
dominant_color = np.array(dominant_color)
dominant_color = np.round(dominant_color).astype(int)
# add back L channel
dominant_color = cv.cvtColor(np.uint8([[dominant_color]]), cv.COLOR_LAB2RGB)
return dominant_color, compactness
def kmeans_dominant_color(image_path):
img = cv.imread(image_path)
img = cv.resize(img, (128, 256))
# img = cv.resize(img, (256, 512))
# img = cv.resize(img, (512, 1024))
# Isolate skin
clahe = cv.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8))
lab = cv.cvtColor(img, cv.COLOR_BGR2LAB)
lab_planes = list(cv.split(lab))
lab_planes[0] = clahe.apply(lab_planes[0])
lab = cv.merge(lab_planes)
clahe_img = cv.cvtColor(lab, cv.COLOR_LAB2BGR)
## Dullrazor
grayscale = cv.cvtColor(clahe_img, cv.COLOR_BGR2GRAY) # [1] uses RGB2GRAY
kernel = cv.getStructuringElement(1, (9, 9)) # [1] uses a 3x3 kernel
blackhat = cv.morphologyEx(grayscale, cv.MORPH_BLACKHAT, kernel)
blurred = cv.GaussianBlur(blackhat, (3, 3), cv.BORDER_DEFAULT)
_, hair_mask = cv.threshold(
blurred, 20, 255, cv.THRESH_BINARY
) # [2] sets the threshold at 10, [1] at 25
masked_img = cv.bitwise_and(clahe_img, clahe_img, mask=255 - hair_mask)
## Threshold to remove pigmentations
hsv = cv.cvtColor(masked_img, cv.COLOR_BGR2HSV)
_, _, v = cv.split(hsv)
v = cv.GaussianBlur(v, (5, 5), 0)
_, v_thresh = cv.threshold(v, 0, 255, cv.THRESH_BINARY_INV + cv.THRESH_OTSU)
thresh = v_thresh
kernel = cv.getStructuringElement(cv.MORPH_ELLIPSE, (3, 3))
thresh = cv.morphologyEx(thresh, cv.MORPH_OPEN, kernel, iterations=5)
thresh = cv.dilate(thresh, kernel, iterations=5)
thresh = cv.morphologyEx(thresh, cv.MORPH_CLOSE, kernel, iterations=5)
thresh = cv.bitwise_or(thresh, hair_mask)
final_image = cv.bitwise_and(img, img, mask=255 - thresh)
ks = range(3, 10)
colors, compactnesses = zip(
*[kmeans_dominant_color_lab(final_image, k) for k in ks]
)
kneedle = kneed.KneeLocator(
ks, compactnesses, S=1.0, curve="convex", direction="decreasing"
)
if kneedle.elbow is None:
dominant_color = colors[-1]
else:
dominant_color = colors[kneedle.elbow]
return dominant_color
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("path", help="dataset root path")
parser.add_argument("-o", "--out", help="output csv file")
parser.add_argument("-f", "--files", nargs="+", help="list of filenames to process")
args = parser.parse_args()
dataset_root = args.path
csv_path = args.out if args.out != None else "skin_tones.csv"
files = args.files if args.files != None else os.listdir(dataset_root)
if os.path.exists(csv_path):
raise FileExistsError(f"File {csv_path} already exists")
with open(csv_path, "a+", newline="") as f:
writer = csv.writer(f)
writer.writerow(CSV_HEADER)
for filename in tqdm(sorted(files)):
file_path = os.path.join(dataset_root, filename)
if not os.path.exists(file_path):
warnings.warn(f"Skipping file {file_path}, file does not exist")
continue
try:
color = kmeans_dominant_color(file_path).squeeze()
angle = get_ita_angle(color)
writer.writerow([filename, angle, color[0], color[1], color[2]])
except Exception as e:
warnings.warn(f"ITA estimation failed on file {filename}: {e}")