-
Notifications
You must be signed in to change notification settings - Fork 3
Speckle pattern generation module #181
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: dev
Are you sure you want to change the base?
Changes from all commits
dae0d3d
e458216
8b04212
85c314d
8c9bd52
8cef9fe
0a2a9ea
4e35282
179f6ec
4b73939
a046e77
0537304
05fdb11
8ee0b46
54fd2f5
b30e6ca
60352a2
bc74eb7
f310c1b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -13,4 +13,6 @@ Detailed Python API | |
| pyvale.mooseherder | ||
| pyvale.verif | ||
| pyvale.dataset | ||
| pyvale.specklegen | ||
|
|
||
|
|
||
| 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 | ||
|
|
||
|
|
| 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() |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -21,3 +21,4 @@ | |
| from . import mooseherder | ||
| from . import dataset | ||
| from . import calib | ||
| from . import specklegen | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| Specklegen Examples | ||
| ================== |
| 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. | ||
| # 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" | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As default we should probably be using |
||
|
|
||
| print('Start') | ||
|
|
||
| assert theme in ['black_on_white', 'white_on_black'], "Theme should be either 'black_on_white' or 'white_on_black'." | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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: |
||
|
|
||
| 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): | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
| 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}") | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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=",", | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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)}") | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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: |
||
| 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. | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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("") | ||
There was a problem hiding this comment.
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