📒 작품 소개 ☆
고정적인 책상 구조에서 벗어나기 위한 아이디어.
"수동으로 직접 높이를 조절하거나 버튼으로 높이 조절하는 방식이 아닌 다른 방식이 없을까?" 생각해보았다.
사람의 움직임에 맞춰 적절한 높이까지 맞춰보자~!!
사람이 직접 개입하지 않고 원하는 동작만 취해도 높이를 맞춰줄 수 있는 스마트한 책상을 개발해보기로 하였다.
그 외에도 책상을 사용하는 동안 생기면 괜찮은 추가 기능들도 만들어보았다.
GitHub - hwanggiju/Capstone_Design: 캡스톤 디자인 - 스마트 데스크
캡스톤 디자인 - 스마트 데스크. Contribute to hwanggiju/Capstone_Design development by creating an account on GitHub.
github.com
목차
#1 스마트 데스크
작품 기간 : 2021.09 ~ 2022.06 (약 1 년간)

1. 개발 환경
- 기본 지식 : 회로에 대한 이해
- 개발 언어 : Python
- OS : RaspberryPi OS
- 개발 환경 : VisualStudio Code, Github
- 개발 보드 : Raspberry Pi 4
2. 목표
- 사람이 일어서거나 앉는 동작에 맞춰 사람의 키만큼 책상을 적정 높이까지 맞춰줄 수 있다.
- 여러 가지 기능성을 부여하기 위해 버튼 입력식 높이 조절과 졸음 체크 기능을 추가하고,
디스플레이를 통해 원하는 기능을 동작시킬 수 있다.
3. 나의 역할
- OpenCV를 활용한 사용자 얼굴 인식 코드 구현
- 얼굴 인식 기반 사용자 키 측정 알고리즘 코드 구현
- 높낮이 조절을 위한 모터 제어 코드 작성 및 가속도-자이로 센서 코드 작성
- 초음파 센서 코드 작성
- 디스플레이 기능 설정 코드 제작 및 추가 기능 구현
4. 활동 내용
>> OpenCV를 활용한 사용자 얼굴 인식 코드 구현
- Caffe의 얼굴 인식 모델을 OpenCV DNN 함수로 불러와 얼굴 인식 기능을 구현하였다.
- 아래 코드는 Caffe 모델을 받아온 후, OpenCV DNN 함수로 불러오는 코드이다.
import cv2
model = 'res10_300x300_ssd_iter_140000.caffemodel'
config = 'deploy.prototxt.txt'
net = cv2.dnn.readNet(model, config)
if net.empty() :
print('Net open failed!')
- 찾아낸 얼굴에 바운딩 박스를 그려서 사용자 키 측정 알고리즘을 세우는 데에 활용하였다.
- 아래 코드는 인식된 얼굴에 바운딩 박스를 그려주는 코드이다.
- 찾은 얼굴 좌표 값에 바운딩 박스를 그려주는 코드와 얼굴 일치 정확도가 87% 이상일 때, 동작할 수 있게 구현하였다.

blob = cv2.dnn.blobFromImage(rotate_frame, # image
1, # scalefactor
(200, 200), # image Size
(104, 177, 123) # Scalar
)
net.setInput(blob)
detect = net.forward()
(h, w) = rotate_frame.shape[:2]
detect = detect[0, 0, :, :]
for i in range(detect.shape[0]):
confidence = detect[i, 2]
if confidence < 0.87:
break
x1 = int(detect[i, 3] * w)
y1 = int(detect[i, 4] * h)
x2 = int(detect[i, 5] * w)
y2 = int(detect[i, 6] * h)
cv2.rectangle(rotate_frame, (x1, y1), (x2, y2), (0, 255, 0)) # green 박스
cv2.rectangle(rotate_frame, (x1+1, y1+1), (x2-1, y2-1), (0, 255, 0)) # green 박스
cv2.rectangle(rotate_frame, (x1+2, y1+2), (x2-2, y2-2), (0, 255, 0)) # green 박스
>> 얼굴 인식 기반 사용자 키 측정 알고리즘 코드 구현
- 얼굴 인식 기반 사용자 키 측정 알고리즘 설계 방식
- 책상을 위에서 내려다보았을 때 카메라와 사용자의 거리(userDistance)를 계산한다.
- 옆으로 바라보았을 때 카메라와 사용자의 거리(userDistance)와 카메라의 세로 화각을 활용하여 책상 판에서부터 사용자의 높이를 계산한다.
- 2번에서 나온 값과 실제 책상 높이 값을 더해주면 사용자 키를 측정할 수 있다.
- 카메라와 사용자의 거리 계산(userDistance)
- 사용자와 카메라와의 최소 실제 거리 : 70cm
- 사용자와 카메라와의 최대 실제 거리 : 137cm
- 카메라 가로 화각 : 56˚
- 우선, 사람이 카메라의 중앙에 위치해 있을 때 정해진 최대 최소 실제 거리 사이(Dmax, Dmin)에서 얼굴로 인식된 바운딩 박스의 최소 최대 폭 길이(Wmax, Wmin)에 대한 비례식을 세워, 현재 사용자의 인식된 얼굴 폭(W)을 기반으로 거리(UD)를 계산한다.
- 수식 : UD = ((Wmax - W)(Dmax-Dmin)/Wmax-Wmin) + Dmin
- UD : 중앙에 위치했을 때, 카메라와 사용자 측정되는 거리 값
- Dmax, Dmin : 사용자와 카메라와의 최대, 최소 거리 값
- Wmax, Wmin : Dmax, Dmin의 거리에 따른 얼굴 폭 최대, 최소 평균값 (사람마다 얼굴 크기가 다 다르게 측정되기 때문에 데이터를 수집하여 평균화 한 것)
- W : 현재 얼굴로 인식되는 바운딩 박스의 가로 폭
- 수식 : UD = ((Wmax - W)(Dmax-Dmin)/Wmax-Wmin) + Dmin
- 또한, 사람이 카메라의 중앙에 위치하지 않았을 때를 고려하여 카메라와 사용자 사이의 각도를 구한 후, 삼각함수로 활용하게 되는데, 카메라와 사용자 사이의 각도(userHorizon∠)를 구하기 위해 인식된 바운딩 박스의 중심 좌표 X 값(coordinatesX)과 카메라 가로 픽셀(480 pixel)의 중심 값 사이의 픽셀 수를 비례식으로 계산한 후, 카메라 가로 화각만큼 다시 곱해서 카메라와 사용자 사이의 각도(userHorizon∠)를 계산한다.
- 수식 : userHorizon∠ = (|coordinatesX - 480/2| / 480) * 56˚
- userHorizon∠ : 카메라와 사용자 사이의 측정된 가로 각도
- coordinatesX : 바운딩 박스의 가로 중심 X 좌표 값
- 가로 pixel 값 : 480p
- 수식 : userHorizon∠ = (|coordinatesX - 480/2| / 480) * 56˚
- 마지막으로, UD값을 userHorizon∠에 cos을 취해 주어 나누어 주면, 카메라 중앙에서 사용자 사이의 각도만큼 거리(userDistance)를 계산한다.
- 수식 : userDistance = UD/cos(userHorizon∠) (이때, python 코드로 작성할 때에는 라디안 값으로 변환하여 계산하도록 작성하였다.)
- userDistance : 카메라와 사용자 사이의 거리
- UD : 중앙에 위치했을 때, 카메라와 사용자 측정되는 거리 값
- userHorizon∠ : 카메라와 사용자 사이의 측정된 각도
- 수식 : userDistance = UD/cos(userHorizon∠) (이때, python 코드로 작성할 때에는 라디안 값으로 변환하여 계산하도록 작성하였다.)
- Problem> 처음에 특정 사람만 측정한 값만 적용하였을 때, 다른 사람이 측정하였을 때에는 카메라와 사용자 사이의 거리가 정확하게 측정되지 않는 문제점이 있었다.
- Solution> 여러 사람의 데이터를 수집하여 수치를 평균화하여 일반화시켜 개선하였다.

- 책상 판에서부터 사용자의 높이를 계산(userHeight)
- 카메라 세로 화각 :76.667˚
- 여기서도 얼굴의 위치가 위, 아래로 카메라 중심 밖에 위치하는 것을 고려한다. 카메라 중심과 사용자 얼굴의 세로 각도(userVerti∠)를 구하기 위해서, 가로 각도를 구할 때와 마찬가지로 인식된 바운딩 박스의 중심 좌표 Y 값(coordinatesY)과 세로 픽셀(640 pixel)의 중심 값 사이의 픽셀 수를 비례식으로 계산한 후, 카메라 세로 화각만큼 다시 곱해서 카메라 중심과 사용자 사이의 세로 각도를 계산한다.
- 수식 : userVerti∠ = (640/2 - coordinatesY / 640) * 76.667˚
- userVerti∠ : 카메라와 사용자 사이의 측정된 세로 각도
- coordinatesY : 바운딩 박스의 가로 중심 Y 좌표 값
- 세로 pixel 값 : 640p
- 수식 : userVerti∠ = (640/2 - coordinatesY / 640) * 76.667˚
- userVerti∠에 tan를 취해주면 userDistance에 대한 카메라와 사용자 얼굴 사이의 거리 값(Height)을 나타낼 수 있고, userDistance를 다시 곱해줘서 카메라와 사용자 얼굴 사이의 거리 값(Height)을 계산한다.
- 수식 : Height = tan(userVerti∠)*userDistance (이때, python 코드로 작성할 때에는 라디안 값으로 변환하여 계산하도록 작성하였다.)
- Height : 카메라와 사용자 얼굴 사이의 거리 값
- userVerti∠ : 카메라와 사용자 사이의 측정된 세로 각도
- userDistance : 카메라와 사용자 사이의 거리
- 수식 : Height = tan(userVerti∠)*userDistance (이때, python 코드로 작성할 때에는 라디안 값으로 변환하여 계산하도록 작성하였다.)
- 최종적으로 책상의 높이(deskHeight)만큼 Height를 더해주면, 사용자의 키(userHeight)를 측정 할 수 있다.
- 수식 : userHeight = deskHeight + Height
- userHeight : 사용자의 현재 키 측정 값
- deskHeight : 책상 높이
- Height : 카메라와 사용자 얼굴 사이의 거리 값
- 수식 : userHeight = deskHeight + Height

- 위의 알고리즘 코드는 아래와 같다.
'''
brief : 카메라 화면 기반 사용자 신장 유도식
note :
param : faceWidth(얼굴폭), pixelX(좌표X), pixelY(좌표Y), nowHeight(현재 카메라 높이)
return: 계산된 신장높이
'''
def getUserHeight1(faceWidth, pixelX, pixelY, nowHeight):
global faceWidthAverage
faceWidthAverage[0] = faceWidth
sumHeight = 0
for i in range(len(faceWidthAverage)):
sumHeight = faceWidthAverage[i] + sumHeight
widthAverage = sumHeight / timeNum
fullHorizontalAngle = cameraAngle
fullVerticalAngle = fullHorizontalAngle * cameraHeight / cameraWidth
faceDifference = faceWidthMax - faceWidthMin
distanceDifference = userDistanceMax - userDistanceMin
calUserDistance = ((faceWidthMax) - widthAverage) / faceDifference * distanceDifference + userDistanceMin
userTopAngle = abs(pixelX - cameraWidth/2) / cameraWidth * fullHorizontalAngle
userSideAngle = abs(cameraHeight/2 - pixelY) / cameraHeight * fullVerticalAngle
userDistance = (calUserDistance / np.cos(userTopAngle * np.pi/180))/ np.cos(userSideAngle * np.pi / 180)
gap = calUserDistance / userDistance
calUserDistance = ((faceWidthMax - (1 - gap) * 80) - widthAverage) / faceDifference * distanceDifference + userDistanceMin
userDistance = (calUserDistance / np.cos(userTopAngle * np.pi / 180)) / np.cos(userSideAngle * np.pi / 180)
cameraUserAngle = (cameraHeight/2 - pixelY) / cameraHeight * fullVerticalAngle
calHeight = np.sin((cameraUserAngle + deskAngle) * np.pi/180) * userDistance# abs(np.sin((cameraUserAngle + deskAngle) * np.pi/180))* 15
for i in range(timeNum-1):#shift array
faceWidthAverage[timeNum-1-i] = faceWidthAverage[timeNum-2-i]
return nowHeight + calHeight
>> 높낮이 조절을 위한 모터 제어 코드 작성 및 가속도-자이로 센서 코드 작성
- 사용자의 키 변화량에 따라 높낮이 조절을 해주기 위해 모터 제어 코드를 작성하였다.
- 먼저, 모터 드라이버 동작 설정을 해주는 코드이다.
'''
brief : 모터 드라이버 설정
note : 0/stop , 1/down, 2/up
param : enA(모터A 펄스), motorA(모터 방향), motorB(모터 방향), enB(모터B 펄스)
return: 변경완료여부
'''
def driverSet(enA, motorA, motorB, enB):
global initial, nowTime, preTime
if initial == True:
preTime = time.time()
initial = False
changePWM(0, 0)
for i in range(1, len(driver)-1):
GPIO.output(driver[i], 0)
if nowTime - preTime > 0.5: # 작동 딜레이 드라이버 보호용
if motorA == 2:#up
GPIO.output(driver[1], 0)
GPIO.output(driver[2], 1)
elif motorA == 1 :
GPIO.output(driver[1], 1)
GPIO.output(driver[2], 0)
else:#stop
GPIO.output(driver[1], 0)
GPIO.output(driver[2], 0)
if motorB == 2:#up
GPIO.output(driver[3], 0)
GPIO.output(driver[4], 1)
elif motorB == 1 :
GPIO.output(driver[3], 1)
GPIO.output(driver[4], 0)
else:#stop
GPIO.output(driver[3], 0)
GPIO.output(driver[4], 0)
changePWM(enA, enB)
initial = True
preTime = nowTime
return True
else:
return False
- 여기서 만약, 책상 위에 물건들로 인해 책상 한쪽 다리에 부하가 걸리게 되면 책상이 기울어지는 현상이 발생할 수 있다. 이 문제를 방지하기 위해 가속도-자이로 센서를 활용하여 기울어짐을 측정하고, 가속도-자이로 센서 측정값에 따라 주기적으로 양쪽 모터의 PWM 값을 변경하여 책상 자세를 유지하였다.
- 아래 코드는 가속도-자이로 센서로 측정한 값을 바탕으로 기울어진 각도를 측정하는 코드이다. 가속도 센서와 자이로 센서의 장점을 가지고 상호 보완시켜주는 상보필터를 적용하여 각도 값을 계산하였다. (아래 코드 외에 센서 동작을 위한 기본 설정 코드는 깃허브 코드를 통해 확인해 볼 수 있다.)
- ※ 가속도-자이로 센서와 라즈베리파이 보드와의 통신 방식은 I2c 방식으로 구현하였다.
'''
brief : 가속도, 각속도를 이용해서 각 도출 (계산 및 상보필터)
note :
param : accel X, accel Y, accel Z, Gyro X, Gyro Y, Gyro Z
return: angle X, angle Y, angle Z 상보필터값
'''
def calGyro(accelX, accelY, accelZ, GyroAccX, GyroAccY, GyroAccZ):
global GyX_deg, GyY_deg, GyZ_deg
global past1 #기존 시간값 충돌방지
FS_CEL = 131 #3 Full-Scale Range 0=131, 1=65.5, 2=32.8, 3=16.4
now = time.time()
dt = (now - past) / 1000.0
# convert gyro val to degree
gyro_x = (GyroAccX - baseGyX) / FS_CEL
gyro_y = (GyroAccY - baseGyY) / FS_CEL
gyro_z = (GyroAccZ - baseGyZ) / FS_CEL
# compute
gyroAngleX = gyro_x * dt + GyX_deg
gyroAngleY = gyro_y * dt + GyY_deg
gyroAngleZ = gyro_z * dt + GyZ_deg
# calculate Gyro
RADIANS_TO_DEGREES = 180 / math.pi
accelAngleX = math.atan(accelY / math.sqrt(math.pow(accelX, 2) + math.pow(accelZ, 2))) * RADIANS_TO_DEGREES
accelAngleY = math.atan(-1 * accelX / math.sqrt(math.pow(accelY, 2) + math.pow(accelZ, 2))) * RADIANS_TO_DEGREES
accelAngleZ = 0
# complementary Filter
alpha = 0.56
GyX_deg = alpha * gyroAngleX + (1.0 - alpha) * accelAngleX
GyY_deg = alpha * gyroAngleY + (1.0 - alpha) * accelAngleY
GyZ_deg = gyroAngleZ
past1 = now
return GyX_deg, GyY_deg, GyZ_deg
- 위에서 측정되는 각도에 따라 책상의 좌우 모터 PWM 값을 변경해주는 코드이다.
'''
brief : 각도 기반 상판 자세유지
note : 가중치 가변 방식
param : nowAngle(현재 각도), comepareAngle(기준이 될 각도)
return: 각 모터 펄스 주기
'''
pwmA = 100
pwmB = 100
pwmA_AVG = 0
pwmB_AVG = 0
preMotorState = 0
def HorizontalHold(nowAngle, compareAngle):
global pwmA, pwmB, preMotorState, pwmB_AVG, pwmA_AVG
angleDiff = nowAngle - compareAngle
if actionPre == 2:
if angleDiff > 0 and preMotorState == 1:
preMotorState = 0 #각 차값이 양수
pwmA = 100
pwmB = 100
elif angleDiff > 0:
if pwmA > 20:
pwmA -= 5
preMotorState = 0
elif angleDiff < 0 and preMotorState == 0:
preMotorState = 1 #각 차값이 음수
pwmA = 100
pwmB = 100
elif angleDiff < 0:
if pwmB > 20:
pwmB -= 5
preMotorState = 1
elif actionPre == 0:
if angleDiff > 0 and preMotorState == 1:
preMotorState = 0 #각 차값이 양수
pwmA = 100
pwmB = 100
elif angleDiff > 0:
if pwmB > 20:
pwmB -= 5
preMotorState = 0
elif angleDiff < 0 and preMotorState == 0:
preMotorState = 1 #각 차값이 음수
pwmA = 100
pwmB = 100
elif angleDiff < 0:
if pwmA > 20:
pwmA -= 5
preMotorState = 1
if TESTMODE == True:
alpha = 0.6
else:
alpha = 0.92
pwmA_AVG = alpha * pwmA_AVG + (1 - alpha) * pwmA
pwmB_AVG = alpha * pwmB_AVG + (1 - alpha) * pwmB
changePWM(pwmA_AVG, pwmB_AVG)
return pwmA_AVG, pwmB_AVG
>> 초음파 센서 코드 작성
- 초음파 센서의 경우 책상의 현재 높이를 측정하기 위해 활용하였다.
- Problem> 초음파 센서 측정값의 경우, 오차가 큰 편이기 때문에 책상 높이의 이상치가 자주 발생하였다.
- Solution> 측정값을 일정 개수만큼 평균값으로 갱신하여 이상치에 대한 오차를 줄이는 방법으로 문제점 해결하였다. (이동평균필터 활용)
- 초음파 동작 코드는 다음과 같이 작성하였다.
'''
brief : 초음파 센서 거리측정
note :
param :
return: 계산된 거리
'''
def waveFunc() :
GPIO.output(wave[0], True)
time.sleep(0.00001)
GPIO.output(wave[0], False)
while GPIO.input(wave[1]) == 0 :
pulse_start = time.time()
# pulse_end = pulse_start
while GPIO.input(wave[1]) == 1 :
pulse_end = time.time()
# if pulse_end - pulse_start > 1:
# return 0
pulse_duration = pulse_end - pulse_start
distance = pulse_duration * 17000
distance = round(distance, 5)
return distance
>> 디스플레이 기능 설정 코드 제작 및 추가 기능 구현
- 추가 기능
- 버튼 입력으로도 높이를 조절할 수 있게 제작한다.
- 책상을 사용하는 동안 수면 방지를 위해 일정 시간 안에 수면 checking을 하는 기능을 추가하였다.
- 버튼 입력을 통해 모터를 제어하는 방식은 위의 모터 제어 코드를 기반으로 동작하게 된다. 그렇기 때문에 버튼을 통해 높낮이를 조절하게 되어도 책상 수평은 일정하게 유지할 수 있다.
- 또한, 수면 체크 기능의 동작 방식은 사용자가 업무를 볼 때, 얼굴이 일정 시간동안 확인이 되어지지 않는다면, 소리로 알림을 주는 기능이다.
- 이러한 기능은 디스플레이를 통해 비동기식으로 구현하여 사용자가 원할 때 활성화시켜 동작할 수 있게끔 구현하였다.
- 해당 코드는 깃허브를 통해 확인해볼 수 있다.
- ※ OLED 디스플레이와 라즈베리파이와의 통신 방식은 SPI 방식으로 구현하였다.

5. 활동 결과
- 평가1 : 사용자 키 측정 알고리즘을 통해 실제 사용자 키와 수치 비교를 해보았다.
- 다음 표는 실제 키와 추정된 키를 비교하고 얼마나 오차가 나는지 확인해볼 수 있었다.
실제 키(cm) | 측정 키(cm) | 키 오차(cm) | |
1 | 180.3 | 180.5 | +0.2 |
2 | 168 | 169.5 | +1.5 |
3 | 171 | 170 | -1 |
4 | 179 | 178 | -1 |
- 결과1 : 코드 작성하기 전에 목표로 설정했던 오차범위는 위 아래로 5cm 이내로 측정되는 것이었는데, 예상보다 더 작은 오차 값으로 나오는 것을 확인해볼 수 있었다.
- 평가2 : 측정된 사용자 키에 맞게 조절된 책상 높이 변화를 확인해보았다.
- 다음 표는 하이닥에서 사용자의 키에 따른 책상 적정 높이 산정 방식을 참고하여 측정된 값이다.
책상 상향 목표 높이 (cm) | 책상 상향 높이 제어 결과 (cm) | 책상 상향 높이 오차 (cm) | 책상 하향 목표 높이 (cm) | 책상 하향 높이 제어 결과 (cm) | 책상 하향 높이 오차 (cm) | |
1 | 106.5 | 107 | +0.5 | 73.8 | 74 | +0.2 |
2 | 99.12 | 100 | +0.88 | 68.88 | 74 | +1.12 |
3 | 100.89 | 101 | +0.11 | 70.11 | 74 | +3.89 |
4 | 105.61 | 106 | +0.39 | 73.39 | 74 | +0.61 |
- 결과2 : 사용자 키에 맞춰 책상 높이에 대한 오차는 1cm 이내로 측정하는 것을 목표로 하였고, 상향 높이 조절을 하는 것에 있어서는 목표한 만큼 오차가 크지 않을 것을 확인해볼 수 있었다. 하향의 경우는 처음 책상을 설계할 때 기본적으로 책상 높이가 74cm 였기 때문에, 더 낮게 높이를 제어할 수 없었기에 결과값이 전부 기존 책상 높이만큼 나온 것을 확인해 볼 수 있다.
- 아래 그래프는 사용자의 키와 초음파 센서를 활용한 책상 높이, 가속도-자이로 센서 측정값, 모터 PWM 변화에 대해 그려진 그래프이다.
- 왼쪽 상단 : 사용자의 키
- 오른쪽 상단 : 초음파 센서를 활용한 책상 높이
- 왼쪽 하단 : 가속도-자이로 센서 측정값
- 오른쪽 하단 : 모터 PWM 변화
- 가속도-자이로 센서의 경우 초기에 값이 튄 것을 볼 수 있는데, 처음에 모터가 동작할 때 책상의 구조가 다소 튼튼하지 못했던 점이 있어 크게 흔들림이 발생하는 부분 때문에 발생한 문제였던 것 같다.
- 이후에 동작했을 때에는 다시 정상적으로 동작하는 것을 볼 수 있었다.

6.마무리
위에서 개별 기능에 대해 따로 작성하였지만, 통합한 코드는 깃허브 smartdesk.py 코드를 참고하면 된다.
그리고, 이 졸업 작품 내용을 바탕으로 논문 대회에도 참가해보았다.
https://www.dbpia.co.kr/journal/articleDetail?nodeId=NODE11183902
딥러닝 기반 얼굴인식을 이용한 자동 책상 높이와 균형 제어 시스템의 구현 | DBpia
배현한, 황기주, 임용석, 신진호 | Proceedings of KIIT Conference | 2022.12
www.dbpia.co.kr
위에서 보였던 활동 내용에서 파이썬 언어로 인공지능 기술을 활용한 임베디드 개발을 경험해 본 결과, 개인적으로 프로그래밍 개발 역량이 크게 오른 느낌이다. 📈