mercredi 1 janvier 2025

Savitsky-Golay Filter

In 1964, A. Savitsky and M.J.E. Golay published an article in Analytical Chemistry describing a simple and effective smoothing technique: “Smoothing and Differentiation of Data by Simplified Least Squares Procedures”.

Their method makes it possible to smooth or derive a time series, with equidistant abscissa values, by a simple convolution with a series of coefficients corresponding to the degree of the chosen polynomial interpolation and to the desired operation: simple smoothing or derivation up to 5th order.

The convolution is performed by n multiplications, followed by the sum of the products and completed by dividing by the corresponding norm. The coefficients and norms are provided in the article. Savitzky and Golay's article is accompanied by 11 tables of coefficients suitable for smoothing or determining of the first 5 derivatives; convolutions are performed for different degrees of polynomials and over ranges from 5 to 25 points. The tables published by Savitzky and Golay contain different typo errors. They were corrected by J. Steiner, Y. Termonia and J. Deltour in 1972.

I really like this filter, as it preserves signal dynamics and effectively filters out background noise. We've used this technique a lot in recent years at R2P2 (https://uniter2p2.fr) to process  videos (of babies) who were shaking. This prevented our neural networks from correctly identifying the baby's body joints.  With this type of filter, everything is back to normal. The video images did not shake and the detection algorithms became perfect (see Taleb, A.,  Rambaud, P., Diop, S., Fauches, R., Tomasik, J., Jouen, F., Bergounioux, J.  "Spinal Muscular Amyotrophy detection using computer vision and artificial intelligence." in JAMA Pediatrics, Published online March 4, 2024.).

The main advantage of this process is that it's rather easy to program, allowing direct access to derivative values. On the other hand, abscissa values must be equidistant, and extreme points are ignored.  

You can find the filter code for Red: 

 (https://github.com/ldci/redCV/blob/master/samples/signal_processing/sgFilter.red

And for Rebol 3 here:

https://github.com/ldci/R3_tests/blob/main/signalProcessing/sgFilter.r3



Here's the SGFiltering function in Rebol 3. 


With Red, it's similar, but I used a Red/System routine to speed up the calculations. I'm not sure that rcvSGFiltering routine is any faster than code written in Red, as Red has come a long way since I wrote this routine.
You can find here:
https://github.com/ldci/Red_KIS/blob/main/Signal_Processing/sgFilter2.red a 100% Red pure version without Routines and Red/System.

These examples work with time series, but can easily be adapted to image processing. After all, an image is just a long vector of RGB values!

References:
A. Savitzky, M.J.E. Golay, ANAL. CHEM., 36,1627 (1964)
J. Steiner, Y. Termonia,  J. Deltour, ANAL. CHEM., 44,1909 (1972)



lundi 30 décembre 2024

Global movements in infants

At the R2P2 unit at Garches hospital (https://uniter2p2.fr/), we are heavily involved in developing systems for analysing spontaneous movements in babies.

In a recent article (A. Taleb, P. Rambaud, S. Diop, R. Fauches, J. Tomasik, F. Jouen, J. Bergounioux. “Spinal Muscular Amyotrophy detection using computer vision and artificial intelligence.” in JAMA Pediatrics, 2024), we developed a video-based AI system. It's a semi-supervised system. But as is often the case, these neural networks don't clearly explain how they make their classification decisions. So we added a SHAP module (https://shap.readthedocs.io/en/latest/) that explained how the network made its decision to distinguish normal motor skills from those of babies with spinal muscular atrophy.  The result is brilliant. This neural network behaves like a human expert: it tells us that it was the absence of lower-limb movements in SMA babies that was its decision-making factor. That's great, because it's clinically consistent! 

Thank you to all the students who took part in this wonderful project.


 

dimanche 15 décembre 2024

Signal processing with Red and Rebol

In many of the data we collect in hospital, we are dealing with time series, some of which show an unexpected variation as a function of time. For example, in our work on the perception of babies' cries by adults, we observed that most of the signals showed a linear temperature drift over the course of the experiment. This is probably linked to the electronics of our camera. For these reasons, I've developed a few simple algorithms in Red and Rebol 3 that solve some of these problems. I mainly use datatype vector!, which is very efficient for numerical calculations with Red or Rebol 3.

One of the first ways is to remove the DC constant from the signal. Simply remove the mean value of the signal for each value of the signal. Rebol and Red have a function (average) that calculates the average of a vector. 

detrendSignal: func [v [vector!]

"Remove continuous component in signal"
][
        ;--basic (x - mean)
        _v: copy v
        _average: average _v    
---average is a native function in Red and Rebol 3
        repeat i _v/length [_v/:i: _v/:i - _average]
        _v
]

Now let's move on to signal normalization. Normalization is basically bringing signals to the same range or a predefined range. A typical example of a predefined range is the statistical approach of the normalization, which is transforming the signal so that its mean is 0 and standard deviation is 1. This is very useful when you want to compare signals with different amplitudes. Simply calculate the standard deviation of the distribution before normalizing the signal.

stddev: func [v [vector!]
"Standard deviation"
][
sigma: 0.0
foreach value v [sigma: sigma + (power (value - average v) 2)]
sqrt sigma / ((v/length) - 1)
]

normalizeSignal: func [v [vector!]
"Z-score algorithm"
][
;--use z-Score algorithm (x - mean / standard deviation)
_v: copy v
_average: average _v;   ;---average is a native function in Red and Rebol 3
_std: stddev _v             ;--get standart deviation
repeat i _v/length [_v/:i: _v/:i - _average / _std]
_v
]

Another way of normalizing data is to use the minimum and maximum values contained in each data series. With this algorithm, the values of each series are in a space [0.0 .. 1.0]. 

minMaxNormalization: func [v [vector!]
"Min Max normalization"
][
;-- use  min-max algorithm (x: x - min / xmax - xmin)
_v: copy v
xmin: _v/minimum xmax: _v/maximum
repeat i _v/length [_v/:i: (_v/:i - xmin) / (xmax - xmin)]
_v
]

But these techniques aren't always effective, because they don't detect the anomalies (outliers) contained in the signal. For this reason, I often use an algorithm based on the median of the distribution.  This algorithm is more robust and minimizes the effects of anomalies. Of course, we need to calculate the median and interquartile range of our signal.

median: func [
"Return the sample median"
sample [vector!]
][
data: sort to block! copy sample
n: length? data
case [
odd?  n [pick data n + 1 / 2]
even? n [(pick data n / 2) + (pick data n / 2 + 1) / 2]
]
]

interquartileRange: func [
"Return the sample Interquartile Range"
sample [vector!]
][
    data: sort to-block copy sample
    n: length? data
    Q1: 0.25 * n ;--(1 / 4)
    Q2: 0.50 * n ;--(2 / 4)
    Q3: 0.75 * n ;--(3 / 4)
    Q4: 1.00 * n ;--(4 / 4)
    Q3 - Q1 ;--IQR
]


medianFilter: func [v [vector!]
"Median filter"
][
;--use median filter (x: x - med / IRQ)
_v: copy v
med: median _v
IQR: interquartileRange _v
repeat i _v/length [_v/:i: (_v/:i - med) / IQR]
_v
]

A sample  





lundi 2 décembre 2024

The Virgina Project

The Virginia project (https://uniter2p2.fr/en/projects/) focuses on studying the thermoregulation of newborns from thermal images. 

The primary goal of this project is to detect any deterioration in the infant’s health as early as possible using their thermal profile. Over 1,000 images of newborns were captured after birth at four different time points, corresponding to Apgar assessments at 1, 3, 5, and 10 minutes after birth. 

The ultimate objective is to analyze the thermal evolution of these infants at these four key moments.

Infrared images were acquired with a FLIR T650sc camera. The T650sc camera is equipped with an uncooled Vanadium Oxide (VOx) microbolometer detector that produces thermal images of 640 x 480 pixels, with an accuracy of +/- 1 °C.

The Virginia software was developed entirely within the R2P2 laboratory (by ldci) using Red programming language (https://www.red-lang.org),  and the redCV library for image processing (https://github.com/ldci/redCV). The Virginia software includes add-on modules for decoding images.


THE FLIR MODULE


This module has been tested with different FLIR cameras. Its main function is to decode the metadata contained in any radiometric file and to extract the visible image (RGB), the infrared image (IR), the color palette associated with the IR image as well as the temperatures (in degrees °C) associated to each pixel.


This module uses two external programs :


ExifTool (
https://exiftool.org), written and maintained by Phil Harvey, is a fabulous program written in Perl that allows you to read and write the metadata of many computer files. ExifTool supports FLIR files. It works on MacOs, Linux and Windows platforms.

 

ImageMagick (https://imagemagick.org/index.php) is a free software, including a library, as well as a set of command line utilities, allowing to create, convert, modify, and display images in a very large number of formats. The FLIR module mainly uses the convert utility for MacOs and Linux versions and the magick utility for Windows.

Once the metadata are extracted, we call a Python library: PixelLib


THE PIXELLIB LIBRARY

 

This superb library written and maintained by Ayoola Olafenwa is used for the semantic segmentation which allows to identify the newborn in the image. We use the latest version of PixelLib (https://github.com/ayoolaolafenwa/PixelLib) which supports PyTorch and is more efficient for segmentation. The PyTorch version of PixelLib uses the PointRend object segmentation architecture by Alexander Kirillov et al.  2019 to replace the Mask R-CNN. PointRend is an excellent neural network for implementing object segmentation. It generates accurate segmentation masks and runs at a high speed that meets the growing demand for real-time computer vision applications.

First, we only look for the class person without looking for other objects in the RGB image. Then, we get the detected mask as a matrix of true or false values. It is then very simple to reconstruct the binary image of the mask by replacing the true values by the white color. With a simple AND logic operator between the FLIR image and the segmentation mask image, we obtain a new image that keeps only the thermal image of the baby. Only the pixel values higher than 0.0.0 (black) will be considered. Here, for example, the values of the baby's crotch will not be included for the various calculations.



After this first operation of body segmentation, we use a double algorithm. The next step is to detect the contours of the body. This operation will detect the contours in the mask as a polygon of vertices connected by a B-Spline curve. The contour detection algorithm uses several techniques.  First, two morphological operators of dilation and erosion are successively applied to smooth the contours of the mask calculated by the semantic segmentation. Then we use the Freeman coding chain technique (FCC). This technique allows the coding with a limited number of bits (8) of the local direction of a contour element defined in the image. This allows the constitution of a chain of codes from an initial pixel, considering that a contour element links two related pixels. 

When the result of the edge detection is adequate we can proceed to the calculation of the body temperatures. We use a ray-tracing algorithm that makes sure that each pixel of the image belongs to the polygon representing the baby's body. This operation allows us to extract from the 2-D temperature matrix only the body temperatures in the form of a vector which is then used for the different calculations. 


The code is not open-source, as we are in the process of registering patents on certain technological innovations.  As soon as this is possible, I will give free access to all sources. The idea was just to show that you can do great things with Red. 



 Freeman H. On the encoding of arbitrary geometric configurations. IRE Transactions on Electronics Computers. 1961. 10:260–268



Panoptic Segmentation. Alexander Kirillov, Kaiming He, Ross Girshick, Carsten Rother, Piotr Dollar; Proceedings of the IEEE/CVF Conference on Computer Vision and Pattern Recognition (CVPR), 2019, pp. 9404-9413


samedi 30 novembre 2024

Using json files with Red and Rebol

Json is an open standard file format and data interchange format that uses human-readable text to store and transmit data objects made of name–value pairs and arrays. Json and Red or Rebol are very similar in the way they represent data. I sincerely believe that the development of Json benefited from all the work done by Carl Sassenrath when he developed Rebol 2.

Between 2020 and 2023, I developed a major program at the Raymond Poincaré hospital (https://uniter2p2.fr/) with Red, which used a thermal camera to measure the body temperature of newborn babies. This was a bit tricky, as we had to extract the baby's body coordinates from the thermal image in order to measure body temperature. To do this, I used semantic segmentation algorithms such as those proposed by Ayoola Olafenwa with her PixelLib library (https://pixellib.readthedocs.io/en/latest/).

In this program, I also included an export of the babies' images in .jpg format, as well as an export of the baby's body coordinates in .json format. The idea was to be able to use this data with annotation tools such as labelMe (https://github.com/wkentaro/labelme).

A few days ago, I had to return to this data to prepare a publication. Bad surprise: PixelLib and LableMe no longer work with recent versions of macOS and Apple's new Silicon processors.

Fortunately, with Red (or Rebol3) I was able to solve the problem with a few lines of code. 

Red [
Needs: 'View
]
;--we use func: all words are global
isFile?: none
loadImage: does [
canvas/image: none
clear f/text
clear info/text
isFile?: no
tmpf: request-file/filter ["jpeg Files" "*.jpg"]
unless none? tmpf [
jpgFile:  tmpf
jsonFile: copy jpgFile
replace jsonFile ".jpg" ".json"
canvas/image: load tmpf
f/text: form tmpf
isFile?: yes
]
]
getCoordinates: func [
f [file!]
][
f: read f ;--Red read json file as string
replace f  ",." ",0." ;--in case of missing 0 values
js: load-json f ;--json string -> redbol map object
keys: keys-of js ;--a block of keys
version: select js keys/1 ;--labelMe version
flags: select js keys/2 ;--none
shapes: select js keys/3 ;--coordinates are here as a block of length 1
imagePath: select js keys/4 ;--jpeg file
imageData: select js keys/5 ;--none
imageHeight: select js keys/6 ;--imageHeight
imageWidth: select js keys/7 ;--imageWidth

bPoints: copy [] ;--block for coordinates
;--Thanks to Oldes for s/points
foreach s shapes [
infos: rejoin ["Label: " s/label " ID: " s/group_id " Shape Type: " s/shape_type]
foreach p s/points [
if all [p/1 > 0.0 p/2 > 0.0] [append bPoints to pair! p]
]
]
bPoints ;--returned coordinates
]
showCoordinates: func [f [file!] b [block!]
][
code: compose [
fill-pen 255.0.0.120 ;--draw command
pen 0.0.0.100 ;--draw command
line-width 1 ;--draw command
polygon ;--draw command
]
img: load f
bb: make image! reduce [3x3 black]
foreach p b [
change at img p bb ;--draw coordinates in image
append code p ;--append polygons
]
]
view win: layout [
title  "Neonate Labelling"
button "Load a Neonate File (.jpg)" [loadImage]
button "Draw Extracted Body" [
if isFile? [
showCoordinates jpgFile getCoordinates jsonFile
info/text: infos
canvas/image: draw img code
]
]
info: field 250
pad 15x0
button "Quit" [Quit]
return
canvas: base 640x480 white
return
f: field 640
do [f/enabled?: info/enabled?: no]
]


And the result: Avatar baby ! 







mercredi 23 octobre 2024

Dynamic Time Warping

Dynamic Time Warping (DTW) is a fabulous tool that I use in various applications with students in R2P2 Lab (https://uniter2p2.fr/  for ballistocardiography, pressure measures, gait analysis...).

Quoting wikipedia:

"In time series analysis, dynamic time warping (DTW) is an algorithm for measuring similarity between two temporal sequences which may vary in time or speed. For instance, similarities in walking patterns could be detected using DTW, even if one person was walking faster than the other, or if there were accelerations and decelerations during the course of an observation."


In redCV we use a basic DTW. The objective is to find a mapping between all points of x and y series. In the first step, we will find out the distance between all pair of points in the two signals. Then, in order to create a mapping between the two signals, we need to create a path. The path should start at (0,0) and want to reach (M,N) where (M, N) are the lengths of the two signals. To do this, we thus build a matrix similar to the distance matrix. This matrix would contain the minimum distances to reach a specific point when starting from (0,0). DTW value corresponds to (M,N) sum value.


Results are pretty good for 1-D series.


Now, the question is can we use DTW in image processing? And the response is yes. Images can be considered as a long 1-D vector, and then we can compare two images, as x and y series, to find similarities  between them. 

Now imagine we must compare characters to measure similarity between shapes such as hand-writing productions for example: DTW gives us a direct measurement of the distance between characters. 

If the characters are identical, DTW equals to 0 as illustrated here:





Now consider b and d characters that are close but different by orientation. In this case, DTW value increases.



dtwFreeman.red and dtwPolar.red illustrate this technique for comparing shapes in image. dtwFreeman.red   is a fast version that use only Freeman code chain to identify external pixels of shapes to be compared. dtwPolar.red is more complete since the code associates Freeman code chain and polar coordinates transform to creates X and Y DTW series. Both programs use rcvDTW functions:  rcvDTWDistances,  rcvDTWCosts and  rcvDTWGetDTW.

These techniques were successfully used for a scientific project comparing cursive vs. script writing learning in French and Canadian children. Data were recorded with a Wacom tablet and then processed with Red and RedCV. Each child’s production was compared to a template, and DTW was calculated allowing to reduce the complexity to a sole value, and then allowing statistics on data.


You can do great things with Red!




 








mardi 15 octobre 2024

Giving voices to your apps

I'm starting to think about developing multimodal interfaces (vision and sound) for Red. With macOS it's quite easy because we can use the ‘say’ system command. For Windows and Linux, I'd be delighted if other developers came up with a similar solution.

Here (https://github.com/ldci/Voices) are a few examples of how to use this command with Red and Rebol 3.

This is an example of Red code:

#!/usr/local/bin/red-view
Red[
Author: "ldci"
needs: view
]
;--for macOS 32-bit (Mojave) 
;--new version 
voices:         []
languages: []
sentences: []
flag:                 1
filename:   %voices.txt
getVoices: does [call/shell/output "say -v '?'" filename]
loadVoices:  does [
vfile: read/lines filename
foreach v vfile [
tmp: split v "#" 
append sentences tmp/2
trim/lines tmp/1
append voices first split tmp/1 space
append languages second split tmp/1 space
]
a/text: sentences/1
f/text: languages/1
]
generate: does [
prog: rejoin ["say -v " voices/:flag " " a/text]
call/shell/wait prog
]
mainWin: layout [
title "Voices"
dp1: drop-down data voices
select 1
on-change [
flag: face/selected
f/text: languages/(face/selected)
a/text: sentences/(face/selected)
]
f: field center
button "Talk" [generate] 
pad 100x0 
button  "Quit" [quit]
return
a: area 450x50
do [unless exists? filename [getVoices] loadVoices]
]
view mainWin


And the GUI result


A very interesting approach is now proposed by Oldes for macOS and Windows: https://github.com/Oldes/Rebol-Speak/releases/tag/0.0.1


Minimalistic but efficient :

#!/usr/local/bin/r3
Rebol [
]
speak: import speak     ;--import module
with speak [
list-voices     ;--list all voices
say/as "Hello Red world!" 15             ;--english voice
say/as "Bonjour Red!" 166     ;--french voice
]

And a Windows equivalent of macOS say developed by Philippe Groarkehttps://github.com/p-groarke/wsay?tab=readme-ov-file


And also Jocko's tests for Windows  and macOS: https://github.com/jocko-jc/red-tts

The code was initially developed for Rebol and is now updated for Red.