A Python/Flask academic prototype for embedding and evaluating grayscale image watermarks. The application performs multilevel 2D DWT, block-based SVD embedding, non-blind extraction, reference-based metrics, and configurable image attack experiments.
Algorithm details: docs/algorithm_explanation.md Academic report and measured example: docs/academic_project_report.md
This remains an educational prototype, not a production ownership, authentication, or security system. A metric being available does not establish that a watermark survived an attack; inspect NC and BER alongside the recovered image and test results.
For every watermark pixel (w_i), normalized to ([0,1]), the selected wavelet band is divided into non-overlapping 2×2 blocks. For block (B_i=U_i\Sigma_iV_i^T), embedding changes only its largest singular value:
\[\sigma'_{i,1}=\sigma_{i,1}+192\alpha w_{\pi(i)}, \qquad B'_i=U_i\operatorname{diag}(\sigma'_{i,1},\sigma_{i,2})V_i^T.\]The inverse DWT reconstructs the watermarked image. Extraction compares the largest singular values of the watermarked and original-cover blocks:
\[\hat{w}_{\pi(i)}=\operatorname{clip}\left( \frac{\sigma^{(w)}_{i,1}-\sigma^{(c)}_{i,1}}{192\alpha},0,1 \right).\]The required reference watermark supplies the expected dimensions. Its samples are not substituted for recovered samples. The optional key supplies a reproducible permutation only; it is not encryption.
The capacity for an (r\times c) selected band is (\lfloor r/2\rfloor\lfloor c/2\rfloor) samples. ALL combines the capacity of LL, LH, HL, and HH (four times a single band for equal-size level-1 bands), and is the UI default. If a single-band selection cannot fit the watermark, the application automatically switches to ALL. If it is still too large, the watermark is downscaled with area interpolation to fit; extraction uses the same fit for comparison and enlarges the recovered image for display. The enlarged image cannot restore details lost during downscaling, so watermark capacity remains finite. A deeper DWT level usually makes each selected band smaller. Crops and geometric transformations can misalign the fixed block layout.
Python 3.11 or later is recommended. The packages are listed in requirements.txt.
Windows PowerShell:
python -m venv .venv
.\.venv\Scripts\Activate.ps1
python -m pip install --upgrade pip
python -m pip install -r requirements.txt
Linux/macOS:
python -m venv .venv
source .venv/bin/activate
python -m pip install --upgrade pip
python -m pip install -r requirements.txt
The Streamlit app is the interactive image-watermarking workspace and reuses the same DWT–SVD algorithms as the Flask app:
streamlit run streamlit_app.py
Open the local URL printed by Streamlit (normally http://localhost:8501/). The interface includes embedding, reference-based extraction, configurable attack experiments, measured strength tuning, result charts, and CSV export. Strength tuning compares 15 candidate strengths against selected attacks, excludes candidates below a configurable cover-PSNR floor (40 dB by default), and ranks the rest by attack coverage, mean NC, lower BER, then lower strength. The recommended image and its exact settings can be reused in the same browser session; uploaded images are limited to 16 MiB and 16 megapixels.
$env:FLASK_SECRET_KEY = "replace-with-a-long-random-value"
python app.py
The app binds to 127.0.0.1:5000 and has debug mode off by default. To explicitly enable the development debugger locally, set FLASK_DEBUG=1; do not do so on a public server. If FLASK_SECRET_KEY is unset, a random process-local key is generated and sessions will not survive an app restart.
Open http://127.0.0.1:5000/.
Uploads are limited to 16 MiB and decoded images to 16 megapixels. Accepted formats are PNG, JPEG, BMP, WEBP, TIFF, and GIF (the first frame is used for animated GIFs). Processing converts files to grayscale.
Metrics reject mismatched dimensions and invalid ranges; arrays are never silently truncated. For the edge case of a black/zero watermark, NC is defined as 1 when both reference and extraction are all zero, and 0 when only one is zero. BER is still computed from the thresholded samples. An extraction failure is recorded with null NC/BER, not fabricated scores.
The current implementation was run on one generated 128×128 grayscale cover (NumPy seed 20261011) and a 32×32 binary checkerboard watermark, with Haar, level 1, LL, strength 0.05, and key demo-key. Clean extraction measured NC 0.9963, BER 0.0000, cover PSNR 45.29 dB, and SSIM 0.99988. The attack rows and limitations are in the academic report. These are one synthetic demonstration, not a general performance claim or an independently validated benchmark.
| Method | Route | Purpose |
|---|---|---|
| GET | / |
Home |
| GET | /embed, /extract, /robustness, /results |
Application pages |
| POST | /api/embed |
Embed and return metrics and preview/download links |
| POST | /api/extract |
Reference-based non-blind extraction |
| POST | /api/robustness |
Run selected attack experiments |
| GET | /api/results.csv |
Download the latest experiment results |
| GET | /preview/<filename> |
Display a generated output image |
| GET | /download/<filename> |
Download a generated output image |
python -m pytest -q
Tests cover multilevel/odd-sized DWT reconstruction, automatic capacity fitting, clean image recovery, reference-required extraction, metric input validation, attack failures, and Flask behavior. The recovery test asserts non-perfect NC and nonzero BER for its deterministic lossy case, and the no-reference test requires an error.
Runtime uploads/outputs, the Python virtual environment, and cache files are excluded from Git by .gitignore.