| 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))
|
|
|
| pixel_values = pixel_values[np.where(pixel_values[:, 0] > 0)]
|
|
|
|
|
| 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)
|
|
|
| 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))
|
|
|
|
|
|
|
|
|
| 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)
|
|
|
|
|
| grayscale = cv.cvtColor(clahe_img, cv.COLOR_BGR2GRAY)
|
| kernel = cv.getStructuringElement(1, (9, 9))
|
| 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
|
| )
|
| masked_img = cv.bitwise_and(clahe_img, clahe_img, mask=255 - hair_mask)
|
|
|
|
|
| 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}")
|
|
|