feat(v3): simplify the project to contain only required tools
This commit is contained in:
@@ -4,7 +4,6 @@ from io import BytesIO
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
from numpy.lib.stride_tricks import sliding_window_view
|
||||
from PIL import Image
|
||||
|
||||
|
||||
@@ -177,52 +176,6 @@ def histogram_equalization(image, params):
|
||||
return gray_to_rgb(equalized)
|
||||
|
||||
|
||||
def target_cdf_from_params(params):
|
||||
if "cdf" in params:
|
||||
cdf = np.array(params["cdf"], dtype=np.float64)
|
||||
if cdf.shape != (256,) or np.any(np.diff(cdf) < 0):
|
||||
raise ProcessingError("cdf must contain 256 non-decreasing values.")
|
||||
if cdf[-1] <= 0:
|
||||
raise ProcessingError("cdf must end with a positive value.")
|
||||
return cdf / cdf[-1]
|
||||
|
||||
mode = params.get("target", "uniform")
|
||||
levels = np.arange(256, dtype=np.float64)
|
||||
if mode == "dark":
|
||||
pdf = np.exp(-levels / 64.0)
|
||||
elif mode == "bright":
|
||||
pdf = np.exp(-(255.0 - levels) / 64.0)
|
||||
elif mode == "bimodal":
|
||||
pdf = np.exp(-((levels - 72.0) ** 2) / (2 * 22.0**2)) + np.exp(-((levels - 190.0) ** 2) / (2 * 28.0**2))
|
||||
else:
|
||||
pdf = np.ones(256, dtype=np.float64)
|
||||
cdf = np.cumsum(pdf)
|
||||
return cdf / cdf[-1]
|
||||
|
||||
|
||||
def histogram_matching(image, params):
|
||||
gray = to_gray(image)
|
||||
source_counts = np.bincount(gray.ravel(), minlength=256).astype(np.float64)
|
||||
source_cdf = np.cumsum(source_counts)
|
||||
source_cdf /= source_cdf[-1]
|
||||
target_cdf = target_cdf_from_params(params)
|
||||
target_levels = np.arange(256)
|
||||
mapping = np.interp(source_cdf, target_cdf, target_levels).round().clip(0, 255).astype(np.uint8)
|
||||
return gray_to_rgb(mapping[gray])
|
||||
|
||||
|
||||
def local_equalization(image, params):
|
||||
size = require_odd(params.get("size", 7), "size")
|
||||
gray = to_gray(image)
|
||||
radius = size // 2
|
||||
padded = np.pad(gray, radius, mode="edge")
|
||||
windows = sliding_window_view(padded, (size, size))
|
||||
centers = gray[..., None, None]
|
||||
ranks = np.count_nonzero(windows <= centers, axis=(-1, -2))
|
||||
equalized = np.round(ranks * 255.0 / (size * size)).astype(np.uint8)
|
||||
return gray_to_rgb(equalized)
|
||||
|
||||
|
||||
def apply_kernel(image, kernel, normalize_derivative=False):
|
||||
source = image.astype(np.float32)
|
||||
if image.ndim == 2:
|
||||
@@ -309,93 +262,6 @@ def roberts(image, params):
|
||||
return gradient_magnitude(image, ROBERTS_GX, ROBERTS_GY)
|
||||
|
||||
|
||||
def rgb_to_hsi(image):
|
||||
rgb = image.astype(np.float32) / 255.0
|
||||
r, g, b = rgb[..., 0], rgb[..., 1], rgb[..., 2]
|
||||
numerator = 0.5 * ((r - g) + (r - b))
|
||||
denominator = np.sqrt((r - g) ** 2 + (r - b) * (g - b)) + 1e-8
|
||||
theta = np.arccos(np.clip(numerator / denominator, -1.0, 1.0))
|
||||
h = np.where(b <= g, theta, 2.0 * np.pi - theta) / (2.0 * np.pi)
|
||||
total = r + g + b
|
||||
s = np.where(total <= 1e-8, 0.0, 1.0 - 3.0 * np.minimum(np.minimum(r, g), b) / total)
|
||||
i = total / 3.0
|
||||
return np.stack([h, s, i], axis=-1)
|
||||
|
||||
|
||||
def hsi_to_rgb(hsi):
|
||||
h = (hsi[..., 0] % 1.0) * 2.0 * np.pi
|
||||
s = np.clip(hsi[..., 1], 0.0, 1.0)
|
||||
i = np.clip(hsi[..., 2], 0.0, 1.0)
|
||||
r = np.zeros_like(h)
|
||||
g = np.zeros_like(h)
|
||||
b = np.zeros_like(h)
|
||||
|
||||
sector0 = h < 2.0 * np.pi / 3.0
|
||||
sector1 = (h >= 2.0 * np.pi / 3.0) & (h < 4.0 * np.pi / 3.0)
|
||||
sector2 = ~sector0 & ~sector1
|
||||
|
||||
h0 = h[sector0]
|
||||
b[sector0] = i[sector0] * (1.0 - s[sector0])
|
||||
r[sector0] = i[sector0] * (1.0 + s[sector0] * np.cos(h0) / (np.cos(np.pi / 3.0 - h0) + 1e-8))
|
||||
g[sector0] = 3.0 * i[sector0] - (r[sector0] + b[sector0])
|
||||
|
||||
h1 = h[sector1] - 2.0 * np.pi / 3.0
|
||||
r[sector1] = i[sector1] * (1.0 - s[sector1])
|
||||
g[sector1] = i[sector1] * (1.0 + s[sector1] * np.cos(h1) / (np.cos(np.pi / 3.0 - h1) + 1e-8))
|
||||
b[sector1] = 3.0 * i[sector1] - (r[sector1] + g[sector1])
|
||||
|
||||
h2 = h[sector2] - 4.0 * np.pi / 3.0
|
||||
g[sector2] = i[sector2] * (1.0 - s[sector2])
|
||||
b[sector2] = i[sector2] * (1.0 + s[sector2] * np.cos(h2) / (np.cos(np.pi / 3.0 - h2) + 1e-8))
|
||||
r[sector2] = 3.0 * i[sector2] - (g[sector2] + b[sector2])
|
||||
|
||||
return ensure_uint8(np.round(np.clip(np.stack([r, g, b], axis=-1), 0.0, 1.0) * 255.0))
|
||||
|
||||
|
||||
def hsi_intensity_filter(image, params):
|
||||
method = params.get("method", "smooth")
|
||||
hsi = rgb_to_hsi(image)
|
||||
intensity = np.round(hsi[..., 2] * 255.0).astype(np.uint8)
|
||||
if method == "sharpen":
|
||||
filtered = laplacian(intensity, {"mode": "sharpen", "sign": params.get("sign", "add")})
|
||||
else:
|
||||
filtered = box_filter(intensity, {"size": params.get("size", 3)})
|
||||
hsi[..., 2] = filtered.astype(np.float32) / 255.0
|
||||
return hsi_to_rgb(hsi)
|
||||
|
||||
|
||||
def pseudo_color_slices(image, params):
|
||||
gray = to_gray(image)
|
||||
slices = params.get(
|
||||
"slices",
|
||||
[
|
||||
{"start": 0, "end": 85, "color": [59, 130, 246]},
|
||||
{"start": 86, "end": 170, "color": [34, 197, 94]},
|
||||
{"start": 171, "end": 255, "color": [239, 68, 68]},
|
||||
],
|
||||
)
|
||||
output = np.zeros((*gray.shape, 3), dtype=np.uint8)
|
||||
for item in slices:
|
||||
start = int(item["start"])
|
||||
end = int(item["end"])
|
||||
color = np.array(item["color"], dtype=np.uint8)
|
||||
if start < 0 or end > 255 or start > end or color.shape != (3,):
|
||||
raise ProcessingError("Each pseudo-color slice requires start/end in 0..255 and an RGB color.")
|
||||
output[(gray >= start) & (gray <= end)] = color
|
||||
return output
|
||||
|
||||
|
||||
def gray_to_color_sinusoidal(image, params):
|
||||
gray = to_gray(image).astype(np.float32) / 255.0
|
||||
hue_frequency = float(params.get("hue_frequency", 1.0))
|
||||
saturation_frequency = float(params.get("saturation_frequency", 0.5))
|
||||
intensity_frequency = float(params.get("intensity_frequency", 0.25))
|
||||
h = (0.5 + 0.5 * np.sin(2.0 * np.pi * hue_frequency * gray)) % 1.0
|
||||
s = 0.55 + 0.4 * np.sin(2.0 * np.pi * saturation_frequency * gray + np.pi / 3.0)
|
||||
i = 0.5 + 0.45 * np.sin(2.0 * np.pi * intensity_frequency * gray - np.pi / 2.0)
|
||||
return hsi_to_rgb(np.stack([h, np.clip(s, 0, 1), np.clip(i, 0, 1)], axis=-1))
|
||||
|
||||
|
||||
OPERATIONS = {
|
||||
"negative": negative,
|
||||
"log": logarithmic,
|
||||
@@ -404,8 +270,6 @@ OPERATIONS = {
|
||||
"gray_slice": gray_slice,
|
||||
"bit_plane": bit_plane,
|
||||
"hist_equalization": histogram_equalization,
|
||||
"hist_match": histogram_matching,
|
||||
"local_equalization": local_equalization,
|
||||
"box_filter": box_filter,
|
||||
"weighted_average": weighted_average,
|
||||
"median_filter": median_filter,
|
||||
@@ -413,9 +277,6 @@ OPERATIONS = {
|
||||
"high_boost": high_boost,
|
||||
"sobel": sobel,
|
||||
"roberts": roberts,
|
||||
"hsi_intensity_filter": hsi_intensity_filter,
|
||||
"pseudo_color_slices": pseudo_color_slices,
|
||||
"gray_to_color_sinusoidal": gray_to_color_sinusoidal,
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user