소켓통신과 OpenCV, SwiftUI 을 사용하여 이미지를 전송받을 수 있습니다
소켓 통신
영상를 전송받기 위해서는 소켓 통신을 진행해야 합니다.
소켓 통신은 전송계층인 TCP 또는 UDP 둘 중 하나를 선택하여 소켓을 만든 후 IP와 Port 번호를 설정하여 바인딩해줘야 합니다.
영상를 어떻게 전송받을 수 있는지
영상를 어떻게 전송받을 수 있는지 생각이 들 수 있습니다.
영상은 사실은 수백, 수천개 그 이상의 이미지들이 모여서 만들어진 자연스러운 여러 이미지의 조합입니다.
이때 이미지의 원본 형태는 컴퓨터에서 바이너리 데이터(0과 1의 조합)으로 저장됩니다. 이 바이너리 데이터는 이미지의 픽셀 정보와 메타데이터 등을 포함하고 있으며 이런 정보를 통해서 AI가 데이터를 학습하고 예측할 수 있습니다.(TMI)
그렇기 때문에 해당 바이너리 데이터를 JSON 형식으로 전송하기 위해서는 base64 인코딩 방식으로 텍스트 문자열로 변환해야 합니다.
base64는 8비트 바이너리 데이터를 ASCII 문자로만 구성된 문자열로 변환하여, 테스트 기반의 데이터 전송이나 저장할 수 있게 합니다.
영상 촬영 및 소켓으로 전송하는 서버 py
파이썬으로 웹소켓 통신을 하기 위해서는 websockets 라이브러리를 설치해야 합니다.
스레드 병렬 처리 작업을 하기 위해서는 asyncio 라이브러리를 설치해야 합니다.
카메라를 사용하기 위해서는 cv2 라이브러리를 설치해야 합니다.
이미지 데이터를 인코딩을 하기 위해서는 base64 를 사용해야 합니다.
인코딩된 이미지 데이터를 전송하기 위해서는 json 을 설치해야 합니다.
import websockets # 없으면 pip install websockets
import asyncio
import cv2 # 없으면 pip install opencv-python
import json
import base64
이후 해당 웹소켓 라이브러리로 간단하게 웹소켓 서버를 설정합니다.
async def start_server():
async with websockets.serve(video_stream, "0.0.0.0", 50001):
await asyncio.Future() # 서버가 종료될 때까지 유지
- serve 는 서버를 비동기적으로 시작하는 코드로 0.0.0.0:50001 에 클라이언트가 연결되면 video_stream 함수를 수행합니다.
- 서버 바인딩 주소를 0.0.0.0 으로 변경해야 외부에서의 접근을 허용할 수 있습니다.
- await asyncio.Future(): 이 코드는 무기한으로 실행을 지속하기 위해 사용되는 구문입니다.
- 이 코드를 사용하지 않으면 start_server 함수는 즉시 종료되고, 서버도 함께 종료됩니다.
- 이 코드를 사용한다면 함수 내에서 웹소켓 서버를 실행한 후, 서버가 종료되지 않고 계속해서 실행되도록 유지하기 위해 이 코드를 실행합니다.
asyncio란?
asyncio란 asyncio는 async/await 구문을 사용하여 동시성 코드를 작성할 수 있게 해주는 모듈로, asyncio를 사용하면 단일 스레드 작업을 병렬로 처리할 수 있습니다.
이후 웹 소켓 서버에 클라이언트가 접속하게 될 시 video_stream 이 실행됩니다.
async def video_stream(websocket): # 새로운 연결이 되었을때 실행
print(f"새로운 연결: {websocket.remote_address}")
camera = cv2.VideoCapture(0) # USB 카메라 초기화 (기본 장치 0번)
if not camera.isOpened():
return
try:
while True:
ret, frame = camera.read()
if not ret: # 프레임을 읽을 수 없을 때
break
_, encoded_image = cv2.imencode('.jpg', frame)
frame_data = encoded_image.tobytes()
frame_base64 = base64.b64encode(frame_data).decode('utf-8')
message = json.dumps({"frame": frame_base64})
await websocket.send(message)
except Exception as e:
print("예외 발생:", e)
finally:
camera.release() # 카메라 연결 종료
- 서버에 클라이언트가 접속하면 실행되는 함수입니다.
- cv2.VideoCapture 를 사용하여 USB 카메라를 설정합니다.
- camera.read() 설정이 완료되면 무한루프를 통해 카메라로부터 프레임을 읽어들입니다.
- cv2 의 imencode 를 통해 이미지 프레임을 .jpg 로 인코딩해줍니다.
- 인코딩이 완료된 이미지를 tobytes() 메서드로 바이트 단위로 바꿔줍니다.
- 바이트 단위데이터를 base64 를 사용하여 ASCII 문자로만 구성된 문자열로 변환 후 UTF-8 로 디코딩을 수행합니다.
- UTF-8 이란 ASCII 문자열과 호환되며 바이트 문자열을 사람이 읽을 수 있는 텍스트 데이터로 저장합니다.
- 이후 json 으로 보낼 UTF-8 문자열 데이터를 전송해줍니다.
추가 실험해본 결과
utf-8 적용하기 전
print(f"{base64.b64encode(frame_data)}")
# b'/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAIBAQEBAQIBAQECAgI......
utf-8 적용한 후
print(f"{base64.b64encode(frame_data).decode('utf-8')}")
# /9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAIBAQEBAQIBAQECAg..........
SwiftUI 코드로 영상 데이터 받기
Swift 에서 소켓 통신을 위해서는 URLSessionWebSocketTask 클래스를 사용해야 합니다. 코드는 비교적 간단합니다.
import SwiftUI
class VideoStreamReceiver: ObservableObject {
@Published var frameImage: UIImage?
private var webSocketTask: URLSessionWebSocketTask?
private let url = URL(string: "ws://127.0.0.1:50001")!
func startConnection() {
let session = URLSession(configuration: .default)
webSocketTask = session.webSocketTask(with: url)
webSocketTask?.resume()
receiveMessage()
}
private func receiveMessage() {
webSocketTask?.receive { [weak self] result in
switch result {
case .failure(let error):
print("Error receiving message: \(error)")
case .success(let message):
switch message {
case .string(let text):
self?.handleFrameMessage(text)
default:
print("Received unknown message type")
}
}
self?.receiveMessage()
}
}
private func handleFrameMessage(_ text: String) {
guard let data = text.data(using: .utf8),
let json = try? JSONSerialization.jsonObject(with: data) as? [String: String],
let base64String = json["frame"],
let imageData = Data(base64Encoded: base64String),
let image = UIImage(data: imageData) else {
print("Failed to decode image data")
return
}
DispatchQueue.main.async {
self.frameImage = image
}
}
func stopConnection() {
webSocketTask?.cancel()
}
}
- 연결을 하기 위해서 URL 을 웹 소켓 작업 세션에 추가해준 다음 resume() 을 통해 소켓 통신을 시작합니다.
- receiveMessage 에서는 receive 를 사용하여 utf-8 로 인코딩된 string 데이터를 처리할 수 있는 함수로 전달합니다.
- handleFrameMessage는 string 데이터를 받기 위해 JSONSerialization 을 사용합니다.
- JSONSerialization은 데이터 모델을 만들지 않고 바로 JSON 으로 인식해 사용할 수 있게끔 만들어줍니다.
- 파이썬 코드에서 "frame" key에 프레임 utf-8 값을 value 로 설정하여 전송하므로 위와 같이 설정합니다.
- 받은 이미지 데이터를 UIImage 로 저장합니다.
이후 다음과 같이 UI 코드를 작성하면 됩니다.
struct ContentView: View {
@StateObject private var receiver = VideoStreamReceiver()
var body: some View {
VStack {
if let image = receiver.frameImage {
Image(uiImage: image)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 300, height: 200)
} else {
Text("Waiting for video feed...")
}
}
.onAppear {
receiver.startConnection()
}
.onDisappear {
receiver.stopConnection()
}
}
}
'SwiftUI' 카테고리의 다른 글
Swift - Core Bluetooth 블루투스 통신 (0) | 2024.11.19 |
---|---|
SwiftUI - 접근제어자 (0) | 2024.11.17 |
SwiftUI - 소켓 통신 (5) | 2024.11.12 |
SwiftUI - 커스텀 카메라 (0) | 2024.11.11 |
WidgetKit - UserDefault 로 위젯과 앱 데이터 공유하기 (0) | 2024.10.23 |