Image Artsification in Mini Micro

JoeStrout - Jan 15 '23 - - Dev Community

A community member (@juh9870) posted a very cool Mini Micro program to our Discord this week. The core algorithm is simple: it randomly selects a pixel of an image, and then applies that color to a 3x3 area around that point. Repeat as long as desired.

The effect is mesmerizing.

Animation of image processing in Mini Micro

The program was initially written as a mold simulation. Instead of giving it an image, you can have it initialize with some random spots of color; then, as the algorithm proceeds, these spots grow and eventually compete for space.

Simulation of growing mold in several colors

But the real fun, in my view at least, comes from running it on images.

Mona Lisa, getting artsified

Apollo astronaut, getting artsified

The core of this program is beautifully simple MiniScript code:

    while true
        x = floor(rnd * w)
        y = floor(rnd * h)
        col = g.pixel(x, y)
        alpha = col[7:]
        rawColor = col[:7]
        if rawColor != color.black and alpha == "FF" then
            g.drawImage mask, x - mw2, y - mh2, mw, mh, 
              0, 0, mw, mh, col
        end if
    end while
Enter fullscreen mode Exit fullscreen mode

It just repeatedly grabs a random point, and if it's an opaque pixel with a nonblack color, draws the mask (our 3x3 square) at that point, tinted by the pixel color.

Want to try it yourself? Download Mini Micro, unpack it, and run it (you may need to right-click and "open" it, as the current build is not code-signed). Use the edit command to bring up the code editor. Then paste in the following code.

(Twist this triangle open to see the complete code.)
clear
display(5).mode = displayMode.pixel
g = display(5)
display(6).mode = displayMode.pixel
bg = display(6)
w = 256
h = 256
black = "#00000000"
maskSource = "square"
initializer = null
noExpand = false
puncture = false

mask = function(mask)
    outer.maskSource = mask
end function

// Initializes field using an image as a source
fromImage = function(src)
    outer.initializer = function
        img = resolveImage(src)
        setSize img.width, img.height
        size = getSize
        ox = (size.w - img.width) / 2
        oy = (size.h - img.height) / 2
        g.drawImage img, ox, oy
    end function
end function

// Initializes field using random points
seed = function(size, seeds = 1, colors = null)
    outer.initializer = function
        if size isa number then locals.size = {"w": size, "h": size}
        if size isa list then locals.size = {"w": size[0], "h": size[1]}
        w = size.w
        h = size.h
        setSize w, h
        size = getSize
        ox = size.w - w
        oy = size.h - h
        if colors == null then colors = [color.red, color.green,
          color.maroon, color.orange, color.yellow, color.blue]
        seed = colors.len * seeds
        while seed > 0
            x = floor(rnd * w) + ox / 2
            y = floor(rnd * h) + oy / 2
            g.setPixel x, y, colors[seed % colors.len]
            seed = seed - 1
        end while
    end function
end function

// Starts the simulation
play = function(maxSteps = null)
    text.clear
    mask = resolveImage(maskSource)
    g.clear
    initializer
    stepsDone = 0
    startTime = time

    bg.clear color.black
    mw = mask.width
    mh = mask.height
    mw2 = floor(mw / 2)
    mh2 = floor(mh / 2)
    while true
        x = floor(rnd * w)
        y = floor(rnd * h)
        col = g.pixel(x, y)
        alpha = col[7:]
        rawColor = col[:7]
        if rawColor != color.black and alpha == "FF" then
            if noExpand then col = rawColor + "FE"
            if puncture then g.setPixel x, y, black

            g.drawImage mask, x - mw2, y - mh2, mw, mh, 
              0, 0, mw, mh, col
        end if
        stepsDone = stepsDone + 1
        if stepsDone == maxSteps then return
        if key.available then
            key.get; key.get  // pause; unpause
        end if
    end while
end function

// Hides the field
hide = function
    g.clear
    bg.clear
end function

resolveImage = function(src)
    img = null
    if src == "square" or src == "hole" then
        g.fillRect 0, 0, 3, 3, color.white
        if src == "hole" then g.setPixel 1,1, color.black
        img = g.getImage(0, 0, 3, 3)
        g.clear
    else if src == "diagonal" then
        g.line 0, 0, 3, 3, color.white
        img = g.getImage(0, 0, 3, 3)
        g.clear
    else if src isa Image then
        img = src
    else if src[:4] == "http" then
        img = http.get(src)
    else
        img = file.loadImage(src)
    end if
    return img
end function

fit = function
    sw = PixelDisplay.width / w
    sh = PixelDisplay.height / h
    scale = 0
    if sw < sh then
        scale = sw
    else
        scale = sh
    end if
    outer.w = floor(PixelDisplay.width / scale)
    outer.h = floor(PixelDisplay.height / scale)
    g.clear black, w, h
    g.scale = scale
end function

setSize = function(w, h)
    outer.w = w
    outer.h = h
    fit
end function

getSize = function
    return {"w": w, "h": h}
end function

hide
seed 128
Enter fullscreen mode Exit fullscreen mode

Then press the green "exit" button in the top-left corner (or press F5) to exit the code editor. run the program. This sets up the environment, including some new commands ready for you to tell it what to do. For starters, just enter

play
Enter fullscreen mode Exit fullscreen mode

This runs the default mold simulation. Press Control-C when you've watched this enough. Now let's try giving it an image to work on! You do this with the fromImage command, and can supply the image in any of several formats. You can, for example, try a Mini Micro file path:

fromImage "/sys/pics/Wumpus.png"
play
Enter fullscreen mode Exit fullscreen mode

And you should quickly get a result that looks something like this:

Artsified Wumpus

Or you can give it a path to a file. The chinchilla images at the top of this article were produced by:

fromImage "https://miniscript.org/img/Icon.png"
play
Enter fullscreen mode Exit fullscreen mode

You can also change the shape of the fill done at every point, referred to in this program as a "mask." There are several built-in masks; "square" is the default, a simple 3x3 square applied at each point. But try "diagonal", and you should get something like this:

fromImage "https://miniscript.org/img/Icon.png"
mask "diagonal"
play
Enter fullscreen mode Exit fullscreen mode

Chinchilla processed with "diagonal" mask

Incidentally, if you want to save out a nice clean (transparent) image of your result after hitting Control-C, you can use a command like this:

file.saveImage "output.png", gfx.getImage
Enter fullscreen mode Exit fullscreen mode

That's how I created the above transparent speedy-chinchilla image.

The last built-in mask is called "hole": a 3x3 square with a black center. It produces in interesting sort of dithered effect:

Chinchilla processed with "hole" mask

But you can also give the mask command a URL or file path to any image you choose. Try a star shape, or an elongated oval. What do you get?

I think it's beautiful when such simple code can produce such artistic results. What do you think? Let me know in the comments below!

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .