Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor blob detection outside of the Touch class #20

Merged
merged 13 commits into from
Dec 19, 2024
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ build/
.vscode/
*.user
*code-workspace
library.json
1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ add_library(puara_gestures
include/puara/descriptors/tilt.h
include/puara/descriptors/touch.h

include/puara/utils/blobDetector.h
include/puara/utils/calibration.h
include/puara/utils/circularbuffer.h
include/puara/utils/leakyintegrator.h
Expand Down
95 changes: 23 additions & 72 deletions include/puara/descriptors/touch.h
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We may leave this as is for now, but my suggestion is to have the blobdetector as a separate class and the touch class only dealing with extracting the features.

In that case, there's no need for multi versions of the gestures as the class itself will deal with 1 DoF at a time. The user can implement multi versions (or we can create wrappers for that) from the main touch class.

Original file line number Diff line number Diff line change
@@ -1,43 +1,41 @@
#pragma once

#include <puara/utils.h>
#include <puara/utils/blobDetector.h>
#include <puara/utils/leakyintegrator.h>

#include <cmath>
#include<iostream>

namespace puara_gestures
{

class Touch
{
public:
static constexpr int maxNumBlobs = BlobDetector::maxNumBlobs;
float touchAll = 0.0f; // f, 0--1
float touchTop = 0.0f; // f, 0--1
float touchMiddle = 0.0f; // f, 0--1
float touchBottom = 0.0f; // f, 0--1
float brush = 0.0f; // f, 0--? (~cm/s)
double multiBrush[4]{}; // ffff, 0--? (~cm/s)
double multiBrush[maxNumBlobs]{}; // ffff, 0--? (~cm/s)
float rub{}; // f, 0--? (~cm/s)
double multiRub[4]{}; // ffff, 0--? (~cm/s)
double multiRub[maxNumBlobs]{}; // ffff, 0--? (~cm/s)

// touch array
int touchSizeEdge
= 4; // amount of touch stripes for top and bottom portions (arbitrary)
int lastState_blobPos[4]{};
int maxBlobs = 4; // max amount of blobs to be detected
int blobAmount{}; // amount of detected blobs
int blobCenter[4]{}; // shows the "center" (index) of each blob (former blobArray)
int blobPos[4]{}; // starting position (index) of each blob
float blobSize[4]{}; // "size" (amount of stripes) of each blob
int brushCounter[4]{};
int touchSizeEdge = 4; // amount of touch stripes for top and bottom portions (arbitrary)

BlobDetector blobDetector;
int brushCounter[maxNumBlobs]{};

// Arrays of LeakyIntegrator instances
utils::LeakyIntegrator multiBrushIntegrator[4];
utils::LeakyIntegrator multiRubIntegrator[4];
utils::LeakyIntegrator multiBrushIntegrator[maxNumBlobs];
utils::LeakyIntegrator multiRubIntegrator[maxNumBlobs];

Touch()
{
for(int i = 0; i < 4; ++i)
for(int i = 0; i < maxNumBlobs; ++i)
{
multiBrushIntegrator[i] = utils::LeakyIntegrator(0.0f, 0.0f, 0.7f, 100, 0);
multiRubIntegrator[i] = utils::LeakyIntegrator(0.0f, 0.0f, 0.7f, 100, 0);
Expand Down Expand Up @@ -67,28 +65,15 @@ class Touch
// normalized between 0 and 1
touchBottom = touchAverage(discrete_touch, (touchSize - touchSizeEdge), touchSize);

// Save last blob detection state before reading new data
for(int i = 0; i < (sizeof(blobPos) / sizeof(blobPos[0])); ++i)
{
lastState_blobPos[i] = blobPos[i];
}

// 1D blob detection: used for brush
blobDetection1D(discrete_touch, touchSize);
// 1D blob detection: used for brush
const auto movement = blobDetector.detect1D(discrete_touch, touchSize);

// brush: direction and intensity of capsense brush motion
// rub: intensity of rub motion
// in ~cm/s (distance between stripes = ~1.5cm)
for(int i = 0; i < (sizeof(blobPos) / sizeof(blobPos[0])); ++i)
for(int i = 0; i < movement.size(); ++i)
{
float movement = blobPos[i] - lastState_blobPos[i];
if(blobPos[i] == -1)
{
multiBrush[i] = 0;
multiRub[i] = 0;
brushCounter[i] = 0;
}
else if(movement == 0)
if(movement[i] == 0)
{
if(brushCounter[i] < 10)
{
Expand All @@ -109,11 +94,11 @@ class Touch
// (std::abs(movement * 0.15)), multiRub[i], 0.7, leakyRubFreq,
// leakyRubTimer);
//
multiBrush[i] = multiBrushIntegrator[i].integrate(movement * 0.15);
multiRub[i] = multiRubIntegrator[i].integrate(std::abs(movement * 0.15));
multiBrush[i] = multiBrushIntegrator[i].integrate(movement[i] * 0.15);
multiRub[i] = multiRubIntegrator[i].integrate(std::abs(movement[i] * 0.15));
Comment on lines +97 to +98
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here we can promote brush and rub as specific classes (separate from the less interesting gestures from this class)

}
}
else if(std::abs(movement) > 1)
else if(std::abs(movement[i]) > 1)
{
// multiBrush[i] = multiBrushIntegrator[i].integrate(
// 0, multiBrush[i], 0.6, leakyBrushFreq, leakyBrushTimer);
Expand All @@ -128,14 +113,14 @@ class Touch
// (std::abs(movement * 0.15)) * 0.15, multiRub[i], 0.99, leakyRubFreq,
// leakyRubTimer);

multiBrush[i] = multiBrushIntegrator[i].integrate(movement * 0.15);
multiRub[i] = multiRubIntegrator[i].integrate((std::abs(movement * 0.15)));
multiBrush[i] = multiBrushIntegrator[i].integrate(movement[i] * 0.15);
multiRub[i] = multiRubIntegrator[i].integrate((std::abs(movement[i] * 0.15)));

brushCounter[i] = 0;
}
}
brush = utils::arrayAverageZero(multiBrush, 4);
rub = utils::arrayAverageZero(multiRub, 4);
brush = utils::arrayAverageZero(multiBrush, maxNumBlobs);
rub = utils::arrayAverageZero(multiRub, maxNumBlobs);
}

float touchAverage(float* touchArrayStrips, int firstStrip, int lastStrip)
Expand All @@ -155,39 +140,5 @@ class Touch

return ((float)sum) / (lastStrip - firstStrip);
}

//TODO: move to utils
void blobDetection1D(int* discrete_touch, int touchSize)
{
blobAmount = 0;
int sizeCounter = 0;
int stripe = 0;
for(int i = 0; i < 4; i++)
{
blobCenter[i] = 0;
blobPos[i] = 0;
blobSize[i] = 0;
}

for(; stripe < touchSize; stripe++)
{
if(blobAmount < maxBlobs)
{
if(discrete_touch[stripe] == 1)
{ // check for beggining of blob...
sizeCounter = 1;
blobPos[blobAmount] = stripe;
while(discrete_touch[stripe + sizeCounter] == 1)
{ // then keep checking for end
sizeCounter++;
}
blobSize[blobAmount] = sizeCounter;
blobCenter[blobAmount] = stripe + (sizeCounter / 2);
stripe += sizeCounter + 1; // skip stripes already read
blobAmount++;
}
}
}
}
};
}
138 changes: 138 additions & 0 deletions include/puara/utils/blobDetector.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
#pragma once

#include <boost/container/small_vector.hpp>

namespace puara_gestures
{

/**
* @struct BlobDetector
* @brief A structure for detecting contiguous regions (blobs) of `1`s in binary arrays.
*
* The `BlobDetector` identifies contiguous blobs in a binary input array where elements are either
* `0` or `1`.
* For each blob, it computes:
* - The start position (`blobStartPos`)
* - The size in terms of consecutive `1`s (`blobSize`)
* - The center index (`blobCenter`)
* - Movement compared to the previous detection (`lastState_blobPos`)
* @warning Most of these values are reset when a detection function, e.g., detect1D(), is called,
* and calculated during the detection funtion calls -- so their value is only really valid right
* after such a call. There is no locking mechanism on any of these values so it is on the client
* to ensure there is no race conditions.
*
* @details
* The struct maintains internal arrays to store information about detected blobs, including their
* start positions, sizes, and centers. It also tracks blob positions from the previous invocation
* of the detection function to compute movement values.
*
* ## Key Functionalities:
* - **Blob Detection:** Detects contiguous regions of `1`s in a binary input array.
* - **Movement Calculation:** Computes the positional changes (movement) of blobs compared to
* their last positions.
* - **Data Management:** Maintains internal state between function calls to facilitate movement
* tracking.
*
* @note
* - The number of blobs processed is limited by `maxNumBlobs` (default is `4`).
* - If the input contains more blobs than `maxNumBlobs`, the additional blobs are ignored.
*/
class BlobDetector
{
public:
/** The maximum number of blobs that the algorithm should detect. */
static constexpr int maxNumBlobs = 4;

/** The start index of detected blobs. */
int blobStartPos[maxNumBlobs]{};

/** The cached start index of detected blobs, from the previous time detect1D
* was called.
*/
int lastState_blobPos[maxNumBlobs]{};

/** size (amount of stripes) of each blob */
int blobSize[maxNumBlobs]{};

/** shows the "center"(index)of each blob */
float blobCenter[maxNumBlobs]{};

/** amount of detected blobs */
int blobAmount{};

/**
* @brief Detects contiguous regions (blobs) of `1`s in a 1D binary array and computes their movement.
*
* This function identifies blobs in the input binary array `touchArray`, calculates their start
* positions, sizes, and centers, and returns the movement of the blobs compared to their positions
* from the previous function call.
*
* @param touchArray Pointer to the 1D binary array representing touch data. Each element is expected to be 0 or 1.
* @param touchArraySize The size of the `touchArray`, representing the number of touch sensor in the array.
* This is expected to be larger than maxNumBlobs, which by definition will be a portion of the number
* of sensors, or at most equal to the number of sensors.
* @return A small_vector of integers representing the movement of each blob's start position since the
* last invocation of `detect1D`.
*
* @note
* - The function updates the global variables `blobStartPos`, `blobSize`, `blobCenter`,
* and `lastState_blobPos`.
* - The number of blobs detected is limited by `maxNumBlobs`.
* - If the number of blobs exceeds `maxNumBlobs`, additional blobs are ignored.
*
* @warning
* - Ensure that `touchArray` has at least `size` elements to avoid out-of-bounds access.
* - The function relies on external global variables (`blobStartPos`, `blobSize`, `blobCenter`,
* `lastState_blobPos`, `maxNumBlobs`, `blobAmount`). Ensure they are initialized appropriately
* before calling the function.
*/
boost::container::small_vector<int, maxNumBlobs>
detect1D(const int* const touchArray, const int touchArraySize)
{
blobAmount = 0;
vberthiaume marked this conversation as resolved.
Show resolved Hide resolved
for(int i = 0; i < maxNumBlobs; i++)
{
//cache the last blobStartPos before clearing it
lastState_blobPos[i] = blobStartPos[i];
blobStartPos[i] = 0;
blobSize[i] = 0;
blobCenter[i] = 0;
}

for(int stripe = 0; stripe < touchArraySize;)
{
if(touchArray[stripe] == 1)
{
//start the blob
blobStartPos[blobAmount] = stripe;

//continue the blob until we no longer have 1s
int sizeCounter = 1;
while((stripe + sizeCounter) <= touchArraySize
&& touchArray[stripe + sizeCounter] == 1)
{
sizeCounter++;
}

blobSize[blobAmount] = sizeCounter;
blobCenter[blobAmount] = stripe + (sizeCounter - 1.0) / 2.0;
stripe += sizeCounter;

if(++blobAmount >= maxNumBlobs)
break;
}
else
{
++stripe;
}
}

//return the movement since the last time detect1D was called
boost::container::small_vector<int, maxNumBlobs> movement(maxNumBlobs, 0);
for(int i = 0; i < maxNumBlobs; ++i)
movement[i] = blobStartPos[i] - lastState_blobPos[i];

return movement;
}
};
}
56 changes: 47 additions & 9 deletions tests/testing_touch.cpp
Original file line number Diff line number Diff line change
@@ -1,26 +1,64 @@
#include <puara/gestures.h>

#include <iostream>

using namespace puara_gestures;

int main()
{
Touch touch;

int touchSize = 16;
int discrete_touch[16] = {0};
constexpr int touchSize = 16;
int discrete_touch[touchSize] = {0};

// simulate a blob of size 1 starting at position 0
jcelerier marked this conversation as resolved.
Show resolved Hide resolved
discrete_touch[0] = 1;

// simulate a touch (1) at a position 5
// simulate a blob of size 2 starting at position 5
discrete_touch[5] = 1;
discrete_touch[6] = 1;

// Update the touch data
touch.updateTouchArray(discrete_touch, touchSize);
// simulate a blob of size 3 starting at position 8
discrete_touch[8] = 1;
discrete_touch[9] = 1;
discrete_touch[10] = 1;

// simulate a blob of size 1 at position 12 -- commented out to test the end of the array in the next blob
//discrete_touch[12] = 1;

// Output the computed values
// simulate a blob of size 2 starting at position 14
discrete_touch[14] = 1;
discrete_touch[15] = 1;

// Update the touch data and print the computed values
touch.updateTouchArray(discrete_touch, touchSize);
std::cout << "touchAll: " << touch.touchAll << std::endl;
std::cout << "brush: " << touch.brush << std::endl;
std::cout << "rub: " << touch.rub << std::endl;

// multi touch
//reset the touch array, and next we'll repeat the above blobs but offset by 1 to simulate movement
for(int i = 0; i < touchSize; ++i)
discrete_touch[i] = 0;

// simulate a blob of size 1 starting at position 1
discrete_touch[1] = 1;

// simulate a blob of size 2 starting at position 6
discrete_touch[6] = 1;
discrete_touch[7] = 1;

// simulate a blob of size 3 starting at position 9
discrete_touch[9] = 1;
discrete_touch[10] = 1;
discrete_touch[11] = 1;

// simulate a blob of size 1 at position 13 -- commented out to test the end of the array in the next blob
//discrete_touch[13] = 1;

// simulate a blob of size 1 starting at position 15
discrete_touch[15] = 1;

// Update the touch data and print the computed values
touch.updateTouchArray(discrete_touch, touchSize);
std::cout << "touchAll: " << touch.touchAll << std::endl;
std::cout << "brush: " << touch.brush << std::endl;
std::cout << "rub: " << touch.rub << std::endl;
}
Loading