samedi 17 novembre 2018

Neural Network with Red language

Thanks to:  
Andrew Blais (onlymice@gnosis.cx), Gnosis Software, Inc. 
David Mertz (mertz@gnosis.cx), Gnosis Software, Inc.
Michael Shook, http://mshook.webfactional.com/talkabout/archive/

For the fun, we'll test Red capacities for generating neural networks. Here we used a simple network with 2 input neurons, 3 hidden neurons and 1 output neuron. Red code is based on Back-Propagation Neural Networks python code by Neil Schemenauer (nas@arctrix.com) and on Karl Lewin’s code for Rebol language.

You’ll find easily on the Internet detailed explanations about neural networks.

Neural Networks

Simply speaking, human brain consists of about billion neurons and a neuron is connected to many other neurons. With this kind of connections, neurons both send and receive varying quantities of signals. One very important feature of neurons is that they don't react immediately to the reception of signals, but they sum signals, and they send their own signals to other neurons only when this sum has reached a threshold. This means that the human brain learns by adjusting the number and strength of connections between neurons.

Threshold logic units (TLUs)

The first step toward understanding neural networks is to abstract from the biological neuron, and to consider artificial neurons as threshold logic units (TLUs). A TLU is an object that inputs an array of weighted values, sums them, and if this sum is equal or superior to some threshold, outputs a signal. This means than TLUs can classify data. Imagine an artificial neuron with two inputs, whose weights equal 1, and threshold equals 1.5. With these weighted inputs [0 0], [0 1], [1,0], and [1,1], the neuron will output 0, 0, 0, and 1 respectively. Hidden neurons used in Red code are TLUs.


Network training 

Since TLUs can classify, neural networks can be built to artificially learn some simple rules as Boolean operators. Learning mechanism is modeled on the brain's adjustments of its neural connections. A TLU learns by changing its weights and threshold. This done by process called training.  The concept is not difficult to understand.  Basically, a set of input values and the desired output for each set of inputs are required. This corresponds to the truth tables associated to Boolean operator we want to be learned such as XOR :
Input 1
Input 2
Output
0
0
0
0
1
1
1
0
1
1
1
0

We set first the weights with random values. Then each set of input values is evaluated and compared to the desired output for that set. We add up each of these differences and get a summed error value for this set of weights. Then we modify the weights and go through each of the input/output sets again to find out the total error for this set of weights. Lastly, we use a backpropagation algorithm to test the network learning. The backpropagation algorithm looks for the minimum value of the error function in weight space using a technique called the delta rule or gradient descent. The weights that minimize the error function is then considered to be a solution to the learning problem.



Different Boolean operators ["XOR" "OR" "NOR" "AND" "NAND"] can be used to test the network. You can also play with the number of iterations you want to train the network. Lastly two algorithms are implemented to compute weights in hidden neurons either exponential or sigmoidal. 

Code

Red [
Title:   "Red Neural Network"
Author:  "Francois Jouen"
File: %neuraln.red
Needs: View
]
{This code is based on Back-Propagation Neural Networks 
by Neil Schemenauer <nas@arctrix.com>
Thanks to  Karl Lewin for the Rebol version}

; default number of input, hidden, and output nodes
nInput: 2
nHidden: 3
nOutput: 1
; activations for nodes
aInput: []
aHidden: []
aOutput: []
; weights matrices
wInput: []
wOutput: []
; matrices for last change in weights for momentum
cInput: []
cOutput: []
learningRate: 0.5; = learning rate
momentumFactor: 0.1; = momentum factor

n: 1280 ; n training sample
netR: copy [] ; learning result
step: 8;

;XOR by default
pattern: [
[[0 0] [0]]
[[1 0] [1]]
[[0 1] [1]]
[[1 1] [0]]
]


;calculate a random number where: a <= rand < b
rand: function [a [number!] b [number!]] [(b - a) * ((random 10000.0) / 10000.0) + a]

; Make matrices
make1DMatrix: function [mSize[integer!] value [number!] return: [block!]][
m: copy []
repeat i mSize [append m value]
m
]
make2DMatrix: function [line [integer!] col [integer!] value [number!] return: [block!]][
m: copy []
repeat i line [
blk: copy []
repeat j col [append blk value]
append/only m blk
]
m
]

tanh: function [x [number!] return: [number!]][ (EXP x - EXP negate x) / (EXP x + EXP negate x)]

;sigmoid function, tanh seems better than the standard 1/(1+e^-x)

sigmoid: function [x [number!] return: [number!]][tanh x]

;derivative of  sigmoid function
dsigmoid: function [y [number!] return: [number!]][1.0 - y * y]

createMatrices: func [] [
aInput: make1DMatrix nInput 1.0
aHidden: make1DMatrix nHidden 1.0
aOutput: make1DMatrix nOutput 1.0
wInput: make2DMatrix nInput nHidden 0.0
wOutput: make2DMatrix nHidden nOutput 0.0
cInput: make2DMatrix nInput nHidden 0.0
cOutput: make2DMatrix nHidden nOutput 0.0
randomizeMatrix wInput -2.0 2.0
randomizeMatrix wOutput -2.0 2.0
]

randomizeMatrix: function [mat [block!] v1 [number!] v2 [number!]][
foreach elt mat [loop length? elt [elt: change/part elt rand v1 v2 1]]
]

computeMatrices: func [inputs [block!] return: [block!]] [
; input activations
repeat i (nInput - 1) [poke aInput i to float! inputs/:i]
; hidden activations
repeat j nHidden [
sum: 0.0
repeat i nInput [sum: sum + (aInput/:i * wInput/:i/:j)]
either cb/data [poke aHidden j sigmoid sum] 
[poke aHidden j 1 / (1 + EXP negate sum)]
]
; output activations
repeat j nOutput [
sum: 0.0
repeat i nHidden [
sum: sum + (aHidden/:i * wOutput/:i/:j)]
either cb/data [poke aOutput j sigmoid sum]
[poke aOutput j 1 / (1 + EXP negate sum)]
]
aOutput
]
backPropagation: func [targets [block!] N [number!] M [number!] return: [number!]] [
; calculate error terms for output
oDeltas: make1DMatrix  nOutput 0.0
sum: 0.0
repeat k nOutput [
either cb/data [
sum: targets/:k - aOutput/:k poke oDeltas k (dsigmoid aOutput/:k) * sum]
[ao: aOutput/:k
poke oDeltas k ao * (1 - ao) * (targets/:k - ao)]
]
; calculate error terms for hidden
hDeltas: make1DMatrix  nHidden 0.0
repeat j nHidden [
sum: 0.0
repeat k nOutput [sum: sum + (oDeltas/:k * wOutput/:j/:k)]
either cb/data [poke hDeltas j (dsigmoid aHidden/:j) * sum]
[poke hDeltas j (aHidden/:j * (1 - aHidden/:j) * sum)]
]
; update output weights
repeat j nHidden [
repeat k nOutput [
chnge: oDeltas/:k * aHidden/:j
poke wOutput/:j k (wOutput/:j/:k + (N * chnge) + (M * cOutput/:j/:k))
poke cOutput/:j k chnge
]
]
; update hidden weights
repeat i nInput [
repeat j nHidden [
chnge: hDeltas/:j * aInput/:i
poke wInput/:i j (wInput/:i/:j + (N * chnge) + (M * cInput/:i/:j))
poke cInput/:i j chnge
]
]
; calculate error
error: 0
repeat k nOutput [error: error + (learningRate * ((targets/:k - aOutput/:k) ** 2))]
error
]
trainNetwork: func [patterns[block!] iterations [number!] return: [block!]] [
blk: copy []
count: 0
x: 10
plot: compose [line-width 1 pen red line 0x230 660x230 pen green]
repeat i iterations [
;sbcount/text: form i
error: 0
foreach p patterns [
r: computeMatrices p/1 
error: error + backPropagation p/2 learningRate momentumFactor
sberr/text: form round/to error 0.001
if system/platform = 'Windows [do-events/no-wait];' win users
do-events/no-wait
append blk error
count: count + 1
]
;visualization
if (mod count step) = 0 [
y: 230 - (error * 320)
if x = 10 [append append plot 'line (as-pair x y)];'
append plot (as-pair x y)
x: x + 1
]
visu/draw: plot
do-events/no-wait
]
sb/text: copy "Neural Network rendered in: "
blk
]
testLearning: func [patterns [block!]] [ 
result2/text: copy ""
foreach p patterns [
r: computeMatrices(p/1) 
append result2/text form to integer! round/half-ceiling first r 
append result2/text newline
]


changePattern: func [v1 v2 v3 v4][
change second first pattern  v1 
change second second pattern v2 
change second third pattern  v3 
change second fourth pattern v4
result2/text: copy ""
result1/text: copy ""
append append result1/text form second first pattern newline
append append result1/text form second second pattern newline
append append result1/text form second third pattern newline
append append result1/text form second fourth pattern newline
]


makeNetwork: func [ni [integer!] nh [integer!] no [integer!] lr [float!] mf [float!]] [
random/seed now/time/precise
nInput: ni + 1
nHidden: nh
nOutput: no
learningRate: lr
momentumFactor: mf
createMatrices
s: copy "Neural Network created: "
append s form ni
append s form " input neurons "
append s form nh
append s form " hidden neurons "
append s form no
append s form " output neuron(s) "
sb/text: s
result2/text: copy ""
sberr/text: copy ""
]


makeTraining: does [
t1: now/time/precise
netR: trainNetwork pattern n ; network training
t2: now/time/precise
testLearning pattern ; test output values after training
append sb/text form t2 - t1
]

view win: layout [
title "Back-Propagation Neural Network"
text  "Pattern" 
dpt: drop-down 70 
data ["XOR" "OR" "NOR" "AND" "NAND"]
select 1
on-change [
switch face/text [
"XOR" [changePattern 0 1 1 0]
"AND" [changePattern 0 0 0 1] 
"OR"  [changePattern 0 1 1 1]
"NOR" [changePattern 1 0 0 0]
"NAND"[changePattern 1 1 1 0]
]
isCreated: false]
text "Sample"
dp2: drop-down 70 
data ["640" "1280" "1920" "2560"]
select 2
on-change [n: to integer! face/text step: (n / 640) * 4 ]
cb: check "Sigmoid" []
button "Run Network" [makeNetwork 2 3 1 0.5 0.1 makeTraining ]  
text 40 "Error"
sberr: field 60
pad 10x0
button "Quit" [quit]
return
visu: base 660x240 black
result1: area 35x80
result2: area 35x80
return
sb: field 660
do [changePattern 0 1 1 0]
]






dimanche 9 septembre 2018

Shape Contour with redCV

Another way to analyse shapes in image is to calculate the signature of the shape. Basically, the centroid of the shape given by the rcvGetCentroid is the origin of polar coordinates system. Consequently all pixels that belong to the contour of the shape can be described by the distance rho to the origin according to the angle theta.



RedCV proposes 2 fonctions for that:
rcvGetEuclidianDistance, which get the rho distance of the pixel (as-pair) to the centroid of the shape.
rcvGetAngle which calculate the theta angle (0..359 °) of the pixel.
© Bruno Keymolen 

As demonstrated here, redCV makes the job correctly. When looking for the signature of a circle, we get a line since all pixels are equidistant from the centroid.



For a square, it is also correct and the four angles of the square are clearly identifiable.


Code sample

Red [
    Title:   "Matrix tests "
    Author:  "Francois Jouen"
    File:    %freeman2.red
    Needs:   'View
]


#include %../../libs/redcv.red ; for redCV functions

iSize: 512x512
mat:  rcvCreateMat 'integer! 32 iSize
bMat: rcvCreateMat 'integer! 32 iSize
img: rcvCreateImage iSize
plot:  copy [fill-pen white box 155x155 355x355]
_plot: copy [line-width 1 pen green 
            text 175x480 "Angle"
            line 5x10 5x470 5x470 375x470 375x5 5x10 
            line 190x10 190x470
            text 10x450 "0" text 178x450 "180" text 345x450 "360" 
            line]
plot2: copy _plot
fgVal: 1
canvas: none


processImage: does [
    img: to-image canvas
    rcvImage2Mat img mat     
    rcvMakeBinaryMat mat bmat
    cg: rcvGetMatCentroid bmat img/size     ; get shape centroid
    border: []
    rcvMatGetBorder bmat iSize fgVal border ; get border
    angles: copy []
    foreach p border [
        ; use x y coordinates and calculate rho and theta
        rho: rcvGetEuclidianDistance p cg
        theta: rcvGetAngle p cg
        bloc: copy []
        append bloc theta
        append bloc rho
        append/only angles bloc 
    ]
    sort angles ; 0.. 359  to use with line draw command
    foreach n angles [
        p: as-pair first n 384 - second n 
        p: p + 10x0
        append plot2 (p)
    ]
    canvas2/draw: reduce [plot2]
]



; ***************** Test Program ****************************
view win: layout [
    title "Contour Signature"
    
    r1: radio "Square" [canvas/image: none 
                        canvas2/image: none
                        plot: compose [fill-pen white box 155x155 355x355]
                        plot2: copy _plot
                        canvas/draw: reduce [plot]
                        canvas2/draw: reduce [plot2]
                        ]
    r2: radio "Circle" [canvas/image: none 
                        canvas2/image: none
                        plot: compose [fill-pen white circle 255x255 120] 
                        plot2: copy _plot
                        canvas/draw: reduce [plot]
                        canvas2/draw: reduce [plot2]
                        ]
    r3: radio "Triangle" [canvas/image: none 
                        canvas2/image: none
                        plot: compose [pen white fill-pen white triangle 256x128 128x300 384x400] 
                        plot2: copy _plot
                        canvas/draw: reduce [plot]
                        canvas2/draw: reduce [plot2]
                        ]
    r4: radio "Polygon" [canvas/image: none 
                        canvas2/image: none
                        plot: compose [pen white fill-pen white polygon 256x100 384x300 128x400 128x300 256x10] 
                        plot2: copy _plot
                        canvas/draw: reduce [plot]
                        canvas2/draw: reduce [plot2]
                        ]
    button "Process" [processImage]
    pad 395x0
    button "Quit" [ rcvReleaseImage img
                    rcvReleaseMat mat
                    rcvReleaseMat bmat
                    Quit]
    return
    canvas: base 512x512 black draw plot
    canvas2: base 380x512 black draw plot2
    do  [r1/data: true]
]


Freeman Code Chain with redCV 2

As demonstrated in the previous article, redCV Freeman code chain works perfectly for regular shapes such as square, triangle, or circle. However many shapes in image are often irregular such as illustrated here.

Using Canny Detector and Morphological Operators

The process is two-fold. First, we use a Canny convolution filter to find the edges of the shape with rcvConvolve function. Since Canny detector is rather noise-sensitive, in some cases, edges detection is incomplete and the result of the function is a discret contour with some values equal to 0. In this case, rcvMatGetChainCode function returns an error (-1). The second process is to use a morphological operator like rcvDilate which slightly dilates the contour. With this simple idea, rcvMatGetChainCode, always returns the correct direction for the next  pixel whatever the complexity of the shape.

Code Sample


Red [
    Title:   "Freeman tests "
    Author:  "Francois Jouen"
    File:    %freemanirregular.red
    Needs:   'View
]


#include %../../libs/redcv.red ; for redCV functions
iSize: 512x512
rSize: 300x300
img: rcvCreateImage iSize
edges: rcvCreateImage iSize
edges2: rcvCreateImage iSize
mat:  rcvCreateMat 'integer! 32 iSize
bMat: rcvCreateMat 'integer! 32 iSize
visited: rcvCreateMat 'integer! 32 iSize
plot: copy []
fgVal: 1
canvas: none
knlSize: 3x3
knl: rcvCreateStructuringElement/rectangle knlSize
factor: 1.0
delta: 0.0
anim: false
canny: [-1.0 -1.0 -1.0
        -1.0 8.0 -1.0 
        -1.0 -1.0 -1.0]
        
generatePolygon: does [
    canvas/image: none
    clear f0/text
    clear f1/text
    clear f2/text
    clear f3/text
    clear f4/text
    clear r/text
    p1: 128x128 + random rSize p2: 128x128 + random rSize  p3: 128x128 + random rSize 
    p4: 128x128 + random rSize  128x128 +  p5: 128x128 + random rSize
    plot: compose [pen white fill-pen white polygon (p1) (p2) (p3) (p4) (p5)]
    canvas/draw: reduce [plot]
    pgb/data: 0%
]

processImage: does [
    img: to-image canvas
    rcvConvolve img edges canny factor delta    ; edges detection with Canny
    rcvDilate edges edges2 knlSize knl          ; dilates shape to suppress 0 values if exist
    rcvImage2Mat edges2 mat                     ; make first matrix 0..255
    rcvMakeBinaryMat mat bmat                   ; make second matrix 0..1
    lPix: rcvMatleftPixel bmat iSize fgVal
    rPix: rcvMatRightPixel bmat iSize fgVal
    uPix: rcvMatUpPixel bmat iSize fgVal
    dPix: rcvMatDownPixel bmat iSize fgVal
    f1/text: form as-pair lPix/x uPix/y
    f2/text: form as-pair rPix/x uPix/y
    f3/text: form as-pair rPix/x dPix/y 
    f4/text: form as-pair lPix/x dPix/y 
    visited: rcvCreateMat 'integer! 32 iSize            ; for storing visited pixels    
    border: []                                          ; for neighbors
    rcvMatGetBorder bmat iSize fgVal border             ; get border
    foreach p border [rcvSetInt2D visited iSize p 1]    ; values in matrix
    perim: (length? border) / 2                         ; pre-processing multiplies number of pixels
    f0/text: form perim
    p: uPix;first border
    i: 1
    s: copy ""
    clear r/text
    append append plot 'pen 'green
    pix: 1
    ; repeat until all pixels are processed
    while [pix > 0] [
        pix: rcvGetInt2D visited iSize p
        d: rcvMatGetChainCode visited iSize p 1     ; get chain code
        rcvSetInt2D visited iSize p 0               ; pixel processed 
        append append append plot 'circle (p) 2 
        if d > -1 [append s form d]
        if anim [do-events/no-wait]; to show progression
        switch d [
            0   [p/x: p/x + 1]              ; east
            1   [p/x: p/x + 1 p/y: p/y + 1] ; southeast
            2   [p/y: p/y + 1]              ; south
            3   [p/x: p/x - 1 p/y: p/y + 1] ; southwest
            4   [p/x: p/x - 1]              ; west
            5   [p/x: p/x - 1 p/y: p/y - 1] ; northwest
            6   [p/y: p/y - 1]              ; north
            7   [p/x: p/x + 1 p/y: p/y - 1] ; northeast
        ]
        pgb/data: to-percent (i / to-float perim)
        i: i + 1
    ]
    r/text: s
]

; ***************** Test Program ****************************
view win: layout [
    title "Chain Code with Canny Detector"
    button "Generate Polygon" [generatePolygon]
    cb: check "Show Anination" [anim: face/data]
    button "Process" [processImage]
    pgb: progress 160
    f0: field 125
    button "Quit" [
                    rcvReleaseImage img
                    rcvReleaseImage edges
                    rcvReleaseImage edges2
                    rcvReleaseMat mat
                    rcvReleaseMat bmat
                    rcvReleaseMat visited
                    Quit]
    return
    canvas: base iSize black draw plot
    r: area 200x512
    return
    pad 120x0
    f1: field 60
    f2: field 60
    f3: field 60
    f4: field 60
    return  
]


Result





Freeman Code Chain with redCV 1

Freeman code chain 

A chain code is a way to represent shapes in a binary image. The basic idea is very simple: One spot on the outer boundary of a shape is selected as the starting point, we then move along the boundary of the shape and describe each movement with a directional code that describes the movement. We continue tracing the boundary of the shape until we return to the starting point.
The Freeman code encodes the movement as an integer number between 0 and 7.
0: east
1: southeast
2: south
3: southwest
4: west
5: northwest
6: north
7: northeast


The simple idea is to look for the neighbors of the current pixel and get the direction according to the neighbor value.

Freeman Code Chain in redCV


We do implement basic Freeman code chain in redCV which requires some image preprocessing.
First of all we need a binary image. This is easy done with 2 redCV functions:
rcvImage2Mat which transforms any red image to a binary matrix [0..255] and 
rcvMakeBinaryMat which makes a binary [0..1] matrix.
Then we have to get all points that belong to the shape. Just call rcvMatGetBorder function that looks for all value (0 or 1) that are part of shape boundary. Found pixels in binary matrix are stored in a block such as border. When found you have to copy pixel in another matrix which will be used then to store visited pixel (foreach p border [rcvSetInt2D visited iSize p 1]).

Then redCV uses the rcvMatGetChainCode to get the successive movement direction such as in the code sample:

p: first border
i: 1
while [i < perim] [
    d: rcvMatGetChainCode visited iSize p fgVal
    idx: (p/y * iSize/x + p/x) + 1  
    visited/:idx: 0; pixel is visited
    append s form d
    switch d [
        0   [p/x: p/x + 1]              ; east
        1   [p/x: p/x + 1 p/y: p/y + 1] ; southeast
        2   [p/y: p/y + 1]              ; south
        3   [p/x: p/x - 1 p/y: p/y + 1] ; southwest
        4   [p/x: p/x - 1]              ; west
        5   [p/x: p/x - 1 p/y: p/y - 1] ; northwest
        6   [p/y: p/y - 1]              ; north
        7   [p/x: p/x + 1 p/y: p/y - 1] ; northeast
    ]
    i: i + 1
]



It is very important to ensure that the current pixel was processed; if not the pixel can be again processed and the chain code could be invalid. This is why we set to 0 visited  pixel in shape matrix (visited/:idx: 0; pixel is visited).

Then according to the direction value returned by the  rcvMatGetChainCode function we move to the new pixel to be analyzed.

Code Sample

Red [
    Title:   "Matrix tests "
    Author:  "Francois Jouen"
    File:    %freeman.red
    Needs:   'View
]


#include %../../libs/redcv.red ; for redCV functions

plot: []
iSize: 512x512
img: rcvCreateImage iSize
mat:  rcvCreateMat 'integer! 32 iSize
bMat: rcvCreateMat 'integer! 32 iSize
visited: rcvCreateMat 'integer! 32 iSize
fgVal: 1
canvas: none


generateImage: does [
    canvas/image: none
    p1: random 400x400
    p2: random 400x400
    color: 255.255.255
    plot: compose [fill-pen (color) box (p1) (p2)]
    processImage
]


processImage: does [
    canvas/draw: reduce [plot]
    img: to-image canvas
    rcvImage2Mat img mat     
    rcvMakeBinaryMat mat bmat
    lPix: rcvMatleftPixel bmat iSize fgVal
    rPix: rcvMatRightPixel bmat iSize fgVal
    uPix: rcvMatUpPixel bmat iSize fgVal
    dPix: rcvMatDownPixel bmat iSize fgVal
    f1/text: form as-pair lPix/x uPix/y 
    f2/text: form as-pair rPix/x uPix/y 
    f3/text: form as-pair rPix/x dPix/y 
    f4/text: form as-pair lPix/x dPix/y
    clear r/text
    visited: rcvCreateMat 'integer! 32 iSize
    border: copy []
    rcvMatGetBorder bmat iSize fgVal border
    foreach p border [rcvSetInt2D visited iSize p 1]
    perim: length? border
    p: first border
    i: 1
    s: copy ""
    while [i < perim] [
        d: rcvMatGetChainCode visited iSize p fgVal
        idx: (p/y * iSize/x + p/x) + 1  
        visited/:idx: 0; pixel is visited
        append s form d
        switch d [
            0   [p/x: p/x + 1]              ; east
            1   [p/x: p/x + 1 p/y: p/y + 1] ; southeast
            2   [p/y: p/y + 1]              ; south
            3   [p/x: p/x - 1 p/y: p/y + 1] ; southwest
            4   [p/x: p/x - 1]              ; west
            5   [p/x: p/x - 1 p/y: p/y - 1] ; northwest
            6   [p/y: p/y - 1]              ; north
            7   [p/x: p/x + 1 p/y: p/y - 1] ; northeast
        ]
        i: i + 1
    ]
    r/text: s
]



; ***************** Test Program ****************************
view win: layout [
    title "Chain Code"
    button "Generate Shape"     [generateImage]
    pad 580x0
    button "Quit"               [rcvReleaseImage img
                                 rcvReleaseMat mat
                                 rcvReleaseMat bmat
                                 rcvReleaseMat visited
                                 Quit]
    return
    canvas: base iSize black draw plot
    r: area 256x512
    return
    pad 100x0
    f1: field 60
    f2: field 60
    f3: field 60
    f4: field 60
]

Result







samedi 3 février 2018

Read movies with Red

In the previous post, we explained how Red can write compressed webcam images to a file. Of course, Red is also able to read back stored images in file. This is possible with 2 functions: readHeader and readImage. 

readHeader Function

As explain in a previous post, Red Video Files (*.rvf) include a 36-byte header which contains all information we need to read video cam. Video file is used as parameter for this first function which reads as binary the first 36 bytes of the file and gets all the information about the movie such as the number of images, the image size or fps and so on.


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
4
DATA


readHeader: func [file [file!]][
    f: read/binary/part file headerSize         ; 36 bytes for the header
    s: to-string copy/part f 4                  ; should be "RCAM"          
    nImages: to-integer copy/part skip f 4 4    ; number of images in movie
    imgSize/x: to-integer copy/part skip f 8 4  ; image X size  
    imgSize/y: to-integer copy/part skip f 12 4 ; image Y size
    duration: to-float copy/part skip f 16 8    ; movie duration in sec
    fps: to-integer copy/part skip f 24 4       ; frames/sec
    zComp: to-integer copy/part skip f 28 4     ; compressed or uncompressed data
    s: to-string copy/part skip f 32 4          ; should be "DATA"
]

After reading the file header, the second operation (see loadMovie function in code sample) is to get the offset of each image included in the movie. We use a simple block! datatype  (movie) to store the list of images offset as explained:

movie: copy []
i: 0 
; makes image offset index
nextIndex: headerSize ; 36 bytes
while [i < nImages] [
    index: nextindex
    rgbSize: to-integer copy/part f 4
    nextindex: index + rgbSize + 8
    f: skip f rgbSize + 8
    append movie index
    i: i + 1
]   

readImage Function

Then it is really simple to write a function which takes the image number as parameter and gets image data associated to the image offset value. Since images are compressed or uncompressed we need to get both sizes (rgbSize and urgbSize). Each image  is organized as follows:


rgbSize
urgbSize
rgb
Compressed Image Size
Non-compressed Image Size
binary rgb values
4 bytes
4 bytes
rgbSize bytes

readImage: func [n [integer!]][
    idx: movie/:n                                   ; get image offset
    rgbSize: to-integer copy/part skip f idx 4      ; get compressed size
    urgbSize: to-integer copy/part skip f idx + 4 4 ; get uncompressed size
    rgb: copy/part skip f idx + 8 rgbSize           ; get binary values 
    ;decompress if necessary
    either zComp = 0 [img/rgb: rgb] [img/rgb: rcvDecompressRGB rgb urgbSize]
    canvas/image: img                               ; update image container
]

Code sample


Red [
    Title:   "Test camera Red VID "
    Author:  "Francois Jouen"
    File:    %movie.red
    Needs:   View redCV
]
#include %../../libs/redcv.red ; for redCV functions
margins: 5x5
iSize: 640x480
imgSize: 0x0
nImages: rgbSize: urgbSize: 0
img: rcvCreateImage iSize
currentImg: 1
duration: 0.0
fps: 0
freq: to-time compose [0 0 0.0]
zComp: 0
headerSize: 36
f: none
isFile: false

readImage: func [n [integer!]][
    if isFile[
        f5/text: form n
        idx: movie/:n                                       ; get image offset
        rgbSize: to-integer copy/part skip f idx 4          ; get compressed size
        urgbSize: to-integer copy/part skip f idx + 4 4     ; get uncompressed size
        rgb: copy/part skip f idx + 8 rgbSize               ; get binary values 
        ;decompress if necessary
        either zComp = 0 [img/rgb: rgb] [img/rgb: rcvDecompressRGB rgb urgbSize]
        canvas/image: img                                   ; update image container
    ]
]

updateSlider: does [sl/data: to-percent (currentImg / to-float nImages)]

readAllImages: does [
    either currentImg < nImages [currentImg: currentImg + 1 readImage  currentImg]
                                [currentImg: 0]
    updateSlider
]

readHeader: func [file [file!]][
    f: read/binary/part file headerSize             ; 36 bytes for the header
    s: to-string copy/part f 4                      ; should be "RCAM"          
    nImages: to-integer copy/part skip f 4 4        ; number of images in movie
    imgSize/x: to-integer copy/part skip f 8 4      ; image X size  
    imgSize/y: to-integer copy/part skip f 12 4     ; image Y size
    duration: to-float copy/part skip f 16 8        ; movie duration in sec
    fps: to-integer copy/part skip f 24 4           ; frames/sec
    zComp: to-integer copy/part skip f 28 4         ; compressed or uncompressed data
    s: to-string copy/part skip f 32 4              ; should be "DATA"
    ; update fields and variables
    either zComp = 0 [f6/text: rejoin [ form zComp " : Uncompressed video"]] 
                     [f6/text: rejoin [ form zComp " : ZLib compressed video"]]
    f1/text: rejoin [form nImages " frames"]
    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)]
]

loadMovie: func [] [
    tmp: request-file/filter ["Red Video Files" "*.rvf"]
    if not none? tmp [
        readHeader tmp                      ; read movie header
        f: read/binary/seek tmp headerSize  ; go to first image
        movie: copy []
        i: 0 
        ; makes image offset index
        nextIndex: headerSize ; 36 bytes
        while [i < nImages] [
            index: nextindex
            rgbSize: to-integer copy/part f 4
            nextindex: index + rgbSize + 8
            f: skip f rgbSize + 8
            append movie index
            i: i + 1
        ]   
        isFile: true 
        sl/data: 0%
        f: read/binary tmp          ; head of file 
        img: rcvCreateImage imgSize ; we need a red image! for displaying video
        currentImg: 1
        readImage currentImg 
        win/text: copy form tmp
    ]
]

view win: layout [
    title "Reading red movie"
    origin margins space margins
    button "Load" [loadMovie]
    f1: field 150
    f2: field 100
    f3: field 100
    f4: field 100
    pad 40x0
    button "Quit" [quit]
    return
    canvas: base iSize img
    return
    sl: slider 615 [
                n: nImages - 1
                currentImg: to-integer (sl/data * n) + 1 
                readImage  currentImg]
    bt: base 20x20 black on-time [readAllImages]
    return
    button "<<" [currentImg: 1 readImage currentImg sl/data: 0%]
    button ">>" [currentImg: nImages readImage currentImg updateSlider]
    button "<"  [if currentImg > 1 [currentImg: currentImg - 1 readImage currentImg]
                updateSlider]
    button ">"  [if currentImg < nImages [currentImg: currentImg + 1 readImage currentImg]
                updateSlider]
    onoff: button "Start/Stop" on-click [
        if isFile [
            either bt/rate = none [bt/color: green bt/rate: freq] 
            [bt/color: black bt/rate: none]
        ]
    ]
    f5: field 75
    f6: field 190
    do [bt/rate: none]
]

Red Interface for reading video