프로그램은 먼저 배경 깊이와 현재 깊이의 차이를 기준으로 소포 영역을 추출하고, 노이즈 간섭을 줄이기 위해 가장 큰 연결 성분을 유지합니다. 그런 다음 수평 및 수직 신뢰도 분포를 각각 계산하고, 에지 보간을 통해 8×8 그리드에서 소포의 왼쪽, 오른쪽, 위쪽, 아래쪽 경계를 구합니다. Sensor의 60° 시야각을 적용하여 그리드 경계를 공간 각도로 변환하고, 측정된 소포 상단까지의 거리를 사용해 삼각 투영으로 실제 길이와 너비를 계산합니다. 소포의 높이는 빈 플랫폼의 배경 거리와 현재 소포 상단까지의 거리의 차이로 구합니다. 마지막으로 수평 치수 중 큰 값을 길이, 작은 값을 너비로 정하고, 길이·너비·높이의 세 가지 측정 결과를 출력합니다.
#include <Arduino.h>
#include <math.h>
#include <string.h>
#include "unihiker_k10.h"
#include "DFRobot_MatrixLidar.h"
UNIHIKER_K10 k10;
DFRobot_MatrixLidar_I2C tof(0x33);
// ---------- Parameters ----------
constexpr uint8_t GRID=8, ZONES=64;
constexpr float FOV_X_DEG=60.0f, FOV_Y_DEG=60.0f;
// ---------- Dimension Calibration ----------
// Reference box size: 300 × 200 × 150 mm
// Current stable measurement: about 261 × 214 × 156 mm
// Correct only the final L/W/H values; keep background learning, foreground extraction, and the 8×8 display unchanged.
constexpr float LENGTH_SCALE = 300.0f / 261.0f; // ≈1.1494
constexpr float WIDTH_SCALE = 200.0f / 214.0f; // ≈0.9346
constexpr float HEIGHT_SCALE = 150.0f / 156.0f; // ≈0.9615
constexpr uint16_t MIN_VALID_MM=20, MAX_VALID_MM=3900;
constexpr float FOREGROUND_START_MM=35.0f;
constexpr float FOREGROUND_FULL_MM=100.0f;
constexpr float MASK_LEVEL=0.35f;
constexpr float EDGE_MIN_LEVEL=0.50f, EDGE_FRACTION=0.60f;
constexpr uint8_t MIN_COMPONENT_ZONES=3;
constexpr float MIN_PACKAGE_HEIGHT_MM=45.0f;
constexpr uint8_t BACKGROUND_FRAMES=36;
constexpr uint8_t BACKGROUND_MIN_FRAMES=28;
constexpr uint8_t BACKGROUND_VALID_ZONES_MIN=40;
constexpr uint32_t BACKGROUND_TIMEOUT_MS=6000;
constexpr uint8_t DEPTH_FILTER_FRAMES=3;
constexpr uint8_t DETECT_CONFIRM_FRAMES=2;
constexpr uint8_t REMOVE_CONFIRM_FRAMES=4;
constexpr uint8_t MEASURE_FILTER_FRAMES=3;
constexpr uint32_t FRAME_INTERVAL_MS=60;
constexpr uint32_t DISPLAY_INTERVAL_MS=120;
constexpr uint8_t SCREEN_DIR=2;
constexpr int SCREEN_W=240, SCREEN_H=320;
constexpr int MAP_CELL=20, MAP_SIZE=MAP_CELL*GRID;
constexpr int MAP_X=(SCREEN_W-MAP_SIZE)/2, MAP_Y=40;
constexpr uint32_t C_BG=0xFFFFFF, C_PANEL=0xE8F0F7, C_BORDER=0xC5D3E0;
constexpr uint32_t C_TEXT=0x1F2937, C_MUTED=0x64748B;
constexpr uint32_t C_BLUE=0x24A7F2, C_GREEN=0x28D17C;
constexpr uint32_t C_DONE_GREEN=0x58E878, C_YELLOW=0xF4D447, C_RED=0xFF5B64;
constexpr uint32_t C_HEAT_FRAME=0x123A5B, C_HEAT_GRID=0x0A2B43;
constexpr bool ROTATE_MATRIX_180=true;
// ---------- Data ----------
uint16_t rawDepth[ZONES]={0}, filteredDepth[ZONES]={0}, backgroundDepth[ZONES]={0};
uint16_t backgroundSamples[BACKGROUND_FRAMES][ZONES]={{0}};
uint16_t depthHistory[DEPTH_FILTER_FRAMES][ZONES]={{0}};
uint8_t backgroundCount=0, historyCount=0, historyHead=0;
bool backgroundReady=false;
float learningProgressDisplay=0;
uint32_t backgroundLearningStartMs=0;
float confidenceMap[ZONES]={0};
bool componentMask[ZONES]={false};
struct Measurement{
float lengthMm=0, widthMm=0, heightMm=0, distanceMm=0;
uint8_t zoneCount=0;
bool valid=false;
};
Measurement measurementHistory[MEASURE_FILTER_FRAMES], displayedMeasurement;
uint8_t measurementCount=0, measurementHead=0;
bool packagePresent=false;
uint8_t detectedFrames=0, emptyFrames=0;
uint32_t lastFrameMs=0, lastDisplayMs=0, lastSerialMs=0;
bool previousButtonA=false;
// ---------- Utilities ----------
bool validDistance(uint16_t mm){ return mm>=MIN_VALID_MM && mm<MAX_VALID_MM; }
float clamp01(float v){ return v<0?0:(v>1?1:v); }
float zoneAngleRad(uint8_t i,float fov){
return (((i+0.5f)/GRID)-0.5f)*fov*DEG_TO_RAD;
}
float axialDistanceMm(uint16_t radial,uint8_t row,uint8_t col){
float tx=tanf(zoneAngleRad(col,FOV_X_DEG));
float ty=tanf(zoneAngleRad(row,FOV_Y_DEG));
return radial/sqrtf(1.0f+tx*tx+ty*ty);
}
template<typename T>
void insertionSort(T *a,uint8_t n){
for(uint8_t i=1;i<n;i++){
T key=a[i]; int j=i-1;
while(j>=0 && a[j]>key){ a[j+1]=a[j]; j--; }
a[j+1]=key;
}
}
float medianFloat(float *a,uint8_t n){
if(!n) return 0;
insertionSort(a,n);
return (n&1)?a[n/2]:0.5f*(a[n/2-1]+a[n/2]);
}
uint16_t medianU16(uint16_t *a,uint8_t n){
if(!n) return 0;
insertionSort(a,n);
return (n&1)?a[n/2]:(uint16_t)(((uint32_t)a[n/2-1]+a[n/2])/2);
}
uint8_t displayIndex(uint8_t row,uint8_t col){
return ROTATE_MATRIX_180 ? (GRID-1-row)*GRID+(GRID-1-col) : row*GRID+col;
}
void setRgb(uint32_t c){ k10.rgb->setRangeColor(0,2,c); }
// ---------- Background Learning ----------
void resetMeasurementState(){
packagePresent=false;
detectedFrames=emptyFrames=measurementCount=measurementHead=0;
displayedMeasurement=Measurement();
memset(componentMask,0,sizeof(componentMask));
}
void startBackgroundLearning(){
backgroundReady=false;
backgroundCount=historyCount=historyHead=0;
learningProgressDisplay=0;
backgroundLearningStartMs=millis();
memset(backgroundDepth,0,sizeof(backgroundDepth));
memset(backgroundSamples,0,sizeof(backgroundSamples));
memset(depthHistory,0,sizeof(depthHistory));
resetMeasurementState();
setRgb(C_YELLOW);
Serial.println("Background learning started. Keep platform empty.");
}
void finishBackgroundLearning(){
for(uint8_t z=0;z<ZONES;z++){
uint16_t values[BACKGROUND_FRAMES];
uint8_t n=0;
for(uint8_t f=0;f<backgroundCount;f++){
uint16_t v=backgroundSamples[f][z];
if(validDistance(v)) values[n++]=v;
}
backgroundDepth[z]=(n>=BACKGROUND_FRAMES/2)?medianU16(values,n):0;
}
backgroundReady=true;
historyCount=historyHead=0;
setRgb(C_BLUE);
Serial.println("Background learning complete.");
}
void addBackgroundFrame(const uint16_t *frame){
if(backgroundCount>=BACKGROUND_FRAMES) return;
uint8_t valid=0;
for(uint8_t i=0;i<ZONES;i++){
backgroundSamples[backgroundCount][i]=frame[i];
if(validDistance(frame[i])) valid++;
}
if(valid>=BACKGROUND_VALID_ZONES_MIN) backgroundCount++;
if(backgroundCount>=BACKGROUND_FRAMES){
finishBackgroundLearning();
}else if(millis()-backgroundLearningStartMs>=BACKGROUND_TIMEOUT_MS &&
backgroundCount>=BACKGROUND_MIN_FRAMES){
finishBackgroundLearning();
}
}
// ---------- Depth Filtering ----------
void updateDepthMedian(const uint16_t *frame){
memcpy(depthHistory[historyHead],frame,sizeof(uint16_t)*ZONES);
historyHead=(historyHead+1)%DEPTH_FILTER_FRAMES;
if(historyCount<DEPTH_FILTER_FRAMES) historyCount++;
for(uint8_t z=0;z<ZONES;z++){
uint16_t values[DEPTH_FILTER_FRAMES];
uint8_t n=0;
for(uint8_t h=0;h<historyCount;h++){
uint16_t v=depthHistory[h][z];
if(validDistance(v)) values[n++]=v;
}
filteredDepth[z]=medianU16(values,n);
}
}
// ---------- Foreground Extraction ----------
void buildConfidenceMap(){
for(uint8_t row=0;row<GRID;row++){
for(uint8_t col=0;col<GRID;col++){
uint8_t i=row*GRID+col;
if(!validDistance(backgroundDepth[i]) || !validDistance(filteredDepth[i])){
confidenceMap[i]=0;
continue;
}
float bg=axialDistanceMm(backgroundDepth[i],row,col);
float now=axialDistanceMm(filteredDepth[i],row,col);
confidenceMap[i]=clamp01((bg-now-FOREGROUND_START_MM)/
(FOREGROUND_FULL_MM-FOREGROUND_START_MM));
}
}
}
uint8_t keepLargestComponent(){
bool visited[ZONES]={false}, best[ZONES]={false};
uint8_t bestCount=0;
const int dr[8]={-1,-1,-1,0,0,1,1,1};
const int dc[8]={-1,0,1,-1,1,-1,0,1};
for(uint8_t seed=0;seed<ZONES;seed++){
if(visited[seed] || confidenceMap[seed]<MASK_LEVEL) continue;
uint8_t q[ZONES], members[ZONES], head=0, tail=0, count=0;
q[tail++]=seed; visited[seed]=true;
while(head<tail){
uint8_t cur=q[head++];
members[count++]=cur;
int row=cur/GRID, col=cur%GRID;
for(uint8_t n=0;n<8;n++){
int nr=row+dr[n], nc=col+dc[n];
if(nr<0 || nr>=GRID || nc<0 || nc>=GRID) continue;
uint8_t next=nr*GRID+nc;
if(!visited[next] && confidenceMap[next]>=MASK_LEVEL){
visited[next]=true;
q[tail++]=next;
}
}
}
if(count>bestCount){
memset(best,0,sizeof(best));
for(uint8_t i=0;i<count;i++) best[members[i]]=true;
bestCount=count;
}
}
memcpy(componentMask,best,sizeof(componentMask));
return bestCount;
}
bool profileEdges(const float p[GRID],float &left,float &right){
float peak=0;
for(uint8_t i=0;i<GRID;i++) peak=max(peak,p[i]);
if(peak<EDGE_MIN_LEVEL) return false;
float level=max(EDGE_MIN_LEVEL,peak*EDGE_FRACTION);
int first=-1,last=-1;
for(uint8_t i=0;i<GRID;i++){
if(p[i]>=level){ if(first<0) first=i; last=i; }
}
if(first<0) return false;
float inX=first+0.5f, outX=first-0.5f;
float inV=p[first], outV=first>0?p[first-1]:0;
float den=inV-outV;
left=fabsf(den)>0.0001f ? outX+(level-outV)/den : (float)first;
inX=last+0.5f; outX=last+1.5f;
inV=p[last]; outV=last<GRID-1?p[last+1]:0;
den=outV-inV;
right=fabsf(den)>0.0001f ? inX+(level-inV)/den : (float)(last+1);
left=constrain(left,0.0f,(float)GRID);
right=constrain(right,0.0f,(float)GRID);
return right>left;
}
float gridBoundaryAngle(float b,float fov){
return ((b/GRID)-0.5f)*fov*DEG_TO_RAD;
}
// ---------- Length / Width / Height ----------
Measurement analyzeForeground(){
Measurement r;
buildConfidenceMap();
uint8_t zones=keepLargestComponent();
if(zones<MIN_COMPONENT_ZONES) return r;
float colP[GRID]={0}, rowP[GRID]={0};
float allZ[ZONES], allH[ZONES], allR[ZONES];
float coreZ[ZONES], coreH[ZONES], coreR[ZONES];
uint8_t allN=0, coreN=0;
for(uint8_t row=0;row<GRID;row++){
for(uint8_t col=0;col<GRID;col++){
uint8_t i=row*GRID+col;
if(!componentMask[i]) continue;
float c=confidenceMap[i];
colP[col]+=c; rowP[row]+=c;
float z=axialDistanceMm(filteredDepth[i],row,col);
float bg=axialDistanceMm(backgroundDepth[i],row,col);
float h=bg-z;
allZ[allN]=z; allH[allN]=h; allR[allN]=filteredDepth[i]; allN++;
if(c>=0.72f){
coreZ[coreN]=z; coreH[coreN]=h; coreR[coreN]=filteredDepth[i]; coreN++;
}
}
}
if(!allN) return r;
float maxCol=0,maxRow=0;
for(uint8_t i=0;i<GRID;i++){
maxCol=max(maxCol,colP[i]);
maxRow=max(maxRow,rowP[i]);
}
if(maxCol<=0.0001f || maxRow<=0.0001f) return r;
for(uint8_t i=0;i<GRID;i++){ colP[i]/=maxCol; rowP[i]/=maxRow; }
float left,right,top,bottom;
if(!profileEdges(colP,left,right) || !profileEdges(rowP,top,bottom)) return r;
bool useCore=coreN>=2;
float topZ=useCore?medianFloat(coreZ,coreN):medianFloat(allZ,allN);
float height=useCore?medianFloat(coreH,coreN):medianFloat(allH,allN);
float radial=useCore?medianFloat(coreR,coreN):medianFloat(allR,allN);
if(topZ<=0 || height<MIN_PACKAGE_HEIGHT_MM) return r;
float aL=gridBoundaryAngle(left,FOV_X_DEG);
float aR=gridBoundaryAngle(right,FOV_X_DEG);
float aT=gridBoundaryAngle(top,FOV_Y_DEG);
float aB=gridBoundaryAngle(bottom,FOV_Y_DEG);
float x=topZ*fabsf(tanf(aR)-tanf(aL));
float y=topZ*fabsf(tanf(aB)-tanf(aT));
// Raw geometric result
const float rawLength=max(x,y);
const float rawWidth=min(x,y);
// Apply three-axis calibration for the current fixed installation
r.lengthMm=rawLength*LENGTH_SCALE;
r.widthMm=rawWidth*WIDTH_SCALE;
r.heightMm=height*HEIGHT_SCALE;
r.distanceMm=radial;
r.zoneCount=zones;
r.valid=r.lengthMm>=20.0f && r.widthMm>=20.0f;
return r;
}
void addMeasurement(const Measurement &m){
if(displayedMeasurement.valid &&
(fabsf(m.lengthMm-displayedMeasurement.lengthMm)>45 ||
fabsf(m.widthMm-displayedMeasurement.widthMm)>45 ||
fabsf(m.heightMm-displayedMeasurement.heightMm)>35)){
measurementCount=measurementHead=0;
}
measurementHistory[measurementHead]=m;
measurementHead=(measurementHead+1)%MEASURE_FILTER_FRAMES;
if(measurementCount<MEASURE_FILTER_FRAMES) measurementCount++;
float L[MEASURE_FILTER_FRAMES],W[MEASURE_FILTER_FRAMES];
float H[MEASURE_FILTER_FRAMES],D[MEASURE_FILTER_FRAMES];
for(uint8_t i=0;i<measurementCount;i++){
L[i]=measurementHistory[i].lengthMm;
W[i]=measurementHistory[i].widthMm;
H[i]=measurementHistory[i].heightMm;
D[i]=measurementHistory[i].distanceMm;
}
float l=medianFloat(L,measurementCount);
float w=medianFloat(W,measurementCount);
float h=medianFloat(H,measurementCount);
float d=medianFloat(D,measurementCount);
if(!displayedMeasurement.valid){
displayedMeasurement.lengthMm=l;
displayedMeasurement.widthMm=w;
displayedMeasurement.heightMm=h;
displayedMeasurement.distanceMm=d;
}else{
if(fabsf(l-displayedMeasurement.lengthMm)>2.5f)
displayedMeasurement.lengthMm=0.35f*displayedMeasurement.lengthMm+0.65f*l;
if(fabsf(w-displayedMeasurement.widthMm)>2.5f)
displayedMeasurement.widthMm=0.35f*displayedMeasurement.widthMm+0.65f*w;
if(fabsf(h-displayedMeasurement.heightMm)>2.0f)
displayedMeasurement.heightMm=0.30f*displayedMeasurement.heightMm+0.70f*h;
if(fabsf(d-displayedMeasurement.distanceMm)>2.0f)
displayedMeasurement.distanceMm=0.35f*displayedMeasurement.distanceMm+0.65f*d;
}
displayedMeasurement.zoneCount=m.zoneCount;
displayedMeasurement.valid=true;
}
void updatePresenceState(const Measurement &m){
if(m.valid){
emptyFrames=0;
if(detectedFrames<DETECT_CONFIRM_FRAMES) detectedFrames++;
if(!packagePresent) setRgb(C_YELLOW);
if(detectedFrames>=DETECT_CONFIRM_FRAMES){
if(!packagePresent){
packagePresent=true;
measurementCount=measurementHead=0;
setRgb(C_GREEN);
}
addMeasurement(m);
}
}else{
detectedFrames=0;
if(packagePresent){
if(emptyFrames<REMOVE_CONFIRM_FRAMES) emptyFrames++;
if(emptyFrames>=REMOVE_CONFIRM_FRAMES){
resetMeasurementState();
setRgb(C_BLUE);
}
}else setRgb(C_BLUE);
}
}
// ---------- UI ----------
void fillScreen(uint32_t c){
k10.canvas->canvasRectangle(0,0,SCREEN_W,SCREEN_H,c,c,true);
}
void drawText(const String &s,int x,int y,uint32_t c,
Canvas::eFontSize_t font=Canvas::eCNAndENFont16){
k10.canvas->canvasText(s,x,y,c,font,30,false);
}
String inch(float mm){ return String(mm/25.4f,1); }
uint32_t matrixCellColor(uint8_t i){
if(!packagePresent && detectedFrames==0) return C_BLUE;
if(!packagePresent) return componentMask[i]?C_YELLOW:C_BLUE;
return componentMask[i]?C_DONE_GREEN:C_BLUE;
}
void drawStateMatrix(){
k10.canvas->canvasSetLineWidth(1);
k10.canvas->canvasRectangle(MAP_X-2,MAP_Y-2,MAP_SIZE+4,MAP_SIZE+4,
C_HEAT_FRAME,C_HEAT_FRAME,true);
for(uint8_t row=0;row<GRID;row++){
for(uint8_t col=0;col<GRID;col++){
uint8_t i=displayIndex(row,col);
int x=MAP_X+col*MAP_CELL, y=MAP_Y+row*MAP_CELL;
uint32_t c=matrixCellColor(i);
k10.canvas->canvasRectangle(x,y,MAP_CELL,MAP_CELL,C_HEAT_GRID,C_HEAT_GRID,true);
k10.canvas->canvasRectangle(x+1,y+1,MAP_CELL-2,MAP_CELL-2,c,c,true);
}
}
}
void drawLearningScreen(){
fillScreen(C_BG);
drawText("Learning",76,72,C_BLUE,Canvas::eCNAndENFont24);
drawText("Keep platform empty",39,132,C_MUTED);
float target=backgroundCount*100.0f/BACKGROUND_FRAMES;
learningProgressDisplay+=(target-learningProgressDisplay)*0.32f;
if(fabsf(target-learningProgressDisplay)<0.2f) learningProgressDisplay=target;
int p=constrain((int)(learningProgressDisplay+0.5f),0,100);
k10.canvas->canvasRectangle(32,190,176,14,C_BORDER,C_PANEL,true);
k10.canvas->canvasRectangle(34,192,172*p/100,10,C_BLUE,C_BLUE,true);
drawText(String(p)+"%",p>=100?86:94,220,C_TEXT,Canvas::eCNAndENFont24);
k10.canvas->updateCanvas();
}
void drawMainScreen(){
if(lastDisplayMs && millis()-lastDisplayMs<DISPLAY_INTERVAL_MS) return;
lastDisplayMs=millis();
fillScreen(C_BG);
drawText("Package Size",48,7,C_TEXT,Canvas::eCNAndENFont24);
drawStateMatrix();
String s="-- x -- x -- in";
if(packagePresent && displayedMeasurement.valid){
s=inch(displayedMeasurement.lengthMm)+" x "+
inch(displayedMeasurement.widthMm)+" x "+
inch(displayedMeasurement.heightMm)+" in";
}
int x=s.length()<=16?22:(s.length()<=20?12:6);
drawText(s,x,210,C_TEXT,Canvas::eCNAndENFont24);
k10.canvas->updateCanvas();
}
void drawSensorError(){
fillScreen(C_BG);
drawText("Sensor Error",48,82,C_RED,Canvas::eCNAndENFont24);
drawText("SEN0628 not found",45,142,C_TEXT);
drawText("Check I2C wiring",50,178,C_MUTED);
k10.canvas->updateCanvas();
}
void handleButtonA(){
bool a=k10.buttonA->isPressed();
if(a && !previousButtonA) startBackgroundLearning();
previousButtonA=a;
}
// ---------- Serial Output ----------
void printDiagnostics(const Measurement &m){
if(millis()-lastSerialMs<500) return;
lastSerialMs=millis();
Serial.print("state=");
Serial.print(!backgroundReady?"LEARNING":(packagePresent?"PRESENT":"WAITING"));
Serial.print(" zones=");
Serial.print(m.zoneCount);
Serial.print(" L/W/H(mm)=");
if(displayedMeasurement.valid){
Serial.print(displayedMeasurement.lengthMm,1); Serial.print("/");
Serial.print(displayedMeasurement.widthMm,1); Serial.print("/");
Serial.println(displayedMeasurement.heightMm,1);
}else{
Serial.println("--/--/--");
}
}
// ---------- Initialization ----------
void setup(){
Serial.begin(115200);
delay(1000);
k10.begin();
k10.initScreen(SCREEN_DIR);
k10.creatCanvas();
k10.setScreenBackground(C_BG);
k10.rgb->brightness(20);
fillScreen(C_BG);
drawText("Connecting sensor",26,120,C_BLUE,Canvas::eCNAndENFont24);
k10.canvas->updateCanvas();
int r=tof.begin();
Serial.print("tof.begin result = "); Serial.println(r);
if(r!=0){
drawSensorError();
while(1) delay(100);
}
r=tof.setRangingMode(eMatrix_8X8);
Serial.print("setRangingMode result = "); Serial.println(r);
if(r!=0){
drawSensorError();
while(1) delay(100);
}
Serial.println("SEN0628 8x8 ready.");
delay(300);
startBackgroundLearning();
}
// ---------- Main Loop ----------
void loop(){
handleButtonA();
uint32_t now=millis();
if(now-lastFrameMs<FRAME_INTERVAL_MS){ delay(2); return; }
lastFrameMs=now;
if(tof.getAllData(rawDepth)!=0){
Serial.println("getAllData failed");
delay(20);
return;
}
if(!backgroundReady){
addBackgroundFrame(rawDepth);
drawLearningScreen();
return;
}
updateDepthMedian(rawDepth);
Measurement current;
if(historyCount>=DEPTH_FILTER_FRAMES) current=analyzeForeground();
updatePresenceState(current);
drawMainScreen();
printDiagnostics(current);
}
8x8 matrix tof 3D distance sensor로 측정한 데이터를 기준으로 깊이 거리를 길이, 너비, 높이로 변환한 값은 (12.0 in x 6.3 in x 5.9 in)입니다. 소포의 실제 치수는 (11.8 in x 7.9 in x 5.9 in)이며, 두 값 모두 오차가 매우 작아 실제 환경에서의 신뢰성을 보여 줍니다.