# Project export: Watermarkitects: Whose Pixel Is It Anyway?

This document was generated by HackStack to give an AI agent context about a hackathon project. Sections are labeled with their provenance; content marked as truncated was cut to keep this document small.

## Project metadata

- Hackathon: CruzHacks 2025
- Tagline: Foolproof Chrome extension that enables secure, imperceptible, tamper-resistant digital watermarking and blind watermark detection for AI-generated images, with a CNN trained to identify tampering.
- Devpost: https://devpost.com/software/watermarkitects-whose-pixel-is-it-anyway
- GitHub: https://github.com/psjcodes/watermarkitects
- Team: 3 GitHub contributor(s) — Pranay Jha (6 commits), vincent3477 (5 commits), Jaisree25 (1 commits)

## Devpost submission (written by the team)

### Inspiration

As AI image generation tools like GPT, Perplexity, and DALL·E explode in popularity, we’re seeing an overwhelming flood of AI-made images online. While it’s exciting to see what these models can create, it also opens up serious concerns: fake images, misinformation, and copyright issues. Existing watermarking solutions either aren’t strong enough (easy to remove) or need the original image to detect a watermark. We wanted to build something better: a solution that’s secure, imperceptible, tamper-resistant, and most importantly, doesn’t rely on having the original image to detect if it’s been watermarked. Our Solution We built a full-stack system that covers both watermarking and blind detection of AI-generated images: DCT-Based Watermarking: We’re embedding watermarks directly into the frequency space of images using Discrete Cosine Transform (DCT). This approach makes the watermark more durable: it holds up against compression, resizing, and noise, and also allows for imperceptibility. Chrome Extension for Real-Time Watermarking: We didn’t want this to just live in scripts or the command line. We built a Chrome extension that: Lets anyone upload an image and check if it’s watermarked. Actively scans web pages in the background for AI-generated images from tools like GPT and Perplexity, and watermarks them in real-time. Technology Details Image Processing: NumPy and OpenCV for handling computing DCTs, adding the watermarks, and PyTorch for our convolutional neural network to detect tampering with AI generated images. Chrome Extension: Built with JavaScript, HTML, and CSS to create an interactive front end. Backend: FastAPI to handle image processing pipelines, including watermark embedding and blind detection. Challenges and Lessons Extension Optimization: Running detection in real-time inside a browser was a challenge. We had to constantly monitor for changes in the DOM for a newly generated image which is passed to our backend for watermark processing. The actual of process of sending information to the backend was also difficult due to data format managing and handling CORS (Cross-Origin Resource Sharing, a mechanism that allows web applications on one domain to access resources of another domain) as a solution to the browser's Same-Origin policy. Making Tamper Identification Work: Our goal was not only to detect if an image was AI-generated using the watermark, but also to identify whether it had been tampered with to avoid detectability. We started by going down a statistical route. Since DCT coefficients tend to follow a Laplacian distribution, we thought we could model the original distribution and then estimate what the untouched DCT coefficients would look like. Using some estimation techniques, we tried to reconstruct the expected original values and compare them to the actual values from the uploaded image. However, in practice, the correlations just weren’t strong enough. Real-world images have so much variability that the statistical estimates couldn’t reliably tell us if an image had been tampered with or not. The noise overwhelmed the signal. That’s when we pivoted to a CNN-based approach. We built a script to generate a dataset of about ~1000 images, and for each image, we created several tampered versions by adding noise, resizing, and compressing them to simulate real-world attacks. Then, instead of working directly with pixels, we fed the DCT coefficients of these images into our convolutional neural network. By focusing on the frequency domain data, the CNN was able to pick up on subtle patterns and distortions that statistical methods couldn’t capture. This approach ended up giving us much better accuracy in distinguishing between tampered and original images. What's Next For Watermarkitects? Expand to Videos: Apply our watermarking and detection system to video content to combat AI-generated deepfakes. Explore Alternative Transforms: Investigate Discrete Shearlet Transform (DST) and other transforms for even stronger watermarking. Integrate with Social Platforms: Work towards integration with social media platforms to automatically flag AI-generated content. References [1] Secure spread spectrum watermarking for multimedia. (1997, December 1). IEEE Journals & Magazine | IEEE Xplore. https://ieeexplore.ieee.org/document/650120 [2] A statistical watermark detection technique without using original images for resolving rightful ownerships of digital images. (1999, November 1). IEEE Journals & Magazine | IEEE Xplore. https://ieeexplore.ieee.org/document/799882

## README (from the GitHub repository)

No README available.

## Detected evidence (automated analysis)

Indexed codebase: 8 recognized source files, 16 KB.
- HTML (language) — detected in the code
- JavaScript (language) — detected in the code
- Python (language) — detected in the code
- FastAPI (technology) — claimed on Devpost, not found in the code
- PyTorch (technology) — claimed on Devpost, not found in the code

## Codebase structure (from repository index)

### Files (15 of 15)

```
.gitignore
backend/app.py
backend/test_app.py
backend/watermark/bruh.py
backend/watermark/trainTamperClassifier_1.ipynb
backend/watermark/watermark.py
backend/watermark/wm_test.ipynb
backend/watermark/wm2_test.ipynb
backend/watermark/wm3_test.ipynb
extention/background.js
extention/content.js
extention/manifest.json
extention/popup.html
extention/popup.js
requirements.txt
```

### Dependencies

- requirements.txt: ipykernel, jupyter, matplotlib, numpy, opencv-python, scipy

### Recent commits (newest first)

- fix comments again
- remove code cell
- fix comments
- fix comments
- finish tamper classifier nn
- everything else
- lot of stuff
- SHIT ISNT WORKING
- temporarily sharing
- more file stuff
- Create wm_test.ipynb
- Create .gitignore

## Key source files (fetched from GitHub, selected and truncated for size)

### requirements.txt

```
ipykernel
jupyter
numpy
opencv-python
matplotlib
scipy
```

### backend/app.py

```python
from fastapi import FastAPI, UploadFile, File, Form, Request, HTTPException
from fastapi.responses import JSONResponse, StreamingResponse
from io import BytesIO
from PIL import Image
from watermark.watermark import detect_watermark
from watermark.watermark import embed_watermark
from fastapi.middleware.cors import CORSMiddleware


app = FastAPI()

app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

#image from event listener (image generator: chatgpt, etc.) --> add watermark
@app.post("/process/")
async def image_processing(file: UploadFile = File(...), signature: str = Form(...)):
    print("Received request")
    contents = await file.read()
    image = Image.open(BytesIO(contents))
    print(image)

    if len(signature) != 0:
        watermarked_image = embed_watermark(image, signature)
        watermarked_image = watermarked_image[..., ::-1]
        pil_image = Image.fromarray(watermarked_image)
        buf = BytesIO()
        pil_image.save(buf, format="PNG")
        buf.seek(0)

        return StreamingResponse(buf, media_type="image/png")
    else:
        raise HTTPException(status_code=400, detail="no signature")


#image from popup --> extract and identify watermark if present
@app.post("/analyze/")
async def analyze_image(file: UploadFile = File(...)):
    contents = await file.read()
    image = Image.open(BytesIO(contents))

    result = detect_watermark(image)
    #detect_watermark(image)
    return JSONResponse(content=result)

```

### backend/test_app.py

```python
from fastapi import FastAPI

app = FastAPI()

@app.get("/")
def read_root():
    return {"message": "Hello, FastAPI!"}

```

### extention/popup.html

```html
<!DOCTYPE html>
<html>
  <head>
    <style>
      body {
        font-family: Monaco, Monospace;
        padding: 10px;
        width: 250px;
        background-color:powderblue;
        font-size: 20px
      }

      h3 {
        margin-bottom: 12px;
        text-align: center;
      }

      input[type="file"],
      input[type="text"],
      button {
        display: block;
        width: 100%;
        margin-bottom: 10px;
        background-color:white;
      }

      #status {
        text-align: center;
        color: #ed0909;
        font-weight: bold;
      }
    </style>
  </head>
  <body>
    <h3>Watermarkitects</h3>

    <input type="file" id="fileInput" />

    <button id="uploadBtn">Upload</button>

    <p id="status"></p>

    <script src="popup.js"></script>
  </body>
</html>


```

### extention/popup.js

```javascript
document.getElementById("uploadBtn").addEventListener("click", async () => {
  const fileInput = document.getElementById("fileInput");
  const urlInput = document.getElementById("imageUrlInput");
  const status = document.getElementById("status");

  let imageBlob;

  if (fileInput.files.length > 0) {
    imageBlob = fileInput.files[0];
  } else if (urlInput.value.trim() !== "") {
    try {
      const response = await fetch(urlInput.value.trim());
      if (!response.ok) throw new Error("Failed to fetch image from URL.");
      imageBlob = await response.blob();
    } catch (error) {
      status.textContent = "Invalid image URL or fetch failed.";
      return;
    }
  } else {
    status.textContent = "Please upload a file or enter an image URL.";
    return;
  }

  const formData = new FormData();
  formData.append("file", imageBlob, "input.jpg");

  try {
    const res = await fetch("http://localhost:8000/analyze/", {
      method: "POST",
      body: formData,
    });

    if (!res.ok) {
      status.textContent = "Error analyzing image.";
      return;
    }

    const data = await res.json();
    const detected = String(data["Watermark Detected"]).toLowerCase() === "true";
    status.innerHTML = `Watermarked: ${data["Watermark Detected"]}${detected ? `<br>Generator: ${data["Generator"]}` : ''}`;

  } catch (err) {
    console.error(err);
    status.textContent = "Server error.";
  }
});

```

### extention/content.js

```javascript
let targetClass = '';
let targetImgElement = '';
let signature = '';

// if current URI ~ chatgpt:
if (window.location.href.includes("chatgpt.com")) {
  console.log("You're on ChatGPT")
  targetClass = 'group/imagegen-image';
  targetImgElement = 'img[alt="Generated image"]'
  signature = 'ChatGPT'
}

//  else set targetclass and targetimgelement to perplexity's
if (window.location.href.includes("perplexity.ai")) {
  console.log("You're on Perplexity")
  targetClass = '@container';
  targetImgElement = 'img[alt="FLUX"]'
  signature = 'Perplexity'
}

console.log("hello1");

const observer = new MutationObserver((mutationsList, observer) => {
  for (let mutation of mutationsList) {
    if (mutation.type === 'childList') {
      const targetDivs = document.getElementsByClassName(targetClass);
      const targetDiv = targetDivs[targetDivs.length - 1];  // get last added
      if (targetDiv) {
        const img = targetDiv.querySelector(targetImgElement);
        if (img && img.src) {
          const imgUrl = img.src;
          downloadImage(imgUrl);  // call async wrapper
        }
      }
    }
  }
});

observer.observe(document.body, {
  childList: true,
  subtree: true,
});

// Function to download the image, now correctly marked async
/*async function downloadImage(url) {
  console.log(url)
  const res = await fetch(url, {
       method: "GET"
  })

  const imageBlob = await res.blob()
  console.log(imageBlob)

  const formData = new FormData();
  formData.append("file", imageBlob, "input.png");
  console.log("we are actually here")

  try {
    const res = await fetch("http://localhost:8000/process/", {
      method: "POST",
      body: formData,
    });
    console.log("we are here")
    const wmBlob = await res.blob()
    const wmImageUrl = URL.createObjectURL(wmBlob)
    const imgElement = document.createElement('img')
    imgElement.src = wmImageUrl
    imgElement.alt = "Watermarked Image"
    document.body.appendChild(imgElement)

  } catch (error) {
    console.error(error)
  }

}*/

async function downloadImage(url) {
  console.log(url);

  try {
    const imageResponse = await fetch(url, { method: "GET" });
    const imageBlob = await imageResponse.blob();
    console.log("Blob:", imageBlob);

    const formData = new FormData();
    formData.append("file", imageBlob, "input.png");
    formData.append("signature", signature);
    console.log("we are actually here");

    const backendResponse = await fetch("http://localhost:8000/process/", {
      method: "POST",
      body: formData,
    });

    if (!backendResponse.ok) {
      throw new Error(`Backend error: ${backendResponse.status}`);
    }

    console.log("we are here");

    const wmBlob = await backendResponse.blob();
    const wmImageUrl = URL.createObjectURL(wmBlob);
    console.log("wm blob:", wmBlob)
    const imgElements = document.querySelectorAll(targetImgElement)
    console.log(imgElements)
    const imgElement = imgElements[imgElements.length - 1]
    if (imgElement) {
      const newImgElement = imgElement.cloneNode(true)
      newImgElement.src = wmImageUrl
      newImgElement.alt = "Watermarked Image"
      imgElement.parentNode.replaceChild(newImgElement, imgElement)
    }


    /*const targetDivs = document.getElementsByClassName(targetClass);
    const targetDiv = targetDivs[targetDivs.length - 1];
    if (targetDiv) {
        const img = targetDiv.querySelector('img');
        if (img && img.src) {
          img.src = wmImageUrl;
        }
      }
    const newElement = targetDiv.cloneNode(true); // or false if you don't need deep clone
    targetDiv.parentNode.replaceChild(newElement, targetDiv);*/

  } catch (error) {
    console.error("Something went wrong:", error);
  }


}


```

### backend/watermark/bruh.py

```python
import numpy as np
import pywt
import os
from PIL import Image
from scipy.fftpack import dct
from scipy.fftpack import idct

current_path = str(os.path.dirname(__file__))  

image = 'imagetest1.jpg'   
watermark = 'qrcodetest1.png' 

def convert_image(image_name, size):
    img = Image.open('./pictures/' + image_name).resize((size, size), 1)
    img = img.convert('L')
    img.save('./dataset/' + image_name)

 
    image_array = np.array(img.getdata(), dtype=np.float).reshape((size, size))
    print image_array[0][0]               
    print image_array[10][10]             

    return image_array

def process_coefficients(imArray, model, level):
    coeffs=pywt.wavedec2(data = imArray, wavelet = model, level = level)
    # print coeffs[0].__len__()
    coeffs_H=list(coeffs) 
   
    return coeffs_H


def embed_mod2(coeff_image, coeff_watermark, offset=0):
    for i in xrange(coeff_watermark.__len__()):
        for j in xrange(coeff_watermark[i].__len__()):
            coeff_image[i*2+offset][j*2+offset] = coeff_watermark[i][j]

    return coeff_image

def embed_mod4(coeff_image, coeff_watermark):
    for i in xrange(coeff_watermark.__len__()):
        for j in xrange(coeff_watermark[i].__len__()):
            coeff_image[i*4][j*4] = coeff_watermark[i][j]

    return coeff_image

            
    
def embed_watermark(watermark_array, orig_image):
    watermark_array_size = watermark_array[0].__len__()
    watermark_flat = watermark_array.ravel()
    ind = 0

    for x in range (0, orig_image.__len__(), 8):
        for y in range (0, orig_image.__len__(), 8):
            if ind < watermark_flat.__len__():
                subdct = orig_image[x:x+8, y:y+8]
                subdct[5][5] = watermark_flat[ind]
                orig_image[x:x+8, y:y+8] = subdct
                ind += 1 


    return orig_image
      


def apply_dct(image_array):
    size = image_array[0].__len__()
    all_subdct = np.empty((size, size))
    for i in range (0, size, 8):
        for j in range (0, size, 8):
            subpixels = image_array[i:i+8, j:j+8]
            subdct = dct(dct(subpixels.T, norm="ortho").T, norm="ortho")
            all_subdct[i:i+8, j:j+8] = subdct

    return all_subdct


def inverse_dct(all_subdct):
    size = all_subdct[0].__len__()
    all_subidct = np.empty((size, size))
    for i in range (0, size, 8):
        for j in range (0, size, 8):
            subidct = idct(idct(all_subdct[i:i+8, j:j+8].T, norm="ortho").T, norm="ortho")
            all_subidct[i:i+8, j:j+8] = subidct

    return all_subidct


def get_watermark(dct_watermarked_coeff, watermark_size):
    
    subwatermarks = []

    for x in range (0, dct_watermarked_coeff.__len__(), 8):
        for y in range (0, dct_watermarked_coeff.__len__(), 8):
            coeff_slice = dct_watermarked_coeff[x:x+8, y:y+8]
            subwatermarks.append(coeff_slice[5][5])

    watermark = np.array(subwatermarks).reshape(watermark_size, watermark_size)

    return watermark


def recover_watermark(image_array, model='haar', level = 1):


    coeffs_watermarked_image = process_coefficients(image_array, model, level=level)
    dct_watermarked_coeff = apply_dct(coeffs_watermarked_image[0])
    
    watermark_array = get_watermark(dct_watermarked_coeff, 128)

    watermark_array =  np.uint8(watermark_array)

#Save result
    img = Image.fromarray(watermark_array)
    img.save('./result/recovered_watermark.jpg')


def print_image_from_array(image_array, name):
  
    image_array_copy = image_array.clip(0, 255)
    image_array_copy = image_array_copy.astype("uint8")
    img = Image.fromarray(image_array_copy)
    img.save('./result/' + name)



def w2d(img):
    model = 'haar'
    level = 1
    image_array = convert_image(image, 2048)
    watermark_array = convert_image(watermark, 128)

    coeffs_image = process_coefficients(image_array, model, level=level)
    dct_array = apply_dct(coeffs_image[0])
    dct_array = embed_watermark(watermark_array, dct_array)
    coeffs_image[0] = inverse_dct(dct_array)
  

# reconstruction
    image_array_H=pywt.waverec2(coeffs_image, model)
    print_image_from_array(image_array_H, 'image_with_watermark.jpg')



# recover images
    recover_watermark(image_array = image_array_H, model=model, level = level)


w2d("test")
```

### backend/watermark/watermark.py

```python
import cv2
import numpy as np
import hashlib
import matplotlib.pyplot as plt

def embed_watermark(image, signature):
    image = np.array(image)
    image = image[..., ::-1]

    return _embed_watermark(image, signature)

def _embed_watermark(original_img, signature, strength=0.1):
    # Convert to YCbCr and extract Y channel
    img_ycbcr = cv2.cvtColor(original_img, cv2.COLOR_BGR2YCrCb)
    y_channel = img_ycbcr[:, :, 0].astype(np.float32)

    # Generate watermark sequence using hash of signature
    seed = int(hashlib.sha256(signature.encode()).hexdigest(), 16) % (2**32)
    np.random.seed(seed)
    watermark = np.random.randn(*y_channel.shape)  # Gaussian sequence

    # Split into 8x8 blocks and apply DCT
    watermarked = y_channel.copy()
    h, w = y_channel.shape
    for i in range(0, h, 8):
        for j in range(0, w, 8):
            block = y_channel[i:i+8, j:j+8]
            dct_block = cv2.dct(block)
            if dct_block.shape != (8,8):
                break
            # Select mid-frequency coefficients (example: indices 5-20 in zigzag order)
            mask = np.zeros((8, 8), dtype=bool)
            mask.flat[5:20] = True  # Adjust based on JND thresholds

            # Embed watermark into selected coefficients
            dct_block[mask] += strength * watermark[i:i+8, j:j+8][mask]
            
            # Inverse DCT
            watermarked_block = cv2.idct(dct_block)
            watermarked[i:i+8, j:j+8] = watermarked_block

    # Reconstruct YCbCr and convert back to BGR
    img_ycbcr[:, :, 0] = np.clip(watermarked, 0, 255)
    watermarked_img = cv2.cvtColor(img_ycbcr, cv2.COLOR_YCrCb2BGR)
    return watermarked_img.astype(np.uint8)

def detect_multiple_watermarks(test_img, candidate_signatures, strength=0.1, threshold=3.0):
    img_ycbcr = cv2.cvtColor(test_img, cv2.COLOR_BGR2YCrCb)
    y_channel = img_ycbcr[:, :, 0].astype(np.float32)
    
    best_q = -np.inf
    best_index = -1
    
    for sig_idx, signature in enumerate(candidate_signatures):
        # Regenerate watermark for this signature
        seed = int(hashlib.sha256(signature.encode()).hexdigest(), 16) % (2**32)
        np.random.seed(seed)
        watermark = np.random.randn(*y_channel.shape)
        
        # Compute correlation
        correlations = []
        h, w = y_channel.shape
        for i in range(0, h, 8):
            for j in range(0, w, 8):
                block = y_channel[i:i+8, j:j+8]
                dct_block = cv2.dct(block)
                if dct_block.shape != (8, 8):
                    break

                mask = np.zeros((8, 8), dtype=bool)
                mask.flat[5:20] = True
                selected_coeffs = dct_block[mask]
                selected_watermark = watermark[i:i+8, j:j+8][mask]
                if len(selected_coeffs) > 0:
                    corr = np.dot(selected_coeffs.flatten(), selected_watermark.flatten())
                    correlations.append(corr)
        
        # Calculate test statistic
        n = len(correlations)
        if n == 0:
            continue
        mean_corr = np.mean(correlations)
        std_corr = np.std(correlations)
        q = (mean_corr / std_corr) * np.sqrt(n)
        
        # Track best match
        if q > best_q:
            best_q = q
            best_index = sig_idx

    # Determine result
    detected = best_q > threshold
    return {
        'detected': detected,
        'best_match_index': best_index if detected else -1,
        'best_match_q': best_q,
        'all_q_values': [best_q if i == best_index else 0 for i in range(len(candidate_signatures))]
    }

def detect_watermark(test_img):

    test_img = np.array(test_img)
    test_img = test_img[..., ::-1]

    # Candidate signatures (up to 3)
    candidates = [
        "ChatGPT",
        "Perplexity"
    ]

    # Detect watermarks
    result = detect_multiple_watermarks(test_img, candidates, threshold=3.0)

    print(f"Watermark Detected: {result['detected']}")
    if result['detected']:
        print(f"Matched Signature Index: {result['best_match_index']}")
        print(f"Matched Signature Text: {candidates[result['best_match_index']]}")
        print(f"Confidence (q-value): {result['best_match_q']:.2f}")


    print("\nAll Q-values:")
    for idx, q in enumerate(result['all_q_values']):
        print(f"Signature {idx} ({candidates[idx]}): {q:.2f}")

    return {"Watermark Detected": str(result["detected"]), "Generator": candidates[result["best_match_index"]]}

```