저번 게시물에서 관절의 위치를 이용해 아바타의 움직임을 구현해 보았다.
이번에는 mediaPipe 모듈로 실시간 감지한 관절의 위치를 유니티에 전송해서 데이터를 잘 받아올 수 있는지 확인해 보겠다.
import mediapipe as mp
import socket
import time
import cv2
mpPose = mp.solutions.pose
pose = mpPose.Pose()
# 0은 웹캡
cap = cv2.VideoCapture(0)
# 이전 프레임 처리 시간 저장 변수 초기화
pTime = 0
# 출력하고 싶은 관절들의 인덱스
desired_indices = [0, 11, 12, 13, 14, 15, 16, 23, 24, 25, 26, 27, 28]
# TCP 서버 생성
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_socket.bind(('localhost', 포트번호))
server_socket.listen(1) # 최대 1개의 연결을 기다림
print("서버가 연결을 대기 중입니다.")
while True:
# 클라이언트 연결 대기
try:
client_socket, addr = server_socket.accept()
print(f"{addr}에서 연결이 수락되었습니다.")
# 문자열 대기
start_message = client_socket.recv(5).decode()
if start_message == 'start':
start_message = ''
# 실시간을 위해 무한 루프 돌려 줍니다
while True:
# 바이트 배열 초기화
coordinates_bytes = bytearray()
# 성공여부, 이미지
success, img = cap.read()
# 이미지 크기 축소
new_width = 480 # 가로 크기
new_height = 640 # 세로 크기
img_small = cv2.resize(img, (new_width, new_height))
#rgb로 변경 (mediapipe는 rgb 이미지를 사용)
imgRGB = cv2.cvtColor(img_small, cv2.COLOR_BGR2RGB)
results = pose.process(imgRGB)
# print("시작")
if results.pose_world_landmarks:
for desired_idx in desired_indices:
landmark = results.pose_world_landmarks.landmark[desired_idx]
x = landmark.x
y = landmark.y
z = landmark.z
# 선택한 관절 지점의 정보 출력
print(f"{x:.5f} {y:.5f} {z:.5f}".format(x,y,z))
# 좌표값을 바이트 배열에 추가
coordinates_bytes += f"{x:.5f} {y:.5f} {z:.5f}\n".format(x, y, z).encode()
coordinates_bytes = coordinates_bytes.rstrip(b'\0')
# 바이트 배열로 변환된 좌표값을 Unity 서버로 전송
coordinates_length = len(coordinates_bytes)
length_bytes = coordinates_length.to_bytes(4, byteorder='big')
# 길이 정보 전송
client_socket.sendall(length_bytes)
# 바이트 배열로 변환된 좌표값을 Unity 서버로 전송
client_socket.sendall(coordinates_bytes)
cTime = time.time()
fps = 1 / (cTime - pTime)
pTime = cTime
# 이미지에 프레임 속도 표시
# cv2.putText(img, str(int(fps)), (70, 50), cv2.FONT_HERSHEY_PLAIN, 3, (255, 0, 0), 3)
cv2.imshow("image", img)
if cv2.waitKey(10) & 0xFF == ord('q'): # 'q' 키를 누르면 루프 종료
break
except Exception as e:
print(f"연결 수락 중 오류 발생: {e}")
# 연결 종료
client_socket.close()
server_socket.close()
cap.release()
cv2.destroyAllWindows()
public class FinalTest : MonoBehaviour
{
public static Socket sock;
string serverIP = "123.45.67.89";
int port = 9999;
bool socketConnect = false;
string startPoint = "start";
public Animator anim;
StoreJointData storeData;
MoveAvatar moveAvatar;
Vector3[] realJoint = new Vector3[13];
string[] textLine;
string[] splitXYZ;
private void Start()
{
anim = GetComponent<Animator>();
storeData = new StoreJointData(anim);
moveAvatar = new MoveAvatar();
try
{
// TCP 소켓 객체 생성
sock = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
// 서버에 연결하기
IPEndPoint serverEP = new IPEndPoint(IPAddress.Parse(serverIP), port);
sock.Connect(serverEP);
socketConnect = true;
Debug.Log("Connect Success");
// 서버에 "Start" 문자열 전송
byte[] buff = Encoding.UTF8.GetBytes(startPoint);
sock.Send(buff);
}
catch (Exception e)
{
Debug.Log("오류: " + e);
}
}
private void Update()
{
try {
if (socketConnect)
{
// 전송 받을 바이트 수 먼저 받아오기
byte[] buffer = new byte[4];
int byteCount = sock.Receive(buffer);
Array.Reverse(buffer);
int dataByteCount = BitConverter.ToInt32(buffer, 0);
// 받으려고 하는 데이터의 수만큼 byte 배열 선언 후 데이터 저장
byte[] receivedBuffer = new byte[dataByteCount];
byteCount = sock.Receive(receivedBuffer);
string msg = Encoding.UTF8.GetString(receivedBuffer, 0, byteCount);
textLine = msg.Split("\n");
for(int i = 0; i < textLine.Length - 1; i++)
{
string line = textLine[i];
splitXYZ = line.Split(' ');
if (splitXYZ.All(x => x is string))
{
realJoint[i].x = float.Parse(splitXYZ[0]);
realJoint[i].y = float.Parse(splitXYZ[1]);
realJoint[i].z = float.Parse(splitXYZ[2]);
}
else
{
realJoint[i].x = 0f;
realJoint[i].y = 0f;
realJoint[i].z = 0f;
}
}
}
}
catch(Exception e)
{
Debug.Log("오류 : " + e);
socketConnect = false;
sock.Close();
}
}
}
13개의 관절 좌표값을 올바르게 전달 받지 못했다.
-0.02801599 1.530401 0.01000143
0.1409548 1.397012 0.02304516
-0.1969829 1.397008 0.01986888
0.37999 1.397016 0.02304507
-0.4362946 1.397007 0.01986932
0.6318882 1.39702 0.02304519
-0.688216 1.397006 0.01986977
0.0739904 0.884761 0.002631317
-0.1300061 0.884761 0.002718598
0.06866307 0.4908463 0.009188599
-0.124666 0.4908559 0.007917952
0.07400247 0.1169901 0.0270884
-0.1299936 0.1169791 0.02709265
통신할 때 13개의 관절 x y z 좌표값이 위와 같은 형태로 전달 되어야 배열에 값이 잘 저장될 수 있다.
-0.02801599 1.530401 0.0100
0.1409548 1.397012 0.02304516345
-0.1969829 1.397008 0.019868883
0.37999 1.397016 0.023045073535
-0.4362946 1.397007 0.0198
0.6318882 1.39702 0.02304519
-0.6 1.397006 0.01986977
0.0739904 0.884761 0.002631317
-0.1300061 0.884761 0.002718598
0.06866307 0.4908463
이처럼 x, y, z값 중에 z값이 없거나, 13개의 관절 위치값을 받아오지 못하는 일이 발생했다.
원인 :
mediaPipe 모듈로 관절의 위치가 매우 미세하게 측정되어 3차원 좌표값의 길이가 매 프레임마다 달라졌기 때문에 일정한 데이터의 길이로 데이터를 전송 받기 어려웠다.
해결 :
전송 받을 바이트 수를 먼저 받아온 후, 그 바이트 수만큼의 배열을 생성해 데이터를 저장했다.
// 전송 받을 바이트 수 먼저 받아오기
byte[] buffer = new byte[4];
int byteCount = sock.Receive(buffer);
Array.Reverse(buffer);
int dataByteCount = BitConverter.ToInt32(buffer, 0);
// 받으려고 하는 데이터의 수만큼 byte 배열 선언 후 데이터 저장
byte[] receivedBuffer = new byte[dataByteCount];
byteCount = sock.Receive(receivedBuffer);
그런데 이 방식은 크기가 다른 배열을 매 프레임마다 새로 생성하기 때문에 힙 메모리를 낭비할 수 있는 문제가 있다.
코드를 수정한다면, 파이썬 서버에서 관절 데이터를 보내기 전에 일정한 길이의 데이터만 보내고, 유니티에서는 배열을 하나만 생성해서 생성한 메모리를 재사용 할 수 있도록 하는 것이 좋을 듯 하다.
원인 : 바이트의 순서가 바뀌어 올바른 값을 얻지 못했다.
파이썬 측에서 보내려고 하는 바이트 수를 먼저 보낸다.
# 바이트 배열로 변환된 좌표값을 Unity 서버로 전송
coordinates_length = len(coordinates_bytes)
length_bytes = coordinates_length.to_bytes(4, byteorder='big')
# 길이 정보 전송
client_socket.sendall(length_bytes)
# 바이트 배열로 변환된 좌표값을 Unity 서버로 전송
client_socket.sendall(coordinates_bytes)
to_bytes()로 전송하려고 하는 바이트의 길이를 빅 엔디안(big endian)으로 저장해서 보냈지만, 유니티 측에서는 리틀 엔디안(little endian)으로 받게 되어 올바른 값을 확인하기 힘들었다.
해결 :
// 전송 받을 바이트 수 먼저 받아오기
byte[] buffer = new byte[4];
int byteCount = sock.Receive(buffer);
Array.Reverse(buffer);
유니티에서 전송받은 데이터를 Array.Reverse() 로 이용해 받은 바이트를 역순으로 바꾼다. 바꾼 값을 확인해 봤을 때 파이썬에서 전송했던 값을 올바르게 받을 수 있었다.