mercredi 17 janvier 2018

Image compression with Red

Image compression could be necessary when processing images. For example in the lab we are using a lot of timelapse  images of cells which correspond to several days of recording. Of course, it's easy to zip or unzip the recorded images. But sometimes, we also need to online compress images such as images originating from camera. Due to courtesy of Bruno Anselme, Red can use zlib (https://zlib.net) and his binding can be used with RedCV without any problem. Since Bruno's binding to zlib is focused on string compression, I just wrote two routines which allow to use binary values rather than strings. Routines are really convenient in Red since it's possible to directly include Red/System code inside a Red program. 

compressRGB 

This routine takes a red binary! as parameter, then calls zlib compression algorithm and returns the compressed values as binary!

compressRGB: routine [rgb [binary!] return: [binary!]
    /local 
    byte-count
    data
    buffer 
    ] [
    byte-count: 0
    data: binary/rs-head as red-binary! rgb
    buffer: zlib/compress data binary/rs-length? rgb :byte-count Z_DEFAULT_COMPRESSION
    as red-binary! stack/set-last as red-value! binary/load buffer byte-count
]


decompressRGB

The routine makes the opposite job and uncompresses the compressed binary values.

decompressRGB: routine [ rgb [binary!] bCount [integer!] return: [binary!]
    /local 
    data
    buffer 
    ] [
    data: binary/rs-head as red-binary!  rgb
    buffer: zlib/decompress data bCount
    as red-binary! stack/set-last as red-value! binary/load buffer bCount
]


Code sample

With these two routines, it is really simple to compress images with Red. Just load a Red supported image and extract rgb values as binary! from the image and then compress and uncompress data.

Red [
    Title:   "Test camera Red VID "
    Author:  "Francois Jouen"
    File:    %compress1.red
    Needs:   'View
]

#system [
    #include %../../libs/ZLib/zlib.reds ; for ZLib compression
    ; Thanks to Bruno Anselme
]

defSize: 256x256
imgSize: 0x0
isFile: false

compressRGB: routine [ rgb [binary!] return: [binary!]
    /local 
    byte-count
    data
    buffer 
    ] [
    byte-count: 0
    data: binary/rs-head as red-binary! rgb
    buffer: zlib/compress data binary/rs-length? rgb :byte-count Z_DEFAULT_COMPRESSION
    as red-binary! stack/set-last as red-value! binary/load buffer byte-count
]


decompressRGB: routine [ rgb [binary!] bCount [integer!] return: [binary!]
    /local 
    data
    buffer 
    ] [
    data: binary/rs-head as red-binary!  rgb
    buffer: zlib/decompress data bCount
    as red-binary! stack/set-last as red-value! binary/load buffer bCount
]



loadImage: does [
    isFile: false
    tmp: request-file
    if not none? tmp [
        img0: load tmp
        imgSize: img0/size
        rgb: copy img0/rgb
        img1: make image! imgSize 
        img2: make image! imgSize 
        img3: make image! imgSize 
        img1/rgb: rgb
        img2/rgb: 0.0.0
        img3/rgb: 0.0.0
        b1/image: img1
        b2/image: img2
        b3/image: img3
        f0/text: f1/text: f11/text: f2/text: f3/text: ""
        result: copy #{}
        result2: copy #{}
        f1/text: form imgSize
        isFile: true
    ]
]


compressImage: does [
    f0/text: f2/text:  ""
    n: length? rgb
    result: compressRGB rgb 
    n1: length? result  
    compression: 100 - (100 * n1 / n)
    f0/text: rejoin [" Compression: " form compression]
    append f0/text " %"
    f11/text: rejoin [form n " bytes"]
    f2/text: rejoin [form n1 " bytes"]
    ; not useful for compression
    ; only to show img2 and avoid pointer error
    if cb/data [
        i: n1 
        while [i < n ] [
            append result 0
            i: i + 1
        ]
        img2/rgb: copy result
        b2/image: img2
    ]
]

uncompressImage: does [
    f3/text: ""
    n: length? rgb
    result2: decompressRGB result n
    f3/text: rejoin [form length? result2 " bytes"]
    img3/rgb: copy result2
    b3/image: img3
]


view win: layout [
    title "Compress/Uncompress Images with Red"
    button "load" [loadImage]
    button "Compress Image" [if isFile [compressImage]]
    pad 50x0
    f0: field 156
    cb: check "Show Result"
    button "Uncompress Image" [if isFile [uncompressImage]]
    pad 25x0
    button "Quit" [quit]
    return
    f1: field 120 f11: field 125
    text 120 "Compressed" f2: field 125
    text 120 "Uncompressed" f3: field 125
    return
    
    b1: base defSize black
    b2: base defSize black
    b3: base defSize black
]


Result

Of course the compression ratio depends on the nature of your image, but zlib compression is pretty good on most of images :)

Example with a low size image (512x512 pixels)


Example with a huge image (4032 × 3024 pixels)




jeudi 4 janvier 2018

Writing and reading movies with Red

As explained in a previous post, Red is able to access usb cameras under macOS and Windows. The idea is now to record the content of the camera and then to read back the recorded movie file. To do that I implemented a specific format for Red Camera which allows to record and to read  red video files.


RCAM (RedCAMera) Format

RCAM files contain rgb values.
RCAM files are stored as binary values.
RCAM files contain a 32-byte header starting at offset 0 within the binary file. Video data are beginning just after the header at offset 32. 
RgbSize (Image x size * Image y size * 3) is used to calculate the offset of each image contained in the file.  


Offset
Size
Description
0
4
RCAM Four CC Red signature
4
4
Number of images in the file
8
4
Image x size
12
4
Image y size
16
8
duration in sec (float value)
24
4
Frames by Sec
28
4
compressed data (1) or not (0)
32
Image x size * Image y size * 3
Binary values for all images


Each image is then stored in a block! which is very convenient for a fast access and for all operations that can be performed on blocks with Red.

Storing video data

Basically we need a block that will be used to store the rgb values provided by the camera such as
movie: copy []
Then for each sampled image it is really simple to store the values into te block:
img: to-image cam
append movie img/rgb
Lastly a simple write/binary is used to store the data into a binary file (see saveData function for details)

Code sample 

Red [
    Title:   "Test camera Red VID "
    Author:  "Francois Jouen"
    File:    %reccam.red
    Needs:   View
]

iSize: 320x240
margins: 10x10
cam: none ; for camera
imgSize: 0
count: 0
cti: 0%
movie: copy []
t1: t2: now/time/precise
fn: %video.rvf
d: 0.0
fps: 0
compression: 0

processCam: does [
    count: count + 1
    ct/text: form count
    if cb/data [
        img: to-image cam
        append movie img/rgb
    ]
]

saveData: does [
    n: length? movie
    i: 1
    write/binary fn "RCAM"                          ;Four CC Red signature
    write/binary/append fn to-binary n              ;Number of images
    write/binary/append fn to-binary img/size/x     ;Image x size
    write/binary/append fn to-binary img/size/y     ;Image y size
    write/binary/append fn to-binary d              ;duration in sec
    write/binary/append fn to-binary fps            ;FPS
    write/binary/append fn to-binary compression    ;compressed data (1) or not (0)
    foreach im movie [
        cti: to-percent i / to-float n
        write/binary/append fn movie/:i             ;binary values
        p1/data: cti
        i: i + 1
    ]   
]

view win: layout [
        title "Red Cam"
        origin margins space margins
        cSize: field 100
        cb: check "Record Camera" true
        pad 25x0
        btnQuit: button "Quit" 60x24 on-click [quit]
        return
        ct: field 100
        button "Save" [saveData]
        p1: progress 130 
        return
        cam: camera iSize 
        return
        active: text 55 "Camera" rate 0:0:1 on-time [processCam]
        cam-list: drop-list 160 on-create [
                face/data: cam/data
        ]
        onoff: button "Start/Stop" on-click [
                either cam/selected [
                    t2: now/time/precise
                    d: to-float t2 - t1
                    fps: to-integer round count / d
                    cam/selected: none
                    active/rate: none
                ][
                    cam/selected: cam-list/selected
                    active/rate: 0:0:0.04;  max 1/25 fps in ms
                    img: to-image cam
                    cSize/text: form img/size
                    imgSize: (img/size/x * img/size/y) * 3 
                    movie: copy []
                    count: 0
                    t1: now/time/precise
                ]
        ]
        do [cam-list/selected: 1 active/rate: none]
]

Reading video files

The reading of red video files is also very simple. You just need to load the file as a binary file and get the 32-bytes header which contains all information required for a correct reading such as the number of images, the size of images or the duration of the movie. With x and y size of the image it's easy to compute the offset of image. Since we use rgb value, the total size of each image equals to x * y * 3. With this value (rgbSize) a simple skip on data allows to get rgb values for each image:

i: 0
while [i < nImages] [
if i > 0 [f: skip f rgbSize]
rgb: copy/part f rgbSize
append movie rgb
i: i + 1
]
]

Code sample

Red [
    Title:   "Test camera Red VID "
    Author:  "Francois Jouen"
    File:    %movie.red
    Needs:   View redCV
]

margins: 5x5
fn: %video.rvf
iSize: 640x480
imgSize: 0x0
cSize: 0x0
n: 0
nImages: 0
strSize: 0
f: none
isFile: false
rgbSize: 0
img: make image! reduce [iSize black] 
activeImage: 1
duration: 0.0
fps: 0
compression: 0
freq: none

readImage: func [n [integer!]][
    if isFile [
        f5/text: form n
        img/rgb: movie/:n
    ]
    canvas/image: img
]

readAllImages: does [
    either activeImage < nImages [activeImage: activeImage + 1 readImage activeImage]
                                  [activeImage: 1]
]

loadMovie: func [] [
    ;tmp: request-file/filter ["Red Video Files" "*.rvf"] ; pb with -t macOS
    tmp: request-file
    if not none? tmp [
        f: read/binary tmp
        ; read header
        s: to-string copy/part f 4  ; should be "RCAM"
        f: skip f 4                 ;
        nImages: to-integer copy/part f 4
        
        f: skip f 4
        imgSize/x: to-integer copy/part f 4
        f: skip f 4
        imgSize/y: to-integer copy/part f 4
        
        rgbSize: (imgSize/x * imgSize/y) * 3
        f: skip f 4
        duration: to-float copy/part f 8
        f: skip f 8
        fps: to-integer copy/part f 4
        f: skip f 4
        compression: to-integer copy/part f 4
        either compression = 0 [f6/text: rejoin [ form compression " : Uncompressed video"]] 
                            [f6/text: rejoin [ form compression " : ZLib compressed video"]]
        f1/text: rejoin [form nImages " images"]
        f2/text: form imgSize
        f3/text: rejoin [form round duration " sec"]
        f4/text: rejoin [form fps " FPS"]
        freq: to-time compose [0 0 (1.0 / fps)]
        f: skip f 4
        
        ; read red movie data
        movie: copy []
        i: 0 
        while [i < nImages] [
            if i > 0 [f: skip f rgbSize]
            rgb: copy/part f rgbSize
            append movie rgb
            i: i + 1
        ]
        img: make image! reduce [imgSize rgb]
        isFile: true 
        activeImage: 1
        readImage activeImage 
    ]
]

view win: layout [
    title "Reading red movie"
    origin margins space margins
    button "Load" [loadMovie]
    f1: field 100
    f2: field 100
    f3: field 100
    f4: field 100
    bt: base 20x20 on-time [readAllImages]
    button "Quit" [quit]
    return
    canvas: base iSize img
    return
    button "<<" [activeImage: 1 readImage activeImage]
    button ">>" [activeImage: nImages readImage activeImage]
    button ">"  [if activeImage < nImages [activeImage: activeImage + 1 readImage activeImage]]
    button "<"  [if activeImage > 1 [activeImage: activeImage - 1 readImage activeImage]]
    button "<>" [if isFile [bt/rate: freq]]
    button "||" [bt/rate: none]
    f5: field 60
    f6: field 160
    do [bt/rate: none]
]

Result



The movie reader is complete with a frame by frame access or a complete reading of the video.