Turning my 3D-Printer into a Laser Engraver 5: Basic Slicing

With the new understanding of basically only needing two-and-a-half commands (Turning the laser on and off, moving the laser), it was time to actually turn an image into instructions to get my first real test run going.

Qt has a build-in image class QImage, which provides some good features and is designed and optimized for I/O, and for direct pixel access and manipulation [1]. This can easily turn any image into grayscale or black-and-white. When doing the latter, it also does dithering by default, that is, it simulates grayscale by mixing black and white pixels nearby. So 50% grey would alternate between black and white, which would make it look like the original grey from a sufficiently large distance. Turning dithering off would instead create a solid black blob.

Due to the very small area in which the laser actually burns, I decide against just treating the engraving plate as a map where every possible point I could burn is treated as a pixel. Instead, I work with boxes of a certain size, which I treat like pixels when processing the image for print, but actually burn in a more complicated pattern. For example, the actual plate I want to engrave has a size of 10x10cm, while the laser burns an area of around 0.1mm² at a time. The printhead can be controlled down to 0.01mm precision. To keep burning times low, I treat each area of 5x5mm as one pixel, which I call, for lack of a better term, a ‘black box’, of which only the borders and diagonals will actually be burned.

This means that the source image in the example above is scaled to a 20×20 pixel pure black and white image. This resulting image is then processed in a really rather simple way:

def image_to_code( settings: Dict, image: QImage ) -> str: #Returns the G-code instructions as a string to save.
    height = image.height()
    width = image.width()

    pixel_box_size = settings[ 'pixel_box_size' ]

    instructions = [ ] # Individual commands, conceptually cleaner than joining to the same string

    for row_inv in range( height ):
        row = height - row_inv - 1 # 0, 0 is the lower left corner in the printer, but upper left in the image. Row is printer-focused.
        for col in range( width ):
            color = image.pixelColor( col, row )
                        
                        # Instructions are only needed if a pixel is black, i.e. the corresponding black box will be burned
            if color == Qt.black:
                instructions.extend( _fill_square( settings, col, row ) )

    return '\n'.join( instructions ) # Put all instructions into a string


def _fill_square( settings: Dict, col: int, row: int ) -> List[ str ]:
    pixel_box_size = settings[ 'pixel_box_size' ]
    return [
        # Move to local origin
        f'G0 X{col * pixel_box_size:.2f} Y{row * pixel_box_size:.2f}',
        _CMD_laser_on,
        # Burn perimeter
        f'G1 X{(col + 1) * pixel_box_size:.2f} Y{row * pixel_box_size:.2f}',
        f'G1 X{(col + 1) * pixel_box_size:.2f} Y{(row + 1) * pixel_box_size:.2f}',
        f'G1 X{col * pixel_box_size:.2f} Y{(row + 1) * pixel_box_size:.2f}',
        f'G0 X{col * pixel_box_size:.2f} Y{row * pixel_box_size:.2f}',
        # Burn first diagonal
        f'G0 X{(col + 1) * pixel_box_size:.2f} Y{(row + 1) * pixel_box_size:.2f}',
        _CMD_laser_off,
        # Move to burn second diagonal
        f'G1 X{(col + 1) * pixel_box_size:.2f} Y{row * pixel_box_size:.2f}',
        _CMD_laser_on,
        # Burn second diagonal
        f'G1 X{col * pixel_box_size:.2f} Y{(row + 1) * pixel_box_size:.2f}',
        _CMD_laser_off
    ]

 

I then went the other way around and wrote a quick script which takes the G-code as input and returns a preview of what the burned image might look like. For this cute rabbit [2], the result looks like this:

Apparently, my code flips and rotates the image. This is why the preview was a good idea – these kinds of errors can be fixed ahead of time.

Even with that error, actually engraving the image, in a recognizable way, works! (There are no interesting pictures to show, I just engraved the letter “K” in a fancy font.

Next step: Adding shades of gray and making the process much, much faster.

The full code can be found on my Github.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.