A Dubious Attempt at Webcam-Based QFD

Inspired by Scott’s QFD, I attempted a modified version of the experiment described in the referenced paper. Using a Logitech C270 webcam, each photodiode on the webcam sensor acts as a noise source, analogous to the Zener diodes in the original experiment.

Part of the webcam’s internal plastic housing was trimmed and the built-in indicator LED bent to illuminate the sensor.

Some limitations are:

  • Sample rate: The webcam operates at 30 fps (30 Hz), far lower than the 1000 Hz sampling rate used in the original experiment.
  • Dynamic range: The webcam’s limited dynamic range is likely insufficient for reading quantum noise.

The live chart shows:

  • Temporal coherence: Measured by averaging the autocorrelation of the signals from the selected pixels (16 evenly-spaced pixels) across a short time window (133.33 ms / 4 frames).
  • Spatial coherence: Measured by averaging the Pearson correlation between signals from the selected pixels.

The code (tested on Windows)

import cv2
import subprocess
import sys
import platform
import math
import numpy as np
import time
import winsound
import threading
import matplotlib.pyplot as plt
from collections import deque
from scipy.stats import pearsonr

TEMPORAL_COHERENCE_ANOMALY_THRESHOLD = 0.3
SPATIAL_COHERENCE_ANOMALY_THRESHOLD = 0.1

def list_cameras():
    cameras = {}
    if platform.system() == "Windows":
        try:
            import pygrabber.dshow_graph
            graph = pygrabber.dshow_graph.FilterGraph()
            devices = graph.get_input_devices()
            cameras = {i: name for i, name in enumerate(devices)}
        except ImportError:
            print("Install 'pygrabber' for better camera detection on Windows: pip install pygrabber")
    elif platform.system() in ["Linux", "Darwin"]:
        try:
            result = subprocess.run(["ffmpeg", "-f", "avfoundation", "-list_devices", "true", "-i", ""], capture_output=True, text=True)
            lines = result.stderr.split("\n")
            for i, line in enumerate(lines):
                if "[AVFoundation input device @ " in line:
                    cam_name = line.split("]")[1].strip().strip('"')
                    cameras[i] = cam_name
        except FileNotFoundError:
            print("Install 'ffmpeg' for better camera detection: https://ffmpeg.org/download.html")
    return cameras

def find_camera_index(camera_name):
    cameras = list_cameras()
    for index, name in cameras.items():
        if camera_name.lower() in name.lower():
            return index
    return None

def temporal_coherence_alert():
    winsound.Beep(4000, 200)

def spatial_coherence_alert():
    winsound.Beep(4000, 200)
    time.sleep(0.1)
    winsound.Beep(4000, 200)

buffer = [[] for _ in range(16)]
temporal_coherence_history = deque(maxlen=60)
spatial_coherence_history = deque(maxlen=60)

time_stamps = deque(maxlen=60)

if len(sys.argv) < 2:
    print('Usage: ./webcam_qfd.py <camera_name_keyword>\n')
    print('Example: ./webcam_qfd.py "logi c270"\n')
    print('Available cameras:')
    cameras = list_cameras()
    for index, name in cameras.items():
        print(f"{index}: {name}")
    exit()

# The sensor should ideally be uniformly illuminated by a diffused LED
camera_name_to_search = sys.argv[1]

cap = cv2.VideoCapture(find_camera_index(camera_name_to_search))
if not cap.isOpened():
    print("Error: Could not open webcam.")
    exit()

plt.ion()
fig, ax = plt.subplots()

while True:
    ret, frame = cap.read()
    if not ret:
        print("Error: Could not read frame.")
        break

    height, width, _ = frame.shape
    grid_size_x = width // 5
    grid_size_y = height // 5
    pixel_positions = [(i * grid_size_x, j * grid_size_y) for i in range(1, 5) for j in range(1, 5)]

    for i, (x, y) in enumerate(pixel_positions):
        buffer[i].append(int(frame[y, x, 1]))

    for (x, y) in pixel_positions:
        cv2.circle(frame, (x, y), 1, (0, 0, 0), -1)

    cv2.imshow("Webcam Noise Coherence Tracker", frame)

    if len(buffer[0]) == 30:
        avg_samples = np.mean(np.nan_to_num(buffer), axis=0)
        autocorrelation_values = [np.corrcoef(avg_samples[:-lag], avg_samples[lag:])[0, 1] for lag in range(3, 8)]
        temporal_coherence = np.mean(np.nan_to_num(autocorrelation_values))
        if abs(temporal_coherence) > TEMPORAL_COHERENCE_ANOMALY_THRESHOLD:
            print(f"Temporal coherence anomaly detected at {time.strftime('%Y-%m-%d %H:%M:%S', time.localtime())}: {temporal_coherence}")
            threading.Thread(target=temporal_coherence_alert).start()
        temporal_coherence_history.append(temporal_coherence)
        
        correlations = []
        for i in range(len(buffer)):
            for j in range(i + 1, len(buffer)):
                corr, _ = pearsonr(buffer[i], buffer[j])
                if math.isnan(corr):
                    corr = 0
                correlations.append(corr)
        spatial_coherence = np.mean(np.nan_to_num(correlations))
        if abs(spatial_coherence) > SPATIAL_COHERENCE_ANOMALY_THRESHOLD:
            print(f"Spatial coherence anomaly detected at {time.strftime('%Y-%m-%d %H:%M:%S', time.localtime())}: {spatial_coherence}")
            threading.Thread(target=spatial_coherence_alert).start()
        spatial_coherence_history.append(spatial_coherence)
        
        time_stamps.append(time.time())
        
        ax.clear()
        ax.plot(time_stamps, list(temporal_coherence_history), label='Temporal Coherence')
        ax.plot(time_stamps, list(spatial_coherence_history), label='Spatial Coherence')
        ax.set_ylim(-0.5, 0.5)
        ax.set_title("Live Noise Coherence Tracking")
        ax.set_ylabel("Coherence Value")
        ax.set_xticks([])
        ax.legend()
        plt.draw()
        plt.pause(0.01)
        
        buffer = [[] for _ in range(len(buffer))]
    
    if cv2.waitKey(1) & 0xFF == ord('q'):
        break    

cap.release()
cv2.destroyAllWindows()
plt.ioff()
plt.show()

While this is nowhere near a functional setup, it could be a fun experiment for exploring the idea using cheap readily available hardware.

1 Like

It’s great to see this level of effort in our art.

The pixels in most cameras are individual photodiodes/photodetectors, so they produce a signal proportional to the number of photons on their surface, plus some leakage current, which itself is mostly random noise. The noise in the optical signal is random, and would be characterized as photon shot noise.

While photon shot noise is quantum mechanical and theoretically random, it is not the same as variations in quantum tunneling current, which is the basis of the Quantum Field Detector.

I wonder about the leakage current in your detector. Is it large enough to extract entropy? If you are interested, you could get an idea about this by shielding the detector from light and running your measurements.

I went ahead and did some analysis of the noise in one of the corner pixels, as corner areas are usually noisier than the center. I locked both the gain and exposure time and captured 1000 samples with and without illumination. The leakage current you mentioned should be part of the dark noise. Here are the results:

c270_noises.zip (4.1 KB)

  • Red channel:
    • Dark noise variance: 3.04
    • Bright noise variance: 11.29
  • Green channel:
    • Dark noise variance: 2.20
    • Bright noise variance: 5.78
  • Blue channel:
    • Dark noise variance: 5.67
    • Bright noise variance: 31.71

Now, if we assume that the dark noise is classical, and the bright noise contains both classical and quantum components, using quadratic summation​,

σ_total² = σ_classical² + σ_quantum²

In all three channels, the quantum component appears to dominate. In the red channel, the quantum noise makes up about 73% of the total noise, in the green channel about 62%, and in the blue channel about 82%.

But again, I’d say this experiment is far from ideal. The MJPEG compression from the webcam likely introduces some artifacts or changes in the noise characteristics.

Thanks for doing these measurements – they are revealing. In individual photodiodes, the dark current is mostly due to non-classical sources. I assume most of the dark noise may therefore be quantum in nature. The use of dark noise is a big advantage because you don’t have to disassemble anything or apply a uniform low-level illumination, just use an opaque covering over the input lens.

There are a number of ways – some better than others – to combine these signals to produce a high quality random sequence. At the first level, XOr all the bits from each sensor to provide 3 signals for each RGB pixel. XOring all the bits is the same as measuring the parity of each output word, though probably only the lower significant bits, perhaps 4, but never more than 8, will get all the entropy available. Finally, XOr the 3 parity bits to produce a (potentially) final output bit. Separate statistical measurements of sequences of the 3 parity bits will indicate how much additional processing would be necessary to achieve the desired level of entropy per bit. From these results, it will be possible to accurately calculate the statistical properties of their XOred combination. It’s even possible to measure the entropy in each of the original output words/bits, allowing theoretical calculation from the beginning, but I believe in the simplest approach, which I outlined above. It’s hard to know without seeing any of this more specific information, but it is also possible to correct the statistical defects, provided the entropy is near 1 (above 0.9 is probably adequate). This is best done by inputting only entropy-containing bits into an optimized linear feedback shift register (LFSR), which will powerfully remove defects without masking the true entropy with deterministic whitening algorithms.

Thank you for your insight.


Regarding dark current, according to much of the literature I’ve read, the primary source of dark current in photodiodes is the thermal generation of carriers, which is a classical process rather than quantum.

Shieh, William, et al. “Optical communication fundamentals.”

The dark current I_d flows through the reversed-biased photodiode even in the absence of incoming light. The dark current consists of electron-hole pairs created due to thermal effects in the p-n junction.

Merchant, Fatima A., et al. “Multispectral fluorescence imaging.”

Dark current is another intrinsic source of noise present in photodetectors. It is the number of induced electrons per pixel that arise from sources such as thermal agitation. Thermal energy is mostly responsible for the generation of dark current, with higher temperatures resulting in higher kinetic energy.

“Maximise the Sensitivity and Decrease the Noise in Scientific Cameras.” Oxford Instruments

The second source of noise is dark noise, also commonly called dark current. It arises from heat generated ionisation in the silicon sensor. It is called dark noise as this noise is generated without any signal applied i.e. in total darkness.

Given this understanding, my rationale for using a light source, in this case the webcam’s own “active” indicator LED, is to introduce shot noise, which is a quantum effect.


As for the XOR and LFSR approach, since I’m aiming to replicate the original experiment,

The device built for this experiment was called a QNG, for “quantum noise generator” (see Fig. 1). Instead of converting electronic noise into random bits, like a commercial RNG, the QNG recorded the analog noise produced by a Zener diode. The noise was digitized with 16-bit resolution, sampled at 1 KHz, and stored on a microSD card. Sixteen independent circuits in the device were used to form 16 parallel sources of random data.

While XOR and LFSR are well-established methods for entropy extraction in an RNG, I believe they are unsuitable for replicating the experiment or for QFD application as the raw analog signal is needed for analyzing the noise properties, such as the autocorrelation (temporal coherence) of each detector’s output, and the correlation / mutual information metric across outputs of multiple identical detectors (spatial coherence).

I’d like to correct my point regarding dark current. I previously described dark current as a classical process, since it arises from thermally generated carriers (electrons). While that’s true in terms of the mechanism of generation, I overlooked that the variance/noise/fluctuations in the dark current caused by the discrete nature of the electrons, which makes it shot noise, a quantum phenomenon.

That said, I still prefer using a light source, as obtaining a meaningful noise/variance in the dark current requires around half a second of exposure time (shutter speed). While using a light source, I can reduce the exposure time to match the webcam’s maximum frame rate, allowing faster sampling rate.

Consider that results of mental influence occur over a finite period of time: from experience, the period is about 150-200ms (about the time it takes the human brain to become aware of a thought). To maximize effect size, use data that was produced during about 200ms for a trial. If you sample faster, I suggest combining data via bias amplification or other method so the data sample window is about 200ms, but first, the statistics must be very good so inherent bias is not amplified as well.

there is an algorithm, which I developed for efficient random extraction from cameras. It’s tailored to be as efficient as possible in terms of entropy usage and to reflect shifts in distributions, which are caused by MMI as direct as possible.
It’s based on AndyNoir’s apporoach, but upgraded.
I can explain algorithm, if you interested and code isn’t sufficient to understand its principle.
also, from my experience, cameras are one of best sources of random for MMI.

1 Like

The original version of your algorithm is:

For each pixel used,

1. Take two successive measurements
2. If the first measured brightness < the second measured brightness, output 1
3. If the first measured brightness > the second measured brightness, output 0
4. If the first measured brightness = the second measured brightness, do nothing

The approach is largely inspired by von Neumann’s randomness extraction algorithm:

1. Take two successive bits
2. If the first bit is 0 and the second bit is 1, output 1
3. If the first bit is 1 and the second bit is 0, output 0
4. If the first bit is equal to the second bit, do nothing

It wastes a lot of entropy. But its intended use in psi experiments influenced the design. Pixel brightness is proportional to the raw analog signal produced by the photodiode, which is proportional to the number of detected photons. Since it is hypothesized that post-processing can reduce the effect of psi, it aims to extract randomness as close to the underlying uncertainty or changes in the raw photon count as possible.

Your randomness extraction algorithm is more efficient and should produce random bits faster than the original version, but it may not align with the original design philosophy.

That said, whether the original version performs better in practice is unproven.

Anyway, for the experiment I’m trying to replicate in this thread, no randomness extraction algorithm is needed. The main focus is analyzing the unprocessed/unextracted noise properties such as autocorrelation and spatial coherence, rather than generating random bits.

1 Like

For an implementation as phone-camera based TRNG we thoroughly tested variants of some of the algorithms above but eventually settled with a different approach:

pub fn pick_and_flip(&self, data: &[u8], low: u8, high: u8, current_frame_index: usize) -> Vec<u8> {
        // Extracts the least significant bit (LSB) of each pixel brightness, flipping it based on the frame index.
        // Generates entropy by combining these bits into bytes.

        let mut entropy = Vec::new();

        for &pixel in data {
            if (low..=high).contains(&pixel) { // filter bias
                let mut lsb = pixel & 1;
                if current_frame_index % 2 == 0 { // flip bits
                    lsb ^= 1;
                }
                entropy.push(lsb);
            }
        }

        self.bits_to_bytes(&entropy)
    }

This really simple solution extracts entropy from an array of 8-bit values (camera frame pixel brightness) by analyzing the least significant bit (LSB) of each value. This process leverages the natural variability in pixel brightness across a frame. Entropy is derived by first examining whether the brightness value of a pixel falls within the range of allowed values, to avoid bias. The LSB of qualifying pixel values is then used to form a bitstream. To optionally avoid correlations the bits of every second array can be flipped.

This even works without covering the camera lens and produces the highest amount of entropy when the image is a neutral background without too much contrast.

This RNG can be tested live in the Android version of the Randonautica app under Games > Camera Entropy Meter where you can also monitor the qualitative evaluation of the randomness live. It will also be implemented as randomness source for randonauting some time this year.

Here is an open Rust library that ports Andy’s original algorithm and the one I mentioned here

1 Like

Btw I’m Andy guys. I think @WanderingIshiki was the first one who noticed :sweat_smile:

2 Likes

I see the chop_and_tack function implements the original algorithm. I think your pick_and_flip approach extracts randomness quite close to the underlying uncertainty of photon count too. Have you tested it using NIST STS or QNG Meter?

Yes, I am using my Rust port of Paul Uszak’s randomness testing suite “ent3000”. That’s also the test suite that shows the live stats in the app. Pick & Flip had the best results.

Hi Andy, I always enjoy seeing others’ approaches to produce systems responsive to mental efforts.

I have extensively measured and modeled sampling of weak entropy sources (H < or << 1.0). The general pattern is, the LSB always has the most entropy, and each increasingly significant bit has about 1/2 the entropy of the lesser significant bit. From this, the total entropy of the word is approximately 2 times the entropy in the LSB alone. If you use a method that takes entropy from all the bits in each word, you will have almost twice the entropy than from just the LSB. An example of an algorithm that will not work is taking odd versus even (or parity) of the word, which is just the same as looking only at the LSB – better algorithms are needed.

You also mention inverting every second word or bit. This is the usual XOring every other bit/word with an alternating 1, 0 sequence. For maximum responsiveness, I strongly suggest not using this deterministic whitening algorithm, but rather use only entropy-containing bit/words to produce output bits/words. In addition, this particular algorithm only removes bias in the input sequence (converting it into negative 1st-order autocorrelation) while leaving other types of defect unchanged. For example, 1st-order autocorrelation is not reduced, but may change sign depending on the phase between the correlation and the inverting signal.

Finally, I don’t recall or see that you measured the entropy contained in raw bits (LSBs in this case). Knowing the actual entropy is essential so the entropy in the output sequence is known and set to a desired minimum level (H > 0.9, or thereabouts). Entropy cannot be measured after any type of post processing, since that would mask the true entropy of the raw data, overestimating it significantly. The QNGMeter includes a good (perhaps the best available) entropy algorithm, so it can be used directly (using the interface software in the documentation).

1 Like

My approach uses neither XOR nor deterministic whitening. When I developed CamRNG in 2019, I initially tried the binning approach based on some papers I read, starting with two bins, where samples above average were assigned a 1 and those below a 0. However, this resulted in a biased binary output. I then tried applying XOR to the generated binary stream with alternating 1 and 0, similar to early PEAR REG devices. While this visually removed the bias in a classic PEAR-style random walk experiment, it still failed some NIST STS tests. I could XOR the stream with a CSPRNG but you advised against it.

So I came up with the von Neumann-like approach I explained a few posts above. The approach was proven to be immune to drift or bias without the need for XOR or any additional postprocessing. It passed all NIST STS tests and QNG Meter. It also seemed to perform well in MMI experiments, especially when @David was the operator. Idk why but his result was almost always statistically significant. Maybe he’s strong with the Force :grinning_face_with_smiling_eyes:

This was David’s result. I believe I reported this to you via email back then

Okay, I didn’t use your words, you said: “To optionally avoid correlations the bits of every second array can be flipped.”

“Flipping”, I take to mean inverting. Every second bit is inverted by XOring with a 1, 0 sequence, which makes it the same function.

Flipping every other bit is the most commonly used DETERMINISTIC whitening algorithm. Any algorithm that includes deterministic numbers (pseudorandom or fixed sequence of bits) is, by definition, deterministic. “Whitening“ is a description of the effect of inverting every other bit, meaning it has a more evenly distributed frequency spectrum.

As I noted, there are many ways of processing that don’t provide good results; and, there are some that work, but are not the best that can be achieved. My intention is always to find the BEST solution, because these measurements are so hard anyway. If you are satisfied with your current results, no need to go farther.

I believe you’re referring to NN_Solex’s method?

His method takes the LSB, then flips the bits alternatingly / XOR’s them with alternating 1’s and 0’s

My method is this one that resembles the von Neumann extractor

It appears that you replied to his post before, but mentioned my name instead

Hi @NN_Solex, I ran a quick test with QNG Meter using ~1 MiB of data to compare our approaches using the noise server I shared the other day. Based on the results, it seems your approach didn’t pass, while mine did

LSB only approach

Binary data: c270_multistream_lsb.bin (1.1 MB)

LSB with bit flipping approach

Binary data: c270_multistream_lsb_flip.bin (1.1 MB)

von Neumann-like / delta approach

Binary data: c270_multistream_delta.bin (1.1 MB)


Below is the code I used to generate the binary files for testing. Let me know if I missed anything

import requests
import threading

NOISE_SERVER_BASE_URL = "https://ensporia.com/noise"
NUM_STREAMS = 20

lock_lsb = threading.Lock()
lock_lsb_flip = threading.Lock()
lock_delta = threading.Lock()


def pack_bits_to_bytes(bits):
    packed = bytearray()
    for i in range(0, len(bits), 8):
        byte = 0
        for j, bit in enumerate(bits[i:i + 8]):
            byte |= (bit << j)
        packed.append(byte)
    return packed


def process_stream(stream_id):
    url = f"{NOISE_SERVER_BASE_URL}/stream/{stream_id}?samples_per_chunk=8"
    resp = requests.get(url, stream=True)

    flip_bit = 0
    previous_sample = None

    buf_lsb = []
    buf_lsb_flip = []
    buf_delta = []

    try:
        for chunk in resp.iter_content(chunk_size=8):
            for sample in chunk:
                # LSB approach
                lsb = sample & 1
                buf_lsb.append(lsb)

                # LSB with bit flipping approach
                lsb_flip = lsb ^ flip_bit
                flip_bit ^= 1
                buf_lsb_flip.append(lsb_flip)

                # von Neumann-like / delta approach
                if previous_sample is None:
                    previous_sample = sample
                elif previous_sample is not None:
                    if sample > previous_sample:
                        buf_delta.append(1)
                        previous_sample = None
                    elif sample < previous_sample:
                        buf_delta.append(0)
                        previous_sample = None
                    else:
                        previous_sample = sample  # based on Peres's improvement to the von Neumann extractor. ~3x bitrate increase

            if len(buf_lsb) >= 8:
                with lock_lsb:
                    byte = pack_bits_to_bytes(buf_lsb[:8])
                    f_lsb.write(byte)
                    f_lsb.flush()
                del buf_lsb[:8]

            if len(buf_lsb_flip) >= 8:
                with lock_lsb_flip:
                    byte = pack_bits_to_bytes(buf_lsb_flip[:8])
                    f_lsb_flip.write(byte)
                    f_lsb_flip.flush()
                del buf_lsb_flip[:8]

            if len(buf_delta) >= 8:
                with lock_delta:
                    byte = pack_bits_to_bytes(buf_delta[:8])
                    f_delta.write(byte)
                    f_delta.flush()
                del buf_delta[:8]

    except Exception as e:
        print(f"Stream {stream_id} error: {e}")


if __name__ == "__main__":
    print("Requesting stream IDs...")

    stream_ids = []
    for _ in range(NUM_STREAMS):
        resp = requests.post(f"{NOISE_SERVER_BASE_URL}/request-stream-id")
        stream_id = resp.json()["id"]
        stream_ids.append(stream_id)
        print(f"Stream ID acquired: {stream_id}")

    print("\nStarting threads to stream and write to binary files...\nPress Ctrl+C to stop.\n")

    with open("c270_multistream_lsb.bin", "wb") as f_lsb, \
            open("c270_multistream_lsb_flip.bin", "wb") as f_lsb_flip, \
            open("c270_multistream_delta.bin", "wb") as f_delta:

        threads = []
        for stream_id in stream_ids:
            t = threading.Thread(target=process_stream, args=(stream_id,))
            t.start()
            threads.append(t)

        try:
            for t in threads:
                t.join()
        except KeyboardInterrupt:
            print("\nStopped by user.")

Thanks for runnimg the tests. Sorry I forgot to mention that the Pick & Flip algorithm’s targeted use case was not a controlled environment with the aim of classic MMI experiments but universality in an uncontrollable environment: a regular randonaut’s uncovered phone camera.

In the final implementation in the app flipping is not used after entropy extraction but a von Neumann algorithm. And the result is only used if it passes a series of randomness tests.

Yes, I may have confused your and NN_Solex’s posts. The tests you ran demonstrated one of my points: bit flipping removes most of the bias, but does nothing about autocorrelation.

I’m not a programmer, so I’m not entirely sure what your code is doing. However, it looks like you have two processing methods: one uses LSBs, with or without flipping, and the other, byte (composed of 8 LSBs) processing (which is not totally clear to me). 1s or 0s are produced if the current sample is larger or smaller respectively than the previous bit/word.