├── .gitignore ├── README.md ├── constants.py ├── image_to_gcode.py ├── images ├── flower.jpg ├── flower_gcode.png ├── tree.png └── tree_gcode.png └── requirements.txt /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/ 2 | .vscode/ 3 | *.png 4 | *.jpg 5 | *.dot 6 | *.nc 7 | !images/* -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Image to G-code converter 2 | 3 | This repository contains a Python 3 script that takes an **image as input** and generates a **2D G-code file as output**. The script can either use the provided image as an edges image, or auto-detect the edges using the Sobel operator. Then a graph is built and it is converted to G-code. You can then **use the produced G-code in a 2D plotter**, you may find this other project of mine useful: [plotter](https://github.com/Stypox/plotter). 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
Auto-detecting edgesConsidering image as already edges
Obtained with http://jherrm.com/gcode-viewer/
20 | 21 | ## Usage 22 | 23 | You can run the script normally with [Python 3](https://www.python.org/downloads/): 24 | ``` 25 | python3 image_to_gcode.py ARGUMENTS... 26 | ``` 27 | This is the help screen with all valid arguments (obtainable with `python3 image_to_gcode.py --help`): 28 | ``` 29 | usage: image_to_gcode.py [-h] -i FILE -o FILE [--dot-output FILE] [-e MODE] [-t VALUE] 30 | 31 | Detects the edges of an image and converts them to 2D gcode that can be printed by a plotter 32 | 33 | optional arguments: 34 | -h, --help show this help message and exit 35 | -i FILE, --input FILE 36 | Image to convert to gcode; all formats supported by the Python imageio library are supported 37 | -o FILE, --output FILE 38 | File in which to save the gcode result 39 | --dot-output FILE Optional file in which to save the graph (in DOT format) generated during an intermediary step of gcode generation 40 | -e MODE, --edges MODE 41 | Consider the input file already as an edges matrix, not as an image of which to detect the edges. MODE should be either `white` or `black`, that is the color of the edges in the image. The image should only be made of white or black pixels. 42 | -t VALUE, --threshold VALUE 43 | The threshold in range (0,255) above which to consider a pixel as part of an edge (after Sobel was applied to the image or on reading the edges from file with the --edges option) 44 | ``` 45 | 46 | The required parameters are the input and the output. You may want to tune the threshold value in order to obtain a better graph. 47 | 48 | An example of command is: 49 | ```sh 50 | python3 image_to_gcode.py --input image.png --output graph.nc --threshold 100 51 | ``` -------------------------------------------------------------------------------- /constants.py: -------------------------------------------------------------------------------- 1 | circumferences = [ 2 | # r=0 3 | [(0,0)], 4 | # r=1 5 | [(1,0),(0,1),(-1,0),(0,-1)], 6 | # r=2 7 | [(2,0),(2,1),(1,2),(0,2),(-1,2),(-2,1),(-2,0),(-2,-1),(-1,-2),(0,-2),(1,-2),(2,-1)], 8 | # r=3 9 | [(3,0),(3,1),(2,2),(1,3),(0,3),(-1,3),(-2,2),(-3,1),(-3,0),(-3,-1),(-2,-2),(-1,-3),(0,-3),(1,-3),(2,-2),(3,-1)], 10 | # r=4 11 | [(4,0),(4,1),(4,2),(3,3),(2,4),(1,4),(0,4),(-1,4),(-2,4),(-3,3),(-4,2),(-4,1),(-4,0),(-4,-1),(-4,-2),(-3,-3),(-2,-4),(-1,-4),(0,-4),(1,-4),(2,-4),(3,-3),(4,-2),(4,-1)], 12 | # r=5 13 | [(5,0),(5,1),(5,2),(4,3),(3,4),(2,5),(1,5),(0,5),(-1,5),(-2,5),(-3,4),(-4,3),(-5,2),(-5,1),(-5,0),(-5,-1),(-5,-2),(-4,-3),(-3,-4),(-2,-5),(-1,-5),(0,-5),(1,-5),(2,-5),(3,-4),(4,-3),(5,-2),(5,-1)], 14 | # r=6 15 | [(6,0),(6,1),(6,2),(5,3),(5,4),(4,5),(3,5),(2,6),(1,6),(0,6),(-1,6),(-2,6),(-3,5),(-4,5),(-5,4),(-5,3),(-6,2),(-6,1),(-6,0),(-6,-1),(-6,-2),(-5,-3),(-5,-4),(-4,-5),(-3,-5),(-2,-6),(-1,-6),(0,-6),(1,-6),(2,-6),(3,-5),(4,-5),(5,-4),(5,-3),(6,-2),(6,-1)], 16 | # r=7 17 | [(7,0),(7,1),(7,2),(6,3),(6,4),(5,5),(4,6),(3,6),(2,7),(1,7),(0,7),(-1,7),(-2,7),(-3,6),(-4,6),(-5,5),(-6,4),(-6,3),(-7,2),(-7,1),(-7,0),(-7,-1),(-7,-2),(-6,-3),(-6,-4),(-5,-5),(-4,-6),(-3,-6),(-2,-7),(-1,-7),(0,-7),(1,-7),(2,-7),(3,-6),(4,-6),(5,-5),(6,-4),(6,-3),(7,-2),(7,-1)], 18 | # r=8 19 | [(8,0),(8,1),(8,2),(7,3),(7,4),(6,5),(5,6),(4,7),(3,7),(2,8),(1,8),(0,8),(-1,8),(-2,8),(-3,7),(-4,7),(-5,6),(-6,5),(-7,4),(-7,3),(-8,2),(-8,1),(-8,0),(-8,-1),(-8,-2),(-7,-3),(-7,-4),(-6,-5),(-5,-6),(-4,-7),(-3,-7),(-2,-8),(-1,-8),(0,-8),(1,-8),(2,-8),(3,-7),(4,-7),(5,-6),(6,-5),(7,-4),(7,-3),(8,-2),(8,-1)], 20 | # r=9 21 | [(9,0),(9,1),(9,2),(9,3),(8,4),(8,5),(7,6),(6,7),(5,8),(4,8),(3,9),(2,9),(1,9),(0,9),(-1,9),(-2,9),(-3,9),(-4,8),(-5,8),(-6,7),(-7,6),(-8,5),(-8,4),(-9,3),(-9,2),(-9,1),(-9,0),(-9,-1),(-9,-2),(-9,-3),(-8,-4),(-8,-5),(-7,-6),(-6,-7),(-5,-8),(-4,-8),(-3,-9),(-2,-9),(-1,-9),(0,-9),(1,-9),(2,-9),(3,-9),(4,-8),(5,-8),(6,-7),(7,-6),(8,-5),(8,-4),(9,-3),(9,-2),(9,-1)], 22 | # r=10 23 | [(10,0),(10,1),(10,2),(10,3),(9,4),(9,5),(8,6),(7,7),(6,8),(5,9),(4,9),(3,10),(2,10),(1,10),(0,10),(-1,10),(-2,10),(-3,10),(-4,9),(-5,9),(-6,8),(-7,7),(-8,6),(-9,5),(-9,4),(-10,3),(-10,2),(-10,1),(-10,0),(-10,-1),(-10,-2),(-10,-3),(-9,-4),(-9,-5),(-8,-6),(-7,-7),(-6,-8),(-5,-9),(-4,-9),(-3,-10),(-2,-10),(-1,-10),(0,-10),(1,-10),(2,-10),(3,-10),(4,-9),(5,-9),(6,-8),(7,-7),(8,-6),(9,-5),(9,-4),(10,-3),(10,-2),(10,-1)] 24 | ] 25 | -------------------------------------------------------------------------------- /image_to_gcode.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | #pylint: disable=no-member 3 | 4 | import numpy as np 5 | from scipy import ndimage 6 | import imageio 7 | from PIL import Image, ImageFilter 8 | import constants 9 | import argparse 10 | 11 | 12 | class CircularRange: 13 | def __init__(self, begin, end, value): 14 | self.begin, self.end, self.value = begin, end, value 15 | 16 | def __repr__(self): 17 | return f"[{self.begin},{self.end})->{self.value}" 18 | 19 | def halfway(self): 20 | return int((self.begin + self.end) / 2) 21 | 22 | class Graph: 23 | class Node: 24 | def __init__(self, point, index): 25 | self.x, self.y = point 26 | self.index = index 27 | self.connections = {} 28 | 29 | def __repr__(self): 30 | return f"({self.y},{-self.x})" 31 | 32 | def _addConnection(self, to): 33 | self.connections[to] = False # i.e. not already used in gcode generation 34 | 35 | def toDotFormat(self): 36 | return (f"{self.index} [pos=\"{self.y},{-self.x}!\", label=\"{self.index}\\n{self.x},{self.y}\"]\n" + 37 | "".join(f"{self.index}--{conn}\n" for conn in self.connections if self.index < conn)) 38 | 39 | 40 | def __init__(self): 41 | self.nodes = [] 42 | 43 | def __getitem__(self, index): 44 | return self.nodes[index] 45 | 46 | def __repr__(self): 47 | return repr(self.nodes) 48 | 49 | 50 | def addNode(self, point): 51 | index = len(self.nodes) 52 | self.nodes.append(Graph.Node(point, index)) 53 | return index 54 | 55 | def addConnection(self, a, b): 56 | self.nodes[a]._addConnection(b) 57 | self.nodes[b]._addConnection(a) 58 | 59 | def distance(self, a, b): 60 | return np.hypot(self[a].x-self[b].x, self[a].y-self[b].y) 61 | 62 | def areConnectedWithin(self, a, b, maxDistance): 63 | if maxDistance < 0: 64 | return False 65 | elif a == b: 66 | return True 67 | else: 68 | for conn in self[a].connections: 69 | if self.areConnectedWithin(conn, b, maxDistance - self.distance(conn, b)): 70 | return True 71 | return False 72 | 73 | def saveAsDotFile(self, f): 74 | f.write("graph G {\nnode [shape=plaintext];\n") 75 | for node in self.nodes: 76 | f.write(node.toDotFormat()) 77 | f.write("}\n") 78 | 79 | def saveAsGcodeFile(self, f): 80 | ### First follow all paths that have a start/end node (i.e. are not cycles) 81 | # The next chosen starting node is the closest to the current position 82 | 83 | def pathGcode(i, insidePath): 84 | f.write(f"G{1 if insidePath else 0} X{self[i].y} Y{-self[i].x}\n") 85 | for connTo, alreadyUsed in self[i].connections.items(): 86 | if not alreadyUsed: 87 | self[i].connections[connTo] = True 88 | self[connTo].connections[i] = True 89 | 90 | return pathGcode(connTo, True) 91 | return i 92 | 93 | possibleStartingNodes = set() 94 | for i in range(len(self.nodes)): 95 | if len(self[i].connections) == 0 or len(self[i].connections) % 2 == 1: 96 | possibleStartingNodes.add(i) 97 | 98 | if len(possibleStartingNodes) != 0: 99 | node = next(iter(possibleStartingNodes)) # first element 100 | while 1: 101 | possibleStartingNodes.remove(node) 102 | pathEndNode = pathGcode(node, False) 103 | 104 | if len(self[node].connections) == 0: 105 | assert pathEndNode == node 106 | f.write(f"G1 X{self[node].y} Y{-self[node].x}\n") 107 | else: 108 | possibleStartingNodes.remove(pathEndNode) 109 | 110 | if len(possibleStartingNodes) == 0: 111 | break 112 | 113 | minDistanceSoFar = np.inf 114 | for nextNode in possibleStartingNodes: 115 | distance = self.distance(pathEndNode, nextNode) 116 | if distance < minDistanceSoFar: 117 | minDistanceSoFar = distance 118 | node = nextNode 119 | 120 | 121 | ### Then pick the node closest to the current position that still has unused/available connections 122 | # That node must belong to a cycle, because otherwise it would have been used above 123 | # TODO improve by finding Eulerian cycles 124 | 125 | cycleNodes = set() 126 | for i in range(len(self.nodes)): 127 | someConnectionsAvailable = False 128 | for _, alreadyUsed in self[i].connections.items(): 129 | if not alreadyUsed: 130 | someConnectionsAvailable = True 131 | break 132 | 133 | if someConnectionsAvailable: 134 | cycleNodes.add(i) 135 | 136 | def cyclePathGcode(i, insidePath): 137 | f.write(f"G{1 if insidePath else 0} X{self[i].y} Y{-self[i].x}\n") 138 | 139 | foundConnections = 0 140 | for connTo, alreadyUsed in self[i].connections.items(): 141 | if not alreadyUsed: 142 | if foundConnections == 0: 143 | self[i].connections[connTo] = True 144 | self[connTo].connections[i] = True 145 | cyclePathGcode(connTo, True) 146 | 147 | foundConnections += 1 148 | if foundConnections > 1: 149 | break 150 | 151 | if foundConnections == 1: 152 | cycleNodes.remove(i) 153 | 154 | if len(cycleNodes) != 0: 155 | node = next(iter(cycleNodes)) # first element 156 | while 1: 157 | # since every node has an even number of connections, ANY path starting from it 158 | # must complete at the same place (see Eulerian paths/cycles properties) 159 | cyclePathGcode(node, False) 160 | 161 | if len(cycleNodes) == 0: 162 | break 163 | 164 | pathEndNode = node 165 | minDistanceSoFar = np.inf 166 | for nextNode in possibleStartingNodes: 167 | distance = self.distance(pathEndNode, nextNode) 168 | if distance < minDistanceSoFar: 169 | minDistanceSoFar = distance 170 | node = nextNode 171 | 172 | class EdgesToGcode: 173 | def __init__(self, edges): 174 | self.edges = edges 175 | self.ownerNode = np.full(np.shape(edges), -1, dtype=int) 176 | self.xSize, self.ySize = np.shape(edges) 177 | self.graph = Graph() 178 | 179 | def getCircularArray(self, center, r, smallerArray = None): 180 | circumferenceSize = len(constants.circumferences[r]) 181 | circularArray = np.zeros(circumferenceSize, dtype=bool) 182 | 183 | if smallerArray is None: 184 | smallerArray = np.ones(1, dtype=bool) 185 | smallerSize = np.shape(smallerArray)[0] 186 | smallerToCurrentRatio = smallerSize / circumferenceSize 187 | 188 | for i in range(circumferenceSize): 189 | x = center[0] + constants.circumferences[r][i][0] 190 | y = center[1] + constants.circumferences[r][i][1] 191 | 192 | if x not in range(self.xSize) or y not in range(self.ySize): 193 | circularArray[i] = False # consider pixels outside of the image as not-edges 194 | else: 195 | iSmaller = i * smallerToCurrentRatio 196 | a, b = int(np.floor(iSmaller)), int(np.ceil(iSmaller)) 197 | 198 | if smallerArray[a] == False and (b not in range(smallerSize) or smallerArray[b] == False): 199 | circularArray[i] = False # do not take into consideration not connected regions (roughly) 200 | else: 201 | circularArray[i] = self.edges[x, y] 202 | 203 | return circularArray 204 | 205 | def toCircularRanges(self, circularArray): 206 | ranges = [] 207 | circumferenceSize = np.shape(circularArray)[0] 208 | 209 | lastValue, lastValueIndex = circularArray[0], 0 210 | for i in range(1, circumferenceSize): 211 | if circularArray[i] != lastValue: 212 | ranges.append(CircularRange(lastValueIndex, i, lastValue)) 213 | lastValue, lastValueIndex = circularArray[i], i 214 | 215 | ranges.append(CircularRange(lastValueIndex, circumferenceSize, lastValue)) 216 | if len(ranges) > 1 and ranges[-1].value == ranges[0].value: 217 | ranges[0].begin = ranges[-1].begin - circumferenceSize 218 | ranges.pop() # the last range is now contained in the first one 219 | return ranges 220 | 221 | def getNextPoints(self, point): 222 | """ 223 | Returns the radius of the circle used to identify the points and 224 | the points toward which propagate, in a tuple `(radius, [point0, point1, ...])` 225 | """ 226 | 227 | bestRadius = 0 228 | circularArray = self.getCircularArray(point, 0) 229 | allRanges = [self.toCircularRanges(circularArray)] 230 | for radius in range(1, len(constants.circumferences)): 231 | circularArray = self.getCircularArray(point, radius, circularArray) 232 | allRanges.append(self.toCircularRanges(circularArray)) 233 | if len(allRanges[radius]) > len(allRanges[bestRadius]): 234 | bestRadius = radius 235 | if len(allRanges[bestRadius]) >= 4 and len(allRanges[-2]) >= len(allRanges[-1]): 236 | # two consecutive circular arrays with the same or decreasing number>=4 of ranges 237 | break 238 | elif len(allRanges[radius]) == 2 and radius > 1: 239 | edge = 0 if allRanges[radius][0].value == True else 1 240 | if allRanges[radius][edge].end-allRanges[radius][edge].begin < len(constants.circumferences[radius]) / 4: 241 | # only two ranges but the edge range is small (1/4 of the circumference) 242 | if bestRadius == 1: 243 | bestRadius = 2 244 | break 245 | elif len(allRanges[radius]) == 1 and allRanges[radius][0].value == False: 246 | # this is a point-shaped edge not sorrounded by any edges 247 | break 248 | 249 | if bestRadius == 0: 250 | return 0, [] 251 | 252 | circularRanges = allRanges[bestRadius] 253 | points = [] 254 | for circularRange in circularRanges: 255 | if circularRange.value == True: 256 | circumferenceIndex = circularRange.halfway() 257 | x = point[0] + constants.circumferences[bestRadius][circumferenceIndex][0] 258 | y = point[1] + constants.circumferences[bestRadius][circumferenceIndex][1] 259 | 260 | if x in range(self.xSize) and y in range(self.ySize) and self.ownerNode[x, y] == -1: 261 | points.append((x,y)) 262 | 263 | return bestRadius, points 264 | 265 | def propagate(self, point, currentNodeIndex): 266 | radius, nextPoints = self.getNextPoints(point) 267 | 268 | # depth first search to set the owner of all reachable connected pixels 269 | # without an owner and find connected nodes 270 | allConnectedNodes = set() 271 | def setSeenDFS(x, y): 272 | if (x in range(self.xSize) and y in range(self.ySize) 273 | and np.hypot(x-point[0], y-point[1]) <= radius + 0.5 274 | and self.edges[x, y] == True and self.ownerNode[x, y] != currentNodeIndex): 275 | if self.ownerNode[x, y] != -1: 276 | allConnectedNodes.add(self.ownerNode[x, y]) 277 | self.ownerNode[x, y] = currentNodeIndex # index of just added node 278 | setSeenDFS(x+1, y) 279 | setSeenDFS(x-1, y) 280 | setSeenDFS(x, y+1) 281 | setSeenDFS(x, y-1) 282 | 283 | self.ownerNode[point] = -1 # reset to allow DFS to start 284 | setSeenDFS(*point) 285 | for nodeIndex in allConnectedNodes: 286 | if not self.graph.areConnectedWithin(currentNodeIndex, nodeIndex, 11): 287 | self.graph.addConnection(currentNodeIndex, nodeIndex) 288 | 289 | validNextPoints = [] 290 | for nextPoint in nextPoints: 291 | if self.ownerNode[nextPoint] == currentNodeIndex: 292 | # only if this point belongs to the current node after the DFS, 293 | # which means it is reachable and connected 294 | validNextPoints.append(nextPoint) 295 | 296 | for nextPoint in validNextPoints: 297 | nodeIndex = self.graph.addNode(nextPoint) 298 | self.graph.addConnection(currentNodeIndex, nodeIndex) 299 | self.propagate(nextPoint, nodeIndex) 300 | self.ownerNode[point] = currentNodeIndex 301 | 302 | def addNodeAndPropagate(self, point): 303 | nodeIndex = self.graph.addNode(point) 304 | self.propagate(point, nodeIndex) 305 | 306 | def buildGraph(self): 307 | for point in np.ndindex(np.shape(self.edges)): 308 | if self.edges[point] == True and self.ownerNode[point] == -1: 309 | radius, nextPoints = self.getNextPoints(point) 310 | if radius == 0: 311 | self.addNodeAndPropagate(point) 312 | else: 313 | for nextPoint in nextPoints: 314 | if self.ownerNode[nextPoint] == -1: 315 | self.addNodeAndPropagate(nextPoint) 316 | 317 | return self.graph 318 | 319 | 320 | def sobel(image): 321 | image = np.array(image, dtype=float) 322 | image /= 255.0 323 | Gx = ndimage.sobel(image, axis=0) 324 | Gy = ndimage.sobel(image, axis=1) 325 | res = np.hypot(Gx, Gy) 326 | res /= np.max(res) 327 | res = np.array(res * 255, dtype=np.uint8) 328 | return res[2:-2, 2:-2, 0:3] 329 | 330 | def convertToBinaryEdges(edges, threshold): 331 | result = np.maximum.reduce([edges[:, :, 0], edges[:, :, 1], edges[:, :, 2]]) >= threshold 332 | if np.shape(edges)[2] > 3: 333 | result[edges[:, :, 3] < threshold] = False 334 | return result 335 | 336 | 337 | def parseArgs(namespace): 338 | argParser = argparse.ArgumentParser(fromfile_prefix_chars="@", 339 | description="Detects the edges of an image and converts them to 2D gcode that can be printed by a plotter") 340 | 341 | argParser.add_argument_group("Data options") 342 | argParser.add_argument("-i", "--input", type=argparse.FileType('br'), required=True, metavar="FILE", 343 | help="Image to convert to gcode; all formats supported by the Python imageio library are supported") 344 | argParser.add_argument("-o", "--output", type=argparse.FileType('w'), required=True, metavar="FILE", 345 | help="File in which to save the gcode result") 346 | argParser.add_argument("--dot-output", type=argparse.FileType('w'), metavar="FILE", 347 | help="Optional file in which to save the graph (in DOT format) generated during an intermediary step of gcode generation") 348 | argParser.add_argument("-e", "--edges", type=str, metavar="MODE", 349 | help="Consider the input file already as an edges matrix, not as an image of which to detect the edges. MODE should be either `white` or `black`, that is the color of the edges in the image. The image should only be made of white or black pixels.") 350 | argParser.add_argument("-t", "--threshold", type=int, default=32, metavar="VALUE", 351 | help="The threshold in range (0,255) above which to consider a pixel as part of an edge (after Sobel was applied to the image or on reading the edges from file with the --edges option)") 352 | 353 | argParser.parse_args(namespace=namespace) 354 | 355 | if namespace.edges is not None and namespace.edges not in ["white", "black"]: 356 | argParser.error("mode for --edges should be `white` or `black`") 357 | if namespace.threshold <= 0 or namespace.threshold >= 255: 358 | argParser.error("value for --threshold should be in range (0,255)") 359 | 360 | def main(): 361 | class Args: pass 362 | parseArgs(Args) 363 | 364 | image = imageio.imread(Args.input) 365 | if Args.edges is None: 366 | edges = sobel(image) 367 | elif Args.edges == "black": 368 | edges = np.invert(image) 369 | else: # Args.edges == "white" 370 | edges = image 371 | edges = convertToBinaryEdges(edges, Args.threshold) 372 | 373 | converter = EdgesToGcode(edges) 374 | converter.buildGraph() 375 | 376 | if Args.dot_output is not None: 377 | converter.graph.saveAsDotFile(Args.dot_output) 378 | converter.graph.saveAsGcodeFile(Args.output) 379 | 380 | if __name__ == "__main__": 381 | main() -------------------------------------------------------------------------------- /images/flower.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stypox/image-to-gcode/cf6143c2d64f3d5b84583ad132d26f13064bbee7/images/flower.jpg -------------------------------------------------------------------------------- /images/flower_gcode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stypox/image-to-gcode/cf6143c2d64f3d5b84583ad132d26f13064bbee7/images/flower_gcode.png -------------------------------------------------------------------------------- /images/tree.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stypox/image-to-gcode/cf6143c2d64f3d5b84583ad132d26f13064bbee7/images/tree.png -------------------------------------------------------------------------------- /images/tree_gcode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stypox/image-to-gcode/cf6143c2d64f3d5b84583ad132d26f13064bbee7/images/tree_gcode.png -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | numpy 2 | scipy 3 | imageio --------------------------------------------------------------------------------