W ramach dzisiejszych zajęć będziemy zajmować się dwoma zagadnieniami — Steganografią, czyli ukrywaniem informacji na obrazach oraz znakowaniem wodnym obrazów. W ramach zajęć dostajecie dostęp do 3 prostych funkcji realizujących te zagadnienia poruszane na dzisiejszych zajęciach:

  1. Funkcja do znakowania wodnego. Przyjmuje jako parametry obraz, który będzie poddawany znakowaniu wodnemu, maskę o tych samych wymiarach co obraz, przechowującą dane w typie boolen lub jako obraz zwykły oraz parametr alfa służący do określania stopnia przesłaniania maski. Jeżeli wyniki będą zbyt prześwietlone należy zmienić wartość parametru gamma z linijki #21 z 0 na -alpha.

    def water_mark(img,mask,alpha=0.25):
        assert (img.shape[0]==mask.shape[0]) and (img.shape[1]==mask.shape[1]), "Wrong size"
        if len(img.shape)<3:
            flag=True
            t_img=cv2.cvtColor(img,cv2.COLOR_GRAY2RGBA)
        else:
            flag=False
            t_img=cv2.cvtColor(img,cv2.COLOR_RGB2RGBA)      
        if (mask.dtype==bool):
            t_mask=cv2.cvtColor((mask*255).astype(np.uint8),cv2.COLOR_GRAY2RGBA)
        elif (mask.dtype==np.uint8):
            if len(mask.shape)<3:
                t_mask=cv2.cvtColor((mask).astype(np.uint8),cv2.COLOR_GRAY2RGBA)
            else:
                t_mask=cv2.cvtColor((mask).astype(np.uint8),cv2.COLOR_RGB2RGBA)
        else:
            if len(mask.shape)<3:
                t_mask=cv2.cvtColor((mask*255).astype(np.uint8),cv2.COLOR_GRAY2RGBA)
            else:
                t_mask=cv2.cvtColor((mask*255).astype(np.uint8),cv2.COLOR_RGB2RGBA)
        t_out=cv2.addWeighted(t_img,1,t_mask,alpha,0)
        if flag:
            out=cv2.cvtColor(t_out,cv2.COLOR_RGBA2GRAY)
        else:
            out=cv2.cvtColor(t_out,cv2.COLOR_RGBA2RGB)
        return out
  2. Funkcja do kodowania informacji jednej warstwie obrazu. Jeżeli chcecie zakodować więcej informacji na obrazie, to do funkcji kodującej należy przekazywać informację dla każdej warstwy osobno. Funkcja jako parametry przyjmuje kolejno: warstwę obrazu, na której będzie kodowana informacja. Informację, jaka ma zostać zakodowana — może być dostarczona jako macierz bool o rozmiarze 1 warstwy obrazu lub dowolny inny wektor, lub tablica uint8 (np. łańcuch tekstowy), która zostanie zakodowana. Ostatni parametr to maska binarna w postaci liczby uint8 określająca, na których bitach chcemy zapisywać. Wartości bitów oznaczają kolejne potęgi liczby \(2\). \(1 =2^0\) oznacza 1 bit, czyli LSB, a \(12 = 2^2 + 2^3\) oznacza zapis na 3 i 4 bicie.

    def put_data(img,data,binary_mask=np.uint8(1)):
        assert img.dtype==np.uint8 , "img wrong data type"
        assert binary_mask.dtype==np.uint8, "binary_mask wrong data type"
        un_binary_mask=np.unpackbits(binary_mask)
        if data.dtype!=bool:
            unpacked_data=np.unpackbits(data)
        else:
            unpacked_data=data
        dataspace=img.shape[0]*img.shape[1]*np.sum(un_binary_mask)
        assert (dataspace>=unpacked_data.size) , "too much data"
        if dataspace==unpacked_data.size:
            prepered_data=unpacked_data.reshape(img.shape[0],img.shape[1],np.sum(un_binary_mask)).astype(np.uint8)
        else:
            prepered_data=np.resize(unpacked_data,(img.shape[0],img.shape[1],np.sum(un_binary_mask))).astype(np.uint8)
        mask=np.full((img.shape[0],img.shape[1]),binary_mask)
        img=np.bitwise_and(img,np.invert(mask))
        bv=0
        for i,b in enumerate(un_binary_mask[::-1]):
            if b:
                temp=prepered_data[:,:,bv]
                temp=np.left_shift(temp,i)
                img=np.bitwise_or(img,temp)
                bv+=1
        return img
  3. Funkcja odkodowująca informację z obrazu. Przyjmuje jako parametr warstwę z zakodowaną informacją oraz maskę binarną użytą przy kodowaniu. Dane zwracane są w formie wartości bool z każdej z warstw podanej w masce. Dodatkowy parametr rozmiaru pozwala odtworzyć dane w innym rozmiarze np. wektora z tekstem, jaki został zakodowany.

    def pop_data(img,binary_mask=np.uint8(1),out_shape=None):
        un_binary_mask=np.unpackbits(binary_mask)
        data=np.zeros((img.shape[0],img.shape[1],np.sum(un_binary_mask))).astype(np.uint8)
        bv=0
        for i,b in enumerate(un_binary_mask[::-1]):
            if b:
                mask=np.full((img.shape[0],img.shape[1]),2**i)
                temp=np.bitwise_and(img,mask)           
                data[:,:,bv]=temp[:,:].astype(np.uint8)             
                bv+=1
        if out_shape!=None:
            tmp=np.packbits(data.flatten())        
            tmp=tmp[:np.prod(out_shape)]
            data=tmp.reshape(out_shape)
        return data

Zadania

  1. Przygotuj zestaw danych testowych (0,20 pkt):

    • co najmniej dwa różne obrazy kolorowe ≈ 512 × 512 px (inne niż Lena),

    • jeden obraz binarny o tych samych wymiarach,

    • plik tekstowy zawierający dowolny cytat (≈ 200 znaków).

  2. Za‑ i odkoduj plik tekstowy w najmłodszym bicie (LSB) kanału niebieskiego wybranego obrazu‑nosiciela (put_data, binary_mask = 1). Zweryfikuj poprawność odzyskanego tekstu (pop_data) oraz oblicz i podaj wartości PSNR / SSIM dla kanału B przed i po ukryciu danych (0,20 pkt).

  3. Ukryj cały przygotowany obraz kolorowy w obrazie‑nosiciela, zapisując (0,20 pkt):

    • 3 najmłodsze bity kanału B,

    • 2 najmłodsze bity kanałów R i G.

    Odzyskaj obraz (pop_data) i oceń jakościowo: podaj PSNR / SSIM osobno dla każdego kanału nosiciela i przedstaw wizualne porównanie „przed–po”.

  4. Systematycznie zwiększaj liczbę zapisywanych bitów (łącznie w kanałach RGB) i określ minimalny „budżet bitowy”, przy którym degradacja obrazu‑nosiciela staje się zauważalna gołym okiem. Swoje spostrzeżenia uzasadnij wartościami PSNR / SSIM oraz zrzutami ekranu (0,20 pkt).

  5. Nałóż na obraz‑nosiciela znak wodny (water_mark) w postaci przygotowanej binarnej maski (np. logo). Zbadaj wpływ wartości α = 0.10, 0.25, 0.50 na (0,20 pkt):

    • wartości PSNR / SSIM (dla całego obrazu),

    • widoczność znaku wodnego w praktyce.

Zaprezentuj wyniki w formie tabeli i krótkiego komentarza.

Do oddania

  • Plik PDF ze sprawozdaniem zawierający wyniki, obserwacje i wnioski.

Przykłady wizualne pokazujące co można zrobić

Przykład wizualny działania funkcji

Przykład dekonstrukcji warstwy obrazu na poszczególne wartości bitów przy użyciu funkcji np.bitwise_and. Proces odwrotny do dekonstrukcji to pomnożenie wartości logicznych przez wartość bitu i dodanie do obrazu.

Przykład rozkładu warstwy obrazu na poszczególne bity

Przykład wykorzystania dekonstrukcji obrazu w praktyce na obrazie kolorowym.

Przykład rozłożenia obrazu kolorowego na poszczególne bity

Przykład ukrycia kolorowego obrazu wewnątrz innego.

Przykład ukrycia kolorowego obrazu wewnątrz innego.