Taste of APL, Revisited


(If you haven't read the last blog post, you can read it here.)

Digital image processing, for real

I didn't actually have my code tested last time. I haven't figure out how to do file input/output in Dyalog APL, and I would like to do that now. Normally when I learn a new language its documentation is the first thing I will choose to read, but that idea somehow flew over me when I started to learn APL. Very strange.

It seems like basic file IO wasn't a part of ISO APL, so probably any file IO code will be restricted to certain specific implementations. I choose Dyalog because it seems to be the most popular and has the best support.

File input/output

Dyalog has ⎕NGET (which returns all content inside a file) and ⎕NPUT (which writes text to a file), enough for what I need to do. "Isn't image files all binary data?" you might ask. Well:

Portable anymap format (PNM)

PNM is a series of very simple graphics formats, basically it's just pixel data & image metadata written out in ASCII characters. e.g. this is a 6x10 bitmap of letter "J":

P1
6 10
0 0 0 0 1 0
0 0 0 0 1 0
0 0 0 0 1 0
0 0 0 0 1 0
0 0 0 0 1 0
0 0 0 0 1 0
1 0 0 0 1 0
0 1 1 1 0 0
0 0 0 0 0 0
0 0 0 0 0 0

I'll be only dealing with PNM images in this blog post.

By the way, somehow searching "netpbm" on google will bring out the picture of npm. Also kinda strange.

A parser for PNM

Normally in other languages you would want some kind of a record type for representing these images, e.g. this in Python:

class PBM():
    def __init__(width, height, data):
        this.width = width
        this.height = height
        this.data = data

class PGM():
    def __init__(width, height, max_depth, data):
        this.width = width
        this.height = height
        this.max_depth = max_depth
        this.data = data

class PPM():
    def __init__(width, height, max_depth, data):
        this.width = width
        this.height = height
        this.max_depth = max_depth
        this.data = data

Dyalog APL seems to have support for these "OOP" constructs but I decided to use a heterogeneous array anyway:

 (type, max_depth, data)
 we can get width & height by binary ⍴.
 PBM can be seen as PGM with max_depth = 1.
PNM_Type←{⊃⍵[1]}
PNM_Data←{⊃⍵[3]}
PNM_MaxDepth←{⊃⍵[2]}

It's better to have disp enabled when learning APL:

)copy dfns disp

So that you can see if a value is scalar or a vector with a shape of 1. These two things look the same in most APL REPL. I wasted a lot of time on dealing with splitting strings & stuff because of that.

⎕NGET will wrap the file content into a scalar, you have to unwrap it using unary before accessing.

PNM_Parse←{
    file←⊃(⎕NGET (⍵ 1)[1])
    TODO: stuff.
    file
}

We have to select all lines that isn't an empty line & does not start with a hash # (these are comments):

if a line does not start with # and is not empty after removing all spaces
and tabs, it's a line containing data.
IsDataLine←{('#'1⊃⍵)∧(0≠≢(⍵~((⎕UCS 32)(⎕UCS 9))))}
ReadFileData←{
    d←⊃(⎕NGET(⍵ 1))[1]
    (IsDataLine¨d)/d
}
fileDataReadFileData 
...

We will need to convert strings into integers:

StringToInt←{1048-⍨⎕UCS¨⍵}

and split the strings with arbitrary single character as separator:

Split←{⍺(≠⊆⊢)⍵}

With these auxillary functions we can finally extract needed data:

type←(⎕UCS(1fileData)[2])-48
shapeStringToInt¨' 'Split (2fileData)
depth←{⍵=1:1  (StringToInt 3fileData)}type
data←(⊖shape)⍴⊃,/{StringToInt¨' 'Split ⍵}¨({⍵=1:2  3}type)↓fileData

The finished full parser:

PNM_Parse←{
    IsDataLine←{('#'1⊃⍵)∧(0≠≢(⍵~((⎕UCS 32)(⎕UCS 9))))}
    ReadFileData←{
        d←⊃(⎕NGET(⍵ 1))[1]
        (IsDataLine¨d)/d
    }
    StringToInt←{1048-⍨⎕UCS¨⍵}
    Split←{⍺(≠⊆⊢)⍵}
    fileDataReadFileData 
    type←(⎕UCS(1fileData)[2])-48
    shapeStringToInt¨' 'Split(2fileData)
    depth←{⍵=1:1  (StringToInt 3fileData)}type
    data←(⊖shape)⍴⊃,/{StringToInt¨' 'Split ⍵}¨({⍵=1:2  3}type)↓fileData
    (type depth data)
}

A writer for PNM

This part is relatively easy. First we need a function that turns integers into strings:

IntToString←{⍵=0:'0'⋄{⎕UCS¨48+(10∘⊥⍣¯1)⍵}

The ⎕UCS¨48+ part is easy, but (10∘⊥⍣¯1)⍵ part is somewhat confusing: 10∘⊥ originally turns a vector of single digits into a single number (e.g. 10 ⊥ 1 2 3 returns 123), and ⍣¯1 turns that function into its inversion, which is turning a single number to a vector of its digits. How does this inversion work I have absolutely no idea.

This function joins string together:

StringJoin←{⊃,/,∘⍺¨⍵}

The finished writer:

PNM_Write←{
    typePNM_Type 
    maxdepthPNM_MaxDepth 
    dataPNM_Data 
    shape←⍴data
    IntToString←{⍵=0:'0'  ,⎕UCS¨48+(10∘⊥⍣¯1)⍵}
    StringJoin←{⊃,/,∘⍺¨⍵}
    header←('P'(IntToString type))
    shapeString' 'StringJoin(IntToString¨shape)
    depthString←{type='1':''  IntToString maxdepth}
    dataString' 'StringJoin(IntToString¨,data)
    res←⊂(⎕UCS 10)StringJoin(header shapeString depthString dataString)
    res ⎕NPUT(⍺ 1)
}

Cheating?

I have to consult https://aplcart.info/; some of them I would never have figure out (e.g. the (10∘⊥⍣¯1)⍵. What kind of people with experiences in any other languages will think it's like "oh I have 10∘⊥ I'll just take its inversion"? Nothing works like that! C/Java/JavaScript doesn't work like that, Haskell/Standard ML doesn't work like that, etc… yet it (and probably its descendants as well) does. Pretty crazy I would say.)

The operators

In the previous write-up I defined MeanBlur as follows:

MeanBlur←{({((+/,)÷(≢,))⍵}⌺⍺) ⍵}

turns out this is no good because we also have to:

  • scale all the numbers to [0, 255]
  • perform floor to all of the numbers because the result returned from the function has decimals while we need integers.

I thought I need (⌈/,⌊/) which returns the maximum and minimum from a vector but it's not because the scale is already fixed by PNM's metadata.

A function that scales [A1, A2] value to [B1, B2] value can be defined as follow:

Scale←{B1+(B2-B1)×((⍵-A1)÷(A2-A1))}

The fixed MeanBlur function can be defined as follow:

It's "PGM_" because this only works on PBM/PGM data...
PGM_MeanBlur←{
    maxdepthPNM_MaxDepth 
    Scale←{255×⍵÷maxdepth}
    MeanBlur←{({((+/,)÷(≢,))⍵}⌺⍺)⍵}
    dataPNM_Data 
    resData←⌊(3 3 MeanBlur Scale data)
    (2 255 resData)
}

I took a picture from https://picsum.photos and did a small test:

test1.png test2.png test3.png test4.png

The image did get more blurry each time PGM_MeanBlur is applied.

Conclusion

It's… not the Silver Bullet (TM) by any means, but it was very revolutionary when it first came out (at least 50 years ago) and some of its idea are still very powerful till this day, so definitely worth spending some time playing around with it. I could have done more with it but I've already spent too much time on it & I really want to work on something else…

Full code can be found here.


Back

© Sebastian Higgins 2021 All Rights Reserved.
Content on this page is distributed under the CC BY-NC-SA 4.0 license unless further specified.
All code snippets on this page are in public domain.
Last update: 2021.3.7