├── .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 |

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 | 
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 | 
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 | 
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()
--------------------------------------------------------------------------------