Question: I am facing a small (big) problem: I want to generate a high resolution speckle pattern and save it as a file that I can import into a laser engraver. Can be PNG, JPEG, PDF, SVG, or TIFF. My script does a decent job of generating the pattern that I want: The user needs to first define the inputs, these are: ############ # INPUTS # ############ dpi = 1000 # dots per inch dpmm = 0.03937 * dpi # dots per mm widthOfSampleMM = 50 # mm heightOfSampleMM = 50 # mm patternSizeMM = 0.1 # mm density = 0.75 # 1 is very dense, 0 is not fine at all variation = 0.75 # 1 is very bad, 0 is very good ############  After this, I generate the empty matrix and fill it with black shapes, in this case a circle. # conversions to pixels widthOfSamplesPX = int(np.ceil(widthOfSampleMM*dpmm)) # get the width widthOfSamplesPX = widthOfSamplesPX + 10 - widthOfSamplesPX % 10 # round up the width to nearest 10 heightOfSamplePX = int(np.ceil(heightOfSampleMM*dpmm)) # get the height heightOfSamplePX = heightOfSamplePX + 10 - heightOfSamplePX % 10 # round up the height to nearest 10 patternSizePX = patternSizeMM*dpmm # this is the size of the pattern, so far I am going with circles # init an empty image im = 255*np.ones((heightOfSamplePX, widthOfSamplesPX), dtype = np.uint8) # horizontal circle centres numPoints = int(density*heightOfSamplePX/patternSizePX) # get number of patterns possible if numPoints==1: horizontal = [heightOfSamplePX // 2] else: horizontal = [int(i * heightOfSamplePX / (numPoints + 1)) for i in range(1, numPoints + 1)] # vertical circle centres numPoints = int(density*widthOfSamplesPX/patternSizePX) if numPoints==1: vertical = [widthOfSamplesPX // 2] else: vertical = [int(i * widthOfSamplesPX / (numPoints + 1)) for i in range(1, numPoints + 1)] for i in vertical: for j in horizontal: # generate the noisy information iWithNoise = i+variation*np.random.randint(-2*patternSizePX/density, +2*patternSizePX/density) jWithNoise = j+variation*np.random.randint(-2*patternSizePX/density, +2*patternSizePX/density) patternSizePXWithNoise = patternSizePX+patternSizePX*variation*(np.random.rand()-0.5)/2 cv2.circle(im, (int(iWithNoise),int(jWithNoise)), int(patternSizePXWithNoise//2), 0, -1) # add circle  After this step, I can get im, here's a low quality example at dpi=1000:  And here's one with my target dpi (5280):  Now I would like to save im in a handlable way at high quality (DPI>1000). Is there any way to do this?  Stuff that I have tried so far:  plotting and saving the plot image with PNG, TIFF, SVG, PDF with different DPI values plt.savefig() with different dpi's cv2.imwrite() too large of a file, only solution here is to reduce DPI, which also reduces quality SVG write from matrix: I developed this function but ultimately, the files were too large:  import svgwrite def matrix_to_svg(matrix, filename, padding = 0, cellSize=1): # get matrix dimensions and extremes rows, cols = matrix.shape minVal = np.min(matrix) maxVal = np.max(matrix) # get a drawing dwg = svgwrite.Drawing(filename, profile='tiny', size = (cols*cellSize+2*padding,rows*cellSize+2*padding)) # define the colormap, in this case grayscale since black and white colorScale = lambda val: svgwrite.utils.rgb(int(255*(val-minVal)/(maxVal-minVal)), int(255*(val-minVal)/(maxVal-minVal)), int(255*(val-minVal)/(maxVal-minVal))) # get the color of each pixel in the matrix and draw it for i in range(rows): for j in range(cols): color = colorScale(matrix[i, j]) dwg.add(dwg.rect(insert=(j * cellSize + padding, i * cellSize + padding), size=(cellSize, cellSize), fill=color)) dwg.save() # save   PIL.save(). Files too large  The problem could be also solved by generating better shapes. This would not be an obstacle either. I am open to re-write using a different method, would be grateful if someone would just point me in the right direction.

Answer: Let's make some observations of the effects of changing the DPI: DPI 1000 Height=1970 Width=1970 # Spots=140625 Raw pixels: 3880900 DPI 10000 Height=19690 Width=19690 # Spots=140625 Raw pixels: 387696100  We can see that while the number of spots drawn remains quite consistent (it does vary due to the various rounding in your calculations, but for all intents and purposes, we can consider it constant), the raw pixel count of a raster image generated increases quadratically. A vector representation would seem desireable, since it is freely scalable (quality depending on the capabilities of a renderer). Unfortunately, the way you generate the SVG is flawed, since you've basically turned it into an extremely inefficient raster representation. This is because you generate a rectangle for each individual pixel (even for those that are technically background). Consider that in an 8-bit grayscale image, such as the PNGs you generate requires 1 byte to represent a raw pixel. On the other hand, your SVG representation of a single pixel looks something like this: <rect fill="rgb(255,255,255)" height="1" width="1" x="12345" y="15432" /> Using ~70 bytes per pixel, when we're talking about tens of megapixels... clearly not the way to go. However, let's recall that the number of spots doesn't depend on DPI. Can we just represent the spots in some efficient way? Well, the spots are actually circles, parametrized by position, radius and colour. SVG supports circles, and their representation looks like this: <circle cx="84" cy="108" fill="rgb(0,0,0)" r="2" /> Let's look at the effects of changing the DPI now. DPI 1000 # Spots=140625 Raw pixels: 3880900 SVG size: 7435966 DPI 10000 # Spots=140625 Raw pixels: 387696100 SVG size: 7857942  The slight increase in size is due to increased range of position/radius values.  I somewhat refactored your code example. Here's the result that demonstrates the SVG output. import numpy as np import cv2 import svgwrite MM_IN_INCH = 0.03937 def round_int_to_10s(value): int_value = int(value) return int_value + 10 - int_value % 10 def get_sizes_pixels(height_mm, width_mm, pattern_size_mm, dpi): dpmm = MM_IN_INCH * dpi # dots per mm width_px = round_int_to_10s(np.ceil(width_mm * dpmm)) height_px = round_int_to_10s(np.ceil(height_mm * dpmm)) pattern_size_px = pattern_size_mm * dpmm return height_px, width_px, pattern_size_px def get_grid_positions(size, pattern_size, density): count = int(density * size / pattern_size) # get number of patterns possible if count == 1: return [size // 2] return [int(i * size / (count + 1)) for i in range(1, count + 1)] def get_spot_grid(height_px, width_px, pattern_size_px, density): vertical = get_grid_positions(height_px, pattern_size_px, density) horizontal = get_grid_positions(width_px, pattern_size_px, density) return vertical, horizontal def generate_spots(vertical, horizontal, pattern_size, density, variation): spots = [] noise_halfspan = 2 * pattern_size / density; noise_min, noise_max = (-noise_halfspan, noise_halfspan) for i in vertical: for j in horizontal: # generate the noisy information center = tuple(map(int, (j, i) + variation * np.random.randint(noise_min, noise_max, 2))) d = int(pattern_size + pattern_size * variation * (np.random.rand()-0.5) / 2) spots.append((center, d//2)) # add circle params return spots def render_raster(height, width, spots): im = 255 * np.ones((height, width), dtype=np.uint8) for center, radius in spots: cv2.circle(im, center, radius, 0, -1) # add circle return im def render_svg(height, width, spots): dwg = svgwrite.Drawing(profile='tiny', size = (width, height)) fill_color = svgwrite.utils.rgb(0, 0, 0) for center, radius in spots: dwg.add(dwg.circle(center, radius, fill=fill_color)) # add circle return dwg.tostring() # INPUTS # ############ dpi = 100 # dots per inch WidthOfSample_mm = 50 # mm HeightOfSample_mm = 50 # mm PatternSize_mm = 1 # mm density = 0.75 # 1 is very dense, 0 is not fine at all Variation = 0.75 # 1 is very bad, 0 is very good ############ height, width, pattern_size = get_sizes_pixels(HeightOfSample_mm, WidthOfSample_mm, PatternSize_mm, dpi) vertical, horizontal = get_spot_grid(height, width, pattern_size, density) spots = generate_spots(vertical, horizontal, pattern_size, density, Variation) img = render_raster(height, width, spots) svg = render_svg(height, width, spots) print(f"Height={height} Width={width} # Spots={len(spots)}") print(f"Raw pixels: {img.size}") print(f"SVG size: {len(svg)}") cv2.imwrite("timo.png", img) with open("timo.svg", "w") as f: f.write(svg)  This generates the following output: PNG  | Rendered SVG  Note: Since it's not possible to upload SVGs here, I put it on pastebin, and provide capture of it rendered by Firefox.  Further improvements to the size of the SVG are possible. For example, we're currently using the same colour over an over. Styling or grouping should help remove this redundancy. Here's an example that groups all the spots in one group with constant fill colour: def render_svg(height, width, spots): dwg = svgwrite.Drawing(profile='tiny', size = (width, height)) dwg_spots = dwg.add(dwg.g(id='spots', fill='black')) for center, radius in spots: dwg_spots.add(dwg.circle(center, radius)) # add circle return dwg.tostring()  The output looks the same, but the file is now 4904718 bytes instead of 7435966 bytes. An alternative (pointed out by AKX) if you only desire to draw in black, you may omit the fill specification as well as the grouping, since the default SVG fill colour is black.  The next thing to notice is that most of the spots have the same radius -- in fact, using your settings at DPI of 1000 the unique radii are [1, 2] and at DPI of 10000 they are [15, 16, 17, 18, 19, 20, 21, 22, 23]. How could we avoid repeatedly specifying the same radius? (As far as I can tell, we can't use groups to specify it) In fact, how can we omit repeatedly specifying it's a circle? Ideally we'd just tell it "Draw this mark at all of those positions" and just provide a list of points. Turns out there are two features of SVG that let us do exactly that. First of all, we can specify custom markers, and later refer to them by an ID. <marker id="id1" markerHeight="2" markerWidth="2" refX="1" refY="1"> <circle cx="1" cy="1" fill="black" r="1" /> </marker>  Second, the polyline element can optionally draw markers at every vertex of the polyline. If we draw the polyline with no stroke and no fill, all we end up is with the markers. <polyline fill="none" marker-end="url(#id1)" marker-mid="url(#id1)" marker-start="url(#id1)" points="2,5 8,22 11,26 9,46 8,45 2,70 ... and so on" stroke="none" />  Here's the code: def group_by_radius(spots): radii = set([r for _,r in spots]) groups = {r: [] for r in radii} for c, r in spots: groups[r].append(c) return groups def render_svg_v2(height, width, spots): dwg = svgwrite.Drawing(profile='full', size=(width, height)) by_radius = group_by_radius(spots) dwg_grp = dwg.add(dwg.g(stroke='none', fill='none')) for r, centers in by_radius.items(): dwg_marker = dwg.marker(id=f'r{r}', insert=(r, r), size=(2*r, 2*r)) dwg_marker.add(dwg.circle((r, r), r=r)) dwg.defs.add(dwg_marker) dwg_line = dwg_grp.add(dwg.polyline(centers)) dwg_line.set_markers((dwg_marker, dwg_marker, dwg_marker)) return dwg.tostring()  The output SVG still looks the same, but now the filesize at DPI of 1000 is down to 1248852 bytes.  With high enough DPI, a lot of the coordinates will be 3, 4 or even 5 digits. If we bin the coordinates into tiles of 100 or 1000 pixels, we can then take advantage of the use element, which lets us apply an offset to the referenced object. Thus, we can limit the polyline coordinates to 2 or 3 digits at the cost of some extra overhead (which is generally worth it). Here's an initial (clumsy) implementation of that: def bin_points(points, bin_size): bins = {} for x,y in points: bin = (max(0, x // bin_size), max(0, y // bin_size)) base = (bin[0] * bin_size, bin[1] * bin_size) offset = (x - base[0], y - base[1]) if base not in bins: bins[base] = [] bins[base].append(offset) return bins def render_svg_v3(height, width, spots, bin_size): dwg = svgwrite.Drawing(profile='full', size=(width, height)) by_radius = group_by_radius(spots) dwg_grp = dwg.add(dwg.g(stroke='none', fill='none')) polyline_counter = 0 for r, centers in by_radius.items(): dwg_marker = dwg.marker(id=f'm{r}', insert=(r, r), size=(2*r, 2*r)) dwg_marker.add(dwg.circle((r, r), r=r, fill='black')) dwg.defs.add(dwg_marker) dwg_marker_grp = dwg_grp.add(dwg.g()) marker_iri = dwg_marker.get_funciri() for kind in ['start','end','mid']: dwg_marker_grp[f'marker-{kind}'] = marker_iri bins = bin_points(centers, bin_size) for base, offsets in bins.items(): dwg_line = dwg.defs.add(dwg.polyline(id=f'p{polyline_counter}', points=offsets)) polyline_counter += 1 dwg_marker_grp.add(dwg.use(dwg_line, insert=base)) return dwg.tostring()  With bin size set to 100, and DPI of 1000, we get to a file size of 875012 bytes, which means about 6.23 bytes per spot. That's not so bad for XML based format. With DPI of 10000 we need bin size of 1000 to make a meaningful improvement, which yields something like 1349325 bytes (~9.6B/spot).