|
import numpy as np |
|
import cv2 as cv |
|
from PIL import Image |
|
|
|
def norm_mat(mat): |
|
return cv.normalize(mat, None, 0, 255, cv.NORM_MINMAX).astype(np.uint8) |
|
|
|
def minmax_dev(patch, mask): |
|
c = patch[1, 1] |
|
minimum, maximum, _, _ = cv.minMaxLoc(patch, mask) |
|
if c < minimum: |
|
return -1 |
|
if c > maximum: |
|
return +1 |
|
return 0 |
|
|
|
def blk_filter(img, radius): |
|
result = np.zeros_like(img, np.float32) |
|
rows, cols = result.shape |
|
block = 2 * radius + 1 |
|
for i in range(radius, rows, block): |
|
for j in range(radius, cols, block): |
|
result[ |
|
i - radius : i + radius + 1, j - radius : j + radius + 1 |
|
] = np.std( |
|
img[i - radius : i + radius + 1, j - radius : j + radius + 1] |
|
) |
|
return cv.normalize(result, None, 0, 127, cv.NORM_MINMAX, cv.CV_8UC1) |
|
|
|
def minmax_process(image, channel=4, radius=2): |
|
""" |
|
Analyzes local pixel value deviations in an image to detect potential manipulation by comparing each pixel with its neighbors. This forensic technique helps identify areas where pixel values deviate significantly from their local context, which can indicate digital tampering or editing. |
|
|
|
Args: |
|
image: Union[np.ndarray, Image.Image]: Input image to process |
|
channel (int): Channel to process (0:Grayscale, 1:Blue, 2:Green, 3:Red, 4:RGB Norm) |
|
radius (int): Radius for block filtering |
|
Returns: |
|
PIL.Image.Image: The processed image showing minmax deviations. |
|
Raises: |
|
ValueError: If the input image is invalid or channel/radius parameters are out of valid range. |
|
""" |
|
if not isinstance(image, np.ndarray): |
|
image = np.array(image) |
|
if channel == 0: |
|
img = cv.cvtColor(image, cv.COLOR_BGR2GRAY) |
|
elif channel == 4: |
|
b, g, r = cv.split(image.astype(np.float64)) |
|
img = cv.sqrt(cv.pow(b, 2) + cv.pow(g, 2) + cv.pow(r, 2)) |
|
else: |
|
img = image[:, :, 3 - channel] |
|
kernel = 3 |
|
border = kernel // 2 |
|
shape = (img.shape[0] - kernel + 1, img.shape[1] - kernel + 1, kernel, kernel) |
|
strides = 2 * img.strides |
|
patches = np.lib.stride_tricks.as_strided(img, shape=shape, strides=strides) |
|
patches = patches.reshape((-1, kernel, kernel)) |
|
mask = np.full((kernel, kernel), 255, dtype=np.uint8) |
|
mask[border, border] = 0 |
|
blocks = [0] * shape[0] * shape[1] |
|
for i, patch in enumerate(patches): |
|
blocks[i] = minmax_dev(patch, mask) |
|
output = np.array(blocks).reshape(shape[:-2]) |
|
output = cv.copyMakeBorder( |
|
output, border, border, border, border, cv.BORDER_CONSTANT |
|
) |
|
low = output == -1 |
|
high = output == +1 |
|
minmax = np.zeros_like(image) |
|
if radius > 0: |
|
radius += 3 |
|
low = blk_filter(low, radius) |
|
high = blk_filter(high, radius) |
|
if channel <= 2: |
|
minmax[:, :, 2 - channel] = low |
|
minmax[:, :, 2 - channel] += high |
|
else: |
|
minmax = np.repeat(low[:, :, np.newaxis], 3, axis=2) |
|
minmax += np.repeat(high[:, :, np.newaxis], 3, axis=2) |
|
minmax = norm_mat(minmax) |
|
else: |
|
if channel == 0: |
|
minmax[low] = [0, 0, 255] |
|
minmax[high] = [0, 0, 255] |
|
elif channel == 1: |
|
minmax[low] = [0, 255, 0] |
|
minmax[high] = [0, 255, 0] |
|
elif channel == 2: |
|
minmax[low] = [255, 0, 0] |
|
minmax[high] = [255, 0, 0] |
|
elif channel == 3: |
|
minmax[low] = [255, 255, 255] |
|
minmax[high] = [255, 255, 255] |
|
return Image.fromarray(minmax) |