├── .gitignore ├── LICENSE ├── README.md ├── example.jpg ├── example.tiff ├── example2.jpg ├── example_checkerboard.jpg ├── example_lines_vertical.jpg ├── generate_pattern.ipynb ├── pyproject.toml └── speckle_pattern ├── __init__.py └── speckle.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | 106 | 107 | .idea/ 108 | .ipynb_checkpoints/ 109 | __pycache__/ 110 | .vscode/ 111 | 112 | test/ 113 | *.jpg 114 | *.tiff 115 | !example.jpg 116 | !example2.jpg 117 | !example_lines_vertical.jpg 118 | !example.tiff 119 | !example_checkerboard.jpg -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Ladisk 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Generate print-ready images of a random speckle pattern for DIC applications. 2 | 3 | #### Installation 4 | 5 | This package is hosted on PyPI. Install it using `pip`: 6 | 7 | ```pip install speckle_pattern``` 8 | 9 | 10 | #### Example speckle pattern 11 | 12 | ```python 13 | from speckle_pattern import generate_and_save 14 | 15 | image_height = 50 # mm 16 | image_width = 100 # mm 17 | speckle_diameter = 3 # mm 18 | dpi = 200 19 | save_path = 'example.jpg' 20 | 21 | generate_and_save(image_height, image_width, dpi, speckle_diameter, save_path) 22 | ``` 23 | 24 |

random speckle

25 | 26 | 27 | #### More speckle generation options 28 | 29 | ```python 30 | from speckle_pattern import generate_and_save 31 | 32 | image_height = 50 # mm 33 | image_width = 100 # mm 34 | speckle_diameter = 7.5 # mm 35 | dpi = 150 36 | save_path = 'example2.tiff' 37 | 38 | size_randomness = 0.9 # set higher for more speckle size variety 39 | position_randomness = 2.5 # set higher for more speckle position variety 40 | speckle_blur = 0. # sigma of smothing Gaussian kernel 41 | grid_step = 2. # approximate grid step, in terms of speckle diameter `D` 42 | 43 | generate_and_save( 44 | image_height, 45 | image_width, 46 | dpi, 47 | speckle_diameter, 48 | save_path, 49 | size_randomness=size_randomness, 50 | position_randomness=position_randomness, 51 | speckle_blur=speckle_blur, 52 | grid_step=grid_step, 53 | ) 54 | ``` 55 | 56 |

random speckle 2

57 | 58 | #### Example line pattern 59 | 60 | ```python 61 | from speckle_pattern import generate_lines 62 | 63 | image_height = 50 # mm 64 | image_width = 100 # mm 65 | line_width = 5 # mm 66 | orientation = 'vertical' 67 | dpi = 200 68 | save_path = f'example_lines_{orientation}.jpg' 69 | 70 | generate_lines(image_height, image_width, dpi, line_width, save_path) 71 | ``` 72 | 73 |

vertical lines

74 | 75 | #### Example checkerboard pattern 76 | 77 | ```python 78 | from speckle_pattern import generate_checkerboard 79 | 80 | image_height = 50 # mm 81 | image_width = 100 # mm 82 | line_width = 4 # mm 83 | dpi = 200 84 | save_path = f'example_checkerboard.jpg' 85 | 86 | generate_checkerboard(image_height, image_width, dpi, line_width=line_width, path=save_path) 87 | ``` 88 | 89 |

checkerboard

90 | 91 | ### Authors 92 | 93 | - [Domen Gorjup](http://ladisk.si/?what=incfl&flnm=gorjup.php) 94 | - [Janko Slavič](http://ladisk.si/?what=incfl&flnm=slavic.php) 95 | - [Miha Boltežar](http://ladisk.si/?what=incfl&flnm=boltezar.php) -------------------------------------------------------------------------------- /example.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ladisk/speckle_pattern/055f45b66c7985564a9fa400d8d2f41ddd181d31/example.jpg -------------------------------------------------------------------------------- /example.tiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ladisk/speckle_pattern/055f45b66c7985564a9fa400d8d2f41ddd181d31/example.tiff -------------------------------------------------------------------------------- /example2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ladisk/speckle_pattern/055f45b66c7985564a9fa400d8d2f41ddd181d31/example2.jpg -------------------------------------------------------------------------------- /example_checkerboard.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ladisk/speckle_pattern/055f45b66c7985564a9fa400d8d2f41ddd181d31/example_checkerboard.jpg -------------------------------------------------------------------------------- /example_lines_vertical.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ladisk/speckle_pattern/055f45b66c7985564a9fa400d8d2f41ddd181d31/example_lines_vertical.jpg -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["hatchling"] 3 | build-backend = "hatchling.build" 4 | 5 | [project] 6 | name = "speckle_pattern" 7 | version = "1.3.2" 8 | 9 | authors = [ 10 | {name="Domen Gorjup, Janko Slavič, Miha Boltežar", email="janko.slavic@fs.uni-lj.si"}, 11 | ] 12 | 13 | description = "Generate print-ready pattern images for DIC applications." 14 | readme = "README.md" 15 | requires-python = ">=3.10" 16 | keywords = ["computer vision", "dic", "speckle pattern"] 17 | license = "MIT" 18 | 19 | packages = [ 20 | { include = "speckle_pattern.speckle.py" }, 21 | ] 22 | 23 | classifiers = [ 24 | 'Development Status :: 5 - Production/Stable', 25 | 'Intended Audience :: Developers', 26 | 'Topic :: Scientific/Engineering', 27 | 'Programming Language :: Python :: 3.10', 28 | "License :: OSI Approved :: MIT License", 29 | ] 30 | 31 | dependencies = [ 32 | 'matplotlib>=2.0.0', 33 | 'numpy>=1.0.0', 34 | 'scipy>=1.0.0', 35 | 'tqdm>=4.10.0', 36 | 'imageio>=2.2.0', 37 | 'piexif>=1.0.13' 38 | ] 39 | 40 | [project.urls] 41 | "Homepage" = "https://github.com/ladisk/speckle_pattern" 42 | "Bug Tracker" = "https://github.com/ladisk/speckle_pattern/issues" -------------------------------------------------------------------------------- /speckle_pattern/__init__.py: -------------------------------------------------------------------------------- 1 | from .speckle import generate_and_save, generate_lines, generate_checkerboard -------------------------------------------------------------------------------- /speckle_pattern/speckle.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | __author__ = 'Domen Gorjup' 3 | 4 | """ 5 | Generate print-ready speckle or line patterns to use in DIC applications. 6 | """ 7 | 8 | from itertools import product 9 | from random import choice 10 | import numpy as np 11 | from numpy.random import multivariate_normal 12 | from scipy.ndimage.filters import gaussian_filter 13 | import matplotlib.pyplot as plt 14 | from imageio import get_writer 15 | from tqdm import tqdm 16 | import piexif 17 | 18 | 19 | def speckle(my, mx, D=3, shape=None, s=0, blur=0.6, value=1.): 20 | """ 21 | Generates a random speckle in an image of given shape. 22 | """ 23 | 24 | if not s: 25 | if D < 3: 26 | raise Exception('Set higher speckle diameter (D >= 3 px)!') 27 | polinom = np.array([ -4.44622133e-06, 1.17748897e-02, 2.58275794e-01, -0.65]) 28 | s = np.polyval( polinom, D) 29 | N = int(s * 300) 30 | 31 | if my == 0 and mx == 0 and shape is None: 32 | my = 2*D 33 | mx = 2*D 34 | 35 | mean = np.array([my, mx]) 36 | cov = np.array([[s, 0], [0, s]]) 37 | y, x = multivariate_normal(mean, cov, N).T 38 | x = np.round(x).astype(int) 39 | y = np.round(y).astype(int) 40 | yx = np.column_stack((y, x)) 41 | 42 | # Sample size: 43 | dx = np.max(yx[:, 1]) - np.min(yx[:, 1]) 44 | dy = np.max(yx[:, 0]) - np.min(yx[:, 0]) 45 | d = np.mean(np.array([dx, dy])) 46 | 47 | neustrezni_i = [] 48 | 49 | if shape is None: 50 | slika = np.zeros((2*int(my), 2*int(mx))) 51 | else: 52 | slika = np.zeros(shape) 53 | 54 | for (y_, x_) in yx: 55 | try: 56 | slika[y_, x_] = value 57 | except: 58 | pass 59 | 60 | return gaussian_filter(slika, blur), d 61 | 62 | 63 | def speckle_image(shape, D, size_randomness=1, position_randomness=1, speckle_blur=0.6, grid_step=2.2, n_unique=100): 64 | ''' 65 | Generates an image of shape (w, d), populated with speckles of 66 | random shape and semi-random position and size. Generates and 67 | picks randomly from `n_unique` unique speckles. 68 | ''' 69 | h, w = shape 70 | D = int(D) 71 | border = int(10*D) # to avoid overlap and clipping near borders 72 | 73 | h += 2*border 74 | w += 2*border 75 | 76 | im = np.ones((int(h), int(w))) 77 | grid_size = D * grid_step 78 | 79 | xs = np.arange(border, w-border//2, grid_size).astype(int) 80 | ys = np.arange(border, h-border//2, grid_size).astype(int) 81 | 82 | speckle_coordinates = list(product(ys, xs)) 83 | N = np.min([len(speckle_coordinates), n_unique]) 84 | 85 | # Generate unique speckles 86 | speckles = [] 87 | for i in range(N): 88 | Dr = np.clip(D + int(np.random.randn(1) * D * size_randomness*0.2), 2, 2*D) 89 | speckles.append(speckle(0, 0, Dr, blur=speckle_blur)[0]) 90 | print('Random speckle generation complete.') 91 | 92 | for y, x in tqdm(speckle_coordinates): 93 | s = choice(speckles) 94 | s_shape = np.array(s.shape) 95 | dy, dx = (s_shape // 2).astype(int) 96 | 97 | x += int(np.random.randn(1)*(D*position_randomness*0.2)) 98 | y += int(np.random.randn(1)*(D*position_randomness*0.2)) 99 | 100 | sl = np.s_[y-dy:y+dy, x-dx:x+dx] 101 | 102 | im[sl] -= s 103 | 104 | im = np.clip(im, 0, 1) 105 | 106 | im = im[border:-border, border:-border] 107 | 108 | return im 109 | 110 | 111 | def add_dpi_meta(path, dpi=300, comment=''): 112 | exif_dict = piexif.load(path) 113 | exif_dict["0th"][piexif.ImageIFD.XPComment] = comment.encode('utf16') 114 | exif_dict["0th"][piexif.ImageIFD.XResolution] = (dpi, 1) 115 | exif_dict["0th"][piexif.ImageIFD.YResolution] = (dpi, 1) 116 | exif_dict["0th"][piexif.ImageIFD.ResolutionUnit] = 2 # 1: no unit, 2: inch, 3: cm 117 | exif_bytes = piexif.dump(exif_dict) 118 | piexif.insert(exif_bytes, path) 119 | 120 | 121 | def save_image(path, image, dpi, comment=''): 122 | """ 123 | Saves a generated pattern image along with metadata 124 | configured for printing. 125 | """ 126 | 127 | if path.split('.')[-1].lower() not in ['jpg', 'jpeg', 'tif', 'tiff']: 128 | path += '.tiff' 129 | 130 | fmt = path.split('.')[-1].lower() 131 | kwargs = {} 132 | 133 | if fmt in ['jpg', 'jpeg']: 134 | jpeg_kwargs = { 135 | 'quality': 100, 136 | } 137 | kwargs.update(jpeg_kwargs) 138 | 139 | elif fmt in ['tiff', 'tif']: 140 | tiff_kwargs = { 141 | 'resolution': (dpi, dpi), 142 | 'description': comment, 143 | } 144 | kwargs.update(tiff_kwargs) 145 | 146 | with get_writer(path, mode='i') as writer: 147 | writer.append_data(np.uint8(image/np.max(image)*255), meta=kwargs) 148 | 149 | if fmt in ['jpg', 'jpeg']: 150 | add_dpi_meta(path, dpi, comment=comment) 151 | 152 | 153 | def generate_and_save(height, width, dpi, speckle_diameter, path, size_randomness=0.5, 154 | position_randomness=0.5, speckle_blur=1, grid_step=1.2): 155 | """ 156 | Generates a speckle image of given shape, speckle diameter etc. and saves 157 | it to specified path as JPEG or TIFF, configured for printing. 158 | 159 | Parameters 160 | ---------- 161 | height: float 162 | the height of output image in mm 163 | width: float 164 | the width of output image in mm 165 | dpi: float 166 | DPI setting for printing 167 | speckle_diameter: float 168 | average speckle diameter in mm 169 | path: str, None 170 | output file save path. If None, the file is named according 171 | to speckle settings. Defaults to None. 172 | size_randomness: float 173 | a measure of speckle diameter randomness. 174 | Should be in [0, 1] range. 175 | position_randomness: float 176 | a measure of speckle position deviation from regular grid. 177 | Should be in [0, 1] range. 178 | speckle_blur: float 179 | sigma parameter of bluring Gaussian filter 180 | grid_step: float 181 | spacing of regular grid for speckle positioning, in terms 182 | of `speckle_diameter`. 183 | 184 | Returns 185 | ------- 186 | image: (h, w), ndarray 187 | resulting speckle image (grayscale, [0, 1]) 188 | """ 189 | ppmm = dpi / 25.4 190 | w = int(np.round((width * ppmm))) 191 | h = int(np.round((height * ppmm))) 192 | D = np.ceil(speckle_diameter*ppmm) 193 | 194 | im = speckle_image((h, w), D, size_randomness, position_randomness, speckle_blur, grid_step) 195 | 196 | if path is None: 197 | path = f'speckle_{width}x{height}mm_D{speckle_diameter}mm_{dpi}DPI.tiff' 198 | 199 | # Add exif comment to image: 200 | image_comment = f'height: {height} mm\nwidth: {width} mm\ndpi: {dpi}\nD: {speckle_diameter} mm\n'\ 201 | f'size_randomness: {size_randomness}\nposition_randomness: {position_randomness}\n'\ 202 | f'speckle_blur: {speckle_blur}\ngrid_step: {grid_step}' 203 | 204 | save_image(path, im, dpi, comment=image_comment) 205 | print(f'Image saved to {path}.') 206 | return im 207 | 208 | 209 | def generate_lines(height, width, dpi, line_width, path, orientation='vertical', N_lines=None): 210 | """ 211 | Generates a pattern of lines and saves it to specified 212 | path as JPEG or TIFF, configured for printing. 213 | 214 | Parameters 215 | ---------- 216 | height: float 217 | the height of output image in mm 218 | width: float 219 | the width of output image in mm 220 | dpi: float 221 | DPI setting for printing 222 | line_width: float 223 | line width in mm 224 | path: str, None 225 | output file name. 226 | orientation: str 227 | line orientation: 'vertical' (default) or 'horizontal'. 228 | N_lines: float 229 | number of lines. If None, `line_width` is used. 230 | Defaults to None. 231 | 232 | Returns 233 | ------- 234 | image: (h, w), ndarray 235 | resulting image (grayscale, [0, 1]) 236 | """ 237 | 238 | ppmm = dpi / 25.4 239 | w = int(np.round((width * ppmm))) 240 | h = int(np.round((height * ppmm))) 241 | 242 | if N_lines is not None: 243 | if orientation == 'vertical': 244 | line_width = width // (2*N_lines) 245 | else: 246 | line_width = height // (2*N_lines) 247 | 248 | D = int(np.round(line_width * ppmm)) 249 | 250 | im = np.full((h, w), 255, dtype=np.uint8) 251 | if orientation == 'vertical': 252 | black_id = np.hstack( [np.arange(i*D, i*D+D) for i in range(0, w//D, 2)] ) 253 | if black_id[-1] + D < w: 254 | black_id = np.hstack([black_id, np.arange(w//D*D, w)]) 255 | im[:, black_id] = 0 256 | else: 257 | black_id = np.hstack( [np.arange(i*D, i*D+D) for i in range(0, h//D, 2)] ) 258 | if black_id[-1] + D < h: 259 | black_id = np.hstack([black_id, np.arange(h//D*D, h)]) 260 | im[black_id] = 0 261 | 262 | image_comment = f'{orientation} lines\nline width: {line_width}\n DPI: {dpi}' 263 | save_image(path, im, dpi, comment=image_comment) 264 | print(f'Image saved to {path}.') 265 | return im 266 | 267 | 268 | def generate_checkerboard(height, width, dpi, path, line_width=1, N_rows=None): 269 | """ 270 | Generates a checkerboard pattern and saves it to specified 271 | path as JPEG or TIFF, configured for printing. 272 | 273 | Parameters 274 | ---------- 275 | height: float 276 | the height of output image in mm 277 | width: float 278 | the width of output image in mm 279 | dpi: float 280 | DPI setting for printing 281 | path: str, None 282 | output file name. 283 | line_width: float 284 | line width in mm. Defaults to 1. 285 | N_rows: float 286 | number of lines. If None, `line_width` is used. 287 | Defaults to None. 288 | 289 | Returns 290 | ------- 291 | image: (h, w), ndarray 292 | resulting image (grayscale, [0, 1]) 293 | """ 294 | 295 | ppmm = dpi / 25.4 296 | w = int(np.round((width * ppmm))) 297 | h = int(np.round((height * ppmm))) 298 | 299 | if N_rows is not None: 300 | line_width = height // (2*N_rows) 301 | 302 | D = int(np.round(line_width * ppmm)) 303 | 304 | im = np.ones((h, w), dtype=np.uint8) 305 | 306 | black_id = np.hstack( [np.clip(np.arange(i*D, i*D+D), 0, h-1) for i in range(0, h//D+1, 2)] ) 307 | im[black_id] = 0 308 | 309 | # invert values in every other column 310 | invert_id = np.hstack( [np.clip(np.arange(i*D, i*D+D), 0, w-1) for i in range(0, w//D+1, 2)] ) 311 | im[:, invert_id] = 1 - im[:, invert_id] 312 | 313 | im = im * 255 314 | 315 | image_comment = f'checkerboard\nline width: {line_width}\n DPI: {dpi}' 316 | save_image(path, im, dpi, comment=image_comment) 317 | print(f'Image saved to {path}.') 318 | return im 319 | 320 | 321 | if __name__ == '__main__': 322 | slika = generate_and_save(70, 70, 200, 3., 'test.tiff', size_randomness=0.75, speckle_blur=1., grid_step=1.2) 323 | plt.figure(figsize=(7, 7)) 324 | plt.imshow(slika, cmap='gray', interpolation='nearest', vmin=0, vmax=1) 325 | plt.show() --------------------------------------------------------------------------------