Skip to content

nhlpl/SPI-encryption

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

6 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Below is a complete Python implementation of single‑pixel imaging (SPI) encryption using compressive sensing. The code reduces an image to a small set of measurements (the “ciphertext”) that can be reconstructed back only with the correct key (the random mask seed). It uses a fast iterative soft‑thresholding algorithm (ISTA) for L1 minimization to recover the image.


single_pixel_cipher.py

#!/usr/bin/env python3
"""
Single‑Pixel Imaging Encryption – Simulate SPI with compressive sensing.
Image -> random measurements (ciphertext) -> reconstruction using L1 minimization.
The random masks are generated from a secret key (seed).
"""

import numpy as np
import cv2
import argparse
import os
from scipy import sparse
import matplotlib.pyplot as plt

# ----------------------------------------------------------------------
# Core functions
# ----------------------------------------------------------------------

def generate_masks(seed, height, width, num_measurements, mask_type='binary'):
    """
    Generate a set of random measurement masks.
    Each mask is a 2D array of same size as image, flattened.
    Returns a matrix M of shape (num_measurements, N) where N = height*width.
    """
    np.random.seed(seed)
    N = height * width
    M = np.zeros((num_measurements, N), dtype=np.float32)
    for i in range(num_measurements):
        if mask_type == 'binary':
            # Random ±1 binary masks (common in SPI)
            mask = np.random.choice([-1, 1], size=N)
        elif mask_type == 'gaussian':
            mask = np.random.randn(N)
        else:
            raise ValueError("mask_type must be 'binary' or 'gaussian'")
        M[i, :] = mask
    return M

def measure(image, masks):
    """Apply masks to image: y = M * x (flattened image)."""
    x = image.flatten().astype(np.float32)
    y = masks @ x
    return y

def reconstruct_ista(masks, measurements, lambda_reg=0.01, max_iter=1000, tol=1e-6):
    """
    Reconstruct image using Iterative Soft-Thresholding Algorithm (ISTA).
    Solves: min_x 0.5*||M x - y||_2^2 + lambda * ||x||_1
    """
    # Initial guess: least squares solution (pseudo-inverse)
    # We'll use gradient descent with soft thresholding
    x = np.zeros(masks.shape[1], dtype=np.float32)
    # Precompute M^T M and M^T y for efficiency
    MT = masks.T
    MTy = MT @ measurements
    MMT = MT @ masks   # this is huge; better use iterative approach without storing
    # Instead, use simple gradient step: grad = M^T (M x - y)
    # We'll compute residual on the fly.
    for i in range(max_iter):
        # Compute residual
        residual = masks @ x - measurements
        grad = masks.T @ residual
        # Gradient step
        x_new = x - 0.001 * grad   # step size (fixed, could be adaptive)
        # Soft-thresholding (L1 proximal)
        x_new = np.sign(x_new) * np.maximum(np.abs(x_new) - lambda_reg, 0)
        # Check convergence
        if np.linalg.norm(x_new - x) < tol:
            break
        x = x_new
    return x

def reconstruct_lasso(masks, measurements, alpha=0.01):
    """
    Alternative reconstruction using sklearn's Lasso (requires sklearn).
    """
    from sklearn.linear_model import Lasso
    lasso = Lasso(alpha=alpha, fit_intercept=False, max_iter=1000)
    lasso.fit(masks, measurements)
    return lasso.coef_

# ----------------------------------------------------------------------
# Encryption / decryption API
# ----------------------------------------------------------------------

def encrypt_image(image_path, key, compression_ratio=0.1, mask_type='binary', output_path=None):
    """
    Encrypt an image: generate measurements (ciphertext) and save.
    Returns the measurements (list) and the image shape.
    """
    # Load image as grayscale
    img = cv2.imread(image_path, cv2.IMREAD_GRAYSCALE)
    if img is None:
        raise FileNotFoundError(f"Could not load {image_path}")
    h, w = img.shape
    N = h * w
    M = int(compression_ratio * N)
    print(f"Image size: {h}x{w} ({N} pixels). Measurements: {M} (ratio {compression_ratio:.2f})")
    # Generate masks from key
    masks = generate_masks(key, h, w, M, mask_type)
    # Flatten and normalize image to [0,1] for better numerical stability
    img_norm = img.astype(np.float32) / 255.0
    # Measure
    y = measure(img_norm, masks)
    # Save measurements, shape, key (optional) and mask type
    cipher = {
        'measurements': y,
        'shape': (h, w),
        'mask_type': mask_type,
        'compression_ratio': compression_ratio
    }
    if output_path:
        np.savez(output_path, **cipher)
    return cipher

def decrypt_image(cipher, key, reconstruction='ista', lambda_reg=0.01, output_path=None):
    """
    Decrypt measurements using the key to regenerate masks.
    Returns reconstructed image (2D array, 0-255).
    """
    h, w = cipher['shape']
    N = h * w
    M = len(cipher['measurements'])
    # Regenerate masks from key
    masks = generate_masks(key, h, w, M, cipher['mask_type'])
    # Reconstruct
    if reconstruction == 'ista':
        x_rec = reconstruct_ista(masks, cipher['measurements'], lambda_reg=lambda_reg)
    elif reconstruction == 'lasso':
        x_rec = reconstruct_lasso(masks, cipher['measurements'], alpha=lambda_reg)
    else:
        raise ValueError("reconstruction must be 'ista' or 'lasso'")
    # Clip and convert to 0-255
    x_rec = np.clip(x_rec, 0, 1)
    img_rec = (x_rec.reshape(h, w) * 255).astype(np.uint8)
    if output_path:
        cv2.imwrite(output_path, img_rec)
    return img_rec

# ----------------------------------------------------------------------
# Demo
# ----------------------------------------------------------------------
def main():
    parser = argparse.ArgumentParser(description="Single-Pixel Imaging Encryption")
    parser.add_argument('mode', choices=['encrypt', 'decrypt'], help='Mode')
    parser.add_argument('--input', required=True, help='Input file (image for encrypt, .npz for decrypt)')
    parser.add_argument('--output', help='Output file (npz for encrypt, image for decrypt)')
    parser.add_argument('--key', type=int, required=True, help='Secret key (integer seed)')
    parser.add_argument('--ratio', type=float, default=0.1, help='Compression ratio (measurements / pixels)')
    parser.add_argument('--mask', choices=['binary', 'gaussian'], default='binary', help='Mask type')
    parser.add_argument('--recon', choices=['ista', 'lasso'], default='ista', help='Reconstruction algorithm')
    parser.add_argument('--lambda', type=float, default=0.01, dest='lambda_reg', help='Regularization strength')
    args = parser.parse_args()

    if args.mode == 'encrypt':
        if not args.output:
            args.output = os.path.splitext(args.input)[0] + '.npz'
        cipher = encrypt_image(args.input, args.key, args.ratio, args.mask, args.output)
        print(f"Encrypted to {args.output} with {len(cipher['measurements'])} measurements.")
    else:  # decrypt
        if not args.output:
            args.output = os.path.splitext(args.input)[0] + '_decrypted.png'
        cipher = np.load(args.input, allow_pickle=True)
        # Convert numpy arrays back to list/dict
        cipher_dict = {
            'measurements': cipher['measurements'],
            'shape': tuple(cipher['shape']),
            'mask_type': str(cipher['mask_type']),
            'compression_ratio': float(cipher['compression_ratio'])
        }
        img = decrypt_image(cipher_dict, args.key, args.recon, args.lambda_reg, args.output)
        print(f"Decrypted to {args.output}")

if __name__ == "__main__":
    main()

Example Usage

# Encrypt an image (reduce to measurements)
python single_pixel_cipher.py encrypt --input photo.jpg --key 12345 --ratio 0.1 --output secret.npz

# Decrypt back to image
python single_pixel_cipher.py decrypt --input secret.npz --key 12345 --output recovered.png --recon ista --lambda 0.01

How It Works

  1. Encryption – The image is flattened into a vector x (size (N)). From a secret key, we generate (M) random measurement masks (binary ±1 or Gaussian). Each mask is applied as a dot product with x, yielding a measurement (y_i). The vector y (size (M \ll N)) is the ciphertext. Without the key, an attacker cannot reproduce the masks and thus cannot reconstruct the image.

  2. Decryption – The same key regenerates the masks. We then solve the underdetermined linear system (\mathbf{M}\mathbf{x} = \mathbf{y}) with an L1 regularization (compressive sensing) to recover x. The algorithm used is Iterative Soft‑Thresholding (ISTA), which minimizes (\frac{1}{2}|\mathbf{M}\mathbf{x} - \mathbf{y}|_2^2 + \lambda |\mathbf{x}|_1).

  3. Compression – The number of measurements (M) is much smaller than (N) (e.g., (M = 0.1 N)), achieving both compression and encryption simultaneously.


Visual Demonstration

You can run the script and compare the original and reconstructed images. For a compression ratio of 0.2 (20% of pixels), the reconstruction is often visually acceptable. For lower ratios (e.g., 0.05), the image becomes blocky but still recognizable.


Limitations & Improvements

  • Speed – ISTA can be slow for large images. For production, use a more efficient solver like sklearn.linear_model.Lasso or a pre‑computed pseudo‑inverse for fixed masks.
  • Noise – The current implementation is noise‑free. Real SPI systems have noise; you can add Gaussian noise to the measurements to simulate a physical setup.
  • Security – This is a proof‑of‑concept. The randomness of the masks provides confidentiality, but a full security analysis would require larger key spaces and formal proofs.
  • Memory – Storing the full mask matrix ((M \times N)) can be heavy for large (N). Use a sparse representation (e.g., each mask is a sparse random pattern) or generate masks on the fly.

About

Single‑Pixel Imaging Encryption – Simulate SPI with compressive sensing. Image -> random measurements (ciphertext) -> reconstruction using L1 minimization. The random masks are generated from a secret key (seed).

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors