ShieldMnt / invisible-watermark

python library for invisible image watermark (blind image watermark)
MIT License
1.61k stars 150 forks source link

Enhancement: Determining if an image can hold the desired watermark #30

Open mexicantexan opened 9 months ago

mexicantexan commented 9 months ago

Using the following code block to create a sample image, encode it with a uuid, and then decode it results in erroneous restructuring of uuid that was encoded.

from imwatermark import WatermarkEncoder, WatermarkDecoder
import cv2
if __name__ == "__main__":
    large_image = np.ones((256, 256, 3), dtype=np.uint8)
    large_image *= 255

    encoder = WatermarkEncoder()
    original_uuid = 'urn:uuid:a9c072b1-c796-4a0c-8b81-25e4f2259c01'
    # Have also tried:
    #     original_uuid = 'a9c072b1-c796-4a0c-8b81-25e4f2259c01'
    #     original_uuid = 'a9c072b1c7964a0c8b8125e4f2259c01'
    encoder.set_watermark('uuid', original_uuid)
    # Have also tried:
    #     encoder.set_by_uuid(original_uuid)
    bgr_encoded = encoder.encode(large_image, 'dwtDct')
    cv2.imwrite('test.png', bgr_encoded)
    print(bgr_encoded.min(), bgr_encoded.max())  # results in: 251 255
    decoder = WatermarkDecoder('uuid')
    watermark = decoder.decode(bgr_encoded, 'dwtDct')
    print(watermark)  # results in: 00000000-0000-0000-0000-000000000000
    assert watermark == original_uuid.replace('urn:uuid:', '')  # results in: False

Have also tried dwtDctSvd

Environment:

mexicantexan commented 9 months ago

Have also tried the bytes method which results in the same erroneous output:

new_uuid = uuid.UUID('urn:uuid:a9c072b1-c796-4a0c-8b81-25e4f2259c01').bytes
print(new_uuid.__repr__())  # b'\xa9\xc0r\xb1\xc7\x96J\x0c\x8b\x81%\xe4\xf2%\x9c\x01'
encoder = WatermarkEncoder()
encoder.set_by_bytes(new_uuid)
bgr_encoded = encoder.encode(large_image, 'dwtDctSvd')
cv2.imwrite('test.png', bgr_encoded)
print(bgr_encoded.min(), bgr_encoded.max())  # results in: 251 255
decoder = WatermarkDecoder('uuid')
watermark = decoder.decode(bgr_encoded, 'dwtDctSvd')
print(watermark)  # results in: 00000000-0000-0000-0000-000000000000

Have also tried the dwtDct as well

mexicantexan commented 9 months ago

Is there a minimum "complexity"/"chaos" (differences in pixel values) that needs to take place in the image? If that is the case, maybe there is a way to calculate this "complexity"/"chaos"?

mexicantexan commented 8 months ago

Could add a helper function like:

def can_fit_watermark(watermark_str, image_cv2, num_coefficients=None, threshold=0.1) -> bool:
    """
    Estimates if an image can hold a given watermark.

    :param watermark_str: The watermark to embed, as a string.
    :param image_cv2: The image into which the watermark is to be embedded, as a cv2.Mat object.
    :param num_coefficients: Optional. The number of DWT-DCT coefficients to consider for watermarking.
                             If None, uses all coefficients.
    :param threshold: The threshold for pass/fail criteria based on the modifications' visibility.
                      Represents the maximum average change allowed per coefficient.
    :return: Boolean indicating whether the image can hold the watermark.
    """
    watermark_bits = ''.join(format(ord(char), '08b') for char in watermark_str)
    watermark_size = len(watermark_bits)

    # Convert image to grayscale for simplicity
    yuv = cv2.cvtColor(image_cv2, cv2.COLOR_BGR2YUV)
    gray_u_image = yuv[:,:,1]

    cA, (cH, cV, cD) = pywt.dwt2(gray_u_image, 'haar')
    dct_coefficients = np.fft.fft2(cA).flatten()
    num_coefficients = num_coefficients if num_coefficients else dct_coefficients.size
    modifiable_coefficients = min(num_coefficients, dct_coefficients.size)
    estimated_capacity = modifiable_coefficients * threshold

    # Check if the watermark fits within the estimated capacity
    return watermark_size <= estimated_capacity