19주차에는 PID제어와 다투는 한 주였습니다.
지난 주에는 Laser 하드웨어 연결 구성,그리고 노이즈 해결 및 제어 기초 코드를 작성하였는데요 금주에는 영상에서의 레이저 포인트를 바탕으로 객체를 쫓아갈 수 있도록 하는 PID 제어를 구현했습니다 .

하지만 너무나도 쉽지 않은 task에 두손 두발을 다 들어버렸다는 ....

그럼 바로 고군분투 이야기를 찾아가보도록 하겠습니다.!!!

PID 제어란 ?

그러면 PID 제어가 무엇인지 한번 Gemini에게 물어봅시다 !

PID 제어는 제어 대상의 출력값을 목표값과 일치시키기 위해 사용하는 가장 대표적인 피드백 제어 알고리즘입니다.

  1. P (Proportional): 비례 제어
    "현재의 오차에 집중하자"

비례 제어는 현재 오차(e(t)e(t))의 크기에 비례하여 제어량을 조절합니다. 오차가 크면 크게 움직이고, 작으면 작게 움직이는 가장 직관적인 방식입니다.

  • 특징: 오차에 즉각적으로 반응합니다.
  • 한계: 잔류 편차(Offset)가 발생합니다. 목표치에 가까워질수록 제어량이 너무 작아져서, 완전히 목표값에 도달하지 못하고 일정한 오차를 남긴 채 평형을 이뤄버리는 현상입니다.
  1. I (Integral): 적분 제어
    "누적된 과거의 오차를 해결하자"
    P 제어에서 해결하지 못한 '잔류 편차'를 없애기 위해 사용합니다. 과거부터 쌓여온 오차의 합계를 계산하여 제어량에 반영합니다.
  • 특징: 아주 미세하게 남아있는 오차도 시간이 지나면 누적되어 제어량을 키우므로, 결국 목표값에 정확히 도달하게 만듭니다.
  • 한계: 오차가 계속 쌓이다 보니 반응이 다소 느려지거나, 목표치를 지나쳐 버리는 오버슈트(Overshoot)가 발생할 수 있습니다.
  1. D (Derivative): 미분 제어
    "미래의 오차를 예측하여 대비하자"
    오차가 변화하는 속도(기울기)를 보고 급격한 변화를 억제합니다. 자동차 브레이크처럼 급하게 목표치에 접근할 때 속도를 줄여주는 '댐퍼' 역할을 합니다.
  • 특징: 오버슈트를 줄여주고 시스템의 안정성을 높여줍니다.
  • 한계: 노이즈(갑작스러운 신호 변화)에 매우 민감하게 반응하여 시스템이 떨리는 등 불안정해질 수 있습니다.

비교

항목명칭중심 관점주요 역할부작용
P비례현재빠른 응답성 확보잔류 편차 발생
I적분과거잔류 편차 제거오버슈트 발생 가능
D미분미래오버슈트 억제 (안정화)노이즈에 민감함

앞서 카메라 영상에서 레이저 포인터를 추출하였듯이 목표 객체와 픽셀간의 오차를 이용하여 제어를 시도하는 P제어 먼저 시도를 했습니다.

초기에는 영상 자체에서 오차를 구해 PID 항을 계산해 pwm 값만 esp8266으로 전송했지만 os 내부에서의 시간 엄격성과 계산량 분산을 위해 STM으로 PID 연산 loop를 넘겼습니다 .


위치형 VS 증분형 PID

1. 두 방식의 가장 큰 차이 한 줄 요약

  • 위치형 PID
    → 매번 “현재 오차에 비례해서 중립 위치에서 얼마나 떨어져야 하는지”를 직접 계산
    출력 = 중립값 + Kp×오차 + Ki×누적오차 + Kd×오차변화율

  • 증분형 PID
    → 매번 “이번에 얼마나 더 움직여야 할지(변화량)”만 계산해서 이전 출력에 더함
    이번 변화량 = Kp×오차 + Ki×오차 + Kd×(오차-이전오차)
    출력 = 이전 출력 + 이번 변화량

2. 증분형 PID를 썼을 때 제가 겪은 문제들

프로젝트 초반에 증분형으로 구현했을 때 로그는 거의 이런 패턴이었습니다.

ex: 45.0 → out: 1430 → 1450 → 1470 → 1490 → … → 2200 (끝까지 감)
ex: -120.0 → out: 1250 → 1220 → 1190 → … → 800 (끝까지 감)
  • 오차가 같은 방향으로 오래 지속되면 PWM이 계속 한쪽으로만 쌓여서 범위 끝(800 or 2200)에 고정됨
  • 목표에 도착했는데도 오차가 조금 남아 있으면 계속 미세하게 더 움직이려고 해서 떨림 발생
  • 오차가 갑자기 반대 방향으로 바뀌면 이전 누적분 때문에 반응이 매우 느림 (한쪽으로 치우친 상태에서 출발)

3. 위치형을 선택한 결정적 이유 2가지

  1. 서보는 절대 위치 제어 장치이기 때문
    RC 서보는 본질적으로 “특정 PWM 값 = 특정 각도”라는 절대 위치 장치입니다.
  2. 오차가 갑자기 반대 방향으로 바뀌면 즉시 반응
    → 이전 누적값이 없기 때문에 바로 새로운 위치로 점프
    → 빠른 방향 전환 시 지연이 거의 없음

P 제어의 한계

왔다 갔다 / 느린 추종 / 정확도 문제

1. 오차가 클 때 발생하는 진동/오버슈트 패턴

  • 현상
    오차가 크면 KpeK_p \cdot e 항이 커져서 PWM 변화량이 급격히 증가 → 목표 근처에 도달 → 오차 방향이 반전 → 다시 반대쪽으로 크게 움직임 → 반복

기본 위치형 PID 출력

u(k)=ucenter+Kpe(k)+Kie(k)Δt+Kde(k)e(k1)Δtu(k) = u_{\text{center}} + K_p \cdot e(k) + K_i \cdot \sum e(k) \cdot \Delta t + K_d \cdot \frac{e(k) - e(k-1)}{\Delta t}

  • 오차 e(k)∣e(k)∣가 크면 → Kpe(k)Kp⋅e(k)가 매우 커짐
    → 한 번에 PWM이 50~100us 이상 변화 → 서보가 목표를 지나침 (overshoot)
  • 지나친 후 오차가 반대 부호로 바뀌면 → 이번에는 반대 방향으로 큰 보정
    → 다시 지나침 → 헌팅(좌우 진동) 발생

주요 원인

  • KpK_p 가 너무 큼 → 초기 응답 과도
  • KdK_d가 없거나 작음 → 변화율 억제 부족
  • 서보 관성 + 백래시 + 카메라 지연 → 실제 도착 시 오차가 과다 보정됨

2. 작은 오차에서 발생하는 정특성 오차 (steady-state error)

현상
오차가 5~20px 정도로 작아지면 PWM 변화가 거의 없어서 항상 그 오차를 남긴 채 멈춤

정상상태(steady-state)에서 오차가 일정
e()=상수dedt=0,e=일정e(∞)=상수⇒dedt=0,∑e=일정

위치형 PID의 정상상태 출력:
u()=ucenter+Kpe()+Kie()u(∞)=ucenter+Kp⋅e(∞)+Ki⋅∑e(∞)

  • Ki=0K_i = 0 또는 매우 작을 때 → u()ucenter+Kpe()u(∞)≈ucenter+Kp⋅e(∞)
    → 작은 오차 e(∞)를 없애려면 Kp가 커야 하지만, Kp를 크게 하면 위 진동이 심해짐
    → 결국 작은 오차를 못 잡고 남김 (정특성 오차 발생)

주요 원인

  • KiK_i 부족 → 정상상태 오차 제거 불가
  • KpK_p를 크게 못 함 (진동 때문에)

요런 문제를 가지고 있다 ...

그래서 I와 D항을 추가하여 overshoot을 해결하면서 settle time을 줄이기 위해 P항을 증가시키며 실험적 결과를 얻어내려고 노력했다 .
어쩌면 핑계일 수 있지만 음 P,I,D 최적의 값을 일일이 찾아내는 것은 유의미할지 모르지만 시간이 조금 부족하다고 느껴졌다.

그래서 우선은 라즈베리로 모터를 옮겼다. 😭😭
사용하는 servo motor는 절대적인 PWM 값으로 위치를 제어하기에 Raspi에서 수신하는 오차와 pwm값으로 최적의 P,I,D 계수를 찾기 위함이었다 .


Raspi와 Auto Tune ...

pysysid

“실제 로그 데이터(입력 PWM + 출력 오차)를 먹이면 시스템의 전달함수를 자동으로 추정해주고, 그 모델로 최적 PID 게인을 뽑아주는 Python 라이브러리”

왜 이걸 쓰면 편한가?

1. 로그만 주면 끝
STM32에서 찍은 PIDLOG (t, ex, out_x 등)를 txt로 떨구면 → pysysid가 자동으로 1차/2차 모델 fitting

2. 전달함수(G(s))를 먼저 알아줌
실제 서보 + 카메라 + 지연까지 포함된 전체 플랜트의 K(게인)와 τ(시간상수)를 뽑아줌
→ “우리 시스템은 실제로 이런 응답을 하네”를 숫자로 알 수 있음

3. 그 모델로 PID 자동 설계
control.pidtune()이나 Ziegler-Nichols, pole placement 등 여러 방법으로 Kp/Ki/Kd 후보를 바로 계산
→ “이론상 최적” 값이 뚝딱 나옴 (미세 조정은 필요)

이론 상 최적의 값이 뚝딱 나올 줄만 알았다 .

ISSUE

  • 레이저 위치 값 오차 지연
    현재 레이저의 위치는 들어오는 영상에서 HSV 변환과 Masking, Morphology 연산을 거쳐 레이저 포인터를 탐지하고 3프레임 당 한번씩 ESP혹은 라즈베리파이 ethernet 통신이 이루어졌다. 통신 과정에서 발생하는 지연은 적을지라도 전체 영상 처리, 그리고 수신 및 연산 과정은 더 길어질 수 있다.

  • 레이저 탐지
    실시간 탐지 중에서도 레이저 포인터가 가끔 오탐지가 되는 경우가 생겼다
    이 때 갑자기 확 늘어난 오차가 전달되며이는 최적의 값을 찾는데 큰 noise가 되었다..

이런 연유로 Raspi-python 라이브러리를 활용한 최적값 탐지는 나에게 절망감을 선사했다. 동일한 환경을 구성하기 위해 wifi 연결부터 코드 이식까지 환경 구성에 하루를 소모했기 때문에 ..

log를 분석하며 제대로 된 값을 얻어낼 수 있었을 수도 있지만 우선은 앞으로 나아가는 길을 선택했다. PID 최적의 값을 얻어냈다면 어땠을까 하는 아쉬움을 남기며 기존의 P,I,D 값을 바탕으로 LUT를 만들었다

PID -> LUT

서보모터는 절대적인 pwm 값으로 각도가 결정된다.
이를 활용하여 화면을 11X19개의 그리드로 나누고 각 그리드의 중앙값을 PID 제어를 통해 레이저로 Pointing 하는 자동화 코드를 완성했다 .

이 과정에서도 화면의 가장자리로 갈 수록 레이저 탐지 확률이 줄어들어 코드 수정을 몇번이나 반복했다 .

데이터 흐름

[RTSP/카메라] → rtsp_laser_demo (e_u, e_v stdout)
       ↓
  ubuntu_tcp_server (stdin → "EX=float,EY=float\n" TCP 전송)
       ↓
  라즈베리 파이 pid_pwm_agent.py (TCP 클라이언트, 50Hz PID → pwm0/pwm1)
       ↓
  GPIO12(pwm0)=X, GPIO13(pwm1)=Y → 서보/레이저 구동

자동 LUT 수집 모드 (AutoLUT)

  • Host: rtsp_laser_demo --lut-auto

    • 그리드 정의

      • LUT_GRID_ROWS = 11, LUT_GRID_COLS = 19
      • 각 셀 중심을 하나의 LUT 포인트로 사용
    • AutoLutCalibrator

      • 현재 셀 인덱스: idx(gr(), gc())
      • 현재 셀 중심에 대응하는 TargetROI 생성:
      • currentTarget(W, H) 에서 (gc+0.5) * W/cols, (gr+0.5) * H/rows
      • 수렴 판정:
        • 조건: |e_u|,|e_v| ≤ SETTLE_THRESH_PX (기본 6px)
        • 시간을 settle_accum_s 에 누적
        • SETTLE_TIME_SEC (예: 3초) 이상이면 “수렴”으로 간주
    • PWM 요청 및 LUT 저장 흐름

    1. Host가 AutoLUT 모드에서, 특정 셀에 대해 수렴 조건 만족:
      • lut.updateConvergence(e_u, e_v, dt)true 를 반환
    2. Host는 Raspi에 현재 PWM 요청:
      • REQUEST_PWM,GR=X,GC=Ystdout 으로 송신
    3. ubuntu_tcp_server:
      • REQUEST_PWM,... 라인은 가공 없이 TCP로 그대로 전달
    4. Raspi pid_pwm_agent.py:
      • recv_loop 에서 REQUEST_PWM_RE 로 매치
      • shared.last_ux_us, shared.last_uy_us 를 읽어
      • PAN=xxxx,TILT=yyyy 형태로 TCP로 응답
    5. Host:
      • /tmp/lut_pwm_response FIFO 에서 PAN/TILT 응답을 읽음
      • lut_data.json
      • {"grid_r":r,"grid_c":c,"target_u":...,"target_v":...,"pan_us":...,"tilt_us":...}
      • 형태로 한 포인트를 즉시 저장 (전체 파일 리라이트)
      • 다음 셀(idx+1) 로 넘어감
    • 수동 레이저 위치 지정 (마우스 클릭)
    • AutoLUT 중 레이저 탐지가 어려운 셀의 경우:
      • 마우스로 이미지 상 레이저가 실제 있는 위치를 클릭
      • g_manual_laser_pending=true, g_manual_laser_pt 설정
      • 다음 프레임에서:
        • laser.found = true, laser.point = g_manual_laser_pt
        • AutoLUT는 이 좌표를 기준으로 바로 PWM 요청·저장 수행
        • 이 프레임에서는 일반 EX/EY 전송을 스킵 (skip_periodic_send=true)

    ====> 가장자리에서 레이저 포인터가 탐지되지 않는 경우를 해결 ❗❗❗

Raspi: pid_pwm_agent.py --lut-mode

  • PID 제어를 사용하여 실제 레이저를 타겟 중앙에 수렴시키고,
    각 셀마다 “오차 이내”일 때 AutoLUT가 PWM을 가져가도록 돕는 모드.

result


이렇게 각 그리드에서 값을 탐지하고 이후 보간법을 이용해 화면 내에서 동일한 픽셀의 경우 빠른 추종이 가능하도록 만들었다.

하지만 추후 발생할 수 있는 문제로는 카메라와 객체의 거리가 달라질 경우 레이저의 각도가 미묘하게 달라질 필요가 있다.

이는 자동 캘리브레이션 코드와 LUT+PID 코드 구현으로 문제점을 해결해 나갈 생각이다.

현재 시스템의 tracking 성능 수준 (로그 기반 평가)

🍎좋은 점
- 위치형 PID로 전환 후 → 오차=0 시 정확히 멈춤
- 시간이 지나면 제자리로 수렴 → 기본적인 closed-loop 동작 확인

☠️아직 부족한 점
- settling time (오차가 줄어드는 시간)이 1~2초 이상 
      → 사람 0.5m/s만 움직여도 따라가지 못함
- 레이저 탐지 불안정 → 오차가 순간 0이 되거나 튀면 PID가 혼란
- 지연 (RTSP + TCP + 3프레임 전송) → PID가 과거 오차로 계산

+칼만 필터

실시간으로 객체를 추적하기 위해 위치 예측을 추가했다. 아직은 간단한 2D 칼만 필터를 추가했지만 추가로 고도화 할 계획이다.

시도

  • 호스트 PC(리눅스에서도 프레임별 연산 외에 비동기로 제어 주기와 동일하게 신호 전송 =. 엄격한 시간 스케줄링)
  • LUT + PID
  • pysysid 라이브러리 최적값 탐지
  • 카메라 캘리브레이션 및 거리 추정

추가로

공채 시즌이 시작되면서 더욱이 바쁜 나날을 보내고 있습니다 ..

죽는 느낌.. ALL RIGHT .!!

짜투리 ...

살자 ..

profile
세상의 어려운 문제를 해결하자

0개의 댓글