Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
dae0d3d
Merge pull request #169 from Computer-Aided-Validation-Laboratory/dev
JoelPhys Aug 1, 2025
e458216
Merge pull request #170 from Computer-Aided-Validation-Laboratory/dev
ScepticalRabbit Aug 8, 2025
8b04212
Added specklegen to src/pyvale.
WieraB Oct 21, 2025
85c314d
Added Perlin and fractal noise with examples.
WieraB Oct 22, 2025
8c9bd52
Added Simplex noise with examples.
WieraB Oct 23, 2025
8cef9fe
Modified Simplex noise and corresponding examples, updated pyproject.…
WieraB Oct 23, 2025
0a2a9ea
Removed print from Simplex function, added log directory creation if …
WieraB Oct 23, 2025
4e35282
Re-arranged specklegen examples for doxyfile, amended docyfiles to in…
WieraB Oct 27, 2025
179f6ec
Added README.md file to example forlder for doxygen.
WieraB Oct 27, 2025
4b73939
- Refactored functions: 1. Grouped all specklegen options into one fu…
WieraB Oct 31, 2025
a046e77
Amended examples in line with the new function structure, added more …
WieraB Nov 2, 2025
0537304
Amended tests in line with the new function structure.
WieraB Nov 2, 2025
05fdb11
Added regression tests for specklegen module.
WieraB Nov 3, 2025
8ee0b46
Added regression tests for specklegen module, amended some headers.
WieraB Nov 3, 2025
54fd2f5
Updated the docs, amended some comments in the examples.
WieraB Nov 3, 2025
b30e6ca
Amended specklegenconst typo.
WieraB Jan 24, 2026
60352a2
Merge branch 'dev' into wb-dev
WieraB Jan 24, 2026
bc74eb7
Fixed missing commas in toml
ScepticalRabbit Jan 24, 2026
f310c1b
Fixed doc files to include specklegen examples. Git ignored examples_…
WieraB Jan 25, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/apidoc.sh
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ echo "Updating generated RST files..."
# sed -i '0,/dataset.dataset/s/dataset.dataset/pyvale.dataset/' source/pyvale.dataset.dataset.rst || error_exit "Failed to update dataset title"

# Modules to process
modules=("dic" "blender" "mooseherder" "sensorsim" "verif" "dataset")
modules=("dic" "blender" "mooseherder" "sensorsim" "verif" "dataset" "specklegen")

for mod in "${modules[@]}"; do
rst="source/pyvale.${mod}.rst"
Expand Down
2 changes: 2 additions & 0 deletions docs/source/api_py.rst
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,6 @@ Detailed Python API
pyvale.mooseherder
pyvale.verif
pyvale.dataset
pyvale.specklegen


2 changes: 2 additions & 0 deletions docs/source/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@
'../../src/pyvale/examples/dic',
'../../src/pyvale/examples/blenderimagedef',
'../../src/pyvale/examples/mooseherder',
'../../src/pyvale/examples/specklegen',
],
# Path to where to save gallery generated output
'gallery_dirs': [
Expand All @@ -125,6 +126,7 @@
'examples/dic',
'examples/blenderimagedef',
'examples/mooseherder',
'examples/specklegen',
],
# Pattern to identify example files
'filename_pattern': '/plot_',
Expand Down
7 changes: 7 additions & 0 deletions docs/source/examples/examples.rst
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,10 @@ Mooseherder
:maxdepth: 2

examples_mooseherder

Specklegen
-------------------------------------
.. toctree::
:maxdepth: 2

examples_specklegen
16 changes: 16 additions & 0 deletions docs/source/examples/examples_specklegen.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
.. _examples_specklegen:

Specklegen
=================

.. toctree::
:maxdepth: 1

specklegen/ex1a_random_disks_overlap.rst
specklegen/ex1b_random_disks_reduce_overlap.rst
specklegen/ex1c_random_disks_grid.rst
specklegen/ex2a_perlin_noise.rst
specklegen/ex3a_fractal_noise.rst
specklegen/ex4a_simplex_noise.rst


6 changes: 5 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,13 @@ dependencies = [
"pybind11>=2.13.6",
"pyqtgraph>=0.13.7",
"opencv-python<=4.9.0.80",
"seaborn>=0.13.2",
"scikit-image>=0.0",
"perlin_numpy>=0.0.0",
"opensimplex>=0.4.5.1",
"pandas>=2.3.1",
"scikit-build-core>=0.11.6",
"ninja>=1.13.0"
"ninja>=1.13.0",
]

[tool.pytest.ini_options]
Expand Down
42 changes: 42 additions & 0 deletions scripts/gengold_specklegen.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
#===============================================================================
# pyvale: the python validation engine
# License: MIT
# Copyright (C) 2025 The Computer Aided Validation Team
#===============================================================================

import pyvale.verif.specklegold as specklegold
import pyvale.verif.specklegenconst as specklegenconst

def main() -> None:

tags = ["random_disks", "random_disks_grid", "perlin", "fractal", "simplex"]

for tag in tags:

param_dict = {
"speckle_size": 20,
"screen_size_width": 1000,
"screen_size_height": 800,
"bit_depth": 8,
"theme": 'white_on_black',
"seed": 123,
"type_gen": tag,
"octaves": 3,
"lacunarity": 2,
"sigma": 4.0,
"reduce_overlap": True,
"attempts_tot": 300,
"perturbation_max": 12,
"case_tot": 3
}

print(80*"=")
print(f"Gold Output Generator for pyvale {tag} speckle pattern generation")
print(80*"=")
print(f"Saving gold output to: {specklegenconst.GOLD_PATH}\n")

print(f"Generating gold output for {tag} field point sensors...")
specklegold.gen_gold_measurements(param_dict)

if __name__ == "__main__":
main()
1 change: 1 addition & 0 deletions src/pyvale/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,4 @@
from . import mooseherder
from . import dataset
from . import calib
from . import specklegen
2 changes: 2 additions & 0 deletions src/pyvale/examples/specklegen/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Specklegen Examples
==================
142 changes: 142 additions & 0 deletions src/pyvale/examples/specklegen/ex1a_random_disks_overlap.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
# ==============================================================================
# pyvale: the python validation engine
# License: MIT
# Copyright (C) 2025 The Computer Aided Validation Team
# ==============================================================================

"""
Specklegen: Speckle pattern generation using random disk placement without checking for overlap
================================================================================
Script to generate a synthetic speckle pattern made from randomly placed circular
speckles (disks), run diagnostics on the generated image, and save both the image
and diagnostics to the selected folder.
"""

import numpy as np
import time
import json
import os
import pyvale.specklegen as specklegen

#%%
# Here we parse command line arguments to set the speckle pattern parameters.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we are actually parsing command line args here are we? Users will probably just want to run this in a plain python script or jupyter notebook so no need for a CLI

# For ease of use in this example script we set parameter values directly in the
# code rather than via bash script.
# The parameter responsible for reducing overlap is set to 'False' in this example.

speckle_size = 20
screen_size_width = 1000
screen_size_height = 800
bit_depth = 8
theme = 'white_on_black'
seed = 10
sigma = 4.0
reduce_overlap = False
type_gen = "random_disks"
output_path = "src/pyvale/examples/specklegen/output/ex1a"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As default we should probably be using pathlib and Path objects to manage all directories. If you look at some of the sensor sim examples we create a standard directory called 'pyvale-output' in the current working directory:

output_path = Path.cwd() / "pyvale-output"
if not output_path.is_dir():
    output_path.mkdir(parents=True, exist_ok=True)


print('Start')

assert theme in ['black_on_white', 'white_on_black'], "Theme should be either 'black_on_white' or 'white_on_black'."
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you want to restrict the options a user can select I would probably use and enum:

import enum

class Theme(enum.Enum):
    BLACK_ON_WHITE = enum.auto()


if reduce_overlap:
print("Reducing overlap between speckles")
else:
print("Not reducing overlap between speckles")

subfolder = f"/{type_gen}_{speckle_size}_{screen_size_width}_{screen_size_height}_{bit_depth}_{theme}_{seed}_{sigma}_{reduce_overlap}"
print(subfolder)
save_path = output_path + subfolder
if not os.path.exists(save_path):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See comment above about pathlib

os.makedirs(save_path)

#%%
# We calculate parameteres aiming for approximately 50/50 black-to-white ratio.
# We now generate the speckle pattern using the specified parameters.
# The background and foreground colours are set based on the chosen theme and bit depth.

speckle_area = np.pi * (speckle_size / 2) ** 2
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These 3 calculations should probably be automated for the user and hidden within the generate_speckles function - the user should only need to specify the black-white ratio and all these calculations are done for them in the function

total_area = screen_size_width * screen_size_height
total_speckles = int((0.5 * total_area) / speckle_area)
print(f"Total number of speckles = {total_speckles}")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I try to put terminal output together in blocks and then take a screenshot which can be embedded in the docs - have a look at some of the sensor sim examples to see how to do this. Having to take a screenshot will mean you have to consolidate terminal output as much as possible which is probably good practice in examples

dynamic_range: int = 2**bit_depth - 1
background_colour = 0 if theme == 'white_on_black' else dynamic_range
foreground_colour = dynamic_range if theme == 'white_on_black' else 0

feature_size_width = speckle_size
feature_size_height = speckle_size

time_start = time.time()
image, results = specklegen.generate_speckles(screen_size_width, screen_size_height,
feature_size_width, feature_size_height,
foreground_colour, background_colour,
bit_depth, type_gen, seed,
total_speckles=total_speckles,
reduce_overlap=reduce_overlap,
sigma=sigma)
time_end = time.time()
time_taken = time_end - time_start
print(f"Time taken for speckle generation: {np.round(time_taken, 3)} seconds")

np.savetxt(f"{save_path}/speckle_placement_results.csv", results, delimiter=",",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tend to stick to an 80 column width in python

header="speckle_number, attempts, overlap(1/0/2), cent_x, cent_y", comments='', fmt=['%d', '%d', '%d', '%.3f', '%.3f'])

#%%
# Now we run diagnostics on the generated speckle pattern and save the results.
# Finally, we print out the key statistics to the console.
# The plots are saved in the provided output folder. However, the diagnostic function outputs the matplotlib figures and axes,
# so the plot formatting could be changed from the default one used by the function.
# We aim to achieve black-to-white ratio as close to unity as possible. Unity ratio means 50/50 distribution of black and white colours.
# However, in this example, black-to-white ratio considerably deviates from unity.
# It is also visible in the irradiance value histogram.
# The proportion of 0 irradiance values, corresponding to black colour, overweighs the 255 values, corresponding to white colour.
# This is a result of speckle overlap.

print("")
print('Starting speckle pattern diagnostics...')
results = specklegen.speckle_pattern_statistics(image, dynamic_range)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably easier for the user to just specify the number of bits and the dynamic range is calculated in the function.

plots = specklegen.speckle_pattern_plots(image, dynamic_range, save_path)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we also include the images in the docs


with open(f"{save_path}/speckle_pattern_diagnostics.json", 'w') as f:
json.dump(results, f, indent=4)

ratio = results.get("black_white_ratio", None)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably don't need to unpack the dictionary like this - just print the key value pairs in a loop

mean_gradient = results.get("mean_intensity_gradient", None)
std_dev = results.get("std_dev_irradiance", None)
avg = results.get("avg_irradiance", None)
contrast = results.get("contrast", None)
entropy = results.get("shannon_entropy", None)
peak_to_mean = results.get("peak_to_mean_ratio", None)
skew = results.get("skewness", None)
kurt = results.get("kurtosis", None)
avg_speckle_size_fwhm = results.get("avg_speckle_size_fwhm", None)
avg_speckle_size_e2 = results.get("avg_speckle_size_e2", None)
H_fit_stats = results.get("H_fit_stats", None)
V_fit_stats = results.get("V_fit_stats", None)

print("")
print("Speckle statistics:")

print(f"Black/White ratio: {np.round(ratio, 3)}")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would just use a loop here to print the dictionary key,value pairs. The dictionary keys should be self explanatory so we should be able to do:

for key,value in results.items():
    print(f"{key}: {value}")

print(f"Mean intensity gradient: {np.round(mean_gradient, 3)}")
print(f"Standard deviation of irradiance values: {np.round(std_dev, 3)}")
print(f"Average irradiance value: {np.round(avg, 3)}")
print(f"Contrast (std/mean): {np.round(contrast, 3)}")
print(f"Skewness: {np.round(skew, 3)}")
print(f"Kurtosis: {np.round(kurt, 3)}")
print(f"Shannon entropy: {np.round(entropy, 3)}")
print(f"Peak to mean ratio: {np.round(peak_to_mean, 3)}")
print(f"Average speckle size (full width at half maximum): {np.round(avg_speckle_size_fwhm, 3)} pixels")
print(f"Average speckle size (1/e^2): {np.round(avg_speckle_size_e2, 3)} pixels")
print(f"R_squared: Horisontal fit: {np.round(H_fit_stats['R_squared'], 3)}, Vertical fit: {np.round(V_fit_stats['R_squared'], 3)}")

#%%
# Finally, the relative errors beetween the specified speckle size and the speckle size approximated using cautocovariance are calculated.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

autocovariance

error = np.abs(avg_speckle_size_fwhm - speckle_size) * 100 / speckle_size
print(f"Percentage error between requested speckle size and measured speckle size from FWHM: {np.round(error, 3)} %")
error = np.abs(avg_speckle_size_e2 - speckle_size) * 100 / speckle_size
print(f"Percentage error between requested speckle size and measured speckle size from 1/e^2: {np.round(error, 3)} %")
np.save(f"{save_path}/image.npy", image)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The user will probably also want to save the speckle image to tiff or bmp - do we have functions for this?

print("")
print('End :)')
print("")
Loading
Loading